feedback1 WD: late views keep the form in the client, 重生 clears statuses, blocked 隐匿 drawn solid

- render/app.js: the views' unit info is built by the exported renderInfo(UnitInfo), which now keeps `form`;
  addInfo dropped it, so a view built from fieldMeta (a teammate's field watched later, 联防 observers, a
  reconnect) still drew the first form. Tested through renderInfo (fake PIXI) and the real enterBattle(meta)
  path in headless Chrome (opt-in).
- content/enemies.js: rebirthCleanse at every 重生 (reborn, husk, statue): operator-applied buffs and
  source-less statuses go, own talents / enemy-given buffs / field state stay (PRTS 特殊机制 §重生). A 逐火
  knocked out while feared is no longer an unblockable ember.
- snapshot.js: an enemy's stealth bit only while its 隐匿 is on (not blocked, not revealed).
- Docs: area damage reaching an unblocked 隐匿 enemy is documented as an engine deviation (PRTS 作战机制
  §AOE伤害判定), not as the rule.
This commit is contained in:
sganggs committed 2026-10-03 03:44:17 +08:00
1 parent 5c23eeb804
commit e1ce0316dd
10 files changed
+274 -49

No files matched your search

+78
View File
@@ -492,3 +492,81 @@ test('every enemy starts with the same profile fields and `moving` / `form` (one
}
assert.equal(shapes.size, 1, 'one key order for every enemy');
});
// ---------------------------------------------------------------------------------------------------------------
// second review: a 重生 clears the statuses; a blocked 隐匿 enemy is drawn solid
describe('a 重生 clears what operators put on the enemy (PRTS 特殊机制 §重生 "清空自身身上除白名单外所有Buff")', () => {
test('a 深池逐火战士 knocked out while feared and slowed (叙拉古-style 恐惧 makes it unblockable) is an ember without them: blocked again, beaten by the gun — its own talents stay', () => {
const h = arena({ units: [{ chessId: 't_gun', row: 10, col: 4 }, { chessId: 't_wall', row: 9, col: 7 }] });
h.step();
const e = put(h, 'enemy_1288_duskls', { mods: { speedMul: 1 } });
const gun = h.unit('t_gun');
assert.ok(h.runUntil(() => !!e.blockedBy, 20), 'blocked by the wall');
assert.ok(h.b.applyStatus(e, 'fear', { duration: 30, source: gun }), 'feared');
assert.ok(h.b.applyStatus(e, 'slow', { duration: 30, source: gun, value: 0.5 }), 'slowed');
h.b.addBuff(e, { key: 'test:opDebuff', duration: 30, mods: { defFlat: -50 }, source: gun }); // a skill's debuff
assert.ok(e.s.flags.fear && e.s.flags.unblockable && !e.blockedBy, 'the fear releases it');
h.b.kill(e, gun);
assert.ok(e.alive && e.form === 'husk', 'knocked out: its 重生');
for (const k of ['fear', 'slow', 'test:opDebuff']) assert.equal(e.findBuff(k), null, `${k} cleared by the 重生`);
assert.ok(e.findBuff('ab:ember') && e.findBuff('ab:hitCount'), 'the ember\'s own buffs');
assert.ok(h.eventsOf('status').some((ev) => ev[1] === e.id && ev[2] === 'fear' && ev[3] === 0), 'the fear icon goes');
h.run(HUSK_REBIRTH + 0.1);
assert.ok(!e.s.flags.fear && !e.s.flags.unblockable && e.s.flags.stealth, 'a 隐匿 ember that can be blocked');
assert.ok(h.runUntil(() => !e.alive, 8), 'blocked, it is beaten before it could stand up');
assert.equal(e.removeReason, 'killed');
assert.ok(h.b.time < 2 + HUSK_REBIRTH + tb('enemy_1288_duskls', 'Revive[Trigger].interval') + 10);
});
test('锏\'s 重生 clears an operator\'s slow and 脆弱 but keeps its 抵抗 (a self-applied talent status); 守墓石像\'s instant 重生 too', () => {
const h = arena({ units: [{ chessId: 't_gun', row: 10, col: 4 }], kits: QUIET_GUNS });
h.step();
const still = { mods: { speedMul: 0 } };
const mace = put(h, 'enemy_1525_blkswb', { pos: [10, 7], ...still });
const stone = put(h, 'enemy_1172_dugago', { pos: [11, 7], ...still });
const gun = h.unit('t_gun');
assert.ok(mace.findBuff('resist'), '锏 has its 抵抗');
for (const u of [mace, stone]) {
assert.ok(h.b.applyStatus(u, 'slow', { duration: 30, source: gun, value: 0.5 }));
assert.ok(h.b.applyStatus(u, 'fragile', { duration: 30, source: gun, value: 0.3 }));
h.b.kill(u, gun);
assert.ok(u.alive, 'a 重生, not a death');
assert.equal(u.findBuff('slow'), null, `${u.defId}: slow cleared`);
assert.equal(u.findBuff('fragile'), null, `${u.defId}: 脆弱 cleared`);
}
assert.ok(mace.findBuff('resist'), '锏 keeps its 抵抗');
assert.ok(stone.findBuff('ab:stone'), 'the statue form starts');
});
});
describe('a blocked 隐匿 enemy is drawn solid (b.snap stealth bit: PRTS 作战机制 §隐匿 "在被阻挡时开关会被关掉")', () => {
test('the 逐火 ember: see-through while it walks unblocked, solid while blocked or revealed; an ally keeps its 迷彩 / 隐匿 look while blocking', async () => {
const { flagsOf } = await import('../../server/sim/snapshot.js');
const { UF } = await import('../../shared/constants.js');
const h = arena({ units: [{ chessId: 't_wall', row: 9, col: 7 }] });
h.step();
const e = put(h, 'enemy_1288_duskls', { mods: { speedMul: 1 } });
h.run(0.5);
h.b.kill(e, null);
h.run(HUSK_REBIRTH + 0.1);
assert.ok(e.s.flags.stealth && !e.blockedBy);
assert.ok(flagsOf(e) & UF.STEALTH, 'unblocked: see-through');
const tuple = () => h.b.snapshot().units.find((t) => t[0] === e.id);
assert.ok(tuple()[7] & UF.STEALTH, 'in b.snap too');
assert.ok(h.runUntil(() => !!e.blockedBy, 10), 'walks into the wall');
assert.ok(e.s.flags.stealth, 'the sim keeps the 隐匿 flag (it returns once the block ends)');
assert.equal(flagsOf(e) & UF.STEALTH, 0, 'blocked: drawn solid');
assert.equal(tuple()[7] & UF.STEALTH, 0);
assert.ok(flagsOf(e) & UF.BLOCKED);
const g = put(h, 'enemy_1288_duskls', { pos: [11, 9], mods: { speedMul: 0 } });
h.b.kill(g, null);
h.run(HUSK_REBIRTH + 0.1);
assert.ok(flagsOf(g) & UF.STEALTH);
h.b.addBuff(g, { key: 'test:reveal', duration: 5, flags: { reveal: true } });
assert.equal(flagsOf(g) & UF.STEALTH, 0, 'revealed: drawn solid');
const wall = h.unit('t_wall');
h.b.addBuff(wall, { key: 'test:stealth', duration: 5, flags: { stealth: true } });
assert.ok(wall.blocking.length > 0 && flagsOf(wall) & UF.STEALTH, 'an ally keeps its 隐匿 look while blocking');
});
});