Files
ark-stronghold-protocol/docs/history/v2.2-playtest3.md
T

14 KiB
Raw Blame History

DESIGN §17 — User playtest #3 (v2.2)

Part of DESIGN.md (the index; section numbers are global).

17. User playtest #3 (v2.2): models, picking, FX, rules, HUD

Nine reports from a real co-op session. Where each is handled: #1 lost operator models → §17.1; #2 the LP did not move while enemies entered the own blue gate → §17.5; #3 equipment pushed into the second bench row vanished after the battle → §6.2 temp overflow (server) + §17.5 (UI); #4 蕾缪安 S3 fired every shell at once → §17.4; #5 回环射手 attacked like a fast shooter → §17.4; #6 bullets / skill effects hardly visible → §17.3; #7 a click selected the unit one row lower (equipment too) → §17.2 (superseded by §18.1); #8 the operator's own effect (特质) sat at the bottom of the detail card → §17.5; #9 spinners next to texts → §17.5.

17.1 Lost operator models (#1) — the Spine store (public/js/assets.js RefLru)

Cause: PIXI.Assets.unload is asynchronous (the loader cache entry is dropped and the textures destroyed a microtask later), so a skeleton evicted and re-acquired in the same task came back as the doomed object and turned invisible for good (a tier chip without a body; the dead data stayed cached, so every later copy of that operator was invisible too). It happened at every battle → prep switch: enterPrepMode → clearViews made the reference count 0 for an instant, the LRU applied its "no scene on screen" budget (0 bytes) at that instant and dropped the bench models idle since the battle started, then setPrep took them back at once. Rules now: an unload may return a promise and a load of a key whose unload is still in flight waits for it (unloadSpineData returns one; stats().spine.unloading); the spine store runs eviction in one pass SPINE_EVICT_DELAY_MS (1 s) after a release (a scene switch takes its models back first); the quiet budget applies only after SPINE_QUIET_DELAY_MS (3 s) with nothing referenced (lobby / room) and no scene holding the cache (a field view in battle mode holds it, §21.27). The count-cap path (60 skeletons) waits the same way. Tests: test/render/assets.test.js (fake PIXI.Assets with the same async unload), test/render/models.browser.test.js (models alive after long battles, drag / wheel stress).

17.2 Picking and the drag target (#7) — superseded by §18.1

v2.2 answered "a click selected the unit one row lower" with body picking: every path took the drawn chibi (a head / body shape per unit, measured from its posed Spine model) and, where two bodies were close, a 1-px render probe of the pixel under the pointer; the dragged ghost stood on the pointer (on touch 0.6 tile above the finger) and an item went to the operator whose sprite was under the plate. User playtest #4 item 1 rejected it ("感觉你想复杂了,地上都画好了一个一个方格,点击对应方格就选中那个方格的人物就行"; the model moved up away from the finger): picking is by the tile under the pointer and the dragged model is held under the pointer (§18.1). The shapes, the probe, the touch lift, pieceDragOver and the DOM-side pickPieceAt / equipRetarget are gone.

17.3 Battle FX (#6) — render/fx.js, style.js PROJ, textures.js

  • Projectiles per kind (PROJ[kind].look), sized in tiles × the camera's px per tile (the same on the 3D board and the 2D fallback, 1280×720 to 1920×1080): arrow a bright tracer with a glowing head, a muzzle flash aimed at the target and a hit flare; bolt / orb / enemy a glowing ball with a particle trail (arts purple with a burst + shockwave, heal green with a sparkle, enemy red); bomb / lob a shell on a visible arc with a ground shadow, smoke and embers, exploding on landing; 阵法术师 / 轰击术师 strikes are instant beam lines to every victim (§21.18); droneBomb (暴鸰) a falling shell at 5 tiles/s (§21.14); drone a small cyan dart; boomerang (回环射手) a spinning boomerang with afterimages that flies out to the moving target at PROJECTILE_SPEEDS.boomerang and back to the thrower's current position at BOOMERANG_RETURN_SPEED (the sim emits no event for the return: the renderer models it; dropped when the thrower's view is gone or dead). A shot lingers 0.08 s in its target so fast shots still read. Flight times use the sim's own speeds, loaded at run time from /sim/constants.js (style.js PROJ keeps a test-guarded copy: test/render/fxproj.test.js fails when they differ), so a shot lands with its damage number.
  • Hits and skills: a melee blow draws a thick crescent slash swept along the blow (attacker → victim on screen) in the damage-type colour with a white core, plus sparks; skill activation = gold light pillar with a white-hot core, flash, shockwave and hex ring on the ground, rising motes; an active skill keeps a slowly turning hex and a soft glow under the unit. Blasts share one explosion (fireball, flash, shockwave, coloured ring, sparks, smoke; heavy kinds — bombard, airstrike, meltdown — add debris and a scorch mark). Beams and chain lightning: glow + body + white core, flare at the target.
  • Anchoring: an fx whose extra.id names a unit is drawn on that unit (following its rendered position) only when the event's (x, y) lies within 0.75 tile of it (or has no position); otherwise at (x, y) — the sim puts the caster in id of many area effects. FX_KINDS entries flagged pt (bombard, bombardShell) always happen at (x, y) (their id is the shooter; 蕾缪安's bombard used to explode on herself).
  • 蕾缪安 S3 contract: 'lock' {x, y, id: enemy, src: shooter} → a reticle following that enemy (its view by id), at the spot it left once it is gone; 'bombardShell' {x, y, id: shooter, r, t: flight game s, i} → a launch streak leaving the shooter and a shell dropping onto (x, y) after t (a flight ≥ 0.45 real s climbs out of her first), with a closing warning ring and the blast radius throbbing; it takes the shooter's nearest free lock; 'bombard' {x, y, id: shooter, r} at the same spot → the heavy explosion and the end of that lock. A lock also ends on the S2 'crit' from the same shooter, when the shooter is gone (locks no shell took yet), or LOCK_T = 5 game s after that shooter's last lock / shell / bombard (an idle timeout, not an age: ammo grants and stuns stretch an S3 far past 5 s); ≤ 48 reticles.
  • Cost: particle, projectile and lock records are pooled (the per-frame emitters allocate nothing); a freed particle is a zero-size transparent quad (Pixi's ParticleRenderer ignores visible); cosmetic extras (trails, muzzle flashes, afterimages, debris, scorch marks, motes) are off at quality 'low' and at adaptive load level ≥ 2, and trails stop at 75 % of the particle cap. Measured: the 120-unit stress scene and the crowded boss-m02 replay stay at 60 fps with no measurable per-frame increase. The FX atlas is 1024×512 with non-overlapping frames (the old light pillar carried two status icons at its foot). The FX are procedural: the local client's per-weapon projectile sprites are not used (the sim's arrow also covers gun snipers, and friends may not have the local art).

17.4 回环射手 (#5) and 蕾缪安 S3 (#4) — the sim (official PRTS notes; numbers from the data)

  • 回环射手 boomerang (professions.js loopshooter, ai.js throwBoomerang; PRTS 跃跃 "投射物飞行速度15,返回时飞行速度3.75", "必须回收全部回旋投掷物才可以进行下一次攻击"): projectile 'boomerang' (resolveProfile keeps it over the data's generic 'arrow') flies out at PROJECTILE_SPEEDS.boomerang = 15 tiles/s and hits on arrival, then flies back to the thrower's current position at BOOMERANG_RETURN_SPEED = 3.75 tiles/s without damage; the thrower attacks only while it holds its boomerang (every one it threw caught) and its attack cooldown is ready, so the real interval is the longer of the two. A target that died mid-flight is not hit (the boomerang still flies to its last position and back); a thrower knocked out / withdrawn loses it and a redeployed one holds a fresh one. 跃跃 S2's extra boomerangs share the one flight (cnt hits, the talent rolls per hit). Event contract unchanged: ['atk', thrower, target, 'boomerang'].
  • 蕾缪安 S3 礼炮·强制追思 (kits/tier6.js lemuen; PRTS S3 note — the timing is not in the blackboard): one lock every 0.5 s while an enemy is in range (unlocked targets first, then lowest DEF), each costing a round; the lock mark follows its enemy and stays where it left (death / leak). After the skill ends ONE shell every 0.3 s in lock order (PRTS "以0.3s为间隔"), aimed at a random point of the 0.4-side square around its mark (± attack@emit_offset 0.2, drawn with the battle rng in her facing frame so a turned board plays alike), landing 0.3 s after it is fired (flight [ASSUMED]: the data and PRTS give none; the PRTS count of ≤ 33 fits a first impact one interval after the end); each shell hits every enemy within dist_2 once — proj_atk_scale_1 within dist_1, else proj_atk_scale_2 — with the ATK cached when the skill ended; at most 10 s of shells (≤ 33). Knocked out / withdrawn after the end: shells in the air still land, the rest are dropped; knocked out while locking: no bombardment. fx: 'lock' per lock, 'bombardShell' when a shell is fired, 'bombard' at its impact (§17.3).
  • Tests: test/sim/professions.test.js, test/content/kits_t1t2.test.js (跃跃), test/content/kits_t6.test.js (shell sequence, spacing, cap, knock-out, rotation invariance).

17.5 Game HUD (#2, #3, #8, #9) — ui/hud.js, teamPanel.js, underframe.js, detailPanel.js, data.js

  • Live LP (#2): in a normal round the top bar's LP tower and the own team row show lp − min(lpCapPerRound, counted leaks) as soon as the own battle's enemies enter the blue gate — the number in red followed by a −N plate that pops again on every leak, clamped at 0, with a tooltip (ui/hud.js liveLp / pendingLoss / pendingTip). The count is ownLeaks(local, server) = the larger of the local runner's state().leaks[own field] (§14) and m.public players[].pendingLp (§8.2; the authority's b.progress leaks, the field's result, the recorded result in 联防) — both only grow within a round, so a paused display replica never freezes the number. The pending part is dropped as soon as the settled m.private lands (its lp or stats.leaks changed; Match.flush sends it before the SETTLE m.public), the phase leaves COMBAT / 联防 or the round changes — never subtracted twice. 联防 (revised by §20.6, user playtest #6 item 7): a leaker's count is its enemies still standing — the local replica's while the 联防 field is on screen, else m.public uniteLeft — shown uncapped as ×N in the capsule and in the leakers' team rows, and the LP tower shows lp − min(10, N), tagged 联防中. Teammates' rows use their pendingLp (teamPanel.js rowLp); server-run fields (bots, taken-over fields) report none before their result is released. Boss rounds are unchanged (the team LP moves live through b.pool).
  • Temp row (#3, UI): while m.private temp holds pieces during the own prep, a dashed red frame surrounds the 临时整备区 row on the board and a label says how many are waiting, what to do and when they go (underframe.js TempRowNotice / tempRowFrame, placed through view.tileScreen — it follows the camera, the mirrored Final Assault prep and the DOM fallback; no pointer events; the label hides while dragging or placing). Its wording follows the server rule (§6.2, tempRowRule(ready)): not ready — "放入整备区或战场、配发或使用后才能准备;休整期结束时仍在此处的将被销毁"; ready in PREP (so whatever lies there arrived after Ready) — "已准备就绪后进入的单位保留到下个休整期,届时仍在此处的将被销毁(取消准备则在本休整期结束时销毁)". The 准备就绪 button shows "临时整备区 N 个单位待处理" under it (not only on hover; the effects column moves down a line meanwhile, .gm.has-readywhy) and Space toasts the reason (hud.js TEMP_RULE / tempReadyReason).
  • Detail card order (#8) (detailPanel.js CHESS_SECTIONS): header (portrait, name, tier, class, bonds) → 特质 (the operator's own effect — garrison trigger chip such as 休整期结束时 + description, a compact gold block, visible without scrolling at 1920×1080, 1280×720, iPhone 14 and 640×360) → 特性 (class trait) → stats (a compact 4×2 grid) + attack range → 技能 → 模组 → 装备 → 天赋. On phones ≤ 460 px tall the portrait is shorter and the romanised name is hidden. A teammate's 策略 is not on the card — while scouting, the 本局信息 drawer shows the watched player's band in place of one's own (§14).
  • Busy indicators and texts (#9): every text of the game (operators, skills, bonds, items, enemies, 特质 …) is static data, /data/*.json loaded once per page and never fetched during a match; the "spinners" the player saw were busy indicators of requests in flight next to texts — and the 特质 chip's type icon: it was picked by the trigger, so every <休整期开始时 / 结束时> 特质 showed the spoked 特异化 glyph (it reads like a loading spinner) before its text; the chip now shows the garrison's own official eventTypeIcon (detailPanel.js garrisonTypeIconKey: crossed swords 作战能力, gold 整备能力, chain link 持续 / 单次叠加, spokes only for 特异化). Now: no cursor: wait / progress anywhere; a loading Button keeps its icon and shows a thin sweeping bar along its bottom edge after 250 ms (no spinner ring); a 机变 card in flight shows a 选择中 strip until the pick appears in m.public (choiceOverlay.js pickBusy); the 作战结束,等待队友完成作战 hourglass is still; the hexagon Spinner is kept for genuine loading screens only (entering the match, the field view, connecting, guide images, the first 干员调配 load, which says the data loads once per page). main.js warmGameData downloads every in-match file (gameComponents GAME_FILES) in the background once the player is in a room, and the match screen waits for them; data.js retries a transient failure (network error, HTTP 5xx / 408 / 429) after 600 ms and 2 s (RETRY_DELAYS_MS) — a 404 or invalid JSON is final.
  • Tests: test/ui/playtest3.test.js, test/match/runner.test.js (live leak counts), test/match/temp.test.js (§6.2), test/docs-consistency.test.js.