mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 18:56:45 +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);
|
||||
}
|
||||
|
||||
.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 {
|
||||
cursor: grabbing;
|
||||
}
|
||||
@@ -204,6 +229,32 @@ export class RendererStatusPanel extends LitElement implements Layer {
|
||||
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) {
|
||||
if (id === "webgpu") return "WebGPU";
|
||||
if (id === "webgl") return "WebGL";
|
||||
@@ -341,6 +392,7 @@ export class RendererStatusPanel extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
const note = this.statusNote();
|
||||
const canOpenSettings = this.rendererSettingsTarget() !== null;
|
||||
return html`
|
||||
<div
|
||||
class="panel ${this.isDragging ? "dragging" : ""}"
|
||||
@@ -349,6 +401,16 @@ export class RendererStatusPanel extends LitElement implements Layer {
|
||||
>
|
||||
<div class="title" @pointerdown=${this.handleDragPointerDown}>
|
||||
<span>Renderer</span>
|
||||
<div class="titleActions">
|
||||
<button
|
||||
type="button"
|
||||
?disabled=${!canOpenSettings}
|
||||
@pointerdown=${this.stopPointerEvent}
|
||||
@click=${this.openRendererSettings}
|
||||
>
|
||||
settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="row">
|
||||
|
||||
@@ -60,28 +60,22 @@ const TRANSPORT_SHIP_MASK = [
|
||||
".BTB.",
|
||||
"..B..",
|
||||
] as const;
|
||||
const TRADE_SHIP_MASK = [
|
||||
"..T..",
|
||||
".TBT.",
|
||||
"TBBBT",
|
||||
".TBT.",
|
||||
"..T..",
|
||||
] as const;
|
||||
const TRADE_SHIP_MASK = ["..T..", ".TBT.", "TBBBT", ".TBT.", "..T.."] as const;
|
||||
|
||||
type TransportTrailState = {
|
||||
type MotionTrailState = {
|
||||
activePlanId: number;
|
||||
epochs: TransportTrailEpoch[];
|
||||
epochs: MotionTrailEpoch[];
|
||||
lastOnScreen: boolean;
|
||||
};
|
||||
|
||||
type TransportTrailEpoch = SegmentTrailPlanView & {
|
||||
type MotionTrailEpoch = SegmentTrailPlanView & {
|
||||
planId: number;
|
||||
targetStep: number;
|
||||
drawnStep: number;
|
||||
sealed: boolean;
|
||||
};
|
||||
|
||||
type ActiveTransportTrailPlan = {
|
||||
type ActiveMotionTrailPlan = {
|
||||
unitId: number;
|
||||
unit: UnitView;
|
||||
plan: SegmentTrailPlanView & { planId: number };
|
||||
@@ -134,7 +128,7 @@ export class UnitLayer implements Layer {
|
||||
|
||||
private gridMoverUnitIds = new Set<number>();
|
||||
|
||||
private transportShipTrails = new Map<number, TransportTrailState>();
|
||||
private segmentTrails = new Map<number, MotionTrailState>();
|
||||
private trailDirty = false;
|
||||
|
||||
private moverState = new Map<number, MoverRenderState>();
|
||||
@@ -201,7 +195,7 @@ export class UnitLayer implements Layer {
|
||||
tick() {
|
||||
const trailPrune = pruneInactiveTrails(
|
||||
this.unitToTrail,
|
||||
this.transportShipTrails,
|
||||
this.segmentTrails,
|
||||
(unitId) => {
|
||||
const current = this.game.unit(unitId);
|
||||
return !!current && current.isActive();
|
||||
@@ -482,13 +476,13 @@ export class UnitLayer implements Layer {
|
||||
const tickFloat = this.game.ticks() + tickAlpha;
|
||||
const viewBounds = this.currentViewBounds();
|
||||
const activeMoverIds = new Set<number>();
|
||||
const activeTransportTrailPlans: ActiveTransportTrailPlan[] = [];
|
||||
const activeMotionTrailPlans: ActiveMotionTrailPlan[] = [];
|
||||
|
||||
for (const [unitId, plan] of this.game.motionPlans()) {
|
||||
const unit = this.game.unit(unitId);
|
||||
if (!unit || !unit.isActive()) {
|
||||
this.clearMoverState(unitId);
|
||||
if (this.transportShipTrails.delete(unitId)) this.trailDirty = true;
|
||||
if (this.segmentTrails.delete(unitId)) this.trailDirty = true;
|
||||
continue;
|
||||
}
|
||||
activeMoverIds.add(unitId);
|
||||
@@ -500,8 +494,8 @@ export class UnitLayer implements Layer {
|
||||
tickFloat,
|
||||
viewBounds,
|
||||
);
|
||||
if (unit.type() === UnitType.TransportShip) {
|
||||
activeTransportTrailPlans.push({
|
||||
if (this.shouldDrawSegmentTrail(unit)) {
|
||||
activeMotionTrailPlans.push({
|
||||
unitId,
|
||||
unit,
|
||||
plan,
|
||||
@@ -530,10 +524,7 @@ export class UnitLayer implements Layer {
|
||||
viewBounds,
|
||||
);
|
||||
|
||||
this.advanceAndDrawTransportTrails(
|
||||
this.game.ticks(),
|
||||
activeTransportTrailPlans,
|
||||
);
|
||||
this.advanceAndDrawSegmentTrails(this.game.ticks(), activeMotionTrailPlans);
|
||||
this.rebuildTrailCanvasIfDirty();
|
||||
|
||||
context.drawImage(
|
||||
@@ -960,7 +951,9 @@ export class UnitLayer implements Layer {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidateRects: MoverSpriteRect[] = [candidateState.lastSpriteRect];
|
||||
const candidateRects: MoverSpriteRect[] = [
|
||||
candidateState.lastSpriteRect,
|
||||
];
|
||||
const candidateSample = this.getConflictSample(
|
||||
candidateId,
|
||||
tickFloat,
|
||||
@@ -998,7 +991,13 @@ export class UnitLayer implements Layer {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.getMoverSample(unitId, unit, plan.planId, tickFloat, sampledCache);
|
||||
return this.getMoverSample(
|
||||
unitId,
|
||||
unit,
|
||||
plan.planId,
|
||||
tickFloat,
|
||||
sampledCache,
|
||||
);
|
||||
}
|
||||
|
||||
private anyRectsOverlap(
|
||||
@@ -1227,10 +1226,7 @@ export class UnitLayer implements Layer {
|
||||
|
||||
private rectsOverlap(a: MoverSpriteRect, b: MoverSpriteRect): boolean {
|
||||
return (
|
||||
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 < b.x + b.w && 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 (
|
||||
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--;
|
||||
}
|
||||
@@ -1344,7 +1341,8 @@ export class UnitLayer implements Layer {
|
||||
|
||||
this.lastDynamicMoverCanvasRescaleMs =
|
||||
this.rebuildDynamicMoverCanvas(targetScale);
|
||||
this.totalDynamicMoverCanvasRescaleMs += this.lastDynamicMoverCanvasRescaleMs;
|
||||
this.totalDynamicMoverCanvasRescaleMs +=
|
||||
this.lastDynamicMoverCanvasRescaleMs;
|
||||
this.dynamicMoverCanvasRescaleCount++;
|
||||
this.dynamicMoverCanvasScale = targetScale;
|
||||
this.lastDynamicMoverCanvasScaleChangeAtMs = nowMs;
|
||||
@@ -1371,8 +1369,14 @@ export class UnitLayer implements Layer {
|
||||
const oldHeight = oldCanvas.height;
|
||||
|
||||
this.dynamicMoverCanvas = document.createElement("canvas");
|
||||
this.dynamicMoverCanvas.width = Math.max(1, this.game.width() * targetScale);
|
||||
this.dynamicMoverCanvas.height = Math.max(1, this.game.height() * targetScale);
|
||||
this.dynamicMoverCanvas.width = Math.max(
|
||||
1,
|
||||
this.game.width() * targetScale,
|
||||
);
|
||||
this.dynamicMoverCanvas.height = Math.max(
|
||||
1,
|
||||
this.game.height() * targetScale,
|
||||
);
|
||||
const dynamicMoverContext = this.dynamicMoverCanvas.getContext("2d");
|
||||
if (dynamicMoverContext === null) {
|
||||
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);
|
||||
}
|
||||
|
||||
private advanceAndDrawTransportTrails(
|
||||
private advanceAndDrawSegmentTrails(
|
||||
currentTick: number,
|
||||
activePlans: readonly ActiveTransportTrailPlan[],
|
||||
activePlans: readonly ActiveMotionTrailPlan[],
|
||||
): void {
|
||||
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 onScreen = moverState ? moverState.bucket === "on" : maybeOnScreen;
|
||||
|
||||
if (onScreen) {
|
||||
this.drawPendingTransportTrailEpochs(unit, state);
|
||||
this.drawPendingSegmentTrailEpochs(unit, state);
|
||||
}
|
||||
state.lastOnScreen = onScreen;
|
||||
}
|
||||
}
|
||||
|
||||
private ensureTransportTrailState(
|
||||
private ensureSegmentTrailState(
|
||||
unitId: number,
|
||||
plan: SegmentTrailPlanView & { planId: number },
|
||||
currentTick: number,
|
||||
): TransportTrailState {
|
||||
let state = this.transportShipTrails.get(unitId);
|
||||
): MotionTrailState {
|
||||
let state = this.segmentTrails.get(unitId);
|
||||
if (!state) {
|
||||
state = {
|
||||
activePlanId: plan.planId,
|
||||
epochs: [],
|
||||
lastOnScreen: false,
|
||||
};
|
||||
this.transportShipTrails.set(unitId, state);
|
||||
this.segmentTrails.set(unitId, state);
|
||||
}
|
||||
|
||||
let activeEpoch = state.epochs[state.epochs.length - 1];
|
||||
@@ -1726,7 +1730,7 @@ export class UnitLayer implements Layer {
|
||||
activeEpoch.sealed = true;
|
||||
}
|
||||
|
||||
activeEpoch = this.createTransportTrailEpoch(plan, currentTick);
|
||||
activeEpoch = this.createSegmentTrailEpoch(plan, currentTick);
|
||||
state.epochs.push(activeEpoch);
|
||||
state.activePlanId = plan.planId;
|
||||
return state;
|
||||
@@ -1741,10 +1745,10 @@ export class UnitLayer implements Layer {
|
||||
return state;
|
||||
}
|
||||
|
||||
private createTransportTrailEpoch(
|
||||
private createSegmentTrailEpoch(
|
||||
plan: SegmentTrailPlanView & { planId: number },
|
||||
currentTick: number,
|
||||
): TransportTrailEpoch {
|
||||
): MotionTrailEpoch {
|
||||
return {
|
||||
planId: plan.planId,
|
||||
startTick: plan.startTick,
|
||||
@@ -1758,9 +1762,9 @@ export class UnitLayer implements Layer {
|
||||
};
|
||||
}
|
||||
|
||||
private drawPendingTransportTrailEpochs(
|
||||
private drawPendingSegmentTrailEpochs(
|
||||
unit: UnitView,
|
||||
state: TransportTrailState,
|
||||
state: MotionTrailState,
|
||||
): void {
|
||||
const ctx = this.trailContext;
|
||||
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);
|
||||
if (!unit || !unit.isActive()) {
|
||||
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 {
|
||||
let rel = this.relationship(unit);
|
||||
const dstPortId = unit.targetUnitId();
|
||||
|
||||
@@ -467,10 +467,41 @@ export class WebGLTerritoryBackend implements TerritoryBackend {
|
||||
root.style.touchAction = "none";
|
||||
|
||||
const title = document.createElement("div");
|
||||
title.textContent = "Territory smoothing";
|
||||
title.style.fontWeight = "700";
|
||||
title.style.marginBottom = "6px";
|
||||
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);
|
||||
|
||||
// Restore last position (if any)
|
||||
|
||||
@@ -106,6 +106,22 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -228,7 +244,7 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
private selectedShaderId() {
|
||||
const selected = this.userSettings.getInt(TERRITORY_SHADER_KEY, 0);
|
||||
const selected = this.userSettings.getInt(TERRITORY_SHADER_KEY, 1);
|
||||
return territoryShaderIdFromInt(selected);
|
||||
}
|
||||
|
||||
@@ -241,7 +257,7 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
private selectedTerrainShaderId() {
|
||||
const selected = this.userSettings.getInt(TERRAIN_SHADER_KEY, 0);
|
||||
const selected = this.userSettings.getInt(TERRAIN_SHADER_KEY, 2);
|
||||
return terrainShaderIdFromInt(selected);
|
||||
}
|
||||
|
||||
@@ -266,7 +282,7 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -403,6 +419,12 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
private closeOverlay(event: Event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.userSettings.setWebgpuDebug(false);
|
||||
}
|
||||
|
||||
private handleDragPointerDown(event: PointerEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -486,6 +508,15 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
>
|
||||
<div class="title" @pointerdown=${this.handleDragPointerDown}>
|
||||
<div>WebGPU Debug</div>
|
||||
<button
|
||||
class="closeButton"
|
||||
type="button"
|
||||
title="Close"
|
||||
@pointerdown=${this.stopPointerEvent}
|
||||
@click=${this.closeOverlay}
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="metrics">
|
||||
|
||||
@@ -121,6 +121,11 @@ export class WebGPUTerritoryBackend implements TerritoryBackend {
|
||||
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,
|
||||
// invoke the renderer's tick() to process compute passes. This ensures
|
||||
// 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).
|
||||
* This ensures terrain is recomputed before the next render.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Theme } from "../../../../core/configuration/Config";
|
||||
import { UnitType } from "../../../../core/game/Game";
|
||||
import type { TileRef } from "../../../../core/game/GameMap";
|
||||
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 {
|
||||
if (!this.needsTerrainParamsUpload) {
|
||||
return;
|
||||
|
||||
@@ -180,7 +180,7 @@ export function terrainShaderIntFromId(id: TerrainShaderId): number {
|
||||
export function readTerrainShaderId(userSettings: {
|
||||
getInt: (key: string, defaultValue: number) => number;
|
||||
}): TerrainShaderId {
|
||||
return terrainShaderIdFromInt(userSettings.getInt(TERRAIN_SHADER_KEY, 0));
|
||||
return terrainShaderIdFromInt(userSettings.getInt(TERRAIN_SHADER_KEY, 2));
|
||||
}
|
||||
|
||||
export function buildTerrainShaderParams(
|
||||
|
||||
@@ -82,7 +82,7 @@ export function readTerritoryPostSmoothingId(userSettings: {
|
||||
getInt: (key: string, defaultValue: number) => number;
|
||||
}): TerritoryPostSmoothingId {
|
||||
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,
|
||||
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: {
|
||||
getInt: (key: string, defaultValue: number) => number;
|
||||
}): TerritoryShaderId {
|
||||
return territoryShaderIdFromInt(userSettings.getInt(TERRITORY_SHADER_KEY, 0));
|
||||
return territoryShaderIdFromInt(userSettings.getInt(TERRITORY_SHADER_KEY, 1));
|
||||
}
|
||||
|
||||
export function buildTerritoryShaderParams(
|
||||
@@ -280,10 +271,7 @@ export function buildTerritoryShaderParams(
|
||||
"settings.webgpu.territory.retro.defendedPatternStrength",
|
||||
0.5,
|
||||
);
|
||||
const defendedThreshold = userSettings.getFloat(
|
||||
"settings.webgpu.territory.retro.defendedThreshold",
|
||||
0.01,
|
||||
);
|
||||
const defendedThreshold = 0.01;
|
||||
|
||||
let flags = 0;
|
||||
if (colorByRelations) flags |= 1 << 0;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import type { MotionPlanRecord } from "../game/MotionPlans";
|
||||
import { UniversalPathFinding } from "../pathfinding/PathFinder";
|
||||
import { ParabolaUniversalPathFinder } from "../pathfinding/PathFinder.Parabola";
|
||||
import { PathStatus } from "../pathfinding/types";
|
||||
@@ -188,6 +189,18 @@ export class NukeExecution implements Execution {
|
||||
targetTile: 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) {
|
||||
this.maybeBreakAlliances();
|
||||
}
|
||||
|
||||
@@ -486,6 +486,9 @@ export class GameImpl implements Game {
|
||||
case "grid_segments":
|
||||
this.planDrivenUnitIds.add(record.unitId);
|
||||
break;
|
||||
case "parabola":
|
||||
this.planDrivenUnitIds.add(record.unitId);
|
||||
break;
|
||||
case "train":
|
||||
this.planDrivenUnitIds.add(record.engineUnitId);
|
||||
for (const unitId of record.carUnitIds) {
|
||||
|
||||
+73
-23
@@ -2,6 +2,8 @@ import { Colord, colord } from "colord";
|
||||
import { base64url } from "jose";
|
||||
import { Config } from "../configuration/Config";
|
||||
import { ColorPalette } from "../CosmeticSchemas";
|
||||
import { UniversalPathFinding } from "../pathfinding/PathFinder";
|
||||
import { PathStatus } from "../pathfinding/types";
|
||||
import { PatternDecoder } from "../PatternDecoder";
|
||||
import { ClientID, GameID, Player, PlayerCosmetics } from "../Schemas";
|
||||
import { createRandomName, formatPlayerDisplayName } from "../Util";
|
||||
@@ -964,7 +966,7 @@ export class GameView implements GameMap {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (seg + 1 < segmentCount && idx >= (segCumSteps[seg + 1] >>> 0)) {
|
||||
while (seg + 1 < segmentCount && idx >= segCumSteps[seg + 1] >>> 0) {
|
||||
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 {
|
||||
for (const record of records) {
|
||||
switch (record.kind) {
|
||||
case "grid_segments": {
|
||||
if (
|
||||
record.ticksPerStep < 1 ||
|
||||
record.points.length < 1 ||
|
||||
record.segmentSteps.length !== Math.max(0, record.points.length - 1)
|
||||
) {
|
||||
this.setGridSegmentMotionPlan(record);
|
||||
break;
|
||||
}
|
||||
case "parabola": {
|
||||
if (record.increment < 1) {
|
||||
break;
|
||||
}
|
||||
const existing = this.unitMotionPlans.get(record.unitId);
|
||||
@@ -1140,31 +1189,32 @@ export class GameView implements GameMap {
|
||||
break;
|
||||
}
|
||||
|
||||
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 pf = UniversalPathFinding.Parabola(this._map, {
|
||||
increment: record.increment,
|
||||
distanceBasedHeight: record.distanceBasedHeight,
|
||||
directionUp: record.directionUp,
|
||||
});
|
||||
|
||||
const segCumSteps = new Uint32Array(segmentSteps.length + 1);
|
||||
for (let i = 0; i < segmentSteps.length; i++) {
|
||||
segCumSteps[i + 1] =
|
||||
(segCumSteps[i] + (segmentSteps[i] >>> 0)) >>> 0;
|
||||
const points: TileRef[] = [record.src];
|
||||
for (let i = 0; i < 20000; i++) {
|
||||
const step = pf.next(record.src, record.dst, record.increment);
|
||||
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,
|
||||
startTick: record.startTick,
|
||||
ticksPerStep: record.ticksPerStep,
|
||||
ticksPerStep: 1,
|
||||
points,
|
||||
segmentSteps,
|
||||
segCumSteps,
|
||||
lastSegIdx: 0,
|
||||
});
|
||||
this.markMotionPlannedUnitIdsDirty();
|
||||
break;
|
||||
}
|
||||
case "train": {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TileRef } from "./GameMap";
|
||||
export enum PackedMotionPlanKind {
|
||||
TrainRailPathSet = 2,
|
||||
GridPathKeypointSegments = 3,
|
||||
ParabolaSet = 4,
|
||||
}
|
||||
|
||||
export interface GridKeypointSegmentPlan {
|
||||
@@ -32,7 +33,22 @@ export interface TrainRailPathPlan {
|
||||
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(
|
||||
records: readonly MotionPlanRecord[],
|
||||
@@ -51,6 +67,10 @@ export function packMotionPlans(
|
||||
totalWords += 2 + 7 + carCount + pathLen;
|
||||
break;
|
||||
}
|
||||
case "parabola": {
|
||||
totalWords += 2 + 7;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +135,22 @@ export function packMotionPlans(
|
||||
}
|
||||
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;
|
||||
}
|
||||
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:
|
||||
// Unknown kind: skip.
|
||||
break;
|
||||
|
||||
@@ -31,6 +31,36 @@ describe("MotionPlans grid_segments", () => {
|
||||
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", () => {
|
||||
const gridPacked = packMotionPlans([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user