diff options
Diffstat (limited to 'internal/world')
| -rw-r--r-- | internal/world/mob.go | 45 | ||||
| -rw-r--r-- | internal/world/room.go | 1 | ||||
| -rw-r--r-- | internal/world/trigger.go | 61 | ||||
| -rw-r--r-- | internal/world/trigger_store.go | 340 |
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 +} |
