From 09d325dcf5e779eab5550d3fd3377bde50101428 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 8 Jul 2026 19:59:14 -0400 Subject: feat: unify on use, on look, and on kill. all support same conditions/actions now. --- internal/behavior/behavior.go | 92 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 9 deletions(-) (limited to 'internal/behavior/behavior.go') diff --git a/internal/behavior/behavior.go b/internal/behavior/behavior.go index d31a0a5..69b91cd 100644 --- a/internal/behavior/behavior.go +++ b/internal/behavior/behavior.go @@ -1,5 +1,7 @@ package behavior +import "gopkg.in/yaml.v3" + type GatherConfig struct { Skill string `yaml:"skill"` Tools []string `yaml:"tools"` @@ -43,16 +45,88 @@ type TalkOption struct { 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"` - SetPlayerFlags map[string]any `yaml:"set_player_flags"` - GiveItem string `yaml:"give_item"` - TakeItem string `yaml:"take_item"` - Teleport int `yaml:"teleport"` - Heal int `yaml:"heal"` - Credits int `yaml:"credits"` - ReputationCost int `yaml:"reputation_cost"` - ApsNode bool `yaml:"aps_node"` + 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"` +} + +// 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. +// +// 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"` + Message string `yaml:"message,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 (message, +// 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"` + Message string `yaml:"message,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 || s.Message != "" || 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. It accepts either a bare string (the mob id) or a full map at +// unmarshal time. +type SpawnMobConfig struct { + ID string `yaml:"id"` + OwnerOnly bool `yaml:"owner_only"` + DespawnOnLeave bool `yaml:"despawn_on_leave"` + DespawnRooms []int `yaml:"despawn_rooms"` + DespawnTicks float64 `yaml:"despawn_ticks"` +} + +func (s *SpawnMobConfig) UnmarshalYAML(value *yaml.Node) error { + if value.Kind == yaml.ScalarNode { + var id string + if err := value.Decode(&id); err != nil { + return err + } + s.ID = id + return nil + } + type raw SpawnMobConfig + return value.Decode((*raw)(s)) } // ShopConfig is a root-level mob property (mob YAML `shop:`). Buy prices are -- cgit v1.2.3