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 @@
+
+
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`
+
+ `;
+ }
+}
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;
}