Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-ling.js
T
sganggs 99d8807328 feedback5 WE2: 自选 summon hand counts include the tokens' own deploy talents (#21)
tools/build-data.mjs tokenTalentDeckBonus adds a summon's talent max_deploy_count /
max_deck_stack_cnt (the hidden "TOKEN数" talent; a module token part of the same talent
replaces it, matched by prefabKey) to its variant deployLimit / deckStack. Regenerated
offline: only data/backups.json tokens of 麦哲伦 (drones), 令 (souls), 白铁 (devices) and
夜莺 (幻影) changed — deployLimit 3 / 4 with SUM-Y stage 3, 2, 3; deckStack 5 (8), 3 (4).
The hand count (player/diy.js placeableTokens) now matches the official one for every
自选 summoner (PRTS 卫戍协议/帮助 "根据召唤物部署数量上限", the owners' talents, PRTS 幻影 备注).
The summoner kits read deckStack / deployLimit as they are (same decks as before).
Golden: no scenario moved (the diy family places no summon pieces).
2026-10-06 15:01:46 +08:00

247 lines
16 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-ling.js — 令 (char_2023_ling) 自选 operator kit: 6★ 召唤师 (辅助), an owned-6★ pick of the
// tier-5 and tier-6 自选 slots; every skill, both talents, the trait and her module (SUM-Y 诗短梦长) at every form, and the
// kits of her summons “清平” / “逍遥” / “弦惊” (token_10020_ling_soul1 / 2 / 3). Kit contract and the 自选 rules: ../README.md
// ("How to add an operator (自选)"); the summon deck: ../shared/summoner.js.
//
// Forms (data/backups.json units.char_2023_ling): normal = E2 Lv1, skills at rank 4, no module; elite = E2 Lv60, rank 7, the
// 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 / the tokens' character_table rows (zh_CN, as built into
// backups.json); PRTS 令 (S2 备注 "令与其召唤物的攻击范围内没有可选目标时,该技能也可开启…但可以进行召唤物的回收", "召唤物的同步
// 触发技能与回收不受缴械或沉默等效果制约"; S3 备注 "召唤物对周围敌人造成的持续法术伤害不可对空,且视为来自令造成的伤害"; SUM-Y
// 特性 "※“清平”“逍遥”:部署费用-3;“弦惊”:部署费用-5", stage 3 "属性更强的召唤物"); PRTS “清平” / “逍遥” / “弦惊” (备注 "持有
// 禁疗", "可以且优先攻击自身阻挡的单位"; 弦惊's merge rules and "高级形态下:生命上限+100%,攻击力+80%,防御力+80%,法术抗性+100%,
// 攻击间隔+0.8s,阻挡数+2…直接加算或直接乘算", "升级时立刻重设召唤物生命值至最大值"); PRTS 分支特性信息 召唤师; the client's
// battle data (charpack char_2023_ling: Talents/1 charge_token[born], Talents/2 ling_t on her summons, CommonAbilities
// die_to_kill_token; buff_template_data ling_t (ON_OWNER_KILLED: modify_sp[trigger] `sp`, ling_atk_up ATK MULTIPLIER STACK
// to max_stack_cnt), ling_s3_aoe[token] (AOEDamage MAGICAL SPLASH WALK_ONLY, source 令, × atk_scale, range x-5 every
// `interval`), ling_soul3_evolution[self] / [another] / _die (the merge); skill prefabs skchr_ling_1 (aura: the summons' mode
// + ATK / ASPD, hers + trigger_charge_token), skchr_ling_2 (Attack: 2 targets, ling_s2_unmovable UNMOVABLE; TokenAttack:
// InterruptCharacterAttack + the summon's ability "2", `_checkCanUseAblityFlag` false; Recycle: 1.2 s later, Withdraw
// `_switchToDeadState` + RechargeToken cnt for every summon below hp_ratio), skchr_ling_3 (aura + ling_s3[charge_token]);
// equips ling_equip_1_* (trigger_charge_token: the trait's cnt)).
// - Trait (召唤师): the profession default (ranged arts on her 3-1, hits air units — PRTS "可对空"), blocks 1, ground enemies
// target her; her summons leave the field with her (shared/summoner.js).
// - T1 挑灯问梦 "可以使用5个召唤物(最多同时部署3个),功能随技能选择而改变": the summon of her pick's skill (“清平” / “逍遥” /
// “弦惊” — the skills' overrideTokenKey) is the hand piece the player places; the deck of ../shared/summoner.js with charge =
// cnt (5), cap = the summon record's deckStack (5: its hidden talent's max_deck_stack_cnt, tools/build-data.mjs
// tokenTalentDeckBonus; +3 with SUM-Y), at most its deployLimit standing (3 = 1 + the talent's max_deploy_count 2; SUM-Y
// stage 2+: 4) — the hand count too. Every summon holds 禁疗 (PRTS; [ASSUMED: given here — the data's `abnormal` lacks it]) and fights with its
// own data: “清平” blocks 1 and strikes in melee (physical) on its 1-1, “逍遥” shoots arts on its 3-1 (air units too), “弦惊”
// blocks 2 and strikes every enemy it blocks (physical; its trait "攻击阻挡的所有敌人").
// - T2 随付笺咏醉屠苏 "召唤物被击倒/吸收/回收时令额外获得3点技力、攻击力+3%(攻击力加成最多叠加5层)": a summon knocked out,
// absorbed by a 弦惊 merge or recalled by S2 ⇒ +sp SP (no SP while a timed skill of hers runs; 阻回 stops it) and a stack of
// ATK +atk (直接乘算) up to max_stack_cnt, kept until she leaves the field — not when her summons leave with her.
// - S1 重进酒 (MANUAL, data DEFAULT, 25 s): +cnt held at the cast; she and her summons ATK +atk, ASPD +attack_speed, and her
// summons' attacks deal arts damage meanwhile ("召唤物伤害类型变为法术"). Passive "召唤物可部署在近战位": placement only.
// - S2 笑鸣瑟 (MANUAL, data DEFAULT, 2 charges — the cast replaces the attack she is about to make): her attack strikes up to
// `value` (2) enemies of her range for atk_scale × ATK arts and 束缚 ling_s2_unmovable.duration s each; every summon of hers
// on the field breaks off its attack (its cooldown restarts) and strikes up to 2 enemies of its own range for its skill's
// 2.atk_scale × its ATK arts + 束缚 2.duration s — stunned / silenced / disarmed ones too (PRTS; `_checkCanUseAblityFlag`
// false) — instant for a melee summon, a bolt for “逍遥”; RECYCLE_DELAY (1.2) s after the cast every summon of hers below
// hp_ratio of its max HP is recalled (+1 held each, T2). Passive "召唤物可部署在远程位,攻击造成法术伤害": “逍遥”'s data.
// The 备注's cast with no target cannot happen here: the data's DEFAULT casts it on an attack.
// - S3 宁作吾 (MANUAL, data DEFAULT, 30 s): she and her summons ATK +atk, DEF +def; every `interval` (0.5) s from the moment
// it covers a summon, every ground enemy on that summon's x-5 takes atk_scale × 令's ATK arts (溅射: not dodgeable, her
// damage — PRTS); +cnt held when it ends (not when she leaves). Passive: the 弦惊 merge — at a 弦惊's deployment, another
// basic 弦惊 of hers on its range ⇒ that one turns 高级形态 and the new one is absorbed (withdrawn); else, the new one
// standing on the range of one or more basic ones ⇒ it turns 高级形态 and the latest deployed of them is absorbed. 高级形态
// (until it leaves the field): its skill's 2.max_hp / 2.atk / 2.def (直接乘算), RES ×(1 + 2.magic_resistance), attack
// interval +2.base_attack_time s, block +2.block_cnt, arts attacks, HP reset to its new maximum. Its "占据2个部署位" (and
// SUM-X-like 部署位 rules) concern the prep's deploy cap, which summon pieces never use in this mode: N/A.
// - Module SUM-Y 诗短梦长: trait "召唤物持有上限+3,召唤物部署费用减少" — +3 held on the card at the battle start, cap +3, the
// summons' lower costs (the module variant); stage 2+ T1 "最多同时部署4个", stage 3 "属性更强的召唤物" (the module variant's
// stats). Its attributes are in the stats.
import { num, talentBb, traitBb, moduleOn, skillRec, batMod, giveSp, up } from '../shared/tier1.js';
import { summonDeck, holdBuff, tokenStat } from '../shared/summoner.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { acquireTargets } from '../../../ai.js';
import { COLS, PROJECTILE_SPEEDS } from '../../../constants.js';
const S1 = 'skchr_ling_1';
const S2 = 'skchr_ling_2';
const S3 = 'skchr_ling_3';
export const SOUL1 = 'token_10020_ling_soul1';
export const SOUL2 = 'token_10020_ling_soul2';
export const SOUL3 = 'token_10020_ling_soul3';
const SOULS = Object.freeze([SOUL1, SOUL2, SOUL3]);
const SUM_Y = 'uniequip_002_ling';
/** skchr_ling_2 Recycle `_preDelay` 1.2 s: the recall of the summons below hp_ratio after the cast. */
export const RECYCLE_DELAY = 1.2;
/** S3's area around each summon (ling_s3[aoe] range_id x-5: its tile and the four next to it). */
const X5 = Object.freeze([[1, 0], [0, -1], [0, 0], [0, 1], [-1, 0]]);
const TAG_S2 = 'ling:s2';
const TAG_S3 = 'ling:s3';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const skillOn = (u, id) => !!(u && u.skill && u.skill.active && u.skill.id === id);
const tileOf = (u) => u.tileR * COLS + u.tileC;
/** Her S1 / S3 acting on a summon (held from each skill start / end and each summon deployment). */
function syncSoul(battle, owner, t, { b1, b3 }) {
if (!t.alive) return;
holdBuff(battle, t, 'ling:s1:soul', skillOn(owner, S1), { atkPct: num(b1.atk), aspd: num(b1.attack_speed) });
holdBuff(battle, t, 'ling:s3:soul', skillOn(owner, S3), { atkPct: num(b3.atk), defPct: num(b3.def) });
}
/** 弦惊 高级形态 (its skill blackboard "2.*"): stats, block, arts attacks, HP reset to the new maximum. */
function evolve(battle, t) {
const tb = t.def.skill?.bb ?? {};
battle.addBuff(t, {
key: 'ling:soul3:advanced', tags: ['token'],
mods: {
hpPct: num(tb['2.max_hp']), atkPct: num(tb['2.atk']), defPct: num(tb['2.def']), resMul: 1 + num(tb['2.magic_resistance']),
batPct: batMod(tb['2.base_attack_time'], { stats: t.def.stats }), blockCnt: num(tb['2.block_cnt']),
},
});
t.mem.lingAdvanced = true;
t.hp = t.s.maxHp;
battle.fx('soul', { x: t.x, y: t.y, id: t.id });
}
/** The summons' kit: 禁疗, her skills' effects on them, S3's pulses, the 弦惊 merge. `owner` = 令. */
function soulKit(t, owner, opts) {
return {
skill: null,
talents: [],
trait: t.defId === SOUL3 ? { hitAllBlocked: true } : null,
install(battle, s) {
battle.addBuff(s, { key: 'ling:soul:abnormal', flags: { noHeal: true }, persist: true, allowDead: true }); // 持有禁疗
battle.on('deploy', (c) => {
if (c.unit !== s) return;
s.mem.lingAdvanced = false;
s.mem.lingAoeAt = null;
syncSoul(battle, owner, s, opts);
if (s.defId === SOUL3) merge(battle, owner, s);
}, { owner: s });
// S1: arts attacks; 高级形态: arts attacks
battle.on('hit', (c) => {
if (c.source !== s || !c.dmg || !c.dmg.isAttack || c.dmg.type === 'element') return;
if (s.mem.lingAdvanced || skillOn(owner, S1)) c.dmg.type = 'arts';
}, { owner: s, priority: 50 });
// S3: every interval s from the moment it covers this summon, atk_scale × 令's ATK arts on the ground enemies of its x-5
const iv = num(opts.b3.interval, 0.5);
battle.on('tick', () => {
if (!up(s) || !skillOn(owner, S3) || !(iv > 0)) { s.mem.lingAoeAt = null; return; }
if (s.mem.lingAoeAt == null) { s.mem.lingAoeAt = battle.time + iv; return; }
if (battle.time + 1e-9 < s.mem.lingAoeAt) return;
s.mem.lingAoeAt += iv;
const keys = absoluteRangeKeys(X5, s.tileR, s.tileC, s.dir, 0);
for (const e of battle.enemiesInKeys(keys, s, { canHitFly: false })) {
if (!e.alive) continue;
battle.dealDamage(owner, e, { amount: owner.s.atk * num(opts.b3.atk_scale), type: 'arts', isSkill: true, isSplash: true, canDodge: false, tags: ['skill', TAG_S3] });
}
}, { owner: s });
},
};
}
/** The 弦惊 merge at the deployment of 弦惊 `t` (see the header). */
function merge(battle, owner, t) {
const deck = owner.mem.summonDeck;
if (!deck || !t.rangeKeySet) return;
const mates = deck.standing().filter((o) => o !== t && o.defId === SOUL3 && !o.mem.lingAdvanced);
const target = mates.find((o) => t.rangeKeySet.has(tileOf(o)));
if (target) { evolve(battle, target); deck.withdraw(t, 'absorb'); return; }
const holders = mates.filter((o) => o.rangeKeySet && o.rangeKeySet.has(tileOf(t)));
if (!holders.length) return;
const material = holders.reduce((a, b) => (b.deploySeq > a.deploySeq ? b : a));
evolve(battle, t);
deck.withdraw(material, 'absorb');
}
/** S2: a summon's synced strike — up to `n` enemies of its range, its skill's 2.atk_scale × ATK arts + 束缚 2.duration s. */
function syncedStrike(battle, t, { b2 }) {
const tb = t.def.skill?.bb ?? {};
const scale = num(tb['2.atk_scale'], num(b2.atk_scale, 1));
const dur = num(tb['2.duration'], num(b2['ling_s2_unmovable.duration']));
const n = Math.max(1, Math.floor(num(b2.value, 2)));
const targets = acquireTargets(battle, t, { ...t.profile, maxTargets: n, allInRange: false });
t.atkCd = t.s.interval; // InterruptCharacterAttack (resetCD): its attack starts over
const strike = (e) => {
if (!e || !e.alive) return;
battle.dealDamage(t, e, { amount: t.s.atk * scale, type: 'arts', isSkill: true, tags: ['skill', TAG_S2] });
if (e.alive && dur > 0) battle.applyStatus(e, 'bind', { duration: dur, source: t });
};
for (const e of targets.slice(0, n)) {
if (t.profile.attack === 'ranged') {
battle.addProjectile({ from: t, target: e, speed: PROJECTILE_SPEEDS.bolt, visual: 'bolt', source: t, onHit: (c) => strike(c.target) });
} else strike(e);
}
}
export default {
char_2023_ling: (bb, chess) => {
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s2 = skillRec(chess, S2);
const t0 = talentBb({ talents: chess?.talentsBase ?? chess?.talents }, 0); // 挑灯问梦: cnt
const t1 = talentBb(chess, 1); // 随付笺咏醉屠苏: sp, atk, max_stack_cnt
const sumY = moduleOn(chess) && chess.module.id === SUM_Y ? Math.max(0, Math.floor(num(traitBb(chess).cnt))) : 0;
const picked = chess?.skill?.skillId ?? null;
const tokenId = skillRec(chess, picked)?.overrideTokenKey ?? null;
const opts = { b1, b2, b3 };
const gain = (unit, n) => unit.mem.summonDeck?.gain(n);
return {
skills: {
[S1]: {
kind: 'duration', mods: { atkPct: num(b1.atk), aspd: num(b1.attack_speed) },
onStart({ unit }) { gain(unit, num(b1.cnt, 1)); }, // 技能开启时获得1个召唤物
},
[S2]: {
kind: num(s2?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
targeting: { maxTargets: Math.max(1, Math.floor(num(b2.value, 2))) },
attack: {
atkScale: num(b2.atk_scale, 1),
onEachHit({ battle, unit, target, kind }) {
const d = num(b2['ling_s2_unmovable.duration']);
if (kind === 'main' && target && target.alive && d > 0) battle.applyStatus(target, 'bind', { duration: d, source: unit });
},
},
onStart({ battle, unit }) {
const deck = unit.mem.summonDeck;
if (!deck) return;
for (const t of deck.standing()) syncedStrike(battle, t, opts);
// 技能结束时回收生命值低于一半的召唤物 (Recycle, 1.2 s after the cast)
const hp = num(b2.hp_ratio, 0.5);
battle.after(RECYCLE_DELAY, () => {
if (!up(unit)) return;
for (const t of deck.standing()) if (t.hpRatio < hp - 1e-9) deck.recall(t);
}, { owner: unit });
},
},
[S3]: {
kind: 'duration', mods: { atkPct: num(b3.atk), defPct: num(b3.def) },
onEnd({ unit, reason }) { if (reason !== 'death') gain(unit, num(b3.cnt, 1)); }, // 技能结束时获得1个召唤物
},
},
talents: [
{ install(battle, unit) { // 挑灯问梦: the deck of her summon pieces and their kit
const cnt = Math.max(0, Math.floor(num(t0.cnt)));
const sp = num(t1.sp), atk = num(t1.atk), stacks = Math.max(1, Math.floor(num(t1.max_stack_cnt, 1)));
summonDeck(battle, unit, {
tokenIds: SOULS,
charge: cnt,
start: sumY,
cap: num(tokenId ? tokenStat(battle, unit, tokenId, 'deckStack') : cnt, cnt),
maxDeployed: num(tokenId ? tokenStat(battle, unit, tokenId, 'deployLimit') : 1, 1),
kit: (t) => soulKit(t, unit, opts),
// 随付笺咏醉屠苏: knocked out / absorbed / recalled ⇒ +sp SP and a stack of ATK +atk (not when they leave with her)
onLeave(t, kind) {
if (!(kind === 'killed' || kind === 'absorb' || kind === 'recall') || !up(unit)) return;
if (sp > 0 && giveSp(unit, sp) > 0) battle.fx('spGain', { x: unit.x, y: unit.y, id: unit.id, n: sp });
if (atk) battle.addBuff(unit, { key: 'ling:t2', mods: { atkPct: atk }, refresh: 'stack', maxStacks: stacks, tags: ['talent'] });
},
});
const sync = (c) => {
if (c.unit !== unit) return;
for (const t of unit.mem.summonDeck.standing()) syncSoul(battle, unit, t, opts);
};
battle.on('skillStart', sync, { owner: unit });
battle.on('skillEnd', sync, { owner: unit });
} },
// 随付笺咏醉屠苏 lives in the deck's onLeave above
],
};
},
};