0.2.3: Clementia S2 releases a dead, hidden, uncarriable or pathless rider at once (slot and weight back before the same tick's boarding checks); same-browser match recovery never takes the seat of a window that is reloading or still starting (a tab keeps its own session token; seats are stamped at the server's welcome, the older welcome wins); golden: diy-135 moves (outcome unchanged)

This commit is contained in:
sganggs committed 2026-10-10 01:58:06 +08:00
1 parent 821ab827bd
commit ba5b998a72
19 files changed
+580 -60

No files matched your search

+5
View File
@@ -174,6 +174,11 @@ otherwise every "之后 / 后续的每场作战" card — e.g. 教鞭's 法术
A `choice:<effectId>` registry handler overrides the default application (§2.4).
### 1.3 Disconnects, AI takeover
* Local multi-window recovery (#431, DESIGN §28.27) sends optional `hello.noReplace` for a tentative shared token,
and `hello.claimAt` for a previously welcomed token. Before binding a different socket, the server rejects a
connected seat when no replacement was allowed or the request has a later stamp than the attached holder
(`SESSION_IN_USE`). Equal stamps still allow normal reload/reconnect. These hints do not authenticate players
and do not change the bearer token or expiration rules below.
* Disconnected human: the seat keeps playing its last lineup; drafts auto-resolve at their deadlines, prep auto-readies at
the deadline (an open 教鞭 choice is picked at random first, §2.5; then the temp pieces due at that prep are sold/destroyed). Nothing is bought for them. A battle the human was authority of goes to the server
(normal / 联防: re-simulated from t = 0) or, on a boss field, to the partner's replica (DESIGN §14). The session stays
+3 -1
View File
@@ -201,7 +201,9 @@
单独选择中文、日语或跟随全局语音,只保存在当前浏览器,日语缺失时沿用中文回退。
同一浏览器的对局窗口意外关闭后,可以从标题或大厅的「恢复本机对局」选择保存的席位。仍被其他窗口使用的席位
不会被接管;服务器重启、对局过期或浏览器无法检查其他窗口时不能恢复。
(包括正在刷新、初始化的窗口)会保留给原窗口;刷新只尝试本窗口原来的身份,不会自动换成另一个近期身份。
恢复在当前页面重新连接,服务器确认后才保存新身份;发生竞争时,较早获得服务器确认的窗口优先。
服务器重启、对局过期或浏览器无法检查其他窗口时不能恢复。
浏览器提供安装条件时会显示「添加到桌面」。安装后仍需连接游戏服务器,不会把服务器或整套素材离线保存。
不支持页面内安装按钮的浏览器可使用其自己的菜单(是否可用由浏览器决定)。
+5
View File
@@ -535,6 +535,11 @@ flag `float` (近地悬浮, PRTS 术语释义 "算作空中单位") or `levitate
walking the ground path). `deploySeq` counts deployments (and identifies one: `seq === u.deploySeq`);
`aggroSeq` is the aggro order (= deploySeq, except the summons of the initial deployment, §1).
克莱门莎 S2's kit stores its cabin in `unit.mem.clemntCabin`. Before boarding each tick, it removes passengers
that died, became hidden / uncarryable or cannot path to the next cabin position, removes their carry buff and
returns their current `weight` (including modifiers). The same tick's count and weight limits use that cleaned
list and budget (DESIGN §28.27); this is local to the kit, not a change to general displacement.
**Hit areas (`body.js`, user playtest #5 item 10).** A regular enemy is a point: in a grid range when the tile of its
position (`round(y)`, `round(x)`) is a range tile, in a radius when its position is (DESIGN §3). A huge enemy (巨型单位:
`enemy.hitArea` from data/enemies.json `hitArea` — 假想敌:胄 / 管 / 盐风主教昆图斯 / 阿利斯泰尔 / “萨米的意志”, PRTS
+12 -2
View File
@@ -101,8 +101,18 @@ room loadouts and their exports.
The title and lobby offer explicit recovery of a previously saved local match seat. `sp.matches` stores bounded name /
room-code metadata indexed by token hash; the existing four-entry `sp.tokens` ring remains the authority. Recovery
rechecks BroadcastChannel claims before adopting the selected token, then reloads. It does not identify players by
name or IP or displace a live tab. Without BroadcastChannel it refuses shared recovery. Server restart / expiry clears
rechecks BroadcastChannel claims before selecting the token in memory and reconnecting in the same document.
Only the server's `welcome` (`saveToken`) persists that selection and its first-welcome stamp (`sp.tokenWelcome`,
bound to the token hash). Refreshing or reconnecting the same token preserves that stamp. A tab with a session token
initializes with that token or null, never another recent token, and answers ownership queries during initialization.
An earlier welcome takes precedence; equal stamps prefer an already welcomed live document over a copied session,
then tab id breaks ties. A session token without a stamp still outranks a tentative shared-token selection. A pending
recovery yields when the original owner returns; a later welcomed holder also gives up its local claim and pending
reconnect. `hello.noReplace` prevents tentative recovery from evicting a connected session even if its hello is already
in flight; `hello.claimAt` preserves the earlier-welcome priority for established holders' in-flight reconnects. A stale
welcome never reaches the store or flushes queued game actions. Nickname persistence also waits for welcome, so a
refused recovery can return to the previous identity and nickname. It does not identify players by name or IP.
Without BroadcastChannel it refuses shared recovery. Server restart / expiry clears
obsolete metadata on the next welcome. A spectator never registers a recoverable player seat.
`manifest.json` enables browser installation; its original fortress favicon is packaged as actual 192/512 px icons in
+3
View File
@@ -172,6 +172,9 @@ Profession traits (default behaviours implemented once in the engine, keyed by `
Each domain file `server/sim/content/{tokens,bonds,garrisons,items,bands,enemies,bosses,devices,choices}.js` exports `install(battle)` (battle side; called once per Battle by `content/index.js → installContent`) and `registerMeta(registry)` (prep side; called once at server boot by `match/effectsMeta.js`, API documented in `docs/META.md`). Kits live one per file in `content/kits/ops/` (`export default { [baseChessId]: (bb, chess, def) => Kit }`), listed in `content/kits/index.js` (guide: `content/kits/README.md`). Content must be **data-driven from blackboards** (numbers never hard-coded when a blackboard key exists). Every effect gets at least one unit test in `test/content/*.test.js` using the `test/helpers/battleHarness.js` harness (provided by sim-core).
- kits: all 112 visible chess + hidden ones used by effects (盟约·辅助干员), default skill + talents + trait specifics; tokens. The 72 non-collaboration 6★ DIY operators include 克莱门莎 (§28.1); her launch-day provisional mechanics are local to her kit. 黍 GUA-Y and 乌尔比安 CRU-Y follow the generated module blackboards and existing module-level rules (§28.2).
克莱门莎 S2 removes dead, hidden, self-bound / teleport-immune and unreachable passengers before checking new
boarding in the same tick. Each released passenger returns its current weight to the cabin's remaining budget
and frees its slot (§28.27); neither corpses nor failed carries reserve capacity.
- bonds: 23 (battle + meta), exact per-layer formulas from research 02.
- garrisons: 43 effect keys (IN_BATTLE via hooks; SERVER_* via meta registry).
远牙's granted `garrison_108_a/b` additionally gains on both directions of `dollSwap`, sharing its deployment
+7
View File
@@ -14,6 +14,13 @@ Platform additions (implemented): `welcome.{resumed, version}`, `welcome.diyKitt
**Spectator seats** (a remake feature — the official room has none; community report #26, owner's decision 2026-10-04; the spectator-seat subsection of §23): `room.spectate {code}` takes one of a co-op room's `MAX_SPECTATORS` (2) spectator seats, in its lobby or during its match; `room.removeSpectator {playerId}` (host, at any time — the room screen's strip and, once the match runs, the game screen's 观战席 capsule in the top bar, GitHub #120) frees one (`room.closed {reason:'kicked'}` to it, no match frame after it); `room.state.spectators: [{playerId, name, connected}]` (still pushed while the match runs). A spectator is never a player (not in `seats`, not counted for the start, never host, keeps no room alive — `room.closed {empty}` when the last human leaves), receives the room's broadcasts and watches like an eliminated player (`Match.addSpectator`: `b.start` watch / `m.field`, never `m.private`); it may only `g.watch`, leave, and store a `room.loadout` — anything else answers `error SPECTATOR`. Its seat is kept and given back on reconnect like a player's.
Local recovery arbitration (0.2.3, §28.27): `hello` also accepts optional `noReplace:boolean` and
`claimAt:number` (finite, nonnegative ms). A shared-token candidate sends `noReplace:true`; a tab with a saved
welcome stamp sends `claimAt`. When that session is already connected to a different socket, `noReplace` or a later
stamp than the attached holder causes `SESSION_IN_USE` before the old socket is touched. A same-stamp own reconnect
can still replace its previous socket. Omitted hints retain legacy behavior; they are not authentication. Client
selection/stamp rules live in design/client.md. In-flight protection requires the updated server as well as client.
### 8.2 Match
C→S (all carry `rid`): `g.infoReady {setupRevision?}` · `g.band {bandId}` · `g.bandSkip` · `g.bandFocus {bandId?}` (the strategy highlighted in the draft screen — what a timed-out turn takes; absent / null clears it; `WRONG_PHASE` outside BAND_DRAFT, `ALREADY` after the pick, `BAD_TARGET` for a band not allowed; §18.2) · `g.buy {slot}` · `g.refresh` · `g.freeze` · `g.levelUp` · `g.sell {uid}` · `g.move {uid, to:{area:'board', row, col}|{area:'hand', idx}, dir?}` · `g.equip {itemUid, targetUid, replaceUid?}` (`replaceUid`: which equipped item a third one replaces, §6.2) · `g.art {itemUid, row, col, dir?}` (`dir` ∈ UP|RIGHT|DOWN|LEFT from the deploy wheel, §3; absent ⇒ `to.dir`, then RIGHT; `g.move` onto the piece's own tile re-orients it; board pieces in `m.private` / `m.field {prep:true}` carry `dir`) · `g.destroy {uid}` (hand / temp items only; equipped items are locked, §6.2) · `g.reward {idx}` · `g.choice {idx, choiceId?}` (with an ID: the recipient's PREP personal choice; without one: the public SP_DRAFT; stale or wrong IDs never fall through to the public draft) · `g.ready {ready}` · `g.emote {id}` · `g.watch {fieldId, playerId?}` (`playerId`: the player tapped — a shared 联防 / boss pair field shows two; what an eliminated viewer follows, §25) · `g.autoplay {on}` · `g.pause {on}` (solo battles only, §14 Solo pause) · `g.unitStats {seq?}` (ROUND_START / SP_DRAFT / PREP, else `WRONG_PHASE`; eliminated: `ELIMINATED`) → push `m.unitStats` (§18.5) · `g.leave`
S→C:
+40 -2
View File
@@ -284,9 +284,9 @@ installability check runs in Chrome's ordinary temporary profile. Native OS inst
Reproduced with an idle window retaining its own session while a second match window closes. The title/lobby now
lists local saved match seats using bounded metadata beside the existing recent-token ring. Explicit selection asks
live tabs to claim the chosen token again before adopting it and reloading. An active tab is never deliberately
live tabs to claim the chosen token again before adopting it (same-document reconnect since §28.27). An active tab is never deliberately
replaced; names/IP are not authentication. Missing BroadcastChannel refuses recovery. A server-generated replacement
token or leaving clears obsolete metadata. No server API changes; normal automatic startup selection is preserved.
token or leaving clears obsolete metadata. The initial feature changed no server API; §28.27 later adds optional hello guards and keeps startup on the tab's own identity.
Tests: live tab protection, closed-tab recovery, simultaneous claim arbitration, no channel/unknown id, malformed
storage and expired metadata; a real two-window browser restores the identical server playerId and room after closing
@@ -417,3 +417,41 @@ clear time and end reason. All other 284 scenarios, including the five other gol
The focused checks cover all four tier/elite forms, selectability/control/cooldown boundaries, actual casts,
healing chains and enemy splash. Full CI passes: 5879 tests, 5876 passed, 0 failed, 3 skipped; setup, smoke, lint,
imports and typecheck pass. This round changes no production UI and does not rerun the opt-in browser suites.
### 28.27 Cabin capacity and local recovery races — #431
- **Observed:** 克莱门莎 S2 removed the carry buff from dead, hidden, uncarryable or unreachable passengers but
retained them in the passenger list and weight budget. Two weight-3 riders blocked a replacement after one died;
a dead zero-weight rider still occupied one of the 15 slots; a dead weight-4 rider retained four budget points.
- **Now:** after moving the cabin, release invalid passengers and return each one's current weight before either
boarding limit is checked that tick. Remaining passengers keep their order and movement. This corrects bookkeeping
under the existing S2 rules and official blackboard limits (sources in §28.1); no new gameplay assumption is added.
- **Observed (#431):** a recovery attempt with a smaller tab id could claim a token while its owner was refreshing
or still initializing. The original page then fell back to another recent identity, or erased its own token.
- **Now:** startup with a session token chooses only that token or null and answers ownership queries immediately.
The first server welcome for a token stamps its ownership; reload/reconnect preserves that stamp. Earlier welcome
wins a conflict, a live document wins an equal-stamp duplicate, and tab id resolves remaining ties. Explicit recovery
keeps its choice only in memory and reconnects in the same document; only welcome persists it. An original owner
returning cancels the lower-priority choice, including a later welcomed holder's reconnect. Nicknames and IPs remain
irrelevant to identity; a refused recovery retains the previous nickname until its own welcome.
- **In-flight handshake:** a hello can reach the server after another connection's welcome. Optional `hello.noReplace`
makes shared candidates refuse a currently connected seat; `hello.claimAt` lets the server refuse an established
holder with a later welcome stamp. Refusal is `SESSION_IN_USE`, before rebinding or closing the incumbent socket.
Normal same-stamp reconnects and older clients remain accepted. These are local ownership hints, not authentication;
the bearer token remains the authority. A revoked hello's stale welcome is ignored before going online, flushing
queued actions or notifying listeners, and retries use a new socket. Reconnect-window duration is unchanged.
- **Regression coverage:** `op_clemnt.test.js` adds same-tick replacement for death / hidden / self-bound /
teleport immunity / failed path, the 15-slot and weight-4 probes, and a changed current weight. The eight regressions
fail before the fix and pass after it. `client-static.test.js` covers live / reload / mid-init, no fallback identity,
welcome-only persistence, earlier-welcome priority and a welcome arriving during init. The real-browser recovery
test holds a refreshing owner's initialization open while another page clicks recovery and checks both server
identities and live sockets; closed-window recovery also checks that welcome arrives without a page reload.
`client-identity-handshake.test.js` controls delayed hellos / welcomes through the real client and server handlers,
including a later holder's buffered reconnect, same-stamp reconnect, stale-welcome action suppression and validation.
- **Golden:** all 285 scenarios regenerated. Only `diy-135` moves: releasing S2 capacity permits more later carries
(teleport events 572 → 630), changing movement, attack/heal timing and RNG consumption. Clear time 194.33 → 198.03 s;
outcome remains `cleared`, 150/150 kills, zero leaks, one allied death and zero errors. The other 284 scenarios and
the `roster`, `bonds`, `fields`, `matches`, `standins` families are unchanged.
- **Contracts updated:** design/engine.md, SIM.md (cabin), design/client.md, design/network.md, META.md and PLAYING.md
(local identity recovery and handshake).