Blueprint kind region: a pressure plate with first/last occupancy
Complete enter/exit graphs that open a referenced door on first arrival and close it on last departure.
Goal
Open a door while at least one eligible token occupies a region. Enter and exit events occur for each eligible crossing, so compare the trusted event context count to1 on arrival and0 on departure. Do not use a state-change action to control region occupancy.
These JSON files describe the stored Blueprint v2 subdocument. They are development fixtures, not an installable module, a complete Builder project or a campaign archive. Recreate the nodes in the editor, or use the fixtures in isolated contributor tests. This guide does not assume a JSON import button in the Smart Object editor.
Download the complete example bundle or the validator. Every example uses original synthetic data. The host's internal Python evaluator is for source development; browser modules must use the public SDK.
Build it in the editor
Create a native door and a Region rectangle in Map Builder. Name both, enable the region and keep it on the desired project layer.
For the built-in equivalent, choose the region’s Controlled door and Apply pressure plate. This replaces its behavior; use it only when intentionally replacing the existing graph.
For the manual example, open Edit behavior. On area enter: add Event context → Occupancy count, Compare value with number/eq/1 and Check state. Connect the Boolean result to the condition’s Test, overriding its fallback state check.
Connect On area enter to that condition; its True goes to Change state: Opened. Add Element reference for the chosen door and connect its Target output to the state action’s Target.
On area exit, build the same chain with number/eq/0 and Change state: Closed. Bind the same door. The fixture UUID 11111111-1111-4111-8111-111111111111 is synthetic; select an actual owned door in the editor.
Keep Empty and Occupied appearances on inherit. Region artwork and occupancy are not controlled by Change sprite or Change state.
Complete JSON
Download region-pressure-plate.json
{
"version": 2,
"kind": "region",
"states": {
"empty": {
"appearance": "inherit"
},
"occupied": {
"appearance": "inherit"
}
},
"graphs": {
"presence:enter": {
"nodes": [
{
"id": "start",
"type": "event",
"x": 40,
"y": 80,
"event": "presence:enter"
},
{
"id": "count",
"type": "eventContext",
"x": 300,
"y": 300,
"property": "count"
},
{
"id": "matches",
"type": "compareValue",
"x": 560,
"y": 300,
"valueType": "number",
"expected": 1,
"operator": "eq"
},
{
"id": "check",
"type": "condition",
"x": 560,
"y": 80,
"state": "empty"
},
{
"id": "door",
"type": "reference",
"x": 820,
"y": 300,
"targetId": "11111111-1111-4111-8111-111111111111"
},
{
"id": "change",
"type": "state",
"x": 820,
"y": 80,
"targetState": "open"
}
],
"edges": [
{
"from": "start",
"to": "check",
"output": "next",
"kind": "execution"
},
{
"from": "check",
"to": "change",
"output": "true",
"kind": "execution"
},
{
"from": "count",
"to": "matches",
"output": "value",
"kind": "data",
"input": "value"
},
{
"from": "matches",
"to": "check",
"output": "result",
"kind": "data",
"input": "test"
},
{
"from": "door",
"to": "change",
"output": "door",
"kind": "data",
"input": "target"
}
]
},
"presence:exit": {
"nodes": [
{
"id": "start",
"type": "event",
"x": 40,
"y": 80,
"event": "presence:exit"
},
{
"id": "count",
"type": "eventContext",
"x": 300,
"y": 300,
"property": "count"
},
{
"id": "matches",
"type": "compareValue",
"x": 560,
"y": 300,
"valueType": "number",
"expected": 0,
"operator": "eq"
},
{
"id": "check",
"type": "condition",
"x": 560,
"y": 80,
"state": "empty"
},
{
"id": "door",
"type": "reference",
"x": 820,
"y": 300,
"targetId": "11111111-1111-4111-8111-111111111111"
},
{
"id": "change",
"type": "state",
"x": 820,
"y": 80,
"targetState": "closed"
}
],
"edges": [
{
"from": "start",
"to": "check",
"output": "next",
"kind": "execution"
},
{
"from": "check",
"to": "change",
"output": "true",
"kind": "execution"
},
{
"from": "count",
"to": "matches",
"output": "value",
"kind": "data",
"input": "value"
},
{
"from": "matches",
"to": "check",
"output": "result",
"kind": "data",
"input": "test"
},
{
"from": "door",
"to": "change",
"output": "door",
"kind": "data",
"input": "target"
}
]
}
}
}Expected result
| Event | Occupancy count after crossing | Predicted action |
|---|---|---|
| presence:enter | 1 | open referenced door |
| presence:enter | 2 | none |
| presence:exit | 1 | none |
| presence:exit | 0 | close referenced door |
The region’s runtime state is occupied for the first three rows and empty for the final row. Conditions read an event snapshot, not live values changed by preceding actions.
Run the fixture in a development installation
Extract the example bundle to data/blueprint-examples inside a disposable checkout of the documented host. From the checkout root, run:
uv run --locked python data/blueprint-examples/check-examples.py --host .The checker configures an in-memory database, blocks database queries and calls the real clean_blueprint and evaluate_graph functions. It validates all four object kinds, both connection kinds and all 44 node types, including requestRoll and setQuestStatus. It checks both roll visibilities, all six quest statuses and continuing execution without drawing dice or changing journals. This verifies document shape and predicted actions. Test behavior is still needed to check real targets, authorization and the complete server chain.
To inspect this recipe in the development Django shell after extracting the files:
uv run --locked python manage.py shell --no-importsimport json
from pathlib import Path
from gravewright.maps.blueprints import clean_blueprint, evaluate_graph
raw = json.loads(Path("data/blueprint-examples/region-pressure-plate.json").read_text(encoding="utf-8"))
blueprint = clean_blueprint(raw, asset_ids=set(), light_ids=set(), track_ids=set())
result = evaluate_graph(blueprint, "presence:enter", "occupied", event_context={"count": 1, "presence": "enter"})
print(result["trace"])
print(result["actions"])
assert result["actions"][0]["targetState"] == "open"
assert result["actions"][0]["target"] == "11111111-1111-4111-8111-111111111111"Test as GM and player
Run Simulate to inspect one local event. Then use Test behavior to rehearse through the server; its changes are rolled back. Generate or update the scene as GM, then test a synthetic scene with a GM and an authorized player. Verify that a streamer cannot write. After editing the project again, use Update scene before expecting different live behavior.
As GM, move token A in, move B in, move A out and move B out. Verify the four rows and Automation’s actual trigger. Repeat with player-controlled tokens and an unlocked door. With a locked target door, the player’s normal lock restrictions still apply; the source region does not grant extra rights. Enabling/editing the region quietly recomputes occupancy and must not synthesize arrival actions.
Common failures
“Removing one of two tokens closed the door”: compare count to0 on exit; an exit event alone does not mean empty.
“The region never becomes occupied”: check enabled state, token center, elevation and presence filters; blocked or preview-only movement does not commit a crossing.
“The UUID validates but nothing opens”: syntactically valid is not an owned live target. Bind the real door; inspect missing/unsupported targets in Automation.
“I tried state: occupied”: region state is derived. A state node only accepts writable door/lever states and cannot set a region.
How to use AI to adapt this example
Review region-pressure-plate.json against Gravewright 0.3.1. Add an original message only on first entry while preserving last-exit close. Use eventContext count and numeric compareValue; preserve empty/occupied as derived region states. Explain source IDs versus generated native IDs and bind the door through the editor. Return complete JSON and tests with two tokens, elevation filters, a missing target and a player moving toward a locked door. Do not invent an interact port for region.All Blueprint kinds · execution/data connections · All 44 node examples