aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/act_agility.go2
-rw-r--r--internal/game/act_farm.go80
-rw-r--r--internal/game/act_fletch.go2
-rw-r--r--internal/game/act_room.go3
-rw-r--r--internal/game/act_steal.go4
-rw-r--r--internal/game/act_talk.go11
-rw-r--r--internal/game/cmd_craft.go2
-rw-r--r--internal/game/combat_attack.go4
-rw-r--r--internal/game/combat_mob.go15
-rw-r--r--internal/game/core_flags.go16
-rw-r--r--internal/game/flagstore.go39
-rw-r--r--internal/game/game.go18
-rw-r--r--internal/game/production_menu.go2
-rw-r--r--internal/game/sys_assassin.go32
-rw-r--r--internal/game/triggers.go291
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
+}