From 99f0d2a783ddef73b60700d36f240ab05c99d405 Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 5 Oct 2026 13:03:22 +0200 Subject: [PATCH] Nothing is awake and watching when the party arrives Room 0 is left empty so that arriving is not an ambush, but that only ever covered room 0. An enemy in a neighbouring room within aggro range and in sight of the entrance woke the moment you arrived -- in one run in eight before the tunnels were widened, and in three runs in eight after, because wider tunnels mean more open ground and more lines of sight. Measured over 200 runs: 25 before, 75 after. A screenshot of an arrival with enemies visibly in range is what prompted counting. Room enemies that would be awake on arrival -- within their aggro range of the entrance with nothing in the way, the same two questions aggro asks -- are now skipped. That is about one enemy in sixteen. Clearing everything within aggro range regardless of sight would have removed nearly a third, which is a different dungeon rather than a fixed one. The skipped enemy's dice have already been rolled, so every other enemy stands exactly where that seed always put it. Co-Authored-By: Claude Opus 5.5 --- src/instances/instance.gd | 21 +++++++++++++++++++++ tests/integration/test_dungeon_flow.gd | 23 +++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/instances/instance.gd b/src/instances/instance.gd index 0edc293..854749d 100644 --- a/src/instances/instance.gd +++ b/src/instances/instance.gd @@ -135,6 +135,8 @@ func _populate() -> void: var at := world.map.tile_centre(tx, ty) if world.map.circle_blocked(at, def.radius): continue + if _would_see_the_entrance(def, at): + continue world.spawn_enemy(def, at, rng.randi_range(0, 120)) # After every room, so adding tunnel groups leaves each room's population # exactly what that seed always produced. @@ -189,6 +191,25 @@ func _populate_tunnels(rng: RandomNumberGenerator, flavour: DungeonDef) -> void: world.spawn_enemy(member, spots[i], rng.randi_range(0, 120)) +## Whether an enemy placed at [param at] would be awake the moment the party +## arrives: within its aggro range of the entrance, with nothing in the way -- +## the same two questions SimWorld._aggro_target asks. +## +## Room 0 is left empty for exactly this reason, but that only ever covered room +## 0. A neighbouring room in sight of the entrance woke on arrival in one run in +## eight, and once tunnels were widened -- more open ground, more lines of sight +## -- in three runs in eight. Skipping these removes about one enemy in sixteen; +## clearing everything within aggro range instead would have removed nearly a +## third, which is a different dungeon rather than a fixed one. +## +## The rng draws for the skipped enemy have already been made, so every other +## enemy in the dungeon is exactly where that seed always put it. +func _would_see_the_entrance(def: EnemyDef, at: Vector2) -> bool: + if at.distance_to(world.spawn_point) > def.aggro_range: + return false + return world.map.has_line_of_sight(at, world.spawn_point) + + ## Floor tiles of [param leg] that are tunnel rather than room: clear of every ## room by a tile, so a group is never standing in a doorway, and outside the ## boss arena. diff --git a/tests/integration/test_dungeon_flow.gd b/tests/integration/test_dungeon_flow.gd index 5db2e8d..4b3c6e9 100644 --- a/tests/integration/test_dungeon_flow.gd +++ b/tests/integration/test_dungeon_flow.gd @@ -211,3 +211,26 @@ func test_boss_rush_leaves_the_tunnels_empty_too() -> void: var run := Instance.make_dungeon(2, 4242, 3) GameOpts.boss_rush = false assert_eq(_tunnel_enemies(run).size(), 0) + + +func test_nothing_is_awake_and_watching_when_the_party_arrives() -> void: + # Room 0 is left empty so that arriving is not an ambush, but that only ever + # covered room 0. With tunnels widened, three runs in eight had an enemy in + # a neighbouring room within aggro range and in sight of the entrance. + var watching := 0 + for s in range(1, 120): + var run := Instance.make_dungeon(2, s * 7919, 1 + (s % 3)) + for e in run.world.enemies.values(): + if e.pos.distance_to(run.world.spawn_point) <= e.def.aggro_range \ + and run.world.map.has_line_of_sight(e.pos, run.world.spawn_point): + watching += 1 + assert_eq(watching, 0, "%d enemies could see the entrance at the moment of arrival" % watching) + + +func test_clearing_the_entrance_leaves_the_rest_of_the_dungeon_alone() -> void: + # It skips the few enemies that would be awake on arrival and nothing else: + # the dungeon keeps nearly all of its population. + var total := 0 + for s in range(1, 40): + total += Instance.make_dungeon(2, s * 7919, 1).world.enemies.size() + assert_gt(float(total) / 39.0, 9.0, "a depth-1 dungeon still has a dungeon's worth of enemies")