From 6abcddc14066ec2daf80febefcb63a2ef544fcfc Mon Sep 17 00:00:00 2001 From: Evan Date: Wed, 5 Feb 2025 20:35:21 -0800 Subject: [PATCH] create player panel --- .gitignore | 4 +- resources/images/InfoIcon.svg | 106 +++++++++ resources/images/XIcon.svg | 68 ++++++ src/client/graphics/GameRenderer.ts | 13 +- src/client/graphics/layers/EmojiTable.ts | 9 +- src/client/graphics/layers/PlayerPanel.ts | 253 ++++++++++++++++++++++ src/client/graphics/layers/RadialMenu.ts | 122 ++++++----- src/client/index.html | 1 + src/core/game/PlayerImpl.ts | 3 + 9 files changed, 517 insertions(+), 62 deletions(-) create mode 100644 resources/images/InfoIcon.svg create mode 100644 resources/images/XIcon.svg create mode 100644 src/client/graphics/layers/PlayerPanel.ts diff --git a/.gitignore b/.gitignore index 1e5d52dc0..d17f2382b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ build/ node_modules/ out/ -TODO.txt \ No newline at end of file +TODO.txt +resources/images/.DS_Store +resources/.DS_Store diff --git a/resources/images/InfoIcon.svg b/resources/images/InfoIcon.svg new file mode 100644 index 000000000..e69ee3c0f --- /dev/null +++ b/resources/images/InfoIcon.svg @@ -0,0 +1,106 @@ + + + + + + Created by Kevin White + from the Noun Project + + + + + + + + + + + + + + diff --git a/resources/images/XIcon.svg b/resources/images/XIcon.svg new file mode 100644 index 000000000..582b3346e --- /dev/null +++ b/resources/images/XIcon.svg @@ -0,0 +1,68 @@ + + diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index c9cff260a..57b5dd0ac 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -23,6 +23,7 @@ import { WinModal } from "./layers/WinModal"; import { SpawnTimer } from "./layers/SpawnTimer"; import { OptionsMenu } from "./layers/OptionsMenu"; import { TopBar } from "./layers/TopBar"; +import { PlayerPanel } from "./layers/PlayerPanel"; export function createRenderer( canvas: HTMLCanvasElement, @@ -104,6 +105,14 @@ export function createRenderer( } topBar.game = game; + const playerPanel = document.querySelector("player-panel") as PlayerPanel; + if (!(playerPanel instanceof PlayerPanel)) { + console.error("player panel not found"); + } + playerPanel.g = game; + playerPanel.eventBus = eventBus; + playerPanel.emojiTable = emojiTable; + const layers: Layer[] = [ new TerrainLayer(game), new TerritoryLayer(game, eventBus), @@ -119,7 +128,8 @@ export function createRenderer( emojiTable as EmojiTable, buildMenu, uiState, - playerInfo + playerInfo, + playerPanel ), new SpawnTimer(game, transformHandler), leaderboard, @@ -128,6 +138,7 @@ export function createRenderer( winModel, optionsMenu, topBar, + playerPanel, ]; return new GameRenderer( diff --git a/src/client/graphics/layers/EmojiTable.ts b/src/client/graphics/layers/EmojiTable.ts index 82bd15c60..1a1b4752a 100644 --- a/src/client/graphics/layers/EmojiTable.ts +++ b/src/client/graphics/layers/EmojiTable.ts @@ -93,7 +93,7 @@ export class EmojiTable extends LitElement { @state() private _hidden = true; - public onEmojiClicked: (emoji: string) => void = () => {}; + private onEmojiClicked: (emoji: string) => void = () => {}; render() { return html` @@ -109,10 +109,10 @@ export class EmojiTable extends LitElement { > ${emoji} - `, + ` )} - `, + ` )} `; @@ -123,7 +123,8 @@ export class EmojiTable extends LitElement { this.requestUpdate(); } - showTable() { + showTable(oneEmojiClicked: (emoji: string) => void) { + this.onEmojiClicked = oneEmojiClicked; this._hidden = false; this.requestUpdate(); } diff --git a/src/client/graphics/layers/PlayerPanel.ts b/src/client/graphics/layers/PlayerPanel.ts new file mode 100644 index 000000000..7d7f8c1b4 --- /dev/null +++ b/src/client/graphics/layers/PlayerPanel.ts @@ -0,0 +1,253 @@ +import { LitElement, html } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { EventBus } from "../../../core/EventBus"; +import { GameView, PlayerView } from "../../../core/game/GameView"; +import { Layer } from "./Layer"; +import { MouseUpEvent } from "../../InputHandler"; +import { AllPlayers, Player, PlayerActions } from "../../../core/game/Game"; +import { TileRef } from "../../../core/game/GameMap"; +import { renderNumber, renderTroops } from "../../Utils"; +import targetIcon from "../../../../resources/images/TargetIconWhite.png"; +import emojiIcon from "../../../../resources/images/EmojiIconWhite.png"; +import donateIcon from "../../../../resources/images/DonateIconWhite.png"; +import traitorIcon from "../../../../resources/images/TraitorIconWhite.png"; +import allianceIcon from "../../../../resources/images/AllianceIconWhite.png"; +import { + SendAllianceRequestIntentEvent, + SendBreakAllianceIntentEvent, + SendDonateIntentEvent, + SendEmojiIntentEvent, + SendTargetPlayerIntentEvent, +} from "../../Transport"; +import { EmojiTable } from "./EmojiTable"; + +@customElement("player-panel") +export class PlayerPanel extends LitElement implements Layer { + public g: GameView; + public eventBus: EventBus; + public emojiTable: EmojiTable; + + private actions: PlayerActions = null; + private tile: TileRef = null; + + @state() + private isVisible: boolean = false; + + public show(actions: PlayerActions, tile: TileRef) { + this.actions = actions; + this.tile = tile; + this.isVisible = true; + this.requestUpdate(); + } + + public hide() { + this.isVisible = false; + this.requestUpdate(); + } + + private handleClose(e: Event) { + e.stopPropagation(); + this.hide(); + } + + private handleAllianceClick( + e: Event, + myPlayer: PlayerView, + other: PlayerView + ) { + e.stopPropagation(); + this.eventBus.emit(new SendAllianceRequestIntentEvent(myPlayer, other)); + this.hide(); + } + + private handleBreakAllianceClick( + e: Event, + myPlayer: PlayerView, + other: PlayerView + ) { + e.stopPropagation(); + this.eventBus.emit(new SendBreakAllianceIntentEvent(myPlayer, other)); + this.hide(); + } + + private handleDonateClick(e: Event, myPlayer: PlayerView, other: PlayerView) { + e.stopPropagation(); + this.eventBus.emit(new SendDonateIntentEvent(myPlayer, other, null)); + this.hide(); + } + + private handleEmojiClick(e: Event, myPlayer: PlayerView, other: PlayerView) { + e.stopPropagation(); + this.emojiTable.showTable((emoji: string) => { + if (myPlayer == other) { + this.eventBus.emit(new SendEmojiIntentEvent(AllPlayers, emoji)); + } else { + this.eventBus.emit(new SendEmojiIntentEvent(other, emoji)); + } + this.emojiTable.hideTable(); + this.hide(); + }); + } + + private handleTargetClick(e: Event, other: PlayerView) { + e.stopPropagation(); + this.eventBus.emit(new SendTargetPlayerIntentEvent(other.id())); + this.hide(); + } + + createRenderRoot() { + return this; + } + + init() { + this.eventBus.on(MouseUpEvent, (e: MouseEvent) => this.hide()); + } + + tick() { + this.requestUpdate(); + } + + render() { + if (!this.isVisible) { + return html``; + } + const myPlayer = this.g.myPlayer(); + if (myPlayer == null) { + return; + } + + let other = this.g.owner(this.tile); + if (!other.isPlayer()) { + throw new Error("Tile is not owned by a player"); + } + other = other as PlayerView; + + const canDonate = this.actions.interaction?.canDonate; + const canSendAllianceRequest = + this.actions.interaction?.canSendAllianceRequest; + const canSendEmoji = this.actions.interaction?.canSendEmoji; + const canBreakAlliance = this.actions.interaction?.canBreakAlliance; + const canTarget = this.actions.interaction?.canTarget; + + return html` +
+
+ + + +
+ +
+
+ ${other?.name()} +
+
+ + +
+
+ +
Gold
+
+ ${renderNumber(other.gold() || 0)} +
+
+
+ +
+ Troops +
+
+ ${renderTroops(other.troops() || 0)} +
+
+
+ + +
+
Traitor
+
+ ${other.isTraitor()} +
+
+ + +
+ ${canTarget + ? html`` + : ""} + ${canBreakAlliance + ? html`` + : ""} + ${canSendAllianceRequest + ? html`` + : ""} + ${canDonate + ? html`` + : ""} + ${canSendEmoji + ? html`` + : ""} +
+
+
+
+ `; + } +} diff --git a/src/client/graphics/layers/RadialMenu.ts b/src/client/graphics/layers/RadialMenu.ts index 4aa0cdfe0..88b7bc216 100644 --- a/src/client/graphics/layers/RadialMenu.ts +++ b/src/client/graphics/layers/RadialMenu.ts @@ -29,9 +29,11 @@ import traitorIcon from "../../../../resources/images/TraitorIconWhite.png"; import allianceIcon from "../../../../resources/images/AllianceIconWhite.png"; import boatIcon from "../../../../resources/images/BoatIconWhite.png"; import swordIcon from "../../../../resources/images/SwordIconWhite.png"; +import infoIcon from "../../../../resources/images/InfoIcon.svg"; import targetIcon from "../../../../resources/images/TargetIconWhite.png"; import emojiIcon from "../../../../resources/images/EmojiIconWhite.png"; import disabledIcon from "../../../../resources/images/DisabledIcon.png"; +import xIcon from "../../../../resources/images/XIcon.svg"; import donateIcon from "../../../../resources/images/DonateIconWhite.png"; import buildIcon from "../../../../resources/images/BuildIconWhite.svg"; import { EmojiTable } from "./EmojiTable"; @@ -41,13 +43,13 @@ import { consolex } from "../../../core/Consolex"; import { GameView, PlayerView } from "../../../core/game/GameView"; import { TileRef } from "../../../core/game/GameMap"; import { PlayerInfoOverlay } from "./PlayerInfoOverlay"; +import { PlayerPanel } from "./PlayerPanel"; enum Slot { - Alliance, + Info, Boat, - Target, - Emoji, Build, + Close, } export class RadialMenu implements Layer { @@ -56,16 +58,6 @@ export class RadialMenu implements Layer { private menuElement: d3.Selection; private isVisible: boolean = false; private readonly menuItems = new Map([ - [ - Slot.Alliance, - { - name: "alliance", - disabled: true, - action: () => {}, - color: null, - icon: null, - }, - ], [ Slot.Boat, { @@ -76,9 +68,18 @@ export class RadialMenu implements Layer { icon: null, }, ], - [Slot.Target, { name: "target", disabled: true, action: () => {} }], - [Slot.Emoji, { name: "emoji", disabled: true, action: () => {} }], + [Slot.Close, { name: "close", disabled: true, action: () => {} }], [Slot.Build, { name: "build", disabled: true, action: () => {} }], + [ + Slot.Info, + { + name: "info", + disabled: true, + action: () => {}, + color: null, + icon: null, + }, + ], ]); private readonly menuSize = 190; @@ -97,7 +98,8 @@ export class RadialMenu implements Layer { private emojiTable: EmojiTable, private buildMenu: BuildMenu, private uiState: UIState, - private playerInfoOverlay: PlayerInfoOverlay + private playerInfoOverlay: PlayerInfoOverlay, + private playerPanel: PlayerPanel ) {} init() { @@ -145,7 +147,9 @@ export class RadialMenu implements Layer { const pie = d3 .pie() .value(() => 1) - .padAngle(0.03); + .padAngle(0.03) + .startAngle(Math.PI / 4) // Start at 45 degrees (π/4 radians) + .endAngle(2 * Math.PI + Math.PI / 4); // Complete the circle but shifted by 45 degrees const arc = d3 .arc() @@ -200,6 +204,7 @@ export class RadialMenu implements Layer { this.hideRadialMenu(); } }); + arcs .append("image") .attr("xlink:href", (d) => d.data.icon) @@ -244,7 +249,6 @@ export class RadialMenu implements Layer { .attr("fill", "#2c3e50") .style("pointer-events", "none"); - // Replace text with sword icon centerButton .append("image") .attr("class", "center-button-icon") @@ -321,26 +325,32 @@ export class RadialMenu implements Layer { this.activateMenuElement(Slot.Build, "#ebe250", buildIcon, () => { this.buildMenu.showMenu(myPlayer, this.clickedCell); }); - const canSendEmojiToPlayer = - this.g.hasOwner(tile) && - this.g.ownerID(tile) != myPlayer.smallID() && - actions.interaction?.canSendEmoji; - const canSendEmojiToAllPlayers = - this.g.ownerID(tile) == myPlayer.smallID() && - actions.canSendEmojiAllPlayers; - if (canSendEmojiToPlayer || canSendEmojiToAllPlayers) { - this.activateMenuElement(Slot.Emoji, "#00a6a4", emojiIcon, () => { - const target = - this.g.owner(tile) == myPlayer - ? AllPlayers - : (this.g.owner(tile) as PlayerView); - this.emojiTable.onEmojiClicked = (emoji: string) => { - this.emojiTable.hideTable(); - this.eventBus.emit(new SendEmojiIntentEvent(target, emoji)); - }; - this.emojiTable.showTable(); + if (this.g.hasOwner(tile)) { + this.activateMenuElement(Slot.Info, "#64748B", infoIcon, () => { + this.playerPanel.show(actions, tile); }); } + this.activateMenuElement(Slot.Close, "#DC2626", xIcon, () => {}); + // const canSendEmojiToPlayer = + // this.g.hasOwner(tile) && + // this.g.ownerID(tile) != myPlayer.smallID() && + // actions.interaction?.canSendEmoji; + // const canSendEmojiToAllPlayers = + // this.g.ownerID(tile) == myPlayer.smallID() && + // actions.canSendEmojiAllPlayers; + // if (canSendEmojiToPlayer || canSendEmojiToAllPlayers) { + // this.activateMenuElement(Slot.Emoji, "#00a6a4", emojiIcon, () => { + // const target = + // this.g.owner(tile) == myPlayer + // ? AllPlayers + // : (this.g.owner(tile) as PlayerView); + // this.emojiTable.onEmojiClicked = (emoji: string) => { + // this.emojiTable.hideTable(); + // this.eventBus.emit(new SendEmojiIntentEvent(target, emoji)); + // }; + // this.emojiTable.showTable(); + // }); + // } if (actions.canBoat) { this.activateMenuElement(Slot.Boat, "#3f6ab1", boatIcon, () => { @@ -362,29 +372,29 @@ export class RadialMenu implements Layer { } const other = this.g.owner(tile) as PlayerView; - if (actions?.interaction.canDonate) { - this.activateMenuElement(Slot.Target, "#53ac75", donateIcon, () => { - this.eventBus.emit(new SendDonateIntentEvent(myPlayer, other, null)); - }); - } + // if (actions?.interaction.canDonate) { + // this.activateMenuElement(Slot.Target, "#53ac75", donateIcon, () => { + // this.eventBus.emit(new SendDonateIntentEvent(myPlayer, other, null)); + // }); + // } - if (actions?.interaction.canTarget) { - this.activateMenuElement(Slot.Target, "#c74848", targetIcon, () => { - this.eventBus.emit(new SendTargetPlayerIntentEvent(other.id())); - }); - } + // if (actions?.interaction.canTarget) { + // this.activateMenuElement(Slot.Target, "#c74848", targetIcon, () => { + // this.eventBus.emit(new SendTargetPlayerIntentEvent(other.id())); + // }); + // } - if (actions?.interaction.canSendAllianceRequest) { - this.activateMenuElement(Slot.Alliance, "#53ac75", allianceIcon, () => { - this.eventBus.emit(new SendAllianceRequestIntentEvent(myPlayer, other)); - }); - } + // if (actions?.interaction.canSendAllianceRequest) { + // this.activateMenuElement(Slot.Alliance, "#53ac75", allianceIcon, () => { + // this.eventBus.emit(new SendAllianceRequestIntentEvent(myPlayer, other)); + // }); + // } - if (actions?.interaction.canBreakAlliance) { - this.activateMenuElement(Slot.Alliance, "#c74848", traitorIcon, () => { - this.eventBus.emit(new SendBreakAllianceIntentEvent(myPlayer, other)); - }); - } + // if (actions?.interaction.canBreakAlliance) { + // this.activateMenuElement(Slot.Alliance, "#c74848", traitorIcon, () => { + // this.eventBus.emit(new SendBreakAllianceIntentEvent(myPlayer, other)); + // }); + // } } private onPointerUp(event: MouseUpEvent) { diff --git a/src/client/index.html b/src/client/index.html index b92e22617..f200402f4 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -178,6 +178,7 @@ +
diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 506b29f7f..9c0ce7904 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -348,6 +348,9 @@ export class PlayerImpl implements Player { } canTarget(other: Player): boolean { + if (this == other) { + return false; + } if (this.isAlliedWith(other)) { return false; }