${this.displayUnitCount(player, UnitType.City, cityIcon)}
- ${this.displayUnitCount(player, UnitType.Factory, factoryIcon)}
+ ${this.displayUnitCount(player, UnitType.OilRig, factoryIcon)}
${this.displayUnitCount(player, UnitType.Port, portIcon)}
${this.displayUnitCount(
player,
diff --git a/src/client/graphics/layers/RailroadLayer.ts b/src/client/graphics/layers/RailroadLayer.ts
index 1b3815ac6..216303b46 100644
--- a/src/client/graphics/layers/RailroadLayer.ts
+++ b/src/client/graphics/layers/RailroadLayer.ts
@@ -29,7 +29,7 @@ type RailRef = {
const SNAPPABLE_STRUCTURES: UnitType[] = [
UnitType.Port,
UnitType.City,
- UnitType.Factory,
+ UnitType.OilRig,
];
export class RailTileChangedEvent implements GameEvent {
constructor(public tile: TileRef) {}
diff --git a/src/client/graphics/layers/StructureDrawingUtils.ts b/src/client/graphics/layers/StructureDrawingUtils.ts
index 9fe6a940b..91e17d0a6 100644
--- a/src/client/graphics/layers/StructureDrawingUtils.ts
+++ b/src/client/graphics/layers/StructureDrawingUtils.ts
@@ -4,8 +4,8 @@ import { Cell, UnitType } from "../../../core/game/Game";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { TransformHandler } from "../TransformHandler";
import anchorIcon from "/images/AnchorIcon.png?url";
+import factoryIcon from "/images/buildings/oil-rig_2623991.png?url";
import cityIcon from "/images/CityIcon.png?url";
-import factoryIcon from "/images/FactoryUnit.png?url";
import missileSiloIcon from "/images/MissileSiloUnit.png?url";
import SAMMissileIcon from "/images/SamLauncherUnit.png?url";
import shieldIcon from "/images/ShieldIcon.png?url";
@@ -13,7 +13,7 @@ import shieldIcon from "/images/ShieldIcon.png?url";
export const STRUCTURE_SHAPES: Partial> = {
[UnitType.City]: "circle",
[UnitType.Port]: "pentagon",
- [UnitType.Factory]: "circle",
+ [UnitType.OilRig]: "circle",
[UnitType.DefensePost]: "octagon",
[UnitType.SAMLauncher]: "square",
[UnitType.MissileSilo]: "triangle",
@@ -57,7 +57,7 @@ export class SpriteFactory {
{ iconPath: string; image: HTMLImageElement | null }
> = new Map([
[UnitType.City, { iconPath: cityIcon, image: null }],
- [UnitType.Factory, { iconPath: factoryIcon, image: null }],
+ [UnitType.OilRig, { iconPath: factoryIcon, image: null }],
[UnitType.DefensePost, { iconPath: shieldIcon, image: null }],
[UnitType.Port, { iconPath: anchorIcon, image: null }],
[UnitType.MissileSilo, { iconPath: missileSiloIcon, image: null }],
@@ -464,7 +464,7 @@ export class SpriteFactory {
case UnitType.SAMLauncher:
radius = this.game.config().samRange(level ?? 1);
break;
- case UnitType.Factory:
+ case UnitType.OilRig:
radius = this.game.config().trainStationMaxRange();
break;
case UnitType.DefensePost:
diff --git a/src/client/graphics/layers/StructureIconsLayer.ts b/src/client/graphics/layers/StructureIconsLayer.ts
index 9055e47e3..afbe05540 100644
--- a/src/client/graphics/layers/StructureIconsLayer.ts
+++ b/src/client/graphics/layers/StructureIconsLayer.ts
@@ -84,7 +84,7 @@ export class StructureIconsLayer implements Layer {
private factory: SpriteFactory;
private readonly structures: Map = new Map([
[UnitType.City, { visible: true }],
- [UnitType.Factory, { visible: true }],
+ [UnitType.OilRig, { visible: true }],
[UnitType.DefensePost, { visible: true }],
[UnitType.Port, { visible: true }],
[UnitType.MissileSilo, { visible: true }],
diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts
index 819c4e3f7..1580d411b 100644
--- a/src/client/graphics/layers/StructureLayer.ts
+++ b/src/client/graphics/layers/StructureLayer.ts
@@ -9,8 +9,8 @@ import { euclDistFN, isometricDistFN } from "../../../core/game/GameMap";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, UnitView } from "../../../core/game/GameView";
import cityIcon from "/images/buildings/cityAlt1.png?url";
-import factoryIcon from "/images/buildings/factoryAlt1.png?url";
import shieldIcon from "/images/buildings/fortAlt3.png?url";
+import oilRigIcon from "/images/buildings/oil-rig_2623991.png?url";
import anchorIcon from "/images/buildings/port1.png?url";
import missileSiloIcon from "/images/buildings/silo1.png?url";
import SAMMissileIcon from "/images/buildings/silo4.png?url";
@@ -49,8 +49,8 @@ export class StructureLayer implements Layer {
borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR,
territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR,
},
- [UnitType.Factory]: {
- icon: factoryIcon,
+ [UnitType.OilRig]: {
+ icon: oilRigIcon,
borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR,
territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR,
},
diff --git a/src/client/graphics/layers/UILayer.ts b/src/client/graphics/layers/UILayer.ts
index d8edb3f02..798dd41b9 100644
--- a/src/client/graphics/layers/UILayer.ts
+++ b/src/client/graphics/layers/UILayer.ts
@@ -114,7 +114,7 @@ export class UILayer implements Layer {
break;
}
case UnitType.City:
- case UnitType.Factory:
+ case UnitType.OilRig:
case UnitType.DefensePost:
case UnitType.Port:
case UnitType.MissileSilo:
@@ -334,7 +334,7 @@ export class UILayer implements Layer {
? unit.missileReadinesss()
: this.deletionProgress(this.game, unit);
case UnitType.City:
- case UnitType.Factory:
+ case UnitType.OilRig:
case UnitType.Port:
case UnitType.DefensePost:
return this.deletionProgress(this.game, unit);
diff --git a/src/client/graphics/layers/UnitDisplay.ts b/src/client/graphics/layers/UnitDisplay.ts
index 82d530dcb..0bcc65105 100644
--- a/src/client/graphics/layers/UnitDisplay.ts
+++ b/src/client/graphics/layers/UnitDisplay.ts
@@ -12,19 +12,19 @@ import { UIState } from "../UIState";
import { Layer } from "./Layer";
import warshipIcon from "/images/BattleshipIconWhite.svg?url";
import cityIcon from "/images/CityIconWhite.svg?url";
-import factoryIcon from "/images/FactoryIconWhite.svg?url";
import goldCoinIcon from "/images/GoldCoinIcon.svg?url";
import mirvIcon from "/images/MIRVIcon.svg?url";
import missileSiloIcon from "/images/MissileSiloIconWhite.svg?url";
import hydrogenBombIcon from "/images/MushroomCloudIconWhite.svg?url";
import atomBombIcon from "/images/NukeIconWhite.svg?url";
+import factoryIcon from "/images/oil-rig_2623991.svg?url";
import portIcon from "/images/PortIcon.svg?url";
import samLauncherIcon from "/images/SamLauncherIconWhite.svg?url";
import defensePostIcon from "/images/ShieldIconWhite.svg?url";
const BUILDABLE_UNITS: UnitType[] = [
UnitType.City,
- UnitType.Factory,
+ UnitType.OilRig,
UnitType.Port,
UnitType.DefensePost,
UnitType.MissileSilo,
@@ -44,7 +44,7 @@ export class UnitDisplay extends LitElement implements Layer {
private keybinds: Record = {};
private _cities = 0;
private _warships = 0;
- private _factories = 0;
+ private _oilRigs = 0;
private _missileSilo = 0;
private _port = 0;
private _defensePost = 0;
@@ -113,7 +113,7 @@ export class UnitDisplay extends LitElement implements Layer {
this._port = player.totalUnitLevels(UnitType.Port);
this._defensePost = player.totalUnitLevels(UnitType.DefensePost);
this._samLauncher = player.totalUnitLevels(UnitType.SAMLauncher);
- this._factories = player.totalUnitLevels(UnitType.Factory);
+ this._oilRigs = player.totalUnitLevels(UnitType.OilRig);
this._warships = player.totalUnitLevels(UnitType.Warship);
this.requestUpdate();
}
@@ -147,10 +147,10 @@ export class UnitDisplay extends LitElement implements Layer {
)}
${this.renderUnitItem(
factoryIcon,
- this._factories,
- UnitType.Factory,
- "factory",
- this.keybinds["buildFactory"]?.key ?? "2",
+ this._oilRigs,
+ UnitType.OilRig,
+ "oil_rig",
+ this.keybinds["buildOilRig"]?.key ?? "2",
)}
${this.renderUnitItem(
portIcon,
diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts
index eb21c193f..f7b3a94ad 100644
--- a/src/core/GameRunner.ts
+++ b/src/core/GameRunner.ts
@@ -107,7 +107,7 @@ export class GameRunner {
this.game.addExecution(...this.execManager.nationExecutions());
}
this.game.addExecution(new WinCheckExecution());
- if (!this.game.config().isUnitDisabled(UnitType.Factory)) {
+ if (!this.game.config().isUnitDisabled(UnitType.OilRig)) {
this.game.addExecution(
new RecomputeRailClusterExecution(this.game.railNetwork()),
);
diff --git a/src/core/StatsSchemas.ts b/src/core/StatsSchemas.ts
index c37607bd6..16239e57c 100644
--- a/src/core/StatsSchemas.ts
+++ b/src/core/StatsSchemas.ts
@@ -34,7 +34,7 @@ export const otherUnits = [
"wshp",
"silo",
"saml",
- "fact",
+ "orig",
] as const;
export const OtherUnitSchema = z.enum(otherUnits);
export type OtherUnit = z.infer;
@@ -45,7 +45,7 @@ export type OtherUnitType =
| UnitType.Port
| UnitType.SAMLauncher
| UnitType.Warship
- | UnitType.Factory;
+ | UnitType.OilRig;
export const unitTypeToOtherUnit = {
[UnitType.City]: "city",
@@ -54,7 +54,7 @@ export const unitTypeToOtherUnit = {
[UnitType.Port]: "port",
[UnitType.SAMLauncher]: "saml",
[UnitType.Warship]: "wshp",
- [UnitType.Factory]: "fact",
+ [UnitType.OilRig]: "orig",
} as const satisfies Record;
// Attacks
diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts
index 55fbab613..e8f3da497 100644
--- a/src/core/configuration/Config.ts
+++ b/src/core/configuration/Config.ts
@@ -132,7 +132,7 @@ export interface Config {
numTradeShips: number,
): number;
trainGold(rel: "self" | "team" | "ally" | "other"): Gold;
- trainSpawnRate(numPlayerFactories: number): number;
+ trainSpawnRate(numPlayerOilRigs: number): number;
trainStationMinRange(): number;
trainStationMaxRange(): number;
railroadMaxSize(): number;
@@ -170,6 +170,7 @@ export interface Config {
structureMinDist(): number;
isReplay(): boolean;
allianceExtensionPromptOffset(): number;
+ randomSeed(): number;
}
export interface Theme {
diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts
index b4ce99715..65f255b05 100644
--- a/src/core/configuration/DefaultConfig.ts
+++ b/src/core/configuration/DefaultConfig.ts
@@ -265,10 +265,10 @@ export class DefaultConfig implements Config {
return BigInt(this._gameConfig.startingGold ?? 0);
}
- trainSpawnRate(numPlayerFactories: number): number {
- // hyperbolic decay, midpoint at 10 factories
- // expected number of trains = numPlayerFactories / trainSpawnRate(numPlayerFactories)
- return (numPlayerFactories + 10) * 18;
+ trainSpawnRate(numPlayerOilRigs: number): number {
+ // hyperbolic decay, midpoint at 10 oil rigs
+ // expected number of trains = numPlayerOilRigs / trainSpawnRate(numPlayerOilRigs)
+ return (numPlayerOilRigs + 10) * 18;
}
trainGold(rel: "self" | "team" | "ally" | "other"): Gold {
const multiplier = this.goldMultiplier();
@@ -362,7 +362,7 @@ export class DefaultConfig implements Config {
(numUnits: number) =>
Math.min(1_000_000, Math.pow(2, numUnits) * 125_000),
UnitType.Port,
- UnitType.Factory,
+ UnitType.OilRig,
),
constructionDuration: this.instantBuild() ? 0 : 2 * 10,
upgradable: true,
@@ -436,12 +436,12 @@ export class DefaultConfig implements Config {
upgradable: true,
};
break;
- case UnitType.Factory:
+ case UnitType.OilRig:
info = {
cost: this.costWrapper(
(numUnits: number) =>
Math.min(1_000_000, Math.pow(2, numUnits) * 125_000),
- UnitType.Factory,
+ UnitType.OilRig,
UnitType.Port,
),
constructionDuration: this.instantBuild() ? 0 : 2 * 10,
@@ -911,4 +911,8 @@ export class DefaultConfig implements Config {
allianceExtensionPromptOffset(): number {
return 300; // 30 seconds before expiration
}
+
+ randomSeed(): number {
+ return simpleHash(JSON.stringify(this.gameConfig()));
+ }
}
diff --git a/src/core/configuration/PastelTheme.ts b/src/core/configuration/PastelTheme.ts
index 23ae4e653..a06603aa9 100644
--- a/src/core/configuration/PastelTheme.ts
+++ b/src/core/configuration/PastelTheme.ts
@@ -15,6 +15,7 @@ export class PastelTheme implements Theme {
private nationColorAllocator = new ColorAllocator(nationColors, nationColors);
private background = colord("rgb(60,60,60)");
+ private oilField = colord("rgb(55,55,85)");
private shore = colord("rgb(204,203,158)");
private falloutColors = [
colord("rgb(120,255,71)"), // Original color
@@ -147,6 +148,9 @@ export class PastelTheme implements Theme {
// | **Water (Shore)** | 0 | Fixed: `rgb(100, 143, 255)` | Light blue near land. |
// | **Water (Deep)** | 1 - 10+ | `rgb(70, 132, 180)` - `rgb(61, 123, 171)` | Darker blue, adjusted slightly by distance to land. |
terrainColor(gm: GameMap, tile: TileRef): Colord {
+ if (gm.hasOilField(tile)) {
+ return this.oilField;
+ }
const mag = gm.magnitude(tile);
if (gm.isShore(tile)) {
return this.shore;
diff --git a/src/core/configuration/PastelThemeDark.ts b/src/core/configuration/PastelThemeDark.ts
index 2cff80685..780d05501 100644
--- a/src/core/configuration/PastelThemeDark.ts
+++ b/src/core/configuration/PastelThemeDark.ts
@@ -19,6 +19,9 @@ export class PastelThemeDark extends PastelTheme {
// | **Water (Deep)** | 1 - 10+ | `rgb(22, 19, 38)` - `rgb(14, 11, 30)` | Very dark blue/black. |
terrainColor(gm: GameMap, tile: TileRef): Colord {
+ if (gm.hasOilField(tile)) {
+ return colord("rgb(50,40,80)");
+ }
const mag = gm.magnitude(tile);
if (gm.isShore(tile)) {
return this.darkShore;
diff --git a/src/core/execution/CityExecution.ts b/src/core/execution/CityExecution.ts
index fd31c940e..9f3919e38 100644
--- a/src/core/execution/CityExecution.ts
+++ b/src/core/execution/CityExecution.ts
@@ -35,7 +35,7 @@ export class CityExecution implements Execution {
const nearbyFactory = this.mg.hasUnitNearby(
this.city.tile()!,
this.mg.config().trainStationMaxRange(),
- UnitType.Factory,
+ UnitType.OilRig,
);
if (nearbyFactory) {
this.mg.addExecution(new TrainStationExecution(this.city));
diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts
index 2fdd92da1..41b398156 100644
--- a/src/core/execution/ConstructionExecution.ts
+++ b/src/core/execution/ConstructionExecution.ts
@@ -2,10 +2,10 @@ import { Execution, Game, Player, Tick, Unit, UnitType } from "../game/Game";
import { TileRef } from "../game/GameMap";
import { CityExecution } from "./CityExecution";
import { DefensePostExecution } from "./DefensePostExecution";
-import { FactoryExecution } from "./FactoryExecution";
import { MirvExecution } from "./MIRVExecution";
import { MissileSiloExecution } from "./MissileSiloExecution";
import { NukeExecution } from "./NukeExecution";
+import { OilRigExecution } from "./OilRigExecution";
import { PortExecution } from "./PortExecution";
import { SAMLauncherExecution } from "./SAMLauncherExecution";
import { WarshipExecution } from "./WarshipExecution";
@@ -141,8 +141,8 @@ export class ConstructionExecution implements Execution {
case UnitType.City:
this.mg.addExecution(new CityExecution(this.structure!));
break;
- case UnitType.Factory:
- this.mg.addExecution(new FactoryExecution(this.structure!));
+ case UnitType.OilRig:
+ this.mg.addExecution(new OilRigExecution(this.structure!));
break;
default:
console.warn(
@@ -159,7 +159,7 @@ export class ConstructionExecution implements Execution {
case UnitType.DefensePost:
case UnitType.SAMLauncher:
case UnitType.City:
- case UnitType.Factory:
+ case UnitType.OilRig:
return true;
default:
return false;
diff --git a/src/core/execution/FactoryExecution.ts b/src/core/execution/OilRigExecution.ts
similarity index 75%
rename from src/core/execution/FactoryExecution.ts
rename to src/core/execution/OilRigExecution.ts
index 92908ed39..afd2e4130 100644
--- a/src/core/execution/FactoryExecution.ts
+++ b/src/core/execution/OilRigExecution.ts
@@ -1,12 +1,12 @@
import { Execution, Game, Unit, UnitType } from "../game/Game";
import { TrainStationExecution } from "./TrainStationExecution";
-export class FactoryExecution implements Execution {
+export class OilRigExecution implements Execution {
private active: boolean = true;
private game: Game;
private stationCreated = false;
- constructor(private factory: Unit) {}
+ constructor(private oilRig: Unit) {}
init(mg: Game, ticks: number): void {
this.game = mg;
@@ -17,7 +17,7 @@ export class FactoryExecution implements Execution {
this.createStation();
this.stationCreated = true;
}
- if (!this.factory.isActive()) {
+ if (!this.oilRig.isActive()) {
this.active = false;
return;
}
@@ -33,12 +33,12 @@ export class FactoryExecution implements Execution {
private createStation(): void {
const structures = this.game.nearbyUnits(
- this.factory.tile()!,
+ this.oilRig.tile()!,
this.game.config().trainStationMaxRange(),
- [UnitType.City, UnitType.Port, UnitType.Factory],
+ [UnitType.City, UnitType.Port, UnitType.OilRig],
);
- this.game.addExecution(new TrainStationExecution(this.factory, true));
+ this.game.addExecution(new TrainStationExecution(this.oilRig, true));
for (const { unit } of structures) {
if (!unit.hasTrainStation()) {
this.game.addExecution(new TrainStationExecution(unit));
diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts
index 9483f1b70..cd10457e2 100644
--- a/src/core/execution/PortExecution.ts
+++ b/src/core/execution/PortExecution.ts
@@ -87,7 +87,7 @@ export class PortExecution implements Execution {
const nearbyFactory = this.mg.hasUnitNearby(
this.port.tile()!,
this.mg.config().trainStationMaxRange(),
- UnitType.Factory,
+ UnitType.OilRig,
);
if (nearbyFactory) {
this.mg.addExecution(new TrainStationExecution(this.port));
diff --git a/src/core/execution/TrainStationExecution.ts b/src/core/execution/TrainStationExecution.ts
index 5f029a0cc..9936eca08 100644
--- a/src/core/execution/TrainStationExecution.ts
+++ b/src/core/execution/TrainStationExecution.ts
@@ -53,7 +53,7 @@ export class TrainStationExecution implements Execution {
private shouldSpawnTrain(): boolean {
const spawnRate = this.mg
.config()
- .trainSpawnRate(this.unit.owner().unitCount(UnitType.Factory));
+ .trainSpawnRate(this.unit.owner().unitCount(UnitType.OilRig));
for (let i = 0; i < this.unit!.level(); i++) {
if (this.random.chance(spawnRate)) {
return true;
diff --git a/src/core/execution/nation/NationNukeBehavior.ts b/src/core/execution/nation/NationNukeBehavior.ts
index 7ab12df5e..885f0fe23 100644
--- a/src/core/execution/nation/NationNukeBehavior.ts
+++ b/src/core/execution/nation/NationNukeBehavior.ts
@@ -82,7 +82,7 @@ export class NationNukeBehavior {
UnitType.MissileSilo,
UnitType.Port,
UnitType.SAMLauncher,
- UnitType.Factory,
+ UnitType.OilRig,
);
const structureTiles = structures.map((u) => u.tile());
const difficulty = this.game.config().gameConfig().difficulty;
@@ -571,7 +571,7 @@ export class NationNukeBehavior {
return 50_000 * level;
case UnitType.Port:
return 15_000 * level;
- case UnitType.Factory:
+ case UnitType.OilRig:
return 15_000 * level;
default:
return 0;
diff --git a/src/core/execution/nation/NationStructureBehavior.ts b/src/core/execution/nation/NationStructureBehavior.ts
index fcd30353d..5034a5bec 100644
--- a/src/core/execution/nation/NationStructureBehavior.ts
+++ b/src/core/execution/nation/NationStructureBehavior.ts
@@ -46,7 +46,7 @@ function getStructureRatios(
): Partial> {
return {
[UnitType.Port]: { ratioPerCity: 0.75, perceivedCostIncreasePerOwned: 1 },
- [UnitType.Factory]: {
+ [UnitType.OilRig]: {
ratioPerCity: 0.75,
perceivedCostIncreasePerOwned: 1,
},
@@ -105,7 +105,7 @@ export class NationStructureBehavior {
const buildOrder: UnitType[] = [
UnitType.DefensePost,
UnitType.Port,
- UnitType.Factory,
+ UnitType.OilRig,
UnitType.SAMLauncher,
UnitType.MissileSilo,
];
@@ -185,7 +185,7 @@ export class NationStructureBehavior {
// Heavily reduce factory spawning if we have coastal tiles
if (
- type === UnitType.Factory &&
+ type === UnitType.OilRig &&
hasCoastalTiles &&
!gameConfig.isUnitDisabled(UnitType.Port)
) {
@@ -446,7 +446,9 @@ export class NationStructureBehavior {
const tiles =
type === UnitType.Port
? this.randCoastalTileArray(25)
- : randTerritoryTileArray(this.random, this.game, this.player, 25);
+ : type === UnitType.OilRig
+ ? this.randOilFieldTileArray(25)
+ : randTerritoryTileArray(this.random, this.game, this.player, 25);
if (tiles.length === 0) return null;
const valueFunction = this.structureSpawnTileValue(type);
if (valueFunction === null) return null;
@@ -470,6 +472,13 @@ export class NationStructureBehavior {
return Array.from(this.arraySampler(tiles, numTiles));
}
+ private randOilFieldTileArray(numTiles: number): TileRef[] {
+ const tiles = Array.from(this.player.tiles()).filter((t) =>
+ this.game.hasOilField(t),
+ );
+ return Array.from(this.arraySampler(tiles, numTiles));
+ }
+
private *arraySampler(a: T[], sampleSize: number): Generator {
if (a.length <= sampleSize) {
// Return all elements
@@ -490,7 +499,7 @@ export class NationStructureBehavior {
): ((tile: TileRef) => number) | null {
switch (type) {
case UnitType.City:
- case UnitType.Factory:
+ case UnitType.OilRig:
case UnitType.MissileSilo:
return this.interiorStructureValue(type);
case UnitType.Port:
@@ -652,7 +661,7 @@ export class NationStructureBehavior {
for (const unit of player.units()) {
switch (unit.type()) {
case UnitType.City:
- case UnitType.Factory:
+ case UnitType.OilRig:
case UnitType.MissileSilo:
case UnitType.Port:
protectEntries.push({
diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts
index 7381c35c7..69ea876de 100644
--- a/src/core/game/Game.ts
+++ b/src/core/game/Game.ts
@@ -249,7 +249,7 @@ export enum UnitType {
MIRV = "MIRV",
MIRVWarhead = "MIRV Warhead",
Train = "Train",
- Factory = "Factory",
+ OilRig = "OilRig",
}
export enum TrainType {
@@ -264,7 +264,7 @@ const _structureTypes: ReadonlySet = new Set([
UnitType.SAMLauncher,
UnitType.MissileSilo,
UnitType.Port,
- UnitType.Factory,
+ UnitType.OilRig,
]);
export const StructureTypes: readonly UnitType[] = [..._structureTypes];
@@ -318,7 +318,7 @@ export interface UnitParamsMap {
loaded?: boolean;
};
- [UnitType.Factory]: Record;
+ [UnitType.OilRig]: Record;
[UnitType.MissileSilo]: Record;
diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts
index 0339fdb33..9054fd5b1 100644
--- a/src/core/game/GameImpl.ts
+++ b/src/core/game/GameImpl.ts
@@ -41,6 +41,7 @@ import {
} from "./Game";
import { GameMap, TileRef, TileUpdate } from "./GameMap";
import { GameUpdate, GameUpdateType } from "./GameUpdates";
+import { generateOilFields } from "./OilFieldGenerator";
import { PlayerImpl } from "./PlayerImpl";
import { RailNetwork } from "./RailNetwork";
import { createRailNetwork } from "./RailNetworkImpl";
@@ -117,6 +118,8 @@ export class GameImpl implements Game {
}
this.addPlayers();
+ generateOilFields(this._map, this._config);
+
if (!_config.disableNavMesh()) {
const graphBuilder = new AbstractGraphBuilder(this.miniGameMap);
this._miniWaterGraph = graphBuilder.build();
@@ -962,6 +965,12 @@ export class GameImpl implements Game {
setOwnerID(ref: TileRef, playerId: number): void {
return this._map.setOwnerID(ref, playerId);
}
+ hasOilField(ref: TileRef): boolean {
+ return this._map.hasOilField(ref);
+ }
+ setOilField(ref: TileRef, value: boolean): void {
+ return this._map.setOilField(ref, value);
+ }
hasFallout(ref: TileRef): boolean {
return this._map.hasFallout(ref);
}
diff --git a/src/core/game/GameMap.ts b/src/core/game/GameMap.ts
index 136fdf1d9..351b42372 100644
--- a/src/core/game/GameMap.ts
+++ b/src/core/game/GameMap.ts
@@ -25,6 +25,8 @@ export interface GameMap {
hasOwner(ref: TileRef): boolean;
setOwnerID(ref: TileRef, playerId: number): void;
+ hasOilField(ref: TileRef): boolean;
+ setOilField(ref: TileRef, value: boolean): void;
hasFallout(ref: TileRef): boolean;
setFallout(ref: TileRef, value: boolean): void;
isOnEdgeOfMap(ref: TileRef): boolean;
@@ -76,6 +78,7 @@ export class GameMapImpl implements GameMap {
// State bits (Uint16Array)
private static readonly PLAYER_ID_MASK = 0xfff;
+ private static readonly OIL_FIELD_BIT = 12;
private static readonly FALLOUT_BIT = 13;
private static readonly DEFENSE_BONUS_BIT = 14;
// Bit 15 still reserved
@@ -192,6 +195,18 @@ export class GameMapImpl implements GameMap {
(this.state[ref] & ~GameMapImpl.PLAYER_ID_MASK) | playerId;
}
+ hasOilField(ref: TileRef): boolean {
+ return Boolean(this.state[ref] & (1 << GameMapImpl.OIL_FIELD_BIT));
+ }
+
+ setOilField(ref: TileRef, value: boolean): void {
+ if (value) {
+ this.state[ref] |= 1 << GameMapImpl.OIL_FIELD_BIT;
+ } else {
+ this.state[ref] &= ~(1 << GameMapImpl.OIL_FIELD_BIT);
+ }
+ }
+
hasFallout(ref: TileRef): boolean {
return Boolean(this.state[ref] & (1 << GameMapImpl.FALLOUT_BIT));
}
diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts
index f02e0e10b..773ee4685 100644
--- a/src/core/game/GameView.ts
+++ b/src/core/game/GameView.ts
@@ -34,6 +34,7 @@ import {
PlayerUpdate,
UnitUpdate,
} from "./GameUpdates";
+import { generateOilFields } from "./OilFieldGenerator";
import { TerrainMapData } from "./TerrainMapLoader";
import { TerraNulliusImpl } from "./TerraNulliusImpl";
import { UnitGrid, UnitPredicate } from "./UnitGrid";
@@ -627,6 +628,7 @@ export class GameView implements GameMap {
flag: nation.flag,
} satisfies PlayerCosmetics);
}
+ generateOilFields(this._map, this._config);
}
isOnEdgeOfMap(ref: TileRef): boolean {
@@ -900,6 +902,12 @@ export class GameView implements GameMap {
setOwnerID(ref: TileRef, playerId: number): void {
return this._map.setOwnerID(ref, playerId);
}
+ hasOilField(ref: TileRef): boolean {
+ return this._map.hasOilField(ref);
+ }
+ setOilField(ref: TileRef, value: boolean): void {
+ return this._map.setOilField(ref, value);
+ }
hasFallout(ref: TileRef): boolean {
return this._map.hasFallout(ref);
}
diff --git a/src/core/game/OilFieldGenerator.ts b/src/core/game/OilFieldGenerator.ts
new file mode 100644
index 000000000..dab1be3f3
--- /dev/null
+++ b/src/core/game/OilFieldGenerator.ts
@@ -0,0 +1,258 @@
+import { Config } from "../configuration/Config";
+import { PseudoRandom } from "../PseudoRandom";
+import { simpleHash } from "../Util";
+import { GameMap, TileRef } from "./GameMap";
+
+export function generateOilFields(map: GameMap, config: Config) {
+ const random = new PseudoRandom(
+ simpleHash("oil-fields-" + config.randomSeed()),
+ );
+
+ const numFields = random.nextInt(7, 10);
+ const width = map.width();
+ const height = map.height();
+
+ // 1. Grid-based Seeding (Ensures spread across the map)
+ const gridDivs = Math.ceil(Math.sqrt(numFields + 2));
+ const cellW = width / gridDivs;
+ const cellH = height / gridDivs;
+
+ const cells: { r: number; c: number }[] = [];
+ for (let r = 0; r < gridDivs; r++) {
+ for (let c = 0; c < gridDivs; c++) {
+ cells.push({ r, c });
+ }
+ }
+
+ for (let i = cells.length - 1; i > 0; i--) {
+ const j = random.nextInt(0, i + 1);
+ [cells[i], cells[j]] = [cells[j], cells[i]];
+ }
+
+ const seeds: TileRef[] = [];
+ for (let i = 0; i < numFields && i < cells.length; i++) {
+ const cell = cells[i];
+ let foundSeed = false;
+ for (let attempt = 0; attempt < 100; attempt++) {
+ const rx = random.nextInt(
+ Math.floor(cell.c * cellW + cellW * 0.1),
+ Math.floor((cell.c + 1) * cellW - cellW * 0.1),
+ );
+ const ry = random.nextInt(
+ Math.floor(cell.r * cellH + cellH * 0.1),
+ Math.floor((cell.r + 1) * cellH - cellH * 0.1),
+ );
+ if (map.isValidCoord(rx, ry)) {
+ const t = map.ref(rx, ry);
+ if (map.isLand(t)) {
+ seeds.push(t);
+ foundSeed = true;
+ break;
+ }
+ }
+ }
+ if (!foundSeed) {
+ for (let fallback = 0; fallback < 100; fallback++) {
+ const rx = random.nextInt(0, width);
+ const ry = random.nextInt(0, height);
+ const t = map.ref(rx, ry);
+ if (map.isLand(t)) {
+ seeds.push(t);
+ foundSeed = true;
+ break;
+ }
+ }
+ }
+ }
+
+ // 2. Elliptical Noise Base
+ const totalLand = map.numLandTiles();
+ const avgTilesPerField = (totalLand * 0.045) / numFields;
+
+ for (const seed of seeds) {
+ const targetSize = avgTilesPerField * (0.6 + random.next() * 1.8);
+ const centerX = map.x(seed);
+ const centerY = map.y(seed);
+
+ // Random rotation and aspect ratio (not too extreme to avoid "thin" shapes)
+ const angle = random.next() * Math.PI;
+ const cosA = Math.cos(angle);
+ const sinA = Math.sin(angle);
+ const ratio = 0.6 + random.next() * 0.4; // 1:1 to 1:1.6
+
+ // Rough radius based on area A = PI * r1 * r2 => A = PI * r * (r * ratio)
+ const r1 = Math.sqrt(targetSize / (Math.PI * ratio));
+ const r2 = r1 * ratio;
+
+ const fieldTiles = new Set();
+
+ // Fill an ellipse with noise - this is the "raw material" for the CA
+ const searchRadius = Math.ceil(Math.max(r1, r2) * 1.5);
+ for (let dx = -searchRadius; dx <= searchRadius; dx++) {
+ for (let dy = -searchRadius; dy <= searchRadius; dy++) {
+ const x = centerX + dx;
+ const y = centerY + dy;
+ if (!map.isValidCoord(x, y)) continue;
+
+ // Rotated coordinate system
+ const rx = dx * cosA + dy * sinA;
+ const ry = -dx * sinA + dy * cosA;
+
+ const dist = (rx * rx) / (r1 * r1) + (ry * ry) / (r2 * r2);
+
+ // Add jittered probability to create organic edges
+ const prob = 0.85 - dist * 0.5;
+ if (random.next() < prob && map.isLand(map.ref(x, y))) {
+ const t = map.ref(x, y);
+ map.setOilField(t, true);
+ fieldTiles.add(t);
+ }
+ }
+ }
+
+ // 3. Heavy Smoothing Pass (Cellular Automata)
+ // Running 5 passes creates very clean, lumpy organic blobs
+ for (let pass = 0; pass < 5; pass++) {
+ const toAdd: TileRef[] = [];
+ const toRemove: TileRef[] = [];
+
+ // Slightly larger bounds to allow smoothing to expand/contract
+ const bounds = getBounds(map, fieldTiles, 2);
+ for (let x = bounds.minX; x <= bounds.maxX; x++) {
+ for (let y = bounds.minY; y <= bounds.maxY; y++) {
+ if (!map.isValidCoord(x, y)) continue;
+ const t = map.ref(x, y);
+ const isLand = map.isLand(t);
+
+ let oilNeighbors = 0;
+ for (let dx = -1; dx <= 1; dx++) {
+ for (let dy = -1; dy <= 1; dy++) {
+ if (dx === 0 && dy === 0) continue;
+ const nx = x + dx,
+ ny = y + dy;
+ if (
+ map.isValidCoord(nx, ny) &&
+ map.hasOilField(map.ref(nx, ny))
+ ) {
+ oilNeighbors++;
+ }
+ }
+ }
+
+ if (map.hasOilField(t)) {
+ // Standard CA "Life" rules for smoothing:
+ // Keep if 4+ neighbors, remove if 3 or less (prunes thin parts)
+ if (oilNeighbors <= 3 || !isLand) {
+ toRemove.push(t);
+ }
+ } else if (isLand) {
+ // Fill if 5+ neighbors (fills gaps/holes)
+ if (oilNeighbors >= 5) {
+ toAdd.push(t);
+ }
+ }
+ }
+ }
+
+ for (const t of toAdd) {
+ map.setOilField(t, true);
+ fieldTiles.add(t);
+ }
+ for (const t of toRemove) {
+ map.setOilField(t, false);
+ fieldTiles.delete(t);
+ }
+ }
+
+ // 4. Guaranteed Hole Filling (Flood fill)
+ if (fieldTiles.size > 0) {
+ fillHoles(map, fieldTiles);
+ }
+ }
+}
+
+function fillHoles(map: GameMap, fieldTiles: Set) {
+ const bounds = getBounds(map, fieldTiles, 1);
+ const outside = new Set();
+ const queue: TileRef[] = [];
+
+ for (let x = bounds.minX; x <= bounds.maxX; x++) {
+ for (let y = bounds.minY; y <= bounds.maxY; y++) {
+ if (
+ x === bounds.minX ||
+ x === bounds.maxX ||
+ y === bounds.minY ||
+ y === bounds.maxY
+ ) {
+ if (map.isValidCoord(x, y)) {
+ const t = map.ref(x, y);
+ if (!map.hasOilField(t)) {
+ outside.add(t);
+ queue.push(t);
+ }
+ }
+ }
+ }
+ }
+
+ while (queue.length > 0) {
+ const curr = queue.shift()!;
+ const cx = map.x(curr);
+ const cy = map.y(curr);
+ const dirs = [
+ [0, 1],
+ [0, -1],
+ [1, 0],
+ [-1, 0],
+ ];
+ for (const [dx, dy] of dirs) {
+ const nx = cx + dx,
+ ny = cy + dy;
+ if (
+ nx >= bounds.minX &&
+ nx <= bounds.maxX &&
+ ny >= bounds.minY &&
+ ny <= bounds.maxY &&
+ map.isValidCoord(nx, ny)
+ ) {
+ const next = map.ref(nx, ny);
+ if (!map.hasOilField(next) && !outside.has(next)) {
+ outside.add(next);
+ queue.push(next);
+ }
+ }
+ }
+ }
+
+ for (let x = bounds.minX; x <= bounds.maxX; x++) {
+ for (let y = bounds.minY; y <= bounds.maxY; y++) {
+ if (map.isValidCoord(x, y)) {
+ const t = map.ref(x, y);
+ if (!map.hasOilField(t) && !outside.has(t) && map.isLand(t)) {
+ map.setOilField(t, true);
+ }
+ }
+ }
+ }
+}
+
+function getBounds(map: GameMap, tiles: Set, margin: number) {
+ let minX = Infinity,
+ maxX = -Infinity,
+ minY = Infinity,
+ maxY = -Infinity;
+ for (const t of tiles) {
+ const x = map.x(t);
+ const y = map.y(t);
+ if (x < minX) minX = x;
+ if (x > maxX) maxX = x;
+ if (y < minY) minY = y;
+ if (y > maxY) maxY = y;
+ }
+ return {
+ minX: minX - margin,
+ maxX: maxX + margin,
+ minY: minY - margin,
+ maxY: maxY + margin,
+ };
+}
diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts
index 2f4d78dc0..1baeb91dc 100644
--- a/src/core/game/PlayerImpl.ts
+++ b/src/core/game/PlayerImpl.ts
@@ -1042,6 +1042,10 @@ export class PlayerImpl implements Player {
(units === undefined || units.some((u) => isStructureType(u)))
? this.validStructureSpawnTiles(tile)
: [];
+ const validOilRigTiles =
+ tile !== null && (units === undefined || units.includes(UnitType.OilRig))
+ ? this.validStructureSpawnTiles(tile, true)
+ : [];
return Object.values(UnitType)
.filter((u) => units === undefined || units.includes(u))
.map((u) => {
@@ -1053,7 +1057,11 @@ export class PlayerImpl implements Player {
canUpgrade = existingUnit.id();
}
if (tile !== null) {
- canBuild = this.canBuild(u, tile, validTiles);
+ canBuild = this.canBuild(
+ u,
+ tile,
+ u === UnitType.OilRig ? validOilRigTiles : validTiles,
+ );
}
}
return {
@@ -1117,8 +1125,12 @@ export class PlayerImpl implements Player {
case UnitType.DefensePost:
case UnitType.SAMLauncher:
case UnitType.City:
- case UnitType.Factory:
- return this.landBasedStructureSpawn(targetTile, validTiles);
+ case UnitType.OilRig:
+ return this.landBasedStructureSpawn(
+ targetTile,
+ validTiles,
+ unitType === UnitType.OilRig,
+ );
default:
assertNever(unitType);
}
@@ -1209,18 +1221,25 @@ export class PlayerImpl implements Player {
landBasedStructureSpawn(
tile: TileRef,
validTiles: TileRef[] | null = null,
+ isOilRig: boolean = false,
): TileRef | false {
- const tiles = validTiles ?? this.validStructureSpawnTiles(tile);
+ const tiles = validTiles ?? this.validStructureSpawnTiles(tile, isOilRig);
if (tiles.length === 0) {
return false;
}
return tiles[0];
}
- private validStructureSpawnTiles(tile: TileRef): TileRef[] {
+ private validStructureSpawnTiles(
+ tile: TileRef,
+ isOilRig: boolean = false,
+ ): TileRef[] {
if (this.mg.owner(tile) !== this) {
return [];
}
+ if (isOilRig && !this.mg.hasOilField(tile)) {
+ return [];
+ }
const searchRadius = 15;
const searchRadiusSquared = searchRadius ** 2;
@@ -1234,7 +1253,8 @@ export class PlayerImpl implements Player {
const nearbyTiles = this.mg.bfs(tile, (gm, t) => {
return (
this.mg.euclideanDistSquared(tile, t) < searchRadiusSquared &&
- gm.ownerID(t) === this.smallID()
+ gm.ownerID(t) === this.smallID() &&
+ (!isOilRig || gm.hasOilField(t))
);
});
const validSet: Set = new Set(nearbyTiles);
diff --git a/src/core/game/RailNetworkImpl.ts b/src/core/game/RailNetworkImpl.ts
index 813098401..b44df5785 100644
--- a/src/core/game/RailNetworkImpl.ts
+++ b/src/core/game/RailNetworkImpl.ts
@@ -248,13 +248,13 @@ export class RailNetworkImpl implements RailNetwork {
const maxPathSize = this.game.config().railroadMaxSize();
// Cannot connect if outside the max range of a factory
- if (!this.game.hasUnitNearby(tile, maxRange, UnitType.Factory)) {
+ if (!this.game.hasUnitNearby(tile, maxRange, UnitType.OilRig)) {
return [];
}
const neighbors = this.game.nearbyUnits(tile, maxRange, [
UnitType.City,
- UnitType.Factory,
+ UnitType.OilRig,
UnitType.Port,
]);
neighbors.sort((a, b) => a.distSquared - b.distSquared);
@@ -293,7 +293,7 @@ export class RailNetworkImpl implements RailNetwork {
const neighbors = this.game.nearbyUnits(
station.tile(),
this.game.config().trainStationMaxRange(),
- [UnitType.City, UnitType.Factory, UnitType.Port],
+ [UnitType.City, UnitType.OilRig, UnitType.Port],
);
const editedClusters = new Set();
diff --git a/src/core/game/TrainStation.ts b/src/core/game/TrainStation.ts
index e2b687a6f..cb35a0916 100644
--- a/src/core/game/TrainStation.ts
+++ b/src/core/game/TrainStation.ts
@@ -45,7 +45,7 @@ export function createTrainStopHandlers(
return {
[UnitType.City]: new TradeStationStopHandler(),
[UnitType.Port]: new TradeStationStopHandler(),
- [UnitType.Factory]: new FactoryStopHandler(),
+ [UnitType.OilRig]: new FactoryStopHandler(),
};
}
diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts
index 79b7c1ed8..e20e2f661 100644
--- a/src/core/game/UnitImpl.ts
+++ b/src/core/game/UnitImpl.ts
@@ -76,7 +76,7 @@ export class UnitImpl implements Unit {
case UnitType.DefensePost:
case UnitType.SAMLauncher:
case UnitType.City:
- case UnitType.Factory:
+ case UnitType.OilRig:
this.mg.stats().unitBuild(_owner, this._type);
}
}
@@ -195,7 +195,7 @@ export class UnitImpl implements Unit {
case UnitType.DefensePost:
case UnitType.SAMLauncher:
case UnitType.City:
- case UnitType.Factory:
+ case UnitType.OilRig:
this.mg.stats().unitCapture(newOwner, this._type);
this.mg.stats().unitLose(this._owner, this._type);
break;
@@ -290,7 +290,7 @@ export class UnitImpl implements Unit {
case UnitType.Port:
case UnitType.SAMLauncher:
case UnitType.Warship:
- case UnitType.Factory:
+ case UnitType.OilRig:
this.mg.stats().unitDestroy(destroyer, this._type);
this.mg.stats().unitLose(this.owner(), this._type);
break;
diff --git a/tests/client/graphics/RadialMenuElements.test.ts b/tests/client/graphics/RadialMenuElements.test.ts
index 3404accbf..09d1667f1 100644
--- a/tests/client/graphics/RadialMenuElements.test.ts
+++ b/tests/client/graphics/RadialMenuElements.test.ts
@@ -30,9 +30,9 @@ vi.mock("../../../src/client/graphics/layers/BuildMenu", async () => {
countable: true,
},
{
- unitType: UnitType.Factory,
- key: "unit_type.factory",
- description: "unit_type.factory_desc",
+ unitType: UnitType.OilRig,
+ key: "unit_type.oil_rig",
+ description: "unit_type.oil_rig_desc",
icon: "factory-icon",
countable: true,
},
@@ -120,7 +120,7 @@ describe("RadialMenuElements", () => {
mockPlayerActions = {
buildableUnits: [
{ type: UnitType.City, canBuild: true },
- { type: UnitType.Factory, canBuild: true },
+ { type: UnitType.OilRig, canBuild: true },
{ type: UnitType.AtomBomb, canBuild: true },
{ type: UnitType.Warship, canBuild: true },
{ type: UnitType.HydrogenBomb, canBuild: true },
@@ -211,7 +211,7 @@ describe("RadialMenuElements", () => {
const subMenu = attackMenuElement.subMenu!(mockParams);
- const constructionUnitTypes = [UnitType.City, UnitType.Factory];
+ const constructionUnitTypes = [UnitType.City, UnitType.OilRig];
const returnedUnitTypes = subMenu.map((item) => {
const unitTypeStr = item.id.replace("attack_", "");
return Object.values(UnitType).find(
@@ -254,7 +254,7 @@ describe("RadialMenuElements", () => {
expect(subMenu).toBeDefined();
expect(subMenu.length).toBeGreaterThan(0);
- const constructionUnitTypes = [UnitType.City, UnitType.Factory];
+ const constructionUnitTypes = [UnitType.City, UnitType.OilRig];
const returnedUnitTypes = subMenu.map((item) => {
const unitTypeStr = item.id.replace("build_", "");
return Object.values(UnitType).find(
@@ -597,8 +597,8 @@ describe("RadialMenuElements", () => {
expect(translateText).toHaveBeenCalledWith("unit_type.city");
expect(translateText).toHaveBeenCalledWith("unit_type.city_desc");
- expect(translateText).toHaveBeenCalledWith("unit_type.factory");
- expect(translateText).toHaveBeenCalledWith("unit_type.factory_desc");
+ expect(translateText).toHaveBeenCalledWith("unit_type.oil_rig");
+ expect(translateText).toHaveBeenCalledWith("unit_type.oil_rig_desc");
});
it("should use translateText for tooltip items in attack menu", async () => {