Files
Ichino/lib/models/user_kaleidx_scope.dart
T
Cookiues 6438fb2171 feat: 万花筒专区可设置门的通关状态
通关态与前两个动作一样落在 userKaleidxScopeList 同一行,是 isClear。
用三态下拉(不改动 / 已通关 / 未通关)而不是勾选框,因为通关可以往回改。

- 标记通关时补 clearDate,格式按客户端 TimeManager.GetDateString 的
  yyyy-MM-dd HH:mm:ss.f;已有首次通关日期不覆盖,撤销通关时一起清空
- 上行是整行替换,只叠加要改的位,playCount / best 成绩 / 日期保持原值
- 通关态只在 found && key 的前提下才显示得出来,缺钥匙时给一条短提示
- 校验放宽:三个动作任选其一即可执行
2026-10-01 11:04:18 +08:00

177 lines
6.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// `Net.VO.Mai2.UserKaleidxScope`(万花筒 / 宿命之门),走
/// `upsertUserAll.userKaleidxScopeList`,主键是 `gateId`。
///
/// 依据反编译客户端:
/// - 「发现门」与「获取钥匙」都在这同一张表里,分别是 `isGateFound` 与
/// `isKeyFound`。钥匙**不**走 `userItemList`——`ExportUserItems` 从不输出
/// itemKind 15,下行也没有 `GetUserItem(KaleidxScopeKey)` 这一步。
/// - 状态机在 `KaleidxScopeGateListController.cs:144-159`,四个组合里:
/// `!found && key` -> AnimState.None(**隐形**,不是锁着的门)
/// ` found && !key` -> Close(可见、未解锁)
/// ` found && key && !clear` -> Open(可挑战)
/// ` found && key && clear` -> Clear
/// 所以只给钥匙不给门等于什么都没给。
class UserKaleidxScopeBean {
final int gateId;
final bool isGateFound;
final bool isKeyFound;
final bool isClear;
final int totalRestLife;
final int totalAchievement;
/// 原样拼写是 `Deluxscore`(不是 `DeluxeScore`),改名服务器就认不出了。
final int totalDeluxscore;
final int bestAchievement;
final int bestDeluxscore;
final String bestAchievementDate;
final String bestDeluxscoreDate;
final int playCount;
final String clearDate;
final String lastPlayDate;
final bool isInfoWatched;
const UserKaleidxScopeBean({
required this.gateId,
this.isGateFound = false,
this.isKeyFound = false,
this.isClear = false,
this.totalRestLife = 0,
this.totalAchievement = 0,
this.totalDeluxscore = 0,
this.bestAchievement = 0,
this.bestDeluxscore = 0,
this.bestAchievementDate = '',
this.bestDeluxscoreDate = '',
this.playCount = 0,
this.clearDate = '',
this.lastPlayDate = '',
this.isInfoWatched = false,
});
/// wire 上的完整 15 个字段。发出去的是整行替换,所以没列在这里的字段
/// 必须从服务器原值带回来,否则会被清零。
Map<String, dynamic> toWireJson() => {
'gateId': gateId,
'isGateFound': isGateFound,
'isKeyFound': isKeyFound,
'isClear': isClear,
'totalRestLife': totalRestLife,
'totalAchievement': totalAchievement,
'totalDeluxscore': totalDeluxscore,
'bestAchievement': bestAchievement,
'bestDeluxscore': bestDeluxscore,
'bestAchievementDate': bestAchievementDate,
'bestDeluxscoreDate': bestDeluxscoreDate,
'playCount': playCount,
'clearDate': clearDate,
'lastPlayDate': lastPlayDate,
'isInfoWatched': isInfoWatched,
};
UserKaleidxScopeBean copyWith({
bool? isGateFound,
bool? isKeyFound,
bool? isClear,
String? clearDate,
}) => UserKaleidxScopeBean(
gateId: gateId,
isGateFound: isGateFound ?? this.isGateFound,
isKeyFound: isKeyFound ?? this.isKeyFound,
isClear: isClear ?? this.isClear,
totalRestLife: totalRestLife,
totalAchievement: totalAchievement,
totalDeluxscore: totalDeluxscore,
bestAchievement: bestAchievement,
bestDeluxscore: bestDeluxscore,
bestAchievementDate: bestAchievementDate,
bestDeluxscoreDate: bestDeluxscoreDate,
playCount: playCount,
clearDate: clearDate ?? this.clearDate,
lastPlayDate: lastPlayDate,
isInfoWatched: isInfoWatched,
);
/// 服务器上还没有这扇门时的新行。`isGateFound` 恒为 true——不存在「只给钥匙
/// 不给门」这种可用状态(见类注释的状态机),新行其余字段取 struct 默认值。
static UserKaleidxScopeBean discovered(
int gateId, {
required bool giveKey,
bool? clear,
String timestamp = '',
}) => UserKaleidxScopeBean(
gateId: gateId,
isGateFound: true,
isKeyFound: giveKey,
isClear: clear ?? false,
clearDate: (clear ?? false) ? timestamp : '',
);
/// 在服务器原行上叠加本次动作。**必须**走这里而不是新建一行:整行替换会把
/// best 成绩、playCount、clearDate 这些字段清零。
///
/// [clear] 传 null 表示不改动通关状态。给钥匙必须连带发现门,否则这行在
/// 客户端是隐形的。
UserKaleidxScopeBean withActions({
required bool discover,
required bool giveKey,
bool? clear,
String timestamp = '',
}) => copyWith(
isGateFound: isGateFound || discover || giveKey,
isKeyFound: isKeyFound || giveKey,
isClear: clear,
// 首次通关日期只在刚转成通关时落一个时间戳(客户端就是这么写的:
// `clearDate = TimeManager.GetNowDateString()`);撤销通关时一起清空,
// 留着一个日期比缺日期更容易让人误读。
clearDate: clear == null
? null
: (clear
? (clearDate.isNotEmpty ? clearDate : timestamp)
: ''),
);
static int _toInt(dynamic v) {
if (v is int) return v;
if (v is num) return v.toInt();
if (v is String) return int.tryParse(v) ?? 0;
return 0;
}
static bool _toBool(dynamic v) {
if (v is bool) return v;
if (v is num) return v != 0;
if (v is String) return v == '1' || v.toLowerCase() == 'true';
return false;
}
factory UserKaleidxScopeBean.fromJson(Map<String, dynamic> json) =>
UserKaleidxScopeBean(
gateId: _toInt(json['gateId']),
isGateFound: _toBool(json['isGateFound']),
isKeyFound: _toBool(json['isKeyFound']),
isClear: _toBool(json['isClear']),
totalRestLife: _toInt(json['totalRestLife']),
totalAchievement: _toInt(json['totalAchievement']),
totalDeluxscore: _toInt(json['totalDeluxscore']),
bestAchievement: _toInt(json['bestAchievement']),
bestDeluxscore: _toInt(json['bestDeluxscore']),
bestAchievementDate: json['bestAchievementDate']?.toString() ?? '',
bestDeluxscoreDate: json['bestDeluxscoreDate']?.toString() ?? '',
playCount: _toInt(json['playCount']),
clearDate: json['clearDate']?.toString() ?? '',
lastPlayDate: json['lastPlayDate']?.toString() ?? '',
isInfoWatched: _toBool(json['isInfoWatched']),
);
static List<UserKaleidxScopeBean> listFromResponse(
Map<String, dynamic> json,
) {
final raw = json['userKaleidxScopeList'];
if (raw is! List) return const [];
return raw
.whereType<Map<String, dynamic>>()
.map(UserKaleidxScopeBean.fromJson)
.toList();
}
}