Procedural dungeon with rooms, loot, mobs, and lifecycle
Dungeon Example
An advanced example using the Dungeon extension to create a procedurally generated dungeon with loot pools, mob spawning, lifecycle events, and management commands.
Prerequisites: - Build rooms in-game using /bridge schem (see the Dungeon docs for details). - Save them as .droom files and place them in a crypt_rooms/ folder next to this script.
Drop this file into plugins/PyJavaBridge/scripts/ and reload the server.
# type: ignore
"""Example: Creating and running a dungeon with .droom room files.
Setup:
1. Build rooms in-game.
2. Place chests and name them [loot:common], [loot:rare], etc.
3. Run /bridge schem <x> <y> <z> <width> <height> <depth> to capture them.
4. Edit the saved .droom files to add exit definitions, set type, and loot pools.
Exits use exact coordinates: exit: x,y,z <facing> <WxH> [tag]
5. Put the .droom files in a rooms/ folder next to this script.
"""
from bridge import *
# Dungeon and loot_pool come from the extensions package — they are
# not part of the core bridge API.
from bridge.extensions import Dungeon, loot_pool
import random
import inspect
# ── Register Loot Generators ───────────────────────────────────────
# A loot pool is a function that fills a chest's inventory when a room
# containing a chest tagged [loot:<pool_name>] is generated.
#
# The @loot_pool decorator registers the function under the given name.
# It receives:
# - inventory: the chest Inventory to fill
# - room: the Room instance the chest belongs to
@loot_pool("common")
def fill_common(inventory, room):
"""Fill a common loot chest with basic supplies."""
# ItemBuilder creates items with a fluent API.
# .amount() sets the stack size. .build() produces the final Item.
inventory.add_item(ItemBuilder("BREAD").amount(8).build())
inventory.add_item(ItemBuilder("IRON_INGOT").amount(3).build())
inventory.add_item(ItemBuilder("ARROW").amount(16).build())
@loot_pool("rare")
def fill_rare(inventory, room):
"""Fill a rare loot chest with enchanted gear."""
inventory.add_item(
ItemBuilder("DIAMOND_SWORD")
.name("§bFrostbite") # Custom display name (§b = aqua)
.enchant("sharpness", 3) # Apply Sharpness III
.lore("§7A blade of ancient ice") # Gray italic lore text
.glow() # Add enchantment glint
.build()
)
inventory.add_item(ItemBuilder("GOLDEN_APPLE").amount(2).build())
@loot_pool("boss")
def fill_boss(inventory, room):
"""Fill the boss room reward chest with top-tier loot."""
inventory.add_item(
ItemBuilder("NETHERITE_CHESTPLATE")
.name("§5Crypt Lord's Plate") # Dark purple name
.enchant("protection", 5) # Stacking all four protection types
.enchant("blast_protection", 5) # makes this truly overpowered
.enchant("projectile_protection", 5)
.enchant("fire_protection", 5)
.unbreakable() # Never loses durability
.glow()
.lore("§7Taken from the crypt lord's corpse")
.build()
)
inventory.add_item(ItemBuilder("DIAMOND").amount(16).build())
# ── Define the Dungeon ─────────────────────────────────────────────
# The Dungeon object describes a dungeon *template*. It doesn't exist
# in the world yet — call create_instance() to actually build one.
#
# rooms_dir: folder containing .droom files (relative to this script)
# room_count: target number of rooms to generate
# branch_factor: 0 = long corridors (depth-first), 1 = wide (breadth-first)
# start_room: the .droom file to use as the entrance (without extension)
crypt = Dungeon(
name="Ancient Crypt",
rooms_dir="crypt_rooms",
room_count=50,
branch_factor=0.8,
description="A crumbling crypt filled with undead horrors.",
difficulty=3,
start_room="entrance", # loads entrance.droom
)
# ── Dungeon Event Handlers ─────────────────────────────────────────
# Dungeon instances emit lifecycle events that you can hook into.
# Decorate handlers with @crypt.<event_name>.
@crypt.on_enter
async def on_enter(instance, player):
"""Greet the player entering the dungeon."""
# instance is the live DungeonInstance; instance.rooms lists all
# generated rooms. instance.dungeon is the Dungeon template.
player.send_message("§5You descend into the Ancient Crypt...")
player.send_message(
f"§7Rooms: {{len(instance.rooms)}} | Difficulty: {{instance.dungeon.difficulty}}"
)
player.play_sound("ambient_cave")
@crypt.on_complete
async def on_complete(instance):
"""Announce dungeon completion and schedule cleanup."""
for p in instance.players:
p.send_message("§a§lDungeon Complete! §7The Ancient Crypt trembles...")
p.play_sound("ui_toast_challenge_complete")
# server.after(ticks) is a coroutine that resolves after N ticks.
# 600 ticks = 30 seconds at 20 TPS.
await server.after(600)
# destroy() removes all placed blocks and restores the original terrain.
await instance.destroy()
@crypt.on_room_enter
async def on_room_enter(player, room):
"""Notify the player which room they entered."""
# room.template holds the static data from the .droom file.
player.send_message(
f"§8Entering: §f{{room.template.name}} §7({{room.template.type}})"
)
@crypt.on_room_clear
async def on_room_clear(room):
"""Log when a room is cleared (all mobs defeated)."""
print(f"Room {{room.template.name}} at {{room.origin}} cleared!")
# ── Mob Spawning on Room Generation ────────────────────────────────
# on_room_generate fires for each room as the dungeon is being built.
# This is where you spawn enemies or set up traps.
@crypt.on_room_generate
async def spawn_room_mobs(room, world):
"""Spawn zombies proportional to room area."""
# Calculate how many mobs to spawn based on room floor area.
area = max(1, room.template.width * room.template.depth)
count = max(1, area // 25) # roughly 1 zombie per 25 blocks of floor
# room.center gives the (x, y, z) center of the room.
cx, cy, cz = room.center
for _ in range(count):
# Add a small random offset so mobs don't stack on the same spot.
loc = (cx + random.uniform(-1, 1), cy, cz + random.uniform(-1, 1))
try:
# world.spawn_entity() may be sync or async depending on
# the entity type — inspect.isawaitable handles both cases.
r = world.spawn_entity(loc, "zombie")
if inspect.isawaitable(r):
await r
except Exception:
# Silently skip if spawning fails (e.g. location is inside a wall).
pass
# ── Commands ────────────────────────────────────────────────────────
@command("Start a dungeon run")
async def dungeon(event: Event):
"""Create and enter a dungeon instance at the player's location."""
player = event.player
loc = player.location
# Use the player's current position as the dungeon origin.
origin = (int(loc.x), int(loc.y), int(loc.z))
try:
# create_instance() generates rooms, fills loot pools, fires
# on_room_generate, and returns a DungeonInstance.
instance = await crypt.create_instance(
players=[player],
origin=origin,
world=player.world,
)
# List the generated rooms for debugging / fun.
player.send_message(f"§dGenerated {{len(instance.rooms)}} rooms:")
for room in instance.rooms:
player.send_message(
f" §8{{room.origin}} §f{{room.template.name}} §7({{room.template.type}})"
)
except RuntimeError as e:
player.send_message(f"§c{{e}}")
@command("Destroy active dungeon")
async def dungeon_destroy(event: Event):
"""Destroy the most recent dungeon instance."""
# crypt.instances holds all live instances of this dungeon template.
if not crypt.instances:
event.player.send_message("§7No active dungeon instances.")
return
# Grab the latest instance and tear it down.
instance = crypt.instances[-1]
await instance.destroy()
event.player.send_message("§eDungeon destroyed and blocks restored.")
@command("List dungeon instances")
async def dungeon_list(event: Event):
"""List all active dungeon instances."""
if not crypt.instances:
event.player.send_message("§7No active dungeon instances.")
return
for inst in crypt.instances:
event.player.send_message(
f"§dInstance #{{inst.instance_id}} §7- "
f"{{inst.progress:.0%}} complete, "
f"{{len(inst.players)}} player(s), "
f"{{len(inst.rooms)}} rooms"
)