feat: 风险页新增数值修改、一键跑图与万花筒专区

- 修改总 Rating:只写 playerRating 与 userRating.rating,钳 0~99999,
  每首歌各自的 ratingList 原样带回
- 添加舞里程(UserDetail.point):完整 int32 区间,支持累加与直接设定,
  允许负值即扣减
- 一键跑图:isClear/isComplete 在客户端是从 distance 派生的,单发 flag 会被
  冲掉,故把 distance 推过 End 里程针;unlockFlag 语义是反的
- 万花筒专区:发现门与获取钥匙落在 userKaleidxScopeList 同一行的两个布尔位,
  钥匙不走 userItemList;上行是整行替换,所以先读回原行再叠加
- 旅行伙伴:修正误导性的示例 ID 1001(合法是三位数,表里没有的 ID 服务器照收
  但客户端静默跳过),编组槽位可设定等级,上传后回读复查以区分失败原因
- 补 MIT 许可证
This commit is contained in:
Cookiues committed 2026-09-27 01:49:55 +08:00
1 parent 329845f228
commit 0808e367f7
15 files changed
+3597 -42

No files matched your search

+156
View File
@@ -0,0 +1,156 @@
/// `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();
}
}