0.2.4-dev: round-29 community fixes and the official shared card pool
- Shared card pool and item stock as in the official mode (the owner's request, from the 路标档案馆 video and public notes): solo low-tier exceptions, co-op operators x2, shared item stock with special-item exceptions, normal / advanced occupancy 1 / 2, re-check on buy, 机变 exhaustion, release on consume / destroy / elimination; 整备 never exceeds stock; invariant held <= cap. - 突袭: one taunt-first reselect right after a revive (the owner's rule); ordinary polls and redeploys keep the 0.2.3 rule; a revive on an enemy's tile stays. #475: online boss / hidden-boss Raid may land on the empty half (owner-limited scope). - 浊心斯卡蒂 S1 x 坚守 no longer bounce redirected damage, and the two shares conserve the hit (idea of PR #464 by @HappyTanuki). - Operator fixes adapted from PRs by @Xinglan233 (#459 哈洛德 S2 elemental trigger, #460 涤火杰西卡 shield on the four adjacent tiles, #461 家族徽章 first hit after stealth, #462 松果 S2 regrow per deploy); 克莱门莎 S2 splash 1.1 / 100% ATK and air auto-trigger (#467); 白面 copying 歌蕾蒂娅 / 余 skips the three-member condition (#473, the owner's video). - 香槟炸弹 is not chosen by friendly auras / heals (PR #468 / #476 by @Siornya); 琳琅诗怀雅 coin balance (PR #450 by @IceCodeNew); Touch prep display (PR #451 by @YinqiMargatroid); lobby header (PR #471 by @YuriRestia); Japanese UI terms (PR #448 by @scutkane). - AI-only boss countdown grace, render-failure cache / recovery without restarting a battle, device units kept on a same-field re-entry, DIY detail / skill previews, large-text result buttons. - golden: battle families equal 0.2.3 except diy-135 / diy-136 (克莱门莎); all 18 release matches move through the shared pool, every flip named in docs/history/0.2.4.md §29.12; two coverage scenarios added.
This commit is contained in:
1 parent
1db8e51023
commit
6bfc08270c
129 files changed
+3477
-1011
No files matched your search
@@ -120,8 +120,13 @@ export class MatchBoss {
|
||||
// overtime drain starts (150 real s), both on the field clock
|
||||
const onClock = (realS) => this.sched.now() + Math.round(((realS * this.gd.combatTimeScale) / this.gameSpeed) * 1000);
|
||||
const levelTime = this.gd.bossLevelTime(this.round);
|
||||
// Maintainer 2026-10-10: only teams with an AI teammate keep the extra grace after the visible countdown.
|
||||
// Freeze this choice for the fight; a disconnect / host transfer cannot change its LP clock midway.
|
||||
const team = this.order.filter((p) => !p.left);
|
||||
this.bossOvertimeStartReal = team.length > 1 && team.some((p) => p.isBot)
|
||||
? this.gd.bossOvertimeAfterReal : (levelTime || this.gd.bossOvertimeAfterReal);
|
||||
this.deadline = this.sched.instant || !levelTime ? 0 : onClock(levelTime);
|
||||
this.overtimeAt = this.sched.instant ? 0 : onClock(this.gd.bossOvertimeAfterReal);
|
||||
this.overtimeAt = this.sched.instant ? 0 : onClock(this.bossOvertimeStartReal);
|
||||
if (this.clientCombat) { this._startFinalClient(hidden); return; }
|
||||
this._defaultWatch();
|
||||
this.markPublic();
|
||||
@@ -480,7 +485,7 @@ export class MatchBoss {
|
||||
* (bossTurnHpReduceTime, gamedata.js bossOvertimeDue).
|
||||
*/
|
||||
_applyOvertime(gt) {
|
||||
const due = this.gd.bossOvertimeDue(gt);
|
||||
const due = this.gd.bossOvertimeDue(gt, this.bossOvertimeStartReal);
|
||||
if (due > this.overtimeApplied) {
|
||||
const loss = due - this.overtimeApplied;
|
||||
this.overtimeApplied = due;
|
||||
|
||||
@@ -16,7 +16,7 @@ const PERSONAL_OFFER_SIZE = 3;
|
||||
|
||||
export class MatchSpDraft {
|
||||
enterSpDraft() {
|
||||
const draft = generateDraft(this.gd, this.rngDraft, this.round, { stageId: this.stageId, bondAvailable: (bondId) => this.bondLive(bondId) });
|
||||
const draft = generateDraft(this.gd, this.rngDraft, this.round, { stageId: this.stageId, bondAvailable: (bondId) => this.bondLive(bondId), itemAvailable: id => this.itemPool.canGain(id) });
|
||||
const alive = this.alivePlayers();
|
||||
if (!draft || !alive.length) { this.enterPrep(); return; }
|
||||
this.phase = PHASE.SP_DRAFT;
|
||||
@@ -38,6 +38,8 @@ export class MatchSpDraft {
|
||||
return s.order[s.idx] ?? null;
|
||||
}
|
||||
|
||||
spCardAvailable(card) { return !!card && this.sp?.taken[card.idx] == null && (card.kind !== 'item' || this.itemPool.canGain(card.id)); }
|
||||
|
||||
startSpTurn() {
|
||||
const s = this.sp;
|
||||
this.cancel(this._turnTimer);
|
||||
@@ -47,7 +49,7 @@ export class MatchSpDraft {
|
||||
if (ps && ps.alive && s.picks[ps.playerId] == null) break;
|
||||
s.idx++;
|
||||
}
|
||||
const available = s.cards.map((c) => c.idx).filter((i) => s.taken[i] == null);
|
||||
const available = s.cards.filter(c => this.spCardAvailable(c)).map(c => c.idx);
|
||||
if (s.idx >= s.order.length || !available.length) { this.setDeadline(0); this.later(0, () => this.finishSpDraft()); return; }
|
||||
const token = ++this._turnToken;
|
||||
if (!s.untimed) {
|
||||
@@ -57,7 +59,7 @@ export class MatchSpDraft {
|
||||
if (this.phase !== PHASE.SP_DRAFT || token !== this._turnToken) return;
|
||||
const ps = this.players.get(this.spTurn());
|
||||
if (!ps) return;
|
||||
const avail = s.cards.map((c) => c.idx).filter((i) => s.taken[i] == null);
|
||||
const avail = s.cards.filter(c => this.spCardAvailable(c)).map(c => c.idx);
|
||||
if (!avail.length) { this.finishSpDraft(); return; }
|
||||
this._applyCard(ps, avail[Math.floor(this.rngDraft() * avail.length)]);
|
||||
});
|
||||
@@ -76,7 +78,7 @@ export class MatchSpDraft {
|
||||
if (this.phase !== PHASE.SP_DRAFT || token !== this._turnToken || !this.sp) return;
|
||||
const ps = this.players.get(this.spTurn());
|
||||
if (!ps || !ps.botControlled) return;
|
||||
const avail = this.sp.cards.map((c) => c.idx).filter((i) => this.sp.taken[i] == null);
|
||||
const avail = this.sp.cards.filter(c => this.spCardAvailable(c)).map(c => c.idx);
|
||||
if (!avail.length) return;
|
||||
this._applyCard(ps, botPickCard(this, ps, this.sp.cards, avail));
|
||||
});
|
||||
@@ -88,14 +90,14 @@ export class MatchSpDraft {
|
||||
if (this.sp.picks[ps.playerId] != null) return fail(ERR.ALREADY);
|
||||
if (this.spTurn() !== ps.playerId) return fail(ERR.NOT_YOUR_TURN);
|
||||
if (!Number.isInteger(idx) || idx < 0 || idx >= this.sp.cards.length) return fail(ERR.BAD_TARGET);
|
||||
if (this.sp.taken[idx] != null) return fail(ERR.SOLD_OUT);
|
||||
if (!this.spCardAvailable(this.sp.cards[idx])) return fail(ERR.SOLD_OUT);
|
||||
this._applyCard(ps, idx);
|
||||
return OK;
|
||||
}
|
||||
|
||||
_applyCard(ps, idx) {
|
||||
const s = this.sp;
|
||||
if (!s || s.picks[ps.playerId] != null || s.taken[idx] != null) return;
|
||||
if (!s || s.picks[ps.playerId] != null || !this.spCardAvailable(s.cards[idx])) return;
|
||||
const card = s.cards[idx];
|
||||
if (!card) return;
|
||||
s.picks[ps.playerId] = idx;
|
||||
@@ -188,24 +190,20 @@ export class MatchSpDraft {
|
||||
const tierList = (lo, hi) => { const out = []; for (let t = lo; t <= hi; t++) for (const id of this.gd.shopItemsByTier[t] || []) out.push(id); return out; };
|
||||
if (p && p.kind === 'equip') {
|
||||
if (Array.isArray(p.weighted) && p.weighted.length) {
|
||||
const pairs = p.weighted.filter((x) => Array.isArray(x) && this.gd.item(x[0]));
|
||||
let total = 0;
|
||||
for (const [, w] of pairs) total += Math.max(0, Number(w) || 0);
|
||||
let r = rng() * total;
|
||||
for (const [id, w] of pairs) { r -= Math.max(0, Number(w) || 0); if (r < 0) return id; }
|
||||
return pairs.length ? pairs[pairs.length - 1][0] : null;
|
||||
const pairs = p.weighted.filter((x) => Array.isArray(x) && this.gd.item(x[0]) && this.itemPool.canGain(x[0]));
|
||||
return this.itemPool.pick(rng, pairs.map(x => x[0]), new Map(pairs));
|
||||
}
|
||||
if (Array.isArray(p.items) && p.items.length) {
|
||||
const items = p.items.filter((id) => this.gd.item(id));
|
||||
return items.length ? items[Math.floor(rng() * items.length)] : null;
|
||||
return this.itemPool.pick(rng, items);
|
||||
}
|
||||
let list;
|
||||
if (Array.isArray(p.tiers) && p.tiers.length) list = p.tiers.flatMap((t) => this.gd.shopItemsByTier[t] || []);
|
||||
else list = tierList(1, p.maxTier === 'shopLevel' ? Math.max(1, Math.min(6, shopLevel)) : 6);
|
||||
return list.length ? list[Math.floor(rng() * list.length)] : null;
|
||||
return this.itemPool.pick(rng, list);
|
||||
}
|
||||
const list = Number.isInteger(tier) ? tierList(tier, tier) : tierList(1, Math.max(1, Math.min(6, maxTier)));
|
||||
return list.length ? list[Math.floor(rng() * list.length)] : null;
|
||||
return this.itemPool.pick(rng, list);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -138,7 +138,8 @@ export class MatchViews {
|
||||
if (this.phase === PHASE.SP_DRAFT && this.sp) {
|
||||
const s = this.sp;
|
||||
v.sp = {
|
||||
family: s.family, name: s.name, desc: s.desc, eventId: s.eventId, cards: s.cards.map(cardView), order: s.order.slice(),
|
||||
family: s.family, name: s.name, desc: s.desc, eventId: s.eventId,
|
||||
cards: s.cards.map(c => ({ ...cardView(c), soldOut: c.kind === 'item' && !this.itemPool.canGain(c.id) })), order: s.order.slice(),
|
||||
turn: this.spTurn(), picks: { ...s.picks }, taken: { ...s.taken }, untimed: !!s.untimed,
|
||||
};
|
||||
}
|
||||
@@ -243,10 +244,29 @@ export class MatchViews {
|
||||
return p ? { charId: p.charId, skillIndex: p.skillIndex, uniEquipId: p.uniEquipId } : undefined;
|
||||
}
|
||||
|
||||
/** The 外勤医疗 reserve medic already standing on each player's field in preparation. Touch replaces it only at
|
||||
* 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 [];
|
||||
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)));
|
||||
const rec = this.gd.token(id);
|
||||
if (!slot || !rec) return [];
|
||||
const dir = slot.dir || 'RIGHT';
|
||||
return [{
|
||||
id: -ps.seat - 1, uid: -ps.seat - 1, kind: 'token', side: 'ally', ownerId: ps.playerId, defId: id,
|
||||
area: 'board', name: rec.name || id, tier: 1, golden: false,
|
||||
spine: rec.assets?.spine || id, avatar: rec.assets?.avatar || id,
|
||||
x: slot.pos[1], y: slot.pos[0], dir, facing: dir === 'LEFT' ? -1 : 1, maxHp: rec.stats?.maxHp || 1,
|
||||
}];
|
||||
}
|
||||
|
||||
/** UnitInfo of a player's board pieces on their (board) tiles — a prep scout's board, the boss partner's (bossMateView).
|
||||
* `area: 'board'` (a bench unit says 'hand' / 'temp': prepFieldMeta). */
|
||||
_prepBoardUnits(ps) {
|
||||
const units = [];
|
||||
const units = this.prepMapChars(ps);
|
||||
// the player's own view of the data (0.2.0 自选编队: its slotted DIY slots are its operators — player/diy.js)
|
||||
const gd = ps.gd || this.gd;
|
||||
for (const { r, c, piece } of boardOrder(ps.board)) {
|
||||
|
||||
Reference in new issue
Block a user