feat(ping): implement ping communication and visual enhancements

This commit implements the full communication loop for the ping system, including:

-   **Schema Definitions:** Added  and  schemas.
-   **Client-Side Sending:** Configured  to emit , which  now intercepts and uses to send  to the server.
-   **Server-Side Execution:** Implemented  to process , generating  (for chat) and  (for visual pings) for friendly players.
-   **Client-Side Receiving:**  handles  by emitting  for local display.
-   **Visual Enhancements:** Updated the ping animation with a glow effect, adjusted duration to 6 seconds, and increased maximum radius to 48 for a more prominent visual.
-   **Test:** Added a unit test for  to verify server-side logic.
This commit is contained in:
Restart2008
2025-11-24 19:43:24 -08:00
parent 60e08dc94b
commit 25af663730
9 changed files with 65 additions and 25 deletions
+8 -1
View File
@@ -30,6 +30,8 @@ import {
AutoUpgradeEvent,
DoBoatAttackEvent,
DoGroundAttackEvent,
GoToPlayerEvent,
GoToPositionEvent,
InputHandler,
MouseMoveEvent,
MouseUpEvent,
@@ -49,7 +51,6 @@ import {
} from "./Transport";
import { createCanvas } from "./Utils";
import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
import { GoToPlayerEvent } from "./graphics/layers/Leaderboard";
import SoundManager from "./sound/SoundManager";
export interface LobbyConfig {
@@ -224,6 +225,12 @@ export class ClientGameRunner {
private gameView: GameView,
) {
this.lastMessageTime = Date.now();
this.eventBus.on(GoToPositionEvent, (e) => this.onGoToPosition(e));
}
private onGoToPosition(event: GoToPositionEvent) {
this.eventBus.emit(new GoToPositionEvent(event.x, event.y));
}
private saveGame(update: WinUpdate) {
+16 -1
View File
@@ -1,6 +1,6 @@
import { EventBus, GameEvent } from "../core/EventBus";
import { UnitType } from "../core/game/Game";
import { UnitView } from "../core/game/GameView";
import { PlayerView, UnitView } from "../core/game/GameView";
import { PingType } from "../core/game/Ping";
import { UserSettings } from "../core/game/UserSettings";
import { TransformHandler } from "./graphics/TransformHandler";
@@ -144,6 +144,21 @@ export class PingPlacedEvent implements GameEvent {
) {}
}
export class GoToPositionEvent implements GameEvent {
constructor(
public readonly x: number,
public readonly y: number,
) {}
}
export class GoToPlayerEvent implements GameEvent {
constructor(public player: PlayerView) {}
}
export class GoToUnitEvent implements GameEvent {
constructor(public unit: UnitView) {}
}
export class InputHandler {
private lastPointerX: number = 0;
private lastPointerY: number = 0;
+4 -2
View File
@@ -1,12 +1,14 @@
import { EventBus } from "../../core/EventBus";
import { Cell } from "../../core/game/Game";
import { GameView } from "../../core/game/GameView";
import { CenterCameraEvent, DragEvent, ZoomEvent } from "../InputHandler";
import {
CenterCameraEvent,
DragEvent,
GoToPlayerEvent,
GoToPositionEvent,
GoToUnitEvent,
} from "./layers/Leaderboard";
ZoomEvent,
} from "../InputHandler";
export const GOTO_INTERVAL_MS = 16;
export const CAMERA_MAX_SPEED = 15;
+19 -1
View File
@@ -10,6 +10,7 @@ import {
} from "../../../core/game/GameUpdates";
import { GameView } from "../../../core/game/GameView";
import { onlyImages } from "../../../core/Util";
import { GoToPositionEvent } from "../../InputHandler";
import { Layer } from "./Layer";
interface ChatEvent {
@@ -17,6 +18,8 @@ interface ChatEvent {
unsafeDescription?: boolean;
createdAt: number;
highlight?: boolean;
x?: number; // New optional field
y?: number; // New optional field
}
@customElement("chat-display")
@@ -68,6 +71,8 @@ export class ChatDisplay extends LitElement implements Layer {
createdAt: this.game.ticks(),
highlight: true,
unsafeDescription: true,
x: event.x, // Transfer coordinates
y: event.y, // Transfer coordinates
});
}
@@ -119,6 +124,7 @@ export class ChatDisplay extends LitElement implements Layer {
: chat.description;
}
// ...
render() {
if (!this.active) {
return html``;
@@ -170,7 +176,19 @@ export class ChatDisplay extends LitElement implements Layer {
(chat) => html`
<tr class="border-b border-opacity-0">
<td class="lg:p-3 p-1 text-left">
${this.getChatContent(chat)}
${chat.x !== undefined && chat.y !== undefined
? html`
<div
class="cursor-pointer text-blue-400 hover:underline"
@click=${() =>
this.eventBus.emit(
new GoToPositionEvent(chat.x!, chat.y!),
)}
>
${this.getChatContent(chat)}
</div>
`
: this.getChatContent(chat)}
</td>
</tr>
`,
+2 -2
View File
@@ -40,12 +40,12 @@ import { Layer } from "./Layer";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { onlyImages } from "../../../core/Util";
import { renderNumber, renderTroops } from "../../Utils";
import {
GoToPlayerEvent,
GoToPositionEvent,
GoToUnitEvent,
} from "./Leaderboard";
} from "../../InputHandler";
import { renderNumber, renderTroops } from "../../Utils";
import { getMessageTypeClasses, translateText } from "../../Utils";
+3 -17
View File
@@ -2,8 +2,9 @@ import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { repeat } from "lit/directives/repeat.js";
import { translateText } from "../../../client/Utils";
import { EventBus, GameEvent } from "../../../core/EventBus";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { EventBus } from "../../../core/EventBus";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { GoToPlayerEvent } from "../../InputHandler";
import { renderNumber } from "../../Utils";
import { Layer } from "./Layer";
@@ -18,21 +19,6 @@ interface Entry {
player: PlayerView;
}
export class GoToPlayerEvent implements GameEvent {
constructor(public player: PlayerView) {}
}
export class GoToPositionEvent implements GameEvent {
constructor(
public x: number,
public y: number,
) {}
}
export class GoToUnitEvent implements GameEvent {
constructor(public unit: UnitView) {}
}
@customElement("leader-board")
export class Leaderboard extends LitElement implements Layer {
public game: GameView | null = null;
+9 -1
View File
@@ -18,7 +18,15 @@ export class PingExecution implements Execution {
for (const recipient of recipients) {
// Create chat message
const message = `${this.sender.name()} pinged ${this.pingType}`;
game.displayMessage(message, MessageType.CHAT, recipient.id());
game.displayMessage(
message,
MessageType.CHAT,
recipient.id(),
undefined,
undefined,
this.x,
this.y,
);
// Create visual ping update
game.addUpdate({
+2
View File
@@ -718,6 +718,8 @@ export interface Game extends GameMap {
playerID: PlayerID | null,
goldAmount?: bigint,
params?: Record<string, string | number>,
x?: number, // New optional field
y?: number, // New optional field
): void;
displayIncomingUnit(
unitID: number,
+2
View File
@@ -233,6 +233,8 @@ export interface DisplayMessageUpdate {
goldAmount?: bigint;
playerID: number | null;
params?: Record<string, string | number>;
x?: number; // New optional field
y?: number; // New optional field
}
export type DisplayChatMessageUpdate = {