Skip to content

jengine-dungeon

Dungeon crawling: level generation, field of view, turn-based combat, items, monster AI.

KindProject package: linked into the builds of the projects that list it
Sourcepackages/jengine-dungeon
Version0.1.0
Entry pointjengine_dungeon::DungeonPackage
Script namespaceDungeon
Listed byprojects/arcade-classics, projects/engine-test-lab, projects/rpg-classics

List it in the top-level "packages" array of project.jengine:

{
"packages": [
{ "name": "jengine-dungeon", "source": { "type": "path", "path": "../../packages/jengine-dungeon" } }
]
}

Then rebuild the editor viewport for the project (jengine viewport-build projects/<name>, or the editor’s Rebuild viewport banner). Native and web builds link it on their own.

A scene that uses its components must be a v2 scene ("version": 2): a v1 scene has no field for a package component and keeps it as inert custom data.

A script can check that the package is linked with if Dungeon == nil then return end.

Scene keyRust type
dungeonDungeonpackage only: needs a v2 scene

Commands run in every runtime and return nothing; a package publishes what scripts read onto components (see the notes below). The flat name also works.

CallFlat name
Dungeon.move(e, dx, dy)dungeon_move
Dungeon.wait(e)dungeon_wait
Dungeon.quaff(e)dungeon_quaff
Dungeon.descend(e)dungeon_descend
Dungeon.travel(e, wx, wy)dungeon_travel
Dungeon.new_game(e, seed)dungeon_new_game
Dungeon.set_ai(e, on)dungeon_set_ai
Dungeon.ascend(e)dungeon_ascend
Dungeon.pickup(e)dungeon_pickup
Dungeon.use(e, slot)dungeon_use
Dungeon.drop(e, slot)dungeon_drop
Dungeon.zap(e, slot, dx, dy)dungeon_zap
Dungeon.start(e, seed, class)dungeon_start
Dungeon.set_calm(e, on)dungeon_set_calm
Dungeon.set_advise(e, on)dungeon_set_advise
  • projects/arcade-classics/assets/scenes/rogue.scene
  • projects/engine-test-lab/assets/scenes/75-dungeon.scene
  • projects/rpg-classics/assets/scenes/rogue.scene

From packages/jengine-dungeon/README.md.

Dungeon crawling: levels, sight, turns, combat, items and monsters. It’s the reusable core of Rogue and the roguelikes after it.

Used by projects/arcade-classics (Rogue), projects/rpg-classics (DEEP BARROW, a NetHack-lite) and Engine Test Lab scene 75.

It comes in three layers, and the first two are plain Rust with no world involved, so a game (or a test) can use them without the component:

ModuleWhat it is
mapA seeded RNG, the Map / Tile types, and a Rogue-style generator. The level is a 3 x 3 grid of rooms joined by a spanning tree of corridors, plus two extra corridors for loops, with doors and stairs down. Every walkable tile is reachable (tested over 200 seeds).
fovRecursive shadowcasting over eight octants. Walls are seen but hide what is behind them.
gameThe turn-based game, described below.
itemsThe pack, magic items with randomised looks, identification, shops.
botTravel (click to walk) and the bot that plays by itself.

game covers:

  • Actors with speed: energy per turn, so a bat at 150 acts three times in two.
  • Bump-to-attack combat: the to-hit chance is 70% plus 5 per point of attack over defence (clamped to 10–95%), and damage is a dice range.
  • Items: healing potions, gold, and weapons and armour that are worn when better than what you have. Picking up the Amulet wins.
  • Experience levels, and slow regeneration.
  • Levels defined by LevelDef, with the last one holding a boss and the Amulet.
  • Monster AI:
    • A sleeping monster may not notice you at once.
    • An awake one chases using the engine’s A* (tilemap::find_path_with), going round other monsters.
    • It remembers where it last saw you, and wanders when it loses you.
  • The bot: it drinks when hurt and rests when it’s safe, fights what’s next to it, goes for monsters and items it sees, explores to the nearest unknown edge, and then takes the stairs.

