feedback5 O21: 自选 kits for 麦哲伦, 令 and 电弧 (with their summons) and the 召唤师 summon deck
- kits/ops/op-mgllan.js, op-ling.js, op-radian.js: every skill, both talents, the trait and every module at every form, and the kits of their summons (龙腾.F / .L / .A, “清平” / “逍遥” / “弦惊”, 戴乌 / 赛柯 / 桑特拉). - kits/shared/summoner.js (new): the summon deck the three share — the holding (charge_token[born], SUM-Y's +3), the return of a placed piece that left the field (owner on the field, redeploy time, one held, DP), recalls, the summons leaving with their owner (die_to_kill_token). - kits/index.js: OPERATOR_KIT_FILES += op-mgllan.js, op-ling.js, op-radian.js; kits/README.md: the layout row and a pointer for summoner kits. - tests: test/content/op_mgllan.test.js, op_ling.test.js, op_radian.test.js. - golden: only diy moved — diy-106 … diy-109 now field the new kits' configurations (operator configurations are appended in OPERATOR_KIT_FILES order, so the prototype chunks shift back) and diy-110 … diy-115 are new; every other family is byte-identical.
This commit is contained in:
1 parent
996f5d20fe
commit
f0d220166a
10 files changed
+2150
-97
No files matched your search
@@ -15,6 +15,7 @@ operators, the self-select 6★ operators, contributions from GitHub issue #136
|
||||
| `ops/op-<codename>.js` | one 自选 operator kit per file (below: "How to add an operator (自选)") |
|
||||
| `shared/tier1.js` | the general kit helpers (blackboard readers, unit predicates, hit hooks, area queries, buffs, zones, free tiles, skill records) and the notes of the tier-1 kits |
|
||||
| `shared/tier2.js` … `tier6.js` | helpers two or more kits of that tier use, and that tier's notes (conventions, simplifications, fx kinds) |
|
||||
| `shared/summoner.js` | the 召唤师 summon deck the 自选 kits of 麦哲伦 / 令 / 电弧 share (`summonDeck`: the holding, a placed piece's return, recalls, the summons leaving with their owner; `holdBuff`, `tokenStat`) |
|
||||
| `tier1.js` … `tier6.js` | re-export shims for the old import paths; removed after the 0.2.0 refactor |
|
||||
|
||||
`content/index.js` takes `KITS` from `index.js`: a unit's kit is `KITS[def.baseId]` (also the exact or the suffix-less
|
||||
@@ -191,7 +192,9 @@ so a kit finds its pieces before the battle starts as `battle.allyUnits` with `k
|
||||
`ownerUnit === unit`, still undeployed (e.g. `op-bgsnow.js` gives its 打字机 pieces their kit in a talent's `install`
|
||||
with `battle._setupUnit(t, kit)`; `op-cgbird.js` counts its 幻影). A kit test fields a piece the same way:
|
||||
`units: [op, { uid, kind: 'token', tokenId, ownerUid, row, col }]` (`test/content/op_bgsnow.test.js`); the match side is
|
||||
covered by `test/match/diy-shop.test.js` (a DIY 鸿雪's 打字机 from prep to battle). Potential is 0 [ASSUMED: no account]. A new kit makes the operator a legal pick at once:
|
||||
covered by `test/match/diy-shop.test.js` (a DIY 鸿雪's 打字机 from prep to battle). A 召唤师 whose talent holds a deck of
|
||||
summons ("可以使用5个召唤物(最多同时部署3个)") takes `shared/summoner.js summonDeck` — the holding, a placed piece's return
|
||||
on its tile, recalls, its pieces' kits (`op-mgllan.js`, `op-ling.js`, `op-radian.js`). Potential is 0 [ASSUMED: no account]. A new kit makes the operator a legal pick at once:
|
||||
the server's `welcome.diyKitted` lists `KITTED_CHARS`, so the 自选编队 picker offers it and `room.diy` keeps it.
|
||||
|
||||
**The fidelity rule and the checklist** above apply item by item: every skill at rank 4 and 7, every talent, every
|
||||
|
||||
@@ -133,6 +133,9 @@ export const OPERATOR_KIT_FILES = Object.freeze([
|
||||
'op-haak.js',
|
||||
'op-phatom.js',
|
||||
'op-crosly.js',
|
||||
'op-mgllan.js',
|
||||
'op-ling.js',
|
||||
'op-radian.js',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
// 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 deck (max_deck_stack_cnt 5, +3 with SUM-Y), at most 1 + max_deploy_count (2 ⇒ 3; SUM-Y stage 3:
|
||||
// 4) standing. 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 tokTalent = (def, key) => (def?.talents ?? []).find((t) => t && t.bb && t.bb[key] != null) ?? null;
|
||||
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 tdef = tokenId ? battle.tokenDef(tokenId, unit) : null;
|
||||
const deckBase = num(tokTalent(tdef, 'max_deck_stack_cnt')?.bb?.max_deck_stack_cnt, cnt);
|
||||
const extra = num(tokTalent(tdef, 'max_deploy_count')?.bb?.max_deploy_count, 0);
|
||||
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: deckBase + num(tokenId ? tokenStat(battle, unit, tokenId, 'deckStack') : 0, 0),
|
||||
maxDeployed: num(tokenId ? tokenStat(battle, unit, tokenId, 'deployLimit') : 1, 1) + extra,
|
||||
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
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,202 @@
|
||||
// server/sim/content/kits/ops/op-mgllan.js — 麦哲伦 (char_248_mgllan) 自选 operator kit: 6★ 召唤师 (辅助), an owned-6★ pick of
|
||||
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules (SUM-X 专业无人机操作模块, SUM-Y
|
||||
// 教学用龙腾无人机) at every form, and the kits of her drones 龙腾.F / 龙腾.L / 龙腾.A (token_10005_mgllan_drone1 / 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_248_mgllan): 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 / the tokens' character_table rows (zh_CN, as built
|
||||
// into backups.json); PRTS 麦哲伦 (光学折射配件 备注 "无人机实际获得隐匿(异常效果)而非隐匿"; S1 / S2 / S3 备注 "技能结束时的回收
|
||||
// 仅是触发召唤物的技能,技能无法触发的场合不会产生效果"; S3 备注 "主动开启时,无人机攻击的爆炸半径从0.75增加至1.25"; SUM-Y 特性
|
||||
// "※龙腾.F:部署费用-1;龙腾.L、龙腾.A:部署费用-3", stage 3 "※龙腾.F:生命上限+100,防御+50 / 龙腾.L:攻击+40,攻击速度+3 /
|
||||
// 龙腾.A:攻击+50,攻击速度+3"); PRTS 龙腾.F / 龙腾.L / 龙腾.A (备注 "持有禁疗"; 龙腾.A "普通攻击的爆炸范围半径0.75,可对空");
|
||||
// PRTS 分支特性信息 召唤师 ("可对空", "干员离场后,附属的召唤物随之消失"); the client's battle data (charpack char_248_mgllan:
|
||||
// Talents/1 charge_token[born], CommonAbilities die_to_kill_token, Modes S1_Default / S1 with the S1[Passive] / S1[Active]
|
||||
// buffs; buff_template_data mgllan_s_1[passive] / [active] and mgllan_s_1_token[passive] / [active] (CreateBuffInRange on her
|
||||
// attack range and, through CreateBuffToToken, each drone's: the DB `sluggish` / mgllan_frozen UNMOVABLE), mgllan_drone_t
|
||||
// (INVISIBLE for hidden_duration at the drone's deployment), mgllan_drone_s[finish] (RechargeToken + Withdraw
|
||||
// `_switchToDeadState`); skill prefabs skchr_mgllan_1 / 2 / 3 (`_buffs` = the drones' mode + bonus, `_passiveBuffs` = hers,
|
||||
// mgllan_s[after] trigger_token_skill); equips mgllan_equip_1_* (free_summon: 部署位 only), mgllan_equip_1_2/3_p2
|
||||
// (mgllan_drone_e_hidden INVISIBLE for e_hidden_duration on her inside a drone's x-5, `_removeBuffWhenTargetLeave`),
|
||||
// mgllan_equip_2_* (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 drones leave the field with her (shared/summoner.js).
|
||||
// - T1 支援无人机·龙腾 "可以使用5个不阻挡敌人的无人机(最多同时部署3个),功能随技能选择而改变": the drone of her pick's skill
|
||||
// (龙腾.F / .L / .A — the skills' overrideTokenKey) is the hand piece the player places; the deck of ../shared/summoner.js
|
||||
// with charge = cnt (5) at each of her deployments, cap = the drone deck (its hidden talent's max_deck_stack_cnt 5, +the
|
||||
// SUM-Y deckStack 3), at most 1 + its max_deploy_count (2 ⇒ 3; SUM-Y stage 3: 3 ⇒ 4) standing. Every drone holds 禁疗
|
||||
// (PRTS; the data's `abnormal` lacks it [ASSUMED: given here]), blocks nothing, has no skill of its own here (its 回收 is
|
||||
// her skills' end, below). 龙腾.F (ATK 0) makes no attack (the engine's rule for a 0-ATK summon; a commented-out PRTS
|
||||
// note "缴械,普通攻击不造成伤害" agrees [ASSUMED]); 龙腾.L strikes one ground enemy on its own tile for arts damage; 龙腾.A
|
||||
// fires physical shells that blow up within BLAST (0.75) of their target, air units too.
|
||||
// - T2 光学折射配件 "无人机在部署后的20秒内处于隐匿状态": each drone holds 隐匿 (the anomaly — flag `stealth`: only an enemy it
|
||||
// blocks could target it, enemy areas skip it) for its token talent's hidden_duration from each deployment. SUM-X stage
|
||||
// 2+ "26秒…若麦哲伦在其周围四格则享受相同效果": the drones' 26 s (the module variant), and she holds 隐匿 while she stands on
|
||||
// the x-5 of a drone still inside its e_hidden_duration window [ASSUMED: the window of that drone's deployment — the equip
|
||||
// aura's buff lasts e_hidden_duration and drops when she leaves the x-5].
|
||||
// - S1 高效制冷模块 (MANUAL, data DEFAULT, 15 s): passive (while S1 is picked and not running) every attack@interval (3) s from
|
||||
// her deployment / the skill's end, every enemy (air units too) on her attack range and on each of her standing drones'
|
||||
// ranges (龙腾.F: x-4) takes 停顿 attack@sluggish s; while it runs, every 3 s from the cast, 束缚 attack@frozen_duration s
|
||||
// instead. It acts while she is stunned or silenced (the buff is neither — isStunnable / isSilenceable 0).
|
||||
// - S2 激光开采模块 (MANUAL, data DEFAULT, 15 s): she and her drones ASPD +attack_speed; a 龙腾.L strikes every enemy on its tile
|
||||
// (群体) while it runs. S3 武装打击模块 (MANUAL, data DEFAULT, 15 s): she and her drones ATK +atk; 龙腾.A's blast radius
|
||||
// BLAST_S3 (1.25). Passive parts ("无人机可以部署在近战位 / 远程位"): placement — the piece's position, nothing in battle.
|
||||
// - Every skill's end (the normal end, not her leaving: the drones go with her then) recalls every standing drone that can
|
||||
// still trigger its own skill (not stunned / frozen / asleep, not silenced — PRTS "技能无法触发的场合不会产生效果"
|
||||
// [ASSUMED: those]): +1 held each, the drone leaves and comes back on its tile its redeploy time (10 s) later as any piece.
|
||||
// - Module SUM-X 专业无人机操作模块: trait "首个召唤物部署时不消耗部署位" — 部署位 are the prep's deploy cap, which summon pieces
|
||||
// never use in this mode (server/match deployCount counts chess only): N/A; stage 2+ is T2's change above; stats.
|
||||
// SUM-Y 教学用龙腾无人机: trait "召唤物持有上限+3,召唤物部署费用减少" — +3 held on the card at the battle start, cap +3, the
|
||||
// drones' lower costs (the module variant); stage 2+ T1 "最多同时部署4个", stage 3 "属性更强的" (the module variant's stats).
|
||||
|
||||
import { num, talentBb, traitBb, moduleOn, skillRec, up } from '../shared/tier1.js';
|
||||
import { summonDeck, holdBuff, tokenStat } from '../shared/summoner.js';
|
||||
import { absoluteRangeKeys } from '../../../targeting.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
|
||||
const S1 = 'skchr_mgllan_1';
|
||||
const S2 = 'skchr_mgllan_2';
|
||||
const S3 = 'skchr_mgllan_3';
|
||||
export const DRONE_F = 'token_10005_mgllan_drone1';
|
||||
export const DRONE_L = 'token_10005_mgllan_drone2';
|
||||
export const DRONE_A = 'token_10005_mgllan_drone3';
|
||||
const DRONES = Object.freeze([DRONE_F, DRONE_L, DRONE_A]);
|
||||
const SUM_Y = 'uniequip_003_mgllan';
|
||||
/** PRTS 龙腾.A 备注 "普通攻击的爆炸范围半径0.75"; S3 备注 "主动开启时,无人机攻击的爆炸半径从0.75增加至1.25". */
|
||||
export const BLAST = 0.75;
|
||||
export const BLAST_S3 = 1.25;
|
||||
/** 光学折射配件 at E2 when the drone def lacks its hidden_duration (PRTS: 20 s). */
|
||||
const HIDDEN_FALLBACK = 20;
|
||||
|
||||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||||
const skillOn = (u, id) => !!(u && u.skill && u.skill.active && u.skill.id === id);
|
||||
/** First talent of a normalised token def holding `key` (token talents keep no index). */
|
||||
const tokTalent = (def, key) => (def?.talents ?? []).find((t) => t && t.bb && t.bb[key] != null) ?? null;
|
||||
|
||||
/** Her skills acting on a drone (held from each skill start / end and each drone deployment). */
|
||||
function syncDrone(battle, owner, t, { b2, b3 }) {
|
||||
if (!t.alive) return;
|
||||
const s2 = skillOn(owner, S2), s3 = skillOn(owner, S3);
|
||||
holdBuff(battle, t, 'mgllan:s2:drone', s2, { aspd: num(b2.attack_speed) });
|
||||
holdBuff(battle, t, 'mgllan:s3:drone', s3, { atkPct: num(b3.atk) });
|
||||
if (t.defId === DRONE_A) t.profile.splashRadius = s3 ? BLAST_S3 : BLAST;
|
||||
}
|
||||
|
||||
/** The drones' kit: 禁疗, 光学折射配件's 隐匿 window, her skills' modes (龙腾.L 群体, 龙腾.A blast). `owner` = 麦哲伦. */
|
||||
function droneKit(t, owner, opts) {
|
||||
return {
|
||||
skill: null,
|
||||
talents: [],
|
||||
trait: t.defId === DRONE_A ? { splashRadius: BLAST, splashScale: 1 } : null,
|
||||
install(battle, d) {
|
||||
battle.addBuff(d, { key: 'mgllan:drone:abnormal', flags: { noHeal: true }, persist: true, allowDead: true }); // 持有禁疗
|
||||
const hidden = num(tokTalent(d.def, 'hidden_duration')?.bb?.hidden_duration, HIDDEN_FALLBACK);
|
||||
battle.on('deploy', (c) => {
|
||||
if (c.unit !== d) return;
|
||||
if (hidden > 0) {
|
||||
battle.addBuff(d, { key: 'mgllan:drone:hidden', flags: { stealth: true }, duration: hidden, status: 'stealth', tags: ['talent'] });
|
||||
battle.fx('stealth', { x: d.x, y: d.y, id: d.id });
|
||||
}
|
||||
syncDrone(battle, owner, d, opts);
|
||||
}, { owner: d });
|
||||
// S2: a 龙腾.L strikes every enemy on its tile
|
||||
if (d.defId === DRONE_L) {
|
||||
battle.on('beforeAttack', (c) => {
|
||||
if (c.attacker !== d || !skillOn(owner, S2)) return;
|
||||
const all = battle.enemiesInKeys(d.rangeKeys, d, c.profile);
|
||||
if (all.length) c.targets = all;
|
||||
}, { owner: d });
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
char_248_mgllan: (bb, chess) => {
|
||||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||||
const t0 = talentBb({ talents: chess?.talentsBase ?? chess?.talents }, 0); // 支援无人机·龙腾: 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 = { b2, b3 };
|
||||
/** 技能结束时回收所有无人机 (her normal end; the drones that cannot trigger their skill stay). */
|
||||
const recall = ({ unit, reason }) => {
|
||||
if (reason === 'death') return;
|
||||
const deck = unit.mem.summonDeck;
|
||||
if (!deck) return;
|
||||
for (const d of deck.standing()) if (d.canAct && !d.s.flags.silence) deck.recall(d);
|
||||
};
|
||||
return {
|
||||
skills: {
|
||||
[S1]: { kind: 'duration', onEnd: recall },
|
||||
[S2]: { kind: 'duration', mods: { aspd: num(b2.attack_speed) }, onEnd: recall },
|
||||
[S3]: { kind: 'duration', mods: { atkPct: num(b3.atk) }, onEnd: recall },
|
||||
},
|
||||
talents: [
|
||||
{ install(battle, unit) { // 支援无人机·龙腾: the deck of her drone pieces and their kit
|
||||
const cnt = Math.max(0, Math.floor(num(t0.cnt)));
|
||||
const tdef = tokenId ? battle.tokenDef(tokenId, unit) : null;
|
||||
const deckBase = num(tokTalent(tdef, 'max_deck_stack_cnt')?.bb?.max_deck_stack_cnt, cnt);
|
||||
const extra = num(tokTalent(tdef, 'max_deploy_count')?.bb?.max_deploy_count, 0);
|
||||
summonDeck(battle, unit, {
|
||||
tokenIds: DRONES,
|
||||
charge: cnt,
|
||||
start: sumY,
|
||||
cap: deckBase + num(tokenId ? tokenStat(battle, unit, tokenId, 'deckStack') : 0, 0),
|
||||
maxDeployed: num(tokenId ? tokenStat(battle, unit, tokenId, 'deployLimit') : 1, 1) + extra,
|
||||
kit: (t) => droneKit(t, unit, opts),
|
||||
});
|
||||
// her skills on the standing drones: at each start / end (a drone deployed meanwhile syncs itself)
|
||||
const sync = (c) => {
|
||||
if (c.unit !== unit) return;
|
||||
for (const d of unit.mem.summonDeck.standing()) syncDrone(battle, unit, d, opts);
|
||||
};
|
||||
battle.on('skillStart', sync, { owner: unit });
|
||||
battle.on('skillEnd', sync, { owner: unit });
|
||||
} },
|
||||
{ install(battle, unit) { // 光学折射配件 (the drones' 隐匿: their kit); SUM-X stage 2+: hers beside a hidden drone
|
||||
const tdef = tokenId ? battle.tokenDef(tokenId, unit) : null;
|
||||
const et = tokTalent(tdef, 'e_hidden_duration');
|
||||
const dur = num(et?.bb?.e_hidden_duration);
|
||||
if (!(dur > 0)) return;
|
||||
const grid = et.rangeGrid?.length ? et.rangeGrid : [[1, 0], [0, -1], [0, 0], [0, 1], [-1, 0]];
|
||||
battle.on('tick', () => {
|
||||
const deck = unit.mem.summonDeck;
|
||||
const want = up(unit) && !!deck && deck.standing().some((d) => battle.time < d.deployedAt + dur - 1e-9
|
||||
&& absoluteRangeKeys(grid, d.tileR, d.tileC, d.dir, 0).includes(unit.tileR * COLS + unit.tileC));
|
||||
const has = unit.findBuff('mgllan:eHidden');
|
||||
if (want && !has) battle.addBuff(unit, { key: 'mgllan:eHidden', flags: { stealth: true }, status: 'stealth', tags: ['module'] });
|
||||
else if (!want && has) battle.removeBuff(unit, 'mgllan:eHidden');
|
||||
}, { owner: unit });
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
// S1 高效制冷模块: every attack@interval s, 停顿 (passive) / 束缚 (running) on her range and her drones' ranges
|
||||
if (picked !== S1) return;
|
||||
const iv = num(b1['attack@interval'], 3);
|
||||
if (!(iv > 0)) return;
|
||||
const restart = (c) => { if (c.unit === unit) unit.mem.mgllanPulseAt = battle.time + iv; };
|
||||
battle.on('deploy', restart, { owner: unit });
|
||||
battle.on('skillStart', restart, { owner: unit });
|
||||
battle.on('skillEnd', restart, { owner: unit });
|
||||
battle.on('tick', () => {
|
||||
if (!up(unit) || !(battle.time + 1e-9 >= (unit.mem.mgllanPulseAt ?? Infinity))) return;
|
||||
unit.mem.mgllanPulseAt += iv;
|
||||
const active = skillOn(unit, S1);
|
||||
const key = active ? 'bind' : 'sluggish';
|
||||
const duration = num(active ? b1['attack@frozen_duration'] : b1['attack@sluggish']);
|
||||
if (!(duration > 0)) return;
|
||||
const done = new Set();
|
||||
for (const src of [unit, ...(unit.mem.summonDeck?.standing() ?? [])]) {
|
||||
for (const e of battle.enemiesInKeys(src.rangeKeys, src, { canHitFly: true })) {
|
||||
if (done.has(e)) continue;
|
||||
done.add(e);
|
||||
battle.applyStatus(e, key, { duration, source: src });
|
||||
}
|
||||
}
|
||||
battle.fx('coldWind', { x: unit.x, y: unit.y, id: unit.id, n: done.size });
|
||||
}, { owner: unit });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,286 @@
|
||||
// server/sim/content/kits/ops/op-radian.js — 电弧 (char_4195_radian) 自选 operator kit: 6★ 召唤师 (辅助), an owned-6★ pick of
|
||||
// the tier-5 and tier-6 自选 slots — not the 补位 stand-in Raidian (char_614_acsupo, standin-acsupo.js); every skill, both
|
||||
// talents, the trait and both modules (SO-A 电弧特勤证章, SO-B 新起点) at every form, and the kits of her summons 戴乌 / 赛柯 /
|
||||
// 桑特拉 (token_10051_radian_tower1, token_10052_radian_tower2, token_10053_radian_tower3). Kit contract and the 自选 rules:
|
||||
// ../README.md ("How to add an operator (自选)"); the summon deck: ../shared/summoner.js.
|
||||
//
|
||||
// Forms (data/backups.json units.char_4195_radian): 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 / the tokens' character_table rows (zh_CN, as built
|
||||
// into backups.json); PRTS 电弧 (加油~ 修正 "电弧自身" for the text's "其自身", 备注 "加成效果的数值每秒更新一次"; S1 备注 "技能期间
|
||||
// 部署的戴乌在部署时也会获得屏障,此时计算屏障量可享受电弧第二天赋的生命值加成"; S2 备注 on 赛柯's bullets; S3 备注 "召唤物可对空",
|
||||
// "停顿与法术脆弱效果为伤害附加效果,于本次伤害前生效", the 协同攻击); PRTS 戴乌 / 赛柯 / 桑特拉 (备注 "持有禁疗"; 赛柯 "不进行索敌,
|
||||
// 会持续向前攻击…飞行速度5.0,最大飞行时间0.6秒,碰撞半径0.5,不可对空,无视迷彩…首个敌方单位…100%的物理普通伤害", the S2 bullet
|
||||
// origins; 桑特拉's 协同请求 / 协同攻击 rules); PRTS 分支特性信息 召唤师; the client's battle data (charpack char_4195_radian:
|
||||
// Talents/1 charge_token[born], Talents/2 hp_to_hp / atk_to_atk / def_to_def[final_addition] on her TOKEN-class allies every
|
||||
// 1 s (鼓舞: FINAL_SCALER of the source's attribute), CommonAbilities die_to_kill_token, Talents/master_1 and LogKillBoss
|
||||
// (`rogue_yan` only); buff_template_data radian_s_1[shield] / [shield_token] (BlockDamage ANY_ATTACK_EXCEPT_ELEMENT of
|
||||
// hp_ratio × MAX_HP, the token's computed 0.03 s after it starts), radian_s_2[token] (the 赛柯 mode),
|
||||
// radian_tower2_s_2[emit_projectile] (two more bullets), radian_s_3[token] (ON_OUTPUT_DAMAGE: DB sluggish +
|
||||
// weak[magic][limit] on the target), token_radian_tower3_t[link_buff] / _s_3[trigger_link] / [prism_aoe] (the 协同攻击);
|
||||
// skill prefabs skchr_radian_1 / 2 / 3 (`_buffs` = the summons' part, `_passiveBuffs` = hers + trigger_charge_token);
|
||||
// battle_equip_table uniequip_002 / 003_radian (the talent parts gated to 集成战略: `rogue_yan`, validInGameTag roguelike)).
|
||||
// - 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 token deck (deckStack 5), at most its deployLimit (3) standing. Every summon holds 禁疗 (PRTS; the
|
||||
// data's `abnormal` lacks it [ASSUMED: given here]). 戴乌 blocks 3 and strikes in melee on its own tile (physical); 赛柯
|
||||
// blocks 2 and never picks a target: every attack interval it fires a bullet straight ahead (BULLET_SPEED tiles/s for
|
||||
// BULLET_LIFE s, collision radius BULLET_RADIUS) that deals 100 % of its ATK as physical 普通伤害 to the first selectable
|
||||
// ground enemy it touches (stunned / frozen: no shot; disarmed: none [ASSUMED]); 桑特拉 shoots arts on its 3-1, air units
|
||||
// too, its splash data capped at 0 extra targets (prism_max_target / normal_aoe max_target 0). 协同攻击 (桑特拉's own rule,
|
||||
// PRTS): when a 桑特拉 attacks, every other 桑特拉 of the field that can attack, is between attacks (its attack ready —
|
||||
// `atkCd` 0 here) and has no valid target of its own, and has the asking 桑特拉 inside its attack range ("其自身范围内其他
|
||||
// 电弧的召唤物"), shoots a link at it — the link's arrival deals prism_atk_scale × the responder's ATK as arts 普通伤害 to the
|
||||
// first asker's target — and asks the others in turn; its attack restarts [ASSUMED: the link is its attack].
|
||||
// - T2 加油~ "电弧的召唤物获得相当于电弧自身12%攻击力、防御力、生命值的鼓舞效果": every INSPIRE_IV (1) s and at each of their
|
||||
// deployments, her summons on the field get 鼓舞 = ratio × her current ATK / DEF / max HP, added after their own
|
||||
// multipliers, the strongest source of each kind kept (the keys 魔王 / 浊心斯卡蒂 use: inspire, inspire:def, inspire:hp).
|
||||
// - S1 律动线 (MANUAL, data DEFAULT, 25 s): +cnt held at the cast; she and her summons DEF +def and a 屏障 of hp_ratio × their
|
||||
// own max HP until the skill ends (every damage type but 元素伤害 — shieldType phys / arts / true); a summon deployed while it
|
||||
// runs takes it at its deployment, after its 鼓舞 (PRTS). Passive "召唤物可部署在近战位": placement only.
|
||||
// - S2 环形鳞地 (MANUAL, data DEFAULT, 25 s): +cnt held; she and her summons ATK +atk (base_attack_time 0: no change); 赛柯's
|
||||
// attacks fire BULLET_OFFSETS (three bullets: its centre, 0.2 tile left-up and right-down — fixed, PRTS) that fly its token
|
||||
// skill's attack@projectile_life_time s (0.6 at rank 4, 0.8 at rank 7: "子弹飞行距离+1"); its 3-2 range is unchanged at ranks
|
||||
// 4 / 7 (PRTS). Passive "会持续向前方发射子弹": 赛柯's data.
|
||||
// - S3 手牵手 (MANUAL, data DEFAULT, 30 s): +cnt held; she and her summons ATK +atk; before each damage of her attacks and of
|
||||
// any damage of her summons on an enemy: 停顿 `sluggish` s and 法术脆弱 (artsFragile damage_scale − 1, "同名效果取最高") for
|
||||
// weak[magic][limit] s (a dodged hit still carries them [ASSUMED]). Passive "召唤物可部署在远程位…协同攻击": 桑特拉 (above).
|
||||
// - Modules SO-A 电弧特勤证章 / SO-B 新起点: their trait and talent parts act "在【岁的界园志异】中" only (the client gates them to
|
||||
// the 集成战略 theme rogue_yan / validInGameTag roguelike): N/A here — the module talents the composed record carries
|
||||
// (SO-A's hidden respawn_time / prob / sp, SO-B's attack@max_target and x-5 recall) are ignored; their attributes are in the
|
||||
// stats. (DIY_EXCLUDED_MODULE_TYPE refuses ISW-* only, so a roster may carry them — see the report.)
|
||||
|
||||
import { num, talentBb, skillRec, up } from '../shared/tier1.js';
|
||||
import { summonDeck, holdBuff, tokenStat } from '../shared/summoner.js';
|
||||
import { acquireTargets } from '../../../ai.js';
|
||||
import { aggregateMods } from '../../../buffs.js';
|
||||
import { dirVec } from '../../../dir.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
|
||||
const S1 = 'skchr_radian_1';
|
||||
const S2 = 'skchr_radian_2';
|
||||
const S3 = 'skchr_radian_3';
|
||||
export const TOWER1 = 'token_10051_radian_tower1';
|
||||
export const TOWER2 = 'token_10052_radian_tower2';
|
||||
export const TOWER3 = 'token_10053_radian_tower3';
|
||||
const TOWERS = Object.freeze([TOWER1, TOWER2, TOWER3]);
|
||||
/** 加油~ "加成效果的数值每秒更新一次" (PRTS; Talents/2 triggerInterval 1). */
|
||||
export const INSPIRE_IV = 1;
|
||||
/** PRTS 赛柯 备注: "飞行速度5.0,最大飞行时间0.6秒,碰撞半径0.5". */
|
||||
export const BULLET_SPEED = 5;
|
||||
export const BULLET_LIFE = 0.6;
|
||||
export const BULLET_RADIUS = 0.5;
|
||||
/** PRTS S2 备注: the skill's bullets start at the tile centre, 0.2 left + 0.2 up and 0.2 right + 0.2 down (any facing). */
|
||||
const BULLET_OFFSETS = Object.freeze([[0, 0], [-0.2, 0.2], [0.2, -0.2]]);
|
||||
/** PRTS 桑特拉 备注 "不论是普通攻击还是协同攻击,弹道飞行速度均为50". */
|
||||
const LINK_SPEED = 50;
|
||||
const SHIELD_TYPES = Object.freeze(['phys', 'arts', 'true']);
|
||||
const KEY_BARRIER = 'radian:s1:barrier';
|
||||
const TAG_BULLET = 'radian:bullet';
|
||||
const TAG_LINK = 'radian:link';
|
||||
|
||||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||||
const skillOn = (u, id) => !!(u && u.skill && u.skill.active && u.skill.id === id);
|
||||
const tokTalent = (def, key) => (def?.talents ?? []).find((t) => t && t.bb && t.bb[key] != null) ?? null;
|
||||
|
||||
/**
|
||||
* 鼓舞 (ba.inspire "获得额外附加的基础属性加成(同类属性取最高)"): +`val` of `stat` after the target's own multipliers (the flat
|
||||
* mod compensated), the strongest source kept — the keys and rule of 魔王's / 浊心斯卡蒂's 鼓舞.
|
||||
*/
|
||||
function inspire(battle, target, val, src, stat) {
|
||||
if (!(val > 0) || !up(target) || target.mem?.noInspire) return;
|
||||
const key = stat === 'atk' ? 'inspire' : `inspire:${stat}`;
|
||||
const cur = target.findBuff(key);
|
||||
if (cur && cur.data && cur.data.src !== src.id && cur.data.val > val && cur.timeLeft > 0.1) return;
|
||||
const { add, mul } = aggregateMods(target.buffs.filter((b) => b.key !== key));
|
||||
const f = stat === 'def' ? Math.max(0, 1 + (add.defPct ?? 0)) * (mul.defMul ?? 1)
|
||||
: stat === 'hp' ? Math.max(0, 1 + (add.hpPct ?? 0)) * (mul.hpMul ?? 1)
|
||||
: Math.max(0, 1 + (add.atkPct ?? 0)) * (mul.atkMul ?? 1);
|
||||
const flat = f > 1e-6 ? val / f : val;
|
||||
const mods = stat === 'def' ? { defFlat: flat } : stat === 'hp' ? { hpFlat: flat } : { atkFlat: flat };
|
||||
battle.addBuff(target, { key, mods, duration: INSPIRE_IV + 0.25, refresh: 'replace', source: src, status: 'inspire', data: { src: src.id, val } });
|
||||
}
|
||||
|
||||
/** 律动线's 屏障: hp_ratio × the holder's max HP, until the skill ends. */
|
||||
function barrier(battle, u, owner, ratio) {
|
||||
if (!(ratio > 0) || !up(u)) return;
|
||||
const left = owner.skill?.active ? owner.skill.timeLeft : 0;
|
||||
if (!(left > 0)) return;
|
||||
battle.addBuff(u, { key: KEY_BARRIER, shield: u.s.maxHp * ratio, shieldType: SHIELD_TYPES, duration: left, tags: ['skill'] });
|
||||
battle.fx('shield', { x: u.x, y: u.y, id: u.id });
|
||||
}
|
||||
|
||||
/** 协同攻击: every 桑特拉 of the field that can answer `asker` shoots its link and asks the others in turn (see the header). */
|
||||
function relayLink(battle, asker, target, seen) {
|
||||
for (const r of battle.allyUnits) {
|
||||
if (r.kind !== 'token' || r.defId !== TOWER3 || seen.has(r) || !up(r) || !r.canAct || r.s.flags.disarm || r.atkCd > 1e-9) continue;
|
||||
if (!r.rangeKeySet || !r.rangeKeySet.has(asker.tileR * COLS + asker.tileC)) continue;
|
||||
if (acquireTargets(battle, r, r.profile).length) continue;
|
||||
seen.add(r);
|
||||
r.atkCd = r.s.interval;
|
||||
const amount = r.s.atk * num(tokTalent(r.def, 'prism_atk_scale')?.bb?.prism_atk_scale, 1);
|
||||
battle.addProjectile({
|
||||
from: r, target: asker, speed: LINK_SPEED, visual: 'bolt', source: r, hitDead: true,
|
||||
onHit: () => { if (target.alive) battle.dealDamage(r, target, { amount, type: 'arts', isAttack: true, tags: [TAG_LINK] }); },
|
||||
});
|
||||
battle.fx('link', { x: r.x, y: r.y, id: r.id, to: asker.id });
|
||||
relayLink(battle, r, target, seen);
|
||||
}
|
||||
}
|
||||
|
||||
/** 赛柯: one volley straight ahead (one bullet; three while S2 runs). */
|
||||
function fireVolley(battle, t, owner) {
|
||||
const s2 = skillOn(owner, S2);
|
||||
const life = s2 ? num(t.def.skill?.bb?.['attack@projectile_life_time'], BULLET_LIFE) : BULLET_LIFE;
|
||||
const [fr, fc] = dirVec(t.dir);
|
||||
const list = (t.mem.radianBullets ??= []);
|
||||
for (const [dx, dy] of s2 ? BULLET_OFFSETS : [[0, 0]]) {
|
||||
list.push({ x: t.x + dx, y: t.y + dy, vx: fc * BULLET_SPEED, vy: fr * BULLET_SPEED, life, atk: t.s.atk });
|
||||
}
|
||||
battle.fx(s2 ? 'volley' : 'strike', { x: t.x, y: t.y, id: t.id, n: s2 ? BULLET_OFFSETS.length : 1 });
|
||||
}
|
||||
|
||||
/** Move 赛柯's bullets one tick: the first selectable ground enemy within the radius takes the hit; expired ones vanish. */
|
||||
function flyBullets(battle, t, dt) {
|
||||
const list = t.mem.radianBullets;
|
||||
if (!list || !list.length) return;
|
||||
const keep = [];
|
||||
for (const b of list) {
|
||||
const hit = () => {
|
||||
const near = battle.foesInRadius(b.x, b.y, BULLET_RADIUS).filter((e) => e.alive && !e.isFlying);
|
||||
if (!near.length) return false;
|
||||
near.sort((p, q) => Math.hypot(p.x - b.x, p.y - b.y) - Math.hypot(q.x - b.x, q.y - b.y) || p.spawnSeq - q.spawnSeq);
|
||||
battle.dealDamage(t, near[0], { amount: b.atk, type: 'phys', isAttack: true, tags: [TAG_BULLET] });
|
||||
return true;
|
||||
};
|
||||
if (hit()) continue;
|
||||
const step = Math.min(dt, b.life);
|
||||
b.x += b.vx * step;
|
||||
b.y += b.vy * step;
|
||||
b.life -= step;
|
||||
if (hit()) continue;
|
||||
if (b.life > 1e-9) keep.push(b);
|
||||
}
|
||||
t.mem.radianBullets = keep;
|
||||
}
|
||||
|
||||
/** Her summons' kit: 禁疗, her skills on them (stats, 屏障), 鼓舞 at each deployment, 赛柯's bullets, 桑特拉's 协同攻击. */
|
||||
function towerKit(t, owner, opts) {
|
||||
return {
|
||||
skill: null,
|
||||
talents: [],
|
||||
trait: t.defId === TOWER2 ? { noAttack: true } : null,
|
||||
install(battle, s) {
|
||||
battle.addBuff(s, { key: 'radian:tower:abnormal', flags: { noHeal: true }, persist: true, allowDead: true }); // 持有禁疗
|
||||
battle.on('deploy', (c) => {
|
||||
if (c.unit !== s) return;
|
||||
s.mem.radianFireCd = 0;
|
||||
opts.sync(s); // her running skill first: the 鼓舞's flat part is set against its multipliers
|
||||
opts.inspireOne(s);
|
||||
if (skillOn(owner, S1)) barrier(battle, s, owner, opts.hpRatio); // 技能期间部署的戴乌在部署时也会获得屏障 (with its 鼓舞 HP)
|
||||
}, { owner: s });
|
||||
if (s.defId === TOWER2) {
|
||||
battle.on('tick', (c) => {
|
||||
const dt = c.dt ?? battle.dt;
|
||||
flyBullets(battle, s, dt);
|
||||
if (!up(s) || !s.canAct) return;
|
||||
s.mem.radianFireCd = Math.max(0, (s.mem.radianFireCd ?? 0) - dt);
|
||||
if (s.s.flags.disarm || s.mem.radianFireCd > 1e-9) return;
|
||||
fireVolley(battle, s, owner);
|
||||
s.mem.radianFireCd = s.s.interval;
|
||||
}, { owner: s });
|
||||
}
|
||||
if (s.defId === TOWER3) {
|
||||
battle.on('attack', (c) => {
|
||||
if (c.attacker !== s) return;
|
||||
const target = (c.targets || []).find((e) => e && e.side === 'enemy' && e.alive);
|
||||
if (target) relayLink(battle, s, target, new Set([s]));
|
||||
}, { owner: s });
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
char_4195_radian: (bb, chess) => {
|
||||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||||
const t0 = talentBb({ talents: chess?.talentsBase ?? chess?.talents }, 0); // 卡带里的灵感: cnt (the module copies' IS keys ignored)
|
||||
const t1 = talentBb({ talents: chess?.talentsBase ?? chess?.talents }, 1); // 加油~: atk / def / max_hp
|
||||
const picked = chess?.skill?.skillId ?? null;
|
||||
const tokenId = skillRec(chess, picked)?.overrideTokenKey ?? null;
|
||||
const hpRatio = num(b1.hp_ratio);
|
||||
const gain = (unit, n) => unit.mem.summonDeck?.gain(n);
|
||||
return {
|
||||
skills: {
|
||||
[S1]: {
|
||||
kind: 'duration', mods: { defPct: num(b1.def) },
|
||||
onStart({ battle, unit }) {
|
||||
gain(unit, num(b1.cnt, 1)); // 技能开启时获得1个召唤物
|
||||
barrier(battle, unit, unit, hpRatio);
|
||||
for (const t of unit.mem.summonDeck?.standing() ?? []) barrier(battle, t, unit, hpRatio);
|
||||
},
|
||||
onEnd({ battle, unit }) {
|
||||
for (const u of [unit, ...(unit.mem.summonDeck?.pieces() ?? [])]) if (u.findBuff(KEY_BARRIER)) battle.removeBuff(u, KEY_BARRIER);
|
||||
},
|
||||
},
|
||||
[S2]: { kind: 'duration', mods: { atkPct: num(b2.atk) }, onStart({ unit }) { gain(unit, num(b2.cnt, 1)); } },
|
||||
[S3]: { kind: 'duration', mods: { atkPct: num(b3.atk) }, onStart({ unit }) { gain(unit, num(b3.cnt, 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 tdef = tokenId ? battle.tokenDef(tokenId, unit) : null;
|
||||
const extra = num(tokTalent(tdef, 'max_deploy_count')?.bb?.max_deploy_count, 0);
|
||||
const ratios = { atk: num(t1.atk), def: num(t1.def), hp: num(t1.max_hp) };
|
||||
const inspireOne = (s) => {
|
||||
if (!up(unit)) return;
|
||||
inspire(battle, s, unit.s.atk * ratios.atk, unit, 'atk');
|
||||
inspire(battle, s, unit.s.def * ratios.def, unit, 'def');
|
||||
inspire(battle, s, unit.s.maxHp * ratios.hp, unit, 'hp');
|
||||
};
|
||||
const sync = (s) => {
|
||||
if (!s.alive) return;
|
||||
holdBuff(battle, s, 'radian:s1:tower', skillOn(unit, S1), { defPct: num(b1.def) });
|
||||
holdBuff(battle, s, 'radian:s2:tower', skillOn(unit, S2), { atkPct: num(b2.atk) });
|
||||
holdBuff(battle, s, 'radian:s3:tower', skillOn(unit, S3), { atkPct: num(b3.atk) });
|
||||
};
|
||||
const opts = { inspireOne, sync, hpRatio };
|
||||
summonDeck(battle, unit, {
|
||||
tokenIds: TOWERS,
|
||||
charge: cnt,
|
||||
cap: num(tokenId ? tokenStat(battle, unit, tokenId, 'deckStack') : cnt, cnt) + num(tokTalent(tdef, 'max_deck_stack_cnt')?.bb?.max_deck_stack_cnt, 0),
|
||||
maxDeployed: num(tokenId ? tokenStat(battle, unit, tokenId, 'deployLimit') : 1, 1) + extra,
|
||||
kit: (t) => towerKit(t, unit, opts),
|
||||
});
|
||||
// her skills on the standing summons at each start / end (a summon deployed meanwhile syncs itself); 鼓舞 again
|
||||
const onSkill = (c) => {
|
||||
if (c.unit !== unit) return;
|
||||
for (const s of unit.mem.summonDeck.standing()) { sync(s); inspireOne(s); }
|
||||
};
|
||||
battle.on('skillStart', onSkill, { owner: unit });
|
||||
battle.on('skillEnd', onSkill, { owner: unit });
|
||||
// 加油~: every second
|
||||
if (ratios.atk > 0 || ratios.def > 0 || ratios.hp > 0) {
|
||||
battle.every(INSPIRE_IV, () => { for (const s of unit.mem.summonDeck.standing()) inspireOne(s); }, { owner: unit });
|
||||
}
|
||||
} },
|
||||
// 加油~ lives in the deck's install above (its 鼓舞 at each deployment and every second)
|
||||
],
|
||||
install(battle, unit) {
|
||||
// S3 手牵手: before each damage of her attacks / of her summons' damage on an enemy, 停顿 + 法术脆弱
|
||||
if (picked !== S3) return;
|
||||
const slug = num(b3.sluggish), dur = num(b3['weak[magic][limit]']), frag = num(b3.damage_scale, 1) - 1;
|
||||
battle.on('hit', (c) => {
|
||||
const src = c.source, e = c.target;
|
||||
if (!src || !e || e.side !== 'enemy' || !e.alive || !c.dmg || c.dmg.type === 'element' || !skillOn(unit, S3)) return;
|
||||
const mine = src === unit ? !!c.dmg.isAttack : (src.kind === 'token' && src.ownerUnit === unit && TOWERS.includes(src.defId));
|
||||
if (!mine) return;
|
||||
if (slug > 0) battle.applyStatus(e, 'sluggish', { duration: slug, source: src });
|
||||
if (frag > 0 && dur > 0) battle.applyStatus(e, 'artsFragile', { duration: dur, value: frag, source: src });
|
||||
}, { owner: unit, priority: 100 });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
// server/sim/content/kits/shared/summoner.js — the 召唤师 (summoner) branch in 卫戍: the summon deck the 自选 kits of 麦哲伦
|
||||
// (ops/op-mgllan.js), 令 (ops/op-ling.js) and 电弧 (ops/op-radian.js) share — the holding (持有), the return of a placed piece
|
||||
// that left the field, recalls (回收) and the summons vanishing with their owner. Kit contract and rules: ../README.md.
|
||||
//
|
||||
// Sources: the client's battle data (charpack char_248_mgllan / char_2023_ling / char_4195_radian: Talents/1
|
||||
// charge_token[born] — RechargeToken `cnt` at every deployment of the owner; CommonAbilities die_to_kill_token — KillTokens
|
||||
// when the owner leaves; the SUM-Y equips mgllan_equip_2_* / ling_equip_1_*: trigger_charge_token — RechargeToken the
|
||||
// trait's `cnt` once, on the undeployed card (`_attachPassiveBuffsOnDummy`); the token decks: max_deck_stack_cnt /
|
||||
// max_deploy_count of the tokens' hidden talent, deckStack / deployLimit of the token records); PRTS 分支特性信息 召唤师
|
||||
// ("干员离场后,附属的召唤物随之消失"); PRTS 卫戍协议/帮助 §作战阶段 (placed summons: "所有手动部署的召唤物,无视所属干员的持有
|
||||
// 状态,不消耗持有数量,作战开始时立即部署一次", "若战场区初始部署有召唤物,若召唤物在战斗期间退场,将在满足条件后立即原地再部署1个").
|
||||
//
|
||||
// The deck (`summonDeck`):
|
||||
// - holding: `start` on the undeployed card (SUM-Y's +3), then +`charge` at each deployment of the owner, at most `cap`
|
||||
// (the token deck: 5, +3 with SUM-Y); a skill's "获得1个召唤物" and a recall add 1 (deck.gain / deck.recall);
|
||||
// - the battle start deploys every placed piece for free without touching the holding (the engine's initial deployment);
|
||||
// - a piece that leaves the field — knocked out, recalled, absorbed, withdrawn with its owner — comes back on its own tile
|
||||
// once "满足条件": its owner on the field [ASSUMED: as in the base game, a summon is deployed only while its owner
|
||||
// stands], its redeploy time (the token's respawnTime) passed since it left, one held (used up), fewer than
|
||||
// `maxDeployed` of the owner's pieces standing ("最多同时部署3个") and its DP cost paid [ASSUMED: "满足条件" = what a manual
|
||||
// deployment needs; the pieces of 夜莺 / 鸿雪 / 凯尔希 pay too];
|
||||
// - the owner leaving (knocked out or withdrawn) takes its standing pieces off the field with it (KillTokens — withdrawn
|
||||
// here, no knock-down: 'retreat' [ASSUMED]); they wait for its next deployment like any piece that left.
|
||||
// `onLeave(piece, kind, ctx)` tells the kit why a piece left: 'killed', 'recall', 'absorb' (令's 弦惊 merge), 'owner' (with
|
||||
// its owner), else the engine's removal reason.
|
||||
|
||||
import { num, up } from './tier1.js';
|
||||
|
||||
/** Seconds between two tries of a piece waiting to come back (its owner off the field, timer, holding, DP, tile). */
|
||||
export const DECK_RETRY = 0.25;
|
||||
|
||||
/**
|
||||
* Keep buff `key` with `mods` on `u` while `want` (replaced when the mods change, removed when not wanted) — the owner's
|
||||
* timed skills acting on its summons, held every tick so a summon deployed during the skill takes them too.
|
||||
*/
|
||||
export function holdBuff(battle, u, key, want, mods, extra = null) {
|
||||
const has = u.findBuff(key);
|
||||
if (!want) { if (has) battle.removeBuff(u, key); return; }
|
||||
const cur = has?.mods ?? {};
|
||||
if (has && Object.keys(mods).length === Object.keys(cur).length && Object.keys(mods).every((k) => cur[k] === mods[k])) return;
|
||||
battle.addBuff(u, { key, mods, tags: ['skill'], ...extra });
|
||||
}
|
||||
|
||||
/**
|
||||
* A field of a summon's token record at its owner's form and module (data/backups.json `tokens[id].variants[<charId>@
|
||||
* <status>]`, `byModule[uniEquipId]`): the stats the normalised def drops (`deployLimit`, `deckStack`).
|
||||
* @param {object} battle
|
||||
* @param {object} owner the summoner unit
|
||||
* @param {string} tokenId
|
||||
* @param {string} key a `stats` field
|
||||
*/
|
||||
export function tokenStat(battle, owner, tokenId, key) {
|
||||
const raw = battle.data?.rawToken?.(tokenId) ?? null;
|
||||
const v = raw?.variants?.[owner?.def?.tokenOwner] ?? null;
|
||||
const mod = owner?.def?.loadout?.diy?.uniEquipId ?? owner?.def?.loadout?.moduleId ?? null;
|
||||
const st = (mod && v?.byModule?.[mod]?.stats) || v?.stats || raw?.stats || null;
|
||||
return st && Number.isFinite(st[key]) ? st[key] : (Number.isFinite(raw?.[key]) ? raw[key] : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the summon deck of `owner` (see the header) and hand its placed pieces their kit.
|
||||
* @param {object} battle
|
||||
* @param {object} owner the summoner unit
|
||||
* @param {{ tokenIds: Iterable<string>, charge: number, start?: number, cap: number, maxDeployed?: number,
|
||||
* kit?: ((piece: object) => object) | null, onLeave?: ((piece: object, kind: string, ctx: object) => void) | null }} opts
|
||||
* @returns {object} the deck: { held, cap, charge, maxDeployed, pieces(), standing(), gain(n), recall(t), withdraw(t, kind) }
|
||||
*/
|
||||
export function summonDeck(battle, owner, { tokenIds, charge, start = 0, cap, maxDeployed = Infinity, kit = null, onLeave = null }) {
|
||||
const ids = new Set(tokenIds);
|
||||
const c = Math.max(0, Math.floor(num(cap, 0)));
|
||||
const deck = {
|
||||
held: Math.min(c, Math.max(0, Math.floor(num(start, 0)))),
|
||||
cap: c,
|
||||
charge: Math.max(0, Math.floor(num(charge, 0))),
|
||||
maxDeployed: num(maxDeployed, Infinity) > 0 ? num(maxDeployed, Infinity) : Infinity,
|
||||
/** Every placed piece of the owner (on the field or waiting). */
|
||||
pieces: () => battle.allyUnits.filter((t) => t.kind === 'token' && t.ownerUnit === owner && ids.has(t.defId)),
|
||||
/** The pieces on the field. */
|
||||
standing: () => deck.pieces().filter(up),
|
||||
/** "获得N个召唤物": +n held, at most the cap. */
|
||||
gain(n = 1) { deck.held = Math.min(deck.cap, deck.held + Math.max(0, Math.floor(num(n, 0)))); },
|
||||
/** Take a standing piece off the field for `kind` (it comes back like any piece that left). */
|
||||
withdraw(t, kind) {
|
||||
if (!up(t)) return false;
|
||||
t.mem.deckLeave = kind;
|
||||
battle.retreat(t, { reason: 'retreat' });
|
||||
return true;
|
||||
},
|
||||
/** 回收: the piece goes back to the holding (+1) and leaves the field (its redeploy timer runs: `_switchToDeadState`). */
|
||||
recall(t) {
|
||||
if (!up(t)) return false;
|
||||
deck.gain(1);
|
||||
battle.fx('disappear', { x: t.x, y: t.y, id: t.id });
|
||||
return deck.withdraw(t, 'recall');
|
||||
},
|
||||
};
|
||||
owner.mem.summonDeck = deck;
|
||||
|
||||
/** The piece comes back on its tile once the conditions are met (see the header); one waiting loop per leave. */
|
||||
const waitAndReturn = (t) => {
|
||||
const seq = (t.mem.deckSeq = (t.mem.deckSeq ?? 0) + 1);
|
||||
battle.every(DECK_RETRY, (b, sched) => {
|
||||
if (t.mem.deckSeq !== seq || t.alive || t.removed || b.finished) { sched.cancel(); return; }
|
||||
if (!up(owner) || b.time + 1e-9 < t.mem.deckReadyAt || deck.held < 1) return;
|
||||
if (deck.standing().length >= deck.maxDeployed) return;
|
||||
if (b.redeploy(t, { free: false })) {
|
||||
deck.held = Math.max(0, deck.held - 1);
|
||||
sched.cancel();
|
||||
}
|
||||
}, { owner: t });
|
||||
};
|
||||
|
||||
for (const t of deck.pieces()) {
|
||||
if (t.alive || t.deployed) continue;
|
||||
if (kit) {
|
||||
if (t.kit) battle.offOwner(t); // a piece set up before its owner: drop its generic kit's hooks
|
||||
battle._setupUnit(t, kit(t));
|
||||
}
|
||||
// a piece that left stays its tile's piece (`removed` false: Battle.redeploy accepts it, its hooks are kept)
|
||||
battle.on('death', (ctx) => {
|
||||
if (ctx.unit !== t || battle.finished) return;
|
||||
const kind = t.mem.deckLeave ?? (ctx.reason === 'killed' ? 'killed' : ctx.reason);
|
||||
t.mem.deckLeave = null;
|
||||
t.removed = false;
|
||||
t.mem.deckReadyAt = battle.time + Math.max(0, num(t.base.respawnTime));
|
||||
if (onLeave) battle._safe(() => onLeave(t, kind, ctx), 'summonDeck.onLeave', t);
|
||||
waitAndReturn(t);
|
||||
}, { owner: t, priority: -10 });
|
||||
}
|
||||
|
||||
// charge_token[born]: +charge at every deployment of the owner (the battle start's included)
|
||||
battle.on('deploy', (ctx) => { if (ctx.unit === owner) deck.gain(deck.charge); }, { owner, priority: 100 });
|
||||
// die_to_kill_token: "干员离场后,附属的召唤物随之消失"
|
||||
battle.on('death', (ctx) => {
|
||||
if (ctx.unit !== owner) return;
|
||||
for (const t of deck.standing()) deck.withdraw(t, 'owner');
|
||||
}, { owner, priority: 50 });
|
||||
return deck;
|
||||
}
|
||||
Reference in new issue
Block a user