diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts index d401d4b94..f04b7032e 100644 --- a/src/client/graphics/layers/PlayerInfoOverlay.ts +++ b/src/client/graphics/layers/PlayerInfoOverlay.ts @@ -1,7 +1,7 @@ import { LitElement, html, css } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { Layer } from './Layer'; -import { Game, GameType, Player, Unit, UnitType } from '../../../core/game/Game'; +import { Game, GameType, Player, PlayerType, Unit, UnitType } from '../../../core/game/Game'; import { ClientID } from '../../../core/Schemas'; import { EventBus } from '../../../core/EventBus'; import { TransformHandler } from '../TransformHandler'; @@ -110,13 +110,20 @@ export class PlayerInfoOverlay extends LitElement implements Layer { } private renderPlayerInfo(player: Player) { - const isAlly = (this.myPlayer()?.isAlliedWith(player) || player == this.myPlayer()) ?? false; + const myPlayer = this.myPlayer(); + const isAlly = (myPlayer?.isAlliedWith(player) || player == this.myPlayer()) ?? false; + let relation = null + if (player.type() == PlayerType.FakeHuman && myPlayer != null) { + // Don't create an HTML string, let Lit handle the templating + relation = html`
Attitude: ${player.relation(myPlayer)}
`; + } return html` -
-
${player.name()}
-
Troops: ${renderTroops(player.troops())}
-
Gold: ${renderNumber(player.gold())}
-
+
+
${player.name()}
+
Troops: ${renderTroops(player.troops())}
+
Gold: ${renderNumber(player.gold())}
+ ${relation == null ? '' : relation} +
`; } diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index a604c1411..f7e40f486 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -40,11 +40,11 @@ export class DevConfig extends DefaultConfig { // return 5000 // } - numBots(): number { - return 0 - } - spawnNPCs(): boolean { - return false - } + // numBots(): number { + // return 0 + // } + // spawnNPCs(): boolean { + // return false + // } } diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index c7cbd35d1..033d45ab7 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -101,6 +101,7 @@ export class AttackExecution implements Execution { // No updates should happen in init. this.breakAlliance = true } + this.target.updateRelation(this._owner, -500) } } diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index a5b52b186..c0d30c315 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -14,6 +14,7 @@ import { consolex } from "../Consolex"; import { CityExecution } from "./CityExecution"; import { NukeExecution } from "./NukeExecution"; import { MissileSiloExecution } from "./MissileSiloExecution"; +import { EmojiExecution } from "./EmojiExecution"; export class FakeHumanExecution implements Execution { @@ -26,9 +27,6 @@ export class FakeHumanExecution implements Execution { private enemy: Player | null = null - private rejected: Set = new Set - - private relations = new Map() constructor(gameID: GameID, private worker: WorkerClient, private playerInfo: PlayerInfo, private cell: Cell, private strength: number) { this.random = new PseudoRandom(simpleHash(playerInfo.id) + simpleHash(gameID)) @@ -69,15 +67,16 @@ export class FakeHumanExecution implements Execution { this.sendAttack(this.mg.terraNullius()) return } + if (!this.player.isAlive()) { + this.active = false + return + } + if (ticks % this.random.nextInt(40, 80) != 0) { return } - if (!this.player.isAlive()) { - this.active = false - return - } if (this.player.troops() > 100_000 && this.player.targetTroopRatio() > .7) { this.player.setTargetTroopRatio(.7) @@ -142,12 +141,22 @@ export class FakeHumanExecution implements Execution { this.enemy = null } + const target = this.player.allies() + .filter(ally => this.player.relation(ally) > 0) + .filter(ally => ally.targets().length > 0) + .map(ally => ({ ally: ally, t: ally.targets() }))[0] ?? null + + if (target != null) { + this.player.updateRelation(target.ally, -2000) + this.enemy = target.t[0] + this.mg.addExecution(new EmojiExecution(this.player.id(), target.ally.id(), "👍")) + } + if (this.enemy == null) { - this.enemy = this.mg.executions() - .filter(e => e instanceof AttackExecution) - .map(e => e as AttackExecution) - .filter(e => e.targetID() == this.player.id()) - .map(e => e.owner())[0] ?? null + const mostHated = this.player.allRelationsSorted()[0] ?? null + if (mostHated != null && mostHated.relation < - 500) { + this.enemy = mostHated.player + } } if (this.enemy) { @@ -157,7 +166,6 @@ export class FakeHumanExecution implements Execution { } return } - } private maybeSendNuke(other: Player) { @@ -300,11 +308,21 @@ export class FakeHumanExecution implements Execution { handleAllianceRequests() { for (const req of this.player.incomingAllianceRequests()) { - if (req.requestor().isTraitor() || req.requestor().alliances().length >= 3) { + if (req.requestor().isTraitor()) { req.reject() - } else { - req.accept() + continue } + if (this.player.relation(req.requestor()) < 0) { + req.reject() + } + const requestorIsMuchLarger = req.requestor().numTilesOwned() > this.player.numTilesOwned() * 3 + if (!requestorIsMuchLarger && req.requestor().alliances().length >= 3) { + req.reject() + continue + } + req.accept() + req.requestor().updateRelation(this.player, 10000) + this.player.updateRelation(req.requestor(), 10000) } } diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 7875870f5..45045f15b 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -1,5 +1,5 @@ import { nextTick } from "process"; -import { Cell, Execution, MutableGame, MutablePlayer, PlayerID, Tile, MutableUnit, UnitType } from "../game/Game"; +import { Cell, Execution, MutableGame, MutablePlayer, PlayerID, Tile, MutableUnit, UnitType, Player, TerraNullius } from "../game/Game"; import { PathFinder } from "../pathfinding/PathFinding"; import { PathFindResultType } from "../pathfinding/AStar"; import { PseudoRandom } from "../PseudoRandom"; @@ -32,6 +32,10 @@ export class NukeExecution implements Execution { this.dst = this.mg.tile(this.cell) } + public target(): Player | TerraNullius { + return this.dst.owner() + } + tick(ticks: number): void { if (this.nuke == null) { const spawn = this.player.canBuild(this.type, this.dst) @@ -98,6 +102,9 @@ export class NukeExecution implements Execution { if (alliance != null) { this.player.breakAlliance(alliance) } + if (other != this.player) { + other.updateRelation(this.player, -10000) + } } } @@ -113,7 +120,7 @@ export class NukeExecution implements Execution { } owner(): MutablePlayer { - return null + return this.player } isActive(): boolean { diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index 667673ed8..9640fb244 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -29,6 +29,7 @@ export class PlayerExecution implements Execution { } tick(ticks: number) { + this.player.decayRelations() this.player.units().forEach(u => { if (u.health() <= 0) { u.delete() @@ -87,6 +88,7 @@ export class PlayerExecution implements Execution { } } + private removeClusters() { const clusters = this.calculateClusters() // if (clusters.length <= 1) { diff --git a/src/core/execution/TargetPlayerExecution.ts b/src/core/execution/TargetPlayerExecution.ts index 60ae31c28..66a8735a0 100644 --- a/src/core/execution/TargetPlayerExecution.ts +++ b/src/core/execution/TargetPlayerExecution.ts @@ -1,4 +1,4 @@ -import {Execution, MutableGame, MutablePlayer, PlayerID} from "../game/Game"; +import { Execution, MutableGame, MutablePlayer, PlayerID } from "../game/Game"; export class TargetPlayerExecution implements Execution { @@ -18,6 +18,7 @@ export class TargetPlayerExecution implements Execution { tick(ticks: number): void { if (this.requestor.canTarget(this.target)) { this.requestor.target(this.target) + this.target.updateRelation(this.requestor, -5000) } this.active = false } diff --git a/src/core/execution/alliance/AllianceRequestReplyExecution.ts b/src/core/execution/alliance/AllianceRequestReplyExecution.ts index dbd68acb1..62c7a8430 100644 --- a/src/core/execution/alliance/AllianceRequestReplyExecution.ts +++ b/src/core/execution/alliance/AllianceRequestReplyExecution.ts @@ -1,5 +1,5 @@ import { consolex } from "../../Consolex"; -import {AllianceRequest, Execution, MutableGame, MutablePlayer, Player, PlayerID} from "../../game/Game"; +import { AllianceRequest, Execution, MutableGame, MutablePlayer, Player, PlayerID } from "../../game/Game"; export class AllianceRequestReplyExecution implements Execution { private active = true diff --git a/src/core/game/AllianceRequestImpl.ts b/src/core/game/AllianceRequestImpl.ts index 54001c5df..696aef8fa 100644 --- a/src/core/game/AllianceRequestImpl.ts +++ b/src/core/game/AllianceRequestImpl.ts @@ -1,16 +1,16 @@ -import {MutableAllianceRequest, Player, Tick} from "./Game"; -import {GameImpl} from "./GameImpl"; +import { MutableAllianceRequest, MutablePlayer, Player, Tick } from "./Game"; +import { GameImpl } from "./GameImpl"; export class AllianceRequestImpl implements MutableAllianceRequest { - constructor(private requestor_, private recipient_, private tickCreated: number, private game: GameImpl) { } + constructor(private requestor_: MutablePlayer, private recipient_: MutablePlayer, private tickCreated: number, private game: GameImpl) { } - requestor(): Player { + requestor(): MutablePlayer { return this.requestor_; } - recipient(): Player { + recipient(): MutablePlayer { return this.recipient_; } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 684e4aed0..05cd08a08 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -120,6 +120,8 @@ export interface AllianceRequest { export interface MutableAllianceRequest extends AllianceRequest { accept(): void reject(): void + requestor(): MutablePlayer + recipient(): MutablePlayer } export interface Alliance { @@ -233,11 +235,16 @@ export interface Player { incomingAllianceRequests(): AllianceRequest[] outgoingAllianceRequests(): AllianceRequest[] alliances(): Alliance[] + allies(): 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 + // How this player feels about other player. + relation(other: Player): number + // Sorted from most hated to most liked + allRelationsSorted(): { player: Player, relation: number }[] isTraitor(): boolean canTarget(other: Player): boolean // Targets for this player @@ -270,9 +277,12 @@ export interface MutablePlayer extends Player { incomingAllianceRequests(): MutableAllianceRequest[] outgoingAllianceRequests(): MutableAllianceRequest[] alliances(): MutableAlliance[] + allies(): MutablePlayer[] allianceWith(other: Player): MutableAlliance | null breakAlliance(alliance: Alliance): void createAllianceRequest(recipient: Player): MutableAllianceRequest + updateRelation(other: Player, delta: number): void + decayRelations(): void target(other: Player): void targets(): MutablePlayer[] transitiveTargets(): MutablePlayer[] diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 28c4c9f4d..8b6f3b626 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -98,7 +98,7 @@ export class GameImpl implements MutableGame { return this.nations_ } - createAllianceRequest(requestor: MutablePlayer, recipient: Player): MutableAllianceRequest { + createAllianceRequest(requestor: MutablePlayer, recipient: MutablePlayer): MutableAllianceRequest { if (requestor.isAlliedWith(recipient)) { consolex.log('cannot request alliance, already allied') return diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index d6e13a7f0..604863588 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -43,6 +43,8 @@ export class PlayerImpl implements MutablePlayer { private sentDonations: Donation[] = [] + private relations = new Map() + constructor(private gs: GameImpl, private readonly playerInfo: PlayerInfo, startPopulation: number) { this._name = playerInfo.name; this._targetTroopRatio = 1 @@ -144,6 +146,10 @@ export class PlayerImpl implements MutablePlayer { return this.gs.alliances_.filter(a => a.requestor() == this || a.recipient() == this) } + allies(): MutablePlayer[] { + return this.alliances().map(a => a.other(this)) + } + isAlliedWith(other: Player): boolean { if (other == this) { return false @@ -191,7 +197,47 @@ export class PlayerImpl implements MutablePlayer { if (this.isAlliedWith(recipient)) { throw new Error(`cannot create alliance request, already allies`) } - return this.gs.createAllianceRequest(this, recipient) + return this.gs.createAllianceRequest(this, recipient as MutablePlayer) + } + + relation(other: Player): number { + if (other == this) { + throw new Error(`cannot get relation with self: ${this}`) + } + if (this.relations.has(other)) { + return this.relations.get(other) + } + return 0 + } + + allRelationsSorted(): { player: Player, relation: number }[] { + return Array.from(this.relations, ([k, v]) => ({ player: k, relation: v })) + .sort((a, b) => a.relation - b.relation) + } + + updateRelation(other: Player, delta: number): void { + if (other == this) { + throw new Error(`cannot update relation with self: ${this}`) + } + let relation = 0 + if (this.relations.has(other)) { + relation = this.relations.get(other) + } + this.relations.set(other, relation + delta) + } + + decayRelations() { + this.relations.forEach((r: number, p: Player) => { + // Have relationships decay over time + if (r > 1) { + r -= 1 + } else if (r < -1) { + r += 1 + } else { + r = 0 + } + this.relations.set(p, r) + }) } canTarget(other: Player): boolean { diff --git a/src/core/game/TileImpl.ts b/src/core/game/TileImpl.ts index 65b3ec26a..982535c96 100644 --- a/src/core/game/TileImpl.ts +++ b/src/core/game/TileImpl.ts @@ -32,7 +32,7 @@ export class TileImpl implements Tile { defenseBonus(player: Player): number { if (this.owner() == player) { - throw Error(`cannot get defense bonus of tile already owned by player`) + throw Error(`cannot get defense bonus of tile already owned by player, ${player}`) } let bonusAmount = 0 for (const bonus of this._defenseBonuses) {