From 9cf760b354c8a632f6f37de270c1126ddf803898 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 24 Oct 2026 19:46:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(state):=20=E8=BF=81=E7=A7=BB=20battleMessa?= =?UTF-8?q?ge=20=E5=B9=B6=E8=A1=A5=E6=88=98=E6=96=97=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3DTank/docs/KtRebuild_Plan.md | 16 +- 3DTank/names/root_wiring.json | 17 ++- 3DTank/tools/gen_root_wiring.js | 4 + .../State/Reducers/BattleMessageReducer.kt | 142 ++++++++++++++++++ .../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 | 88 ++++++++++- 8 files changed, 261 insertions(+), 22 deletions(-) create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleMessageReducer.kt diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 3726903..3913e47 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` 动作名。 - - 已迁移切片 **75/89**: + - 已迁移切片 **76/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) @@ -379,12 +379,17 @@ `SetParts` 把零件表拆成 `hulls`(hullId 列表)与 `weapons`(index → weaponIds)、并写 `paints`; `SetPartsInfo` 按零件 / 涂装各自的 id 建索引;零件模型跨模块,收口为 `TankBuilderPart` / `TankBuilderPartInfo` / `TankBuilderPaintInfo`;`Trt`→`ResetTankBuilder` 整片复位。 + - `battleMessage` → `FE` / `vT().rqb`(`BattleMessageReducer.kt`):1 字段,7 个分支。 + 四种消息模型按源码 `toString` 原样收口:`TextMessage(type, hideTimeMs, text, isHidden)`、 + `LocalizedMessage(…, content, arguments, isHidden)`、`LocalizedUserMessage(…, content, userTeamRelation, userUid)`、 + `LocalizedTeamMessage(…, content, teamRelation)`;`HideMessage` 只把 `hideTimeMs` 匹配的消息标隐藏, + `RemoveMessage` 删掉匹配项,`sT`→`ResetBattleMessages` 整片复位。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 74 个有 reducer 的切片, - `launchParams` 与其余 **14 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 75/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **823 项全绿**,含 89 槽位完整性、74 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 75 个有 reducer 的切片, + `launchParams` 与其余 **13 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 76/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **832 项全绿**,含 89 槽位完整性、75 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); @@ -411,6 +416,7 @@ (`Bet`/`v2`/`B2`/`u2`/`K0`/`C7` 共 6 个)。 contractState 的 `Fnt`/`Hnt` 也已登记(`Fnt` 因与任务切片同名改名为 `ClaimContractReward`); tankBuilderState 的 `Trt`(`ResetTankBuilder`)同样按效果定名。 + battleMessage 的 `sT`(`ResetBattleMessages`)也已登记。 ### 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 3a849fb..3756ae1 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T09:32:15.650Z", + "generatedAt": "2026-10-24T11:41:22.721Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -81,7 +81,8 @@ "controlSettings", "battleSettings", "contractState", - "tankBuilderState" + "tankBuilderState", + "battleMessage" ], "AnonymousActions": [ { @@ -1374,6 +1375,12 @@ "Name": "ResetTankBuilder", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "new brt() 整片复位" + }, + { + "Local": "sT", + "Name": "ResetBattleMessages", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "new FE() 整片复位" } ], "Markers": [ @@ -2464,16 +2471,16 @@ "Reducer": "vT().rqb", "Delegate": "vT().rqb(t, n.qpw_1)", "Actions": [ - "?sT", "DoAddLocalizedMessage", "DoAddLocalizedTeamMessage", "DoAddLocalizedUserMessage", "DoAddTextMessage", "HideMessage", - "RemoveMessage" + "RemoveMessage", + "ResetBattleMessages" ], "Default": "new FE()", - "Migrated": false + "Migrated": true }, { "Field": "battleChat", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 48f47af..7cef127 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -316,6 +316,8 @@ const AnonymousActionNames = new Map([ ['Hnt', 'RemoveCompletedContracts'], // tankBuilderState 切片(brt / Irt.ati):Trt 没有 toString。 ['Trt', 'ResetTankBuilder'], + // battleMessage 切片(FE / fT.rqb):sT 没有 toString。 + ['sT', 'ResetBattleMessages'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -792,6 +794,7 @@ const Migrated = { battleSettings: { Type: 'BattleSettingsStateSlice', Reduce: 'ReduceBattleSettings' }, contractState: { Type: 'ContractStateSlice', Reduce: 'ReduceContractState' }, tankBuilderState: { Type: 'TankBuilderStateSlice', Reduce: 'ReduceTankBuilder' }, + battleMessage: { Type: 'BattleMessageStateSlice', Reduce: 'ReduceBattleMessage' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -1198,6 +1201,7 @@ const AppSettingsAnonymousEffects = { Fnt: '复制该合同并把 rewards 清空(原名 GetReward,与任务切片同名)', Hnt: '只保留 rewards 非空的合同', Trt: 'new brt() 整片复位', + sT: 'new FE() 整片复位', }; fs.writeFileSync( diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleMessageReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleMessageReducer.kt new file mode 100644 index 0000000..c9814d1 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleMessageReducer.kt @@ -0,0 +1,142 @@ +// BattleMessageReducer.kt —— 战斗消息切片(对应 module_10032 的 FE 状态 + fT.rqb) +// +// 证据链: +// 状态类 FE:1 个字段,toString 名 "BattleMessageState(messages=…)"。 +// 默认:messages = emptyList()。 +// 根委派:`vT().rqb(t, n.qpw_1)`。 +// 消息模型(四种,toString 给出完整字段名): +// HE TextMessage(type, hideTimeMs, text, isHidden) +// GE LocalizedMessage(type, hideTimeMs, content, arguments, isHidden) +// WE LocalizedUserMessage(type, hideTimeMs, isHidden, content, userTeamRelation, userUid) +// VE LocalizedTeamMessage(type, hideTimeMs, isHidden, content, teamRelation) +// reducer(6 个实例臂): +// aT(DoAddTextMessage) → 追加 TextMessage +// oT(DoAddLocalizedMessage) → 追加 LocalizedMessage +// _T(DoAddLocalizedUserMessage) → 追加 LocalizedUserMessage +// hT(DoAddLocalizedTeamMessage) → 追加 LocalizedTeamMessage +// eT(HideMessage) → 把 hideTimeMs 匹配的消息标成 isHidden=true +// uT(RemoveMessage) → 删掉 hideTimeMs 匹配的消息 +// sT(无 toString) → `new FE()` 整片复位 +package App.State.Reducers + +import App.State.DoAddLocalizedMessage +import App.State.DoAddLocalizedTeamMessage +import App.State.DoAddLocalizedUserMessage +import App.State.DoAddTextMessage +import App.State.HideMessage +import App.State.RemoveMessage + +/* ------------------------------------------------------------------ 消息模型 */ + +sealed interface BattleMessageEntry { + val Type: Any? + val HideTimeMs: Any? + val IsHidden: Boolean + + fun WithHidden(Value: Boolean): BattleMessageEntry +} + +data class TextMessage( + override val Type: Any?, + override val HideTimeMs: Any?, + val Text: String, + override val IsHidden: Boolean = false, +) : BattleMessageEntry { + override fun WithHidden(Value: Boolean) = copy(IsHidden = Value) +} + +data class LocalizedMessage( + override val Type: Any?, + override val HideTimeMs: Any?, + val Content: Any?, + val Arguments: List = emptyList(), + override val IsHidden: Boolean = false, +) : BattleMessageEntry { + override fun WithHidden(Value: Boolean) = copy(IsHidden = Value) +} + +data class LocalizedUserMessage( + override val Type: Any?, + override val HideTimeMs: Any?, + override val IsHidden: Boolean = false, + val Content: Any?, + val UserTeamRelation: Any?, + val UserUid: Any?, +) : BattleMessageEntry { + override fun WithHidden(Value: Boolean) = copy(IsHidden = Value) +} + +data class LocalizedTeamMessage( + override val Type: Any?, + override val HideTimeMs: Any?, + override val IsHidden: Boolean = false, + val Content: Any?, + val TeamRelation: Any?, +) : BattleMessageEntry { + override fun WithHidden(Value: Boolean) = copy(IsHidden = Value) +} + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `sT`:整片复位(`new FE()`)。 */ +object ResetBattleMessages + +/* ------------------------------------------------------------------ 状态 */ + +data class BattleMessageStateSlice( + val Messages: List = emptyList(), +) + +/* ------------------------------------------------------------------ reducer */ + +private fun HideByTime(Messages: List, HideTimeMs: Any?): List = + Messages.map { + val Entry = it as? BattleMessageEntry + if (Entry != null && Entry.HideTimeMs == HideTimeMs) Entry.WithHidden(true) else it + } + +fun ReduceBattleMessage( + State0: BattleMessageStateSlice, + Action: Any, +): BattleMessageStateSlice = when (Action) { + is DoAddTextMessage -> State0.copy( + Messages = State0.Messages + TextMessage( + Type = Action.type, + HideTimeMs = Action.hideTimeMs, + Text = Action.text?.toString() ?: "", + ), + ) + is DoAddLocalizedMessage -> State0.copy( + Messages = State0.Messages + LocalizedMessage( + Type = Action.type, + HideTimeMs = Action.hideTimeMs, + Content = Action.message, + Arguments = (Action.args as? List) ?: emptyList(), + ), + ) + is DoAddLocalizedUserMessage -> State0.copy( + Messages = State0.Messages + LocalizedUserMessage( + Type = Action.type, + HideTimeMs = Action.hideTimeMs, + Content = Action.message, + UserTeamRelation = Action.userTeamRelation, + UserUid = Action.userUid, + ), + ) + is DoAddLocalizedTeamMessage -> State0.copy( + Messages = State0.Messages + LocalizedTeamMessage( + Type = Action.type, + HideTimeMs = Action.hideTimeMs, + Content = Action.message, + TeamRelation = Action.teamRelation, + ), + ) + is HideMessage -> State0.copy(Messages = HideByTime(State0.Messages, Action.hideTimeMs)) + is RemoveMessage -> State0.copy( + Messages = State0.Messages.filterNot { + (it as? BattleMessageEntry)?.HideTimeMs == Action.hideTimeMs + }, + ) + is ResetBattleMessages -> BattleMessageStateSlice() + 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 484f4cb..01b0401 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 出新状态)。 -// 这里采用同一形状。已迁移的 75 个切片里,74 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 76 个切片里,75 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -77,9 +77,10 @@ // 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)`) +// battleMessage → ReduceBattleMessage(FE / vT().rqb,根委派 `vT().rqb(t, n.qpw_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 14 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 13 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -88,6 +89,7 @@ import App.State.Reducers.ReduceAccountSettings import App.State.Reducers.ReduceAnnouncement import App.State.Reducers.ReduceAntiAddiction import App.State.Reducers.ReduceBattleResult +import App.State.Reducers.ReduceBattleMessage import App.State.Reducers.ReduceBattlePause import App.State.Reducers.ReduceBattleSearch import App.State.Reducers.ReduceBattleSettings @@ -246,6 +248,7 @@ object RootReducer { BattleSettings = ReduceBattleSettings(State0.BattleSettings, Action), ContractState = ReduceContractState(State0.ContractState, Action), TankBuilderState = ReduceTankBuilder(State0.TankBuilderState, Action), + BattleMessage = ReduceBattleMessage(State0.BattleMessage, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 171eb03..82bb511 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -9,6 +9,7 @@ import App.State.Reducers.AnnouncementStateSlice import App.State.Reducers.AntiAddictionStateSlice import App.State.Reducers.AvailabilityByRankStateSlice import App.State.Reducers.BattleInviteStateSlice +import App.State.Reducers.BattleMessageStateSlice import App.State.Reducers.BattlePauseStateSlice import App.State.Reducers.BattleResultStateSlice import App.State.Reducers.BattleSearchStateSlice @@ -171,8 +172,8 @@ data class RootState( val ClanUserInfo: ClanUserInfoStateSlice = ClanUserInfoStateSlice(), /** 原 tankActionsLogState(vI / wi.gqe)。已迁移。 */ val TankActionsLogState: TankActionsLogStateSlice = TankActionsLogStateSlice(), - /** 原 battleMessage(FE / vT().rqb)。未迁移:原样透传。 */ - val BattleMessage: Any? = null, + /** 原 battleMessage(FE / vT().rqb)。已迁移。 */ + val BattleMessage: BattleMessageStateSlice = BattleMessageStateSlice(), /** 原 battleChat(Q6 / Hf.wtg)。未迁移:原样透传。 */ val BattleChat: Any? = null, /** 原 droppableGold(Vet / Wp.kth)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index 0955096..31b3526 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -58,7 +58,7 @@ object RootWiring { 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("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("battleMessage", "qpw_1", "FE", "vT().rqb", "vT().rqb(t, n.qpw_1)", listOf("DoAddLocalizedMessage", "DoAddLocalizedTeamMessage", "DoAddLocalizedUserMessage", "DoAddTextMessage", "HideMessage", "RemoveMessage", "ResetBattleMessages")), 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")), SliceWiring("home", "tpw_1", "s1", "(inline@home)", "(function (t, n) { return t instanceof o1 ? n.f7j(!0) : n; })(t, n.tpw_1)", listOf("ShowShopDiscountNotification")), @@ -106,7 +106,7 @@ object RootWiring { ) /** 已迁移(强类型;launchParams 为透传,不调用 reducer)的字段。 */ - val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "settings", "accountSettings", "proBattle", "language", "announcement", "launchParams", "news", "notifications", "quests", "lobbyLoader", "purchaseDialogState", "promoCode", "supportUrl", "rulesUpdated", "notificationByCountry", "availabilityByRank", "icons", "questCategories", "mainQuest", "securitySettings", "referralEvent", "tipsState", "rankUpDialogState", "battleResult", "captchaState", "modalScreen", "droppableGold", "eventGoods", "creatorTagState", "rewardedVideosState", "passwordChangeState", "system", "contextTutorial", "battleTutorial", "spectator", "twoStepVerificationState", "antiAddiction", "moderation", "migrationState", "register", "userReportsState", "friends", "suppliesState", "clanUserInfo", "battleInviteState", "partner", "userRenameState", "battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial", "clanChatState", "battleSearchState", "battleStatistics", "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState") + val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "settings", "accountSettings", "proBattle", "language", "announcement", "launchParams", "news", "notifications", "quests", "lobbyLoader", "purchaseDialogState", "promoCode", "supportUrl", "rulesUpdated", "notificationByCountry", "availabilityByRank", "icons", "questCategories", "mainQuest", "securitySettings", "referralEvent", "tipsState", "rankUpDialogState", "battleResult", "captchaState", "modalScreen", "droppableGold", "eventGoods", "creatorTagState", "rewardedVideosState", "passwordChangeState", "system", "contextTutorial", "battleTutorial", "spectator", "twoStepVerificationState", "antiAddiction", "moderation", "migrationState", "register", "userReportsState", "friends", "suppliesState", "clanUserInfo", "battleInviteState", "partner", "userRenameState", "battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial", "clanChatState", "battleSearchState", "battleStatistics", "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 519f43d..dd2b8e9 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt @@ -19,7 +19,7 @@ // System / ContextTutorial / BattleTutorial / Spectator / TwoStepVerification / AntiAddiction / // Moderation / MigrationState / Register / UserReports / // Friends / Supplies / ClanUserInfo / BattleInvite / Partner / UserRename / -// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder; +// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder / BattleMessage; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -716,6 +716,20 @@ import App.State.Reducers.TankBuilderPaintInfo import App.State.Reducers.TankBuilderPart import App.State.Reducers.TankBuilderPartInfo import App.State.Reducers.TankBuilderStateSlice +import App.State.DoAddLocalizedMessage +import App.State.DoAddLocalizedTeamMessage +import App.State.DoAddLocalizedUserMessage +import App.State.DoAddTextMessage +import App.State.HideMessage +import App.State.RemoveMessage +import App.State.Reducers.BattleMessageEntry +import App.State.Reducers.BattleMessageStateSlice +import App.State.Reducers.LocalizedMessage +import App.State.Reducers.LocalizedTeamMessage +import App.State.Reducers.LocalizedUserMessage +import App.State.Reducers.ReduceBattleMessage +import App.State.Reducers.ResetBattleMessages +import App.State.Reducers.TextMessage import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -1143,7 +1157,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 75 个已迁移切片(74 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 76 个已迁移切片(75 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1207,7 +1221,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 75 个已迁移字段,其余 14 个待迁", + "根:迁移表 = 76 个已迁移字段,其余 13 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1230,9 +1244,9 @@ object StateCheck { "clanChatState", "battleSearchState", "battleStatistics", "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", - "tankBuilderState", + "tankBuilderState", "battleMessage", ) && - RootWiring.Unmigrated.size == 14 + RootWiring.Unmigrated.size == 13 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1288,7 +1302,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 75/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 76/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -6273,6 +6287,68 @@ object StateCheck { ) } + // 86 切片 reducer:BattleMessage(FE / vT().rqb,1 字段;7 个动作) + run { + val S0 = BattleMessageStateSlice() + Check("切片 BattleMessage:默认空消息表", S0.Messages.isEmpty()) + val S1 = ReduceBattleMessage(S0, DoAddTextMessage("info", 100L, "hello")) + Check( + "切片 BattleMessage:DoAddTextMessage 追加 TextMessage", + S1.Messages.size == 1 && + (S1.Messages[0] as TextMessage).Text == "hello" && + (S1.Messages[0] as TextMessage).HideTimeMs == 100L + ) + val S2 = ReduceBattleMessage( + ReduceBattleMessage( + ReduceBattleMessage(S1, DoAddLocalizedMessage("sys", 200L, "KILL", listOf("a"))), + DoAddLocalizedUserMessage("enemy", "user", 300L, "MSG", "uid1"), + ), + DoAddLocalizedTeamMessage("team", 400L, "TEAM", "red"), + ) + Check( + "切片 BattleMessage:四种消息按各自字段入表", + S2.Messages.size == 4 && + (S2.Messages[1] as LocalizedMessage).Content == "KILL" && + (S2.Messages[1] as LocalizedMessage).Arguments == listOf("a") && + (S2.Messages[2] as LocalizedUserMessage).UserTeamRelation == "enemy" && + (S2.Messages[2] as LocalizedUserMessage).UserUid == "uid1" && + (S2.Messages[3] as LocalizedTeamMessage).TeamRelation == "red" + ) + val S3 = ReduceBattleMessage(S2, HideMessage(100L)) + Check( + "切片 BattleMessage:HideMessage 只把匹配 hideTimeMs 的标隐藏", + (S3.Messages[0] as TextMessage).IsHidden && + !(S3.Messages[1] as LocalizedMessage).IsHidden + ) + val S4 = ReduceBattleMessage(S3, RemoveMessage(200L)) + Check( + "切片 BattleMessage:RemoveMessage 删掉匹配的那条", + S4.Messages.size == 3 && + S4.Messages.none { (it as BattleMessageEntry).HideTimeMs == 200L } + ) + Check( + "切片 BattleMessage:ResetBattleMessages 整片复位", + ReduceBattleMessage(S4, ResetBattleMessages) == BattleMessageStateSlice() + ) + Check( + "切片 BattleMessage:无关动作不改状态", + ReduceBattleMessage(S4, AddFrags(1)) == S4 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, DoAddTextMessage("info", 1L, "x")) + Check( + "根:DoAddTextMessage 只动 battleMessage", + Root1.BattleMessage.Messages.size == 1 && + Root1.TankBuilderState == Root0.TankBuilderState + ) + val W = RootWiring.ByField("battleMessage")!! + Check( + "根:battleMessage 委派到 FE / vT().rqb 且 7 个动作", + W.SliceClass == "FE" && W.Reducer == "vT().rqb" && W.Actions.size == 7 + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)