# 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
