diff --git a/src/Blocktavius.AppDQB2/MainWindow.xaml.cs b/src/Blocktavius.AppDQB2/MainWindow.xaml.cs index 3500fbc..db62d58 100644 --- a/src/Blocktavius.AppDQB2/MainWindow.xaml.cs +++ b/src/Blocktavius.AppDQB2/MainWindow.xaml.cs @@ -30,7 +30,7 @@ internal async void DoPreview() var vm = (this.DataContext as MainWindowVM)?.CurrentContent as ProjectVM; if (EyeOfRubissDriver != null && vm != null) { - var scriptedStage = await vm.TryRebuildStage(); + var scriptedStage = await vm.TryRebuildStage(preview: true); if (scriptedStage != null) { await EyeOfRubissDriver.WriteStageAsync(scriptedStage); diff --git a/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs b/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs index c4c08a1..a253f36 100644 --- a/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs +++ b/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs @@ -171,7 +171,7 @@ public PlanScriptVM(ProjectVM project) ScriptName = project.SelectedScript.GetScriptName() ?? ""; Title = $"Plan and Run -- {ScriptName}"; - rebuildTask = project.TryRebuildStage(); + rebuildTask = project.TryRebuildStage(preview: false); if (project.BackupsEnabled(out var backupDir)) { diff --git a/src/Blocktavius.AppDQB2/ProjectVM.cs b/src/Blocktavius.AppDQB2/ProjectVM.cs index cc2c379..71ee8db 100644 --- a/src/Blocktavius.AppDQB2/ProjectVM.cs +++ b/src/Blocktavius.AppDQB2/ProjectVM.cs @@ -294,7 +294,7 @@ private void ExpandChunks(IReadOnlySet expansion) return stage; } - public async Task TryRebuildStage() + public async Task TryRebuildStage(bool preview) { var workingStage = await TryLoadMutableStage(expandChunks: true); if (workingStage == null) @@ -302,6 +302,12 @@ private void ExpandChunks(IReadOnlySet expansion) return null; } + if (this.SelectedScript == Scripts.FirstOrDefault()) // NOMERGE!! + { + TERRAGEN.DropTheHammer(workingStage, preview); + return workingStage; + } + var context = new StageRebuildContext(workingStage); var mutation = this.SelectedScript?.BuildMutation(context); if (mutation != null) diff --git a/src/Blocktavius.AppDQB2/TERRAGEN.cs b/src/Blocktavius.AppDQB2/TERRAGEN.cs new file mode 100644 index 0000000..c8863f2 --- /dev/null +++ b/src/Blocktavius.AppDQB2/TERRAGEN.cs @@ -0,0 +1,458 @@ +using Blocktavius.Core; +using Blocktavius.DQB2; +using Blocktavius.DQB2.LiquidRoof; +using Blocktavius.DQB2.Mutations; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blocktavius.AppDQB2; + +static class TERRAGEN +{ + private static (Rect, IReadOnlyList) InferAvailableSpace(IStage stage) + { + var offsets = stage.ChunksInUse.ToList(); + + // drop dock offsets (trailing horizontal rows having count < 3) + while (offsets.Count > 0) + { + var last = offsets.Last(); + if (offsets.Where(o => o.OffsetZ == last.OffsetZ).Count() < 3) + { + offsets.RemoveAt(offsets.Count - 1); + } + else + { + break; + } + } + + var totalBounds = new Rect.BoundsFinder(); + foreach (var offset in offsets) + { + totalBounds.Include(offset.RawUnscaledOffset); + } + var bounds = totalBounds.CurrentBounds() ?? Rect.Zero; + return (new Rect(bounds.start.Scale(32), bounds.end.Scale(32)), offsets); + } + + /// + /// Surrounds the given with extra chunks + /// + private static IReadOnlySet ExpandChunks(IEnumerable offsets) + { + var expansion = new HashSet(); + foreach (var offset in offsets) + { + foreach (var dir in Direction.CardinalDirections().Concat(Direction.OrdinalDirections())) + { + var neighbor = offset.RawUnscaledOffset.Step(dir); + expansion.Add(new ChunkOffset(neighbor.X, neighbor.Z)); + } + } + return expansion; + } + + public static void DropTheHammer(IMutableStage stage, bool preview) + { + var (availableSpace, nonDockChunks) = InferAvailableSpace(stage); + stage.ExpandChunks(ExpandChunks(nonDockChunks)); + + const int groundMinY = 7; + stage.Mutate(new ClearEverythingMutation() { StartY = groundMinY, Where = availableSpace }); + + var prng = PRNG.Deserialize("1-2-5-67-67-67"); + + stage.Mutate(MakeGround(availableSpace, groundMinY)); + + List components = new(); + components.Add(new LakeComponent + { + HillRequest = HillRequests().First(), + Enforest = false + }); + components.AddRange(HillRequests().Skip(1).Index().Select(i => new HillComponent + { + HillRequest = i.Item, + Enforest = i.Index == 0, + })); + + var arranged = Arrange(components, availableSpace, prng); + + arranged.Reverse(); // reorder: lowest elevation to highest + foreach (var (position, component) in arranged) + { + var context = new TerraformMutationContext + { + ArrangedPosition = position, + PRNG = prng, + Stage = stage, + }; + component.Mutate(context); + } + + stage.Mutate(new RepairSeaMutation() + { + ColumnCleanupMode = ColumnCleanupMode.ExpandBedrock, + SeaLevel = 11, + }); + + if (!preview) + { + var roofOptions = new LiquidRoofOptions + { + FilterArea = availableSpace.AsArea().AsSampler(), + }; + var minimapUpdater = LiquidRoofPlan.Create(stage, roofOptions); + stage.Mutate(minimapUpdater.GetMutation()); + } + } + + private static PutHillMutation MakeGround(Rect rect, int y) + { + var sampler = new ConstantSampler { Bounds = rect, Value = y }; + return new PutHillMutation() + { + Block = 146, // seaside sand + Sampler = sampler, + YFloor = 1, + RespectExistingBedrock = true, + }; + } + + sealed record HillSpec + { + public required int Elevation { get; init; } + public required int Size { get; init; } + } + + interface IArrangable + { + Rect SeedSize { get; } + } + + sealed record TerraformMutationContext + { + public required IMutableStage Stage { get; init; } + public required PRNG PRNG { get; init; } + public required Rect ArrangedPosition { get; init; } + } + + interface ITerraformComponent : IArrangable + { + void Mutate(TerraformMutationContext context); + } + + static I2DSampler TranslateToCenter(this I2DSampler sampler, Rect target) + { + var center = target.start.Add(target.Size.Unscale(new XZ(2, 2))); + return TranslateToCenter(sampler, center); + } + + static I2DSampler TranslateToCenter(I2DSampler sampler, XZ target) + { + var corner = target.Add(sampler.Bounds.Size.Unscale(new XZ(-2, -2))); + return sampler.TranslateTo(corner); + } + + class HillComponent : ITerraformComponent + { + public required WIP.HillRequest HillRequest { get; init; } + + public Rect SeedSize => HillRequest.SeedSize; + + public required bool Enforest { get; init; } + + protected virtual IEnumerable> GetForestAreas(PRNG prng, I2DSampler origPlateau) + { + if (Enforest) + { + yield return origPlateau; + } + } + + public virtual void Mutate(TerraformMutationContext context) + { + // chalk=8, chunky chalk=9, clodstone=21 (or 115?) + // dolomite light=130, dark=131, chert=149 (or 174?), chunky chert=153 + // umber=209, lumpy umber=241 + // umber sandstone! = 210 + const ushort wallBlockId = 241; + + var prng = context.PRNG.AdvanceAndClone(); + + var hill = WIP.Blah(prng, this.HillRequest, out var plateauArea); + var hill2 = hill.Project(item => + { + ushort blockId; + if (item.Kind == WIP.HillItemKind.Plateau) + { + blockId = 4; + } + else if (item.Kind == WIP.HillItemKind.Chisel) + { + blockId = wallBlockId | 0xe000; + } + else if (item.Kind == WIP.HillItemKind.Cliff) + { + blockId = wallBlockId; + } + else + { + blockId = 0; + return (-1, blockId); + } + return (item.Elevation, blockId); + }); + + var origStart = hill2.Bounds.start; + hill2 = hill2.TranslateToCenter(context.ArrangedPosition); + var translateAmount = hill2.Bounds.start.Subtract(origStart); + + var mut = new PutHillMutation2() + { + Sampler = hill2, + YFloor = 1, + }; + + context.Stage.Mutate(mut); + + var forestAreas = GetForestAreas(prng, plateauArea).ToList(); + foreach (var forestArea in forestAreas) + { + AddForest(forestArea.Translate(translateAmount), context.Stage, prng); + } + } + + private void AddForest(I2DSampler area, IMutableStage stage, PRNG prng) + { + MutableArray2D hasTree = new MutableArray2D(area.Bounds, false); + List centers = new(); // centers of trees + double skipChance = 0.15; // chance of skipping the tree, while reserving its space and counting it as success + int skippedTreeCount = 0; // count of skipped trees + + int xStart = area.Bounds.start.X; + int xEnd = area.Bounds.end.X; + int zStart = area.Bounds.start.Z; + int zEnd = area.Bounds.end.Z; + + int targetCount = (area.Bounds.Size.X * area.Bounds.Size.Z) / 85; + int consecutiveFailures = 0; + while (centers.Count + skippedTreeCount < targetCount && consecutiveFailures < targetCount * 2) + { + var x = prng.NextInt32(xStart, xEnd); + var z = prng.NextInt32(zStart, zEnd); + var center = new XZ(x, z); + + var locs = TreeLocs(center); + if (!locs.All(area.InArea)) + { + continue; + } + + var paddedLocs = locs.Concat(PadLocs(center)); + if (paddedLocs.Any(hasTree.InArea)) + { + consecutiveFailures++; + continue; + } + + // reserve space no matter what... + consecutiveFailures = 0; + foreach (var xz in locs) + { + hasTree.Put(xz, true); + } + // ... but maybe skip this tree + if (prng.NextDouble() > skipChance) + { + centers.Add(center); + } + else + { + skippedTreeCount++; + } + } + + foreach (var center in centers) + { + stage.Mutate(new PutTreeMutation() + { + Center = center, + Elevation = this.HillRequest.Elevation + 1, + Prng = prng, + }); + } + } + + private static IEnumerable TreeLocs(XZ center) + { + yield return center; + foreach (var dir in Direction.CardinalDirections()) + { + for (int i = 1; i <= 2; i++) + { + yield return center.Step(dir, i); + } + } + + foreach (var dir in Direction.OrdinalDirections()) + { + yield return center.Step(dir); + } + } + + private static IEnumerable PadLocs(XZ center) + { + foreach (var dir in Direction.CardinalDirections()) + { + yield return center.Step(dir, 3); + yield return center.Step(dir, 3).Step(dir.TurnLeft90, 1); + yield return center.Step(dir, 2).Step(dir.TurnRight90, 1); + yield return center.Step(dir, 2).Step(dir.TurnRight90, 2); + yield return center.Step(dir, 1).Step(dir.TurnRight90, 2); + yield return center.Step(dir, 1).Step(dir.TurnRight90, 3); + } + } + } + + class LakeComponent : HillComponent + { + protected override IEnumerable> GetForestAreas(PRNG prng, I2DSampler origPlateau) + { + return []; + } + + public override void Mutate(TerraformMutationContext context) + { + base.Mutate(context); + + int plateauElevation = HillRequest.Elevation; + const int depth = 8; + + var lakeRequest = new WIP.HillRequest + { + Elevation = plateauElevation, + SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(6, 6)), + ExpansionRatio = 20m, + Steepness = 5, + }; + + var sampler = WIP.Blah(context.PRNG, lakeRequest, out _); + sampler = sampler.TranslateToCenter(context.ArrangedPosition); + + var lakebed = sampler.Project(item => + { + if (item.Elevation < 1) { return -1; } + int elevation = plateauElevation - depth + (plateauElevation - item.Elevation); + if (elevation >= plateauElevation) { return -1; } + return elevation; + }); + + var mut = new PutLakeMutation() + { + LakebedElevation = lakebed, + Liquid = LiquidFamily.ClearWater, + TopLayerAmount = LiquidAmountIndex.SurfaceLow, + TopLayerY = plateauElevation, + }; + + context.Stage.Mutate(mut); + } + } + + private static IEnumerable HillRequests() + { + // random rotation should be applied here, or at least before Arrange() + + // First one will be a lake: + yield return new WIP.HillRequest { Elevation = 18, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; + + //* TEMP SPEEDUP + yield return new WIP.HillRequest { Elevation = 50, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(120, 80)) }; + yield return new WIP.HillRequest { Elevation = 48, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(80, 120)) }; + yield return new WIP.HillRequest { Elevation = 40, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(60, 45)) }; + yield return new WIP.HillRequest { Elevation = 32, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(80, 150)) }; + yield return new WIP.HillRequest { Elevation = 30, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 80)) }; + yield return new WIP.HillRequest { Elevation = 26, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(100, 100)) }; + yield return new WIP.HillRequest { Elevation = 24, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(70, 140)) }; + yield return new WIP.HillRequest { Elevation = 20, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; + //yield return new WIP.HillRequest { Elevation = 18, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; + //*/ + } + + private static List<(Rect translatedPosition, TComponent request)> Arrange(IEnumerable requests, Rect fullSpace, PRNG prng) + where TComponent : IArrangable + { + var positions = requests.Select(request => (position: request.SeedSize, request)).ToList(); + var best = (positions, overlap: int.MaxValue); // lower values are better, zero is perfect + + for (int attempt = 0; attempt < 5; attempt++) + { + int totalOverlap = 0; + + for (int i = 0; i < positions.Count; i++) + { + var item = positions[i]; + var range = new Rect(fullSpace.start, fullSpace.end.Subtract(item.request.SeedSize.Size)); + var x = prng.NextInt32(range.start.X, range.end.X); + var z = prng.NextInt32(range.start.Z, range.end.Z); + var start = new XZ(x, z); + var pos = new Rect(start, start.Add(item.request.SeedSize.Size)); + + var overlappers = positions.Take(i).Select(x => x.position).ToList(); + + var current = (overlap: int.MaxValue, pos); + foreach (var dir in Direction.CardinalDirections()) + { + if (current.overlap == 0) { break; } + var pushed = BestPush(pos, overlappers, dir.Step, fullSpace); + if (pushed.Item1 < current.overlap) + { + current = pushed; + } + } + + positions[i] = (current.pos, item.request); + totalOverlap += current.overlap; + } + + if (totalOverlap < best.overlap) + { + best = (positions.ToList(), totalOverlap); + } + } + + return best.positions; + } + + private static (int, Rect) BestPush(Rect rect, IReadOnlyList overlappers, XZ pushDir, Rect fullSpace) + { + int overlap(Rect rect) + { + int o = 0; + foreach (var other in overlappers) + { + var i = other.Intersection(rect); + o += i.Size.X * i.Size.Z; + } + return o; + } + + var best = (overlap(rect), rect); + while (best.Item1 > 0 && fullSpace.Intersection(rect).Size == rect.Size) + { + rect = new Rect(rect.start.Add(pushDir), rect.end.Add(pushDir)); + var o = overlap(rect); + if (o < best.Item1) + { + best = (o, rect); + } + } + + return best; + } +} diff --git a/src/Blocktavius.Core/WIP.cs b/src/Blocktavius.Core/WIP.cs new file mode 100644 index 0000000..77dd195 --- /dev/null +++ b/src/Blocktavius.Core/WIP.cs @@ -0,0 +1,135 @@ +using Blocktavius.Core.Generators.Hills; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blocktavius.Core; + +public static class WIP +{ + public sealed record HillRequest + { + public required int Elevation { get; init; } + public required Rect SeedSize { get; init; } + public decimal ExpansionRatio { get; init; } = 1.4m; + public int Steepness { get; init; } = 11; // default steepness from CornerPusher + + internal HillItem PlateauItem => new HillItem { Elevation = this.Elevation, Kind = HillItemKind.Plateau }; + } + + public static I2DSampler Blah(PRNG prng, HillRequest request, out I2DSampler PlateauArea) + { + int maxElevation = request.Elevation; + + var area = BuildPlateau(prng, request); + + PlateauArea = area.GetArea(area.CurrentExpansionId()); + + AddChisel(area, maxElevation); + PatchHoles(area, request); + + var settings = new CornerPusherHill.Settings() + { + Prng = prng.Clone(), + MinElevation = 1, + MaxElevation = maxElevation - 1, + MaxConsecutiveMisses = request.Steepness, + }; + CornerPusherHill.BuildHill(settings, area, y => new HillItem { Elevation = y, Kind = HillItemKind.Cliff }); + + return area.GetSampler(ExpansionId.MaxValue, request.PlateauItem) + .Project(t => t.Item1 ? t.Item2 : HillItem.Nothing); + } + + public enum HillItemKind + { + None, + Plateau, + Chisel, + Cliff, + } + + public readonly record struct HillItem + { + public int Elevation { get; init; } + public HillItemKind Kind { get; init; } + + public static readonly HillItem Nothing = new() { Elevation = -1, Kind = HillItemKind.None }; + } + + private static ExpandableArea BuildPlateau(PRNG prng, HillRequest request) + { + var fillValue = request.PlateauItem; + + //var initArea = new Rect(XZ.Zero, XZ.Zero.Add(1, 1)).AsArea(); + //var initArea = new Rect(XZ.Zero, XZ.Zero.Add(20, 50)).AsArea(); + var initArea = request.SeedSize.AsArea(); + var initShell = ShellLogic.ComputeShells(initArea).Single(); + + var area = new ExpandableArea(initShell); + var shell = area.CurrentShell(); + //while (shell.Count < 600) + + int targetSize = Convert.ToInt32(request.ExpansionRatio * GetSize(request.SeedSize)); + var size = initArea.Bounds; + while (GetSize(size) < targetSize) + { + int i = prng.NextInt32(shell.Count); + var item = shell[i]; + if (item.CornerType != CornerType.Outside) // outside corners would cause "not connected" exceptions + { + area.Expand([(shell[i].XZ, fillValue)]); + shell = area.CurrentShell(); + // NOMERGE - this should be CurrentBounds or something: + size = area.GetSampler(ExpansionId.MaxValue, fillValue).Bounds; + } + } + + return area; + } + + private static int GetSize(Rect rect) => rect.Size.X * rect.Size.Z; + + private static void AddChisel(ExpandableArea area, int elevation) + { + var shell = area.CurrentShell(); + var value = new HillItem { Elevation = elevation, Kind = HillItemKind.Chisel }; + area.Expand(shell.Select(item => (item.XZ, value))); + } + + private static void PatchHoles(ExpandableArea area, HillRequest request) + { + HashSet holes = new(); + + var sampler = area.GetSampler(ExpansionId.MaxValue, request.PlateauItem); + foreach (var xz in sampler.Bounds.Enumerate()) + { + // At this point, any XZ that is empty and next to a plateau item must be a hole, + // because we have surrounded the plateau with the chisel layer. + bool isEmpty = !sampler.Sample(xz).Item1; + bool hasPlateauNeighbor = xz.CardinalNeighbors() + .Any(neighbor => sampler.Sample(neighbor).Item2.Kind == HillItemKind.Plateau); + + if (isEmpty && hasPlateauNeighbor) + { + // flood fill + var queue = new Queue(); + queue.Enqueue(xz); + while (queue.TryDequeue(out var floodXZ) && holes.Add(floodXZ)) + { + foreach (var neighbor in floodXZ.CardinalNeighbors()) + { + if (!sampler.Sample(neighbor).Item1) + { + queue.Enqueue(neighbor); + } + } + } + } + } + + area.Expand(holes.Select(xz => (xz, request.PlateauItem))); + } +} diff --git a/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs b/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs index 99fc664..bc4e0ac 100644 --- a/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs +++ b/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs @@ -12,10 +12,12 @@ public sealed class PutHillMutation : StageMutation public required I2DSampler Sampler { get; init; } public required ushort Block { get; init; } public int? YFloor { get; init; } = null; + public bool RespectExistingBedrock { get; init; } = false; internal override void Apply(IMutableStage stage) { int yFloor = this.YFloor ?? 1; + bool checkBedrock = RespectExistingBedrock; foreach (var chunk in Enumerate(Sampler.Bounds, stage)) { @@ -24,6 +26,10 @@ internal override void Apply(IMutableStage stage) var elevation = Sampler.Sample(xz); if (elevation > 0) { + if (checkBedrock && chunk.GetBlock(new Point(xz, 0)) == 0) + { + continue; + } for (int y = yFloor; y <= elevation; y++) { chunk.SetBlock(new Point(xz, y), Block); @@ -33,3 +39,217 @@ internal override void Apply(IMutableStage stage) } } } + +public sealed class PutHillMutation2 : StageMutation +{ + public required I2DSampler<(int, ushort)> Sampler { get; init; } + public int? YFloor { get; init; } = null; + + internal override void Apply(IMutableStage stage) + { + int yFloor = this.YFloor ?? 1; + + foreach (var chunk in Enumerate(Sampler.Bounds, stage)) + { + foreach (var xz in chunk.Offset.Bounds.Intersection(Sampler.Bounds).Enumerate()) + { + var (elevation, block) = Sampler.Sample(xz); + if (elevation > 0) + { + var unchiseled = (ushort)Block.MakeCanonical(block); + for (int y = yFloor; y < elevation; y++) + { + chunk.SetBlock(new Point(xz, y), unchiseled); + } + chunk.SetBlock(new Point(xz, elevation), block); + } + } + } + } +} + +public sealed class ClearEverythingMutation : StageMutation +{ + public int StartY { get; init; } = 1; + public Rect? Where { get; init; } = null; + + internal override void Apply(IMutableStage stage) + { + int startY = this.StartY; + + foreach (var offset in stage.ChunksInUse) + { + if (!stage.TryGetChunk(offset, out var chunk)) + { + continue; + } + + var where = this.Where ?? offset.Bounds; + + foreach (var xz in chunk.Offset.Bounds.Intersection(where).Enumerate()) + { + if (chunk.GetBlock(new Point(xz, 0)) == 0) // TODO this is unsound, I think + { + continue; + } + for (int y = StartY; y < DQB2Constants.MaxElevation; y++) + { + chunk.SetBlock(new Point(xz, y), 0); + } + } + } + } +} + +public sealed class PutLakeMutation : StageMutation +{ + public required I2DSampler LakebedElevation { get; init; } + public required LiquidFamily Liquid { get; init; } + public required LiquidAmountIndex TopLayerAmount { get; init; } + public required int TopLayerY { get; init; } + + internal override void Apply(IMutableStage stage) + { + ushort subsurfaceId = Liquid.BlockIdSubsurface; + ushort topLayerId = TopLayerAmount switch + { + LiquidAmountIndex.Subsurface => subsurfaceId, + LiquidAmountIndex.SurfaceHigh => Liquid.BlockIdSurfaceHigh, + _ => Liquid.BlockIdSurfaceLow, + }; + + // TESTING: + //subsurfaceId = 0; + //topLayerId = 0; + + foreach (var offset in stage.ChunksInUse) + { + if (!stage.TryGetChunk(offset, out var chunk)) + { + continue; + } + + var intersection = LakebedElevation.Bounds.Intersection(offset.Bounds); + foreach (var xz in intersection.Enumerate()) + { + int lakebed = LakebedElevation.Sample(xz); + if (lakebed > 0 && lakebed < TopLayerY) + { + chunk.SetBlock(new Point(xz, TopLayerY), topLayerId); + for (int y = TopLayerY - 1; y > lakebed; y--) + { + chunk.SetBlock(new Point(xz, y), subsurfaceId); + } + + for (int y = lakebed; y > 0; y--) + { + chunk.SetBlock(new Point(xz, y), 21); + } + } + } + } + } +} + +public sealed class PutTreeMutation : StageMutation +{ + public required XZ Center { get; init; } + public required int Elevation { get; init; } + public required PRNG Prng { get; init; } + + internal override void Apply(IMutableStage stage) + { + const ushort bark = 139; // "Bark" + const ushort leaves = 492; // "Teal Leaves" + + bool layer3Leaves = Prng.NextBool(); + bool tallCorner = Prng.NextBool(); + bool peak = Prng.NextBool(); + + // layer 1 + int y = Elevation; + PutBlock(stage, Center, y, bark); + foreach (var dir in Direction.CardinalDirections()) + { + PutBlock(stage, Center.Step(dir), y, SetChisel(bark, dir)); + } + + // layer 2 + y++; + PutBlock(stage, Center, y, bark); + + // layer 3 + y++; + PutBlock(stage, Center, y, bark); + foreach (var dir in Direction.CardinalDirections()) + { + PutBlock(stage, Center.Step(dir), y, SetChisel(leaves, Chisel.TopHalf)); + PutBlock(stage, Center.Step(dir, 2), y, leaves); + } + if (layer3Leaves) + { + foreach (var dir in Direction.OrdinalDirections()) + { + PutBlock(stage, Center.Step(dir), y, SetChisel(leaves, Chisel.TopHalf)); + } + } + + // layer 4 + y++; + PutBlock(stage, Center, y, bark); + foreach (var dir in Direction.CardinalDirections()) + { + PutBlock(stage, Center.Step(dir), y, leaves); + } + foreach (var dir in Direction.OrdinalDirections()) + { + ushort block = tallCorner ? leaves : SetChisel(leaves, dir); + PutBlock(stage, Center.Step(dir), y, block); + } + + // layer 5 + y++; + PutBlock(stage, Center, y, bark); + foreach (var dir in Direction.CardinalDirections()) + { + PutBlock(stage, Center.Step(dir), y, leaves); + } + if (tallCorner) + { + foreach (var dir in Direction.OrdinalDirections()) + { + PutBlock(stage, Center.Step(dir), y, SetChisel(leaves, dir)); + } + } + + // layer 6 + y++; + PutBlock(stage, Center, y, leaves); + foreach (var dir in Direction.CardinalDirections()) + { + PutBlock(stage, Center.Step(dir), y, SetChisel(leaves, dir)); + } + + // layer 7 + y++; + if (peak) + { + PutBlock(stage, Center, y, SetChisel(leaves, Chisel.BottomHalf)); + } + } + + private void PutBlock(IMutableStage stage, XZ xz, int y, ushort block) + { + if (stage.TryGetChunk(ChunkOffset.FromXZ(xz), out var chunk)) + { + chunk.SetBlock(new Point(xz, y), block); + } + } + + private static ushort SetChisel(ushort block, Direction dir) => SetChisel(block, dir.Turn180.GetDiagonalChisel()); + + private static ushort SetChisel(ushort block, Chisel chisel) + { + return Block.Lookup(block).SetChisel(chisel).BlockIdComplete; + } +}