fix: 再部署重置浮游倍率;roster-026、solo-HARD-1、solo-ABYSS-1 不再沿用旧缓存
This commit is contained in:
1 parent
bc50cac01f
commit
7434e9eaf0
8 files changed
+210
-11
No files matched your search
+1
-1
@@ -1341,7 +1341,7 @@ header of professions.js).
|
||||
| loopshooter | 回环射手 (user playtest #3): every attack throws a boomerang (`ai.js throwBoomerang`, projectile `'boomerang'`) out to the target at 15 tiles/s — it hits on arrival — and back to the thrower's current position at 3.75 tiles/s without damage (PRTS 跃跃 "投射物飞行速度15,返回时飞行速度3.75"); attacks only while holding it (every boomerang thrown caught — "必须回收全部回旋投掷物才可以进行下一次攻击", `unit.trait.boomerangsOut`) and with the attack cooldown ready, so the real interval is the longer of the two; a target dead mid-flight is not hit (it still flies to the last position and back); knocked out / withdrawn ⇒ lost, a redeployed thrower holds a fresh one; 跃跃 S2's extra boomerangs share the one flight (cnt hits); each catch fires `boomerangCaught`, and an attack profile's `boomerangOnward(ctx)` flies it on after the first hit before it turns back — ctx `hit` (a hit of the same attack), `comeBack(x, y)` once (a content error sends it back from the hit point): 娜仁图亚 S1 bounces, S2 dashes on and hits on its way back (kits/ops/op-narant.js) |
|
||||
| reaperrange | hits every enemy in range; ×1.5 (bb atk_scale) on the trait front grid (or its own line ahead) — both along its direction |
|
||||
| chain | chain N (trait text/bb max_target) with −15 % per jump (bb chain.atk_scale), 1.7-tile jumps (constants.js CHAIN_RADIUS, PRTS 溅射半径一览: 链术师 1.7; 1.8 until 0.1.1), sluggish on each hit |
|
||||
| funnel | drone damage 20 % → +15 %/hit on the same target → 110 % (bb init/delta/max) |
|
||||
| funnel | drone damage 20 % → +15 %/hit on the same target → 110 % (bb init/delta/max); a normal deployment resets target/scale and any per-target ramp map to init, including revival against the same living target; free `moveRedeploy` keeps the ramp (§22.3) |
|
||||
| mystic | 秘术师 (PRTS 分支特性信息 秘术师, GitHub #181): at its attack check (the attack ready, able to act, not disarmed) with no valid target — no target, or its kit's `canAttack` false (深靛 never picks a bound enemy) — it stores one energy (`storeEnergy`, up to bb times: 3, 深靛's MSC-X 4), an attack action: the attack interval restarts; a full store idles with the attack ready. The energies leave with its next attack that happens (`releaseEnergy` in `performAttack`, after `beforeAttack`: an attack cancelled before its shot keeps them) and land with the main hit, one arts attack hit each (`hitsFn` reads the hit's `info.energy`); a redeployment holds none [ASSUMED]. 维伊 / 黑键 plug their own `storeEnergy` (转置能量, elite energies) into the same check |
|
||||
| phalanx | no attack & DEF +200 %, RES +20 (bb) while the skill is off; while on, `rangeAoe`: each attack strikes every selectable enemy on its range at once (blocked enemies included; a stealthed one only when revealed or blocked), the same damage near and far, instant (`'beam'`) — "群体法术伤害" (secondary: Terra Wiki, Phalanx Caster: "attacks hit all enemies within their range"; supporting: PRTS 林 S3 备注 "单次普攻最多触发1次效果" — one normal attack can kill several — the same 锁定攻击范围 shape as the 轰击术师, and PRTS 溅射半径一览 documents no splash radius for it (it omits the 撼地者 too, so this is not proof); no primary source states a target cap; community report E3: it used to be one bolt + a 1.1 splash). 卡涅利安's charged S1 keeps the skill-off trait, 不攻击 included (kit `canAttack`, PRTS 备注) |
|
||||
| physician | heal the lowest HP% injured ally in range (a skill `targeting.maxTargets` widens any heal profile) |
|
||||
|
||||
@@ -167,6 +167,12 @@ Profession traits (default behaviours implemented once in the engine, keyed by `
|
||||
|
||||
---
|
||||
|
||||
Funnel drone ramps return to the configured initial scale on a normal deployment, including automatic revival
|
||||
against the same living target ([PRTS branch traits, 驭械术师](https://prts.wiki/w/分支特性信息/data#驭械术师)).
|
||||
The profession resets its target/scale and any per-target ramp map, leaving other trait state alone. A free
|
||||
`moveRedeploy` keeps the ramp under the existing move carry decision (§22.3); uninterrupted attacks still ramp and
|
||||
cap normally, and changing targets still starts at the initial scale.
|
||||
|
||||
## 7. Content modules (server/sim/content + match/effectsMeta handlers)
|
||||
|
||||
Each domain file `server/sim/content/{tokens,bonds,garrisons,items,bands,enemies,bosses,devices,choices}.js` exports `install(battle)` (battle side; called once per Battle by `content/index.js → installContent`) and `registerMeta(registry)` (prep side; called once at server boot by `match/effectsMeta.js`, API documented in `docs/META.md`). Kits live one per file in `content/kits/ops/` (`export default { [baseChessId]: (bb, chess, def) => Kit }`), listed in `content/kits/index.js` (guide: `content/kits/README.md`). Content must be **data-driven from blackboards** (numbers never hard-coded when a blackboard key exists). Every effect gets at least one unit test in `test/content/*.test.js` using the `test/helpers/battleHarness.js` harness (provided by sim-core).
|
||||
|
||||
@@ -274,3 +274,24 @@ enemy: picking emitted a unit click with no definition, instead of the tile clic
|
||||
engine-canvas regression retains the card/100 HP assertions and also covers another field refresh, live HP loss and
|
||||
closing the card when the crate is destroyed. Device metadata supplied by the new entry still wins; changing fields
|
||||
does not carry old devices across. Gameplay and allied targeting rules are unchanged.
|
||||
|
||||
### 29.15 Funnel ramps on redeployment
|
||||
|
||||
PRTS [branch traits, 驭械术师](https://prts.wiki/w/分支特性信息/data#驭械术师) resets drone damage to its initial
|
||||
scale when the operator redeploys. The profession installer previously initialized the ramp only once, so a
|
||||
knocked-out Rockrock revived by Ermengard could keep 110% against the same surviving enemy. Normal deployments now
|
||||
reset the funnel target/scale and the per-target map used by Minimalist and Kjera. Other trait state is retained;
|
||||
continuous attacks, target changes and the existing free-move carry decision (§22.3) keep their rules. No operator
|
||||
kit or attack/chase calculation changes.
|
||||
|
||||
The regression uses the production Match with seed 8, NORMAL solo mode and Ermengard: buy Caper in round 1,
|
||||
upgrade in round 2, buy Rockrock in round 5 and leave both fighting normally. In round 6 a real enemy knocks out
|
||||
Rockrock; automatic revival must start the same living target at 20%. Controlled Battle tests separately cover
|
||||
normal/elite Rockrock and hidden Kjera, Minimalist/Kjera per-target maps, uninterrupted ramping, target changes,
|
||||
other-unit deployments, unrelated trait state and free movement.
|
||||
|
||||
Golden updates: `roster-026` reduces Minimalist's damage from 3936 to 2397 and updates the affected enemy,
|
||||
player and snapshot totals; `solo-HARD-1` and `solo-ABYSS-1` reduce the AI lineup's damage after funnel
|
||||
redeployments (boss damage decreases by 572 and 413 respectively). Their victory, round, RNG and error outcomes
|
||||
are unchanged. These three scenarios now start each normal deployment with a fresh ramp instead of carrying
|
||||
the previous deployment's per-target cache.
|
||||
@@ -385,8 +385,15 @@ const installTactician = (battle, unit) => {
|
||||
};
|
||||
|
||||
const installFunnel = (battle, unit) => {
|
||||
unit.trait.funnelTarget = null;
|
||||
unit.trait.funnelScale = unit.profile.funnel?.init ?? 0.2;
|
||||
// PRTS branch traits, funnel: redeployment resets the ramp even against the same living enemy.
|
||||
const reset = () => {
|
||||
unit.trait.funnelTarget = null;
|
||||
unit.trait.funnelScale = unit.profile.funnel?.init ?? 0.2;
|
||||
unit.trait.funnelMap?.clear(); // Minimalist / Kjera keep one ramp per target.
|
||||
};
|
||||
reset();
|
||||
// A free move keeps the existing state under the owner's carry decision (DESIGN §22.3).
|
||||
battle.on('deploy', (ctx) => { if (ctx.unit === unit && !ctx.move) reset(); }, { owner: unit });
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -246,7 +246,7 @@
|
||||
"combat": "client",
|
||||
"boost": null,
|
||||
"setup": {"stageId":"act1autochess_m03","bossId":"boss_5","hiddenBossId":"boss_8","factions":["TIMES","DOT","INVISIBLE"]},
|
||||
"end": {"victory":false,"roundsPassed":13,"hiddenReached":false,"hiddenCleared":false,"reason":"defeat","teamLp":0,"bossPoolLeft":341377,"virtualMs":107800},
|
||||
"end": {"victory":false,"roundsPassed":13,"hiddenReached":false,"hiddenCleared":false,"reason":"defeat","teamLp":0,"bossPoolLeft":341949,"virtualMs":107800},
|
||||
"rng": {"setup":25,"shop":215,"waves":0,"draft":42,"bots":8070,"meta":1},
|
||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||
"rounds": [
|
||||
@@ -299,7 +299,7 @@
|
||||
"title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"},
|
||||
"trophies": 0,
|
||||
"reward": 340,
|
||||
"stats": {"activatedLayers":212,"bossDamage":438623,"dmgDealt":2261523,"gold":135,"itemsEquipped":16,"kills":402,"leaks":14,"lpLost":14,"merges":4,"perfectRounds":10,"refreshes":17},
|
||||
"stats": {"activatedLayers":212,"bossDamage":438051,"dmgDealt":2260950,"gold":135,"itemsEquipped":16,"kills":402,"leaks":14,"lpLost":14,"merges":4,"perfectRounds":10,"refreshes":17},
|
||||
"lineup": ["3_13_a@12,5[1_01_e_b+3_08_e_a]","3_20_a@11,8[5_02_e_a]","4_13_a@10,3","6_19_a@10,5[1_02_e_a+3_11_e_a]","6_17_a@10,8[2_01_e_a+5_01_e_a]","2_08_a@9,3[4_02_e_a+3_07_e_b]","1_19_b@9,4","1_12_b@9,5[4_10_e_a]"],
|
||||
"bonds": ["deputShip 0/24","kazimierzShip 1/26","kjeragShip 1/15","sargonShip 1/41","skillfulShip 1/31","soloShip 0/15","steadShip 0/11","suntShip 1/22","swiftShip 1/77"]
|
||||
}
|
||||
@@ -384,7 +384,7 @@
|
||||
"combat": "client",
|
||||
"boost": null,
|
||||
"setup": {"stageId":"act1autochess_m03","bossId":"boss_5","hiddenBossId":"boss_8","factions":["TIMES","DOT","INVISIBLE"]},
|
||||
"end": {"victory":false,"roundsPassed":13,"hiddenReached":false,"hiddenCleared":false,"reason":"defeat","teamLp":0,"bossPoolLeft":2832671,"virtualMs":107800},
|
||||
"end": {"victory":false,"roundsPassed":13,"hiddenReached":false,"hiddenCleared":false,"reason":"defeat","teamLp":0,"bossPoolLeft":2833084,"virtualMs":107800},
|
||||
"rng": {"setup":25,"shop":215,"waves":0,"draft":42,"bots":8070,"meta":1},
|
||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||
"rounds": [
|
||||
@@ -437,7 +437,7 @@
|
||||
"title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"},
|
||||
"trophies": 0,
|
||||
"reward": 340,
|
||||
"stats": {"activatedLayers":220,"bossDamage":167329,"dmgDealt":3078206,"gold":135,"itemsEquipped":16,"kills":386,"leaks":22,"lpLost":20,"merges":4,"perfectRounds":11,"refreshes":17},
|
||||
"stats": {"activatedLayers":220,"bossDamage":166916,"dmgDealt":3077793,"gold":135,"itemsEquipped":16,"kills":386,"leaks":22,"lpLost":20,"merges":4,"perfectRounds":11,"refreshes":17},
|
||||
"lineup": ["3_20_a@12,5[5_02_e_a]","4_13_a@11,8","3_13_a@10,3[1_01_e_b+3_08_e_a]","2_08_a@10,5[4_02_e_a+3_07_e_b]","6_17_a@10,8","1_12_b@9,4[4_10_e_a]","6_19_a@9,5[1_02_e_a+3_11_e_a]","1_19_b@9,9[2_01_e_a+5_01_e_a]"],
|
||||
"bonds": ["deputShip 0/24","kazimierzShip 1/26","kjeragShip 1/15","sargonShip 1/41","skillfulShip 1/31","soloShip 0/15","steadShip 0/11","suntShip 1/22","swiftShip 1/85"]
|
||||
}
|
||||
|
||||
@@ -1630,7 +1630,7 @@
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 179,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":19,"total":71,"perfect":false,"leaks":54,"leaksUncounted":0,"coins":6,"dmg":212430,"bossDmg":0,"heal":2065,"deaths":22,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":19,"total":71,"perfect":false,"leaks":54,"leaksUncounted":0,"coins":6,"dmg":210891,"bossDmg":0,"heal":2065,"deaths":22,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_3_12_a",0,null,0,0,99,2,9831,6171,2065,21,4],
|
||||
["p1",2,"op","chess_char_3_16_a",0,null,0,0,101,0,4246,4781,0,15,1],
|
||||
@@ -1639,7 +1639,7 @@
|
||||
["p1",5,"op","chess_char_3_09_a",0,null,1393.7,1,103,1,16928,1394,0,24,3],
|
||||
["p1",6,"op","chess_char_3_10_a",0,null,0,0,89,1,36230,3630,0,22,5],
|
||||
["p1",7,"op","chess_char_3_11_a",0,null,0,0,7,1,12325,1448,0,6,1],
|
||||
["p1",8,"op","chess_char_3_13_a",0,null,0,0,53,0,3936,5199,0,23,1],
|
||||
["p1",8,"op","chess_char_3_13_a",0,null,0,0,53,0,2397,5199,0,23,1],
|
||||
["p1",9,"op","chess_char_3_14_a",0,null,0,0,79,0,13946,2752,0,15,0],
|
||||
["p1",10,"op","chess_char_3_17_a",0,null,1218.8,1,96,4,42240,3722,0,49,9],
|
||||
["p1",11,"op","chess_char_3_19_a",0,null,0,0,77,1,20696,3166,0,32,0],
|
||||
@@ -1653,7 +1653,7 @@
|
||||
"enemies": {
|
||||
"enemy_10042_prtrop": [12,0,12,7101,28836],
|
||||
"enemy_10042_prtrop_2": [12,0,10,6218,18414],
|
||||
"enemy_10043_sailor": [39,12,27,20980,197254],
|
||||
"enemy_10043_sailor": [39,12,27,20980,195715],
|
||||
"enemy_1005_yokai_2": [1,1,0,68,2678],
|
||||
"enemy_10156_mncrer": [1,1,0,140,6912],
|
||||
"enemy_1116_liprr_2": [1,1,0,247,12960],
|
||||
@@ -1667,7 +1667,7 @@
|
||||
"hooks": [1,102,4200,412,412,1525,1585,10,42,95,33,33,0,10321,22,47,73,52,1,46,0,0,0],
|
||||
"events": {"atk":478,"bounty":3,"deploy":26,"die":43,"dmg":1585,"engage":14,"fx":26,"heal":10,"leak":52,"skill":66,"spawn":90,"status":54},
|
||||
"fx": {"dp":7,"explode":3,"revive":4,"sp":4,"strike":1,"summon":2,"telegraph":2,"wolfShadow":1,"wolfShadowLost":2},
|
||||
"snaps": "226d72ef 4944a14f b2954de1 522e52cb dd46cce1 b1febb8b ac7e7cf4 bb6af571 080c3ba3 fc1505bd c0528264 4a64f7c2 beffba7c a423b42e"
|
||||
"snaps": "226d72ef 4944a14f b2954de1 3ffcf693 a30dc502 d84524d6 4ead34e9 4b042eb0 8e0173d6 43359664 a5bda569 a184d2eb 172129c9 aa1a4797"
|
||||
},
|
||||
"roster-027": {
|
||||
"about": "pass 1 band 4: chess_char_3_06_b chess_char_3_12_b chess_char_3_02_b chess_char_3_03_b chess_char_3_04_b chess_char_3_05_b chess_char_3_21_a chess_char_3_01_b chess_char_3_08_b chess_char_3_09_b chess_char_3_10_b chess_char_3_11_b",
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Real production Match + Battle, legal normal intents from an empty board. Seed 8 fixes the shop/wave route.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Battle } from '../../server/sim/Battle.js';
|
||||
import { makeMatch } from './harness.js';
|
||||
|
||||
test('Rockrock killed by a real enemy and automatically revived by Ermengard starts the same target at init', () => {
|
||||
const observed = [];
|
||||
class ObservedBattle extends Battle {
|
||||
constructor(opts) {
|
||||
super(opts);
|
||||
const log = { round: opts.round, deaths: [], deploys: [], firstHits: [] };
|
||||
observed.push(log);
|
||||
const seen = new Set(), launches = new Map();
|
||||
this.on('death', c => {
|
||||
if (c.unit.defId === 'chess_char_2_10_a') log.deaths.push({ t: this.time, seq: c.unit.deploySeq,
|
||||
target: c.unit.trait.funnelTarget, scale: c.unit.trait.funnelScale, reason: c.reason });
|
||||
}, { priority: 1000 });
|
||||
this.on('death', c => {
|
||||
if (c.unit.defId === 'chess_char_2_10_a') log.deaths.at(-1).revivedBy = c.revivedBy;
|
||||
}, { priority: -1000 });
|
||||
this.on('deploy', c => {
|
||||
if (c.unit.defId === 'chess_char_2_10_a') log.deploys.push({ seq: c.unit.deploySeq,
|
||||
target: c.unit.trait.funnelTarget, scale: c.unit.trait.funnelScale });
|
||||
}, { priority: -1000 });
|
||||
this.on('attack', c => {
|
||||
if (c.attacker.defId !== 'chess_char_2_10_a') return;
|
||||
// This unmodified Rockrock route has no nested attack callback. The attack hook follows its shot creation.
|
||||
launches.set(this._attackSeq, { t: this.time, seq: c.attacker.deploySeq });
|
||||
});
|
||||
this.on('damaged', c => {
|
||||
const u = c.source;
|
||||
const launch = launches.get(c.dmg?.attackId);
|
||||
if (u?.defId !== 'chess_char_2_10_a' || seen.has(u.deploySeq) || launch?.seq !== u.deploySeq) return;
|
||||
seen.add(u.deploySeq);
|
||||
log.firstHits.push({ t: this.time, launchAt: launch.t, seq: u.deploySeq, target: c.target.id,
|
||||
scale: u.trait.funnelScale, hp: c.target.hp });
|
||||
});
|
||||
}
|
||||
}
|
||||
const h = makeMatch({ mode: 'solo', seed: 8 });
|
||||
h.m.BattleClass = ObservedBattle; // read-only observers, no combat state writes
|
||||
const p = h.ps('p_0'), act = msg => assert.deepEqual(h.m.handle('p_0', msg), { ok: true });
|
||||
try {
|
||||
h.start().toPrep(1, { band: 'band_ermengard' });
|
||||
act({ t: 'g.buy', slot: 0 });
|
||||
act({ t: 'g.move', uid: p.hand.find(x => x?.id === 'chess_char_1_09_a').uid,
|
||||
to: { area: 'board', row: 12, col: 6 }, dir: 'RIGHT' });
|
||||
h.toPrep(2, { band: 'band_ermengard' });
|
||||
act({ t: 'g.levelUp' });
|
||||
h.toPrep(5, { band: 'band_ermengard' });
|
||||
assert.equal(p.shop.slots[3].id, 'chess_char_2_10_a');
|
||||
act({ t: 'g.buy', slot: 3 });
|
||||
act({ t: 'g.move', uid: p.hand.find(x => x?.id === 'chess_char_2_10_a').uid,
|
||||
to: { area: 'board', row: 12, col: 8 }, dir: 'RIGHT' });
|
||||
h.toPrep(7, { band: 'band_ermengard' });
|
||||
const log = observed.find(x => x.round === 6);
|
||||
assert.ok(log?.deaths.length > 0);
|
||||
const paired = log.deaths.map(d => ({ death: d, hit: log.firstHits.find(x => x.seq > d.seq) }))
|
||||
.filter(x => x.hit && x.death.target === x.hit.target && x.death.scale > 0.2);
|
||||
assert.ok(paired.length > 0, JSON.stringify(log));
|
||||
for (const { death, hit } of paired) {
|
||||
assert.equal(death.reason, 'killed');
|
||||
assert.equal(death.revivedBy, 'band');
|
||||
assert.ok(hit.launchAt >= death.t, 'fresh attack after the knockout and revival');
|
||||
assert.ok(hit.hp > 0, 'original target still lives');
|
||||
const deploy = log.deploys.find(x => x.seq === hit.seq);
|
||||
assert.equal(deploy.target, null);
|
||||
assert.ok(Math.abs(deploy.scale - 0.2) < 1e-8);
|
||||
assert.ok(Math.abs(hit.scale - 0.2) < 1e-8, JSON.stringify({ death, hit }));
|
||||
}
|
||||
assert.deepEqual(h.logs.error, []);
|
||||
h.invariants();
|
||||
} finally { h.m.dispose(); }
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
// PRTS 分支特性信息/data §驭械术师: changing targets / redeploying resets the drone ramp.
|
||||
// Controlled battle inputs here; the real Match route is covered in test/match/funnel-redeploy.test.js.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
|
||||
const make = (id = 'chess_char_2_10_a', extra = []) => makeBattle({
|
||||
units: [{ chessId: id, row: 10, col: 4 }, ...extra],
|
||||
defs: { enemies: { dummy: enemyRec({ key: 'dummy', hp: 1e7, speed: 0 }) } },
|
||||
enemies: [{ key: 'dummy', pos: [10, 6] }], timeLimit: 300, autoFinish: false,
|
||||
hooks: ['damaged'], captureNoisy: true,
|
||||
});
|
||||
const hits = (h, u) => h.hooksOf('damaged').filter(c => c.source === u && c.target.side === 'enemy');
|
||||
const near = (a, b) => assert.ok(Math.abs(a - b) < 1e-8, `${a} != ${b}`);
|
||||
const ramp = (h, u) => {
|
||||
h.runUntil(() => hits(h, u).length >= 8, 20);
|
||||
assert.ok(hits(h, u).length >= 8);
|
||||
};
|
||||
|
||||
for (const id of ['chess_char_2_10_a', 'chess_char_2_10_b', 'chess_char_4_03_a', 'chess_char_4_03_b']) {
|
||||
test(`${id}: same living target starts at the configured initial scale after automatic redeployment`, () => {
|
||||
const h = make(id), u = h.unit(id);
|
||||
ramp(h, u);
|
||||
const e = h.enemies()[0];
|
||||
assert.ok(u.trait.funnelScale > u.profile.funnel.init);
|
||||
const before = hits(h, u).length;
|
||||
h.b.dealDamage(e, u, { amount: 1e7, type: 'true' });
|
||||
assert.equal(u.alive, false);
|
||||
h.runUntil(() => u.alive, 100);
|
||||
assert.equal(u.alive, true);
|
||||
assert.equal(e.alive, true);
|
||||
assert.equal(u.trait.funnelTarget, null);
|
||||
near(u.trait.funnelScale, u.profile.funnel.init);
|
||||
assert.equal(h.runUntil(() => hits(h, u).length > before, 5), true);
|
||||
assert.ok(hits(h, u).length > before);
|
||||
assert.equal(hits(h, u).at(-1).target, e);
|
||||
near(u.trait.funnelScale, u.profile.funnel.init);
|
||||
checkInvariants(h.b);
|
||||
assert.deepEqual(h.b.errors, []);
|
||||
});
|
||||
}
|
||||
|
||||
for (const id of ['chess_char_3_13_a', 'chess_char_3_13_b', 'chess_char_3_20_a', 'chess_char_3_20_b']) {
|
||||
test(`${id}: per-target funnel map restarts on redeployment`, () => {
|
||||
const h = make(id), u = h.unit(id);
|
||||
ramp(h, u);
|
||||
const e = h.enemies()[0];
|
||||
assert.ok(u.trait.funnelMap.get(e.id).s > u.profile.funnel.init);
|
||||
h.b.retreat(u);
|
||||
assert.equal(h.b.redeploy(u), true);
|
||||
assert.equal(u.trait.funnelMap.size, 0);
|
||||
const before = hits(h, u).length;
|
||||
assert.equal(h.runUntil(() => hits(h, u).length > before, 5), true);
|
||||
assert.ok(hits(h, u).length > before);
|
||||
near(u.trait.funnelMap.get(e.id).s, u.profile.funnel.init);
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
}
|
||||
|
||||
test('continuous attacks ramp and cap; changing the living target resets normally', () => {
|
||||
const h = make(), u = h.unit('chess_char_2_10_a');
|
||||
ramp(h, u);
|
||||
near(u.trait.funnelScale, u.profile.funnel.max);
|
||||
const replacement = h.spawn('dummy', { pos: [10, 5] });
|
||||
const before = hits(h, u).length;
|
||||
assert.equal(h.runUntil(() => hits(h, u).length > before, 5), true);
|
||||
assert.ok(hits(h, u).length > before);
|
||||
assert.equal(hits(h, u).at(-1).target, replacement);
|
||||
near(u.trait.funnelScale, u.profile.funnel.init);
|
||||
});
|
||||
|
||||
test('another unit deployment and a free move keep this ramp; unrelated trait state survives redeployment', () => {
|
||||
const h = make('chess_char_2_10_a', [{ chessId: 'chess_char_1_02_a', row: 12, col: 3 }]);
|
||||
const u = h.unit('chess_char_2_10_a'), other = h.unit('chess_char_1_02_a');
|
||||
ramp(h, u);
|
||||
const scale = u.trait.funnelScale, target = u.trait.funnelTarget;
|
||||
u.trait.testUnrelated = { value: 7 }; // controlled sentinel: no production trait object may be replaced
|
||||
const sentinel = u.trait.testUnrelated;
|
||||
h.b.retreat(other);
|
||||
assert.equal(h.b.redeploy(other), true);
|
||||
assert.equal(u.trait.funnelTarget, target);
|
||||
near(u.trait.funnelScale, scale);
|
||||
assert.equal(h.b.moveRedeploy(u, 10, 3), true); // owner-approved move carry, DESIGN §22.3
|
||||
assert.equal(u.trait.funnelTarget, target);
|
||||
near(u.trait.funnelScale, scale);
|
||||
h.b.retreat(u);
|
||||
assert.equal(h.b.redeploy(u), true);
|
||||
assert.equal(u.trait.testUnrelated, sentinel);
|
||||
near(u.trait.funnelScale, u.profile.funnel.init);
|
||||
});
|
||||
Reference in new issue
Block a user