demiurge 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +5 -0
- data/.yardopts +5 -0
- data/AUTHORS.txt +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONCEPTS.md +271 -0
- data/Gemfile +4 -0
- data/HACKING.md +34 -0
- data/LICENSE.txt +21 -0
- data/README.md +181 -0
- data/RELOADING.md +94 -0
- data/Rakefile +10 -0
- data/SECURITY.md +103 -0
- data/WORLD_FILES.md +134 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/demiurge.gemspec +31 -0
- data/exe/demirun +16 -0
- data/lib/demiurge/action_item.rb +643 -0
- data/lib/demiurge/agent.rb +338 -0
- data/lib/demiurge/container.rb +194 -0
- data/lib/demiurge/dsl.rb +583 -0
- data/lib/demiurge/exception.rb +170 -0
- data/lib/demiurge/inert_state_item.rb +21 -0
- data/lib/demiurge/intention.rb +164 -0
- data/lib/demiurge/location.rb +85 -0
- data/lib/demiurge/notification_names.rb +93 -0
- data/lib/demiurge/tmx.rb +439 -0
- data/lib/demiurge/util.rb +67 -0
- data/lib/demiurge/version.rb +4 -0
- data/lib/demiurge/zone.rb +108 -0
- data/lib/demiurge.rb +812 -0
- metadata +165 -0
@@ -0,0 +1,338 @@
|
|
1
|
+
module Demiurge
|
2
|
+
|
3
|
+
# Agents correspond roughly to "mobiles" in many games. An agent
|
4
|
+
# isn't particularly different from other Demiurge objects, but it's
|
5
|
+
# useful to have some helper classes for things like pathfinding.
|
6
|
+
# Also, humans expect agents to have some finite ability to perform
|
7
|
+
# actions over time, so it's nice to regulate how much an agent can
|
8
|
+
# get done and how "busy" it is. This keeps an AI agent from just
|
9
|
+
# queueing up 30 move intentions and crossing the room in a single
|
10
|
+
# tick, for instance. It does *not* keep that same AI from having an
|
11
|
+
# intentional 30-square move that works in a single tick, but it
|
12
|
+
# slows the rate of actions. Agents get a single "real" intention,
|
13
|
+
# unlike, say, rooms, which can have lots going on at once.
|
14
|
+
#
|
15
|
+
# @since 0.0.1
|
16
|
+
class Agent < ActionItem
|
17
|
+
|
18
|
+
def initialize(*args)
|
19
|
+
super
|
20
|
+
state["queued_actions"] ||= []
|
21
|
+
state["queue_number"] ||= 0
|
22
|
+
end
|
23
|
+
|
24
|
+
def finished_init
|
25
|
+
super
|
26
|
+
@agent_maintenance = AgentInternal::AgentMaintenanceIntention.new(engine, @name)
|
27
|
+
state["busy"] ||= 0 # By default, start out idle.
|
28
|
+
end
|
29
|
+
|
30
|
+
# An Agent is, indeed, an Agent.
|
31
|
+
#
|
32
|
+
# @return [Boolean] Return true for Agent and its subclasses.
|
33
|
+
# @since 0.0.1
|
34
|
+
def agent?
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
# This method will move the agent and notify about that change. It
|
39
|
+
# doesn't use an intention or an agent's action queue, and it
|
40
|
+
# doesn't wait for a tick to happen. It just does it. The method
|
41
|
+
# *does* handle exits and generally allows the location to
|
42
|
+
# respond. But it's assumed that the offer cycle, if it needs to
|
43
|
+
# happen, has happened already.
|
44
|
+
#
|
45
|
+
# @param pos [String] A position string to move to
|
46
|
+
# @return [void]
|
47
|
+
# @since 0.0.1
|
48
|
+
def move_to_position(pos)
|
49
|
+
old_pos = self.position
|
50
|
+
old_loc = self.location_name
|
51
|
+
old_zone_name = self.zone_name
|
52
|
+
expected_new_loc = pos.split("#")[0]
|
53
|
+
|
54
|
+
if expected_new_loc == old_loc
|
55
|
+
self.location.item_change_position(self, old_pos, pos)
|
56
|
+
else
|
57
|
+
# This also handles zone changes.
|
58
|
+
self.location.item_change_location(self, old_pos, pos)
|
59
|
+
end
|
60
|
+
# We're not guaranteed to wind up where we expected, so get the
|
61
|
+
# new location *after* item_change_location or
|
62
|
+
# item_change_position.
|
63
|
+
new_loc = self.location_name
|
64
|
+
|
65
|
+
@engine.send_notification({ old_position: old_pos, old_location: old_loc, new_position: self.position, new_location: new_loc },
|
66
|
+
type: Demiurge::Notifications::MoveFrom, zone: old_zone_name, location: old_loc, actor: @name, include_context: true)
|
67
|
+
@engine.send_notification({ old_position: old_pos, old_location: old_loc, new_position: self.position, new_location: new_loc },
|
68
|
+
type: Demiurge::Notifications::MoveTo, zone: self.zone_name, location: self.location_name, actor: @name, include_context: true)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Calculate the agent's intentions for the following tick. These
|
72
|
+
# Intentions can potentially trigger other Intentions.
|
73
|
+
#
|
74
|
+
# @return [Array<Intention>] The Intentions for the (first part of the) following tick.
|
75
|
+
# @since 0.0.1
|
76
|
+
def intentions_for_next_step
|
77
|
+
agent_action = AgentInternal::AgentActionIntention.new(@name, engine)
|
78
|
+
super + [@agent_maintenance, agent_action]
|
79
|
+
end
|
80
|
+
|
81
|
+
# Queue an action to be run after previous actions are complete,
|
82
|
+
# and when the agent is no longer busy from taking them. The
|
83
|
+
# action queue entry is assigned a unique-per-agent queue number,
|
84
|
+
# which is returned from this action.
|
85
|
+
#
|
86
|
+
# @param action_name [String] The name of the action to take when possible
|
87
|
+
# @param args [Array] Additional arguments to pass to the action's code block
|
88
|
+
# @return [Integer] Returns the queue number for this action - note that queue numbers are only unique per-agent
|
89
|
+
# @since 0.0.1
|
90
|
+
def queue_action(action_name, *args)
|
91
|
+
raise ::Demiurge::Errors::NoSuchActionError.new("Not an action: #{action_name.inspect}!", { "action_name" => action_name },
|
92
|
+
execution_context: @engine.execution_context) unless get_action(action_name)
|
93
|
+
state["queue_number"] += 1
|
94
|
+
state["queued_actions"].push([action_name, args, state["queue_number"]])
|
95
|
+
state["queue_number"]
|
96
|
+
end
|
97
|
+
|
98
|
+
# Any queued actions waiting to occur will be discarded.
|
99
|
+
#
|
100
|
+
# @return [void]
|
101
|
+
# @since 0.0.1
|
102
|
+
def clear_intention_queue
|
103
|
+
state.delete "queued_actions"
|
104
|
+
nil
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Code objects internal to the Agent implementation
|
109
|
+
# @api private
|
110
|
+
module AgentInternal; end
|
111
|
+
|
112
|
+
# The AgentMaintenanceIntention reduces the level of "busy"-ness of
|
113
|
+
# the agent on each tick.
|
114
|
+
# @todo Merge this with the AgentActionIntention used for taking queued actions
|
115
|
+
#
|
116
|
+
# @api private
|
117
|
+
class AgentInternal::AgentMaintenanceIntention < Intention
|
118
|
+
# Constructor. Takes an engine and agent name.
|
119
|
+
def initialize(engine, name)
|
120
|
+
@name = name
|
121
|
+
super(engine)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Normally, the agent's maintenance intention can't be blocked,
|
125
|
+
# cancelled or modified.
|
126
|
+
def offer
|
127
|
+
end
|
128
|
+
|
129
|
+
# An AgentMaintenanceIntention is always considered to be allowed.
|
130
|
+
def allowed?
|
131
|
+
true
|
132
|
+
end
|
133
|
+
|
134
|
+
# Reduce the amount of busy-ness.
|
135
|
+
def apply
|
136
|
+
agent = @engine.item_by_name(@name)
|
137
|
+
agent.state["busy"] -= 1 if agent.state["busy"] > 0
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# An AgentActionIntention is how the agent takes queued actions each
|
142
|
+
# tick.
|
143
|
+
#
|
144
|
+
# @note There is a bit of weirdness in how this intention handles
|
145
|
+
# {#allowed?} and {#offer}. We want to be able to queue an action
|
146
|
+
# on the same tick that we execute it if the agent is idle. So we
|
147
|
+
# count this intention as #allowed? even if the queue is empty,
|
148
|
+
# then silent-cancel the intention during {#offer} if nobody has
|
149
|
+
# added anything to it. If you see a lot of cancel notifications
|
150
|
+
# from this object with "silent" set, now you know why.
|
151
|
+
#
|
152
|
+
# @api private
|
153
|
+
class AgentInternal::AgentActionIntention < ActionItemInternal::ActionIntention
|
154
|
+
# @return [StateItem] The agent to whom this Intention applies
|
155
|
+
attr_reader :agent
|
156
|
+
# @return [String] The queued action name this Intention will next take
|
157
|
+
attr_reader :action_name
|
158
|
+
|
159
|
+
# Constructor. Takes an agent name and an engine
|
160
|
+
def initialize(name, engine)
|
161
|
+
super(engine, name, "")
|
162
|
+
@agent = engine.item_by_name(name)
|
163
|
+
raise ::Demiurge::Errors::NoSuchAgentError.new("No such agent as #{name.inspect} found in AgentActionIntention!", "agent" => name,
|
164
|
+
execution_context: engine.execution_context) unless @agent
|
165
|
+
end
|
166
|
+
|
167
|
+
# An action being pulled from the action queue is offered normally.
|
168
|
+
def offer
|
169
|
+
# Don't offer the action if it's going to be a no-op.
|
170
|
+
if @agent.state["busy"] > 0
|
171
|
+
# See comment on "silent" in #allowed? below.
|
172
|
+
self.cancel "Agent #{@name.inspect} was too busy to act (#{@agent.state["busy"]}).", "silent" => "true"
|
173
|
+
return
|
174
|
+
elsif @agent.state["queued_actions"].empty?
|
175
|
+
self.cancel "Agent #{@name.inspect} had no actions during the 'offer' phase.", "silent" => "true"
|
176
|
+
return
|
177
|
+
end
|
178
|
+
# Now offer the agent's action via the usual channels
|
179
|
+
action = @agent.state["queued_actions"][0]
|
180
|
+
@action_name, @action_args, @action_queue_number = *action
|
181
|
+
@action_struct = @agent.get_action(@action_name)
|
182
|
+
super
|
183
|
+
end
|
184
|
+
|
185
|
+
# This action is allowed if the agent is not busy, or will become not-busy soon
|
186
|
+
def allowed?
|
187
|
+
# If the agent's busy state will clear this turn, this action
|
188
|
+
# could happen. We intentionally don't send a "disallowed"
|
189
|
+
# notification for the action. It's not cancelled, nor is it
|
190
|
+
# dispatched successfully. It's just waiting for a later tick to
|
191
|
+
# do one of those two things.
|
192
|
+
return false if @agent.state["busy"] > 1
|
193
|
+
|
194
|
+
# A dilemma: if we cancel now when no actions are queued, then
|
195
|
+
# any action queued this turn (e.g. from an
|
196
|
+
# EveryXActionsIntention) won't be executed -- we said this
|
197
|
+
# intention wasn't happening. If we *don't* return false in the
|
198
|
+
# "allowed?" phase then we'll wind up sending out a cancel
|
199
|
+
# notice every turn when there are no actions. So we add a
|
200
|
+
# "silent" info option to the normal-every-turn cancellations,
|
201
|
+
# but we *do* allow-then-cancel even in perfectly normal
|
202
|
+
# circumstances.
|
203
|
+
true
|
204
|
+
end
|
205
|
+
|
206
|
+
# If the agent can do so, take the action in question.
|
207
|
+
def apply
|
208
|
+
unless agent.state["busy"] > 0 || agent.state["queued_actions"].empty?
|
209
|
+
# Pull the first entry off the action queue
|
210
|
+
queue = @agent.state["queued_actions"]
|
211
|
+
if queue && queue.size > 0
|
212
|
+
if @action_queue_number != queue[0][2]
|
213
|
+
@engine.admin_warning("Somehow the agent's action queue has gotten screwed up mid-offer!", "agent" => @name)
|
214
|
+
else
|
215
|
+
queue.shift # Remove the queue entry
|
216
|
+
end
|
217
|
+
end
|
218
|
+
agent.run_action(@action_name, *@action_args, current_intention: self)
|
219
|
+
agent.state["busy"] += (@action_struct["busy"] || 1)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Send out a notification to indicate this ActionIntention was
|
224
|
+
# cancelled. If "silent" is set to true in the cancellation info,
|
225
|
+
# no notification will be sent.
|
226
|
+
#
|
227
|
+
# @return [void]
|
228
|
+
# @since 0.2.0
|
229
|
+
def cancel_notification
|
230
|
+
return if @cancelled_info && @cancelled_info["silent"]
|
231
|
+
@engine.send_notification({
|
232
|
+
reason: @cancelled_reason,
|
233
|
+
by: @cancelled_by,
|
234
|
+
id: @intention_id,
|
235
|
+
intention_type: self.class.to_s,
|
236
|
+
info: @cancelled_info,
|
237
|
+
queue_number: @action_queue_number,
|
238
|
+
action_name: @action_name,
|
239
|
+
action_args: @action_args,
|
240
|
+
},
|
241
|
+
type: Demiurge::Notifications::IntentionCancelled,
|
242
|
+
zone: @item.zone_name,
|
243
|
+
location: @item.location_name,
|
244
|
+
actor: @item.name,
|
245
|
+
include_context: true)
|
246
|
+
nil
|
247
|
+
end
|
248
|
+
|
249
|
+
# Send out a notification to indicate this ActionIntention was
|
250
|
+
# applied.
|
251
|
+
#
|
252
|
+
# @return [void]
|
253
|
+
# @since 0.2.0
|
254
|
+
def apply_notification
|
255
|
+
@engine.send_notification({
|
256
|
+
id: @intention_id,
|
257
|
+
intention_type: self.class.to_s,
|
258
|
+
queue_number: @action_queue_number,
|
259
|
+
action_name: @action_name,
|
260
|
+
action_args: @action_args,
|
261
|
+
},
|
262
|
+
type: Demiurge::Notifications::IntentionApplied,
|
263
|
+
zone: @item.zone_name,
|
264
|
+
location: @item.location_name,
|
265
|
+
actor: @item.name,
|
266
|
+
include_context: true)
|
267
|
+
nil
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# This agent will wander around. A simple way to make a decorative
|
272
|
+
# mobile. Do we want this longer term, or should it be merged into
|
273
|
+
# the normal agent?
|
274
|
+
class WanderingAgent < Agent
|
275
|
+
# Constructor
|
276
|
+
def initialize(name, engine, state)
|
277
|
+
super
|
278
|
+
state["wander_counter"] ||= 0
|
279
|
+
end
|
280
|
+
|
281
|
+
# If we're in a room but don't know where, pick a legal location.
|
282
|
+
def finished_init
|
283
|
+
super
|
284
|
+
@wander_intention = AgentInternal::WanderIntention.new(engine, name)
|
285
|
+
unless state["position"] && state["position"]["#"]
|
286
|
+
# Move to legal position. If this is a TMX location or similar, it will assign a specific position.
|
287
|
+
if self.location.respond_to?(:any_legal_position)
|
288
|
+
state["position"] = self.location.any_legal_position
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# Get intentions for the next upcoming tick
|
294
|
+
def intentions_for_next_step
|
295
|
+
super + [@wander_intention]
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# This is a simple Wandering agent for use with TmxLocations and similar grid-based maps.
|
300
|
+
#
|
301
|
+
# @api private
|
302
|
+
class AgentInternal::WanderIntention < ActionItemInternal::ActionIntention
|
303
|
+
# Constructor
|
304
|
+
def initialize(engine, name, *args)
|
305
|
+
@name = name
|
306
|
+
super(engine, name, "", *args)
|
307
|
+
end
|
308
|
+
|
309
|
+
# Always allowed
|
310
|
+
def allowed?
|
311
|
+
true
|
312
|
+
end
|
313
|
+
|
314
|
+
# For now, WanderIntention is unblockable. That's not perfect, but
|
315
|
+
# otherwise we have to figure out how to offer an action without
|
316
|
+
# an action name.
|
317
|
+
def offer
|
318
|
+
end
|
319
|
+
|
320
|
+
# Actually wander to an adjacent position, chosen randomly
|
321
|
+
def apply
|
322
|
+
agent = @engine.item_by_name(@name)
|
323
|
+
agent.state["wander_counter"] += 1
|
324
|
+
wander_every = agent.state["wander_every"] || 3
|
325
|
+
return if agent.state["wander_counter"] < wander_every
|
326
|
+
next_coords = agent.zone.adjacent_positions(agent.position)
|
327
|
+
if next_coords.empty?
|
328
|
+
@engine.admin_warning("Oh no! Wandering agent #{@name.inspect} is stuck and can't get out!",
|
329
|
+
"zone" => agent.zone_name, "location" => agent.location_name, "agent" => @name)
|
330
|
+
return
|
331
|
+
end
|
332
|
+
chosen = next_coords.sample
|
333
|
+
pos = "#{agent.location_name}##{chosen.join(",")}"
|
334
|
+
agent.move_to_position(pos)
|
335
|
+
agent.state["wander_counter"] = 0
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
# A Container may contain other items. Common examples include Zones and Locations.
|
2
|
+
|
3
|
+
module Demiurge
|
4
|
+
# Container is the parent class of Locations, Zones and other items that can contain items.
|
5
|
+
class Container < ActionItem
|
6
|
+
# Constructor - set up contents
|
7
|
+
#
|
8
|
+
# @param name [String] The Engine-unique item name
|
9
|
+
# @param engine [Demiurge::Engine] The Engine this item is part of
|
10
|
+
# @param state [Hash] State data to initialize from
|
11
|
+
# @return [void]
|
12
|
+
# @since 0.0.1
|
13
|
+
def initialize(name, engine, state)
|
14
|
+
super
|
15
|
+
state["contents"] ||= []
|
16
|
+
end
|
17
|
+
|
18
|
+
# Gets the array of item names of all items contained in this
|
19
|
+
# container.
|
20
|
+
#
|
21
|
+
# @return [Array<String>] The array of item names
|
22
|
+
# @since 0.0.1
|
23
|
+
def contents_names
|
24
|
+
state["contents"]
|
25
|
+
end
|
26
|
+
|
27
|
+
# The finished_init hook is called after all items are loaded. For
|
28
|
+
# containers, this makes sure all items set as contents of this
|
29
|
+
# container also have it correctly set as their position.
|
30
|
+
#
|
31
|
+
# @return [void]
|
32
|
+
# @since 0.0.1
|
33
|
+
def finished_init
|
34
|
+
# Can't move all items inside until they all exist, which isn't guaranteed until init is finished.
|
35
|
+
state["contents"].each do |item|
|
36
|
+
move_item_inside(@engine.item_by_name(item))
|
37
|
+
end
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
# This makes sure the given item name is listed in the container's
|
42
|
+
# contents. It does *not* make sure that the item currently
|
43
|
+
# exists, or that its position is set to this container.
|
44
|
+
#
|
45
|
+
# @see #move_item_inside
|
46
|
+
# @see #ensure_does_not_contain
|
47
|
+
# @param item_name [String] The item name to ensure is listed in the container
|
48
|
+
# @return [void]
|
49
|
+
# @since 0.0.1
|
50
|
+
def ensure_contains(item_name)
|
51
|
+
raise("Pass only item names to ensure_contains!") unless item_name.is_a?(String)
|
52
|
+
@state["contents"] |= [item_name]
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
# This makes sure the given item name is *not* listed in the
|
57
|
+
# container's contents. It does not make sure the item exists, nor
|
58
|
+
# do anything with the item's position.
|
59
|
+
#
|
60
|
+
# @see #ensure_contains
|
61
|
+
# @see #move_item_inside
|
62
|
+
# @param item_name [String] The item name
|
63
|
+
# @return [void]
|
64
|
+
# @since 0.0.1
|
65
|
+
def ensure_does_not_contain(item_name)
|
66
|
+
raise("Pass only item names to ensure_does_not_contain!") unless item_name.is_a?(String)
|
67
|
+
@state["contents"] -= [item_name]
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
# This makes sure the given StateItem is contained in this
|
72
|
+
# container. It sets the item's position to be in this container,
|
73
|
+
# and if there is an old location it attempts to properly remove
|
74
|
+
# the item from it.
|
75
|
+
#
|
76
|
+
# @see Demiurge::Agent#move_to_position
|
77
|
+
# @param item [Demiurge::StateItem] The item to be moved into this container
|
78
|
+
# @return [void]
|
79
|
+
# @since 0.0.1
|
80
|
+
def move_item_inside(item)
|
81
|
+
old_pos = item.position
|
82
|
+
if old_pos
|
83
|
+
old_loc_name = old_pos.split("#")[0]
|
84
|
+
old_loc = @engine.item_by_name(old_loc_name)
|
85
|
+
old_loc.ensure_does_not_contain(item.name)
|
86
|
+
end
|
87
|
+
|
88
|
+
@state["contents"] |= [ item.name ]
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
# This is a callback to indicate that an item has changed
|
93
|
+
# position, but remains inside this location. Other than changing
|
94
|
+
# the item's position state variable, this may not require any
|
95
|
+
# changes. A different callback is called when the item changes
|
96
|
+
# from one location to another.
|
97
|
+
#
|
98
|
+
# @see #item_change_location
|
99
|
+
# @param item [Demiurge::StateItem] The item changing position
|
100
|
+
# @param old_pos [String] The pre-movement position, which is current when this is called
|
101
|
+
# @param new_pos [String] The post-movement position, which should be current when this method completes
|
102
|
+
# @return [void]
|
103
|
+
# @since 0.0.1
|
104
|
+
def item_change_position(item, old_pos, new_pos)
|
105
|
+
item.state["position"] = new_pos
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
|
109
|
+
# This is a callback to indicate that an item has changed from one
|
110
|
+
# location to another. This will normally require removing the
|
111
|
+
# item from its first location and adding it to a new location. A
|
112
|
+
# different callback is called when the item changes position
|
113
|
+
# within a single location.
|
114
|
+
#
|
115
|
+
# @see #item_change_position
|
116
|
+
# @param item [Demiurge::StateItem] The item changing position
|
117
|
+
# @param old_pos [String] The pre-movement position, which is current when this is called
|
118
|
+
# @param new_pos [String] The post-movement position, which should be current when this method completes
|
119
|
+
# @return [void]
|
120
|
+
# @since 0.0.1
|
121
|
+
def item_change_location(item, old_pos, new_pos)
|
122
|
+
old_loc = old_pos.split("#")[0]
|
123
|
+
old_loc_item = @engine.item_by_name(old_loc)
|
124
|
+
old_loc_item.ensure_does_not_contain(item.name)
|
125
|
+
new_loc = new_pos.split("#")[0]
|
126
|
+
new_loc_item = @engine.item_by_name(new_loc)
|
127
|
+
new_loc_item.ensure_contains(item.name)
|
128
|
+
item.state["position"] = new_pos
|
129
|
+
|
130
|
+
old_zone = old_loc_item.zone_name
|
131
|
+
new_zone = new_loc_item.zone_name
|
132
|
+
if new_zone != old_zone
|
133
|
+
item.state["zone"] = new_zone
|
134
|
+
end
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
|
138
|
+
# When an item has an {Demiurge::Intention}, that Intention is
|
139
|
+
# offered in order to be potentially modified or cancelled by
|
140
|
+
# environmental effects. For instance, a room might have a muddy
|
141
|
+
# floor that slows walking or prevents running, or an icy floor
|
142
|
+
# that causes sliding around. That offer is normally coordinated
|
143
|
+
# through the item's location. The location will receive this
|
144
|
+
# callback ({#receive_offer}) and make appropriate modifications
|
145
|
+
# to the Intention. Any other items or agents that want to modify
|
146
|
+
# the Intention will have to coordinate with the appropriate item
|
147
|
+
# location.
|
148
|
+
#
|
149
|
+
# @see file:CONCEPTS.md
|
150
|
+
# @param action_name [String] The name of the action for this Intention.
|
151
|
+
# @param intention [Demiurge::Intention] The Intention being offered
|
152
|
+
# @return [void]
|
153
|
+
# @since 0.0.1
|
154
|
+
def receive_offer(action_name, intention)
|
155
|
+
# Run handlers, if any
|
156
|
+
on_actions = @state["on_action_handlers"]
|
157
|
+
if on_actions && (on_actions[action_name] || on_actions["all"])
|
158
|
+
run_action(on_actions["all"], intention, current_intention: intention) if on_actions["all"]
|
159
|
+
run_action(on_actions[action_name], intention, current_intention: intention) if on_actions[action_name]
|
160
|
+
end
|
161
|
+
nil
|
162
|
+
end
|
163
|
+
|
164
|
+
# This method determines if a given agent can exist at the
|
165
|
+
# specified position inside this container. By default, a
|
166
|
+
# container can accomodate anyone or anything. Subclass to change
|
167
|
+
# this behavior. This should take into account the size, shape and
|
168
|
+
# current condition of the agent, and might take into account
|
169
|
+
# whether the agent has certain movement abilities.
|
170
|
+
#
|
171
|
+
# @param agent [Demiurge::Agent] The agent being checked
|
172
|
+
# @param position [String] The position being checked within this container
|
173
|
+
# @return [Boolean] Whether the agent can exist at that position
|
174
|
+
# @since 0.0.1
|
175
|
+
def can_accomodate_agent?(agent, position)
|
176
|
+
true
|
177
|
+
end
|
178
|
+
|
179
|
+
# This determines the intentions for the next tick for this
|
180
|
+
# container and for all items inside it.
|
181
|
+
#
|
182
|
+
# @return [Array<Intention>] The array of intentions for next tick
|
183
|
+
# @since 0.0.1
|
184
|
+
def intentions_for_next_step
|
185
|
+
intentions = super
|
186
|
+
@state["contents"].each do |item_name|
|
187
|
+
item = @engine.item_by_name(item_name)
|
188
|
+
intentions += item.intentions_for_next_step
|
189
|
+
end
|
190
|
+
intentions
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
end
|