aboutsummaryrefslogtreecommitdiff
path: root/internal/game/action_room.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-24 20:58:25 -0400
committerhistoria <[not public]>2026-06-24 20:58:25 -0400
commit9d4b799db4868bcab291b83599ff088763cd4ed6 (patch)
tree252f0fcc779acf35243983d643d6399ee2e0cd0b /internal/game/action_room.go
parentd25638d98fe63efdeab570ef77e6a6a97f2d7a60 (diff)
downloadthehouseoficarus-9d4b799db4868bcab291b83599ff088763cd4ed6.tar.gz
feat: new type of non-violent 'combat': work
Diffstat (limited to 'internal/game/action_room.go')
-rw-r--r--internal/game/action_room.go179
1 files changed, 177 insertions, 2 deletions
diff --git a/internal/game/action_room.go b/internal/game/action_room.go
index af5b99b..6c886c1 100644
--- a/internal/game/action_room.go
+++ b/internal/game/action_room.go
@@ -5,19 +5,194 @@ import (
"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
}
- if step.Message != "" {
- sess.WriteLine(fmt.Sprintf("%s", step.Message))
+ 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) {
+ for k, v := range setFlags {
+ g.WorldFlags[k] = v
+ }
+ if len(setPlayerFlags) > 0 {
+ p.EnsureFlags()
+ for k, v := range setPlayerFlags {
+ p.Flags[k] = v
}
}
}