Files
ark-stronghold-protocol/test/sim/feedback5-o8-engine.test.js
sganggs db7a177164 feedback5 O8: two additive engine rules for the 自选 kits — a 屏障 of one damage type and a profile's own target exclusion
- buffs.js makeBuff / damage.js absorbShields: a shield buff with `shieldType` ('phys' | 'arts' | 'true' | 'elemental')
  absorbs that damage type only (夜莺 S2 法术护盾 "屏障能吸收…法术伤害"); a shield without a type absorbs every type as
  before (PRTS 术语释义 屏障 "若无特殊说明,屏障可吸收全种类伤害"); shields are still spent oldest first.
- targeting.js canTargetEnemy: a profile's `skipEnemy(e)` (a kit trait) keeps such an enemy out of the unit's normal
  targets, its block targets and its skill-trigger target condition (嵯峨 劝善 "嵯峨不攻击重伤单位").
- docs/SIM.md §3 / §4 / §8, professions.js profile field list.
No content uses either yet: golden unchanged (roster 49, bonds 46, fields 22, matches 17, standins 10, diy 6 match).
Test: test/sim/feedback5-o8-engine.test.js (2).
2026-10-05 23:55:13 +08:00

85 lines
5.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// test/sim/feedback5-o8-engine.test.js — two additive engine rules the 0.2.0 自选 kits of 夜莺 and 嵯峨 need:
// - a 屏障 of one damage type (buff `shieldType`, damage.js absorbShields): 夜莺 S2 法术护盾 "能吸收…法术伤害"; a shield
// without a type absorbs every type (PRTS 术语释义 屏障 "若无特殊说明,屏障可吸收全种类伤害"); older shields first;
// - a profile's own target exclusion (`skipEnemy(e)`, targeting.js canTargetEnemy): 嵯峨 "不攻击重伤单位" — no normal
// attack on such an enemy, not as a block target, and the DEFAULT trigger condition ignores it.
// Run: node --test test/sim/feedback5-o8-engine.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, enemyRec, chessRec, checkInvariants } from '../helpers/battleHarness.js';
import { canTargetEnemy } from '../../server/sim/targeting.js';
const ENEMIES = { enemy_dummy: enemyRec({ key: 'enemy_dummy', hp: 1e9, speed: 0, mass: 0 }) };
const CHESS = { test_guard_a: chessRec({ id: 'test_guard_a', profession: 'WARRIOR', stats: { maxHp: 10000, atk: 300, def: 0, res: 0 }, skill: { duration: 5, spCost: 1, initSp: 0, trigger: { rule: 'DEFAULT' } } }) };
function field(kits = undefined) {
const h = makeBattle({
defs: { enemies: ENEMIES, chess: CHESS }, timeLimit: 600, autoFinish: false, seed: 3, kits,
hooks: ['damaged', 'skillStart'], captureNoisy: true,
units: [{ uid: 1, chessId: 'test_guard_a', row: 10, col: 5 }],
});
h.step();
return { h, u: h.unit(1) };
}
test('typed shield: an arts 屏障 absorbs arts damage only; an untyped one every type; the older shield is spent first', () => {
const { h, u } = field();
h.b.addBuff(u, { key: 'test:artsBarrier', shield: 100, shieldType: 'arts', duration: 30 });
for (const type of ['phys', 'true', 'elemental']) {
const hp = u.hp;
h.b.dealDamage(null, u, { amount: 10, type, canDodge: false });
assert.equal(u.hp, hp - 10, `${type} passes the arts barrier`);
}
assert.equal(u.findBuff('test:artsBarrier').shield, 100);
assert.equal(u.s.shield, 100, 'it counts in the shield bar');
let hp = u.hp;
h.b.dealDamage(null, u, { amount: 40, type: 'arts', canDodge: false });
assert.deepEqual([u.hp, u.findBuff('test:artsBarrier').shield], [hp, 60], 'arts absorbed');
// an untyped shield added later: arts goes to the older (typed) one first, physical to the untyped one
h.b.addBuff(u, { key: 'test:barrier', shield: 50, duration: 30 });
h.b.dealDamage(null, u, { amount: 30, type: 'arts', canDodge: false });
assert.deepEqual([u.findBuff('test:artsBarrier').shield, u.findBuff('test:barrier').shield], [30, 50], 'older first');
h.b.dealDamage(null, u, { amount: 20, type: 'phys', canDodge: false });
assert.deepEqual([u.findBuff('test:artsBarrier').shield, u.findBuff('test:barrier').shield], [30, 30], 'physical: the untyped one only');
hp = u.hp;
h.b.dealDamage(null, u, { amount: 100, type: 'arts', canDodge: false });
assert.equal(u.findBuff('test:artsBarrier'), null, 'spent and removed');
assert.equal(u.findBuff('test:barrier'), null, 'the rest on the untyped one, spent too');
assert.equal(u.hp, hp - 40, '100 − 30 − 30 reaches HP');
// a typed hit-negating barrier
h.b.addBuff(u, { key: 'test:physBlock', shieldHits: 1, shieldType: 'phys', duration: 30 });
hp = u.hp;
h.b.dealDamage(null, u, { amount: 25, type: 'arts', canDodge: false });
assert.equal(u.hp, hp - 25, 'arts passes a physical block');
h.b.dealDamage(null, u, { amount: 25, type: 'phys', canDodge: false });
assert.equal(u.hp, hp - 25, 'the physical hit is negated');
assert.equal(u.findBuff('test:physBlock'), null);
checkInvariants(h.b);
});
test('profile skipEnemy: the unit never selects such an enemy — no attack, no block target, no DEFAULT trigger — and other enemies stay its targets', () => {
const skip = new Set();
const { h, u } = field({ test_guard_a: () => ({ skill: { kind: 'duration' }, trait: { skipEnemy: (e) => skip.has(e) } }) });
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
skip.add(e);
assert.equal(canTargetEnemy(u, e, u.profile), false);
assert.equal(canTargetEnemy(u, e, { canHitFly: false }), true, 'another profile still may');
h.run(3);
assert.equal(h.hooksOf('damaged').filter((c) => c.source === u).length, 0, 'never attacked');
assert.equal(u.skill.activations, 0, 'DEFAULT: no target');
// blocked: still not a target
const w = h.spawn('enemy_dummy', { pos: [10, 5] });
skip.add(w);
h.run(1);
assert.ok(u.blocking.includes(w), 'she blocks it');
assert.equal(h.b.blockedTargets(u, u.profile).length, 0, 'not a block target');
assert.equal(h.hooksOf('damaged').filter((c) => c.source === u).length, 0);
// an ordinary enemy is attacked
const o = h.spawn('enemy_dummy', { pos: [10, 6] });
assert.ok(h.runUntil(() => h.hooksOf('damaged').some((c) => c.source === u && c.target === o), 3), 'attacks the other one');
assert.ok(h.hooksOf('damaged').filter((c) => c.source === u).every((c) => c.target === o), 'only that one');
assert.ok(u.skill.activations >= 1, 'DEFAULT fires for it');
checkInvariants(h.b);
});