mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-23 08:10:28 +00:00
Add filters tabs to EvensDisplay to let users filter events (#1080)
## Description: Big update to the EventsDisplay - Style update for EventsDisplay, look & feel similar to other windows - Component now hidden during spawn phase - Adds new functionality for filtering events by category. Allows the player to remove specific event types - Displays latest gold amount, decays after 5 seconds <img width="1147" alt="Screenshot 2025-06-07 at 20 18 55" src="https://github.com/user-attachments/assets/11c39818-55ad-4ba1-a998-360057e2856c" /> <img width="422" alt="Screenshot 2025-06-07 at 19 01 55" src="https://github.com/user-attachments/assets/09c0b998-6046-49fb-9fba-33b4f57f337b" /> <img width="444" alt="Screenshot 2025-06-07 at 20 20 25" src="https://github.com/user-attachments/assets/022deadc-3a49-442a-85f5-f1cd128a5805" />  ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [X] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: maxion_ Fixes #1025 Fixes #1034
This commit is contained in:
@@ -402,6 +402,10 @@
|
||||
"health": "Health",
|
||||
"attitude": "Attitude"
|
||||
},
|
||||
"events_display": {
|
||||
"retreating": "retreating",
|
||||
"boat": "Boat"
|
||||
},
|
||||
"relation": {
|
||||
"hostile": "Hostile",
|
||||
"distrustful": "Distrustful",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MessageType } from "../core/game/Game";
|
||||
import { LangSelector } from "./LangSelector";
|
||||
|
||||
export function renderTroops(troops: number): string {
|
||||
@@ -95,3 +96,57 @@ export const translateText = (
|
||||
|
||||
return langSelector.translateText(key, params);
|
||||
};
|
||||
|
||||
/**
|
||||
* Severity colors mapping for message types
|
||||
*/
|
||||
export const severityColors: Record<string, string> = {
|
||||
fail: "text-red-400",
|
||||
warn: "text-yellow-400",
|
||||
success: "text-green-400",
|
||||
info: "text-gray-200",
|
||||
blue: "text-blue-400",
|
||||
white: "text-white",
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the CSS classes for styling message types based on their severity
|
||||
* @param type The message type to get styling for
|
||||
* @returns CSS class string for the message type
|
||||
*/
|
||||
export function getMessageTypeClasses(type: MessageType): string {
|
||||
switch (type) {
|
||||
case MessageType.SAM_HIT:
|
||||
case MessageType.CAPTURED_ENEMY_UNIT:
|
||||
case MessageType.RECEIVED_GOLD_FROM_TRADE:
|
||||
case MessageType.CONQUERED_PLAYER:
|
||||
return severityColors["success"];
|
||||
case MessageType.ATTACK_FAILED:
|
||||
case MessageType.ALLIANCE_REJECTED:
|
||||
case MessageType.ALLIANCE_BROKEN:
|
||||
case MessageType.UNIT_CAPTURED_BY_ENEMY:
|
||||
case MessageType.UNIT_DESTROYED:
|
||||
return severityColors["fail"];
|
||||
case MessageType.ATTACK_CANCELLED:
|
||||
case MessageType.ATTACK_REQUEST:
|
||||
case MessageType.ALLIANCE_ACCEPTED:
|
||||
case MessageType.SENT_GOLD_TO_PLAYER:
|
||||
case MessageType.SENT_TROOPS_TO_PLAYER:
|
||||
case MessageType.RECEIVED_GOLD_FROM_PLAYER:
|
||||
case MessageType.RECEIVED_TROOPS_FROM_PLAYER:
|
||||
return severityColors["blue"];
|
||||
case MessageType.MIRV_INBOUND:
|
||||
case MessageType.NUKE_INBOUND:
|
||||
case MessageType.HYDROGEN_BOMB_INBOUND:
|
||||
case MessageType.SAM_MISS:
|
||||
case MessageType.ALLIANCE_EXPIRED:
|
||||
case MessageType.NAVAL_INVASION_INBOUND:
|
||||
return severityColors["warn"];
|
||||
case MessageType.CHAT:
|
||||
case MessageType.ALLIANCE_REQUEST:
|
||||
return severityColors["info"];
|
||||
default:
|
||||
console.warn(`Message type ${type} has no explicit color`);
|
||||
return severityColors["white"];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,15 @@ import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { DirectiveResult } from "lit/directive.js";
|
||||
import { unsafeHTML, UnsafeHTMLDirective } from "lit/directives/unsafe-html.js";
|
||||
import allianceIcon from "../../../../resources/images/AllianceIconWhite.svg";
|
||||
import chatIcon from "../../../../resources/images/ChatIconWhite.svg";
|
||||
import donateGoldIcon from "../../../../resources/images/DonateGoldIconWhite.svg";
|
||||
import swordIcon from "../../../../resources/images/SwordIconWhite.svg";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import {
|
||||
AllPlayers,
|
||||
getMessageCategory,
|
||||
MessageCategory,
|
||||
MessageType,
|
||||
PlayerType,
|
||||
Tick,
|
||||
@@ -32,14 +38,14 @@ import { Layer } from "./Layer";
|
||||
|
||||
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
|
||||
import { onlyImages } from "../../../core/Util";
|
||||
import { renderTroops } from "../../Utils";
|
||||
import { renderNumber, renderTroops } from "../../Utils";
|
||||
import {
|
||||
GoToPlayerEvent,
|
||||
GoToPositionEvent,
|
||||
GoToUnitEvent,
|
||||
} from "./Leaderboard";
|
||||
|
||||
import { translateText } from "../../Utils";
|
||||
import { getMessageTypeClasses, translateText } from "../../Utils";
|
||||
|
||||
interface GameEvent {
|
||||
description: string;
|
||||
@@ -73,7 +79,50 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
@state() private outgoingLandAttacks: AttackUpdate[] = [];
|
||||
@state() private outgoingBoats: UnitView[] = [];
|
||||
@state() private _hidden: boolean = false;
|
||||
@state() private _isVisible: boolean = false;
|
||||
@state() private newEvents: number = 0;
|
||||
@state() private latestGoldAmount: bigint | null = null;
|
||||
@state() private goldAmountAnimating: boolean = false;
|
||||
private goldAmountTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
@state() private eventsFilters: Map<MessageCategory, boolean> = new Map([
|
||||
[MessageCategory.ATTACK, false],
|
||||
[MessageCategory.TRADE, false],
|
||||
[MessageCategory.ALLIANCE, false],
|
||||
[MessageCategory.CHAT, false],
|
||||
]);
|
||||
|
||||
private renderButton(options: {
|
||||
content: any; // Can be string, TemplateResult, or other renderable content
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
translate?: boolean;
|
||||
hidden?: boolean;
|
||||
}) {
|
||||
const {
|
||||
content,
|
||||
onClick,
|
||||
className = "",
|
||||
disabled = false,
|
||||
translate = true,
|
||||
hidden = false,
|
||||
} = options;
|
||||
|
||||
if (hidden) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<button
|
||||
class="${className}"
|
||||
@click=${onClick}
|
||||
?disabled=${disabled}
|
||||
?translate=${translate}
|
||||
>
|
||||
${content}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
private toggleHidden() {
|
||||
this._hidden = !this._hidden;
|
||||
@@ -83,6 +132,12 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private toggleEventFilter(filterName: MessageCategory) {
|
||||
const currentState = this.eventsFilters.get(filterName) || false;
|
||||
this.eventsFilters.set(filterName, !currentState);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private updateMap = new Map([
|
||||
[GameUpdateType.DisplayEvent, (u) => this.onDisplayMessageEvent(u)],
|
||||
[GameUpdateType.DisplayChatEvent, (u) => this.onDisplayChatEvent(u)],
|
||||
@@ -109,6 +164,21 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
|
||||
tick() {
|
||||
this.active = true;
|
||||
|
||||
if (!this._isVisible && !this.game.inSpawnPhase()) {
|
||||
this._isVisible = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (!myPlayer || !myPlayer.isAlive()) {
|
||||
if (this._isVisible) {
|
||||
this._isVisible = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
if (updates) {
|
||||
for (const [ut, fn] of this.updateMap) {
|
||||
@@ -134,11 +204,6 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (!myPlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update attacks
|
||||
this.incomingAttacks = myPlayer.incomingAttacks().filter((a) => {
|
||||
const t = (this.game.playerBySmallID(a.attackerID) as PlayerView).type();
|
||||
@@ -160,6 +225,13 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.goldAmountTimeoutId !== null) {
|
||||
clearTimeout(this.goldAmountTimeoutId);
|
||||
this.goldAmountTimeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private addEvent(event: GameEvent) {
|
||||
this.events = [...this.events, event];
|
||||
if (this._hidden === true) {
|
||||
@@ -190,6 +262,29 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.goldAmount !== undefined) {
|
||||
const hasChanged = this.latestGoldAmount !== event.goldAmount;
|
||||
this.latestGoldAmount = event.goldAmount;
|
||||
|
||||
if (this.goldAmountTimeoutId !== null) {
|
||||
clearTimeout(this.goldAmountTimeoutId);
|
||||
}
|
||||
|
||||
this.goldAmountTimeoutId = setTimeout(() => {
|
||||
this.latestGoldAmount = null;
|
||||
this.goldAmountTimeoutId = null;
|
||||
this.requestUpdate();
|
||||
}, 5000);
|
||||
|
||||
if (hasChanged) {
|
||||
this.goldAmountAnimating = true;
|
||||
setTimeout(() => {
|
||||
this.goldAmountAnimating = false;
|
||||
this.requestUpdate();
|
||||
}, 600);
|
||||
}
|
||||
}
|
||||
|
||||
this.addEvent({
|
||||
description: event.message,
|
||||
createdAt: this.game.ticks(),
|
||||
@@ -267,7 +362,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
},
|
||||
],
|
||||
highlight: true,
|
||||
type: MessageType.INFO,
|
||||
type: MessageType.ALLIANCE_REQUEST,
|
||||
createdAt: this.game.ticks(),
|
||||
onDelete: () =>
|
||||
this.eventBus.emit(
|
||||
@@ -293,7 +388,9 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
description: `${recipient.name()} ${
|
||||
update.accepted ? "accepted" : "rejected"
|
||||
} your alliance request`,
|
||||
type: update.accepted ? MessageType.SUCCESS : MessageType.ERROR,
|
||||
type: update.accepted
|
||||
? MessageType.ALLIANCE_ACCEPTED
|
||||
: MessageType.ALLIANCE_REJECTED,
|
||||
highlight: true,
|
||||
createdAt: this.game.ticks(),
|
||||
focusID: update.request.recipientID,
|
||||
@@ -322,7 +419,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
description:
|
||||
`You broke your alliance with ${betrayed.name()}, making you a TRAITOR ` +
|
||||
`(${malusPercent}% defense debuff for ${durationText})`,
|
||||
type: MessageType.ERROR,
|
||||
type: MessageType.ALLIANCE_BROKEN,
|
||||
highlight: true,
|
||||
createdAt: this.game.ticks(),
|
||||
focusID: update.betrayedID,
|
||||
@@ -330,7 +427,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
} else if (betrayed === myPlayer) {
|
||||
this.addEvent({
|
||||
description: `${traitor.name()} broke their alliance with you`,
|
||||
type: MessageType.ERROR,
|
||||
type: MessageType.ALLIANCE_BROKEN,
|
||||
highlight: true,
|
||||
createdAt: this.game.ticks(),
|
||||
focusID: update.traitorID,
|
||||
@@ -354,7 +451,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
|
||||
this.addEvent({
|
||||
description: `Your alliance with ${other.name()} expired`,
|
||||
type: MessageType.WARN,
|
||||
type: MessageType.ALLIANCE_EXPIRED,
|
||||
highlight: true,
|
||||
createdAt: this.game.ticks(),
|
||||
focusID: otherID,
|
||||
@@ -370,7 +467,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
|
||||
this.addEvent({
|
||||
description: `${other.name()} requests you attack ${target.name()}`,
|
||||
type: MessageType.INFO,
|
||||
type: MessageType.ATTACK_REQUEST,
|
||||
highlight: true,
|
||||
createdAt: this.game.ticks(),
|
||||
focusID: event.targetID,
|
||||
@@ -419,7 +516,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
this.addEvent({
|
||||
description: `${sender.displayName()}:${update.emoji.message}`,
|
||||
unsafeDescription: true,
|
||||
type: MessageType.INFO,
|
||||
type: MessageType.CHAT,
|
||||
highlight: true,
|
||||
createdAt: this.game.ticks(),
|
||||
focusID: update.emoji.senderID,
|
||||
@@ -430,7 +527,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
update.emoji.message
|
||||
}`,
|
||||
unsafeDescription: true,
|
||||
type: MessageType.INFO,
|
||||
type: MessageType.CHAT,
|
||||
highlight: true,
|
||||
createdAt: this.game.ticks(),
|
||||
focusID: recipient.smallID(),
|
||||
@@ -457,23 +554,6 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
});
|
||||
}
|
||||
|
||||
private getMessageTypeClasses(type: MessageType): string {
|
||||
switch (type) {
|
||||
case MessageType.SUCCESS:
|
||||
return "text-green-300";
|
||||
case MessageType.INFO:
|
||||
return "text-gray-200";
|
||||
case MessageType.CHAT:
|
||||
return "text-gray-200";
|
||||
case MessageType.WARN:
|
||||
return "text-yellow-300";
|
||||
case MessageType.ERROR:
|
||||
return "text-red-300";
|
||||
default:
|
||||
return "text-white";
|
||||
}
|
||||
}
|
||||
|
||||
private getEventDescription(
|
||||
event: GameEvent,
|
||||
): string | DirectiveResult<typeof UnsafeHTMLDirective> {
|
||||
@@ -506,27 +586,24 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
return html`
|
||||
${this.incomingAttacks.length > 0
|
||||
? html`
|
||||
<tr class="border-t border-gray-700">
|
||||
<td class="lg:p-3 p-1 text-left text-red-400">
|
||||
${this.incomingAttacks.map(
|
||||
(attack) => html`
|
||||
<button
|
||||
translate="no"
|
||||
class="ml-2"
|
||||
@click=${() => this.attackWarningOnClick(attack)}
|
||||
>
|
||||
${renderTroops(attack.troops)}
|
||||
${(
|
||||
this.game.playerBySmallID(
|
||||
attack.attackerID,
|
||||
) as PlayerView
|
||||
)?.name()}
|
||||
</button>
|
||||
${attack.retreating ? "(retreating...)" : ""}
|
||||
${this.incomingAttacks.map(
|
||||
(attack) => html`
|
||||
${this.renderButton({
|
||||
content: html`
|
||||
${renderTroops(attack.troops)}
|
||||
${(
|
||||
this.game.playerBySmallID(attack.attackerID) as PlayerView
|
||||
)?.name()}
|
||||
${attack.retreating
|
||||
? `(${translateText("events_display.retreating")}...)`
|
||||
: ""}
|
||||
`,
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
onClick: () => this.attackWarningOnClick(attack),
|
||||
className: "text-left text-red-400",
|
||||
translate: false,
|
||||
})}
|
||||
`,
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
@@ -536,35 +613,39 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
return html`
|
||||
${this.outgoingAttacks.length > 0
|
||||
? html`
|
||||
<tr class="border-t border-gray-700">
|
||||
<td class="lg:p-3 p-1 text-left text-blue-400">
|
||||
${this.outgoingAttacks.map(
|
||||
(attack) => html`
|
||||
<button
|
||||
translate="no"
|
||||
class="ml-2"
|
||||
@click=${async () => this.attackWarningOnClick(attack)}
|
||||
>
|
||||
${renderTroops(attack.troops)}
|
||||
${(
|
||||
this.game.playerBySmallID(attack.targetID) as PlayerView
|
||||
)?.name()}
|
||||
</button>
|
||||
|
||||
<div class="flex flex-wrap gap-y-1 gap-x-2">
|
||||
${this.outgoingAttacks.map(
|
||||
(attack) => html`
|
||||
<div class="inline-flex items-center gap-1">
|
||||
${this.renderButton({
|
||||
content: html`
|
||||
${renderTroops(attack.troops)}
|
||||
${(
|
||||
this.game.playerBySmallID(
|
||||
attack.targetID,
|
||||
) as PlayerView
|
||||
)?.name()}
|
||||
`,
|
||||
onClick: async () => this.attackWarningOnClick(attack),
|
||||
className: "text-left text-blue-400",
|
||||
translate: false,
|
||||
})}
|
||||
${!attack.retreating
|
||||
? html`<button
|
||||
${attack.retreating ? "disabled" : ""}
|
||||
@click=${() => {
|
||||
this.emitCancelAttackIntent(attack.id);
|
||||
}}
|
||||
>
|
||||
❌
|
||||
</button>`
|
||||
: "(retreating...)"}
|
||||
`,
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
? this.renderButton({
|
||||
content: "❌",
|
||||
onClick: () => this.emitCancelAttackIntent(attack.id),
|
||||
className: "text-left flex-shrink-0",
|
||||
disabled: attack.retreating,
|
||||
})
|
||||
: html`<span class="flex-shrink-0 text-blue-400"
|
||||
>(${translateText(
|
||||
"events_display.retreating",
|
||||
)}...)</span
|
||||
>`}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
@@ -574,28 +655,33 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
return html`
|
||||
${this.outgoingLandAttacks.length > 0
|
||||
? html`
|
||||
<tr class="border-t border-gray-700">
|
||||
<td class="lg:p-3 p-1 text-left text-gray-400">
|
||||
${this.outgoingLandAttacks.map(
|
||||
(landAttack) => html`
|
||||
<button translate="no" class="ml-2">
|
||||
${renderTroops(landAttack.troops)} Wilderness
|
||||
</button>
|
||||
|
||||
<div class="flex flex-wrap gap-y-1 gap-x-2">
|
||||
${this.outgoingLandAttacks.map(
|
||||
(landAttack) => html`
|
||||
<div class="inline-flex items-center gap-1">
|
||||
${this.renderButton({
|
||||
content: html`${renderTroops(landAttack.troops)}
|
||||
Wilderness`,
|
||||
className: "text-left text-gray-400",
|
||||
translate: false,
|
||||
})}
|
||||
${!landAttack.retreating
|
||||
? html`<button
|
||||
${landAttack.retreating ? "disabled" : ""}
|
||||
@click=${() => {
|
||||
this.emitCancelAttackIntent(landAttack.id);
|
||||
}}
|
||||
>
|
||||
❌
|
||||
</button>`
|
||||
: "(retreating...)"}
|
||||
`,
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
? this.renderButton({
|
||||
content: "❌",
|
||||
onClick: () =>
|
||||
this.emitCancelAttackIntent(landAttack.id),
|
||||
className: "text-left flex-shrink-0",
|
||||
disabled: landAttack.retreating,
|
||||
})
|
||||
: html`<span class="flex-shrink-0 text-blue-400"
|
||||
>(${translateText(
|
||||
"events_display.retreating",
|
||||
)}...)</span
|
||||
>`}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
@@ -605,41 +691,71 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
return html`
|
||||
${this.outgoingBoats.length > 0
|
||||
? html`
|
||||
<tr class="border-t border-gray-700">
|
||||
<td class="lg:p-3 p-1 text-left text-blue-400">
|
||||
${this.outgoingBoats.map(
|
||||
(boat) => html`
|
||||
<button
|
||||
translate="no"
|
||||
@click=${() => this.emitGoToUnitEvent(boat)}
|
||||
>
|
||||
Boat: ${renderTroops(boat.troops())}
|
||||
</button>
|
||||
<div class="flex flex-wrap gap-y-1 gap-x-2">
|
||||
${this.outgoingBoats.map(
|
||||
(boat) => html`
|
||||
<div class="inline-flex items-center gap-1">
|
||||
${this.renderButton({
|
||||
content: html`${translateText("events_display.boat")}:
|
||||
${renderTroops(boat.troops())}`,
|
||||
onClick: () => this.emitGoToUnitEvent(boat),
|
||||
className: "text-left text-blue-400",
|
||||
translate: false,
|
||||
})}
|
||||
${!boat.retreating()
|
||||
? html`<button
|
||||
${boat.retreating() ? "disabled" : ""}
|
||||
@click=${() => {
|
||||
this.emitBoatCancelIntent(boat.id());
|
||||
}}
|
||||
>
|
||||
❌
|
||||
</button>`
|
||||
: "(retreating...)"}
|
||||
`,
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
? this.renderButton({
|
||||
content: "❌",
|
||||
onClick: () => this.emitBoatCancelIntent(boat.id()),
|
||||
className: "text-left flex-shrink-0",
|
||||
disabled: boat.retreating(),
|
||||
})
|
||||
: html`<span class="flex-shrink-0 text-blue-400"
|
||||
>(${translateText(
|
||||
"events_display.retreating",
|
||||
)}...)</span
|
||||
>`}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.active) {
|
||||
if (!this.active || !this._isVisible) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
this.events.sort((a, b) => {
|
||||
const styles = html`
|
||||
<style>
|
||||
@keyframes goldBounce {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
30% {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
70% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
const filteredEvents = this.events.filter((event) => {
|
||||
const category = getMessageCategory(event.type);
|
||||
return !this.eventsFilters.get(category);
|
||||
});
|
||||
|
||||
filteredEvents.sort((a, b) => {
|
||||
const aPrior = a.priority ?? 100000;
|
||||
const bPrior = b.priority ?? 100000;
|
||||
if (aPrior === bPrior) {
|
||||
@@ -649,109 +765,265 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
});
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="${this._hidden
|
||||
? "w-fit px-[10px] py-[5px]"
|
||||
: ""} rounded-md bg-black bg-opacity-60 relative max-h-[30vh] flex flex-col-reverse overflow-y-auto w-full lg:bottom-2.5 lg:right-2.5 z-50 lg:max-w-[30vw] lg:w-full lg:w-auto"
|
||||
>
|
||||
<div>
|
||||
<div class="w-full bg-black/80 sticky top-0 px-[10px]">
|
||||
<button
|
||||
class="text-white cursor-pointer pointer-events-auto ${this
|
||||
._hidden
|
||||
? "hidden"
|
||||
: ""}"
|
||||
@click=${this.toggleHidden}
|
||||
>
|
||||
Hide
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="text-white cursor-pointer pointer-events-auto ${this._hidden
|
||||
? ""
|
||||
: "hidden"}"
|
||||
@click=${this.toggleHidden}
|
||||
>
|
||||
Events
|
||||
<span
|
||||
class="${this.newEvents
|
||||
? ""
|
||||
: "hidden"} inline-block px-2 bg-red-500 rounded-sm"
|
||||
>${this.newEvents}</span
|
||||
>
|
||||
</button>
|
||||
<table
|
||||
class="w-full border-collapse text-white shadow-lg lg:text-xl text-xs ${this
|
||||
._hidden
|
||||
? "hidden"
|
||||
: ""}"
|
||||
style="pointer-events: auto;"
|
||||
>
|
||||
<tbody>
|
||||
${this.events.map(
|
||||
(event, index) => html`
|
||||
<tr
|
||||
class="border-b border-opacity-0 ${this.getMessageTypeClasses(
|
||||
event.type,
|
||||
)}"
|
||||
${styles}
|
||||
<!-- Events Toggle (when hidden) -->
|
||||
${this._hidden
|
||||
? html`
|
||||
<div class="relative w-fit lg:bottom-2.5 lg:right-2.5 z-50">
|
||||
${this.renderButton({
|
||||
content: html`
|
||||
Events
|
||||
<span
|
||||
class="${this.newEvents
|
||||
? ""
|
||||
: "hidden"} inline-block px-2 bg-red-500 rounded-xl text-sm"
|
||||
>${this.newEvents}</span
|
||||
>
|
||||
<td class="lg:p-3 p-1 text-left">
|
||||
${event.focusID
|
||||
? html`<button
|
||||
@click=${() => {
|
||||
event.focusID &&
|
||||
this.emitGoToPlayerEvent(event.focusID);
|
||||
}}
|
||||
>
|
||||
${this.getEventDescription(event)}
|
||||
</button>`
|
||||
: event.unitView
|
||||
? html`<button
|
||||
@click=${() => {
|
||||
event.unitView &&
|
||||
this.emitGoToUnitEvent(event.unitView);
|
||||
}}
|
||||
`,
|
||||
onClick: this.toggleHidden,
|
||||
className:
|
||||
"text-white cursor-pointer pointer-events-auto w-fit p-2 lg:p-3 rounded-md bg-gray-800/70 backdrop-blur",
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<!-- Main Events Display -->
|
||||
<div
|
||||
class="relative w-full lg:bottom-2.5 lg:right-2.5 z-50 lg:w-96 backdrop-blur"
|
||||
>
|
||||
<!-- Button Bar -->
|
||||
<div
|
||||
class="w-full p-2 lg:p-3 rounded-t-none md:rounded-t-md bg-gray-800/70"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex gap-4">
|
||||
${this.renderButton({
|
||||
content: html`<img
|
||||
src="${swordIcon}"
|
||||
class="w-5 h-5"
|
||||
style="filter: ${this.eventsFilters.get(
|
||||
MessageCategory.ATTACK,
|
||||
)
|
||||
? "grayscale(1) opacity(0.5)"
|
||||
: "none"}"
|
||||
/>`,
|
||||
onClick: () =>
|
||||
this.toggleEventFilter(MessageCategory.ATTACK),
|
||||
className: "cursor-pointer pointer-events-auto",
|
||||
})}
|
||||
${this.renderButton({
|
||||
content: html`<img
|
||||
src="${donateGoldIcon}"
|
||||
class="w-5 h-5"
|
||||
style="filter: ${this.eventsFilters.get(
|
||||
MessageCategory.TRADE,
|
||||
)
|
||||
? "grayscale(1) opacity(0.5)"
|
||||
: "none"}"
|
||||
/>`,
|
||||
onClick: () =>
|
||||
this.toggleEventFilter(MessageCategory.TRADE),
|
||||
className: "cursor-pointer pointer-events-auto",
|
||||
})}
|
||||
${this.renderButton({
|
||||
content: html`<img
|
||||
src="${allianceIcon}"
|
||||
class="w-5 h-5"
|
||||
style="filter: ${this.eventsFilters.get(
|
||||
MessageCategory.ALLIANCE,
|
||||
)
|
||||
? "grayscale(1) opacity(0.5)"
|
||||
: "none"}"
|
||||
/>`,
|
||||
onClick: () =>
|
||||
this.toggleEventFilter(MessageCategory.ALLIANCE),
|
||||
className: "cursor-pointer pointer-events-auto",
|
||||
})}
|
||||
${this.renderButton({
|
||||
content: html`<img
|
||||
src="${chatIcon}"
|
||||
class="w-5 h-5"
|
||||
style="filter: ${this.eventsFilters.get(
|
||||
MessageCategory.CHAT,
|
||||
)
|
||||
? "grayscale(1) opacity(0.5)"
|
||||
: "none"}"
|
||||
/>`,
|
||||
onClick: () =>
|
||||
this.toggleEventFilter(MessageCategory.CHAT),
|
||||
className: "cursor-pointer pointer-events-auto",
|
||||
})}
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
${this.latestGoldAmount !== null
|
||||
? html`<span
|
||||
class="text-green-400 font-semibold transition-all duration-300 ${this
|
||||
.goldAmountAnimating
|
||||
? "animate-pulse scale-110"
|
||||
: "scale-100"}"
|
||||
style="animation: ${this.goldAmountAnimating
|
||||
? "goldBounce 0.6s ease-out"
|
||||
: "none"}"
|
||||
>+${renderNumber(this.latestGoldAmount)}</span
|
||||
>`
|
||||
: ""}
|
||||
${this.renderButton({
|
||||
content: "Hide",
|
||||
onClick: this.toggleHidden,
|
||||
className:
|
||||
"text-white cursor-pointer pointer-events-auto",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Area -->
|
||||
<div
|
||||
class="rounded-b-none md:rounded-b-md bg-gray-800/70 max-h-[30vh] flex flex-col-reverse overflow-y-auto w-full h-full"
|
||||
>
|
||||
<div>
|
||||
<table
|
||||
class="w-full max-h-none border-collapse text-white shadow-lg lg:text-base text-md md:text-xs"
|
||||
style="pointer-events: auto;"
|
||||
>
|
||||
<tbody>
|
||||
${filteredEvents.map(
|
||||
(event, index) => html`
|
||||
<tr>
|
||||
<td
|
||||
class="lg:px-2 lg:py-1 p-1 text-left ${getMessageTypeClasses(
|
||||
event.type,
|
||||
)}"
|
||||
>
|
||||
${this.getEventDescription(event)}
|
||||
</button>`
|
||||
: this.getEventDescription(event)}
|
||||
${event.buttons
|
||||
? html`
|
||||
<div class="flex flex-wrap gap-1.5 mt-1">
|
||||
${event.buttons.map(
|
||||
(btn) => html`
|
||||
<button
|
||||
class="inline-block px-3 py-1 text-white rounded text-sm cursor-pointer transition-colors duration-300
|
||||
${event.focusID
|
||||
? this.renderButton({
|
||||
content: this.getEventDescription(event),
|
||||
onClick: () => {
|
||||
event.focusID &&
|
||||
this.emitGoToPlayerEvent(event.focusID);
|
||||
},
|
||||
className: "text-left",
|
||||
})
|
||||
: event.unitView
|
||||
? this.renderButton({
|
||||
content: this.getEventDescription(event),
|
||||
onClick: () => {
|
||||
event.unitView &&
|
||||
this.emitGoToUnitEvent(
|
||||
event.unitView,
|
||||
);
|
||||
},
|
||||
className: "text-left",
|
||||
})
|
||||
: this.getEventDescription(event)}
|
||||
<!-- Events with buttons (Alliance requests) -->
|
||||
${event.buttons
|
||||
? html`
|
||||
<div class="flex flex-wrap gap-1.5 mt-1">
|
||||
${event.buttons.map(
|
||||
(btn) => html`
|
||||
<button
|
||||
class="inline-block px-3 py-1 text-white rounded text-md md:text-sm cursor-pointer transition-colors duration-300
|
||||
${btn.className.includes("btn-info")
|
||||
? "bg-blue-500 hover:bg-blue-600"
|
||||
: btn.className.includes("btn-gray")
|
||||
? "bg-gray-500 hover:bg-gray-600"
|
||||
: "bg-green-600 hover:bg-green-700"}"
|
||||
@click=${() => {
|
||||
btn.action();
|
||||
if (!btn.preventClose) {
|
||||
this.removeEvent(index);
|
||||
}
|
||||
this.requestUpdate();
|
||||
}}
|
||||
>
|
||||
${btn.text}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
? "bg-blue-500 hover:bg-blue-600"
|
||||
: btn.className.includes(
|
||||
"btn-gray",
|
||||
)
|
||||
? "bg-gray-500 hover:bg-gray-600"
|
||||
: "bg-green-600 hover:bg-green-700"}"
|
||||
@click=${() => {
|
||||
btn.action();
|
||||
if (!btn.preventClose) {
|
||||
const originalIndex =
|
||||
this.events.findIndex(
|
||||
(e) => e === event,
|
||||
);
|
||||
if (originalIndex !== -1) {
|
||||
this.removeEvent(
|
||||
originalIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
this.requestUpdate();
|
||||
}}
|
||||
>
|
||||
${btn.text}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</td>
|
||||
</tr>
|
||||
`,
|
||||
)}
|
||||
<!--- Incoming attacks row -->
|
||||
${this.incomingAttacks.length > 0
|
||||
? html`
|
||||
<tr class="lg:px-2 lg:py-1 p-1">
|
||||
<td class="lg:px-2 lg:py-1 p-1 text-left">
|
||||
${this.renderIncomingAttacks()}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
: ""}
|
||||
</td>
|
||||
</tr>
|
||||
`,
|
||||
)}
|
||||
${this.renderIncomingAttacks()} ${this.renderOutgoingAttacks()}
|
||||
${this.renderOutgoingLandAttacks()} ${this.renderBoats()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--- Outgoing attacks row -->
|
||||
${this.outgoingAttacks.length > 0
|
||||
? html`
|
||||
<tr class="lg:px-2 lg:py-1 p-1">
|
||||
<td class="lg:px-2 lg:py-1 p-1 text-left">
|
||||
${this.renderOutgoingAttacks()}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<!--- Outgoing land attacks row -->
|
||||
${this.outgoingLandAttacks.length > 0
|
||||
? html`
|
||||
<tr class="lg:px-2 lg:py-1 p-1">
|
||||
<td class="lg:px-2 lg:py-1 p-1 text-left">
|
||||
${this.renderOutgoingLandAttacks()}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<!--- Boats row -->
|
||||
${this.outgoingBoats.length > 0
|
||||
? html`
|
||||
<tr class="lg:px-2 lg:py-1 p-1">
|
||||
<td class="lg:px-2 lg:py-1 p-1 text-left">
|
||||
${this.renderBoats()}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<!--- Empty row when no events or attacks -->
|
||||
${filteredEvents.length === 0 &&
|
||||
this.incomingAttacks.length === 0 &&
|
||||
this.outgoingAttacks.length === 0 &&
|
||||
this.outgoingLandAttacks.length === 0 &&
|
||||
this.outgoingBoats.length === 0
|
||||
? html`
|
||||
<tr>
|
||||
<td
|
||||
class="lg:px-2 lg:py-1 p-1 min-w-72 text-left"
|
||||
>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
: ""}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ export class AttackExecution implements Execution {
|
||||
if (deaths) {
|
||||
this.mg.displayMessage(
|
||||
`Attack cancelled, ${renderTroops(deaths)} soldiers killed during retreat.`,
|
||||
MessageType.SUCCESS,
|
||||
MessageType.ATTACK_CANCELLED,
|
||||
this._owner.id(),
|
||||
);
|
||||
}
|
||||
@@ -340,8 +340,9 @@ export class AttackExecution implements Execution {
|
||||
`Conquered ${this.target.displayName()} received ${renderNumber(
|
||||
gold,
|
||||
)} gold`,
|
||||
MessageType.SUCCESS,
|
||||
MessageType.CONQUERED_PLAYER,
|
||||
this._owner.id(),
|
||||
gold,
|
||||
);
|
||||
this.target.removeGold(gold);
|
||||
this._owner.addGold(gold);
|
||||
|
||||
@@ -67,8 +67,9 @@ export class MirvExecution implements Execution {
|
||||
|
||||
this.mg.displayIncomingUnit(
|
||||
this.nuke.id(),
|
||||
// TODO TranslateText
|
||||
`⚠️⚠️⚠️ ${this.player.name()} - MIRV INBOUND ⚠️⚠️⚠️`,
|
||||
MessageType.ERROR,
|
||||
MessageType.MIRV_INBOUND,
|
||||
this.targetPlayer.id(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,16 +111,18 @@ export class NukeExecution implements Execution {
|
||||
} else if (this.nukeType === UnitType.AtomBomb) {
|
||||
this.mg.displayIncomingUnit(
|
||||
this.nuke.id(),
|
||||
// TODO TranslateText
|
||||
`${this.player.name()} - atom bomb inbound`,
|
||||
MessageType.ERROR,
|
||||
MessageType.NUKE_INBOUND,
|
||||
target.id(),
|
||||
);
|
||||
this.breakAlliances(this.tilesToDestroy());
|
||||
} else if (this.nukeType === UnitType.HydrogenBomb) {
|
||||
this.mg.displayIncomingUnit(
|
||||
this.nuke.id(),
|
||||
// TODO TranslateText
|
||||
`${this.player.name()} - hydrogen bomb inbound`,
|
||||
MessageType.ERROR,
|
||||
MessageType.HYDROGEN_BOMB_INBOUND,
|
||||
target.id(),
|
||||
);
|
||||
this.breakAlliances(this.tilesToDestroy());
|
||||
|
||||
@@ -207,8 +207,9 @@ export class PlayerExecution implements Execution {
|
||||
`Conquered ${this.player.displayName()} received ${renderNumber(
|
||||
gold,
|
||||
)} gold`,
|
||||
MessageType.SUCCESS,
|
||||
MessageType.CONQUERED_PLAYER,
|
||||
capturing.id(),
|
||||
gold,
|
||||
);
|
||||
capturing.addGold(gold);
|
||||
this.player.removeGold(gold);
|
||||
|
||||
@@ -174,7 +174,7 @@ export class SAMLauncherExecution implements Execution {
|
||||
if (!hit) {
|
||||
this.mg.displayMessage(
|
||||
`Missile failed to intercept ${type}`,
|
||||
MessageType.ERROR,
|
||||
MessageType.SAM_MISS,
|
||||
this.sam.owner().id(),
|
||||
);
|
||||
} else {
|
||||
@@ -182,7 +182,7 @@ export class SAMLauncherExecution implements Execution {
|
||||
// Message
|
||||
this.mg.displayMessage(
|
||||
`${mirvWarheadTargets.length} MIRV warheads intercepted`,
|
||||
MessageType.SUCCESS,
|
||||
MessageType.SAM_HIT,
|
||||
this.sam.owner().id(),
|
||||
);
|
||||
// Delete warheads
|
||||
|
||||
@@ -62,7 +62,7 @@ export class SAMMissileExecution implements Execution {
|
||||
if (result === true) {
|
||||
this.mg.displayMessage(
|
||||
`Missile intercepted ${this.target.type()}`,
|
||||
MessageType.SUCCESS,
|
||||
MessageType.SAM_HIT,
|
||||
this._owner.id(),
|
||||
);
|
||||
this.active = false;
|
||||
|
||||
@@ -131,21 +131,24 @@ export class TradeShipExecution implements Execution {
|
||||
this.tradeShip!.owner().addGold(gold);
|
||||
this.mg.displayMessage(
|
||||
`Received ${renderNumber(gold)} gold from ship captured from ${this.origOwner.displayName()}`,
|
||||
MessageType.SUCCESS,
|
||||
MessageType.CAPTURED_ENEMY_UNIT,
|
||||
this.tradeShip!.owner().id(),
|
||||
gold,
|
||||
);
|
||||
} else {
|
||||
this.srcPort.owner().addGold(gold);
|
||||
this._dstPort.owner().addGold(gold);
|
||||
this.mg.displayMessage(
|
||||
`Received ${renderNumber(gold)} gold from trade with ${this.srcPort.owner().displayName()}`,
|
||||
MessageType.SUCCESS,
|
||||
MessageType.RECEIVED_GOLD_FROM_TRADE,
|
||||
this._dstPort.owner().id(),
|
||||
gold,
|
||||
);
|
||||
this.mg.displayMessage(
|
||||
`Received ${renderNumber(gold)} gold from trade with ${this._dstPort.owner().displayName()}`,
|
||||
MessageType.SUCCESS,
|
||||
MessageType.RECEIVED_GOLD_FROM_TRADE,
|
||||
this.srcPort.owner().id(),
|
||||
gold,
|
||||
);
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -62,7 +62,7 @@ export class TransportShipExecution implements Execution {
|
||||
) {
|
||||
mg.displayMessage(
|
||||
`No boats available, max ${mg.config().boatMaxNumber()}`,
|
||||
MessageType.WARN,
|
||||
MessageType.ATTACK_FAILED,
|
||||
this.attacker.id(),
|
||||
);
|
||||
this.active = false;
|
||||
@@ -130,8 +130,9 @@ export class TransportShipExecution implements Execution {
|
||||
if (this.targetID && this.targetID !== mg.terraNullius().id()) {
|
||||
mg.displayIncomingUnit(
|
||||
this.boat.id(),
|
||||
// TODO TranslateText
|
||||
`Naval invasion incoming from ${this.attacker.displayName()}`,
|
||||
MessageType.WARN,
|
||||
MessageType.NAVAL_INVASION_INBOUND,
|
||||
this.targetID,
|
||||
);
|
||||
}
|
||||
|
||||
+67
-4
@@ -587,6 +587,7 @@ export interface Game extends GameMap {
|
||||
message: string,
|
||||
type: MessageType,
|
||||
playerID: PlayerID | null,
|
||||
goldAmount?: bigint,
|
||||
): void;
|
||||
displayIncomingUnit(
|
||||
unitID: number,
|
||||
@@ -653,13 +654,75 @@ export interface EmojiMessage {
|
||||
}
|
||||
|
||||
export enum MessageType {
|
||||
SUCCESS,
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR,
|
||||
ATTACK_FAILED,
|
||||
ATTACK_CANCELLED,
|
||||
ATTACK_REQUEST,
|
||||
CONQUERED_PLAYER,
|
||||
MIRV_INBOUND,
|
||||
NUKE_INBOUND,
|
||||
HYDROGEN_BOMB_INBOUND,
|
||||
NAVAL_INVASION_INBOUND,
|
||||
SAM_MISS,
|
||||
SAM_HIT,
|
||||
CAPTURED_ENEMY_UNIT,
|
||||
UNIT_CAPTURED_BY_ENEMY,
|
||||
UNIT_DESTROYED,
|
||||
ALLIANCE_ACCEPTED,
|
||||
ALLIANCE_REJECTED,
|
||||
ALLIANCE_REQUEST,
|
||||
ALLIANCE_BROKEN,
|
||||
ALLIANCE_EXPIRED,
|
||||
SENT_GOLD_TO_PLAYER,
|
||||
RECEIVED_GOLD_FROM_PLAYER,
|
||||
RECEIVED_GOLD_FROM_TRADE,
|
||||
SENT_TROOPS_TO_PLAYER,
|
||||
RECEIVED_TROOPS_FROM_PLAYER,
|
||||
CHAT,
|
||||
}
|
||||
|
||||
// Message categories used for filtering events in the EventsDisplay
|
||||
export enum MessageCategory {
|
||||
ATTACK = "ATTACK",
|
||||
ALLIANCE = "ALLIANCE",
|
||||
TRADE = "TRADE",
|
||||
CHAT = "CHAT",
|
||||
}
|
||||
|
||||
// Ensures that all message types are included in a category
|
||||
export const MESSAGE_TYPE_CATEGORIES: Record<MessageType, MessageCategory> = {
|
||||
[MessageType.ATTACK_FAILED]: MessageCategory.ATTACK,
|
||||
[MessageType.ATTACK_CANCELLED]: MessageCategory.ATTACK,
|
||||
[MessageType.ATTACK_REQUEST]: MessageCategory.ATTACK,
|
||||
[MessageType.CONQUERED_PLAYER]: MessageCategory.ATTACK,
|
||||
[MessageType.MIRV_INBOUND]: MessageCategory.ATTACK,
|
||||
[MessageType.NUKE_INBOUND]: MessageCategory.ATTACK,
|
||||
[MessageType.HYDROGEN_BOMB_INBOUND]: MessageCategory.ATTACK,
|
||||
[MessageType.NAVAL_INVASION_INBOUND]: MessageCategory.ATTACK,
|
||||
[MessageType.SAM_MISS]: MessageCategory.ATTACK,
|
||||
[MessageType.SAM_HIT]: MessageCategory.ATTACK,
|
||||
[MessageType.CAPTURED_ENEMY_UNIT]: MessageCategory.ATTACK,
|
||||
[MessageType.UNIT_CAPTURED_BY_ENEMY]: MessageCategory.ATTACK,
|
||||
[MessageType.UNIT_DESTROYED]: MessageCategory.ATTACK,
|
||||
[MessageType.ALLIANCE_ACCEPTED]: MessageCategory.ALLIANCE,
|
||||
[MessageType.ALLIANCE_REJECTED]: MessageCategory.ALLIANCE,
|
||||
[MessageType.ALLIANCE_REQUEST]: MessageCategory.ALLIANCE,
|
||||
[MessageType.ALLIANCE_BROKEN]: MessageCategory.ALLIANCE,
|
||||
[MessageType.ALLIANCE_EXPIRED]: MessageCategory.ALLIANCE,
|
||||
[MessageType.SENT_GOLD_TO_PLAYER]: MessageCategory.TRADE,
|
||||
[MessageType.RECEIVED_GOLD_FROM_PLAYER]: MessageCategory.TRADE,
|
||||
[MessageType.RECEIVED_GOLD_FROM_TRADE]: MessageCategory.TRADE,
|
||||
[MessageType.SENT_TROOPS_TO_PLAYER]: MessageCategory.TRADE,
|
||||
[MessageType.RECEIVED_TROOPS_FROM_PLAYER]: MessageCategory.TRADE,
|
||||
[MessageType.CHAT]: MessageCategory.CHAT,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Get the category of a message type
|
||||
*/
|
||||
export function getMessageCategory(messageType: MessageType): MessageCategory {
|
||||
return MESSAGE_TYPE_CATEGORIES[messageType];
|
||||
}
|
||||
|
||||
export interface NameViewData {
|
||||
x: number;
|
||||
y: number;
|
||||
|
||||
@@ -612,6 +612,7 @@ export class GameImpl implements Game {
|
||||
message: string,
|
||||
type: MessageType,
|
||||
playerID: PlayerID | null,
|
||||
goldAmount?: bigint,
|
||||
): void {
|
||||
let id: number | null = null;
|
||||
if (playerID !== null) {
|
||||
@@ -622,6 +623,7 @@ export class GameImpl implements Game {
|
||||
messageType: type,
|
||||
message: message,
|
||||
playerID: id,
|
||||
goldAmount: goldAmount,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ export interface DisplayMessageUpdate {
|
||||
type: GameUpdateType.DisplayEvent;
|
||||
message: string;
|
||||
messageType: MessageType;
|
||||
goldAmount?: bigint;
|
||||
playerID: number | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -558,12 +558,12 @@ export class PlayerImpl implements Player {
|
||||
this.sentDonations.push(new Donation(recipient, this.mg.ticks()));
|
||||
this.mg.displayMessage(
|
||||
`Sent ${renderTroops(troops)} troops to ${recipient.name()}`,
|
||||
MessageType.INFO,
|
||||
MessageType.SENT_TROOPS_TO_PLAYER,
|
||||
this.id(),
|
||||
);
|
||||
this.mg.displayMessage(
|
||||
`Received ${renderTroops(troops)} troops from ${this.name()}`,
|
||||
MessageType.SUCCESS,
|
||||
MessageType.RECEIVED_TROOPS_FROM_PLAYER,
|
||||
recipient.id(),
|
||||
);
|
||||
return true;
|
||||
@@ -578,13 +578,14 @@ export class PlayerImpl implements Player {
|
||||
this.sentDonations.push(new Donation(recipient, this.mg.ticks()));
|
||||
this.mg.displayMessage(
|
||||
`Sent ${renderNumber(gold)} gold to ${recipient.name()}`,
|
||||
MessageType.INFO,
|
||||
MessageType.SENT_GOLD_TO_PLAYER,
|
||||
this.id(),
|
||||
);
|
||||
this.mg.displayMessage(
|
||||
`Received ${renderNumber(gold)} gold from ${this.name()}`,
|
||||
MessageType.SUCCESS,
|
||||
MessageType.RECEIVED_GOLD_FROM_PLAYER,
|
||||
recipient.id(),
|
||||
gold,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ export class UnitImpl implements Unit {
|
||||
case UnitType.City:
|
||||
this.mg.stats().unitCapture(newOwner, this._type);
|
||||
this.mg.stats().unitLose(this._owner, this._type);
|
||||
break;
|
||||
}
|
||||
this._lastOwner = this._owner;
|
||||
this._lastOwner._units = this._lastOwner._units.filter((u) => u !== this);
|
||||
@@ -168,12 +169,12 @@ export class UnitImpl implements Unit {
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
this.mg.displayMessage(
|
||||
`Your ${this.type()} was captured by ${newOwner.displayName()}`,
|
||||
MessageType.ERROR,
|
||||
MessageType.UNIT_CAPTURED_BY_ENEMY,
|
||||
this._lastOwner.id(),
|
||||
);
|
||||
this.mg.displayMessage(
|
||||
`Captured ${this.type()} from ${this._lastOwner.displayName()}`,
|
||||
MessageType.SUCCESS,
|
||||
MessageType.CAPTURED_ENEMY_UNIT,
|
||||
newOwner.id(),
|
||||
);
|
||||
}
|
||||
@@ -200,7 +201,7 @@ export class UnitImpl implements Unit {
|
||||
if (displayMessage !== false && this._type !== UnitType.MIRVWarhead) {
|
||||
this.mg.displayMessage(
|
||||
`Your ${this._type} was destroyed`,
|
||||
MessageType.ERROR,
|
||||
MessageType.UNIT_DESTROYED,
|
||||
this.owner().id(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { getMessageTypeClasses, severityColors } from "../src/client/Utils";
|
||||
import { MessageType } from "../src/core/game/Game";
|
||||
|
||||
describe("getMessageTypeClasses", () => {
|
||||
// Spy on console.warn to track when the default case is hit
|
||||
let consoleSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should return a valid CSS class for every MessageType", () => {
|
||||
const messageTypes = Object.values(MessageType).filter(
|
||||
(value) => typeof value === "number",
|
||||
) as MessageType[];
|
||||
|
||||
messageTypes.forEach((messageType) => {
|
||||
const result = getMessageTypeClasses(messageType);
|
||||
|
||||
expect(Object.values(severityColors)).toContain(result);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(typeof result).toBe("string");
|
||||
});
|
||||
});
|
||||
|
||||
it("should not trigger console.warn for any MessageType", () => {
|
||||
const messageTypes = Object.values(MessageType).filter(
|
||||
(value) => typeof value === "number",
|
||||
) as MessageType[];
|
||||
|
||||
messageTypes.forEach((messageType) => {
|
||||
getMessageTypeClasses(messageType);
|
||||
});
|
||||
|
||||
// No message type should fall through to the default case
|
||||
expect(consoleSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return white color and warn for unknown message types", () => {
|
||||
// Cast to MessageType to test the default case
|
||||
const unknownType = 999 as MessageType;
|
||||
|
||||
const result = getMessageTypeClasses(unknownType);
|
||||
|
||||
expect(result).toBe(severityColors["white"]);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"Message type 999 has no explicit color",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user