mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 08:00:43 +00:00
Map Generator Go Code Documentation (#2656)
Resolves #2602 ## Description: tldr: `npm run docs:map-generator` Adds documentation to the `map-generator` go code. This has no functional changes, other than the renaming of the package. I used the github url, though this can be set to anything as long as it contains a `.` so that the docs parse it correctly. Go doc best practices seem a little verbose and terse, but attempted to comply Future Facing (to get these docs viewable without running locally): - Wait until the -http issue is sorted, then these are easy to statically host alongside builds - Could use the legacy `godoc` - Could do formatting after outputting the `txt` output ## Change List: - Add documentation to all types/fns in map-generator go code - Ensure this outputs correctly with `go doc` - Add `docs:map-generator` command to package.json. This runs `go doc` in `map-generator` w/ appropriate flags to generate full documentation. (see notes in readme) - rename `map-generator` module to work around pkgsite assuming all packages without a . are stdlib (this makes `-http` work at all) - Add new sections to README and update existing sections - Add additional references to locations in the primary code base where things can be found - Update documentation in the ts theme files to add output color mappings - this ensures that everything needed to trace the input file -> in game rendered asset is fully documented. ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: tidwell
This commit is contained in:
+68
-14
@@ -1,19 +1,25 @@
|
|||||||
# MapGenerator
|
# MapGenerator
|
||||||
|
|
||||||
This is a tool to generate map files for OpenFront.
|
This is a go-based tool to generate map files for OpenFront.
|
||||||
|
|
||||||
|
The map generator reads PNG files and converts pixels into terrain based primarily on the **Blue** channel.
|
||||||
|
Because only blue values are used, grayscale and other formats are fully supported. Many maps in `assets/maps/<mapname>` are grayscale.
|
||||||
|
|
||||||
|
Additional Guides, Tutorials, Scripts, Resources, and Third Party Unofficial Applications can be found on
|
||||||
|
the [Official Openfront Wiki](https://openfront.wiki/Map_Making)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Install go https://go.dev/doc/install
|
1. Install go <https://go.dev/doc/install>
|
||||||
2. Install dependencies: `go mod download`
|
2. Install dependencies: `go mod download`
|
||||||
3. Run the generator: `go run .`
|
3. Run the generator: `go run .`
|
||||||
|
|
||||||
## Creating a new map
|
## Creating a new map
|
||||||
|
|
||||||
1. Create a new folder in `assets/maps/<map_name>`
|
1. Create a new folder in `assets/maps/<map_name>`
|
||||||
2. Create image.png
|
2. Create `assets/maps/<map_name>/image.png`
|
||||||
3. Create info.json with name and countries
|
3. Create `assets/maps/<map_name>/info.json` with name and countries
|
||||||
4. Add the map name in main.go
|
4. Add the map name in `main.go` The `<name>` in `{Name: "<name>"},` should match the `<map-name>` folder at `assets/maps/<map_name>`
|
||||||
5. Run the generator: `go run .`
|
5. Run the generator: `go run .`
|
||||||
6. Find the output folder at `../resources/maps/<map_name>`
|
6. Find the output folder at `../resources/maps/<map_name>`
|
||||||
|
|
||||||
@@ -29,16 +35,27 @@ To process a subset of maps, pass a comma-separated list:
|
|||||||
|
|
||||||
## Create image.png
|
## Create image.png
|
||||||
|
|
||||||
|
The map-generator will process your input file at `assets/maps/<map_name>/image.png` to generate the map
|
||||||
|
thumbnail and binary files. To create this `png` input file, you can crop the world map:
|
||||||
|
|
||||||
1. [Download world map (warning very large file)](https://drive.google.com/file/d/1W2oMPj1L5zWRyPhh8LfmnY3_kve-FBR2/view?usp=sharing)
|
1. [Download world map (warning very large file)](https://drive.google.com/file/d/1W2oMPj1L5zWRyPhh8LfmnY3_kve-FBR2/view?usp=sharing)
|
||||||
2. Crop the file (recommend Gimp)
|
2. Crop the file (recommend Gimp)
|
||||||
|
|
||||||
- We recommend roughly 2 million pixels for performance reasons
|
If you are doing work in image editing software or using automated tools, `./map_generator.go` contains documentation for:
|
||||||
- Do not go over 4 million pixels.
|
|
||||||
|
- `Pixel` -> `Terrain Type & Magnitude` mapping in `GenerateMap`
|
||||||
|
- `Terrain Type` -> `Thumbnail Color` mapping in `getThumbnailColor`
|
||||||
|
|
||||||
|
In-Game, terrain is rendered using themes. The color of a tile is determined dynamically based on
|
||||||
|
its **Terrain Type** and **Magnitude**. Theme Files:
|
||||||
|
|
||||||
|
- `../src/core/configuration/PastelTheme.ts` (Light)
|
||||||
|
- `../src/core/configuration/PastelThemeDark.ts` (Dark).
|
||||||
|
|
||||||
## Create info.json
|
## Create info.json
|
||||||
|
|
||||||
- Look at existing info.json for structure
|
The map-generator will process your input file at `assets/maps/<map_name>/info.json` to determine the
|
||||||
- [Use country codes found here](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes)
|
position of Nations, their starting coordinates, and any flags.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -55,18 +72,43 @@ Example:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`coordinates` is x/y position of the nation spawn on the map. Origin is at top left, with x extending right and y extending down
|
||||||
|
|
||||||
|
`name` is a `CamelCaseName` of your map. It is used to enable the map in-game.
|
||||||
|
|
||||||
|
`flag` is the code for a country
|
||||||
|
|
||||||
|
- The full list of supported codes can be seen in `../src/client/data/countries.json` - all ISO_3166 codes are supported, with several additions.
|
||||||
|
|
||||||
|
- For quick reference, [Use country codes found here](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes)
|
||||||
|
|
||||||
|
## Update CREDITS.md
|
||||||
|
|
||||||
|
Add License & Attribution information to `../CREDITS.md`. If you are unsure if
|
||||||
|
a map's license can be used, open an issue or ask in Discord before beginning work.
|
||||||
|
|
||||||
|
## Adding Flags
|
||||||
|
|
||||||
|
Flags can be added to `../resources/flags/<iso_code>.svg`
|
||||||
|
|
||||||
|
The country will need to be added to `../src/client/data/countries.json`
|
||||||
|
|
||||||
## To Enable In-Game
|
## To Enable In-Game
|
||||||
|
|
||||||
- Add a translation for the map name to `resources/lang/en.json`
|
Using the `name` from your json:
|
||||||
- Add the MapDescription `src/client/components/Maps.ts`
|
|
||||||
- Add the numPlayersConfig `src/core/configuration/DefaultConfig.ts`
|
- Add to the MapDescription `../src/client/components/Maps.ts`
|
||||||
- Add the GameMapType `src/core/game/Game.ts`
|
- Add to the numPlayersConfig `../src/core/configuration/DefaultConfig.ts`
|
||||||
- To add to the map playlist, modify `src/server/MapPlaylist.ts`
|
- Add to the mapCategories `../src/core/game/Game.ts`
|
||||||
|
- Add to the map playlist `../src/server/MapPlaylist.ts`
|
||||||
|
- Add to the `map` translation object in `../resources/lang/en.json`
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
- Maps should be between 2 - 3 million pixels square (area)
|
||||||
- Islands smaller than 30 tiles (pixels) are automatically removed by the script.
|
- Islands smaller than 30 tiles (pixels) are automatically removed by the script.
|
||||||
- Bodies of water smaller than 200 tiles (pixels) are also removed.
|
- Bodies of water smaller than 200 tiles (pixels) are also removed.
|
||||||
|
- The map generator normalizes dimensions to multiples of 4. Any pixels beyond `Width - (Width % 4)` or `Height - (Height % 4)` are cropped.
|
||||||
|
|
||||||
## 🛠️ Development Tools
|
## 🛠️ Development Tools
|
||||||
|
|
||||||
@@ -75,3 +117,15 @@ Example:
|
|||||||
```bash
|
```bash
|
||||||
go fmt .
|
go fmt .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **Output Map Generator Documentation**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go doc -cmd -u -all
|
||||||
|
```
|
||||||
|
|
||||||
|
The map-generator is a cli tool, to get any visibility, we pass `-cmd`. It also
|
||||||
|
does not expose any API, so we use `-u` and `-all` to show all documentation for
|
||||||
|
unexposed values.
|
||||||
|
|
||||||
|
_Known Bug_ Using `-http` does not respect the other flags and only renders the README
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
module map-generator
|
module github.com/openfrontio/OpenFrontIO/map-generator
|
||||||
|
|
||||||
go 1.24.4
|
go 1.24.4
|
||||||
|
|
||||||
require github.com/chai2010/webp v1.4.0
|
require github.com/chai2010/webp v1.4.0
|
||||||
|
|||||||
@@ -11,8 +11,13 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// mapsFlag holds the comma-separated list of map names passed via the --maps command-line argument.
|
||||||
var mapsFlag string
|
var mapsFlag string
|
||||||
|
|
||||||
|
// maps defines the registry of available maps to be processed.
|
||||||
|
// Each entry contains the folder name and a flag indicating if it's a test map.
|
||||||
|
//
|
||||||
|
// New maps need to be added here in order to allow the map-generator to process them.
|
||||||
var maps = []struct {
|
var maps = []struct {
|
||||||
Name string
|
Name string
|
||||||
IsTest bool
|
IsTest bool
|
||||||
@@ -61,6 +66,8 @@ var maps = []struct {
|
|||||||
{Name: "giantworldmap", IsTest: true},
|
{Name: "giantworldmap", IsTest: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// outputMapDir returns the absolute path to the directory where generated map files should be written.
|
||||||
|
// It distinguishes between test and production output locations.
|
||||||
func outputMapDir(isTest bool) (string, error) {
|
func outputMapDir(isTest bool) (string, error) {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -72,6 +79,8 @@ func outputMapDir(isTest bool) (string, error) {
|
|||||||
return filepath.Join(cwd, "..", "resources", "maps"), nil
|
return filepath.Join(cwd, "..", "resources", "maps"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inputMapDir returns the absolute path to the directory containing source map assets.
|
||||||
|
// It distinguishes between test and production asset locations.
|
||||||
func inputMapDir(isTest bool) (string, error) {
|
func inputMapDir(isTest bool) (string, error) {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -84,6 +93,8 @@ func inputMapDir(isTest bool) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processMap handles the end-to-end generation for a single map.
|
||||||
|
// It reads the source image and JSON, generates the terrain data, and writes the binary outputs and updated manifest.
|
||||||
func processMap(name string, isTest bool) error {
|
func processMap(name string, isTest bool) error {
|
||||||
outputMapBaseDir, err := outputMapDir(isTest)
|
outputMapBaseDir, err := outputMapDir(isTest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -169,6 +180,8 @@ func processMap(name string, isTest bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseMapsFlag validates and parses the --maps command-line argument.
|
||||||
|
// It returns a set of selected map names or nil if no flag was provided (implying all maps).
|
||||||
func parseMapsFlag() (map[string]bool, error) {
|
func parseMapsFlag() (map[string]bool, error) {
|
||||||
if mapsFlag == "" {
|
if mapsFlag == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -189,6 +202,8 @@ func parseMapsFlag() (map[string]bool, error) {
|
|||||||
return selected, nil
|
return selected, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadTerrainMaps manages the concurrent generation of all selected maps.
|
||||||
|
// It spins up goroutines for each map and aggregates any errors.
|
||||||
func loadTerrainMaps() error {
|
func loadTerrainMaps() error {
|
||||||
selectedMaps, err := parseMapsFlag()
|
selectedMaps, err := parseMapsFlag()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -226,6 +241,8 @@ func loadTerrainMaps() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// main is the entry point for the map generator tool.
|
||||||
|
// It parses flags and triggers the map generation process.
|
||||||
func main() {
|
func main() {
|
||||||
flag.StringVar(&mapsFlag, "maps", "", "optional comma-separated list of maps to process. ex: --maps=world,eastasia,big_plains")
|
flag.StringVar(&mapsFlag, "maps", "", "optional comma-separated list of maps to process. ex: --maps=world,eastasia,big_plains")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|||||||
+111
-10
@@ -12,28 +12,35 @@ import (
|
|||||||
"github.com/chai2010/webp"
|
"github.com/chai2010/webp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The smallest a body of land or lake can be, all smaller are removed
|
||||||
const (
|
const (
|
||||||
minIslandSize = 30
|
minIslandSize = 30
|
||||||
minLakeSize = 200
|
minLakeSize = 200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Holds raw RGBA image data for the thumbnail
|
||||||
type ThumbData struct {
|
type ThumbData struct {
|
||||||
Data []byte
|
Data []byte
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XY coord, origin top left, x extending right, y extends down
|
||||||
type Coord struct {
|
type Coord struct {
|
||||||
X, Y int
|
X, Y int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TerrainType represents the classification of a map tile (e.g., Land or Water).
|
||||||
type TerrainType int
|
type TerrainType int
|
||||||
|
|
||||||
|
// Enumeration of possible TerrainType values.
|
||||||
const (
|
const (
|
||||||
Land TerrainType = iota
|
Land TerrainType = iota
|
||||||
Water
|
Water
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Terrain represents the properties of a single map tile.
|
||||||
|
// Magnitude represents elevation for Land (0-30) or distance to land for Water.
|
||||||
type Terrain struct {
|
type Terrain struct {
|
||||||
Type TerrainType
|
Type TerrainType
|
||||||
Shoreline bool
|
Shoreline bool
|
||||||
@@ -41,6 +48,7 @@ type Terrain struct {
|
|||||||
Ocean bool
|
Ocean bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MapResult is the output format from the GenerateMap workflow
|
||||||
type MapResult struct {
|
type MapResult struct {
|
||||||
Thumbnail []byte
|
Thumbnail []byte
|
||||||
Map MapInfo
|
Map MapInfo
|
||||||
@@ -48,19 +56,44 @@ type MapResult struct {
|
|||||||
Map16x MapInfo
|
Map16x MapInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MapInfo contains the serialized map data and metadata for a specific scale.
|
||||||
type MapInfo struct {
|
type MapInfo struct {
|
||||||
Data []byte
|
Data []byte // packed map data
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
NumLandTiles int
|
NumLandTiles int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GeneratorArgs defines the input parameters for the map generation process.
|
||||||
type GeneratorArgs struct {
|
type GeneratorArgs struct {
|
||||||
Name string
|
Name string
|
||||||
ImageBuffer []byte
|
ImageBuffer []byte
|
||||||
RemoveSmall bool
|
RemoveSmall bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateMap is the main map-generator workflow.
|
||||||
|
// - Maps each pixel to a Terrain type based on its blue value
|
||||||
|
// - Removes small islands and lakes
|
||||||
|
// - Creates a WebP thumbnail
|
||||||
|
// - Packs the map data into binary format for full scale, 1/4 tile count (half dimensions), and 1/16 tile count (quarter dimensions)
|
||||||
|
//
|
||||||
|
// Red/green pixel values have no impact, only blue values are used
|
||||||
|
// For Land tiles, "Magnitude" is determined by `(Blue - 140) / 2“.
|
||||||
|
// 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. |
|
||||||
|
//
|
||||||
|
// Misc Notes
|
||||||
|
// - It normalizes map width/height to multiples of 4 for the mini map downscaling.
|
||||||
func GenerateMap(args GeneratorArgs) (MapResult, error) {
|
func GenerateMap(args GeneratorArgs) (MapResult, error) {
|
||||||
img, err := png.Decode(bytes.NewReader(args.ImageBuffer))
|
img, err := png.Decode(bytes.NewReader(args.ImageBuffer))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -150,6 +183,7 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convertToWebP encodes raw RGBA thumbnail data into WebP format.
|
||||||
func convertToWebP(thumb ThumbData) ([]byte, error) {
|
func convertToWebP(thumb ThumbData) ([]byte, error) {
|
||||||
// Create RGBA image from raw data
|
// Create RGBA image from raw data
|
||||||
img := image.NewRGBA(image.Rect(0, 0, thumb.Width, thumb.Height))
|
img := image.NewRGBA(image.Rect(0, 0, thumb.Width, thumb.Height))
|
||||||
@@ -171,6 +205,10 @@ func convertToWebP(thumb ThumbData) ([]byte, error) {
|
|||||||
return webpData, nil
|
return webpData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
func createMiniMap(tm [][]Terrain) [][]Terrain {
|
func createMiniMap(tm [][]Terrain) [][]Terrain {
|
||||||
width := len(tm)
|
width := len(tm)
|
||||||
height := len(tm[0])
|
height := len(tm[0])
|
||||||
@@ -200,6 +238,10 @@ func createMiniMap(tm [][]Terrain) [][]Terrain {
|
|||||||
return miniMap
|
return miniMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processShore identifies shoreline tiles by checking adjacency.
|
||||||
|
// It marks Land tiles as shoreline if they neighbor Water, and Water tiles as
|
||||||
|
// shoreline if they neighbor Land.
|
||||||
|
// Returns a list of coordinates for all shoreline Water tiles found.
|
||||||
func processShore(terrain [][]Terrain) []Coord {
|
func processShore(terrain [][]Terrain) []Coord {
|
||||||
log.Println("Identifying shorelines")
|
log.Println("Identifying shorelines")
|
||||||
var shorelineWaters []Coord
|
var shorelineWaters []Coord
|
||||||
@@ -235,6 +277,9 @@ func processShore(terrain [][]Terrain) []Coord {
|
|||||||
return shorelineWaters
|
return shorelineWaters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processDistToLand calculates the distance of water tiles from the nearest land.
|
||||||
|
// It uses a Breadth-First Search (BFS) starting from the shoreline water tiles.
|
||||||
|
// The distance is stored in the Magnitude field of the Water tiles.
|
||||||
func processDistToLand(shorelineWaters []Coord, terrain [][]Terrain) {
|
func processDistToLand(shorelineWaters []Coord, terrain [][]Terrain) {
|
||||||
log.Println("Setting Water tiles magnitude = Manhattan distance from nearest land")
|
log.Println("Setting Water tiles magnitude = Manhattan distance from nearest land")
|
||||||
|
|
||||||
@@ -280,6 +325,7 @@ func processDistToLand(shorelineWaters []Coord, terrain [][]Terrain) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getNeighbors returns a list of Terrain tiles adjacent to the specified coordinates.
|
||||||
func getNeighbors(x, y int, terrain [][]Terrain) []Terrain {
|
func getNeighbors(x, y int, terrain [][]Terrain) []Terrain {
|
||||||
coords := getNeighborCoords(x, y, terrain)
|
coords := getNeighborCoords(x, y, terrain)
|
||||||
neighbors := make([]Terrain, len(coords))
|
neighbors := make([]Terrain, len(coords))
|
||||||
@@ -289,6 +335,8 @@ func getNeighbors(x, y int, terrain [][]Terrain) []Terrain {
|
|||||||
return neighbors
|
return neighbors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getNeighborCoords returns a list of valid adjacent coordinates (up, down, left, right).
|
||||||
|
// It ensures that the returned coordinates are within the bounds of the terrain grid.
|
||||||
func getNeighborCoords(x, y int, terrain [][]Terrain) []Coord {
|
func getNeighborCoords(x, y int, terrain [][]Terrain) []Coord {
|
||||||
width := len(terrain)
|
width := len(terrain)
|
||||||
height := len(terrain[0])
|
height := len(terrain[0])
|
||||||
@@ -310,6 +358,10 @@ func getNeighborCoords(x, y int, terrain [][]Terrain) []Coord {
|
|||||||
return coords
|
return coords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processWater identifies and processes bodies of water in the terrain.
|
||||||
|
// It finds all connected water bodies and marks the largest one as Ocean.
|
||||||
|
// If removeSmall is true, lakes smaller than minLakeSize are converted to Land.
|
||||||
|
// Finally, it triggers shoreline identification and distance-to-land calculations.
|
||||||
func processWater(terrain [][]Terrain, removeSmall bool) {
|
func processWater(terrain [][]Terrain, removeSmall bool) {
|
||||||
log.Println("Processing water bodies")
|
log.Println("Processing water bodies")
|
||||||
visited := make(map[string]bool)
|
visited := make(map[string]bool)
|
||||||
@@ -382,6 +434,9 @@ func processWater(terrain [][]Terrain, removeSmall bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getArea performs a Breadth-First Search (BFS) to find a contiguous area of tiles
|
||||||
|
// sharing the same TerrainType as the passed x,y coordinates.
|
||||||
|
// The visited map is updated to prevent reprocessing tiles.
|
||||||
func getArea(x, y int, terrain [][]Terrain, visited map[string]bool) []Coord {
|
func getArea(x, y int, terrain [][]Terrain, visited map[string]bool) []Coord {
|
||||||
targetType := terrain[x][y].Type
|
targetType := terrain[x][y].Type
|
||||||
var area []Coord
|
var area []Coord
|
||||||
@@ -408,6 +463,8 @@ func getArea(x, y int, terrain [][]Terrain, visited map[string]bool) []Coord {
|
|||||||
return area
|
return area
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// removeSmallIslands identifies and removes small land masses from the terrain.
|
||||||
|
// If removeSmall is true, any removed bodies are converted to Water.
|
||||||
func removeSmallIslands(terrain [][]Terrain, removeSmall bool) {
|
func removeSmallIslands(terrain [][]Terrain, removeSmall bool) {
|
||||||
if !removeSmall {
|
if !removeSmall {
|
||||||
return
|
return
|
||||||
@@ -456,6 +513,14 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool) {
|
|||||||
smallIslands, minIslandSize)
|
smallIslands, minIslandSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// packTerrain serializes the terrain grid into a byte slice.
|
||||||
|
// Each byte represents a single tile with bit flags:
|
||||||
|
// - Bit 7: Land (1) / Water (0)
|
||||||
|
// - Bit 6: Shoreline
|
||||||
|
// - Bit 5: Ocean
|
||||||
|
// - Bits 0-4: Magnitude (0-31). For Water, this is (Distance / 2).
|
||||||
|
//
|
||||||
|
// Returns the packed data and the count of land tiles.
|
||||||
func packTerrain(terrain [][]Terrain) (data []byte, numLandTiles int) {
|
func packTerrain(terrain [][]Terrain) (data []byte, numLandTiles int) {
|
||||||
width := len(terrain)
|
width := len(terrain)
|
||||||
height := len(terrain[0])
|
height := len(terrain[0])
|
||||||
@@ -492,6 +557,9 @@ func packTerrain(terrain [][]Terrain) (data []byte, numLandTiles int) {
|
|||||||
return packedData, numLandTiles
|
return packedData, numLandTiles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createMapThumbnail generates an RGBA image representation of the terrain.
|
||||||
|
// It scales the map dimensions based on the provided quality factor.
|
||||||
|
// Each pixel's color is determined by the terrain type and magnitude via getThumbnailColor.
|
||||||
func createMapThumbnail(terrain [][]Terrain, quality float64) *image.RGBA {
|
func createMapThumbnail(terrain [][]Terrain, quality float64) *image.RGBA {
|
||||||
log.Println("Creating thumbnail")
|
log.Println("Creating thumbnail")
|
||||||
|
|
||||||
@@ -520,10 +588,28 @@ func createMapThumbnail(terrain [][]Terrain, quality float64) *image.RGBA {
|
|||||||
return img
|
return img
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RGBA represents a color with Red, Green, Blue, and Alpha channels.
|
||||||
|
// It is used locally for thumbnail generation.
|
||||||
type RGBA struct {
|
type RGBA struct {
|
||||||
R, G, B, A uint8
|
R, G, B, A uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getThumbnailColor determines the RGBA color for a specific terrain tile for
|
||||||
|
// the map preview thumbnail.
|
||||||
|
//
|
||||||
|
// It handles color generation for Water (shoreline vs deep water) and Land
|
||||||
|
// (shoreline, plains, highlands, mountains) based on the tile's magnitude.
|
||||||
|
//
|
||||||
|
// The thumbnail renders its own set of colors separate from the in-game light/dark
|
||||||
|
// color schemes.
|
||||||
|
//
|
||||||
|
// For thumbnail purposes, the terrain type -> color mapping:
|
||||||
|
// - Water Shoreline: (Transparent)
|
||||||
|
// - Deep Water: (Transparent)
|
||||||
|
// - Land Shoreline: `rgb(204, 203, 158)`
|
||||||
|
// - Plains (Mag < 10): `rgb(190, 220, 138)` - `rgb(190, 202, 138)`
|
||||||
|
// - 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 {
|
func getThumbnailColor(t Terrain) RGBA {
|
||||||
if t.Type == Water {
|
if t.Type == Water {
|
||||||
// Shoreline water
|
// Shoreline water
|
||||||
@@ -576,6 +662,8 @@ func getThumbnailColor(t Terrain) RGBA {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logBinaryAsBits logs the binary representation of the first 'length' bytes of data.
|
||||||
|
// It is a helper function for debugging packed terrain data.
|
||||||
func logBinaryAsBits(data []byte, length int) {
|
func logBinaryAsBits(data []byte, length int) {
|
||||||
if length > len(data) {
|
if length > len(data) {
|
||||||
length = len(data)
|
length = len(data)
|
||||||
@@ -588,21 +676,23 @@ func logBinaryAsBits(data []byte, length int) {
|
|||||||
log.Printf("Binary data (bits): %s", bits)
|
log.Printf("Binary data (bits): %s", bits)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createCombinedBinary combines the info JSON, map data, and mini-map data into a single binary buffer.
|
||||||
|
//
|
||||||
|
// Note: This function is currently unused by the main workflow, which writes separate files instead.
|
||||||
|
// It creates a header with the following structure:
|
||||||
|
// - Bytes 0-3: Version (1)
|
||||||
|
// - Bytes 4-7: Info section offset
|
||||||
|
// - Bytes 8-11: Info section size
|
||||||
|
// - Bytes 12-15: Map section offset
|
||||||
|
// - Bytes 16-19: Map section size
|
||||||
|
// - Bytes 20-23: MiniMap section offset
|
||||||
|
// - Bytes 24-27: MiniMap section size
|
||||||
func createCombinedBinary(infoBuffer []byte, mapData []byte, miniMapData []byte) []byte {
|
func createCombinedBinary(infoBuffer []byte, mapData []byte, miniMapData []byte) []byte {
|
||||||
// Calculate section sizes
|
// Calculate section sizes
|
||||||
infoSize := len(infoBuffer)
|
infoSize := len(infoBuffer)
|
||||||
mapSize := len(mapData)
|
mapSize := len(mapData)
|
||||||
miniMapSize := len(miniMapData)
|
miniMapSize := len(miniMapData)
|
||||||
|
|
||||||
// Header structure:
|
|
||||||
// Bytes 0-3: Version (1)
|
|
||||||
// Bytes 4-7: Info section offset (always 28)
|
|
||||||
// Bytes 8-11: Info section size
|
|
||||||
// Bytes 12-15: Map section offset
|
|
||||||
// Bytes 16-19: Map section size
|
|
||||||
// Bytes 20-23: MiniMap section offset
|
|
||||||
// Bytes 24-27: MiniMap section size
|
|
||||||
|
|
||||||
headerSize := 28
|
headerSize := 28
|
||||||
infoOffset := headerSize
|
infoOffset := headerSize
|
||||||
mapOffset := infoOffset + infoSize
|
mapOffset := infoOffset + infoSize
|
||||||
@@ -634,6 +724,9 @@ func createCombinedBinary(infoBuffer []byte, mapData []byte, miniMapData []byte)
|
|||||||
return combined
|
return combined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeUint32 writes a 32-bit unsigned integer to the byte slice at the specified offset.
|
||||||
|
// It uses Little Endian byte order.
|
||||||
|
// Note: This function is currently unused.
|
||||||
func writeUint32(data []byte, offset int, value uint32) {
|
func writeUint32(data []byte, offset int, value uint32) {
|
||||||
data[offset] = byte(value & 0xff)
|
data[offset] = byte(value & 0xff)
|
||||||
data[offset+1] = byte((value >> 8) & 0xff)
|
data[offset+1] = byte((value >> 8) & 0xff)
|
||||||
@@ -641,10 +734,16 @@ func writeUint32(data []byte, offset int, value uint32) {
|
|||||||
data[offset+3] = byte((value >> 24) & 0xff)
|
data[offset+3] = byte((value >> 24) & 0xff)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readUint32 reads a 32-bit unsigned integer from the byte slice at the specified offset.
|
||||||
|
// It assumes Little Endian byte order.
|
||||||
|
// Note: This function is currently unused.
|
||||||
func readUint32(data []byte, offset int) uint32 {
|
func readUint32(data []byte, offset int) uint32 {
|
||||||
return uint32(data[offset]) | uint32(data[offset+1])<<8 | uint32(data[offset+2])<<16 | uint32(data[offset+3])<<24
|
return uint32(data[offset]) | uint32(data[offset+1])<<8 | uint32(data[offset+2])<<16 | uint32(data[offset+3])<<24
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decodeCombinedBinary parses a combined binary buffer into its constituent parts.
|
||||||
|
// It validates the header and extracts the Info JSON, Map data, and MiniMap data sections.
|
||||||
|
// Note: This function is currently unused.
|
||||||
func decodeCombinedBinary(data []byte) (*CombinedBinaryHeader, []byte, []byte, []byte, error) {
|
func decodeCombinedBinary(data []byte) (*CombinedBinaryHeader, []byte, []byte, []byte, error) {
|
||||||
if len(data) < 28 {
|
if len(data) < 28 {
|
||||||
return nil, nil, nil, nil, fmt.Errorf("data too short for header")
|
return nil, nil, nil, nil, fmt.Errorf("data too short for header")
|
||||||
@@ -675,6 +774,8 @@ func decodeCombinedBinary(data []byte) (*CombinedBinaryHeader, []byte, []byte, [
|
|||||||
return header, infoData, mapData, miniMapData, nil
|
return header, infoData, mapData, miniMapData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CombinedBinaryHeader represents the metadata header of the combined map file format.
|
||||||
|
// Note: This struct is currently unused.
|
||||||
type CombinedBinaryHeader struct {
|
type CombinedBinaryHeader struct {
|
||||||
Version uint32
|
Version uint32
|
||||||
InfoOffset uint32
|
InfoOffset uint32
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"dev": "cross-env GAME_ENV=dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
"dev": "cross-env GAME_ENV=dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
||||||
"dev:staging": "cross-env GAME_ENV=dev API_DOMAIN=api.openfront.dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
"dev:staging": "cross-env GAME_ENV=dev API_DOMAIN=api.openfront.dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
||||||
"dev:prod": "cross-env GAME_ENV=dev API_DOMAIN=api.openfront.io concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
"dev:prod": "cross-env GAME_ENV=dev API_DOMAIN=api.openfront.io concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
||||||
|
"docs:map-generator": "cd map-generator && go doc -cmd -u -all",
|
||||||
"tunnel": "npm run build-prod && npm run start:server",
|
"tunnel": "npm run build-prod && npm run start:server",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"perf": "npx tsx tests/perf/*.ts",
|
"perf": "npx tsx tests/perf/*.ts",
|
||||||
|
|||||||
@@ -138,6 +138,14 @@ export class PastelTheme implements Theme {
|
|||||||
return player.type() === PlayerType.Human ? "#000000" : "#4D4D4D";
|
return player.type() === PlayerType.Human ? "#000000" : "#4D4D4D";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// | Terrain Type | Magnitude | Base Color Logic | Visual Description |
|
||||||
|
// | :---------------- | :-------- | :---------------------------------------------- | :------------------------------------------------------------------- |
|
||||||
|
// | **Shore (Land)** | N/A | Fixed: `rgb(204, 203, 158)` | Sandy beige. Overrides other land types if adjacent to water. |
|
||||||
|
// | **Plains** | 0 - 9 | `rgb(190, 220, 138)` - `rgb(190, 202, 138)` | Light green. Gets slightly darker/less green as magnitude increases. |
|
||||||
|
// | **Highland** | 10 - 19 | `rgb(220, 203, 158)` - `rgb(238, 221, 176)` | Tan/Beige. Gets lighter as magnitude increases. |
|
||||||
|
// | **Mountain** | 20 - 30 | `rgb(240, 240, 240)` - `rgb(245, 245, 245)` | Grayscale (White/Grey). Represents snow caps or rocky peaks. |
|
||||||
|
// | **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 {
|
terrainColor(gm: GameMap, tile: TileRef): Colord {
|
||||||
const mag = gm.magnitude(tile);
|
const mag = gm.magnitude(tile);
|
||||||
if (gm.isShore(tile)) {
|
if (gm.isShore(tile)) {
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ export class PastelThemeDark extends PastelTheme {
|
|||||||
private darkWater = colord("rgb(14,11,30)");
|
private darkWater = colord("rgb(14,11,30)");
|
||||||
private darkShorelineWater = colord("rgb(50,50,50)");
|
private darkShorelineWater = colord("rgb(50,50,50)");
|
||||||
|
|
||||||
|
// | Terrain Type | Magnitude | Base Color Logic | Visual Description |
|
||||||
|
// | :---------------- | :-------- | :---------------------------------------------- | :-------------------- |
|
||||||
|
// | **Shore (Land)** | N/A | Fixed: `rgb(134, 133, 88)` | Dark olive. |
|
||||||
|
// | **Plains** | 0 - 9 | `rgb(140, 170, 88)` - `rgb(140, 152, 88)` | Muted green. |
|
||||||
|
// | **Highland** | 10 - 19 | `rgb(170, 153, 108)` - `rgb(188, 171, 126)` | Dark earth tone. |
|
||||||
|
// | **Mountain** | 20 - 30 | `rgb(190, 190, 190)` - `rgb(195, 195, 195)` | Dark gray. |
|
||||||
|
// | **Water (Shore)** | 0 | Fixed: `rgb(50, 50, 50)` | Dark gray/black. |
|
||||||
|
// | **Water (Deep)** | 1 - 10+ | `rgb(22, 19, 38)` - `rgb(14, 11, 30)` | Very dark blue/black. |
|
||||||
|
|
||||||
terrainColor(gm: GameMap, tile: TileRef): Colord {
|
terrainColor(gm: GameMap, tile: TileRef): Colord {
|
||||||
const mag = gm.magnitude(tile);
|
const mag = gm.magnitude(tile);
|
||||||
if (gm.isShore(tile)) {
|
if (gm.isShore(tile)) {
|
||||||
|
|||||||
@@ -249,6 +249,8 @@ export class GameMapImpl implements GameMap {
|
|||||||
return this.magnitude(ref) < 10 ? 2 : 1;
|
return this.magnitude(ref) < 10 ? 2 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if updating these magnitude values, also update
|
||||||
|
// `../../../map-generator/map_generator.go` `getThumbnailColor`
|
||||||
terrainType(ref: TileRef): TerrainType {
|
terrainType(ref: TileRef): TerrainType {
|
||||||
if (this.isLand(ref)) {
|
if (this.isLand(ref)) {
|
||||||
const magnitude = this.magnitude(ref);
|
const magnitude = this.magnitude(ref);
|
||||||
|
|||||||
Reference in New Issue
Block a user