From 86979eb168b7ca94b28de68bf79949d546f24158 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 24 Oct 2026 16:55:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(state):=20=E8=BF=81=E7=A7=BB=20battleStati?= =?UTF-8?q?stics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3DTank/docs/KtRebuild_Plan.md | 14 +- 3DTank/names/root_wiring.json | 105 ++++++++-- 3DTank/tools/gen_root_wiring.js | 36 ++++ .../State/Reducers/BattleStatisticsReducer.kt | 183 ++++++++++++++++++ .../jsMain/kotlin/App/State/RootReducer.kt | 7 +- .../src/jsMain/kotlin/App/State/RootState.kt | 5 +- .../src/jsMain/kotlin/App/State/RootWiring.kt | 6 +- .../app/src/jsMain/kotlin/App/StateCheck.kt | 144 +++++++++++++- 8 files changed, 466 insertions(+), 34 deletions(-) create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleStatisticsReducer.kt diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 4b10bb6..0174887 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` 动作名。 - - 已迁移切片 **65/89**: + - 已迁移切片 **66/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) @@ -347,12 +347,15 @@ - `battleSearchState` → `Itt` / `Htt().vtg`(`BattleSearchStateReducer.kt`):5 字段,9 个分支。 `Fight` 只发副作用、状态不变;`RemoveBattle` 仅当选中 id 匹配时清空; `selectedFormat` 默认值来自服务定位器,按 `null` 收口。 + - `battleStatistics` → `jR` / `rr.fqr`(`BattleStatisticsReducer.kt`):27 字段,22 个分支。 + `FR`(14 字段的 Init)与通知切片的同名类冲突,改名为 `InitBattle`(contractState 之后复用); + 其余为战斗元信息 setter + 回合 / 统计面板 / 菜单 / 旗主 / 加载位;`SetRemainingTime` 用 `$m` 把时长转秒。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 64 个有 reducer 的切片, - `launchParams` 与其余 **24 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 65/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **741 项全绿**,含 89 槽位完整性、64 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 65 个有 reducer 的切片, + `launchParams` 与其余 **23 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 66/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **755 项全绿**,含 89 槽位完整性、65 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); @@ -373,6 +376,7 @@ userRenameState 的 5 个也已登记(其中 `mtt` 因同名类冲突改名为 `AddInvitedUser`); battlePauseState 的 2 个、battleViewState 的 12 个、tankActionsLogState 的 1 个同样按效果定名。 clanChatState 的 3 个(`$nt`/`xnt`/`Tnt`)、battleSearchState 的 1 个也已登记。 + battleStatistics 的 12 个(含改名的 `FR`→`InitBattle`)同样按效果定名。 ### 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 2022765..e72ea47 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T08:38:53.378Z", + "generatedAt": "2026-10-24T08:50:07.796Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -71,7 +71,8 @@ "tankActionsLogState", "suppliesTutorial", "clanChatState", - "battleSearchState" + "battleSearchState", + "battleStatistics" ], "AnonymousActions": [ { @@ -1208,6 +1209,78 @@ "Name": "ClearSelectedBattle", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "selectedBattleId=null(`n.vtn(cd, cd, cd, cd, null)`)" + }, + { + "Local": "FR", + "Name": "InitBattle", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "14 字段战斗初始化(原名 Init,与通知切片的同名类冲突,改名 InitBattle)" + }, + { + "Local": "PR", + "Name": "ResetRoundState", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "roundStarted=false、menuVisible=false(`n.cqq(…, !1, cd, !1)`)" + }, + { + "Local": "KR", + "Name": "ShowStat", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "statVisible=true;menuVisible=!(menu && !stat) && menu(`n.cqq(…, !0, cd, cd, …)`)" + }, + { + "Local": "YR", + "Name": "HideStat", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "statVisible=false(`n.cqq(…, !1)`)" + }, + { + "Local": "XR", + "Name": "BeginRound", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isContinueBattle=false、score=emptyMap()、flagOrBallOwner=null、statVisible=false、roundStarted=true、menuVisible=false" + }, + { + "Local": "UR", + "Name": "ContinueBattle", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isContinueBattle=true(`n.cqq(…, !0)`)" + }, + { + "Local": "QR", + "Name": "CloseStatMenu", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "menuVisible=false(`n.cqq(…, !1)`)" + }, + { + "Local": "ZR", + "Name": "ToggleStatMenu", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "menuVisible=!menuVisible(`n.cqq(…, !n.bq2_1)`)" + }, + { + "Local": "DR", + "Name": "ClearFlagOrBallOwner", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "flagOrBallOwner=null(`n.cqq(…, null)`)" + }, + { + "Local": "tP", + "Name": "MarkBattleLoaded", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "battleLoaded=true(`n.cqq(…, !0)`)" + }, + { + "Local": "nP", + "Name": "MarkBattleUnloaded", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "battleLoaded=false(`n.cqq(…, !1)`)" + }, + { + "Local": "HR", + "Name": "ResetBattleStatistics", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "new jR() 整片复位" } ], "Markers": [ @@ -1337,19 +1410,17 @@ "Reducer": "rr.fqr", "Delegate": "rr.fqr(t, n.bpv_1)", "Actions": [ - "?DR", - "?HR", - "?KR", - "?PR", - "?QR", - "?UR", - "?XR", - "?YR", - "?ZR", - "?nP", - "?tP", - "Init", + "BeginRound", + "ClearFlagOrBallOwner", + "CloseStatMenu", + "ContinueBattle", + "HideStat", + "InitBattle", "InitBattleModeWithLevels", + "MarkBattleLoaded", + "MarkBattleUnloaded", + "ResetBattleStatistics", + "ResetRoundState", "SetBattleMode", "SetDisplayClientInfo", "SetFlagOrBallOwner", @@ -1357,11 +1428,13 @@ "SetReArmor", "SetRemainingTime", "SetRoundRestartTime", + "ShowStat", + "ToggleStatMenu", "UpdateFund", "UpdateScore" ], "Default": "new jR()", - "Migrated": false + "Migrated": true }, { "Field": "battleTutorial", @@ -2910,7 +2983,7 @@ "Actions": [ "?Hnt", "GetReward", - "Init", + "InitBattle", "SetActiveContract", "SetBattleProgress", "SetContract", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 5ed91a4..3f96037 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -281,6 +281,19 @@ const AnonymousActionNames = new Map([ ['Tnt', 'ResetClanChat'], // battleSearchState 切片(Itt / Gtt.vtg):Dtt 没有 toString。 ['Dtt', 'ClearSelectedBattle'], + // battleStatistics 切片(jR / rP.fqr):11 个类没有 toString;FR 与通知切片的 Init 同名,改名。 + ['FR', 'InitBattle'], + ['PR', 'ResetRoundState'], + ['KR', 'ShowStat'], + ['YR', 'HideStat'], + ['XR', 'BeginRound'], + ['UR', 'ContinueBattle'], + ['QR', 'CloseStatMenu'], + ['ZR', 'ToggleStatMenu'], + ['DR', 'ClearFlagOrBallOwner'], + ['tP', 'MarkBattleLoaded'], + ['nP', 'MarkBattleUnloaded'], + ['HR', 'ResetBattleStatistics'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -739,6 +752,7 @@ const Migrated = { }, clanChatState: { Type: 'ClanChatStateSlice', Reduce: 'ReduceClanChat' }, battleSearchState: { Type: 'BattleSearchStateSlice', Reduce: 'ReduceBattleSearch' }, + battleStatistics: { Type: 'BattleStatisticsStateSlice', Reduce: 'ReduceBattleStatistics' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -1113,6 +1127,22 @@ const BattleSearchAnonymousEffects = { Dtt: 'selectedBattleId=null(`n.vtn(cd, cd, cd, cd, null)`)', }; +// battleStatistics 切片(jR / rP.fqr):11 个类没有 toString;FR 与通知切片 Init 同名。 +const BattleStatisticsAnonymousEffects = { + FR: '14 字段战斗初始化(原名 Init,与通知切片的同名类冲突,改名 InitBattle)', + PR: 'roundStarted=false、menuVisible=false(`n.cqq(…, !1, cd, !1)`)', + KR: 'statVisible=true;menuVisible=!(menu && !stat) && menu(`n.cqq(…, !0, cd, cd, …)`)', + YR: 'statVisible=false(`n.cqq(…, !1)`)', + XR: 'isContinueBattle=false、score=emptyMap()、flagOrBallOwner=null、statVisible=false、roundStarted=true、menuVisible=false', + UR: 'isContinueBattle=true(`n.cqq(…, !0)`)', + QR: 'menuVisible=false(`n.cqq(…, !1)`)', + ZR: 'menuVisible=!menuVisible(`n.cqq(…, !n.bq2_1)`)', + DR: 'flagOrBallOwner=null(`n.cqq(…, null)`)', + tP: 'battleLoaded=true(`n.cqq(…, !0)`)', + nP: 'battleLoaded=false(`n.cqq(…, !1)`)', + HR: 'new jR() 整片复位', +}; + fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), JSON.stringify( @@ -1369,6 +1399,12 @@ fs.writeFileSync( DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', Effect, })), + ...Object.entries(BattleStatisticsAnonymousEffects).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/BattleStatisticsReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleStatisticsReducer.kt new file mode 100644 index 0000000..a07ec8a --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleStatisticsReducer.kt @@ -0,0 +1,183 @@ +// BattleStatisticsReducer.kt —— 战斗统计切片(对应 module_10032 的 jR 状态 + rP.fqr) +// +// 证据链: +// 状态类 jR:27 个字段,toString 名 "BattleStatistics(…)";copy 是 cqq(27 字段 + 回调)。 +// 根委派:`rr.fqr(t, n.bpv_1)`。 +// reducer(22 个实例臂): +// FR(Init) → 用 14 字段的重置动作灌入战斗元信息 +// GR(SetDisplayClientInfo) → DisplayClientInfo +// IR(SetBattleMode) → Mode +// AR(SetReArmor) → IsReArmorEnabled +// RR(SetRankRange) → RankRange +// ER(UpdateScore) → Score 写键值 +// TR(UpdateFund) → Fund +// PR / KR / YR / XR / UR / QR / ZR(无 toString) +// → 回合开始/结束、统计面板与菜单的显示切换 +// NR(SetRoundRestartTime) → RoundRestartTime +// MR(SetRemainingTime) → RemainingTimeInSec(时长 → 秒) +// OR(SetFlagOrBallOwner) → FlagOrBallOwner +// DR / tP / nP(无 toString) → 清旗主、BattleLoaded 开关 +// HR(无 toString) → `new jR()` 整片复位 +// JR(InitBattleModeWithLevels)→ GameModeLevelsCount +// +// `FR` 与 contractState 共用同一个 14 字段动作类,生成物里的 `Init` 撞到了通知切片的 +// 同名类,因此这里改名为 `InitBattle`(两个切片都直接读这 14 个字段)。 +package App.State.Reducers + +import App.State.InitBattleModeWithLevels +import App.State.Range +import App.State.SetBattleMode +import App.State.SetDisplayClientInfo +import App.State.SetFlagOrBallOwner +import App.State.SetRankRange +import App.State.SetReArmor +import App.State.SetRemainingTime +import App.State.SetRoundRestartTime +import App.State.UpdateFund +import App.State.UpdateScore + +/* ------------------------------------------------------------------ 动作 */ + +/** 原 `FR`(原名 `Init`,与通知切片的同名类冲突):14 字段的战斗初始化动作。 */ +data class InitBattle( + val battleId: Any? = null, + val remainingTime: Any? = null, + val battleName: String = "", + val matchBattle: Boolean = true, + val isParkourMode: Boolean = false, + val isTutorial: Boolean = false, + val valuableBattle: Boolean = true, + val scoreLimit: Int = 0, + val fund: Int = 0, + val options: List = emptyList(), + val customBattleName: String? = null, + val battleFormat: Any? = null, + val isMatchmakingMark: Boolean = false, + val mapNameWithoutMode: String = "", +) + +/** 原 `PR`:回合结束,复位开始位与菜单。 */ +object ResetRoundState + +/** 原 `KR`:显示统计面板(菜单保持原状,除非原本「菜单开且统计关」)。 */ +object ShowStat + +/** 原 `YR`:隐藏统计面板。 */ +object HideStat + +/** 原 `XR`:新回合开始,清分数 / 旗主并打开回合位。 */ +object BeginRound + +/** 原 `UR`:标记为连续战斗。 */ +object ContinueBattle + +/** 原 `QR`:关闭统计菜单。 */ +object CloseStatMenu + +/** 原 `ZR`:切换统计菜单。 */ +object ToggleStatMenu + +/** 原 `DR`:清空旗主 / 球主。 */ +object ClearFlagOrBallOwner + +/** 原 `tP`:标记战斗资源已加载。 */ +object MarkBattleLoaded + +/** 原 `nP`:标记战斗资源未加载。 */ +object MarkBattleUnloaded + +/** 原 `HR`:整片复位(`new jR()`)。 */ +object ResetBattleStatistics + +/* ------------------------------------------------------------------ 状态 */ + +data class BattleStatisticsStateSlice( + val BattleId: Any? = null, + val MapName: String = "", + val Mode: String? = null, + val IsMMBattle: Boolean = true, + val IsParkourMode: Boolean = false, + val IsTutorial: Boolean = false, + val ValuableBattle: Boolean = true, + val IsReArmorEnabled: Boolean = true, + val RankRange: Range? = null, + val IsContinueBattle: Boolean = false, + val RoundRestartTime: Any? = null, + val Score: Map = emptyMap(), + val Fund: Int = 0, + val GameModeLevelsCount: Int = 0, + val FlagOrBallOwner: Any? = null, + val StatVisible: Boolean = false, + val RoundStarted: Boolean = false, + val ScoreLimit: Int = 0, + val MenuVisible: Boolean = false, + val RemainingTimeInSec: Int = -1, + val BattleLoaded: Boolean = false, + val Options: List = emptyList(), + val CustomBattleName: String? = null, + val BattleFormat: Any? = null, + val IsMatchmakingMark: Boolean = false, + val MapNameWithoutMode: String = "", + val DisplayClientInfo: Boolean = true, +) + +/* ------------------------------------------------------------------ reducer */ + +/** 原 `$m`:时长 → 秒。 */ +private fun AsSeconds(Value: Any?): Int = (Value as? Number)?.toInt() ?: 0 + +fun ReduceBattleStatistics( + State0: BattleStatisticsStateSlice, + Action: Any, +): BattleStatisticsStateSlice = when (Action) { + is InitBattle -> State0.copy( + MapName = Action.battleName, + IsMMBattle = Action.matchBattle, + IsParkourMode = Action.isParkourMode, + IsTutorial = Action.isTutorial, + ValuableBattle = Action.valuableBattle, + IsContinueBattle = false, + Score = emptyMap(), + Fund = Action.fund, + ScoreLimit = Action.scoreLimit, + MenuVisible = false, + RemainingTimeInSec = AsSeconds(Action.remainingTime), + Options = Action.options, + CustomBattleName = Action.customBattleName, + BattleFormat = Action.battleFormat, + IsMatchmakingMark = Action.isMatchmakingMark, + MapNameWithoutMode = Action.mapNameWithoutMode, + ) + is SetDisplayClientInfo -> State0.copy(DisplayClientInfo = Action.visible) + is SetBattleMode -> State0.copy(Mode = Action.mode) + is SetReArmor -> State0.copy(IsReArmorEnabled = Action.enabled) + is SetRankRange -> State0.copy(RankRange = Action.rankRange) + is UpdateScore -> State0.copy(Score = State0.Score + (Action.team to Action.score)) + is UpdateFund -> State0.copy(Fund = Action.fund) + is ResetRoundState -> State0.copy(RoundStarted = false, MenuVisible = false) + is ShowStat -> State0.copy( + StatVisible = true, + MenuVisible = !(State0.MenuVisible && !State0.StatVisible) && State0.MenuVisible, + ) + is HideStat -> State0.copy(StatVisible = false) + is BeginRound -> State0.copy( + IsContinueBattle = false, + Score = emptyMap(), + FlagOrBallOwner = null, + StatVisible = false, + RoundStarted = true, + MenuVisible = false, + ) + is ContinueBattle -> State0.copy(IsContinueBattle = true) + is CloseStatMenu -> State0.copy(MenuVisible = false) + is ToggleStatMenu -> State0.copy(MenuVisible = !State0.MenuVisible) + is SetRoundRestartTime -> State0.copy(RoundRestartTime = Action.time) + is SetRemainingTime -> State0.copy(RemainingTimeInSec = AsSeconds(Action.time)) + is SetFlagOrBallOwner -> State0.copy(FlagOrBallOwner = Action.ownerUserId) + is ClearFlagOrBallOwner -> State0.copy(FlagOrBallOwner = null) + is MarkBattleLoaded -> State0.copy(BattleLoaded = true) + is MarkBattleUnloaded -> State0.copy(BattleLoaded = false) + is ResetBattleStatistics -> BattleStatisticsStateSlice() + is InitBattleModeWithLevels -> State0.copy(GameModeLevelsCount = Action.levelsCount as? Int ?: 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 8cc0e08..3094e15 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 出新状态)。 -// 这里采用同一形状。已迁移的 65 个切片里,64 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 66 个切片里,65 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -67,9 +67,10 @@ // suppliesTutorial → ReduceSuppliesTutorial(ust / Uw.uth,根委派 `Uw.uth(t, n.zpx_1)`) // clanChatState → ReduceClanChat(qnt / yv.qtg,根委派 `yv.qtg(t, n.ppv_1)`) // battleSearchState → ReduceBattleSearch(Itt / Htt().vtg,根委派 `Htt().vtg(t, n.epw_1)`) +// battleStatistics → ReduceBattleStatistics(jR / rr.fqr,根委派 `rr.fqr(t, n.bpv_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 24 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 23 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -80,6 +81,7 @@ import App.State.Reducers.ReduceAntiAddiction import App.State.Reducers.ReduceBattleResult import App.State.Reducers.ReduceBattlePause import App.State.Reducers.ReduceBattleSearch +import App.State.Reducers.ReduceBattleStatistics import App.State.Reducers.ReduceBattleTutorial import App.State.Reducers.ReduceBattleView import App.State.Reducers.ReduceBattleInvite @@ -216,6 +218,7 @@ object RootReducer { SuppliesTutorial = ReduceSuppliesTutorial(State0.SuppliesTutorial, Action), ClanChatState = ReduceClanChat(State0.ClanChatState, Action), BattleSearchState = ReduceBattleSearch(State0.BattleSearchState, Action), + BattleStatistics = ReduceBattleStatistics(State0.BattleStatistics, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 21ff834..7bb5999 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -12,6 +12,7 @@ import App.State.Reducers.BattleInviteStateSlice import App.State.Reducers.BattlePauseStateSlice import App.State.Reducers.BattleResultStateSlice import App.State.Reducers.BattleSearchStateSlice +import App.State.Reducers.BattleStatisticsStateSlice import App.State.Reducers.BattleTutorialStateSlice import App.State.Reducers.BattleViewStateSlice import App.State.Reducers.CaptchaStateSlice @@ -79,8 +80,8 @@ data class RootState( val Screen: ScreenStateSlice = ScreenStateSlice(), /** 原 modalScreen(nV / Fs.js1)。已迁移。 */ val ModalScreen: ModalScreenStateSlice = ModalScreenStateSlice(), - /** 原 battleStatistics(jR / rr.fqr)。未迁移:原样透传。 */ - val BattleStatistics: Any? = null, + /** 原 battleStatistics(jR / rr.fqr)。已迁移。 */ + val BattleStatistics: BattleStatisticsStateSlice = BattleStatisticsStateSlice(), /** 原 battleTutorial(rE / Q.sq8)。已迁移。 */ val BattleTutorial: BattleTutorialStateSlice = BattleTutorialStateSlice(), /** 原 garage(UB / hF)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index 455f409..be88b19 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -17,7 +17,7 @@ object RootWiring { SliceWiring("user", "ypu_1", "t8", "M8().qtd", "M8().qtd(t, n.ypu_1)", listOf("AddCoins", "AddRubies", "ClearInDebt", "DisableLobbyTutorial", "SetBatteryLevel", "SetClanInfo", "SetCoins", "SetDisabledWithoutPremiumLocations", "SetHasTemporaryCash", "SetInDebt", "SetPremiumExpirationTime", "SetRankNames", "SetRubies", "SetUserId", "SetUserPropertiesAction", "SpendCoins", "SpendCrystals", "SpendRubies", "UpdateClanTag", "UpdateCrystals", "UpdateEventPass", "UpdateGearScore", "UpdatePremium", "UpdateRank", "UpdateScore", "UpdateUid", "UpdateUserRating")), SliceWiring("screen", "zpu_1", "WK", "dY().ks5", "dY().ks5(t, n.zpu_1)", listOf("ClearScreenStack", "DelayNextGoTo", "DisableBack", "DisableForeground", "DisplayMainEntranceScreen", "DisplayReauthorizationMessage", "EnableBack", "EnableForeground", "FilterScreenStack", "GoTo", "PopScreen", "PressedAnyButton", "ResetScreenStackOnNextGoTo")), SliceWiring("modalScreen", "apv_1", "nV", "Fs.js1", "Fs.js1(t, n.apv_1)", listOf("AddModal", "CloseCurrentModal", "RemoveCurrentByName", "RemoveModal")), - SliceWiring("battleStatistics", "bpv_1", "jR", "rr.fqr", "rr.fqr(t, n.bpv_1)", listOf("?DR", "?HR", "?KR", "?PR", "?QR", "?UR", "?XR", "?YR", "?ZR", "?nP", "?tP", "Init", "InitBattleModeWithLevels", "SetBattleMode", "SetDisplayClientInfo", "SetFlagOrBallOwner", "SetRankRange", "SetReArmor", "SetRemainingTime", "SetRoundRestartTime", "UpdateFund", "UpdateScore")), + SliceWiring("battleStatistics", "bpv_1", "jR", "rr.fqr", "rr.fqr(t, n.bpv_1)", listOf("BeginRound", "ClearFlagOrBallOwner", "CloseStatMenu", "ContinueBattle", "HideStat", "InitBattle", "InitBattleModeWithLevels", "MarkBattleLoaded", "MarkBattleUnloaded", "ResetBattleStatistics", "ResetRoundState", "SetBattleMode", "SetDisplayClientInfo", "SetFlagOrBallOwner", "SetRankRange", "SetReArmor", "SetRemainingTime", "SetRoundRestartTime", "ShowStat", "ToggleStatMenu", "UpdateFund", "UpdateScore")), 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")), @@ -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", "Init", "SetActiveContract", "SetBattleProgress", "SetContract", "UpdateProgress")), + SliceWiring("contractState", "fpy_1", "Ont", "xv.bti", "xv.bti(t, n.fpy_1)", listOf("?Hnt", "GetReward", "InitBattle", "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") + 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") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 cc24e15..74b7d94 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; +// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -643,6 +643,30 @@ import App.State.SetSuspiciousFilter import App.State.Reducers.BattleSearchStateSlice import App.State.Reducers.ClearSelectedBattle import App.State.Reducers.ReduceBattleSearch +import App.State.InitBattleModeWithLevels +import App.State.Range +import App.State.SetBattleMode +import App.State.SetDisplayClientInfo +import App.State.SetFlagOrBallOwner +import App.State.SetRankRange +import App.State.SetReArmor +import App.State.SetRemainingTime +import App.State.SetRoundRestartTime +import App.State.UpdateFund +import App.State.Reducers.BattleStatisticsStateSlice +import App.State.Reducers.BeginRound +import App.State.Reducers.ClearFlagOrBallOwner +import App.State.Reducers.CloseStatMenu +import App.State.Reducers.ContinueBattle +import App.State.Reducers.HideStat +import App.State.Reducers.InitBattle +import App.State.Reducers.MarkBattleLoaded +import App.State.Reducers.MarkBattleUnloaded +import App.State.Reducers.ReduceBattleStatistics +import App.State.Reducers.ResetBattleStatistics +import App.State.Reducers.ResetRoundState +import App.State.Reducers.ShowStat +import App.State.Reducers.ToggleStatMenu import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -1070,7 +1094,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 65 个已迁移切片(64 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 66 个已迁移切片(65 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1134,7 +1158,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 65 个已迁移字段,其余 24 个待迁", + "根:迁移表 = 66 个已迁移字段,其余 23 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1154,9 +1178,9 @@ object StateCheck { "friends", "suppliesState", "clanUserInfo", "battleInviteState", "partner", "userRenameState", "battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial", - "clanChatState", "battleSearchState", + "clanChatState", "battleSearchState", "battleStatistics", ) && - RootWiring.Unmigrated.size == 24 + RootWiring.Unmigrated.size == 23 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1212,7 +1236,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 65/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 66/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -5610,6 +5634,114 @@ object StateCheck { ) } + // 76 切片 reducer:BattleStatistics(jR / rr.fqr,27 字段;22 个动作) + run { + val S0 = BattleStatisticsStateSlice() + Check( + "切片 BattleStatistics:默认值完整", + S0.BattleId == null && S0.MapName == "" && S0.Mode == null && + S0.IsMMBattle && !S0.IsParkourMode && !S0.IsTutorial && + S0.ValuableBattle && S0.IsReArmorEnabled && S0.RankRange == null && + !S0.IsContinueBattle && S0.RoundRestartTime == null && S0.Score.isEmpty() && + S0.Fund == 0 && S0.GameModeLevelsCount == 0 && S0.FlagOrBallOwner == null && + !S0.StatVisible && !S0.RoundStarted && S0.ScoreLimit == 0 && + !S0.MenuVisible && S0.RemainingTimeInSec == -1 && !S0.BattleLoaded && + S0.Options.isEmpty() && S0.CustomBattleName == null && + S0.BattleFormat == null && !S0.IsMatchmakingMark && + S0.MapNameWithoutMode == "" && S0.DisplayClientInfo + ) + val S1 = ReduceBattleStatistics( + S0, + InitBattle( + battleId = "b1", remainingTime = 180, battleName = "Desert", + matchBattle = true, isParkourMode = false, isTutorial = false, + valuableBattle = true, scoreLimit = 100, fund = 50, + options = listOf("o"), customBattleName = "custom", + battleFormat = "fmt", isMatchmakingMark = true, mapNameWithoutMode = "Desert", + ), + ) + Check( + "切片 BattleStatistics:InitBattle 灌入元信息并清分数 / 菜单", + S1.MapName == "Desert" && S1.IsMMBattle && S1.Fund == 50 && + S1.ScoreLimit == 100 && S1.RemainingTimeInSec == 180 && + S1.CustomBattleName == "custom" && S1.IsMatchmakingMark && + S1.MapNameWithoutMode == "Desert" && S1.Options == listOf("o") && + S1.Score.isEmpty() && !S1.MenuVisible && !S1.IsContinueBattle + ) + Check( + "切片 BattleStatistics:模式 / 护甲 / 段位 / 客户端信息", + ReduceBattleStatistics(S1, SetBattleMode("DM")).Mode == "DM" && + !ReduceBattleStatistics(S1, SetReArmor(false)).IsReArmorEnabled && + ReduceBattleStatistics(S1, SetRankRange(Range(10, 1))).RankRange == + Range(10, 1) && + !ReduceBattleStatistics(S1, SetDisplayClientInfo(false)).DisplayClientInfo + ) + Check( + "切片 BattleStatistics:UpdateScore / UpdateFund 写表与资金", + ReduceBattleStatistics(S1, UpdateScore("A", 3)).Score == + mapOf("A" to 3) && + ReduceBattleStatistics(S1, UpdateFund(0L, 9)).Fund == 9 + ) + Check( + "切片 BattleStatistics:SetRemainingTime 时长转秒", + ReduceBattleStatistics(S1, SetRemainingTime(90)).RemainingTimeInSec == 90 + ) + Check( + "切片 BattleStatistics:旗主设置 / 清空", + ReduceBattleStatistics(S1, SetFlagOrBallOwner(7L)).FlagOrBallOwner == 7L && + ReduceBattleStatistics( + ReduceBattleStatistics(S1, SetFlagOrBallOwner(7L)), + ClearFlagOrBallOwner, + ).FlagOrBallOwner == null + ) + Check( + "切片 BattleStatistics:ShowStat / HideStat / 菜单两态", + ReduceBattleStatistics(S1, ShowStat).StatVisible && + !ReduceBattleStatistics(S1, HideStat).StatVisible && + !ReduceBattleStatistics(S1, CloseStatMenu).MenuVisible && + ReduceBattleStatistics(S1, ToggleStatMenu).MenuVisible + ) + val S2 = ReduceBattleStatistics(S1, BeginRound) + Check( + "切片 BattleStatistics:BeginRound 开回合并清分数", + S2.RoundStarted && !S2.StatVisible && !S2.MenuVisible && + S2.Score.isEmpty() && S2.FlagOrBallOwner == null + ) + Check( + "切片 BattleStatistics:ContinueBattle / ResetRoundState", + ReduceBattleStatistics(S2, ContinueBattle).IsContinueBattle && + !ReduceBattleStatistics(S2, ResetRoundState).RoundStarted + ) + Check( + "切片 BattleStatistics:BattleLoaded 两态与 levelsCount", + ReduceBattleStatistics(S2, MarkBattleLoaded).BattleLoaded && + !ReduceBattleStatistics(S2, MarkBattleUnloaded).BattleLoaded && + ReduceBattleStatistics(S2, InitBattleModeWithLevels(4)) + .GameModeLevelsCount == 4 + ) + Check( + "切片 BattleStatistics:ResetBattleStatistics 整片复位", + ReduceBattleStatistics(S2, ResetBattleStatistics) == BattleStatisticsStateSlice() + ) + Check( + "切片 BattleStatistics:无关动作不改状态", + ReduceBattleStatistics(S2, AddFrags(1)) == S2 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, MarkBattleLoaded) + Check( + "根:MarkBattleLoaded 只动 battleStatistics", + Root1.BattleStatistics.BattleLoaded && + Root1.BattleSearchState == Root0.BattleSearchState + ) + val W = RootWiring.ByField("battleStatistics")!! + Check( + "根:battleStatistics 委派到 jR / rr.fqr 且 22 个动作", + W.SliceClass == "jR" && W.Reducer == "rr.fqr" && W.Actions.size == 22 + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)