Entity Component System (ECS)
Woodlands uses jecs for enemy entity management. The ECS world is a singleton shared between systems.
World
// shared/ecs/world.ts
import { World } from "@rbxts/jecs";
export const world = new World();
Components
Defined in shared/ecs/components/:
Transform Components
| Component | Type | Purpose |
|---|---|---|
Position | CFrame | World-space transform |
SpawnPosition | CFrame | Original spawn location |
Velocity | Vector3 | Movement vector |
Enemy Components
| Component | Type | Purpose |
|---|---|---|
EnemyType | string | Entity classification |
EnemyState | string | Current AI state (Idle, Chasing, Attacking...) |
StateSince | number | Timestamp of last state change |
CooldownUntil | number | Attack cooldown expiry |
TargetPlayer | Player | Current pursuit target |
ObservedBy | Player[] | Players currently looking at entity |
ChunkBounds | {min, max} | Confinement area |
PatrolNodes | Vector3[] | Patrol waypoints |
PatrolIndex | number | Current patrol position |
IsLethal | boolean | Whether contact kills |
NetworkId | string | Unique ID for network sync |
ModelRef | Model | 3D model reference |
IsHostile | boolean | Hostile vs neutral variant |
PetalColors | Color3[] | Flower trap petal palette |
DownedUntil | number | Lost Quirkymal despawn timer |
Tags
Tags are components with no data — used for classification:
IsEnemy, IsFlowerTrap, IsPathMonster, IsTreeBranchSnatcher,
IsRootSnare, IsNeutralRootSnare, IsGigglingBush,
IsNeutralFlowerTrap, IsWatcher, IsLostQuirkymal, IsMimicEntity,
NeedsPositionBroadcast, NeedsStateBroadcast, NeedsSync
Behavior Systems
All systems run on the server in shared/ecs/systems/server/:
FlowerTrap
Weeping Angel mechanic — can't move while observed.
States: Idle → Arming → Chasing → Attacking → Cooldown
- Stops movement when any player looks at it
- Lunges for 20 damage on contact
- Neutral variant: identical appearance, never attacks
- Drag mechanic: caught players must mash to escape
PathMonster
Line sweep — charges down the path at high speed.
- Spawns ahead or behind player
- Follows global path nodes at 36 studs/sec
- Instant death if player isn't hiding (8-stud kill radius)
- Cannot be outrun
- Despawns at +2 chunks from player
Watcher
Boss entity — hunts off-path players.
- Follows using path nodes
- Direct pursuit when close
- Safe zone check prevents entering
- Despawns at safe zones
TreeBranchSnatcher
Drop attack from above.
- 0.3s charge, 0.5s drop
- 6-stud hit radius
- Returns to perch after attack
RootSnare
Ground trap — hidden roots.
- 3-stud trigger radius
- 0.2s burst animation
- 5s snare duration (immobilize)
- Hostile vs neutral (visible) variants
- Can be jumped over
GigglingBush
50/50 gamble — lethal or harmless.
- 3s giggling warning
- 0.8s attack window
- Random lethal determination per instance
LostQuirkymal
Rescue objective — downed character.
- 30s downed timer before despawn
- Mimic variant attacks rescuers
Network Sync
Server ECS → enemy-network-sync-system → Remotes → Client rendering
- Entities with
NeedsSynctag broadcast position/state changes - Client renders enemy models at synced positions
- 25 MeshPart clones per frame budget
Entity Lifecycle
1. EnemyService.spawnEnemy(type, position, chunkBounds)
2. world.entity() → add components + tags
3. Clone model from prefab → set ModelRef
4. Behavior system runs each frame
5. Network sync broadcasts to clients
6. Despawn: remove entity, destroy model