add cellular automata generator

This commit is contained in:
2026-04-26 04:20:06 +02:00
parent 7f0869ffa4
commit b2a406ca2b
17 changed files with 937 additions and 1 deletions

View File

@@ -0,0 +1,131 @@
using System;
namespace Mr.BrickAdventures.Tools.CaLevelGenerator;
public static class CaGenerator
{
public static bool[,] Generate(CaGeneratorSettings s)
{
var rng = new Random(s.Seed);
return s.Mode switch
{
CaMode.Cave => GenerateCave(s, rng),
CaMode.Platform => GeneratePlatform(s, rng),
CaMode.Terrain => GenerateTerrain(s, rng),
_ => GenerateCave(s, rng),
};
}
public static bool[,] Smooth(bool[,] grid) => SmoothPass(grid, 5);
// ── Cave ──────────────────────────────────────────────────────────────
private static bool[,] GenerateCave(CaGeneratorSettings s, Random rng)
{
var grid = RandomFill(s.Width, s.Height, s.FillDensity, rng);
if (s.BorderWalls) EnforceBorder(grid);
for (int i = 0; i < s.SmoothingPasses; i++)
{
grid = SmoothPass(grid, 5);
if (s.BorderWalls) EnforceBorder(grid);
}
return grid;
}
// ── Platform ──────────────────────────────────────────────────────────
private static bool[,] GeneratePlatform(CaGeneratorSettings s, Random rng)
{
int w = s.Width, h = s.Height;
var grid = new bool[w, h];
for (int x = 0; x < w; x++)
{
grid[x, h - 1] = true;
grid[x, h - 2] = true;
for (int y = 0; y < h - 2; y++)
{
// Density linearly increases from 10% at top to 60% at bottom,
// scaled by user's FillDensity relative to default 0.48.
float t = (float)y / (h - 3);
float rowDensity = (0.1f + t * 0.5f) * (s.FillDensity / 0.48f);
grid[x, y] = rng.NextDouble() < rowDensity;
}
}
for (int i = 0; i < s.SmoothingPasses; i++)
{
// Threshold 4 (not 5): horizontal platform groups survive, isolated
// vertical pixels collapse into flat platforms.
grid = SmoothPass(grid, 4);
for (int x = 0; x < w; x++) { grid[x, h - 1] = true; grid[x, h - 2] = true; }
}
return grid;
}
// ── Terrain ───────────────────────────────────────────────────────────
private static bool[,] GenerateTerrain(CaGeneratorSettings s, Random rng)
{
int w = s.Width, h = s.Height;
var grid = new bool[w, h];
// Start terrain surface at ~60% from bottom (40% from top)
int surfaceRow = (int)(h * 0.4f);
for (int x = 0; x < w; x++)
{
int delta = rng.Next(-2, 3);
surfaceRow = Math.Clamp(surfaceRow + delta, h / 6, h - 2);
for (int y = surfaceRow; y < h; y++)
grid[x, y] = true;
}
for (int i = 0; i < s.SmoothingPasses; i++)
grid = SmoothPass(grid, 5);
return grid;
}
// ── Shared helpers ────────────────────────────────────────────────
private static bool[,] RandomFill(int w, int h, float density, Random rng)
{
var grid = new bool[w, h];
for (int x = 0; x < w; x++)
for (int y = 0; y < h; y++)
grid[x, y] = rng.NextDouble() < density;
return grid;
}
private static bool[,] SmoothPass(bool[,] grid, int threshold)
{
int w = grid.GetLength(0), h = grid.GetLength(1);
var next = new bool[w, h];
for (int x = 0; x < w; x++)
for (int y = 0; y < h; y++)
next[x, y] = CountSolidNeighbors(grid, x, y) >= threshold;
return next;
}
private static int CountSolidNeighbors(bool[,] grid, int cx, int cy)
{
int w = grid.GetLength(0), h = grid.GetLength(1), count = 0;
for (int dx = -1; dx <= 1; dx++)
for (int dy = -1; dy <= 1; dy++)
{
if (dx == 0 && dy == 0) continue;
int nx = cx + dx, ny = cy + dy;
// Out-of-bounds counts as solid (closes cave edges naturally)
if (nx < 0 || nx >= w || ny < 0 || ny >= h) count++;
else if (grid[nx, ny]) count++;
}
return count;
}
private static void EnforceBorder(bool[,] grid)
{
int w = grid.GetLength(0), h = grid.GetLength(1);
for (int x = 0; x < w; x++) { grid[x, 0] = true; grid[x, h - 1] = true; }
for (int y = 0; y < h; y++) { grid[0, y] = true; grid[w - 1, y] = true; }
}
}

