# Room System Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a room/wall system where rooms are tile groups, walls are derived from room boundaries, openings are explicit doorways, and movement is blocked by walls. **Architecture:** Two new Resources (`Room`, `MapLayout`) define the room data. `MapLayout` computes walls from room boundaries and exposes passability queries. `CombatMap` renders walls on tile edges and uses `MapLayout` for movement checks. `PlayerController` is updated to check edge passability instead of tile-level wall checks. **Tech Stack:** Godot 4.6 / GDScript --- ### Task 1: Create Room Resource **Files:** - Create: `resources/resource_definitions/room.gd` - [ ] **Step 1: Create the Room resource** ```gdscript class_name Room extends Resource @export var id: int @export var tiles: Array[Vector2i] ``` - [ ] **Step 2: Verify it loads in the editor** Open Godot, create a new Resource, confirm `Room` appears as a type and `id`/`tiles` fields are visible in the inspector. --- ### Task 2: Create MapLayout Resource **Files:** - Create: `resources/resource_definitions/map_layout.gd` - [ ] **Step 1: Create the MapLayout resource with room data and opening storage** ```gdscript class_name MapLayout extends Resource @export var rooms: Array[Room] @export var openings: Array[Vector2i] ## Openings are stored as a flat array of pairs: [from1, to1, from2, to2, ...]. ## Each consecutive pair of Vector2i values represents a bidirectional doorway ## between two adjacent tiles in different rooms. ``` - [ ] **Step 2: Add the tile-to-room lookup cache and initialization** ```gdscript var _tile_room_map: Dictionary = {} var _opening_set: Dictionary = {} func initialize() -> void: _tile_room_map.clear() _opening_set.clear() for room in rooms: for tile in room.tiles: _tile_room_map[tile] = room for i in range(0, openings.size(), 2): var a := openings[i] var b := openings[i + 1] _opening_set[_edge_key(a, b)] = true static func _edge_key(a: Vector2i, b: Vector2i) -> String: if a < b: return "%d,%d-%d,%d" % [a.x, a.y, b.x, b.y] return "%d,%d-%d,%d" % [b.x, b.y, a.x, a.y] ``` - [ ] **Step 3: Add the public API methods** ```gdscript func is_tile_valid(tile: Vector2i) -> bool: return _tile_room_map.has(tile) func get_room_at(tile: Vector2i) -> Room: return _tile_room_map.get(tile, null) func is_passable(from: Vector2i, to: Vector2i) -> bool: if not is_tile_valid(from) or not is_tile_valid(to): return false var room_from: Room = _tile_room_map[from] var room_to: Room = _tile_room_map[to] if room_from == room_to: return true return _opening_set.has(_edge_key(from, to)) func get_walls() -> Array: ## Returns an array of [Vector2i, Vector2i] pairs representing wall edges. ## A wall exists where a room tile borders void or a different room (without an opening). var walls: Array = [] var directions := [Vector2i.RIGHT, Vector2i.DOWN, Vector2i.LEFT, Vector2i.UP] var visited_edges: Dictionary = {} for room in rooms: for tile in room.tiles: for dir in directions: var neighbor := tile + dir var key := _edge_key(tile, neighbor) if visited_edges.has(key): continue visited_edges[key] = true var neighbor_room: Room = _tile_room_map.get(neighbor, null) if neighbor_room == room: continue # Neighbor is void or different room — wall unless opening if not _opening_set.has(key): walls.append([tile, neighbor]) return walls ``` - [ ] **Step 4: Verify it loads in the editor** Open Godot, create a new Resource, confirm `MapLayout` appears as a type and `rooms`/`openings` fields are visible. --- ### Task 3: Integrate MapLayout into CombatMap **Files:** - Modify: `nodes/combat_map.gd` - [ ] **Step 1: Add MapLayout export and initialization** Add after the existing `@export var tile_set: DLTileset` line in `combat_map.gd`: ```gdscript @export var map_layout: MapLayout ``` Add at the end of the existing `_ready()` function: ```gdscript if map_layout: map_layout.initialize() ``` - [ ] **Step 2: Add passability and validity methods that delegate to MapLayout** Add these methods to `combat_map.gd`: ```gdscript func is_tile_passable(from: Vector2i, to: Vector2i) -> bool: if map_layout: return map_layout.is_passable(from, to) # Fallback: no room system, use legacy wall check return not is_wall(to) func is_tile_valid(coords: Vector2i) -> bool: if map_layout: return map_layout.is_tile_valid(coords) # Fallback: no room system, any non-wall tile is valid return not is_wall(coords) ``` - [ ] **Step 3: Add wall rendering from MapLayout** Add this method to `combat_map.gd`: ```gdscript func draw_room_walls() -> void: if not map_layout: return var walls := map_layout.get_walls() for wall in walls: var from_world := coords_to_world(wall[0]) + Vector2(TILE_SIZE / 2, TILE_SIZE / 2) var to_world := coords_to_world(wall[1]) + Vector2(TILE_SIZE / 2, TILE_SIZE / 2) var midpoint := (from_world + to_world) / 2 var diff := to_world - from_world var wall_dir := Vector2(-diff.y, diff.x).normalized() var half_len := TILE_SIZE / 2 var line := Line2D.new() line.add_point(midpoint - wall_dir * half_len) line.add_point(midpoint + wall_dir * half_len) line.width = 4.0 line.default_color = Color(0.6, 0.5, 0.4) add_child(line) ``` This draws a line segment on the border between each pair of tiles that has a wall. The line is perpendicular to the direction between the two tiles and spans the full tile edge. - [ ] **Step 4: Add floor rendering from MapLayout and call everything from _ready()** Add this method to `combat_map.gd`: ```gdscript func load_from_layout() -> void: if not map_layout: return for room in map_layout.rooms: for tile in room.tiles: draw_floor(tile) ``` Update the `_ready()` addition so it renders floors then walls: ```gdscript if map_layout: map_layout.initialize() load_from_layout() draw_room_walls() ``` --- ### Task 4: Update PlayerController to Use Edge-Based Passability **Files:** - Modify: `nodes/player_controller.gd` - [ ] **Step 1: Update movement check in `_physics_process`** In `player_controller.gd`, find the movement blocking check in `_physics_process` (around line 121-123): ```gdscript if dl_map.is_wall(grid_coords): _goal_pos = _selected_unit.position return ``` Replace with: ```gdscript var current_coords := dl_map.world_to_coords(_selected_unit.position) if not dl_map.is_tile_passable(current_coords, grid_coords): _goal_pos = _selected_unit.position return ``` - [ ] **Step 2: Update click destination check in `_handle_left_click`** In `player_controller.gd`, find the wall check in `_handle_left_click` (around line 142-143): ```gdscript if dl_map.is_wall(grid_coords): return ``` Replace with: ```gdscript if not dl_map.is_tile_valid(grid_coords): return ``` This prevents clicking on void tiles as a destination. The step-by-step movement in `_physics_process` handles wall/opening checks as the unit walks. --- ### Task 5: Create a Test Map Layout **Files:** - Modify: `nodes/strategy_phase.gd` - [ ] **Step 1: Add test layout setup in strategy_phase.gd** Add at the top of `_ready()` in `strategy_phase.gd`, before existing code: ```gdscript # -- Test room layout (remove once map editor exists) -- var room_a := Room.new() room_a.id = 0 room_a.tiles = [ Vector2i(0, 0), Vector2i(1, 0), Vector2i(2, 0), Vector2i(0, 1), Vector2i(1, 1), Vector2i(2, 1), Vector2i(0, 2), Vector2i(1, 2), Vector2i(2, 2), ] var room_b := Room.new() room_b.id = 1 room_b.tiles = [ Vector2i(3, 0), Vector2i(4, 0), Vector2i(5, 0), Vector2i(3, 1), Vector2i(4, 1), Vector2i(5, 1), Vector2i(3, 2), Vector2i(4, 2), Vector2i(5, 2), ] var layout := MapLayout.new() layout.rooms = [room_a, room_b] # Opening between (2,1) in room_a and (3,1) in room_b layout.openings = [Vector2i(2, 1), Vector2i(3, 1)] combat_map.map_layout = layout # -- End test room layout -- ``` - [ ] **Step 2: Run the game and verify** Run the strategy phase scene. Expected: - Two 3x3 rooms appear side by side - Walls are drawn on all outer edges and on the room-to-room border - The middle row (y=1) has no wall between tiles (2,1) and (3,1) — that's the opening - A unit can walk through the opening but not through the walls - Clicking on void tiles (outside rooms) does nothing --- ### Task 6: Review All Changes **Files:** - All modified files - [ ] **Step 1: Verify completeness** Confirm: - `Room` resource has `id` and `tiles` - `MapLayout` resource has `rooms`, `openings`, `initialize()`, `is_passable()`, `is_tile_valid()`, `get_room_at()`, `get_walls()` - `CombatMap` has `map_layout` export, `is_tile_passable()`, `is_tile_valid()`, `draw_room_walls()`, `load_from_layout()` - `PlayerController` uses `is_tile_passable()` for movement and `is_tile_valid()` for click validation - Legacy `is_wall()` still works as fallback when no `map_layout` is set