Systems
github.com/mechanical-lich/ml-rogue-lib/pkg/rlsystems
Turn-based ECS systems. Each system implements MLGE’s ecs.SystemInterface (except CleanUpSystem, which has its own Update method) and is registered with a ecs.SystemManager or called directly each frame.
All systems expose extension hook fields — Go function values you assign at startup to layer game-specific logic on top of the built-in behaviour without subclassing or forking the library.
InitiativeSystem
Ticks entity initiative counters and grants MyTurn when the counter reaches zero, respecting nocturnal/diurnal schedules.
type InitiativeSystem struct {
Speed int
OnEntityTurn func(entity *ecs.Entity)
}
Fields
| Field | Type | Description |
|---|---|---|
Speed | int | Amount subtracted from each entity’s InitiativeComponent.Ticks per frame |
OnEntityTurn | func(entity *ecs.Entity) | Called each time an entity receives MyTurn. Use for per-turn setup: animation reset, UI updates, etc. |
Requires
Initiative
Behaviour
- Decrements
InitiativeComponent.TicksbySpeed. - When
Ticks <= 0, resets toDefaultValue(orOverrideValueif> 0). - Checks sleep schedule:
Nocturnalentities only act whenlevel.IsNight().- All other entities only act when it is not night.
AlertedorNeverSleepentities bypass the schedule entirely.
- Adds
GetMyTurn()to the entity and callsOnEntityTurnif set.
AISystem
Handles WanderAI, HostileAI, and DefensiveAI each turn.
type AISystem struct {
HostileTargetMatch func(self, candidate *ecs.Entity) bool
GetPath func(level rlworld.LevelInterface, from, to rlworld.TileInterface, reuse []path.Pather) []path.Pather
OnWander func(entity *ecs.Entity)
OnHostileAttack func(level rlworld.LevelInterface, attacker, target *ecs.Entity)
}
func NewAISystem() *AISystem
Fields
| Field | Type | Description |
|---|---|---|
HostileTargetMatch | func(self, candidate *ecs.Entity) bool | Returns true if candidate is a valid attack target for self. Default: has Health, not Dead, different faction. |
GetPath | func(level, from, to, reuse) []path.Pather | Pathfinding function used by HostileAI. If nil, hostile entities fall back to direct delta movement. |
OnWander | func(entity *ecs.Entity) | Called after each wander step. |
OnHostileAttack | func(level, attacker, target *ecs.Entity) | Called when a hostile entity lands a hit via rlcombat.Hit. |
Requires
Position, MyTurn
Behaviour
- Calls
rlai.HandleDeath— skips dead entities. - WanderAI: picks a random cardinal direction, calls
rlai.Move, thenrlai.Face. CallsOnWanderif set. - HostileAI: searches for the nearest valid target via
GetClosestEntityMatching. If found, moves toward it (usingGetPathif provided) and callsrlcombat.Hitif adjacent. CallsOnHostileAttackon a successful hit. - DefensiveAI: responds to having been attacked by moving toward the recorded attacker position.
StatusConditionSystem
Ticks decaying status effects and applies their per-turn damage. Also handles Regeneration.
type StatusConditionSystem struct {
ExtraStatuses map[string]ecs.ComponentType
OnStatusEffect func(entity *ecs.Entity, effectName string)
}
Fields
| Field | Type | Description |
|---|---|---|
ExtraStatuses | map[string]ecs.ComponentType | Additional DecayingComponent types to tick alongside the built-ins. Key = effect name. |
OnStatusEffect | func(entity *ecs.Entity, effectName string) | Called for every active status each turn. Built-in damage runs first; use this for sounds, FX, custom damage, etc. |
Requires
Position, MyTurn
Built-in Effects
| Effect | Per-turn behaviour |
|---|---|
"Poisoned" | −1 HP |
"Burning" | −2 HP |
"Alerted" | Decay only — removes the alert state when it expires |
"StatCondition" | Applies StatConditionComponent.Mods on first tick via ConditionModifier.ApplyOnce; reverts on expiry |
"DamageCondition" | Calls DamageConditionComponent.Roll() and routes the result through the standard damage path |
ConditionModifier — automatic apply/revert: status components that implement this interface have ApplyOnce called on the first tick and Revert called immediately before the component is removed. HasteComponent, SlowedComponent, and StatConditionComponent all use this mechanism.
Damage routing: when the entity has a BodyComponent, damage is applied to a random non-amputated body part. If all parts are amputated the entity receives DeadComponent. Entities without a BodyComponent take damage directly to HealthComponent.
Regeneration is handled separately after the status loop: restores RegenerationComponent.Amount HP per turn, capped at MaxHealth. Regeneration also clears the Broken flag on a part once its HP returns above zero.
Registering Speed-Modifying Statuses
HasteComponent and SlowedComponent implement ConditionModifier and must be registered in ExtraStatuses so the system ticks them and calls ApplyOnce/Revert at the right time:
statusSystem := &rlsystems.StatusConditionSystem{
ExtraStatuses: map[string]ecs.ComponentType{
"Haste": rlcomponents.Haste,
"Slowed": rlcomponents.Slowed,
},
}
Adding Fully Custom Statuses
statusSystem := &rlsystems.StatusConditionSystem{
ExtraStatuses: map[string]ecs.ComponentType{
"Frozen": mycomponents.Frozen,
},
OnStatusEffect: func(entity *ecs.Entity, effectName string) {
if effectName == "Frozen" {
// play ice sound, spawn FX entity, etc.
}
},
}
DoorSystem
Updates the visual sprite of Door entities based on open/closed state.
type DoorSystem struct {
OnDoorStateChange func(entity *ecs.Entity, open bool)
AppearanceType ecs.ComponentType
}
Fields
| Field | Type | Description |
|---|---|---|
AppearanceType | ecs.ComponentType | Your game’s Appearance component type. When set, the system calls SetSprite via the AppearanceUpdater interface. |
OnDoorStateChange | func(entity *ecs.Entity, open bool) | Called every tick with the door’s current state. Use for sounds, pathfinding cache invalidation, etc. |
AppearanceUpdater Interface
type AppearanceUpdater interface {
SetSprite(x, y int)
}
Implement this on your Appearance component to let DoorSystem directly set sprite sheet coordinates without depending on a concrete type.
Requires
Door (plus AppearanceType if set)
CleanUpSystem
Removes dead entities and strips MyTurn from all entities at the end of each frame.
type CleanUpSystem struct {
OnEntityDead func(level rlworld.LevelInterface, entity *ecs.Entity)
OnEntityRemoved func(level rlworld.LevelInterface, entity *ecs.Entity)
OnEntityCleanup func(level rlworld.LevelInterface, entity *ecs.Entity)
}
func (s *CleanUpSystem) Update(level rlworld.LevelInterface)
Note:
CleanUpSystemdoes not implementecs.SystemInterface. Callcleanup.Update(level)directly, once per frame, before running the other systems.
Fields
| Field | Type | Description |
|---|---|---|
OnEntityDead | func(level, entity) | Called for each dead entity before removal. Use for loot drops, XP awards, death sounds. |
OnEntityRemoved | func(level, entity) | Called immediately after level.RemoveEntity. Use for secondary cleanup (e.g. custom registries). |
OnEntityCleanup | func(level, entity) | Called for every entity each frame, regardless of death state. |
Behaviour
- Iterates all entities, stripping
MyTurnand callingOnEntityCleanup. - Collects entities carrying
DeadComponentinto a buffer. - For each dead entity: calls
OnEntityDead, skips removal if it is a food entity withAmount > 0, then callslevel.RemoveEntityandOnEntityRemoved. - Repeats steps 2–3 for static entities.
Typical Frame Order
func (g *Game) Update() error {
// 1. Strip MyTurn from last frame; remove newly-dead entities.
g.cleanup.Update(g.level)
// 2. Run systems for all entities (initiative → AI → status → door).
g.systemMgr.UpdateSystemsForEntities(g.level, g.level.GetEntities())
return nil
}