Online co-op browser remake of Arknights' Stronghold Protocol: Alliance (client-simulated combat, official waves/bonds/items/strategies, 3D board, facing wheel, operator loadout, official emotes, deployment scripts).
136 lines
6.4 KiB
JavaScript
136 lines
6.4 KiB
JavaScript
// 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.
|
||
// 远见 discounts never push a price below 1 (research 02 §3.15 [ASSUMED]; a price already below 1 is left alone).
|
||
// 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;
|
||
}
|
||
const price = n(ev.price);
|
||
if (price > 1) ctx.modifyPrice(-Math.min(disc, price - 1));
|
||
},
|
||
});
|
||
|
||
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');
|
||
}
|
||
},
|
||
});
|
||
}
|