Skip to main content

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

ComponentTypePurpose
PositionCFrameWorld-space transform
SpawnPositionCFrameOriginal spawn location
VelocityVector3Movement vector

Enemy Components

ComponentTypePurpose
EnemyTypestringEntity classification
EnemyStatestringCurrent AI state (Idle, Chasing, Attacking...)
StateSincenumberTimestamp of last state change
CooldownUntilnumberAttack cooldown expiry
TargetPlayerPlayerCurrent pursuit target
ObservedByPlayer[]Players currently looking at entity
ChunkBounds{min, max}Confinement area
PatrolNodesVector3[]Patrol waypoints
PatrolIndexnumberCurrent patrol position
IsLethalbooleanWhether contact kills
NetworkIdstringUnique ID for network sync
ModelRefModel3D model reference
IsHostilebooleanHostile vs neutral variant
PetalColorsColor3[]Flower trap petal palette
DownedUntilnumberLost 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 NeedsSync tag 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