aboutsummaryrefslogtreecommitdiff
path: root/internal/world
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-09 22:15:54 -0400
committerhistoria <[not public]>2026-07-09 22:15:54 -0400
commitecba7f726f70b37126d852c38c7e3eec7b04d730 (patch)
tree9129215c6e5015336fde1116395336d82bb952d1 /internal/world
parentb3d4c616f59ad2519f3a0b77e3b47d6571cd2486 (diff)
downloadthehouseoficarus-ecba7f726f70b37126d852c38c7e3eec7b04d730.tar.gz
feat: old standalone trigger systems completely unified into trigger->condition->action system
Diffstat (limited to 'internal/world')
-rw-r--r--internal/world/grid.go10
-rw-r--r--internal/world/insert_remove_test.go2
-rw-r--r--internal/world/mob.go22
-rw-r--r--internal/world/room.go64
-rw-r--r--internal/world/room_migrate.go162
-rw-r--r--internal/world/room_migrate_test.go104
-rw-r--r--internal/world/trigger.go16
-rw-r--r--internal/world/trigger_store.go330
8 files changed, 247 insertions, 463 deletions
diff --git a/internal/world/grid.go b/internal/world/grid.go
index 7be8eed..ac80476 100644
--- a/internal/world/grid.go
+++ b/internal/world/grid.go
@@ -87,11 +87,11 @@ func BuildGrid(seed int, load func(int) (*Room, bool), include func(int) bool, e
continue
}
- g.Coord[target] = want
- g.RoomAt[want] = target
- g.Dist[target] = g.Dist[rid] + 1
- queue = append(queue, target)
- }
+ g.Coord[target] = want
+ g.RoomAt[want] = target
+ g.Dist[target] = g.Dist[rid] + 1
+ queue = append(queue, target)
+ }
}
return g
diff --git a/internal/world/insert_remove_test.go b/internal/world/insert_remove_test.go
index 15d752e..201b0d4 100644
--- a/internal/world/insert_remove_test.go
+++ b/internal/world/insert_remove_test.go
@@ -95,8 +95,10 @@ func TestInsertGridConflictsOverlap(t *testing.T) {
// TestInsertGridConflictsTwist: room 3 beyond 2 is also reachable from 1 via a
// separate path, so after the push 3 would have two candidate positions.
+//
// 1 east→2 east→3 (3 at (2,0,0), pushed to (3,0,0))
// 1 south→4 east→5 north→3 (3 at (1,0,0))
+//
// 3 is reachable two ways already → the pre-edit world is itself a twist. The
// insert helper must surface a twist conflict.
func TestInsertGridConflictsTwist(t *testing.T) {
diff --git a/internal/world/mob.go b/internal/world/mob.go
index 45b0fb8..fa0cdbf 100644
--- a/internal/world/mob.go
+++ b/internal/world/mob.go
@@ -78,13 +78,13 @@ type MobCombat struct {
}
type MobDef struct {
- ID string `yaml:"id"`
- Name string `yaml:"name"`
- Description string `yaml:"description"`
- IdleDescriptions []string `yaml:"idle_descriptions"`
- Protected bool `yaml:"protected"`
- Unique bool `yaml:"unique"`
- Drops MobDropTable `yaml:"drops"`
+ ID string `yaml:"id"`
+ Name string `yaml:"name"`
+ Description string `yaml:"description"`
+ IdleDescriptions []string `yaml:"idle_descriptions"`
+ Protected bool `yaml:"protected"`
+ Unique bool `yaml:"unique"`
+ Drops MobDropTable `yaml:"drops"`
Steal *MobSteal `yaml:"steal,omitempty"`
Task *MobTask `yaml:"task,omitempty"`
@@ -94,7 +94,7 @@ type MobDef struct {
Talk *behavior.TalkConfig `yaml:"talk,omitempty"`
Shop *behavior.ShopConfig `yaml:"shop,omitempty"`
- OnKill []behavior.Interaction `yaml:"on_kill,omitempty"`
+ OnKill []behavior.Trigger `yaml:"on_kill,omitempty"`
}
func (d *MobDef) IsTalkable() bool { return d.Talk != nil }
@@ -132,7 +132,7 @@ type MobInstance struct {
AttackBonus int
StrengthBonus int
- AttackTypes []string
+ AttackTypes []string
RangedBonus int
ScienceBonus int
@@ -172,7 +172,7 @@ type MobInstance struct {
// HP draining to zero (the "completion" of the work). The first entry
// whose item filter, Condition, and the first-match-wins rule all pass
// fires.
- OnKill []behavior.Interaction
+ OnKill []behavior.Trigger
// Shop is the (shared, read-only) shop config from the def. Per-instance
// live stock is tracked in ShopStock; ShopRestock holds per-item countdown
@@ -313,7 +313,7 @@ func NewMobInstance(def *MobDef, instanceID string, roomID int, wanderRooms []in
Drops: def.Drops,
AttackBonus: attackBonus,
StrengthBonus: strengthBonus,
- AttackTypes: attackType,
+ AttackTypes: attackType,
RangedBonus: rangedBonus,
ScienceBonus: scienceBonus,
SciencePercentBonus: sciencePercentBonus,
diff --git a/internal/world/room.go b/internal/world/room.go
index 8ae2d13..bc115f8 100644
--- a/internal/world/room.go
+++ b/internal/world/room.go
@@ -75,27 +75,29 @@ type SpawnDef struct {
}
type ExitDef struct {
- Room int `yaml:"room"`
- Condition *behavior.Condition `yaml:"condition,omitempty"`
- BlockedMessage string `yaml:"blocked_message,omitempty"`
- OnTraverse []behavior.Interaction `yaml:"on_traverse,omitempty"`
- Hidden bool `yaml:"hidden,omitempty"`
- AlwaysBlocked bool `yaml:"always_blocked,omitempty"`
+ Room int `yaml:"room"`
+ Condition *behavior.Condition `yaml:"condition,omitempty"`
+ BlockedMessage string `yaml:"blocked_message,omitempty"`
+ OnTraverse []behavior.Trigger `yaml:"on_traverse,omitempty"`
+ Hidden bool `yaml:"hidden,omitempty"`
+ AlwaysBlocked bool `yaml:"always_blocked,omitempty"`
}
type Room struct {
- ID int `yaml:"id"`
- Name string `yaml:"name"`
- Color string `yaml:"color"`
- Description behavior.DescList `yaml:"description"`
- Exits map[ExitDir]ExitDef `yaml:"exits"`
- Objects []RoomObject `yaml:"objects"`
- ItemSpawns []SpawnDef `yaml:"item_spawns"`
- Mobs []RoomMob `yaml:"mobs"`
- OnEnter []behavior.StepAction `yaml:"on_enter"`
- Hazard string `yaml:"hazard"`
- BlockTransport bool `yaml:"block_transport"`
- Triggers []TriggerDef `yaml:"triggers"`
+ ID int `yaml:"id"`
+ Name string `yaml:"name"`
+ Color string `yaml:"color"`
+ Description behavior.DescList `yaml:"description"`
+ Exits map[ExitDir]ExitDef `yaml:"exits"`
+ Objects []RoomObject `yaml:"objects"`
+ ItemSpawns []SpawnDef `yaml:"item_spawns"`
+ Mobs []RoomMob `yaml:"mobs"`
+ OnEnter []behavior.Trigger `yaml:"on_enter,omitempty"`
+ OnExit []behavior.Trigger `yaml:"on_exit,omitempty"`
+ OnFlagChange []behavior.Trigger `yaml:"on_flag_change,omitempty"`
+ OnGlobalFlagChange []behavior.Trigger `yaml:"on_global_flag_change,omitempty"`
+ Hazard string `yaml:"hazard"`
+ BlockTransport bool `yaml:"block_transport"`
}
type RoomMob struct {
@@ -151,19 +153,19 @@ func (ro *RoomObject) UnmarshalYAML(value *yaml.Node) error {
func (ro RoomObject) MarshalYAML() (interface{}, error) {
if ro.Local != nil {
type inlineObj struct {
- Name string `yaml:"name"`
- Aliases []string `yaml:"aliases,omitempty"`
- Color string `yaml:"color,omitempty"`
- Hidden bool `yaml:"hidden,omitempty"`
- InRoomDescription string `yaml:"inroom_description,omitempty"`
- RemovalItem string `yaml:"removal_item,omitempty"`
- Description behavior.DescList `yaml:"description,omitempty"`
- OnUse []behavior.Interaction `yaml:"on_use,omitempty"`
- Steal *object.ObjectSteal `yaml:"steal,omitempty"`
- Gather *behavior.GatherConfig `yaml:"gather,omitempty"`
- Talk *behavior.TalkConfig `yaml:"talk,omitempty"`
- Safespot *object.SafespotConfig `yaml:"safespot,omitempty"`
- OnLook []behavior.Interaction `yaml:"on_look,omitempty"`
+ Name string `yaml:"name"`
+ Aliases []string `yaml:"aliases,omitempty"`
+ Color string `yaml:"color,omitempty"`
+ Hidden bool `yaml:"hidden,omitempty"`
+ InRoomDescription string `yaml:"inroom_description,omitempty"`
+ RemovalItem string `yaml:"removal_item,omitempty"`
+ Description behavior.DescList `yaml:"description,omitempty"`
+ OnUse []behavior.Trigger `yaml:"on_use,omitempty"`
+ Steal *object.ObjectSteal `yaml:"steal,omitempty"`
+ Gather *behavior.GatherConfig `yaml:"gather,omitempty"`
+ Talk *behavior.TalkConfig `yaml:"talk,omitempty"`
+ Safespot *object.SafespotConfig `yaml:"safespot,omitempty"`
+ OnLook []behavior.Trigger `yaml:"on_look,omitempty"`
}
def := ro.Local
return inlineObj{
diff --git a/internal/world/room_migrate.go b/internal/world/room_migrate.go
index 946417f..d609c6a 100644
--- a/internal/world/room_migrate.go
+++ b/internal/world/room_migrate.go
@@ -1,66 +1,174 @@
package world
+import (
+ "thehouseoficarus/internal/behavior"
+)
+
// RewriteRoomIDs remaps every genuine room-ID reference in this room per idMap
-// (oldID -> newID): exit targets, trigger Room/Teleport/DespawnRooms, on-enter
-// Teleport/DespawnRooms, and mob WanderRooms. It does NOT touch author-chosen
-// set_flags/set_player_flags values or condition `value:` fields — those are not
-// structural room references (and historically the swapid regex rewrote them
-// incorrectly as a side effect of matching any bare integer).
+// (oldID -> newID): exit targets; every Step.Teleport and Step.SpawnMob
+// .DespawnRooms inside on_enter/on_exit/on_flag_change/on_global_flag_change/
+// on_traverse/on_use/on_look/on_kill blocks; every Condition.Room anywhere in
+// the room (exits, descriptions, trigger conditions, per-step conditions,
+// including nested all_of/any_of); and mob WanderRooms. It does NOT touch
+// author-chosen set_flags/set_player_flags values or condition `value:` fields
+// — those are not structural room references.
//
// idMap is assumed to be a bijection (each old ID maps to a distinct new ID);
-// this holds for both swap (A<->B) and rename-to-unused (A->B). Returns whether
-// any field was changed.
+// this holds for both swap (A<->B) and rename-to-unused (A->B). Returns
+// whether any field was changed.
func (r *Room) RewriteRoomIDs(idMap map[int]int) bool {
if r == nil || len(idMap) == 0 {
return false
}
changed := false
+
+ // Exit targets + their on_traverse steps + exit/desc conditions.
for dir, exit := range r.Exits {
var exitChanged bool
if remapInt(idMap, &exit.Room) {
exitChanged = true
}
- for i := range exit.OnTraverse {
- if exit.OnTraverse[i].Action != nil {
- if remapInt(idMap, &exit.OnTraverse[i].Action.Teleport) {
- exitChanged = true
- }
- if exit.OnTraverse[i].Action.SpawnMob != nil &&
- remapIntSlice(idMap, exit.OnTraverse[i].Action.SpawnMob.DespawnRooms) {
- exitChanged = true
- }
- }
+ if remapTriggers(idMap, exit.OnTraverse) {
+ exitChanged = true
+ }
+ if exit.Condition != nil && remapCondition(idMap, exit.Condition) {
+ exitChanged = true
}
if exitChanged {
r.Exits[dir] = exit
changed = true
}
}
- for i := range r.Triggers {
- if remapInt(idMap, &r.Triggers[i].Room) {
+
+ // Every trigger-shaped block on the room shares the Step list + Condition
+ // structure. Remap their steps and conditions uniformly.
+ for i := range r.OnEnter {
+ c := false
+ if remapStepList(idMap, r.OnEnter[i].Steps) {
+ c = true
+ }
+ if remapCondition(idMap, r.OnEnter[i].Condition) {
+ c = true
+ }
+ changed = changed || c
+ }
+ for i := range r.OnExit {
+ c := false
+ if remapStepList(idMap, r.OnExit[i].Steps) {
+ c = true
+ }
+ if remapCondition(idMap, r.OnExit[i].Condition) {
+ c = true
+ }
+ changed = changed || c
+ }
+ for i := range r.OnFlagChange {
+ c := false
+ if remapStepList(idMap, r.OnFlagChange[i].Steps) {
+ c = true
+ }
+ if remapCondition(idMap, r.OnFlagChange[i].Condition) {
+ c = true
+ }
+ changed = changed || c
+ }
+ for i := range r.OnGlobalFlagChange {
+ c := false
+ if remapStepList(idMap, r.OnGlobalFlagChange[i].Steps) {
+ c = true
+ }
+ if remapCondition(idMap, r.OnGlobalFlagChange[i].Condition) {
+ c = true
+ }
+ changed = changed || c
+ }
+
+ // Conditional room descriptions carry a Condition.
+ for i := range r.Description {
+ if remapCondition(idMap, r.Description[i].Condition) {
changed = true
}
- for j := range r.Triggers[i].Steps {
- s := &r.Triggers[i].Steps[j]
- if remapInt(idMap, &s.Teleport) {
+ }
+
+ // Per-room local objects: on_use/on_look steps + object desc conditions.
+ for i := range r.Objects {
+ if r.Objects[i].Local != nil {
+ loc := r.Objects[i].Local
+ if remapTriggers(idMap, loc.OnUse) {
changed = true
}
- if s.SpawnMob != nil && remapIntSlice(idMap, s.SpawnMob.DespawnRooms) {
+ if remapTriggers(idMap, loc.OnLook) {
changed = true
}
+ for j := range loc.Description {
+ if remapCondition(idMap, loc.Description[j].Condition) {
+ changed = true
+ }
+ }
}
}
- for i := range r.OnEnter {
- s := &r.OnEnter[i]
+
+ for i := range r.Mobs {
+ if remapIntSlice(idMap, r.Mobs[i].WanderRooms) {
+ changed = true
+ }
+ }
+ return changed
+}
+
+// remapTriggers remaps step fields across a []behavior.Trigger (each trigger's
+// Steps + per-step Condition + trigger-level Condition).
+func remapTriggers(idMap map[int]int, list []behavior.Trigger) bool {
+ changed := false
+ for i := range list {
+ c := false
+ if remapStepList(idMap, list[i].Steps) {
+ c = true
+ }
+ if remapCondition(idMap, list[i].Condition) {
+ c = true
+ }
+ changed = changed || c
+ }
+ return changed
+}
+
+// remapStepList remaps Step.Teleport and Step.SpawnMob.DespawnRooms plus every
+// step's per-step Condition.Room across a single Trigger's Steps.
+func remapStepList(idMap map[int]int, steps []behavior.Step) bool {
+ changed := false
+ for i := range steps {
+ s := &steps[i]
if remapInt(idMap, &s.Teleport) {
changed = true
}
if s.SpawnMob != nil && remapIntSlice(idMap, s.SpawnMob.DespawnRooms) {
changed = true
}
+ if remapCondition(idMap, s.Condition) {
+ changed = true
+ }
}
- for i := range r.Mobs {
- if remapIntSlice(idMap, r.Mobs[i].WanderRooms) {
+ return changed
+}
+
+// remapCondition walks a Condition tree and remaps any Room field (a
+// structural room reference) it finds, recursing into AllOf/AnyOf.
+func remapCondition(idMap map[int]int, c *behavior.Condition) bool {
+ if c == nil {
+ return false
+ }
+ changed := false
+ if c.Room != 0 && remapInt(idMap, &c.Room) {
+ changed = true
+ }
+ for i := range c.AllOf {
+ if remapCondition(idMap, &c.AllOf[i]) {
+ changed = true
+ }
+ }
+ for i := range c.AnyOf {
+ if remapCondition(idMap, &c.AnyOf[i]) {
changed = true
}
}
diff --git a/internal/world/room_migrate_test.go b/internal/world/room_migrate_test.go
index c54adf2..96f74ae 100644
--- a/internal/world/room_migrate_test.go
+++ b/internal/world/room_migrate_test.go
@@ -2,29 +2,42 @@ package world
import (
"testing"
-
"thehouseoficarus/internal/behavior"
)
func TestRoomRewriteRoomIDs(t *testing.T) {
r := &Room{
Exits: map[ExitDir]ExitDef{
- North: {Room: 10, OnTraverse: []behavior.Interaction{{Action: &behavior.StepAction{
- NodeAction: behavior.NodeAction{SetGlobalFlags: map[string]any{"last": 10}, Teleport: 110},
- }}}},
- South: {Room: 99},
- },
- Triggers: []TriggerDef{
- {
- Room: 30,
- Steps: []behavior.StepAction{
- {NodeAction: behavior.NodeAction{Teleport: 40}, SpawnMob: &behavior.SpawnMobConfig{DespawnRooms: []int{50, 60}}},
- },
+ North: {
+ Room: 10,
+ OnTraverse: []behavior.Trigger{{
+ Condition: &behavior.Condition{Room: 60}, // projectile, ensures trigger-condition Room remaps too
+ Steps: []behavior.Step{{
+ SetGlobalFlags: map[string]any{"last": 10},
+ Teleport: 110,
+ }},
+ }},
+ Condition: &behavior.Condition{Room: 40},
},
+ South: {Room: 99},
},
- OnEnter: []behavior.StepAction{
- {NodeAction: behavior.NodeAction{Teleport: 70}, SpawnMob: &behavior.SpawnMobConfig{DespawnRooms: []int{80}}},
- },
+ OnEnter: []behavior.Trigger{{
+ Condition: &behavior.Condition{Room: 20},
+ Steps: []behavior.Step{{
+ Teleport: 70,
+ SpawnMob: &behavior.SpawnMobConfig{DespawnRooms: []int{80}},
+ }},
+ }},
+ OnExit: []behavior.Trigger{{
+ Steps: []behavior.Step{{Teleport: 50}},
+ }},
+ OnFlagChange: []behavior.Trigger{{
+ Condition: &behavior.Condition{Room: 30},
+ Steps: []behavior.Step{{
+ Teleport: 40,
+ SpawnMob: &behavior.SpawnMobConfig{DespawnRooms: []int{50, 60}},
+ }},
+ }},
Mobs: []RoomMob{
{ID: "guard", WanderRooms: []int{90, 100}},
},
@@ -38,40 +51,48 @@ func TestRoomRewriteRoomIDs(t *testing.T) {
t.Fatal("expected changed=true")
}
- checkExit := func(dir ExitDir, want int) {
- t.Helper()
- if got := r.Exits[dir].Room; got != want {
- t.Errorf("exit %s room = %d, want %d", dir, got, want)
- }
- }
- checkExit(North, 11)
- checkExit(South, 99) // unchanged (not in idMap)
-
- // Author set_flags values are NOT structural room references and must not
- // be remapped (the old swapid regex wrongly rewrote these).
- if v := r.Exits[North].OnTraverse[0].Action.SetGlobalFlags["last"]; v != 10 {
+ if got := r.Exits[North].Room; got != 11 {
+ t.Errorf("exit north room = %d, want 11", got)
+ }
+ if got := r.Exits[South].Room; got != 99 {
+ t.Errorf("exit south room = %d, want 99 (unchanged)", got)
+ }
+ // Author set_flags values are NOT structural room references and must not remap.
+ if v := r.Exits[North].OnTraverse[0].Steps[0].SetGlobalFlags["last"]; v != 10 {
t.Errorf("set_global_flags value remapped: got %v, want 10", v)
}
- // Exit on_traverse teleport IS a structural room reference and must remap.
- if r.Exits[North].OnTraverse[0].Action.Teleport != 111 {
- t.Errorf("exit on_traverse teleport = %d, want 111", r.Exits[North].OnTraverse[0].Action.Teleport)
+ if got := r.Exits[North].OnTraverse[0].Steps[0].Teleport; got != 111 {
+ t.Errorf("on_traverse teleport = %d, want 111", got)
}
-
- if r.Triggers[0].Room != 31 {
- t.Errorf("trigger room = %d, want 31", r.Triggers[0].Room)
+ // Exit-level condition Room remaps.
+ if got := r.Exits[North].Condition.Room; got != 41 {
+ t.Errorf("exit condition room = %d, want 41", got)
+ }
+ // Trigger-level + per-step Condition.Room remaps.
+ if got := r.Exits[North].OnTraverse[0].Condition.Room; got != 61 {
+ t.Errorf("on_traverse trigger condition room = %d, want 61", got)
}
- if r.Triggers[0].Steps[0].Teleport != 41 {
- t.Errorf("trigger teleport = %d, want 41", r.Triggers[0].Steps[0].Teleport)
+ if got := r.OnEnter[0].Condition.Room; got != 21 {
+ t.Errorf("on_enter trigger condition room = %d, want 21", got)
}
- if got := r.Triggers[0].Steps[0].SpawnMob.DespawnRooms; len(got) != 2 || got[0] != 51 || got[1] != 61 {
- t.Errorf("trigger despawn_rooms = %v, want [51 61]", got)
+ if got := r.OnFlagChange[0].Condition.Room; got != 31 {
+ t.Errorf("on_flag_change condition room = %d, want 31", got)
}
- if r.OnEnter[0].Teleport != 71 {
- t.Errorf("on_enter teleport = %d, want 71", r.OnEnter[0].Teleport)
+ if got := r.OnEnter[0].Steps[0].Teleport; got != 71 {
+ t.Errorf("on_enter teleport = %d, want 71", got)
}
- if got := r.OnEnter[0].SpawnMob.DespawnRooms; len(got) != 1 || got[0] != 81 {
+ if got := r.OnEnter[0].Steps[0].SpawnMob.DespawnRooms; len(got) != 1 || got[0] != 81 {
t.Errorf("on_enter despawn_rooms = %v, want [81]", got)
}
+ if got := r.OnExit[0].Steps[0].Teleport; got != 51 {
+ t.Errorf("on_exit teleport = %d, want 51", got)
+ }
+ if got := r.OnFlagChange[0].Steps[0].Teleport; got != 41 {
+ t.Errorf("on_flag_change teleport = %d, want 41", got)
+ }
+ if got := r.OnFlagChange[0].Steps[0].SpawnMob.DespawnRooms; len(got) != 2 || got[0] != 51 || got[1] != 61 {
+ t.Errorf("on_flag_change despawn_rooms = %v, want [51 61]", got)
+ }
if got := r.Mobs[0].WanderRooms; len(got) != 2 || got[0] != 91 || got[1] != 101 {
t.Errorf("mob wander_rooms = %v, want [91 101]", got)
}
@@ -81,7 +102,6 @@ func TestRoomRewriteRoomIDsNoChange(t *testing.T) {
r := &Room{
Exits: map[ExitDir]ExitDef{North: {Room: 5}},
}
- // idMap does not mention 5 -> nothing changes.
if r.RewriteRoomIDs(map[int]int{99: 100}) {
t.Error("expected changed=false when no references match")
}
@@ -100,7 +120,6 @@ func TestRoomRewriteRoomIDsSwapRoundTrips(t *testing.T) {
if r.Exits[North].Room != 20 || r.Exits[South].Room != 10 {
t.Errorf("after swap: north=%d south=%d, want north=20 south=10", r.Exits[North].Room, r.Exits[South].Room)
}
- // Applying the same swap again reverts (a swap is its own inverse).
r.RewriteRoomIDs(swap)
if r.Exits[North].Room != 10 || r.Exits[South].Room != 20 {
t.Errorf("after double swap: north=%d south=%d, want north=10 south=20", r.Exits[North].Room, r.Exits[South].Room)
@@ -114,7 +133,6 @@ func TestRoomRewriteRoomIDsIdempotentRename(t *testing.T) {
if r.Exits[North].Room != 11 {
t.Fatalf("after rename: room=%d, want 11", r.Exits[North].Room)
}
- // Second pass: 11 is not a key in idMap, so nothing changes.
if r.RewriteRoomIDs(rename) {
t.Error("expected changed=false on second pass of a rename idMap")
}
diff --git a/internal/world/trigger.go b/internal/world/trigger.go
deleted file mode 100644
index de07c18..0000000
--- a/internal/world/trigger.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package world
-
-import "thehouseoficarus/internal/behavior"
-
-type TriggerDef struct {
- ID string `yaml:"id"`
- OnPlayerFlag string `yaml:"on_player_flag"`
- OnGlobalFlag string `yaml:"on_global_flag"`
- Value any `yaml:"value"`
- Room int `yaml:"room"`
- Steps []behavior.StepAction `yaml:"steps"`
-}
-
-func (t *TriggerDef) IsPlayerFlagTrigger() bool { return t.OnPlayerFlag != "" }
-
-func (t *TriggerDef) IsGlobalFlagTrigger() bool { return t.OnGlobalFlag != "" } \ No newline at end of file
diff --git a/internal/world/trigger_store.go b/internal/world/trigger_store.go
deleted file mode 100644
index e50ae81..0000000
--- a/internal/world/trigger_store.go
+++ /dev/null
@@ -1,330 +0,0 @@
-package world
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "sync"
-
- "gopkg.in/yaml.v3"
- "thehouseoficarus/internal/behavior"
- "thehouseoficarus/internal/engine"
-)
-
-type TriggerStore struct {
- mu sync.Mutex
-
- globalByPlayerFlag map[string][]*TriggerDef
- globalByGlobalFlag map[string][]*TriggerDef
- roomByPlayerFlag map[int]map[string][]*TriggerDef
- roomByGlobalFlag map[int]map[string][]*TriggerDef
-
- playerSeqs map[string]*PlayerTriggerSeq
- globalSeqs map[string]*GlobalTriggerSeq
-}
-
-type PlayerTriggerSeq struct {
- PlayerName string
- TriggerID string
- RoomID int
- Steps []behavior.StepAction
- Wait int
- Started bool
- FlagValue any
-}
-
-type GlobalTriggerSeq struct {
- TriggerID string
- RoomID int
- Steps []behavior.StepAction
- Wait int
- FlagValue any
-}
-
-func NewTriggerStore() *TriggerStore {
- return &TriggerStore{
- globalByPlayerFlag: make(map[string][]*TriggerDef),
- globalByGlobalFlag: make(map[string][]*TriggerDef),
- roomByPlayerFlag: make(map[int]map[string][]*TriggerDef),
- roomByGlobalFlag: make(map[int]map[string][]*TriggerDef),
- playerSeqs: make(map[string]*PlayerTriggerSeq),
- globalSeqs: make(map[string]*GlobalTriggerSeq),
- }
-}
-
-func (ts *TriggerStore) ClearTriggers() {
- ts.mu.Lock()
- defer ts.mu.Unlock()
- ts.globalByPlayerFlag = make(map[string][]*TriggerDef)
- ts.globalByGlobalFlag = make(map[string][]*TriggerDef)
- ts.roomByPlayerFlag = make(map[int]map[string][]*TriggerDef)
- ts.roomByGlobalFlag = make(map[int]map[string][]*TriggerDef)
-}
-
-func (ts *TriggerStore) LoadGlobal(dataDir string) error {
- dir := filepath.Join(dataDir, "triggers")
- if _, err := os.Stat(dir); os.IsNotExist(err) {
- return nil
- }
-
- entries, err := os.ReadDir(dir)
- if err != nil {
- return err
- }
-
- for _, entry := range entries {
- if entry.IsDir() {
- continue
- }
- path := filepath.Join(dir, entry.Name())
- ext := strings.ToLower(filepath.Ext(path))
- if ext != ".yaml" && ext != ".yml" {
- continue
- }
- id := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name()))
- data, err := os.ReadFile(path)
- if err != nil {
- continue
- }
- var trigger TriggerDef
- if err := yaml.Unmarshal(data, &trigger); err != nil {
- return fmt.Errorf("trigger %q: %w", path, err)
- }
- trigger.ID = id
- ts.addGlobal(&trigger)
- }
- return nil
-}
-
-func (ts *TriggerStore) SeedRoomTriggers(roomID int, triggers []TriggerDef) {
- if len(triggers) == 0 {
- return
- }
- ts.mu.Lock()
- defer ts.mu.Unlock()
- for i := range triggers {
- t := &triggers[i]
- if t.IsPlayerFlagTrigger() {
- if ts.roomByPlayerFlag[roomID] == nil {
- ts.roomByPlayerFlag[roomID] = make(map[string][]*TriggerDef)
- }
- ts.roomByPlayerFlag[roomID][t.OnPlayerFlag] = append(
- ts.roomByPlayerFlag[roomID][t.OnPlayerFlag], t)
- }
- if t.IsGlobalFlagTrigger() {
- if ts.roomByGlobalFlag[roomID] == nil {
- ts.roomByGlobalFlag[roomID] = make(map[string][]*TriggerDef)
- }
- t.Room = roomID
- ts.roomByGlobalFlag[roomID][t.OnGlobalFlag] = append(
- ts.roomByGlobalFlag[roomID][t.OnGlobalFlag], t)
- }
- }
-}
-
-func (ts *TriggerStore) addGlobal(t *TriggerDef) {
- ts.mu.Lock()
- defer ts.mu.Unlock()
- if t.IsPlayerFlagTrigger() {
- ts.globalByPlayerFlag[t.OnPlayerFlag] = append(
- ts.globalByPlayerFlag[t.OnPlayerFlag], t)
- }
- if t.IsGlobalFlagTrigger() {
- ts.globalByGlobalFlag[t.OnGlobalFlag] = append(
- ts.globalByGlobalFlag[t.OnGlobalFlag], t)
- }
-}
-
-func (ts *TriggerStore) FirePlayer(playerName string, flagName string, flagValue any) []*PlayerTriggerSeq {
- ts.mu.Lock()
- defer ts.mu.Unlock()
-
- var started []*PlayerTriggerSeq
-
- for _, t := range ts.globalByPlayerFlag[flagName] {
- if !ts.valueMatches(t.Value, flagValue) {
- continue
- }
- seq := ts.startPlayerSeqLocked(playerName, t, 0, flagValue)
- if seq != nil {
- started = append(started, seq)
- }
- }
-
- return started
-}
-
-func (ts *TriggerStore) FirePlayerInRoom(playerName string, roomID int, flagName string, flagValue any) []*PlayerTriggerSeq {
- ts.mu.Lock()
- defer ts.mu.Unlock()
-
- var started []*PlayerTriggerSeq
-
- for _, t := range ts.globalByPlayerFlag[flagName] {
- if !ts.valueMatches(t.Value, flagValue) {
- continue
- }
- seq := ts.startPlayerSeqLocked(playerName, t, roomID, flagValue)
- if seq != nil {
- started = append(started, seq)
- }
- }
-
- if roomFlags, ok := ts.roomByPlayerFlag[roomID]; ok {
- for _, t := range roomFlags[flagName] {
- if !ts.valueMatches(t.Value, flagValue) {
- continue
- }
- seq := ts.startPlayerSeqLocked(playerName, t, roomID, flagValue)
- if seq != nil {
- started = append(started, seq)
- }
- }
- }
-
- return started
-}
-
-func (ts *TriggerStore) FireGlobal(flagName string, flagValue any) []*GlobalTriggerSeq {
- ts.mu.Lock()
- defer ts.mu.Unlock()
-
- var started []*GlobalTriggerSeq
-
- for _, t := range ts.globalByGlobalFlag[flagName] {
- if !ts.valueMatches(t.Value, flagValue) {
- continue
- }
- seq := ts.startGlobalSeqLocked(t, flagValue)
- if seq != nil {
- started = append(started, seq)
- }
- }
-
- for roomID, roomFlags := range ts.roomByGlobalFlag {
- for _, t := range roomFlags[flagName] {
- if !ts.valueMatches(t.Value, flagValue) {
- continue
- }
- if t.Room == 0 {
- t.Room = roomID
- }
- seq := ts.startGlobalSeqLocked(t, flagValue)
- if seq != nil {
- started = append(started, seq)
- }
- }
- }
-
- return started
-}
-
-func (ts *TriggerStore) startPlayerSeqLocked(playerName string, t *TriggerDef, roomID int, flagValue any) *PlayerTriggerSeq {
- key := playerName + ":" + t.ID
- if _, exists := ts.playerSeqs[key]; exists {
- return nil
- }
- seq := &PlayerTriggerSeq{
- PlayerName: playerName,
- TriggerID: t.ID,
- RoomID: roomID,
- Steps: make([]behavior.StepAction, len(t.Steps)),
- FlagValue: flagValue,
- }
- copy(seq.Steps, t.Steps)
- if len(seq.Steps) > 0 {
- seq.Wait = seq.Steps[0].Delay
- }
- ts.playerSeqs[key] = seq
- return seq
-}
-
-func (ts *TriggerStore) startGlobalSeqLocked(t *TriggerDef, flagValue any) *GlobalTriggerSeq {
- key := t.ID
- if _, exists := ts.globalSeqs[key]; exists {
- return nil
- }
- seq := &GlobalTriggerSeq{
- TriggerID: t.ID,
- RoomID: t.Room,
- Steps: make([]behavior.StepAction, len(t.Steps)),
- FlagValue: flagValue,
- }
- copy(seq.Steps, t.Steps)
- if len(seq.Steps) > 0 {
- seq.Wait = seq.Steps[0].Delay
- }
- ts.globalSeqs[key] = seq
- return seq
-}
-
-func (ts *TriggerStore) valueMatches(want, got any) bool {
- if want == nil {
- return true
- }
- return engine.ValuesEqual(want, got)
-}
-
-func (ts *TriggerStore) SnapshotPlayerSeqs() []*PlayerTriggerSeq {
- ts.mu.Lock()
- defer ts.mu.Unlock()
- out := make([]*PlayerTriggerSeq, 0, len(ts.playerSeqs))
- for _, s := range ts.playerSeqs {
- out = append(out, s)
- }
- return out
-}
-
-func (ts *TriggerStore) SnapshotGlobalSeqs() []*GlobalTriggerSeq {
- ts.mu.Lock()
- defer ts.mu.Unlock()
- out := make([]*GlobalTriggerSeq, 0, len(ts.globalSeqs))
- for _, s := range ts.globalSeqs {
- out = append(out, s)
- }
- return out
-}
-
-func (ts *TriggerStore) RemovePlayerSeq(playerName, triggerID string) {
- ts.mu.Lock()
- defer ts.mu.Unlock()
- key := playerName + ":" + triggerID
- delete(ts.playerSeqs, key)
-}
-
-func (ts *TriggerStore) RemoveGlobalSeq(triggerID string) {
- ts.mu.Lock()
- defer ts.mu.Unlock()
- delete(ts.globalSeqs, triggerID)
-}
-
-func (ts *TriggerStore) AllGlobalTriggers() []*TriggerDef {
- ts.mu.Lock()
- defer ts.mu.Unlock()
- var out []*TriggerDef
- for _, triggers := range ts.globalByPlayerFlag {
- out = append(out, triggers...)
- }
- for _, triggers := range ts.globalByGlobalFlag {
- out = append(out, triggers...)
- }
- return out
-}
-
-func (ts *TriggerStore) AllRoomTriggers() map[int][]*TriggerDef {
- ts.mu.Lock()
- defer ts.mu.Unlock()
- out := make(map[int][]*TriggerDef)
- for roomID, byFlag := range ts.roomByPlayerFlag {
- for _, triggers := range byFlag {
- out[roomID] = append(out[roomID], triggers...)
- }
- }
- for roomID, byFlag := range ts.roomByGlobalFlag {
- for _, triggers := range byFlag {
- out[roomID] = append(out[roomID], triggers...)
- }
- }
- return out
-}