From e795b2222030bce0f43996e8dc356b6c199d7dfe Mon Sep 17 00:00:00 2001 From: evanpelle Date: Fri, 4 Oct 2024 13:08:20 -0700 Subject: [PATCH] send fire emoji to other players --- TODO.txt | 6 +-- resources/images/EmojiIconWhite.png | Bin 0 -> 13751 bytes src/client/Transport.ts | 18 +++++++- src/client/graphics/layers/EventsDisplay.ts | 18 +++++++- src/client/graphics/layers/RadialMenu.ts | 25 +++++++----- src/core/Schemas.ts | 19 +++++++-- src/core/configuration/Config.ts | 2 + src/core/configuration/DefaultConfig.ts | 6 +++ src/core/configuration/DevConfig.ts | 14 +++---- src/core/execution/EmojiExecution.ts | 43 ++++++++++++++++++++ src/core/execution/ExecutionManager.ts | 3 ++ src/core/game/Game.ts | 28 +++++++++++++ src/core/game/PlayerImpl.ts | 27 +++++++++++- 13 files changed, 182 insertions(+), 27 deletions(-) create mode 100644 resources/images/EmojiIconWhite.png create mode 100644 src/core/execution/EmojiExecution.ts diff --git a/TODO.txt b/TODO.txt index bef78fde6..362545bee 100644 --- a/TODO.txt +++ b/TODO.txt @@ -157,17 +157,17 @@ * BUG: boat icon appears when click inland DONE 10/2/2024 * Auto deploy to dev on commit DONE 10/2/2024 * add emoji button +* make disabled icon crossed out icon +* donate troops button * Make fake humans spawn by their country * BUG: double tap zooms on mobile * fake humans target enemies -* make year clock +* create private lobby menu * block user inputs if too far behind server * BUG: FakeHuman don't be enemy if don't share border (or send boat) * store cookies * BUG: names dissapear on bottom of screen * UI: leader board -* UI: boats -* UI: current attacks * Load terrain dataImage in background * BUG: half encircle Antartica causes capture diff --git a/resources/images/EmojiIconWhite.png b/resources/images/EmojiIconWhite.png new file mode 100644 index 0000000000000000000000000000000000000000..f480a40e395095229179fe3f348871f52b695e57 GIT binary patch literal 13751 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 O0%vuNbt<$jQ~nQYIfxkm literal 0 HcmV?d00001 diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 8a22fb98d..190b7020f 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -1,5 +1,5 @@ import {EventBus, GameEvent} from "../core/EventBus" -import {AllianceRequest, Cell, Game, Player, PlayerID, PlayerType} from "../core/game/Game" +import {AllianceRequest, AllPlayers, Cell, Emoji, Player, PlayerID, PlayerType} from "../core/game/Game" import {ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema} from "../core/Schemas" @@ -9,6 +9,7 @@ export class SendAllianceRequestIntentEvent implements GameEvent { public readonly recipient: Player ) { } } + export class SendBreakAllianceIntentEvent implements GameEvent { constructor( public readonly requestor: Player, @@ -49,6 +50,10 @@ export class SendTargetPlayerIntentEvent implements GameEvent { ) { } } +export class SendEmojiIntentEvent implements GameEvent { + constructor(public readonly recipient: Player | typeof AllPlayers, public readonly emoji: Emoji) { } +} + export class Transport { public onconnect: () => {} @@ -68,6 +73,7 @@ export class Transport { this.eventBus.on(SendAttackIntentEvent, (e) => this.onSendAttackIntent(e)) this.eventBus.on(SendBoatAttackIntentEvent, (e) => this.onSendBoatAttackIntent(e)) this.eventBus.on(SendTargetPlayerIntentEvent, (e) => this.onSendTargetPlayerIntent(e)) + this.eventBus.on(SendEmojiIntentEvent, (e) => this.onSendEmojiIntent(e)) } connect(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) { @@ -200,6 +206,16 @@ export class Transport { }) } + private onSendEmojiIntent(event: SendEmojiIntentEvent) { + this.sendIntent({ + type: "emoji", + clientID: this.clientID, + sender: this.playerID, + recipient: event.recipient == AllPlayers ? AllPlayers : event.recipient.id(), + emoji: event.emoji + }) + } + private sendIntent(intent: Intent) { if (this.socket.readyState === WebSocket.OPEN) { const msg = ClientIntentMessageSchema.parse({ diff --git a/src/client/graphics/layers/EventsDisplay.ts b/src/client/graphics/layers/EventsDisplay.ts index 198769760..e090cf5e1 100644 --- a/src/client/graphics/layers/EventsDisplay.ts +++ b/src/client/graphics/layers/EventsDisplay.ts @@ -1,6 +1,6 @@ import {nullable} from "zod"; import {EventBus, GameEvent} from "../../../core/EventBus"; -import {AllianceExpiredEvent, AllianceRequestEvent, AllianceRequestReplyEvent, BrokeAllianceEvent, Game, Player, PlayerID, TargetPlayerEvent} from "../../../core/game/Game"; +import {AllianceExpiredEvent, AllianceRequestEvent, AllianceRequestReplyEvent, BrokeAllianceEvent, EmojiMessageEvent, Game, Player, PlayerID, TargetPlayerEvent} from "../../../core/game/Game"; import {ClientID} from "../../../core/Schemas"; import {Layer} from "./Layer"; import {SendAllianceReplyIntentEvent} from "../../Transport"; @@ -53,6 +53,7 @@ export class EventsDisplay implements Layer { this.eventBus.on(BrokeAllianceEvent, e => this.onBrokeAllianceEvent(e)) this.eventBus.on(AllianceExpiredEvent, e => this.onAllianceExpiredEvent(e)) this.eventBus.on(TargetPlayerEvent, e => this.onTargetPlayerEvent(e)) + this.eventBus.on(EmojiMessageEvent, e => this.onEmojiMessageEvent(e)) this.renderTable() } @@ -226,6 +227,21 @@ export class EventsDisplay implements Layer { } } + onEmojiMessageEvent(event: EmojiMessageEvent) { + const myPlayer = this.game.playerByClientID(this.clientID) + if (myPlayer == null) { + return + } + if (event.message.recipient == myPlayer) { + this.addEvent({ + description: `${event.message.sender.name()}:${event.message.emoji}`, + type: MessageType.INFO, + highlight: true, + createdAt: this.game.ticks(), + }) + } + } + addEvent(event: Event): void { this.events.push(event); this.renderTable() diff --git a/src/client/graphics/layers/RadialMenu.ts b/src/client/graphics/layers/RadialMenu.ts index d2c6fc601..fc6502fb0 100644 --- a/src/client/graphics/layers/RadialMenu.ts +++ b/src/client/graphics/layers/RadialMenu.ts @@ -1,9 +1,9 @@ import {EventBus} from "../../../core/EventBus"; -import {Cell, Game, Player, PlayerID} from "../../../core/game/Game"; +import {AllPlayers, Cell, Emoji, Game, Player, PlayerID} 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, SendSpawnIntentEvent, SendTargetPlayerIntentEvent} from "../../Transport"; +import {SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBoatAttackIntentEvent, SendBreakAllianceIntentEvent, SendEmojiIntentEvent, SendSpawnIntentEvent, SendTargetPlayerIntentEvent} from "../../Transport"; import {TransformHandler} from "../TransformHandler"; import {MessageType} from "./EventsDisplay"; import {Layer} from "./Layer"; @@ -13,20 +13,14 @@ import allianceIcon from '../../../../resources/images/AllianceIconWhite.png'; import boatIcon from '../../../../resources/images/BoatIconWhite.png'; import swordIcon from '../../../../resources/images/SwordIconWhite.png'; import targetIcon from '../../../../resources/images/TargetIconWhite.png'; +import emojiIcon from '../../../../resources/images/EmojiIconWhite.png'; -enum RadialElement { - RequestAlliance, - BreakAlliance, - BoatAttack, - Target, -} - enum Slot { Alliance, Boat, Target, - FOURTH + Emoji } export class RadialMenu implements Layer { @@ -38,7 +32,7 @@ export class RadialMenu implements Layer { [Slot.Alliance, {name: "alliance", disabled: true, action: () => { }, color: null, icon: null, defaultIcon: allianceIcon}], [Slot.Boat, {name: "boat", disabled: true, action: () => { }, color: null, icon: null, defaultIcon: boatIcon}], [Slot.Target, {name: "target", disabled: true, action: () => { }, defaultIcon: targetIcon}], - + [Slot.Emoji, {name: "emoji", disabled: true, action: () => { }, defaultIcon: emojiIcon}], ]); private readonly menuSize = 190; @@ -230,6 +224,15 @@ export class RadialMenu implements Layer { return } + if (tile.hasOwner()) { + const target = tile.owner() == myPlayer ? AllPlayers : (tile.owner() as Player) + this.activateMenuElement(Slot.Emoji, "#ebe250", emojiIcon, () => { + this.eventBus.emit( + new SendEmojiIntentEvent(target, Emoji.Fire) + ) + }) + } + if (tile.owner() != myPlayer && tile.isLand() && myPlayer.sharesBorderWith(other)) { if (other.isPlayer()) { if (!myPlayer.isAlliedWith(other)) { diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 00cc524f8..4de8c1c25 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -1,5 +1,5 @@ import {z} from 'zod'; -import {PlayerType} from './game/Game'; +import {Emoji, PlayerType} from './game/Game'; export type GameID = string export type ClientID = string @@ -12,6 +12,7 @@ export type Intent = SpawnIntent | AllianceRequestReplyIntent | BreakAllianceIntent | TargetPlayerIntent + | EmojiIntent export type AttackIntent = z.infer export type SpawnIntent = z.infer @@ -21,7 +22,7 @@ export type AllianceRequestIntent = z.infer export type AllianceRequestReplyIntent = z.infer export type BreakAllianceIntent = z.infer export type TargetPlayerIntent = z.infer - +export type EmojiIntent = z.infer export type Turn = z.infer @@ -37,6 +38,8 @@ export type ClientJoinMessage = z.infer export type ClientLeaveMessage = z.infer const PlayerTypeSchema = z.nativeEnum(PlayerType); +const EmojiSchema = z.nativeEnum(Emoji); + // TODO: create Cell schema @@ -46,9 +49,11 @@ export interface Lobby { numClients: number; } + + // Zod schemas const BaseIntentSchema = z.object({ - type: z.enum(['attack', 'spawn', 'boat', 'name']), + type: z.enum(['attack', 'spawn', 'boat', 'name', 'targetPlayer', 'emoji']), clientID: z.string(), }); @@ -112,6 +117,13 @@ export const TargetPlayerIntentSchema = BaseIntentSchema.extend({ target: z.string(), }) +export const EmojiIntentSchema = BaseIntentSchema.extend({ + type: z.literal('emoji'), + sender: z.string(), + recipient: z.string(), + emoji: EmojiSchema, +}) + const IntentSchema = z.union([ AttackIntentSchema, SpawnIntentSchema, @@ -121,6 +133,7 @@ const IntentSchema = z.union([ AllianceRequestReplyIntentSchema, BreakAllianceIntentSchema, TargetPlayerIntentSchema, + EmojiIntentSchema, ]); const TurnSchema = z.object({ diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 8e15e31ab..4f5c72030 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -51,6 +51,8 @@ export interface Config { allianceRequestCooldown(): Tick targetDuration(): Tick targetCooldown(): Tick + emojiMessageCooldown(): Tick + emojiMessageDuration(): Tick } export interface Theme { diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 7e2c86e71..995fef196 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 { + emojiMessageDuration(): Tick { + return 5 * 10 + } + emojiMessageCooldown(): Tick { + return 15 * 10 + } targetDuration(): Tick { return 10 * 10 } diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index e2824aadf..5912d6eea 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -5,14 +5,14 @@ export const devConfig = new class extends DefaultConfig { return 95 } numSpawnPhaseTurns(): number { - return 40 - } - gameCreationRate(): number { - return 2 * 1000 - } - lobbyLifetime(): number { - return 2 * 1000 + return 80 } + // gameCreationRate(): number { + // return 2 * 1000 + // } + // lobbyLifetime(): number { + // return 2 * 1000 + // } turnIntervalMs(): number { return 100 } diff --git a/src/core/execution/EmojiExecution.ts b/src/core/execution/EmojiExecution.ts new file mode 100644 index 000000000..f526fb76b --- /dev/null +++ b/src/core/execution/EmojiExecution.ts @@ -0,0 +1,43 @@ +import {AllPlayers, Emoji, Execution, MutableGame, MutablePlayer, PlayerID} from "../game/Game"; + +export class EmojiExecution implements Execution { + + private requestor: MutablePlayer + private recipient: MutablePlayer | typeof AllPlayers + + private active = true + + constructor( + private senderID: PlayerID, + private recipientID: PlayerID | typeof AllPlayers, + private emoji: Emoji + ) { } + + + init(mg: MutableGame, ticks: number): void { + this.requestor = mg.player(this.senderID) + this.recipient = this.recipientID == AllPlayers ? AllPlayers : mg.player(this.recipientID) + } + + tick(ticks: number): void { + if (this.requestor.canSendEmoji(this.recipient)) { + this.requestor.sendEmoji(this.recipient, this.emoji) + } else { + console.warn(`cannot send emoji from ${this.requestor} 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 266f77da7..ac2223635 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -13,6 +13,7 @@ import {AllianceRequestExecution} from "./alliance/AllianceRequestExecution"; import {AllianceRequestReplyExecution} from "./alliance/AllianceRequestReplyExecution"; import {BreakAllianceExecution} from "./alliance/BreakAllianceExecution"; import {TargetPlayerExecution} from "./TargetPlayerExecution"; +import {EmojiExecution} from "./EmojiExecution"; @@ -67,6 +68,8 @@ export class Executor { return new BreakAllianceExecution(intent.requestor, intent.recipient) } else if (intent.type == "targetPlayer") { return new TargetPlayerExecution(intent.requestor, intent.target) + } else if (intent.type == "emoji") { + return new EmojiExecution(intent.sender, intent.recipient, intent.emoji) } else { throw new Error(`intent type ${intent} not found`) diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 0061058af..6b83630c0 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -8,6 +8,26 @@ import {BreakAllianceExecution} from "../execution/alliance/BreakAllianceExecuti export type PlayerID = string export type Tick = number +export enum Emoji { + ThumbsUp = "👍", + ThumbsDown = "👎", + Smile = "😊", + Sad = "😢", + Heart = "❤️", + Fire = "🔥", +} + +export const AllPlayers = "AllPlayers" as const; + +export class EmojiMessage { + constructor( + public readonly sender: Player, + public readonly recipient: Player | typeof AllPlayers, + public readonly emoji: Emoji, + public readonly createdAt: Tick + ) { } +} + export class Cell { private strRepr: string @@ -156,6 +176,8 @@ export interface Player { // Targets of player and all allies. transitiveTargets(): Player[] toString(): string + canSendEmoji(recipient: Player | typeof AllPlayers): boolean + outgoingEmojis(): EmojiMessage[] } export interface MutablePlayer extends Player { @@ -178,6 +200,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: Emoji): void } export interface Game { @@ -241,4 +265,8 @@ export class AllianceExpiredEvent implements GameEvent { export class TargetPlayerEvent implements GameEvent { constructor(public readonly player: Player, public readonly target: Player) { } +} + +export class EmojiMessageEvent implements GameEvent { + constructor(public readonly message: EmojiMessage) { } } \ No newline at end of file diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 13483f085..84598f027 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -1,4 +1,4 @@ -import {MutablePlayer, Tile, PlayerInfo, PlayerID, PlayerType, Player, TerraNullius, Cell, MutableGame, Execution, AllianceRequest, MutableAllianceRequest, MutableAlliance, Alliance, Tick, TargetPlayerEvent} from "./Game"; +import {MutablePlayer, Tile, PlayerInfo, PlayerID, PlayerType, Player, TerraNullius, Cell, MutableGame, Execution, AllianceRequest, MutableAllianceRequest, MutableAlliance, Alliance, Tick, TargetPlayerEvent, Emoji, EmojiMessage, EmojiMessageEvent, AllPlayers} from "./Game"; import {ClientID} from "../Schemas"; import {simpleHash} from "../Util"; import {CellString, GameImpl} from "./GameImpl"; @@ -26,6 +26,8 @@ export class PlayerImpl implements MutablePlayer { private targets_: Target[] = [] + private outgoingEmojis_: EmojiMessage[] = [] + constructor(private gs: GameImpl, private readonly playerInfo: PlayerInfo, private _troops) { this._name = playerInfo.name; } @@ -207,6 +209,29 @@ export class PlayerImpl implements MutablePlayer { return [...new Set(ts)] } + sendEmoji(recipient: Player | typeof AllPlayers, emoji: Emoji): void { + if (recipient == this) { + throw Error(`Cannot send emoji to oneself: ${this}`) + } + const msg = new EmojiMessage(this, recipient, emoji, this.gs.ticks()) + this.outgoingEmojis_.push(msg) + this.gs.eventBus.emit(new EmojiMessageEvent(msg)) + } + + outgoingEmojis(): EmojiMessage[] { + return null + } + + canSendEmoji(recipient: Player | null): boolean { + const prevMsgs = this.outgoingEmojis_.filter(msg => msg.recipient == recipient) + for (const msg of prevMsgs) { + if (this.gs.ticks() - msg.createdAt < this.gs.config().emojiMessageCooldown()) { + return false + } + } + return true + } + hash(): number { return simpleHash(this.id()) * (this.troops() + this.numTilesOwned()); }