baseline: working salvaged prototype on Godot 4.7.1 (pre-F1)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stewart Howe 2026-08-05 01:45:41 -04:00
commit 7f0f43b5fb
48 changed files with 2913 additions and 0 deletions

58
.gitignore vendored Normal file
View file

@ -0,0 +1,58 @@
# ---------------------------------------------------------------------------
# IslaApocalypse — Godot 4 / C# .gitignore
#
# Rule of thumb: commit SOURCE (what you wrote), ignore GENERATED (what a tool
# can rebuild). Everything below is rebuilt automatically by Godot or the C#
# compiler, so keeping it out of git keeps the repo small and avoids conflicts.
# ---------------------------------------------------------------------------
# Godot's editor cache: imported textures, shader cache, the generated C# glue.
# Regenerated on first open. This is the big one (~9 MB here, and it grows).
.godot/
.import/
# C# / .NET build output. Rebuilt by any build.
bin/
obj/
*.dll
*.pdb
*.exe
*.so
*.dylib
*.nupkg
# Old Mono-era folder, kept out in case an older tool recreates it.
.mono/
# Per-developer IDE/editor settings — not project settings, so not shared.
*.user
*.userprefs
.vs/
.vscode/
.idea/
# Exported game builds — artifacts of a release, not source.
/build/
/builds/
/export/
/exports/
*.pck
*.zip
# Crash dumps.
mono_crash.*.json
logs/
# OS clutter.
.DS_Store
Thumbs.db
# ---------------------------------------------------------------------------
# Deliberately NOT ignored (these are source and MUST be committed):
# *.cs *.tscn project.godot *.csproj *.sln ServerConfig.json
# icon.svg + icon.svg.import READMEs Data/ and Resources/ contents
#
# Note on generated world data: the map blueprint (MapData_Seed_*.dat, ~512 MB)
# and its PNG are written to Godot's `user://` folder, which lives OUTSIDE this
# project directory — so they can never be committed by accident. Nothing to do.
# ---------------------------------------------------------------------------

19
Client/README.md Normal file
View file

@ -0,0 +1,19 @@
# Client Module - IslaApocalypse
**Phase 2 Status:** Active (Mesh Rendering & Vertex Painting Online)
## Overview
The `Client` directory handles the visual representation of the 3D Voxel Engine. It is strictly responsible for receiving pre-calculated mathematical data from the `Server` and `Core` modules and translating it into Godot-native visual nodes (`MeshInstance3D`). It does not calculate collisions, densities, or terrain generation.
## Key Architecture & Components
### 1. The Visualizer (`ChunkRenderer.cs`)
A script attached to a `MeshInstance3D` node that physically draws a single 32x32 chunk of the world.
* **Mesh Generation:** Calls `MarchingCubes.GenerateMesh()` from the `Core` module, passing in the `Densities` and `BlockIDs` scalar fields.
* **Vertex Painting:** Dynamically creates a `StandardMaterial3D` and enables `VertexColorUseAsAlbedo = true`. This tells Godot to ignore standard texture maps and instead color the mesh using the exact RGB values we assigned to the vertices based on their `BlockID` (e.g., Green for Grass, Brown for Wasteland).
* **World Positioning:** Multiplies the `ChunkPosition` (e.g., [1, 0]) by `Constants.CHUNK_SIZE` and `Constants.VOXEL_SCALE` (1.0f) to perfectly align the chunk in the global 3D space.
* **Backface Rendering:** `CullMode` is explicitly disabled to ensure players don't see through the bottom of the map if they clip inside a mountain.
## Next Immediate Steps (Phase 2, Step 4)
* Currently, the renderer draws the exact topography. Once the `Server` implements **Road Carving**, the `Client` will automatically inherit those flattened vertices and paint them with the `ASPHALT` vertex color without needing any code changes here.
* Future implementations will include reading `BlockIDs` to paint different UV maps/textures instead of just raw RGB vertex colors.

View file

@ -0,0 +1,31 @@
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Client
{
// Notice this inherits from MeshInstance3D, which is Godot's visual 3D node!
public partial class ChunkRenderer : MeshInstance3D
{
public void RenderChunk(ChunkData data)
{
// 1. Pass the BlockIDs array to the generator!
Mesh = MarchingCubes.GenerateMesh(data.Densities, data.BlockIDs, Constants.ISO_LEVEL, Constants.VOXEL_SCALE);
StandardMaterial3D mat = new StandardMaterial3D();
// 2. THE FIX: Tell the material to use the colors we paint on the vertices!
mat.VertexColorUseAsAlbedo = true;
mat.Roughness = 0.8f;
mat.CullMode = BaseMaterial3D.CullModeEnum.Disabled;
MaterialOverride = mat;
Position = new Vector3(
data.ChunkPosition.X * Constants.CHUNK_SIZE_X * Constants.VOXEL_SCALE,
0,
data.ChunkPosition.Y * Constants.CHUNK_SIZE_Z * Constants.VOXEL_SCALE
);
}
}
}

View file

@ -0,0 +1 @@
uid://8que4lnxlubg

27
Core/README.md Normal file
View file

@ -0,0 +1,27 @@
# Core Module - IslaApocalypse
**Phase 2 Status:** Active (Voxel Math & Parsing Online)
## Overview
The `Core` directory contains the mathematical foundation, data structures, and parser required to bridge the 2D procedural generation with the 3D Voxel Engine. It is strictly logic-driven and contains no Godot Nodes or visual elements.
## Key Architecture & Components
### 1. The Data Bridge (`MapDataParser.cs`)
Responsible for deserializing the highly compressed binary `.dat` file generated by the `/Tools` pipeline.
* **Outputs:** A `WorldBlueprint` object held in RAM.
* **Capabilities:** Reads the 2D heightmap, biome map, and town locations. Crucially, it parses all four tiers of A* road vectors (Highways, Branch Roads, Rugged Roads, Trails) for the Server to use in terrain flattening.
### 2. Voxel Data Containers (`ChunkData.cs` & `Constants.cs`)
* **Chunk Dimensions:** Defined in `Constants.cs`. Currently optimized to `32x32` horizontal with a massive `256` vertical height limit to allow for true AAA-scale mountain ranges without heightmap compression.
* **Seamless Borders:** `ChunkData.cs` expands its scalar field arrays (`Densities` and `BlockIDs`) by `+1` on all axes. This allows the Marching Cubes algorithm to sample the neighboring chunk's data, ensuring meshes connect perfectly without gaps.
### 3. The Math Engine (`MarchingCubes.cs`)
Transforms the raw `ChunkData` scalar fields into physical Godot `ArrayMesh` geometry.
* **Analytical Normals:** Instead of flat shading, the algorithm calculates exact 3D slope gradients at the corners of each voxel. This produces smooth, realistic lighting across curved terrain.
* **Vertex Painting:** Reads the `BlockID` assigned to each voxel corner and applies physical RGB colors directly to the mesh vertices before committing the geometry.
### 4. Biome & Material Mapping (`BiomePalette.cs`, `BlockRegistry.cs`)
Translates the 2D world into physical 3D materials.
* **BlockRegistry:** A byte-based lookup table defining physical materials (`SAND`, `STONE`, `WASTELAND_DIRT`, `ASPHALT`).
* **BiomePalette Logic:** Uses the 2D `Biome` pixel combined with the 3D `currentY` depth to determine the block type. For example, `Biome.Wasteland` combined with a depth of 0 yields `WASTELAND_DIRT`, while a depth > 4 yields `STONE`.

View file

@ -0,0 +1,63 @@
using Godot;
namespace IslaApocalypse.Core
{
public static class BiomePalette
{
/// <summary>
/// Determines the exact Block ID for a specific voxel in the 3D world based on its depth and biome.
/// </summary>
/// <param name="biome">The 2D biome pixel from the MapData.</param>
/// <param name="surfaceHeight">The maximum height of the terrain at this X/Z coordinate.</param>
/// <param name="currentY">The Y coordinate of the specific voxel we are painting.</param>
/// <param name="isRoad">Is this coordinate part of the highway/branch road network?</param>
public static byte GetVoxelID(Biome biome, int surfaceHeight, int currentY, bool isRoad)
{
// 1. Air Check: If we are above the terrain, it's Air.
if (currentY > surfaceHeight) return BlockRegistry.AIR;
// 2. Bedrock Check: The very bottom of the world is indestructible.
if (currentY == 0) return BlockRegistry.BEDROCK;
// 3. Infrastructure Check: If this is a road, and we are exactly on the surface, paint asphalt.
if (isRoad && currentY == surfaceHeight) return BlockRegistry.ASPHALT;
int depthBelowSurface = surfaceHeight - currentY;
// 4. Subsurface Logic (Dirt layer vs Stone layer)
// The top 4 blocks are usually dirt/sand, everything below that is solid stone.
if (depthBelowSurface > 4) return BlockRegistry.STONE;
// 5. Surface & Shallow Subsurface Logic (Based on Biome)
switch (biome)
{
case Biome.Beach:
case Biome.Crater:
return BlockRegistry.SAND; // Craters and Beaches are deep sand
case Biome.Snow:
case Biome.Mountain:
if (depthBelowSurface == 0) return BlockRegistry.SNOW; // Only the top layer is snow
return BlockRegistry.STONE; // Mountains have shallow topsoil, mostly stone
case Biome.Wasteland:
return BlockRegistry.WASTELAND_DIRT;
case Biome.Jungle:
if (depthBelowSurface == 0) return BlockRegistry.GRASS_JUNGLE;
return BlockRegistry.DIRT;
case Biome.Paradise:
if (depthBelowSurface == 0) return BlockRegistry.GRASS_PARADISE;
return BlockRegistry.DIRT;
case Biome.Tropical:
if (depthBelowSurface == 0) return BlockRegistry.GRASS_TROPICAL;
return BlockRegistry.DIRT;
default:
return BlockRegistry.DIRT; // Fallback
}
}
}
}

View file

@ -0,0 +1 @@
uid://dx8hj4h8v0f2j

20
Core/Scripts/BlockData.cs Normal file
View file

@ -0,0 +1,20 @@
using Godot;
namespace IslaApocalypse.Core
{
public struct BlockData
{
public byte ID;
public string Name;
public bool IsSolid;
public Color BaseColor; // We use color for now; later we replace this with Texture Array IDs for the Shader
public BlockData(byte id, string name, bool isSolid, Color baseColor)
{
ID = id;
Name = name;
IsSolid = isSolid;
BaseColor = baseColor;
}
}
}

View file

@ -0,0 +1 @@
uid://bjhct6ctt0j46

View file

@ -0,0 +1,53 @@
using Godot;
using System.Collections.Generic;
namespace IslaApocalypse.Core
{
public static class BlockRegistry
{
// A dictionary holding all the block types in the game, searchable by their byte ID
public static readonly Dictionary<byte, BlockData> Blocks = new Dictionary<byte, BlockData>();
// Hardcoded IDs for easy reference in code
public const byte AIR = 0;
public const byte BEDROCK = 1;
public const byte STONE = 2;
public const byte DIRT = 3;
public const byte SAND = 4;
public const byte GRASS_TROPICAL = 5;
public const byte GRASS_JUNGLE = 6;
public const byte GRASS_PARADISE = 7;
public const byte SNOW = 8;
public const byte WASTELAND_DIRT = 9;
public const byte ASPHALT = 10; // For our roads!
// This static constructor runs automatically the first time the registry is accessed
static BlockRegistry()
{
// ID 0 is ALWAYS Air in voxel engines.
Blocks.Add(AIR, new BlockData(AIR, "Air", false, Colors.Transparent));
// The Foundation Blocks
Blocks.Add(BEDROCK, new BlockData(BEDROCK, "Bedrock", true, new Color(0.1f, 0.1f, 0.1f)));
Blocks.Add(STONE, new BlockData(STONE, "Stone", true, new Color(0.5f, 0.5f, 0.5f)));
Blocks.Add(DIRT, new BlockData(DIRT, "Dirt", true, new Color(0.4f, 0.3f, 0.2f)));
Blocks.Add(SAND, new BlockData(SAND, "Sand", true, new Color(0.8f, 0.7f, 0.4f)));
// The Biome Surface Blocks
Blocks.Add(GRASS_TROPICAL, new BlockData(GRASS_TROPICAL, "Tropical Grass", true, new Color(0.2f, 0.6f, 0.2f)));
Blocks.Add(GRASS_JUNGLE, new BlockData(GRASS_JUNGLE, "Jungle Grass", true, new Color(0.1f, 0.4f, 0.1f)));
Blocks.Add(GRASS_PARADISE, new BlockData(GRASS_PARADISE, "Paradise Grass", true, new Color(0.4f, 0.8f, 0.4f)));
Blocks.Add(SNOW, new BlockData(SNOW, "Snow", true, new Color(0.9f, 0.9f, 0.9f)));
Blocks.Add(WASTELAND_DIRT, new BlockData(WASTELAND_DIRT, "Wasteland Dirt", true, new Color(0.5f, 0.2f, 0.5f)));
// Infrastructure
Blocks.Add(ASPHALT, new BlockData(ASPHALT, "Asphalt", true, new Color(0.15f, 0.15f, 0.15f)));
}
public static BlockData GetBlock(byte id)
{
if (Blocks.TryGetValue(id, out BlockData data)) return data;
return Blocks[AIR]; // Safe fallback so the game doesn't crash if an ID is missing
}
}
}

View file

@ -0,0 +1 @@
uid://c2145lfqj00uk

29
Core/Scripts/ChunkData.cs Normal file
View file

@ -0,0 +1,29 @@
using Godot;
namespace IslaApocalypse.Core
{
public class ChunkData
{
public Vector2I ChunkPosition; // The X/Z coordinate of this chunk (e.g., 0,0 or 1,0)
// We need Width+1 and Depth+1 so the Marching Cubes mesh connects seamlessly to the neighbor chunks!
// public float[,,] Densities = new float[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_HEIGHT, Constants.CHUNK_SIZE_Z + 1];
// public byte[,,] BlockIDs = new byte[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_HEIGHT, Constants.CHUNK_SIZE_Z + 1];
// OLD:
// Densities = new float[Constants.CHUNK_SIZE_X, Constants.CHUNK_HEIGHT, Constants.CHUNK_SIZE_Z];
// BlockIDs = new int[Constants.CHUNK_SIZE_X, Constants.CHUNK_HEIGHT, Constants.CHUNK_SIZE_Z];
// NEW (Fix 3): We expand by 1 to sample the neighbor's border perfectly without seams!
public float[,,] Densities = new float[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_HEIGHT + 1, Constants.CHUNK_SIZE_Z + 1];
public byte[,,] BlockIDs = new byte[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_HEIGHT + 1, Constants.CHUNK_SIZE_Z + 1];
// For our future delta-save system
public bool IsPlayerProtected = false;
public bool NeedsSaving = false;
public ChunkData(Vector2I position)
{
ChunkPosition = position;
}
}
}

View file

@ -0,0 +1 @@
uid://cturk2xl2lod

View file

@ -0,0 +1,111 @@
using Godot;
using Godot.Collections; // Required for Godot's built-in JSON parser
namespace IslaApocalypse.Core // Change this if your namespace is different
{
public static class ConfigManager
{
// Default Fallbacks
public static int WorldSeed = 1063685222;
// public static int WorldSeed = (int)GD.Randi(); if 0 also randomizes
public static int MapSize = 8192;
public static float CraterRadius = 400f;
public static float DensityMultiplier = 1.0f; // <-- Replaces TownCount
public static int ChunkRadius = 24; // Default chunk size, can be overridden by config
public static void LoadConfig()
{
string path = "res://ServerConfig.json";
if (!FileAccess.FileExists(path))
{
GD.PrintErr("[ConfigManager] ServerConfig.json not found! Defaulting to 8K.");
return;
}
// Read the file
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
string content = file.GetAsText();
// Parse the JSON
var json = new Json();
var error = json.Parse(content);
if (error != Error.Ok)
{
GD.PrintErr($"[ConfigManager] JSON Parse Error: {json.GetErrorMessage()}");
return;
}
var data = (Dictionary)json.Data;
// Extract the Seed
if (data.ContainsKey("WorldSeed"))
{
WorldSeed = (int)data["WorldSeed"];
}
// Extract the MapProfile and run your Switch/Case logic!
string profile = "8K";
if (data.ContainsKey("MapProfile"))
{
profile = (string)data["MapProfile"];
}
// Extract the TownDensity and run your Switch/Case logic!
string townDensity = "Normal";
if (data.ContainsKey("TownDensity"))
{
townDensity = (string)data["TownDensity"];
}
// Extract the ChunkRadius
if (data.ContainsKey("ChunkRadius")) {
ChunkRadius = (int)data["ChunkRadius"];
}
switch (profile)
{
case "4K":
MapSize = 4096;
CraterRadius = 400f; // Scaled down crater
break;
case "6K":
MapSize = 6144;
CraterRadius = 600f;
break;
case "8K":
MapSize = 8192;
CraterRadius = 800f; // Your original crater size spread over an 8K map
break;
case "10K":
MapSize = 10240;
CraterRadius = 1000f;
break;
default:
GD.PrintErr($"[ConfigManager] Unknown MapProfile '{profile}'. Defaulting to 8K.");
MapSize = 8192;
CraterRadius = 800f;
break;
}
switch (townDensity)
{
case "Sparse":
DensityMultiplier = 0.5f;
break;
case "Normal":
DensityMultiplier = 1.0f;
break;
case "Dense":
DensityMultiplier = 2.0f;
break;
default:
GD.PrintErr($"[ConfigManager] Unknown TownDensity '{townDensity}'. Defaulting to Normal.");
DensityMultiplier = 1.0f;
break;
}
GD.Print($"[ConfigManager] Successfully loaded {profile} Profile. MapSize set to {MapSize}.");
}
}
}

