M2: World map render in Godot
Renders the full worldgen output as a Godot scene at visual parity with
worldgen-dump's PNG output: biome tiles, rivers/roads/rails as Line2D
polylines, settlements as filled circles. Pan + zoom via Camera2D.
WorldMapView.cs:
- Loads Content/Data via res:// walk-up, runs WorldGenerator.RunAll
- Tile palette built from BiomeDef.ParsedColor() — same source as the
PNG dump, so colours are identical
- Tiles rendered as a 256x256 Image scaled by WORLD_TILE_PIXELS to
cover world-pixel space (matches polyline coord system)
- Polyline draw order mirrors LineFeatureRenderer.cs: roads (smaller
first) -> rivers -> rail tie underlay -> rail line. Bridges as
short Line2Ds; settlements as SettlementDot (Node2D + _Draw circle)
- Line widths in world-pixel space, tuned for visibility at world-map
zoom; M4 will add zoom-aware width scaling for tactical view
- Camera fits the whole world (95% of viewport) on first frame
PanZoomCamera.cs:
- Mouse-wheel zoom centered on cursor (cursor world-point stays fixed)
- Middle/right click + drag to pan
- MinZoom/MaxZoom configurable per-instance
Main.cs:
- --world-map [seed] flag launches the view (default seed 12345)
- Arg parser now reads both GetCmdlineArgs and GetCmdlineUserArgs so
callers don't need to remember the "--" separator
- --smoke-test path and M0 hello-world fallback unchanged
Visual diff against world_seed12345.png (generated by
worldgen-dump --seed 12345) confirmed manually: same biome palette, same
rivers/roads topology, same settlement placement and tier colours.
3 rivers, 91 roads, 226 settlements (138 PoIs), 0 rails (ENABLE_RAIL=false),
0 bridges (this seed has no road/river crossings). All match the PNG.
Settlement dot sizes iterated twice from user feedback — final values
in tile units, scaled to world-pixel space, so they shrink at world-map
zoom and grow toward tactical zoom (the right "scale with the map"
behaviour).
Closes M2 of theriapolis-rpg-implementation-plan-godot-port.md.
Next: M3 (asset pipeline).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using Godot;
|
using Godot;
|
||||||
|
using Theriapolis.GodotHost.Rendering;
|
||||||
|
|
||||||
namespace Theriapolis.GodotHost;
|
namespace Theriapolis.GodotHost;
|
||||||
|
|
||||||
@@ -6,8 +7,17 @@ public partial class Main : Node
|
|||||||
{
|
{
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
var args = OS.GetCmdlineUserArgs();
|
// GetCmdlineArgs returns every arg (Godot's own flags + ours);
|
||||||
|
// GetCmdlineUserArgs only returns args after a "--" separator.
|
||||||
|
// Use the union so users don't have to remember the separator.
|
||||||
|
var userArgs = OS.GetCmdlineUserArgs();
|
||||||
|
var allArgs = OS.GetCmdlineArgs();
|
||||||
|
var args = new string[userArgs.Length + allArgs.Length];
|
||||||
|
userArgs.CopyTo(args, 0);
|
||||||
|
allArgs.CopyTo(args, userArgs.Length);
|
||||||
|
|
||||||
ulong? smokeTestSeed = null;
|
ulong? smokeTestSeed = null;
|
||||||
|
ulong? worldMapSeed = null;
|
||||||
for (int i = 0; i < args.Length; i++)
|
for (int i = 0; i < args.Length; i++)
|
||||||
{
|
{
|
||||||
if (args[i] == "--smoke-test")
|
if (args[i] == "--smoke-test")
|
||||||
@@ -18,6 +28,14 @@ public partial class Main : Node
|
|||||||
smokeTestSeed = seed;
|
smokeTestSeed = seed;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (args[i] == "--world-map")
|
||||||
|
{
|
||||||
|
ulong seed = 12345UL;
|
||||||
|
if (i + 1 < args.Length && ulong.TryParse(args[i + 1], out var parsed))
|
||||||
|
seed = parsed;
|
||||||
|
worldMapSeed = seed;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (smokeTestSeed.HasValue)
|
if (smokeTestSeed.HasValue)
|
||||||
@@ -27,6 +45,15 @@ public partial class Main : Node
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (worldMapSeed.HasValue)
|
||||||
|
{
|
||||||
|
// Replace the M0 hello-world children with the M2 world-map view.
|
||||||
|
foreach (Node child in GetChildren())
|
||||||
|
child.QueueFree();
|
||||||
|
AddChild(new WorldMapView(worldMapSeed.Value));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
GD.Print("Theriapolis.Godot host ready (M0 hello-world).");
|
GD.Print("Theriapolis.Godot host ready (M0 hello-world).");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Theriapolis.GodotHost.Rendering;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Camera2D with mouse-wheel zoom and middle/right-button click-drag pan.
|
||||||
|
/// Zoom is centered on the cursor so zooming feels natural. Bounds:
|
||||||
|
/// the zoom factor is clamped between MinZoom (everything fits) and
|
||||||
|
/// MaxZoom (1 tile fills the screen).
|
||||||
|
/// </summary>
|
||||||
|
public partial class PanZoomCamera : Camera2D
|
||||||
|
{
|
||||||
|
[Export] public float MinZoom { get; set; } = 0.04f;
|
||||||
|
[Export] public float MaxZoom { get; set; } = 4.0f;
|
||||||
|
[Export] public float ZoomStep { get; set; } = 1.15f;
|
||||||
|
|
||||||
|
private bool _dragging;
|
||||||
|
private Vector2 _dragStartCursor;
|
||||||
|
private Vector2 _dragStartCameraPos;
|
||||||
|
|
||||||
|
public override void _UnhandledInput(InputEvent @event)
|
||||||
|
{
|
||||||
|
switch (@event)
|
||||||
|
{
|
||||||
|
case InputEventMouseButton mb when mb.Pressed:
|
||||||
|
HandleMouseButtonPressed(mb);
|
||||||
|
break;
|
||||||
|
case InputEventMouseButton mb when !mb.Pressed:
|
||||||
|
if (mb.ButtonIndex is MouseButton.Middle or MouseButton.Right)
|
||||||
|
_dragging = false;
|
||||||
|
break;
|
||||||
|
case InputEventMouseMotion mm when _dragging:
|
||||||
|
Position = _dragStartCameraPos
|
||||||
|
+ (_dragStartCursor - mm.Position) / Zoom;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleMouseButtonPressed(InputEventMouseButton mb)
|
||||||
|
{
|
||||||
|
switch (mb.ButtonIndex)
|
||||||
|
{
|
||||||
|
case MouseButton.WheelUp:
|
||||||
|
ApplyZoom(ZoomStep, mb.Position);
|
||||||
|
break;
|
||||||
|
case MouseButton.WheelDown:
|
||||||
|
ApplyZoom(1f / ZoomStep, mb.Position);
|
||||||
|
break;
|
||||||
|
case MouseButton.Middle:
|
||||||
|
case MouseButton.Right:
|
||||||
|
_dragging = true;
|
||||||
|
_dragStartCursor = mb.Position;
|
||||||
|
_dragStartCameraPos = Position;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyZoom(float factor, Vector2 cursorScreen)
|
||||||
|
{
|
||||||
|
float newZoom = Mathf.Clamp(Zoom.X * factor, MinZoom, MaxZoom);
|
||||||
|
if (Mathf.IsEqualApprox(newZoom, Zoom.X)) return;
|
||||||
|
|
||||||
|
// Zoom toward the cursor: keep the world point under the cursor fixed.
|
||||||
|
Vector2 worldBefore = GetCanvasTransform().AffineInverse() * cursorScreen;
|
||||||
|
Zoom = new Vector2(newZoom, newZoom);
|
||||||
|
Vector2 worldAfter = GetCanvasTransform().AffineInverse() * cursorScreen;
|
||||||
|
Position += worldBefore - worldAfter;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
using Godot;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Theriapolis.Core;
|
||||||
|
using Theriapolis.Core.Util;
|
||||||
|
using Theriapolis.Core.World;
|
||||||
|
using Theriapolis.Core.World.Generation;
|
||||||
|
using Theriapolis.Core.World.Polylines;
|
||||||
|
|
||||||
|
namespace Theriapolis.GodotHost.Rendering;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// M2 world-map view. Runs the full worldgen pipeline for a seed and
|
||||||
|
/// renders it: biome tiles as a scaled ImageTexture sprite, polylines
|
||||||
|
/// (rivers/roads/rails) as Line2D children, settlements + bridges as
|
||||||
|
/// dot markers. Camera fits the whole world on first frame and supports
|
||||||
|
/// pan + zoom.
|
||||||
|
///
|
||||||
|
/// The visual diff target is
|
||||||
|
/// <c>dotnet run --project Theriapolis.Tools -- worldgen-dump --seed N</c>:
|
||||||
|
/// same biome colours, same polyline topology and classification colours.
|
||||||
|
/// Widths are tuned for visibility at world-map zoom (one tile ≈ 1 screen
|
||||||
|
/// pixel) — they become chunky at tactical zoom, which M4 will fix with
|
||||||
|
/// zoom-aware width scaling.
|
||||||
|
/// </summary>
|
||||||
|
public partial class WorldMapView : Node2D
|
||||||
|
{
|
||||||
|
private readonly ulong _seed;
|
||||||
|
|
||||||
|
// Polyline colours mirror Theriapolis.Game/Rendering/LineFeatureRenderer.cs
|
||||||
|
// and Theriapolis.Tools/Commands/WorldgenDump.cs (the PNG visual-diff target).
|
||||||
|
private static readonly Color RiverMajorColour = ColorByte(40, 100, 200);
|
||||||
|
private static readonly Color RiverColour = ColorByte(60, 120, 200);
|
||||||
|
private static readonly Color StreamColour = ColorByte(100, 150, 220);
|
||||||
|
private static readonly Color HighwayColour = ColorByte(210, 180, 80);
|
||||||
|
private static readonly Color PostRoadColour = ColorByte(180, 155, 70);
|
||||||
|
private static readonly Color DirtRoadColour = ColorByte(150, 130, 90);
|
||||||
|
private static readonly Color RailColour = ColorByte(80, 65, 50);
|
||||||
|
private static readonly Color BridgeColour = ColorByte(160, 140, 100);
|
||||||
|
|
||||||
|
// Line widths in world-pixel space, tuned for visibility at world-map
|
||||||
|
// zoom. WORLD_TILE_PIXELS is 32, so a 32-px line is "1 tile thick".
|
||||||
|
private const float RiverMajorWidth = 48f;
|
||||||
|
private const float RiverWidth = 32f;
|
||||||
|
private const float StreamWidth = 20f;
|
||||||
|
private const float HighwayWidth = 32f;
|
||||||
|
private const float PostRoadWidth = 24f;
|
||||||
|
private const float DirtRoadWidth = 16f;
|
||||||
|
private const float RailTieWidth = 32f;
|
||||||
|
private const float RailLineWidth = 14f;
|
||||||
|
|
||||||
|
public WorldMapView(ulong seed) => _seed = seed;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
string dataDir = ResolveDataDir();
|
||||||
|
if (!Directory.Exists(dataDir))
|
||||||
|
{
|
||||||
|
GD.PrintErr($"[world-map] Data directory not found: {dataDir}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GD.Print($"[world-map] seed=0x{_seed:X} data-dir={dataDir}");
|
||||||
|
var ctx = new WorldGenContext(_seed, dataDir);
|
||||||
|
WorldGenerator.RunAll(ctx);
|
||||||
|
var world = ctx.World;
|
||||||
|
GD.Print($"[world-map] worldgen done — rivers={world.Rivers.Count} " +
|
||||||
|
$"roads={world.Roads.Count} rails={world.Rails.Count} " +
|
||||||
|
$"settlements={world.Settlements.Count} bridges={world.Bridges.Count}");
|
||||||
|
|
||||||
|
BuildTileSprite(world);
|
||||||
|
BuildPolylines(world);
|
||||||
|
BuildBridges(world);
|
||||||
|
BuildSettlements(world);
|
||||||
|
AddCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildTileSprite(WorldState world)
|
||||||
|
{
|
||||||
|
int W = C.WORLD_WIDTH_TILES;
|
||||||
|
int H = C.WORLD_HEIGHT_TILES;
|
||||||
|
|
||||||
|
// Build biome -> Color map from the loaded BiomeDef[]. Mirrors
|
||||||
|
// WorldgenDump.BuildColorMap so the PNG and the Godot view share
|
||||||
|
// the exact same palette source.
|
||||||
|
var palette = new Color[(int)BiomeId.Mangrove + 1];
|
||||||
|
foreach (var def in world.BiomeDefs!)
|
||||||
|
{
|
||||||
|
var (r, g, b) = def.ParsedColor();
|
||||||
|
int id = (int)ParseBiomeId(def.Id);
|
||||||
|
if (id >= 0 && id < palette.Length) palette[id] = ColorByte(r, g, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
var image = Image.CreateEmpty(W, H, false, Image.Format.Rgb8);
|
||||||
|
for (int y = 0; y < H; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < W; x++)
|
||||||
|
{
|
||||||
|
int id = (int)world.Tiles[x, y].Biome;
|
||||||
|
Color c = (id >= 0 && id < palette.Length && palette[id].A > 0f)
|
||||||
|
? palette[id]
|
||||||
|
: ColorByte(255, 0, 255); // magenta for any unmapped biome
|
||||||
|
image.SetPixel(x, y, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tex = ImageTexture.CreateFromImage(image);
|
||||||
|
var sprite = new Sprite2D
|
||||||
|
{
|
||||||
|
Texture = tex,
|
||||||
|
Centered = false,
|
||||||
|
// Scale so 1 image pixel = WORLD_TILE_PIXELS world pixels,
|
||||||
|
// matching the polyline coordinate space.
|
||||||
|
Scale = new Vector2(C.WORLD_TILE_PIXELS, C.WORLD_TILE_PIXELS),
|
||||||
|
TextureFilter = TextureFilterEnum.Nearest,
|
||||||
|
};
|
||||||
|
AddChild(sprite);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildPolylines(WorldState world)
|
||||||
|
{
|
||||||
|
var polylineLayer = new Node2D { Name = "Polylines" };
|
||||||
|
AddChild(polylineLayer);
|
||||||
|
|
||||||
|
// Draw order: roads (smaller first) → rivers → rail. Rail on top
|
||||||
|
// mirrors LineFeatureRenderer's order so junctions look the same.
|
||||||
|
foreach (var road in world.Roads.OrderBy(RoadDrawRank))
|
||||||
|
{
|
||||||
|
var (color, width) = road.RoadClassification switch
|
||||||
|
{
|
||||||
|
RoadType.Highway => (HighwayColour, HighwayWidth),
|
||||||
|
RoadType.PostRoad => (PostRoadColour, PostRoadWidth),
|
||||||
|
_ => (DirtRoadColour, DirtRoadWidth),
|
||||||
|
};
|
||||||
|
polylineLayer.AddChild(MakeLine(road.Points, color, width));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var river in world.Rivers)
|
||||||
|
{
|
||||||
|
var (color, baseWidth) = river.RiverClassification switch
|
||||||
|
{
|
||||||
|
RiverClass.MajorRiver => (RiverMajorColour, RiverMajorWidth),
|
||||||
|
RiverClass.River => (RiverColour, RiverWidth),
|
||||||
|
_ => (StreamColour, StreamWidth),
|
||||||
|
};
|
||||||
|
float flowScale = 1f + (river.FlowAccumulation / (float)C.RIVER_MAJOR_THRESHOLD) * 0.3f;
|
||||||
|
float width = Mathf.Min(baseWidth * flowScale, RiverMajorWidth * 1.5f);
|
||||||
|
polylineLayer.AddChild(MakeLine(river.Points, color, width));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var rail in world.Rails)
|
||||||
|
{
|
||||||
|
// Tie underlay first, rail line on top. Same two-pass approach
|
||||||
|
// LineFeatureRenderer.DrawRail uses.
|
||||||
|
polylineLayer.AddChild(MakeLine(rail.Points, ColorByte(120, 100, 80), RailTieWidth));
|
||||||
|
polylineLayer.AddChild(MakeLine(rail.Points, RailColour, RailLineWidth));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildBridges(WorldState world)
|
||||||
|
{
|
||||||
|
if (world.Bridges.Count == 0) return;
|
||||||
|
var layer = new Node2D { Name = "Bridges" };
|
||||||
|
AddChild(layer);
|
||||||
|
|
||||||
|
foreach (var bridge in world.Bridges)
|
||||||
|
{
|
||||||
|
var line = new Line2D
|
||||||
|
{
|
||||||
|
Width = 56f, // wider than HighwayWidth so the deck visibly covers the road
|
||||||
|
DefaultColor = BridgeColour,
|
||||||
|
};
|
||||||
|
line.AddPoint(new Vector2(bridge.Start.X, bridge.Start.Y));
|
||||||
|
line.AddPoint(new Vector2(bridge.End.X, bridge.End.Y));
|
||||||
|
layer.AddChild(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildSettlements(WorldState world)
|
||||||
|
{
|
||||||
|
if (world.Settlements.Count == 0) return;
|
||||||
|
var layer = new Node2D { Name = "Settlements" };
|
||||||
|
AddChild(layer);
|
||||||
|
|
||||||
|
foreach (var s in world.Settlements)
|
||||||
|
{
|
||||||
|
// Radii are in tile units (matching WorldgenDump.cs DrawDot which
|
||||||
|
// works at 1 px/tile). Multiplied by WORLD_TILE_PIXELS to translate
|
||||||
|
// into the polyline coordinate space; the dots then scale naturally
|
||||||
|
// with the Camera2D zoom (1 tile-unit = WORLD_TILE_PIXELS world px,
|
||||||
|
// scales down to ~1 screen px at world-map zoom, up at tactical zoom).
|
||||||
|
var (colour, tileRadius) = s.Tier switch
|
||||||
|
{
|
||||||
|
1 => (ColorByte(255, 215, 0), 2.5f), // gold, capital
|
||||||
|
2 => (ColorByte(230, 230, 230), 1.8f),// white, city
|
||||||
|
3 => (ColorByte(150, 200, 255), 1.3f),// blue, town
|
||||||
|
4 => (ColorByte(200, 200, 200), 0.8f),// grey, village
|
||||||
|
_ => (ColorByte(200, 60, 60), 0.7f), // red, PoI
|
||||||
|
};
|
||||||
|
float radius = tileRadius * C.WORLD_TILE_PIXELS;
|
||||||
|
|
||||||
|
var dot = new SettlementDot
|
||||||
|
{
|
||||||
|
Position = new Vector2(
|
||||||
|
s.TileX * C.WORLD_TILE_PIXELS + C.WORLD_TILE_PIXELS * 0.5f,
|
||||||
|
s.TileY * C.WORLD_TILE_PIXELS + C.WORLD_TILE_PIXELS * 0.5f),
|
||||||
|
Radius = radius,
|
||||||
|
FillColor = colour,
|
||||||
|
};
|
||||||
|
layer.AddChild(dot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddCamera()
|
||||||
|
{
|
||||||
|
var worldSizeWorldPixels = new Vector2(
|
||||||
|
C.WORLD_WIDTH_TILES * C.WORLD_TILE_PIXELS,
|
||||||
|
C.WORLD_HEIGHT_TILES * C.WORLD_TILE_PIXELS);
|
||||||
|
Vector2 viewport = GetViewport().GetVisibleRect().Size;
|
||||||
|
float fitZoom = Mathf.Min(
|
||||||
|
viewport.X / worldSizeWorldPixels.X,
|
||||||
|
viewport.Y / worldSizeWorldPixels.Y) * 0.95f;
|
||||||
|
|
||||||
|
var camera = new PanZoomCamera
|
||||||
|
{
|
||||||
|
Position = worldSizeWorldPixels * 0.5f,
|
||||||
|
Zoom = new Vector2(fitZoom, fitZoom),
|
||||||
|
MinZoom = fitZoom * 0.5f,
|
||||||
|
MaxZoom = 4.0f,
|
||||||
|
};
|
||||||
|
AddChild(camera);
|
||||||
|
camera.MakeCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Line2D MakeLine(System.Collections.Generic.IReadOnlyList<Vec2> pts, Color color, float width)
|
||||||
|
{
|
||||||
|
var line = new Line2D
|
||||||
|
{
|
||||||
|
Width = width,
|
||||||
|
DefaultColor = color,
|
||||||
|
JointMode = Line2D.LineJointMode.Round,
|
||||||
|
BeginCapMode = Line2D.LineCapMode.Round,
|
||||||
|
EndCapMode = Line2D.LineCapMode.Round,
|
||||||
|
Antialiased = false,
|
||||||
|
};
|
||||||
|
for (int i = 0; i < pts.Count; i++)
|
||||||
|
line.AddPoint(new Vector2(pts[i].X, pts[i].Y));
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int RoadDrawRank(Polyline r) => r.RoadClassification switch
|
||||||
|
{
|
||||||
|
RoadType.Footpath => 0,
|
||||||
|
RoadType.DirtRoad => 1,
|
||||||
|
RoadType.PostRoad => 2,
|
||||||
|
RoadType.Highway => 3,
|
||||||
|
_ => 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Color ColorByte(byte r, byte g, byte b) =>
|
||||||
|
new(r / 255f, g / 255f, b / 255f);
|
||||||
|
|
||||||
|
private static BiomeId ParseBiomeId(string id) => id.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"ocean" => BiomeId.Ocean,
|
||||||
|
"tundra" => BiomeId.Tundra,
|
||||||
|
"boreal" => BiomeId.Boreal,
|
||||||
|
"temperate_deciduous" => BiomeId.TemperateDeciduous,
|
||||||
|
"temperate_grassland" => BiomeId.TemperateGrassland,
|
||||||
|
"mountain_alpine" => BiomeId.MountainAlpine,
|
||||||
|
"mountain_forested" => BiomeId.MountainForested,
|
||||||
|
"subtropical_forest" => BiomeId.SubtropicalForest,
|
||||||
|
"wetland" => BiomeId.Wetland,
|
||||||
|
"coastal" => BiomeId.Coastal,
|
||||||
|
"river_valley" => BiomeId.RiverValley,
|
||||||
|
"scrubland" => BiomeId.Scrubland,
|
||||||
|
"desert_cold" => BiomeId.DesertCold,
|
||||||
|
"forest_edge" => BiomeId.ForestEdge,
|
||||||
|
"foothills" => BiomeId.Foothills,
|
||||||
|
"marsh_edge" => BiomeId.MarshEdge,
|
||||||
|
"beach" => BiomeId.Beach,
|
||||||
|
"cliff" => BiomeId.Cliff,
|
||||||
|
"tidal_flat" => BiomeId.TidalFlat,
|
||||||
|
"mangrove" => BiomeId.Mangrove,
|
||||||
|
_ => BiomeId.TemperateGrassland,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string ResolveDataDir()
|
||||||
|
{
|
||||||
|
string fromRes = ProjectSettings.GlobalizePath("res://../Content/Data");
|
||||||
|
if (Directory.Exists(fromRes)) return fromRes;
|
||||||
|
|
||||||
|
string? dir = ProjectSettings.GlobalizePath("res://").TrimEnd('/', '\\');
|
||||||
|
for (int i = 0; i < 6; i++)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(dir)) break;
|
||||||
|
string candidate = Path.Combine(dir, "Content", "Data");
|
||||||
|
if (Directory.Exists(candidate)) return candidate;
|
||||||
|
dir = Path.GetDirectoryName(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fromRes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filled circle used as a settlement marker on the world map.
|
||||||
|
/// </summary>
|
||||||
|
public partial class SettlementDot : Node2D
|
||||||
|
{
|
||||||
|
public float Radius { get; set; } = 8f;
|
||||||
|
public Color FillColor { get; set; } = Colors.White;
|
||||||
|
|
||||||
|
public override void _Draw() =>
|
||||||
|
DrawCircle(Vector2.Zero, Radius, FillColor);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user