0.2.3: prepare follow-up fixes and client features (#407, #408, #410, #413, #431, #435, #436, #440, #444)

Add special-target activation without bypassing attack cadence, cooperative
card preferences and emotes, skill sound banks, per-operator voice choices,
text sizing, install prompts, closed-tab recovery and unanimous setup rerolls.
Retain late deployment elapsed time and resolve echo/counter pulses without
recursive hook overflow. Independent final review remains pending.

Golden: roster-016, roster-035, roster-036, roster-045;
boss-boss_1-pair, boss-boss_3-pair, boss-boss_5-pair, boss-boss_7-pair,
hidden-boss_9-pair; solo-HARD-9-boosted: Lemuen attack cadence.
diy-132: Necras extra activation targets.
coop2-NORMAL-14-standins: cooperative purchase preference and simulation fixes.
hidden-boss_10-solo: echo pulse FIFO resolution.
See DESIGN 28.13-28.24 for assumptions, sources and contributor credit.
This commit is contained in:
sganggs committed 2026-10-09 22:55:24 +08:00
1 parent 314564c139
commit f3658a0dd4
104 files changed
+3674 -741

No files matched your search

+11 -1
View File
@@ -221,7 +221,8 @@ All paths are URL paths relative to the site root, for example `/assets/char/ava
disconnect, settlementSucceed, settlementFail, settlementTeam, settlementBossSign,
goodEvaluation, load, start, matchSucceed, matchFail, matchCancel, joinRoom },
battle: { deploy, tokenDeploy, charDie, enemyDie, enemyDieHeavy, enemyHit, heal, leak, win, lose, killCoin },
units: { [charId|tokenId|enemyId]: { attack?, hit?, skill?, skills?: {[skillIndex]: url}, die?, born?,
units: { [charId|tokenId|enemyId]: { attack?, hit?, skill?, skills?: {[skillIndex]: url}, attacks?: {[skillIndex]: url}, hits?: {[skillIndex]: url}, die?, born?,
attackMix?: {[skillIndex]: {p?, vol?}}, hitMix?: {[skillIndex]: {p?, vol?}},
mix?: { [attack|hit|die|born]: { p?, vol? } } } }
}
},
@@ -452,3 +453,12 @@ The project's code is GPL-3.0-or-later (`LICENSE`); none of the items below is c
- **Novecento Wide:** © Jan Tonellato / Synthview. Free licence.
- Both are mirrored from TimWangZi/The-font-of-Arknights.
- Noto Sans SC and Noto Serif SC (SIL OFL) are loaded from Google Fonts, not self-hosted.
### Skill attack banks (0.2.3, PR #410)
`pickModeAttacks` / `pickModeHits` select a uniform d/h/s bank from the official audio table (attack/combat abilities
only). A skill's activation sound supplies its mode letter; without one, d/h/s = slots 1/2/3 is [ASSUMED]. Competing
banks prefer START over ON, then plain/numeric order; this is a deterministic approximation, not proof of every
operator's runtime ability graph. Mixed-letter banks are skipped. Preserve `mixOf` for each selected bank separately;
`attacks`/`hits` and `attackMix`/`hitMix` use matching skill keys. Regenerate the current manifest instead of importing
an older PR's full JSON, preserving JP voices and current operators. See DESIGN §28.17.
+12
View File
@@ -898,3 +898,15 @@ round was over. The official 1 s `broadcastBeginDelay` is not modelled.
* A merge completed after the prep (SETTLE effects) keeps its reward offer for the next prep; its elite goes where a prep
merge's would — onto the tile of a consumed deployed copy (PRTS 卫戍协议/帮助 "若消耗已部署至作战区的干员,则发送至作战区
对应位置"), else to the hand, overflowing into temp (temp pieces that arrive after the prep wait through the next prep).
## Opening rerolls and bot reactions (0.2.3)
`match/setupVote.js` owns the INFO_CHECK vote and transactional opening replacement; `Lobby.rerollSetup` checks host
permission for `room.rerollSetup {setupRevision}` and `room.cancelReroll {voteId}`. Participants send
`g.rerollVote {voteId, agree}`; `g.infoReady {setupRevision?}` accepts an omitted revision only before the first reroll.
`m.public` includes `setupRevision` and `rerollVote` (null or `{id, proposerId, voters, agreed}`). Old vote/revision
requests cannot affect a new opening. See DESIGN §28.21 for cancellation and timer behavior.
`botEmotes.js` is isolated from bot decisions and RNG. It sends the existing `m.emote` only in mixed matches, under
normal cooldown/whitelist rules; `SP_BOT_EMOTES=0` disables it. The cooperation score in `bot.js` and these cosmetic
choices are explicitly [ASSUMED]; DESIGN §28.15–16 records the source and validation.
+14
View File
@@ -194,3 +194,17 @@
**什么算一局**:打到结算页的对局,以及中途退出(「放弃模拟」)前至少清完 1 个回合的对局,计入以上所有统计——和官方一样,中途退出按淘汰处理,通过的回合数就是退出时已经通过的回合。**进去就退、没清完第一个回合就退的对局不计入统计**(不算一局,不影响胜率和策略通过率),但会留在「对局记录」里,标着「中途退出」和「不计入统计」。已经被淘汰后再退出的对局算(你输了这一局)。下面这些不会记录:观战;被服务器结束的对局(服务器重启、同盟解散);关掉页面后没有再回来的对局。
**存储与隐私**:最多保留最近 1000 局,所有记录加起来不超过约 0.8 MB(超出时最旧的先丢)。只有最新的 30 局保留完整的阵容、盟约和队友信息,更早的压缩成你自己的一行汇总。同盟对局的记录里有**队友的代号**,它们保存在本机,也会出现在你导出的文件里;不会发给任何人。想删掉就点「清空」(会先确认),或清除这个网站的数据。读不出来的存档(损坏的数据)会另存为备份并重新开始记录,不会让页面出错;由更新版本导出的文件会被拒绝导入,也不会被旧版本覆盖。
### 0.2.3 的便捷设置
设置中的「文字大小」有小、中、大、特大四档,默认小;棋盘尺寸不随文字放大。干员调配详情和已选自选干员卡可
单独选择中文、日语或跟随全局语音,只保存在当前浏览器,日语缺失时沿用中文回退。
同一浏览器的对局窗口意外关闭后,可以从标题或大厅的「恢复本机对局」选择保存的席位。仍被其他窗口使用的席位
不会被接管;服务器重启、对局过期或浏览器无法检查其他窗口时不能恢复。
浏览器提供安装条件时会显示「添加到桌面」。安装后仍需连接游戏服务器,不会把服务器或整套素材离线保存。
不支持页面内安装按钮的浏览器可使用其自己的菜单(是否可用由浏览器决定)。
确认开局信息时,房主可发起「重刷本局」。全体真人同意后重抽开局,保留房间和个人配置;投票暂停确认倒计时,
拒绝、取消、断线或退出则保留原开局和剩余时间。只有一名真人时立即刷新。随机结果可能重复。
+8 -3
View File
@@ -947,13 +947,13 @@ flag alters movement or 失衡.
**and** about to attack/heal **and** an enemy (heal skills: an injured ally) inside the **initial** range
(`unit.baseRangeKeys`: its own grid + its permanent rangeExtend — "攻击范围扩大" modules/talents as persist never-expiring
`rangeExtend` buffs; no skill range, no temporary extend, no extra keys) or blocked by the melee unit — or, checked
**every tick**, an enemy (flyers included) inside a content trigger range (`unit.skill.addTriggerRange(fn)`,
**every tick**, an enemy (flyers included) inside a content trigger range (`unit.skill.addTriggerRange(fn, { attackOnly: false })`,
`fn(battle, unit)` → list of ally units (their current range), tile-key arrays or `{ keys, profile }` (tile keys with
the enemy profile the effect selects by — `canHitFly` false: ground enemies only); returns an unregister fn; not for
heal skills: 海嗣 "攻击范围视为自身攻击范围的延伸", 流形, 谬因 S2's beam, and the areas a skill acts through around its
owner's standing summons — 麦哲伦 S1 (her drones' ranges), 令 S3 (each summon's x-5, ground), 电弧 S2 (赛柯's range,
ground) / S3 (桑特拉's range): the owner's larger-range rule of 2026-10-06, kits/shared/summoner.js
`summonTriggerArea`); `SKILL_RANGE` — "不通过普通攻击/治疗触发技能,仅在技能范围内存在敌人(无视其不可选中)时释放技能": any
`summonTriggerArea`). With `{ attackOnly: true }`, the extra range is checked only at the normal attack opportunity, not each tick: 蕾缪安’s own wanted targets and 死芒’s own living summons’ blocked targets (S1/S2); normal selectability and the skill’s actual effect range remain intact (§28.13). `SKILL_RANGE` — "不通过普通攻击/治疗触发技能,仅在技能范围内存在敌人(无视其不可选中)时释放技能": any
living enemy (stealthed, untargetable, flying included) on `trigger.customRangeGrid` (= the skill's rangeGrid), every
tick, no attack needed; `trigger.allies` (+ `hpAtMost`, default 1) — set by a kit, or by the data for an ally row (黍 S3's
official `TRY_SEARCH_ALLY_SKILL`, "技能范围内存在可治疗的我方单位时释放技能": tools/build-data.mjs `TRIGGER_ALLY_RULES`,
@@ -1011,7 +1011,7 @@ flag alters movement or 失衡.
`skillEnd` fires. `onAttack` ctx carries `noAmmo` (set it to true: this attack spends no bullet, no `ammoUsed` — 流明's
free heals).
- Runtime helpers on `unit.skill`: `activate(reason, {free})`, `end(reason)`, `stop()`, `addAmmo(n)`, `extend(s)`,
`addCharge(n)`, `gainSp(n, reason)`, `addTriggerRange(fn)`, `setTrigger(rule, grid)` (a kit's own rule change mid-battle —
`addCharge(n)`, `gainSp(n, reason)`, `addTriggerRange(fn, { attackOnly?: boolean })`, `setTrigger(rule, grid)` (a kit's own rule change mid-battle —
薇薇安娜 S3's ACTIVE_RANGE on 3-2 after its first cast; 0.2.0); fields `sp, spCost (= floor(base×spCostMul + spCostFlat)), spCostMul, charges,
maxCharges, active, timeLeft, ammoLeft, ammoMax, activations, kind, rule, bb` (`ammoMax`: the most bullets the running ammo skill
has held — set at activation, raised by skillStart additions and `addAmmo` above it; 0 when inactive).
@@ -1547,3 +1547,8 @@ window — `push` / `pull`; a wall-shortened slide keeps its row time, a wall at
platforms/mounds are ground obstacles that elevate operators; the generic kit maps 凋亡 to `apoptosis` (侵蚀 has no element key
yet — enemy content must pick one); 抵抗 covers the control statuses of `RESIST_STATUSES` (the official term lists 晕眩/寒冷/
冻结/恐惧/诱导…, the rest follow the operator kits that grant it) and caps at 0.95; tactical points prefer enemy path tiles.
“余音” owns a same-call pulse queue (DESIGN §28.24): reflected hits enqueue their earned pulses instead of recursively
entering another pulse. This retains the lethal-hit pulse and prevents valid high hit-count chains from tripping the
general hook guard. The queue is emptied in `finally`; it introduces no timer or simulation RNG.
+32
View File
@@ -89,3 +89,35 @@ are verified, and cleans up / restores focus on both success and failure. Failur
Push/pull slides preserve the target's pre-hit facing through the waiting and moving phases (`keepFacing`, §28.9).
Explicit false and unmarked raw displacements face the travel direction. Normal snapshot facing resumes on landing.
[ASSUMED] unrecorded push sources and reverse-facing hook pulls use the same display rule as the cited examples.
### Local preferences and recovery (§28.14, §28.18–20)
Settings persist `textSize` (sm/md/lg/xl; default sm) and `voiceOverrides` (charId → cn/jp; default empty) alongside the
existing global `voiceLang`. CSS text uses `--t`; the layout rem, camera and board geometry stay unchanged. Larger text
and translated controls wrap within the settings modal. The normal roster detail and chosen DIY cards expose each
operator's voice preference, shared across that charId's forms. An absent override follows the global preference;
missing Japanese lines retain the existing Chinese fallback. Overrides are local listening preferences, absent from
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
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
`public/icons/`, including lite packages. The install button appears only for a live browser install offer and hides
in standalone/fullscreen windows or after installation. Every offer is consumed before awaiting the prompt, including
cancellation or failure. No service worker, offline cache or persistent server is introduced; HTTPS or localhost and
browser support still govern installation.
### Skill-mode sound and delayed models (§28.17, §28.22)
The audio manifest can carry `attacks` / `hits` plus independent `attackMix` / `hitMix` by skill index. Only an active
skill uses a mode bank. Impacts remember the mode at the time of the attack; an end event cancels a queued start for
an unknown unit, and a late-join snapshot restores the current mode. A mode without a bank falls back to its normal
sound. A selected mode with no mix uses its own default volume/probability, not the ordinary bank's mix.
A model that arrives after an actual deployment continues Start at the elapsed game time; after the clip's duration it
shows the current base pose. A subsequent attack/death supersedes that pending entrance. No extra deployment sound
or per-phase animation schedule is added by this change.
+13
View File
@@ -189,3 +189,16 @@ and marks. Leaving clears that source's state without clearing another still-dep
凯尔希·思衡托 S2 uses its mixed attack/heal target condition for automatic activation: either an injured, healable ally
or a selectable enemy in its expanded running range; operation cooldown and other controls still apply (§28.10).
### Additional DEFAULT trigger targets (§28.13)
`addTriggerRange(fn, { attackOnly: true })` extends the DEFAULT target check at the owner's next attack opportunity.
It does not create a per-tick trigger. 蕾缪安 S1–S3 use only their own wanted targets; 死芒 S1/S2 use targets blocked by
living summons owned by that operator. Sleeping or otherwise unselectable enemies do not count. This only changes
the condition for activation: 死芒 S2 still links inside its own effect range. Other registered summon areas retain
the existing per-tick behavior. The optional flag defaults to false for compatibility.
0.2.3 echo/counter interaction (DESIGN §28.24): recursively earned “余音” pulses are drained FIFO by that echo in the
same call, retaining the earned attack value and the lethal-hit pulse. [ASSUMED] This local order avoids exhausting
the hook-depth guard on scaled hit-count HP; the guard remains unchanged for other recursive handlers.
+22
View File
@@ -117,3 +117,25 @@ Official rule (bwiki 盟约, 更新公告 5114): before a match the player canno
**自选编队 (0.2.0):** the overlay's third tab fills the four DIY slots (5阶 ×2, 6阶 ×2) with an owned 6★ outside the chess pool (any of its three skills, any module of its elite form but a 集成战略 one) or a prototype (its locked skill / module); only operators with a kit are offered (`welcome.diyKitted`). Per-browser `sp.pref.diy` = `{ v, picks }`, synced as `room.diy { picks }` by the same sync engine; 导出 / 导入 as `{ kind: 'stronghold.diy', v, exportedAt, count, picks }` (`ui/diyModel.js`, `screens/diy.js`). In the match a slotted piece is the operator for its player (name, art, class, bonds from its factions, no 特质, the pick's skill and module; the slot's tier, price and merge), sold only in that player's shop from the slot's 调度中心 level with its own stock; the shop / reward / detail cards and the own pieces carry a 「自选」 badge (`ui/gameLogic/diy.js`); the 0.2.0 自选编队 design subsection has the rules and sources.
---
### Cooperative buying, reactions and opening rerolls (§28.15–16, §28.21)
[ASSUMED] Bot purchasing applies one soft 18-point penalty to non-elite, non-DIY tier V/VI shared-pool cards whose
active bonds overlap another living teammate's opening strategy. Human and AI teammates are treated equally; it
neither reserves stock nor prevents a stronger purchase. Existing strategy presets and rehearsal counts are unchanged.
[ASSUMED] AI reactions are cosmetic social choices, enabled by default and disabled with `SP_BOT_EMOTES=0`. Only
mixed human/AI matches emit them, through the existing whitelist and cooldown. A human emote gets at most one eligible
bot reply. Settlement uses that round's results (kills then damage, stable seat tie-break), once per round; the third
chess merge reacts once and resets at settlement. Gifts are classified by trusted source playerId, never display name.
These hooks consume no gameplay RNG and schedule no game actions.
[ASSUMED] During INFO_CHECK the host may request an opening reroll; all connected human participants must explicitly
agree, including autoplay humans. Bots and spectators do not vote; one human refreshes immediately. A vote pauses the
confirmation timer and blocks Ready. Rejection, host cancellation, disconnect or leaving cancels it and restores the
remaining time / prior Ready state. Success preserves the room, seats, settings and connections, redraws the complete
opening, resets Ready, and starts the full original timer (single-human matches remain untimed). Fixed stage/bans
remain fixed and random results can repeat. A separate seed-derived stream leaves shop/draft RNG untouched; requests
have a 300 ms anti-burst guard, revision and vote identities. All new setup data and player DIY stocks are prepared
before mutation; data preparation failure leaves the old opening intact. This is a remake convenience, not an official
mode rule. Unrelated result-screen changes from PR #408 are not included.
+11 -1
View File
@@ -15,7 +15,7 @@ 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.
### 8.2 Match
C→S (all carry `rid`): `g.infoReady` · `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`
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:
- `m.public` — full public state on every change (throttled ≤ 10/s): `{ phase, round, lastRound, deadline, serverNow, modeId, difficulty, stageId, factions, disabledBonds, bannedChess, bossId, hiddenBossId?, teamLp?, bossHp?:{hp,max}, overtimeAt? /* FA / HC: ms epoch the overtime drain starts, §4 */, paused /* solo pause, §14 */, draft?:{order,turn,picks,skipsLeft,turnDeadline,turnSeconds /* one turn's length, 0 untimed; deadline = turnDeadline (§18.2) */,untimed}, sp?:{family,cards /* bounty cards carry descRaw?, §20.6 */,turn,picks}, players:[{playerId, seat, name, isBot, connected, alive, lp, bandId, shopLevel, boardCount, ready, bonds:[{bondId,count,active,tier,layers,harmony? /* 调和's +1 is in count, §21.26 */}], fieldId, status, pendingLp? /* COMBAT / 联防 of a normal round: the LP the player's own battle will cost at settlement so far, min(lpCapPerRound, counted leaks) — in 联防 min(lpCapPerRound, uniteLeft) — omitted when 0 and in every other phase (§17.5 live LP) */, uniteLeft? /* 联防: the leaker's enemies still standing — not spawned yet + alive + counted leaks, uncapped, live (§20.6) */}] , fields:[{fieldId, kind, players:[playerId...], progress?:{killed, resolved, total, done} /* §14 顶栏胶囊: resolved = the field's own scheduled enemies knocked down or leaked, null while unknown */}] }` · `m.public.uniteResult` — SETTLE after a 联防 only, its outcome as data: `{ through /* the leakers' enemies that still got through, uncapped */, helpers:[playerId...], leakers:[playerId...], losses:{playerId: the LP settlement charged this round — every alive player} }`; absent when no 联防 resolved (the client's result box then reads the round's own battle; GitHub #235, PR #112)
- `m.private` — the recipient's full PlayerState view (§6.2 minus server-only data) on every change; in a boss round's prep also `bossMate { playerId, side, units }` — the pair partner's board on its half of the boss field (UnitInfo, mirrored on the right half), re-sent when the partner moves (0.2.0, community report item 51).
@@ -117,3 +117,13 @@ Piece = { uid, kind: 'chess'|'item'|'token', id, golden: bool, tier, items: [ {
- Emotes: 6 themes × 6 battle emotes (research 09 §4.2), image-only bubbles beside the sender avatar, 1 s cooldown, 3 s bubble; `g.emote {id}` with the official emoji id.
---
### Opening reroll messages and local seat recovery
C→S adds host-only `room.rerollSetup {setupRevision}` / `room.cancelReroll {voteId}` and participant
`g.rerollVote {voteId, agree}`. `m.public` carries `setupRevision` and nullable `rerollVote {id, proposerId, voters, agreed}`.
Ready must reference the current revision; omission means revision 0 for old clients. The server blocks Ready while
voting, refuses stale ids and cancels on disconnect/leave; DESIGN §28.21 / META.md describe the complete contract.
Explicit local recovery (§28.20) reuses saved hello tokens after a fresh live-tab claim check. Display names and room
codes are UI metadata only, never authentication. No new network message or nickname/IP matching is introduced.
+157
View File
@@ -187,3 +187,160 @@ assertion is corrected to require actual healing, and development builds retain
新增真实子进程继承 stderr 的回归,以及关闭失败/信号退出的边界检查;验收要求原异常文件单独重跑并确认正常结束。首轮异常与复测结果分别保留在本轮验收记录中。
同一轮的版本重连测试在刻意停服时,Chrome 可能报告连接拒绝、连接重置或未连接的 socket。测试只在主动停服至恢复握手的窗口内排除这些已知传输日志;正常运行期间仍检查它们,脚本异常始终保留,所有版本指纹、重连及换局刷新断言不变。
### 28.13 Special targets respect DEFAULT attack timing — #438–440
Seen: 死芒's normal attack reaches enemies blocked by his own summons, but S1/S2 did not open for those targets.
The earlier 蕾缪安 fix also registered a per-tick trigger, allowing an early cast before her attack countdown elapsed.
The [PRTS mode help](https://prts.wiki/w/卫戍协议/帮助) describes DEFAULT activation at the next ordinary attack.
`addTriggerRange` now accepts `attackOnly`; both operators use it, while existing summon-area triggers retain their
per-tick behavior. Only live owned summons / wanted marks contribute selectable targets. S2's actual link range is
unchanged. PR #438 by @YinqiMargatroid addresses the same wanted-target symptom; the existing registration API is
extended instead of introducing a second targeting flag. SIM.md and design/engine.md contain the current contract.
Tests: real summoned-bone blocking, S1/S2 and united fields, attack countdown, sleep/ownership, wanted-target S1–S3,
plus existing 令 / 麦哲伦 trigger regressions. The combined operator checks pass 27 tests; the additional area-trigger
and champagne controls also pass. Golden movement is recorded below.
### 28.14 Interface text size — #435 / PR #446
Adapted from @7eLaryn: four persisted text sizes use `--t` without changing the rem layout root. All shipped language
packs retain their current translations. Added the missing explanatory note and fixed English XL settings overflow:
segmented controls and header actions wrap within their available width. Long briefing alliance labels size and wrap
with their cards instead of overlapping neighboring names. Default sm preserves the previous sizes.
Tests: settings / text-unit checks, phone landscape growth and unchanged board/canvas/shop geometry, reload persistence,
and XL settings in Chinese, English, Japanese, Korean and Traditional Chinese. The first multilingual check exposed
English overflow; the corrected browser suite passes. Current behavior is in design/client.md and PLAYING.md.
### 28.15 Cooperative card purchasing — PR #407
Adapted from @IceCodeNew. [ASSUMED] A tier V/VI shared-pool card matching another living teammate's active opening
bond receives one 18-point buying-score penalty. It is a soft preference, not reserved stock; normal/elite and DIY
stock semantics remain unchanged. Humans and bots receive the same cooperation. This does not add persona presets.
Tests: 14 targeted bot tests; 40-seed solo:HARD and coop4:HARD before/after benchmark is part of integration evidence.
The comparison includes the special-target timing changes of §28.13, so it must not be interpreted as an isolated
measurement of the purchasing heuristic. Current rule: design/match.md §6.6 and its follow-up contract.
### 28.16 AI teammate emotes — PR #444
Event wiring from @DDDarkstar, with corrected dispatch semantics. [ASSUMED] Cosmetic reactions are enabled only in
mixed human/AI matches, under the existing emote whitelist and cooldown; `SP_BOT_EMOTES=0` disables all hooks.
Responses skip cooling bots and stop after one reply. MVP uses this round's kills and damage, with stable order for
ties; settlement reacts once per round. Only the third chess merge reacts, then resets for the next round. A gift uses
its source playerId rather than a display-name heuristic. No RNG, gameplay decisions or timers are introduced.
Tests cover whitelist/cooldown, no human impersonation or recursive AI reply, pure-AI silence, round statistics,
settlement/merge limits, gift identities, the disable switch and actual emote/acquisition hooks. README, META.md and
design/match.md document the switch and boundary.
### 28.17 Active skill attack and impact sounds — PR #410
Adapted from @Convey123's updated PR. The current official `audio_data.json` gives 司霆惊蛰 mode attack banks and
赤刃明霄陈 S3 separate swing/impact banks. The generator adds per-slot `attacks` / `hits`, each with its own optional
`attackMix` / `hitMix`. It preserves the selected bank's chance and volume rather than applying the normal attack's
mix. A late impact keeps the mode at launch; an end before spawn cancels a pending start. Snapshot restoration and
ordinary-mode fallback remain supported. JP voices and current roster assets survive regeneration.
[ASSUMED] Where no activation sound names the mode, slot 1/2/3 is inferred as d/h/s. Bank selection prefers START to
ON and then plain/numeric order; this approximates an ability graph and is not independently established for every
operator. Mixed-letter banks are rejected. Instant effects that emit no normal attack event remain outside this
mechanism. ASSETS.md and design/client.md specify the manifest/runtime contract.
Tests: 85 audio, manifest, official-mix and JP-voice checks, including independent mode gain, delayed impacts,
end-before-spawn, late join and field reset. Regeneration is additive; no existing manifest leaf was removed. Four
unresolved upstream candidate icon/avatar leaves still make strict asset fetching report failure; they are not new
sound failures or deletions of existing entries. Packaging must retain this limitation visibly.
### 28.18 Per-operator voice language — #436
Roster details and selected DIY cards offer 中文 / 日本語 / 跟随全局. Settings persist a sanitized charId map locally;
normal/elite/DIY forms of that charId share the choice. Existing profiles migrate to an empty map, so global language
continues to apply. Deleting an override restores the current global choice. Missing Japanese slots / files keep the
existing Chinese fallback. No room protocol or exported combat-loadout data changes.
Tests cover malformed/inherited entries, persistence, changing the global default, deleting an override and fallback;
a real phone-sized browser changes the control, reloads and resets it. Current contract: design/client.md / PLAYING.md.
### 28.19 Browser installation — PR #413
Based on @Cromemadnd's proposal, with a one-use install controller. Buttons appear only after a browser install offer;
standalone/fullscreen and installed sessions hide them. Accept, dismiss, failure and duplicate clicks all consume the
same offer once. See [the prompt API](https://developer.mozilla.org/en-US/docs/Web/API/BeforeInstallPromptEvent/prompt)
and [installability requirements](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable).
The existing original fortress favicon supplies correctly sized 192/512 px icons, shipped outside optional game art.
No service worker or offline cache is installed; the server is still required.
Tests: offer lifecycle/disposal, actual PNG dimensions, real Chrome manifest and installability checks, and a synthetic
dismissed offer through the actual button. Initial incognito rejection was a test-environment limitation; the real
installability check runs in Chrome's ordinary temporary profile. Native OS installation itself is not performed.
### 28.20 Recover a closed local match window — #431
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
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.
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
the match tab. The existing identity/static suite also passes. Current contract: design/client.md and network.md.
### 28.21 Unanimous opening reroll — #77 / PR #408
Adapted from @Siornya, excluding the unrelated result-screen redesign. [ASSUMED] This remake feature is host-proposed,
requires every connected human's consent, and pauses confirmation while voting. Bots/spectators do not vote; autoplay
humans still do. Single-human matches refresh immediately. Refusal, cancellation, disconnect or leave restores the
old setup and remaining confirmation time. Success preserves room/seats/settings, resets Ready and restarts the full
normal timer; fixed stages/bans remain fixed. The random result may repeat.
A dedicated seed-derived reroll stream does not advance the original shop/draft/meta streams. A 300 ms anti-burst guard,
setup revision and vote id reject stale actions. Opening data and every player's DIY stock are prepared before any
mutation; a forced failure on the second player's stock leaves all previous state intact. Current contracts are in
META.md, design/match.md, design/network.md and PLAYING.md; all four translation packs have the new messages.
Tests: 13 match checks including real four-player sockets, permission/spectator checks, cancellation/disconnect,
reconnect, last-second and queued-Ready timers, outdated messages, one human with bots, fixed rules, fuzzed requests
and transactional failure. Final browser/integration validation supplements these targeted checks.
### 28.22 Late-loading deployment animation — part of PR #416
@Cromemadnd's proposal exposed a real missing case: onDeploy arrived before the model, so Start was never played.
The view now retains elapsed game time until the model arrives and continues the existing clip at that point. A clip
already over is not restarted; an intervening attack/death cancels the pending entrance. Existing Front/Back transfer
and single-source deployment audio remain unchanged. The proposed repeated phase/purchase sound sequence is not part
of this fix: its timing and duplication need separate visual evidence.
Tests: delayed load, simulation rate, load after clip end, attack/death supersession, destroyed view and existing
UnitView behavior, 48 checks passing. No simulation/golden change from this visual fix.
### 28.23 Follow-up golden movement
Recomputed all 285 scenarios from the integrated code; never copied PR baselines. The DEFAULT cadence correction moves
`roster-016`, `roster-035`, `roster-036`, `roster-045`; paired fields `boss-boss_1-pair`, `boss-boss_3-pair`,
`boss-boss_5-pair`, `boss-boss_7-pair`, `hidden-boss_9-pair`; and match `solo-HARD-9-boosted`. 死芒's added activation
condition moves `diy-132`. `coop2-NORMAL-14-standins` includes both cooperative purchasing and the corrected simulation.
The pulse queue correction of §28.24 additionally moves `hidden-boss_10-solo`. The bonds and standins families are unchanged. Cosmetic emotes, preferences, installation and opening rerolls that
are not requested do not intentionally alter those games. The validation record lists any outcome flips and measured
benchmark changes; the independent final review remains pending.
### 28.24 Echo / counter recursion found by the cooperative benchmark
The 40-seed benchmark exposed existing hook-depth errors in co-op seeds 3 and 29, plus seed 14 after the purchasing
change. In all three, “余音” damage pulses and 刺玫 / 泡泡 counters synchronously called each other. A scaled echo can
have 67 hit-count HP; the generic 32-hook guard cut off effects before the legitimate chain finished. The same chain
could also enter another hit while zero HP was waiting for death bookkeeping.
[PRTS “余音”](https://prts.wiki/w/“余音”) establishes the hit-count and damage-pulse behavior. [ASSUMED] The exact nested
ordering is not documented: each echo now queues recursively earned pulses and resolves them FIFO in the same call,
after the current pulse finishes. Every earned pulse retains its attack value, including the lethal hit's pulse. No
extra tick, RNG draw, global hook-depth increase or arbitrary pulse suppression is introduced. Death prevents further
counter targeting normally. This local scheduling choice needs final fidelity review.
Tests use real elite 刺玫 and 泡泡 against 67-hit echoes: all 67 pulses resolve, the echo dies, no engine error is logged,
and the queue clears. Both tests fail against the previous implementation. Together with the existing enemy/boss and
刺玫 healing suites, 273 tests pass. The benchmark now writes actual diagnostic messages beside nonzero error counts,
so failures cannot be mistaken for ordinary performance results. Recomputed golden and final benchmark results are
recorded with the candidate; do not reuse the earlier, erroneous benchmark as a clean result.