From 0cd2a23c9eeb8dbd1ef0c6ca1cc267e9e95239dd Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 24 Oct 2026 17:42:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(state):=20=E8=BF=81=E7=A7=BB=20tankBuilder?= =?UTF-8?q?State?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3DTank/docs/KtRebuild_Plan.md | 17 ++-- 3DTank/names/root_wiring.json | 15 +++- 3DTank/tools/gen_root_wiring.js | 4 + .../State/Reducers/TankBuilderStateReducer.kt | 87 +++++++++++++++++++ .../jsMain/kotlin/App/State/RootReducer.kt | 7 +- .../src/jsMain/kotlin/App/State/RootState.kt | 5 +- .../src/jsMain/kotlin/App/State/RootWiring.kt | 4 +- .../app/src/jsMain/kotlin/App/StateCheck.kt | 87 +++++++++++++++++-- 8 files changed, 205 insertions(+), 21 deletions(-) create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/TankBuilderStateReducer.kt diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index c70b3c8..3726903 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` 动作名。 - - 已迁移切片 **74/89**: + - 已迁移切片 **75/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) @@ -375,12 +375,16 @@ `UpdateProgress` / `GetReward` 复制单条合同再放回;`Fnt`(原名 `GetReward`,与任务切片同名)改名 `ClaimContractReward`,`Hnt`→`RemoveCompletedContracts` 只留还有奖励的合同; `InitBattle` 只把 `lastBattleProgress` 归零。 + - `tankBuilderState` → `brt` / `wp.ati`(`TankBuilderStateReducer.kt`):6 字段,4 个分支。 + `SetParts` 把零件表拆成 `hulls`(hullId 列表)与 `weapons`(index → weaponIds)、并写 `paints`; + `SetPartsInfo` 按零件 / 涂装各自的 id 建索引;零件模型跨模块,收口为 + `TankBuilderPart` / `TankBuilderPartInfo` / `TankBuilderPaintInfo`;`Trt`→`ResetTankBuilder` 整片复位。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 73 个有 reducer 的切片, - `launchParams` 与其余 **15 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 74/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **815 项全绿**,含 89 槽位完整性、73 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 74 个有 reducer 的切片, + `launchParams` 与其余 **14 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 75/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **823 项全绿**,含 89 槽位完整性、74 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); @@ -405,7 +409,8 @@ 设置类切片的 `_C`(共用重载)+ `Bet`/`Fet`/`Uet`/`v2`/`C7`/`E7`/`Iut` 也已登记, 其中原名都是 `UpdateSettings` 的类按切片改名为 `Apply*`,避免生成物同名冲突 (`Bet`/`v2`/`B2`/`u2`/`K0`/`C7` 共 6 个)。 - contractState 的 `Fnt`/`Hnt` 也已登记(`Fnt` 因与任务切片同名改名为 `ClaimContractReward`)。 + contractState 的 `Fnt`/`Hnt` 也已登记(`Fnt` 因与任务切片同名改名为 `ClaimContractReward`); + tankBuilderState 的 `Trt`(`ResetTankBuilder`)同样按效果定名。 ### 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 54a1f2b..3a849fb 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T09:26:08.293Z", + "generatedAt": "2026-10-24T09:32:15.650Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -80,7 +80,8 @@ "graphicsSettings", "controlSettings", "battleSettings", - "contractState" + "contractState", + "tankBuilderState" ], "AnonymousActions": [ { @@ -1367,6 +1368,12 @@ "Name": "RemoveCompletedContracts", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "只保留 rewards 非空的合同" + }, + { + "Local": "Trt", + "Name": "ResetTankBuilder", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "new brt() 整片复位" } ], "Markers": [ @@ -3052,13 +3059,13 @@ "Reducer": "wp.ati", "Delegate": "wp.ati(t, n.epy_1)", "Actions": [ - "?Trt", + "ResetTankBuilder", "SetKode", "SetParts", "SetPartsInfo" ], "Default": "new brt()", - "Migrated": false + "Migrated": true }, { "Field": "contractState", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index aa4f94c..48f47af 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -314,6 +314,8 @@ const AnonymousActionNames = new Map([ // contractState 切片(Ont / Wnt.bti):Fnt 的 GetReward 与任务切片同名,Hnt 没有 toString。 ['Fnt', 'ClaimContractReward'], ['Hnt', 'RemoveCompletedContracts'], + // tankBuilderState 切片(brt / Irt.ati):Trt 没有 toString。 + ['Trt', 'ResetTankBuilder'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -789,6 +791,7 @@ const Migrated = { controlSettings: { Type: 'ControlSettingsStateSlice', Reduce: 'ReduceControlSettings' }, battleSettings: { Type: 'BattleSettingsStateSlice', Reduce: 'ReduceBattleSettings' }, contractState: { Type: 'ContractStateSlice', Reduce: 'ReduceContractState' }, + tankBuilderState: { Type: 'TankBuilderStateSlice', Reduce: 'ReduceTankBuilder' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -1194,6 +1197,7 @@ const AppSettingsAnonymousEffects = { K0: '战斗显示设置整体替换(原名 UpdateSettings)', Fnt: '复制该合同并把 rewards 清空(原名 GetReward,与任务切片同名)', Hnt: '只保留 rewards 非空的合同', + Trt: 'new brt() 整片复位', }; fs.writeFileSync( diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/TankBuilderStateReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/TankBuilderStateReducer.kt new file mode 100644 index 0000000..2e21445 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/TankBuilderStateReducer.kt @@ -0,0 +1,87 @@ +// TankBuilderStateReducer.kt —— 坦克构建器切片(对应 module_10032 的 brt 状态 + Irt.ati) +// +// 证据链: +// 状态类 brt:6 个字段,toString 名 "TankBuilderState(…)";copy 是 ktt(6 字段 + 回调)。 +// 默认:hulls=emptyList()、weapons=emptyMap()、paints=emptyList()、 +// paintInfo=emptyMap()、partInfo=emptyMap()、kode=emptyList()。 +// 根委派:`wp.ati(t, n.epy_1)`。 +// reducer(4 个实例臂): +// yrt(SetKode) → kode +// $rt(SetParts) → hulls = parts.map { hullId };weapons = parts 按 index 建表;paints +// xrt(SetPartsInfo) → paintInfo / partInfo 分别按各自的 id 建索引 +// Trt(无 toString) → `new brt()` 整片复位 +// +// 零件 / 涂装模型在另一个模块(零件字段 `vek_1`=hullId、`xek()`=weapons;info 的键分别是 `iek_1` / `cek_1`), +// 这里收口成 `TankBuilderPart` / `TankBuilderPartInfo` / `TankBuilderPaintInfo`。 +package App.State.Reducers + +import App.State.SetKode +import App.State.SetParts +import App.State.SetPartsInfo + +/* ------------------------------------------------------------------ 模型 */ + +data class TankBuilderPart( + val hullId: Any?, + val weaponIds: Any?, +) + +data class TankBuilderPartInfo( + val partId: Any?, + val info: Any? = null, +) + +data class TankBuilderPaintInfo( + val paintId: Any?, + val info: Any? = null, +) + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `Trt`:整片复位(`new brt()`)。 */ +object ResetTankBuilder + +/* ------------------------------------------------------------------ 状态 */ + +data class TankBuilderStateSlice( + val Hulls: List = emptyList(), + val Weapons: Map = emptyMap(), + val Paints: List = emptyList(), + val PaintInfo: Map = emptyMap(), + val PartInfo: Map = emptyMap(), + val Kode: List = emptyList(), +) + +/* ------------------------------------------------------------------ reducer */ + +private fun PartsOf(Value: Any?): List = + (Value as? Iterable<*>) + ?.mapNotNull { it as? TankBuilderPart } + ?: emptyList() + +fun ReduceTankBuilder( + State0: TankBuilderStateSlice, + Action: Any, +): TankBuilderStateSlice = when (Action) { + is SetKode -> State0.copy(Kode = Action.kode as? List ?: emptyList()) + is SetParts -> { + val Parts = PartsOf(Action.parts) + State0.copy( + Hulls = Parts.map { it.hullId }, + Weapons = Parts.mapIndexed { Index, Part -> Index to Part.weaponIds }.toMap(), + Paints = (Action.paints as? List) ?: emptyList(), + ) + } + is SetPartsInfo -> { + val PartInfos = (Action.partsInfo as? Iterable<*>) + ?.mapNotNull { it as? TankBuilderPartInfo } ?: emptyList() + val PaintInfos = (Action.paintsInfo as? Iterable<*>) + ?.mapNotNull { it as? TankBuilderPaintInfo } ?: emptyList() + State0.copy( + PartInfo = PartInfos.associate { it.partId to it.info }, + PaintInfo = PaintInfos.associate { it.paintId to it.info }, + ) + } + is ResetTankBuilder -> TankBuilderStateSlice() + 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 20be6bf..484f4cb 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 出新状态)。 -// 这里采用同一形状。已迁移的 74 个切片里,73 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 75 个切片里,74 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -76,9 +76,10 @@ // 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)`) +// tankBuilderState → ReduceTankBuilder(brt / wp.ati,根委派 `wp.ati(t, n.epy_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 15 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 14 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -149,6 +150,7 @@ import App.State.Reducers.ReduceSupplies import App.State.Reducers.ReduceSuppliesTutorial import App.State.Reducers.ReduceSystem import App.State.Reducers.ReduceTankActionsLog +import App.State.Reducers.ReduceTankBuilder import App.State.Reducers.ReduceTips import App.State.Reducers.ReduceTouchscreenHudSettings import App.State.Reducers.ReduceTwitch @@ -243,6 +245,7 @@ object RootReducer { ControlSettings = ReduceControlSettings(State0.ControlSettings, Action), BattleSettings = ReduceBattleSettings(State0.BattleSettings, Action), ContractState = ReduceContractState(State0.ContractState, Action), + TankBuilderState = ReduceTankBuilder(State0.TankBuilderState, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 9959ad9..171eb03 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -69,6 +69,7 @@ import App.State.Reducers.SuppliesTutorialStateSlice import App.State.Reducers.SupportUrlStateSlice import App.State.Reducers.SystemStateSlice import App.State.Reducers.TankActionsLogStateSlice +import App.State.Reducers.TankBuilderStateSlice import App.State.Reducers.TipsStateSlice import App.State.Reducers.TouchscreenHudSettingsStateSlice import App.State.Reducers.TwitchStateSlice @@ -250,8 +251,8 @@ data class RootState( val AvailabilityByRank: AvailabilityByRankStateSlice = AvailabilityByRankStateSlice(), /** 原 userReportsState(Zst / qot.zth)。已迁移。 */ val UserReportsState: UserReportsStateSlice = UserReportsStateSlice(), - /** 原 tankBuilderState(brt / wp.ati)。未迁移:原样透传。 */ - val TankBuilderState: Any? = null, + /** 原 tankBuilderState(brt / wp.ati)。已迁移。 */ + val TankBuilderState: TankBuilderStateSlice = TankBuilderStateSlice(), /** 原 contractState(Ont / xv.bti)。已迁移。 */ val ContractState: ContractStateSlice = ContractStateSlice(), /** 原 promoCode(cZ / Q_.ysg)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index 6b28953..0955096 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -98,7 +98,7 @@ object RootWiring { SliceWiring("proBattle", "bpy_1", "Wtt", "uv.wth", "uv.wth(t, n.bpy_1)", listOf("Fight", "SetPassword")), 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("tankBuilderState", "epy_1", "brt", "wp.ati", "wp.ati(t, n.epy_1)", listOf("ResetTankBuilder", "SetKode", "SetParts", "SetPartsInfo")), 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")), @@ -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") + 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") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 ec1a99b..519f43d 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; +// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -707,6 +707,15 @@ import App.State.Reducers.ContractEntry import App.State.Reducers.ContractStateSlice import App.State.Reducers.ReduceContractState import App.State.Reducers.RemoveCompletedContracts +import App.State.SetKode +import App.State.SetParts +import App.State.SetPartsInfo +import App.State.Reducers.ReduceTankBuilder +import App.State.Reducers.ResetTankBuilder +import App.State.Reducers.TankBuilderPaintInfo +import App.State.Reducers.TankBuilderPart +import App.State.Reducers.TankBuilderPartInfo +import App.State.Reducers.TankBuilderStateSlice import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -1134,7 +1143,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 74 个已迁移切片(73 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 75 个已迁移切片(74 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1198,7 +1207,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 74 个已迁移字段,其余 15 个待迁", + "根:迁移表 = 75 个已迁移字段,其余 14 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1221,8 +1230,9 @@ object StateCheck { "clanChatState", "battleSearchState", "battleStatistics", "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", + "tankBuilderState", ) && - RootWiring.Unmigrated.size == 15 + RootWiring.Unmigrated.size == 14 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1278,7 +1288,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 74/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 75/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -6196,6 +6206,73 @@ object StateCheck { ) } + // 85 切片 reducer:TankBuilder(brt / wp.ati,6 字段;4 个动作) + run { + val S0 = TankBuilderStateSlice() + Check( + "切片 TankBuilder:默认值完整", + S0.Hulls.isEmpty() && S0.Weapons.isEmpty() && S0.Paints.isEmpty() && + S0.PaintInfo.isEmpty() && S0.PartInfo.isEmpty() && S0.Kode.isEmpty() + ) + val S1 = ReduceTankBuilder(S0, SetKode(listOf("k1"))) + Check("切片 TankBuilder:SetKode 写 kode", S1.Kode == listOf("k1")) + val S2 = ReduceTankBuilder( + S1, + SetParts( + parts = listOf( + TankBuilderPart("hull1", listOf("w1")), + TankBuilderPart("hull2", listOf("w2", "w3")), + ), + paints = listOf("p1"), + ), + ) + Check( + "切片 TankBuilder:SetParts 拆出 hulls / weapons / paints", + S2.Hulls == listOf("hull1", "hull2") && + S2.Weapons == mapOf( + 0 to listOf("w1"), + 1 to listOf("w2", "w3"), + ) && + S2.Paints == listOf("p1") + ) + val S3 = ReduceTankBuilder( + S2, + SetPartsInfo( + partsInfo = listOf(TankBuilderPartInfo("part1", "info1")), + paintsInfo = listOf(TankBuilderPaintInfo("paint1", "info2")), + ), + ) + Check( + "切片 TankBuilder:SetPartsInfo 按各自 id 建索引", + S3.PartInfo == mapOf("part1" to "info1") && + S3.PaintInfo == mapOf("paint1" to "info2") + ) + Check( + "切片 TankBuilder:ResetTankBuilder 整片复位", + ReduceTankBuilder(S3, ResetTankBuilder) == TankBuilderStateSlice() + ) + Check( + "切片 TankBuilder:无关动作不改状态", + ReduceTankBuilder(S3, AddFrags(1)) == S3 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetKode(listOf("x"))) + Check( + "根:SetKode 只动 tankBuilderState", + Root1.TankBuilderState.Kode == listOf("x") && + Root1.ContractState == Root0.ContractState + ) + val W = RootWiring.ByField("tankBuilderState")!! + Check( + "根:tankBuilderState 委派到 brt / wp.ati 且 4 个动作", + W.SliceClass == "brt" && W.Reducer == "wp.ati" && + W.Actions.toSet() == setOf( + "SetKode", "SetParts", "SetPartsInfo", "ResetTankBuilder", + ) + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)