mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 14:23:30 +00:00
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:
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user