aboutsummaryrefslogtreecommitdiff
path: root/internal/game/act_effects.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-09 22:15:54 -0400
committerhistoria <[not public]>2026-07-09 22:15:54 -0400
commitecba7f726f70b37126d852c38c7e3eec7b04d730 (patch)
tree9129215c6e5015336fde1116395336d82bb952d1 /internal/game/act_effects.go
parentb3d4c616f59ad2519f3a0b77e3b47d6571cd2486 (diff)
downloadthehouseoficarus-ecba7f726f70b37126d852c38c7e3eec7b04d730.tar.gz
feat: old standalone trigger systems completely unified into trigger->condition->action system
Diffstat (limited to 'internal/game/act_effects.go')
-rw-r--r--internal/game/act_effects.go203
1 files changed, 78 insertions, 125 deletions
diff --git a/internal/game/act_effects.go b/internal/game/act_effects.go
index 9fce264..cb60c5d 100644
--- a/internal/game/act_effects.go
+++ b/internal/game/act_effects.go
@@ -10,74 +10,47 @@ import (
"thehouseoficarus/internal/player"
)
-// effectScope controls which subset of a StepAction's effects applyStepAction
-// will fire. The three scopes cover the three callers of the unified executor:
+// effectScope controls which subset of a Step's effects applyStep will fire.
//
-// - scopePlayer: inline triggers (talk nodes, on_use, on_look,
-// on_kill, exit traversal). Receives all
-// player-targeted effects (flags, items, heal, credits,
-// teleport, aps_node). Broadcast is allowed (so on_kill
-// can announce to the room); broadcast_global is not
-// (there's no per-tick sequence owner to author it).
-// - scopePlayerSeq: on_enter steps + trigger player sequences. Same as
-// scopePlayer plus broadcast_global (a sequence can
-// shout to the whole world) and spawn_mob/despawn_mob
-// (player-owned transient mobs).
-// - scopeGlobal: trigger global sequences. No player is involved; only
-// broadcasts, global flag mutations, and world-owned
-// spawn/despawn fire.
+// - scopePlayer: every player-bound trigger (on_use, on_look, on_kill,
+// on_enter, on_exit, on_traverse, on_flag_change). Receives
+// the full effect vocabulary: messages, broadcast,
+// broadcast_global, flags, items, heal, credits, mob
+// spawn/despawn (player-owned), teleport, aps_node.
+// - scopeGlobal: global-flag triggers (on_global_flag_change) with no
+// originating player. Only broadcasts, global flag
+// mutations, and world-owned mob spawn/despawn fire.
type effectScope int
const (
scopePlayer effectScope = iota
- scopePlayerSeq
scopeGlobal
)
-// applyStepAction is the single effect executor used by every conditional
-// interaction and timed step in the game: talk nodes, talk options,
-// on_use/on_look/on_kill interactions, exit traversal, on_enter steps, and
-// room/global trigger sequences. The previous fireEnterStep /
-// executeTriggerStep / executeGlobalTriggerStep / applyNodeAction executors
-// are all thin wrappers over this one.
+// applyStep is the single effect executor used by every trigger in the game:
+// talk nodes/options, on_use/on_look/on_kill/on_enter/on_exit/on_traverse
+// triggers, and on_flag_change/on_global_flag_change sequences. scopeGlobal
+// ignores the player entirely.
//
// flagValue (may be nil) is substituted into %v in any message/broadcast
-// template; playerName (derived from p when non-nil) is substituted into %p.
-// scopeGlobal ignores p entirely.
+// template; the player's name is substituted into %p.
//
-// The fixed effect order (matching the original executors, normalized):
-// 1. messages (player/seq scopes — skipped for scopeGlobal)
-// 2. broadcast → all in room (seq/global scopes)
-// 3. broadcast_global → all online (seq/global scopes)
-// 4. set_global_flags (scopeGlobal only; player scopes handle globals in step 5
-// via applyFlagMutations so they aren't set twice)
-// 5. set_player_flags (player/seq scopes; cascades via setPlayerFlag)
-// 6. take_item (player/seq scopes)
-// 7. give_item (player/seq scopes; honors 28-slot inventory limit)
-// 8. heal (player/seq scopes; clamps to MaxHP)
-// 9. credits (player/seq scopes; negative is gated by affordability)
-// 10. spawn_mob (seq/global scopes; player-owned when seq)
-// 11. despawn_mob (seq/global scopes; filtered by owner when seq)
-// 12. teleport (player/seq scopes; re-runs look + on_enter of target)
-// 13. aps_node (player/seq scopes; marks "aps_node_<roomID>" player flag)
-
-// writePlayerMessage expands the message template (player name, flag value),
-// colorizes inline {spec}text{/} tags under the caller's color mode, and writes
-// it to sess. It is the single path used by every per-player message emission
-// in the interaction system (applyStepAction step 1, the interaction sequencer,
-// and the advance handlers) so color tags render consistently.
-func (g *Game) writePlayerMessage(sess *net.Session, msg string, playerName string, flagValue any) {
- if sess == nil || msg == "" {
- return
- }
- rendered := expandTemplate(msg, playerName, flagValue)
- seqSpec := g.resolveColor(sess, "sequence")
- mode := g.colorMode(sess)
- rendered = color.ExpandTagsDefault(mode, seqSpec, rendered)
- sess.WriteLine(rendered)
-}
-
-func (g *Game) applyStepAction(sess *net.Session, p *player.Player, step *behavior.StepAction, roomID int, sc effectScope, flagValue any) {
+// The fixed effect order:
+// 1. messages (player scope — skipped for scopeGlobal)
+// 2. broadcast -> all in room (both scopes)
+// 3. broadcast_global -> all online (both scopes)
+// 4. set_global_flags (scopeGlobal sets here; player scope folds globals
+// into step 5 via applyFlagMutations so they aren't set twice)
+// 5. set_player_flags (player scope; cascades via setPlayerFlag)
+// 6. take_item (player scope)
+// 7. give_item (player scope; honors 28-slot inventory limit)
+// 8. heal (player scope; clamps to MaxHP)
+// 9. credits (player scope; negative is gated by affordability)
+// 10. spawn_mob (player scope: player-owned; global scope: world-owned)
+// 11. despawn_mob (player scope: owner-filtered; global scope: all)
+// 12. teleport (player scope; re-runs look + on_enter of target)
+// 13. aps_node (player scope; marks "aps_node_<roomID>" player flag)
+func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Step, roomID int, sc effectScope, flagValue any) {
if step == nil {
return
}
@@ -87,20 +60,16 @@ func (g *Game) applyStepAction(sess *net.Session, p *player.Player, step *behavi
playerName = p.Name
}
- // 1. messages — direct to the triggering session. Skipped for scopeGlobal
- // (no per-player session is the target). All Messages in the step emit in
- // order; per-message delays are ignored here (they're honored by the
- // interaction sequencer or handled as inter-step delays by on_enter/triggers).
+ // 1. messages — direct to the triggering session.
if sc != scopeGlobal {
for _, msg := range step.Messages {
- g.writePlayerMessage(sess, msg.Message, playerName, flagValue)
+ g.writePlayerMessage(sess, msg, playerName, flagValue)
}
}
- // 2 & 3. broadcast / broadcast_global — re-render color tags per recipient
- // so each player sees them in their own color mode.
- broadcastSpec := g.resolveColor(nil, "broadcast")
- if (sc == scopePlayerSeq || sc == scopeGlobal) && g.Hub != nil {
+ // 2 & 3. broadcast / broadcast_global — re-render color tags per recipient.
+ if g.Hub != nil && (step.Broadcast != "" || step.BroadcastGlobal != "") {
+ broadcastSpec := g.resolveColor(nil, "broadcast")
if step.Broadcast != "" {
raw := expandTemplate(step.Broadcast, playerName, flagValue)
for _, other := range g.Hub.PlayersInRoom(roomID) {
@@ -122,16 +91,13 @@ func (g *Game) applyStepAction(sess *net.Session, p *player.Player, step *behavi
}
}
- // 4. set_global_flags — scopeGlobal has no player, so it sets globals here
- // (cascades via OnChange). Player scopes handle globals in step 5 alongside
- // player flags (applyFlagMutations sets both atomically per-scope), avoiding
- // a redundant second SetAll.
+ // 4. set_global_flags — scopeGlobal has no player, set globals here.
if sc == scopeGlobal && len(step.SetGlobalFlags) > 0 {
g.GlobalFlags.SetAll(step.SetGlobalFlags)
}
- // Global triggers have no player — stop after the world-scoped effects.
if sc == scopeGlobal {
+ // 10/11. world-owned spawn/despawn.
if step.SpawnMob != nil {
g.spawnWorldTriggerMob(step.SpawnMob, roomID)
}
@@ -141,13 +107,11 @@ func (g *Game) applyStepAction(sess *net.Session, p *player.Player, step *behavi
return
}
- // Everything below needs a player.
if p == nil {
return
}
- // 5. set_player_flags — cascade via setPlayerFlag (may synchronously re-fire
- // other player-flag triggers).
+ // 5. set_player_flags (and globals, folded in so the SetAll happens once).
if len(step.SetGlobalFlags) > 0 || len(step.SetPlayerFlags) > 0 {
g.applyFlagMutations(p, step.SetGlobalFlags, step.SetPlayerFlags)
g.AccountStore.SaveCharacter(p)
@@ -186,7 +150,7 @@ func (g *Game) applyStepAction(sess *net.Session, p *player.Player, step *behavi
}
}
- // 9. credits (signed; negative is gated)
+ // 9. credits (signed; negative is gated).
if step.Credits != 0 {
if step.Credits < 0 {
if p.Credits >= -step.Credits {
@@ -201,12 +165,11 @@ func (g *Game) applyStepAction(sess *net.Session, p *player.Player, step *behavi
}
}
- // 10/11. spawn_mob / despawn_mob — sequence-only effects. Inline callers
- // (scopePlayer) leave these empty; nothing fires.
- if sc == scopePlayerSeq && step.SpawnMob != nil {
+ // 10/11. spawn_mob / despawn_mob (player-owned / owner-filtered).
+ if step.SpawnMob != nil {
g.spawnTriggerMob(sess, p, step.SpawnMob, roomID)
}
- if sc == scopePlayerSeq && step.DespawnMob != "" {
+ if step.DespawnMob != "" {
g.despawnTriggerMobs(step.DespawnMob, p.Name)
}
@@ -215,68 +178,58 @@ func (g *Game) applyStepAction(sess *net.Session, p *player.Player, step *behavi
g.teleportPlayer(sess, p, step.Teleport)
}
- // 13. aps_node — marks this current room as a discovered APS node on the
- // player's datapad. Kept as a dedicated effect because the room-id
- // substitution is dynamic (the player's current room), not expressible
- // via a static set_player_flags entry.
+ // 13. aps_node — dynamic room-id flag, kept as a dedicated effect.
if step.ApsNode {
g.setPlayerFlag(p, "aps_node_"+strconv.Itoa(p.RoomID), true)
g.AccountStore.SaveCharacter(p)
}
}
-// applyNodeAction is the inline-only convenience wrapper used by talk nodes,
-// talk options, and any historical caller that holds a *NodeAction rather
-// than a *StepAction. It wraps the NodeAction in a StepAction (sequence-only
-// fields empty) and dispatches to applyStepAction with scopePlayer scoped
-// to the player's current room.
-func (g *Game) applyNodeAction(sess *net.Session, na *behavior.NodeAction) {
- p := sess.Player
- if p == nil || na == nil {
+// writePlayerMessage expands the message template (player name, flag value),
+// colorizes inline {spec}text{/} tags under the caller's color mode, and writes
+// it to sess. It is the single path used by every per-player message emission
+// in the trigger system.
+func (g *Game) writePlayerMessage(sess *net.Session, msg string, playerName string, flagValue any) {
+ if sess == nil || msg == "" {
return
}
- g.applyStepAction(sess, p, &behavior.StepAction{NodeAction: *na}, p.RoomID, scopePlayer, nil)
+ rendered := expandTemplate(msg, playerName, flagValue)
+ seqSpec := g.resolveColor(sess, "sequence")
+ mode := g.colorMode(sess)
+ rendered = color.ExpandTagsDefault(mode, seqSpec, rendered)
+ sess.WriteLine(rendered)
}
-// applyInteraction fires the first matching entry in a list of conditional
-// interactions (on_use, on_look, on_kill, exit traversal). Returns true when
-// an entry matched and fired. The action runs at scopePlayer (or scopePlayerSeq
-// if allowSeq is set, for on_kill which may broadcast/spawn).
-//
-// itemMatch (optional) gates entries that carry an item_id: for on_look it
-// requires the player to have the item in inventory; for on_kill it requires
-// the player to be wielding it. nil means the item_id field is ignored (used
-// by callers without an item filter, e.g. exit traversal). Entries are walked
-// top-to-bottom; the first whose item filter, Condition, and the first-match-
-// wins rule all pass fires.
-func (g *Game) applyInteraction(sess *net.Session, p *player.Player, list []behavior.Interaction, roomID int, allowSeq bool, itemMatch func(string) bool) bool {
- sc := scopePlayer
- if allowSeq {
- sc = scopePlayerSeq
+// applyTalkStep fires a talk node/option's Action (a single Step)
+// synchronously at scopePlayer, scoped to the player's current room.
+func (g *Game) applyTalkStep(sess *net.Session, step *behavior.Step) {
+ p := sess.Player
+ if p == nil || step == nil {
+ return
}
- for _, it := range list {
- if it.Item != "" && itemMatch != nil && !itemMatch(it.Item) {
+ g.applyStep(sess, p, step, p.RoomID, scopePlayer, nil)
+}
+
+// runTrigger runs the first matching entry in a trigger block. itemMatch
+// (optional) gates entries that carry an ItemID: for on_look it requires the
+// player to hold the item; for on_kill it requires wielding it. nil means the
+// ItemID field is ignored (on_enter/on_exit/on_traverse/on_flag_change). The
+// first entry whose item filter and Condition pass wins; its Steps run as a
+// scripted sequence. roomLocked controls the per-step room-lock: when true,
+// steps are skipped if the player has left roomID (used by on_enter so a
+// cutscene doesn't play into the wrong room). Returns true if a trigger
+// matched and fired.
+func (g *Game) runTrigger(sess *net.Session, p *player.Player, list []behavior.Trigger, roomID int, itemMatch func(string) bool, roomLocked bool) bool {
+ for i := range list {
+ t := &list[i]
+ if t.ItemID != "" && itemMatch != nil && !itemMatch(t.ItemID) {
continue
}
- if it.Condition != nil && !g.checkCondition(sess, it.Condition) {
+ if t.Condition != nil && !g.checkCondition(sess, t.Condition) {
continue
}
- g.runInteraction(sess, p, it.Action, roomID, sc, nil)
+ g.startSequence(sess, p, t.Steps, roomID, scopePlayer, nil, t.Lock, roomLocked, verbSeqKey(p, t.ItemID))
return true
}
return false
}
-
-// runInteraction fires a single interaction's Action. If the action carries
-// delayed Messages the player is locked for the duration (see the interaction
-// sequencer); otherwise the effects apply synchronously.
-func (g *Game) runInteraction(sess *net.Session, p *player.Player, step *behavior.StepAction, roomID int, sc effectScope, flagValue any) {
- if step == nil {
- return
- }
- if len(step.Messages) > 0 {
- g.startInteractionSeq(sess, p, step, roomID, sc, flagValue)
- return
- }
- g.applyStepAction(sess, p, step, roomID, sc, flagValue)
-} \ No newline at end of file