create player panel

This commit is contained in:
Evan
2025-02-05 20:35:21 -08:00
parent fa22861d43
commit 6abcddc140
9 changed files with 517 additions and 62 deletions
+12 -1
View File
@@ -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(
+5 -4
View File
@@ -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}
</button>
`,
`
)}
</div>
`,
`
)}
</div>
`;
@@ -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();
}
+253
View File
@@ -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`
<div
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 pointer-events-auto"
>
<div
class="bg-opacity-60 bg-gray-900 p-1 lg:p-2 rounded-lg backdrop-blur-md relative"
>
<!-- Close button -->
<button
@click=${this.handleClose}
class="absolute -top-2 -right-2 w-6 h-6 flex items-center justify-center
bg-red-500 hover:bg-red-600 text-white rounded-full
text-sm font-bold transition-colors"
>
</button>
<div class="flex flex-col gap-2 min-w-[240px]">
<!-- Name section -->
<div class="flex items-center gap-1 lg:gap-2">
<div
class="px-4 h-8 lg:h-10 flex items-center justify-center
bg-opacity-50 bg-gray-700 text-opacity-90 text-white
rounded text-sm lg:text-xl w-full"
>
${other?.name()}
</div>
</div>
<!-- Resources section -->
<div class="grid grid-cols-2 gap-2">
<div class="flex flex-col gap-1">
<!-- Gold -->
<div class="text-white text-opacity-80 text-sm px-2">Gold</div>
<div class="bg-opacity-50 bg-gray-700 rounded p-2 text-white">
${renderNumber(other.gold() || 0)}
</div>
</div>
<div class="flex flex-col gap-1">
<!-- Troops -->
<div class="text-white text-opacity-80 text-sm px-2">
Troops
</div>
<div class="bg-opacity-50 bg-gray-700 rounded p-2 text-white">
${renderTroops(other.troops() || 0)}
</div>
</div>
</div>
<!-- Attitude section -->
<div class="flex flex-col gap-1">
<div class="text-white text-opacity-80 text-sm px-2">Traitor</div>
<div class="bg-opacity-50 bg-gray-700 rounded p-2 text-white">
${other.isTraitor()}
</div>
</div>
<!-- Action buttons -->
<div class="flex justify-center gap-2">
${canTarget
? html`<button
@click=${(e) => this.handleTargetClick(e, other)}
class="w-10 h-10 flex items-center justify-center
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
text-white rounded-lg transition-colors"
>
<img src=${targetIcon} alt="Target" class="w-6 h-6" />
</button>`
: ""}
${canBreakAlliance
? html`<button
@click=${(e) =>
this.handleBreakAllianceClick(e, myPlayer, other)}
class="w-10 h-10 flex items-center justify-center
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
text-white rounded-lg transition-colors"
>
<img
src=${traitorIcon}
alt="Break Alliance"
class="w-6 h-6"
/>
</button>`
: ""}
${canSendAllianceRequest
? html`<button
@click=${(e) =>
this.handleAllianceClick(e, myPlayer, other)}
class="w-10 h-10 flex items-center justify-center
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
text-white rounded-lg transition-colors"
>
<img src=${allianceIcon} alt="Alliance" class="w-6 h-6" />
</button>`
: ""}
${canDonate
? html`<button
@click=${(e) => this.handleDonateClick(e, myPlayer, other)}
class="w-10 h-10 flex items-center justify-center
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
text-white rounded-lg transition-colors"
>
<img src=${donateIcon} alt="Donate" class="w-6 h-6" />
</button>`
: ""}
${canSendEmoji
? html`<button
@click=${(e) => this.handleEmojiClick(e, myPlayer, other)}
class="w-10 h-10 flex items-center justify-center
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
text-white rounded-lg transition-colors"
>
<img src=${emojiIcon} alt="Emoji" class="w-6 h-6" />
</button>`
: ""}
</div>
</div>
</div>
</div>
`;
}
}
+66 -56
View File
@@ -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<HTMLDivElement, unknown, null, undefined>;
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<any>()
.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<any>()
@@ -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) {
+1
View File
@@ -178,6 +178,7 @@
<build-menu></build-menu>
<win-modal></win-modal>
<top-bar></top-bar>
<player-panel></player-panel>
<div class="fixed right-0 top-0 z-50 flex flex-col w-32 sm:w-32 lg:w-48">
<options-menu></options-menu>
+3
View File
@@ -348,6 +348,9 @@ export class PlayerImpl implements Player {
}
canTarget(other: Player): boolean {
if (this == other) {
return false;
}
if (this.isAlliedWith(other)) {
return false;
}