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:
1 parent
314564c139
commit
f3658a0dd4
104 files changed
+3674
-741
No files matched your search
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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.
|
||||
Reference in new issue
Block a user