From abd612c15799f604e671e83dc7c410ed2b44185f Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Thu, 25 Jun 2026 15:40:48 -0400 Subject: slop refactor --- internal/game/act_room.go | 220 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 internal/game/act_room.go (limited to 'internal/game/act_room.go') diff --git a/internal/game/act_room.go b/internal/game/act_room.go new file mode 100644 index 0000000..86c1737 --- /dev/null +++ b/internal/game/act_room.go @@ -0,0 +1,220 @@ +package game + +import ( + "fmt" + "strings" + + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +// enterSeq is an in-flight on_enter cutscene for a single player. It is driven +// one step at a time by EnterSeqTick. +type enterSeq struct { + sess *net.Session + playerName string + roomID int + steps []world.EnterStep + wait int + started bool +} + +// runEnterSteps evaluates a room's on_enter script for the player. Steps whose +// condition fails are dropped. If none of the surviving steps are "timed" +// (no delay, no flag mutation) the messages are printed synchronously, exactly +// as before. Otherwise the surviving steps become a scheduled cutscene driven +// by EnterSeqTick. +func (g *Game) runEnterSteps(sess *net.Session, roomID int) { + room, err := g.World.LoadRoom(roomID) + if err != nil || len(room.OnEnter) == 0 { + return + } + p := sess.Player + if p == nil { + return + } + + var steps []world.EnterStep + cutscene := false + for _, step := range room.OnEnter { + if step.Condition != nil && !g.checkCondition(sess, step.Condition) { + continue + } + steps = append(steps, step) + if step.IsTimed() { + cutscene = true + } + } + if len(steps) == 0 { + return + } + + if !cutscene { + for _, step := range steps { + if step.Message != "" { + sess.WriteLine(step.Message) + } + } + return + } + + g.startEnterCutscene(sess, p, roomID, steps) +} + +// startEnterCutscene registers a scheduled on_enter sequence and persists a +// marker on the player so the cutscene can be resumed on reconnect if it is +// interrupted (disconnect, server restart). +func (g *Game) startEnterCutscene(sess *net.Session, p *player.Player, roomID int, steps []world.EnterStep) { + g.cancelEnterSeq(p.Name) + if p.CutsceneRoom != roomID { + p.CutsceneRoom = roomID + g.AccountStore.SaveCharacter(p) + } + g.enterMu.Lock() + g.enterSeqs[p.Name] = &enterSeq{ + sess: sess, + playerName: p.Name, + roomID: roomID, + steps: steps, + wait: steps[0].Delay, + } + g.enterMu.Unlock() +} + +// EnterSeqTick advances every in-flight cutscene by one tick. Sequences for a +// player who is no longer connected are paused (left for resume); sequences are +// removed from the map on completion or cancellation. +func (g *Game) EnterSeqTick() { + g.enterMu.Lock() + seqs := make([]*enterSeq, 0, len(g.enterSeqs)) + for _, s := range g.enterSeqs { + seqs = append(seqs, s) + } + g.enterMu.Unlock() + + for _, seq := range seqs { + if !g.isCharLive(seq.playerName, seq.sess) { + continue + } + + var jobs []world.EnterStep + done := false + + g.enterMu.Lock() + if g.enterSeqs[seq.playerName] != seq { + g.enterMu.Unlock() + continue + } + seq.wait-- + for seq.wait <= 0 && len(seq.steps) > 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 + } else { + seq.wait = 0 + } + } + if len(seq.steps) == 0 { + done = true + delete(g.enterSeqs, seq.playerName) + } + g.enterMu.Unlock() + + for _, step := range jobs { + g.fireEnterStep(seq, step) + } + if done { + g.completeEnterSeq(seq) + } + } +} + +func (g *Game) fireEnterStep(seq *enterSeq, step world.EnterStep) { + p := seq.sess.Player + if p == nil || p.RoomID != seq.roomID { + return + } + if step.Message != "" { + // Break from the prompt on the first line only, so a multi-line + // cutscene reads as tight consecutive lines. + if !seq.started { + seq.sess.WriteLine("\n" + step.Message) + seq.started = true + } else { + seq.sess.WriteLine(step.Message) + } + } + if len(step.SetFlags) > 0 || len(step.SetPlayerFlags) > 0 { + g.applyFlagMutations(p, step.SetFlags, step.SetPlayerFlags) + g.AccountStore.SaveCharacter(p) + } +} + +func (g *Game) completeEnterSeq(seq *enterSeq) { + if !g.isCharLive(seq.playerName, seq.sess) { + return + } + p := seq.sess.Player + if p == nil { + return + } + if p.CutsceneRoom == seq.roomID { + p.CutsceneRoom = 0 + g.AccountStore.SaveCharacter(p) + } + g.writePrompt(seq.sess) +} + +// cancelEnterSeq drops any in-flight cutscene for the player. It does not touch +// the persisted CutsceneRoom marker, so a cutscene cancelled by disconnect can +// still be resumed on reconnect. +func (g *Game) cancelEnterSeq(name string) { + g.enterMu.Lock() + delete(g.enterSeqs, name) + g.enterMu.Unlock() +} + +func (g *Game) isCharLive(name string, sess *net.Session) bool { + g.charsMu.Lock() + defer g.charsMu.Unlock() + return g.loggedInChars[name] == sess +} + +// applyFlagMutations sets world and player flags, shared by on_enter steps and +// exit traversal. +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 + } + } +} + +func (g *Game) BroadcastRespawns() { + respawns := g.World.FlushObjRespawns() + for _, st := range respawns { + def, err := g.ObjectStore.Load(st.DefID) + if err != nil || !def.IsGatherable() { + continue + } + cfg := def.Gather + if cfg.RespawnBroadcast == "" { + continue + } + name := def.Name + if idx := st.Index + 1; len(g.World.FindObjInstances(st.RoomID, st.DefID)) > 1 { + name += fmt.Sprintf(" [%d]", idx) + } + msg := strings.ReplaceAll(cfg.RespawnBroadcast, "{name}", name) + if g.Hub != nil { + for _, sess := range g.Hub.PlayersInRoom(st.RoomID) { + sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s", msg))) + } + } + } +} -- cgit v1.2.3