init
This commit is contained in:
commit
e45f121fb9
89 changed files with 336069 additions and 0 deletions
47
.gitignore
vendored
Normal file
47
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Compiled genesis binary
|
||||||
|
/bin/genesis
|
||||||
|
|
||||||
|
# C build objects
|
||||||
|
/obj/
|
||||||
|
|
||||||
|
# Godot GDExtension build artifacts
|
||||||
|
/godot/build/
|
||||||
|
/demo/addons/brogue_gen/
|
||||||
|
|
||||||
|
# godot-cpp build artifacts (keep sources, drop generated/compiled)
|
||||||
|
/godot/godot-cpp/bin/
|
||||||
|
/godot/godot-cpp/gen/
|
||||||
|
|
||||||
|
# Python cache
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
# Object / library files
|
||||||
|
*.o
|
||||||
|
*.os
|
||||||
|
*.obj
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
|
||||||
|
# Godot editor artifacts
|
||||||
|
.godot/
|
||||||
|
*.import
|
||||||
|
|
||||||
|
# SCons
|
||||||
|
.sconsign.dblite
|
||||||
|
.scons_node_count
|
||||||
|
config.log
|
||||||
|
|
||||||
|
# Editors / IDEs
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
42
Makefile
Normal file
42
Makefile
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
CC := cc
|
||||||
|
CFLAGS := -std=c99 -Wall -Wpedantic -Werror=implicit -Wmissing-prototypes -O2 -g -Isrc
|
||||||
|
LDFLAGS :=
|
||||||
|
|
||||||
|
GEN_SRC := $(wildcard src/gen/*.c)
|
||||||
|
GEN_OBJ := $(GEN_SRC:src/%.c=obj/%.o)
|
||||||
|
|
||||||
|
.PHONY: all clean genesis test stress godot godot-clean
|
||||||
|
all: genesis
|
||||||
|
|
||||||
|
test: bin/genesis
|
||||||
|
@bash test/run_tests.sh
|
||||||
|
|
||||||
|
stress: bin/genesis
|
||||||
|
@./bin/genesis --stress 5000
|
||||||
|
|
||||||
|
godot:
|
||||||
|
@cd godot && scons -j$$(nproc) platform=linux target=template_debug
|
||||||
|
|
||||||
|
godot-clean:
|
||||||
|
@rm -rf godot/build godot/godot-cpp/bin demo/addons/brogue_gen
|
||||||
|
|
||||||
|
genesis: bin/genesis
|
||||||
|
|
||||||
|
bin/genesis: $(GEN_OBJ) obj/genesis_main.o obj/json_emit.o
|
||||||
|
@mkdir -p bin
|
||||||
|
$(CC) $(LDFLAGS) -o $@ $^ -lm
|
||||||
|
|
||||||
|
obj/%.o: src/%.c
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
obj/genesis_main.o: src/genesis_main.c
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
obj/json_emit.o: src/json_emit.c src/json_emit.h
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf obj bin
|
||||||
8
bin/f.fish
Executable file
8
bin/f.fish
Executable file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env fish
|
||||||
|
|
||||||
|
for i in (seq 1 9999)
|
||||||
|
echo "```" >>out.md
|
||||||
|
./genesis --seed $i 2&>>out.md
|
||||||
|
echo "```" >>out.md
|
||||||
|
echo "" >>out.md
|
||||||
|
end
|
||||||
329967
bin/out.md
Normal file
329967
bin/out.md
Normal file
File diff suppressed because it is too large
Load diff
23
demo/project.godot
Normal file
23
demo/project.godot
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
; 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
|
||||||
|
|
||||||
|
[animation]
|
||||||
|
|
||||||
|
compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
||||||
|
|
||||||
|
[application]
|
||||||
|
|
||||||
|
config/name="Brogue Genesis Demo"
|
||||||
|
config/features=PackedStringArray("4.6", "GL Compatibility")
|
||||||
|
|
||||||
|
[rendering]
|
||||||
|
|
||||||
|
renderer/rendering_method="gl_compatibility"
|
||||||
|
renderer/rendering_method.mobile="gl_compatibility"
|
||||||
127
demo/scenes/arcade.tscn
Normal file
127
demo/scenes/arcade.tscn
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
[gd_scene format=3 uid="uid://jysbib77g851"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/arcade/arcade_scene.gd" id="1_scene"]
|
||||||
|
[ext_resource type="Script" path="res://scripts/arcade/player_arcade.gd" id="2_player"]
|
||||||
|
[ext_resource type="Script" path="res://scripts/arcade/hud.gd" id="3_hud"]
|
||||||
|
|
||||||
|
[sub_resource type="Environment" id="Env1"]
|
||||||
|
background_mode = 1
|
||||||
|
background_color = Color(0.03, 0.03, 0.05, 1)
|
||||||
|
ambient_light_source = 2
|
||||||
|
ambient_light_color = Color(0.55, 0.55, 0.65, 1)
|
||||||
|
ambient_light_energy = 0.45
|
||||||
|
fog_enabled = false
|
||||||
|
|
||||||
|
[sub_resource type="CapsuleShape3D" id="PlayerCapsule"]
|
||||||
|
radius = 0.3
|
||||||
|
height = 1.7
|
||||||
|
|
||||||
|
[node name="Arcade" type="Node3D"]
|
||||||
|
script = ExtResource("1_scene")
|
||||||
|
base_seed = 25
|
||||||
|
start_depth = 1
|
||||||
|
total_depths = 10
|
||||||
|
|
||||||
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||||
|
environment = SubResource("Env1")
|
||||||
|
|
||||||
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||||
|
transform = Transform3D(0.707107, -0.5, 0.5, 0, 0.707107, 0.707107, -0.707107, -0.5, 0.5, 0, 10, 0)
|
||||||
|
light_color = Color(1, 0.95, 0.85, 1)
|
||||||
|
light_energy = 0.9
|
||||||
|
shadow_enabled = true
|
||||||
|
|
||||||
|
[node name="World" type="Node3D" parent="."]
|
||||||
|
|
||||||
|
[node name="Player" type="CharacterBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 39, 1.1, 14)
|
||||||
|
script = ExtResource("2_player")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
|
||||||
|
shape = SubResource("PlayerCapsule")
|
||||||
|
|
||||||
|
[node name="Camera3D" type="Camera3D" parent="Player"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
|
||||||
|
current = true
|
||||||
|
near = 0.05
|
||||||
|
far = 500.0
|
||||||
|
|
||||||
|
[node name="HUD" type="CanvasLayer" parent="."]
|
||||||
|
script = ExtResource("3_hud")
|
||||||
|
|
||||||
|
[node name="HPBar" type="Control" parent="HUD"]
|
||||||
|
offset_left = 16.0
|
||||||
|
offset_top = 16.0
|
||||||
|
offset_right = 240.0
|
||||||
|
offset_bottom = 56.0
|
||||||
|
|
||||||
|
[node name="BG" type="ColorRect" parent="HUD/HPBar"]
|
||||||
|
offset_right = 200.0
|
||||||
|
offset_bottom = 20.0
|
||||||
|
color = Color(0.1, 0.1, 0.12, 0.75)
|
||||||
|
|
||||||
|
[node name="Fill" type="ColorRect" parent="HUD/HPBar"]
|
||||||
|
offset_right = 200.0
|
||||||
|
offset_bottom = 20.0
|
||||||
|
color = Color(0.32, 0.82, 0.36, 1)
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="HUD/HPBar"]
|
||||||
|
offset_top = 22.0
|
||||||
|
offset_right = 240.0
|
||||||
|
offset_bottom = 40.0
|
||||||
|
theme_override_colors/font_color = Color(0.95, 0.95, 0.95, 1)
|
||||||
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/outline_size = 3
|
||||||
|
text = "HP 20 / 20"
|
||||||
|
|
||||||
|
[node name="InfoLabel" type="Label" parent="HUD"]
|
||||||
|
offset_left = 16.0
|
||||||
|
offset_top = 64.0
|
||||||
|
offset_right = 500.0
|
||||||
|
offset_bottom = 88.0
|
||||||
|
theme_override_colors/font_color = Color(0.95, 0.95, 0.95, 1)
|
||||||
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/outline_size = 3
|
||||||
|
text = "Depth 1 / 10"
|
||||||
|
|
||||||
|
[node name="EndOverlay" type="Control" parent="HUD"]
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="Dim" type="ColorRect" parent="HUD/EndOverlay"]
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
color = Color(0, 0, 0, 0.55)
|
||||||
|
|
||||||
|
[node name="Title" type="Label" parent="HUD/EndOverlay"]
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.4
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.4
|
||||||
|
offset_left = -200.0
|
||||||
|
offset_top = -40.0
|
||||||
|
offset_right = 200.0
|
||||||
|
offset_bottom = 20.0
|
||||||
|
theme_override_colors/font_color = Color(0.92, 0.28, 0.28, 1)
|
||||||
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/outline_size = 4
|
||||||
|
theme_override_font_sizes/font_size = 56
|
||||||
|
text = "YOU DIED"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="Hint" type="Label" parent="HUD/EndOverlay"]
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.55
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.55
|
||||||
|
offset_left = -160.0
|
||||||
|
offset_top = 0.0
|
||||||
|
offset_right = 160.0
|
||||||
|
offset_bottom = 30.0
|
||||||
|
theme_override_colors/font_color = Color(0.9, 0.9, 0.9, 1)
|
||||||
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/outline_size = 3
|
||||||
|
text = "Press R to restart"
|
||||||
|
horizontal_alignment = 1
|
||||||
32
demo/scenes/demo_3d.tscn
Normal file
32
demo/scenes/demo_3d.tscn
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
[gd_scene format=3 uid="uid://jysbic1s50to"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://brsi02a7ei24j" path="res://scripts/demo_3d.gd" id="1_demo3d"]
|
||||||
|
[ext_resource type="Script" uid="uid://d0srrm35g1m0t" path="res://scripts/fly_camera.gd" id="2_flycam"]
|
||||||
|
|
||||||
|
[sub_resource type="Environment" id="Env1"]
|
||||||
|
background_mode = 1
|
||||||
|
background_color = Color(0.04, 0.04, 0.05, 1)
|
||||||
|
ambient_light_source = 2
|
||||||
|
ambient_light_color = Color(0.6, 0.6, 0.7, 1)
|
||||||
|
ambient_light_energy = 0.4
|
||||||
|
|
||||||
|
[node name="Demo3D" type="Node3D" unique_id=36819859]
|
||||||
|
script = ExtResource("1_demo3d")
|
||||||
|
|
||||||
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1412341693]
|
||||||
|
environment = SubResource("Env1")
|
||||||
|
|
||||||
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=236128294]
|
||||||
|
transform = Transform3D(0.707107, -0.5, 0.5, 0, 0.707107, 0.707107, -0.707107, -0.5, 0.5, 0, 10, 0)
|
||||||
|
light_color = Color(1, 0.95, 0.85, 1)
|
||||||
|
light_energy = 0.9
|
||||||
|
shadow_enabled = true
|
||||||
|
|
||||||
|
[node name="FlyCamera" type="Camera3D" parent="." unique_id=658486835]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.819152, 0.573576, 0, -0.573576, 0.819152, 39, 14, 32)
|
||||||
|
current = true
|
||||||
|
near = 0.1
|
||||||
|
far = 500.0
|
||||||
|
script = ExtResource("2_flycam")
|
||||||
|
|
||||||
|
[node name="Levels" type="Node3D" parent="." unique_id=1476525758]
|
||||||
58
demo/scenes/demo_fps.tscn
Normal file
58
demo/scenes/demo_fps.tscn
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
[gd_scene format=3 uid="uid://c0brogue3dfps"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/demo_fps.gd" id="1_demofps"]
|
||||||
|
[ext_resource type="Script" path="res://scripts/player.gd" id="2_player"]
|
||||||
|
[ext_resource type="Script" path="res://scripts/fps_overlay.gd" id="3_fpsov"]
|
||||||
|
|
||||||
|
[sub_resource type="Environment" id="Env1"]
|
||||||
|
background_mode = 1
|
||||||
|
background_color = Color(0.04, 0.04, 0.05, 1)
|
||||||
|
ambient_light_source = 2
|
||||||
|
ambient_light_color = Color(0.6, 0.6, 0.7, 1)
|
||||||
|
ambient_light_energy = 0.4
|
||||||
|
fog_enabled = false
|
||||||
|
|
||||||
|
[sub_resource type="CapsuleShape3D" id="Capsule1"]
|
||||||
|
radius = 0.3
|
||||||
|
height = 1.7
|
||||||
|
|
||||||
|
[node name="DemoFPS" type="Node3D"]
|
||||||
|
script = ExtResource("1_demofps")
|
||||||
|
|
||||||
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||||
|
environment = SubResource("Env1")
|
||||||
|
|
||||||
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||||
|
transform = Transform3D(0.707107, -0.5, 0.5, 0, 0.707107, 0.707107, -0.707107, -0.5, 0.5, 0, 10, 0)
|
||||||
|
light_color = Color(1, 0.95, 0.85, 1)
|
||||||
|
light_energy = 0.9
|
||||||
|
shadow_enabled = true
|
||||||
|
|
||||||
|
[node name="Levels" type="Node3D" parent="."]
|
||||||
|
|
||||||
|
[node name="Player" type="CharacterBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 39, 1.1, 14)
|
||||||
|
script = ExtResource("2_player")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
|
||||||
|
shape = SubResource("Capsule1")
|
||||||
|
|
||||||
|
[node name="Camera3D" type="Camera3D" parent="Player"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
|
||||||
|
current = true
|
||||||
|
near = 0.05
|
||||||
|
far = 500.0
|
||||||
|
|
||||||
|
[node name="FPSOverlay" type="CanvasLayer" parent="."]
|
||||||
|
script = ExtResource("3_fpsov")
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="FPSOverlay"]
|
||||||
|
offset_left = 12.0
|
||||||
|
offset_top = 12.0
|
||||||
|
offset_right = 800.0
|
||||||
|
offset_bottom = 80.0
|
||||||
|
theme_override_colors/font_color = Color(0.95, 0.95, 0.95, 1)
|
||||||
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/outline_size = 3
|
||||||
|
text = "FPS: --"
|
||||||
65
demo/scenes/demo_large.tscn
Normal file
65
demo/scenes/demo_large.tscn
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
[gd_scene format=3 uid="uid://c0broguelarge"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/demo_fps.gd" id="1_demofps"]
|
||||||
|
[ext_resource type="Script" path="res://scripts/player.gd" id="2_player"]
|
||||||
|
[ext_resource type="Script" path="res://scripts/fps_overlay.gd" id="3_fpsov"]
|
||||||
|
|
||||||
|
[sub_resource type="Environment" id="Env1"]
|
||||||
|
background_mode = 1
|
||||||
|
background_color = Color(0.02, 0.02, 0.03, 1)
|
||||||
|
ambient_light_source = 2
|
||||||
|
ambient_light_color = Color(0.6, 0.6, 0.7, 1)
|
||||||
|
ambient_light_energy = 0.35
|
||||||
|
fog_enabled = false
|
||||||
|
|
||||||
|
[sub_resource type="CapsuleShape3D" id="Capsule1"]
|
||||||
|
radius = 0.3
|
||||||
|
height = 1.7
|
||||||
|
|
||||||
|
[node name="DemoLarge" type="Node3D"]
|
||||||
|
script = ExtResource("1_demofps")
|
||||||
|
base_seed = 2028
|
||||||
|
depth_start = 20
|
||||||
|
level_count = 3
|
||||||
|
level_spacing = 6.0
|
||||||
|
chunks_x = 5
|
||||||
|
chunks_y = 5
|
||||||
|
player_spawn_height = 1.1
|
||||||
|
|
||||||
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||||
|
environment = SubResource("Env1")
|
||||||
|
|
||||||
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||||
|
transform = Transform3D(0.707107, -0.5, 0.5, 0, 0.707107, 0.707107, -0.707107, -0.5, 0.5, 0, 10, 0)
|
||||||
|
light_color = Color(1, 0.95, 0.85, 1)
|
||||||
|
light_energy = 0.9
|
||||||
|
shadow_enabled = true
|
||||||
|
|
||||||
|
[node name="Levels" type="Node3D" parent="."]
|
||||||
|
|
||||||
|
[node name="Player" type="CharacterBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 39, 1.1, 14)
|
||||||
|
script = ExtResource("2_player")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
|
||||||
|
shape = SubResource("Capsule1")
|
||||||
|
|
||||||
|
[node name="Camera3D" type="Camera3D" parent="Player"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
|
||||||
|
current = true
|
||||||
|
near = 0.05
|
||||||
|
far = 2000.0
|
||||||
|
|
||||||
|
[node name="FPSOverlay" type="CanvasLayer" parent="."]
|
||||||
|
script = ExtResource("3_fpsov")
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="FPSOverlay"]
|
||||||
|
offset_left = 12.0
|
||||||
|
offset_top = 12.0
|
||||||
|
offset_right = 1000.0
|
||||||
|
offset_bottom = 80.0
|
||||||
|
theme_override_colors/font_color = Color(0.95, 0.95, 0.95, 1)
|
||||||
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/outline_size = 3
|
||||||
|
text = "FPS: --"
|
||||||
281
demo/scenes/game_root.tscn
Normal file
281
demo/scenes/game_root.tscn
Normal file
File diff suppressed because one or more lines are too long
136
demo/scenes/generator.gd
Normal file
136
demo/scenes/generator.gd
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
@tool
|
||||||
|
extends Node3D
|
||||||
|
class_name DungeonGenerator
|
||||||
|
|
||||||
|
const T_NOTHING := 0
|
||||||
|
const T_FLOOR := 1
|
||||||
|
const T_WALL := 2
|
||||||
|
const T_DOOR := 3
|
||||||
|
const T_CORRIDOR := 4
|
||||||
|
const T_LIQUID := 5
|
||||||
|
const T_BRIDGE := 6
|
||||||
|
const T_STAIRS_UP := 7
|
||||||
|
const T_STAIRS_DOWN := 8
|
||||||
|
|
||||||
|
const L_NONE := 0
|
||||||
|
const L_WATER := 1
|
||||||
|
const L_LAVA := 2
|
||||||
|
const L_CHASM := 3
|
||||||
|
const L_BRIMSTONE := 4
|
||||||
|
|
||||||
|
# Match the tile IDs exposed by MeshLibraryBuilder.
|
||||||
|
const TILE_FLOOR := 0
|
||||||
|
const TILE_WALL := 1
|
||||||
|
const TILE_DOOR := 2
|
||||||
|
const TILE_CORRIDOR := 3
|
||||||
|
const TILE_WATER := 4
|
||||||
|
const TILE_LAVA := 5
|
||||||
|
const TILE_BRIMSTONE := 6
|
||||||
|
const TILE_BRIDGE := 7
|
||||||
|
const TILE_STAIRS_UP := 8
|
||||||
|
const TILE_STAIRS_DOWN := 9
|
||||||
|
|
||||||
|
@export var base_seed: int = 2321
|
||||||
|
@export var depth_start: int = 20
|
||||||
|
@export var level_count: int = 10
|
||||||
|
@export var level_spacing: float = 6.0
|
||||||
|
@export var generate: bool = false:
|
||||||
|
set(value):
|
||||||
|
if value and Engine.is_editor_hint():
|
||||||
|
generate_dungeon()
|
||||||
|
generate = false
|
||||||
|
@export var clear_levels: bool = false:
|
||||||
|
set(value):
|
||||||
|
if value and Engine.is_editor_hint():
|
||||||
|
_clear_levels()
|
||||||
|
clear_levels = false
|
||||||
|
|
||||||
|
@onready var levels_root: Node3D = %Levels
|
||||||
|
|
||||||
|
var _mesh_library: MeshLibrary
|
||||||
|
|
||||||
|
# Populate one level's GridMap from the grid dict. Returns the count of
|
||||||
|
# chasm cells that were deliberately skipped (for reporting).
|
||||||
|
func _populate_level(grid_map: GridMap, grid: Dictionary) -> int:
|
||||||
|
var w: int = grid["width"]
|
||||||
|
var h: int = grid["height"]
|
||||||
|
var terrain: PackedByteArray = grid["terrain"]
|
||||||
|
var liquid: PackedByteArray = grid["liquid"]
|
||||||
|
|
||||||
|
var chasm_cells := 0
|
||||||
|
for y in range(h):
|
||||||
|
for x in range(w):
|
||||||
|
var idx := y * w + x
|
||||||
|
var t: int = terrain[idx]
|
||||||
|
var liq: int = liquid[idx]
|
||||||
|
# Chasm liquid renders as an actual see-through pit.
|
||||||
|
if t == T_LIQUID and liq == L_CHASM:
|
||||||
|
chasm_cells += 1
|
||||||
|
continue
|
||||||
|
var tile_id := _tile_for(t, liq)
|
||||||
|
if tile_id == -1:
|
||||||
|
continue # T_NOTHING or other empty — leave unrendered
|
||||||
|
# GridMap coords: (X, Y, Z). We want dungeon x → X, dungeon y → Z.
|
||||||
|
grid_map.set_cell_item(Vector3i(x, 0, y), tile_id)
|
||||||
|
return chasm_cells
|
||||||
|
|
||||||
|
func _tile_for(terrain: int, liquid: int) -> int:
|
||||||
|
match terrain:
|
||||||
|
T_FLOOR: return TILE_FLOOR
|
||||||
|
T_CORRIDOR: return TILE_CORRIDOR
|
||||||
|
T_DOOR: return TILE_DOOR
|
||||||
|
T_WALL: return TILE_WALL
|
||||||
|
T_BRIDGE: return TILE_BRIDGE
|
||||||
|
T_STAIRS_UP: return TILE_STAIRS_UP
|
||||||
|
T_STAIRS_DOWN: return TILE_STAIRS_DOWN
|
||||||
|
T_LIQUID:
|
||||||
|
match liquid:
|
||||||
|
L_WATER: return TILE_WATER
|
||||||
|
L_LAVA: return TILE_LAVA
|
||||||
|
L_BRIMSTONE: return TILE_BRIMSTONE
|
||||||
|
L_CHASM: return -1 # empty cell — see through to level below
|
||||||
|
_: return TILE_WATER
|
||||||
|
_: return -1 # T_NOTHING or unknown
|
||||||
|
|
||||||
|
func _clear_levels() -> void:
|
||||||
|
if levels_root == null:
|
||||||
|
levels_root = %Levels
|
||||||
|
for child in levels_root.get_children():
|
||||||
|
levels_root.remove_child(child)
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
func generate_dungeon():
|
||||||
|
if levels_root == null:
|
||||||
|
levels_root = %Levels
|
||||||
|
if levels_root.get_child_count() > 0:
|
||||||
|
push_warning("Levels already generated — toggle clear_levels first to regenerate.")
|
||||||
|
return
|
||||||
|
|
||||||
|
_mesh_library = MeshLibraryBuilder.build()
|
||||||
|
|
||||||
|
var edited_root: Node = get_tree().edited_scene_root if Engine.is_editor_hint() else null
|
||||||
|
var total_chasm_cells := 0
|
||||||
|
for level_index in range(level_count):
|
||||||
|
var level_seed := base_seed + level_index
|
||||||
|
var depth := depth_start + level_index
|
||||||
|
var gen := BrogueGen.new()
|
||||||
|
var grid: Dictionary = gen.generate(level_seed, depth)
|
||||||
|
gen.free()
|
||||||
|
|
||||||
|
var grid_map := GridMap.new()
|
||||||
|
grid_map.name = "Level%d" % level_index
|
||||||
|
grid_map.mesh_library = _mesh_library
|
||||||
|
grid_map.cell_size = Vector3(1, 1, 1)
|
||||||
|
grid_map.position.y = -level_index * level_spacing
|
||||||
|
levels_root.add_child(grid_map)
|
||||||
|
if edited_root:
|
||||||
|
grid_map.owner = edited_root
|
||||||
|
|
||||||
|
var chasm_cells := _populate_level(grid_map, grid)
|
||||||
|
total_chasm_cells += chasm_cells
|
||||||
|
|
||||||
|
print("Level %d: seed=%d depth=%d rooms=%d machines=%d chasms=%d"
|
||||||
|
% [level_index, level_seed, depth,
|
||||||
|
(grid["rooms"] as Array).size(),
|
||||||
|
(grid["machines"] as Array).size(),
|
||||||
|
chasm_cells])
|
||||||
1
demo/scenes/generator.gd.uid
Normal file
1
demo/scenes/generator.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ox0s7xjdj3lw
|
||||||
233
demo/scripts/arcade/arcade_scene.gd
Normal file
233
demo/scripts/arcade/arcade_scene.gd
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
# Arcade main scene. Owns:
|
||||||
|
# - current depth
|
||||||
|
# - map generation + rendering
|
||||||
|
# - player spawning
|
||||||
|
# - enemy spawning (proportional to room size)
|
||||||
|
# - level clear detection (player steps onto stairs_down)
|
||||||
|
# - death / restart
|
||||||
|
# Emits signals for the HUD.
|
||||||
|
|
||||||
|
signal depth_changed(depth: int, total: int)
|
||||||
|
signal enemies_changed(remaining: int, total: int)
|
||||||
|
signal game_over(won: bool)
|
||||||
|
|
||||||
|
const T_STAIRS_DOWN := 8
|
||||||
|
|
||||||
|
@export var base_seed: int = 25
|
||||||
|
@export var start_depth: int = 1
|
||||||
|
@export var total_depths: int = 10 # win after clearing this many
|
||||||
|
# Enemy count per room scales with depth. At depth 1, dungeons are sparse;
|
||||||
|
# by depth 10 they're thick.
|
||||||
|
@export var enemy_density_base: float = 0.04
|
||||||
|
@export var enemy_density_per_depth: float = 0.015
|
||||||
|
@export var enemy_min_per_room: int = 0
|
||||||
|
@export var enemy_max_per_room: int = 3
|
||||||
|
|
||||||
|
@onready var world_root: Node3D = $World
|
||||||
|
@onready var player: CharacterBody3D = $Player
|
||||||
|
@onready var hud: Node = $HUD
|
||||||
|
|
||||||
|
var _current_depth := 1
|
||||||
|
var _mesh_library: MeshLibrary
|
||||||
|
var _grid_map: GridMap = null
|
||||||
|
var _grid: Dictionary
|
||||||
|
var _enemies: Array[Node3D] = []
|
||||||
|
var _rng := RandomNumberGenerator.new()
|
||||||
|
var _transitioning := false
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_rng.randomize()
|
||||||
|
_mesh_library = MeshLibraryBuilder.build(true)
|
||||||
|
player.died.connect(_on_player_died)
|
||||||
|
player.hp_changed.connect(_on_player_hp)
|
||||||
|
_current_depth = start_depth
|
||||||
|
_build_level()
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if _transitioning:
|
||||||
|
return
|
||||||
|
_check_stairs_down()
|
||||||
|
|
||||||
|
# --- level lifecycle ---
|
||||||
|
|
||||||
|
func _build_level() -> void:
|
||||||
|
# Clear old level.
|
||||||
|
if _grid_map:
|
||||||
|
_grid_map.queue_free()
|
||||||
|
for e in _enemies:
|
||||||
|
if is_instance_valid(e):
|
||||||
|
e.queue_free()
|
||||||
|
_enemies.clear()
|
||||||
|
|
||||||
|
# Generate.
|
||||||
|
var gen := BrogueGen.new()
|
||||||
|
_grid = gen.generate(base_seed + _current_depth, _current_depth)
|
||||||
|
gen.free()
|
||||||
|
|
||||||
|
_grid_map = GridMap.new()
|
||||||
|
_grid_map.mesh_library = _mesh_library
|
||||||
|
_grid_map.cell_size = Vector3(1, 1, 1)
|
||||||
|
world_root.add_child(_grid_map)
|
||||||
|
_populate_grid_map(_grid_map, _grid)
|
||||||
|
|
||||||
|
# Spawn player at up-stair.
|
||||||
|
var up: Vector2i = _grid["stairs_up"] as Vector2i
|
||||||
|
if up.x >= 0:
|
||||||
|
player.global_position = GridUtil.grid_to_world(up, 1.1)
|
||||||
|
player.velocity = Vector3.ZERO
|
||||||
|
# Reset facing so each level starts neutral.
|
||||||
|
player.rotation.y = 0.0
|
||||||
|
|
||||||
|
# Spawn enemies.
|
||||||
|
_spawn_enemies()
|
||||||
|
|
||||||
|
print("Arcade: built depth %d — %d rooms, %d enemies, up=%s down=%s" % [
|
||||||
|
_current_depth, (_grid["rooms"] as Array).size(), _enemies.size(),
|
||||||
|
_grid["stairs_up"], _grid["stairs_down"],
|
||||||
|
])
|
||||||
|
depth_changed.emit(_current_depth, total_depths)
|
||||||
|
enemies_changed.emit(_enemies.size(), _enemies.size())
|
||||||
|
|
||||||
|
func _populate_grid_map(gm: GridMap, grid: Dictionary) -> void:
|
||||||
|
var w: int = grid["width"]
|
||||||
|
var h: int = grid["height"]
|
||||||
|
var terrain: PackedByteArray = grid["terrain"]
|
||||||
|
var liquid: PackedByteArray = grid["liquid"]
|
||||||
|
for y in range(h):
|
||||||
|
for x in range(w):
|
||||||
|
var idx := y * w + x
|
||||||
|
var t: int = terrain[idx]
|
||||||
|
var liq: int = liquid[idx]
|
||||||
|
if t == GridUtil.T_LIQUID and liq == GridUtil.L_CHASM:
|
||||||
|
continue
|
||||||
|
var tile := _tile_for(t, liq)
|
||||||
|
if tile < 0:
|
||||||
|
continue
|
||||||
|
gm.set_cell_item(Vector3i(x, 0, y), tile)
|
||||||
|
|
||||||
|
func _tile_for(terrain: int, liquid: int) -> int:
|
||||||
|
match terrain:
|
||||||
|
GridUtil.T_FLOOR: return MeshLibraryBuilder.TILE_FLOOR
|
||||||
|
GridUtil.T_CORRIDOR: return MeshLibraryBuilder.TILE_CORRIDOR
|
||||||
|
GridUtil.T_DOOR: return MeshLibraryBuilder.TILE_DOOR
|
||||||
|
GridUtil.T_WALL: return MeshLibraryBuilder.TILE_WALL
|
||||||
|
GridUtil.T_BRIDGE: return MeshLibraryBuilder.TILE_BRIDGE
|
||||||
|
GridUtil.T_STAIRS_UP: return MeshLibraryBuilder.TILE_STAIRS_UP
|
||||||
|
GridUtil.T_STAIRS_DOWN: return MeshLibraryBuilder.TILE_STAIRS_DOWN
|
||||||
|
GridUtil.T_LIQUID:
|
||||||
|
match liquid:
|
||||||
|
GridUtil.L_WATER: return MeshLibraryBuilder.TILE_WATER
|
||||||
|
GridUtil.L_LAVA: return MeshLibraryBuilder.TILE_LAVA
|
||||||
|
GridUtil.L_BRIMSTONE: return MeshLibraryBuilder.TILE_BRIMSTONE
|
||||||
|
_: return MeshLibraryBuilder.TILE_WATER
|
||||||
|
_: return -1
|
||||||
|
|
||||||
|
# --- enemy spawning ---
|
||||||
|
|
||||||
|
func _spawn_enemies() -> void:
|
||||||
|
var player_grid := GridUtil.world_to_grid(player.global_position)
|
||||||
|
var rooms: Array = _grid["rooms"]
|
||||||
|
for room in rooms:
|
||||||
|
var cells: Array = room["cells"]
|
||||||
|
if cells.size() < 4:
|
||||||
|
continue
|
||||||
|
var density := enemy_density_base + enemy_density_per_depth * (_current_depth - 1)
|
||||||
|
var target := int(round(cells.size() * density))
|
||||||
|
target = clamp(target, enemy_min_per_room, enemy_max_per_room)
|
||||||
|
# Room containing the player gets at most 1 enemy, placed far from them.
|
||||||
|
var room_has_player := false
|
||||||
|
for c in cells:
|
||||||
|
if c == player_grid:
|
||||||
|
room_has_player = true
|
||||||
|
break
|
||||||
|
if room_has_player:
|
||||||
|
target = min(target, 1)
|
||||||
|
for i in range(target):
|
||||||
|
var cell: Vector2i = cells[_rng.randi() % cells.size()]
|
||||||
|
if room_has_player and cell.distance_to(player_grid) < 4.0:
|
||||||
|
continue
|
||||||
|
_spawn_enemy_at(cell, cells)
|
||||||
|
|
||||||
|
func _spawn_enemy_at(cell: Vector2i, room_cells: Array) -> void:
|
||||||
|
var enemy := CharacterBody3D.new()
|
||||||
|
enemy.set_script(load("res://scripts/arcade/enemy.gd"))
|
||||||
|
|
||||||
|
# Visible body: a red box above the floor.
|
||||||
|
var mesh := MeshInstance3D.new()
|
||||||
|
var box := BoxMesh.new()
|
||||||
|
box.size = Vector3(0.6, 1.4, 0.6)
|
||||||
|
mesh.mesh = box
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = Color(0.80, 0.20, 0.20)
|
||||||
|
box.material = mat
|
||||||
|
mesh.position = Vector3(0, 0.7, 0)
|
||||||
|
enemy.add_child(mesh)
|
||||||
|
|
||||||
|
# Collision shape.
|
||||||
|
var col := CollisionShape3D.new()
|
||||||
|
var shape := CapsuleShape3D.new()
|
||||||
|
shape.radius = 0.3
|
||||||
|
shape.height = 1.4
|
||||||
|
col.shape = shape
|
||||||
|
col.position = Vector3(0, 0.7, 0)
|
||||||
|
enemy.add_child(col)
|
||||||
|
|
||||||
|
world_root.add_child(enemy)
|
||||||
|
enemy.global_position = GridUtil.grid_to_world(cell, 0.1)
|
||||||
|
enemy.grid_ref = _grid
|
||||||
|
enemy.set_room(room_cells)
|
||||||
|
enemy.player_ref = player
|
||||||
|
enemy.died.connect(_on_enemy_died.bind(enemy))
|
||||||
|
_enemies.append(enemy)
|
||||||
|
|
||||||
|
# --- transitions ---
|
||||||
|
|
||||||
|
func _check_stairs_down() -> void:
|
||||||
|
var pg := GridUtil.world_to_grid(player.global_position)
|
||||||
|
var w: int = _grid["width"]
|
||||||
|
var terrain: PackedByteArray = _grid["terrain"]
|
||||||
|
var idx := pg.y * w + pg.x
|
||||||
|
if idx < 0 or idx >= terrain.size():
|
||||||
|
return
|
||||||
|
if terrain[idx] == T_STAIRS_DOWN:
|
||||||
|
_advance_depth()
|
||||||
|
|
||||||
|
func _advance_depth() -> void:
|
||||||
|
if _transitioning:
|
||||||
|
return
|
||||||
|
_transitioning = true
|
||||||
|
_current_depth += 1
|
||||||
|
if _current_depth > start_depth + total_depths - 1:
|
||||||
|
game_over.emit(true)
|
||||||
|
return
|
||||||
|
call_deferred("_build_level")
|
||||||
|
call_deferred("_clear_transition_flag")
|
||||||
|
|
||||||
|
func _clear_transition_flag() -> void:
|
||||||
|
_transitioning = false
|
||||||
|
|
||||||
|
func _on_player_died() -> void:
|
||||||
|
game_over.emit(false)
|
||||||
|
|
||||||
|
func _on_player_hp(_cur: int, _max: int) -> void:
|
||||||
|
# HUD listens directly; no-op here.
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _on_enemy_died(enemy: Node3D) -> void:
|
||||||
|
_enemies.erase(enemy)
|
||||||
|
var total_spawned := _enemies.size()
|
||||||
|
# We emit remaining = enemies.size; total isn't tracked separately for
|
||||||
|
# this MVP. HUD shows alive count.
|
||||||
|
enemies_changed.emit(_enemies.size(), total_spawned)
|
||||||
|
|
||||||
|
# Called by HUD restart button.
|
||||||
|
func restart_from_start() -> void:
|
||||||
|
_transitioning = true
|
||||||
|
_current_depth = start_depth
|
||||||
|
player.hp = player.max_hp
|
||||||
|
player.hp_changed.emit(player.hp, player.max_hp)
|
||||||
|
player._alive = true
|
||||||
|
call_deferred("_build_level")
|
||||||
|
call_deferred("_clear_transition_flag")
|
||||||
1
demo/scripts/arcade/arcade_scene.gd.uid
Normal file
1
demo/scripts/arcade/arcade_scene.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://3hwwo1hwe12
|
||||||
174
demo/scripts/arcade/enemy.gd
Normal file
174
demo/scripts/arcade/enemy.gd
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
class_name ArcadeEnemy
|
||||||
|
extends CharacterBody3D
|
||||||
|
|
||||||
|
# Three-state FSM:
|
||||||
|
# WANDER: pick a random passable cell in this enemy's room, walk there,
|
||||||
|
# occasionally idle.
|
||||||
|
# CHASE: move toward last-known player grid cell via BFS.
|
||||||
|
# ATTACK: within melee range — swing, tick cooldown.
|
||||||
|
#
|
||||||
|
# Transitions:
|
||||||
|
# WANDER -> CHASE when player has LOS AND distance ≤ sight_radius
|
||||||
|
# CHASE -> WANDER when player has been out of sight for lose_sight_turns
|
||||||
|
# CHASE -> ATTACK when close enough to hit
|
||||||
|
# ATTACK -> CHASE when out of range
|
||||||
|
|
||||||
|
signal died
|
||||||
|
|
||||||
|
enum State { WANDER, CHASE, ATTACK }
|
||||||
|
|
||||||
|
@export var hp := 8
|
||||||
|
@export var move_speed := 3.0
|
||||||
|
@export var sight_radius := 10.0
|
||||||
|
@export var melee_range := 1.4
|
||||||
|
@export var melee_damage := 2
|
||||||
|
@export var melee_cooldown := 0.9
|
||||||
|
@export var lose_sight_seconds := 3.0
|
||||||
|
|
||||||
|
# Set by spawner on instantiate.
|
||||||
|
var grid_ref: Dictionary = {}
|
||||||
|
var room_cells: Array = [] # Array[Vector2i]
|
||||||
|
var player_ref: Node3D = null
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
|
||||||
|
var _state := State.WANDER
|
||||||
|
var _path_target: Vector2i = Vector2i(-1, -1)
|
||||||
|
var _repath_timer := 0.0
|
||||||
|
var _attack_timer := 0.0
|
||||||
|
var _lose_sight_timer := 0.0
|
||||||
|
var _alive := true
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
rng.randomize()
|
||||||
|
_pick_wander_target()
|
||||||
|
|
||||||
|
func set_room(cells: Array) -> void:
|
||||||
|
room_cells = cells
|
||||||
|
_pick_wander_target()
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
if not _alive:
|
||||||
|
return
|
||||||
|
if _attack_timer > 0.0:
|
||||||
|
_attack_timer -= delta
|
||||||
|
|
||||||
|
_update_state(delta)
|
||||||
|
|
||||||
|
match _state:
|
||||||
|
State.WANDER: _do_wander(delta)
|
||||||
|
State.CHASE: _do_chase(delta)
|
||||||
|
State.ATTACK: _do_attack(delta)
|
||||||
|
|
||||||
|
# Gravity always.
|
||||||
|
if not is_on_floor():
|
||||||
|
velocity.y -= 22.0 * delta
|
||||||
|
else:
|
||||||
|
velocity.y = max(velocity.y, 0.0)
|
||||||
|
|
||||||
|
move_and_slide()
|
||||||
|
|
||||||
|
# --- state machine ---
|
||||||
|
|
||||||
|
func _update_state(delta: float) -> void:
|
||||||
|
if player_ref == null:
|
||||||
|
return
|
||||||
|
var my_grid := GridUtil.world_to_grid(global_position)
|
||||||
|
var p_grid := GridUtil.world_to_grid(player_ref.global_position)
|
||||||
|
var dist := float(my_grid.distance_to(p_grid))
|
||||||
|
var can_see := dist <= sight_radius and GridUtil.has_los(grid_ref, my_grid, p_grid)
|
||||||
|
|
||||||
|
match _state:
|
||||||
|
State.WANDER:
|
||||||
|
if can_see:
|
||||||
|
_state = State.CHASE
|
||||||
|
_path_target = p_grid
|
||||||
|
_repath_timer = 0.0
|
||||||
|
State.CHASE:
|
||||||
|
if can_see:
|
||||||
|
_lose_sight_timer = 0.0
|
||||||
|
_path_target = p_grid
|
||||||
|
else:
|
||||||
|
_lose_sight_timer += delta
|
||||||
|
if _lose_sight_timer >= lose_sight_seconds:
|
||||||
|
_state = State.WANDER
|
||||||
|
_pick_wander_target()
|
||||||
|
return
|
||||||
|
# Close enough to swing?
|
||||||
|
var world_dist := global_position.distance_to(player_ref.global_position)
|
||||||
|
if world_dist <= melee_range:
|
||||||
|
_state = State.ATTACK
|
||||||
|
State.ATTACK:
|
||||||
|
var world_dist2 := global_position.distance_to(player_ref.global_position)
|
||||||
|
if world_dist2 > melee_range * 1.3:
|
||||||
|
_state = State.CHASE
|
||||||
|
if not can_see:
|
||||||
|
_lose_sight_timer += delta
|
||||||
|
if _lose_sight_timer >= lose_sight_seconds:
|
||||||
|
_state = State.WANDER
|
||||||
|
_pick_wander_target()
|
||||||
|
|
||||||
|
# --- behaviors ---
|
||||||
|
|
||||||
|
func _do_wander(delta: float) -> void:
|
||||||
|
_repath_timer -= delta
|
||||||
|
if _path_target == Vector2i(-1, -1) or _at_target():
|
||||||
|
if _repath_timer <= 0.0:
|
||||||
|
_repath_timer = rng.randf_range(0.8, 2.0)
|
||||||
|
_pick_wander_target()
|
||||||
|
_step_toward(_path_target, move_speed * 0.5)
|
||||||
|
|
||||||
|
func _do_chase(delta: float) -> void:
|
||||||
|
_repath_timer -= delta
|
||||||
|
if _repath_timer <= 0.0:
|
||||||
|
_repath_timer = 0.25 # repath 4×/sec
|
||||||
|
_step_toward(_path_target, move_speed)
|
||||||
|
|
||||||
|
func _do_attack(_delta: float) -> void:
|
||||||
|
velocity.x = 0.0
|
||||||
|
velocity.z = 0.0
|
||||||
|
if _attack_timer <= 0.0:
|
||||||
|
_attack_timer = melee_cooldown
|
||||||
|
if player_ref and player_ref.has_method("take_damage"):
|
||||||
|
player_ref.take_damage(melee_damage, self)
|
||||||
|
|
||||||
|
# --- helpers ---
|
||||||
|
|
||||||
|
func _pick_wander_target() -> void:
|
||||||
|
if room_cells.is_empty():
|
||||||
|
_path_target = GridUtil.world_to_grid(global_position)
|
||||||
|
return
|
||||||
|
_path_target = GridUtil.random_passable(grid_ref, room_cells, rng)
|
||||||
|
|
||||||
|
func _at_target() -> bool:
|
||||||
|
var my_grid := GridUtil.world_to_grid(global_position)
|
||||||
|
return my_grid == _path_target
|
||||||
|
|
||||||
|
func _step_toward(target: Vector2i, speed: float) -> void:
|
||||||
|
if target == Vector2i(-1, -1):
|
||||||
|
velocity.x = 0.0
|
||||||
|
velocity.z = 0.0
|
||||||
|
return
|
||||||
|
var my_grid := GridUtil.world_to_grid(global_position)
|
||||||
|
var nxt := GridUtil.next_step_toward(grid_ref, my_grid, target)
|
||||||
|
if nxt == my_grid:
|
||||||
|
velocity.x = 0.0
|
||||||
|
velocity.z = 0.0
|
||||||
|
return
|
||||||
|
var target_world := GridUtil.grid_to_world(nxt, global_position.y)
|
||||||
|
var dir := (target_world - global_position)
|
||||||
|
dir.y = 0
|
||||||
|
dir = dir.normalized()
|
||||||
|
velocity.x = dir.x * speed
|
||||||
|
velocity.z = dir.z * speed
|
||||||
|
# Face movement direction so we look right visually.
|
||||||
|
if dir.length() > 0.1:
|
||||||
|
look_at(global_position + dir, Vector3.UP)
|
||||||
|
|
||||||
|
func take_damage(amount: int, _source: Node) -> void:
|
||||||
|
if not _alive:
|
||||||
|
return
|
||||||
|
hp -= amount
|
||||||
|
if hp <= 0:
|
||||||
|
_alive = false
|
||||||
|
died.emit()
|
||||||
|
queue_free()
|
||||||
1
demo/scripts/arcade/enemy.gd.uid
Normal file
1
demo/scripts/arcade/enemy.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dr7jelri306sd
|
||||||
139
demo/scripts/arcade/grid_util.gd
Normal file
139
demo/scripts/arcade/grid_util.gd
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
class_name GridUtil
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
# Static helpers for the arcade game. Single 79×29 grid per level; the
|
||||||
|
# generator dict is the source of truth. World↔grid mapping uses 1-unit
|
||||||
|
# cells (matches demo_fps.gd GridMap cell_size).
|
||||||
|
#
|
||||||
|
# Pathfinding is a plain 4-connected BFS, tiny code, fast enough at this
|
||||||
|
# scale (<1 ms for a full grid flood on a laptop). For an arcade game we
|
||||||
|
# recompute per-enemy every ~200ms, not every frame.
|
||||||
|
|
||||||
|
const T_FLOOR := 1
|
||||||
|
const T_WALL := 2
|
||||||
|
const T_DOOR := 3
|
||||||
|
const T_CORRIDOR := 4
|
||||||
|
const T_LIQUID := 5
|
||||||
|
const T_BRIDGE := 6
|
||||||
|
const T_STAIRS_UP := 7
|
||||||
|
const T_STAIRS_DOWN := 8
|
||||||
|
|
||||||
|
const L_WATER := 1
|
||||||
|
const L_LAVA := 2
|
||||||
|
const L_CHASM := 3
|
||||||
|
const L_BRIMSTONE := 4
|
||||||
|
|
||||||
|
const CELL_SIZE := 1.0
|
||||||
|
|
||||||
|
# --- coordinate mapping ---
|
||||||
|
|
||||||
|
static func grid_to_world(g: Vector2i, y: float = 0.0) -> Vector3:
|
||||||
|
return Vector3(float(g.x), y, float(g.y))
|
||||||
|
|
||||||
|
static func world_to_grid(w: Vector3) -> Vector2i:
|
||||||
|
return Vector2i(int(floor(w.x + 0.5)), int(floor(w.z + 0.5)))
|
||||||
|
|
||||||
|
# --- passability ---
|
||||||
|
|
||||||
|
# A cell is passable if an enemy (or player, for pathfinding intent) can
|
||||||
|
# step on it. Corridors, floor, doors, bridges, stairs, water count.
|
||||||
|
# Walls, chasms, lava, brimstone do not.
|
||||||
|
static func is_passable(grid: Dictionary, pos: Vector2i) -> bool:
|
||||||
|
var w: int = grid["width"]
|
||||||
|
var h: int = grid["height"]
|
||||||
|
if pos.x < 0 or pos.y < 0 or pos.x >= w or pos.y >= h:
|
||||||
|
return false
|
||||||
|
var idx := pos.y * w + pos.x
|
||||||
|
var t: int = (grid["terrain"] as PackedByteArray)[idx]
|
||||||
|
if t == T_LIQUID:
|
||||||
|
var liq: int = (grid["liquid"] as PackedByteArray)[idx]
|
||||||
|
return liq == L_WATER # water walkable, lava/chasm/brimstone not
|
||||||
|
return t == T_FLOOR or t == T_CORRIDOR or t == T_DOOR \
|
||||||
|
or t == T_BRIDGE or t == T_STAIRS_UP or t == T_STAIRS_DOWN
|
||||||
|
|
||||||
|
# --- BFS pathfinding ---
|
||||||
|
|
||||||
|
# Returns the next grid step from `from` toward `to`, or from itself if no
|
||||||
|
# path exists or we're already there. 4-connected, uniform cost.
|
||||||
|
static func next_step_toward(grid: Dictionary, from: Vector2i, to: Vector2i) -> Vector2i:
|
||||||
|
if from == to:
|
||||||
|
return from
|
||||||
|
if not is_passable(grid, from) or not is_passable(grid, to):
|
||||||
|
return from
|
||||||
|
|
||||||
|
var w: int = grid["width"]
|
||||||
|
var h: int = grid["height"]
|
||||||
|
var came_from := {} # Vector2i → Vector2i
|
||||||
|
came_from[from] = from
|
||||||
|
var queue: Array[Vector2i] = [from]
|
||||||
|
var found := false
|
||||||
|
var dirs := [Vector2i(1,0), Vector2i(-1,0), Vector2i(0,1), Vector2i(0,-1)]
|
||||||
|
|
||||||
|
while queue.size() > 0 and not found:
|
||||||
|
var cur: Vector2i = queue.pop_front()
|
||||||
|
for d in dirs:
|
||||||
|
var nxt: Vector2i = cur + d
|
||||||
|
if came_from.has(nxt): continue
|
||||||
|
if not is_passable(grid, nxt): continue
|
||||||
|
came_from[nxt] = cur
|
||||||
|
if nxt == to:
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
queue.push_back(nxt)
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
return from
|
||||||
|
|
||||||
|
# Walk back from to → find cell whose came_from is `from`
|
||||||
|
var cur: Vector2i = to
|
||||||
|
while came_from[cur] != from:
|
||||||
|
cur = came_from[cur]
|
||||||
|
return cur
|
||||||
|
|
||||||
|
# True if there's an unobstructed line from a → b for enemy LOS purposes.
|
||||||
|
# Walks the Bresenham ray; any non-passable cell (wall / chasm / lava)
|
||||||
|
# blocks sight. Doors are transparent for LOS.
|
||||||
|
static func has_los(grid: Dictionary, a: Vector2i, b: Vector2i) -> bool:
|
||||||
|
var dx := absi(b.x - a.x)
|
||||||
|
var dy := absi(b.y - a.y)
|
||||||
|
var sx := 1 if a.x < b.x else -1
|
||||||
|
var sy := 1 if a.y < b.y else -1
|
||||||
|
var err := dx - dy
|
||||||
|
var x := a.x
|
||||||
|
var y := a.y
|
||||||
|
while true:
|
||||||
|
if Vector2i(x, y) == b: return true
|
||||||
|
# Skip blocking check at start cell.
|
||||||
|
if not (x == a.x and y == a.y):
|
||||||
|
if not _transparent(grid, Vector2i(x, y)):
|
||||||
|
return false
|
||||||
|
var e2 := 2 * err
|
||||||
|
if e2 > -dy:
|
||||||
|
err -= dy
|
||||||
|
x += sx
|
||||||
|
if e2 < dx:
|
||||||
|
err += dx
|
||||||
|
y += sy
|
||||||
|
return false
|
||||||
|
|
||||||
|
static func _transparent(grid: Dictionary, pos: Vector2i) -> bool:
|
||||||
|
var w: int = grid["width"]
|
||||||
|
var h: int = grid["height"]
|
||||||
|
if pos.x < 0 or pos.y < 0 or pos.x >= w or pos.y >= h:
|
||||||
|
return false
|
||||||
|
var idx := pos.y * w + pos.x
|
||||||
|
var t: int = (grid["terrain"] as PackedByteArray)[idx]
|
||||||
|
# Walls block; everything else is transparent for LOS.
|
||||||
|
return t != T_WALL and t != 0
|
||||||
|
|
||||||
|
# Pick a random passable cell in the given list of cells. Returns (-1,-1)
|
||||||
|
# if none are passable. Used by enemy wander target selection.
|
||||||
|
static func random_passable(grid: Dictionary, cells: Array, rng: RandomNumberGenerator) -> Vector2i:
|
||||||
|
if cells.is_empty():
|
||||||
|
return Vector2i(-1, -1)
|
||||||
|
var shuffled := cells.duplicate()
|
||||||
|
shuffled.shuffle()
|
||||||
|
for c in shuffled:
|
||||||
|
if is_passable(grid, c):
|
||||||
|
return c
|
||||||
|
return Vector2i(-1, -1)
|
||||||
1
demo/scripts/arcade/grid_util.gd.uid
Normal file
1
demo/scripts/arcade/grid_util.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://buo0sbr01qcm5
|
||||||
57
demo/scripts/arcade/hud.gd
Normal file
57
demo/scripts/arcade/hud.gd
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
# HP bar, depth label, enemy count, death / win overlay.
|
||||||
|
|
||||||
|
@onready var hp_bar: ColorRect = $HPBar/Fill
|
||||||
|
@onready var hp_label: Label = $HPBar/Label
|
||||||
|
@onready var depth_label: Label = $InfoLabel
|
||||||
|
@onready var overlay: Control = $EndOverlay
|
||||||
|
@onready var overlay_title: Label = $EndOverlay/Title
|
||||||
|
@onready var overlay_hint: Label = $EndOverlay/Hint
|
||||||
|
|
||||||
|
const HP_COLOR_HIGH := Color(0.32, 0.82, 0.36)
|
||||||
|
const HP_COLOR_LOW := Color(0.88, 0.28, 0.22)
|
||||||
|
const HP_BAR_MAX_WIDTH := 200.0
|
||||||
|
|
||||||
|
var _game_over := false
|
||||||
|
var _scene_ref: Node = null
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
overlay.hide()
|
||||||
|
_scene_ref = get_parent()
|
||||||
|
_scene_ref.depth_changed.connect(_on_depth)
|
||||||
|
_scene_ref.enemies_changed.connect(_on_enemies)
|
||||||
|
_scene_ref.game_over.connect(_on_game_over)
|
||||||
|
# Player is a sibling; @onready on parent isn't set yet when we fire.
|
||||||
|
var p: Node = get_node("../Player")
|
||||||
|
p.hp_changed.connect(_on_hp)
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if _game_over and event is InputEventKey and event.pressed and event.keycode == KEY_R:
|
||||||
|
_game_over = false
|
||||||
|
overlay.hide()
|
||||||
|
_scene_ref.restart_from_start()
|
||||||
|
|
||||||
|
func _on_hp(current: int, maximum: int) -> void:
|
||||||
|
var frac := 0.0 if maximum <= 0 else float(current) / float(maximum)
|
||||||
|
hp_bar.size.x = HP_BAR_MAX_WIDTH * frac
|
||||||
|
hp_bar.color = HP_COLOR_LOW.lerp(HP_COLOR_HIGH, frac)
|
||||||
|
hp_label.text = "HP %d / %d" % [current, maximum]
|
||||||
|
|
||||||
|
func _on_depth(depth: int, total: int) -> void:
|
||||||
|
depth_label.text = "Depth %d / %d" % [depth, total]
|
||||||
|
|
||||||
|
func _on_enemies(remaining: int, _total: int) -> void:
|
||||||
|
depth_label.text = "%s Enemies: %d" % [depth_label.text.split(" ")[0], remaining]
|
||||||
|
|
||||||
|
func _on_game_over(won: bool) -> void:
|
||||||
|
_game_over = true
|
||||||
|
overlay.show()
|
||||||
|
if won:
|
||||||
|
overlay_title.text = "YOU WIN"
|
||||||
|
overlay_title.modulate = Color(0.35, 0.90, 0.45)
|
||||||
|
else:
|
||||||
|
overlay_title.text = "YOU DIED"
|
||||||
|
overlay_title.modulate = Color(0.92, 0.28, 0.28)
|
||||||
|
overlay_hint.text = "Press R to restart"
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
1
demo/scripts/arcade/hud.gd.uid
Normal file
1
demo/scripts/arcade/hud.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dd7ud5jyshij5
|
||||||
118
demo/scripts/arcade/player_arcade.gd
Normal file
118
demo/scripts/arcade/player_arcade.gd
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
class_name PlayerArcade
|
||||||
|
extends CharacterBody3D
|
||||||
|
|
||||||
|
# Arcade player: same FPS controller as the demo, plus HP and a melee
|
||||||
|
# attack on left-click. Emits hp_changed and died signals; arcade_scene
|
||||||
|
# listens and updates the HUD / restart logic.
|
||||||
|
|
||||||
|
signal hp_changed(current: int, maximum: int)
|
||||||
|
signal died
|
||||||
|
signal attacked(hit_point: Vector3, hit_body: Node)
|
||||||
|
|
||||||
|
@export var speed := 6.0
|
||||||
|
@export var run_multiplier := 1.8
|
||||||
|
@export var jump_velocity := 6.0
|
||||||
|
@export var gravity := 22.0
|
||||||
|
@export var mouse_sensitivity := 0.002
|
||||||
|
@export var max_hp := 20
|
||||||
|
@export var melee_range := 1.6
|
||||||
|
@export var melee_damage := 6
|
||||||
|
@export var melee_cooldown := 0.35
|
||||||
|
|
||||||
|
@onready var camera: Camera3D = $Camera3D
|
||||||
|
|
||||||
|
var hp := 20
|
||||||
|
var _pitch := 0.0
|
||||||
|
var _captured := false
|
||||||
|
var _melee_timer := 0.0
|
||||||
|
var _alive := true
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
hp = max_hp
|
||||||
|
hp_changed.emit(hp, max_hp)
|
||||||
|
_capture()
|
||||||
|
|
||||||
|
func _capture() -> void:
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||||
|
_captured = true
|
||||||
|
|
||||||
|
func _release() -> void:
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
_captured = false
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if not _alive:
|
||||||
|
return
|
||||||
|
if event is InputEventMouseMotion and _captured:
|
||||||
|
var m := event as InputEventMouseMotion
|
||||||
|
rotation.y -= m.relative.x * mouse_sensitivity
|
||||||
|
_pitch -= m.relative.y * mouse_sensitivity
|
||||||
|
_pitch = clamp(_pitch, deg_to_rad(-85.0), deg_to_rad(85.0))
|
||||||
|
camera.rotation.x = _pitch
|
||||||
|
elif event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||||
|
_release()
|
||||||
|
elif event is InputEventMouseButton and event.pressed:
|
||||||
|
if not _captured:
|
||||||
|
_capture()
|
||||||
|
elif event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
_try_attack()
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
if not _alive:
|
||||||
|
return
|
||||||
|
if _melee_timer > 0.0:
|
||||||
|
_melee_timer -= delta
|
||||||
|
|
||||||
|
if not is_on_floor():
|
||||||
|
velocity.y -= gravity * delta
|
||||||
|
|
||||||
|
var input := Vector2.ZERO
|
||||||
|
if Input.is_key_pressed(KEY_W): input.y -= 1.0
|
||||||
|
if Input.is_key_pressed(KEY_S): input.y += 1.0
|
||||||
|
if Input.is_key_pressed(KEY_A): input.x -= 1.0
|
||||||
|
if Input.is_key_pressed(KEY_D): input.x += 1.0
|
||||||
|
input = input.normalized()
|
||||||
|
|
||||||
|
var s := speed
|
||||||
|
if Input.is_key_pressed(KEY_SHIFT):
|
||||||
|
s *= run_multiplier
|
||||||
|
|
||||||
|
var forward := -transform.basis.z
|
||||||
|
var right := transform.basis.x
|
||||||
|
var horiz := (forward * -input.y + right * input.x) * s
|
||||||
|
velocity.x = horiz.x
|
||||||
|
velocity.z = horiz.z
|
||||||
|
|
||||||
|
if is_on_floor() and Input.is_key_pressed(KEY_SPACE):
|
||||||
|
velocity.y = jump_velocity
|
||||||
|
|
||||||
|
move_and_slide()
|
||||||
|
|
||||||
|
# Deal damage to any enemy within melee_range in front of the camera.
|
||||||
|
func _try_attack() -> void:
|
||||||
|
if _melee_timer > 0.0:
|
||||||
|
return
|
||||||
|
_melee_timer = melee_cooldown
|
||||||
|
|
||||||
|
var origin := camera.global_position
|
||||||
|
var dir := -camera.global_transform.basis.z
|
||||||
|
var space := get_world_3d().direct_space_state
|
||||||
|
var query := PhysicsRayQueryParameters3D.create(origin, origin + dir * melee_range)
|
||||||
|
query.exclude = [self]
|
||||||
|
var hit := space.intersect_ray(query)
|
||||||
|
if hit.is_empty():
|
||||||
|
return
|
||||||
|
var body := hit.get("collider") as Node
|
||||||
|
if body and body.has_method("take_damage"):
|
||||||
|
body.take_damage(melee_damage, self)
|
||||||
|
attacked.emit(hit.get("position", Vector3.ZERO), body)
|
||||||
|
|
||||||
|
# Called by enemies.
|
||||||
|
func take_damage(amount: int, _source: Node) -> void:
|
||||||
|
if not _alive:
|
||||||
|
return
|
||||||
|
hp = max(0, hp - amount)
|
||||||
|
hp_changed.emit(hp, max_hp)
|
||||||
|
if hp <= 0:
|
||||||
|
_alive = false
|
||||||
|
died.emit()
|
||||||
1
demo/scripts/arcade/player_arcade.gd.uid
Normal file
1
demo/scripts/arcade/player_arcade.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dxyvug2vl2jjq
|
||||||
135
demo/scripts/bake_dungeon.gd
Normal file
135
demo/scripts/bake_dungeon.gd
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
# Usage:
|
||||||
|
# godot --headless --path demo --script scripts/bake_dungeon.gd -- SEED DEPTH OUT_DIR
|
||||||
|
#
|
||||||
|
# Shells out to bin/genesis --emit=json and bakes the result into two sibling
|
||||||
|
# assets under OUT_DIR:
|
||||||
|
# - mesh_library.tres (tile catalog, same content MLB.build()
|
||||||
|
# produces at runtime — shareable across bakes)
|
||||||
|
# - dungeon_seed<S>_depth<D>.tscn (PackedScene with a GridMap referencing
|
||||||
|
# the MeshLibrary, populated per the
|
||||||
|
# generated dungeon)
|
||||||
|
#
|
||||||
|
# Override the genesis binary path with the GENESIS_BIN env var; default is
|
||||||
|
# "../bin/genesis" relative to the Godot project.
|
||||||
|
|
||||||
|
const MESH_LIB_NAME := "mesh_library.tres"
|
||||||
|
const MLB = preload("res://scripts/mesh_library_builder.gd")
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
var args := OS.get_cmdline_user_args()
|
||||||
|
if args.size() != 3:
|
||||||
|
push_error("usage: -- SEED DEPTH OUT_DIR")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
var seed := int(args[0])
|
||||||
|
var depth := int(args[1])
|
||||||
|
var out_dir := args[2]
|
||||||
|
|
||||||
|
var bin_path := OS.get_environment("GENESIS_BIN")
|
||||||
|
if bin_path == "":
|
||||||
|
bin_path = ProjectSettings.globalize_path("res://") + "../bin/genesis"
|
||||||
|
bin_path = bin_path.simplify_path()
|
||||||
|
|
||||||
|
if not DirAccess.dir_exists_absolute(out_dir):
|
||||||
|
var err := DirAccess.make_dir_recursive_absolute(out_dir)
|
||||||
|
if err != OK:
|
||||||
|
push_error("cannot create %s (err %d)" % [out_dir, err])
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
var stdout: Array = []
|
||||||
|
var exit_code := OS.execute(bin_path, [
|
||||||
|
"--seed", str(seed), "--depth", str(depth), "--emit=json",
|
||||||
|
], stdout, true)
|
||||||
|
if exit_code != 0:
|
||||||
|
push_error("genesis exited %d (bin=%s)" % [exit_code, bin_path])
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
var grid: Dictionary = JSON.parse_string(stdout[0])
|
||||||
|
if grid.is_empty():
|
||||||
|
push_error("genesis produced invalid JSON")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Step 1 — MeshLibrary.tres.
|
||||||
|
var lib_path := "%s/%s" % [out_dir, MESH_LIB_NAME]
|
||||||
|
var save_err := MLB.save_resource(lib_path, true)
|
||||||
|
if save_err != OK:
|
||||||
|
push_error("failed to save %s (err %d)" % [lib_path, save_err])
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Step 2 — PackedScene with populated GridMap.
|
||||||
|
var lib: MeshLibrary = load(lib_path)
|
||||||
|
var root := Node3D.new()
|
||||||
|
root.name = "Dungeon_seed%d_depth%d" % [seed, depth]
|
||||||
|
var gm := GridMap.new()
|
||||||
|
gm.name = "GridMap"
|
||||||
|
gm.mesh_library = lib
|
||||||
|
gm.cell_size = Vector3(1, 1, 1)
|
||||||
|
root.add_child(gm)
|
||||||
|
gm.owner = root
|
||||||
|
|
||||||
|
var cells_set := _populate(gm, grid)
|
||||||
|
|
||||||
|
var packed := PackedScene.new()
|
||||||
|
var pack_err := packed.pack(root)
|
||||||
|
if pack_err != OK:
|
||||||
|
push_error("failed to pack scene (err %d)" % pack_err)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
var scn_path := "%s/dungeon_seed%d_depth%d.tscn" % [out_dir, seed, depth]
|
||||||
|
var scn_err := ResourceSaver.save(packed, scn_path)
|
||||||
|
if scn_err != OK:
|
||||||
|
push_error("failed to save %s (err %d)" % [scn_path, scn_err])
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
print("wrote %s + %s — %d cells" % [scn_path, lib_path, cells_set])
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
# Decode the base64 layers and stamp tile IDs into the GridMap. Mirrors
|
||||||
|
# arcade_scene.gd _populate_grid_map so the CLI produces the same geometry
|
||||||
|
# as the in-engine path for the same seed/depth. Chasms stay as empty cells.
|
||||||
|
func _populate(gm: GridMap, grid: Dictionary) -> int:
|
||||||
|
var w := int(grid["width"])
|
||||||
|
var h := int(grid["height"])
|
||||||
|
var terrain := Marshalls.base64_to_raw(grid["terrain"])
|
||||||
|
var liquid := Marshalls.base64_to_raw(grid["liquid"])
|
||||||
|
|
||||||
|
var count := 0
|
||||||
|
for y in range(h):
|
||||||
|
for x in range(w):
|
||||||
|
var idx := y * w + x
|
||||||
|
var t := terrain[idx]
|
||||||
|
var liq := liquid[idx]
|
||||||
|
if t == 5 and liq == 3: # T_LIQUID + L_CHASM
|
||||||
|
continue
|
||||||
|
var tile := _tile_for(t, liq)
|
||||||
|
if tile < 0:
|
||||||
|
continue
|
||||||
|
gm.set_cell_item(Vector3i(x, 0, y), tile)
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
func _tile_for(terrain: int, liquid: int) -> int:
|
||||||
|
match terrain:
|
||||||
|
1: return MLB.TILE_FLOOR
|
||||||
|
4: return MLB.TILE_CORRIDOR
|
||||||
|
3: return MLB.TILE_DOOR
|
||||||
|
2: return MLB.TILE_WALL
|
||||||
|
6: return MLB.TILE_BRIDGE
|
||||||
|
7: return MLB.TILE_STAIRS_UP
|
||||||
|
8: return MLB.TILE_STAIRS_DOWN
|
||||||
|
5:
|
||||||
|
match liquid:
|
||||||
|
1: return MLB.TILE_WATER
|
||||||
|
2: return MLB.TILE_LAVA
|
||||||
|
4: return MLB.TILE_BRIMSTONE
|
||||||
|
_: return MLB.TILE_WATER
|
||||||
|
_: return -1
|
||||||
1
demo/scripts/bake_dungeon.gd.uid
Normal file
1
demo/scripts/bake_dungeon.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://y0qm8301m7w6
|
||||||
122
demo/scripts/demo_3d.gd
Normal file
122
demo/scripts/demo_3d.gd
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
# Demo_3D: renders multiple stacked levels of Brogue dungeons using GridMap.
|
||||||
|
# Chasm cells are rendered as empty (no tile), so looking down through one
|
||||||
|
# reveals the floor of the level below.
|
||||||
|
|
||||||
|
# Must match src/gen/grid.h
|
||||||
|
const T_NOTHING := 0
|
||||||
|
const T_FLOOR := 1
|
||||||
|
const T_WALL := 2
|
||||||
|
const T_DOOR := 3
|
||||||
|
const T_CORRIDOR := 4
|
||||||
|
const T_LIQUID := 5
|
||||||
|
const T_BRIDGE := 6
|
||||||
|
const T_STAIRS_UP := 7
|
||||||
|
const T_STAIRS_DOWN := 8
|
||||||
|
|
||||||
|
const L_NONE := 0
|
||||||
|
const L_WATER := 1
|
||||||
|
const L_LAVA := 2
|
||||||
|
const L_CHASM := 3
|
||||||
|
const L_BRIMSTONE := 4
|
||||||
|
|
||||||
|
# Match the tile IDs exposed by MeshLibraryBuilder.
|
||||||
|
const TILE_FLOOR := 0
|
||||||
|
const TILE_WALL := 1
|
||||||
|
const TILE_DOOR := 2
|
||||||
|
const TILE_CORRIDOR := 3
|
||||||
|
const TILE_WATER := 4
|
||||||
|
const TILE_LAVA := 5
|
||||||
|
const TILE_BRIMSTONE := 6
|
||||||
|
const TILE_BRIDGE := 7
|
||||||
|
const TILE_STAIRS_UP := 8
|
||||||
|
const TILE_STAIRS_DOWN := 9
|
||||||
|
|
||||||
|
@export var base_seed: int = 2321
|
||||||
|
@export var depth_start: int = 20
|
||||||
|
@export var level_count: int = 10
|
||||||
|
@export var level_spacing: float = 6.0
|
||||||
|
|
||||||
|
@onready var levels_root: Node3D = $Levels
|
||||||
|
@onready var camera: Camera3D = $FlyCamera
|
||||||
|
|
||||||
|
var _mesh_library: MeshLibrary
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_mesh_library = MeshLibraryBuilder.build()
|
||||||
|
|
||||||
|
var total_chasm_cells := 0
|
||||||
|
for level_index in range(level_count):
|
||||||
|
var level_seed := base_seed + level_index
|
||||||
|
var depth := depth_start + level_index
|
||||||
|
var gen := BrogueGen.new()
|
||||||
|
var grid: Dictionary = gen.generate(level_seed, depth)
|
||||||
|
gen.free()
|
||||||
|
|
||||||
|
var grid_map := GridMap.new()
|
||||||
|
grid_map.name = "Level%d" % level_index
|
||||||
|
grid_map.mesh_library = _mesh_library
|
||||||
|
grid_map.cell_size = Vector3(1, 1, 1)
|
||||||
|
grid_map.position.y = -level_index * level_spacing
|
||||||
|
levels_root.add_child(grid_map)
|
||||||
|
|
||||||
|
var chasm_cells := _populate_level(grid_map, grid)
|
||||||
|
total_chasm_cells += chasm_cells
|
||||||
|
|
||||||
|
print("Level %d: seed=%d depth=%d rooms=%d machines=%d chasms=%d"
|
||||||
|
% [level_index, level_seed, depth,
|
||||||
|
(grid["rooms"] as Array).size(),
|
||||||
|
(grid["machines"] as Array).size(),
|
||||||
|
chasm_cells])
|
||||||
|
|
||||||
|
# Position the camera above level 0's grid center, tilted down.
|
||||||
|
var grid_w := 79
|
||||||
|
var grid_h := 29
|
||||||
|
camera.position = Vector3(grid_w * 0.5, 14, grid_h * 0.5 + 18)
|
||||||
|
camera.rotation_degrees = Vector3(-35, 0, 0)
|
||||||
|
|
||||||
|
print("Demo3D ready. %d levels, %d chasm cells total." % [level_count, total_chasm_cells])
|
||||||
|
|
||||||
|
# Populate one level's GridMap from the grid dict. Returns the count of
|
||||||
|
# chasm cells that were deliberately skipped (for reporting).
|
||||||
|
func _populate_level(grid_map: GridMap, grid: Dictionary) -> int:
|
||||||
|
var w: int = grid["width"]
|
||||||
|
var h: int = grid["height"]
|
||||||
|
var terrain: PackedByteArray = grid["terrain"]
|
||||||
|
var liquid: PackedByteArray = grid["liquid"]
|
||||||
|
|
||||||
|
var chasm_cells := 0
|
||||||
|
for y in range(h):
|
||||||
|
for x in range(w):
|
||||||
|
var idx := y * w + x
|
||||||
|
var t: int = terrain[idx]
|
||||||
|
var liq: int = liquid[idx]
|
||||||
|
# Chasm liquid renders as an actual see-through pit.
|
||||||
|
if t == T_LIQUID and liq == L_CHASM:
|
||||||
|
chasm_cells += 1
|
||||||
|
continue
|
||||||
|
var tile_id := _tile_for(t, liq)
|
||||||
|
if tile_id == -1:
|
||||||
|
continue # T_NOTHING or other empty — leave unrendered
|
||||||
|
# GridMap coords: (X, Y, Z). We want dungeon x → X, dungeon y → Z.
|
||||||
|
grid_map.set_cell_item(Vector3i(x, 0, y), tile_id)
|
||||||
|
return chasm_cells
|
||||||
|
|
||||||
|
func _tile_for(terrain: int, liquid: int) -> int:
|
||||||
|
match terrain:
|
||||||
|
T_FLOOR: return TILE_FLOOR
|
||||||
|
T_CORRIDOR: return TILE_CORRIDOR
|
||||||
|
T_DOOR: return TILE_DOOR
|
||||||
|
T_WALL: return TILE_WALL
|
||||||
|
T_BRIDGE: return TILE_BRIDGE
|
||||||
|
T_STAIRS_UP: return TILE_STAIRS_UP
|
||||||
|
T_STAIRS_DOWN: return TILE_STAIRS_DOWN
|
||||||
|
T_LIQUID:
|
||||||
|
match liquid:
|
||||||
|
L_WATER: return TILE_WATER
|
||||||
|
L_LAVA: return TILE_LAVA
|
||||||
|
L_BRIMSTONE: return TILE_BRIMSTONE
|
||||||
|
L_CHASM: return -1 # empty cell — see through to level below
|
||||||
|
_: return TILE_WATER
|
||||||
|
_: return -1 # T_NOTHING or unknown
|
||||||
1
demo/scripts/demo_3d.gd.uid
Normal file
1
demo/scripts/demo_3d.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://brsi02a7ei24j
|
||||||
143
demo/scripts/demo_fps.gd
Normal file
143
demo/scripts/demo_fps.gd
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
# First-person demo. Generates a grid of chunks (each a 79×29 dungeon) times
|
||||||
|
# level_count stacked layers. Default is 1×1 chunks × 3 levels. Bump
|
||||||
|
# chunks_x / chunks_y to stress-test the renderer with larger worlds.
|
||||||
|
|
||||||
|
const T_NOTHING := 0
|
||||||
|
const T_FLOOR := 1
|
||||||
|
const T_WALL := 2
|
||||||
|
const T_DOOR := 3
|
||||||
|
const T_CORRIDOR := 4
|
||||||
|
const T_LIQUID := 5
|
||||||
|
const T_BRIDGE := 6
|
||||||
|
const T_STAIRS_UP := 7
|
||||||
|
const T_STAIRS_DOWN := 8
|
||||||
|
|
||||||
|
const L_WATER := 1
|
||||||
|
const L_LAVA := 2
|
||||||
|
const L_CHASM := 3
|
||||||
|
const L_BRIMSTONE := 4
|
||||||
|
|
||||||
|
const TILE_FLOOR := 0
|
||||||
|
const TILE_WALL := 1
|
||||||
|
const TILE_DOOR := 2
|
||||||
|
const TILE_CORRIDOR := 3
|
||||||
|
const TILE_WATER := 4
|
||||||
|
const TILE_LAVA := 5
|
||||||
|
const TILE_BRIMSTONE := 6
|
||||||
|
const TILE_BRIDGE := 7
|
||||||
|
const TILE_STAIRS_UP := 8
|
||||||
|
const TILE_STAIRS_DOWN := 9
|
||||||
|
|
||||||
|
const CHUNK_W := 79
|
||||||
|
const CHUNK_H := 29
|
||||||
|
|
||||||
|
@export var base_seed: int = 2028
|
||||||
|
@export var depth_start: int = 20
|
||||||
|
@export var level_count: int = 3
|
||||||
|
@export var level_spacing: float = 6.0
|
||||||
|
@export var chunks_x: int = 1
|
||||||
|
@export var chunks_y: int = 1
|
||||||
|
@export var player_spawn_height: float = 1.1
|
||||||
|
|
||||||
|
@onready var levels_root: Node3D = $Levels
|
||||||
|
@onready var player: CharacterBody3D = $Player
|
||||||
|
@onready var fps_overlay: Node = $FPSOverlay if has_node("FPSOverlay") else null
|
||||||
|
|
||||||
|
var _mesh_library: MeshLibrary
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var t0 := Time.get_ticks_msec()
|
||||||
|
_mesh_library = MeshLibraryBuilder.build(true)
|
||||||
|
|
||||||
|
var total_cells := 0
|
||||||
|
var total_chunks := 0
|
||||||
|
var spawn_world := Vector3.ZERO
|
||||||
|
var spawn_found := false
|
||||||
|
|
||||||
|
for level_index in range(level_count):
|
||||||
|
for cy in range(chunks_y):
|
||||||
|
for cx in range(chunks_x):
|
||||||
|
var seed := base_seed \
|
||||||
|
+ level_index * 1000 \
|
||||||
|
+ cy * chunks_x + cx
|
||||||
|
var depth := depth_start + level_index
|
||||||
|
var gen := BrogueGen.new()
|
||||||
|
var grid: Dictionary = gen.generate(seed, depth)
|
||||||
|
gen.free()
|
||||||
|
|
||||||
|
var grid_map := GridMap.new()
|
||||||
|
grid_map.name = "L%d_C%d_%d" % [level_index, cx, cy]
|
||||||
|
grid_map.mesh_library = _mesh_library
|
||||||
|
grid_map.cell_size = Vector3(1, 1, 1)
|
||||||
|
grid_map.position = Vector3(
|
||||||
|
cx * CHUNK_W,
|
||||||
|
-level_index * level_spacing,
|
||||||
|
cy * CHUNK_H
|
||||||
|
)
|
||||||
|
levels_root.add_child(grid_map)
|
||||||
|
|
||||||
|
var cells := _populate_level(grid_map, grid)
|
||||||
|
total_cells += cells
|
||||||
|
total_chunks += 1
|
||||||
|
|
||||||
|
# First up-stair on level 0, chunk (0,0) is the spawn point.
|
||||||
|
if not spawn_found and level_index == 0 and cx == 0 and cy == 0:
|
||||||
|
var up: Vector2i = grid["stairs_up"] as Vector2i
|
||||||
|
if up.x >= 0:
|
||||||
|
spawn_world = Vector3(up.x, player_spawn_height, up.y)
|
||||||
|
spawn_found = true
|
||||||
|
|
||||||
|
if not spawn_found:
|
||||||
|
spawn_world = Vector3(CHUNK_W * 0.5, player_spawn_height, CHUNK_H * 0.5)
|
||||||
|
player.position = spawn_world
|
||||||
|
|
||||||
|
var build_ms := Time.get_ticks_msec() - t0
|
||||||
|
var info := "%d chunks (%dx%d x %d lvls) %d placed cells build=%d ms" % [
|
||||||
|
total_chunks, chunks_x, chunks_y, level_count, total_cells, build_ms,
|
||||||
|
]
|
||||||
|
print(info)
|
||||||
|
print("Player spawned at %s" % spawn_world)
|
||||||
|
if fps_overlay and fps_overlay.has_method("set_subtitle"):
|
||||||
|
fps_overlay.set_subtitle(info)
|
||||||
|
|
||||||
|
# Place every non-chasm cell into the GridMap. Returns the number of cells
|
||||||
|
# actually placed (excludes T_NOTHING and chasms).
|
||||||
|
func _populate_level(grid_map: GridMap, grid: Dictionary) -> int:
|
||||||
|
var w: int = grid["width"]
|
||||||
|
var h: int = grid["height"]
|
||||||
|
var terrain: PackedByteArray = grid["terrain"]
|
||||||
|
var liquid: PackedByteArray = grid["liquid"]
|
||||||
|
|
||||||
|
var placed := 0
|
||||||
|
for y in range(h):
|
||||||
|
for x in range(w):
|
||||||
|
var idx := y * w + x
|
||||||
|
var t: int = terrain[idx]
|
||||||
|
var liq: int = liquid[idx]
|
||||||
|
if t == T_LIQUID and liq == L_CHASM:
|
||||||
|
continue
|
||||||
|
var tile_id := _tile_for(t, liq)
|
||||||
|
if tile_id == -1:
|
||||||
|
continue
|
||||||
|
grid_map.set_cell_item(Vector3i(x, 0, y), tile_id)
|
||||||
|
placed += 1
|
||||||
|
return placed
|
||||||
|
|
||||||
|
func _tile_for(terrain: int, liquid: int) -> int:
|
||||||
|
match terrain:
|
||||||
|
T_FLOOR: return TILE_FLOOR
|
||||||
|
T_CORRIDOR: return TILE_CORRIDOR
|
||||||
|
T_DOOR: return TILE_DOOR
|
||||||
|
T_WALL: return TILE_WALL
|
||||||
|
T_BRIDGE: return TILE_BRIDGE
|
||||||
|
T_STAIRS_UP: return TILE_STAIRS_UP
|
||||||
|
T_STAIRS_DOWN: return TILE_STAIRS_DOWN
|
||||||
|
T_LIQUID:
|
||||||
|
match liquid:
|
||||||
|
L_WATER: return TILE_WATER
|
||||||
|
L_LAVA: return TILE_LAVA
|
||||||
|
L_BRIMSTONE: return TILE_BRIMSTONE
|
||||||
|
_: return TILE_WATER
|
||||||
|
_: return -1
|
||||||
1
demo/scripts/demo_fps.gd.uid
Normal file
1
demo/scripts/demo_fps.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dvoatkhtgji2u
|
||||||
261
demo/scripts/export_map.gd
Normal file
261
demo/scripts/export_map.gd
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
# Usage:
|
||||||
|
# godot --headless --path demo --script scripts/export_map.gd -- SEED DEPTH [LEVELS] OUT.map
|
||||||
|
#
|
||||||
|
# Generates N dungeons (seeds SEED..SEED+N-1, depths DEPTH..DEPTH+N-1),
|
||||||
|
# stacks them vertically, and writes one Standard Quake .map file.
|
||||||
|
#
|
||||||
|
# Chasms on non-bottom levels are real holes — no floor brush, and the
|
||||||
|
# level below has its ceiling drilled out at the same (x,y) so the shaft
|
||||||
|
# stays open all the way down until it hits a non-chasm floor. Chasms on
|
||||||
|
# the bottom level get a local pit floor at CHASM_TOP so you don't fall
|
||||||
|
# into the void.
|
||||||
|
|
||||||
|
const TILE_SIZE := 64
|
||||||
|
const HEIGHT := 128
|
||||||
|
const WALL_THICKNESS := 64
|
||||||
|
const TEXTURE := "__TB_empty"
|
||||||
|
|
||||||
|
const FLOOR_TOP := 0
|
||||||
|
const WATER_TOP := -32
|
||||||
|
const CHASM_TOP := -128
|
||||||
|
const PIT_BOTTOM := CHASM_TOP - WALL_THICKNESS # = -192
|
||||||
|
const WALL_TOP := HEIGHT + WALL_THICKNESS # = 192
|
||||||
|
|
||||||
|
# Per-level Z offset. Each level's brushes get translated by
|
||||||
|
# -level_index * LEVEL_SPACING. Sized so level N+1's ceiling top sits at
|
||||||
|
# or below level N's PIT_BOTTOM — no overlap, no z-fighting.
|
||||||
|
const LEVEL_SPACING := 384
|
||||||
|
|
||||||
|
# terrain_t
|
||||||
|
const T_NOTHING := 0
|
||||||
|
const T_FLOOR := 1
|
||||||
|
const T_WALL := 2
|
||||||
|
const T_DOOR := 3
|
||||||
|
const T_CORRIDOR := 4
|
||||||
|
const T_LIQUID := 5
|
||||||
|
const T_BRIDGE := 6
|
||||||
|
const T_STAIRS_UP := 7
|
||||||
|
const T_STAIRS_DOWN := 8
|
||||||
|
|
||||||
|
# liquid_t
|
||||||
|
const L_WATER := 1
|
||||||
|
const L_CHASM := 3
|
||||||
|
|
||||||
|
enum Kind { EMPTY, WALL, FLOOR, WATER, CHASM }
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
var args := OS.get_cmdline_user_args()
|
||||||
|
var seed: int
|
||||||
|
var depth: int
|
||||||
|
var levels: int
|
||||||
|
var out_path: String
|
||||||
|
match args.size():
|
||||||
|
3:
|
||||||
|
seed = int(args[0])
|
||||||
|
depth = int(args[1])
|
||||||
|
levels = 1
|
||||||
|
out_path = args[2]
|
||||||
|
4:
|
||||||
|
seed = int(args[0])
|
||||||
|
depth = int(args[1])
|
||||||
|
levels = int(args[2])
|
||||||
|
out_path = args[3]
|
||||||
|
_:
|
||||||
|
push_error("usage: -- SEED DEPTH [LEVELS] OUT.map")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
if levels < 1:
|
||||||
|
push_error("levels must be >= 1")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
var grids: Array = []
|
||||||
|
for k in range(levels):
|
||||||
|
var gen := BrogueGen.new()
|
||||||
|
grids.append(gen.generate(seed + k, depth + k))
|
||||||
|
gen.free()
|
||||||
|
|
||||||
|
var f := FileAccess.open(out_path, FileAccess.WRITE)
|
||||||
|
if f == null:
|
||||||
|
push_error("cannot open %s for write" % out_path)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
var brush_count := _write_map(f, grids, seed, depth)
|
||||||
|
f.close()
|
||||||
|
print("wrote %s — %d levels, %d brushes" % [out_path, levels, brush_count])
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
func _write_map(f: FileAccess, grids: Array, seed: int, depth: int) -> int:
|
||||||
|
f.store_string("// Game: FuncGodot\n")
|
||||||
|
f.store_string("// Format: Valve\n")
|
||||||
|
f.store_string("// Generated by brogue-genesis — seed: %d, depth: %d, levels: %d\n" % [
|
||||||
|
seed, depth, grids.size(),
|
||||||
|
])
|
||||||
|
f.store_string("{\n")
|
||||||
|
f.store_string("\"classname\" \"worldspawn\"\n")
|
||||||
|
|
||||||
|
var count := 0
|
||||||
|
var w: int = grids[0]["width"]
|
||||||
|
var h: int = grids[0]["height"]
|
||||||
|
# Per-(x,y) flag: "some level above has a chasm here, so our ceiling
|
||||||
|
# is drilled out so the shaft stays open". Accumulates top-down.
|
||||||
|
var chasm_above := PackedByteArray()
|
||||||
|
chasm_above.resize(w * h)
|
||||||
|
|
||||||
|
for k in range(grids.size()):
|
||||||
|
var grid: Dictionary = grids[k]
|
||||||
|
var is_bottom := (k == grids.size() - 1)
|
||||||
|
var z_offset := -k * LEVEL_SPACING
|
||||||
|
count += _write_level(f, grid, z_offset, chasm_above, is_bottom)
|
||||||
|
# Update chasm_above for the next level using THIS level's chasms.
|
||||||
|
_propagate_chasms(grid, chasm_above)
|
||||||
|
|
||||||
|
f.store_string("}\n")
|
||||||
|
return count
|
||||||
|
|
||||||
|
func _write_level(f: FileAccess, grid: Dictionary, z_off: int,
|
||||||
|
chasm_above: PackedByteArray, is_bottom: bool) -> int:
|
||||||
|
var w: int = grid["width"]
|
||||||
|
var h: int = grid["height"]
|
||||||
|
var terrain: PackedByteArray = grid["terrain"]
|
||||||
|
var liquid: PackedByteArray = grid["liquid"]
|
||||||
|
|
||||||
|
var count := 0
|
||||||
|
var ts := TILE_SIZE
|
||||||
|
|
||||||
|
# Pass 1 — floors / walls, row-merged by kind.
|
||||||
|
for gy in range(h):
|
||||||
|
var run_start := 0
|
||||||
|
var run_kind := _kind(terrain, liquid, w, 0, gy)
|
||||||
|
for gx in range(1, w + 1):
|
||||||
|
var cur: int = Kind.EMPTY
|
||||||
|
if gx < w:
|
||||||
|
cur = _kind(terrain, liquid, w, gx, gy)
|
||||||
|
if cur == run_kind and gx < w:
|
||||||
|
continue
|
||||||
|
var x0 := run_start * ts
|
||||||
|
var x1 := gx * ts
|
||||||
|
var y0 := gy * ts
|
||||||
|
var y1 := (gy + 1) * ts
|
||||||
|
match run_kind:
|
||||||
|
Kind.WALL:
|
||||||
|
count += _emit_box(f, x0, y0, z_off + PIT_BOTTOM,
|
||||||
|
x1, y1, z_off + WALL_TOP)
|
||||||
|
Kind.FLOOR:
|
||||||
|
count += _emit_box(f, x0, y0, z_off + PIT_BOTTOM,
|
||||||
|
x1, y1, z_off + FLOOR_TOP)
|
||||||
|
Kind.WATER:
|
||||||
|
count += _emit_box(f, x0, y0, z_off + PIT_BOTTOM,
|
||||||
|
x1, y1, z_off + WATER_TOP)
|
||||||
|
Kind.CHASM:
|
||||||
|
# Non-bottom chasms are real holes — no floor. Bottom
|
||||||
|
# chasms get a local pit floor so the player doesn't
|
||||||
|
# fall into empty void.
|
||||||
|
if is_bottom:
|
||||||
|
count += _emit_box(f, x0, y0, z_off + PIT_BOTTOM,
|
||||||
|
x1, y1, z_off + CHASM_TOP)
|
||||||
|
_:
|
||||||
|
pass
|
||||||
|
run_start = gx
|
||||||
|
run_kind = cur
|
||||||
|
|
||||||
|
# Pass 2 — ceilings, row-merged by "is ceiling present here?". A
|
||||||
|
# ceiling is present if the cell has floor-like terrain AND no level
|
||||||
|
# above has drilled a chasm through it.
|
||||||
|
for gy in range(h):
|
||||||
|
var run_start := 0
|
||||||
|
var run_has := _has_ceiling(terrain, chasm_above, w, 0, gy)
|
||||||
|
for gx in range(1, w + 1):
|
||||||
|
var cur := false
|
||||||
|
if gx < w:
|
||||||
|
cur = _has_ceiling(terrain, chasm_above, w, gx, gy)
|
||||||
|
if cur == run_has and gx < w:
|
||||||
|
continue
|
||||||
|
if run_has:
|
||||||
|
var x0 := run_start * ts
|
||||||
|
var x1 := gx * ts
|
||||||
|
var y0 := gy * ts
|
||||||
|
var y1 := (gy + 1) * ts
|
||||||
|
count += _emit_box(f, x0, y0, z_off + HEIGHT,
|
||||||
|
x1, y1, z_off + WALL_TOP)
|
||||||
|
run_start = gx
|
||||||
|
run_has = cur
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
# Record any chasm cells at this level into the running mask so the NEXT
|
||||||
|
# level knows to drill its ceiling there.
|
||||||
|
func _propagate_chasms(grid: Dictionary, chasm_above: PackedByteArray) -> void:
|
||||||
|
var w: int = grid["width"]
|
||||||
|
var h: int = grid["height"]
|
||||||
|
var terrain: PackedByteArray = grid["terrain"]
|
||||||
|
var liquid: PackedByteArray = grid["liquid"]
|
||||||
|
for gy in range(h):
|
||||||
|
for gx in range(w):
|
||||||
|
var idx := gy * w + gx
|
||||||
|
if terrain[idx] == T_LIQUID and liquid[idx] == L_CHASM:
|
||||||
|
chasm_above[idx] = 1
|
||||||
|
|
||||||
|
# Draw kind for floor/wall emission.
|
||||||
|
func _kind(terrain: PackedByteArray, liquid: PackedByteArray,
|
||||||
|
w: int, x: int, y: int) -> int:
|
||||||
|
var idx := y * w + x
|
||||||
|
var t: int = terrain[idx]
|
||||||
|
match t:
|
||||||
|
T_NOTHING: return Kind.EMPTY
|
||||||
|
T_WALL: return Kind.WALL
|
||||||
|
T_LIQUID:
|
||||||
|
var liq: int = liquid[idx]
|
||||||
|
if liq == L_CHASM: return Kind.CHASM
|
||||||
|
if liq == L_WATER: return Kind.WATER
|
||||||
|
return Kind.FLOOR # lava / brimstone sit at floor level
|
||||||
|
_: return Kind.FLOOR
|
||||||
|
|
||||||
|
# Ceiling emitted when the cell is floor-like AND no chasm sits above it.
|
||||||
|
# Walls and empty cells don't get ceilings either (wall brush already
|
||||||
|
# reaches WALL_TOP; empty is empty).
|
||||||
|
func _has_ceiling(terrain: PackedByteArray, chasm_above: PackedByteArray,
|
||||||
|
w: int, x: int, y: int) -> bool:
|
||||||
|
var idx := y * w + x
|
||||||
|
if chasm_above[idx] != 0:
|
||||||
|
return false
|
||||||
|
var t: int = terrain[idx]
|
||||||
|
return t != T_NOTHING and t != T_WALL
|
||||||
|
|
||||||
|
# Standard Quake brush: 6 axis-aligned planes, 3 points per plane, inward
|
||||||
|
# normals per TrenchBroom convention. Mirrors libd's emit_solid_box.
|
||||||
|
func _emit_box(f: FileAccess, x0: int, y0: int, z0: int,
|
||||||
|
x1: int, y1: int, z1: int) -> int:
|
||||||
|
if x0 >= x1 or y0 >= y1 or z0 >= z1:
|
||||||
|
return 0
|
||||||
|
f.store_string("{\n")
|
||||||
|
# Valve 220 texture axes per face normal (Quake convention).
|
||||||
|
# X-facing walls: U=Y, V=-Z. Y-facing walls: U=X, V=-Z. Z-facing: U=X, V=-Y.
|
||||||
|
var ax_x := "[ 0 1 0 0 ] [ 0 0 -1 0 ]"
|
||||||
|
var ax_y := "[ 1 0 0 0 ] [ 0 0 -1 0 ]"
|
||||||
|
var ax_z := "[ 1 0 0 0 ] [ 0 -1 0 0 ]"
|
||||||
|
# -X
|
||||||
|
_face(f, x0, y0, z0, x0, y1, z0, x0, y0, z1, ax_x)
|
||||||
|
# +X
|
||||||
|
_face(f, x1, y0, z0, x1, y0, z1, x1, y1, z0, ax_x)
|
||||||
|
# -Y
|
||||||
|
_face(f, x0, y0, z1, x1, y0, z1, x0, y0, z0, ax_y)
|
||||||
|
# +Y
|
||||||
|
_face(f, x0, y1, z0, x1, y1, z0, x0, y1, z1, ax_y)
|
||||||
|
# -Z
|
||||||
|
_face(f, x0, y0, z0, x1, y0, z0, x0, y1, z0, ax_z)
|
||||||
|
# +Z
|
||||||
|
_face(f, x0, y0, z1, x0, y1, z1, x1, y0, z1, ax_z)
|
||||||
|
f.store_string("}\n")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
func _face(f: FileAccess, x1: int, y1: int, z1: int,
|
||||||
|
x2: int, y2: int, z2: int,
|
||||||
|
x3: int, y3: int, z3: int, axes: String) -> void:
|
||||||
|
f.store_string("( %d %d %d ) ( %d %d %d ) ( %d %d %d ) %s %s 0 1 1\n" % [
|
||||||
|
x1, y1, z1, x2, y2, z2, x3, y3, z3, TEXTURE, axes,
|
||||||
|
])
|
||||||
1
demo/scripts/export_map.gd.uid
Normal file
1
demo/scripts/export_map.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://braurh5w6hjt2
|
||||||
61
demo/scripts/fly_camera.gd
Normal file
61
demo/scripts/fly_camera.gd
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
class_name FlyCamera
|
||||||
|
extends Camera3D
|
||||||
|
|
||||||
|
# WASD + QE + mouse look fly camera. No collision — you fly through walls
|
||||||
|
# on purpose, so you can inspect the dungeon from any angle.
|
||||||
|
|
||||||
|
@export var move_speed := 8.0
|
||||||
|
@export var boost_multiplier := 3.0
|
||||||
|
@export var mouse_sensitivity := 0.002
|
||||||
|
|
||||||
|
var _yaw := 0.0
|
||||||
|
var _pitch := 0.0
|
||||||
|
var _captured := false
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_capture()
|
||||||
|
_yaw = rotation.y
|
||||||
|
_pitch = rotation.x
|
||||||
|
|
||||||
|
func _capture() -> void:
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||||
|
_captured = true
|
||||||
|
|
||||||
|
func _release() -> void:
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
_captured = false
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseMotion and _captured:
|
||||||
|
var m := event as InputEventMouseMotion
|
||||||
|
_yaw -= m.relative.x * mouse_sensitivity
|
||||||
|
_pitch -= m.relative.y * mouse_sensitivity
|
||||||
|
_pitch = clamp(_pitch, deg_to_rad(-85.0), deg_to_rad(85.0))
|
||||||
|
rotation = Vector3(_pitch, _yaw, 0.0)
|
||||||
|
elif event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||||
|
_release()
|
||||||
|
elif event is InputEventMouseButton and event.pressed and not _captured:
|
||||||
|
_capture()
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
var input := Vector3.ZERO
|
||||||
|
if Input.is_key_pressed(KEY_W): input.z -= 1.0
|
||||||
|
if Input.is_key_pressed(KEY_S): input.z += 1.0
|
||||||
|
if Input.is_key_pressed(KEY_A): input.x -= 1.0
|
||||||
|
if Input.is_key_pressed(KEY_D): input.x += 1.0
|
||||||
|
if Input.is_key_pressed(KEY_E): input.y += 1.0
|
||||||
|
if Input.is_key_pressed(KEY_Q): input.y -= 1.0
|
||||||
|
|
||||||
|
var speed := move_speed
|
||||||
|
if Input.is_key_pressed(KEY_SHIFT):
|
||||||
|
speed *= boost_multiplier
|
||||||
|
|
||||||
|
if input == Vector3.ZERO:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Horizontal movement is yaw-relative; vertical stays in world space.
|
||||||
|
var yaw_basis := Basis(Vector3.UP, _yaw)
|
||||||
|
var dir := yaw_basis * Vector3(input.x, 0.0, input.z)
|
||||||
|
dir.y = input.y
|
||||||
|
dir = dir.normalized()
|
||||||
|
position += dir * speed * delta
|
||||||
1
demo/scripts/fly_camera.gd.uid
Normal file
1
demo/scripts/fly_camera.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://d0srrm35g1m0t
|
||||||
123
demo/scripts/fov.gd
Normal file
123
demo/scripts/fov.gd
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
class_name BrogueFOV
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
# Symmetric recursive shadowcasting, tile-accurate FOV for 2D grids.
|
||||||
|
#
|
||||||
|
# Port of the standard 8-octant algorithm (Bjoern Bergstrom / Adam Milazzo's
|
||||||
|
# symmetric variant). In ~100 lines of pure GDScript.
|
||||||
|
#
|
||||||
|
# Properties:
|
||||||
|
# - Tile-accurate: every cell is either fully visible or not visible.
|
||||||
|
# - Symmetric: if A sees B, B sees A. Critical for fair AI awareness.
|
||||||
|
# - Circular radius clipping (euclidean distance).
|
||||||
|
# - Opaque = walls, T_NOTHING, and unbridged liquid (lava/chasm/brimstone).
|
||||||
|
# Water is transparent (you can see across water).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# var vis := BrogueFOV.compute(grid, Vector2i(39, 14), 8)
|
||||||
|
# for y in grid["height"]:
|
||||||
|
# for x in grid["width"]:
|
||||||
|
# if vis[y * grid["width"] + x] == 1:
|
||||||
|
# draw_cell(x, y)
|
||||||
|
|
||||||
|
# Must match src/gen/grid.h:terrain_t
|
||||||
|
const T_NOTHING := 0
|
||||||
|
const T_FLOOR := 1
|
||||||
|
const T_WALL := 2
|
||||||
|
const T_DOOR := 3
|
||||||
|
const T_CORRIDOR := 4
|
||||||
|
const T_LIQUID := 5
|
||||||
|
const T_BRIDGE := 6
|
||||||
|
const T_STAIRS_UP := 7
|
||||||
|
const T_STAIRS_DOWN := 8
|
||||||
|
|
||||||
|
const L_WATER := 1
|
||||||
|
|
||||||
|
# Octant transforms: each octant maps (row, col) of the shadowcasting loop
|
||||||
|
# to grid deltas. 8 octants cover all directions symmetrically.
|
||||||
|
const OCTANTS := [
|
||||||
|
Vector4i( 1, 0, 0, 1),
|
||||||
|
Vector4i( 0, 1, 1, 0),
|
||||||
|
Vector4i( 0, -1, 1, 0),
|
||||||
|
Vector4i(-1, 0, 0, 1),
|
||||||
|
Vector4i(-1, 0, 0, -1),
|
||||||
|
Vector4i( 0, -1, -1, 0),
|
||||||
|
Vector4i( 0, 1, -1, 0),
|
||||||
|
Vector4i( 1, 0, 0, -1),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Returns PackedByteArray of width*height, row-major, 1 = visible.
|
||||||
|
static func compute(grid: Dictionary, origin: Vector2i, radius: int) -> PackedByteArray:
|
||||||
|
var w: int = grid["width"]
|
||||||
|
var h: int = grid["height"]
|
||||||
|
var out := PackedByteArray()
|
||||||
|
out.resize(w * h)
|
||||||
|
for i in range(w * h):
|
||||||
|
out[i] = 0
|
||||||
|
|
||||||
|
if origin.x < 0 or origin.y < 0 or origin.x >= w or origin.y >= h:
|
||||||
|
return out
|
||||||
|
|
||||||
|
# Origin is always visible.
|
||||||
|
out[origin.y * w + origin.x] = 1
|
||||||
|
|
||||||
|
var terrain: PackedByteArray = grid["terrain"]
|
||||||
|
var liquid: PackedByteArray = grid["liquid"]
|
||||||
|
|
||||||
|
for oct in OCTANTS:
|
||||||
|
_cast_octant(out, terrain, liquid, origin, radius, w, h,
|
||||||
|
1, 1.0, 0.0, oct)
|
||||||
|
return out
|
||||||
|
|
||||||
|
static func _is_opaque(terrain: PackedByteArray, liquid: PackedByteArray,
|
||||||
|
x: int, y: int, w: int) -> bool:
|
||||||
|
var idx := y * w + x
|
||||||
|
var t: int = terrain[idx]
|
||||||
|
if t == T_WALL or t == T_NOTHING:
|
||||||
|
return true
|
||||||
|
if t == T_LIQUID:
|
||||||
|
# Water is transparent; lava / chasm / brimstone are opaque.
|
||||||
|
return liquid[idx] != L_WATER
|
||||||
|
return false
|
||||||
|
|
||||||
|
static func _cast_octant(out: PackedByteArray,
|
||||||
|
terrain: PackedByteArray, liquid: PackedByteArray,
|
||||||
|
origin: Vector2i, radius: int, w: int, h: int,
|
||||||
|
row: int, start_slope: float, end_slope: float,
|
||||||
|
oct: Vector4i) -> void:
|
||||||
|
if start_slope < end_slope:
|
||||||
|
return
|
||||||
|
var r2: int = radius * radius
|
||||||
|
var new_start := start_slope
|
||||||
|
var blocked := false
|
||||||
|
for i in range(row, radius + 1):
|
||||||
|
if blocked:
|
||||||
|
break
|
||||||
|
for dy in range(-i, 1):
|
||||||
|
var dx := -i
|
||||||
|
while dx <= 0:
|
||||||
|
var tx: int = origin.x + dx * oct.x + dy * oct.y
|
||||||
|
var ty: int = origin.y + dx * oct.z + dy * oct.w
|
||||||
|
var l_slope := (dx - 0.5) / (dy + 0.5)
|
||||||
|
var r_slope := (dx + 0.5) / (dy - 0.5)
|
||||||
|
if start_slope < r_slope:
|
||||||
|
dx += 1
|
||||||
|
continue
|
||||||
|
if end_slope > l_slope:
|
||||||
|
break
|
||||||
|
if tx >= 0 and tx < w and ty >= 0 and ty < h:
|
||||||
|
if dx * dx + dy * dy <= r2:
|
||||||
|
out[ty * w + tx] = 1
|
||||||
|
if blocked:
|
||||||
|
if _is_opaque(terrain, liquid, tx, ty, w):
|
||||||
|
new_start = r_slope
|
||||||
|
else:
|
||||||
|
blocked = false
|
||||||
|
start_slope = new_start
|
||||||
|
else:
|
||||||
|
if _is_opaque(terrain, liquid, tx, ty, w) and i < radius:
|
||||||
|
blocked = true
|
||||||
|
_cast_octant(out, terrain, liquid, origin, radius, w, h,
|
||||||
|
i + 1, start_slope, l_slope, oct)
|
||||||
|
new_start = r_slope
|
||||||
|
dx += 1
|
||||||
1
demo/scripts/fov.gd.uid
Normal file
1
demo/scripts/fov.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://d0ypl6iuo62ox
|
||||||
18
demo/scripts/fps_overlay.gd
Normal file
18
demo/scripts/fps_overlay.gd
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
# Cheap FPS + stats overlay. Attach as a child of any scene.
|
||||||
|
# The parent scene can call set_subtitle(text) to add a line below the FPS.
|
||||||
|
|
||||||
|
@onready var label: Label = $Label
|
||||||
|
var _subtitle := ""
|
||||||
|
|
||||||
|
func set_subtitle(text: String) -> void:
|
||||||
|
_subtitle = text
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
var fps := Engine.get_frames_per_second()
|
||||||
|
var frame_ms := 0.0 if fps <= 0 else 1000.0 / fps
|
||||||
|
var txt := "FPS: %d %.2f ms" % [fps, frame_ms]
|
||||||
|
if _subtitle != "":
|
||||||
|
txt += "\n" + _subtitle
|
||||||
|
label.text = txt
|
||||||
1
demo/scripts/fps_overlay.gd.uid
Normal file
1
demo/scripts/fps_overlay.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://chq0nb6xd2e5w
|
||||||
3
demo/scripts/game_root.gd
Normal file
3
demo/scripts/game_root.gd
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
@warning_ignore("empty_file")
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
1
demo/scripts/game_root.gd.uid
Normal file
1
demo/scripts/game_root.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://yh36kisxu2q3
|
||||||
199
demo/scripts/mesh_library_builder.gd
Normal file
199
demo/scripts/mesh_library_builder.gd
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
class_name MeshLibraryBuilder
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
# Builds a MeshLibrary at runtime for the 3D dungeon demo. No art assets —
|
||||||
|
# BoxMesh / PlaneMesh primitives with tinted StandardMaterial3D. Each tile
|
||||||
|
# ID here matches a terrain enum the renderer cares about.
|
||||||
|
|
||||||
|
# Tile IDs (exposed as constants so demo_3d.gd can pass them to
|
||||||
|
# GridMap.set_cell_item).
|
||||||
|
const TILE_FLOOR := 0
|
||||||
|
const TILE_WALL := 1
|
||||||
|
const TILE_DOOR := 2
|
||||||
|
const TILE_CORRIDOR := 3
|
||||||
|
const TILE_WATER := 4
|
||||||
|
const TILE_LAVA := 5
|
||||||
|
const TILE_BRIMSTONE := 6
|
||||||
|
const TILE_BRIDGE := 7
|
||||||
|
const TILE_STAIRS_UP := 8
|
||||||
|
const TILE_STAIRS_DOWN := 9
|
||||||
|
|
||||||
|
# Floors and liquid surfaces are thin slabs centered at the cell origin.
|
||||||
|
# Walls are tall enough that the player (camera ~2.6u) can't see over them.
|
||||||
|
# Doors are chest-height so they read as entry markers without blocking view
|
||||||
|
# along corridors — can raise to full height once we have swinging doors.
|
||||||
|
const CELL_SIZE := 1.0
|
||||||
|
const WALL_HEIGHT := 3.0
|
||||||
|
const FLOOR_THICKNESS := 0.05
|
||||||
|
const LIQUID_THICKNESS := 0.05
|
||||||
|
const DOOR_HEIGHT := 0.8
|
||||||
|
const BRIDGE_THICKNESS := 0.1
|
||||||
|
const STAIRS_HEIGHT := 0.7
|
||||||
|
|
||||||
|
static func build(with_collision: bool = true) -> MeshLibrary:
|
||||||
|
var lib := MeshLibrary.new()
|
||||||
|
_add_slab(lib, TILE_FLOOR, "Floor", _mat(Color(0.64, 0.54, 0.42)), FLOOR_THICKNESS, 0.0)
|
||||||
|
_add_cube(lib, TILE_WALL, "Wall", _mat(Color(0.28, 0.28, 0.32)), WALL_HEIGHT, WALL_HEIGHT * 0.5)
|
||||||
|
_add_cube(lib, TILE_DOOR, "Door", _mat(Color(0.72, 0.36, 0.20)), DOOR_HEIGHT, DOOR_HEIGHT * 0.5)
|
||||||
|
_add_slab(lib, TILE_CORRIDOR, "Corridor", _mat(Color(0.44, 0.37, 0.29)), FLOOR_THICKNESS, 0.0)
|
||||||
|
_add_slab(lib, TILE_WATER, "Water", _mat_trans(Color(0.18, 0.42, 0.78, 0.75)), LIQUID_THICKNESS, -0.05)
|
||||||
|
_add_slab(lib, TILE_LAVA, "Lava", _mat_emissive(Color(0.90, 0.30, 0.12), Color(1.00, 0.45, 0.15), 1.5), LIQUID_THICKNESS, -0.05)
|
||||||
|
_add_slab(lib, TILE_BRIMSTONE, "Brimstone", _mat_emissive(Color(0.72, 0.50, 0.22), Color(0.95, 0.65, 0.25), 0.8), LIQUID_THICKNESS, -0.05)
|
||||||
|
_add_slab(lib, TILE_BRIDGE, "Bridge", _mat(Color(0.55, 0.38, 0.22)), BRIDGE_THICKNESS, 0.0)
|
||||||
|
_add_ramp(lib, TILE_STAIRS_UP, "StairsUp", _mat(Color(0.74, 0.88, 0.94)), true)
|
||||||
|
_add_ramp(lib, TILE_STAIRS_DOWN, "StairsDown", _mat(Color(0.20, 0.44, 0.80)), false)
|
||||||
|
|
||||||
|
if with_collision:
|
||||||
|
_attach_collision(lib)
|
||||||
|
return lib
|
||||||
|
|
||||||
|
# Bake the library to a .tres so the CLI and runtime can share the same asset
|
||||||
|
# instead of rebuilding it each time a scene loads.
|
||||||
|
static func save_resource(path: String, with_collision: bool = true) -> Error:
|
||||||
|
return ResourceSaver.save(build(with_collision), path)
|
||||||
|
|
||||||
|
# --- collision shapes ---
|
||||||
|
|
||||||
|
# Every tile except chasm (which has no tile at all) gets a box collider
|
||||||
|
# sized to match its mesh. Chasms therefore let the player fall through
|
||||||
|
# cleanly onto the level below. Stairs are treated as solid blocks; walking
|
||||||
|
# onto them works well enough with gravity + move_and_slide.
|
||||||
|
static func _attach_collision(lib: MeshLibrary) -> void:
|
||||||
|
# Shape, y_offset pairs, in the same geometry as the meshes above.
|
||||||
|
var floor_shape := _box_shape(Vector3(CELL_SIZE, FLOOR_THICKNESS, CELL_SIZE))
|
||||||
|
var wall_shape := _box_shape(Vector3(CELL_SIZE, WALL_HEIGHT, CELL_SIZE))
|
||||||
|
var door_shape := _box_shape(Vector3(CELL_SIZE, DOOR_HEIGHT, CELL_SIZE))
|
||||||
|
var liquid_shape := _box_shape(Vector3(CELL_SIZE, LIQUID_THICKNESS, CELL_SIZE))
|
||||||
|
var bridge_shape := _box_shape(Vector3(CELL_SIZE, BRIDGE_THICKNESS, CELL_SIZE))
|
||||||
|
var stairs_shape := _box_shape(Vector3(CELL_SIZE, STAIRS_HEIGHT, CELL_SIZE))
|
||||||
|
|
||||||
|
_set_shape(lib, TILE_FLOOR, floor_shape, FLOOR_THICKNESS * 0.5)
|
||||||
|
_set_shape(lib, TILE_WALL, wall_shape, WALL_HEIGHT * 0.5)
|
||||||
|
_set_shape(lib, TILE_DOOR, door_shape, DOOR_HEIGHT * 0.5)
|
||||||
|
_set_shape(lib, TILE_CORRIDOR, floor_shape, FLOOR_THICKNESS * 0.5)
|
||||||
|
_set_shape(lib, TILE_WATER, liquid_shape, -0.05 + LIQUID_THICKNESS * 0.5)
|
||||||
|
_set_shape(lib, TILE_LAVA, liquid_shape, -0.05 + LIQUID_THICKNESS * 0.5)
|
||||||
|
_set_shape(lib, TILE_BRIMSTONE, liquid_shape, -0.05 + LIQUID_THICKNESS * 0.5)
|
||||||
|
_set_shape(lib, TILE_BRIDGE, bridge_shape, BRIDGE_THICKNESS * 0.5)
|
||||||
|
_set_shape(lib, TILE_STAIRS_UP, stairs_shape, STAIRS_HEIGHT * 0.5)
|
||||||
|
_set_shape(lib, TILE_STAIRS_DOWN, stairs_shape, STAIRS_HEIGHT * 0.5)
|
||||||
|
|
||||||
|
static func _box_shape(size: Vector3) -> BoxShape3D:
|
||||||
|
var b := BoxShape3D.new()
|
||||||
|
b.size = size
|
||||||
|
return b
|
||||||
|
|
||||||
|
static func _set_shape(lib: MeshLibrary, id: int, shape: Shape3D, y: float) -> void:
|
||||||
|
var t := Transform3D.IDENTITY
|
||||||
|
t.origin = Vector3(0, y, 0)
|
||||||
|
lib.set_item_shapes(id, [shape, t])
|
||||||
|
|
||||||
|
# --- material helpers ---
|
||||||
|
|
||||||
|
static func _mat(base: Color) -> StandardMaterial3D:
|
||||||
|
var m := StandardMaterial3D.new()
|
||||||
|
m.albedo_color = base
|
||||||
|
m.roughness = 0.8
|
||||||
|
m.metallic = 0.0
|
||||||
|
return m
|
||||||
|
|
||||||
|
static func _mat_trans(base: Color) -> StandardMaterial3D:
|
||||||
|
var m := _mat(base)
|
||||||
|
m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
return m
|
||||||
|
|
||||||
|
static func _mat_emissive(base: Color, emis: Color, strength: float) -> StandardMaterial3D:
|
||||||
|
var m := _mat(base)
|
||||||
|
m.emission_enabled = true
|
||||||
|
m.emission = emis
|
||||||
|
m.emission_energy_multiplier = strength
|
||||||
|
return m
|
||||||
|
|
||||||
|
# --- mesh builders ---
|
||||||
|
|
||||||
|
# A flat slab filling the cell's XZ footprint, with the given thickness and
|
||||||
|
# base at y_base (the slab extends UP from y_base by `thickness`).
|
||||||
|
static func _add_slab(lib: MeshLibrary, id: int, name: String,
|
||||||
|
mat: StandardMaterial3D,
|
||||||
|
thickness: float, y_base: float) -> void:
|
||||||
|
var mesh := BoxMesh.new()
|
||||||
|
mesh.size = Vector3(CELL_SIZE, thickness, CELL_SIZE)
|
||||||
|
mesh.material = mat
|
||||||
|
var t := Transform3D.IDENTITY
|
||||||
|
t.origin = Vector3(0, y_base + thickness * 0.5, 0)
|
||||||
|
_install(lib, id, name, mesh, t)
|
||||||
|
|
||||||
|
# A cube centered on the cell. y_center is the Y of the cube's center.
|
||||||
|
static func _add_cube(lib: MeshLibrary, id: int, name: String,
|
||||||
|
mat: StandardMaterial3D,
|
||||||
|
height: float, y_center: float) -> void:
|
||||||
|
var mesh := BoxMesh.new()
|
||||||
|
mesh.size = Vector3(CELL_SIZE, height, CELL_SIZE)
|
||||||
|
mesh.material = mat
|
||||||
|
var t := Transform3D.IDENTITY
|
||||||
|
t.origin = Vector3(0, y_center, 0)
|
||||||
|
_install(lib, id, name, mesh, t)
|
||||||
|
|
||||||
|
# Simple visual ramp using two cubes at different heights. Not geometrically
|
||||||
|
# correct stairs — just enough to read as "stairs" from flight.
|
||||||
|
# going_up=true means the tall step is at +Z; false means the tall step is
|
||||||
|
# at -Z (visually going down).
|
||||||
|
static func _add_ramp(lib: MeshLibrary, id: int, name: String,
|
||||||
|
mat: StandardMaterial3D, going_up: bool) -> void:
|
||||||
|
# GridMap.set_mesh expects a single Mesh. Fake the steps by stacking two
|
||||||
|
# BoxMeshes into an ArrayMesh via SurfaceTool.
|
||||||
|
var st := SurfaceTool.new()
|
||||||
|
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||||
|
st.set_material(mat)
|
||||||
|
|
||||||
|
var tall := STAIRS_HEIGHT
|
||||||
|
var short := STAIRS_HEIGHT * 0.35
|
||||||
|
var half := CELL_SIZE * 0.25
|
||||||
|
|
||||||
|
# Step 1: back (taller going up, shorter going down).
|
||||||
|
var z_back := half if going_up else -half
|
||||||
|
_append_box(st, Vector3(0, (tall if going_up else short) * 0.5, z_back),
|
||||||
|
Vector3(CELL_SIZE, (tall if going_up else short), CELL_SIZE * 0.5))
|
||||||
|
# Step 2: front.
|
||||||
|
var z_front := -half if going_up else half
|
||||||
|
_append_box(st, Vector3(0, (short if going_up else tall) * 0.5, z_front),
|
||||||
|
Vector3(CELL_SIZE, (short if going_up else tall), CELL_SIZE * 0.5))
|
||||||
|
|
||||||
|
var arr := st.commit()
|
||||||
|
_install(lib, id, name, arr, Transform3D.IDENTITY)
|
||||||
|
|
||||||
|
static func _append_box(st: SurfaceTool, center: Vector3, size: Vector3) -> void:
|
||||||
|
var h := size * 0.5
|
||||||
|
var c := center
|
||||||
|
var v := [
|
||||||
|
c + Vector3(-h.x, -h.y, -h.z), # 0
|
||||||
|
c + Vector3( h.x, -h.y, -h.z), # 1
|
||||||
|
c + Vector3( h.x, h.y, -h.z), # 2
|
||||||
|
c + Vector3(-h.x, h.y, -h.z), # 3
|
||||||
|
c + Vector3(-h.x, -h.y, h.z), # 4
|
||||||
|
c + Vector3( h.x, -h.y, h.z), # 5
|
||||||
|
c + Vector3( h.x, h.y, h.z), # 6
|
||||||
|
c + Vector3(-h.x, h.y, h.z), # 7
|
||||||
|
]
|
||||||
|
# 6 faces, each 2 tris, wound CCW with outward normals.
|
||||||
|
var faces := [
|
||||||
|
[0, 1, 2, 3, Vector3(0, 0, -1)],
|
||||||
|
[5, 4, 7, 6, Vector3(0, 0, 1)],
|
||||||
|
[1, 5, 6, 2, Vector3( 1, 0, 0)],
|
||||||
|
[4, 0, 3, 7, Vector3(-1, 0, 0)],
|
||||||
|
[3, 2, 6, 7, Vector3(0, 1, 0)],
|
||||||
|
[4, 5, 1, 0, Vector3(0, -1, 0)],
|
||||||
|
]
|
||||||
|
for f in faces:
|
||||||
|
var n: Vector3 = f[4]
|
||||||
|
for i in [0, 1, 2, 0, 2, 3]:
|
||||||
|
st.set_normal(n)
|
||||||
|
st.add_vertex(v[f[i]])
|
||||||
|
|
||||||
|
# Register a mesh + transform with the library under the given ID.
|
||||||
|
static func _install(lib: MeshLibrary, id: int, name: String,
|
||||||
|
mesh: Mesh, t: Transform3D) -> void:
|
||||||
|
lib.create_item(id)
|
||||||
|
lib.set_item_name(id, name)
|
||||||
|
lib.set_item_mesh(id, mesh)
|
||||||
|
lib.set_item_mesh_transform(id, t)
|
||||||
1
demo/scripts/mesh_library_builder.gd.uid
Normal file
1
demo/scripts/mesh_library_builder.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ccjvjlugb7wpu
|
||||||
69
demo/scripts/player.gd
Normal file
69
demo/scripts/player.gd
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
class_name Player
|
||||||
|
extends CharacterBody3D
|
||||||
|
|
||||||
|
# First-person player controller. WASD relative to body yaw, mouse look
|
||||||
|
# (yaw on the body, pitch on the child Camera3D). Jump on space. Gravity
|
||||||
|
# is always on — falling into a chasm drops you onto the level below.
|
||||||
|
|
||||||
|
@export var speed := 6.0
|
||||||
|
@export var run_multiplier := 1.8
|
||||||
|
@export var jump_velocity := 6.0
|
||||||
|
@export var gravity := 22.0
|
||||||
|
@export var mouse_sensitivity := 0.002
|
||||||
|
|
||||||
|
@onready var camera: Camera3D = $Camera3D
|
||||||
|
|
||||||
|
var _pitch := 0.0
|
||||||
|
var _captured := false
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_capture()
|
||||||
|
|
||||||
|
func _capture() -> void:
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||||
|
_captured = true
|
||||||
|
|
||||||
|
func _release() -> void:
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
_captured = false
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseMotion and _captured:
|
||||||
|
var m := event as InputEventMouseMotion
|
||||||
|
rotation.y -= m.relative.x * mouse_sensitivity
|
||||||
|
_pitch -= m.relative.y * mouse_sensitivity
|
||||||
|
_pitch = clamp(_pitch, deg_to_rad(-85.0), deg_to_rad(85.0))
|
||||||
|
camera.rotation.x = _pitch
|
||||||
|
elif event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||||
|
_release()
|
||||||
|
elif event is InputEventMouseButton and event.pressed and not _captured:
|
||||||
|
_capture()
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
# Gravity.
|
||||||
|
if not is_on_floor():
|
||||||
|
velocity.y -= gravity * delta
|
||||||
|
|
||||||
|
# Horizontal movement relative to yaw.
|
||||||
|
var input := Vector2.ZERO
|
||||||
|
if Input.is_key_pressed(KEY_W): input.y -= 1.0
|
||||||
|
if Input.is_key_pressed(KEY_S): input.y += 1.0
|
||||||
|
if Input.is_key_pressed(KEY_A): input.x -= 1.0
|
||||||
|
if Input.is_key_pressed(KEY_D): input.x += 1.0
|
||||||
|
input = input.normalized()
|
||||||
|
|
||||||
|
var s := speed
|
||||||
|
if Input.is_key_pressed(KEY_SHIFT):
|
||||||
|
s *= run_multiplier
|
||||||
|
|
||||||
|
var forward := -transform.basis.z
|
||||||
|
var right := transform.basis.x
|
||||||
|
var horiz := (forward * -input.y + right * input.x) * s
|
||||||
|
velocity.x = horiz.x
|
||||||
|
velocity.z = horiz.z
|
||||||
|
|
||||||
|
# Jump.
|
||||||
|
if is_on_floor() and Input.is_key_pressed(KEY_SPACE):
|
||||||
|
velocity.y = jump_velocity
|
||||||
|
|
||||||
|
move_and_slide()
|
||||||
1
demo/scripts/player.gd.uid
Normal file
1
demo/scripts/player.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bj1fb7syiqys7
|
||||||
12
demo/stuff/dungeon_seed52_depth100.tscn
Normal file
12
demo/stuff/dungeon_seed52_depth100.tscn
Normal file
File diff suppressed because one or more lines are too long
192
demo/stuff/mesh_library.tres
Normal file
192
demo/stuff/mesh_library.tres
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
[gd_resource type="MeshLibrary" format=4 uid="uid://u7pnhcouyiss"]
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_mxq65"]
|
||||||
|
albedo_color = Color(0.64, 0.54, 0.42, 1)
|
||||||
|
roughness = 0.8
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_nfq1i"]
|
||||||
|
material = SubResource("StandardMaterial3D_mxq65")
|
||||||
|
size = Vector3(1, 0.05, 1)
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_c471q"]
|
||||||
|
size = Vector3(1, 0.05, 1)
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_iuw7j"]
|
||||||
|
albedo_color = Color(0.28, 0.28, 0.32, 1)
|
||||||
|
roughness = 0.8
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_7yf4u"]
|
||||||
|
material = SubResource("StandardMaterial3D_iuw7j")
|
||||||
|
size = Vector3(1, 3, 1)
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_aewbc"]
|
||||||
|
size = Vector3(1, 3, 1)
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_dpcan"]
|
||||||
|
albedo_color = Color(0.72, 0.36, 0.2, 1)
|
||||||
|
roughness = 0.8
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_ba7m6"]
|
||||||
|
material = SubResource("StandardMaterial3D_dpcan")
|
||||||
|
size = Vector3(1, 0.8, 1)
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_pqd2j"]
|
||||||
|
size = Vector3(1, 0.8, 1)
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_7bx42"]
|
||||||
|
albedo_color = Color(0.44, 0.37, 0.29, 1)
|
||||||
|
roughness = 0.8
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_q4frx"]
|
||||||
|
material = SubResource("StandardMaterial3D_7bx42")
|
||||||
|
size = Vector3(1, 0.05, 1)
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_n2rcg"]
|
||||||
|
transparency = 1
|
||||||
|
albedo_color = Color(0.18, 0.42, 0.78, 0.75)
|
||||||
|
roughness = 0.8
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_qowgg"]
|
||||||
|
material = SubResource("StandardMaterial3D_n2rcg")
|
||||||
|
size = Vector3(1, 0.05, 1)
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_n4omc"]
|
||||||
|
size = Vector3(1, 0.05, 1)
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_rdtaw"]
|
||||||
|
albedo_color = Color(0.9, 0.3, 0.12, 1)
|
||||||
|
roughness = 0.8
|
||||||
|
emission_enabled = true
|
||||||
|
emission = Color(1, 0.45, 0.15, 1)
|
||||||
|
emission_energy_multiplier = 1.5
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_bbjka"]
|
||||||
|
material = SubResource("StandardMaterial3D_rdtaw")
|
||||||
|
size = Vector3(1, 0.05, 1)
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_e4bfb"]
|
||||||
|
albedo_color = Color(0.72, 0.5, 0.22, 1)
|
||||||
|
roughness = 0.8
|
||||||
|
emission_enabled = true
|
||||||
|
emission = Color(0.95, 0.65, 0.25, 1)
|
||||||
|
emission_energy_multiplier = 0.8
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_r1428"]
|
||||||
|
material = SubResource("StandardMaterial3D_e4bfb")
|
||||||
|
size = Vector3(1, 0.05, 1)
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_04bo8"]
|
||||||
|
albedo_color = Color(0.55, 0.38, 0.22, 1)
|
||||||
|
roughness = 0.8
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_qyws6"]
|
||||||
|
material = SubResource("StandardMaterial3D_04bo8")
|
||||||
|
size = Vector3(1, 0.1, 1)
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_ym5kt"]
|
||||||
|
size = Vector3(1, 0.1, 1)
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_nv4rv"]
|
||||||
|
albedo_color = Color(0.74, 0.88, 0.94, 1)
|
||||||
|
roughness = 0.8
|
||||||
|
|
||||||
|
[sub_resource type="ArrayMesh" id="ArrayMesh_xoewy"]
|
||||||
|
_surfaces = [{
|
||||||
|
"aabb": AABB(-0.5, 0, -0.5, 1, 0.7, 1),
|
||||||
|
"format": 34359738375,
|
||||||
|
"material": SubResource("StandardMaterial3D_nv4rv"),
|
||||||
|
"primitive": 3,
|
||||||
|
"uv_scale": Vector4(0, 0, 0, 0),
|
||||||
|
"vertex_count": 72,
|
||||||
|
"vertex_data": PackedByteArray("AAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAPzMzMz8AAAAAAAAAvwAAAAAAAAAAAAAAPzMzMz8AAAAAAAAAvzMzMz8AAAAAAAAAPwAAAAAAAAA/AAAAvwAAAAAAAAA/AAAAvzMzMz8AAAA/AAAAPwAAAAAAAAA/AAAAvzMzMz8AAAA/AAAAPzMzMz8AAAA/AAAAPwAAAAAAAAAAAAAAPwAAAAAAAAA/AAAAPzMzMz8AAAA/AAAAPwAAAAAAAAAAAAAAPzMzMz8AAAA/AAAAPzMzMz8AAAAAAAAAvwAAAAAAAAA/AAAAvwAAAAAAAAAAAAAAvzMzMz8AAAAAAAAAvwAAAAAAAAA/AAAAvzMzMz8AAAAAAAAAvzMzMz8AAAA/AAAAvzMzMz8AAAAAAAAAPzMzMz8AAAAAAAAAPzMzMz8AAAA/AAAAvzMzMz8AAAAAAAAAPzMzMz8AAAA/AAAAvzMzMz8AAAA/AAAAvwAAAAAAAAA/AAAAPwAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAAAAAAAvwAAAAAAAAC/AAAAPwAAAAAAAAC/AAAAP0jhej4AAAC/AAAAvwAAAAAAAAC/AAAAP0jhej4AAAC/AAAAv0jhej4AAAC/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAAAAAAAv0jhej4AAAAAAAAAPwAAAAAAAAAAAAAAv0jhej4AAAAAAAAAP0jhej4AAAAAAAAAPwAAAAAAAAC/AAAAPwAAAAAAAAAAAAAAP0jhej4AAAAAAAAAPwAAAAAAAAC/AAAAP0jhej4AAAAAAAAAP0jhej4AAAC/AAAAvwAAAAAAAAAAAAAAvwAAAAAAAAC/AAAAv0jhej4AAAC/AAAAvwAAAAAAAAAAAAAAv0jhej4AAAC/AAAAv0jhej4AAAAAAAAAv0jhej4AAAC/AAAAP0jhej4AAAC/AAAAP0jhej4AAAAAAAAAv0jhej4AAAC/AAAAP0jhej4AAAAAAAAAv0jhej4AAAAAAAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAPwAAAAAAAAC/AAAAvwAAAAAAAAAAAAAAPwAAAAAAAAC/AAAAvwAAAAAAAAC///////9/AID//////38AgP//////fwCA//////9/AID//////38AgP//////fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA////f/9//7////9//3//v////3//f/+/////f/9//7////9//3//v////3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+///////9/AID//////38AgP//////fwCA//////9/AID//////38AgP//////fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA////f/9//7////9//3//v////3//f/+/////f/9//7////9//3//v////3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+/")
|
||||||
|
}]
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_ksa0k"]
|
||||||
|
size = Vector3(1, 0.7, 1)
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_wq4fl"]
|
||||||
|
albedo_color = Color(0.2, 0.44, 0.8, 1)
|
||||||
|
roughness = 0.8
|
||||||
|
|
||||||
|
[sub_resource type="ArrayMesh" id="ArrayMesh_fk0ts"]
|
||||||
|
_surfaces = [{
|
||||||
|
"aabb": AABB(-0.5, 0, -0.5, 1, 0.7, 1),
|
||||||
|
"format": 34359738375,
|
||||||
|
"material": SubResource("StandardMaterial3D_wq4fl"),
|
||||||
|
"primitive": 3,
|
||||||
|
"uv_scale": Vector4(0, 0, 0, 0),
|
||||||
|
"vertex_count": 72,
|
||||||
|
"vertex_data": PackedByteArray("AAAAvwAAAAAAAAC/AAAAPwAAAAAAAAC/AAAAP0jhej4AAAC/AAAAvwAAAAAAAAC/AAAAP0jhej4AAAC/AAAAv0jhej4AAAC/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAAAAAAAv0jhej4AAAAAAAAAPwAAAAAAAAAAAAAAv0jhej4AAAAAAAAAP0jhej4AAAAAAAAAPwAAAAAAAAC/AAAAPwAAAAAAAAAAAAAAP0jhej4AAAAAAAAAPwAAAAAAAAC/AAAAP0jhej4AAAAAAAAAP0jhej4AAAC/AAAAvwAAAAAAAAAAAAAAvwAAAAAAAAC/AAAAv0jhej4AAAC/AAAAvwAAAAAAAAAAAAAAv0jhej4AAAC/AAAAv0jhej4AAAAAAAAAv0jhej4AAAC/AAAAP0jhej4AAAC/AAAAP0jhej4AAAAAAAAAv0jhej4AAAC/AAAAP0jhej4AAAAAAAAAv0jhej4AAAAAAAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAPwAAAAAAAAC/AAAAvwAAAAAAAAAAAAAAPwAAAAAAAAC/AAAAvwAAAAAAAAC/AAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAPzMzMz8AAAAAAAAAvwAAAAAAAAAAAAAAPzMzMz8AAAAAAAAAvzMzMz8AAAAAAAAAPwAAAAAAAAA/AAAAvwAAAAAAAAA/AAAAvzMzMz8AAAA/AAAAPwAAAAAAAAA/AAAAvzMzMz8AAAA/AAAAPzMzMz8AAAA/AAAAPwAAAAAAAAAAAAAAPwAAAAAAAAA/AAAAPzMzMz8AAAA/AAAAPwAAAAAAAAAAAAAAPzMzMz8AAAA/AAAAPzMzMz8AAAAAAAAAvwAAAAAAAAA/AAAAvwAAAAAAAAAAAAAAvzMzMz8AAAAAAAAAvwAAAAAAAAA/AAAAvzMzMz8AAAAAAAAAvzMzMz8AAAA/AAAAvzMzMz8AAAAAAAAAPzMzMz8AAAAAAAAAPzMzMz8AAAA/AAAAvzMzMz8AAAAAAAAAPzMzMz8AAAA/AAAAvzMzMz8AAAA/AAAAvwAAAAAAAAA/AAAAPwAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAAA//////9/AID//////38AgP//////fwCA//////9/AID//////38AgP//////fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA////f/9//7////9//3//v////3//f/+/////f/9//7////9//3//v////3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+///////9/AID//////38AgP//////fwCA//////9/AID//////38AgP//////fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA////f/9//7////9//3//v////3//f/+/////f/9//7////9//3//v////3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+/")
|
||||||
|
}]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
item/0/name = "Floor"
|
||||||
|
item/0/mesh = SubResource("BoxMesh_nfq1i")
|
||||||
|
item/0/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.025, 0)
|
||||||
|
item/0/mesh_cast_shadow = 1
|
||||||
|
item/0/shapes = [SubResource("BoxShape3D_c471q"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.025, 0)]
|
||||||
|
item/0/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
|
||||||
|
item/0/navigation_layers = 1
|
||||||
|
item/1/name = "Wall"
|
||||||
|
item/1/mesh = SubResource("BoxMesh_7yf4u")
|
||||||
|
item/1/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
|
||||||
|
item/1/mesh_cast_shadow = 1
|
||||||
|
item/1/shapes = [SubResource("BoxShape3D_aewbc"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)]
|
||||||
|
item/1/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
|
||||||
|
item/1/navigation_layers = 1
|
||||||
|
item/2/name = "Door"
|
||||||
|
item/2/mesh = SubResource("BoxMesh_ba7m6")
|
||||||
|
item/2/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.4, 0)
|
||||||
|
item/2/mesh_cast_shadow = 1
|
||||||
|
item/2/shapes = [SubResource("BoxShape3D_pqd2j"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.4, 0)]
|
||||||
|
item/2/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
|
||||||
|
item/2/navigation_layers = 1
|
||||||
|
item/3/name = "Corridor"
|
||||||
|
item/3/mesh = SubResource("BoxMesh_q4frx")
|
||||||
|
item/3/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.025, 0)
|
||||||
|
item/3/mesh_cast_shadow = 1
|
||||||
|
item/3/shapes = [SubResource("BoxShape3D_c471q"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.025, 0)]
|
||||||
|
item/3/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
|
||||||
|
item/3/navigation_layers = 1
|
||||||
|
item/4/name = "Water"
|
||||||
|
item/4/mesh = SubResource("BoxMesh_qowgg")
|
||||||
|
item/4/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)
|
||||||
|
item/4/mesh_cast_shadow = 1
|
||||||
|
item/4/shapes = [SubResource("BoxShape3D_n4omc"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)]
|
||||||
|
item/4/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
|
||||||
|
item/4/navigation_layers = 1
|
||||||
|
item/5/name = "Lava"
|
||||||
|
item/5/mesh = SubResource("BoxMesh_bbjka")
|
||||||
|
item/5/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)
|
||||||
|
item/5/mesh_cast_shadow = 1
|
||||||
|
item/5/shapes = [SubResource("BoxShape3D_n4omc"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)]
|
||||||
|
item/5/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
|
||||||
|
item/5/navigation_layers = 1
|
||||||
|
item/6/name = "Brimstone"
|
||||||
|
item/6/mesh = SubResource("BoxMesh_r1428")
|
||||||
|
item/6/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)
|
||||||
|
item/6/mesh_cast_shadow = 1
|
||||||
|
item/6/shapes = [SubResource("BoxShape3D_n4omc"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)]
|
||||||
|
item/6/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
|
||||||
|
item/6/navigation_layers = 1
|
||||||
|
item/7/name = "Bridge"
|
||||||
|
item/7/mesh = SubResource("BoxMesh_qyws6")
|
||||||
|
item/7/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0)
|
||||||
|
item/7/mesh_cast_shadow = 1
|
||||||
|
item/7/shapes = [SubResource("BoxShape3D_ym5kt"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0)]
|
||||||
|
item/7/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
|
||||||
|
item/7/navigation_layers = 1
|
||||||
|
item/8/name = "StairsUp"
|
||||||
|
item/8/mesh = SubResource("ArrayMesh_xoewy")
|
||||||
|
item/8/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
|
||||||
|
item/8/mesh_cast_shadow = 1
|
||||||
|
item/8/shapes = [SubResource("BoxShape3D_ksa0k"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.35, 0)]
|
||||||
|
item/8/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
|
||||||
|
item/8/navigation_layers = 1
|
||||||
|
item/9/name = "StairsDown"
|
||||||
|
item/9/mesh = SubResource("ArrayMesh_fk0ts")
|
||||||
|
item/9/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
|
||||||
|
item/9/mesh_cast_shadow = 1
|
||||||
|
item/9/shapes = [SubResource("BoxShape3D_ksa0k"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.35, 0)]
|
||||||
|
item/9/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
|
||||||
|
item/9/navigation_layers = 1
|
||||||
330
godot/README.md
Normal file
330
godot/README.md
Normal file
|
|
@ -0,0 +1,330 @@
|
||||||
|
# brogue_gen — Godot GDExtension API Reference
|
||||||
|
|
||||||
|
Brogue-style dungeon generator for Godot 4.3+. Pure C99 generator under the hood; Godot-native Dictionary interface on top.
|
||||||
|
|
||||||
|
**Scope.** Map generation only. One class, one method. Call `BrogueGen.generate(seed, depth)` and get back everything you need to spawn a level: terrain grid, liquids, markers, rooms, machines, chokepoints, stairs. Gameplay is yours to write.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Project layout](#project-layout)
|
||||||
|
- [Class `BrogueGen`](#class-broguegen)
|
||||||
|
- [Returned dictionary schema](#returned-dictionary-schema)
|
||||||
|
- [Data structures](#data-structures)
|
||||||
|
- [Enums](#enums)
|
||||||
|
- [Usage pattern](#usage-pattern)
|
||||||
|
- [FOV reference (`BrogueFOV`)](#fov-reference)
|
||||||
|
- [Determinism guarantees](#determinism-guarantees)
|
||||||
|
- [Build and rebuild](#build-and-rebuild)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Drop the `addons/brogue_gen/` directory into any Godot 4.3+ project:
|
||||||
|
|
||||||
|
```
|
||||||
|
your_project/
|
||||||
|
└── addons/
|
||||||
|
└── brogue_gen/
|
||||||
|
├── brogue_gen.gdextension ← registered by Godot scan
|
||||||
|
└── bin/
|
||||||
|
└── libbrogue_gen.linux.template_debug.x86_64.so
|
||||||
|
```
|
||||||
|
|
||||||
|
Re-open the project once so Godot scans and registers the extension. `BrogueGen` then appears in **Create New Node** and `class_name` auto-complete.
|
||||||
|
|
||||||
|
**Compatibility:** The `.gdextension` manifest declares `compatibility_minimum = "4.3"`. The extension is built against `godot-cpp` 4.5 and loads in any Godot 4.3+, including 4.6.
|
||||||
|
|
||||||
|
**Linux x86_64 only for now.** Windows / macOS require building additional targets and adding paths to the `[libraries]` section of the `.gdextension` manifest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
```
|
||||||
|
brogue-genesis/
|
||||||
|
├── src/ pure C99 generator (no Godot, no SDL)
|
||||||
|
│ └── gen/ room carving, lakes, machines, chokepoints, ...
|
||||||
|
├── godot/ GDExtension wrapper
|
||||||
|
│ ├── godot-cpp/ submodule, branch 4.5
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── register_types.* module init
|
||||||
|
│ │ ├── brogue_gen.* BrogueGen class
|
||||||
|
│ │ └── grid_to_dict.* shared grid → Dictionary serializer
|
||||||
|
│ ├── SConstruct build script
|
||||||
|
│ └── brogue_gen.gdextension source-of-truth manifest
|
||||||
|
└── demo/ reference Godot 4.6 project
|
||||||
|
├── addons/brogue_gen/ ← build output
|
||||||
|
│ ├── brogue_gen.gdextension
|
||||||
|
│ └── bin/libbrogue_gen...so
|
||||||
|
├── project.godot
|
||||||
|
├── scenes/
|
||||||
|
└── scripts/
|
||||||
|
```
|
||||||
|
|
||||||
|
Build with `make godot` from the repo root.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Class `BrogueGen`
|
||||||
|
|
||||||
|
`extends Node`
|
||||||
|
|
||||||
|
One-shot blocking dungeon generator. Instantiate, call `generate()`, free.
|
||||||
|
|
||||||
|
Typical generation time on the reference machine: **under 20 ms per level** including machines. Safe to call synchronously in `_ready()`.
|
||||||
|
|
||||||
|
### `generate(seed: int, depth: int) -> Dictionary`
|
||||||
|
|
||||||
|
Runs the full pipeline (accretion → loops → lakes → wreaths → bridges → walls → stairs → chokepoints → machines) and returns a Dictionary describing the final dungeon.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
| name | type | range | notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `seed` | int | any 64-bit | same seed + depth always yields identical output |
|
||||||
|
| `depth` | int | 1–26 | influences liquid type weights |
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
var gen := BrogueGen.new()
|
||||||
|
var grid: Dictionary = gen.generate(2026, 1)
|
||||||
|
gen.free()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Returned dictionary schema
|
||||||
|
|
||||||
|
| key | type | notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `seed` | `int` | echoed back |
|
||||||
|
| `depth` | `int` | 1..26 |
|
||||||
|
| `width` | `int` | always `79` |
|
||||||
|
| `height` | `int` | always `29` |
|
||||||
|
| `terrain` | `PackedByteArray` | length `width * height`, row-major. Index = `y * width + x`. Values in [terrain enum](#terrain-enum-terrain-byte). |
|
||||||
|
| `liquid` | `PackedByteArray` | same layout. Values in [liquid enum](#liquid-enum). Meaningful only when `terrain[i] == T_LIQUID`. |
|
||||||
|
| `surface` | `PackedByteArray` | semantic marker byte. Values in [marker enum](#marker-enum). `MK_NONE` for un-marked cells. |
|
||||||
|
| `flags` | `PackedInt32Array` | bitmask of [cell flags](#cell-flags). |
|
||||||
|
| `room_id` | `PackedByteArray` | 1–255 inside a room, `0` elsewhere. |
|
||||||
|
| `machine_id` | `PackedByteArray` | 1–255 inside a machine, `0` elsewhere. |
|
||||||
|
| `stairs_up` | `Vector2i` | always valid grid coords; `(-1, -1)` only on degenerate seeds. |
|
||||||
|
| `stairs_down` | `Vector2i` | farthest reachable floor from `stairs_up` (BFS). |
|
||||||
|
| `rooms` | `Array[Dictionary]` | see [room dictionary](#room-dictionary). |
|
||||||
|
| `machines` | `Array[Dictionary]` | see [machine dictionary](#machine-dictionary). |
|
||||||
|
| `chokepoints` | `Array[Vector2i]` | every articulation point in the map graph. |
|
||||||
|
| `gate_sites` | `Array[Vector2i]` | the "best" chokepoint per pocket — ideal vestibule candidates. Subset of `chokepoints`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data structures
|
||||||
|
|
||||||
|
### Room dictionary
|
||||||
|
|
||||||
|
Each entry in `grid["rooms"]`:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
{
|
||||||
|
"id": int, # matches room_id in the cell arrays
|
||||||
|
"cells": Array[Vector2i], # every cell that belongs to this room
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Machine dictionary
|
||||||
|
|
||||||
|
Each entry in `grid["machines"]`:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
{
|
||||||
|
"id": int, # matches machine_id in the cell arrays
|
||||||
|
"interior_cells": Array[Vector2i],
|
||||||
|
"gate": Vector2i, # the one F_MACHINE_GATE cell; (-1,-1) if unflagged
|
||||||
|
"markers": Dictionary, # marker_id (int) -> Array[Vector2i]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
var m: Dictionary = grid["machines"][0]
|
||||||
|
for marker_id in m["markers"]:
|
||||||
|
var positions: Array = m["markers"][marker_id]
|
||||||
|
match marker_id:
|
||||||
|
4: # MK_PEDESTAL
|
||||||
|
for p in positions:
|
||||||
|
spawn_pedestal(p)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enums
|
||||||
|
|
||||||
|
### Terrain enum (`terrain` byte)
|
||||||
|
|
||||||
|
| id | constant | meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | `T_NOTHING` | outside the dungeon — not walkable |
|
||||||
|
| 1 | `T_FLOOR` | room floor |
|
||||||
|
| 2 | `T_WALL` | wall |
|
||||||
|
| 3 | `T_DOOR` | door in a room wall |
|
||||||
|
| 4 | `T_CORRIDOR` | corridor between rooms |
|
||||||
|
| 5 | `T_LIQUID` | water / lava / chasm / brimstone — see `liquid` byte |
|
||||||
|
| 6 | `T_BRIDGE` | walkable span across water/chasm |
|
||||||
|
| 7 | `T_STAIRS_UP` | up-stair |
|
||||||
|
| 8 | `T_STAIRS_DOWN` | down-stair |
|
||||||
|
|
||||||
|
`T_FLOOR`, `T_CORRIDOR`, `T_DOOR`, `T_BRIDGE`, `T_STAIRS_UP`, `T_STAIRS_DOWN` are **passable**.
|
||||||
|
|
||||||
|
### Liquid enum (`liquid` byte)
|
||||||
|
|
||||||
|
| id | constant | bridges allowed | |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0 | `L_NONE` | — | |
|
||||||
|
| 1 | `L_WATER` | yes | shallow levels |
|
||||||
|
| 2 | `L_LAVA` | no | damages walker |
|
||||||
|
| 3 | `L_CHASM` | yes | gameplay-defined fall behavior |
|
||||||
|
| 4 | `L_BRIMSTONE` | no | lowest depths |
|
||||||
|
|
||||||
|
### Marker enum (`surface` byte)
|
||||||
|
|
||||||
|
Semantic placement markers. The generator stamps these; gameplay interprets them.
|
||||||
|
|
||||||
|
| id | constant | typical use |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | `MK_NONE` | no marker |
|
||||||
|
| 1 | `MK_CARPET` | decorative floor tint (paint pass) |
|
||||||
|
| 2 | `MK_ALTAR` | item pedestal on an altar |
|
||||||
|
| 3 | `MK_CAGE` | item behind a lock |
|
||||||
|
| 4 | `MK_PEDESTAL` | item on pedestal |
|
||||||
|
| 5 | `MK_STATUE` | decorative statue |
|
||||||
|
| 6 | `MK_TORCH` | point light source |
|
||||||
|
| 7 | `MK_BRAZIER` | point light source (stronger) |
|
||||||
|
| 8 | `MK_CHEST` | item container |
|
||||||
|
| 9 | `MK_SPAWN_MONSTER` | spawn an enemy here |
|
||||||
|
| 10 | `MK_SPAWN_BOSS` | spawn a boss here |
|
||||||
|
| 11 | `MK_KEY_ITEM` | the key that unlocks this machine's parent |
|
||||||
|
|
||||||
|
### Cell flags
|
||||||
|
|
||||||
|
Bitmask in `flags[]`. Test with `(flags[idx] & F_X) != 0`.
|
||||||
|
|
||||||
|
| bit | value | constant | meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0 | `0x0001` | `F_IN_ROOM` | cell is room floor |
|
||||||
|
| 1 | `0x0002` | `F_IN_LOOP` | cell was carved by the loop pass |
|
||||||
|
| 2 | `0x0004` | `F_CHOKEPT` | reserved |
|
||||||
|
| 3 | `0x0008` | `F_BRIDGE` | cell is a bridge (redundant with `T_BRIDGE`) |
|
||||||
|
| 4 | `0x0010` | `F_WREATH` | shoreline cell (adjacent to liquid) |
|
||||||
|
| 6 | `0x0040` | `F_IN_MACHINE` | cell belongs to a machine |
|
||||||
|
| 7 | `0x0080` | `F_MACHINE_INTERIOR` | interior (not boundary) |
|
||||||
|
| 8 | `0x0100` | `F_MACHINE_GATE` | the vestibule / entry |
|
||||||
|
| 9 | `0x0200` | `F_MACHINE_IMPREGNABLE` | cell is protected from later modification (reserved) |
|
||||||
|
| 10 | `0x0400` | `F_MACHINE_KEY` | key goes here (gameplay hint) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage pattern
|
||||||
|
|
||||||
|
Generate a level, instantiate everything up front, hand off to gameplay.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
func build_level(seed: int, depth: int) -> void:
|
||||||
|
var gen := BrogueGen.new()
|
||||||
|
var grid: Dictionary = gen.generate(seed, depth)
|
||||||
|
gen.free()
|
||||||
|
|
||||||
|
# Player at the up-stair.
|
||||||
|
$Player.position = _grid_to_world(grid["stairs_up"])
|
||||||
|
|
||||||
|
# Goal marker at the down-stair.
|
||||||
|
$GoalMarker.position = _grid_to_world(grid["stairs_down"])
|
||||||
|
|
||||||
|
# One reward per machine.
|
||||||
|
for m in grid["machines"]:
|
||||||
|
for marker_id in m["markers"]:
|
||||||
|
for cell in m["markers"][marker_id]:
|
||||||
|
spawn_for_marker(marker_id, cell)
|
||||||
|
|
||||||
|
# Monsters proportional to room size.
|
||||||
|
for room in grid["rooms"]:
|
||||||
|
var size: int = (room["cells"] as Array).size()
|
||||||
|
if size > 40 and randf() < 0.6:
|
||||||
|
spawn_monster(_random_cell(room["cells"]))
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FOV reference
|
||||||
|
|
||||||
|
`demo/scripts/fov.gd` ships a reference `BrogueFOV` class. It is **not** part of the extension — it's pure GDScript you can copy into your project and modify.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# demo/scripts/fov.gd
|
||||||
|
class_name BrogueFOV extends RefCounted
|
||||||
|
|
||||||
|
static func compute(grid: Dictionary, origin: Vector2i, radius: int) -> PackedByteArray
|
||||||
|
```
|
||||||
|
|
||||||
|
Symmetric recursive shadowcasting, tile-accurate, O(visible cells). Opaque cells = walls, nothing, and non-water liquids (lava / chasm / brimstone block sight; water does not).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
var vis := BrogueFOV.compute(grid, player_pos, 8)
|
||||||
|
for y in grid["height"]:
|
||||||
|
for x in grid["width"]:
|
||||||
|
if vis[y * grid["width"] + x] == 1:
|
||||||
|
mark_visible(x, y)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Determinism guarantees
|
||||||
|
|
||||||
|
- Same `(seed, depth)` input always produces identical output, across runs, platforms, and future minor versions of the plugin.
|
||||||
|
- The C generator uses a PCG32 RNG seeded from the `seed` argument only. No platform calls (`time()`, `rand()`) are read.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build and rebuild
|
||||||
|
|
||||||
|
From the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make godot # builds the extension
|
||||||
|
make godot-clean # removes build artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs `scons -j$(nproc) platform=linux target=template_debug` in `godot/`. The resulting `.so` and manifest land together in `demo/addons/brogue_gen/` per Godot's plugin convention.
|
||||||
|
|
||||||
|
For release builds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd godot && scons -j$(nproc) platform=linux target=template_release
|
||||||
|
```
|
||||||
|
|
||||||
|
To add a new platform (Windows/macOS), add the SCons target + append a new line to `brogue_gen.gdextension`:
|
||||||
|
|
||||||
|
```
|
||||||
|
windows.debug.x86_64 = "res://bin/libbrogue_gen.windows.template_debug.x86_64.dll"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**"Could not find type BrogueGen" / "Cannot get class 'BrogueGen'".**
|
||||||
|
Godot has not registered the extension. Check:
|
||||||
|
1. `addons/brogue_gen/brogue_gen.gdextension` exists in the project.
|
||||||
|
2. `addons/brogue_gen/bin/libbrogue_gen...so` exists and matches the path in the manifest.
|
||||||
|
3. The project has been opened in the editor at least once (this triggers the extension scan).
|
||||||
|
4. Delete the `.godot/` cache directory and re-open if in doubt.
|
||||||
|
|
||||||
|
**Godot 4.6 crashes on startup (SIGABRT in `WaylandThread`).**
|
||||||
|
This is a Godot 4.6.1 Wayland bug, not the extension. Launch with X11 instead:
|
||||||
|
```bash
|
||||||
|
godot --display-driver x11 --path /path/to/your/project
|
||||||
|
```
|
||||||
66
godot/SConstruct
Normal file
66
godot/SConstruct
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
SConstruct for the brogue_gen GDExtension.
|
||||||
|
|
||||||
|
Build: cd godot && scons target=template_debug
|
||||||
|
(or target=template_release for optimized)
|
||||||
|
|
||||||
|
Output: ../demo/bin/libbrogue_gen.<platform>.<target>.<arch>.so
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
env = SConscript("godot-cpp/SConstruct")
|
||||||
|
|
||||||
|
# Where to find our C generator library headers.
|
||||||
|
env.Append(CPPPATH=["src/", "../src/"])
|
||||||
|
|
||||||
|
# Compile all of our C generator sources directly into the extension — the
|
||||||
|
# generator is a pure C99 library with no Godot deps. This avoids needing a
|
||||||
|
# separate libbroguegen.a.
|
||||||
|
c_sources = [
|
||||||
|
"../src/gen/rng.c",
|
||||||
|
"../src/gen/grid.c",
|
||||||
|
"../src/gen/events.c",
|
||||||
|
"../src/gen/room_types.c",
|
||||||
|
"../src/gen/accretion.c",
|
||||||
|
"../src/gen/dijkstra.c",
|
||||||
|
"../src/gen/loops.c",
|
||||||
|
"../src/gen/ca.c",
|
||||||
|
"../src/gen/lakes.c",
|
||||||
|
"../src/gen/walls.c",
|
||||||
|
"../src/gen/stairs.c",
|
||||||
|
"../src/gen/chokepoints.c",
|
||||||
|
"../src/gen/machines.c",
|
||||||
|
"../src/gen/blueprints_data.c",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Ensure C files compile with C99; don't inherit godot-cpp's C++ flags verbatim.
|
||||||
|
c_env = env.Clone()
|
||||||
|
c_env.Replace(CFLAGS=["-std=c99", "-Wall", "-Wpedantic", "-Werror=implicit",
|
||||||
|
"-O2", "-g", "-fPIC"])
|
||||||
|
c_objects = [c_env.SharedObject(target="build/" + os.path.basename(s).replace(".c", ""),
|
||||||
|
source=s) for s in c_sources]
|
||||||
|
|
||||||
|
cpp_sources = Glob("src/*.cpp")
|
||||||
|
|
||||||
|
library_name = "libbrogue_gen{}{}".format(env["suffix"], env["SHLIBSUFFIX"])
|
||||||
|
|
||||||
|
# Godot plugin layout convention: addons/<plugin_name>/
|
||||||
|
# brogue_gen.gdextension — manifest, references the .so path relative to res://
|
||||||
|
# bin/<name>.so — binaries per platform/target
|
||||||
|
import os as _os
|
||||||
|
import shutil as _shutil
|
||||||
|
_ADDON_DIR = "../demo/addons/brogue_gen"
|
||||||
|
_os.makedirs(_ADDON_DIR + "/bin", exist_ok=True)
|
||||||
|
|
||||||
|
library = env.SharedLibrary(
|
||||||
|
_ADDON_DIR + "/bin/" + library_name,
|
||||||
|
source=cpp_sources + c_objects,
|
||||||
|
)
|
||||||
|
|
||||||
|
_shutil.copyfile("brogue_gen.gdextension",
|
||||||
|
_ADDON_DIR + "/brogue_gen.gdextension")
|
||||||
|
|
||||||
|
Default(library)
|
||||||
11
godot/brogue_gen.gdextension
Normal file
11
godot/brogue_gen.gdextension
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[configuration]
|
||||||
|
|
||||||
|
entry_symbol = "brogue_gen_library_init"
|
||||||
|
compatibility_minimum = "4.3"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
|
||||||
|
linux.debug.x86_64 = "res://addons/brogue_gen/bin/libbrogue_gen.linux.template_debug.x86_64.so"
|
||||||
|
linux.release.x86_64 = "res://addons/brogue_gen/bin/libbrogue_gen.linux.template_release.x86_64.so"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
1
godot/godot-cpp
Submodule
1
godot/godot-cpp
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 60b5a4196de8442b43b32ba68ebe1e79cfcb762f
|
||||||
68
godot/src/brogue_gen.cpp
Normal file
68
godot/src/brogue_gen.cpp
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
#include "brogue_gen.h"
|
||||||
|
|
||||||
|
#include <godot_cpp/core/class_db.hpp>
|
||||||
|
#include <godot_cpp/variant/packed_byte_array.hpp>
|
||||||
|
#include <godot_cpp/variant/packed_int32_array.hpp>
|
||||||
|
#include <godot_cpp/variant/vector2i.hpp>
|
||||||
|
#include <godot_cpp/variant/typed_array.hpp>
|
||||||
|
#include <godot_cpp/variant/utility_functions.hpp>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include "gen/grid.h"
|
||||||
|
#include "gen/rng.h"
|
||||||
|
#include "gen/events.h"
|
||||||
|
#include "gen/accretion.h"
|
||||||
|
#include "gen/loops.h"
|
||||||
|
#include "gen/lakes.h"
|
||||||
|
#include "gen/walls.h"
|
||||||
|
#include "gen/stairs.h"
|
||||||
|
#include "gen/chokepoints.h"
|
||||||
|
#include "gen/machines.h"
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "grid_to_dict.h"
|
||||||
|
|
||||||
|
using namespace godot;
|
||||||
|
|
||||||
|
BrogueGen::BrogueGen() = default;
|
||||||
|
BrogueGen::~BrogueGen() = default;
|
||||||
|
|
||||||
|
void BrogueGen::_bind_methods() {
|
||||||
|
ClassDB::bind_method(D_METHOD("generate", "seed", "depth"), &BrogueGen::generate);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary BrogueGen::generate(int seed, int depth) {
|
||||||
|
grid_t g;
|
||||||
|
event_sink_t sink = {nullptr, nullptr, 0};
|
||||||
|
rng_t rng;
|
||||||
|
rng_seed(&rng, (uint64_t)seed);
|
||||||
|
grid_fill(g, T_NOTHING);
|
||||||
|
|
||||||
|
event_emit(&sink, EV_GEN_BEGIN, NULL, 0);
|
||||||
|
accrete_rooms(g, &rng, &sink);
|
||||||
|
add_loops(g, &sink);
|
||||||
|
place_lakes(g, &rng, &sink, depth);
|
||||||
|
apply_wreaths(g, &sink);
|
||||||
|
place_bridges(g, &sink);
|
||||||
|
finish_walls(g, &sink);
|
||||||
|
place_stairs(g, &sink);
|
||||||
|
|
||||||
|
static choke_map_t choke;
|
||||||
|
static choke_flag_t is_choke, is_gate;
|
||||||
|
compute_chokepoints(g, &sink, choke, is_choke, is_gate);
|
||||||
|
place_machines(g, &rng, depth, &sink, choke, is_gate);
|
||||||
|
event_emit(&sink, EV_GEN_END, NULL, 0);
|
||||||
|
|
||||||
|
Dictionary d = grid_to_dictionary(g, seed, depth, /*add_metadata=*/true);
|
||||||
|
|
||||||
|
TypedArray<Vector2i> chokes, gates;
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++) {
|
||||||
|
if (is_choke[y][x]) chokes.push_back(Vector2i(x, y));
|
||||||
|
if (is_gate[y][x]) gates.push_back(Vector2i(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d["chokepoints"] = chokes;
|
||||||
|
d["gate_sites"] = gates;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
25
godot/src/brogue_gen.h
Normal file
25
godot/src/brogue_gen.h
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <godot_cpp/classes/node.hpp>
|
||||||
|
#include <godot_cpp/variant/dictionary.hpp>
|
||||||
|
|
||||||
|
namespace godot {
|
||||||
|
|
||||||
|
/* One-shot facade over the pure C generator. Call generate() with a seed and
|
||||||
|
depth; receive a Dictionary describing the final dungeon. See the plugin
|
||||||
|
README for the full dict shape (terrain / liquid / surface / flags arrays,
|
||||||
|
rooms[], machines[], chokepoints[], stairs). */
|
||||||
|
class BrogueGen : public Node {
|
||||||
|
GDCLASS(BrogueGen, Node)
|
||||||
|
|
||||||
|
protected:
|
||||||
|
static void _bind_methods();
|
||||||
|
|
||||||
|
public:
|
||||||
|
BrogueGen();
|
||||||
|
~BrogueGen();
|
||||||
|
|
||||||
|
Dictionary generate(int seed, int depth);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace godot
|
||||||
132
godot/src/grid_to_dict.cpp
Normal file
132
godot/src/grid_to_dict.cpp
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
#include "grid_to_dict.h"
|
||||||
|
|
||||||
|
#include <godot_cpp/variant/packed_byte_array.hpp>
|
||||||
|
#include <godot_cpp/variant/packed_int32_array.hpp>
|
||||||
|
#include <godot_cpp/variant/array.hpp>
|
||||||
|
#include <godot_cpp/variant/typed_array.hpp>
|
||||||
|
|
||||||
|
using namespace godot;
|
||||||
|
|
||||||
|
static void build_rooms_and_machines(grid_t g, Array &rooms, Array &machines) {
|
||||||
|
/* Bucket cells by room_id and machine_id. room_id and machine_id are 1..255. */
|
||||||
|
struct Bucket { TypedArray<Vector2i> cells; Vector2i gate = Vector2i(-1, -1); };
|
||||||
|
Bucket rbuckets[256];
|
||||||
|
Bucket mbuckets[256];
|
||||||
|
|
||||||
|
/* Also collect marker-per-cell for each machine. */
|
||||||
|
Dictionary mmarkers[256];
|
||||||
|
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++) {
|
||||||
|
const cell_t *c = &g[y][x];
|
||||||
|
Vector2i p(x, y);
|
||||||
|
if (c->room_id > 0) {
|
||||||
|
rbuckets[c->room_id].cells.push_back(p);
|
||||||
|
}
|
||||||
|
if (c->machine_id > 0) {
|
||||||
|
mbuckets[c->machine_id].cells.push_back(p);
|
||||||
|
if (c->flags & F_MACHINE_GATE) {
|
||||||
|
mbuckets[c->machine_id].gate = p;
|
||||||
|
}
|
||||||
|
if (c->surface != MK_NONE) {
|
||||||
|
Dictionary &mk = mmarkers[c->machine_id];
|
||||||
|
int key = (int)c->surface;
|
||||||
|
TypedArray<Vector2i> list;
|
||||||
|
if (mk.has(key)) {
|
||||||
|
list = (TypedArray<Vector2i>)(Variant)mk[key];
|
||||||
|
}
|
||||||
|
list.push_back(p);
|
||||||
|
mk[key] = list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 1; i < 256; i++) {
|
||||||
|
if (rbuckets[i].cells.size() == 0) continue;
|
||||||
|
Dictionary d;
|
||||||
|
d["id"] = i;
|
||||||
|
d["cells"] = rbuckets[i].cells;
|
||||||
|
rooms.push_back(d);
|
||||||
|
}
|
||||||
|
for (int i = 1; i < 256; i++) {
|
||||||
|
if (mbuckets[i].cells.size() == 0) continue;
|
||||||
|
Dictionary d;
|
||||||
|
d["id"] = i;
|
||||||
|
d["interior_cells"] = mbuckets[i].cells;
|
||||||
|
d["gate"] = mbuckets[i].gate;
|
||||||
|
d["markers"] = mmarkers[i];
|
||||||
|
machines.push_back(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary godot::grid_to_dictionary(grid_t g, int seed, int depth, bool add_metadata) {
|
||||||
|
const int N = DCOLS * DROWS;
|
||||||
|
PackedByteArray terrain; terrain.resize(N);
|
||||||
|
PackedByteArray liquid; liquid.resize(N);
|
||||||
|
PackedByteArray room_id; room_id.resize(N);
|
||||||
|
PackedByteArray machine_id; machine_id.resize(N);
|
||||||
|
PackedByteArray surface; surface.resize(N);
|
||||||
|
PackedInt32Array flags; flags.resize(N);
|
||||||
|
|
||||||
|
uint8_t *t = terrain.ptrw();
|
||||||
|
uint8_t *lq = liquid.ptrw();
|
||||||
|
uint8_t *ri = room_id.ptrw();
|
||||||
|
uint8_t *mi = machine_id.ptrw();
|
||||||
|
uint8_t *sf = surface.ptrw();
|
||||||
|
int32_t *fl = flags.ptrw();
|
||||||
|
|
||||||
|
Vector2i stairs_up(-1, -1), stairs_down(-1, -1);
|
||||||
|
int i = 0;
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++, i++) {
|
||||||
|
const cell_t *c = &g[y][x];
|
||||||
|
t[i] = c->terrain;
|
||||||
|
lq[i] = c->liquid;
|
||||||
|
ri[i] = c->room_id;
|
||||||
|
mi[i] = c->machine_id;
|
||||||
|
sf[i] = c->surface;
|
||||||
|
fl[i] = (int32_t)c->flags;
|
||||||
|
if (c->terrain == T_STAIRS_UP) stairs_up = Vector2i(x, y);
|
||||||
|
if (c->terrain == T_STAIRS_DOWN) stairs_down = Vector2i(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary d;
|
||||||
|
d["seed"] = seed;
|
||||||
|
d["depth"] = depth;
|
||||||
|
d["width"] = DCOLS;
|
||||||
|
d["height"] = DROWS;
|
||||||
|
d["terrain"] = terrain;
|
||||||
|
d["liquid"] = liquid;
|
||||||
|
d["flags"] = flags;
|
||||||
|
d["room_id"] = room_id;
|
||||||
|
d["machine_id"] = machine_id;
|
||||||
|
d["surface"] = surface;
|
||||||
|
d["stairs_up"] = stairs_up;
|
||||||
|
d["stairs_down"] = stairs_down;
|
||||||
|
|
||||||
|
if (add_metadata) {
|
||||||
|
Array rooms;
|
||||||
|
Array machines;
|
||||||
|
build_rooms_and_machines(g, rooms, machines);
|
||||||
|
d["rooms"] = rooms;
|
||||||
|
d["machines"] = machines;
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary godot::cell_to_dictionary(grid_t g, int x, int y) {
|
||||||
|
Dictionary d;
|
||||||
|
if (!grid_in_bounds(x, y)) return d;
|
||||||
|
const cell_t *c = &g[y][x];
|
||||||
|
d["x"] = x;
|
||||||
|
d["y"] = y;
|
||||||
|
d["terrain"] = (int)c->terrain;
|
||||||
|
d["liquid"] = (int)c->liquid;
|
||||||
|
d["surface"] = (int)c->surface;
|
||||||
|
d["flags"] = (int)c->flags;
|
||||||
|
d["room_id"] = (int)c->room_id;
|
||||||
|
d["machine_id"] = (int)c->machine_id;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
22
godot/src/grid_to_dict.h
Normal file
22
godot/src/grid_to_dict.h
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <godot_cpp/variant/dictionary.hpp>
|
||||||
|
#include <godot_cpp/variant/vector2i.hpp>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include "gen/grid.h"
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace godot {
|
||||||
|
|
||||||
|
/* Pack a grid_t into a Dictionary with the shape documented in the plugin
|
||||||
|
README (terrain/liquid/flags/room_id/machine_id/surface arrays). Used by
|
||||||
|
both BrogueGen.generate() and BrogueReplay.get_grid(). Additional
|
||||||
|
metadata (rooms, machines, chokepoints, stairs) are computed and added
|
||||||
|
when add_metadata is true. */
|
||||||
|
Dictionary grid_to_dictionary(grid_t g, int seed, int depth, bool add_metadata);
|
||||||
|
|
||||||
|
/* Pack a single cell as a Dictionary. */
|
||||||
|
Dictionary cell_to_dictionary(grid_t g, int x, int y);
|
||||||
|
|
||||||
|
} // namespace godot
|
||||||
33
godot/src/register_types.cpp
Normal file
33
godot/src/register_types.cpp
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
#include "register_types.h"
|
||||||
|
|
||||||
|
#include <gdextension_interface.h>
|
||||||
|
#include <godot_cpp/core/defs.hpp>
|
||||||
|
#include <godot_cpp/core/class_db.hpp>
|
||||||
|
#include <godot_cpp/godot.hpp>
|
||||||
|
|
||||||
|
#include "brogue_gen.h"
|
||||||
|
|
||||||
|
using namespace godot;
|
||||||
|
|
||||||
|
void initialize_brogue_gen_module(ModuleInitializationLevel p_level) {
|
||||||
|
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) return;
|
||||||
|
ClassDB::register_class<BrogueGen>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uninitialize_brogue_gen_module(ModuleInitializationLevel p_level) {
|
||||||
|
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
GDExtensionBool GDE_EXPORT brogue_gen_library_init(
|
||||||
|
GDExtensionInterfaceGetProcAddress p_get_proc_address,
|
||||||
|
const GDExtensionClassLibraryPtr p_library,
|
||||||
|
GDExtensionInitialization *r_initialization) {
|
||||||
|
|
||||||
|
GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization);
|
||||||
|
init_obj.register_initializer(initialize_brogue_gen_module);
|
||||||
|
init_obj.register_terminator(uninitialize_brogue_gen_module);
|
||||||
|
init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE);
|
||||||
|
return init_obj.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
8
godot/src/register_types.h
Normal file
8
godot/src/register_types.h
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <godot_cpp/core/class_db.hpp>
|
||||||
|
|
||||||
|
using namespace godot;
|
||||||
|
|
||||||
|
void initialize_brogue_gen_module(ModuleInitializationLevel p_level);
|
||||||
|
void uninitialize_brogue_gen_module(ModuleInitializationLevel p_level);
|
||||||
304
src/gen/accretion.c
Normal file
304
src/gen/accretion.c
Normal file
|
|
@ -0,0 +1,304 @@
|
||||||
|
#include "accretion.h"
|
||||||
|
#include "room_types.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int x, y;
|
||||||
|
int dx, dy;
|
||||||
|
} door_site_t;
|
||||||
|
|
||||||
|
#define MAX_SITES 512
|
||||||
|
#define MAX_ATTEMPTS 30
|
||||||
|
|
||||||
|
static int is_open(uint8_t terrain) {
|
||||||
|
return terrain == T_FLOOR || terrain == T_CORRIDOR || terrain == T_DOOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Find door sites: FLOOR cells with a NOTHING neighbor in one of 4 cardinal
|
||||||
|
directions. dx,dy is the outward direction (floor -> nothing). */
|
||||||
|
static int find_main_sites(grid_t g, door_site_t *out, int cap) {
|
||||||
|
static const int dirs[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
|
||||||
|
int n = 0;
|
||||||
|
for (int y = 1; y < DROWS - 1 && n < cap; y++) {
|
||||||
|
for (int x = 1; x < DCOLS - 1 && n < cap; x++) {
|
||||||
|
if (g[y][x].terrain != T_FLOOR) continue;
|
||||||
|
for (int d = 0; d < 4; d++) {
|
||||||
|
int nx = x + dirs[d][0];
|
||||||
|
int ny = y + dirs[d][1];
|
||||||
|
if (!grid_in_bounds(nx, ny)) continue;
|
||||||
|
if (g[ny][nx].terrain == T_NOTHING) {
|
||||||
|
out[n].x = x; out[n].y = y;
|
||||||
|
out[n].dx = dirs[d][0]; out[n].dy = dirs[d][1];
|
||||||
|
if (++n >= cap) return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Find scratch sites: FLOOR cells with a NOTHING neighbor outward. */
|
||||||
|
static int find_scratch_sites(grid_t g, door_site_t *out, int cap) {
|
||||||
|
return find_main_sites(g, out, cap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Corridor path: door is path[0]; subsequent cells are corridor tiles; after
|
||||||
|
the last path cell comes the entry floor where the scratch room blits. A
|
||||||
|
straight corridor has a monotonic path; an L-bent one has one inflection. */
|
||||||
|
#define MAX_PATH 24
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int x[MAX_PATH];
|
||||||
|
int y[MAX_PATH];
|
||||||
|
int len;
|
||||||
|
int entry_x, entry_y;
|
||||||
|
} corridor_path_t;
|
||||||
|
|
||||||
|
/* Check whether placement is valid: all corridor path cells are NOTHING on
|
||||||
|
main, entry is NOTHING, scratch floor lands on NOTHING with 1-cell buffer
|
||||||
|
against existing rooms. */
|
||||||
|
static int fits_placement(grid_t scratch, grid_t main_g,
|
||||||
|
int tx, int ty, const corridor_path_t *p) {
|
||||||
|
for (int i = 0; i < p->len; i++) {
|
||||||
|
if (!grid_in_bounds(p->x[i], p->y[i])) return 0;
|
||||||
|
if (main_g[p->y[i]][p->x[i]].terrain != T_NOTHING) return 0;
|
||||||
|
}
|
||||||
|
if (!grid_in_bounds(p->entry_x, p->entry_y)) return 0;
|
||||||
|
if (main_g[p->entry_y][p->entry_x].terrain != T_NOTHING) return 0;
|
||||||
|
|
||||||
|
for (int sy = 0; sy < DROWS; sy++) {
|
||||||
|
for (int sx = 0; sx < DCOLS; sx++) {
|
||||||
|
if (scratch[sy][sx].terrain != T_FLOOR) continue;
|
||||||
|
int mx = sx + tx;
|
||||||
|
int my = sy + ty;
|
||||||
|
if (mx < 1 || my < 1 || mx >= DCOLS - 1 || my >= DROWS - 1) return 0;
|
||||||
|
if (main_g[my][mx].terrain != T_NOTHING) return 0;
|
||||||
|
|
||||||
|
for (int ny = -1; ny <= 1; ny++) {
|
||||||
|
for (int nx = -1; nx <= 1; nx++) {
|
||||||
|
if (nx == 0 && ny == 0) continue;
|
||||||
|
int qx = mx + nx;
|
||||||
|
int qy = my + ny;
|
||||||
|
if (!grid_in_bounds(qx, qy)) continue;
|
||||||
|
/* neighbor is another translated scratch floor? ok */
|
||||||
|
int ssx = qx - tx, ssy = qy - ty;
|
||||||
|
if (ssx >= 0 && ssx < DCOLS && ssy >= 0 && ssy < DROWS
|
||||||
|
&& scratch[ssy][ssx].terrain == T_FLOOR) continue;
|
||||||
|
/* neighbor is on the corridor path? ok (we're about to
|
||||||
|
carve it; NOTHING at check time). */
|
||||||
|
if (is_open(main_g[qy][qx].terrain)) return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep only the largest 4-connected FLOOR region in scratch; zero others to
|
||||||
|
NOTHING. Prevents disconnected multi-bump rooms (e.g., chunky with
|
||||||
|
non-overlapping circles) from creating orphan rooms on main. */
|
||||||
|
static void scratch_keep_largest(grid_t scratch) {
|
||||||
|
static int label[DROWS][DCOLS];
|
||||||
|
memset(label, 0, sizeof(label));
|
||||||
|
static int sx_stack[DCOLS * DROWS];
|
||||||
|
static int sy_stack[DCOLS * DROWS];
|
||||||
|
int best = 0, best_size = 0, next = 0;
|
||||||
|
static const int dx4[4] = {1, -1, 0, 0};
|
||||||
|
static const int dy4[4] = {0, 0, 1, -1};
|
||||||
|
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++) {
|
||||||
|
if (scratch[y][x].terrain != T_FLOOR || label[y][x]) continue;
|
||||||
|
next++;
|
||||||
|
int sp = 0;
|
||||||
|
sx_stack[sp] = x; sy_stack[sp] = y; sp++;
|
||||||
|
int size = 0;
|
||||||
|
while (sp > 0) {
|
||||||
|
sp--;
|
||||||
|
int cx = sx_stack[sp], cy = sy_stack[sp];
|
||||||
|
if (!grid_in_bounds(cx, cy)) continue;
|
||||||
|
if (label[cy][cx] || scratch[cy][cx].terrain != T_FLOOR) continue;
|
||||||
|
label[cy][cx] = next;
|
||||||
|
size++;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
sx_stack[sp] = cx + dx4[i];
|
||||||
|
sy_stack[sp] = cy + dy4[i];
|
||||||
|
sp++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (size > best_size) { best_size = size; best = next; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (next <= 1) return;
|
||||||
|
for (int y = 0; y < DROWS; y++)
|
||||||
|
for (int x = 0; x < DCOLS; x++)
|
||||||
|
if (scratch[y][x].terrain == T_FLOOR && label[y][x] != best) {
|
||||||
|
scratch[y][x].terrain = T_NOTHING;
|
||||||
|
scratch[y][x].flags = 0;
|
||||||
|
scratch[y][x].room_id = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void blit_room(grid_t scratch, grid_t main_g, int tx, int ty, uint8_t room_id) {
|
||||||
|
for (int sy = 0; sy < DROWS; sy++) {
|
||||||
|
for (int sx = 0; sx < DCOLS; sx++) {
|
||||||
|
if (scratch[sy][sx].terrain != T_FLOOR) continue;
|
||||||
|
int mx = sx + tx, my = sy + ty;
|
||||||
|
if (!grid_in_bounds(mx, my)) continue;
|
||||||
|
main_g[my][mx].terrain = T_FLOOR;
|
||||||
|
main_g[my][mx].room_id = room_id;
|
||||||
|
main_g[my][mx].flags |= F_IN_ROOM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void carve_path(grid_t g, const corridor_path_t *p, uint8_t room_id) {
|
||||||
|
if (p->len == 0) return;
|
||||||
|
g[p->y[0]][p->x[0]].terrain = T_DOOR;
|
||||||
|
g[p->y[0]][p->x[0]].room_id = room_id;
|
||||||
|
for (int i = 1; i < p->len; i++) {
|
||||||
|
g[p->y[i]][p->x[i]].terrain = T_CORRIDOR;
|
||||||
|
g[p->y[i]][p->x[i]].room_id = room_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void build_straight(corridor_path_t *p,
|
||||||
|
int door_x, int door_y, int len, int dx, int dy) {
|
||||||
|
if (len > MAX_PATH) len = MAX_PATH;
|
||||||
|
p->len = len;
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
p->x[i] = door_x + i * dx;
|
||||||
|
p->y[i] = door_y + i * dy;
|
||||||
|
}
|
||||||
|
p->entry_x = door_x + len * dx;
|
||||||
|
p->entry_y = door_y + len * dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* L-bent corridor: L1 cells in (dx1,dy1), then L2 cells in (dx2,dy2). dx2/dy2
|
||||||
|
must be perpendicular to dx1/dy1. Total path cells = L1 + L2 (≤ MAX_PATH).
|
||||||
|
Entry is one step past the last leg2 cell. */
|
||||||
|
static void build_lbent(corridor_path_t *p,
|
||||||
|
int door_x, int door_y,
|
||||||
|
int L1, int dx1, int dy1,
|
||||||
|
int L2, int dx2, int dy2) {
|
||||||
|
int total = L1 + L2;
|
||||||
|
if (total > MAX_PATH) total = MAX_PATH;
|
||||||
|
p->len = total;
|
||||||
|
int i = 0;
|
||||||
|
for (int k = 0; k < L1 && i < total; k++, i++) {
|
||||||
|
p->x[i] = door_x + k * dx1;
|
||||||
|
p->y[i] = door_y + k * dy1;
|
||||||
|
}
|
||||||
|
int ex = door_x + (L1 - 1) * dx1;
|
||||||
|
int ey = door_y + (L1 - 1) * dy1;
|
||||||
|
for (int k = 1; k <= L2 && i < total; k++, i++) {
|
||||||
|
p->x[i] = ex + k * dx2;
|
||||||
|
p->y[i] = ey + k * dy2;
|
||||||
|
}
|
||||||
|
p->entry_x = ex + (L2 + 1) * dx2;
|
||||||
|
p->entry_y = ey + (L2 + 1) * dy2;
|
||||||
|
}
|
||||||
|
|
||||||
|
int accrete_rooms(grid_t g, rng_t *rng, event_sink_t *sink) {
|
||||||
|
/* First room at center. Cavern is only attempted here (too big for the
|
||||||
|
accretion buffer check to fit against existing rooms). */
|
||||||
|
grid_fill(g, T_NOTHING);
|
||||||
|
uint8_t room_id = 1;
|
||||||
|
room_type_t t0;
|
||||||
|
if (rng_percent(rng, 25)) {
|
||||||
|
t0 = ROOM_CAVERN;
|
||||||
|
} else {
|
||||||
|
/* pick any non-cavern type */
|
||||||
|
t0 = (room_type_t)rng_range(rng, 0, ROOM_COUNT - 2);
|
||||||
|
if (t0 == ROOM_CAVERN) t0 = ROOM_SMALL_RECT;
|
||||||
|
}
|
||||||
|
int tries = 0;
|
||||||
|
while (!room_carve(g, t0, rng, DCOLS/2, DROWS/2, room_id) && tries++ < 8) {
|
||||||
|
t0 = (room_type_t)rng_range(rng, 0, ROOM_COUNT - 2);
|
||||||
|
if (t0 == ROOM_CAVERN) t0 = ROOM_SMALL_RECT;
|
||||||
|
}
|
||||||
|
scratch_keep_largest(g);
|
||||||
|
event_emit(sink, EV_ROOM_PLACED, NULL, 0);
|
||||||
|
|
||||||
|
grid_t scratch;
|
||||||
|
door_site_t main_sites[MAX_SITES];
|
||||||
|
door_site_t scratch_sites[MAX_SITES];
|
||||||
|
|
||||||
|
for (room_id = 2; room_id <= MAX_ROOMS; room_id++) {
|
||||||
|
int placed = 0;
|
||||||
|
for (int attempt = 0; attempt < MAX_ATTEMPTS && !placed; attempt++) {
|
||||||
|
/* Exclude cavern (too big to attach reliably). */
|
||||||
|
room_type_t rt = (room_type_t)rng_range(rng, 0, ROOM_COUNT - 2);
|
||||||
|
if (rt == ROOM_CAVERN) rt = ROOM_SMALL_RECT;
|
||||||
|
event_emit(sink, EV_ROOM_PROPOSED, NULL, 0);
|
||||||
|
|
||||||
|
grid_fill(scratch, T_NOTHING);
|
||||||
|
if (!room_carve(scratch, rt, rng, DCOLS/2, DROWS/2, room_id)) continue;
|
||||||
|
scratch_keep_largest(scratch);
|
||||||
|
|
||||||
|
int n_scratch = find_scratch_sites(scratch, scratch_sites, MAX_SITES);
|
||||||
|
int n_main = find_main_sites(g, main_sites, MAX_SITES);
|
||||||
|
if (n_scratch == 0 || n_main == 0) continue;
|
||||||
|
|
||||||
|
/* Pick main site, then find scratch site facing opposite. */
|
||||||
|
door_site_t ms = main_sites[rng_range(rng, 0, n_main - 1)];
|
||||||
|
int opp_idx[MAX_SITES];
|
||||||
|
int n_opp = 0;
|
||||||
|
for (int i = 0; i < n_scratch; i++) {
|
||||||
|
if (scratch_sites[i].dx == -ms.dx && scratch_sites[i].dy == -ms.dy)
|
||||||
|
opp_idx[n_opp++] = i;
|
||||||
|
}
|
||||||
|
if (n_opp == 0) continue;
|
||||||
|
door_site_t ss = scratch_sites[opp_idx[rng_range(rng, 0, n_opp - 1)]];
|
||||||
|
|
||||||
|
int horiz = (ms.dy == 0);
|
||||||
|
int L = horiz ? rng_range(rng, 2, 10) : rng_range(rng, 2, 6);
|
||||||
|
|
||||||
|
int door_x = ms.x + ms.dx;
|
||||||
|
int door_y = ms.y + ms.dy;
|
||||||
|
|
||||||
|
corridor_path_t path;
|
||||||
|
int want_bend = L >= 4 && rng_percent(rng, 30);
|
||||||
|
if (want_bend) {
|
||||||
|
int L1 = rng_range(rng, 2, L - 2);
|
||||||
|
int L2 = L - L1;
|
||||||
|
int sign = rng_percent(rng, 50) ? 1 : -1;
|
||||||
|
int dx2 = -ms.dy * sign, dy2 = ms.dx * sign;
|
||||||
|
build_lbent(&path, door_x, door_y,
|
||||||
|
L1, ms.dx, ms.dy, L2, dx2, dy2);
|
||||||
|
} else {
|
||||||
|
build_straight(&path, door_x, door_y, L, ms.dx, ms.dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scratch room's entry floor must land at path.entry. Its door-
|
||||||
|
facing scratch cell ss points OUT of the scratch room in its
|
||||||
|
outward dir; we want scratch to be placed so ss lands at entry,
|
||||||
|
and the scratch room extends AWAY from the corridor's final
|
||||||
|
direction. For straight corridors that direction is ms.dir; for
|
||||||
|
bent it's the leg2 direction. We recover it from path endpoints. */
|
||||||
|
int final_dx = ms.dx, final_dy = ms.dy;
|
||||||
|
if (want_bend) {
|
||||||
|
int last = path.len - 1;
|
||||||
|
final_dx = path.entry_x - path.x[last];
|
||||||
|
final_dy = path.entry_y - path.y[last];
|
||||||
|
/* Scratch site must face opposite to this final direction. */
|
||||||
|
if (ss.dx != -final_dx || ss.dy != -final_dy) continue;
|
||||||
|
}
|
||||||
|
(void)final_dx; (void)final_dy;
|
||||||
|
|
||||||
|
int tx = path.entry_x - ss.x;
|
||||||
|
int ty = path.entry_y - ss.y;
|
||||||
|
|
||||||
|
if (!fits_placement(scratch, g, tx, ty, &path)) continue;
|
||||||
|
|
||||||
|
event_emit(sink, EV_DOOR_SITE_CHOSEN, NULL, 0);
|
||||||
|
carve_path(g, &path, room_id);
|
||||||
|
event_emit(sink, EV_CORRIDOR_CARVED, NULL, 0);
|
||||||
|
blit_room(scratch, g, tx, ty, room_id);
|
||||||
|
event_emit(sink, EV_ROOM_PLACED, NULL, 0);
|
||||||
|
placed = 1;
|
||||||
|
}
|
||||||
|
if (!placed) event_emit(sink, EV_ROOM_REJECTED, NULL, 0);
|
||||||
|
}
|
||||||
|
return (int)(room_id - 1);
|
||||||
|
}
|
||||||
13
src/gen/accretion.h
Normal file
13
src/gen/accretion.h
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
#ifndef GENESIS_GEN_ACCRETION_H
|
||||||
|
#define GENESIS_GEN_ACCRETION_H
|
||||||
|
|
||||||
|
#include "grid.h"
|
||||||
|
#include "rng.h"
|
||||||
|
#include "events.h"
|
||||||
|
|
||||||
|
#define MAX_ROOMS 35
|
||||||
|
|
||||||
|
/* Place rooms by accretion into g. Returns the number of rooms placed. */
|
||||||
|
int accrete_rooms(grid_t g, rng_t *rng, event_sink_t *sink);
|
||||||
|
|
||||||
|
#endif
|
||||||
66
src/gen/blueprints.h
Normal file
66
src/gen/blueprints.h
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
#ifndef GENESIS_GEN_BLUEPRINTS_H
|
||||||
|
#define GENESIS_GEN_BLUEPRINTS_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "grid.h"
|
||||||
|
|
||||||
|
/* Blueprint = a template for a machine (locked vault, reward room, monster
|
||||||
|
den, etc.). Each blueprint has metadata (depth range, pocket size range,
|
||||||
|
selection frequency) and a small list of features that describe what to
|
||||||
|
place inside. Terrain and markers are intentionally generic so the Godot
|
||||||
|
side can interpret them into game-specific content. */
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
BP_ROOM = 1u << 0, /* must occupy a room-sized region */
|
||||||
|
BP_VESTIBULE = 1u << 1, /* attaches to a chokepoint pocket */
|
||||||
|
BP_REWARD = 1u << 2, /* counts as a "reward" for pacing */
|
||||||
|
BP_PURGE_INTERIOR = 1u << 3, /* clear liquid etc before building */
|
||||||
|
BP_SURROUND_WITH_WALLS = 1u << 4,
|
||||||
|
BP_OPEN_INTERIOR = 1u << 5, /* re-pave interior to floor */
|
||||||
|
BP_IMPREGNABLE = 1u << 6, /* walls cannot be later modified */
|
||||||
|
BP_ADOPT_KEY = 1u << 7, /* receives key from parent machine */
|
||||||
|
} blueprint_flags_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
MF_EVERYWHERE = 1u << 0, /* feature occupies every interior cell */
|
||||||
|
MF_BUILD_AT_ORIGIN = 1u << 1, /* placed at the blueprint's origin */
|
||||||
|
MF_BUILD_VESTIBULE = 1u << 2, /* spawns a child vestibule (cascade) */
|
||||||
|
MF_BUILD_IN_WALLS = 1u << 3,
|
||||||
|
MF_NEAR_ORIGIN = 1u << 4,
|
||||||
|
MF_FAR_FROM_ORIGIN = 1u << 5,
|
||||||
|
MF_PERMIT_BLOCKING = 1u << 6, /* may block a walking path */
|
||||||
|
MF_TREAT_AS_BLOCKING = 1u << 7, /* others must route around */
|
||||||
|
MF_IMPREGNABLE = 1u << 8,
|
||||||
|
MF_ALTERNATIVE = 1u << 9, /* pick one of a group */
|
||||||
|
MF_GENERATE_KEY = 1u << 10, /* this instance IS the key */
|
||||||
|
MF_REQUIRE_ADOPTED_KEY = 1u << 11,
|
||||||
|
} feature_flags_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint16_t terrain; /* terrain_t to paint, 0 = unchanged */
|
||||||
|
uint16_t marker; /* marker_t to stamp into cell.surface, MK_NONE = none */
|
||||||
|
uint8_t min_count; /* minimum instances required for acceptance */
|
||||||
|
uint8_t max_count; /* upper bound on instances attempted */
|
||||||
|
uint32_t flags; /* feature_flags_t */
|
||||||
|
} feature_t;
|
||||||
|
|
||||||
|
#define MAX_FEATURES 8
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *name;
|
||||||
|
int16_t depth_min;
|
||||||
|
int16_t depth_max;
|
||||||
|
int16_t room_size_min; /* pocket size in cells */
|
||||||
|
int16_t room_size_max;
|
||||||
|
uint16_t frequency; /* relative selection weight */
|
||||||
|
uint32_t flags; /* blueprint_flags_t */
|
||||||
|
uint8_t feature_count;
|
||||||
|
feature_t features[MAX_FEATURES];
|
||||||
|
} blueprint_t;
|
||||||
|
|
||||||
|
/* The catalog. Defined in blueprints_data.c. NUM_BLUEPRINTS is the live
|
||||||
|
length; index 0 is a no-op sentinel ("nothing") to match common usage. */
|
||||||
|
extern const blueprint_t blueprint_catalog[];
|
||||||
|
extern const int NUM_BLUEPRINTS;
|
||||||
|
|
||||||
|
#endif
|
||||||
36
src/gen/blueprints_data.c
Normal file
36
src/gen/blueprints_data.c
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
#include "blueprints.h"
|
||||||
|
|
||||||
|
/* One entry for A.6c. More coming in A.6f. The sentinel at index 0 lets
|
||||||
|
callers loop from i=1 and skip the no-op, matching Brogue's convention. */
|
||||||
|
const blueprint_t blueprint_catalog[] = {
|
||||||
|
/* 0: sentinel */
|
||||||
|
{ .name = "(none)", .feature_count = 0 },
|
||||||
|
|
||||||
|
/* 1: Reward Vault
|
||||||
|
A small pocket behind a chokepoint, carpeted, containing 3-5 pedestals
|
||||||
|
each bearing an item. The gate cell is marked as a vestibule candidate
|
||||||
|
(used by the cascade in A.6d; for A.6c it's just an entry marker). */
|
||||||
|
{
|
||||||
|
.name = "Reward Vault",
|
||||||
|
.depth_min = 1,
|
||||||
|
.depth_max = 26,
|
||||||
|
.room_size_min = 6,
|
||||||
|
.room_size_max = 40,
|
||||||
|
.frequency = 100,
|
||||||
|
.flags = BP_VESTIBULE | BP_REWARD,
|
||||||
|
.feature_count = 3,
|
||||||
|
.features = {
|
||||||
|
/* Carpet paints every interior floor; purely visual. */
|
||||||
|
{ .terrain = 0, .marker = MK_CARPET, .min_count = 0, .max_count = 0,
|
||||||
|
.flags = MF_EVERYWHERE },
|
||||||
|
/* Pedestals with items — blocking, since you walk around them. */
|
||||||
|
{ .terrain = 0, .marker = MK_PEDESTAL, .min_count = 3, .max_count = 5,
|
||||||
|
.flags = MF_TREAT_AS_BLOCKING },
|
||||||
|
/* Gate cell — will spawn a vestibule child once A.6d lands. */
|
||||||
|
{ .terrain = 0, .marker = MK_NONE, .min_count = 1, .max_count = 1,
|
||||||
|
.flags = MF_BUILD_AT_ORIGIN | MF_PERMIT_BLOCKING | MF_BUILD_VESTIBULE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const int NUM_BLUEPRINTS = (int)(sizeof(blueprint_catalog) / sizeof(blueprint_catalog[0]));
|
||||||
117
src/gen/ca.c
Normal file
117
src/gen/ca.c
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
#include "ca.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#define SEED_PERCENT 55
|
||||||
|
#define ITERATIONS 5
|
||||||
|
|
||||||
|
static int count_n8(bitgrid_t g, int cx, int cy, int bx0, int by0, int bx1, int by1) {
|
||||||
|
int n = 0;
|
||||||
|
for (int dy = -1; dy <= 1; dy++) {
|
||||||
|
for (int dx = -1; dx <= 1; dx++) {
|
||||||
|
if (dx == 0 && dy == 0) continue;
|
||||||
|
int x = cx + dx, y = cy + dy;
|
||||||
|
if (x < bx0 || x >= bx1 || y < by0 || y >= by1) continue;
|
||||||
|
if (g[y][x]) n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void step(bitgrid_t in, bitgrid_t out,
|
||||||
|
int bx0, int by0, int bx1, int by1) {
|
||||||
|
memset(out, 0, sizeof(bitgrid_t));
|
||||||
|
for (int y = by0; y < by1; y++) {
|
||||||
|
for (int x = bx0; x < bx1; x++) {
|
||||||
|
int n = count_n8(in, x, y, bx0, by0, bx1, by1);
|
||||||
|
if (in[y][x]) {
|
||||||
|
/* S45678 */
|
||||||
|
out[y][x] = (n >= 4) ? 1 : 0;
|
||||||
|
} else {
|
||||||
|
/* B678 */
|
||||||
|
out[y][x] = (n >= 6) ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int extract_largest_region(bitgrid_t g, int bx0, int by0, int bx1, int by1) {
|
||||||
|
static int visited[DROWS][DCOLS];
|
||||||
|
memset(visited, 0, sizeof(visited));
|
||||||
|
int best_size = 0;
|
||||||
|
int best_id = 0;
|
||||||
|
int label = 0;
|
||||||
|
|
||||||
|
static int stack_x[DCOLS * DROWS];
|
||||||
|
static int stack_y[DCOLS * DROWS];
|
||||||
|
|
||||||
|
static const int dx4[4] = {1, -1, 0, 0};
|
||||||
|
static const int dy4[4] = {0, 0, 1, -1};
|
||||||
|
|
||||||
|
for (int y = by0; y < by1; y++) {
|
||||||
|
for (int x = bx0; x < bx1; x++) {
|
||||||
|
if (!g[y][x] || visited[y][x]) continue;
|
||||||
|
label++;
|
||||||
|
int sp = 0;
|
||||||
|
stack_x[sp] = x; stack_y[sp] = y; sp++;
|
||||||
|
int size = 0;
|
||||||
|
while (sp > 0) {
|
||||||
|
sp--;
|
||||||
|
int cx = stack_x[sp], cy = stack_y[sp];
|
||||||
|
if (cx < bx0 || cx >= bx1 || cy < by0 || cy >= by1) continue;
|
||||||
|
if (visited[cy][cx] || !g[cy][cx]) continue;
|
||||||
|
visited[cy][cx] = label;
|
||||||
|
size++;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
stack_x[sp] = cx + dx4[i];
|
||||||
|
stack_y[sp] = cy + dy4[i];
|
||||||
|
sp++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (size > best_size) { best_size = size; best_id = label; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zero out everything that isn't in best_id. */
|
||||||
|
for (int y = by0; y < by1; y++) {
|
||||||
|
for (int x = bx0; x < bx1; x++) {
|
||||||
|
if (visited[y][x] != best_id) g[y][x] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ca_blob(bitgrid_t out, rng_t *rng, event_sink_t *sink,
|
||||||
|
int x, int y, int w, int h) {
|
||||||
|
int bx0 = x, by0 = y;
|
||||||
|
int bx1 = x + w, by1 = y + h;
|
||||||
|
if (bx0 < 0) bx0 = 0;
|
||||||
|
if (by0 < 0) by0 = 0;
|
||||||
|
if (bx1 > DCOLS) bx1 = DCOLS;
|
||||||
|
if (by1 > DROWS) by1 = DROWS;
|
||||||
|
|
||||||
|
memset(out, 0, sizeof(bitgrid_t));
|
||||||
|
for (int j = by0; j < by1; j++) {
|
||||||
|
for (int i = bx0; i < bx1; i++) {
|
||||||
|
out[j][i] = rng_percent(rng, SEED_PERCENT) ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bitgrid_t buf;
|
||||||
|
for (int k = 0; k < ITERATIONS; k++) {
|
||||||
|
if (k % 2 == 0) step(out, buf, bx0, by0, bx1, by1);
|
||||||
|
else step(buf, out, bx0, by0, bx1, by1);
|
||||||
|
event_emit(sink, EV_CA_ITERATION, NULL, 0);
|
||||||
|
}
|
||||||
|
if (ITERATIONS % 2 == 1) memcpy(out, buf, sizeof(bitgrid_t));
|
||||||
|
|
||||||
|
extract_largest_region(out, bx0, by0, bx1, by1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int bitgrid_count(bitgrid_t g) {
|
||||||
|
int n = 0;
|
||||||
|
for (int y = 0; y < DROWS; y++)
|
||||||
|
for (int x = 0; x < DCOLS; x++)
|
||||||
|
if (g[y][x]) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
21
src/gen/ca.h
Normal file
21
src/gen/ca.h
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#ifndef GENESIS_GEN_CA_H
|
||||||
|
#define GENESIS_GEN_CA_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "grid.h"
|
||||||
|
#include "rng.h"
|
||||||
|
#include "events.h"
|
||||||
|
|
||||||
|
typedef uint8_t bitgrid_t[DROWS][DCOLS];
|
||||||
|
|
||||||
|
/* Generate a CA blob in a bounding box (x,y,w,h) within bitgrid.
|
||||||
|
Rule: B678/S45678, 55% seed probability, 5 iterations, then retain only the
|
||||||
|
largest 4-connected region within the box. Cells outside the box are 0.
|
||||||
|
Emits EV_CA_ITERATION for each of the 5 steps. */
|
||||||
|
void ca_blob(bitgrid_t out, rng_t *rng, event_sink_t *sink,
|
||||||
|
int x, int y, int w, int h);
|
||||||
|
|
||||||
|
/* Count filled cells in bitgrid. */
|
||||||
|
int bitgrid_count(bitgrid_t g);
|
||||||
|
|
||||||
|
#endif
|
||||||
134
src/gen/chokepoints.c
Normal file
134
src/gen/chokepoints.c
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
#include "chokepoints.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* Port of Brogue's chokepoint analysis (Architect.c:analyzeMap), simplified:
|
||||||
|
- Identify chokepoint cells — narrow passages where all 4 cardinal arcs
|
||||||
|
change state and one axis is fully blocked.
|
||||||
|
- For each chokepoint, flood-fill from its adjacent open neighbors while
|
||||||
|
pretending the chokepoint is blocked. The flood count is the "pocket
|
||||||
|
size" that depends on this chokepoint. Every cell in that pocket gets
|
||||||
|
min(current value, flood count) in the choke map.
|
||||||
|
- The chokepoint next to the smallest pocket it guards is flagged as the
|
||||||
|
pocket's gate site. */
|
||||||
|
|
||||||
|
static const int CDIRS[8][2] = {
|
||||||
|
{ 1, 0}, { 1, -1}, { 0, -1}, {-1, -1},
|
||||||
|
{-1, 0}, {-1, 1}, { 0, 1}, { 1, 1},
|
||||||
|
};
|
||||||
|
|
||||||
|
static int pass(grid_t g, int x, int y) {
|
||||||
|
if (!grid_in_bounds(x, y)) return 0;
|
||||||
|
return terrain_is_passable(g[y][x].terrain);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rotating around 8 neighbors, count transitions between pass/not-pass. */
|
||||||
|
static int narrow_passage(grid_t g, int x, int y) {
|
||||||
|
int arcs = 0;
|
||||||
|
for (int d = 0; d < 8; d++) {
|
||||||
|
int old_x = x + CDIRS[(d + 7) % 8][0];
|
||||||
|
int old_y = y + CDIRS[(d + 7) % 8][1];
|
||||||
|
int new_x = x + CDIRS[d][0];
|
||||||
|
int new_y = y + CDIRS[d][1];
|
||||||
|
int a = pass(g, new_x, new_y);
|
||||||
|
int b = pass(g, old_x, old_y);
|
||||||
|
if (a != b) {
|
||||||
|
if (++arcs > 2) {
|
||||||
|
int horiz_blocked = !pass(g, x - 1, y) && !pass(g, x + 1, y);
|
||||||
|
int vert_blocked = !pass(g, x, y - 1) && !pass(g, x, y + 1);
|
||||||
|
return horiz_blocked || vert_blocked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4-connected flood from (sx,sy) through passable cells, but treat
|
||||||
|
(block_x, block_y) as impassable. Marks visited cells in out[y][x] and
|
||||||
|
returns the count. */
|
||||||
|
static int flood_blocked(grid_t g, int sx, int sy,
|
||||||
|
int block_x, int block_y,
|
||||||
|
uint8_t out[DROWS][DCOLS]) {
|
||||||
|
memset(out, 0, (size_t)DROWS * DCOLS);
|
||||||
|
if (!pass(g, sx, sy)) return 0;
|
||||||
|
if (sx == block_x && sy == block_y) return 0;
|
||||||
|
static int qx[DCOLS * DROWS];
|
||||||
|
static int qy[DCOLS * DROWS];
|
||||||
|
int head = 0, tail = 0;
|
||||||
|
out[sy][sx] = 1;
|
||||||
|
qx[tail] = sx; qy[tail] = sy; tail++;
|
||||||
|
int count = 0;
|
||||||
|
static const int d4[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
|
||||||
|
while (head < tail) {
|
||||||
|
int cx = qx[head], cy = qy[head]; head++;
|
||||||
|
count++;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
int nx = cx + d4[i][0];
|
||||||
|
int ny = cy + d4[i][1];
|
||||||
|
if (!grid_in_bounds(nx, ny)) continue;
|
||||||
|
if (out[ny][nx]) continue;
|
||||||
|
if (nx == block_x && ny == block_y) continue;
|
||||||
|
if (!pass(g, nx, ny)) continue;
|
||||||
|
out[ny][nx] = 1;
|
||||||
|
qx[tail] = nx; qy[tail] = ny; tail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void compute_chokepoints(grid_t g, event_sink_t *sink,
|
||||||
|
choke_map_t choke_out,
|
||||||
|
choke_flag_t is_chokepoint,
|
||||||
|
choke_flag_t is_gate_site) {
|
||||||
|
/* Init */
|
||||||
|
for (int y = 0; y < DROWS; y++)
|
||||||
|
for (int x = 0; x < DCOLS; x++) {
|
||||||
|
choke_out[y][x] = CHOKE_UNREACHABLE;
|
||||||
|
is_chokepoint[y][x] = 0;
|
||||||
|
is_gate_site[y][x] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pass 1: flag chokepoints. */
|
||||||
|
for (int y = 1; y < DROWS - 1; y++) {
|
||||||
|
for (int x = 1; x < DCOLS - 1; x++) {
|
||||||
|
if (!pass(g, x, y)) continue;
|
||||||
|
if (narrow_passage(g, x, y)) is_chokepoint[y][x] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pass 2: for each chokepoint, flood through each open cardinal neighbor
|
||||||
|
with the chokepoint treated as blocked. Update choke_out for the
|
||||||
|
flooded pocket. Track gate sites. */
|
||||||
|
static uint8_t pocket[DROWS][DCOLS];
|
||||||
|
static const int d4[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
|
||||||
|
for (int cy = 0; cy < DROWS; cy++) {
|
||||||
|
for (int cx = 0; cx < DCOLS; cx++) {
|
||||||
|
if (!is_chokepoint[cy][cx]) continue;
|
||||||
|
int best_flood = CHOKE_UNREACHABLE;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
int nx = cx + d4[i][0];
|
||||||
|
int ny = cy + d4[i][1];
|
||||||
|
if (!pass(g, nx, ny)) continue;
|
||||||
|
if (is_chokepoint[ny][nx]) continue;
|
||||||
|
int n = flood_blocked(g, nx, ny, cx, cy, pocket);
|
||||||
|
if (n < 4) continue; /* too tiny to matter */
|
||||||
|
for (int py = 0; py < DROWS; py++) {
|
||||||
|
for (int px = 0; px < DCOLS; px++) {
|
||||||
|
if (!pocket[py][px]) continue;
|
||||||
|
if (n < choke_out[py][px]) {
|
||||||
|
choke_out[py][px] = (int16_t)n;
|
||||||
|
is_gate_site[py][px] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (n < best_flood) best_flood = n;
|
||||||
|
if (n < choke_out[cy][cx]) {
|
||||||
|
choke_out[cy][cx] = (int16_t)n;
|
||||||
|
is_gate_site[cy][cx] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best_flood < CHOKE_UNREACHABLE)
|
||||||
|
event_emit(sink, EV_CHOKEPOINT_FOUND, NULL, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/gen/chokepoints.h
Normal file
29
src/gen/chokepoints.h
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
#ifndef GENESIS_GEN_CHOKEPOINTS_H
|
||||||
|
#define GENESIS_GEN_CHOKEPOINTS_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "grid.h"
|
||||||
|
#include "events.h"
|
||||||
|
|
||||||
|
/* A cell's choke value is the number of passable cells that would become
|
||||||
|
unreachable if the nearest chokepoint between that cell and the rest of
|
||||||
|
the map were blocked. CHOKE_UNREACHABLE is the "untouched" sentinel. */
|
||||||
|
#define CHOKE_UNREACHABLE 30000
|
||||||
|
|
||||||
|
typedef int16_t choke_map_t[DROWS][DCOLS];
|
||||||
|
typedef uint8_t choke_flag_t[DROWS][DCOLS];
|
||||||
|
|
||||||
|
/* Compute:
|
||||||
|
- choke_out[y][x]: how many cells would be cut off if the relevant nearby
|
||||||
|
chokepoint were blocked. Lower = more isolated pocket.
|
||||||
|
- is_chokepoint[y][x]: 1 if cell is a chokepoint (articulation point).
|
||||||
|
- is_gate_site[y][x]: 1 if cell is the smallest chokepoint seen for its
|
||||||
|
pocket (ideal vestibule site).
|
||||||
|
|
||||||
|
Each emits at most one CHOKEPOINT_FOUND event per gate site. */
|
||||||
|
void compute_chokepoints(grid_t g, event_sink_t *sink,
|
||||||
|
choke_map_t choke_out,
|
||||||
|
choke_flag_t is_chokepoint,
|
||||||
|
choke_flag_t is_gate_site);
|
||||||
|
|
||||||
|
#endif
|
||||||
63
src/gen/dijkstra.c
Normal file
63
src/gen/dijkstra.c
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
#include "dijkstra.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
static int passable(uint8_t t) {
|
||||||
|
return t == T_FLOOR || t == T_CORRIDOR || t == T_DOOR || t == T_BRIDGE
|
||||||
|
|| t == T_STAIRS_UP || t == T_STAIRS_DOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bfs_from(grid_t g, int sx, int sy, dist_map_t dist,
|
||||||
|
int extra_passable_x, int extra_passable_y) {
|
||||||
|
for (int y = 0; y < DROWS; y++)
|
||||||
|
for (int x = 0; x < DCOLS; x++)
|
||||||
|
dist[y][x] = DIST_UNREACHABLE;
|
||||||
|
|
||||||
|
if (!grid_in_bounds(sx, sy)) return;
|
||||||
|
|
||||||
|
static const int dx4[4] = {1, -1, 0, 0};
|
||||||
|
static const int dy4[4] = {0, 0, 1, -1};
|
||||||
|
|
||||||
|
int qx[DCOLS * DROWS];
|
||||||
|
int qy[DCOLS * DROWS];
|
||||||
|
int head = 0, tail = 0;
|
||||||
|
|
||||||
|
dist[sy][sx] = 0;
|
||||||
|
qx[tail] = sx; qy[tail] = sy; tail++;
|
||||||
|
|
||||||
|
while (head < tail) {
|
||||||
|
int cx = qx[head], cy = qy[head]; head++;
|
||||||
|
int16_t d = dist[cy][cx];
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
int nx = cx + dx4[i], ny = cy + dy4[i];
|
||||||
|
if (!grid_in_bounds(nx, ny)) continue;
|
||||||
|
int is_extra = (nx == extra_passable_x && ny == extra_passable_y);
|
||||||
|
if (!is_extra && !passable(g[ny][nx].terrain)) continue;
|
||||||
|
if (dist[ny][nx] <= d + 1) continue;
|
||||||
|
dist[ny][nx] = d + 1;
|
||||||
|
qx[tail] = nx; qy[tail] = ny; tail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int is_connected(grid_t g, int *out_unreached) {
|
||||||
|
int sx = -1, sy = -1, total = 0;
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++) {
|
||||||
|
if (passable(g[y][x].terrain)) {
|
||||||
|
if (sx < 0) { sx = x; sy = y; }
|
||||||
|
total++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (total == 0) { if (out_unreached) *out_unreached = 0; return 1; }
|
||||||
|
|
||||||
|
static dist_map_t dist;
|
||||||
|
bfs_from(g, sx, sy, dist, -1, -1);
|
||||||
|
int reached = 0;
|
||||||
|
for (int y = 0; y < DROWS; y++)
|
||||||
|
for (int x = 0; x < DCOLS; x++)
|
||||||
|
if (passable(g[y][x].terrain) && dist[y][x] != DIST_UNREACHABLE)
|
||||||
|
reached++;
|
||||||
|
if (out_unreached) *out_unreached = total - reached;
|
||||||
|
return reached == total;
|
||||||
|
}
|
||||||
22
src/gen/dijkstra.h
Normal file
22
src/gen/dijkstra.h
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#ifndef GENESIS_GEN_DIJKSTRA_H
|
||||||
|
#define GENESIS_GEN_DIJKSTRA_H
|
||||||
|
|
||||||
|
#include "grid.h"
|
||||||
|
|
||||||
|
#define DIST_UNREACHABLE 30000
|
||||||
|
|
||||||
|
typedef int16_t dist_map_t[DROWS][DCOLS];
|
||||||
|
|
||||||
|
/* 4-connected BFS from (sx,sy) through passable terrain (floor/corridor/door).
|
||||||
|
dist[][] filled with shortest path length in cells; DIST_UNREACHABLE if no path.
|
||||||
|
If extra_passable_x/y >= 0, that wall cell is treated as passable for the search. */
|
||||||
|
void bfs_from(grid_t g, int sx, int sy, dist_map_t dist,
|
||||||
|
int extra_passable_x, int extra_passable_y);
|
||||||
|
|
||||||
|
/* Returns 1 if every passable cell (floor/corridor/door/bridge) in g is
|
||||||
|
4-connected to every other, 0 otherwise. Trivially true if zero passable
|
||||||
|
cells. If out_unreached is non-NULL, writes the count of passable cells that
|
||||||
|
were NOT reached from the first seed. */
|
||||||
|
int is_connected(grid_t g, int *out_unreached);
|
||||||
|
|
||||||
|
#endif
|
||||||
127
src/gen/events.c
Normal file
127
src/gen/events.c
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
#include "events.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
void event_emit(event_sink_t *sink, event_kind_t kind,
|
||||||
|
const cell_diff_t *diffs, uint32_t diff_count) {
|
||||||
|
if (!sink || !sink->fn) return;
|
||||||
|
gen_event_t ev = {0};
|
||||||
|
ev.step = sink->step++;
|
||||||
|
ev.kind = (uint16_t)kind;
|
||||||
|
ev.diffs = (cell_diff_t *)diffs;
|
||||||
|
ev.diff_count = diff_count;
|
||||||
|
sink->fn(sink->ctx, &ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *event_kind_name(event_kind_t k) {
|
||||||
|
switch (k) {
|
||||||
|
case EV_GEN_BEGIN: return "GEN_BEGIN";
|
||||||
|
case EV_ROOM_PROPOSED: return "ROOM_PROPOSED";
|
||||||
|
case EV_ROOM_PLACED: return "ROOM_PLACED";
|
||||||
|
case EV_ROOM_REJECTED: return "ROOM_REJECTED";
|
||||||
|
case EV_DOOR_SITE_CHOSEN: return "DOOR_SITE_CHOSEN";
|
||||||
|
case EV_CORRIDOR_CARVED: return "CORRIDOR_CARVED";
|
||||||
|
case EV_LOOP_TESTED: return "LOOP_TESTED";
|
||||||
|
case EV_LOOP_ACCEPTED: return "LOOP_ACCEPTED";
|
||||||
|
case EV_CA_ITERATION: return "CA_ITERATION";
|
||||||
|
case EV_LAKE_PROPOSED: return "LAKE_PROPOSED";
|
||||||
|
case EV_LAKE_REJECTED_IMPASSABLE: return "LAKE_REJECTED_IMPASSABLE";
|
||||||
|
case EV_LAKE_PLACED: return "LAKE_PLACED";
|
||||||
|
case EV_WREATH_APPLIED: return "WREATH_APPLIED";
|
||||||
|
case EV_BRIDGE_PLACED: return "BRIDGE_PLACED";
|
||||||
|
case EV_WALLS_FINISHED: return "WALLS_FINISHED";
|
||||||
|
case EV_STAIRS_PLACED_UP: return "STAIRS_PLACED_UP";
|
||||||
|
case EV_STAIRS_PLACED_DOWN: return "STAIRS_PLACED_DOWN";
|
||||||
|
case EV_CHOKEPOINT_FOUND: return "CHOKEPOINT_FOUND";
|
||||||
|
case EV_MACHINE_PROPOSED: return "MACHINE_PROPOSED";
|
||||||
|
case EV_BLUEPRINT_CASCADE_BEGIN: return "BLUEPRINT_CASCADE_BEGIN";
|
||||||
|
case EV_FEATURE_BUILT: return "FEATURE_BUILT";
|
||||||
|
case EV_MACHINE_ACCEPTED: return "MACHINE_ACCEPTED";
|
||||||
|
case EV_MACHINE_RESTORED_ON_FAIL: return "MACHINE_RESTORED_ON_FAIL";
|
||||||
|
case EV_GEN_END: return "GEN_END";
|
||||||
|
default: return "?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *event_kind_what(event_kind_t k) {
|
||||||
|
switch (k) {
|
||||||
|
case EV_GEN_BEGIN: return "Phase 0: empty grid";
|
||||||
|
case EV_ROOM_PROPOSED: return "Phase 1: trying a room";
|
||||||
|
case EV_DOOR_SITE_CHOSEN: return "Phase 1: door site picked";
|
||||||
|
case EV_CORRIDOR_CARVED: return "Phase 1: corridor carved";
|
||||||
|
case EV_ROOM_PLACED: return "Phase 1: room placed";
|
||||||
|
case EV_ROOM_REJECTED: return "Phase 1: room gave up";
|
||||||
|
case EV_LOOP_TESTED: return "Phase 2: testing a loop";
|
||||||
|
case EV_LOOP_ACCEPTED: return "Phase 2: loop punched";
|
||||||
|
case EV_CA_ITERATION: return "Phase 3: CA smoothing step";
|
||||||
|
case EV_LAKE_PROPOSED: return "Phase 3: lake proposed";
|
||||||
|
case EV_LAKE_REJECTED_IMPASSABLE: return "Phase 3: lake rejected";
|
||||||
|
case EV_LAKE_PLACED: return "Phase 3: lake placed";
|
||||||
|
case EV_WREATH_APPLIED: return "Phase 3: shorelines marked";
|
||||||
|
case EV_BRIDGE_PLACED: return "Phase 3: bridge laid";
|
||||||
|
case EV_WALLS_FINISHED: return "Phase 4: walls finished";
|
||||||
|
case EV_STAIRS_PLACED_UP: return "Phase 5: up-stair placed";
|
||||||
|
case EV_STAIRS_PLACED_DOWN: return "Phase 5: down-stair placed";
|
||||||
|
case EV_CHOKEPOINT_FOUND: return "Phase 6: chokepoint found";
|
||||||
|
case EV_MACHINE_PROPOSED: return "Phase 6: machine proposed";
|
||||||
|
case EV_BLUEPRINT_CASCADE_BEGIN: return "Phase 6: cascade begins";
|
||||||
|
case EV_FEATURE_BUILT: return "Phase 6: feature built";
|
||||||
|
case EV_MACHINE_ACCEPTED: return "Phase 6: machine accepted";
|
||||||
|
case EV_MACHINE_RESTORED_ON_FAIL: return "Phase 6: machine rolled back";
|
||||||
|
case EV_GEN_END: return "Done";
|
||||||
|
default: return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *event_kind_why(event_kind_t k) {
|
||||||
|
switch (k) {
|
||||||
|
case EV_GEN_BEGIN:
|
||||||
|
return "Bare 79x29 grid. Phases run in order: rooms -> loops -> lakes -> walls.";
|
||||||
|
case EV_ROOM_PROPOSED:
|
||||||
|
return "Carved a candidate room in scratch space. Searching for a place to attach it.";
|
||||||
|
case EV_DOOR_SITE_CHOSEN:
|
||||||
|
return "An existing room has a perimeter cell facing empty space - that's the doorway.";
|
||||||
|
case EV_CORRIDOR_CARVED:
|
||||||
|
return "Vertical corridors are 2-9 cells, horizontal 5-15. Connects the candidate to the existing dungeon.";
|
||||||
|
case EV_ROOM_PLACED:
|
||||||
|
return "Buffer + corridor checks passed. Map remains fully connected by construction.";
|
||||||
|
case EV_ROOM_REJECTED:
|
||||||
|
return "After 30 attempts, no valid attachment was found. Move on.";
|
||||||
|
case EV_LOOP_TESTED:
|
||||||
|
return "Wall with passable cells on opposite sides. If their shortest path is far apart, a shortcut helps.";
|
||||||
|
case EV_LOOP_ACCEPTED:
|
||||||
|
return "Graph distance >= 20. Punching the wall creates a useful loop in the dungeon.";
|
||||||
|
case EV_CA_ITERATION:
|
||||||
|
return "Cellular automata smoothing the lake blob: B678/S45678 rule, 5 iterations total.";
|
||||||
|
case EV_LAKE_PROPOSED:
|
||||||
|
return "Blue overlay = where the lake would go. Red overlay = cells that would become unreachable.";
|
||||||
|
case EV_LAKE_REJECTED_IMPASSABLE:
|
||||||
|
return "Lake would orphan part of the map. Reject and try a smaller bounding box.";
|
||||||
|
case EV_LAKE_PLACED:
|
||||||
|
return "Passability test passed. Lake painted onto the map; connectivity preserved.";
|
||||||
|
case EV_WREATH_APPLIED:
|
||||||
|
return "Cells adjacent (8-conn) to liquid are tagged shoreline. Used later for visual variety.";
|
||||||
|
case EV_BRIDGE_PLACED:
|
||||||
|
return "Found a floor-liquid-floor span <= 5 cells. Liquid replaced with a bridge.";
|
||||||
|
case EV_WALLS_FINISHED:
|
||||||
|
return "Polish pass: every empty cell next to any open terrain becomes a wall.";
|
||||||
|
case EV_STAIRS_PLACED_UP:
|
||||||
|
return "Up-stair placed at the first room's center - the level's entry point.";
|
||||||
|
case EV_STAIRS_PLACED_DOWN:
|
||||||
|
return "Down-stair placed at the floor cell with the longest graph-distance from the up-stair.";
|
||||||
|
case EV_CHOKEPOINT_FOUND:
|
||||||
|
return "Articulation point: if blocked, part of the map becomes unreachable. Ideal vestibule candidate.";
|
||||||
|
case EV_MACHINE_PROPOSED:
|
||||||
|
return "A blueprint is being tried. Looking for a chokepoint or room that matches the constraints.";
|
||||||
|
case EV_BLUEPRINT_CASCADE_BEGIN:
|
||||||
|
return "Reward rooms may spawn vestibules, which may spawn key-guard machines - a cascade of nested blueprints.";
|
||||||
|
case EV_FEATURE_BUILT:
|
||||||
|
return "A single feature within the current blueprint was placed (terrain / item marker / monster marker).";
|
||||||
|
case EV_MACHINE_ACCEPTED:
|
||||||
|
return "All required features fit. Machine committed; any nested cascades ran successfully.";
|
||||||
|
case EV_MACHINE_RESTORED_ON_FAIL:
|
||||||
|
return "Placement couldn't satisfy all constraints. Level state is rolled back to before the attempt.";
|
||||||
|
case EV_GEN_END:
|
||||||
|
return "Final dungeon. Every passable cell is 4-connected to every other - Brogue's invariant.";
|
||||||
|
default: return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/gen/events.h
Normal file
68
src/gen/events.h
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
#ifndef GENESIS_GEN_EVENTS_H
|
||||||
|
#define GENESIS_GEN_EVENTS_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "grid.h"
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
EV_GEN_BEGIN = 0,
|
||||||
|
EV_ROOM_PROPOSED,
|
||||||
|
EV_ROOM_PLACED,
|
||||||
|
EV_ROOM_REJECTED,
|
||||||
|
EV_DOOR_SITE_CHOSEN,
|
||||||
|
EV_CORRIDOR_CARVED,
|
||||||
|
EV_LOOP_TESTED,
|
||||||
|
EV_LOOP_ACCEPTED,
|
||||||
|
EV_CA_ITERATION,
|
||||||
|
EV_LAKE_PROPOSED,
|
||||||
|
EV_LAKE_REJECTED_IMPASSABLE,
|
||||||
|
EV_LAKE_PLACED,
|
||||||
|
EV_WREATH_APPLIED,
|
||||||
|
EV_BRIDGE_PLACED,
|
||||||
|
EV_WALLS_FINISHED,
|
||||||
|
EV_STAIRS_PLACED_UP,
|
||||||
|
EV_STAIRS_PLACED_DOWN,
|
||||||
|
EV_CHOKEPOINT_FOUND,
|
||||||
|
EV_MACHINE_PROPOSED,
|
||||||
|
EV_BLUEPRINT_CASCADE_BEGIN,
|
||||||
|
EV_FEATURE_BUILT,
|
||||||
|
EV_MACHINE_ACCEPTED,
|
||||||
|
EV_MACHINE_RESTORED_ON_FAIL,
|
||||||
|
EV_GEN_END,
|
||||||
|
EV_KIND_COUNT
|
||||||
|
} event_kind_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint16_t x;
|
||||||
|
uint16_t y;
|
||||||
|
cell_t before;
|
||||||
|
cell_t after;
|
||||||
|
} cell_diff_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t step;
|
||||||
|
uint16_t kind;
|
||||||
|
uint16_t payload_u16[4];
|
||||||
|
int32_t payload_i32[2];
|
||||||
|
uint32_t diff_count;
|
||||||
|
cell_diff_t *diffs;
|
||||||
|
} gen_event_t;
|
||||||
|
|
||||||
|
typedef void (*event_sink_fn)(void *ctx, const gen_event_t *ev);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
event_sink_fn fn;
|
||||||
|
void *ctx;
|
||||||
|
uint32_t step;
|
||||||
|
} event_sink_t;
|
||||||
|
|
||||||
|
void event_emit(event_sink_t *sink, event_kind_t kind,
|
||||||
|
const cell_diff_t *diffs, uint32_t diff_count);
|
||||||
|
|
||||||
|
const char *event_kind_name(event_kind_t k);
|
||||||
|
/* Short human title (~20 chars). */
|
||||||
|
const char *event_kind_what(event_kind_t k);
|
||||||
|
/* One-line explanation of why this step matters. May be ~120 chars. */
|
||||||
|
const char *event_kind_why(event_kind_t k);
|
||||||
|
|
||||||
|
#endif
|
||||||
83
src/gen/grid.c
Normal file
83
src/gen/grid.c
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
#include "grid.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
void grid_fill(grid_t g, terrain_t t) {
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++) {
|
||||||
|
memset(&g[y][x], 0, sizeof(cell_t));
|
||||||
|
g[y][x].terrain = (uint8_t)t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int grid_in_bounds(int x, int y) {
|
||||||
|
return x >= 0 && x < DCOLS && y >= 0 && y < DROWS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void grid_draw_rect(grid_t g, int x, int y, int w, int h, terrain_t t, uint8_t room_id) {
|
||||||
|
for (int j = y; j < y + h; j++) {
|
||||||
|
for (int i = x; i < x + w; i++) {
|
||||||
|
if (!grid_in_bounds(i, j)) continue;
|
||||||
|
g[j][i].terrain = (uint8_t)t;
|
||||||
|
if (room_id) {
|
||||||
|
g[j][i].room_id = room_id;
|
||||||
|
g[j][i].flags |= F_IN_ROOM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void grid_draw_circle(grid_t g, int cx, int cy, int r, terrain_t t, uint8_t room_id) {
|
||||||
|
int r2 = r * r;
|
||||||
|
for (int j = cy - r; j <= cy + r; j++) {
|
||||||
|
for (int i = cx - r; i <= cx + r; i++) {
|
||||||
|
if (!grid_in_bounds(i, j)) continue;
|
||||||
|
int dx = i - cx, dy = j - cy;
|
||||||
|
if (dx * dx + dy * dy <= r2) {
|
||||||
|
g[j][i].terrain = (uint8_t)t;
|
||||||
|
if (room_id) {
|
||||||
|
g[j][i].room_id = room_id;
|
||||||
|
g[j][i].flags |= F_IN_ROOM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flood fill 4-connected floor cells; writes marker into room_id for visited floor.
|
||||||
|
Returns count of cells filled. Non-destructive to terrain. */
|
||||||
|
int grid_flood_fill4(grid_t g, int x, int y, uint8_t marker) {
|
||||||
|
if (!grid_in_bounds(x, y)) return 0;
|
||||||
|
if (g[y][x].terrain != T_FLOOR && g[y][x].terrain != T_CORRIDOR && g[y][x].terrain != T_DOOR) return 0;
|
||||||
|
if (g[y][x].room_id == marker) return 0;
|
||||||
|
|
||||||
|
int stack_x[DCOLS * DROWS];
|
||||||
|
int stack_y[DCOLS * DROWS];
|
||||||
|
int sp = 0;
|
||||||
|
stack_x[sp] = x; stack_y[sp] = y; sp++;
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
while (sp > 0) {
|
||||||
|
sp--;
|
||||||
|
int cx = stack_x[sp], cy = stack_y[sp];
|
||||||
|
if (!grid_in_bounds(cx, cy)) continue;
|
||||||
|
cell_t *c = &g[cy][cx];
|
||||||
|
if (c->terrain != T_FLOOR && c->terrain != T_CORRIDOR && c->terrain != T_DOOR) continue;
|
||||||
|
if (c->room_id == marker) continue;
|
||||||
|
c->room_id = marker;
|
||||||
|
count++;
|
||||||
|
stack_x[sp] = cx + 1; stack_y[sp] = cy; sp++;
|
||||||
|
stack_x[sp] = cx - 1; stack_y[sp] = cy; sp++;
|
||||||
|
stack_x[sp] = cx; stack_y[sp] = cy + 1; sp++;
|
||||||
|
stack_x[sp] = cx; stack_y[sp] = cy - 1; sp++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int grid_count_floor(grid_t g) {
|
||||||
|
int n = 0;
|
||||||
|
for (int y = 0; y < DROWS; y++)
|
||||||
|
for (int x = 0; x < DCOLS; x++)
|
||||||
|
if (g[y][x].terrain == T_FLOOR || g[y][x].terrain == T_CORRIDOR || g[y][x].terrain == T_DOOR) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
90
src/gen/grid.h
Normal file
90
src/gen/grid.h
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
#ifndef GENESIS_GEN_GRID_H
|
||||||
|
#define GENESIS_GEN_GRID_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define DCOLS 79
|
||||||
|
#define DROWS 29
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
T_NOTHING = 0,
|
||||||
|
T_FLOOR = 1,
|
||||||
|
T_WALL = 2,
|
||||||
|
T_DOOR = 3,
|
||||||
|
T_CORRIDOR = 4,
|
||||||
|
T_LIQUID = 5,
|
||||||
|
T_BRIDGE = 6,
|
||||||
|
T_STAIRS_UP = 7,
|
||||||
|
T_STAIRS_DOWN = 8,
|
||||||
|
} terrain_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
L_NONE = 0,
|
||||||
|
L_WATER = 1,
|
||||||
|
L_LAVA = 2,
|
||||||
|
L_CHASM = 3,
|
||||||
|
L_BRIMSTONE = 4,
|
||||||
|
} liquid_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
F_IN_ROOM = 1u << 0,
|
||||||
|
F_IN_LOOP = 1u << 1,
|
||||||
|
F_CHOKEPT = 1u << 2,
|
||||||
|
F_BRIDGE = 1u << 3,
|
||||||
|
F_WREATH = 1u << 4,
|
||||||
|
/* Set on cells of a lake currently being tested for passability. Cleared
|
||||||
|
on the next event (rejection or placement). Lets the viewer render the
|
||||||
|
proposal overlay without any out-of-band metadata. */
|
||||||
|
F_PROPOSED_LAKE = 1u << 5,
|
||||||
|
F_IN_MACHINE = 1u << 6, /* cell belongs to a machine instance */
|
||||||
|
F_MACHINE_INTERIOR = 1u << 7, /* interior cell (not boundary) */
|
||||||
|
F_MACHINE_GATE = 1u << 8, /* the vestibule / entry point */
|
||||||
|
F_MACHINE_IMPREGNABLE = 1u << 9, /* later phases must not alter */
|
||||||
|
F_MACHINE_KEY = 1u << 10, /* key goes here (gameplay hint) */
|
||||||
|
} cell_flag_t;
|
||||||
|
|
||||||
|
/* Semantic placement markers, stored in cell.surface. Brogue uses the surface
|
||||||
|
byte for atmospherics (gas/fluid splash). We repurpose it for machine
|
||||||
|
markers, which the Godot RL interprets to spawn concrete items / decor /
|
||||||
|
monsters. If we later want atmospherics back, cell_t can grow to 12 bytes
|
||||||
|
and the .gen format bumps version; for MVP 8 bytes is plenty. */
|
||||||
|
typedef enum {
|
||||||
|
MK_NONE = 0,
|
||||||
|
MK_CARPET, /* decorative floor tint */
|
||||||
|
MK_ALTAR, /* item pedestal on an altar */
|
||||||
|
MK_CAGE, /* item behind a cage (key-guarded) */
|
||||||
|
MK_PEDESTAL, /* item on pedestal */
|
||||||
|
MK_STATUE, /* decorative statue */
|
||||||
|
MK_TORCH, /* light source */
|
||||||
|
MK_BRAZIER, /* light source */
|
||||||
|
MK_CHEST, /* item container */
|
||||||
|
MK_SPAWN_MONSTER, /* Godot spawns an enemy here */
|
||||||
|
MK_SPAWN_BOSS,
|
||||||
|
MK_KEY_ITEM, /* the key that unlocks this machine's parent */
|
||||||
|
} marker_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t terrain;
|
||||||
|
uint8_t liquid;
|
||||||
|
uint8_t surface; /* marker_t — repurposed; see above */
|
||||||
|
uint8_t gas;
|
||||||
|
uint16_t flags;
|
||||||
|
uint8_t room_id;
|
||||||
|
uint8_t machine_id; /* 0 = not in any machine, 1..N = machine instance */
|
||||||
|
} cell_t;
|
||||||
|
|
||||||
|
typedef cell_t grid_t[DROWS][DCOLS];
|
||||||
|
|
||||||
|
void grid_fill(grid_t g, terrain_t t);
|
||||||
|
int grid_in_bounds(int x, int y);
|
||||||
|
/* True if the terrain is walkable (floor, corridor, door, bridge, stairs). */
|
||||||
|
static inline int terrain_is_passable(uint8_t t) {
|
||||||
|
return t == T_FLOOR || t == T_CORRIDOR || t == T_DOOR || t == T_BRIDGE
|
||||||
|
|| t == T_STAIRS_UP || t == T_STAIRS_DOWN;
|
||||||
|
}
|
||||||
|
void grid_draw_rect(grid_t g, int x, int y, int w, int h, terrain_t t, uint8_t room_id);
|
||||||
|
void grid_draw_circle(grid_t g, int cx, int cy, int r, terrain_t t, uint8_t room_id);
|
||||||
|
int grid_flood_fill4(grid_t g, int x, int y, uint8_t marker);
|
||||||
|
int grid_count_floor(grid_t g);
|
||||||
|
|
||||||
|
#endif
|
||||||
191
src/gen/lakes.c
Normal file
191
src/gen/lakes.c
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
#include "lakes.h"
|
||||||
|
#include "ca.h"
|
||||||
|
#include "dijkstra.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
static const int LAKE_BOXES[][2] = {
|
||||||
|
{30, 15}, {30, 15}, {28, 14}, {24, 12}, {22, 11}, {20, 10},
|
||||||
|
};
|
||||||
|
#define LAKE_COUNT (int)(sizeof(LAKE_BOXES) / sizeof(LAKE_BOXES[0]))
|
||||||
|
|
||||||
|
static int is_passable(uint8_t t) {
|
||||||
|
return t == T_FLOOR || t == T_CORRIDOR || t == T_DOOR || t == T_BRIDGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Returns 1 if the proposed lake preserves connectivity of all remaining
|
||||||
|
passable cells on the map. */
|
||||||
|
static int passability_test(grid_t g, bitgrid_t lake) {
|
||||||
|
/* Find first passable cell not in lake. */
|
||||||
|
int sx = -1, sy = -1;
|
||||||
|
int total = 0;
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++) {
|
||||||
|
if (lake[y][x]) continue;
|
||||||
|
if (is_passable(g[y][x].terrain)) {
|
||||||
|
if (sx < 0) { sx = x; sy = y; }
|
||||||
|
total++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sx < 0) return 1; /* no passable cells — trivially fine */
|
||||||
|
|
||||||
|
/* BFS, treating lake as impassable. */
|
||||||
|
static int visited[DROWS][DCOLS];
|
||||||
|
memset(visited, 0, sizeof(visited));
|
||||||
|
int qx[DCOLS * DROWS], qy[DCOLS * DROWS];
|
||||||
|
int head = 0, tail = 0;
|
||||||
|
qx[tail] = sx; qy[tail] = sy; tail++;
|
||||||
|
visited[sy][sx] = 1;
|
||||||
|
int reached = 0;
|
||||||
|
static const int dx4[4] = {1, -1, 0, 0};
|
||||||
|
static const int dy4[4] = {0, 0, 1, -1};
|
||||||
|
while (head < tail) {
|
||||||
|
int cx = qx[head], cy = qy[head]; head++;
|
||||||
|
reached++;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
int nx = cx + dx4[i], ny = cy + dy4[i];
|
||||||
|
if (!grid_in_bounds(nx, ny)) continue;
|
||||||
|
if (visited[ny][nx]) continue;
|
||||||
|
if (lake[ny][nx]) continue;
|
||||||
|
if (!is_passable(g[ny][nx].terrain)) continue;
|
||||||
|
visited[ny][nx] = 1;
|
||||||
|
qx[tail] = nx; qy[tail] = ny; tail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reached == total;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void paint_lake(grid_t g, bitgrid_t lake, liquid_t kind) {
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++) {
|
||||||
|
if (!lake[y][x]) continue;
|
||||||
|
g[y][x].terrain = T_LIQUID;
|
||||||
|
g[y][x].liquid = (uint8_t)kind;
|
||||||
|
g[y][x].flags &= (uint16_t)~F_IN_ROOM;
|
||||||
|
g[y][x].room_id = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Brogue-ish weighted selection by depth. Water dominates shallow; lava,
|
||||||
|
chasm, brimstone phase in as depth grows. */
|
||||||
|
static liquid_t pick_liquid(rng_t *rng, int depth) {
|
||||||
|
int w_water = (depth >= 1 && depth <= 14) ? 20 : 0;
|
||||||
|
int w_lava = (depth >= 3 && depth <= 17) ? (depth - 2) : 0;
|
||||||
|
int w_chasm = (depth >= 7 && depth <= 24) ? (depth - 6) : 0;
|
||||||
|
int w_brimstone = (depth >= 14 && depth <= 26) ? (depth - 13) : 0;
|
||||||
|
int total = w_water + w_lava + w_chasm + w_brimstone;
|
||||||
|
if (total <= 0) return L_WATER;
|
||||||
|
int roll = rng_range(rng, 0, total - 1);
|
||||||
|
if ((roll -= w_water) < 0) return L_WATER;
|
||||||
|
if ((roll -= w_lava) < 0) return L_LAVA;
|
||||||
|
if ((roll -= w_chasm) < 0) return L_CHASM;
|
||||||
|
return L_BRIMSTONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void mark_proposed(grid_t g, bitgrid_t lake, int set) {
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++) {
|
||||||
|
if (!lake[y][x]) continue;
|
||||||
|
if (set) g[y][x].flags |= F_PROPOSED_LAKE;
|
||||||
|
else g[y][x].flags &= (uint16_t)~F_PROPOSED_LAKE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int place_lakes(grid_t g, rng_t *rng, event_sink_t *sink, int depth) {
|
||||||
|
(void)depth; /* used in Phase A.2 for liquid type selection */
|
||||||
|
int placed = 0;
|
||||||
|
static bitgrid_t lake;
|
||||||
|
|
||||||
|
for (int i = 0; i < LAKE_COUNT; i++) {
|
||||||
|
int bw = LAKE_BOXES[i][0];
|
||||||
|
int bh = LAKE_BOXES[i][1];
|
||||||
|
if (bw >= DCOLS - 2) bw = DCOLS - 3;
|
||||||
|
if (bh >= DROWS - 2) bh = DROWS - 3;
|
||||||
|
|
||||||
|
int max_bx = DCOLS - bw - 1;
|
||||||
|
int max_by = DROWS - bh - 1;
|
||||||
|
if (max_bx < 1) max_bx = 1;
|
||||||
|
if (max_by < 1) max_by = 1;
|
||||||
|
int bx = rng_range(rng, 1, max_bx);
|
||||||
|
int by = rng_range(rng, 1, max_by);
|
||||||
|
|
||||||
|
ca_blob(lake, rng, sink, bx, by, bw, bh);
|
||||||
|
if (bitgrid_count(lake) < 5) continue;
|
||||||
|
|
||||||
|
/* Mark proposal so the recorder + viewer can show what's being
|
||||||
|
tested. The flag is cleared before the outcome event. */
|
||||||
|
mark_proposed(g, lake, 1);
|
||||||
|
event_emit(sink, EV_LAKE_PROPOSED, NULL, 0);
|
||||||
|
|
||||||
|
if (!passability_test(g, lake)) {
|
||||||
|
mark_proposed(g, lake, 0);
|
||||||
|
event_emit(sink, EV_LAKE_REJECTED_IMPASSABLE, NULL, 0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
mark_proposed(g, lake, 0);
|
||||||
|
paint_lake(g, lake, pick_liquid(rng, depth));
|
||||||
|
event_emit(sink, EV_LAKE_PLACED, NULL, 0);
|
||||||
|
placed++;
|
||||||
|
}
|
||||||
|
return placed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_wreaths(grid_t g, event_sink_t *sink) {
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++) {
|
||||||
|
if (g[y][x].terrain != T_LIQUID) continue;
|
||||||
|
for (int dy = -1; dy <= 1; dy++) {
|
||||||
|
for (int dx = -1; dx <= 1; dx++) {
|
||||||
|
if (dx == 0 && dy == 0) continue;
|
||||||
|
int qx = x + dx, qy = y + dy;
|
||||||
|
if (!grid_in_bounds(qx, qy)) continue;
|
||||||
|
if (g[qy][qx].terrain == T_LIQUID) continue;
|
||||||
|
g[qy][qx].flags |= F_WREATH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event_emit(sink, EV_WREATH_APPLIED, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int try_bridge_run(grid_t g, int x, int y, int dx, int dy, event_sink_t *sink) {
|
||||||
|
/* (x,y) must be passable floor; the next cells must be liquid for 1..5
|
||||||
|
steps, terminating at another passable cell. Water and chasms support
|
||||||
|
bridges; lava and brimstone don't. */
|
||||||
|
if (!is_passable(g[y][x].terrain)) return 0;
|
||||||
|
int k = 0;
|
||||||
|
int nx = x + dx, ny = y + dy;
|
||||||
|
while (grid_in_bounds(nx, ny) && g[ny][nx].terrain == T_LIQUID && k < 5) {
|
||||||
|
uint8_t l = g[ny][nx].liquid;
|
||||||
|
if (l != L_WATER && l != L_CHASM) return 0;
|
||||||
|
k++;
|
||||||
|
nx += dx; ny += dy;
|
||||||
|
}
|
||||||
|
if (k == 0) return 0;
|
||||||
|
if (!grid_in_bounds(nx, ny)) return 0;
|
||||||
|
if (!is_passable(g[ny][nx].terrain)) return 0;
|
||||||
|
|
||||||
|
for (int i = 1; i <= k; i++) {
|
||||||
|
int bx = x + i * dx, by = y + i * dy;
|
||||||
|
g[by][bx].terrain = T_BRIDGE;
|
||||||
|
g[by][bx].liquid = L_NONE;
|
||||||
|
g[by][bx].flags |= F_BRIDGE;
|
||||||
|
}
|
||||||
|
event_emit(sink, EV_BRIDGE_PLACED, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int place_bridges(grid_t g, event_sink_t *sink) {
|
||||||
|
int n = 0;
|
||||||
|
for (int y = 1; y < DROWS - 1; y++) {
|
||||||
|
for (int x = 1; x < DCOLS - 1; x++) {
|
||||||
|
if (!is_passable(g[y][x].terrain)) continue;
|
||||||
|
n += try_bridge_run(g, x, y, 1, 0, sink);
|
||||||
|
n += try_bridge_run(g, x, y, 0, 1, sink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
23
src/gen/lakes.h
Normal file
23
src/gen/lakes.h
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
#ifndef GENESIS_GEN_LAKES_H
|
||||||
|
#define GENESIS_GEN_LAKES_H
|
||||||
|
|
||||||
|
#include "grid.h"
|
||||||
|
#include "rng.h"
|
||||||
|
#include "events.h"
|
||||||
|
|
||||||
|
/* Attempt up to 6 lakes at decreasing bounding-box sizes, with passability
|
||||||
|
flood-fill rejection. Emits LAKE_PROPOSED, LAKE_REJECTED_IMPASSABLE,
|
||||||
|
LAKE_PLACED. Depth influences liquid type selection (added in Phase A.2).
|
||||||
|
Returns number of lakes placed. */
|
||||||
|
int place_lakes(grid_t g, rng_t *rng, event_sink_t *sink, int depth);
|
||||||
|
|
||||||
|
/* Mark F_WREATH on every non-liquid cell adjacent (8-neighbor) to a liquid
|
||||||
|
cell. Emits WREATH_APPLIED once. */
|
||||||
|
void apply_wreaths(grid_t g, event_sink_t *sink);
|
||||||
|
|
||||||
|
/* Bridge scan: for each cardinal axis, find floor…liquid×k…floor spans with
|
||||||
|
1 ≤ k ≤ 5 and convert those liquid cells to T_BRIDGE. Emits BRIDGE_PLACED per
|
||||||
|
bridge. */
|
||||||
|
int place_bridges(grid_t g, event_sink_t *sink);
|
||||||
|
|
||||||
|
#endif
|
||||||
44
src/gen/loops.c
Normal file
44
src/gen/loops.c
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#include "loops.h"
|
||||||
|
#include "dijkstra.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
static int is_passable(uint8_t t) {
|
||||||
|
return t == T_FLOOR || t == T_CORRIDOR || t == T_DOOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
int add_loops(grid_t g, event_sink_t *sink) {
|
||||||
|
dist_map_t dist;
|
||||||
|
int carved = 0;
|
||||||
|
|
||||||
|
/* Iterate in a scan order so output is deterministic. */
|
||||||
|
for (int y = 1; y < DROWS - 1; y++) {
|
||||||
|
for (int x = 1; x < DCOLS - 1; x++) {
|
||||||
|
if (g[y][x].terrain != T_NOTHING && g[y][x].terrain != T_WALL) continue;
|
||||||
|
|
||||||
|
/* Two pairings: horizontal (E-W neighbors both passable) and
|
||||||
|
vertical (N-S both passable). Require the OTHER axis to be
|
||||||
|
impassable so we don't carve through corners. */
|
||||||
|
int horiz = is_passable(g[y][x-1].terrain) && is_passable(g[y][x+1].terrain)
|
||||||
|
&& !is_passable(g[y-1][x].terrain) && !is_passable(g[y+1][x].terrain);
|
||||||
|
int vert = is_passable(g[y-1][x].terrain) && is_passable(g[y+1][x].terrain)
|
||||||
|
&& !is_passable(g[y][x-1].terrain) && !is_passable(g[y][x+1].terrain);
|
||||||
|
if (!horiz && !vert) continue;
|
||||||
|
|
||||||
|
int ax = horiz ? x - 1 : x;
|
||||||
|
int ay = horiz ? y : y - 1;
|
||||||
|
int bx = horiz ? x + 1 : x;
|
||||||
|
int by = horiz ? y : y + 1;
|
||||||
|
|
||||||
|
bfs_from(g, ax, ay, dist, -1, -1);
|
||||||
|
int d = dist[by][bx];
|
||||||
|
event_emit(sink, EV_LOOP_TESTED, NULL, 0);
|
||||||
|
if (d >= LOOP_THRESHOLD && d < DIST_UNREACHABLE) {
|
||||||
|
g[y][x].terrain = T_DOOR;
|
||||||
|
g[y][x].flags |= F_IN_LOOP;
|
||||||
|
carved++;
|
||||||
|
event_emit(sink, EV_LOOP_ACCEPTED, NULL, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return carved;
|
||||||
|
}
|
||||||
13
src/gen/loops.h
Normal file
13
src/gen/loops.h
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
#ifndef GENESIS_GEN_LOOPS_H
|
||||||
|
#define GENESIS_GEN_LOOPS_H
|
||||||
|
|
||||||
|
#include "grid.h"
|
||||||
|
#include "events.h"
|
||||||
|
|
||||||
|
#define LOOP_THRESHOLD 20
|
||||||
|
|
||||||
|
/* Punch extra doors where two passable regions are far apart (Dijkstra dist ≥
|
||||||
|
LOOP_THRESHOLD). Returns number of loops carved. */
|
||||||
|
int add_loops(grid_t g, event_sink_t *sink);
|
||||||
|
|
||||||
|
#endif
|
||||||
206
src/gen/machines.c
Normal file
206
src/gen/machines.c
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
#include "machines.h"
|
||||||
|
#include "blueprints.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static int is_pass(uint8_t t) {
|
||||||
|
return terrain_is_passable(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flood 4-connected from (sx,sy) through passable cells, but pretend
|
||||||
|
(block_x,block_y) is blocked. Fills pocket[][] with 1 for reached cells,
|
||||||
|
zero otherwise. Returns count. */
|
||||||
|
static int flood_pocket(grid_t g, int sx, int sy, int block_x, int block_y,
|
||||||
|
uint8_t pocket[DROWS][DCOLS]) {
|
||||||
|
memset(pocket, 0, (size_t)DROWS * DCOLS);
|
||||||
|
if (!grid_in_bounds(sx, sy)) return 0;
|
||||||
|
if (!is_pass(g[sy][sx].terrain)) return 0;
|
||||||
|
static int qx[DCOLS * DROWS];
|
||||||
|
static int qy[DCOLS * DROWS];
|
||||||
|
int head = 0, tail = 0;
|
||||||
|
pocket[sy][sx] = 1;
|
||||||
|
qx[tail] = sx; qy[tail] = sy; tail++;
|
||||||
|
int count = 1;
|
||||||
|
static const int d4[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
|
||||||
|
while (head < tail) {
|
||||||
|
int cx = qx[head], cy = qy[head]; head++;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
int nx = cx + d4[i][0];
|
||||||
|
int ny = cy + d4[i][1];
|
||||||
|
if (!grid_in_bounds(nx, ny)) continue;
|
||||||
|
if (pocket[ny][nx]) continue;
|
||||||
|
if (nx == block_x && ny == block_y) continue;
|
||||||
|
if (!is_pass(g[ny][nx].terrain)) continue;
|
||||||
|
pocket[ny][nx] = 1;
|
||||||
|
qx[tail] = nx; qy[tail] = ny; tail++;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pick a random blueprint that fits the given depth and pocket size,
|
||||||
|
weighted by frequency. Returns index into blueprint_catalog, or 0 if no
|
||||||
|
blueprint matches. */
|
||||||
|
static int pick_blueprint(rng_t *rng, int depth, int pocket_size) {
|
||||||
|
int total_weight = 0;
|
||||||
|
for (int i = 1; i < NUM_BLUEPRINTS; i++) {
|
||||||
|
const blueprint_t *bp = &blueprint_catalog[i];
|
||||||
|
if (depth < bp->depth_min || depth > bp->depth_max) continue;
|
||||||
|
if (pocket_size < bp->room_size_min || pocket_size > bp->room_size_max) continue;
|
||||||
|
total_weight += bp->frequency;
|
||||||
|
}
|
||||||
|
if (total_weight <= 0) return 0;
|
||||||
|
int roll = rng_range(rng, 0, total_weight - 1);
|
||||||
|
for (int i = 1; i < NUM_BLUEPRINTS; i++) {
|
||||||
|
const blueprint_t *bp = &blueprint_catalog[i];
|
||||||
|
if (depth < bp->depth_min || depth > bp->depth_max) continue;
|
||||||
|
if (pocket_size < bp->room_size_min || pocket_size > bp->room_size_max) continue;
|
||||||
|
roll -= bp->frequency;
|
||||||
|
if (roll < 0) return i;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collect interior cells into a flat list. Returns count. */
|
||||||
|
static int list_interior(uint8_t pocket[DROWS][DCOLS], int cells_x[], int cells_y[]) {
|
||||||
|
int n = 0;
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++) {
|
||||||
|
if (pocket[y][x]) {
|
||||||
|
cells_x[n] = x;
|
||||||
|
cells_y[n] = y;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply one feature. Returns the number of cells placed (counted against
|
||||||
|
min_count for acceptance). */
|
||||||
|
static int apply_feature(grid_t g, rng_t *rng, const feature_t *f,
|
||||||
|
int origin_x, int origin_y,
|
||||||
|
int cells_x[], int cells_y[], int cell_count,
|
||||||
|
uint8_t blocked[DROWS][DCOLS]) {
|
||||||
|
int placed = 0;
|
||||||
|
|
||||||
|
if (f->flags & MF_EVERYWHERE) {
|
||||||
|
for (int i = 0; i < cell_count; i++) {
|
||||||
|
int x = cells_x[i], y = cells_y[i];
|
||||||
|
if (f->terrain) g[y][x].terrain = (uint8_t)f->terrain;
|
||||||
|
if (f->marker) g[y][x].surface = (uint8_t)f->marker;
|
||||||
|
placed++;
|
||||||
|
}
|
||||||
|
return placed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f->flags & MF_BUILD_AT_ORIGIN) {
|
||||||
|
if (grid_in_bounds(origin_x, origin_y)) {
|
||||||
|
if (f->terrain) g[origin_y][origin_x].terrain = (uint8_t)f->terrain;
|
||||||
|
if (f->marker) g[origin_y][origin_x].surface = (uint8_t)f->marker;
|
||||||
|
g[origin_y][origin_x].flags |= F_MACHINE_GATE;
|
||||||
|
placed++;
|
||||||
|
}
|
||||||
|
return placed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Random interior placements, avoiding cells already marked blocking. */
|
||||||
|
int want = rng_range(rng, f->min_count, f->max_count);
|
||||||
|
int attempts = 0;
|
||||||
|
while (placed < want && attempts < cell_count * 4) {
|
||||||
|
attempts++;
|
||||||
|
int idx = rng_range(rng, 0, cell_count - 1);
|
||||||
|
int x = cells_x[idx], y = cells_y[idx];
|
||||||
|
if (blocked[y][x]) continue;
|
||||||
|
if (f->terrain) g[y][x].terrain = (uint8_t)f->terrain;
|
||||||
|
if (f->marker) g[y][x].surface = (uint8_t)f->marker;
|
||||||
|
if (f->flags & MF_TREAT_AS_BLOCKING) blocked[y][x] = 1;
|
||||||
|
if (f->flags & MF_GENERATE_KEY) g[y][x].flags |= F_MACHINE_KEY;
|
||||||
|
placed++;
|
||||||
|
}
|
||||||
|
return placed;
|
||||||
|
}
|
||||||
|
|
||||||
|
int place_machines(grid_t g, rng_t *rng, int depth, event_sink_t *sink,
|
||||||
|
choke_map_t choke, choke_flag_t is_gate) {
|
||||||
|
static uint8_t pocket[DROWS][DCOLS];
|
||||||
|
static uint8_t blocked[DROWS][DCOLS];
|
||||||
|
static int cells_x[DCOLS * DROWS];
|
||||||
|
static int cells_y[DCOLS * DROWS];
|
||||||
|
|
||||||
|
uint8_t next_machine_id = 1;
|
||||||
|
|
||||||
|
/* Scan gate sites in ascending pocket size. */
|
||||||
|
for (int target_size = 4; target_size <= DCOLS * DROWS; target_size++) {
|
||||||
|
for (int gy = 0; gy < DROWS; gy++) {
|
||||||
|
for (int gx = 0; gx < DCOLS; gx++) {
|
||||||
|
if (!is_gate[gy][gx]) continue;
|
||||||
|
if (choke[gy][gx] != target_size) continue;
|
||||||
|
|
||||||
|
/* Find the passable neighbor inside the pocket. */
|
||||||
|
static const int d4[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
|
||||||
|
int px = -1, py = -1;
|
||||||
|
for (int d = 0; d < 4; d++) {
|
||||||
|
int nx = gx + d4[d][0];
|
||||||
|
int ny = gy + d4[d][1];
|
||||||
|
if (!grid_in_bounds(nx, ny)) continue;
|
||||||
|
if (!is_pass(g[ny][nx].terrain)) continue;
|
||||||
|
/* The pocket side is the one where blocking the gate
|
||||||
|
would strand N cells. Try each neighbor and flood; the
|
||||||
|
smaller flood is the pocket. */
|
||||||
|
int n = flood_pocket(g, nx, ny, gx, gy, pocket);
|
||||||
|
if (n > 0 && n <= target_size + 2) {
|
||||||
|
px = nx; py = ny;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (px < 0) continue;
|
||||||
|
|
||||||
|
int pocket_count = flood_pocket(g, px, py, gx, gy, pocket);
|
||||||
|
if (pocket_count < 4) continue;
|
||||||
|
|
||||||
|
int bp_idx = pick_blueprint(rng, depth, pocket_count);
|
||||||
|
if (bp_idx == 0) continue;
|
||||||
|
const blueprint_t *bp = &blueprint_catalog[bp_idx];
|
||||||
|
|
||||||
|
event_emit(sink, EV_MACHINE_PROPOSED, NULL, 0);
|
||||||
|
|
||||||
|
int cell_count = list_interior(pocket, cells_x, cells_y);
|
||||||
|
memset(blocked, 0, sizeof(blocked));
|
||||||
|
|
||||||
|
/* Apply features; track acceptance. */
|
||||||
|
int all_required_met = 1;
|
||||||
|
for (int i = 0; i < bp->feature_count; i++) {
|
||||||
|
const feature_t *f = &bp->features[i];
|
||||||
|
int placed = apply_feature(g, rng, f, gx, gy,
|
||||||
|
cells_x, cells_y, cell_count,
|
||||||
|
blocked);
|
||||||
|
if (placed < f->min_count) {
|
||||||
|
all_required_met = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
event_emit(sink, EV_FEATURE_BUILT, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!all_required_met) continue;
|
||||||
|
|
||||||
|
/* Commit: tag every interior cell with F_IN_MACHINE + id. */
|
||||||
|
for (int i = 0; i < cell_count; i++) {
|
||||||
|
int x = cells_x[i], y = cells_y[i];
|
||||||
|
g[y][x].flags |= F_IN_MACHINE | F_MACHINE_INTERIOR;
|
||||||
|
g[y][x].machine_id = next_machine_id;
|
||||||
|
}
|
||||||
|
/* The gate itself is part of the machine boundary. */
|
||||||
|
g[gy][gx].flags |= F_IN_MACHINE | F_MACHINE_GATE;
|
||||||
|
g[gy][gx].machine_id = next_machine_id;
|
||||||
|
|
||||||
|
event_emit(sink, EV_MACHINE_ACCEPTED, NULL, 0);
|
||||||
|
next_machine_id++;
|
||||||
|
|
||||||
|
return 1; /* A.6c: one machine per seed */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
16
src/gen/machines.h
Normal file
16
src/gen/machines.h
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#ifndef GENESIS_GEN_MACHINES_H
|
||||||
|
#define GENESIS_GEN_MACHINES_H
|
||||||
|
|
||||||
|
#include "grid.h"
|
||||||
|
#include "rng.h"
|
||||||
|
#include "events.h"
|
||||||
|
#include "chokepoints.h"
|
||||||
|
|
||||||
|
/* Place up to one machine per call (A.6c milestone — cascade + multi-machine
|
||||||
|
come in A.6d/f). Requires a precomputed chokepoint map + gate-site flags.
|
||||||
|
Emits MACHINE_PROPOSED / FEATURE_BUILT / MACHINE_ACCEPTED events.
|
||||||
|
Returns the number of machines placed. */
|
||||||
|
int place_machines(grid_t g, rng_t *rng, int depth, event_sink_t *sink,
|
||||||
|
choke_map_t choke, choke_flag_t is_gate);
|
||||||
|
|
||||||
|
#endif
|
||||||
27
src/gen/rng.c
Normal file
27
src/gen/rng.c
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#include "rng.h"
|
||||||
|
|
||||||
|
void rng_seed(rng_t *r, uint64_t seed) {
|
||||||
|
r->state = 0u;
|
||||||
|
r->inc = (seed << 1u) | 1u;
|
||||||
|
rng_u32(r);
|
||||||
|
r->state += seed;
|
||||||
|
rng_u32(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t rng_u32(rng_t *r) {
|
||||||
|
uint64_t old = r->state;
|
||||||
|
r->state = old * 6364136223846793005ULL + r->inc;
|
||||||
|
uint32_t xorshifted = (uint32_t)(((old >> 18u) ^ old) >> 27u);
|
||||||
|
uint32_t rot = (uint32_t)(old >> 59u);
|
||||||
|
return (xorshifted >> rot) | (xorshifted << ((-rot) & 31));
|
||||||
|
}
|
||||||
|
|
||||||
|
int rng_range(rng_t *r, int lo, int hi) {
|
||||||
|
if (hi <= lo) return lo;
|
||||||
|
uint32_t span = (uint32_t)(hi - lo + 1);
|
||||||
|
return lo + (int)(rng_u32(r) % span);
|
||||||
|
}
|
||||||
|
|
||||||
|
int rng_percent(rng_t *r, int pct) {
|
||||||
|
return (int)(rng_u32(r) % 100) < pct;
|
||||||
|
}
|
||||||
16
src/gen/rng.h
Normal file
16
src/gen/rng.h
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#ifndef GENESIS_GEN_RNG_H
|
||||||
|
#define GENESIS_GEN_RNG_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint64_t state;
|
||||||
|
uint64_t inc;
|
||||||
|
} rng_t;
|
||||||
|
|
||||||
|
void rng_seed(rng_t *r, uint64_t seed);
|
||||||
|
uint32_t rng_u32(rng_t *r);
|
||||||
|
int rng_range(rng_t *r, int lo, int hi);
|
||||||
|
int rng_percent(rng_t *r, int pct);
|
||||||
|
|
||||||
|
#endif
|
||||||
143
src/gen/room_types.c
Normal file
143
src/gen/room_types.c
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
#include "room_types.h"
|
||||||
|
#include "ca.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static int carve_small_rect(grid_t g, rng_t *rng, int cx, int cy, uint8_t room_id) {
|
||||||
|
int w = rng_range(rng, 3, 6);
|
||||||
|
int h = rng_range(rng, 2, 4);
|
||||||
|
int x = cx - w / 2;
|
||||||
|
int y = cy - h / 2;
|
||||||
|
if (x < 1 || y < 1 || x + w >= DCOLS - 1 || y + h >= DROWS - 1) return 0;
|
||||||
|
grid_draw_rect(g, x, y, w, h, T_FLOOR, room_id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int carve_cross(grid_t g, rng_t *rng, int cx, int cy, uint8_t room_id) {
|
||||||
|
int majw = rng_range(rng, 4, 8);
|
||||||
|
int majh = rng_range(rng, 3, 4);
|
||||||
|
int minw = rng_range(rng, 3, 5);
|
||||||
|
int minh = rng_range(rng, 2, 3);
|
||||||
|
int ax = cx - majw / 2, ay = cy - majh / 2;
|
||||||
|
int bx = cx - minw / 2, by = cy - minh / 2;
|
||||||
|
if (ax < 1 || ay < 1 || ax + majw >= DCOLS - 1 || ay + majh >= DROWS - 1) return 0;
|
||||||
|
if (bx < 1 || by < 1 || bx + minw >= DCOLS - 1 || by + minh >= DROWS - 1) return 0;
|
||||||
|
grid_draw_rect(g, ax, ay, majw, majh, T_FLOOR, room_id);
|
||||||
|
grid_draw_rect(g, bx, by, minw, minh, T_FLOOR, room_id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int carve_sym_cross(grid_t g, rng_t *rng, int cx, int cy, uint8_t room_id) {
|
||||||
|
int majw = rng_range(rng, 4, 8) | 1;
|
||||||
|
int majh = 3;
|
||||||
|
int minw = 3;
|
||||||
|
int minh = rng_range(rng, 3, 5) | 1;
|
||||||
|
int ax = cx - majw / 2, ay = cy - majh / 2;
|
||||||
|
int bx = cx - minw / 2, by = cy - minh / 2;
|
||||||
|
if (ax < 1 || ay < 1 || ax + majw >= DCOLS - 1 || ay + majh >= DROWS - 1) return 0;
|
||||||
|
if (bx < 1 || by < 1 || bx + minw >= DCOLS - 1 || by + minh >= DROWS - 1) return 0;
|
||||||
|
grid_draw_rect(g, ax, ay, majw, majh, T_FLOOR, room_id);
|
||||||
|
grid_draw_rect(g, bx, by, minw, minh, T_FLOOR, room_id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int carve_circular(grid_t g, rng_t *rng, int cx, int cy, uint8_t room_id) {
|
||||||
|
int r = rng_range(rng, 2, 4);
|
||||||
|
if (cx - r < 1 || cy - r < 1 || cx + r >= DCOLS - 1 || cy + r >= DROWS - 1) return 0;
|
||||||
|
grid_draw_circle(g, cx, cy, r, T_FLOOR, room_id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int carve_chunky(grid_t g, rng_t *rng, int cx, int cy, uint8_t room_id) {
|
||||||
|
int bumps = rng_range(rng, 2, 4);
|
||||||
|
int placed = 0;
|
||||||
|
for (int i = 0; i < bumps; i++) {
|
||||||
|
int ox = rng_range(rng, -2, 2);
|
||||||
|
int oy = rng_range(rng, -2, 2);
|
||||||
|
int r = rng_range(rng, 1, 2);
|
||||||
|
int x = cx + ox, y = cy + oy;
|
||||||
|
if (x - r < 1 || y - r < 1 || x + r >= DCOLS - 1 || y + r >= DROWS - 1) continue;
|
||||||
|
grid_draw_circle(g, x, y, r, T_FLOOR, room_id);
|
||||||
|
placed++;
|
||||||
|
}
|
||||||
|
return placed > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Carve a CA blob into the scratch grid, then transcribe filled cells as
|
||||||
|
T_FLOOR with the given room_id. The blob is generated centered on (cx,cy)
|
||||||
|
with the given bounding box (clipped to grid). Returns 1 if any cells got
|
||||||
|
carved. ca_blob is called with NULL sink to avoid spamming CA_ITERATION
|
||||||
|
events from inside accretion. */
|
||||||
|
static int carve_ca_blob(grid_t g, rng_t *rng, int cx, int cy,
|
||||||
|
int bw, int bh, uint8_t room_id) {
|
||||||
|
int x0 = cx - bw / 2;
|
||||||
|
int y0 = cy - bh / 2;
|
||||||
|
if (x0 < 1) x0 = 1;
|
||||||
|
if (y0 < 1) y0 = 1;
|
||||||
|
if (x0 + bw >= DCOLS - 1) x0 = DCOLS - 1 - bw;
|
||||||
|
if (y0 + bh >= DROWS - 1) y0 = DROWS - 1 - bh;
|
||||||
|
if (x0 < 1 || y0 < 1) return 0;
|
||||||
|
|
||||||
|
static bitgrid_t blob;
|
||||||
|
ca_blob(blob, rng, NULL, x0, y0, bw, bh);
|
||||||
|
int count = bitgrid_count(blob);
|
||||||
|
if (count < 4) return 0;
|
||||||
|
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++) {
|
||||||
|
if (!blob[y][x]) continue;
|
||||||
|
g[y][x].terrain = T_FLOOR;
|
||||||
|
g[y][x].room_id = room_id;
|
||||||
|
g[y][x].flags |= F_IN_ROOM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int carve_cave(grid_t g, rng_t *rng, int cx, int cy, uint8_t room_id) {
|
||||||
|
int bw = rng_range(rng, 6, 9);
|
||||||
|
int bh = rng_range(rng, 4, 6);
|
||||||
|
return carve_ca_blob(g, rng, cx, cy, bw, bh, room_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int carve_cavern(grid_t g, rng_t *rng, int cx, int cy, uint8_t room_id) {
|
||||||
|
int bw = rng_range(rng, 18, 26);
|
||||||
|
int bh = rng_range(rng, 10, 14);
|
||||||
|
return carve_ca_blob(g, rng, cx, cy, bw, bh, room_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int carve_entrance(grid_t g, rng_t *rng, int cx, int cy, uint8_t room_id) {
|
||||||
|
(void)rng;
|
||||||
|
int w = 5, h = 4;
|
||||||
|
int x = cx - w / 2, y = cy - h / 2;
|
||||||
|
if (x < 1 || y < 1 || x + w >= DCOLS - 1 || y + h >= DROWS - 1) return 0;
|
||||||
|
grid_draw_rect(g, x, y, w, h, T_FLOOR, room_id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int room_carve(grid_t g, room_type_t type, rng_t *rng, int cx, int cy, uint8_t room_id) {
|
||||||
|
switch (type) {
|
||||||
|
case ROOM_SMALL_RECT: return carve_small_rect(g, rng, cx, cy, room_id);
|
||||||
|
case ROOM_CROSS: return carve_cross(g, rng, cx, cy, room_id);
|
||||||
|
case ROOM_SYM_CROSS: return carve_sym_cross(g, rng, cx, cy, room_id);
|
||||||
|
case ROOM_CIRCULAR: return carve_circular(g, rng, cx, cy, room_id);
|
||||||
|
case ROOM_CHUNKY: return carve_chunky(g, rng, cx, cy, room_id);
|
||||||
|
case ROOM_CAVE: return carve_cave(g, rng, cx, cy, room_id);
|
||||||
|
case ROOM_CAVERN: return carve_cavern(g, rng, cx, cy, room_id);
|
||||||
|
case ROOM_ENTRANCE: return carve_entrance(g, rng, cx, cy, room_id);
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *room_type_name(room_type_t t) {
|
||||||
|
switch (t) {
|
||||||
|
case ROOM_SMALL_RECT: return "small_rect";
|
||||||
|
case ROOM_CROSS: return "cross";
|
||||||
|
case ROOM_SYM_CROSS: return "sym_cross";
|
||||||
|
case ROOM_CIRCULAR: return "circular";
|
||||||
|
case ROOM_CHUNKY: return "chunky";
|
||||||
|
case ROOM_CAVE: return "cave";
|
||||||
|
case ROOM_CAVERN: return "cavern";
|
||||||
|
case ROOM_ENTRANCE: return "entrance";
|
||||||
|
default: return "?";
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/gen/room_types.h
Normal file
25
src/gen/room_types.h
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#ifndef GENESIS_GEN_ROOM_TYPES_H
|
||||||
|
#define GENESIS_GEN_ROOM_TYPES_H
|
||||||
|
|
||||||
|
#include "grid.h"
|
||||||
|
#include "rng.h"
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ROOM_SMALL_RECT = 0,
|
||||||
|
ROOM_CROSS = 1,
|
||||||
|
ROOM_SYM_CROSS = 2,
|
||||||
|
ROOM_CIRCULAR = 3,
|
||||||
|
ROOM_CHUNKY = 4,
|
||||||
|
ROOM_CAVE = 5,
|
||||||
|
ROOM_CAVERN = 6,
|
||||||
|
ROOM_ENTRANCE = 7,
|
||||||
|
ROOM_COUNT
|
||||||
|
} room_type_t;
|
||||||
|
|
||||||
|
/* Carve a room into the hyperspace scratch grid g, filling floor terrain with
|
||||||
|
room_id. Returns 1 on success, 0 if the type couldn't fit at (cx, cy). */
|
||||||
|
int room_carve(grid_t g, room_type_t type, rng_t *rng, int cx, int cy, uint8_t room_id);
|
||||||
|
|
||||||
|
const char *room_type_name(room_type_t t);
|
||||||
|
|
||||||
|
#endif
|
||||||
47
src/gen/stairs.c
Normal file
47
src/gen/stairs.c
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
#include "stairs.h"
|
||||||
|
#include "dijkstra.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
static int is_stair_candidate(uint8_t t) {
|
||||||
|
return t == T_FLOOR; /* avoid corridors and doors so stairs sit in rooms */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Find the floor cell nearest to (cx,cy) in Chebyshev distance. */
|
||||||
|
static int nearest_floor(grid_t g, int cx, int cy, int *ox, int *oy) {
|
||||||
|
int best = -1, bx = -1, by = -1;
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++) {
|
||||||
|
if (!is_stair_candidate(g[y][x].terrain)) continue;
|
||||||
|
int dx = x - cx, dy = y - cy;
|
||||||
|
int d = (dx < 0 ? -dx : dx) + (dy < 0 ? -dy : dy);
|
||||||
|
if (best < 0 || d < best) { best = d; bx = x; by = y; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best < 0) return 0;
|
||||||
|
*ox = bx; *oy = by;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void place_stairs(grid_t g, event_sink_t *sink) {
|
||||||
|
int ux, uy;
|
||||||
|
if (!nearest_floor(g, DCOLS / 2, DROWS / 2, &ux, &uy)) return;
|
||||||
|
g[uy][ux].terrain = T_STAIRS_UP;
|
||||||
|
event_emit(sink, EV_STAIRS_PLACED_UP, NULL, 0);
|
||||||
|
|
||||||
|
/* BFS from up-stair; pick farthest passable cell that is still a room
|
||||||
|
floor candidate. */
|
||||||
|
static dist_map_t dist;
|
||||||
|
bfs_from(g, ux, uy, dist, -1, -1);
|
||||||
|
|
||||||
|
int best_d = -1, dx_ = -1, dy_ = -1;
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++) {
|
||||||
|
if (!is_stair_candidate(g[y][x].terrain)) continue;
|
||||||
|
if (dist[y][x] == DIST_UNREACHABLE) continue;
|
||||||
|
if (dist[y][x] > best_d) { best_d = dist[y][x]; dx_ = x; dy_ = y; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best_d < 0) return; /* degenerate: no other floor — leave down-stair unplaced */
|
||||||
|
g[dy_][dx_].terrain = T_STAIRS_DOWN;
|
||||||
|
event_emit(sink, EV_STAIRS_PLACED_DOWN, NULL, 0);
|
||||||
|
}
|
||||||
12
src/gen/stairs.h
Normal file
12
src/gen/stairs.h
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#ifndef GENESIS_GEN_STAIRS_H
|
||||||
|
#define GENESIS_GEN_STAIRS_H
|
||||||
|
|
||||||
|
#include "grid.h"
|
||||||
|
#include "events.h"
|
||||||
|
|
||||||
|
/* Place an up-stair near the map center (first room) and a down-stair at the
|
||||||
|
floor cell with the greatest graph distance from the up-stair. Called after
|
||||||
|
wall finishing. Emits EV_STAIRS_PLACED_UP then EV_STAIRS_PLACED_DOWN. */
|
||||||
|
void place_stairs(grid_t g, event_sink_t *sink);
|
||||||
|
|
||||||
|
#endif
|
||||||
24
src/gen/walls.c
Normal file
24
src/gen/walls.c
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#include "walls.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
void finish_walls(grid_t g, event_sink_t *sink) {
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++) {
|
||||||
|
if (g[y][x].terrain != T_NOTHING) continue;
|
||||||
|
int adj_open = 0;
|
||||||
|
for (int ny = -1; ny <= 1 && !adj_open; ny++) {
|
||||||
|
for (int nx = -1; nx <= 1 && !adj_open; nx++) {
|
||||||
|
if (nx == 0 && ny == 0) continue;
|
||||||
|
int qx = x + nx, qy = y + ny;
|
||||||
|
if (!grid_in_bounds(qx, qy)) continue;
|
||||||
|
uint8_t tt = g[qy][qx].terrain;
|
||||||
|
if (tt == T_FLOOR || tt == T_CORRIDOR || tt == T_DOOR) {
|
||||||
|
adj_open = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (adj_open) g[y][x].terrain = T_WALL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event_emit(sink, EV_WALLS_FINISHED, NULL, 0);
|
||||||
|
}
|
||||||
10
src/gen/walls.h
Normal file
10
src/gen/walls.h
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#ifndef GENESIS_GEN_WALLS_H
|
||||||
|
#define GENESIS_GEN_WALLS_H
|
||||||
|
|
||||||
|
#include "grid.h"
|
||||||
|
#include "events.h"
|
||||||
|
|
||||||
|
/* Convert every NOTHING cell adjacent (8-neighbor) to open terrain into WALL. */
|
||||||
|
void finish_walls(grid_t g, event_sink_t *sink);
|
||||||
|
|
||||||
|
#endif
|
||||||
214
src/genesis_main.c
Normal file
214
src/genesis_main.c
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "gen/grid.h"
|
||||||
|
#include "gen/rng.h"
|
||||||
|
#include "gen/room_types.h"
|
||||||
|
#include "gen/events.h"
|
||||||
|
#include "gen/accretion.h"
|
||||||
|
#include "gen/loops.h"
|
||||||
|
#include "gen/lakes.h"
|
||||||
|
#include "gen/walls.h"
|
||||||
|
#include "gen/stairs.h"
|
||||||
|
#include "gen/chokepoints.h"
|
||||||
|
#include "gen/machines.h"
|
||||||
|
#include "gen/dijkstra.h"
|
||||||
|
#include "json_emit.h"
|
||||||
|
|
||||||
|
static int g_verbose = 0;
|
||||||
|
static int g_quiet = 0;
|
||||||
|
static int g_mark_unreached = 0;
|
||||||
|
static int g_emit_json = 0;
|
||||||
|
|
||||||
|
static void print_ascii(grid_t g) {
|
||||||
|
dist_map_t dist;
|
||||||
|
if (g_mark_unreached) {
|
||||||
|
int sx = -1, sy = -1;
|
||||||
|
for (int y = 0; y < DROWS && sx < 0; y++)
|
||||||
|
for (int x = 0; x < DCOLS && sx < 0; x++) {
|
||||||
|
uint8_t t = g[y][x].terrain;
|
||||||
|
if (t == T_FLOOR || t == T_CORRIDOR || t == T_DOOR || t == T_BRIDGE) {
|
||||||
|
sx = x; sy = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sx >= 0) bfs_from(g, sx, sy, dist, -1, -1);
|
||||||
|
}
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++) {
|
||||||
|
char ch;
|
||||||
|
uint8_t t = g[y][x].terrain;
|
||||||
|
int passable = (t == T_FLOOR || t == T_CORRIDOR || t == T_DOOR || t == T_BRIDGE);
|
||||||
|
if (g_mark_unreached && passable && dist[y][x] == DIST_UNREACHABLE) {
|
||||||
|
ch = '!';
|
||||||
|
} else switch (t) {
|
||||||
|
case T_FLOOR: ch = (g[y][x].flags & F_WREATH) ? ';' : '.'; break;
|
||||||
|
case T_CORRIDOR: ch = ','; break;
|
||||||
|
case T_DOOR: ch = '+'; break;
|
||||||
|
case T_WALL: ch = (g[y][x].flags & F_WREATH) ? ':' : '#'; break;
|
||||||
|
case T_LIQUID:
|
||||||
|
switch (g[y][x].liquid) {
|
||||||
|
case L_LAVA: ch = '^'; break;
|
||||||
|
case L_CHASM: ch = 'v'; break;
|
||||||
|
case L_BRIMSTONE: ch = '%'; break;
|
||||||
|
case L_WATER:
|
||||||
|
default: ch = '~'; break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case T_BRIDGE: ch = '='; break;
|
||||||
|
case T_STAIRS_UP: ch = '<'; break;
|
||||||
|
case T_STAIRS_DOWN: ch = '>'; break;
|
||||||
|
default: ch = ' '; break;
|
||||||
|
}
|
||||||
|
putchar(ch);
|
||||||
|
}
|
||||||
|
putchar('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stderr_sink(void *ctx, const gen_event_t *ev) {
|
||||||
|
(void)ctx;
|
||||||
|
if (!g_verbose) return;
|
||||||
|
fprintf(stderr, "[ev %4u] %s\n",
|
||||||
|
ev->step, event_kind_name((event_kind_t)ev->kind));
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int counts[EV_KIND_COUNT];
|
||||||
|
} sink_ctx_t;
|
||||||
|
|
||||||
|
static void counting_sink(void *ctx, const gen_event_t *ev) {
|
||||||
|
sink_ctx_t *s = (sink_ctx_t *)ctx;
|
||||||
|
s->counts[ev->kind]++;
|
||||||
|
if (g_verbose) stderr_sink(NULL, ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int rooms, loops, lakes_proposed, lakes_rejected, lakes_placed, bridges;
|
||||||
|
int machines_proposed, machines_placed;
|
||||||
|
int floor_cells;
|
||||||
|
int connected;
|
||||||
|
int unreached;
|
||||||
|
} run_stats_t;
|
||||||
|
|
||||||
|
static void run_once(uint64_t seed, int depth, grid_t *gp, run_stats_t *out, int verify) {
|
||||||
|
cell_t (*g)[DCOLS] = *gp;
|
||||||
|
rng_t rng;
|
||||||
|
rng_seed(&rng, seed);
|
||||||
|
grid_fill(g, T_NOTHING);
|
||||||
|
|
||||||
|
sink_ctx_t sctx = {0};
|
||||||
|
event_sink_t sink = { .fn = counting_sink, .ctx = &sctx, .step = 0 };
|
||||||
|
|
||||||
|
event_emit(&sink, EV_GEN_BEGIN, NULL, 0);
|
||||||
|
|
||||||
|
accrete_rooms(g, &rng, &sink);
|
||||||
|
if (verify && !is_connected(g, NULL))
|
||||||
|
fprintf(stderr, "!! seed %llu: disconnected after accretion\n", (unsigned long long)seed);
|
||||||
|
add_loops(g, &sink);
|
||||||
|
if (verify && !is_connected(g, NULL))
|
||||||
|
fprintf(stderr, "!! seed %llu: disconnected after loops\n", (unsigned long long)seed);
|
||||||
|
place_lakes(g, &rng, &sink, depth);
|
||||||
|
if (verify && !is_connected(g, NULL))
|
||||||
|
fprintf(stderr, "!! seed %llu: disconnected after lakes\n", (unsigned long long)seed);
|
||||||
|
apply_wreaths(g, &sink);
|
||||||
|
place_bridges(g, &sink);
|
||||||
|
finish_walls(g, &sink);
|
||||||
|
place_stairs(g, &sink);
|
||||||
|
|
||||||
|
/* Chokepoint analysis + machine placement. */
|
||||||
|
static choke_map_t choke;
|
||||||
|
static choke_flag_t is_choke, is_gate;
|
||||||
|
compute_chokepoints(g, &sink, choke, is_choke, is_gate);
|
||||||
|
place_machines(g, &rng, depth, &sink, choke, is_gate);
|
||||||
|
|
||||||
|
event_emit(&sink, EV_GEN_END, NULL, 0);
|
||||||
|
|
||||||
|
out->rooms = sctx.counts[EV_ROOM_PLACED];
|
||||||
|
out->loops = sctx.counts[EV_LOOP_ACCEPTED];
|
||||||
|
out->lakes_proposed = sctx.counts[EV_LAKE_PROPOSED];
|
||||||
|
out->lakes_rejected = sctx.counts[EV_LAKE_REJECTED_IMPASSABLE];
|
||||||
|
out->lakes_placed = sctx.counts[EV_LAKE_PLACED];
|
||||||
|
out->bridges = sctx.counts[EV_BRIDGE_PLACED];
|
||||||
|
out->machines_proposed = sctx.counts[EV_MACHINE_PROPOSED];
|
||||||
|
out->machines_placed = sctx.counts[EV_MACHINE_ACCEPTED];
|
||||||
|
out->floor_cells = grid_count_floor(g);
|
||||||
|
out->connected = is_connected(g, &out->unreached);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_summary(uint64_t seed, int depth, const run_stats_t *s) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"seed=%llu depth=%d rooms=%d loops=%d "
|
||||||
|
"lakes_proposed=%d lakes_rejected=%d lakes_placed=%d "
|
||||||
|
"bridges=%d machines=%d/%d floor=%d connected=%d unreached=%d\n",
|
||||||
|
(unsigned long long)seed, depth,
|
||||||
|
s->rooms, s->loops,
|
||||||
|
s->lakes_proposed, s->lakes_rejected, s->lakes_placed,
|
||||||
|
s->bridges, s->machines_placed, s->machines_proposed,
|
||||||
|
s->floor_cells, s->connected, s->unreached);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmd_stress(int n, uint64_t base_seed, int depth) {
|
||||||
|
grid_t g;
|
||||||
|
int failures = 0;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
uint64_t seed = base_seed + (uint64_t)i;
|
||||||
|
run_stats_t s = {0};
|
||||||
|
run_once(seed, depth, &g, &s, 0);
|
||||||
|
if (!s.connected) {
|
||||||
|
fprintf(stderr, "FAIL seed=%llu unreached=%d\n",
|
||||||
|
(unsigned long long)seed, s.unreached);
|
||||||
|
failures++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fprintf(stderr, "stress: %d seeds, %d failures\n", n, failures);
|
||||||
|
return failures == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
uint64_t seed = 1234;
|
||||||
|
int depth = 1;
|
||||||
|
int verify = 0;
|
||||||
|
int stress_n = 0;
|
||||||
|
uint64_t stress_base = 1;
|
||||||
|
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
if (!strcmp(argv[i], "--seed") && i + 1 < argc) {
|
||||||
|
seed = strtoull(argv[++i], NULL, 10);
|
||||||
|
} else if (!strcmp(argv[i], "--depth") && i + 1 < argc) {
|
||||||
|
depth = atoi(argv[++i]);
|
||||||
|
if (depth < 1) depth = 1;
|
||||||
|
if (depth > 26) depth = 26;
|
||||||
|
} else if (!strcmp(argv[i], "--verbose")) {
|
||||||
|
g_verbose = 1;
|
||||||
|
} else if (!strcmp(argv[i], "--quiet")) {
|
||||||
|
g_quiet = 1;
|
||||||
|
} else if (!strcmp(argv[i], "--verify")) {
|
||||||
|
verify = 1;
|
||||||
|
} else if (!strcmp(argv[i], "--stress") && i + 1 < argc) {
|
||||||
|
stress_n = atoi(argv[++i]);
|
||||||
|
} else if (!strcmp(argv[i], "--stress-base") && i + 1 < argc) {
|
||||||
|
stress_base = strtoull(argv[++i], NULL, 10);
|
||||||
|
} else if (!strcmp(argv[i], "--mark-unreached")) {
|
||||||
|
g_mark_unreached = 1;
|
||||||
|
} else if (!strcmp(argv[i], "--emit=json") || !strcmp(argv[i], "--json")) {
|
||||||
|
g_emit_json = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stress_n > 0) return cmd_stress(stress_n, stress_base, depth);
|
||||||
|
|
||||||
|
grid_t g;
|
||||||
|
run_stats_t s = {0};
|
||||||
|
run_once(seed, depth, &g, &s, verify);
|
||||||
|
if (g_emit_json) {
|
||||||
|
emit_grid_json(g, seed, depth);
|
||||||
|
if (verify && !s.connected) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
print_summary(seed, depth, &s);
|
||||||
|
if (!g_quiet) print_ascii(g);
|
||||||
|
|
||||||
|
if (verify && !s.connected) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
173
src/json_emit.c
Normal file
173
src/json_emit.c
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
#include "json_emit.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static const char B64[] =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
|
||||||
|
static void b64_write(const uint8_t *src, size_t n) {
|
||||||
|
size_t i = 0;
|
||||||
|
while (i + 3 <= n) {
|
||||||
|
uint32_t v = ((uint32_t)src[i] << 16) | ((uint32_t)src[i+1] << 8) | src[i+2];
|
||||||
|
putchar(B64[(v >> 18) & 0x3F]);
|
||||||
|
putchar(B64[(v >> 12) & 0x3F]);
|
||||||
|
putchar(B64[(v >> 6) & 0x3F]);
|
||||||
|
putchar(B64[ v & 0x3F]);
|
||||||
|
i += 3;
|
||||||
|
}
|
||||||
|
size_t rem = n - i;
|
||||||
|
if (rem == 1) {
|
||||||
|
uint32_t v = (uint32_t)src[i] << 16;
|
||||||
|
putchar(B64[(v >> 18) & 0x3F]);
|
||||||
|
putchar(B64[(v >> 12) & 0x3F]);
|
||||||
|
putchar('=');
|
||||||
|
putchar('=');
|
||||||
|
} else if (rem == 2) {
|
||||||
|
uint32_t v = ((uint32_t)src[i] << 16) | ((uint32_t)src[i+1] << 8);
|
||||||
|
putchar(B64[(v >> 18) & 0x3F]);
|
||||||
|
putchar(B64[(v >> 12) & 0x3F]);
|
||||||
|
putchar(B64[(v >> 6) & 0x3F]);
|
||||||
|
putchar('=');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_b64_field(const char *name, const uint8_t *bytes, size_t n) {
|
||||||
|
printf("\"%s\":\"", name);
|
||||||
|
b64_write(bytes, n);
|
||||||
|
putchar('"');
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_xy(const char *name, int x, int y) {
|
||||||
|
printf("\"%s\":[%d,%d]", name, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bucket helpers. Room IDs and machine IDs are 1..255. */
|
||||||
|
typedef struct {
|
||||||
|
int count;
|
||||||
|
int cap;
|
||||||
|
int *xs;
|
||||||
|
int *ys;
|
||||||
|
} cellbuf_t;
|
||||||
|
|
||||||
|
static void cellbuf_push(cellbuf_t *b, int x, int y) {
|
||||||
|
if (b->count == b->cap) {
|
||||||
|
b->cap = b->cap ? b->cap * 2 : 16;
|
||||||
|
b->xs = (int *)realloc(b->xs, b->cap * sizeof(int));
|
||||||
|
b->ys = (int *)realloc(b->ys, b->cap * sizeof(int));
|
||||||
|
}
|
||||||
|
b->xs[b->count] = x;
|
||||||
|
b->ys[b->count] = y;
|
||||||
|
b->count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cellbuf_free(cellbuf_t *b) {
|
||||||
|
free(b->xs);
|
||||||
|
free(b->ys);
|
||||||
|
b->xs = NULL; b->ys = NULL; b->count = 0; b->cap = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_cellbuf(const cellbuf_t *b) {
|
||||||
|
putchar('[');
|
||||||
|
for (int i = 0; i < b->count; i++) {
|
||||||
|
if (i) putchar(',');
|
||||||
|
printf("[%d,%d]", b->xs[i], b->ys[i]);
|
||||||
|
}
|
||||||
|
putchar(']');
|
||||||
|
}
|
||||||
|
|
||||||
|
void emit_grid_json(grid_t g, uint64_t seed, int depth) {
|
||||||
|
const int N = DCOLS * DROWS;
|
||||||
|
|
||||||
|
uint8_t *terrain = (uint8_t *)malloc(N);
|
||||||
|
uint8_t *liquid = (uint8_t *)malloc(N);
|
||||||
|
uint8_t *surface = (uint8_t *)malloc(N);
|
||||||
|
uint8_t *room_id = (uint8_t *)malloc(N);
|
||||||
|
uint8_t *machine_id = (uint8_t *)malloc(N);
|
||||||
|
uint8_t *flags_le = (uint8_t *)malloc(N * 4);
|
||||||
|
|
||||||
|
int stairs_up_x = -1, stairs_up_y = -1;
|
||||||
|
int stairs_down_x = -1, stairs_down_y = -1;
|
||||||
|
|
||||||
|
cellbuf_t rooms[256] = {0};
|
||||||
|
cellbuf_t mach_interior[256] = {0};
|
||||||
|
int mach_gate_x[256], mach_gate_y[256];
|
||||||
|
for (int i = 0; i < 256; i++) { mach_gate_x[i] = -1; mach_gate_y[i] = -1; }
|
||||||
|
|
||||||
|
int idx = 0;
|
||||||
|
for (int y = 0; y < DROWS; y++) {
|
||||||
|
for (int x = 0; x < DCOLS; x++, idx++) {
|
||||||
|
const cell_t *c = &g[y][x];
|
||||||
|
terrain[idx] = c->terrain;
|
||||||
|
liquid[idx] = c->liquid;
|
||||||
|
surface[idx] = c->surface;
|
||||||
|
room_id[idx] = c->room_id;
|
||||||
|
machine_id[idx] = c->machine_id;
|
||||||
|
/* Little-endian int32 of the uint16 flags (zero-extended). */
|
||||||
|
uint32_t f = c->flags;
|
||||||
|
flags_le[idx*4 + 0] = (uint8_t)( f & 0xFF);
|
||||||
|
flags_le[idx*4 + 1] = (uint8_t)((f >> 8) & 0xFF);
|
||||||
|
flags_le[idx*4 + 2] = (uint8_t)((f >> 16) & 0xFF);
|
||||||
|
flags_le[idx*4 + 3] = (uint8_t)((f >> 24) & 0xFF);
|
||||||
|
|
||||||
|
if (c->terrain == T_STAIRS_UP) { stairs_up_x = x; stairs_up_y = y; }
|
||||||
|
if (c->terrain == T_STAIRS_DOWN) { stairs_down_x = x; stairs_down_y = y; }
|
||||||
|
|
||||||
|
if (c->room_id > 0) {
|
||||||
|
cellbuf_push(&rooms[c->room_id], x, y);
|
||||||
|
}
|
||||||
|
if (c->machine_id > 0) {
|
||||||
|
cellbuf_push(&mach_interior[c->machine_id], x, y);
|
||||||
|
if (c->flags & F_MACHINE_GATE) {
|
||||||
|
mach_gate_x[c->machine_id] = x;
|
||||||
|
mach_gate_y[c->machine_id] = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
putchar('{');
|
||||||
|
printf("\"seed\":%llu,\"depth\":%d,\"width\":%d,\"height\":%d,",
|
||||||
|
(unsigned long long)seed, depth, DCOLS, DROWS);
|
||||||
|
|
||||||
|
write_b64_field("terrain", terrain, N); putchar(',');
|
||||||
|
write_b64_field("liquid", liquid, N); putchar(',');
|
||||||
|
write_b64_field("surface", surface, N); putchar(',');
|
||||||
|
write_b64_field("room_id", room_id, N); putchar(',');
|
||||||
|
write_b64_field("machine_id", machine_id, N); putchar(',');
|
||||||
|
write_b64_field("flags", flags_le, N * 4); putchar(',');
|
||||||
|
|
||||||
|
write_xy("stairs_up", stairs_up_x, stairs_up_y); putchar(',');
|
||||||
|
write_xy("stairs_down", stairs_down_x, stairs_down_y); putchar(',');
|
||||||
|
|
||||||
|
fputs("\"rooms\":[", stdout);
|
||||||
|
int first = 1;
|
||||||
|
for (int i = 1; i < 256; i++) {
|
||||||
|
if (rooms[i].count == 0) continue;
|
||||||
|
if (!first) putchar(',');
|
||||||
|
first = 0;
|
||||||
|
printf("{\"id\":%d,\"cells\":", i);
|
||||||
|
write_cellbuf(&rooms[i]);
|
||||||
|
putchar('}');
|
||||||
|
}
|
||||||
|
fputs("],", stdout);
|
||||||
|
|
||||||
|
fputs("\"machines\":[", stdout);
|
||||||
|
first = 1;
|
||||||
|
for (int i = 1; i < 256; i++) {
|
||||||
|
if (mach_interior[i].count == 0) continue;
|
||||||
|
if (!first) putchar(',');
|
||||||
|
first = 0;
|
||||||
|
printf("{\"id\":%d,\"interior_cells\":", i);
|
||||||
|
write_cellbuf(&mach_interior[i]);
|
||||||
|
printf(",\"gate\":[%d,%d]}", mach_gate_x[i], mach_gate_y[i]);
|
||||||
|
}
|
||||||
|
fputs("]", stdout);
|
||||||
|
|
||||||
|
fputs("}\n", stdout);
|
||||||
|
|
||||||
|
free(terrain); free(liquid); free(surface);
|
||||||
|
free(room_id); free(machine_id); free(flags_le);
|
||||||
|
for (int i = 0; i < 256; i++) { cellbuf_free(&rooms[i]); cellbuf_free(&mach_interior[i]); }
|
||||||
|
}
|
||||||
15
src/json_emit.h
Normal file
15
src/json_emit.h
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#ifndef JSON_EMIT_H
|
||||||
|
#define JSON_EMIT_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "gen/grid.h"
|
||||||
|
|
||||||
|
/* Emit the grid as a single JSON object to stdout. Layout mirrors the
|
||||||
|
* Dictionary produced by godot/src/grid_to_dict.cpp so the CLI and the
|
||||||
|
* GDExtension expose the same schema. Byte layers are base64-encoded;
|
||||||
|
* the flags layer is emitted as base64 of width*height little-endian
|
||||||
|
* int32 values.
|
||||||
|
*/
|
||||||
|
void emit_grid_json(grid_t g, uint64_t seed, int depth);
|
||||||
|
|
||||||
|
#endif
|
||||||
33
test/README
Normal file
33
test/README
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
Tests for the brogue-genesis generator.
|
||||||
|
|
||||||
|
Running
|
||||||
|
-------
|
||||||
|
make test # goldens + 1000-seed invariant stress
|
||||||
|
make stress # 5000-seed invariant stress only
|
||||||
|
|
||||||
|
Files
|
||||||
|
-----
|
||||||
|
run_tests.sh Runs goldens against current binary, then stress.
|
||||||
|
update_goldens.sh Regenerates goldens.txt. Run this only after an
|
||||||
|
intentional change to generation logic.
|
||||||
|
goldens.txt seed|map_sha256|summary_line, one per committed seed.
|
||||||
|
|
||||||
|
What's checked
|
||||||
|
--------------
|
||||||
|
1. Map hash stability — sha256 of the ASCII map for each committed seed.
|
||||||
|
2. Summary line stability — room/loop/lake/bridge counts and connectivity.
|
||||||
|
3. Connectivity invariant — for a rolling sweep of seeds, every passable
|
||||||
|
cell must be 4-connected to every other. This is the load-bearing
|
||||||
|
property of Brogue's pipeline.
|
||||||
|
|
||||||
|
When to update the goldens
|
||||||
|
--------------------------
|
||||||
|
If you intentionally change generation (new room type, tweak CA rule, change
|
||||||
|
lake sizes, adjust corridor bounds, reorder phases, etc.), the goldens will
|
||||||
|
break by design. Inspect the diff, confirm it looks right, then:
|
||||||
|
|
||||||
|
make && test/update_goldens.sh
|
||||||
|
make test
|
||||||
|
|
||||||
|
If the stress pass ever reports non-zero failures, a connectivity-preserving
|
||||||
|
invariant has been broken — investigate before committing.
|
||||||
20
test/goldens.txt
Normal file
20
test/goldens.txt
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# seed_depth|map_sha256|summary_line
|
||||||
|
# Generated 2026-04-13T16:05:27Z
|
||||||
|
1:1|7c7a6b6897f950ffe50cc4617a64ef6dc235c821e8bd0fd2e90be0a91773c0a5|seed=1 depth=1 rooms=31 loops=4 lakes_proposed=6 lakes_rejected=5 lakes_placed=1 bridges=1 machines=1/1 floor=642 connected=1 unreached=0
|
||||||
|
2:1|7f41699a774c4c23acde501d157f1f5d4561989637b6fd0419688668a162f6ea|seed=2 depth=1 rooms=26 loops=6 lakes_proposed=6 lakes_rejected=5 lakes_placed=1 bridges=2 machines=1/1 floor=534 connected=1 unreached=0
|
||||||
|
42:1|2d89ffde34b0aa2ce4ffe11dc4feac26b045aef160e8bfa52c85d398e0464ae7|seed=42 depth=1 rooms=27 loops=2 lakes_proposed=6 lakes_rejected=5 lakes_placed=1 bridges=1 machines=1/1 floor=663 connected=1 unreached=0
|
||||||
|
77:1|af72b9a505777abd87b6d2693b6ee2710f12708ca223b08b0a8a385e110b414a|seed=77 depth=1 rooms=30 loops=4 lakes_proposed=6 lakes_rejected=5 lakes_placed=1 bridges=0 machines=1/1 floor=574 connected=1 unreached=0
|
||||||
|
1234:1|c62f7b9a68ccaaae6f10621de685b3066d87b098eed9ef9fade351658ca638d9|seed=1234 depth=1 rooms=31 loops=3 lakes_proposed=6 lakes_rejected=4 lakes_placed=2 bridges=0 machines=1/1 floor=646 connected=1 unreached=0
|
||||||
|
2026:1|bd55089a42166b92daf0814b20aff9b6e0b17d49ed7d53d4f25085bd579e44ea|seed=2026 depth=1 rooms=31 loops=5 lakes_proposed=6 lakes_rejected=6 lakes_placed=0 bridges=0 machines=1/1 floor=664 connected=1 unreached=0
|
||||||
|
9999:1|e3042b10055bc0ed45667b6804ae05e7a7695a76018d555552c472d8a4d8e159|seed=9999 depth=1 rooms=30 loops=4 lakes_proposed=6 lakes_rejected=4 lakes_placed=2 bridges=1 machines=1/1 floor=652 connected=1 unreached=0
|
||||||
|
123456:1|cabc305545fbc230c3e4667681bf725d854da3e465f281067077dfc5ddfed435|seed=123456 depth=1 rooms=31 loops=6 lakes_proposed=6 lakes_rejected=5 lakes_placed=1 bridges=8 machines=1/1 floor=680 connected=1 unreached=0
|
||||||
|
31337:1|f747922a14158050cd871553393d0247becf5f64b4c21a303d36d55e328330c3|seed=31337 depth=1 rooms=29 loops=1 lakes_proposed=6 lakes_rejected=5 lakes_placed=1 bridges=2 machines=1/1 floor=523 connected=1 unreached=0
|
||||||
|
7:1|e6f748b2690e4ce9fc594f686ee5ddfdfa55c02ca21d428c2eb64c12e042fbdf|seed=7 depth=1 rooms=33 loops=7 lakes_proposed=6 lakes_rejected=6 lakes_placed=0 bridges=0 machines=1/1 floor=673 connected=1 unreached=0
|
||||||
|
100:1|5291c7836429b2291c2a2e19d31e8503c53b7e6b491bcc3af608eb5bcc99582e|seed=100 depth=1 rooms=28 loops=5 lakes_proposed=6 lakes_rejected=3 lakes_placed=3 bridges=5 machines=1/1 floor=575 connected=1 unreached=0
|
||||||
|
500:1|11b6eb403ca4d9f5d72d6603097793ad163e6dd33a4840099b1eca86b2c86adc|seed=500 depth=1 rooms=31 loops=5 lakes_proposed=6 lakes_rejected=5 lakes_placed=1 bridges=1 machines=1/1 floor=672 connected=1 unreached=0
|
||||||
|
2026:10|bd55089a42166b92daf0814b20aff9b6e0b17d49ed7d53d4f25085bd579e44ea|seed=2026 depth=10 rooms=31 loops=5 lakes_proposed=6 lakes_rejected=6 lakes_placed=0 bridges=0 machines=1/1 floor=664 connected=1 unreached=0
|
||||||
|
2026:15|bd55089a42166b92daf0814b20aff9b6e0b17d49ed7d53d4f25085bd579e44ea|seed=2026 depth=15 rooms=31 loops=5 lakes_proposed=6 lakes_rejected=6 lakes_placed=0 bridges=0 machines=1/1 floor=664 connected=1 unreached=0
|
||||||
|
2026:20|bd55089a42166b92daf0814b20aff9b6e0b17d49ed7d53d4f25085bd579e44ea|seed=2026 depth=20 rooms=31 loops=5 lakes_proposed=6 lakes_rejected=6 lakes_placed=0 bridges=0 machines=1/1 floor=664 connected=1 unreached=0
|
||||||
|
2026:25|bd55089a42166b92daf0814b20aff9b6e0b17d49ed7d53d4f25085bd579e44ea|seed=2026 depth=25 rooms=31 loops=5 lakes_proposed=6 lakes_rejected=6 lakes_placed=0 bridges=0 machines=1/1 floor=664 connected=1 unreached=0
|
||||||
|
1234:18|aa9083fa66adc1fbb8c59540364b12390c3a871091c676cadeaa63c4fd5392fd|seed=1234 depth=18 rooms=31 loops=3 lakes_proposed=6 lakes_rejected=4 lakes_placed=2 bridges=0 machines=1/1 floor=646 connected=1 unreached=0
|
||||||
|
9999:22|837916e2c833481487443d99cfeddc1c6b6adb01ef4c3f6d790e4cf08c8052d9|seed=9999 depth=22 rooms=30 loops=4 lakes_proposed=6 lakes_rejected=4 lakes_placed=2 bridges=1 machines=1/1 floor=652 connected=1 unreached=0
|
||||||
53
test/run_tests.sh
Executable file
53
test/run_tests.sh
Executable file
|
|
@ -0,0 +1,53 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Golden-seed regression. For each seed in goldens.txt, regenerate the map and
|
||||||
|
# compare (sha256 of map + summary line). Non-zero exit on any mismatch.
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
BIN=./bin/genesis
|
||||||
|
GOLDENS=test/goldens.txt
|
||||||
|
|
||||||
|
if [[ ! -x "$BIN" ]]; then
|
||||||
|
echo "error: $BIN not built. Run 'make' first." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$GOLDENS" ]]; then
|
||||||
|
echo "error: $GOLDENS missing. Run test/update_goldens.sh to create." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
total=0
|
||||||
|
while IFS='|' read -r key expected_hash expected_summary; do
|
||||||
|
[[ -z "${key:-}" || "${key:0:1}" == "#" ]] && continue
|
||||||
|
seed="${key%:*}"
|
||||||
|
depth="${key#*:}"
|
||||||
|
[[ "$depth" == "$seed" ]] && depth=1
|
||||||
|
total=$((total + 1))
|
||||||
|
actual_hash=$("$BIN" --seed "$seed" --depth "$depth" 2>/dev/null | sha256sum | awk '{print $1}')
|
||||||
|
actual_summary=$("$BIN" --seed "$seed" --depth "$depth" --quiet 2>&1 >/dev/null)
|
||||||
|
if [[ "$actual_hash" != "$expected_hash" ]]; then
|
||||||
|
echo "FAIL $key hash mismatch"
|
||||||
|
echo " expected: $expected_hash"
|
||||||
|
echo " actual: $actual_hash"
|
||||||
|
fail=$((fail + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "$actual_summary" != "$expected_summary" ]]; then
|
||||||
|
echo "FAIL $key summary mismatch"
|
||||||
|
echo " expected: $expected_summary"
|
||||||
|
echo " actual: $actual_summary"
|
||||||
|
fail=$((fail + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "PASS $key"
|
||||||
|
done < "$GOLDENS"
|
||||||
|
|
||||||
|
echo "----"
|
||||||
|
echo "$((total - fail))/$total passed"
|
||||||
|
|
||||||
|
# Invariant sweep: run a wider stress check to catch regressions the goldens
|
||||||
|
# might miss.
|
||||||
|
echo "---- stress (connectivity invariant, 1000 seeds) ----"
|
||||||
|
"$BIN" --stress 1000 2>&1 | tail -1
|
||||||
|
|
||||||
|
exit $fail
|
||||||
34
test/update_goldens.sh
Executable file
34
test/update_goldens.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerate test/goldens.txt from current binary output. Run only after
|
||||||
|
# intentional changes to generation logic.
|
||||||
|
set -eu
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
BIN=./bin/genesis
|
||||||
|
OUT=test/goldens.txt
|
||||||
|
|
||||||
|
if [[ ! -x "$BIN" ]]; then
|
||||||
|
echo "error: $BIN not built. Run 'make' first." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pairs of seed:depth. Mix shallow (water) and deep (lava/chasm/brimstone) to
|
||||||
|
# cover the full liquid range.
|
||||||
|
PAIRS=(
|
||||||
|
"1:1" "2:1" "42:1" "77:1" "1234:1" "2026:1" "9999:1"
|
||||||
|
"123456:1" "31337:1" "7:1" "100:1" "500:1"
|
||||||
|
"2026:10" "2026:15" "2026:20" "2026:25"
|
||||||
|
"1234:18" "9999:22"
|
||||||
|
)
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# seed_depth|map_sha256|summary_line"
|
||||||
|
echo "# Generated $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
for pair in "${PAIRS[@]}"; do
|
||||||
|
seed="${pair%:*}"
|
||||||
|
depth="${pair#*:}"
|
||||||
|
hash=$("$BIN" --seed "$seed" --depth "$depth" 2>/dev/null | sha256sum | awk '{print $1}')
|
||||||
|
summary=$("$BIN" --seed "$seed" --depth "$depth" --quiet 2>&1 >/dev/null)
|
||||||
|
printf '%s:%s|%s|%s\n' "$seed" "$depth" "$hash" "$summary"
|
||||||
|
done
|
||||||
|
} > "$OUT"
|
||||||
|
echo "wrote $OUT ($(wc -l < "$OUT") lines)"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue