0.2.5-dev: community round 30 and follow-ups

- Stock: fixed trait gifts (缪尔赛思 / 变形者集群 变形同构体, 耶拉 不融冰, 蜜蜡 / 卡涅利安 迅捷作战粮, 诗怀雅 盟约之币) still occupy stock but are granted past the cap; an operator card already shown in the shop or a reward can be taken after the pool runs out and overdraws (GitHub #498, the owner's decision); item buys and item rewards keep the stock check. 洛洛 / 焰尾 sub-pools use the measured weights (PR #497 by @2321Robin; the occupancy idea of PR #496 by @2321Robin re-implemented differently).
- Boss pairs are drawn at random once per match and fixed to battle start; the Hidden Core reuses them; a leaver does not split a full pair (the owner's decision).
- 阿戈尔 in 联防 no longer re-applies the opening devour HP loss or a false revive; Touch's team medic stays for the match after its holder leaves (owner-approved, official lifetime unverified).
- Auto-skill ranges: 司霆惊蛰 S2, 斩业星熊 S3 against air, 予愿安洁莉娜 S3; 自在's post-rebirth invulnerability is also untargetable (#510); Horn / Clementia / catapult / 忍冬 first hit (PR #502 by @Xinglan233); 不融冰 / tear gas trigger on real damage.
- Bond layer-up sound in round start / draft / prep / battle (#482): one channel, a new cue replaces the old, no extra cooldown; a prep update that adds layers and activates a bond plays one cue.
- Client: multi-swing fix (#506), refresh single-flight (PR #507 by @robll-v1), LP tower keeps the settled value with a −N tick (PR #504 by @robll-v1), export with only 潜能 / 练度 changes (PR #503 by @Xinglan233), spectator rosters list DIY members (PR #505 by @Xinglan233), banned members beside bond tooltips (PR #494 by @YuriRestia), recovery action above the away overlay (PR #511 by @RainoutOvO), the /dev/sim-check.html consistency page (PR #492 by @Cloudnyco), Korean (PR #490 by @D-Roote) and English Victoria wording (PR #499 by @YuriRestia).
- golden: 45 of 287 scenarios move against 0.2.4 (roster 17, bonds 3, fields 6, matches 17, standins 2), each attributed in docs/history/0.2.5.md §30.9 / §30.16.
This commit is contained in:
sganggs committed 2026-10-12 14:07:08 +08:00
1 parent bc50cac01f
commit 221e6b8dd2
134 files changed
+4590 -1201

No files matched your search

+3 -2
View File
@@ -6,7 +6,7 @@
// Installed on Match.prototype by server/match/Match.js (a method container: never instantiated; `this` is the match).
import { PHASE, GEO } from '../../../shared/constants.js';
import { deriveSeed } from '../../sim/rng.js';
import { createRng, deriveSeed } from '../../sim/rng.js';
import { buildBossWave, bountySpawns } from '../waves.js';
import { pairPlayers, bossPoolHp, SharedBossPool, CreditPool, hiddenEligible, BOSS_HIT_STEPS } from '../finalAssault.js';
import { FieldRunner, HeadlessPacer, syntheticResult, BOSS_SILENCE_MS, CATCHUP_TICKS_PER_INTERVAL } from '../fields.js';
@@ -66,7 +66,8 @@ export class MatchBoss {
},
});
this.bossPool = pool;
const groups = pairPlayers(alive);
const groups = pairPlayers(alive, createRng(deriveSeed(this.seed, 'boss-pairs')), this.bossPairs);
this.bossPairs = groups.map(g => g.map(p => p.playerId));
const reuse = this.bossWaves && this.bossWaves.length === groups.length && this.bossWaves.every((w, i) => w.players.join() === groups[i].map((p) => p.playerId).join());
this.fields = groups.map((g, i) => {
const solo = this.isSolo || g.length === 1;
+4 -1
View File
@@ -88,8 +88,11 @@ export class MatchInfra {
}
humans() { return this.order.filter((p) => !p.isBot && !p.left); }
/** [ASSUMED] Touch's initial, match-long team grant survives its holder's elimination/quit; focus alone grants none. */
teamMedicGranted() { return this.order.some(p => p.bandId === 'band_amedic'); }
/**
* The boss-round group of a player (`bossWaves`: the seat pairs of finalAssault.js pairPlayers, planned in startRound
* The boss-round group of a player (`bossWaves`: the per-match random pairs, planned in startRound
* before the players' round start) and its side — 'L', or 'R' for the second player of a pair (the mirrored right
* half) — or null outside a boss round / for a player without a field.
* @returns {{ wave: object, players: string[], side: 'L'|'R' } | null}
+6 -2
View File
@@ -7,6 +7,7 @@
import { PHASE, ERR } from '../../../shared/constants.js';
import { buildNormalWave, buildBossWave } from '../waves.js';
import { pairPlayers } from '../finalAssault.js';
import { createRng, deriveSeed } from '../../sim/rng.js';
import { botPickBand } from '../bot.js';
import { OK, fail, DELAYS, BAND_TURN_SECONDS } from './common.js';
@@ -288,11 +289,14 @@ export class MatchPhases {
for (const ps of this._viewers()) this._followScout(ps);
}
/** The boss round's fields (seat pairs of the alive players) and their templates, generated for the prep preview. */
/** Stable per-match pairs and their templates, generated before the prep preview / board validation. */
_planBossWaves() {
const r = this.round;
const bossId = r === this.gd.hiddenRound && r !== this.gd.bossRound ? this.hiddenBossId : this.bossId;
this.bossWaves = pairPlayers(this.alivePlayers()).map((g) => ({
// Pairing uses an independent seeded draw, consuming no shop, wave, draft or battle randomness.
const groups = pairPlayers(this.alivePlayers(), createRng(deriveSeed(this.seed, 'boss-pairs')), this.bossPairs);
this.bossPairs = groups.map(g => g.map(p => p.playerId));
this.bossWaves = groups.map((g) => ({
players: g.map((p) => p.playerId),
wave: buildBossWave(this.gd, this.rngWaves, this.factions, r, { bossId, solo: this.isSolo || g.length === 1 }),
}));
+2 -1
View File
@@ -86,6 +86,7 @@ export class MatchViews {
hiddenBossId: this.hiddenBossId,
bossRound: this.gd.bossRound,
hiddenRound: this.gd.hiddenRound,
bossPairs: this.bossWaves ? this.bossWaves.map(w => w.players.slice()) : this.bossPairs ? this.bossPairs.map(ids => ids.slice()) : null,
spRound: this.gd.spRounds().includes(this.round),
// DESIGN §14: 'client' = battles are simulated by the browsers (b.start specs), 'server' = legacy streaming
combatMode: this.clientCombat ? 'client' : 'server',
@@ -248,7 +249,7 @@ export class MatchViews {
* battleStart, so the prep view always uses the reserve medic, regardless of the current elite count. The stage's
* normal-board slot is transformed to the correct half of a boss field by _onBossHalf / the client prep camera. */
prepMapChars(ps) {
if (!ps?.alive || this.phase !== PHASE.PREP || !this.order.some((p) => p.alive && p.bandId === 'band_amedic')) return [];
if (!ps?.alive || this.phase !== PHASE.PREP || !this.teamMedicGranted()) return [];
const id = 'char_605_cmedic';
const slot = this.stage?.mapChars?.find((m) => m?.key === id && Array.isArray(m.pos) && m.pos[0] >= 9 && m.pos[0] <= 12
&& m.pos[1] >= 0 && m.pos[1] <= 10 && !/multi_only/.test(String(m.alias)));