From a59ba07dff077bace43b7a666d8eac22d6f3c832 Mon Sep 17 00:00:00 2001 From: sganggs Date: Tue, 6 Oct 2026 18:22:44 +0800 Subject: [PATCH] =?UTF-8?q?feedback5=20WV:=20=E5=BF=8D=E5=86=AC,=20?= =?UTF-8?q?=E6=B7=B1=E9=9D=9B=20and=20=E9=98=BF=20settled=20by=20the=20sou?= =?UTF-8?q?rces=20with=20no=20rule=20change=20=E2=80=94=20PRTS's=20"?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E9=80=89=E4=B8=AD=E8=A2=AB=E8=87=AA=E8=BA=AB?= =?UTF-8?q?=E9=98=BB=E6=8C=A1=E8=80=8C=E6=B2=A1=E6=9C=89=E4=BD=8D=E4=BA=8E?= =?UTF-8?q?=E6=8A=80=E8=83=BD=E8=8C=83=E5=9B=B4=E5=86=85=E7=9A=84=E6=95=8C?= =?UTF-8?q?=E4=BA=BA"=20is=20=E5=BF=8D=E5=86=AC=20S2's=20=E5=A4=87?= =?UTF-8?q?=E6=B3=A8=20(its=20kit=20selects=20on=20the=203-12=20only;=20S3?= =?UTF-8?q?=20has=20no=20such=20note),=20=E6=B7=B1=E9=9D=9B=20S1's=20?= =?UTF-8?q?=E6=94=BB=E5=87=BB=E5=80=8D=E7=8E=87=20applies=20to=20her=20sto?= =?UTF-8?q?red=20energies=20(PRTS=20S1=20=E5=A4=87=E6=B3=A8,=20the=20clien?= =?UTF-8?q?t's=20ChargeAttackS1=20atk=5Fscale),=20=E9=98=BF=20S2=20/=20S3?= =?UTF-8?q?=20keep=20the=20data's=20DEFAULT=20(no=20trigger=20row,=20the?= =?UTF-8?q?=20skill=20prefabs=20cast=20with=20no=20target=20and=20keep=20t?= =?UTF-8?q?he=20SP);=20kit=20comments=20and=20two=20pin=20tests,=20golden?= =?UTF-8?q?=20unchanged?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kits/ops/chess_char_1_17-indigo.js | 3 +- .../kits/ops/chess_char_3_18-vulpis.js | 4 +- server/sim/content/kits/ops/op-haak.js | 4 +- test/content/feedback5-wv-sources.test.js | 70 +++++++++++++++++++ 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 test/content/feedback5-wv-sources.test.js diff --git a/server/sim/content/kits/ops/chess_char_1_17-indigo.js b/server/sim/content/kits/ops/chess_char_1_17-indigo.js index f42ae20d..632bdc93 100644 --- a/server/sim/content/kits/ops/chess_char_1_17-indigo.js +++ b/server/sim/content/kits/ops/chess_char_1_17-indigo.js @@ -11,7 +11,8 @@ export default { // 柔光缚目: attacks bind the target `duration` s with `prob`; bound enemies are never chosen as targets. // Alternate S1 灯塔守卫者: for `duration` s the skill range (4-1), attack interval ×(1 + base_attack_time) (PRTS: // "大幅度缩短(-80%)" ⇒ ×0.2 — a flat −0.8 s on 3 s would be less than S2's "略微缩短 ×0.7"), every attack hits for - // attack@atk_scale × ATK arts. Elite module MSC-X (store 4) is the mystic profile of the module trait (TUNE.mystic); + // attack@atk_scale × ATK arts — its stored energies too (PRTS S1 备注 "该技能的"每次攻击的攻击倍率"会实时应用在特性积攒的"攻击 + // 能量"抛射物上"; the client's ChargeAttackS1 reads the same atk_scale), not the branch's general 100 %. Elite module MSC-X (store 4) is the mystic profile of the module trait (TUNE.mystic); // the store itself is the shared 秘术师 profile (professions.js installMystic: at the attack check, PRTS 分支特性信息). chess_char_1_17_a: (bb, chess, def) => { const t = talentBb(chess, 0); diff --git a/server/sim/content/kits/ops/chess_char_3_18-vulpis.js b/server/sim/content/kits/ops/chess_char_3_18-vulpis.js index a7ce204d..1e8b77e6 100644 --- a/server/sim/content/kits/ops/chess_char_3_18-vulpis.js +++ b/server/sim/content/kits/ops/chess_char_3_18-vulpis.js @@ -20,7 +20,9 @@ export default { // regen; 精锐 module SOL-X: ATK/DEF + while blocking // S1 小施惩戒 (charges): next attack + extra_damage_ratio × ATK arts and +cost DP; S2 坠刃拷问 (charges, the data rule // SKILL_RANGE: fires once an enemy is inside its own range 3-12, charges 3 s apart): +cost DP, ≤ max_target enemies of that range take atk_scale arts and - // 停顿 — the ones already 停顿 are also stunned. (SOL-Y "首次部署时部署费用-4": the initial deployment is free.) + // 停顿 — the ones already 停顿 are also stunned; its targets come from the 3-12 only (PRTS S2 备注 "可对空;无法选中被自身 + // 阻挡而没有位于技能范围内的敌人" — an S2 note, not S3's: S3 隐狐之艺 has none, its client selector is the standard + // 「同时攻击阻挡的所有敌人」 one). (SOL-Y "首次部署时部署费用-4": the initial deployment is free.) chess_char_3_18_a: (bb, chess, def) => { const d = defOf(chess, def); const t0 = talentBb(d, 0), t1 = talentBb(d, 1); diff --git a/server/sim/content/kits/ops/op-haak.js b/server/sim/content/kits/ops/op-haak.js index 3228e946..b8c8941b 100644 --- a/server/sim/content/kits/ops/op-haak.js +++ b/server/sim/content/kits/ops/op-haak.js @@ -43,7 +43,9 @@ // +attack_speed (S3) for the skill's duration (per 阿 on the target: independentCharacterSource). [ASSUMED] the volley // lands at once (its 0.42 s of shots and flight are not modelled); with no ally in his range the cast does nothing (the // client's sequence stops when its attack finds no target) — his skill still runs and spends its SP: the data's DEFAULT -// trigger needs an enemy, not an ally (reported as an open question). +// trigger needs an enemy, not an ally — settled by the sources in 0.2.0 WV: no skillTriggerDataList row names 阿 or 怪杰 +// (the basic strategy), the skill prefabs cast with no target and keep the SP spent (skchr_haak_2 / _3 `_allowNoTarget` +// 1, `_checkHasTargetBeforeDoCast` 0, `_recoverSpIfNoTarget` 0) and PRTS 阿 has no note on it. import { num, talentBb, moduleBb, moduleOn, skillRec, statBuff, toggleBuff, up } from '../shared/tier1.js'; import { toLocal } from '../../../dir.js'; diff --git a/test/content/feedback5-wv-sources.test.js b/test/content/feedback5-wv-sources.test.js new file mode 100644 index 00000000..7a7390ea --- /dev/null +++ b/test/content/feedback5-wv-sources.test.js @@ -0,0 +1,70 @@ +// test/content/feedback5-wv-sources.test.js — 0.2.0 WV #4: the questions settled by the sources with no rule change, pinned. +// * 忍冬 — PRTS's "无法选中被自身阻挡而没有位于技能范围内的敌人" is the 备注 of S2 坠刃拷问 (技能范围 3-12; WE1 read it as S3's): its +// kit selects on the skill range only, so an enemy she blocks off that range is never one of its targets. S3 has no such +// note (its client selector is the standard 「同时攻击阻挡的所有敌人」 one). +// * 深靛 S1 灯塔守卫者 — PRTS 深靛 S1 备注 "该技能的"每次攻击的攻击倍率"会实时应用在特性积攒的"攻击能量"抛射物上" (the client's +// ChargeAttackS1 reads the same `atk_scale`): an energy she stored lands with S1's attack@atk_scale × ATK, not the +// branch's general 100 % (PRTS 分支特性信息 秘术师 "由储存能量形成的弹道造成攻击力100%的法术普通伤害"). +// (阿 S2 / S3 — no ally condition in the data: no skillTriggerDataList row for him or 怪杰, the skill prefab casts with no +// target and keeps the SP spent — is pinned by test/content/op_haak.test.js "爆发剂 edge cases".) +// Run: node --test test/content/feedback5-wv-sources.test.js + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js'; +import { getDefaultSource } from '../../server/sim/simdata.js'; + +const ds = getDefaultSource(); +const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o }); +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 done = (h) => { checkInvariants(h.b); assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0])); }; +const skillIndexOf = (id, skillId) => ds.rawChess(id).skills.find((s) => s.skillId === skillId).index; + +test('#4 忍冬 S2 坠刃拷问 (PRTS 备注 "无法选中被自身阻挡而没有位于技能范围内的敌人"): an enemy she blocks off her 3-12 is never hit; the one inside is', () => { + for (const id of ['chess_char_3_18_a', 'chess_char_3_18_b']) { + const h = makeBattle({ + seed: 3, autoFinish: false, timeLimit: 60, hooks: ['damaged'], captureNoisy: true, defs: { enemies: { e: dummy('e') } }, + units: [{ chessId: id, row: 10, col: 5, skillIndex: skillIndexOf(id, 'skchr_vulpis_2') }], + }); + h.step(); + const u = h.unit(id); + const behind = h.spawn('e', { pos: [10, 4] }); + behind.x = 4.4; // in contact (0.6 < 0.71) but on the tile behind her: off the 3-12 + h.step(); + assert.equal(behind.blockedBy, u, `${id}: she blocks it`); + const inside = h.spawn('e', { pos: [10, 7] }); + h.step(); + assert.ok(u.skill.activate('test', { free: true }), `${id}: cast`); + h.step(); + const hit = h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.tags?.includes('vulpisTorture')); + assert.ok(hit.some((c) => c.target === inside), `${id}: the enemy inside the 3-12`); + assert.ok(!hit.some((c) => c.target === behind), `${id}: not the blocked one off the skill range`); + done(h); + } +}); + +test('#4 深靛 S1 灯塔守卫者 (PRTS 备注): the energies she stored land with S1\'s attack@atk_scale × ATK, like its main hit', () => { + const id = 'chess_char_1_17_a'; + const r = ds.rawChess(id); + const rec = { ...r, talents: r.talents.map((t) => ({ ...t, bb: { ...t.bb, prob: 0 } })) }; // no random 柔光缚目 bind + const h = makeBattle({ + seed: 3, autoFinish: false, timeLimit: 120, hooks: ['damaged', 'skillStart'], captureNoisy: true, + defs: { chess: { [id]: rec }, enemies: { e: dummy('e') } }, + units: [{ chessId: id, row: 10, col: 4, skillIndex: skillIndexOf(id, 'skchr_indigo_1') }], + }); + h.step(); + const u = h.unit(id); + const scale = ds.getChess(id, { skillIndex: skillIndexOf(id, 'skchr_indigo_1') }).skill.bb['attack@atk_scale']; + assert.ok(scale > 0 && scale < 1, 'S1 strikes below 100 %'); + assert.ok(h.runUntil(() => u.trait.stored === 3, 15), 'three energies stored with no target'); + u.skill.gainSp(999); + h.spawn('e', { pos: [10, 6] }); + assert.ok(h.runUntil(() => u.skill.active, u.s.interval + 1), 'S1 cast at her next attack check'); + h.run(1); + const first = h.hooksOf('damaged').filter((c) => c.source === u && c.dmg.isAttack); + const id0 = first[0].dmg.attackId; + const volley = first.filter((c) => c.dmg.attackId === id0); + assert.equal(volley.length, 1 + 3, 'the S1 attack + its 3 energies'); + for (const c of volley) approx(c.amount, u.s.atk * scale, 'S1\'s 攻击倍率 on each'); + done(h); +});