aboutsummaryrefslogtreecommitdiff
path: root/internal/behavior/behavior.go
blob: 75f836b5a3b966879aefe43bd59fa1153eec42fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
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"`
}