package behavior 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 *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 *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). // // 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 } // 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"` DespawnOnLeave bool `yaml:"despawn_on_leave"` DespawnRooms []int `yaml:"despawn_rooms"` DespawnTicks float64 `yaml:"despawn_ticks"` } // 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 } // 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,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"` }