diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 40001c5..0803522 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` 动作名。 - - 已迁移切片 **59/89**: + - 已迁移切片 **63/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) @@ -330,12 +330,23 @@ `SetValidationStatus` 按 ordinal 映射输入状态(EMPTY→SHORT、SHORT→NOT_VALID、RESERVED→RESERVED、 FORBIDDEN/NOT_VALID→FORBIDDEN、FREE→FREE)并把 `isResponseDelivered` 置 true; `G8`→`ResetRenameDialog` 复位弹窗字段但保留 rubies / hash / email。 + - `battlePauseState` → `dT` / `NT().mqc`(`BattlePauseStateReducer.kt`):3 字段,4 个分支。 + `OnPauseMode` 置 `inAutoPauseMode=true` 并写剩余时长;`ResetBattlePause` 只保留 + `idleKickPeriodInMsec`;`timeForAutoLeave` 原值是「当前时间 + 时长」,时钟换算留给中间件层。 + - `battleViewState` → `DT` / `sI.jqd`(`BattleViewStateReducer.kt`):7 字段,14 个分支。 + `SetVisibility` / `SetElementsVisibility` 按 `visible` 增删 `invisibleElements`; + 其余 12 个无 `toString` 的动作两两成对,分别切换输入 / 统计 / 菜单 / 隐藏 / 后台位。 + - `tankActionsLogState` → `vI` / `wi.gqe`(`TankActionsLogReducer.kt`):2 字段,4 个分支。 + `UpdateLog` 在超过 10 条时先丢弃最早一条再追加(源码 `(Lf = 10)`); + `OnUserJoinTheBattle` / `OnUserLeaveTheBattle` 增删 `noticed`;`dI`→`ResetTankActionsLog` 整片复位。 + - `suppliesTutorial` → `ust` / `Uw.uth`(`SuppliesTutorialReducer.kt`):2 字段,2 个分支。 + 生成器认不出切片类(根构造默认值不是 `new ust()`),在迁移表里显式补 `SliceClass: 'ust'`。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 58 个有 reducer 的切片, - `launchParams` 与其余 **30 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 59/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **685 项全绿**,含 89 槽位完整性、58 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 62 个有 reducer 的切片, + `launchParams` 与其余 **26 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 63/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **719 项全绿**,含 89 槽位完整性、62 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); @@ -353,7 +364,8 @@ battleTutorial 的 2 个、spectator 的 4 个、antiAddiction 的 3 个、moderation 的 13 个、 迁移 / 注册共用的 10 个(`Mit`…`_rt`)匿名类 / 单例同样按效果定名并登记。 friends 的 2 个、suppliesState 的 1 个、battleInviteState 的 2 个、partner 的 4 个、 - userRenameState 的 5 个也已登记(其中 `mtt` 因同名类冲突改名为 `AddInvitedUser`)。 + userRenameState 的 5 个也已登记(其中 `mtt` 因同名类冲突改名为 `AddInvitedUser`); + battlePauseState 的 2 个、battleViewState 的 12 个、tankActionsLogState 的 1 个同样按效果定名。 ### 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 86aba2f..2a8b046 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T07:48:13.464Z", + "generatedAt": "2026-10-24T08:27:50.811Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -65,7 +65,11 @@ "clanUserInfo", "battleInviteState", "partner", - "userRenameState" + "userRenameState", + "battlePauseState", + "battleViewState", + "tankActionsLogState", + "suppliesTutorial" ], "AnonymousActions": [ { @@ -1088,6 +1092,96 @@ "Name": "HideLinkErrorWarning", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "showLinkErrorWarning=false(`n.cte(…, !1)`)" + }, + { + "Local": "xT", + "Name": "ResetBattlePause", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "inAutoPauseMode=false、timeForAutoLeave 复位,保留 idleKickPeriodInMsec(`new dT(cd, cd, n.uqb_1)`)" + }, + { + "Local": "TT", + "Name": "CancelAutoPause", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "inAutoPauseMode=false(`n.wqb(!1)`)" + }, + { + "Local": "WT", + "Name": "ResetBattleView", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "new DT() 整片复位" + }, + { + "Local": "YT", + "Name": "ClearInvisibleElements", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "invisibleElements=emptySet()(`n.vqc(cd, cd, Yg())`)" + }, + { + "Local": "JT", + "Name": "EnableBattleInput", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "inputEnabled=true(`n.vqc(cd, cd, cd, !0)`)" + }, + { + "Local": "ZT", + "Name": "DisableBattleInput", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "inputEnabled=false(`n.vqc(cd, cd, cd, !1)`)" + }, + { + "Local": "QT", + "Name": "EnableBattleStatistics", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "battleStatisticsActive=true(`n.vqc(cd, cd, cd, cd, !0)`)" + }, + { + "Local": "tI", + "Name": "DisableBattleStatistics", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "battleStatisticsActive=false(`n.vqc(cd, cd, cd, cd, !1)`)" + }, + { + "Local": "nI", + "Name": "EnableBattleMenu", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "battleMenuActive=true(`n.vqc(cd, cd, cd, cd, cd, !0)`)" + }, + { + "Local": "iI", + "Name": "DisableBattleMenu", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "battleMenuActive=false(`n.vqc(cd, cd, cd, cd, cd, !1)`)" + }, + { + "Local": "nE", + "Name": "HideBattleView", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isHidden=true(`n.vqc(cd, !0)`)" + }, + { + "Local": "tE", + "Name": "ShowBattleView", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isHidden=false(`n.vqc(cd, !1)`)" + }, + { + "Local": "rI", + "Name": "EnterBackground", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "inBackgroundState=true(`n.vqc(!0)`)" + }, + { + "Local": "eI", + "Name": "LeaveBackground", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "inBackgroundState=false(`n.vqc(!1)`)" + }, + { + "Local": "dI", + "Name": "ResetTankActionsLog", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "new vI() 整片复位" } ], "Markers": [ @@ -2098,13 +2192,13 @@ "Reducer": "NT().mqc", "Delegate": "NT().mqc(t, n.lpw_1)", "Actions": [ - "?TT", - "?xT", + "CancelAutoPause", "OnPauseMode", + "ResetBattlePause", "SetIdleKickPeriod" ], "Default": "new dT()", - "Migrated": false + "Migrated": true }, { "Field": "battleViewState", @@ -2113,23 +2207,23 @@ "Reducer": "sI.jqd", "Delegate": "(null == $n && new sI(), $n).jqd(t, n.mpw_1)", "Actions": [ - "?JT", - "?QT", - "?WT", - "?YT", - "?ZT", - "?eI", - "?iI", - "?nE", - "?nI", - "?rI", - "?tE", - "?tI", + "ClearInvisibleElements", + "DisableBattleInput", + "DisableBattleMenu", + "DisableBattleStatistics", + "EnableBattleInput", + "EnableBattleMenu", + "EnableBattleStatistics", + "EnterBackground", + "HideBattleView", + "LeaveBackground", + "ResetBattleView", "SetElementsVisibility", - "SetVisibility" + "SetVisibility", + "ShowBattleView" ], "Default": "new DT()", - "Migrated": false + "Migrated": true }, { "Field": "rankUpDialogState", @@ -2163,13 +2257,13 @@ "Reducer": "wi.gqe", "Delegate": "wi.gqe(t, n.ppw_1)", "Actions": [ - "?dI", "OnUserJoinTheBattle", "OnUserLeaveTheBattle", + "ResetTankActionsLog", "UpdateLog" ], "Default": "new vI()", - "Migrated": false + "Migrated": true }, { "Field": "battleMessage", @@ -2699,7 +2793,7 @@ { "Field": "suppliesTutorial", "Slot": "zpx_1", - "SliceClass": null, + "SliceClass": "ust", "Reducer": "Uw.uth", "Delegate": "Uw.uth(t, n.zpx_1)", "Actions": [ @@ -2707,7 +2801,7 @@ "SetLoadedElement" ], "Default": "", - "Migrated": false + "Migrated": true }, { "Field": "questCategories", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 77fa661..5180751 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -257,6 +257,24 @@ const AnonymousActionNames = new Map([ ['U8', 'MarkRenameFailed'], ['G8', 'ResetRenameDialog'], ['Q8', 'HideLinkErrorWarning'], + // battlePauseState 切片(dT / PT.mqc):xT / TT 没有 toString。 + ['xT', 'ResetBattlePause'], + ['TT', 'CancelAutoPause'], + // battleViewState 切片(DT / sI.jqd):WT/YT/JT/ZT/QT/tI/nI/iI/nE/tE/rI/eI 没有 toString。 + ['WT', 'ResetBattleView'], + ['YT', 'ClearInvisibleElements'], + ['JT', 'EnableBattleInput'], + ['ZT', 'DisableBattleInput'], + ['QT', 'EnableBattleStatistics'], + ['tI', 'DisableBattleStatistics'], + ['nI', 'EnableBattleMenu'], + ['iI', 'DisableBattleMenu'], + ['nE', 'HideBattleView'], + ['tE', 'ShowBattleView'], + ['rI', 'EnterBackground'], + ['eI', 'LeaveBackground'], + // tankActionsLogState 切片(vI / yI.gqe):dI 没有 toString。 + ['dI', 'ResetTankActionsLog'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -704,6 +722,15 @@ const Migrated = { battleInviteState: { Type: 'BattleInviteStateSlice', Reduce: 'ReduceBattleInvite' }, partner: { Type: 'PartnerStateSlice', Reduce: 'ReducePartner' }, userRenameState: { Type: 'UserRenameStateSlice', Reduce: 'ReduceUserRename' }, + battlePauseState: { Type: 'BattlePauseStateSlice', Reduce: 'ReduceBattlePause' }, + battleViewState: { Type: 'BattleViewStateSlice', Reduce: 'ReduceBattleView' }, + tankActionsLogState: { Type: 'TankActionsLogStateSlice', Reduce: 'ReduceTankActionsLog' }, + // 根构造里 suppliesTutorial 的默认值不是 `new ()`,生成器认不出切片类,这里显式补。 + suppliesTutorial: { + Type: 'SuppliesTutorialStateSlice', + SliceClass: 'ust', + Reduce: 'ReduceSuppliesTutorial', + }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -1039,6 +1066,33 @@ const UserRenameAnonymousEffects = { Q8: 'showLinkErrorWarning=false(`n.cte(…, !1)`)', }; +// battlePauseState 切片(dT / PT.mqc):xT / TT 没有 toString。 +const BattlePauseAnonymousEffects = { + xT: 'inAutoPauseMode=false、timeForAutoLeave 复位,保留 idleKickPeriodInMsec(`new dT(cd, cd, n.uqb_1)`)', + TT: 'inAutoPauseMode=false(`n.wqb(!1)`)', +}; + +// battleViewState 切片(DT / sI.jqd):12 个类没有 toString。 +const BattleViewAnonymousEffects = { + WT: 'new DT() 整片复位', + YT: 'invisibleElements=emptySet()(`n.vqc(cd, cd, Yg())`)', + JT: 'inputEnabled=true(`n.vqc(cd, cd, cd, !0)`)', + ZT: 'inputEnabled=false(`n.vqc(cd, cd, cd, !1)`)', + QT: 'battleStatisticsActive=true(`n.vqc(cd, cd, cd, cd, !0)`)', + tI: 'battleStatisticsActive=false(`n.vqc(cd, cd, cd, cd, !1)`)', + nI: 'battleMenuActive=true(`n.vqc(cd, cd, cd, cd, cd, !0)`)', + iI: 'battleMenuActive=false(`n.vqc(cd, cd, cd, cd, cd, !1)`)', + nE: 'isHidden=true(`n.vqc(cd, !0)`)', + tE: 'isHidden=false(`n.vqc(cd, !1)`)', + rI: 'inBackgroundState=true(`n.vqc(!0)`)', + eI: 'inBackgroundState=false(`n.vqc(!1)`)', +}; + +// tankActionsLogState 切片(vI / yI.gqe):dI 没有 toString。 +const TankActionsLogAnonymousEffects = { + dI: 'new vI() 整片复位', +}; + fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), JSON.stringify( @@ -1265,6 +1319,24 @@ fs.writeFileSync( DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', Effect, })), + ...Object.entries(BattlePauseAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), + ...Object.entries(BattleViewAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), + ...Object.entries(TankActionsLogAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), ], Markers: [ { diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattlePauseStateReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattlePauseStateReducer.kt new file mode 100644 index 0000000..53f81c3 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattlePauseStateReducer.kt @@ -0,0 +1,50 @@ +// BattlePauseStateReducer.kt —— 战斗自动暂停切片(对应 module_10032 的 dT 状态 + PT.mqc) +// +// 证据链: +// 状态类 dT:3 个字段,toString 名 "BattlePauseState(…)"。 +// 默认:inAutoPauseMode=false、timeForAutoLeave=new Dd(0,0)、idleKickPeriodInMsec=0。 +// 根委派:`NT().mqc(t, n.lpw_1)`。 +// reducer: +// xT(无 toString) → `new dT(cd, cd, n.uqb_1)`:只保留 idleKickPeriodInMsec +// jT(OnPauseMode) → inAutoPauseMode=true、timeForAutoLeave=剩余时长 +// TT(无 toString) → inAutoPauseMode=false +// CT(SetIdleKickPeriod)→ idleKickPeriodInMsec +// +// `timeForAutoLeave` 原值是 `Fg(Md(), Wg(remaining))`(当前时间 + 时长),需要时钟服务; +// 离线重建把动作给的值原样收口在 `Any?` 里,时间换算留给中间件层。 +package App.State.Reducers + +import App.State.OnPauseMode +import App.State.SetIdleKickPeriod + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `xT`:退出战斗时复位暂停状态,只保留挂机踢出周期。 */ +object ResetBattlePause + +/** 原 `TT`:取消自动暂停。 */ +object CancelAutoPause + +/* ------------------------------------------------------------------ 状态 */ + +data class BattlePauseStateSlice( + val InAutoPauseMode: Boolean = false, + val TimeForAutoLeave: Any? = null, + val IdleKickPeriodInMsec: Any? = 0, +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceBattlePause( + State0: BattlePauseStateSlice, + Action: Any, +): BattlePauseStateSlice = when (Action) { + is ResetBattlePause -> BattlePauseStateSlice(IdleKickPeriodInMsec = State0.IdleKickPeriodInMsec) + is OnPauseMode -> State0.copy( + InAutoPauseMode = true, + TimeForAutoLeave = Action.remainingTimeForAutoLeave, + ) + is CancelAutoPause -> State0.copy(InAutoPauseMode = false) + is SetIdleKickPeriod -> State0.copy(IdleKickPeriodInMsec = Action.idleKickPeriodMsec) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleViewStateReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleViewStateReducer.kt new file mode 100644 index 0000000..0889b1d --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleViewStateReducer.kt @@ -0,0 +1,100 @@ +// BattleViewStateReducer.kt —— 战斗视图切片(对应 module_10032 的 DT 状态 + sI.jqd) +// +// 证据链: +// 状态类 DT:7 个字段,toString 名 "BattleViewState(…)"。 +// 默认:inBackgroundState=true、isHidden=true、invisibleElements=emptySet()、inputEnabled=true、 +// battleStatisticsActive=false、battleMenuActive=false、joystickPosition=null。 +// 根委派:`(null == $n && new sI(), $n).jqd(t, n.mpw_1)`。 +// reducer(13 个实例臂): +// WT(无 toString) → `new DT()` 整片复位 +// VT(SetVisibility) → invisibleElements:visible=true 删元素、false 加元素 +// KT(SetElementsVisibility)→ 同上,但按集合批量增删 +// YT(无 toString) → invisibleElements=emptySet() +// JT / ZT(无 toString) → inputEnabled=true / false +// QT / tI(无 toString) → battleStatisticsActive=true / false +// nI / iI(无 toString) → battleMenuActive=true / false +// nE / tE(无 toString) → isHidden=true / false +// rI / eI(无 toString) → inBackgroundState=true / false +package App.State.Reducers + +import App.State.SetElementsVisibility +import App.State.SetVisibility + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `WT`:整片复位(`new DT()`)。 */ +object ResetBattleView + +/** 原 `YT`:清空隐藏元素集合。 */ +object ClearInvisibleElements + +/** 原 `JT` / `ZT`:开 / 关输入。 */ +object EnableBattleInput +object DisableBattleInput + +/** 原 `QT` / `tI`:开 / 关战斗统计。 */ +object EnableBattleStatistics +object DisableBattleStatistics + +/** 原 `nI` / `iI`:开 / 关战斗菜单。 */ +object EnableBattleMenu +object DisableBattleMenu + +/** 原 `nE` / `tE`:隐藏 / 显示战斗界面。 */ +object HideBattleView +object ShowBattleView + +/** 原 `rI` / `eI`:切到后台 / 前台。 */ +object EnterBackground +object LeaveBackground + +/* ------------------------------------------------------------------ 状态 */ + +data class BattleViewStateSlice( + val InBackgroundState: Boolean = true, + val IsHidden: Boolean = true, + val InvisibleElements: Set = emptySet(), + val InputEnabled: Boolean = true, + val BattleStatisticsActive: Boolean = false, + val BattleMenuActive: Boolean = false, + val JoystickPosition: Any? = null, +) + +/* ------------------------------------------------------------------ reducer */ + +/** 原 `Qg`:集合 -= 集合 / 单值。 */ +private fun Minus(Target: Set, Value: Any?): Set = + if (Value is Iterable<*>) Target - Value.toSet() else Target - Value + +/** 原 `Zg`:集合 += 集合 / 单值。 */ +private fun Plus(Target: Set, Value: Any?): Set = + if (Value is Iterable<*>) Target + Value.toSet() else Target + Value + +fun ReduceBattleView( + State0: BattleViewStateSlice, + Action: Any, +): BattleViewStateSlice = when (Action) { + is ResetBattleView -> BattleViewStateSlice() + is SetVisibility -> State0.copy( + InvisibleElements = + if (Action.visible) State0.InvisibleElements - Action.element + else State0.InvisibleElements + Action.element, + ) + is SetElementsVisibility -> State0.copy( + InvisibleElements = + if (Action.visible) Minus(State0.InvisibleElements, Action.elements) + else Plus(State0.InvisibleElements, Action.elements), + ) + is ClearInvisibleElements -> State0.copy(InvisibleElements = emptySet()) + is EnableBattleInput -> State0.copy(InputEnabled = true) + is DisableBattleInput -> State0.copy(InputEnabled = false) + is EnableBattleStatistics -> State0.copy(BattleStatisticsActive = true) + is DisableBattleStatistics -> State0.copy(BattleStatisticsActive = false) + is EnableBattleMenu -> State0.copy(BattleMenuActive = true) + is DisableBattleMenu -> State0.copy(BattleMenuActive = false) + is HideBattleView -> State0.copy(IsHidden = true) + is ShowBattleView -> State0.copy(IsHidden = false) + is EnterBackground -> State0.copy(InBackgroundState = true) + is LeaveBackground -> State0.copy(InBackgroundState = false) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/SuppliesTutorialReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/SuppliesTutorialReducer.kt new file mode 100644 index 0000000..abe795b --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/SuppliesTutorialReducer.kt @@ -0,0 +1,32 @@ +// SuppliesTutorialReducer.kt —— 补给上手指引切片(对应 module_10032 的 ust 状态 + fst.uth) +// +// 证据链: +// 状态类 ust:2 个字段,toString 名 "SuppliesTutorialState(loadedElement=…, activatedInCurrentBattle=…)"。 +// 默认:loadedElement=null、activatedInCurrentBattle=false。 +// 根委派:`Uw.uth(t, n.zpx_1)`。 +// reducer: +// hst(ActivatedInCurrentBattle)→ activatedInCurrentBattle +// ast(SetLoadedElement) → loadedElement +package App.State.Reducers + +import App.State.ActivatedInCurrentBattle +import App.State.SetLoadedElement + +/* ------------------------------------------------------------------ 状态 */ + +data class SuppliesTutorialStateSlice( + val LoadedElement: Any? = null, + val ActivatedInCurrentBattle: Boolean = false, +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceSuppliesTutorial( + State0: SuppliesTutorialStateSlice, + Action: Any, +): SuppliesTutorialStateSlice = when (Action) { + is ActivatedInCurrentBattle -> + State0.copy(ActivatedInCurrentBattle = Action.effectActivated) + is SetLoadedElement -> State0.copy(LoadedElement = Action.loadedElement) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/TankActionsLogReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/TankActionsLogReducer.kt new file mode 100644 index 0000000..e353d5b --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/TankActionsLogReducer.kt @@ -0,0 +1,51 @@ +// TankActionsLogReducer.kt —— 战斗日志切片(对应 module_10032 的 vI 状态 + yI.gqe) +// +// 证据链: +// 状态类 vI:2 个字段,toString 名 "TankActionsLogState(…)"。 +// 默认:logs=emptyList()、noticed=emptySet()。 +// 根委派:`wi.gqe(t, n.ppw_1)`。 +// reducer: +// dI(无 toString) → `new vI()` 整片复位 +// zI(OnUserJoinTheBattle)→ noticed += userId +// bI(OnUserLeaveTheBattle)→ noticed -= userId +// qI(UpdateLog) → logs 先按上限截断,再追加一条 +// +// 上限 `Lf = 10`(module_10032 初始化:`(Lf = 10)`);原逻辑是 +// 「size >= 10 时从第 2 条开始拷贝,再 append 新日志」。 +package App.State.Reducers + +import App.State.OnUserJoinTheBattle +import App.State.OnUserLeaveTheBattle +import App.State.UpdateLog + +/* ------------------------------------------------------------------ 常量 */ + +private const val MaxLogs = 10 + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `dI`:整片复位(`new vI()`)。 */ +object ResetTankActionsLog + +/* ------------------------------------------------------------------ 状态 */ + +data class TankActionsLogStateSlice( + val Logs: List = emptyList(), + val Noticed: Set = emptySet(), +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceTankActionsLog( + State0: TankActionsLogStateSlice, + Action: Any, +): TankActionsLogStateSlice = when (Action) { + is ResetTankActionsLog -> TankActionsLogStateSlice() + is OnUserJoinTheBattle -> State0.copy(Noticed = State0.Noticed + Action.userId) + is OnUserLeaveTheBattle -> State0.copy(Noticed = State0.Noticed - Action.userId) + is UpdateLog -> { + val Kept = if (State0.Logs.size >= MaxLogs) State0.Logs.drop(1) else State0.Logs + State0.copy(Logs = Kept + Action.tankActionLog) + } + 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 a48a6fc..4c95c6e 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 出新状态)。 -// 这里采用同一形状。已迁移的 59 个切片里,58 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 63 个切片里,62 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -61,9 +61,13 @@ // battleInviteState → ReduceBattleInvite(att / Ttt().nth,根委派 `Ttt().nth(t, n.cpx_1)`) // partner → ReducePartner(UJ / `(inline@partner)`,根委派内联 IIFE) // userRenameState → ReduceUserRename(O8 / Rf.pte,根委派 `Rf.pte(t, n.qpx_1)`) +// battlePauseState → ReduceBattlePause(dT / NT().mqc,根委派 `NT().mqc(t, n.lpw_1)`) +// battleViewState → ReduceBattleView(DT / sI.jqd,根委派 `(null == $n && new sI(), $n).jqd(t, n.mpw_1)`) +// tankActionsLogState → ReduceTankActionsLog(vI / wi.gqe,根委派 `wi.gqe(t, n.ppw_1)`) +// suppliesTutorial → ReduceSuppliesTutorial(ust / Uw.uth,根委派 `Uw.uth(t, n.zpx_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 30 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 26 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -72,7 +76,9 @@ import App.State.Reducers.ReduceAccountSettings import App.State.Reducers.ReduceAnnouncement import App.State.Reducers.ReduceAntiAddiction import App.State.Reducers.ReduceBattleResult +import App.State.Reducers.ReduceBattlePause import App.State.Reducers.ReduceBattleTutorial +import App.State.Reducers.ReduceBattleView import App.State.Reducers.ReduceBattleInvite import App.State.Reducers.ReduceCaptcha import App.State.Reducers.ReduceClanUserInfo @@ -119,7 +125,9 @@ import App.State.Reducers.ReduceSupportUrl import App.State.Reducers.ReduceSettings import App.State.Reducers.ReduceShop import App.State.Reducers.ReduceSupplies +import App.State.Reducers.ReduceSuppliesTutorial import App.State.Reducers.ReduceSystem +import App.State.Reducers.ReduceTankActionsLog import App.State.Reducers.ReduceTips import App.State.Reducers.ReduceTwitch import App.State.Reducers.ReduceTwoStepVerification @@ -198,6 +206,10 @@ object RootReducer { BattleInviteState = ReduceBattleInvite(State0.BattleInviteState, Action), Partner = ReducePartner(State0.Partner, Action), UserRenameState = ReduceUserRename(State0.UserRenameState, Action), + BattlePauseState = ReduceBattlePause(State0.BattlePauseState, Action), + BattleViewState = ReduceBattleView(State0.BattleViewState, Action), + TankActionsLogState = ReduceTankActionsLog(State0.TankActionsLogState, Action), + SuppliesTutorial = ReduceSuppliesTutorial(State0.SuppliesTutorial, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 27f4e94..5f9195a 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -9,8 +9,10 @@ import App.State.Reducers.AnnouncementStateSlice import App.State.Reducers.AntiAddictionStateSlice import App.State.Reducers.AvailabilityByRankStateSlice import App.State.Reducers.BattleInviteStateSlice +import App.State.Reducers.BattlePauseStateSlice import App.State.Reducers.BattleResultStateSlice import App.State.Reducers.BattleTutorialStateSlice +import App.State.Reducers.BattleViewStateSlice import App.State.Reducers.CaptchaStateSlice import App.State.Reducers.ClanUserInfoStateSlice import App.State.Reducers.ContextTutorialStateSlice @@ -53,8 +55,10 @@ import App.State.Reducers.SettingsStateSlice import App.State.Reducers.ShopStateSlice import App.State.Reducers.SpectatorStateSlice import App.State.Reducers.SuppliesStateSlice +import App.State.Reducers.SuppliesTutorialStateSlice import App.State.Reducers.SupportUrlStateSlice import App.State.Reducers.SystemStateSlice +import App.State.Reducers.TankActionsLogStateSlice import App.State.Reducers.TipsStateSlice import App.State.Reducers.TwitchStateSlice import App.State.Reducers.TwoStepVerificationStateSlice @@ -145,16 +149,16 @@ data class RootState( val MatchmakingGroup: Any? = null, /** 原 shop(j4 / (inline@shop))。已迁移。 */ val Shop: ShopStateSlice = ShopStateSlice(), - /** 原 battlePauseState(dT / NT().mqc)。未迁移:原样透传。 */ - val BattlePauseState: Any? = null, - /** 原 battleViewState(DT / sI.jqd)。未迁移:原样透传。 */ - val BattleViewState: Any? = null, + /** 原 battlePauseState(dT / NT().mqc)。已迁移。 */ + val BattlePauseState: BattlePauseStateSlice = BattlePauseStateSlice(), + /** 原 battleViewState(DT / sI.jqd)。已迁移。 */ + val BattleViewState: BattleViewStateSlice = BattleViewStateSlice(), /** 原 rankUpDialogState(LO / Se.br3)。已迁移。 */ val RankUpDialogState: RankUpDialogStateSlice = RankUpDialogStateSlice(), /** 原 clanUserInfo(hO / de.gr1)。已迁移。 */ val ClanUserInfo: ClanUserInfoStateSlice = ClanUserInfoStateSlice(), - /** 原 tankActionsLogState(vI / wi.gqe)。未迁移:原样透传。 */ - val TankActionsLogState: Any? = null, + /** 原 tankActionsLogState(vI / wi.gqe)。已迁移。 */ + val TankActionsLogState: TankActionsLogStateSlice = TankActionsLogStateSlice(), /** 原 battleMessage(FE / vT().rqb)。未迁移:原样透传。 */ val BattleMessage: Any? = null, /** 原 battleChat(Q6 / Hf.wtg)。未迁移:原样透传。 */ @@ -225,8 +229,8 @@ data class RootState( val ItemNotifications: Any? = null, /** 原 contextTutorial(Mst / Jst().tth)。已迁移。 */ val ContextTutorial: ContextTutorialStateSlice = ContextTutorialStateSlice(), - /** 原 suppliesTutorial(? / Uw.uth)。未迁移:原样透传。 */ - val SuppliesTutorial: Any? = null, + /** 原 suppliesTutorial(ust / Uw.uth)。已迁移。 */ + val SuppliesTutorial: SuppliesTutorialStateSlice = SuppliesTutorialStateSlice(), /** 原 questCategories(zZ / th.lsh)。已迁移。 */ val QuestCategories: QuestCategoriesStateSlice = QuestCategoriesStateSlice(), /** 原 proBattle(Wtt / uv.wth)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index e1779e3..0e6ec93 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -53,11 +53,11 @@ object RootWiring { SliceWiring("suppliesState", "ipw_1", "wE", "\$t.fq9", "\$t.fq9(t, n.ipw_1)", listOf("SetSupplyCount")), SliceWiring("matchmakingGroup", "jpw_1", "rW", "NW().is0", "NW().is0(t, n.jpw_1)", listOf("?zW", "AcceptedInviteToGroup", "AddUser", "AddUserToSelected", "InitMatchmakingGroup", "Invite", "RejectedInviteToGroup", "RemoveUserToSelected", "SetAllowedFromFriends", "SetSelectedUsers", "UpdateMountItems", "UpdateUserReadiness", "UserRemoved")), SliceWiring("shop", "kpw_1", "j4", "(inline@shop)", "(function (t, n) { var i; if (t instanceof w4) i = n.qt6(cd, cd, cd, cd, cd, cd, cd, t.lt6_1); else if (t instanceof Yut) i = n.qt6(cd, cd, cd, cd, !0); else if (t instanceof F5) i = new j4(cd, cd, cd, cd, cd, cd, cd, n.qq3_1); else if (t instanceof h4) i = n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.st5_1); else if (t instanceof c4) i = n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.ut5_1); else if (t instanceof u4) i = n.qt6( cd, cd, cd, cd, cd, cd, …", listOf("?x4", "AddCategoriesWithBonus", "AddItemToOrderProcessing", "BlockBackButton", "BraintreeLoadingInterrupt", "ClearPreviousScreen", "ClearTargetCategory", "ClearTargetProductId", "CurrentPayMode", "DeleteOneTimePurchaseItem", "InitAdditionalDescription", "InitAmountString", "InitCategory", "InitCoinPackage", "InitCompensation", "InitCrystalKitPackage", "InitFeatureItem", "InitGroup", "InitItemViewType", "InitKeysPackage", "InitKitBundle", "InitKitPackage", "InitLootBoxAndPaint", "InitOneTimePurchaseItem", "InitPayModeAction", "InitPaymentModeAsShopItem", "InitPaymentProviderItems", "InitPaymentViewType", "InitRestrictionByPayMode", "InitRewardedVideosBlocks", "InitShopCard", "InitShopCardDiscount", "InitShopCardView", "InitShopItem", "InitShopItemName", "InitSingleItemKit", "InitSpecialKitPackage", "MobilePaymentStateChanged", "PaymentProviderFinishFail", "PaymentProviderFinishSuccess", "PaymentProviderInProgress", "PaymentUrlReceiveFailed", "PaymentUrlReceived", "RemoveDiscountItemForItem", "RemoveItemFromOrderProcessing", "ResetPaymentState", "ResetShop", "SavePreviousScreen", "SaveTargetProductId", "SaveTargetVisitedCategory", "SelectCategory", "SelectCategoryId", "SelectItem", "SetBraintreeToken", "SetCoinPaymentProviderUrl", "SetGoToHomeAfterBuy", "SetInitShopItemId", "SetPurchaseCompleted", "SetStripeIntentParams", "SetupDiscountCategoryByItem", "SetupDiscountItem", "SetupDiscountItemForItem", "ShopCardExpired", "ShopCardRemoved", "ShopCardSold", "ShopLoaded", "ToggleShopActivity", "TryToBuyOneTimePurchase", "UnblockBackButton")), - SliceWiring("battlePauseState", "lpw_1", "dT", "NT().mqc", "NT().mqc(t, n.lpw_1)", listOf("?TT", "?xT", "OnPauseMode", "SetIdleKickPeriod")), - SliceWiring("battleViewState", "mpw_1", "DT", "sI.jqd", "(null == \$n && new sI(), \$n).jqd(t, n.mpw_1)", listOf("?JT", "?QT", "?WT", "?YT", "?ZT", "?eI", "?iI", "?nE", "?nI", "?rI", "?tE", "?tI", "SetElementsVisibility", "SetVisibility")), + SliceWiring("battlePauseState", "lpw_1", "dT", "NT().mqc", "NT().mqc(t, n.lpw_1)", listOf("CancelAutoPause", "OnPauseMode", "ResetBattlePause", "SetIdleKickPeriod")), + SliceWiring("battleViewState", "mpw_1", "DT", "sI.jqd", "(null == \$n && new sI(), \$n).jqd(t, n.mpw_1)", listOf("ClearInvisibleElements", "DisableBattleInput", "DisableBattleMenu", "DisableBattleStatistics", "EnableBattleInput", "EnableBattleMenu", "EnableBattleStatistics", "EnterBackground", "HideBattleView", "LeaveBackground", "ResetBattleView", "SetElementsVisibility", "SetVisibility", "ShowBattleView")), SliceWiring("rankUpDialogState", "npw_1", "LO", "Se.br3", "Se.br3(t, n.npw_1)", listOf("ResetRankUpDialog", "ShowAvailableItems")), SliceWiring("clanUserInfo", "opw_1", "hO", "de.gr1", "de.gr1(t, n.opw_1)", listOf("Loaded")), - SliceWiring("tankActionsLogState", "ppw_1", "vI", "wi.gqe", "wi.gqe(t, n.ppw_1)", listOf("?dI", "OnUserJoinTheBattle", "OnUserLeaveTheBattle", "UpdateLog")), + SliceWiring("tankActionsLogState", "ppw_1", "vI", "wi.gqe", "wi.gqe(t, n.ppw_1)", listOf("OnUserJoinTheBattle", "OnUserLeaveTheBattle", "ResetTankActionsLog", "UpdateLog")), SliceWiring("battleMessage", "qpw_1", "FE", "vT().rqb", "vT().rqb(t, n.qpw_1)", listOf("?sT", "DoAddLocalizedMessage", "DoAddLocalizedTeamMessage", "DoAddLocalizedUserMessage", "DoAddTextMessage", "HideMessage", "RemoveMessage")), SliceWiring("battleChat", "rpw_1", "Q6", "Hf.wtg", "Hf.wtg(t, n.rpw_1)", listOf("?ett", "?rtt", "?stt", "?utt", "AddMessage", "ChatLoaded")), SliceWiring("droppableGold", "spw_1", "Vet", "Wp.kth", "Wp.kth(t, n.spw_1)", listOf("HideDroppableGoldAuthor", "ShowDroppableGoldAuthor")), @@ -93,7 +93,7 @@ object RootWiring { SliceWiring("icons", "wpx_1", "oH", "vs.kru", "vs.kru(t, n.wpx_1)", listOf("SetIcons")), SliceWiring("itemNotifications", "xpx_1", "_1", "A1().dso", "A1().dso(t, n.xpx_1)", listOf("?S1", "?_C", "AddItemNotification", "AddItemNotifications", "RemoveItemNotification", "UpdateItemNotification")), SliceWiring("contextTutorial", "ypx_1", "Mst", "Jst().tth", "Jst().tth(t, n.ypx_1)", listOf("LoadCurrentElement", "MarkElementAsViewed", "SetLoadedElements", "SetMissionToElement", "SetUnViewedElements")), - SliceWiring("suppliesTutorial", "zpx_1", "?", "Uw.uth", "Uw.uth(t, n.zpx_1)", listOf("ActivatedInCurrentBattle", "SetLoadedElement")), + SliceWiring("suppliesTutorial", "zpx_1", "ust", "Uw.uth", "Uw.uth(t, n.zpx_1)", listOf("ActivatedInCurrentBattle", "SetLoadedElement")), SliceWiring("questCategories", "apy_1", "zZ", "th.lsh", "th.lsh(t, n.apy_1)", listOf("RemoveQuestCategory", "RequestQuestCategoriesStatistic", "SetQuestCategories", "SetQuestCategoriesStatistic")), 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")), @@ -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") + 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") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 5cc5edf..3ebe7cc 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt @@ -18,7 +18,8 @@ // CreatorTagState / RewardedVideosState / PasswordChangeState / // System / ContextTutorial / BattleTutorial / Spectator / TwoStepVerification / AntiAddiction / // Moderation / MigrationState / Register / UserReports / -// Friends / Supplies / ClanUserInfo / BattleInvite / Partner / UserRename; +// Friends / Supplies / ClanUserInfo / BattleInvite / Partner / UserRename / +// BattlePause / BattleView / TankActionsLog / SuppliesTutorial; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -592,6 +593,38 @@ import App.State.Reducers.ReduceUserRename import App.State.Reducers.ResetRenameDialog import App.State.Reducers.RenameUidState import App.State.Reducers.UserRenameStateSlice +import App.State.OnPauseMode +import App.State.SetIdleKickPeriod +import App.State.Reducers.BattlePauseStateSlice +import App.State.Reducers.CancelAutoPause +import App.State.Reducers.ReduceBattlePause +import App.State.Reducers.ResetBattlePause +import App.State.SetElementsVisibility +import App.State.SetVisibility +import App.State.Reducers.BattleViewStateSlice +import App.State.Reducers.ClearInvisibleElements +import App.State.Reducers.DisableBattleInput +import App.State.Reducers.DisableBattleMenu +import App.State.Reducers.DisableBattleStatistics +import App.State.Reducers.EnableBattleInput +import App.State.Reducers.EnableBattleMenu +import App.State.Reducers.EnableBattleStatistics +import App.State.Reducers.EnterBackground +import App.State.Reducers.HideBattleView +import App.State.Reducers.LeaveBackground +import App.State.Reducers.ReduceBattleView +import App.State.Reducers.ResetBattleView +import App.State.Reducers.ShowBattleView +import App.State.OnUserJoinTheBattle +import App.State.OnUserLeaveTheBattle +import App.State.UpdateLog +import App.State.Reducers.ReduceTankActionsLog +import App.State.Reducers.ResetTankActionsLog +import App.State.Reducers.TankActionsLogStateSlice +import App.State.ActivatedInCurrentBattle +import App.State.SetLoadedElement +import App.State.Reducers.ReduceSuppliesTutorial +import App.State.Reducers.SuppliesTutorialStateSlice import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -1019,7 +1052,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 59 个已迁移切片(58 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 63 个已迁移切片(62 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1083,7 +1116,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 59 个已迁移字段,其余 30 个待迁", + "根:迁移表 = 63 个已迁移字段,其余 26 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1102,8 +1135,9 @@ object StateCheck { "migrationState", "register", "userReportsState", "friends", "suppliesState", "clanUserInfo", "battleInviteState", "partner", "userRenameState", + "battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial", ) && - RootWiring.Unmigrated.size == 30 + RootWiring.Unmigrated.size == 26 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1159,7 +1193,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 59/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 63/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -5233,6 +5267,215 @@ object StateCheck { ) } + // 70 切片 reducer:BattlePause(dT / NT().mqc,3 字段;4 个动作) + run { + val S0 = BattlePauseStateSlice() + Check( + "切片 BattlePause:默认不暂停、无剩余时间", + !S0.InAutoPauseMode && S0.TimeForAutoLeave == null && S0.IdleKickPeriodInMsec == 0 + ) + val S1 = ReduceBattlePause(S0, OnPauseMode("30s")) + Check( + "切片 BattlePause:OnPauseMode 置暂停并写剩余时间", + S1.InAutoPauseMode && S1.TimeForAutoLeave == "30s" + ) + Check( + "切片 BattlePause:CancelAutoPause 只清暂停位", + ReduceBattlePause(S1, CancelAutoPause).let { + !it.InAutoPauseMode && it.TimeForAutoLeave == "30s" + } + ) + Check( + "切片 BattlePause:SetIdleKickPeriod 写周期", + ReduceBattlePause(S1, SetIdleKickPeriod(120)).IdleKickPeriodInMsec == 120 + ) + Check( + "切片 BattlePause:ResetBattlePause 保留挂机周期", + ReduceBattlePause( + ReduceBattlePause(S1, SetIdleKickPeriod(120)), + ResetBattlePause, + ).let { !it.InAutoPauseMode && it.IdleKickPeriodInMsec == 120 } + ) + Check( + "切片 BattlePause:无关动作不改状态", + ReduceBattlePause(S1, AddFrags(1)) == S1 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, CancelAutoPause) + Check( + "根:CancelAutoPause 只动 battlePauseState", + !Root1.BattlePauseState.InAutoPauseMode && + Root1.BattleViewState == Root0.BattleViewState + ) + val W = RootWiring.ByField("battlePauseState")!! + Check( + "根:battlePauseState 委派到 dT / NT().mqc 且 4 个动作", + W.SliceClass == "dT" && W.Reducer == "NT().mqc" && + W.Actions.toSet() == setOf( + "ResetBattlePause", "OnPauseMode", "CancelAutoPause", "SetIdleKickPeriod", + ) + ) + } + + // 71 切片 reducer:BattleView(DT / sI.jqd,7 字段;14 个动作) + run { + val S0 = BattleViewStateSlice() + Check( + "切片 BattleView:默认在后台、隐藏、输入开", + S0.InBackgroundState && S0.IsHidden && S0.InvisibleElements.isEmpty() && + S0.InputEnabled && !S0.BattleStatisticsActive && !S0.BattleMenuActive && + S0.JoystickPosition == null + ) + val S1 = ReduceBattleView(S0, SetVisibility(true, "e1")) + Check( + "切片 BattleView:SetVisibility(true) 从隐藏集合删元素", + S1.InvisibleElements.isEmpty() + ) + val S2 = ReduceBattleView(S1, SetVisibility(false, "e1")) + Check( + "切片 BattleView:SetVisibility(false) 加入隐藏集合", + S2.InvisibleElements == setOf("e1") + ) + val S3 = ReduceBattleView( + S2, + SetElementsVisibility(true, listOf("e1", "e2")), + ) + Check( + "切片 BattleView:SetElementsVisibility 按集合增删", + S3.InvisibleElements.isEmpty() && + ReduceBattleView(S3, SetElementsVisibility(false, listOf("x", "y"))) + .InvisibleElements == setOf("x", "y") + ) + Check( + "切片 BattleView:ClearInvisibleElements 清空", + ReduceBattleView(S2, ClearInvisibleElements).InvisibleElements.isEmpty() + ) + Check( + "切片 BattleView:输入开关两态", + ReduceBattleView(S2, DisableBattleInput).let { !it.InputEnabled } && + ReduceBattleView(S2, EnableBattleInput).InputEnabled + ) + Check( + "切片 BattleView:统计与菜单开关两态", + ReduceBattleView(S2, EnableBattleStatistics).BattleStatisticsActive && + !ReduceBattleView(S2, DisableBattleStatistics).BattleStatisticsActive && + ReduceBattleView(S2, EnableBattleMenu).BattleMenuActive && + !ReduceBattleView(S2, DisableBattleMenu).BattleMenuActive + ) + Check( + "切片 BattleView:显示 / 后台开关两态", + !ReduceBattleView(S2, ShowBattleView).IsHidden && + ReduceBattleView(S2, HideBattleView).IsHidden && + !ReduceBattleView(S2, LeaveBackground).InBackgroundState && + ReduceBattleView(S2, EnterBackground).InBackgroundState + ) + Check( + "切片 BattleView:ResetBattleView 整片复位", + ReduceBattleView(S2, ResetBattleView) == BattleViewStateSlice() + ) + Check( + "切片 BattleView:无关动作不改状态", + ReduceBattleView(S2, AddFrags(1)) == S2 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, DisableBattleInput) + Check( + "根:DisableBattleInput 只动 battleViewState", + !Root1.BattleViewState.InputEnabled && + Root1.BattlePauseState == Root0.BattlePauseState + ) + val W = RootWiring.ByField("battleViewState")!! + Check( + "根:battleViewState 委派到 DT / sI.jqd 且 14 个动作", + W.SliceClass == "DT" && W.Reducer == "sI.jqd" && W.Actions.size == 14 + ) + } + + // 72 切片 reducer:TankActionsLog(vI / wi.gqe,2 字段;4 个动作) + run { + val S0 = TankActionsLogStateSlice() + Check( + "切片 TankActionsLog:默认两个集合为空", + S0.Logs.isEmpty() && S0.Noticed.isEmpty() + ) + val S1 = ReduceTankActionsLog(S0, OnUserJoinTheBattle(1L)) + Check( + "切片 TankActionsLog:OnUserJoinTheBattle 记入 noticed", + S1.Noticed == setOf(1L) + ) + Check( + "切片 TankActionsLog:OnUserLeaveTheBattle 移除 noticed", + ReduceTankActionsLog(S1, OnUserLeaveTheBattle(1L)).Noticed.isEmpty() + ) + var S2 = S1 + for (I in 1..12) S2 = ReduceTankActionsLog(S2, UpdateLog("log$I")) + Check( + "切片 TankActionsLog:超过 10 条从头截断", + S2.Logs.size == 10 && S2.Logs.first() == "log3" && S2.Logs.last() == "log12" + ) + Check( + "切片 TankActionsLog:ResetTankActionsLog 整片复位", + ReduceTankActionsLog(S2, ResetTankActionsLog) == TankActionsLogStateSlice() + ) + Check( + "切片 TankActionsLog:无关动作不改状态", + ReduceTankActionsLog(S2, AddFrags(1)) == S2 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, UpdateLog("x")) + Check( + "根:UpdateLog 只动 tankActionsLogState", + Root1.TankActionsLogState.Logs == listOf("x") && + Root1.SuppliesTutorial == Root0.SuppliesTutorial + ) + val W = RootWiring.ByField("tankActionsLogState")!! + Check( + "根:tankActionsLogState 委派到 vI / wi.gqe 且 4 个动作", + W.SliceClass == "vI" && W.Reducer == "wi.gqe" && + W.Actions.toSet() == setOf( + "ResetTankActionsLog", "OnUserJoinTheBattle", "OnUserLeaveTheBattle", + "UpdateLog", + ) + ) + } + + // 73 切片 reducer:SuppliesTutorial(ust / Uw.uth,2 字段;2 个动作) + run { + val S0 = SuppliesTutorialStateSlice() + Check( + "切片 SuppliesTutorial:默认未加载、未激活", + S0.LoadedElement == null && !S0.ActivatedInCurrentBattle + ) + val S1 = ReduceSuppliesTutorial(S0, SetLoadedElement("hull")) + Check("切片 SuppliesTutorial:SetLoadedElement 写元素", S1.LoadedElement == "hull") + Check( + "切片 SuppliesTutorial:ActivatedInCurrentBattle 写激活位", + ReduceSuppliesTutorial(S1, ActivatedInCurrentBattle(true)) + .ActivatedInCurrentBattle + ) + Check( + "切片 SuppliesTutorial:无关动作不改状态", + ReduceSuppliesTutorial(S1, AddFrags(1)) == S1 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetLoadedElement("x")) + Check( + "根:SetLoadedElement 只动 suppliesTutorial", + Root1.SuppliesTutorial.LoadedElement == "x" && + Root1.TankActionsLogState == Root0.TankActionsLogState + ) + val W = RootWiring.ByField("suppliesTutorial")!! + Check( + "根:suppliesTutorial 委派到 ust / Uw.uth", + W.SliceClass == "ust" && W.Reducer == "Uw.uth" && + W.Actions.toSet() == setOf("ActivatedInCurrentBattle", "SetLoadedElement") + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)