feat: discrete party controller + aligned stair pairs (PR 2)

Adds the Wizardry/M&M core loop: you walk cell-by-cell with 90° turns
and descend stairs between levels that actually line up.

C side:
- pipeline.c: after per-level 2D generation, a link_stairs() pass
  replaces the randomly-placed down/up stairs with aligned pairs
  (room cells preferred). Bottom level loses its down-stair; top
  level keeps the up-stair as the entry point.
- dungeon_to_dict.cpp: expose sizeof(cell3d_t) as "cell_stride" so
  GDScript can index raw cell bytes without hardcoding layout.

Godot side:
- scripts/blobber_party.gd: reads cell3d_t bytes directly for wall
  queries, tweens position/rotation on step/turn, swaps level when
  stair cell is activated.
- scripts/dungeon_builder.gd: now hands the generated Dictionary to
  a party node via `party_path` and groups mesh instances under a
  "Meshes" child for clean regeneration.
- scenes/demo_blobber.tscn: FlyCamera replaced with a Party node
  (script-driven) holding a child Camera3D. num_levels=3 by default.

Still deferred to later PRs: the full port/retirement of src/gen/,
and a standalone plan.c/h module (linkage is currently inlined in
pipeline.c with just StairPair-equivalent data).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
saarsena@gmail.com 2026-04-18 14:00:53 -04:00
parent 06ef034866
commit 5235b5bb22
6 changed files with 345 additions and 11 deletions

View file

@ -3,6 +3,11 @@ extends Node3D
# Blobber dungeon builder. Calls BrogueGen.generate_dungeon() and assembles
# per-material MeshInstance3D children from the returned mesh surface arrays.
# Material ids mirror src/mesh/material_ids.h.
#
# If `party_path` is set, the builder also feeds the generated Dictionary to
# the party controller so it lands on the entry stair with fresh cell data.
const BlobberPartyScript := preload("res://scripts/blobber_party.gd")
const MAT_STONE_FLOOR := 0
const MAT_STONE_CEILING := 1
@ -21,8 +26,10 @@ const MAT_CAVE_WALL := 11
@export var num_levels: int = 1
@export var depth: int = 1
@export var regenerate_on_ready: bool = true
@export var party_path: NodePath
var _materials: Dictionary = {}
var _mesh_parent: Node3D
func _ready() -> void:
_materials = _build_materials()
@ -30,8 +37,11 @@ func _ready() -> void:
regenerate()
func regenerate() -> void:
for child in get_children():
child.queue_free()
if _mesh_parent and is_instance_valid(_mesh_parent):
_mesh_parent.queue_free()
_mesh_parent = Node3D.new()
_mesh_parent.name = "Meshes"
add_child(_mesh_parent)
var gen := BrogueGen.new()
var dungeon: Dictionary = gen.generate_dungeon(seed_value, num_levels, depth)
@ -56,13 +66,18 @@ func regenerate() -> void:
var mi := MeshInstance3D.new()
mi.mesh = mesh
mi.name = "Surface_%d" % material_id
add_child(mi)
_mesh_parent.add_child(mi)
var dims: Vector3i = dungeon.get("dimensions", Vector3i(79, 1, 29))
var levels: Array = dungeon.get("levels", [])
print("Dungeon built: seed=%d dims=%s surfaces=%d levels=%d" %
[seed_value, dims, meshes.size(), levels.size()])
if party_path != NodePath(""):
var party := get_node_or_null(party_path)
if party and party.has_method("setup"):
party.setup(dungeon)
# Distinct albedo colors per material for instant visual debugging.
# Later PRs replace these with proper StandardMaterial3D + textures.
func _build_materials() -> Dictionary: