aboutsummaryrefslogtreecommitdiff
path: root/internal/world
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-25 20:33:47 -0400
committerhistoria <[not public]>2026-06-25 20:33:47 -0400
commit84d74ca4351a97148ad8339676f9df7946bc21fb (patch)
treea9f50ac56e8bbcdf2b307ef6f7143b67bce46c61 /internal/world
parentc55d0e4150b23f2c5c9f96bbc3d4dc2fc6dbaa36 (diff)
downloadthehouseoficarus-84d74ca4351a97148ad8339676f9df7946bc21fb.tar.gz
feat: trigger system for scripted events added.
Diffstat (limited to 'internal/world')
-rw-r--r--internal/world/mob.go45
-rw-r--r--internal/world/room.go1
-rw-r--r--internal/world/trigger.go61
-rw-r--r--internal/world/trigger_store.go340
4 files changed, 447 insertions, 0 deletions
diff --git a/internal/world/mob.go b/internal/world/mob.go
index f11d6fe..d3ea05b 100644
--- a/internal/world/mob.go
+++ b/internal/world/mob.go
@@ -127,6 +127,14 @@ type MobInstance struct {
CompleteMessage string
TalkConfig *behavior.TalkConfig
+
+ Owner string
+ OwnerOnly bool
+ SpawnedByTrigger bool
+ DespawnOnLeave bool
+ DespawnRooms []int
+ DespawnTicks float64
+ DespawnCounter int
}
func (m *MobInstance) IsTalkable() bool { return m.TalkConfig != nil }
@@ -215,6 +223,20 @@ func (m *MobInstance) StartRegen() {
}
}
+func (m *MobInstance) DespawnTickCount() int {
+ return m.DespawnCounter
+}
+
+func (m *MobInstance) DecrementDespawnCounter() {
+ if m.DespawnCounter > 0 {
+ m.DespawnCounter--
+ }
+}
+
+func (m *MobInstance) ResetDespawnCounter(ticks int) {
+ m.DespawnCounter = ticks
+}
+
type MobStore struct {
dataDir string
mu sync.Mutex
@@ -326,6 +348,29 @@ func (s *MobStore) RollIdleDescription(inst *MobInstance) {
inst.WanderTickCounter = 0
}
+func (s *MobStore) SpawnTransient(def *MobDef, cfg *SpawnMobConfig, roomID int, owner string) *MobInstance {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ instID := fmt.Sprintf("trigger_%s_%d_%d", cfg.ID, roomID, len(s.instances))
+ inst := NewMobInstance(def, instID, roomID, nil, 0)
+ inst.Owner = owner
+ inst.OwnerOnly = cfg.OwnerOnly
+ inst.SpawnedByTrigger = true
+ inst.DespawnOnLeave = cfg.DespawnOnLeave
+ inst.DespawnRooms = cfg.DespawnRooms
+ inst.DespawnTicks = cfg.DespawnTicks
+ inst.IdleDescription = pickIdleDescription(def.IdleDescriptions)
+ s.instances[instID] = inst
+ return inst
+}
+
+func (s *MobStore) RemoveInstance(instanceID string) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ delete(s.instances, instanceID)
+}
+
func (s *MobStore) SeedMobs(roomID int, entries []RoomMob) {
type defWrapper struct {
def *MobDef
diff --git a/internal/world/room.go b/internal/world/room.go
index 71a36ed..22e7f79 100644
--- a/internal/world/room.go
+++ b/internal/world/room.go
@@ -75,6 +75,7 @@ type Room struct {
Mobs []RoomMob `yaml:"mobs"`
OnEnter []EnterStep `yaml:"on_enter"`
Hazard string `yaml:"hazard"`
+ Triggers []TriggerDef `yaml:"triggers"`
}
type RoomMob struct {
diff --git a/internal/world/trigger.go b/internal/world/trigger.go
new file mode 100644
index 0000000..7c06be4
--- /dev/null
+++ b/internal/world/trigger.go
@@ -0,0 +1,61 @@
+package world
+
+import (
+ "gopkg.in/yaml.v3"
+)
+
+type TriggerDef struct {
+ ID string `yaml:"id"`
+ OnPlayerFlag string `yaml:"on_player_flag"`
+ OnFlag string `yaml:"on_flag"`
+ Value any `yaml:"value"`
+ Room int `yaml:"room"`
+ Steps []TriggerStep `yaml:"steps"`
+}
+
+func (t *TriggerDef) IsPlayerFlagTrigger() bool { return t.OnPlayerFlag != "" }
+
+func (t *TriggerDef) IsWorldFlagTrigger() bool { return t.OnFlag != "" }
+
+type TriggerStep struct {
+ Delay int `yaml:"delay"`
+ Message string `yaml:"message"`
+ Broadcast string `yaml:"broadcast"`
+ BroadcastGlobal string `yaml:"broadcast_global"`
+ SetFlags map[string]any `yaml:"set_flags"`
+ SetPlayerFlags map[string]any `yaml:"set_player_flags"`
+ SpawnMob *SpawnMobConfig `yaml:"spawn_mob"`
+ GiveItem string `yaml:"give_item"`
+ TakeItem string `yaml:"take_item"`
+ Teleport int `yaml:"teleport"`
+ Heal int `yaml:"heal"`
+ DespawnMob string `yaml:"despawn_mob"`
+}
+
+type SpawnMobConfig struct {
+ ID string `yaml:"id"`
+ OwnerOnly bool `yaml:"owner_only"`
+ DespawnOnLeave bool `yaml:"despawn_on_leave"`
+ DespawnRooms []int `yaml:"despawn_rooms"`
+ DespawnTicks float64 `yaml:"despawn_ticks"`
+}
+
+func (s *SpawnMobConfig) UnmarshalYAML(value *yaml.Node) error {
+ if value.Kind == yaml.ScalarNode {
+ var id string
+ if err := value.Decode(&id); err != nil {
+ return err
+ }
+ s.ID = id
+ return nil
+ }
+ type raw SpawnMobConfig
+ return value.Decode((*raw)(s))
+}
+
+func (t TriggerStep) IsTimed() bool {
+ return t.Delay > 0 || len(t.SetFlags) > 0 || len(t.SetPlayerFlags) > 0 ||
+ t.Message != "" || t.Broadcast != "" || t.BroadcastGlobal != "" ||
+ t.SpawnMob != nil || t.GiveItem != "" || t.TakeItem != "" ||
+ t.Teleport != 0 || t.Heal != 0 || t.DespawnMob != ""
+}
diff --git a/internal/world/trigger_store.go b/internal/world/trigger_store.go
new file mode 100644
index 0000000..f00d256
--- /dev/null
+++ b/internal/world/trigger_store.go
@@ -0,0 +1,340 @@
+package world
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "gopkg.in/yaml.v3"
+)
+
+type TriggerStore struct {
+ mu sync.Mutex
+
+ globalByPlayerFlag map[string][]*TriggerDef
+ globalByWorldFlag map[string][]*TriggerDef
+ roomByPlayerFlag map[int]map[string][]*TriggerDef
+ roomByWorldFlag map[int]map[string][]*TriggerDef
+
+ playerSeqs map[string]*PlayerTriggerSeq
+ worldSeqs map[string]*WorldTriggerSeq
+}
+
+type PlayerTriggerSeq struct {
+ PlayerName string
+ TriggerID string
+ RoomID int
+ Steps []TriggerStep
+ Wait int
+ Started bool
+ FlagValue any
+}
+
+type WorldTriggerSeq struct {
+ TriggerID string
+ RoomID int
+ Steps []TriggerStep
+ Wait int
+ FlagValue any
+}
+
+func NewTriggerStore() *TriggerStore {
+ return &TriggerStore{
+ globalByPlayerFlag: make(map[string][]*TriggerDef),
+ globalByWorldFlag: make(map[string][]*TriggerDef),
+ roomByPlayerFlag: make(map[int]map[string][]*TriggerDef),
+ roomByWorldFlag: make(map[int]map[string][]*TriggerDef),
+ playerSeqs: make(map[string]*PlayerTriggerSeq),
+ worldSeqs: make(map[string]*WorldTriggerSeq),
+ }
+}
+
+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.IsWorldFlagTrigger() {
+ if ts.roomByWorldFlag[roomID] == nil {
+ ts.roomByWorldFlag[roomID] = make(map[string][]*TriggerDef)
+ }
+ t.Room = roomID
+ ts.roomByWorldFlag[roomID][t.OnFlag] = append(
+ ts.roomByWorldFlag[roomID][t.OnFlag], 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.IsWorldFlagTrigger() {
+ ts.globalByWorldFlag[t.OnFlag] = append(
+ ts.globalByWorldFlag[t.OnFlag], 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) FireWorld(flagName string, flagValue any) []*WorldTriggerSeq {
+ ts.mu.Lock()
+ defer ts.mu.Unlock()
+
+ var started []*WorldTriggerSeq
+
+ for _, t := range ts.globalByWorldFlag[flagName] {
+ if !ts.valueMatches(t.Value, flagValue) {
+ continue
+ }
+ seq := ts.startWorldSeqLocked(t, flagValue)
+ if seq != nil {
+ started = append(started, seq)
+ }
+ }
+
+ for roomID, roomFlags := range ts.roomByWorldFlag {
+ for _, t := range roomFlags[flagName] {
+ if !ts.valueMatches(t.Value, flagValue) {
+ continue
+ }
+ if t.Room == 0 {
+ t.Room = roomID
+ }
+ seq := ts.startWorldSeqLocked(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([]TriggerStep, 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) startWorldSeqLocked(t *TriggerDef, flagValue any) *WorldTriggerSeq {
+ key := t.ID
+ if _, exists := ts.worldSeqs[key]; exists {
+ return nil
+ }
+ seq := &WorldTriggerSeq{
+ TriggerID: t.ID,
+ RoomID: t.Room,
+ Steps: make([]TriggerStep, len(t.Steps)),
+ FlagValue: flagValue,
+ }
+ copy(seq.Steps, t.Steps)
+ if len(seq.Steps) > 0 {
+ seq.Wait = seq.Steps[0].Delay
+ }
+ ts.worldSeqs[key] = seq
+ return seq
+}
+
+func (ts *TriggerStore) valueMatches(want, got any) bool {
+ if want == nil {
+ return true
+ }
+ return 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) SnapshotWorldSeqs() []*WorldTriggerSeq {
+ ts.mu.Lock()
+ defer ts.mu.Unlock()
+ out := make([]*WorldTriggerSeq, 0, len(ts.worldSeqs))
+ for _, s := range ts.worldSeqs {
+ 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) RemoveWorldSeq(triggerID string) {
+ ts.mu.Lock()
+ defer ts.mu.Unlock()
+ delete(ts.worldSeqs, 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.globalByWorldFlag {
+ out = append(out, triggers...)
+ }
+ return out
+}
+
+func valuesEqual(a, b any) bool {
+ ai, aok := numericValue(a)
+ bi, bok := numericValue(b)
+ if aok && bok {
+ return ai == bi
+ }
+ return a == b
+}
+
+func numericValue(v any) (float64, bool) {
+ switch x := v.(type) {
+ case int:
+ return float64(x), true
+ case int64:
+ return float64(x), true
+ case float64:
+ return x, true
+ }
+ return 0, false
+}
+
+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.roomByWorldFlag {
+ for _, triggers := range byFlag {
+ out[roomID] = append(out[roomID], triggers...)
+ }
+ }
+ return out
+}