mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 16:04:36 +00:00
initial test
This commit is contained in:
@@ -54,6 +54,7 @@ export function createRenderer(
|
||||
attackRatio: 20,
|
||||
ghostStructure: null,
|
||||
rocketDirectionUp: true,
|
||||
lockedGhostTile: null,
|
||||
} as UIState;
|
||||
|
||||
//hide when the game renders
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { UnitType } from "../../core/game/Game";
|
||||
import { TileRef } from "../../core/game/GameMap";
|
||||
|
||||
export interface UIState {
|
||||
attackRatio: number;
|
||||
ghostStructure: UnitType | null;
|
||||
rocketDirectionUp: boolean;
|
||||
lockedGhostTile: TileRef | null;
|
||||
}
|
||||
|
||||
@@ -104,30 +104,44 @@ export class NukeTrajectoryPreviewLayer implements Layer {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert mouse position to world coordinates
|
||||
const rect = this.transformHandler.boundingRect();
|
||||
if (!rect) {
|
||||
this.trajectoryPoints = [];
|
||||
this.cachedSpawnTile = null;
|
||||
return;
|
||||
let targetTile: TileRef | null = null;
|
||||
|
||||
// If ghost is locked, use the locked tile; otherwise use mouse position
|
||||
if (this.uiState.lockedGhostTile) {
|
||||
targetTile = this.uiState.lockedGhostTile;
|
||||
} else {
|
||||
// Convert mouse position to world coordinates
|
||||
const rect = this.transformHandler.boundingRect();
|
||||
if (!rect) {
|
||||
this.trajectoryPoints = [];
|
||||
this.cachedSpawnTile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const localX = this.mousePos.x - rect.left;
|
||||
const localY = this.mousePos.y - rect.top;
|
||||
const worldCoords = this.transformHandler.screenToWorldCoordinates(
|
||||
localX,
|
||||
localY,
|
||||
);
|
||||
|
||||
if (!this.game.isValidCoord(worldCoords.x, worldCoords.y)) {
|
||||
this.trajectoryPoints = [];
|
||||
this.lastTargetTile = null;
|
||||
this.cachedSpawnTile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
targetTile = this.game.ref(worldCoords.x, worldCoords.y);
|
||||
}
|
||||
|
||||
const localX = this.mousePos.x - rect.left;
|
||||
const localY = this.mousePos.y - rect.top;
|
||||
const worldCoords = this.transformHandler.screenToWorldCoordinates(
|
||||
localX,
|
||||
localY,
|
||||
);
|
||||
|
||||
if (!this.game.isValidCoord(worldCoords.x, worldCoords.y)) {
|
||||
if (!targetTile) {
|
||||
this.trajectoryPoints = [];
|
||||
this.lastTargetTile = null;
|
||||
this.cachedSpawnTile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const targetTile = this.game.ref(worldCoords.x, worldCoords.y);
|
||||
|
||||
// Only recalculate if target tile changed
|
||||
if (this.lastTargetTile === targetTile) {
|
||||
return;
|
||||
@@ -190,27 +204,39 @@ export class NukeTrajectoryPreviewLayer implements Layer {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert mouse position to world coordinates
|
||||
const rect = this.transformHandler.boundingRect();
|
||||
if (!rect) {
|
||||
let targetTile: TileRef | null = null;
|
||||
|
||||
// If ghost is locked, use the locked tile; otherwise use mouse position
|
||||
if (this.uiState.lockedGhostTile) {
|
||||
targetTile = this.uiState.lockedGhostTile;
|
||||
} else {
|
||||
// Convert mouse position to world coordinates
|
||||
const rect = this.transformHandler.boundingRect();
|
||||
if (!rect) {
|
||||
this.trajectoryPoints = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const localX = this.mousePos.x - rect.left;
|
||||
const localY = this.mousePos.y - rect.top;
|
||||
const worldCoords = this.transformHandler.screenToWorldCoordinates(
|
||||
localX,
|
||||
localY,
|
||||
);
|
||||
|
||||
if (!this.game.isValidCoord(worldCoords.x, worldCoords.y)) {
|
||||
this.trajectoryPoints = [];
|
||||
return;
|
||||
}
|
||||
|
||||
targetTile = this.game.ref(worldCoords.x, worldCoords.y);
|
||||
}
|
||||
|
||||
if (!targetTile) {
|
||||
this.trajectoryPoints = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const localX = this.mousePos.x - rect.left;
|
||||
const localY = this.mousePos.y - rect.top;
|
||||
const worldCoords = this.transformHandler.screenToWorldCoordinates(
|
||||
localX,
|
||||
localY,
|
||||
);
|
||||
|
||||
if (!this.game.isValidCoord(worldCoords.x, worldCoords.y)) {
|
||||
this.trajectoryPoints = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const targetTile = this.game.ref(worldCoords.x, worldCoords.y);
|
||||
|
||||
// Calculate trajectory using ParabolaPathFinder with cached spawn tile
|
||||
const pathFinder = new ParabolaPathFinder(this.game);
|
||||
const speed = this.game.config().defaultNukeSpeed();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AllPlayers, PlayerActions, UnitType } from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { Emoji, flattenedEmojiTable } from "../../../core/Util";
|
||||
import { GhostStructureChangedEvent } from "../../InputHandler";
|
||||
import { renderNumber, translateText } from "../../Utils";
|
||||
import { UIState } from "../UIState";
|
||||
import { BuildItemDisplay, BuildMenu, flattenedBuildTable } from "./BuildMenu";
|
||||
@@ -412,7 +413,23 @@ function createMenuElements(
|
||||
if (buildableUnit === undefined) {
|
||||
return;
|
||||
}
|
||||
if (params.buildMenu.canBuildOrUpgrade(item)) {
|
||||
const isNukeSelection =
|
||||
filterType === "attack" &&
|
||||
(item.unitType === UnitType.AtomBomb ||
|
||||
item.unitType === UnitType.HydrogenBomb);
|
||||
|
||||
const canBuild = params.buildMenu.canBuildOrUpgrade(item);
|
||||
|
||||
if (isNukeSelection && canBuild && params.uiState) {
|
||||
// Reuse ghost placement flow so players see the full nuke trajectory/interception preview
|
||||
params.uiState.ghostStructure = item.unitType;
|
||||
params.uiState.lockedGhostTile = params.tile;
|
||||
params.eventBus.emit(new GhostStructureChangedEvent(item.unitType));
|
||||
params.closeMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
if (canBuild) {
|
||||
params.buildMenu.sendBuildOrUpgrade(buildableUnit, params.tile);
|
||||
}
|
||||
params.closeMenu();
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
GhostStructureChangedEvent,
|
||||
MouseMoveEvent,
|
||||
MouseUpEvent,
|
||||
SwapRocketDirectionEvent,
|
||||
ToggleStructureEvent as ToggleStructuresEvent,
|
||||
} from "../../InputHandler";
|
||||
import {
|
||||
@@ -84,6 +85,12 @@ export class StructureIconsLayer implements Layer {
|
||||
private readonly mousePos = { x: 0, y: 0 };
|
||||
private renderSprites = true;
|
||||
private factory: SpriteFactory;
|
||||
private ghostControls: {
|
||||
container: HTMLDivElement;
|
||||
confirm: HTMLButtonElement;
|
||||
cancel: HTMLButtonElement;
|
||||
flip: HTMLButtonElement;
|
||||
} | null = null;
|
||||
private readonly structures: Map<UnitType, { visible: boolean }> = new Map([
|
||||
[UnitType.City, { visible: true }],
|
||||
[UnitType.Factory, { visible: true }],
|
||||
@@ -244,21 +251,44 @@ export class StructureIconsLayer implements Layer {
|
||||
renderGhost() {
|
||||
if (!this.ghostUnit) return;
|
||||
|
||||
const rect = this.transformHandler.boundingRect();
|
||||
if (!rect) return;
|
||||
|
||||
let localX = this.mousePos.x - rect.left;
|
||||
let localY = this.mousePos.y - rect.top;
|
||||
let tileRef: TileRef | undefined;
|
||||
|
||||
// Always reposition locked ghost every frame (smooth when panning)
|
||||
if (
|
||||
this.uiState.lockedGhostTile &&
|
||||
this.isLockableGhost(this.ghostUnit.buildableUnit.type)
|
||||
) {
|
||||
tileRef = this.uiState.lockedGhostTile;
|
||||
const screen = this.transformHandler.worldToScreenCoordinates(
|
||||
new Cell(this.game.x(tileRef), this.game.y(tileRef)),
|
||||
);
|
||||
localX = screen.x - rect.left;
|
||||
localY = screen.y - rect.top;
|
||||
this.ghostUnit.container.position.set(localX, localY);
|
||||
this.ghostUnit.range?.position.set(localX, localY);
|
||||
this.updateGhostControls(localX, localY, rect);
|
||||
} else {
|
||||
this.hideGhostControls();
|
||||
const tile = this.transformHandler.screenToWorldCoordinates(
|
||||
localX,
|
||||
localY,
|
||||
);
|
||||
if (this.game.isValidCoord(tile.x, tile.y)) {
|
||||
tileRef = this.game.ref(tile.x, tile.y);
|
||||
}
|
||||
}
|
||||
|
||||
// Throttle expensive tile action queries
|
||||
const now = performance.now();
|
||||
if (now - this.lastGhostQueryAt < 50) {
|
||||
return;
|
||||
}
|
||||
const rect = this.transformHandler.boundingRect();
|
||||
if (!rect) return;
|
||||
|
||||
const localX = this.mousePos.x - rect.left;
|
||||
const localY = this.mousePos.y - rect.top;
|
||||
this.lastGhostQueryAt = now;
|
||||
let tileRef: TileRef | undefined;
|
||||
const tile = this.transformHandler.screenToWorldCoordinates(localX, localY);
|
||||
if (this.game.isValidCoord(tile.x, tile.y)) {
|
||||
tileRef = this.game.ref(tile.x, tile.y);
|
||||
}
|
||||
|
||||
// Check if targeting an ally (for nuke warning visual)
|
||||
// Uses shared logic with NukeExecution.maybeBreakAlliances()
|
||||
@@ -377,44 +407,42 @@ export class StructureIconsLayer implements Layer {
|
||||
}
|
||||
|
||||
private createStructure(e: MouseUpEvent) {
|
||||
if (!this.ghostUnit) return;
|
||||
// When locked ghost is active, clicking changes the target location
|
||||
if (
|
||||
this.ghostUnit.buildableUnit.canBuild === false &&
|
||||
this.ghostUnit.buildableUnit.canUpgrade === false
|
||||
this.uiState.lockedGhostTile &&
|
||||
this.isLockableGhost(this.ghostUnit?.buildableUnit.type ?? null)
|
||||
) {
|
||||
const newTile = this.getTileFromMouseEvent(e);
|
||||
if (newTile) {
|
||||
this.uiState.lockedGhostTile = newTile;
|
||||
// Force trajectory recalculation by clearing cached tile
|
||||
this.eventBus.emit(
|
||||
new GhostStructureChangedEvent(this.uiState.ghostStructure),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.ghostUnit) return;
|
||||
if (this.isGhostBuildBlocked()) {
|
||||
this.removeGhostStructure();
|
||||
return;
|
||||
}
|
||||
const rect = this.transformHandler.boundingRect();
|
||||
if (!rect) return;
|
||||
const x = e.x - rect.left;
|
||||
const y = e.y - rect.top;
|
||||
const tile = this.transformHandler.screenToWorldCoordinates(x, y);
|
||||
if (this.ghostUnit.buildableUnit.canUpgrade !== false) {
|
||||
this.eventBus.emit(
|
||||
new SendUpgradeStructureIntentEvent(
|
||||
this.ghostUnit.buildableUnit.canUpgrade,
|
||||
this.ghostUnit.buildableUnit.type,
|
||||
),
|
||||
);
|
||||
} else if (this.ghostUnit.buildableUnit.canBuild) {
|
||||
const unitType = this.ghostUnit.buildableUnit.type;
|
||||
const rocketDirectionUp =
|
||||
unitType === UnitType.AtomBomb || unitType === UnitType.HydrogenBomb
|
||||
? this.uiState.rocketDirectionUp
|
||||
: undefined;
|
||||
this.eventBus.emit(
|
||||
new BuildUnitIntentEvent(
|
||||
unitType,
|
||||
this.game.ref(tile.x, tile.y),
|
||||
rocketDirectionUp,
|
||||
),
|
||||
);
|
||||
const tileRef = this.resolveTargetTileFromEvent(e);
|
||||
if (!tileRef) {
|
||||
this.removeGhostStructure();
|
||||
return;
|
||||
}
|
||||
this.removeGhostStructure();
|
||||
this.commitStructure(tileRef);
|
||||
}
|
||||
|
||||
private moveGhost(e: MouseMoveEvent) {
|
||||
if (
|
||||
this.uiState.lockedGhostTile &&
|
||||
this.isLockableGhost(this.ghostUnit?.buildableUnit.type ?? null)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.mousePos.x = e.x;
|
||||
this.mousePos.y = e.y;
|
||||
|
||||
@@ -434,9 +462,22 @@ export class StructureIconsLayer implements Layer {
|
||||
if (type === null) {
|
||||
return;
|
||||
}
|
||||
if (!this.isLockableGhost(type)) {
|
||||
this.uiState.lockedGhostTile = null;
|
||||
}
|
||||
const rect = this.transformHandler.boundingRect();
|
||||
const localX = this.mousePos.x - rect.left;
|
||||
const localY = this.mousePos.y - rect.top;
|
||||
let localX = this.mousePos.x - rect.left;
|
||||
let localY = this.mousePos.y - rect.top;
|
||||
if (this.uiState.lockedGhostTile && this.isLockableGhost(type)) {
|
||||
const screen = this.transformHandler.worldToScreenCoordinates(
|
||||
new Cell(
|
||||
this.game.x(this.uiState.lockedGhostTile),
|
||||
this.game.y(this.uiState.lockedGhostTile),
|
||||
),
|
||||
);
|
||||
localX = screen.x - rect.left;
|
||||
localY = screen.y - rect.top;
|
||||
}
|
||||
const ghost = this.factory.createGhostContainer(
|
||||
player,
|
||||
this.ghostStage,
|
||||
@@ -464,6 +505,7 @@ export class StructureIconsLayer implements Layer {
|
||||
this.ghostUnit.range?.destroy();
|
||||
this.ghostUnit = null;
|
||||
}
|
||||
this.destroyGhostControls();
|
||||
if (this.potentialUpgrade) {
|
||||
this.potentialUpgrade.iconContainer.filters = [];
|
||||
this.potentialUpgrade.dotContainer.filters = [];
|
||||
@@ -474,9 +516,154 @@ export class StructureIconsLayer implements Layer {
|
||||
private removeGhostStructure() {
|
||||
this.clearGhostStructure();
|
||||
this.uiState.ghostStructure = null;
|
||||
this.uiState.lockedGhostTile = null;
|
||||
this.eventBus.emit(new GhostStructureChangedEvent(null));
|
||||
}
|
||||
|
||||
private emitBuildIntent(tileRef: TileRef) {
|
||||
if (!this.ghostUnit) return;
|
||||
if (this.ghostUnit.buildableUnit.canUpgrade !== false) {
|
||||
this.eventBus.emit(
|
||||
new SendUpgradeStructureIntentEvent(
|
||||
this.ghostUnit.buildableUnit.canUpgrade,
|
||||
this.ghostUnit.buildableUnit.type,
|
||||
),
|
||||
);
|
||||
} else if (this.ghostUnit.buildableUnit.canBuild) {
|
||||
const unitType = this.ghostUnit.buildableUnit.type;
|
||||
const rocketDirectionUp =
|
||||
unitType === UnitType.AtomBomb || unitType === UnitType.HydrogenBomb
|
||||
? this.uiState.rocketDirectionUp
|
||||
: undefined;
|
||||
this.eventBus.emit(
|
||||
new BuildUnitIntentEvent(unitType, tileRef, rocketDirectionUp),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private commitStructure(tileRef: TileRef) {
|
||||
this.emitBuildIntent(tileRef);
|
||||
this.removeGhostStructure();
|
||||
}
|
||||
|
||||
private getTileFromMouseEvent(e: MouseUpEvent): TileRef | null {
|
||||
const rect = this.transformHandler.boundingRect();
|
||||
if (!rect) return null;
|
||||
const x = e.x - rect.left;
|
||||
const y = e.y - rect.top;
|
||||
const tile = this.transformHandler.screenToWorldCoordinates(x, y);
|
||||
if (!this.game.isValidCoord(tile.x, tile.y)) return null;
|
||||
return this.game.ref(tile.x, tile.y);
|
||||
}
|
||||
|
||||
private resolveTargetTileFromEvent(e: MouseUpEvent): TileRef | null {
|
||||
if (
|
||||
this.uiState.lockedGhostTile &&
|
||||
this.isLockableGhost(this.ghostUnit?.buildableUnit.type ?? null)
|
||||
) {
|
||||
return this.uiState.lockedGhostTile;
|
||||
}
|
||||
return this.getTileFromMouseEvent(e);
|
||||
}
|
||||
|
||||
private isGhostBuildBlocked(): boolean {
|
||||
return (
|
||||
!this.ghostUnit ||
|
||||
(this.ghostUnit.buildableUnit.canBuild === false &&
|
||||
this.ghostUnit.buildableUnit.canUpgrade === false)
|
||||
);
|
||||
}
|
||||
|
||||
private isLockableGhost(type: UnitType | null): boolean {
|
||||
return type === UnitType.AtomBomb || type === UnitType.HydrogenBomb;
|
||||
}
|
||||
|
||||
private ensureGhostControls() {
|
||||
if (this.ghostControls) return;
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.style.position = "absolute";
|
||||
container.style.display = "flex";
|
||||
container.style.gap = "8px";
|
||||
container.style.transform = "translate(-50%, 0)";
|
||||
container.style.pointerEvents = "auto";
|
||||
container.style.zIndex = "5";
|
||||
|
||||
const makeButton = (
|
||||
label: string,
|
||||
background: string,
|
||||
onClick: () => void,
|
||||
): HTMLButtonElement => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.textContent = label;
|
||||
button.style.minHeight = "48px";
|
||||
button.style.minWidth = "48px";
|
||||
button.style.padding = "8px 16px";
|
||||
button.style.borderRadius = "6px";
|
||||
button.style.border = "none";
|
||||
button.style.fontWeight = "700";
|
||||
button.style.fontSize = "13px";
|
||||
button.style.color = "#ffffff";
|
||||
button.style.background = background;
|
||||
button.style.cursor = "pointer";
|
||||
button.style.boxShadow = "0 2px 6px rgba(0,0,0,0.25)";
|
||||
button.style.whiteSpace = "nowrap";
|
||||
button.addEventListener("click", onClick);
|
||||
return button;
|
||||
};
|
||||
|
||||
const confirm = makeButton("OK", "#2e7d32", () => {
|
||||
if (this.uiState.lockedGhostTile) {
|
||||
this.emitBuildIntent(this.uiState.lockedGhostTile);
|
||||
}
|
||||
});
|
||||
|
||||
const flip = makeButton("Flip", "#1565c0", () => {
|
||||
const next = !this.uiState.rocketDirectionUp;
|
||||
this.eventBus.emit(new SwapRocketDirectionEvent(next));
|
||||
});
|
||||
|
||||
const cancel = makeButton("X", "#b71c1c", () =>
|
||||
this.removeGhostStructure(),
|
||||
);
|
||||
|
||||
container.append(confirm, flip, cancel);
|
||||
document.body.appendChild(container);
|
||||
|
||||
this.ghostControls = { container, confirm, cancel, flip };
|
||||
}
|
||||
|
||||
private destroyGhostControls() {
|
||||
if (!this.ghostControls) return;
|
||||
this.ghostControls.container.remove();
|
||||
this.ghostControls = null;
|
||||
}
|
||||
|
||||
private hideGhostControls() {
|
||||
this.destroyGhostControls();
|
||||
}
|
||||
|
||||
private updateGhostControls(localX: number, localY: number, rect: DOMRect) {
|
||||
if (
|
||||
!this.ghostUnit ||
|
||||
!this.uiState.lockedGhostTile ||
|
||||
!this.isLockableGhost(this.ghostUnit.buildableUnit.type)
|
||||
) {
|
||||
this.destroyGhostControls();
|
||||
return;
|
||||
}
|
||||
this.ensureGhostControls();
|
||||
const offsetY = 40;
|
||||
const scale = Math.max(
|
||||
0.75,
|
||||
Math.min(1.4, this.transformHandler.scale / 2),
|
||||
);
|
||||
this.ghostControls!.container.style.left = `${rect.left + localX}px`;
|
||||
this.ghostControls!.container.style.top = `${rect.top + localY + offsetY}px`;
|
||||
this.ghostControls!.container.style.transform = `translate(-50%, 0) scale(${scale})`;
|
||||
}
|
||||
|
||||
private resolveGhostRangeLevel(
|
||||
buildableUnit: BuildableUnit,
|
||||
): number | undefined {
|
||||
|
||||
Reference in New Issue
Block a user