package game import ( "fmt" "strings" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" ) func (g *Game) TriggerSeqTick() { g.processPlayerTriggerSequences() g.processGlobalTriggerSequences() } 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 []behavior.StepAction 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 i := range jobs { g.executeTriggerStep(sess, p, &jobs[i], 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) processGlobalTriggerSequences() { seqs := g.TriggerStore.SnapshotGlobalSeqs() for _, seq := range seqs { seq.Wait-- if seq.Wait > 0 { continue } var jobs []behavior.StepAction 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 i := range jobs { g.executeGlobalTriggerStep(&jobs[i], seq.RoomID, seq.FlagValue) } if len(seq.Steps) == 0 { g.TriggerStore.RemoveGlobalSeq(seq.TriggerID) } } } // executeTriggerStep is a thin wrapper over applyStepAction for trigger player // sequences (full player-targeted effects + sequence/broadcast/spawn fields). func (g *Game) executeTriggerStep(sess *net.Session, p *player.Player, step *behavior.StepAction, roomID int, flagValue any) { // On every step, evaluate the per-step condition (a trigger can now gate // individual steps via the Condition field inherited from StepAction). if step.Condition != nil && !g.checkCondition(sess, step.Condition) { return } g.applyStepAction(sess, p, step, roomID, scopePlayerSeq, flagValue) } // executeGlobalTriggerStep is a thin wrapper over applyStepAction for global // trigger sequences (no player; only broadcasts/global-flags/spawn/despawn). func (g *Game) executeGlobalTriggerStep(step *behavior.StepAction, roomID int, flagValue any) { g.applyStepAction(nil, nil, step, roomID, scopeGlobal, flagValue) } 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 }