diff options
Diffstat (limited to 'internal/game/act_effects.go')
| -rw-r--r-- | internal/game/act_effects.go | 203 |
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 |
