diff --git a/server/sim/content/kits/ops/standin-acspec.js b/server/sim/content/kits/ops/standin-acspec.js index cea5a111..b52dfac9 100644 --- a/server/sim/content/kits/ops/standin-acspec.js +++ b/server/sim/content/kits/ops/standin-acspec.js @@ -12,8 +12,10 @@ import { bodyInKeys } from '../../../body.js'; const S1 = 'skchr_acspec_1'; const S2 = 'skchr_acspec_2'; const S3 = 'skchr_acspec_3'; -const S1_KEY = 'acspec:s1'; const PULL_TAG = 'acspec:pull'; +/** A 被动 ON_DEPLOY skill with a duration: a duration skill every deployment starts (no SP, never cast otherwise). */ +const ON_DEPLOY = Object.freeze({ kind: 'duration', activateOnDeploy: true, spCost: 0, spType: 'none', trigger: 'NEVER' }); +const s1On = (unit) => !!(unit.skill && unit.skill.active && unit.skill.id === S1); /** * 四维分离's "周围四格": her tile and the four tiles beside it [ASSUMED: her own tile counts — an enemy she blocks stands * on it, and the talent is about her being alone with one enemy]. @@ -63,9 +65,12 @@ export default { // attack@prob (PRTS 备注: it is her normal attack's basic logic; EXE-X levels 2 / 3 make it +atk ATK and a higher // chance). Talent 2 四维分离: ATK +atk while exactly `cnt` enemy stands on her tile or the four beside it. // All three skills are 被动, ON_DEPLOY — they start at every deployment (the battle-start one included, when no enemy - // is on the field yet: S3 then hits nothing, as in the official mode; it matters on a redeploy): - // S1 物理的服从: for the skill's duration (10 s) the talent-1 chance is attack@prob and physical dodge prob. - // S2 战争的恭顺: for the skill's duration (10 s) ATK +atk, ASPD +attack_speed. + // is on the field yet: S3 then hits nothing, as in the official mode; it matters on a redeploy). S1 / S2 last the + // skill's duration (10 s): duration skills started by the deployment (`activateOnDeploy`, the deploy-timed skill + // contract of PR #109 — their own skillStart / skillEnd and the client's skill on / off), not a passive holding a timed + // buff: + // S1 物理的服从: while it runs the talent-1 chance is attack@prob and physical dodge prob. + // S2 战争的恭顺: while it runs ATK +atk, ASPD +attack_speed. // S3 空间的归依: spaceBurst above (ground only), right after each deployment (install). // Module EXE-X (trait atk): ATK +atk while no ally stands on the four tiles beside her (PRTS: unlike the usual EXE-X; // shared/tier4.js lonely, as 缄默德克萨斯's module of the same text). @@ -81,11 +86,11 @@ export default { const cnt = Math.max(1, Math.floor(num(t1.cnt, 1))); return { skills: { - [S1]: { kind: 'passive', onStart({ battle, unit }) { battle.addBuff(unit, { key: S1_KEY, duration: num(r1?.duration, 10), mods: { dodgePhys: num(b1.prob) }, tags: ['skill'] }); } }, - [S2]: { kind: 'passive', onStart({ battle, unit }) { battle.addBuff(unit, { key: 'acspec:s2', duration: num(r2?.duration, 10), mods: { atkPct: num(b2.atk), aspd: num(b2.attack_speed) }, tags: ['skill'] }); } }, + [S1]: { ...ON_DEPLOY, duration: num(r1?.duration, 10), mods: { dodgePhys: num(b1.prob) } }, + [S2]: { ...ON_DEPLOY, duration: num(r2?.duration, 10), mods: { atkPct: num(b2.atk), aspd: num(b2.attack_speed) } }, [S3]: { kind: 'passive' }, }, - trait: { hitsFn: (battle, unit) => (battle.rng.chance(unit.findBuff(S1_KEY) ? s1Prob : talentProb) ? 2 : 1) }, + trait: { hitsFn: (battle, unit) => (battle.rng.chance(s1On(unit) ? s1Prob : talentProb) ? 2 : 1) }, talents: [ { install(battle, unit) { statBuff(battle, unit, 'acspec:t1', { atkPct: num(t0.atk) }); } }, { install(battle, unit) { toggleBuff(battle, unit, 'acspec:t2', () => enemiesAround4(battle, unit) === cnt, { atkPct: num(t1.atk) }); } }, diff --git a/test/content/standin_acspec.test.js b/test/content/standin_acspec.test.js index 3881910d..5c54b705 100644 --- a/test/content/standin_acspec.test.js +++ b/test/content/standin_acspec.test.js @@ -2,7 +2,9 @@ // server/sim/content/kits/ops/standin-acspec.js) on every chess she replaces: 缄默德克萨斯 (tier 4, S2), 山 (tier 5, S3 + // EXE-X), 新约能天使 / 锏 (tier 6, S3 + EXE-X), normal (E2 Lv1, skill 4) and elite (E2 Lv60, skill 7). Her three skills // are ON_DEPLOY passives: they start at every deployment — at the battle start no enemy is on the field yet, so S3 hits -// nothing there and matters on a redeploy (再部署时间大幅度减少: 18 s). Numbers are read back from data/backups.json. +// nothing there and matters on a redeploy (再部署时间大幅度减少: 18 s). S1 / S2 last 10 s: since 0.2.0 they are duration skills +// started by every deployment (activateOnDeploy — PR #109's deploy-timed contract, their own skillStart / skillEnd), no +// longer a passive holding a 10 s buff. Numbers are read back from data/backups.json. // Run: node --test test/content/standin_acspec.test.js import { test } from 'node:test'; @@ -26,7 +28,7 @@ const ALLY = 'chess_char_1_02_a'; // 角峰, a melee blocker function battle(units, o = {}) { return makeBattle({ defs: { enemies: ENEMIES }, units, timeLimit: o.timeLimit ?? 400, autoFinish: false, seed: o.seed ?? 5, - flags: { dpPerSec: 0 }, hooks: ['damaged', 'deploy', 'death'], captureNoisy: true, + flags: { dpPerSec: 0 }, hooks: ['damaged', 'deploy', 'death', 'skillStart', 'skillEnd'], captureNoisy: true, }); } const MISERY = (id, standIn = true) => ({ chessId: id, row: 10, col: 4, standIn }); @@ -38,7 +40,7 @@ function done(h) { assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0])); } -test('Misery on every chess she replaces (normal + elite): her body (再部署 18 s), the backup skill as a passive, this kit; EXE-X on the tier-5 / 6 elites only', () => { +test('Misery on every chess she replaces (normal + elite): her body (再部署 18 s), the backup skill (S2 a deploy-timed duration skill, S3 a passive), this kit; EXE-X on the tier-5 / 6 elites only', () => { const ids = UNIT.standsIn.flatMap((a) => [a, a.replace(/_a$/, '_b')]); assert.equal(ids.length, 8); const withModule = []; @@ -48,7 +50,8 @@ test('Misery on every chess she replaces (normal + elite): her body (再部署 1 h.step(); const u = h.unit(1); const sk = form.skills.find((s) => s.index === c.backup.skillIndex); - assert.deepEqual([u.def.charId, u.def.standInFor, u.skill.id, u.skill.kind, !!u.kit.generic, u.kit.skillSource], [CHAR, c.charId, sk.skillId, 'passive', false, 'skills'], id); + const kind = sk.skillId === 'skchr_acspec_3' ? 'passive' : 'duration'; + assert.deepEqual([u.def.charId, u.def.standInFor, u.skill.id, u.skill.kind, !!u.kit.generic, u.kit.skillSource], [CHAR, c.charId, sk.skillId, kind, false, 'skills'], id); assert.deepEqual([sk.skillType, sk.spType], ['PASSIVE', 'ON_DEPLOY'], id); const mod = c.backup.uniEquipId && c.status.equipLevel > 0 ? form.modules.find((m) => m.uniEquipId === c.backup.uniEquipId) : null; assert.equal(!!u.def.raw.module?.active, !!mod, `${id}: module`); @@ -137,7 +140,7 @@ test('Misery S1 物理的服从 (no chess names it — a 自选 slot may): for 1 const h = battle([MISERY(id, { skillIndex: 0 })]); h.step(); const u = h.unit(1); - assert.deepEqual([u.skill.id, u.skill.kind, sk.duration], ['skchr_acspec_1', 'passive', 10]); + assert.deepEqual([u.skill.id, u.skill.kind, u.skill.duration, sk.duration], ['skchr_acspec_1', 'duration', 10, 10]); const prob = () => { let p = null; u.profile.hitsFn({ rng: { chance: (x) => { p = x; return false; } } }, u); return p; }; const on = () => { approx(u.s.dodgePhys, bb.prob, `${id}: dodge ${bb.prob}`); assert.equal(prob(), bb['attack@prob'], `${id}: chance ${bb['attack@prob']}`); }; const off = () => { assert.equal(u.s.dodgePhys, 0, `${id}: no dodge`); assert.equal(prob(), talentBb(u, 0)['attack@prob'], `${id}: the talent's chance`); }; @@ -162,9 +165,13 @@ test('Misery S2 战争的恭顺 (缄默德克萨斯): for 10 s from every deploy const h = battle([MISERY(id)]); h.step(); const u = h.unit(1); - assert.deepEqual([u.skill.id, sk.duration], ['skchr_acspec_2', 10]); - const on = () => { approx(u.s.atk, u.base.atk * (1 + bb.atk), `${id}: ATK +${bb.atk}`); assert.equal(u.s.aspd, 100 + bb.attack_speed, `${id}: ASPD`); }; - const off = () => { approx(u.s.atk, u.base.atk, `${id}: ATK back`); assert.equal(u.s.aspd, 100, `${id}: ASPD back`); }; + assert.deepEqual([u.skill.id, u.skill.kind, u.skill.duration, sk.duration], ['skchr_acspec_2', 'duration', 10, 10]); + // the deploy-timed contract (PR #109): the skill runs — skillStart now, skillEnd 10 s later — and then is off + const on = () => { approx(u.s.atk, u.base.atk * (1 + bb.atk), `${id}: ATK +${bb.atk}`); assert.equal(u.s.aspd, 100 + bb.attack_speed, `${id}: ASPD`); assert.ok(u.skill.active, `${id}: running`); }; + const off = () => { approx(u.s.atk, u.base.atk, `${id}: ATK back`); assert.equal(u.s.aspd, 100, `${id}: ASPD back`); assert.ok(!u.skill.active, `${id}: over`); }; + const starts = () => h.hooksOf('skillStart').filter((c) => c.unit === u).length; + const ends = () => h.hooksOf('skillEnd').filter((c) => c.unit === u).length; + assert.deepEqual([starts(), ends()], [1, 0], `${id}: started by the deployment`); on(); h.run(9.8); on(); @@ -176,6 +183,7 @@ test('Misery S2 战争的恭顺 (缄默德克萨斯): for 10 s from every deploy on(); h.run(10.1); off(); + assert.deepEqual([starts(), ends()], [2, 2], `${id}: one window per deployment (the retreat ends none — it had ended)`); done(h); } }); diff --git a/test/golden/standins.json b/test/golden/standins.json index 7ea7413d..fd467fba 100644 --- a/test/golden/standins.json +++ b/test/golden/standins.json @@ -48,7 +48,7 @@ ["p1",4,"op","chess_char_3_18_a",2,null,2638,1,7,2,32526,8770,0,75,2], ["p1",5,"op","chess_char_4_04_a",1,null,1264.1,1,91,2,17806,8706,0,100,9], ["p1",6,"op","chess_char_4_07_a",1,null,0,0,74,1,16008,10455,0,37,1], - ["p1",7,"op","chess_char_4_16_a",1,null,807.8,1,92,1,9136,11490,0,98,0], + ["p1",7,"op","chess_char_4_16_a",1,null,807.8,1,92,1,9136,11490,0,98,14], ["p1",8,"op","chess_char_3_01_a",2,null,1271,1,5,0,15937,0,0,131,2], ["p1",9,"op","chess_char_3_04_a",2,null,1218,1,8,0,0,0,26765,59,3], ["p1",10,"op","chess_char_3_21_a",2,null,161.7,1,6,1,42388,1110,0,121,2], @@ -68,10 +68,10 @@ "enemy_1119_vofsd": [12,6,6,19166,93491], "enemy_1121_lifbos_2": [9,1,7,9882,35401] }, - "hooks": [1,93,5100,939,939,1515,1509,3842,41,82,27,38,0,22264,26,41,50,27,1,41,5,0,6], - "events": {"atk":1088,"bounty":2,"deploy":43,"die":55,"dmg":1509,"engage":11,"fx":61,"heal":59,"layer":5,"leak":27,"skill":52,"spawn":62,"status":123}, + "hooks": [1,93,5100,939,939,1515,1509,3842,41,82,41,39,0,22264,26,41,50,27,1,41,5,0,6], + "events": {"atk":1088,"bounty":2,"deploy":43,"die":55,"dmg":1509,"engage":11,"fx":61,"heal":59,"layer":5,"leak":27,"skill":80,"spawn":62,"status":123}, "fx": {"blink":14,"dodge":7,"dp":5,"garrison":5,"liberate":13,"slash":16,"telegraph":1}, - "snaps": "f6b212e6 305ae509 ec53440d 68686487 36b691ce 6a3a0f75 3bf73b20 56836b06 21e8ab42 092dab80 ab8cb21f 0d51c9c6 b1e5b425 ac801d55 57850eec ee889f70 5b8b495b" + "snaps": "8b8be66d 3d41101a 548df15a 1009f8b0 08d13c52 9fe4b099 64799759 70e168de 0b87fdee 880a5dbc 4750bf5e 71d1cbac a6d3a940 eda8aa0d 5dec048f 8b7b727d 0b96370a" }, "standin-02": { "about": "normal band 5 as stand-ins: chess_char_4_17_a→char_610_acfend chess_char_4_18_a→char_602_cdfend chess_char_5_05_a→char_602_cdfend chess_char_4_19_a→char_608_acpion chess_char_5_01_a→char_600_cpione chess_char_4_22_a→char_609_acguad chess_char_4_23_a→char_609_acguad chess_char_5_06_a→char_609_acguad chess_char_4_20_a→char_603_csnipe chess_char_4_24_a→char_604_ccast chess_char_5_02_a→char_604_ccast chess_char_5_03_a→char_612_accast", @@ -334,7 +334,7 @@ ["p1",4,"op","chess_char_3_18_b",2,"none",0,0,93,3,28745,9190,2808,27,2], ["p1",5,"op","chess_char_4_04_b",1,"none",0,0,137,0,25476,10224,0,40,9], ["p1",6,"op","chess_char_4_07_b",1,"none",0,0,101,3,28031,10981,0,27,1], - ["p1",7,"op","chess_char_4_16_b",1,"none",0,0,139,7,42175,16104,0,70,0], + ["p1",7,"op","chess_char_4_16_b",1,"none",0,0,139,7,42175,16104,0,70,12], ["p1",8,"op","chess_char_3_01_b",2,"none",0,0,122,1,16716,3020,0,24,1], ["p1",9,"op","chess_char_3_04_b",2,"none",0,0,4,0,0,15932,26703,25,2], ["p1",10,"op","chess_char_3_21_b",2,"none",0,0,118,12,175716,7911,0,55,1], @@ -354,10 +354,10 @@ "enemy_1252_lysytb_2": [21,8,13,29953,226804], "enemy_1329_cbshld_2": [12,1,11,40556,69057] }, - "hooks": [1,140,5511,596,596,875,906,94,81,139,25,37,0,7322,36,60,98,48,1,89,7,0,4], - "events": {"atk":684,"bounty":4,"deploy":42,"die":91,"dmg":906,"engage":11,"fx":62,"heal":67,"layer":7,"leak":48,"skill":50,"spawn":110,"status":117}, + "hooks": [1,140,5511,596,596,875,906,94,81,139,37,37,0,7322,36,60,98,48,1,89,7,0,4], + "events": {"atk":684,"bounty":4,"deploy":42,"die":91,"dmg":906,"engage":11,"fx":62,"heal":67,"layer":7,"leak":48,"skill":74,"spawn":110,"status":117}, "fx": {"blink":10,"camouflage":1,"dodge":6,"dp":13,"explode":1,"garrison":7,"slash":24}, - "snaps": "accd1698 74126f20 63d105c2 6f46af20 41b2aaf8 62757331 5ee97ef6 6331a731 2b18e7ef 8540aa5c 5e961254 ad125465 3ac7012a 04cc07f3 968df17c 2499c755 43a6f5dc 312679f4 5b9c22ce" + "snaps": "be6d1821 fa43a30d 52099f77 a860ccb6 ea311a6e 44affa90 9dabf4d8 83adbc77 77dfae35 f02fd736 470fba6a a9871a60 b7e054aa ae8c4b73 55df9303 1c374baa 0d780d78 e03a2b60 b7879d42" }, "standin-07": { "about": "elite band 6 as stand-ins: chess_char_4_17_b→char_610_acfend chess_char_4_18_b→char_602_cdfend chess_char_5_05_b→char_602_cdfend chess_char_4_19_b→char_608_acpion chess_char_5_01_b→char_600_cpione chess_char_4_22_b→char_609_acguad chess_char_4_23_b→char_609_acguad chess_char_5_06_b→char_609_acguad chess_char_4_20_b→char_603_csnipe chess_char_4_24_b→char_604_ccast chess_char_5_02_b→char_604_ccast chess_char_5_03_b→char_612_accast",