feat(state): 迁移登录邀请切片 reducer(InviteState)

1) 动作在 Kotlin 侧不再要求公共基类:原包动作也没有基类,Store/Gateway 改用 Any,817 个生成类无需实现接口。

2) 新增 App/State/Reducers/InviteReducer.kt,逐条对照 module_10032:状态 aD=InviteState(isInviteNeed, invite, status, existsUid, inviteAccepted)、枚举 lD=InviteStatus(UNKNOWN/FREE/NOT_FOUND/ALREADY_ACTIVATED)、reducer pD.er5 的三条分支。默认值取自构造函数(isInviteNeed 默认 true、invite 默认空串、status 默认 UNKNOWN、inviteAccepted 默认 false)。

3) 自检 19 增到 26 项:默认值、三条动作分支(含 FREE 推导 inviteAccepted、ApplyInvite 重置 status/existsUid)、无关动作不动、接 Store 派发后归约。

结果:TANKIOFFLINE_STATE_TEST=1 全通过。
This commit is contained in:
TermiNexus committed 2026-10-23 00:05:58 +08:00
1 parent 6a07f9a39f
commit b4ecb80b23
3 files changed
+128 -18

No files matched your search

@@ -0,0 +1,67 @@
// InviteReducer.kt —— 登录邀请切片(对应 module_10032 的 aD 状态 + pD.er5 reducer)
//
// 证据链:
// 状态类 aD:`function aD(t, n, i, r, e) { t = t === cd || t; n = n === cd ? "" : n;
// i = i === cd ? wD() : i; r = r === cd ? null : r; e = e !== cd && e; … }`
// toString:`InviteState(isInviteNeed=…, invite=…, status=…, existsUid=…, inviteAccepted=…)`
// 枚举 lD(cD()):UNKNOWN / FREE / NOT_FOUND / ALREADY_ACTIVATED
// reducer pD.er5:
// SetInviteNeed → copy(inviteNeed)
// SetInviteStatus → copy(cd, cd, inviteStatus, existUid, inviteStatus.equals(FREE))
// ApplyInvite → copy(cd, invite, UNKNOWN, null)
// 其它 → 原状态
package App.State.Reducers
import App.State.ApplyInvite
import App.State.SetInviteNeed
import App.State.SetInviteStatus
/** 原 `lD`(module_10032 的邀请状态枚举)。 */
enum class InviteStatus {
UNKNOWN,
FREE,
NOT_FOUND,
ALREADY_ACTIVATED,
;
companion object {
/**
* 动作字段在运行期是 JS 枚举对象,toString 就是成员名(`SetInviteStatus(inviteStatus=UNKNOWN…)`)。
* 认不出时按原逻辑落到默认值 UNKNOWN。
*/
fun FromText(Text: Any?): InviteStatus =
entries.firstOrNull { it.name == Text?.toString() } ?: UNKNOWN
}
}
/**
* 原 `aD`:`InviteState(isInviteNeed, invite, status, existsUid, inviteAccepted)`。
* 字段类型只在有证据处收紧:isInviteNeed / inviteAccepted 由 reducer 逻辑确定为 Boolean,
* invite / existsUid 运行时还没采到,保持宽松。
*/
data class InviteStateSlice(
val IsInviteNeed: Boolean = true,
val Invite: Any? = "",
val Status: InviteStatus = InviteStatus.UNKNOWN,
val ExistsUid: Any? = null,
val InviteAccepted: Boolean = false,
)
/** 原 `pD.er5`。 */
fun ReduceInvite(State0: InviteStateSlice, A: Any): InviteStateSlice = when (A) {
is SetInviteNeed -> State0.copy(IsInviteNeed = A.inviteNeed)
is SetInviteStatus -> {
val Status = InviteStatus.FromText(A.inviteStatus)
State0.copy(
Status = Status,
ExistsUid = A.existUid,
InviteAccepted = Status == InviteStatus.FREE,
)
}
is ApplyInvite -> State0.copy(
Invite = A.invite,
Status = InviteStatus.UNKNOWN,
ExistsUid = null,
)
else -> State0
}
@@ -10,16 +10,21 @@
// 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1)
package App
import State.Action
import State.Middleware
import State.ReduxGateway
import State.ReduxStore
import State.StoreListener
import App.State.ActionCatalog
import App.State.ActionSamples
import App.State.ApplyInvite
import App.State.Reducers.InviteStateSlice
import App.State.Reducers.InviteStatus
import App.State.Reducers.ReduceInvite
import App.State.SetInviteNeed
import App.State.SetInviteStatus
private data class AddFrags(val fragsCount: Int) : Action
private data class SetScreen(val screen: String) : Action
private data class AddFrags(val fragsCount: Int)
private data class SetScreen(val screen: String)
private data class UiState(val Frags: Int = 0, val Screen: String = "Preloader")
@@ -213,6 +218,47 @@ object StateCheck {
Check("网关:全目录 ${ActionCatalog.All.size} 个动作类可注册且只路由命中项", Hits == 1)
}
// ⑨ 切片 reducer:InviteState(原 module_10032 的 aD / pD.er5)
run {
val S0 = InviteStateSlice()
Check(
"切片 Invite:默认值与原构造一致",
S0.IsInviteNeed && S0.Invite == "" && S0.Status == InviteStatus.UNKNOWN &&
S0.ExistsUid == null && !S0.InviteAccepted
)
val S1 = ReduceInvite(S0, SetInviteNeed(false))
Check("切片 Invite:SetInviteNeed 只改 isInviteNeed", !S1.IsInviteNeed && S1.Status == S0.Status)
val S2 = ReduceInvite(S1, SetInviteStatus("FREE", 12345L))
Check(
"切片 Invite:SetInviteStatus 更新 status/existsUid/inviteAccepted",
S2.Status == InviteStatus.FREE && S2.ExistsUid == 12345L && S2.InviteAccepted
)
val S3 = ReduceInvite(S2, SetInviteStatus("NOT_FOUND", null))
Check("切片 Invite:非 FREE 状态 inviteAccepted 归 false", !S3.InviteAccepted && S3.Status == InviteStatus.NOT_FOUND)
val S4 = ReduceInvite(S3, ApplyInvite("INVITE-CODE"))
Check(
"切片 Invite:ApplyInvite 写入邀请码并重置 status/existsUid",
S4.Invite == "INVITE-CODE" && S4.Status == InviteStatus.UNKNOWN && S4.ExistsUid == null &&
!S4.InviteAccepted
)
val S5 = ReduceInvite(S4, AddFrags(1))
Check("切片 Invite:无关动作不改状态", S5 == S4)
val Store = ReduxStore(S0, { S, A -> ReduceInvite(S, A) })
var Notified = false
Store.Subscribe(StoreListener { Notified = true })
Store.Dispatch(SetInviteStatus("ALREADY_ACTIVATED", 7L))
Check(
"切片 Invite:接 Store 派发后状态已归约",
Store.State.Status == InviteStatus.ALREADY_ACTIVATED && Store.State.ExistsUid == 7L && Notified
)
}
val Failed = Results.filter { !it.second }
println("TankiOffline 状态层自检:${Results.size} 项")
for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)
@@ -9,22 +9,19 @@
// ft/Ct = 派生值(selector)与组件订阅,S3 暂不移植(UI 层才需要)
//
// 这里的实现保持同样语义:
// · 动作以 Kotlin 类为键(等价原包的构造函数作 Map key)
// · 动作没有公共基类(原包同样没有),以 Kotlin 类作键(等价原包的构造函数作 Map key)
// · 中间件顺序与原包一致:列表里靠后的中间件包在更外层
// · 监听器在状态更新之后被通知;网关的按动作监听器在状态更新之前被通知
package State
import kotlin.reflect.KClass
/** 动作基类:与原包一致,动作是不可变数据类(Kotlin data class)。 */
interface Action
/**
* 原 module_10849 的 `ct`:`InitState(type, action)`。
* 证据:`(p(ct).toString = ... "InitState(type=" + d(this.zap_1) + ", action=" + d(this.aaq_1) + ")")`。
* 启动时用它把初始状态灌进 Store。
*/
data class InitState(val type: Any?, val action: Any?) : Action
data class InitState(val type: Any?, val action: Any?)
/** Store 订阅者:状态变化后回调(对应原 `vt.maq_1` 里那些对象的 `kaq`)。 */
fun interface StoreListener {
@@ -33,7 +30,7 @@ fun interface StoreListener {
/** 中间件:签名等价原包的 `o(store, next) -> dispatch`。 */
fun interface Middleware<S> {
fun Invoke(Store: ReduxStore<S>, Next: (Action) -> Action): (Action) -> Action
fun Invoke(Store: ReduxStore<S>, Next: (Any) -> Any): (Any) -> Any
}
/**
@@ -45,18 +42,18 @@ fun interface Middleware<S> {
*/
class ReduxStore<S>(
InitialState: S,
private val Reducer: (S, Action) -> S,
private val Reducer: (S, Any) -> S,
Middlewares: List<Middleware<S>> = emptyList(),
) {
var State: S = InitialState
private set
private val Listeners = ArrayList<StoreListener>()
private val DispatchChain: (Action) -> Action
private val DispatchChain: (Any) -> Any
init {
// 原 `vt` 构造里的 invokeReducer:先归约,再逐个通知监听器,最后把动作原样返回
var Chain: (Action) -> Action = { A ->
var Chain: (Any) -> Any = { A ->
State = Reducer(State, A)
for (L in Listeners.toList()) L.OnStoreChanged()
A
@@ -65,7 +62,7 @@ class ReduxStore<S>(
DispatchChain = Chain
}
fun Dispatch(A: Action): Action = DispatchChain(A)
fun Dispatch(A: Any): Any = DispatchChain(A)
fun Subscribe(L: StoreListener) {
Listeners.add(L)
@@ -82,7 +79,7 @@ class ReduxStore<S>(
class ModelActionScope internal constructor(private val Gateway: ReduxGateway<*>) {
internal val Removers = ArrayList<() -> Unit>()
fun Listen(Type: KClass<*>, Listener: (Action) -> Unit) {
fun Listen(Type: KClass<*>, Listener: (Any) -> Unit) {
Removers.add(Gateway.OnAction(Type, Listener))
}
}
@@ -99,7 +96,7 @@ class ReduxGateway<S>(
private val Store: ReduxStore<S>,
private val Scheduler: (() -> Unit) -> Unit,
) {
private val ActionListeners = LinkedHashMap<KClass<*>, MutableList<(Action) -> Unit>>()
private val ActionListeners = LinkedHashMap<KClass<*>, MutableList<(Any) -> Unit>>()
private val ModelScopes = LinkedHashMap<Any, MutableList<() -> Unit>>()
private val KeyScopes = LinkedHashMap<Any, MutableList<() -> Unit>>()
private val GameObjectScopes = LinkedHashMap<Long, MutableList<() -> Unit>>()
@@ -109,19 +106,19 @@ class ReduxGateway<S>(
val StoreState: S get() = Store.State
/** 原 `uar`:把派发排进调度器(调度器负责错误隔离)。 */
fun Dispatch(A: Action) {
fun Dispatch(A: Any) {
Scheduler { StoreDispatch(A) }
}
/** 原 `gas` 返回的派发包装:先按动作类通知,再交给 Store。 */
fun StoreDispatch(A: Action) {
fun StoreDispatch(A: Any) {
val List = ActionListeners[A::class]
if (List != null) for (L in List.toList()) L(A)
Store.Dispatch(A)
}
/** 注册按动作类的监听器,返回摘除函数(原 `sar` / `tar`)。 */
fun OnAction(Type: KClass<*>, Listener: (Action) -> Unit): () -> Unit {
fun OnAction(Type: KClass<*>, Listener: (Any) -> Unit): () -> Unit {
ActionListeners.getOrPut(Type) { ArrayList() }.add(Listener)
return { ActionListeners[Type]?.remove(Listener) }
}