feedback1 integrate: WS 坚固维式重锤's lock before the M3茧甲 revive (PRIO_RESPAWN); 休露丝 discount used only by shop purchases (F2 not reproduced)
This commit is contained in:
commit
8e42500bb6
7 files changed
+295
-11
No files matched your search
+1
-1
@@ -316,7 +316,7 @@ Every handler method is `(ctx, ev)`; `ev` is shared by all handlers of one dispa
|
||||
| `onSold` | a chess was sold (after refund of copies) | `{ piece, gain }` — **write `ev.gain`** to change the funds paid |
|
||||
| `onRefresh` | manual refresh | `{ slots, free, price }` (slots mutable, or use `ctx.setShopSlot`) |
|
||||
| `onPrice` | every price query of a shop slot (views + buy) — must be **pure** | `{ slot, kind, id, price }` — write `ev.price` / `ctx.setPrice` / `ctx.modifyPrice` |
|
||||
| `onBuy` | after a purchase | `{ piece, slot, price, kind }` (`piece` = owned result, elite after a merge) |
|
||||
| `onBuy` | after a shop purchase (`g.buy` only) | `{ piece, slot, price, kind }` (`piece` = owned result, elite after a merge). The "购买" counters — 休露丝 首名<谢拉格> 1 资金, 玛恩纳 每购买一名<卡西米尔>, 升华 / 整备 "接下来购买的…" — hook it, so a free pick (promotion reward, 寻呼模块 / 信标 / 凯瑟琳 offers: `g.reward`) or a grant (紧急调度券, 简易通讯机, 拟态物质, strategies) never counts as a purchase (player report F2 after 0.1.0; PRTS 帮助 describes the promotion reward as a 0-cost temporary shop — whether the official 雪域礼赠 counts that pick is unknown, [ASSUMED] no) |
|
||||
| `onMerge` | chess or item merge | `{ kind, piece, baseId|itemId, consumed:[uid], area? }` — a chess merge's `area` is where its elite went: 'board' (a consumed copy's tile) \| 'hand' \| 'temp' |
|
||||
| `onLevelUp` | shop level up | `{ level, price }` |
|
||||
| `onSpend` | a payment's action is complete (buy / refresh / levelUp / reward / effect) | `{ amount, reason, total }` (`total` = funds spent this match) |
|
||||
|
||||
+1
-1
@@ -618,7 +618,7 @@ registration order. `battle.off(handle)` / `battle.off(name, fn)` / `battle.offO
|
||||
| `elementHit` | `{ source, target, dmg }` | before a gauge fill (`dmg.type === 'element'`); mutate `dmg.amount`/`dmg.mul`, set `dmg.cancel` |
|
||||
| `damaged` | `{ source, target, amount, type, dmg, credit }` | after application (`amount` may be 0 when shielded); element fills too (with their source); 无来源: `source` null, `credit` set |
|
||||
| `heal` | `{ source, target, amount, opts }` | mutable `amount` |
|
||||
| `fatal` | `{ unit, source, credit, dmg, amount, prevented }` | HP would reach 0 — set `prevented` (substitutes, 不屈, 复活 items) |
|
||||
| `fatal` | `{ unit, source, credit, dmg, amount, prevented }` | HP would reach 0 — set `prevented` (substitutes, kit savers, 不死 / 复活 items, 埃芒加德; 不屈 is a `death` hook). Fired by every HP loss of a unit without a boss pool — hits of any type, element bursts, 无来源 damage, `loseHp` 流失. Order: kits' own savers (10 … −60) → items' 不死 (坚固维式重锤, `PRIO_REVIVE` −100; substitutes −100) → items' 复活 (M3茧甲, `PRIO_RESPAWN` −101: PRTS "复活" acts on a knock-out, which a 不死 prevents) → 埃芒加德 (−110) |
|
||||
| `kill` | `{ killer, victim }` | victim HP reached 0 (a handler may revive by restoring HP) |
|
||||
| `death` | `{ unit, reason:'killed'|'leak'|'retreat'|'merchant'|'expired'|'forcedExit', killer }` | unit removed (`'forcedExit'`: an operator entering 联防 knocked out, §1.1) |
|
||||
| `skillStart` / `skillEnd` | `{ unit, skill, reason }` | mutate `skill.ammoLeft` / `skill.timeLeft` in skillStart |
|
||||
|
||||
@@ -40,7 +40,7 @@ import { weaknessRetype, addShieldLayer, PRIO_REVIVE } from '../items/battle.js'
|
||||
import { spawnMapChar } from '../tokens.js';
|
||||
|
||||
export const AMEDIC_BAND = 'band_amedic';
|
||||
/** 'fatal' priority of 埃芒加德: after the operators' own items (PRIO_REVIVE) and every talent / skill saver. */
|
||||
/** 'fatal' priority of 埃芒加德: after the operators' own items (PRIO_REVIVE / PRIO_RESPAWN) and every talent / skill saver. */
|
||||
export const PRIO_BAND_REVIVE = PRIO_REVIVE - 10;
|
||||
|
||||
const keyOf = (bandId, part = '') => `band:${bandId}${part ? `:${part}` : ''}`;
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
// band_coin_cost_gain_random_char_by_shop_level 绮良 every coin_cnt funds spent → count random chess ≤ shop level
|
||||
// up_shop_next_refresh_must_present_bond_char 佩佩 level-up to a level of lvlist → +1 free refresh (price 0) that prefers <bond>
|
||||
// gain_bond_char_per_round {round,preround,bond} 哈洛德 rounds round, round+preround, …: a <bond> chess (≤ shop level, else any tier)
|
||||
// first_buy_in_round_char_price_change {price,bond} 休露丝 first <bond> chess of the round costs `price`
|
||||
// first_buy_in_round_char_price_change {price,bond} 休露丝 first <bond> chess of the round costs `price` (bought in the
|
||||
// shop: onBuy — a free pick / grant never uses it up)
|
||||
// band_cost_coin_reach_cnt_gain_chess_from_pool 潘格尼尼 once coin_cnt funds spent in total: 1 chess of pool (elite 拉特兰 ≥ T4)
|
||||
// round_start_bond_check_gain_layer 余 round start of `round`: exactly factioncount active bonds → +count1, else each active +count2
|
||||
// up_shop_add_special_goods {count,choice,pool} 凯瑟琳 every level-up: pick 1 of `count` items of pool (free)
|
||||
|
||||
@@ -19,11 +19,14 @@
|
||||
// core bond is active). Combo partners (bbStr.equip_chess_id / other_equip) are item keys without the _a/_b suffix:
|
||||
// either quality satisfies them; lent items count as carried. Both are evaluated live, at the moment of use.
|
||||
//
|
||||
// Hook priorities (non-default): 'fatal' — consumable death savers (坚固维式重锤's first lethal hit, M3茧甲 revives)
|
||||
// run LAST (PRIO_REVIVE −100: "被击倒时" = nothing else prevented the knock-down, so a skill's / talent's own undying
|
||||
// (kits use 10 … −60) never wastes a charge); 骑士戒律's free in-skill undying runs early (20). Flat damage reduction
|
||||
// 'hit' −10 (after the other damage modifiers). Proc damage dealt by items carries the tag 'item' and never
|
||||
// re-triggers item procs.
|
||||
// Hook priorities (non-default): 'fatal' — consumable death savers run LAST, so a skill's / talent's own undying (kits
|
||||
// use 10 … −60) never wastes a charge: 坚固维式重锤's first-lethal-hit lock (异常效果 不死) at PRIO_REVIVE −100, then the
|
||||
// M3茧甲 revive at PRIO_RESPAWN −101 — PRTS 卫戍协议:盟约 下半/PRTS盟约记录 备注 "“复活”的实现方式为:受益者因移动之外的
|
||||
// 原因退场时下次部署的再部署时间和费用归零": a revive acts on a knock-out, which a 不死 prevents, so the lock always comes
|
||||
// first whatever the equip order (player report F1 after 0.1.0: with the 茧甲 equipped first the revive ran first and
|
||||
// the first lethal hit showed no lock); 埃芒加德's band revive follows (bands/battle.js PRIO_BAND_REVIVE −110).
|
||||
// 骑士戒律's free in-skill undying runs early (20). Flat damage reduction 'hit' −10 (after the other damage modifiers).
|
||||
// Proc damage dealt by items carries the tag 'item' and never re-triggers item procs.
|
||||
//
|
||||
// 蒸汽之心 (Victoria carrier) — LIVE model: the hammer types (灼燃/坚固/加速/战栗维式重锤) carried by the owner's operators
|
||||
// that are on the field right now (cached ≤ 0.5 s, dropped whenever an operator deploys or leaves) are granted to the
|
||||
@@ -60,6 +63,8 @@ const chance = (battle, p) => p > 0 && (p >= 1 || battle.rng() < p);
|
||||
const ENEMY_RECORD = /^enemy_/;
|
||||
/** 'fatal' priorities (see header). */
|
||||
export const PRIO_REVIVE = -100;
|
||||
/** The items' 复活 (M3茧甲): acts on a knock-out, so after every 不死 (PRIO_REVIVE) — see header. */
|
||||
export const PRIO_RESPAWN = PRIO_REVIVE - 1;
|
||||
const PRIO_FREE_UNDYING = 20;
|
||||
|
||||
/** Stat keys of a stat buff → mods (直接乘算, see header). */
|
||||
@@ -267,7 +272,11 @@ function hammerAcquire(battle, rt, u) {
|
||||
if (t && t.side === 'enemy' && t.alive && chance(battle, pr)) battle.applyStatus(t, 'tremble', { duration: num(p.disarmed_duration, 2), source: u });
|
||||
}
|
||||
});
|
||||
// 坚固: first lethal hit per battle ⇒ HP never below 1 for undeadable_duration × m s
|
||||
// 坚固: first lethal hit per battle ⇒ HP never below 1 for undeadable_duration × m s. Per battle [ASSUMED]: the text
|
||||
// only says 首次 (M3茧甲 says 一场战斗, 拉特兰桥夹 每次部署); the hammer state lives on the unit, which a redeploy
|
||||
// reuses, so a carrier knocked out after its lock comes back without one. Per deployment would reset
|
||||
// undyingUsed / undyingUntil on the carrier's own non-initial 'deploy'. Any lethal HP loss sets it off, an ally's (the
|
||||
// 阿戈尔 battle-start devour, "造成5000点物理伤害") or the carrier's own (源石溶剂) included.
|
||||
S.on('fatal', (c) => {
|
||||
if (c.unit !== u || c.prevented) return;
|
||||
if (battle.time < hs.undyingUntil) { c.prevented = true; return; }
|
||||
@@ -562,7 +571,7 @@ const BY_ITEM = {
|
||||
c.prevented = true;
|
||||
u.hp = u.s.maxHp;
|
||||
fxOn(battle, 'revive', u, 'item:chess_item_4_12_e', rec.id, { left: max - used });
|
||||
}, PRIO_REVIVE);
|
||||
}, PRIO_RESPAWN); // after the hammer's 不死 lock, whatever the equip order (header)
|
||||
},
|
||||
// 催泪瓦斯: on attack prob ⇒ 1 麻痹 stack
|
||||
chess_item_5_01_e(battle, u, rec, S) {
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// Player report F1 after 0.1.0 (2026-10-03): "维多利亚坚固锤子的锁血没生效" — 坚固维式重锤 (chess_item_3_09_e_a / _b):
|
||||
// "首次受到致命伤害时生命值不低于1,持续8秒" (activity_table act2autochess eff_acarm043 / eff_acgarm043, blackboard
|
||||
// undeadable_duration 8; PRTS 卫戍协议:盟约 下半/PRTS盟约记录 备注 "持有不死" — 异常效果 不死 UNDEADABLE "重设常规生命值时不会
|
||||
// 使其低于1"). The lock itself works for every kind of lethal damage (pinned here on synthetic and real operators). One
|
||||
// ordering fault is fixed: with M3茧甲 equipped before the hammer, its revive ran first at the same priority and the first
|
||||
// lethal hit showed no lock. PRTS (same page, M3茧甲 / 埃芒加德 / 阿戈尔 备注): "“复活”的实现方式为:受益者因移动之外的原因退场时
|
||||
// 下次部署的再部署时间和费用归零" — a revive acts on a knock-out (退场), which a 不死 prevents, so the lock always comes first
|
||||
// (items/battle.js PRIO_RESPAWN). Pinned too, as the ways a lock can look missing: one lock per battle [ASSUMED] (a
|
||||
// redeployed carrier has none) and the 阿戈尔 battle-start devour spending it.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, chessRec, enemyRec } from '../helpers/battleHarness.js';
|
||||
import { PRIO_REVIVE, PRIO_RESPAWN } from '../../server/sim/content/items/battle.js';
|
||||
import { PRIO_BAND_REVIVE } from '../../server/sim/content/bands/battle.js';
|
||||
|
||||
const HAMMER = 'chess_item_3_09_e_a';
|
||||
const HAMMER_B = 'chess_item_3_09_e_b';
|
||||
const M3 = 'chess_item_4_12_e_a';
|
||||
const SOLVENT = 'chess_item_1_05_e_a';
|
||||
|
||||
/** Synthetic operator: 2000 HP, 500 ATK, 200 DEF, 1 s attacks, 20 s redeploy. */
|
||||
const op = (id) => chessRec({
|
||||
id, profession: 'WARRIOR', bonds: [], tier: 1, rangeGrid: [[0, 0], [0, 1]],
|
||||
stats: { maxHp: 2000, atk: 500, def: 200, res: 10, aspd: 100, bat: 1, respawnTime: 20, spRecovery: 1, blockCnt: 2 },
|
||||
skill: { spCost: 60, duration: 5, initSp: 0 },
|
||||
});
|
||||
|
||||
/** One synthetic carrier at (10, 4) with `items` and a harmless static dummy at (10, 9). */
|
||||
function carrier(items) {
|
||||
const h = makeBattle({
|
||||
defs: { chess: { t_op: op('t_op') }, enemies: { e_d: enemyRec({ key: 'e_d', hp: 1e7, speed: 0 }) } },
|
||||
units: [{ chessId: 't_op', row: 10, col: 4, items }],
|
||||
enemies: [{ key: 'e_d', pos: [10, 9] }], timeLimit: 999, autoFinish: false,
|
||||
});
|
||||
h.step(1);
|
||||
return { h, u: h.unit('t_op'), e: h.b.enemies.find((x) => x.alive) };
|
||||
}
|
||||
|
||||
/** fx events of `kind` on `u` (the hammer's lock: 'undying'; M3茧甲: 'revive'). */
|
||||
function fxLog(h, u) {
|
||||
const log = [];
|
||||
const fx0 = h.b.fx.bind(h.b);
|
||||
h.b.fx = (kind, p) => { if (p && p.id === u.id) log.push([kind, Math.round(h.b.time * 100) / 100, p.src ?? null]); return fx0(kind, p); };
|
||||
return log;
|
||||
}
|
||||
|
||||
test('F1 坚固维式重锤: every kind of lethal damage is held at ≥ 1 HP for 8 s (hit, burst, 流失, 无来源, own drain)', () => {
|
||||
const kinds = {
|
||||
phys: ({ h, u, e }) => h.b.dealDamage(e, u, { amount: 1e7, type: 'phys', canDodge: false }),
|
||||
arts: ({ h, u, e }) => h.b.dealDamage(e, u, { amount: 1e7, type: 'arts', canDodge: false }),
|
||||
true: ({ h, u, e }) => h.b.dealDamage(e, u, { amount: 1e7, type: 'true', canDodge: false }),
|
||||
sourceless: ({ h, u, e }) => h.b.dealDamage(e, u, { amount: 1e7, type: 'true', canDodge: false, sourceless: true }),
|
||||
// a full 神经 gauge bursts on the carrier: stun, then 1000 无来源 true damage (sim/constants.js ELEMENT)
|
||||
burst: ({ h, u, e }) => { u.hp = 5; h.b.dealDamage(e, u, { type: 'element', element: 'neural', amount: 5000 }); },
|
||||
hpLoss: ({ h, u, e }) => h.b.loseHp(u, 1e7, { source: e }),
|
||||
hpLossSourceless: ({ h, u, e }) => h.b.loseHp(u, 1e7, { source: e, sourceless: true }),
|
||||
};
|
||||
for (const [name, hit] of Object.entries(kinds)) {
|
||||
for (const id of [HAMMER, HAMMER_B]) {
|
||||
const c = carrier([id]);
|
||||
const log = fxLog(c.h, c.u);
|
||||
hit(c);
|
||||
assert.ok(c.u.alive && c.u.hp >= 1, `${name} ${id}: the first lethal ${name} leaves ≥ 1 HP`);
|
||||
assert.deepEqual(log.filter((x) => x[0] === 'undying').map((x) => x[2]), ['item:hammer'], `${name}: the hammer's lock`);
|
||||
c.h.run(7.8);
|
||||
for (let i = 0; i < 3; i++) c.h.b.dealDamage(c.e, c.u, { amount: 1e7, type: 'true', canDodge: false });
|
||||
assert.ok(c.u.alive && c.u.hp >= 1, `${name} ${id}: still held inside the 8 s`);
|
||||
c.h.run(0.4);
|
||||
c.h.b.dealDamage(c.e, c.u, { amount: 1e7, type: 'true', canDodge: false });
|
||||
assert.equal(c.u.alive, false, `${name} ${id}: knocked out after the 8 s`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('F1 坚固维式重锤: the carrier\'s own 源石溶剂 drain cannot finish it inside the lock', () => {
|
||||
const c = carrier([HAMMER, SOLVENT]);
|
||||
c.u.hp = 1;
|
||||
c.h.run(1.05); // the drain's first tick (60 per second) is lethal at 1 HP
|
||||
assert.ok(c.u.alive && c.u.hp >= 1, 'held by the lock');
|
||||
c.h.run(6.5);
|
||||
assert.ok(c.u.alive, 'every drain tick inside the 8 s is held');
|
||||
c.h.run(2.5);
|
||||
assert.equal(c.u.alive, false, 'the first tick after the 8 s knocks it out');
|
||||
});
|
||||
|
||||
// [ASSUMED] scope, an open question for the user: the official text says only 首次 (no 一场战斗 / 每次部署), and a
|
||||
// carrier knocked out after its lock comes back without one — the likeliest reading of "锁血没生效" (forced-hammer bot
|
||||
// matches, 3 绝境 + 4 困难: 27 of 382 carrier knock-outs came in a later deployment with the lock spent). Per deployment
|
||||
// would flip the third assertion below.
|
||||
test('F1 坚固维式重锤: one lock per battle (a redeploy does not re-arm it [ASSUMED]); a new battle re-arms it', () => {
|
||||
const c = carrier([HAMMER]);
|
||||
const hit = () => c.h.b.dealDamage(c.e, c.u, { amount: 1e7, type: 'true', canDodge: false });
|
||||
hit();
|
||||
assert.ok(c.u.alive);
|
||||
c.h.run(8.2);
|
||||
hit();
|
||||
assert.equal(c.u.alive, false, 'knocked out after the window');
|
||||
c.h.runUntil(() => c.u.alive, 60);
|
||||
assert.ok(c.u.alive, 'redeployed');
|
||||
hit();
|
||||
assert.equal(c.u.alive, false, 'the second deployment of the battle has no lock left');
|
||||
const next = carrier([HAMMER]);
|
||||
next.h.b.dealDamage(next.e, next.u, { amount: 1e7, type: 'true', canDodge: false });
|
||||
assert.ok(next.u.alive && next.u.hp >= 1, 'the next battle locks again');
|
||||
});
|
||||
|
||||
// The other way a lock is gone before the enemies hit: the 阿戈尔 battle-start devour ("吞噬身前一格干员对其造成5000点物理
|
||||
// 伤害") is a lethal hit on the fodder, so a hammer carrier in front of an 阿戈尔 survives it at 1 HP and spends its one
|
||||
// lock at t = 0 (in forced-hammer 绝境 bot matches 10 of 179 locks went this way). Kept: the text calls it damage and
|
||||
// 异常效果 不死 holds any HP reset at 1 [ASSUMED that the official engine does the same].
|
||||
test('F1 坚固维式重锤 in front of an 阿戈尔: the battle-start devour spends the lock (fodder kept at 1 HP for 8 s)', () => {
|
||||
const g = (id) => chessRec({ id, bonds: ['egirShip'], profession: 'WARRIOR', skill: null, stats: { atk: 1000, maxHp: 10000, def: 0, blockCnt: 2 } });
|
||||
const h = makeBattle({
|
||||
defs: { chess: { g1_a: g('g1_a'), g2_a: g('g2_a'), g3_a: g('g3_a'), t_op: op('t_op') }, enemies: { e_d: enemyRec({ key: 'e_d', hp: 1e7, speed: 0 }) } },
|
||||
units: [{ chessId: 'g1_a', row: 10, col: 3 }, { chessId: 't_op', row: 10, col: 4, items: [HAMMER] }, { chessId: 'g2_a', row: 12, col: 3 }, { chessId: 'g3_a', row: 12, col: 5 }],
|
||||
bonds: { egirShip: { count: 3, active: true, tier: 1, layers: 0 } },
|
||||
enemies: [{ key: 'e_d', pos: [9, 9] }], timeLimit: 999, autoFinish: false,
|
||||
});
|
||||
const u = h.unit('t_op');
|
||||
const log = fxLog(h, u);
|
||||
h.step(1);
|
||||
assert.ok(u.alive && u.hp >= 1 && u.hp < 2, 'devoured (5000 > 2000 HP) but held at 1 HP');
|
||||
assert.deepEqual(log.filter((x) => x[0] === 'undying').map((x) => [x[1], x[2]]), [[0, 'item:hammer']], 'the lock fired at t = 0');
|
||||
const e = h.b.enemies.find((x) => x.alive);
|
||||
h.run(8.2);
|
||||
h.b.dealDamage(e, u, { amount: 1e7, type: 'true', canDodge: false });
|
||||
assert.equal(u.alive, false, 'the first enemy kill after the window finds no lock left');
|
||||
});
|
||||
|
||||
test('F1 坚固维式重锤 + M3茧甲: the lock comes before the revive whatever the equip order (PRTS: a revive acts on 退场)', () => {
|
||||
assert.ok(PRIO_RESPAWN < PRIO_REVIVE && PRIO_RESPAWN > PRIO_BAND_REVIVE, 'revive items: after every 不死, before 埃芒加德');
|
||||
for (const items of [[M3, HAMMER], [HAMMER, M3]]) {
|
||||
const c = carrier(items);
|
||||
const log = fxLog(c.h, c.u);
|
||||
const hit = () => c.h.b.dealDamage(c.e, c.u, { amount: 1e7, type: 'true', canDodge: false });
|
||||
hit();
|
||||
assert.ok(c.u.alive && c.u.hp < 2, `${items}: the first lethal hit is held at 1 HP (lock), not revived to full`);
|
||||
assert.deepEqual(log.map((x) => x[0]), ['undying'], `${items}: lock first`);
|
||||
c.h.run(8.2);
|
||||
hit();
|
||||
assert.ok(c.u.alive, `${items}: after the 8 s the 茧甲 revives`);
|
||||
assert.equal(Math.round(c.u.hp), 2000, 'full HP');
|
||||
assert.deepEqual(log.map((x) => x[0]), ['undying', 'revive']);
|
||||
hit();
|
||||
assert.equal(c.u.alive, false, `${items}: nothing left`);
|
||||
}
|
||||
});
|
||||
|
||||
test('F1 坚固维式重锤 on a real operator in a real fight: 角峰 blocking scaled-up real enemies stands 8 s at 1 HP', () => {
|
||||
for (const id of [HAMMER, HAMMER_B]) {
|
||||
const h = makeBattle({
|
||||
stageId: 'flat', seed: 7,
|
||||
units: [{ chessId: 'chess_char_1_02_a', row: 9, col: 5, items: [id] }],
|
||||
enemies: Array.from({ length: 6 }, (_, i) => ({ key: 'enemy_1422_lrsldr', time: i * 0.5, route: 0, mods: { atkMul: 30, hpMul: 50 } })),
|
||||
timeLimit: 120, autoFinish: false,
|
||||
});
|
||||
const u = h.unit('chess_char_1_02_a');
|
||||
const log = fxLog(h, u);
|
||||
let out = null;
|
||||
h.b.on('death', (c) => { if (c.unit === u && out == null) out = h.b.time; });
|
||||
h.runUntil(() => out != null, 60);
|
||||
const lock = log.find((x) => x[0] === 'undying');
|
||||
assert.ok(lock && lock[2] === 'item:hammer', `${id}: the hammer locked (${JSON.stringify(log)})`);
|
||||
assert.ok(out != null && out >= lock[1] + 8 - 1e-6, `${id}: knocked out ${out} ≥ 8 s after the lock at ${lock[1]}`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
// Player report F2 after 0.1.0 (2026-10-03): with 休露丝 【雪域礼赠】 ("每回合购买的首名<谢拉格>干员消耗资金为1",
|
||||
// band_sciurus first_buy_in_round_char_price_change {price 1, bond kjeragShip}) a <谢拉格> taken for free — the promotion
|
||||
// reward's 3-choose-1 after a merge, an item's pick-one offer or grant — was said to use up the round's discount. Checked
|
||||
// through real intents (g.buy / g.reward / g.equip on a real Match with the real registry): only a shop purchase (g.buy →
|
||||
// onBuy) counts; every free acquisition leaves the discount for the next <谢拉格> bought. Same rule for 玛恩纳 【业务指标】
|
||||
// ("每购买一名<卡西米尔>干员,下回合开始时资金+1"). Pinned here (the report did not reproduce on 0.1.0).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeMatch, give, giveItem, DATA } from './harness.js';
|
||||
|
||||
const OK = { ok: true };
|
||||
const KJ = (id) => !!(DATA.chess[id] && DATA.chess[id].bonds.includes('kjeragShip'));
|
||||
const slot = (id) => ({ kind: 'chess', id, basePrice: DATA.chess[id].tier === 1 ? 2 : DATA.chess[id].tier <= 4 ? 3 : 4, frozen: false, sold: false });
|
||||
const NOT_KJ_T1 = Object.values(DATA.chess).find((c) => c.visible && !c.isGolden && c.tier === 1 && !c.bonds.includes('kjeragShip') && !c.bonds.includes('kazimierzShip')).chessId;
|
||||
|
||||
/** Solo match at R1 prep with an empty board, 50 funds, shop level 3 and `band`; the shop = `shop` chess ids. */
|
||||
function setup(band, shop) {
|
||||
const h = makeMatch({ mode: 'solo', humans: 1, seed: 3 }).start();
|
||||
h.toPrep(1);
|
||||
const m = h.m;
|
||||
const ps = h.ps('p_0');
|
||||
for (const p of [...ps.board.values(), ...ps.hand.filter(Boolean)]) if (p.kind === 'chess') ps.returnCopies(p);
|
||||
ps.board.clear(); ps.hand.fill(null); ps.offers.length = 0;
|
||||
ps.funds = 50; ps.counters = {}; ps.pendingFunds = 0; ps.shop.level = 3; ps.bandId = band;
|
||||
ps.shop.slots = shop.map(slot);
|
||||
ps.recompute();
|
||||
const viewPrice = (i) => { m.flush(true); return h.lastTo('p_0', 'm.private').shop.slots[i].price; };
|
||||
return { h, m, ps, viewPrice };
|
||||
}
|
||||
|
||||
/** After a free acquisition: the shop's <谢拉格> at `i` still costs 1 (server and m.private), a purchase pays 1, the next costs full. */
|
||||
function discountIntact({ m, ps, viewPrice }, i, j, what) {
|
||||
assert.equal(ps.priceOf(ps.shop.slots[i]), 1, `${what}: the shop <谢拉格> still costs 1`);
|
||||
assert.equal(viewPrice(i), 1, `${what}: m.private shows 1`);
|
||||
const f0 = ps.funds;
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.buy', slot: i }), OK);
|
||||
assert.equal(f0 - ps.funds, 1, `${what}: the first <谢拉格> bought pays 1`);
|
||||
assert.equal(ps.priceOf(ps.shop.slots[j]), ps.shop.slots[j].basePrice, `${what}: the second <谢拉格> of the round costs full`);
|
||||
}
|
||||
|
||||
test('F2 休露丝: a <谢拉格> taken from the promotion reward after a merge keeps the round\'s discount', () => {
|
||||
const s = setup('band_sciurus', ['chess_char_3_11_a', 'chess_char_3_14_a', NOT_KJ_T1]);
|
||||
give(s.m, s.ps, NOT_KJ_T1); give(s.m, s.ps, NOT_KJ_T1);
|
||||
assert.deepEqual(s.m.handle('p_0', { t: 'g.buy', slot: 2 }), OK, 'the third copy merges');
|
||||
const offer = s.ps.offers[0];
|
||||
assert.ok(offer && offer.source === 'merge', 'promotion reward');
|
||||
if (!offer.slots.some((x) => KJ(x.id))) offer.slots[0].id = 'chess_char_3_20_a';
|
||||
const idx = offer.slots.findIndex((x) => KJ(x.id));
|
||||
assert.deepEqual(s.m.handle('p_0', { t: 'g.reward', idx }), OK);
|
||||
assert.ok(s.ps.hand.some((p) => p && p.kind === 'chess' && KJ(p.id)), 'the <谢拉格> joined the hand');
|
||||
discountIntact(s, 0, 1, 'promotion reward');
|
||||
});
|
||||
|
||||
test('F2 休露丝: 寻呼模块\'s pick-one and the free grants of items (拟态物质, 紧急调度券, 简易通讯机) keep the discount', () => {
|
||||
// 寻呼模块 on 银灰 (only <谢拉格>): three <谢拉格> ≤ shop level offered, one taken for free
|
||||
{
|
||||
const s = setup('band_sciurus', ['chess_char_3_11_a', 'chess_char_3_14_a']);
|
||||
const t = give(s.m, s.ps, 'chess_char_4_22_a');
|
||||
const it = giveItem(s.m, s.ps, 'chess_item_4_01_e_a');
|
||||
assert.deepEqual(s.m.handle('p_0', { t: 'g.equip', itemUid: it.uid, targetUid: t.uid }), OK);
|
||||
const offer = s.ps.offers[0];
|
||||
assert.ok(offer && offer.slots.length && offer.slots.every((x) => KJ(x.id)), `寻呼模块 offers <谢拉格> (${offer && offer.slots.map((x) => x.id)})`);
|
||||
assert.deepEqual(s.m.handle('p_0', { t: 'g.reward', idx: 0 }), OK);
|
||||
discountIntact(s, 0, 1, '寻呼模块');
|
||||
}
|
||||
// 拟态物质 on one of two 角峰: the third 角峰 (a merge) and its promotion reward
|
||||
{
|
||||
const s = setup('band_sciurus', ['chess_char_3_11_a', 'chess_char_3_14_a']);
|
||||
give(s.m, s.ps, 'chess_char_1_02_a');
|
||||
const t = give(s.m, s.ps, 'chess_char_1_02_a');
|
||||
const it = giveItem(s.m, s.ps, 'chess_item_5_05_e_a');
|
||||
assert.deepEqual(s.m.handle('p_0', { t: 'g.equip', itemUid: it.uid, targetUid: t.uid }), OK);
|
||||
assert.ok([...s.ps.hand, ...s.ps.board.values()].some((p) => p && p.id === 'chess_char_1_02_b'), 'elite 角峰');
|
||||
const offer = s.ps.offers[0];
|
||||
if (!offer.slots.some((x) => KJ(x.id))) offer.slots[0].id = 'chess_char_3_20_a';
|
||||
assert.deepEqual(s.m.handle('p_0', { t: 'g.reward', idx: offer.slots.findIndex((x) => KJ(x.id)) }), OK);
|
||||
discountIntact(s, 0, 1, '拟态物质');
|
||||
}
|
||||
// 紧急调度券: a shop operator taken for free (here one of three <谢拉格>)
|
||||
{
|
||||
const s = setup('band_sciurus', ['chess_char_3_11_a', 'chess_char_3_14_a', 'chess_char_3_20_a']);
|
||||
const t = give(s.m, s.ps, NOT_KJ_T1);
|
||||
const it = giveItem(s.m, s.ps, 'chess_item_2_02_e_a');
|
||||
assert.deepEqual(s.m.handle('p_0', { t: 'g.equip', itemUid: it.uid, targetUid: t.uid }), OK);
|
||||
const left = s.ps.shop.slots.map((x, i) => (x && !x.sold ? i : -1)).filter((i) => i >= 0);
|
||||
assert.equal(left.length, 2, 'one shop operator taken');
|
||||
discountIntact(s, left[0], left[1], '紧急调度券');
|
||||
}
|
||||
// 简易通讯机 on 银灰: a random <谢拉格> granted
|
||||
{
|
||||
const s = setup('band_sciurus', ['chess_char_3_11_a', 'chess_char_3_14_a']);
|
||||
const t = give(s.m, s.ps, 'chess_char_4_22_a');
|
||||
const it = giveItem(s.m, s.ps, 'chess_item_2_06_e_a');
|
||||
assert.deepEqual(s.m.handle('p_0', { t: 'g.equip', itemUid: it.uid, targetUid: t.uid }), OK);
|
||||
assert.equal(s.ps.hand.filter((p) => p && p.kind === 'chess' && KJ(p.id)).length, 2, 'a <谢拉格> granted');
|
||||
discountIntact(s, 0, 1, '简易通讯机');
|
||||
}
|
||||
});
|
||||
|
||||
test('F2 玛恩纳 业务指标: only a shop purchase of a <卡西米尔> pays the next round\'s +1 (a free reward pick does not)', () => {
|
||||
const kz = Object.values(DATA.chess).filter((c) => c.visible && !c.isGolden && c.tier <= 3 && c.bonds.includes('kazimierzShip')).map((c) => c.chessId);
|
||||
const s = setup('band_mlynar', [kz[0], kz[1], NOT_KJ_T1]);
|
||||
s.ps.pushRewardOffer('merge', { ids: [kz[2] ?? kz[0]] });
|
||||
assert.deepEqual(s.m.handle('p_0', { t: 'g.reward', idx: 0 }), OK);
|
||||
assert.equal(s.ps.pendingFunds, 0, 'a free <卡西米尔> pays nothing');
|
||||
assert.deepEqual(s.m.handle('p_0', { t: 'g.buy', slot: 0 }), OK);
|
||||
assert.equal(s.ps.pendingFunds, 1, 'a bought <卡西米尔>: +1 next round');
|
||||
});
|
||||
Reference in new issue
Block a user