From 5327312b13b1b8cb5c56bc48359f597db37963ca Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 24 Oct 2026 20:08:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(state):=20=E8=BF=81=E7=A7=BB=20battleChat?= =?UTF-8?q?=EF=BC=88=E5=A4=8D=E7=94=A8=20ChatMessage=20=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=EF=BC=89?= 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 | 41 +++++++-- 3DTank/tools/gen_root_wiring.js | 10 +++ .../State/Reducers/BattleChatStateReducer.kt | 74 ++++++++++++++++ .../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 | 84 +++++++++++++++++-- 8 files changed, 214 insertions(+), 25 deletions(-) create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleChatStateReducer.kt diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index b6a62dd..3145e24 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` 动作名。 - - 已迁移切片 **77/89**: + - 已迁移切片 **78/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) @@ -388,12 +388,16 @@ `AddMap` 的动作载荷就是生成物里的 `MapInfoCC`(11 个字段,与源码 properties 一致), 直接复用;reducer 以 `info.mapId` 为键新建 / 复用 `MapsDescriptionEntry`, 再把 `MapsThemeEntry` 写进 `themes[info.theme]`(同地图多主题是合并)。 + - `battleChat` → `Q6` / `Hf.wtg`(`BattleChatStateReducer.kt`):6 字段,6 个分支。 + `ChatLoaded` 写两个频道开关并置 `loaded`;`AddMessage` 只保留最后 120 条再追加; + `ett`/`utt` 开关输入框、`stt` 取反历史面板、`rtt`→`ResetBattleChat` 整片复位。 + 消息类型直接复用生成物里的协议模型 `ChatMessage`(13 字段)。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 76 个有 reducer 的切片, - `launchParams` 与其余 **12 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 77/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **838 项全绿**,含 89 槽位完整性、76 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 77 个有 reducer 的切片, + `launchParams` 与其余 **11 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 78/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **848 项全绿**,含 89 槽位完整性、77 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); diff --git a/3DTank/names/root_wiring.json b/3DTank/names/root_wiring.json index 120ae7d..1114727 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T11:49:38.715Z", + "generatedAt": "2026-10-24T12:02:07.703Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -83,7 +83,8 @@ "contractState", "tankBuilderState", "battleMessage", - "maps" + "maps", + "battleChat" ], "AnonymousActions": [ { @@ -1387,6 +1388,30 @@ "Local": "FA", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "按 info.mapId 新建/复用 MapDescription,并把 MapInfo 写进 themes[themeId](原名 AddMap)" + }, + { + "Local": "ett", + "Name": "OpenBattleChatInput", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isInputActive=true(`n.ctl(cd, cd, cd, cd, !0)`)" + }, + { + "Local": "utt", + "Name": "CloseBattleChatInput", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isInputActive=false(`n.ctl(cd, cd, cd, cd, !1)`)" + }, + { + "Local": "stt", + "Name": "ToggleBattleChatHistory", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isHistoryOpen=!isHistoryOpen(`n.ctl(cd, cd, cd, cd, cd, !n.atl_1)`)" + }, + { + "Local": "rtt", + "Name": "ResetBattleChat", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "new Q6() 整片复位" } ], "Markers": [ @@ -2495,15 +2520,15 @@ "Reducer": "Hf.wtg", "Delegate": "Hf.wtg(t, n.rpw_1)", "Actions": [ - "?ett", - "?rtt", - "?stt", - "?utt", "AddMessage", - "ChatLoaded" + "ChatLoaded", + "CloseBattleChatInput", + "OpenBattleChatInput", + "ResetBattleChat", + "ToggleBattleChatHistory" ], "Default": "new Q6()", - "Migrated": false + "Migrated": true }, { "Field": "droppableGold", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index c23021d..6cadd04 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -318,6 +318,11 @@ const AnonymousActionNames = new Map([ ['Trt', 'ResetTankBuilder'], // battleMessage 切片(FE / fT.rqb):sT 没有 toString。 ['sT', 'ResetBattleMessages'], + // battleChat 切片(Q6 / htt.wtg):ett/utt/stt/rtt 没有 toString。 + ['ett', 'OpenBattleChatInput'], + ['utt', 'CloseBattleChatInput'], + ['stt', 'ToggleBattleChatHistory'], + ['rtt', 'ResetBattleChat'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -796,6 +801,7 @@ const Migrated = { tankBuilderState: { Type: 'TankBuilderStateSlice', Reduce: 'ReduceTankBuilder' }, battleMessage: { Type: 'BattleMessageStateSlice', Reduce: 'ReduceBattleMessage' }, maps: { Type: 'MapsStateSlice', Reduce: 'ReduceMaps' }, + battleChat: { Type: 'BattleChatStateSlice', Reduce: 'ReduceBattleChat' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -1204,6 +1210,10 @@ const AppSettingsAnonymousEffects = { Trt: 'new brt() 整片复位', sT: 'new FE() 整片复位', FA: '按 info.mapId 新建/复用 MapDescription,并把 MapInfo 写进 themes[themeId](原名 AddMap)', + ett: 'isInputActive=true(`n.ctl(cd, cd, cd, cd, !0)`)', + utt: 'isInputActive=false(`n.ctl(cd, cd, cd, cd, !1)`)', + stt: 'isHistoryOpen=!isHistoryOpen(`n.ctl(cd, cd, cd, cd, cd, !n.atl_1)`)', + rtt: 'new Q6() 整片复位', }; fs.writeFileSync( diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleChatStateReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleChatStateReducer.kt new file mode 100644 index 0000000..bdf3bd9 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleChatStateReducer.kt @@ -0,0 +1,74 @@ +// BattleChatStateReducer.kt —— 战斗聊天切片(对应 module_10032 的 Q6 状态 + htt.wtg) +// +// 证据链: +// 状态类 Q6:6 个字段,toString 名 "BattleChatState(…)"。 +// 默认:commonBattleChatEnabled=true、teamBattleChatEnabled=true、loaded=false、 +// messages=emptyList()、isInputActive=false、isHistoryOpen=false。 +// 根委派:`Hf.wtg(t, n.rpw_1)`。 +// reducer(6 个实例臂): +// itt(ChatLoaded) → 两个频道开关 + loaded=true +// ntt(AddMessage) → messages 只保留最后 120 条再追加(`r.q1(max(size-120,0), size)`) +// ett(无 toString) → isInputActive=true +// utt(无 toString) → isInputActive=false +// stt(无 toString) → isHistoryOpen 取反 +// rtt(无 toString) → `new Q6()` 整片复位 +// +// 消息类型复用生成物里的协议模型 `ChatMessage`(addressMode / channel / id / messageType / +// sourceUser / text / userId / timePassedInSec 等 13 个字段)。 +package App.State.Reducers + +import App.State.AddMessage +import App.State.ChatLoaded + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `ett`:打开输入框。 */ +object OpenBattleChatInput + +/** 原 `utt`:收起输入框。 */ +object CloseBattleChatInput + +/** 原 `stt`:切换历史记录面板。 */ +object ToggleBattleChatHistory + +/** 原 `rtt`:整片复位(`new Q6()`)。 */ +object ResetBattleChat + +/* ------------------------------------------------------------------ 状态 */ + +data class BattleChatStateSlice( + val CommonBattleChatEnabled: Boolean = true, + val TeamBattleChatEnabled: Boolean = true, + val Loaded: Boolean = false, + val Messages: List = emptyList(), + val IsInputActive: Boolean = false, + val IsHistoryOpen: Boolean = false, +) + +/* ------------------------------------------------------------------ reducer */ + +/** 原 `r.q1(max(0, size - 120), size)`:只保留最后 120 条。 */ +private const val MaxMessages = 120 + +private fun AppendMessage(Current: List, Message: Any?): List { + val Added = Current + Message + return if (Added.size <= MaxMessages) Added + else Added.subList(Added.size - MaxMessages, Added.size) +} + +fun ReduceBattleChat( + State0: BattleChatStateSlice, + Action: Any, +): BattleChatStateSlice = when (Action) { + is ChatLoaded -> State0.copy( + CommonBattleChatEnabled = Action.commonBattleChatEnabled, + TeamBattleChatEnabled = Action.teamBattleChatEnabled, + Loaded = true, + ) + is AddMessage -> State0.copy(Messages = AppendMessage(State0.Messages, Action.message)) + is OpenBattleChatInput -> State0.copy(IsInputActive = true) + is CloseBattleChatInput -> State0.copy(IsInputActive = false) + is ToggleBattleChatHistory -> State0.copy(IsHistoryOpen = !State0.IsHistoryOpen) + is ResetBattleChat -> BattleChatStateSlice() + 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 6a9401a..34332d0 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 出新状态)。 -// 这里采用同一形状。已迁移的 77 个切片里,76 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 78 个切片里,77 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -79,9 +79,10 @@ // tankBuilderState → ReduceTankBuilder(brt / wp.ati,根委派 `wp.ati(t, n.epy_1)`) // battleMessage → ReduceBattleMessage(FE / vT().rqb,根委派 `vT().rqb(t, n.qpw_1)`) // maps → ReduceMaps(OA / Oi.sql,根委派 `Oi.sql(t, n.bpw_1)`) +// battleChat → ReduceBattleChat(Q6 / Hf.wtg,根委派 `Hf.wtg(t, n.rpw_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 12 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 11 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -90,6 +91,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.ReduceBattleChat import App.State.Reducers.ReduceBattleMessage import App.State.Reducers.ReduceBattlePause import App.State.Reducers.ReduceBattleSearch @@ -252,6 +254,7 @@ object RootReducer { TankBuilderState = ReduceTankBuilder(State0.TankBuilderState, Action), BattleMessage = ReduceBattleMessage(State0.BattleMessage, Action), Maps = ReduceMaps(State0.Maps, Action), + BattleChat = ReduceBattleChat(State0.BattleChat, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index c61be38..cccce04 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -8,6 +8,7 @@ import App.State.Reducers.AccountSettingsStateSlice import App.State.Reducers.AnnouncementStateSlice import App.State.Reducers.AntiAddictionStateSlice import App.State.Reducers.AvailabilityByRankStateSlice +import App.State.Reducers.BattleChatStateSlice import App.State.Reducers.BattleInviteStateSlice import App.State.Reducers.BattleMessageStateSlice import App.State.Reducers.BattlePauseStateSlice @@ -175,8 +176,8 @@ data class RootState( val TankActionsLogState: TankActionsLogStateSlice = TankActionsLogStateSlice(), /** 原 battleMessage(FE / vT().rqb)。已迁移。 */ val BattleMessage: BattleMessageStateSlice = BattleMessageStateSlice(), - /** 原 battleChat(Q6 / Hf.wtg)。未迁移:原样透传。 */ - val BattleChat: Any? = null, + /** 原 battleChat(Q6 / Hf.wtg)。已迁移。 */ + val BattleChat: BattleChatStateSlice = BattleChatStateSlice(), /** 原 droppableGold(Vet / Wp.kth)。已迁移。 */ val DroppableGold: DroppableGoldStateSlice = DroppableGoldStateSlice(), /** 原 home(s1 / (inline@home))。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index 8683db2..56ae4c4 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -59,7 +59,7 @@ object RootWiring { 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("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("battleChat", "rpw_1", "Q6", "Hf.wtg", "Hf.wtg(t, n.rpw_1)", listOf("AddMessage", "ChatLoaded", "CloseBattleChatInput", "OpenBattleChatInput", "ResetBattleChat", "ToggleBattleChatHistory")), 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")), SliceWiring("localBattleUserState", "upw_1", "lE", "(inline@localBattleUserState)", "(function (t, n) { return t instanceof fE ? n.xq8(t.yq8_1) : t instanceof vE ? n.xq8(cd, t.zq8_1) : n; })(t, n.upw_1)", listOf("SetLocalWeaponResistanceProperty", "SetReArmorTemporaryDisabled")), @@ -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", "battleMessage", "maps") + 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", "maps", "battleChat") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 cdcbd96..8752265 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 / BattleMessage / Maps; +// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder / BattleMessage / Maps / BattleChat; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -736,6 +736,15 @@ import App.State.Reducers.MapsDescriptionEntry import App.State.Reducers.MapsStateSlice import App.State.Reducers.MapsThemeEntry import App.State.Reducers.ReduceMaps +import App.State.AddMessage +import App.State.ChatLoaded +import App.State.ChatMessage +import App.State.Reducers.BattleChatStateSlice +import App.State.Reducers.CloseBattleChatInput +import App.State.Reducers.OpenBattleChatInput +import App.State.Reducers.ReduceBattleChat +import App.State.Reducers.ResetBattleChat +import App.State.Reducers.ToggleBattleChatHistory import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -1163,7 +1172,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 77 个已迁移切片(76 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 78 个已迁移切片(77 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1227,7 +1236,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 77 个已迁移字段,其余 12 个待迁", + "根:迁移表 = 78 个已迁移字段,其余 11 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1250,9 +1259,9 @@ object StateCheck { "clanChatState", "battleSearchState", "battleStatistics", "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", - "tankBuilderState", "battleMessage", "maps", + "tankBuilderState", "battleMessage", "maps", "battleChat", ) && - RootWiring.Unmigrated.size == 12 + RootWiring.Unmigrated.size == 11 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1308,7 +1317,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 77/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 78/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -6397,6 +6406,69 @@ object StateCheck { ) } + // 88 切片 reducer:BattleChat(Q6 / Hf.wtg,6 字段;6 个动作) + run { + fun Msg(Id: Int): ChatMessage = ChatMessage( + addressMode = "", battleLinks = emptyList(), channel = "battle", id = Id, + links = emptyList(), messageType = "text", sourceUser = "u", ip = "", + uid = null, userId = "1", targetUser = null, text = "m$Id", timePassedInSec = 0, + ) + + val S0 = BattleChatStateSlice() + Check( + "切片 BattleChat:默认两个频道开、未加载", + S0.CommonBattleChatEnabled && S0.TeamBattleChatEnabled && !S0.Loaded && + S0.Messages.isEmpty() && !S0.IsInputActive && !S0.IsHistoryOpen + ) + val S1 = ReduceBattleChat(S0, ChatLoaded(false, true)) + Check( + "切片 BattleChat:ChatLoaded 写频道开关并置 loaded", + !S1.CommonBattleChatEnabled && S1.TeamBattleChatEnabled && S1.Loaded + ) + val S2 = ReduceBattleChat(S1, AddMessage(Msg(1))) + Check( + "切片 BattleChat:AddMessage 追加消息", + S2.Messages.size == 1 && (S2.Messages[0] as ChatMessage).id == 1 + ) + var S3 = S2 + for (I in 2..130) S3 = ReduceBattleChat(S3, AddMessage(Msg(I))) + Check( + "切片 BattleChat:超过 120 条只保留最后 120", + S3.Messages.size == 120 && + (S3.Messages.last() as ChatMessage).id == 130 + ) + Check( + "切片 BattleChat:输入框开关两态", + ReduceBattleChat(S3, OpenBattleChatInput).IsInputActive && + !ReduceBattleChat(S3, CloseBattleChatInput).IsInputActive + ) + Check( + "切片 BattleChat:ToggleBattleChatHistory 取反", + ReduceBattleChat(S3, ToggleBattleChatHistory).IsHistoryOpen + ) + Check( + "切片 BattleChat:ResetBattleChat 整片复位", + ReduceBattleChat(S3, ResetBattleChat) == BattleChatStateSlice() + ) + Check( + "切片 BattleChat:无关动作不改状态", + ReduceBattleChat(S3, AddFrags(1)) == S3 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, OpenBattleChatInput) + Check( + "根:OpenBattleChatInput 只动 battleChat", + Root1.BattleChat.IsInputActive && Root1.Maps == Root0.Maps + ) + val W = RootWiring.ByField("battleChat")!! + Check( + "根:battleChat 委派到 Q6 / Hf.wtg 且 6 个动作", + W.SliceClass == "Q6" && W.Reducer == "Hf.wtg" && + W.Actions.size == 6 + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)