From f66f4316709a5feaf865ef68833cf21cdbbc2b3c Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Fri, 23 Oct 2026 12:25:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(state):=20=E8=BF=81=E7=A7=BB=E8=A1=A5?= =?UTF-8?q?=E7=BB=99=E7=AE=B1=E5=88=87=E7=89=87=EF=BC=8818=20=E4=B8=AA?= =?UTF-8?q?=E5=8A=A8=E4=BD=9C=20+=20ContainerType=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3DTank/docs/KtRebuild_Plan.md | 17 +- 3DTank/names/root_wiring.json | 55 ++++- 3DTank/names/slice_lootBoxes.json | 155 ++++++++++++++ 3DTank/tools/gen_action_kt.js | 5 + 3DTank/tools/gen_root_wiring.js | 24 +++ .../jsMain/kotlin/App/State/ActionCatalog.kt | 6 +- .../src/jsMain/kotlin/App/State/Inventory.kt | 8 +- .../kotlin/App/State/LootBoxesModels.kt | 21 ++ .../App/State/Reducers/LootBoxesReducer.kt | 135 ++++++++++++ .../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 | 199 +++++++++++++++++- 13 files changed, 606 insertions(+), 35 deletions(-) create mode 100644 3DTank/names/slice_lootBoxes.json create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/LootBoxesModels.kt create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/LootBoxesReducer.kt diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 0c74446..e3afc31 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` 动作名。 - - 已迁移切片 **7/89**: + - 已迁移切片 **8/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) @@ -158,9 +158,15 @@ `current` 换成目标屏并清 `isResetBackStackOnNextGoTo` / `isDelayedGoto`;`screen_show` analytics 是副作用,纯 reducer 只保留状态。 屏幕身份用 `App.State.ScreenId`(`EntranceScreen` / `LoaderScreen` / `NamedScreen(name)`); `LX` / `RX` 是屏幕标识类,已从动作表剔除并记入 `root_wiring.json` 的 `Markers`(`screen-id-class`)。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 只归约上述七片,其余 **82 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 7/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **141 项全绿**,含 89 槽位完整性、七片一次归约互不串片、未迁移字段透传, + - `lootBoxes` → `aH` / `EH.wrv`(`LootBoxesReducer.kt`):18 字段,18 个 `instanceof` 臂全量翻译。 + `timeToGiveLootBox` 是 Long 毫秒,`SetTimeToGiveLootBox` 按 `Date.now() + seconds * 1000` 写入; + `AddKeys` 沿用原 `Map.getValue` 语义(缺键抛错),`SetRewards` 同时把 `isReopening` 置 false; + `ClearRewards` 清空 `rewards` / `shownRewards`,`ShownRewards` 是追加(原 `List.plus`)。 + 补给箱类型用 `App.State.ContainerType`(`STANDARD/RARE/EPIC/LEGENDARY`,默认 `STANDARD`); + 无 `toString` 的 `mH/kH/zH/gH/CH/SH` 按效果定名(`SetTimeToGiveLootBox`/`SetPeriodOfGiveLootBox`/…)。 + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 只归约上述八片,其余 **81 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 8/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **163 项全绿**,含 89 槽位完整性、八片一次归约互不串片、未迁移字段透传, 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); @@ -168,7 +174,8 @@ - 无 `toString` 的默认动作按证据定名(`root_wiring.json` 的 `AnonymousActions`): `o1` → `ShowShopDiscountNotification`(`$_$.gl` 单例,由 module_18334 的 `Qx.qge` 派发; 效果是把 `HomeScreen.shopDiscountNotification` 置 true,对应主菜单商店项的折扣提示); - shop 的 17 个匿名类按 reducer 效果定名并登记(`Yut`→`ShopLoaded`、`F5`→`ResetShop` 等)。 + shop 的 17 个匿名类按 reducer 效果定名并登记(`Yut`→`ShopLoaded`、`F5`→`ResetShop` 等); + screen 的 9 个、lootBoxes 的 6 个匿名类同样按效果定名并登记。 ### 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 abd8cb0..0aae3fa 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-22T19:22:01.390Z", + "generatedAt": "2026-10-23T04:19:26.216Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -13,7 +13,8 @@ "home", "localBattleUserState", "shop", - "screen" + "screen", + "lootBoxes" ], "AnonymousActions": [ { @@ -178,6 +179,42 @@ "Name": "PopScreen", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "screenStack = dropLast(1)(`f$(stack, 1)` 是 Kotlin dropLast)" + }, + { + "Local": "mH", + "Name": "SetTimeToGiveLootBox", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "timeToGiveLootBox = Date.now() + seconds * 1000(第 9 个构造参数 `Fg(Md(), Qk.y2o(Wg(t.zrv_1)))`)" + }, + { + "Local": "kH", + "Name": "SetPeriodOfGiveLootBox", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "periodOfGiveLootBoxInSec = action.yrv_1(第 10 个构造参数)" + }, + { + "Local": "zH", + "Name": "ClearRewards", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "rewards = emptyList()、shownRewards = emptyList()(第 6、7 个构造参数 `md(), md()`)" + }, + { + "Local": "gH", + "Name": "LootBoxOpeningFailed", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "openingFailed = true(第 11 个构造参数 !0)" + }, + { + "Local": "CH", + "Name": "SetIsReopening", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isReopening = true(第 17 个构造参数 !0)" + }, + { + "Local": "SH", + "Name": "SetIsBlockOpenButtons", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isBlockOpenButtons = action.xrv_1(第 18 个构造参数)" } ], "Markers": [ @@ -798,27 +835,27 @@ "Reducer": "ws.wrv", "Delegate": "ws.wrv(t, n.wpv_1)", "Actions": [ - "?CH", - "?SH", - "?gH", - "?kH", - "?mH", - "?zH", "AddKeys", + "ClearRewards", + "LootBoxOpeningFailed", "SelectCategory", "SetContainerData", "SetCurrentContainer", "SetGoToGarage", "SetIsAccelerated", + "SetIsBlockOpenButtons", "SetIsOpening", + "SetIsReopening", "SetIsShopVisited", "SetKeys", + "SetPeriodOfGiveLootBox", "SetPossibleRewards", "SetRewards", + "SetTimeToGiveLootBox", "ShownRewards" ], "Default": "new aH()", - "Migrated": false + "Migrated": true }, { "Field": "notifications", diff --git a/3DTank/names/slice_lootBoxes.json b/3DTank/names/slice_lootBoxes.json new file mode 100644 index 0000000..528b7f0 --- /dev/null +++ b/3DTank/names/slice_lootBoxes.json @@ -0,0 +1,155 @@ +{ + "generatedAt": "2026-10-23T03:54:25.170Z", + "module": "module_10032.js", + "Field": "lootBoxes", + "Slot": "wpv_1", + "Class": "aH", + "ClassSource": "new aH()", + "Params": [ + "t", + "n", + "i", + "r", + "e", + "u", + "s", + "o", + "_", + "h", + "a", + "c", + "l", + "f", + "v", + "p", + "w", + "d" + ], + "Fields": [ + { + "Field": "rewardsForKeys", + "Slot": "lru_1", + "Kotlin": "RewardsForKeys", + "Default": "md()" + }, + { + "Field": "currentContainerType", + "Slot": "mru_1", + "Kotlin": "CurrentContainerType", + "Default": "Ly()" + }, + { + "Field": "keys", + "Slot": "nru_1", + "Kotlin": "Keys", + "Default": "ag()" + }, + { + "Field": "containerNames", + "Slot": "oru_1", + "Kotlin": "ContainerNames", + "Default": "ag()" + }, + { + "Field": "containerDescriptions", + "Slot": "pru_1", + "Kotlin": "ContainerDescriptions", + "Default": "ag()" + }, + { + "Field": "rewards", + "Slot": "qru_1", + "Kotlin": "Rewards", + "Default": "md()" + }, + { + "Field": "shownRewards", + "Slot": "rru_1", + "Kotlin": "ShownRewards", + "Default": "md()" + }, + { + "Field": "gotoGarage", + "Slot": "sru_1", + "Kotlin": "GotoGarage", + "Default": "false" + }, + { + "Field": "timeToGiveLootBox", + "Slot": "tru_1", + "Kotlin": "TimeToGiveLootBox", + "Default": "new Dd(0, 0)" + }, + { + "Field": "periodOfGiveLootBoxInSec", + "Slot": "uru_1", + "Kotlin": "PeriodOfGiveLootBoxInSec", + "Default": "0" + }, + { + "Field": "openingFailed", + "Slot": "vru_1", + "Kotlin": "OpeningFailed", + "Default": "false" + }, + { + "Field": "adsAvailable", + "Slot": "wru_1", + "Kotlin": "AdsAvailable", + "Default": "false" + }, + { + "Field": "currentCategory", + "Slot": "xru_1", + "Kotlin": "CurrentCategory", + "Default": "null" + }, + { + "Field": "isOpening", + "Slot": "yru_1", + "Kotlin": "IsOpening", + "Default": "false" + }, + { + "Field": "isAccelerated", + "Slot": "zru_1", + "Kotlin": "IsAccelerated", + "Default": "false" + }, + { + "Field": "isShopVisited", + "Slot": "arv_1", + "Kotlin": "IsShopVisited", + "Default": "false" + }, + { + "Field": "isReopening", + "Slot": "brv_1", + "Kotlin": "IsReopening", + "Default": "false" + }, + { + "Field": "isBlockOpenButtons", + "Slot": "crv_1", + "Kotlin": "IsBlockOpenButtons", + "Default": "false" + } + ], + "CopyMethod": null, + "Branches": [ + { + "Guard": "(具名 reducer)", + "Locals": [], + "Kind": "named", + "Expr": "ws.wrv(t, n.wpv_1)" + } + ], + "SubReducers": [], + "Counts": { + "TotalInstanceof": null, + "TopLevelBranches": 1, + "SubReducerArms": 0 + }, + "Locals": {}, + "Delegate": "ws.wrv(t, n.wpv_1)" +} diff --git a/3DTank/tools/gen_action_kt.js b/3DTank/tools/gen_action_kt.js index 8a5493b..3c967fc 100644 --- a/3DTank/tools/gen_action_kt.js +++ b/3DTank/tools/gen_action_kt.js @@ -569,6 +569,11 @@ function InferDeep(Expr, Mod, Off, Depth) { // 类型定义见 TankiOffline/.../App/State/ScreenId.kt。 const FieldTypeOverrides = new Map([ ['GoTo.screen', 'ScreenId'], + // lootBoxes 切片:lH/qH/yH 的字段在别处的调用点只能给出 Boolean / Any?,但 reducer 里分别是 + // ContainerType(默认 Ly()=STANDARD)、可空 ContainerType、及补给箱奖励列表。 + ['SetCurrentContainer.type', 'ContainerType'], + ['SelectCategory.containerCategory', 'ContainerType?'], + ['ShownRewards.rewards', 'List'], ]); const Emitted = []; diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index d054255..47a0be1 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -68,6 +68,13 @@ const AnonymousActionNames = new Map([ ['lY', 'EnableBack'], ['cY', 'DisableBack'], ['fY', 'PopScreen'], + // 补给箱切片(aH / EH.wrv):没有 toString / 不在 action_inventory 的 6 个类。 + ['mH', 'SetTimeToGiveLootBox'], + ['kH', 'SetPeriodOfGiveLootBox'], + ['zH', 'ClearRewards'], + ['gH', 'LootBoxOpeningFailed'], + ['CH', 'SetIsReopening'], + ['SH', 'SetIsBlockOpenButtons'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -433,6 +440,7 @@ const Migrated = { localBattleUserState: { Type: 'LocalBattleUserStateSlice', Reduce: 'ReduceLocalBattleUser' }, shop: { Type: 'ShopStateSlice', Reduce: 'ReduceShop' }, screen: { Type: 'ScreenStateSlice', Reduce: 'ReduceScreen' }, + lootBoxes: { Type: 'LootBoxesStateSlice', Reduce: 'ReduceLootBoxes' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -487,6 +495,16 @@ const ScreenAnonymousEffects = { fY: 'screenStack = dropLast(1)(`f$(stack, 1)` 是 Kotlin dropLast)', }; +// 补给箱切片(aH / EH.wrv)的匿名动作:按 reducer 分支效果定名。 +const LootBoxesAnonymousEffects = { + mH: 'timeToGiveLootBox = Date.now() + seconds * 1000(第 9 个构造参数 `Fg(Md(), Qk.y2o(Wg(t.zrv_1)))`)', + kH: 'periodOfGiveLootBoxInSec = action.yrv_1(第 10 个构造参数)', + zH: 'rewards = emptyList()、shownRewards = emptyList()(第 6、7 个构造参数 `md(), md()`)', + gH: 'openingFailed = true(第 11 个构造参数 !0)', + CH: 'isReopening = true(第 17 个构造参数 !0)', + SH: 'isBlockOpenButtons = action.xrv_1(第 18 个构造参数)', +}; + fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), JSON.stringify( @@ -515,6 +533,12 @@ fs.writeFileSync( DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', Effect, })), + ...Object.entries(LootBoxesAnonymousEffects).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/ActionCatalog.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/ActionCatalog.kt index 9f83f46..14610e6 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/ActionCatalog.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/ActionCatalog.kt @@ -346,14 +346,14 @@ object ActionCatalog { ActionMeta("SetIcons", SetIcons::class, listOf(ActionFieldMeta("icons", "List"))), ActionMeta("LootBoxes", LootBoxes::class, listOf(ActionFieldMeta("rewardsForKeys", "Any?"), ActionFieldMeta("currentContainerType", "Any?"), ActionFieldMeta("keys", "Any?"), ActionFieldMeta("containerNames", "Any?"), ActionFieldMeta("containerDescriptions", "Any?"), ActionFieldMeta("rewards", "Any?"), ActionFieldMeta("shownRewards", "Any?"), ActionFieldMeta("gotoGarage", "Any?"), ActionFieldMeta("timeToGiveLootBox", "Any?"), ActionFieldMeta("periodOfGiveLootBoxInSec", "Any?"), ActionFieldMeta("openingFailed", "Any?"), ActionFieldMeta("adsAvailable", "Any?"), ActionFieldMeta("currentCategory", "Any?"), ActionFieldMeta("isOpening", "Any?"), ActionFieldMeta("isAccelerated", "Any?"), ActionFieldMeta("isShopVisited", "Any?"), ActionFieldMeta("isReopening", "Any?"), ActionFieldMeta("isBlockOpenButtons", "Any?"))), ActionMeta("SetPossibleRewards", SetPossibleRewards::class, listOf(ActionFieldMeta("rewards", "Any?"))), - ActionMeta("SetCurrentContainer", SetCurrentContainer::class, listOf(ActionFieldMeta("type", "Boolean"))), + ActionMeta("SetCurrentContainer", SetCurrentContainer::class, listOf(ActionFieldMeta("type", "ContainerType"))), ActionMeta("SetKeys", SetKeys::class, listOf(ActionFieldMeta("keys", "Map"))), ActionMeta("AddKeys", AddKeys::class, listOf(ActionFieldMeta("keyType", "String"), ActionFieldMeta("delta", "Int"))), ActionMeta("SetContainerData", SetContainerData::class, listOf(ActionFieldMeta("type", "String"), ActionFieldMeta("name", "String"), ActionFieldMeta("description", "String"))), ActionMeta("SetGoToGarage", SetGoToGarage::class, listOf(ActionFieldMeta("gotoGarage", "Boolean"))), ActionMeta("OpenContainer", OpenContainer::class, listOf(ActionFieldMeta("amount", "Any?"), ActionFieldMeta("type", "Any?"))), - ActionMeta("SelectCategory", SelectCategory::class, listOf(ActionFieldMeta("containerCategory", "Any?"))), - ActionMeta("ShownRewards", ShownRewards::class, listOf(ActionFieldMeta("rewards", "Any?"))), + ActionMeta("SelectCategory", SelectCategory::class, listOf(ActionFieldMeta("containerCategory", "ContainerType?"))), + ActionMeta("ShownRewards", ShownRewards::class, listOf(ActionFieldMeta("rewards", "List"))), ActionMeta("SetIsOpening", SetIsOpening::class, listOf(ActionFieldMeta("isOpening", "Boolean"))), ActionMeta("SetIsAccelerated", SetIsAccelerated::class, listOf(ActionFieldMeta("isAccelerated", "Boolean"))), ActionMeta("SetIsShopVisited", SetIsShopVisited::class, listOf(ActionFieldMeta("isShopVisited", "Boolean"))), diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Inventory.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Inventory.kt index 2e6204b..4f919a0 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/Inventory.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Inventory.kt @@ -5,7 +5,7 @@ package App.State import Core.Vector3 -// 动作/状态类共 816 个,字段 2150 个(已定类型 509 个) +// 动作/状态类共 816 个,字段 2150 个(已定类型 511 个) data class AnnouncementState( val isEnabled: Any?, // 仅 copy 转发(无类型信息) val modelLoaded: Any?, // 仅 copy 转发(无类型信息) @@ -2019,7 +2019,7 @@ data class SetPossibleRewards( ) data class SetCurrentContainer( - val type: Boolean, // callsite + val type: ContainerType, // override ) data class SetKeys( @@ -2047,11 +2047,11 @@ data class OpenContainer( ) data class SelectCategory( - val containerCategory: Any?, // 调用点: i | null + val containerCategory: ContainerType?, // override ) data class ShownRewards( - val rewards: Any?, // 调用点: t.state.rewards | HT(n) + val rewards: List, // override ) data class SetIsOpening( diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/LootBoxesModels.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/LootBoxesModels.kt new file mode 100644 index 0000000..de8ac8e --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/LootBoxesModels.kt @@ -0,0 +1,21 @@ +// LootBoxesModels.kt —— 补给箱切片用到的枚举(App.State 包内,供 Inventory.kt 的字段类型引用) +// +// 证据链:module_88693 的 `Vt()` 初始化 +// `(u = new Kt("STANDARD", 0)), (s = new Kt("RARE", 1)), (o = new Kt("EPIC", 2)), (_ = new Kt("LEGENDARY", 3))` +// `Yt()` 返回 `u`(STANDARD)——module_10032 的 `Ly()` 正是 `Yt`,即 aH.currentContainerType 的默认值。 +package App.State + +/** 原 `Kt`:补给箱类型,顺序与枚举 ordinal 一致(STANDARD=0 … LEGENDARY=3)。 */ +enum class ContainerType { + STANDARD, + RARE, + EPIC, + LEGENDARY, + ; + + companion object { + /** 运行期是 JS 枚举对象,toString 就是成员名;认不出时与原包默认值一致落到 STANDARD。 */ + fun FromText(Text: Any?): ContainerType = + entries.firstOrNull { it.name == Text?.toString() } ?: STANDARD + } +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/LootBoxesReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/LootBoxesReducer.kt new file mode 100644 index 0000000..1f317f9 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/LootBoxesReducer.kt @@ -0,0 +1,135 @@ +// LootBoxesReducer.kt —— 补给箱切片(对应 module_10032 的 aH 状态 + EH.wrv reducer) +// +// 证据链: +// 状态类 aH:`function aH(t, n, i, r, e, u, s, o, _, h, a, c, l, f, v, p, w, d)`,18 个字段, +// toString 名为 `LootBoxes(rewardsForKeys=…, currentContainerType=…, …)`。默认值取自构造: +// rewardsForKeys = md()(emptyList)、currentContainerType = Ly()(ContainerType.STANDARD, +// 见 module_88693 的 `Vt()` / `Yt()`)、keys / containerNames / containerDescriptions = ag()(emptyMap)、 +// rewards / shownRewards = md()、gotoGarage = false、timeToGiveLootBox = new Dd(0, 0)(Long 0)、 +// periodOfGiveLootBoxInSec = 0、openingFailed / adsAvailable = false、currentCategory = null、 +// 其余布尔 = false。 +// reducer:`ld(EH).wrv`(module_10032 第 33150 行)18 个 instanceof 臂: +// +// cH SetPossibleRewards → rewardsForKeys = action.rewards +// lH SetCurrentContainer → currentContainerType = action.type +// fH SetKeys → keys = action.keys +// vH AddKeys → keys[action.keyType] += action.delta(原 `nm` 是 Map.getValue, +// 缺键抛 "Key … is missing in the map.";加法按 32 位 Int 截断) +// pH SetContainerData → containerNames[type] = name、containerDescriptions[type] = description +// (原 `cg(map, Vd(k, v))` 是 Map.plus(Pair)) +// wH SetGoToGarage → gotoGarage = action.gotoGarage +// bH SetRewards → rewards = action.rewards,同时 isReopening = false(第 17 个构造参数 !1) +// mH SetTimeToGiveLootBox → timeToGiveLootBox = Date.now() + action.seconds * 1000 +// (原 `Fg(Md(), Qk.y2o(Wg(sec)))`:Md()=Date.now()、Wg=Int→Long、 +// Qk.y2o=秒→毫秒、Fg=Long 加法) +// kH SetPeriodOfGiveLootBox → periodOfGiveLootBoxInSec = action.periodOfGiveLootBoxInSec +// zH ClearRewards → rewards = emptyList()、shownRewards = emptyList() +// gH LootBoxOpeningFailed → openingFailed = true +// qH SelectCategory → currentCategory = action.containerCategory +// yH ShownRewards → shownRewards += action.rewards(原 `Im(list, list)` 是 List.plus) +// $H SetIsOpening → isOpening = action.isOpening +// xH SetIsAccelerated → isAccelerated = action.isAccelerated +// jH SetIsShopVisited → isShopVisited = action.isShopVisited +// CH SetIsReopening → isReopening = true +// SH SetIsBlockOpenButtons → isBlockOpenButtons = action.isBlockOpenButtons +// +// 无 toString / 不在 action_inventory 的 6 个类按 reducer 效果定名,原名写在注释里;名字同时登记在 +// 3DTank/tools/gen_root_wiring.js 的 AnonymousActionNames,重新生成 RootWiring 后显示。 +package App.State.Reducers + +import App.State.AddKeys +import App.State.ContainerGivenItem +import App.State.ContainerType +import App.State.SelectCategory +import App.State.SetContainerData +import App.State.SetCurrentContainer +import App.State.SetGoToGarage +import App.State.SetIsAccelerated +import App.State.SetIsOpening +import App.State.SetIsShopVisited +import App.State.SetKeys +import App.State.SetPossibleRewards +import App.State.SetRewards +import App.State.ShownRewards + +/** 原 `mH`:补给箱倒计时(秒);归约后写到 timeToGiveLootBox = now + seconds * 1000。 */ +data class SetTimeToGiveLootBox(val seconds: Int) + +/** 原 `kH`:补给箱发放周期(秒)。 */ +data class SetPeriodOfGiveLootBox(val periodOfGiveLootBoxInSec: Int) + +/** 原 `zH`:无字段;清空 rewards 与 shownRewards。 */ +object ClearRewards + +/** 原 `gH`:无字段;openingFailed = true。 */ +object LootBoxOpeningFailed + +/** 原 `CH`:无字段;isReopening = true。 */ +object SetIsReopening + +/** 原 `SH`:isBlockOpenButtons = action.xrv_1。 */ +data class SetIsBlockOpenButtons(val isBlockOpenButtons: Boolean) + +/** + * 原 `aH` / `LootBoxes`:`LootBoxes(rewardsForKeys, currentContainerType, keys, containerNames, + * containerDescriptions, rewards, shownRewards, gotoGarage, timeToGiveLootBox, periodOfGiveLootBoxInSec, + * openingFailed, adsAvailable, currentCategory, isOpening, isAccelerated, isShopVisited, isReopening, + * isBlockOpenButtons)`。 + */ +data class LootBoxesStateSlice( + /** 本 reducer 只 copy 转发、从不读;原构造默认 md()(emptyList),元素类型没有证据,保留 Any?。 */ + val RewardsForKeys: Any? = emptyList(), + val CurrentContainerType: ContainerType = ContainerType.STANDARD, + val Keys: Map = emptyMap(), + val ContainerNames: Map = emptyMap(), + val ContainerDescriptions: Map = emptyMap(), + val Rewards: List = emptyList(), + val ShownRewards: List = emptyList(), + val GotoGarage: Boolean = false, + val TimeToGiveLootBox: Long = 0L, + val PeriodOfGiveLootBoxInSec: Int = 0, + val OpeningFailed: Boolean = false, + val AdsAvailable: Boolean = false, + val CurrentCategory: ContainerType? = null, + val IsOpening: Boolean = false, + val IsAccelerated: Boolean = false, + val IsShopVisited: Boolean = false, + val IsReopening: Boolean = false, + val IsBlockOpenButtons: Boolean = false, +) + +/** 原 `Md()`:`ve(Date.now())`,即当前毫秒(Long)。 */ +private fun CurrentTimeMillis(): Long = kotlin.js.Date.now().toLong() + +/** 根 reducer 的 lootBoxes 分支(原 `ws.wrv(t, n.wpv_1)`)。 */ +fun ReduceLootBoxes(State0: LootBoxesStateSlice, A: Any): LootBoxesStateSlice = when (A) { + is SetPossibleRewards -> State0.copy(RewardsForKeys = A.rewards) + is SetCurrentContainer -> State0.copy(CurrentContainerType = A.type) + is SetKeys -> State0.copy(Keys = A.keys) + + is AddKeys -> State0.copy( + Keys = State0.Keys + (A.keyType to (State0.Keys.getValue(A.keyType) + A.delta)) + ) + + is SetContainerData -> State0.copy( + ContainerNames = State0.ContainerNames + (A.type to A.name), + ContainerDescriptions = State0.ContainerDescriptions + (A.type to A.description), + ) + + is SetGoToGarage -> State0.copy(GotoGarage = A.gotoGarage) + is SetRewards -> State0.copy(Rewards = A.rewards, IsReopening = false) + is SetTimeToGiveLootBox -> State0.copy( + TimeToGiveLootBox = CurrentTimeMillis() + A.seconds.toLong() * 1000L + ) + is SetPeriodOfGiveLootBox -> State0.copy(PeriodOfGiveLootBoxInSec = A.periodOfGiveLootBoxInSec) + is ClearRewards -> State0.copy(Rewards = emptyList(), ShownRewards = emptyList()) + is LootBoxOpeningFailed -> State0.copy(OpeningFailed = true) + is SelectCategory -> State0.copy(CurrentCategory = A.containerCategory) + is ShownRewards -> State0.copy(ShownRewards = State0.ShownRewards + A.rewards) + is SetIsOpening -> State0.copy(IsOpening = A.isOpening) + is SetIsAccelerated -> State0.copy(IsAccelerated = A.isAccelerated) + is SetIsShopVisited -> State0.copy(IsShopVisited = A.isShopVisited) + is SetIsReopening -> State0.copy(IsReopening = true) + is SetIsBlockOpenButtons -> State0.copy(IsBlockOpenButtons = A.isBlockOpenButtons) + 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 55a805f..c621ad3 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt @@ -10,8 +10,9 @@ // localBattleUserState → ReduceLocalBattleUser(lE / `(inline@localBattleUserState)` 分支) // shop → ReduceShop(j4 / `(inline@shop)` 分支) // screen → ReduceScreen(WK / dY().ks5) +// lootBoxes → ReduceLootBoxes(aH / EH.wrv) // -// 其余 82 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 81 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -20,6 +21,7 @@ import App.State.Reducers.ReduceHome import App.State.Reducers.ReduceInvite import App.State.Reducers.ReduceLocalBattleUser import App.State.Reducers.ReduceLoginByPassword +import App.State.Reducers.ReduceLootBoxes import App.State.Reducers.ReduceScreen import App.State.Reducers.ReduceShop import App.State.Reducers.ReduceTwitch @@ -34,7 +36,7 @@ object RootReducer { val Coverage: String get() = "已迁移 ${Migrated.size}/${RootWiring.All.size}" /** - * 归约一次。未迁移字段原样带过去(`copy` 只改已迁移的三个)。 + * 归约一次。未迁移字段原样带过去(`copy` 只改已迁移的字段)。 */ fun Reduce(State0: RootState, Action: Any): RootState = State0.copy( InviteState = ReduceInvite(State0.InviteState, Action), @@ -44,6 +46,7 @@ object RootReducer { LocalBattleUserState = ReduceLocalBattleUser(State0.LocalBattleUserState, Action), Shop = ReduceShop(State0.Shop, Action), Screen = ReduceScreen(State0.Screen, Action), + LootBoxes = ReduceLootBoxes(State0.LootBoxes, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index f6c6788..6ea2035 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.HomeStateSlice import App.State.Reducers.InviteStateSlice import App.State.Reducers.LocalBattleUserStateSlice import App.State.Reducers.LoginByPasswordStateSlice +import App.State.Reducers.LootBoxesStateSlice import App.State.Reducers.ScreenStateSlice import App.State.Reducers.ShopStateSlice import App.State.Reducers.TwitchStateSlice @@ -65,8 +66,8 @@ data class RootState( val Settings: Any? = null, /** 原 accountSettings(oet / Lp.mtg)。未迁移:原样透传。 */ val AccountSettings: Any? = null, - /** 原 lootBoxes(aH / ws.wrv)。未迁移:原样透传。 */ - val LootBoxes: Any? = null, + /** 原 lootBoxes(aH / ws.wrv)。已迁移。 */ + val LootBoxes: LootBoxesStateSlice = LootBoxesStateSlice(), /** 原 notifications(? / B_.ise)。未迁移:原样透传。 */ val Notifications: Any? = null, /** 原 friends(KD / NL().cr9)。未迁移:原样透传。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index 12b6f4a..41be502 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -38,7 +38,7 @@ object RootWiring { SliceWiring("news", "tpv_1", "ZX", "E_.asd", "E_.asd(t, n.tpv_1)", listOf("?hJ", "AddNewsItemAction", "RemoveNewsItemAction", "SetNewsItemAction", "SetUnreadNewsCounter")), SliceWiring("settings", "upv_1", "Uut", "xw.ltg", "xw.ltg(t, n.upv_1)", listOf("?Fut", "?Gut")), SliceWiring("accountSettings", "vpv_1", "oet", "Lp.mtg", "Lp.mtg(t, n.vpv_1)", listOf("?aet", "?cet", "?det", "?fet", "?get", "?ket", "?met", "?pet", "?vet", "?wet", "?xet", "ClearInputUid", "SetAccountRemovalDays")), - SliceWiring("lootBoxes", "wpv_1", "aH", "ws.wrv", "ws.wrv(t, n.wpv_1)", listOf("?CH", "?SH", "?gH", "?kH", "?mH", "?zH", "AddKeys", "SelectCategory", "SetContainerData", "SetCurrentContainer", "SetGoToGarage", "SetIsAccelerated", "SetIsOpening", "SetIsShopVisited", "SetKeys", "SetPossibleRewards", "SetRewards", "ShownRewards")), + SliceWiring("lootBoxes", "wpv_1", "aH", "ws.wrv", "ws.wrv(t, n.wpv_1)", listOf("AddKeys", "ClearRewards", "LootBoxOpeningFailed", "SelectCategory", "SetContainerData", "SetCurrentContainer", "SetGoToGarage", "SetIsAccelerated", "SetIsBlockOpenButtons", "SetIsOpening", "SetIsReopening", "SetIsShopVisited", "SetKeys", "SetPeriodOfGiveLootBox", "SetPossibleRewards", "SetRewards", "SetTimeToGiveLootBox", "ShownRewards")), SliceWiring("notifications", "xpv_1", "?", "B_.ise", "B_.ise(t, n.xpv_1)", listOf("?PJ", "?RJ", "AddNotification", "InitNotificationSound")), SliceWiring("friends", "ypv_1", "KD", "NL().cr9", "NL().cr9(t, n.ypv_1)", listOf("?_L", "?hL", "AddUsersToAccepted", "AddUsersToIncoming", "AddUsersToNewAccepted", "AddUsersToNewIncoming", "ChangeFriendAddFindState", "RemoveUserFromAccepted", "RemoveUserFromIncoming", "RemoveUserFromNewAccepted", "RemoveUsersFromNewIncoming", "SetLimits", "SetValidationStatus")), SliceWiring("remoteUserDataStorage", "zpv_1", "\$Q", "vh.isl", "vh.isl(t, n.zpv_1)", listOf("UpdateClanTags", "UpdateOnlineStatus", "UpdatePremiumStatus", "UpdateRanks", "UpdateUids", "UserEnterBattle", "UserLeaveBattle", "UserLeaveGroup")), @@ -106,7 +106,7 @@ object RootWiring { ) /** 已迁移(由 RootReducer 真正归约)的字段。 */ - val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen") + val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 74d656d..509734c 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt @@ -9,8 +9,8 @@ // ⑥ 动作目录:816 个类与 138 个真实派发名对齐 // ⑦ 回放:真实 toString 样例的类/字段/类型校验 // ⑧ 全目录动作类都能按 KClass 路由 -// ⑨~⑰ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState / -// Shop(含 U7 类别表与 w3 物品表两个子 reducer)/ Screen;⑭ 根 reducer 的 89 槽位委派 +// ⑨~⑱ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState / +// Shop(含 U7 类别表与 w3 物品表两个子 reducer)/ Screen / LootBoxes;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) package App @@ -144,6 +144,28 @@ import App.State.Reducers.PopScreen import App.State.Reducers.ReduceScreen import App.State.Reducers.ResetScreenStackOnNextGoTo import App.State.Reducers.ScreenStateSlice +import App.State.AddKeys +import App.State.ContainerGivenItem +import App.State.ContainerType +import App.State.SelectCategory +import App.State.SetContainerData +import App.State.SetCurrentContainer +import App.State.SetGoToGarage +import App.State.SetIsAccelerated +import App.State.SetIsOpening +import App.State.SetIsShopVisited +import App.State.SetKeys +import App.State.SetPossibleRewards +import App.State.SetRewards +import App.State.ShownRewards +import App.State.Reducers.ClearRewards +import App.State.Reducers.LootBoxOpeningFailed +import App.State.Reducers.LootBoxesStateSlice +import App.State.Reducers.ReduceLootBoxes +import App.State.Reducers.SetIsBlockOpenButtons +import App.State.Reducers.SetIsReopening +import App.State.Reducers.SetPeriodOfGiveLootBox +import App.State.Reducers.SetTimeToGiveLootBox private data class AddFrags(val fragsCount: Int) private data class SetScreen(val screen: String) @@ -517,7 +539,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 六个已迁移切片一次归约 + // ⑭ 根 reducer:89 个槽位的委派表 + 八个已迁移切片一次归约 run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -581,13 +603,16 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 七个已迁移字段,其余 82 个待迁", + "根:迁移表 = 八个已迁移字段,其余 81 个待迁", RootReducer.Migrated == - setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen") && - RootWiring.Unmigrated.size == 82 + setOf( + "inviteState", "loginByPassword", "twitchState", "home", + "localBattleUserState", "shop", "screen", "lootBoxes", + ) && + RootWiring.Unmigrated.size == 81 ) - // 一次 ReduceRoot 里六个切片各归约自己的动作,互不串片 + // 一次 ReduceRoot 里八个切片各归约自己的动作,互不串片 val S0 = RootState(User = "user-1", Screen = ScreenStateSlice(Current = NamedScreen("Preloader"))) val S1 = ReduceRoot(S0, ApplyInvite("uid-1")) Check( @@ -640,7 +665,7 @@ object StateCheck { S5.User == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 7/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 8/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -1067,6 +1092,164 @@ object StateCheck { ).all { it in W.Actions } ) } + + // ⑱ 切片 reducer:LootBoxes(原 module_10032 的 aH / EH.wrv,18 个 instanceof 臂) + run { + val Item = { Name: String -> + ContainerGivenItem("INVENTORY", 1, "INVENTORY", 10007277, null, Name, "mine") + } + val S0 = LootBoxesStateSlice() + Check( + "切片 LootBoxes:默认值与原构造一致", + S0.RewardsForKeys == emptyList() && + S0.CurrentContainerType == ContainerType.STANDARD && + S0.Keys.isEmpty() && S0.ContainerNames.isEmpty() && + S0.ContainerDescriptions.isEmpty() && S0.Rewards.isEmpty() && + S0.ShownRewards.isEmpty() && !S0.GotoGarage && S0.TimeToGiveLootBox == 0L && + S0.PeriodOfGiveLootBoxInSec == 0 && !S0.OpeningFailed && !S0.AdsAvailable && + S0.CurrentCategory == null && !S0.IsOpening && !S0.IsAccelerated && + !S0.IsShopVisited && !S0.IsReopening && !S0.IsBlockOpenButtons + ) + + val Rewards = listOf(Item("mine")) + Check( + "切片 LootBoxes:SetPossibleRewards / SetCurrentContainer 各写本字段", + ReduceLootBoxes(S0, SetPossibleRewards(Rewards)).RewardsForKeys == Rewards && + ReduceLootBoxes(S0, SetCurrentContainer(ContainerType.EPIC)) + .CurrentContainerType == ContainerType.EPIC && + ReduceLootBoxes(S0, SetCurrentContainer(ContainerType.EPIC)) + .CurrentContainerType != ContainerType.RARE + ) + + val Keyed = ReduceLootBoxes(S0, SetKeys(mapOf("STANDARD" to 3))) + Check( + "切片 LootBoxes:SetKeys 覆盖整张表", + Keyed.Keys == mapOf("STANDARD" to 3) && + ReduceLootBoxes(S0, SetKeys(emptyMap())).Keys.isEmpty() + ) + Check( + "切片 LootBoxes:AddKeys 在原值上加 delta", + ReduceLootBoxes(Keyed, AddKeys("STANDARD", 2)).Keys == mapOf("STANDARD" to 5) && + ReduceLootBoxes(Keyed, AddKeys("STANDARD", -3)).Keys == mapOf("STANDARD" to 0) + ) + Check( + "切片 LootBoxes:AddKeys 缺键抛错(原 nm 是 Map.getValue)", + runCatching { ReduceLootBoxes(Keyed, AddKeys("MISSING", 1)) }.isFailure + ) + + val Described = ReduceLootBoxes(S0, SetContainerData("STANDARD", "标准箱", "普通补给箱")) + Check( + "切片 LootBoxes:SetContainerData 同时写名字与描述两张表", + Described.ContainerNames == mapOf("STANDARD" to "标准箱") && + Described.ContainerDescriptions == mapOf("STANDARD" to "普通补给箱") && + Described.Keys.isEmpty() && Described.Rewards.isEmpty() + ) + + Check( + "切片 LootBoxes:SetGoToGarage 写 gotoGarage", + ReduceLootBoxes(S0, SetGoToGarage(true)).GotoGarage && + !ReduceLootBoxes(S0.copy(GotoGarage = true), SetGoToGarage(false)).GotoGarage + ) + + val Rewarded = ReduceLootBoxes( + S0.copy(IsReopening = true, ShownRewards = listOf(Item("old"))), + SetRewards(Rewards) + ) + Check( + "切片 LootBoxes:SetRewards 换奖励并把 isReopening 归 false", + Rewarded.Rewards == Rewards && !Rewarded.IsReopening && + Rewarded.ShownRewards == listOf(Item("old")) + ) + + val Before = kotlin.js.Date.now().toLong() + val Timed = ReduceLootBoxes(S0, SetTimeToGiveLootBox(3)) + val After = kotlin.js.Date.now().toLong() + Check( + "切片 LootBoxes:SetTimeToGiveLootBox = 当前毫秒 + 秒 * 1000", + Timed.TimeToGiveLootBox >= Before + 3000L && Timed.TimeToGiveLootBox <= After + 3000L + ) + Check( + "切片 LootBoxes:SetPeriodOfGiveLootBox 写周期(秒)", + ReduceLootBoxes(S0, SetPeriodOfGiveLootBox(3600)).PeriodOfGiveLootBoxInSec == 3600 + ) + + val Cleared = ReduceLootBoxes( + S0.copy(Rewards = Rewards, ShownRewards = Rewards, OpeningFailed = true), + ClearRewards + ) + Check( + "切片 LootBoxes:ClearRewards 只清两个奖励表", + Cleared.Rewards.isEmpty() && Cleared.ShownRewards.isEmpty() && Cleared.OpeningFailed + ) + Check( + "切片 LootBoxes:LootBoxOpeningFailed 置 openingFailed", + ReduceLootBoxes(S0, LootBoxOpeningFailed).OpeningFailed + ) + + Check( + "切片 LootBoxes:SelectCategory 写可空 currentCategory", + ReduceLootBoxes(S0, SelectCategory(ContainerType.RARE)).CurrentCategory == ContainerType.RARE && + ReduceLootBoxes(S0.copy(CurrentCategory = ContainerType.RARE), SelectCategory(null)) + .CurrentCategory == null + ) + + val Shown = ReduceLootBoxes(S0.copy(ShownRewards = listOf(Item("old"))), ShownRewards(Rewards)) + Check( + "切片 LootBoxes:ShownRewards 追加到 shownRewards(原 Im 是 List.plus)", + Shown.ShownRewards == listOf(Item("old"), Item("mine")) && Shown.Rewards.isEmpty() + ) + + Check( + "切片 LootBoxes:四个布尔动作各写本字段", + !ReduceLootBoxes(S0.copy(IsOpening = true), SetIsOpening(false)).IsOpening && + ReduceLootBoxes(S0, SetIsOpening(true)).IsOpening && + ReduceLootBoxes(S0, SetIsAccelerated(true)).IsAccelerated && + ReduceLootBoxes(S0, SetIsShopVisited(true)).IsShopVisited + ) + Check( + "切片 LootBoxes:SetIsReopening 置 true;SetIsBlockOpenButtons 写值", + ReduceLootBoxes(S0, SetIsReopening).IsReopening && + ReduceLootBoxes(S0, SetIsBlockOpenButtons(true)).IsBlockOpenButtons && + !ReduceLootBoxes(S0.copy(IsBlockOpenButtons = true), SetIsBlockOpenButtons(false)) + .IsBlockOpenButtons + ) + Check("切片 LootBoxes:无关动作不改状态", ReduceLootBoxes(S0, AddFrags(1)) == S0) + + // 根:lootBoxes 动作只动 lootBoxes + val R0 = RootState() + val R1 = ReduceRoot(R0, SetCurrentContainer(ContainerType.LEGENDARY)) + Check( + "根:SetCurrentContainer 只动 lootBoxes", + R1.LootBoxes.CurrentContainerType == ContainerType.LEGENDARY && + R1.Shop == R0.Shop && R1.Screen == R0.Screen && + R1.InviteState == R0.InviteState && R1.Home == R0.Home + ) + val R2 = ReduceRoot(R1, SetRewards(listOf(Item("root")))) + Check( + "根:SetRewards 只动 lootBoxes,且保留前一次归约", + R2.LootBoxes.Rewards == listOf(Item("root")) && + R2.LootBoxes.CurrentContainerType == ContainerType.LEGENDARY && + R2.Shop == R1.Shop + ) + Check("根:lootBoxes 迁移后无关动作仍返回等值状态", ReduceRoot(R2, AddFrags(1)) == R2) + + val W = RootWiring.ByField("lootBoxes")!! + Check( + "根:lootBoxes 委派到 aH / ws.wrv 且 18 个动作全部具名", + W.SliceClass == "aH" && W.Reducer == "ws.wrv" && W.Actions.size == 18 && + W.Actions.none { it.startsWith("?") } + ) + Check( + "根:lootBoxes 的动作名覆盖 18 个 instanceof 臂", + listOf( + "SetPossibleRewards", "SetCurrentContainer", "SetKeys", "AddKeys", + "SetContainerData", "SetGoToGarage", "SetRewards", "SetTimeToGiveLootBox", + "SetPeriodOfGiveLootBox", "ClearRewards", "LootBoxOpeningFailed", + "SelectCategory", "ShownRewards", "SetIsOpening", "SetIsAccelerated", + "SetIsShopVisited", "SetIsReopening", "SetIsBlockOpenButtons", + ).all { it in W.Actions } + ) + } } val Failed = Results.filter { !it.second }