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:
1 parent
01a44cd288
commit
0aba602ecf
72 files changed
+5132
-571
No files matched your search
@@ -195,6 +195,86 @@ describe('field view teardown (app.js releaseGl)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// user playtest #3 item 7: the body as drawn, for render/pick.js (the shared picking rule)
|
||||
describe('pickShape / bounds / hitTest', () => {
|
||||
test('the avatar diamond until the Spine model shows, then the chibi at its own measured height', async () => {
|
||||
const v = view({ defId: 'char_p1', avatar: 'char_p1' }, { prep: true }, store({ spine: true, imageDelay: 50 }));
|
||||
v.update(1 / 60, cam(), 0);
|
||||
assert.equal(v.pickShape().kind, 'diamond', 'no model yet');
|
||||
await tick(); await tick();
|
||||
assert.ok(v.spineReady);
|
||||
// the posed skeleton's bounds: 425.6 units above the feet = 1.33 tiles (UNIT.modelScale 1/320)
|
||||
v.actor.spine.getLocalBounds = () => ({ x: -60, y: -425.6, width: 120, height: 430 });
|
||||
v.update(1 / 60, cam(), 1 / 60);
|
||||
const b = v.pickShape();
|
||||
assert.equal(b.kind, 'chibi');
|
||||
assert.ok(Math.abs(b.h - 1.33) < 1e-6, `measured height ${b.h}`);
|
||||
assert.equal(b.w, 1, 'operators: the measured width profile');
|
||||
assert.equal(b.x, v.screen.x); assert.equal(b.y, v.screen.y); assert.equal(b.s, v.screen.s);
|
||||
assert.equal(b.depth, v.root.zIndex);
|
||||
// measured once: a later pose (a raised weapon) does not move the outline
|
||||
v.actor.spine.getLocalBounds = () => ({ x: -60, y: -700, width: 120, height: 700 });
|
||||
v.update(1 / 60, cam(), 2 / 60);
|
||||
assert.ok(Math.abs(v.pickShape().h - 1.33) < 1e-6);
|
||||
// the outline tops out at the model; the tier chip (prep) is part of the pick shape, not of bounds()
|
||||
const r = v.bounds();
|
||||
assert.ok(Math.abs(r.y - (v.screen.y - 1.33 * v.screen.s)) < 1, 'bounds top = model top');
|
||||
assert.ok(Math.abs(r.y + r.height - (v.screen.y + 0.08 * v.screen.s)) < 1, 'bounds bottom = just below the feet');
|
||||
assert.ok(b.hud && b.hud.y1 < v.screen.y - v.screen.s, 'the chip is up at the head (UNIT.headroom)');
|
||||
assert.ok(v.hitTest(v.screen.x, v.screen.y - 0.5 * v.screen.s), 'the torso');
|
||||
assert.ok(!v.hitTest(v.screen.x + 0.6 * v.screen.s, v.screen.y - 0.5 * v.screen.s), 'beside it');
|
||||
// a new model (Front ⇄ Back swap, _dropActor) is measured again
|
||||
v._dropActor();
|
||||
assert.equal(v._bodyH, null);
|
||||
});
|
||||
|
||||
test('implausible skeleton bounds are clamped; enemies scale their width with their size', async () => {
|
||||
const op = view({ defId: 'char_p2', avatar: 'char_p2' }, { prep: true }, store({ spine: true }));
|
||||
await tick(); await tick();
|
||||
op.actor.spine.getLocalBounds = () => ({ x: -500, y: -1200, width: 1000, height: 1200 }); // an effect in the idle pose
|
||||
op.update(1 / 60, cam(), 0);
|
||||
assert.equal(op.pickShape().h, 1.55, 'operators ≤ 1.55 tiles');
|
||||
const foe = view({ side: 'enemy', kind: 'enemy', defId: 'enemy_big' }, {}, store({ spine: true }));
|
||||
await tick(); await tick();
|
||||
foe.actor.spine.getLocalBounds = () => ({ x: -300, y: -640, width: 600, height: 640 });
|
||||
foe.update(1 / 60, cam(), 0);
|
||||
const b = foe.pickShape();
|
||||
assert.ok(Math.abs(b.h - 2) < 1e-6);
|
||||
assert.ok(Math.abs(b.w - 2 / 1.2) < 1e-6, 'width factor from the height');
|
||||
assert.equal(b.hud, null, 'an undamaged enemy shows no bar');
|
||||
});
|
||||
|
||||
test('a battle unit\'s bars are its HUD rect; a dead unit is not hit', async () => {
|
||||
const v = view({ defId: 'char_p3', avatar: 'char_p3' }, {}, store({ spine: true }));
|
||||
await tick(); await tick();
|
||||
v.sync({ x: 5, y: 12, hp: 500, maxHp: 1000, sp: 3, spMax: 10, flags: 0, anim: 0, vx: 0 });
|
||||
v.update(1 / 60, cam(), 0);
|
||||
const hud = v.pickShape().hud;
|
||||
assert.ok(hud && hud.x1 > hud.x0 && hud.y1 > hud.y0, 'HP / SP bars and chip');
|
||||
assert.ok(hud.y1 < v.screen.y - v.screen.s, 'above the head');
|
||||
v.die();
|
||||
assert.equal(v.hitTest(v.screen.x, v.screen.y - 0.5 * v.screen.s), false);
|
||||
});
|
||||
|
||||
test('item plates: floating above the slot; centred on the pointer while dragged (lifted)', async () => {
|
||||
const { ItemView } = await import('../../public/js/render/units.js');
|
||||
const ctx = fakeViewCtx(fake.P, { assets: store(), cam });
|
||||
const it = new ItemView(ctx, { id: 'p:9', uid: 9, defId: 'item_x', x: 3, y: 7 });
|
||||
it.setWorld(3, 7, 0.16);
|
||||
it.update(1 / 60, cam(), 0);
|
||||
let b = it.pickShape();
|
||||
assert.equal(b.kind, 'plate');
|
||||
assert.ok(Math.abs(b.y - (it.screen.y - 0.31 * it.screen.s)) < 1e-6, 'resting: the plate above its anchor');
|
||||
it.lift = 0.3;
|
||||
it.update(1 / 60, cam(), 0);
|
||||
b = it.pickShape();
|
||||
const g = cam().project(3, 7, 0.16);
|
||||
assert.ok(Math.abs(b.x - g.x) < 1e-6 && Math.abs(b.y - g.y) < 1e-6, 'dragged: centred on its ground point (the pointer)');
|
||||
assert.equal(it.plate.anchor.y, 0.5);
|
||||
assert.ok(it.hitTest(g.x, g.y));
|
||||
});
|
||||
});
|
||||
|
||||
describe('enemy preview pen figures (lod idle)', () => {
|
||||
/** A UnitView of a pen figure with a Spine model, an impostor atlas (full or not) and a counting renderer. */
|
||||
async function penFigure(full) {
|
||||
|
||||
Reference in new issue
Block a user