Bomb Direction (#2435)

Resolves #2434 

## Description:

Allows bomb direction to be inverted by pressing a hotkey - currently
"U".

**Check the issue for screenshots / videos.**

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

w.o.n

---------

Co-authored-by: Evan <evanpelle@gmail.com>
Co-authored-by: iamlewis <lewismmmm@gmail.com>
This commit is contained in:
Ryan
2025-12-29 17:03:46 +00:00
committed by GitHub
parent 6b14d9cca1
commit f1561df470
15 changed files with 90 additions and 12 deletions
+2 -1
View File
@@ -130,7 +130,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",
+4
View File
@@ -138,6 +138,10 @@ export class HelpModal extends LitElement {
<td>${this.renderKey(keybinds.toggleView)}</td>
<td>${translateText("help_modal.action_alt_view")}</td>
</tr>
<tr>
<td><span class="key">U</span></td>
<td>${translateText("help_modal.bomb_direction")}</td>
</tr>
<tr>
<td>
<div class="scroll-combo-horizontal">
+8
View File
@@ -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",
@@ -427,6 +430,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) {
+2
View File
@@ -91,6 +91,7 @@ export class BuildUnitIntentEvent implements GameEvent {
constructor(
public readonly unit: UnitType,
public readonly tile: TileRef,
public readonly rocketDirectionUp?: boolean,
) {}
}
@@ -573,6 +574,7 @@ export class Transport {
clientID: this.lobbyConfig.clientID,
unit: event.unit,
tile: event.tile,
rocketDirectionUp: event.rocketDirectionUp,
});
}
+7 -2
View File
@@ -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;
@@ -241,7 +246,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,
+1
View File
@@ -3,4 +3,5 @@ import { UnitType } from "../../core/game/Game";
export interface UIState {
attackRatio: number;
ghostStructure: UnitType | null;
rocketDirectionUp: boolean;
}
+10 -1
View File
@@ -22,6 +22,7 @@ import {
} from "../../Transport";
import { renderNumber } from "../../Utils";
import { TransformHandler } from "../TransformHandler";
import { UIState } from "../UIState";
import { Layer } from "./Layer";
import warshipIcon from "/images/BattleshipIconWhite.svg?url";
import cityIcon from "/images/CityIconWhite.svg?url";
@@ -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();
}
@@ -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();
@@ -373,10 +373,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,
),
);
}
+1
View File
@@ -311,6 +311,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({
+10 -1
View File
@@ -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:
+6 -1
View File
@@ -106,7 +106,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);
}
+2
View File
@@ -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 {
@@ -137,6 +138,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,
+15 -3
View File
@@ -1,6 +1,7 @@
import { Game } from "../game/Game";
import { GameMap, TileRef } from "../game/GameMap";
import { PseudoRandom } from "../PseudoRandom";
import { within } from "../Util";
import { DistanceBasedBezierCurve } from "../utilities/Line";
import { AStar, AStarResult, PathFindResultType } from "./AStar";
import { MiniAStar } from "./MiniAStar";
@@ -16,6 +17,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 +27,24 @@ 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: within(
p0.y + (p3.y - p0.y) / 4 + heightMultiplier * maxHeight,
0,
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: within(
p0.y + ((p3.y - p0.y) * 3) / 4 + heightMultiplier * maxHeight,
0,
mapHeight - 1,
),
};
this.curve = new DistanceBasedBezierCurve(p0, p1, p2, p3, increment);
+1 -1
View File
@@ -34,7 +34,7 @@ describe("InputHandler AutoUpgrade", () => {
eventBus = new EventBus();
inputHandler = new InputHandler(
{ attackRatio: 20, ghostStructure: null },
{ attackRatio: 20, ghostStructure: null, rocketDirectionUp: true },
mockCanvas,
eventBus,
);