Commit Graph
169 Commits
Author SHA1 Message Date
sganggs fbcbfdffbc feedback7: merge fb7-voices — the 选中干员 line on every tap (prep / shop / hand, official FOCUS_CHAR: priority 10, cooldown 0, no gap restart), the Japanese voice tree (audio.voiceJp, 2674 files, setting 语音语言 中文 / 日本語, CN fallback), the full zip ships both
- Conflict: public/js/ui/settings.js imports (store from #321 + VOICE_LANGS) — both kept. test/ui/playtest6-elite.test.js's
  source pin of the onDetail line allows the new tap counter (bcee737f).
- Grok review: MERGE AFTER FIXES — the three findings fixed on the branch (bcee737f). Golden unchanged; broad node --test
  5334 (the one failure was that source pin, fixed), i18n strict clean, lint 0 errors, typecheck.
2026-10-08 20:35:26 +08:00
sganggs 7d6fefe8f5 feedback7: merge fb7-hud — HP-bar readouts (#303 / #378 / #383: ammo cells, 伺夜's wolf-shadow pips, 斩业星熊's 我执 bar), #302 renderer abort, #292 held skill cue, #334 atlas slots, #354 梓兰's per-card frost
- Grok review: MERGE (no defect). Golden unchanged (union OK); broad node --test 5327 (5326 pass, 0 fail, 1 skipped); lint 0 errors.
2026-10-08 20:29:27 +08:00
sganggs bcee737f40 feedback7 R22-VOICES: review fixes — a 选中 tap no longer restarts the battle-voice gap, and the second shop / reward card of one operator says its 选中 line too; the package switch's comment quotes the measured JP size; golden: unchanged
1. VoiceGate.start no longer moves lastAt for VOICE_TAP_SLOTS: a tap still skips the 1.2 s gap but starts none, so a
   部署 / 技能 / 接敌 the battle asks for once after a short 选中 line has ended plays where the last battle line's gap
   ends (a 部署 at 0, a tap at 500: a 作战中 at 1300 was dropped, now plays; inside the 部署 gap it still waits).
2. Shop / reward cards hand only the chess id to the detail, so two cards of one operator (the pool deals duplicates)
   had one 选中 key and the second tap said nothing nor replaced the first line: game.js numbers every card it opens
   (`tap`, cardTap), resolveDetail keeps it, and detailPanel's new selectVoiceKey() includes it (a re-render of the
   same tap keeps its key; pieces / units unchanged). Checked in the mock match: the second card of 歌蕾蒂娅 now calls
   voice() (it made no call on the parent).
3. tools/package.mjs FULL_ZIP_JP_VOICE comment: 89,388,041 bytes = 85.2 MiB on disk, about 76 MB zipped (as ASSETS /
   DEPLOY), not "~93 MB, barely compresses".
Tests: test/ui/audio.test.js (the tap-gap case; one earlier assertion that encoded the old gap after a tap replaced),
test/feedback7-voices.test.js (new: the card tap in resolveDetail / selectVoiceKey / game.js) — both fail on the parent.
§21.30's 0.2.2 note updated.
2026-10-08 20:28:38 +08:00
sganggs fc2e57ebff feedback7: merge fb7-g-small — PR #368 (AI teammates draft last, default off), #374 (double-click a teammate's avatar in prep), #321 (copy local diagnostics; whole-token / exact-value redaction) with the review's fixes
- Golden unchanged (union OK); broad node --test 5292 (5291 pass, 0 fail, 1 skipped); lint 0 errors.
2026-10-08 20:23:26 +08:00
sganggs f9969aeeef feedback7: merge fb7-g-ui — PRs #306 #309 #341 #349 #356 #358 #361 applied as-is (Grok coding run; static review p3: MERGE)
- Golden unchanged (union OK); broad node --test 5257 (5256 pass, 0 fail, 1 skipped); lint 0 errors.
2026-10-08 20:14:09 +08:00
shadiaojumao-hyb 073507eca3 feedback7 G-SMALL: double-click a teammate avatar in prep to open their field (PR #374 by @shadiaojumao-hyb)
Applied the PHASE.PREP double-click on a teammate's avatar. 「前往查看」 stays. Boss-prep phases still use that button.

A teammate with no field is not opened: the click still toasts, and the double-click does not call onWatch. A click while that teammate is already on screen does not open the view again, including the second click of a double-click in server-run combat, where the first click already watches.

Tests: node --test test/ui/prep-dblclick.test.js test/ui/playtest6-unite.test.js — 13 pass. node --test test/ui/playtest3.test.js — 25 pass (same run, before the source-assertion fix; that file was not changed again). npx eslint public/js/ui/teamPanel.js test/ui/prep-dblclick.test.js — 0 errors.
2026-10-08 20:00:57 +08:00
anAtheist987 fd791a66cb feedback7 G-SMALL: co-op rooms can make AI teammates draft last (PR #368 by @anAtheist987)
Applied the host-only room option 「AI 队友最后选择」, default off. Off, the draft order and the random streams stay as they are. On, humans draft before AI seats after the existing shuffle, and a skip stays ahead of the AI seats.

Dropped docs/history/0.2.2.md and every §27 citation. The player-facing sentence is in docs/PLAYING.md. The room-option rule is in docs/design/match.md, docs/design/client.md and docs/design/network.md. The #338 per-bot faction and ban presets are not included.

Tests: node --test test/lobby-ai-last.test.js test/match/ai-picks-last.test.js test/ui/ai-picks-last.test.js — 18 pass. node --test test/lobby.test.js test/client-static.test.js — 378 pass. node tools/i18n.mjs check --all --strict — 1032/1032 in en, ja, ko and zh-TW, 0 errors.
2026-10-08 19:57:26 +08:00
sganggs 16df0bf466 feedback7 R22-HUD: the card 梓兰's 猎头顾问 freezes shows its frost — every shop card draws its own slot's freeze, not the toggle's (GitHub #354 by @DuolaCCCCat); golden: unchanged
Seen (reproduced on the parent, notes repro-354.mjs): band_orchid's active refresh copies the top-tier operator into another
slot and freezes the copy (bands/meta.js band_shop_refresh_copy_max_lv_char, slot.frozen = true) while shop.frozen — the
toggle — stays false; m.private sends both (views.js: shop.frozen and slots[i].frozen). The shop bar passed the toggle
(`frozen = !!shop.frozen`) to every ChessCard / ItemCard, so no card showed the frost: the server kept the card to the next
prep, the player could not see which one.

Now: shopBar.js slotFrozen(slot, toggle) = the slot's own flag, or the toggle (a frame without per-slot flags — the dev
mock — and the old behaviour for the 冻结 button, which still copies the toggle onto every unsold slot); both card kinds
use it. The bar's frame (is-frozen) and the 冻结 / 解冻 button stay on the toggle. The rule is not touched (bands.test.js,
and the new test): a second manual refresh re-rolls the frozen card too — like the toggle, "再手动刷新会重抽" — and
freezes one of the new pair; with no further refresh the frozen card stays in the next prep (the flags are cleared
then, round.js startRound).

- Tests: test/ui/feedback7-orchid-freeze.test.js (4: the wire — exactly one frozen card of the pair, toggle off — and the
  cards drawn from it; the toggle still freezes every slot; the rule unchanged across two refreshes and a round; the
  ShopBar source pin); fails on the parent (no slotFrozen, the cards passed `frozen`).
- Docs: PLAYING.md (冻结 row), design/network.md (m.private slot.frozen). Golden: unchanged (client render only).
2026-10-08 19:22:08 +08:00
LimitlessHPPK 3484178e2e feedback7 R22-HUD: a skill cue that reaches the client before its unit is held and sounds once when the unit is tracked — the first cast of a deploy-time skill was silent (PR #292 by @LimitlessHPPK, without handing the whole early buffer to the sound); golden: unchanged
Seen (Grok's review, reproduced here with the real sim): the sim's deploy emits a deployment-activated skill's
['skill', id, 1] before the unit's ['spawn', unitInfo] (宴: three events earlier) and audio.js dropped a skill event of an
unknown unit, so that one cast made no sound while every later one did.

- audio.js: pendingSkill — a 'skill' ON for an unknown unit is held (one per unit, 64 at most, the oldest go first);
  _track plays it once when the unit's spawn arrives; a skill OFF is no cast and is not held; setFieldUnits drops the holds.
  Changed from the PR: setFieldUnits clears the holds BEFORE it tracks the new field's units (the PR cleared after, so a hold
  left over from the previous field would have sounded for a unit of the new field that shares its id — ids are per battle).
- screens/game.js: NOT the PR's change. The pre-entry buffer (keepEarly: every spawn, die, deploy, status and skill since the
  field's m.field, up to 1500 of them) is handed to the sound as its 'spawn' tuples only (new early.js audioEarly): the whole
  list would play every old death, deploy line and cast at once on entering. The live path (onEv) already forwards the full
  list, so the hold alone fixes the live silent first cast; a deploy-tick cast that falls into the short pre-entry gap
  stays unheard (a deliberate gap — replaying "only the casts before their spawn" would still sound a minute-old cast when a
  field was buffered while another one was on screen).
- Tests: test/ui/audio.test.js (3: the held cast in one batch and across batches, once, no hold for a skill OFF; a hold does
  not outlive its field and is bounded; 宴's real deploy: the sim's cast comes before the spawn and sounds exactly once — all
  fail on the parent) and test/match/feedback1-runner-forms.test.js (the audio gets the spawn tuples only: silent replay that
  still teaches the units, the next live cast sounds; the whole list would have sounded old cues). Docs: design/client.md.
- Golden: unchanged (client audio only).
2026-10-08 19:22:08 +08:00
siruimei07 030d0a2938 feedback7 R22-HUD: a renderer that loses the 12 s race no longer mounts its canvas late — the attempt is aborted, a view that still comes back is destroyed, the host is no longer wiped (PR #302 by @siruimei07); golden: unchanged
Applied unchanged after review (Grok static review: MERGE; the diff read in full: no network, no new dependency, no
shipped eval). The leak was real on feedback7: mountFieldView raced createFieldView against a timeout and, on failure,
removed every child of the host while the factory kept running — Pixi finished later and appended its canvas over the
simplified view (taking its clicks); the host wipe could also delete a newer view mounted by the same hook.

- ui/fieldHost.js: mountFieldView(host, { signal }) owns an AbortController; the timeout, a failed import or an invalid
  view aborts it, a view a factory returns after that is destroyed, an outer abort (useFieldView's cleanup) rejects
  without a fallback; nothing clears the host's children any more.
- render/app.js createFieldView takes the signal: the Pixi load / manifest / font waits end on abort, the synchronous
  build registers its disposables (setupCleanup) so a throw releases the app, tiles, fx, impostors, listeners and
  observers it made, the board-art and 3D-board waits moved after the view exists (aborting them is view.destroy(),
  idempotent), destroy() also removes the canvas. render/app/host.js withTimeout(p, ms, signal).
- Tests: test/ui/fieldHost-lifecycle.test.js (5, node vm, fake timers: all fail on the parent) and the opt-in real-browser
  test/render/initialization.browser.test.js (6 scenarios in headless Chrome, 5 fail on the parent: a late Pixi load
  covering the fallback — with a factory that honours the signal and one that ignores it —, art timeout, a setup
  exception, an unmount racing a newer mount); test/ui/playtest2.test.js's source pin takes the new indentation.
  fieldview / browser suites in Chrome unchanged (the two 4× throttled fps tests flake when two Chrome files run
  at once, pass alone).
- The createFieldView body stays unindented inside its new try (the PR's choice, to keep the diff reviewable).
- Docs: design/client.md §9. Golden: unchanged (client lifecycle only).
2026-10-08 19:22:08 +08:00
sganggs ddf8c39ed0 feedback7 R22-VOICES: the Japanese voice dub as a second tree beside the Chinese one — audio.voiceJp (the same slots and file names from ArknightsAssets2 voice/: 2674 files, 89.4 MB, for the 191 voiced operators), 设置 → 语音语言 中文 / 日本語 (default 中文, not tied to the interface language), a JP line the manifest or the host lacks plays the Chinese one; the full zip ships both dubs (one switch, tools/package.mjs FULL_ZIP_JP_VOICE), the lite zip stays art-free (the owner's request of 2026-10-08 「全套的日配语音」); golden: unchanged
- tools/assets/plan.mjs: one voiceTree() per dub — audio.voice (--voice-lang, cn by default) and audio.voiceJp
  (VOICE_JP_LANG, always the JP dub, audio/voice/jp/<charId>/cn_nnn.mp3); fetch-assets plans and downloads both
  (stats.voiceJpChars), so setup (checkAssets walks every listed URL) fetches both too.
- data/assets.json regenerated with the project's tool (node tools/fetch-assets.mjs --add-only, raw.githubusercontent.com,
  107 s): the only change is audio.voiceJp + stats (+2674 files, +89,388,041 bytes, voiceJpChars 191) + hash — the rest
  is byte-identical; the JP tree is the Chinese one with the folder swapped (191 × 14 files, every file a valid MP3).
  The audio files are not committed (public/assets/ is git-ignored).
- public/js/audio.js voiceLine(): 'jp' draws from voiceJp and falls back to the Chinese line per slot (a JP entry the
  manifest lacks) and per line at play time (the Chinese file of the same name when the host lacks the JP one —
  same token, same gate); operators without voice (stand-ins, 盟约·辅助干员, summons) stay silent. setVoiceLang().
- settings: voiceLang 'cn' | 'jp' (VOICE_LANGS, sanitizeSettings), the 语音语言 row under 干员语音 (each dub named in
  its own language, like the language switch), 语音语言 in the en / ja / ko / zh-TW packs.
- tools/package.mjs FULL_ZIP_JP_VOICE (true): false holds the JP files out of the full zip (and its update zip) —
  not missing, not orphans, never in an update's `removed`; setup then downloads them.
- Sizes measured: art download 548 MiB (was 463), full zip ≈ 505 MB (0.2.1: 428 MiB + 76 MiB of zipped JP voice),
  unzipped 711 MB (package --dry-run); lite zip unchanged. README, DEPLOY, ASSETS, setup's fallback size updated;
  PLAYING voice paragraph; §21.30 "Changed in 0.2.2" note.
Tests: test/feedback7-voices.test.js (new: plan, manifest, settings, package switch), test/ui/audio.test.js (voiceLine,
the manager's dub choice and fallback), test/ui/gameLogic.test.js, test/docs-consistency.test.js — all fail on the parent.
2026-10-08 19:09:52 +08:00
sganggs cb8865d9d8 feedback7 R22-VOICES: a tap on an operator says its 选中干员 line in every phase — the prep, the shop and the hand too, not only while a battle runs (the owner's request of 2026-10-08 「添加一下干员点击上去的语气一样的语音」, lifting 2026-10-03's 「整备阶段不需要干员语音」 for this line only; [ASSUMED] the official prep tap = the battle's FOCUS_CHAR); golden: unchanged
screens/game.js passes the detail panel voice=${true} instead of its combat flag, so the panel's existing
audio.voice(charId, 'select') runs on every opened operator: a piece on the field or in the hand, a shop /
reward card, a bond member. The gate follows the official FOCUS_CHAR row (audio_data.json battleVoice:
priority 10, cooldown 0, overlapIfSamePriority true): VOICE_TAP_SLOTS — on an idle channel a tap always
plays (no 1.2 s global gap; VOICE_COOLDOWN_MS.select 1500 → 0), a newer tap replaces the 选中 line on air,
and a higher-priority line (部署 20, 作战中 70, 开战 100 …) is never interrupted. Deploy / skill / engage /
result lines stay battle-only; stand-ins, 盟约·辅助干员 and summons stay silent (no voice entry).
Tests: test/ui/audio.test.js (new: the gate and two taps through the manager; fails on the parent),
test/docs-consistency.test.js pins updated. Docs: §21.30 "Changed in 0.2.2" note, PLAYING.md voice paragraph.
2026-10-08 18:54:02 +08:00
Xinglan233 5f0a06cc70 feedback7 G-UI: retreat with a full bench counts the slot of the own summon stack (PR #361 by @Xinglan233)
Applied as-is. The client precheck for withdrawing a board operator onto an occupied hand slot now treats that operator's own hand summon stack as a free slot, which the server already accepted. Temp stacks, board summons, another player's tokens, and an item that only carries ownerUid stay HAND_FULL. stack.count is not extra slots. Dropping on a chess still checks the swap first.

Tests: node --test test/ui/gameLogic.test.js test/ui/facing.test.js test/ui/uiFixes.test.js test/match/feedback1-placement.test.js test/match/feedback5-we2-placement.test.js — 116 pass, 0 fail. Before this commit: npm run -s lint — 0 errors (134 existing warnings); npm run -s typecheck — pass; npm run -s check:imports — pass; node --test test/docs-consistency.test.js test/docs-paths.test.js test/version.test.js test/package.test.js — 47 pass, 0 fail. No new UI strings, so the i18n check was not run. No golden update.
2026-10-08 18:45:01 +08:00
Xinglan233 bc22f9cbe6 feedback7 G-UI: Enter on the equip-replace dialog's buttons keeps its native behaviour (PR #358 by @Xinglan233)
Applied as-is. The replace dialog no longer treats Enter that started on a button as confirm. Enter or Space on 取消 closes the dialog and leaves the chosen equipment in place. Enter on an option only changes the selection. Enter on the confirm button still sends g.equip with that replaceUid. This matches the guard already used by DialogHost.

Tests: node --test test/ui/mock.e2e.test.js test/ui/leftovers.test.js test/ui/standin-ui.test.js — 31 pass, 0 fail. The mock e2e suite skipped because SP_E2E is unset. eslint on the touched files: 0 errors (one pre-existing unused-assignment warning in mock.e2e.test.js).
2026-10-08 18:44:09 +08:00
Xinglan233 5952539046 feedback7 G-UI: clear the 调度中心 upgrade confirm for the reached level after pressing D (PR #356 by @Xinglan233)
Applied as-is. The shop bar's two-tap arm for the level button is now lv:<level>. A successful upgrade, including the D shortcut, changes the level and drops that arm, so the next click only selects the new level. A freeze at the same level keeps the confirm. Another card's arm is left alone, and D still upgrades immediately.

Tests: node --test test/ui/mock.e2e.test.js test/ui/playtest6-elite.test.js test/ui/playtest6-ui.test.js — 20 pass, 0 fail. The mock e2e suite skipped because SP_E2E is unset. eslint on the touched files: 0 errors (one pre-existing unused-assignment warning in mock.e2e.test.js).
2026-10-08 18:43:40 +08:00
siruimei07 8597b66df3 feedback7 G-TOOLS: the boot build tag includes sim, shared and data changes (PR #351 by @siruimei07)
Applied as-is. BUILD_INPUTS now covers server/sim, shared and data as well as the page, so a restart after a simulation-only or data-only change is visible to an already-open page. The tag is still computed once per process. A page still waits until the match, including settlement, is over before it reloads, and a failed check still does not reload.

Tests: node --test test/build.test.js test/ui/build-runtime.browser.test.js — 8 pass, 1 skipped (the Chrome restart test; SP_E2E unset), 0 fail.
2026-10-08 18:42:46 +08:00
Xinglan233 e2503c7f60 feedback7 G-UI: the import dialog traps keyboard focus and yields while the guide is open (PR #341 by @Xinglan233)
Applied as-is. The shared Modal keeps Tab inside the topmost dialog, pulls focus back if it leaves, skips disabled, hidden, inert and zero-size controls, and does not trap while a .guide overlay is open. Escape still closes the dialog. Enter is unchanged, so a focused button keeps its native click.

Tests: node --test test/ui/modal.e2e.test.js test/ui/loadout.e2e.test.js test/ui/leftovers.test.js test/ui/match-info.test.js test/ui/loadout.test.js test/ui/playtest3.test.js test/ui/playtest4.test.js test/ui/uiFixes.test.js test/ui/localArt.test.js — 114 pass, 0 fail. The modal and loadout e2e suites skipped because SP_E2E is unset. eslint on the touched files: 0 errors.
2026-10-08 18:42:38 +08:00
siruimei07 5662b7ef65 feedback7 G-TOOLS: out-of-order language-pack responses no longer overwrite the newer choice (PR #304 by @siruimei07)
Applied as-is. initLang and switchLang record a request counter before the first await and apply a pack only when that request is still the latest. Choosing the language already on screen cancels an in-flight load. A failed latest choice still saves the preference and leaves the previous UI.

Tests: node --test test/ui/lang.test.js test/ui/i18n.e2e.test.js test/i18n-packs.test.js test/i18n-server.test.js — 29 pass, 0 fail. The Chrome suite in test/ui/i18n.e2e.test.js skipped (SP_E2E unset).
2026-10-08 18:41:58 +08:00
Xinglan233 25bcdff3a7 feedback7 G-UI: reselecting the same operator keeps its draft skill and module (PR #306 by @Xinglan233)
Applied as-is. Clicking the operator already in the 自选 draft no longer rebuilds the draft from the saved pick or the default, so an in-progress skill or module edit stays. Choosing a different operator still loads that operator's saved pick or defaultPick.

Tests: node --test test/ui/diy-ui.test.js test/ui/feedback5-module-trait.test.js test/ui/diy.e2e.test.js — 25 pass, 0 fail. The diy.e2e suites skipped because SP_E2E is unset.
2026-10-08 18:41:36 +08:00
sganggs 10ef14817b feedback6: Esc in the 自选 picker closes only the picker, like its 取消 (GitHub #284 by @Xinglan233, idea from PR #286)
- The 干员调配 overlay's capture-phase Esc closed the whole overlay while a 自选 picker was open (it only gave way to a
  .modal; the picker is a .diy-pick section without its own key handling), so one Esc went back to the lobby.
- DiyPicker registers its own capture-phase Escape handler (calls onClose, as 取消 and the × do; skipped while a .modal
  — the 导入 dialog — is open, which handles Esc first); loadout.js returns early on Escape while a .diy-pick is open.
  The second Esc closes the overlay as before. Saved picks are untouched (the draft is dropped, as with 取消).
- test/ui/diy.e2e.test.js: a real-server browser test that needs no art (5阶 slot, then a 6阶 slot with Esc pressed in
  the search field): the picker closes, the overlay and its four slots stay, sp.pref.diy is unchanged, the next Esc
  closes the overlay. Fails on the parent (「Esc keeps the 干员调配 overlay」), passes here.
- Tests: SP_E2E=1 diy.e2e (the new test) 1 / 1; client-static, i18n, ui/diy*, ui/loadout* 369 / 369; eslint clean.
2026-10-07 18:38:20 +08:00
sganggs 3f918c9f03 feedback6: merge fb6-small-fixes (see its commits) 2026-10-07 18:16:10 +08:00
sganggs 3c36cdff48 feedback6: merge R21D — with both slots full, every consumable (博士投影, 盟约之币 …) opens the replace dialog and destroys only the picked item once its effect succeeds (PRTS 卫戍协议/帮助 「达到上限强行佩戴会改为替换装备…并销毁被指定的装备」; GitHub #263, PR #264 by @SpiritedAwayCN, ported); golden as R21D's commit (10 matches: bots using a consumable on a full operator), the merged tree differs from the branch only in R21B's 联防 scenarios 2026-10-07 18:11:53 +08:00
sganggs 8cef750e14 feedback6 R21D: a consume-on-equip item on a full carrier opens the replace dialog and replaces the picked item first (GitHub #263, PR #264); golden: matches 10 of 18 moved (a targeted consumable on a full carrier now destroys the oldest item), the rest unchanged
PRTS 卫戍协议/帮助 §装备: 「达到上限强行佩戴会改为替换装备:指定一件已佩戴装备替换为将要佩戴的装备,并销毁被指定的装备」,
no exception for the 装备时销毁 items. Before: a normal 博士投影 opened no dialog and its kept item pushed the OLDEST
equipped item out (replaceUid ignored); 盟约之币 and the other consumables kept both items.

- server PlayerState.equip (player/items.js): on a full carrier the consume path takes the pick (replaceUid, else the
  oldest) off before onEquip (_takeReplaced), destroys it (onDestroy reason replace) once the effect went through —
  the normal 博士投影 takes the freed slot —, and puts it back at its index when the effect refuses (博士投影 on an elite:
  nothing destroyed)
- client gameLogic equipReplaces: the replace dialog opens for every item on a full carrier, consumables included,
  except an attaching item that completes an item merge and a 博士投影 on an elite; itemAttaches removed
- bots (itemTarget): a consumable whose effect is the same on anyone goes to a carrier with a free slot first (PR #264)
- docs: design/match.md equipment rules, META §2.5, PLAYING 装备 row, research 04

golden (matches only; each traced equip by equip against the parent — the first change of state is a bot's, or the
AI 托管 human seat's, targeted consumable on a full carrier destroying its oldest item): solo-FUNNY-1 (r9 ai_0 随身身份牌),
solo-FUNNY-2 (r9 ai_0 寻呼模块), solo-HARD-2 (r9 ai_0 随身身份牌, r12 简易通讯机), coop2-NORMAL-3 (r9 ai_1 随身身份牌),
coop3-ABYSS-5 (r11 ai_1 随身身份牌), coop4-FUNNY-6 (r4 ai_1 随身身份牌), solo-HARD-9-boosted (r14 ai_0 随身身份牌),
coop2-ABYSS-10-boosted (r13 ai_0 随身身份牌, r14 ai_1 简易通讯机), coop2-NORMAL-14-standins (r9 ai_0 随身身份牌; now won,
14 rounds), coop2-NORMAL-70-diy (r14 h_0 随身身份牌); the bots' free-slot carrier for target-agnostic consumables moves
no scenario by itself; roster / bonds / fields / standins / diy unchanged
2026-10-07 18:03:53 +08:00
sganggs c623550f29 feedback6 R21E: the 外勤医疗 Touch casts 恳切福音 on an injured ally inside its 5-2 (ACTIVE_RANGE in touchKit — the larger-range rule the Touch 补位 already runs by its data; the map character's record keeps DEFAULT), and the Touch / 预备干员-医疗 card is an operator card (干员 · 医疗 · 远程位, 特性, skill row with SP / duration, talents from the tokens.json record — it took the summon path: 召唤物, base stats only); 恳切福音 gets a line under its text that the boost is strictly below half (PRTS 修正 of 不高于, what touchGospel does; the operator card of the 补位 Touch too: SKILL_TEXT_NOTES); GitHub #260, PR #278; golden: roster-041 (its strategy Touch heals 8438 → 17072, 19 → 27 heal actions, casts unchanged at 2, the same numbers as PR #278; p1 deaths 7 → 4) and matches coop2-NORMAL-14-standins (h_0 plays 外勤医疗: defeat in round 13 → victory, teamLp 16) — both battles with that strategy's Touch, no other scenario moved 2026-10-07 17:54:17 +08:00
sganggs 41fd3c7e39 feedback6 R21C: the bond popup lists every 自选 pick of the player (m.private.diy, the V and VI slots) in each bond its operator carries — dimmed like an unowned member until a copy is owned (not bought, or not in the shop yet), ✕ like a banned member when all its bonds are off this match (m.private.diyBanned), one row per slot drawn as its normal form with the 「自选」 tag, a tap opening the operator's card; 在场 and the 成员 header count an unowned pick for nothing, the order rule is kept (the owner's report of 2026-10-07 「自选编队的干员在局内对应盟约展开的名单里没有显示出来」); a watched teammate's popup still lists only their 自选 pieces on the field (m.public sends no picks), and such a row now hands the unit's pick to the card (BondPopup onMember → the detail target's diy → resolveDetail) instead of opening the empty 甄选干员 slot's card; golden: unchanged (client) 2026-10-07 17:43:37 +08:00
sganggs d0fc57ff4c feedback5: merge UI-TW — the Traditional Chinese interface (all msgids converted with OpenCC cn → twp, the official TW terms, a manual pass) and the machine-translation mark (_meta.machineTranslated → the pack index → a note under the language switch in 设置); golden unchanged 2026-10-07 12:07:07 +08:00
sganggs c11ba03da4 fb5 UI-TW: the machine-translation mark — _meta.machineTranslated: true, a new optional manifest field of a language pack (docs/PACKS.md): shared/i18nPacks.js langFields reads it (true or false), shared/packs.js normalizeManifest warns about a value that is not a boolean (read as false; the pack still loads) and packIndexEntry passes it into the index entry like complete (only when true), so the client's registry has it from the index (langMetaOf → registerLangs → langInfo) or from the pack's own _meta without one; 设置 shows a note under the language switch while the CURRENT language's pack has it (ui/lang.js machineTranslationNote, msgid 「当前语言的界面文字为机器翻译,可能不够准确,欢迎在 GitHub 上指正。」 — English in en.json, 目前語言的介面文字為機器翻譯… in zh-TW.json; ja / ko leave it to the integrator); tests: test/i18n-packs.test.js (the field from the manifest through the index to langInfo, the warning, the shipped zh-TW manifest, the note shown / hidden by language), test/ui/i18n.e2e.test.js (the shipped packs' own UI strings read from their files — zh-TW now 開始 — and the 设置 note, none in Chinese); docs/I18N.md says how the zh-TW pack is made (OpenCC twp, the official TW terms, a manual pass; a later msgid the same way) and that the ja / ko strings are machine translations open to corrections (edit public/i18n/<code>.json, node tools/i18n.mjs check <code> --strict) 2026-10-07 11:47:09 +08:00
sganggs 93a45f733f feedback5: merge R19C — GitHub #235: the official 「作战结束」 result box (全员无伤! / 生命值减少 −N) and the three battle-over sounds at settlement, PR #112 by @Convey123 ported onto the 0.2.0 layout (test/ui/gameLogic.test.js: both streams' tests kept); golden unchanged 2026-10-07 11:31:55 +08:00
sganggs f661a47773 feedback5 R19C: the official 「作战结束」 result box and the three battle-over sounds at settlement (GitHub #235; PR #112 by @Convey123, approved and never merged, ported by hand onto the 0.2.0 modules) — the box says only the viewer's own LP, 「全员无伤!」 or 「生命值减少 −N」; after a 联防 the number is the server's own per-player charge (m.public.uniteResult in the SETTLE view) and 「全员无伤!」 only when nothing got through; no box for spectator seats, eliminated players or the boss rounds; golden: unchanged 2026-10-07 11:21:24 +08:00
sganggs b4d5a72a43 feedback5 R19B: 干员调配 says plainly when its game data did not load — useData counts a /data file that never arrived (404, blocked, bad JSON: 'missing') as settled, so the tab drew an empty roster, the loadout import blamed the file (「这份数据在当前版本没有可用的调配」) and the 干员持有 / 自选编队 imports stored an empty list over the saved one; now missingGameData(tab) (chess.json, plus backups.json for 自选编队) shows DataMissing in place of the roster (which files, saved settings unaffected, reload, then the ad blocker and the network) and importRefusal(kind, ready) refuses every import before anything is parsed or applied, with the same words; three UI strings with English; GitHub #173; golden: unchanged (client only) 2026-10-07 11:13:25 +08:00
sganggs 7d97b80bfd feedback5 R19B: the round-start banner reads 观战中 to an eliminated player and to a spectator seat instead of 资金已到账 — the server pays round income only to the players still in (startRound runs ps.startRound for alivePlayers(), eliminate() zeroes funds and pendingFunds); phaseBanner takes the viewer { alive, spectator } from the match screen (a spectator seat has no row in m.public); the idea from PR #237 (not merged: it sits on the #189 / #191 stack); GitHub #236; golden: unchanged (client text only) 2026-10-07 11:03:28 +08:00
sganggs 2e83a6b977 feedback5: Japanese, Korean and Traditional Chinese ship — the official game texts of the JP / KR / TW clients (data/i18n/ja.json 96.5 %, ko.json 97.4 %, zh-TW.json 91.1 % of 22 114 texts, built with build-i18n --lang from ArknightsAssets/ArknightsGamedata) and minimal UI packs public/i18n/ja.json, ko.json, zh-TW.json (a _meta manifest, no UI strings yet): ja / ko fall back to the English UI, zh-TW to the Simplified Chinese UI (no fallback; numbers in 萬 / 億); the owner's decision of 2026-10-07; UI translations welcome from the community; golden: unchanged (client text only)
- The packs: _meta = type lang, the code, its own and English name (日本語 / Japanese, 한국어 / Korean, 繁體中文 /
  Traditional Chinese), version 0.2.0, app >=0.2.0, authors, credits (the official client data, © Hypergryph (/ Yostar
  for JP and KR), via ArknightsAssets/ArknightsGamedata), fallback ["en"] for ja / ko and [] for zh-TW; formatted as
  `node tools/i18n.mjs template` writes them, so a translator runs it on the shipped file and keeps the manifest.
- Built offline from the tables W5AD downloaded and checked today (jp / kr 51.10.0, tw 51.2.0; copied into
  .cache/gamedata-<code>): coverage as W5AD measured; `build-i18n --lang <code> --check` up to date for en, ja, ko,
  zh-TW. `node tools/i18n.mjs check --all --strict`: 0 errors (en complete; ja / ko / zh-TW 0 of 1022 msgids, fallback).
  `node tools/packs.mjs list` lists the four packs with their game texts; `tools/package.mjs --dry-run --list` ships
  data/i18n and public/i18n of all four and the generated packs/index.json.
- With five languages the menu is a list (ui/lang.js SEGMENTED_MAX 4, W5AD's rule): test/ui/i18n.e2e.test.js reads and
  picks either form, expects the shipped languages, and checks each new pack in the browser (its official game text,
  the fallback UI, <html lang>) — SP_E2E=1: 3 / 3; the title page of the five devices (devices.e2e "title page") still
  fits: 5 / 5.
- Docs: docs/I18N.md (intro, §2, §5 "Shipped packs" with the call for UI translations), docs/PLAYING.md (语言),
  DESIGN §25.20 and the §25 intro's decisions of 2026-10-07.

Gate: non-browser suites 5024 / 5024 (test/i18n*, test/package, client-static 358 / 358), npm run golden ok,
test/golden.test.js 9 / 9, lint 0 errors, check:imports ok, typecheck ok.
2026-10-07 04:16:59 +08:00
sganggs 9ca52ba798 feedback5: merge CB1 — community reports (bonds / strategies / items): a pair of colds on an enemy becomes the freeze and leaves no cold (the 谢拉格 wind no longer freezes for good), a 复活 (埃芒加德's 命结之秘, M3茧甲) is a knock-out and an immediate free redeploy where the operator lies (砾's and 瑕光's 被击倒时 / 部署时 layers fire), 佩佩's special refresh is paid, the result card hides the layers of 调和 / 协防干员 / 独行 / 绝技, the 炎佑 keeps off 外勤医疗's slot, 凯瑟琳's 定向投放 offers every shop item; 烛煌's 绝处重燃 before the hammer's lock and the 寻呼模块 / 简易通讯机 tier cap kept as they are (the owner's decisions of 2026-10-07); §25.15 → §25.21 (§25.15 is the shortcuts'); golden: vs feedback5 CB1's own rules — roster-005/009/010/012/014/015/016/018/026/029/031/033/036/040/042/046/047/049, bond-egirShip-high, bond-indomShip-low, bond-kjeragShip-high, bond-maniShip-high, bond-raidShip-low, bond-steadShip-high, bond-suntShip-low, boss-boss_2/3/4/6-pair, hidden-boss_8/9/10-pair, standin-07, matches solo-FUNNY-1, solo-NORMAL-1, solo-HARD-1, solo-ABYSS-1, coop3-ABYSS-5, coop4-FUNNY-6, coop4-ABYSS-7; vs CB1 what feedback5 changed since its base (CB2, CB3, CB4, CB5) plus roster-014
Conflicts resolved by hand:
- server/sim/content/items/battle.js: CB3 had extended `revivedInPlace` (an in-place 复活 restarts a deploy-timed
  skill, item 44); CB1 replaced the in-place revive by PRTS's form — `reviveNow`, a `death` hook that redeploys the
  operator at once, free, where it lies — and removed `revivedInPlace`. CB1's side is kept: its redeploy resets the
  skill, so an `activateOnDeploy` skill starts anew by itself; CB3's tests (test/sim/feedback5-raid-revive.test.js:
  the jump at once after M3茧甲 / 埃芒加德, the window back to full, 砾's fresh barrier) pass on it unchanged. Their
  header, bonds_core's comment, DESIGN §25.17.5 say so.
- docs/SIM.md, docs/design/engine.md: CB1's `fatal` rows (the 复活 are `death` hooks now); the `death` rows also say a
  deploy-timed skill starts anew (§25.17.5).
- data/choices.json: no conflict; `node tools/build-data.mjs --offline` on the merged tree rewrites all 15 data files
  byte-identically, and the JSON compare with feedback5 shows only pools.pool_equip_kathe.maxTier removed (CB1).
- docs/history/0.2.0.md: CB1's section renumbered §25.21 (its §25.15.1–10 and two self-citations; no citation of it
  elsewhere), placed last; the owner's decisions of 2026-10-07 on items 5 and 45 recorded there, in the intro and in the
  two pin tests' headers (feedback5-blaze-hammer, feedback5-cb1-pins).

Golden: solo-FUNNY-1 (outside CB1's list) has 6 埃芒加德 revives and 85 freezes on feedback5's path (instrumented).
roster-014 differs from CB1's own digest by 玛恩纳's 无动于衷: CB3's rule (every enemy damage instance) answers the
萨卡兹枯朽战车's pollution that hits a 卡西米尔 operator in CB1's timeline — with that counter limited to attacks the merged
roster-014 equals CB1's digest exactly (checked).

Gate: non-browser suites 5021 / 5021, npm run golden ok, test/golden.test.js 9 / 9, lint 0 errors, check:imports ok,
typecheck ok.
2026-10-07 03:54:35 +08:00
sganggs 66cdf493fc feedback5: merge W5AD — language packs as the first type of a generic content pack: a language is a file in the language folder (public/i18n/<code>.json with a _meta manifest, optional data/i18n/<code>.json) or a pack folder (packs/<id>/pack.json); the server lists them live at /packs/index.json, the menu shows every pack, strings fall back pack → base → declared fallback → Chinese, broken placeholders are skipped, plurals follow Intl.PluralRules (zh / en unchanged); tools i18n.mjs template / check, packs.mjs list / index, build-i18n --lang ja|ko|zh-TW; the release zips ship packs/ and packs/index.json; docs/PACKS.md, docs/I18N.md §5; §25.16 for now (renumbered next); golden: unchanged vs feedback5; vs W5AD exactly what feedback5 changed since f1df0d3 (CB5's 18 bot matches, CB2's roster 13 / bonds 7 / fields 6, CB4's roster-016 / 7 boss fields / standin-04/06/08 / diy-028/124)
No conflicts outside docs/history/0.2.0.md; the §25 intro keeps one sentence per stream (W5AD's line that also named
the hotkeys' §25.15 joined to CB4's).

Gate: non-browser suites 4981 / 4981 (test/i18n-packs 12 new, test/package 1 new), npm run golden ok,
test/golden.test.js 9 / 9, lint 0 errors, check:imports ok, typecheck ok, docs-consistency + docs-paths 31 / 31.
2026-10-07 03:27:10 +08:00
sganggs 37558f0d5e feedback5: merge CB5 — community reports (match / UI): module 特性追加 shown after the base trait (text only), a shop upgrade fills its new slot at once, spectators follow the player they picked through every phase (g.watch carries the playerId — PR #189's idea), the boss shows in boss-round prep scouting, the final-boss prep shows the pair together with green avatar frames (PR #191's idea, official colour); §25.19 after the hotkeys' §25.15; golden: the 18 bot matches (the upgrade's new card), battles unchanged 2026-10-07 02:41:19 +08:00
sganggs 1ee39a79b5 feedback5 WAD: docs for language packs and content packs — docs/PACKS.md (the manifest, the two layouts, discovery and the index, what is served, the types and how a later type plugs in), docs/I18N.md §5 "Adding a language" (step by step for a translator) with the fallback chain, placeholder validation and plural categories, CONTRIBUTING / README pointers, DEPLOY §7 (packs/ and packs/index.json in the zips), ARCHITECTURE, docs/history/0.2.0.md §25.16; build-i18n --dict (a community dictionary for a language without an official client); the browser check of a pack file and a pack folder dropped in while the server runs (test/ui/i18n.e2e.test.js); golden unchanged 2026-10-07 02:39:11 +08:00
sganggs 3e13bb4436 feedback5 CB1: item 43 follow-up — the two tests that pinned 凯瑟琳's old shop-level cap (bands.test.js, ui/feedback1-offers.test.js) now check shop items of any tier
No product change; golden unchanged.
2026-10-07 02:38:16 +08:00
sganggs ef10164773 feedback5 CB5: the boss round's prep shows both players of a pair together — the partner's pieces on the other half, as the battle places them — and frames the pair's two avatars in the official green (community report of 2026-10-06, item 51)
The own half was already the official one (the right-hand player on the right half with the
right_boss_*_camera_param camera, its layout mirrored col c -> 20 - c with RIGHT <-> LEFT, so its operators
face the leader as before: research 09 §1.2, DESIGN §15); the partner's half stood empty and nothing framed
the pair.
Server: in a boss round's prep m.private.bossMate { playerId, side, units } = the partner's board as UnitInfo
on its half (Match.bossMateView, finalAssault.js bossFieldPlacement); markPrivate marks the partner's
m.private with the mover's, so it follows live; a scout of either player (Match.prepFieldMeta) carries both
halves (`mate`), and its signature covers the partner's board.
Client: render/app.js syncMates draws the partner's units read-only ('m:<uid>' views: never picked, dragged
or swept with the own pieces) and the whole boss field is lit; the team panel frames the pair's avatars
(gameLogic teamFrameIds: the players still in by seat, two by two, as prepCamera — the boss field in the
fight) from the round start through the Final Assault / Hidden Core, with the official ui/battle
bg_team_border tinted the autochess green (#4ed8af; a plain ring without the local art); none for a lone
player, an eliminated player, a spectator seat, or 联防.
Overlap with PR #191 (same-field frames, issue #190; not merged): it published m.public bossPairing and
framed the partner alone in gold, 联防 included, plus a banner naming the partner; here the pairing is read
as the prep camera already does, both avatars get the official green frame, boss rounds only.
[ASSUMED] the partner's bench is not shown; one frame per avatar.

Golden unchanged. Tests: test/match/feedback5-boss-pair-prep.test.js (new, 4), the item 55 test now checks
the scouted player's own pieces (the partner's stand on the other half); SP_E2E
test/ui/feedback5-boss-pair.e2e.test.js (new, 2: both halves and the frames in prep and fight; item 55's
eliminated guest on the boss field with the leader). Docs: design/client.md §10 / §15, design/network.md,
PLAYING.md.
2026-10-07 02:29:49 +08:00
sganggs ebbbfe70ef feedback5 CB1: the result card no longer prints the hidden layers of 调和 / 协防干员 / 独行 / 绝技 — they stack, the game never shows them (community report 「独行这种盟约是不能叠层的(顺便排查下还有没有类似的)」, item 21)
Not a rule change: PRTS 卫戍协议:盟约 下半/PRTS盟约记录, its header, 「下述盟约中部分盟约不会显示叠加层数,但是叠加层数的特质/
策略/装备等效果仍然对其生效」 — the four bonds.json `noStack` bonds take layers from 助力 / 余 / 华法琳 / traits like every
other, and their effects do not read them (research 02 §2.2, 0.1.4 PR #66, GitHub #139's answer). PR #66 hid the count on
the bond strip and popup; the result card (screens/result.js) still showed 「独行 24」 and ranked its six bonds by it.
resultBonds: a noStack bond carries no number and no rank by its hidden count. No sim change; golden unchanged. Test:
test/ui/feedback5-result-bonds.test.js. Docs: PLAYING.md (层数), research 02 §2.2.
2026-10-07 02:01:12 +08:00
sganggs f2bec76176 feedback5 WAC: players rebind the in-match shortcuts in 设置 → 快捷键 (community request 「快捷键可不可以自己设置」, the owner's decision of 2026-10-07)
- The settings (sp.pref.settings) hold a key map, action → KeyboardEvent.code, for refresh / freeze / level-up /
  retreat / sell / ready (the ready key also pauses a solo battle); the defaults are the 0.1.4 keys R F D Q X Space,
  so nothing changes for a player who never opens the section. sanitizeSettings keeps each saved key that may be a
  shortcut and gives any other its default; not a map, or two actions on one key, gives the defaults.
- One lookup, actionForKey (the character typed first, then the physical key: the 0.1.4 code-or-key match, identical
  for the defaults), serves shortcutFor(e, keys) in game.js and the facing wheel; the modifier, text-field, repeat and
  overlay guards are unchanged. The shop bar, ready / pause and underframe key hints read the map.
- 设置 → 快捷键 (the one SettingsModal, in a match and on the title screen): each action with its key; click or Enter
  waits, the next key binds; Esc cancels (the dialog stays open), Tab leaves; Ctrl / Alt / ⌘ combinations and the
  interface keys (Esc, Tab, Enter, arrows, F1–F12) are refused; a key in use is swapped and the line under the list
  names both; 恢复默认. Esc stays fixed. Touch-only devices: a keyboard note and a folded list; one column under 768 px.
- Tests: test/ui/feedback5-hotkeys.test.js (map, sanitising, conflicts, lookup, capture, handlers and hints with a
  rebound key), test/ui/gameLogic.test.js (sanitizeSettings now returns keys), test/ui/hotkeys.e2e.test.js (SP_E2E=1:
  撤退 rebound to W with the keyboard only in the mock match; phone width).

Golden unchanged (client UI only).
2026-10-07 01:56:19 +08:00
sganggs 02353e13f8 feedback5 CB5: a module's added trait line (official additionalDescription, PRTS 特性追加) shows after the class trait, not instead of it — the 干员调配 module card (特性 + 特性追加 rows), its 局内数值, the detail card and the 自选 module row (community report of 2026-10-06, item 16.2: 114 of the 164 modules the screen offers showed the extra line alone)
Text only: data/chess.json keeps both lines (traitOverride.desc = the class trait or the module's rewrite,
moduleDesc = the added line) and the battle unit keeps the class trait with the module on — the same trait
text, targeting and attack shape as 不装备 for every added-line module (圣约送葬人 REA-Y also rewrites it).
New helpers ui/loadoutModel.js traitLines / fullTraitText; traitText (ui/detailPanel.js) composes the two.
Golden unchanged (no sim change). Tests: test/ui/feedback5-module-trait.test.js (new), loadout-stats updated.
2026-10-07 01:33:21 +08:00
sganggs 30d9a41c51 feedback5 WAB: the collapsible bond strip test waits for the mock's answer to the teammate tap before it starts the battle (test/ui/bond-collapse.e2e.test.js; flaky 1 in 4 since 0.1.4, no product change)
.gm__watching shows at the tap, but the dev mock answers the row's g.watch 40-100 ms later (game-mock.js mockRequest). The test started the battle about 50 ms after the tap. When the answer came after that, the mock took the scouting request as a battle watch, as a server-run server does, and moved m.field and the battle frames to the teammate's field. The strip follows the field on screen, so it rightly showed that teammate, who has no bonds in the mock, and `.bslot .bond` never came (3 of 10 runs at 1920x1080). Each 640x360 touch tap takes about 100 ms, so the answer always came first there. A player never loses their own strip this way: client-side combat (the default) refuses a teammate's field while the own battle runs. Under server-run combat the late tap shows that teammate's battle with their real bonds and the eye tag.

Now the test waits until m.field is the teammate's read-only board before the phase change. It also asserts that the own battle's expanded strip is the player's own (no data-owner). Every other check is unchanged. 10 of 10 runs pass. Golden unchanged.
2026-10-07 00:05:21 +08:00
sganggs b9b647eecf feedback5: merge W5Z — the co-op briefing's top bar fits the third tab (自选编队) in zh and en at every width (the only product bug of the 0.2.0 full pass); three tests hardened against load (client-wait counts calls, leftovers waits for the item to fly home, bossfield checks 瘫痪 on copies with a golden 催泪瓦斯); golden unchanged 2026-10-06 22:29:39 +08:00
sganggs 24578cf2cb feedback5 WZ: the real-server equip-replace check waits until 取消 has drawn the item back on its bench slot before it drops it again (test/ui/leftovers.e2e.test.js)
The 0.2.0 candidate's full pass failed at the second drop ('Waiting for selector `.eqr .eqr__opt` failed'). The view
keeps a dropped piece on the drop tile for 1.3 s (render/app/tune.js DROP_PENDING_MS, unchanged since the first import)
and then flies it back in a frame-timed tween. The test picked the item up again 0.5 s after 取消, where it had been
drawn a moment before. Even alone the first try often grabbed nothing (the helper's retry hid it). With few frames
(software-rendered headless Chrome on a loaded machine; reproduced with frames ~600 ms apart) the press grabbed the
operator under the item, the operator dropped on its own tile opened the direction wheel, and the wheel swallowed the
retry. The dialog itself opens and renders as before (补位 / i18n / 自选 not involved).

Now 取消 is also checked to draw the item back on its bench slot, and the next drop waits for that; under the same slow
frames the drop succeeds at the first try. Alone: 5/5 twice; mock.e2e 32/32, devices.e2e 26/26. A test timing flake,
no product change; golden unchanged.
2026-10-06 22:00:26 +08:00
sganggs ba4f5e195a feedback5 WZ: the 干员调配 overlay's top bar keeps its right column clear of the tabs (public/css/screens/loadout.css)
0.2.0's third tab 自选编队 (WK 0def7ba, merged in d6fd800e — the toolbar check passes on its parent) widened the centre
column of the symmetric grid, and the co-op briefing's countdown + 导出 / 导入 / 全部恢复默认 no longer fit in their half:
they ran 4 px into the tabs at 667×375 (loadout.e2e's toolbar check), 63 px at 844×390 (the countdown on 自选编队) and,
in English, at every size up to 1920×1080. Not the briefing's 本局禁用干员 lines: the overlap is the overlay's .lo-top.

- the grid centres the tabs while both sides fit in equal halves (minmax(max-content, 1fr) minmax(0, auto)
  minmax(max-content, 1fr)) and shifts them otherwise; the tab strip scrolls only as a last resort, never overlaps;
- the countdown caption breaks at its spaces (Loadout / Deadline; 调配截止 stays one line);
- tighter tabs at ≤ 1200 px; at ≤ 1100 px the briefing's countdown takes the place of the 已调整 / 同步 chips;
- phones in landscape (≤ 1000 px, up to the 932 px ones): no chips, and 返回 / 全部恢复默认 (全部持有, 全部清空 — each
  confirms first) show their icon, the text kept as their accessible name. Desktop zh looks as before.

Checked: zh and en × briefing and lobby × the three tabs × 18 sizes from 667×375 to 2560×1440 — no overflow, overlap
or offscreen button, the tabs never scroll. The e2e toolbar check now runs zh and en at 667×375 and 844×390 (and en at
1920×1080) and asserts the three tabs show in full; it fails on the old CSS. Golden unchanged.
2026-10-06 21:31:57 +08:00
sganggs 269d7b0bcf feedback5 WY: 华法琳's granted trait stops at the data's 7 / 14 layers per battle, not 12 / 24 (GitHub #175; from PR #192 by @kukiC; the owner's decision of 2026-10-06)
PRTS 卫戍协议:盟约 下半, 3月27日更新#2调整 (revision 439189): 「[Ⅳ阶]华法琳:赋予的特质的叠层上限从 初始12/精锐24
降低至 初始7/精锐14」 — a nerf; the current 参战干员 record says 至多7层 / 14层 and the client blackboard is 7 / 14.
Research 02 Addendum 1 had read the line backwards, so the remake overrode garrison_95's cap to 12 / 24.

Ported from PR #192 by @kukiC: GRANTED_CAP_OVERRIDE is removed from server/sim/content/garrisons/battle.js and from
the client-result layer bound (server/match/fields.js layerAllowanceOf); a granted trait keeps its data cap like a
native one. Tests from the PR: garrisons_battle (updated on purpose: the 7th cast reaches 7 / 14, the 8th adds
nothing), feedback1-gaps (the bound with 华法琳 + 古米), playtest3 (both cards show 7 / 14); the first two fail without
the fix. Docs: research 00 / 01 / 02 corrected (02-bonds.json's note; data/bonds.json regenerated offline — only
miraShip.spec.layerGain changed), history 0.1.1's pointer, DESIGN §25.13.2 and the §25.10 table.
Golden unchanged (no corpus battle casts 8 times in front of 华法琳).
2026-10-06 21:03:56 +08:00
sganggs 3ac88dea43 feedback5: merge W5S — i18n phase 2: every player-facing Chinese literal in the client, shared and server goes through t() / tParts() / N_() (1 128 → 0; en.json 1 015 entries), server-sent data texts show the English record, a language switch refreshes cached names; test/i18n.test.js enforces extract 0 / check --strict over the whole tree; Chinese output unchanged (per-file render proof); golden unchanged 2026-10-06 18:39:35 +08:00
sganggs 647bd23ef2 feedback5 W5S: i18n phase 2, ui — HUD, shop bar, detail card, bond strip and popup, 机变 overlay, enemy drawer, team panel, under-frame, guide, emotes, match chrome, connection banner, components, the loadout / roster models, the store, net and observe texts through t() / tParts() (whole sentences where a number or a name sat inside), msgid tables marked N_() and translated where shown, the shop's refusal reasons compared through t(); tools/i18n.mjs reads aliased imports (t as tr); English for the 59 new msgids; Chinese output unchanged (zh render proof; five rewrites checked by hand, listed in the notes) 2026-10-06 18:09:50 +08:00
sganggs c50b406874 feedback5: merge feedback4 (9f93096) — W4C ported: the board canvas is a click target, so a phone tap on the back row stays on the tile (PR #149's fold button had pushed the bond discs over row 12); browser suites poll long waits in slices under the 90 s protocol timeout; golden unchanged 2026-10-06 17:53:19 +08:00
sganggs 166c4fbbfb feedback5 W5S: i18n phase 2, screens — band draft, briefing, game screen and its overlays, 干员调配 / 干员持有, result through t() / tParts() (sentences with a number or a <strong> inside stay whole), English for the 58 new msgids; Chinese output unchanged (zh render proof: only ownership's 阶 heading is a text node instead of a <span>, same text and layout) 2026-10-06 17:52:19 +08:00