Gessa Docs
Recipes

Recipe

Collectibles pickups scoring and win condition

The player gathers items, a counter goes up, and the level ends when they are all collected. There is no CollectibleComponent, CollectorComponent, or SpawnPoolComponent (all stripped). This is the canonical AI-authored collect-count-win loop, mirrored by the engine's own shooter-collector-v2 eval case.
engine v1.0.234since script-semantic-patch-ops.v1, action-catalog.v1.0.232, persistence.v1, world.v1, match.v1Copy for LLM

Use this for

coins, gems, pickups; counting a score; despawning collected items; winning when all targets are gone

Not for

durable cross-session save files only (see saves-and-persistence); anything expecting a CollectibleComponent or CollectorComponent

Pairs with: World build document authoring compile apply loop, Game loop assembly entities scripts HUD win shippable loop, UI HUD panels widgets and world nameplates, Saves persistence player progress and data stores, Physics colliders rigid bodies triggers and joints

The one-document path (default): batch + collectible cluster

Do not create the pickups one call at a time, and do not hand-build the loop. ONE world build document (see authoring-with-the-world-build-document) carries BOTH: an entity.primitive_batch.create whose payload.items place one body per coin (write the required exact key and the query tags on each item), and a gameplay.collectible_cluster.create with tag, entityKeys, scriptKey, score, victory. The cluster composes the whole proven loop: the onTriggerEnter collect handler (despawn + score increment), the onTick tag scan with the won flag and win condition, the HUD wire when the document carries a panel, the ScriptComponent attached to EVERY entity in entityKeys, the static trigger collider completed onto every collectible body the same document creates (never add RigidBody to a static sensor), and the referenced typed-state store auto-created. The sections below teach the same loop as script-patch ops - use them to UNDERSTAND or to repair/extend a loop, not to rebuild what the cluster already landed.

The scoring recipe (the script pattern)

Author with project_apply_script_semantic_patch using the ai_safe op pack. Ops inside a handler take scope: { "kind":"handler", "handlerRef": <key> }.

  1. Tag every collectible entity (a tag on the entity, for example coin) so a query can find them, and give each a trigger ColliderComponent (collision:"trigger") so contact is detectable. The document already does both when you author batch items with tags and a collectible cluster with entityKeys; hand-attach them (one atomic project_apply_transaction) only for entities the document did not create.
  2. Seed the score: writeTypedStateValue { "field":"score", "valueType":"number", "value": 0 } (storeKey defaults progress, recordKey player).
  3. On pickup, react with addTriggerHandler { "handlerKey":"onCollect" }. Inside it:
    • despawnEntity { "scope":{...}, "reason":"collected" } removes the item.
    • incrementNumericState { "scope":{...}, "field":"score", "by": 1 } adds to the score.
  4. To evaluate a win each tick, add addTickHandler { "handlerKey":"onTick" } and inside it:
    • findEntitiesByTag { "scope":{...}, "tag":"coin", "assignTo":"coins", "limit": 64 } collects the remaining targets.
    • compareRemainingCount { "scope":{...}, "arrayRef":"coins", "operator":"lte", "count":0, "assignTo":"allGone" }, or go straight to
    • declareWinCondition { "scope":{...}, "arrayRef":"coins", "remainingCount":0, "phaseKey":"won", "reason":"all coins collected" } which transitions the match phase when the count reaches the threshold.
  5. Show the score with wireHudStateBinding (see ui-hud-panels-and-widgets) or a TextRenderableComponent bound to the stat.

Pitfalls

  • findEntitiesByTag limit caps at 1000; forEachEntityInResult maxIterations caps at 1000. Size them to your item count.
  • forEachEntityInResult.body accepts only the loop-intent ops (despawnEntity, emitCustomEvent, incrementNumericState, attachComponentWithValidatedDefaults); it is not arbitrary code.
  • declareWinCondition carries writeState/emitEvent/requiresAuthority; it runs on the authority. Do not also flip the phase client-side.
  • The one-shot addShooterCollectorLoop macro is NOT AI-expressible; build from the primitives above.
  • The document places the bodies AND can carry the tag, the trigger collider (component.put operations), and the behavior patch in the same apply; per mutation calls are the precision tier for targeted repairs, not for bulk placement or the initial loop.

Verify

  • simulation_run (qa.run.start) to prove the score rises and the win phase fires when the last item is collected.
  • world_playtest_scenario to PLAY it: an after state with the coins gone and the score raised is the proof; an unchanged digest means the loop did not run.
  • project_get_graph_snapshot to confirm the tick and trigger handlers landed.
Was this helpful?Report an issueContact support

On this page