feedback1 integrate: form fx survive the render engine's stale-event drop; hidden-tab backlog compacted and flushed when a finished battle is shown; same-field re-entry buffered
- render/interp.js isCosmeticEvent: an fx with a form is state, never dropped as stale nor shed from a full queue; a late one switches the model without its telegraph, its closing clip shortened (render/app.js) - battle/runner.js: a backlog past HELD_MAX keeps only the last status / skill toggle per unit; a battle that ended while the tab was hidden delivers its backlog and last snapshot when the tab is shown - screens/game.js: a new field meta for the field on screen buffers its frames until the view re-enters it - browser test stalls the main thread (blocking 1 s long tasks); Node tests drive the runner into the render buffer
This commit is contained in:
1 parent
88d419b095
commit
4e68100065
11 files changed
+259
-39
No files matched your search
+13
-6
@@ -24,7 +24,7 @@
|
||||
// user playtest #5 item 2) goes straight to the held pose, no burst; an
|
||||
// fx with a `form` (shared/protocol.js fxForm) switches the enemy's
|
||||
// model to that clip set (render/units.js FORMS)
|
||||
// view.setLocalFeed({ on, speed }) frames come from the local sim every frame (client-side combat):
|
||||
// view.setLocalFeed({ on, speed }) frames come from the local sim every frame (client-side combat):
|
||||
// ~2-frame buffer at the battle's game speed
|
||||
// view.highlightTiles(tiles, style) [[r,c]] | [{row,col}]; style 'legal'|'illegal'|'range'|'rangeStand'|
|
||||
// 'hover'|'target'|{color,fill,line,group}; highlightTiles(null) clears all
|
||||
@@ -1376,9 +1376,12 @@ export async function createFieldView(host, options = {}) {
|
||||
}
|
||||
|
||||
const EVS = [];
|
||||
/** State events handed out more than 1.5 game s late (a stall, a hidden tab, a field entered late) → how late. */
|
||||
const LATE = new Map();
|
||||
function processEvents(renderT) {
|
||||
EVS.length = 0;
|
||||
interp.takeEvents(renderT, EVS, renderT - 1.5);
|
||||
LATE.clear();
|
||||
interp.takeEvents(renderT, EVS, renderT - 1.5, LATE);
|
||||
for (const e of EVS) {
|
||||
try { handleEvent(e, renderT); } catch (err) { if (!handleEvent.warned) { handleEvent.warned = true; console.warn('[render] event failed', e, err); } }
|
||||
}
|
||||
@@ -1446,15 +1449,19 @@ export async function createFieldView(host, options = {}) {
|
||||
// an enemy's mode change — the `form` of a sim setForm fx (shared/protocol.js fxForm: 掠海漂移体 → 爬行模式, user
|
||||
// playtest #5 item 1; 转译基底's forms, a 逐火 ember and its revival, the leaders' 重生, 守墓石像 — user report after
|
||||
// 0.1.0) — switches the view's clip set (UnitView.setForm; a kind without a clip set of that skeleton changes
|
||||
// nothing); the info keeps it for a view built later. The client runner never drops these fx (catch-up frames,
|
||||
// hidden-tab backlog), nor does the game screen's pre-entry buffer.
|
||||
// nothing); the info keeps it for a view built later. No client stage drops these fx: battle/runner.js
|
||||
// keepsState (catch-up frames, hidden-tab backlog), screens/game.js keepEarly (the pre-entry buffer) and
|
||||
// render/interp.js isCosmeticEvent (stale-event drop, full-queue shed). One handed out late switches the model
|
||||
// without its telegraph, its closing clip shortened by the lateness.
|
||||
const form = fxForm(e);
|
||||
const late = form !== undefined ? LATE.get(e) || 0 : 0;
|
||||
if (form !== undefined) {
|
||||
const inf = infos.get(e[4].id);
|
||||
if (inf && (form === null || FORMS[inf.spine || inf.defId]?.[form])) inf.form = form;
|
||||
views.get(e[4].id)?.setForm?.(form, e[4]);
|
||||
const x = late > 0 && Number(e[4].dur) > 0 ? { ...e[4], dur: Math.max(0, Number(e[4].dur) - late) } : e[4];
|
||||
views.get(e[4].id)?.setForm?.(form, x);
|
||||
}
|
||||
fx.simFx(e[1], Number(e[2]), Number(e[3]), e[4]);
|
||||
if (!(late > 0)) fx.simFx(e[1], Number(e[2]), Number(e[3]), e[4]);
|
||||
break;
|
||||
}
|
||||
case 'layer': {
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
// * event queue: a `b.ev` batch is stamped with its game time (`gt`, the snapshot it was drained with); a batch
|
||||
// without one is placed inside the latest snapshot interval (newest snapshot time minus half an interval),
|
||||
// and handed out by `takeEvents()` once renderT passes the stamp. Stale cosmetic events (> `eventMaxLag`
|
||||
// game s behind) are dropped by the caller's choice (`isCosmeticEvent`); state events are always delivered.
|
||||
// game s behind) are dropped by the caller's choice (`isCosmeticEvent`); state events — an enemy's form fx
|
||||
// included — are always delivered, and a full queue sheds only cosmetic ones.
|
||||
//
|
||||
// Snapshot tuple layout (DESIGN §8.2): [id, x, y, hp, maxHp, sp, spMax, flags, anim]. Two optional lists ride along
|
||||
// (server/sim/Battle.js snapshot, user playtest #4 items 8 / 9):
|
||||
@@ -27,6 +28,8 @@
|
||||
// `units`): downAt(time) returns the list of the snapshot at `time`.
|
||||
// Game times in both (`cooldownEnd`, `respawnAt`) are on the snapshots' clock, so a view compares them with renderT.
|
||||
|
||||
import { fxForm } from '../../../shared/protocol.js';
|
||||
|
||||
export const TUPLE = Object.freeze({ ID: 0, X: 1, Y: 2, HP: 3, MAXHP: 4, SP: 5, SPMAX: 6, FLAGS: 7, ANIM: 8, EL: 9, EL_FILL: 10, EL_UNTIL: 11, EL_DUR: 12 });
|
||||
/** Element keys a snapshot `elem` entry may carry (server/sim/constants.js ELEMENT_ORDER). */
|
||||
const ELEMENT_KEYS = new Set(['neural', 'erosion', 'burn', 'apoptosis', 'necrosis']);
|
||||
@@ -37,7 +40,13 @@ const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
|
||||
|
||||
/** Cosmetic event kinds that may be dropped when far behind (never state-changing). */
|
||||
export const COSMETIC_EVENTS = new Set(['atk', 'dmg', 'heal', 'fx', 'layer', 'bounty']);
|
||||
export const isCosmeticEvent = (ev) => Array.isArray(ev) && COSMETIC_EVENTS.has(ev[0]);
|
||||
/**
|
||||
* Whether an event may be dropped when stale or shed from a full queue. An 'fx' that carries an enemy's model `form`
|
||||
* (shared/protocol.js fxForm) is state: dropping it left the view on the old model after a long main-thread stall, on a
|
||||
* watched field after a hidden tab and on a field entered late (user report #5 after 0.1.0) — the fourth place the client
|
||||
* keeps it, with battle/runner.js keepsState and screens/game.js keepEarly.
|
||||
*/
|
||||
export const isCosmeticEvent = (ev) => Array.isArray(ev) && COSMETIC_EVENTS.has(ev[0]) && fxForm(ev) === undefined;
|
||||
|
||||
/**
|
||||
* Game time (s) of a b.snap / b.ev payload, or NaN. On the wire every frame is `{ t: '<type>', … }`, so the server
|
||||
@@ -308,9 +317,12 @@ export class SnapshotBuffer {
|
||||
|
||||
/**
|
||||
* Remove and return (in order) the queued events due at `time` (default renderT). When `dropCosmeticBefore`
|
||||
* is a number, cosmetic events stamped earlier than it are discarded instead of returned.
|
||||
* is a number, cosmetic events stamped earlier than it are discarded instead of returned; a state event stamped
|
||||
* earlier is returned, and when `late` (a Map) is given it records that event → how far (game s) its stamp lies
|
||||
* before `time`, so the caller can skip its stale cosmetics (render/app.js: a late form fx switches the model without
|
||||
* replaying its telegraph and with its closing clip shortened).
|
||||
*/
|
||||
takeEvents(time = this.renderT, out = [], dropCosmeticBefore = null) {
|
||||
takeEvents(time = this.renderT, out = [], dropCosmeticBefore = null, late = null) {
|
||||
if (!this.events.length || !Number.isFinite(time)) return out;
|
||||
let n = 0;
|
||||
while (n < this.events.length && this.events[n].t <= time) n++;
|
||||
@@ -318,7 +330,10 @@ export class SnapshotBuffer {
|
||||
const list = this.events;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const e = list[i];
|
||||
if (typeof dropCosmeticBefore === 'number' && e.t < dropCosmeticBefore && isCosmeticEvent(e.ev)) continue;
|
||||
if (typeof dropCosmeticBefore === 'number' && e.t < dropCosmeticBefore) {
|
||||
if (isCosmeticEvent(e.ev)) continue;
|
||||
if (late) late.set(e.ev, time - e.t);
|
||||
}
|
||||
out.push(e.ev);
|
||||
}
|
||||
const rest = list.length - n;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
// redeploy) plays the deploy clip and restores the normal look; `setDown(null)` on a dead view lets it fade out.
|
||||
// An operator that enters a battle already knocked out (联防, user playtest #5 item 2: sim 'die' reason 'forcedExit')
|
||||
// goes down with `die(true)`: straight to the held end of the clip, no fall.
|
||||
// Enemy modes (sim fx 'phase' { id, kind } or another fx's `form` → `setForm(kind, fx)`): 掠海漂移体 dropping to 爬行模式
|
||||
// Enemy modes (the `form` of a sim fx — shared/protocol.js fxForm — → `setForm(form, fx)`): 掠海漂移体 dropping to 爬行模式
|
||||
// (user playtest #5 item 1) plays its skeleton's 'Change' clip once, then the crawl set (*_02); 暴鸰 flies on without its
|
||||
// bomb (*_2) after the drop (feedback D4); 转译基底's forms, the 逐火 embers, 再生's puppet, the leaders' 重生 and 守墓石像
|
||||
// likewise (user report after 0.1.0) — FORMS. A view built later (`info.form` = UnitInfo `form`, the sim's current form,
|
||||
@@ -113,7 +113,7 @@ export const ALIAS_TINT = Object.freeze({ enemy_1305_mhslim: 0xffc48a, enemy_130
|
||||
export const EL_BAR = Object.freeze({ icon: 0.15, min: 8, max: 15, enemy: 0.8, gap: 1, pulse: 0.45, pulseHz: 1.5 });
|
||||
|
||||
/**
|
||||
* Enemy modes drawn with another clip set of the same skeleton (sim fx 'phase' kind, or the `form` of another sim fx
|
||||
* Enemy modes drawn with another clip set of the same skeleton (the `form` of a sim fx — shared/protocol.js fxForm —
|
||||
* → UnitView.setForm), per Spine id; the mode's roles override the manifest's (data/assets.json anims), `change` plays
|
||||
* once first:
|
||||
* - 掠海漂移体 (PRTS: 受晕眩/沉睡/冻结影响后进入爬行模式 — for good) crawls on its *_02 clips after 'Change';
|
||||
|
||||
Reference in new issue
Block a user