mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-10 23:44:13 +00:00
Add relations, NPCs attack enemies using relation, NPCs attack targeted players
This commit is contained in:
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ export class AttackExecution implements Execution {
|
||||
// No updates should happen in init.
|
||||
this.breakAlliance = true
|
||||
}
|
||||
this.target.updateRelation(this._owner, -500)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
}
|
||||
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user