Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-orchd2.js
T
sganggs 4075338517 feedback5 O6: 自选 operator kits for 黑, 焰狐龙梓兰 and 鸿雪 (+ her “打字机”) — every skill, talent, trait and module at every form
- op-shwaz.js 黑: 破甲箭头 (one roll per attack, raised by S1 / S2 / S3; ×atk_scale, then defDown on a landed hit; numbers
  from the base talent + ARC-Y's named change — the data folds ARC-Y's hidden trait talent into talent 0 at stage 1),
  交叉火力 (field aura; ARC-X stage 3 "携带": her player's snipers for the battle), ARC-Y 正前方 ×1.05 undodgeable, S3 3-2 +
  interval +0.4; triggers from the data (S3 SKILL_RANGE — reported).
- op-orchd2.js 焰狐龙梓兰: 三连击 normal attack cut to 33.3 % after DEF (PRTS 特性备注), 强击瓶专家 stacks per round, 翔虫机动
  (respawn −15 / −18 s, ATK +15 / 20 % for 30 s on a redeploy at her mark), S1 刚射 / 刚连射 (charges, refund without a
  target, stun per arrow), S2 飞翔瞪射 (free cast at every deployment, 起飞, three volleys on the 4-4, landing on the 1-3,
  timed by the official skeleton), S3 龙之箭 (3 s charge, piercing arrow at 10 tiles/s, strikes every 0.25 tile, push).
- op-bgsnow.js 鸿雪: S1 toggle + proc, S2 点题 (立即, 普通 + 2 × 持续伤害), S3 3-1 with 正前方3格 scale, ARC-Y as 黑's; her
  placed “打字机” pieces get their kit at her install (25 s life, 40 s redeploy × S2's 65 / 60 %, 5 DP; the skill copies;
  弱点速记 DEF cut 18 / 23 %, ARC-X stage 3 28 / 33 % with 八格).
- kits/index.js OPERATOR_KIT_FILES: one line per file. Tests: test/content/op_shwaz / op_orchd2 / op_bgsnow (23).
- Golden: only the diy family moved — diy-003 … diy-006 now hold the 66 new 自选 configurations (黑, 焰狐龙梓兰, 鸿雪) in
  place of the prototype picks, diy-007 … diy-012 are new (the prototype battles moved down); every other family is
  byte-identical. test/golden/README.md: 12 diy battles, 61 of 156.
2026-10-06 00:10:34 +08:00

307 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// server/sim/content/kits/ops/op-orchd2.js — 焰狐龙梓兰 (char_1048_orchd2) 自选 operator kit: 6★ 重射手 (狙击), an owned-6★
// pick of the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and her module (ARC-X 梓兰特制箭靶) at every
// form. Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_1048_orchd2): normal = E2 Lv1, skills at rank 4, no module; elite = E2 Lv60, rank 7,
// ARC-X 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 焰狐龙梓兰 (特性备注,
// the 备注 of both talents and all three skills, quoted where used); the official skeleton of char_1048_orchd2 (front .skel:
// the Skill_X animations and their OnAttack events) for the moments the skills act — PRTS 飞翔瞪射 备注 "三轮攻击的时间模式
// 均为跟随动画", 龙之箭 备注 (frame counts of the first / later casts). NB she came with a 联动寻访 (PRTS 获得方式; Monster
// Hunter); her team is 预备行动组 (reserve6), not one of build-data DIY_EXCLUDED_TEAMS, so the data offers her — reported.
// - Trait (重射手) "高精度的近距离射击": ranged physical arrows, 3-6, can hit air units (PRTS 分支特性信息 重射手 "可对空"),
// blocks 1, ground enemies target her except while airborne (S2). PRTS 特性备注: her normal attack is a 三连击 of 100 %
// ATK each, and every normal-attack damage is cut to 33.3 % after DEF / RES (hidden talent attack@damage_scale 0.333):
// profile hits 3 (the count is in no blackboard: NORMAL_HITS) and dmg.mul × damage_scale on her non-skill attack damage.
// - T1 强击瓶专家 "部署后首次开启技能时,接下来50次攻击的攻击力提升至115%": the first cast of each deployment gives
// power_attack_count stacks; each attack ROUND (备注: a normal attack's three hits, a skill volley, 飞翔瞪射's landing, one
// 龙之箭) spends one and all of that round's damage is ×power_attack_scale (an ATK-scale multiplier, 备注). A round spends
// it at its first hit, a 龙之箭 when it is shot [ASSUMED: the 备注 spends it "于弹道脱手前" — only a round whose arrows all
// fizzle differs]. The first cast also uses the longer Skill_X_Begin_First clip (备注): its waits below are the longer ones.
// - T2 翔虫机动 "再部署时间-15秒且不提高部署费用;部署至上次部署位置周围时,30秒内攻击力+15%…可以部署在近战位": the hidden
// talent's respawn_time (−15; ARC-X stage 3 −18) shortens her redeploy time; when she leaves the field a mark stays on her
// tile (备注) and a later deployment inside its range (bbStr ignore_build_type_target_range: x-1, ARC-X stage 3 x-2, from
// range_table) gives ATK +atk for atk_duration s — the mode's redeploy brings her back on that very tile, so every
// redeploy does. "不提高部署费用" and "可以部署在近战位": nothing in battle (the mode never raises a redeploy cost; the
// board places her).
// - Module ARC-X “梓兰特制箭靶”: "再部署时间减少" (attribute respawn_time −25, in the stats) + ATK / DEF; stage 3 upgrades T2.
// - S1 刚射 (MANUAL, charges 2 / 3, data DEFAULT): the cast's attack is 4 arrows of atk_scale_1 × ATK (the text's counts:
// "发射4支", "发射5支" — parsed); with a charge left it spends one more (备注: "消耗2次充能所需的技力") for 刚连射 1.167 s
// later (Skill_1_End_2: OnAttack at 0.667 / 1.833 s): 5 arrows of atk_scale_2 × ATK, each stun_prob to stun `stun` s;
// no target then ⇒ the skill stops and the charge comes back (备注). No normal attack until the end clip is over.
// "充能至最大层数时自动释放一次": the DEFAULT rule casts it as soon as she is about to attack — the 3 s operation cooldown
// never binds at max charges (a refill takes ≥ 10 s), so no extra rule.
// - S2 飞翔瞪射 (MANUAL, charges 2 / 3, data SKILL_RANGE on its 4-4): 起飞 (liftoff + blockFly, as 蒂比: no ground enemy
// blocked or attacking her) for 4.2 s, no normal attack (备注 "自身丢失全部视野"), three volleys at every target of the 4-4
// (air units too) of 3 / 4 / 5 arrows × attack@atk_scale_loop each, then she lands with attack@atk_scale_end × ATK on every
// enemy of the 1-3 (备注 "降落时伤害范围为1-3"; air units too [ASSUMED]). Timeline from the skeleton: volleys 0.167 s into
// each 0.833 s Skill_2_Loop after the 0.667 s Skill_2_Begin (first cast: Begin_First 1.5 s, the skill 0.833 s longer),
// the landing 0.233 s into Skill_2_End. "部署后立即释放一次": every deployment casts it at once, free (备注 "无视技力…不会消耗
// 技力").
// - S3 龙之箭 (MANUAL, charges 1 / 2, data DEFAULT): after 3 s (Skill_3_Begin 1.5 s + wait_duration; first cast
// Begin_First 2.167 s, 备注 ≈110 frames) and 0.1 s more (Skill_3_End OnAttack) a piercing arrow flies straight ahead from
// her at 10 tiles/s (备注), collision radius 0.5 (备注), until it leaves the field: every dist_interval tiles it deals
// atk_scale × ATK physical, then atk_scale_magic × ATK arts (备注 order) to every enemy around it (air units too
// [ASSUMED]), and it pushes each enemy it touches with 力度 `force` (中等力度) along its flight, once per
// knockback_duration s per enemy (备注). ATK is read when it is shot [ASSUMED]. Her range does not change (备注 "不影响
// 实际视野"). No normal attack until Skill_3_End is over (1.233 s).
// [ASSUMED] for the three skills: a stun / freeze does not cancel the scheduled volleys, bursts and arrow (no animation
// model); the wind-up of 刚射 before its first shot (Skill_1_Begin + 0.667 s) is not modelled, as no attack's wind-up is.
import { num, up, skillRec } from '../shared/tier1.js';
import { performAttack, acquireTargets } from '../../../ai.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { dirVec } from '../../../dir.js';
import { hasHp } from '../../../damage.js';
const S1 = 'skchr_orchd2_1';
const S2 = 'skchr_orchd2_2';
const S3 = 'skchr_orchd2_3';
/** PRTS 特性备注 "普通攻击为三连击": the hits of her normal attack (no blackboard key). */
export const NORMAL_HITS = 3;
/**
* The official skeleton of char_1048_orchd2 (front .skel), seconds: clip lengths and their OnAttack events.
* Skill_1: Begin 0.1 (First 1.0), End_1 1.5 (shot 0.667), End_2 2.667 (shots 0.667, 1.833); Skill_2: Begin 0.667 (First
* 1.5), three Loop clips 0.833 (shot 0.167), End 1.0 (landing 0.233); Skill_3: Begin 1.5 (First 2.167), End 1.233 (shot 0.1).
*/
export const CLIP = Object.freeze({
s1Shot: 0.667, s1BurstShot: 1.833, s1End1: 1.5, s1End2: 2.667,
s2Begin: 0.667, s2BeginFirst: 1.5, s2Loop: 0.833, s2LoopShot: 0.167, s2EndShot: 0.233,
s3Begin: 1.5, s3BeginFirst: 2.167, s3EndShot: 0.1, s3End: 1.233,
});
/** 龙之箭 (PRTS 备注): flight speed (tiles/s), collision radius (tiles), lifetime (s). */
export const ARROW = Object.freeze({ speed: 10, radius: 0.5, life: 30 });
/** 1-3 (range_table): 飞翔瞪射's landing area (PRTS 备注). */
const GRID_1_3 = Object.freeze([[1, 1], [0, 0], [0, 1], [-1, 1]]);
/** 翔虫机动's mark ranges (range_table): x-1 = |dr| + |dc| ≤ 2; x-2 = the 5 × 5 square without its corners. */
const MARK_RANGE = Object.freeze({
'x-1': (dr, dc) => Math.abs(dr) + Math.abs(dc) <= 2,
'x-2': (dr, dc) => Math.abs(dr) <= 2 && Math.abs(dc) <= 2 && !(Math.abs(dr) === 2 && Math.abs(dc) === 2),
});
const TAG_BURST = 'orchd2:burst';
const TAG_ARROW = 'orchd2:arrow';
const LOCK = 'orchd2:lock';
const LIFT = 'orchd2:liftoff';
const WIREBUG = 'talent:orchd2:wirebug';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/** A talent of the record by a blackboard key it carries (index-independent: hidden talents included). */
const talentWith = (chess, key) => (chess?.talents ?? []).find((t) => t && t.bb && t.bb[key] != null) ?? null;
/** Numbers of a skill text ("发射4支…发射5支", "分别射出3、4、5支"), or `fallback`. */
function textCounts(desc, re, fallback) {
const m = re.exec(String(desc ?? ''));
if (!m) return fallback;
const out = m[1].split(/[、,,]/).map((s) => Number.parseInt(s, 10)).filter((n) => n > 0);
return out.length ? out : fallback;
}
/** No normal attack for `s` seconds (the rest of a skill clip). */
function lock(battle, unit, s) {
if (s > 0) battle.addBuff(unit, { key: LOCK, duration: s, flags: { disarm: true }, tags: ['skill'] });
}
export default {
char_1048_orchd2: (bb, chess) => {
const s1 = skillRec(chess, S1), s2 = skillRec(chess, S2), s3 = skillRec(chess, S3);
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const shots = [...String(s1?.desc ?? '').matchAll(/发射(\d+)支/g)].map((m) => Number(m[1]));
const arrows1 = shots[0] > 0 ? shots[0] : 4, arrows2 = shots[1] > 0 ? shots[1] : 5;
const volleys = textCounts(s2?.desc, /射出([\d、]+)支/, [3, 4, 5]);
const power = talentWith(chess, 'power_attack_count')?.bb ?? {};
const wire = talentWith(chess, 'atk_duration') ?? { bb: {}, bbStr: {} };
const respawn = num(talentWith(chess, 'respawn_time')?.bb?.respawn_time);
const normalCut = num(talentWith(chess, 'attack@damage_scale')?.bb?.['attack@damage_scale'], 1);
const markIn = MARK_RANGE[wire.bbStr?.ignore_build_type_target_range] ?? MARK_RANGE['x-1'];
/** The first cast of this deployment: T1's stacks and the longer first clip. */
const firstCast = (unit) => {
if (unit.mem.orchdCast) return false;
unit.mem.orchdCast = true;
unit.mem.orchdPower = Math.max(0, Math.floor(num(power.power_attack_count)));
return true;
};
/** T1: one stack for a round ⇒ its multiplier (1 without stacks). */
const takePower = (unit) => {
if (!(unit.mem.orchdPower > 0)) return 1;
unit.mem.orchdPower--;
return num(power.power_attack_scale, 1);
};
/** A skill attack profile: her own (targeting, projectile) with the skill's scale and hit count. */
const skillProfile = (unit, atkScale, hits, extra = {}) => ({ ...unit.profile, isSkill: true, atkScale, hits, tags: [], ...extra });
/** Targetable enemies of a facing-RIGHT grid around her (air units too: she can hit them). */
const inGrid = (battle, unit, grid) => battle.enemiesInKeys(absoluteRangeKeys(grid, unit.tileR, unit.tileC, unit.dir, 0), unit, { canHitFly: true });
/** The skill still runs this very activation (scheduled steps of a cast). */
const running = (unit, seq, act) => up(unit) && unit.deploySeq === seq && !!unit.skill?.active && unit.skill.activations === act;
/** 龙之箭: shot from her tile straight ahead (see the header). */
const shootArrow = (battle, unit) => {
const mul = takePower(unit);
const atk = unit.s.atk;
const [fr, fc] = dirVec(unit.dir);
const ux = fc, uy = fr; // x = column, y = row
const every = Math.max(0.05, num(b3.dist_interval, 0.25));
const maxDist = num(b3.max_dist, 99);
const force = num(b3.force, 1), cd = num(b3.knockback_duration, 1);
const phys = atk * num(b3.atk_scale) * mul, arts = atk * num(b3.atk_scale_magic) * mul;
const pushReady = new Map();
let x = unit.x, y = unit.y, acc = 0, flown = 0, age = 0;
battle.fx('dragonArrow', { x, y, id: unit.id, dx: ux, dy: uy, speed: ARROW.speed });
const strike = () => {
for (const e of battle.foesInRadius(x, y, ARROW.radius)) {
if (phys > 0) battle.dealDamage(unit, e, { amount: phys, type: 'phys', isSkill: true, tags: ['skill', TAG_ARROW] });
if (arts > 0 && e.alive) battle.dealDamage(unit, e, { amount: arts, type: 'arts', isSkill: true, tags: ['skill', TAG_ARROW] });
}
};
const h = battle.on('tick', ({ dt }) => {
const step = ARROW.speed * dt;
let moved = 0;
while (moved < step - 1e-9) {
const s = Math.min(step - moved, every - acc);
x += ux * s; y += uy * s; moved += s; acc += s; flown += s;
if (acc >= every - 1e-9) { acc = 0; strike(); }
}
// the collision (push) is checked once per frame (备注), each enemy at most once per knockback_duration
for (const e of battle.foesInRadius(x, y, ARROW.radius)) {
if ((pushReady.get(e) ?? -Infinity) > battle.time + 1e-9) continue;
pushReady.set(e, battle.time + cd);
battle.push(e, force, { from: { x, y }, dir: { x: ux, y: uy }, fixed: true });
}
age += dt;
if (age >= ARROW.life || flown >= maxDist || !battle.grid.inRect(Math.round(y), Math.round(x))) battle.off(h);
}, { owner: unit });
};
return {
trait: { hits: NORMAL_HITS },
skills: {
[S1]: {
kind: num(s1?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
attack: { atkScale: num(b1.atk_scale_1, 1), hits: arrows1 },
onStart({ battle, unit, skill }) {
firstCast(unit);
const burst = skill.charges >= 1;
if (burst) skill.charges -= 1; // "若还有充能则额外消耗1层施展刚连射"
lock(battle, unit, (burst ? CLIP.s1End2 : CLIP.s1End1) - CLIP.s1Shot);
battle.fx('quadShot', { x: unit.x, y: unit.y, id: unit.id });
if (!burst) return;
const seq = unit.deploySeq;
battle.after(CLIP.s1BurstShot - CLIP.s1Shot, () => {
if (!up(unit) || unit.deploySeq !== seq) return;
const prof = skillProfile(unit, num(b1.atk_scale_2, 1), arrows2, { tags: [TAG_BURST], maxTargets: 1, allInRange: false });
const targets = acquireTargets(battle, unit, prof).slice(0, 1);
if (!targets.length) { // "找不到目标,将立刻终止技能并返还与1次充能等额的技力"
unit.skill?.addCharge(1);
battle.removeBuff(unit, LOCK);
return;
}
performAttack(battle, unit, prof, targets);
}, { owner: unit });
},
},
[S2]: {
kind: 'duration',
attack: { noAttack: true },
onStart({ battle, unit, skill }) {
const lead = firstCast(unit) ? CLIP.s2BeginFirst : CLIP.s2Begin;
if (lead > CLIP.s2Begin) skill.extend(lead - CLIP.s2Begin);
battle.addBuff(unit, { key: LIFT, flags: { liftoff: true, blockFly: true }, tags: ['skill'] });
battle.releaseBlocked(unit);
battle.fx('takeoff', { x: unit.x, y: unit.y, id: unit.id });
const seq = unit.deploySeq, act = skill.activations;
const grid = s2?.rangeGrid ?? [];
volleys.forEach((n, i) => battle.after(lead + i * CLIP.s2Loop + CLIP.s2LoopShot, () => {
if (!running(unit, seq, act)) return;
const targets = inGrid(battle, unit, grid);
if (targets.length) performAttack(battle, unit, skillProfile(unit, num(b2['attack@atk_scale_loop'], 1), n, { allInRange: true }), targets);
}, { owner: unit }));
battle.after(lead + volleys.length * CLIP.s2Loop + CLIP.s2EndShot, () => {
if (!running(unit, seq, act)) return;
battle.removeBuff(unit, LIFT);
battle.releaseBlocked(unit);
const targets = inGrid(battle, unit, GRID_1_3);
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1, id: unit.id, skill: 'orchd2:landing' });
if (targets.length) performAttack(battle, unit, skillProfile(unit, num(b2['attack@atk_scale_end'], 1), 1, { allInRange: true, projectile: 'none', attack: 'melee' }), targets);
}, { owner: unit });
},
onEnd({ battle, unit }) {
if (unit.findBuff(LIFT)) { battle.removeBuff(unit, LIFT); battle.releaseBlocked(unit); }
},
},
[S3]: {
kind: num(s3?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
attack: { noAttack: true }, // the clip: the cast waits (pending) with no normal attack until it ends below
onStart({ battle, unit, skill }) {
const lead = (firstCast(unit) ? CLIP.s3BeginFirst : CLIP.s3Begin) + num(b3.wait_duration, 1.5);
const seq = unit.deploySeq, act = skill.activations;
battle.after(lead + CLIP.s3EndShot, () => { if (running(unit, seq, act)) shootArrow(battle, unit); }, { owner: unit });
battle.after(lead + CLIP.s3End, () => { if (running(unit, seq, act)) skill.end('clip'); }, { owner: unit });
},
},
},
talents: [
{ install(battle, unit) { // 强击瓶专家: a round's first hit spends one stack; every hit of that round ×scale
const rounds = new Map();
battle.on('hit', (ctx) => {
const d = ctx.dmg;
if (ctx.source !== unit || !ctx.target || ctx.target.side !== 'enemy' || !d.isAttack || !d.attackId || d.type === 'element') return;
let mul = rounds.get(d.attackId);
if (mul === undefined) {
mul = takePower(unit);
rounds.set(d.attackId, mul);
if (rounds.size > 16) rounds.delete(rounds.keys().next().value);
}
if (mul !== 1) d.amount *= mul;
}, { owner: unit });
} },
{ install(battle, unit) { // 翔虫机动: shorter redeploy; the mark on her last tile ⇒ ATK +atk for atk_duration s
if (respawn && !unit.mem.orchdRespawn) {
unit.mem.orchdRespawn = true;
unit.base.respawnTime = Math.max(0, unit.base.respawnTime + respawn);
}
const a = num(wire.bb?.atk), dur = num(wire.bb?.atk_duration);
battle.on('death', (ctx) => {
if (ctx.unit === unit) unit.mem.orchdMark = [unit.tileR, unit.tileC];
}, { owner: unit });
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
const mark = unit.mem.orchdMark;
unit.mem.orchdMark = null; // "持续存在直至下次焰狐龙梓兰部署"
if (!mark || !(a > 0) || !(dur > 0) || !markIn(unit.tileR - mark[0], unit.tileC - mark[1])) return;
battle.addBuff(unit, { key: WIREBUG, duration: dur, mods: { atkPct: a }, visible: true, tags: ['talent'] });
battle.fx('buff', { x: unit.x, y: unit.y, id: unit.id, kind: 'wirebug' });
}, { owner: unit });
} },
],
install(battle, unit) {
// every deployment: T1 and the first-clip rule start over; S2 "部署后立即释放一次" (free, after the deployment
// is done — a 联防 SP carry is applied first)
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
unit.mem.orchdCast = false;
unit.mem.orchdPower = 0;
if (unit.skill?.id !== S2) return;
const seq = unit.deploySeq;
battle.after(0, () => {
if (up(unit) && unit.deploySeq === seq && unit.skill && !unit.skill.active) unit.skill.activate('deploy', { free: true });
}, { owner: unit });
}, { owner: unit, priority: 10 });
// 特性备注: every normal-attack damage ×damage_scale after DEF / RES
if (normalCut !== 1) {
battle.on('hit', (ctx) => {
const d = ctx.dmg;
if (ctx.source === unit && d.isAttack && !d.isSkill && d.type !== 'element') d.mul *= normalCut;
}, { owner: unit });
}
// 刚连射: every arrow stun_prob to stun the target hit
if (unit.skill?.id === S1) {
const p = num(b1.stun_prob), s = num(b1.stun);
battle.on('damaged', (ctx) => {
if (ctx.source !== unit || !ctx.dmg?.tags?.includes(TAG_BURST) || !hasHp(ctx.target) || ctx.target.side !== 'enemy') return;
if (p > 0 && s > 0 && battle.rng.chance(p)) battle.applyStatus(ctx.target, 'stun', { duration: s, source: unit });
}, { owner: unit });
}
},
};
},
};