feat: add impassable terrain for non-rectangular maps

This commit is contained in:
FloPinguin
2026-06-18 19:56:30 +02:00
parent ca5342d6bf
commit dbd4beef11
20 changed files with 906 additions and 38 deletions
+10
View File
@@ -86,6 +86,16 @@ If you are doing work in image editing software or using automated tools, `./map
- `Pixel` -> `Terrain Type & Magnitude` mapping in `GenerateMap`
- `Terrain Type` -> `Thumbnail Color` mapping in `getThumbnailColor`
### Impassable Terrain
Pure black pixels (`#000000` / `rgb(0, 0, 0)` with alpha ≥ 20) are encoded as **impassable terrain**. This is a solid, static void that:
- Cannot be owned, attacked, or nuked.
- Nuke trajectories cannot pass over it (just as they cannot leave the map border).
- Renders as the map background colour, making the map appear non-rectangular.
Use impassable terrain to carve out non-rectangular map shapes or to create barriers that divide regions without water.
In-Game, terrain is rendered using themes. The color of a tile is determined dynamically based on
its **Terrain Type** and **Magnitude**. Theme Files:
+61 -19
View File
@@ -42,6 +42,7 @@ type TerrainType uint8
const (
Land TerrainType = iota
Water
Impassable
)
// Terrain represents the properties of a single map tile.
@@ -90,15 +91,20 @@ type GeneratorArgs struct {
// For Water tiles, "Magnitude" is calculated during generation as the distance to the nearest land.
//
// Pixel -> Terrain & Magnitude mapping
// | Input Condition | Terrain Type | Magnitude | Notes |
// | :----------------- | :-------------- | :----------------- | :------------------------------- |
// | **Alpha < 20** | Water | Distance to Land\* | Transparent pixels become water. |
// | **Blue = 106** | Water | Distance to Land\* | Specific key color for water. |
// | **Blue < 140** | Land (Plains) | 0 | Clamped to minimum magnitude. |
// | **Blue 140 - 158** | Land (Plains) | 0 - 9 | |
// | **Blue 159 - 178** | Land (Highland) | 10 - 19 | |
// | **Blue 179 - 200** | Land (Mountain) | 20 - 30 | |
// | **Blue > 200** | Land (Mountain) | 30 | Clamped to maximum magnitude. |
// | Input Condition | Terrain Type | Magnitude | Notes |
// | :----------------- | :--------------- | :----------------- | :------------------------------- |
// | **Alpha < 20** | Water | Distance to Land\* | Transparent pixels become water. |
// | **Blue = 106** | Water | Distance to Land\* | Specific key color for water. |
// | **#000 (black)** | Impassable | 31 (fixed) | Solid void; cannot be owned/attacked/nuked. |
// | **Blue < 140** | Land (Plains) | 0 | Clamped to minimum magnitude. |
// | **Blue 140 - 158** | Land (Plains) | 0 - 9 | |
// | **Blue 159 - 178** | Land (Highland) | 10 - 19 | |
// | **Blue 179 - 200** | Land (Mountain) | 20 - 30 | |
// | **Blue > 200** | Land (Mountain) | 30 | Clamped to maximum magnitude. |
//
// Impassable terrain is encoded in the binary format as isLand=1 + magnitude=31.
// It renders as the map background colour (making the map appear non-rectangular)
// and cannot be owned, attacked, or nuked. Nuke trajectories cannot cross it.
//
// Misc Notes
// - It normalizes map width/height to multiples of 4 for the mini map downscaling.
@@ -132,14 +138,19 @@ func GenerateMap(ctx context.Context, args GeneratorArgs) (MapResult, error) {
// Process each pixel
for x := 0; x < width; x++ {
for y := 0; y < height; y++ {
_, _, b, a := img.At(x, y).RGBA()
r, g, b, a := img.At(x, y).RGBA()
// Convert from 16-bit to 8-bit values
alpha := uint8(a >> 8)
red := uint8(r >> 8)
green := uint8(g >> 8)
blue := uint8(b >> 8)
alpha := uint8(a >> 8)
if alpha < 20 || blue == 106 {
// Transparent or specific blue value = water
terrain[x][y] = Terrain{Type: Water}
} else if red == 0 && green == 0 && blue == 0 {
// Pure black (#000) = impassable terrain
terrain[x][y] = Terrain{Type: Impassable}
} else {
// Land
terrain[x][y] = Terrain{Type: Land}
@@ -239,8 +250,9 @@ func convertToWebP(thumb ThumbData) ([]byte, error) {
// createMiniMap downscales the terrain grid by half.
// It maps 2x2 blocks of input tiles to a single output tile.
// The logic prioritizes Water: if any of the 4 source tiles is Water,
// the resulting mini-map tile becomes Water.
// Priority: Impassable > Water > Land. If any of the 4 source tiles is
// Impassable, the result is Impassable; else if any is Water, the result is
// Water; otherwise the last Land tile wins.
func createMiniMap(tm [][]Terrain) [][]Terrain {
width := len(tm)
height := len(tm[0])
@@ -258,12 +270,24 @@ func createMiniMap(tm [][]Terrain) [][]Terrain {
miniX := x / 2
miniY := y / 2
if miniX < miniWidth && miniY < miniHeight {
// If any of the 4 tiles has water, mini tile is water
if miniMap[miniX][miniY].Type != Water {
miniMap[miniX][miniY] = tm[x][y]
}
if miniX >= miniWidth || miniY >= miniHeight {
continue
}
src := tm[x][y]
dst := &miniMap[miniX][miniY]
// Impassable wins over everything; once set, keep it.
if dst.Type == Impassable {
continue
}
if src.Type == Impassable {
*dst = src
continue
}
// Water wins over land; once set to water, keep it.
if dst.Type == Water {
continue
}
*dst = src
}
}
@@ -296,7 +320,7 @@ func processShore(ctx context.Context, terrain [][]Terrain) []Coord {
break
}
}
} else {
} else if tile.Type == Water {
// Water tile adjacent to land is shoreline
for _, c := range buf[:n] {
if terrain[c.X][c.Y].Type == Land {
@@ -306,6 +330,7 @@ func processShore(ctx context.Context, terrain [][]Terrain) []Coord {
}
}
}
// Impassable tiles: never shoreline (renders as background, no outline)
}
}
@@ -567,6 +592,9 @@ func removeSmallIslands(ctx context.Context, terrain [][]Terrain, minSize int, r
// - Bit 5: Ocean
// - Bits 0-4: Magnitude (0-31). For Water, this is (Distance / 2).
//
// Impassable tiles are encoded as 0b10011111 (isLand=1, magnitude=31) and are
// NOT counted in numLandTiles (they cannot be owned/attacked/nuked).
//
// Returns the packed data and the count of land tiles.
func packTerrain(ctx context.Context, terrain [][]Terrain) (data []byte, numLandTiles int) {
width := len(terrain)
@@ -577,6 +605,14 @@ func packTerrain(ctx context.Context, terrain [][]Terrain) (data []byte, numLand
for x := 0; x < width; x++ {
for y := 0; y < height; y++ {
tile := terrain[x][y]
if tile.Type == Impassable {
// Impassable: isLand=1, magnitude=31, no shoreline, no ocean.
// Not counted as a land tile (can't be owned/attacked/nuked).
packedData[y*width+x] = 0b10011111
continue
}
var packedByte byte = 0
if tile.Type == Land {
@@ -652,6 +688,9 @@ type RGBA struct {
// color schemes.
//
// For thumbnail purposes, the terrain type -> color mapping:
// - Impassable: (Transparent) — renders as the map background in-game, so
// the thumbnail matches by being transparent (the map picker background
// shows through).
// - Water Shoreline: (Transparent)
// - Deep Water: (Transparent)
// - Land Shoreline: `rgb(204, 203, 158)`
@@ -659,6 +698,9 @@ type RGBA struct {
// - Highlands (Mag 10-19): `rgb(220, 203, 158)` - `rgb(238, 221, 176)`
// - Mountains (Mag >= 20): `rgb(240, 240, 240)` - `rgb(245, 245, 245)`
func getThumbnailColor(t Terrain) RGBA {
if t.Type == Impassable {
return RGBA{R: 0, G: 0, B: 0, A: 0}
}
if t.Type == Water {
// Shoreline water
if t.Shoreline {