- 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).
85 lines
5.1 KiB
JavaScript
85 lines
5.1 KiB
JavaScript
// 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);
|
||
});
|