all my stuff
|
Before Width: | Height: | Size: 938 KiB After Width: | Height: | Size: 282 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 7.2 MiB After Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 654 KiB After Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 663 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 MiB After Width: | Height: | Size: 469 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 662 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 9.8 MiB After Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 6.1 MiB After Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -292,7 +292,7 @@ export class TerritoryLayer implements Layer {
|
||||
this.game.x(tile),
|
||||
this.game.y(tile),
|
||||
this.theme.territoryColor(owner),
|
||||
150,
|
||||
200,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,16 +450,57 @@ export class DefaultConfig implements Config {
|
||||
mag = 85;
|
||||
speed = 16.5;
|
||||
break;
|
||||
case TerrainType.Highland:
|
||||
case TerrainType.Forest:
|
||||
mag = 95;
|
||||
speed = 18;
|
||||
break; // Forests slightly harder
|
||||
case TerrainType.Desert:
|
||||
mag = 75;
|
||||
speed = 20;
|
||||
break; // Deserts easier to defend, slower to cross
|
||||
case TerrainType.DesertTransition:
|
||||
mag = 80;
|
||||
speed = 17;
|
||||
break;
|
||||
case TerrainType.ArcticForest:
|
||||
mag = 90;
|
||||
speed = 22;
|
||||
break; // Renamed Arctic
|
||||
case TerrainType.Beach:
|
||||
mag = 70;
|
||||
speed = 15;
|
||||
break; // Easy to attack/cross
|
||||
case TerrainType.MidMountain:
|
||||
mag = 100;
|
||||
speed = 20;
|
||||
break;
|
||||
case TerrainType.Mountain:
|
||||
case TerrainType.HighMountain:
|
||||
mag = 120;
|
||||
speed = 25;
|
||||
break;
|
||||
case TerrainType.Jungle:
|
||||
mag = 95;
|
||||
speed = 20;
|
||||
break; // Difficult terrain
|
||||
case TerrainType.JunglePlains:
|
||||
mag = 85;
|
||||
speed = 18;
|
||||
break; // Easier than pure jungle
|
||||
case TerrainType.ArcticPlains:
|
||||
mag = 85;
|
||||
speed = 18;
|
||||
break; // Similar to plains but maybe slightly slower
|
||||
case TerrainType.SnowyHighMountain:
|
||||
mag = 120;
|
||||
speed = 25;
|
||||
break; // Same as High Mountain
|
||||
case TerrainType.Lake:
|
||||
case TerrainType.Ocean:
|
||||
throw new Error(
|
||||
`Cannot calculate attack logic for water type: ${type}`,
|
||||
);
|
||||
default:
|
||||
throw new Error(`terrain type ${type} not supported`);
|
||||
assertNever(type); // Ensure all types are handled
|
||||
}
|
||||
if (defender.isPlayer()) {
|
||||
for (const dp of gm.nearbyUnits(
|
||||
|
||||
@@ -23,8 +23,6 @@ export const pastelTheme = new (class implements Theme {
|
||||
private rand = new PseudoRandom(123);
|
||||
|
||||
private background = colord({ r: 60, g: 60, b: 60 });
|
||||
private land = colord({ r: 194, g: 193, b: 148 });
|
||||
private shore = colord({ r: 204, g: 203, b: 158 });
|
||||
private falloutColors = [
|
||||
colord({ r: 120, g: 255, b: 71 }), // Original color
|
||||
colord({ r: 130, g: 255, b: 85 }), // Slightly lighter
|
||||
@@ -32,8 +30,8 @@ export const pastelTheme = new (class implements Theme {
|
||||
colord({ r: 125, g: 255, b: 75 }), // Warmer tint
|
||||
colord({ r: 115, g: 250, b: 68 }), // Cooler tint
|
||||
];
|
||||
private water = colord({ r: 70, g: 132, b: 180 });
|
||||
private shorelineWater = colord({ r: 100, g: 143, b: 255 });
|
||||
private water = colord({ r: 70, g: 122, b: 207 });
|
||||
private shorelineWater = colord({ r: 84, g: 139, b: 226 });
|
||||
|
||||
private _selfColor = colord({ r: 0, g: 255, b: 0 });
|
||||
private _allyColor = colord({ r: 255, g: 255, b: 0 });
|
||||
@@ -115,41 +113,48 @@ export const pastelTheme = new (class implements Theme {
|
||||
}
|
||||
|
||||
terrainColor(gm: GameMap, tile: TileRef): Colord {
|
||||
const mag = gm.magnitude(tile);
|
||||
if (gm.isShore(tile)) {
|
||||
return this.shore;
|
||||
}
|
||||
switch (gm.terrainType(tile)) {
|
||||
const type = gm.terrainType(tile);
|
||||
const mag = gm.magnitude(tile); // Magnitude for water is distance/2
|
||||
|
||||
switch (type) {
|
||||
case TerrainType.Ocean:
|
||||
case TerrainType.Lake:
|
||||
const w = this.water.rgba;
|
||||
if (gm.isShoreline(tile) && gm.isWater(tile)) {
|
||||
return this.shorelineWater;
|
||||
}
|
||||
const baseWater = this.water.rgba;
|
||||
const adjustment = 11 - Math.min(mag, 10); // Water magnitude influence
|
||||
return colord({
|
||||
r: Math.max(w.r - 10 + (11 - Math.min(mag, 10)), 0),
|
||||
g: Math.max(w.g - 10 + (11 - Math.min(mag, 10)), 0),
|
||||
b: Math.max(w.b - 10 + (11 - Math.min(mag, 10)), 0),
|
||||
r: Math.max(baseWater.r - 10 + adjustment, 0),
|
||||
g: Math.max(baseWater.g - 10 + adjustment, 0),
|
||||
b: Math.max(baseWater.b - 10 + adjustment, 0),
|
||||
});
|
||||
|
||||
case TerrainType.Plains:
|
||||
return colord({
|
||||
r: 190,
|
||||
g: 220 - 2 * mag,
|
||||
b: 138,
|
||||
});
|
||||
case TerrainType.Highland:
|
||||
return colord({
|
||||
r: 200 + 2 * mag,
|
||||
g: 183 + 2 * mag,
|
||||
b: 138 + 2 * mag,
|
||||
});
|
||||
case TerrainType.Mountain:
|
||||
return colord({
|
||||
r: 230 + mag / 2,
|
||||
g: 230 + mag / 2,
|
||||
b: 230 + mag / 2,
|
||||
});
|
||||
return colord("#7CB25C");
|
||||
case TerrainType.Forest:
|
||||
return colord("#628144");
|
||||
case TerrainType.Desert:
|
||||
return colord("#e5d789");
|
||||
case TerrainType.DesertTransition:
|
||||
return colord("#e0b641");
|
||||
case TerrainType.ArcticForest:
|
||||
return colord("#98C0E0");
|
||||
case TerrainType.Beach:
|
||||
return colord("#dec470");
|
||||
case TerrainType.MidMountain:
|
||||
return colord("#5C615E");
|
||||
case TerrainType.HighMountain:
|
||||
return colord("#43484a");
|
||||
case TerrainType.Jungle:
|
||||
return colord("#22752d");
|
||||
case TerrainType.JunglePlains:
|
||||
return colord("#47a35d");
|
||||
case TerrainType.ArcticPlains:
|
||||
return colord("#adcfed");
|
||||
case TerrainType.SnowyHighMountain:
|
||||
return colord("#e7f5ff");
|
||||
default:
|
||||
return this.background; // Fallback
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,6 @@ export const pastelThemeDark = new (class implements Theme {
|
||||
private rand = new PseudoRandom(123);
|
||||
|
||||
private background = colord({ r: 0, g: 0, b: 0 });
|
||||
private land = colord({ r: 194, g: 193, b: 148 });
|
||||
private shore = colord({ r: 134, g: 133, b: 88 });
|
||||
private falloutColors = [
|
||||
colord({ r: 120, g: 255, b: 71 }), // Original color
|
||||
colord({ r: 130, g: 255, b: 85 }), // Slightly lighter
|
||||
@@ -115,43 +113,48 @@ export const pastelThemeDark = new (class implements Theme {
|
||||
}
|
||||
|
||||
terrainColor(gm: GameMap, tile: TileRef): Colord {
|
||||
const mag = gm.magnitude(tile);
|
||||
if (gm.isShore(tile)) {
|
||||
return this.shore;
|
||||
}
|
||||
switch (gm.terrainType(tile)) {
|
||||
const type = gm.terrainType(tile);
|
||||
const mag = gm.magnitude(tile); // Magnitude for water is distance/2
|
||||
|
||||
switch (type) {
|
||||
case TerrainType.Ocean:
|
||||
case TerrainType.Lake:
|
||||
const w = this.water.rgba;
|
||||
if (gm.isShoreline(tile) && gm.isWater(tile)) {
|
||||
return this.shorelineWater;
|
||||
}
|
||||
if (gm.magnitude(tile) < 10) {
|
||||
return colord({
|
||||
r: Math.max(w.r + 9 - mag, 0),
|
||||
g: Math.max(w.g + 9 - mag, 0),
|
||||
b: Math.max(w.b + 9 - mag, 0),
|
||||
});
|
||||
}
|
||||
return this.water;
|
||||
const baseWater = this.water.rgba;
|
||||
const adjustment = 9 - Math.min(mag, 10); // Water magnitude influence
|
||||
return colord({
|
||||
r: Math.max(baseWater.r + adjustment, 0),
|
||||
g: Math.max(baseWater.g + adjustment, 0),
|
||||
b: Math.max(baseWater.b + adjustment, 0),
|
||||
});
|
||||
case TerrainType.Plains:
|
||||
return colord({
|
||||
r: 140,
|
||||
g: 170 - 2 * mag,
|
||||
b: 88,
|
||||
});
|
||||
case TerrainType.Highland:
|
||||
return colord({
|
||||
r: 150 + 2 * mag,
|
||||
g: 133 + 2 * mag,
|
||||
b: 88 + 2 * mag,
|
||||
});
|
||||
case TerrainType.Mountain:
|
||||
return colord({
|
||||
r: 180 + mag / 2,
|
||||
g: 180 + mag / 2,
|
||||
b: 180 + mag / 2,
|
||||
});
|
||||
return colord("#64823c"); // Darker #7cb25c
|
||||
case TerrainType.Forest:
|
||||
return colord("#3d551e"); // Darker #61853e
|
||||
case TerrainType.Desert:
|
||||
return colord("#b8a971"); // Darker #e8db91
|
||||
case TerrainType.DesertTransition:
|
||||
return colord("#b58a35"); // Darker #e5ba45
|
||||
case TerrainType.ArcticForest:
|
||||
return colord("#78a0b0"); // Darker #98c0e0
|
||||
case TerrainType.Beach:
|
||||
return colord("#b09859"); // Darker #e0c879
|
||||
case TerrainType.MidMountain:
|
||||
return colord("#3c413e"); // Darker #5c615e
|
||||
case TerrainType.HighMountain:
|
||||
return colord("#23282a"); // Darker #43484a
|
||||
case TerrainType.Jungle:
|
||||
return colord("#12451d"); // Darker #22752d
|
||||
case TerrainType.JunglePlains:
|
||||
return colord("#27733d"); // Darker #47a35d
|
||||
case TerrainType.ArcticPlains:
|
||||
return colord("#8dafcd"); // Darker #adcfed
|
||||
case TerrainType.SnowyHighMountain:
|
||||
return colord("#c7d5df"); // Darker #e7f5ff (more gray)
|
||||
default:
|
||||
return this.background; // Fallback
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -274,15 +274,36 @@ export class AttackExecution implements Execution {
|
||||
.neighbors(neighbor)
|
||||
.filter((t) => this.mg.owner(t) == this._owner).length;
|
||||
let mag = 0;
|
||||
switch (this.mg.terrainType(tile)) {
|
||||
const terrainType = this.mg.terrainType(tile);
|
||||
switch (terrainType) {
|
||||
case TerrainType.Plains:
|
||||
mag = 1;
|
||||
case TerrainType.DesertTransition:
|
||||
case TerrainType.ArcticPlains:
|
||||
case TerrainType.JunglePlains:
|
||||
mag = 1.0;
|
||||
break;
|
||||
case TerrainType.Highland:
|
||||
mag = 1.5;
|
||||
case TerrainType.Beach:
|
||||
mag = 0.8;
|
||||
break; // Easier to take
|
||||
case TerrainType.Forest:
|
||||
case TerrainType.ArcticForest:
|
||||
mag = 1.2;
|
||||
break;
|
||||
case TerrainType.Mountain:
|
||||
mag = 2;
|
||||
case TerrainType.Desert:
|
||||
mag = 1.3;
|
||||
break; // Slightly harder than plains due to conditions
|
||||
case TerrainType.Jungle:
|
||||
mag = 1.8;
|
||||
break; // Harder
|
||||
case TerrainType.MidMountain:
|
||||
mag = 2.0;
|
||||
break;
|
||||
case TerrainType.HighMountain:
|
||||
case TerrainType.SnowyHighMountain:
|
||||
mag = 2.5;
|
||||
break;
|
||||
default:
|
||||
mag = 1.0;
|
||||
break;
|
||||
}
|
||||
this.toConquer.enqueue(
|
||||
|
||||
@@ -563,8 +563,11 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
const tile = this.mg.ref(x, y);
|
||||
if (this.mg.isLand(tile) && !this.mg.hasOwner(tile)) {
|
||||
const terrainType = this.mg.terrainType(tile);
|
||||
if (
|
||||
this.mg.terrainType(tile) == TerrainType.Mountain &&
|
||||
(terrainType === TerrainType.MidMountain ||
|
||||
terrainType === TerrainType.HighMountain ||
|
||||
terrainType === TerrainType.SnowyHighMountain) &&
|
||||
this.random.chance(2)
|
||||
) {
|
||||
continue;
|
||||
|
||||
@@ -88,7 +88,7 @@ export const mapCategories: Record<string, GameMapType[]> = {
|
||||
GameMapType.Oceania,
|
||||
],
|
||||
regional: [
|
||||
GameMapType.BlackSea,
|
||||
// GameMapType.BlackSea, - the heightmap is reversed cant make it into terrain for now i have to remake it from scratch
|
||||
GameMapType.Britannia,
|
||||
GameMapType.GatewayToTheAtlantic,
|
||||
GameMapType.BetweenTwoSeas,
|
||||
@@ -193,11 +193,20 @@ export class Cell {
|
||||
}
|
||||
|
||||
export enum TerrainType {
|
||||
Plains,
|
||||
Highland,
|
||||
Mountain,
|
||||
Lake,
|
||||
Ocean,
|
||||
Ocean = 0,
|
||||
Lake = 1,
|
||||
Plains = 2,
|
||||
Forest = 3,
|
||||
Desert = 4,
|
||||
DesertTransition = 5,
|
||||
ArcticForest = 6, // Renamed from Arctic
|
||||
Beach = 7, // Replaced LowMountain
|
||||
MidMountain = 8,
|
||||
HighMountain = 9,
|
||||
Jungle = 10,
|
||||
JunglePlains = 11,
|
||||
ArcticPlains = 12, // New
|
||||
SnowyHighMountain = 13, // New
|
||||
}
|
||||
|
||||
export enum PlayerType {
|
||||
|
||||
@@ -62,8 +62,7 @@ export class GameMapImpl implements GameMap {
|
||||
private static readonly IS_LAND_BIT = 7;
|
||||
private static readonly SHORELINE_BIT = 6;
|
||||
private static readonly OCEAN_BIT = 5;
|
||||
private static readonly MAGNITUDE_OFFSET = 4; // Uses bits 3-7 (5 bits)
|
||||
private static readonly MAGNITUDE_MASK = 0x1f; // 11111 in binary
|
||||
private static readonly TERRAIN_TYPE_MASK = 0x1f; // Use bits 0-4 for TerrainType enum or water magnitude
|
||||
|
||||
// State bits (Uint16Array)
|
||||
private static readonly PLAYER_ID_OFFSET = 0; // Uses bits 0-11 (12 bits)
|
||||
@@ -145,10 +144,34 @@ export class GameMapImpl implements GameMap {
|
||||
}
|
||||
|
||||
magnitude(ref: TileRef): number {
|
||||
return this.terrain[ref] & GameMapImpl.MAGNITUDE_MASK;
|
||||
if (this.isLand(ref)) {
|
||||
const type = this.terrainType(ref);
|
||||
switch (type) {
|
||||
case TerrainType.Plains:
|
||||
case TerrainType.Desert:
|
||||
case TerrainType.ArcticPlains:
|
||||
case TerrainType.Beach:
|
||||
case TerrainType.JunglePlains:
|
||||
return 0;
|
||||
case TerrainType.Forest:
|
||||
case TerrainType.DesertTransition:
|
||||
case TerrainType.ArcticForest:
|
||||
case TerrainType.Jungle:
|
||||
return 5;
|
||||
case TerrainType.MidMountain:
|
||||
return 20;
|
||||
case TerrainType.HighMountain:
|
||||
case TerrainType.SnowyHighMountain: // Same magnitude
|
||||
return 30;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
const storedMag = this.terrain[ref] & GameMapImpl.TERRAIN_TYPE_MASK;
|
||||
return storedMag * 2;
|
||||
}
|
||||
}
|
||||
|
||||
// State getters and setters (mutable)
|
||||
ownerID(ref: TileRef): number {
|
||||
return this.state[ref] & GameMapImpl.PLAYER_ID_MASK;
|
||||
}
|
||||
@@ -224,17 +247,55 @@ export class GameMapImpl implements GameMap {
|
||||
}
|
||||
|
||||
cost(ref: TileRef): number {
|
||||
return this.magnitude(ref) < 10 ? 2 : 1;
|
||||
const type = this.terrainType(ref);
|
||||
switch (type) {
|
||||
case TerrainType.Plains:
|
||||
return 2;
|
||||
case TerrainType.Forest:
|
||||
return 3;
|
||||
case TerrainType.Desert:
|
||||
return 3;
|
||||
case TerrainType.DesertTransition:
|
||||
return 2;
|
||||
case TerrainType.ArcticForest:
|
||||
return 4; // Renamed from Arctic
|
||||
case TerrainType.Beach:
|
||||
return 1; // Easy to cross
|
||||
case TerrainType.MidMountain:
|
||||
return 6;
|
||||
case TerrainType.HighMountain:
|
||||
return 8;
|
||||
case TerrainType.Jungle:
|
||||
return 5; // Difficult
|
||||
case TerrainType.JunglePlains:
|
||||
return 3; // Easier Jungle
|
||||
case TerrainType.ArcticPlains:
|
||||
return 3; // Like regular plains but colder?
|
||||
case TerrainType.SnowyHighMountain:
|
||||
return 8; // Same as HighMountain
|
||||
case TerrainType.Lake:
|
||||
return 1;
|
||||
case TerrainType.Ocean:
|
||||
return 1;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
terrainType(ref: TileRef): TerrainType {
|
||||
if (this.isLand(ref)) {
|
||||
const magnitude = this.magnitude(ref);
|
||||
if (magnitude < 10) return TerrainType.Plains;
|
||||
if (magnitude < 20) return TerrainType.Highland;
|
||||
return TerrainType.Mountain;
|
||||
const packedByte = this.terrain[ref];
|
||||
if (!this.isLand(ref)) {
|
||||
return this.isOcean(ref) ? TerrainType.Ocean : TerrainType.Lake;
|
||||
}
|
||||
return this.isOcean(ref) ? TerrainType.Ocean : TerrainType.Lake;
|
||||
const typeValue = packedByte & GameMapImpl.TERRAIN_TYPE_MASK;
|
||||
if (
|
||||
typeValue >= TerrainType.Plains &&
|
||||
typeValue <= TerrainType.SnowyHighMountain
|
||||
) {
|
||||
return typeValue as TerrainType;
|
||||
}
|
||||
console.warn(`Unexpected terrain type value ${typeValue} at ref ${ref}`);
|
||||
return TerrainType.Plains;
|
||||
}
|
||||
|
||||
neighbors(ref: TileRef): TileRef[] {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Bitmap, decodePNGFromStream } from "pureimage";
|
||||
//import fs from "fs/promises";
|
||||
//import { createReadStream } from "fs";
|
||||
import { Readable } from "stream";
|
||||
import { TerrainType } from "../core/game/Game";
|
||||
|
||||
const min_island_size = 30;
|
||||
const min_lake_size = 200;
|
||||
@@ -12,7 +13,7 @@ interface Coord {
|
||||
y: number;
|
||||
}
|
||||
|
||||
enum TerrainType {
|
||||
enum InternalTerrainType {
|
||||
Land,
|
||||
Water,
|
||||
}
|
||||
@@ -21,7 +22,41 @@ class Terrain {
|
||||
public shoreline: boolean = false;
|
||||
public magnitude: number = 0;
|
||||
public ocean: boolean = false;
|
||||
constructor(public type: TerrainType) {}
|
||||
constructor(
|
||||
public type: InternalTerrainType,
|
||||
public specificType: TerrainType,
|
||||
) {}
|
||||
}
|
||||
|
||||
const TARGET_COLORS: {
|
||||
[key in TerrainType]?: { r: number; g: number; b: number };
|
||||
} = {
|
||||
[TerrainType.Plains]: { r: 124, g: 178, b: 92 }, // #7cb25c - green (plains)
|
||||
[TerrainType.Forest]: { r: 97, g: 133, b: 62 }, // #61853e - dark green (forest)
|
||||
[TerrainType.Desert]: { r: 232, g: 219, b: 145 }, // #e8db91 - yellow (desert)
|
||||
[TerrainType.DesertTransition]: { r: 229, g: 186, b: 69 }, // #e5ba45 - dark yellow (transition)
|
||||
[TerrainType.ArcticForest]: { r: 152, g: 192, b: 224 }, // #98c0e0 - arctic blue (snowy arctic forest - OLD Arctic)
|
||||
[TerrainType.Beach]: { r: 224, g: 200, b: 121 }, // #e0c879 - beach
|
||||
[TerrainType.MidMountain]: { r: 92, g: 97, b: 94 }, // #5c615e - gray (middle mountain)
|
||||
[TerrainType.HighMountain]: { r: 67, g: 72, b: 74 }, // #43484a - dark gray (mountain peaks)
|
||||
[TerrainType.Jungle]: { r: 34, g: 117, b: 45 }, // #22752d - jungle
|
||||
[TerrainType.JunglePlains]: { r: 71, g: 163, b: 93 }, // #47a35d - jungle plains
|
||||
[TerrainType.ArcticPlains]: { r: 173, g: 207, b: 237 }, // #adcfed - arctic plains
|
||||
[TerrainType.SnowyHighMountain]: { r: 231, g: 245, b: 255 }, // #e7f5ff - snowy high mountains
|
||||
};
|
||||
|
||||
const OCEAN_COLOR = { r: 0, g: 0, b: 106 }; // #00006a
|
||||
|
||||
const COLOR_TOLERANCE = 15; // Allow slight variations in color matching
|
||||
|
||||
function colorDistanceSquared(
|
||||
c1: { r: number; g: number; b: number },
|
||||
c2: { r: number; g: number; b: number },
|
||||
): number {
|
||||
const dr = c1.r - c2.r;
|
||||
const dg = c1.g - c2.g;
|
||||
const db = c1.b - c2.b;
|
||||
return dr * dr + dg * dg + db * db;
|
||||
}
|
||||
|
||||
export async function generateMap(
|
||||
@@ -42,20 +77,73 @@ export async function generateMap(
|
||||
|
||||
for (let x = 0; x < img.width; x++) {
|
||||
for (let y = 0; y < img.height; y++) {
|
||||
const color = img.getPixelRGBA(x, y);
|
||||
const alpha = color & 0xff;
|
||||
const blue = (color >> 8) & 0xff;
|
||||
const colorValue = img.getPixelRGBA(x, y);
|
||||
const r = (colorValue >> 24) & 0xff;
|
||||
const g = (colorValue >> 16) & 0xff;
|
||||
const b = (colorValue >> 8) & 0xff;
|
||||
const a = colorValue & 0xff;
|
||||
const pixelColor = { r, g, b };
|
||||
|
||||
if (alpha < 20 || blue == 106) {
|
||||
// transparent
|
||||
terrain[x][y] = new Terrain(TerrainType.Water);
|
||||
if (
|
||||
a < 20 ||
|
||||
colorDistanceSquared(pixelColor, OCEAN_COLOR) <=
|
||||
COLOR_TOLERANCE * COLOR_TOLERANCE
|
||||
) {
|
||||
terrain[x][y] = new Terrain(
|
||||
InternalTerrainType.Water,
|
||||
TerrainType.Ocean,
|
||||
);
|
||||
} else {
|
||||
terrain[x][y] = new Terrain(TerrainType.Land);
|
||||
terrain[x][y].magnitude = 0;
|
||||
let matchedType: TerrainType | null = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
// 140 -> 200 = 60
|
||||
const mag = Math.min(200, Math.max(140, blue)) - 140;
|
||||
terrain[x][y].magnitude = mag / 2;
|
||||
for (const typeStr in TARGET_COLORS) {
|
||||
const type = parseInt(typeStr) as TerrainType;
|
||||
const targetColor = TARGET_COLORS[type];
|
||||
const distance = colorDistanceSquared(pixelColor, targetColor);
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
matchedType = type;
|
||||
}
|
||||
}
|
||||
|
||||
if (minDistance <= COLOR_TOLERANCE * COLOR_TOLERANCE) {
|
||||
terrain[x][y] = new Terrain(InternalTerrainType.Land, matchedType!);
|
||||
switch (matchedType) {
|
||||
case TerrainType.Plains:
|
||||
case TerrainType.Desert:
|
||||
case TerrainType.Beach:
|
||||
case TerrainType.ArcticPlains:
|
||||
case TerrainType.JunglePlains:
|
||||
terrain[x][y].magnitude = 0;
|
||||
break;
|
||||
case TerrainType.Forest:
|
||||
case TerrainType.DesertTransition:
|
||||
case TerrainType.ArcticForest:
|
||||
case TerrainType.Jungle:
|
||||
terrain[x][y].magnitude = 5;
|
||||
break;
|
||||
case TerrainType.MidMountain:
|
||||
terrain[x][y].magnitude = 20;
|
||||
break;
|
||||
case TerrainType.HighMountain:
|
||||
case TerrainType.SnowyHighMountain:
|
||||
terrain[x][y].magnitude = 30;
|
||||
break;
|
||||
default:
|
||||
terrain[x][y].magnitude = 0;
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`Color ${r},${g},${b} at ${x},${y} did not match any target, defaulting to Plains.`,
|
||||
);
|
||||
terrain[x][y] = new Terrain(
|
||||
InternalTerrainType.Land,
|
||||
TerrainType.Plains,
|
||||
);
|
||||
terrain[x][y].magnitude = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,23 +162,63 @@ export async function generateMap(
|
||||
}
|
||||
|
||||
async function createMiniMap(tm: Terrain[][]): Promise<Terrain[][]> {
|
||||
// Create 2D array properly with correct dimensions
|
||||
const miniMap: Terrain[][] = Array(Math.floor(tm.length / 2))
|
||||
const miniMapWidth = Math.floor(tm.length / 2);
|
||||
const miniMapHeight = Math.floor(tm[0].length / 2);
|
||||
const miniMap: Terrain[][] = Array(miniMapWidth)
|
||||
.fill(null)
|
||||
.map(() => Array(Math.floor(tm[0].length / 2)).fill(null));
|
||||
.map(() => Array(miniMapHeight).fill(null));
|
||||
|
||||
for (let x = 0; x < tm.length; x++) {
|
||||
for (let y = 0; y < tm[0].length; y++) {
|
||||
const miniX = Math.floor(x / 2);
|
||||
const miniY = Math.floor(y / 2);
|
||||
for (let miniX = 0; miniX < miniMapWidth; miniX++) {
|
||||
for (let miniY = 0; miniY < miniMapHeight; miniY++) {
|
||||
const startX = miniX * 2;
|
||||
const startY = miniY * 2;
|
||||
let isWater = false;
|
||||
let isOcean = false;
|
||||
let isShoreline = false;
|
||||
let totalMagnitude = 0;
|
||||
let landCount = 0;
|
||||
let waterCount = 0;
|
||||
let lastLandType: TerrainType = TerrainType.Plains;
|
||||
|
||||
if (
|
||||
miniMap[miniX][miniY] == null ||
|
||||
miniMap[miniX][miniY].type != TerrainType.Water
|
||||
) {
|
||||
// We shrink 4 tiles into 1 tile. If any of the 4 large tiles
|
||||
// has water, then the mini tile is considered water.
|
||||
miniMap[miniX][miniY] = tm[x][y];
|
||||
for (let dx = 0; dx < 2; dx++) {
|
||||
for (let dy = 0; dy < 2; dy++) {
|
||||
const x = startX + dx;
|
||||
const y = startY + dy;
|
||||
if (x >= tm.length || y >= tm[0].length) continue;
|
||||
const tile = tm[x][y];
|
||||
if (tile.type === InternalTerrainType.Water) {
|
||||
isWater = true;
|
||||
waterCount++;
|
||||
if (tile.ocean) isOcean = true;
|
||||
if (tile.shoreline) isShoreline = true;
|
||||
totalMagnitude += tile.magnitude;
|
||||
} else {
|
||||
landCount++;
|
||||
totalMagnitude += tile.magnitude;
|
||||
lastLandType = tile.specificType;
|
||||
if (tile.shoreline) isShoreline = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isWater) {
|
||||
const avgMagnitude = waterCount > 0 ? totalMagnitude / waterCount : 0;
|
||||
const waterType = isOcean ? TerrainType.Ocean : TerrainType.Lake;
|
||||
miniMap[miniX][miniY] = new Terrain(
|
||||
InternalTerrainType.Water,
|
||||
waterType,
|
||||
);
|
||||
miniMap[miniX][miniY].ocean = isOcean;
|
||||
miniMap[miniX][miniY].magnitude = Math.round(avgMagnitude);
|
||||
miniMap[miniX][miniY].shoreline = isShoreline;
|
||||
} else {
|
||||
const avgMagnitude = landCount > 0 ? totalMagnitude / landCount : 0;
|
||||
miniMap[miniX][miniY] = new Terrain(
|
||||
InternalTerrainType.Land,
|
||||
lastLandType,
|
||||
);
|
||||
miniMap[miniX][miniY].magnitude = Math.round(avgMagnitude);
|
||||
miniMap[miniX][miniY].shoreline = isShoreline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,13 +231,14 @@ function processShore(map: Terrain[][]): Coord[] {
|
||||
for (let x = 0; x < map.length; x++) {
|
||||
for (let y = 0; y < map[0].length; y++) {
|
||||
const tile = map[x][y];
|
||||
tile.shoreline = false;
|
||||
const ns = neighbors(x, y, map);
|
||||
if (tile.type == TerrainType.Land) {
|
||||
if (ns.filter((t) => t.type == TerrainType.Water).length > 0) {
|
||||
if (tile.type == InternalTerrainType.Land) {
|
||||
if (ns.some((t) => t.type == InternalTerrainType.Water)) {
|
||||
tile.shoreline = true;
|
||||
}
|
||||
} else {
|
||||
if (ns.filter((t) => t.type == TerrainType.Land).length > 0) {
|
||||
if (ns.some((t) => t.type == InternalTerrainType.Land)) {
|
||||
tile.shoreline = true;
|
||||
shorelineWaters.push({ x, y });
|
||||
}
|
||||
@@ -158,7 +287,7 @@ function processDistToLand(shorelineWaters: Coord[], map: Terrain[][]) {
|
||||
nx < width &&
|
||||
ny < height &&
|
||||
!visited[nx][ny] &&
|
||||
map[nx][ny].type === TerrainType.Water
|
||||
map[nx][ny].type === InternalTerrainType.Water
|
||||
) {
|
||||
visited[nx][ny] = true;
|
||||
map[nx][ny].magnitude = dist + 1;
|
||||
@@ -182,10 +311,9 @@ function processWater(map: Terrain[][], removeSmall: boolean) {
|
||||
const visited = new Set<string>();
|
||||
const waterBodies: { coords: Coord[]; size: number }[] = [];
|
||||
|
||||
// Find all distinct water bodies
|
||||
for (let x = 0; x < map.length; x++) {
|
||||
for (let y = 0; y < map[0].length; y++) {
|
||||
if (map[x][y].type === TerrainType.Water) {
|
||||
if (map[x][y].type === InternalTerrainType.Water) {
|
||||
const key = `${x},${y}`;
|
||||
if (visited.has(key)) continue;
|
||||
|
||||
@@ -218,7 +346,8 @@ function processWater(map: Terrain[][], removeSmall: boolean) {
|
||||
if (waterBodies[w].size < min_lake_size) {
|
||||
smallLakes++;
|
||||
for (const coord of waterBodies[w].coords) {
|
||||
map[coord.x][coord.y].type = TerrainType.Land;
|
||||
map[coord.x][coord.y].type = InternalTerrainType.Land;
|
||||
// map[coord.x][coord.y].specificType = TerrainType.Plains; // Default fill
|
||||
map[coord.x][coord.y].magnitude = 0;
|
||||
}
|
||||
}
|
||||
@@ -256,7 +385,8 @@ function packTerrain(map: Terrain[][]): Uint8Array {
|
||||
throw new Error(`terrain null at ${x}:${y}`);
|
||||
}
|
||||
|
||||
if (tile.type === TerrainType.Land) {
|
||||
const terrainTypeValue = tile.specificType;
|
||||
if (tile.type === InternalTerrainType.Land) {
|
||||
packedByte |= 0b10000000;
|
||||
}
|
||||
if (tile.shoreline) {
|
||||
@@ -265,10 +395,11 @@ function packTerrain(map: Terrain[][]): Uint8Array {
|
||||
if (tile.ocean) {
|
||||
packedByte |= 0b00100000;
|
||||
}
|
||||
if (tile.type == TerrainType.Land) {
|
||||
packedByte |= Math.min(Math.ceil(tile.magnitude), 31);
|
||||
} else {
|
||||
packedByte |= Math.min(Math.ceil(tile.magnitude / 2), 31);
|
||||
|
||||
packedByte |= terrainTypeValue & 0x1f;
|
||||
|
||||
if (tile.type === InternalTerrainType.Water) {
|
||||
packedByte |= Math.min(Math.ceil(tile.magnitude / 2), 31) & 0x1f;
|
||||
}
|
||||
|
||||
packedData[4 + y * width + x] = packedByte;
|
||||
@@ -284,7 +415,7 @@ function getArea(
|
||||
map: Terrain[][],
|
||||
visited: Set<string>,
|
||||
): Coord[] {
|
||||
const targetType: TerrainType = map[x][y].type;
|
||||
const targetType: InternalTerrainType = map[x][y].type;
|
||||
const area: Coord[] = [];
|
||||
const queue: Coord[] = [{ x, y }];
|
||||
|
||||
@@ -315,7 +446,7 @@ function removeSmallIslands(map: Terrain[][], removeSmall: boolean) {
|
||||
// Find all distinct land bodies
|
||||
for (let x = 0; x < map.length; x++) {
|
||||
for (let y = 0; y < map[0].length; y++) {
|
||||
if (map[x][y].type === TerrainType.Land) {
|
||||
if (map[x][y].type === InternalTerrainType.Land) {
|
||||
const key = `${x},${y}`;
|
||||
if (visited.has(key)) continue;
|
||||
|
||||
@@ -334,7 +465,8 @@ function removeSmallIslands(map: Terrain[][], removeSmall: boolean) {
|
||||
if (landBodies[b].size < min_island_size) {
|
||||
smallIslands++;
|
||||
for (const coord of landBodies[b].coords) {
|
||||
map[coord.x][coord.y].type = TerrainType.Water;
|
||||
map[coord.x][coord.y].type = InternalTerrainType.Water;
|
||||
// map[coord.x][coord.y].specificType = TerrainType.Ocean;
|
||||
map[coord.x][coord.y].magnitude = 0;
|
||||
}
|
||||
}
|
||||
@@ -402,50 +534,36 @@ function getThumbnailColor(t: Terrain): {
|
||||
b: number;
|
||||
a: number;
|
||||
} {
|
||||
if (t.type === TerrainType.Water) {
|
||||
// Shoreline water
|
||||
if (t.shoreline) return { r: 100, g: 143, b: 255, a: 0 };
|
||||
// All other water: adjust based on magnitude
|
||||
const waterAdjRGB: number = 11 - Math.min(t.magnitude / 2, 10) - 10;
|
||||
return {
|
||||
r: Math.max(70 + waterAdjRGB, 0),
|
||||
g: Math.max(132 + waterAdjRGB, 0),
|
||||
b: Math.max(180 + waterAdjRGB, 0),
|
||||
a: 0,
|
||||
};
|
||||
}
|
||||
//shoreline land
|
||||
if (t.shoreline) {
|
||||
return { r: 204, g: 203, b: 158, a: 255 };
|
||||
}
|
||||
let adjRGB: number;
|
||||
switch (true) {
|
||||
//plains
|
||||
case t.magnitude < 10:
|
||||
adjRGB = 220 - 2 * t.magnitude;
|
||||
return {
|
||||
r: 190,
|
||||
g: adjRGB,
|
||||
b: 138,
|
||||
a: 255,
|
||||
};
|
||||
//highlands
|
||||
case t.magnitude < 20:
|
||||
adjRGB = 2 * t.magnitude;
|
||||
return {
|
||||
r: 200 + adjRGB,
|
||||
g: 183 + adjRGB,
|
||||
b: 138 + adjRGB,
|
||||
a: 255,
|
||||
};
|
||||
//mountains
|
||||
case t.magnitude >= 20:
|
||||
adjRGB = Math.floor(230 + t.magnitude / 2);
|
||||
return {
|
||||
r: adjRGB,
|
||||
g: adjRGB,
|
||||
b: adjRGB,
|
||||
a: 255,
|
||||
};
|
||||
switch (t.specificType) {
|
||||
case TerrainType.Ocean:
|
||||
return { r: 0, g: 0, b: 106, a: 0 };
|
||||
case TerrainType.Lake:
|
||||
return { r: 70, g: 132, b: 180, a: 0 };
|
||||
case TerrainType.Plains:
|
||||
return { r: 124, g: 178, b: 92, a: 255 };
|
||||
case TerrainType.Forest:
|
||||
return { r: 97, g: 133, b: 62, a: 255 };
|
||||
case TerrainType.Desert:
|
||||
return { r: 232, g: 219, b: 145, a: 255 };
|
||||
case TerrainType.DesertTransition:
|
||||
return { r: 229, g: 186, b: 69, a: 255 };
|
||||
case TerrainType.ArcticForest:
|
||||
return { r: 152, g: 192, b: 224, a: 255 };
|
||||
case TerrainType.Beach:
|
||||
return { r: 224, g: 200, b: 121, a: 255 };
|
||||
case TerrainType.MidMountain:
|
||||
return { r: 92, g: 97, b: 94, a: 255 };
|
||||
case TerrainType.HighMountain:
|
||||
return { r: 67, g: 72, b: 74, a: 255 };
|
||||
case TerrainType.Jungle:
|
||||
return { r: 34, g: 117, b: 45, a: 255 };
|
||||
case TerrainType.JunglePlains:
|
||||
return { r: 71, g: 163, b: 93, a: 255 };
|
||||
case TerrainType.ArcticPlains:
|
||||
return { r: 173, g: 207, b: 237, a: 255 };
|
||||
case TerrainType.SnowyHighMountain:
|
||||
return { r: 231, g: 245, b: 255, a: 255 };
|
||||
default:
|
||||
return { r: 128, g: 128, b: 128, a: 255 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,27 +4,14 @@ import sharp from "sharp";
|
||||
import { generateMap } from "./TerrainMapGenerator.js";
|
||||
|
||||
const maps = [
|
||||
"Africa",
|
||||
"Asia",
|
||||
"WorldMap",
|
||||
"BlackSea",
|
||||
"Europe",
|
||||
"EuropeClassic",
|
||||
"Mars",
|
||||
"Mena",
|
||||
"Oceania",
|
||||
"NorthAmerica",
|
||||
"SouthAmerica",
|
||||
"Britannia",
|
||||
"GatewayToTheAtlantic",
|
||||
"Australia",
|
||||
"Pangaea",
|
||||
"Iceland",
|
||||
"BetweenTwoSeas",
|
||||
"Japan",
|
||||
"KnownWorld",
|
||||
"FaroeIslands",
|
||||
"DeglaciatedAntarctica",
|
||||
];
|
||||
|
||||
const removeSmall = true;
|
||||
|
||||