feat(state): 迁移 battleStatistics

This commit is contained in:
TermiNexus committed 2026-10-24 16:55:21 +08:00
1 parent 4aec83f684
commit 86979eb168
8 files changed
+466 -34

No files matched your search

+9 -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` 动作名。
- 已迁移切片 **65/89**:
- 已迁移切片 **66/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)
@@ -347,12 +347,15 @@
- `battleSearchState` → `Itt` / `Htt().vtg`(`BattleSearchStateReducer.kt`):5 字段,9 个分支。
`Fight` 只发副作用、状态不变;`RemoveBattle` 仅当选中 id 匹配时清空;
`selectedFormat` 默认值来自服务定位器,按 `null` 收口。
- `battleStatistics` → `jR` / `rr.fqr`(`BattleStatisticsReducer.kt`):27 字段,22 个分支。
`FR`(14 字段的 Init)与通知切片的同名类冲突,改名为 `InitBattle`(contractState 之后复用);
其余为战斗元信息 setter + 回合 / 统计面板 / 菜单 / 旗主 / 加载位;`SetRemainingTime` 用 `$m` 把时长转秒。
- `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map;
`RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map<String, String>`,不新增 reducer/动作。
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 64 个有 reducer 的切片,
`launchParams` 与其余 **24 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 65/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **741 项全绿**,含 89 槽位完整性、64 个归约切片一次归约互不串片、未迁移字段透传,
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 65 个有 reducer 的切片,
`launchParams` 与其余 **23 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 66/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **755 项全绿**,含 89 槽位完整性、65 个归约切片一次归约互不串片、未迁移字段透传,
以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE,
已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
@@ -373,6 +376,7 @@
userRenameState 的 5 个也已登记(其中 `mtt` 因同名类冲突改名为 `AddInvitedUser`);
battlePauseState 的 2 个、battleViewState 的 12 个、tankActionsLogState 的 1 个同样按效果定名。
clanChatState 的 3 个(`$nt`/`xnt`/`Tnt`)、battleSearchState 的 1 个也已登记。
battleStatistics 的 12 个(含改名的 `FR`→`InitBattle`)同样按效果定名。
### S4 引擎核心
- 模块:32088(3D 数学)、92980(渲染)、61378(ECS)、55088(物理)、36659 + 71105(资源)、36734(音频)、3569(输入)
+89 -16
View File
@@ -1,5 +1,5 @@
{
"generatedAt": "2026-10-24T08:38:53.378Z",
"generatedAt": "2026-10-24T08:50:07.796Z",
"module": "module_10032.js",
"RootState": {
"Class": "z6",
@@ -71,7 +71,8 @@
"tankActionsLogState",
"suppliesTutorial",
"clanChatState",
"battleSearchState"
"battleSearchState",
"battleStatistics"
],
"AnonymousActions": [
{
@@ -1208,6 +1209,78 @@
"Name": "ClearSelectedBattle",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "selectedBattleId=null(`n.vtn(cd, cd, cd, cd, null)`)"
},
{
"Local": "FR",
"Name": "InitBattle",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "14 字段战斗初始化(原名 Init,与通知切片的同名类冲突,改名 InitBattle)"
},
{
"Local": "PR",
"Name": "ResetRoundState",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "roundStarted=false、menuVisible=false(`n.cqq(…, !1, cd, !1)`)"
},
{
"Local": "KR",
"Name": "ShowStat",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "statVisible=true;menuVisible=!(menu && !stat) && menu(`n.cqq(…, !0, cd, cd, …)`)"
},
{
"Local": "YR",
"Name": "HideStat",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "statVisible=false(`n.cqq(…, !1)`)"
},
{
"Local": "XR",
"Name": "BeginRound",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isContinueBattle=false、score=emptyMap()、flagOrBallOwner=null、statVisible=false、roundStarted=true、menuVisible=false"
},
{
"Local": "UR",
"Name": "ContinueBattle",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isContinueBattle=true(`n.cqq(…, !0)`)"
},
{
"Local": "QR",
"Name": "CloseStatMenu",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "menuVisible=false(`n.cqq(…, !1)`)"
},
{
"Local": "ZR",
"Name": "ToggleStatMenu",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "menuVisible=!menuVisible(`n.cqq(…, !n.bq2_1)`)"
},
{
"Local": "DR",
"Name": "ClearFlagOrBallOwner",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "flagOrBallOwner=null(`n.cqq(…, null)`)"
},
{
"Local": "tP",
"Name": "MarkBattleLoaded",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "battleLoaded=true(`n.cqq(…, !0)`)"
},
{
"Local": "nP",
"Name": "MarkBattleUnloaded",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "battleLoaded=false(`n.cqq(…, !1)`)"
},
{
"Local": "HR",
"Name": "ResetBattleStatistics",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "new jR() 整片复位"
}
],
"Markers": [
@@ -1337,19 +1410,17 @@
"Reducer": "rr.fqr",
"Delegate": "rr.fqr(t, n.bpv_1)",
"Actions": [
"?DR",
"?HR",
"?KR",
"?PR",
"?QR",
"?UR",
"?XR",
"?YR",
"?ZR",
"?nP",
"?tP",
"Init",
"BeginRound",
"ClearFlagOrBallOwner",
"CloseStatMenu",
"ContinueBattle",
"HideStat",
"InitBattle",
"InitBattleModeWithLevels",
"MarkBattleLoaded",
"MarkBattleUnloaded",
"ResetBattleStatistics",
"ResetRoundState",
"SetBattleMode",
"SetDisplayClientInfo",
"SetFlagOrBallOwner",
@@ -1357,11 +1428,13 @@
"SetReArmor",
"SetRemainingTime",
"SetRoundRestartTime",
"ShowStat",
"ToggleStatMenu",
"UpdateFund",
"UpdateScore"
],
"Default": "new jR()",
"Migrated": false
"Migrated": true
},
{
"Field": "battleTutorial",
@@ -2910,7 +2983,7 @@
"Actions": [
"?Hnt",
"GetReward",
"Init",
"InitBattle",
"SetActiveContract",
"SetBattleProgress",
"SetContract",
+36
View File
@@ -281,6 +281,19 @@ const AnonymousActionNames = new Map([
['Tnt', 'ResetClanChat'],
// battleSearchState 切片(Itt / Gtt.vtg):Dtt 没有 toString。
['Dtt', 'ClearSelectedBattle'],
// battleStatistics 切片(jR / rP.fqr):11 个类没有 toString;FR 与通知切片的 Init 同名,改名。
['FR', 'InitBattle'],
['PR', 'ResetRoundState'],
['KR', 'ShowStat'],
['YR', 'HideStat'],
['XR', 'BeginRound'],
['UR', 'ContinueBattle'],
['QR', 'CloseStatMenu'],
['ZR', 'ToggleStatMenu'],
['DR', 'ClearFlagOrBallOwner'],
['tP', 'MarkBattleLoaded'],
['nP', 'MarkBattleUnloaded'],
['HR', 'ResetBattleStatistics'],
]);
// 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。
@@ -739,6 +752,7 @@ const Migrated = {
},
clanChatState: { Type: 'ClanChatStateSlice', Reduce: 'ReduceClanChat' },
battleSearchState: { Type: 'BattleSearchStateSlice', Reduce: 'ReduceBattleSearch' },
battleStatistics: { Type: 'BattleStatisticsStateSlice', Reduce: 'ReduceBattleStatistics' },
};
for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field);
@@ -1113,6 +1127,22 @@ const BattleSearchAnonymousEffects = {
Dtt: 'selectedBattleId=null(`n.vtn(cd, cd, cd, cd, null)`)',
};
// battleStatistics 切片(jR / rP.fqr):11 个类没有 toString;FR 与通知切片 Init 同名。
const BattleStatisticsAnonymousEffects = {
FR: '14 字段战斗初始化(原名 Init,与通知切片的同名类冲突,改名 InitBattle)',
PR: 'roundStarted=false、menuVisible=false(`n.cqq(…, !1, cd, !1)`)',
KR: 'statVisible=true;menuVisible=!(menu && !stat) && menu(`n.cqq(…, !0, cd, cd, …)`)',
YR: 'statVisible=false(`n.cqq(…, !1)`)',
XR: 'isContinueBattle=false、score=emptyMap()、flagOrBallOwner=null、statVisible=false、roundStarted=true、menuVisible=false',
UR: 'isContinueBattle=true(`n.cqq(…, !0)`)',
QR: 'menuVisible=false(`n.cqq(…, !1)`)',
ZR: 'menuVisible=!menuVisible(`n.cqq(…, !n.bq2_1)`)',
DR: 'flagOrBallOwner=null(`n.cqq(…, null)`)',
tP: 'battleLoaded=true(`n.cqq(…, !0)`)',
nP: 'battleLoaded=false(`n.cqq(…, !1)`)',
HR: 'new jR() 整片复位',
};
fs.writeFileSync(
path.join(P, 'names', 'root_wiring.json'),
JSON.stringify(
@@ -1369,6 +1399,12 @@ fs.writeFileSync(
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(BattleStatisticsAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
],
Markers: [
{
@@ -0,0 +1,183 @@
// BattleStatisticsReducer.kt —— 战斗统计切片(对应 module_10032 的 jR 状态 + rP.fqr)
//
// 证据链:
// 状态类 jR:27 个字段,toString 名 "BattleStatistics(…)";copy 是 cqq(27 字段 + 回调)。
// 根委派:`rr.fqr(t, n.bpv_1)`。
// reducer(22 个实例臂):
// FR(Init) → 用 14 字段的重置动作灌入战斗元信息
// GR(SetDisplayClientInfo) → DisplayClientInfo
// IR(SetBattleMode) → Mode
// AR(SetReArmor) → IsReArmorEnabled
// RR(SetRankRange) → RankRange
// ER(UpdateScore) → Score 写键值
// TR(UpdateFund) → Fund
// PR / KR / YR / XR / UR / QR / ZR(无 toString)
// → 回合开始/结束、统计面板与菜单的显示切换
// NR(SetRoundRestartTime) → RoundRestartTime
// MR(SetRemainingTime) → RemainingTimeInSec(时长 → 秒)
// OR(SetFlagOrBallOwner) → FlagOrBallOwner
// DR / tP / nP(无 toString) → 清旗主、BattleLoaded 开关
// HR(无 toString) → `new jR()` 整片复位
// JR(InitBattleModeWithLevels)→ GameModeLevelsCount
//
// `FR` 与 contractState 共用同一个 14 字段动作类,生成物里的 `Init` 撞到了通知切片的
// 同名类,因此这里改名为 `InitBattle`(两个切片都直接读这 14 个字段)。
package App.State.Reducers
import App.State.InitBattleModeWithLevels
import App.State.Range
import App.State.SetBattleMode
import App.State.SetDisplayClientInfo
import App.State.SetFlagOrBallOwner
import App.State.SetRankRange
import App.State.SetReArmor
import App.State.SetRemainingTime
import App.State.SetRoundRestartTime
import App.State.UpdateFund
import App.State.UpdateScore
/* ------------------------------------------------------------------ 动作 */
/** 原 `FR`(原名 `Init`,与通知切片的同名类冲突):14 字段的战斗初始化动作。 */
data class InitBattle(
val battleId: Any? = null,
val remainingTime: Any? = null,
val battleName: String = "",
val matchBattle: Boolean = true,
val isParkourMode: Boolean = false,
val isTutorial: Boolean = false,
val valuableBattle: Boolean = true,
val scoreLimit: Int = 0,
val fund: Int = 0,
val options: List<Any?> = emptyList(),
val customBattleName: String? = null,
val battleFormat: Any? = null,
val isMatchmakingMark: Boolean = false,
val mapNameWithoutMode: String = "",
)
/** 原 `PR`:回合结束,复位开始位与菜单。 */
object ResetRoundState
/** 原 `KR`:显示统计面板(菜单保持原状,除非原本「菜单开且统计关」)。 */
object ShowStat
/** 原 `YR`:隐藏统计面板。 */
object HideStat
/** 原 `XR`:新回合开始,清分数 / 旗主并打开回合位。 */
object BeginRound
/** 原 `UR`:标记为连续战斗。 */
object ContinueBattle
/** 原 `QR`:关闭统计菜单。 */
object CloseStatMenu
/** 原 `ZR`:切换统计菜单。 */
object ToggleStatMenu
/** 原 `DR`:清空旗主 / 球主。 */
object ClearFlagOrBallOwner
/** 原 `tP`:标记战斗资源已加载。 */
object MarkBattleLoaded
/** 原 `nP`:标记战斗资源未加载。 */
object MarkBattleUnloaded
/** 原 `HR`:整片复位(`new jR()`)。 */
object ResetBattleStatistics
/* ------------------------------------------------------------------ 状态 */
data class BattleStatisticsStateSlice(
val BattleId: Any? = null,
val MapName: String = "",
val Mode: String? = null,
val IsMMBattle: Boolean = true,
val IsParkourMode: Boolean = false,
val IsTutorial: Boolean = false,
val ValuableBattle: Boolean = true,
val IsReArmorEnabled: Boolean = true,
val RankRange: Range? = null,
val IsContinueBattle: Boolean = false,
val RoundRestartTime: Any? = null,
val Score: Map<Any?, Any?> = emptyMap(),
val Fund: Int = 0,
val GameModeLevelsCount: Int = 0,
val FlagOrBallOwner: Any? = null,
val StatVisible: Boolean = false,
val RoundStarted: Boolean = false,
val ScoreLimit: Int = 0,
val MenuVisible: Boolean = false,
val RemainingTimeInSec: Int = -1,
val BattleLoaded: Boolean = false,
val Options: List<Any?> = emptyList(),
val CustomBattleName: String? = null,
val BattleFormat: Any? = null,
val IsMatchmakingMark: Boolean = false,
val MapNameWithoutMode: String = "",
val DisplayClientInfo: Boolean = true,
)
/* ------------------------------------------------------------------ reducer */
/** 原 `$m`:时长 → 秒。 */
private fun AsSeconds(Value: Any?): Int = (Value as? Number)?.toInt() ?: 0
fun ReduceBattleStatistics(
State0: BattleStatisticsStateSlice,
Action: Any,
): BattleStatisticsStateSlice = when (Action) {
is InitBattle -> State0.copy(
MapName = Action.battleName,
IsMMBattle = Action.matchBattle,
IsParkourMode = Action.isParkourMode,
IsTutorial = Action.isTutorial,
ValuableBattle = Action.valuableBattle,
IsContinueBattle = false,
Score = emptyMap(),
Fund = Action.fund,
ScoreLimit = Action.scoreLimit,
MenuVisible = false,
RemainingTimeInSec = AsSeconds(Action.remainingTime),
Options = Action.options,
CustomBattleName = Action.customBattleName,
BattleFormat = Action.battleFormat,
IsMatchmakingMark = Action.isMatchmakingMark,
MapNameWithoutMode = Action.mapNameWithoutMode,
)
is SetDisplayClientInfo -> State0.copy(DisplayClientInfo = Action.visible)
is SetBattleMode -> State0.copy(Mode = Action.mode)
is SetReArmor -> State0.copy(IsReArmorEnabled = Action.enabled)
is SetRankRange -> State0.copy(RankRange = Action.rankRange)
is UpdateScore -> State0.copy(Score = State0.Score + (Action.team to Action.score))
is UpdateFund -> State0.copy(Fund = Action.fund)
is ResetRoundState -> State0.copy(RoundStarted = false, MenuVisible = false)
is ShowStat -> State0.copy(
StatVisible = true,
MenuVisible = !(State0.MenuVisible && !State0.StatVisible) && State0.MenuVisible,
)
is HideStat -> State0.copy(StatVisible = false)
is BeginRound -> State0.copy(
IsContinueBattle = false,
Score = emptyMap(),
FlagOrBallOwner = null,
StatVisible = false,
RoundStarted = true,
MenuVisible = false,
)
is ContinueBattle -> State0.copy(IsContinueBattle = true)
is CloseStatMenu -> State0.copy(MenuVisible = false)
is ToggleStatMenu -> State0.copy(MenuVisible = !State0.MenuVisible)
is SetRoundRestartTime -> State0.copy(RoundRestartTime = Action.time)
is SetRemainingTime -> State0.copy(RemainingTimeInSec = AsSeconds(Action.time))
is SetFlagOrBallOwner -> State0.copy(FlagOrBallOwner = Action.ownerUserId)
is ClearFlagOrBallOwner -> State0.copy(FlagOrBallOwner = null)
is MarkBattleLoaded -> State0.copy(BattleLoaded = true)
is MarkBattleUnloaded -> State0.copy(BattleLoaded = false)
is ResetBattleStatistics -> BattleStatisticsStateSlice()
is InitBattleModeWithLevels -> State0.copy(GameModeLevelsCount = Action.levelsCount as? Int ?: 0)
else -> State0
}
@@ -1,7 +1,7 @@
// RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6)
//
// 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。
// 这里采用同一形状。已迁移的 65 个切片里,64 个真正调用 reducer,launchParams 只做类型收口:
// 这里采用同一形状。已迁移的 66 个切片里,65 个真正调用 reducer,launchParams 只做类型收口:
//
// inviteState → ReduceInvite(aD / pD.er5)
// loginByPassword → ReduceLoginByPassword(gD / MD.cr6)
@@ -67,9 +67,10 @@
// suppliesTutorial → ReduceSuppliesTutorial(ust / Uw.uth,根委派 `Uw.uth(t, n.zpx_1)`)
// 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)`)
// launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap())
//
// 其余 24 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 其余 23 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。
// 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。
package App.State
@@ -80,6 +81,7 @@ import App.State.Reducers.ReduceAntiAddiction
import App.State.Reducers.ReduceBattleResult
import App.State.Reducers.ReduceBattlePause
import App.State.Reducers.ReduceBattleSearch
import App.State.Reducers.ReduceBattleStatistics
import App.State.Reducers.ReduceBattleTutorial
import App.State.Reducers.ReduceBattleView
import App.State.Reducers.ReduceBattleInvite
@@ -216,6 +218,7 @@ object RootReducer {
SuppliesTutorial = ReduceSuppliesTutorial(State0.SuppliesTutorial, Action),
ClanChatState = ReduceClanChat(State0.ClanChatState, Action),
BattleSearchState = ReduceBattleSearch(State0.BattleSearchState, Action),
BattleStatistics = ReduceBattleStatistics(State0.BattleStatistics, Action),
)
}
@@ -12,6 +12,7 @@ import App.State.Reducers.BattleInviteStateSlice
import App.State.Reducers.BattlePauseStateSlice
import App.State.Reducers.BattleResultStateSlice
import App.State.Reducers.BattleSearchStateSlice
import App.State.Reducers.BattleStatisticsStateSlice
import App.State.Reducers.BattleTutorialStateSlice
import App.State.Reducers.BattleViewStateSlice
import App.State.Reducers.CaptchaStateSlice
@@ -79,8 +80,8 @@ data class RootState(
val Screen: ScreenStateSlice = ScreenStateSlice(),
/** 原 modalScreen(nV / Fs.js1)。已迁移。 */
val ModalScreen: ModalScreenStateSlice = ModalScreenStateSlice(),
/** 原 battleStatistics(jR / rr.fqr)。未迁移:原样透传。 */
val BattleStatistics: Any? = null,
/** 原 battleStatistics(jR / rr.fqr)。已迁移。 */
val BattleStatistics: BattleStatisticsStateSlice = BattleStatisticsStateSlice(),
/** 原 battleTutorial(rE / Q.sq8)。已迁移。 */
val BattleTutorial: BattleTutorialStateSlice = BattleTutorialStateSlice(),
/** 原 garage(UB / hF)。已迁移。 */
@@ -17,7 +17,7 @@ object RootWiring {
SliceWiring("user", "ypu_1", "t8", "M8().qtd", "M8().qtd(t, n.ypu_1)", listOf("AddCoins", "AddRubies", "ClearInDebt", "DisableLobbyTutorial", "SetBatteryLevel", "SetClanInfo", "SetCoins", "SetDisabledWithoutPremiumLocations", "SetHasTemporaryCash", "SetInDebt", "SetPremiumExpirationTime", "SetRankNames", "SetRubies", "SetUserId", "SetUserPropertiesAction", "SpendCoins", "SpendCrystals", "SpendRubies", "UpdateClanTag", "UpdateCrystals", "UpdateEventPass", "UpdateGearScore", "UpdatePremium", "UpdateRank", "UpdateScore", "UpdateUid", "UpdateUserRating")),
SliceWiring("screen", "zpu_1", "WK", "dY().ks5", "dY().ks5(t, n.zpu_1)", listOf("ClearScreenStack", "DelayNextGoTo", "DisableBack", "DisableForeground", "DisplayMainEntranceScreen", "DisplayReauthorizationMessage", "EnableBack", "EnableForeground", "FilterScreenStack", "GoTo", "PopScreen", "PressedAnyButton", "ResetScreenStackOnNextGoTo")),
SliceWiring("modalScreen", "apv_1", "nV", "Fs.js1", "Fs.js1(t, n.apv_1)", listOf("AddModal", "CloseCurrentModal", "RemoveCurrentByName", "RemoveModal")),
SliceWiring("battleStatistics", "bpv_1", "jR", "rr.fqr", "rr.fqr(t, n.bpv_1)", listOf("?DR", "?HR", "?KR", "?PR", "?QR", "?UR", "?XR", "?YR", "?ZR", "?nP", "?tP", "Init", "InitBattleModeWithLevels", "SetBattleMode", "SetDisplayClientInfo", "SetFlagOrBallOwner", "SetRankRange", "SetReArmor", "SetRemainingTime", "SetRoundRestartTime", "UpdateFund", "UpdateScore")),
SliceWiring("battleStatistics", "bpv_1", "jR", "rr.fqr", "rr.fqr(t, n.bpv_1)", listOf("BeginRound", "ClearFlagOrBallOwner", "CloseStatMenu", "ContinueBattle", "HideStat", "InitBattle", "InitBattleModeWithLevels", "MarkBattleLoaded", "MarkBattleUnloaded", "ResetBattleStatistics", "ResetRoundState", "SetBattleMode", "SetDisplayClientInfo", "SetFlagOrBallOwner", "SetRankRange", "SetReArmor", "SetRemainingTime", "SetRoundRestartTime", "ShowStat", "ToggleStatMenu", "UpdateFund", "UpdateScore")),
SliceWiring("battleTutorial", "cpv_1", "rE", "Q.sq8", "Q.sq8(t, n.cpv_1)", listOf("MarkMovementStarted", "ResetBattleTutorial", "SetFragsCount", "ShowMessage")),
SliceWiring("garage", "dpv_1", "UB", "hF", "hF(t, n)", listOf("AddGarageItemCount", "AddNewItem", "AddOverdriveDescription", "AddRelatedBazooka", "AddRelatedResistanceModule", "AddToFavoriteItem", "BuyProtectionSlot", "ClearWaitServerItemMount", "GarageAddItemToDepot", "GarageApplyResistanceMount", "GarageBuyDevice", "GarageDeviceTrySetInfinityLifetime", "GarageDevicesLoadedAction", "GarageLoaded", "GarageMountItem", "GarageMountItems", "GarageRemoveDevice", "GarageRemoveDiscountForDevice", "GarageRemoveItemFromDepot", "GarageRemoveItemFromStore", "GarageResistanceUnMount", "GarageSelectAlteration", "GarageSelectItem", "GarageSelectShotSkin", "GarageSelectSkin", "GarageUnmountItem", "GarageUpdateItemCount", "GarageUpgradeItemAction", "GarageUpgradeItemConfirm", "GarageUpgradeItemCoolDownFinished", "GarageUpgradeItemCoolDownStarted", "GoTo", "HideVideoInDescription", "InitState", "RemoveFromFavoriteItem", "RemoveFromSale", "RemoveNewItem", "RemoveWaitServerItemMount", "ResetGarage", "ResetMountDelay", "SelectGarageCategory", "SetCooldownReduceEndTime", "SetGarageNotLoaded", "SetGarageObject", "SetGarageSkinsAction", "SetItemRarity", "SetItemsOnDepot", "SetItemsOnMarket", "SetMountedItems", "SetMountedShotSkinForItems", "SetMountedSkinForItems", "SetNewItems", "SetResistanceReadyToMount", "SetRestrictionMount", "ShowGarageDiscountNotification", "StartCooldownReduce", "SwitchingRowsScroll", "UpdateDiscount")),
SliceWiring("garagePreview", "epv_1", "IG", "fs.eru", "fs.eru(t, n.epv_1)", listOf("AddRequestedItem", "CaptureScreenshot", "CaptureVideo", "ClearDroneData", "ClearGrenadeData", "ClearHasGrenades", "ClearHullData", "ClearWeaponData", "InitGarageResources", "ResetItem3dModelActionCache", "SetCaptureActions", "SetDrone", "SetExpectedItemId", "SetGrenadeData", "SetHasGrenades", "SetHullData", "SetMountedItemsProperties", "SetPaint", "SetWeaponData", "ShowRequest", "UpdatePreviewSettings", "UpdateViewStyle")),
@@ -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", "Init", "SetActiveContract", "SetBattleProgress", "SetContract", "UpdateProgress")),
SliceWiring("contractState", "fpy_1", "Ont", "xv.bti", "xv.bti(t, n.fpy_1)", listOf("?Hnt", "GetReward", "InitBattle", "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")
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")
/** 未迁移字段:归约时原样透传,作为后续迁移清单。 */
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;
// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics;
// ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派
//
// 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1)
@@ -643,6 +643,30 @@ import App.State.SetSuspiciousFilter
import App.State.Reducers.BattleSearchStateSlice
import App.State.Reducers.ClearSelectedBattle
import App.State.Reducers.ReduceBattleSearch
import App.State.InitBattleModeWithLevels
import App.State.Range
import App.State.SetBattleMode
import App.State.SetDisplayClientInfo
import App.State.SetFlagOrBallOwner
import App.State.SetRankRange
import App.State.SetReArmor
import App.State.SetRemainingTime
import App.State.SetRoundRestartTime
import App.State.UpdateFund
import App.State.Reducers.BattleStatisticsStateSlice
import App.State.Reducers.BeginRound
import App.State.Reducers.ClearFlagOrBallOwner
import App.State.Reducers.CloseStatMenu
import App.State.Reducers.ContinueBattle
import App.State.Reducers.HideStat
import App.State.Reducers.InitBattle
import App.State.Reducers.MarkBattleLoaded
import App.State.Reducers.MarkBattleUnloaded
import App.State.Reducers.ReduceBattleStatistics
import App.State.Reducers.ResetBattleStatistics
import App.State.Reducers.ResetRoundState
import App.State.Reducers.ShowStat
import App.State.Reducers.ToggleStatMenu
import App.State.Reducers.UserRankBounds
import App.State.Reducers.UserStateSlice
import App.State.Reducers.ClearHullData
@@ -1070,7 +1094,7 @@ object StateCheck {
Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled)
}
// ⑭ 根 reducer:89 个槽位的委派表 + 65 个已迁移切片(64 归约 + launchParams 透传)
// ⑭ 根 reducer:89 个槽位的委派表 + 66 个已迁移切片(65 归约 + launchParams 透传)
run {
Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89)
Check(
@@ -1134,7 +1158,7 @@ object StateCheck {
LocalBattleW.Reducer == "(inline@localBattleUserState)"
)
Check(
"根:迁移表 = 65 个已迁移字段,其余 24 个待迁",
"根:迁移表 = 66 个已迁移字段,其余 23 个待迁",
RootReducer.Migrated ==
setOf(
"inviteState", "loginByPassword", "twitchState", "home",
@@ -1154,9 +1178,9 @@ object StateCheck {
"friends", "suppliesState", "clanUserInfo", "battleInviteState",
"partner", "userRenameState",
"battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial",
"clanChatState", "battleSearchState",
"clanChatState", "battleSearchState", "battleStatistics",
) &&
RootWiring.Unmigrated.size == 24
RootWiring.Unmigrated.size == 23
)
// 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片
@@ -1212,7 +1236,7 @@ object StateCheck {
S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader")
)
Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5)
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 65/89")
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 66/89")
val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) })
Store.Dispatch(SetStatus("LOGGING_IN"))
@@ -5610,6 +5634,114 @@ object StateCheck {
)
}
// 76 切片 reducer:BattleStatistics(jR / rr.fqr,27 字段;22 个动作)
run {
val S0 = BattleStatisticsStateSlice()
Check(
"切片 BattleStatistics:默认值完整",
S0.BattleId == null && S0.MapName == "" && S0.Mode == null &&
S0.IsMMBattle && !S0.IsParkourMode && !S0.IsTutorial &&
S0.ValuableBattle && S0.IsReArmorEnabled && S0.RankRange == null &&
!S0.IsContinueBattle && S0.RoundRestartTime == null && S0.Score.isEmpty() &&
S0.Fund == 0 && S0.GameModeLevelsCount == 0 && S0.FlagOrBallOwner == null &&
!S0.StatVisible && !S0.RoundStarted && S0.ScoreLimit == 0 &&
!S0.MenuVisible && S0.RemainingTimeInSec == -1 && !S0.BattleLoaded &&
S0.Options.isEmpty() && S0.CustomBattleName == null &&
S0.BattleFormat == null && !S0.IsMatchmakingMark &&
S0.MapNameWithoutMode == "" && S0.DisplayClientInfo
)
val S1 = ReduceBattleStatistics(
S0,
InitBattle(
battleId = "b1", remainingTime = 180, battleName = "Desert",
matchBattle = true, isParkourMode = false, isTutorial = false,
valuableBattle = true, scoreLimit = 100, fund = 50,
options = listOf("o"), customBattleName = "custom",
battleFormat = "fmt", isMatchmakingMark = true, mapNameWithoutMode = "Desert",
),
)
Check(
"切片 BattleStatistics:InitBattle 灌入元信息并清分数 / 菜单",
S1.MapName == "Desert" && S1.IsMMBattle && S1.Fund == 50 &&
S1.ScoreLimit == 100 && S1.RemainingTimeInSec == 180 &&
S1.CustomBattleName == "custom" && S1.IsMatchmakingMark &&
S1.MapNameWithoutMode == "Desert" && S1.Options == listOf<Any?>("o") &&
S1.Score.isEmpty() && !S1.MenuVisible && !S1.IsContinueBattle
)
Check(
"切片 BattleStatistics:模式 / 护甲 / 段位 / 客户端信息",
ReduceBattleStatistics(S1, SetBattleMode("DM")).Mode == "DM" &&
!ReduceBattleStatistics(S1, SetReArmor(false)).IsReArmorEnabled &&
ReduceBattleStatistics(S1, SetRankRange(Range(10, 1))).RankRange ==
Range(10, 1) &&
!ReduceBattleStatistics(S1, SetDisplayClientInfo(false)).DisplayClientInfo
)
Check(
"切片 BattleStatistics:UpdateScore / UpdateFund 写表与资金",
ReduceBattleStatistics(S1, UpdateScore("A", 3)).Score ==
mapOf<Any?, Any?>("A" to 3) &&
ReduceBattleStatistics(S1, UpdateFund(0L, 9)).Fund == 9
)
Check(
"切片 BattleStatistics:SetRemainingTime 时长转秒",
ReduceBattleStatistics(S1, SetRemainingTime(90)).RemainingTimeInSec == 90
)
Check(
"切片 BattleStatistics:旗主设置 / 清空",
ReduceBattleStatistics(S1, SetFlagOrBallOwner(7L)).FlagOrBallOwner == 7L &&
ReduceBattleStatistics(
ReduceBattleStatistics(S1, SetFlagOrBallOwner(7L)),
ClearFlagOrBallOwner,
).FlagOrBallOwner == null
)
Check(
"切片 BattleStatistics:ShowStat / HideStat / 菜单两态",
ReduceBattleStatistics(S1, ShowStat).StatVisible &&
!ReduceBattleStatistics(S1, HideStat).StatVisible &&
!ReduceBattleStatistics(S1, CloseStatMenu).MenuVisible &&
ReduceBattleStatistics(S1, ToggleStatMenu).MenuVisible
)
val S2 = ReduceBattleStatistics(S1, BeginRound)
Check(
"切片 BattleStatistics:BeginRound 开回合并清分数",
S2.RoundStarted && !S2.StatVisible && !S2.MenuVisible &&
S2.Score.isEmpty() && S2.FlagOrBallOwner == null
)
Check(
"切片 BattleStatistics:ContinueBattle / ResetRoundState",
ReduceBattleStatistics(S2, ContinueBattle).IsContinueBattle &&
!ReduceBattleStatistics(S2, ResetRoundState).RoundStarted
)
Check(
"切片 BattleStatistics:BattleLoaded 两态与 levelsCount",
ReduceBattleStatistics(S2, MarkBattleLoaded).BattleLoaded &&
!ReduceBattleStatistics(S2, MarkBattleUnloaded).BattleLoaded &&
ReduceBattleStatistics(S2, InitBattleModeWithLevels(4))
.GameModeLevelsCount == 4
)
Check(
"切片 BattleStatistics:ResetBattleStatistics 整片复位",
ReduceBattleStatistics(S2, ResetBattleStatistics) == BattleStatisticsStateSlice()
)
Check(
"切片 BattleStatistics:无关动作不改状态",
ReduceBattleStatistics(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, MarkBattleLoaded)
Check(
"根:MarkBattleLoaded 只动 battleStatistics",
Root1.BattleStatistics.BattleLoaded &&
Root1.BattleSearchState == Root0.BattleSearchState
)
val W = RootWiring.ByField("battleStatistics")!!
Check(
"根:battleStatistics 委派到 jR / rr.fqr 且 22 个动作",
W.SliceClass == "jR" && W.Reducer == "rr.fqr" && W.Actions.size == 22
)
}
val Failed = Results.filter { !it.second }
println("TankiOffline 状态层自检:${Results.size} 项")
for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)