Add relations, NPCs attack enemies using relation, NPCs attack targeted players

This commit is contained in:
Evan
2024-12-27 16:53:56 -08:00
parent cb75f6780c
commit ac1d0c0378
13 changed files with 133 additions and 41 deletions
@@ -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`<div class="type-label">Attitude: ${player.relation(myPlayer)}</div>`;
}
return html`
<div class="info-content">
<div class="player-name ${isAlly ? 'ally' : ''}">${player.name()}</div>
<div class="type-label">Troops: ${renderTroops(player.troops())}</div>
<div class="type-label">Gold: ${renderNumber(player.gold())}</div>
</div>
<div class="info-content">
<div class="player-name ${isAlly ? 'ally' : ''}">${player.name()}</div>
<div class="type-label">Troops: ${renderTroops(player.troops())}</div>
<div class="type-label">Gold: ${renderNumber(player.gold())}</div>
${relation == null ? '' : relation}
</div>
`;
}
+6 -6
View File
@@ -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
// }
}
+1
View File
@@ -101,6 +101,7 @@ export class AttackExecution implements Execution {
// No updates should happen in init.
this.breakAlliance = true
}
this.target.updateRelation(this._owner, -500)
}
}
+34 -16
View File
@@ -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<Player> = new Set<Player>
private relations = new Map<Player, number>()
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)
}
}
+9 -2
View File
@@ -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 {
+2
View File
@@ -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) {
+2 -1
View File
@@ -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
}
@@ -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
+5 -5
View File
@@ -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_;
}
+10
View File
@@ -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[]
+1 -1
View File
@@ -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
+47 -1
View File
@@ -43,6 +43,8 @@ export class PlayerImpl implements MutablePlayer {
private sentDonations: Donation[] = []
private relations = new Map<Player, number>()
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 {
+1 -1
View File
@@ -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) {