From 915359feca9e0392c4b970b0f38cbb0ff525da38 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 1 Nov 2024 20:12:26 -0700 Subject: [PATCH] updated EventsDisplay to be lit component --- src/client/graphics/GameRenderer.ts | 11 +- src/client/graphics/layers/EventsDisplay.ts | 580 +++++++++++--------- src/client/index.html | 1 + src/client/styles.css | 64 +-- src/core/configuration/DevConfig.ts | 22 +- 5 files changed, 321 insertions(+), 357 deletions(-) diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index dbdb80f0c..f85ab688e 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -40,12 +40,21 @@ export function createRenderer(canvas: HTMLCanvasElement, game: Game, eventBus: controlPanel.eventBus = eventBus controlPanel.uiState = uiState + const eventsDisplay = document.querySelector('events-display') as EventsDisplay; + if (!(eventsDisplay instanceof EventsDisplay)) { + console.error('events display not found') + } + eventsDisplay.eventBus = eventBus + eventsDisplay.game = game + eventsDisplay.clientID = clientID + + const layers: Layer[] = [ new TerrainLayer(game), new TerritoryLayer(game, eventBus), new NameLayer(game, game.config().theme(), transformHandler, clientID), new UILayer(eventBus, game, clientID, transformHandler), - new EventsDisplay(eventBus, game, clientID), + eventsDisplay, new RadialMenu(eventBus, game, transformHandler, clientID, emojiTable as EmojiTable, uiState), leaderboard, controlPanel, diff --git a/src/client/graphics/layers/EventsDisplay.ts b/src/client/graphics/layers/EventsDisplay.ts index f51199d71..f97637f43 100644 --- a/src/client/graphics/layers/EventsDisplay.ts +++ b/src/client/graphics/layers/EventsDisplay.ts @@ -1,325 +1,359 @@ -import { nullable } from "zod"; +import { LitElement, html, css } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; import { EventBus, GameEvent } from "../../../core/EventBus"; -import { AllianceExpiredEvent, AllianceRequestEvent, AllianceRequestReplyEvent, AllPlayers, BrokeAllianceEvent, EmojiMessageEvent, Game, Player, PlayerID, TargetPlayerEvent } from "../../../core/game/Game"; +import { + AllianceExpiredEvent, + AllianceRequestEvent, + AllianceRequestReplyEvent, + AllPlayers, + BrokeAllianceEvent, + EmojiMessageEvent, + Game, + Player, + PlayerID, + TargetPlayerEvent +} from "../../../core/game/Game"; import { ClientID } from "../../../core/Schemas"; import { Layer } from "./Layer"; import { SendAllianceReplyIntentEvent } from "../../Transport"; export enum MessageType { - SUCCESS, - INFO, - WARN, - ERROR, + SUCCESS, + INFO, + WARN, + ERROR, } export class DisplayMessageEvent implements GameEvent { - constructor( - public readonly message: string, - public readonly type: MessageType, - public readonly playerID: PlayerID | null = null - ) { } + constructor( + public readonly message: string, + public readonly type: MessageType, + public readonly playerID: PlayerID | null = null + ) { } } interface Event { - description: string; - buttons?: { - text: string - className: string - action: () => void - }[]; - type: MessageType; - highlight?: boolean; - createdAt: number - onDelete?: () => void + description: string; + buttons?: { + text: string; + className: string; + action: () => void; + }[]; + type: MessageType; + highlight?: boolean; + createdAt: number; + onDelete?: () => void; } -export class EventsDisplay implements Layer { - private events: Event[]; - private tableContainer: HTMLDivElement; +@customElement('events-display') +export class EventsDisplay extends LitElement implements Layer { + public eventBus: EventBus; + public game: Game; + public clientID: ClientID; + private events: Event[] = []; - constructor(private eventBus: EventBus, private game: Game, private clientID: ClientID) { - const element = document.getElementById("app"); - element.style.zIndex = "1000" - if (!element) throw new Error(`Container element with id app not found`); - this.events = []; - this.createTableContainer() + static styles = css` + :host { + display: block; + position: fixed; + bottom: 10px; + right: 10px; + z-index: 1000; + max-width: 400px; } - init() { - this.eventBus.on(AllianceRequestEvent, a => this.onAllianceRequestEvent(a)) - this.eventBus.on(AllianceRequestReplyEvent, a => this.onAllianceRequestReplyEvent(a)) - this.eventBus.on(DisplayMessageEvent, e => this.onDisplayMessageEvent(e)) - this.eventBus.on(BrokeAllianceEvent, e => this.onBrokeAllianceEvent(e)) - this.eventBus.on(AllianceExpiredEvent, e => this.onAllianceExpiredEvent(e)) - this.eventBus.on(TargetPlayerEvent, e => this.onTargetPlayerEvent(e)) - this.eventBus.on(EmojiMessageEvent, e => this.onEmojiMessageEvent(e)) - this.renderTable() + .events-table { + width: 100%; + border-collapse: collapse; + background-color: rgba(0, 0, 0, 0.7); + color: white; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); + font-size: 1.2em; } - tick() { - let remainingEvents: Event[] = [] - for (const event of this.events) { - if (this.game.ticks() - event.createdAt < 50) { - remainingEvents.push(event) - } else if (event.onDelete != null) { - event.onDelete() - } - } - if (remainingEvents.length > 5) { - remainingEvents = remainingEvents.slice(-5) - } - - let shouldRender = false - if (this.events.length != remainingEvents.length) { - shouldRender = true - } - this.events = remainingEvents - if (shouldRender) { - this.renderTable() - } + .events-table th, + .events-table td { + padding: 15px; + text-align: left; + border-bottom: 1px solid rgba(255, 255, 255, 0.0); + z-index: 1000; } - private createTableContainer() { - this.tableContainer = document.createElement('div'); - this.tableContainer.id = 'table-container'; - this.tableContainer.className = 'events-display'; - this.tableContainer.style.display = "none"; - document.body.appendChild(this.tableContainer); + .events-table th { + background-color: rgba(0, 0, 0, 0.0); + font-size: 1.2em; + text-transform: uppercase; } - shouldTransform(): boolean { - return false + .events-table tr:hover { + background-color: rgba(255, 255, 255, 0.0); } - onDisplayMessageEvent(event: DisplayMessageEvent) { - if (event.playerID != null) { - const myPlayer = this.game.playerByClientID(this.clientID) - if (myPlayer == null) { - return - } - if (myPlayer == null) { - return - } - if (myPlayer.id() != event.playerID) { - return - } - } - this.addEvent({ - description: event.message, - createdAt: this.game.ticks(), - highlight: true, - type: event.type, - }) - this.renderTable() + .btn { + display: inline-block; + padding: 8px 16px; + margin: 5px 10px 5px 0; + background-color: #4CAF50; + color: white; + text-decoration: none; + border-radius: 4px; + transition: background-color 0.3s; + border: none; + cursor: pointer; } - onAllianceRequestEvent(event: AllianceRequestEvent): void { - const myPlayer = this.game.playerByClientID(this.clientID) - if (myPlayer == null) { - return - } - - if (event.allianceRequest.recipient() != myPlayer) { - return - } - - this.addEvent({ - description: `${event.allianceRequest.requestor().name()} requests an alliance!`, - buttons: [ - { - text: "Accept", - className: "btn", - action: () => this.eventBus.emit(new SendAllianceReplyIntentEvent(event.allianceRequest, true)), - }, - { - text: "Reject", - className: "btn btn-info", - action: () => this.eventBus.emit(new SendAllianceReplyIntentEvent(event.allianceRequest, false)), - } - ], - highlight: true, - type: MessageType.INFO, - createdAt: this.game.ticks(), - onDelete: () => this.eventBus.emit(new SendAllianceReplyIntentEvent(event.allianceRequest, false)) - }); + .btn:hover { + background-color: #45a049; } - // TODO: move this to DisplayMessageEvent - onAllianceRequestReplyEvent(event: AllianceRequestReplyEvent) { - const myPlayer = this.game.playerByClientID(this.clientID) - if (myPlayer == null) { - return - } - - if (event.allianceRequest.requestor() != myPlayer) { - return - } - this.addEvent({ - description: `${event.allianceRequest.recipient().name()} ${event.accepted ? "accepted" : "rejected"} your alliance request`, - type: event.accepted ? MessageType.SUCCESS : MessageType.ERROR, - highlight: true, - createdAt: this.game.ticks(), - }); + .btn-info { + background-color: #2196F3; } - onBrokeAllianceEvent(event: BrokeAllianceEvent) { - const myPlayer = this.game.playerByClientID(this.clientID) - if (myPlayer == null) { - return - } - if (event.traitor == myPlayer) { - this.addEvent({ - description: `You broke your alliance with ${event.betrayed.name()}, making you a TRAITOR`, - type: MessageType.ERROR, - highlight: true, - createdAt: this.game.ticks(), - }) - } - if (event.betrayed == myPlayer) { - this.addEvent({ - description: `${event.traitor.name()}, broke their alliance with you`, - type: MessageType.ERROR, - highlight: true, - createdAt: this.game.ticks(), - }) - } + .btn-info:hover { + background-color: #0b7dda; } - onAllianceExpiredEvent(event: AllianceExpiredEvent) { - const myPlayer = this.game.playerByClientID(this.clientID) - if (myPlayer == null) { - return - } - let other: Player = null - if (event.player1 == myPlayer) { - other = event.player2 - } - if (event.player2 == myPlayer) { - other = event.player1 - } - if (other == null) { - return - } - if (!myPlayer.isAlive() || !other.isAlive()) { - return - } - this.addEvent({ - description: `Your alliance with ${other.name()} expired`, - type: MessageType.WARN, - highlight: true, - createdAt: this.game.ticks(), - }) + .success td { color: rgb(120, 255, 140); } + .info td { color: rgb(230, 230, 230); } + .warn td { color: rgb(255, 220, 80) } + .error td { color: rgb(255, 100, 100); } + + @media (max-width: 600px) { + .events-table th, + .events-table td { + padding: 10px; + } + + .btn { + display: block; + margin: 5px 0; + } } - onTargetPlayerEvent(event: TargetPlayerEvent) { - const myPlayer = this.game.playerByClientID(this.clientID) - if (myPlayer == null) { - return - } - if (myPlayer.isAlliedWith(event.player)) { - this.addEvent({ - description: `${event.player.name()} requests you attack ${event.target.name()}`, - type: MessageType.INFO, - highlight: true, - createdAt: this.game.ticks(), - }) - } + .button-container { + display: flex; + gap: 8px; + flex-wrap: wrap; } - onEmojiMessageEvent(event: EmojiMessageEvent) { - const myPlayer = this.game.playerByClientID(this.clientID) - if (myPlayer == null) { - return - } - if (event.message.recipient == myPlayer) { - this.addEvent({ - description: `${event.message.sender.displayName()}:${event.message.emoji}`, - type: MessageType.INFO, - highlight: true, - createdAt: this.game.ticks(), - }) - } - if (event.message.sender == myPlayer && event.message.recipient != AllPlayers) { - this.addEvent({ - description: `Sent ${event.message.recipient.displayName()} ${event.message.emoji}`, - type: MessageType.INFO, - highlight: true, - createdAt: this.game.ticks(), - }) - } + @media (max-width: 600px) { + .button-container { + flex-direction: column; + } + } + `; + + constructor() { + super(); + this.events = []; + } + + init() { + this.eventBus.on(AllianceRequestEvent, a => this.onAllianceRequestEvent(a)); + this.eventBus.on(AllianceRequestReplyEvent, a => this.onAllianceRequestReplyEvent(a)); + this.eventBus.on(DisplayMessageEvent, e => this.onDisplayMessageEvent(e)); + this.eventBus.on(BrokeAllianceEvent, e => this.onBrokeAllianceEvent(e)); + this.eventBus.on(AllianceExpiredEvent, e => this.onAllianceExpiredEvent(e)); + this.eventBus.on(TargetPlayerEvent, e => this.onTargetPlayerEvent(e)); + this.eventBus.on(EmojiMessageEvent, e => this.onEmojiMessageEvent(e)); + } + + tick() { + let remainingEvents = this.events.filter(event => { + const shouldKeep = this.game.ticks() - event.createdAt < 50; + if (!shouldKeep && event.onDelete) { + event.onDelete(); + } + return shouldKeep; + }); + + if (remainingEvents.length > 5) { + remainingEvents = remainingEvents.slice(-5); } - addEvent(event: Event): void { - this.events.push(event); - this.renderTable() + if (this.events.length !== remainingEvents.length) { + this.events = remainingEvents; + this.requestUpdate() + } + } + + private addEvent(event: Event) { + this.events = [...this.events, event]; + this.requestUpdate() + } + + private removeEvent(index: number) { + this.events = [ + ...this.events.slice(0, index), + ...this.events.slice(index + 1) + ]; + } + + shouldTransform(): boolean { + return false; + } + + renderLayer(): void { } + + onDisplayMessageEvent(event: DisplayMessageEvent) { + const myPlayer = this.game.playerByClientID(this.clientID); + if (event.playerID != null && (!myPlayer || myPlayer.id() !== event.playerID)) { + return; } - removeEvent(index: number): void { - this.events.splice(index, 1); + this.addEvent({ + description: event.message, + createdAt: this.game.ticks(), + highlight: true, + type: event.type, + }); + } + + onAllianceRequestEvent(event: AllianceRequestEvent) { + const myPlayer = this.game.playerByClientID(this.clientID); + if (!myPlayer || event.allianceRequest.recipient() !== myPlayer) { + return; } - updateEvent(index: number, event: Event): void { - this.events[index] = event; - } - - renderLayer(): void { } - - renderTable(): void { - if (this.events.length === 0) { - this.tableContainer.innerHTML = ""; - this.tableContainer.style.display = "none"; - return; + this.addEvent({ + description: `${event.allianceRequest.requestor().name()} requests an alliance!`, + buttons: [ + { + text: "Accept", + className: "btn", + action: () => this.eventBus.emit(new SendAllianceReplyIntentEvent(event.allianceRequest, true)), + }, + { + text: "Reject", + className: "btn btn-info", + action: () => this.eventBus.emit(new SendAllianceReplyIntentEvent(event.allianceRequest, false)), } + ], + highlight: true, + type: MessageType.INFO, + createdAt: this.game.ticks(), + onDelete: () => this.eventBus.emit(new SendAllianceReplyIntentEvent(event.allianceRequest, false)) + }); + } - this.tableContainer.style.display = "block"; + onAllianceRequestReplyEvent(event: AllianceRequestReplyEvent) { + const myPlayer = this.game.playerByClientID(this.clientID); + if (!myPlayer || event.allianceRequest.requestor() !== myPlayer) { + return; + } + this.addEvent({ + description: `${event.allianceRequest.recipient().name()} ${event.accepted ? "accepted" : "rejected"} your alliance request`, + type: event.accepted ? MessageType.SUCCESS : MessageType.ERROR, + highlight: true, + createdAt: this.game.ticks(), + }); + } - let tableHtml = ` - - - `; + onBrokeAllianceEvent(event: BrokeAllianceEvent) { + const myPlayer = this.game.playerByClientID(this.clientID); + if (!myPlayer) return; - this.events.forEach((event, eventIndex) => { - const typeClass = MessageType[event.type].toLowerCase(); - tableHtml += ` - - + if (event.traitor === myPlayer) { + this.addEvent({ + description: `You broke your alliance with ${event.betrayed.name()}, making you a TRAITOR`, + type: MessageType.ERROR, + highlight: true, + createdAt: this.game.ticks(), + }); + } else if (event.betrayed === myPlayer) { + this.addEvent({ + description: `${event.traitor.name()}, broke their alliance with you`, + type: MessageType.ERROR, + highlight: true, + createdAt: this.game.ticks(), + }); + } + } + + onAllianceExpiredEvent(event: AllianceExpiredEvent) { + const myPlayer = this.game.playerByClientID(this.clientID); + if (!myPlayer) return; + + const other = event.player1 === myPlayer ? event.player2 : event.player2 === myPlayer ? event.player1 : null; + if (!other || !myPlayer.isAlive() || !other.isAlive()) return; + + this.addEvent({ + description: `Your alliance with ${other.name()} expired`, + type: MessageType.WARN, + highlight: true, + createdAt: this.game.ticks(), + }); + } + + onTargetPlayerEvent(event: TargetPlayerEvent) { + const myPlayer = this.game.playerByClientID(this.clientID); + if (!myPlayer || !myPlayer.isAlliedWith(event.player)) return; + + this.addEvent({ + description: `${event.player.name()} requests you attack ${event.target.name()}`, + type: MessageType.INFO, + highlight: true, + createdAt: this.game.ticks(), + }); + } + + onEmojiMessageEvent(event: EmojiMessageEvent) { + const myPlayer = this.game.playerByClientID(this.clientID); + if (!myPlayer) return; + + if (event.message.recipient === myPlayer) { + this.addEvent({ + description: `${event.message.sender.displayName()}:${event.message.emoji}`, + type: MessageType.INFO, + highlight: true, + createdAt: this.game.ticks(), + }); + } else if (event.message.sender === myPlayer && event.message.recipient !== AllPlayers) { + this.addEvent({ + description: `Sent ${event.message.recipient.displayName()} ${event.message.emoji}`, + type: MessageType.INFO, + highlight: true, + createdAt: this.game.ticks(), + }); + } + } + + render() { + if (this.events.length === 0) { + return html``; + } + + return html` +
- ${event.description} - ${event.buttons ? '
' + event.buttons.map((btn, btnIndex) => - `` - ).join('') + '
' : ''} -
+ + ${this.events.map((event, index) => html` + + - `; - }); - - tableHtml += ` - -
+ ${event.description} + ${event.buttons ? html` +
+ ${event.buttons.map(btn => html` + + `)} +
+ ` : ''} +
+ `)} + + `; - this.tableContainer.innerHTML = tableHtml; - - - // Add event listeners to buttons - this.tableContainer.querySelectorAll('button').forEach(button => { - button.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - const target = e.target as HTMLElement; - const eventIndex = parseInt(target.getAttribute('data-event-index') || ''); - const buttonIndex = parseInt(target.getAttribute('data-button-index') || ''); - - if (!isNaN(eventIndex) && !isNaN(buttonIndex)) { - const event = this.events[eventIndex]; - const buttonAction = event.buttons?.[buttonIndex]?.action; - if (buttonAction) { - buttonAction(); - this.removeEvent(eventIndex); - this.renderTable(); - } - } - }); - }); - } + } } \ No newline at end of file diff --git a/src/client/index.html b/src/client/index.html index 02758b3e8..f8840d61f 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -73,6 +73,7 @@ +