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).
141 lines
8.2 KiB
JavaScript
141 lines
8.2 KiB
JavaScript
// 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: deckStack / deployLimit of
|
|
// the token records, which carry the tokens' hidden talent max_deck_stack_cnt / max_deploy_count — tools/build-data.mjs
|
|
// tokenTalentDeckBonus); 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;
|
|
}
|