feat(state): 迁移 contractState

This commit is contained in:
TermiNexus committed 2026-10-24 17:30:56 +08:00
1 parent 0d1e2d00c5
commit 6a1c8b4306
8 files changed
+224 -22

No files matched your search

+11 -5
View File
@@ -143,7 +143,7 @@
`names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。
- 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。
- 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。
- 已迁移切片 **73/89**:
- 已迁移切片 **74/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)
@@ -370,12 +370,17 @@
`u2`(原名 `UpdateSettings`)改名为 `ApplyControlSettings`。
- `battleSettings` → `W0` / `X0().esq`(`BattleSettingsReducer.kt`):11 字段,2 个分支。
`K0`(原名 `UpdateSettings`)改名为 `ApplyBattleSettings`;`_C` 的旧键迁移属 storage port,保留现状态。
- `contractState` → `Ont` / `xv.bti`(`ContractStateReducer.kt`):3 字段,7 个分支。
合同模型跨模块(id / progress / rewards),收口为 `ContractEntry`;`SetContract` 是**合并**(`putAll`);
`UpdateProgress` / `GetReward` 复制单条合同再放回;`Fnt`(原名 `GetReward`,与任务切片同名)改名
`ClaimContractReward`,`Hnt`→`RemoveCompletedContracts` 只留还有奖励的合同;
`InitBattle` 只把 `lastBattleProgress` 归零。
- `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map;
`RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map<String, String>`,不新增 reducer/动作。
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 72 个有 reducer 的切片,
`launchParams` 与其余 **16 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 73/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **804 项全绿**,含 89 槽位完整性、72 个归约切片一次归约互不串片、未迁移字段透传,
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 73 个有 reducer 的切片,
`launchParams` 与其余 **15 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 74/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **815 项全绿**,含 89 槽位完整性、73 个归约切片一次归约互不串片、未迁移字段透传,
以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE,
已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
@@ -400,6 +405,7 @@
设置类切片的 `_C`(共用重载)+ `Bet`/`Fet`/`Uet`/`v2`/`C7`/`E7`/`Iut` 也已登记,
其中原名都是 `UpdateSettings` 的类按切片改名为 `Apply*`,避免生成物同名冲突
(`Bet`/`v2`/`B2`/`u2`/`K0`/`C7` 共 6 个)。
contractState 的 `Fnt`/`Hnt` 也已登记(`Fnt` 因与任务切片同名改名为 `ClaimContractReward`)。
### S4 引擎核心
- 模块:32088(3D 数学)、92980(渲染)、61378(ECS)、55088(物理)、36659 + 71105(资源)、36734(音频)、3569(输入)
+18 -5
View File
@@ -1,5 +1,5 @@
{
"generatedAt": "2026-10-24T09:19:04.205Z",
"generatedAt": "2026-10-24T09:26:08.293Z",
"module": "module_10032.js",
"RootState": {
"Class": "z6",
@@ -79,7 +79,8 @@
"touchscreenHudSettings",
"graphicsSettings",
"controlSettings",
"battleSettings"
"battleSettings",
"contractState"
],
"AnonymousActions": [
{
@@ -1354,6 +1355,18 @@
"Name": "ApplyBattleSettings",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "战斗显示设置整体替换(原名 UpdateSettings)"
},
{
"Local": "Fnt",
"Name": "ClaimContractReward",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "复制该合同并把 rewards 清空(原名 GetReward,与任务切片同名)"
},
{
"Local": "Hnt",
"Name": "RemoveCompletedContracts",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "只保留 rewards 非空的合同"
}
],
"Markers": [
@@ -3054,16 +3067,16 @@
"Reducer": "xv.bti",
"Delegate": "xv.bti(t, n.fpy_1)",
"Actions": [
"?Hnt",
"GetReward",
"ClaimContractReward",
"InitBattle",
"RemoveCompletedContracts",
"SetActiveContract",
"SetBattleProgress",
"SetContract",
"UpdateProgress"
],
"Default": "new Ont()",
"Migrated": false
"Migrated": true
},
{
"Field": "promoCode",
+6
View File
@@ -311,6 +311,9 @@ const AnonymousActionNames = new Map([
['B2', 'ApplyGraphicsSettings'],
['u2', 'ApplyControlSettings'],
['K0', 'ApplyBattleSettings'],
// contractState 切片(Ont / Wnt.bti):Fnt 的 GetReward 与任务切片同名,Hnt 没有 toString。
['Fnt', 'ClaimContractReward'],
['Hnt', 'RemoveCompletedContracts'],
]);
// 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。
@@ -785,6 +788,7 @@ const Migrated = {
},
controlSettings: { Type: 'ControlSettingsStateSlice', Reduce: 'ReduceControlSettings' },
battleSettings: { Type: 'BattleSettingsStateSlice', Reduce: 'ReduceBattleSettings' },
contractState: { Type: 'ContractStateSlice', Reduce: 'ReduceContractState' },
};
for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field);
@@ -1188,6 +1192,8 @@ const AppSettingsAnonymousEffects = {
B2: '画质设置整体替换(原名 UpdateSettings)',
u2: '操作设置整体替换(原名 UpdateSettings)',
K0: '战斗显示设置整体替换(原名 UpdateSettings)',
Fnt: '复制该合同并把 rewards 清空(原名 GetReward,与任务切片同名)',
Hnt: '只保留 rewards 非空的合同',
};
fs.writeFileSync(
@@ -0,0 +1,93 @@
// ContractStateReducer.kt —— 合同(contract)切片(对应 module_10032 的 Ont 状态 + Wnt.bti)
//
// 证据链:
// 状态类 Ont:3 个字段,toString 名 "ContractState(…)";copy 是 ftq(3 字段 + 回调)。
// 默认:activeContractId=null、contracts=emptyMap()、lastBattleProgress=0。
// 根委派:`xv.bti(t, n.fpy_1)`。
// reducer(7 个实例臂):
// Dnt(SetActiveContract)→ activeContractId
// Lnt(SetContract) → contracts 合并(`jm` = copy + `putAll`,键是合同 id)
// Bnt(UpdateProgress) → 复制该合同、写 progress 后放回
// Unt(SetBattleProgress)→ lastBattleProgress
// Fnt(GetReward) → 复制该合同、把 rewards 清空后放回
// Hnt(无 toString) → 只保留 rewards 非空的合同
// InitBattle(原 `FR`) → 只把 lastBattleProgress 归零
//
// 合同模型在另一个模块(字段 `fdv_1`=id / `ldv_1`=progress / `odv_1`=rewards,`tdv()` 返回 rewards),
// 这里收口成 `ContractEntry(contractId, progress, rewards)`。
// `Fnt` 的生成类 `GetReward` 撞到了任务切片的同名动作(字段是 quests),改名 `ClaimContractReward`。
package App.State.Reducers
import App.State.SetActiveContract
import App.State.SetBattleProgress
import App.State.SetContract
import App.State.UpdateProgress
/* ------------------------------------------------------------------ 动作 */
/** 原 `Fnt`(原名 `GetReward`,与任务切片的同名类冲突):领取该合同的奖励。 */
data class ClaimContractReward(val contractId: Any?)
/** 原 `Hnt`:战斗结束后清掉已经没有奖励的合同。 */
object RemoveCompletedContracts
/* ------------------------------------------------------------------ 模型 */
data class ContractEntry(
val ContractId: Any?,
val Progress: Any? = null,
val Rewards: List<Any?> = emptyList(),
)
/* ------------------------------------------------------------------ 状态 */
data class ContractStateSlice(
val ActiveContractId: Any? = null,
val Contracts: Map<Any?, Any?> = emptyMap(),
val LastBattleProgress: Any? = 0,
)
/* ------------------------------------------------------------------ reducer */
private fun UpdateEntry(
Contracts: Map<Any?, Any?>,
ContractId: Any?,
Transform: (ContractEntry) -> ContractEntry,
): Map<Any?, Any?> {
val Entry = Contracts[ContractId] as? ContractEntry ?: return Contracts
return Contracts + (ContractId to Transform(Entry))
}
fun ReduceContractState(
State0: ContractStateSlice,
Action: Any,
): ContractStateSlice = when (Action) {
is SetActiveContract -> State0.copy(ActiveContractId = Action.contractId)
is SetContract -> {
val Incoming = Action.contract as? List<*> ?: listOfNotNull(Action.contract)
val Merged = LinkedHashMap<Any?, Any?>(State0.Contracts)
for (Item in Incoming) {
val Entry = Item as? ContractEntry ?: continue
Merged[Entry.ContractId] = Entry
}
State0.copy(Contracts = Merged)
}
is UpdateProgress -> State0.copy(
Contracts = UpdateEntry(State0.Contracts, Action.contactId) {
it.copy(Progress = Action.progress)
},
)
is SetBattleProgress -> State0.copy(LastBattleProgress = Action.progress)
is ClaimContractReward -> State0.copy(
Contracts = UpdateEntry(State0.Contracts, Action.contractId) {
it.copy(Rewards = emptyList())
},
)
is RemoveCompletedContracts -> State0.copy(
Contracts = State0.Contracts.filterValues {
(it as? ContractEntry)?.Rewards?.isNotEmpty() == true
},
)
is InitBattle -> State0.copy(LastBattleProgress = 0)
else -> State0
}
@@ -1,7 +1,7 @@
// RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6)
//
// 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。
// 这里采用同一形状。已迁移的 73 个切片里,72 个真正调用 reducer,launchParams 只做类型收口:
// 这里采用同一形状。已迁移的 74 个切片里,73 个真正调用 reducer,launchParams 只做类型收口:
//
// inviteState → ReduceInvite(aD / pD.er5)
// loginByPassword → ReduceLoginByPassword(gD / MD.cr6)
@@ -75,9 +75,10 @@
// graphicsSettings → ReduceGraphicsSettings(L2 / lc.isu,根委派 `lc.isu(t, n.ypw_1)`)
// controlSettings → ReduceControlSettings(i2 / _2().ess,根委派 `_2().ess(t, n.zpw_1)`)
// battleSettings → ReduceBattleSettings(W0 / X0().esq,根委派 `X0().esq(t, n.xpw_1)`)
// contractState → ReduceContractState(Ont / xv.bti,根委派 `xv.bti(t, n.fpy_1)`)
// launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap())
//
// 其余 16 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 其余 15 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。
// 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。
package App.State
@@ -97,6 +98,7 @@ import App.State.Reducers.ReduceCaptcha
import App.State.Reducers.ReduceClanUserInfo
import App.State.Reducers.ReduceClanChat
import App.State.Reducers.ReduceContextTutorial
import App.State.Reducers.ReduceContractState
import App.State.Reducers.ReduceControlSettings
import App.State.Reducers.ReduceCreatorTag
import App.State.Reducers.ReduceDroppableGold
@@ -240,6 +242,7 @@ object RootReducer {
GraphicsSettings = ReduceGraphicsSettings(State0.GraphicsSettings, Action),
ControlSettings = ReduceControlSettings(State0.ControlSettings, Action),
BattleSettings = ReduceBattleSettings(State0.BattleSettings, Action),
ContractState = ReduceContractState(State0.ContractState, Action),
)
}
@@ -20,6 +20,7 @@ import App.State.Reducers.CaptchaStateSlice
import App.State.Reducers.ClanChatStateSlice
import App.State.Reducers.ClanUserInfoStateSlice
import App.State.Reducers.ContextTutorialStateSlice
import App.State.Reducers.ContractStateSlice
import App.State.Reducers.ControlSettingsStateSlice
import App.State.Reducers.CreatorTagStateSlice
import App.State.Reducers.DroppableGoldStateSlice
@@ -251,8 +252,8 @@ data class RootState(
val UserReportsState: UserReportsStateSlice = UserReportsStateSlice(),
/** 原 tankBuilderState(brt / wp.ati)。未迁移:原样透传。 */
val TankBuilderState: Any? = null,
/** 原 contractState(Ont / xv.bti)。未迁移:原样透传。 */
val ContractState: Any? = null,
/** 原 contractState(Ont / xv.bti)。已迁移。 */
val ContractState: ContractStateSlice = ContractStateSlice(),
/** 原 promoCode(cZ / Q_.ysg)。已迁移。 */
val PromoCode: PromoCodeStateSlice = PromoCodeStateSlice(),
/** 原 referralEvent(pQ / lh.osk)。已迁移。 */
@@ -99,14 +99,14 @@ object RootWiring {
SliceWiring("availabilityByRank", "cpy_1", "tet", "\$p.xth", "\$p.xth(t, n.cpy_1)", listOf("SetLoadedData")),
SliceWiring("userReportsState", "dpy_1", "Zst", "qot.zth", "(null == sd && new qot(), sd).zth(t, n.dpy_1)", listOf("InitUserReports", "ReportBattleChatMessage", "ReportLobbyChatMessage", "ReportProBattleName", "SetAvailableGameReports", "SetAvailableReports")),
SliceWiring("tankBuilderState", "epy_1", "brt", "wp.ati", "wp.ati(t, n.epy_1)", listOf("?Trt", "SetKode", "SetParts", "SetPartsInfo")),
SliceWiring("contractState", "fpy_1", "Ont", "xv.bti", "xv.bti(t, n.fpy_1)", listOf("?Hnt", "GetReward", "InitBattle", "SetActiveContract", "SetBattleProgress", "SetContract", "UpdateProgress")),
SliceWiring("contractState", "fpy_1", "Ont", "xv.bti", "xv.bti(t, n.fpy_1)", listOf("ClaimContractReward", "InitBattle", "RemoveCompletedContracts", "SetActiveContract", "SetBattleProgress", "SetContract", "UpdateProgress")),
SliceWiring("promoCode", "gpy_1", "cZ", "Q_.ysg", "Q_.ysg(t, n.gpy_1)", listOf("ActivatePromoCode", "SetCodeValidateStatus")),
SliceWiring("referralEvent", "hpy_1", "pQ", "lh.osk", "lh.osk(t, n.hpy_1)", listOf("SetPromoCode", "SetReferralEventData", "SetReferrals", "SetReferrer")),
SliceWiring("supportUrl", "ipy_1", "I4", "Yl.ct7", "Yl.ct7(t, n.ipy_1)", listOf("ClearSupportUrl", "SetUrl")),
)
/** 已迁移(强类型;launchParams 为透传,不调用 reducer)的字段。 */
val Migrated: Set<String> = 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")
val Migrated: Set<String> = 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")
/** 未迁移字段:归约时原样透传,作为后续迁移清单。 */
val Unmigrated: List<SliceWiring> get() = All.filter { it.Field !in Migrated }
@@ -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;
// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState;
// ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派
//
// 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1)
@@ -698,6 +698,15 @@ import App.State.Reducers.GraphicsSettingsStateSlice
import App.State.Reducers.ReduceBattleSettings
import App.State.Reducers.ReduceControlSettings
import App.State.Reducers.ReduceGraphicsSettings
import App.State.SetActiveContract
import App.State.SetBattleProgress
import App.State.SetContract
import App.State.UpdateProgress
import App.State.Reducers.ClaimContractReward
import App.State.Reducers.ContractEntry
import App.State.Reducers.ContractStateSlice
import App.State.Reducers.ReduceContractState
import App.State.Reducers.RemoveCompletedContracts
import App.State.Reducers.UserRankBounds
import App.State.Reducers.UserStateSlice
import App.State.Reducers.ClearHullData
@@ -1125,7 +1134,7 @@ object StateCheck {
Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled)
}
// ⑭ 根 reducer:89 个槽位的委派表 + 73 个已迁移切片(72 归约 + launchParams 透传)
// ⑭ 根 reducer:89 个槽位的委派表 + 74 个已迁移切片(73 归约 + launchParams 透传)
run {
Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89)
Check(
@@ -1189,7 +1198,7 @@ object StateCheck {
LocalBattleW.Reducer == "(inline@localBattleUserState)"
)
Check(
"根:迁移表 = 73 个已迁移字段,其余 16 个待迁",
"根:迁移表 = 74 个已迁移字段,其余 15 个待迁",
RootReducer.Migrated ==
setOf(
"inviteState", "loginByPassword", "twitchState", "home",
@@ -1211,9 +1220,9 @@ object StateCheck {
"battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial",
"clanChatState", "battleSearchState", "battleStatistics",
"soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings",
"graphicsSettings", "controlSettings", "battleSettings",
"graphicsSettings", "controlSettings", "battleSettings", "contractState",
) &&
RootWiring.Unmigrated.size == 16
RootWiring.Unmigrated.size == 15
)
// 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片
@@ -1269,7 +1278,7 @@ object StateCheck {
S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader")
)
Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5)
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 73/89")
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 74/89")
val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) })
Store.Dispatch(SetStatus("LOGGING_IN"))
@@ -6116,6 +6125,77 @@ object StateCheck {
)
}
// 84 切片 reducer:ContractState(Ont / xv.bti,3 字段;7 个动作)
run {
fun Contract(Id: Any?, Progress: Any? = 0, Rewards: List<Any?> = emptyList()) =
ContractEntry(ContractId = Id, Progress = Progress, Rewards = Rewards)
val S0 = ContractStateSlice()
Check(
"切片 ContractState:默认值完整",
S0.ActiveContractId == null && S0.Contracts.isEmpty() &&
S0.LastBattleProgress == 0
)
val S1 = ReduceContractState(S0, SetActiveContract("c1"))
Check("切片 ContractState:SetActiveContract 写 id", S1.ActiveContractId == "c1")
val S2 = ReduceContractState(S1, SetContract(listOf(Contract("c1"), Contract("c2"))))
Check(
"切片 ContractState:SetContract 按合同 id 合并",
S2.Contracts.keys == setOf<Any?>("c1", "c2")
)
Check(
"切片 ContractState:UpdateProgress 复制该合同",
ReduceContractState(S2, UpdateProgress("c1", 5))
.let { (it.Contracts["c1"] as ContractEntry).Progress == 5 }
)
Check(
"切片 ContractState:SetBattleProgress 写战斗进度",
ReduceContractState(S2, SetBattleProgress(7)).LastBattleProgress == 7
)
val S3 = ReduceContractState(
ReduceContractState(S2, SetContract(listOf(Contract("c2", 1, listOf("r"))))),
ClaimContractReward("c2"),
)
Check(
"切片 ContractState:ClaimContractReward 清空该合同奖励",
(S3.Contracts["c2"] as ContractEntry).Rewards.isEmpty() &&
(S3.Contracts["c1"] as ContractEntry).Rewards.isEmpty()
)
Check(
"切片 ContractState:RemoveCompletedContracts 只留还有奖励的",
ReduceContractState(
ReduceContractState(
S2,
SetContract(listOf(Contract("c1", 2, listOf("r")))),
),
RemoveCompletedContracts,
).Contracts.keys == setOf<Any?>("c1")
)
Check(
"切片 ContractState:InitBattle 只把战斗进度归零",
ReduceContractState(S3, InitBattle()).let {
it.LastBattleProgress == 0 && it.Contracts.keys == setOf<Any?>("c1", "c2")
}
)
Check(
"切片 ContractState:无关动作不改状态",
ReduceContractState(S3, AddFrags(1)) == S3
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetActiveContract("x"))
Check(
"根:SetActiveContract 只动 contractState",
Root1.ContractState.ActiveContractId == "x" &&
Root1.BattleSettings == Root0.BattleSettings
)
val W = RootWiring.ByField("contractState")!!
Check(
"根:contractState 委派到 Ont / xv.bti 且 7 个动作",
W.SliceClass == "Ont" && W.Reducer == "xv.bti" && W.Actions.size == 7
)
}
val Failed = Results.filter { !it.second }
println("TankiOffline 状态层自检:${Results.size} 项")
for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)