Files
OpenFrontIO/src/core/GameRunner.ts
T
Vivacious Box 311d43ab4f Build bar (#2059)
## Description:

Make the unit display bar a proper unit build bar
Add shortcuts for all structures and units
Add ranges for ranged structures and units
Changed the shortcuts to use the key instead of the code for
internationalization purposes


![buildbar](https://github.com/user-attachments/assets/6407dc9c-14b4-40cc-8faa-cdd9e88c9fd2)
<img width="285" height="517" alt="image"
src="https://github.com/user-attachments/assets/91bb01e6-e48c-4255-ace1-306af9cdc25b"
/>

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

Mr.Box

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
Co-authored-by: icslucas <carolinacarazolli@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-02 12:38:28 -07:00

263 lines
7.1 KiB
TypeScript

import { placeName } from "../client/graphics/NameBoxCalculator";
import { getConfig } from "./configuration/ConfigLoader";
import { Executor } from "./execution/ExecutionManager";
import { WinCheckExecution } from "./execution/WinCheckExecution";
import {
AllPlayers,
Attack,
Cell,
Game,
GameUpdates,
NameViewData,
Nation,
Player,
PlayerActions,
PlayerBorderTiles,
PlayerID,
PlayerInfo,
PlayerProfile,
PlayerType,
} from "./game/Game";
import { createGame } from "./game/GameImpl";
import { TileRef } from "./game/GameMap";
import { GameMapLoader } from "./game/GameMapLoader";
import {
ErrorUpdate,
GameUpdateType,
GameUpdateViewData,
} from "./game/GameUpdates";
import { loadTerrainMap as loadGameMap } from "./game/TerrainMapLoader";
import { PseudoRandom } from "./PseudoRandom";
import { ClientID, GameStartInfo, Turn } from "./Schemas";
import { sanitize, simpleHash } from "./Util";
import { fixProfaneUsername } from "./validations/username";
export async function createGameRunner(
gameStart: GameStartInfo,
clientID: ClientID,
mapLoader: GameMapLoader,
callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
): Promise<GameRunner> {
const config = await getConfig(gameStart.config, null);
const gameMap = await loadGameMap(
gameStart.config.gameMap,
gameStart.config.gameMapSize,
mapLoader,
);
const random = new PseudoRandom(simpleHash(gameStart.gameID));
const humans = gameStart.players.map(
(p) =>
new PlayerInfo(
p.clientID === clientID
? sanitize(p.username)
: fixProfaneUsername(sanitize(p.username)),
PlayerType.Human,
p.clientID,
random.nextID(),
),
);
const nations = gameStart.config.disableNPCs
? []
: gameMap.nations.map(
(n) =>
new Nation(
new Cell(n.coordinates[0], n.coordinates[1]),
n.strength,
new PlayerInfo(n.name, PlayerType.FakeHuman, null, random.nextID()),
),
);
const game: Game = createGame(
humans,
nations,
gameMap.gameMap,
gameMap.miniGameMap,
config,
);
const gr = new GameRunner(
game,
new Executor(game, gameStart.gameID, clientID),
callBack,
);
gr.init();
return gr;
}
export class GameRunner {
private turns: Turn[] = [];
private currTurn = 0;
private isExecuting = false;
private playerViewData: Record<PlayerID, NameViewData> = {};
constructor(
public game: Game,
private execManager: Executor,
private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
) {}
init() {
if (this.game.config().bots() > 0) {
this.game.addExecution(
...this.execManager.spawnBots(this.game.config().numBots()),
);
}
if (this.game.config().spawnNPCs()) {
this.game.addExecution(...this.execManager.fakeHumanExecutions());
}
this.game.addExecution(new WinCheckExecution());
}
public addTurn(turn: Turn): void {
this.turns.push(turn);
}
public executeNextTick() {
if (this.isExecuting) {
return;
}
if (this.currTurn >= this.turns.length) {
return;
}
this.isExecuting = true;
this.game.addExecution(
...this.execManager.createExecs(this.turns[this.currTurn]),
);
this.currTurn++;
let updates: GameUpdates;
try {
updates = this.game.executeNextTick();
} catch (error: unknown) {
if (error instanceof Error) {
console.error("Game tick error:", error.message);
this.callBack({
errMsg: error.message,
stack: error.stack,
} as ErrorUpdate);
} else {
console.error("Game tick error:", error);
}
return;
}
if (this.game.inSpawnPhase() && this.game.ticks() % 2 === 0) {
this.game
.players()
.filter(
(p) =>
p.type() === PlayerType.Human || p.type() === PlayerType.FakeHuman,
)
.forEach(
(p) => (this.playerViewData[p.id()] = placeName(this.game, p)),
);
}
if (this.game.ticks() < 3 || this.game.ticks() % 30 === 0) {
this.game.players().forEach((p) => {
this.playerViewData[p.id()] = placeName(this.game, p);
});
}
// Many tiles are updated to pack it into an array
const packedTileUpdates = updates[GameUpdateType.Tile].map((u) => u.update);
updates[GameUpdateType.Tile] = [];
this.callBack({
tick: this.game.ticks(),
packedTileUpdates: new BigUint64Array(packedTileUpdates),
updates: updates,
playerNameViewData: this.playerViewData,
});
this.isExecuting = false;
}
public playerActions(
playerID: PlayerID,
x?: number,
y?: number,
): PlayerActions {
const player = this.game.player(playerID);
const tile =
x !== undefined && y !== undefined ? this.game.ref(x, y) : null;
const actions = {
canAttack: tile !== null && player.canAttack(tile),
buildableUnits: player.buildableUnits(tile),
canSendEmojiAllPlayers: player.canSendEmoji(AllPlayers),
} as PlayerActions;
if (tile !== null && this.game.hasOwner(tile)) {
const other = this.game.owner(tile) as Player;
actions.interaction = {
sharedBorder: player.sharesBorderWith(other),
canSendEmoji: player.canSendEmoji(other),
canTarget: player.canTarget(other),
canSendAllianceRequest: player.canSendAllianceRequest(other),
canBreakAlliance: player.isAlliedWith(other),
canDonateGold: player.canDonateGold(other),
canDonateTroops: player.canDonateTroops(other),
canEmbargo: !player.hasEmbargoAgainst(other),
};
const alliance = player.allianceWith(other as Player);
if (alliance) {
actions.interaction.allianceExpiresAt = alliance.expiresAt();
}
}
return actions;
}
public playerProfile(playerID: number): PlayerProfile {
const player = this.game.playerBySmallID(playerID);
if (!player.isPlayer()) {
throw new Error(`player with id ${playerID} not found`);
}
return player.playerProfile();
}
public playerBorderTiles(playerID: PlayerID): PlayerBorderTiles {
const player = this.game.player(playerID);
if (!player.isPlayer()) {
throw new Error(`player with id ${playerID} not found`);
}
return {
borderTiles: player.borderTiles(),
} as PlayerBorderTiles;
}
public attackAveragePosition(
playerID: number,
attackID: string,
): Cell | null {
const player = this.game.playerBySmallID(playerID);
if (!player.isPlayer()) {
throw new Error(`player with id ${playerID} not found`);
}
const condition = (a: Attack) => a.id() === attackID;
const attack =
player.outgoingAttacks().find(condition) ??
player.incomingAttacks().find(condition);
if (attack === undefined) {
return null;
}
return attack.averagePosition();
}
public bestTransportShipSpawn(
playerID: PlayerID,
targetTile: TileRef,
): TileRef | false {
const player = this.game.player(playerID);
if (!player.isPlayer()) {
throw new Error(`player with id ${playerID} not found`);
}
return player.bestTransportShipSpawn(targetTile);
}
}