View file

@ -0,0 +1 @@
uid://dttxbmvvhlvkl

14
Core/Scripts/Constants.cs Normal file
View file

@ -0,0 +1,14 @@
namespace IslaApocalypse.Core
{
public static class Constants
{
// Chunk Dimensions
public const int CHUNK_SIZE_X = 24;
public const int CHUNK_SIZE_Z = 24;
public const int CHUNK_HEIGHT = 256; // Our agreed-upon depth!
// World Generation
public const float ISO_LEVEL = 0.0f; // The threshold for Marching Cubes
public const float VOXEL_SCALE = 1.0f; // 1 Pixel = 1 Meter
}
}

View file

@ -0,0 +1 @@
uid://dhnft3c7f6ek2

8
Core/Scripts/Enums.cs Normal file
View file

@ -0,0 +1,8 @@
namespace IslaApocalypse.Core
{
// The master list of shared identifiers for the game
public enum Biome { Ocean, Lake, Beach, Paradise, Tropical, Jungle, Wasteland, Crater, Mountain, Snow }
public enum TownTier { Village, Hub, Capitol, Outpost, IslandLoot, MiniPOI }
public enum MapHalf { Any, Left, Right }
}

View file

@ -0,0 +1 @@
uid://b573cexjylgn8

View file

@ -0,0 +1,55 @@
# 📐 The Math of Marching Cubes (Smooth Voxels)
## Overview
In a standard blocky game like *Minecraft* (Greedy Meshing), if a coordinate has a block, you draw a square. If it doesn't, you draw nothing.
In a smooth voxel game like *7 Days to Die*, we use an algorithm called **Marching Cubes**. Instead of looking at a single block, the algorithm looks at the **8 corners** of an invisible 3D grid cell (a cube) and asks: *"Which of these 8 corners are underground, and which are in the air?"*
Based on the answer, it draws a specific set of triangles *through* the invisible cube to create a smooth surface.
---
## 🔑 Key Concepts
### 1. Density (The "Float" Value)
In smooth voxels, a point in space isn't just "Solid" (1) or "Empty" (0). It has a continuous **Density** value.
* ` 1.0` = Deep underground (Solid Rock).
* ` 0.5` = Slightly underground (Dirt).
* ` 0.0` = The exact surface of the ground.
* `-0.5` = Slightly above ground (Air).
* `-1.0` = High in the sky (Empty Space).
### 2. The Iso-Level (The Threshold)
The Iso-Level is the exact density value where the "skin" of the world is drawn. Usually, this is `0.0`.
* If a corner's density is **greater** than `0.0`, it is "Inside" the terrain.
* If a corner's density is **less** than `0.0`, it is "Outside" in the air.
### 3. The Triangulation Table (The Magic Array)
A cube has 8 corners. Each corner can either be Inside or Outside.
That means there are **2^8 = 256 possible combinations**.
Instead of writing 256 `if/else` statements, the algorithm uses a hardcoded **Lookup Table** (an array of integers).
* *Example:* If corner 0 is inside, but corners 1-7 are outside, the table instantly says: *"Draw a single small triangle slicing off corner 0."*
* *Example:* If corners 0, 1, 2, and 3 are inside (the whole bottom half), the table says: *"Draw a flat quad (two triangles) straight across the middle of the cube."*
### 4. Linear Interpolation (Why it looks Smooth)
If the algorithm just drew triangles exactly halfway between the inside and outside corners, the terrain would still look a bit jagged (like a low-poly PS1 game).
To make it perfectly smooth, we **Interpolate**:
* Corner A has a density of `1.0` (Very solid).
* Corner B has a density of `-0.1` (Just barely in the air).
* Because `-0.1` is much closer to our `0.0` Iso-Level than `1.0` is, the algorithm slides the triangle vertex much closer to Corner B. This creates gentle slopes and sharp cliffs dynamically.
---
## ⚙️ The Execution Loop (What our C# Script will do)
When the Server tells the Client to render a 16x16x64 chunk, the mesher does this:
1. Loops through every X, Y, Z coordinate in the chunk.
2. Checks the 8 corners of the current voxel.
3. Creates an `8-bit integer` (a byte) based on which corners are solid (e.g., `00001111`).
4. Plugs that byte into the **Triangulation Table**.
5. The table spits out a list of edges.
6. The script calculates the exact interpolated point on those edges.
7. It connects those points into triangles and adds them to Godot's `ArrayMesh`.
8. *It Marches* to the next cube and repeats.

View file

@ -0,0 +1,129 @@
using Godot;
using System.IO;
using System.Collections.Generic;
namespace IslaApocalypse.Core
{
// 1. The Data Container
// This holds the unpacked data in RAM so the Server can ask it questions
// without having to re-read the hard drive every single frame.
public struct TownLocation
{
public Vector2 Position;
public TownTier Tier;
}
public class WorldBlueprint
{
public int MapSize;
public float[,] HeightMap;
public Biome[,] BiomeMap;
public List<TownLocation> Towns = new List<TownLocation>();
// All 4 road tiers!
public List<Vector2[]> Highways = new List<Vector2[]>();
public List<Vector2[]> BranchRoads = new List<Vector2[]>();
public List<Vector2[]> RuggedRoads = new List<Vector2[]>();
public List<Vector2[]> TrailRoads = new List<Vector2[]>();
}
// 2. The Parser Utility
public static class MapDataParser
{
public static WorldBlueprint LoadMapData(string seedStr)
{
string filePath = ProjectSettings.GlobalizePath($"user://MapData_Seed_{seedStr}.dat");
if (!File.Exists(filePath))
{
GD.PrintErr($"[MapDataParser] CRITICAL ERROR: Map file not found at {filePath}");
return null;
}
WorldBlueprint blueprint = new WorldBlueprint();
using (FileStream stream = File.OpenRead(filePath))
{
using (BinaryReader reader = new BinaryReader(stream))
{
// 1. Header Validation (CRITICAL: Must read string first to align bytes!)
string header = reader.ReadString();
if (header != "ISLA_V1")
{
GD.PrintErr("[MapDataParser] ERROR: Invalid map version or corrupted file.");
return null;
}
// 2. Read Map Dimensions & Initialize Arrays
blueprint.MapSize = reader.ReadInt32();
blueprint.HeightMap = new float[blueprint.MapSize, blueprint.MapSize];
blueprint.BiomeMap = new Biome[blueprint.MapSize, blueprint.MapSize];
// 3. Read Raw Voxel Data (Height & Biome)
for (int x = 0; x < blueprint.MapSize; x++)
{
for (int y = 0; y < blueprint.MapSize; y++)
{
blueprint.HeightMap[x, y] = reader.ReadSingle();
blueprint.BiomeMap[x, y] = (Biome)reader.ReadInt32();
}
}
// 4. Read Logistical Anchors (Towns & POIs)
int townCount = reader.ReadInt32();
for (int i = 0; i < townCount; i++)
{
TownLocation town = new TownLocation();
town.Position = new Vector2(reader.ReadSingle(), reader.ReadSingle());
town.Tier = (TownTier)reader.ReadInt32();
blueprint.Towns.Add(town);
}
// 5. Read Road Networks
// Highway
int highwayCount = reader.ReadInt32();
for (int i = 0; i < highwayCount; i++)
{
int pathLength = reader.ReadInt32();
Vector2[] path = new Vector2[pathLength];
for (int p = 0; p < pathLength; p++) { path[p] = new Vector2(reader.ReadSingle(), reader.ReadSingle()); }
blueprint.Highways.Add(path);
}
// Branch
int branchCount = reader.ReadInt32();
for (int i = 0; i < branchCount; i++)
{
int pathLength = reader.ReadInt32();
Vector2[] path = new Vector2[pathLength];
for (int p = 0; p < pathLength; p++) { path[p] = new Vector2(reader.ReadSingle(), reader.ReadSingle()); }
blueprint.BranchRoads.Add(path);
}
// Rugged
int ruggedCount = reader.ReadInt32();
for (int i = 0; i < ruggedCount; i++)
{
int pathLength = reader.ReadInt32();
Vector2[] path = new Vector2[pathLength];
for (int p = 0; p < pathLength; p++) { path[p] = new Vector2(reader.ReadSingle(), reader.ReadSingle()); }
blueprint.RuggedRoads.Add(path);
}
// Trails
int trailCount = reader.ReadInt32();
for (int i = 0; i < trailCount; i++)
{
int pathLength = reader.ReadInt32();
Vector2[] path = new Vector2[pathLength];
for (int p = 0; p < pathLength; p++) { path[p] = new Vector2(reader.ReadSingle(), reader.ReadSingle()); }
blueprint.TrailRoads.Add(path);
}
}
}
GD.Print($"[MapDataParser] Successfully loaded Blueprint for Seed {seedStr}. Dimension: {blueprint.MapSize}x{blueprint.MapSize}.");
return blueprint;
}
}
}

View file

@ -0,0 +1 @@
uid://6qednxmubxia

View file

