package game import ( "fmt" "strings" "thehouseoficarus/internal/color" "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("%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 { s := step g.executeTriggerStep(sess, p, &s, 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) { seqSpec := g.resolveColor(sess, "sequence") broadcastSpec := g.resolveColor(sess, "broadcast") playerMode := g.colorMode(sess) if step.Message != "" { msg := expandTemplate(step.Message, p.Name, flagValue) msg = color.ExpandTagsDefault(playerMode, seqSpec, msg) sess.WriteLine(msg) } if step.Broadcast != "" && g.Hub != nil { msg := expandTemplate(step.Broadcast, p.Name, flagValue) for _, other := range g.Hub.PlayersInRoom(roomID) { otherMode := g.colorMode(other) rendered := color.ExpandTagsDefault(otherMode, broadcastSpec, msg) other.WriteLine(g.colorize(other, "broadcast", rendered)) } } if step.BroadcastGlobal != "" && g.Hub != nil { msg := expandTemplate(step.BroadcastGlobal, p.Name, flagValue) for _, other := range g.Hub.AllSessions() { if other.Player != nil { otherMode := g.colorMode(other) rendered := color.ExpandTagsDefault(otherMode, broadcastSpec, msg) other.WriteLine(g.colorize(other, "broadcast", rendered)) } } } 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) broadcastSpec := g.resolveColor(nil, "broadcast") for _, other := range g.Hub.PlayersInRoom(roomID) { otherMode := g.colorMode(other) rendered := color.ExpandTagsDefault(otherMode, broadcastSpec, msg) other.WriteLine(g.colorize(other, "broadcast", rendered)) } } if step.BroadcastGlobal != "" && g.Hub != nil { msg := expandTemplate(step.BroadcastGlobal, "", flagValue) broadcastSpec := g.resolveColor(nil, "broadcast") for _, other := range g.Hub.AllSessions() { if other.Player != nil { otherMode := g.colorMode(other) rendered := color.ExpandTagsDefault(otherMode, broadcastSpec, msg) other.WriteLine(g.colorize(other, "broadcast", rendered)) } } } 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", 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", 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("%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 }