View File

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

View File

@@ -0,0 +1,23 @@
using Godot;
namespace Mr.BrickAdventures.Tools.CaLevelGenerator;
public enum CaMode { Cave, Platform, Terrain }
public class CaGeneratorSettings
{
public CaMode Mode { get; set; } = CaMode.Cave;
public int Width { get; set; } = 40;
public int Height { get; set; } = 22;
public Vector2I Offset { get; set; } = Vector2I.Zero;
public float FillDensity { get; set; } = 0.48f;
public int SmoothingPasses { get; set; } = 4;
public bool BorderWalls { get; set; } = true;
public int Seed { get; set; } = 12345;
// TerrainSet == -1 → raw tile mode
public int TerrainSet { get; set; } = 0;
public int Terrain { get; set; } = 0;
// Used only when TerrainSet == -1
public int SourceId { get; set; } = 0;
public Vector2I AtlasCoords { get; set; } = Vector2I.Zero;
}

View File

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

View File

@@ -0,0 +1,79 @@
#if TOOLS
using Godot;
namespace Mr.BrickAdventures.Tools.CaLevelGenerator;
[Tool]
public partial class CaGeneratorTestRunner : Node
{
public override void _Ready()
{
int pass = 0, fail = 0;
void Assert(bool cond, string msg)
{
if (cond) { GD.Print($" PASS: {msg}"); pass++; }
else { GD.PrintErr($" FAIL: {msg}"); fail++; }
}
GD.Print("=== CaGenerator Tests ===");
// Grid dimensions
var s = new CaGeneratorSettings { Width = 20, Height = 15, SmoothingPasses = 0, Seed = 42 };
var g = CaGenerator.Generate(s);
Assert(g.GetLength(0) == 20, "Width == 20");
Assert(g.GetLength(1) == 15, "Height == 15");
// Determinism
var g2 = CaGenerator.Generate(s);
bool det = true;
for (int x = 0; x < 20 && det; x++)
for (int y = 0; y < 15 && det; y++)
if (g[x, y] != g2[x, y]) det = false;
Assert(det, "Same seed → same output");
// Different seeds → different output
var s2 = new CaGeneratorSettings { Width = 20, Height = 15, SmoothingPasses = 0, Seed = 99 };
var g3 = CaGenerator.Generate(s2);
bool diff = false;
for (int x = 0; x < 20 && !diff; x++)
for (int y = 0; y < 15 && !diff; y++)
if (g[x, y] != g3[x, y]) diff = true;
Assert(diff, "Different seeds → different output");
// Cave border walls
var sb = new CaGeneratorSettings { Width = 20, Height = 15, BorderWalls = true, SmoothingPasses = 0, Seed = 42 };
var gb = CaGenerator.Generate(sb);
bool border = true;
for (int x = 0; x < 20; x++) if (!gb[x, 0] || !gb[x, 14]) { border = false; break; }
for (int y = 0; y < 15; y++) if (!gb[0, y] || !gb[19, y]) { border = false; break; }
Assert(border, "Cave: border cells all solid");
// Platform floor
var sp = new CaGeneratorSettings { Mode = CaMode.Platform, Width = 20, Height = 15, SmoothingPasses = 0, Seed = 42 };
var gp = CaGenerator.Generate(sp);
bool floor = true;
for (int x = 0; x < 20; x++) if (!gp[x, 14] || !gp[x, 13]) { floor = false; break; }
Assert(floor, "Platform: bottom 2 rows solid");
// Platform: top row not all solid (mostly empty at top)
int topSolid = 0;
for (int x = 0; x < 20; x++) if (gp[x, 0]) topSolid++;
Assert(topSolid < 15, "Platform: top row mostly empty");
// Terrain: bottom always solid
var st = new CaGeneratorSettings { Mode = CaMode.Terrain, Width = 20, Height = 15, SmoothingPasses = 0, Seed = 42 };
var gt = CaGenerator.Generate(st);
bool terrBottom = true;
for (int x = 0; x < 20; x++) if (!gt[x, 14]) { terrBottom = false; break; }
Assert(terrBottom, "Terrain: bottom row always solid");
// Smooth: output same dimensions
var smoothed = CaGenerator.Smooth(g);
Assert(smoothed.GetLength(0) == 20, "Smooth: width preserved");
Assert(smoothed.GetLength(1) == 15, "Smooth: height preserved");
GD.Print($"=== Results: {pass} passed, {fail} failed ===");
}
}
#endif