@ -0,0 +1,522 @@
using Godot;
using System;
using System.Collections.Generic;
namespace IslaApocalypse.Core
{
public static class MarchingCubes
{
/// <summary>
/// The Edge Table (256 values).
/// This array uses an 8-bit index (representing the 8 corners of a cube)
/// to look up a 12-bit value. Each bit in the returned value corresponds to
/// one of the 12 edges of the cube, telling the engine if that edge is intersected.
/// </summary>
public static readonly int[] EdgeTable = new int[256] {
0x0 , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90,
0x230, 0x339, 0x33 , 0x13a, 0x636, 0x73f, 0x435, 0x53c,
0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
0x3a0, 0x2a9, 0x1a3, 0xaa , 0x7a6, 0x6af, 0x5a5, 0x4ac,
0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0,
0x460, 0x569, 0x663, 0x76a, 0x66 , 0x16f, 0x265, 0x36c,
0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60,
0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff , 0x3f5, 0x2fc,
0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55 , 0x15c,
0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950,
0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc ,
0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0,
0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc,
0xcc , 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c,
0x15c, 0x55 , 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650,
0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
0x2fc, 0x3f5, 0xff , 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0,
0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c,
0x36c, 0x265, 0x16f, 0x66 , 0x76a, 0x663, 0x569, 0x460,
0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac,
0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa , 0x1a3, 0x2a9, 0x3a0,
0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c,
0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33 , 0x339, 0x230,
0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190,
0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0
};
/// <summary>
/// The Triangulation Table (256 rows, 16 columns).
/// Once we know which edges are intersected, this table tells us exactly
/// how to connect those intersections to draw up to 5 triangles.
/// A value of -1 means "stop drawing triangles".
/// </summary>
public static readonly int[,] TriTable = new int[256, 16] {
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{2, 8, 3, 2, 10, 8, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1},
{3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{0, 11, 2, 8, 11, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{1, 11, 2, 1, 9, 11, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1},
{3, 10, 1, 11, 10, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{0, 10, 1, 0, 8, 10, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1},
{3, 9, 0, 3, 11, 9, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1},
{9, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{4, 3, 0, 7, 3, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{0, 1, 9, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{4, 1, 9, 4, 7, 1, 7, 3, 1, -1, -1, -1, -1, -1, -1, -1},
{1, 2, 10, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{3, 4, 7, 3, 0, 4, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1},
{9, 2, 10, 9, 0, 2, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1},
{2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4, -1, -1, -1, -1},
{8, 4, 7, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{11, 4, 7, 11, 2, 4, 2, 0, 4, -1, -1, -1, -1, -1, -1, -1},
{9, 0, 1, 8, 4, 7, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1},
{4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1, -1, -1, -1, -1},
{3, 10, 1, 3, 11, 10, 7, 8, 4, -1, -1, -1, -1, -1, -1, -1},
{1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4, -1, -1, -1, -1},
{4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3, -1, -1, -1, -1},
{4, 7, 11, 4, 11, 9, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1},
{9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{9, 5, 4, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{0, 5, 4, 1, 5, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{8, 5, 4, 8, 3, 5, 3, 1, 5, -1, -1, -1, -1, -1, -1, -1},
{1, 2, 10, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{3, 0, 8, 1, 2, 10, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1},
{5, 2, 10, 5, 4, 2, 4, 0, 2, -1, -1, -1, -1, -1, -1, -1},
{2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8, -1, -1, -1, -1},
{9, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{0, 11, 2, 0, 8, 11, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1},
{0, 5, 4, 0, 1, 5, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1},
{2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5, -1, -1, -1, -1},
{10, 3, 11, 10, 1, 3, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1},
{4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10, -1, -1, -1, -1},
{5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3, -1, -1, -1, -1},
{5, 4, 8, 5, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1},
{9, 7, 8, 5, 7, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{9, 3, 0, 9, 5, 3, 5, 7, 3, -1, -1, -1, -1, -1, -1, -1},
{0, 7, 8, 0, 1, 7, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1},
{1, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{9, 7, 8, 9, 5, 7, 10, 1, 2, -1, -1, -1, -1, -1, -1, -1},
{10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3, -1, -1, -1, -1},
{8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2, -1, -1, -1, -1},
{2, 10, 5, 2, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1},
{7, 9, 5, 7, 8, 9, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1},
{9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11, -1, -1, -1, -1},
{2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7, -1, -1, -1, -1},
{11, 2, 1, 11, 1, 7, 7, 1, 5, -1, -1, -1, -1, -1, -1, -1},
{9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11, -1, -1, -1, -1},
{5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0, -1},
{11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0, -1},
{11, 10, 5, 7, 11, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{0, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{9, 0, 1, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{1, 8, 3, 1, 9, 8, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1},
{1, 6, 5, 2, 6, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{1, 6, 5, 1, 2, 6, 3, 0, 8, -1, -1, -1, -1, -1, -1, -1},
{9, 6, 5, 9, 0, 6, 0, 2, 6, -1, -1, -1, -1, -1, -1, -1},
{5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8, -1, -1, -1, -1},
{2, 3, 11, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{11, 0, 8, 11, 2, 0, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1},
{0, 1, 9, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1},
{5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11, -1, -1, -1, -1},
{6, 3, 11, 6, 5, 3, 5, 1, 3, -1, -1, -1, -1, -1, -1, -1},
{0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6, -1, -1, -1, -1},
{3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9, -1, -1, -1, -1},
{6, 5, 9, 6, 9, 11, 11, 9, 8, -1, -1, -1, -1, -1, -1, -1},
{5, 10, 6, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{4, 3, 0, 4, 7, 3, 6, 5, 10, -1, -1, -1, -1, -1, -1, -1},
{1, 9, 0, 5, 10, 6, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1},
{10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4, -1, -1, -1, -1},
{6, 1, 2, 6, 5, 1, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1},
{1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7, -1, -1, -1, -1},
{8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6, -1, -1, -1, -1},
{7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9, -1},
{3, 11, 2, 7, 8, 4, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1},
{5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11, -1, -1, -1, -1},
{0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1},
{9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6, -1},
{8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6, -1, -1, -1, -1},
{5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11, -1},
{0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7, -1},
{6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9, -1, -1, -1, -1},
{10, 4, 9, 6, 4, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{4, 10, 6, 4, 9, 10, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1},
{10, 0, 1, 10, 6, 0, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1},
{8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10, -1, -1, -1, -1},
{1, 4, 9, 1, 2, 4, 2, 6, 4, -1, -1, -1, -1, -1, -1, -1},
{3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4, -1, -1, -1, -1},
{0, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{8, 3, 2, 8, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1},
{10, 4, 9, 10, 6, 4, 11, 2, 3, -1, -1, -1, -1, -1, -1, -1},
{0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6, -1, -1, -1, -1},
{3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10, -1, -1, -1, -1},
{6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1, -1},
{9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3, -1, -1, -1, -1},
{8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1, -1},
{3, 11, 6, 3, 6, 0, 0, 6, 4, -1, -1, -1, -1, -1, -1, -1},
{6, 4, 8, 11, 6, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{7, 10, 6, 7, 8, 10, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1},
{0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10, -1, -1, -1, -1},
{10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0, -1, -1, -1, -1},
{10, 6, 7, 10, 7, 1, 1, 7, 3, -1, -1, -1, -1, -1, -1, -1},
{1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7, -1, -1, -1, -1},
{2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9, -1},
{7, 8, 0, 7, 0, 6, 6, 0, 2, -1, -1, -1, -1, -1, -1, -1},
{7, 3, 2, 6, 7, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7, -1, -1, -1, -1},
{2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7, -1},
{1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11, -1},
{11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1, -1, -1, -1, -1},
{8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6, -1},
{0, 9, 1, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0, -1, -1, -1, -1},
{7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{3, 0, 8, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{0, 1, 9, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{8, 1, 9, 8, 3, 1, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1},
{10, 1, 2, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{1, 2, 10, 3, 0, 8, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1},
{2, 9, 0, 2, 10, 9, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1},
{6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8, -1, -1, -1, -1},
{7, 2, 3, 6, 2, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{7, 0, 8, 7, 6, 0, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1},
{2, 7, 6, 2, 3, 7, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1},
{1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6, -1, -1, -1, -1},
{10, 7, 6, 10, 1, 7, 1, 3, 7, -1, -1, -1, -1, -1, -1, -1},
{10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8, -1, -1, -1, -1},
{0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7, -1, -1, -1, -1},
{7, 6, 10, 7, 10, 8, 8, 10, 9, -1, -1, -1, -1, -1, -1, -1},
{6, 8, 4, 11, 8, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{3, 6, 11, 3, 0, 6, 0, 4, 6, -1, -1, -1, -1, -1, -1, -1},
{8, 6, 11, 8, 4, 6, 9, 0, 1, -1, -1, -1, -1, -1, -1, -1},
{9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6, -1, -1, -1, -1},
{6, 8, 4, 6, 11, 8, 2, 10, 1, -1, -1, -1, -1, -1, -1, -1},
{1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6, -1, -1, -1, -1},
{4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9, -1, -1, -1, -1},
{10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3, -1},
{8, 2, 3, 8, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1},
{0, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8, -1, -1, -1, -1},
{1, 9, 4, 1, 4, 2, 2, 4, 6, -1, -1, -1, -1, -1, -1, -1},
{8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1, -1, -1, -1, -1},
{10, 1, 0, 10, 0, 6, 6, 0, 4, -1, -1, -1, -1, -1, -1, -1},
{4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3, -1},
{10, 9, 4, 6, 10, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{4, 9, 5, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{0, 8, 3, 4, 9, 5, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1},
{5, 0, 1, 5, 4, 0, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1},
{11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5, -1, -1, -1, -1},
{9, 5, 4, 10, 1, 2, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1},
{6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5, -1, -1, -1, -1},
{7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2, -1, -1, -1, -1},
{3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6, -1},
{7, 2, 3, 7, 6, 2, 5, 4, 9, -1, -1, -1, -1, -1, -1, -1},
{9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7, -1, -1, -1, -1},
{3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0, -1, -1, -1, -1},
{6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8, -1},
{9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7, -1, -1, -1, -1},
{1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4, -1},
{4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10, -1},
{7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10, -1, -1, -1, -1},
{6, 9, 5, 6, 11, 9, 11, 8, 9, -1, -1, -1, -1, -1, -1, -1},
{3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5, -1, -1, -1, -1},
{0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11, -1, -1, -1, -1},
{6, 11, 3, 6, 3, 5, 5, 3, 1, -1, -1, -1, -1, -1, -1, -1},
{1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6, -1, -1, -1, -1},
{0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10, -1},
{11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5, -1},
{6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3, -1, -1, -1, -1},
{5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2, -1, -1, -1, -1},
{9, 5, 6, 9, 6, 0, 0, 6, 2, -1, -1, -1, -1, -1, -1, -1},
{1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8, -1},
{1, 5, 6, 2, 1, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6, -1},
{10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0, -1, -1, -1, -1},
{0, 3, 8, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{11, 5, 10, 7, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{11, 5, 10, 11, 7, 5, 8, 3, 0, -1, -1, -1, -1, -1, -1, -1},
{5, 11, 7, 5, 10, 11, 1, 9, 0, -1, -1, -1, -1, -1, -1, -1},
{10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1, -1, -1, -1, -1},
{11, 1, 2, 11, 7, 1, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1},
{0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11, -1, -1, -1, -1},
{9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7, -1, -1, -1, -1},
{7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2, -1},
{2, 5, 10, 2, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1},
{8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5, -1, -1, -1, -1},
{9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2, -1, -1, -1, -1},
{9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2, -1},
{1, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{0, 8, 7, 0, 7, 1, 1, 7, 5, -1, -1, -1, -1, -1, -1, -1},
{9, 0, 3, 9, 3, 5, 5, 3, 7, -1, -1, -1, -1, -1, -1, -1},
{9, 8, 7, 5, 9, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{5, 8, 4, 5, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1},
{5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0, -1, -1, -1, -1},
{0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5, -1, -1, -1, -1},
{10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4, -1},
{2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8, -1, -1, -1, -1},
{0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11, -1},
{0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5, -1},
{9, 4, 5, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4, -1, -1, -1, -1},
{5, 10, 2, 5, 2, 4, 4, 2, 0, -1, -1, -1, -1, -1, -1, -1},
{3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9, -1},
{5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2, -1, -1, -1, -1},
{8, 4, 5, 8, 5, 3, 3, 5, 1, -1, -1, -1, -1, -1, -1, -1},
{0, 4, 5, 1, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5, -1, -1, -1, -1},
{9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{4, 11, 7, 4, 9, 11, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1},
{0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11, -1, -1, -1, -1},
{1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11, -1, -1, -1, -1},
{3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4, -1},
{4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2, -1, -1, -1, -1},
{9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3, -1},
{11, 7, 4, 11, 4, 2, 2, 4, 0, -1, -1, -1, -1, -1, -1, -1},
{11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4, -1, -1, -1, -1},
{2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9, -1, -1, -1, -1},
{9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7, -1},
{3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10, -1},
{1, 10, 2, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{4, 9, 1, 4, 1, 7, 7, 1, 3, -1, -1, -1, -1, -1, -1, -1},
{4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1, -1, -1, -1, -1},
{4, 0, 3, 7, 4, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{9, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{3, 0, 9, 3, 9, 11, 11, 9, 10, -1, -1, -1, -1, -1, -1, -1},
{0, 1, 10, 0, 10, 8, 8, 10, 11, -1, -1, -1, -1, -1, -1, -1},
{3, 1, 10, 11, 3, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{1, 2, 11, 1, 11, 9, 9, 11, 8, -1, -1, -1, -1, -1, -1, -1},
{3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9, -1, -1, -1, -1},
{0, 2, 11, 8, 0, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{2, 3, 8, 2, 8, 10, 10, 8, 9, -1, -1, -1, -1, -1, -1, -1},
{9, 10, 2, 0, 9, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8, -1, -1, -1, -1},
{1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{1, 3, 8, 9, 1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}
};
/// <summary>
/// The local offsets of the 8 corners of a single voxel cube.
/// </summary>
public static readonly Vector3[] CornerOffsets = new Vector3[8] {
new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 1), new Vector3(0, 0, 1),
new Vector3(0, 1, 0), new Vector3(1, 1, 0), new Vector3(1, 1, 1), new Vector3(0, 1, 1)
};
/// <summary>
/// Defines which two corners make up each of the 12 edges.
/// </summary>
public static readonly int[,] EdgeConnections = new int[12, 2] {
{0,1}, {1,2}, {2,3}, {3,0}, // Bottom face edges
{4,5}, {5,6}, {6,7}, {7,4}, // Top face edges
{0,4}, {1,5}, {2,6}, {3,7} // Vertical pillars
};
/// <summary>
/// Maps block IDs to vertex colors for the generated mesh. You can customize this to match your BiomePalette or block types!
/// </summary>
/// <param name="blockID"></param>
/// <returns></returns>
private static Color GetBlockColor(byte blockID)
{
// These map your BiomePalette selections to actual visual colors!
if (blockID == BlockRegistry.SAND) return new Color(0.8f, 0.7f, 0.4f);
if (blockID == BlockRegistry.SNOW) return new Color(0.9f, 0.9f, 0.9f);
if (blockID == BlockRegistry.STONE) return new Color(0.4f, 0.4f, 0.4f);
if (blockID == BlockRegistry.WASTELAND_DIRT) return new Color(0.35f, 0.25f, 0.25f);
if (blockID == BlockRegistry.ASPHALT) return new Color(0.15f, 0.15f, 0.15f);
// Grass variations
if (blockID == BlockRegistry.GRASS_JUNGLE) return new Color(0.1f, 0.4f, 0.1f);
if (blockID == BlockRegistry.GRASS_PARADISE) return new Color(0.4f, 0.8f, 0.4f);
if (blockID == BlockRegistry.GRASS_TROPICAL) return new Color(0.2f, 0.6f, 0.2f);
// Default Dirt
return new Color(0.4f, 0.3f, 0.15f);
}
/// <summary>
/// A deterministic, integer-based key for our Dictionary welder.
/// </summary>
private struct EdgeKey : IEquatable<EdgeKey>
{
public Vector3I P1, P2;
public EdgeKey(Vector3I p1, Vector3I p2)
{
// Always order them from lowest to highest so neighboring chunks generate the exact same key!
if (p1.X < p2.X || (p1.X == p2.X && p1.Y < p2.Y) || (p1.X == p2.X && p1.Y == p2.Y && p1.Z < p2.Z)) {
P1 = p1; P2 = p2;
} else {
P1 = p2; P2 = p1;
}
}
public bool Equals(EdgeKey other) => P1.Equals(other.P1) && P2.Equals(other.P2);
public override int GetHashCode() => HashCode.Combine(P1, P2);
}
public static Vector3 VertexInterp(float isolevel, Vector3 p1, Vector3 p2, float valp1, float valp2)
{
if (Mathf.Abs(isolevel - valp1) < 0.00001f) return p1;
if (Mathf.Abs(isolevel - valp2) < 0.00001f) return p2;
if (Mathf.Abs(valp1 - valp2) < 0.00001f) return p1;
float mu = (isolevel - valp1) / (valp2 - valp1);
return new Vector3(
p1.X + mu * (p2.X - p1.X),
p1.Y + mu * (p2.Y - p1.Y),
p1.Z + mu * (p2.Z - p1.Z)
);
}
/// <summary>
/// Calculates the exact 3D slope (gradient) of the terrain at a specific grid coordinate.
/// </summary>
public static Vector3 GetDensityGradient(float[,,] field, int x, int y, int z)
{
// Clamp to valid array indices so we don't crash when checking the chunk borders
int x0 = Math.Max(0, x - 1), x1 = Math.Min(field.GetLength(0) - 1, x + 1);
int y0 = Math.Max(0, y - 1), y1 = Math.Min(field.GetLength(1) - 1, y + 1);
int z0 = Math.Max(0, z - 1), z1 = Math.Min(field.GetLength(2) - 1, z + 1);
// Calculate the difference in density across the X, Y, and Z axes
float gx = field[x1, y, z] - field[x0, y, z];
float gy = field[x, y1, z] - field[x, y0, z];
float gz = field[x, y, z1] - field[x, y, z0];
return new Vector3(gx, gy, gz).Normalized();
}
public static ArrayMesh GenerateMesh(float[,,] scalarField, byte[,,] blockIDs, float isolevel = 0.0f, float voxelScale = 1.0f)
{
var st = new SurfaceTool();
st.Begin(Mesh.PrimitiveType.Triangles);
st.SetSmoothGroup(1);
int width = scalarField.GetLength(0) - 1;
int height = scalarField.GetLength(1) - 1;
int depth = scalarField.GetLength(2) - 1;
float[] cornerDensities = new float[8];
int[] edgeVertexIndices = new int[12]; // We now store the integer index, not the raw Vector3!
// The bulletproof integer welder
Dictionary<EdgeKey, int> vertexCache = new Dictionary<EdgeKey, int>();
int currentIndex = 0;
int totalIndicesDrawn = 0; // For our diagnostic ratio
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
for (int z = 0; z < depth; z++)
{
cornerDensities[0] = scalarField[x, y, z];
cornerDensities[1] = scalarField[x + 1, y, z];
cornerDensities[2] = scalarField[x + 1, y, z + 1];
cornerDensities[3] = scalarField[x, y, z + 1];
cornerDensities[4] = scalarField[x, y + 1, z];
cornerDensities[5] = scalarField[x + 1, y + 1, z];
cornerDensities[6] = scalarField[x + 1, y + 1, z + 1];
cornerDensities[7] = scalarField[x, y + 1, z + 1];
int cubeIndex = 0;
if (cornerDensities[0] < isolevel) cubeIndex |= 1;
if (cornerDensities[1] < isolevel) cubeIndex |= 2;
if (cornerDensities[2] < isolevel) cubeIndex |= 4;
if (cornerDensities[3] < isolevel) cubeIndex |= 8;
if (cornerDensities[4] < isolevel) cubeIndex |= 16;
if (cornerDensities[5] < isolevel) cubeIndex |= 32;
if (cornerDensities[6] < isolevel) cubeIndex |= 64;
if (cornerDensities[7] < isolevel) cubeIndex |= 128;
if (EdgeTable[cubeIndex] == 0) continue;
Vector3 basePos = new Vector3(x, y, z) * voxelScale;
// Calculate the 12 edges
for (int i = 0; i < 12; i++)
{
if ((EdgeTable[cubeIndex] & (1 << i)) != 0)
{
int cornerA = EdgeConnections[i, 0];
int cornerB = EdgeConnections[i, 1];
// Create the flawless integer key
Vector3I gridPosA = new Vector3I(x + (int)CornerOffsets[cornerA].X, y + (int)CornerOffsets[cornerA].Y, z + (int)CornerOffsets[cornerA].Z);
Vector3I gridPosB = new Vector3I(x + (int)CornerOffsets[cornerB].X, y + (int)CornerOffsets[cornerB].Y, z + (int)CornerOffsets[cornerB].Z);
EdgeKey key = new EdgeKey(gridPosA, gridPosB);
// If this edge hasn't been drawn by a neighbor yet, calculate it and cache it!
if (!vertexCache.TryGetValue(key, out int vertIdx))
{
// 1. Calculate precise position
Vector3 exactPos = basePos + VertexInterp(isolevel, CornerOffsets[cornerA] * voxelScale, CornerOffsets[cornerB] * voxelScale, cornerDensities[cornerA], cornerDensities[cornerB]);
// 2. ANALYTICAL NORMALS: Calculate the exact 3D slope at both corners
Vector3 gradA = GetDensityGradient(scalarField, x + (int)CornerOffsets[cornerA].X, y + (int)CornerOffsets[cornerA].Y, z + (int)CornerOffsets[cornerA].Z);
Vector3 gradB = GetDensityGradient(scalarField, x + (int)CornerOffsets[cornerB].X, y + (int)CornerOffsets[cornerB].Y, z + (int)CornerOffsets[cornerB].Z);
// 3. Interpolate the normal using the exact same fraction used for the position
float valA = cornerDensities[cornerA];
float valB = cornerDensities[cornerB];
float mu = (isolevel - valA) / (valB - valA);
Vector3 exactNormal = (gradA + mu * (gradB - gradA)).Normalized();
// 4. THE COLOR FIX: Grab the BlockID from CornerA and paint the vertex!
// (We just reuse the gridPosA we already declared above the if-statement!)
byte blockID = blockIDs[gridPosA.X, gridPosA.Y, gridPosA.Z];
st.SetColor(GetBlockColor(blockID));
// 5. Feed the perfect normal AND the position to Godot!
// (SetColor and SetNormal MUST be called immediately before AddVertex)
st.SetNormal(exactNormal);
st.AddVertex(exactPos);
// 6. Cache it so neighbors don't redraw it!
vertIdx = currentIndex++;
vertexCache[key] = vertIdx;
}
// Save the index for this specific cube to use in the TriTable
edgeVertexIndices[i] = vertIdx;
}
}
// Connect the cached indices!
for (int i = 0; TriTable[cubeIndex, i] != -1; i += 3)
{
st.AddIndex(edgeVertexIndices[TriTable[cubeIndex, i + 2]]);
st.AddIndex(edgeVertexIndices[TriTable[cubeIndex, i + 1]]);
st.AddIndex(edgeVertexIndices[TriTable[cubeIndex, i]]);
totalIndicesDrawn += 3;
}
}
}
}
// Diagnostic Print
// GD.Print($"[MarchingCubes] Vertices: {currentIndex}, Indices: {totalIndicesDrawn}, Ratio: {(float)totalIndicesDrawn / Math.Max(1, currentIndex):F2}");
return st.Commit();
}
}
}

View file

@ -0,0 +1 @@
uid://4p2co3ctt0is

46
Core/Scripts/README.md Normal file
View file

@ -0,0 +1,46 @@
# 📜 Core Scripts Directory - Isla Apocalypse
## Overview
The `/Core/Scripts` directory is the dedicated code repository for all pure C# data structures, universal enumerations, and static mathematical utilities. Everything in this folder is compiled into the shared assembly used by both the authoritative `/Server` and the rendering `/Client`.
**Core Philosophy: "Pure Data, Zero State."**
Scripts in this folder define *what* things are and *how* to calculate them, but they never remember *current* game events. They do not track who is online, what chunks are loaded, or what time of day it is. They are stateless, universal blueprints.
---
## 🧩 Current Systems (Phase 2)
### 1. The Voxel Palette System
* **`BlockData.cs`**: The fundamental `struct` defining the properties of a single voxel (ID, Name, IsSolid, BaseColor).
* **`BlockRegistry.cs`**: The static master dictionary. Contains the hardcoded IDs for every block in the game (Air, Bedrock, Dirt, Asphalt) and provides a safe lookup method (`GetBlock`) for both the mesher and the physics engine.
* **`BiomePalette.cs`**: The translation layer between 2D and 3D. Contains the logic that dictates the vertical stacking of blocks based on biome and depth (e.g., ensuring mountains have stone beneath snow, and roads are topped with asphalt).
### 2. Universal Identifiers
* **`Enums.cs`**: The master repository for global enums (`Biome`, `TownTier`, `MapHalf`). By keeping these here, the offline Map Generator, the Server, and the Client are guaranteed to use the exact same integer values for biomes and POIs.
---
## 🚀 Future Roadmap & Planned Additions (Phase 2 & 3)
As we build the 3D Chunk Manager and multiplayer networking, this folder will expand to include:
### 3. The Blueprint Parser
* **`MapDataParser.cs`** *(Next Step)*: A static utility to open and decode the `MapData_Seed_[X].dat` binary file into usable C# arrays for the Server to ingest.
### 4. Chunk Data Structures
* **`ChunkData.cs`**: The raw 3D array (`byte[,,]`) that holds the voxel IDs for a specific 16x16x64 area.
* *Note:* This structure will include the `IsPlayerProtected` boolean flag to support the "Land Claim" delta-backup system, preventing player bases from being wiped out by chunk corruption.
### 5. Math & Coordinate Utilities
* **`VoxelMath.cs`**: Static helper functions for translating massive 3D World Coordinates into local Chunk Coordinates (e.g., finding out which specific chunk file to load when a player walks to X: 5000, Z: -200).
### 6. Network Packet Definitions
* Structs that define the exact byte layout of multiplayer messages (e.g., `PlayerDigPacket`, `ChatPacket`) to ensure precise Server/Client synchronization.
---
## ⚠️ Directory Rules & Best Practices
1. **No Godot Node Inheritance:** Scripts here should rarely, if ever, inherit from `Node` or `Node3D`. They are pure C# classes, structs, or static utilities.
2. **No `_Process` or `_Ready`:** Because these are not attached to active objects in the game world, they do not use Godot's frame-by-frame loop functions.
3. **Strictly Independent:** A script in `/Core/Scripts` cannot reference anything in `/Client` or `/Server`. The dependency flows one way: the Client and Server look *in* to the Core; the Core never looks *out*.

49
Data/README.md Normal file
View file

@ -0,0 +1,49 @@
# 🗄️ Data Directory - Isla Apocalypse
## Overview
The `/Data` directory is the static repository for all generated world files, visual previews, and configuration structures. It acts as the handoff point between the offline `/Tools` (which write the data) and the live `/Server` (which reads the data).
**Core Philosophy: "The Immutable Source of Truth."**
Files stored in this primary directory are considered *Base Blueprints*. They are the starting point of the world before any player interacts with it. The Game Client should almost never read from this folder directly; it receives this data via the Server.
---
## 📦 Current Contents (Phase 1)
### 1. Binary Blueprints (`.dat`)
* **Format:** Custom serialized binary (`MapData_Seed_[X].dat`).
* **Purpose:** Contains the highly compressed, raw mathematical output of the 2D Generator (Map Size, Height floats, Biome IDs, POI coordinates, Highway vectors).
* **Usage:** Read by the Server's Chunk Manager at startup to construct the untouched 3D voxel world.
### 2. Map Previews (`.png`)
* **Format:** 1024x1024 RGBA Images (`Map_Seed_[X]_[Time].png`).
* **Purpose:** High-resolution visual snapshots of the generated seeds.
* **Usage:** Used by the developer/server admin to visually review and select the best island layout before spinning up the 3D server.
---
## 🚀 Future Roadmap & Planned Additions
As the game expands, this folder will be subdivided to organize different types of static game data:
### 3. /Prefabs (Planned - Phase 2/3)
* **Format:** `.json` or custom `.prefab` binaries.
* **Purpose:** 3D building templates (e.g., `GasStation_Tier3.json`) created by the Prefab Editor. The Server uses these files to spawn structures at the coordinates dictated by the master `.dat` blueprint.
### 4. /Configs (Planned - Phase 3)
* **Format:** `.json` or `.cfg`.
* **Purpose:** Human-readable configuration files for game balance.
* `LootTables.json`: Defines what items spawn in which containers based on POI Tier.
* `VoxelPalette.json`: Maps the 2D Biome IDs to 3D Voxel types.
* `SpawnRates.json`: Defines zombie/animal spawn limits and Blood Moon horde scaling.
---
## ⚠️ Directory Rules & Best Practices
1. **NO Scripts or Code:** This directory must strictly contain data files (`.dat`, `.png`, `.json`, `.csv`). It contains zero executable logic.
2. **Live Saves vs. Base Data:** * This folder holds the **Base Game Data**.
* *Live Player Saves* and *Terrain Deformation Diff Files* should generally be saved to Godot's safe `user://` path on the host machine, NOT in this project folder. This prevents live server saves from accidentally being bundled into the game's base code.
3. **Version Control (Git) Warnings:**
* `.dat` and `.png` files can become very large.
* **Rule:** Only commit the "Official Release" seed files to your repository. Use a `.gitignore` file to ignore random test generations so you don't bloat your project repository with gigabytes of discarded test maps.

28
README.md Normal file
View file

@ -0,0 +1,28 @@
# IslaApocalypse - Voxel Engine Architecture
**Current Version:** v0.0.1 (Phase 2 - Voxel Engine Online, v0.0.x Prototyping / Tech Demo)
**Engine:** Godot 4 (C#)
## Project Overview
IslaApocalypse is a procedurally generated, multiplayer-ready survival/driving game. The engine uses a two-phase generation system: first building a massive 4096x4096 2D topographical blueprint, and then dynamically translating that data into a 3D Marching Cubes voxel world at runtime.
## Development State
* **Phase 1: 2D Blueprint Engine** -> **[COMPLETE]**
* **Phase 2: 3D Voxel Engine** -> **[IN PROGRESS - Basic Rendering Online]**
### Current Capabilities (What works right now):
1. **The 2D Blueprint (`MapGenerator.cs`):** Generates a 4096x4096 map using `FastNoiseLite`. It features dynamic sea levels, a massive impact crater, tiered city placements (Capitol, Hubs, Villages), and a heavily optimized A* pathfinding network (Highways, Branch Roads, Trails) that utilizes Chaikin's smoothing algorithm.
2. **Binary Serialization:** Exports the entire 16-million pixel array and path vectors into a lightweight, byte-aligned `.dat` file.
3. **The Data Bridge (`MapDataParser.cs`):** Seamlessly deserializes the `.dat` file back into RAM as a `WorldBlueprint` for the server to read.
4. **Authoritative Chunk Management (`ServerChunkManager.cs`):** Dynamically locates the Capitol City, converts pixel coordinates to 3D chunk coordinates, and generates a grid of `ChunkData` objects (32x32x256 meters) around the player.
5. **Marching Cubes Rendering (`MarchingCubes.cs`):** Translates 2D heightmaps into 3D geometry. Features analytical normals for smooth lighting and dynamically paints Vertex Colors based on the 2D `BiomeMap` (Wasteland, Sand, Grass, Stone).
## Directory Architecture
* `/Tools/`: Contains the v1.0 2D Map Generator and preview scenes. Run `MapCaptureTool.tscn` to generate a new world seed.
* `/Core/`: The math and data foundation. Contains the Binary Parser, Marching Cubes algorithm, Biome Palettes, and `ChunkData` structs.
* `/Server/`: The Authoritative Server logic. Manages chunk loading, calculates voxel densities, and handles chunk culling.
* `/Client/`: The visual layer. Takes raw `ChunkData` and translates it into Godot `MeshInstance3D` objects with vertex-colored materials.
## Next Immediate Steps (Phase 2, Step 4)
* **Road Carving:** Inject A* road vector data into the `ServerChunkManager` to override the natural heightmap, physically flattening the 3D terrain and painting Asphalt blocks to create drivable highways through the mountains.
* **Chunk Seams:** Implement normal-blending across chunk borders to remove grid-line lighting artifacts.

49
Resources/README.md Normal file
View file

@ -0,0 +1,49 @@
# 📦 Resources Directory - Isla Apocalypse
## Overview
The `/Resources` directory acts as the game's central library for all static assets and Godot-specific data containers (primarily `.tres` files, 3D models, textures, and audio).
**Core Philosophy: "The Reusable Building Blocks."**
If the Server is the brain and the Client is the eyes, Resources are the memories. They define the *properties* of things in the world. If there are 50 zombies on screen, you don't want 50 copies of their texture and max health data in your RAM. Instead, all 50 zombies reference a single `ZombieBase.tres` resource.
---
## 🧩 Core Systems (Phase 2 & 3)
### 1. Voxel & Block Definitions
* **Format:** Custom `.tres` files based on a `BlockData` C# script (located in `/Core`).
* **Function:** Defines the properties of every voxel in the game.
* **Examples:** `Dirt.tres` (Texture: brown_dirt.png, Durability: 100, StepSound: dirt_crunch.ogg), `Asphalt.tres` (Texture: road.png, Durability: 500, StepSound: hard_step.ogg). Both the Client and Server read these to know how a block looks and behaves.
### 2. Item & Inventory Data
* **Format:** Custom `.tres` files for `ItemData`.
* **Function:** Defines every item a player can hold, craft, or drop.
* **Examples:** `StoneAxe.tres` (Icon: axe.png, Damage: 15, BlockDamage: 40, MaxStack: 1). The Server uses this to calculate damage; the Client uses this to draw the icon in your hotbar.
### 3. Audio & Visual Assets
* **Materials & Shaders:** The `.material` and `.gdshader` files used by the Client's chunk mesher (e.g., `TerrainMaterial.tres`).
* **3D Models:** The `.glb` or `.gltf` files for weapons, dropped items, and entities.
* **Audio Streams:** The `.wav` or `.ogg` files for ambient wind, UI clicks, and zombie attacks.
### 4. UI Themes & Fonts
* Godot `Theme` resources that dictate the colors, fonts, and border styles of the entire user interface, ensuring the inventory menus match the main menu.
---
## 🚀 Directory Organization Strategy
To prevent this folder from turning into a massive junk drawer, it must be strictly subdivided:
* `/Resources/Blocks/`
* `/Resources/Items/`
* `/Resources/Models/`
* `/Resources/Audio/`
* `/Resources/UI/`
---
## ⚠️ Directory Rules & Best Practices
1. **Text Over Binary:** Always save custom Godot resources as `.tres` (Text Resource) rather than `.res` (Binary Resource). Text resources can be read by version control (Git), allowing you to see exactly when someone changed the damage of a Stone Axe from 15 to 20.
2. **Stateless Data Only:** A Resource should never track live game state. For example, `Pistol.tres` holds the *Max Ammo* capacity (15), but it should NEVER hold the *Current Ammo* (7). Current ammo is a live state tracked by the Server.
3. **Shared Access:** Because Resources are just data, they are completely safe to be referenced by both the `/Server` and the `/Client`.

17
Scenes/Main.tscn Normal file
View file

@ -0,0 +1,17 @@
[gd_scene format=3 uid="uid://pkacw81eqg5w"]
[ext_resource type="Script" uid="uid://c1dxqbgohxr6k" path="res://Server/Scripts/ServerChunkManager.cs" id="1_r150o"]
[sub_resource type="Environment" id="Environment_r150o"]
ambient_light_color = Color(0.37245482, 0.5677467, 1, 1)
[node name="World" type="Node3D" unique_id=412138339]
script = ExtResource("1_r150o")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1569534216]
[node name="Camera3D" type="Camera3D" parent="." unique_id=1064925728]
transform = Transform3D(1, 0, 0, 0, 0.49999997, -0.86602545, 0, 0.86602545, 0.49999997, 2559.9224, 640.90497, -171.46881)
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1968388012]
environment = SubResource("Environment_r150o")

View file

@ -0,0 +1,13 @@
[gd_scene format=3 uid="uid://cu28aj74lxhjn"]
[ext_resource type="PackedScene" uid="uid://croq62eckpnvx" path="res://Tools/Scenes/MapPreview.tscn" id="1_gvjeg"]
[node name="MapCaptureTool" type="SubViewportContainer" unique_id=69177420]
custom_minimum_size = Vector2(4096, 4096)
offset_right = 40.0
offset_bottom = 40.0
[node name="SubViewport" type="SubViewport" parent="." unique_id=1249553404]
size = Vector2i(4096, 4096)
[node name="MapPreview" parent="SubViewport" unique_id=971733442 instance=ExtResource("1_gvjeg")]

37
Scenes/README.md Normal file
View file

@ -0,0 +1,37 @@
# 🌍 /Scenes - The Playable Game
## Overview
This directory contains the core runtime scenes for *Isla Apocalypse*. Unlike the `/Tools` directory, the scenes here are exactly what the end-user (or the Dedicated Server) will run.
**Core Philosophy:** These scenes do **not** generate the world from scratch. They strictly load and interpret the immutable `MapData.dat` blueprint provided by the developers. This ensures lightning-fast server boot times and complete parity between the server and the clients.
---
## 🏗️ Core Scenes
### `Main.tscn` (The Entry Point)
**Status:** In Transition (Prepping for 1:1 Scale Ingestion)
This is the master root scene. Currently, it acts as the Dedicated Server environment, responsible for reading the 2D blueprint and translating it into a physical 3D voxel world using the `ServerChunkManager`.
#### ⚔️ The Great 4096 Struggle (The 3D Ramifications)
Our decision to upgrade the master blueprint from 1024x1024 to a massive 4096x4096 (1:1 scale) fundamentally changed how `Main.tscn` must operate.
While the 2D generator struggled with math and UI layouts, the 3D server must now deal with **Memory Management**.
* **The Scale Shift:** In the old 1024 version, 1 data pixel equaled 4 meters of 3D space. Now, 1 pixel = 1 meter. This perfectly accommodates human-scale base building (like *7 Days to Die*), but it means the total data footprint of the world is 16 times larger.
* **The Chunking Mandate:** `Main.tscn` can no longer afford to load the entire map into RAM at startup. The `ServerChunkManager` must operate strictly on a localized, dynamic grid—only loading the 1-meter chunks immediately surrounding active players.
#### 🛡️ The Arsenal is Ready
Despite the massive scale increase, `Main.tscn` is structurally prepared for the 4096 blueprint thanks to heavy engine optimizations built during Phase 1:
1. **The EdgeKey Welder:** Completely eliminates floating-point drift and mesh seams across the new high-density chunk borders.
2. **Analytical Normals:** Bypasses Godot's slow native normal calculation, using mathematical gradients to light the massive 1:1 voxel world instantly.
3. **The +1 Border Fix:** Chunk arrays are padded by exactly 1 unit to flawlessly sample neighbor data without requiring expensive cross-chunk blurring.
---
## 🚀 The Immediate Roadmap
When Phase 2 resumes, `Main.tscn` will undergo the following upgrades to ingest the new 4096 map:
1. **Re-syncing the Parser:** Uncommenting the Logistical Network logic in `MapDataParser.cs` so the server can read the newly fixed roads and towns.
2. **Dynamic Chunk Loading:** Scrapping the hardcoded `10x10` test grid in favor of a proximity-based chunk loader.
3. **Terrain Flattening:** Implementing logic for the voxel engine to read the road and town data and automatically flatten the 3D terrain beneath them.

22
Server/README.md Normal file
View file

@ -0,0 +1,22 @@
# Server Module - IslaApocalypse
**Phase 2 Status:** Active (Authoritative Chunk Management Online)
## Overview
The `Server` directory contains the authoritative logic for the 3D Voxel Engine. It acts as the bridge between the static 2D `WorldBlueprint` residing in RAM and the physical 3D environment generated around the player. This architecture ensures that in a future multiplayer environment, the server dictates the terrain and sends chunk data to the clients.
## Key Architecture & Components
### 1. The Authoritative Node (`ServerChunkManager.cs`)
Attached to the root of the `Main.tscn` world scene, this script manages the lifecycle of the entire 3D environment.
* **Initialization:** Upon boot, it reads the `WorldBlueprint` and dynamically locates the exact X/Y pixel coordinates of the `Capitol` city to use as the initial world spawn point.
* **Coordinate Translation:** Converts 2D pixel coordinates into 3D chunk grid coordinates (e.g., Map Pixel [2048, 2048] becomes Chunk [64, 64]).
* **Memory Management:** Maintains an `_activeChunks` dictionary to track which chunks are currently loaded in RAM, preventing duplicate generation and allowing for future chunk culling/unloading when the player moves away.
### 2. Terrain Math & Generation
* **True Vertical Scaling:** Height calculation directly multiplies the raw `FastNoiseLite` float (0.0 to 1.0) against `Constants.CHUNK_HEIGHT` (256 meters). This avoids heightmap compression and allows for towering, AAA-scale mountain cliffs.
* **Density Calculation:** Uses exact surface interpolation to calculate the signed 3D distance of every voxel relative to the surface. This prevents terrain collapsing on horizontal edges and creates mathematically perfect slopes for the Marching Cubes algorithm.
* **Biome Injection:** Samples the `BiomeMap` at the exact X/Z world coordinate to assign a specific `BlockID` (via `BiomePalette`) to every voxel in the chunk.
## Next Immediate Steps (Phase 2, Step 4)
* **A* Road Carving:** The Server Chunk Manager needs to be updated to cross-reference the `Highways` and `BranchRoads` vectors from the blueprint. If a chunk contains a road, the server must mathematically flatten the `exactSurfaceY` and override the `BlockID` to `ASPHALT`.

View file

@ -0,0 +1,225 @@
using Godot;
using System.Collections.Generic;
using IslaApocalypse.Core;
namespace IslaApocalypse.Server
{
public partial class ServerChunkManager : Node
{
private WorldBlueprint _blueprint;
private Dictionary<Vector2I, ChunkData> _activeChunks = new Dictionary<Vector2I, ChunkData>();
public int chunkSize = 24;
public override void _Ready()
{
// 1. LOAD CONFIGURATION and the seed-based blueprint data from the MapDataParser!
ConfigManager.LoadConfig();
chunkSize = ConfigManager.ChunkRadius; // Update chunk radius from config file
_blueprint = MapDataParser.LoadMapData(ConfigManager.WorldSeed.ToString());
if (_blueprint != null)
{
GD.Print("[Server] Blueprint loaded. Locating Capitol City...");
// 2. Find the Capitol in the parsed data
Vector2 capitolPos = new Vector2(2048, 2048); // Safe fallback center
foreach (var town in _blueprint.Towns) {
if (town.Tier == TownTier.Capitol) {
capitolPos = town.Position;
break;
}
}
// Old original png map coords
// Vector2 capitolPos = new Vector2(2405, 3296); // hardcoded for now since we know exactly where it is in this seed, which is the "paradise" biome hub
GD.Print($"[Server] Capitol found at {capitolPos}. Generating chunks...");
// 3. Convert pixel coordinates to Chunk coordinates
int capitolChunkX = (int)(capitolPos.X / Constants.CHUNK_SIZE_X);
int capitolChunkZ = (int)(capitolPos.Y / Constants.CHUNK_SIZE_Z);
int radius = ConfigManager.ChunkRadius;
for (int x = capitolChunkX - radius; x < capitolChunkX + radius; x++)
{
for (int z = capitolChunkZ - radius; z < capitolChunkZ + radius; z++)
{
GenerateChunk(new Vector2I(x, z));
}
}
// 5. Teleport the Camera to look down at our creation!
Camera3D cam = GetNodeOrNull<Camera3D>("Camera3D");
if (cam != null)
{
// Put the camera 120 meters in the air above the Capitol
cam.GlobalPosition = new Vector3(capitolPos.X, 120f, capitolPos.Y);
// Point it straight down at the ground
cam.LookAt(new Vector3(capitolPos.X, 0, capitolPos.Y));
}
}
}
public void GenerateChunk(Vector2I chunkCoord)
{
if (_activeChunks.ContainsKey(chunkCoord)) return;
ChunkData newChunk = new ChunkData(chunkCoord);
int startX = chunkCoord.X * Constants.CHUNK_SIZE_X;
int startZ = chunkCoord.Y * Constants.CHUNK_SIZE_Z;
// --- NEW: SPATIAL CULLING FOR ROADS ---
// Create a bounding box for this chunk, plus a 15-meter padding to account for the road's dirt shoulders
Rect2 chunkBounds = new Rect2(startX - 15, startZ - 15, Constants.CHUNK_SIZE_X + 30, Constants.CHUNK_SIZE_Z + 30);
List<Vector2[]> localRoadSegments = new List<Vector2[]>();
// Filter Highways: Only save the segments that actually cross this specific chunk!
foreach (var highway in _blueprint.Highways) {
for (int i = 0; i < highway.Length - 1; i++) {
Vector2 a = highway[i];
Vector2 b = highway[i+1];
Rect2 segBounds = new Rect2(Mathf.Min(a.X, b.X), Mathf.Min(a.Y, b.Y), Mathf.Abs(b.X - a.X), Mathf.Abs(b.Y - a.Y));
if (chunkBounds.Intersects(segBounds.Grow(1.0f))) {
localRoadSegments.Add(new Vector2[] { a, b });
}
}
}
// Filter Branch Roads
foreach (var branch in _blueprint.BranchRoads) {
for (int i = 0; i < branch.Length - 1; i++) {
Vector2 a = branch[i];
Vector2 b = branch[i+1];
Rect2 segBounds = new Rect2(Mathf.Min(a.X, b.X), Mathf.Min(a.Y, b.Y), Mathf.Abs(b.X - a.X), Mathf.Abs(b.Y - a.Y));
if (chunkBounds.Intersects(segBounds.Grow(1.0f))) {
localRoadSegments.Add(new Vector2[] { a, b });
}
}
}
// ---------------------------------------
for (int x = 0; x <= Constants.CHUNK_SIZE_X; x++)
{
for (int z = 0; z <= Constants.CHUNK_SIZE_Z; z++)
{
int globalX = startX + x;
int globalZ = startZ + z;
if (globalX >= _blueprint.MapSize || globalZ >= _blueprint.MapSize || globalX < 0 || globalZ < 0)
continue;
float GetExactSurface(int gX, int gZ, out bool isRoad)
{
isRoad = false;
if (gX >= _blueprint.MapSize) gX = _blueprint.MapSize - 1;
if (gZ >= _blueprint.MapSize) gZ = _blueprint.MapSize - 1;
float raw = _blueprint.HeightMap[gX, gZ];
float baseHeight = Mathf.Clamp(raw * (Constants.CHUNK_HEIGHT - 5), 2.0f, Constants.CHUNK_HEIGHT - 2.0f);
// If no roads are in this chunk, skip the heavy math entirely!
if (localRoadSegments.Count == 0) return baseHeight;
float finalHeight = baseHeight;
float minDist = 9999f;
Vector2 currentPos = new Vector2(gX, gZ);
float roadRadius = 4.0f; // The flat asphalt part (8 meters total width)
float shoulderRadius = 12.0f; // The sloped dirt/rock carving into the mountain
float closestRoadElevation = baseHeight;
foreach (var seg in localRoadSegments)
{
float dist = DistanceToLineSegment(currentPos, seg[0], seg[1]);
if (dist < minDist)
{
minDist = dist;
if (dist <= shoulderRadius)
{
// Sample the heightmap precisely at the center of the road segment
// so the whole road stays at a uniform elevation, ignoring the sloped mountain under it.
Vector2 midPoint = (seg[0] + seg[1]) / 2.0f;
float roadRaw = _blueprint.HeightMap[(int)midPoint.X, (int)midPoint.Y];
closestRoadElevation = Mathf.Clamp(roadRaw * (Constants.CHUNK_HEIGHT - 5), 2.0f, Constants.CHUNK_HEIGHT - 2.0f);
}
}
}
// THE BULLDOZER: Carve the terrain
if (minDist <= roadRadius) {
finalHeight = closestRoadElevation; // Flatten it completely!
isRoad = true;
} else if (minDist <= shoulderRadius) {
// Smoothly interpolate from the flat road up/down to the natural mountain height
float t = (minDist - roadRadius) / (shoulderRadius - roadRadius);
t = t * t * (3f - 2f * t); // SmoothStep equation for a beautiful curved slope
finalHeight = Mathf.Lerp(closestRoadElevation, baseHeight, t);
}
return finalHeight;
}
// Pass the boolean out for the main voxel so we can paint it!
bool isMainRoad = false;
float exactSurfaceY = GetExactSurface(globalX, globalZ, out isMainRoad);
// We discard the 'out' variable for the normals using an underscore '_'
float surfaceRight = GetExactSurface(globalX + 1, globalZ, out _);
float surfaceFwd = GetExactSurface(globalX, globalZ + 1, out _);
Biome columnBiome = _blueprint.BiomeMap[globalX, globalZ];
float hRight = surfaceRight - exactSurfaceY;
float hFwd = surfaceFwd - exactSurfaceY;
float slopeX = hRight / Constants.VOXEL_SCALE;
float slopeZ = hFwd / Constants.VOXEL_SCALE;
float len = Mathf.Sqrt(slopeX * slopeX + 1.0f + slopeZ * slopeZ);
for (int y = 0; y <= Constants.CHUNK_HEIGHT; y++)
{
float verticalDist = y - exactSurfaceY;
float density = verticalDist / len;
newChunk.Densities[x, y, z] = density;
int blockY = Mathf.RoundToInt(exactSurfaceY);
// THE PAINT: Pass isMainRoad into the Biome Palette!
newChunk.BlockIDs[x, y, z] = BiomePalette.GetVoxelID(columnBiome, blockY, y, isMainRoad);
}
} // End of Z loop
} // End of X loop
_activeChunks.Add(chunkCoord, newChunk);
var renderer = new IslaApocalypse.Client.ChunkRenderer();
AddChild(renderer);
renderer.RenderChunk(newChunk);
}
/// <summary>
/// Calculates the shortest distance from a point to a line segment defined by v and w.
/// </summary>
private float DistanceToLineSegment(Vector2 point, Vector2 v, Vector2 w)
{
float l2 = v.DistanceSquaredTo(w);
if (l2 == 0) return point.DistanceTo(v); // v == w case
// Consider the line extending the segment, parameterized as v + t (w - v).
// We find projection of point p onto the line.
// It falls where t = [(p-v) . (w-v)] / |w-v|^2
float t = Mathf.Max(0, Mathf.Min(1, (point - v).Dot(w - v) / l2));
// Projection falls on the segment
Vector2 projection = v + t * (w - v);
return point.DistanceTo(projection);
}
}
}

View file

@ -0,0 +1 @@
uid://c1dxqbgohxr6k

6
ServerConfig.json Normal file
View file

@ -0,0 +1,6 @@
{
"WorldSeed": 1063685222,
"MapProfile": "8K",
"TownDensity": "Normal",
"ChunkRadius": 32
}

52
Tools/README.md Normal file
View file

@ -0,0 +1,52 @@
# 🧰 Tools Directory - Isla Apocalypse
## Overview
The `/Tools` directory houses all offline developer utilities, administrative scenes, and generation scripts.
**Core Philosophy:** Nothing in this folder is shipped to the end-user. These tools are used exclusively by the developers or server admins to generate the immutable "Source Data" (blueprints, prefabs, configs) that the `/Server` and `/Client` will later ingest. By keeping this logic completely isolated, we ensure the game client remains lightweight and secure from reverse-engineered world-generation exploits.
---
## 🛠️ Current Tools
### 1. The 2D World Generator (`MapCaptureTool.tscn` & `MapPreview.tscn`)
**Status:** Active / Upgraded to 1:1 Scale Architecture
This tool handles the heavy mathematical lifting of procedural generation. It creates the topographical and logistical foundation of the island before any 3D rendering occurs.
* **Primary Script:** `MapGenerator.cs`
* **Resolution:** 4096 x 4096 pixels (1:1 Scale. 1 Pixel = 1 In-Game Meter / 1 Voxel footprint).
* **Capture Architecture:** Uses a dynamic, C#-driven offscreen `SubViewport` and a custom `MapDrawProxy`. This bypasses Godot's UI layout engine, allowing the generation of massive, high-resolution maps regardless of the developer's physical monitor size.
* **Key Sub-Systems:**
* *Topography:* Uses `FastNoiseLite` and distance-falloff math to guarantee a mainland island shape with cold/mountainous northern regions.
* *Logistics:* A* Pathfinding that generates looping coastal highways, connecting branch roads, and rugged mountain trails. Pathing is dynamically scaled to the map resolution.
* *Zoning:* Biome mapping and hierarchical Town/POI placement based on slope, temperature, and proximity.
**Outputs (Saved to `user://`):**
1. **The Blueprint (`MapData_Seed_[X].dat`):** A heavily compressed binary file containing the map size, heightmap floats, biome enums, road vectors, and POI coordinates. This is the master file read by the Dedicated Server.
2. **The Snapshot (`Map_Seed_[X].png`):** A high-resolution 1:1 visual render of the map layout (including all drawn logistical routes) for admin review and seed selection. Timestamps have been removed to prevent directory bloating.
---
## 🚀 Future Roadmap & Planned Additions
As we move into Phase 2 (3D Voxels) and Phase 3 (Gameplay), this directory will expand to include the following standalone utilities:
### 2. The Prefab Editor (Planned - Phase 3)
A standalone 3D sandbox scene used to build custom Points of Interest (POIs) block-by-block (e.g., houses, bunkers, gas stations).
* **Function:** Allows the developer to build structures visually and define "Loot Container" or "Zombie Spawn" block locations.
* **Output:** Generates lightweight `.prefab` or `.json` files that the Server's Chunk Manager reads to spawn buildings at the coordinates dictated by the `MapData.dat` file.
### 3. Voxel Palette Manager (Planned - Phase 2)
A data-entry tool or inspector script to map 2D Biome Enums to 3D Voxel IDs.
* **Function:** Defines that `Biome.Jungle` at `Height 0.5` should spawn "Voxel ID 12 (Jungle Grass)", while `Height 0.2` should spawn "Voxel ID 4 (Dirt)".
### 4. Loot & Balance Tweaker (Planned - Phase 3)
A simple UI tool to adjust spawn weights and loot tables without having to dig through massive JSON files manually.
* **Output:** Bakes final configuration files for the Server to use when rolling loot table RNG.
---
## ⚠️ Directory Rules
1. **No Client Dependencies:** Scripts in this folder must *never* reference UI, Player Controllers, or Shaders located in the `/Client` folder.
2. **Data Agnostic:** Tools here should output raw data (`.dat`, `.json`, `.png`). They should not directly modify the live server's save state to avoid corruption.

View file

@ -0,0 +1,7 @@
[gd_scene format=3 uid="uid://croq62eckpnvx"]
[ext_resource type="Script" uid="uid://bl6pk71fngm8p" path="res://Tools/Scripts/MapGenerator.cs" id="1_56pfr"]
[node name="MapPreview" type="TextureRect" unique_id=971733442]
custom_minimum_size = Vector2(4096, 4096)
script = ExtResource("1_56pfr")

47
Tools/Scenes/README.md Normal file
View file

@ -0,0 +1,47 @@
# 🎬 /Tools/Scenes - Developer Environments
## Overview
This directory contains the standalone Godot scenes (`.tscn` files) used to execute our offline generation scripts. These scenes act as isolated "sandboxes" for the developers to build the world data.
**CRITICAL RULE:** These scenes must **never** be added to the main game's build export or scene tree. They are run strictly inside the Godot Editor by pressing **F6 (Play Current Scene)**.
---
## 🏗️ Core Scenes
### 1. `MapPreview.tscn`
The original 2D map generation scene.
* **Root Node:** `TextureRect`
* **Attached Script:** `MapGenerator.cs`
* **Function:** Runs the FastNoiseLite math, builds the `_heightMap`, and executes the `_Draw()` commands to visually plot the A* road networks and town circles.
### 2. `MapCaptureTool.tscn` (The Wrapper)
The master scene used to actually execute and capture the map generation.
* **Root Node:** `SubViewportContainer`
* **Child Nodes:** `SubViewport` -> `MapPreview`
* **Function:** Acts as a massive, invisible "virtual monitor" to capture the high-resolution PNG of the map without the Godot UI layout engine interfering.
---
## ⚔️ The Great 4096 Struggle (Beating the UI Engine)
When we upgraded *Isla Apocalypse* from a 1024 to a 4096 (1:1 scale) map, we encountered a massive rendering roadblock. Godot's UI Layout Engine is designed to make things fit on a player's physical monitor.
When we asked `MapPreview` to render a 4096x4096 texture, Godot panicked. It saw that the developer's monitor was only 1080p, so it violently crushed the map down to fit the screen, resulting in exported PNGs that were either 1KB postage stamps or completely black.
#### The "Virtual Monitor" Solution
To bypass the UI engine, we had to build `MapCaptureTool.tscn`.
1. **The Container Trap:** We initially used a `SubViewportContainer` with "Stretch" enabled. This was a trap. It locked the resolution to the editor window. We had to forcefully disable `Stretch` in the C# code.
2. **The `SubViewport`:** We placed `MapPreview` inside a `SubViewport` set explicitly to 4096x4096. This acts as an offscreen virtual monitor that doesn't care about the physical screen size.
3. **The `MapDrawProxy`:** We realized that `Texture.GetImage()` only grabs the base pixels, completely ignoring the colored roads and towns drawn by `_Draw()`. We had to dynamically instantiate a `Control` proxy node to stamp the roads onto the virtual monitor *before* taking the picture.
4. **The Flash Timing:** Generating a 4096 map takes time. We had to implement `await ToSignal(GetTree(), "process_frame");` to let Godot unlock the scene tree, and `await ToSignal(RenderingServer... FramePostDraw)` to force the camera to wait until the GPU physically finished painting the roads before snapping the photo.
---
## 🚀 How to Generate a New World
1. Open `MapCaptureTool.tscn` in the Godot Editor.
2. Ensure you do **not** click the main Play button.
3. Press **F6** (Play Current Scene).
4. The editor will freeze for 10-30 seconds while the `ExportMapData` function writes the binary `.dat` file.
5. Once the console prints the success message, check your `user://` folder for the massive, full-resolution `.png` and the `.dat` blueprint.

View file

@ -0,0 +1,975 @@
using Godot;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using IslaApocalypse.Core;
public partial class MapGenerator : TextureRect
{
public int MapSize = 4096;
[Export] public float FalloffStrength = 1.0f; // [Export] adds to the "inspector" in Godot so we can easily tweak this value!
public struct TownData
{
public Vector2 Position;
public TownTier Tier;
public bool IsHighwayNode;
}
private FastNoiseLite _noise; // The one and only original detailed noise!
private Vector2 _impactCenter;
private float _impactRadius;
private float[,] _heightMap;
private float[,] _tempMap;
private Biome[,] _biomeMap;
private bool[,] _isTrueOcean;
private bool[,] _isMainland;
internal List<TownData> _towns = new List<TownData>();
internal List<Vector2[]> _highwayPaths = new List<Vector2[]>();
internal List<Vector2[]> _branchPaths = new List<Vector2[]>();
internal List<Vector2[]> _ruggedPaths = new List<Vector2[]>();
internal List<Vector2[]> _trailPaths = new List<Vector2[]>();
public override async void _Ready()
{
// 1. LOAD CONFIGURATION
ConfigManager.LoadConfig();
MapSize = ConfigManager.MapSize;
_impactRadius = ConfigManager.CraterRadius;
// LOCK THE GLOBAL RNG TO OUR SEED!
GD.Seed((ulong)ConfigManager.WorldSeed);
// 2. OVERRIDE GODOT INSPECTOR
this.CustomMinimumSize = new Vector2(MapSize, MapSize);
_heightMap = new float[MapSize, MapSize];
_tempMap = new float[MapSize, MapSize];
_biomeMap = new Biome[MapSize, MapSize];
_isTrueOcean = new bool[MapSize, MapSize];
_isMainland = new bool[MapSize, MapSize];
float scaleFactor = MapSize / 1024f; // Equals 4
// --- MASTER SEED & DETAILED NOISE (Restored!) ---
_noise = new FastNoiseLite();
_noise.Seed = ConfigManager.WorldSeed == 0 ? (int)GD.Randi() : ConfigManager.WorldSeed;
_noise.NoiseType = FastNoiseLite.NoiseTypeEnum.Simplex;
_noise.Frequency = 0.004f / scaleFactor;
// THE CRATER FIX: Push it into the ocean (scales via percentage of MapSize!)
// Supposedly! We will have to test this manually on other map sizes to confirm the crater is properly scaled and submerged on the north coast!
float randomX = (float)GD.RandRange(0.35f, 0.65f);
float randomY = (float)GD.RandRange(0.05f, 0.12f);
_impactCenter = new Vector2(MapSize * randomX, MapSize * randomY);
GenerateTopography();
CalculateTrueOcean();
CalculateMainland();
AssignBiomesAndDraw();
GenerateTowns();
// NEW: We await the roads so the engine doesn't freeze!
await GenerateRoadsAsync();
ExportMapData();
SaveMapSnapshot();
}
private async void SaveMapSnapshot()
{
// 1. THE FIX: Wait exactly one frame to let Godot finish _Ready() and unlock the Scene Tree!
await ToSignal(GetTree(), "process_frame");
GD.Print("Starting offscreen map capture...");
// 2. Build the invisible, offscreen 4096 monitor
var offscreenVP = new SubViewport();
offscreenVP.Size = new Vector2I(MapSize, MapSize);
offscreenVP.RenderTargetUpdateMode = SubViewport.UpdateMode.Always;
offscreenVP.TransparentBg = false;
GetTree().Root.AddChild(offscreenVP); // The tree is now unlocked, so this is safe!
// 3. Stamp the raw terrain texture onto it
var bgRect = new TextureRect();
bgRect.Texture = this.Texture;
bgRect.CustomMinimumSize = new Vector2(MapSize, MapSize);
offscreenVP.AddChild(bgRect);
// 4. Attach the proxy to draw the roads and towns on top
var drawProxy = new MapDrawProxy();
drawProxy.Source = this;
drawProxy.CustomMinimumSize = new Vector2(MapSize, MapSize);
offscreenVP.AddChild(drawProxy);
// 5. WAIT FOR THE GPU! (Wait 2 render frames to guarantee the framebuffer is painted)
await ToSignal(RenderingServer.Singleton, RenderingServer.SignalName.FramePostDraw);
await ToSignal(RenderingServer.Singleton, RenderingServer.SignalName.FramePostDraw);
// 6. Pull the image from the invisible monitor and save it!
Image capture = offscreenVP.GetTexture().GetImage();
string seedStr = _noise.Seed.ToString();
string fileName = $"user://Map_Seed_{seedStr}.png";
Error saveResult = capture.SavePng(fileName);
if (saveResult == Error.Ok)
GD.Print($"Map saved successfully to: {ProjectSettings.GlobalizePath(fileName)}");
else
GD.PrintErr($"Failed to save map. Godot Error code: {saveResult}");
// 7. Delete the invisible monitor to free up RAM
offscreenVP.QueueFree();
}
private void ExportMapData()
{
string seedStr = _noise.Seed.ToString();
string filePath = $"user://MapData_Seed_{seedStr}.dat";
using (FileStream stream = File.Open(ProjectSettings.GlobalizePath(filePath), FileMode.Create))
{
using (BinaryWriter writer = new BinaryWriter(stream))
{
// 1. Header & Versioning
writer.Write("ISLA_V1");
writer.Write(MapSize);
// 2. RAW DATA (Height & Temp)
for (int x = 0; x < MapSize; x++) {
for (int y = 0; y < MapSize; y++) {
writer.Write(_heightMap[x, y]);
writer.Write((int)_biomeMap[x, y]);
}
}
// 3. LOGISTICAL ANCHORS (Towns & POIs)
writer.Write(_towns.Count);
foreach (var town in _towns) {
writer.Write(town.Position.X);
writer.Write(town.Position.Y);
writer.Write((int)town.Tier);
}
// 4. ROAD NETWORKS (Exported by Tier for Voxel Painting!)
// Highway (Paved)
writer.Write(_highwayPaths.Count);
foreach (var path in _highwayPaths) {
writer.Write(path.Length);
foreach (Vector2 p in path) { writer.Write(p.X); writer.Write(p.Y); }
}
// Branch (Paved/Gravel)
writer.Write(_branchPaths.Count);
foreach (var path in _branchPaths) {
writer.Write(path.Length);
foreach (Vector2 p in path) { writer.Write(p.X); writer.Write(p.Y); }
}
// Rugged (Dirt)
writer.Write(_ruggedPaths.Count);
foreach (var path in _ruggedPaths) {
writer.Write(path.Length);
foreach (Vector2 p in path) { writer.Write(p.X); writer.Write(p.Y); }
}
// Trails (Faded Dirt)
writer.Write(_trailPaths.Count);
foreach (var path in _trailPaths) {
writer.Write(path.Length);
foreach (Vector2 p in path) { writer.Write(p.X); writer.Write(p.Y); }
}
}
}
GD.Print("Binary Data Exported to: " + ProjectSettings.GlobalizePath(filePath));
}
// --- THE ORIGINAL MAP GENERATION CODE (Restored!) ---
private void GenerateTopography()
{
Vector2 center = new Vector2(MapSize / 2.0f, MapSize / 2.0f);
for (int x = 0; x < MapSize; x++)
{
for (int y = 0; y < MapSize; y++)
{
// --- 1. TEMPERATURE ---
float temperature = (float)y / MapSize;
float tempNoise = (_noise.GetNoise2D(x + 1000, y + 1000) + 1.0f) / 2.0f;
temperature += (tempNoise * 0.2f) - 0.1f;
_tempMap[x, y] = temperature;
// --- 2. THE ORIGINAL ISLAND FALLOFF ---
float nx = Mathf.Abs(x - center.X) / (MapSize / 2.0f * 1.15f);
float ny = Mathf.Abs(y - center.Y) / (MapSize / 2.0f * 0.90f);
float squircleFalloff = Mathf.Max(nx, ny);
Vector2 ellipticalPos = new Vector2((x - center.X) / 1.15f, (y - center.Y) / 0.90f);
float ellipticalFalloff = ellipticalPos.Length() / (MapSize / 1.3f);
float finalFalloff = Mathf.Lerp(ellipticalFalloff, squircleFalloff, 0.5f);
// The beautiful jagged edge noise!
float edgeNoise = (_noise.GetNoise2D(x * 2.5f, y * 2.5f) + 1.0f) / 2.0f;
finalFalloff += (edgeNoise * 0.15f) * squircleFalloff;
// --- NEW: THE SOUTHERN ARCHIPELAGO FIX ---
// If we are in the bottom 25% of the map, apply extra sinking pressure
float southThreshold = MapSize * 0.75f;
if (y > southThreshold)
{
float southDepth = (y - southThreshold) / (MapSize - southThreshold);
finalFalloff += southDepth * 0.6f; // Sink the stretched land bridges!
}
finalFalloff = Mathf.Pow(finalFalloff, 2.5f);
float distX = Mathf.Abs(x - center.X) / (MapSize / 2.0f);
float distY = Mathf.Abs(y - center.Y) / (MapSize / 2.0f);
if (distX > 0.90f) finalFalloff += (distX - 0.90f) * 15.0f;
if (distY > 0.90f) finalFalloff += (distY - 0.90f) * 15.0f;
// --- 3. THE MOUNTAIN SPINE ---
float mountainSpine = 0f;
if (temperature < 0.65f)
{
float distanceToCenterX = Mathf.Abs(x - center.X) / (MapSize / 2.0f * 1.15f);
mountainSpine = 1.0f - distanceToCenterX;
float southernFade = Mathf.Clamp((0.65f - temperature) * 4.0f, 0.0f, 1.0f);
mountainSpine = Mathf.Pow(mountainSpine, 3.0f) * southernFade * 0.6f;
}
// --- 4. COMBINE HEIGHT ---
float rawBase = (_noise.GetNoise2D(x, y) + 1.0f) / 2.0f;
float finalH = rawBase + mountainSpine - (finalFalloff * FalloffStrength);
// --- 5. CARVE THE CRATER (The Flooded Bay & Landbridge Fix!) ---
float distToCrater = new Vector2(x, y).DistanceTo(_impactCenter);
// We only carve the physical hole at 80% of the radius to guarantee a landbridge!
float physicalCraterRadius = _impactRadius * 0.80f;
if (distToCrater < physicalCraterRadius)
{
float craterDepth = 1.0f - (distToCrater / physicalCraterRadius);
// Dialed back to -0.15f as per your excellent instinct!
finalH = Mathf.Lerp(finalH, GetSeaLevel(temperature) - 0.15f, craterDepth * 0.9f);
}
_heightMap[x, y] = finalH;
}
}
}
private void CalculateTrueOcean()
{
Queue<Vector2I> queue = new Queue<Vector2I>();
queue.Enqueue(new Vector2I(0, 0));
_isTrueOcean[0, 0] = true;
Vector2I[] directions = { Vector2I.Up, Vector2I.Down, Vector2I.Left, Vector2I.Right };
while (queue.Count > 0)
{
Vector2I current = queue.Dequeue();
foreach (var dir in directions)
{
Vector2I neighbor = current + dir;
if (neighbor.X >= 0 && neighbor.X < MapSize && neighbor.Y >= 0 && neighbor.Y < MapSize)
{
if (!_isTrueOcean[neighbor.X, neighbor.Y] && _heightMap[neighbor.X, neighbor.Y] < GetSeaLevel(_tempMap[neighbor.X, neighbor.Y]))
{
_isTrueOcean[neighbor.X, neighbor.Y] = true;
queue.Enqueue(neighbor);
}
}
}
}
}
private void CalculateMainland()
{
Queue<Vector2I> queue = new Queue<Vector2I>();
Vector2I center = new Vector2I(MapSize / 2, MapSize / 2);
if (_heightMap[center.X, center.Y] >= GetSeaLevel(_tempMap[center.X, center.Y]))
{
queue.Enqueue(center);
_isMainland[center.X, center.Y] = true;
}
Vector2I[] directions = { Vector2I.Up, Vector2I.Down, Vector2I.Left, Vector2I.Right };
while (queue.Count > 0)
{
Vector2I current = queue.Dequeue();
foreach (var dir in directions)
{
Vector2I neighbor = current + dir;
if (neighbor.X >= 0 && neighbor.X < MapSize && neighbor.Y >= 0 && neighbor.Y < MapSize)
{
if (!_isMainland[neighbor.X, neighbor.Y] && _heightMap[neighbor.X, neighbor.Y] >= GetSeaLevel(_tempMap[neighbor.X, neighbor.Y]))
{
_isMainland[neighbor.X, neighbor.Y] = true;
queue.Enqueue(neighbor);
}
}
}
}
}
private void AssignBiomesAndDraw()
{
Image mapImage = Image.CreateEmpty(MapSize, MapSize, false, Image.Format.Rgba8);
for (int x = 0; x < MapSize; x++)
{
for (int y = 0; y < MapSize; y++)
{
float h = _heightMap[x, y];
float t = _tempMap[x, y];
Biome b;
float currentSeaLevel = GetSeaLevel(t);
if (h < currentSeaLevel) b = _isTrueOcean[x, y] ? Biome.Ocean : Biome.Lake;
else
{
float baseDist = new Vector2(x, y).DistanceTo(_impactCenter);
float craterNoise = _noise.GetNoise2D(x * 2.5f, y * 2.5f) * 25.0f;
float warpedDist = baseDist + craterNoise;
if (warpedDist < _impactRadius * 0.5f) b = Biome.Crater;
else
{
float beachThickness = Mathf.Lerp(0.04f, 0.10f, Mathf.Clamp(t, 0f, 1f));
if (h < currentSeaLevel + beachThickness) b = Biome.Beach;
else
{
float snowDetail = (_noise.GetNoise2D(x * 5.0f, y * 5.0f) + 1.0f) / 2.0f;
float wastelandFray = _noise.GetNoise2D(x * 1.5f + 5000, y * 1.5f + 5000) * 60.0f; // Add a separate, jagged noise layer just for the wasteland boundary
if (h > 0.88f || (h > 0.80f && snowDetail > 0.6f)) b = Biome.Snow;
else if (h > 0.72f) b = Biome.Mountain;
else if (warpedDist < _impactRadius + wastelandFray + 20.0f) b = Biome.Wasteland;
else if (warpedDist < _impactRadius * 1.35f || t < 0.35f) b = Biome.Jungle;
else if (t > 0.65f) b = Biome.Paradise;
else b = Biome.Tropical;
}
}
}
_biomeMap[x, y] = b;
Color c = Colors.Black;
switch(b) {
case Biome.Ocean: c = new Color(0.1f, 0.4f, 0.7f); break;
case Biome.Lake: c = new Color(0.2f, 0.6f, 0.8f); break;
case Biome.Beach: c = new Color(0.8f, 0.7f, 0.4f); break;
case Biome.Snow: c = new Color(0.9f, 0.9f, 0.9f); break;
case Biome.Mountain: c = new Color(0.5f, 0.5f, 0.5f); break;
case Biome.Crater: c = new Color(0.3f, 0.1f, 0.3f); break;
case Biome.Wasteland: c = new Color(0.5f, 0.2f, 0.5f); break;
case Biome.Jungle: c = new Color(0.1f, 0.4f, 0.1f); break;
case Biome.Paradise: c = new Color(0.4f, 0.8f, 0.4f); break;
case Biome.Tropical: c = new Color(0.2f, 0.6f, 0.2f); break;
}
mapImage.SetPixel(x, y, c);
}
}
Texture = ImageTexture.CreateFromImage(mapImage);
}
private void GenerateTowns()
{
_towns.Clear();
// 1. Calculate Multipliers (Used ONLY for lower-tier towns now!)
float sizeRatio = MapSize / 4096f;
float areaMulti = Mathf.Max(1.0f, sizeRatio * sizeRatio);
float densityMulti = ConfigManager.DensityMultiplier;
int Calc(int baseCount) {
return Mathf.Max(1, Mathf.RoundToInt(baseCount * areaMulti * densityMulti));
}
// --- 2. THE HIGHWAY ANCHORS (Strictly 1 per biome, ignoring multipliers) ---
PlaceCapitol(); // Capitol is automatically a Highway Node
bool southIsLeft = GD.Randi() % 2 == 0;
MapHalf southHalf = southIsLeft ? MapHalf.Left : MapHalf.Right;
MapHalf midHalf = southIsLeft ? MapHalf.Right : MapHalf.Left;
// Spawn exactly 1 Hub per green biome
PlaceTownNodes(1, TownTier.Hub, Biome.Paradise, false, true, true, 0.04f, southHalf);
PlaceTownNodes(1, TownTier.Hub, Biome.Tropical, false, true, true, 0.04f, midHalf, true, false, 0.40f, 0.60f);
PlaceTownNodes(1, TownTier.Hub, Biome.Jungle, true, false, false, 0.04f, MapHalf.Any);
// Safety check: Force all currently spawned Hubs to be active Highway Nodes
for (int i = 0; i < _towns.Count; i++) {
var t = _towns[i];
t.IsHighwayNode = true;
_towns[i] = t;
}
// --- 3. THE MOUNTAIN BOSS (Strictly 1, Center Biased, NOT a Highway Node) ---
int beforeSnowCount = _towns.Count;
// We use `true` for the 9th parameter (centerBias) to push it deep into the frozen mountains
PlaceTownNodes(1, TownTier.Hub, Biome.Snow, true, false, false, 0.15f, MapHalf.Any, true);
// Safety check: Force the Snow Hub to be excluded from the Highway Loop
for (int i = beforeSnowCount; i < _towns.Count; i++) {
var t = _towns[i];
t.IsHighwayNode = false;
_towns[i] = t;
}
// --- 4. THE RUGGED NETWORK & POIs (Scaled by Config Multipliers) ---
PlaceTownNodes(Calc(1), TownTier.Village, Biome.Paradise, false, true, false, 0.04f, MapHalf.Any);
PlaceTownNodes(Calc(2), TownTier.Village, Biome.Paradise, true, false, false, 0.04f, MapHalf.Any);
PlaceTownNodes(Calc(1), TownTier.Village, Biome.Tropical, false, true, false, 0.04f, MapHalf.Any);
PlaceTownNodes(Calc(3), TownTier.Village, Biome.Tropical, true, false, false, 0.04f, MapHalf.Any);
PlaceTownNodes(Calc(2), TownTier.Village, Biome.Jungle, true, false, false, 0.05f, MapHalf.Any);
PlaceTownNodes(Calc(1), TownTier.Village, Biome.Snow, true, false, false, 0.05f, MapHalf.Any);
PlaceTownNodes(Calc(2), TownTier.Outpost, Biome.Snow, true, false, false, 0.12f, MapHalf.Any);
PlaceTownNodes(Calc(1), TownTier.Outpost, Biome.Mountain, true, false, false, 0.15f, MapHalf.Any);
PlaceTownNodes(Calc(1), TownTier.IslandLoot, Biome.Paradise, false, false, false, 0.08f, MapHalf.Any, false, true);
PlaceTownNodes(Calc(1), TownTier.IslandLoot, Biome.Tropical, false, false, false, 0.08f, MapHalf.Any, false, true);
PlaceTownNodes(Calc(3), TownTier.MiniPOI, Biome.Jungle, true, false, false, 0.1f, MapHalf.Any);
PlaceTownNodes(Calc(3), TownTier.MiniPOI, Biome.Tropical, true, false, false, 0.1f, MapHalf.Any);
PlaceTownNodes(Calc(2), TownTier.MiniPOI, Biome.Paradise, true, false, false, 0.1f, MapHalf.Any);
QueueRedraw();
}
private void PlaceCapitol() // This method is a bit more complex than the others because of the unique requirements for the Capitol's location. We want it to be in the Wasteland biome, on the coast (but not too high elevation), and safely outside the blast radius. To achieve this, we'll use a tiered approach with multiple fallback strategies.
{
bool TestCandidate(int cx, int cy, bool requireCoastal)
{
if (!_isMainland[cx, cy]) return false; // MUST be mainland
float localSeaLevel = GetSeaLevel(_tempMap[cx, cy]);
if (_heightMap[cx, cy] < localSeaLevel) return false; // Must be land
if (_biomeMap[cx, cy] != Biome.Wasteland) return false; // Must be Wasteland
if (requireCoastal && _heightMap[cx, cy] >= localSeaLevel + 0.15f) return false;
return true;
}
float minDist = _impactRadius + 30f;
float maxDist = _impactRadius + 150f;
// TIER 1: Ideal (Wasteland + Coastal + Mainland)
for (int i = 0; i < 5000; i++)
{
float angle = (float)GD.RandRange(0, Mathf.Tau);
float dist = (float)GD.RandRange(minDist, maxDist);
Vector2 pos = _impactCenter + new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * dist;
int cx = Mathf.Clamp((int)pos.X, 0, MapSize - 1);
int cy = Mathf.Clamp((int)pos.Y, 0, MapSize - 1);
if (TestCandidate(cx, cy, requireCoastal: true)) {
CommitCapitol(cx, cy);
return;
}
}
// TIER 2: Relax coastal requirement
for (int i = 0; i < 10000; i++)
{
float angle = (float)GD.RandRange(0, Mathf.Tau);
float dist = (float)GD.RandRange(minDist, _impactRadius + 400f);
Vector2 pos = _impactCenter + new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * dist;
int cx = Mathf.Clamp((int)pos.X, 0, MapSize - 1);
int cy = Mathf.Clamp((int)pos.Y, 0, MapSize - 1);
if (TestCandidate(cx, cy, requireCoastal: false)) {
CommitCapitol(cx, cy);
return;
}
}
// TIER 3: Deterministic Scan (Find closest valid pixel)
GD.PrintErr("Capitol: Random tiers failed. Running deterministic wasteland scan...");
var candidates = new List<(int x, int y, float dist)>();
for (int x = 0; x < MapSize; x++) {
for (int y = 0; y < MapSize; y++) {
if (_isMainland[x, y] && _biomeMap[x, y] == Biome.Wasteland) {
candidates.Add((x, y, new Vector2(x, y).DistanceTo(_impactCenter)));
}
}
}
if (candidates.Count > 0) {
candidates.Sort((a, b) => a.dist.CompareTo(b.dist));
int pickIdx = (int)GD.RandRange(0, Mathf.Max(1, candidates.Count / 20));
CommitCapitol(candidates[pickIdx].x, candidates[pickIdx].y);
return;
}
// TIER 4: Doomsday (Walk south until we hit mainland)
GD.PrintErr("Capitol: No mainland wasteland found! Placing at fallback offset.");
int fx = Mathf.Clamp((int)_impactCenter.X, 0, MapSize - 1);
int fy = Mathf.Clamp((int)(_impactCenter.Y + _impactRadius + 50f), 0, MapSize - 1);
while (fy < MapSize - 1 && !_isMainland[fx, fy]) fy++;
CommitCapitol(fx, fy);
}
private void CommitCapitol(int cx, int cy)
{
_towns.Add(new TownData {
Position = new Vector2(cx, cy),
Tier = TownTier.Capitol,
IsHighwayNode = true
});
GD.Print($"Capitol placed at ({cx},{cy}).");
}
private void PlaceTownNodes(int count, TownTier tier, Biome targetBiome, bool requireInland, bool requireCoastal, bool isHighway, float maxSlope, MapHalf halfConstraint, bool requireMainland = true, bool requireIsland = false, float minTemp = 0f, float maxTemp = 1f)
{
int placed = 0;
int attempts = 0;
// --- TUNED SPACING ---
// Big Hubs/Capitol need 120px (1.8km) spacing. Villages/POIs only need 60px (900m).
float minDistance = (tier == TownTier.Hub || tier == TownTier.Capitol) ? MapSize * 0.12f : MapSize * 0.06f;
bool ignoreBiome = false;
while (placed < count && attempts < 80000)
{
attempts++;
if (attempts == 35000)
{
if (requireIsland) ignoreBiome = true;
else {
if (!isHighway) requireCoastal = false;
requireInland = false;
maxSlope = 0.08f;
minTemp = 0f;
maxTemp = 1f;
}
}
if (attempts == 65000 && requireIsland) {
requireIsland = false;
requireMainland = true;
requireCoastal = true;
}
int rx = (int)GD.RandRange(20, MapSize - 20);
int ry = (int)GD.RandRange(20, MapSize - 20);
if (halfConstraint == MapHalf.Left && rx > MapSize / 2) continue;
if (halfConstraint == MapHalf.Right && rx < MapSize / 2) continue;
float t = _tempMap[rx, ry];
float currentSeaLevel = GetSeaLevel(t);
if (_heightMap[rx, ry] < currentSeaLevel) continue;
if (!ignoreBiome && _biomeMap[rx, ry] != targetBiome) continue;
if (requireMainland && !_isMainland[rx, ry]) continue;
if (requireIsland && _isMainland[rx, ry]) continue;
if (t < minTemp || t > maxTemp) continue;
float centerH = _heightMap[rx, ry];
// Sample slope a bit wider (10px) due to higher resolution
int slopeRadius = (int)(MapSize * 0.005f); // Automatically scales!
if (Mathf.Abs(_heightMap[rx+slopeRadius, ry] - centerH) > maxSlope) continue;
if (Mathf.Abs(_heightMap[rx-slopeRadius, ry] - centerH) > maxSlope) continue;
if (Mathf.Abs(_heightMap[rx, ry+slopeRadius] - centerH) > maxSlope) continue;
if (Mathf.Abs(_heightMap[rx, ry-slopeRadius] - centerH) > maxSlope) continue;
bool nearTrueOcean = false;
bool nearAnyWater = false;
// --- TUNED WATER DETECTION ---
// Increased to 24px (approx 375m) to ensure coastal towns are actually near the surf
int waterSearch = (int)(MapSize * 0.015f); // Automatically scales!
for(int i = -waterSearch; i <= waterSearch; i += 2) {
for(int j = -waterSearch; j <= waterSearch; j += 2) {
int checkX = Mathf.Clamp(rx + i, 0, MapSize - 1);
int checkY = Mathf.Clamp(ry + j, 0, MapSize - 1);
float checkSeaLevel = GetSeaLevel(_tempMap[checkX, checkY]);
if (_isTrueOcean[checkX, checkY]) nearTrueOcean = true;
if (_heightMap[checkX, checkY] < checkSeaLevel) nearAnyWater = true;
}
}
if (!requireIsland) {
if (requireCoastal && !nearTrueOcean) continue;
if (requireInland && nearAnyWater) continue;
}
bool tooClose = false;
foreach (var town in _towns) {
if (new Vector2(rx, ry).DistanceTo(town.Position) < minDistance) tooClose = true;
}
if (tooClose) continue;
_towns.Add(new TownData { Position = new Vector2(rx, ry), Tier = tier, IsHighwayNode = isHighway });
placed++;
}
}
// --- LOGISTICS & ROAD GENERATION ---
private async Task GenerateRoadsAsync()
{
GD.Print($"[A*] Starting Logistics Pathfinding for {MapSize}x{MapSize} world...");
_highwayPaths.Clear(); _branchPaths.Clear(); _ruggedPaths.Clear(); _trailPaths.Clear();
AStarGrid2D astar = new AStarGrid2D();
astar.Region = new Rect2I(0, 0, MapSize, MapSize);
astar.CellSize = new Vector2I(1, 1);
astar.DiagonalMode = AStarGrid2D.DiagonalModeEnum.OnlyIfNoObstacles;
astar.Update();
SetBaseAStarWeights(astar);
int loopRepulsion = (int)(120 * (MapSize / 4096f));
Vector2 center = new Vector2(MapSize / 2f, MapSize / 2f);
HashSet<Vector2I> highwayPixels = new HashSet<Vector2I>();
// --- 1. THE CONTINENTAL LOOP ---
List<TownData> highwayNodes = _towns.FindAll(t => t.IsHighwayNode);
highwayNodes.Sort((a, b) => {
float angleA = Mathf.Atan2(a.Position.Y - center.Y, a.Position.X - center.X);
float angleB = Mathf.Atan2(b.Position.Y - center.Y, b.Position.X - center.X);
return angleA.CompareTo(angleB);
});
GD.Print($"[A*] 1/4: Plotting the {highwayNodes.Count}-Node Continental Loop...");
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE
if (highwayNodes.Count >= 3) {
for (int i = 0; i < highwayNodes.Count; i++) {
Vector2 startPos = highwayNodes[i].Position;
Vector2 endPos = highwayNodes[(i + 1) % highwayNodes.Count].Position;
Vector2[] rawPath = astar.GetPointPath(
new Vector2I((int)startPos.X, (int)startPos.Y),
new Vector2I((int)endPos.X, (int)endPos.Y)
);
if (rawPath.Length > 1) {
_highwayPaths.Add(SmoothPath(rawPath));
foreach(var p in rawPath) highwayPixels.Add(new Vector2I((int)p.X, (int)p.Y));
PenalizePath(astar, rawPath, loopRepulsion);
}
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE
}
}
// --- 2. THE MOUNTAIN BOSS BRANCH ---
GD.Print("[A*] 2/4: Connecting the Snow Boss...");
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE
List<TownData> connectedTowns = new List<TownData>(highwayNodes);
TownData snowBoss = _towns.Find(t => t.Tier == TownTier.Hub && !t.IsHighwayNode);
if (snowBoss.Tier == TownTier.Hub) {
Vector2I bestPixel = new Vector2I((int)center.X, (int)center.Y);
float closestDist = float.MaxValue;
int skip = 0;
foreach (var pixel in highwayPixels) {
if (skip++ % 5 != 0) continue;
float d = snowBoss.Position.DistanceSquaredTo(new Vector2(pixel.X, pixel.Y));
if (d < closestDist) {
closestDist = d;
bestPixel = pixel;
}
}
Vector2[] bossPath = astar.GetPointPath(
new Vector2I((int)snowBoss.Position.X, (int)snowBoss.Position.Y), bestPixel
);
if (bossPath.Length > 1) {
_branchPaths.Add(SmoothPath(bossPath));
PenalizePath(astar, bossPath, loopRepulsion);
}
connectedTowns.Add(snowBoss);
}
// --- 3 & 4. COUNTY ROADS (The Daisy-Chain Protocol) ---
GD.Print("[A*] 3/4 & 4/4: Weaving County Roads (Daisy-Chaining)...");
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE
List<TownData> unconnectedTowns = _towns.FindAll(t => !connectedTowns.Contains(t) && t.Tier != TownTier.IslandLoot);
int count = 0;
int totalToConnect = unconnectedTowns.Count;
// Prim's Algorithm: Organically grow the network out from the Hubs
while (unconnectedTowns.Count > 0)
{
TownData bestUnconnected = unconnectedTowns[0];
TownData bestConnected = connectedTowns[0];
float shortestDist = float.MaxValue;
// Find the single closest gap between the Unconnected wilderness and the Connected grid
foreach (var unconn in unconnectedTowns) {
foreach (var conn in connectedTowns) {
float d = unconn.Position.DistanceSquaredTo(conn.Position);
if (d < shortestDist) {
shortestDist = d;
bestUnconnected = unconn;
bestConnected = conn;
}
}
}
count++;
float actualDist = Mathf.Sqrt(shortestDist);
// NEW: Live Diagnostics! Print the exact town we are testing BEFORE we calculate.
GD.Print($" -> ({count}/{totalToConnect}) Pathing {bestUnconnected.Tier} to grid. Dist: {actualDist:F1}px");
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE!
Vector2[] countyPath = new Vector2[0];
// THE ABANDON PROTOCOL: If it's more than 8% of the map away, it's a hermit camp. Leave it!
if (actualDist > (MapSize * 0.08f))
{
GD.Print($" [!] Too isolated. Abandoning road.");
}
else
{
// Attempt the connection. (If it freezes here, we know exactly which town caused it!)
countyPath = astar.GetPointPath(
new Vector2I((int)bestUnconnected.Position.X, (int)bestUnconnected.Position.Y),
new Vector2I((int)bestConnected.Position.X, (int)bestConnected.Position.Y)
);
}
// If a path was found, draw it and penalize the pixels
if (countyPath.Length > 1) {
if (bestUnconnected.Tier == TownTier.Village) _ruggedPaths.Add(SmoothPath(countyPath));
else _trailPaths.Add(SmoothPath(countyPath));
PenalizePath(astar, countyPath, loopRepulsion / 2);
}
else if (actualDist <= (MapSize * 0.08f))
{
GD.Print($" [!] Path impossible (Trapped by terrain). Abandoning road.");
}
unconnectedTowns.Remove(bestUnconnected);
connectedTowns.Add(bestUnconnected);
}
GD.Print("[A*] Logistics Network Complete!");
}
private float GetSeaLevel(float t) => Mathf.Lerp(0.26f, 0.15f, Mathf.Clamp(t, 0f, 1f));
private Vector2I FindClosestPixel(Vector2 pos, HashSet<Vector2I> set) {
Vector2I closest = new Vector2I(0,0); float min = float.MaxValue;
foreach(var p in set) { float d = pos.DistanceSquaredTo(p); if(d < min){ min = d; closest = p; }}
return closest;
}
private float GetShortestDistToSet(Vector2 pos, HashSet<Vector2I> set) {
float shortest = float.MaxValue;
Vector2I pInt = new Vector2I((int)pos.X, (int)pos.Y);
foreach (var p in set) {
float dist = pInt.DistanceSquaredTo(p);
if (dist < shortest) shortest = dist;
}
return shortest;
}
private void SetBaseAStarWeights(AStarGrid2D astar) {
for (int x = 0; x < MapSize; x++) {
for (int y = 0; y < MapSize; y++) {
Biome b = _biomeMap[x, y];
// 1. CRITICAL FIX: Make the crater physically impassable first!
if (b == Biome.Crater) {
astar.SetPointSolid(new Vector2I(x, y), true);
continue; // "Skip the rest, go to next pixel"
}
// 2. Existing water/mainland checks
if (b == Biome.Ocean || b == Biome.Lake || !_isMainland[x, y]) {
astar.SetPointSolid(new Vector2I(x, y), true);
continue; // "Skip the rest, go to next pixel"
}
// 3. We only reach this point if it's mainland AND not a crater!
float h = _heightMap[x, y];
float localSea = GetSeaLevel(_tempMap[x, y]);
float normalizedElevation = Mathf.Clamp((h - localSea) / (1.0f - localSea), 0.0f, 1.0f);
// Simple, steep curve to avoid mountains
float weight = 1.0f + Mathf.Pow(normalizedElevation, 3.0f) * 400.0f;
// Push off the beach (not an insane wall, just enough to prefer grass)
if (b == Biome.Beach) weight += 15.0f;
// Let it path through the wasteland normally
if (b == Biome.Wasteland) weight += 3.0f;
astar.SetPointWeightScale(new Vector2I(x, y), weight);
}
}
}
private void PenalizePath(AStarGrid2D astar, Vector2[] path, int radius)
{
int step = Mathf.Max(1, radius / 2);
for (int i = 0; i < path.Length; i += step) {
Vector2 p = path[i];
for (int dx = -radius; dx <= radius; dx++) {
for (int dy = -radius; dy <= radius; dy++) {
int nx = Mathf.Clamp((int)p.X + dx, 0, MapSize - 1);
int ny = Mathf.Clamp((int)p.Y + dy, 0, MapSize - 1);
var cell = new Vector2I(nx, ny);
if (!astar.IsPointSolid(cell)) {
// The true Iron Curtain
astar.SetPointWeightScale(cell, astar.GetPointWeightScale(cell) + 10000f);
}
}
}
}
}
private Vector2[] SmoothPath(Vector2[] raw)
{
if (raw.Length < 3) return raw;
List<Vector2> decimated = RamerDouglasPeucker(raw, 4.0f);
if (decimated.Count < 3) return raw;
List<Vector2> smoothed = decimated;
for (int pass = 0; pass < 4; pass++) smoothed = ChaikinPass(smoothed, true);
return smoothed.ToArray();
}
private List<Vector2> RamerDouglasPeucker(Vector2[] points, float tolerance)
{
if (points.Length <= 2) return new List<Vector2>(points);
float maxDist = 0f;
int maxIdx = 0;
Vector2 lineStart = points[0];
Vector2 lineEnd = points[points.Length - 1];
for (int i = 1; i < points.Length - 1; i++) {
float dist = PerpendicularDistance(points[i], lineStart, lineEnd);
if (dist > maxDist) { maxDist = dist; maxIdx = i; }
}
if (maxDist > tolerance) {
var left = RamerDouglasPeucker(points[..(maxIdx + 1)], tolerance);
var right = RamerDouglasPeucker(points[maxIdx..], tolerance);
left.RemoveAt(left.Count - 1);
left.AddRange(right);
return left;
} else {
return new List<Vector2> { lineStart, lineEnd };
}
}
private float PerpendicularDistance(Vector2 point, Vector2 lineStart, Vector2 lineEnd)
{
Vector2 line = lineEnd - lineStart;
float len = line.Length();
if (len < 0.0001f) return point.DistanceTo(lineStart);
return Mathf.Abs((lineEnd.X - lineStart.X) * (lineStart.Y - point.Y) - (lineStart.X - point.X) * (lineEnd.Y - lineStart.Y)) / len;
}
private List<Vector2> ChaikinPass(List<Vector2> points, bool open)
{
var result = new List<Vector2>();
if (open) result.Add(points[0]);
for (int i = 0; i < points.Count - 1; i++) {
Vector2 p0 = points[i];
Vector2 p1 = points[(i + 1) % points.Count];
result.Add(p0 * 0.75f + p1 * 0.25f);
result.Add(p0 * 0.25f + p1 * 0.75f);
}
if (open) result.Add(points[points.Count - 1]);
return result;
}
// --- REFINED DRAWING FOR SCALE ---
public override void _Draw()
{
float scaleMod = MapSize / 1024f; // equals 4 at 4096
// Trails: Thin brown
foreach (var path in _trailPaths) DrawPolyline(path, new Color(0.5f, 0.4f, 0.3f, 0.7f), 1.0f * scaleMod, true);
// Rugged: Thicker dark brown
foreach (var path in _ruggedPaths) DrawPolyline(path, new Color(0.35f, 0.25f, 0.15f), 1.5f * scaleMod, true);
// Main Roads: Solid Black
foreach (var path in _branchPaths) DrawPolyline(path, Colors.Black, 1.0f * scaleMod, true);
// Highway: Thick Red
foreach (var path in _highwayPaths) DrawPolyline(path, new Color(0.9f, 0.1f, 0.1f), 2.0f * scaleMod, true);
// Draw Towns
foreach (var town in _towns) {
float radius = (town.Tier == TownTier.Capitol) ? 12f : (town.Tier == TownTier.Hub ? 8f : 4f);
radius *= scaleMod; // Scale the circles up!
Color c = town.IsHighwayNode ? Colors.Yellow : (town.Tier == TownTier.Outpost ? Colors.Cyan : Colors.Orange);
if (town.Tier == TownTier.IslandLoot) c = Colors.Red;
if (town.Tier == TownTier.MiniPOI) { radius = 2.5f * scaleMod; c = Colors.SaddleBrown; }
DrawCircle(town.Position, radius + (1.5f * scaleMod), Colors.Black);
DrawCircle(town.Position, radius, c);
}
}
}
// Place this at the very bottom of the file!
public partial class MapDrawProxy : Control
{
public MapGenerator Source;
public override void _Draw()
{
if (Source == null) return;
float scaleMod = Source.MapSize / 1024f;
foreach (var path in Source._trailPaths)
DrawPolyline(path, new Color(0.5f, 0.4f, 0.3f, 0.7f), 1.0f * scaleMod, true);
foreach (var path in Source._ruggedPaths)
DrawPolyline(path, new Color(0.35f, 0.25f, 0.15f), 1.5f * scaleMod, true);
foreach (var path in Source._branchPaths)
DrawPolyline(path, Colors.Black, 1.0f * scaleMod, true);
foreach (var path in Source._highwayPaths)
DrawPolyline(path, new Color(0.9f, 0.1f, 0.1f), 2.0f * scaleMod, true);
foreach (var town in Source._towns)
{
float radius = (town.Tier == TownTier.Capitol) ? 12f : (town.Tier == TownTier.Hub ? 8f : 4f);
radius *= scaleMod;
Color c = town.IsHighwayNode ? Colors.Yellow : (town.Tier == TownTier.Outpost ? Colors.Cyan : Colors.Orange);
if (town.Tier == TownTier.IslandLoot) c = Colors.Red;
if (town.Tier == TownTier.MiniPOI) { radius = 2.5f * scaleMod; c = Colors.SaddleBrown; }
DrawCircle(town.Position, radius + (1.5f * scaleMod), Colors.Black);
DrawCircle(town.Position, radius, c);
}
}
}

View file

@ -0,0 +1 @@
uid://bl6pk71fngm8p

View file

@ -0,0 +1,47 @@
# MapGenerator.cs - Architecture & Historical Decisions
**Date:** March 2026
**Purpose:** Generates a 4096x4096 2D topographical blueprint (Topography, Biomes, Cities, and Road Networks) to be parsed and translated into a 3D Voxel World.
---
## 1. Topography & The Impact Crater
* **Base Generation:** Uses `FastNoiseLite` to generate standard heightmaps and temperature maps, which drive dynamic biomes.
* **Dynamic Sea Level:** Sea level is calculated based on the temperature map (simulating ice caps/equatorial swelling).
* **The Crater:** A massive (radius 400) blast zone is forced along the northern coast. Surrounding it is a `Biome.Wasteland` with a noise-driven "fray" on its borders.
* **Design Choice:** We allowed the crater to occasionally spawn slightly inland rather than strictly on the beach. This creates natural topographical "pinch points" that will result in highly memorable, claustrophobic 3D driving mechanics.
## 2. City Placement & The Capitol Tether
* **The Challenge:** The Capitol City must spawn inside the radioactive Wasteland, but it MUST be connected to the contiguous mainland for the A* highway to reach it. Early iterations spawned the Capitol underwater or stranded it on offshore islands caused by the Wasteland noise bleeding over the ocean.
* **The Solution (Smart Tiered Spawner):** We abandoned a rigid `while` loop in favor of a tiered attempt system.
* **Tier 1:** Searches for ideal (Wasteland + Coastal + Mainland).
* **Tier 2:** Relaxes the coastal requirement.
* **Tier 3:** Scans the entire array for the closest valid pixel to the impact center.
* **Tier 4 (Doomsday):** Walks strictly south until it hits the mainland.
## 3. The Pathfinding Wars (A* Road Optimization)
The road network generation was the hardest logistical hurdle. Standard grid-based `AStarGrid2D` math does not scale cleanly to a 4096 map.
### Phase 1: The 16-Minute Bottleneck
* **The Flaw:** To force the highway to create a large loop connecting our 3 Hubs, we used an `ApplyRepulsion` function that penalized pixels in a 400px radius around the first drawn road.
* **The Math:** Repelling a 2,000-point path with a 400px radius resulted in over **1.3 Billion** nested loop iterations. The CPU choked, and generation took 1116 minutes.
* **The Result:** Unusable generation times, extreme stair-stepping (zig-zagging), and roads cutting straight over mountains.
### Phase 2: The Speedrun & The Double-Back
* **The Flaw:** We ripped out the repulsion completely to save CPU time, dropping the render to **1 minute**.
* **The Result:** Because A* is deterministic, the pathfinder just used the exact same pixels to return to the start, completely breaking the 3-point loop and causing the highway to double back on itself.
### Phase 3: The Over-Engineered Spaghetti (The LLM Trap)
* **The Flaw:** We brought in an external LLM to fix the pathing. It introduced a "Midland Sweet Spot" to force roads off the coast, and a "Daisy-Chain" mechanic for branch roads.
* **The Result:** The highway started tracing narrow elevation contour lines like a drunk driver. The branch roads turned into a game of "Snake," meandering wildly to connect to each other instead of taking logical paths to the highway. Furthermore, adding a soft weight to the crater combined with our loop repulsion created a "Wasteland Phobia" where the A* algorithm mathematically folded and refused to complete the loop.
### Phase 4: The Tactical Revert (Our Final State)
We executed a deliberate tactical revert, stripping out the over-engineered LLM spaghetti and keeping only robust, simple math:
1. **Direct Branches:** Black roads path directly to the closest highway pixel. No daisy-chaining.
2. **The Iron Curtain Repulsion:** We restored a `+10000f` penalty to old highway segments with a radius of 120px to physically force the 3-point loop. We optimized it by only applying the penalty to every `step` (half the radius) pixel. Render time remains at ~1 minute.
3. **Simple Elevation Weights:** Instead of a complex U-curve, we use `Mathf.Pow(normalizedElevation, 3.0f) * 400.0f` to aggressively punish mountains, and a flat `+15.0f` penalty to gently push roads off the beach sand.
## 4. The AAA Smoothing Pipeline
Standard A* creates jagged, octagonal stair-steps that look terrible in a 3D voxel world. We completely eradicated this using a 2-step geometric smoothing pipeline:
1. **Ramer-Douglas-Peucker (RDP):** Decimates the raw A* path, removing thousands of useless straight-line grid points and leaving only the major turns.
2. **Chaikin's Algorithm:** Performs 4 passes of corner-cutting on the decimated points, resulting in buttery-smooth, sweeping, AAA-style highway curves ready for 3D mesh generation.

31
Tools/Scripts/README.md Normal file
View file

@ -0,0 +1,31 @@
# 📜 /Tools/Scripts - Generation Logic
## Overview
This directory contains the raw C# backend logic for all of our offline developer tools. These scripts are strictly for generating source data (Maps, Prefabs, Balance Configs) and are completely decoupled from the live game server and client.
---
## 🏗️ Core Scripts
### `MapGenerator.cs`
**The Master World Builder.** This script is responsible for generating the 2D topographical and logistical blueprint of *Isla Apocalypse*. It runs FastNoiseLite math, calculates distance falloffs, plots A* road networks, and zones the biomes.
#### ⚔️ The Great 4096 Struggle (The 1:1 Scale Transition)
Originally, this script generated a 1024x1024 map where 1 pixel equaled 1 chunk (4 meters). To accommodate industry-standard human-scale base building, we transitioned the engine to a 1:1 scale (1 pixel = 1 meter), effectively quadrupling the map size to 4096x4096.
This transition broke almost everything, requiring a massive architectural overhaul:
1. **The "TV Static" Math Fix:** Multiplying the map size by 4 caused the noise generator to pack 4 kilometers of terrain into 1 kilometer of space, creating visual static. We introduced a dynamic `scaleFactor` (`MapSize / 1024f`) to stretch the noise frequency back out, preserving the massive, sweeping "chonky" biomes while maintaining the 1:1 scale.
2. **Eradicating Magic Numbers:** Hardcoded pixel checks (e.g., "check 10 pixels for water") were completely broken by the upscaling. We refactored the logic to use dynamic map percentages for slope checking, water detection, and A* highway repulsion.
3. **The 1KB Postage Stamp (Defeating the UI Engine):** Trying to capture a 4096 image inside a standard Godot UI layout node resulted in the engine crushing the map into a tiny 1KB square. To fix this, `SaveMapSnapshot()` was rewritten to build an invisible, offscreen `SubViewport` entirely in C#.
4. **The Draw Proxy:** Because raw `Texture.GetImage()` does not capture Godot's `_Draw()` functions, we created `MapDrawProxy`. This stamps the colored roads and towns onto the offscreen monitor. We then implemented asynchronous thread-locking (`await ToSignal(RenderingServer...)`) to force the code to wait for the GPU to physically paint the roads before snapping the PNG.
**Outputs:**
* `MapData_Seed_[X].dat` - The binary map data for the Server.
* `Map_Seed_[X].png` - The 4096 high-resolution visual render.
---
## ⚠️ Scripting Rules
1. **No Magic Numbers:** Any distance, radius, or length must be calculated as a percentage of `MapSize` or use the `scaleFactor`.
2. **GPU Respect:** Any script exporting visual data must use `CallDeferred` or `await` frames to ensure the Godot rendering engine has finished processing.

1
icon.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>

After

Width:  |  Height:  |  Size: 995 B

43
icon.svg.import Normal file
View file

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dsbi1fbmyn4o1"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

7
islaApocolypse.csproj Normal file
View file

@ -0,0 +1,7 @@
<Project Sdk="Godot.NET.Sdk/4.7.1">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'android' ">net9.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,7 @@
<Project Sdk="Godot.NET.Sdk/4.6.1">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'android' ">net9.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
</Project>

19
islaApocolypse.sln Normal file
View file

@ -0,0 +1,19 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "islaApocolypse", "islaApocolypse.csproj", "{627E1C36-76D1-4AFE-A400-580FC002DDED}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
ExportDebug|Any CPU = ExportDebug|Any CPU
ExportRelease|Any CPU = ExportRelease|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{627E1C36-76D1-4AFE-A400-580FC002DDED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{627E1C36-76D1-4AFE-A400-580FC002DDED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{627E1C36-76D1-4AFE-A400-580FC002DDED}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
{627E1C36-76D1-4AFE-A400-580FC002DDED}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
{627E1C36-76D1-4AFE-A400-580FC002DDED}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
{627E1C36-76D1-4AFE-A400-580FC002DDED}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
EndGlobalSection
EndGlobal

34
project.godot Normal file
View file

@ -0,0 +1,34 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="islaApocolypse"
run/main_scene="uid://croq62eckpnvx"
config/features=PackedStringArray("4.7", "C#", "Forward Plus")
config/icon="res://icon.svg"
[display]
window/size/viewport_width=1024
window/size/viewport_height=768
window/stretch/mode="viewport"
[dotnet]
project/assembly_name="islaApocolypse"
[physics]
3d/physics_engine="Jolt Physics"
[rendering]
rendering_device/driver.windows="d3d12"