All of the content (creatures, gear, levels, numbers) lives in Content, whose default is a two-level dungeon. A scene overrides only what it changes.

Everything below is off by default, so a scene that names none of it plays exactly as the arcade’s Rogue always did - down to the random numbers.

  • Classes (classes): stats, stealth, regeneration, gold, a starting kit (by name; "random wand" picks one), and what each knows (an item, or a whole kind like "wand"). Dungeon.start(e, seed, class).
  • A pack (pack_size): items are carried, gear is wielded and worn by choice (Dungeon.use), dropped (Dungeon.drop), and found with an enchantment from -1 to +2.
  • Magic items (items): potions, scrolls, wands (with charges) and food, each an effect with a power range, levels, weight and price. Potions, scrolls and wands wear a look from looks - “bubbling potion”, “scroll labelled ZELGO MER”, “oak wand” - shuffled every game, and are identified when their effect is seen, by a scroll of identify, or (in a shop) guessed from their price. Effects: potion heal, extra_heal, strength, speed, detect, sleep, confuse, sick, juice; scroll identify, teleport, mapping, enchant_weapon, enchant_armor, scare, summon, fire; wand striking, fire, sleep, dig, teleport (Dungeon.zap(e, slot, dx, dy)).
  • Hunger (hunger.start > 0): nutrition falls a point a turn; hungry, weak (a point off attack and damage), fainting, then starving to death.
  • Monster behaviours (CreatureDef::behavior): pack (comes in a group that wakes together), thief (steals gold, else an item, vanishes and runs; drops it all when killed), sleeper (sleeps until struck), erratic (flits about), peaceful (a shopkeeper: talked to, never fought). Any monster with a range shoots along a clear line.
  • Shops (LevelDef::shop + shopkeeper): a room of priced goods; step on one to see its price, Dungeon.pickup to buy it.
  • The climb back (climb): the Amulet does not win by itself - you carry it back up and out of the first level’s stairs (Dungeon.ascend). Levels are kept as you left them, and climb_monsters come for you on each one you revisit with the Amulet.
  • Explore mode (Dungeon.set_calm): monsters hit half as hard and the hero cannot die - a test’s or a learner’s run.
  • Causes of death: DungeonState.cause reads “killed by a jackal on dungeon level 3”, “starved to death on dungeon level 7”, “killed by an arrow shot by a kobold archer on …”.

Put it on an entity that also has an engine tilemap. The package writes the level into that tilemap at runtime, and draws the creatures and items as atlas-region sprites it spawns and moves.

"tilemap": { "tile_size": [24, 24], "layers": [], "tilesets": [ { "texture_id": "atlases/rogue_tiles.png",
"tile_width": 24, "tile_height": 24, "columns": 7, "tile_count": 7, "first_gid": 1,
"image_width": 168, "image_height": 24 } ] },
"dungeon": { "width": 44, "height": 26, "atlas": "atlases/rogue.png", "step_time": 0.12,
"tiles": { "wall": 1, "floor": 2, "corridor": 3, "door": 4, "stairs_down": 5, "stairs_up": 6, "fog": 7 },
"content": { "...": "overrides" } }
  • The tilemap: the entity’s transform is the map’s top-left corner. The package keeps two layers: terrain (what you have explored) and fog (a dimming tile over explored ground that’s out of view). It rewrites them only when a turn passes.
  • Sprites: creatures show only while in view, and flash red when hit. Items stay where you saw them, dimmed. A step glides rather than jumps.
  • Draw order: tilemaps draw in their own pass, under every sprite whatever the z_order. A full-screen background sprite would hide the dungeon, so leave the background to the project’s background_color.