View File

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

View File

@@ -0,0 +1,360 @@
#if TOOLS
using Godot;
namespace Mr.BrickAdventures.Tools.CaLevelGenerator;
[Tool]
public partial class CaLevelGeneratorDock : Control
{
public CaLevelGeneratorPlugin Plugin { get; set; }
// Controls referenced in Task 8 wiring
private OptionButton _layerDropdown;
private OptionButton _terrainDropdown;
private HBoxContainer _rawTileRow;
private SpinBox _sourceIdSpin;
private SpinBox _atlasXSpin;
private SpinBox _atlasYSpin;
private Button _caveBtn;
private Button _platformBtn;
private Button _terrainBtn;
private SpinBox _widthSpin;
private SpinBox _heightSpin;
private SpinBox _offsetXSpin;
private SpinBox _offsetYSpin;
private HSlider _densitySlider;
private Label _densityLabel;
private HSlider _smoothPassSlider;
private Label _smoothPassLabel;
private CheckBox _borderCheck;
private SpinBox _seedSpin;
private Button _genBtn;
private Button _smoothBtn;
private Button _clearBtn;
private CaMode _mode = CaMode.Cave;
private System.Collections.Generic.List<TileMapLayer> _layers = new();
private TileMapLayer _selectedLayer;
public override void _Ready()
{
CustomMinimumSize = new Vector2(200, 0);
var scroll = new ScrollContainer();
scroll.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
scroll.SizeFlagsVertical = SizeFlags.ExpandFill;
AddChild(scroll);
var vbox = new VBoxContainer();
vbox.SizeFlagsHorizontal = SizeFlags.ExpandFill;
scroll.AddChild(vbox);
// Target Layer
vbox.AddChild(new Label { Text = "Target Layer" });
_layerDropdown = new OptionButton();
_layerDropdown.SizeFlagsHorizontal = SizeFlags.ExpandFill;
vbox.AddChild(_layerDropdown);
// Terrain
vbox.AddChild(new Label { Text = "Terrain" });
_terrainDropdown = new OptionButton();
_terrainDropdown.SizeFlagsHorizontal = SizeFlags.ExpandFill;
vbox.AddChild(_terrainDropdown);
// Raw tile row (hidden when terrain is available)
_rawTileRow = new HBoxContainer();
_rawTileRow.Visible = false;
_rawTileRow.AddChild(new Label { Text = "Src" });
_sourceIdSpin = new SpinBox { MinValue = 0, MaxValue = 99, Value = 0 };
_rawTileRow.AddChild(_sourceIdSpin);
_rawTileRow.AddChild(new Label { Text = "X" });
_atlasXSpin = new SpinBox { MinValue = 0, MaxValue = 99, Value = 0 };
_rawTileRow.AddChild(_atlasXSpin);
_rawTileRow.AddChild(new Label { Text = "Y" });
_atlasYSpin = new SpinBox { MinValue = 0, MaxValue = 99, Value = 0 };
_rawTileRow.AddChild(_atlasYSpin);
vbox.AddChild(_rawTileRow);
// Mode toggle
vbox.AddChild(new Label { Text = "Mode" });
var modeRow = new HBoxContainer();
modeRow.SizeFlagsHorizontal = SizeFlags.ExpandFill;
_caveBtn = MakeModeButton("Cave", () => SetMode(CaMode.Cave));
_platformBtn = MakeModeButton("Platform", () => SetMode(CaMode.Platform));
_terrainBtn = MakeModeButton("Terrain", () => SetMode(CaMode.Terrain));
_caveBtn.ButtonPressed = true;
modeRow.AddChild(_caveBtn);
modeRow.AddChild(_platformBtn);
modeRow.AddChild(_terrainBtn);
vbox.AddChild(modeRow);
vbox.AddChild(new HSeparator());
// Width / Height
var dimRow = new HBoxContainer();
dimRow.AddChild(MakeSpinColumn("Width", out _widthSpin, 5, 500, 40));
dimRow.AddChild(MakeSpinColumn("Height", out _heightSpin, 5, 500, 22));
vbox.AddChild(dimRow);
// Offset X / Y
var offRow = new HBoxContainer();
offRow.AddChild(MakeSpinColumn("Offset X", out _offsetXSpin, -9999, 9999, 0));
offRow.AddChild(MakeSpinColumn("Offset Y", out _offsetYSpin, -9999, 9999, 0));
vbox.AddChild(offRow);
// Fill Density
var densHeader = new HBoxContainer();
densHeader.AddChild(new Label { Text = "Fill Density", SizeFlagsHorizontal = SizeFlags.ExpandFill });
_densityLabel = new Label { Text = "48%" };
densHeader.AddChild(_densityLabel);
vbox.AddChild(densHeader);
_densitySlider = new HSlider { MinValue = 0, MaxValue = 100, Value = 48, Step = 1 };
_densitySlider.ValueChanged += v => _densityLabel.Text = $"{(int)v}%";
vbox.AddChild(_densitySlider);
// Smoothing Passes
var smoothHeader = new HBoxContainer();
smoothHeader.AddChild(new Label { Text = "Smoothing Passes", SizeFlagsHorizontal = SizeFlags.ExpandFill });
_smoothPassLabel = new Label { Text = "4" };
smoothHeader.AddChild(_smoothPassLabel);
vbox.AddChild(smoothHeader);
_smoothPassSlider = new HSlider { MinValue = 0, MaxValue = 10, Value = 4, Step = 1 };
_smoothPassSlider.ValueChanged += v => _smoothPassLabel.Text = $"{(int)v}";
vbox.AddChild(_smoothPassSlider);
// Border walls
_borderCheck = new CheckBox { Text = "Solid border walls", ButtonPressed = true };
vbox.AddChild(_borderCheck);
// Seed
vbox.AddChild(new Label { Text = "Seed" });
var seedRow = new HBoxContainer();
_seedSpin = new SpinBox { MinValue = 0, MaxValue = int.MaxValue, Value = 12345,
AllowGreater = true, SizeFlagsHorizontal = SizeFlags.ExpandFill };
var randBtn = new Button { Text = "🎲" };
randBtn.Pressed += () => _seedSpin.Value = GD.Randi();
seedRow.AddChild(_seedSpin);
seedRow.AddChild(randBtn);
vbox.AddChild(seedRow);
vbox.AddChild(new HSeparator());
// Action buttons
_genBtn = new Button { Text = "▶ Generate", SizeFlagsHorizontal = SizeFlags.ExpandFill };
vbox.AddChild(_genBtn);
var actionRow = new HBoxContainer();
actionRow.SizeFlagsHorizontal = SizeFlags.ExpandFill;
_smoothBtn = new Button { Text = "✦ Smooth", SizeFlagsHorizontal = SizeFlags.ExpandFill };
_clearBtn = new Button { Text = "✕ Clear", SizeFlagsHorizontal = SizeFlags.ExpandFill };
actionRow.AddChild(_smoothBtn);
actionRow.AddChild(_clearBtn);
vbox.AddChild(actionRow);
_layerDropdown.ItemSelected += OnLayerSelected;
_terrainDropdown.ItemSelected += _ => SaveSettings();
_genBtn.Pressed += OnGenerate;
_smoothBtn.Pressed += OnSmooth;
_clearBtn.Pressed += OnClear;
LoadSettings();
}
public void OnSceneChanged(Node sceneRoot)
{
ScanLayers(sceneRoot);
RestoreLastLayer(sceneRoot?.SceneFilePath ?? "");
}
private void SetMode(CaMode mode)
{
_mode = mode;
_caveBtn.ButtonPressed = mode == CaMode.Cave;
_platformBtn.ButtonPressed = mode == CaMode.Platform;
_terrainBtn.ButtonPressed = mode == CaMode.Terrain;
}
private static Button MakeModeButton(string text, System.Action onPressed)
{
var btn = new Button { Text = text, ToggleMode = true,
SizeFlagsHorizontal = SizeFlags.ExpandFill };
btn.Pressed += onPressed;
return btn;
}
private static Control MakeSpinColumn(string label, out SpinBox spin,
double min, double max, double value)
{
var col = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
col.AddChild(new Label { Text = label });
spin = new SpinBox { MinValue = min, MaxValue = max, Value = value,
SizeFlagsHorizontal = SizeFlags.ExpandFill };
col.AddChild(spin);
return col;
}
private void ScanLayers(Node root)
{
_layers.Clear();
_layerDropdown.Clear();
if (root == null) return;
CollectLayers(root);
foreach (var layer in _layers)
_layerDropdown.AddItem(layer.Name);
if (_layers.Count > 0) OnLayerSelected(0);
}
private void CollectLayers(Node node)
{
if (node is TileMapLayer layer) _layers.Add(layer);
foreach (Node child in node.GetChildren()) CollectLayers(child);
}
private void OnLayerSelected(long index)
{
if (index < 0 || index >= _layers.Count) return;
_selectedLayer = _layers[(int)index];
PopulateTerrainDropdown(_selectedLayer);
var sceneRoot = EditorInterface.Singleton.GetEditedSceneRoot();
if (sceneRoot != null)
{
var key = $"ca_generator/last_layer_{sceneRoot.SceneFilePath}";
EditorInterface.Singleton.GetEditorSettings()
.SetSetting(key, _selectedLayer.GetPath().ToString());
}
SaveSettings();
}
private void RestoreLastLayer(string sceneFilePath)
{
if (string.IsNullOrEmpty(sceneFilePath) || _layers.Count == 0) return;
var key = $"ca_generator/last_layer_{sceneFilePath}";
var es = EditorInterface.Singleton.GetEditorSettings();
if (!es.HasSetting(key)) return;
var path = es.GetSetting(key).AsString();
for (int i = 0; i < _layers.Count; i++)
{
if (_layers[i].GetPath().ToString() == path)
{
_layerDropdown.Select(i);
OnLayerSelected(i);
return;
}
}
}
private void PopulateTerrainDropdown(TileMapLayer layer)
{
_terrainDropdown.Clear();
var tileSet = layer.TileSet;
bool hasTerrains = false;
if (tileSet != null)
{
for (int ts = 0; ts < tileSet.GetTerrainSetsCount(); ts++)
{
for (int t = 0; t < tileSet.GetTerrainsCount(ts); t++)
{
_terrainDropdown.AddItem(tileSet.GetTerrainName(ts, t));
_terrainDropdown.SetItemMetadata(
_terrainDropdown.ItemCount - 1,
Variant.From((ts << 16) | t));
hasTerrains = true;
}
}
}
_terrainDropdown.Visible = hasTerrains;
_rawTileRow.Visible = !hasTerrains;
}
private CaGeneratorSettings BuildSettings()
{
int terrainSet = -1, terrain = 0;
if (_terrainDropdown.Visible && _terrainDropdown.ItemCount > 0)
{
int packed = _terrainDropdown.GetSelectedMetadata().AsInt32();
terrainSet = packed >> 16;
terrain = packed & 0xFFFF;
}
return new CaGeneratorSettings
{
Mode = _mode,
Width = (int)_widthSpin.Value,
Height = (int)_heightSpin.Value,
Offset = new Vector2I((int)_offsetXSpin.Value, (int)_offsetYSpin.Value),
FillDensity = (float)_densitySlider.Value / 100f,
SmoothingPasses = (int)_smoothPassSlider.Value,
BorderWalls = _borderCheck.ButtonPressed,
Seed = (int)_seedSpin.Value,
TerrainSet = terrainSet,
Terrain = terrain,
SourceId = (int)_sourceIdSpin.Value,
AtlasCoords = new Vector2I((int)_atlasXSpin.Value, (int)_atlasYSpin.Value),
};
}
private void OnGenerate()
{
if (_selectedLayer == null) return;
var settings = BuildSettings();
var grid = CaGenerator.Generate(settings);
TilemapPainter.Paint(_selectedLayer, grid, settings, Plugin.UndoRedo);
SaveSettings();
}
private void OnSmooth()
{
if (_selectedLayer == null) return;
TilemapPainter.Smooth(_selectedLayer, BuildSettings(), Plugin.UndoRedo);
}
private void OnClear()
{
if (_selectedLayer == null) return;
TilemapPainter.Clear(_selectedLayer, BuildSettings(), Plugin.UndoRedo);
}
private void SaveSettings()
{
var es = EditorInterface.Singleton.GetEditorSettings();
es.SetSetting("ca_generator/mode", (int)_mode);
es.SetSetting("ca_generator/width", (int)_widthSpin.Value);
es.SetSetting("ca_generator/height", (int)_heightSpin.Value);
es.SetSetting("ca_generator/offset_x", (int)_offsetXSpin.Value);
es.SetSetting("ca_generator/offset_y", (int)_offsetYSpin.Value);
es.SetSetting("ca_generator/fill_density", _densitySlider.Value);
es.SetSetting("ca_generator/smoothing_passes", (int)_smoothPassSlider.Value);
es.SetSetting("ca_generator/border_walls", _borderCheck.ButtonPressed);
es.SetSetting("ca_generator/seed", (int)_seedSpin.Value);
es.SetSetting("ca_generator/source_id", (int)_sourceIdSpin.Value);
es.SetSetting("ca_generator/atlas_x", (int)_atlasXSpin.Value);
es.SetSetting("ca_generator/atlas_y", (int)_atlasYSpin.Value);
}
private void LoadSettings()
{
var es = EditorInterface.Singleton.GetEditorSettings();
if (es == null) return;
SetMode((CaMode)(es.HasSetting("ca_generator/mode") ? es.GetSetting("ca_generator/mode").AsInt32() : 0));
_widthSpin.Value = es.HasSetting("ca_generator/width") ? es.GetSetting("ca_generator/width").AsDouble() : 40.0;
_heightSpin.Value = es.HasSetting("ca_generator/height") ? es.GetSetting("ca_generator/height").AsDouble() : 22.0;
_offsetXSpin.Value = es.HasSetting("ca_generator/offset_x") ? es.GetSetting("ca_generator/offset_x").AsDouble() : 0.0;
_offsetYSpin.Value = es.HasSetting("ca_generator/offset_y") ? es.GetSetting("ca_generator/offset_y").AsDouble() : 0.0;
_densitySlider.Value = es.HasSetting("ca_generator/fill_density") ? es.GetSetting("ca_generator/fill_density").AsDouble() : 48.0;
_smoothPassSlider.Value = es.HasSetting("ca_generator/smoothing_passes") ? es.GetSetting("ca_generator/smoothing_passes").AsDouble() : 4.0;
_borderCheck.ButtonPressed = es.HasSetting("ca_generator/border_walls") ? es.GetSetting("ca_generator/border_walls").AsBool() : true;
_seedSpin.Value = es.HasSetting("ca_generator/seed") ? es.GetSetting("ca_generator/seed").AsDouble() : 12345.0;
_sourceIdSpin.Value = es.HasSetting("ca_generator/source_id") ? es.GetSetting("ca_generator/source_id").AsDouble() : 0.0;
_atlasXSpin.Value = es.HasSetting("ca_generator/atlas_x") ? es.GetSetting("ca_generator/atlas_x").AsDouble() : 0.0;
_atlasYSpin.Value = es.HasSetting("ca_generator/atlas_y") ? es.GetSetting("ca_generator/atlas_y").AsDouble() : 0.0;
_densityLabel.Text = $"{(int)_densitySlider.Value}%";
_smoothPassLabel.Text = $"{(int)_smoothPassSlider.Value}";
}
}
#endif

