mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:40:44 +00:00
Feat: Quick donate troops via radial menu (#2708)
If this PR fixes an issue, link it below. If not, delete these two lines. Resolves #2705 ## Description: Introduces a quick donation feature in games where the `canDonateTroops` option is enabled. It works by converting the center button in the radial menu from a disabled attack button to a troop donate button when the player right clicked on is friendly (teammate or ally). Also adds donate gold button to Attack slot on radial menu when right clicking friendly troops. ### Video Example https://github.com/user-attachments/assets/d9b2c3f7-b6c0-482a-9dbd-b3841676cbe5 ### New Icon <img width="1310" height="931" alt="image" src="https://github.com/user-attachments/assets/85225858-6971-470d-92f6-db68a5d05bb2" /> ### Donate Gold https://github.com/user-attachments/assets/b116bc06-d53d-47c7-9504-871eada6a21e ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: bijx
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
rootMenuElement,
|
||||
} from "./RadialMenuElements";
|
||||
|
||||
import donateTroopIcon from "../../../../resources/images/DonateTroopIconWhite.svg";
|
||||
import swordIcon from "../../../../resources/images/SwordIconWhite.svg";
|
||||
import { ContextMenuEvent } from "../../InputHandler";
|
||||
|
||||
@@ -127,10 +128,22 @@ export class MainRadialMenu extends LitElement implements Layer {
|
||||
playerActionHandler: this.playerActionHandler,
|
||||
playerPanel: this.playerPanel,
|
||||
chatIntegration: this.chatIntegration,
|
||||
uiState: this.uiState,
|
||||
closeMenu: () => this.closeMenu(),
|
||||
eventBus: this.eventBus,
|
||||
};
|
||||
|
||||
const isFriendlyTarget =
|
||||
recipient !== null && recipient.isFriendly(myPlayer);
|
||||
|
||||
this.radialMenu.setCenterButtonAppearance(
|
||||
isFriendlyTarget ? donateTroopIcon : swordIcon,
|
||||
isFriendlyTarget ? "#34D399" : "#2c3e50",
|
||||
isFriendlyTarget
|
||||
? this.radialMenu.getDefaultCenterIconSize() * 0.75
|
||||
: this.radialMenu.getDefaultCenterIconSize(),
|
||||
);
|
||||
|
||||
this.radialMenu.setParams(params);
|
||||
if (screenX !== null && screenY !== null) {
|
||||
this.radialMenu.showRadialMenu(screenX, screenY);
|
||||
|
||||
@@ -84,8 +84,12 @@ export class PlayerActionHandler {
|
||||
this.eventBus.emit(new SendDonateGoldIntentEvent(recipient, null));
|
||||
}
|
||||
|
||||
handleDonateTroops(recipient: PlayerView) {
|
||||
this.eventBus.emit(new SendDonateTroopsIntentEvent(recipient, null));
|
||||
handleDonateTroops(recipient: PlayerView, troops?: number) {
|
||||
const amount = troops ?? null;
|
||||
if (amount !== null && amount <= 0) {
|
||||
return;
|
||||
}
|
||||
this.eventBus.emit(new SendDonateTroopsIntentEvent(recipient, amount));
|
||||
}
|
||||
|
||||
handleEmbargo(recipient: PlayerView, action: "start" | "stop") {
|
||||
|
||||
@@ -62,6 +62,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
@state() private allianceExpiryText: string | null = null;
|
||||
@state() private allianceExpirySeconds: number | null = null;
|
||||
@state() private otherProfile: PlayerProfile | null = null;
|
||||
@state() private suppressNextHide: boolean = false;
|
||||
|
||||
private ctModal: ChatModal;
|
||||
|
||||
@@ -78,7 +79,13 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
});
|
||||
}
|
||||
init() {
|
||||
this.eventBus.on(MouseUpEvent, () => this.hide());
|
||||
this.eventBus.on(MouseUpEvent, () => {
|
||||
if (this.suppressNextHide) {
|
||||
this.suppressNextHide = false;
|
||||
return;
|
||||
}
|
||||
this.hide();
|
||||
});
|
||||
|
||||
this.ctModal = document.querySelector("chat-modal") as ChatModal;
|
||||
if (!this.ctModal) {
|
||||
@@ -131,6 +138,20 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
public openSendGoldModal(
|
||||
actions: PlayerActions,
|
||||
tile: TileRef,
|
||||
target: PlayerView,
|
||||
) {
|
||||
this.suppressNextHide = true;
|
||||
this.actions = actions;
|
||||
this.tile = tile;
|
||||
this.sendTarget = target;
|
||||
this.sendMode = "gold";
|
||||
this.isVisible = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
public hide() {
|
||||
this.isVisible = false;
|
||||
this.sendMode = "none";
|
||||
@@ -164,11 +185,13 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
private openSendTroops(target: PlayerView) {
|
||||
this.suppressNextHide = true;
|
||||
this.sendTarget = target;
|
||||
this.sendMode = "troops";
|
||||
}
|
||||
|
||||
private openSendGold(target: PlayerView) {
|
||||
this.suppressNextHide = true;
|
||||
this.sendTarget = target;
|
||||
this.sendMode = "gold";
|
||||
}
|
||||
|
||||
@@ -78,6 +78,9 @@ export class RadialMenu implements Layer {
|
||||
private backButtonHoverTimeout: number | null = null;
|
||||
private navigationInProgress: boolean = false;
|
||||
private originalCenterButtonIcon: string = "";
|
||||
private readonly defaultCenterButtonColor = "#2c3e50";
|
||||
private centerButtonColor: string;
|
||||
private centerButtonIconSize: number;
|
||||
|
||||
private params: MenuElementParams | null = null;
|
||||
|
||||
@@ -103,6 +106,8 @@ export class RadialMenu implements Layer {
|
||||
};
|
||||
this.originalCenterButtonIcon = this.config.centerButtonIcon;
|
||||
this.backIconSize = this.config.centerIconSize * 0.8;
|
||||
this.centerButtonColor = this.defaultCenterButtonColor;
|
||||
this.centerButtonIconSize = this.config.centerIconSize;
|
||||
}
|
||||
|
||||
init() {
|
||||
@@ -193,17 +198,17 @@ export class RadialMenu implements Layer {
|
||||
.append("circle")
|
||||
.attr("class", "center-button-visible")
|
||||
.attr("r", this.config.centerButtonSize)
|
||||
.attr("fill", "#2c3e50")
|
||||
.attr("fill", this.centerButtonColor)
|
||||
.style("pointer-events", "none");
|
||||
|
||||
centerButton
|
||||
.append("image")
|
||||
.attr("class", "center-button-icon")
|
||||
.attr("xlink:href", this.config.centerButtonIcon)
|
||||
.attr("width", this.config.centerIconSize)
|
||||
.attr("height", this.config.centerIconSize)
|
||||
.attr("x", -this.config.centerIconSize / 2)
|
||||
.attr("y", -this.config.centerIconSize / 2)
|
||||
.attr("width", this.centerButtonIconSize)
|
||||
.attr("height", this.centerButtonIconSize)
|
||||
.attr("x", -this.centerButtonIconSize / 2)
|
||||
.attr("y", -this.centerButtonIconSize / 2)
|
||||
.style("pointer-events", "none");
|
||||
}
|
||||
|
||||
@@ -888,10 +893,10 @@ export class RadialMenu implements Layer {
|
||||
const iconImg = this.menuElement.select(".center-button-icon");
|
||||
iconImg
|
||||
.attr("xlink:href", this.originalCenterButtonIcon)
|
||||
.attr("width", this.config.centerIconSize)
|
||||
.attr("height", this.config.centerIconSize)
|
||||
.attr("x", -this.config.centerIconSize / 2)
|
||||
.attr("y", -this.config.centerIconSize / 2);
|
||||
.attr("width", this.centerButtonIconSize)
|
||||
.attr("height", this.centerButtonIconSize)
|
||||
.attr("x", -this.centerButtonIconSize / 2)
|
||||
.attr("y", -this.centerButtonIconSize / 2);
|
||||
}
|
||||
|
||||
const centerButton = this.menuElement.select(".center-button");
|
||||
@@ -904,7 +909,7 @@ export class RadialMenu implements Layer {
|
||||
|
||||
centerButton
|
||||
.select(".center-button-visible")
|
||||
.attr("fill", enabled ? "#2c3e50" : "#999999");
|
||||
.attr("fill", enabled ? this.centerButtonColor : "#999999");
|
||||
|
||||
centerButton
|
||||
.select(".center-button-icon")
|
||||
@@ -953,6 +958,39 @@ export class RadialMenu implements Layer {
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
public getDefaultCenterIconSize(): number {
|
||||
return this.config.centerIconSize;
|
||||
}
|
||||
|
||||
public setCenterButtonAppearance(
|
||||
icon: string,
|
||||
color?: string,
|
||||
iconSize?: number,
|
||||
) {
|
||||
this.originalCenterButtonIcon = icon;
|
||||
this.centerButtonColor = color ?? this.defaultCenterButtonColor;
|
||||
this.centerButtonIconSize = iconSize ?? this.config.centerIconSize;
|
||||
|
||||
if (!this.menuElement) return;
|
||||
|
||||
const iconImg = this.menuElement.select(".center-button-icon");
|
||||
iconImg
|
||||
.attr("xlink:href", icon)
|
||||
.attr("width", this.centerButtonIconSize)
|
||||
.attr("height", this.centerButtonIconSize)
|
||||
.attr("x", -this.centerButtonIconSize / 2)
|
||||
.attr("y", -this.centerButtonIconSize / 2);
|
||||
|
||||
this.menuElement
|
||||
.select(".center-button-visible")
|
||||
.attr(
|
||||
"fill",
|
||||
this.isCenterButtonEnabled() ? this.centerButtonColor : "#999999",
|
||||
);
|
||||
|
||||
this.updateCenterButtonState(this.centerButtonState);
|
||||
}
|
||||
|
||||
private findMenuItem(id: string): MenuElement | undefined {
|
||||
return this.currentMenuItems.find((item) => item.id === id);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { Emoji, flattenedEmojiTable } from "../../../core/Util";
|
||||
import { renderNumber, translateText } from "../../Utils";
|
||||
import { UIState } from "../UIState";
|
||||
import { BuildItemDisplay, BuildMenu, flattenedBuildTable } from "./BuildMenu";
|
||||
import { ChatIntegration } from "./ChatIntegration";
|
||||
import { EmojiTable } from "./EmojiTable";
|
||||
@@ -37,6 +38,7 @@ export interface MenuElementParams {
|
||||
playerPanel: PlayerPanel;
|
||||
chatIntegration: ChatIntegration;
|
||||
eventBus: EventBus;
|
||||
uiState?: UIState;
|
||||
closeMenu: () => void;
|
||||
}
|
||||
|
||||
@@ -107,6 +109,14 @@ export enum Slot {
|
||||
Delete = "delete",
|
||||
}
|
||||
|
||||
function isFriendlyTarget(params: MenuElementParams): boolean {
|
||||
const selectedPlayer = params.selected;
|
||||
if (selectedPlayer === null) return false;
|
||||
const isFriendly = (selectedPlayer as PlayerView).isFriendly;
|
||||
if (typeof isFriendly !== "function") return false;
|
||||
return isFriendly.call(selectedPlayer, params.myPlayer);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const infoChatElement: MenuElement = {
|
||||
id: "info_chat",
|
||||
@@ -423,6 +433,24 @@ export const attackMenuElement: MenuElement = {
|
||||
},
|
||||
};
|
||||
|
||||
const donateGoldRadialElement: MenuElement = {
|
||||
id: Slot.Attack,
|
||||
name: "radial_donate_gold",
|
||||
disabled: (params: MenuElementParams) =>
|
||||
params.game.inSpawnPhase() ||
|
||||
!params.playerActions?.interaction?.canDonateGold,
|
||||
icon: donateGoldIcon,
|
||||
color: "#EAB308",
|
||||
action: (params: MenuElementParams) => {
|
||||
if (!params.selected) return;
|
||||
params.playerPanel.openSendGoldModal(
|
||||
params.playerActions,
|
||||
params.tile,
|
||||
params.selected,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const deleteUnitElement: MenuElement = {
|
||||
id: Slot.Delete,
|
||||
name: "delete",
|
||||
@@ -549,16 +577,33 @@ export const centerButtonElement: CenterButtonElement = {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isFriendlyTarget(params)) {
|
||||
return !params.playerActions.interaction?.canDonateTroops;
|
||||
}
|
||||
|
||||
return !params.playerActions.canAttack;
|
||||
},
|
||||
action: (params: MenuElementParams) => {
|
||||
if (params.game.inSpawnPhase()) {
|
||||
params.playerActionHandler.handleSpawn(params.tile);
|
||||
} else {
|
||||
params.playerActionHandler.handleAttack(
|
||||
params.myPlayer,
|
||||
params.selected?.id() ?? null,
|
||||
);
|
||||
if (isFriendlyTarget(params)) {
|
||||
const selectedPlayer = params.selected as PlayerView;
|
||||
const ratio = params.uiState?.attackRatio ?? 1;
|
||||
const troopsToDonate = Math.floor(ratio * params.myPlayer.troops());
|
||||
if (troopsToDonate > 0) {
|
||||
params.playerActionHandler.handleDonateTroops(
|
||||
selectedPlayer,
|
||||
troopsToDonate,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
params.playerActionHandler.handleAttack(
|
||||
params.myPlayer,
|
||||
params.selected?.id() ?? null,
|
||||
);
|
||||
}
|
||||
}
|
||||
params.closeMenu();
|
||||
},
|
||||
@@ -585,7 +630,13 @@ export const rootMenuElement: MenuElement = {
|
||||
infoMenuElement,
|
||||
...(isOwnTerritory
|
||||
? [deleteUnitElement, ally, buildMenuElement]
|
||||
: [boatMenuElement, ally, attackMenuElement]),
|
||||
: [
|
||||
boatMenuElement,
|
||||
ally,
|
||||
isFriendlyTarget(params)
|
||||
? donateGoldRadialElement
|
||||
: attackMenuElement,
|
||||
]),
|
||||
];
|
||||
|
||||
return menuItems.filter((item): item is MenuElement => item !== null);
|
||||
|
||||
Reference in New Issue
Block a user