Playtest #6 follow-up (v2.5.1): leader HP review — 直接乘算, one debuff instance, 999 layer cap, 限伤; elite to the board; 假想敌:胄 kit
- co-op leaders died within seconds: leader HP is the official data (40 values pinned); bond / strategy / item / 机变 / 特质 "+X%" bonuses now add up (PRTS 直接乘算) instead of compounding; a same-named enemy debuff keeps one (strongest) instance per target (奥术 across a pair field); leader parts pass damage 1:1; fixed co-op pool (the user's call) - official limits read from the client: each bond stops at 999 layers (MAX_GARRISON_STACK); a leader hit of 300000+ in boss / hidden battles is cancelled (MAX_BATTLE_DAMAGE, 限伤) — shared/constants.js switches, research 11 - a merge that consumes a deployed copy puts the elite on that tile and facing (user-confirmed); gold target highlight, promotion effect - 假想敌:胄 per PRTS: one shell, shells within range, any drone death costs 2 % max HP, 剑/锤 sorties and 瘫痪 - DESIGN §20.10–§20.14; 2842 unit tests, all 28 browser suites green; Chrome = Node on boss fields
This commit is contained in:
1 parent
6464e4b46a
commit
620f88c0d8
81 files changed
+4194
-447
No files matched your search
+32
-2
@@ -8,7 +8,10 @@
|
||||
// and the field clear of `hud(kind, size)` = { top, bottom } px of
|
||||
// DOM HUD along the top / bottom edge (projection.js clearHud; user
|
||||
// playtest #5 item 9: the shop bar covered the bench on phones)
|
||||
// view.setPrep(privateState, { editable, canPlace }) hand/temp/board pieces; editable enables drag & drop
|
||||
// view.setPrep(privateState, { editable, canPlace }) hand/temp/board pieces; editable enables drag & drop; a
|
||||
// new board piece flashes (fx.deploy); a merge's elite — on the tile of
|
||||
// the deployed copy it replaced, or on its bench slot — gets the
|
||||
// promotion cue instead (render/promote.js, fx.promote)
|
||||
// view.enterBattle(fieldMeta) m.field { fieldId, kind, rect, stageId, units: [UnitInfo] }
|
||||
// view.pushSnapshot(snap); view.pushEvents(ev | { ev, gt }) b.snap / b.ev wire frames as received (game time in
|
||||
// `gt`; a numeric `t` is accepted for raw Battle snapshots / recordings)
|
||||
@@ -101,6 +104,7 @@ import { AREAS, areaFor, unionAreas } from './board3d/layout.js';
|
||||
import { layoutPen, penSignature } from './pen.js';
|
||||
import { IDENTITY, bossPrepField, tilesToDisp } from './prepfield.js';
|
||||
import { pickOnTile, pickBattle, hitRectAt } from './pick.js';
|
||||
import { promotionsOf } from './promote.js';
|
||||
|
||||
const VENDOR = { pixi: '/vendor/pixi.min.js', spine: '/vendor/pixi-spine.js' };
|
||||
const PIECE_DIRS = new Set(['UP', 'RIGHT', 'DOWN', 'LEFT']);
|
||||
@@ -427,6 +431,10 @@ export async function createFieldView(host, options = {}) {
|
||||
let pendingView = null; // tile band/focus to apply when the camera transition ends
|
||||
const views = new Map(); // key → view (prep: 'p:'+uid; battle: unit id)
|
||||
let prepPieces = []; // { uid, piece, area, idx, row, col, key }
|
||||
// the last prep pieces before a battle / scouting board emptied prepPieces: a merge completed between two preps (a band
|
||||
// or 机变 grant at ROUND_START, a SETTLE merge) is still recognised by the next setPrep (QA 6b)
|
||||
let promoBase = [];
|
||||
const promotions = []; // the last merges cued by setPrep (fx.promote): { uid, id, area, row, col, idx, copies } — dev / tests
|
||||
let battleMeta = null;
|
||||
const infos = new Map(); // battle unit id → UnitInfo
|
||||
// battle ids whose view finished its death / leak fade: a snapshot may still list them for a moment (the sim keeps
|
||||
@@ -845,6 +853,20 @@ export async function createFieldView(host, options = {}) {
|
||||
addList(src.board, 'board');
|
||||
const seen = new Set();
|
||||
const prevBoard = new Set(prepPieces.filter((p) => p.area === 'board').map((p) => p.uid));
|
||||
// merges since the last state (render/promote.js): new elite uid → where its consumed copies stood (their views
|
||||
// still exist until the sweep below) — the elite gets the promotion cue instead of the deploy flash
|
||||
const promoFrom = new Map();
|
||||
const before = prepPieces.length ? prepPieces : promoBase;
|
||||
promoBase = [];
|
||||
for (const [uid, copies] of promotionsOf(before, list, (id) => data.chess(id))) {
|
||||
// the consumed copies' views (gone after a battle rebuilt the scene: their slots instead) — the streaks' origins
|
||||
promoFrom.set(uid, copies.map((g) => {
|
||||
const cv = views.get(g.key);
|
||||
if (cv && !cv.destroyed) return { x: cv.x, y: cv.y, z: cv.z || 0 };
|
||||
const w = slotWorld(g);
|
||||
return w ? { x: w.x, y: w.y, z: w.z || 0 } : null;
|
||||
}).filter(Boolean));
|
||||
}
|
||||
for (const e of list) {
|
||||
if (seen.has(e.uid)) continue; // duplicate uid in bad state: keep the first
|
||||
seen.add(e.uid);
|
||||
@@ -863,7 +885,13 @@ export async function createFieldView(host, options = {}) {
|
||||
v._sig = sig;
|
||||
v.setWorld(w.x, w.y, w.z);
|
||||
views.set(key, v);
|
||||
if (e.area === 'board' && prevBoard.size && !prevBoard.has(e.uid)) { v.onDeploy?.(); fx.deploy(v); }
|
||||
if (promoFrom.has(e.uid)) {
|
||||
// a merge's elite: on the tile of the deployed copy it replaced, or on its bench slot
|
||||
if (e.area === 'board') v.onDeploy?.();
|
||||
fx.promote(v, promoFrom.get(e.uid).filter((f) => Math.abs(f.x - w.x) + Math.abs(f.y - w.y) > 1e-3));
|
||||
promotions.push({ uid: e.uid, id: e.piece.id, area: e.area, row: e.row ?? null, col: e.col ?? null, idx: e.idx ?? null, copies: promoFrom.get(e.uid).length });
|
||||
if (promotions.length > 20) promotions.shift();
|
||||
} else if (e.area === 'board' && prevBoard.size && !prevBoard.has(e.uid)) { v.onDeploy?.(); fx.deploy(v); }
|
||||
} else {
|
||||
const prevHome = v._home;
|
||||
const moved = !prevHome || prevHome.x !== w.x || prevHome.y !== w.y || prevHome.z !== w.z;
|
||||
@@ -1226,6 +1254,7 @@ export async function createFieldView(host, options = {}) {
|
||||
drag.reset();
|
||||
endDragVisual(false);
|
||||
clearViews();
|
||||
if (prepPieces.length) promoBase = prepPieces;
|
||||
prepPieces = [];
|
||||
infos.clear();
|
||||
gone.clear();
|
||||
@@ -1783,6 +1812,7 @@ export async function createFieldView(host, options = {}) {
|
||||
/** Dev hooks (demo / tests). */
|
||||
debug: {
|
||||
app, get cam() { return cam; }, get board3d() { return board3d; }, tiles, views, penViews, interp, fx, ctx, drag, get camKind() { return viewKind(camKind, camOpts); },
|
||||
promotions,
|
||||
// picking (render/pick.js) at canvas px: the prep piece / battle view / pen view there, the ground tile under it
|
||||
pick: { pieceAt, battleUnitAt, penUnitAt, groundTile },
|
||||
},
|
||||
|
||||
+30
-3
@@ -212,6 +212,9 @@ export const FX_KINDS = Object.freeze({
|
||||
thorns: { a: 'counter', c: 0xff9aa6 }, downed: { a: 'down', c: 0xbfeee2 }, stone: { a: 'down', c: 0xc0b8a8, smoke: 0x5a5448 },
|
||||
dp: { a: 'dp', c: 0x9fd4ff }, coin: { a: 'coin', c: 0xffc600 }, steal: { a: 'coin', c: 0xffc600 }, crateBreak: { a: 'crate', c: 0xc89a5a },
|
||||
lpLoss: { a: 'lp', c: 0xff3b30 },
|
||||
// 限伤 (sim/damage.js leaderHitCancelled): a leader's hit of ≥ 300000 dealt nothing — the official shows no number and
|
||||
// no effect [ASSUMED], so nothing is drawn (`a: 'none'`)
|
||||
hitCap: { a: 'none', c: 0xffffff },
|
||||
// beams
|
||||
beam: { a: 'beam', c: 0xff7a5a }, link: { a: 'beam', c: 0x9ff0dc }, lightning: { a: 'bolt', c: 0xc9a2ff }, tentacle: { a: 'beam', c: 0x5fe0ff },
|
||||
sandChains: { a: 'beam', c: 0xd8c8a0 }, sandChainsCharged: { a: 'beam', c: 0xffd45a },
|
||||
@@ -1578,6 +1581,29 @@ export class FxSystem {
|
||||
this.burst(p.x, p.y, s, 6, col, { speed: 1.6, up: 0.4, life: 0.4, tex: 'dot' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Promotion (精锐晋升, a merge) at the elite's spot — the board tile of the consumed copy it replaced (PRTS 卫戍协议/
|
||||
* 帮助 "若消耗已部署至作战区的干员,则发送至作战区对应位置") or its bench slot: a gold pillar with a white core, a shockwave
|
||||
* and a hex ring on the ground, rising motes; `from` = world points of the other consumed copies (gold streaks from
|
||||
* them to the elite). Counted in `promotions` (render/app.js setPrep; tests).
|
||||
* @param {any} view
|
||||
* @param {Array<{ x: number, y: number, z?: number }>} [from]
|
||||
*/
|
||||
promote(view, from = []) {
|
||||
if (!view) return;
|
||||
this.promotions = (this.promotions || 0) + 1;
|
||||
const z = view.z || 0;
|
||||
const g = this._proj(view.x, view.y, z, this._g);
|
||||
const gx = g.x, gy = g.y, s = g.s;
|
||||
for (const f of from) if (f && Number.isFinite(f.x) && Number.isFinite(f.y)) this.streak(f.x, f.y, view.x, view.y, Math.max(z, f.z || 0) + 0.3, SKILL_GOLD, 0.45);
|
||||
this.particle('pillar', gx, gy, { tint: SKILL_GOLD, life: 0.8, s0: (s / 64) * 1.1, s1: (s / 64) * 1.4, a0: 0.95, a1: 0, sx: 0.85, ay: 1 });
|
||||
this.particle('pillar', gx, gy, { tint: 0xffffff, life: 0.42, s0: (s / 64) * 0.9, s1: (s / 64) * 1.2, a0: 0.9, a1: 0, sx: 0.3, ay: 1 });
|
||||
this.ring(view.x, view.y, z, 0.15, 1.6, 0xffe7a0, 0.5, 'shock');
|
||||
this.ring(view.x, view.y, z, 0.3, 1.2, SKILL_GOLD, 0.7, 'hex');
|
||||
const c = this._chest(view, this._q);
|
||||
this.burst(c.x, c.y, s, this.rich ? 12 : 5, 0xffe28a, { speed: 1.5, up: 1.8, life: 0.8, tex: 'dot', size: 0.36 });
|
||||
}
|
||||
|
||||
death(view) {
|
||||
if (!view) return;
|
||||
const p = this._chest(view);
|
||||
@@ -1637,14 +1663,15 @@ export class FxSystem {
|
||||
/**
|
||||
* b.ev 'fx': every kind the sim / content emits has a visual (FX_KINDS archetypes: blast, shell, zone, telegraph,
|
||||
* heal, sp, shield, shatter, summon, vanish, blink, move, wave, mark, reticle, buff, lift, sleep, crit, dodge,
|
||||
* counter, dp, coin, crate, down, beam, bolt, strike, volley, pillar, lp, chill, element, flame); unknown kinds get a
|
||||
* generic sparkle. `extra` keys used: id (anchor unit — or the shooter of a `pt` kind), r | radius, dur | duration,
|
||||
* counter, dp, coin, crate, down, beam, bolt, strike, volley, pillar, lp, chill, element, flame), except kinds whose
|
||||
* archetype is 'none' (hitCap: a leader hit cancelled by 限伤 draws nothing); unknown kinds get a generic sparkle. `extra` keys used: id (anchor unit — or the shooter of a `pt` kind), r | radius, dur | duration,
|
||||
* t (shell flight, game s), src / from / to / target / targets (unit ids), fx, fy / fromX, fromY / tx, ty (positions),
|
||||
* element, n, scale, kind, tiles.
|
||||
*/
|
||||
simFx(kind, x, y, extra) {
|
||||
const ex = extra && typeof extra === 'object' ? extra : {};
|
||||
const spec = fxSpec(kind, ex);
|
||||
if (spec.a === 'none') return; // an event the screen does not show (hitCap)
|
||||
const at = spec.pt ? this._point(Number(x), Number(y)) : this._where(Number(x), Number(y), ex);
|
||||
if (!Number.isFinite(at.x) || !Number.isFinite(at.y)) return;
|
||||
// 蕾缪安 S2: the aimed snipe ('crit' on the locked enemy, from her) ends that aim lock
|
||||
@@ -2174,7 +2201,7 @@ export class FxSystem {
|
||||
}
|
||||
|
||||
get counts() {
|
||||
return { particles: this.parts.length, projectiles: this.projs.length, numbers: this.nums.length, rings: this.rings.length, auras: this.auras.size, locks: this.locks.length, flames: this.flames.length };
|
||||
return { particles: this.parts.length, projectiles: this.projs.length, numbers: this.nums.length, rings: this.rings.length, auras: this.auras.size, locks: this.locks.length, flames: this.flames.length, promotions: this.promotions || 0 };
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// public/js/render/promote.js — which prep pieces were just promoted (精锐晋升), for the merge cue of render/app.js setPrep.
|
||||
//
|
||||
// A merge replaces the consumed normal copies by ONE new elite piece in the same m.private update: on the board tile of
|
||||
// the consumed copy that deploys first when a copy was deployed (PRTS 卫戍协议/帮助 "若消耗已部署至作战区的干员,则发送至
|
||||
// 作战区对应位置"; server board.js mergeTile), else in the hand / temp. So a promotion = a chess piece with a new uid that is
|
||||
// an elite, while normal copies of the same operator (same baseId) vanished from the previous state. The cue then plays
|
||||
// on the elite (fx.promote: gold pillar + rings, streaks from the vanished copies) instead of the plain deploy flash.
|
||||
|
||||
/**
|
||||
* @param {Array<{ uid: number, area: string, piece: any }>} prev entries of the previous setPrep (render/app.js prepPieces)
|
||||
* @param {Array<{ uid: number, area: string, piece: any }>} next entries of this setPrep
|
||||
* @param {(id: string) => any} chessOf chess record lookup (`baseId`, `isGolden`)
|
||||
* @returns {Map<number, Array<{ uid: number, area: string, piece: any }>>} new elite uid → the vanished copies (prev entries)
|
||||
*/
|
||||
export function promotionsOf(prev, next, chessOf) {
|
||||
const out = new Map();
|
||||
if (!Array.isArray(prev) || !prev.length || !Array.isArray(next)) return out;
|
||||
const before = new Set(prev.map((e) => e.uid));
|
||||
const now = new Set(next.map((e) => e.uid));
|
||||
const baseOf = (id) => chessOf(id)?.baseId || id;
|
||||
const isElite = (p) => !!(p?.golden || chessOf(p?.id)?.isGolden);
|
||||
const gone = prev.filter((e) => e.piece?.kind === 'chess' && !now.has(e.uid) && !isElite(e.piece));
|
||||
if (!gone.length) return out;
|
||||
const used = new Set();
|
||||
for (const e of next) {
|
||||
if (e.piece?.kind !== 'chess' || before.has(e.uid) || !isElite(e.piece)) continue;
|
||||
const base = baseOf(e.piece.id);
|
||||
const copies = gone.filter((g) => !used.has(g.uid) && baseOf(g.piece.id) === base);
|
||||
if (!copies.length) continue;
|
||||
for (const g of copies) used.add(g.uid);
|
||||
out.set(e.uid, copies);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in new issue
Block a user