diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 3b84c2e65..8a22fb98d 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -43,6 +43,12 @@ export class SendBoatAttackIntentEvent implements GameEvent { ) { } } +export class SendTargetPlayerIntentEvent implements GameEvent { + constructor( + public readonly targetID: PlayerID, + ) { } +} + export class Transport { public onconnect: () => {} @@ -61,6 +67,7 @@ export class Transport { this.eventBus.on(SendSpawnIntentEvent, (e) => this.onSendSpawnIntentEvent(e)) this.eventBus.on(SendAttackIntentEvent, (e) => this.onSendAttackIntent(e)) this.eventBus.on(SendBoatAttackIntentEvent, (e) => this.onSendBoatAttackIntent(e)) + this.eventBus.on(SendTargetPlayerIntentEvent, (e) => this.onSendTargetPlayerIntent(e)) } connect(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) { @@ -184,6 +191,15 @@ export class Transport { }) } + private onSendTargetPlayerIntent(event: SendTargetPlayerIntentEvent) { + this.sendIntent({ + type: "targetPlayer", + clientID: this.clientID, + requestor: this.playerID, + target: event.targetID, + }) + } + private sendIntent(intent: Intent) { if (this.socket.readyState === WebSocket.OPEN) { const msg = ClientIntentMessageSchema.parse({ diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts index 529b5b2c0..4025a901c 100644 --- a/src/client/graphics/layers/NameLayer.ts +++ b/src/client/graphics/layers/NameLayer.ts @@ -9,6 +9,7 @@ import {renderTroops} from "../Utils" import traitorIcon from '../../../../resources/images/TraitorIcon.png'; import allianceIcon from '../../../../resources/images/AllianceIcon.png'; import crownIcon from '../../../../resources/images/CrownIcon.png'; +import targetIcon from '../../../../resources/images/TargetIcon.png'; import {ClientID} from "../../../core/Schemas" @@ -34,6 +35,7 @@ export class NameLayer implements Layer { private seenPlayers: Set = new Set() private traitorIconImage: HTMLImageElement; private allianceIconImage: HTMLImageElement; + private targetIconImage: HTMLImageElement; private crownIconImage: HTMLImageElement; private myPlayer: Player | null = null @@ -49,6 +51,9 @@ export class NameLayer implements Layer { this.crownIconImage = new Image() this.crownIconImage.src = crownIcon + + this.targetIconImage = new Image() + this.targetIconImage.src = targetIcon } shouldTransform(): boolean { @@ -173,6 +178,16 @@ export class NameLayer implements Layer { ); } + if (new Set(myPlayer.transitiveTargets()).has(render.player)) { + context.drawImage( + this.targetIconImage, + nameCenterX - iconSize / 2, + nameCenterY - iconSize / 2, + iconSize, + iconSize + ); + } + context.textRendering = "optimizeSpeed"; context.font = `${render.fontSize}px ${this.theme.font()}`; diff --git a/src/client/graphics/layers/RadialMenu.ts b/src/client/graphics/layers/RadialMenu.ts index 2191e4ccb..9f346531e 100644 --- a/src/client/graphics/layers/RadialMenu.ts +++ b/src/client/graphics/layers/RadialMenu.ts @@ -3,7 +3,7 @@ import {Cell, Game, Player, PlayerID} from "../../../core/game/Game"; import {ClientID} from "../../../core/Schemas"; import {manhattanDist, manhattanDistWrapped, sourceDstOceanShore} from "../../../core/Util"; import {ContextMenuEvent, MouseUpEvent} from "../../InputHandler"; -import {SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBoatAttackIntentEvent, SendBreakAllianceIntentEvent, SendSpawnIntentEvent} from "../../Transport"; +import {SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBoatAttackIntentEvent, SendBreakAllianceIntentEvent, SendSpawnIntentEvent, SendTargetPlayerIntentEvent} from "../../Transport"; import {TransformHandler} from "../TransformHandler"; import {MessageType} from "./EventsDisplay"; import {Layer} from "./Layer"; @@ -254,6 +254,11 @@ export class RadialMenu implements Layer { new SendAllianceRequestIntentEvent(myPlayer, other) ) }) + this.activateMenuElement(Slot.Target, "#c74848", targetIcon, () => { + this.eventBus.emit( + new SendTargetPlayerIntentEvent(other.id()) + ) + }) } } diff --git a/src/client/graphics/layers/UILayer.ts b/src/client/graphics/layers/UILayer.ts index 416a89898..34d49234c 100644 --- a/src/client/graphics/layers/UILayer.ts +++ b/src/client/graphics/layers/UILayer.ts @@ -7,7 +7,7 @@ import {ContextMenuEvent} from "../../InputHandler"; import {Layer} from "./Layer"; import {TransformHandler} from "../TransformHandler"; import {MessageType} from "./EventsDisplay"; -import {SendAllianceRequestIntentEvent, SendBreakAllianceIntentEvent} from "../../Transport"; +import {SendBreakAllianceIntentEvent} from "../../Transport"; interface MenuOption { label: string; diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 38a2f1708..00cc524f8 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -11,6 +11,7 @@ export type Intent = SpawnIntent | AllianceRequestIntent | AllianceRequestReplyIntent | BreakAllianceIntent + | TargetPlayerIntent export type AttackIntent = z.infer export type SpawnIntent = z.infer @@ -19,6 +20,7 @@ export type UpdateNameIntent = z.infer export type AllianceRequestIntent = z.infer export type AllianceRequestReplyIntent = z.infer export type BreakAllianceIntent = z.infer +export type TargetPlayerIntent = z.infer export type Turn = z.infer @@ -104,6 +106,12 @@ export const BreakAllianceIntentSchema = BaseIntentSchema.extend({ recipient: z.string(), }) +export const TargetPlayerIntentSchema = BaseIntentSchema.extend({ + type: z.literal('targetPlayer'), + requestor: z.string(), + target: z.string(), +}) + const IntentSchema = z.union([ AttackIntentSchema, SpawnIntentSchema, @@ -111,7 +119,8 @@ const IntentSchema = z.union([ UpdateNameIntentSchema, AllianceRequestIntentSchema, AllianceRequestReplyIntentSchema, - BreakAllianceIntentSchema + BreakAllianceIntentSchema, + TargetPlayerIntentSchema, ]); const TurnSchema = z.object({ diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 0c7385bcf..8e15e31ab 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -49,6 +49,8 @@ export interface Config { boatMaxNumber(): number allianceDuration(): Tick allianceRequestCooldown(): Tick + targetDuration(): Tick + targetCooldown(): Tick } export interface Theme { diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 97d28a3bb..6d17c24bb 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 { + targetDuration(): Tick { + return 10 * 10 + } + targetCooldown(): Tick { + return 30 * 10 + } allianceRequestCooldown(): Tick { return 30 * 10 } diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 03c696189..a925d7a94 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -1,20 +1,18 @@ -import {Tick} from "../game/Game"; -import {GameID} from "../Schemas"; import {DefaultConfig} from "./DefaultConfig"; export const devConfig = new class extends DefaultConfig { percentageTilesOwnedToWin(): number { return 95 } - numSpawnPhaseTurns(): number { - return 40 - } - gameCreationRate(): number { - return 2 * 1000 - } - lobbyLifetime(): number { - return 2 * 1000 - } + // numSpawnPhaseTurns(): number { + // return 40 + // } + // gameCreationRate(): number { + // return 2 * 1000 + // } + // lobbyLifetime(): number { + // return 2 * 1000 + // } turnIntervalMs(): number { return 100 } diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index ab09f0d41..266f77da7 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -12,6 +12,7 @@ import {simpleHash} from "../Util"; import {AllianceRequestExecution} from "./alliance/AllianceRequestExecution"; import {AllianceRequestReplyExecution} from "./alliance/AllianceRequestReplyExecution"; import {BreakAllianceExecution} from "./alliance/BreakAllianceExecution"; +import {TargetPlayerExecution} from "./TargetPlayerExecution"; @@ -64,6 +65,8 @@ export class Executor { return new AllianceRequestReplyExecution(intent.requestor, intent.recipient, intent.accept) } else if (intent.type == "breakAlliance") { return new BreakAllianceExecution(intent.requestor, intent.recipient) + } else if (intent.type == "targetPlayer") { + return new TargetPlayerExecution(intent.requestor, intent.target) } else { throw new Error(`intent type ${intent} not found`) diff --git a/src/core/execution/TargetPlayerExecution.ts b/src/core/execution/TargetPlayerExecution.ts new file mode 100644 index 000000000..60ae31c28 --- /dev/null +++ b/src/core/execution/TargetPlayerExecution.ts @@ -0,0 +1,37 @@ +import {Execution, MutableGame, MutablePlayer, PlayerID} from "../game/Game"; + +export class TargetPlayerExecution implements Execution { + + private requestor: MutablePlayer + private target: MutablePlayer + + private active = true + + constructor(private requestorID: PlayerID, private targetID: PlayerID) { } + + + init(mg: MutableGame, ticks: number): void { + this.requestor = mg.player(this.requestorID) + this.target = mg.player(this.targetID) + } + + tick(ticks: number): void { + if (this.requestor.canTarget(this.target)) { + this.requestor.target(this.target) + } + 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/game/AllianceImpl.ts b/src/core/game/AllianceImpl.ts index 7a941767c..3624d06e5 100644 --- a/src/core/game/AllianceImpl.ts +++ b/src/core/game/AllianceImpl.ts @@ -10,6 +10,13 @@ export class AllianceImpl implements MutableAlliance { readonly createdAtTick_: Tick, ) { } + other(player: Player): PlayerImpl { + if (this.requestor_ == player) { + return this.recipient_ + } + return this.requestor_ + } + requestor(): MutablePlayer { return this.requestor_ } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 96142795e..bb54a4178 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -64,10 +64,12 @@ export interface Alliance { requestor(): Player recipient(): Player createdAt(): Tick + other(player: Player): Player } export interface MutableAlliance extends Alliance { expire(): void + other(player: Player): MutablePlayer } export class PlayerInfo { @@ -148,6 +150,11 @@ export interface Player { // Includes recent requests that are in cooldown recentOrPendingAllianceRequestWith(other: Player): boolean isTraitor(): boolean + canTarget(other: Player): boolean + // Targets for this player + targets(): Player[] + // Targets of player and all allies. + transitiveTargets(): Player[] toString(): string } @@ -168,6 +175,9 @@ export interface MutablePlayer extends Player { breakAlliance(alliance: Alliance): void createAllianceRequest(recipient: Player): MutableAllianceRequest addBoat(troops: number, tile: Tile, target: Player | TerraNullius): MutableBoat + target(other: Player): void + targets(): MutablePlayer[] + transitiveTargets(): MutablePlayer[] } export interface Game { diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index b6fea5825..4e2543604 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} from "./Game"; +import {MutablePlayer, Tile, PlayerInfo, PlayerID, PlayerType, Player, TerraNullius, Cell, MutableGame, Execution, AllianceRequest, MutableAllianceRequest, MutableAlliance, Alliance, Tick} from "./Game"; import {ClientID} from "../Schemas"; import {simpleHash} from "../Util"; import {CellString, GameImpl} from "./GameImpl"; @@ -7,6 +7,10 @@ import {TileImpl} from "./TileImpl"; import {TerraNulliusImpl} from "./TerraNulliusImpl"; import {threadId} from "worker_threads"; +interface Target { + tick: Tick + target: Player +} export class PlayerImpl implements MutablePlayer { isTraitor_ = false @@ -20,6 +24,8 @@ export class PlayerImpl implements MutablePlayer { public pastOutgoingAllianceRequests: AllianceRequest[] = [] + private targets_: Target[] = [] + constructor(private gs: GameImpl, private readonly playerInfo: PlayerInfo, private _troops) { this._name = playerInfo.name; } @@ -172,6 +178,33 @@ export class PlayerImpl implements MutablePlayer { return this.gs.createAllianceRequest(this, recipient) } + canTarget(other: Player): boolean { + for (const t of this.targets_) { + if (t.target == other) { + if (this.gs.ticks() - t.tick < this.gs.config().targetCooldown()) { + return false + } + } + } + return true + } + + target(other: Player): void { + this.targets_.push({tick: this.gs.ticks(), target: other}) + } + + targets(): PlayerImpl[] { + return this.targets_ + .filter(t => this.gs.ticks() - t.tick < this.gs.config().targetDuration()) + .map(t => t.target as PlayerImpl) + } + + transitiveTargets(): MutablePlayer[] { + const ts = this.alliances().map(a => a.other(this)).flatMap(ally => ally.targets()) + ts.push(...this.targets()) + return [...new Set(ts)] + } + hash(): number { return simpleHash(this.id()) * (this.troops() + this.numTilesOwned()); }