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 22:35:56 -08:00
parent 25af663730
commit cb68b26915
8 changed files with 54 additions and 34 deletions
+4 -6
View File
@@ -225,12 +225,10 @@ 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));
// If forwarding is needed, emit a different event or handle directly
this.eventBus.on(GoToPositionEvent, (e) => {
// Handle position navigation here instead of re-emitting
});
}
private saveGame(update: WinUpdate) {
+1 -1
View File
@@ -428,7 +428,7 @@ export class GameRenderer {
handleTransformState(false, isTransformActive); // Ensure context is clean after rendering
this.transformHandler.resetChanged();
requestAnimationFrame(() => this.renderGame());
this.rafId = requestAnimationFrame(() => this.renderGame());
const duration = performance.now() - start;
const layerDurations = FrameProfiler.consume();
+6 -4
View File
@@ -103,6 +103,8 @@ export class ChatDisplay extends LitElement implements Layer {
description: msg.message,
unsafeDescription: true,
createdAt: this.game.ticks(),
x: msg.x, // Transfer coordinates
y: msg.y, // Transfer coordinates
},
];
}
@@ -124,7 +126,6 @@ export class ChatDisplay extends LitElement implements Layer {
: chat.description;
}
// ...
render() {
if (!this.active) {
return html``;
@@ -178,15 +179,16 @@ export class ChatDisplay extends LitElement implements Layer {
<td class="lg:p-3 p-1 text-left">
${chat.x !== undefined && chat.y !== undefined
? html`
<div
class="cursor-pointer text-blue-400 hover:underline"
<button
type="button"
class="cursor-pointer text-blue-400 hover:underline focus:outline-none focus:ring-2 focus:ring-blue-500 bg-transparent border-0 p-0 text-left"
@click=${() =>
this.eventBus.emit(
new GoToPositionEvent(chat.x!, chat.y!),
)}
>
${this.getChatContent(chat)}
</div>
</button>
`
: this.getChatContent(chat)}
</td>
+11 -4
View File
@@ -350,12 +350,19 @@ export class FxLayer implements Layer {
this.allFx.push(shockwave);
}
private pingEventCleanup?: () => void;
destroy() {
if (this.pingEventCleanup) {
this.pingEventCleanup();
this.pingEventCleanup = undefined;
// End all active effects
for (const fx of this.boatTargetFxByUnitId.values()) {
(fx as any).end?.();
}
for (const fx of this.nukeTargetFxByUnitId.values()) {
fx.end();
}
// Clear collections
this.allFx = [];
this.boatTargetFxByUnitId.clear();
this.nukeTargetFxByUnitId.clear();
}
async init() {
this.redraw();
+11 -8
View File
@@ -46,7 +46,7 @@ class Ping {
this.sprite = new PIXI.Sprite(texture);
this.sprite.anchor.set(0.5);
const aspectRatio = texture.width / texture.height;
const aspectRatio = texture.height > 0 ? texture.width / texture.height : 1;
this.sprite.height = 24;
this.sprite.width = 24 * aspectRatio;
@@ -100,27 +100,26 @@ class Ping {
for (let i = 0; i < glowSteps; i++) {
const glowRadius = maxRad + i * 8; // Circles outside the main ring
const glowAlpha = 0.1 * dramaticPulse * (1 - i / glowSteps); // Fades out with distance
this.circle.beginFill(staticColor.toRgb(), glowAlpha);
this.circle.drawCircle(0, 0, glowRadius);
this.circle.endFill();
this.circle.circle(0, 0, glowRadius).fill({
color: staticColor.toRgb(),
alpha: glowAlpha,
});
}
// --- Main Rings (as before) ---
// Outer static ring
this.circle.stroke({
this.circle.circle(0, 0, maxRad).stroke({
width: 3,
color: staticColor.toRgb(),
alpha: 0.5 * dramaticPulse,
});
this.circle.circle(0, 0, maxRad);
// Inner pulsing ring
this.circle.stroke({
this.circle.circle(0, 0, currentRadius).stroke({
width: 6,
color: pulseColor.toRgb(),
alpha: overallFadeAlpha * dramaticPulse,
});
this.circle.circle(0, 0, currentRadius);
}
destroy() {
@@ -172,6 +171,10 @@ export class PingMarkerLayer implements Layer {
destroy() {
this.eventBus.off(PingPlacedEvent, this.handlePingPlaced);
for (const ping of this.pings) {
ping.destroy();
}
this.pings = [];
window.removeEventListener("resize", this.resizeCanvas);
this.renderer?.destroy();
this.stage.destroy(true);
+20 -6
View File
@@ -5,8 +5,11 @@ export interface EventConstructor<T extends GameEvent = GameEvent> {
}
export class EventBus {
private listeners: Map<EventConstructor, Array<(event: GameEvent) => void>> =
new Map();
private listeners: Map<
EventConstructor,
Array<{ id: number; callback: (event: GameEvent) => void }>
> = new Map();
private nextId = 0;
emit<T extends GameEvent>(event: T): void {
const eventConstructor = event.constructor as EventConstructor<T>;
@@ -14,7 +17,7 @@ export class EventBus {
const callbacks = this.listeners.get(eventConstructor);
if (callbacks) {
for (const callback of callbacks) {
callback(event);
callback.callback(event);
}
}
}
@@ -26,9 +29,20 @@ export class EventBus {
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, []);
}
this.listeners.get(eventType)!.push(callback as (event: GameEvent) => void);
const id = this.nextId++;
this.listeners
.get(eventType)!
.push({ id, callback: callback as (event: GameEvent) => void });
return () => this.off(eventType, callback);
return () => {
const callbacks = this.listeners.get(eventType);
if (callbacks) {
const index = callbacks.findIndex((item) => item.id === id);
if (index > -1) {
callbacks.splice(index, 1);
}
}
};
}
off<T extends GameEvent>(
@@ -37,7 +51,7 @@ export class EventBus {
): void {
const callbacks = this.listeners.get(eventType);
if (callbacks) {
const index = callbacks.indexOf(callback as (event: GameEvent) => void);
const index = callbacks.findIndex((item) => item.callback === callback);
if (index > -1) {
callbacks.splice(index, 1);
}
-1
View File
@@ -413,7 +413,6 @@ export class PlayerInfo {
public readonly clientID: ClientID | null,
// TODO: make player id the small id
public readonly id: PlayerID,
public readonly nation?: Nation | null,
) {
this.clan = getClanTag(name);
}
+1 -4
View File
@@ -5,6 +5,7 @@ import {
Gold,
MessageType,
NameViewData,
PingType,
PlayerID,
PlayerType,
Team,
@@ -266,10 +267,6 @@ export interface UnitIncomingUpdate {
messageType: MessageType;
playerID: number;
}
import { PingType } from "./Ping";
//...
//...
export interface EmbargoUpdate {
type: GameUpdateType.EmbargoEvent;
event: "start" | "stop";