mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-19 02:04:39 +00:00
Restore smooth nuke motion and terrain updates
This commit is contained in:
@@ -89,6 +89,31 @@ export class RendererStatusPanel extends LitElement implements Layer {
|
|||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.titleActions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 4px 7px;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
background: rgba(255, 255, 255, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
.panel.dragging .title {
|
.panel.dragging .title {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
@@ -204,6 +229,32 @@ export class RendererStatusPanel extends LitElement implements Layer {
|
|||||||
this.preference = this.userSettings.territoryRenderer();
|
this.preference = this.userSettings.territoryRenderer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private rendererSettingsTarget(): "webgl" | "webgpu" | null {
|
||||||
|
if (this.activeRenderer === "webgl" || this.activeRenderer === "webgpu") {
|
||||||
|
return this.activeRenderer;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.activeRenderer === null &&
|
||||||
|
(this.preference === "webgl" || this.preference === "webgpu")
|
||||||
|
) {
|
||||||
|
return this.preference;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private openRendererSettings(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const renderer = this.rendererSettingsTarget();
|
||||||
|
if (renderer === "webgl") {
|
||||||
|
this.userSettings.setWebgpuDebug(false);
|
||||||
|
this.userSettings.setWebglDebug(true);
|
||||||
|
} else if (renderer === "webgpu") {
|
||||||
|
this.userSettings.setWebglDebug(false);
|
||||||
|
this.userSettings.setWebgpuDebug(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private rendererLabel(id: TerritoryRendererId | TerritoryRendererPreference) {
|
private rendererLabel(id: TerritoryRendererId | TerritoryRendererPreference) {
|
||||||
if (id === "webgpu") return "WebGPU";
|
if (id === "webgpu") return "WebGPU";
|
||||||
if (id === "webgl") return "WebGL";
|
if (id === "webgl") return "WebGL";
|
||||||
@@ -341,6 +392,7 @@ export class RendererStatusPanel extends LitElement implements Layer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const note = this.statusNote();
|
const note = this.statusNote();
|
||||||
|
const canOpenSettings = this.rendererSettingsTarget() !== null;
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="panel ${this.isDragging ? "dragging" : ""}"
|
class="panel ${this.isDragging ? "dragging" : ""}"
|
||||||
@@ -349,6 +401,16 @@ export class RendererStatusPanel extends LitElement implements Layer {
|
|||||||
>
|
>
|
||||||
<div class="title" @pointerdown=${this.handleDragPointerDown}>
|
<div class="title" @pointerdown=${this.handleDragPointerDown}>
|
||||||
<span>Renderer</span>
|
<span>Renderer</span>
|
||||||
|
<div class="titleActions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
?disabled=${!canOpenSettings}
|
||||||
|
@pointerdown=${this.stopPointerEvent}
|
||||||
|
@click=${this.openRendererSettings}
|
||||||
|
>
|
||||||
|
settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@@ -60,28 +60,22 @@ const TRANSPORT_SHIP_MASK = [
|
|||||||
".BTB.",
|
".BTB.",
|
||||||
"..B..",
|
"..B..",
|
||||||
] as const;
|
] as const;
|
||||||
const TRADE_SHIP_MASK = [
|
const TRADE_SHIP_MASK = ["..T..", ".TBT.", "TBBBT", ".TBT.", "..T.."] as const;
|
||||||
"..T..",
|
|
||||||
".TBT.",
|
|
||||||
"TBBBT",
|
|
||||||
".TBT.",
|
|
||||||
"..T..",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
type TransportTrailState = {
|
type MotionTrailState = {
|
||||||
activePlanId: number;
|
activePlanId: number;
|
||||||
epochs: TransportTrailEpoch[];
|
epochs: MotionTrailEpoch[];
|
||||||
lastOnScreen: boolean;
|
lastOnScreen: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TransportTrailEpoch = SegmentTrailPlanView & {
|
type MotionTrailEpoch = SegmentTrailPlanView & {
|
||||||
planId: number;
|
planId: number;
|
||||||
targetStep: number;
|
targetStep: number;
|
||||||
drawnStep: number;
|
drawnStep: number;
|
||||||
sealed: boolean;
|
sealed: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ActiveTransportTrailPlan = {
|
type ActiveMotionTrailPlan = {
|
||||||
unitId: number;
|
unitId: number;
|
||||||
unit: UnitView;
|
unit: UnitView;
|
||||||
plan: SegmentTrailPlanView & { planId: number };
|
plan: SegmentTrailPlanView & { planId: number };
|
||||||
@@ -134,7 +128,7 @@ export class UnitLayer implements Layer {
|
|||||||
|
|
||||||
private gridMoverUnitIds = new Set<number>();
|
private gridMoverUnitIds = new Set<number>();
|
||||||
|
|
||||||
private transportShipTrails = new Map<number, TransportTrailState>();
|
private segmentTrails = new Map<number, MotionTrailState>();
|
||||||
private trailDirty = false;
|
private trailDirty = false;
|
||||||
|
|
||||||
private moverState = new Map<number, MoverRenderState>();
|
private moverState = new Map<number, MoverRenderState>();
|
||||||
@@ -201,7 +195,7 @@ export class UnitLayer implements Layer {
|
|||||||
tick() {
|
tick() {
|
||||||
const trailPrune = pruneInactiveTrails(
|
const trailPrune = pruneInactiveTrails(
|
||||||
this.unitToTrail,
|
this.unitToTrail,
|
||||||
this.transportShipTrails,
|
this.segmentTrails,
|
||||||
(unitId) => {
|
(unitId) => {
|
||||||
const current = this.game.unit(unitId);
|
const current = this.game.unit(unitId);
|
||||||
return !!current && current.isActive();
|
return !!current && current.isActive();
|
||||||
@@ -482,13 +476,13 @@ export class UnitLayer implements Layer {
|
|||||||
const tickFloat = this.game.ticks() + tickAlpha;
|
const tickFloat = this.game.ticks() + tickAlpha;
|
||||||
const viewBounds = this.currentViewBounds();
|
const viewBounds = this.currentViewBounds();
|
||||||
const activeMoverIds = new Set<number>();
|
const activeMoverIds = new Set<number>();
|
||||||
const activeTransportTrailPlans: ActiveTransportTrailPlan[] = [];
|
const activeMotionTrailPlans: ActiveMotionTrailPlan[] = [];
|
||||||
|
|
||||||
for (const [unitId, plan] of this.game.motionPlans()) {
|
for (const [unitId, plan] of this.game.motionPlans()) {
|
||||||
const unit = this.game.unit(unitId);
|
const unit = this.game.unit(unitId);
|
||||||
if (!unit || !unit.isActive()) {
|
if (!unit || !unit.isActive()) {
|
||||||
this.clearMoverState(unitId);
|
this.clearMoverState(unitId);
|
||||||
if (this.transportShipTrails.delete(unitId)) this.trailDirty = true;
|
if (this.segmentTrails.delete(unitId)) this.trailDirty = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
activeMoverIds.add(unitId);
|
activeMoverIds.add(unitId);
|
||||||
@@ -500,8 +494,8 @@ export class UnitLayer implements Layer {
|
|||||||
tickFloat,
|
tickFloat,
|
||||||
viewBounds,
|
viewBounds,
|
||||||
);
|
);
|
||||||
if (unit.type() === UnitType.TransportShip) {
|
if (this.shouldDrawSegmentTrail(unit)) {
|
||||||
activeTransportTrailPlans.push({
|
activeMotionTrailPlans.push({
|
||||||
unitId,
|
unitId,
|
||||||
unit,
|
unit,
|
||||||
plan,
|
plan,
|
||||||
@@ -530,10 +524,7 @@ export class UnitLayer implements Layer {
|
|||||||
viewBounds,
|
viewBounds,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.advanceAndDrawTransportTrails(
|
this.advanceAndDrawSegmentTrails(this.game.ticks(), activeMotionTrailPlans);
|
||||||
this.game.ticks(),
|
|
||||||
activeTransportTrailPlans,
|
|
||||||
);
|
|
||||||
this.rebuildTrailCanvasIfDirty();
|
this.rebuildTrailCanvasIfDirty();
|
||||||
|
|
||||||
context.drawImage(
|
context.drawImage(
|
||||||
@@ -960,7 +951,9 @@ export class UnitLayer implements Layer {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const candidateRects: MoverSpriteRect[] = [candidateState.lastSpriteRect];
|
const candidateRects: MoverSpriteRect[] = [
|
||||||
|
candidateState.lastSpriteRect,
|
||||||
|
];
|
||||||
const candidateSample = this.getConflictSample(
|
const candidateSample = this.getConflictSample(
|
||||||
candidateId,
|
candidateId,
|
||||||
tickFloat,
|
tickFloat,
|
||||||
@@ -998,7 +991,13 @@ export class UnitLayer implements Layer {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getMoverSample(unitId, unit, plan.planId, tickFloat, sampledCache);
|
return this.getMoverSample(
|
||||||
|
unitId,
|
||||||
|
unit,
|
||||||
|
plan.planId,
|
||||||
|
tickFloat,
|
||||||
|
sampledCache,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private anyRectsOverlap(
|
private anyRectsOverlap(
|
||||||
@@ -1227,10 +1226,7 @@ export class UnitLayer implements Layer {
|
|||||||
|
|
||||||
private rectsOverlap(a: MoverSpriteRect, b: MoverSpriteRect): boolean {
|
private rectsOverlap(a: MoverSpriteRect, b: MoverSpriteRect): boolean {
|
||||||
return (
|
return (
|
||||||
a.x < b.x + b.w &&
|
a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y
|
||||||
a.x + a.w > b.x &&
|
|
||||||
a.y < b.y + b.h &&
|
|
||||||
a.y + a.h > b.y
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1304,7 +1300,8 @@ export class UnitLayer implements Layer {
|
|||||||
|
|
||||||
while (
|
while (
|
||||||
idx > 0 &&
|
idx > 0 &&
|
||||||
zoom < DYNAMIC_MOVER_ZOOM_THRESHOLDS[idx - 1] - DYNAMIC_MOVER_ZOOM_HYSTERESIS
|
zoom <
|
||||||
|
DYNAMIC_MOVER_ZOOM_THRESHOLDS[idx - 1] - DYNAMIC_MOVER_ZOOM_HYSTERESIS
|
||||||
) {
|
) {
|
||||||
idx--;
|
idx--;
|
||||||
}
|
}
|
||||||
@@ -1344,7 +1341,8 @@ export class UnitLayer implements Layer {
|
|||||||
|
|
||||||
this.lastDynamicMoverCanvasRescaleMs =
|
this.lastDynamicMoverCanvasRescaleMs =
|
||||||
this.rebuildDynamicMoverCanvas(targetScale);
|
this.rebuildDynamicMoverCanvas(targetScale);
|
||||||
this.totalDynamicMoverCanvasRescaleMs += this.lastDynamicMoverCanvasRescaleMs;
|
this.totalDynamicMoverCanvasRescaleMs +=
|
||||||
|
this.lastDynamicMoverCanvasRescaleMs;
|
||||||
this.dynamicMoverCanvasRescaleCount++;
|
this.dynamicMoverCanvasRescaleCount++;
|
||||||
this.dynamicMoverCanvasScale = targetScale;
|
this.dynamicMoverCanvasScale = targetScale;
|
||||||
this.lastDynamicMoverCanvasScaleChangeAtMs = nowMs;
|
this.lastDynamicMoverCanvasScaleChangeAtMs = nowMs;
|
||||||
@@ -1371,8 +1369,14 @@ export class UnitLayer implements Layer {
|
|||||||
const oldHeight = oldCanvas.height;
|
const oldHeight = oldCanvas.height;
|
||||||
|
|
||||||
this.dynamicMoverCanvas = document.createElement("canvas");
|
this.dynamicMoverCanvas = document.createElement("canvas");
|
||||||
this.dynamicMoverCanvas.width = Math.max(1, this.game.width() * targetScale);
|
this.dynamicMoverCanvas.width = Math.max(
|
||||||
this.dynamicMoverCanvas.height = Math.max(1, this.game.height() * targetScale);
|
1,
|
||||||
|
this.game.width() * targetScale,
|
||||||
|
);
|
||||||
|
this.dynamicMoverCanvas.height = Math.max(
|
||||||
|
1,
|
||||||
|
this.game.height() * targetScale,
|
||||||
|
);
|
||||||
const dynamicMoverContext = this.dynamicMoverCanvas.getContext("2d");
|
const dynamicMoverContext = this.dynamicMoverCanvas.getContext("2d");
|
||||||
if (dynamicMoverContext === null) {
|
if (dynamicMoverContext === null) {
|
||||||
throw new Error("2d context not supported");
|
throw new Error("2d context not supported");
|
||||||
@@ -1681,35 +1685,35 @@ export class UnitLayer implements Layer {
|
|||||||
this.dynamicMoverContext.clearRect(rect.x, rect.y, rect.w, rect.h);
|
this.dynamicMoverContext.clearRect(rect.x, rect.y, rect.w, rect.h);
|
||||||
}
|
}
|
||||||
|
|
||||||
private advanceAndDrawTransportTrails(
|
private advanceAndDrawSegmentTrails(
|
||||||
currentTick: number,
|
currentTick: number,
|
||||||
activePlans: readonly ActiveTransportTrailPlan[],
|
activePlans: readonly ActiveMotionTrailPlan[],
|
||||||
): void {
|
): void {
|
||||||
for (const { unitId, unit, plan, maybeOnScreen } of activePlans) {
|
for (const { unitId, unit, plan, maybeOnScreen } of activePlans) {
|
||||||
const state = this.ensureTransportTrailState(unitId, plan, currentTick);
|
const state = this.ensureSegmentTrailState(unitId, plan, currentTick);
|
||||||
const moverState = this.moverState.get(unitId);
|
const moverState = this.moverState.get(unitId);
|
||||||
const onScreen = moverState ? moverState.bucket === "on" : maybeOnScreen;
|
const onScreen = moverState ? moverState.bucket === "on" : maybeOnScreen;
|
||||||
|
|
||||||
if (onScreen) {
|
if (onScreen) {
|
||||||
this.drawPendingTransportTrailEpochs(unit, state);
|
this.drawPendingSegmentTrailEpochs(unit, state);
|
||||||
}
|
}
|
||||||
state.lastOnScreen = onScreen;
|
state.lastOnScreen = onScreen;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureTransportTrailState(
|
private ensureSegmentTrailState(
|
||||||
unitId: number,
|
unitId: number,
|
||||||
plan: SegmentTrailPlanView & { planId: number },
|
plan: SegmentTrailPlanView & { planId: number },
|
||||||
currentTick: number,
|
currentTick: number,
|
||||||
): TransportTrailState {
|
): MotionTrailState {
|
||||||
let state = this.transportShipTrails.get(unitId);
|
let state = this.segmentTrails.get(unitId);
|
||||||
if (!state) {
|
if (!state) {
|
||||||
state = {
|
state = {
|
||||||
activePlanId: plan.planId,
|
activePlanId: plan.planId,
|
||||||
epochs: [],
|
epochs: [],
|
||||||
lastOnScreen: false,
|
lastOnScreen: false,
|
||||||
};
|
};
|
||||||
this.transportShipTrails.set(unitId, state);
|
this.segmentTrails.set(unitId, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
let activeEpoch = state.epochs[state.epochs.length - 1];
|
let activeEpoch = state.epochs[state.epochs.length - 1];
|
||||||
@@ -1726,7 +1730,7 @@ export class UnitLayer implements Layer {
|
|||||||
activeEpoch.sealed = true;
|
activeEpoch.sealed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
activeEpoch = this.createTransportTrailEpoch(plan, currentTick);
|
activeEpoch = this.createSegmentTrailEpoch(plan, currentTick);
|
||||||
state.epochs.push(activeEpoch);
|
state.epochs.push(activeEpoch);
|
||||||
state.activePlanId = plan.planId;
|
state.activePlanId = plan.planId;
|
||||||
return state;
|
return state;
|
||||||
@@ -1741,10 +1745,10 @@ export class UnitLayer implements Layer {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
private createTransportTrailEpoch(
|
private createSegmentTrailEpoch(
|
||||||
plan: SegmentTrailPlanView & { planId: number },
|
plan: SegmentTrailPlanView & { planId: number },
|
||||||
currentTick: number,
|
currentTick: number,
|
||||||
): TransportTrailEpoch {
|
): MotionTrailEpoch {
|
||||||
return {
|
return {
|
||||||
planId: plan.planId,
|
planId: plan.planId,
|
||||||
startTick: plan.startTick,
|
startTick: plan.startTick,
|
||||||
@@ -1758,9 +1762,9 @@ export class UnitLayer implements Layer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawPendingTransportTrailEpochs(
|
private drawPendingSegmentTrailEpochs(
|
||||||
unit: UnitView,
|
unit: UnitView,
|
||||||
state: TransportTrailState,
|
state: MotionTrailState,
|
||||||
): void {
|
): void {
|
||||||
const ctx = this.trailContext;
|
const ctx = this.trailContext;
|
||||||
const strokeStyle = this.motionTrailColor(unit);
|
const strokeStyle = this.motionTrailColor(unit);
|
||||||
@@ -1816,7 +1820,7 @@ export class UnitLayer implements Layer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [unitId, trailState] of this.transportShipTrails) {
|
for (const [unitId, trailState] of this.segmentTrails) {
|
||||||
const unit = this.game.unit(unitId);
|
const unit = this.game.unit(unitId);
|
||||||
if (!unit || !unit.isActive()) {
|
if (!unit || !unit.isActive()) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1837,6 +1841,17 @@ export class UnitLayer implements Layer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shouldDrawSegmentTrail(unit: UnitView): boolean {
|
||||||
|
const type = unit.type();
|
||||||
|
return (
|
||||||
|
type === UnitType.TransportShip ||
|
||||||
|
type === UnitType.AtomBomb ||
|
||||||
|
type === UnitType.HydrogenBomb ||
|
||||||
|
type === UnitType.MIRV ||
|
||||||
|
type === UnitType.MIRVWarhead
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private relationshipForAlternateView(unit: UnitView): Relationship {
|
private relationshipForAlternateView(unit: UnitView): Relationship {
|
||||||
let rel = this.relationship(unit);
|
let rel = this.relationship(unit);
|
||||||
const dstPortId = unit.targetUnitId();
|
const dstPortId = unit.targetUnitId();
|
||||||
|
|||||||
@@ -467,10 +467,41 @@ export class WebGLTerritoryBackend implements TerritoryBackend {
|
|||||||
root.style.touchAction = "none";
|
root.style.touchAction = "none";
|
||||||
|
|
||||||
const title = document.createElement("div");
|
const title = document.createElement("div");
|
||||||
title.textContent = "Territory smoothing";
|
|
||||||
title.style.fontWeight = "700";
|
title.style.fontWeight = "700";
|
||||||
title.style.marginBottom = "6px";
|
title.style.marginBottom = "6px";
|
||||||
title.style.cursor = "move";
|
title.style.cursor = "move";
|
||||||
|
title.style.display = "flex";
|
||||||
|
title.style.alignItems = "center";
|
||||||
|
title.style.justifyContent = "space-between";
|
||||||
|
title.style.gap = "10px";
|
||||||
|
|
||||||
|
const titleText = document.createElement("span");
|
||||||
|
titleText.textContent = "Territory smoothing";
|
||||||
|
title.appendChild(titleText);
|
||||||
|
|
||||||
|
const closeButton = document.createElement("button");
|
||||||
|
closeButton.type = "button";
|
||||||
|
closeButton.textContent = "x";
|
||||||
|
closeButton.title = "Close";
|
||||||
|
closeButton.style.width = "22px";
|
||||||
|
closeButton.style.height = "22px";
|
||||||
|
closeButton.style.border = "1px solid rgba(255,255,255,0.18)";
|
||||||
|
closeButton.style.borderRadius = "5px";
|
||||||
|
closeButton.style.background = "rgba(255,255,255,0.08)";
|
||||||
|
closeButton.style.color = "rgba(255,255,255,0.88)";
|
||||||
|
closeButton.style.font = "12px monospace";
|
||||||
|
closeButton.style.lineHeight = "1";
|
||||||
|
closeButton.style.cursor = "pointer";
|
||||||
|
closeButton.addEventListener("pointerdown", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
closeButton.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.userSettings.setWebglDebug(false);
|
||||||
|
});
|
||||||
|
title.appendChild(closeButton);
|
||||||
root.appendChild(title);
|
root.appendChild(title);
|
||||||
|
|
||||||
// Restore last position (if any)
|
// Restore last position (if any)
|
||||||
|
|||||||
@@ -106,6 +106,22 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
|||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
border-radius: 5px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: rgba(255, 255, 255, 0.88);
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
.metrics {
|
.metrics {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
@@ -228,7 +244,7 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private selectedShaderId() {
|
private selectedShaderId() {
|
||||||
const selected = this.userSettings.getInt(TERRITORY_SHADER_KEY, 0);
|
const selected = this.userSettings.getInt(TERRITORY_SHADER_KEY, 1);
|
||||||
return territoryShaderIdFromInt(selected);
|
return territoryShaderIdFromInt(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +257,7 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private selectedTerrainShaderId() {
|
private selectedTerrainShaderId() {
|
||||||
const selected = this.userSettings.getInt(TERRAIN_SHADER_KEY, 0);
|
const selected = this.userSettings.getInt(TERRAIN_SHADER_KEY, 2);
|
||||||
return terrainShaderIdFromInt(selected);
|
return terrainShaderIdFromInt(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +282,7 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private selectedPostSmoothingId() {
|
private selectedPostSmoothingId() {
|
||||||
const selected = this.userSettings.getInt(TERRITORY_POST_SMOOTHING_KEY, 0);
|
const selected = this.userSettings.getInt(TERRITORY_POST_SMOOTHING_KEY, 1);
|
||||||
return territoryPostSmoothingIdFromInt(selected);
|
return territoryPostSmoothingIdFromInt(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,6 +419,12 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private closeOverlay(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.userSettings.setWebgpuDebug(false);
|
||||||
|
}
|
||||||
|
|
||||||
private handleDragPointerDown(event: PointerEvent) {
|
private handleDragPointerDown(event: PointerEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -486,6 +508,15 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
|||||||
>
|
>
|
||||||
<div class="title" @pointerdown=${this.handleDragPointerDown}>
|
<div class="title" @pointerdown=${this.handleDragPointerDown}>
|
||||||
<div>WebGPU Debug</div>
|
<div>WebGPU Debug</div>
|
||||||
|
<button
|
||||||
|
class="closeButton"
|
||||||
|
type="button"
|
||||||
|
title="Close"
|
||||||
|
@pointerdown=${this.stopPointerEvent}
|
||||||
|
@click=${this.closeOverlay}
|
||||||
|
>
|
||||||
|
x
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="metrics">
|
<div class="metrics">
|
||||||
|
|||||||
@@ -121,6 +121,11 @@ export class WebGPUTerritoryBackend implements TerritoryBackend {
|
|||||||
this.markTile(updatedTiles[i]);
|
this.markTile(updatedTiles[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatedTerrainTiles = this.game.recentlyUpdatedTerrainTiles();
|
||||||
|
if (updatedTerrainTiles.length > 0) {
|
||||||
|
this.territoryRenderer?.updateTerrainDataTiles(updatedTerrainTiles);
|
||||||
|
}
|
||||||
|
|
||||||
// After collecting pending updates and handling palette/theme changes,
|
// After collecting pending updates and handling palette/theme changes,
|
||||||
// invoke the renderer's tick() to process compute passes. This ensures
|
// invoke the renderer's tick() to process compute passes. This ensures
|
||||||
// compute shaders run at the simulation rate rather than every frame.
|
// compute shaders run at the simulation rate rather than every frame.
|
||||||
|
|||||||
@@ -470,6 +470,16 @@ export class TerritoryRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateTerrainDataTiles(tiles: readonly number[]): void {
|
||||||
|
if (!this.resources || !this.device || tiles.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.resources.uploadTerrainDataTiles(tiles);
|
||||||
|
if (this.terrainComputePass) {
|
||||||
|
this.terrainComputePass.markDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Immediately execute terrain compute pass (for theme changes).
|
* Immediately execute terrain compute pass (for theme changes).
|
||||||
* This ensures terrain is recomputed before the next render.
|
* This ensures terrain is recomputed before the next render.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Theme } from "../../../../core/configuration/Config";
|
import { Theme } from "../../../../core/configuration/Config";
|
||||||
import { UnitType } from "../../../../core/game/Game";
|
import { UnitType } from "../../../../core/game/Game";
|
||||||
|
import type { TileRef } from "../../../../core/game/GameMap";
|
||||||
import { GameView } from "../../../../core/game/GameView";
|
import { GameView } from "../../../../core/game/GameView";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -606,6 +607,53 @@ export class GroundTruthData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadTerrainDataTiles(tiles: readonly TileRef[]): void {
|
||||||
|
if (tiles.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.needsTerrainDataUpload) {
|
||||||
|
this.uploadTerrainData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowBounds = new Map<number, { minX: number; maxX: number }>();
|
||||||
|
for (let i = 0; i < tiles.length; i++) {
|
||||||
|
const tile = tiles[i] >>> 0;
|
||||||
|
if (tile >= this.terrainData.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const y = Math.floor(tile / this.mapWidth);
|
||||||
|
if (y < 0 || y >= this.mapHeight) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const x = tile - y * this.mapWidth;
|
||||||
|
const existing = rowBounds.get(y);
|
||||||
|
if (existing) {
|
||||||
|
existing.minX = Math.min(existing.minX, x);
|
||||||
|
existing.maxX = Math.max(existing.maxX, x);
|
||||||
|
} else {
|
||||||
|
rowBounds.set(y, { minX: x, maxX: x });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [y, bounds] of rowBounds) {
|
||||||
|
const width = bounds.maxX - bounds.minX + 1;
|
||||||
|
const paddedBytesPerRow = align(width, 256);
|
||||||
|
const row = new Uint8Array(paddedBytesPerRow);
|
||||||
|
const start = y * this.mapWidth + bounds.minX;
|
||||||
|
row.set(this.terrainData.subarray(start, start + width), 0);
|
||||||
|
this.device.queue.writeTexture(
|
||||||
|
{
|
||||||
|
texture: this.terrainDataTexture,
|
||||||
|
origin: { x: bounds.minX, y },
|
||||||
|
},
|
||||||
|
row,
|
||||||
|
{ bytesPerRow: paddedBytesPerRow, rowsPerImage: 1 },
|
||||||
|
{ width, height: 1, depthOrArrayLayers: 1 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uploadTerrainParams(): void {
|
uploadTerrainParams(): void {
|
||||||
if (!this.needsTerrainParamsUpload) {
|
if (!this.needsTerrainParamsUpload) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export function terrainShaderIntFromId(id: TerrainShaderId): number {
|
|||||||
export function readTerrainShaderId(userSettings: {
|
export function readTerrainShaderId(userSettings: {
|
||||||
getInt: (key: string, defaultValue: number) => number;
|
getInt: (key: string, defaultValue: number) => number;
|
||||||
}): TerrainShaderId {
|
}): TerrainShaderId {
|
||||||
return terrainShaderIdFromInt(userSettings.getInt(TERRAIN_SHADER_KEY, 0));
|
return terrainShaderIdFromInt(userSettings.getInt(TERRAIN_SHADER_KEY, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildTerrainShaderParams(
|
export function buildTerrainShaderParams(
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function readTerritoryPostSmoothingId(userSettings: {
|
|||||||
getInt: (key: string, defaultValue: number) => number;
|
getInt: (key: string, defaultValue: number) => number;
|
||||||
}): TerritoryPostSmoothingId {
|
}): TerritoryPostSmoothingId {
|
||||||
return territoryPostSmoothingIdFromInt(
|
return territoryPostSmoothingIdFromInt(
|
||||||
userSettings.getInt(TERRITORY_POST_SMOOTHING_KEY, 0),
|
userSettings.getInt(TERRITORY_POST_SMOOTHING_KEY, 1),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -189,15 +189,6 @@ export const TERRITORY_SHADERS: TerritoryShaderDefinition[] = [
|
|||||||
max: 1,
|
max: 1,
|
||||||
step: 0.01,
|
step: 0.01,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
kind: "range",
|
|
||||||
key: "settings.webgpu.territory.retro.defendedThreshold",
|
|
||||||
label: "Defended Threshold",
|
|
||||||
defaultValue: 0.01,
|
|
||||||
min: 0.01,
|
|
||||||
max: 1,
|
|
||||||
step: 0.01,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -223,7 +214,7 @@ export function territoryShaderIntFromId(id: TerritoryShaderId): number {
|
|||||||
export function readTerritoryShaderId(userSettings: {
|
export function readTerritoryShaderId(userSettings: {
|
||||||
getInt: (key: string, defaultValue: number) => number;
|
getInt: (key: string, defaultValue: number) => number;
|
||||||
}): TerritoryShaderId {
|
}): TerritoryShaderId {
|
||||||
return territoryShaderIdFromInt(userSettings.getInt(TERRITORY_SHADER_KEY, 0));
|
return territoryShaderIdFromInt(userSettings.getInt(TERRITORY_SHADER_KEY, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildTerritoryShaderParams(
|
export function buildTerritoryShaderParams(
|
||||||
@@ -280,10 +271,7 @@ export function buildTerritoryShaderParams(
|
|||||||
"settings.webgpu.territory.retro.defendedPatternStrength",
|
"settings.webgpu.territory.retro.defendedPatternStrength",
|
||||||
0.5,
|
0.5,
|
||||||
);
|
);
|
||||||
const defendedThreshold = userSettings.getFloat(
|
const defendedThreshold = 0.01;
|
||||||
"settings.webgpu.territory.retro.defendedThreshold",
|
|
||||||
0.01,
|
|
||||||
);
|
|
||||||
|
|
||||||
let flags = 0;
|
let flags = 0;
|
||||||
if (colorByRelations) flags |= 1 << 0;
|
if (colorByRelations) flags |= 1 << 0;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
UnitType,
|
UnitType,
|
||||||
} from "../game/Game";
|
} from "../game/Game";
|
||||||
import { TileRef } from "../game/GameMap";
|
import { TileRef } from "../game/GameMap";
|
||||||
|
import type { MotionPlanRecord } from "../game/MotionPlans";
|
||||||
import { UniversalPathFinding } from "../pathfinding/PathFinder";
|
import { UniversalPathFinding } from "../pathfinding/PathFinder";
|
||||||
import { ParabolaUniversalPathFinder } from "../pathfinding/PathFinder.Parabola";
|
import { ParabolaUniversalPathFinder } from "../pathfinding/PathFinder.Parabola";
|
||||||
import { PathStatus } from "../pathfinding/types";
|
import { PathStatus } from "../pathfinding/types";
|
||||||
@@ -188,6 +189,18 @@ export class NukeExecution implements Execution {
|
|||||||
targetTile: this.dst,
|
targetTile: this.dst,
|
||||||
trajectory: this.getTrajectory(this.dst),
|
trajectory: this.getTrajectory(this.dst),
|
||||||
});
|
});
|
||||||
|
const motionPlan: MotionPlanRecord = {
|
||||||
|
kind: "parabola",
|
||||||
|
unitId: this.nuke.id(),
|
||||||
|
planId: 1,
|
||||||
|
startTick: ticks + 1 + this.waitTicks,
|
||||||
|
src: spawn,
|
||||||
|
dst: this.dst,
|
||||||
|
increment: this.speed,
|
||||||
|
distanceBasedHeight: this.nukeType !== UnitType.MIRVWarhead,
|
||||||
|
directionUp: this.rocketDirectionUp,
|
||||||
|
};
|
||||||
|
this.mg.recordMotionPlan(motionPlan);
|
||||||
if (this.nuke.type() !== UnitType.MIRVWarhead) {
|
if (this.nuke.type() !== UnitType.MIRVWarhead) {
|
||||||
this.maybeBreakAlliances();
|
this.maybeBreakAlliances();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,6 +486,9 @@ export class GameImpl implements Game {
|
|||||||
case "grid_segments":
|
case "grid_segments":
|
||||||
this.planDrivenUnitIds.add(record.unitId);
|
this.planDrivenUnitIds.add(record.unitId);
|
||||||
break;
|
break;
|
||||||
|
case "parabola":
|
||||||
|
this.planDrivenUnitIds.add(record.unitId);
|
||||||
|
break;
|
||||||
case "train":
|
case "train":
|
||||||
this.planDrivenUnitIds.add(record.engineUnitId);
|
this.planDrivenUnitIds.add(record.engineUnitId);
|
||||||
for (const unitId of record.carUnitIds) {
|
for (const unitId of record.carUnitIds) {
|
||||||
|
|||||||
+73
-23
@@ -2,6 +2,8 @@ import { Colord, colord } from "colord";
|
|||||||
import { base64url } from "jose";
|
import { base64url } from "jose";
|
||||||
import { Config } from "../configuration/Config";
|
import { Config } from "../configuration/Config";
|
||||||
import { ColorPalette } from "../CosmeticSchemas";
|
import { ColorPalette } from "../CosmeticSchemas";
|
||||||
|
import { UniversalPathFinding } from "../pathfinding/PathFinder";
|
||||||
|
import { PathStatus } from "../pathfinding/types";
|
||||||
import { PatternDecoder } from "../PatternDecoder";
|
import { PatternDecoder } from "../PatternDecoder";
|
||||||
import { ClientID, GameID, Player, PlayerCosmetics } from "../Schemas";
|
import { ClientID, GameID, Player, PlayerCosmetics } from "../Schemas";
|
||||||
import { createRandomName, formatPlayerDisplayName } from "../Util";
|
import { createRandomName, formatPlayerDisplayName } from "../Util";
|
||||||
@@ -964,7 +966,7 @@ export class GameView implements GameMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
while (seg + 1 < segmentCount && idx >= (segCumSteps[seg + 1] >>> 0)) {
|
while (seg + 1 < segmentCount && idx >= segCumSteps[seg + 1] >>> 0) {
|
||||||
seg++;
|
seg++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1124,15 +1126,62 @@ export class GameView implements GameMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setGridSegmentMotionPlan(record: {
|
||||||
|
unitId: number;
|
||||||
|
planId: number;
|
||||||
|
startTick: number;
|
||||||
|
ticksPerStep: number;
|
||||||
|
points: readonly TileRef[] | Uint32Array;
|
||||||
|
segmentSteps: readonly number[] | Uint32Array;
|
||||||
|
}): boolean {
|
||||||
|
if (
|
||||||
|
record.ticksPerStep < 1 ||
|
||||||
|
record.points.length < 1 ||
|
||||||
|
record.segmentSteps.length !== Math.max(0, record.points.length - 1)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const existing = this.unitMotionPlans.get(record.unitId);
|
||||||
|
if (existing && record.planId <= existing.planId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const points =
|
||||||
|
record.points instanceof Uint32Array
|
||||||
|
? record.points
|
||||||
|
: Uint32Array.from(record.points);
|
||||||
|
const segmentSteps =
|
||||||
|
record.segmentSteps instanceof Uint32Array
|
||||||
|
? record.segmentSteps
|
||||||
|
: Uint32Array.from(record.segmentSteps);
|
||||||
|
|
||||||
|
const segCumSteps = new Uint32Array(segmentSteps.length + 1);
|
||||||
|
for (let i = 0; i < segmentSteps.length; i++) {
|
||||||
|
segCumSteps[i + 1] = (segCumSteps[i] + (segmentSteps[i] >>> 0)) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.unitMotionPlans.set(record.unitId, {
|
||||||
|
planId: record.planId,
|
||||||
|
startTick: record.startTick,
|
||||||
|
ticksPerStep: record.ticksPerStep,
|
||||||
|
points,
|
||||||
|
segmentSteps,
|
||||||
|
segCumSteps,
|
||||||
|
lastSegIdx: 0,
|
||||||
|
});
|
||||||
|
this.markMotionPlannedUnitIdsDirty();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private applyMotionPlanRecords(records: readonly MotionPlanRecord[]): void {
|
private applyMotionPlanRecords(records: readonly MotionPlanRecord[]): void {
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
switch (record.kind) {
|
switch (record.kind) {
|
||||||
case "grid_segments": {
|
case "grid_segments": {
|
||||||
if (
|
this.setGridSegmentMotionPlan(record);
|
||||||
record.ticksPerStep < 1 ||
|
break;
|
||||||
record.points.length < 1 ||
|
}
|
||||||
record.segmentSteps.length !== Math.max(0, record.points.length - 1)
|
case "parabola": {
|
||||||
) {
|
if (record.increment < 1) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const existing = this.unitMotionPlans.get(record.unitId);
|
const existing = this.unitMotionPlans.get(record.unitId);
|
||||||
@@ -1140,31 +1189,32 @@ export class GameView implements GameMap {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const points =
|
const pf = UniversalPathFinding.Parabola(this._map, {
|
||||||
record.points instanceof Uint32Array
|
increment: record.increment,
|
||||||
? record.points
|
distanceBasedHeight: record.distanceBasedHeight,
|
||||||
: Uint32Array.from(record.points);
|
directionUp: record.directionUp,
|
||||||
const segmentSteps =
|
});
|
||||||
record.segmentSteps instanceof Uint32Array
|
|
||||||
? record.segmentSteps
|
|
||||||
: Uint32Array.from(record.segmentSteps);
|
|
||||||
|
|
||||||
const segCumSteps = new Uint32Array(segmentSteps.length + 1);
|
const points: TileRef[] = [record.src];
|
||||||
for (let i = 0; i < segmentSteps.length; i++) {
|
for (let i = 0; i < 20000; i++) {
|
||||||
segCumSteps[i + 1] =
|
const step = pf.next(record.src, record.dst, record.increment);
|
||||||
(segCumSteps[i] + (segmentSteps[i] >>> 0)) >>> 0;
|
if (step.status === PathStatus.NEXT) {
|
||||||
|
points.push(step.node);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.unitMotionPlans.set(record.unitId, {
|
const segmentSteps = new Uint32Array(Math.max(0, points.length - 1));
|
||||||
|
segmentSteps.fill(1);
|
||||||
|
this.setGridSegmentMotionPlan({
|
||||||
|
unitId: record.unitId,
|
||||||
planId: record.planId,
|
planId: record.planId,
|
||||||
startTick: record.startTick,
|
startTick: record.startTick,
|
||||||
ticksPerStep: record.ticksPerStep,
|
ticksPerStep: 1,
|
||||||
points,
|
points,
|
||||||
segmentSteps,
|
segmentSteps,
|
||||||
segCumSteps,
|
|
||||||
lastSegIdx: 0,
|
|
||||||
});
|
});
|
||||||
this.markMotionPlannedUnitIdsDirty();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "train": {
|
case "train": {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { TileRef } from "./GameMap";
|
|||||||
export enum PackedMotionPlanKind {
|
export enum PackedMotionPlanKind {
|
||||||
TrainRailPathSet = 2,
|
TrainRailPathSet = 2,
|
||||||
GridPathKeypointSegments = 3,
|
GridPathKeypointSegments = 3,
|
||||||
|
ParabolaSet = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GridKeypointSegmentPlan {
|
export interface GridKeypointSegmentPlan {
|
||||||
@@ -32,7 +33,22 @@ export interface TrainRailPathPlan {
|
|||||||
path: readonly TileRef[] | Uint32Array;
|
path: readonly TileRef[] | Uint32Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MotionPlanRecord = GridKeypointSegmentPlan | TrainRailPathPlan;
|
export interface ParabolaPlan {
|
||||||
|
kind: "parabola";
|
||||||
|
unitId: number;
|
||||||
|
planId: number;
|
||||||
|
startTick: number;
|
||||||
|
src: TileRef;
|
||||||
|
dst: TileRef;
|
||||||
|
increment: number;
|
||||||
|
distanceBasedHeight: boolean;
|
||||||
|
directionUp: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MotionPlanRecord =
|
||||||
|
| GridKeypointSegmentPlan
|
||||||
|
| TrainRailPathPlan
|
||||||
|
| ParabolaPlan;
|
||||||
|
|
||||||
export function packMotionPlans(
|
export function packMotionPlans(
|
||||||
records: readonly MotionPlanRecord[],
|
records: readonly MotionPlanRecord[],
|
||||||
@@ -51,6 +67,10 @@ export function packMotionPlans(
|
|||||||
totalWords += 2 + 7 + carCount + pathLen;
|
totalWords += 2 + 7 + carCount + pathLen;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "parabola": {
|
||||||
|
totalWords += 2 + 7;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +135,22 @@ export function packMotionPlans(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "parabola": {
|
||||||
|
const flags =
|
||||||
|
(record.distanceBasedHeight ? 1 : 0) | (record.directionUp ? 2 : 0);
|
||||||
|
const wordCount = 2 + 7;
|
||||||
|
|
||||||
|
out[offset++] = PackedMotionPlanKind.ParabolaSet;
|
||||||
|
out[offset++] = wordCount >>> 0;
|
||||||
|
out[offset++] = record.unitId >>> 0;
|
||||||
|
out[offset++] = record.planId >>> 0;
|
||||||
|
out[offset++] = record.startTick >>> 0;
|
||||||
|
out[offset++] = record.src >>> 0;
|
||||||
|
out[offset++] = record.dst >>> 0;
|
||||||
|
out[offset++] = record.increment >>> 0;
|
||||||
|
out[offset++] = flags >>> 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +255,34 @@ export function unpackMotionPlans(packed: Uint32Array): MotionPlanRecord[] {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case PackedMotionPlanKind.ParabolaSet: {
|
||||||
|
if (wordCount !== 2 + 7) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const unitId = packed[offset + 2] >>> 0;
|
||||||
|
const planId = packed[offset + 3] >>> 0;
|
||||||
|
const startTick = packed[offset + 4] >>> 0;
|
||||||
|
const src = packed[offset + 5] as TileRef;
|
||||||
|
const dst = packed[offset + 6] as TileRef;
|
||||||
|
const increment = packed[offset + 7] >>> 0;
|
||||||
|
const flags = packed[offset + 8] >>> 0;
|
||||||
|
if (increment < 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
records.push({
|
||||||
|
kind: "parabola",
|
||||||
|
unitId,
|
||||||
|
planId,
|
||||||
|
startTick,
|
||||||
|
src,
|
||||||
|
dst,
|
||||||
|
increment,
|
||||||
|
distanceBasedHeight: (flags & 1) !== 0,
|
||||||
|
directionUp: (flags & 2) !== 0,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
// Unknown kind: skip.
|
// Unknown kind: skip.
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -31,6 +31,36 @@ describe("MotionPlans grid_segments", () => {
|
|||||||
expect(Array.from(r.segmentSteps)).toEqual([5, 5]);
|
expect(Array.from(r.segmentSteps)).toEqual([5, 5]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("packs/unpacks parabola records", () => {
|
||||||
|
const packed = packMotionPlans([
|
||||||
|
{
|
||||||
|
kind: "parabola",
|
||||||
|
unitId: 44,
|
||||||
|
planId: 3,
|
||||||
|
startTick: 99,
|
||||||
|
src: 10,
|
||||||
|
dst: 20,
|
||||||
|
increment: 7,
|
||||||
|
distanceBasedHeight: true,
|
||||||
|
directionUp: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const records = unpackMotionPlans(packed);
|
||||||
|
expect(records).toHaveLength(1);
|
||||||
|
const r = records[0];
|
||||||
|
expect(r.kind).toBe("parabola");
|
||||||
|
if (r.kind !== "parabola") throw new Error("type guard");
|
||||||
|
expect(r.unitId).toBe(44);
|
||||||
|
expect(r.planId).toBe(3);
|
||||||
|
expect(r.startTick).toBe(99);
|
||||||
|
expect(r.src).toBe(10);
|
||||||
|
expect(r.dst).toBe(20);
|
||||||
|
expect(r.increment).toBe(7);
|
||||||
|
expect(r.distanceBasedHeight).toBe(true);
|
||||||
|
expect(r.directionUp).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("skips unknown kinds using wordCount", () => {
|
it("skips unknown kinds using wordCount", () => {
|
||||||
const gridPacked = packMotionPlans([
|
const gridPacked = packMotionPlans([
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user