diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 0174887..3822546 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` 动作名。 - - 已迁移切片 **66/89**: + - 已迁移切片 **70/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) @@ -350,12 +350,23 @@ - `battleStatistics` → `jR` / `rr.fqr`(`BattleStatisticsReducer.kt`):27 字段,22 个分支。 `FR`(14 字段的 Init)与通知切片的同名类冲突,改名为 `InitBattle`(contractState 之后复用); 其余为战斗元信息 setter + 回合 / 统计面板 / 菜单 / 旗主 / 加载位;`SetRemainingTime` 用 `$m` 把时长转秒。 + - `soundSettings` → `Det` / `Wet().jth`(`SoundSettingsReducer.kt`):10 字段,4 个分支。 + 音量缺省 100/100/50/50/50/50/50,三个开关 false/false/true;`Bet`/`Fet` 都改名为 + `ApplySoundSettings` / `SetEntranceSoundSettings`(原名都是 `UpdateSettings`,与其它设置切片同名)。 + storage 读写属 storage port,`_C`→`ResetSettingsFromStorage` 保留现有状态。 + - `gameSettings` → `c2` / `g2().zss`(`GameSettingsReducer.kt`):6 字段,2 个分支。 + 五个显示开关缺省 true、`mobilePadding=null`;`v2` 同样因 `UpdateSettings` 重名改名为 `ApplyGameSettings`。 + - `keyboardSettings` → `Sut` / `bw.oth`(`KeyboardSettingsReducer.kt`):1 字段 `Map>`,5 个分支。 + `ChangeAction` 先按 keyState 从其它 index 解绑再写入新绑定;`RemoveAction` 解绑单个 index; + `ResetAction` 依赖外部 helper,纯 reducer 保留原状态;两个默认复位清空绑定表。 + - `touchscreenHudSettings` → `j7` / `N7().xsv`(`TouchscreenHudSettingsReducer.kt`):1 字段,3 个分支。 + `C7`(原名 `UpdateSettings`)改名为 `ApplyTouchscreenHudSettings`;`E7`→`ResetTouchscreenHudMapping` 恢复默认布局。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 65 个有 reducer 的切片, - `launchParams` 与其余 **23 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 66/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **755 项全绿**,含 89 槽位完整性、65 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 69 个有 reducer 的切片, + `launchParams` 与其余 **19 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 70/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **787 项全绿**,含 89 槽位完整性、69 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); @@ -377,6 +388,8 @@ battlePauseState 的 2 个、battleViewState 的 12 个、tankActionsLogState 的 1 个同样按效果定名。 clanChatState 的 3 个(`$nt`/`xnt`/`Tnt`)、battleSearchState 的 1 个也已登记。 battleStatistics 的 12 个(含改名的 `FR`→`InitBattle`)同样按效果定名。 + 设置类切片的 `_C`(共用重载)+ `Bet`/`Fet`/`Uet`/`v2`/`C7`/`E7`/`Iut` 也已登记, + 其中四个原名都是 `UpdateSettings` 的类按切片改名为 `Apply*`,避免生成物同名冲突。 ### 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 e72ea47..762d5d5 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T08:50:07.796Z", + "generatedAt": "2026-10-24T09:04:37.952Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -72,7 +72,11 @@ "suppliesTutorial", "clanChatState", "battleSearchState", - "battleStatistics" + "battleStatistics", + "soundSettings", + "gameSettings", + "keyboardSettings", + "touchscreenHudSettings" ], "AnonymousActions": [ { @@ -1281,6 +1285,54 @@ "Name": "ResetBattleStatistics", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "new jR() 整片复位" + }, + { + "Local": "_C", + "Name": "ResetSettingsFromStorage", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "各设置切片共用的重载动作:sound/game/control 读 localStorage,keyboard/touchscreen 恢复默认表" + }, + { + "Local": "Bet", + "Name": "ApplySoundSettings", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "听到设置整体替换(原名 UpdateSettings)" + }, + { + "Local": "Fet", + "Name": "SetEntranceSoundSettings", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "入口音效开关后整体替换设置(原名 SetEntranceSoundEnabled)" + }, + { + "Local": "Uet", + "Name": "EnableEntranceSound", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isEntranceSoundOn=true(存储缺省)" + }, + { + "Local": "v2", + "Name": "ApplyGameSettings", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "游戏内显示设置整体替换(原名 UpdateSettings)" + }, + { + "Local": "C7", + "Name": "ApplyTouchscreenHudSettings", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "触屏布局整体替换(原名 UpdateSettings)" + }, + { + "Local": "E7", + "Name": "ResetTouchscreenHudMapping", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "恢复默认触屏布局(`new j7()`)" + }, + { + "Local": "Iut", + "Name": "ResetKeyMappingFromDefaults", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "恢复键位默认绑定(`rut().mu7_1`)" } ], "Markers": [ @@ -1726,7 +1778,6 @@ "Actions": [ "?QP", "?XP", - "?_C", "?bN", "?fN", "?lN", @@ -1740,6 +1791,7 @@ "CleanUsersMessage", "Configure", "IgnoreUserMessages", + "ResetSettingsFromStorage", "SaveChatMessage", "SendMessageToServer", "SetMessages", @@ -2444,13 +2496,13 @@ "Reducer": "Wet().jth", "Delegate": "Wet().jth(t, n.vpw_1)", "Actions": [ - "?Uet", - "?_C", - "SetEntranceSoundEnabled", - "UpdateSettings" + "ApplySoundSettings", + "EnableEntranceSound", + "ResetSettingsFromStorage", + "SetEntranceSoundSettings" ], "Default": "new Det()", - "Migrated": false + "Migrated": true }, { "Field": "gameSettings", @@ -2459,11 +2511,11 @@ "Reducer": "g2().zss", "Delegate": "g2().zss(t, n.wpw_1)", "Actions": [ - "?_C", - "UpdateSettings" + "ApplyGameSettings", + "ResetSettingsFromStorage" ], "Default": "new c2()", - "Migrated": false + "Migrated": true }, { "Field": "battleSettings", @@ -2472,7 +2524,7 @@ "Reducer": "X0().esq", "Delegate": "X0().esq(t, n.xpw_1)", "Actions": [ - "?_C", + "ResetSettingsFromStorage", "UpdateSettings" ], "Default": "new W0()", @@ -2497,7 +2549,7 @@ "Reducer": "_2().ess", "Delegate": "_2().ess(t, n.zpw_1)", "Actions": [ - "?_C", + "ResetSettingsFromStorage", "UpdateSettings" ], "Default": "new i2()", @@ -2553,14 +2605,14 @@ "Reducer": "bw.oth", "Delegate": "bw.oth(t, n.dpx_1)", "Actions": [ - "?Iut", - "?_C", "ChangeAction", "RemoveAction", - "ResetAction" + "ResetAction", + "ResetKeyMappingFromDefaults", + "ResetSettingsFromStorage" ], "Default": "new Sut()", - "Migrated": false + "Migrated": true }, { "Field": "touchscreenHudSettings", @@ -2569,12 +2621,12 @@ "Reducer": "N7().xsv", "Delegate": "N7().xsv(t, n.epx_1)", "Actions": [ - "?E7", - "?_C", - "UpdateSettings" + "ApplyTouchscreenHudSettings", + "ResetSettingsFromStorage", + "ResetTouchscreenHudMapping" ], "Default": "new j7()", - "Migrated": false + "Migrated": true }, { "Field": "mainQuest", @@ -2864,10 +2916,10 @@ "Delegate": "A1().dso(t, n.xpx_1)", "Actions": [ "?S1", - "?_C", "AddItemNotification", "AddItemNotifications", "RemoveItemNotification", + "ResetSettingsFromStorage", "UpdateItemNotification" ], "Default": "new _1()", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 3f96037..28c4649 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -294,6 +294,19 @@ const AnonymousActionNames = new Map([ ['tP', 'MarkBattleLoaded'], ['nP', 'MarkBattleUnloaded'], ['HR', 'ResetBattleStatistics'], + // 设置类切片的共用匿名动作与同名类:_C 是各设置切片共用的「从存储重载」动作。 + ['_C', 'ResetSettingsFromStorage'], + // soundSettings 切片(Det / Het.jth):Bet/Fet 的 toString 名都是 UpdateSettings/…,与其它设置切片同名。 + ['Bet', 'ApplySoundSettings'], + ['Fet', 'SetEntranceSoundSettings'], + ['Uet', 'EnableEntranceSound'], + // gameSettings 切片(c2 / d2.zss):v2 的 toString 名也是 UpdateSettings。 + ['v2', 'ApplyGameSettings'], + // touchscreenHudSettings 切片(j7 / P7.xsv):C7 的 toString 名也是 UpdateSettings;E7 没有 toString。 + ['C7', 'ApplyTouchscreenHudSettings'], + ['E7', 'ResetTouchscreenHudMapping'], + // keyboardSettings 切片(Sut / Nut.oth):Iut 没有 toString。 + ['Iut', 'ResetKeyMappingFromDefaults'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -753,6 +766,13 @@ const Migrated = { clanChatState: { Type: 'ClanChatStateSlice', Reduce: 'ReduceClanChat' }, battleSearchState: { Type: 'BattleSearchStateSlice', Reduce: 'ReduceBattleSearch' }, battleStatistics: { Type: 'BattleStatisticsStateSlice', Reduce: 'ReduceBattleStatistics' }, + soundSettings: { Type: 'SoundSettingsStateSlice', Reduce: 'ReduceSoundSettings' }, + gameSettings: { Type: 'GameSettingsStateSlice', Reduce: 'ReduceGameSettings' }, + keyboardSettings: { Type: 'KeyboardSettingsStateSlice', Reduce: 'ReduceKeyboardSettings' }, + touchscreenHudSettings: { + Type: 'TouchscreenHudSettingsStateSlice', + Reduce: 'ReduceTouchscreenHudSettings', + }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -1143,6 +1163,18 @@ const BattleStatisticsAnonymousEffects = { HR: 'new jR() 整片复位', }; +// 设置类切片:_C 是共用的「从存储重载」,其余是各切片里与 UpdateSettings 同名的动作。 +const AppSettingsAnonymousEffects = { + _C: '各设置切片共用的重载动作:sound/game/control 读 localStorage,keyboard/touchscreen 恢复默认表', + Bet: '听到设置整体替换(原名 UpdateSettings)', + Fet: '入口音效开关后整体替换设置(原名 SetEntranceSoundEnabled)', + Uet: 'isEntranceSoundOn=true(存储缺省)', + v2: '游戏内显示设置整体替换(原名 UpdateSettings)', + C7: '触屏布局整体替换(原名 UpdateSettings)', + E7: '恢复默认触屏布局(`new j7()`)', + Iut: '恢复键位默认绑定(`rut().mu7_1`)', +}; + fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), JSON.stringify( @@ -1405,6 +1437,12 @@ fs.writeFileSync( DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', Effect, })), + ...Object.entries(AppSettingsAnonymousEffects).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/GameSettingsReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/GameSettingsReducer.kt new file mode 100644 index 0000000..19f2c43 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/GameSettingsReducer.kt @@ -0,0 +1,39 @@ +// GameSettingsReducer.kt —— 游戏内显示设置切片(对应 module_10032 的 c2 状态 + d2.zss) +// +// 证据链: +// 状态类 c2:6 个字段,toString 名 "GameSettings(…)"。 +// 默认:5 个显示开关全 true、mobilePadding=null。 +// 根委派:`g2().zss(t, n.wpw_1)`。 +// reducer: +// _C(ResetSettingsFromStorage)→ 从 localStorage 重新读 +// v2(UpdateSettings) → 整体替换设置对象 +// +// storage 读写属 storage port 职责:`_C` 保留现有状态,`v2` 用动作携带的设置对象替换。 +package App.State.Reducers + +/* ------------------------------------------------------------------ 动作 */ + +/** 原 `v2`(`UpdateSettings`,与其它设置切片同名):整体替换游戏内显示设置。 */ +data class ApplyGameSettings(val settings: GameSettingsStateSlice) + +/* ------------------------------------------------------------------ 状态 */ + +data class GameSettingsStateSlice( + val ShowDropZones: Boolean = true, + val ShowYourDrone: Boolean = true, + val ShowChat: Boolean = true, + val ShowNotifications: Boolean = true, + val ShowOtherDrone: Boolean = true, + val MobilePadding: Any? = null, +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceGameSettings( + State0: GameSettingsStateSlice, + Action: Any, +): GameSettingsStateSlice = when (Action) { + is ApplyGameSettings -> Action.settings + is ResetSettingsFromStorage -> State0 + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/KeyboardSettingsReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/KeyboardSettingsReducer.kt new file mode 100644 index 0000000..d873413 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/KeyboardSettingsReducer.kt @@ -0,0 +1,73 @@ +// KeyboardSettingsReducer.kt —— 键盘按键绑定切片(对应 module_10032 的 Sut 状态 + Nut.oth) +// +// 证据链: +// 状态类 Sut:1 个字段,toString 名 "KeyboardSettings(mapping=…)"。 +// 默认:mapping = rut().mu7_1(内置默认绑定表)。 +// 根委派:`bw.oth(t, n.dpx_1)`。 +// reducer: +// _C(ResetSettingsFromStorage)→ 恢复内置默认绑定(`Mut()`) +// Iut(无 toString) → 另一个默认来源(`rut().mu7_1`) +// Cut(ChangeAction) → 把 keyState 从其它 index 上解绑,再绑到 (action, index) +// Eut(RemoveAction) → 解绑 (action, index);action 不存在时 no-op +// Tut(ResetAction) → `Aut(...)`:把某个 action 的绑定重置为默认(外部 helper) +// +// mapping 形状:`Map>`(源码里外层 key 是 `v2()`、内层是 `Vd(index, keyState)`)。 +// storage / 输入服务的写回属 side effect,`Tut` 在纯 reducer 里保留原状态。 +package App.State.Reducers + +import App.State.ChangeAction +import App.State.RemoveAction +import App.State.ResetAction + +/* ------------------------------------------------------------------ 动作 */ + +/** 原 `Iut`:从另一个默认来源恢复绑定。 */ +object ResetKeyMappingFromDefaults + +/* ------------------------------------------------------------------ 状态 */ + +data class KeyboardSettingsStateSlice( + val Mapping: Map = emptyMap(), +) + +/* ------------------------------------------------------------------ reducer */ + +@Suppress("UNCHECKED_CAST") +private fun InnerOf(Mapping: Map, Action: Any?): Map = + Mapping[Action] as? Map ?: emptyMap() + +/** 原 `Cut` 的映射重建:每个 action 的绑定表先去掉占用同一 keyState 的项,再写入新绑定。 */ +private fun ChangeBinding( + Mapping: Map, + Action: ChangeAction, +): Map { + val Result = LinkedHashMap() + for ((ActionKey, Inner) in Mapping) { + val Kept = LinkedHashMap() + for ((Index, KeyState) in (Inner as? Map ?: emptyMap())) { + if (KeyState != Action.keyState) Kept[Index] = KeyState + } + Result[ActionKey] = Kept + } + val Target = Result[Action.action] as? Map ?: emptyMap() + Result[Action.action] = Target + (Action.index to Action.keyState) + return Result +} + +fun ReduceKeyboardSettings( + State0: KeyboardSettingsStateSlice, + Action: Any, +): KeyboardSettingsStateSlice = when (Action) { + // 两个默认来源在离线重建里都收口为空绑定表(storage/输入服务不在纯 reducer 内)。 + is ResetSettingsFromStorage -> KeyboardSettingsStateSlice() + is ResetKeyMappingFromDefaults -> KeyboardSettingsStateSlice() + is ChangeAction -> State0.copy(Mapping = ChangeBinding(State0.Mapping, Action)) + is RemoveAction -> { + if (State0.Mapping.containsKey(Action.action)) { + val Inner = InnerOf(State0.Mapping, Action.action) - Action.index + State0.copy(Mapping = State0.Mapping + (Action.action to Inner)) + } else State0 + } + is ResetAction -> State0 + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/SoundSettingsReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/SoundSettingsReducer.kt new file mode 100644 index 0000000..00042a4 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/SoundSettingsReducer.kt @@ -0,0 +1,58 @@ +// SoundSettingsReducer.kt —— 音量设置切片(对应 module_10032 的 Det 状态 + Het.jth) +// +// 证据链: +// 状态类 Det:10 个字段,toString 名 "SoundSettings(…)"。 +// 默认:master/ui=100,music/ambient/sfx/hitmarkPlayer/hitmarkEnemy=50, +// mute=false、muteInBackground=false、isEntranceSoundOn=true。 +// 根委派:`Wet().jth(t, n.vpw_1)`。 +// reducer: +// _C(ResetSettingsFromStorage) → 从 localStorage 重新读整套音量 +// Bet(UpdateSettings) → 整体替换设置对象 +// Uet(无 toString) → isEntranceSoundOn=true(从存储读默认) +// Fet(SetEntranceSoundEnabled) → 用动作里的设置对象替换(并写回存储) +// +// storage 相关读写属 storage port 职责,纯 reducer 不做 IO: +// `_C` 保留现有状态,`Bet`/`Fet` 用动作携带的设置对象整体替换。 +package App.State.Reducers + +/* ------------------------------------------------------------------ 动作 */ + +/** 原 `Bet`(`UpdateSettings`,与其它设置切片同名):整体替换音量设置。 */ +data class ApplySoundSettings(val settings: SoundSettingsStateSlice) + +/** 原 `Fet`(`SetEntranceSoundEnabled`):入口音效开关后整体替换设置。 */ +data class SetEntranceSoundSettings(val settings: SoundSettingsStateSlice) + +/** 原 `Uet`:把入口音效打开(存储缺省 true)。 */ +object EnableEntranceSound + +/** 原 `_C`:从存储重载设置;没有 storage port 时保留现有状态。 */ +object ResetSettingsFromStorage + +/* ------------------------------------------------------------------ 状态 */ + +data class SoundSettingsStateSlice( + val MasterVolume: Double = 100.0, + val UiVolume: Double = 100.0, + val MusicVolume: Double = 50.0, + val AmbientVolume: Double = 50.0, + val SfxVolume: Double = 50.0, + val HitmarkPlayerVolume: Double = 50.0, + val HitmarkEnemyVolume: Double = 50.0, + val Mute: Boolean = false, + val MuteInBackground: Boolean = false, + val IsEntranceSoundOn: Boolean = true, +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceSoundSettings( + State0: SoundSettingsStateSlice, + Action: Any, +): SoundSettingsStateSlice = when (Action) { + is ApplySoundSettings -> Action.settings + is SetEntranceSoundSettings -> Action.settings + is EnableEntranceSound -> State0.copy(IsEntranceSoundOn = true) + is ResetSettingsFromStorage -> State0 + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/TouchscreenHudSettingsReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/TouchscreenHudSettingsReducer.kt new file mode 100644 index 0000000..b1f787e --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/TouchscreenHudSettingsReducer.kt @@ -0,0 +1,39 @@ +// TouchscreenHudSettingsReducer.kt —— 触屏 HUD 布局切片(对应 module_10032 的 j7 状态 + P7.xsv) +// +// 证据链: +// 状态类 j7:1 个字段,toString 名 "TouchscreenHudSettings(mapping=…)"。 +// 默认:mapping = c7().rsv_1。 +// 根委派:`N7().xsv(t, n.epx_1)`。 +// reducer: +// _C(ResetSettingsFromStorage)→ 从存储读默认布局(`T7(this)`) +// C7(UpdateSettings) → 整体替换 mapping(并写回存储) +// E7(无 toString) → 恢复默认 mapping(`new j7()`) +// +// storage 读写属 storage port 职责;`_C` 与 `E7` 都收口为「恢复空 mapping」。 +package App.State.Reducers + +/* ------------------------------------------------------------------ 动作 */ + +/** 原 `C7`(`UpdateSettings`,与其它设置切片同名):整体替换触屏布局。 */ +data class ApplyTouchscreenHudSettings(val settings: TouchscreenHudSettingsStateSlice) + +/** 原 `E7`:恢复默认触屏布局。 */ +object ResetTouchscreenHudMapping + +/* ------------------------------------------------------------------ 状态 */ + +data class TouchscreenHudSettingsStateSlice( + val Mapping: Map = emptyMap(), +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceTouchscreenHudSettings( + State0: TouchscreenHudSettingsStateSlice, + Action: Any, +): TouchscreenHudSettingsStateSlice = when (Action) { + is ApplyTouchscreenHudSettings -> Action.settings + is ResetTouchscreenHudMapping -> TouchscreenHudSettingsStateSlice() + is ResetSettingsFromStorage -> TouchscreenHudSettingsStateSlice() + 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 3094e15..5d468ea 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 出新状态)。 -// 这里采用同一形状。已迁移的 66 个切片里,65 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 70 个切片里,69 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -68,9 +68,13 @@ // 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)`) +// soundSettings → ReduceSoundSettings(Det / Wet().jth,根委派 `Wet().jth(t, n.vpw_1)`) +// gameSettings → ReduceGameSettings(c2 / g2().zss,根委派 `g2().zss(t, n.wpw_1)`) +// keyboardSettings → ReduceKeyboardSettings(Sut / bw.oth,根委派 `bw.oth(t, n.dpx_1)`) +// touchscreenHudSettings → ReduceTouchscreenHudSettings(j7 / N7().xsv,根委派 `N7().xsv(t, n.epx_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 23 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 19 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -96,9 +100,11 @@ import App.State.Reducers.ReduceEventGoods import App.State.Reducers.ReduceGarage import App.State.Reducers.ReduceGaragePreview import App.State.Reducers.ReduceFriends +import App.State.Reducers.ReduceGameSettings import App.State.Reducers.ReduceHome import App.State.Reducers.ReduceIcons import App.State.Reducers.ReduceInvite +import App.State.Reducers.ReduceKeyboardSettings import App.State.Reducers.ReduceLanguage import App.State.Reducers.ReduceLocalBattleUser import App.State.Reducers.ReduceLoginByPassword @@ -130,11 +136,13 @@ import App.State.Reducers.ReduceSpectator import App.State.Reducers.ReduceSupportUrl import App.State.Reducers.ReduceSettings import App.State.Reducers.ReduceShop +import App.State.Reducers.ReduceSoundSettings 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.ReduceTouchscreenHudSettings import App.State.Reducers.ReduceTwitch import App.State.Reducers.ReduceTwoStepVerification import App.State.Reducers.ReduceUser @@ -219,6 +227,10 @@ object RootReducer { ClanChatState = ReduceClanChat(State0.ClanChatState, Action), BattleSearchState = ReduceBattleSearch(State0.BattleSearchState, Action), BattleStatistics = ReduceBattleStatistics(State0.BattleStatistics, Action), + SoundSettings = ReduceSoundSettings(State0.SoundSettings, Action), + GameSettings = ReduceGameSettings(State0.GameSettings, Action), + KeyboardSettings = ReduceKeyboardSettings(State0.KeyboardSettings, Action), + TouchscreenHudSettings = ReduceTouchscreenHudSettings(State0.TouchscreenHudSettings, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 7bb5999..2860b2e 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -24,11 +24,13 @@ import App.State.Reducers.DroppableGoldStateSlice import App.State.Reducers.EntranceStateSlice import App.State.Reducers.EventGoodsStateSlice import App.State.Reducers.FriendsStateSlice +import App.State.Reducers.GameSettingsStateSlice import App.State.Reducers.GarageStateSlice import App.State.Reducers.GaragePreviewStateSlice import App.State.Reducers.HomeStateSlice import App.State.Reducers.IconsStateSlice import App.State.Reducers.InviteStateSlice +import App.State.Reducers.KeyboardSettingsStateSlice import App.State.Reducers.LobbyLoaderStateSlice import App.State.Reducers.LocalBattleUserStateSlice import App.State.Reducers.LoginByPasswordStateSlice @@ -56,6 +58,7 @@ import App.State.Reducers.ScreenStateSlice import App.State.Reducers.SecuritySettingsStateSlice import App.State.Reducers.SettingsStateSlice import App.State.Reducers.ShopStateSlice +import App.State.Reducers.SoundSettingsStateSlice import App.State.Reducers.SpectatorStateSlice import App.State.Reducers.SuppliesStateSlice import App.State.Reducers.SuppliesTutorialStateSlice @@ -63,6 +66,7 @@ import App.State.Reducers.SupportUrlStateSlice import App.State.Reducers.SystemStateSlice import App.State.Reducers.TankActionsLogStateSlice import App.State.Reducers.TipsStateSlice +import App.State.Reducers.TouchscreenHudSettingsStateSlice import App.State.Reducers.TwitchStateSlice import App.State.Reducers.TwoStepVerificationStateSlice import App.State.Reducers.UserStateSlice @@ -172,10 +176,10 @@ data class RootState( val Home: HomeStateSlice = HomeStateSlice(), /** 原 localBattleUserState(lE / (inline@localBattleUserState))。已迁移。 */ val LocalBattleUserState: LocalBattleUserStateSlice = LocalBattleUserStateSlice(), - /** 原 soundSettings(Det / Wet().jth)。未迁移:原样透传。 */ - val SoundSettings: Any? = null, - /** 原 gameSettings(c2 / g2().zss)。未迁移:原样透传。 */ - val GameSettings: Any? = null, + /** 原 soundSettings(Det / Wet().jth)。已迁移。 */ + val SoundSettings: SoundSettingsStateSlice = SoundSettingsStateSlice(), + /** 原 gameSettings(c2 / g2().zss)。已迁移。 */ + val GameSettings: GameSettingsStateSlice = GameSettingsStateSlice(), /** 原 battleSettings(W0 / X0().esq)。未迁移:原样透传。 */ val BattleSettings: Any? = null, /** 原 graphicsSettings(? / lc.isu)。未迁移:原样透传。 */ @@ -188,10 +192,10 @@ data class RootState( val PurchaseDialogState: PurchaseDialogStateSlice = PurchaseDialogStateSlice(), /** 原 battleInviteState(att / Ttt().nth)。已迁移。 */ val BattleInviteState: BattleInviteStateSlice = BattleInviteStateSlice(), - /** 原 keyboardSettings(Sut / bw.oth)。未迁移:原样透传。 */ - val KeyboardSettings: Any? = null, - /** 原 touchscreenHudSettings(j7 / N7().xsv)。未迁移:原样透传。 */ - val TouchscreenHudSettings: Any? = null, + /** 原 keyboardSettings(Sut / bw.oth)。已迁移。 */ + val KeyboardSettings: KeyboardSettingsStateSlice = KeyboardSettingsStateSlice(), + /** 原 touchscreenHudSettings(j7 / N7().xsv)。已迁移。 */ + val TouchscreenHudSettings: TouchscreenHudSettingsStateSlice = TouchscreenHudSettingsStateSlice(), /** 原 mainQuest(WZ / ih.bsk)。已迁移。 */ val MainQuest: MainQuestStateSlice = MainQuestStateSlice(), /** 原 language(? / m6().jtg)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index be88b19..e3a7fb8 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -30,7 +30,7 @@ object RootWiring { SliceWiring("passwordChangeState", "lpv_1", "VD", "uu.pr6", "uu.pr6(t, n.lpv_1)", listOf("SetPasswordChangeHash", "ShowEmailAndPasswordChangeDialog")), SliceWiring("system", "mpv_1", "_9", "w9().xt9", "w9().xt9(t, n.mpv_1)", listOf("DoShowMessage", "SetFatalError")), SliceWiring("quests", "npv_1", "Frt", "Yrt.otg", "(null == zp && new Yrt(), zp).otg(t, n.npv_1)", listOf("GoTo", "SetCurrentCategory", "SetNotCompletedQuests")), - SliceWiring("chatState", "opv_1", "LP", "\$r.wqv", "\$r.wqv(t, n.opv_1)", listOf("?QP", "?XP", "?_C", "?bN", "?fN", "?lN", "?nN", "?pN", "?qN", "?sN", "?vN", "?wN", "ChangeChannel", "CleanUsersMessage", "Configure", "IgnoreUserMessages", "SaveChatMessage", "SendMessageToServer", "SetMessages", "SetSelectedUid", "ShowContextMenu", "ShowUserMessages", "UpdateTypingSpeedAntifloodParams", "UpdateUsers")), + SliceWiring("chatState", "opv_1", "LP", "\$r.wqv", "\$r.wqv(t, n.opv_1)", listOf("?QP", "?XP", "?bN", "?fN", "?lN", "?nN", "?pN", "?qN", "?sN", "?vN", "?wN", "ChangeChannel", "CleanUsersMessage", "Configure", "IgnoreUserMessages", "ResetSettingsFromStorage", "SaveChatMessage", "SendMessageToServer", "SetMessages", "SetSelectedUid", "ShowContextMenu", "ShowUserMessages", "UpdateTypingSpeedAntifloodParams", "UpdateUsers")), SliceWiring("clanChatState", "ppv_1", "qnt", "yv.qtg", "yv.qtg(t, n.ppv_1)", listOf("CloseClanChat", "Configure", "DoAddMessages", "OpenClanChat", "ResetClanChat")), SliceWiring("clan", "qpv_1", "jN", "we.hr0", "we.hr0(t, n.qpv_1)", listOf("?FM", "?IM", "?KM", "?PM", "?RM", "?VM", "?YM", "?iM", "AcceptedUserViewed", "AddIncomingClan", "AddNewIncoming", "AddNewIncomingClan", "AddOutgoingClan", "AddUsersToAccepted", "AddUsersToNewAccepted", "ClanBanned", "ClanInfoLoaded", "ClanLoaded", "ClanModalVisible", "IncomingAdded", "IncomingRemoved", "IncomingUserViewed", "MembersFetched", "NewAcceptedUserNotification", "NewClanInviteNotification", "NewIncomingUserNotification", "OutgoingAdded", "OutgoingRemoved", "RemoveIncomingClan", "RemoveNewIncoming", "RemoveNewIncomingClan", "RemoveOutgoingClan", "RemoveUserFromAccepted", "RemoveUsersFromNewAccepted", "SetClanEditPrice", "SetMinRankForCreate", "UpdateClanLicense", "UpdateClanNameValidation", "UpdateClanSearchValidation", "UpdateClanTagValidation", "UpdateHasClan", "UpdateTimeToJoinRestriction", "UpdateValidation")), SliceWiring("clansStorage", "rpv_1", "lO", "me.vr1", "me.vr1(t, n.rpv_1)", listOf("?mO", "AddUser", "AddUsersToNewAccepted", "BuyAdditionalPlace", "ClanLoaded", "ClanUnloaded", "ForeignClanLoaded", "MinimalRankChanged", "RecruitmentChanged", "RemoveUser", "RemoveUsersFromNewAccepted", "ShowForeignClan", "UpdateClan")), @@ -63,16 +63,16 @@ object RootWiring { 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")), - SliceWiring("soundSettings", "vpw_1", "Det", "Wet().jth", "Wet().jth(t, n.vpw_1)", listOf("?Uet", "?_C", "SetEntranceSoundEnabled", "UpdateSettings")), - SliceWiring("gameSettings", "wpw_1", "c2", "g2().zss", "g2().zss(t, n.wpw_1)", listOf("?_C", "UpdateSettings")), - SliceWiring("battleSettings", "xpw_1", "W0", "X0().esq", "X0().esq(t, n.xpw_1)", listOf("?_C", "UpdateSettings")), + SliceWiring("soundSettings", "vpw_1", "Det", "Wet().jth", "Wet().jth(t, n.vpw_1)", listOf("ApplySoundSettings", "EnableEntranceSound", "ResetSettingsFromStorage", "SetEntranceSoundSettings")), + SliceWiring("gameSettings", "wpw_1", "c2", "g2().zss", "g2().zss(t, n.wpw_1)", listOf("ApplyGameSettings", "ResetSettingsFromStorage")), + SliceWiring("battleSettings", "xpw_1", "W0", "X0().esq", "X0().esq(t, n.xpw_1)", listOf("ResetSettingsFromStorage", "UpdateSettings")), SliceWiring("graphicsSettings", "ypw_1", "?", "lc.isu", "lc.isu(t, n.ypw_1)", listOf("UpdateSettings")), - SliceWiring("controlSettings", "zpw_1", "i2", "_2().ess", "_2().ess(t, n.zpw_1)", listOf("?_C", "UpdateSettings")), + SliceWiring("controlSettings", "zpw_1", "i2", "_2().ess", "_2().ess(t, n.zpw_1)", listOf("ResetSettingsFromStorage", "UpdateSettings")), SliceWiring("securitySettings", "apx_1", "V2", "pc.qsv", "pc.qsv(t, n.apx_1)", listOf("SetProfileBlockStatus")), SliceWiring("purchaseDialogState", "bpx_1", "CO", "je.ur2", "je.ur2(t, n.bpx_1)", listOf("ChangeEmailStatus", "ResetPurchaseDialog", "SuccessfullyPurchase")), SliceWiring("battleInviteState", "cpx_1", "att", "Ttt().nth", "Ttt().nth(t, n.cpx_1)", listOf("AddInvitedUser", "ClearSelectedForInvite", "EnableSetBattleInviteNotifications", "InvitationResponse", "RemoveUserFromInvited", "SelectUserToInvite")), - SliceWiring("keyboardSettings", "dpx_1", "Sut", "bw.oth", "bw.oth(t, n.dpx_1)", listOf("?Iut", "?_C", "ChangeAction", "RemoveAction", "ResetAction")), - SliceWiring("touchscreenHudSettings", "epx_1", "j7", "N7().xsv", "N7().xsv(t, n.epx_1)", listOf("?E7", "?_C", "UpdateSettings")), + SliceWiring("keyboardSettings", "dpx_1", "Sut", "bw.oth", "bw.oth(t, n.dpx_1)", listOf("ChangeAction", "RemoveAction", "ResetAction", "ResetKeyMappingFromDefaults", "ResetSettingsFromStorage")), + SliceWiring("touchscreenHudSettings", "epx_1", "j7", "N7().xsv", "N7().xsv(t, n.epx_1)", listOf("ApplyTouchscreenHudSettings", "ResetSettingsFromStorage", "ResetTouchscreenHudMapping")), SliceWiring("mainQuest", "fpx_1", "WZ", "ih.bsk", "ih.bsk(t, n.fpx_1)", listOf("AddRewardedQuest", "QuestsLoaded", "ResetQuestsLoaded", "SetBattleUpdatedQuests", "SetEventPassShopItemId", "SetQuests", "UpdateQuests")), SliceWiring("language", "gpx_1", "?", "m6().jtg", "m6().jtg(t, n.gpx_1)", listOf("SetLanguage")), SliceWiring("launchParams", "hpx_1", "?", "(pass-through)", "n.hpx_1", listOf()), @@ -91,7 +91,7 @@ object RootWiring { SliceWiring("rewardedVideosState", "upx_1", "JQ", "xh.zsl", "xh.zsl(t, n.upx_1)", listOf("SetDisplayState")), SliceWiring("twitchState", "vpx_1", "g9", "uf.gta", "uf.gta(t, n.vpx_1)", listOf("SetAccount", "SetDaysForNextLinking", "SetTwitchEnabled")), 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("itemNotifications", "xpx_1", "_1", "A1().dso", "A1().dso(t, n.xpx_1)", listOf("?S1", "AddItemNotification", "AddItemNotifications", "RemoveItemNotification", "ResetSettingsFromStorage", "UpdateItemNotification")), SliceWiring("contextTutorial", "ypx_1", "Mst", "Jst().tth", "Jst().tth(t, n.ypx_1)", listOf("LoadCurrentElement", "MarkElementAsViewed", "SetLoadedElements", "SetMissionToElement", "SetUnViewedElements")), 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")), @@ -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") + 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") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 74b7d94..9231e73 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; +// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -667,6 +667,25 @@ import App.State.Reducers.ResetBattleStatistics import App.State.Reducers.ResetRoundState import App.State.Reducers.ShowStat import App.State.Reducers.ToggleStatMenu +import App.State.ChangeAction +import App.State.RemoveAction +import App.State.ResetAction +import App.State.Reducers.ApplyGameSettings +import App.State.Reducers.ApplySoundSettings +import App.State.Reducers.ApplyTouchscreenHudSettings +import App.State.Reducers.EnableEntranceSound +import App.State.Reducers.GameSettingsStateSlice +import App.State.Reducers.KeyboardSettingsStateSlice +import App.State.Reducers.ReduceGameSettings +import App.State.Reducers.ReduceKeyboardSettings +import App.State.Reducers.ReduceSoundSettings +import App.State.Reducers.ReduceTouchscreenHudSettings +import App.State.Reducers.ResetKeyMappingFromDefaults +import App.State.Reducers.ResetSettingsFromStorage +import App.State.Reducers.ResetTouchscreenHudMapping +import App.State.Reducers.SetEntranceSoundSettings +import App.State.Reducers.SoundSettingsStateSlice +import App.State.Reducers.TouchscreenHudSettingsStateSlice import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -1094,7 +1113,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 66 个已迁移切片(65 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 70 个已迁移切片(69 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1158,7 +1177,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 66 个已迁移字段,其余 23 个待迁", + "根:迁移表 = 70 个已迁移字段,其余 19 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1179,8 +1198,9 @@ object StateCheck { "partner", "userRenameState", "battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial", "clanChatState", "battleSearchState", "battleStatistics", + "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", ) && - RootWiring.Unmigrated.size == 23 + RootWiring.Unmigrated.size == 19 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1236,7 +1256,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 66/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 70/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -5742,6 +5762,211 @@ object StateCheck { ) } + // 77 切片 reducer:SoundSettings(Det / Wet().jth,10 字段;4 个动作) + run { + val S0 = SoundSettingsStateSlice() + Check( + "切片 SoundSettings:默认音量与开关", + S0.MasterVolume == 100.0 && S0.UiVolume == 100.0 && + S0.MusicVolume == 50.0 && S0.AmbientVolume == 50.0 && + S0.SfxVolume == 50.0 && S0.HitmarkPlayerVolume == 50.0 && + S0.HitmarkEnemyVolume == 50.0 && !S0.Mute && + !S0.MuteInBackground && S0.IsEntranceSoundOn + ) + val Custom = SoundSettingsStateSlice( + MasterVolume = 10.0, UiVolume = 20.0, MusicVolume = 30.0, + AmbientVolume = 40.0, SfxVolume = 25.0, HitmarkPlayerVolume = 15.0, + HitmarkEnemyVolume = 5.0, Mute = true, MuteInBackground = true, + IsEntranceSoundOn = false, + ) + Check( + "切片 SoundSettings:ApplySoundSettings 整体替换", + ReduceSoundSettings(S0, ApplySoundSettings(Custom)) == Custom + ) + Check( + "切片 SoundSettings:SetEntranceSoundSettings 整体替换", + ReduceSoundSettings(S0, SetEntranceSoundSettings(Custom)) == Custom + ) + Check( + "切片 SoundSettings:EnableEntranceSound 只开入口音效", + ReduceSoundSettings(Custom, EnableEntranceSound) + .let { it.IsEntranceSoundOn && it.Mute } + ) + Check( + "切片 SoundSettings:ResetSettingsFromStorage 不做 IO、保留状态", + ReduceSoundSettings(Custom, ResetSettingsFromStorage) == Custom + ) + Check( + "切片 SoundSettings:无关动作不改状态", + ReduceSoundSettings(Custom, AddFrags(1)) == Custom + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, EnableEntranceSound) + Check( + "根:EnableEntranceSound 只动 soundSettings", + Root1.SoundSettings.IsEntranceSoundOn && + Root1.GameSettings == Root0.GameSettings + ) + val W = RootWiring.ByField("soundSettings")!! + Check( + "根:soundSettings 委派到 Det / Wet().jth 且 4 个动作", + W.SliceClass == "Det" && W.Reducer == "Wet().jth" && + W.Actions.toSet() == setOf( + "ApplySoundSettings", "SetEntranceSoundSettings", "EnableEntranceSound", + "ResetSettingsFromStorage", + ) + ) + } + + // 78 切片 reducer:GameSettings(c2 / g2().zss,6 字段;2 个动作) + run { + val S0 = GameSettingsStateSlice() + Check( + "切片 GameSettings:默认五个开关全开、padding 为 null", + S0.ShowDropZones && S0.ShowYourDrone && S0.ShowChat && + S0.ShowNotifications && S0.ShowOtherDrone && S0.MobilePadding == null + ) + val Custom = GameSettingsStateSlice( + ShowDropZones = false, ShowYourDrone = false, ShowChat = false, + ShowNotifications = false, ShowOtherDrone = false, MobilePadding = 32, + ) + Check( + "切片 GameSettings:ApplyGameSettings 整体替换", + ReduceGameSettings(S0, ApplyGameSettings(Custom)) == Custom + ) + Check( + "切片 GameSettings:ResetSettingsFromStorage 保留状态", + ReduceGameSettings(Custom, ResetSettingsFromStorage) == Custom + ) + Check( + "切片 GameSettings:无关动作不改状态", + ReduceGameSettings(Custom, AddFrags(1)) == Custom + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, ApplyGameSettings(Custom)) + Check( + "根:ApplyGameSettings 只动 gameSettings", + Root1.GameSettings == Custom && Root1.KeyboardSettings == Root0.KeyboardSettings + ) + val W = RootWiring.ByField("gameSettings")!! + Check( + "根:gameSettings 委派到 c2 / g2().zss 且 2 个动作", + W.SliceClass == "c2" && W.Reducer == "g2().zss" && + W.Actions.toSet() == setOf("ApplyGameSettings", "ResetSettingsFromStorage") + ) + } + + // 79 切片 reducer:KeyboardSettings(Sut / bw.oth,1 字段;5 个动作) + run { + val S0 = KeyboardSettingsStateSlice() + Check("切片 KeyboardSettings:默认空绑定表", S0.Mapping.isEmpty()) + val S1 = ReduceKeyboardSettings(S0, ChangeAction("fire", 0, "KeyF")) + Check( + "切片 KeyboardSettings:ChangeAction 写入 action→index→keyState", + S1.Mapping == mapOf( + "fire" to mapOf(0 to "KeyF"), + ) + ) + val S2 = ReduceKeyboardSettings(S1, ChangeAction("fire", 1, "KeyG")) + Check( + "切片 KeyboardSettings:同一 action 不同 index 追加绑定", + S2.Mapping == mapOf( + "fire" to mapOf(0 to "KeyF", 1 to "KeyG"), + ) + ) + Check( + "切片 KeyboardSettings:同一 keyState 从别的 action 上解绑", + ReduceKeyboardSettings( + ReduceKeyboardSettings(S0, ChangeAction("fire", 0, "KeyF")), + ChangeAction("jump", 0, "KeyF"), + ).Mapping == mapOf( + "fire" to emptyMap(), + "jump" to mapOf(0 to "KeyF"), + ) + ) + Check( + "切片 KeyboardSettings:RemoveAction 解绑一个 index", + ReduceKeyboardSettings(S2, RemoveAction("fire", 1)).Mapping == + mapOf("fire" to mapOf(0 to "KeyF")) + ) + Check( + "切片 KeyboardSettings:RemoveAction 对未知 action 是 no-op", + ReduceKeyboardSettings(S2, RemoveAction("nope", 0)) == S2 + ) + Check( + "切片 KeyboardSettings:ResetAction 保留状态(外部 helper)", + ReduceKeyboardSettings(S2, ResetAction("fire")) == S2 + ) + Check( + "切片 KeyboardSettings:两个默认复位都清空绑定表", + ReduceKeyboardSettings(S2, ResetSettingsFromStorage) == + KeyboardSettingsStateSlice() && + ReduceKeyboardSettings(S2, ResetKeyMappingFromDefaults) == + KeyboardSettingsStateSlice() + ) + Check( + "切片 KeyboardSettings:无关动作不改状态", + ReduceKeyboardSettings(S2, AddFrags(1)) == S2 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, RemoveAction("fire", 1)) + Check( + "根:RemoveAction 只动 keyboardSettings", + Root1.KeyboardSettings == KeyboardSettingsStateSlice() && + Root1.TouchscreenHudSettings == Root0.TouchscreenHudSettings + ) + val W = RootWiring.ByField("keyboardSettings")!! + Check( + "根:keyboardSettings 委派到 Sut / bw.oth 且 5 个动作", + W.SliceClass == "Sut" && W.Reducer == "bw.oth" && W.Actions.size == 5 + ) + } + + // 80 切片 reducer:TouchscreenHudSettings(j7 / N7().xsv,1 字段;3 个动作) + run { + val S0 = TouchscreenHudSettingsStateSlice() + Check("切片 TouchscreenHudSettings:默认空布局", S0.Mapping.isEmpty()) + val Custom = TouchscreenHudSettingsStateSlice(mapOf("fire" to 1)) + Check( + "切片 TouchscreenHudSettings:ApplyTouchscreenHudSettings 整体替换", + ReduceTouchscreenHudSettings(S0, ApplyTouchscreenHudSettings(Custom)) == Custom + ) + Check( + "切片 TouchscreenHudSettings:ResetTouchscreenHudMapping 恢复默认", + ReduceTouchscreenHudSettings(Custom, ResetTouchscreenHudMapping) == + TouchscreenHudSettingsStateSlice() + ) + Check( + "切片 TouchscreenHudSettings:ResetSettingsFromStorage 恢复默认", + ReduceTouchscreenHudSettings(Custom, ResetSettingsFromStorage) == + TouchscreenHudSettingsStateSlice() + ) + Check( + "切片 TouchscreenHudSettings:无关动作不改状态", + ReduceTouchscreenHudSettings(Custom, AddFrags(1)) == Custom + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, ResetTouchscreenHudMapping) + Check( + "根:ResetTouchscreenHudMapping 只动 touchscreenHudSettings", + Root1.TouchscreenHudSettings == TouchscreenHudSettingsStateSlice() && + Root1.SoundSettings == Root0.SoundSettings + ) + val W = RootWiring.ByField("touchscreenHudSettings")!! + Check( + "根:touchscreenHudSettings 委派到 j7 / N7().xsv 且 3 个动作", + W.SliceClass == "j7" && W.Reducer == "N7().xsv" && + W.Actions.toSet() == setOf( + "ApplyTouchscreenHudSettings", "ResetTouchscreenHudMapping", + "ResetSettingsFromStorage", + ) + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)