Now and then, a group of enemies in a tunnel
Tunnels are wide enough to fight in now, so occasionally one is occupied: each tunnel leg rolls a 10% chance of a group of two or three of one kind, standing within two tiles of each other. Most legs are too short or too near the entrance to qualify, so in practice it is about one depth-1 run in three (60 of 200 measured), rising with depth as tunnels get longer. Rare on purpose -- tunnels are where you go between fights, and a tunnel that is usually empty is what makes the one that is not a surprise. Never within aggro range of the entrance, checked per member rather than per group: members stand up to two tiles off the group's centre, so a centre that clears the entrance does not mean everyone does. The first version checked the centre, and the nearest member landed 15 units outside aggro range by luck. Never a lone straggler: a leg that cannot fit two gets nobody. Placed after every room, so each room's population is exactly what its seed always produced, and every leg rolls the same dice whether or not it is used, so one tunnel's luck never depends on another's shape. Scaled by the dungeon like every other spawn, each member from its own fresh definition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
855669a665
commit
069abdb3fa
4 files changed
+184
-6
No files matched your search
+12
-6
@@ -151,10 +151,11 @@ static func generate(seed_value: int, depth: int, arena: StringName = &"") -> Di
|
||||
# Connect each room to the previous one, then the last to the arena door,
|
||||
# so every room is reachable by construction rather than by luck.
|
||||
var widths := PackedInt32Array()
|
||||
var legs: Array[Rect2i] = []
|
||||
for i in range(1, rooms.size()):
|
||||
var from := _centre_of(rooms[i - 1])
|
||||
widths.append_array(_carve_corridor(grid, rng, from, _centre_of(rooms[i]),
|
||||
(from.x + from.y) % 2 == 0, boss_room))
|
||||
(from.x + from.y) % 2 == 0, boss_room, legs))
|
||||
if not rooms.is_empty():
|
||||
# Aimed at the tile OUTSIDE the door, and always arriving along the
|
||||
# door's own row. Aiming at the door itself, an L that bent the other
|
||||
@@ -164,7 +165,7 @@ static func generate(seed_value: int, depth: int, arena: StringName = &"") -> Di
|
||||
# rather than eating into it.
|
||||
var outside := door + Vector2i(-1, 0)
|
||||
widths.append_array(_carve_corridor(grid, rng,
|
||||
_centre_of(rooms[rooms.size() - 1]), outside, false, boss_room))
|
||||
_centre_of(rooms[rooms.size() - 1]), outside, false, boss_room, legs))
|
||||
# The door tile itself, and the tile outside it, must be floor.
|
||||
grid.set_tile(door.x, door.y, MapGrid.Kind.FLOOR)
|
||||
grid.set_tile(outside.x, outside.y, MapGrid.Kind.FLOOR)
|
||||
@@ -193,6 +194,10 @@ static func generate(seed_value: int, depth: int, arena: StringName = &"") -> Di
|
||||
# builds a world -- it is here so a test can check the widths actually
|
||||
# vary, which the tiles alone cannot say once tunnels overlap.
|
||||
"corridor_widths": widths,
|
||||
# The rectangle each tunnel leg was carved over. Overlaps the rooms it
|
||||
# joins at its ends, so whoever uses it decides what counts as tunnel --
|
||||
# Instance._populate_tunnels keeps only floor clear of every room.
|
||||
"tunnels": legs,
|
||||
# Which boss lives here follows from WHICH arena was stamped. Choosing
|
||||
# it anywhere else would let a dungeon put the Cantor in the Warden's
|
||||
# hall, where its patterns assume geometry that is not there.
|
||||
@@ -214,7 +219,7 @@ static func _centre_of(r: Rect2i) -> Vector2i:
|
||||
## walls it was drawn with.
|
||||
static func _carve_corridor(grid: MapGrid, rng: RandomNumberGenerator,
|
||||
from: Vector2i, to: Vector2i, horizontal_first: bool,
|
||||
keep_out: Rect2i) -> PackedInt32Array:
|
||||
keep_out: Rect2i, legs: Array[Rect2i]) -> PackedInt32Array:
|
||||
var first := rng.randi_range(CORRIDOR_MIN_WIDTH, CORRIDOR_MAX_WIDTH)
|
||||
var second := rng.randi_range(CORRIDOR_MIN_WIDTH, CORRIDOR_MAX_WIDTH)
|
||||
var bend := Vector2i(to.x, from.y) if horizontal_first else Vector2i(from.x, to.y)
|
||||
@@ -223,8 +228,8 @@ static func _carve_corridor(grid: MapGrid, rng: RandomNumberGenerator,
|
||||
# the INNER side of the bend, so leaving this out never narrows a tunnel
|
||||
# below its width -- it just bevels the outer corner, which reads as a
|
||||
# notch rather than a turn.
|
||||
_carve_leg(grid, from, bend, first, second / 2, keep_out)
|
||||
_carve_leg(grid, bend, to, second, first / 2, keep_out)
|
||||
legs.append(_carve_leg(grid, from, bend, first, second / 2, keep_out))
|
||||
legs.append(_carve_leg(grid, bend, to, second, first / 2, keep_out))
|
||||
return PackedInt32Array([first, second])
|
||||
|
||||
|
||||
@@ -235,7 +240,7 @@ static func _carve_corridor(grid: MapGrid, rng: RandomNumberGenerator,
|
||||
## world is sealed by construction, so a wide tunnel that hugs the edge must
|
||||
## stop one tile short of it rather than open the map to nothing.
|
||||
static func _carve_leg(grid: MapGrid, a: Vector2i, b: Vector2i, width: int,
|
||||
overshoot: int, keep_out: Rect2i) -> void:
|
||||
overshoot: int, keep_out: Rect2i) -> Rect2i:
|
||||
# Width is laid across the leg, centred on its line: two tiles is the line
|
||||
# and the one after it, three is one either side, four leans one way.
|
||||
var lo := -(width - 1) / 2
|
||||
@@ -254,6 +259,7 @@ static func _carve_leg(grid: MapGrid, a: Vector2i, b: Vector2i, width: int,
|
||||
if keep_out.has_point(Vector2i(x, y)):
|
||||
continue
|
||||
grid.set_tile(x, y, MapGrid.Kind.FLOOR)
|
||||
return rect
|
||||
|
||||
|
||||
## A few pillars and pits inside generated rooms, so open rooms still have
|
||||
|
||||
Reference in new issue
Block a user