aboutsummaryrefslogtreecommitdiff
path: root/internal/behavior/behavior.go
blob: 69b91cd1a59c2e3274c14a1bbeac914ec2f8576e (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
224
225
226
227
228
229
230
231
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"`
}