mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 03:34:03 +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:
@@ -33,6 +33,7 @@ import {
|
||||
InputHandler,
|
||||
MouseMoveEvent,
|
||||
MouseUpEvent,
|
||||
PingPlacedEvent,
|
||||
TickMetricsEvent,
|
||||
} from "./InputHandler";
|
||||
import { endGame, startGame, startTime } from "./LocalPersistantStats";
|
||||
@@ -301,6 +302,11 @@ export class ClientGameRunner {
|
||||
gu.updates[GameUpdateType.Hash].forEach((hu: HashUpdate) => {
|
||||
this.eventBus.emit(new SendHashEvent(hu.tick, hu.hash));
|
||||
});
|
||||
gu.updates[GameUpdateType.PingPlaced].forEach((ppu) => {
|
||||
if (this.gameView.myPlayer()?.smallID() === ppu.playerID) {
|
||||
this.eventBus.emit(new PingPlacedEvent(ppu.pingType, ppu.x, ppu.y));
|
||||
}
|
||||
});
|
||||
this.gameView.update(gu);
|
||||
this.renderer.tick();
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import defendIconUrl from "../../../../resources/images/ShieldIconWhite.svg";
|
||||
import attackIconUrl from "../../../../resources/images/SwordIconWhite.svg";
|
||||
|
||||
// Configuration for pings
|
||||
const PING_DURATION_MS = 3000; // 3 seconds
|
||||
const PING_DURATION_MS = 6000; // 6 seconds
|
||||
const PING_COLORS: Record<PingType, Colord> = {
|
||||
attack: colord("#ff0000"),
|
||||
retreat: colord("#ffa600"),
|
||||
@@ -23,7 +23,7 @@ const PING_COLORS: Record<PingType, Colord> = {
|
||||
watchOut: colord("#ffff00"),
|
||||
};
|
||||
const PING_RING_MIN_RADIUS = 8;
|
||||
const PING_RING_MAX_RADIUS = 32;
|
||||
const PING_RING_MAX_RADIUS = 48;
|
||||
|
||||
// The core class for a single ping marker, handles its own animation and rendering
|
||||
class Ping {
|
||||
@@ -60,22 +60,22 @@ class Ping {
|
||||
return false;
|
||||
}
|
||||
|
||||
const progress = elapsedTime / PING_DURATION_MS;
|
||||
const progress = elapsedTime / PING_DURATION_MS; // Overall fade progress
|
||||
const overallFadeAlpha = 1 - progress; // Overall fade alpha for sprite
|
||||
|
||||
this.sprite.alpha = 1 - progress; // Fade out
|
||||
|
||||
// Breathing ring animation
|
||||
const ringRadius =
|
||||
const pulseProgress = 0.5 + 0.5 * Math.sin(elapsedTime / 200); // Sinusoidal pulse for size and opacity
|
||||
const currentRadius =
|
||||
PING_RING_MIN_RADIUS +
|
||||
(PING_RING_MAX_RADIUS - PING_RING_MIN_RADIUS) *
|
||||
(0.5 + 0.5 * Math.sin(elapsedTime / 200));
|
||||
(PING_RING_MAX_RADIUS - PING_RING_MIN_RADIUS) * pulseProgress;
|
||||
|
||||
this.drawBreathingRing(
|
||||
PING_RING_MIN_RADIUS,
|
||||
PING_RING_MAX_RADIUS,
|
||||
ringRadius,
|
||||
currentRadius,
|
||||
this.color.alpha(0.4), // Static outer ring
|
||||
this.color.alpha(0.8), // Pulsing inner ring
|
||||
pulseProgress, // Pass pulseProgress
|
||||
overallFadeAlpha, // Pass overallFadeAlpha
|
||||
);
|
||||
|
||||
return true;
|
||||
@@ -88,21 +88,37 @@ class Ping {
|
||||
currentRadius: number,
|
||||
staticColor: Colord,
|
||||
pulseColor: Colord,
|
||||
pulseProgress: number, // New parameter for opacity pulse
|
||||
overallFadeAlpha: number, // New parameter for overall fade
|
||||
) {
|
||||
this.circle.clear();
|
||||
|
||||
const progress = (currentRadius - minRad) / (maxRad - minRad);
|
||||
const alpha = 1 - progress;
|
||||
const dramaticPulse = pulseProgress * pulseProgress;
|
||||
|
||||
// --- Glow Simulation ---
|
||||
const glowSteps = 3;
|
||||
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();
|
||||
}
|
||||
|
||||
// --- Main Rings (as before) ---
|
||||
// Outer static ring
|
||||
this.circle.stroke({ width: 2, color: staticColor.toRgb(), alpha: 0.4 });
|
||||
this.circle.stroke({
|
||||
width: 3,
|
||||
color: staticColor.toRgb(),
|
||||
alpha: 0.5 * dramaticPulse,
|
||||
});
|
||||
this.circle.circle(0, 0, maxRad);
|
||||
|
||||
// Inner pulsing ring
|
||||
this.circle.stroke({
|
||||
width: 4,
|
||||
width: 6,
|
||||
color: pulseColor.toRgb(),
|
||||
alpha: alpha * 0.8,
|
||||
alpha: overallFadeAlpha * dramaticPulse,
|
||||
});
|
||||
this.circle.circle(0, 0, currentRadius);
|
||||
}
|
||||
|
||||
+11
-1
@@ -47,7 +47,8 @@ export type Intent =
|
||||
| EmbargoAllIntent
|
||||
| UpgradeStructureIntent
|
||||
| DeleteUnitIntent
|
||||
| KickPlayerIntent;
|
||||
| KickPlayerIntent
|
||||
| PingIntent;
|
||||
|
||||
export type AttackIntent = z.infer<typeof AttackIntentSchema>;
|
||||
export type CancelAttackIntent = z.infer<typeof CancelAttackIntentSchema>;
|
||||
@@ -79,6 +80,7 @@ export type AllianceExtensionIntent = z.infer<
|
||||
>;
|
||||
export type DeleteUnitIntent = z.infer<typeof DeleteUnitIntentSchema>;
|
||||
export type KickPlayerIntent = z.infer<typeof KickPlayerIntentSchema>;
|
||||
export type PingIntent = z.infer<typeof PingIntentSchema>;
|
||||
|
||||
export type Turn = z.infer<typeof TurnSchema>;
|
||||
export type GameConfig = z.infer<typeof GameConfigSchema>;
|
||||
@@ -348,6 +350,13 @@ export const KickPlayerIntentSchema = BaseIntentSchema.extend({
|
||||
target: ID,
|
||||
});
|
||||
|
||||
export const PingIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("ping"),
|
||||
pingType: z.enum(["attack", "retreat", "defend", "watchOut"]),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
});
|
||||
|
||||
const IntentSchema = z.discriminatedUnion("type", [
|
||||
AttackIntentSchema,
|
||||
CancelAttackIntentSchema,
|
||||
@@ -371,6 +380,7 @@ const IntentSchema = z.discriminatedUnion("type", [
|
||||
AllianceExtensionIntentSchema,
|
||||
DeleteUnitIntentSchema,
|
||||
KickPlayerIntentSchema,
|
||||
PingIntentSchema,
|
||||
]);
|
||||
|
||||
//
|
||||
|
||||
@@ -20,6 +20,7 @@ import { FakeHumanExecution } from "./FakeHumanExecution";
|
||||
import { MarkDisconnectedExecution } from "./MarkDisconnectedExecution";
|
||||
import { MoveWarshipExecution } from "./MoveWarshipExecution";
|
||||
import { NoOpExecution } from "./NoOpExecution";
|
||||
import { PingExecution } from "./PingExecution";
|
||||
import { QuickChatExecution } from "./QuickChatExecution";
|
||||
import { RetreatExecution } from "./RetreatExecution";
|
||||
import { SpawnExecution } from "./SpawnExecution";
|
||||
@@ -109,7 +110,6 @@ export class Executor {
|
||||
case "allianceExtension": {
|
||||
return new AllianceExtensionExecution(player, intent.recipient);
|
||||
}
|
||||
|
||||
case "upgrade_structure":
|
||||
return new UpgradeStructureExecution(player, intent.unitId);
|
||||
case "delete_unit":
|
||||
@@ -123,6 +123,8 @@ export class Executor {
|
||||
);
|
||||
case "mark_disconnected":
|
||||
return new MarkDisconnectedExecution(player, intent.isDisconnected);
|
||||
case "ping":
|
||||
return new PingExecution(player, intent.pingType, intent.x, intent.y);
|
||||
default:
|
||||
throw new Error(`intent type ${intent} not found`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Execution, Game, MessageType, Player } from "../game/Game";
|
||||
import { GameUpdateType, PingPlacedUpdate } from "../game/GameUpdates";
|
||||
import { PingType } from "../game/Ping";
|
||||
|
||||
export class PingExecution implements Execution {
|
||||
constructor(
|
||||
private sender: Player,
|
||||
private pingType: PingType,
|
||||
private x: number,
|
||||
private y: number,
|
||||
) {}
|
||||
|
||||
init(game: Game): void {
|
||||
const recipients = game
|
||||
.players()
|
||||
.filter((p) => p.isFriendly(this.sender, true));
|
||||
|
||||
for (const recipient of recipients) {
|
||||
// Create chat message
|
||||
const message = `${this.sender.name()} pinged ${this.pingType}`;
|
||||
game.displayMessage(message, MessageType.CHAT, recipient.id());
|
||||
|
||||
// Create visual ping update
|
||||
game.addUpdate({
|
||||
type: GameUpdateType.PingPlaced,
|
||||
playerID: recipient.smallID(),
|
||||
senderID: this.sender.smallID(),
|
||||
pingType: this.pingType,
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
} as PingPlacedUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
// Pings are instantaneous, no need for tick logic
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return false; // It's an instantaneous event
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return true; // Pings can be used anytime
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ export enum GameUpdateType {
|
||||
RailroadEvent,
|
||||
ConquestEvent,
|
||||
EmbargoEvent,
|
||||
PingPlaced,
|
||||
}
|
||||
|
||||
export type GameUpdate =
|
||||
@@ -68,7 +69,8 @@ export type GameUpdate =
|
||||
| BonusEventUpdate
|
||||
| RailroadUpdate
|
||||
| ConquestUpdate
|
||||
| EmbargoUpdate;
|
||||
| EmbargoUpdate
|
||||
| PingPlacedUpdate;
|
||||
|
||||
export interface BonusEventUpdate {
|
||||
type: GameUpdateType.BonusEvent;
|
||||
@@ -263,9 +265,21 @@ export interface UnitIncomingUpdate {
|
||||
playerID: number;
|
||||
}
|
||||
|
||||
import { PingType } from "./Ping";
|
||||
//...
|
||||
//...
|
||||
export interface EmbargoUpdate {
|
||||
type: GameUpdateType.EmbargoEvent;
|
||||
event: "start" | "stop";
|
||||
playerID: number;
|
||||
embargoedID: number;
|
||||
}
|
||||
|
||||
export interface PingPlacedUpdate {
|
||||
type: GameUpdateType.PingPlaced;
|
||||
playerID: number;
|
||||
senderID: number;
|
||||
pingType: PingType;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user