diff --git a/resources/lang/en.json b/resources/lang/en.json
index 074a35c4ad..8d372517e3 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -129,7 +129,8 @@
"icon_embargo": "Dollar stop sign - Embargo. This player has stopped trading with you automatically or manually.",
"icon_request": "Envelope - Alliance request. This player has sent you an alliance request.",
"info_enemy_panel": "Enemy info panel",
- "exit_confirmation": "Are you sure you want to exit the game?"
+ "exit_confirmation": "Are you sure you want to exit the game?",
+ "bomb_direction": "Atom / Hydrogen bomb arc direction"
},
"single_modal": {
"title": "Single Player",
diff --git a/src/client/HelpModal.ts b/src/client/HelpModal.ts
index 1b840c27ba..dac511056a 100644
--- a/src/client/HelpModal.ts
+++ b/src/client/HelpModal.ts
@@ -55,6 +55,10 @@ export class HelpModal extends LitElement {
|
diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts
index 26d8f6c27d..7d4fb5e48c 100644
--- a/src/client/InputHandler.ts
+++ b/src/client/InputHandler.ts
@@ -89,6 +89,8 @@ export class GhostStructureChangedEvent implements GameEvent {
constructor(public readonly ghostStructure: UnitType | null) {}
}
+export class SwapRocketDirectionEvent implements GameEvent {}
+
export class ShowBuildMenuEvent implements GameEvent {
constructor(
public readonly x: number,
@@ -200,6 +202,7 @@ export class InputHandler {
attackRatioUp: "KeyY",
boatAttack: "KeyB",
groundAttack: "KeyG",
+ swapDirection: "KeyU",
modifierKey: isMac ? "MetaLeft" : "ControlLeft",
altKey: "AltLeft",
buildCity: "Digit1",
@@ -417,6 +420,11 @@ export class InputHandler {
this.setGhostStructure(UnitType.MIRV);
}
+ if (e.code === this.keybinds.swapDirection) {
+ e.preventDefault();
+ this.eventBus.emit(new SwapRocketDirectionEvent());
+ }
+
// Shift-D to toggle performance overlay
console.log(e.code, e.shiftKey, e.ctrlKey, e.altKey, e.metaKey);
if (e.code === "KeyD" && e.shiftKey) {
diff --git a/src/client/Transport.ts b/src/client/Transport.ts
index 9f4f1f5a7f..96958b0823 100644
--- a/src/client/Transport.ts
+++ b/src/client/Transport.ts
@@ -90,6 +90,7 @@ export class BuildUnitIntentEvent implements GameEvent {
constructor(
public readonly unit: UnitType,
public readonly tile: TileRef,
+ public readonly rocketDirectionUp?: boolean,
) {}
}
@@ -571,6 +572,7 @@ export class Transport {
clientID: this.lobbyConfig.clientID,
unit: event.unit,
tile: event.tile,
+ rocketDirectionUp: event.rocketDirectionUp,
});
}
diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts
index 50911c8dbc..c553cfa2ca 100644
--- a/src/client/graphics/GameRenderer.ts
+++ b/src/client/graphics/GameRenderer.ts
@@ -50,7 +50,11 @@ export function createRenderer(
const transformHandler = new TransformHandler(game, eventBus, canvas);
const userSettings = new UserSettings();
- const uiState = { attackRatio: 20, ghostStructure: null } as UIState;
+ const uiState = {
+ attackRatio: 20,
+ ghostStructure: null,
+ rocketDirectionUp: true,
+ } as UIState;
//hide when the game renders
const startingModal = document.querySelector(
@@ -73,6 +77,7 @@ export function createRenderer(
}
buildMenu.game = game;
buildMenu.eventBus = eventBus;
+ buildMenu.uiState = uiState;
buildMenu.transformHandler = transformHandler;
const leaderboard = document.querySelector("leader-board") as Leaderboard;
@@ -245,7 +250,7 @@ export function createRenderer(
new UnitLayer(game, eventBus, transformHandler),
new FxLayer(game),
new UILayer(game, eventBus, transformHandler),
- new NukeTrajectoryPreviewLayer(game, eventBus, transformHandler),
+ new NukeTrajectoryPreviewLayer(game, eventBus, transformHandler, uiState),
new StructureIconsLayer(game, eventBus, uiState, transformHandler),
new NameLayer(game, transformHandler, eventBus),
eventsDisplay,
diff --git a/src/client/graphics/UIState.ts b/src/client/graphics/UIState.ts
index 01c4a60cb9..f47acef885 100644
--- a/src/client/graphics/UIState.ts
+++ b/src/client/graphics/UIState.ts
@@ -3,4 +3,5 @@ import { UnitType } from "../../core/game/Game";
export interface UIState {
attackRatio: number;
ghostStructure: UnitType | null;
+ rocketDirectionUp: boolean;
}
diff --git a/src/client/graphics/layers/BuildMenu.ts b/src/client/graphics/layers/BuildMenu.ts
index 46adc33677..39fd604a11 100644
--- a/src/client/graphics/layers/BuildMenu.ts
+++ b/src/client/graphics/layers/BuildMenu.ts
@@ -33,6 +33,7 @@ import {
} from "../../Transport";
import { renderNumber } from "../../Utils";
import { TransformHandler } from "../TransformHandler";
+import { UIState } from "../UIState";
import { Layer } from "./Layer";
export interface BuildItemDisplay {
@@ -125,6 +126,7 @@ export const flattenedBuildTable = buildTable.flat();
export class BuildMenu extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
+ public uiState: UIState;
private clickedTile: TileRef;
public playerActions: PlayerActions | null;
private filteredBuildTable: BuildItemDisplay[][] = buildTable;
@@ -395,7 +397,14 @@ export class BuildMenu extends LitElement implements Layer {
),
);
} else if (buildableUnit.canBuild) {
- this.eventBus.emit(new BuildUnitIntentEvent(buildableUnit.type, tile));
+ const rocketDirectionUp =
+ buildableUnit.type === UnitType.AtomBomb ||
+ buildableUnit.type === UnitType.HydrogenBomb
+ ? this.uiState.rocketDirectionUp
+ : undefined;
+ this.eventBus.emit(
+ new BuildUnitIntentEvent(buildableUnit.type, tile, rocketDirectionUp),
+ );
}
this.hideMenu();
}
diff --git a/src/client/graphics/layers/NukeTrajectoryPreviewLayer.ts b/src/client/graphics/layers/NukeTrajectoryPreviewLayer.ts
index c0e74af83e..67632585dd 100644
--- a/src/client/graphics/layers/NukeTrajectoryPreviewLayer.ts
+++ b/src/client/graphics/layers/NukeTrajectoryPreviewLayer.ts
@@ -3,8 +3,13 @@ import { UnitType } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView } from "../../../core/game/GameView";
import { ParabolaPathFinder } from "../../../core/pathfinding/PathFinding";
-import { GhostStructureChangedEvent, MouseMoveEvent } from "../../InputHandler";
+import {
+ GhostStructureChangedEvent,
+ MouseMoveEvent,
+ SwapRocketDirectionEvent,
+} from "../../InputHandler";
import { TransformHandler } from "../TransformHandler";
+import { UIState } from "../UIState";
import { Layer } from "./Layer";
/**
@@ -27,6 +32,7 @@ export class NukeTrajectoryPreviewLayer implements Layer {
private game: GameView,
private eventBus: EventBus,
private transformHandler: TransformHandler,
+ private uiState: UIState,
) {}
shouldTransform(): boolean {
@@ -50,6 +56,12 @@ export class NukeTrajectoryPreviewLayer implements Layer {
this.cachedSpawnTile = null;
}
});
+ this.eventBus.on(SwapRocketDirectionEvent, () => {
+ // Toggle rocket direction
+ this.uiState.rocketDirectionUp = !this.uiState.rocketDirectionUp;
+ // Force trajectory recalculation
+ this.lastTargetTile = null;
+ });
}
tick() {
@@ -210,6 +222,7 @@ export class NukeTrajectoryPreviewLayer implements Layer {
targetTile,
speed,
distanceBasedHeight,
+ this.uiState.rocketDirectionUp,
);
this.trajectoryPoints = pathFinder.allTiles();
diff --git a/src/client/graphics/layers/StructureIconsLayer.ts b/src/client/graphics/layers/StructureIconsLayer.ts
index 1458e7affd..0b12cb90d7 100644
--- a/src/client/graphics/layers/StructureIconsLayer.ts
+++ b/src/client/graphics/layers/StructureIconsLayer.ts
@@ -334,10 +334,16 @@ export class StructureIconsLayer implements Layer {
),
);
} 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(
- this.ghostUnit.buildableUnit.type,
+ unitType,
this.game.ref(tile.x, tile.y),
+ rocketDirectionUp,
),
);
}
diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts
index 74b999b13c..c0eb8b0306 100644
--- a/src/core/Schemas.ts
+++ b/src/core/Schemas.ts
@@ -304,6 +304,7 @@ export const BuildUnitIntentSchema = BaseIntentSchema.extend({
type: z.literal("build_unit"),
unit: z.enum(UnitType),
tile: z.number(),
+ rocketDirectionUp: z.boolean().optional(),
});
export const UpgradeStructureIntentSchema = BaseIntentSchema.extend({
diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts
index 799556fe06..2fdd92da1c 100644
--- a/src/core/execution/ConstructionExecution.ts
+++ b/src/core/execution/ConstructionExecution.ts
@@ -21,6 +21,7 @@ export class ConstructionExecution implements Execution {
private player: Player,
private constructionType: UnitType,
private tile: TileRef,
+ private rocketDirectionUp?: boolean,
) {}
init(mg: Game, ticks: number): void {
@@ -104,7 +105,15 @@ export class ConstructionExecution implements Execution {
case UnitType.AtomBomb:
case UnitType.HydrogenBomb:
this.mg.addExecution(
- new NukeExecution(this.constructionType, player, this.tile),
+ new NukeExecution(
+ this.constructionType,
+ player,
+ this.tile,
+ null,
+ -1,
+ 0,
+ this.rocketDirectionUp,
+ ),
);
break;
case UnitType.MIRV:
diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts
index 4a8e5df916..af97167adb 100644
--- a/src/core/execution/ExecutionManager.ts
+++ b/src/core/execution/ExecutionManager.ts
@@ -105,7 +105,12 @@ export class Executor {
case "embargo_all":
return new EmbargoAllExecution(player, intent.action);
case "build_unit":
- return new ConstructionExecution(player, intent.unit, intent.tile);
+ return new ConstructionExecution(
+ player,
+ intent.unit,
+ intent.tile,
+ intent.rocketDirectionUp,
+ );
case "allianceExtension": {
return new AllianceExtensionExecution(player, intent.recipient);
}
diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts
index 131533784b..f3b97249ad 100644
--- a/src/core/execution/NukeExecution.ts
+++ b/src/core/execution/NukeExecution.ts
@@ -30,6 +30,7 @@ export class NukeExecution implements Execution {
private src?: TileRef | null,
private speed: number = -1,
private waitTicks = 0,
+ private rocketDirectionUp: boolean = true,
) {}
init(mg: Game, ticks: number): void {
@@ -115,6 +116,7 @@ export class NukeExecution implements Execution {
this.dst,
this.speed,
this.nukeType !== UnitType.MIRVWarhead,
+ this.rocketDirectionUp,
);
this.nuke = this.player.buildUnit(this.nukeType, spawn, {
targetTile: this.dst,
diff --git a/src/core/pathfinding/PathFinding.ts b/src/core/pathfinding/PathFinding.ts
index 2050cc162d..d62487f0b0 100644
--- a/src/core/pathfinding/PathFinding.ts
+++ b/src/core/pathfinding/PathFinding.ts
@@ -16,6 +16,7 @@ export class ParabolaPathFinder {
dst: TileRef,
increment: number = 3,
distanceBasedHeight = true,
+ directionUp = true,
) {
const p0 = { x: this.mg.x(orig), y: this.mg.y(orig) };
const p3 = { x: this.mg.x(dst), y: this.mg.y(dst) };
@@ -25,14 +26,28 @@ export class ParabolaPathFinder {
const maxHeight = distanceBasedHeight
? Math.max(distance / 3, parabolaMinHeight)
: 0;
- // Use a bezier curve always pointing up
+ // Use a bezier curve pointing up or down based on directionUp parameter
+ const heightMultiplier = directionUp ? -1 : 1;
+ const mapHeight = this.mg.height();
const p1 = {
x: p0.x + (p3.x - p0.x) / 4,
- y: Math.max(p0.y + (p3.y - p0.y) / 4 - maxHeight, 0),
+ y: Math.max(
+ 0,
+ Math.min(
+ p0.y + (p3.y - p0.y) / 4 + heightMultiplier * maxHeight,
+ mapHeight - 1,
+ ),
+ ),
};
const p2 = {
x: p0.x + ((p3.x - p0.x) * 3) / 4,
- y: Math.max(p0.y + ((p3.y - p0.y) * 3) / 4 - maxHeight, 0),
+ y: Math.max(
+ 0,
+ Math.min(
+ p0.y + ((p3.y - p0.y) * 3) / 4 + heightMultiplier * maxHeight,
+ mapHeight - 1,
+ ),
+ ),
};
this.curve = new DistanceBasedBezierCurve(p0, p1, p2, p3, increment);
diff --git a/tests/InputHandler.test.ts b/tests/InputHandler.test.ts
index 210d65739e..16cadc97ff 100644
--- a/tests/InputHandler.test.ts
+++ b/tests/InputHandler.test.ts
@@ -37,7 +37,7 @@ describe("InputHandler AutoUpgrade", () => {
eventBus = new EventBus();
inputHandler = new InputHandler(
- { attackRatio: 20, ghostStructure: null },
+ { attackRatio: 20, ghostStructure: null, rocketDirectionUp: true },
mockCanvas,
eventBus,
);
|