feedback5: merge O18 — 自选 kits for 阿, 傀影 (with his 镜中虚影) and 弑君者; golden: only diy moved
This commit is contained in:
commit
996f5d20fe
8 files changed
+1903
-135
No files matched your search
@@ -130,6 +130,9 @@ export const OPERATOR_KIT_FILES = Object.freeze([
|
||||
'op-sleach.js',
|
||||
'op-closur.js',
|
||||
'op-gdglow.js',
|
||||
'op-haak.js',
|
||||
'op-phatom.js',
|
||||
'op-crosly.js',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
// server/sim/content/kits/ops/op-crosly.js — 弑君者 (char_1502_crosly) 自选 operator kit: 6★ 处决者 (特种), an owned-6★ pick
|
||||
// of the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules (EXE-X 弑君刃, EXE-Y 木炭画) at
|
||||
// every form. Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
|
||||
//
|
||||
// Forms (data/backups.json units.char_1502_crosly, the DIY slot statuses): normal = E2 Lv1, skills at rank 4, no module;
|
||||
// elite = E2 Lv60, rank 7, the picked module at stage 1 (tier 5) or 3 (tier 6) — the owner's decision of 2026-10-05.
|
||||
// Potential 0 [ASSUMED: no account]. Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into
|
||||
// backups.json); PRTS 弑君者 (吞咽苦厄 备注 "天赋生效范围为x-4"; 弑君者威名 备注 "<Y模组>任何来源为弑君者的晕眩…均可触发模组的额外
|
||||
// 效果" / "<Y模组>额外效果在弑君者付与晕眩效果时检查目标是否伤害过自身,若没有则令该次晕眩Buff的持续时间变为1.25倍/1.5倍"; 硝烟震爆
|
||||
// 备注 "技能持续期间获得嘲讽等级-1"; 烽烟行刑场 备注 "部署时阻挡数立刻归零;不会改变攻击范围/视野范围" / "“6秒内只会触发一次”的
|
||||
// 实际效果为受到弑君者伤害的单位获得持续6秒的“已攻击”标记Buff,可因任何来源于弑君者的伤害触发;技能期间弑君者始终不攻击具有“已攻击”
|
||||
// 标记的单位" / "此技能包含隐匿的Buff被隐匿反制解除时,失去“阻挡数变为0”的效果"); PRTS 命中率; the client's battle data —
|
||||
// charpack char_1502_crosly (modes Default / S1 / S2 (no attack, Talents/t1 `_applyTalentScale`) / S3 (attack: cooldown
|
||||
// `s3_cd`, `atk_scale_s3`, `_additionalTimes` 1, active buff stun, selector: ground enemies without her crosly_s_3[mark];
|
||||
// `_rangeToShow` 1-1) / S2_End (attack `atk_scale_s2` on every ground enemy of the x-4, `_allowNoTarget`); Talents/t1 per
|
||||
// mode: the hit-rate aura over ground enemies (attributes 33 / 34, range x-4 — S3: the skill's range); Talents/t2:
|
||||
// crosly_t_2), the skill prefabs skchr_crosly_1 / _2 / _3 (S1: crosly_s_1[mode] ATK + crosly_s_1[evade] (evade: physical
|
||||
// and arts) for `duration`; S2: crosly_s_2[start] (taunt level) for `duration`, then mode S2_End; S3: crosly_s_3[mode]
|
||||
// for `duration` + crosly_s_3[invisible] (abnormal flag 隐匿, block count ×0)), the equips crosly_equip_2_* (the EXE-Y
|
||||
// trait: ATK while no allied unit of profession mask 639 — the eight operator professions — stands at Manhattan distance 1)
|
||||
// / crosly_equip_2_2/3_p2 (crosly_e_003_t2[status-resist] on every enemy without her crosly_t_2[mark], every 0.1 s);
|
||||
// buff_template_data (crosly_t_2: an enemy whose non-continuous, uncancelled damage she takes gets crosly_t_2[mark] — a
|
||||
// derived buff, gone with her talent buff when she leaves the field; DamageScale `damage_scale` on PHYSICAL damage she
|
||||
// deals to a target without it; crosly_s_2[end] (finished at once when she is STUNNED / FROZEN / DISARMED — no blast);
|
||||
// crosly_s_3[mode] (her damage marks the target crosly_s_3[mark] for mark_duration, derived: gone when the skill ends);
|
||||
// crosly_e_003_t2[status-resist] (a stun of hers on such an enemy: duration × |1 + one_minus_status_resistance|)).
|
||||
// - Trait (处决者) "再部署时间大幅度减少": the data's respawnTime (22 s); melee physical, 1-1, blocks 1, ground-only (data
|
||||
// canHitFly false), attacks the enemies it blocks first (the engine's blocked-first rule); ground enemies target her.
|
||||
// - Module EXE-X: "撤退时返还大量该次部署费用" — no manual retreat in battle in this mode ⇒ no effect (as 砾's EXE-X); stats;
|
||||
// stage 2+ changes T1 (−23 % / −25 %). EXE-Y: "周围四格没有友方干员时攻击力+10%" (trait bb atk): ATK +atk while no allied
|
||||
// operator — summons and devices do not count — stands on the 4 tiles beside her (op-phatom.js noOperatorBeside); stats;
|
||||
// stage 2+ changes T2 (below).
|
||||
// - T1 吞咽苦厄 "技能持续期间在自身周围产生烟雾,使其中的地面敌人物理与法术命中率-20%": while any of her skills runs, the smoke covers
|
||||
// her x-4 (PRTS 备注; S3: the skill's x-1, "第一天赋的生效范围扩大"); each physical / arts attack of a selectable ground
|
||||
// enemy whose body is in it misses with |damage_hitrate_physical| / |damage_hitrate_magical| (×talent_scale while S2
|
||||
// runs): PRTS 命中率 — one roll per attack (its first damage instance) that cancels every hit of it, a battle-wide `hit`
|
||||
// handler as Raidian's / 阿斯卡纶's (the strongest smoke holding the attacker; other kinds of 命中率 cut roll apart
|
||||
// [ASSUMED, as theirs]).
|
||||
// - T2 弑君者威名 "对未伤害过自身的地面敌人造成的物理伤害提升20%": physical damage she deals to a ground enemy that has not damaged
|
||||
// her during this deployment ×damage_scale (a final multiplier, dmg.mul); an enemy is marked by any damage of it she
|
||||
// takes — not a 持续伤害 tick, not a 流失, not an element 损伤, not a dodged hit — and the marks go when she leaves the field.
|
||||
// EXE-Y stage 2+ (crosly_e_003_t2[status-resist]): "且造成的晕眩效果影响时间+50%" — a stun of hers (any source, PRTS 备注)
|
||||
// on an enemy without that mark lasts ×(1 + one_minus_status_resistance) (the hidden module talent; 1.5 at stage 3).
|
||||
// - S1 尘烟蔽目 (被动, at each deployment, the skill's duration 10 s): ATK +atk, physical and arts dodge `prob`.
|
||||
// - S2 硝烟震爆 (被动, at each deployment, bb duration 8 s): no attacks, taunt level +taunt_level (−1), T1 ×talent_scale; at its
|
||||
// end — unless she cannot act (晕眩 / 冻结 / …) or is 缴械 then (crosly_s_2[end]) — every selectable ground enemy of the
|
||||
// x-4 smoke takes attack@atk_scale_s2 × ATK physical skill damage. [ASSUMED] the blast lands at once (the end clip's
|
||||
// wind-up is not modelled).
|
||||
// - S3 烽烟行刑场 (被动, at each deployment, the skill's duration 16 s): 隐匿 and block count 0 (one buff: no enemy counters an
|
||||
// ally's 隐匿 in this mode); her normal attacks stop; her attack is a strike — ready at the start, then every
|
||||
// attack@s3_cd (2) s [ASSUMED: not shortened by attack speed] — on one selectable ground enemy of the x-1 smoke without
|
||||
// her mark (the engine's default target order): attack@times (2) hits of attack@atk_scale_s3 × ATK physical (a skill
|
||||
// attack: Battle.forceAttack) and 晕眩 attack@stun s; every damage she deals meanwhile marks its target for mark_duration
|
||||
// (6) s, the marks gone when the skill ends. Her attack range stays 1-1 (PRTS "不会改变攻击范围").
|
||||
// Triggers: the three skills are 被动 ON_DEPLOY — timed deployment skills (activateOnDeploy, trigger NEVER).
|
||||
|
||||
import { num, talentBb, moduleBb, traitBb, skillRec, toggleBuff, up } from '../shared/tier1.js';
|
||||
import { absoluteRangeKeys, canTargetEnemy, sortEnemyTargets } from '../../../targeting.js';
|
||||
import { bodyInKeys } from '../../../body.js';
|
||||
import { noOperatorBeside } from './op-phatom.js';
|
||||
|
||||
const S1 = 'skchr_crosly_1';
|
||||
const S2 = 'skchr_crosly_2';
|
||||
const S3 = 'skchr_crosly_3';
|
||||
/** 吞咽苦厄's smoke (PRTS 备注 "天赋生效范围为x-4"): her tile and the eight around it. */
|
||||
const X4 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]]);
|
||||
/** 烽烟行刑场's smoke when its record carries no grid: range x-1 (the diamond of radius 2). */
|
||||
const X1 = Object.freeze([[2, 0], [1, -1], [1, 0], [1, 1], [0, -2], [0, -1], [0, 0], [0, 1], [0, 2], [-1, -1], [-1, 0], [-1, 1], [-2, 0]]);
|
||||
/** The smoke's 命中率 handler runs after Raidian's (300) and 阿斯卡纶's (290), before the enemy content's riders (200). */
|
||||
const MISS_PRIORITY = 280;
|
||||
const GROUND = Object.freeze({ canHitFly: false, groundOnly: true });
|
||||
const TIMED = Object.freeze({ kind: 'duration', activateOnDeploy: true, spCost: 0, spType: 'none', trigger: 'NEVER' });
|
||||
const TAG_BLAST = 'crosly:blast';
|
||||
const skillOn = (u, id) => !!(u.skill && u.skill.active && (id == null || u.skill.id === id));
|
||||
|
||||
// ---- T1 命中率: one battle-wide handler over every 弑君者 of the battle ---------------------------------------------
|
||||
/** battle → { srcs: [{ unit, phys, arts, scale, s3Grid }], last: WeakMap(enemy → { id, miss }) }. */
|
||||
const SMOKE = new WeakMap();
|
||||
|
||||
/** The smoke's tiles of `u` now (S3: the skill's x-1, else her x-4). */
|
||||
const smokeKeys = (u, s3Grid) => absoluteRangeKeys(skillOn(u, S3) ? s3Grid : X4, u.tileR, u.tileC, u.dir, 0);
|
||||
|
||||
/** The 命中率 cut (0–1) that ground enemy `e`'s attack of damage type `type` suffers: the strongest smoke holding it. */
|
||||
function smokeCut(st, e, type) {
|
||||
let cut = 0;
|
||||
for (const s of st.srcs) {
|
||||
const u = s.unit;
|
||||
if (!up(u) || !skillOn(u)) continue;
|
||||
const v = (type === 'arts' ? s.arts : s.phys) * (skillOn(u, S2) ? s.scale : 1);
|
||||
if (!(v > cut) || !canTargetEnemy(u, e, GROUND) || !bodyInKeys(e, smokeKeys(u, s.s3Grid))) continue;
|
||||
cut = v;
|
||||
}
|
||||
return Math.min(1, cut);
|
||||
}
|
||||
|
||||
function smokeHit(battle, st, ctx) {
|
||||
const e = ctx.source, d = ctx.dmg;
|
||||
if (!e || e.side !== 'enemy' || e.isFlying || !d || !d.isAttack || d.cancel || (d.type !== 'phys' && d.type !== 'arts')) return;
|
||||
// one roll per attack: every damage instance of it shares the first instance's result
|
||||
const prev = d.attackId ? st.last.get(e) : null;
|
||||
let miss;
|
||||
if (prev && prev.id === d.attackId) miss = prev.miss;
|
||||
else {
|
||||
const cut = smokeCut(st, e, d.type);
|
||||
miss = cut > 0 && battle.rng() < cut;
|
||||
if (d.attackId) st.last.set(e, { id: d.attackId, miss });
|
||||
}
|
||||
if (!miss) return;
|
||||
d.cancel = true;
|
||||
ctx.stopPropagation = true;
|
||||
if (ctx.target) battle.fx('dodge', { x: ctx.target.x, y: ctx.target.y, id: ctx.target.id });
|
||||
}
|
||||
|
||||
function installSmoke(battle, src) {
|
||||
if (!(src.phys > 0) && !(src.arts > 0)) return;
|
||||
let st = SMOKE.get(battle);
|
||||
if (!st) {
|
||||
st = { srcs: [], last: new WeakMap() };
|
||||
SMOKE.set(battle, st);
|
||||
// no owner: it serves every 弑君者 of the battle, whichever is on the field
|
||||
battle.on('hit', (ctx) => smokeHit(battle, st, ctx), { priority: MISS_PRIORITY });
|
||||
}
|
||||
st.srcs.push(src);
|
||||
}
|
||||
|
||||
// ---- S3 strikes -------------------------------------------------------------------------------------------------------
|
||||
/** The strike's target: a selectable ground enemy of the x-1 smoke without her mark, the engine's default order. */
|
||||
function strikeTarget(battle, unit, s3Grid) {
|
||||
const marks = unit.mem.croslyMarks;
|
||||
const list = battle.enemiesInKeys(smokeKeys(unit, s3Grid), unit, GROUND)
|
||||
.filter((e) => !(marks && marks.get(e) > battle.time + 1e-9));
|
||||
if (!list.length) return null;
|
||||
sortEnemyTargets(battle, unit, list, null);
|
||||
return list[0];
|
||||
}
|
||||
|
||||
export default {
|
||||
char_1502_crosly: (bb, chess) => {
|
||||
const t0 = talentBb(chess, 0); // 吞咽苦厄 (EXE-X stage 2+: −0.25)
|
||||
const t1 = talentBb(chess, 1); // 弑君者威名 (EXE-Y stage 2+: 1.35)
|
||||
const osr = num(moduleBb(chess).one_minus_status_resistance); // EXE-Y stage 2+: the stun duration share
|
||||
const lonelyAtk = num(traitBb(chess).atk); // EXE-Y trait (EXE-X: withdraw_cost_recover_ratio)
|
||||
const r1 = skillRec(chess, S1), r2 = skillRec(chess, S2), r3 = skillRec(chess, S3);
|
||||
const b1 = r1?.bb ?? {}, b2 = r2?.bb ?? {}, b3 = r3?.bb ?? {};
|
||||
const s3Grid = Array.isArray(r3?.rangeGrid) && r3.rangeGrid.length ? r3.rangeGrid.map((p) => [p[0], p[1]]) : X1;
|
||||
const cd3 = Math.max(0.1, num(b3['attack@s3_cd'], 2));
|
||||
const markDur = num(b3.mark_duration, 6);
|
||||
return {
|
||||
skills: {
|
||||
[S1]: { ...TIMED, duration: num(r1?.duration, 10), mods: { atkPct: num(b1.atk), dodgePhys: num(b1.prob), dodgeArts: num(b1.prob) } },
|
||||
[S2]: {
|
||||
...TIMED, duration: num(b2.duration, 8), mods: { taunt: num(b2.taunt_level) }, attack: { noAttack: true },
|
||||
onEnd({ battle, unit, reason }) {
|
||||
// crosly_s_2[end]: STUNNED / FROZEN / DISARMED when it ends ⇒ no blast
|
||||
if (reason !== 'duration' || !up(unit) || !unit.canAct || unit.s.flags.disarm) return;
|
||||
const scale = num(b2['attack@atk_scale_s2']);
|
||||
for (const e of battle.enemiesInKeys(absoluteRangeKeys(X4, unit.tileR, unit.tileC, unit.dir, 0), unit, GROUND)) {
|
||||
if (e.alive && scale > 0) battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'phys', isSkill: true, tags: ['skill', TAG_BLAST] });
|
||||
}
|
||||
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1, id: unit.id, skill: TAG_BLAST });
|
||||
},
|
||||
},
|
||||
[S3]: {
|
||||
...TIMED, duration: num(r3?.duration, 16),
|
||||
flags: { stealth: true }, mods: { blockCnt: -99 },
|
||||
attack: {
|
||||
noAttack: true, atkScale: num(b3['attack@atk_scale_s3'], 1), hits: Math.max(1, Math.floor(num(b3['attack@times'], 2))),
|
||||
onHitStatus: { key: 'stun', duration: num(b3['attack@stun']) },
|
||||
},
|
||||
onStart({ battle, unit }) {
|
||||
unit.mem.croslyMarks = new Map();
|
||||
unit.mem.croslyNext = battle.time; // the strike is ready at the start
|
||||
battle.fx('stealth', { x: unit.x, y: unit.y, id: unit.id });
|
||||
},
|
||||
onTick({ battle, unit }) {
|
||||
if (!unit.canAct || unit.s.flags.disarm || battle.time + 1e-9 < num(unit.mem.croslyNext, 0)) return;
|
||||
const t = strikeTarget(battle, unit, s3Grid);
|
||||
if (!t) return;
|
||||
if (battle.forceAttack(unit, [t])) {
|
||||
unit.mem.croslyNext = battle.time + cd3;
|
||||
battle.fx('strike', { x: t.x, y: t.y, id: unit.id, target: t.id });
|
||||
}
|
||||
},
|
||||
onEnd({ unit }) { unit.mem.croslyMarks = null; },
|
||||
},
|
||||
},
|
||||
talents: [
|
||||
{ install(battle, unit) { // 吞咽苦厄: the smoke's 命中率 cut (×talent_scale while S2 runs)
|
||||
installSmoke(battle, { unit, phys: Math.abs(num(t0.damage_hitrate_physical)), arts: Math.abs(num(t0.damage_hitrate_magical)), scale: num(b2.talent_scale, 1), s3Grid });
|
||||
} },
|
||||
{ install(battle, unit) { // 弑君者威名 (+ EXE-Y stage 2+: the stun duration)
|
||||
const ds = num(t1.damage_scale, 1);
|
||||
battle.on('deploy', (c) => { if (c.unit === unit && !c.move) unit.mem.croslyHurtBy = new WeakSet(); }, { owner: unit, priority: 10 });
|
||||
battle.on('damaged', (c) => {
|
||||
const e = c.source, d = c.dmg;
|
||||
if (c.target !== unit || !e || e.side !== 'enemy' || c.type === 'element' || !d) return;
|
||||
if (d.tags && (d.tags.includes('dot') || d.tags.includes('periodic') || d.tags.includes('hpLoss'))) return;
|
||||
(unit.mem.croslyHurtBy ??= new WeakSet()).add(e);
|
||||
}, { owner: unit });
|
||||
const fresh = (e) => !(unit.mem.croslyHurtBy && unit.mem.croslyHurtBy.has(e));
|
||||
if (ds !== 1) {
|
||||
battle.on('hit', (c) => {
|
||||
const t = c.target, d = c.dmg;
|
||||
if (c.source !== unit || !t || t.side !== 'enemy' || t.isFlying || !d || d.type !== 'phys' || !fresh(t)) return;
|
||||
d.mul *= ds;
|
||||
}, { owner: unit });
|
||||
}
|
||||
if (osr > 0) {
|
||||
battle.on('beforeStatus', (c) => {
|
||||
if (c.source !== unit || c.status !== 'stun' || !c.target || c.target.side !== 'enemy' || !fresh(c.target)) return;
|
||||
c.duration *= Math.abs(1 + osr);
|
||||
}, { owner: unit });
|
||||
}
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
// 烽烟行刑场: every damage she deals while it runs marks its target for mark_duration s
|
||||
if (chess?.skill?.skillId === S3) {
|
||||
battle.on('damaged', (c) => {
|
||||
const t = c.target;
|
||||
if (c.source !== unit || !t || t.side !== 'enemy' || c.type === 'element' || !skillOn(unit, S3) || !unit.mem.croslyMarks) return;
|
||||
unit.mem.croslyMarks.set(t, battle.time + markDur);
|
||||
}, { owner: unit });
|
||||
}
|
||||
// EXE-Y "周围四格没有友方干员时攻击力+10%"
|
||||
if (lonelyAtk > 0) toggleBuff(battle, unit, 'trait:crosly:lonely', () => noOperatorBeside(battle, unit), { atkPct: lonelyAtk });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,184 @@
|
||||
// server/sim/content/kits/ops/op-haak.js — 阿 (char_225_haak) 自选 operator kit: 6★ 怪杰 (特种), an owned-6★ pick of the
|
||||
// tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules (GEE-X 什锦果味医用箱, GEE-Y 毒医残章) at
|
||||
// every form. Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
|
||||
//
|
||||
// Forms (data/backups.json units.char_225_haak, the DIY slot statuses): normal = E2 Lv1, skills at rank 4, no module;
|
||||
// elite = E2 Lv60, rank 7, the picked module at stage 1 (tier 5) or 3 (tier 6) — the owner's decision of 2026-10-05.
|
||||
// Potential 0 [ASSUMED: no account]. Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into
|
||||
// backups.json); PRTS 阿 (混合药物射击 备注 "每次攻击必定触发随机效果,4种效果的概率平均分布" / "<X模组>先判断是否“同时触发”所有
|
||||
// 效果,未触发情况下再判断随机效果"; 爆发剂 备注 "进行完整的15次攻击后阿与目标获得持续时间相当于技能持续时间的增益;15次攻击均为无途径
|
||||
// 攻击,但可满足近战/远程攻击相关的触发条件"); PRTS 分支特性信息 怪杰 ("可对空", "每秒流失最大生命值1%的生命值,不会致命且最低保留1
|
||||
// 生命值"); the client's battle data — charpack char_225_haak (Talents/1: the additive active buff haak_t_1; Talents/2:
|
||||
// heal_scale; Trait: periodic_damage_by_hp_ratio[skip_modifier_fix][undeadable] every 1 s), buff_template_data
|
||||
// (haak_t_1: RandomCreateBuff of haak_t_atk (AtkScaleUp `atk_scale` on ON_CALCULATE_DAMAGE) / sluggish / stun / haak_t_heal
|
||||
// (HealViaMaxHpRatio), weight 1 each; haak_e_002[dice]: Dice `prob` ⇒ all four, else the random one), the skill prefabs
|
||||
// skchr_haak_2 / _3 (Attack + Fake_Attack: 15 shots — `_additionalTimes` 14, `_triggerDelta` 0.03 s — of
|
||||
// instant_damage_physical[add_sp] = FixedValueDamage PHYSICAL `damage`; the target's buff fed with the last shot, his own
|
||||
// by the Buff ability after the volley; ally selector `_targetSide` 1, `_excludeOwner`, `_postFilter` 21, his attack
|
||||
// range) and the equips haak_equip_1_* (the hidden sp_recovery talent while his HP ratio is in [0.8, 1]) / haak_equip_2_2,
|
||||
// _2_3 (heal_scale + the ability healinrange: one ally of his range, the same selector, healed healinrange.heal_scale ×
|
||||
// ATK at once); Terra Wiki Aak (爆发剂: "an ally within range (prioritizes allies directly in front)", "the one in the
|
||||
// frontal row/column from the closest to the furthest").
|
||||
// - Trait (怪杰) "自身生命会不断流失": the profession default (professions.js geek, installHpDrain: hp_ratio × max HP every
|
||||
// second, never below 1 HP), ranged physical arrows that hit air units (data canHitFly; PRTS "可对空"), blocks 1, 3-3;
|
||||
// ground enemies target him (no flag). Module GEE-X adds "生命值高于80%时,技力自然回复速度+0.25/秒" (the hidden module
|
||||
// talent sp_recovery_per_sec: SP recovery +0.25/s while his HP ratio is at least 80 % — the equip checker's [0.8, 1]);
|
||||
// GEE-Y "再部署时间减少" is its attribute respawn_time −15 (in the stats, as its ATK / ASPD).
|
||||
// - T1 混合药物射击 "每次攻击时会随机触发下列效果之一": every attack of his on an enemy rolls once, at its hit (the talent's
|
||||
// additive active buff rides the attack onto the target — not damage-missable: a dodged hit still rolls), one of four
|
||||
// with equal weight: heal himself hp_ratio × max HP (a 治疗: T2 scales it), that attack's damage ×atk_scale (an ATK scale,
|
||||
// before mitigation — "当次攻击力提升至150%"), 停顿 `sluggish` s or 晕眩 `stun` s on the target. GEE-X stage 2+: first
|
||||
// `prob` to trigger all four at once (haak_e_002[dice]), else the random one; stage 3's numbers are the module's.
|
||||
// [ASSUMED] his 爆发剂 shots at an ally never roll (PRTS names no such effect); the client's haak_t_atk lingers until his
|
||||
// next roll and could scale damage he deals meanwhile — here it scales the attack that rolled it only.
|
||||
// - T2 药剂扩散 "自身受到的治疗量+20%": healing received ×heal_scale. GEE-Y stage 2+ (1.25 at stage 3) adds "部署后立即恢复攻击
|
||||
// 范围内一名友方干员(优先选取正前方)的生命值,恢复量为阿攻击力的150%": at each deployment, after the deploy-time buffs, a heal
|
||||
// of healinrange.heal_scale × ATK on the ally frontAlly picks among the healable ones [ASSUMED: injured or not — the
|
||||
// selector has no HP filter; summons count, as for 爆发剂's identical selector].
|
||||
// - S1 快速射击 (MANUAL, data DEFAULT): ASPD +attack_speed for its duration.
|
||||
// - S2 爆发剂·γ型 / S3 爆发剂·榴莲味 (MANUAL, data DEFAULT; timed: 30 / 20 s): at the cast, `stimulant`: the ally frontAlly
|
||||
// picks takes 15 physical hits of `damage` (500) fixed attack power each (FixedValueDamage: DEF applies, 5 % minimum;
|
||||
// 普通伤害, skill damage — 受击回复 SP and the 重装 TAKE_DAMAGE trigger answer them like any damage taken: the
|
||||
// template's "[add_sp]"); then he and — still standing — the target get max HP +max_hp / DEF +def (S2) or ATK +atk / ASPD
|
||||
// +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).
|
||||
|
||||
import { num, talentBb, moduleBb, moduleOn, skillRec, statBuff, toggleBuff, up } from '../shared/tier1.js';
|
||||
import { toLocal } from '../../../dir.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
|
||||
const S1 = 'skchr_haak_1';
|
||||
const S2 = 'skchr_haak_2';
|
||||
const S3 = 'skchr_haak_3';
|
||||
/** 爆发剂's shots when the text names none ("攻击15次"; skill prefab `_additionalTimes` 14 + the first). */
|
||||
const SHOTS_FALLBACK = 15;
|
||||
/** GEE-X "生命值高于80%时" when the trait text names none (the equip checker's `_minHpRatio`). */
|
||||
const GEEX_HP_FALLBACK = 0.8;
|
||||
/** 混合药物射击's four effects, in the client's RandomCreateBuff order (weight 1 each). */
|
||||
const DICE = Object.freeze(['atk', 'sluggish', 'stun', 'heal']);
|
||||
const TAG_STIM = 'haak:stim';
|
||||
|
||||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||||
|
||||
/**
|
||||
* 爆发剂 / 药剂扩散's target (the client selector `_postFilter` 21; Terra Wiki Aak): an ally — an operator or a summon,
|
||||
* never a device, never a 孤立 one (Battle.allySelectable) — standing in his current attack range, himself excluded; the
|
||||
* ones on his straight line ahead (local row 0) first, each group nearest first; ties: the higher taunt level, then the
|
||||
* latest deployed [ASSUMED: the Terra Wiki's "highest aggression level"], then the unit id. `heal` = a heal selector: no
|
||||
* 禁疗 / 无法被友方治疗 ally (PRTS 选择器 "若掩码中禁疗为1,且选择器属于治疗行为,则不可选中").
|
||||
*/
|
||||
export function frontAlly(battle, unit, heal = false) {
|
||||
const keys = unit.rangeKeySet || new Set(unit.rangeKeys || []);
|
||||
let best = null, bk = null;
|
||||
for (const a of battle.allyUnits) {
|
||||
if (a === unit || !a.alive || !a.deployed || a.hidden || a.kind === 'device') continue;
|
||||
if (!keys.has(a.tileR * COLS + a.tileC) || !battle.allySelectable(a, unit)) continue;
|
||||
if (heal && (a.s.flags.noHeal || a.s.flags.healFree || a.profile?.noHeal)) continue;
|
||||
const dr = a.tileR - unit.tileR, dc = a.tileC - unit.tileC;
|
||||
const [lr, lc] = toLocal(dr, dc, unit.dir);
|
||||
const k = [lr === 0 && lc > 0 ? 0 : 1, Math.hypot(dr, dc), -(a.s.taunt || 0), -(a.aggroSeq || a.deploySeq || 0), a.id];
|
||||
let less = !bk;
|
||||
for (let i = 0; !less && i < k.length; i++) {
|
||||
if (k[i] === bk[i]) continue;
|
||||
less = k[i] < bk[i];
|
||||
break;
|
||||
}
|
||||
if (less) { best = a; bk = k; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* 爆发剂 at the cast: `shots` hits of `damage` fixed attack power on frontAlly's pick, then `mods` on him and on the
|
||||
* target (if it still stands) for `dur` s. No ally in range: nothing [ASSUMED, header].
|
||||
*/
|
||||
function stimulant(battle, unit, { damage, shots, dur, mods, key }) {
|
||||
const t = frontAlly(battle, unit);
|
||||
if (!t) return;
|
||||
for (let i = 0; i < shots && t.alive && unit.alive; i++) {
|
||||
battle.dealDamage(unit, t, { amount: damage, type: 'phys', isSkill: true, tags: ['skill', TAG_STIM] });
|
||||
}
|
||||
battle.fx('volley', { x: t.x, y: t.y, id: unit.id, target: t.id, n: shots, skill: key });
|
||||
if (!(dur > 0)) return;
|
||||
if (unit.alive) battle.addBuff(unit, { key: `${key}:self`, duration: dur, mods, source: unit, tags: ['skill'] });
|
||||
if (t.alive && t !== unit) {
|
||||
battle.addBuff(t, { key: `${key}:${unit.id}`, duration: dur, mods, source: unit, tags: ['skill'] });
|
||||
battle.fx('buff', { x: t.x, y: t.y, id: t.id, kind: key });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 混合药物射击: one roll per attack on an enemy (its `attackId`), at its first hit — `all` (GEE-X stage 2+) for the four
|
||||
* at once, else one of DICE uniformly; the heal and the statuses then, the ATK scale on every damage instance of it.
|
||||
*/
|
||||
function installDice(battle, unit, t0) {
|
||||
const hr = num(t0.hp_ratio), scale = num(t0.atk_scale, 1), slug = num(t0.sluggish), stun = num(t0.stun), all = num(t0.prob);
|
||||
battle.on('hit', (ctx) => {
|
||||
const d = ctx.dmg, t = ctx.target;
|
||||
if (ctx.source !== unit || !d || !d.isAttack || d.isSplash || d.type === 'element' || !t || t.side !== 'enemy') return;
|
||||
const m = unit.mem;
|
||||
if (!d.attackId || m.haakDiceId !== d.attackId) {
|
||||
m.haakDiceId = d.attackId;
|
||||
m.haakDice = all > 0 && battle.rng.chance(all) ? DICE : [DICE[battle.rng.int(DICE.length)]];
|
||||
for (const e of m.haakDice) {
|
||||
if (e === 'heal' && hr > 0) battle.heal(unit, unit, unit.s.maxHp * hr, { self: true });
|
||||
else if (e === 'sluggish' && slug > 0) battle.applyStatus(t, 'sluggish', { duration: slug, source: unit });
|
||||
else if (e === 'stun' && stun > 0) battle.applyStatus(t, 'stun', { duration: stun, source: unit });
|
||||
}
|
||||
if (m.haakDice.includes('atk') && scale !== 1) battle.fx('crit', { x: t.x, y: t.y, id: unit.id });
|
||||
}
|
||||
if (m.haakDice.includes('atk')) d.amount *= scale;
|
||||
}, { owner: unit });
|
||||
}
|
||||
|
||||
export default {
|
||||
char_225_haak: (bb, chess) => {
|
||||
const t0 = talentBb(chess, 0); // 混合药物射击 (GEE-X stage 2+: + prob, its own numbers)
|
||||
const t1 = talentBb(chess, 1); // 药剂扩散 (GEE-Y stage 2+: heal_scale 1.25, healinrange.heal_scale)
|
||||
const hidden = moduleBb(chess); // GEE-X: sp_recovery_per_sec
|
||||
const b1 = bbOf(chess, S1);
|
||||
const stim = (id, key, mods) => {
|
||||
const rec = skillRec(chess, id);
|
||||
const b = rec?.bb ?? {};
|
||||
const shots = Math.max(1, Math.floor(num(+(/攻击(\d+)次/.exec(String(rec?.desc ?? ''))?.[1]), SHOTS_FALLBACK)));
|
||||
const dur = num(rec?.duration, 0);
|
||||
return {
|
||||
kind: 'duration',
|
||||
onStart({ battle, unit }) { stimulant(battle, unit, { damage: num(b.damage, 500), shots, dur, mods: mods(b), key }); },
|
||||
};
|
||||
};
|
||||
return {
|
||||
skills: {
|
||||
[S1]: { kind: 'duration', mods: { aspd: num(b1.attack_speed) } },
|
||||
[S2]: stim(S2, 'haak:s2', (b) => ({ hpPct: num(b.max_hp), defPct: num(b.def) })),
|
||||
[S3]: stim(S3, 'haak:s3', (b) => ({ atkPct: num(b.atk), aspd: num(b.attack_speed) })),
|
||||
},
|
||||
talents: [
|
||||
{ install(battle, unit) { installDice(battle, unit, t0); } }, // 混合药物射击
|
||||
{ install(battle, unit) { // 药剂扩散: healing received ×heal_scale; GEE-Y stage 2+: the deploy heal
|
||||
statBuff(battle, unit, 'talent:haak:heal', { healingTakenMul: num(t1.heal_scale, 1) });
|
||||
const hs = num(t1['healinrange.heal_scale']);
|
||||
if (!(hs > 0)) return;
|
||||
battle.on('deploy', (c) => {
|
||||
if (c.unit !== unit || c.move || !up(unit)) return;
|
||||
const t = frontAlly(battle, unit, true);
|
||||
if (!t) return;
|
||||
battle.heal(unit, t, unit.s.atk * hs);
|
||||
battle.fx('heal', { x: t.x, y: t.y, id: t.id, src: unit.id });
|
||||
}, { owner: unit, priority: -10 });
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
// GEE-X "生命值高于80%时,技力自然回复速度+0.25/秒"
|
||||
const sp = num(hidden.sp_recovery_per_sec);
|
||||
if (sp > 0 && moduleOn(chess)) {
|
||||
const pct = +(/生命值高于(\d+)%/.exec(String(chess?.trait?.moduleDesc ?? ''))?.[1]);
|
||||
const thr = Number.isFinite(pct) ? pct / 100 : GEEX_HP_FALLBACK;
|
||||
toggleBuff(battle, unit, 'module:haak:sp', () => unit.hp >= unit.s.maxHp * thr - 1e-9, { spRecoveryFlat: sp });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,233 @@
|
||||
// server/sim/content/kits/ops/op-phatom.js — 傀影 (char_250_phatom) 自选 operator kit: 6★ 处决者 (特种), an owned-6★ pick of
|
||||
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait, his modules (EXE-X 克里斯汀小姐的毛毡, EXE-Y “如影随形”,
|
||||
// ISW-A 傀影特限证章) at every form, and the kit of his summon 镜中虚影 (token_10007_phatom_twin). Kit contract and the 自选
|
||||
// rules: ../README.md ("How to add an operator (自选)").
|
||||
//
|
||||
// Forms (data/backups.json units.char_250_phatom; the twin: tokens.token_10007_phatom_twin, variants by owner form with
|
||||
// bySkill / byModule): normal = E2 Lv1, skills at rank 4, no module; elite = E2 Lv60, rank 7, the picked module at stage 1
|
||||
// (tier 5) or 3 (tier 6) — the owner's decision of 2026-10-05. Potential 0 [ASSUMED: no account]. Sources:
|
||||
// character_table / skill_table / battle_equip_table / token_table (zh_CN, as built into backups.json); PRTS 傀影 (镜中虚影
|
||||
// 备注 "虚影再部署与拥有复数召唤物的干员相同,即“部署召唤物后立刻开始计算再部署时间,结束后可以部署下一个”"; 血色乐章 备注 "触发消耗
|
||||
// 层数效果的行动节点为“成功造成伤害后”。若造成的伤害被伤判效果归零则不会消耗层数"; 夜幕突袭 备注 "不可对空"; EXE-X "撤退时返还大量
|
||||
// (80%)该次部署费用"); PRTS 镜中虚影 (备注 "持有禁疗", "可以且优先攻击自身阻挡的单位", "退场时返还1个可部署的镜中虚影", "傀影退场时
|
||||
// 强制撤退场上的镜中虚影"); PRTS 分支特性信息 处决者; the client's battle data — charpack char_250_phatom (Talents/1:
|
||||
// charge_token[born] (RechargeToken on his deployment) + die_to_kill_token (KillTokens when he leaves), CommonAbilities
|
||||
// KillTokens; modes S2 / S3), token prefab token_10007_phatom_twin (CommonAbilities: charge_token[finish] carrying abnormal
|
||||
// flag 7 = 禁疗; Talents/2: phatom_twin_t, RESPAWN_TIME + respawn_time; the same modes), the skill
|
||||
// prefabs skchr_phatom_1 / _2 / _3 and sktok_phatom_1 / _2 / _3 (S1: evade_physical + phatom_s_1[shield_physical] = a
|
||||
// shield_physical of hp_ratio × max HP, both `duration`; S2: phatom_s_2 = `times` stacked phatom_s_2[atk] (ATK MULTIPLIER
|
||||
// `atk`), one stack finished per ON_AFTER_OUTPUT_DAMAGE; S3: RandomCastAbility of the sluggish / stun / root attack
|
||||
// abilities — one drawn per cast, each hitting every ground enemy of the x-4 with `atk_scale` physical, knockback[relative]
|
||||
// (`force`) and its status), the equips phatom_equip_2_* (the EXE-Y trait: ATK while every allied unit of profession mask
|
||||
// 639 — the eight operator professions, no summon — stands at Manhattan distance ≥ 2) / phatom_equip_2_2/3_p2 (the twin's
|
||||
// hidden talent phatom_equip_token[atk] → phatom_equip_host[atk] on him) / phatom_equip_3_* (`_checkRoguelikeMode`: ISW-A
|
||||
// is 集成战略-only); buff_template_data (die_to_kill_token, charge_token[born] / [finish], phatom_equip_token[host]).
|
||||
// - Trait (处决者) "再部署时间大幅度减少": the data's respawnTime (18 s); melee physical, 1-1, blocks 1, ground-only (data
|
||||
// canHitFly false), attacks the enemies it blocks first (the engine's blocked-first rule); ground enemies target him.
|
||||
// - Module EXE-X: "撤退时返还大量该次部署费用" — no manual retreat in battle in this mode ⇒ no effect (as 砾's EXE-X); HP / ATK
|
||||
// in the stats; stage 2+ changes T2 (below). EXE-Y: "周围四格没有友方干员时攻击力+10%" (trait bb atk): ATK +atk while no
|
||||
// allied operator — summons and devices do not count (mask 639) — stands on the 4 tiles beside him (noOperatorBeside);
|
||||
// stage 2+ changes T1 (below); ATK / DEF in the stats. ISW-A: its trait ("在集成战略中,攻击时使自身和召唤物下次再部署时间减少
|
||||
// 1秒") and talent ("在集成战略中,虚影不占用部署位,且部署虚影时自身再次释放技能") are 集成战略-only: N/A here, the trait
|
||||
// blackboard it carries is not used; ATK / ASPD in the stats.
|
||||
// - T1 镜中虚影 "可以使用一个属性更强的虚影,虚影拥有和自己一样的技能,拥有独立的再部署时间": the twin is a hand piece the player
|
||||
// places (a talent summon: tokens `placeable`, not owner-range); it deploys with the board after the operators and fights
|
||||
// with its own stats (the variant of his form; EXE-Y stage 3: "属性进一步增强" = the byModule stats) as an executor (melee,
|
||||
// blocks 1, ground-only) holding 禁疗 (PRTS; the client's flag 7 — the data's `abnormal` lacks it [ASSUMED: given here, as
|
||||
// the 凯尔希 / 机械师 kits do for their summons]), its copy of his picked skill (bySkill sktok_phatom_1 / 2 / 3) running at
|
||||
// each of its own deployments. When he leaves the field (knocked out, withdrawn, forced out) it is withdrawn (PRTS "强制
|
||||
// 撤退"; the client's KillTokens); a twin off the field comes back on its tile — paying its cost (5 DP) as a deployment,
|
||||
// the 卫戍 auto redeploy of a placed summon [ASSUMED, as 鸿雪's 打字机 / 凯尔希's Mon3tr] — once its redeploy time has
|
||||
// passed since its LAST DEPLOYMENT (PRTS 备注: the timer starts at the summon's deployment) and only while he stands (it
|
||||
// is recharged by his deployment, withdrawn by his leaving) [ASSUMED: his redeployment does not cut the twin's own timer
|
||||
// short — the client's RechargeToken NORMAL; 虚影精通 / EXE-X would matter little otherwise; the 凯尔希 kit reads the same
|
||||
// charge_token[born] as "readies at once"]. EXE-Y stage 2+ "本体和虚影同时在场时,攻击力各+10%": the twin's hidden module
|
||||
// talent `atk` — ATK +atk on both while both are on the field.
|
||||
// - T2 虚影精通 "虚影的再部署时间-10秒" (EXE-X stage 3: −16): the twin's own talent respawn_time, added to its 45 s.
|
||||
// - S1 暗夜魅影 (被动, at each deployment): physical dodge `prob` and a 屏障 of hp_ratio × max HP that absorbs physical damage
|
||||
// only (shieldType 'phys'), both for `duration` (10) s.
|
||||
// - S2 血色乐章 (被动, at each deployment): `times` stacks of ATK +atk each (直接乘算, summed); each damage instance he deals
|
||||
// uses one, after it is dealt (a dodged or cancelled hit — invulnerable, 限伤 — uses none; a hit a barrier absorbs does);
|
||||
// the stacks go when he leaves the field.
|
||||
// - S3 夜幕突袭 (被动, at each deployment, after the deploy-time buffs): every selectable ground enemy of the x-4 (PRTS "不可
|
||||
// 对空") takes atk_scale × ATK physical skill damage, is pushed away from him radially with 力度 `force` (0 = 小力,
|
||||
// knockback[relative]: Battle.push) and gets the one status drawn for this cast — 停顿 / 晕眩 / 束缚 for its bb duration
|
||||
// (the client draws one of the three abilities per cast, so every enemy gets the same) [ASSUMED: the push and the status
|
||||
// land on a dodged hit too — the abilities' buffs are not damage-missable]. At the battle-start deployment no enemy is
|
||||
// on the field yet: it hits nothing (as Misery's 空间的归依).
|
||||
|
||||
import { num, traitBb, skillRec, toggleBuff, up } from '../shared/tier1.js';
|
||||
import { absoluteRangeKeys } from '../../../targeting.js';
|
||||
|
||||
const S1 = 'skchr_phatom_1';
|
||||
const S2 = 'skchr_phatom_2';
|
||||
const S3 = 'skchr_phatom_3';
|
||||
export const TWIN = 'token_10007_phatom_twin';
|
||||
const TS1 = 'sktok_phatom_1';
|
||||
const TS2 = 'sktok_phatom_2';
|
||||
const TS3 = 'sktok_phatom_3';
|
||||
/** 夜幕突袭's area when a record carries no grid: range x-4, his tile and the eight around it. */
|
||||
const X4 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]]);
|
||||
/** The four tiles beside a unit (EXE-Y "周围四格"). */
|
||||
const BESIDE = Object.freeze([[1, 0], [-1, 0], [0, 1], [0, -1]]);
|
||||
/** 血色乐章's stack cap (buff_template_data phatom_s_2[atk] max_stack_cnt: the ISW-α stacking limit, never reached here). */
|
||||
const OPUS_CAP = 80;
|
||||
/** 夜幕突袭's statuses: the client's RandomCastAbility list (sluggish, stun, root) and the blackboard key of each duration. */
|
||||
const RAID = Object.freeze([{ status: 'sluggish', key: 'sluggish' }, { status: 'stun', key: 'stun' }, { status: 'bind', key: 'root' }]);
|
||||
/** How often a fallen twin tries to come back once its conditions may hold. */
|
||||
const RETRY = 0.25;
|
||||
const TAG_RAID = 'phatom:raid';
|
||||
|
||||
/** First talent of a normalised token def whose blackboard holds `key` (token talents keep no index). */
|
||||
const tokTalentBb = (def, key) => (def?.talents ?? []).find((t) => t && t.bb && t.bb[key] != null)?.bb ?? {};
|
||||
const isTwinOf = (t, owner) => !!t && t.kind === 'token' && t.defId === TWIN && t.ownerUnit === owner;
|
||||
const twinsOf = (battle, owner) => battle.allyUnits.filter((t) => isTwinOf(t, owner));
|
||||
|
||||
/**
|
||||
* Module EXE-Y "周围四格没有友方干员" (the equip checker over profession mask 639 = the eight operator professions): no
|
||||
* allied operator — of any player of the field — stands on one of the 4 tiles beside `unit`; summons and devices do not
|
||||
* count. Shared with 弑君者's EXE-Y (op-crosly.js).
|
||||
*/
|
||||
export function noOperatorBeside(battle, unit) {
|
||||
for (const [dr, dc] of BESIDE) {
|
||||
const r = unit.tileR + dr, c = unit.tileC + dc;
|
||||
if (!battle.grid.inBounds(r, c)) continue;
|
||||
const o = battle.unitAt(r, c);
|
||||
if (o && o !== unit && o.side === 'ally' && o.kind === 'op') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 暗夜魅影 / its copy at a deployment of `u`: physical dodge and a physical-only 屏障 of hp_ratio × its max HP. */
|
||||
function nightPhantom(battle, u, b, key) {
|
||||
const dur = num(b.duration, 10), p = num(b.prob), sh = u.s.maxHp * num(b.hp_ratio);
|
||||
if (!(dur > 0)) return;
|
||||
if (p > 0) battle.addBuff(u, { key: `${key}:evade`, duration: dur, mods: { dodgePhys: p }, tags: ['skill'] });
|
||||
if (sh > 0) {
|
||||
battle.addBuff(u, { key: `${key}:shield`, duration: dur, shield: sh, shieldType: 'phys', tags: ['skill'] });
|
||||
battle.fx('shield', { x: u.x, y: u.y, id: u.id });
|
||||
}
|
||||
}
|
||||
|
||||
/** 血色乐章 / its copy at a deployment of `u`: `times` stacks of ATK +atk. */
|
||||
function bloodyOpus(battle, u, b, key) {
|
||||
const n = Math.floor(num(b.times)), a = num(b.atk);
|
||||
if (!(n > 0) || !a) return;
|
||||
battle.addBuff(u, { key, refresh: 'stack', stacks: n, maxStacks: Math.max(n, OPUS_CAP), mods: { atkPct: a }, tags: ['skill'] });
|
||||
battle.fx('buff', { x: u.x, y: u.y, id: u.id, kind: 'atk' });
|
||||
}
|
||||
|
||||
/** 血色乐章: every damage instance `u` deals — after it is dealt — uses one stack of buff `key`. */
|
||||
function installOpusUse(battle, u, key) {
|
||||
battle.on('damaged', (c) => {
|
||||
if (c.source !== u || c.type === 'element' || !c.target || c.target.side === u.side) return;
|
||||
const b = u.findBuff(key);
|
||||
if (!b) return;
|
||||
if (b.stacks > 1) { b.stacks -= 1; u.markDirty(); } else battle.removeBuff(u, b);
|
||||
}, { owner: u });
|
||||
}
|
||||
|
||||
/** 夜幕突袭 / its copy at a deployment of `u` (record `rec`: its blackboard and x-4). */
|
||||
function nightRaid(battle, u, rec) {
|
||||
const b = rec?.bb ?? {};
|
||||
const grid = Array.isArray(rec?.rangeGrid) && rec.rangeGrid.length ? rec.rangeGrid : X4;
|
||||
const foes = battle.enemiesInKeys(absoluteRangeKeys(grid, u.tileR, u.tileC, u.dir, 0), u, { canHitFly: false, groundOnly: true });
|
||||
const pick = RAID[battle.rng.int(RAID.length)]; // one ability per cast (RandomCastAbility)
|
||||
const dur = num(b[pick.key]), scale = num(b.atk_scale), force = num(b.force, 0);
|
||||
for (const e of foes) {
|
||||
if (!e.alive) continue;
|
||||
if (scale > 0) battle.dealDamage(u, e, { amount: u.s.atk * scale, type: 'phys', isSkill: true, tags: ['skill', TAG_RAID] });
|
||||
if (e.alive) battle.push(e, force, { from: u });
|
||||
if (e.alive && dur > 0) battle.applyStatus(e, pick.status, { duration: dur, source: u });
|
||||
}
|
||||
battle.fx('aoe', { x: u.x, y: u.y, radius: 1, id: u.id, skill: TAG_RAID });
|
||||
}
|
||||
|
||||
/** The deploy-time part of skill record `rec` (his or the twin's copy) on `u`; `key` names its buffs. */
|
||||
function deployEffect(battle, u, rec, key) {
|
||||
const id = rec?.skillId ?? rec?.id ?? null;
|
||||
if (id === S1 || id === TS1) nightPhantom(battle, u, rec.bb ?? {}, key);
|
||||
else if (id === S2 || id === TS2) bloodyOpus(battle, u, rec.bb ?? {}, key);
|
||||
else if (id === S3 || id === TS3) nightRaid(battle, u, rec);
|
||||
}
|
||||
|
||||
/**
|
||||
* The twin's kit (`owner` = 傀影): 禁疗, its skill copy at each of its deployments, EXE-Y stage 2+'s shared ATK, and its own
|
||||
* redeploys (withdrawn with him; back on its tile once its timer has run from its last deployment, he stands and the DP is
|
||||
* there).
|
||||
*/
|
||||
function twinKit(t, owner) {
|
||||
const rec = t.def?.skill ?? null;
|
||||
const key = 'phatom:twin:skill';
|
||||
const cut = num(tokTalentBb(t.def, 'respawn_time').respawn_time); // 虚影精通 (−10; EXE-X stage 3: −16)
|
||||
const both = num(tokTalentBb(t.def, 'atk').atk); // EXE-Y stage 2+: 本体和虚影同时在场时
|
||||
return {
|
||||
skill: { kind: 'passive' },
|
||||
install(battle, unit) {
|
||||
battle.addBuff(unit, { key: 'phatom:twin:abnormal', flags: { noHeal: true }, persist: true, allowDead: true }); // 持有禁疗
|
||||
if (rec?.id === TS2) installOpusUse(battle, unit, key);
|
||||
battle.on('deploy', (c) => {
|
||||
if (c.unit !== unit || c.move || !up(unit)) return;
|
||||
unit.mem.twinReadyAt = battle.time + Math.max(0, num(unit.base.respawnTime) + cut); // timer from its deployment
|
||||
deployEffect(battle, unit, rec ? { ...rec, skillId: rec.id } : null, key);
|
||||
}, { owner: unit, priority: -10 });
|
||||
if (both > 0) toggleBuff(battle, unit, 'module:phatom:both', () => up(owner), { atkPct: both });
|
||||
battle.on('death', (c) => {
|
||||
if (c.unit !== unit || battle.finished) return;
|
||||
unit.removed = false; // the piece stays (Battle.redeploy accepts it; its hooks are kept)
|
||||
if (unit.mem.twinRetry) return;
|
||||
unit.mem.twinRetry = battle.every(RETRY, (b, sched) => {
|
||||
const stop = () => { sched.cancel(); unit.mem.twinRetry = null; };
|
||||
if (unit.alive || unit.removed || b.finished) { stop(); return; }
|
||||
if (!up(owner) || b.time + 1e-9 < num(unit.mem.twinReadyAt, 0)) return;
|
||||
if (b.redeploy(unit, { free: false })) stop();
|
||||
}, { owner: unit });
|
||||
}, { owner: unit, priority: -10 });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
char_250_phatom: (bb, chess) => {
|
||||
const lonelyAtk = num(traitBb(chess).atk); // EXE-Y trait (EXE-X: withdraw_cost_recover_ratio, ISW-A: 集成战略-only)
|
||||
const t0desc = String((chess?.talents ?? []).find((t) => t && t.index === 0)?.desc ?? '');
|
||||
const recOf = (id) => skillRec(chess, id);
|
||||
return {
|
||||
skills: {
|
||||
[S1]: { kind: 'passive' },
|
||||
[S2]: { kind: 'passive' },
|
||||
[S3]: { kind: 'passive' },
|
||||
},
|
||||
talents: [
|
||||
{ install(battle, unit) { // 镜中虚影: his twin pieces run the twin's kit; withdrawn when he leaves; EXE-Y stage 2+: both +ATK
|
||||
for (const t of twinsOf(battle, unit)) {
|
||||
if (t.alive || t.deployed) continue;
|
||||
if (t.kit) battle.offOwner(t); // a piece set up before him: drop its generic kit's hooks
|
||||
battle._setupUnit(t, twinKit(t, unit));
|
||||
}
|
||||
battle.on('death', (c) => {
|
||||
if (c.unit !== unit) return;
|
||||
// "傀影退场时强制撤退场上的镜中虚影" (KillTokens): a withdrawal, not a knock-out
|
||||
for (const t of twinsOf(battle, unit)) if (t.alive) battle.retreat(t, { reason: 'retreat' });
|
||||
}, { owner: unit });
|
||||
// 本体和虚影同时在场时,攻击力各+N%: the twin's hidden module talent (his text when no twin def resolves)
|
||||
const both = num(tokTalentBb(battle.tokenDef(TWIN, unit), 'atk').atk, /本体和虚影同时在场时/.test(t0desc)
|
||||
? num(+(/攻击力各\+(\d+)%/.exec(t0desc)?.[1]) / 100) : 0);
|
||||
if (both > 0) toggleBuff(battle, unit, 'module:phatom:both', () => twinsOf(battle, unit).some(up), { atkPct: both });
|
||||
} },
|
||||
// 虚影精通 is the twin's own talent (its redeploy time): twinKit
|
||||
],
|
||||
install(battle, unit) {
|
||||
// the picked skill's deploy-time effects (S1 dodge + barrier, S2 stacks, S3 burst), after the deploy-time buffs
|
||||
const rec = recOf(chess?.skill?.skillId);
|
||||
if (rec?.skillId === S2) installOpusUse(battle, unit, 'phatom:skill');
|
||||
battle.on('deploy', (c) => {
|
||||
if (c.unit !== unit || c.move || !up(unit)) return;
|
||||
deployEffect(battle, unit, rec, 'phatom:skill');
|
||||
}, { owner: unit, priority: -10 });
|
||||
// EXE-Y "周围四格没有友方干员时攻击力+10%"
|
||||
if (lonelyAtk > 0) toggleBuff(battle, unit, 'trait:phatom:lonely', () => noOperatorBeside(battle, unit), { atkPct: lonelyAtk });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,302 @@
|
||||
// test/content/op_crosly.test.js — the 自选 operator kit of 弑君者 (char_1502_crosly, 6★ 处决者; kit
|
||||
// server/sim/content/kits/ops/op-crosly.js), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy) in
|
||||
// every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module, EXE-X 弑君刃
|
||||
// or EXE-Y 木炭画 at stage 1 (tier 5) / 3 (tier 6). Every number is read back from data/backups.json (the form of that slot
|
||||
// status); the fidelity checklist of kits/README.md item by item.
|
||||
// Run: node --test test/content/op_crosly.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
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';
|
||||
|
||||
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
|
||||
const CHESS = load('chess');
|
||||
const BACKUPS = load('backups');
|
||||
const CROSLY = 'char_1502_crosly';
|
||||
const FORMS = BACKUPS.units[CROSLY].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const EX = 'uniequip_002_crosly', EY = 'uniequip_003_crosly';
|
||||
const S1 = 'skchr_crosly_1', S2 = 'skchr_crosly_2', S3 = 'skchr_crosly_3';
|
||||
const YAK = 'chess_char_1_02_a', TEXAS = 'chess_char_1_08_a';
|
||||
const formOf = (tier, elite) => FORMS[elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0'];
|
||||
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
||||
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
||||
/** The talents a form fights with: the module's changes composed (index 0 / 1, −1 = hidden). */
|
||||
const talentOf = (tier, elite, mod, i) => {
|
||||
const base = formOf(tier, elite).talents.find((t) => t.index === i)?.bb ?? {};
|
||||
const ch = (elite ? modOf(tier, mod)?.talentChanges ?? [] : []).find((t) => t.talentIndex === i)?.bb ?? {};
|
||||
return { ...base, ...ch };
|
||||
};
|
||||
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_hitter: dummy('enemy_hitter', { atk: 200, range: 1.5, bat: 0.5 }), enemy_caster: dummy('enemy_caster', { atk: 200, range: 1.5, bat: 0.5, dmgType: 'arts' }),
|
||||
enemy_flyhitter: dummy('enemy_flyhitter', { atk: 200, range: 1.5, bat: 0.5, motion: 'FLY' }),
|
||||
};
|
||||
/** Every 自选 form: [tier, elite, module]. */
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, EX, EY].map((m) => [t, true, m]))];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
|
||||
/** A battle with 弑君者 as uid 1 at (row, col) facing RIGHT, plus `others`. */
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 4, others = [], seed = 5 } = {}) {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'death'], captureNoisy: true,
|
||||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: CROSLY, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1) };
|
||||
}
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
/** Knock `u` out and bring it straight back on its tile (free). */
|
||||
function redeployNow(h, u) {
|
||||
h.b.kill(u, null);
|
||||
assert.ok(h.b.redeploy(u, { free: true }), `${u.name} redeployed`);
|
||||
}
|
||||
/**
|
||||
* Watch the 命中率 cut `hitter` suffers from now on: its attacks counted before (priority 295) and after (270) the smoke's
|
||||
* handler (280); allies are kept alive. Returns () => { n, rate }.
|
||||
*/
|
||||
function watch(h, hitter) {
|
||||
const seen = new Set(), passed = new Set();
|
||||
h.b.on('hit', (c) => { if (c.source === hitter && c.dmg.isAttack) seen.add(c.dmg.attackId); }, { priority: 295 });
|
||||
h.b.on('hit', (c) => { if (c.source === hitter && c.dmg.isAttack && !c.dmg.cancel) passed.add(c.dmg.attackId); }, { priority: 270 });
|
||||
h.b.on('fatal', (c) => { if (c.unit.side === 'ally') c.prevented = true; });
|
||||
return () => ({ n: seen.size, rate: seen.size ? 1 - passed.size / seen.size : 0 });
|
||||
}
|
||||
|
||||
test('弑君者 in every 自选 form: her operator kit (all three skills authored), the form\'s stats + module attributes, 1-1, melee ground-only, 22 s redeploy, 叙拉古, no 特质; each skill a timed deployment skill (被动 ON_DEPLOY), running from the deployment', () => {
|
||||
assert.equal(OPERATOR_KITS[CROSLY], KITS[CROSLY]);
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
for (const skill of [0, 1, 2]) {
|
||||
const { h, u } = field({ tier, elite, mod, skill });
|
||||
const form = formOf(tier, elite), m = elite ? modOf(tier, mod) : null, rec = form.skills[skill];
|
||||
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [CROSLY, SLOT[tier], rec.skillId, false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.respawnTime, u.base.cost],
|
||||
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), 22, 12], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.profile.attack, u.profile.dmgType, u.profile.canHitFly], ['melee', 'phys', false], `${label(f)}: 处决者`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 1-1 (S3 too)`);
|
||||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['siracusaShip'], []], `${label(f)}: bonds / 特质`);
|
||||
assert.deepEqual([rec.skillType, rec.spType, u.skill.kind, u.skill.rule, u.skill.active, u.skill.activations], ['PASSIVE', 'ON_DEPLOY', 'duration', 'NEVER', true, 1], `${label(f)}: S${skill + 1} from the deployment`);
|
||||
approx(u.skill.timeLeft, [rec.duration, rec.bb.duration, rec.duration][skill], `${label(f)}: S${skill + 1} duration`, 0.01);
|
||||
assert.equal(u.s.blockCnt, skill === 2 ? 0 : 1, `${label(f)}: blocks ${skill === 2 ? 0 : 1}`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
// the numbers (zh_CN): E2 Lv1 1322 / 425 / 273, E2 Lv60 1569 / 505 / 307; EXE-X +90 / +34 / +23 → +130 / +45 / +30,
|
||||
// EXE-Y +84 / +63 → +135 / +88 HP / ATK
|
||||
assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.maxHp, FORMS['2/60/7/1'].stats.atk], [1322, 425, 1569, 505]);
|
||||
assert.deepEqual([modOf(5, EX).attr, modOf(6, EX).attr, modOf(5, EY).attr, modOf(6, EY).attr],
|
||||
[{ maxHp: 90, atk: 34, def: 23 }, { maxHp: 130, atk: 45, def: 30 }, { maxHp: 84, atk: 63 }, { maxHp: 135, atk: 88 }]);
|
||||
});
|
||||
|
||||
test('a 自选 pick: 弑君者 is offered at tiers 5 and 6 (she has a kit) and a roster with her passes validateDiyPicks', () => {
|
||||
const data = { chess: CHESS, backups: BACKUPS };
|
||||
assert.ok(KITTED_CHARS.includes(CROSLY));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(CROSLY), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[5]]: { charId: CROSLY, skillIndex: 2, uniEquipId: EY } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[5]]: { charId: CROSLY, skillIndex: 2, uniEquipId: EY } } });
|
||||
});
|
||||
|
||||
test('S1 尘烟蔽目 (被动): for 10 s from each deployment ATK +65 % / +80 % and 30 % / 40 % physical and arts dodge', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, EX]]) {
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.prob], [10, elite ? 0.8 : 0.65, elite ? 0.4 : 0.3]);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0 });
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), 'ATK');
|
||||
assert.deepEqual([u.s.dodgePhys, u.s.dodgeArts], [sk.bb.prob, sk.bb.prob], 'dodge');
|
||||
h.run(10.1);
|
||||
assert.deepEqual([u.skill.active, u.s.atk, u.s.dodgePhys, u.s.dodgeArts], [false, u.base.atk, 0, 0], 'over');
|
||||
redeployNow(h, u);
|
||||
assert.ok(u.skill.active && u.skill.activations === 2, 'again at a redeployment');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 硝烟震爆 (被动): for 8 s no attacks and taunt level −1; then 270 % / 350 % ATK physical to every ground enemy of the x-4 smoke (fresh ones ×T2); no blast when she is stunned as it ends', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, EY]]) {
|
||||
const sk = skillOf(tier, elite, S2), t1 = talentOf(tier, elite, mod, 1);
|
||||
assert.deepEqual([sk.bb.duration, sk.bb.taunt_level, sk.bb['attack@atk_scale_s2'], sk.bb.talent_scale], [8, -1, elite ? 3.5 : 2.7, 2]);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 1 });
|
||||
assert.equal(u.s.taunt, -1, 'taunt level −1');
|
||||
const front = h.spawn('enemy_dummy', { pos: [10, 5] }), diag = h.spawn('enemy_dummy', { pos: [11, 3] });
|
||||
const marked = h.spawn('enemy_dummy', { pos: [9, 4] });
|
||||
const fly = h.spawn('enemy_fly', { pos: [9, 5] }), far = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
h.step();
|
||||
h.b.dealDamage(marked, u, { amount: 1, type: 'phys', canDodge: false }); // it has damaged her
|
||||
h.run(7);
|
||||
assert.equal(h.hooksOf('damaged').filter((c) => c.source === u).length, 0, 'no attacks while it runs');
|
||||
assert.ok(h.runUntil(() => !u.skill.active, 2), 'over after 8 s');
|
||||
const blast = h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.tags?.includes('crosly:blast'));
|
||||
assert.deepEqual(blast.map((c) => c.target).sort((a, b) => a.id - b.id), [front, diag, marked].sort((a, b) => a.id - b.id), 'the ground enemies of her x-4');
|
||||
for (const c of blast) {
|
||||
approx(c.amount, u.s.atk * sk.bb['attack@atk_scale_s2'] * (c.target === marked ? 1 : t1.damage_scale), `${c.target === marked ? 'marked' : 'fresh'}: ${sk.bb['attack@atk_scale_s2'] * 100} %`);
|
||||
assert.deepEqual([c.type, c.dmg.isSkill], ['phys', true]);
|
||||
}
|
||||
assert.ok(!blast.some((c) => c.target === fly || c.target === far), 'flyer / outside untouched');
|
||||
assert.equal(u.s.taunt, 0, 'taunt back');
|
||||
// stunned as it ends: no blast
|
||||
redeployNow(h, u);
|
||||
h.run(7.5);
|
||||
h.b.applyStatus(u, 'stun', { duration: 2, force: true });
|
||||
const n0 = h.hooksOf('damaged').length;
|
||||
h.run(1);
|
||||
assert.ok(!u.skill.active, 'ended');
|
||||
assert.equal(h.hooksOf('damaged').slice(n0).filter((c) => c.dmg?.tags?.includes('crosly:blast')).length, 0, 'stunned: no blast');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 烽烟行刑场 (被动, 16 s): 隐匿 and block count 0 (enemies neither target nor are blocked by her); a strike every 2 s on one unmarked ground enemy of the x-1 — 2 hits × 150 % / 190 % ATK and 晕眩 1.5 s — marking it 6 s; her range stays 1-1; all back after', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, EX]]) {
|
||||
const sk = skillOf(tier, elite, S3), t1 = talentOf(tier, elite, mod, 1);
|
||||
assert.deepEqual([sk.duration, sk.rangeId, sk.bb['attack@s3_cd'], sk.bb['attack@times'], sk.bb['attack@atk_scale_s3'], sk.bb['attack@stun'], sk.bb.mark_duration],
|
||||
[16, 'x-1', 2, 2, elite ? 1.9 : 1.5, 1.5, 6]);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2 });
|
||||
assert.deepEqual([!!u.s.flags.stealth, u.s.blockCnt, u.liveRangeGrid], [true, 0, formOf(tier, elite).rangeGrid], '隐匿, block 0, 1-1');
|
||||
const a = h.spawn('enemy_dummy', { pos: [10, 5] }), b = h.spawn('enemy_dummy', { pos: [12, 4] }), c = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
const fly = h.spawn('enemy_fly', { pos: [9, 4] }), far = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
const hitter = h.spawn('enemy_hitter', { pos: [11, 4] });
|
||||
h.run(15.5);
|
||||
const hits = h.hooksOf('damaged').filter((x) => x.source === u && x.dmg?.isAttack);
|
||||
assert.equal(hits.length % 2, 0, 'two hits a strike');
|
||||
const strikes = [];
|
||||
for (const x of hits) if (!strikes.length || Math.abs(strikes[strikes.length - 1].t - x.t) > 1e-6) strikes.push({ t: x.t, target: x.target });
|
||||
assert.ok(strikes.length >= 7, `${strikes.length} strikes`);
|
||||
for (let i = 1; i < strikes.length; i++) approx(strikes[i].t - strikes[i - 1].t, 2, 'every 2 s', 0.02);
|
||||
assert.ok(hits.every((x) => [a, b, c, hitter].includes(x.target)), 'unmarked ground enemies of the x-1 only (no flyer, nothing 3 tiles away)');
|
||||
assert.ok(!hits.some((x) => x.target === fly || x.target === far));
|
||||
for (const x of hits) {
|
||||
assert.deepEqual([x.type, x.dmg.isSkill], ['phys', true]);
|
||||
approx(x.amount, u.s.atk * sk.bb['attack@atk_scale_s3'] * t1.damage_scale, `${sk.bb['attack@atk_scale_s3'] * 100} % ×T2 (fresh)`);
|
||||
}
|
||||
// the 6 s mark: never the same target twice within 6 s
|
||||
const last = new Map();
|
||||
for (const st of strikes) {
|
||||
if (last.has(st.target)) assert.ok(st.t - last.get(st.target) >= 6 - 1e-6, `marked 6 s (${st.target.defId})`);
|
||||
last.set(st.target, st.t);
|
||||
}
|
||||
const stuns = h.hooksOf('statusApplied').filter((x) => x.source === u && x.status === 'stun');
|
||||
assert.equal(stuns.length, strikes.length, 'one 晕眩 a strike');
|
||||
for (const x of stuns) approx(x.duration, sk.bb['attack@stun'], '1.5 s');
|
||||
assert.equal(h.hooksOf('damaged').filter((x) => x.source === hitter && x.target === u).length, 0, '隐匿: the hitter beside her never targets her');
|
||||
assert.equal(u.blocking.length, 0, 'blocks nothing');
|
||||
h.run(1);
|
||||
assert.deepEqual([u.skill.active, !!u.s.flags.stealth, u.s.blockCnt], [false, false, 1], 'all back after 16 s');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T1 吞咽苦厄: while her skill runs, the attacks of ground enemies in her smoke miss 20 % (EXE-X stage 3: 25 %; S2: ×2) — x-4, S3 x-1; none outside it, none for air units, none once the skill is over', () => {
|
||||
// S1, normal: a hitter in her x-4 (beside her) — 20 %; another two tiles ahead — 0 %
|
||||
{
|
||||
const t0 = talentOf(5, false, null, 0);
|
||||
assert.deepEqual([t0.damage_hitrate_physical, t0.damage_hitrate_magical], [-0.2, -0.2]);
|
||||
const { h, u } = field({ tier: 5, skill: 0, others: [{ uid: 2, chessId: YAK, row: 10, col: 7 }] });
|
||||
const inside = h.spawn('enemy_caster', { pos: [11, 4] }), outside = h.spawn('enemy_hitter', { pos: [10, 6] });
|
||||
u.skill.extend(400);
|
||||
const wIn = watch(h, inside), wOut = watch(h, outside);
|
||||
h.run(300);
|
||||
const rIn = wIn(), rOut = wOut();
|
||||
assert.ok(rIn.n > 300 && Math.abs(rIn.rate - 0.2) < 0.05, `arts attacker in the x-4: ${rIn.rate} of ${rIn.n}`);
|
||||
assert.ok(rOut.n > 300 && rOut.rate === 0, `two tiles ahead: ${rOut.rate} of ${rOut.n}`);
|
||||
u.skill.extend(-999);
|
||||
h.step(2);
|
||||
const wAfter = watch(h, inside);
|
||||
h.run(60);
|
||||
const after = wAfter();
|
||||
assert.ok(after.n > 60 && after.rate === 0, 'skill over: no smoke');
|
||||
done(h);
|
||||
}
|
||||
// S2 at EXE-X stage 3: 25 % × 2
|
||||
{
|
||||
const t0 = talentOf(6, true, EX, 0);
|
||||
assert.equal(t0.damage_hitrate_physical, -0.25);
|
||||
const { h, u } = field({ tier: 6, elite: true, mod: EX, skill: 1 });
|
||||
const hitter = h.spawn('enemy_hitter', { pos: [10, 5] });
|
||||
const fly = h.spawn('enemy_flyhitter', { pos: [9, 4] });
|
||||
u.skill.extend(400);
|
||||
const w = watch(h, hitter), wFly = watch(h, fly);
|
||||
h.run(300);
|
||||
const r = w(), rFly = wFly();
|
||||
assert.ok(r.n > 300 && Math.abs(r.rate - 0.5) < 0.05, `S2 ×2: ${r.rate} of ${r.n}`);
|
||||
assert.ok(rFly.n > 300 && rFly.rate === 0, `an air unit in the smoke: ${rFly.rate} of ${rFly.n}`);
|
||||
done(h);
|
||||
}
|
||||
// S3: the x-1 reaches two tiles ahead
|
||||
{
|
||||
const { h, u } = field({ tier: 5, skill: 2, others: [{ uid: 2, chessId: YAK, row: 10, col: 7 }] });
|
||||
const hitter = h.spawn('enemy_hitter', { pos: [10, 6] });
|
||||
u.skill.extend(400);
|
||||
const w = watch(h, hitter);
|
||||
h.run(300);
|
||||
const r = w();
|
||||
assert.ok(r.n > 150 && Math.abs(r.rate - 0.2) < 0.06, `S3 x-1: ${r.rate} of ${r.n}`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T2 弑君者威名: physical damage she deals to a ground enemy that has not damaged her this deployment ×1.2 (EXE-Y stage 3: ×1.35, and her 晕眩 on it ×1.5); a 持续伤害 tick marks nothing; the marks go when she leaves the field', () => {
|
||||
for (const f of [[5, false, null], [6, true, EY], [5, true, EY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const t1 = talentOf(tier, elite, mod, 1), hidden = talentOf(tier, elite, mod, -1);
|
||||
const y3 = elite && mod === EY && tier === 6;
|
||||
assert.deepEqual([t1.damage_scale, hidden.one_minus_status_resistance ?? 0], y3 ? [1.35, 0.5] : [1.2, 0], label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0 });
|
||||
h.run(10.1); // S1 over: plain attacks
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
const hitsOn = () => h.hooksOf('damaged').filter((c) => c.source === u && c.target === e && c.dmg?.isAttack);
|
||||
h.run(2);
|
||||
approx(hitsOn().slice(-1)[0].amount, u.s.atk * t1.damage_scale, `${label(f)}: fresh ×${t1.damage_scale}`);
|
||||
h.b.dealDamage(e, u, { amount: 5, type: 'true', canDodge: false, tags: ['dot'] });
|
||||
h.run(2);
|
||||
approx(hitsOn().slice(-1)[0].amount, u.s.atk * t1.damage_scale, `${label(f)}: a DoT tick marks nothing`);
|
||||
h.b.dealDamage(e, u, { amount: 5, type: 'phys', canDodge: false });
|
||||
h.run(2);
|
||||
approx(hitsOn().slice(-1)[0].amount, u.s.atk, `${label(f)}: it damaged her ⇒ ×1`);
|
||||
// EXE-Y stage 3: a stun of hers lasts ×1.5 on a fresh enemy only
|
||||
const fresh = h.spawn('enemy_dummy', { pos: [9, 5] });
|
||||
h.b.applyStatus(fresh, 'stun', { duration: 2, source: u });
|
||||
h.b.applyStatus(e, 'stun', { duration: 2, source: u });
|
||||
const st = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'stun').slice(-2);
|
||||
approx(st[0].duration, y3 ? 3 : 2, `${label(f)}: fresh ×${y3 ? 1.5 : 1}`);
|
||||
approx(st[1].duration, 2, `${label(f)}: marked ×1`);
|
||||
// her redeployment clears the marks
|
||||
redeployNow(h, u);
|
||||
h.run(10.1);
|
||||
h.run(2);
|
||||
approx(hitsOn().slice(-1)[0].amount, u.s.atk * t1.damage_scale, `${label(f)}: fresh again after a redeployment`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('EXE-X 弑君刃: the refund has no effect (no retreat in battle); EXE-Y 木炭画: ATK +10 % while no allied operator stands on the 4 tiles beside her (stages 1 and 3)', () => {
|
||||
{
|
||||
const { h, u } = field({ tier: 6, elite: true, mod: EX, skill: 0 });
|
||||
assert.deepEqual(u.def.traitBb, { withdraw_cost_recover_ratio: 0.8 });
|
||||
h.b.players[0].dp = 3;
|
||||
h.b.retreat(u);
|
||||
h.step();
|
||||
assert.equal(h.b.players[0].dp, 3, 'no refund');
|
||||
done(h);
|
||||
}
|
||||
for (const f of [[5, true, EY], [6, true, EY], [6, true, EX]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const { h, u } = field({ tier, elite, mod, skill: 1, others: [{ uid: 2, chessId: TEXAS, row: 12, col: 7 }] });
|
||||
const texas = h.unit(2);
|
||||
assert.equal(!!u.findBuff('trait:crosly:lonely'), mod === EY, `${label(f)}: alone`);
|
||||
if (mod === EY) approx(u.s.atk, u.base.atk * 1.1, `${label(f)}: ATK +10 %`);
|
||||
assert.ok(h.b.relocate(texas, 10, 5));
|
||||
h.step();
|
||||
assert.equal(u.findBuff('trait:crosly:lonely'), null, `${label(f)}: an operator beside her`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,293 @@
|
||||
// test/content/op_haak.test.js — the 自选 operator kit of 阿 (char_225_haak, 6★ 怪杰; kit
|
||||
// server/sim/content/kits/ops/op-haak.js), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy) in
|
||||
// every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module, GEE-X
|
||||
// 什锦果味医用箱 or GEE-Y 毒医残章 at stage 1 (tier 5) / 3 (tier 6). Every number is read back from data/backups.json (the
|
||||
// form of that slot status); the fidelity checklist of kits/README.md item by item.
|
||||
// Run: node --test test/content/op_haak.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
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';
|
||||
|
||||
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
|
||||
const CHESS = load('chess');
|
||||
const BACKUPS = load('backups');
|
||||
const HAAK = 'char_225_haak';
|
||||
const FORMS = BACKUPS.units[HAAK].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const GX = 'uniequip_002_haak', GY = 'uniequip_003_haak';
|
||||
const S1 = 'skchr_haak_1', S2 = 'skchr_haak_2', S3 = 'skchr_haak_3';
|
||||
const YAK = 'chess_char_1_02_a', TEXAS = 'chess_char_1_08_a', LISKAM = 'chess_char_1_20_a';
|
||||
const formOf = (tier, elite) => FORMS[elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0'];
|
||||
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
||||
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
||||
/** The talents a form fights with: the module's changes composed (index 0 / 1). */
|
||||
const talentOf = (tier, elite, mod, i) => {
|
||||
const base = formOf(tier, elite).talents.find((t) => t.index === i)?.bb ?? {};
|
||||
const ch = (elite ? modOf(tier, mod)?.talentChanges ?? [] : []).find((t) => t.talentIndex === i)?.bb ?? {};
|
||||
return { ...base, ...ch };
|
||||
};
|
||||
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' }) };
|
||||
/** Every 自选 form: [tier, elite, module]. */
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, GX, GY].map((m) => [t, true, m]))];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
|
||||
/** A battle with 阿 as uid 1 at (row, col) facing RIGHT, plus `others`. */
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 4, others = [], seed = 5 } = {}) {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'heal', 'spGain'], captureNoisy: true,
|
||||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: HAAK, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1) };
|
||||
}
|
||||
const atkHits = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack);
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
/** Keep him from casting (no SP). */
|
||||
function noSkill(h, u) {
|
||||
u.skill.charges = 0; u.skill.sp = 0;
|
||||
h.b.addBuff(u, { key: 'test:noSp', flags: { noSp: true } });
|
||||
}
|
||||
|
||||
test('阿 in every 自选 form: his operator kit (all three skills authored), the form\'s stats + module attributes, 3-3, blocks 1, ranged physical arrows that hit air units, the 怪杰 drain, 炎, no 特质, the data triggers', () => {
|
||||
assert.equal(OPERATOR_KITS[HAAK], KITS[HAAK]);
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
for (const skill of [0, 1, 2]) {
|
||||
const { h, u } = field({ tier, elite, mod, skill });
|
||||
const form = formOf(tier, elite), m = elite ? modOf(tier, mod) : null;
|
||||
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [HAAK, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.aspd, u.base.respawnTime],
|
||||
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def, 100 + (m?.attr.aspd ?? 0), 70 + (m?.attr.respawnTime ?? 0)], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.profile.projectile, u.profile.hpDrain], [1, 'ranged', 'phys', true, 'arrow', 0.01], `${label(f)}: 怪杰`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-3`);
|
||||
assert.equal(form.rangeId, '3-3');
|
||||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['yanShip'], []], `${label(f)}: bonds / 特质`);
|
||||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target him`);
|
||||
assert.deepEqual([u.skill.kind, u.skill.rule, form.skills[skill].trigger.rule], ['duration', 'DEFAULT', 'DEFAULT'], `${label(f)}: S${skill + 1} trigger (data)`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
// the numbers of the forms (zh_CN): E2 Lv1 1627 / 583 / 121, E2 Lv60 1897 / 663 / 142; GEE-X +135 / +37 → +240 / +57 HP / ATK,
|
||||
// GEE-Y +43 / +3 → +75 / +5 ATK / ASPD and respawn −15
|
||||
assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.maxHp, FORMS['2/60/7/1'].stats.atk], [1627, 583, 1897, 663]);
|
||||
assert.deepEqual([modOf(5, GX).attr, modOf(6, GX).attr, modOf(5, GY).attr, modOf(6, GY).attr],
|
||||
[{ maxHp: 135, atk: 37 }, { maxHp: 240, atk: 57 }, { atk: 43, aspd: 3, respawnTime: -15 }, { atk: 75, aspd: 5, respawnTime: -15 }]);
|
||||
});
|
||||
|
||||
test('a 自选 pick: 阿 is offered at tiers 5 and 6 (he has a kit) and a roster with him passes validateDiyPicks', () => {
|
||||
const data = { chess: CHESS, backups: BACKUPS };
|
||||
assert.ok(KITTED_CHARS.includes(HAAK));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(HAAK), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: HAAK, skillIndex: 2, uniEquipId: GY } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[6]]: { charId: HAAK, skillIndex: 2, uniEquipId: GY } } });
|
||||
});
|
||||
|
||||
test('trait 怪杰: 1 % of his max HP lost every second (a 流失, never below 1 HP); he shoots an air unit in his range', () => {
|
||||
for (const f of [[5, false, null], [6, true, GX]]) {
|
||||
const { h, u } = field({ tier: f[0], elite: f[1], mod: f[2], skill: 0 });
|
||||
noSkill(h, u);
|
||||
assert.deepEqual(u.def.traitBb, { hp_ratio: 0.01 }, label(f));
|
||||
h.run(3.05);
|
||||
approx(u.hp, u.s.maxHp * (1 - 0.03), `${label(f)}: three ticks of 1 %`, 1e-6);
|
||||
u.hp = 5;
|
||||
h.run(10);
|
||||
assert.equal(u.hp, 1, `${label(f)}: not lethal — 1 HP kept`);
|
||||
assert.ok(u.alive);
|
||||
u.hp = u.s.maxHp;
|
||||
const fly = h.spawn('enemy_fly', { pos: [10, 6] });
|
||||
h.run(3);
|
||||
assert.ok(atkHits(h, u).some((c) => c.target === fly && c.type === 'phys'), `${label(f)}: physical arrows at the flyer`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('GEE-X “什锦果味医用箱” (stages 1 and 3): SP recovery +0.25/s while his HP ratio is at least 80 %, none below; no such bonus without it', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2 });
|
||||
const gx = elite && mod === GX;
|
||||
if (gx) {
|
||||
assert.equal(modOf(tier, GX).traitOverride.moduleDesc, '生命值高于80%时,技力自然回复速度+0.25/秒');
|
||||
assert.deepEqual(modOf(tier, GX).talentChanges.find((t) => t.talentIndex === -1).bb, { sp_recovery_per_sec: 0.25 });
|
||||
}
|
||||
h.step();
|
||||
assert.equal(u.s.spRecovery, gx ? 1.25 : 1, `${label(f)}: full HP`);
|
||||
const sp0 = u.skill.sp;
|
||||
h.run(2);
|
||||
approx(u.skill.sp - sp0, 2 * (gx ? 1.25 : 1), `${label(f)}: SP over 2 s`, 0.02);
|
||||
u.hp = u.s.maxHp * 0.79;
|
||||
h.step();
|
||||
assert.equal(u.s.spRecovery, 1, `${label(f)}: below 80 %`);
|
||||
u.hp = u.s.maxHp * 0.8;
|
||||
h.step();
|
||||
assert.equal(u.s.spRecovery, gx ? 1.25 : 1, `${label(f)}: at 80 %`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T1 混合药物射击: every attack on an enemy rolls one of four with equal weight — heal 15 % max HP (×T2), that attack ×1.5, 停顿 1.4 s, 晕眩 1 s; GEE-X stage 3: 20 % / ×1.65 / 1.6 s / 1.2 s and 30 % all four at once', () => {
|
||||
for (const f of [[5, false, null], [6, true, null], [5, true, GX], [6, true, GX], [6, true, GY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const t0 = talentOf(tier, elite, mod, 0), t1 = talentOf(tier, elite, mod, 1);
|
||||
const x3 = elite && mod === GX && tier === 6;
|
||||
assert.deepEqual([t0.hp_ratio, t0.atk_scale, t0.sluggish, t0.stun, t0.prob ?? 0], x3 ? [0.2, 1.65, 1.6, 1.2, 0.3] : [0.15, 1.5, 1.4, 1, 0], label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 7 });
|
||||
noSkill(h, u);
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
h.run(400);
|
||||
const hits = atkHits(h, u).filter((c) => c.target === e);
|
||||
const n = hits.length;
|
||||
assert.ok(n > 250, `${label(f)}: ${n} attacks`);
|
||||
const atk = u.s.atk;
|
||||
const boosted = hits.filter((c) => Math.abs(c.amount - atk * t0.atk_scale) < 1e-6);
|
||||
assert.equal(boosted.length + hits.filter((c) => Math.abs(c.amount - atk) < 1e-6).length, n, `${label(f)}: ×1 or ×${t0.atk_scale}`);
|
||||
const st = h.hooksOf('statusApplied').filter((c) => c.source === u && c.target === e);
|
||||
const slug = st.filter((c) => c.status === 'sluggish'), stun = st.filter((c) => c.status === 'stun');
|
||||
for (const c of slug) approx(c.duration, t0.sluggish, `${label(f)}: 停顿`);
|
||||
for (const c of stun) approx(c.duration, t0.stun, `${label(f)}: 晕眩`);
|
||||
const heals = h.hooksOf('heal').filter((c) => c.source === u && c.target === u && !c.opts?.regen);
|
||||
for (const c of heals.slice(0, 5)) approx(c.amount, u.s.maxHp * t0.hp_ratio * t1.heal_scale, `${label(f)}: heal ${t0.hp_ratio * 100} % × ${t1.heal_scale}`, 1e-6);
|
||||
const share = x3 ? 0.3 + 0.7 / 4 : 0.25;
|
||||
for (const [what, list] of [['×atk', boosted], ['停顿', slug], ['晕眩', stun], ['heal', heals]]) {
|
||||
assert.ok(Math.abs(list.length / n - share) < 0.07, `${label(f)}: ${what} ${list.length} / ${n} ≈ ${share}`);
|
||||
}
|
||||
// all four at once: only GEE-X stage 3 — the boosted attacks that also stun, slow and heal in the same tick
|
||||
const at = (list) => new Set(list.map((c) => c.t.toFixed(4)));
|
||||
const sS = at(slug), sT = at(stun), sH = at(heals);
|
||||
const four = boosted.filter((c) => sS.has(c.t.toFixed(4)) && sT.has(c.t.toFixed(4)) && sH.has(c.t.toFixed(4))).length;
|
||||
if (x3) assert.ok(Math.abs(four / n - 0.3) < 0.07, `${label(f)}: all four ${four} / ${n} ≈ 30 %`);
|
||||
else assert.equal(four, 0, `${label(f)}: never two at once`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T2 药剂扩散: healing he receives ×1.2 (GEE-Y stage 3: ×1.25); GEE-Y stage 3 heals one ally of his range at each deployment for 150 % ATK — the straight line ahead first, never a 禁疗 one', () => {
|
||||
const others = [{ uid: 2, chessId: YAK, row: 10, col: 6 }, { uid: 3, chessId: TEXAS, row: 11, col: 4 }, { uid: 4, chessId: LISKAM, row: 10, col: 5 }];
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const t1 = talentOf(tier, elite, mod, 1);
|
||||
const y3 = elite && mod === GY && tier === 6;
|
||||
assert.deepEqual([t1.heal_scale, t1['healinrange.heal_scale'] ?? 0], y3 ? [1.25, 1.5] : [1.2, 0], label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, others });
|
||||
assert.equal(u.s.healingTakenMul, t1.heal_scale, `${label(f)}: ×${t1.heal_scale}`);
|
||||
const yak = h.unit(2), texas = h.unit(3), liskam = h.unit(4);
|
||||
// 雷蛇 on the line ahead is 禁疗 here: 角峰, two tiles ahead on the same line, beats 德克萨斯 beside him (nearer)
|
||||
h.b.addBuff(liskam, { key: 'test:noHeal', flags: { noHeal: true } });
|
||||
for (const a of [yak, texas, liskam]) a.hp = 100;
|
||||
h.b.kill(u, null);
|
||||
const n0 = h.hooksOf('heal').length;
|
||||
assert.ok(h.b.redeploy(u, { free: true }), 'redeployed');
|
||||
const heals = h.hooksOf('heal').slice(n0).filter((c) => c.source === u && c.target !== u);
|
||||
if (y3) {
|
||||
assert.deepEqual(heals.map((c) => c.target.uid), [2], `${label(f)}: 角峰, ahead on his line`);
|
||||
approx(heals[0].amount, u.s.atk * 1.5, `${label(f)}: 150 % ATK`);
|
||||
} else assert.equal(heals.length, 0, `${label(f)}: no deploy heal`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S1 快速射击 (MANUAL, data DEFAULT): ASPD +50 / +70 for 23 / 26 s, then back', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, GY]]) {
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
assert.deepEqual([sk.duration, sk.spCost, sk.initSp, sk.bb.attack_speed], [elite ? 26 : 23, 30, elite ? 17 : 16, elite ? 70 : 50]);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0 });
|
||||
const aspd0 = u.s.aspd;
|
||||
u.skill.gainSp(999);
|
||||
h.run(2);
|
||||
assert.equal(u.skill.activations, 0, 'no enemy, no cast');
|
||||
h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast with an enemy in his range');
|
||||
approx(u.skill.timeLeft, sk.duration, 'duration', 0.02);
|
||||
assert.equal(u.s.aspd, aspd0 + sk.bb.attack_speed, 'ASPD');
|
||||
h.runUntil(() => !u.skill.active, sk.duration + 1);
|
||||
assert.equal(u.s.aspd, aspd0, 'back');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 爆发剂·γ型 (MANUAL, data DEFAULT, 30 s): 15 physical hits of 500 attack power on the ally of his range ahead (DEF applies, 5 % floor, 受击回复 SP), then max HP / DEF +40 % / +50 % on both for 30 s; the line ahead beats a nearer side ally', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, GX]]) {
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
assert.deepEqual([sk.duration, sk.spCost, sk.bb.damage, sk.bb.max_hp, sk.bb.def], [30, elite ? 34 : 37, 500, elite ? 0.5 : 0.4, elite ? 0.5 : 0.4]);
|
||||
assert.match(sk.desc, /攻击15次/);
|
||||
// 雷蛇 three tiles ahead on his line (DEF 489: 25 per hit, 5 %), 德克萨斯 diagonal-adjacent (nearer)
|
||||
const others = [{ uid: 2, chessId: LISKAM, row: 10, col: 7 }, { uid: 3, chessId: TEXAS, row: 11, col: 5 }];
|
||||
const { h, u } = field({ tier, elite, mod, skill: 1, others });
|
||||
const liskam = h.unit(2), texas = h.unit(3);
|
||||
assert.equal(liskam.skill.spType, 'hurt');
|
||||
liskam.skill.sp = 0;
|
||||
const hp0 = liskam.hp, max0 = liskam.s.maxHp, def0 = liskam.s.def;
|
||||
u.skill.gainSp(999);
|
||||
h.spawn('enemy_dummy', { pos: [9, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast');
|
||||
const shots = h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.tags?.includes('haak:stim'));
|
||||
assert.equal(shots.length, 15, '15 hits');
|
||||
assert.ok(shots.every((c) => c.target === liskam && c.type === 'phys' && c.dmg.isSkill), '雷蛇, physical skill damage');
|
||||
for (const c of shots) approx(c.amount, Math.max(500 - def0, 25), 'max(500 − DEF, 5 %)');
|
||||
// 受击回复: +1 SP per hit (雷蛇's own 战术防御 "受到攻击时…回复…1点技力" answers each hit too)
|
||||
assert.equal(h.hooksOf('spGain').filter((c) => c.unit === liskam && c.reason === 'hurt').length, 15, '受击回复: +1 SP per hit');
|
||||
assert.equal(h.hooksOf('damaged').filter((c) => c.source === u && c.target === texas).length, 0, '德克萨斯 untouched');
|
||||
const mods = { hpPct: sk.bb.max_hp, defPct: sk.bb.def };
|
||||
assert.deepEqual(u.findBuff('haak:s2:self')?.mods, mods, 'himself');
|
||||
assert.deepEqual(liskam.findBuff(`haak:s2:${u.id}`)?.mods, mods, 'the target');
|
||||
approx(liskam.findBuff(`haak:s2:${u.id}`).timeLeft, 30, 'for the skill duration', 0.02);
|
||||
approx(liskam.s.maxHp, max0 * (1 + sk.bb.max_hp), 'max HP');
|
||||
approx(liskam.hp, (hp0 - 15 * Math.max(500 - def0, 25)) * (1 + sk.bb.max_hp), 'the HP ratio kept');
|
||||
h.run(30.1);
|
||||
assert.ok(!u.findBuff('haak:s2:self') && !liskam.findBuff(`haak:s2:${u.id}`), 'gone after 30 s');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('爆发剂 edge cases: a low-DEF ally can be knocked out by the volley (he still gets his part); with no ally in his range the cast does nothing', () => {
|
||||
{
|
||||
const { h, u } = field({ tier: 5, skill: 1, others: [{ uid: 2, chessId: TEXAS, row: 10, col: 5 }] });
|
||||
const texas = h.unit(2);
|
||||
u.skill.gainSp(999);
|
||||
h.spawn('enemy_dummy', { pos: [9, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3));
|
||||
const shots = h.hooksOf('damaged').filter((c) => c.source === u && c.target === texas);
|
||||
assert.ok(!texas.alive && shots.length < 15, `德克萨斯 (DEF 260) knocked out after ${shots.length} hits`);
|
||||
assert.ok(u.findBuff('haak:s2:self'), 'his own part');
|
||||
done(h);
|
||||
}
|
||||
{
|
||||
const { h, u } = field({ tier: 6, elite: true, skill: 2, others: [{ uid: 2, chessId: YAK, row: 10, col: 3 }] }); // behind him
|
||||
u.skill.gainSp(999);
|
||||
h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast: the DEFAULT rule needs an enemy only');
|
||||
assert.equal(h.hooksOf('damaged').filter((c) => c.dmg?.tags?.includes('haak:stim')).length, 0, 'no volley');
|
||||
assert.equal(u.findBuff('haak:s3:self'), null, 'no buff');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 爆发剂·榴莲味 (MANUAL, data DEFAULT, 20 s): the same volley, then ATK +30 % / +35 % and ASPD +30 / +35 on both for 20 s', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, GY]]) {
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
assert.deepEqual([sk.duration, sk.spCost, sk.bb.damage, sk.bb.atk, sk.bb.attack_speed], [20, elite ? 41 : 44, 500, elite ? 0.35 : 0.3, elite ? 35 : 30]);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2, others: [{ uid: 2, chessId: YAK, row: 9, col: 5 }] });
|
||||
const yak = h.unit(2);
|
||||
const atk0 = yak.s.atk, aspd0 = yak.s.aspd, myAtk = u.s.atk, myAspd = u.s.aspd;
|
||||
u.skill.gainSp(999);
|
||||
h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast');
|
||||
assert.equal(h.hooksOf('damaged').filter((c) => c.source === u && c.target === yak && c.dmg?.tags?.includes('haak:stim')).length, 15, '角峰 (beside, ahead diagonal) takes the volley');
|
||||
approx(yak.s.atk, atk0 * (1 + sk.bb.atk), 'its ATK');
|
||||
assert.equal(yak.s.aspd, aspd0 + sk.bb.attack_speed, 'its ASPD');
|
||||
approx(u.s.atk, myAtk * (1 + sk.bb.atk), 'his ATK');
|
||||
assert.equal(u.s.aspd, myAspd + sk.bb.attack_speed, 'his ASPD');
|
||||
approx(u.findBuff('haak:s3:self').timeLeft, 20, '20 s', 0.02);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,290 @@
|
||||
// test/content/op_phatom.test.js — the 自选 operator kit of 傀影 (char_250_phatom, 6★ 处决者; kit
|
||||
// server/sim/content/kits/ops/op-phatom.js) and of his summon 镜中虚影 (token_10007_phatom_twin), fielded the production way
|
||||
// (a DIY slot + its `diy` pick, simdata getDiy; the twin as a placed token piece `{ kind: 'token', tokenId, ownerUid }`) in
|
||||
// every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module, EXE-X
|
||||
// 克里斯汀小姐的毛毡, EXE-Y “如影随形” or ISW-A 傀影特限证章 at stage 1 (tier 5) / 3 (tier 6). Every number is read back from
|
||||
// data/backups.json (the form of that slot status, the token variant of that owner form); the fidelity checklist of
|
||||
// kits/README.md item by item.
|
||||
// Run: node --test test/content/op_phatom.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
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 { TWIN } from '../../server/sim/content/kits/ops/op-phatom.js';
|
||||
|
||||
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
|
||||
const CHESS = load('chess');
|
||||
const BACKUPS = load('backups');
|
||||
const PHATOM = 'char_250_phatom';
|
||||
const FORMS = BACKUPS.units[PHATOM].forms;
|
||||
const TOKEN = BACKUPS.tokens[TWIN];
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const EX = 'uniequip_002_phatom', EY = 'uniequip_003_phatom', IS = 'uniequip_004_phatom';
|
||||
const S1 = 'skchr_phatom_1', S2 = 'skchr_phatom_2', S3 = 'skchr_phatom_3';
|
||||
const TEXAS = 'chess_char_1_08_a';
|
||||
const statusKey = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
|
||||
const formOf = (tier, elite) => FORMS[statusKey(tier, elite)];
|
||||
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
||||
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
||||
/** The twin's variant of an owner form (+ its module part). */
|
||||
const twinOf = (tier, elite, mod) => {
|
||||
const v = TOKEN.variants[`${PHATOM}@${statusKey(tier, elite)}`];
|
||||
const bm = elite && mod ? v.byModule?.[mod] : null;
|
||||
return { stats: bm?.stats ?? v.stats, talents: bm?.talents ?? v.talents, skillOf: (i) => (i === 0 ? v.skill : v.bySkill[i].skill) };
|
||||
};
|
||||
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_heavy: dummy('enemy_heavy', { mass: 9 }), enemy_evade: dummy('enemy_evade'),
|
||||
};
|
||||
/** Every 自选 form: [tier, elite, module]. */
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, EX, EY, IS].map((m) => [t, true, m]))];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
|
||||
/** A battle with 傀影 as uid 1 at (row, col) facing RIGHT, his twin piece (uid 2) at `twinAt` unless null, plus `others`. */
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 4, twinAt = [12, 4], others = [], seed = 5, dp = 0 } = {}) {
|
||||
const units = [{ uid: 1, diy: { slot: SLOT[tier], charId: PHATOM, skillIndex: skill, uniEquipId: mod }, elite, row, col }];
|
||||
if (twinAt) units.push({ uid: 2, kind: 'token', tokenId: TWIN, ownerUid: 1, row: twinAt[0], col: twinAt[1] });
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'statusApplied', 'death', 'deploy', 'dodge'], captureNoisy: true,
|
||||
units: [...units, ...others],
|
||||
});
|
||||
h.step();
|
||||
h.b.players[0].dp = dp;
|
||||
return { h, u: h.unit(1), t: twinAt ? h.unit(2) : null };
|
||||
}
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
/** Knock `u` out and bring it straight back on its tile (free). */
|
||||
function redeployNow(h, u) {
|
||||
h.b.kill(u, null);
|
||||
assert.ok(h.b.redeploy(u, { free: true }), `${u.name} redeployed`);
|
||||
}
|
||||
|
||||
test('傀影 in every 自选 form: his operator kit (all three skills authored), the form\'s stats + module attributes, 1-1, blocks 1, melee ground-only, 18 s redeploy, 维多利亚, no 特质; his placed 镜中虚影 deploys after him with the variant\'s stats, 禁疗 and its copy of his skill', () => {
|
||||
assert.equal(OPERATOR_KITS[PHATOM], KITS[PHATOM]);
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
for (const skill of [0, 1, 2]) {
|
||||
const { h, u, t } = field({ tier, elite, mod, skill });
|
||||
const form = formOf(tier, elite), m = elite ? modOf(tier, mod) : null, tw = twinOf(tier, elite, mod);
|
||||
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, u.skill.kind, !!u.kit.generic, u.kit.skillSource], [PHATOM, SLOT[tier], form.skills[skill].skillId, 'passive', false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.aspd, u.base.respawnTime, u.base.cost],
|
||||
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), 100 + (m?.attr.aspd ?? 0), 18, 10], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly], [1, 'melee', 'phys', false], `${label(f)}: 处决者`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 1-1`);
|
||||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['victoriaShip'], []], `${label(f)}: bonds / 特质`);
|
||||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target him`);
|
||||
// the twin
|
||||
assert.deepEqual([t.defId, t.ownerUnit === u, t.alive, t.deployed, t.deploySeq > u.deploySeq, !!t.kit.generic], [TWIN, true, true, true, true, false], `${label(f)}: twin deployed after him`);
|
||||
assert.deepEqual([t.base.maxHp, t.base.atk, t.base.def, t.base.respawnTime, t.base.cost], [tw.stats.maxHp, tw.stats.atk, tw.stats.def, 45, 5], `${label(f)}: twin stats`);
|
||||
assert.deepEqual([t.def.skill.id, t.skill.kind], [tw.skillOf(skill).skillId, 'passive'], `${label(f)}: twin skill copy`);
|
||||
assert.deepEqual([t.s.blockCnt, t.profile.attack, t.profile.canHitFly, t.liveRangeGrid], [1, 'melee', false, [[0, 0], [0, 1]]], `${label(f)}: twin executor`);
|
||||
assert.ok(t.s.flags.noHeal, `${label(f)}: twin 持有禁疗`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
// the numbers (zh_CN): 傀影 E2 Lv1 1283 / 435 / 270, E2 Lv60 1523 / 517 / 304; the twin 1126 / 429 / 226 → 1318 / 508 / 254,
|
||||
// EXE-Y stage 3 "属性进一步增强" 1418 / 568 / 294; modules
|
||||
assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.maxHp, FORMS['2/60/7/1'].stats.atk], [1283, 435, 1523, 517]);
|
||||
assert.deepEqual([twinOf(5, false).stats.maxHp, twinOf(5, true).stats.atk, twinOf(6, true, EY).stats.maxHp, twinOf(6, true, EY).stats.atk, twinOf(5, true, EY).stats.atk], [1126, 508, 1418, 568, 508]);
|
||||
assert.deepEqual([modOf(5, EX).attr, modOf(6, EX).attr, modOf(5, EY).attr, modOf(6, EY).attr, modOf(5, IS).attr, modOf(6, IS).attr],
|
||||
[{ maxHp: 100, atk: 50 }, { maxHp: 140, atk: 73 }, { atk: 40, def: 24 }, { atk: 75, def: 30 }, { atk: 37, aspd: 5 }, { atk: 71, aspd: 7 }]);
|
||||
});
|
||||
|
||||
test('a 自选 pick: 傀影 is offered at tiers 5 and 6; a roster with EXE-X / EXE-Y passes validateDiyPicks, his 集成战略 module ISW-A is refused', () => {
|
||||
const data = { chess: CHESS, backups: BACKUPS };
|
||||
assert.ok(KITTED_CHARS.includes(PHATOM));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(PHATOM), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: PHATOM, skillIndex: 2, uniEquipId: EY } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[6]]: { charId: PHATOM, skillIndex: 2, uniEquipId: EY } } });
|
||||
const bad = validateDiyPicks({ [SLOT[5]]: { charId: PHATOM, skillIndex: 0, uniEquipId: IS } }, { data, kitted: KITTED_CHARS });
|
||||
assert.equal(bad.error, 'BAD_TARGET');
|
||||
assert.match(bad.detail, /集成战略/);
|
||||
});
|
||||
|
||||
test('S1 暗夜魅影 (被动): at each deployment 30 % / 40 % physical dodge and a 屏障 of 35 % / 50 % max HP that absorbs physical damage only, both 10 s; the twin\'s copy on itself', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, EY]]) {
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
assert.deepEqual([sk.skillType, sk.spType, sk.bb.prob, sk.bb.hp_ratio, sk.bb.duration], ['PASSIVE', 'ON_DEPLOY', elite ? 0.4 : 0.3, elite ? 0.5 : 0.35, 10]);
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 0 });
|
||||
for (const x of [u, t]) {
|
||||
const b = x === u ? sk.bb : twinOf(tier, elite, mod).skillOf(0).bb;
|
||||
approx(x.s.dodgePhys, b.prob, `${x.name}: physical dodge`);
|
||||
assert.equal(x.s.dodgeArts, 0, `${x.name}: no arts dodge`);
|
||||
const sh = x.buffs.find((y) => y.shield > 0);
|
||||
approx(sh.shield, x.s.maxHp * b.hp_ratio, `${x.name}: ${b.hp_ratio * 100} % max HP`);
|
||||
assert.equal(sh.shieldType, 'phys', `${x.name}: physical only`);
|
||||
}
|
||||
// arts damage passes the barrier, physical damage is absorbed
|
||||
const hp0 = u.hp;
|
||||
h.b.dealDamage(null, u, { amount: 100, type: 'arts', canDodge: false });
|
||||
approx(u.hp, hp0 - 100, 'arts passes');
|
||||
h.b.dealDamage(null, u, { amount: 100, type: 'phys', canDodge: false });
|
||||
approx(u.hp, hp0 - 100, 'physical absorbed');
|
||||
h.run(10.2);
|
||||
assert.ok(u.s.dodgePhys === 0 && !u.buffs.some((y) => y.shield > 0), 'gone after 10 s');
|
||||
redeployNow(h, u);
|
||||
approx(u.s.dodgePhys, sk.bb.prob, 'again at a redeployment');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 血色乐章 (被动): at each deployment 8 / 9 stacks of ATK +13 % / +16 %; each damage instance he deals uses one (a dodged attack none); the twin\'s copy has its own', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, EX]]) {
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
assert.deepEqual([sk.bb.times, sk.bb.atk], [elite ? 9 : 8, elite ? 0.16 : 0.13]);
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 1 });
|
||||
assert.equal(u.findBuff('phatom:skill').stacks, sk.bb.times);
|
||||
assert.equal(t.findBuff('phatom:twin:skill').stacks, sk.bb.times, 'the twin\'s own stacks');
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.times * sk.bb.atk + (u.findBuff('trait:phatom:lonely') ? 0.1 : 0)), 'ATK');
|
||||
// an enemy that dodges everything: no stack used
|
||||
const ev = h.spawn('enemy_evade', { pos: [10, 5] });
|
||||
h.b.addBuff(ev, { key: 'test:dodge', mods: { dodgePhys: 1 } });
|
||||
h.run(3);
|
||||
assert.ok(h.hooksOf('dodge').some((c) => c.source === u), 'dodged attacks');
|
||||
assert.equal(u.findBuff('phatom:skill').stacks, sk.bb.times, 'none used');
|
||||
h.b.removeBuff(ev, 'test:dodge');
|
||||
const fullAtk = u.s.atk;
|
||||
const n0 = h.hooksOf('damaged').filter((c) => c.source === u).length;
|
||||
h.run(0.95);
|
||||
const hits = h.hooksOf('damaged').filter((c) => c.source === u).slice(n0);
|
||||
assert.ok(hits.length >= 1, 'hits');
|
||||
assert.equal(u.findBuff('phatom:skill').stacks, sk.bb.times - hits.length, 'one per damage instance');
|
||||
approx(hits[0].amount, fullAtk, 'the first hit at full stacks (a 0-DEF target)');
|
||||
approx(u.s.atk, fullAtk - u.base.atk * sk.bb.atk * hits.length, 'ATK −atk per stack used');
|
||||
h.run(30);
|
||||
assert.equal(u.findBuff('phatom:skill'), null, 'all used');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 夜幕突袭 (被动): at each deployment 210 % / 240 % ATK physical to every ground enemy of the x-4, a radial 小力 push and ONE status for all — 停顿 / 晕眩 / 束缚 2.5 / 3 s (each drawn over the casts); flyers and enemies outside untouched; the twin\'s copy at its deployment', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, EY]]) {
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
assert.deepEqual([sk.rangeId, sk.bb.atk_scale, sk.bb.force, sk.bb.sluggish, sk.bb.root, sk.bb.stun], ['x-4', elite ? 2.4 : 2.1, 0, elite ? 3 : 2.5, elite ? 3 : 2.5, elite ? 3 : 2.5]);
|
||||
const drawn = new Set();
|
||||
for (const seed of [1, 2, 3, 4, 5, 6, 7, 8]) {
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 2, seed, twinAt: [11, 6] });
|
||||
const ahead = h.spawn('enemy_dummy', { pos: [10, 5] }), diag = h.spawn('enemy_dummy', { pos: [9, 3] });
|
||||
const heavy = h.spawn('enemy_heavy', { pos: [11, 4] });
|
||||
const fly = h.spawn('enemy_fly', { pos: [9, 4] }), far = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
h.step(); // the enemy tile index
|
||||
const n0 = h.hooksOf('damaged').length, s0 = h.hooksOf('statusApplied').length;
|
||||
redeployNow(h, u);
|
||||
const hits = h.hooksOf('damaged').slice(n0).filter((c) => c.source === u && c.dmg?.tags?.includes('phatom:raid'));
|
||||
assert.deepEqual(hits.map((c) => c.target).sort((a, b) => a.id - b.id), [ahead, diag, heavy].sort((a, b) => a.id - b.id), `seed ${seed}: the ground enemies of his x-4`);
|
||||
for (const c of hits) {
|
||||
approx(c.amount, u.s.atk * sk.bb.atk_scale, `seed ${seed}: ${sk.bb.atk_scale * 100} % ATK`);
|
||||
assert.deepEqual([c.type, c.dmg.isSkill], ['phys', true]);
|
||||
}
|
||||
assert.ok(!hits.some((c) => c.target === fly || c.target === far), 'flyer / outside untouched');
|
||||
assert.ok(ahead.x > 5.5, `seed ${seed}: pushed away (radial, 小力)`);
|
||||
approx(heavy.y, 11, `seed ${seed}: 重量 9 — 小力 moves nothing`);
|
||||
const st = h.hooksOf('statusApplied').slice(s0).filter((c) => c.source === u);
|
||||
assert.equal(new Set(st.map((c) => c.status)).size, 1, `seed ${seed}: one status for the cast`);
|
||||
assert.equal(st.length, 3, `seed ${seed}: on each of the three`);
|
||||
for (const c of st) approx(c.duration, sk.bb[c.status === 'bind' ? 'root' : c.status], `seed ${seed}: ${c.status}`);
|
||||
drawn.add(st[0].status);
|
||||
// the twin's copy at its own deployment
|
||||
const tn0 = h.hooksOf('damaged').length;
|
||||
redeployNow(h, t);
|
||||
const th = h.hooksOf('damaged').slice(tn0).filter((c) => c.source === t && c.dmg?.tags?.includes('phatom:raid'));
|
||||
assert.ok(th.length >= 1 && th.every((c) => Math.abs(c.amount - t.s.atk * twinOf(tier, elite, mod).skillOf(2).bb.atk_scale) < 1e-6), `seed ${seed}: the twin's burst`);
|
||||
done(h);
|
||||
}
|
||||
assert.deepEqual([...drawn].sort(), ['bind', 'sluggish', 'stun'], 'all three statuses over the casts');
|
||||
}
|
||||
});
|
||||
|
||||
test('T1 镜中虚影 / T2 虚影精通: the twin (禁疗: no heal reaches it) is withdrawn when he leaves the field and comes back on its tile 35 s (45 − 10; EXE-X stage 3: 45 − 16) after its LAST deployment, paying 5 DP, only while he stands', () => {
|
||||
for (const f of [[5, false, null], [5, true, EX], [6, true, EX]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const cut = twinOf(tier, elite, mod).talents.find((x) => x.bb.respawn_time != null).bb.respawn_time;
|
||||
assert.equal(cut, tier === 6 && mod === EX ? -16 : -10, label(f));
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 1, dp: 50 });
|
||||
const wait = 45 + cut;
|
||||
// 禁疗: an ally heal does not reach it
|
||||
t.hp = 100;
|
||||
h.b.heal(u, t, 500);
|
||||
assert.equal(t.hp, 100, `${label(f)}: 禁疗`);
|
||||
// knocked out while he stands: back `wait` s after its deployment at 0 s, 5 DP paid
|
||||
h.run(5);
|
||||
h.b.kill(t, null);
|
||||
assert.ok(!t.alive && !t.removed, `${label(f)}: the piece stays`);
|
||||
h.run(wait - 5 - 1);
|
||||
assert.ok(!t.alive, `${label(f)}: not before ${wait} s`);
|
||||
assert.ok(h.runUntil(() => t.alive, 1.5), `${label(f)}: back at ${wait} s`);
|
||||
assert.deepEqual([t.tileR, t.tileC, h.b.players[0].dp], [12, 4, 45], `${label(f)}: its tile, 5 DP`);
|
||||
const at = h.b.time;
|
||||
// he is knocked out: the twin is withdrawn with him (强制撤退 — KillTokens) and waits for him
|
||||
h.b.kill(u, null);
|
||||
u.respawnAt = Infinity; // he stays down until the test brings him back
|
||||
assert.ok(!t.alive, `${label(f)}: withdrawn with him`);
|
||||
assert.equal(h.hooksOf('death').filter((c) => c.unit === t).slice(-1)[0].reason, 'retreat', `${label(f)}: a withdrawal, not a knock-out`);
|
||||
h.run(wait + 1);
|
||||
assert.ok(!t.alive && !u.alive, `${label(f)}: no return while he is down`);
|
||||
assert.ok(h.b.redeploy(u, { free: true }));
|
||||
assert.ok(h.runUntil(() => t.alive, 0.5), `${label(f)}: back with him once its own timer (from ${at.toFixed(1)} s) has run`);
|
||||
// no DP: it waits
|
||||
h.b.kill(t, null);
|
||||
h.b.players[0].dp = 0;
|
||||
h.run(wait + 1);
|
||||
assert.ok(!t.alive, `${label(f)}: no DP, no return`);
|
||||
h.b.players[0].dp = 5;
|
||||
assert.ok(h.runUntil(() => t.alive, 0.5), `${label(f)}: back with the DP`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('EXE-X / ISW-A: EXE-X\'s refund has no effect (no retreat in battle) — only its attributes and T2 at stage 3; ISW-A acts by its attributes only (集成战略-only trait and talent)', () => {
|
||||
{
|
||||
const { h, u } = field({ tier: 6, elite: true, mod: EX, skill: 0, dp: 7 });
|
||||
assert.deepEqual(u.def.traitBb, { withdraw_cost_recover_ratio: 0.8 });
|
||||
h.b.retreat(u);
|
||||
h.step();
|
||||
assert.equal(h.b.players[0].dp, 7, 'no refund');
|
||||
done(h);
|
||||
}
|
||||
{
|
||||
const { h, u, t } = field({ tier: 6, elite: true, mod: IS, skill: 1, dp: 50 });
|
||||
assert.match(modOf(6, IS).traitOverride.moduleDesc, /^在集成战略中/);
|
||||
assert.ok(modOf(6, IS).talentChanges.filter((x) => x.desc).every((x) => /在集成战略中/.test(x.desc)));
|
||||
assert.equal(u.findBuff('module:phatom:both'), null, 'no ATK while both stand');
|
||||
h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
h.run(5);
|
||||
h.b.kill(t, null);
|
||||
assert.ok(!h.runUntil(() => t.alive, 29), 'its attacks shorten no redeploy time');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('EXE-Y “如影随形”: ATK +10 % while no allied operator stands on the 4 tiles beside him (the twin does not count); stage 3: ATK +10 % on him and the twin while both are on the field', () => {
|
||||
for (const f of [[5, true, EY], [6, true, EY], [6, true, null]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const ey = mod === EY, y3 = ey && tier === 6;
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 0, twinAt: [10, 5], others: [{ uid: 3, chessId: TEXAS, row: 12, col: 7 }] });
|
||||
const texas = h.unit(3);
|
||||
assert.equal(!!u.findBuff('trait:phatom:lonely'), ey, `${label(f)}: the twin beside him does not count`);
|
||||
if (ey) assert.deepEqual(u.findBuff('trait:phatom:lonely').mods, { atkPct: 0.1 });
|
||||
assert.equal(!!u.findBuff('module:phatom:both'), y3, `${label(f)}: both on the field`);
|
||||
assert.equal(!!t.findBuff('module:phatom:both'), y3, `${label(f)}: the twin too`);
|
||||
if (y3) assert.match(FORMS['2/60/7/3'].modules.find((m) => m.uniEquipId === EY).talentChanges[0].desc, /本体和虚影同时在场时,攻击力各\+10%/);
|
||||
// an operator beside him
|
||||
assert.ok(h.b.relocate(texas, 11, 4), 'moved beside him');
|
||||
h.step();
|
||||
assert.equal(u.findBuff('trait:phatom:lonely'), null, `${label(f)}: an operator beside`);
|
||||
// the twin down: the shared ATK goes
|
||||
h.b.kill(t, null);
|
||||
h.step();
|
||||
assert.equal(u.findBuff('module:phatom:both'), null, `${label(f)}: twin down`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
+368
-135
@@ -4211,185 +4211,171 @@
|
||||
"snaps": "ab6ec9e5 0cd29374 535b8f29 3a00102a 4e2f0e45 89bec34e 3229b975 60a9b6e7 f72a6124 fdcf62df d8ebef5d 58a1a24d 40484f5d 030d318d 2d3105b6 b2e4e0b0 fa368d3b a4d01055 23684e50 b6c74b76 de80150b 2b817302 aae3bd11"
|
||||
},
|
||||
"diy-099": {
|
||||
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 6_diy1_b=gdglow/S1/002_gdglow 6_diy2_b=gdglow/S2/002_gdglow 6_diy1_b=gdglow/S3/002_gdglow 6_diy2_b=gdglow/S1/003_gdglow 6_diy1_b=gdglow/S2/003_gdglow 6_diy2_b=gdglow/S3/003_gdglow 5_diy1_a=csnipe 5_diy2_b=csnipe",
|
||||
"fast": true,
|
||||
"about": "6_diy1_b=gdglow/S1/002_gdglow 6_diy2_b=gdglow/S2/002_gdglow 6_diy1_b=gdglow/S3/002_gdglow 6_diy2_b=gdglow/S1/003_gdglow 6_diy1_b=gdglow/S2/003_gdglow 6_diy2_b=gdglow/S3/003_gdglow 5_diy1_a=haak/S1 5_diy2_a=haak/S2 5_diy1_a=haak/S3 5_diy2_b=haak/S1 5_diy1_b=haak/S2 5_diy2_b=haak/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m04",
|
||||
"modeId": "mode_single_normal",
|
||||
"round": 13,
|
||||
"seed": 213318073,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 228.97,
|
||||
"ticks": 6869,
|
||||
"reason": "cleared",
|
||||
"killed": 44,
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 26,
|
||||
"total": 129,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 1205,
|
||||
"rngDraws": 1665,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":44,"total":129,"perfect":false,"leaks":85,"leaksUncounted":0,"coins":0,"dmg":1076426,"bossDmg":0,"heal":0,"deaths":11,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":26,"total":129,"perfect":false,"leaks":103,"leaksUncounted":0,"coins":0,"dmg":954503,"bossDmg":0,"heal":23555,"deaths":12,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,141,0,442,6384,0,32,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,128,0,2841,5816,0,77,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,150,2,6181,7692,0,24,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"none",0,0,149,1,3684,10002,0,31,1],
|
||||
["p1",5,"op","chess_char_6_diy1_b",0,"uniequip_002_gdglow",1485,1,3,14,219946,0,0,87,4],
|
||||
["p1",6,"op","chess_char_6_diy2_b",1,"uniequip_002_gdglow",1485,1,4,13,204111,0,0,41,1],
|
||||
["p1",7,"op","chess_char_6_diy1_b",2,"uniequip_002_gdglow",1485,1,11,3,168014,0,0,88,3],
|
||||
["p1",8,"op","chess_char_6_diy2_b",0,"uniequip_003_gdglow",1360,1,13,0,29119,0,0,48,3],
|
||||
["p1",9,"op","chess_char_6_diy1_b",1,"uniequip_003_gdglow",1360,1,5,5,225706,0,0,45,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_003_gdglow",1360,1,10,6,159660,0,0,96,3],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,1271,1,12,0,24093,0,0,193,3],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",1510,1,14,0,32630,0,0,190,3],
|
||||
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_002_gdglow",1485,1,3,5,165581,0,0,80,4],
|
||||
["p1",2,"op","chess_char_6_diy2_b",1,"uniequip_002_gdglow",1485,1,4,4,223522,0,0,38,1],
|
||||
["p1",3,"op","chess_char_6_diy1_b",2,"uniequip_002_gdglow",1485,1,151,4,140987,1485,0,45,3],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_003_gdglow",1360,1,153,0,36388,2720,0,17,1],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_003_gdglow",1360,1,5,7,221819,0,0,43,1],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_003_gdglow",1360,1,152,0,76219,2720,0,35,1],
|
||||
["p1",7,"op","chess_char_5_diy1_a",0,null,1448.03,1,131,2,10127,4181,2375,130,3],
|
||||
["p1",8,"op","chess_char_5_diy2_a",1,null,1138.9,1,14,0,25715,4116,3570,153,3],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,1301.6,1,9,0,16759,3726,3400,169,3],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,97,3,18466,8849,5055,91,2],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"none",1707.3,1,96,1,15955,11182,8946,111,3],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,135,0,2965,5900,209,14,0],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_10124_uashld_2":[18,1,17,7878,352011],"enemy_1043_zomsbr":[66,39,27,1291,322100],"enemy_1061_zomshd":[45,4,41,20725,439034]},
|
||||
"hooks": [1,150,6869,1151,1151,3763,3763,53819,55,140,25,23,0,35062,581,35,129,85,1,55,0,0,0],
|
||||
"events": {"atk":1151,"deploy":19,"die":55,"dmg":3763,"fx":1605,"leak":85,"skill":48,"spawn":143,"status":370},
|
||||
"fx": {"drone":1205,"droneLock":269,"drones":15,"explode":116},
|
||||
"snaps": "9996902b 61b66e98 732c6d67 48ae38a8 b8731a98 00463d97 bbba43ed 58597bf1 871b7fd8 4e975154 4ff72676 f097682e 29ef1afa 5fdb0354 b7df1987 d1307d71 a8af977d 090fb008 25361ebd ff7fb0e8 ca2e7947 559c1e70 cec6ba4d"
|
||||
"enemies": {"enemy_10124_uashld_2":[18,0,18,3695,305556],"enemy_1043_zomsbr":[66,21,45,559,253330],"enemy_1061_zomshd":[45,5,39,11888,439378]},
|
||||
"hooks": [1,153,6900,959,959,3506,4432,54021,38,140,25,22,0,29534,647,8,129,102,1,38,0,0,0],
|
||||
"events": {"atk":959,"deploy":22,"die":38,"dmg":3506,"fx":1477,"heal":195,"leak":102,"skill":47,"spawn":143,"status":803},
|
||||
"fx": {"crit":158,"drone":997,"droneLock":207,"drones":11,"explode":96,"volley":8},
|
||||
"snaps": "98413af2 2fa81246 d591e5bb d9cae5f7 c3e39e54 c1f740f5 59125806 78753917 da019f83 4a6274cd a16cbe1d ff744b46 40488fd0 bb0d16d1 2957f170 7877cf59 df091f23 945d5132 0e51d256 970e2332 8047d98c 05c5b16d 6648379d"
|
||||
},
|
||||
"diy-100": {
|
||||
"about": "5_diy1_a=acfend 5_diy2_b=acfend 5_diy1_a=acpion 5_diy2_b=acpion 5_diy1_a=acguad 5_diy2_b=acguad 5_diy1_a=ccast 5_diy2_b=ccast 5_diy1_a=cmedic 5_diy2_b=cmedic 5_diy1_a=csuppo 5_diy2_b=csuppo",
|
||||
"about": "5_diy1_b=haak/S1/002_haak 5_diy2_b=haak/S2/002_haak 5_diy1_b=haak/S3/002_haak 5_diy2_b=haak/S1/003_haak 5_diy1_b=haak/S2/003_haak 5_diy2_b=haak/S3/003_haak 6_diy1_a=haak/S1 6_diy2_a=haak/S2 6_diy1_a=haak/S3 6_diy2_b=haak/S1 6_diy1_b=haak/S2 6_diy2_b=haak/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m01",
|
||||
"modeId": "mode_multi_abyss",
|
||||
"round": 12,
|
||||
"round": 13,
|
||||
"seed": 1853641125,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 203.93,
|
||||
"ticks": 6118,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 219.67,
|
||||
"ticks": 6590,
|
||||
"reason": "cleared",
|
||||
"killed": 60,
|
||||
"total": 153,
|
||||
"killed": 0,
|
||||
"total": 171,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 131,
|
||||
"rngDraws": 85,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":60,"total":153,"perfect":false,"leaks":93,"leaksUncounted":0,"coins":0,"dmg":727055,"bossDmg":0,"heal":52388,"deaths":10,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":0,"total":171,"perfect":false,"leaks":171,"leaksUncounted":0,"coins":0,"dmg":57011,"bossDmg":0,"heal":3727,"deaths":28,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,114,0,9773,5208,0,45,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,129,1,38916,6742,0,80,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,162,8,21954,8669,0,25,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",1904,1,169,29,281570,24533,0,100,6],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,2638,1,15,7,102220,25677,0,137,3],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,136,4,66511,6822,0,38,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,4,4,40791,0,0,108,3],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,5,6,63600,0,0,148,3],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,11,0,0,0,25465,59,3],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,13,0,0,0,26923,57,3],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,6,1,39821,0,0,112,3],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,14,0,61898,0,0,137,3],
|
||||
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_haak",0,0,197,0,12690,6828,732,17,1],
|
||||
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_haak",0,0,202,0,6750,8072,1123,11,1],
|
||||
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_haak",0,0,180,0,7618,6360,264,10,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_haak",1726.27,1,203,0,4545,6013,152,6,0],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_haak",0,0,170,0,1052,5691,0,2,0],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_haak",0,0,189,0,2284,5691,0,3,0],
|
||||
["p1",7,"op","chess_char_6_diy1_a",0,null,0,0,113,0,1882,3547,293,4,1],
|
||||
["p1",8,"op","chess_char_6_diy2_a",1,null,0,0,9,0,4940,2549,303,9,1],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,107,0,4900,3433,179,8,0],
|
||||
["p1",10,"op","chess_char_6_diy2_b",0,"none",0,0,12,0,6273,2238,341,10,1],
|
||||
["p1",11,"op","chess_char_6_diy1_b",1,"none",0,0,97,0,1438,4135,341,2,0],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"none",0,0,108,0,2640,3794,0,3,1],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {
|
||||
"enemy_1195_sfyin": [45,11,34,18165,212500],
|
||||
"enemy_1196_msfyin": [22,20,2,0,41],
|
||||
"enemy_1197_sfshu": [39,11,28,33792,288323],
|
||||
"enemy_1198_msfshu": [33,16,17,0,58],
|
||||
"enemy_1199_sfjin": [12,2,10,25694,226118],
|
||||
"enemy_1200_msfjin": [2,0,2,0,15]
|
||||
},
|
||||
"hooks": [1,174,6118,1612,1612,2011,1965,116,70,163,32,32,0,34916,243,55,153,93,1,79,0,0,46],
|
||||
"events": {"atk":1612,"deploy":18,"die":70,"dmg":1965,"fx":206,"heal":116,"leak":93,"skill":64,"spawn":168,"status":496},
|
||||
"fx": {"aoe":36,"dodge":46,"dp":44,"slash":56,"summon":24},
|
||||
"snaps": "e63c08bc 993deae4 dd02af70 2d6546d3 04bbd4f3 1830ce73 2a424989 7fa082c5 ce8dd734 cc64a140 25615b6b 12274f2a dd8c1029 a4f58d5c 46f041f6 104cd848 39c8bf6f abe3608e 30a3b7d5 ab980aa3 d803a54b"
|
||||
"enemies": {"enemy_1009_lurker":[66,0,66,2255,32183],"enemy_1019_jshoot_2":[57,0,57,35466,12393],"enemy_1404_msnip":[48,0,48,11016,12436]},
|
||||
"hooks": [1,203,6590,196,196,215,459,16,28,199,7,7,0,5134,53,9,171,171,1,28,0,0,0],
|
||||
"events": {"atk":196,"deploy":29,"die":28,"dmg":215,"fx":32,"heal":15,"leak":171,"skill":14,"spawn":186,"status":76},
|
||||
"fx": {"crit":28,"volley":4},
|
||||
"snaps": "4fd08fc1 4003dc33 8eef9078 71317e70 f00f3e81 3dbdaf3a 32b4db08 46f4ccd8 f11e1792 d1998c8f 91c73d4a 021d0f3f 9f00ecb4 da6fd4cc f6331b84 a0bf9dfd b544011e a7294c1e e9b0059f 53fe9fe6 555cde26 63e02c6a"
|
||||
},
|
||||
"diy-101": {
|
||||
"about": "5_diy1_a=sharp2 5_diy2_b=sharp2 5_diy1_a=acspec 5_diy2_b=acspec 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc 5_diy1_a=acsupo 5_diy2_b=acsupo",
|
||||
"about": "5_diy1_a=phatom/S1 5_diy2_a=phatom/S2 5_diy1_a=phatom/S3 5_diy2_b=phatom/S1 5_diy1_b=phatom/S2 5_diy2_b=phatom/S3 6_diy1_b=haak/S1/002_haak 6_diy2_b=haak/S2/002_haak 6_diy1_b=haak/S3/002_haak 6_diy2_b=haak/S1/003_haak 6_diy1_b=haak/S2/003_haak 6_diy2_b=haak/S3/003_haak",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m02",
|
||||
"modeId": "mode_multi_funny",
|
||||
"round": 12,
|
||||
"round": 13,
|
||||
"seed": 739544218,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 204.8,
|
||||
"ticks": 6144,
|
||||
"reason": "cleared",
|
||||
"killed": 87,
|
||||
"total": 99,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 20,
|
||||
"total": 114,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 1064,
|
||||
"rngDraws": 809,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":87,"total":99,"perfect":false,"leaks":12,"leaksUncounted":0,"coins":0,"dmg":704152,"bossDmg":0,"heal":47857,"deaths":1,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":20,"total":114,"perfect":false,"leaks":94,"leaksUncounted":0,"coins":0,"dmg":279846,"bossDmg":0,"heal":26108,"deaths":27,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,841,1,3,20,91078,1024,0,124,3],
|
||||
["p1",2,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",2374,1,1,6,68740,0,0,55,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,311,1,4,0,1569,843,0,26,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,75,1,7264,1902,0,29,0],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,1272,1,5,16,84001,0,0,202,2],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,8,18,178620,0,0,188,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1372,1,6,0,18491,0,0,17,2],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1749,1,9,20,187306,0,0,77,2],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,1224,1,10,0,0,8016,24220,63,2],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1414,1,11,0,0,14535,23637,67,3],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,12,5,36418,24853,0,129,4],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,2,1,30664,0,0,54,2]
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,142,2,10314,5132,0,140,0],
|
||||
["p1",2,"op","chess_char_5_diy2_a",1,null,603,1,144,1,11973,3246,0,83,0],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,1283,1,147,1,13020,2566,0,118,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"none",299,1,138,0,3154,4270,0,122,0],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"none",0,0,141,2,21295,6092,0,154,0],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",1387,1,146,3,24531,4705,0,179,0],
|
||||
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_002_haak",2051.52,1,145,1,5895,5684,1325,67,2],
|
||||
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_002_haak",0,0,79,1,23647,7041,1784,52,2],
|
||||
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_002_haak",1923.3,1,6,5,90541,4894,4680,176,4],
|
||||
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_003_haak",0,0,107,0,23753,6658,967,60,1],
|
||||
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_003_haak",0,0,68,1,18593,13492,9698,101,3],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_003_haak",0,0,2,3,33130,9550,7653,130,3]
|
||||
],
|
||||
"enemies": {"enemy_1010_demon":[42,41,1,41095,296286],"enemy_1320_wdrrl_2":[12,2,10,0,174829],"enemy_1422_lrsldr_2":[45,44,1,10077,233038]},
|
||||
"hooks": [1,112,6144,1243,1243,1798,1861,200,88,100,24,22,0,36141,6922,29,99,12,1,88,0,0,0],
|
||||
"events": {"atk":1498,"deploy":13,"die":88,"dmg":1860,"fx":40,"heal":155,"leak":12,"skill":51,"spawn":111,"status":674},
|
||||
"fx": {"aoe":13,"displace":1,"dodge":26},
|
||||
"snaps": "9b12b5a2 cc58812c b15c7a02 65275a10 32f7558d 695f18fe 4167d093 a33b1752 1bd38a64 97735c90 9cfcd534 393bf4da dc5c2c12 2b2038b4 dbf5688c 52665c6d 997dacea 92e59d36 d02c5b07 0d9e13e2 efc5468b"
|
||||
"enemies": {"enemy_1381_winman":[27,6,21,198,66587],"enemy_1381_winman_2":[48,14,34,8018,171095],"enemy_1387_winshd":[39,0,36,32469,42164]},
|
||||
"hooks": [1,147,6900,1680,1680,1757,2511,205,47,138,15,31,0,11994,453,37,114,91,1,47,0,0,2],
|
||||
"events": {"atk":1680,"deploy":33,"die":47,"dmg":1743,"fx":266,"heal":197,"leak":91,"skill":71,"spawn":126,"status":676},
|
||||
"fx": {"aoe":7,"buff":7,"crit":224,"displace":7,"dodge":2,"heal":3,"shield":7,"volley":9},
|
||||
"snaps": "6b767508 5d03a97a 2d0a8872 83b6bdf6 30c7f795 2280398a e3f7061e 37a4f460 5169880b 1d79c97a 7474fffa 0089c61d 4d424b28 963ea833 4c39a866 c407fce3 15948146 643a0451 02d7d711 c3432adf ca20a7f5 365b255e 2505dc85"
|
||||
},
|
||||
"diy-102": {
|
||||
"about": "6_diy1_a=acfend 6_diy2_b=acfend 6_diy1_a=acpion 6_diy2_b=acpion 6_diy1_a=acguad 6_diy2_b=acguad 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc",
|
||||
"about": "5_diy1_b=phatom/S1/002_phatom 5_diy2_b=phatom/S2/002_phatom 5_diy1_b=phatom/S3/002_phatom 5_diy2_b=phatom/S1/003_phatom 5_diy1_b=phatom/S2/003_phatom 5_diy2_b=phatom/S3/003_phatom 5_diy1_b=phatom/S1/004_phatom 5_diy2_b=phatom/S2/004_phatom 5_diy1_b=phatom/S3/004_phatom 6_diy1_a=phatom/S1 6_diy2_a=phatom/S2 6_diy1_a=phatom/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m03",
|
||||
"modeId": "mode_single_hard",
|
||||
"round": 13,
|
||||
"round": 12,
|
||||
"seed": 2379881591,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 201.57,
|
||||
"ticks": 6047,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 208,
|
||||
"ticks": 6240,
|
||||
"reason": "cleared",
|
||||
"killed": 261,
|
||||
"total": 297,
|
||||
"killed": 29,
|
||||
"total": 117,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 207,
|
||||
"rngDraws": 52,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":261,"total":297,"perfect":false,"leaks":36,"leaksUncounted":0,"coins":0,"dmg":479266,"bossDmg":0,"heal":53831,"deaths":10,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":29,"total":117,"perfect":false,"leaks":88,"leaksUncounted":0,"coins":0,"dmg":143138,"bossDmg":0,"heal":0,"deaths":32,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",2,null,2604,1,253,12,2143,2604,0,41,2],
|
||||
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",2890.55,1,7,4,24083,33048,0,133,3],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,1207.76,1,268,37,35114,4792,0,39,3],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,129,84,102697,5299,0,57,6],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,2343.99,1,288,5,10524,7451,0,22,1],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",2020.71,1,132,20,93856,20393,0,81,3],
|
||||
["p1",7,"op","chess_char_6_diy1_a",2,null,1272,1,8,5,3227,0,0,28,1],
|
||||
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,12,17,31071,0,0,65,2],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,1372,1,14,1,17185,0,0,23,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",1819,1,4,76,159366,0,0,73,2],
|
||||
["p1",11,"op","chess_char_6_diy1_a",2,null,1224,1,6,0,0,0,40735,67,2],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,152,0,0,4115,13096,22,2],
|
||||
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_phatom",0,0,146,3,15272,8115,0,94,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_phatom",0,0,151,6,24336,8115,0,74,0],
|
||||
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_phatom",0,0,152,0,18052,9738,0,54,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_phatom",0,0,139,3,13058,4569,0,60,0],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_phatom",1523,1,153,5,19717,3046,0,44,0],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_phatom",0,0,45,3,13679,3046,0,33,0],
|
||||
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_004_phatom",0,0,39,4,16627,3046,0,54,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_004_phatom",0,0,7,1,4292,1523,0,24,0],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_004_phatom",0,0,53,2,8950,3046,0,50,0],
|
||||
["p1",10,"op","chess_char_6_diy1_a",0,null,0,0,47,2,8965,2566,0,38,0],
|
||||
["p1",11,"op","chess_char_6_diy2_a",1,null,0,0,8,0,125,1283,0,3,0],
|
||||
["p1",12,"op","chess_char_6_diy1_a",2,null,0,0,9,0,65,1283,0,3,0],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {
|
||||
"enemy_1195_sfyin": [39,31,8,2507,109166],
|
||||
"enemy_1196_msfyin": [62,60,2,0,122],
|
||||
"enemy_1197_sfshu": [48,42,6,5882,183185],
|
||||
"enemy_1198_msfshu": [126,118,8,0,366],
|
||||
"enemy_1207_sfji": [12,10,2,69314,186308],
|
||||
"enemy_1208_msfji": [10,0,10,0,119]
|
||||
},
|
||||
"hooks": [1,320,6047,960,960,1820,1800,140,271,307,28,25,0,31391,0,80,297,36,1,272,0,0,20],
|
||||
"events": {"atk":1011,"deploy":20,"die":271,"dmg":1800,"fx":414,"heal":133,"leak":36,"skill":53,"spawn":312,"status":40},
|
||||
"fx": {"aoe":97,"dodge":20,"dp":130,"slash":72,"spGain":12,"summon":83},
|
||||
"snaps": "6506a5c4 821dac7d 9216e8f9 291487fb 5d47614f 5dff6977 3eeeee66 9a21bbca 8e530e36 4e93e9d1 54f80175 b1404805 b528127f 7fbbfb92 0cf1a8e3 356ad797 4d9a6a79 edae73d6 39b44426 08667abf 724dcbc6"
|
||||
"enemies": {"enemy_1006_shield_3":[18,0,18,15024,2518],"enemy_1043_zomsbr":[72,29,43,1115,132266],"enemy_1402_tgshd_2":[27,0,27,33237,8353]},
|
||||
"hooks": [1,153,6240,753,753,785,772,8968,61,149,0,32,0,0,32,63,117,88,1,61,0,0,13],
|
||||
"events": {"atk":753,"deploy":33,"die":61,"dmg":753,"fx":323,"leak":88,"skill":66,"spawn":132,"status":114},
|
||||
"fx": {"aoe":11,"buff":10,"displace":15,"dodge":13,"expose":262,"shield":12},
|
||||
"snaps": "0b83bf08 e3cbec55 dbf28a7a 7290d54c 30785162 f37ddaa7 69c93760 67445b70 e91b1fab dd19c80c ac906355 31e16c17 4913717b bd5627a8 7ea314a7 a6868304 dd6a0e15 ba15048d 5e6cb7d2 41afc869 516fdedf"
|
||||
},
|
||||
"diy-103": {
|
||||
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acsupo 6_diy2_b=acsupo",
|
||||
"about": "6_diy1_b=phatom/S1 6_diy2_b=phatom/S2 6_diy1_b=phatom/S3 6_diy2_b=phatom/S1/002_phatom 6_diy1_b=phatom/S2/002_phatom 6_diy2_b=phatom/S3/002_phatom 6_diy1_b=phatom/S1/003_phatom 6_diy2_b=phatom/S2/003_phatom 6_diy1_b=phatom/S3/003_phatom 6_diy2_b=phatom/S1/004_phatom 6_diy1_b=phatom/S2/004_phatom 6_diy2_b=phatom/S3/004_phatom",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m04",
|
||||
@@ -4397,32 +4383,279 @@
|
||||
"round": 13,
|
||||
"seed": 1753717681,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 210.67,
|
||||
"ticks": 6320,
|
||||
"time": 219.57,
|
||||
"ticks": 6587,
|
||||
"reason": "cleared",
|
||||
"killed": 23,
|
||||
"killed": 7,
|
||||
"total": 123,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 202,
|
||||
"rngDraws": 61,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":23,"total":123,"perfect":false,"leaks":100,"leaksUncounted":0,"coins":0,"dmg":407198,"bossDmg":0,"heal":0,"deaths":21,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":7,"total":123,"perfect":false,"leaks":116,"leaksUncounted":0,"coins":0,"dmg":243998,"bossDmg":0,"heal":0,"deaths":33,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,86,2,27050,3730,0,26,1],
|
||||
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,126,14,178020,4988,0,57,1],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,148,0,42484,9232,0,50,0],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",651.83,1,150,3,84913,11220,0,77,0],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,160,1,132,0,30869,2160,0,81,2],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",450,1,133,2,43862,2450,0,84,2],
|
||||
["p1",1,"op","chess_char_6_diy1_b",0,"none",1523,1,160,1,28313,7615,0,75,0],
|
||||
["p1",2,"op","chess_char_6_diy2_b",1,"none",0,0,150,2,37180,7615,0,46,0],
|
||||
["p1",3,"op","chess_char_6_diy1_b",2,"none",0,0,157,1,38907,7615,0,50,0],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_002_phatom",0,0,159,0,32770,8315,0,68,0],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_002_phatom",0,0,43,0,21310,3326,0,20,0],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_phatom",0,0,13,1,10286,1663,0,23,0],
|
||||
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_003_phatom",0,0,45,0,12344,3046,0,26,0],
|
||||
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_003_phatom",0,0,39,0,29363,3046,0,32,0],
|
||||
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_003_phatom",0,0,5,0,3978,1523,0,9,0],
|
||||
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_004_phatom",0,0,8,0,4380,1523,0,10,0],
|
||||
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_004_phatom",0,0,40,0,7981,3046,0,7,0],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_004_phatom",0,0,42,0,17186,3046,0,28,0],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1148_dssbr":[51,12,39,8112,169910],"enemy_1148_dssbr_2":[60,11,49,15328,229068],"enemy_1439_dslntf":[12,0,12,10340,113857]},
|
||||
"hooks": [1,150,6320,499,499,2118,2622,0,44,144,6,19,0,7621,4142,38,123,100,1,44,0,10,0],
|
||||
"events": {"atk":538,"deploy":24,"die":44,"dmg":2564,"fx":61,"leak":100,"skill":42,"spawn":132,"status":458},
|
||||
"fx": {"burst":10,"displace":45,"dodge":6},
|
||||
"snaps": "76a5789a 0d8fe1fc aca88ba3 93412a3b 57642d38 7d0e9d04 05edb1c1 12d5b5b4 b880f5a2 706856a1 61435e3d a65a20a2 35fd9300 9021e870 94a24b43 85a49207 c097c8a1 9acd4c1a a1420866 0769fc29 60f3e7a3"
|
||||
"enemies": {"enemy_1148_dssbr":[51,6,45,17620,134552],"enemy_1148_dssbr_2":[60,1,59,17407,124417],"enemy_1439_dslntf":[12,0,12,11733,88895]},
|
||||
"hooks": [1,160,6587,596,596,2180,2671,0,40,156,0,33,0,0,32,41,123,116,1,40,0,12,23],
|
||||
"events": {"atk":596,"deploy":34,"die":40,"dmg":2652,"fx":89,"leak":116,"skill":68,"spawn":138,"status":100},
|
||||
"fx": {"aoe":9,"buff":11,"burst":12,"displace":20,"dodge":23,"shield":14},
|
||||
"snaps": "04c8f385 d7b109b1 2b067eac 9fa119a2 05dc8e7b 3d1792e4 83e94026 703969fb 36239407 2cf0e563 fe7dabf3 99566ff9 0d501ee7 57ad0932 06695c20 852706de 5448c37b 22f489b5 e5b6f91e fb38acbf ae3846f9 4392eba7"
|
||||
},
|
||||
"diy-104": {
|
||||
"about": "5_diy1_a=crosly/S1 5_diy2_a=crosly/S2 5_diy1_a=crosly/S3 5_diy2_b=crosly/S1 5_diy1_b=crosly/S2 5_diy2_b=crosly/S3 5_diy1_b=crosly/S1/002_crosly 5_diy2_b=crosly/S2/002_crosly 5_diy1_b=crosly/S3/002_crosly 5_diy2_b=crosly/S1/003_crosly 5_diy1_b=crosly/S2/003_crosly 5_diy2_b=crosly/S3/003_crosly",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m05",
|
||||
"modeId": "mode_multi_hard",
|
||||
"round": 12,
|
||||
"seed": 3393927053,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 0,
|
||||
"total": 90,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 43,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":0,"total":90,"perfect":false,"leaks":90,"leaksUncounted":0,"coins":0,"dmg":133736,"bossDmg":0,"heal":0,"deaths":23,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,1322,1,120,0,3639,6610,0,47,6],
|
||||
["p1",2,"op","chess_char_5_diy2_a",1,null,1322,1,121,0,24456,5288,0,67,5],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,1322,1,118,0,18135,3966,0,103,4],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"none",0,0,119,0,21337,6276,0,135,4],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"none",1569,1,122,0,39794,4707,0,90,4],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,41,0,18065,3138,0,68,2],
|
||||
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_crosly",0,0,32,0,4954,3318,0,24,2],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_crosly",1659,1,1,0,0,0,0,0,1],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_crosly",1659,1,3,0,0,0,0,0,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_crosly",1653,1,5,0,0,0,0,0,1],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_crosly",1653,1,7,0,0,0,0,0,1],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_crosly",1653,1,9,0,3356,0,0,2,1]
|
||||
],
|
||||
"enemies": {"enemy_1116_liprr_2":[27,0,27,7104,101485],"enemy_1119_vofsd":[39,0,38,14792,77994],"enemy_1121_lifbos_2":[24,0,24,11407,37016]},
|
||||
"hooks": [1,122,6900,598,598,2742,2738,3045,23,112,32,31,0,0,40,24,90,89,1,23,0,0,4],
|
||||
"events": {"atk":598,"deploy":32,"die":23,"dmg":2738,"fx":90,"leak":89,"skill":63,"spawn":102,"status":214},
|
||||
"fx": {"aoe":10,"dodge":10,"liberate":22,"stealth":8,"strike":40},
|
||||
"snaps": "b4a599d5 f0e7a0e6 67bb7e23 d947fe35 14a27831 67ab7140 151aec35 99d48975 6e0f3173 e7fbed65 83be7ed9 cfcf338b be6d7006 6d9bb357 576a726b 6a935307 c9318640 af79fb18 c9280591 a388c6a2 4d247744 c75bfcb1 7f2dee0e"
|
||||
},
|
||||
"diy-105": {
|
||||
"about": "6_diy1_a=crosly/S1 6_diy2_a=crosly/S2 6_diy1_a=crosly/S3 6_diy2_b=crosly/S1 6_diy1_b=crosly/S2 6_diy2_b=crosly/S3 6_diy1_b=crosly/S1/002_crosly 6_diy2_b=crosly/S2/002_crosly 6_diy1_b=crosly/S3/002_crosly 6_diy2_b=crosly/S1/003_crosly 6_diy1_b=crosly/S2/003_crosly 6_diy2_b=crosly/S3/003_crosly",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m06",
|
||||
"modeId": "mode_single_normal",
|
||||
"round": 13,
|
||||
"seed": 3007159473,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 2,
|
||||
"total": 105,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 47,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":2,"total":105,"perfect":false,"leaks":103,"leaksUncounted":0,"coins":0,"dmg":139079,"bossDmg":0,"heal":0,"deaths":26,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,135,0,16384,7932,0,70,6],
|
||||
["p1",2,"op","chess_char_6_diy2_a",1,null,1322,1,7,0,0,0,0,0,1],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,1322,1,9,0,1386,0,0,1,1],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"none",1569,1,137,0,11417,7845,0,110,6],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"none",1569,1,134,0,17398,4707,0,62,4],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"none",1569,1,119,1,37618,3138,0,59,3],
|
||||
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_002_crosly",1699,1,136,0,2756,3398,0,52,3],
|
||||
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_002_crosly",0,0,84,0,26603,5097,0,42,3],
|
||||
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_002_crosly",0,0,63,1,21447,3398,0,41,2],
|
||||
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_003_crosly",0,0,1,0,535,1704,0,16,1],
|
||||
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_003_crosly",0,0,3,0,328,1704,0,10,1],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_003_crosly",0,0,5,0,3208,1704,0,11,1]
|
||||
],
|
||||
"enemies": {"enemy_1116_liprr_2":[27,2,25,10560,92016],"enemy_1119_vofsd":[48,0,47,17542,33098],"enemy_1121_lifbos_2":[30,0,30,12525,13966]},
|
||||
"hooks": [1,137,6900,594,594,629,624,3574,28,130,32,31,0,0,31,28,105,102,1,28,0,0,5],
|
||||
"events": {"atk":594,"deploy":32,"die":28,"dmg":624,"fx":90,"leak":102,"skill":63,"spawn":117,"status":217},
|
||||
"fx": {"aoe":9,"dodge":17,"liberate":26,"stealth":7,"strike":31},
|
||||
"snaps": "9e71a576 5f85bc01 c3ec0c6e 907b650d 8d0a1507 16eb2156 378d038b 6506577e 30807fb9 f2c86b9d ec752e72 d7303d83 83002a0a 01b70ca0 09e839c4 98d2358c 161de0dc 5f1fb139 3915cceb aeddf47d a8963bbe 76037450 84a29f92"
|
||||
},
|
||||
"diy-106": {
|
||||
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 5_diy1_a=csnipe 5_diy2_b=csnipe 5_diy1_a=ccast 5_diy2_b=ccast 5_diy1_a=cmedic 5_diy2_b=cmedic 5_diy1_a=csuppo 5_diy2_b=csuppo",
|
||||
"fast": true,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m07",
|
||||
"modeId": "mode_multi_abyss",
|
||||
"round": 12,
|
||||
"seed": 352330692,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 193.8,
|
||||
"ticks": 5814,
|
||||
"reason": "cleared",
|
||||
"killed": 0,
|
||||
"total": 126,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":0,"total":126,"perfect":false,"leaks":126,"leaksUncounted":0,"coins":0,"dmg":59922,"bossDmg":0,"heal":8548,"deaths":25,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,3,0,1808,2128,0,8,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,6,0,9112,3634,0,34,1],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,83,0,1003,6580,0,8,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"none",0,0,86,0,14738,7835,0,17,0],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,153,0,9754,4473,0,27,1],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,94,0,11704,3746,0,27,1],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,90,0,2802,2266,0,12,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"none",0,0,79,0,3020,2720,0,11,0],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,141,0,0,5763,2109,6,0],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"none",0,0,128,0,0,3807,6439,10,1],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,112,0,2354,1864,0,12,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,148,0,3628,4041,0,16,1],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1251_lysyta_2":[54,0,54,8019,34461],"enemy_1252_lysytb_2":[42,0,42,35352,24551],"enemy_1329_cbshld_2":[30,0,30,5487,911]},
|
||||
"hooks": [1,153,5814,380,380,358,358,16,25,151,5,5,0,8987,28,10,126,126,1,25,0,0,0],
|
||||
"events": {"atk":380,"deploy":25,"die":25,"dmg":358,"heal":16,"leak":126,"skill":10,"spawn":140,"status":50},
|
||||
"fx": {},
|
||||
"snaps": "57186c39 a89f274b e649d7d9 75621917 006f6ba9 7f3b8dda bb3864de e8193348 16cc7f17 db8194aa e07167eb 0946e952 5d27c316 103f7135 f7e98be4 ef855c6a d34a4bb6 5d39faae c6e66696 77bde8df"
|
||||
},
|
||||
"diy-107": {
|
||||
"about": "5_diy1_a=acfend 5_diy2_b=acfend 5_diy1_a=acpion 5_diy2_b=acpion 5_diy1_a=acguad 5_diy2_b=acguad 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m01",
|
||||
"modeId": "mode_multi_funny",
|
||||
"round": 12,
|
||||
"seed": 4020340196,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 203.3,
|
||||
"ticks": 6099,
|
||||
"reason": "cleared",
|
||||
"killed": 90,
|
||||
"total": 90,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 450,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":90,"total":90,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":792864,"bossDmg":0,"heal":34212,"deaths":7,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,2604,1,106,0,2245,2604,0,9,1],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",2367.8,1,105,0,4281,4374,0,19,1],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,100,5,45221,3058,0,15,4],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,7,3,48414,1904,0,24,6],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,95,13,46166,11997,0,51,3],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",3411,1,8,3,33047,3326,0,19,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1272,1,2,0,13858,0,0,19,1],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,3,38,146945,0,0,177,2],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,1372,1,4,1,19328,0,0,20,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1495.56,1,12,27,433358,24418,0,89,3],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,1224,1,1,0,0,0,11831,36,2],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1414,1,5,0,0,0,22381,66,2]
|
||||
],
|
||||
"enemies": {"enemy_1229_darmy":[27,27,0,3192,108864],"enemy_1229_darmy_2":[51,51,0,12310,269280],"enemy_9007_acelem":[12,12,0,36180,414720]},
|
||||
"hooks": [1,106,6099,594,594,1735,1777,160,97,97,28,27,0,32480,0,62,90,0,1,97,0,2,0],
|
||||
"events": {"atk":764,"deploy":16,"die":97,"dmg":1777,"fx":154,"heal":142,"skill":55,"spawn":102,"status":62},
|
||||
"fx": {"aoe":52,"beam":25,"burst":2,"dp":18,"slash":57},
|
||||
"snaps": "03984647 ace8dbe4 49520875 0ad8fbe6 ba7c56bb e58d0120 a539ef32 0e2bc9be 3b333b61 db04449a dace616b 63a39256 5c34a56e d1619dfd 63bd653a d74deb99 1d733ab1 6e8cbc08 04eb4bb7 74b3c8bb 80dc47fc"
|
||||
},
|
||||
"diy-108": {
|
||||
"about": "6_diy1_a=acfend 6_diy2_b=acfend 5_diy1_a=sharp2 5_diy2_b=sharp2 6_diy1_a=acpion 6_diy2_b=acpion 5_diy1_a=acspec 5_diy2_b=acspec 6_diy1_a=acguad 6_diy2_b=acguad 5_diy1_a=acsupo 5_diy2_b=acsupo",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m02",
|
||||
"modeId": "mode_single_hard",
|
||||
"round": 13,
|
||||
"seed": 1366160473,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 15,
|
||||
"total": 36,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 95,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":15,"total":36,"perfect":false,"leaks":21,"leaksUncounted":0,"coins":0,"dmg":223366,"bossDmg":0,"heal":0,"deaths":32,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,3,0,0,2604,0,0,0],
|
||||
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,4,0,0,3431,0,0,0],
|
||||
["p1",3,"op","chess_char_5_diy1_a",0,null,0,0,5,1,41288,1865,0,28,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,8,2,117437,2374,0,51,1],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,1051.96,1,71,0,9905,3535,0,0,2],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,68,0,22211,5712,0,0,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1154,1,72,0,0,10386,0,0,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,73,0,0,10136,0,0,0],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,43,0,0,5276,0,0,0],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",0,0,52,0,0,7062,0,0,0],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,6,0,13315,1160,0,28,1],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",0,0,54,0,19210,2860,0,23,1],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1005_yokai_2":[12,12,0,102,639],"enemy_1005_yokai_3":[12,0,11,29968,87430],"enemy_9009_acfort":[12,3,9,26331,135297]},
|
||||
"hooks": [1,73,6900,439,439,497,550,0,47,67,8,24,0,10810,814,0,36,20,1,35,0,0,17],
|
||||
"events": {"atk":472,"deploy":35,"die":47,"dmg":550,"fx":92,"leak":20,"skill":48,"spawn":50,"status":162},
|
||||
"fx": {"beam":35,"dodge":21,"dp":4,"slash":32},
|
||||
"snaps": "7c7487b3 43a949cf 206930da 90551654 2113b5a2 a7c4855b 59932997 42395489 3f83a15d ffbd721d 646072f3 fc3be7c4 b210ec54 960a9e51 aa1af455 09ccfce3 50e6e3d0 380e639b 9eaf5bef 3b943f89 3933d1fc 3718a2f5 a8ae4307"
|
||||
},
|
||||
"diy-109": {
|
||||
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc 6_diy1_a=acsupo 6_diy2_b=acsupo",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m03",
|
||||
"modeId": "mode_multi_normal",
|
||||
"round": 13,
|
||||
"seed": 500258608,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 215.33,
|
||||
"ticks": 6460,
|
||||
"reason": "cleared",
|
||||
"killed": 15,
|
||||
"total": 123,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 453,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":15,"total":123,"perfect":false,"leaks":108,"leaksUncounted":0,"coins":0,"dmg":280261,"bossDmg":0,"heal":30749,"deaths":28,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,5,0,1376,2285,0,5,0],
|
||||
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,7,0,982,2494,0,2,0],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,155,2,36128,9372,0,79,0],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",1484,1,156,2,52788,20167,0,62,0],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,140,4,59650,2964,0,77,1],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",0,0,154,6,91210,11325,0,66,1],
|
||||
["p1",7,"op","chess_char_6_diy1_a",2,null,0,0,86,0,3596,3133,0,4,0],
|
||||
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,83,0,19042,8583,0,5,1],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,4,0,0,2326,11819,20,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,10,0,0,4670,18930,25,1],
|
||||
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,108,0,5752,2320,0,16,1],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",0,0,8,1,9738,1450,0,16,1],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1019_jshoot_2":[57,10,47,52058,82202],"enemy_1299_ymkilr_2":[27,0,27,9819,9240],"enemy_1422_lrsldr_2":[39,5,34,9213,188819]},
|
||||
"hooks": [1,156,6460,632,632,849,850,92,43,151,7,20,0,9133,611,30,123,108,1,43,0,0,0],
|
||||
"events": {"atk":705,"deploy":30,"die":43,"dmg":822,"fx":36,"heal":76,"leak":108,"skill":42,"spawn":138,"status":149},
|
||||
"fx": {"aoe":2,"displace":17,"dodge":2,"spGain":15},
|
||||
"snaps": "17351197 d85b764f 170d4969 276e2353 d5273c76 84c8775e 178ead82 e788140a 7761c24b 8d4c0668 9647f92d 7caa4901 24e1ffd3 00675a6a 82e3afd4 846d95f4 87dfd927 4f075077 94a7b0a4 a3a639d3 4179fb65 3ad0ed6c"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user