Free-Field Generation (RE-measured)

A PSZ free field is not a hand-drawn dungeon and it is not the invented 5×5 sketch the old generator produced. It is a small, gated tree of reused room tiles whose shape, gates, keys, and enemy tables were measured out of the original ROM by the psz-re project. This page is the normative contract for reproducing one to parity. The reference observations live in data/re_reference/; the Valley field (data/field_quests/valley_field.json) is the first built to it.

The room vocabulary

Every stage ships the same room codes; the first letter of the code encodes the room's door count and shape — this is confirmed by the game's own name-format table (psz-re FUN_0206D2E4):

A field's variant letter is its area role: a/b are the two explorable areas, e the single-room transition between them, z the boss arena. So a full field is four sections — grid a → transition e → grid b → boss z.

The layout is always a tree

The generator MUST satisfy these invariants (measured across four fields, 36 rooms; tested in test_field_layout.gd):

The goal room ends the section

Every a/b section has exactly one goal room — the set's ga1 — and it is the section's terminal: a 1-door leaf whose only door is the connection it is entered through. The section exit lives inside the room, as an in-room warp pad standing where the original authors its warp object — never as a doorway warp bolted onto whatever room the path happened to finish in. A generator MUST reserve ga1 for this role and never roll it as an ordinary mid-path room.

Rotation is derived, not stored

The game does not store a room's rotation. A port MUST derive it: rotate the room's door set until it covers the directions the layout needs at that cell — there is exactly one 90° multiple that fits (psz-re "Rooms are rotated, and the rotation is derived"). In this engine the room's authored config portal directions are the door set, and StageRotation.rotate_dir is a logical label remap (never a mesh rotation — see Field Stage Lifecycle and issue #218).

Gates and keys

/states/field-gates is normative for the gate rule — the four attribute values, the key-gate budget, how one key versus two is chosen, the per-room enemy-defeat roll, where keys are scattered, and the solvability invariant. What follows here is the captured Valley layout: a transcribed instance of that rule, not the rule itself. Where the two appear to disagree, field-gates wins and this page is the example.

Each outward edge (away from the start) carries a gate value; the way-back edge is always open:

A field places exactly as many keys as its gates require, on cells reachable before the gate — so a layout is always solvable without backtracking to an unreachable room. Valley: a 2-key gate B1 → C1, with both keys on B2. This balance is a tested invariant.

Two psz-godot divergences from the captured instance, both recorded on the normative page: generated fields offer the scatter pool's dead-end rooms first (#639), and a key drops when its room is cleared rather than standing on the ground at load — the authored slots still decide where it stands (Key Placement). In the captured Valley above the keys sit on B2 the moment the room draws; in a generated field they appear when B2's last enemy dies.

Enemy population

Spawns are drawn from the measured wave tables, not authored by hand. Each room code has an allowed list of wave templates with selection weights (psz-re enemy_room_assignment); each template is a fixed set of enemy ids (enemy_wave_templates). The start and goal rooms carry no wave and spawn nothing. The exact per-instance roll (which allowed template a room draws) is not yet decoded, so the builder draws deterministically against the measured weights — preserving the "mostly ordinary fights, occasional rare" texture rather than making every instance identical. A room runs 1–3 waves, each an independent draw from this pool; the wave count and its trigger timing are governed by Enemy Waves.

psz-re internal enemy names map to this engine's ids: snake→garapython, vulture→vulkure, lion→helion, lion_rare→blaze_helion, hyena→grimble, lizard→ghowl, rappy→rappy.

Objects and traps are AUTHORED, not generated

Boxes, walls and traps are not scattered by a placement pass. Every one is authored per room in the original's set/<NN>/<v>/<room>/*.rel, with a room-local (x, y, z) in the same frame as room_doorways.json and a 16-bit facing. The table is data/re_reference/room_objects.json (290 rooms — every cell of every field quest resolves), imported by scripts/tools/refield/import_re_objects.py. A port MUST place them as authored; it MUST NOT ring-scatter or raycast-and-snap.

A set file is six groups, and which of them a room instance builds is the one part that is rolled:

Every draw goes through the generator's seeded RNG, so a seed reproduces a field exactly. Authored walls are in the table and are deliberately not placed — see the approximation list below.

Traps

The original has five field trap types. Four are contact traps that share one behaviour and differ only in damage; the fifth — the elemental family — is its own thing and is the interesting one.

FamilyIdsBehaviour
Contact needler_trap (25 dmg), burn_trap (50), gun_trap, capture_trap Visible floor furniture. Damage on contact; capture holds the player. The 25 and 50 are measured constants from the original's parameter blocks.
Elemental heal_trap, heat_trap, light_trap, ice_trap Invisible, proximity-armed, fused — see below.

The elemental field trap and the player's trap item are the same object. psz-re decoded the original's class: factory index 19 / type 0x1C13, and the only thing separating the two families is one byte in the object's parameter block. This engine models that with TrapBall.field_placed rather than a second script.

An authored elemental trap MUST:

A player-placed trap of the same element is always visible to its owner, arms on a timer, waits for a valid target, and then runs its own measured fuse — 150 frames for heal, 75 for the rest.

Seeing a dormant trap

A dormant elemental trap MUST be drawn when either holds:

Trap Vision's duration is not measured — psz-re found no such timer anywhere in the trap class, which also means the reveal is a renderer-side concern in the original and carries no gameplay coupling.

The boss room

The z section is a single arena. Valley's is s01z_na1, and the assignment table's sole s01z entry is boss_dragon — so Valley's boss is Reyburn. See Boss Behavior. Clearing the boss spawns the return-to-city warp (the existing type: "boss" section handling).

What is deliberately approximated