feedback5 O20: 自选 kits for 多萝西 (with her 共振装置), 予愿安洁莉娜 (with “一会儿见!”) and 黑键 (with his 旧日残影)

Every skill, talent, trait and module (stages 1 and 3) at every form, from backups.json, PRTS (备注) and the client's
battle data (charpacks, skill / equip / projectile / token prefabs, buff templates); [ASSUMED] items in each file header.
- 多萝西 (op-doroth.js): placed traps deploy with the board and come back on their tile once the card is ready, a trap is in
  stock and the cost is paid; stock 8 refilled at each deployment, +1 per cast (AUTO acting on herself: SP_FULL), 阻回 when
  full; T1 summons on deploy; S1 / S2 / S3 trap effects (S3 chains other traps 2 s later); 梦想家 stacks; TRP-Y x2 traps;
  TRP-X cost 2 / limit 13, stage 3: 3 summons and 50 % extra per placed piece (re-rolled at full T2).
- 予愿安洁莉娜 (op-aglna2.js): 起飞 only while a skill runs (air block, 对地规避); T1 arts addition (weight <= 3) and 失重 in
  her range airborne; T2 ATK and 生命回复速度 for airborne operators; S1 56 s from each deployment; S2 glide (浮空 / 缚地
  sweep, 2.5 s chant); S3 ammo with 对空庇护, 3 ground + flyers, the move to a blockable flyer, the marker, the anchored range.
- 黑键 (op-ebnhlz.js): his own store (on time), 强弱法 elite energy, 倚音 per projectile (+ MSC-Y splash, MSC-Δ 元素伤害),
  MSC-Y ASPD, MSC-Δ 凋亡; S1 4-1; S2 remnants (activation, 0.93 s blast, inward push, 30 s); S3 elite-only and conversions.
- kits/index.js: three lines in OPERATOR_KIT_FILES; docs/SIM.md §8: the skywalker / traper rows.
Golden: only diy moved — diy-106 … diy-111 now field the new configs, the prototype battles moved to diy-111 … diy-115
(the 12-per-battle chunks shift); diy-001 … diy-105 unchanged. Tests: test/content/op_doroth.test.js (11),
op_aglna2.test.js (9), op_ebnhlz.test.js (10).
This commit is contained in:
sganggs committed 2026-10-06 05:22:08 +08:00
1 parent 83e879a335
commit 5029b646a3
9 files changed
+2635 -102

No files matched your search

