修复赤刃明霄陈三技能对空自动开启并核实社区反馈
S3 的扩展范围含可选中的飞行目标时可以自动开启剑气;普攻保持仅对地。 [ASSUMED] 自动触发区域沿用扩展攻击格和永久射程延长,不扩张到未来剑气的整条路线。 补测部署技能与休整属性预览、后排协防蕾缪安真实锁定耗弹、诗怀雅落地金币、 莎草对炸弹的治疗链,以及予愿三种技能的普通地面溅射和无视可选性例外。 未复现的报告保留证据边界;诗怀雅被动落地投瓶符合 PRTS。 全 285 场 golden 重建并验证,仅 diy-074 变化:新增 4 次陈 S3,伤害 142376→163563, 时序/选敌改变后击杀 21→18、漏怪 96→99,223.83 秒和结束方式不变。 其余 284 场不变。 CI:5879 tests,5876 pass,0 fail,3 skip;setup/smoke/lint/imports/typecheck 均通过。 新增 16 项回归,相关 6 文件 63/63,补充 2 文件 20/20,文档 31/31。
This commit is contained in:
1 parent
708caf7d06
commit
821ab827bd
9 files changed
+211
-14
No files matched your search
+3
-1
@@ -953,7 +953,9 @@ flag alters movement or 失衡.
|
||||
heal skills: 海嗣 "攻击范围视为自身攻击范围的延伸", 流形, 谬因 S2's beam, and the areas a skill acts through around its
|
||||
owner's standing summons — 麦哲伦 S1 (her drones' ranges), 令 S3 (each summon's x-5, ground), 电弧 S2 (赛柯's range,
|
||||
ground) / S3 (桑特拉's range): the owner's larger-range rule of 2026-10-06, kits/shared/summoner.js
|
||||
`summonTriggerArea`). With `{ attackOnly: true }`, the extra range is checked only at the normal attack opportunity, not each tick: 蕾缪安’s own wanted targets and 死芒’s own living summons’ blocked targets (S1/S2); normal selectability and the skill’s actual effect range remain intact (§28.13). `SKILL_RANGE` — "不通过普通攻击/治疗触发技能,仅在技能范围内存在敌人(无视其不可选中)时释放技能": any
|
||||
`summonTriggerArea`). 赤刃明霄陈 S3 uses an air-capable content range on its expanded grid for `ACTIVE_RANGE` too
|
||||
(§28.26): the wave can hit flyers, its normal attacks remain ground-only. [ASSUMED] This activation area includes
|
||||
permanent range extension but not the wave's eventual route. With `{ attackOnly: true }`, the extra range is checked only at the normal attack opportunity, not each tick: 蕾缪安’s own wanted targets and 死芒’s own living summons’ blocked targets (S1/S2); normal selectability and the skill’s actual effect range remain intact (§28.13). `SKILL_RANGE` — "不通过普通攻击/治疗触发技能,仅在技能范围内存在敌人(无视其不可选中)时释放技能": any
|
||||
living enemy (stealthed, untargetable, flying included) on `trigger.customRangeGrid` (= the skill's rangeGrid), every
|
||||
tick, no attack needed; `trigger.allies` (+ `hpAtMost`, default 1) — set by a kit, or by the data for an ally row (黍 S3's
|
||||
official `TRY_SEARCH_ALLY_SKILL`, "技能范围内存在可治疗的我方单位时释放技能": tools/build-data.mjs `TRIGGER_ALLY_RULES`,
|
||||
|
||||
@@ -198,6 +198,11 @@ living summons owned by that operator. Sleeping or otherwise unselectable enemie
|
||||
the condition for activation: 死芒 S2 still links inside its own effect range. Other registered summon areas retain
|
||||
the existing per-tick behavior. The optional flag defaults to false for compatibility.
|
||||
|
||||
赤刃明霄陈 S3 also registers selectable air targets in its expanded attack grid for `ACTIVE_RANGE` automation
|
||||
(§28.26): its sword wave can hit flyers while its normal attacks remain ground-only. The existing operation cooldown,
|
||||
silence and control checks apply. [ASSUMED] The activation area is that grid plus permanent attack-range extension,
|
||||
not every tile the roaming wave might eventually reach.
|
||||
|
||||
|
||||
0.2.3 echo/counter interaction (DESIGN §28.24): recursively earned “余音” pulses are drained FIFO by that echo in the
|
||||
same call, retaining the earned attack value and the lethal-hit pulse. [ASSUMED] This local order avoids exhausting
|
||||
|
||||
@@ -395,3 +395,25 @@ Full golden regeneration covers all 285 scenarios. Four digests intentionally mo
|
||||
|
||||
No scenario's ending reason changes. The bonds, matches and DIY families are byte-identical to the preceding golden
|
||||
files. The 50-second setting and fallback-message change do not alter battle rules in those digests.
|
||||
|
||||
### 28.26 Further community reports: deployment, air targets and champagne
|
||||
|
||||
Five reports were checked against the current candidate and PRTS on 2026-10-10. Reports without an operator,
|
||||
phase or enemy identity remain bounded by the scenarios actually tested.
|
||||
|
||||
| Report | Evidence and result |
|
||||
|---|---|
|
||||
| Deployment skill counts down before combat | Not reproduced in the battle lifecycle or real match prep-stat path. Constructing a battle does not deploy units; starting it activates deployment skills. Prep previews start their own battle for initial stats, never step it, send no SP/skill timer and cannot spend the next battle's duration. Tests cover 予愿安洁莉娜 S1 and repeated prep previews with 缄默德克萨斯. This does not identify the unspecified UI/phase in the report. |
|
||||
| 赤刃明霄陈 will not open S3 against flyers | Reproduced and fixed. `ACTIVE_RANGE` checked her ground-only normal-attack profile, although the sword wave can hit air ([PRTS](https://prts.wiki/w/赤刃明霄陈)). S3 now registers air-capable targeting on its expanded attack grid. Normal swings remain ground-only; operation cooldown, stealth/sleep selectability and stun still apply. [ASSUMED] The auto-cast region follows the existing expanded-grid policy, including permanent range extension, rather than the wave's whole future route. This is an automation choice, not a claim about the official mode's automatic-cast rule. |
|
||||
| Rear player's 蕾缪安 does not open S3 on front player's targets | Already handled by §28.4/§28.13. Added normal/elite united-field cases with the second player's 蕾缪安 and the first player's distant Laterano operator: the real wanted timer completes, S3 starts automatically, locks that enemy and spends ammo. The existing all-skill tests retain attack cadence and selectability. |
|
||||
| 琳琅诗怀雅 S2 immediately creates a bottle on deployment | Expected. [PRTS](https://prts.wiki/w/琳琅诗怀雅) identifies S2 as passive, deployment with S1/S2 as skill activation, and her talent as granting a coin on activation. The first bomb spends that coin without waiting for a merchant payment. No new SP cooldown is added. Existing throw-animation timing remains an assumption. |
|
||||
| 莎草 shields a champagne bomb | Not reproduced on the current candidate, which already gives bombs 禁疗 under the maintainer's earlier decision. Both skills actually heal/shield an adjacent operator but never chain to the bomb produced by 琳琅诗怀雅. [PRTS 莎草](https://prts.wiki/w/莎草) also excludes 禁疗 from S2's main-target selection. |
|
||||
| 予愿安洁莉娜 dies to splash while airborne | All three skills avoid ordinary ground 控潮术师 splash while airborne and take it after landing. A 萨卡兹悖谬暴虐兵长 splash explicitly ignoring target selectability still damages her, as expected. 起飞's 对地规避 is a selectability effect, not immunity to every damage source ([PRTS 异常效果](https://prts.wiki/w/异常效果), [暴虐兵长](https://prts.wiki/w/萨卡兹悖谬暴虐兵长)). Without the reported enemy's identity these checks cannot settle that particular encounter. No global immunity change. |
|
||||
|
||||
All 285 golden scenarios were regenerated and verified. Only `diy-074` moves: the mixed 陈 loadout against
|
||||
flying enemies now starts four previously idle S3 activations and emits sword-wave effects. Damage rises
|
||||
142376 → 163563; changed combat/target timing moves kills 21 → 18 and leaks 96 → 99, with the same 223.83 s
|
||||
clear time and end reason. All other 284 scenarios, including the five other golden families, are unchanged.
|
||||
The focused checks cover all four tier/elite forms, selectability/control/cooldown boundaries, actual casts,
|
||||
healing chains and enemy splash. Full CI passes: 5879 tests, 5876 passed, 0 failed, 3 skipped; setup, smoke, lint,
|
||||
imports and typecheck pass. This round changes no production UI and does not rerun the opt-in browser suites.
|
||||
@@ -87,7 +87,8 @@ export class MatchIntents {
|
||||
* band and 机变 effects — computed exactly by the shared sim. The player's battle input after the onBattleStart meta
|
||||
* handlers (`ev.preview: true`, no enemies — those handlers must not change the match for a preview) builds a Battle
|
||||
* of the battle's options that is started (initial deployment + battleStart hooks), read and dropped: it is never
|
||||
* stepped, so skills and timed effects do not show. Pushed to the player as `m.unitStats { seq, round, units }`
|
||||
* stepped: deployment skills contribute their initial stats but their timers do not advance. No SP / skill timer is
|
||||
* sent, and the real battle gets fresh units. Pushed to the player as `m.unitStats { seq, round, units }`
|
||||
* (units: shared/protocol.js unitStatsEntry, board operators and summons by uid); cached per input (a build costs
|
||||
* ≈ 0.3–0.7 ms). Prep phases only (ROUND_START, 机变, PREP); a battle's live stats come from the browser's own sim.
|
||||
* @param {PlayerState} ps @param {number|null} seq echoed (the client keeps the newest answer)
|
||||
|
||||
@@ -286,6 +286,12 @@ export default {
|
||||
}, { owner: unit });
|
||||
// S3: the sword wave
|
||||
if (unit.skill?.id !== S3) return;
|
||||
// PRTS: the sword wave hits air, although her S3 normal attacks remain ground-only. Include those selectable
|
||||
// targets in the existing ACTIVE_RANGE automation. [ASSUMED] Use its expanded attack grid (plus permanent
|
||||
// range extension), not the wave's entire eventual route; operation cooldown / control checks stay shared.
|
||||
unit.skill.addTriggerRange(() => [{
|
||||
keys: absoluteRangeKeys(grid3, unit.tileR, unit.tileC, unit.dir, unit.s.baseRangeExtend || 0), profile: AIR,
|
||||
}]);
|
||||
const ratio = num(b3.hp_ratio), minScale = num(b3.projectile_min_atk_scale);
|
||||
battle.on('tick', ({ dt }) => {
|
||||
const w = unit.mem.chen3Wave;
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Community follow-up: deployment clocks, air-only casts, champagne healing and airborne splash.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, chessRec, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
|
||||
const TRAP = 'token_10031_swire2_gdtrap';
|
||||
const SW = 'chess_char_3_04_a';
|
||||
const wall = chessRec({ id: 'wall', skill: null, stats: { maxHp: 1e7, atk: 0, def: 0, blockCnt: 3 } });
|
||||
const dummy = enemyRec({ key: 'dummy', hp: 1e9, atk: 0, speed: 0 });
|
||||
const done = (h) => { checkInvariants(h.b); assert.deepEqual(h.b.errors, []); };
|
||||
const diy = (charId, skillIndex, tier = 5) => ({ slot: `chess_char_${tier}_diy1_a`, charId, skillIndex });
|
||||
|
||||
test('deployment duration starts at battle start, never advances from repeated start/read calls', () => {
|
||||
const h = makeBattle({ autoFinish: false, units: [{ uid: 1, diy: diy('char_1015_aglna2', 0), row: 10, col: 4 }] });
|
||||
const u = h.unit(1);
|
||||
assert.equal(h.b.started, false); assert.equal(u.deployed, false);
|
||||
assert.ok(!u.skill?.active);
|
||||
h.b.start(); const duration = u.skill.duration;
|
||||
assert.equal(u.skill.timeLeft, duration); assert.equal(u.skill.activations, 1);
|
||||
for (let i = 0; i < 10; i++) { h.b.start(); h.b.snapshot(); }
|
||||
assert.equal(h.b.time, 0); assert.equal(u.skill.timeLeft, duration);
|
||||
h.run(2); assert.ok(Math.abs(u.skill.timeLeft - (duration - 2)) < 1e-8);
|
||||
done(h);
|
||||
});
|
||||
|
||||
for (const tier of [5, 6]) for (const elite of [false, true]) {
|
||||
test(`赤刃陈 S3 opens for a flyer in its expanded range and only the wave damages it (T${tier}, elite=${elite})`, () => {
|
||||
const h = makeBattle({ autoFinish: false, flags: { startOpCooldown: 3 }, hooks: ['damaged'], captureNoisy: true,
|
||||
defs: { enemies: { fly: enemyRec({ key: 'fly', hp: 1e9, atk: 0, speed: 0, motion: 'FLY' }) } },
|
||||
units: [{ uid: 1, diy: diy('char_1050_chen3', 2, tier), elite, row: 10, col: 4 }] });
|
||||
h.step(); const u = h.unit(1); const e = h.spawn('fly', { pos: [10, 6] });
|
||||
u.skill.gainSp(999, 'init'); h.run(2);
|
||||
assert.equal(u.skill.activations, 0, 'the operation cooldown still applies');
|
||||
assert.ok(h.runUntil(() => u.skill.active, 2), 'air-only target can start the sword wave');
|
||||
h.run(2);
|
||||
const hits = h.hooksOf('damaged').filter(c => c.source === u && c.target === e);
|
||||
assert.ok(hits.length > 0, 'the wave hits the flyer');
|
||||
assert.ok(hits.every(c => c.dmg.tags.includes('chen3:wave')), 'normal S3 swings remain ground-only');
|
||||
done(h);
|
||||
});
|
||||
}
|
||||
|
||||
test('赤刃陈 S3 still rejects an out-of-range, hidden or sleeping flyer, and cannot cast while stunned', () => {
|
||||
const h = makeBattle({ autoFinish: false,
|
||||
defs: { enemies: { fly: enemyRec({ key: 'fly', hp: 1e9, atk: 0, speed: 0, motion: 'FLY' }) } },
|
||||
units: [{ uid: 1, diy: diy('char_1050_chen3', 2), row: 10, col: 4 }] });
|
||||
h.step(); const u = h.unit(1); u.skill.gainSp(999, 'init');
|
||||
const far = h.spawn('fly', { pos: [12, 12] }); h.run(0.3); assert.equal(u.skill.activations, 0);
|
||||
const e = h.spawn('fly', { pos: [10, 6] });
|
||||
h.b.addBuff(e, { key: 'test:stealth', flags: { stealth: true } });
|
||||
h.run(0.3); assert.equal(u.skill.activations, 0);
|
||||
h.b.removeBuff(e, 'test:stealth'); h.b.applyStatus(e, 'sleep', { duration: 0.4 });
|
||||
h.run(0.2); assert.equal(u.skill.activations, 0);
|
||||
h.b.applyStatus(u, 'stun', { duration: 0.7 }); h.run(0.5); assert.equal(u.skill.activations, 0);
|
||||
h.run(0.4); assert.equal(u.skill.activations, 1); assert.ok(far.alive); done(h);
|
||||
});
|
||||
|
||||
test('琳琅诗怀雅 S2 receives the deployment coin and throws without waiting for the first merchant payment', () => {
|
||||
const h = makeBattle({ autoFinish: false, units: [{ uid: 1, chessId: SW, row: 10, col: 4, skillIndex: 1, potential: 1 }] });
|
||||
h.b.start(); const u = h.unit(1);
|
||||
assert.equal(u.skill.kind, 'passive'); assert.equal(u.mem.coins, 1);
|
||||
assert.equal(h.b.allyUnits.filter(a => a.defId === TRAP).length, 0);
|
||||
h.step(); assert.equal(h.b.allyUnits.filter(a => a.defId === TRAP).length, 1); assert.equal(u.mem.coins, 0);
|
||||
h.run(1); assert.equal(h.b.allyUnits.filter(a => a.defId === TRAP).length, 1, 'no free extra bombs');
|
||||
done(h);
|
||||
});
|
||||
|
||||
for (const skillIndex of [0, 1]) {
|
||||
test(`莎草 S${skillIndex + 1} heals/shields an adjacent operator but never chains to 琳琅诗怀雅's actual bomb`, () => {
|
||||
const h = makeBattle({ autoFinish: false, hooks: ['heal'], captureNoisy: true,
|
||||
flat: { rows: { 9: '##ERRrrRrrSrrrrrrrS##', 10: '##hrrRrrrrfrrrrrrrf##', 11: '##hrrRrrrrfrrrrrrrf##' } },
|
||||
defs: { chess: { wall } },
|
||||
units: [{ uid: 1, chessId: SW, row: 9, col: 5 },
|
||||
{ uid: 2, chessId: 'chess_char_2_06_a', row: 10, col: 6, skillIndex }, { uid: 3, chessId: 'wall', row: 10, col: 5 }] });
|
||||
h.step(); const bomb = h.b.allyUnits.find(a => a.defId === TRAP); assert.ok(bomb);
|
||||
assert.deepEqual([bomb.tileR, bomb.tileC], [9, 6]);
|
||||
const p = h.unit(2), a = h.unit(3); a.hp = 100;
|
||||
p.skill.gainSp(999, 'init');
|
||||
assert.ok(h.runUntil(() => !!a.findBuff('papyrs:shield'), 5), 'a real heal gave the operator a shield');
|
||||
h.run(3);
|
||||
assert.equal(bomb.findBuff('papyrs:shield'), null);
|
||||
assert.ok(!h.hooksOf('heal').some(c => c.source === p && c.target === bomb));
|
||||
if (skillIndex === 1) assert.notEqual(p.mem.papyrsLock, bomb);
|
||||
done(h);
|
||||
});
|
||||
}
|
||||
|
||||
for (const skillIndex of [0, 1, 2]) {
|
||||
test(`予愿 S${skillIndex + 1}: ground caster splash skips her while airborne and hits after landing`, () => {
|
||||
const h = makeBattle({ autoFinish: false, timeLimit: 120, hooks: ['damaged'], captureNoisy: true,
|
||||
defs: { chess: { wall }, enemies: { dummy } },
|
||||
units: [{ uid: 1, diy: diy('char_1015_aglna2', skillIndex), row: 9, col: 5 },
|
||||
{ uid: 2, chessId: 'wall', row: 10, col: 5 }] });
|
||||
h.step(); const u = h.unit(1), bait = h.unit(2);
|
||||
if (skillIndex > 0) assert.ok(u.skill.activate('test', { free: true }));
|
||||
h.run(3); assert.ok(u.s.flags.liftoff, 'past S2 invulnerable chant, still airborne');
|
||||
const e = h.spawn('enemy_1161_tidmag', { pos: [10, 7], mods: { hpMul: 1e4, speedMul: 0, atkMul: 0.1 } });
|
||||
h.run(7);
|
||||
assert.ok(h.hooksOf('damaged').some(c => c.source === e && c.target === bait), 'caster attacked the adjacent bait');
|
||||
assert.ok(!h.hooksOf('damaged').some(c => c.source === e && c.target === u), 'ordinary ground splash cannot hit her');
|
||||
u.skill.end('test'); u.skill.spCostMul = 1000; h.run(7);
|
||||
assert.ok(h.hooksOf('damaged').some(c => c.source === e && c.target === u), 'same splash reaches her after landing');
|
||||
done(h);
|
||||
});
|
||||
}
|
||||
|
||||
test('予愿起飞 still takes the brute leader splash explicitly marked 无视无法选择', () => {
|
||||
const h = makeBattle({ autoFinish: false, hooks: ['damaged'], captureNoisy: true,
|
||||
defs: { chess: { wall } },
|
||||
units: [{ uid: 1, diy: diy('char_1015_aglna2', 0), row: 9, col: 5 }, { uid: 2, chessId: 'wall', row: 9, col: 4 }],
|
||||
enemies: [{ key: 'enemy_1320_wdrrl_2', route: { motion: 'WALK', start: [9, 8], end: [9, 1], checkpoints: [] }, mods: { hpMul: 1e3, atkMul: 0.1 } }] });
|
||||
h.run(14); const u = h.unit(1);
|
||||
assert.ok(u.s.flags.liftoff);
|
||||
assert.ok(h.hooksOf('damaged').some(c => c.target === u && c.dmg.ignoreSelect));
|
||||
done(h);
|
||||
});
|
||||
@@ -17,6 +17,25 @@ function field({ skillIndex = 2, second = false, unite = false } = {}) {
|
||||
const h = makeBattle(config);h.step();return { h, u: h.unit(1), e: h.enemies()[0] };
|
||||
}
|
||||
function done(h) { checkInvariants(h.b); assert.deepEqual(h.b.errors, []); }
|
||||
for (const elite of [false, true]) test(`rear player's 蕾缪安 S3 locks and spends ammo on the front player's wanted target (elite=${elite})`, () => {
|
||||
const h = makeBattle({ kind: 'unite', autoFinish: false, flags: { startOpCooldown: 3 },
|
||||
hooks: ['ammoUsed'], captureNoisy: true,
|
||||
defs: { enemies: { target: enemyRec({ key: 'target', rank: 'ELITE', hp: 1e9, atk: 0, speed: 0 }) } },
|
||||
players: [
|
||||
{ playerId: 'front', seat: 0, side: 'L', colOffset: 0, bonds: {},
|
||||
units: [{ kind: 'chess', uid: 1, chessId: 'chess_char_2_01_a', row: 9, col: 7 }] },
|
||||
{ playerId: 'rear', seat: 1, side: 'L', colOffset: 0, bonds: {},
|
||||
units: [{ kind: 'chess', uid: 2, chessId: elite ? 'chess_char_6_01_b' : LEM, row: 12, col: 2,
|
||||
skillIndex: 2, carryState: { sp: 999 } }] },
|
||||
], enemies: [{ key: 'target', pos: [9, 8] }],
|
||||
});
|
||||
h.step(); const u = h.unit(2), e = h.enemies()[0];
|
||||
assert.equal(u.ownerId, 'rear'); assert.equal(h.b.enemiesInKeys(u.baseRangeKeys, u, u.profile).length, 0);
|
||||
assert.ok(h.runUntil(() => h.hooksOf('ammoUsed').some(c => c.unit === u), 25));
|
||||
assert.ok(e.findBuff('lemuen:wanted')); assert.ok(u.mem.lemLocks.some(l => l.e === e));
|
||||
assert.ok(u.skill.activations > 0); done(h);
|
||||
});
|
||||
|
||||
for (const skillIndex of [0, 1, 2]) for (const unite of [false, true]) {
|
||||
test(`通缉 opens S${skillIndex + 1} outside original range (unite=${unite})`, () => {
|
||||
const { h, u, e } = field({ skillIndex, unite });
|
||||
|
||||
+12
-12
@@ -3131,32 +3131,32 @@
|
||||
"time": 223.83,
|
||||
"ticks": 6715,
|
||||
"reason": "cleared",
|
||||
"killed": 21,
|
||||
"killed": 18,
|
||||
"total": 117,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 69,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":21,"total":117,"perfect":false,"leaks":96,"leaksUncounted":0,"coins":0,"dmg":142376,"bossDmg":0,"heal":1036,"deaths":22,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":18,"total":117,"perfect":false,"leaks":99,"leaksUncounted":0,"coins":0,"dmg":163563,"bossDmg":0,"heal":1036,"deaths":22,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,112,0,0,4864,0,0,0],
|
||||
["p1",2,"op","chess_char_5_diy2_a",1,null,0,0,142,0,0,7296,0,0,0],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,2432.1,1,143,0,0,4864,0,0,0],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,2432.1,1,143,0,3055,4864,0,0,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"none",0,0,132,0,0,5883,0,0,0],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"none",0,0,97,7,60432,5883,0,0,1],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,139,0,0,5883,0,0,0],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,139,0,7594,5883,0,0,1],
|
||||
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_chen3",0,0,88,0,0,6953,542,0,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_chen3",0,0,52,8,46092,3205,0,0,1],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_chen3",0,0,2,0,0,3205,0,0,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_chen3",0,0,51,3,36751,3205,0,0,1],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_chen3",0,0,2,1,5271,3205,0,0,1],
|
||||
["p1",10,"op","chess_char_6_diy1_a",0,null,0,0,6,0,0,2432,0,0,0],
|
||||
["p1",11,"op","chess_char_6_diy2_a",1,null,0,0,89,6,35852,4864,0,0,1],
|
||||
["p1",12,"op","chess_char_6_diy1_a",2,null,0,0,90,0,0,5359,495,0,0]
|
||||
["p1",12,"op","chess_char_6_diy1_a",2,null,0,0,90,1,14608,5359,495,0,1]
|
||||
],
|
||||
"enemies": {"enemy_1005_yokai":[51,13,38,0,31054],"enemy_1005_yokai_2":[54,6,48,3242,27770],"enemy_1112_emppnt":[12,2,10,0,83553]},
|
||||
"hooks": [1,143,6715,260,260,297,278,54,43,139,3,3,0,8200,0,0,117,96,1,43,0,0,19],
|
||||
"events": {"atk":260,"deploy":26,"die":43,"dmg":278,"fx":210,"heal":2,"leak":96,"skill":6,"spawn":129},
|
||||
"fx": {"blink":3,"bombard":36,"bombardShell":36,"dash":3,"dodge":19,"shield":54,"slash":59},
|
||||
"snaps": "0ab278ec e14c9c28 e3dbdeec 64907efb 3a831eb1 1a9c57a1 09f92fde 247d01ad 6846cc08 b6fc7137 20c12774 630ab4f0 01a4bbd9 29d8c702 9370a558 d3751005 05b6064d 165e2edc 6a570fdc 746b0dc3 7ee6853f a5969f03 b0c33e95"
|
||||
"enemies": {"enemy_1005_yokai":[51,12,39,0,28665],"enemy_1005_yokai_2":[54,4,50,3242,18513],"enemy_1112_emppnt":[12,2,10,0,116385]},
|
||||
"hooks": [1,143,6715,255,255,303,283,54,40,139,7,7,0,8148,0,0,117,99,1,40,0,0,20],
|
||||
"events": {"atk":255,"deploy":26,"die":40,"dmg":283,"fx":590,"heal":2,"leak":99,"skill":14,"spawn":129},
|
||||
"fx": {"blink":3,"bombard":34,"bombardShell":34,"chen3Wave":374,"dash":7,"dodge":20,"shield":54,"slash":64},
|
||||
"snaps": "0ab278ec f44030d3 46e9154f 51baa875 6092a9f9 ca78b52c 84f1d8ef 976fde48 f4eac9ab 59813547 25cbd407 84abcaed 17862595 6335f961 f69a635f 3c5d9184 78ecdc54 8d1f5a6e 52029bff 7352b2fd ed9a864d c37a98ce 1b9d99b7"
|
||||
},
|
||||
"diy-075": {
|
||||
"about": "5_diy1_a=leizi2/S1 5_diy2_a=leizi2/S2 5_diy1_a=leizi2/S3 5_diy2_b=leizi2/S1 5_diy1_b=leizi2/S2 5_diy2_b=leizi2/S3 6_diy1_b=chen3/S1 6_diy2_b=chen3/S2 6_diy1_b=chen3/S3 6_diy2_b=chen3/S1/002_chen3 6_diy1_b=chen3/S2/002_chen3 6_diy2_b=chen3/S3/002_chen3",
|
||||
|
||||
@@ -113,6 +113,32 @@ test('g.unitStats follows the prep state: another item changes the numbers; prep
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('prep stat previews do not spend a deployment skill duration in the next real battle', () => {
|
||||
const { h, m, ps } = setup(27);
|
||||
const id = 'chess_char_4_16_a'; // 缄默德克萨斯: a real timed deployment skill
|
||||
const piece = give(m, ps, id, 'board', legalTileFor(m, ps, id));
|
||||
const previews = [];
|
||||
const create = m.newBattle.bind(m);
|
||||
m.newBattle = (opts) => { const b = create(opts); previews.push(b); return b; };
|
||||
for (let i = 0; i < 3; i++) {
|
||||
m._unitStatsCache = null; // test fresh previews as well as the cached request
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.unitStats', seq: i }), { ok: true });
|
||||
const entry = h.lastTo('p_0', 'm.unitStats').units.find(u => u.uid === piece.uid);
|
||||
assert.ok(entry); assert.equal('sp' in entry, false); assert.equal('timeLeft' in entry, false);
|
||||
}
|
||||
assert.equal(m.phase, PHASE.PREP);
|
||||
for (const b of previews) {
|
||||
const u = b.allyUnits.find(u => u.uid === piece.uid);
|
||||
assert.equal(b.time, 0); assert.equal(b.tickCount, 0);
|
||||
assert.ok(u.skill.active); assert.equal(u.skill.timeLeft, u.skill.duration);
|
||||
}
|
||||
const b = create(m._normalOpts(ps)); const u = b.allyUnits.find(u => u.uid === piece.uid);
|
||||
assert.equal(b.started, false); assert.equal(u.deployed, false);
|
||||
b.start(); assert.equal(u.skill.timeLeft, u.skill.duration);
|
||||
b.step(); assert.ok(Math.abs(u.skill.timeLeft - (u.skill.duration - b.dt)) < 1e-8);
|
||||
assert.deepEqual(b.errors, []); m.dispose();
|
||||
});
|
||||
|
||||
test('the preview changes nothing of the match: two identical matches, one asking for the stats all along, stay identical', () => {
|
||||
const run = (ask) => {
|
||||
const { h, m, ps } = setup(23);
|
||||
|
||||
Reference in new issue
Block a user