feat: Add core map generation and management system
- Introduced MapData, MapGenerator, and MapPainter classes for procedural map generation. - Implemented LevelManager to handle level initialization and navigation. - Created TurnManager for managing turn order in a turn-based system. - Added Player and Enemy character classes with movement and turn-taking capabilities. - Established pathfinding using AStar2D in PathfindingHandler. - Integrated new scenes for player and enemy characters, along with world setup. - Included a new tileset for futuristic Zelda-themed assets.
@@ -0,0 +1,4 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Godot 4+ specific ignores
|
||||||
|
.godot/
|
||||||
|
/android/
|
||||||
|
*.uid
|
||||||
|
*.import
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"godotTools.editorPath.godot4": "/home/stefwill/Applications/Godot/Godot_v4.6.1-stable_linux.x86_64"
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
**Shadow Pixel Run** — a 2D pixel art game built with Godot 4.6 (Forward Plus renderer).
|
||||||
|
|
||||||
|
## Running the Game
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Launch the game
|
||||||
|
/home/stefwill/Applications/Godot/Godot_v4.6-stable_linux.x86_64 --path .
|
||||||
|
|
||||||
|
# Run headless (e.g. for script testing)
|
||||||
|
/home/stefwill/Applications/Godot/Godot_v4.6-stable_linux.x86_64 --path . --headless
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no build step — Godot runs the project directly from source.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Scene Structure
|
||||||
|
|
||||||
|
- **Main scene:** `scenes/world.tscn` — a `Node2D` containing two `TileMapLayer` nodes (`FloorLayer`, `WallLayer`) and a `Player` node.
|
||||||
|
- **Player:** `CharacterBody2D` at `scenes/world.tscn > Player`, driven by `scripts/actors/player.gd`.
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
All scripts live under `scripts/` organized by category:
|
||||||
|
- `scripts/actors/` — character scripts (`player.gd` defines `class_name Player extends CharacterBody2D`)
|
||||||
|
|
||||||
|
### Assets
|
||||||
|
|
||||||
|
All assets live under `assets/sprites/`. Godot auto-generates `.import` files alongside source assets — these are gitignored and should not be manually edited.
|
||||||
|
|
||||||
|
### Tilemaps
|
||||||
|
|
||||||
|
The world uses two `TileMapLayer` nodes with 32×32 tile atlases:
|
||||||
|
- `FloorLayer` — `office_floor.png`, simple single-tile floor atlas
|
||||||
|
- `WallLayer` — `office_walls.png`, terrain tileset with corner/edge peering bits for seamless auto-connected walls
|
||||||
|
|
||||||
|
### Input Map
|
||||||
|
|
||||||
|
Defined in `project.godot`:
|
||||||
|
- `move_left` → A / Left Arrow
|
||||||
|
- `move_right` → D / Right Arrow
|
||||||
|
- `move_up` → W / Up Arrow
|
||||||
|
- `move_down` → S / Down Arrow
|
||||||
|
|
||||||
|
### Viewport
|
||||||
|
|
||||||
|
640×360 with 4× window scale — targets a retro pixel art look. Default texture filter is **Linear** (set per-sprite if pixelated filtering is needed).
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2016-2023 The Godot Engine community
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[configuration]
|
||||||
|
|
||||||
|
entry_symbol = "git_plugin_init"
|
||||||
|
compatibility_minimum = "4.2.0"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
|
||||||
|
linux.editor.x86_64 = "linux/libgit_plugin.linux.editor.x86_64.so"
|
||||||
|
macos.editor = "macos/libgit_plugin.macos.editor.universal.dylib"
|
||||||
|
windows.editor.x86_64 = "windows/libgit_plugin.windows.editor.x86_64.dll"
|
||||||
|
After Width: | Height: | Size: 737 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 995 B |
|
After Width: | Height: | Size: 140 B |
|
After Width: | Height: | Size: 455 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 416 B |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,80 @@
|
|||||||
|
[gd_resource type="TileSet" format=3 uid="uid://142kl47o7n2m"]
|
||||||
|
|
||||||
|
[ext_resource type="Texture2D" uid="uid://c55vbj78e0yrl" path="res://assets/sprites/tileset_office_walls.png" id="1"]
|
||||||
|
|
||||||
|
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_t1ruc"]
|
||||||
|
texture = ExtResource("1")
|
||||||
|
texture_region_size = Vector2i(32, 32)
|
||||||
|
0:0/0 = 0
|
||||||
|
0:0/0/terrain_set = 0
|
||||||
|
0:0/0/terrain = 0
|
||||||
|
0:0/0/terrains_peering_bit/bottom_left_corner = 0
|
||||||
|
0:0/0/terrains_peering_bit/top_left_corner = 0
|
||||||
|
0:0/0/terrains_peering_bit/top_right_corner = 0
|
||||||
|
1:0/0 = 0
|
||||||
|
1:0/0/terrain_set = 0
|
||||||
|
1:0/0/terrain = 0
|
||||||
|
1:0/0/terrains_peering_bit/top_left_corner = 0
|
||||||
|
1:0/0/terrains_peering_bit/top_right_corner = 0
|
||||||
|
1:1/0 = 0
|
||||||
|
1:1/0/terrain_set = 0
|
||||||
|
2:1/0 = 0
|
||||||
|
2:1/0/terrain_set = 0
|
||||||
|
2:1/0/terrain = 0
|
||||||
|
2:1/0/terrains_peering_bit/bottom_right_corner = 0
|
||||||
|
2:1/0/terrains_peering_bit/top_right_corner = 0
|
||||||
|
2:2/0 = 0
|
||||||
|
2:2/0/terrain_set = 0
|
||||||
|
2:2/0/terrain = 0
|
||||||
|
2:2/0/terrains_peering_bit/bottom_right_corner = 0
|
||||||
|
2:2/0/terrains_peering_bit/bottom_left_corner = 0
|
||||||
|
2:2/0/terrains_peering_bit/top_right_corner = 0
|
||||||
|
1:2/0 = 0
|
||||||
|
1:2/0/terrain_set = 0
|
||||||
|
1:2/0/terrain = 0
|
||||||
|
1:2/0/terrains_peering_bit/bottom_right_corner = 0
|
||||||
|
1:2/0/terrains_peering_bit/bottom_left_corner = 0
|
||||||
|
0:1/0 = 0
|
||||||
|
0:1/0/terrain_set = 0
|
||||||
|
0:1/0/terrain = 0
|
||||||
|
0:1/0/terrains_peering_bit/bottom_left_corner = 0
|
||||||
|
0:1/0/terrains_peering_bit/top_left_corner = 0
|
||||||
|
0:2/0 = 0
|
||||||
|
0:2/0/terrain_set = 0
|
||||||
|
0:2/0/terrain = 0
|
||||||
|
0:2/0/terrains_peering_bit/bottom_right_corner = 0
|
||||||
|
0:2/0/terrains_peering_bit/bottom_left_corner = 0
|
||||||
|
0:2/0/terrains_peering_bit/top_left_corner = 0
|
||||||
|
2:0/0 = 0
|
||||||
|
2:0/0/terrain_set = 0
|
||||||
|
2:0/0/terrain = 0
|
||||||
|
2:0/0/terrains_peering_bit/bottom_right_corner = 0
|
||||||
|
2:0/0/terrains_peering_bit/top_left_corner = 0
|
||||||
|
2:0/0/terrains_peering_bit/top_right_corner = 0
|
||||||
|
3:0/0 = 0
|
||||||
|
3:0/0/terrain_set = 0
|
||||||
|
3:0/0/terrains_peering_bit/bottom_right_corner = 0
|
||||||
|
4:0/0 = 0
|
||||||
|
4:0/0/terrain_set = 0
|
||||||
|
4:0/0/terrains_peering_bit/bottom_left_corner = 0
|
||||||
|
4:1/0 = 0
|
||||||
|
4:1/0/terrain_set = 0
|
||||||
|
4:1/0/terrains_peering_bit/top_left_corner = 0
|
||||||
|
4:2/0 = 0
|
||||||
|
4:2/0/terrain_set = 0
|
||||||
|
4:2/0/terrains_peering_bit/bottom_right_corner = 0
|
||||||
|
4:2/0/terrains_peering_bit/top_left_corner = 0
|
||||||
|
3:2/0 = 0
|
||||||
|
3:2/0/terrain_set = 0
|
||||||
|
3:2/0/terrains_peering_bit/bottom_left_corner = 0
|
||||||
|
3:2/0/terrains_peering_bit/top_right_corner = 0
|
||||||
|
3:1/0 = 0
|
||||||
|
3:1/0/terrain_set = 0
|
||||||
|
3:1/0/terrains_peering_bit/top_right_corner = 0
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
tile_size = Vector2i(32, 32)
|
||||||
|
terrain_set_0/mode = 1
|
||||||
|
terrain_set_0/terrain_0/name = "office_walls"
|
||||||
|
terrain_set_0/terrain_0/color = Color(0.5, 0.34375, 0.25, 1)
|
||||||
|
sources/0 = SubResource("TileSetAtlasSource_t1ruc")
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
[gd_resource type="TileSet" format=3 uid="uid://cmycomep4ap8y"]
|
||||||
|
|
||||||
|
[ext_resource type="Texture2D" uid="uid://c55vbj78e0yrl" path="res://assets/sprites/tileset_office_walls.png" id="1_cyef1"]
|
||||||
|
|
||||||
|
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_t1ruc"]
|
||||||
|
texture = ExtResource("1_cyef1")
|
||||||
|
texture_region_size = Vector2i(32, 32)
|
||||||
|
0:0/0 = 0
|
||||||
|
0:0/0/terrain_set = 0
|
||||||
|
0:0/0/terrain = 0
|
||||||
|
0:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-16, -16, 16, -16, 16, -7.3285065, -7.965769, -7.0098763, -7.6471386, 16, -16, 16)
|
||||||
|
0:0/0/terrains_peering_bit/right_side = 0
|
||||||
|
0:0/0/terrains_peering_bit/bottom_right_corner = 0
|
||||||
|
0:0/0/terrains_peering_bit/bottom_side = 0
|
||||||
|
1:0/0 = 0
|
||||||
|
1:0/0/terrain_set = 0
|
||||||
|
1:0/0/terrain = 0
|
||||||
|
1:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(16, -16, 16, -7.3285065, -16, -7.6471386, -16, -16)
|
||||||
|
1:0/0/terrains_peering_bit/right_side = 0
|
||||||
|
1:0/0/terrains_peering_bit/bottom_right_corner = 0
|
||||||
|
1:0/0/terrains_peering_bit/bottom_side = 0
|
||||||
|
1:0/0/terrains_peering_bit/bottom_left_corner = 0
|
||||||
|
1:0/0/terrains_peering_bit/left_side = 0
|
||||||
|
2:1/0 = 0
|
||||||
|
2:1/0/terrain_set = 0
|
||||||
|
2:1/0/terrain = 0
|
||||||
|
2:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(16, 16, 7.3285065, 16, 7.6471386, -16, 16, -16)
|
||||||
|
2:1/0/terrains_peering_bit/bottom_side = 0
|
||||||
|
2:1/0/terrains_peering_bit/bottom_left_corner = 0
|
||||||
|
2:1/0/terrains_peering_bit/left_side = 0
|
||||||
|
2:1/0/terrains_peering_bit/top_left_corner = 0
|
||||||
|
2:1/0/terrains_peering_bit/top_side = 0
|
||||||
|
2:2/0 = 0
|
||||||
|
2:2/0/terrain_set = 0
|
||||||
|
2:2/0/terrain = 0
|
||||||
|
2:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(16, 16, -16, 16, -16, 7.3285065, 7.965769, 7.0098763, 7.6471386, -16, 16, -16)
|
||||||
|
2:2/0/terrains_peering_bit/left_side = 0
|
||||||
|
2:2/0/terrains_peering_bit/top_left_corner = 0
|
||||||
|
2:2/0/terrains_peering_bit/top_side = 0
|
||||||
|
1:2/0 = 0
|
||||||
|
1:2/0/terrain_set = 0
|
||||||
|
1:2/0/terrain = 0
|
||||||
|
1:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-16, 16, -16, 7.3285065, 16, 7.6471386, 16, 16)
|
||||||
|
1:2/0/terrains_peering_bit/right_side = 0
|
||||||
|
1:2/0/terrains_peering_bit/left_side = 0
|
||||||
|
1:2/0/terrains_peering_bit/top_left_corner = 0
|
||||||
|
1:2/0/terrains_peering_bit/top_side = 0
|
||||||
|
1:2/0/terrains_peering_bit/top_right_corner = 0
|
||||||
|
0:1/0 = 0
|
||||||
|
0:1/0/terrain_set = 0
|
||||||
|
0:1/0/terrain = 0
|
||||||
|
0:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-16, -16, -7.3285065, -16, -7.6471386, 16, -16, 16)
|
||||||
|
0:1/0/terrains_peering_bit/right_side = 0
|
||||||
|
0:1/0/terrains_peering_bit/bottom_right_corner = 0
|
||||||
|
0:1/0/terrains_peering_bit/bottom_side = 0
|
||||||
|
0:1/0/terrains_peering_bit/top_side = 0
|
||||||
|
0:1/0/terrains_peering_bit/top_right_corner = 0
|
||||||
|
0:2/0 = 0
|
||||||
|
0:2/0/terrain_set = 0
|
||||||
|
0:2/0/terrain = 0
|
||||||
|
0:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-16, 16, -16, -16, -7.3285065, -16, -7.0098763, 7.965769, 16, 7.6471386, 16, 16)
|
||||||
|
0:2/0/terrains_peering_bit/right_side = 0
|
||||||
|
0:2/0/terrains_peering_bit/top_side = 0
|
||||||
|
0:2/0/terrains_peering_bit/top_right_corner = 0
|
||||||
|
2:0/0 = 0
|
||||||
|
2:0/0/terrain_set = 0
|
||||||
|
2:0/0/terrain = 0
|
||||||
|
2:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(16, -16, 16, 16, 7.3285065, 16, 7.0098763, -7.965769, -16, -7.6471386, -16, -16)
|
||||||
|
2:0/0/terrains_peering_bit/bottom_side = 0
|
||||||
|
2:0/0/terrains_peering_bit/bottom_left_corner = 0
|
||||||
|
2:0/0/terrains_peering_bit/left_side = 0
|
||||||
|
3:0/0 = 0
|
||||||
|
3:0/0/terrain_set = 0
|
||||||
|
3:0/0/terrain = 0
|
||||||
|
3:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(16, 16, 6.654772, 16, 6.4749146, 7.014491, 16, 6.834633)
|
||||||
|
3:0/0/terrains_peering_bit/right_side = 0
|
||||||
|
3:0/0/terrains_peering_bit/bottom_side = 0
|
||||||
|
3:0/0/terrains_peering_bit/bottom_left_corner = 0
|
||||||
|
3:0/0/terrains_peering_bit/left_side = 0
|
||||||
|
3:0/0/terrains_peering_bit/top_left_corner = 0
|
||||||
|
3:0/0/terrains_peering_bit/top_side = 0
|
||||||
|
3:0/0/terrains_peering_bit/top_right_corner = 0
|
||||||
|
4:0/0 = 0
|
||||||
|
4:0/0/terrain_set = 0
|
||||||
|
4:0/0/terrain = 0
|
||||||
|
4:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-16, 16, -16, 6.654772, -7.014491, 6.4749146, -6.834633, 16)
|
||||||
|
4:0/0/terrains_peering_bit/right_side = 0
|
||||||
|
4:0/0/terrains_peering_bit/bottom_right_corner = 0
|
||||||
|
4:0/0/terrains_peering_bit/bottom_side = 0
|
||||||
|
4:0/0/terrains_peering_bit/left_side = 0
|
||||||
|
4:0/0/terrains_peering_bit/top_left_corner = 0
|
||||||
|
4:0/0/terrains_peering_bit/top_side = 0
|
||||||
|
4:0/0/terrains_peering_bit/top_right_corner = 0
|
||||||
|
4:1/0 = 0
|
||||||
|
4:1/0/terrain_set = 0
|
||||||
|
4:1/0/terrain = 0
|
||||||
|
4:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-16, -16, -6.654772, -16, -6.4749146, -7.014491, -16, -6.834633)
|
||||||
|
4:1/0/terrains_peering_bit/right_side = 0
|
||||||
|
4:1/0/terrains_peering_bit/bottom_right_corner = 0
|
||||||
|
4:1/0/terrains_peering_bit/bottom_side = 0
|
||||||
|
4:1/0/terrains_peering_bit/bottom_left_corner = 0
|
||||||
|
4:1/0/terrains_peering_bit/left_side = 0
|
||||||
|
4:1/0/terrains_peering_bit/top_side = 0
|
||||||
|
4:1/0/terrains_peering_bit/top_right_corner = 0
|
||||||
|
4:2/0 = 0
|
||||||
|
4:2/0/terrain_set = 0
|
||||||
|
4:2/0/terrain = 0
|
||||||
|
4:2/0/terrains_peering_bit/right_side = 0
|
||||||
|
4:2/0/terrains_peering_bit/bottom_right_corner = 0
|
||||||
|
4:2/0/terrains_peering_bit/bottom_side = 0
|
||||||
|
4:2/0/terrains_peering_bit/bottom_left_corner = 0
|
||||||
|
4:2/0/terrains_peering_bit/left_side = 0
|
||||||
|
4:2/0/terrains_peering_bit/top_left_corner = 0
|
||||||
|
4:2/0/terrains_peering_bit/top_side = 0
|
||||||
|
4:2/0/terrains_peering_bit/top_right_corner = 0
|
||||||
|
3:2/0 = 0
|
||||||
|
3:2/0/terrain_set = 0
|
||||||
|
3:2/0/terrain = 0
|
||||||
|
3:2/0/terrains_peering_bit/right_side = 0
|
||||||
|
3:2/0/terrains_peering_bit/bottom_right_corner = 0
|
||||||
|
3:2/0/terrains_peering_bit/bottom_side = 0
|
||||||
|
3:2/0/terrains_peering_bit/bottom_left_corner = 0
|
||||||
|
3:2/0/terrains_peering_bit/left_side = 0
|
||||||
|
3:2/0/terrains_peering_bit/top_left_corner = 0
|
||||||
|
3:2/0/terrains_peering_bit/top_side = 0
|
||||||
|
3:2/0/terrains_peering_bit/top_right_corner = 0
|
||||||
|
3:1/0 = 0
|
||||||
|
3:1/0/terrain_set = 0
|
||||||
|
3:1/0/terrain = 0
|
||||||
|
3:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(16, -16, 16, -6.654772, 7.014491, -6.4749146, 6.834633, -16)
|
||||||
|
3:1/0/terrains_peering_bit/right_side = 0
|
||||||
|
3:1/0/terrains_peering_bit/bottom_right_corner = 0
|
||||||
|
3:1/0/terrains_peering_bit/bottom_side = 0
|
||||||
|
3:1/0/terrains_peering_bit/bottom_left_corner = 0
|
||||||
|
3:1/0/terrains_peering_bit/left_side = 0
|
||||||
|
3:1/0/terrains_peering_bit/top_left_corner = 0
|
||||||
|
3:1/0/terrains_peering_bit/top_side = 0
|
||||||
|
1:1/next_alternative_id = 3
|
||||||
|
1:1/0 = 0
|
||||||
|
1:1/0/terrain_set = 0
|
||||||
|
1:1/1 = 1
|
||||||
|
1:1/1/transpose = true
|
||||||
|
1:1/1/terrain_set = 0
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
tile_size = Vector2i(32, 32)
|
||||||
|
physics_layer_0/collision_layer = 1
|
||||||
|
terrain_set_0/mode = 0
|
||||||
|
terrain_set_0/terrain_0/name = "office_walls"
|
||||||
|
terrain_set_0/terrain_0/color = Color(0.7063632, 0, 0.35112187, 1)
|
||||||
|
sources/0 = SubResource("TileSetAtlasSource_t1ruc")
|
||||||
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
Thank you for purchasing my assets! You can find others or games on my itch.io:
|
||||||
|
|
||||||
|
https://deakcor.itch.io/
|
||||||
|
|
||||||
|
If you want to contact me or follow my works, you can by following my twitter!
|
||||||
|
|
||||||
|
https://twitter.com/deakcor
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
## LevelManager.gd
|
||||||
|
# Manages the current level's data, visual representation, and navigation graph.
|
||||||
|
# Responsible for generating, painting, and initializing the level, as well as providing pathfinding and coordinate helpers.
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
# The source of truth for the current level's layout (MapData resource).
|
||||||
|
var _data: MapData
|
||||||
|
# The TileMapLayer node where the map is visually rendered.
|
||||||
|
var _layer: TileMapLayer
|
||||||
|
|
||||||
|
# Service objects for map generation, painting, and pathfinding.
|
||||||
|
# MapGenerator and MapPainter are stateless; PathfindingHandler manages the navigation graph.
|
||||||
|
var _generator: MapGenerator = MapGenerator.new()
|
||||||
|
var _painter: MapPainter = MapPainter.new()
|
||||||
|
var _pathfinding: PathfindingHandler = PathfindingHandler.new()
|
||||||
|
|
||||||
|
# Orchestrates the creation of a new level.
|
||||||
|
# 1. Generates the map data.
|
||||||
|
# 2. Paints the map visually.
|
||||||
|
# 3. Builds the navigation graph for AI/pathfinding.
|
||||||
|
# 4. Spawns the player at the correct location.
|
||||||
|
func initialize(settings: MapSettings, layer: TileMapLayer, player: CharacterBody2D) -> void:
|
||||||
|
_layer = layer
|
||||||
|
|
||||||
|
# 1. Generate the abstract map data (rooms, corridors, etc.).
|
||||||
|
# This avoids side effects on the settings resource.
|
||||||
|
_data = _generator.generate(settings)
|
||||||
|
|
||||||
|
# 2. Paint the visual layer for the player to see.
|
||||||
|
# This does not modify the underlying map data.
|
||||||
|
_painter.paint(_data, _layer)
|
||||||
|
|
||||||
|
# 3. Build the AStar graph for AI and navigation.
|
||||||
|
# Reads directly from the grid stored in _data.
|
||||||
|
_pathfinding.build(_data)
|
||||||
|
|
||||||
|
# 4. Finalize world state by spawning the player at the spawn point.
|
||||||
|
# Uses the TileMapLayer to convert grid coordinates to world pixel positions.
|
||||||
|
player.position = _layer.map_to_local(_data.spawn_point)
|
||||||
|
|
||||||
|
# Public API for other systems (like AI) to query the navigation grid.
|
||||||
|
# Encapsulates the PathfindingHandler so callers don't need to know about it directly.
|
||||||
|
func find_path(from: Vector2i, to: Vector2i) -> Array[Vector2i]:
|
||||||
|
return _pathfinding.find_path(from, to)
|
||||||
|
|
||||||
|
# Coordinate conversion helpers — lets the player and other systems convert between world and tile coordinates without touching the TileMapLayer directly.
|
||||||
|
func world_to_tile(world_pos: Vector2) -> Vector2i:
|
||||||
|
return _layer.local_to_map(world_pos)
|
||||||
|
|
||||||
|
func tile_to_world(tile: Vector2i) -> Vector2:
|
||||||
|
return _layer.map_to_local(tile)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
## TurnManager.gd
|
||||||
|
# Autoload singleton for managing turn order in a turn-based game.
|
||||||
|
# Handles registration, sequencing, and signaling for all turn-taking entities.
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
# List of entities that participate in the turn order (e.g., Player, Enemies).
|
||||||
|
var turn_entities: Array[Node] = []
|
||||||
|
# Index of the entity whose turn is currently active.
|
||||||
|
var current_index: int = 0
|
||||||
|
# Whether the turn system is currently running.
|
||||||
|
var active: bool = false
|
||||||
|
|
||||||
|
# Registers an entity to participate in the turn order.
|
||||||
|
# Ensures no duplicates.
|
||||||
|
func register_entity(entity: Node) -> void:
|
||||||
|
if entity not in turn_entities:
|
||||||
|
turn_entities.append(entity)
|
||||||
|
|
||||||
|
# Unregisters an entity (e.g., when it is removed or defeated).
|
||||||
|
func unregister_entity(entity: Node) -> void:
|
||||||
|
if entity in turn_entities:
|
||||||
|
turn_entities.erase(entity)
|
||||||
|
|
||||||
|
# Starts the turn sequence for all registered entities.
|
||||||
|
# Resets the index and begins the first turn.
|
||||||
|
func start_turns() -> void:
|
||||||
|
if turn_entities.size() == 0:
|
||||||
|
return
|
||||||
|
active = true
|
||||||
|
current_index = 0
|
||||||
|
_next_turn()
|
||||||
|
|
||||||
|
# Advances to the next entity's turn.
|
||||||
|
# Connects to the entity's turn_finished signal and calls its take_turn() method.
|
||||||
|
func _next_turn() -> void:
|
||||||
|
if not active or turn_entities.size() == 0:
|
||||||
|
return
|
||||||
|
if current_index >= turn_entities.size():
|
||||||
|
current_index = 0 # New round
|
||||||
|
# Get the next entity in the list.
|
||||||
|
var entity: Node = turn_entities[current_index]
|
||||||
|
# Connect to the entity's turn_finished signal if not already connected.
|
||||||
|
if not entity.is_connected("turn_finished", Callable(self, "_on_entity_turn_finished")):
|
||||||
|
entity.connect("turn_finished", Callable(self, "_on_entity_turn_finished"))
|
||||||
|
# Notify the entity to take its turn.
|
||||||
|
entity.take_turn()
|
||||||
|
|
||||||
|
# Called when an entity finishes its turn (emits turn_finished).
|
||||||
|
# Disconnects the signal and advances to the next entity.
|
||||||
|
func _on_entity_turn_finished() -> void:
|
||||||
|
# Disconnect to avoid duplicate connections.
|
||||||
|
var entity: Node = turn_entities[current_index]
|
||||||
|
if entity.is_connected("turn_finished", Callable(self, "_on_entity_turn_finished")):
|
||||||
|
entity.disconnect("turn_finished", Callable(self, "_on_entity_turn_finished"))
|
||||||
|
current_index += 1
|
||||||
|
_next_turn()
|
||||||
|
|
||||||
|
# Optionally, stops the turn sequence and resets state.
|
||||||
|
func stop_turns() -> void:
|
||||||
|
active = false
|
||||||
|
current_index = 0
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
## MapData.gd
|
||||||
|
# Resource class that stores all data for a generated map, including tile types, rooms, and spawn points.
|
||||||
|
class_name MapData
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
# Enum for all possible tile types in the map grid.
|
||||||
|
# Used to distinguish between empty space, walkable floor, walls, doors, and stairs.
|
||||||
|
enum TileType {
|
||||||
|
VOID, # Unused/empty space
|
||||||
|
FLOOR, # Walkable floor tile
|
||||||
|
WALL, # Solid wall tile
|
||||||
|
DOOR, # Door tile (can be opened/closed)
|
||||||
|
STAIRS # Stairs tile (for level transitions)
|
||||||
|
}
|
||||||
|
|
||||||
|
# The main grid: Dictionary mapping Vector2i coordinates to TileType values.
|
||||||
|
# This is the authoritative source for the map's layout.
|
||||||
|
@export var grid: Dictionary = {}
|
||||||
|
|
||||||
|
# The number of rooms generated for this map (set during generation).
|
||||||
|
@export var room_count: int = 0
|
||||||
|
|
||||||
|
# List of all room rectangles (Rect2i) in the map, used for spawning and logic.
|
||||||
|
@export var rooms: Array[Rect2i] = []
|
||||||
|
|
||||||
|
# The starting position for the player (in grid coordinates).
|
||||||
|
@export var spawn_point: Vector2i = Vector2i.ZERO
|
||||||
|
|
||||||
|
# Returns true if the tile at 'coords' is of the given TileType.
|
||||||
|
# Used for pathfinding and logic checks.
|
||||||
|
func is_tile_type(coords: Vector2i, type: TileType) -> bool:
|
||||||
|
return grid.get(coords, TileType.VOID) == type
|
||||||
|
|
||||||
|
# Clears all map data so this resource can be reused for a new map.
|
||||||
|
func clear() -> void:
|
||||||
|
grid.clear()
|
||||||
|
rooms.clear()
|
||||||
|
room_count = 0
|
||||||
|
spawn_point = Vector2i.ZERO
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
## MapGenerator.gd
|
||||||
|
# Responsible for procedural generation of dungeon maps, including rooms, corridors, and walls.
|
||||||
|
class_name MapGenerator
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
# Main entry point: generates a new MapData resource based on the provided settings.
|
||||||
|
# Handles room placement, corridor carving, wall painting, and spawn point selection.
|
||||||
|
func generate(settings: MapSettings) -> MapData:
|
||||||
|
var map_data: MapData = MapData.new()
|
||||||
|
# Randomly choose how many rooms to generate within the allowed range.
|
||||||
|
map_data.room_count = randi_range(settings.min_room_count, settings.max_room_count)
|
||||||
|
|
||||||
|
_generate_rooms(map_data, settings)
|
||||||
|
_generate_corridors(map_data, settings)
|
||||||
|
_generate_walls(map_data)
|
||||||
|
_pick_spawn_point(map_data)
|
||||||
|
|
||||||
|
return map_data
|
||||||
|
|
||||||
|
# Places non-overlapping rectangular rooms in the map grid.
|
||||||
|
# Each room is checked for overlap and only added if it fits.
|
||||||
|
func _generate_rooms(data: MapData, settings: MapSettings) -> void:
|
||||||
|
for _room_idx in range(data.room_count):
|
||||||
|
for _try in range(settings.max_tries):
|
||||||
|
var w: int = randi_range(settings.min_room_size, settings.max_room_size)
|
||||||
|
var h: int = randi_range(settings.min_room_size, settings.max_room_size)
|
||||||
|
var x: int = randi_range(1, settings.width - w - 1)
|
||||||
|
var y: int = randi_range(1, settings.height - h - 1)
|
||||||
|
|
||||||
|
var new_room: Rect2i = Rect2i(x, y, w, h)
|
||||||
|
|
||||||
|
var overlaps: bool = false
|
||||||
|
for existing_room in data.rooms:
|
||||||
|
if not (new_room.position.x + new_room.size.x + settings.room_padding <= existing_room.position.x or
|
||||||
|
new_room.position.x >= existing_room.position.x + existing_room.size.x + settings.room_padding or
|
||||||
|
new_room.position.y + new_room.size.y + settings.room_padding <= existing_room.position.y or
|
||||||
|
new_room.position.y >= existing_room.position.y + existing_room.size.y + settings.room_padding):
|
||||||
|
overlaps = true
|
||||||
|
break
|
||||||
|
|
||||||
|
if not overlaps:
|
||||||
|
data.rooms.append(new_room)
|
||||||
|
for rx in range(new_room.position.x, new_room.end.x):
|
||||||
|
for ry in range(new_room.position.y, new_room.end.y):
|
||||||
|
data.grid[Vector2i(rx, ry)] = MapData.TileType.FLOOR
|
||||||
|
break
|
||||||
|
|
||||||
|
# Connects all rooms with corridors using Manhattan paths (L-shaped).
|
||||||
|
# Also adds a few extra loops for map variety.
|
||||||
|
func _generate_corridors(data: MapData, _settings: MapSettings) -> void:
|
||||||
|
var room_centers: Array[Vector2i] = []
|
||||||
|
for room in data.rooms:
|
||||||
|
room_centers.append(room.get_center())
|
||||||
|
|
||||||
|
if room_centers.is_empty(): return
|
||||||
|
|
||||||
|
var connected: Array[Vector2i] = []
|
||||||
|
var unconnected: Array[Vector2i] = room_centers.duplicate()
|
||||||
|
var potential_loops: Array[Array] = []
|
||||||
|
|
||||||
|
connected.append(unconnected.pop_front())
|
||||||
|
|
||||||
|
while unconnected.size() > 0:
|
||||||
|
var best_distance: int = 999999
|
||||||
|
var best_pair: Array[Vector2i] = [Vector2i.ZERO, Vector2i.ZERO]
|
||||||
|
var best_un_idx: int = -1
|
||||||
|
|
||||||
|
for i in range(connected.size()):
|
||||||
|
for j in range(unconnected.size()):
|
||||||
|
var p1: Vector2i = connected[i]
|
||||||
|
var p2: Vector2i = unconnected[j]
|
||||||
|
var dist: int = abs(p1.x - p2.x) + abs(p1.y - p2.y)
|
||||||
|
if dist < best_distance:
|
||||||
|
best_distance = dist
|
||||||
|
best_pair = [p1, p2]
|
||||||
|
best_un_idx = j
|
||||||
|
else:
|
||||||
|
potential_loops.append([p1, p2])
|
||||||
|
|
||||||
|
_carve_manhattan_corridor(data, best_pair[0], best_pair[1])
|
||||||
|
connected.append(unconnected[best_un_idx])
|
||||||
|
unconnected.remove_at(best_un_idx)
|
||||||
|
|
||||||
|
# Add extra loops (~15% of room count) for more interesting layouts
|
||||||
|
potential_loops.shuffle()
|
||||||
|
var loop_count: int = int(data.rooms.size() * 0.15)
|
||||||
|
for i in range(min(loop_count, potential_loops.size())):
|
||||||
|
_carve_manhattan_corridor(data, potential_loops[i][0], potential_loops[i][1])
|
||||||
|
|
||||||
|
# Helper to carve a corridor between two points using Manhattan (L-shaped) path.
|
||||||
|
# Chooses the best valid path and writes it to the grid.
|
||||||
|
func _carve_manhattan_corridor(data: MapData, start: Vector2i, end: Vector2i) -> void:
|
||||||
|
var path_hv: Array[Vector2i] = _get_manhattan_points(start, end, false)
|
||||||
|
var path_vh: Array[Vector2i] = _get_manhattan_points(start, end, true)
|
||||||
|
|
||||||
|
# Determine which room indices we are connecting for validation
|
||||||
|
var ri_a: int = -1
|
||||||
|
var ri_b: int = -1
|
||||||
|
for i in range(data.rooms.size()):
|
||||||
|
if data.rooms[i].has_point(start): ri_a = i
|
||||||
|
if data.rooms[i].has_point(end): ri_b = i
|
||||||
|
|
||||||
|
var chosen: Array[Vector2i] = path_hv
|
||||||
|
if _is_corridor_valid(data, path_hv, ri_a, ri_b):
|
||||||
|
chosen = path_hv
|
||||||
|
elif _is_corridor_valid(data, path_vh, ri_a, ri_b):
|
||||||
|
chosen = path_vh
|
||||||
|
|
||||||
|
for pt in chosen:
|
||||||
|
data.grid[pt] = MapData.TileType.FLOOR
|
||||||
|
|
||||||
|
# Returns an array of points representing a Manhattan (L-shaped) path between two points.
|
||||||
|
# v_first determines whether to go vertical then horizontal, or vice versa.
|
||||||
|
func _get_manhattan_points(start: Vector2i, end: Vector2i, v_first: bool) -> Array[Vector2i]:
|
||||||
|
var pts: Array[Vector2i] = []
|
||||||
|
var x_step: int = sign(end.x - start.x) if end.x != start.x else 1
|
||||||
|
var y_step: int = sign(end.y - start.y) if end.y != start.y else 1
|
||||||
|
|
||||||
|
if v_first:
|
||||||
|
for y in range(start.y, end.y + y_step, y_step):
|
||||||
|
pts.append(Vector2i(start.x, y))
|
||||||
|
for x in range(start.x + x_step, end.x + x_step, x_step):
|
||||||
|
pts.append(Vector2i(x, end.y))
|
||||||
|
else:
|
||||||
|
for x in range(start.x, end.x + x_step, x_step):
|
||||||
|
pts.append(Vector2i(x, start.y))
|
||||||
|
for y in range(start.y + y_step, end.y + y_step, y_step):
|
||||||
|
pts.append(Vector2i(end.x, y))
|
||||||
|
return pts
|
||||||
|
|
||||||
|
# Returns true if the corridor path does not violate the 2-tile gap rule between rooms.
|
||||||
|
# Ensures corridors do not pass too close to other rooms.
|
||||||
|
func _is_corridor_valid(data: MapData, points: Array[Vector2i], ri_a: int, ri_b: int) -> bool:
|
||||||
|
for pt in points:
|
||||||
|
var inside_any_room: bool = false
|
||||||
|
for r in data.rooms:
|
||||||
|
if r.has_point(pt):
|
||||||
|
inside_any_room = true
|
||||||
|
break
|
||||||
|
if inside_any_room: continue
|
||||||
|
|
||||||
|
# Check gap from other rooms
|
||||||
|
for i in range(data.rooms.size()):
|
||||||
|
if i == ri_a or i == ri_b: continue
|
||||||
|
if data.rooms[i].grow(2).has_point(pt):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
# Paints wall tiles around all floor tiles in the grid.
|
||||||
|
# Any empty neighbor of a floor tile becomes a wall.
|
||||||
|
func _generate_walls(data: MapData) -> void:
|
||||||
|
for tile: Vector2i in data.grid.keys():
|
||||||
|
if data.grid[tile] == MapData.TileType.FLOOR:
|
||||||
|
for dx in range(-1, 2):
|
||||||
|
for dy in range(-1, 2):
|
||||||
|
var neighbor: Vector2i = tile + Vector2i(dx, dy)
|
||||||
|
if not data.grid.has(neighbor):
|
||||||
|
data.grid[neighbor] = MapData.TileType.WALL
|
||||||
|
|
||||||
|
# Picks a spawn point for the player (center of the first room).
|
||||||
|
func _pick_spawn_point(data: MapData) -> void:
|
||||||
|
if not data.rooms.is_empty():
|
||||||
|
data.spawn_point = data.rooms[0].get_center()
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
## MapPainter.gd
|
||||||
|
# Responsible for painting the generated map data onto a TileMapLayer in the scene.
|
||||||
|
# Handles both floor and wall tiles, using atlas coordinates for floor variety and terrain peering for walls.
|
||||||
|
class_name MapPainter
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
# The source ID for the floor tile in the TileSet atlas.
|
||||||
|
const FLOOR_SOURCE_ID: int = 0
|
||||||
|
# Two different atlas coordinates for floor tiles, used to create a checkerboard effect for visual variety.
|
||||||
|
const FLOOR_ATLAS_COORDS_1: Vector2i = Vector2i(3, 2)
|
||||||
|
const FLOOR_ATLAS_COORDS_2: Vector2i = Vector2i(4, 2)
|
||||||
|
|
||||||
|
# Terrain set and terrain index for wall tiles, used for Godot's terrain peering system.
|
||||||
|
const WALL_TERRAIN_SET: int = 0
|
||||||
|
const WALL_TERRAIN: int = 0
|
||||||
|
|
||||||
|
# Paints all tiles from the given MapData onto the provided TileMapLayer.
|
||||||
|
# Floors are painted with alternating atlas tiles for variety; walls are collected and painted with terrain peering.
|
||||||
|
func paint(data: MapData, layer: TileMapLayer) -> void:
|
||||||
|
layer.clear() # Remove any existing tiles from the layer.
|
||||||
|
var wall_cells: Array[Vector2i] = [] # Collect wall tile positions for batch painting.
|
||||||
|
|
||||||
|
for coords: Vector2i in data.grid.keys():
|
||||||
|
match data.grid[coords]:
|
||||||
|
MapData.TileType.FLOOR:
|
||||||
|
# Alternate between two floor atlas tiles for a checkerboard effect.
|
||||||
|
var use_first: bool = ((coords.x + coords.y) % 2) == 0
|
||||||
|
var atlas_coords: Vector2i = FLOOR_ATLAS_COORDS_1 if use_first else FLOOR_ATLAS_COORDS_2
|
||||||
|
layer.set_cell(coords, FLOOR_SOURCE_ID, atlas_coords)
|
||||||
|
MapData.TileType.WALL:
|
||||||
|
# Collect wall positions for terrain peering.
|
||||||
|
wall_cells.append(coords)
|
||||||
|
|
||||||
|
# Use Godot's terrain peering to auto-connect wall tiles for seamless edges/corners.
|
||||||
|
layer.set_cells_terrain_connect(wall_cells, WALL_TERRAIN_SET, WALL_TERRAIN, false)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
## MapSettings.gd
|
||||||
|
# Resource for storing all configurable map generation settings.
|
||||||
|
# Used by MapGenerator to control map size, room count, and room properties.
|
||||||
|
class_name MapSettings
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
# Level size in tiles (width × height).
|
||||||
|
@export var width: int = 50
|
||||||
|
@export var height: int = 50
|
||||||
|
# Minimum and maximum number of rooms to generate.
|
||||||
|
@export var min_room_count: int = 5
|
||||||
|
@export var max_room_count: int = 15
|
||||||
|
|
||||||
|
# Room size constraints (in tiles).
|
||||||
|
@export var min_room_size: int = 4
|
||||||
|
@export var max_room_size: int = 10
|
||||||
|
# Minimum number of tiles between rooms (prevents overlap).
|
||||||
|
@export var room_padding:int = 5
|
||||||
|
|
||||||
|
# Maximum number of attempts to place a room before giving up.
|
||||||
|
@export var max_tries: int = 20
|
||||||
|
|
||||||
|
# The actual number of rooms generated (set by MapGenerator).
|
||||||
|
var room_count: int
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
## PathfindingHandler.gd
|
||||||
|
# Handles A* pathfinding for the map using Godot's AStar2D.
|
||||||
|
# Builds a navigation graph from walkable tiles and provides path queries.
|
||||||
|
class_name PathfindingHandler
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
# All 8 possible neighbor offsets (cardinal and diagonal directions).
|
||||||
|
const NEIGHBORS: Array[Vector2i] = [
|
||||||
|
Vector2i.RIGHT, Vector2i.LEFT, Vector2i.DOWN, Vector2i.UP,
|
||||||
|
Vector2i(1, 1), Vector2i(1, -1), Vector2i(-1, 1), Vector2i(-1, -1),
|
||||||
|
]
|
||||||
|
|
||||||
|
# The underlying AStar2D instance for pathfinding.
|
||||||
|
var _astar: AStar2D = AStar2D.new()
|
||||||
|
# Maps grid coordinates (Vector2i) to AStar2D point IDs.
|
||||||
|
var _coord_to_id: Dictionary = {}
|
||||||
|
|
||||||
|
# Builds the AStar2D graph from all walkable tiles in MapData.
|
||||||
|
# Safe to call again for a new level — clears all previous state first.
|
||||||
|
func build(data: MapData) -> void:
|
||||||
|
_astar.clear()
|
||||||
|
_coord_to_id.clear()
|
||||||
|
|
||||||
|
# Register a point for every walkable tile (FLOOR).
|
||||||
|
for coords: Vector2i in data.grid.keys():
|
||||||
|
if data.grid[coords] == MapData.TileType.FLOOR:
|
||||||
|
var id: int = _astar.get_available_point_id()
|
||||||
|
_astar.add_point(id, Vector2(coords.x, coords.y))
|
||||||
|
_coord_to_id[coords] = id
|
||||||
|
|
||||||
|
# Connect each point to its walkable neighbors (cardinal and diagonal).
|
||||||
|
for coords: Vector2i in _coord_to_id.keys():
|
||||||
|
for offset: Vector2i in NEIGHBORS:
|
||||||
|
var neighbor: Vector2i = coords + offset
|
||||||
|
if _coord_to_id.has(neighbor):
|
||||||
|
_astar.connect_points(_coord_to_id[coords], _coord_to_id[neighbor], false)
|
||||||
|
|
||||||
|
# Returns the path as grid coordinates, or an empty array if no path exists.
|
||||||
|
# Used by LevelManager and Player for navigation.
|
||||||
|
func find_path(from: Vector2i, to: Vector2i) -> Array[Vector2i]:
|
||||||
|
if not _coord_to_id.has(from) or not _coord_to_id.has(to):
|
||||||
|
return []
|
||||||
|
|
||||||
|
var raw: PackedVector2Array = _astar.get_point_path(_coord_to_id[from], _coord_to_id[to])
|
||||||
|
var result: Array[Vector2i] = []
|
||||||
|
for point in raw:
|
||||||
|
result.append(Vector2i(point))
|
||||||
|
return result
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
## Room.gd
|
||||||
|
# Resource representing a single room in the map.
|
||||||
|
# Stores the room's position and size in grid coordinates.
|
||||||
|
class_name Room
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
# Top-left position of the room in grid coordinates.
|
||||||
|
var room_pos: Vector2i
|
||||||
|
# Size of the room (width, height) in tiles.
|
||||||
|
var room_size: Vector2i
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
## Tile.gd
|
||||||
|
# Resource representing a single tile in the map.
|
||||||
|
# Used for storing tile properties such as walkability.
|
||||||
|
class_name Tile
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
# Whether this tile can be walked on (used for pathfinding).
|
||||||
|
var is_walkable: bool = false
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
; Engine configuration file.
|
||||||
|
; It's best edited using the editor UI and not directly,
|
||||||
|
; since the parameters that go here are not all obvious.
|
||||||
|
;
|
||||||
|
; Format:
|
||||||
|
; [section] ; section goes between []
|
||||||
|
; param=value ; assign values to parameters
|
||||||
|
|
||||||
|
config_version=5
|
||||||
|
|
||||||
|
[application]
|
||||||
|
|
||||||
|
config/name="Shadow Pixel Run"
|
||||||
|
run/main_scene="uid://cnb0ykbiglgo6"
|
||||||
|
config/features=PackedStringArray("4.6", "Forward Plus")
|
||||||
|
config/icon="res://assets/sprites/icon.svg"
|
||||||
|
|
||||||
|
[autoload]
|
||||||
|
|
||||||
|
LevelManager="*uid://c2kvqdy85klps"
|
||||||
|
TurnManager="*uid://qaa5cunrehr0"
|
||||||
|
|
||||||
|
[debug]
|
||||||
|
|
||||||
|
gdscript/warnings/untyped_declaration=1
|
||||||
|
|
||||||
|
[display]
|
||||||
|
|
||||||
|
window/size/viewport_width=640
|
||||||
|
window/size/viewport_height=320
|
||||||
|
|
||||||
|
[editor]
|
||||||
|
|
||||||
|
version_control/plugin_name="GitPlugin"
|
||||||
|
version_control/autoload_on_startup=true
|
||||||
|
|
||||||
|
[input]
|
||||||
|
|
||||||
|
move_left={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
move_right={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
move_up={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
move_down={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
[physics]
|
||||||
|
|
||||||
|
3d/physics_engine="Jolt Physics"
|
||||||
|
|
||||||
|
[rendering]
|
||||||
|
|
||||||
|
textures/canvas_textures/default_texture_filter=0
|
||||||
|
rendering_device/driver.windows="d3d12"
|
||||||
|
renderer/rendering_method="gl_compatibility"
|
||||||
|
renderer/rendering_method.mobile="gl_compatibility"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[gd_scene format=3 uid="uid://hwbs3q16spqx"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://dh5hx2lospkug" path="res://scripts/actors/character.gd" id="1_4i4j6"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://b7s06o6gm42rn" path="res://assets/sprites/icon.svg" id="2_qkj82"]
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_35asm"]
|
||||||
|
size = Vector2(32, 32)
|
||||||
|
|
||||||
|
[node name="Enemy" type="CharacterBody2D" unique_id=1059490557]
|
||||||
|
script = ExtResource("1_4i4j6")
|
||||||
|
metadata/_custom_type_script = "uid://dh5hx2lospkug"
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=569576189]
|
||||||
|
self_modulate = Color(1, 0, 0, 1)
|
||||||
|
scale = Vector2(0.25, 0.25)
|
||||||
|
texture = ExtResource("2_qkj82")
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=328269646]
|
||||||
|
shape = SubResource("RectangleShape2D_35asm")
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[gd_scene format=3 uid="uid://1ls2pjitfl5m"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://ciusoegxdh344" path="res://scripts/actors/player.gd" id="1_ru62r"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://b7s06o6gm42rn" path="res://assets/sprites/icon.svg" id="2_ktjr4"]
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_qfnet"]
|
||||||
|
size = Vector2(32, 32)
|
||||||
|
|
||||||
|
[node name="Player" type="CharacterBody2D" unique_id=179780959]
|
||||||
|
script = ExtResource("1_ru62r")
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=29528982]
|
||||||
|
scale = Vector2(0.25, 0.25)
|
||||||
|
texture = ExtResource("2_ktjr4")
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=272748828]
|
||||||
|
shape = SubResource("RectangleShape2D_qfnet")
|
||||||
|
|
||||||
|
[node name="Camera2D" type="Camera2D" parent="." unique_id=2033102226]
|
||||||
|
position_smoothing_enabled = true
|
||||||
|
drag_horizontal_enabled = true
|
||||||
|
drag_vertical_enabled = true
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[gd_scene format=3 uid="uid://cnb0ykbiglgo6"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://6xvdnyxsb2uq" path="res://scripts/world.gd" id="1_71j4m"]
|
||||||
|
[ext_resource type="TileSet" uid="uid://cmycomep4ap8y" path="res://assets/tilesets/office_02.tres" id="4_qfnet"]
|
||||||
|
|
||||||
|
[node name="World" type="Node2D" unique_id=1504396068]
|
||||||
|
script = ExtResource("1_71j4m")
|
||||||
|
|
||||||
|
[node name="MapLayer" type="TileMapLayer" parent="." unique_id=2055425416]
|
||||||
|
tile_set = ExtResource("4_qfnet")
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
## Character.gd
|
||||||
|
# Base class for all turn-based characters (player, enemies, etc.)
|
||||||
|
# Handles registration with TurnManager and provides a turn interface.
|
||||||
|
class_name Character
|
||||||
|
extends CharacterBody2D
|
||||||
|
|
||||||
|
# Signal emitted when this character's turn is finished.
|
||||||
|
signal turn_finished
|
||||||
|
|
||||||
|
# Called by TurnManager to let this character take its turn.
|
||||||
|
# Subclasses should override this to implement their own turn logic.
|
||||||
|
func take_turn() -> void:
|
||||||
|
# Virtual method to be overridden by subclasses
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Called when the node enters the scene tree.
|
||||||
|
# Registers this character with the TurnManager for turn order.
|
||||||
|
func _ready() -> void:
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
return
|
||||||
|
TurnManager.register_entity(self)
|
||||||
|
|
||||||
|
# Call this to remove the character from the turn system and free it from the scene.
|
||||||
|
func die() -> void:
|
||||||
|
TurnManager.unregister_entity(self)
|
||||||
|
queue_free()
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
## player.gd
|
||||||
|
# Player character script. Handles input, pathfinding, and movement.
|
||||||
|
class_name Player
|
||||||
|
extends Character
|
||||||
|
|
||||||
|
# Movement speed in pixels per second.
|
||||||
|
const SPEED: float = 250.0
|
||||||
|
# Distance threshold for snapping to a tile (prevents jitter).
|
||||||
|
const ARRIVAL_THRESHOLD: float = 4.0
|
||||||
|
|
||||||
|
|
||||||
|
# The current path (as a list of tile coordinates) the player is following.
|
||||||
|
var _path: Array[Vector2i] = []
|
||||||
|
|
||||||
|
|
||||||
|
# Handles mouse input for movement. Calculates a path to the clicked tile.
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||||||
|
var clicked_tile: Vector2i = LevelManager.world_to_tile(get_global_mouse_position())
|
||||||
|
var current_tile: Vector2i = LevelManager.world_to_tile(position)
|
||||||
|
_path = LevelManager.find_path(current_tile, clicked_tile)
|
||||||
|
# Drop the first entry — it's the tile the player is already on.
|
||||||
|
if not _path.is_empty():
|
||||||
|
_path.remove_at(0)
|
||||||
|
|
||||||
|
# Moves the player along the path, one tile at a time, using velocity.
|
||||||
|
func _physics_process(_delta: float) -> void:
|
||||||
|
if _path.is_empty():
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
move_and_slide()
|
||||||
|
return
|
||||||
|
|
||||||
|
var target_world: Vector2 = LevelManager.tile_to_world(_path[0])
|
||||||
|
var to_target: Vector2 = target_world - position
|
||||||
|
|
||||||
|
if to_target.length() < ARRIVAL_THRESHOLD:
|
||||||
|
position = target_world
|
||||||
|
_path.remove_at(0)
|
||||||
|
else:
|
||||||
|
velocity = to_target.normalized() * SPEED
|
||||||
|
move_and_slide()
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
## world.gd
|
||||||
|
# Main world node script. Sets up the map, player, and level on scene ready.
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
# Reference to the TileMapLayer node for rendering the map.
|
||||||
|
|
||||||
|
# Reference to the TileMapLayer node for rendering the map.
|
||||||
|
var map_layer: TileMapLayer
|
||||||
|
# Path to the Player scene to instance.
|
||||||
|
const PLAYER_SCENE_PATH: String = "res://scenes/actors/player.tscn"
|
||||||
|
# Reference to the Player node.
|
||||||
|
var player: Player
|
||||||
|
# Map generation settings (size, room count, etc.).
|
||||||
|
var settings: MapSettings = MapSettings.new()
|
||||||
|
|
||||||
|
# Called when the node is added to the scene tree.
|
||||||
|
# Initializes references and triggers map setup.
|
||||||
|
func _ready() -> void:
|
||||||
|
map_layer = $MapLayer
|
||||||
|
# Do not assume Player is already in the scene tree; instance it after map generation.
|
||||||
|
call_deferred("_initialize_map")
|
||||||
|
|
||||||
|
# Initializes the map and level using LevelManager.
|
||||||
|
# Ensures the map is generated and the player is placed.
|
||||||
|
func _initialize_map() -> void:
|
||||||
|
# Instance the Player after the map is generated and add to the scene tree.
|
||||||
|
var player_scene: PackedScene = load(PLAYER_SCENE_PATH)
|
||||||
|
player = player_scene.instantiate() as Player
|
||||||
|
add_child(player)
|
||||||
|
LevelManager.initialize(settings, map_layer, player)
|
||||||