using Theriapolis.Core.Data; using Theriapolis.Core.Tactical; using Theriapolis.Core.World; namespace Theriapolis.Core.Dungeons; /// /// Phase 7 M1 — paints the rooms + corridors of a planned dungeon into a /// tactical-tile array. Pure, deterministic given the same plan input. /// /// The painter: /// 1. Allocates a [w, h] array of the planned /// dungeon size. /// 2. For each room, copies its char /// by char into the array at the room's AABB top-left, mapping each /// char to a + /// pair via . /// 3. For each connection, runs a Manhattan-path corridor between the /// two door tiles, writing /// and ensuring the door tiles themselves are walkable /// (). /// /// The dungeon's surface family (DungeonFloor vs Cave vs MineFloor) is /// chosen by dungeon type so different dungeons feel visually distinct /// even before art lands. /// internal static class RoomTilePainter { public static TacticalTile[,] Paint( RoomGraphAssembler.Plan plan, IReadOnlyDictionary templatesById, PoiType dungeonType) { var tiles = new TacticalTile[plan.DungeonW, plan.DungeonH]; // Default fill: solid wall (so any unpainted gap is automatically // impassable). The painter then carves rooms + corridors out of it. for (int y = 0; y < plan.DungeonH; y++) for (int x = 0; x < plan.DungeonW; x++) { tiles[x, y] = new TacticalTile { Surface = TacticalSurface.Wall, Deco = TacticalDeco.None, Variant = 0, Flags = 0, }; } TacticalSurface defaultFloor = SurfaceForDungeonType(dungeonType); // Paint each room's grid. foreach (var room in plan.Rooms) { if (!templatesById.TryGetValue(room.TemplateId, out var def)) continue; // shouldn't happen — ContentLoader validates these for (int gy = 0; gy < def.FootprintHTiles; gy++) { for (int gx = 0; gx < def.FootprintWTiles; gx++) { int dx = room.AabbX + gx; int dy = room.AabbY + gy; if (dx < 0 || dy < 0 || dx >= plan.DungeonW || dy >= plan.DungeonH) continue; char ch = def.Grid[gy][gx]; var (surface, deco) = MapChar(ch, defaultFloor); tiles[dx, dy].Surface = surface; tiles[dx, dy].Deco = deco; } } } // Carve corridors between door tiles. foreach (var conn in plan.Connections) { CarveCorridor(tiles, plan, conn, defaultFloor); } // Mark the entrance tile so the renderer can highlight it. Stairs // is the canonical "interactable enter / exit" deco. if (plan.EntranceTile.X >= 0 && plan.EntranceTile.X < plan.DungeonW && plan.EntranceTile.Y >= 0 && plan.EntranceTile.Y < plan.DungeonH) { tiles[plan.EntranceTile.X, plan.EntranceTile.Y].Surface = defaultFloor; tiles[plan.EntranceTile.X, plan.EntranceTile.Y].Deco = TacticalDeco.Stairs; } return tiles; } private static (TacticalSurface surface, TacticalDeco deco) MapChar(char ch, TacticalSurface defaultFloor) => ch switch { '#' => (TacticalSurface.Wall, TacticalDeco.None), '.' => (defaultFloor, TacticalDeco.None), ',' => (TacticalSurface.DungeonRubble, TacticalDeco.None), 'D' => (defaultFloor, TacticalDeco.DungeonDoor), '@' => (defaultFloor, TacticalDeco.None), // encounter slot — spawn placed by populator 'C' => (defaultFloor, TacticalDeco.Container), 'T' => (defaultFloor, TacticalDeco.Trap), 'P' => (defaultFloor, TacticalDeco.Pillar), 'B' => (defaultFloor, TacticalDeco.Brazier), 'M' => (TacticalSurface.DungeonTile, TacticalDeco.None), // mosaic / narrative inlay 'S' => (defaultFloor, TacticalDeco.Stairs), ' ' => (TacticalSurface.None, TacticalDeco.None), _ => (defaultFloor, TacticalDeco.None), // unknown → walkable floor }; private static TacticalSurface SurfaceForDungeonType(PoiType type) => type switch { PoiType.AbandonedMine => TacticalSurface.MineFloor, PoiType.NaturalCave => TacticalSurface.Cave, PoiType.CultDen => TacticalSurface.Cave, PoiType.OvergrownSettlement => TacticalSurface.DungeonFloor, PoiType.ImperiumRuin => TacticalSurface.DungeonFloor, _ => TacticalSurface.DungeonFloor, }; /// /// Manhattan corridor from door A to door B. Picks one of the two L-bends /// deterministically (vertical-first if the connection is "more vertical" /// than horizontal; horizontal-first otherwise) so two seeds don't /// produce different routes through the same plan. /// private static void CarveCorridor( TacticalTile[,] tiles, RoomGraphAssembler.Plan plan, RoomConnection conn, TacticalSurface defaultFloor) { int x0 = conn.DoorAx, y0 = conn.DoorAy; int x1 = conn.DoorBx, y1 = conn.DoorBy; // Ensure the door tiles themselves are walkable. SafeSet(tiles, plan, x0, y0, defaultFloor, TacticalDeco.DungeonDoor); SafeSet(tiles, plan, x1, y1, defaultFloor, TacticalDeco.DungeonDoor); // Decide bend axis. int dx = Math.Abs(x1 - x0); int dy = Math.Abs(y1 - y0); bool horizontalFirst = dx >= dy; if (horizontalFirst) { int xa = Math.Min(x0, x1); int xb = Math.Max(x0, x1); for (int x = xa; x <= xb; x++) SafeSet(tiles, plan, x, y0, defaultFloor, TacticalDeco.None, preserveDoor: true); int ya = Math.Min(y0, y1); int yb = Math.Max(y0, y1); for (int y = ya; y <= yb; y++) SafeSet(tiles, plan, x1, y, defaultFloor, TacticalDeco.None, preserveDoor: true); } else { int ya = Math.Min(y0, y1); int yb = Math.Max(y0, y1); for (int y = ya; y <= yb; y++) SafeSet(tiles, plan, x0, y, defaultFloor, TacticalDeco.None, preserveDoor: true); int xa = Math.Min(x0, x1); int xb = Math.Max(x0, x1); for (int x = xa; x <= xb; x++) SafeSet(tiles, plan, x, y1, defaultFloor, TacticalDeco.None, preserveDoor: true); } } private static void SafeSet( TacticalTile[,] tiles, RoomGraphAssembler.Plan plan, int x, int y, TacticalSurface surface, TacticalDeco deco, bool preserveDoor = false) { if (x < 0 || y < 0 || x >= plan.DungeonW || y >= plan.DungeonH) return; // Don't bulldoze a room's interior decoration when the corridor // happens to clip through (carry-over from straightline paths // that cross a room edge). The painter already laid the room // first, so corridor only needs to convert Wall/None → Floor. var existing = tiles[x, y]; if (existing.Surface != TacticalSurface.Wall && existing.Surface != TacticalSurface.None && !(preserveDoor && existing.Deco == TacticalDeco.DungeonDoor)) return; tiles[x, y].Surface = surface; if (!preserveDoor || existing.Deco != TacticalDeco.DungeonDoor) tiles[x, y].Deco = deco; } }