Room system added
This commit is contained in:
BIN
assets/sprites/aux_terrain.BMP
Normal file
BIN
assets/sprites/aux_terrain.BMP
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
40
assets/sprites/aux_terrain.BMP.import
Normal file
40
assets/sprites/aux_terrain.BMP.import
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b20mhn7ca5xyo"
|
||||||
|
path="res://.godot/imported/aux_terrain.BMP-15c2f0fd910deee8ff95cb1125e18906.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/aux_terrain.BMP"
|
||||||
|
dest_files=["res://.godot/imported/aux_terrain.BMP-15c2f0fd910deee8ff95cb1125e18906.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
320
docs/superpowers/plans/2026-04-05-room-system.md
Normal file
320
docs/superpowers/plans/2026-04-05-room-system.md
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
# 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
|
||||||
56
docs/superpowers/specs/2026-04-05-room-system-design.md
Normal file
56
docs/superpowers/specs/2026-04-05-room-system-design.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Room System Design
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
A room system for battle maps where rooms are defined as groups of tiles, walls exist on tile borders (not as tiles), and openings are explicitly marked doorways between rooms. Tiles outside rooms are impassable void.
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
### Room (Resource)
|
||||||
|
|
||||||
|
- `id: int` — unique room identifier
|
||||||
|
- `tiles: Array[Vector2i]` — tile coordinates belonging to this room
|
||||||
|
|
||||||
|
### MapLayout (Resource)
|
||||||
|
|
||||||
|
- `rooms: Array[Room]` — all rooms on the map
|
||||||
|
- `openings: Array` — list of tile-coordinate pairs (`[Vector2i, Vector2i]`) representing bidirectional doorways between adjacent tiles in different rooms
|
||||||
|
|
||||||
|
### Derived Wall Computation
|
||||||
|
|
||||||
|
A wall exists on a tile edge when:
|
||||||
|
|
||||||
|
1. One side is a room tile and the other is void (not in any room), OR
|
||||||
|
2. The two sides belong to different rooms AND the edge is not in the openings list
|
||||||
|
|
||||||
|
### MapLayout API
|
||||||
|
|
||||||
|
- `get_walls() -> Array` — computes all wall edge segments from room and opening data
|
||||||
|
- `is_passable(from: Vector2i, to: Vector2i) -> bool` — returns whether movement is allowed between two adjacent tiles (false if wall, false if either tile is void)
|
||||||
|
- `is_tile_valid(tile: Vector2i) -> bool` — returns whether a tile belongs to any room
|
||||||
|
- `get_room_at(tile: Vector2i) -> Room` — returns the room a tile belongs to, or null
|
||||||
|
|
||||||
|
## Integration with Movement
|
||||||
|
|
||||||
|
- `CombatMap` holds a reference to the `MapLayout`
|
||||||
|
- Before allowing a unit to move from tile A to tile B, the movement system calls `MapLayout.is_passable(a, b)`
|
||||||
|
- If there is a wall on that edge (and no opening), movement is blocked
|
||||||
|
- Tiles not belonging to any room are impassable — `is_tile_valid()` returns false for void tiles
|
||||||
|
- Future pathfinding uses the same `is_passable` check as its neighbor filter
|
||||||
|
|
||||||
|
## Wall Rendering
|
||||||
|
|
||||||
|
- `CombatMap` iterates over computed wall edges from `MapLayout.get_walls()`
|
||||||
|
- Each wall edge is defined by two adjacent tile coordinates — the wall is drawn on the border between them
|
||||||
|
- Openings have no wall drawn
|
||||||
|
- Initial implementation uses simple `Line2D` segments or sprite strips on tile edges
|
||||||
|
- Can be replaced with proper art assets later
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
- **Rooms are the primary data, walls are derived** — single source of truth, no sync issues
|
||||||
|
- **Openings are explicit** — all room boundaries are walls by default; doorways are punched explicitly as tile-coordinate pairs
|
||||||
|
- **Openings are always bidirectional**
|
||||||
|
- **No room metadata beyond ID** — tile ownership stays tile-level per existing system
|
||||||
|
- **Void tiles are impassable** — no hallway/corridor concept; all walkable tiles belong to a room
|
||||||
|
- **Room data is independent of rendering** — clean for future map editor serialization
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
* Debug menu on F1, right now just include quick scene swapping
|
|
||||||
* Finish buttons on dialogue scene
|
|
||||||
* Reogranize files
|
* Reogranize files
|
||||||
* Singletons named 'XXXServer'
|
* Singletons named 'XXXServer'
|
||||||
* Dialogue scene command system (ShowText, ShowSprite, MoveSprite, PlaySound, ChangeBackground, etc)
|
* Dialogue scene command system (ShowText, ShowSprite, MoveSprite, PlaySound, ChangeBackground, etc)
|
||||||
|
* Finish Dialogue Scene (fast forward, auto, history functionality, etc)
|
||||||
* Setup room system (everything is unpassable, carve out rooms, walls automatic, specify connections between rooms on tiles)
|
* Setup room system (everything is unpassable, carve out rooms, walls automatic, specify connections between rooms on tiles)
|
||||||
* Basic map editor (test map data will be harder to craft the more we add)
|
* Basic map editor (test map data will be harder to craft the more we add)
|
||||||
@@ -2,6 +2,7 @@ class_name CombatMap
|
|||||||
extends Node2D
|
extends Node2D
|
||||||
|
|
||||||
@export var tile_set: DLTileset
|
@export var tile_set: DLTileset
|
||||||
|
@export var map_layout: MapLayout
|
||||||
@onready var tile_map: TileMapLayer = %TerrainLayer
|
@onready var tile_map: TileMapLayer = %TerrainLayer
|
||||||
@onready var highlight_map: GridOverlay = %OverlayLayer
|
@onready var highlight_map: GridOverlay = %OverlayLayer
|
||||||
|
|
||||||
@@ -18,6 +19,8 @@ func _ready() -> void:
|
|||||||
for entry in _pending_units:
|
for entry in _pending_units:
|
||||||
_apply_deploy(entry.unit, entry.coords)
|
_apply_deploy(entry.unit, entry.coords)
|
||||||
_pending_units.clear()
|
_pending_units.clear()
|
||||||
|
if map_layout:
|
||||||
|
apply_layout(map_layout)
|
||||||
|
|
||||||
|
|
||||||
func snap_to_grid(pos: Vector2) -> Vector2:
|
func snap_to_grid(pos: Vector2) -> Vector2:
|
||||||
@@ -79,3 +82,52 @@ func remove_unit(unit: Unit) -> void:
|
|||||||
|
|
||||||
func target_tile(coords: Vector2i) -> void:
|
func target_tile(coords: Vector2i) -> void:
|
||||||
highlight_map.target_tile(coords)
|
highlight_map.target_tile(coords)
|
||||||
|
|
||||||
|
|
||||||
|
func apply_layout(layout: MapLayout) -> void:
|
||||||
|
map_layout = layout
|
||||||
|
map_layout.initialize()
|
||||||
|
load_from_layout()
|
||||||
|
draw_room_walls()
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
func load_from_layout() -> void:
|
||||||
|
if not map_layout:
|
||||||
|
return
|
||||||
|
for room in map_layout.rooms:
|
||||||
|
for tile in room.tiles:
|
||||||
|
draw_floor(tile)
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ func _physics_process(delta: float) -> void:
|
|||||||
|
|
||||||
var next_pos := _selected_unit.position + dir * dl_map.TILE_SIZE
|
var next_pos := _selected_unit.position + dir * dl_map.TILE_SIZE
|
||||||
var grid_coords := dl_map.world_to_coords(next_pos)
|
var grid_coords := dl_map.world_to_coords(next_pos)
|
||||||
if dl_map.is_wall(grid_coords):
|
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
|
_goal_pos = _selected_unit.position
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -139,7 +140,7 @@ func _handle_left_click(screen_pos: Vector2) -> void:
|
|||||||
elif _selected_unit:
|
elif _selected_unit:
|
||||||
var snapped_pos := dl_map.snap_to_grid(world_pos)
|
var snapped_pos := dl_map.snap_to_grid(world_pos)
|
||||||
var grid_coords := dl_map.world_to_coords(world_pos)
|
var grid_coords := dl_map.world_to_coords(world_pos)
|
||||||
if dl_map.is_wall(grid_coords):
|
if not dl_map.is_tile_valid(grid_coords):
|
||||||
return
|
return
|
||||||
_goal_pos = snapped_pos
|
_goal_pos = snapped_pos
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
|||||||
@@ -7,6 +7,31 @@ class_name StrategyPhase extends Node2D
|
|||||||
@onready var camera: CameraController = $Camera2D
|
@onready var camera: CameraController = $Camera2D
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
# -- 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.apply_layout(layout)
|
||||||
|
# -- End test room layout --
|
||||||
|
|
||||||
player_controller.combat_requested.connect(_on_combat_requested)
|
player_controller.combat_requested.connect(_on_combat_requested)
|
||||||
player_controller.mouse_grid_changed.connect(_on_mouse_grid_changed)
|
player_controller.mouse_grid_changed.connect(_on_mouse_grid_changed)
|
||||||
player_controller.camera_drag.connect(camera.apply_drag)
|
player_controller.camera_drag.connect(camera.apply_drag)
|
||||||
|
|||||||
72
resources/resource_definitions/map_layout.gd
Normal file
72
resources/resource_definitions/map_layout.gd
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
class_name MapLayout extends Resource
|
||||||
|
|
||||||
|
@export var rooms: Array[Room]
|
||||||
|
## 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.
|
||||||
|
@export var openings: Array[Vector2i]
|
||||||
|
|
||||||
|
var _tile_room_map: Dictionary = {}
|
||||||
|
var _opening_set: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func initialize() -> void:
|
||||||
|
assert(openings.size() % 2 == 0, "Openings must be provided as pairs of Vector2i")
|
||||||
|
_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]
|
||||||
|
|
||||||
|
|
||||||
|
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: Vector2i = 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
|
||||||
1
resources/resource_definitions/map_layout.gd.uid
Normal file
1
resources/resource_definitions/map_layout.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dj7qfdelq4ja4
|
||||||
4
resources/resource_definitions/room.gd
Normal file
4
resources/resource_definitions/room.gd
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
class_name Room extends Resource
|
||||||
|
|
||||||
|
@export var id: int
|
||||||
|
@export var tiles: Array[Vector2i]
|
||||||
1
resources/resource_definitions/room.gd.uid
Normal file
1
resources/resource_definitions/room.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ja34p4vpwamd
|
||||||
BIN
room_wall_example.webp
Normal file
BIN
room_wall_example.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
40
room_wall_example.webp.import
Normal file
40
room_wall_example.webp.import
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://2d7pwqqr03ra"
|
||||||
|
path="res://.godot/imported/room_wall_example.webp-46656a4dae88a645a1bd5646f3349f4d.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://room_wall_example.webp"
|
||||||
|
dest_files=["res://.godot/imported/room_wall_example.webp-46656a4dae88a645a1bd5646f3349f4d.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -80,20 +80,11 @@ const UNIT_SCENE = preload(\"res://prefabs/unit.tscn\")
|
|||||||
const PLAYER_ALLEGIANCE = preload(\"res://resources/allegiance_types/player_allegiance.tres\")
|
const PLAYER_ALLEGIANCE = preload(\"res://resources/allegiance_types/player_allegiance.tres\")
|
||||||
const ENEMY_ALLEGIANCE = preload(\"res://resources/allegiance_types/enemy_allegiance.tres\")
|
const ENEMY_ALLEGIANCE = preload(\"res://resources/allegiance_types/enemy_allegiance.tres\")
|
||||||
|
|
||||||
const MAP_LAYOUT := \"\"\"\\
|
|
||||||
#####
|
|
||||||
#...#
|
|
||||||
#...#
|
|
||||||
#...#
|
|
||||||
#####\"\"\"
|
|
||||||
|
|
||||||
func _pressed() -> void:
|
func _pressed() -> void:
|
||||||
await get_tree().create_timer(0.2).timeout
|
await get_tree().create_timer(0.2).timeout
|
||||||
var combat_instance := COMBAT_SCENE.instantiate()
|
var combat_instance := COMBAT_SCENE.instantiate()
|
||||||
var combat_map: CombatMap = combat_instance.find_child(\"CombatMap\")
|
var combat_map: CombatMap = combat_instance.find_child(\"CombatMap\")
|
||||||
|
|
||||||
combat_map.load_map(MAP_LAYOUT)
|
|
||||||
|
|
||||||
var player_unit: Unit = UNIT_SCENE.instantiate()
|
var player_unit: Unit = UNIT_SCENE.instantiate()
|
||||||
player_unit.stat_template = UnitStats.new(50)
|
player_unit.stat_template = UnitStats.new(50)
|
||||||
player_unit.info_template = UnitInfo.new()
|
player_unit.info_template = UnitInfo.new()
|
||||||
|
|||||||
Reference in New Issue
Block a user