package behavior import "gopkg.in/yaml.v3" type GatherConfig struct { 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"` } type SuccessFormula struct { Base float64 `yaml:"base"` PerLevel float64 `yaml:"per_level"` Cap float64 `yaml:"cap"` } type TalkConfig struct { Nodes map[string]TalkNode `yaml:"nodes"` } type TalkNode struct { Messages []string `yaml:"messages,omitempty"` Condition *Condition `yaml:"condition,omitempty"` Options []TalkOption `yaml:"options"` Action *NodeAction `yaml:"action"` 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"` } // 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 // always 100% of the item's value; sell prices follow the OSRS store formula: // // sell = value * max(10, buy_percentage - stock*change_percentage) / 100 // // where stock is the shop's current holding of that item. BuyPercentage // defaults to 40 and ChangePercentage to 3 when unset (see ShopBuyPercentage / // ShopChangePercentage). type ShopConfig struct { Message string `yaml:"message"` BuyPercentage float64 `yaml:"buy_percentage"` // sell price at zero stock, % of value (default 40) ChangePercentage float64 `yaml:"change_percentage"` // sell price drop per unit of stock, % of value (default 3) BuysAnything *bool `yaml:"buys_anything"` // buy items not in the list (default true) Items []ShopItem `yaml:"items"` } type ShopItem struct { ItemID string `yaml:"item_id"` Stock int `yaml:"stock"` // target/max stock; also starting stock RestockTicks float64 `yaml:"restock_ticks"` // per-item restock interval (overrides shop default) } // Shop pricing defaults, applied when the YAML leaves a field at zero. const ( ShopBuyPercentage = 40.0 ShopChangePercentage = 3.0 ShopMinSellPercent = 10.0 ) // ShopDefaultRestock is the fallback restock interval (in ticks) for a shop item // that does not set its own restock_ticks. It is configurable via // game_constants.shop_default_restock in config.yaml (wired up at startup); the // value here is only the default used when no config is loaded. var ShopDefaultRestock = 1000.0 // EffectiveBuyPercentage returns BuyPercentage or the default. func (c *ShopConfig) EffectiveBuyPercentage() float64 { if c.BuyPercentage > 0 { return c.BuyPercentage } return ShopBuyPercentage } // EffectiveChangePercentage returns ChangePercentage or the default. func (c *ShopConfig) EffectiveChangePercentage() float64 { if c.ChangePercentage > 0 { return c.ChangePercentage } return ShopChangePercentage } // Buys reports whether the shop will buy items not in its item list. func (c *ShopConfig) Buys() bool { return c.BuysAnything == nil || *c.BuysAnything } // SellPrice computes the per-unit price the shop pays for an item of the given // value when the shop currently holds `stock` of it. func (c *ShopConfig) SellPrice(value, stock int) int { if stock < 0 { stock = 0 } pct := c.EffectiveBuyPercentage() - float64(stock)*c.EffectiveChangePercentage() if pct < ShopMinSellPercent { pct = ShopMinSellPercent } return int(float64(value) * pct / 100.0) } // RestockInterval returns the effective restock interval (in ticks) for an item, // honoring the per-item override, then the configurable global default. func (c *ShopConfig) RestockInterval(item *ShopItem) int { if item != nil && item.RestockTicks > 0 { return int(item.RestockTicks) } return int(ShopDefaultRestock) } // FindItem returns the configured shop item with the given id, or nil. func (c *ShopConfig) FindItem(itemID string) *ShopItem { for i := range c.Items { if c.Items[i].ItemID == itemID { return &c.Items[i] } } return nil } 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"` }