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:
parent
06ef034866
commit
5235b5bb22
6 changed files with 345 additions and 11 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
[gd_scene format=3 uid="uid://c8blbrbrbr001"]
|
[gd_scene format=3 uid="uid://c8blbrbrbr001"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://b2r6hnyvt7ef7" path="res://scripts/dungeon_builder.gd" id="1_builder"]
|
[ext_resource type="Script" uid="uid://b2r6hnyvt7ef7" path="res://scripts/dungeon_builder.gd" id="1_builder"]
|
||||||
[ext_resource type="Script" uid="uid://d0srrm35g1m0t" path="res://scripts/fly_camera.gd" id="2_flycam"]
|
[ext_resource type="Script" path="res://scripts/blobber_party.gd" id="2_party"]
|
||||||
|
|
||||||
[sub_resource type="Environment" id="Environment_1"]
|
[sub_resource type="Environment" id="Environment_1"]
|
||||||
background_mode = 1
|
background_mode = 1
|
||||||
|
|
@ -15,8 +15,9 @@ ambient_light_energy = 0.5
|
||||||
[node name="Dungeon" type="Node3D" parent="." unique_id=2089249369]
|
[node name="Dungeon" type="Node3D" parent="." unique_id=2089249369]
|
||||||
script = ExtResource("1_builder")
|
script = ExtResource("1_builder")
|
||||||
seed_value = 43
|
seed_value = 43
|
||||||
num_levels = 8
|
num_levels = 3
|
||||||
depth = 20
|
depth = 20
|
||||||
|
party_path = NodePath("../Party")
|
||||||
|
|
||||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1913401313]
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1913401313]
|
||||||
transform = Transform3D(0.86602527, -0.35310844, 0.35399818, 0, 0.70799595, 0.70621645, -0.5000003, -0.6116013, 0.6131424, 80, 40, 80)
|
transform = Transform3D(0.86602527, -0.35310844, 0.35399818, 0, 0.70799595, 0.70621645, -0.5000003, -0.6116013, 0.6131424, 80, 40, 80)
|
||||||
|
|
@ -25,9 +26,10 @@ shadow_enabled = true
|
||||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1337946831]
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1337946831]
|
||||||
environment = SubResource("Environment_1")
|
environment = SubResource("Environment_1")
|
||||||
|
|
||||||
[node name="FlyCamera" type="Camera3D" parent="." unique_id=987300286]
|
[node name="Party" type="Node3D" parent="." unique_id=987300286]
|
||||||
transform = Transform3D(0.9, 0, 0, 0, 0.7, 0.7, 0, -0.7, 0.7, 120, 40, 80)
|
script = ExtResource("2_party")
|
||||||
fov = 65.0
|
|
||||||
|
[node name="Camera3D" type="Camera3D" parent="Party"]
|
||||||
|
fov = 75.0
|
||||||
near = 0.1
|
near = 0.1
|
||||||
far = 500.0
|
far = 500.0
|
||||||
script = ExtResource("2_flycam")
|
|
||||||
|
|
|
||||||
219
demo/scripts/blobber_party.gd
Normal file
219
demo/scripts/blobber_party.gd
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
class_name BlobberParty
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
# Discrete cell-step party controller (Wizardry / M&M style).
|
||||||
|
# Reads cell3d_t bytes straight out of the dungeon Dictionary to query wall
|
||||||
|
# flags; no collision shape needed. Stair cells trigger a level switch.
|
||||||
|
|
||||||
|
signal level_changed(new_level: int)
|
||||||
|
signal stepped(cell: Vector2i, level: int)
|
||||||
|
signal blocked(face: int)
|
||||||
|
|
||||||
|
const FACE_N := 0
|
||||||
|
const FACE_E := 1
|
||||||
|
const FACE_S := 2
|
||||||
|
const FACE_W := 3
|
||||||
|
|
||||||
|
# cell3d_t byte offsets (see src/blobber/cell3d.h)
|
||||||
|
const OFF_FLOOR := 0
|
||||||
|
const OFF_WALLS := 2 # walls[0..3]
|
||||||
|
|
||||||
|
# b_floor_t values
|
||||||
|
const FT_VOID := 0
|
||||||
|
const FT_STONE := 1
|
||||||
|
const FT_STAIR_UP := 5
|
||||||
|
const FT_STAIR_DOWN := 6
|
||||||
|
|
||||||
|
@export var step_duration: float = 0.18
|
||||||
|
@export var turn_duration: float = 0.14
|
||||||
|
@export var eye_height_ratio: float = 0.55 # fraction of cell_size above floor
|
||||||
|
|
||||||
|
var cell: Vector2i = Vector2i.ZERO
|
||||||
|
var level: int = 0
|
||||||
|
var facing: int = FACE_N
|
||||||
|
|
||||||
|
var _cells: PackedByteArray
|
||||||
|
var _width: int = 0
|
||||||
|
var _height: int = 0
|
||||||
|
var _n_levels: int = 1
|
||||||
|
var _cell_size: float = 3.0
|
||||||
|
var _cell_stride: int = 12
|
||||||
|
var _levels_meta: Array = []
|
||||||
|
var _busy: bool = false
|
||||||
|
|
||||||
|
func setup(dungeon: Dictionary) -> void:
|
||||||
|
_cells = dungeon.get("cells", PackedByteArray())
|
||||||
|
var dims: Vector3i = dungeon.get("dimensions", Vector3i(79, 1, 29))
|
||||||
|
_width = dims.x
|
||||||
|
_n_levels = dims.y
|
||||||
|
_height = dims.z
|
||||||
|
_cell_size = float(dungeon.get("cell_size", 3.0))
|
||||||
|
_cell_stride = int(dungeon.get("cell_stride", 12))
|
||||||
|
_levels_meta = dungeon.get("levels", [])
|
||||||
|
|
||||||
|
var entry := _entry_cell()
|
||||||
|
cell = entry
|
||||||
|
level = 0
|
||||||
|
facing = FACE_N
|
||||||
|
_snap_to_cell(true)
|
||||||
|
|
||||||
|
# --- Public controls -------------------------------------------------------
|
||||||
|
|
||||||
|
func try_step_forward() -> void:
|
||||||
|
_try_step(_facing_delta(facing))
|
||||||
|
|
||||||
|
func try_step_back() -> void:
|
||||||
|
_try_step(-_facing_delta(facing))
|
||||||
|
|
||||||
|
func try_step_left() -> void:
|
||||||
|
_try_step(_facing_delta((facing + 3) % 4))
|
||||||
|
|
||||||
|
func try_step_right() -> void:
|
||||||
|
_try_step(_facing_delta((facing + 1) % 4))
|
||||||
|
|
||||||
|
func try_turn_left() -> void:
|
||||||
|
if _busy: return
|
||||||
|
facing = (facing + 3) % 4
|
||||||
|
_tween_rotation()
|
||||||
|
|
||||||
|
func try_turn_right() -> void:
|
||||||
|
if _busy: return
|
||||||
|
facing = (facing + 1) % 4
|
||||||
|
_tween_rotation()
|
||||||
|
|
||||||
|
func try_use_stair() -> void:
|
||||||
|
if _busy: return
|
||||||
|
var f := _cell_floor(level, cell.x, cell.y)
|
||||||
|
if f == FT_STAIR_DOWN and level + 1 < _n_levels:
|
||||||
|
_change_level(level + 1)
|
||||||
|
elif f == FT_STAIR_UP and level > 0:
|
||||||
|
_change_level(level - 1)
|
||||||
|
|
||||||
|
# --- Input -----------------------------------------------------------------
|
||||||
|
|
||||||
|
func _unhandled_key_input(event: InputEvent) -> void:
|
||||||
|
if not (event is InputEventKey) or not event.pressed or event.echo:
|
||||||
|
return
|
||||||
|
var key: int = event.keycode
|
||||||
|
match key:
|
||||||
|
KEY_W, KEY_UP: try_step_forward()
|
||||||
|
KEY_S, KEY_DOWN: try_step_back()
|
||||||
|
KEY_A: try_step_left()
|
||||||
|
KEY_D: try_step_right()
|
||||||
|
KEY_Q, KEY_LEFT: try_turn_left()
|
||||||
|
KEY_E, KEY_RIGHT: try_turn_right()
|
||||||
|
KEY_SPACE, KEY_ENTER, KEY_PERIOD, KEY_GREATER, KEY_LESS: try_use_stair()
|
||||||
|
|
||||||
|
# --- Movement internals ----------------------------------------------------
|
||||||
|
|
||||||
|
func _try_step(delta: Vector2i) -> void:
|
||||||
|
if _busy: return
|
||||||
|
var face: int = _delta_to_face(delta)
|
||||||
|
if face < 0:
|
||||||
|
return
|
||||||
|
if _cell_wall(level, cell.x, cell.y, face) != 0: # W_NONE == 0
|
||||||
|
blocked.emit(face)
|
||||||
|
return
|
||||||
|
var nx := cell.x + delta.x
|
||||||
|
var ny := cell.y + delta.y
|
||||||
|
if nx < 0 or ny < 0 or nx >= _width or ny >= _height:
|
||||||
|
return
|
||||||
|
var dest_floor := _cell_floor(level, nx, ny)
|
||||||
|
if dest_floor == FT_VOID:
|
||||||
|
return
|
||||||
|
cell = Vector2i(nx, ny)
|
||||||
|
stepped.emit(cell, level)
|
||||||
|
_tween_position()
|
||||||
|
|
||||||
|
func _change_level(new_level: int) -> void:
|
||||||
|
# Land on the paired stair on the adjacent level. Plan guarantees the XYs
|
||||||
|
# match, so we keep our cell coordinates.
|
||||||
|
level = new_level
|
||||||
|
level_changed.emit(level)
|
||||||
|
_snap_to_cell(true)
|
||||||
|
|
||||||
|
# --- Cell byte reads -------------------------------------------------------
|
||||||
|
|
||||||
|
func _cell_index(lvl: int, x: int, y: int) -> int:
|
||||||
|
return ((lvl * _height + y) * _width + x) * _cell_stride
|
||||||
|
|
||||||
|
func _cell_floor(lvl: int, x: int, y: int) -> int:
|
||||||
|
return _cells[_cell_index(lvl, x, y) + OFF_FLOOR]
|
||||||
|
|
||||||
|
func _cell_wall(lvl: int, x: int, y: int, face: int) -> int:
|
||||||
|
return _cells[_cell_index(lvl, x, y) + OFF_WALLS + face]
|
||||||
|
|
||||||
|
# --- Geometry --------------------------------------------------------------
|
||||||
|
|
||||||
|
func _facing_delta(f: int) -> Vector2i:
|
||||||
|
match f:
|
||||||
|
FACE_N: return Vector2i(0, -1)
|
||||||
|
FACE_E: return Vector2i(1, 0)
|
||||||
|
FACE_S: return Vector2i(0, 1)
|
||||||
|
FACE_W: return Vector2i(-1, 0)
|
||||||
|
return Vector2i.ZERO
|
||||||
|
|
||||||
|
func _delta_to_face(d: Vector2i) -> int:
|
||||||
|
if d == Vector2i(0, -1): return FACE_N
|
||||||
|
if d == Vector2i(1, 0): return FACE_E
|
||||||
|
if d == Vector2i(0, 1): return FACE_S
|
||||||
|
if d == Vector2i(-1, 0): return FACE_W
|
||||||
|
return -1
|
||||||
|
|
||||||
|
func _cell_world_position(lvl: int, x: int, y: int) -> Vector3:
|
||||||
|
var s := _cell_size
|
||||||
|
return Vector3(
|
||||||
|
(float(x) + 0.5) * s,
|
||||||
|
-float(lvl) * s - s * (1.0 - eye_height_ratio),
|
||||||
|
(float(y) + 0.5) * s)
|
||||||
|
|
||||||
|
func _facing_yaw(f: int) -> float:
|
||||||
|
# Camera default forward = -Z. N (-y → -z) = 0. Then +x is -PI/2, etc.
|
||||||
|
match f:
|
||||||
|
FACE_N: return 0.0
|
||||||
|
FACE_E: return -PI * 0.5
|
||||||
|
FACE_S: return PI
|
||||||
|
FACE_W: return PI * 0.5
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
func _snap_to_cell(reset_rotation: bool) -> void:
|
||||||
|
position = _cell_world_position(level, cell.x, cell.y)
|
||||||
|
if reset_rotation:
|
||||||
|
rotation.y = _facing_yaw(facing)
|
||||||
|
|
||||||
|
func _tween_position() -> void:
|
||||||
|
var target := _cell_world_position(level, cell.x, cell.y)
|
||||||
|
_busy = true
|
||||||
|
var tw := create_tween()
|
||||||
|
tw.tween_property(self, "position", target, step_duration)
|
||||||
|
tw.finished.connect(func(): _busy = false)
|
||||||
|
|
||||||
|
func _tween_rotation() -> void:
|
||||||
|
var target_yaw := _facing_yaw(facing)
|
||||||
|
# Take the shortest angular path.
|
||||||
|
var cur := rotation.y
|
||||||
|
var diff := wrapf(target_yaw - cur, -PI, PI)
|
||||||
|
target_yaw = cur + diff
|
||||||
|
_busy = true
|
||||||
|
var tw := create_tween()
|
||||||
|
tw.tween_property(self, "rotation:y", target_yaw, turn_duration)
|
||||||
|
tw.finished.connect(func(): _busy = false)
|
||||||
|
|
||||||
|
# --- Entry point -----------------------------------------------------------
|
||||||
|
|
||||||
|
func _entry_cell() -> Vector2i:
|
||||||
|
if _levels_meta.size() > 0:
|
||||||
|
var lv0: Dictionary = _levels_meta[0]
|
||||||
|
var su: Vector2i = lv0.get("stairs_up", Vector2i(-1, -1))
|
||||||
|
if su.x >= 0:
|
||||||
|
return su
|
||||||
|
var sd: Vector2i = lv0.get("stairs_down", Vector2i(-1, -1))
|
||||||
|
if sd.x >= 0:
|
||||||
|
return sd
|
||||||
|
# Fallback: first standable cell on level 0.
|
||||||
|
for y in _height:
|
||||||
|
for x in _width:
|
||||||
|
var f := _cell_floor(0, x, y)
|
||||||
|
if f != FT_VOID:
|
||||||
|
return Vector2i(x, y)
|
||||||
|
return Vector2i.ZERO
|
||||||
|
|
@ -3,6 +3,11 @@ extends Node3D
|
||||||
# Blobber dungeon builder. Calls BrogueGen.generate_dungeon() and assembles
|
# Blobber dungeon builder. Calls BrogueGen.generate_dungeon() and assembles
|
||||||
# per-material MeshInstance3D children from the returned mesh surface arrays.
|
# per-material MeshInstance3D children from the returned mesh surface arrays.
|
||||||
# Material ids mirror src/mesh/material_ids.h.
|
# 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_FLOOR := 0
|
||||||
const MAT_STONE_CEILING := 1
|
const MAT_STONE_CEILING := 1
|
||||||
|
|
@ -21,8 +26,10 @@ const MAT_CAVE_WALL := 11
|
||||||
@export var num_levels: int = 1
|
@export var num_levels: int = 1
|
||||||
@export var depth: int = 1
|
@export var depth: int = 1
|
||||||
@export var regenerate_on_ready: bool = true
|
@export var regenerate_on_ready: bool = true
|
||||||
|
@export var party_path: NodePath
|
||||||
|
|
||||||
var _materials: Dictionary = {}
|
var _materials: Dictionary = {}
|
||||||
|
var _mesh_parent: Node3D
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_materials = _build_materials()
|
_materials = _build_materials()
|
||||||
|
|
@ -30,8 +37,11 @@ func _ready() -> void:
|
||||||
regenerate()
|
regenerate()
|
||||||
|
|
||||||
func regenerate() -> void:
|
func regenerate() -> void:
|
||||||
for child in get_children():
|
if _mesh_parent and is_instance_valid(_mesh_parent):
|
||||||
child.queue_free()
|
_mesh_parent.queue_free()
|
||||||
|
_mesh_parent = Node3D.new()
|
||||||
|
_mesh_parent.name = "Meshes"
|
||||||
|
add_child(_mesh_parent)
|
||||||
|
|
||||||
var gen := BrogueGen.new()
|
var gen := BrogueGen.new()
|
||||||
var dungeon: Dictionary = gen.generate_dungeon(seed_value, num_levels, depth)
|
var dungeon: Dictionary = gen.generate_dungeon(seed_value, num_levels, depth)
|
||||||
|
|
@ -56,13 +66,18 @@ func regenerate() -> void:
|
||||||
var mi := MeshInstance3D.new()
|
var mi := MeshInstance3D.new()
|
||||||
mi.mesh = mesh
|
mi.mesh = mesh
|
||||||
mi.name = "Surface_%d" % material_id
|
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 dims: Vector3i = dungeon.get("dimensions", Vector3i(79, 1, 29))
|
||||||
var levels: Array = dungeon.get("levels", [])
|
var levels: Array = dungeon.get("levels", [])
|
||||||
print("Dungeon built: seed=%d dims=%s surfaces=%d levels=%d" %
|
print("Dungeon built: seed=%d dims=%s surfaces=%d levels=%d" %
|
||||||
[seed_value, dims, meshes.size(), levels.size()])
|
[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.
|
# Distinct albedo colors per material for instant visual debugging.
|
||||||
# Later PRs replace these with proper StandardMaterial3D + textures.
|
# Later PRs replace these with proper StandardMaterial3D + textures.
|
||||||
func _build_materials() -> Dictionary:
|
func _build_materials() -> Dictionary:
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ Dictionary godot::dungeon_to_dictionary(const dungeon_t *d, const mesh_build_t *
|
||||||
out["depth"] = depth;
|
out["depth"] = depth;
|
||||||
out["num_levels"] = num_levels;
|
out["num_levels"] = num_levels;
|
||||||
out["cell_size"] = mb->cell_size;
|
out["cell_size"] = mb->cell_size;
|
||||||
|
out["cell_stride"] = (int)sizeof(cell3d_t);
|
||||||
out["dimensions"] = Vector3i(BL_DCOLS, num_levels, BL_DROWS);
|
out["dimensions"] = Vector3i(BL_DCOLS, num_levels, BL_DROWS);
|
||||||
|
|
||||||
// Raw voxel cell data for future mutation. Order: z-major (level,y,x).
|
// Raw voxel cell data for future mutation. Order: z-major (level,y,x).
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,99 @@ static void materialize_level(dungeon_t *d, int lvl, grid_t g) {
|
||||||
compute_face_walls(d, lvl);
|
compute_face_walls(d, lvl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Count cells that are FT_STONE (and optionally S_ROOM) on both levels. */
|
||||||
|
static int count_shared_floor(const dungeon_t *d, int la, int lb, int require_room) {
|
||||||
|
int n = 0;
|
||||||
|
for (int y = 0; y < BL_DROWS; y++) {
|
||||||
|
for (int x = 0; x < BL_DCOLS; x++) {
|
||||||
|
const cell3d_t *ca = dungeon_cell_c(d, la, x, y);
|
||||||
|
const cell3d_t *cb = dungeon_cell_c(d, lb, x, y);
|
||||||
|
if (ca->floor != FT_STONE || cb->floor != FT_STONE) continue;
|
||||||
|
if (require_room && (ca->style != S_ROOM || cb->style != S_ROOM)) continue;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int pick_nth_shared(const dungeon_t *d, int la, int lb, int require_room,
|
||||||
|
int target, int *ox, int *oy) {
|
||||||
|
int n = 0;
|
||||||
|
for (int y = 0; y < BL_DROWS; y++) {
|
||||||
|
for (int x = 0; x < BL_DCOLS; x++) {
|
||||||
|
const cell3d_t *ca = dungeon_cell_c(d, la, x, y);
|
||||||
|
const cell3d_t *cb = dungeon_cell_c(d, lb, x, y);
|
||||||
|
if (ca->floor != FT_STONE || cb->floor != FT_STONE) continue;
|
||||||
|
if (require_room && (ca->style != S_ROOM || cb->style != S_ROOM)) continue;
|
||||||
|
if (n == target) { *ox = x; *oy = y; return 1; }
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If (x,y) on level lvl currently holds expected_floor, reset it to FT_STONE.
|
||||||
|
Used to undo the per-level 2D stair placement before dropping an aligned
|
||||||
|
stair pair at a new XY. */
|
||||||
|
static void clear_stair_cell(dungeon_t *d, int lvl, int x, int y, uint8_t expected_floor) {
|
||||||
|
if (x < 0 || y < 0 || !b_in_bounds(x, y)) return;
|
||||||
|
cell3d_t *c = dungeon_cell(d, lvl, x, y);
|
||||||
|
if (c->floor == expected_floor) c->floor = FT_STONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Replace each level's per-level 2D stair placement with aligned stair pairs
|
||||||
|
so level L's down-stair and level L+1's up-stair share the same XY. The
|
||||||
|
bottom level's down-stair is removed (no level below). The top level keeps
|
||||||
|
its existing up-stair as the party's entry point. Does nothing when
|
||||||
|
n_levels < 2. */
|
||||||
|
static void link_stairs(dungeon_t *d, uint64_t seed) {
|
||||||
|
if (d->n_levels < 2) return;
|
||||||
|
|
||||||
|
rng_t rng;
|
||||||
|
rng_seed(&rng, seed ^ 0xB106B3D5B4B83A07ULL);
|
||||||
|
|
||||||
|
for (int lvl = 0; lvl < d->n_levels - 1; lvl++) {
|
||||||
|
clear_stair_cell(d, lvl, d->levels[lvl].stairs_down_x,
|
||||||
|
d->levels[lvl].stairs_down_y, FT_STAIR_DOWN);
|
||||||
|
clear_stair_cell(d, lvl + 1, d->levels[lvl + 1].stairs_up_x,
|
||||||
|
d->levels[lvl + 1].stairs_up_y, FT_STAIR_UP);
|
||||||
|
|
||||||
|
int sx = -1, sy = -1;
|
||||||
|
int n_rooms = count_shared_floor(d, lvl, lvl + 1, 1);
|
||||||
|
if (n_rooms > 0) {
|
||||||
|
int pick = (int)(rng_u32(&rng) % (uint32_t)n_rooms);
|
||||||
|
pick_nth_shared(d, lvl, lvl + 1, 1, pick, &sx, &sy);
|
||||||
|
} else {
|
||||||
|
int n_any = count_shared_floor(d, lvl, lvl + 1, 0);
|
||||||
|
if (n_any > 0) {
|
||||||
|
int pick = (int)(rng_u32(&rng) % (uint32_t)n_any);
|
||||||
|
pick_nth_shared(d, lvl, lvl + 1, 0, pick, &sx, &sy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sx < 0) {
|
||||||
|
/* No shared standable cell — leave this boundary unlinked. */
|
||||||
|
d->levels[lvl].stairs_down_x = -1;
|
||||||
|
d->levels[lvl].stairs_down_y = -1;
|
||||||
|
d->levels[lvl + 1].stairs_up_x = -1;
|
||||||
|
d->levels[lvl + 1].stairs_up_y = -1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
dungeon_cell(d, lvl, sx, sy)->floor = FT_STAIR_DOWN;
|
||||||
|
dungeon_cell(d, lvl + 1, sx, sy)->floor = FT_STAIR_UP;
|
||||||
|
d->levels[lvl].stairs_down_x = sx;
|
||||||
|
d->levels[lvl].stairs_down_y = sy;
|
||||||
|
d->levels[lvl + 1].stairs_up_x = sx;
|
||||||
|
d->levels[lvl + 1].stairs_up_y = sy;
|
||||||
|
}
|
||||||
|
|
||||||
|
int last = d->n_levels - 1;
|
||||||
|
clear_stair_cell(d, last, d->levels[last].stairs_down_x,
|
||||||
|
d->levels[last].stairs_down_y, FT_STAIR_DOWN);
|
||||||
|
d->levels[last].stairs_down_x = -1;
|
||||||
|
d->levels[last].stairs_down_y = -1;
|
||||||
|
}
|
||||||
|
|
||||||
dungeon_t *blobber_generate(uint64_t seed, int n_levels, int depth) {
|
dungeon_t *blobber_generate(uint64_t seed, int n_levels, int depth) {
|
||||||
if (n_levels < 1) n_levels = 1;
|
if (n_levels < 1) n_levels = 1;
|
||||||
dungeon_t *d = dungeon_create(n_levels);
|
dungeon_t *d = dungeon_create(n_levels);
|
||||||
|
|
@ -120,5 +213,6 @@ dungeon_t *blobber_generate(uint64_t seed, int n_levels, int depth) {
|
||||||
gen_level_2d(g, level_seed, depth);
|
gen_level_2d(g, level_seed, depth);
|
||||||
materialize_level(d, lvl, g);
|
materialize_level(d, lvl, g);
|
||||||
}
|
}
|
||||||
|
link_stairs(d, seed);
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,11 @@
|
||||||
/* Generate a dungeon using the existing 2D pipeline under the hood, then
|
/* Generate a dungeon using the existing 2D pipeline under the hood, then
|
||||||
materialize into a 3D cell3d_t grid with per-face wall flags.
|
materialize into a 3D cell3d_t grid with per-face wall flags.
|
||||||
|
|
||||||
PR 1: each of the n_levels runs an independent 2D generation with seed +
|
Each of the n_levels runs an independent 2D generation with seed +
|
||||||
level_index. Inter-level linkage is absent (lands in later PRs).
|
level_index. When n_levels > 1, a post-pass replaces the per-level stair
|
||||||
|
placement with aligned pairs: level L's down-stair and level L+1's
|
||||||
|
up-stair share the same XY. The bottom level has no down-stair, the top
|
||||||
|
level keeps its up-stair as the party's entry point.
|
||||||
|
|
||||||
Returns a newly-allocated dungeon_t; caller must dungeon_destroy() it.
|
Returns a newly-allocated dungeon_t; caller must dungeon_destroy() it.
|
||||||
Returns NULL on allocation failure. */
|
Returns NULL on allocation failure. */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue