---
title: "Collectibles pickups scoring and win condition"
description: "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."
engineVersion: v1.0.234
date: 2026-09-28
license: "(c) Gessa, proprietary. Cite with attribution to https://gessa.ai/docs/. Terms: https://gessa.ai/terms/."
canonical: https://gessa.ai/docs/knowledge/playbooks/collectibles-and-scoring/
---

# Collectibles pickups scoring and win condition

## When to use

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.

## 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.
