Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-amgoat.js
T
sganggs 806a047717 feedback6 R21P: every operator fights at full potential (潜能 6) — the owner's decision of 2026-10-07 (GitHub #252, PR #255; no host slider): tools/build-data.mjs OPERATOR_POTENTIAL adds every character_table potentialRanks attribute step (部署费用 / 再部署时间 / ATK / HP / DEF / RES / ASPD, all ADDITION, before rounding) to every chess (normal and elite), 补位 and 自选 unit form and picks talent / trait / module-talent candidates up to requiredPotentialRank 5 (「天赋效果增强」); a summon picks its talent candidates at its owner's potential (their requiredPotentialRank mirrors the owner's — PRTS 凯尔希: Mon3tr 3.5 s / 1400) and takes none of the owner's attribute steps; the 原型干员 stand-ins have no potential ranks (unchanged). 刺玫 413 / 17 → 435 / 15 as in the tournament video, 砾 18 → 16 s, 至简 80 → 70 s. data/chess.json (256 records), backups.json (the 71 owned picks, 8 summons), tokens.json (5 summons) and the data translations en / ja / ko / zh-TW rebuilt — only potential-driven fields (JSON-compared); the 干员调配 局内数值 and detail cards read the records. op-wang.js reads her stock cap 「最多拥有8(+1)枚」 through the potential step (the pattern fell back to 7: she sat at the cap from the start). Docs: DATA §2 / §18 / tokens, PLAYING (detail card), research 03, kits README, shared/diy.js / standIn.js, the 71 自选 kit headers and the kit comments that quoted potential-0 numbers. Tests: test/full-potential.test.js (new, fails on the parent); data.test.js re-derivation adds the potential steps (+ respawnTime); every test that hard-coded a potential-0 number carries the full-potential one (from the data and the test's own formula; no check loosened); golden: moves almost everywhere — roster 49/49, bonds 46/46, fields 22/22, matches 18/18, diy 133/138 (each battle parts on a stat: roster-001 / roster-020 at tick 1 on 角峰's HP (生命上限+250), bond-arcaneShip-high at 4.73 s and diy-001 at 3.70 s on an enemy's HP from a higher ATK; diy-110 / 111 also by 望's cap; the matches through those battles); unchanged: standins 10/10 and diy-135…138 (no potential ranks), diy-012 (twelve 夜莺 never hit, never redeployed)
2026-10-07 18:35:39 +08:00

194 lines
13 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-amgoat.js — 艾雅法拉 (char_180_amgoat) 自选 operator kit: 6★ 中坚术师 (术师), an owned-6★
// pick of the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules at every form.
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_180_amgoat, 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). Full potential (the owner's decision of 2026-10-07).
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json; every module part is
// valid outside 集成战略 — no `validInGameTag`) and PRTS 艾雅法拉 (S1 备注 "每次部署艾雅法拉时,重新计算该技能的使用次数";
// S2 备注 "爆炸范围为半径1.5", "技能实际效果为先削减主攻击目标的法术抗性,再对爆炸范围内所有敌人造成½倍率的伤害(包括主攻击目标),
// 最后对主攻击目标额外造成一次½倍率的伤害", 法术抗性 "{{**|80%|-20%}}" = a multiplier; S3 "攻击间隔大幅度缩短(-1.1)";
// 乱火's hidden 备注 "在最小值与最大值之间随机生成一个浮点数后向下取整").
// - Trait (中坚术师) "攻击造成法术伤害": the profession default (ranged arts, hits air, block 1). Module CCR-X “错过的声音”
// adds "无视目标10点法术抗性" (trait bb magic_resist_penetrate_fixed: a permanent resIgnoreFlat), CCR-Y “宠物大赛第一名”
// "普通攻击命中精英或领袖敌人时获得1点技力" (trait bb sp: per hit of one of her attacks on an elite / leader — the S2
// explosion's splash and its second ½ are not further 命中 [ASSUMED]; a running timed skill takes no SP, as always).
// - T1 炎息 "在场时,所有友方【术师】职业干员的攻击力+14%" (full potential: +16%): an aura on every 【术师】 operator of the
// field (her included) while she is deployed (installAura, the kits' convention for field-wide talents; several sources
// keep the strongest). CCR-X stage 2+ turns it into "携带时…+22%" (full potential: +24%; the module talent's bb): the
// same aura whether she is on the field or not — she is carried while she belongs to the battle, knocked out or waiting
// to redeploy included.
// - T2 乱火 "部署后立即随机获得7~15点技力" (full potential: 10~19; bb sp_min 10, sp_max 20): floor(uniform [sp_min, sp_max))
// SP at every deployment (PRTS hidden 备注). CCR-Y stage 3 adds "并随机提升6~15的攻击速度" (attack_speed_min / _max, the
// same draw) for that deployment [ASSUMED: no duration given — until she leaves the field], and with `factor`
// "部署后范围内存在精英或领袖敌人时,获得的技力和提升的攻击速度取最大值": an elite or leader enemy in her attack range at
// that deployment gives the text's maxima, 19 SP / 15 ASPD — the blackboard maxima are the exclusive bounds of the
// draw (stage 2's attack_speed_max 1 with no ASPD in its text says so) [ASSUMED: 取最大值 = the largest value the draw
// yields; checked at the deployment instant — at the battle start no enemy is on the field yet].
// - S1 二重咏唱 (MANUAL, 25 s, data DEFAULT): ASPD +[a].attack_speed; from the second cast of a deployment on, ASPD
// +[b].attack_speed and ATK +[b].atk (PRTS 备注: the count restarts at every deployment).
// - S2 点燃 (AUTO, 可充能 2, data DEFAULT): the next attack, on impact — the main target's RES ×(1 + magic_resistance) for
// `duration` s first, then an explosion of radius 1.5 around it (a 中点判定 splash) dealing atk_scale × ATK arts to
// every enemy in it, the main target included, each of the others then getting the same RES cut, and last
// atk_scale_2 × ATK arts to the main target again (the PRTS 备注's order; it names the main target's cut only as
// coming first, so the others' cut follows their hit [ASSUMED]; the cut is "同名效果取最高" through applyStrongest).
// The explosion reaches air units and skips enemies an area effect cannot select (foesInRadius); like the engine's
// profession splash it can be dodged.
// - S3 火山 (MANUAL, 15 s): ATK +atk, attack interval base_attack_time (−1.1 s flat on her 1.6 s), the attack range x-3
// while it runs, each attack throws lava at attack@max_target enemies of that range drawn at random (all of them when
// fewer) — one projectile each, no splash [ASSUMED: the text names none]. Trigger: the data's SKILL_RANGE on
// x-3 — its text says "攻击范围增大", which tools/build-data.mjs ATTACK_RANGE_CHANGE does not list, so the owner's
// ACTIVE_RANGE rule did not reach it (reported; the kit keeps the data rule).
// - No summon (the forms list no token).
import { num, talentBb, traitBb, skillRec, statBuff, installAura, batMod, giveSp, onHitBy } from '../shared/tier1.js';
import { hasHp } from '../../../damage.js';
import { canTargetEnemy } from '../../../targeting.js';
import { bodyInKeys } from '../../../body.js';
const S1 = 'skchr_amgoat_1';
const S2 = 'skchr_amgoat_2';
const S3 = 'skchr_amgoat_3';
/** S2 点燃: the explosion radius (PRTS 备注 "爆炸范围为半径1.5"; no blackboard key). */
const IGNITE_RADIUS = 1.5;
/** S3 火山's 技能范围 x-3 when the data carries no grid (range_table x-3: the tiles within 3 steps). */
const X3 = Object.freeze([[3, 0], [2, -1], [2, 0], [2, 1], [1, -2], [1, -1], [1, 0], [1, 1], [1, 2], [0, -3], [0, -2], [0, -1],
[0, 0], [0, 1], [0, 2], [0, 3], [-1, -2], [-1, -1], [-1, 0], [-1, 1], [-1, 2], [-2, -1], [-2, 0], [-2, 1], [-3, 0]]);
/** Damage of the S2 explosion and its second ½ (not a further 命中 of the attack: CCR-Y counts the first). */
const IGNITE_TAG = 'amgoat:ignite';
const AURA_KEY = 'talent:amgoat:casters';
const AURA_EVERY = 0.5;
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/** A 【术师】 operator (summons are never operators). */
const isCaster = (a) => !!a && a.kind === 'op' && a.def?.profession === 'CASTER';
/** An elite or leader enemy (精英或领袖敌人). */
const isEliteEnemy = (e) => !!e && e.side === 'enemy' && (e.isBoss || e.def?.rank === 'ELITE' || e.def?.rank === 'BOSS');
const tagged = (d, tag) => !!(d && d.tags && d.tags.includes(tag));
/** A module trait that works 在集成战略中 only (ISW modules): nothing of it applies in this mode. */
const roguelikeOnly = (chess) => /集成战略/.test(String(chess?.trait?.moduleDesc ?? ''));
/** Named talent `i` of the record (its desc tells the module stage's wording). */
const talentRec = (chess, i) => (chess?.talents ?? []).filter((t) => t && t.index !== -1)[i] ?? null;
/** "携带时": the aura of installAura, held while she belongs to the battle — on the field or not. */
function carriedAura(battle, unit, { key, select, mods, value }) {
battle.every(AURA_EVERY, () => {
for (const a of battle.alliesFor(unit)) {
if (!select(a)) continue;
const cur = a.findBuff(key);
if (cur && cur.source !== unit && (cur.data?.v ?? 0) > value && cur.timeLeft > 0.05) continue;
battle.addBuff(a, { key, duration: AURA_EVERY + 0.1, mods, source: unit, data: { v: value }, tags: ['aura'] });
}
}, { owner: unit, immediate: true });
}
export default {
char_180_amgoat: (bb, chess) => {
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
const carried = /携带时/.test(String(talentRec(chess, 0)?.desc ?? ''));
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s2 = skillRec(chess, S2), s3 = skillRec(chess, S3);
const mr = num(b2.magic_resistance), resDur = num(b2.duration);
const resCut = (battle, unit, e) => {
if (e && e.alive && mr) battle.applyStrongest(e, 'amgoat:ignite', { duration: resDur, value: mr, mods: (v) => ({ resMul: Math.max(0, 1 + v) }), source: unit });
};
const maxLava = Math.max(1, Math.floor(num(b3['attack@max_target'], 1)));
return {
skills: {
[S1]: {
kind: 'duration',
onStart({ battle, unit }) {
// the casts of this deployment (PRTS 备注: "每次部署…重新计算该技能的使用次数")
const n = unit.mem.amgoatDuetSeq === unit.deploySeq ? num(unit.mem.amgoatDuetN) + 1 : 1;
unit.mem.amgoatDuetSeq = unit.deploySeq;
unit.mem.amgoatDuetN = n;
const mods = n >= 2
? { aspd: num(b1['amgoat_s_1[b].attack_speed']), atkPct: num(b1['amgoat_s_1[b].atk']) }
: { aspd: num(b1['amgoat_s_1[a].attack_speed']) };
battle.addBuff(unit, { key: 'skill:amgoat:duet', mods, tags: ['skill'] });
},
onEnd({ battle, unit }) { battle.removeBuff(unit, 'skill:amgoat:duet'); },
},
[S2]: {
kind: num(s2?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
attack: {
// the engine's hit on the main target is its share of the explosion (½); the RES cut came first (install)
atkScale: num(b2.atk_scale, 1),
onEachHit({ battle, unit, target, kind }) {
if (kind !== 'main' || !target) return;
const atk = unit.s.atk, x = target.x, y = target.y;
for (const e of battle.foesInRadius(x, y, IGNITE_RADIUS, true)) {
if (e === target) continue;
battle.dealDamage(unit, e, { amount: atk * num(b2.atk_scale, 1), type: 'arts', isAttack: true, isSplash: true, isSkill: true, tags: [IGNITE_TAG] });
resCut(battle, unit, e);
}
if (hasHp(target)) battle.dealDamage(unit, target, { amount: atk * num(b2.atk_scale_2, num(b2.atk_scale, 1)), type: 'arts', isAttack: true, isSkill: true, tags: [IGNITE_TAG] });
battle.fx('explosion', { x, y, id: unit.id, r: IGNITE_RADIUS, dmgType: 'arts' });
},
},
},
[S3]: {
kind: 'duration',
mods: { atkPct: num(b3.atk), batPct: batMod(b3.base_attack_time, chess) },
// every enemy of x-3 is a candidate; install draws the lava targets among them
targeting: { rangeGrid: s3?.rangeGrid ?? X3, allInRange: true },
},
},
talents: [
{ install(battle, unit) { // 炎息: the field's 【术师】 ATK +atk — while she is deployed, or (CCR-X stage 2+) carried
const a = num(t0.atk);
if (!(a > 0)) return;
const opts = { key: AURA_KEY, value: a, select: isCaster, mods: { atkPct: a } };
if (carried) carriedAura(battle, unit, opts);
else installAura(battle, unit, { ...opts, interval: AURA_EVERY });
} },
{ install(battle, unit) { // 乱火: SP (and with CCR-Y stage 3 ASPD) at every deployment; the maxima with an elite in range
const spLo = num(t1.sp_min), spHi = num(t1.sp_max), asLo = num(t1.attack_speed_min), asHi = num(t1.attack_speed_max);
const maxRule = num(t1.factor) > 0;
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit || !unit.alive) return;
// (a scan of the enemies: the tile index of enemiesInKeys is the last tick's)
const top = maxRule && battle.enemies.some((e) => isEliteEnemy(e) && canTargetEnemy(unit, e, unit.profile) && bodyInKeys(e, unit.rangeKeySet));
// floor of a uniform draw in [lo, hi); "取最大值" = the largest value the draw yields (hi − 1 for integer bounds)
const draw = (lo, hi) => (hi > lo ? (top ? Math.ceil(hi) - 1 : Math.floor(battle.rng.range(lo, hi))) : 0);
const sp = draw(spLo, spHi);
if (sp > 0) giveSp(unit, sp);
const as = draw(asLo, asHi);
if (as > 0) battle.addBuff(unit, { key: 'talent:amgoat:wildfire', mods: { aspd: as }, tags: ['talent'] });
}, { owner: unit });
} },
],
install(battle, unit) {
// module traits (none on the normal form): CCR-X 无视目标10点法术抗性, CCR-Y SP per hit on an elite / leader
if (!roguelikeOnly(chess)) {
const tb = traitBb(chess);
const pen = num(tb.magic_resist_penetrate_fixed);
if (pen > 0) statBuff(battle, unit, 'trait:amgoat:resIgnore', { resIgnoreFlat: pen });
const sp = num(tb.sp);
if (sp > 0) {
battle.on('damaged', (ctx) => {
const d = ctx.dmg;
if (ctx.source !== unit || !d || !d.isAttack || d.isSplash || tagged(d, IGNITE_TAG) || !isEliteEnemy(ctx.target)) return;
giveSp(unit, sp, 'trait');
}, { owner: unit });
}
}
const sid = unit.skill?.id;
if (sid === S2) {
// 点燃: "先削减主攻击目标的法术抗性" — the main target's RES cut lands before the first damage of the attack
onHitBy(battle, unit, (ctx) => {
const d = ctx.dmg;
if (d.isAttack && d.isSkill && !d.isSplash && !tagged(d, IGNITE_TAG)) resCut(battle, unit, ctx.target);
}, 50);
} else if (sid === S3) {
// 火山: "随机对攻击范围内至多N个敌人发射熔岩" — a random draw among every enemy of the range
battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit || !unit.skill?.active || ctx.targets.length <= maxLava) return;
ctx.targets = battle.rng.shuffle(ctx.targets.slice()).slice(0, maxLava);
}, { owner: unit });
}
},
};
},
};