mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:30:45 +00:00
Enhance InputHandler to allow using NumPad (#3317)
## Description: Adds **Enter** and **Numpad Enter** as confirmation for placing a ghost structure after selecting a building with hotkeys (1–0 or numpad). Players can cancel with Esc but previously had to click to confirm; they can now confirm with Enter or Numpad Enter at the current cursor position. This supports keyboard-only or mouse + numpad workflows (e.g. one hand on numpad for select + confirm, one on mouse for aiming). ## 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: .wozniakpl
This commit is contained in:
+79
-47
@@ -92,6 +92,8 @@ export class GhostStructureChangedEvent implements GameEvent {
|
||||
constructor(public readonly ghostStructure: PlayerBuildableUnitType | null) {}
|
||||
}
|
||||
|
||||
export class ConfirmGhostStructureEvent implements GameEvent {}
|
||||
|
||||
export class SwapRocketDirectionEvent implements GameEvent {
|
||||
constructor(public readonly rocketDirectionUp: boolean) {}
|
||||
}
|
||||
@@ -339,6 +341,14 @@ export class InputHandler {
|
||||
this.setGhostStructure(null);
|
||||
}
|
||||
|
||||
if (
|
||||
(e.code === "Enter" || e.code === "NumpadEnter") &&
|
||||
this.uiState.ghostStructure !== null
|
||||
) {
|
||||
e.preventDefault();
|
||||
this.eventBus.emit(new ConfirmGhostStructureEvent());
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
this.keybinds.moveUp,
|
||||
@@ -410,54 +420,11 @@ export class InputHandler {
|
||||
this.eventBus.emit(new CenterCameraEvent());
|
||||
}
|
||||
|
||||
if (e.code === this.keybinds.buildCity) {
|
||||
// Two-phase build keybind matching: exact code match first, then digit/Numpad alias.
|
||||
const matchedBuild = this.resolveBuildKeybind(e.code);
|
||||
if (matchedBuild !== null) {
|
||||
e.preventDefault();
|
||||
this.setGhostStructure(UnitType.City);
|
||||
}
|
||||
|
||||
if (e.code === this.keybinds.buildFactory) {
|
||||
e.preventDefault();
|
||||
this.setGhostStructure(UnitType.Factory);
|
||||
}
|
||||
|
||||
if (e.code === this.keybinds.buildPort) {
|
||||
e.preventDefault();
|
||||
this.setGhostStructure(UnitType.Port);
|
||||
}
|
||||
|
||||
if (e.code === this.keybinds.buildDefensePost) {
|
||||
e.preventDefault();
|
||||
this.setGhostStructure(UnitType.DefensePost);
|
||||
}
|
||||
|
||||
if (e.code === this.keybinds.buildMissileSilo) {
|
||||
e.preventDefault();
|
||||
this.setGhostStructure(UnitType.MissileSilo);
|
||||
}
|
||||
|
||||
if (e.code === this.keybinds.buildSamLauncher) {
|
||||
e.preventDefault();
|
||||
this.setGhostStructure(UnitType.SAMLauncher);
|
||||
}
|
||||
|
||||
if (e.code === this.keybinds.buildAtomBomb) {
|
||||
e.preventDefault();
|
||||
this.setGhostStructure(UnitType.AtomBomb);
|
||||
}
|
||||
|
||||
if (e.code === this.keybinds.buildHydrogenBomb) {
|
||||
e.preventDefault();
|
||||
this.setGhostStructure(UnitType.HydrogenBomb);
|
||||
}
|
||||
|
||||
if (e.code === this.keybinds.buildWarship) {
|
||||
e.preventDefault();
|
||||
this.setGhostStructure(UnitType.Warship);
|
||||
}
|
||||
|
||||
if (e.code === this.keybinds.buildMIRV) {
|
||||
e.preventDefault();
|
||||
this.setGhostStructure(UnitType.MIRV);
|
||||
this.setGhostStructure(matchedBuild);
|
||||
}
|
||||
|
||||
if (e.code === this.keybinds.swapDirection) {
|
||||
@@ -616,6 +583,71 @@ export class InputHandler {
|
||||
this.eventBus.emit(new GhostStructureChangedEvent(ghostStructure));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the digit character from KeyboardEvent.code.
|
||||
* Codes look like "Digit0".."Digit9" (6 chars, digit at index 5) and
|
||||
* "Numpad0".."Numpad9" (7 chars, digit at index 6). Returns null if not a digit key.
|
||||
*/
|
||||
private digitFromKeyCode(code: string): string | null {
|
||||
if (
|
||||
code?.length === 6 &&
|
||||
code.startsWith("Digit") &&
|
||||
/^[0-9]$/.test(code[5])
|
||||
)
|
||||
return code[5];
|
||||
if (
|
||||
code?.length === 7 &&
|
||||
code.startsWith("Numpad") &&
|
||||
/^[0-9]$/.test(code[6])
|
||||
)
|
||||
return code[6];
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Strict equality only: used for first-pass exact KeyboardEvent.code match. */
|
||||
private buildKeybindMatches(code: string, keybindValue: string): boolean {
|
||||
return code === keybindValue;
|
||||
}
|
||||
|
||||
/** Digit/Numpad alias match: used only when no exact match was found. */
|
||||
private buildKeybindMatchesDigit(
|
||||
code: string,
|
||||
keybindValue: string,
|
||||
): boolean {
|
||||
const digit = this.digitFromKeyCode(code);
|
||||
const bindDigit = this.digitFromKeyCode(keybindValue);
|
||||
return digit !== null && bindDigit !== null && digit === bindDigit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a keyup code to a build action: exact code match first, then digit/Numpad alias.
|
||||
* Returns the UnitType to set as ghost, or null if no build keybind matched.
|
||||
*/
|
||||
private resolveBuildKeybind(code: string): PlayerBuildableUnitType | null {
|
||||
const buildKeybinds: ReadonlyArray<{
|
||||
key: string;
|
||||
type: PlayerBuildableUnitType;
|
||||
}> = [
|
||||
{ key: "buildCity", type: UnitType.City },
|
||||
{ key: "buildFactory", type: UnitType.Factory },
|
||||
{ key: "buildPort", type: UnitType.Port },
|
||||
{ key: "buildDefensePost", type: UnitType.DefensePost },
|
||||
{ key: "buildMissileSilo", type: UnitType.MissileSilo },
|
||||
{ key: "buildSamLauncher", type: UnitType.SAMLauncher },
|
||||
{ key: "buildAtomBomb", type: UnitType.AtomBomb },
|
||||
{ key: "buildHydrogenBomb", type: UnitType.HydrogenBomb },
|
||||
{ key: "buildWarship", type: UnitType.Warship },
|
||||
{ key: "buildMIRV", type: UnitType.MIRV },
|
||||
];
|
||||
for (const { key, type } of buildKeybinds) {
|
||||
if (this.buildKeybindMatches(code, this.keybinds[key])) return type;
|
||||
}
|
||||
for (const { key, type } of buildKeybinds) {
|
||||
if (this.buildKeybindMatchesDigit(code, this.keybinds[key])) return type;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private getPinchDistance(): number {
|
||||
const pointerEvents = Array.from(this.pointers.values());
|
||||
const dx = pointerEvents[0].clientX - pointerEvents[1].clientX;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { GameView, UnitView } from "../../../core/game/GameView";
|
||||
import {
|
||||
ConfirmGhostStructureEvent,
|
||||
GhostStructureChangedEvent,
|
||||
MouseMoveEvent,
|
||||
MouseUpEvent,
|
||||
@@ -43,6 +44,11 @@ import {
|
||||
} from "./StructureDrawingUtils";
|
||||
import bitmapFont from "/fonts/round_6x6_modified.xml?url";
|
||||
|
||||
/** True for nuke types (AtomBomb, HydrogenBomb): ghost is preserved after placement so user can place multiple or keep selection (Enter/key confirm). */
|
||||
export function shouldPreserveGhostAfterBuild(unitType: UnitType): boolean {
|
||||
return unitType === UnitType.AtomBomb || unitType === UnitType.HydrogenBomb;
|
||||
}
|
||||
|
||||
extend([a11yPlugin]);
|
||||
|
||||
class StructureRenderInfo {
|
||||
@@ -92,6 +98,7 @@ export class StructureIconsLayer implements Layer {
|
||||
> = new Map(Structures.types.map((type) => [type, { visible: true }]));
|
||||
private lastGhostQueryAt: number;
|
||||
private visibilityStateDirty = true;
|
||||
private pendingConfirm: MouseUpEvent | null = null;
|
||||
private hasHiddenStructure = false;
|
||||
potentialUpgrade: StructureRenderInfo | undefined;
|
||||
|
||||
@@ -171,7 +178,12 @@ export class StructureIconsLayer implements Layer {
|
||||
);
|
||||
this.eventBus.on(MouseMoveEvent, (e) => this.moveGhost(e));
|
||||
|
||||
this.eventBus.on(MouseUpEvent, (e) => this.createStructure(e));
|
||||
this.eventBus.on(MouseUpEvent, (e) => this.requestConfirmStructure(e));
|
||||
this.eventBus.on(ConfirmGhostStructureEvent, () =>
|
||||
this.requestConfirmStructure(
|
||||
new MouseUpEvent(this.mousePos.x, this.mousePos.y),
|
||||
),
|
||||
);
|
||||
|
||||
window.addEventListener("resize", () => this.resizeCanvas());
|
||||
await this.setupRenderer();
|
||||
@@ -307,7 +319,10 @@ export class StructureIconsLayer implements Layer {
|
||||
this.ghostUnit.container.filters = [];
|
||||
}
|
||||
|
||||
if (!this.ghostUnit) return;
|
||||
if (!this.ghostUnit) {
|
||||
this.pendingConfirm = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const unit = buildables.find(
|
||||
(u) => u.type === this.ghostUnit!.buildableUnit.type,
|
||||
@@ -322,6 +337,7 @@ export class StructureIconsLayer implements Layer {
|
||||
this.ghostUnit.container.filters = [
|
||||
new OutlineFilter({ thickness: 2, color: "rgba(255, 0, 0, 1)" }),
|
||||
];
|
||||
this.pendingConfirm = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -369,6 +385,14 @@ export class StructureIconsLayer implements Layer {
|
||||
: Math.min(1, scale / ICON_SCALE_FACTOR_ZOOMED_OUT);
|
||||
this.ghostUnit.container.scale.set(s);
|
||||
this.ghostUnit.range?.scale.set(this.transformHandler.scale);
|
||||
|
||||
if (this.pendingConfirm !== null) {
|
||||
const ev = this.pendingConfirm;
|
||||
this.pendingConfirm = null;
|
||||
if (this.isGhostReadyForConfirm()) {
|
||||
this.createStructure(ev);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -399,6 +423,30 @@ export class StructureIconsLayer implements Layer {
|
||||
.fill({ color: 0x000000, alpha: 0.65 });
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the ghost exists and buildableUnit has been refreshed (canBuild or canUpgrade set).
|
||||
* Used to avoid running createStructure before renderGhost's async buildables() has updated the ghost.
|
||||
*/
|
||||
private isGhostReadyForConfirm(): boolean {
|
||||
if (!this.ghostUnit) return false;
|
||||
const bu = this.ghostUnit.buildableUnit;
|
||||
return bu.canBuild !== false || bu.canUpgrade !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request confirm (place/upgrade): run createStructure now if ghost is ready, otherwise defer until
|
||||
* renderGhost's buildables() callback has updated the ghost. Shared by Enter (ConfirmGhostStructureEvent)
|
||||
* and mouse click (MouseUpEvent) so numpad-select-then-confirm works.
|
||||
*/
|
||||
private requestConfirmStructure(e: MouseUpEvent): void {
|
||||
if (!this.ghostUnit && !this.uiState.ghostStructure) return;
|
||||
if (this.isGhostReadyForConfirm()) {
|
||||
this.createStructure(e);
|
||||
} else {
|
||||
this.pendingConfirm = e;
|
||||
}
|
||||
}
|
||||
|
||||
private createStructure(e: MouseUpEvent) {
|
||||
if (!this.ghostUnit) return;
|
||||
if (
|
||||
@@ -420,6 +468,7 @@ export class StructureIconsLayer implements Layer {
|
||||
this.ghostUnit.buildableUnit.type,
|
||||
),
|
||||
);
|
||||
this.removeGhostStructure();
|
||||
} else if (this.ghostUnit.buildableUnit.canBuild) {
|
||||
const unitType = this.ghostUnit.buildableUnit.type;
|
||||
const rocketDirectionUp =
|
||||
@@ -433,8 +482,12 @@ export class StructureIconsLayer implements Layer {
|
||||
rocketDirectionUp,
|
||||
),
|
||||
);
|
||||
if (!shouldPreserveGhostAfterBuild(unitType)) {
|
||||
this.removeGhostStructure();
|
||||
}
|
||||
} else {
|
||||
this.removeGhostStructure();
|
||||
}
|
||||
this.removeGhostStructure();
|
||||
}
|
||||
|
||||
private moveGhost(e: MouseMoveEvent) {
|
||||
@@ -489,6 +542,7 @@ export class StructureIconsLayer implements Layer {
|
||||
}
|
||||
|
||||
private clearGhostStructure() {
|
||||
this.pendingConfirm = null;
|
||||
if (this.ghostUnit) {
|
||||
this.ghostUnit.container.destroy();
|
||||
this.ghostUnit.range?.destroy();
|
||||
|
||||
+188
-1
@@ -1,5 +1,11 @@
|
||||
import { AutoUpgradeEvent, InputHandler } from "../src/client/InputHandler";
|
||||
import {
|
||||
AutoUpgradeEvent,
|
||||
ConfirmGhostStructureEvent,
|
||||
InputHandler,
|
||||
} from "../src/client/InputHandler";
|
||||
import { UIState } from "../src/client/graphics/UIState";
|
||||
import { EventBus } from "../src/core/EventBus";
|
||||
import { UnitType } from "../src/core/game/Game";
|
||||
|
||||
class MockPointerEvent {
|
||||
button: number;
|
||||
@@ -462,4 +468,185 @@ describe("InputHandler AutoUpgrade", () => {
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Enter key confirm ghost structure", () => {
|
||||
let uiState: UIState;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.removeItem("settings.keybinds");
|
||||
uiState = {
|
||||
attackRatio: 20,
|
||||
ghostStructure: null,
|
||||
rocketDirectionUp: true,
|
||||
overlappingRailroads: [],
|
||||
ghostRailPaths: [],
|
||||
} as UIState;
|
||||
inputHandler = new InputHandler(uiState, mockCanvas, eventBus);
|
||||
inputHandler.initialize();
|
||||
});
|
||||
|
||||
test("emits ConfirmGhostStructureEvent on Enter when ghost structure is set", () => {
|
||||
const mockEmit = vi.spyOn(eventBus, "emit");
|
||||
uiState.ghostStructure = UnitType.City;
|
||||
|
||||
window.dispatchEvent(new KeyboardEvent("keydown", { code: "Enter" }));
|
||||
|
||||
expect(mockEmit).toHaveBeenCalledWith(
|
||||
expect.any(ConfirmGhostStructureEvent),
|
||||
);
|
||||
});
|
||||
|
||||
test("emits ConfirmGhostStructureEvent on NumpadEnter when ghost structure is set", () => {
|
||||
const mockEmit = vi.spyOn(eventBus, "emit");
|
||||
uiState.ghostStructure = UnitType.Factory;
|
||||
|
||||
window.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadEnter" }),
|
||||
);
|
||||
|
||||
expect(mockEmit).toHaveBeenCalledWith(
|
||||
expect.any(ConfirmGhostStructureEvent),
|
||||
);
|
||||
});
|
||||
|
||||
test("does not emit ConfirmGhostStructureEvent on Enter when no ghost structure", () => {
|
||||
const mockEmit = vi.spyOn(eventBus, "emit");
|
||||
expect(uiState.ghostStructure).toBeNull();
|
||||
|
||||
window.dispatchEvent(new KeyboardEvent("keydown", { code: "Enter" }));
|
||||
|
||||
const confirmCalls = mockEmit.mock.calls.filter(
|
||||
(call) => call[0] instanceof ConfirmGhostStructureEvent,
|
||||
);
|
||||
expect(confirmCalls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Numpad number keys for build keybinds", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.removeItem("settings.keybinds");
|
||||
inputHandler.destroy();
|
||||
const uiState: UIState = {
|
||||
attackRatio: 20,
|
||||
ghostStructure: null,
|
||||
rocketDirectionUp: true,
|
||||
overlappingRailroads: [],
|
||||
ghostRailPaths: [],
|
||||
} as UIState;
|
||||
inputHandler = new InputHandler(uiState, mockCanvas, eventBus);
|
||||
inputHandler.initialize();
|
||||
});
|
||||
|
||||
test("Numpad1 sets ghost structure to City when buildCity is Digit1", () => {
|
||||
window.dispatchEvent(
|
||||
new KeyboardEvent("keyup", { code: "Numpad1", key: "1" }),
|
||||
);
|
||||
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.City);
|
||||
});
|
||||
|
||||
test("Numpad5 sets ghost structure to MissileSilo when buildMissileSilo is Digit5", () => {
|
||||
window.dispatchEvent(
|
||||
new KeyboardEvent("keyup", { code: "Numpad5", key: "5" }),
|
||||
);
|
||||
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.MissileSilo);
|
||||
});
|
||||
|
||||
test("Numpad0 sets ghost structure to MIRV when buildMIRV is Digit0", () => {
|
||||
window.dispatchEvent(
|
||||
new KeyboardEvent("keyup", { code: "Numpad0", key: "0" }),
|
||||
);
|
||||
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.MIRV);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Build keybind two-phase matching (exact code first, then digit/Numpad alias)", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.removeItem("settings.keybinds");
|
||||
inputHandler.destroy();
|
||||
const uiState: UIState = {
|
||||
attackRatio: 20,
|
||||
ghostStructure: null,
|
||||
rocketDirectionUp: true,
|
||||
overlappingRailroads: [],
|
||||
ghostRailPaths: [],
|
||||
} as UIState;
|
||||
inputHandler = new InputHandler(uiState, mockCanvas, eventBus);
|
||||
inputHandler.initialize();
|
||||
});
|
||||
|
||||
test("exact code match wins: Digit1 sets City when buildCity=Digit1 and buildFactory=Numpad1", () => {
|
||||
localStorage.setItem(
|
||||
"settings.keybinds",
|
||||
JSON.stringify({
|
||||
buildCity: "Digit1",
|
||||
buildFactory: "Numpad1",
|
||||
}),
|
||||
);
|
||||
inputHandler.destroy();
|
||||
const uiState: UIState = {
|
||||
attackRatio: 20,
|
||||
ghostStructure: null,
|
||||
rocketDirectionUp: true,
|
||||
overlappingRailroads: [],
|
||||
ghostRailPaths: [],
|
||||
} as UIState;
|
||||
inputHandler = new InputHandler(uiState, mockCanvas, eventBus);
|
||||
inputHandler.initialize();
|
||||
|
||||
window.dispatchEvent(
|
||||
new KeyboardEvent("keyup", { code: "Digit1", key: "1" }),
|
||||
);
|
||||
|
||||
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.City);
|
||||
});
|
||||
|
||||
test("exact code match wins: Numpad1 sets Factory when buildCity=Digit1 and buildFactory=Numpad1", () => {
|
||||
localStorage.setItem(
|
||||
"settings.keybinds",
|
||||
JSON.stringify({
|
||||
buildCity: "Digit1",
|
||||
buildFactory: "Numpad1",
|
||||
}),
|
||||
);
|
||||
inputHandler.destroy();
|
||||
const uiState: UIState = {
|
||||
attackRatio: 20,
|
||||
ghostStructure: null,
|
||||
rocketDirectionUp: true,
|
||||
overlappingRailroads: [],
|
||||
ghostRailPaths: [],
|
||||
} as UIState;
|
||||
inputHandler = new InputHandler(uiState, mockCanvas, eventBus);
|
||||
inputHandler.initialize();
|
||||
|
||||
window.dispatchEvent(
|
||||
new KeyboardEvent("keyup", { code: "Numpad1", key: "1" }),
|
||||
);
|
||||
|
||||
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.Factory);
|
||||
});
|
||||
|
||||
test("digit alias used when no exact match: Numpad1 sets City when only buildCity=Digit1", () => {
|
||||
localStorage.setItem(
|
||||
"settings.keybinds",
|
||||
JSON.stringify({ buildCity: "Digit1" }),
|
||||
);
|
||||
inputHandler.destroy();
|
||||
const uiState: UIState = {
|
||||
attackRatio: 20,
|
||||
ghostStructure: null,
|
||||
rocketDirectionUp: true,
|
||||
overlappingRailroads: [],
|
||||
ghostRailPaths: [],
|
||||
} as UIState;
|
||||
inputHandler = new InputHandler(uiState, mockCanvas, eventBus);
|
||||
inputHandler.initialize();
|
||||
|
||||
window.dispatchEvent(
|
||||
new KeyboardEvent("keyup", { code: "Numpad1", key: "1" }),
|
||||
);
|
||||
|
||||
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.City);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { shouldPreserveGhostAfterBuild } from "../../../../src/client/graphics/layers/StructureIconsLayer";
|
||||
import { UnitType } from "../../../../src/core/game/Game";
|
||||
|
||||
/**
|
||||
* Tests for StructureIconsLayer edge cases mentioned in comments:
|
||||
* - Locked nuke / AtomBomb / HydrogenBomb: when confirming placement (Enter or key),
|
||||
* the ghost is preserved so the user can place multiple nukes or keep the nuke
|
||||
* selected. Other structure types clear the ghost after placement.
|
||||
*/
|
||||
describe("StructureIconsLayer ghost preservation (locked nuke / Enter confirm)", () => {
|
||||
describe("shouldPreserveGhostAfterBuild", () => {
|
||||
test("returns true for AtomBomb so ghost is not cleared after placement", () => {
|
||||
expect(shouldPreserveGhostAfterBuild(UnitType.AtomBomb)).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true for HydrogenBomb so ghost is not cleared after placement", () => {
|
||||
expect(shouldPreserveGhostAfterBuild(UnitType.HydrogenBomb)).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false for City so ghost is cleared after placement", () => {
|
||||
expect(shouldPreserveGhostAfterBuild(UnitType.City)).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false for Factory so ghost is cleared after placement", () => {
|
||||
expect(shouldPreserveGhostAfterBuild(UnitType.Factory)).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false for other buildable types (Port, DefensePost, MissileSilo, SAMLauncher, Warship, MIRV)", () => {
|
||||
expect(shouldPreserveGhostAfterBuild(UnitType.Port)).toBe(false);
|
||||
expect(shouldPreserveGhostAfterBuild(UnitType.DefensePost)).toBe(false);
|
||||
expect(shouldPreserveGhostAfterBuild(UnitType.MissileSilo)).toBe(false);
|
||||
expect(shouldPreserveGhostAfterBuild(UnitType.SAMLauncher)).toBe(false);
|
||||
expect(shouldPreserveGhostAfterBuild(UnitType.Warship)).toBe(false);
|
||||
expect(shouldPreserveGhostAfterBuild(UnitType.MIRV)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user