通关态与前两个动作一样落在 userKaleidxScopeList 同一行,是 isClear。 用三态下拉(不改动 / 已通关 / 未通关)而不是勾选框,因为通关可以往回改。 - 标记通关时补 clearDate,格式按客户端 TimeManager.GetDateString 的 yyyy-MM-dd HH:mm:ss.f;已有首次通关日期不覆盖,撤销通关时一起清空 - 上行是整行替换,只叠加要改的位,playCount / best 成绩 / 日期保持原值 - 通关态只在 found && key 的前提下才显示得出来,缺钥匙时给一条短提示 - 校验放宽:三个动作任选其一即可执行
177 lines
6.3 KiB
Dart
177 lines
6.3 KiB
Dart
/// `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();
|
||
}
|
||
}
|