Added warship selection mechanic

This commit is contained in:
Bruno Jurković
2025-02-28 20:52:38 +01:00
parent db55f4facc
commit c3dfcc63a5
3 changed files with 360 additions and 2 deletions
+1 -1
View File
@@ -117,7 +117,7 @@ export function createRenderer(
new TerrainLayer(game),
new TerritoryLayer(game, eventBus),
new StructureLayer(game, eventBus),
new UnitLayer(game, eventBus, clientID),
new UnitLayer(game, eventBus, clientID, transformHandler),
new NameLayer(game, transformHandler, clientID),
eventsDisplay,
buildMenu,
+94
View File
@@ -0,0 +1,94 @@
import { EventBus, GameEvent } from "../../core/EventBus";
import { UnitType } from "../../core/game/Game";
import { UnitView } from "../../core/game/GameView";
/**
* Event emitted when a unit is selected or deselected
*/
export class UnitSelectionEvent implements GameEvent {
constructor(
public readonly unit: UnitView | null,
public readonly isSelected: boolean,
) {}
}
/**
* Manages the currently selected units in the game
*/
export class SelectedUnits {
private selectedUnit: UnitView | null = null;
constructor(private eventBus: EventBus) {}
/**
* Select a unit. Deselects any previously selected unit.
* @param unit The unit to select
* @returns true if the selection changed, false otherwise
*/
selectUnit(unit: UnitView): boolean {
if (this.selectedUnit === unit) {
return false;
}
if (this.selectedUnit) {
this.deselectCurrentUnit();
}
this.selectedUnit = unit;
this.eventBus.emit(new UnitSelectionEvent(unit, true));
return true;
}
/**
* Deselect the currently selected unit, if any
* @returns true if a unit was deselected, false otherwise
*/
deselectCurrentUnit(): boolean {
if (!this.selectedUnit) {
return false;
}
const unit = this.selectedUnit;
this.selectedUnit = null;
this.eventBus.emit(new UnitSelectionEvent(unit, false));
return true;
}
/**
* Toggle selection for the given unit
* @param unit The unit to toggle selection for
* @returns true if the unit is now selected, false if it was deselected
*/
toggleUnitSelection(unit: UnitView): boolean {
if (this.selectedUnit === unit) {
this.deselectCurrentUnit();
return false;
} else {
this.selectUnit(unit);
return true;
}
}
/**
* Get the currently selected unit, if any
*/
getSelectedUnit(): UnitView | null {
return this.selectedUnit;
}
/**
* Check if the given unit is currently selected
* @param unit The unit to check
*/
isSelected(unit: UnitView): boolean {
return this.selectedUnit === unit;
}
/**
* Check if a unit of the specified type is currently selected
* @param type The unit type to check for
*/
hasSelectedUnitOfType(type: UnitType): boolean {
return this.selectedUnit !== null && this.selectedUnit.type() === type;
}
}
+265 -1
View File
@@ -3,7 +3,7 @@ import { Theme } from "../../../core/configuration/Config";
import { Unit, UnitType, Player } from "../../../core/game/Game";
import { Layer } from "./Layer";
import { EventBus } from "../../../core/EventBus";
import { AlternateViewEvent } from "../../InputHandler";
import { AlternateViewEvent, MouseUpEvent } from "../../InputHandler";
import { ClientID } from "../../../core/Schemas";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import {
@@ -12,6 +12,8 @@ import {
TileRef,
} from "../../../core/game/GameMap";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { SelectedUnits, UnitSelectionEvent } from "../SelectedUnits";
import { TransformHandler } from "../TransformHandler";
enum Relationship {
Self,
@@ -33,12 +35,22 @@ export class UnitLayer implements Layer {
private oldShellTile = new Map<UnitView, TileRef>();
private selectedUnits: SelectedUnits;
private selectionAnimTime = 0;
private transformHandler: TransformHandler;
// Configuration for unit selection
private readonly WARSHIP_SELECTION_RADIUS = 3; // Radius in game cells for warship selection hit zone
constructor(
private game: GameView,
private eventBus: EventBus,
private clientID: ClientID,
transformHandler: TransformHandler,
) {
this.theme = game.config().theme();
this.selectedUnits = new SelectedUnits(eventBus);
this.transformHandler = transformHandler;
}
shouldTransform(): boolean {
@@ -52,13 +64,133 @@ export class UnitLayer implements Layer {
this.game.updatesSinceLastTick()?.[GameUpdateType.Unit]?.forEach((unit) => {
this.onUnitEvent(this.game.unit(unit.id));
});
// Update the selection animation time
this.selectionAnimTime = (this.selectionAnimTime + 1) % 60;
// If there's a selected warship, redraw to update the selection box animation
if (this.selectedUnits.hasSelectedUnitOfType(UnitType.Warship)) {
this.redrawSelectedUnit(this.selectedUnits.getSelectedUnit());
}
}
init() {
this.eventBus.on(AlternateViewEvent, (e) => this.onAlternativeViewEvent(e));
this.eventBus.on(MouseUpEvent, (e) => this.onMouseUp(e));
this.eventBus.on(UnitSelectionEvent, (e) => this.onUnitSelection(e));
this.redraw();
}
/**
* Find warships near the given cell within a configurable radius
* @param cell The cell to check
* @returns Array of warships in range, sorted by distance (closest first)
*/
private findWarshipsNearCell(cell: { x: number; y: number }): UnitView[] {
const clickRef = this.game.ref(cell.x, cell.y);
return this.game
.units(UnitType.Warship)
.filter(
(unit) =>
unit.isActive() &&
this.game.manhattanDist(unit.tile(), clickRef) <=
this.WARSHIP_SELECTION_RADIUS,
)
.sort((a, b) => {
// Sort by distance (closest first)
const distA = this.game.manhattanDist(a.tile(), clickRef);
const distB = this.game.manhattanDist(b.tile(), clickRef);
return distA - distB;
});
}
private onMouseUp(event: MouseUpEvent) {
// Convert screen coordinates to world coordinates
const cell = this.transformHandler.screenToWorldCoordinates(
event.x,
event.y,
);
// Find warships near this cell, sorted by distance
const nearbyWarships = this.findWarshipsNearCell(cell);
if (nearbyWarships.length > 0) {
// Select/deselect the closest warship
this.selectedUnits.toggleUnitSelection(nearbyWarships[0]);
} else if (this.selectedUnits.getSelectedUnit()) {
// If clicked elsewhere and there's a selection, deselect it
this.selectedUnits.deselectCurrentUnit();
}
}
private onUnitSelection(event: UnitSelectionEvent) {
if (event.unit && event.unit.type() === UnitType.Warship) {
if (event.isSelected) {
// Highlight the selected warship
this.redrawSelectedUnit(event.unit);
} else {
// Remove the highlight
this.onUnitEvent(event.unit);
// Also clear any lingering selection box
if (this.lastSelectionBoxCenter) {
const { x, y, size } = this.lastSelectionBoxCenter;
for (let px = x - size; px <= x + size; px++) {
for (let py = y - size; py <= y + size; py++) {
if (
px === x - size ||
px === x + size ||
py === y - size ||
py === y + size
) {
this.clearCell(px, py);
}
}
}
this.lastSelectionBoxCenter = null;
}
}
}
}
/**
* Handle unit deactivation or destruction
* If the selected unit is removed from the game, deselect it
*/
private handleUnitDeactivation(unit: UnitView) {
if (this.selectedUnits.isSelected(unit) && !unit.isActive()) {
// Clear the selection box before deselecting
if (
this.lastSelectionBoxCenter &&
this.lastSelectionBoxCenter.unit === unit
) {
const { x, y, size } = this.lastSelectionBoxCenter;
for (let px = x - size; px <= x + size; px++) {
for (let py = y - size; py <= y + size; py++) {
if (
px === x - size ||
px === x + size ||
py === y - size ||
py === y + size
) {
this.clearCell(px, py);
}
}
}
this.lastSelectionBoxCenter = null;
}
this.selectedUnits.deselectCurrentUnit();
}
}
private redrawSelectedUnit(unit: UnitView) {
if (unit && unit.type() === UnitType.Warship && unit.isActive()) {
this.onUnitEvent(unit);
}
}
renderLayer(context: CanvasRenderingContext2D) {
context.drawImage(
this.canvas,
@@ -101,6 +233,11 @@ export class UnitLayer implements Layer {
}
onUnitEvent(unit: UnitView) {
// Check if unit was deactivated
if (!unit.isActive()) {
this.handleUnitDeactivation(unit);
}
switch (unit.type()) {
case UnitType.TransportShip:
this.handleBoatEvent(unit);
@@ -175,6 +312,133 @@ export class UnitLayer implements Layer {
255,
);
}
// If this is a selected warship, draw the selection box
if (this.selectedUnits.isSelected(unit)) {
this.drawSelectionBox(unit);
}
}
// Keep track of previous selection box positions for cleanup
private lastSelectionBoxCenter: {
unit: UnitView;
x: number;
y: number;
size: number;
} | null = null;
// Visual settings for selection
private readonly SELECTION_BOX_SIZE = 6; // Size of the selection box (should be larger than the warship)
/**
* Draw a selection box around the warship
*/
private drawSelectionBox(unit: UnitView) {
// Use the configured selection box size
const selectionSize = this.SELECTION_BOX_SIZE;
// Calculate pulsating effect based on animation time (25% variation in opacity)
const baseOpacity = 200;
const pulseAmount = 55;
const opacity =
baseOpacity + Math.sin(this.selectionAnimTime * 0.1) * pulseAmount;
// Get the warship's owner color for the box
const ownerColor = this.theme.territoryColor(unit.owner().info());
// Create a brighter version of the owner color for the selection
const selectionColor = ownerColor.lighten(0.2);
// Get current center position
const center = unit.tile();
const centerX = this.game.x(center);
const centerY = this.game.y(center);
// Clear previous selection box if it exists and is different from current position
if (
this.lastSelectionBoxCenter &&
(this.lastSelectionBoxCenter.x !== centerX ||
this.lastSelectionBoxCenter.y !== centerY ||
this.lastSelectionBoxCenter.unit !== unit)
) {
const lastSize = this.lastSelectionBoxCenter.size;
const lastX = this.lastSelectionBoxCenter.x;
const lastY = this.lastSelectionBoxCenter.y;
// Clear the previous selection box
for (let x = lastX - lastSize; x <= lastX + lastSize; x++) {
for (let y = lastY - lastSize; y <= lastY + lastSize; y++) {
if (
x === lastX - lastSize ||
x === lastX + lastSize ||
y === lastY - lastSize ||
y === lastY + lastSize
) {
this.clearCell(x, y);
}
}
}
// Redraw the tiles at the previous location
for (const t of this.game.bfs(
this.lastSelectionBoxCenter.unit.lastTile(),
euclDistFN(this.lastSelectionBoxCenter.unit.lastTile(), 5),
)) {
const tileX = this.game.x(t);
const tileY = this.game.y(t);
// Only redraw if it's near the selection border
if (
Math.abs(tileX - lastX) <= lastSize + 1 &&
Math.abs(tileY - lastY) <= lastSize + 1
) {
if (this.game.hasOwner(t)) {
const owner = this.game.owner(t);
if (owner.isPlayer()) {
this.paintCell(
tileX,
tileY,
this.relationship(unit),
this.theme.territoryColor(owner.info()),
255,
);
}
}
}
}
}
// Draw the selection box
for (let x = centerX - selectionSize; x <= centerX + selectionSize; x++) {
for (let y = centerY - selectionSize; y <= centerY + selectionSize; y++) {
// Only draw if it's on the border (not inside or outside the box)
if (
x === centerX - selectionSize ||
x === centerX + selectionSize ||
y === centerY - selectionSize ||
y === centerY + selectionSize
) {
// Create a dashed effect by only drawing some pixels
const dashPattern = (x + y) % 2 === 0;
if (dashPattern) {
this.paintCell(
x,
y,
this.relationship(unit),
selectionColor,
opacity,
);
}
}
}
}
// Store current selection box position for next cleanup
this.lastSelectionBoxCenter = {
unit,
x: centerX,
y: centerY,
size: selectionSize,
};
}
private handleShellEvent(unit: UnitView) {