View File

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

View File

@@ -0,0 +1,33 @@
#if TOOLS
using Godot;
namespace Mr.BrickAdventures.Tools.CaLevelGenerator;
[Tool]
public partial class CaLevelGeneratorPlugin : EditorPlugin
{
private CaLevelGeneratorDock _dock;
public override void _EnterTree()
{
_dock = new CaLevelGeneratorDock { Plugin = this };
AddControlToDock(DockSlot.RightUl, _dock);
SceneChanged += OnSceneChanged;
}
public override void _ExitTree()
{
SceneChanged -= OnSceneChanged;
RemoveControlFromDocks(_dock);
_dock.QueueFree();
_dock = null;
}
private void OnSceneChanged(Node sceneRoot)
{
_dock?.OnSceneChanged(sceneRoot);
}
public EditorUndoRedoManager UndoRedo => GetUndoRedo();
}
#endif

View File

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

View File

@@ -0,0 +1,100 @@
#if TOOLS
using Godot;
using Godot.Collections;
namespace Mr.BrickAdventures.Tools.CaLevelGenerator;
public static class TilemapPainter
{
public static void Paint(
TileMapLayer layer,
bool[,] grid,
CaGeneratorSettings settings,
EditorUndoRedoManager undoRedo)
{
var before = layer.TileMapData;
ApplyGrid(layer, grid, settings);
var after = layer.TileMapData;
RegisterUndo(layer, before, after, undoRedo, "CA Generate");
}
public static void Smooth(
TileMapLayer layer,
CaGeneratorSettings settings,
EditorUndoRedoManager undoRedo)
{
var grid = ReadGrid(layer, settings.Offset, new Vector2I(settings.Width, settings.Height));
grid = CaGenerator.Smooth(grid);
var before = layer.TileMapData;
ApplyGrid(layer, grid, settings);
var after = layer.TileMapData;
RegisterUndo(layer, before, after, undoRedo, "CA Smooth");
}
public static void Clear(
TileMapLayer layer,
CaGeneratorSettings settings,
EditorUndoRedoManager undoRedo)
{
var before = layer.TileMapData;
for (int x = 0; x < settings.Width; x++)
for (int y = 0; y < settings.Height; y++)
layer.EraseCell(settings.Offset + new Vector2I(x, y));
var after = layer.TileMapData;
RegisterUndo(layer, before, after, undoRedo, "CA Clear");
}
private static bool[,] ReadGrid(TileMapLayer layer, Vector2I offset, Vector2I size)
{
var grid = new bool[size.X, size.Y];
for (int x = 0; x < size.X; x++)
for (int y = 0; y < size.Y; y++)
grid[x, y] = layer.GetCellSourceId(offset + new Vector2I(x, y)) != -1;
return grid;
}
private static void ApplyGrid(TileMapLayer layer, bool[,] grid, CaGeneratorSettings settings)
{
int w = grid.GetLength(0), h = grid.GetLength(1);
for (int x = 0; x < w; x++)
for (int y = 0; y < h; y++)
layer.EraseCell(settings.Offset + new Vector2I(x, y));
if (settings.TerrainSet >= 0)
{
var solidCells = new Array<Vector2I>();
for (int x = 0; x < w; x++)
for (int y = 0; y < h; y++)
if (grid[x, y])
solidCells.Add(settings.Offset + new Vector2I(x, y));
if (solidCells.Count > 0)
layer.SetCellsTerrainConnect(solidCells, settings.TerrainSet, settings.Terrain);
}
else
{
for (int x = 0; x < w; x++)
for (int y = 0; y < h; y++)
if (grid[x, y])
layer.SetCell(
settings.Offset + new Vector2I(x, y),
settings.SourceId,
settings.AtlasCoords);
}
}
private static void RegisterUndo(
TileMapLayer layer,
byte[] before,
byte[] after,
EditorUndoRedoManager undoRedo,
string actionName)
{
undoRedo.CreateAction(actionName);
undoRedo.AddUndoProperty(layer, "tile_map_data", before);
undoRedo.AddDoProperty(layer, "tile_map_data", after);
undoRedo.CommitAction(false);
}
}
#endif

View File

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

View File

@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://test_ca_generator"]
[ext_resource type="Script" path="res://addons/ca_level_generator/CaGeneratorTestRunner.cs" id="1_catest"]
[node name="CaGeneratorTestRunner" type="Node"]
script = ExtResource("1_catest")

View File

@@ -0,0 +1,6 @@
[plugin]
name="CA Level Generator"
description="Cellular automata level blockout generator"
author="Mr. Brick Adventures"
version="1.0"
script="CaLevelGeneratorPlugin.cs"