|
|
|
@ -48,6 +48,7 @@ public partial class Terrain |
|
|
|
|
var radius = FloorToInt(drawSize / 2.0f); |
|
|
|
|
|
|
|
|
|
// Offset hover tile position by corner. |
|
|
|
|
// FIXME: This causes FLATTEN to calculate the wrong height in some cases. |
|
|
|
|
if (isEven) hover.Position = hover.Corner switch { |
|
|
|
|
Corner.TopLeft => hover.Position.Offset(0, 0), |
|
|
|
|
Corner.TopRight => hover.Position.Offset(1, 0), |
|
|
|
@ -73,30 +74,51 @@ public partial class Terrain |
|
|
|
|
center.DistanceSquaredTo(tile.ToCenter()) < distanceSqr); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// TODO: Handle different tool modes, such as painting. |
|
|
|
|
// TODO: Allow click-dragging which doesn't affect already changed tiles / corners. |
|
|
|
|
// TODO: Use ArrayMesh instead of ImmediateMesh. |
|
|
|
|
// TODO: Dynamically expand terrain instead of having it be a set size. |
|
|
|
|
|
|
|
|
|
// Holds onto all the tiles and which of their corners corners will be affected by this edit operation. |
|
|
|
|
var tilesToChange = new Dictionary<TilePos, Corners<bool>>(); |
|
|
|
|
ref Corners<bool> GetTileToChange(TilePos position) |
|
|
|
|
// Don't look at this black magic. The Dictionary type should have this by default I swear! |
|
|
|
|
=> ref CollectionsMarshal.GetValueRefOrAddDefault(tilesToChange, position, out _dummy); |
|
|
|
|
// Basically, this returns a reference to an entry in the dictionary that can be modified directly. |
|
|
|
|
ref Corners<bool> Tile(TilePos position) => ref CollectionsMarshal.GetValueRefOrAddDefault(tilesToChange, position, out _dummy); |
|
|
|
|
|
|
|
|
|
if (toolMode == ToolMode.Paint) { |
|
|
|
|
// In PAINT mode, only full tiles are ever affected. |
|
|
|
|
// So this makes populating 'tilesToChange' very straight-forward. |
|
|
|
|
|
|
|
|
|
var tiles = toolShape switch { |
|
|
|
|
// While in PAINT mode, the CORNER shape instead affects |
|
|
|
|
// a single tile, regardless of the current 'draw_size'. |
|
|
|
|
ToolShape.Corner => [ hover.Position ], |
|
|
|
|
ToolShape.Circle => GetTilesInRadius(), |
|
|
|
|
ToolShape.Square => GetTilesInSquare(), |
|
|
|
|
_ => throw new InvalidOperationException(), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
foreach (var pos in tiles) |
|
|
|
|
tilesToChange.Add(pos, new(true)); |
|
|
|
|
|
|
|
|
|
} else if (toolShape == ToolShape.Corner) { |
|
|
|
|
// With the CORNER shape, only a single corner is affected. |
|
|
|
|
|
|
|
|
|
if (toolShape == ToolShape.Corner) { |
|
|
|
|
// Modify selected corner itself. |
|
|
|
|
GetTileToChange(hover.Position)[hover.Corner] = true; |
|
|
|
|
Tile(hover.Position)[hover.Corner] = true; |
|
|
|
|
|
|
|
|
|
// If the 'connected_toggle' button is active, move "connected" corners. |
|
|
|
|
// This is a simplified version of the code below that only affects the 3 neighboring corners. |
|
|
|
|
if (isConnected) { |
|
|
|
|
var height = GetTile(hover.Position).Height[hover.Corner]; |
|
|
|
|
foreach (var neighbor in GetNeighbors(hover.Position, hover.Corner)) { |
|
|
|
|
var neighborHeight = GetTile(neighbor.Position).Height[neighbor.Corner]; |
|
|
|
|
if (neighborHeight != height) continue; |
|
|
|
|
GetTileToChange(neighbor.Position)[neighbor.Corner] = true; |
|
|
|
|
Tile(neighbor.Position)[neighbor.Corner] = true; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
|
|
var tiles = (toolShape switch { |
|
|
|
|
ToolShape.Circle => GetTilesInRadius(), |
|
|
|
|
ToolShape.Square => GetTilesInSquare(), |
|
|
|
@ -105,7 +127,7 @@ public partial class Terrain |
|
|
|
|
|
|
|
|
|
// Modify selected tiles themselves. |
|
|
|
|
foreach (var pos in tiles) |
|
|
|
|
GetTileToChange(pos) = new(true); |
|
|
|
|
tilesToChange.Add(pos, new(true)); |
|
|
|
|
|
|
|
|
|
// If the 'connected_toggle' button is active, move "connected" corners. |
|
|
|
|
// Connected corners are the ones that are at the same height as ones already being moved. |
|
|
|
@ -117,22 +139,45 @@ public partial class Terrain |
|
|
|
|
if (tiles.Contains(neighbor.Position)) continue; |
|
|
|
|
var neighborHeight = GetTile(neighbor.Position).Height[neighbor.Corner]; |
|
|
|
|
if (neighborHeight != height) continue; |
|
|
|
|
GetTileToChange(neighbor.Position)[neighbor.Corner] = true; |
|
|
|
|
Tile(neighbor.Position)[neighbor.Corner] = true; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Raise / lower the terrain when left mouse button is pressed. |
|
|
|
|
// Modify the terrain when left mouse button is pressed. |
|
|
|
|
if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) { |
|
|
|
|
prevent_default = true; |
|
|
|
|
|
|
|
|
|
string action; |
|
|
|
|
StringName method; |
|
|
|
|
Variant[] doArgs; |
|
|
|
|
Variant[] undoArgs; |
|
|
|
|
|
|
|
|
|
if (toolMode == ToolMode.Paint) { |
|
|
|
|
// TODO: Support blending somehow. |
|
|
|
|
var tilesPrevious = new List<(TilePos, int)>(); |
|
|
|
|
var tilesChanged = new List<(TilePos, int)>(); |
|
|
|
|
|
|
|
|
|
foreach (var (pos, corners) in tilesToChange) { |
|
|
|
|
var tile = GetTile(pos); |
|
|
|
|
tilesPrevious.Add((pos, tile.TexturePrimary)); |
|
|
|
|
tilesChanged.Add((pos, texture)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
action = "Paint terrain"; |
|
|
|
|
method = nameof(DoModifyTerrainTexture); |
|
|
|
|
doArgs = [ PackTextureData(tilesChanged) ]; |
|
|
|
|
undoArgs = [ PackTextureData(tilesPrevious) ]; |
|
|
|
|
} else { |
|
|
|
|
var tilesPrevious = new List<(TilePos, Corners<float>)>(); |
|
|
|
|
var tilesChanged = new List<(TilePos, Corners<float>)>(); |
|
|
|
|
|
|
|
|
|
const float AdjustHeight = 0.5f; |
|
|
|
|
var amount = isFlatten ? GetTile(hover.Position).Height[hover.Corner] |
|
|
|
|
: isRaise ? AdjustHeight : -AdjustHeight; |
|
|
|
|
|
|
|
|
|
var tilesPrevious = new List<(TilePos, Corners<float>)>(); |
|
|
|
|
var tilesChanged = new List<(TilePos, Corners<float>)>(); |
|
|
|
|
foreach (var (pos, corners) in tilesToChange) { |
|
|
|
|
var tile = GetTile(pos); |
|
|
|
|
tilesPrevious.Add((pos, tile.Height)); |
|
|
|
@ -145,15 +190,22 @@ public partial class Terrain |
|
|
|
|
tilesChanged.Add((pos, newHeight)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
action = isFlatten ? "Flatten terrain" |
|
|
|
|
: isRaise ? "Raise terrain" |
|
|
|
|
: "Lower Terrain"; |
|
|
|
|
method = nameof(DoModifyTerrainHeight); |
|
|
|
|
doArgs = [ PackHeightData(tilesChanged) ]; |
|
|
|
|
undoArgs = [ PackHeightData(tilesPrevious) ]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (EditorUndoRedo is EditorUndoRedoManager undo) { |
|
|
|
|
var name = "Modify terrain height"; // TODO: Change name depending on tool mode. |
|
|
|
|
undo.CreateAction(name, backwardUndoOps: false); |
|
|
|
|
undo.CreateAction(action, backwardUndoOps: false); |
|
|
|
|
|
|
|
|
|
undo.AddDoMethod(this, nameof(DoModifyTerrainHeight), Pack(tilesChanged)); |
|
|
|
|
undo.AddDoMethod(this, method, doArgs); |
|
|
|
|
undo.AddDoMethod(this, nameof(UpdateMeshAndShape)); |
|
|
|
|
undo.AddDoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); |
|
|
|
|
|
|
|
|
|
undo.AddUndoMethod(this, nameof(DoModifyTerrainHeight), Pack(tilesPrevious)); |
|
|
|
|
undo.AddUndoMethod(this, method, undoArgs); |
|
|
|
|
undo.AddUndoMethod(this, nameof(UpdateMeshAndShape)); |
|
|
|
|
undo.AddUndoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); |
|
|
|
|
|
|
|
|
@ -170,13 +222,22 @@ public partial class Terrain |
|
|
|
|
|
|
|
|
|
public void DoModifyTerrainHeight(byte[] data) |
|
|
|
|
{ |
|
|
|
|
foreach (var (pos, corners) in Unpack(data)) { |
|
|
|
|
foreach (var (pos, corners) in UnpackHeightData(data)) { |
|
|
|
|
var tile = GetTile(pos); |
|
|
|
|
tile.Height = corners; |
|
|
|
|
SetTile(pos, tile); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public void DoModifyTerrainTexture(byte[] data) |
|
|
|
|
{ |
|
|
|
|
foreach (var (pos, texture) in UnpackTextureData(data)) { |
|
|
|
|
var tile = GetTile(pos); |
|
|
|
|
tile.TexturePrimary = texture; |
|
|
|
|
SetTile(pos, tile); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void UpdateEditToolMesh(Dictionary<TilePos, Corners<bool>> tiles) |
|
|
|
|
{ |
|
|
|
@ -234,7 +295,8 @@ public partial class Terrain |
|
|
|
|
static IEnumerable<(TilePos Position, Corner Corner)> GetNeighbors(TilePos pos, Corner corner) |
|
|
|
|
=> _offsetLookup[corner].Select(e => (new TilePos(pos.X + e.X, pos.Y + e.Y), e.Opposite)); |
|
|
|
|
|
|
|
|
|
static byte[] Pack(IEnumerable<(TilePos Position, Corners<float> Corners)> data) |
|
|
|
|
|
|
|
|
|
static byte[] PackHeightData(IEnumerable<(TilePos Position, Corners<float> Corners)> data) |
|
|
|
|
{ |
|
|
|
|
using var stream = new MemoryStream(); |
|
|
|
|
using var writer = new BinaryWriter(stream); |
|
|
|
@ -249,7 +311,7 @@ public partial class Terrain |
|
|
|
|
return stream.ToArray(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static IEnumerable<(TilePos Position, Corners<float> Corners)> Unpack(byte[] data) |
|
|
|
|
static IEnumerable<(TilePos Position, Corners<float> Corners)> UnpackHeightData(byte[] data) |
|
|
|
|
{ |
|
|
|
|
using var stream = new MemoryStream(data); |
|
|
|
|
using var reader = new BinaryReader(stream); |
|
|
|
@ -262,4 +324,28 @@ public partial class Terrain |
|
|
|
|
yield return (new(x, y), corners); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static byte[] PackTextureData(IEnumerable<(TilePos Position, int Texture)> data) |
|
|
|
|
{ |
|
|
|
|
using var stream = new MemoryStream(); |
|
|
|
|
using var writer = new BinaryWriter(stream); |
|
|
|
|
foreach (var (pos, texture) in data) { |
|
|
|
|
writer.Write(pos.X); |
|
|
|
|
writer.Write(pos.Y); |
|
|
|
|
writer.Write((byte)texture); |
|
|
|
|
} |
|
|
|
|
return stream.ToArray(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static IEnumerable<(TilePos Position, int Texture)> UnpackTextureData(byte[] data) |
|
|
|
|
{ |
|
|
|
|
using var stream = new MemoryStream(data); |
|
|
|
|
using var reader = new BinaryReader(stream); |
|
|
|
|
while (stream.Position < stream.Length) { |
|
|
|
|
var x = reader.ReadInt32(); |
|
|
|
|
var y = reader.ReadInt32(); |
|
|
|
|
var texture = reader.ReadByte(); |
|
|
|
|
yield return (new(x, y), texture); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|