feedback5 R3: Battle.js → server/sim/battle/*.js, a pure move — the 131 methods, verbatim with their comments, go to 14 per-concern method containers (players, lifecycle, hooks, spawns, deploy, blocking, status, combat, queries, summons, tiles, displacement, economy, events; fin / clone in util.js) that Battle.js installs on Battle.prototype in a fixed order with class-method descriptors; Battle.js keeps the header, the constructor and the class, and re-exports pushTiles. docs-consistency reads the method modules (battleText) and pins layerGainRoom in battle/economy.js
This commit is contained in:
1 parent
6b489b1776
commit
13224f9483
17 files changed
+2488
-2328
No files matched your search
@@ -0,0 +1,188 @@
|
||||
// server/sim/battle/hooks.js — Battle methods: the hook bus (on / off / offOwner / emit, the MAX_HOOK_DEPTH guard and
|
||||
// its chain diagnostics), the timers (after / every, run at the start of each tick) and the isolation of content
|
||||
// callbacks (_safe, _handlerError: a failing handler is skipped and counted; repeated engine errors end the battle as a
|
||||
// timeout).
|
||||
// Installed on Battle.prototype by server/sim/Battle.js (a method container: never instantiated; `this` is the battle).
|
||||
|
||||
import { TICK, MAX_INTERNAL_ERRORS, MAX_HOOK_DEPTH } from '../constants.js';
|
||||
|
||||
let hookSeq = 0;
|
||||
|
||||
let schedSeq = 0;
|
||||
|
||||
export class BattleHooks {
|
||||
// =============================================================================================================
|
||||
// hook bus
|
||||
|
||||
on(name, fn, opts = {}) {
|
||||
if (typeof fn !== 'function') return null;
|
||||
const h = { name, fn, priority: Number(opts.priority) || 0, owner: opts.owner ?? null, seq: ++hookSeq, removed: false, once: !!opts.once };
|
||||
const list = this._hooks[name] ?? (this._hooks[name] = []);
|
||||
let i = list.length;
|
||||
while (i > 0 && list[i - 1].priority < h.priority) i--;
|
||||
list.splice(i, 0, h);
|
||||
return h;
|
||||
}
|
||||
|
||||
off(nameOrHandle, fn) {
|
||||
if (nameOrHandle && typeof nameOrHandle === 'object') {
|
||||
const h = nameOrHandle;
|
||||
h.removed = true;
|
||||
const list = this._hooks[h.name];
|
||||
if (list) {
|
||||
const i = list.indexOf(h);
|
||||
if (i >= 0) list.splice(i, 1);
|
||||
if (!list.length) delete this._hooks[h.name];
|
||||
}
|
||||
return;
|
||||
}
|
||||
const list = this._hooks[nameOrHandle];
|
||||
if (!list) return;
|
||||
for (let i = list.length - 1; i >= 0; i--) if (!fn || list[i].fn === fn) { list[i].removed = true; list.splice(i, 1); }
|
||||
if (!list.length) delete this._hooks[nameOrHandle];
|
||||
}
|
||||
|
||||
/** Remove every hook and scheduled callback registered with `owner`. */
|
||||
offOwner(owner) {
|
||||
for (const name of Object.keys(this._hooks)) {
|
||||
const list = this._hooks[name];
|
||||
for (let i = list.length - 1; i >= 0; i--) if (list[i].owner === owner) { list[i].removed = true; list.splice(i, 1); }
|
||||
if (!list.length) delete this._hooks[name];
|
||||
}
|
||||
for (const s of this._sched) if (s.owner === owner) s.cancelled = true;
|
||||
}
|
||||
|
||||
hasHook(name) { return !!this._hooks[name]; }
|
||||
|
||||
emit(name, ctx) {
|
||||
const list = this._hooks[name];
|
||||
if (!list) return ctx;
|
||||
// Recursion guard: handlers that re-trigger their own event (damaged → dealDamage → damaged …) would otherwise
|
||||
// recurse until a stack overflow, whose depth (and thus the outcome) depends on the machine — not deterministic.
|
||||
if (this._emitDepth >= MAX_HOOK_DEPTH) {
|
||||
this._handlerError(`hookDepth:${name}`, null, new Error(`hook nesting deeper than ${MAX_HOOK_DEPTH}; handlers skipped — ${this._chain(name)}`));
|
||||
return ctx;
|
||||
}
|
||||
const arr = list.length === 1 ? [list[0]] : list.slice();
|
||||
const d = this._emitDepth++;
|
||||
this._frameCtx[d] = ctx;
|
||||
try {
|
||||
for (const h of arr) {
|
||||
if (h.removed) continue;
|
||||
this._frameName[d] = name;
|
||||
this._frameOwner[d] = h.owner;
|
||||
try { h.fn(ctx, this); } catch (e) { this._handlerError(`hook:${name}`, h.owner, e); }
|
||||
if (h.once) this.off(h);
|
||||
if (ctx && ctx.stopPropagation) break;
|
||||
}
|
||||
} finally {
|
||||
this._emitDepth--;
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* The open hook/callback frames (outermost first) — names the loop when the nesting guard trips, e.g.
|
||||
* "chain: hit(enemy_1430_lrrook) > damaged(chess_char_3_18_a) > hit(enemy_1430_lrrook) > … > damaged".
|
||||
*/
|
||||
_chain(last) {
|
||||
const id = (u) => (u && typeof u === 'object' ? (u.defId ?? u.name ?? '?') : u == null ? '' : String(u));
|
||||
const lab = (i) => {
|
||||
const o = this._frameOwner[i];
|
||||
let who = id(o);
|
||||
const c = this._frameCtx[i];
|
||||
if (!who && c && typeof c === 'object') { // global handler: show the units of the event instead
|
||||
const a = c.source ?? c.attacker ?? c.killer ?? null, t = c.target ?? c.unit ?? c.victim ?? c.enemy ?? null;
|
||||
who = a || t ? `${id(a)}→${id(t)}` : '';
|
||||
}
|
||||
return `${this._frameName[i]}${who ? `(${who})` : ''}`;
|
||||
};
|
||||
const n = this._emitDepth;
|
||||
const parts = [];
|
||||
for (let i = 0; i < Math.min(n, 4); i++) parts.push(lab(i));
|
||||
if (n > 12) parts.push('…');
|
||||
for (let i = Math.max(4, n - 8); i < n; i++) parts.push(lab(i));
|
||||
parts.push(last);
|
||||
return `chain: ${parts.join(' > ')}`;
|
||||
}
|
||||
|
||||
// =============================================================================================================
|
||||
// scheduling
|
||||
|
||||
after(seconds, fn, opts = {}) {
|
||||
const s = { due: this.time + Math.max(0, Number(seconds) || 0), fn, owner: opts.owner ?? null, interval: 0, seq: ++schedSeq, cancelled: false, holdsBattle: !!opts.holdsBattle };
|
||||
s.cancel = () => { s.cancelled = true; };
|
||||
this._sched.push(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
every(seconds, fn, opts = {}) {
|
||||
const iv = Math.max(TICK, Number(seconds) || TICK);
|
||||
const s = { due: this.time + (opts.immediate ? 0 : iv), fn, owner: opts.owner ?? null, interval: iv, seq: ++schedSeq, cancelled: false, count: 0, holdsBattle: false };
|
||||
s.cancel = () => { s.cancelled = true; };
|
||||
this._sched.push(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
_runScheduled() {
|
||||
if (!this._sched.length) return;
|
||||
const now = this.time + 1e-9;
|
||||
let due = this._sched.filter((s) => !s.cancelled && s.due <= now);
|
||||
if (!due.length) { if (this._sched.some((s) => s.cancelled)) this._sched = this._sched.filter((s) => !s.cancelled); return; }
|
||||
due.sort((a, b) => a.due - b.due || a.seq - b.seq);
|
||||
for (const s of due) {
|
||||
if (s.cancelled) continue;
|
||||
if (s.interval > 0) {
|
||||
let n = 0;
|
||||
while (s.due <= now && !s.cancelled && n++ < 8) {
|
||||
s.count++;
|
||||
this._safe(() => s.fn(this, s), 'every', s.owner);
|
||||
s.due += s.interval;
|
||||
}
|
||||
if (s.due <= now) s.due = this.time + s.interval;
|
||||
} else {
|
||||
s.cancelled = true;
|
||||
this._safe(() => s.fn(this, s), 'after', s.owner);
|
||||
}
|
||||
}
|
||||
this._sched = this._sched.filter((s) => !s.cancelled);
|
||||
}
|
||||
|
||||
// =============================================================================================================
|
||||
// callback isolation and errors
|
||||
|
||||
/** Run a content callback (skill/buff/projectile/timer/install) isolated from errors; shares the hook depth guard. */
|
||||
_safe(fn, label, owner = null) {
|
||||
if (this._emitDepth >= MAX_HOOK_DEPTH) {
|
||||
this._handlerError(`hookDepth:${label}`, owner, new Error(`callback nesting deeper than ${MAX_HOOK_DEPTH}; skipped — ${this._chain(label)}`));
|
||||
return undefined;
|
||||
}
|
||||
const d = this._emitDepth++;
|
||||
this._frameName[d] = label;
|
||||
this._frameOwner[d] = owner;
|
||||
this._frameCtx[d] = null;
|
||||
try { return fn(); } catch (e) { this._handlerError(label, owner, e); return undefined; } finally { this._emitDepth--; }
|
||||
}
|
||||
|
||||
_handlerError(label, owner, e, internal = false) {
|
||||
this.errorCount++;
|
||||
const who = owner && owner.defId ? owner.defId : (owner && owner.name) || '';
|
||||
const key = `${label}|${who}|${e && e.message}`;
|
||||
if (!this._errKeys.has(key)) {
|
||||
this._errKeys.add(key);
|
||||
if (this.errors.length < 100) this.errors.push({ label, who, message: String(e && e.message), stack: e && e.stack });
|
||||
if (this.opts.quiet !== true) this.logger.error?.(`[sim] ${label}${who ? ' (' + who + ')' : ''} failed: ${e && e.stack ? e.stack.split('\n').slice(0, 3).join(' | ') : e}`);
|
||||
}
|
||||
// Content handler errors are isolated (the handler is skipped, the battle continues). Repeated *engine*
|
||||
// errors mean the field state is suspect: force-end it as a timeout (DESIGN §11).
|
||||
if (internal) {
|
||||
this.internalErrorCount = (this.internalErrorCount ?? 0) + 1;
|
||||
if (this.internalErrorCount > MAX_INTERNAL_ERRORS && !this.finished && !this._forcing) {
|
||||
this._forcing = true;
|
||||
try { this.forceEnd('timeout'); } catch { this._hardFinish('timeout'); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_internalError(label, e) { this._handlerError('internal:' + label, null, e, true); }
|
||||
}
|
||||
Reference in new issue
Block a user