refactor(rust): one placing verb per kind — rust.crate.place and rust.npc.place (D97)

Core learns which caps an action accepts by pricing its declared examples
once, and drops a dimension priced at zero. A single rust.prefab.place whose
cost moved between rust.prefabs and rust.npcs by its prefab param could only
ever show the crates cap, so D89's separate dial for fights was unreachable.

Two verbs, each pricing exactly one dimension, with the prefab source split
to match (rust.options.crates / rust.options.npcs). The switchboard can now
allow crates and leave NPCs off. The plugin's world.place is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-24 02:09:49 -05:00
parent a3bcec9cde
commit fab31f23e8
3 changed files with 144 additions and 110 deletions

View File

@@ -153,19 +153,19 @@ test('the world verbs are registered, and every budget has a verb that spends it
const actions = api.record.eventActions
const budgets = api.record.eventBudgets
assert.deepStrictEqual(actions.map((a) => a.id).sort(), ['rust.prefab.place', 'rust.zone.open'])
assert.deepStrictEqual(actions.map((a) => a.id).sort(), ['rust.crate.place', 'rust.npc.place', 'rust.zone.open'])
assert.deepStrictEqual(budgets.map((b) => b.id).sort(), ['rust.npcs', 'rust.prefabs', 'rust.zone.minutes'])
// D79/D89: no dimension without a verb that spends it. A crate step and an
// NPC step are priced on different dials, and a zone on its minutes.
// D79/D89/D97: every dimension has a verb that spends it, and each verb spends
// exactly ONE — priced from its own declared examples, which is how core
// decides which cap boxes the switchboard shows. A verb whose dimension moved
// with its params would hide the other dial from every operator.
const spent = new Set()
const place = actions.find((a) => a.id === 'rust.prefab.place')
for (const cost of [
place.cost({ prefab: 'crate.elite', count: 3 }),
place.cost({ prefab: 'npc.scientist', count: 2 }),
actions.find((a) => a.id === 'rust.zone.open').cost({ minutes: 90 }),
]) {
for (const id of Object.keys(cost)) spent.add(id)
for (const a of actions) {
const example = Object.fromEntries(a.params.map((p) => [p.name, p.example]))
const dims = Object.keys(a.cost(example)).filter((id) => a.cost(example)[id] > 0)
assert.strictEqual(dims.length, 1, `${a.id} prices ${dims.join(', ')}`)
spent.add(dims[0])
}
assert.deepStrictEqual([...spent].sort(), budgets.map((b) => b.id).sort())