In 2000, Looking Glass Studios shipped Thief II: The Metal Age, built on the Dark Engine — an object system where every entity is an integer ID with typed properties, directed links, and a message protocol. The trap/trigger system, documented in a technical design document by Marc LeBlanc, is one of the most elegant data-driven puzzle architectures ever shipped in a game engine. This post traces its adaptation into CatGame's ECS.
The Dark Engine Object Model
The Dark Engine's core is deceptively simple. Every game entity is an object: a unique integer ID with a minimal footprint. Behavior is assembled from three primitives:
The trap system lives under the fnord → TrapTrig branch of the object hierarchy. fnord objects are invisible logic nodes — zero-cost abstract nodes that designers litter throughout a level without performance fear. This is the key insight: logic is cheap, rendering is expensive.
CatGame's ECS Translation
CatGame uses a C++20 ECS architecture with EnTT. The mapping from Dark Engine concepts to ECS is direct:
| Dark Engine | CatGame ECS | Status |
|---|---|---|
| Object (integer ID) | entt::entity | ✅ Done |
| Properties (typed data bags) | ECS components | ✅ Done |
| Links (directed, flavored) | LinkComponent + LinkFlavor | ✅ Done |
| ControlDevice flavor | LinkFlavor::ControlDevice | ✅ Done |
| TurnOn / TurnOff messages | ControlDeviceSignalEvent | ✅ Done |
| TrapTrig (invisible logic node) | Receiver entities + ControlDeviceReceiverComponent | ✅ Done |
| RequireAll / RequireAny | require_all field | ✅ Done |
| Inverter | invert field | ✅ Done |
| Signal depth limit | kMaxSignalDepth = 8 | ✅ Done |
| Cycle detection | visited vector | ✅ Done |
| TOML level definitions | [[links]] + [entities.name] | ✅ Done |
The Link Graph: Wiring the World
In Dromed, designers wire the world by creating ControlDevice links between objects. A button links to a door. A pressure plate links to an emitter trap. A relay trap links to multiple targets with ordered, delayed signals. The link graph is the puzzle logic.
CatGame's LinkSystem stores links as entities with a LinkComponent holding from, to, flavor, and a LinkData struct with signal, order, delay, one_shot, and consume_source fields. The ControlDeviceSystem dispatches signals along links, respecting ordering and delay, with a priority queue keyed by (fire_time, order, link_entity).
Trap-Control Flags (Phase B)
Dromed receivers support per-receiver flags that control signal filtering. CatGame implements all five:
| Flag | Behavior | Status |
|---|---|---|
Once | Fire only once, then ignore further inputs | ✅ Implemented |
NoOff | Ignore TurnOff signals (latch on) | ✅ Implemented |
OffFilter | Only react to TurnOff, not TurnOn | ✅ Implemented |
Revert | Auto-revert to false after revert_delay seconds | ✅ Implemented |
Direction | GoForward / GoReverse signal direction | ❌ Not yet |
Specialized Trap Archetypes (Phases C–D)
Dromed's TrapTrig hierarchy includes specialized archetypes that CatGame implements as ECS components:
Act/React: The Stimulus System (Phase E)
The Act/React system is Dromed's continuous-signal layer, orthogonal to the discrete TurnOn/TurnOff protocol. An object can have Sources (stimulus + propagator + intensity) and Receptrons (stimulus listener + threshold + effect). When a source's stimulus reaches a receptron at sufficient intensity, the receptron fires its effect.
Propagators
Four propagator types control how stimuli travel from source to receptron:
| Propagator | Mechanism | Status |
|---|---|---|
| Contact | Stimulus fires on collision/overlap between source and target | ✅ Implemented |
| Radius | Spherical propagation; intensity may attenuate with distance. Uses SpatialHashGrid for broad-phase culling. | ✅ Implemented |
| Flow | Stimulus flows along ControlDevice links from source to directly linked targets | ✅ Implemented |
| Script | Direct application to all matching receptrons regardless of spatial proximity | ✅ Implemented |
Receptron Effects
| Effect | Behavior | Status |
|---|---|---|
| Damage | Applies damage_amount * dt to HealthComponent. Sets is_dead at 0 health. | ✅ Implemented |
| Destroy | Removes the entity from the registry | ✅ Implemented |
| SendMessage | Dispatches a ControlDeviceSignalEvent with the configured message | ✅ Implemented |
| AddMetaproperty | Adds a metaproperty to the entity (behavior modification) | ❌ Not yet |
The StimTrap Bridge
The StimTrapComponent bridges the discrete ControlDevice protocol and the continuous Act/React system. When a receiver with a StimTrap gets TurnOn, it activates a StimulusSourceComponent with the configured stimulus type and intensity. On TurnOff, it either deactivates the source or emits a brief negative-intensity pulse if negative_intensity > 0.
Designer Composition Patterns (Phase F)
The TDD describes six standard composition patterns. CatGame implements and tests all of them in a fixture level (trap_fixture.toml) with 9 integration tests:
Pattern 1: Basic ControlDevice link. The simplest composition.
Pattern 2: Physics volume triggers projectile spawn.
Pattern 3: Complex logic with ordered, delayed relay dispatch.
Pattern 4: One-shot collapsing floor.
Pattern 5: Two-state toggle from a single input.
Pattern 6: Discrete signal → continuous stimulus → effect.
TOML Level Definition
CatGame levels are TOML files. The trap/trigger system is fully data-driven — designers wire logic by editing TOML, not C++:
Parity with the TDD
The Dromed TDD (by Marc LeBlanc) defines the full trap/trigger architecture. Here's where CatGame stands against it:
| TDD Feature | CatGame Status |
|---|---|
| Object = integer ID | ✅ EnTT entity |
| Properties (typed data bags) | ✅ ECS components |
| Links (directed, flavored, data-carrying) | ✅ LinkComponent |
| ControlDevice link flavor | ✅ LinkFlavor::ControlDevice |
| TurnOn / TurnOff message protocol | ✅ ControlDeviceSignalEvent |
| Trap-control flags (Once, NoOff, OffFilter, Revert) | ✅ 4 of 5 (Direction pending) |
| RelayTrap | ✅ RelayTrapComponent |
| DestroyTrap | ✅ DestroyTrapComponent |
| FlipFlopTrap | ✅ FlipFlopTrapComponent |
| EmitterTrap (projectile spawn) | ✅ EmitterTrapComponent + ProjectileComponent |
| Act/React Sources | ✅ StimulusSourceComponent |
| Act/React Receptrons | ✅ ReceptronComponent |
| StimTrap bridge | ✅ StimTrapComponent |
| Propagators (Contact, Radius, Flow, Script) | ✅ All 4 implemented |
| Receptron effects (Damage, Destroy, SendMessage) | ✅ 3 of 4 (AddMetaproperty pending) |
| Link ordering + delay | ✅ LinkData::order + delay |
| Signal depth limit + cycle detection | ✅ kMaxSignalDepth=8 + visited vector |
| Designer composition patterns | ✅ 7 patterns tested in trap_fixture.toml |
| Direction (GoForward / GoReverse) | ❌ Not yet |
| AddMetaproperty receptron effect | ❌ Not yet |
| Physics-volume triggers (BoundsTrigger) | ❌ Not yet (physics disabled in PnC) |
| RequireOne / N-of-M gating | ❌ Not yet |
| RandomRelay (random destination) | ❌ Not yet |
| QuestVar traps | ❌ Not yet |
| TeleportTrap | ❌ Not yet |
| TimedStimulator / TrapTimer | ❌ Not yet |
| Metaproperty add/remove (A-links / R-links) | ❌ Not yet |
Test Coverage
Related Pages
- ECS Architecture — the EnTT foundation this system is built on.
- The Anatomy of Point-and-Click Games — the puzzle dependency graph foundation.
- PnC Continuum Expertise — how point-and-click expertise scales across the project.
Glossary
- ControlDevice
- The primary link flavor in Dromed. When a source object fires TurnOn/TurnOff, the signal travels along all ControlDevice links from that object to their targets.
- Fnord
- The Dromed category for invisible logic objects. TrapTrig, RelayTrap, and other logic nodes live under fnord in the object hierarchy. They have no renderable model and minimal overhead.
- Receptron
- A listener for a specific stimulus type. When the stimulus reaches the receptron at sufficient intensity (above threshold), the receptron fires its configured effect.
- Stimulus Source
- A continuous signal emitter. Defined by stimulus type, propagator (Contact/Radius/Flow/Script), intensity, radius, and lifecycle.
- Trap-Control Flags
- Per-receiver flags that filter incoming signals: Once (fire once only), NoOff (ignore TurnOff), OffFilter (only react to TurnOff), Revert (auto-revert after delay), Direction (signal direction control).
- StimTrap
- A bridge component that converts discrete TurnOn/TurnOff signals into continuous stimulus source activation/deactivation.
- Thief II screenshots are copyrighted by Looking Glass Studios / Eidos Interactive. All diagrams in this post are original SVG illustrations created by the CatGame team, not game assets.
- The Dromed TDD is an internal Looking Glass Studios document by Marc LeBlanc. This post summarizes its architecture and describes CatGame's independent implementation. No proprietary code or assets are reproduced.
- Dark Engine vs Dark Engine (game): "Dark Engine" refers to Looking Glass Studios' game engine (Thief: The Dark Project, Thief II, System Shock 2). It is unrelated to "dark energy" in physics or any same-named concept in other domains.
- Implementation status: Features marked ✅ are implemented and tested. Features marked ❌ are roadmap items not yet shipped. See the parity table above for the full breakdown.
- Act/React intensity attenuation: The TDD describes quadratic and biquadratic falloff for Radius propagation. CatGame currently uses a simple distance check without attenuation. Falloff curves are a roadmap item.
- SpatialHashGrid: The broad-phase grid uses a default cell size of 16.0 units. This is configurable but not yet exposed in TOML.
References: Dromesday Book — Sources & Receptron Basics · Thief Editors' Guild — S&R Guide · TTLG Forums — S&R System Info
