Target player creates target icon

This commit is contained in:
evanpelle
2024-10-02 16:25:13 -07:00
parent 8c81a75a53
commit 2223e40d53
13 changed files with 156 additions and 15 deletions
+16
View File
@@ -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({
+15
View File
@@ -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<Player> = 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()}`;
+6 -1
View File
@@ -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())
)
})
}
}
+1 -1
View File
@@ -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;
+10 -1
View File
@@ -11,6 +11,7 @@ export type Intent = SpawnIntent
| AllianceRequestIntent
| AllianceRequestReplyIntent
| BreakAllianceIntent
| TargetPlayerIntent
export type AttackIntent = z.infer<typeof AttackIntentSchema>
export type SpawnIntent = z.infer<typeof SpawnIntentSchema>
@@ -19,6 +20,7 @@ export type UpdateNameIntent = z.infer<typeof UpdateNameIntentSchema>
export type AllianceRequestIntent = z.infer<typeof AllianceRequestIntentSchema>
export type AllianceRequestReplyIntent = z.infer<typeof AllianceRequestReplyIntentSchema>
export type BreakAllianceIntent = z.infer<typeof BreakAllianceIntentSchema>
export type TargetPlayerIntent = z.infer<typeof TargetPlayerIntentSchema>
export type Turn = z.infer<typeof TurnSchema>
@@ -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({
+2
View File
@@ -49,6 +49,8 @@ export interface Config {
boatMaxNumber(): number
allianceDuration(): Tick
allianceRequestCooldown(): Tick
targetDuration(): Tick
targetCooldown(): Tick
}
export interface Theme {
+6
View File
@@ -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
}
+9 -11
View File
@@ -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
}
+3
View File
@@ -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`)
@@ -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
}
}
+7
View File
@@ -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_
}
+10
View File
@@ -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 {
+34 -1
View File
@@ -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());
}