CallDoes
Dungeon.move(e, dx, dy)step, or attack what’s there (8 directions; a diagonal can’t cut a corner or pass a doorway)
Dungeon.wait(e)pass a turn
Dungeon.quaff(e)drink a potion
Dungeon.descend(e)take the stairs down
Dungeon.travel(e, wx, wy)walk to a world position over explored ground, a step at a time, until a monster comes into view
Dungeon.new_game(e, seed)a new dungeon
Dungeon.set_ai(e, on)let the bot play
Dungeon.start(e, seed, class)a new dungeon as class class (0-based) of content.classes
Dungeon.ascend(e)take the stairs up (out of the first level, with the Amulet, on a climb)
Dungeon.pickup(e)pick up, or buy, what lies here
Dungeon.use(e, slot)drink, read, eat, wield, wear, take off the pack item in slot (0-based)
Dungeon.zap(e, slot, dx, dy)zap the wand in slot in a direction
Dungeon.drop(e, slot)drop the pack item in slot
Dungeon.set_calm(e, on)explore mode
Dungeon.set_advise(e, on)publish the bot’s next action as DungeonState.advice without letting it play

DungeonState, rewritten every frame (read it from a script with tick_rate = 60):

FieldMeaning
events{ kind, text, x, y, wx, wy, amount }; kind is below, and text is a ready-made message line
hp, max_hp, level, levels, xl, xp, gold, potions, kills, turnthe player now
attack, damage, defense, weapon, armorwith gear
dead, won, busy (walking a Dungeon.travel route), px, py
advicewith set_advise or set_ai: { kind, dx, dy, slot }, kind one of move, wait, descend, ascend, pickup, use, zap, drop, none

With a pack, classes or hunger switched on it adds: pack ({ slot, letter, name, kind, region, worn, unknown }), pack_size, here (what lies at your feet, with prices), nutrition, hunger ("", Hungry, Weak, Fainting), fast, confused, helpless (turns left), class, cause, amulet, max_depth, stairs (down, up or ""), discoveries (“bubbling potion = potion of healing”), seed, calm, seen (monsters in view) and str; weapon and armor become the worn items’ names with their enchantment.

Component fields beside content: class, sprite_size (draw actors and items this many pixels square), level_tiles (tile ids per level, so the dungeon changes its look as it goes down), advise, calm.

An event’s kind is one of:

  • combat: hit, miss, kill, hurt, dodge, die, notice
  • items: pickup, gold, equip, quaff, read, zap, eat, wield, wear, drop, buy, identify, enchant, teleport, map, amulet
  • monsters: steal, shoot (from fwx, fwy to wx, wy); a wand’s bolt likewise, its text the effect
  • the hero: hungry (amount 1-3), ascend
  • progress: levelup, level, descend, win
  • info, for anything else
  • src/map.rs: 200 seeds are all connected, with reachable stairs and a solid border, and a seed reproduces its level.
  • src/fov.rs: an open room is seen whole, walls hide what’s behind them, the radius limits sight, and sight is symmetric between open tiles.
  • src/game.rs:
    • start state; walls take no turn; bumping kills and gives XP
    • potions; the stairs and the Amulet
    • a monster chasing and attacking
    • the bot winning at least 8 of 30 dungeons and not all of them
    • cargo test -p jengine-dungeon balance -- --ignored --nocapture prints how the bot fares per seed
  • src/game.rs also: packs wake together, thieves steal gold or an item and drop it when killed, sleepers sleep through company, archers shoot along a clear line, levels are kept and the climb ends out of the first level, hunger weakens and starves, explore mode cannot die.
  • src/items.rs: looks shuffled per game, a class’s kit, drinking and identify identify, sleeping, a wand of striking and of digging, food, gear worn by choice, a shop.
  • tests/deep_barrow.rs: RPG Classics’ DEEP BARROW content as shipped - the bot goes deep and sometimes dies, and explore-mode runs climb out.
  • tests/rogue_scene.rs: the arcade’s Rogue scene.
    • The level is drawn into the tilemap layers, and actors are atlas sprites.
    • A move command takes a turn.
    • The bot reaches level 2, with fog over what it left behind.
    • It also covers test-lab scene 75.