| name | task-points-and-resources |
| description | Authoritative reference for TaskPoint and ResourceHolder mechanics — kind system, requirements graph, expiration modes, collection modes, pawn-resource coupling, and canvas management. Use when designing resource scenarios, adding new TaskPoint behaviour, or reasoning about how pawns satisfy needs. |
Task Points and Resources
Class Hierarchy
Entity
└─ Goal
└─ TaskPoint — interactable resource node on a canvas
Entity
└─ Pawn
└─ ResourceHolder (attached as pawn.resources)
TaskPoint
└─ ResourceHolder (attached as node.resources)
TaskPoint lives in scripts/Entities/TaskPoint.ts.
ResourceHolder lives in scripts/Entities/ResourceHolder.ts.
Resource Kind System
Each TaskPoint has a kind (integer 1–5). Kind determines three things:
| Kind | Color | Requirements | What it produces |
|---|
| 1 | pale green | — | kind 1 |
| 2 | pale blue | — | kind 2 |
| 3 | green | kind 1 | kind 3 |
| 4 | teal | kind 1 + kind 2 | kind 4 |
| 5 | blue | kind 2 | kind 5 |
TaskPoint.requirements(kind) returns the array of required kinds. An empty array means the node is freely collectible.
TaskPoint.canPerformTask(kind, resources) checks whether a ResourceHolder holds non-zero amounts of every required kind.
ResourceHolder
A bounded inventory attached to both Pawn and TaskPoint. Amounts are floats, capped at 20 per kind.
| Method | Effect |
|---|
collectResource(kind) | Adds 0.2 to the named kind's amount (capped at 20). Creates the slot if absent. |
extractResource(kind) | Removes the resource and returns true. Returns false if the slot is empty. |
isResourceEmpty(kind) | true if amount < 0.1 (absent or near-zero). |
hasSufficientResource(kind) | true if amount ≥ 20 (full slot). |
getAmount(kind) | Returns the current float amount, or 0 if the slot is absent. |
setResource(kind, amount) | Directly sets amount; used in initialization. |
Each collectResource call adds 0.2, so a slot goes from empty to full in 100 calls. Each extractResource call removes the full slot at once.
TaskPoint Properties
| Property | Type | Default | Purpose |
|---|
kind | number | set at construction | What the node produces and what it requires |
lifetime | number | 5000 | Remaining lifetime in frames (time-based expiry) |
lifetimeDecay | number | 0 | Frames subtracted from lifetime per frame; read by TimeDecayExpiry |
expiryStrategy | TaskPointExpiry | new TimeDecayExpiry() | Strategy object that controls when and how the node expires (see below) |
maxWorkers | number | 1 | Max simultaneous workers via the BT work path |
workers | Pawn[] | [] | Pawns currently occupying this node via BT work |
active | boolean | — | Whether work is currently succeeding (set by work()) |
occupied | boolean | — | Whether the node is blocked waiting for requirements |
canvas | BaseCanvas | passed in | Back-reference used to call canvas.removeResource(this) on expiry |
Expiration — Strategy Pattern
Expiry behavior is controlled by an expiryStrategy: TaskPointExpiry on each node. TaskPoint.display() calls strategy.onFrame(node) every frame. The canvas calls three neutral hooks on the node — the strategy handles all state internally:
| Hook | Canvas calls | Strategy does |
|---|
node.canCollect(pawn) | Before attempting to collect | Return false to block this pawn this frame |
node.onCollected(pawn) | After a successful collection | Update internal state; expire the node if done |
node.onLeft(pawn) | When pawn leaves pickup radius | Release any lock held for that pawn |
The canvas never inspects which strategy is active.
TimeDecayExpiry (default)
node.expiryStrategy = new TimeDecayExpiry() (set automatically in the constructor).
onFrame decrements node.lifetime by node.lifetimeDecay each frame while canvas.hasStarted. When lifetime ≤ 0 the node removes itself. The typical lifetimeDecay range across boards is 1.2–2.5, giving a lifespan of ~2000–4000 frames at 60 fps.
lifetimeDecay = 0 (the default) means the node never expires on its own.
canCollect always returns true. onCollect and onLeave are no-ops.
CollectStockExpiry
node.expiryStrategy = new CollectStockExpiry().
The node expires once its ResourceHolder stock (keyed to node.kind) is fully drained. Uses the same unit scale as the rest of the codebase: collectResource adds 0.2, extractResource subtracts 0.2, isResourceEmpty triggers at < 0.1, max slot value is 20.
Initialize stock at setup with node.resources.setResource(kind, amount):
| Amount | Collection events before expiry |
|---|
| 0.2 | 1 |
| 0.6 | 3 |
| 1.0 | 5 |
| 2.0 | 10 |
| 20.0 | 100 |
onFrame is a no-op — the node does not decay over time.
canCollect enforces exclusive one-pawn-at-a-time access via a private collectingPawn field inside the strategy: returns false if another pawn holds the lock, or if this pawn already collected this visit. onCollect sets the lock and calls extractResource. onLeave clears the lock when the pawn steps away, making the node available again.
Collection Modes
A — Proximity-Driven (canvas-managed)
Used by CommandMapCanvas and similar boards where pawns do not have BT task-routing to resource nodes.
The canvas implements collectNearbyResources() in its draw() loop. Each frame, for each (pawn, node) pair:
- Compute distance. Check pickup radius:
pawn.diameter * 0.6 + node.r + 2.
- Check requirements: for each required kind,
pawn.resources.getAmount(req) ≥ 0.2.
- If all requirements met:
extractResource each required kind from the pawn, then collectResource(node.kind) into the pawn.
- Apply expiry logic based on the node's expiration mode.
B — BT Work Path
Used by boards where pawns navigate to TaskPoint nodes as explicit tasks (TaskDirection targets).
The pawn's behavior tree calls node.work(pawn) each frame the pawn is at the node. work() extracts required kinds from pawn.resources, deposits them into node.resources, and grants node.kind back to the pawn — if and only if all requirements were extractable. node.workStops(pawn) and node.forceWorkStops(pawn) clean up the worker slot.
These two modes are mutually exclusive per board — a canvas uses one or the other, not both.
Pawn-Resource Coupling (Hunger)
| Pawn property | Purpose |
|---|
pawn.consumes | true to enable hunger. When false the pawn ignores hunger entirely. |
pawn.needs | The resource kind that satisfies hunger (must exist in pawn.resources). |
pawn.maxHunger | Starting and maximum hunger level (frames). |
pawn.hungerMeter | Current hunger. Decremented by hungerRate each frame. |
pawn.hungerRate | Frames of hunger consumed per frame. Scale this inversely with world size. |
The BT eat sequence (HasToEat → Eat → Replenish) fires when hunger falls below a threshold and the pawn holds the required kind in its resources. Pawns that reach 0 hunger die via the die BT node.
Scaling rule: the default world is 800 × 400. For a world N× larger in linear dimension, set hungerRate ≈ defaultRate / N so pawns have time to traverse the map before starving.
Canvas Resource Management
BaseCanvas.removeResource(resource) splices the node from this.canvas.resources.
Subclasses override removeResource to schedule replacements via setTimeout. The replacement strategy is the primary control knob for resource pressure:
| Strategy | Effect |
|---|
| Respawn in viewport | Resources always reachable without navigation; low pressure |
| Respawn outside viewport at fixed distance | Forces navigation; medium pressure |
| Respawn at growing distance from viewport | Pressure increases over time; models scarcity escalation |
No respawn (resourcesFrequency = 0) | Finite supply; terminal depletion possible |
Phase Pattern
Boards that escalate resource pressure over time use a frame-counter phase system:
this.phaseOneAtFrame = this.sketch.frameCount + N;
if (this.currentPhase === 0 && s.frameCount >= this.phaseOneAtFrame) {
this.currentPhase = 1;
this.activatePhaseOne();
}
Each phase transition adjusts lifetimeDecay, charges, spawn location strategy, or pawn needs / hungerRate. Do not change pawn.needs mid-simulation — it confuses the scenario narrative. Establish the need at setup and keep it fixed.
Adding a New Resource Node
Minimum required:
const node = new TaskPoint(sketch, x, y, kind, canvasRef);
node.lifetimeDecay = 1.4;
canvas.resources.push(node);
For stock-based expiry (one pawn at a time, expires after stock is drained):
const node = new TaskPoint(sketch, x, y, kind, canvasRef);
node.expireOnCollect = true;
node.resources.setResource(kind, 0.6);
canvas.resources.push(node);
The node renders itself via node.display() — call this inside the canvas draw() loop, under the same coordinate transform as the pawns.
Requirement Chains in Practice
A two-step chain (kind 1 → kind 3):
- Pawns walk to a kind-1 node. No requirements — freely collectible.
pawn.resources accumulates kind 1.
- Pawns walk to a kind-3 node. Requirement: kind 1 ≥ 0.2. On collect: kind 1 is extracted, kind 3 is deposited.
- If
pawn.needs = 3, the BT eat sequence uses the kind-3 stock to replenish hunger.
Pre-seeding pawn inventory at setup is the standard way to skip step 1 on first contact:
for (let j = 0; j < 50; j++) pawn.resources.collectResource(1);
Without pre-seeding, pawns will starve before they can complete one cycle if kind-1 nodes do not appear until later.