Optimize mover rendering and segment plan pipeline

This commit is contained in:
scamiv
2026-02-27 15:06:48 +01:00
parent 83a8dc00e4
commit 0a96ab8e30
17 changed files with 939 additions and 377 deletions
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { GameMapImpl } from "../src/core/game/GameMap";
import { densePathToKeypointSegments } from "../src/core/game/MotionPlans";
import { MiniMapTransformer } from "../src/core/pathfinding/transformers/MiniMapTransformer";
function makeMap(width: number, height: number): GameMapImpl {
return new GameMapImpl(width, height, new Uint8Array(width * height), 0);
@@ -58,3 +59,51 @@ describe("densePathToKeypointSegments", () => {
expect(expanded).toEqual(dense.map((t) => t >>> 0));
});
});
describe("MiniMapTransformer planSegments compression", () => {
it("preserves endpoints and total steps while merging collinear runs", () => {
const map = makeMap(10, 10);
const miniMap = makeMap(5, 5);
const miniPath = [
miniMap.ref(0, 0),
miniMap.ref(1, 0),
miniMap.ref(2, 0),
miniMap.ref(2, 1),
miniMap.ref(2, 2),
];
const inner = {
findPath() {
return miniPath.slice();
},
planSegments() {
return {
points: Uint32Array.from(miniPath),
segmentSteps: Uint32Array.from([1, 1, 1, 1]),
};
},
};
const transformer = new MiniMapTransformer(inner as any, map, miniMap);
const from = map.ref(0, 0);
const to = map.ref(4, 4);
const dense = transformer.findPath(from, to);
expect(dense).not.toBeNull();
const plan = transformer.planSegments(from, to);
expect(plan).not.toBeNull();
if (!plan) return;
expect(Array.from(plan.points)).toEqual([
from >>> 0,
map.ref(4, 0) >>> 0,
to >>> 0,
]);
expect(Array.from(plan.segmentSteps)).toEqual([4, 4]);
const totalSteps = Array.from(plan.segmentSteps).reduce((a, b) => a + b, 0);
expect(totalSteps).toBe(8);
});
});
+2 -2
View File
@@ -3,7 +3,7 @@ import { PathFinderStepper } from "../src/core/pathfinding/PathFinderStepper";
import { PathStatus } from "../src/core/pathfinding/types";
describe("PathFinderStepper cache priming", () => {
it("does not prime next() cache via findPath()", () => {
it("primes next() cache via findPath()", () => {
let calls = 0;
const finder = {
findPath(from: number | number[], to: number) {
@@ -29,6 +29,6 @@ describe("PathFinderStepper cache priming", () => {
if (r1.status === PathStatus.NEXT) {
expect(r1.node).toBe(to);
}
expect(calls).toBe(2);
expect(calls).toBe(1);
});
});
+42
View File
@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import { pruneInactiveTrails } from "../src/client/graphics/layers/TrailLifecycle";
describe("UnitLayer trail lifecycle helpers", () => {
it("removes transport and nuke trails for inactive units", () => {
const nukeTrails = new Map<number, number[]>([
[10, [1, 2, 3]],
[11, [4, 5]],
]);
const transportTrails = new Map<number, { xy: number[] }>([
[10, { xy: [1, 1, 2, 2] }],
[12, { xy: [5, 5, 6, 6] }],
]);
const result = pruneInactiveTrails(
nukeTrails,
transportTrails,
(unitId) => unitId === 11,
);
expect(result).toEqual({ removedNukes: 1, removedTransport: 2 });
expect(Array.from(nukeTrails.keys())).toEqual([11]);
expect(transportTrails.size).toBe(0);
});
it("keeps all trails when units are active", () => {
const nukeTrails = new Map<number, number[]>([[1, [1]]]);
const transportTrails = new Map<number, { xy: number[] }>([
[2, { xy: [0, 0, 1, 1] }],
]);
const result = pruneInactiveTrails(
nukeTrails,
transportTrails,
() => true,
);
expect(result).toEqual({ removedNukes: 0, removedTransport: 0 });
expect(nukeTrails.size).toBe(1);
expect(transportTrails.size).toBe(1);
});
});
+53
View File
@@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import {
UnitMotionRenderQueue,
UnitMotionRenderQueueEntry,
} from "../src/client/graphics/layers/UnitMotionRenderQueue";
describe("UnitMotionRenderQueue", () => {
it("returns highest-priority entry first", () => {
const queue = new UnitMotionRenderQueue();
queue.enqueue({
unitId: 1,
version: 1,
priority: 10,
onScreenHint: false,
});
queue.enqueue({
unitId: 2,
version: 1,
priority: 20,
onScreenHint: true,
});
const first = queue.pollValid(() => true);
expect(first?.unitId).toBe(2);
});
it("skips stale entries when validator rejects old versions", () => {
const queue = new UnitMotionRenderQueue();
const latestVersion = new Map<number, number>([[42, 2]]);
const stale: UnitMotionRenderQueueEntry = {
unitId: 42,
version: 1,
priority: 100,
onScreenHint: true,
};
const fresh: UnitMotionRenderQueueEntry = {
unitId: 42,
version: 2,
priority: 50,
onScreenHint: true,
};
queue.enqueue(stale);
queue.enqueue(fresh);
const picked = queue.pollValid((entry) => {
return latestVersion.get(entry.unitId) === entry.version;
});
expect(picked).toEqual(fresh);
});
});