diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 4af4e7f..c70b3c8 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` 动作名。 - - 已迁移切片 **73/89**: + - 已迁移切片 **74/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) @@ -370,12 +370,17 @@ `u2`(原名 `UpdateSettings`)改名为 `ApplyControlSettings`。 - `battleSettings` → `W0` / `X0().esq`(`BattleSettingsReducer.kt`):11 字段,2 个分支。 `K0`(原名 `UpdateSettings`)改名为 `ApplyBattleSettings`;`_C` 的旧键迁移属 storage port,保留现状态。 + - `contractState` → `Ont` / `xv.bti`(`ContractStateReducer.kt`):3 字段,7 个分支。 + 合同模型跨模块(id / progress / rewards),收口为 `ContractEntry`;`SetContract` 是**合并**(`putAll`); + `UpdateProgress` / `GetReward` 复制单条合同再放回;`Fnt`(原名 `GetReward`,与任务切片同名)改名 + `ClaimContractReward`,`Hnt`→`RemoveCompletedContracts` 只留还有奖励的合同; + `InitBattle` 只把 `lastBattleProgress` 归零。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 72 个有 reducer 的切片, - `launchParams` 与其余 **16 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 73/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **804 项全绿**,含 89 槽位完整性、72 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 73 个有 reducer 的切片, + `launchParams` 与其余 **15 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 74/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **815 项全绿**,含 89 槽位完整性、73 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); @@ -400,6 +405,7 @@ 设置类切片的 `_C`(共用重载)+ `Bet`/`Fet`/`Uet`/`v2`/`C7`/`E7`/`Iut` 也已登记, 其中原名都是 `UpdateSettings` 的类按切片改名为 `Apply*`,避免生成物同名冲突 (`Bet`/`v2`/`B2`/`u2`/`K0`/`C7` 共 6 个)。 + contractState 的 `Fnt`/`Hnt` 也已登记(`Fnt` 因与任务切片同名改名为 `ClaimContractReward`)。 ### S4 引擎核心 - 模块:32088(3D 数学)、92980(渲染)、61378(ECS)、55088(物理)、36659 + 71105(资源)、36734(音频)、3569(输入) diff --git a/3DTank/names/root_wiring.json b/3DTank/names/root_wiring.json index d0c399e..54a1f2b 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T09:19:04.205Z", + "generatedAt": "2026-10-24T09:26:08.293Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -79,7 +79,8 @@ "touchscreenHudSettings", "graphicsSettings", "controlSettings", - "battleSettings" + "battleSettings", + "contractState" ], "AnonymousActions": [ { @@ -1354,6 +1355,18 @@ "Name": "ApplyBattleSettings", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "战斗显示设置整体替换(原名 UpdateSettings)" + }, + { + "Local": "Fnt", + "Name": "ClaimContractReward", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "复制该合同并把 rewards 清空(原名 GetReward,与任务切片同名)" + }, + { + "Local": "Hnt", + "Name": "RemoveCompletedContracts", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "只保留 rewards 非空的合同" } ], "Markers": [ @@ -3054,16 +3067,16 @@ "Reducer": "xv.bti", "Delegate": "xv.bti(t, n.fpy_1)", "Actions": [ - "?Hnt", - "GetReward", + "ClaimContractReward", "InitBattle", + "RemoveCompletedContracts", "SetActiveContract", "SetBattleProgress", "SetContract", "UpdateProgress" ], "Default": "new Ont()", - "Migrated": false + "Migrated": true }, { "Field": "promoCode", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 137b6d1..aa4f94c 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -311,6 +311,9 @@ const AnonymousActionNames = new Map([ ['B2', 'ApplyGraphicsSettings'], ['u2', 'ApplyControlSettings'], ['K0', 'ApplyBattleSettings'], + // contractState 切片(Ont / Wnt.bti):Fnt 的 GetReward 与任务切片同名,Hnt 没有 toString。 + ['Fnt', 'ClaimContractReward'], + ['Hnt', 'RemoveCompletedContracts'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -785,6 +788,7 @@ const Migrated = { }, controlSettings: { Type: 'ControlSettingsStateSlice', Reduce: 'ReduceControlSettings' }, battleSettings: { Type: 'BattleSettingsStateSlice', Reduce: 'ReduceBattleSettings' }, + contractState: { Type: 'ContractStateSlice', Reduce: 'ReduceContractState' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -1188,6 +1192,8 @@ const AppSettingsAnonymousEffects = { B2: '画质设置整体替换(原名 UpdateSettings)', u2: '操作设置整体替换(原名 UpdateSettings)', K0: '战斗显示设置整体替换(原名 UpdateSettings)', + Fnt: '复制该合同并把 rewards 清空(原名 GetReward,与任务切片同名)', + Hnt: '只保留 rewards 非空的合同', }; fs.writeFileSync( diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ContractStateReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ContractStateReducer.kt new file mode 100644 index 0000000..9904aa9 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ContractStateReducer.kt @@ -0,0 +1,93 @@ +// ContractStateReducer.kt —— 合同(contract)切片(对应 module_10032 的 Ont 状态 + Wnt.bti) +// +// 证据链: +// 状态类 Ont:3 个字段,toString 名 "ContractState(…)";copy 是 ftq(3 字段 + 回调)。 +// 默认:activeContractId=null、contracts=emptyMap()、lastBattleProgress=0。 +// 根委派:`xv.bti(t, n.fpy_1)`。 +// reducer(7 个实例臂): +// Dnt(SetActiveContract)→ activeContractId +// Lnt(SetContract) → contracts 合并(`jm` = copy + `putAll`,键是合同 id) +// Bnt(UpdateProgress) → 复制该合同、写 progress 后放回 +// Unt(SetBattleProgress)→ lastBattleProgress +// Fnt(GetReward) → 复制该合同、把 rewards 清空后放回 +// Hnt(无 toString) → 只保留 rewards 非空的合同 +// InitBattle(原 `FR`) → 只把 lastBattleProgress 归零 +// +// 合同模型在另一个模块(字段 `fdv_1`=id / `ldv_1`=progress / `odv_1`=rewards,`tdv()` 返回 rewards), +// 这里收口成 `ContractEntry(contractId, progress, rewards)`。 +// `Fnt` 的生成类 `GetReward` 撞到了任务切片的同名动作(字段是 quests),改名 `ClaimContractReward`。 +package App.State.Reducers + +import App.State.SetActiveContract +import App.State.SetBattleProgress +import App.State.SetContract +import App.State.UpdateProgress + +/* ------------------------------------------------------------------ 动作 */ + +/** 原 `Fnt`(原名 `GetReward`,与任务切片的同名类冲突):领取该合同的奖励。 */ +data class ClaimContractReward(val contractId: Any?) + +/** 原 `Hnt`:战斗结束后清掉已经没有奖励的合同。 */ +object RemoveCompletedContracts + +/* ------------------------------------------------------------------ 模型 */ + +data class ContractEntry( + val ContractId: Any?, + val Progress: Any? = null, + val Rewards: List = emptyList(), +) + +/* ------------------------------------------------------------------ 状态 */ + +data class ContractStateSlice( + val ActiveContractId: Any? = null, + val Contracts: Map = emptyMap(), + val LastBattleProgress: Any? = 0, +) + +/* ------------------------------------------------------------------ reducer */ + +private fun UpdateEntry( + Contracts: Map, + ContractId: Any?, + Transform: (ContractEntry) -> ContractEntry, +): Map { + val Entry = Contracts[ContractId] as? ContractEntry ?: return Contracts + return Contracts + (ContractId to Transform(Entry)) +} + +fun ReduceContractState( + State0: ContractStateSlice, + Action: Any, +): ContractStateSlice = when (Action) { + is SetActiveContract -> State0.copy(ActiveContractId = Action.contractId) + is SetContract -> { + val Incoming = Action.contract as? List<*> ?: listOfNotNull(Action.contract) + val Merged = LinkedHashMap(State0.Contracts) + for (Item in Incoming) { + val Entry = Item as? ContractEntry ?: continue + Merged[Entry.ContractId] = Entry + } + State0.copy(Contracts = Merged) + } + is UpdateProgress -> State0.copy( + Contracts = UpdateEntry(State0.Contracts, Action.contactId) { + it.copy(Progress = Action.progress) + }, + ) + is SetBattleProgress -> State0.copy(LastBattleProgress = Action.progress) + is ClaimContractReward -> State0.copy( + Contracts = UpdateEntry(State0.Contracts, Action.contractId) { + it.copy(Rewards = emptyList()) + }, + ) + is RemoveCompletedContracts -> State0.copy( + Contracts = State0.Contracts.filterValues { + (it as? ContractEntry)?.Rewards?.isNotEmpty() == true + }, + ) + is InitBattle -> State0.copy(LastBattleProgress = 0) + 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 4184935..20be6bf 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 出新状态)。 -// 这里采用同一形状。已迁移的 73 个切片里,72 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 74 个切片里,73 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -75,9 +75,10 @@ // graphicsSettings → ReduceGraphicsSettings(L2 / lc.isu,根委派 `lc.isu(t, n.ypw_1)`) // controlSettings → ReduceControlSettings(i2 / _2().ess,根委派 `_2().ess(t, n.zpw_1)`) // battleSettings → ReduceBattleSettings(W0 / X0().esq,根委派 `X0().esq(t, n.xpw_1)`) +// contractState → ReduceContractState(Ont / xv.bti,根委派 `xv.bti(t, n.fpy_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 16 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 15 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -97,6 +98,7 @@ import App.State.Reducers.ReduceCaptcha import App.State.Reducers.ReduceClanUserInfo import App.State.Reducers.ReduceClanChat import App.State.Reducers.ReduceContextTutorial +import App.State.Reducers.ReduceContractState import App.State.Reducers.ReduceControlSettings import App.State.Reducers.ReduceCreatorTag import App.State.Reducers.ReduceDroppableGold @@ -240,6 +242,7 @@ object RootReducer { GraphicsSettings = ReduceGraphicsSettings(State0.GraphicsSettings, Action), ControlSettings = ReduceControlSettings(State0.ControlSettings, Action), BattleSettings = ReduceBattleSettings(State0.BattleSettings, Action), + ContractState = ReduceContractState(State0.ContractState, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 5ea565a..9959ad9 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -20,6 +20,7 @@ import App.State.Reducers.CaptchaStateSlice import App.State.Reducers.ClanChatStateSlice import App.State.Reducers.ClanUserInfoStateSlice import App.State.Reducers.ContextTutorialStateSlice +import App.State.Reducers.ContractStateSlice import App.State.Reducers.ControlSettingsStateSlice import App.State.Reducers.CreatorTagStateSlice import App.State.Reducers.DroppableGoldStateSlice @@ -251,8 +252,8 @@ data class RootState( val UserReportsState: UserReportsStateSlice = UserReportsStateSlice(), /** 原 tankBuilderState(brt / wp.ati)。未迁移:原样透传。 */ val TankBuilderState: Any? = null, - /** 原 contractState(Ont / xv.bti)。未迁移:原样透传。 */ - val ContractState: Any? = null, + /** 原 contractState(Ont / xv.bti)。已迁移。 */ + val ContractState: ContractStateSlice = ContractStateSlice(), /** 原 promoCode(cZ / Q_.ysg)。已迁移。 */ val PromoCode: PromoCodeStateSlice = PromoCodeStateSlice(), /** 原 referralEvent(pQ / lh.osk)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index c409287..6b28953 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -99,14 +99,14 @@ object RootWiring { SliceWiring("availabilityByRank", "cpy_1", "tet", "\$p.xth", "\$p.xth(t, n.cpy_1)", listOf("SetLoadedData")), SliceWiring("userReportsState", "dpy_1", "Zst", "qot.zth", "(null == sd && new qot(), sd).zth(t, n.dpy_1)", listOf("InitUserReports", "ReportBattleChatMessage", "ReportLobbyChatMessage", "ReportProBattleName", "SetAvailableGameReports", "SetAvailableReports")), SliceWiring("tankBuilderState", "epy_1", "brt", "wp.ati", "wp.ati(t, n.epy_1)", listOf("?Trt", "SetKode", "SetParts", "SetPartsInfo")), - SliceWiring("contractState", "fpy_1", "Ont", "xv.bti", "xv.bti(t, n.fpy_1)", listOf("?Hnt", "GetReward", "InitBattle", "SetActiveContract", "SetBattleProgress", "SetContract", "UpdateProgress")), + SliceWiring("contractState", "fpy_1", "Ont", "xv.bti", "xv.bti(t, n.fpy_1)", listOf("ClaimContractReward", "InitBattle", "RemoveCompletedContracts", "SetActiveContract", "SetBattleProgress", "SetContract", "UpdateProgress")), SliceWiring("promoCode", "gpy_1", "cZ", "Q_.ysg", "Q_.ysg(t, n.gpy_1)", listOf("ActivatePromoCode", "SetCodeValidateStatus")), SliceWiring("referralEvent", "hpy_1", "pQ", "lh.osk", "lh.osk(t, n.hpy_1)", listOf("SetPromoCode", "SetReferralEventData", "SetReferrals", "SetReferrer")), SliceWiring("supportUrl", "ipy_1", "I4", "Yl.ct7", "Yl.ct7(t, n.ipy_1)", listOf("ClearSupportUrl", "SetUrl")), ) /** 已迁移(强类型;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") + 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") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 ec1ab9c..ec1a99b 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; +// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -698,6 +698,15 @@ import App.State.Reducers.GraphicsSettingsStateSlice import App.State.Reducers.ReduceBattleSettings import App.State.Reducers.ReduceControlSettings import App.State.Reducers.ReduceGraphicsSettings +import App.State.SetActiveContract +import App.State.SetBattleProgress +import App.State.SetContract +import App.State.UpdateProgress +import App.State.Reducers.ClaimContractReward +import App.State.Reducers.ContractEntry +import App.State.Reducers.ContractStateSlice +import App.State.Reducers.ReduceContractState +import App.State.Reducers.RemoveCompletedContracts import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -1125,7 +1134,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 73 个已迁移切片(72 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 74 个已迁移切片(73 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1189,7 +1198,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 73 个已迁移字段,其余 16 个待迁", + "根:迁移表 = 74 个已迁移字段,其余 15 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1211,9 +1220,9 @@ object StateCheck { "battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial", "clanChatState", "battleSearchState", "battleStatistics", "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", - "graphicsSettings", "controlSettings", "battleSettings", + "graphicsSettings", "controlSettings", "battleSettings", "contractState", ) && - RootWiring.Unmigrated.size == 16 + RootWiring.Unmigrated.size == 15 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1269,7 +1278,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 73/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 74/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -6116,6 +6125,77 @@ object StateCheck { ) } + // 84 切片 reducer:ContractState(Ont / xv.bti,3 字段;7 个动作) + run { + fun Contract(Id: Any?, Progress: Any? = 0, Rewards: List = emptyList()) = + ContractEntry(ContractId = Id, Progress = Progress, Rewards = Rewards) + + val S0 = ContractStateSlice() + Check( + "切片 ContractState:默认值完整", + S0.ActiveContractId == null && S0.Contracts.isEmpty() && + S0.LastBattleProgress == 0 + ) + val S1 = ReduceContractState(S0, SetActiveContract("c1")) + Check("切片 ContractState:SetActiveContract 写 id", S1.ActiveContractId == "c1") + val S2 = ReduceContractState(S1, SetContract(listOf(Contract("c1"), Contract("c2")))) + Check( + "切片 ContractState:SetContract 按合同 id 合并", + S2.Contracts.keys == setOf("c1", "c2") + ) + Check( + "切片 ContractState:UpdateProgress 复制该合同", + ReduceContractState(S2, UpdateProgress("c1", 5)) + .let { (it.Contracts["c1"] as ContractEntry).Progress == 5 } + ) + Check( + "切片 ContractState:SetBattleProgress 写战斗进度", + ReduceContractState(S2, SetBattleProgress(7)).LastBattleProgress == 7 + ) + val S3 = ReduceContractState( + ReduceContractState(S2, SetContract(listOf(Contract("c2", 1, listOf("r"))))), + ClaimContractReward("c2"), + ) + Check( + "切片 ContractState:ClaimContractReward 清空该合同奖励", + (S3.Contracts["c2"] as ContractEntry).Rewards.isEmpty() && + (S3.Contracts["c1"] as ContractEntry).Rewards.isEmpty() + ) + Check( + "切片 ContractState:RemoveCompletedContracts 只留还有奖励的", + ReduceContractState( + ReduceContractState( + S2, + SetContract(listOf(Contract("c1", 2, listOf("r")))), + ), + RemoveCompletedContracts, + ).Contracts.keys == setOf("c1") + ) + Check( + "切片 ContractState:InitBattle 只把战斗进度归零", + ReduceContractState(S3, InitBattle()).let { + it.LastBattleProgress == 0 && it.Contracts.keys == setOf("c1", "c2") + } + ) + Check( + "切片 ContractState:无关动作不改状态", + ReduceContractState(S3, AddFrags(1)) == S3 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetActiveContract("x")) + Check( + "根:SetActiveContract 只动 contractState", + Root1.ContractState.ActiveContractId == "x" && + Root1.BattleSettings == Root0.BattleSettings + ) + val W = RootWiring.ByField("contractState")!! + Check( + "根:contractState 委派到 Ont / xv.bti 且 7 个动作", + W.SliceClass == "Ont" && W.Reducer == "xv.bti" && W.Actions.size == 7 + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)