|
|
@ -31,92 +31,91 @@ public partial class Terrain |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary> Returns if this terrain grid contains the specified tile position. </summary> |
|
|
|
public bool Contains(TilePos pos) |
|
|
|
public bool ContainsTilePos(Vector2I tilePos) |
|
|
|
=> (pos.X >= 0) && (pos.X < Size.X) |
|
|
|
=> (tilePos.X >= 0) && (tilePos.X < Size.X) |
|
|
|
&& (pos.Y >= 0) && (pos.Y < Size.Y); |
|
|
|
&& (tilePos.Y >= 0) && (tilePos.Y < Size.Y); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary> Transforms a 3D position local to the equivalent tile position. </summary> |
|
|
|
/// <summary> Transforms a 3D position local to the equivalent tile position. </summary> |
|
|
|
public Vector2I ToTilePos(Vector3 localPos) |
|
|
|
public TilePos ToTilePos(Vector3 localPos) |
|
|
|
=> new(RoundToInt(localPos.X / TileSize + Size.X / 2.0f), |
|
|
|
=> new(RoundToInt(localPos.X / TileSize + Size.X / 2.0f), |
|
|
|
RoundToInt(localPos.Z / TileSize + Size.Y / 2.0f)); |
|
|
|
RoundToInt(localPos.Z / TileSize + Size.Y / 2.0f)); |
|
|
|
|
|
|
|
|
|
|
|
public float GetCornerHeight(Vector2I tilePos, Corner corner) |
|
|
|
public Corners<float> GetCornerHeights(TilePos pos) |
|
|
|
=> GetTileRaw(tilePos) switch { |
|
|
|
=> GetTileRaw(pos) switch { |
|
|
|
{ VariantType: Variant.Type.Nil } => 0, |
|
|
|
{ VariantType: Variant.Type.Nil } => default, |
|
|
|
{ VariantType: Variant.Type.Float } num => (float)num, |
|
|
|
{ VariantType: Variant.Type.Float } v => Corners<float>.From((float)v), |
|
|
|
{ VariantType: Variant.Type.PackedFloat32Array } nums => ((float[])nums)[(int)corner], |
|
|
|
{ VariantType: Variant.Type.PackedFloat32Array } v => Corners<float>.From((float[])v), |
|
|
|
_ => throw new Exception("Invalid type in Points dictionary"), |
|
|
|
_ => throw new Exception("Invalid type in Points dictionary"), |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
public void SetCornerHeight(Vector2I tilePos, Corner corner, float value) |
|
|
|
public void SetCornerHeight(TilePos pos, Corner corner, float value) |
|
|
|
|
|
|
|
=> SetCornerHeights(pos, default(Corners<float?>).With(corner, value)); |
|
|
|
|
|
|
|
public void SetCornerHeights(TilePos pos, Corners<float> values) |
|
|
|
|
|
|
|
=> SetCornerHeights(pos, new Corners<float?>(values.TopLeft, values.TopRight, values.BottomRight, values.BottomLeft)); |
|
|
|
|
|
|
|
public void SetCornerHeights(TilePos pos, Corners<float?> values) |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (!ContainsTilePos(tilePos)) return; |
|
|
|
if (!Contains(pos)) return; |
|
|
|
var existing = GetTileRaw(tilePos) switch { |
|
|
|
|
|
|
|
{ VariantType: Variant.Type.Nil } => [ 0, 0, 0, 0 ], |
|
|
|
var current = GetCornerHeights(pos); |
|
|
|
{ VariantType: Variant.Type.Float } num => [ (float)num, (float)num, (float)num, (float)num ], |
|
|
|
var changed = false; |
|
|
|
{ VariantType: Variant.Type.PackedFloat32Array } nums => (float[])nums, |
|
|
|
|
|
|
|
_ => throw new Exception("Invalid type in Points dictionary"), |
|
|
|
foreach (var corner in Enum.GetValues<Corner>()) |
|
|
|
}; |
|
|
|
if (values[corner] is float value) |
|
|
|
existing[(int)corner] = value; |
|
|
|
{ current = current.With(corner, value); changed = true; } |
|
|
|
if (existing.All(IsZeroApprox)) |
|
|
|
if (!changed) return; |
|
|
|
Tiles?.Remove(tilePos); |
|
|
|
|
|
|
|
if (IsEqualApprox(existing[0], existing[1]) |
|
|
|
if (current.IsZeroApprox()) RemoveTileRaw(pos); |
|
|
|
&& IsEqualApprox(existing[0], existing[2]) |
|
|
|
else if (current.IsEqualApprox()) SetTileRaw(pos, current.TopLeft); |
|
|
|
&& IsEqualApprox(existing[0], existing[3])) |
|
|
|
else SetTileRaw(pos, current.ToArray()); |
|
|
|
SetTileRaw(tilePos, existing[0]); |
|
|
|
|
|
|
|
else |
|
|
|
|
|
|
|
SetTileRaw(tilePos, existing); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public void SetTileHeight(Vector2I tilePos, float value) |
|
|
|
public void SetTileHeight(TilePos pos, float value) |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (!ContainsTilePos(tilePos)) return; |
|
|
|
if (!Contains(pos)) return; |
|
|
|
if (IsZeroApprox(value)) Tiles?.Remove(tilePos); |
|
|
|
if (IsZeroApprox(value)) RemoveTileRaw(pos); |
|
|
|
else SetTileRaw(tilePos, value); |
|
|
|
else SetTileRaw(pos, value); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public void AdjustTileHeight(Vector2I tilePos, float relative) |
|
|
|
public void AdjustTileHeight(TilePos pos, float relative) |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (relative == 0) return; |
|
|
|
if (relative == 0) return; |
|
|
|
if (!ContainsTilePos(tilePos)) return; |
|
|
|
if (!Contains(pos)) return; |
|
|
|
switch (GetTileRaw(tilePos)) { |
|
|
|
switch (GetTileRaw(pos)) { |
|
|
|
case { VariantType: Variant.Type.Nil }: |
|
|
|
case { VariantType: Variant.Type.Nil }: |
|
|
|
SetTileRaw(tilePos, relative); |
|
|
|
SetTileRaw(pos, relative); |
|
|
|
break; |
|
|
|
break; |
|
|
|
case { VariantType: Variant.Type.Float } num: |
|
|
|
case { VariantType: Variant.Type.Float } num: |
|
|
|
var newNum = (float)num + relative; |
|
|
|
var newNum = (float)num + relative; |
|
|
|
if (IsZeroApprox(newNum)) Tiles?.Remove(tilePos); |
|
|
|
if (IsZeroApprox(newNum)) RemoveTileRaw(pos); |
|
|
|
else SetTileRaw(tilePos, newNum); |
|
|
|
else SetTileRaw(pos, newNum); |
|
|
|
break; |
|
|
|
break; |
|
|
|
case { VariantType: Variant.Type.PackedFloat32Array } nums: |
|
|
|
case { VariantType: Variant.Type.PackedFloat32Array } nums: |
|
|
|
var newNums = (float[])nums; |
|
|
|
var newNums = (float[])nums; |
|
|
|
for (var i = 0; i < 4; i++) newNums[i] += relative; |
|
|
|
for (var i = 0; i < 4; i++) newNums[i] += relative; |
|
|
|
SetTileRaw(tilePos, newNums); |
|
|
|
SetTileRaw(pos, newNums); |
|
|
|
break; |
|
|
|
break; |
|
|
|
default: throw new Exception("Invalid type in Points dictionary"); |
|
|
|
default: throw new Exception("Invalid type in Points dictionary"); |
|
|
|
}; |
|
|
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public GridCorners GetGridCorners(Vector2I tilePos) |
|
|
|
public Corners<Vector3> GetCornersPosition(TilePos pos) |
|
|
|
{ |
|
|
|
{ |
|
|
|
var halfSize = TileSize / 2; |
|
|
|
var heights = GetCornerHeights(pos); |
|
|
|
var vx = (tilePos.X - Size.X / 2.0f) * TileSize; |
|
|
|
var vx = (pos.X - Size.X / 2.0f) * TileSize; |
|
|
|
var vz = (tilePos.Y - Size.Y / 2.0f) * TileSize; |
|
|
|
var vz = (pos.Y - Size.Y / 2.0f) * TileSize; |
|
|
|
|
|
|
|
var half = TileSize / 2; |
|
|
|
return new() { |
|
|
|
return new(new(vx - half, heights[Corner.TopLeft ], vz - half), |
|
|
|
TopLeft = new(vx - halfSize, GetCornerHeight(tilePos, Corner.TopLeft ), vz - halfSize), |
|
|
|
new(vx + half, heights[Corner.TopRight ], vz - half), |
|
|
|
TopRight = new(vx + halfSize, GetCornerHeight(tilePos, Corner.TopRight ), vz - halfSize), |
|
|
|
new(vx + half, heights[Corner.BottomRight], vz + half), |
|
|
|
BottomLeft = new(vx - halfSize, GetCornerHeight(tilePos, Corner.BottomLeft ), vz + halfSize), |
|
|
|
new(vx - half, heights[Corner.BottomLeft ], vz + half)); |
|
|
|
BottomRight = new(vx + halfSize, GetCornerHeight(tilePos, Corner.BottomRight), vz + halfSize), |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Variant GetTileRaw(Vector2I tilePos) |
|
|
|
Variant GetTileRaw(TilePos pos) |
|
|
|
=> (Tiles?.TryGetValue(tilePos, out var result) == true) ? result : default; |
|
|
|
=> (Tiles?.TryGetValue(pos.ToVector2I(), out var result) == true) ? result : default; |
|
|
|
|
|
|
|
void SetTileRaw(TilePos pos, Variant value) |
|
|
|
void SetTileRaw(Vector2I tilePos, Variant value) |
|
|
|
=> (Tiles ??= [])[pos.ToVector2I()] = value; |
|
|
|
=> (Tiles ??= [])[tilePos] = value; |
|
|
|
void RemoveTileRaw(TilePos pos) |
|
|
|
|
|
|
|
=> Tiles?.Remove(pos.ToVector2I()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void UpdateMeshAndShape() |
|
|
|
void UpdateMeshAndShape() |
|
|
@ -146,7 +145,7 @@ public partial class Terrain |
|
|
|
|
|
|
|
|
|
|
|
for (var x = 0; x < Size.X; x++) { |
|
|
|
for (var x = 0; x < Size.X; x++) { |
|
|
|
for (var z = 0; z < Size.Y; z++) { |
|
|
|
for (var z = 0; z < Size.Y; z++) { |
|
|
|
var corners = GetGridCorners(new(x, z)); |
|
|
|
var corners = GetCornersPosition(new(x, z)); |
|
|
|
AddTriangle(corners.TopLeft , new(0.0f, 0.0f), |
|
|
|
AddTriangle(corners.TopLeft , new(0.0f, 0.0f), |
|
|
|
corners.TopRight , new(1.0f, 0.0f), |
|
|
|
corners.TopRight , new(1.0f, 0.0f), |
|
|
|
corners.BottomLeft , new(0.0f, 1.0f)); |
|
|
|
corners.BottomLeft , new(0.0f, 1.0f)); |
|
|
@ -186,19 +185,105 @@ public partial class Terrain |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public enum Corner |
|
|
|
public readonly record struct TilePos(int X, int Y) |
|
|
|
{ |
|
|
|
{ |
|
|
|
TopLeft, |
|
|
|
public TilePos GetNeighbor(Side side) |
|
|
|
TopRight, |
|
|
|
=> side switch { |
|
|
|
BottomLeft, |
|
|
|
Side.Left => new(X - 1, Y), |
|
|
|
BottomRight, |
|
|
|
Side.Top => new(X, Y - 1), |
|
|
|
|
|
|
|
Side.Right => new(X + 1, Y), |
|
|
|
|
|
|
|
Side.Bottom => new(X, Y + 1), |
|
|
|
|
|
|
|
_ => throw new ArgumentException($"Invalid Side value '{side}'", nameof(side)), |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public TilePos GetNeighbor(Corner corner) |
|
|
|
|
|
|
|
=> corner switch { |
|
|
|
|
|
|
|
Corner.TopLeft => new(X - 1, Y - 1), |
|
|
|
|
|
|
|
Corner.TopRight => new(X + 1, Y - 1), |
|
|
|
|
|
|
|
Corner.BottomRight => new(X + 1, Y + 1), |
|
|
|
|
|
|
|
Corner.BottomLeft => new(X - 1, Y + 1), |
|
|
|
|
|
|
|
_ => throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)), |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static TilePos From(Vector2I value) => new(value.X, value.Y); |
|
|
|
|
|
|
|
public Vector2I ToVector2I() => new(X, Y); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public readonly struct GridCorners |
|
|
|
public readonly record struct Corners<T> |
|
|
|
|
|
|
|
(T TopLeft, T TopRight, T BottomRight, T BottomLeft) |
|
|
|
{ |
|
|
|
{ |
|
|
|
public Vector3 TopLeft { get; init; } |
|
|
|
public T this[Corner corner] { |
|
|
|
public Vector3 TopRight { get; init; } |
|
|
|
readonly get => corner switch { |
|
|
|
public Vector3 BottomLeft { get; init; } |
|
|
|
Corner.TopLeft => TopLeft, |
|
|
|
public Vector3 BottomRight { get; init; } |
|
|
|
Corner.TopRight => TopRight, |
|
|
|
|
|
|
|
Corner.BottomRight => BottomRight, |
|
|
|
|
|
|
|
Corner.BottomLeft => BottomLeft, |
|
|
|
|
|
|
|
_ => throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)), |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
init { switch (corner) { |
|
|
|
|
|
|
|
case Corner.TopLeft : TopLeft = value; break; |
|
|
|
|
|
|
|
case Corner.TopRight : TopRight = value; break; |
|
|
|
|
|
|
|
case Corner.BottomRight : BottomRight = value; break; |
|
|
|
|
|
|
|
case Corner.BottomLeft : BottomLeft = value; break; |
|
|
|
|
|
|
|
default: throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)); |
|
|
|
|
|
|
|
} } |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static Corners<T> From(T value) => new() |
|
|
|
|
|
|
|
{ TopLeft = value, TopRight = value, BottomRight = value, BottomLeft = value }; |
|
|
|
|
|
|
|
public static Corners<T> From(T[] values) => new() |
|
|
|
|
|
|
|
{ TopLeft = values[0], TopRight = values[1], BottomRight = values[2], BottomLeft = values[3] }; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public Corners<T> With(Corner corner, T value) |
|
|
|
|
|
|
|
=> new((corner == Corner.TopLeft) ? value : TopLeft, |
|
|
|
|
|
|
|
(corner == Corner.TopRight) ? value : TopRight, |
|
|
|
|
|
|
|
(corner == Corner.BottomRight) ? value : BottomRight, |
|
|
|
|
|
|
|
(corner == Corner.BottomLeft) ? value : BottomLeft); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public readonly T[] ToArray() |
|
|
|
|
|
|
|
=> [ TopLeft, TopRight, BottomRight, BottomLeft ]; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static class CornersExtensions |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
public static bool IsZeroApprox(this Terrain.Corners<float> self) |
|
|
|
|
|
|
|
=> Mathf.IsZeroApprox(self.TopLeft) && Mathf.IsZeroApprox(self.TopRight) |
|
|
|
|
|
|
|
&& Mathf.IsZeroApprox(self.BottomRight) && Mathf.IsZeroApprox(self.BottomLeft); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static bool IsEqualApprox(this Terrain.Corners<float> self) |
|
|
|
|
|
|
|
=> Mathf.IsEqualApprox(self.TopLeft, self.TopRight) |
|
|
|
|
|
|
|
&& Mathf.IsEqualApprox(self.TopLeft, self.BottomRight) |
|
|
|
|
|
|
|
&& Mathf.IsEqualApprox(self.TopLeft, self.BottomLeft); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: Put this in a different file. |
|
|
|
|
|
|
|
public static class TerrainExtensions |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
public static (Corner, Corner) GetCorners(this Side side) |
|
|
|
|
|
|
|
=> side switch { |
|
|
|
|
|
|
|
Side.Left => (Corner.TopLeft, Corner.BottomLeft), |
|
|
|
|
|
|
|
Side.Top => (Corner.TopLeft, Corner.TopRight), |
|
|
|
|
|
|
|
Side.Right => (Corner.TopRight, Corner.BottomRight), |
|
|
|
|
|
|
|
Side.Bottom => (Corner.BottomLeft, Corner.BottomRight), |
|
|
|
|
|
|
|
_ => throw new ArgumentException($"Invalid Side value '{side}'", nameof(side)), |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static Side GetOpposite(this Side side) |
|
|
|
|
|
|
|
=> side switch { |
|
|
|
|
|
|
|
Side.Left => Side.Right, |
|
|
|
|
|
|
|
Side.Top => Side.Bottom, |
|
|
|
|
|
|
|
Side.Right => Side.Left, |
|
|
|
|
|
|
|
Side.Bottom => Side.Top, |
|
|
|
|
|
|
|
_ => throw new ArgumentException($"Invalid Side value '{side}'", nameof(side)), |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static Corner GetOpposite(this Corner corner) |
|
|
|
|
|
|
|
=> corner switch { |
|
|
|
|
|
|
|
Corner.TopLeft => Corner.BottomRight, |
|
|
|
|
|
|
|
Corner.TopRight => Corner.BottomLeft, |
|
|
|
|
|
|
|
Corner.BottomRight => Corner.TopLeft, |
|
|
|
|
|
|
|
Corner.BottomLeft => Corner.TopRight, |
|
|
|
|
|
|
|
_ => throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)), |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
} |
|
|
|