0.2.5-dev: 萨卡兹枯朽战士 (and every enemy sharing its death fog) leaves no 污秽侵蚀 when it dies silenced — the owner's official-play recall, against the PRTS / in-game wording (#470); the 调度中心 5 / 6 promotion reward includes 自选 operators of the newly reached tier (#515, after research; the owner's decision); golden: unchanged

This commit is contained in:
sganggs committed 2026-10-12 15:24:07 +08:00
1 parent a8ff5e7c1d
commit 9b37a68fcf
15 files changed
+157 -49

No files matched your search

+6 -6
View File
@@ -70,12 +70,12 @@ test('stats-only enemies attach no ability and fight with their data stats', ()
}
});
// Abilities the client makes silenceable although their handbook line is NORMAL (the battle prefab's buff template checks
// SILENCED): 萨卡兹枯朽战士 / 组长's PollutedDie = template projectile_on_killed, the 死亡爆炸 template of 高能源石虫's
// SILENCE-flagged blast too (community report of 2026-10-06, item 10; DESIGN §25.18.2)
// 萨卡兹枯朽战士 / 组长's PollutedDie is silenced even though the handbook line is NORMAL and the PRTS / in-game text
// says 「不可沉默」. The owner's official-play recall of 2026-10-12 (GitHub #470) is that a death while silenced
// creates no fog. Only these two keys share that ability (DESIGN §30.20; 0.2.0's client-template reading is §25.18.2).
const SILENCE_BY_TEMPLATE = new Set(['enemy_1267_nhpbr', 'enemy_1267_nhpbr_2']);
test('沉默: exactly the abilities whose handbook line is SILENCE-flagged can be silenced (every authored enemy), plus the ones whose client template checks it', () => {
test('沉默: exactly the abilities whose handbook line is SILENCE-flagged can be silenced (every authored enemy), plus 萨卡兹枯朽战士 / 组长\'s death fog', () => {
const h = arena();
h.step();
for (const key of [...Object.keys(KITS), ...Object.keys(BOSS_KITS)]) {
@@ -768,8 +768,8 @@ for (const key of ['enemy_1267_nhpbr', 'enemy_1267_nhpbr_2']) {
approx(h.unit('t_wall').stats.taken, 3 * tb(key, 'PollutedDie.polluted_damage_low'));
});
// PRTS 特殊机制 死亡爆炸 "默认可沉默…若不处于沉默状态,将会…释放"; client template projectile_on_killed: CheckAbnormalFlag
// SILENCED (unset) before EmitProjectile (community report of 2026-10-06, item 10: a silenced one still poisoned)
// Owner's official-play recall of 2026-10-12 (GitHub #470): a death while the silence icon is already showing
// creates no fog. That contradicts the PRTS / in-game text 「不可沉默」. The same PollutedDie ability is on both keys.
test(`${nm(key)}: silenced when it dies, it releases no 污染秽蚀; a silence that ran out does not stop it`, () => {
const h = arena({ units: [{ chessId: 't_wall', row: 10, col: 6 }] });
h.step();
+35 -9
View File
@@ -2,7 +2,8 @@
// the picks (room.diy → seats[].diy) are checked leniently, fixed for the match and applied to that player only — the
// player's data view makes a slotted slot the operator (name, class, bonds from its factions, no 特质, the pick's skill
// and module; the slot's tier, price and merge), each slotted piece has its own stock (8 at tier 5, 5 at tier 6), never
// enters the shared pool, joins only its player's shop once the 调度中心 reaches the slot's level, leaves the shop when
// enters the shared pool, joins only its player's shop once the 调度中心 reaches the slot's level, joins that player's
// promotion reward at the reward tier (one shop level earlier: shop 4 → tier 5, shop 5 → tier 6), leaves the shop when
// every bond of it is banned, merges 3 → elite, and fields its operator in battle (PlayerBattleInput `diy`), client-run
// and server-run identical; prep scouting and the result name the operator.
import { test } from 'node:test';
@@ -177,16 +178,35 @@ test('buy → 3 copies merge into the elite (the stock pays the copies); sell /
assert.equal(p0.allChess().filter(p => p0.gd.baseIdOf(p.id) === T5A).reduce((n, p) => n + p.poolCopies, 0), 9);
assert.equal(p0.diyStock.left(T5A), 0, 'the extra copy does not reopen available stock');
h.invariants();
// reward offers (the temporary refresh) draw it once the 调度中心 is at the slot's level
// The promotion reward draws 自选 of the reward tier (shop 4 → tier 5, shop 5 → tier 6), one shop level before the
// ordinary shop (owner 2026-10-12, #515). clear() returned the drained T5A copies; the loops roll this stock.
clear(p0);
p0.offers = [];
let hit = 0;
p0.shop.level = 4;
for (let i = 0; i < 200; i++) { const o = p0.pushRewardOffer('merge'); if (o && o.slots.some((s) => /_diy/.test(s.id))) hit++; p0.offers = []; }
assert.equal(hit, 0, 'a level-4 promotion reward (tier 5) never holds a tier-5 自选 piece');
p0.shop.level = 6;
for (let i = 0; i < 300; i++) { const o = p0.pushRewardOffer('merge'); if (o && o.slots.some((s) => s.id === T6A)) hit++; p0.offers = []; }
assert.ok(hit > 0, 'a level-6 reward can hold the tier-6 piece');
assert.deepEqual(p0.diyStock.snapshot(), { [T5A]: 8, [T5B]: 8, [T6A]: 5 });
const tally = (level, n) => {
p0.shop.level = level;
const c = { t5: 0, t6: 0, any: 0 };
for (let i = 0; i < n; i++) {
const o = p0.pushRewardOffer('merge');
if (o) {
const ids = o.slots.map((s) => s.id);
if (ids.some((id) => id === T5A || id === T5B)) c.t5++;
if (ids.some((id) => id === T6A || id === T6B)) c.t6++;
if (ids.some((id) => /_diy/.test(id))) c.any++;
}
p0.offers = [];
}
return c;
};
assert.equal(tally(3, 80).any, 0, 'a level-3 promotion reward (tier 4) holds no 自选 piece');
const lv4 = tally(4, 200);
assert.ok(lv4.t5 > 0, 'a level-4 promotion reward (tier 5) can hold a tier-5 自选 piece');
assert.equal(lv4.t6, 0, 'a level-4 promotion reward never holds a tier-6 自选 piece');
const lv5 = tally(5, 300);
assert.ok(lv5.t6 > 0, 'a level-5 promotion reward (tier 6) can hold the tier-6 piece');
assert.equal(lv5.t5, 0, 'while tier 6 still has copies, a level-5 reward does not walk down to a tier-5 自选 piece');
const lv6 = tally(6, 300);
assert.ok(lv6.t6 > 0, 'a level-6 reward can still hold the tier-6 piece');
// elimination returns everything
gain(p0, T6A);
p0.eliminate(m.round);
@@ -211,6 +231,12 @@ test('bans: a 自选 piece whose every bond is off this match has no stock (out
h.toPrep(1);
p0.shop.level = 5;
for (let i = 0; i < 300; i++) { p0.rollShop(); assert.ok(!p0.shop.slots.some((s) => s && s.id === T5A)); }
p0.shop.level = 4;
for (let i = 0; i < 40; i++) {
const o = p0.pushRewardOffer('merge');
assert.ok(!o || !o.slots.some((s) => s.id === T5A), 'a banned slot has no stock, so the tier-5 reward never holds it');
p0.offers = [];
}
h.m.flush(true);
assert.deepEqual(h.lastTo('p_0', 'm.private').diyBanned, [T5A]);
h.invariants();