feedback5 O13: 假日威龙陈's slime follows the client data (her attack range, per source, DEF as a final addition) and the water TAG is one flag; 莱伊's S2 bullet recovery takes HUN-X's extra on an empty magazine

- op-chen2.js: the S2 / S3 slime (projectile_chr_chen2_s2 / _s3: fired by each attack, sitting on her in her direction,
  5 s, every 0.2 s chen2_slime 0.25 s on the ground enemies of its range, ignoring 无法选择) now covers the tiles of her
  attack range at each attack instead of the struck enemies' tiles; one effect per 假日威龙陈 (independentCharacterSource);
  the DEF cut compensated for the target's DEF multipliers (FINAL_ADDITION). RPR-Y stage 3's water TAG is the level's one
  flag (chen2_e_003_t[map_tag]: set at her skill's start, cleared at its end or her exit). S2's attacks stay skill attacks.
- op-ray.js: the 沙地兽's recovered bullets go through ray_tr_add (≤ its counter of 8, HUN-X's extra_add first when she is
  empty, ≤ her magazine); kit headers cite the client templates (also op-ascln.js).
- Tests: op_chen2.test.js slime test rewritten (her range, stealthed walkers, the final-addition DEF, two sources).
- Golden: only diy moved — diy-049 / diy-050 (假日威龙陈's slime); every other scenario and family unchanged.
This commit is contained in:
sganggs committed 2026-10-06 01:38:31 +08:00
1 parent f44e524afd
commit 1d62a062d5
5 files changed
+201 -152

No files matched your search

+44 -24
View File
@@ -11,7 +11,7 @@ import { readFileSync } from 'node:fs';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
import { diyPool, validateDiyPicks } from '../../shared/diy.js';
import { spareChance, waterOn, holidayLines, slimeOf, SLIME_KEY } from '../../server/sim/content/kits/ops/op-chen2.js';
import { spareChance, waterOn, holidayLines, slimeOf, slimeKey } from '../../server/sim/content/kits/ops/op-chen2.js';
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
const CHESS = load('chess');
@@ -27,7 +27,7 @@ const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uni
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
const ENEMIES = {
enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }),
enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }), enemy_armor: dummy('enemy_armor', { def: 500 }),
enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.5, mass: 0 }),
};
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, RX, RY].map((m) => [t, true, m]))];
@@ -161,38 +161,58 @@ test('S2 “堇青之夜” (MANUAL, data DEFAULT, 蓄力 2): 27 / 24 SP from 5
}
});
test('slime (S2 / S3): each struck enemy\'s tile holds slime 5 s; a ground enemy on it moves ×0.8 / ×0.7 (S2) and loses 100 / 150 DEF (not stacking), a stealthed one too, a flyer never; it lapses after the 5 s', () => {
for (const [tier, elite, skill] of [[5, false, 1], [6, true, 1], [6, true, 2]]) {
test('slime (S2 / S3): each attack slimes the tiles of her attack range (2-5; S3 2-6) for 5 s; a ground enemy there — struck or not, stealthed too — moves ×0.8 / ×0.7 / ×0.6 and loses 100 / 150 / 200 DEF after its multipliers (not stacking); a flyer or an enemy outside never; it lapses 5 s after her last attack; two 假日威龙陈 each slow', () => {
for (const [tier, elite, skill] of [[5, false, 1], [6, true, 1], [5, false, 2], [6, true, 2]]) {
const sk = skillOf(tier, elite, [S1, S2, S3][skill]);
const [ms, def, life] = [sk.bb['attack@move_speed'], sk.bb['attack@def'], sk.bb['attack@projectile_life_time']];
const { h, u } = field({ tier, elite, skill });
const e = h.spawn('enemy_dummy', { pos: [10, 5] });
const e = h.spawn('enemy_armor', { pos: [10, 5] });
const fly = h.spawn('enemy_fly', { pos: [10, 6] });
const out = h.spawn('enemy_armor', { pos: [10, 8] });
cast(h, u);
assert.ok(h.runUntil(() => atkHits(h, u).some((c) => c.dmg.isSkill && c.target === e), 5), 'struck');
assert.ok(h.runUntil(() => h.hooksOf('attack').some((c) => c.attacker === u && c.isSkill), 5), 'an attack');
h.run(0.25);
const pud = slimeOf(h.b);
assert.ok(pud && pud.has(10 * 21 + 5) && pud.has(10 * 21 + 6), `T${tier} S${skill + 1}: puddles under both struck enemies`);
const b = e.findBuff(SLIME_KEY);
const tiles = slimeOf(h.b, u);
assert.deepEqual([...tiles.keys()].sort((a, b) => a - b), [...u.rangeKeys].sort((a, b) => a - b), `T${tier} S${skill + 1}: her range (${skill === 2 ? '2-6' : '2-5'})`);
const key = slimeKey(u);
const b = e.findBuff(key);
assert.ok(b, `T${tier} S${skill + 1}: the ground enemy is slimed`);
assert.deepEqual(b.mods, { moveMul: 1 + sk.bb['attack@move_speed'], defFlat: sk.bb['attack@def'] }, `T${tier} S${skill + 1}: ×${1 + sk.bb['attack@move_speed']} / ${sk.bb['attack@def']}`);
assert.equal(fly.findBuff(SLIME_KEY), null, `T${tier}: never a flyer`);
approx(e.s.def, Math.max(0, e.base.def + sk.bb['attack@def']), 'DEF');
// 不叠加: one effect however many puddles / attacks
h.run(5);
assert.equal(e.buffs.filter((x) => x.key === SLIME_KEY).length, 1);
// a stealthed walker passing through (无视无法选择)
const w = h.spawn('enemy_dummy', { pos: [10, 5] });
h.b.addBuff(w, { key: 'test:stealth', flags: { stealth: true } });
h.run(0.3);
assert.ok(w.findBuff(SLIME_KEY), 'a stealthed enemy on the slime is slimed');
// stop the skill and let the puddles run out
approx(b.mods.moveMul, 1 + ms, `×${1 + ms}`);
approx(e.s.def, 500 + def, `DEF ${def}`);
assert.equal(fly.findBuff(key), null, 'never a flyer');
assert.equal(out.findBuff(key), null, 'not outside her range');
// a stealthed ground enemy that walks in (never struck: she cannot see it) — 无视无法选择
const sneak = h.spawn('enemy_armor', { pos: [9, 6] });
h.b.addBuff(sneak, { key: 'test:stealth', flags: { stealth: true } });
h.run(0.25);
assert.ok(sneak.findBuff(key), 'a stealthed enemy in the slime is slimed');
// the DEF cut is a final addition: after a ×0.7 DEF status
h.b.applyStatus(e, 'defDown', { duration: 30, value: 0.3, source: u });
h.run(0.25);
approx(e.s.def, 500 * 0.7 + def, 'DEF ×0.7 then the cut');
assert.equal(e.buffs.filter((x) => x.key === key).length, 1, '不叠加');
// stop her: the slime lapses life s after her last attack
u.skill.end('test');
h.b.addBuff(u, { key: 'test:disarm', flags: { disarm: true } });
h.run(sk.bb['attack@projectile_life_time'] + 0.5);
assert.equal(e.findBuff(SLIME_KEY), null, `T${tier}: gone ${sk.bb['attack@projectile_life_time']} s after the last attack`);
assert.equal(pud.size, 0, 'no puddle left');
const tLast = h.hooksOf('attack').filter((c) => c.attacker === u).slice(-1)[0].t;
h.run(tLast + life - 0.3 - h.b.time);
assert.ok(e.findBuff(key), 'still slimed');
h.run(0.7);
assert.equal(e.findBuff(key), null, `gone ${life} s after her last attack`);
done(h);
}
// two of them: one slow each (independentCharacterSource)
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 60, autoFinish: false, seed: 3, flags: { dpPerSec: 0 },
units: [{ uid: 1, diy: { slot: 5, charId: CHEN, skillIndex: 1 }, row: 10, col: 4 }, { uid: 2, diy: { slot: 6, charId: CHEN, skillIndex: 1 }, row: 11, col: 4 }],
});
h.step();
const e = h.spawn('enemy_armor', { pos: [10, 5] });
for (const id of [1, 2]) h.unit(id).skill.gainSp(999);
h.run(3);
assert.ok(e.findBuff(slimeKey(h.unit(1))) && e.findBuff(slimeKey(h.unit(2))), 'both slimes');
approx(e.s.def, 500 - 200, 'DEF −100 twice');
done(h);
});
test('S3 “假日风暴” (MANUAL, data ACTIVE_RANGE on 2-6): an enemy only on 2-6 casts it; range 2-6 (and back); ATK +55 % / +70 %, every attack strikes twice with the trait multiplier on every enemy; 32 bullets, 2 per attack (16 attacks), one left still attacks', () => {
+39 -39
View File
@@ -1982,35 +1982,35 @@
"round": 12,
"seed": 2215018795,
"waveId": "act1autochess_h05",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 23,
"time": 229.93,
"ticks": 6898,
"reason": "cleared",
"killed": 27,
"total": 60,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 299,
"rngDraws": 300,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":23,"total":60,"perfect":false,"leaks":37,"leaksUncounted":0,"coins":0,"dmg":1146961,"bossDmg":0,"heal":0,"deaths":10,"layerGains":{}}},
"players": {"p1":{"killed":27,"total":60,"perfect":false,"leaks":33,"leaksUncounted":0,"coins":0,"dmg":1206322,"bossDmg":0,"heal":0,"deaths":9,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,1875,1,10,0,22517,0,0,51,5],
["p1",2,"op","chess_char_5_diy2_a",1,null,1875,1,11,0,37968,0,0,56,3],
["p1",3,"op","chess_char_5_diy1_a",2,null,1875,1,1,8,77034,0,0,88,2],
["p1",4,"op","chess_char_5_diy2_b",0,"none",2290,1,3,5,126301,0,0,97,10],
["p1",5,"op","chess_char_5_diy1_b",1,"none",2290,1,5,3,143056,0,0,96,5],
["p1",6,"op","chess_char_5_diy2_b",2,"none",2290,1,7,1,420504,0,0,87,2],
["p1",1,"op","chess_char_5_diy1_a",0,null,1875,1,10,0,26812,0,0,51,5],
["p1",2,"op","chess_char_5_diy2_a",1,null,1875,1,11,0,43295,0,0,55,3],
["p1",3,"op","chess_char_5_diy1_a",2,null,1875,1,1,5,54590,0,0,87,2],
["p1",4,"op","chess_char_5_diy2_b",0,"none",2290,1,3,5,121247,0,0,96,9],
["p1",5,"op","chess_char_5_diy1_b",1,"none",2290,1,5,1,143603,0,0,86,5],
["p1",6,"op","chess_char_5_diy2_b",2,"none",2290,1,7,7,485647,0,0,86,2],
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_chen2",2290,1,9,0,0,0,0,0,0],
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_chen2",2290,1,12,0,115378,0,0,80,4],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_chen2",2290,1,79,4,35459,4580,0,26,1],
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_chen2",1116.3,1,80,1,58145,6159,0,32,3],
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_chen2",0,0,77,0,50242,7275,0,21,1],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_chen2",0,0,78,0,60357,7275,0,34,1]
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_chen2",2290,1,12,0,123659,0,0,82,4],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_chen2",0,0,72,2,43929,4580,0,23,1],
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_chen2",2425,1,79,2,16074,4850,0,28,2],
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_chen2",1116.3,1,78,2,82150,6159,0,38,3],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_chen2",0,0,77,3,65315,7275,0,32,1]
],
"enemies": {"enemy_1045_hammer_2":[12,0,12,12971,88407],"enemy_1061_zomshd":[48,23,24,12317,1128449]},
"hooks": [1,80,6900,699,699,4321,4321,47243,33,69,37,35,397,26577,0,12,60,36,1,33,0,0,0],
"events": {"atk":2237,"deploy":20,"die":33,"dmg":4321,"fx":107,"leak":36,"skill":72,"spawn":72},
"fx": {"reload":51,"zone":56},
"snaps": "cd5ab93f 0fdb1e7c 557181fd 2610c393 ffe004fb 79253f5d aacadc7f fca301fb 9f41cc07 88f28c30 1eed72e2 4f156152 051e0c90 2d42fdf6 b5c18831 5882c826 bb8dea4d 7d347742 a689f424 cf326f7e fc946b30 8ca74e36 d030e9c5"
"enemies": {"enemy_1045_hammer_2":[12,0,12,9835,121832],"enemy_1061_zomshd":[48,27,21,13029,1152890]},
"hooks": [1,79,6898,694,694,4275,4275,45863,36,69,37,33,392,26342,1,14,60,33,1,36,0,0,0],
"events": {"atk":2215,"deploy":19,"die":36,"dmg":4275,"fx":72,"leak":33,"skill":70,"spawn":72,"status":2},
"fx": {"reload":51,"zone":21},
"snaps": "cd5ab93f 2396c2a8 5077137f dd863ee2 d4b68780 9808f85e b478a497 d3cf815c 52e73b12 21f1f91b dcae9b46 2b6c86eb 72df1443 707243c1 9fa5d9a1 c2398758 d1d86183 f988258a 8172a2fe 50e548a7 b9a30dce c379a931 ad469f5c"
},
"diy-050": {
"about": "6_diy1_a=chen2/S1 6_diy2_a=chen2/S2 6_diy1_a=chen2/S3 6_diy2_b=chen2/S1 6_diy1_b=chen2/S2 6_diy2_b=chen2/S3 6_diy1_b=chen2/S1/002_chen2 6_diy2_b=chen2/S2/002_chen2 6_diy1_b=chen2/S3/002_chen2 6_diy2_b=chen2/S1/003_chen2 6_diy1_b=chen2/S2/003_chen2 6_diy2_b=chen2/S3/003_chen2",
@@ -2021,35 +2021,35 @@
"round": 13,
"seed": 574294847,
"waveId": "act1autochess_h06",
"time": 213.6,
"ticks": 6408,
"time": 213.57,
"ticks": 6407,
"reason": "cleared",
"killed": 9,
"total": 105,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 20,
"rngDraws": 21,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":9,"total":105,"perfect":false,"leaks":96,"leaksUncounted":0,"coins":0,"dmg":176875,"bossDmg":0,"heal":0,"deaths":20,"layerGains":{}}},
"players": {"p1":{"killed":9,"total":105,"perfect":false,"leaks":96,"leaksUncounted":0,"coins":0,"dmg":185004,"bossDmg":0,"heal":0,"deaths":20,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,1,1,4895,1875,0,7,1],
["p1",2,"op","chess_char_6_diy2_a",1,null,0,0,3,0,12046,1875,0,7,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,5,0,7430,1875,0,7,0],
["p1",4,"op","chess_char_6_diy2_b",0,"none",0,0,2,1,11540,2290,0,7,1],
["p1",5,"op","chess_char_6_diy1_b",1,"none",0,0,8,2,14249,2290,0,5,1],
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,1,0,3193,1875,0,6,0],
["p1",2,"op","chess_char_6_diy2_a",1,null,0,0,3,0,12580,1875,0,8,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,5,0,12686,1875,0,8,0],
["p1",4,"op","chess_char_6_diy2_b",0,"none",0,0,2,0,5688,2290,0,6,1],
["p1",5,"op","chess_char_6_diy1_b",1,"none",0,0,8,2,15978,2290,0,5,1],
["p1",6,"op","chess_char_6_diy2_b",2,"none",0,0,67,0,11342,4580,0,6,0],
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_002_chen2",0,0,65,0,5635,4580,0,6,0],
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_002_chen2",0,0,7,2,22739,2290,0,8,1],
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_002_chen2",0,0,7,2,29571,2290,0,8,1],
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_002_chen2",0,0,9,0,10530,2290,0,6,0],
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_003_chen2",0,0,111,0,14362,7410,0,11,0],
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_003_chen2",0,0,118,2,28476,7410,0,11,1],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_003_chen2",0,0,125,1,33633,7410,0,12,0]
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_003_chen2",0,0,125,4,32571,7410,0,13,1],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_003_chen2",0,0,118,1,30868,7410,0,12,0]
],
"enemies": {"enemy_1165_duhond":[51,9,42,4962,106064],"enemy_1174_duholy_2":[24,0,24,16928,28959],"enemy_1175_dushdo_2":[30,0,30,24285,41852]},
"hooks": [1,125,6408,148,148,298,298,0,29,125,6,6,20,5349,0,27,105,96,1,29,0,0,0],
"events": {"atk":302,"deploy":20,"die":29,"dmg":298,"fx":13,"leak":96,"skill":12,"spawn":117,"status":95},
"fx": {"reload":4,"zone":9},
"snaps": "bd64d67f f3df83b6 2b856b69 b6ac8170 0e517a9b f3c238f3 e39423d6 d8b530e9 7deb9725 9e8e1489 6b298bbe ca59d939 083a021d 65b35107 8994b3f8 ca994722 c744f8dc e6e50b34 b7c27ed6 31666d8b 37e1fde4 b8746a75"
"enemies": {"enemy_1165_duhond":[51,9,42,3408,101286],"enemy_1174_duholy_2":[24,0,24,16928,37448],"enemy_1175_dushdo_2":[30,0,30,25840,46269]},
"hooks": [1,125,6407,148,148,304,304,0,29,125,5,5,21,5444,0,25,105,96,1,29,0,0,0],
"events": {"atk":307,"deploy":20,"die":29,"dmg":304,"fx":8,"leak":96,"skill":10,"spawn":117,"status":93},
"fx": {"reload":4,"zone":4},
"snaps": "bd64d67f 89474186 f29e9c8c d2709b01 c98c193b 65062fc6 ce1a7762 7d13c1c1 b2dfa5bf 40376417 76ece10a e7c87d80 94d0f82e 780eb42e 38046d89 c8c7aa8d 5d373dc7 b3db1c31 32680bc2 5263ed68 de37dd9f e4371a62"
},
"diy-051": {
"about": "5_diy1_a=ray/S1 5_diy2_a=ray/S2 5_diy1_a=ray/S3 5_diy2_b=ray/S1 5_diy1_b=ray/S2 5_diy2_b=ray/S3 5_diy1_b=ray/S1/002_ray 5_diy2_b=ray/S2/002_ray 5_diy1_b=ray/S3/002_ray 5_diy2_b=ray/S1/003_ray 5_diy1_b=ray/S2/003_ray 5_diy2_b=ray/S3/003_ray",