From ecba7f726f70b37126d852c38c7e3eec7b04d730 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Thu, 9 Jul 2026 22:15:54 -0400 Subject: feat: old standalone trigger systems completely unified into trigger->condition->action system --- internal/behavior/behavior.go | 189 +++++++++++++++++++++--------------------- 1 file changed, 94 insertions(+), 95 deletions(-) (limited to 'internal/behavior/behavior.go') diff --git a/internal/behavior/behavior.go b/internal/behavior/behavior.go index 12eef0b..75f836b 100644 --- a/internal/behavior/behavior.go +++ b/internal/behavior/behavior.go @@ -1,21 +1,21 @@ package behavior type GatherConfig struct { - Skill string `yaml:"skill"` - Tools []string `yaml:"tools"` - NoToolSpeed float64 `yaml:"no_tool_speed,omitempty"` - Bait string `yaml:"bait"` + Skill string `yaml:"skill"` + Tools []string `yaml:"tools"` + NoToolSpeed float64 `yaml:"no_tool_speed,omitempty"` + Bait string `yaml:"bait"` Success SuccessFormula `yaml:"success"` - GatherMessage string `yaml:"gather_message"` - DepletedMessage string `yaml:"depleted_message"` - ExhaustedMessage string `yaml:"exhausted_message"` - FailMessage string `yaml:"fail_message"` - Drops []DropEntry `yaml:"drops"` - RespawnTimer float64 `yaml:"respawn_timer"` - RespawnBroadcast string `yaml:"respawn_broadcast"` - DepleteTimer float64 `yaml:"deplete_timer"` - NestChance int `yaml:"nest_chance"` - BroadcastMessage string `yaml:"broadcast_message"` + GatherMessage string `yaml:"gather_message"` + DepletedMessage string `yaml:"depleted_message"` + ExhaustedMessage string `yaml:"exhausted_message"` + FailMessage string `yaml:"fail_message"` + Drops []DropEntry `yaml:"drops"` + RespawnTimer float64 `yaml:"respawn_timer"` + RespawnBroadcast string `yaml:"respawn_broadcast"` + DepleteTimer float64 `yaml:"deplete_timer"` + NestChance int `yaml:"nest_chance"` + BroadcastMessage string `yaml:"broadcast_message"` } type SuccessFormula struct { @@ -32,88 +32,83 @@ type TalkNode struct { Messages []string `yaml:"messages,omitempty"` Condition *Condition `yaml:"condition,omitempty"` Options []TalkOption `yaml:"options"` - Action *NodeAction `yaml:"action"` + Action *Step `yaml:"action,omitempty"` Goto string `yaml:"goto,omitempty"` } type TalkOption struct { - Text string `yaml:"text"` - Goto string `yaml:"goto"` - Condition *Condition `yaml:"condition"` - Action *NodeAction `yaml:"action,omitempty"` -} - -// NodeAction is the inline-only effects payload — used by talk nodes, -// talk options, and any callers that need a simple "do these effects now" -// primitive (use/look/kill interactions, exits). It is the embedded -// subset of StepAction (the universal superset used by on_enter and triggers). -type NodeAction struct { - SetGlobalFlags map[string]any `yaml:"set_global_flags,omitempty"` - SetPlayerFlags map[string]any `yaml:"set_player_flags,omitempty"` - GiveItem string `yaml:"give_item,omitempty"` - TakeItem string `yaml:"take_item,omitempty"` - Teleport int `yaml:"teleport,omitempty"` - Heal int `yaml:"heal,omitempty"` - Credits int `yaml:"credits,omitempty"` - ApsNode bool `yaml:"aps_node,omitempty"` -} - -// DelayedMessage is a single message with a delay (in ticks) to wait before -// showing it. Used by the interaction sequencer (on_use / on_look / on_kill / -// on_traverse); on_enter and trigger steps emit their Messages list -// immediately when the step fires (use the step's own Delay for inter-step -// spacing). -type DelayedMessage struct { - Message string `yaml:"message,omitempty"` - Delay int `yaml:"delay,omitempty"` -} - -// Interaction is one conditional trigger entry shared by on_use, on_look -// (objects) and on_kill (mobs). Item filters by the appropriate held/weapon -// item depending on the interaction kind (see itemMatch on applyInteraction). -// The first entry whose item filter and Condition pass wins and fires exactly -// once. + Text string `yaml:"text"` + Goto string `yaml:"goto"` + Condition *Condition `yaml:"condition"` + Action *Step `yaml:"action,omitempty"` +} + +// Step is the universal effect primitive — one struct shared across every +// effect executor 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 +// triggers. A trigger's effects are expressed as an ordered list of Steps; +// spacing between steps is the Wait field (a pure {wait: N} step pauses the +// sequence for N ticks before the next step fires). // -// Action is a full StepAction so on_kill may broadcast a kill announcement or -// spawn a follow-up mob; inline callers (use/look) simply leave the -// sequence-only fields empty. -type Interaction struct { - Item string `yaml:"item_id,omitempty"` - Condition *Condition `yaml:"condition,omitempty"` - Action *StepAction `yaml:"action,omitempty"` -} - -// StepAction is the universal effect primitive — one superset struct shared -// across all effect executors (on_enter steps, room/global triggers, talk -// nodes, use/look/kill interactions, exit traversal). The embedded -// NodeAction holds the inline-only effects (flags/items/teleport/heal/credits/ -// aps_node); the additional fields are the sequence-only effects (messages, -// broadcasts, mob spawn/despawn, delay). Condition is evaluated per-step by -// the on_enter / trigger schedulers; inline callers ignore it (the gating -// happens at the parent Interaction.ExitDef level instead). -type StepAction struct { - NodeAction `yaml:",inline"` - Condition *Condition `yaml:"condition,omitempty"` - Messages []DelayedMessage `yaml:"messages,omitempty"` - Broadcast string `yaml:"broadcast,omitempty"` - BroadcastGlobal string `yaml:"broadcast_global,omitempty"` - SpawnMob *SpawnMobConfig `yaml:"spawn_mob,omitempty"` - DespawnMob string `yaml:"despawn_mob,omitempty"` - Delay int `yaml:"delay,omitempty"` -} - -// IsTimed reports whether the step carries sequence semantics (any non-zero -// field means it needs per-tick scheduling rather than synchronous printing). -func (s StepAction) IsTimed() bool { - return s.Delay > 0 || len(s.Messages) > 0 || s.Broadcast != "" || s.BroadcastGlobal != "" || +// Condition is evaluated per-step by the sequence scheduler; an inline caller +// (talk node, synchronous use/look/kill) fires a single step without the +// scheduler and ignores the per-step Condition (its gating is done at the +// parent Trigger level instead). +type Step struct { + Condition *Condition `yaml:"condition,omitempty"` + Wait int `yaml:"wait,omitempty"` + Messages []string `yaml:"messages,omitempty"` + Broadcast string `yaml:"broadcast,omitempty"` + BroadcastGlobal string `yaml:"broadcast_global,omitempty"` + SpawnMob *SpawnMobConfig `yaml:"spawn_mob,omitempty"` + DespawnMob string `yaml:"despawn_mob,omitempty"` + SetGlobalFlags map[string]any `yaml:"set_global_flags,omitempty"` + SetPlayerFlags map[string]any `yaml:"set_player_flags,omitempty"` + GiveItem string `yaml:"give_item,omitempty"` + TakeItem string `yaml:"take_item,omitempty"` + Teleport int `yaml:"teleport,omitempty"` + Heal int `yaml:"heal,omitempty"` + Credits int `yaml:"credits,omitempty"` + ApsNode bool `yaml:"aps_node,omitempty"` +} + +// HasEffects reports whether the step carries any effect beyond a pure wait. +func (s Step) HasEffects() bool { + return len(s.Messages) > 0 || s.Broadcast != "" || s.BroadcastGlobal != "" || s.SpawnMob != nil || s.DespawnMob != "" || len(s.SetGlobalFlags) > 0 || len(s.SetPlayerFlags) > 0 || s.GiveItem != "" || s.TakeItem != "" || s.Teleport != 0 || s.Heal != 0 || s.Credits != 0 || s.ApsNode } -// SpawnMobConfig configures a transient mob spawned by a trigger or on_enter -// step. The mob id is required; other fields are optional. +// Trigger is the one conditional wrapper used by every event block in the +// game: on_use, on_look, on_kill, on_enter, on_exit, on_traverse, +// on_flag_change, and on_global_flag_change. Entries in a block are walked +// top-to-bottom; the first whose ItemID filter and Condition pass wins and +// its Steps run as a scripted sequence. +// +// For verb blocks only ItemID/Condition/Steps/Lock are meaningful. For +// on_flag_change/on_global_flag_change blocks, OnPlayerFlag/OnGlobalFlag (and +// optional Value) declare the flag subscription; Condition provides an +// additional gate (use the Room condition to scope a flag trigger to a room). +// +// Lock, when true, makes the sequence atomic: the player is blocked from any +// verb (except quit) until it completes, and the sequence is saved and resumed +// across disconnect. Unlocked sequences (the default) are interruptable by any +// verb and are not persisted. +type Trigger struct { + Lock bool `yaml:"lock,omitempty"` + ItemID string `yaml:"item_id,omitempty"` + Condition *Condition `yaml:"condition,omitempty"` + Steps []Step `yaml:"steps"` + OnPlayerFlag string `yaml:"on_player_flag,omitempty"` + OnGlobalFlag string `yaml:"on_global_flag,omitempty"` + Value any `yaml:"value,omitempty"` + DedupKey string `yaml:"-"` // runtime only — disambiguates flag-trigger dedup keys +} + +// SpawnMobConfig configures a transient mob spawned by a trigger step. The +// mob id is required; other fields are optional. type SpawnMobConfig struct { ID string `yaml:"id"` OwnerOnly bool `yaml:"owner_only"` @@ -210,15 +205,19 @@ func (c *ShopConfig) FindItem(itemID string) *ShopItem { return nil } +// Condition is the single predicate struct used everywhere a gate is needed: +// exits, descriptions, talk nodes/options, trigger entries, and per-step +// conditions. The atomic predicates combine via AllOf/AnyOf nesting and the +// Not inverter. Room matches when the triggering player is currently in that +// room (ignored for global-flag triggers with no player). type Condition struct { - GlobalFlag string `yaml:"global_flag"` - Value any `yaml:"value"` - Not bool `yaml:"not"` - PlayerFlag string `yaml:"player_flag"` - HasItem string `yaml:"has_item"` - MinCredits int `yaml:"min_credits"` - AllOf []Condition `yaml:"all_of"` - AnyOf []Condition `yaml:"any_of"` + GlobalFlag string `yaml:"global_flag,omitempty"` + PlayerFlag string `yaml:"player_flag,omitempty"` + Room int `yaml:"room,omitempty"` + Value any `yaml:"value,omitempty"` + Not bool `yaml:"not,omitempty"` + HasItem string `yaml:"has_item,omitempty"` + MinCredits int `yaml:"min_credits,omitempty"` + AllOf []Condition `yaml:"all_of,omitempty"` + AnyOf []Condition `yaml:"any_of,omitempty"` } - - -- cgit v1.2.3