diff --git a/src/TSMapEditor/Mutations/Classes/HeightMutations/AlterElevationMutationBase.cs b/src/TSMapEditor/Mutations/Classes/HeightMutations/AlterElevationMutationBase.cs index 804e4a973..b184c4a1b 100644 --- a/src/TSMapEditor/Mutations/Classes/HeightMutations/AlterElevationMutationBase.cs +++ b/src/TSMapEditor/Mutations/Classes/HeightMutations/AlterElevationMutationBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using TSMapEditor.CCEngine; using TSMapEditor.GameMath; @@ -20,24 +20,15 @@ protected AlterElevationMutationBase(IMutationTarget mutationTarget, Point2D ori protected readonly BrushSize BrushSize; protected readonly TileSet RampTileSet; - protected List cellsToProcess = new List(); - protected List processedCellsThisIteration = new List(); - protected List totalProcessedCells = new List(); protected List undoData = new List(); protected static readonly Point2D[] SurroundingTiles = new Point2D[] { new Point2D(-1, 0), new Point2D(1, 0), new Point2D(0, -1), new Point2D(0, 1), new Point2D(-1, -1), new Point2D(-1, 1), new Point2D(1, -1), new Point2D(1, 1) }; - protected bool IsCellMorphable(MapTile cell) - { - return Map.IsCellMorphable(cell); - } + protected bool IsCellMorphable(MapTile cell) => Map.IsCellMorphable(cell); protected void Clear() { - cellsToProcess.Clear(); - processedCellsThisIteration.Clear(); - totalProcessedCells.Clear(); undoData.Clear(); } @@ -57,112 +48,85 @@ protected void AddCellToUndoData(Point2D cellCoords) undoData.Add(new AlterGroundElevationUndoData(cellCoords, cell.TileIndex, cell.SubTileIndex, cell.Level)); } - protected void RegisterCell(Point2D cellCoords) - { - if (processedCellsThisIteration.Contains(cellCoords) || cellsToProcess.Contains(cellCoords) || totalProcessedCells.Contains(cellCoords)) - return; - - cellsToProcess.Add(cellCoords); - } - - protected void MarkCellAsProcessed(Point2D cellCoords) - { - processedCellsThisIteration.Add(cellCoords); - cellsToProcess.Remove(cellCoords); - - if (!totalProcessedCells.Contains(cellCoords)) - totalProcessedCells.Add(cellCoords); - } + /// + /// Runs a corner-field smoothing pass that sets each targeted cell to a uniform + /// height and then smooths the surrounding terrain, applying ramps where needed. + /// Returns the list of cells that were changed, or null if the edit was rejected + /// because it would have had to alter terrain anchored by an immutable cell, in + /// which case nothing on the map was changed. + /// + protected List SmoothFlat(List targetedCells, int newLevel, HeightFloodMode mode, bool allowSteep) + => SmoothFlat(targetedCells, null, newLevel, mode, allowSteep); /// - /// Main function for ground height level alteration. - /// Prior to this, a derived class has already raised or lowered - /// some target cells. Now we need to figure out what kinds of changes - /// to the map are necessary for these altered height levels to look smooth. - /// - /// Algorithm goes as follows: - /// - /// 1) Check surrounding cells for height differences of 2 levels, if found then - /// raise the relevant cells to lower the difference to only 1 level. - /// Repeat this process recursively until there are no cells to change. - /// - /// 2) Apply some miscellaneous cell height fixes, the game does not have ramps for - /// every potential case. - /// - /// 3) Check the affected cells and their neighbours for necessary ramp changes. + /// As , but with a + /// second set of cells that are only seeded if they legally can be. A required cell that + /// cannot be seeded (it would move a corner anchored by immutable terrain) rejects the + /// whole edit; an optional cell that cannot be seeded is simply skipped. /// - protected void Process() + protected List SmoothFlat(List requiredCells, List optionalCells, int newLevel, HeightFloodMode mode, bool allowSteep) { - ProcessCells(); - - CellHeightFixes(); + var allCells = new List(requiredCells); + if (optionalCells != null) + allCells.AddRange(optionalCells); - ApplyRamps(); + if (allCells.Count == 0) + return null; - RefreshLighting(); + GetCellBounds(allCells, out int minX, out int minY, out int maxX, out int maxY); - MutationTarget.InvalidateMap(); - } + var field = new CornerHeightField(Map, minX, minY, maxX, maxY); + field.Build(); - protected void ProcessCells() - { - while (cellsToProcess.Count > 0) + foreach (var cellCoords in requiredCells) { - var cellsCopy = new List(cellsToProcess); - cellsToProcess.Clear(); - processedCellsThisIteration.Clear(); - - foreach (var cell in cellsCopy) - CheckCell(cell); + if (!field.TrySeedFlat(cellCoords, newLevel)) + return null; } - } - - protected abstract void CheckCell(Point2D cellCoords); - protected abstract TransitionRampInfo[] GetTransitionRampInfos(); - - protected abstract TransitionRampInfo[] GetHeightFixers(); - - protected abstract void CellHeightFixes(); + if (optionalCells != null) + { + foreach (var cellCoords in optionalCells) + { + if (field.CanSeedFlat(cellCoords, newLevel)) + field.TrySeedFlat(cellCoords, newLevel); + } + } - protected abstract void ApplyRamps(); + return RunSmoothing(field, mode, allowSteep); + } - protected void CellHeightFix_DifferentHeightDiffsOnStraightLine(Point2D cellCoords, Point2D offset1, Point2D offset2) + /// + /// Floods, writes back and refreshes lighting for an already-built and already-seeded + /// field. Returns the changed cells, or null if the edit was rejected. + /// + protected List RunSmoothing(CornerHeightField field, HeightFloodMode mode, bool allowSteep) { - var cell = Map.GetTile(cellCoords); - if (cell == null) - return; + if (!field.Flood(mode, allowSteep)) + return null; - if (!IsCellMorphable(cell)) - return; - - int totalLevelDifference = 0; - - Point2D otherCellCoords = cellCoords + offset1; - var otherCell = Map.GetTile(otherCellCoords); - if (otherCell != null && IsCellMorphable(otherCell)) - totalLevelDifference += otherCell.Level - cell.Level; + var changedCells = field.WriteBack(allowSteep, AddCellToUndoData); - otherCellCoords = cellCoords + offset2; - otherCell = Map.GetTile(otherCellCoords); - if (otherCell != null && IsCellMorphable(otherCell)) - totalLevelDifference += otherCell.Level - cell.Level; + foreach (var cell in changedCells) + RefreshCellLighting(cell); - if (totalLevelDifference >= 3) - { - AddCellToUndoData(cell.CoordsToPoint()); - cell.ChangeTileIndex(0, 0); - cell.Level++; - } + MutationTarget.InvalidateMap(); + return changedCells; } - private void RefreshLighting() + private static void GetCellBounds(List cells, out int minX, out int minY, out int maxX, out int maxY) { - for (int i = 0; i < totalProcessedCells.Count; i++) + minX = int.MaxValue; + minY = int.MaxValue; + maxX = int.MinValue; + maxY = int.MinValue; + + foreach (var cell in cells) { - var cellCoords = totalProcessedCells[i]; - var cell = Map.GetTile(cellCoords); - RefreshCellLighting(cell); + if (cell.X < minX) minX = cell.X; + if (cell.Y < minY) minY = cell.Y; + if (cell.X > maxX) maxX = cell.X; + if (cell.Y > maxY) maxY = cell.Y; } } diff --git a/src/TSMapEditor/Mutations/Classes/HeightMutations/CornerHeightField.cs b/src/TSMapEditor/Mutations/Classes/HeightMutations/CornerHeightField.cs new file mode 100644 index 000000000..2b7ed5a93 --- /dev/null +++ b/src/TSMapEditor/Mutations/Classes/HeightMutations/CornerHeightField.cs @@ -0,0 +1,664 @@ +using System; +using System.Collections.Generic; +using TSMapEditor.CCEngine; +using TSMapEditor.GameMath; +using TSMapEditor.Models; +using TSMapEditor.Models.Enums; + +namespace TSMapEditor.Mutations.Classes.HeightMutations +{ + /// + /// The direction in which a flood-fill smoothing pass is allowed to move cell corners. + /// + public enum HeightFloodMode + { + /// Only raise corners that are too low (used when raising ground). + Up, + + /// Only lower corners that are too high (used when lowering ground). + Down, + + /// + /// Move corners either way toward their neighbours (used when flattening ground). + /// Implemented as a downward pass followed by an upward pass so that every corner + /// moves monotonically and the field always converges. + /// + Both + } + + /// + /// A transient cell-corner height field used for "smart" ground-height smoothing. + /// + /// Instead of operating on per-cell height levels and guessing ramps from neighbour + /// patterns, this assigns a height to every cell corner, smooths the corner + /// field so neighbouring corners never differ by more than the allowed slope, and then + /// derives each cell's ramp as a pure function of its four corner heights. + /// + /// Heights are expressed in whole map height levels: a normal ramp raises a corner by + /// one level, a steep ramp raises its far corner by two levels. + /// + public class CornerHeightField + { + /// + /// Corner point offsets from a cell, in the order used by . + /// Verified against RaiseGroundMutationBase.CreateSmallHill: index 0..3 are the + /// cell's NW, NE, SE and SW corner points respectively. + /// + public static readonly Point2D[] CornerOffsets = + { + new Point2D(0, 0), new Point2D(1, 0), new Point2D(1, 1), new Point2D(0, 1) + }; + + // Per-ramp corner heights in level units, in CornerOffsets order, indexed by RampType. + // Each row is the height of the four corners for that ramp type: normal ramps raise a + // corner by one level, steep ramps raise their far corner by two. Rows 19/20 + // (DoubleUp/DownNWSE) duplicate 17/18 and exist only so existing tiles of those types + // reconstruct correctly; they are never emitted. + private static readonly int[][] RampCornerHeights = + { + new[] { 0, 0, 0, 0 }, // None + new[] { 0, 1, 1, 0 }, // West + new[] { 0, 0, 1, 1 }, // North + new[] { 1, 0, 0, 1 }, // East + new[] { 1, 1, 0, 0 }, // South + new[] { 0, 0, 1, 0 }, // CornerNW + new[] { 0, 0, 0, 1 }, // CornerNE + new[] { 1, 0, 0, 0 }, // CornerSE + new[] { 0, 1, 0, 0 }, // CornerSW + new[] { 0, 1, 1, 1 }, // MidNW + new[] { 1, 0, 1, 1 }, // MidNE + new[] { 1, 1, 0, 1 }, // MidSE + new[] { 1, 1, 1, 0 }, // MidSW + new[] { 0, 1, 2, 1 }, // SteepSE + new[] { 1, 0, 1, 2 }, // SteepSW + new[] { 2, 1, 0, 1 }, // SteepNW + new[] { 1, 2, 1, 0 }, // SteepNE + new[] { 0, 1, 0, 1 }, // DoubleUpSWNE + new[] { 1, 0, 1, 0 }, // DoubleDownSWNE + new[] { 0, 1, 0, 1 }, // DoubleUpNWSE (duplicate of DoubleUpSWNE) + new[] { 1, 0, 1, 0 }, // DoubleDownNWSE (duplicate of DoubleDownSWNE) + }; + + // The highest ramp index that may be emitted. Rows above this duplicate earlier rows. + private const int LastEmittedRamp = 18; + + // Reverse lookup: normalized 4-corner pattern (base-3 key) -> RampType. Built from + // rows 0..18 only, first match wins, so the ambiguous double patterns + // {0,1,0,1}/{1,0,1,0} canonically resolve to the SWNE variants (17/18) and the + // duplicate NWSE rows are never emitted. + private static readonly Dictionary RampByCornerPattern = BuildReverseLookup(); + + public CornerHeightField(Map map, int cellMinX, int cellMinY, int cellMaxX, int cellMaxY) + { + this.map = map; + rampTileSetStart = map.TheaterInstance.Theater.RampTileSet.StartTileIndex; + + // A single height change can ripple outward up to MaxMapHeightLevel cells (each + // level of difference pushes the smoothing one cell further). Expand the working + // region by that much (+2 slack) so a flood started inside never needs to + // reference a corner outside the region. + int margin = Constants.MaxMapHeightLevel + 2; + originX = Math.Max(0, cellMinX - margin); + originY = Math.Max(0, cellMinY - margin); + int regionMaxCellX = cellMaxX + margin; + int regionMaxCellY = cellMaxY + margin; + + // Cells [originX..regionMaxCellX] own corner points [originX..regionMaxCellX + 1]. + pointWidth = (regionMaxCellX - originX) + 2; + pointHeight = (regionMaxCellY - originY) + 2; + + heights = new int[pointWidth, pointHeight]; + rigid = new bool[pointWidth, pointHeight]; + done = new bool[pointWidth, pointHeight]; + hasOwner = new bool[pointWidth, pointHeight]; + } + + private readonly Map map; + private readonly int rampTileSetStart; + + private readonly int originX; + private readonly int originY; + private readonly int pointWidth; + private readonly int pointHeight; + + private readonly int[,] heights; + private readonly bool[,] rigid; + private readonly bool[,] done; + private readonly bool[,] hasOwner; + + private readonly Queue worklist = new Queue(); + + /// + /// Reconstructs the corner-height field from the current terrain in the region. + /// + public void Build() + { + for (int iy = 0; iy < pointHeight; iy++) + { + for (int ix = 0; ix < pointWidth; ix++) + { + int cellX = originX + ix; + int cellY = originY + iy; + + // A corner takes its height from a single owning cell (the cell whose + // NW corner this point is). This keeps the field well-defined even on + // hand-edited maps where neighbours disagree on a shared corner. + var owner = map.GetTile(cellX, cellY); + if (owner == null) + { + hasOwner[ix, iy] = false; + heights[ix, iy] = 0; + } + else + { + hasOwner[ix, iy] = true; + heights[ix, iy] = owner.Level + RampCornerHeights[GetRampIndex(owner)][0]; + } + + // A corner is rigid if any of the up-to-four cells touching it cannot + // be morphed (or is off the map). + rigid[ix, iy] = IsCellRigid(cellX, cellY) || IsCellRigid(cellX - 1, cellY) || + IsCellRigid(cellX - 1, cellY - 1) || IsCellRigid(cellX, cellY - 1); + + done[ix, iy] = false; + } + } + } + + /// + /// Seeds a cell to a uniform height (used to raise/lower/flatten a targeted cell). + /// A corner anchored by an immutable cell that cannot take the height itself is + /// snapped to the nearest height the immutable terrain actually has at that corner, + /// if one exists within a one-level slope of the target — so a cell flattened against + /// e.g. a cliff lip becomes a ramp rising to the lip. Returns false if some corner + /// could not legally be seeded, in which case the whole operation must be rejected. + /// + public bool TrySeedFlat(Point2D cellCoords, int level) + { + level = Math.Clamp(level, 0, Constants.MaxMapHeightLevel); + + bool ok = true; + for (int i = 0; i < CornerOffsets.Length; i++) + { + var pt = cellCoords + CornerOffsets[i]; + if (!TryResolveSeedCorner(pt.X, pt.Y, level, out int resolved)) + { + ok = false; + continue; + } + + SetCornerDirect(pt.X, pt.Y, resolved); + } + + return ok; + } + + /// + /// Adjusts a single corner by a delta (used to raise the shared centre corner of a + /// 2x2 "small hill"). Returns false if the corner is anchored by an immutable cell. + /// + public bool TrySeedAdjustCorner(Point2D point, int delta) + { + if (!InRegion(point.X, point.Y)) + return false; + + int current = heights[point.X - originX, point.Y - originY]; + return TrySetCorner(point.X, point.Y, current + delta); + } + + /// + /// Read-only check of whether would succeed for a cell. + /// Used to skip (rather than abort on) ramp cells that are already at the desired level + /// but happen to be pinned by adjacent immutable terrain. + /// + public bool CanSeedFlat(Point2D cellCoords, int level) + { + level = Math.Clamp(level, 0, Constants.MaxMapHeightLevel); + + for (int i = 0; i < CornerOffsets.Length; i++) + { + var pt = cellCoords + CornerOffsets[i]; + if (!TryResolveSeedCorner(pt.X, pt.Y, level, out _)) + return false; + } + + return true; + } + + /// + /// Determines the height a corner should take when its cell is seeded to + /// : the level itself for free corners, or the nearest height + /// the immutable terrain actually has at the corner (within a one-level slope of the + /// target) for anchored corners. Returns false if the corner cannot legally be seeded. + /// + private bool TryResolveSeedCorner(int px, int py, int level, out int resolved) + { + resolved = level; + + if (!InRegion(px, py)) + return false; + + int ix = px - originX; + int iy = py - originY; + + // Off-map / outside-diamond corners are a free boundary; there is nothing to seed. + if (!hasOwner[ix, iy]) + return true; + + if (!rigid[ix, iy]) + return true; + + // Anchored corners always snap to a height the immutable terrain actually has at + // this corner. This makes the result independent of the order in which cells are + // brushed: a cell flattened one level below a cliff lip, for example, always gets + // its lip corners at the lip height (becoming a ramp), never a stale in-between + // value from earlier smoothing. + int? snapped = GetBestExactHeight(px, py, level - 1, level + 1, level); + if (snapped == null) + return false; + + resolved = snapped.Value; + return true; + } + + private void SetCornerDirect(int px, int py, int height) + { + int ix = px - originX; + int iy = py - originY; + + if (!hasOwner[ix, iy]) + return; + + heights[ix, iy] = height; + done[ix, iy] = true; + worklist.Enqueue(new Point2D(px, py)); + } + + private bool TrySetCorner(int px, int py, int newHeight) + { + newHeight = Math.Clamp(newHeight, 0, Constants.MaxMapHeightLevel); + + if (!InRegion(px, py)) + return false; + + if (!CornerCanTake(px, py, newHeight)) + return false; + + int ix = px - originX; + int iy = py - originY; + + // Off-map / outside-diamond corners are a free boundary; there is nothing to seed. + if (!hasOwner[ix, iy]) + return true; + + heights[ix, iy] = newHeight; + done[ix, iy] = true; + worklist.Enqueue(new Point2D(px, py)); + return true; + } + + /// + /// Whether a corner may legally be set to . Off-map corners + /// are a free boundary (a no-op, not a rejection). A rigid corner may only take a height + /// within the span that the immutable terrain touching it covers: a cliff face spans from + /// its base level to its top level, so ground may legally meet it at any height in + /// between. Flat immutable terrain (e.g. water, or a cliff base) has a single-height + /// span, keeping mismatched edits against it rejected. + /// + private bool CornerCanTake(int px, int py, int newHeight) + { + int ix = px - originX; + int iy = py - originY; + + if (!hasOwner[ix, iy]) + return true; + + if (rigid[ix, iy] && heights[ix, iy] != newHeight) + { + GetAdmissibleRange(px, py, out int admMin, out int admMax, out _); + if (newHeight < admMin || newHeight > admMax) + return false; + } + + return true; + } + + /// + /// Smooths the field so that neighbouring corners differ by no more than the allowed + /// slope. Returns false if the edit cannot be smoothed without moving a corner that + /// is anchored by an immutable cell, in which case nothing has been written to the map. + /// + public bool Flood(HeightFloodMode mode, bool allowSteep) + { + if (mode == HeightFloodMode.Both) + { + if (!FloodPass(HeightFloodMode.Down, allowSteep)) + return false; + + RequeueAllDone(); + + return FloodPass(HeightFloodMode.Up, allowSteep); + } + + return FloodPass(mode, allowSteep); + } + + private bool FloodPass(HeightFloodMode mode, bool allowSteep) + { + while (worklist.Count > 0) + { + var p = worklist.Dequeue(); + int six = p.X - originX; + int siy = p.Y - originY; + int startHeight = heights[six, siy]; + bool startRigid = rigid[six, siy]; + + for (int dy = -1; dy <= 1; dy++) + { + for (int dx = -1; dx <= 1; dx++) + { + if (dx == 0 && dy == 0) + continue; + + int nx = p.X + dx; + int ny = p.Y + dy; + if (!InRegion(nx, ny)) + continue; // the region margin guarantees a real ripple never reaches the edge + + int nix = nx - originX; + int niy = ny - originY; + + // Corners with no owning cell (off the map / outside the iso diamond) + // are a free boundary, not a constraint: they neither move nor reject. + if (!hasOwner[nix, niy]) + continue; + + bool diagonal = dx != 0 && dy != 0; + int threshold = (diagonal && allowSteep) ? 2 : 1; + + int neighborHeight = heights[nix, niy]; + int diff = neighborHeight - startHeight; + if (Math.Abs(diff) <= threshold) + continue; + + if (rigid[nix, niy]) + { + // A slope violation between two rigid corners is the immutable + // terrain's own geometry (e.g. a diagonal cliff cell whose art spans + // its full height within one cell) — never a conflict to resolve or + // reject over. + if (startRigid) + continue; + + // A rigid corner may only move within the range of heights that the + // immutable terrain touching it implies (a cliff face, for example, + // spans from its base level to its top level, so ground may meet it + // at any height in between). If the start corner is out of slope + // range of even that span, the two cannot legally coexist — this + // happens around art anomalies such as exposed diagonal cliff ends. + // The immutable terrain wins: yield the morphable start corner into + // compliance instead of rejecting the whole edit. + GetAdmissibleRange(nx, ny, out int admMin, out int admMax, out bool bordersMorphable); + if (startHeight < admMin - threshold || startHeight > admMax + threshold) + { + startHeight = Math.Clamp(startHeight, admMin - threshold, admMax + threshold); + heights[six, siy] = startHeight; + done[six, siy] = true; + worklist.Enqueue(p); + } + + // Slide the corner along the immutable face just far enough to be in + // slope range of the start corner. This ignores the pass direction on + // purpose: the move is bounded by the art span either way, and without + // it a corner can be left pinned at the wrong end of the face, + // producing "holes" (cells skipped by the write-back spread guard). + // Corners interior to immutable terrain (no morphable cell touches + // them) are left alone: moving them has no visual meaning and would + // create false conflicts inside cliff bodies. + int target = Math.Clamp( + Math.Clamp(neighborHeight, startHeight - threshold, startHeight + threshold), + admMin, admMax); + + if (bordersMorphable && target != neighborHeight) + { + // Mark done so the touching cells are written back as ramps, but + // do NOT enqueue: the flood must never propagate through immutable + // terrain to its far side. + heights[nix, niy] = target; + done[nix, niy] = true; + } + + continue; + } + + int newHeight = neighborHeight; + if (diff < 0) + { + // Neighbour is too low; raise it (unless we are only lowering). + if (mode != HeightFloodMode.Down) + newHeight = startHeight - threshold; + } + else + { + // Neighbour is too high; lower it (unless we are only raising). + if (mode != HeightFloodMode.Up) + newHeight = startHeight + threshold; + } + + if (newHeight != neighborHeight) + { + heights[nix, niy] = newHeight; + done[nix, niy] = true; + worklist.Enqueue(new Point2D(nx, ny)); + } + } + } + } + + return true; + } + + private void RequeueAllDone() + { + for (int iy = 0; iy < pointHeight; iy++) + { + for (int ix = 0; ix < pointWidth; ix++) + { + // Rigid corners never act as flood fronts: they may have been marked done + // for write-back purposes, but propagation must not start from (or pass + // through) immutable terrain. + if (done[ix, iy] && !rigid[ix, iy]) + worklist.Enqueue(new Point2D(originX + ix, originY + iy)); + } + } + } + + /// + /// Writes the smoothed field back to the map: for every cell with at least one + /// changed corner, sets its height level and ramp tile derived from its four corner + /// heights. Returns the list of cells that were changed. + /// + /// Whether steep ramps (corner spread of 2) may be emitted. + /// Called with a cell's coords immediately before it is mutated. + public List WriteBack(bool allowSteep, Action addUndo) + { + var changedCells = new List(); + int maxSpread = allowSteep ? 2 : 1; + + // Cells [originX..originX + pointWidth - 2] have all four corners inside the field. + int lastCellX = originX + pointWidth - 2; + int lastCellY = originY + pointHeight - 2; + + for (int cellY = originY; cellY <= lastCellY; cellY++) + { + for (int cellX = originX; cellX <= lastCellX; cellX++) + { + bool anyDone = false; + bool allOwned = true; + int min = int.MaxValue; + int max = int.MinValue; + int c0 = 0, c1 = 0, c2 = 0, c3 = 0; + + for (int i = 0; i < CornerOffsets.Length; i++) + { + int ix = (cellX + CornerOffsets[i].X) - originX; + int iy = (cellY + CornerOffsets[i].Y) - originY; + + if (done[ix, iy]) + anyDone = true; + if (!hasOwner[ix, iy]) + allOwned = false; + + int h = heights[ix, iy]; + switch (i) + { + case 0: c0 = h; break; + case 1: c1 = h; break; + case 2: c2 = h; break; + default: c3 = h; break; + } + + if (h < min) min = h; + if (h > max) max = h; + } + + if (!anyDone || !allOwned) + continue; + + if (max - min > maxSpread) + continue; + + var cell = map.GetTile(cellX, cellY); + if (cell == null || !map.IsCellMorphable(cell)) + continue; + + var tmpImage = map.TheaterInstance.GetTile(cell.TileIndex).GetSubTile(cell.SubTileIndex).TmpImage; + LandType landType = (LandType)tmpImage.TerrainType; + if (landType == LandType.Rock || landType == LandType.Water) + continue; + + int key = PatternKey(c0 - min, c1 - min, c2 - min, c3 - min); + if (!RampByCornerPattern.TryGetValue(key, out RampType rampType)) + continue; // unreachable given the spread guard, but stay safe + + addUndo(new Point2D(cellX, cellY)); + + int oldLevel = cell.Level; + cell.Level = (byte)min; + + if (rampType == RampType.None) + { + // Reset to the clear tile if the cell is currently a ramp, or if its + // level changed (which also safely breaks up any multi-cell tile that + // would otherwise be split across height levels). Flat ground that is + // merely adjacent to the edit keeps its tile, preserving its texture. + if (tmpImage.RampType != RampType.None || min != oldLevel) + cell.ChangeTileIndex(0, 0); + } + else + { + cell.ChangeTileIndex(rampTileSetStart + ((int)rampType - 1), 0); + } + + changedCells.Add(cell); + } + } + + return changedCells; + } + + private bool InRegion(int px, int py) + => px >= originX && py >= originY && px < originX + pointWidth && py < originY + pointHeight; + + private bool IsCellRigid(int cellX, int cellY) + { + var cell = map.GetTile(cellX, cellY); + return cell == null || !map.IsCellMorphable(cell); + } + + private int GetRampIndex(MapTile cell) + { + var subTile = map.TheaterInstance.GetTile(cell.TileIndex).GetSubTile(cell.SubTileIndex); + int rampIndex = (int)subTile.TmpImage.RampType; + if (rampIndex < 0 || rampIndex >= RampCornerHeights.Length) + rampIndex = 0; + + return rampIndex; + } + + /// + /// Of the corner heights that the immutable cells touching corner point (px, py) have + /// at it, returns the one closest to that lies within + /// [, ], or null if none does. Morphable + /// touching cells are ignored: their current heights are editable, not anchors. + /// The cell touching the corner at corner-index k sits at (px, py) - CornerOffsets[k], + /// and contributes its own corner k's height. The index k must match on both sides. + /// + private int? GetBestExactHeight(int px, int py, int lo, int hi, int preferred) + { + int? best = null; + + for (int k = 0; k < CornerOffsets.Length; k++) + { + var cell = map.GetTile(px - CornerOffsets[k].X, py - CornerOffsets[k].Y); + if (cell == null || map.IsCellMorphable(cell)) + continue; + + int h = cell.Level + RampCornerHeights[GetRampIndex(cell)][k]; + if (h < lo || h > hi) + continue; + + if (best == null || Math.Abs(h - preferred) < Math.Abs(best.Value - preferred)) + best = h; + } + + return best; + } + + /// + /// The lowest and highest corner heights implied for corner point (px, py) by the cells + /// touching it (see ). Only called for corners with an + /// owner, so at least the owning cell always contributes a value. + /// Also reports whether any morphable cell touches the corner; corners interior to + /// immutable terrain are treated differently by the flood. + /// + private void GetAdmissibleRange(int px, int py, out int min, out int max, out bool bordersMorphable) + { + min = int.MaxValue; + max = int.MinValue; + bordersMorphable = false; + + for (int k = 0; k < CornerOffsets.Length; k++) + { + var cell = map.GetTile(px - CornerOffsets[k].X, py - CornerOffsets[k].Y); + if (cell == null) + continue; + + if (map.IsCellMorphable(cell)) + bordersMorphable = true; + + int h = cell.Level + RampCornerHeights[GetRampIndex(cell)][k]; + if (h < min) min = h; + if (h > max) max = h; + } + } + + private static Dictionary BuildReverseLookup() + { + var dict = new Dictionary(); + for (int ramp = 0; ramp <= LastEmittedRamp; ramp++) + { + int key = PatternKey(RampCornerHeights[ramp][0], RampCornerHeights[ramp][1], + RampCornerHeights[ramp][2], RampCornerHeights[ramp][3]); + + if (!dict.ContainsKey(key)) + dict.Add(key, (RampType)ramp); + } + + return dict; + } + + private static int PatternKey(int c0, int c1, int c2, int c3) + => c0 + c1 * 3 + c2 * 9 + c3 * 27; + } +} diff --git a/src/TSMapEditor/Mutations/Classes/HeightMutations/FSLowerGroundMutation.cs b/src/TSMapEditor/Mutations/Classes/HeightMutations/FSLowerGroundMutation.cs index 3d828f81d..206db1aea 100644 --- a/src/TSMapEditor/Mutations/Classes/HeightMutations/FSLowerGroundMutation.cs +++ b/src/TSMapEditor/Mutations/Classes/HeightMutations/FSLowerGroundMutation.cs @@ -1,99 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using TSMapEditor.CCEngine; using TSMapEditor.GameMath; -using TSMapEditor.Models; using TSMapEditor.UI; -using HCT = TSMapEditor.Mutations.Classes.HeightMutations.HeightComparisonType; namespace TSMapEditor.Mutations.Classes.HeightMutations { + /// + /// A mutation for "smartly" lowering ground, with automatic application of ramps, + /// using non-steep ramps only. + /// public class FSLowerGroundMutation : LowerGroundMutationBase { public FSLowerGroundMutation(IMutationTarget mutationTarget, Point2D originCell, BrushSize brushSize) : base(mutationTarget, originCell, brushSize) { } - private static readonly TransitionRampInfo[] transitionRampInfos = new[] - { - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal }), - - new TransitionRampInfo(RampType.West, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual } ), - new TransitionRampInfo(RampType.North, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.East, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.South, new() { HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - - new TransitionRampInfo(RampType.CornerNW, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.CornerNE, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.CornerSE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.Equal, HCT.Higher }), - new TransitionRampInfo(RampType.CornerSW, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - - new TransitionRampInfo(RampType.MidNW, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Equal, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidNE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - - new TransitionRampInfo(RampType.DoubleUpSWNE, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant }), - new TransitionRampInfo(RampType.DoubleDownSWNE, new() { HCT.Equal, HCT.Irrelevant, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant, HCT.Equal, HCT.Higher }), - - // Fixes for ramps in "odd angles" between cells - - new TransitionRampInfo(RampType.MidNE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.Equal, HCT.Equal, HCT.Higher, HCT.Irrelevant, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Higher }), - - new TransitionRampInfo(RampType.MidNW, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal }), - - new TransitionRampInfo(RampType.MidSE, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Irrelevant, HCT.Higher, HCT.Equal }), - new TransitionRampInfo(RampType.MidNW, new() { HCT.Equal, HCT.Irrelevant, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual }), - - new TransitionRampInfo(RampType.MidNE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.LowerOrEqual, HCT.Equal }), - - // "Less likely" cases of mid-ramps, where the cell direclty behind the ramp is not higher but the cells on the "backsides" are - new TransitionRampInfo(RampType.MidNW, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidNW, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidNE, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidNE, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - - // In case it's anything else, we probably need to flatten it - new TransitionRampInfo(RampType.None, new() { HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant }, 0), - }; - - // Pre-ramp-placement height fix checks - private static readonly TransitionRampInfo[] heightFixers = new[] - { - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.HigherOrEqual }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.Higher, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Irrelevant, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant, HCT.Higher }, 1), - - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Irrelevant, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant }, 1), - }; - - protected override TransitionRampInfo[] GetTransitionRampInfos() => transitionRampInfos; - - protected override TransitionRampInfo[] GetHeightFixers() => heightFixers; + protected override bool AllowSteep => false; public override string GetDisplayString() { @@ -102,103 +22,5 @@ public override string GetDisplayString() } public override void Perform() => LowerGround(); - - - protected override void CheckCell(Point2D cellCoords) - { - if (processedCellsThisIteration.Contains(cellCoords) || cellsToProcess.Contains(cellCoords)) - return; - - MarkCellAsProcessed(cellCoords); - - var thisCell = Map.GetTile(cellCoords); - if (thisCell == null) - return; - - if (!IsCellMorphable(thisCell)) - return; - - int biggestHeightDiff = 0; - - for (int direction = 0; direction < (int)Direction.Count; direction++) - { - var cell = Map.GetTile(cellCoords + Helpers.VisualDirectionToPoint((Direction)direction)); - if (cell == null) - continue; - - if (!IsCellMorphable(cell)) - continue; - - if (cell.Level < thisCell.Level) - biggestHeightDiff = Math.Max(biggestHeightDiff, thisCell.Level - cell.Level); - } - - // If nearby cells are lower by more than 1 cell, it's necessary to also lower this cell - if (biggestHeightDiff > 1) - { - AddCellToUndoData(thisCell.CoordsToPoint()); - - if (thisCell.Level > 0) - { - if (thisCell.Level >= biggestHeightDiff - 1) - thisCell.Level = (byte)(thisCell.Level - (biggestHeightDiff - 1)); - else - thisCell.Level--; - } - - foreach (Point2D offset in SurroundingTiles) - { - RegisterCell(cellCoords + offset); - } - } - } - - /// - /// Applies miscellaneous fixes to height data of processed cells. - /// - protected override void CellHeightFixes() - { - // We process one set of cells at a time, starting from the cells - // processed so far. - // During the processing, we might get new cells to process. - // We repeat the process until no new cells to process have been added to the list. - List cellsToCheck = new(totalProcessedCells); - List newCells = new(); - - while (cellsToCheck.Count > 0) - { - // Special height fixes - totalProcessedCells.ForEach(cc => - { - var cell = Map.GetTile(cc); - if (cell == null) - return; - - if (!IsCellMorphable(cell)) - return; - - var applyingTransition = Array.Find(heightFixers, tri => tri.Matches(Map, cc, cell.Level)); - if (applyingTransition != null) - { - AddCellToUndoData(cell.CoordsToPoint()); - cell.ChangeTileIndex(0, 0); - cell.Level = (byte)(cell.Level + applyingTransition.HeightChange); - - // If we fixed this flaw from one cell, also check the surrounding ones - foreach (var surroundingCell in SurroundingTiles) - { - if (!newCells.Contains(cc + surroundingCell)) - newCells.Add(cc + surroundingCell); - } - } - }); - - var cellsForNextRound = newCells.Where(c => !cellsToCheck.Contains(c)); - cellsToCheck.Clear(); - cellsToCheck.AddRange(cellsForNextRound); - totalProcessedCells.AddRange(cellsForNextRound.Where(c => !totalProcessedCells.Contains(c))); - newCells.Clear(); - } - } } } diff --git a/src/TSMapEditor/Mutations/Classes/HeightMutations/FSRaiseGroundMutation.cs b/src/TSMapEditor/Mutations/Classes/HeightMutations/FSRaiseGroundMutation.cs index 818b45a96..04312754d 100644 --- a/src/TSMapEditor/Mutations/Classes/HeightMutations/FSRaiseGroundMutation.cs +++ b/src/TSMapEditor/Mutations/Classes/HeightMutations/FSRaiseGroundMutation.cs @@ -1,92 +1,19 @@ -using System; -using TSMapEditor.CCEngine; using TSMapEditor.GameMath; -using TSMapEditor.Models; using TSMapEditor.UI; -using HCT = TSMapEditor.Mutations.Classes.HeightMutations.HeightComparisonType; namespace TSMapEditor.Mutations.Classes.HeightMutations { + /// + /// A mutation for "smartly" raising ground, with automatic application of ramps, + /// using non-steep ramps only. + /// public class FSRaiseGroundMutation : RaiseGroundMutationBase { public FSRaiseGroundMutation(IMutationTarget mutationTarget, Point2D originCell, BrushSize brushSize) : base(mutationTarget, originCell, brushSize) { } - private static readonly TransitionRampInfo[] transitionRampInfos = new[] - { - new TransitionRampInfo(RampType.West, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual } ), - new TransitionRampInfo(RampType.North, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.East, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.South, new() { HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - - new TransitionRampInfo(RampType.CornerNW, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.CornerNE, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.CornerSE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.Equal, HCT.Higher }), - new TransitionRampInfo(RampType.CornerSW, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - - new TransitionRampInfo(RampType.MidNW, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Equal, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidNE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - - new TransitionRampInfo(RampType.DoubleUpSWNE, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant }), - new TransitionRampInfo(RampType.DoubleDownSWNE, new() { HCT.Equal, HCT.Irrelevant, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant, HCT.Equal, HCT.Higher }), - - // Fixes for ramps in "odd angles" between cells - - new TransitionRampInfo(RampType.MidNE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.Equal, HCT.Equal, HCT.Higher, HCT.Irrelevant, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Higher }), - - new TransitionRampInfo(RampType.MidNW, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal }), - - new TransitionRampInfo(RampType.MidSE, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Irrelevant, HCT.Higher, HCT.Equal }), - new TransitionRampInfo(RampType.MidNW, new() { HCT.Equal, HCT.Irrelevant, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual }), - - new TransitionRampInfo(RampType.MidNE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.LowerOrEqual, HCT.Equal }), - - // "Less likely" cases of mid-ramps, where the cell direclty behind the ramp is not higher but the cells on the "backsides" are - new TransitionRampInfo(RampType.MidNW, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidNW, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidNE, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidNE, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - }; - - // Pre-ramp-placement height fix checks - private static readonly TransitionRampInfo[] heightFixers = new[] - { - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.HigherOrEqual }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.Higher, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Irrelevant, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant, HCT.Higher }, 1), - - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Irrelevant, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant }, 1), - }; - - protected override TransitionRampInfo[] GetTransitionRampInfos() => transitionRampInfos; - - protected override TransitionRampInfo[] GetHeightFixers() => heightFixers; - + protected override bool AllowSteep => false; public override string GetDisplayString() { @@ -94,50 +21,6 @@ public override string GetDisplayString() OriginCell, BrushSize); } - public override void Perform() => RaiseGround(); - - - protected override void CheckCell(Point2D cellCoords) - { - if (processedCellsThisIteration.Contains(cellCoords) || cellsToProcess.Contains(cellCoords)) - return; - - MarkCellAsProcessed(cellCoords); - - var thisCell = Map.GetTile(cellCoords); - if (thisCell == null) - return; - - if (!IsCellMorphable(thisCell)) - return; - - int biggestHeightDiff = 0; - - for (int direction = 0; direction < (int)Direction.Count; direction++) - { - var cell = Map.GetTile(cellCoords + Helpers.VisualDirectionToPoint((Direction)direction)); - if (cell == null) - continue; - - if (!IsCellMorphable(cell)) - continue; - - if (cell.Level > thisCell.Level) - biggestHeightDiff = Math.Max(biggestHeightDiff, cell.Level - thisCell.Level); - } - - // If nearby cells are raised by more than 1 cell, it's necessary to also raise this cell - if (biggestHeightDiff > 1) - { - AddCellToUndoData(thisCell.CoordsToPoint()); - thisCell.Level += (byte)(biggestHeightDiff - 1); - - foreach (Point2D offset in SurroundingTiles) - { - RegisterCell(cellCoords + offset); - } - } - } } } diff --git a/src/TSMapEditor/Mutations/Classes/HeightMutations/FlattenGroundMutation.cs b/src/TSMapEditor/Mutations/Classes/HeightMutations/FlattenGroundMutation.cs index 6f3094743..c24da8cf3 100644 --- a/src/TSMapEditor/Mutations/Classes/HeightMutations/FlattenGroundMutation.cs +++ b/src/TSMapEditor/Mutations/Classes/HeightMutations/FlattenGroundMutation.cs @@ -1,11 +1,9 @@ -using System.Collections.Generic; -using System; +using System.Collections.Generic; using TSMapEditor.CCEngine; using TSMapEditor.GameMath; +using TSMapEditor.Models; using TSMapEditor.Models.Enums; using TSMapEditor.UI; -using System.Linq; -using HCT = TSMapEditor.Mutations.Classes.HeightMutations.HeightComparisonType; namespace TSMapEditor.Mutations.Classes.HeightMutations { @@ -45,367 +43,62 @@ private void FlattenGround() int beginX = OriginCell.X - (xSize - 1) / 2; int endX = OriginCell.X + xSize / 2; + // Cells at a different level must be flattened (and reject the whole edit if they + // cannot be). Ramp cells already at the desired level (e.g. a ridge where two slopes + // meet, with no flat top) should also be flattened, but only where they legally can + // be — otherwise they are skipped rather than blocking the edit. + var requiredCells = new List(); + var optionalCells = new List(); for (int y = beginY; y <= endY; y++) { for (int x = beginX; x <= endX; x++) { var cellCoords = new Point2D(x, y); - var targetCell = Map.GetTile(cellCoords); - if (targetCell == null || targetCell.Level == desiredHeightLevel) + var cell = Map.GetTile(cellCoords); + if (cell == null || !IsCellMorphable(cell)) continue; - if (!IsCellMorphable(targetCell)) + var tmpImage = Map.TheaterInstance.GetTile(cell.TileIndex).GetSubTile(cell.SubTileIndex).TmpImage; + LandType landType = (LandType)tmpImage.TerrainType; + if (landType == LandType.Rock || landType == LandType.Water) continue; - AddCellToUndoData(cellCoords); - - targetCell.Level = (byte)desiredHeightLevel; - targetCell.ChangeTileIndex(0, 0); - foreach (Point2D surroundingCellOffset in SurroundingTiles) - { - RegisterCell(cellCoords + surroundingCellOffset); - } - - MarkCellAsProcessed(cellCoords); + if (cell.Level != desiredHeightLevel) + requiredCells.Add(cellCoords); + else if (tmpImage.RampType != RampType.None) + optionalCells.Add(cellCoords); } } - Process(); - - if (MutationTarget.AutoLATEnabled) - ApplyAutoLAT(); - } - - private void ApplyAutoLAT() - { - int minX = totalProcessedCells.Aggregate(int.MaxValue, (min, point) => point.X < min ? point.X : min) - 1; - int minY = totalProcessedCells.Aggregate(int.MaxValue, (min, point) => point.Y < min ? point.Y : min) - 1; - int maxX = totalProcessedCells.Aggregate(int.MinValue, (max, point) => point.X > max ? point.X : max) + 1; - int maxY = totalProcessedCells.Aggregate(int.MinValue, (max, point) => point.Y > max ? point.Y : max) + 1; - - ApplyGenericAutoLAT(minX, minY, maxX, maxY); - } - - private static readonly TransitionRampInfo[] transitionRampInfos = new[] - { - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal }), - - new TransitionRampInfo(RampType.West, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual } ), - new TransitionRampInfo(RampType.North, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.East, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.South, new() { HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - - new TransitionRampInfo(RampType.CornerNW, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.CornerNE, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.CornerSE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.Equal, HCT.Higher }), - new TransitionRampInfo(RampType.CornerSW, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - - new TransitionRampInfo(RampType.MidNW, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Equal, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidNE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - - new TransitionRampInfo(RampType.DoubleUpSWNE, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant }), - new TransitionRampInfo(RampType.DoubleDownSWNE, new() { HCT.Equal, HCT.Irrelevant, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant, HCT.Equal, HCT.Higher }), - - // Fixes for ramps in "odd angles" between cells - - new TransitionRampInfo(RampType.MidNE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.Equal, HCT.Equal, HCT.Higher, HCT.Irrelevant, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Higher }), - - new TransitionRampInfo(RampType.MidNW, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal }), - - new TransitionRampInfo(RampType.MidSE, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Irrelevant, HCT.Higher, HCT.Equal }), - new TransitionRampInfo(RampType.MidNW, new() { HCT.Equal, HCT.Irrelevant, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual }), - - new TransitionRampInfo(RampType.MidNE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.LowerOrEqual, HCT.Equal }), - - // "Less likely" cases of mid-ramps, where the cell direclty behind the ramp is not higher but the cells on the "backsides" are - new TransitionRampInfo(RampType.MidNW, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidNW, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidNE, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidNE, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - - // Special on-ramp-placement height fix checks - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.HigherOrEqual }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.Higher, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal }, 1), - - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Irrelevant, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant }, 1), - - // In case it's anything else, we probably need to flatten it - new TransitionRampInfo(RampType.None, new() { HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant }, 0), - }; - - // Pre-ramp-placement height fix checks - private static readonly TransitionRampInfo[] heightFixers = Array.Empty(); - - protected override TransitionRampInfo[] GetTransitionRampInfos() => transitionRampInfos; - - protected override TransitionRampInfo[] GetHeightFixers() => heightFixers; - - protected override void CheckCell(Point2D cellCoords) - { - if (processedCellsThisIteration.Contains(cellCoords) || cellsToProcess.Contains(cellCoords)) - return; - - MarkCellAsProcessed(cellCoords); - - var thisCell = Map.GetTile(cellCoords); - if (thisCell == null) + if (requiredCells.Count == 0 && optionalCells.Count == 0) return; - if (!IsCellMorphable(thisCell)) - return; - - if (thisCell.Level < desiredHeightLevel) - CheckCell_Higher(cellCoords); - else - CheckCell_Lower(cellCoords); - } - - private void CheckCell_Higher(Point2D cellCoords) - { - var thisCell = Map.GetTileOrFail(cellCoords); - - int biggestHeightDiff = 0; - - for (int direction = 0; direction < (int)Direction.Count; direction++) - { - var cell = Map.GetTile(cellCoords + Helpers.VisualDirectionToPoint((Direction)direction)); - if (cell == null) - continue; - - if (!IsCellMorphable(cell)) - continue; + // Flattening only ever produces non-steep ramps. + var changedCells = SmoothFlat(requiredCells, optionalCells, desiredHeightLevel, HeightFloodMode.Both, allowSteep: false); - if (cell.Level > thisCell.Level) - biggestHeightDiff = Math.Max(biggestHeightDiff, cell.Level - thisCell.Level); - } - - // If nearby cells are raised by more than 1 cell, it's necessary to also raise this cell - if (biggestHeightDiff > 1) - { - AddCellToUndoData(thisCell.CoordsToPoint()); - thisCell.Level += (byte)(biggestHeightDiff - 1); - - foreach (Point2D offset in SurroundingTiles) - { - RegisterCell(cellCoords + offset); - } - } + if (changedCells != null && MutationTarget.AutoLATEnabled) + ApplyAutoLAT(changedCells); } - private void CheckCell_Lower(Point2D cellCoords) + private void ApplyAutoLAT(List changedCells) { - var thisCell = Map.GetTileOrFail(cellCoords); - - int biggestHeightDiff = 0; - - for (int direction = 0; direction < (int)Direction.Count; direction++) - { - var cell = Map.GetTile(cellCoords + Helpers.VisualDirectionToPoint((Direction)direction)); - if (cell == null) - continue; - - if (!IsCellMorphable(cell)) - continue; - - if (cell.Level < thisCell.Level) - biggestHeightDiff = Math.Max(biggestHeightDiff, thisCell.Level - cell.Level); - } - - // If nearby cells are lower by more than 1 cell, it's necessary to also lower this cell - if (biggestHeightDiff > 1) - { - AddCellToUndoData(thisCell.CoordsToPoint()); - - if (thisCell.Level > 0) - { - if (thisCell.Level >= biggestHeightDiff - 1) - thisCell.Level = (byte)(thisCell.Level - (biggestHeightDiff - 1)); - else - thisCell.Level--; - } - - foreach (Point2D offset in SurroundingTiles) - { - RegisterCell(cellCoords + offset); - } - } - } - - private void CellHeightFix_CheckStraightDiagonalLine(Point2D cellCoords, List newCells, bool isXAxis) - { - var cell = Map.GetTile(cellCoords); - if (cell == null) - return; - - if (!IsCellMorphable(cell)) - return; - - Point2D otherCellCoords = cellCoords + new Point2D(isXAxis ? 1 : 0, isXAxis ? 0 : 1); - var otherCell = Map.GetTile(otherCellCoords); - if (otherCell == null || otherCell.Level <= cell.Level) + if (changedCells.Count == 0) return; - int otherLevel = otherCell.Level; - - otherCellCoords = cellCoords + new Point2D(isXAxis ? -1 : 0, isXAxis ? 0 : -1); - otherCell = Map.GetTile(otherCellCoords); - if (otherCell == null) - return; - - if (otherCell.Level == otherLevel) - { - AddCellToUndoData(cell.CoordsToPoint()); - cell.ChangeTileIndex(0, 0); - cell.Level = otherCell.Level; - - // If we fixed this flaw from one cell, also check the surrounding ones - foreach (var surroundingCell in SurroundingTiles) - { - if (!newCells.Contains(cellCoords + surroundingCell)) - newCells.Add(cellCoords + surroundingCell); - } - } - } - - /// - /// Applies miscellaneous fixes to height data of processed cells. - /// - protected override void CellHeightFixes() - { - // We process one set of cells at a time, starting from the cells - // processed so far. - // During the processing, we might get new cells to process. - // We repeat the process until no new cells to process have been added to the list. - List cellsToCheck = new(totalProcessedCells); - List newCells = new(); + int minX = int.MaxValue; + int minY = int.MaxValue; + int maxX = int.MinValue; + int maxY = int.MinValue; - while (cellsToCheck.Count > 0) + foreach (var cell in changedCells) { - // If a cell has higher cells on both of its sides in a straight diagonal line, - // we need to normalize its height to be on the same level with the diagonal sides, - // because no "one cell" depression exists - cellsToCheck.ForEach(cc => CellHeightFix_CheckStraightDiagonalLine(cc, newCells, true)); - cellsToCheck.ForEach(cc => CellHeightFix_CheckStraightDiagonalLine(cc, newCells, false)); - - // If a cell has a two-levels-higher cell next to it on a horizontal or vertical line, - // and a one-level-higher cell next to it on the opposite side, - // we need to increase its level by 1 - totalProcessedCells.ForEach(cc => CellHeightFix_DifferentHeightDiffsOnStraightLine(cc, new Point2D(1, -1), new Point2D(-1, 1))); - totalProcessedCells.ForEach(cc => CellHeightFix_DifferentHeightDiffsOnStraightLine(cc, new Point2D(-1, -1), new Point2D(1, 1))); - - // Special height fixes - totalProcessedCells.ForEach(cc => - { - var cell = Map.GetTile(cc); - if (cell == null) - return; - - if (!IsCellMorphable(cell)) - return; - - var applyingTransition = Array.Find(heightFixers, tri => tri.Matches(Map, cc, cell.Level)); - if (applyingTransition != null) - { - AddCellToUndoData(cell.CoordsToPoint()); - cell.ChangeTileIndex(0, 0); - cell.Level += (byte)applyingTransition.HeightChange; - - // If we fixed this flaw from one cell, also check the surrounding ones - foreach (var surroundingCell in SurroundingTiles) - { - if (!newCells.Contains(cc + surroundingCell)) - newCells.Add(cc + surroundingCell); - } - } - }); - - var cellsForNextRound = newCells; //.Where(c => !cellsToCheck.Contains(c)); - cellsToCheck.Clear(); - cellsToCheck.AddRange(cellsForNextRound); - totalProcessedCells.AddRange(cellsForNextRound.Where(c => !totalProcessedCells.Contains(c))); - newCells.Clear(); + if (cell.X < minX) minX = cell.X; + if (cell.Y < minY) minY = cell.Y; + if (cell.X > maxX) maxX = cell.X; + if (cell.Y > maxY) maxY = cell.Y; } - } - - protected override void ApplyRamps() - { - var cellsToProcess = new List(totalProcessedCells); - - // Go through all processed cells and add their neighbours to be processed too - totalProcessedCells.ForEach(cc => - { - for (int i = 0; i < (int)Direction.Count; i++) - { - var otherCellCoords = cc + Helpers.VisualDirectionToPoint((Direction)i); - - if (!cellsToProcess.Contains(otherCellCoords)) - cellsToProcess.Add(otherCellCoords); - } - }); - - foreach (var cellCoords in cellsToProcess) - { - var cell = Map.GetTile(cellCoords); - if (cell == null) - continue; - - if (!IsCellMorphable(cell)) - continue; - - var cellTmpImage = Map.TheaterInstance.GetTile(cell.TileIndex).GetSubTile(cell.SubTileIndex).TmpImage; - LandType landType = (LandType)cellTmpImage.TerrainType; - if (landType == LandType.Rock || landType == LandType.Water) - continue; - foreach (var transitionRampInfo in transitionRampInfos) - { - if (transitionRampInfo.Matches(Map, cellCoords, cell.Level)) - { - AddCellToUndoData(cell.CoordsToPoint()); - - if (transitionRampInfo.RampType == RampType.None) - { - if (cellTmpImage.RampType != RampType.None) - cell.ChangeTileIndex(0, 0); - } - else - { - cell.ChangeTileIndex(RampTileSet.StartTileIndex + ((int)transitionRampInfo.RampType - 1), 0); - } - - if (transitionRampInfo.HeightChange != 0) - { - cell.Level += (byte)transitionRampInfo.HeightChange; - } - - break; - } - } - } + ApplyGenericAutoLAT(minX - 1, minY - 1, maxX + 1, maxY + 1); } } } diff --git a/src/TSMapEditor/Mutations/Classes/HeightMutations/HeightComparisonType.cs b/src/TSMapEditor/Mutations/Classes/HeightMutations/HeightComparisonType.cs deleted file mode 100644 index beb75b921..000000000 --- a/src/TSMapEditor/Mutations/Classes/HeightMutations/HeightComparisonType.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace TSMapEditor.Mutations.Classes.HeightMutations -{ - /// - /// Defines what kind of comparison to use when comparing the height of a cell - /// to the height of another cell. - /// - public enum HeightComparisonType - { - /// - /// The height of the other cell is irrelevant for the resulting ramp type. - /// - Irrelevant, - - /// - /// The other cell must be higher by 1 level. - /// - Higher, - - /// - /// The other cell must be higher by 2 or more levels. - /// - MuchHigher, - - /// - /// The other cell must be higher (by 1 level) or equal. - /// - HigherOrEqual, - - /// - /// The other cell must be lower by 1 level. - /// - Lower, - - /// - /// The other cell must be lower by 2 or more levels. - /// - MuchLower, - - /// - /// The other cell must be lower or equal. - /// - LowerOrEqual, - - /// - /// The other cell must be equal. - /// - Equal - } -} diff --git a/src/TSMapEditor/Mutations/Classes/HeightMutations/LowerGroundMutation.cs b/src/TSMapEditor/Mutations/Classes/HeightMutations/LowerGroundMutation.cs index c0e009183..e3e049245 100644 --- a/src/TSMapEditor/Mutations/Classes/HeightMutations/LowerGroundMutation.cs +++ b/src/TSMapEditor/Mutations/Classes/HeightMutations/LowerGroundMutation.cs @@ -1,99 +1,19 @@ -using System; -using TSMapEditor.CCEngine; using TSMapEditor.GameMath; -using TSMapEditor.Models; using TSMapEditor.UI; namespace TSMapEditor.Mutations.Classes.HeightMutations { - using HCT = HeightComparisonType; - + /// + /// A mutation for "smartly" lowering ground, with automatic application of ramps, + /// allowing steep ramps. + /// internal class LowerGroundMutation : LowerGroundMutationBase { public LowerGroundMutation(IMutationTarget mutationTarget, Point2D originCell, BrushSize brushSize) : base(mutationTarget, originCell, brushSize) { } - private static readonly TransitionRampInfo[] transitionRampInfos = new[] - { - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal }), - - new TransitionRampInfo(RampType.West, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual } ), - new TransitionRampInfo(RampType.North, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.East, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.South, new() { HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - - new TransitionRampInfo(RampType.CornerNW, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.CornerNE, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.CornerSE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.Equal, HCT.Higher }), - new TransitionRampInfo(RampType.CornerSW, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - - new TransitionRampInfo(RampType.MidNW, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Equal, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidNE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - - new TransitionRampInfo(RampType.SteepSE, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.MuchHigher, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.SteepSW, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.MuchHigher, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.SteepNW, new() { HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.MuchHigher }), - new TransitionRampInfo(RampType.SteepNE, new() { HCT.Higher, HCT.MuchHigher, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - - new TransitionRampInfo(RampType.DoubleUpSWNE, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant }), - new TransitionRampInfo(RampType.DoubleDownSWNE, new() { HCT.Equal, HCT.Irrelevant, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant, HCT.Equal, HCT.Higher }), - - // Fixes for ramps in "odd angles" between cells - - new TransitionRampInfo(RampType.MidNE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.Equal, HCT.Equal, HCT.Higher, HCT.Irrelevant, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Higher }), - - new TransitionRampInfo(RampType.MidNW, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal }), - - new TransitionRampInfo(RampType.MidSE, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Irrelevant, HCT.Higher, HCT.Equal }), - new TransitionRampInfo(RampType.MidNW, new() { HCT.Equal, HCT.Irrelevant, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual }), - - new TransitionRampInfo(RampType.MidNE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.LowerOrEqual, HCT.Equal }), - - // "Less likely" cases of mid-ramps, where the cell direclty behind the ramp is not higher but the cells on the "backsides" are - new TransitionRampInfo(RampType.MidNW, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidNW, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidNE, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidNE, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - - // Special on-ramp-placement height fix checks - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal }, 1), - - // In case it's anything else, we probably need to flatten it - new TransitionRampInfo(RampType.None, new() { HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant }, 0), - }; - - // Pre-ramp-placement height fix checks - private static readonly TransitionRampInfo[] heightFixers = new[] - { - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal }, 1), - }; - - protected override TransitionRampInfo[] GetTransitionRampInfos() => transitionRampInfos; - - protected override TransitionRampInfo[] GetHeightFixers() => heightFixers; - + protected override bool AllowSteep => true; public override string GetDisplayString() { @@ -101,68 +21,6 @@ public override string GetDisplayString() OriginCell, BrushSize); } - public override void Perform() => LowerGround(); - - - protected override void CheckCell(Point2D cellCoords) - { - if (processedCellsThisIteration.Contains(cellCoords) || cellsToProcess.Contains(cellCoords)) - return; - - MarkCellAsProcessed(cellCoords); - - var thisCell = Map.GetTile(cellCoords); - if (thisCell == null) - return; - - if (!IsCellMorphable(thisCell)) - return; - - int biggestHeightDiff = 0; - - var northernCell = Map.GetTile(cellCoords + new Point2D(0, -1)); - if (northernCell != null && northernCell.Level < thisCell.Level && IsCellMorphable(northernCell)) - { - biggestHeightDiff = Math.Max(biggestHeightDiff, thisCell.Level - northernCell.Level); - } - - var southernCell = Map.GetTile(cellCoords + new Point2D(0, 1)); - if (southernCell != null && southernCell.Level < thisCell.Level && IsCellMorphable(southernCell)) - { - biggestHeightDiff = Math.Max(biggestHeightDiff, thisCell.Level - southernCell.Level); - } - - var westernCell = Map.GetTile(cellCoords + new Point2D(-1, 0)); - if (westernCell != null && westernCell.Level < thisCell.Level && IsCellMorphable(westernCell)) - { - biggestHeightDiff = Math.Max(biggestHeightDiff, thisCell.Level - westernCell.Level); - } - - var easternCell = Map.GetTile(cellCoords + new Point2D(1, 0)); - if (easternCell != null && easternCell.Level < thisCell.Level && IsCellMorphable(easternCell)) - { - biggestHeightDiff = Math.Max(biggestHeightDiff, thisCell.Level - easternCell.Level); - } - - // If nearby cells are lower by more than 1 cell, it's necessary to also lower this cell - if (biggestHeightDiff > 1) - { - AddCellToUndoData(thisCell.CoordsToPoint()); - - if (thisCell.Level > 0) - { - if (thisCell.Level >= biggestHeightDiff - 1) - thisCell.Level = (byte)(thisCell.Level - (biggestHeightDiff - 1)); - else - thisCell.Level--; - } - - foreach (Point2D offset in SurroundingTiles) - { - RegisterCell(cellCoords + offset); - } - } - } } } diff --git a/src/TSMapEditor/Mutations/Classes/HeightMutations/LowerGroundMutationBase.cs b/src/TSMapEditor/Mutations/Classes/HeightMutations/LowerGroundMutationBase.cs index 733895828..7a621d261 100644 --- a/src/TSMapEditor/Mutations/Classes/HeightMutations/LowerGroundMutationBase.cs +++ b/src/TSMapEditor/Mutations/Classes/HeightMutations/LowerGroundMutationBase.cs @@ -1,9 +1,6 @@ -using System; using System.Collections.Generic; -using System.Linq; -using TSMapEditor.CCEngine; using TSMapEditor.GameMath; -using TSMapEditor.Models.Enums; +using TSMapEditor.Models; using TSMapEditor.UI; namespace TSMapEditor.Mutations.Classes.HeightMutations @@ -14,8 +11,16 @@ public LowerGroundMutationBase(IMutationTarget mutationTarget, Point2D originCel { } + /// + /// Whether this lower operation is allowed to create steep ramps + /// (a corner spread of two levels across a single cell). + /// + protected abstract bool AllowSteep { get; } + protected void LowerGround() { + Clear(); + var targetCell = Map.GetTile(OriginCell); if (targetCell == null || targetCell.Level < 1 || !IsCellMorphable(targetCell)) @@ -41,153 +46,56 @@ protected void LowerGround() int beginX = OriginCell.X - (xSize - 1) / 2; int endX = OriginCell.X + xSize / 2; - // For all other brush sizes we can have a generic implementation + // Gather the cells we want to lower. We only lower ground that was on the same + // level as our original target cell, otherwise things get illogical. + var targetedCells = new List(); for (int y = beginY; y <= endY; y++) { for (int x = beginX; x <= endX; x++) { var cellCoords = new Point2D(x, y); - targetCell = Map.GetTile(cellCoords); - if (targetCell == null || targetCell.Level < 1) + var cell = Map.GetTile(cellCoords); + if (cell == null || cell.Level < 1) continue; - // Only lower ground that was on the same level with our original target cell, - // otherwise things get illogical - if (targetCell.Level != targetCellHeight) + if (cell.Level != targetCellHeight) continue; - // Lower this cell and check surrounding cells whether they need ramps - AddCellToUndoData(cellCoords); - targetCell.Level--; - targetCell.ChangeTileIndex(0, 0); - foreach (Point2D surroundingCellOffset in SurroundingTiles) - { - RegisterCell(cellCoords + surroundingCellOffset); - } + if (!IsCellMorphable(cell)) + continue; - MarkCellAsProcessed(cellCoords); + targetedCells.Add(cellCoords); } } - Process(); - - if (MutationTarget.AutoLATEnabled) - ApplyAutoLAT(); - } + if (targetedCells.Count == 0) + return; - private void ApplyAutoLAT() - { - int minX = totalProcessedCells.Aggregate(int.MaxValue, (min, point) => point.X < min ? point.X : min) - 1; - int minY = totalProcessedCells.Aggregate(int.MaxValue, (min, point) => point.Y < min ? point.Y : min) - 1; - int maxX = totalProcessedCells.Aggregate(int.MinValue, (max, point) => point.X > max ? point.X : max) + 1; - int maxY = totalProcessedCells.Aggregate(int.MinValue, (max, point) => point.Y > max ? point.Y : max) + 1; + var changedCells = SmoothFlat(targetedCells, targetCellHeight - 1, HeightFloodMode.Down, AllowSteep); - ApplyGenericAutoLAT(minX, minY, maxX, maxY); + if (changedCells != null && MutationTarget.AutoLATEnabled) + ApplyAutoLAT(changedCells); } - /// - /// Applies miscellaneous fixes to height data of processed cells. - /// - protected override void CellHeightFixes() + private void ApplyAutoLAT(List changedCells) { - // We process one set of cells at a time, starting from the cells - // processed so far. - // During the processing, we might get new cells to process. - // We repeat the process until no new cells to process have been added to the list. - List cellsToCheck = new(totalProcessedCells); - List newCells = new(); - - while (cellsToCheck.Count > 0) - { - // Special height fixes - totalProcessedCells.ForEach(cc => - { - var cell = Map.GetTile(cc); - if (cell == null) - return; - - if (!IsCellMorphable(cell)) - return; - - var applyingTransition = Array.Find(GetHeightFixers(), tri => tri.Matches(Map, cc, cell.Level)); - if (applyingTransition != null) - { - AddCellToUndoData(cell.CoordsToPoint()); - cell.ChangeTileIndex(0, 0); - cell.Level = (byte)(cell.Level + applyingTransition.HeightChange); - - // If we fixed this flaw from one cell, also check the surrounding ones - foreach (var surroundingCell in SurroundingTiles) - { - if (!newCells.Contains(cc + surroundingCell)) - newCells.Add(cc + surroundingCell); - } - } - }); - - var cellsForNextRound = newCells.Where(c => !cellsToCheck.Contains(c)); - cellsToCheck.Clear(); - cellsToCheck.AddRange(cellsForNextRound); - totalProcessedCells.AddRange(cellsForNextRound.Where(c => !totalProcessedCells.Contains(c))); - newCells.Clear(); - } - } + if (changedCells.Count == 0) + return; - protected override void ApplyRamps() - { - var cellsToProcess = new List(totalProcessedCells); + int minX = int.MaxValue; + int minY = int.MaxValue; + int maxX = int.MinValue; + int maxY = int.MinValue; - // Go through all processed cells and add their neighbours to be processed too - totalProcessedCells.ForEach(cc => + foreach (var cell in changedCells) { - for (int i = 0; i < (int)Direction.Count; i++) - { - var otherCellCoords = cc + Helpers.VisualDirectionToPoint((Direction)i); - - if (!cellsToProcess.Contains(otherCellCoords)) - cellsToProcess.Add(otherCellCoords); - } - }); - - foreach (var cellCoords in cellsToProcess) - { - var cell = Map.GetTile(cellCoords); - if (cell == null) - continue; - - if (!IsCellMorphable(cell)) - continue; - - var cellTmpImage = Map.TheaterInstance.GetTile(cell.TileIndex).GetSubTile(cell.SubTileIndex).TmpImage; - LandType landType = (LandType)cellTmpImage.TerrainType; - if (landType == LandType.Rock || landType == LandType.Water) - continue; - - foreach (var transitionRampInfo in GetTransitionRampInfos()) - { - if (transitionRampInfo.Matches(Map, cellCoords, cell.Level)) - { - AddCellToUndoData(cell.CoordsToPoint()); - - if (transitionRampInfo.RampType == RampType.None) - { - if (cellTmpImage.RampType != RampType.None) - cell.ChangeTileIndex(0, 0); - } - else - { - cell.ChangeTileIndex(RampTileSet.StartTileIndex + ((int)transitionRampInfo.RampType - 1), 0); - } - - if (transitionRampInfo.HeightChange != 0) - { - cell.Level = (byte)(cell.Level + transitionRampInfo.HeightChange); - } - - break; - } - } + if (cell.X < minX) minX = cell.X; + if (cell.Y < minY) minY = cell.Y; + if (cell.X > maxX) maxX = cell.X; + if (cell.Y > maxY) maxY = cell.Y; } + + ApplyGenericAutoLAT(minX - 1, minY - 1, maxX + 1, maxY + 1); } } } diff --git a/src/TSMapEditor/Mutations/Classes/HeightMutations/RaiseGroundMutation.cs b/src/TSMapEditor/Mutations/Classes/HeightMutations/RaiseGroundMutation.cs index 3d7552a32..f128973dd 100644 --- a/src/TSMapEditor/Mutations/Classes/HeightMutations/RaiseGroundMutation.cs +++ b/src/TSMapEditor/Mutations/Classes/HeightMutations/RaiseGroundMutation.cs @@ -1,14 +1,11 @@ -using System; -using TSMapEditor.CCEngine; using TSMapEditor.GameMath; -using TSMapEditor.Models; using TSMapEditor.UI; -using HCT = TSMapEditor.Mutations.Classes.HeightMutations.HeightComparisonType; namespace TSMapEditor.Mutations.Classes.HeightMutations { /// - /// A mutation for "smartly" raising ground, with automatic application of ramps. + /// A mutation for "smartly" raising ground, with automatic application of ramps, + /// allowing steep ramps. /// public class RaiseGroundMutation : RaiseGroundMutationBase { @@ -16,146 +13,15 @@ public RaiseGroundMutation(IMutationTarget mutationTarget, Point2D originCell, B { } - private static readonly TransitionRampInfo[] transitionRampInfos = new[] - { - new TransitionRampInfo(RampType.West, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual } ), - new TransitionRampInfo(RampType.North, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.East, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.South, new() { HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - - new TransitionRampInfo(RampType.CornerNW, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.CornerNE, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.CornerSE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.Equal, HCT.Higher }), - new TransitionRampInfo(RampType.CornerSW, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - - new TransitionRampInfo(RampType.MidNW, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Equal, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidNE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Equal, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - - new TransitionRampInfo(RampType.SteepSE, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.MuchHigher, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.SteepSW, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.MuchHigher, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.SteepNW, new() { HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.MuchHigher }), - new TransitionRampInfo(RampType.SteepNE, new() { HCT.Higher, HCT.MuchHigher, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - - new TransitionRampInfo(RampType.DoubleUpSWNE, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant }), - new TransitionRampInfo(RampType.DoubleDownSWNE, new() { HCT.Equal, HCT.Irrelevant, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant, HCT.Equal, HCT.Higher }), - - // Fixes for ramps in "odd angles" between cells - - new TransitionRampInfo(RampType.MidNE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.Equal, HCT.Equal, HCT.Higher, HCT.Irrelevant, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Higher }), - - new TransitionRampInfo(RampType.MidNW, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal }), - - new TransitionRampInfo(RampType.MidSE, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Irrelevant, HCT.Higher, HCT.Equal }), - new TransitionRampInfo(RampType.MidNW, new() { HCT.Equal, HCT.Irrelevant, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.LowerOrEqual }), - - new TransitionRampInfo(RampType.MidNE, new() { HCT.Equal, HCT.LowerOrEqual, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.LowerOrEqual, HCT.Equal }), - - // "Less likely" cases of mid-ramps, where the cell direclty behind the ramp is not higher but the cells on the "backsides" are - new TransitionRampInfo(RampType.MidNW, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidNW, new() { HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual }), - new TransitionRampInfo(RampType.MidNE, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidNE, new() { HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSE, new() { HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.Higher, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - new TransitionRampInfo(RampType.MidSW, new() { HCT.Higher, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.HigherOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.LowerOrEqual, HCT.HigherOrEqual }), - }; - - // Pre-ramp-placement height fix checks - private static readonly TransitionRampInfo[] heightFixers = new TransitionRampInfo[] - { - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Higher, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.HigherOrEqual }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.Higher, HCT.Equal }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal, HCT.HigherOrEqual, HCT.Higher, HCT.Equal }, 1), - - new TransitionRampInfo(RampType.None, new() { HCT.Higher, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant, HCT.Irrelevant, HCT.Irrelevant }, 1), - new TransitionRampInfo(RampType.None, new() { HCT.Irrelevant, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant, HCT.Higher, HCT.Irrelevant }, 1), - }; - - protected override TransitionRampInfo[] GetTransitionRampInfos() => transitionRampInfos; - - protected override TransitionRampInfo[] GetHeightFixers() => heightFixers; + protected override bool AllowSteep => true; public override string GetDisplayString() { - return string.Format(Translate(this, "DisplayString", + return string.Format(Translate(this, "DisplayString", "Raise ground at {0} with a brush size of {1} using steep ramps"), OriginCell, BrushSize); } public override void Perform() => RaiseGround(); - - protected override void CheckCell(Point2D cellCoords) - { - if (processedCellsThisIteration.Contains(cellCoords) || cellsToProcess.Contains(cellCoords)) - return; - - MarkCellAsProcessed(cellCoords); - - var thisCell = Map.GetTile(cellCoords); - if (thisCell == null) - return; - - if (!IsCellMorphable(thisCell)) - return; - - int biggestHeightDiff = 0; - - // With steep ramps we only consider the nearest 4 cells in the "primary directions" - - var northernCell = Map.GetTile(cellCoords + new Point2D(0, -1)); - if (northernCell != null && northernCell.Level > thisCell.Level && IsCellMorphable(northernCell)) - { - biggestHeightDiff = Math.Max(biggestHeightDiff, northernCell.Level - thisCell.Level); - } - - var southernCell = Map.GetTile(cellCoords + new Point2D(0, 1)); - if (southernCell != null && southernCell.Level > thisCell.Level && IsCellMorphable(southernCell)) - { - biggestHeightDiff = Math.Max(biggestHeightDiff, southernCell.Level - thisCell.Level); - } - - var westernCell = Map.GetTile(cellCoords + new Point2D(-1, 0)); - if (westernCell != null && westernCell.Level > thisCell.Level && IsCellMorphable(westernCell)) - { - biggestHeightDiff = Math.Max(biggestHeightDiff, westernCell.Level - thisCell.Level); - } - - var easternCell = Map.GetTile(cellCoords + new Point2D(1, 0)); - if (easternCell != null && easternCell.Level > thisCell.Level && IsCellMorphable(easternCell)) - { - biggestHeightDiff = Math.Max(biggestHeightDiff, easternCell.Level - thisCell.Level); - } - - // If nearby cells are raised by more than 1 cell, it's necessary to also raise this cell - if (biggestHeightDiff > 1) - { - AddCellToUndoData(thisCell.CoordsToPoint()); - thisCell.Level += (byte)(biggestHeightDiff - 1); - - foreach (Point2D offset in SurroundingTiles) - { - RegisterCell(cellCoords + offset); - } - } - } } } diff --git a/src/TSMapEditor/Mutations/Classes/HeightMutations/RaiseGroundMutationBase.cs b/src/TSMapEditor/Mutations/Classes/HeightMutations/RaiseGroundMutationBase.cs index d2c79958c..34eaafd24 100644 --- a/src/TSMapEditor/Mutations/Classes/HeightMutations/RaiseGroundMutationBase.cs +++ b/src/TSMapEditor/Mutations/Classes/HeightMutations/RaiseGroundMutationBase.cs @@ -1,11 +1,7 @@ -using System; using System.Collections.Generic; -using System.Linq; using TSMapEditor.CCEngine; using TSMapEditor.GameMath; -using TSMapEditor.Models.Enums; using TSMapEditor.UI; -using HCT = TSMapEditor.Mutations.Classes.HeightMutations.HeightComparisonType; namespace TSMapEditor.Mutations.Classes.HeightMutations { @@ -15,14 +11,19 @@ protected RaiseGroundMutationBase(IMutationTarget mutationTarget, Point2D origin { } - private TransitionRampInfo flatGroundCheck = new TransitionRampInfo(RampType.None, new() { HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal, HCT.Equal }); - + /// + /// Whether this raise operation is allowed to create steep ramps + /// (a corner spread of two levels across a single cell). + /// + protected abstract bool AllowSteep { get; } /// /// Entry point for raising ground. /// protected void RaiseGround() { + Clear(); + var targetCell = Map.GetTile(OriginCell); if (targetCell == null || targetCell.Level >= Constants.MaxMapHeightLevel || !IsCellMorphable(targetCell)) @@ -30,35 +31,12 @@ protected void RaiseGround() int targetCellHeight = targetCell.Level; - int xSize = BrushSize.Width - 2; - int ySize = BrushSize.Height - 2; - // Special case for 2x2 brush. // Check if we can create a 2x2 "hill". If yes, then do so. // Otherwise, process it as 1x1. if (BrushSize.Width == 2 && BrushSize.Height == 2) { - bool canCreateSmallHill = true; - int height = targetCell.Level; - BrushSize.DoForBrushSize(offset => - { - if (!canCreateSmallHill) - return; - - var otherCellCoords = OriginCell + offset; - var otherCell = Map.GetTile(otherCellCoords); - if (otherCell == null) - { - canCreateSmallHill = false; - return; - } - - var subTile = Map.TheaterInstance.GetTile(otherCell.TileIndex).GetSubTile(otherCell.SubTileIndex); - if (!IsCellMorphable(otherCell) || otherCell.Level != height || subTile.TmpImage.RampType != RampType.None) - canCreateSmallHill = false; - }); - - if (canCreateSmallHill) + if (CanCreateSmallHill(targetCellHeight)) { CreateSmallHill(OriginCell); return; @@ -75,245 +53,79 @@ protected void RaiseGround() return; } + int xSize = BrushSize.Width - 2; + int ySize = BrushSize.Height - 2; + int beginY = OriginCell.Y - ySize / 2; int endY = OriginCell.Y + ySize / 2; int beginX = OriginCell.X - xSize / 2; int endX = OriginCell.X + xSize / 2; - // For all other brush sizes we can have a generic implementation + // Gather the cells we want to raise. We only raise ground that was on the same + // level as our original target cell, otherwise things get illogical. + var targetedCells = new List(); for (int y = beginY; y <= endY; y++) { for (int x = beginX; x <= endX; x++) { var cellCoords = new Point2D(x, y); - targetCell = Map.GetTile(cellCoords); - if (targetCell == null || targetCell.Level >= Constants.MaxMapHeightLevel) + var cell = Map.GetTile(cellCoords); + if (cell == null || cell.Level >= Constants.MaxMapHeightLevel) continue; - // Only raise ground that was on the same level with our original target cell, - // otherwise things get illogical - if (targetCell.Level != targetCellHeight) + if (cell.Level != targetCellHeight) continue; - // Raise this cell and check surrounding cells whether they need ramps - AddCellToUndoData(cellCoords); - targetCell.Level++; - targetCell.ChangeTileIndex(0, 0); - foreach (Point2D surroundingCellOffset in SurroundingTiles) - { - RegisterCell(cellCoords + surroundingCellOffset); - } + if (!IsCellMorphable(cell)) + continue; - MarkCellAsProcessed(cellCoords); + targetedCells.Add(cellCoords); } } - Process(); - } - - private void CreateSmallHill(Point2D cellCoords) - { - AddCellToUndoData(cellCoords); - AddCellToUndoData(cellCoords + new Point2D(1, 0)); - AddCellToUndoData(cellCoords + new Point2D(0, 1)); - AddCellToUndoData(cellCoords + new Point2D(1, 1)); - - Map.GetTile(cellCoords).ChangeTileIndex(RampTileSet.StartTileIndex + ((int)RampType.CornerNW - 1), 0); - Map.GetTile(cellCoords + new Point2D(1, 0)).ChangeTileIndex(RampTileSet.StartTileIndex + ((int)RampType.CornerNE - 1), 0); - Map.GetTile(cellCoords + new Point2D(0, 1)).ChangeTileIndex(RampTileSet.StartTileIndex + ((int)RampType.CornerSW - 1), 0); - Map.GetTile(cellCoords + new Point2D(1, 1)).ChangeTileIndex(RampTileSet.StartTileIndex + ((int)RampType.CornerSE - 1), 0); - - MutationTarget.InvalidateMap(); - } - - /// - /// Applies miscellaneous fixes to height data of processed cells. - /// - protected override void CellHeightFixes() - { - // We process one set of cells at a time, starting from the cells - // processed so far. - // During the processing, we might get new cells to process. - // We repeat the process until no new cells to process have been added to the list. - List cellsToCheck = new(totalProcessedCells); - List newCells = new(); - - while (cellsToCheck.Count > 0) - { - // If a cell has higher cells on both of its sides in a straight diagonal line, - // we need to normalize its height to be on the same level with the diagonal sides, - // because no "one cell" depression exists - cellsToCheck.ForEach(cc => CellHeightFix_CheckStraightDiagonalLine(cc, newCells, true)); - cellsToCheck.ForEach(cc => CellHeightFix_CheckStraightDiagonalLine(cc, newCells, false)); - - // If a cell has a two-levels-higher cell next to it on a horizontal or vertical line, - // and a one-level-higher cell next to it on the opposite side, - // we need to increase its level by 1 - totalProcessedCells.ForEach(cc => CellHeightFix_DifferentHeightDiffsOnStraightLine(cc, new Point2D(1, -1), new Point2D(-1, 1))); - totalProcessedCells.ForEach(cc => CellHeightFix_DifferentHeightDiffsOnStraightLine(cc, new Point2D(-1, -1), new Point2D(1, 1))); - - // Special height fixes - totalProcessedCells.ForEach(cc => - { - var cell = Map.GetTile(cc); - if (cell == null) - return; - - if (!IsCellMorphable(cell)) - return; - - var applyingTransition = Array.Find(GetHeightFixers(), tri => tri.Matches(Map, cc, cell.Level)); - if (applyingTransition != null) - { - AddCellToUndoData(cell.CoordsToPoint()); - cell.ChangeTileIndex(0, 0); - cell.Level += (byte)applyingTransition.HeightChange; - - // If we fixed this flaw from one cell, also check the surrounding ones - foreach (var surroundingCell in SurroundingTiles) - { - if (!newCells.Contains(cc + surroundingCell)) - newCells.Add(cc + surroundingCell); - } - } - }); + if (targetedCells.Count == 0) + return; - var cellsForNextRound = newCells; //.Where(c => !cellsToCheck.Contains(c)); - cellsToCheck.Clear(); - cellsToCheck.AddRange(cellsForNextRound); - totalProcessedCells.AddRange(cellsForNextRound.Where(c => !totalProcessedCells.Contains(c))); - newCells.Clear(); - } + SmoothFlat(targetedCells, targetCellHeight + 1, HeightFloodMode.Up, AllowSteep); } - private void CellHeightFix_CheckStraightDiagonalLine(Point2D cellCoords, List newCells, bool isXAxis) + private bool CanCreateSmallHill(int height) { - var cell = Map.GetTile(cellCoords); - if (cell == null) - return; - - if (!IsCellMorphable(cell)) - return; + bool canCreateSmallHill = true; - Point2D otherCellCoords = cellCoords + new Point2D(isXAxis ? 1 : 0, isXAxis ? 0 : 1); - var otherCell = Map.GetTile(otherCellCoords); - if (otherCell == null || otherCell.Level <= cell.Level) - return; - - int otherLevel = otherCell.Level; - - otherCellCoords = cellCoords + new Point2D(isXAxis ? -1 : 0, isXAxis ? 0 : -1); - otherCell = Map.GetTile(otherCellCoords); - if (otherCell == null) - return; - - if (otherCell.Level == otherLevel) + BrushSize.DoForBrushSize(offset => { - AddCellToUndoData(cell.CoordsToPoint()); - cell.ChangeTileIndex(0, 0); - cell.Level = otherCell.Level; + if (!canCreateSmallHill) + return; - // If we fixed this flaw from one cell, also check the surrounding ones - foreach (var surroundingCell in SurroundingTiles) + var otherCell = Map.GetTile(OriginCell + offset); + if (otherCell == null) { - if (!newCells.Contains(cellCoords + surroundingCell)) - newCells.Add(cellCoords + surroundingCell); + canCreateSmallHill = false; + return; } - } - } - - protected override void ApplyRamps() - { - foreach (var cellCoords in totalProcessedCells) - { - var cell = Map.GetTile(cellCoords); - if (cell == null) - continue; - - if (!IsCellMorphable(cell)) - continue; - - LandType landType = (LandType)Map.TheaterInstance.GetTile(cell.TileIndex).GetSubTile(cell.SubTileIndex).TmpImage.TerrainType; - if (landType == LandType.Rock || landType == LandType.Water) - continue; - - foreach (var transitionRampInfo in GetTransitionRampInfos()) - { - if (transitionRampInfo.Matches(Map, cellCoords, cell.Level)) - { - AddCellToUndoData(cell.CoordsToPoint()); - - if (transitionRampInfo.RampType == RampType.None) - { - cell.ChangeTileIndex(0, 0); - } - else - { - cell.ChangeTileIndex(RampTileSet.StartTileIndex + ((int)transitionRampInfo.RampType - 1), 0); - } - - if (transitionRampInfo.HeightChange != 0) - { - cell.Level += (byte)transitionRampInfo.HeightChange; - } - break; - } - } - } + var subTile = Map.TheaterInstance.GetTile(otherCell.TileIndex).GetSubTile(otherCell.SubTileIndex); + if (!IsCellMorphable(otherCell) || otherCell.Level != height || subTile.TmpImage.RampType != RampType.None) + canCreateSmallHill = false; + }); - CheckForRampsOnFlatGround(); + return canCreateSmallHill; } - /// - /// Checks for leftover ramps on ground surrouding the altered area. - /// - private void CheckForRampsOnFlatGround() + private void CreateSmallHill(Point2D originCell) { - var cellsToProcess = new List(totalProcessedCells); - - // Go through all processed cells and add their neighbours to be processed too - totalProcessedCells.ForEach(cc => - { - for (int i = 0; i < (int)Direction.Count; i++) - { - var otherCellCoords = cc + Helpers.VisualDirectionToPoint((Direction)i); - - if (!cellsToProcess.Contains(otherCellCoords)) - cellsToProcess.Add(otherCellCoords); - } - }); - - for (int i = totalProcessedCells.Count; i < cellsToProcess.Count; i++) - { - var cellCoords = cellsToProcess[i]; - - var cell = Map.GetTile(cellCoords); - if (cell == null) - continue; - - if (!IsCellMorphable(cell)) - continue; - - var subTile = Map.TheaterInstance.GetTile(cell.TileIndex).GetSubTile(cell.SubTileIndex); - LandType landType = (LandType)subTile.TmpImage.TerrainType; - if (landType == LandType.Rock || landType == LandType.Water) - continue; - - if (flatGroundCheck.Matches(Map, cellCoords, cell.Level) && subTile.TmpImage.RampType != RampType.None) - { - cell.ChangeTileIndex(0, 0); - - // Add surroundings of the cell to check as well - for (int dir = 0; dir < (int)Direction.Count; dir++) - { - var otherCellCoords = cellCoords + Helpers.VisualDirectionToPoint((Direction)dir); + // A 2x2 hill is created by raising the single corner shared by all four cells + // (the bottom-right corner of the origin cell). This turns each of the four cells + // into a one-corner ramp, exactly matching the old hard-coded corner stamp. + var field = new CornerHeightField(Map, originCell.X, originCell.Y, originCell.X + 1, originCell.Y + 1); + field.Build(); + + var sharedCorner = new Point2D(originCell.X + 1, originCell.Y + 1); + if (!field.TrySeedAdjustCorner(sharedCorner, 1)) + return; - if (!cellsToProcess.Contains(otherCellCoords)) - cellsToProcess.Add(otherCellCoords); - } - } - } + RunSmoothing(field, HeightFloodMode.Up, AllowSteep); } } } diff --git a/src/TSMapEditor/Mutations/Classes/HeightMutations/TransitionRampInfo.cs b/src/TSMapEditor/Mutations/Classes/HeightMutations/TransitionRampInfo.cs deleted file mode 100644 index 4f77e1b7d..000000000 --- a/src/TSMapEditor/Mutations/Classes/HeightMutations/TransitionRampInfo.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Collections.Generic; -using TSMapEditor.CCEngine; -using TSMapEditor.GameMath; -using TSMapEditor.Models; - -namespace TSMapEditor.Mutations.Classes.HeightMutations -{ - using HCT = HeightComparisonType; - - /// - /// Defines a condition for applying a ramp on a cell based on the - /// height level difference between it and its neighbouring cells. - /// - public class TransitionRampInfo - { - public TransitionRampInfo(RampType rampType, List comparisonTypesForDirections, int heightChange = 0) - { - RampType = rampType; - ComparisonTypesForDirections = comparisonTypesForDirections; - HeightChange = heightChange; - - if (comparisonTypesForDirections.Count != (int)Direction.Count) - { - throw new ArgumentException($"The length of {nameof(comparisonTypesForDirections)} must match " + - $"the number of primary directions in the game ({Direction.Count})"); - } - } - - public readonly RampType RampType; - - public readonly List ComparisonTypesForDirections; - - public int HeightChange { get; } - - public bool Matches(Map map, Point2D cellCoords, int cellHeight) - { - for (int i = 0; i < (int)Direction.Count; i++) - { - var offset = Helpers.VisualDirectionToPoint((Direction)i); - - var otherCellCoords = cellCoords + offset; - var otherCell = map.GetTile(otherCellCoords); - if (otherCell == null) - continue; - - HCT expected = ComparisonTypesForDirections[i]; - - switch (expected) - { - case HCT.Irrelevant: - continue; - - case HCT.Higher: - if (otherCell.Level != cellHeight + 1) - return false; - break; - - case HCT.MuchHigher: - if (otherCell.Level - cellHeight < 2) - return false; - break; - - case HCT.HigherOrEqual: - if (otherCell.Level < cellHeight /*|| otherCell.Level - cellHeight > 1*/) - return false; - break; - - case HCT.Lower: - if (otherCell.Level != cellHeight - 1) - return false; - break; - - case HCT.MuchLower: - if (cellHeight - otherCell.Level < 2) - return false; - break; - - case HCT.LowerOrEqual: - if (otherCell.Level > cellHeight /*|| cellHeight - otherCell.Level > 1*/) - return false; - break; - - case HCT.Equal: - if (otherCell.Level != cellHeight) - return false; - break; - } - } - - return true; - } - } -} diff --git a/src/TSMapEditor/UI/CursorActions/HeightActions/FlattenGroundCursorAction.cs b/src/TSMapEditor/UI/CursorActions/HeightActions/FlattenGroundCursorAction.cs index dd19488ec..924a06fbb 100644 --- a/src/TSMapEditor/UI/CursorActions/HeightActions/FlattenGroundCursorAction.cs +++ b/src/TSMapEditor/UI/CursorActions/HeightActions/FlattenGroundCursorAction.cs @@ -1,4 +1,6 @@ -using TSMapEditor.GameMath; +using TSMapEditor.CCEngine; +using TSMapEditor.GameMath; +using TSMapEditor.Models.Enums; using TSMapEditor.Mutations.Classes.HeightMutations; namespace TSMapEditor.UI.CursorActions.HeightActions @@ -45,11 +47,11 @@ public override void LeftDown(Point2D cellCoords) return; } - // Don't act on non-morphable terrain - if (!Map.IsCellMorphable(cell)) - { - return; - } + // Note: the hovered cell itself is intentionally NOT required to be morphable. + // When flattening ground up to a cliff, the cursor naturally rests on the cliff + // cells themselves (especially for back-facing cliffs), and the brush area still + // covers flattenable ground next to them. Non-morphable cells are filtered + // per-cell below and in the mutation. // Check the area of the brush on whether it has any cells that do not match // the height of the origin cell. @@ -64,19 +66,27 @@ public override void LeftDown(Point2D cellCoords) bool perform = false; - for (int y = beginY; y <= endY; y++) + for (int y = beginY; y <= endY && !perform; y++) { for (int x = beginX; x <= endX; x++) { var targetCellCoords = new Point2D(x, y); var targetCell = Map.GetTile(targetCellCoords); - if (targetCell != null && targetCell.Level != desiredHeightLevel) - { - // Don't act on non-morphable terrain - if (!Map.IsCellMorphable(cell)) - continue; + // Don't act on non-morphable terrain + if (targetCell == null || !Map.IsCellMorphable(targetCell)) + continue; + + var tmpImage = Map.TheaterInstance.GetTile(targetCell.TileIndex).GetSubTile(targetCell.SubTileIndex).TmpImage; + LandType landType = (LandType)tmpImage.TerrainType; + if (landType == LandType.Rock || landType == LandType.Water) + continue; + // A cell needs flattening if it is at a different level, or if it is a ramp + // (a ramp's Level is its lowest corner, so a ridge of ramps can all be "at" + // the desired level yet still need flattening). + if (targetCell.Level != desiredHeightLevel || tmpImage.RampType != RampType.None) + { perform = true; break; }