Files
ark-stronghold-protocol/server/sim/content/bonds/addon/meta.js
T

137 lines
6.6 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/bonds/addon/meta.js — prep side of the add-on bonds (research 02 §3.14–§3.17, §3.20).
//
// 助力 deputShip onPrepEnd: every currently active bond +`layer` (tier 2 = 3 助力 by name or elite state:
// +`more_layer`) — buff bond_activated_add_layer
// 远见 visiShip every `layer` layers +`count` funds (bond_layer_gain_coin); first time at `layer1`: 远见 chess cost
// −`discount`; first time at `layer2`: every chess costs −`discount` instead (latched, permanent)
// 奇迹 miraShip manual refresh with no free refresh pending → p = min(1, baseprob + prob·L) the next refresh is free;
// every 100 layers +20 funds (bond_layer_gain_coin). `probk` has no known meaning and is unused.
// 投资人 ×2 / ×3 "获得时" garrisons: EffectDispatcher.investRepeat (server/match/effectsMeta.js).
// 调和 +1 member count for core bonds: server/match/bondsMeta.js.
//
// Milestones read the persistent layers and pay only while the bond is active; payouts owed for layers gained while it
// was inactive ("无需激活" sources) are caught up the next time a hook sees it active (research 02 §8 Q7 [ASSUMED]).
// Funds earned after the prep phase ended (助力 / 寒檀 / 号角 … prep-end layers — the match wipes leftover funds right
// after onPrepEnd) are paid as pending funds, credited at the next round start, so "获得…资金" is never silently lost
// [ASSUMED]; `global:bondaddon_prepend` (first in every dispatch) marks the round whose prep has ended.
// 远见's −1 (「购买价格永久-1资金」) has no floor but 0: a price of 1 (至简 / 红豆's 购买价格为1 特质, run first on onPrice;
// 休露丝's first 谢拉格) becomes 0 — the text names none (owner's decision 2026-10-04; before 0.1.3 research 02 §3.15
// assumed 1).
// Latched state lives in player counters (prefix `bondaddon:`); onPrice only reads them (pure).
const C_VISI_PAID = 'bondaddon:visi:paid';
const C_VISI_DISC = 'bondaddon:visi:disc';
const C_MIRA_PAID = 'bondaddon:mira:paid';
const C_PREP_ENDED = 'bondaddon:prepEndedRound';
/** Hooks on which owed milestones are caught up (besides the bond's own onLayers). */
const SETTLE_HOOKS = Object.freeze(['onRoundStart', 'onPrepStart', 'onPrepEnd', 'onBuy', 'onGain', 'onSold', 'onMerge']);
const n = (v, d = 0) => (typeof v === 'number' && Number.isFinite(v) ? v : d);
/** `{ ...bb, ...bbStr }` of a bond buff from the match data (ctx.data), or {}. */
function buff(ctx, bondId, key) {
const rec = ctx.data && ctx.data.bonds ? ctx.data.bonds[bondId] : null;
for (const b of (rec && Array.isArray(rec.buffs) ? rec.buffs : [])) {
if (b && b.key === key) return { ...(b.bb || {}), ...(b.bbStr || {}) };
}
return {};
}
/** Pay "every `layer` layers +`count` funds" up to the current layers (only while active). */
function settleCoins(ctx, bondId, counterKey, label) {
if (!ctx.bondActive(bondId)) return 0;
const p = buff(ctx, bondId, 'bond_layer_gain_coin');
const step = n(p.layer), count = n(p.count);
if (!(step > 0) || !(count > 0)) return 0;
const due = Math.floor(ctx.layers(bondId) / step);
const paid = ctx.counter(counterKey);
if (due <= paid) return 0;
ctx.setCounter(counterKey, due);
const gain = (due - paid) * count;
if (prepEnded(ctx)) {
ctx.addPendingFunds(gain);
ctx.toast(`【${label}】层数达成,下回合开始时获得${gain}资金`, 'info');
} else {
ctx.addFunds(gain, `bond:${bondId}`);
ctx.toast(`【${label}】层数达成,获得${gain}资金`, 'info');
}
return gain;
}
/** True between this round's onPrepEnd and the end of its PREP phase (leftover funds are about to be wiped). */
const prepEnded = (ctx) => ctx.phase === 'PREP' && ctx.counter(C_PREP_ENDED) === ctx.round;
function latchVisiDiscount(ctx) {
if (!ctx.bondActive('visiShip')) return;
const p = buff(ctx, 'visiShip', 'bond_multi_layer_char_goods_price_bond_discount');
const L = ctx.layers('visiShip');
const cur = ctx.counter(C_VISI_DISC);
const t = L >= n(p.layer2, Infinity) ? 2 : L >= n(p.layer1, Infinity) ? 1 : 0;
if (t > cur) {
ctx.setCounter(C_VISI_DISC, t);
ctx.toast(t >= 2 ? '【远见】所有干员购买价格永久降低' : '【远见】远见干员购买价格永久降低', 'info');
}
}
function visiSettle(ctx) {
settleCoins(ctx, 'visiShip', C_VISI_PAID, '远见');
latchVisiDiscount(ctx);
}
const miraSettle = (ctx) => { settleCoins(ctx, 'miraShip', C_MIRA_PAID, '奇迹'); };
/** A handler object running `fn(ctx)` on every catch-up hook and on the bond's own layer gains. */
function settler(bondId, fn) {
const h = { onLayers(ctx, ev) { if (ev && ev.bondId === bondId) fn(ctx); } };
for (const hook of SETTLE_HOOKS) h[hook] = (ctx) => fn(ctx);
return h;
}
export function registerMeta(registry) {
registry.global('bondaddon_prepend', {
onPrepEnd(ctx) { ctx.setCounter(C_PREP_ENDED, ctx.round); },
});
registry.bond('deputShip', {
onPrepEnd(ctx) {
const me = ctx.bond('deputShip');
if (!me || !me.active) return;
const p = buff(ctx, 'deputShip', 'bond_activated_add_layer');
const add = me.tier >= 2 ? n(p.more_layer, n(p.layer)) : n(p.layer);
if (!(add > 0)) return;
const active = Object.entries(ctx.bonds()).filter(([, b]) => b && b.active).map(([id]) => id);
for (const id of active) ctx.addLayers(id, add, { requireActive: true, reason: 'bond:deputShip' });
},
});
registry.bond('visiShip', {
...settler('visiShip', visiSettle),
onPrice(ctx, ev) {
if (!ev || ev.kind !== 'chess') return;
const t = ctx.counter(C_VISI_DISC);
if (t <= 0) return;
const p = buff(ctx, 'visiShip', 'bond_multi_layer_char_goods_price_bond_discount');
const disc = n(p.discount);
if (!(disc > 0)) return;
if (t < 2) {
const rec = ctx.chessRecord(ev.id);
if (!rec || !Array.isArray(rec.bonds) || !rec.bonds.includes(p.bond || 'visiShip')) return;
}
ctx.modifyPrice(-disc); // never below 0 (ctx.modifyPrice); 1 → 0 like any price (owner's decision 2026-10-04)
},
});
registry.bond('miraShip', {
...settler('miraShip', miraSettle),
onRefresh(ctx, ev) {
if (!ev || ev.trigger || !ctx.bondActive('miraShip')) return;
if (ctx.grantFreeRefresh(0) > 0) return;
const p = buff(ctx, 'miraShip', 'bond_refresh_shop_next_free');
const chance = Math.max(0, Math.min(1, n(p.baseprob) + n(p.prob) * ctx.layers('miraShip')));
if (ctx.rng() < chance) {
ctx.grantFreeRefresh(1);
ctx.toast('【奇迹】下次刷新不消耗资金', 'info');
}
},
});
}