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 000000000..f480a40e3 Binary files /dev/null and b/resources/images/EmojiIconWhite.png differ 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()); }