Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CentrED/Map/MapManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -848,9 +848,9 @@ public Vector3 Unproject(int x, int y, int z)
);
}

private bool CanDrawLand(LandObject lo)
public bool CanDrawLand(LandObject lo)
{
if(!ShowLand || (lo.Tile.Id <= 2 && !ShowNoDraw))
if(!ShowLand || (lo.Tile.Id <= 2 && !ShowNoDraw))
return false;
return WithinZRange(lo.Tile.Z);
}
Expand Down
217 changes: 213 additions & 4 deletions CentrED/Tools/LandBrushTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ public class LandBrushTool : BaseTool

private bool _fixedZ = false;
private int _fixedHeightZ = 0, _randomZ = 0;
private bool _fixEdges = false;
private bool _fixInside = false;
private string _activeLandBrushName;
private LandBrushManagerWindow _manager => UIManager.GetWindow<LandBrushManagerWindow>();

public LandBrushTool()
{
_activeLandBrushName = ProfileManager.ActiveProfile.LandBrush.Keys.FirstOrDefault("");
Expand All @@ -36,8 +38,17 @@ internal override void Draw()
{
_activeLandBrushName = ProfileManager.ActiveProfile.LandBrush.Keys.FirstOrDefault("");
}

_manager.LandBrushCombo(ref _activeLandBrushName);
ImGui.Checkbox("Fix edges", ref _fixEdges);
ImGui.SetItemTooltip
("Repair mode: keeps the existing shapes and only corrects the selected brush's transition tiles. Nothing is painted.");
if (_fixEdges)
{
ImGui.Checkbox("Fix inside", ref _fixInside);
ImGui.SetItemTooltip
("Also redraw the shape's interior with randomized full tiles.");
}
ImGui.Checkbox(LangManager.Get(FIXED_Z), ref _fixedZ);
if (_fixedZ)
{
Expand Down Expand Up @@ -65,8 +76,16 @@ private sbyte CalculateNewZ(sbyte height)

protected override void GhostApply(TileObject? o)
{
if (o is not LandObject lo)
return;

if (_fixEdges)
{
RepairTile(lo.Tile.X, lo.Tile.Y);
return;
}

var defaultTransitionDirection = Direction.Up;
if (o is LandObject lo)
{
var direction = AddTransistion(lo, defaultTransitionDirection);
var offsets = CalculateOffsets(direction);
Expand All @@ -77,7 +96,7 @@ protected override void GhostApply(TileObject? o)
if (Client.IsValidX(newX) && Client.IsValidY(newY))
{
var tile = MapManager.LandTiles[newX, newY];
if (tile != null)
if (tile != null && MapManager.CanDrawLand(tile))
{
AddTransistion(tile, valueTuple.Value);
}
Expand Down Expand Up @@ -334,6 +353,196 @@ out string toBrushName
return result;
}

#region Fix edges (repair mode)

// A tile's art is determined by which brush owns each of its four diamond corners
// (Up=NW, Right=NE, Down=SE, Left=SW); each corner is shared with three neighbours.

private static readonly Direction[] LatticeCorners = { Direction.Up, Direction.Right, Direction.Down, Direction.Left };
private static readonly Direction CornerMask = Direction.Up | Direction.Right | Direction.Down | Direction.Left;

private static Direction Complement(Direction d) => (Direction)(byte)~(byte)d;

private static Direction CornerFromOffset(int dx, int dy) => (dx, dy) switch
{
(-1, -1) => Direction.Up,
(1, -1) => Direction.Right,
(1, 1) => Direction.Down,
(-1, 1) => Direction.Left,
_ => Direction.None,
};

private ushort EffectiveId(int x, int y)
{
if (!Client.IsValidX(x) || !Client.IsValidY(y))
return 0;
var lo = MapManager.LandTiles[x, y];
if (lo == null)
return 0;
if (MapManager.GhostLandTiles.TryGetValue(lo, out var ghost))
return ghost.Tile.Id;
return lo.Tile.Id;
}

// Full tiles give from==to; ambiguous ids prefer a pair involving the active brush.
private bool TryGetPair(ushort tileId, out string from, out string to, out Direction dir)
{
from = to = "";
dir = Direction.None;
if (!_manager.tileToLandBrushNames.TryGetValue(tileId, out var pairs) || pairs.Count == 0)
return false;
var best = pairs[0];
foreach (var p in pairs)
{
if (p.Item1 == _activeLandBrushName || p.Item2 == _activeLandBrushName)
{
best = p;
break;
}
}
from = best.Item1;
to = best.Item2;
if (from != to)
{
var lb = ProfileManager.ActiveProfile.LandBrush;
if (lb.TryGetValue(from, out var fb) && fb.Transitions.TryGetValue(to, out var lst))
{
var tr = lst.FirstOrDefault(t => t.TileID == tileId);
if (tr != null)
dir = tr.Direction;
}
}
return true;
}

private string? TileCornerBrush(int x, int y, Direction corner)
{
var id = EffectiveId(x, y);
if (id == 0 || !TryGetPair(id, out var from, out var to, out var dir))
return null;
if (from == to)
return from;
return (dir & corner) != Direction.None ? to : from;
}

// Majority vote of the four tiles sharing the point, tie-broken toward the active brush.
private string? LatticeBrush(int x, int y, Direction corner)
{
var (dx, dy) = corner.Offset();
Span<(int tx, int ty, Direction c)> shares = stackalloc (int, int, Direction)[4]
{
(x, y, corner),
(x + dx, y, CornerFromOffset(-dx, dy)),
(x, y + dy, CornerFromOffset(dx, -dy)),
(x + dx, y + dy, CornerFromOffset(-dx, -dy)),
};
var votes = new Dictionary<string, int>();
foreach (var (tx, ty, c) in shares)
{
var b = TileCornerBrush(tx, ty, c);
if (b == null)
continue;
votes.TryGetValue(b, out var n);
votes[b] = n + 1;
}
if (votes.Count == 0)
return null;
var max = votes.Values.Max();
if (votes.TryGetValue(_activeLandBrushName, out var av) && av == max)
return _activeLandBrushName;
return votes.First(kv => kv.Value == max).Key;
}

// Corner order Up,Right,Down,Left.
private List<ushort> ValidTiles(string?[] cornerBrushes)
{
var result = new List<ushort>();
if (cornerBrushes.Any(b => b == null))
return result;
var distinct = cornerBrushes.Distinct().ToList();
var lb = ProfileManager.ActiveProfile.LandBrush;
if (distinct.Count == 1)
{
if (lb.TryGetValue(distinct[0]!, out var b))
result.AddRange(b.Tiles);
return result;
}
if (distinct.Count > 2)
return result; // no art for three-brush junctions
string a = distinct[0]!, other = distinct[1]!;
Direction otherBits = Direction.None;
for (var i = 0; i < 4; i++)
{
if (cornerBrushes[i] == other)
otherBits |= LatticeCorners[i];
}
if (lb.TryGetValue(a, out var ab) && ab.Transitions.TryGetValue(other, out var fwd))
foreach (var t in fwd)
if ((t.Direction & CornerMask) == otherBits)
result.Add(t.TileID);
if (lb.TryGetValue(other, out var ob) && ob.Transitions.TryGetValue(a, out var rev))
foreach (var t in rev)
if ((Complement(t.Direction) & CornerMask) == otherBits)
result.Add(t.TileID);
return result;
}

private void SetGhost(LandObject lo, ushort newId)
{
lo.Visible = false;
if (MapManager.GhostLandTiles.TryGetValue(lo, out var ghost))
{
ghost.LandTile.Id = newId;
ghost.UpdateId(newId);
}
else
{
MapManager.GhostLandTiles[lo] = new LandObject(new LandTile(newId, lo.Tile.X, lo.Tile.Y, lo.Tile.Z));
}
}

private void RepairTile(int x, int y)
{
if (!Client.IsValidX(x) || !Client.IsValidY(y))
return;
var lo = MapManager.LandTiles[x, y];
if (lo == null || !MapManager.CanDrawLand(lo))
return;
if (!ProfileManager.ActiveProfile.LandBrush.TryGetValue(_activeLandBrushName, out var active))
return;

var corners = new string?[4];
for (var i = 0; i < 4; i++)
corners[i] = LatticeBrush(x, y, LatticeCorners[i]);
if (corners.Any(c => c == null))
return;

var distinct = corners.Distinct().ToList();
if (distinct.Count == 1)
{
if (distinct[0] != _activeLandBrushName || !_fixInside)
return;
if (MapManager.GhostLandTiles.ContainsKey(lo))
return; // one re-roll per drag, not per mouse move
if (active.Tiles.Count == 0)
return;
SetGhost(lo, active.Tiles[Random.Shared.Next(active.Tiles.Count)]);
return;
}

if (!distinct.Contains(_activeLandBrushName))
return;
var valid = ValidTiles(corners);
if (valid.Count == 0)
return;
var currentId = EffectiveId(x, y);
if (valid.Contains(currentId))
return;
SetGhost(lo, valid[Random.Shared.Next(valid.Count)]);
}

#endregion

protected override void GhostClear(TileObject? o)
{
if (o is LandObject lo)
Expand Down
Loading