feat(events): the five world verbs an author sees (Phase 12a)
`uo.creature.spawn`, `uo.boss.spawn`, `uo.npc.place`, `uo.gate.open` and `uo.decor.place`, over protocol 7's one command family. Five actions because five is what an author has; one `perform`/`revert`/`reconcile` because on the wire they are one thing. Five new budget dimensions -- `uo.creatures`, `uo.bosses`, `uo.npcs`, `uo.decor`, `uo.gate.minutes` -- all declared by THIS MODULE (org lead, 2026-09-07). Core meters whatever dimensions a module declares and holds no UO knowledge, which is the whole of what MODULE_API means by game-agnostic. A gate is priced in minutes rather than in gates: one standing all day and twelve standing five minutes each are not the same imposition on a world. `reconcile()` ASKS the shard, and is the one place in this file that must not use `reconcileByBootId`. A crier line lives in shard memory, so a changed `bootId` IS proof it is gone; a spawned creature is in the world SAVE and survives the restart the stamp would report it lost by. Anything `world.owned` does not list is gone -- safe only because the shard's registry and the objects it describes are written by the same save. Teardown reports `gone` as success and `refused` as failed. A creature a player killed is the point of having spawned it, and a run that ended `incomplete` because its event worked would be a report nobody could read. `refused` means the shard denies this run ever owned the serial, so nothing will delete it through this path and the row must land unresolved with a reason. The atlas gains a decoration index, parsed from the shard's own `Data/Decoration/**/*.cfg` -- 120 files, read RECURSIVELY because the real tree nests two deep and a flat read would index a fraction of it while looking like it worked. 313 distinct types. The decor verb resolves through it rather than passing a type name through, which keeps the verb to this shard's own decoration vocabulary AND fetches the item id: `Static` alone accounts for 5031 placements under 1992 different graphics, so a bare type name places the wrong thing. `PARSER_VERSION` -> 3, so an already-imported tree is re-read. Two things the build found in code that had already shipped: `uo.options.creatures` answered with the atlas SLUG -- unique, stable, and not something the shard can build, because a creature is constructed from a ServUO class name and `orc-brute` is not one. The atlas's `name` is the raw type token from the spawn files, so the fix was to stop discarding the half that works. Safe to change because Phase 12a is the source's first consumer; the file said so when it shipped. `uo.npc.place` could not be performed from its own required params. Both ends refuse an oracle with neither a greeting nor a line, but both fields were optional -- so a cross-field rule sat where no authoring form could render it. The greeting is now `required`, which says the same thing in the contract itself. Caught by the existing dry-run sweep, which is a better argument for that test than anything written about it when it shipped. 605 tests pass. `swagger-fragment.json` is stale on `edge` already and this phase adds no route, so it is left alone. Refs: docs/link/v7.md, docs/website/EVENTS_PLAN.md Phase 12a Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
@@ -14,6 +14,7 @@ const {
|
||||
buildFacetIndex,
|
||||
resolveFacetName,
|
||||
slugify,
|
||||
parseDecoration,
|
||||
decodeEntities,
|
||||
} = require('../utils/spawnAtlasParse')
|
||||
|
||||
@@ -599,3 +600,49 @@ test('parsePoints: DelayInSec decides the unit, and both come out in seconds', (
|
||||
assert.equal(seconds.minDelay, 5)
|
||||
assert.equal(seconds.maxDelay, 10)
|
||||
})
|
||||
|
||||
// ── parseDecoration (Phase 12a) ───────────────────────────────
|
||||
|
||||
test('parseDecoration: reads the type off each header and ignores the placements', () => {
|
||||
const rows = parseDecoration(`# switch
|
||||
Static 0x108F
|
||||
5552 1864 11
|
||||
5399 1875 17
|
||||
|
||||
# crate
|
||||
LargeCrate 0x0E3C
|
||||
5408 607 45
|
||||
`)
|
||||
assert.deepEqual(rows, [
|
||||
{ type: 'Static', itemId: 0x108f },
|
||||
{ type: 'LargeCrate', itemId: 0x0e3c },
|
||||
])
|
||||
})
|
||||
|
||||
test('parseDecoration: a parenthesised property list is not part of the type', () => {
|
||||
// These are the shard's own decoration details — which way a door faces, what
|
||||
// hue a banner is — and an event author is choosing neither. Only the class
|
||||
// name is, because that is what the plugin constructs from.
|
||||
assert.deepEqual(parseDecoration('AnkhNorth 0x0004 (Hue=0x47E)'), [
|
||||
{ type: 'AnkhNorth', itemId: 4 },
|
||||
])
|
||||
assert.deepEqual(parseDecoration('ArmsAndWeaponsPrimer 0x0FEF (Name=a life of travel)'), [
|
||||
{ type: 'ArmsAndWeaponsPrimer', itemId: 0x0fef },
|
||||
])
|
||||
})
|
||||
|
||||
test('parseDecoration: a negative z on a placement line is not mistaken for a type', () => {
|
||||
// The real trap in this format: a coordinate line starts with a digit OR a
|
||||
// minus, so "not a comment" is not the test. A z of -12 is ordinary in every
|
||||
// dungeon file in the tree.
|
||||
assert.deepEqual(parseDecoration(`Static 0x07A4
|
||||
5558 1826 -12
|
||||
-5 -5 -5
|
||||
`), [{ type: 'Static', itemId: 0x07a4 }])
|
||||
})
|
||||
|
||||
test('parseDecoration: empty, comment-only and absent input all yield nothing', () => {
|
||||
assert.deepEqual(parseDecoration(''), [])
|
||||
assert.deepEqual(parseDecoration(null), [])
|
||||
assert.deepEqual(parseDecoration('# nothing but a comment\n\n'), [])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user