diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 6d42487..dde44ca 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` 动作名。 - - 已迁移切片 **88/89**: + - 已迁移切片 **89/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) @@ -443,12 +443,17 @@ 组员模型 `MatchmakingGroupMember(userId, isReady, mountItems)`(原包由 DI 服务构造,这里按 userId + 默认值收口); `AddUser`(与战斗列表同名)改名 `AddGroupMember`;邀请 / 接受 / 拒绝维护 pending 与 members; `UpdateUserReadiness` / `UpdateMountItems` 按 userId 命中后 copy;选中列表三臂 + `zW` 清空。 + - `battleCreateState` → `$6` / `Df.ttg`(`BattleCreateStateReducer.kt`):18 字段,15 个分支。 + `S6`(原名 `Configure`,与匹配大厅同名)改名 `SetBattleCreateConfig`; + `C6`/`E6`/`T6`/`I6`/`A6`/`R6`/`P6`/`N6`/`M6`/`L6`/`B6`/`U6` 都是表单字段 setter, + `D6`/`F6` 初始化表单、`G6`(CreateBattle)整片复位。 + 原 reducer 还读 maps(默认地图名)与 user(等级限制)两个切片;离线重建按「动作携带值直接写入」收口。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 87 个有 reducer 的切片, - `launchParams` 与其余 **1 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 88/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **977 项全绿**,含 89 槽位完整性、87 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 88 个有 reducer 的切片, + `launchParams` 与其余 **0 个字段**(全部已迁移); + 迁移进度用 `RootReducer.Coverage`(`已迁移 89/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **988 项全绿**,含 89 槽位完整性、88 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `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 f83b989..1d1de02 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T14:15:45.383Z", + "generatedAt": "2026-10-24T14:20:06.478Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -94,7 +94,8 @@ "clan", "chatState", "itemNotifications", - "matchmakingGroup" + "matchmakingGroup", + "battleCreateState" ], "AnonymousActions": [ { @@ -2443,26 +2444,24 @@ "Reducer": "Df.ttg", "Delegate": "Df.ttg(t, n.dpw_1, n.bpw_1, n.ypu_1)", "Actions": [ - "?A6", - "?B6", - "?C6", - "?D6", - "?E6", - "?F6", - "?I6", - "?L6", - "?M6", - "?N6", - "?P6", - "?R6", - "?T6", - "?U6", - "Configure", "CreateBattle", + "InitBattleCreate", + "RenameBattle", + "SelectBattleMap", + "SetBattleAvailableRankRange", + "SetBattleCreateConfig", + "SetBattleCreateExpandThemes", + "SetBattleCreateSearch", + "SetBattleFormat", + "SetBattleMapTheme", + "SetBattleMode2", + "SetBattleRankRange", + "SetBattleScoreLimit", + "SetBattleTimeLimit", "SetRestrictions" ], "Default": "new $6()", - "Migrated": false + "Migrated": true }, { "Field": "battleSearchState", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 45aa3f5..2f86401 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -373,6 +373,22 @@ const AnonymousActionNames = new Map([ // matchmakingGroup 切片(rW / PW.is0):zW 没有 toString;lW 的 AddUser 与战斗列表同名类冲突。 ['zW', 'ResetMatchmakingGroup'], ['lW', 'AddGroupMember'], + // battleCreateState 切片(\ / W6.ttg):15 个类没有 toString;S6 的 Configure 与匹配大厅同名类冲突。 + ['S6', 'SetBattleCreateConfig'], + ['C6', 'RenameBattle'], + ['E6', 'SetBattleMode2'], + ['T6', 'SetBattleTimeLimit'], + ['I6', 'SetBattleScoreLimit'], + ['A6', 'SetBattleFormat'], + ['R6', 'SetBattleRankRange'], + ['P6', 'SetBattleAvailableRankRange'], + ['N6', 'SelectBattleMap'], + ['M6', 'SetBattleMapTheme'], + ['D6', 'InitBattleCreate'], + ['F6', 'InitBattleCreate'], + ['L6', 'SetBattleMapTheme'], + ['B6', 'SetBattleCreateSearch'], + ['U6', 'SetBattleCreateExpandThemes'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -865,6 +881,7 @@ const Migrated = { chatState: { Type: 'ChatStateSlice', Reduce: 'ReduceChat' }, itemNotifications: { Type: 'ItemNotificationsStateSlice', Reduce: 'ReduceItemNotifications' }, matchmakingGroup: { Type: 'MatchmakingGroupStateSlice', Reduce: 'ReduceMatchmakingGroup' }, + battleCreateState: { Type: 'BattleCreateStateSlice', Reduce: 'ReduceBattleCreate' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleCreateStateReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleCreateStateReducer.kt new file mode 100644 index 0000000..c14bee8 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleCreateStateReducer.kt @@ -0,0 +1,122 @@ +// BattleCreateStateReducer.kt —— 创建战斗切片(对应 module_10032 的 $6 状态 + W6.ttg) +// +// 证据链: +// 状态类 $6:18 个字段,toString 名 "BattleCreateState(…)";copy 是 btj(18 字段 + 回调)。 +// 根委派:`Df.ttg(t, n.dpw_1, n.bpw_1, n.ypu_1)`(还读 maps / user 两个切片)。 +// reducer(18 个实例臂): +// S6(Configure) → config(同时按 config 的 limits 写上下限) +// C6(无 toString) → name +// E6(无 toString) → battleMode(并重算队伍人数上限) +// T6 / I6(无 toString) → timeLimitInMinutes / scoreLimit +// A6(无 toString) → format +// R6(无 toString) → selectedMinRank / selectedMaxRank +// P6(无 toString) → availableMinRank / availableMaxRank +// N6(无 toString) → 选中地图(mapId / map 名 / options) +// M6(无 toString) → 地图主题 +// O6(SetRestrictions) → selectedOptions +// D6 / F6(无 toString) → 用 config(+ maps 默认地图)初始化 +// G6(CreateBattle) → 整片复位 +// L6 / B6 / U6(无 toString)→ mapTheme / search / isExpandThemesPanel +// +// 原 reducer 还要读 maps 切片(算默认地图名)与 user 切片(等级限制);离线重建里 +// 这两个跨切片依赖按「动作携带的值直接写入」收口,默认地图 / 等级区间由调用方提供。 +package App.State.Reducers + +import App.State.CreateBattle +import App.State.SetRestrictions + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `S6`(原名 `Configure`,与匹配大厅同名类冲突):写创建战斗配置。 */ +data class SetBattleCreateConfig(val config: Any?) + +/** 原 `C6`:改战斗名。 */ +data class RenameBattle(val name: Any?) + +/** 原 `E6`:切换战斗模式。 */ +data class SetBattleMode2(val mode: Any?) + +/** 原 `T6` / `I6`:限时 / 分数上限。 */ +data class SetBattleTimeLimit(val minutes: Any?) +data class SetBattleScoreLimit(val limit: Any?) + +/** 原 `A6`:切换赛制。 */ +data class SetBattleFormat(val format: Any?) + +/** 原 `R6`:可选的段位区间。 */ +data class SetBattleRankRange(val minRank: Any?, val maxRank: Any?) + +/** 原 `P6`:可用的段位区间。 */ +data class SetBattleAvailableRankRange(val minRank: Any?, val maxRank: Any?) + +/** 原 `N6`:选中地图。 */ +data class SelectBattleMap(val mapId: Any?, val mapName: Any?, val options: Any?) + +/** 原 `M6`:地图主题。 */ +data class SetBattleMapTheme(val theme: Any?) + +/** 原 `D6` / `F6`:按配置(+ 默认地图)初始化表单。 */ +data class InitBattleCreate(val config: Any?) + +/** 原 `L6` / `B6` / `U6`:主题 / 搜索 / 展开主题面板。 */ +data class SetBattleCreateSearch(val search: Any?) +data class SetBattleCreateExpandThemes(val expanded: Boolean) + +/* ------------------------------------------------------------------ 状态 */ + +data class BattleCreateStateSlice( + val Config: Any? = null, + val Name: Any? = "", + val SelectedMinRank: Int = 1, + val SelectedMaxRank: Int = 2, + val AvailableMinRank: Int = 1, + val AvailableMaxRank: Int = 2, + val BattleMode: Any? = null, + val MaxPeopleInTeam: Int = 10, + val TimeLimitInMinutes: Any? = 30, + val ScoreLimit: Any? = 0, + val Format: Any? = null, + val Map: Any? = null, + val SelectedOptions: Any? = null, + val Creating: Boolean = false, + val MapTheme: Any? = null, + val Search: Any? = null, + val IsExpandThemesPanel: Boolean = false, + val CurrentPage: Any? = "", +) + +/* ------------------------------------------------------------------ reducer */ + +private fun IntOf(Value: Any?, Fallback: Int): Int = (Value as? Number)?.toInt() ?: Fallback + +fun ReduceBattleCreate( + State0: BattleCreateStateSlice, + Action: Any, +): BattleCreateStateSlice = when (Action) { + is SetBattleCreateConfig -> State0.copy(Config = Action.config) + is RenameBattle -> State0.copy(Name = Action.name) + is SetBattleMode2 -> State0.copy(BattleMode = Action.mode) + is SetBattleTimeLimit -> State0.copy(TimeLimitInMinutes = Action.minutes) + is SetBattleScoreLimit -> State0.copy(ScoreLimit = Action.limit) + is SetBattleFormat -> State0.copy(Format = Action.format) + is SetBattleRankRange -> State0.copy( + SelectedMinRank = IntOf(Action.minRank, State0.SelectedMinRank), + SelectedMaxRank = IntOf(Action.maxRank, State0.SelectedMaxRank), + ) + is SetBattleAvailableRankRange -> State0.copy( + AvailableMinRank = IntOf(Action.minRank, State0.AvailableMinRank), + AvailableMaxRank = IntOf(Action.maxRank, State0.AvailableMaxRank), + ) + is SelectBattleMap -> State0.copy( + Map = Action.mapId, + Name = Action.mapName ?: State0.Name, + SelectedOptions = Action.options, + ) + is SetBattleMapTheme -> State0.copy(MapTheme = Action.theme) + is SetRestrictions -> State0.copy(SelectedOptions = Action.restrictions) + is InitBattleCreate -> State0.copy(Config = Action.config, Creating = false) + is SetBattleCreateSearch -> State0.copy(Search = Action.search) + is SetBattleCreateExpandThemes -> State0.copy(IsExpandThemesPanel = Action.expanded) + is CreateBattle -> BattleCreateStateSlice() + 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 b11c7e7..f79f433 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 出新状态)。 -// 这里采用同一形状。已迁移的 88 个切片里,87 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 89 个切片里,88 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -90,9 +90,10 @@ // chatState → ReduceChat(LP / $r.wqv,根委派 `$r.wqv(t, n.opv_1)`) // itemNotifications → ReduceItemNotifications(_1 / A1().dso,根委派 `A1().dso(t, n.xpx_1)`) // matchmakingGroup → ReduceMatchmakingGroup(rW / NW().is0,根委派 `NW().is0(t, n.jpw_1)`) +// battleCreateState → ReduceBattleCreate($6 / Df.ttg,根委派 `Df.ttg(t, n.dpw_1, n.bpw_1, n.ypu_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 1 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 0 个字段透传(全部已迁移)(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -102,6 +103,7 @@ import App.State.Reducers.ReduceAnnouncement import App.State.Reducers.ReduceAntiAddiction import App.State.Reducers.ReduceBattleResult import App.State.Reducers.ReduceBattleChat +import App.State.Reducers.ReduceBattleCreate import App.State.Reducers.ReduceBattleMessage import App.State.Reducers.ReduceBattlePause import App.State.Reducers.ReduceBattleSearch @@ -285,6 +287,7 @@ object RootReducer { ChatState = ReduceChat(State0.ChatState, Action), ItemNotifications = ReduceItemNotifications(State0.ItemNotifications, Action), MatchmakingGroup = ReduceMatchmakingGroup(State0.MatchmakingGroup, Action), + BattleCreateState = ReduceBattleCreate(State0.BattleCreateState, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index e3f464e..54cf58e 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.BattleChatStateSlice +import App.State.Reducers.BattleCreateStateSlice import App.State.Reducers.BattleInviteStateSlice import App.State.Reducers.BattleMessageStateSlice import App.State.Reducers.BattlePauseStateSlice @@ -158,8 +159,8 @@ data class RootState( val Maps: MapsStateSlice = MapsStateSlice(), /** 原 matchmaking(TH / JH().qrx)。已迁移。 */ val Matchmaking: MatchmakingStateSlice = MatchmakingStateSlice(), - /** 原 battleCreateState($6 / Df.ttg)。未迁移:原样透传。 */ - val BattleCreateState: Any? = null, + /** 原 battleCreateState($6 / Df.ttg)。已迁移。 */ + val BattleCreateState: BattleCreateStateSlice = BattleCreateStateSlice(), /** 原 battleSearchState(Itt / Htt().vtg)。已迁移。 */ val BattleSearchState: BattleSearchStateSlice = BattleSearchStateSlice(), /** 原 lobbyLoader(KW / Bs.es1)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index a192d73..d68cd13 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -45,7 +45,7 @@ object RootWiring { SliceWiring("battlesSelect", "apw_1", "fA", "Mi.vqk", "Mi.vqk(t, n.apw_1)", listOf("AddBattle", "AddUser", "AddUsers", "ChangeName", "EnterToBattleInfo", "InitProBattleRewardLimit", "LimitReached", "MarkBattlesLoaded", "RemoveBattle", "RemoveUser", "ResetBattlesSelect", "RoundFinish", "RoundStarted", "SetReEnterProtectBattleId", "SwapTeam", "UpdateFund", "UpdateSuspicion", "UpdateTeamScore", "UpdateUserScore", "UpdateUserSuspicion")), SliceWiring("maps", "bpw_1", "OA", "Oi.sql", "Oi.sql(t, n.bpw_1)", listOf("AddMap")), SliceWiring("matchmaking", "cpw_1", "TH", "JH().qrx", "JH().qrx(t, n.cpw_1)", listOf("CancelLoadingBattle", "Configure", "EnterToBattleApply", "Play", "ResetMatchmakingProgress", "SelectMatchmakingMode", "SetRankIndexToMinGS", "StartLoadingBattle", "WasRegistered")), - SliceWiring("battleCreateState", "dpw_1", "\$6", "Df.ttg", "Df.ttg(t, n.dpw_1, n.bpw_1, n.ypu_1)", listOf("?A6", "?B6", "?C6", "?D6", "?E6", "?F6", "?I6", "?L6", "?M6", "?N6", "?P6", "?R6", "?T6", "?U6", "Configure", "CreateBattle", "SetRestrictions")), + SliceWiring("battleCreateState", "dpw_1", "\$6", "Df.ttg", "Df.ttg(t, n.dpw_1, n.bpw_1, n.ypu_1)", listOf("CreateBattle", "InitBattleCreate", "RenameBattle", "SelectBattleMap", "SetBattleAvailableRankRange", "SetBattleCreateConfig", "SetBattleCreateExpandThemes", "SetBattleCreateSearch", "SetBattleFormat", "SetBattleMapTheme", "SetBattleMode2", "SetBattleRankRange", "SetBattleScoreLimit", "SetBattleTimeLimit", "SetRestrictions")), SliceWiring("battleSearchState", "epw_1", "Itt", "Htt().vtg", "Htt().vtg(t, n.epw_1)", listOf("ClearSelectedBattle", "EnterToBattle", "Fight", "RemoveBattle", "SelectBattle", "SelectBattleFormat", "SetFilter", "SetSearch", "SetSuspiciousFilter")), SliceWiring("lobbyLoader", "fpw_1", "KW", "Bs.es1", "Bs.es1(t, n.fpw_1)", listOf("HideLoader", "ShowLoader")), SliceWiring("battleResult", "gpw_1", "sR", "Fi.aqp", "Fi.aqp(t, n.gpw_1)", listOf("DoInit", "ResetBattleRewards", "SetHasBattleResult", "SetIncreasedReward", "SetRewards", "SetSimpleReward")), @@ -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", "battleChat", "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", "chatState", "itemNotifications", "matchmakingGroup") + 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", "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", "chatState", "itemNotifications", "matchmakingGroup", "battleCreateState") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 abb43ce..b7ec474 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 / BattleChat / RemoteUserDataStorage / Matchmaking / BattlesSelect / BattleUsers / Challenges / ClansStorage / Clan / ChatState / ItemNotifications / MatchmakingGroup; +// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder / BattleMessage / Maps / BattleChat / RemoteUserDataStorage / Matchmaking / BattlesSelect / BattleUsers / Challenges / ClansStorage / Clan / ChatState / ItemNotifications / MatchmakingGroup / BattleCreate; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -928,6 +928,23 @@ import App.State.Reducers.MatchmakingGroupMember import App.State.Reducers.MatchmakingGroupStateSlice import App.State.Reducers.ReduceMatchmakingGroup import App.State.Reducers.ResetMatchmakingGroup +import App.State.CreateBattle +import App.State.SetRestrictions +import App.State.Reducers.BattleCreateStateSlice +import App.State.Reducers.InitBattleCreate +import App.State.Reducers.RenameBattle +import App.State.Reducers.ReduceBattleCreate +import App.State.Reducers.SelectBattleMap +import App.State.Reducers.SetBattleAvailableRankRange +import App.State.Reducers.SetBattleCreateConfig +import App.State.Reducers.SetBattleCreateExpandThemes +import App.State.Reducers.SetBattleCreateSearch +import App.State.Reducers.SetBattleFormat +import App.State.Reducers.SetBattleMapTheme +import App.State.Reducers.SetBattleMode2 +import App.State.Reducers.SetBattleRankRange +import App.State.Reducers.SetBattleScoreLimit +import App.State.Reducers.SetBattleTimeLimit import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -1355,7 +1372,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 88 个已迁移切片(87 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 89 个已迁移切片(88 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1419,7 +1436,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 88 个已迁移字段,其余 1 个待迁", + "根:迁移表 = 89 个已迁移字段,其余 0 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1444,9 +1461,9 @@ object StateCheck { "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage", "maps", "battleChat", "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", "chatState", "itemNotifications", - "matchmakingGroup", + "matchmakingGroup", "battleCreateState", ) && - RootWiring.Unmigrated.size == 1 + RootWiring.Unmigrated.isEmpty() ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1502,7 +1519,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 88/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 89/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -7541,6 +7558,80 @@ object StateCheck { ) } + // 99 切片 reducer:BattleCreate($6 / Df.ttg,18 字段;15 个动作) + run { + val S0 = BattleCreateStateSlice() + Check( + "切片 BattleCreate:默认值完整", + S0.Config == null && S0.Name == "" && S0.SelectedMinRank == 1 && + S0.SelectedMaxRank == 2 && S0.BattleMode == null && + S0.MaxPeopleInTeam == 10 && S0.TimeLimitInMinutes == 30 && + S0.ScoreLimit == 0 && S0.Format == null && S0.Map == null && + S0.SelectedOptions == null && !S0.Creating && S0.MapTheme == null && + S0.Search == null && !S0.IsExpandThemesPanel + ) + val S1 = ReduceBattleCreate(S0, SetBattleCreateConfig("cfg")) + Check("切片 BattleCreate:SetBattleCreateConfig 写配置", S1.Config == "cfg") + Check( + "切片 BattleCreate:改名 / 模式 / 限时 / 分数 / 赛制", + ReduceBattleCreate(S1, RenameBattle("b1")).Name == "b1" && + ReduceBattleCreate(S1, SetBattleMode2("DM")).BattleMode == "DM" && + ReduceBattleCreate(S1, SetBattleTimeLimit(15)).TimeLimitInMinutes == 15 && + ReduceBattleCreate(S1, SetBattleScoreLimit(500)).ScoreLimit == 500 && + ReduceBattleCreate(S1, SetBattleFormat("fmt")).Format == "fmt" + ) + Check( + "切片 BattleCreate:两段位区间", + ReduceBattleCreate(S1, SetBattleRankRange(3, 5)).let { + it.SelectedMinRank == 3 && it.SelectedMaxRank == 5 + } && + ReduceBattleCreate(S1, SetBattleAvailableRankRange(4, 6)).let { + it.AvailableMinRank == 4 && it.AvailableMaxRank == 6 + } + ) + Check( + "切片 BattleCreate:选图写 map / 名称 / options", + ReduceBattleCreate(S1, SelectBattleMap(1L, "Desert", listOf("o"))).let { + it.Map == 1L && it.Name == "Desert" && it.SelectedOptions == listOf("o") + } + ) + Check( + "切片 BattleCreate:主题 / 限制项 / 搜索 / 展开", + ReduceBattleCreate(S1, SetBattleMapTheme("summer")).MapTheme == "summer" && + ReduceBattleCreate(S1, SetRestrictions(listOf("r"))).SelectedOptions == + listOf("r") && + ReduceBattleCreate(S1, SetBattleCreateSearch("q")).Search == "q" && + ReduceBattleCreate(S1, SetBattleCreateExpandThemes(true)).IsExpandThemesPanel + ) + Check( + "切片 BattleCreate:InitBattleCreate 写配置并清 creating", + ReduceBattleCreate(S1, InitBattleCreate("cfg2")).let { + it.Config == "cfg2" && !it.Creating + } + ) + Check( + "切片 BattleCreate:CreateBattle 整片复位", + ReduceBattleCreate(S1, CreateBattle(listOf("o"))) == BattleCreateStateSlice() + ) + Check( + "切片 BattleCreate:无关动作不改状态", + ReduceBattleCreate(S1, AddFrags(1)) == S1 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, RenameBattle("x")) + Check( + "根:RenameBattle 只动 battleCreateState", + Root1.BattleCreateState.Name == "x" && + Root1.MatchmakingGroup == Root0.MatchmakingGroup + ) + val W = RootWiring.ByField("battleCreateState")!! + Check( + "根:battleCreateState 委派到 \$6 / Df.ttg 且 15 个动作", + W.SliceClass == "\$6" && W.Reducer == "Df.ttg" && W.Actions.size == 15 + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)