A game where you get to play as a slime, made with Godot.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

325 lines
11 KiB

using System.IO;
public partial class Terrain
{
// These mirror the modes / shapes in 'terrain_editing_controls.gd'.
enum ToolMode { Height, Flatten, Paint, Erase }
enum ToolShape { Corner, Circle, Square }
// Set by the terrain editing plugin.
// Enables access to the in-editor undo/redo system.
public EditorUndoRedoManager EditorUndoRedo { get; set; }
Material _editToolMaterial;
public override void _EnterTree()
{
_editToolMaterial = new StandardMaterial3D {
VertexColorUseAsAlbedo = true,
BlendMode = BaseMaterial3D.BlendModeEnum.Mix,
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled,
NoDepthTest = true,
};
}
Vector3 _lastPosition;
public bool EditorInput(InputEvent ev, Control controls, Vector3 maybePos)
{
var prevent_default = false;
// If 'maybePosition' is Vector3.Zero, use previous know position, otherwise update.
var position = (maybePos == Vector3.Zero) ? _lastPosition : (_lastPosition = maybePos);
var toolMode = (ToolMode)(int)controls.Get("tool_mode");
var toolShape = (ToolShape)(int)controls.Get("tool_shape");
var texture = (byte)(int)controls.Get("texture");
var drawSize = (int)controls.Get("draw_size");
var isRaise = (bool)controls.Get("is_raise");
var isConnected = (bool)controls.Get("is_connected");
var isFlatten = toolMode == ToolMode.Flatten;
var isCorner = toolShape == ToolShape.Corner;
var hover = ToTilePos(position);
if (isCorner) drawSize = 1;
var isEven = (drawSize % 2) == 0;
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 => (0, 0),
Corner.TopRight => (1, 0),
Corner.BottomRight => (1, 1),
Corner.BottomLeft => (0, 1),
_ => throw new InvalidOperationException(),
};
IEnumerable<TilePos> GetTilesInSquare() {
var min = hover.Position + (-radius, -radius);
var max = hover.Position + (+radius, +radius);
if (isEven) max += (-1, -1);
for (var x = min.X; x <= max.X; x++)
for (var z = min.Z; z <= max.Z; z++)
yield return new(x, z);
}
IEnumerable<TilePos> GetTilesInRadius() {
var center = isEven ? (Vector2I)hover.Position
: hover.Position.Center;
var distanceSqr = Pow(radius + (isEven ? -1 : 1) * 0.25f, 2);
return GetTilesInSquare().Where(tile =>
center.DistanceSquaredTo(tile.Center) < distanceSqr);
}
// TODO: Allow click-dragging which doesn't affect already changed tiles / corners.
// TODO: Use ArrayMesh instead of ImmediateMesh.
// TODO: Support texture blending somehow.
// TODO: Clear empty chunks.
// Data structure for holding tiles to be modified.
var tilesToChange = new Dictionary<TilePos, TileModification>();
// Utility function to set 'Affected' for the specified position and corner.
void SetCornerAffected((TilePos Position, Corner Corner) pair)
=> tilesToChange.GetOrAddNew(pair.Position).Affected[pair.Corner] = true;
// Utility function to get the height for the specified position and corner.
short GetCornerHeight((TilePos Position, Corner Corner) pair)
=> Data.GetTileOrDefault(pair.Position).Height[pair.Corner];
if (toolMode is ToolMode.Paint or ToolMode.Erase) {
// In PAINT or ERASE 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(){ Affected = new(true) });
} else if (toolShape == ToolShape.Corner) {
// With the CORNER shape, only a single corner is affected.
// Modify selected corner itself.
SetCornerAffected(hover);
// 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 = GetCornerHeight(hover);
foreach (var neighbor in GetNeighbors(hover.Position, hover.Corner))
if (GetCornerHeight(neighbor) == height)
SetCornerAffected(neighbor);
}
} else {
var tiles = (toolShape switch {
ToolShape.Circle => GetTilesInRadius(),
ToolShape.Square => GetTilesInSquare(),
_ => throw new InvalidOperationException(),
}).ToHashSet();
// Modify selected tiles themselves.
foreach (var pos in tiles)
tilesToChange.Add(pos, new(){ Affected = 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.
if (isConnected) foreach (var pos in tiles) {
var tile = Data.GetTileOrDefault(pos);
foreach (var corner in Enum.GetValues<Corner>()) {
var height = tile.Height[corner];
foreach (var neighbor in GetNeighbors(pos, corner))
if (!tiles.Contains(neighbor.Position))
if (GetCornerHeight(neighbor) == height)
SetCornerAffected(neighbor);
}
}
}
// Modify the terrain when left mouse button is pressed.
if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) {
prevent_default = true;
// Fill with current tile data.
foreach (var (pos, modified) in tilesToChange)
modified.Original = modified.New = Data.GetTileOrDefault(pos);
string action;
if (toolMode == ToolMode.Paint) {
action = "Paint terrain";
foreach (var (_, modified) in tilesToChange)
modified.New.TexturePrimary = texture;
} else if (toolMode == ToolMode.Erase) {
action = "Erase terrain";
foreach (var (_, modified) in tilesToChange)
modified.New = default;
} else if (isFlatten) {
action = "Flatten terrain";
var amount = GetCornerHeight(hover);
foreach (var (_, modified) in tilesToChange) {
ref var height = ref modified.New.Height;
if (modified.Affected.TopLeft ) height.TopLeft = amount;
if (modified.Affected.TopRight ) height.TopRight = amount;
if (modified.Affected.BottomRight) height.BottomRight = amount;
if (modified.Affected.BottomLeft ) height.BottomLeft = amount;
}
} else {
action = isRaise ? "Raise terrain" : "Lower terrain";
var amount = isRaise ? (short)+1 : (short)-1;
foreach (var (_, modified) in tilesToChange) {
ref var height = ref modified.New.Height;
if (modified.Affected.TopLeft ) height.TopLeft += amount;
if (modified.Affected.TopRight ) height.TopRight += amount;
if (modified.Affected.BottomRight) height.BottomRight += amount;
if (modified.Affected.BottomLeft ) height.BottomLeft += amount;
}
}
if (EditorUndoRedo is EditorUndoRedoManager undo) {
undo.CreateAction(action, backwardUndoOps: false);
var doData = Pack(tilesToChange.Select(e => (e.Key, e.Value.New)));
var undoData = Pack(tilesToChange.Select(e => (e.Key, e.Value.Original)));
undo.AddDoMethod(this, nameof(DoModifyTerrain), doData);
undo.AddDoMethod(this, nameof(UpdateMeshAndShape));
undo.AddDoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged);
undo.AddUndoMethod(this, nameof(DoModifyTerrain), undoData);
undo.AddUndoMethod(this, nameof(UpdateMeshAndShape));
undo.AddUndoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged);
undo.CommitAction(true);
}
}
UpdateEditToolMesh(tilesToChange.Select(e => (e.Key, e.Value.Affected)));
return prevent_default;
}
class TileModification
{
public Corners<bool> Affected;
public Tile Original;
public Tile New;
}
public void EditorUnfocus()
=> ClearEditToolMesh();
public void DoModifyTerrain(byte[] data)
{
foreach (var (pos, tile) in Unpack(data))
Data[pos] = tile;
}
void UpdateEditToolMesh(IEnumerable<(TilePos, Corners<bool>)> tiles)
{
var mesh = GetOrCreateMesh("EditToolMesh");
mesh.ClearSurfaces();
mesh.SurfaceBegin(Mesh.PrimitiveType.Lines);
void AddLine((Vector3 Position, bool Visible) start,
(Vector3 Position, bool Visible) end) {
mesh.SurfaceSetColor(start.Visible ? Colors.Blue : Colors.Transparent);
mesh.SurfaceAddVertex(start.Position);
mesh.SurfaceSetColor(end.Visible ? Colors.Blue : Colors.Transparent);
mesh.SurfaceAddVertex(end.Position);
}
foreach (var (tile, visible) in tiles) {
var positions = ToPositions(tile);
foreach (var side in Enum.GetValues<Side>()) {
var (corner1, corner2) = side.GetCorners();
if (!visible[corner1] && !visible[corner2]) continue;
AddLine((positions[corner1], visible[corner1]),
(positions[corner2], visible[corner2]));
}
}
mesh.SurfaceEnd();
mesh.SurfaceSetMaterial(0, _editToolMaterial);
}
void ClearEditToolMesh()
=> GetNodeOrNull("EditToolMesh")?.QueueFree();
(TilePos Position, Corner Corner) ToTilePos(Vector3 position)
{
var local = ToLocal(position);
var coord = new Vector2(local.X, local.Z) / TileSize;
var corner = coord.PosMod(1).RoundToVector2I() switch {
(0, 0) => Corner.TopLeft,
(1, 0) => Corner.TopRight,
(1, 1) => Corner.BottomRight,
(0, 1) => Corner.BottomLeft,
_ => throw new InvalidOperationException(),
};
return ((TilePos)coord, corner);
}
static readonly Dictionary<Corner, (int X, int Z, Corner Opposite)[]> _offsetLookup = new(){
[Corner.TopLeft ] = [(-1, -1, Corner.BottomRight), (-1, 0, Corner.TopRight ), (0, -1, Corner.BottomLeft )],
[Corner.TopRight ] = [(+1, -1, Corner.BottomLeft ), (+1, 0, Corner.TopLeft ), (0, -1, Corner.BottomRight)],
[Corner.BottomRight] = [(+1, +1, Corner.TopLeft ), (+1, 0, Corner.BottomLeft ), (0, +1, Corner.TopRight )],
[Corner.BottomLeft ] = [(-1, +1, Corner.TopRight ), (-1, 0, Corner.BottomRight), (0, +1, Corner.TopLeft )],
};
static IEnumerable<(TilePos Position, Corner Corner)> GetNeighbors(TilePos pos, Corner corner)
=> _offsetLookup[corner].Select(e => (new TilePos(pos.X + e.X, pos.Z + e.Z), e.Opposite));
static byte[] Pack(IEnumerable<(TilePos Position, Tile tile)> data)
{
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream);
foreach (var (pos, tile) in data) {
writer.Write(pos.X);
writer.Write(pos.Z);
writer.Write(tile.Height.TopLeft);
writer.Write(tile.Height.TopRight);
writer.Write(tile.Height.BottomRight);
writer.Write(tile.Height.BottomLeft);
writer.Write(tile.TexturePrimary);
writer.Write(tile.TextureSecondary);
writer.Write(tile.TextureBlend);
}
return stream.ToArray();
}
static IEnumerable<(TilePos Position, Tile tile)> Unpack(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 height = new Corners<short>(
reader.ReadInt16(), reader.ReadInt16(),
reader.ReadInt16(), reader.ReadInt16());
var texPrimary = reader.ReadByte();
var texSecondary = reader.ReadByte();
var texBlend = reader.ReadByte();
yield return (new(x, y), new(){
Height = height,
TexturePrimary = texPrimary,
TextureSecondary = texSecondary,
TextureBlend = texBlend,
});
}
}
}