Playtest #3 fixes: lost models, picking, live LP, temp overflow, 蕾缪安 S3, 回环射手, FX, detail card, spinners

- #1 lost operator models: the Spine store no longer hands out a skeleton whose unload is in flight
  (PIXI.Assets.unload is async); eviction runs 1 s after a release and the empty-scene budget only after 3 s
- #7 picking: one shared rule (render/pick.js, pixel probe where bodies overlap) for prep/battle clicks,
  hover, the enemy pen and equipment drops; the drag ghost stands on its drop tile; hover probes capped
  at one per frame
- #2 the top-bar LP drops live as the own battle's enemies enter the blue gate (−N, 联防中), teammates'
  pending LP in the team panel
- #3 temp overflow pieces that arrive after a prep deadline survive through the next prep (tempDue);
  temp-row warning and ready-button reason
- #4 蕾缪安 S3: locks every 0.5 s, then one shell every 0.3 s in lock order (PRTS notes), bombardShell fx
- #5 回环射手: real boomerang (out 15, back 3.75 tiles/s, attack only when every boomerang is caught)
- #6 visible projectiles, muzzle flashes, slashes, skill bursts and auras at unchanged frame cost
- #8 特质 right under the detail card header, with its official type icon
- #9 no spinning busy cursors / spinners next to texts; game data warmed before the match
- docs, recordings and tests updated (2414 unit tests, all browser suites green)
This commit is contained in:
sganggs committed 2026-09-29 23:42:55 +08:00
1 parent 01a44cd288
commit 0aba602ecf
72 files changed
+5132 -571

No files matched your search

+22 -7
View File
@@ -18,6 +18,7 @@ import { GEO, PHASE, UF } from '../../../shared/constants.js';
import { resolveLoadout, loadoutOptions, MODULE_NONE } from '../../../shared/protocol.js';
import { resolveRecordLoadout, loadoutRecord } from '../../../shared/loadoutRecord.js';
import { layoutPen } from '../render/pen.js';
import { pickBody } from '../render/pick.js';
import { bossLevelSeconds } from './matchStatus.js';
// ---- small helpers -------------------------------------------------------------------------------
@@ -871,17 +872,31 @@ export function dropIntent(ctx, uid, target) {
}
/**
* The front-most piece whose screen rect contains (x, y): the one standing nearest the camera (largest bottom).
* @param {Array<{uid:number, left:number, top:number, right:number, bottom:number}|null>} rects
* The piece under a screen point (user playtest #3 item 7). Rects of the render engine carry the drawn body
* (`shape`, view.pieceScreenRect) and are picked by the shared rule of render/pick.js — the face / torso of a unit is
* its own; elsewhere the piece standing on the tile under the pointer (`tile`, same space as the rects' `tile`) wins.
* Plain rects (the DOM fallback) go to the one whose centre is nearest relative to its size (a unit standing in front
* no longer takes the whole overlap), ties → the front-most (largest bottom).
* @param {Array<{uid:number, left:number, top:number, right:number, bottom:number, shape?:object, tile?:{row:number,col:number}}|null>} rects
* @param {number} x
* @param {number} y
* @param {{row:number, col:number}|null} [tile] the tile under the point
* @returns {number|null} uid
*/
export function pickPieceAt(rects, x, y) {
export function pickPieceAt(rects, x, y, tile = null) {
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
let best = null;
for (const r of Array.isArray(rects) ? rects : []) {
if (!isObj(r) || !Number.isInteger(r.uid)) continue;
const list = (Array.isArray(rects) ? rects : []).filter((r) => isObj(r) && Number.isInteger(r.uid));
const shaped = list.filter((r) => isObj(r.shape) && r.shape.s > 0);
if (shaped.length && shaped.length === list.length) {
const hit = pickBody(shaped.map((r) => ({ ...r.shape, tile: r.tile || null, uid: r.uid })), x, y, tile);
return hit ? hit.uid : null;
}
let best = null, bestD = Infinity;
for (const r of list) {
if (x < r.left || x > r.right || y < r.top || y > r.bottom) continue;
if (!best || r.bottom > best.bottom) best = r;
const hw = Math.max(1, (r.right - r.left) / 2), hh = Math.max(1, (r.bottom - r.top) / 2);
const d = Math.hypot((x - (r.left + r.right) / 2) / hw, (y - (r.top + r.bottom) / 2) / hh);
if (d < bestD - 1e-9 || (Math.abs(d - bestD) <= 1e-9 && r.bottom > best.bottom)) { best = r; bestD = d; }
}
return best ? best.uid : null;
}