diff options
| author | historia <[not public]> | 2026-06-25 20:33:47 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-25 20:33:47 -0400 |
| commit | 84d74ca4351a97148ad8339676f9df7946bc21fb (patch) | |
| tree | a9f50ac56e8bbcdf2b307ef6f7143b67bce46c61 /internal/game | |
| parent | c55d0e4150b23f2c5c9f96bbc3d4dc2fc6dbaa36 (diff) | |
| download | thehouseoficarus-84d74ca4351a97148ad8339676f9df7946bc21fb.tar.gz | |
feat: trigger system for scripted events added.
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/act_agility.go | 2 | ||||
| -rw-r--r-- | internal/game/act_farm.go | 80 | ||||
| -rw-r--r-- | internal/game/act_fletch.go | 2 | ||||
| -rw-r--r-- | internal/game/act_room.go | 3 | ||||
| -rw-r--r-- | internal/game/act_steal.go | 4 | ||||
| -rw-r--r-- | internal/game/act_talk.go | 11 | ||||
| -rw-r--r-- | internal/game/cmd_craft.go | 2 | ||||
| -rw-r--r-- | internal/game/combat_attack.go | 4 | ||||
| -rw-r--r-- | internal/game/combat_mob.go | 15 | ||||
| -rw-r--r-- | internal/game/core_flags.go | 16 | ||||
| -rw-r--r-- | internal/game/flagstore.go | 39 | ||||
| -rw-r--r-- | internal/game/game.go | 18 | ||||
| -rw-r--r-- | internal/game/production_menu.go | 2 | ||||
| -rw-r--r-- | internal/game/sys_assassin.go | 32 | ||||
| -rw-r--r-- | internal/game/triggers.go | 291 |
15 files changed, 446 insertions, 75 deletions
diff --git a/internal/game/act_agility.go b/internal/game/act_agility.go index 2136699..4a68580 100644 --- a/internal/game/act_agility.go +++ b/internal/game/act_agility.go @@ -103,7 +103,7 @@ func (g *Game) advanceObstacle(sess *net.Session, p *player.Player) { lapKey := "agility_laps_" + courseID laps := getPlayerFlagInt(p, lapKey) + 1 - setPlayerFlag(p, lapKey, laps) + g.setPlayerFlag(p, lapKey, laps) courseName := p.Action.TargetName sess.WriteLine(g.colorize(sess, "broadcast", diff --git a/internal/game/act_farm.go b/internal/game/act_farm.go index 62e08fd..a4d96b4 100644 --- a/internal/game/act_farm.go +++ b/internal/game/act_farm.go @@ -41,13 +41,13 @@ func farmFlagPrefix(defID string, index int) string { func (g *Game) ensureFarmState(p *player.Player, prefix string) { p.EnsureFlags() if _, exists := p.Flags[prefix+"_seed"]; !exists { - p.Flags[prefix+"_weeds"] = true - p.Flags[prefix+"_seed"] = "" - p.Flags[prefix+"_stage"] = 0 - p.Flags[prefix+"_watered"] = false - p.Flags[prefix+"_diseased"] = false - p.Flags[prefix+"_dead"] = false - p.Flags[prefix+"_ready"] = false + g.setPlayerFlag(p, prefix+"_weeds", true) + g.setPlayerFlag(p, prefix+"_seed", "") + g.setPlayerFlag(p, prefix+"_stage", 0) + g.setPlayerFlag(p, prefix+"_watered", false) + g.setPlayerFlag(p, prefix+"_diseased", false) + g.setPlayerFlag(p, prefix+"_dead", false) + g.setPlayerFlag(p, prefix+"_ready", false) } } @@ -111,8 +111,8 @@ func (g *Game) advanceFarmGrowth(sess *net.Session, p *player.Player) { } if diseased { - p.Flags[prefix+"_dead"] = true - p.Flags[prefix+"_diseased"] = false + g.setPlayerFlag(p, prefix+"_dead", true) + g.setPlayerFlag(p, prefix+"_diseased", false) sess.WriteLine(g.colorize(sess, "farm_disease", fmt.Sprintf("\nYour %s has died from disease!", g.seedDisplayName(sess, seedID)))) g.AccountStore.SaveCharacter(p) @@ -135,23 +135,23 @@ func (g *Game) advanceFarmGrowth(sess *net.Session, p *player.Player) { stage++ if stage >= maxStages { - p.Flags[prefix+"_stage"] = stage - p.Flags[prefix+"_ready"] = true - p.Flags[prefix+"_watered"] = false + g.setPlayerFlag(p, prefix+"_stage", stage) + g.setPlayerFlag(p, prefix+"_ready", true) + g.setPlayerFlag(p, prefix+"_watered", false) sess.WriteLine(g.colorize(sess, "farm_grow", fmt.Sprintf("\nYour %s is fully grown and ready to harvest!", g.seedDisplayName(sess, seedID)))) } else { if !watered && rand.Float64() < 0.10 { - p.Flags[prefix+"_stage"] = stage - p.Flags[prefix+"_diseased"] = true - p.Flags[prefix+"_watered"] = false + g.setPlayerFlag(p, prefix+"_stage", stage) + g.setPlayerFlag(p, prefix+"_diseased", true) + g.setPlayerFlag(p, prefix+"_watered", false) sess.WriteLine(g.colorize(sess, "farm_disease", fmt.Sprintf("\nYour %s has become diseased!", g.seedDisplayName(sess, seedID)))) } else { - p.Flags[prefix+"_stage"] = stage - p.Flags[prefix+"_watered"] = false + g.setPlayerFlag(p, prefix+"_stage", stage) + g.setPlayerFlag(p, prefix+"_watered", false) sess.WriteLine(g.colorize(sess, "farm_grow", fmt.Sprintf("\nYour %s has grown to stage %d/%d.", g.seedDisplayName(sess, seedID), stage, maxStages))) @@ -182,13 +182,13 @@ func (g *Game) advancePlant(sess *net.Session, p *player.Player) { g.ensureFarmState(p, prefix) - p.Flags[prefix+"_weeds"] = false - p.Flags[prefix+"_seed"] = seedID - p.Flags[prefix+"_stage"] = 0 - p.Flags[prefix+"_watered"] = false - p.Flags[prefix+"_diseased"] = false - p.Flags[prefix+"_dead"] = false - p.Flags[prefix+"_ready"] = false + g.setPlayerFlag(p, prefix+"_weeds", false) + g.setPlayerFlag(p, prefix+"_seed", seedID) + g.setPlayerFlag(p, prefix+"_stage", 0) + g.setPlayerFlag(p, prefix+"_watered", false) + g.setPlayerFlag(p, prefix+"_diseased", false) + g.setPlayerFlag(p, prefix+"_dead", false) + g.setPlayerFlag(p, prefix+"_ready", false) if intXP := int(xp); intXP > 0 { g.awardSkillXP(sess, p, player.Farming, intXP) @@ -280,13 +280,13 @@ func (g *Game) advanceHarvest(sess *net.Session, p *player.Player) { } g.ensureFarmState(p, prefix) - p.Flags[prefix+"_seed"] = "" - p.Flags[prefix+"_stage"] = 0 - p.Flags[prefix+"_ready"] = false - p.Flags[prefix+"_watered"] = false - p.Flags[prefix+"_weeds"] = true - p.Flags[prefix+"_diseased"] = false - p.Flags[prefix+"_dead"] = false + g.setPlayerFlag(p, prefix+"_seed", "") + g.setPlayerFlag(p, prefix+"_stage", 0) + g.setPlayerFlag(p, prefix+"_ready", false) + g.setPlayerFlag(p, prefix+"_watered", false) + g.setPlayerFlag(p, prefix+"_weeds", true) + g.setPlayerFlag(p, prefix+"_diseased", false) + g.setPlayerFlag(p, prefix+"_dead", false) g.AccountStore.SaveCharacter(p) @@ -314,13 +314,13 @@ func (g *Game) advanceRake(sess *net.Session, p *player.Player) { } prefix := data.Prefix - p.Flags[prefix+"_weeds"] = false - p.Flags[prefix+"_dead"] = false - p.Flags[prefix+"_seed"] = "" - p.Flags[prefix+"_stage"] = 0 - p.Flags[prefix+"_ready"] = false - p.Flags[prefix+"_diseased"] = false - p.Flags[prefix+"_watered"] = false + g.setPlayerFlag(p, prefix+"_weeds", false) + g.setPlayerFlag(p, prefix+"_dead", false) + g.setPlayerFlag(p, prefix+"_seed", "") + g.setPlayerFlag(p, prefix+"_stage", 0) + g.setPlayerFlag(p, prefix+"_ready", false) + g.setPlayerFlag(p, prefix+"_diseased", false) + g.setPlayerFlag(p, prefix+"_watered", false) g.AccountStore.SaveCharacter(p) @@ -340,7 +340,7 @@ func (g *Game) advanceWater(sess *net.Session, p *player.Player) { } prefix := data.Prefix - p.Flags[prefix+"_watered"] = true + g.setPlayerFlag(p, prefix+"_watered", true) g.AccountStore.SaveCharacter(p) @@ -361,7 +361,7 @@ func (g *Game) advanceCure(sess *net.Session, p *player.Player) { prefix := data.Prefix p.RemoveItem("plant_cure", 1) - p.Flags[prefix+"_diseased"] = false + g.setPlayerFlag(p, prefix+"_diseased", false) g.AccountStore.SaveCharacter(p) diff --git a/internal/game/act_fletch.go b/internal/game/act_fletch.go index 78a19ad..e85f2c1 100644 --- a/internal/game/act_fletch.go +++ b/internal/game/act_fletch.go @@ -12,7 +12,7 @@ import ( func (g *Game) startFletchAction(sess *net.Session, p *player.Player, def *item.ItemDef, count int) { p.EnsureFlags() - p.Flags["last_fletch"] = def.ID + g.setPlayerFlag(p, "last_fletch", def.ID) g.AccountStore.SaveCharacter(p) if def.FirstCraft().Wait <= 0 { diff --git a/internal/game/act_room.go b/internal/game/act_room.go index 86c1737..fec7304 100644 --- a/internal/game/act_room.go +++ b/internal/game/act_room.go @@ -188,9 +188,8 @@ func (g *Game) isCharLive(name string, sess *net.Session) bool { func (g *Game) applyFlagMutations(p *player.Player, setFlags, setPlayerFlags map[string]any) { g.Flags.SetAll(setFlags) if len(setPlayerFlags) > 0 { - p.EnsureFlags() for k, v := range setPlayerFlags { - p.Flags[k] = v + g.setPlayerFlag(p, k, v) } } } diff --git a/internal/game/act_steal.go b/internal/game/act_steal.go index 7371782..39df9e2 100644 --- a/internal/game/act_steal.go +++ b/internal/game/act_steal.go @@ -203,6 +203,10 @@ func (g *Game) startSteal(sess *net.Session, p *player.Player, targetType string targetID = mob.InstanceID mobInstanceID = mob.InstanceID + if mob.OwnerOnly && mob.Owner != "" && mob.Owner != p.Name { + sess.WriteLine("They don't seem interested in you.") + return + } if stealTable == "" { sess.WriteLine(fmt.Sprintf("You can't steal from the %s.", targetName)) return diff --git a/internal/game/act_talk.go b/internal/game/act_talk.go index f54290a..14361f8 100644 --- a/internal/game/act_talk.go +++ b/internal/game/act_talk.go @@ -14,6 +14,10 @@ import ( ) func (g *Game) startMobTalk(sess *net.Session, p *player.Player, mob *world.MobInstance) { + if mob.OwnerOnly && mob.Owner != "" && mob.Owner != p.Name { + sess.WriteLine("They ignore you.") + return + } cfg := mob.TalkConfig if cfg == nil { sess.WriteLine("This person has nothing to say.") @@ -267,7 +271,7 @@ func (g *Game) applyNodeAction(sess *net.Session, na *behavior.NodeAction) { sess.WriteLine(fmt.Sprintf("You don't have enough Reputation. (Need %d, have %d)", na.ReputationCost, rep)) return } - setPlayerFlag(p, "assassin_reputation", rep-na.ReputationCost) + g.setPlayerFlag(p, "assassin_reputation", rep-na.ReputationCost) g.AccountStore.SaveCharacter(p) } @@ -275,8 +279,7 @@ func (g *Game) applyNodeAction(sess *net.Session, na *behavior.NodeAction) { g.Flags.Set(k, v) } for k, v := range na.SetPlayerFlags { - p.EnsureFlags() - p.Flags[k] = v + g.setPlayerFlag(p, k, v) } if na.TakeItem != "" { if p.HasItem(na.TakeItem) { @@ -323,6 +326,6 @@ func (g *Game) applyNodeAction(sess *net.Session, na *behavior.NodeAction) { g.processSawmill(sess, p) } if na.ApsNode { - setPlayerFlag(p, "aps_node_"+strconv.Itoa(p.RoomID), true) + g.setPlayerFlag(p, "aps_node_"+strconv.Itoa(p.RoomID), true) } } diff --git a/internal/game/cmd_craft.go b/internal/game/cmd_craft.go index 254b938..0fe3d5b 100644 --- a/internal/game/cmd_craft.go +++ b/internal/game/cmd_craft.go @@ -164,7 +164,7 @@ func (g *Game) availableItems(p *player.Player, allItems []*item.ItemDef) []*ite func (g *Game) startItem(sess *net.Session, p *player.Player, def *item.ItemDef, count int, flagKey string) { p.EnsureFlags() - p.Flags[flagKey] = def.ID + g.setPlayerFlag(p, flagKey, def.ID) g.AccountStore.SaveCharacter(p) g.startProductionFromItem(sess, p, def, count) diff --git a/internal/game/combat_attack.go b/internal/game/combat_attack.go index ee7dee8..61eb9c1 100644 --- a/internal/game/combat_attack.go +++ b/internal/game/combat_attack.go @@ -130,6 +130,10 @@ func (g *Game) findMob(sess *net.Session, input string, roomID int) *world.MobIn } func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobInstance) { + if mob.OwnerOnly && mob.Owner != "" && mob.Owner != p.Name { + sess.WriteLine("They don't seem interested in you.") + return + } g.cancelRest(p.Name) g.cancelBgAction(p) diff --git a/internal/game/combat_mob.go b/internal/game/combat_mob.go index e06e307..5558578 100644 --- a/internal/game/combat_mob.go +++ b/internal/game/combat_mob.go @@ -212,10 +212,17 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst respawnTicks = engine.ToTicks(30) } instanceID := mob.InstanceID - g.Ticks.Subscribe(respawnTicks, func() bool { - g.respawnMob(instanceID) - return false - }) + if mob.SpawnedByTrigger { + g.Ticks.Subscribe(10, func() bool { + g.MobStore.RemoveInstance(instanceID) + return false + }) + } else { + g.Ticks.Subscribe(respawnTicks, func() bool { + g.respawnMob(instanceID) + return false + }) + } g.writePrompt(sess) } } diff --git a/internal/game/core_flags.go b/internal/game/core_flags.go index 6595cc4..5e1c812 100644 --- a/internal/game/core_flags.go +++ b/internal/game/core_flags.go @@ -33,9 +33,23 @@ func getPlayerFlagString(p *player.Player, key string) string { return s } -func setPlayerFlag(p *player.Player, key string, val any) { +func (g *Game) setPlayerFlag(p *player.Player, key string, val any) { p.EnsureFlags() + old, existed := p.Flags[key] p.Flags[key] = val + if !existed || !valuesEqual(old, val) { + g.firePlayerFlagTrigger(p, key, val) + } +} + +func (g *Game) firePlayerFlagTrigger(p *player.Player, flagName string, flagValue any) { + g.charsMu.Lock() + sess := g.loggedInChars[p.Name] + g.charsMu.Unlock() + if sess == nil { + return + } + g.TriggerStore.FirePlayerInRoom(p.Name, p.RoomID, flagName, flagValue) } func intFromFlag(flags map[string]any, key string) int { diff --git a/internal/game/flagstore.go b/internal/game/flagstore.go index 2790d89..bc72c60 100644 --- a/internal/game/flagstore.go +++ b/internal/game/flagstore.go @@ -2,18 +2,29 @@ package game import "sync" +// FlagChangeCallback is invoked when a world flag value actually changes +// (old value differs from new, or new flag is created with a truthy value). +type FlagChangeCallback func(name string, value any) + // FlagStore holds world flags — shared mutable state visible to all players // (e.g. opened doors, quest state). Player-specific flags live on // *player.Player.Flags instead. FlagStore is safe for concurrent use. type FlagStore struct { - mu sync.Mutex - flags map[string]any + mu sync.Mutex + flags map[string]any + callbacks []FlagChangeCallback } func NewFlagStore() *FlagStore { return &FlagStore{flags: make(map[string]any)} } +func (f *FlagStore) OnChange(cb FlagChangeCallback) { + f.mu.Lock() + defer f.mu.Unlock() + f.callbacks = append(f.callbacks, cb) +} + func (f *FlagStore) Get(name string) (any, bool) { f.mu.Lock() defer f.mu.Unlock() @@ -23,15 +34,35 @@ func (f *FlagStore) Get(name string) (any, bool) { func (f *FlagStore) Set(name string, value any) { f.mu.Lock() - defer f.mu.Unlock() + old, existed := f.flags[name] f.flags[name] = value + cbs := f.callbacks + f.mu.Unlock() + + if !existed || !valuesEqual(old, value) { + for _, cb := range cbs { + cb(name, value) + } + } } func (f *FlagStore) SetAll(m map[string]any) { f.mu.Lock() - defer f.mu.Unlock() + changed := make(map[string]any) for k, v := range m { + old, existed := f.flags[k] f.flags[k] = v + if !existed || !valuesEqual(old, v) { + changed[k] = v + } + } + cbs := f.callbacks + f.mu.Unlock() + + for k, v := range changed { + for _, cb := range cbs { + cb(k, v) + } } } diff --git a/internal/game/game.go b/internal/game/game.go index 320304f..acd2924 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -52,6 +52,7 @@ type Game struct { Hub *net.Hub Flags *FlagStore Combat *combat.Tracker + TriggerStore *world.TriggerStore queue *CommandQueue safespot *SafespotManager ValidationConfig config.ValidationConfig @@ -84,6 +85,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig, valConfig config.Vali }, Flags: NewFlagStore(), Combat: combat.NewTracker(), + TriggerStore: world.NewTriggerStore(), queue: NewCommandQueue(), safespot: NewSafespotManager(), ValidationConfig: valConfig, @@ -97,6 +99,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig, valConfig config.Vali g.LoadMods() g.LoadTechs() g.buildCraftIndex() + g.seedRoomTriggers() g.ValidateAndLog() return g } @@ -116,6 +119,9 @@ func (g *Game) SetHub(hub *net.Hub) { } } }) + g.Flags.OnChange(func(name string, value any) { + g.TriggerStore.FireWorld(name, value) + }) } func (g *Game) HandleSession(sess *net.Session, input string) { @@ -334,6 +340,18 @@ func (g *Game) buildCraftIndex() { g.CraftIndex.Build(items) } +func (g *Game) seedRoomTriggers() { + g.TriggerStore.LoadGlobal(g.DataDir) + roomIndex := g.World.RoomIndex() + for id := range roomIndex { + room, err := g.World.LoadRoom(id) + if err != nil || len(room.Triggers) == 0 { + continue + } + g.TriggerStore.SeedRoomTriggers(id, room.Triggers) + } +} + type pendingDepletion struct { instanceKey string objDefID string diff --git a/internal/game/production_menu.go b/internal/game/production_menu.go index 7181a0e..c1d9a96 100644 --- a/internal/game/production_menu.go +++ b/internal/game/production_menu.go @@ -258,7 +258,7 @@ func (g *Game) handleProductChoice(sess *net.Session, input string) { } p.EnsureFlags() - p.Flags[lastFlagKey] = sel.Item.ID + g.setPlayerFlag(p, lastFlagKey, sel.Item.ID) g.AccountStore.SaveCharacter(p) if background { diff --git a/internal/game/sys_assassin.go b/internal/game/sys_assassin.go index 1927399..720529c 100644 --- a/internal/game/sys_assassin.go +++ b/internal/game/sys_assassin.go @@ -51,9 +51,9 @@ func (g *Game) assignAssassinTask(sess *net.Session, p *player.Player) { } } count := chosen.MinCount + rand.Intn(chosen.MaxCount-chosen.MinCount+1) - setPlayerFlag(p, "assassin_task_mob", chosen.MobID) - setPlayerFlag(p, "assassin_task_total", count) - setPlayerFlag(p, "assassin_task_remaining", count) + g.setPlayerFlag(p, "assassin_task_mob", chosen.MobID) + g.setPlayerFlag(p, "assassin_task_total", count) + g.setPlayerFlag(p, "assassin_task_remaining", count) g.AccountStore.SaveCharacter(p) def, err := g.MobStore.LoadDef(chosen.MobID) @@ -79,20 +79,20 @@ func (g *Game) onAssassinKill(sess *net.Session, p *player.Player, mob *world.Mo sess.WriteLine(g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp asm)", xp))) } remaining-- - setPlayerFlag(p, "assassin_task_remaining", remaining) + g.setPlayerFlag(p, "assassin_task_remaining", remaining) if remaining <= 0 { completed := getPlayerFlagInt(p, "assassin_tasks_completed") + 1 streak := getPlayerFlagInt(p, "assassin_streak") + 1 - setPlayerFlag(p, "assassin_tasks_completed", completed) - setPlayerFlag(p, "assassin_streak", streak) + g.setPlayerFlag(p, "assassin_tasks_completed", completed) + g.setPlayerFlag(p, "assassin_streak", streak) delete(p.Flags, "assassin_task_mob") - setPlayerFlag(p, "assassin_task_remaining", 0) - setPlayerFlag(p, "assassin_task_total", 0) + g.setPlayerFlag(p, "assassin_task_remaining", 0) + g.setPlayerFlag(p, "assassin_task_total", 0) rep := 1 bonus := streakBonus(streak) rep += bonus currentRep := getPlayerFlagInt(p, "assassin_reputation") - setPlayerFlag(p, "assassin_reputation", currentRep+rep) + g.setPlayerFlag(p, "assassin_reputation", currentRep+rep) sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf("\n*** Assassin task complete! ***"))) if bonus > 0 { sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf(" Streak bonus! %d tasks in a row. +%d bonus reputation.", streak, bonus))) @@ -116,11 +116,11 @@ func (g *Game) skipAssassinTask(sess *net.Session, p *player.Player) { sess.WriteLine("You don't have enough Reputation to skip. (Need 30, have " + fmt.Sprint(rep) + ")") return } - setPlayerFlag(p, "assassin_reputation", rep-30) + g.setPlayerFlag(p, "assassin_reputation", rep-30) delete(p.Flags, "assassin_task_mob") - setPlayerFlag(p, "assassin_task_remaining", 0) - setPlayerFlag(p, "assassin_task_total", 0) - setPlayerFlag(p, "assassin_streak", 0) + g.setPlayerFlag(p, "assassin_task_remaining", 0) + g.setPlayerFlag(p, "assassin_task_total", 0) + g.setPlayerFlag(p, "assassin_streak", 0) g.AccountStore.SaveCharacter(p) } @@ -135,15 +135,15 @@ func (g *Game) extendAssassinTask(sess *net.Session, p *player.Player) { sess.WriteLine("You don't have an active task to extend.") return } - setPlayerFlag(p, "assassin_reputation", rep-30) + g.setPlayerFlag(p, "assassin_reputation", rep-30) total := getPlayerFlagInt(p, "assassin_task_total") remaining := getPlayerFlagInt(p, "assassin_task_remaining") extension := total / 2 if extension < 5 { extension = 5 } - setPlayerFlag(p, "assassin_task_total", total+extension) - setPlayerFlag(p, "assassin_task_remaining", remaining+extension) + g.setPlayerFlag(p, "assassin_task_total", total+extension) + g.setPlayerFlag(p, "assassin_task_remaining", remaining+extension) g.AccountStore.SaveCharacter(p) def, _ := g.MobStore.LoadDef(taskMob) diff --git a/internal/game/triggers.go b/internal/game/triggers.go new file mode 100644 index 0000000..76ce9cd --- /dev/null +++ b/internal/game/triggers.go @@ -0,0 +1,291 @@ +package game + +import ( + "fmt" + "strings" + + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +func (g *Game) TriggerSeqTick() { + g.processPlayerTriggerSequences() + g.processWorldTriggerSequences() +} + +func (g *Game) TransientMobTick() { + if g.Hub == nil { + return + } + for _, inst := range g.MobStore.AllInstances() { + if inst.Owner == "" || inst.HP <= 0 { + continue + } + if !inst.DespawnOnLeave { + continue + } + + g.charsMu.Lock() + ownerSess := g.loggedInChars[inst.Owner] + g.charsMu.Unlock() + if ownerSess == nil || ownerSess.Player == nil { + continue + } + + ownerRoom := ownerSess.Player.RoomID + allowed := inst.DespawnRooms + if len(allowed) == 0 { + allowed = []int{inst.HomeRoomID} + } + inAllowed := false + for _, r := range allowed { + if ownerRoom == r { + inAllowed = true + break + } + } + if inAllowed { + inst.DespawnCounter = inst.DespawnTickCount() + continue + } + inst.DecrementDespawnCounter() + if inst.DespawnCounter <= 0 { + g.MobStore.RemoveInstance(inst.InstanceID) + for _, sess := range g.Hub.PlayersInRoom(inst.RoomID) { + sess.WriteLine(g.colorize(sess, "broadcast", + fmt.Sprintf("\n%s fades away.", mobDisplayName(inst, true)))) + } + } + } +} + +func (g *Game) processPlayerTriggerSequences() { + seqs := g.TriggerStore.SnapshotPlayerSeqs() + for _, seq := range seqs { + g.charsMu.Lock() + sess := g.loggedInChars[seq.PlayerName] + g.charsMu.Unlock() + if sess == nil || sess.Player == nil { + g.TriggerStore.RemovePlayerSeq(seq.PlayerName, seq.TriggerID) + continue + } + p := sess.Player + if p.RoomID != seq.RoomID { + continue + } + + seq.Wait-- + if seq.Wait > 0 { + continue + } + + var jobs []world.TriggerStep + for len(seq.Steps) > 0 && seq.Wait <= 0 { + step := seq.Steps[0] + seq.Steps = seq.Steps[1:] + jobs = append(jobs, step) + if len(seq.Steps) > 0 { + seq.Wait = seq.Steps[0].Delay + } + } + + for _, step := range jobs { + g.executeTriggerStep(sess, p, &step, seq.RoomID, seq.FlagValue) + } + + if len(seq.Steps) == 0 { + g.TriggerStore.RemovePlayerSeq(seq.PlayerName, seq.TriggerID) + if p.RoomID == seq.RoomID && sess.State == net.StateGame { + g.writePrompt(sess) + } + } + } +} + +func (g *Game) processWorldTriggerSequences() { + seqs := g.TriggerStore.SnapshotWorldSeqs() + for _, seq := range seqs { + seq.Wait-- + if seq.Wait > 0 { + continue + } + + var jobs []world.TriggerStep + for len(seq.Steps) > 0 && seq.Wait <= 0 { + step := seq.Steps[0] + seq.Steps = seq.Steps[1:] + jobs = append(jobs, step) + if len(seq.Steps) > 0 { + seq.Wait = seq.Steps[0].Delay + } + } + + for _, step := range jobs { + g.executeWorldTriggerStep(&step, seq.RoomID, seq.FlagValue) + } + + if len(seq.Steps) == 0 { + g.TriggerStore.RemoveWorldSeq(seq.TriggerID) + } + } +} + +func (g *Game) executeTriggerStep(sess *net.Session, p *player.Player, step *world.TriggerStep, roomID int, flagValue any) { + if step.Message != "" { + msg := expandTemplate(step.Message, p.Name, flagValue) + sess.WriteLine(msg) + } + if step.Broadcast != "" && g.Hub != nil { + msg := expandTemplate(step.Broadcast, p.Name, flagValue) + for _, other := range g.Hub.PlayersInRoom(roomID) { + other.WriteLine(g.colorize(other, "broadcast", "\n"+msg)) + } + } + if step.BroadcastGlobal != "" && g.Hub != nil { + msg := expandTemplate(step.BroadcastGlobal, p.Name, flagValue) + for _, other := range g.Hub.AllSessions() { + if other.Player != nil { + other.WriteLine(g.colorize(other, "broadcast", "\n"+msg)) + } + } + } + if len(step.SetFlags) > 0 { + g.Flags.SetAll(step.SetFlags) + } + if len(step.SetPlayerFlags) > 0 { + for k, v := range step.SetPlayerFlags { + g.setPlayerFlag(p, k, v) + } + g.AccountStore.SaveCharacter(p) + } + if step.GiveItem != "" { + slot := p.FirstFreeSlot() + if slot == -1 { + sess.WriteLine("Your inventory is too full to receive that.") + } else { + p.SetInvSlot(slot, &player.InventorySlot{ItemID: step.GiveItem, Quantity: 1}) + g.AccountStore.SaveCharacter(p) + } + } + if step.TakeItem != "" { + if p.HasItem(step.TakeItem) { + p.RemoveItem(step.TakeItem, 1) + g.AccountStore.SaveCharacter(p) + } + } + if step.Heal > 0 { + p.HP += step.Heal + if maxHP := p.MaxHP(); p.HP > maxHP { + p.HP = maxHP + } + g.AccountStore.SaveCharacter(p) + sess.WriteLine(fmt.Sprintf("You regain %d hitpoints.", step.Heal)) + } + if step.SpawnMob != nil { + g.spawnTriggerMob(sess, p, step.SpawnMob, roomID) + } + if step.DespawnMob != "" { + g.despawnTriggerMobs(step.DespawnMob, p.Name) + } + if step.Teleport > 0 { + g.teleportPlayer(sess, p, step.Teleport) + } +} + +func (g *Game) executeWorldTriggerStep(step *world.TriggerStep, roomID int, flagValue any) { + if step.Broadcast != "" && g.Hub != nil { + msg := expandTemplate(step.Broadcast, "", flagValue) + for _, other := range g.Hub.PlayersInRoom(roomID) { + other.WriteLine(g.colorize(other, "broadcast", "\n"+msg)) + } + } + if step.BroadcastGlobal != "" && g.Hub != nil { + msg := expandTemplate(step.BroadcastGlobal, "", flagValue) + for _, other := range g.Hub.AllSessions() { + if other.Player != nil { + other.WriteLine(g.colorize(other, "broadcast", "\n"+msg)) + } + } + } + if len(step.SetFlags) > 0 { + g.Flags.SetAll(step.SetFlags) + } + if step.SpawnMob != nil { + g.spawnWorldTriggerMob(step.SpawnMob, roomID) + } + if step.DespawnMob != "" { + g.despawnTriggerMobs(step.DespawnMob, "") + } +} + +func (g *Game) spawnTriggerMob(sess *net.Session, p *player.Player, cfg *world.SpawnMobConfig, roomID int) { + if cfg.ID == "" { + return + } + def, err := g.MobStore.LoadDef(cfg.ID) + if err != nil { + return + } + inst := g.MobStore.SpawnTransient(def, cfg, roomID, p.Name) + if inst == nil { + return + } + inst.ResetDespawnCounter(engine.ToTicks(cfg.DespawnTicks)) + sess.WriteLine(g.colorize(sess, "broadcast", "\n"+mobDisplayName(inst, true)+" appears!")) +} + +func (g *Game) spawnWorldTriggerMob(cfg *world.SpawnMobConfig, roomID int) { + if cfg.ID == "" { + return + } + def, err := g.MobStore.LoadDef(cfg.ID) + if err != nil { + return + } + inst := g.MobStore.SpawnTransient(def, cfg, roomID, "") + if inst == nil { + return + } + inst.ResetDespawnCounter(engine.ToTicks(cfg.DespawnTicks)) + if g.Hub != nil { + for _, sess := range g.Hub.PlayersInRoom(roomID) { + sess.WriteLine(g.colorize(sess, "broadcast", + "\n"+mobDisplayName(inst, true)+" appears!")) + } + } +} + +func (g *Game) despawnTriggerMobs(mobID string, owner string) { + for _, inst := range g.MobStore.AllInstances() { + if inst.DefID != mobID { + continue + } + if owner != "" && inst.Owner != owner { + continue + } + if inst.HP <= 0 { + g.MobStore.RemoveInstance(inst.InstanceID) + continue + } + if g.Hub != nil { + for _, sess := range g.Hub.PlayersInRoom(inst.RoomID) { + sess.WriteLine(g.colorize(sess, "broadcast", + fmt.Sprintf("\n%s vanishes.", mobDisplayName(inst, true)))) + } + } + g.MobStore.RemoveInstance(inst.InstanceID) + } +} + +func expandTemplate(tmpl string, playerName string, flagValue any) string { + s := tmpl + if playerName != "" { + s = strings.ReplaceAll(s, "%p", playerName) + } + if flagValue != nil { + s = strings.ReplaceAll(s, "%v", fmt.Sprint(flagValue)) + } + return s +} |
