From db7a25958713c4d2b28d44344c93911006343e17 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Mon, 7 Oct 2024 20:17:15 -0700 Subject: [PATCH] can donate to allies --- TODO.txt | 1 + src/client/Transport.ts | 24 ++++++++++++- src/client/graphics/layers/RadialMenu.ts | 12 ++++++- src/core/Schemas.ts | 10 ++++++ src/core/configuration/Config.ts | 2 ++ src/core/configuration/DefaultConfig.ts | 8 ++++- src/core/execution/DonateExecution.ts | 46 ++++++++++++++++++++++++ src/core/execution/ExecutionManager.ts | 3 ++ src/core/game/Game.ts | 7 ++-- src/core/game/PlayerImpl.ts | 37 ++++++++++++++++--- 10 files changed, 141 insertions(+), 9 deletions(-) create mode 100644 src/core/execution/DonateExecution.ts diff --git a/TODO.txt b/TODO.txt index 496fa9230..2f0d14036 100644 --- a/TODO.txt +++ b/TODO.txt @@ -161,6 +161,7 @@ * disable select on mobile DONE 10/6/2024 * disable double tap on mobile DONE 10/6/2024 * donate troops button +* rewrite mobile input handling * Make fake humans spawn by their country * fake humans target enemies * create private lobby menu diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 52b10d971..bd3d34dec 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -51,7 +51,18 @@ export class SendTargetPlayerIntentEvent implements GameEvent { } export class SendEmojiIntentEvent implements GameEvent { - constructor(public readonly recipient: Player | typeof AllPlayers, public readonly emoji: string) { } + constructor( + public readonly recipient: Player | typeof AllPlayers, + public readonly emoji: string + ) { } +} + +export class SendDonateIntentEvent implements GameEvent { + constructor( + public readonly sender: Player, + public readonly recipient: Player, + public readonly troops: number | null, + ) { } } export class Transport { @@ -74,6 +85,7 @@ export class Transport { this.eventBus.on(SendBoatAttackIntentEvent, (e) => this.onSendBoatAttackIntent(e)) this.eventBus.on(SendTargetPlayerIntentEvent, (e) => this.onSendTargetPlayerIntent(e)) this.eventBus.on(SendEmojiIntentEvent, (e) => this.onSendEmojiIntent(e)) + this.eventBus.on(SendDonateIntentEvent, (e) => this.onSendDonateIntent(e)) } connect(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) { @@ -216,6 +228,16 @@ export class Transport { }) } + private onSendDonateIntent(event: SendDonateIntentEvent) { + this.sendIntent({ + type: "donate", + clientID: this.clientID, + sender: event.sender.id(), + recipient: event.recipient.id(), + troops: event.troops, + }) + } + private sendIntent(intent: Intent) { if (this.socket.readyState === WebSocket.OPEN) { const msg = ClientIntentMessageSchema.parse({ diff --git a/src/client/graphics/layers/RadialMenu.ts b/src/client/graphics/layers/RadialMenu.ts index 0bd58e9db..fdbfd12a9 100644 --- a/src/client/graphics/layers/RadialMenu.ts +++ b/src/client/graphics/layers/RadialMenu.ts @@ -3,7 +3,7 @@ import {AllPlayers, Cell, Game, Player} from "../../../core/game/Game"; import {ClientID} from "../../../core/Schemas"; import {and, bfs, dist, manhattanDist, manhattanDistWrapped, sourceDstOceanShore} from "../../../core/Util"; import {ContextMenuEvent, MouseUpEvent} from "../../InputHandler"; -import {SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBoatAttackIntentEvent, SendBreakAllianceIntentEvent, SendEmojiIntentEvent, SendSpawnIntentEvent, SendTargetPlayerIntentEvent} from "../../Transport"; +import {SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBoatAttackIntentEvent, SendBreakAllianceIntentEvent, SendDonateIntentEvent, SendEmojiIntentEvent, SendSpawnIntentEvent, SendTargetPlayerIntentEvent} from "../../Transport"; import {TransformHandler} from "../TransformHandler"; import {Layer} from "./Layer"; import * as d3 from 'd3'; @@ -14,6 +14,8 @@ import swordIcon from '../../../../resources/images/SwordIconWhite.png'; import targetIcon from '../../../../resources/images/TargetIconWhite.png'; import emojiIcon from '../../../../resources/images/EmojiIconWhite.png'; import disabledIcon from '../../../../resources/images/DisabledIcon.png'; +import donateIcon from '../../../../resources/images/DonateIconWhite.png'; +import {MessageType} from "./EventsDisplay"; enum Slot { @@ -298,6 +300,14 @@ export class RadialMenu implements Layer { return } + if (myPlayer.canDonate(other)) { + this.activateMenuElement(Slot.Target, "#53ac75", donateIcon, () => { + this.eventBus.emit( + new SendDonateIntentEvent(myPlayer, other, null) + ) + }) + } + if (myPlayer.isAlliedWith(other)) { this.activateMenuElement(Slot.Alliance, "#c74848", traitorIcon, () => { this.eventBus.emit( diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 7133f0d37..575c4b197 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -13,6 +13,7 @@ export type Intent = SpawnIntent | BreakAllianceIntent | TargetPlayerIntent | EmojiIntent + | DonateIntent export type AttackIntent = z.infer export type SpawnIntent = z.infer @@ -23,6 +24,7 @@ export type AllianceRequestReplyIntent = z.infer export type TargetPlayerIntent = z.infer export type EmojiIntent = z.infer +export type DonateIntent = z.infer export type Turn = z.infer @@ -128,6 +130,13 @@ export const EmojiIntentSchema = BaseIntentSchema.extend({ emoji: EmojiSchema, }) +export const DonateIntentSchema = BaseIntentSchema.extend({ + type: z.literal('donate'), + sender: z.string(), + recipient: z.string(), + troops: z.number().nullable(), +}) + const IntentSchema = z.union([ AttackIntentSchema, SpawnIntentSchema, @@ -138,6 +147,7 @@ const IntentSchema = z.union([ BreakAllianceIntentSchema, TargetPlayerIntentSchema, EmojiIntentSchema, + DonateIntentSchema, ]); const TurnSchema = z.object({ diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 4f5c72030..5b45b63a2 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -53,6 +53,8 @@ export interface Config { targetCooldown(): Tick emojiMessageCooldown(): Tick emojiMessageDuration(): Tick + donateCooldown(): Tick + defaultDonationAmount(sender: Player): number } export interface Theme { diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 80af01d9f..65018cb4f 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -7,6 +7,12 @@ import {pastelTheme} from "./PastelTheme"; export class DefaultConfig implements Config { + defaultDonationAmount(sender: Player): number { + return Math.floor(sender.troops() / 3) + } + donateCooldown(): Tick { + return 10 * 10 + } emojiMessageDuration(): Tick { return 5 * 10 } @@ -114,7 +120,7 @@ export class DefaultConfig implements Config { } boatAttackAmount(attacker: Player, defender: Player | TerraNullius): number { - return attacker.troops() / 5 + return Math.floor(attacker.troops() / 5) } attackAmount(attacker: Player, defender: Player | TerraNullius) { diff --git a/src/core/execution/DonateExecution.ts b/src/core/execution/DonateExecution.ts new file mode 100644 index 000000000..995c63e3b --- /dev/null +++ b/src/core/execution/DonateExecution.ts @@ -0,0 +1,46 @@ +import {AllPlayers, Execution, MutableGame, MutablePlayer, PlayerID} from "../game/Game"; + +export class DonateExecution implements Execution { + + private sender: MutablePlayer + private recipient: MutablePlayer + + private active = true + + constructor( + private senderID: PlayerID, + private recipientID: PlayerID, + private troops: number | null + ) { } + + + init(mg: MutableGame, ticks: number): void { + this.sender = mg.player(this.senderID) + this.recipient = mg.player(this.recipientID) + if (this.troops == null) { + this.troops = mg.config().defaultDonationAmount(this.sender) + } + } + + tick(ticks: number): void { + if (this.sender.canDonate(this.recipient)) { + this.sender.donate(this.recipient, this.troops) + } else { + console.warn(`cannot send tropps from ${this.sender} to ${this.recipient}`) + } + this.active = false + } + + owner(): MutablePlayer { + return null + } + + isActive(): boolean { + return this.active + } + + activeDuringSpawnPhase(): boolean { + return false + } + +} \ No newline at end of file diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index ac2223635..3af8f2149 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -14,6 +14,7 @@ import {AllianceRequestReplyExecution} from "./alliance/AllianceRequestReplyExec import {BreakAllianceExecution} from "./alliance/BreakAllianceExecution"; import {TargetPlayerExecution} from "./TargetPlayerExecution"; import {EmojiExecution} from "./EmojiExecution"; +import {DonateExecution} from "./DonateExecution"; @@ -70,6 +71,8 @@ export class Executor { return new TargetPlayerExecution(intent.requestor, intent.target) } else if (intent.type == "emoji") { return new EmojiExecution(intent.sender, intent.recipient, intent.emoji) + } else if (intent.type == "donate") { + return new DonateExecution(intent.sender, intent.recipient, intent.troops) } else { throw new Error(`intent type ${intent} not found`) diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index a37ee73fb..75c5579a2 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -4,6 +4,7 @@ import {GameEvent} from "../EventBus" import {ClientID, GameID} from "../Schemas" import {DisplayMessageEvent, MessageType} from "../../client/graphics/layers/EventsDisplay" import {BreakAllianceExecution} from "../execution/alliance/BreakAllianceExecution" +import {DonateExecution} from "../execution/DonateExecution" export type PlayerID = string export type Tick = number @@ -159,6 +160,7 @@ export interface Player { isAlliedWith(other: Player): boolean allianceWith(other: Player): Alliance | null // Includes recent requests that are in cooldown + // TODO: why can't I have "canSendAllyRequest" function instead? recentOrPendingAllianceRequestWith(other: Player): boolean isTraitor(): boolean canTarget(other: Player): boolean @@ -169,13 +171,14 @@ export interface Player { toString(): string canSendEmoji(recipient: Player | typeof AllPlayers): boolean outgoingEmojis(): EmojiMessage[] + canDonate(recipient: Player): boolean } export interface MutablePlayer extends Player { setName(name: string): void setTroops(troops: number): void addTroops(troops: number): void - removeTroops(troops: number): void + removeTroops(troops: number): number conquer(tile: Tile): void relinquish(tile: Tile): void executions(): Execution[] @@ -191,8 +194,8 @@ export interface MutablePlayer extends Player { target(other: Player): void targets(): MutablePlayer[] transitiveTargets(): MutablePlayer[] - // Null means send to all Players sendEmoji(recipient: Player | typeof AllPlayers, emoji: string): void + donate(recipient: MutablePlayer, troops: number): void } export interface Game { diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index dfea968f7..5cfd738cf 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -5,13 +5,18 @@ import {CellString, GameImpl} from "./GameImpl"; import {BoatImpl} from "./BoatImpl"; import {TileImpl} from "./TileImpl"; import {TerraNulliusImpl} from "./TerraNulliusImpl"; -import {threadId} from "worker_threads"; +import {MessageType} from "../../client/graphics/layers/EventsDisplay"; +import {renderTroops} from "../../client/graphics/Utils"; interface Target { tick: Tick target: Player } +class Donation { + constructor(public readonly recipient: Player, public readonly tick: Tick) { } +} + export class PlayerImpl implements MutablePlayer { isTraitor_ = false @@ -28,6 +33,8 @@ export class PlayerImpl implements MutablePlayer { private outgoingEmojis_: EmojiMessage[] = [] + private sentDonations: Donation[] = [] + constructor(private gs: GameImpl, private readonly playerInfo: PlayerInfo, private _troops) { this._name = playerInfo.name; } @@ -99,9 +106,10 @@ export class PlayerImpl implements MutablePlayer { addTroops(troops: number): void { this._troops += Math.floor(troops); } - removeTroops(troops: number): void { - this._troops -= Math.floor(troops); - this._troops = Math.max(this._troops, 0); + removeTroops(troops: number): number { + const toRemove = Math.floor(Math.min(this._troops, troops)) + this._troops -= toRemove; + return toRemove } isPlayer(): this is MutablePlayer {return true as const;} @@ -234,6 +242,27 @@ export class PlayerImpl implements MutablePlayer { return true } + canDonate(recipient: Player): boolean { + if (!this.isAlliedWith(recipient)) { + return false + } + for (const donation of this.sentDonations) { + if (donation.recipient == recipient) { + if (this.gs.ticks() - donation.tick < this.gs.config().donateCooldown()) { + return false + } + } + } + return true + } + + donate(recipient: MutablePlayer, troops: number): void { + this.sentDonations.push(new Donation(recipient, this.gs.ticks())) + recipient.addTroops(this.removeTroops(troops)) + this.gs.displayMessage(`Sent ${renderTroops(troops)} troops to ${recipient.name()}`, MessageType.INFO, this.id()) + this.gs.displayMessage(`Recieved ${renderTroops(troops)} troops from ${this.name()}`, MessageType.SUCCESS, recipient.id()) + } + hash(): number { return simpleHash(this.id()) * (this.troops() + this.numTilesOwned()); }