From 97cf3ed6d1046e18357423883e2a5cff63455105 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Sat, 20 Jun 2026 15:34:20 -0500 Subject: [PATCH] this kind of works... --- .../CliffDesigners/FacileCliffDesigner.cs | 52 +++ src/Blocktavius.Core/IRandomValues.cs | 10 + src/Blocktavius.DQB2/Snippet.cs | 409 ++++++++++++++++++ 3 files changed, 471 insertions(+) diff --git a/src/Blocktavius.AppDQB2/ScriptNodes/CliffDesigners/FacileCliffDesigner.cs b/src/Blocktavius.AppDQB2/ScriptNodes/CliffDesigners/FacileCliffDesigner.cs index dd078cd..59d7130 100644 --- a/src/Blocktavius.AppDQB2/ScriptNodes/CliffDesigners/FacileCliffDesigner.cs +++ b/src/Blocktavius.AppDQB2/ScriptNodes/CliffDesigners/FacileCliffDesigner.cs @@ -2,6 +2,7 @@ using Blocktavius.Core; using Blocktavius.Core.Generators.Hills; using Blocktavius.DQB2; +using Blocktavius.DQB2.Mutations; using System; using System.Collections.Generic; using System.Linq; @@ -73,6 +74,57 @@ public int OverhangDepth public StageMutation? CreateMutation(CliffDesignContext context) { + if (1.ToString() == "1") // NOMERGE + { + var settings = new NEWCLIFF.Settings2 + { + Block = context.FillBlockId, + Jaunt = context.PositionedJaunt.Jaunt, + Prng = context.Prng.AdvanceAndClone(), + CliffSpec = NEWCLIFF.StandardSpec( + startElevation: this.BaseCliffElevation, + targetElevation: this.BaseCliffElevation + this.MiddleHeight + this.OverhangHeight, + overhangStartElevation: this.BaseCliffElevation + this.MiddleHeight), + }; + + var TODO = new XZ(5, 0); // just make it easier to see what's happening + + var snippet = NEWCLIFF.Build(settings) + .Rotate(context.PositionedJaunt.Rotation) + .TranslateTo(context.PositionedJaunt.Bounds.start.Add(TODO)); + + return new PutSnippetMutation() + { + Snippet = snippet, + }; + } + + if (1.ToString() == "1") // NOMERGE + { + var settings = new NEWCLIFF.Settings + { + Block = context.FillBlockId, + C = 2, + R = 4, + LayerC = 1, + LayerR = 2, + FloorY = this.BaseCliffElevation, + TargetY = this.BaseCliffElevation + this.OverhangHeight, + Prng = context.Prng.AdvanceAndClone(), + }; + + XZ TODO = new(0, -1); // see also "Spacer" below + + var snippet = NEWCLIFF.Build(context.PositionedJaunt.Jaunt, settings) + .Rotate(context.PositionedJaunt.Rotation) + .TranslateTo(context.PositionedJaunt.Bounds.start.Add(TODO)); + + return new PutSnippetMutation() + { + Snippet = snippet, + }; + } + ushort fillBlockId = context.FillBlockId; var config = new FacileCliffBuilder.Config diff --git a/src/Blocktavius.Core/IRandomValues.cs b/src/Blocktavius.Core/IRandomValues.cs index f4c9a94..747d1b3 100644 --- a/src/Blocktavius.Core/IRandomValues.cs +++ b/src/Blocktavius.Core/IRandomValues.cs @@ -29,6 +29,16 @@ public static class RandomValues public static IBoundedRandomValues FromRange(int min, int max) => new RangeValues() { MinValue = min, MaxValue = max }; + public static IBoundedRandomValues Constant(int value) => new ConstantValue { Value = value }; + + private sealed class ConstantValue : IBoundedRandomValues + { + public required int Value { get; init; } + public int MinValue => Value; + public int MaxValue => Value; + public int NextValue(PRNG prng) => Value; + } + private sealed class RangeValues : IBoundedRandomValues { public required int MinValue { get; init; } diff --git a/src/Blocktavius.DQB2/Snippet.cs b/src/Blocktavius.DQB2/Snippet.cs index d803cd9..a74586d 100644 --- a/src/Blocktavius.DQB2/Snippet.cs +++ b/src/Blocktavius.DQB2/Snippet.cs @@ -47,6 +47,43 @@ private Snippet(IReadOnlyList blockdata, XZ sizeXZ, int sizeY) sizePerLayer = sizeX * sizeZ; } + public sealed class Builder + { + private readonly XZ size; + private readonly int sizeY; + private readonly ushort[] blockdata; + private bool built = false; + public Builder(XZ size, int sizeY) + { + this.size = size; + this.sizeY = sizeY; + this.blockdata = new ushort[size.X * size.Z * sizeY]; + } + + private int GetIndex(Point point) => 0 + + point.xz.X + + point.xz.Z * size.X + + point.Y * size.X * size.Z; + + public ushort this[Point point] + { + get => blockdata[GetIndex(point)]; + set + { + if (built) + { + throw new InvalidOperationException("already built, no further modifications are allowed"); + } + blockdata[GetIndex(point)] = value; + } + } + + public Snippet BuildSnippet() + { + return new Snippet(blockdata, size, sizeY); + } + } + public static Snippet Create(IStage stage, Rect bounds, int floorY = 0) { if (floorY < 0 || floorY >= DQB2Constants.MaxElevation) @@ -124,3 +161,375 @@ public ushort GetBlock(int y) } } } + +public sealed class NEWCLIFF +{ + /// + /// A "Slab" is like a wall with some spikes in front of it. + /// By convention, we think of this wall running from West to East. + /// First we generate a spike at each X coordinate (from West to East). + /// Then we choose the height of the wall by adding a randomly chosen + /// Cap Choice to the max spike that we generated. + /// So if we generated spikes [3,5,3,4,4,5] and cap 2 the result would look like this: + /// + /// ------ (this line is just above the top of the slab/wall) + /// (7) + /// (6) + /// x x (5) + /// x xxx (4) + /// xxxxxx (3) + /// xxxxxx (2) + /// xxxxxx (1) + /// ------ (this line is just below the bottom of the slab/wall) + /// + /// This means that a slab is always flat on the top and bottom; + /// in other words it has a uniform height along its entire width. + /// It is only the spikes that are not uniform. + /// This allows us to easily stack slabs on top of each other. + /// + /// If this slab , we would invert it. + /// + public sealed record SlabShape : IRandomValues + { + public required IRandomValues SpikeChoices { get; init; } + public required IRandomValues CapChoices { get; init; } + public required bool IsOverhang { get; init; } + + SlabShape IRandomValues.NextValue(PRNG prng) => this; + } + + public sealed record SlabSpec + { + public required int? RepeatUntilElevation { get; init; } + public required IRandomValues ShapeChoices { get; init; } + } + + public sealed record CliffSpec + { + public required IReadOnlyList SlabSpecs { get; init; } + public required IRandomValues StartFromElevationChoices { get; init; } + } + + public sealed record Settings2 + { + public required CliffSpec CliffSpec { get; init; } + public required PRNG Prng { get; init; } + public required ushort Block { get; init; } + public required Jaunt Jaunt { get; init; } + } + + public static CliffSpec StandardSpec(int startElevation, int targetElevation, int? overhangStartElevation) + { + List slabs = new(); + + slabs.Add(new SlabSpec + { + ShapeChoices = new SlabShape + { + IsOverhang = false, + SpikeChoices = RandomValues.FromRange(2, 5), + CapChoices = RandomValues.FromRange(1, 2), + }, + RepeatUntilElevation = overhangStartElevation ?? targetElevation, + }); + + if (overhangStartElevation.HasValue) + { + slabs.Add(new SlabSpec + { + ShapeChoices = new SlabShape + { + IsOverhang = true, + SpikeChoices = RandomValues.FromRange(2, 5), + CapChoices = RandomValues.FromRange(1, 2), + }, + RepeatUntilElevation = targetElevation, + }); + } + + return new CliffSpec + { + SlabSpecs = slabs, + StartFromElevationChoices = RandomValues.Constant(startElevation) + }; + } + + sealed record Slab + { + public required SlabShape SlabShape { get; init; } + public required int LaneOffset { get; init; } + public required int SlabMinElevation { get; init; } + public required int SlabMaxElevation { get; init; } + public required int SpikeMinElevation { get; init; } + public required int SpikeMaxElevation { get; init; } + public required IReadOnlyList SpikeElevations { get; init; } + + private Slab() { } + + public Slab RemapLaneOffset(int delta) => this with { LaneOffset = this.LaneOffset + delta }; + + public static Slab Create(int startElevation, int width, SlabSpec spec, PRNG prng, int laneOffset) + { + var shape = spec.ShapeChoices.NextValue(prng); + + var spikes = new List(capacity: width); + for (int i = 0; i < width; i++) + { + spikes.Add(startElevation + shape.SpikeChoices.NextValue(prng)); + } + + int minSpike = spikes.Min(); + int maxSpike = spikes.Max(); + + int maxSlab = maxSpike + shape.CapChoices.NextValue(prng); + + return new Slab() + { + SlabShape = shape, + SlabMinElevation = startElevation, + SlabMaxElevation = maxSlab, + SpikeMinElevation = minSpike, + SpikeMaxElevation = maxSpike, + SpikeElevations = spikes, + LaneOffset = laneOffset, + }; + } + } + + sealed record Run + { + public required Jaunt.Run JauntRun { get; init; } + public required IReadOnlyList Slabs { get; init; } + } + + public static Snippet Build(Settings2 settings) + { + var runs = settings.Jaunt.Runs.Select(r => BuildSlabs(settings, r)).ToList(); + + const int SPIKE_OFFSET = 1; + + var offsets = runs.SelectMany(r => r.Slabs).Select(s => s.LaneOffset); + int minZ = offsets.Min(); + int maxZ = offsets.Max() + SPIKE_OFFSET; + + int zAdjust = -minZ; // adjust so that minZ is remapped to 0 + + var snippetSize = new XZ(settings.Jaunt.TotalLength, maxZ - minZ); + + int targetY; + var foo = settings.CliffSpec.SlabSpecs.Last().RepeatUntilElevation; + if (foo.HasValue) + { + targetY = foo.Value; + } + else + { + targetY = runs.Max(r => r.Slabs.Last().SlabMaxElevation); + } + + var builder = new Snippet.Builder(snippetSize.Add(1, 1), targetY + 1); + foreach (var run in runs) + { + foreach (var slab in run.Slabs.Reverse()) + { + int z = slab.LaneOffset + zAdjust; + + int backstopStartY = slab.SlabShape.IsOverhang ? slab.SlabMinElevation : 1; + int backstopEndY = Math.Min(slab.SlabMaxElevation, targetY) + 1; + + for (int i = 0; i < run.JauntRun.length; i++) + { + int x = run.JauntRun.start + i; + + for (int y = backstopStartY; y < backstopEndY; y++) + { + builder[new Point(new XZ(x, z), y)] = 9;// settings.Block; + } + + int spikeStartY; + int spikeEndY; + if (slab.SlabShape.IsOverhang) + { + int spikeHeight = slab.SpikeElevations[i] - slab.SlabMinElevation; + spikeStartY = slab.SlabMaxElevation - spikeHeight; + spikeEndY = Math.Min(slab.SlabMaxElevation, targetY) + 1; + } + else + { + spikeStartY = 1; + spikeEndY = Math.Min(slab.SpikeElevations[i], targetY) + 1; + } + + for (int y = spikeStartY; y < spikeEndY; y++) + { + builder[new Point(new XZ(x, z + SPIKE_OFFSET), y)] = settings.Block; + } + } + } + } + + return builder.BuildSnippet(); + } + + private static Run BuildSlabs(Settings2 settings, Jaunt.Run run) + { + var slabs = new List(); + + int startElevation = settings.CliffSpec.StartFromElevationChoices.NextValue(settings.Prng); + int laneOffset = run.laneOffset; + + var slabSpecIterator = settings.CliffSpec.SlabSpecs.GetEnumerator(); + + bool keepGoing = slabSpecIterator.MoveNext(); + while (keepGoing) + { + var prev = slabs.LastOrDefault(); + + var slab = Slab.Create(startElevation, run.length, slabSpecIterator.Current, settings.Prng, laneOffset); + startElevation = slab.SlabMaxElevation + 1; + + // UGLY!! Patch up lane offset after the fact :( + if (prev != null && prev.SlabShape.IsOverhang != slab.SlabShape.IsOverhang) + { + // Don't change lane offset! + slab = slab with { LaneOffset = prev.LaneOffset }; + laneOffset = prev.LaneOffset; + } + laneOffset += slab.SlabShape.IsOverhang ? 1 : -1; + + slabs.Add(slab); + + var targetElevation = slabSpecIterator.Current.RepeatUntilElevation; + if (targetElevation.HasValue) + { + if (slab.SlabMaxElevation >= targetElevation.Value) + { + keepGoing = slabSpecIterator.MoveNext(); // target elevation has now been reached, advance + } + else + { + keepGoing = true; // repeat current spec until target elevation reached + } + } + else + { + keepGoing = slabSpecIterator.MoveNext(); // no repeat condition specified, advance + } + } + + // This is weird. We want the *top* (last) slab to line up with the lane offset from the Jaunt, + // so now that we're done we have to remap it. + var aha = slabs.LastOrDefault(); + if (aha != null) + { + int adjust = aha.LaneOffset - run.laneOffset; + slabs = slabs.Select(s => s.RemapLaneOffset(-adjust)).ToList(); + } + + return new Run + { + JauntRun = run, + Slabs = slabs, + }; + } + + public sealed record Settings + { + public required PRNG Prng { get; init; } + public required int FloorY { get; init; } + public required int TargetY { get; init; } + + public required int C { get; init; } + public required int R { get; init; } + + public required int LayerC { get; init; } + public required int LayerR { get; init; } + + public required ushort Block { get; init; } + } + + public static Snippet Build(Jaunt jaunt, Settings settings) + { + List> segments = new(); + foreach (var run in jaunt.Runs) + { + segments.Add(BuildLayers(run, settings)); + } + + int depth = segments.Max(x => + { + var last = x.Last(); + return last.run.laneOffset + last.layerNumber; + }); + + var snippetSize = new XZ(jaunt.TotalLength, depth); + var builder = new Snippet.Builder(snippetSize.Add(1, 1), settings.TargetY + 1); + foreach (var segment in segments) + { + int z = segment[0].run.laneOffset + segment.Count; + + foreach (var layer in segment) + { + for (int i = 0; i < layer.elevations.Count; i++) + { + var xz = new XZ(layer.run.start + i, z); + int elevation = Math.Min(layer.elevations[i], settings.TargetY); + for (int y = 0; y <= elevation; y++) + { + builder[new Point(xz, y)] = settings.Block; + } + } + + z--; + } + } + + return builder.BuildSnippet(); + } + + readonly record struct Layer(IReadOnlyList elevations, Jaunt.Run run, int layerNumber); + + private static List BuildLayers(Jaunt.Run run, Settings settings) + { + var layers = new List(); + + var current = BuildFirstLayer(run, settings); + layers.Add(current); + + while (current.elevations.Min() < settings.TargetY) + { + current = BuildNextLayer(current, settings); + layers.Add(current); + } + + return layers; + } + + private static Layer BuildFirstLayer(Jaunt.Run run, Settings settings) + { + var elevations = RandomElevations(run, settings.FloorY + settings.C, settings.R, settings.Prng); + return new Layer + { + elevations = elevations, + layerNumber = 0, + run = run, + }; + } + + private static Layer BuildNextLayer(Layer previous, Settings settings) + { + int floorY = previous.elevations.Max() + settings.LayerC + settings.Prng.NextInt32(settings.LayerR); + var elevations = RandomElevations(previous.run, floorY, settings.R, settings.Prng); + return new Layer + { + elevations = elevations, + layerNumber = previous.layerNumber + 1, + run = previous.run, + }; + } + + private static IReadOnlyList RandomElevations(Jaunt.Run run, int minY, int randY, PRNG prng) + { + return Enumerable.Range(0, run.end - run.start).Select(_ => minY + prng.NextInt32(randY)).ToList(); + } +}