feat(ta): add order queueing with CTRL and visual waypoint feedback
- Queue multiple orders per unit by holding CTRL (matching original game) - Draw colored waypoint chains on the minimap/world for each queued order - Display queued order count in selection detail panel - Limit queue overlay to MAX_QUEUE_LINES to avoid clutter in large selections - Use distinct colors per order type (move, attack, patrol, build, etc.) - Update hint text and placement UI to mention queueing behavior - Add verification tests for queue append/replace semantics, consumption order, and queued build orders
This commit is contained in:
parent
e7da172896
commit
ddac7fcd5a
|
|
@ -25,6 +25,10 @@ const BOT_H = 190; // bottom command bar
|
||||||
const MINI = 176; // minimap edge length
|
const MINI = 176; // minimap edge length
|
||||||
const BTN_W = 88, BTN_H = 74; // build-menu button size
|
const BTN_W = 88, BTN_H = 74; // build-menu button size
|
||||||
|
|
||||||
|
// CTRL is the queue modifier (as in the original game); SHIFT adds to the selection and
|
||||||
|
// buys x5 from a factory. Kept in one place so the hint and the bindings cannot drift.
|
||||||
|
const HINT = 'LMB select · drag box · RMB order · CTRL+RMB queue · A attack-move · X stop · H hold · WASD pan';
|
||||||
|
|
||||||
/** Build-button captions have ~7 characters of room; "Vehicle Plant" needs shortening. */
|
/** Build-button captions have ~7 characters of room; "Vehicle Plant" needs shortening. */
|
||||||
function shortName(name) {
|
function shortName(name) {
|
||||||
const words = name.split(' ');
|
const words = name.split(' ');
|
||||||
|
|
@ -316,6 +320,9 @@ export default class TAHud {
|
||||||
const sel = [...selection];
|
const sel = [...selection];
|
||||||
const sig = sel.map((e) => e.id).join(',') + '|' + (placementDef?.id ?? '')
|
const sig = sel.map((e) => e.id).join(',') + '|' + (placementDef?.id ?? '')
|
||||||
+ '|' + sel.map((e) => e.queue?.length ?? 0).join(',')
|
+ '|' + sel.map((e) => e.queue?.length ?? 0).join(',')
|
||||||
|
// Order-queue length is part of the signature, otherwise queueing a command with CTRL
|
||||||
|
// changes nothing on screen until the selection itself changes.
|
||||||
|
+ '|' + sel.map((e) => e.orders?.length ?? 0).join(',')
|
||||||
+ '|' + Math.floor((this.state.armies[this.playerArmy]?.mass ?? 0) / 25);
|
+ '|' + Math.floor((this.state.armies[this.playerArmy]?.mass ?? 0) / 25);
|
||||||
if (sig === this._selSig) { this._refreshQueue(sel); return; }
|
if (sig === this._selSig) { this._refreshQueue(sel); return; }
|
||||||
this._selSig = sig;
|
this._selSig = sig;
|
||||||
|
|
@ -324,7 +331,7 @@ export default class TAHud {
|
||||||
this.selTitle.setText('');
|
this.selTitle.setText('');
|
||||||
this.selDetail.setText('');
|
this.selDetail.setText('');
|
||||||
this.queueText.setText('');
|
this.queueText.setText('');
|
||||||
this.hint.setText('LMB select · drag box · RMB order · A attack-move · X stop · H hold · WASD pan · Ctrl+1-9 groups');
|
this.hint.setText(HINT);
|
||||||
this._clearGrid();
|
this._clearGrid();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -342,18 +349,21 @@ export default class TAHud {
|
||||||
if (sel.length === 1) {
|
if (sel.length === 1) {
|
||||||
lines.push(`HP ${Math.ceil(lead.hp)} / ${lead.maxHp}`);
|
lines.push(`HP ${Math.ceil(lead.hp)} / ${lead.maxHp}`);
|
||||||
if (lead.site) lines.push(`Under construction — ${Math.round((lead.progress ?? 0) * 100)}%`);
|
if (lead.site) lines.push(`Under construction — ${Math.round((lead.progress ?? 0) * 100)}%`);
|
||||||
|
if (lead.orders?.length > 1) lines.push(`${lead.orders.length} orders queued`);
|
||||||
if (!lead.site && lead.hasRally) lines.push('Rally point set');
|
if (!lead.site && lead.hasRally) lines.push('Rally point set');
|
||||||
for (const w of def.weaponDefs ?? []) lines.push(`${w.name}${w.manual ? ' — manual (attack order)' : ''}`);
|
for (const w of def.weaponDefs ?? []) lines.push(`${w.name}${w.manual ? ' — manual (attack order)' : ''}`);
|
||||||
} else {
|
} else {
|
||||||
const hp = sel.reduce((s, e) => s + e.hp, 0), max = sel.reduce((s, e) => s + e.maxHp, 0);
|
const hp = sel.reduce((s, e) => s + e.hp, 0), max = sel.reduce((s, e) => s + e.maxHp, 0);
|
||||||
lines.push(`${sel.length} units HP ${Math.ceil(hp)} / ${max}`);
|
lines.push(`${sel.length} units HP ${Math.ceil(hp)} / ${max}`);
|
||||||
|
const queued = sel.reduce((n, e) => Math.max(n, e.orders?.length ?? 0), 0);
|
||||||
|
if (queued > 1) lines.push(`${queued} orders queued (hold CTRL to add more)`);
|
||||||
}
|
}
|
||||||
this.selDetail.setText(lines.join('\n'));
|
this.selDetail.setText(lines.join('\n'));
|
||||||
|
|
||||||
if (placementDef) {
|
if (placementDef) {
|
||||||
this.hint.setText(`Placing ${placementDef.name} — LMB to site it, Esc to cancel`);
|
this.hint.setText(`Placing ${placementDef.name} — LMB to site it, hold CTRL to queue several, Esc to cancel`);
|
||||||
} else {
|
} else {
|
||||||
this.hint.setText('LMB select · drag box · RMB order · A attack-move · X stop · H hold · WASD pan · Ctrl+1-9 groups');
|
this.hint.setText(HINT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grid shows the builder's structures, or the factory's units — never both.
|
// Grid shows the builder's structures, or the factory's units — never both.
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,13 @@ import { colorInt } from './TAFx.js';
|
||||||
const CHUNK_PX = 1024;
|
const CHUNK_PX = 1024;
|
||||||
const ZOOMS = [0.5, 0.7, 1.0, 1.4];
|
const ZOOMS = [0.5, 0.7, 1.0, 1.4];
|
||||||
const CHUNK_IDLE_MS = 30000; // reclaim chunks the camera hasn't looked at in a while
|
const CHUNK_IDLE_MS = 30000; // reclaim chunks the camera hasn't looked at in a while
|
||||||
|
const MAX_QUEUE_LINES = 24; // selected units whose order queue is drawn
|
||||||
|
|
||||||
|
/** Waypoint colour per order type, so a queue reads at a glance. */
|
||||||
|
const ORDER_COLORS = {
|
||||||
|
move: 0x7dff9b, attackMove: 0xff8a5a, attack: 0xff5a5a, patrol: 0x8ad4ff,
|
||||||
|
guard: 0xffd27a, assist: 0x8affd0, build: 0x8ad4ff,
|
||||||
|
};
|
||||||
|
|
||||||
export const DEPTHS = {
|
export const DEPTHS = {
|
||||||
terrain: 0, decal: 5, fxUnder: 8, ghost: 12,
|
terrain: 0, decal: 5, fxUnder: 8, ghost: 12,
|
||||||
|
|
@ -59,9 +66,11 @@ export default class TAWorldView {
|
||||||
this._setupFog();
|
this._setupFog();
|
||||||
|
|
||||||
this.gSelection = scene.add.graphics().setDepth(DEPTHS.selection);
|
this.gSelection = scene.add.graphics().setDepth(DEPTHS.selection);
|
||||||
|
this.gOrders = scene.add.graphics().setDepth(DEPTHS.selection);
|
||||||
this.gBars = scene.add.graphics().setDepth(DEPTHS.bars);
|
this.gBars = scene.add.graphics().setDepth(DEPTHS.bars);
|
||||||
this.gGhost = scene.add.graphics().setDepth(DEPTHS.ghost);
|
this.gGhost = scene.add.graphics().setDepth(DEPTHS.ghost);
|
||||||
this._addWorld(this.gSelection);
|
this._addWorld(this.gSelection);
|
||||||
|
this._addWorld(this.gOrders);
|
||||||
this._addWorld(this.gBars);
|
this._addWorld(this.gBars);
|
||||||
this._addWorld(this.gGhost);
|
this._addWorld(this.gGhost);
|
||||||
|
|
||||||
|
|
@ -413,6 +422,7 @@ export default class TAWorldView {
|
||||||
|
|
||||||
for (const id of [...this.sprites.keys()]) if (!live.has(id)) this._releaseSprite(id);
|
for (const id of [...this.sprites.keys()]) if (!live.has(id)) this._releaseSprite(id);
|
||||||
|
|
||||||
|
this._drawOrderQueues();
|
||||||
this._drawPlacementGhost();
|
this._drawPlacementGhost();
|
||||||
|
|
||||||
// Actor depth is recomputed every frame from Y, so the container has to be re-sorted
|
// Actor depth is recomputed every frame from Y, so the container has to be re-sorted
|
||||||
|
|
@ -456,6 +466,61 @@ export default class TAWorldView {
|
||||||
return { nanoLinks, projectiles };
|
return { nanoLinks, projectiles };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw the pending order queue for every selected unit: a waypoint chain from the unit
|
||||||
|
* through each queued order, coloured by order type.
|
||||||
|
*
|
||||||
|
* Queued commands are close to unusable without this — once orders stop replacing each
|
||||||
|
* other, the player has no way to know what a unit has already been told to do, and a
|
||||||
|
* queued build is otherwise indistinguishable from one that failed to register.
|
||||||
|
*/
|
||||||
|
_drawOrderQueues() {
|
||||||
|
const g = this.gOrders;
|
||||||
|
g.clear();
|
||||||
|
if (!this.selection.size) return;
|
||||||
|
|
||||||
|
let drawn = 0;
|
||||||
|
for (const e of this.state.entities) {
|
||||||
|
if (e.dead || e.isBuilding || !this.selection.has(e.id)) continue;
|
||||||
|
if (!e.orders.length) continue;
|
||||||
|
// With a big selection every unit has near-identical orders; drawing all of them is
|
||||||
|
// just cost and clutter.
|
||||||
|
if (++drawn > MAX_QUEUE_LINES) break;
|
||||||
|
|
||||||
|
let px = e.x, py = e.y;
|
||||||
|
for (let i = 0; i < e.orders.length; i++) {
|
||||||
|
const o = e.orders[i];
|
||||||
|
const pt = this._orderPoint(o);
|
||||||
|
if (!pt) continue;
|
||||||
|
const col = ORDER_COLORS[o.type] ?? 0x9fe8ff;
|
||||||
|
const active = i === 0;
|
||||||
|
g.lineStyle(active ? 2.5 : 1.5, col, active ? 0.9 : 0.5);
|
||||||
|
g.lineBetween(px, py, pt.x, pt.y);
|
||||||
|
if (o.type === 'build') {
|
||||||
|
g.lineStyle(2, col, 0.9);
|
||||||
|
g.strokeRect(pt.x - 14, pt.y - 14, 28, 28);
|
||||||
|
} else {
|
||||||
|
g.fillStyle(col, active ? 0.9 : 0.6);
|
||||||
|
g.fillCircle(pt.x, pt.y, active ? 5 : 4);
|
||||||
|
}
|
||||||
|
px = pt.x; py = pt.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** World position an order points at, or null if it has no meaningful location. */
|
||||||
|
_orderPoint(o) {
|
||||||
|
if (o.type === 'move' || o.type === 'attackMove' || o.type === 'patrol') {
|
||||||
|
const x = o.sx ?? o.x, y = o.sy ?? o.y;
|
||||||
|
return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null;
|
||||||
|
}
|
||||||
|
if (o.targetId) {
|
||||||
|
const t = this.state.entities.find((e) => e.id === o.targetId && !e.dead);
|
||||||
|
return t ? { x: t.x, y: t.y } : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
_drawPlacementGhost() {
|
_drawPlacementGhost() {
|
||||||
const g = this.gGhost;
|
const g = this.gGhost;
|
||||||
g.clear();
|
g.clear();
|
||||||
|
|
@ -498,6 +563,7 @@ export default class TAWorldView {
|
||||||
for (const [, chunk] of this.chunks) chunk.rt.destroy();
|
for (const [, chunk] of this.chunks) chunk.rt.destroy();
|
||||||
this.chunks.clear();
|
this.chunks.clear();
|
||||||
this.massSpotG?.destroy();
|
this.massSpotG?.destroy();
|
||||||
|
this.gOrders?.destroy();
|
||||||
this.fogImg?.destroy();
|
this.fogImg?.destroy();
|
||||||
if (this.scene.textures.exists('ta-fog-canvas')) this.scene.textures.remove('ta-fog-canvas');
|
if (this.scene.textures.exists('ta-fog-canvas')) this.scene.textures.remove('ta-fog-canvas');
|
||||||
this._stamp?.destroy();
|
this._stamp?.destroy();
|
||||||
|
|
|
||||||
|
|
@ -374,10 +374,13 @@ export default class TotalAnnihilationGame extends Phaser.Scene {
|
||||||
if (p.middleButtonDown()) { this._panAnchor = { x: p.x, y: p.y }; return; }
|
if (p.middleButtonDown()) { this._panAnchor = { x: p.x, y: p.y }; return; }
|
||||||
|
|
||||||
const w = this.view.worldPoint(p.x, p.y);
|
const w = this.view.worldPoint(p.x, p.y);
|
||||||
if (p.rightButtonDown()) { this._issueContextual(w, p.event?.shiftKey); return; }
|
// CTRL is the queue modifier, as in the original: hold it and every order appends to the
|
||||||
|
// unit's queue instead of replacing it. SHIFT stays on selection (add to selection) and
|
||||||
|
// on the factory buttons (x5), so the two never fight over the same click.
|
||||||
|
if (p.rightButtonDown()) { this._issueContextual(w, queueHeld(p)); return; }
|
||||||
|
|
||||||
if (this.placement) { this._commitPlacement(w, p.event?.shiftKey); return; }
|
if (this.placement) { this._commitPlacement(w, queueHeld(p)); return; }
|
||||||
if (this.pendingCommand) { this._applyPendingCommand(w, p.event?.shiftKey); return; }
|
if (this.pendingCommand) { this._applyPendingCommand(w, queueHeld(p)); return; }
|
||||||
|
|
||||||
this._dragStart = { x: p.x, y: p.y, wx: w.x, wy: w.y, add: !!p.event?.shiftKey };
|
this._dragStart = { x: p.x, y: p.y, wx: w.x, wy: w.y, add: !!p.event?.shiftKey };
|
||||||
this._dragging = false;
|
this._dragging = false;
|
||||||
|
|
@ -656,6 +659,11 @@ function summarise(state, armyIdx) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Is the queue modifier down for this pointer event? */
|
||||||
|
function queueHeld(p) {
|
||||||
|
return !!(p.event?.ctrlKey || p.event?.metaKey);
|
||||||
|
}
|
||||||
|
|
||||||
function readJson(key, fallback) {
|
function readJson(key, fallback) {
|
||||||
try { return JSON.parse(localStorage.getItem(key)) ?? fallback; } catch (_) { return fallback; }
|
try { return JSON.parse(localStorage.getItem(key)) ?? fallback; } catch (_) { return fallback; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,22 @@ section('2c. View layering and first frame');
|
||||||
check('the opening camera frames the player\'s Commander', onScreen,
|
check('the opening camera frames the player\'s Commander', onScreen,
|
||||||
`unit ${own.x.toFixed(0)},${own.y.toFixed(0)} vs view ${cam.x.toFixed(0)},${cam.y.toFixed(0)} ${cam.width}x${cam.height}`);
|
`unit ${own.x.toFixed(0)},${own.y.toFixed(0)} vs view ${cam.x.toFixed(0)},${cam.y.toFixed(0)} ${cam.width}x${cam.height}`);
|
||||||
|
|
||||||
|
// The queue overlay is the only feedback a player gets that a CTRL-queued order
|
||||||
|
// registered at all, so assert it actually strokes something for a queued unit and
|
||||||
|
// nothing at all when the selection is empty.
|
||||||
|
const cmdr = st.entities.find((e) => e.army === 0);
|
||||||
|
const tsz = st.tileSize;
|
||||||
|
L.issueOrder(st, rules, { army: 0, unitIds: [cmdr.id], order: { type: 'move', x: cmdr.x + 3 * tsz, y: cmdr.y } });
|
||||||
|
L.issueOrder(st, rules, { army: 0, unitIds: [cmdr.id], order: { type: 'move', x: cmdr.x + 3 * tsz, y: cmdr.y + 3 * tsz }, queue: true });
|
||||||
|
L.issueOrder(st, rules, { army: 0, unitIds: [cmdr.id], order: { type: 'attackMove', x: cmdr.x, y: cmdr.y + 3 * tsz }, queue: true });
|
||||||
|
view.selection = new Set([cmdr.id]);
|
||||||
|
view.render(0);
|
||||||
|
check('the order-queue overlay draws for a queued unit', view.gOrders.ops > 0, `${view.gOrders.ops} ops`);
|
||||||
|
view.selection = new Set();
|
||||||
|
view.render(0);
|
||||||
|
check('the order-queue overlay draws nothing with no selection', view.gOrders.ops === 0);
|
||||||
|
L.issueOrder(st, rules, { army: 0, unitIds: [cmdr.id], order: { type: 'stop' } });
|
||||||
|
|
||||||
// Wheel zoom must keep the world point under the cursor fixed, at any cursor position —
|
// Wheel zoom must keep the world point under the cursor fixed, at any cursor position —
|
||||||
// including well away from the screen centre, which is where a centre-anchored zoom (or a
|
// including well away from the screen centre, which is where a centre-anchored zoom (or a
|
||||||
// correction computed from a stale camera matrix) visibly drifts.
|
// correction computed from a stale camera matrix) visibly drifts.
|
||||||
|
|
@ -431,6 +447,83 @@ section('4. Pathfinding');
|
||||||
segmentClear(blocked, mc, 1, 0.5 * ts, 0.5 * ts, 2.5 * ts, 0.5 * ts));
|
segmentClear(blocked, mc, 1, 0.5 * ts, 0.5 * ts, 2.5 * ts, 0.5 * ts));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
section('4b. Order queueing (CTRL)');
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
{
|
||||||
|
// Holding CTRL passes `queue: true` to issueOrder. The engine contract that has to hold:
|
||||||
|
// a queued order APPENDS and leaves the current one running, an unqueued one REPLACES the
|
||||||
|
// lot, and the unit then works through them in order.
|
||||||
|
const map = generateMap(rules, { seed: 61, size: 'small', symmetry: 'mirror-x' });
|
||||||
|
const st = L.createMatch(rules, { seed: 61, map, armies: [{ armyId: 'arm' }, { armyId: 'core' }] });
|
||||||
|
const cmd = st.entities.find((e) => e.army === 0);
|
||||||
|
const ts = st.tileSize;
|
||||||
|
|
||||||
|
const pt = (dx, dy) => ({ x: cmd.x + dx * ts, y: cmd.y + dy * ts });
|
||||||
|
const a = pt(3, 0), b = pt(3, 3), c = pt(0, 3);
|
||||||
|
L.issueOrder(st, rules, { army: 0, unitIds: [cmd.id], order: { type: 'move', ...a } });
|
||||||
|
check('first order replaces an empty queue', cmd.orders.length === 1);
|
||||||
|
L.issueOrder(st, rules, { army: 0, unitIds: [cmd.id], order: { type: 'move', ...b }, queue: true });
|
||||||
|
L.issueOrder(st, rules, { army: 0, unitIds: [cmd.id], order: { type: 'move', ...c }, queue: true });
|
||||||
|
check('queued orders append', cmd.orders.length === 3, `${cmd.orders.length}`);
|
||||||
|
check('the queue keeps its issue order',
|
||||||
|
Math.abs(cmd.orders[0].x - a.x) < 1 && Math.abs(cmd.orders[2].x - c.x) < 1);
|
||||||
|
|
||||||
|
// An unqueued order wipes the queue — the standard "plain click cancels everything" rule.
|
||||||
|
L.issueOrder(st, rules, { army: 0, unitIds: [cmd.id], order: { type: 'move', ...a } });
|
||||||
|
check('an unqueued order clears the queue', cmd.orders.length === 1, `${cmd.orders.length}`);
|
||||||
|
|
||||||
|
// Run a three-leg queue and confirm it is actually consumed in sequence.
|
||||||
|
L.issueOrder(st, rules, { army: 0, unitIds: [cmd.id], order: { type: 'move', ...b }, queue: true });
|
||||||
|
L.issueOrder(st, rules, { army: 0, unitIds: [cmd.id], order: { type: 'move', ...c }, queue: true });
|
||||||
|
const seen = [cmd.orders.length];
|
||||||
|
for (let i = 0; i < 240 * HZ && cmd.orders.length; i++) {
|
||||||
|
L.tick(st, rules);
|
||||||
|
if (cmd.orders.length !== seen[seen.length - 1]) seen.push(cmd.orders.length);
|
||||||
|
}
|
||||||
|
check('a queued route is consumed one leg at a time',
|
||||||
|
seen.join(',') === '3,2,1,0', seen.join(','));
|
||||||
|
check('the unit ends up at the final waypoint',
|
||||||
|
Math.hypot(cmd.x - c.x, cmd.y - c.y) < ts * 2,
|
||||||
|
`${Math.hypot(cmd.x - c.x, cmd.y - c.y).toFixed(0)}px away`);
|
||||||
|
|
||||||
|
// Queued BUILD orders: each places its site immediately (so the player sees the ghosts)
|
||||||
|
// while the builder works through them one at a time.
|
||||||
|
const st2 = L.createMatch(rules, { seed: 62, map, armies: [{ armyId: 'arm' }, { armyId: 'core' }] });
|
||||||
|
const b2 = st2.armies[0];
|
||||||
|
b2.mass = 99999; b2.energy = 99999; b2.massCap = 99999; b2.energyCap = 99999;
|
||||||
|
const builder = st2.entities.find((e) => e.army === 0);
|
||||||
|
const gen = rules.buildingById.energygen;
|
||||||
|
// Candidate spots must clear each other by the building's own footprint, or siting the
|
||||||
|
// first one blocks the second and the "queue" under test never forms.
|
||||||
|
const spots = [];
|
||||||
|
const step = gen.footprint.w + 1;
|
||||||
|
for (let d = 2; d < 30 && spots.length < 3; d += step) {
|
||||||
|
const tx = worldToTileX(st2.nav, builder.x) + d;
|
||||||
|
const ty = worldToTileY(st2.nav, builder.y);
|
||||||
|
if (L.canPlaceAt(st2, rules, tx, ty, gen).ok) spots.push({ tx, ty });
|
||||||
|
}
|
||||||
|
check('found room for three queued generators', spots.length === 3, `${spots.length}`);
|
||||||
|
let queuedOk = 0;
|
||||||
|
spots.forEach((sp, i) => {
|
||||||
|
const r = L.issueOrder(st2, rules, {
|
||||||
|
army: 0, unitIds: [builder.id],
|
||||||
|
order: { type: 'build', defId: 'energygen', tx: sp.tx, ty: sp.ty }, queue: i > 0,
|
||||||
|
});
|
||||||
|
if (r.ok) queuedOk++;
|
||||||
|
});
|
||||||
|
check('three build orders queue onto one builder', queuedOk === 3, `${queuedOk}`);
|
||||||
|
check('every queued building is sited straight away',
|
||||||
|
st2.entities.filter((e) => e.defId === 'energygen').length === 3);
|
||||||
|
check('the builder holds all three in its queue', builder.orders.length === 3, `${builder.orders.length}`);
|
||||||
|
for (let i = 0; i < 400 * HZ && builder.orders.length; i++) {
|
||||||
|
b2.mass = 99999; b2.energy = 99999;
|
||||||
|
L.tick(st2, rules);
|
||||||
|
}
|
||||||
|
const finished = st2.entities.filter((e) => e.defId === 'energygen' && !e.site && !e.dead).length;
|
||||||
|
check('a queued build list completes', finished === 3, `${finished}/3 built`);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
section('5. Movement, separation and size classes');
|
section('5. Movement, separation and size classes');
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue