mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 02:47:45 +00:00
create player panel
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -348,6 +348,9 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
canTarget(other: Player): boolean {
|
||||
if (this == other) {
|
||||
return false;
|
||||
}
|
||||
if (this.isAlliedWith(other)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user