diff --git a/server/sim/content/kits/tier3.js b/server/sim/content/kits/tier3.js index cbb7a8a2..1dfc9428 100644 --- a/server/sim/content/kits/tier3.js +++ b/server/sim/content/kits/tier3.js @@ -406,8 +406,8 @@ const KITS = { // a small push, radial despite the text's 向前 — PRTS 备注 "推开效果为径向推动"; client charpack char_1033_swire2: // the RandomGold ability (Skill_3_End) carries swire2_s_3[knockback] of template knockback[relative]; 地面敌方单位, // 弹道不可对空). It does not close itself when the purse is full (PRTS 卫戍协议/帮助 「通常不会自动关闭技能」; the skill - // text is 「可随时主动关闭」). Owner's decision 2026-10-04: the written cap (金币上限为10) is the close — at the cap the - // coins are shot at once (skill.end('manual')), with or without a mark. Below the cap it stays open. + // text is 「可随时主动关闭」). Owner 2026-10-04: at the cap (金币上限为10) she shoots once an enemy is in the skill's + // attack range (the 1-tile range, not the coin-mark 2-4 grid). Below the cap, or with nobody there, it stays open. // 精锐 module MER-Y: ATK +4 % per trait payment (≤ 5 stacks). chess_char_3_04_a: (bb, chess, def) => { const d = defOf(chess, def); @@ -506,11 +506,13 @@ const KITS = { return { kind: 'toggle', attack: { hits: 2 }, - // Owner 2026-10-04: 「金币上限为10」 is the close. Reaching the cap shoots the coins (the onEnd below). - // It does not wait for a mark — a cap with nobody in range still spends them and hits nothing. Below the - // cap the skill stays open (the old close also required a markable enemy; that wait is withdrawn). - onTick({ unit, skill }) { - if (skill.active && (unit.mem.coins ?? 0) + 1e-9 >= purseCap) skill.end('manual'); + // Owner 2026-10-04: at the cap, shoot once a ground enemy is in the skill attack range (data rangeGrid, + // the same 1-tile range as her attack). The coin marks (前方 2-4) are who the burst pays, not the trigger. + // Nobody in that range: stay open and keep the coins. Below the cap: stay open. + onTick({ battle, unit, skill }) { + if (!skill.active || (unit.mem.coins ?? 0) + 1e-9 < purseCap) return; + const keys = gridKeys(skillGrid ?? unit.rangeGrid, unit); + if (enemiesOn(battle, unit, keys, 0, { ...unit.profile, canHitFly: false }).length) skill.end('manual'); }, onEnd({ battle, unit, reason }) { if (reason !== 'manual' || !unit.alive) return; diff --git a/test/sim/feedback3-swire-s3.test.js b/test/sim/feedback3-swire-s3.test.js index 3e9803ec..518e21f1 100644 --- a/test/sim/feedback3-swire-s3.test.js +++ b/test/sim/feedback3-swire-s3.test.js @@ -1,6 +1,6 @@ -// test/sim/feedback3-swire-s3.test.js — 琳琅诗怀雅 S3 千金一掷. Owner 2026-10-04: the written purse cap -// (「金币上限为10」) is the close. At the cap the coins are shot (skill.end('manual'): damage + radial push). -// Below the cap the skill stays open. No mark still spends and hits nothing. +// test/sim/feedback3-swire-s3.test.js — 琳琅诗怀雅 S3 千金一掷. Owner 2026-10-04: at the purse cap of 10 +// she shoots once a ground enemy is in the skill attack range (the 1-tile range). Below the cap, or with +// the enemy only further ahead, the skill stays open and the coins stay. A manual close still spends. import { test } from 'node:test'; import assert from 'node:assert/strict'; @@ -11,11 +11,11 @@ const REAL = { skip: !hasGeneratedData() }; const ID = 'chess_char_3_04_a'; const foe = enemyRec({ key: 'e_sw', hp: 1e9, atk: 0, speed: 0, def: 0, res: 0, mass: 1 }); -function battle(withEnemy) { +function battle(pos) { return makeBattle({ defs: { enemies: { e_sw: foe } }, units: [{ chessId: ID, row: 10, col: 5, skillIndex: 2 }], - enemies: withEnemy ? [{ key: 'e_sw', pos: [10, 6] }] : [], + enemies: pos ? [{ key: 'e_sw', pos }] : [], hooks: ['damaged', 'skillEnd'], autoFinish: false, timeLimit: 30, }); } @@ -27,42 +27,43 @@ const arm = (h) => { return u; }; -test('琳琅诗怀雅 S3 shoots the purse when it reaches the cap of 10, and stays open below it', REAL, () => { - const under = battle(true); +test('琳琅诗怀雅 S3 shoots at the cap only once an enemy is in the skill attack range', REAL, () => { + const under = battle([10, 6]); const u9 = arm(under); u9.mem.coins = 9; - // shorter than the merchant trait's 3 s payment, which would add a coin and reach the cap under.run(2); assert.equal(u9.skill.active, true, '9 coins do not close it'); assert.equal(u9.mem.coins, 9); - const h = battle(true); + const far = battle([10, 7]); // two tiles ahead: outside the 1-tile attack range, inside nothing she swings at + const uf = arm(far); + uf.mem.coins = 10; + far.run(0.5); + assert.equal(uf.skill.active, true, 'an enemy outside the attack range does not make her shoot'); + assert.equal(uf.mem.coins, 10); + + const none = battle(null); + const un = arm(none); + un.mem.coins = 10; + none.run(0.5); + assert.equal(un.skill.active, true, 'nobody in range: the purse stays'); + assert.equal(un.mem.coins, 10); + + const h = battle([10, 6]); // one tile ahead: the skill attack range const u = arm(h); const e = h.enemies()[0]; const hp0 = e.hp; u.mem.coins = 10; h.run(0.2); - assert.equal(u.skill.active, false, 'the cap shoots'); + assert.equal(u.skill.active, false, 'the enemy in the attack range is shot'); assert.equal(u.mem.coins, 0, 'every coin is spent'); - assert.ok(e.hp < hp0, 'the coins hit the enemy in range'); - assert.equal(h.b.errors.length, 0); - checkInvariants(h.b); -}); - -test('琳琅诗怀雅 S3: the cap with no markable target still ends and spends, and does not throw', REAL, () => { - const h = battle(false); - const u = arm(h); - u.mem.coins = 10; - assert.doesNotThrow(() => h.run(0.2)); - assert.equal(u.skill.active, false); - assert.equal(u.mem.coins, 0); - assert.equal(h.hooksOf('damaged').length, 0, 'nothing to pay'); + assert.ok(e.hp < hp0, 'the coins hit that enemy'); assert.equal(h.b.errors.length, 0); checkInvariants(h.b); }); test('琳琅诗怀雅 S3: a manual close below the cap still spends', REAL, () => { - const h = battle(false); + const h = battle(null); const u = arm(h); u.mem.coins = 4; assert.doesNotThrow(() => u.skill.end('manual'));