Add trains (#1159)

## Description:

Add a rail network to handle train stations/railroad between structures.

Changes:
- `RailNetwork` is responsible for the train station graph. Use it to
connect new `TrainStations`
- A `RailRoad` connects two `TrainStation`
- No loop possible in the rail network
- Train stations handles its railroads
- Added a layer to draw the railroads under the structures

#### Clusters
- To speed up computations, each `TrainStation` references its own
cluster
- A cluster is a list of `TrainStation` connected with each other,
created by the `RailNetwork` when connecting the station
- Train stations spawn trains randomly depending on its current cluster
size
- A `TrainStation` decides randomly of the train destination by picking
one from the cluster

#### Production building:
- Added a factory which has no gameplay impact currently. _To be
discussed._

#### Train stops:
- When a train reaches a factory, it's filled with a "cargo". The loaded
trains has no impact currently. _To be discussed._
- When a train reaches a city, the player earn 10k gold
- When a train reaches a port, it sends a new tradeship if possible
- If a destination/source is destroyed, the train & railroad are deleted
too


https://github.com/user-attachments/assets/42375c17-9e04-4a42-98d0-708c81ffd609


https://github.com/user-attachments/assets/fbecdb53-a516-4df8-87fb-1f9a62c4efa0



## 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
- [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:

IngloriousTom

---------

Co-authored-by: Scott Anderson <scottanderson@users.noreply.github.com>
This commit is contained in:
DevelopingTom
2025-06-22 08:14:08 -07:00
committed by GitHub
co-authored by Scott Anderson
parent 0f2008a68d
commit 43397779fa
60 changed files with 2427 additions and 104 deletions
+56 -10
View File
@@ -4,13 +4,25 @@ import hydrogenBombSprite from "../../../resources/sprites/hydrogenbomb.png";
import mirvSprite from "../../../resources/sprites/mirv2.png";
import samMissileSprite from "../../../resources/sprites/samMissile.png";
import tradeShipSprite from "../../../resources/sprites/tradeship.png";
import trainCarriageSprite from "../../../resources/sprites/trainCarriage.png";
import trainLoadedCarriageSprite from "../../../resources/sprites/trainCarriageLoaded.png";
import trainEngineSprite from "../../../resources/sprites/trainEngine.png";
import transportShipSprite from "../../../resources/sprites/transportship.png";
import warshipSprite from "../../../resources/sprites/warship.png";
import { Theme } from "../../core/configuration/Config";
import { UnitType } from "../../core/game/Game";
import { TrainType, UnitType } from "../../core/game/Game";
import { UnitView } from "../../core/game/GameView";
const SPRITE_CONFIG: Partial<Record<UnitType, string>> = {
// Can't reuse TrainType because "loaded" is not a type, just an attribute
const TrainTypeSprite = {
Engine: "Engine",
Carriage: "Carriage",
LoadedCarriage: "LoadedCarriage",
} as const;
type TrainTypeSprite = (typeof TrainTypeSprite)[keyof typeof TrainTypeSprite];
const SPRITE_CONFIG: Partial<Record<UnitType | TrainTypeSprite, string>> = {
[UnitType.TransportShip]: transportShipSprite,
[UnitType.Warship]: warshipSprite,
[UnitType.SAMMissile]: samMissileSprite,
@@ -18,9 +30,12 @@ const SPRITE_CONFIG: Partial<Record<UnitType, string>> = {
[UnitType.HydrogenBomb]: hydrogenBombSprite,
[UnitType.TradeShip]: tradeShipSprite,
[UnitType.MIRV]: mirvSprite,
[TrainTypeSprite.Engine]: trainEngineSprite,
[TrainTypeSprite.Carriage]: trainCarriageSprite,
[TrainTypeSprite.LoadedCarriage]: trainLoadedCarriageSprite,
};
const spriteMap: Map<UnitType, ImageBitmap> = new Map();
const spriteMap: Map<UnitType | TrainTypeSprite, ImageBitmap> = new Map();
// preload all images
export const loadAllSprites = async (): Promise<void> => {
@@ -30,7 +45,7 @@ export const loadAllSprites = async (): Promise<void> => {
await Promise.all(
entries.map(async ([unitType, url]) => {
const typedUnitType = unitType as UnitType;
const typedUnitType = unitType as UnitType | TrainTypeSprite;
if (!url || url === "") {
console.warn(`No sprite URL for ${typedUnitType}, skipping...`);
@@ -61,11 +76,32 @@ export const loadAllSprites = async (): Promise<void> => {
);
};
const getSpriteForUnit = (unitType: UnitType): ImageBitmap | null => {
return spriteMap.get(unitType) ?? null;
/**
* The train sprites rely on the train attributes and not only on its type
*/
function trainTypeToSpriteType(unit: UnitView): TrainTypeSprite {
return unit.trainType() === TrainType.Engine
? TrainTypeSprite.Engine
: unit.isLoaded()
? TrainTypeSprite.LoadedCarriage
: TrainTypeSprite.Carriage;
}
const getSpriteForUnit = (unit: UnitView): ImageBitmap | null => {
const unitType = unit.type();
if (unitType === UnitType.Train) {
const trainType = trainTypeToSpriteType(unit);
return spriteMap.get(trainType) || null;
}
return spriteMap.get(unitType) || null;
};
export const isSpriteReady = (unitType: UnitType): boolean => {
export const isSpriteReady = (unit: UnitView): boolean => {
const unitType = unit.type();
if (unitType === UnitType.Train) {
const trainType = trainTypeToSpriteType(unit);
return spriteMap.has(trainType);
}
return spriteMap.has(unitType);
};
@@ -118,6 +154,17 @@ export const colorizeCanvas = (
return canvas;
};
function computeSpriteKey(
unit: UnitView,
territoryColor: Colord,
borderColor: Colord,
): string {
const owner = unit.owner();
const type = `${unit.type()}-${unit.trainType()}-${unit.isLoaded()}`;
const key = `${type}-${owner.id()}-${territoryColor.toRgbString()}-${borderColor.toRgbString()}`;
return key;
}
export const getColoredSprite = (
unit: UnitView,
theme: Theme,
@@ -129,13 +176,12 @@ export const getColoredSprite = (
customTerritoryColor ?? theme.territoryColor(owner);
const borderColor: Colord = customBorderColor ?? theme.borderColor(owner);
const spawnHighlightColor = theme.spawnHighlightColor();
const key = `${unit.type()}-${owner.id()}-${territoryColor.toRgbString()}-${borderColor.toRgbString()}`;
const key = computeSpriteKey(unit, territoryColor, borderColor);
if (coloredSpriteCache.has(key)) {
return coloredSpriteCache.get(key)!;
}
const sprite = getSpriteForUnit(unit.type());
const sprite = getSpriteForUnit(unit);
if (sprite === null) {
throw new Error(`Failed to load sprite for ${unit.type()}`);
}