Playtest #6 fixes: summons placed by hand, boss pool stall, bounties, 联防 leak counter, distinct offers, 机变 card text, phone buy tap, skill triggers, blocking, push force, 孤立, enemies, element bar
- #1 #2 skill / device summons (赫默's drone, 巫恋's doll, 凯瑟琳's devices) are hand cards placed by the player; placed summons deploy once at battle start, then a skill summon re-appears at its tile on each cast (the user's call) - #3 叙拉古 隐匿 ends on time (32 + 0.4 × layers s); 忍冬 S3 迷彩 lasts until her next cast (official), kept apart from 隐匿 - #4 multi-round bounty cards last two battles (the user's call; MULTI_ROUND_BOUNTY_BATTLES), blue / red card text - #5 the shared boss pool's float dust (≈3.6e-12 HP) stalled Final Assault leaders at 0 %; < 1 HP now clears the pool, and the team LP reaching 0 first is a defeat (PRTS) - #6 #10 机变 cards show their official effect text; an invisible 详情 button no longer swallows the confirming tap on phones - #7 live ×N counter of a leaker's enemies still on the 联防 field (LP loss stays capped at 10) - #8 #15 #16 skill triggers per 技能策略: skill-range triggers, injured-ally heals (古米, 塞雷娅), tank take-damage triggers with a 3 s cooldown; 新约能天使 S2 shields the highest-aggro operator in range - #9 official per-enemy model scales (威龙) - #11 element bar under the unit's HP bar (white = 元素值 left, element-coloured refill after a burst), floor rounding - #12 枯朽战车 kit per PRTS; every ranged enemy reaches its target's 0.25 collider - #13 恐惧 wander (萨科塔之翼 at half HP, operator fears make enemies flee) with re-planning after pushes - #14 push / pull distance by 力度 − 重量 (PRTS table); #17 every blocker attacks what it blocks; ASPD floor 20 - #18 炎佑 is 孤立: no heals or ally buffs - #19 every free pick-one chess offer (promotion reward, 信标, 寻呼模块) shows different operators - DESIGN §20 and the normative lines, SIM / META / DATA / PLAYING updated; 2747 unit tests and all 27 browser suites green; dev recordings regenerated
This commit is contained in:
1 parent
e6628f0cfe
commit
6464e4b46a
138 files changed
+7946
-1379
No files matched your search
+46
-5
@@ -23,6 +23,7 @@
|
||||
|
||||
import { Battle } from './Battle.js';
|
||||
import { toDataSource, withUnitLoadouts } from './simdata.js';
|
||||
import { BOSS_POOL_MIN_HP } from './constants.js';
|
||||
|
||||
export const SPEC_VERSION = 1;
|
||||
|
||||
@@ -133,10 +134,14 @@ export function createBattleFromSpec(spec, dataSource, opts = {}) {
|
||||
/**
|
||||
* Client-side view of the shared boss HP pool (DESIGN §14 b.pool): the server owns the pool; this field adds its own
|
||||
* damage locally and shows `server hp − local damage the server has not acknowledged yet`.
|
||||
* damage(playerId, amount) called by the sim; returns the damage dealt (≤ the remaining displayed hp)
|
||||
* damage(playerId, amount) called by the sim; returns the damage dealt (≤ the remaining displayed hp; the hit that
|
||||
* would leave less than BOSS_POOL_MIN_HP (1) takes the rest)
|
||||
* sync(serverHp, ackedCum) a b.pool broadcast: the server's hp and how much of THIS field's cumulative damage
|
||||
* (`cum`) it has already counted
|
||||
* cum / byPlayer cumulative damage of this field (reported as b.progress.bossDmg / .by)
|
||||
* `hp` below 1 reads 0 (the leader is down; user playtest #6 item 5): the difference of the server's float hp and the
|
||||
* local counters can leave dust (3.6e-12) smaller than half an ulp of `cum`, which no hit could remove — `cum += dust`
|
||||
* changes nothing — so the leader stood at "0 HP" and the field never ended.
|
||||
*/
|
||||
export class LocalBossPool {
|
||||
constructor(maxHp, hp = maxHp) {
|
||||
@@ -149,7 +154,10 @@ export class LocalBossPool {
|
||||
this.byPlayer = {};
|
||||
}
|
||||
|
||||
get hp() { return Math.max(0, this.serverHp - Math.max(0, this.cum - this.acked)); }
|
||||
get hp() {
|
||||
const h = this.serverHp - Math.max(0, this.cum - this.acked);
|
||||
return h < BOSS_POOL_MIN_HP ? 0 : h;
|
||||
}
|
||||
|
||||
/** Only used by the sim's fallback path when damage() throws. */
|
||||
set hp(v) {
|
||||
@@ -161,7 +169,7 @@ export class LocalBossPool {
|
||||
const a = Number(amount);
|
||||
const left = this.hp;
|
||||
if (!Number.isFinite(a) || a <= 0 || left <= 0) return 0;
|
||||
const dealt = Math.min(left, a);
|
||||
const dealt = left - a < BOSS_POOL_MIN_HP ? left : a;
|
||||
this.cum += dealt;
|
||||
if (playerId != null) this.byPlayer[playerId] = (this.byPlayer[playerId] || 0) + dealt;
|
||||
return dealt;
|
||||
@@ -198,9 +206,37 @@ export function attachLpMeter(battle) {
|
||||
return meter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 联防 (user playtest #6 item 7): each source player's enemies still standing on a unite field — not spawned yet,
|
||||
* alive, or already through the objective again — i.e. what settlement would charge them if the battle ended now
|
||||
* (server/match/unite.js uniteSurvivors: the unite result's counted leaks + unspawned entries by `sourcePlayerId`;
|
||||
* alive enemies become leaks on a timeout, keyed like Battle._recordLeak). It falls as the helpers strike them down and
|
||||
* rises when one splits or summons (content-spawned children inherit the parent's sourcePlayerId).
|
||||
* `{ [playerId]: n }` (a player with none left is absent), or null for a battle without the sim's state (a stand-in).
|
||||
* @returns {Record<string, number> | null}
|
||||
*/
|
||||
export function uniteLeft(battle) {
|
||||
if (!battle) return null;
|
||||
const out = {};
|
||||
const add = (pid) => { if (typeof pid === 'string' && pid) out[pid] = (out[pid] || 0) + 1; };
|
||||
if (battle.finished && typeof battle.result === 'function') {
|
||||
const r = battle.result();
|
||||
if (!r || !r.perPlayer) return null;
|
||||
for (const pp of Object.values(r.perPlayer)) for (const l of (pp && pp.leaked) || []) if (l && l.counted !== false) add(l.sourcePlayerId);
|
||||
for (const u of r.unspawned || []) if (u) add(u.sourcePlayerId);
|
||||
return out;
|
||||
}
|
||||
if (!battle._perPlayer || !Array.isArray(battle._pending) || !Array.isArray(battle.enemies)) return null;
|
||||
for (const p of battle._pending) if (p) add(p.sourcePlayerId);
|
||||
for (const e of battle.enemies) if (e && e.alive && e.counted) add(e.sourcePlayerId ?? e.ownerId);
|
||||
for (const pp of Object.values(battle._perPlayer)) for (const l of (pp && pp.leaked) || []) if (l && l.counted !== false) add(l.sourcePlayerId);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Progress numbers of a battle for b.progress / the teammates' waiting UI: game time, kills, total, counted leaks
|
||||
* (normal / unite) and the boss pool damage of this field.
|
||||
* (normal / unite), the boss pool damage of this field and — unite fields — `left` (uniteLeft: each leaker's enemies
|
||||
* still standing).
|
||||
*/
|
||||
export function battleProgress(battle) {
|
||||
const r = battle && typeof battle.result === 'function' && battle.finished ? battle.result() : null;
|
||||
@@ -209,7 +245,7 @@ export function battleProgress(battle) {
|
||||
for (const k of Object.keys(pp)) for (const l of pp[k].leaked || []) if (l && l.counted !== false) leaks++;
|
||||
const pool = battle && battle.sharedBoss;
|
||||
const gt = Number(battle && battle.time) || 0;
|
||||
return {
|
||||
const out = {
|
||||
gt: Math.round(gt * 1000) / 1000,
|
||||
killed: Math.max(0, Math.trunc(Number(battle && battle.killed) || 0)),
|
||||
total: Math.max(0, Math.trunc(Number(battle && battle.total) || 0)),
|
||||
@@ -217,6 +253,11 @@ export function battleProgress(battle) {
|
||||
bossDmg: pool && Number.isFinite(pool.cum) ? pool.cum : 0,
|
||||
done: !!(battle && battle.finished),
|
||||
};
|
||||
if (battle && battle.kind === 'unite') {
|
||||
const left = uniteLeft(battle);
|
||||
if (left) out.left = left;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const r4 = (v) => Math.round((Number(v) || 0) * 1e4) / 1e4;
|
||||
|
||||
Reference in new issue
Block a user