diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index a4ecddc..3f5d6f4 100644 --- a/3DTank/docs/KtRebuild_Plan.md +++ b/3DTank/docs/KtRebuild_Plan.md @@ -143,7 +143,7 @@ `names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。 - 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。 - 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。 - - 已迁移切片 **82/89**: + - 已迁移切片 **83/89**: - `inviteState` → `aD` / `pD.er5`(`InviteReducer.kt`) - `loginByPassword` → `gD` / `MD.cr6`(`LoginReducer.kt`,含 `SetLogin`/`SetPassword` 把 status 打回 `NONE` 的原语义) - `twitchState` → `g9` / `y9.gta`(`TwitchReducer.kt`;`SetAccount` 是 Twitch 账号,不是 UID) @@ -413,12 +413,17 @@ 用户加入时同时写 onlineUsers / uids / ranks / clanTags / usersPremium / teams / stats; stats 值是生成物 `UserStat`(deaths / killScore / kills / score / user); 坦克信息与抗性复用 `TankInfoCC` / `TankResistance`;`sP`(旧队伍枚举迁移)按 no-op 收口。 + - `challenges` → `CZ` / `nh.hsj`(`ChallengesReducer.kt`):4 字段,9 个分支。 + `stars` / `battlePassPackages` 整体替换;`SetChallenge` 按 `version` 建索引、`RemoveChallenge` 按 `version` 删; + `SetBattlePass(flavor)` 追加一个 `Challenge` 战令条目,两个「已领取索引」动作按 flavor 命中后 copy; + `SetRewardForDialog` / `LZ` 依赖 `FZ`/`GZ` 构造与外部刷新,纯 reducer 里按 no-op 收口。 + `Challenge` 直接复用生成物模型(9 字段)。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 81 个有 reducer 的切片, - `launchParams` 与其余 **7 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 82/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **902 项全绿**,含 89 槽位完整性、81 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 82 个有 reducer 的切片, + `launchParams` 与其余 **6 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 83/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **913 项全绿**,含 89 槽位完整性、82 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); diff --git a/3DTank/names/root_wiring.json b/3DTank/names/root_wiring.json index 8f20e6e..108765e 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T13:07:20.497Z", + "generatedAt": "2026-10-24T13:23:52.900Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -88,7 +88,8 @@ "remoteUserDataStorage", "matchmaking", "battlesSelect", - "battleUsers" + "battleUsers", + "challenges" ], "AnonymousActions": [ { @@ -1482,6 +1483,12 @@ "Name": "MigrateBattleUserTeams", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "旧队伍枚举迁移(`qm/bm/em` 映射),离线重建按 no-op 收口" + }, + { + "Local": "LZ", + "Name": "RefreshBattlePasses", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "战令条目批量刷新(`GZ`),离线重建按 no-op 收口" } ], "Markers": [ @@ -1761,7 +1768,7 @@ "Reducer": "nh.hsj", "Delegate": "nh.hsj(t, n.fpv_1)", "Actions": [ - "?LZ", + "RefreshBattlePasses", "RemoveChallenge", "SetBattlePass", "SetBattlePassPackages", @@ -1772,7 +1779,7 @@ "UpdateStars" ], "Default": "new CZ()", - "Migrated": false + "Migrated": true }, { "Field": "loginByPassword", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index a905e03..38f6be3 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -337,6 +337,8 @@ const AnonymousActionNames = new Map([ ['qP', 'RemoveOnlineUser'], ['lP', 'SetUserPremium'], ['sP', 'MigrateBattleUserTeams'], + // challenges 切片(CZ / nh.hsj):LZ 没有 toString。 + ['LZ', 'RefreshBattlePasses'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -823,6 +825,7 @@ const Migrated = { matchmaking: { Type: 'MatchmakingStateSlice', Reduce: 'ReduceMatchmaking' }, battlesSelect: { Type: 'BattlesSelectStateSlice', Reduce: 'ReduceBattlesSelect' }, battleUsers: { Type: 'BattleUsersStateSlice', Reduce: 'ReduceBattleUsers' }, + challenges: { Type: 'ChallengesStateSlice', Reduce: 'ReduceChallenges' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -1246,6 +1249,7 @@ const AppSettingsAnonymousEffects = { qP: 'onlineUsers -= userId(`n.hqr(Jg(n.iqa_1, t.gqs_1))`)', lP: 'usersPremium[userId](`cg(n.mqa_1, Vd(t.iqs_1, t.jqs_1))`)', sP: '旧队伍枚举迁移(`qm/bm/em` 映射),离线重建按 no-op 收口', + LZ: '战令条目批量刷新(`GZ`),离线重建按 no-op 收口', }; fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ChallengesReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ChallengesReducer.kt new file mode 100644 index 0000000..3098456 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ChallengesReducer.kt @@ -0,0 +1,94 @@ +// ChallengesReducer.kt —— 挑战 / 战令切片(对应 module_10032 的 CZ 状态 + nh.hsj) +// +// 证据链: +// 状态类 CZ:4 个字段,toString 名 "Challenges(…)"。 +// 默认:stars=emptyMap()、challenge=emptyMap()、battlePassPackages=emptyMap()、battlePasses=emptySet()。 +// 根委派:`nh.hsj(t, n.fpv_1)`。 +// reducer(9 个实例臂): +// AZ(UpdateStars) → stars(整体替换) +// BZ(SetChallenge) → challenge[challenge.version] +// UZ(RemoveChallenge) → challenge -= challengeVersion +// OZ(SetBattlePass) → battlePasses += 该 flavor 的战令条目 +// Jrt(SetRewardForDialog) → 战令条目的弹窗奖励(原 `FZ/GZ` 构造,离线重建按 no-op) +// LZ(无 toString) → 战令条目批量刷新(`GZ`),离线重建按 no-op +// DZ(SetBattlePassPackages) → battlePassPackages(整体替换) +// NZ(SetTakenFreeRewardIndex) → 命中 flavor 的战令条目写 takenFreeRewardIndex +// MZ(SetTakenBattlePassRewardIndex)→ 命中 flavor 的战令条目写 takenBattlePassRewardIndex +// +// `Challenge` 直接复用生成物模型(version / endTime / tiers / battlePassFlavor / +// mainPrizeImage / background / tierReward / takenFreeRewardIndex / takenBattlePassRewardIndex)。 +package App.State.Reducers + +import App.State.Challenge +import App.State.RemoveChallenge +import App.State.SetBattlePass +import App.State.SetBattlePassPackages +import App.State.SetChallenge +import App.State.SetRewardForDialog +import App.State.SetTakenBattlePassRewardIndex +import App.State.SetTakenFreeRewardIndex +import App.State.UpdateStars + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `LZ`:战令条目批量刷新(离线重建按 no-op)。 */ +object RefreshBattlePasses + +/* ------------------------------------------------------------------ 状态 */ + +data class ChallengesStateSlice( + val Stars: Map = emptyMap(), + val Challenge: Map = emptyMap(), + val BattlePassPackages: Map = emptyMap(), + val BattlePasses: Set = emptySet(), +) + +/* ------------------------------------------------------------------ reducer */ + +/** 原 `hsi`:按 flavor 造一个新的战令条目。 */ +private fun NewBattlePass(Flavor: Any?): Challenge = Challenge( + version = Flavor, + endTime = null, + tiers = emptyList(), + battlePassFlavor = (Flavor as? Int) ?: 0, + mainPrizeImage = 0L, + background = 0L, + tierReward = null, + takenFreeRewardIndex = 0, + takenBattlePassRewardIndex = 0, +) + +/** 原 `esi`:按 flavor 命中后替换战令条目。 */ +private fun MapBattlePass( + Passes: Set, + Flavor: Any?, + Transform: (Challenge) -> Challenge, +): Set = Passes.map { if (it.version == Flavor) Transform(it) else it }.toSet() + +fun ReduceChallenges( + State0: ChallengesStateSlice, + Action: Any, +): ChallengesStateSlice = when (Action) { + is UpdateStars -> State0.copy(Stars = Action.stars) + is SetChallenge -> State0.copy( + Challenge = State0.Challenge + (Action.challenge.version to Action.challenge), + ) + is RemoveChallenge -> State0.copy(Challenge = State0.Challenge - Action.challengeVersion) + is SetBattlePass -> State0.copy( + BattlePasses = State0.BattlePasses + NewBattlePass(Action.battlePassFlavor), + ) + is SetRewardForDialog -> State0 + is RefreshBattlePasses -> State0 + is SetBattlePassPackages -> State0.copy(BattlePassPackages = Action.battlePassPackages) + is SetTakenFreeRewardIndex -> State0.copy( + BattlePasses = MapBattlePass(State0.BattlePasses, Action.flavor) { + it.copy(takenFreeRewardIndex = (Action.index as? Int) ?: it.takenFreeRewardIndex) + }, + ) + is SetTakenBattlePassRewardIndex -> State0.copy( + BattlePasses = MapBattlePass(State0.BattlePasses, Action.flavor) { + it.copy(takenBattlePassRewardIndex = (Action.index as? Int) ?: it.takenBattlePassRewardIndex) + }, + ) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt index 7aefebf..13a557a 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt @@ -1,7 +1,7 @@ // RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6) // // 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。 -// 这里采用同一形状。已迁移的 82 个切片里,81 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 83 个切片里,82 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -84,9 +84,10 @@ // matchmaking → ReduceMatchmaking(TH / JH().qrx,根委派 `JH().qrx(t, n.cpw_1)`) // battlesSelect → ReduceBattlesSelect(fA / Mi.vqk,根委派 `Mi.vqk(t, n.apw_1)`) // battleUsers → ReduceBattleUsers(uP / sr.hqs,根委派 `sr.hqs(t, n.hpw_1)`) +// challenges → ReduceChallenges(CZ / nh.hsj,根委派 `nh.hsj(t, n.fpv_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 7 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 6 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -107,6 +108,7 @@ import App.State.Reducers.ReduceBattleUsers import App.State.Reducers.ReduceBattleView import App.State.Reducers.ReduceBattleInvite import App.State.Reducers.ReduceCaptcha +import App.State.Reducers.ReduceChallenges import App.State.Reducers.ReduceClanUserInfo import App.State.Reducers.ReduceClanChat import App.State.Reducers.ReduceContextTutorial @@ -267,6 +269,7 @@ object RootReducer { Matchmaking = ReduceMatchmaking(State0.Matchmaking, Action), BattlesSelect = ReduceBattlesSelect(State0.BattlesSelect, Action), BattleUsers = ReduceBattleUsers(State0.BattleUsers, Action), + Challenges = ReduceChallenges(State0.Challenges, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index fabd217..f1f4a82 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -21,6 +21,7 @@ import App.State.Reducers.BattleUsersStateSlice import App.State.Reducers.BattleViewStateSlice import App.State.Reducers.BattlesSelectStateSlice import App.State.Reducers.CaptchaStateSlice +import App.State.Reducers.ChallengesStateSlice import App.State.Reducers.ClanChatStateSlice import App.State.Reducers.ClanUserInfoStateSlice import App.State.Reducers.ContextTutorialStateSlice @@ -104,8 +105,8 @@ data class RootState( val Garage: GarageStateSlice = GarageStateSlice(), /** 原 garagePreview(IG / fs.eru)。已迁移。 */ val GaragePreview: GaragePreviewStateSlice = GaragePreviewStateSlice(), - /** 原 challenges(CZ / nh.hsj)。未迁移:原样透传。 */ - val Challenges: Any? = null, + /** 原 challenges(CZ / nh.hsj)。已迁移。 */ + val Challenges: ChallengesStateSlice = ChallengesStateSlice(), /** 原 loginByPassword(gD / nu.cr6)。已迁移。 */ val LoginByPassword: LoginByPasswordStateSlice = LoginByPasswordStateSlice(), /** 原 register(Cit / cp.stg)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index d2e4e2d..16bcd29 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -21,7 +21,7 @@ object RootWiring { SliceWiring("battleTutorial", "cpv_1", "rE", "Q.sq8", "Q.sq8(t, n.cpv_1)", listOf("MarkMovementStarted", "ResetBattleTutorial", "SetFragsCount", "ShowMessage")), SliceWiring("garage", "dpv_1", "UB", "hF", "hF(t, n)", listOf("AddGarageItemCount", "AddNewItem", "AddOverdriveDescription", "AddRelatedBazooka", "AddRelatedResistanceModule", "AddToFavoriteItem", "BuyProtectionSlot", "ClearWaitServerItemMount", "GarageAddItemToDepot", "GarageApplyResistanceMount", "GarageBuyDevice", "GarageDeviceTrySetInfinityLifetime", "GarageDevicesLoadedAction", "GarageLoaded", "GarageMountItem", "GarageMountItems", "GarageRemoveDevice", "GarageRemoveDiscountForDevice", "GarageRemoveItemFromDepot", "GarageRemoveItemFromStore", "GarageResistanceUnMount", "GarageSelectAlteration", "GarageSelectItem", "GarageSelectShotSkin", "GarageSelectSkin", "GarageUnmountItem", "GarageUpdateItemCount", "GarageUpgradeItemAction", "GarageUpgradeItemConfirm", "GarageUpgradeItemCoolDownFinished", "GarageUpgradeItemCoolDownStarted", "GoTo", "HideVideoInDescription", "InitState", "RemoveFromFavoriteItem", "RemoveFromSale", "RemoveNewItem", "RemoveWaitServerItemMount", "ResetGarage", "ResetMountDelay", "SelectGarageCategory", "SetCooldownReduceEndTime", "SetGarageNotLoaded", "SetGarageObject", "SetGarageSkinsAction", "SetItemRarity", "SetItemsOnDepot", "SetItemsOnMarket", "SetMountedItems", "SetMountedShotSkinForItems", "SetMountedSkinForItems", "SetNewItems", "SetResistanceReadyToMount", "SetRestrictionMount", "ShowGarageDiscountNotification", "StartCooldownReduce", "SwitchingRowsScroll", "UpdateDiscount")), SliceWiring("garagePreview", "epv_1", "IG", "fs.eru", "fs.eru(t, n.epv_1)", listOf("AddRequestedItem", "CaptureScreenshot", "CaptureVideo", "ClearDroneData", "ClearGrenadeData", "ClearHasGrenades", "ClearHullData", "ClearWeaponData", "InitGarageResources", "ResetItem3dModelActionCache", "SetCaptureActions", "SetDrone", "SetExpectedItemId", "SetGrenadeData", "SetHasGrenades", "SetHullData", "SetMountedItemsProperties", "SetPaint", "SetWeaponData", "ShowRequest", "UpdatePreviewSettings", "UpdateViewStyle")), - SliceWiring("challenges", "fpv_1", "CZ", "nh.hsj", "nh.hsj(t, n.fpv_1)", listOf("?LZ", "RemoveChallenge", "SetBattlePass", "SetBattlePassPackages", "SetChallenge", "SetRewardForDialog", "SetTakenBattlePassRewardIndex", "SetTakenFreeRewardIndex", "UpdateStars")), + SliceWiring("challenges", "fpv_1", "CZ", "nh.hsj", "nh.hsj(t, n.fpv_1)", listOf("RefreshBattlePasses", "RemoveChallenge", "SetBattlePass", "SetBattlePassPackages", "SetChallenge", "SetRewardForDialog", "SetTakenBattlePassRewardIndex", "SetTakenFreeRewardIndex", "UpdateStars")), SliceWiring("loginByPassword", "gpv_1", "gD", "nu.cr6", "nu.cr6(t, n.gpv_1)", listOf("?ND", "?PD", "?RD", "IncorrectLogin2FA", "RequestsNumber2FA", "SetLogin", "SetPassword", "SetRememberMe", "SetStatus")), SliceWiring("register", "hpv_1", "Cit", "cp.stg", "cp.stg(t, n.hpv_1)", listOf("DisableRegister", "EnteredUidIsBusy", "ForceSetUid", "ReceiveOAuthUri", "RegisterFailed", "RegisterPasswordCorrect", "RegisterPasswordIncorrect", "ResetAuthenticationForm", "ResetRegisterStatus", "ResetUidStatus", "SetOpenedPassword", "SetPassword", "SetPasswordRestrictions", "SetUid", "SocialNetworkAvailability", "SocialNetworkClientIds", "UidIsFree", "UidIsIncorrect", "UidTooShort")), SliceWiring("migrationState", "ipv_1", "\$it", "Uv.rtg", "Uv.rtg(t, n.ipv_1)", listOf("EnteredUidIsBusy", "ForceSetUid", "RegisterFailed", "RegisterPasswordIncorrect", "ResetAuthenticationForm", "ResetUidStatus", "SetPassword", "SetUid", "UidIsFree", "UidIsIncorrect", "UidTooShort")), @@ -106,7 +106,7 @@ object RootWiring { ) /** 已迁移(强类型;launchParams 为透传,不调用 reducer)的字段。 */ - val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "settings", "accountSettings", "proBattle", "language", "announcement", "launchParams", "news", "notifications", "quests", "lobbyLoader", "purchaseDialogState", "promoCode", "supportUrl", "rulesUpdated", "notificationByCountry", "availabilityByRank", "icons", "questCategories", "mainQuest", "securitySettings", "referralEvent", "tipsState", "rankUpDialogState", "battleResult", "captchaState", "modalScreen", "droppableGold", "eventGoods", "creatorTagState", "rewardedVideosState", "passwordChangeState", "system", "contextTutorial", "battleTutorial", "spectator", "twoStepVerificationState", "antiAddiction", "moderation", "migrationState", "register", "userReportsState", "friends", "suppliesState", "clanUserInfo", "battleInviteState", "partner", "userRenameState", "battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial", "clanChatState", "battleSearchState", "battleStatistics", "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage", "maps", "battleChat", "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers") + val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "settings", "accountSettings", "proBattle", "language", "announcement", "launchParams", "news", "notifications", "quests", "lobbyLoader", "purchaseDialogState", "promoCode", "supportUrl", "rulesUpdated", "notificationByCountry", "availabilityByRank", "icons", "questCategories", "mainQuest", "securitySettings", "referralEvent", "tipsState", "rankUpDialogState", "battleResult", "captchaState", "modalScreen", "droppableGold", "eventGoods", "creatorTagState", "rewardedVideosState", "passwordChangeState", "system", "contextTutorial", "battleTutorial", "spectator", "twoStepVerificationState", "antiAddiction", "moderation", "migrationState", "register", "userReportsState", "friends", "suppliesState", "clanUserInfo", "battleInviteState", "partner", "userRenameState", "battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial", "clanChatState", "battleSearchState", "battleStatistics", "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage", "maps", "battleChat", "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ val Unmigrated: List get() = All.filter { it.Field !in Migrated } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt index 12e6bd9..b2bab8f 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt @@ -19,7 +19,7 @@ // System / ContextTutorial / BattleTutorial / Spectator / TwoStepVerification / AntiAddiction / // Moderation / MigrationState / Register / UserReports / // Friends / Supplies / ClanUserInfo / BattleInvite / Partner / UserRename / -// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder / BattleMessage / Maps / BattleChat / RemoteUserDataStorage / Matchmaking / BattlesSelect / BattleUsers; +// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder / BattleMessage / Maps / BattleChat / RemoteUserDataStorage / Matchmaking / BattlesSelect / BattleUsers / Challenges; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -814,6 +814,18 @@ import App.State.Reducers.ResetBattleUsers import App.State.Reducers.SetUserPremium import App.State.RemoveTankInfo import App.State.UserStat +import App.State.Challenge +import App.State.RemoveChallenge +import App.State.SetBattlePass +import App.State.SetBattlePassPackages +import App.State.SetChallenge +import App.State.SetRewardForDialog +import App.State.SetTakenBattlePassRewardIndex +import App.State.SetTakenFreeRewardIndex +import App.State.UpdateStars +import App.State.Reducers.ChallengesStateSlice +import App.State.Reducers.ReduceChallenges +import App.State.Reducers.RefreshBattlePasses import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -1241,7 +1253,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 82 个已迁移切片(81 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 83 个已迁移切片(82 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1305,7 +1317,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 82 个已迁移字段,其余 7 个待迁", + "根:迁移表 = 83 个已迁移字段,其余 6 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1329,9 +1341,9 @@ object StateCheck { "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage", "maps", "battleChat", - "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", + "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", ) && - RootWiring.Unmigrated.size == 7 + RootWiring.Unmigrated.size == 6 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1387,7 +1399,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 82/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 83/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -6914,6 +6926,71 @@ object StateCheck { ) } + // 93 切片 reducer:Challenges(CZ / nh.hsj,4 字段;9 个动作) + run { + fun ChallengeOf(Version: Any?): Challenge = Challenge( + version = Version, endTime = null, tiers = emptyList(), + battlePassFlavor = 1, mainPrizeImage = 0L, background = 0L, + tierReward = null, takenFreeRewardIndex = 0, + takenBattlePassRewardIndex = 0, + ) + + val S0 = ChallengesStateSlice() + Check("切片 Challenges:默认值完整", S0.Stars.isEmpty() && S0.Challenge.isEmpty()) + val S1 = ReduceChallenges(S0, UpdateStars(mapOf("a" to 3))) + Check("切片 Challenges:UpdateStars 整体替换", S1.Stars == mapOf("a" to 3)) + val S2 = ReduceChallenges(S1, SetChallenge(ChallengeOf(1))) + Check( + "切片 Challenges:SetChallenge 按 version 建索引", + (S2.Challenge[1] as Challenge).version == 1 + ) + Check( + "切片 Challenges:RemoveChallenge 按 version 删", + ReduceChallenges(S2, RemoveChallenge(1)).Challenge.isEmpty() + ) + Check( + "切片 Challenges:SetBattlePassPackages 写包表", + ReduceChallenges(S2, SetBattlePassPackages(mapOf("x" to 5L))) + .BattlePassPackages == mapOf("x" to 5L) + ) + val S3 = ReduceChallenges(S2, SetBattlePass(1)) + Check( + "切片 Challenges:SetBattlePass 追加战令条目", + S3.BattlePasses.size == 1 && S3.BattlePasses.first().version == 1 + ) + val S4 = ReduceChallenges( + ReduceChallenges(S3, SetTakenFreeRewardIndex(1, 2)), + SetTakenBattlePassRewardIndex(1, 4), + ) + Check( + "切片 Challenges:两个已领取索引按 flavor 命中", + S4.BattlePasses.first().takenFreeRewardIndex == 2 && + S4.BattlePasses.first().takenBattlePassRewardIndex == 4 + ) + Check( + "切片 Challenges:SetRewardForDialog / RefreshBattlePasses 是 no-op", + ReduceChallenges(S4, SetRewardForDialog("1", 1)) == S4 && + ReduceChallenges(S4, RefreshBattlePasses) == S4 + ) + Check( + "切片 Challenges:无关动作不改状态", + ReduceChallenges(S4, AddFrags(1)) == S4 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, UpdateStars(mapOf("b" to 1))) + Check( + "根:UpdateStars 只动 challenges", + Root1.Challenges.Stars == mapOf("b" to 1) && + Root1.BattleUsers == Root0.BattleUsers + ) + val W = RootWiring.ByField("challenges")!! + Check( + "根:challenges 委派到 CZ / nh.hsj 且 9 个动作", + W.SliceClass == "CZ" && W.Reducer == "nh.hsj" && W.Actions.size == 9 + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)