jengine-goldbox
A party of six across three packages - the first package here that depends on another.
| Kind | Project package: linked into the builds of the projects that list it |
| Source | packages/jengine-goldbox |
| Version | 0.1.0 |
| Entry point | jengine_goldbox::GoldboxPackage |
| Script namespace | Goldbox |
| Depends on | jengine-crawler, jengine-realm |
| Listed by | projects/arcade-classics |
Using it
Section titled “Using it”List it in the top-level "packages" array of project.jengine, together with every package it depends on: a crate dependency compiles a package in but does not register it, and packages_a_project_lists_cover_their_dependencies fails a project that lists only the outer one:
{ "packages": [ { "name": "jengine-goldbox", "source": { "type": "path", "path": "../../packages/jengine-goldbox" } }, { "name": "jengine-crawler", "source": { "type": "path", "path": "../../packages/jengine-crawler" } }, { "name": "jengine-realm", "source": { "type": "path", "path": "../../packages/jengine-realm" } } ]}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 Goldbox == nil then return end.
Components
Section titled “Components”| Scene key | Rust type | |
|---|---|---|
gb_game | GoldBox | package only: needs a v2 scene |
Script commands
Section titled “Script commands”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.
| Call | Flat name |
|---|---|
Goldbox.restore(e, party) | gb_restore |
Goldbox.walk(e, dx, dy) | gb_walk |
Goldbox.turn(e, steps) | gb_turn |
Goldbox.fight_move(e, dx, dy) | gb_fight_move |
Goldbox.new_party(e) | gb_new_party |
Goldbox.enter(e) | gb_enter |
Goldbox.leave(e) | gb_leave |
Goldbox.use(e) | gb_use |
Goldbox.rest(e) | gb_rest |
Goldbox.fight_attack(e) | gb_fight_attack |
Goldbox.fight_approach(e) | gb_fight_approach |
Goldbox.fight_guard(e) | gb_fight_guard |
Goldbox.fight_flee(e) | gb_fight_flee |
Goldbox.fight_ai(e) | gb_fight_ai |
Scenes that use it
Section titled “Scenes that use it”projects/arcade-classics/assets/scenes/goldbox.scene
From packages/jengine-goldbox/src/lib.rs.
Gold Box - one party, three packages.
What this game is here to prove
Section titled “What this game is here to prove”Not a genre. Every piece of a Gold Box game already exists in this arcade:
the first-person grid view is Umber Warren’s (jengine-crawler), the
tactical grid battle is Hollow Crown’s (jengine-realm), the party and bag
are Monster Town’s (jengine-rpg). A row claiming this breaks new renderer
or simulation ground would be wrong.
What it breaks is structural, and it is the shape the bugs that actually reach players come from. Audited Sep 2026:
- No package in this repo depends on another. All thirty-eight depend
on
jengine-coreand nothing else. Two scenes do use two packages at once (Loam Hollow, Steel Rain), but those are two independent packages on one entity list, not one package using another. - No package has ever read a resource another package inserted. Every
ResourceMapread is a package reading its own, or the frame the host lends (InputState,FrameCamera; seedocs/writing-packages.md). - Each of the three owns a whole party of its own. They were written as complete games, not as parts, so composing them means deciding who owns the party - which is the question this package exists to answer.
The rule that comes with a crate dependency
Section titled “The rule that comes with a crate dependency”A crate dependency compiles a package in. It does not register it.
jengine-builder writes one host.add_package(Box::new(..)) per package
listed in the project’s top-level "packages" array. A package reached
only as a crate dependency never appears in that list, so its components
are never registered and its script commands are never installed - the
scene loads, the key lands in CustomComponents, and the first sign is
attempt to call a nil value. That is how Dusk Rampart shipped broken, one
level further out.
So: depend on the crate, and list both packages.
tests/composing_packages.rs says it out loud, and
packages_a_project_lists_cover_their_dependencies in jengine-core
fails a project that lists one and not the other.
Who owns the party
Section titled “Who owns the party”This package does, in Party, carried on the GoldBox (gb_game)
component beside the run it is on. The crawler and
the realm keep their own parties for their own games; Gold Box hands each
of them the state it needs and reads the result back, because teaching two
shipped games to share a party they know nothing about would change Umber
Warren and Hollow Crown to serve something neither has heard of.
The scene
Section titled “The scene”gb_game on one entity. It names Umber Warren’s tables, because the
dungeon is Umber Warren’s - not a copy of it.
"gb_game": { "tables": { "levels": "data/crawl_levels", "monsters": "data/crawl_monsters", "crawlers": "data/crawl_party" }, "frame": { "cell_w": 420.0, "cell_h": 380.0, "falloff": 0.56, "gloom": 0.82 }, "seed": 20260922}Script commands
Section titled “Script commands”| Call | Does |
|---|---|
Goldbox.new_party(e) | roll a fresh four and forget any run |
Goldbox.restore(e, party) | put back a party a script saved - see below |
Goldbox.enter(e) | go down. A party already in a dungeon stays where it is |
Goldbox.leave(e) | come back up, keeping what it came out with |
Goldbox.walk(e, dx, dy) | a step: dy forward and back, dx sideways. A step into a monster opens a fight |
Goldbox.turn(e, steps) | turn right by that many quarters |
Goldbox.use(e) | use what is in front, or the stairs underfoot |
Goldbox.rest(e) | make camp: heal a little, at the cost of four ticks |
Goldbox.fight_move(e, dx, dy) | step the acting character on the battle grid |
Goldbox.fight_attack(e) | strike whatever it can reach |
Goldbox.fight_approach(e) | one tile along the shortest way to the nearest foe |
Goldbox.fight_guard(e) | stand fast, and end the turn |
Goldbox.fight_flee(e) | run for the edge. The monster stays in the corridor |
Goldbox.fight_ai(e) | take one foe’s turn |
Three of those are easy to leave out and each one costs the game:
gb_fight_ai. Nothing advances a foe’s turn on its own. A game that never calls this opens a fight, hands the first turn to a rat, and waits forever - which is a hung game rather than a lost one. It is one turn per call so a script can pace it and the player can watch the rat cross the grid; calling it every frame is fine, it does nothing on the party’s move.gb_fight_guard. The only action that always ends a turn. A character out of moves with nothing in reach cannot step and cannot strike, so without a pass the fight stops on it.gb_fight_approach.gb_fight_moverefuses a blocked tile without spending a move, so a script that walks toward a foe by comparing coordinates loops forever the first time a body is in the way.
What scripts read
Section titled “What scripts read”One gb_game bag, republished every frame: phase (town, crawling,
fighting, won, lost), party, level, x, y, facing, quads,
battle and events.
quads is the crawler’s own picture, from Crawl::picture - every wall
face, thing and monster, sorted back to front, so a script draws the
list in order and gets depth for free. battle carries side_to_act and
moves_left as well as the fighters, because a game needs both: whether to
accept a keypress at all, and whether it is time to pace a foe’s turn.
events carries this package’s own (battle_started, battle_won,
battle_fled, party_wiped, escaped, crawl), the crawler’s passed
through, and the battle’s tagged "from": "battle" - a crawl and a fight
both have a hit and they carry different fields.
Crossing a scene swap
Section titled “Crossing a scene swap”A package cannot persist itself: data::global() hands packages an
immutable registry, and every write in the package contract goes through
a script or a runtime. So the party crosses a scene swap the way everything
else does - read party out of the published bag, save_set it, and hand
it back to gb_restore on the far side. Once per swap, not per frame.