mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 17:46:39 +00:00
8d6a0bce2c
## Description: There are fewer bots on the mainland, so taking control there can make you more powerful. To balance that, no rivers have been placed there, meaning you won’t gain trade advantages from them. The battlefield is split into two fronts, allowing for exciting amphibious assaults with nuclear warfare. I'm not sure about the white line on the map, but I don't think it's a problem. <img width="382" alt="スクリーンショット 2025-03-31 21 21 26" src="https://github.com/user-attachments/assets/e1428ad8-febc-4eee-9c73-f27dd6fc207a" /> <img width="541" alt="スクリーンショット 2025-03-31 21 22 01" src="https://github.com/user-attachments/assets/e2e63664-dde6-4d0b-a9d3-4f759d8924c6" /> <img width="441" alt="スクリーンショット 2025-03-31 21 22 15" src="https://github.com/user-attachments/assets/eb386896-ed02-463b-ba2e-1b991dba8acb" />  ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: <DISCORD USERNAME> aotumuri
520 lines
12 KiB
TypeScript
520 lines
12 KiB
TypeScript
import { Config } from "../configuration/Config";
|
|
import { GameEvent } from "../EventBus";
|
|
import { PlayerView } from "./GameView";
|
|
import { ClientID, GameConfig, GameID, AllPlayersStats } from "../Schemas";
|
|
import { GameMap, GameMapImpl, TileRef } from "./GameMap";
|
|
import {
|
|
GameUpdate,
|
|
GameUpdateType,
|
|
PlayerUpdate,
|
|
UnitUpdate,
|
|
} from "./GameUpdates";
|
|
import { Stats } from "./Stats";
|
|
|
|
export type PlayerID = string;
|
|
export type Tick = number;
|
|
export type Gold = number;
|
|
|
|
export const AllPlayers = "AllPlayers" as const;
|
|
|
|
// export type GameUpdates = Record<GameUpdateType, GameUpdate[]>;
|
|
// Create a type that maps GameUpdateType to its corresponding update type
|
|
type UpdateTypeMap<T extends GameUpdateType> = Extract<GameUpdate, { type: T }>;
|
|
|
|
// Then use it to create the record type
|
|
export type GameUpdates = {
|
|
[K in GameUpdateType]: UpdateTypeMap<K>[];
|
|
};
|
|
|
|
export interface MapPos {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
export enum Difficulty {
|
|
Easy = "Easy",
|
|
Medium = "Medium",
|
|
Hard = "Hard",
|
|
Impossible = "Impossible",
|
|
}
|
|
|
|
export enum TeamName {
|
|
Red = "Red",
|
|
Blue = "Blue",
|
|
Bot = "Bot",
|
|
}
|
|
|
|
export enum GameMapType {
|
|
World = "World",
|
|
Europe = "Europe",
|
|
Mena = "Mena",
|
|
NorthAmerica = "North America",
|
|
SouthAmerica = "South America",
|
|
Oceania = "Oceania",
|
|
BlackSea = "Black Sea",
|
|
Africa = "Africa",
|
|
Pangaea = "Pangaea",
|
|
Asia = "Asia",
|
|
Mars = "Mars",
|
|
Britannia = "Britannia",
|
|
GatewayToTheAtlantic = "Gateway to the Atlantic",
|
|
Australia = "Australia",
|
|
Iceland = "Iceland",
|
|
Japan = "Japan",
|
|
}
|
|
|
|
export enum GameType {
|
|
Singleplayer = "Singleplayer",
|
|
Public = "Public",
|
|
Private = "Private",
|
|
}
|
|
|
|
export enum GameMode {
|
|
FFA = "Free For All",
|
|
Team = "Team",
|
|
}
|
|
|
|
export interface Team {
|
|
name: TeamName;
|
|
}
|
|
|
|
export interface UnitInfo {
|
|
cost: (player: Player | PlayerView) => Gold;
|
|
// Determines if its owner changes when its tile is conquered.
|
|
territoryBound: boolean;
|
|
maxHealth?: number;
|
|
damage?: number;
|
|
constructionDuration?: number;
|
|
}
|
|
|
|
export enum UnitType {
|
|
TransportShip = "Transport",
|
|
Warship = "Warship",
|
|
Shell = "Shell",
|
|
SAMMissile = "SAMMissile",
|
|
Port = "Port",
|
|
AtomBomb = "Atom Bomb",
|
|
HydrogenBomb = "Hydrogen Bomb",
|
|
TradeShip = "Trade Ship",
|
|
MissileSilo = "Missile Silo",
|
|
DefensePost = "Defense Post",
|
|
SAMLauncher = "SAM Launcher",
|
|
City = "City",
|
|
MIRV = "MIRV",
|
|
MIRVWarhead = "MIRV Warhead",
|
|
Construction = "Construction",
|
|
}
|
|
|
|
export const nukeTypes = [
|
|
UnitType.AtomBomb,
|
|
UnitType.HydrogenBomb,
|
|
UnitType.MIRVWarhead,
|
|
UnitType.MIRV,
|
|
] as UnitType[];
|
|
|
|
export type NukeType = (typeof nukeTypes)[number];
|
|
|
|
export enum Relation {
|
|
Hostile = 0,
|
|
Distrustful = 1,
|
|
Neutral = 2,
|
|
Friendly = 3,
|
|
}
|
|
|
|
export class Nation {
|
|
constructor(
|
|
public readonly flag: string,
|
|
public readonly name: string,
|
|
public readonly cell: Cell,
|
|
public readonly strength: number,
|
|
) {}
|
|
}
|
|
|
|
export class Cell {
|
|
public index: number;
|
|
|
|
private strRepr: string;
|
|
|
|
constructor(
|
|
public readonly x,
|
|
public readonly y,
|
|
) {
|
|
this.strRepr = `Cell[${this.x},${this.y}]`;
|
|
}
|
|
|
|
pos(): MapPos {
|
|
return {
|
|
x: this.x,
|
|
y: this.y,
|
|
};
|
|
}
|
|
|
|
toString(): string {
|
|
return this.strRepr;
|
|
}
|
|
}
|
|
|
|
export enum TerrainType {
|
|
Plains,
|
|
Highland,
|
|
Mountain,
|
|
Lake,
|
|
Ocean,
|
|
}
|
|
|
|
export enum PlayerType {
|
|
Bot = "BOT",
|
|
Human = "HUMAN",
|
|
FakeHuman = "FAKEHUMAN",
|
|
}
|
|
|
|
export interface Execution {
|
|
isActive(): boolean;
|
|
activeDuringSpawnPhase(): boolean;
|
|
init(mg: Game, ticks: number): void;
|
|
tick(ticks: number): void;
|
|
}
|
|
|
|
export interface Attack {
|
|
id(): string;
|
|
retreating(): boolean;
|
|
retreated(): boolean;
|
|
orderRetreat(): void;
|
|
executeRetreat(): void;
|
|
target(): Player | TerraNullius;
|
|
attacker(): Player;
|
|
troops(): number;
|
|
setTroops(troops: number): void;
|
|
isActive(): boolean;
|
|
delete(): void;
|
|
// The tile the attack originated from, mostly used for boat attacks.
|
|
sourceTile(): TileRef | null;
|
|
}
|
|
|
|
export interface AllianceRequest {
|
|
accept(): void;
|
|
reject(): void;
|
|
requestor(): Player;
|
|
recipient(): Player;
|
|
createdAt(): Tick;
|
|
}
|
|
|
|
export interface Alliance {
|
|
requestor(): Player;
|
|
recipient(): Player;
|
|
createdAt(): Tick;
|
|
other(player: Player): Player;
|
|
}
|
|
|
|
export interface MutableAlliance extends Alliance {
|
|
expire(): void;
|
|
other(player: Player): Player;
|
|
}
|
|
|
|
export class PlayerInfo {
|
|
public readonly clan: string | null;
|
|
|
|
constructor(
|
|
public readonly flag: string,
|
|
public readonly name: string,
|
|
public readonly playerType: PlayerType,
|
|
// null if bot.
|
|
public readonly clientID: ClientID | null,
|
|
// TODO: make player id the small id
|
|
public readonly id: PlayerID,
|
|
public readonly nation?: Nation | null,
|
|
) {
|
|
// Compute clan from name
|
|
if (!name.startsWith("[") || !name.includes("]")) {
|
|
this.clan = null;
|
|
} else {
|
|
const clanMatch = name.match(/^\[([A-Z]{2,5})\]/);
|
|
this.clan = clanMatch ? clanMatch[1] : null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Some units have info specific to them
|
|
export interface UnitSpecificInfos {
|
|
dstPort?: Unit; // Only for trade ships
|
|
detonationDst?: TileRef; // Only for nukes
|
|
warshipTarget?: Unit;
|
|
cooldownDuration?: number;
|
|
}
|
|
|
|
export interface Unit {
|
|
id(): number;
|
|
|
|
// Properties
|
|
type(): UnitType;
|
|
troops(): number;
|
|
owner(): Player;
|
|
info(): UnitInfo;
|
|
|
|
// Location
|
|
tile(): TileRef;
|
|
lastTile(): TileRef;
|
|
move(tile: TileRef): void;
|
|
|
|
// State
|
|
isActive(): boolean;
|
|
hasHealth(): boolean;
|
|
health(): number;
|
|
modifyHealth(delta: number): void;
|
|
|
|
setWarshipTarget(target: Unit): void; // warship only
|
|
warshipTarget(): Unit;
|
|
|
|
setCooldown(triggerCooldown: boolean): void;
|
|
ticksLeftInCooldown(cooldownDuration: number): Tick;
|
|
isCooldown(): boolean;
|
|
setDstPort(dstPort: Unit): void;
|
|
dstPort(): Unit; // Only for trade ships
|
|
detonationDst(): TileRef; // Only for nukes
|
|
|
|
setMoveTarget(cell: TileRef): void;
|
|
moveTarget(): TileRef | null;
|
|
|
|
setTargetedBySAM(targeted: boolean): void;
|
|
targetedBySAM(): boolean;
|
|
|
|
// Mutations
|
|
setTroops(troops: number): void;
|
|
delete(displayerMessage?: boolean): void;
|
|
|
|
// Only for Construction type
|
|
constructionType(): UnitType | null;
|
|
setConstructionType(type: UnitType): void;
|
|
|
|
// Updates
|
|
toUpdate(): UnitUpdate;
|
|
}
|
|
|
|
export interface TerraNullius {
|
|
isPlayer(): false;
|
|
id(): PlayerID; // always zero, maybe make it TerraNulliusID?
|
|
clientID(): ClientID;
|
|
smallID(): number;
|
|
}
|
|
|
|
export interface Player {
|
|
// Basic Info
|
|
smallID(): number;
|
|
info(): PlayerInfo;
|
|
name(): string;
|
|
displayName(): string;
|
|
clientID(): ClientID;
|
|
id(): PlayerID;
|
|
type(): PlayerType;
|
|
isPlayer(): this is Player;
|
|
toString(): string;
|
|
|
|
// State & Properties
|
|
isAlive(): boolean;
|
|
isTraitor(): boolean;
|
|
largestClusterBoundingBox: { min: Cell; max: Cell } | null;
|
|
lastTileChange(): Tick;
|
|
|
|
// Territory
|
|
tiles(): ReadonlySet<TileRef>;
|
|
borderTiles(): ReadonlySet<TileRef>;
|
|
numTilesOwned(): number;
|
|
conquer(tile: TileRef): void;
|
|
relinquish(tile: TileRef): void;
|
|
|
|
// Resources & Population
|
|
gold(): Gold;
|
|
population(): number;
|
|
workers(): number;
|
|
troops(): number;
|
|
targetTroopRatio(): number;
|
|
addGold(toAdd: Gold): void;
|
|
removeGold(toRemove: Gold): void;
|
|
addWorkers(toAdd: number): void;
|
|
removeWorkers(toRemove: number): void;
|
|
setTargetTroopRatio(target: number): void;
|
|
setTroops(troops: number): void;
|
|
addTroops(troops: number): void;
|
|
removeTroops(troops: number): number;
|
|
|
|
// Units
|
|
units(...types: UnitType[]): Unit[];
|
|
unitsIncludingConstruction(type: UnitType): Unit[];
|
|
canBuild(type: UnitType, targetTile: TileRef): TileRef | false;
|
|
buildUnit(
|
|
type: UnitType,
|
|
troops: number,
|
|
tile: TileRef,
|
|
unitSpecificInfos?: UnitSpecificInfos,
|
|
): Unit;
|
|
captureUnit(unit: Unit): void;
|
|
|
|
// Relations & Diplomacy
|
|
neighbors(): (Player | TerraNullius)[];
|
|
sharesBorderWith(other: Player | TerraNullius): boolean;
|
|
relation(other: Player): Relation;
|
|
allRelationsSorted(): { player: Player; relation: Relation }[];
|
|
updateRelation(other: Player, delta: number): void;
|
|
decayRelations(): void;
|
|
isOnSameTeam(other: Player): boolean;
|
|
// Either allied or on same team.
|
|
isFriendly(other: Player): boolean;
|
|
team(): Team | null;
|
|
clan(): string | null;
|
|
incomingAllianceRequests(): AllianceRequest[];
|
|
outgoingAllianceRequests(): AllianceRequest[];
|
|
alliances(): MutableAlliance[];
|
|
allies(): Player[];
|
|
isAlliedWith(other: Player): boolean;
|
|
allianceWith(other: Player): MutableAlliance | null;
|
|
canSendAllianceRequest(other: Player): boolean;
|
|
breakAlliance(alliance: Alliance): void;
|
|
createAllianceRequest(recipient: Player): AllianceRequest;
|
|
|
|
// Targeting
|
|
canTarget(other: Player): boolean;
|
|
target(other: Player): void;
|
|
targets(): Player[];
|
|
transitiveTargets(): Player[];
|
|
|
|
// Communication
|
|
canSendEmoji(recipient: Player | typeof AllPlayers): boolean;
|
|
outgoingEmojis(): EmojiMessage[];
|
|
sendEmoji(recipient: Player | typeof AllPlayers, emoji: string): void;
|
|
|
|
// Donation
|
|
canDonate(recipient: Player): boolean;
|
|
donate(recipient: Player, troops: number): void;
|
|
|
|
// Embargo
|
|
hasEmbargoAgainst(other: Player): boolean;
|
|
tradingPartners(): Player[];
|
|
addEmbargo(other: PlayerID): void;
|
|
stopEmbargo(other: PlayerID): void;
|
|
canTrade(other: Player): boolean;
|
|
|
|
// Attacking.
|
|
canAttack(tile: TileRef): boolean;
|
|
createAttack(
|
|
target: Player | TerraNullius,
|
|
troops: number,
|
|
sourceTile: TileRef,
|
|
): Attack;
|
|
outgoingAttacks(): Attack[];
|
|
incomingAttacks(): Attack[];
|
|
orderRetreat(attackID: string): void;
|
|
executeRetreat(attackID: string): void;
|
|
|
|
// Misc
|
|
toUpdate(): PlayerUpdate;
|
|
playerProfile(): PlayerProfile;
|
|
canBoat(tile: TileRef): boolean;
|
|
tradingPorts(port: Unit): Unit[];
|
|
}
|
|
|
|
export interface Game extends GameMap {
|
|
// Map & Dimensions
|
|
isOnMap(cell: Cell): boolean;
|
|
width(): number;
|
|
height(): number;
|
|
map(): GameMap;
|
|
miniMap(): GameMap;
|
|
forEachTile(fn: (tile: TileRef) => void): void;
|
|
|
|
// Player Management
|
|
player(id: PlayerID): Player;
|
|
players(): Player[];
|
|
allPlayers(): Player[];
|
|
playerByClientID(id: ClientID): Player | null;
|
|
playerBySmallID(id: number): Player | TerraNullius;
|
|
hasPlayer(id: PlayerID): boolean;
|
|
addPlayer(playerInfo: PlayerInfo, manpower: number): Player;
|
|
terraNullius(): TerraNullius;
|
|
owner(ref: TileRef): Player | TerraNullius;
|
|
|
|
teams(): Team[];
|
|
|
|
// Game State
|
|
ticks(): Tick;
|
|
inSpawnPhase(): boolean;
|
|
executeNextTick(): GameUpdates;
|
|
setWinner(winner: Player | TeamName, allPlayersStats: AllPlayersStats): void;
|
|
config(): Config;
|
|
|
|
// Units
|
|
units(...types: UnitType[]): Unit[];
|
|
unitInfo(type: UnitType): UnitInfo;
|
|
nearbyUnits(
|
|
tile: TileRef,
|
|
searchRange: number,
|
|
types: UnitType | UnitType[],
|
|
): Array<{ unit: Unit; distSquared: number }>;
|
|
|
|
addExecution(...exec: Execution[]): void;
|
|
displayMessage(
|
|
message: string,
|
|
type: MessageType,
|
|
playerID: PlayerID | null,
|
|
): void;
|
|
|
|
// Nations
|
|
nations(): Nation[];
|
|
|
|
numTilesWithFallout(): number;
|
|
// Optional as it's not initialized before the end of spawn phase
|
|
stats(): Stats;
|
|
}
|
|
|
|
export interface PlayerActions {
|
|
canBoat: boolean;
|
|
canAttack: boolean;
|
|
buildableUnits: BuildableUnit[];
|
|
canSendEmojiAllPlayers: boolean;
|
|
interaction?: PlayerInteraction;
|
|
}
|
|
|
|
export interface BuildableUnit {
|
|
canBuild: boolean;
|
|
type: UnitType;
|
|
cost: number;
|
|
}
|
|
|
|
export interface PlayerProfile {
|
|
relations: Record<number, Relation>;
|
|
alliances: number[];
|
|
}
|
|
|
|
export interface PlayerBorderTiles {
|
|
borderTiles: ReadonlySet<TileRef>;
|
|
}
|
|
|
|
export interface PlayerInteraction {
|
|
sharedBorder: boolean;
|
|
canSendEmoji: boolean;
|
|
canSendAllianceRequest: boolean;
|
|
canBreakAlliance: boolean;
|
|
canTarget: boolean;
|
|
canDonate: boolean;
|
|
canEmbargo: boolean;
|
|
}
|
|
|
|
export interface EmojiMessage {
|
|
message: string;
|
|
senderID: number;
|
|
recipientID: number | typeof AllPlayers;
|
|
createdAt: Tick;
|
|
}
|
|
|
|
export enum MessageType {
|
|
SUCCESS,
|
|
INFO,
|
|
WARN,
|
|
ERROR,
|
|
}
|
|
|
|
export interface NameViewData {
|
|
x: number;
|
|
y: number;
|
|
size: number;
|
|
}
|