+292
View File
@@ -0,0 +1,292 @@
// server/sim/content/kits/ops/op-doroth.js — 多萝西 (char_4048_doroth) 自选 operator kit: 6★ 陷阱师 (特种), an owned-6★ pick
// of the tier-5 and tier-6 自选 slots; every skill, both talents, the trait, both modules (TRP-Y 童话书, TRP-X 梦中人) at
// every form, and the kit of her trap 共振装置 (token_10025_doroth_recttp). Kit contract and the 自选 rules: ../README.md
// ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_4048_doroth): 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 多萝西 / 共振装置 (备注) and 分支特性信息 §陷阱师, and the client's battle data (charpack char_4048_doroth: SpawnTokens,
// KillTokens, the T2 mode switch; skills skchr_doroth_1–3 / sktok_doroth_1–3; equips doroth_equip_1_* / 2_3_p1; buff
// templates doroth_t_2, doroth_token[*], trapper_e_trait[*], doroth_e_003[advanced_spawn_token], charge_token[refresh],
// trigger_charge_token, die_to_kill_token).
// - Trait (陷阱师) "可以使用陷阱来协助作战,但陷阱无法放置于敌人已在的格子中": ranged physical arrows, 3-3, hits air units
// (分支特性信息 "可对空"; the data's canHitFly over the profession table), blocks 1, ground enemies target her. Her traps
// never (re)deploy on a tile a ground enemy stands on (分支特性信息: "适用飞行单位判定的敌人(如浮空、近地悬浮)不影响陷阱的部署")
// and leave when she leaves the field (分支特性信息 "干员离场后,附属的陷阱随之消失"; die_to_kill_token).
// - The traps in this mode: her 共振装置 are hand pieces the player places (data `placeable`; the prep hands out a stack —
// content/tokens.js, server/match/player/diy.js). Placed pieces deploy with the board, free and ignoring her stock (PRTS
// 卫戍协议/帮助 "所有手动部署的召唤物,无视所属干员的持有状态…作战开始时立即部署一次"); a used-up one stays its tile's piece and
// comes back there (PRTS "若召唤物在战斗期间退场,将在满足条件后立即原地再部署1个") once the deployment conditions hold: she is on
// the field, her stock holds a trap, the token card's redeploy time has run since the last trap deployment (data
// respawnTime 5 s: 备注 "…会让共振装置进入再部署CD"), its player has its cost in DP (3; TRP-X 2), fewer of her traps than
// the deploy limit stand (data deployLimit 10; TRP-X 13 — 备注 "陷阱部署上限与最多拥有数量相同") and no ground enemy is on
// the tile; one piece per check (uid order), paying the cost and spending one trap.
// - T1 共振装置 "可以使用8个共振装置(最多拥有10个),踩上去的第一个敌人会触发其效果,部署后立刻在攻击范围内召唤2个共振装置"
// (bb cnt, attack@max_cnt; TRP-X: 13 and stage 3: 3): her stock is refilled to cnt at each deployment (charge_token[refresh])
// and holds at most the deploy limit; at each deployment attack@max_cnt traps appear on free melee tiles of her attack
// range with no ground enemy (the enemies' ground paths first, random among equals — [ASSUMED]: the client's selector
// filter 16 is not decoded), spending no trap but counting toward the limit (备注); the placed pieces off the field keep
// their slots (no summon takes one) [ASSUMED]. 阻回 while the stock is full (备注; `_stopSpWhenTokenIsFull`).
// - 共振装置 (its kit below): 不会受到攻击 (data: untargetable), blocks nothing, no normal attack. The first selectable ground
// enemy standing on its tile (sktok_doroth_* SkillTrigger: targetMotion ground, range 0-1; the earliest spawned of
// several) sets it off — the effect of the token skill of her pick (bySkill), with her current ATK (PRTS "技能伤害始终借用
// 持有者的攻击力计算"), from the trap (its source) —, then it withdraws (doroth_token[withdraw]; the blast fx carries
// `consumed`). S1 危险目标清除: that enemy takes atk_scale × ATK physical and DEF ×(1 + def) for `duration` s (formula
// FINAL_SCALER: the catalogue `defDown`). S2 流沙区域生成: every selectable ground enemy within 1.2 (备注 "陷阱爆炸范围为1.2半径
// 的圆") takes atk_scale × ATK physical and 束缚 `duration` s — `duration_2` s when it hit only one (cnt_2). S3
// 高速共振排障: every selectable ground enemy on the trap's x-6 takes atk_scale × ATK arts and 停顿 `sluggish` s, and every
// other trap of hers on that x-6 goes off `interval` s later (备注 "延迟触发时长为2s"; doroth_token_s3[trigger]: once — a
// trap already waiting keeps its time [ASSUMED]), with no enemy needed, and sets off its own x-6 the same way.
// - T2 梦想家 "陷阱触发后,多萝西获得2%的攻击力,最多叠加10层" (bb atk, max_stack_cnt; TRP-Y stage 3: 4%): every trap that goes
// off gives her one stack (ATK +atk, 直接乘算) until she leaves the field; with S1 / S2 before its damage, with S3 after
// it (备注). Full stacks switch her attack mode (doroth_t_2 SwitchMode: the Attack_2 clip — cosmetic here).
// - TRP-Y 童话书 "有20%概率部署造成2倍伤害的陷阱" (trait bb prob, atk_scale): each trap deployment rolls prob; such a trap deals
// ×atk_scale (trapper_e_trait[token_atk_scale] AtkScaleUp; the red "!" — fx 'mark'). Stage 3: T2 4 %.
// - TRP-X 梦中人: traps cost 1 less and the limit is 13 (the token's module stats), T1 "最多拥有13个"; stage 3: T1 summons 3 and
// "部署共振装置时有50%概率在攻击范围内额外召唤一个共振装置" (hidden talent prob, max_cnt, rangeGrid): on each deployment of a
// placed piece (备注 "仅手动部署的共振装置可触发模组3级效果" — the pieces the player placed [ASSUMED]: the start deployment and
// the redeploys) prob to summon max_cnt more on a tile as T1's, within the limit, spending no trap and ignoring the card's
// redeploy time but restarting it (备注); and when her T2 stacks fill (her attack mode switch) every placed piece standing
// rolls again (备注 "多萝西切换攻击模式时…为所有已手动部署的共振装置重新判定“概率额外召唤”").
// - S1 / S2 / S3 (AUTO, 19 / 16 SP, the data's DEFAULT): 被动效果 = the trap's effect above; 主动效果 "立即获得一个陷阱" (bb cnt):
// +cnt to her stock (trigger_charge_token). The cast acts on herself only, so it fires as soon as its SP is full
// (`trigger: 'SP_FULL'` — the owner's AUTO rule, kits/README.md checklist 5; skchr_doroth_* `_allowNoTarget`).
import { num, talentBb, moduleBb, traitBb, up, toggleBuff, summonTileFree } from '../shared/tier1.js';
import { absoluteRangeKeys, canTargetEnemy } from '../../../targeting.js';
import { bodyOnTile } from '../../../body.js';
import { COLS } from '../../../constants.js';
const S1 = 'skchr_doroth_1';
const S2 = 'skchr_doroth_2';
const S3 = 'skchr_doroth_3';
/** 共振装置, her trap. */
export const RESONATOR = 'token_10025_doroth_recttp';
const TS1 = 'sktok_doroth_1';
const TS2 = 'sktok_doroth_2';
const TS3 = 'sktok_doroth_3';
/** S2's blast (PRTS S2 备注 "陷阱爆炸范围为1.2半径的圆"). */
const S2_RADIUS = 1.2;
/** S3's area when the token skill carries no grid: range x-6 (the cross of radius 2). */
const X6 = Object.freeze([[2, 0], [1, 0], [0, -2], [0, -1], [0, 0], [0, 1], [0, 2], [-1, 0], [-2, 0]]);
/** S3's delayed trigger when the blackboard carries none (PRTS 备注 "延迟触发时长为2s"). */
const S3_DELAY = 2;
/** How often a used-up piece checks its redeploy conditions. */
const WATCH = 0.25;
const GROUND = Object.freeze({ canHitFly: false, groundOnly: true });
const DREAM_KEY = 'talent:doroth:dream';
const tokenOf = (t) => t && t.kind === 'token' && t.defId === RESONATOR;
/** A ground enemy stands on tile (r, c) — a huge one on any tile of its body (body.js); air units never count. */
function groundEnemyOn(battle, r, c) {
for (const e of battle.enemies) {
if (!e.alive || e.hidden || e.isFlying) continue;
if (e.hitArea ? bodyOnTile(e, r, c) : Math.abs(e.x - c) <= 0.5 && Math.abs(e.y - r) <= 0.5) return true;
}
return false;
}
export default {
char_4048_doroth: (bb, chess) => {
const t0 = talentBb(chess, 0); // cnt, attack@max_cnt
const t1 = talentBb(chess, 1); // atk, max_stack_cnt
const tb = traitBb(chess); // TRP-Y: prob, atk_scale
const hidden = moduleBb(chess); // TRP-X stage 3: prob, max_cnt
const hiddenGrid = (chess?.talents ?? []).find((t) => t && t.index === -1 && t.rangeGrid)?.rangeGrid ?? null;
const stockRefill = Math.max(0, Math.floor(num(t0.cnt, 0)));
const onDeploy = Math.max(0, Math.floor(num(t0['attack@max_cnt'], 0)));
const dream = { atk: num(t1.atk), max: Math.max(0, Math.floor(num(t1.max_stack_cnt, 0))) };
const dbl = { prob: num(tb.prob), scale: num(tb.atk_scale, 1) };
const extra = { prob: num(hidden.prob), cnt: Math.max(0, Math.floor(num(hidden.max_cnt, 0))) };
// her trap stock, the card's ready time, whether her T2 has filled this deployment, the deploy limit (per unit: the
// factory runs per unit; also `unit.trait.doroth` for diagnostics and tests)
const D = { stock: 0, readyAt: -Infinity, full: false, limit: Infinity };
const traps = (battle, unit) => battle.allyUnits.filter((t) => tokenOf(t) && t.ownerUnit === unit);
const standing = (battle, unit) => traps(battle, unit).filter((t) => t.alive).length;
/** Placed pieces still to come (not on the field, not gone): the T1 summons leave their slots free. */
const waiting = (battle, unit) => traps(battle, unit).filter((t) => t.uid != null && !t.alive && !t.removed).length;
/** The token's deploy limit for her loadout (variant stats, the module's when it carries one); her max stock too. */
function limitOf(battle, unit) {
const raw = battle.data.rawToken?.(RESONATOR);
const v = raw?.variants?.[unit.def?.tokenOwner];
const mod = unit.def?.loadout?.moduleId;
const n = num(v?.byModule?.[mod]?.stats?.deployLimit, num(v?.stats?.deployLimit, num(raw?.deployLimit, 0)));
if (n > 0) return n;
const m = String(chess?.talents?.find((t) => t && t.index === 0)?.desc ?? '').match(/最多拥有(\d+)个/);
return m ? +m[1] : Infinity;
}
/** Free melee tiles of her range (or of `grid` around her) with no ground enemy: enemy paths first, then random. */
function summonTiles(battle, unit, grid = null) {
const keys = grid ? absoluteRangeKeys(grid, unit.tileR, unit.tileC, unit.dir, unit.s.rangeExtend || 0) : unit.rangeKeys || [];
const path = battle.groundPathTiles();
const out = [];
for (const k of keys) {
const r = Math.floor(k / COLS), c = k % COLS;
if (!summonTileFree(battle, r, c) || !battle.grid.canStand(r, c) || groundEnemyOn(battle, r, c)) continue;
out.push({ r, c, p: path.has(k) ? 0 : 1 });
}
battle.rng.shuffle(out);
return out.sort((a, b) => a.p - b.p);
}
/** Summon up to `n` traps (no stock spent) on summonTiles, within the limit (`room` more at most). */
function summon(battle, unit, n, grid = null, room = Infinity) {
let left = Math.min(n, room, D.limit - standing(battle, unit));
if (!(left > 0)) return 0;
let made = 0;
for (const { r, c } of summonTiles(battle, unit, grid)) {
if (left <= 0) break;
const t = battle.spawnToken(unit, RESONATOR, r, c, { kit: trapKit(unit) });
if (!t) continue;
battle.fx('summon', { x: t.x, y: t.y, id: t.id, token: RESONATOR });
left--; made++;
}
return made;
}
/** TRP-X stage 3 on a placed piece's deployment (and her mode switch): prob to summon max_cnt more. */
function rollExtra(battle, unit) {
if (!(extra.prob > 0) || !(extra.cnt > 0) || !up(unit) || !battle.rng.chance(extra.prob)) return;
summon(battle, unit, extra.cnt, hiddenGrid, D.limit - standing(battle, unit) - waiting(battle, unit));
}
/** 梦想家: one stack (to max_stack_cnt); full stacks are her attack mode switch (TRP-X stage 3 re-rolls). */
function dreamStack(battle, unit) {
if (!up(unit) || !(dream.atk > 0) || !(dream.max > 0)) return;
const b = unit.findBuff(DREAM_KEY);
const n = b ? b.stacks : 0;
if (n >= dream.max) return;
battle.addBuff(unit, { key: DREAM_KEY, refresh: 'stack', stacks: 1, maxStacks: dream.max, mods: { atkPct: dream.atk }, tags: ['talent'] });
if (n + 1 >= dream.max && !D.full) {
D.full = true;
for (const t of traps(battle, unit)) if (t.alive && t.uid != null) rollExtra(battle, unit);
}
}
/** The trap `t` goes off (`e` = the enemy that stepped on it; null for a delayed S3 trigger). */
function fire(battle, unit, t, e) {
if (!t.alive || t.mem.dorFired) return;
t.mem.dorFired = true;
const sk = t.def?.skill ?? null;
const tb2 = sk?.bb ?? {};
const id = sk?.id ?? TS1;
const s3 = id === TS3;
if (!s3) dreamStack(battle, unit); // 备注: with S1 / S2 before the damage
const atk = (unit.s ? unit.s.atk : t.s.atk) * (t.mem.dorDouble ? dbl.scale : 1);
const tags = ['summon', 'trap', 'doroth:trap'];
if (id === TS2) {
const victims = battle.foesInRadius(t.x, t.y, S2_RADIUS, true).filter((x) => canTargetEnemy(t, x, GROUND));
const dur = victims.length === Math.max(1, Math.floor(num(tb2.cnt_2, 1))) ? num(tb2.duration_2, num(tb2.duration)) : num(tb2.duration);
for (const x of victims) {
battle.dealDamage(t, x, { amount: atk * num(tb2.atk_scale), type: 'phys', isSkill: true, tags });
if (x.alive && dur > 0) battle.applyStatus(x, 'bind', { duration: dur, source: t });
}
battle.fx('explode', { x: t.x, y: t.y, r: S2_RADIUS, id: t.id, consumed: true, kind: 'doroth:quicksand' });
} else if (s3) {
const keys = absoluteRangeKeys(sk?.rangeGrid ?? X6, t.tileR, t.tileC, t.dir, 0);
for (const x of battle.enemiesInKeys(keys, t, GROUND)) {
battle.dealDamage(t, x, { amount: atk * num(tb2.atk_scale), type: 'arts', isSkill: true, tags });
if (x.alive && num(tb2.sluggish) > 0) battle.applyStatus(x, 'sluggish', { duration: num(tb2.sluggish), source: t });
}
// 并延迟触发范围内的其他共振装置
const set = new Set(keys);
const delay = num(tb2.interval, S3_DELAY);
for (const o of traps(battle, unit)) {
if (o === t || !o.alive || o.mem.dorFired || o.mem.dorWaiting || !set.has(o.tileR * COLS + o.tileC)) continue;
o.mem.dorWaiting = true;
const seq = o.deploySeq;
battle.after(delay, () => { if (o.alive && o.deploySeq === seq) fire(battle, unit, o, null); }, { owner: o });
}
battle.fx('explode', { x: t.x, y: t.y, r: 2, id: t.id, consumed: true, dmgType: 'arts', kind: 'doroth:resonance' });
dreamStack(battle, unit); // 备注: with S3 after the damage
} else {
if (e && e.alive) {
battle.dealDamage(t, e, { amount: atk * num(tb2.atk_scale), type: 'phys', isSkill: true, tags });
if (e.alive && num(tb2.duration) > 0 && num(tb2.def) < 0) battle.applyStatus(e, 'defDown', { duration: num(tb2.duration), value: -num(tb2.def), source: t });
}
battle.fx('explode', { x: t.x, y: t.y, r: 0.6, id: t.id, consumed: true, kind: 'doroth:clear' });
}
battle.retreat(t, { reason: 'expired', permanent: true });
}
/** 共振装置's kit (`unit` = 多萝西): its rolls at each deployment, the trigger, a placed piece kept for its redeploys. */
function trapKit(unit) {
return {
skill: null,
trait: { noAttack: true },
install(battle, t) {
battle.on('deploy', (ctx) => {
if (ctx.unit !== t) return;
t.mem.dorFired = false;
t.mem.dorWaiting = false;
t.mem.dorDouble = dbl.prob > 0 && dbl.scale !== 1 && battle.rng.chance(dbl.prob);
if (t.mem.dorDouble) battle.fx('mark', { x: t.x, y: t.y, id: t.id, kind: 'doroth:double' });
D.readyAt = battle.time + Math.max(0, num(t.base.respawnTime, 0));
if (t.uid != null) rollExtra(battle, unit);
}, { owner: t });
battle.on('tick', () => {
if (!t.alive || t.mem.dorFired) return;
let hit = null;
for (const e of battle.enemies) {
if (!canTargetEnemy(t, e, GROUND)) continue;
const on = e.hitArea ? bodyOnTile(e, t.tileR, t.tileC) : Math.abs(e.x - t.tileC) <= 0.5 && Math.abs(e.y - t.tileR) <= 0.5;
if (on && (!hit || e.spawnSeq < hit.spawnSeq)) hit = e;
}
if (hit) fire(battle, unit, t, hit);
}, { owner: t });
// a placed piece stays its tile's piece (redeployed by her kit's watch), whatever took it off the field
if (t.uid != null) battle.on('death', (ctx) => { if (ctx.unit === t && !battle.finished) t.removed = false; }, { owner: t, priority: -10 });
},
};
}
const skillSpec = () => ({
kind: 'instant', trigger: 'SP_FULL',
onStart({ skill }) { // 主动效果:立即获得一个陷阱
D.stock = Math.min(D.limit, D.stock + Math.max(1, Math.floor(num(skill.bb?.cnt, 1))));
},
});
return {
skills: { [S1]: skillSpec(), [S2]: skillSpec(), [S3]: skillSpec() },
talents: [
{ install(battle, unit) { // 共振装置: the pieces' kit, the stock, the summons at each deployment, the redeploys
D.limit = limitOf(battle, unit);
unit.trait.doroth = D;
for (const t of battle.allyUnits) {
if (!tokenOf(t) || t.ownerUnit !== unit || t.alive || t.deployed) continue;
if (t.kit) battle.offOwner(t); // a piece set up before her: drop its generic kit's hooks
battle._setupUnit(t, trapKit(unit));
}
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit || ctx.move) return;
D.stock = Math.min(D.limit, stockRefill); // charge_token[refresh]
D.full = false;
if (onDeploy > 0) summon(battle, unit, onDeploy, null, D.limit - standing(battle, unit) - waiting(battle, unit));
}, { owner: unit });
// her traps leave with her (die_to_kill_token); the placed pieces wait for her return
battle.on('death', (ctx) => {
if (ctx.unit !== unit) return;
for (const t of traps(battle, unit)) if (t.alive) battle.retreat(t, { reason: 'expired', permanent: true });
}, { owner: unit });
// a used-up / withdrawn piece comes back on its tile once the deployment conditions hold (see the header)
battle.every(WATCH, () => {
if (!up(unit) || D.stock < 1 || battle.time + 1e-9 < D.readyAt || standing(battle, unit) >= D.limit) return;
const pieces = traps(battle, unit).filter((t) => t.uid != null && !t.alive && !t.removed).sort((a, b) => a.uid - b.uid);
for (const p of pieces) {
if (groundEnemyOn(battle, p.homeR, p.homeC)) continue;
if (battle.redeploy(p, { free: false })) { D.stock--; break; }
}
}, { owner: unit });
// 阻回 while her stock is full (_stopSpWhenTokenIsFull)
toggleBuff(battle, unit, 'talent:doroth:full', () => D.stock >= D.limit, {}, { flags: { noSp: true } });
} },
{ install() {} }, // 梦想家: the stacks come with every trap that goes off (fire → dreamStack)
],
};
},
};