- 修改总 Rating:只写 playerRating 与 userRating.rating,钳 0~99999, 每首歌各自的 ratingList 原样带回 - 添加舞里程(UserDetail.point):完整 int32 区间,支持累加与直接设定, 允许负值即扣减 - 一键跑图:isClear/isComplete 在客户端是从 distance 派生的,单发 flag 会被 冲掉,故把 distance 推过 End 里程针;unlockFlag 语义是反的 - 万花筒专区:发现门与获取钥匙落在 userKaleidxScopeList 同一行的两个布尔位, 钥匙不走 userItemList;上行是整行替换,所以先读回原行再叠加 - 旅行伙伴:修正误导性的示例 ID 1001(合法是三位数,表里没有的 ID 服务器照收 但客户端静默跳过),编组槽位可设定等级,上传后回读复查以区分失败原因 - 补 MIT 许可证
157 lines
5.6 KiB
Dart
157 lines
5.6 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,
|
||
}) => 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,
|
||
lastPlayDate: lastPlayDate,
|
||
isInfoWatched: isInfoWatched,
|
||
);
|
||
|
||
/// 服务器上还没有这扇门时的新行。`isGateFound` 恒为 true——不存在「只给钥匙
|
||
/// 不给门」这种可用状态(见类注释的状态机),新行其余字段取 struct 默认值。
|
||
static UserKaleidxScopeBean discovered(int gateId, {required bool giveKey}) =>
|
||
UserKaleidxScopeBean(
|
||
gateId: gateId,
|
||
isGateFound: true,
|
||
isKeyFound: giveKey,
|
||
);
|
||
|
||
/// 在服务器原行上叠加本次动作。**必须**走这里而不是新建一行:整行替换会把
|
||
/// best 成绩、playCount、clearDate 这些字段清零。
|
||
UserKaleidxScopeBean withActions({
|
||
required bool discover,
|
||
required bool giveKey,
|
||
}) => copyWith(
|
||
// 给钥匙连带发现门,否则这行在客户端是隐形的。
|
||
isGateFound: isGateFound || discover || giveKey,
|
||
isKeyFound: isKeyFound || giveKey,
|
||
);
|
||
|
||
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();
|
||
}
|
||
}
|