demiurge 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,643 @@
1
+ module Demiurge
2
+ # A Demiurge::ActionItem keeps track of actions from Ruby code
3
+ # blocks and implements the Demiurge DSL for action code, including
4
+ # inside World Files.
5
+ #
6
+ # @since 0.0.1
7
+ class ActionItem < StateItem
8
+ # Constructor. Set up ActionItem-specific things like EveryXTicks actions.
9
+ #
10
+ # @param name [String] The registered StateItem name
11
+ # @param engine [Demiurge::Engine] The Engine this item is part of
12
+ # @param state [Hash] The state hash for this item
13
+ # @return [void]
14
+ # @since 0.0.1
15
+ def initialize(name, engine, state)
16
+ super # Set @name and @engine and @state
17
+ @every_x_ticks_intention = ActionItemInternal::EveryXTicksIntention.new(engine, name)
18
+ nil
19
+ end
20
+
21
+ # Callback to be called from the Engine when all items are loaded.
22
+ #
23
+ # @return [void]
24
+ # @since 0.0.1
25
+ def finished_init
26
+ loc = self.location
27
+ loc.move_item_inside(self) unless loc.nil?
28
+ nil
29
+ end
30
+
31
+ # Get the name of this item's location. This is compatible with
32
+ # complex positions, and removes any sub-location suffix, if there
33
+ # is one.
34
+ #
35
+ # @return [String, nil] The location name where this item exists, or nil if it has no location
36
+ # @since 0.0.1
37
+ def location_name
38
+ pos = @state["position"]
39
+ pos ? pos.split("#",2)[0] : nil
40
+ end
41
+
42
+ # Get the location StateItem where this item is located.
43
+ #
44
+ # @return [Demiurge::StateItem, nil] The location's StateItem, or nil if this item has no location
45
+ # @since 0.0.1
46
+ def location
47
+ ln = location_name
48
+ return nil if ln == "" || ln == nil
49
+ @engine.item_by_name(location_name)
50
+ end
51
+
52
+ # A Position can be simply a location ("here's a room-type object
53
+ # and you're in it") or something more specific, such as a
54
+ # specific coordinate within a room. In general, a Position
55
+ # consists of a location's unique item name, optionally followed
56
+ # by an optional pound sign ("#") and zone-specific additional
57
+ # coordinates.
58
+ #
59
+ # @return [String, nil] This item's position, or nil if it has no location.
60
+ def position
61
+ @state["position"]
62
+ end
63
+
64
+ # Get the StateItem of the Zone where this item is located. This
65
+ # may be different from its "home" Zone.
66
+ #
67
+ # @return [StateItem, nil] This item's Zone's StateItem, or nil in the very unusual case that it has no current Zone.
68
+ def zone
69
+ zn = zone_name
70
+ zn ? @engine.item_by_name(zn) : nil
71
+ end
72
+
73
+ # Get the Zone name for this StateItem's current location, which
74
+ # may be different from its "home" Zone.
75
+ #
76
+ # @return [String, nil] This item's Zone's name, or nil in the very unusual case that it has no current Zone.
77
+ def zone_name
78
+ l = location
79
+ l ? l.zone_name : state["zone"]
80
+ end
81
+
82
+ # An internal function that provides the object's internal state
83
+ # to an action block via a Runner class.
84
+ #
85
+ # @return [Hash] The internal state of this item for use in DSL action blocks
86
+ # @api private
87
+ # @since 0.0.1
88
+ def __state_internal
89
+ @state
90
+ end
91
+
92
+ # Get this item's intentions for the next tick.
93
+ #
94
+ # @return [Array<Demiurge::Intention>] An array of intentions for next tick
95
+ # @since 0.0.1
96
+ def intentions_for_next_step
97
+ everies = @state["everies"]
98
+ return [] if everies.nil? || everies.empty?
99
+ [@every_x_ticks_intention]
100
+ end
101
+
102
+ # Legal keys to pass to ActionItem#register_actions' hash
103
+ # @since 0.0.1
104
+ ACTION_LEGAL_KEYS = [ "name", "block", "busy", "engine_code", "tags" ]
105
+
106
+ # This method is called by (among other things) define_action to
107
+ # specify things about an action. It's how to specify the
108
+ # action's code, how busy it makes an agent when it occurs, what
109
+ # Runner to use with it, and any appropriate String tags. While it
110
+ # can be called multiple times to specify different things about a
111
+ # single action, it must not be called with the same information.
112
+ # So the block can only be specified once, "busy" can only be
113
+ # specified once and so on.
114
+ #
115
+ # This means that if an action's block is given implicitly by
116
+ # something like an every_X_ticks declaration, it can use
117
+ # define_action to set "busy" or "engine_code". But it can't
118
+ # define a different block of code to run with define_action.
119
+ #
120
+ # @param action_hash [Hash] Specify something or everything about an action by its name.
121
+ # @option action_hash [String] name The name of the action, which is required.
122
+ # @option action_hash [Proc] block The block of code for the action itself
123
+ # @return void
124
+ # @since 0.0.1
125
+ def register_actions(action_hash)
126
+ @engine.register_actions_by_item_and_action_name(@name => action_hash)
127
+ end
128
+
129
+ # This is a raw, low-level way to execute an action of an
130
+ # ActionItem. It doesn't wait for Intentions. It doesn't send
131
+ # extra notifications. It doesn't offer or give a chance to cancel
132
+ # the action. It just runs.
133
+ #
134
+ # @param action_name [String] The name of the action to run. Must already be registered.
135
+ # @param args [Array] Additional arguments to pass to the action's code block
136
+ # @param current_intention [nil, Intention] Current intention being executed, if any. This is used for to cancel an intention, if necessary
137
+ # @return [void]
138
+ # @since 0.0.1
139
+ def run_action(action_name, *args, current_intention: nil)
140
+ action = get_action(action_name)
141
+ raise ::Demiurge::Errors::NoSuchActionError.new("No such action as #{action_name.inspect} for #{@name.inspect}!",
142
+ "item" => self.name, "action" => action_name,
143
+ execution_context: @engine.execution_context) unless action
144
+ block = action["block"]
145
+ raise ::Demiurge::Errors::NoSuchActionError.new("Action was never defined for #{action_name.inspect} of object #{@name.inspect}!",
146
+ "item" => self.name, "action" => action_name,
147
+ execution_context: @engine.execution_context) unless block
148
+
149
+ runner_constructor_args = {}
150
+ if action["engine_code"]
151
+ block_runner_type = ActionItemInternal::EngineBlockRunner
152
+ elsif self.agent?
153
+ block_runner_type = ActionItemInternal::AgentBlockRunner
154
+ runner_constructor_args[:current_intention] = current_intention
155
+ else
156
+ block_runner_type = ActionItemInternal::ActionItemBlockRunner
157
+ runner_constructor_args[:current_intention] = current_intention
158
+ end
159
+ # TODO: can we save block runners between actions?
160
+ block_runner = block_runner_type.new(self, **runner_constructor_args)
161
+ begin
162
+ @engine.push_context("running_action" => action_name, "running_action_item" => @name) do
163
+ block_runner.instance_exec(*args, &block)
164
+ end
165
+ rescue
166
+ #STDERR.puts "#{$!.message}\n#{$!.backtrace.join("\n")}"
167
+ raise ::Demiurge::Errors::BadScriptError.new("Script error of type #{$!.class} with message: #{$!.message}",
168
+ { "runner type": block_runner_type.to_s, "action" => action_name, },
169
+ execution_context: @engine.execution_context);
170
+ end
171
+ nil
172
+ end
173
+
174
+ # Get the action hash structure for a given action name. This is
175
+ # normally done to verify that a specific action name exists at
176
+ # all.
177
+ #
178
+ # @param action_name [String] The action name to query
179
+ # @return [Hash, nil] A hash of information about the action, or nil if the action doesn't exist
180
+ # @since 0.0.1
181
+ def get_action(action_name)
182
+ action = @engine.action_for_item(@name, action_name)
183
+ if !action && state["parent"]
184
+ # Do we have a parent and no action definition yet? If so, defer to the parent.
185
+ action = @engine.item_by_name(state["parent"]).get_action(action_name)
186
+ end
187
+ action
188
+ end
189
+
190
+ # Return all actions which have the given String tags specified for them.
191
+ #
192
+ # @param tags [Array<String>] An array of tags the returned actions should match
193
+ # @return [Array<Hash>] An array of action structures. The "name" field of each gives the action name
194
+ # @since 0.0.1
195
+ def get_actions_with_tags(tags)
196
+ tags = [tags].flatten # Allow calling with a single tag string
197
+ @actions = []
198
+ @engine.actions_for_item(@name).each do |action_name, action_struct|
199
+ # I'm sure there's some more clever way to check if the action contains all these tags...
200
+ if (tags - (action_struct["tags"] || [])).empty?
201
+ @actions.push action_struct
202
+ end
203
+ end
204
+ @actions
205
+ end
206
+ end
207
+
208
+ # This module is for Intentions, BlockRunners and whatnot that are
209
+ # internal implementation details of ActionItem.
210
+ #
211
+ # @api private
212
+ # @since 0.0.1
213
+ module ActionItemInternal; end
214
+
215
+ # BlockRunners set up the environment for an action's block of code.
216
+ # They provide available information and available actions. The
217
+ # BlockRunner parent class is mostly to provide a root location to
218
+ # begin looking for BlockRunners.
219
+ #
220
+ # @since 0.0.1
221
+ class ActionItemInternal::BlockRunner
222
+ # @return [Demiurge::ActionItem] The item the BlockRunner is attached to
223
+ attr_reader :item
224
+
225
+ # @return [Demiurge::Engine] The engine the BlockRunner is attached to
226
+ attr_reader :engine
227
+
228
+ # Constructor: set the item
229
+ # Ruby bug: with no unused kw args, passing this an empty hash of kw args will give "ArgumentError: wrong number of arguments"
230
+ #
231
+ # @param item [Demiurge::ActionItem] The item using the block, and (usually) the item taking action
232
+ def initialize(item, unused_kw_arg:nil)
233
+ @item = item
234
+ @engine = item.engine
235
+ end
236
+ end
237
+
238
+ # This is a very simple BlockRunner for defining DSL actions that
239
+ # need very little extra support. It's for weird, powerful actions
240
+ # that mess with the internals of the engine. You can request it by
241
+ # passing the "engine_code" option to define_action.
242
+ #
243
+ # @since 0.0.1
244
+ class ActionItemInternal::EngineBlockRunner < ActionItemInternal::BlockRunner
245
+ end
246
+
247
+ # The ActionItemBlockRunner is a good, general-purpose block runner
248
+ # that supplies more context and more "gentle" object accessors to
249
+ # the block code. The methods of this class are generally intended
250
+ # to be used in the block code.
251
+ #
252
+ # @since 0.0.1
253
+ class ActionItemInternal::ActionItemBlockRunner < ActionItemInternal::BlockRunner
254
+ # @return [Demiurge::Intention, nil] The current intention, if any
255
+ # @since 0.0.1
256
+ attr_reader :current_intention
257
+
258
+ # The constructor
259
+ #
260
+ # @param item The item receiving the block and (usually) taking action
261
+ # @param current_intention The current intention, if any; used for canceling
262
+ # @since 0.0.1
263
+ def initialize(item, current_intention:)
264
+ super(item)
265
+ @current_intention = current_intention
266
+ end
267
+
268
+ # Access the item's state via a state wrapper. This only allows
269
+ # setting new fields or reading fields that already exist.
270
+ #
271
+ # @return [Demiurge::ActionItemInternal::ActionItemStateWrapper] The state wrapper to control access
272
+ # @since 0.0.1
273
+ def state
274
+ @state_wrapper ||= ActionItemInternal::ActionItemStateWrapper.new(@item)
275
+ end
276
+
277
+ private
278
+ def to_demiurge_name(item)
279
+ return item if item.is_a?(String)
280
+ return item.name if item.respond_to?(:name)
281
+ raise Demiurge::Errors::BadScriptError.new("Not sure how to convert PORO to Demiurge name: #{item.inspect}!",
282
+ execution_context: @item.engine.execution_context)
283
+ end
284
+ public
285
+
286
+ # Send a notification, starting from the location of the
287
+ # ActionItem. Any fields other than the special "type", "zone",
288
+ # "location" and "actor" fields will be sent as additional
289
+ # notification fields.
290
+ #
291
+ # @param data [Hash] The fields for the notification to send
292
+ # @option data [String] type The notification type to send
293
+ # @option data [String] zone The zone name to send the notification in; defaults to ActionItem's zone
294
+ # @option data [String] location The location name to send the notification in; defaults to ActionItem's location
295
+ # @option data [String] actor The acting item's name; defaults to this ActionItem
296
+ # @return [void]
297
+ # @since 0.0.1
298
+ def notification(data)
299
+ type = data.delete("type") || data.delete(:type) || data.delete("type") || data.delete(:type)
300
+ zone = to_demiurge_name(data.delete("zone") || data.delete(:zone) || @item.zone)
301
+ location = to_demiurge_name(data.delete("location") || data.delete(:location) || @item.location)
302
+ actor = to_demiurge_name(data.delete("actor") || data.delete(:actor) || @item)
303
+ @item.engine.send_notification(data, type: type.to_s, zone: zone, location: location, actor: actor, include_context: true)
304
+ nil
305
+ end
306
+
307
+ # Create an action to be executed immediately. This doesn't go
308
+ # through an agent's action queue or make anybody busy. It just
309
+ # happens during the current tick, but it uses the normal
310
+ # allow/offer/execute/notify cycle.
311
+ #
312
+ # @param name [String] The action name
313
+ # @param args [Array] Additional arguments to send to the action
314
+ # @return [void]
315
+ # @since 0.0.1
316
+ def action(name, *args)
317
+ intention = ActionItemInternal::ActionIntention.new(engine, @item.name, name, *args)
318
+ @item.engine.queue_intention(intention)
319
+ nil
320
+ end
321
+
322
+ # For tiled maps, cut the position string apart into a location
323
+ # and X and Y tile coordinates within that location.
324
+ #
325
+ # @param position [String] The position string
326
+ # @return [String, Integer, Integer] The location string, the X coordinate and the Y coordinate
327
+ # @since 0.0.1
328
+ def position_to_location_and_tile_coords(position)
329
+ ::Demiurge::TmxLocation.position_to_loc_coords(position)
330
+ end
331
+
332
+ # Cancel the current intention. Raise a NoCurrentIntentionError if there isn't one.
333
+ #
334
+ # @param reason [String] The reason to cancel
335
+ # @param extra_info [Hash] Additional cancellation info, if any
336
+ # @return [void]
337
+ # @since 0.0.1
338
+ def cancel_intention(reason, extra_info = {})
339
+ raise ::Demiurge::Errors::NoCurrentIntentionError.new("No current intention in action of item #{@item.name}!", { "script_item": @item.name },
340
+ execution_context: @item.engine.execution_context) unless @current_intention
341
+ @current_intention.cancel(reason, extra_info)
342
+ nil
343
+ end
344
+
345
+ # Cancel the current intention. Do nothing if there isn't one.
346
+ #
347
+ # @param reason [String] The reason to cancel
348
+ # @param extra_info [Hash] Additional cancellation info, if any
349
+ # @return [void]
350
+ # @since 0.0.1
351
+ def cancel_intention_if_present(reason, extra_info = {})
352
+ @current_intention.cancel(reason, extra_info) if @current_intention
353
+ end
354
+ end
355
+
356
+ # This is a BlockRunner for an agent's actions - it will be used if
357
+ # "engine_code" isn't set and the item for the action is an agent.
358
+ #
359
+ # @since 0.0.1
360
+ class ActionItemInternal::AgentBlockRunner < ActionItemInternal::ActionItemBlockRunner
361
+ # Move the agent to a specific position immediately. Don't play a
362
+ # walking animation or anything. Just put it where it needs to be.
363
+ #
364
+ # @param position [String] The position to move to
365
+ # @return [void]
366
+ # @since 0.0.1
367
+ def move_to_instant(position)
368
+ # TODO: We don't have a great way to do this for non-agent entities. How does "accomodate" work for non-agents?
369
+ # This may be app-specific.
370
+
371
+ loc_name, next_x, next_y = TmxLocation.position_to_loc_coords(position)
372
+ location = @item.engine.item_by_name(loc_name)
373
+ if !location
374
+ cancel_intention_if_present "Location #{loc_name.inspect} doesn't exist.", "position" => position, "mover" => @item.name
375
+ elsif location.can_accomodate_agent?(@item, position)
376
+ @item.move_to_position(position)
377
+ else
378
+ cancel_intention_if_present "That position is blocked.", "position" => position, "message" => "position blocked", "mover" => @item.name
379
+ end
380
+ end
381
+
382
+ # Queue an action for this agent, to be performed during the next
383
+ # tick.
384
+ #
385
+ # @param action_name [String] The action name to queue up
386
+ # @param args [Array] Additional arguments to pass to the action block
387
+ # @return [void]
388
+ # @since 0.0.1
389
+ def queue_action(action_name, *args)
390
+ unless @item.is_a?(::Demiurge::Agent)
391
+ @engine.admin_warning("Trying to queue an action #{action_name.inspect} for an item #{@item.name.inspect} that isn't an agent! Skipping.")
392
+ return
393
+ end
394
+ act = @item.get_action(action_name)
395
+ unless act
396
+ raise Demiurge::Errors::NoSuchActionError.new("Trying to queue an action #{action_name.inspect} for an item #{@item.name.inspect} that doesn't have it!",
397
+ "item" => @item.name, "action" => action_name, execution_context: @item.engine.execution_context)
398
+ return
399
+ end
400
+ @item.queue_action(action_name, args)
401
+ end
402
+
403
+ # Dump the engine's state as JSON, as an admin-only action.
404
+ #
405
+ # @param filename [String] The filename to dump state to.
406
+ # @return [void]
407
+ # @since 0.0.1
408
+ def dump_state(filename = "statedump.json")
409
+ unless @item.state["admin"] # Admin-only command
410
+ cancel_intention_if_present("The dump_state operation is admin-only!")
411
+ return
412
+ end
413
+
414
+ ss = @item.engine.structured_state
415
+ File.open(filename) do |f|
416
+ f.print MultiJson.dump(ss, :pretty => true)
417
+ end
418
+ nil
419
+ end
420
+ end
421
+
422
+ # An Intention for an ActionItem to perform one of its actions. This
423
+ # isn't an agent-specific intention which checks if the agent is
424
+ # busy and performs the action exclusively. Instead, it's an
425
+ # ActionItem performing this action as soon as the next tick happens
426
+ # - more than one can occur, for instance.
427
+ #
428
+ # @since 0.0.1
429
+ class ActionItemInternal::ActionIntention < Demiurge::Intention
430
+ # @return [String] The action name to perform
431
+ # @since 0.0.1
432
+ attr :action_name
433
+
434
+ # @return [Array] Additional arguments to pass to the argument's code block
435
+ # @since 0.0.1
436
+ attr :action_args
437
+
438
+ # Constructor. Pass in the engine, item name, action name and additional arguments.
439
+ #
440
+ # @param engine [Demiurge::Engine] The engine this Intention operates within
441
+ # @param name [String] The item name of the ActionItem acting
442
+ # @param action_name [String] The action name to perform
443
+ # @param args [Array] Additional arguments to pass to the code block
444
+ # @return [void]
445
+ # @since 0.0.1
446
+ def initialize(engine, name, action_name, *args)
447
+ @name = name
448
+ @item = engine.item_by_name(name)
449
+ raise Demiurge::Errors::NoSuchAgentError.new("Can't get agent's item for name #{name.inspect}!", execution_context: engine.execution_context) unless @item
450
+ @action_name = action_name
451
+ @action_args = args
452
+ super(engine)
453
+ nil
454
+ end
455
+
456
+ # For now, ActionIntentions don't have a way to specify "allowed"
457
+ # blocks in their DSL, so they are always considered "allowed".
458
+ #
459
+ # return [void]
460
+ # @since 0.0.1
461
+ def allowed?
462
+ true
463
+ end
464
+
465
+ # Make an offer of this ActionIntention and see if it is cancelled
466
+ # or modified. By default, offers are coordinated through the
467
+ # item's location.
468
+ #
469
+ # return [void]
470
+ # @since 0.0.1
471
+ # @note This method changed signature in 0.2.0 to stop taking an intention ID.
472
+ def offer
473
+ loc = @item.location || @item.zone
474
+ @engine.push_context("offered_action" => @action_name, "offered_location" => loc.name, "offering_item" => @item.name) do
475
+ loc.receive_offer(@action_name, self)
476
+ end
477
+ end
478
+
479
+ # Apply the ActionIntention's effects to the appropriate StateItems.
480
+ #
481
+ # return [void]
482
+ # @since 0.0.1
483
+ def apply
484
+ @item.run_action(@action_name, *@action_args, current_intention: self)
485
+ end
486
+
487
+ # Send out a notification to indicate this ActionIntention was
488
+ # cancelled. If "silent" is set to true in the cancellation info,
489
+ # no notification will be sent.
490
+ #
491
+ # @return [void]
492
+ # @since 0.0.1
493
+ def cancel_notification
494
+ return if @cancelled_info && @cancelled_info["silent"]
495
+ @engine.send_notification({
496
+ reason: @cancelled_reason,
497
+ by: @cancelled_by,
498
+ id: @intention_id,
499
+ intention_type: self.class.to_s,
500
+ info: @cancelled_info,
501
+ },
502
+ type: Demiurge::Notifications::IntentionCancelled,
503
+ zone: @item.zone_name,
504
+ location: @item.location_name,
505
+ actor: @item.name,
506
+ include_context: true)
507
+ nil
508
+ end
509
+
510
+ # Send out a notification to indicate this ActionIntention was
511
+ # applied.
512
+ #
513
+ # @return [void]
514
+ # @since 0.2.0
515
+ def apply_notification
516
+ @engine.send_notification({
517
+ id: @intention_id,
518
+ intention_type: self.class.to_s,
519
+ },
520
+ type: Demiurge::Notifications::IntentionApplied,
521
+ zone: @item.zone_name,
522
+ location: @item.location_name,
523
+ actor: @item.name,
524
+ include_context: true)
525
+ nil
526
+ end
527
+ end
528
+
529
+ # This class acts to wrap item state to avoid reading fields that
530
+ # haven't been set. Later, it may prevent access to protected state
531
+ # from lower-privilege code. Though it should always be kept in
532
+ # mind that no World File DSL code is actually secure. At best,
533
+ # security in this API may prevent accidents by the
534
+ # well-intentioned.
535
+ #
536
+ # ActionItemStateWrappers can act as Hashes (preferred) with
537
+ # square-bracket assignment, or can use (deprecated) method_missing
538
+ # to set fields. The method_missing version is deprecated both
539
+ # because it's slower and because it only allows certain key names
540
+ # to be used.
541
+ #
542
+ # @api private
543
+ # @since 0.0.1
544
+ class ActionItemInternal::ActionItemStateWrapper
545
+ def initialize(item)
546
+ @item = item
547
+ end
548
+
549
+ def has_key?(key)
550
+ @item.__state_internal.has_key?(key)
551
+ end
552
+
553
+ def [](key)
554
+ unless @item.__state_internal.has_key?(key)
555
+ raise ::Demiurge::Errors::NoSuchStateKeyError.new("No such state key as #{method_name.inspect}",
556
+ "method" => method_name, "item" => @item.name,
557
+ execution_context: @item.engine.execution_context)
558
+ end
559
+ @item.__state_internal[key]
560
+ end
561
+
562
+ def []=(key, value)
563
+ @item.__state_internal[key] = value
564
+ end
565
+
566
+ def method_missing(method_name, *args, &block)
567
+ if method_name.to_s[-1] == "="
568
+ getter_name = method_name.to_s[0..-2]
569
+ setter_name = method_name.to_s
570
+ else
571
+ getter_name = method_name.to_s
572
+ setter_name = method_name.to_s + "="
573
+ end
574
+
575
+ if @item.state.has_key?(getter_name) || method_name.to_s[-1] == "="
576
+ self.class.send(:define_method, getter_name) do
577
+ @item.__state_internal[getter_name]
578
+ end
579
+ self.class.send(:define_method, setter_name) do |val|
580
+ @item.__state_internal[getter_name] = val
581
+ end
582
+
583
+ # Call to new defined method
584
+ return self.send(method_name, *args, &block)
585
+ end
586
+
587
+ # Nope, no matching state.
588
+ raise ::Demiurge::Errors::NoSuchStateKeyError.new("No such state key as #{method_name.inspect}", "method" => method_name, "item" => @item.name,
589
+ execution_context: @item.engine.execution_context)
590
+ super
591
+ end
592
+
593
+ def respond_to_missing?(method_name, include_private = false)
594
+ @item.state.has_key?(method_name.to_s) || super
595
+ end
596
+ end
597
+
598
+ # This is a simple Intention that performs a particular action every
599
+ # so many ticks. It expects its state to be set up via the DSL
600
+ # Builder classes.
601
+ #
602
+ # @since 0.0.1
603
+ class ActionItemInternal::EveryXTicksIntention < Intention
604
+ def initialize(engine, name)
605
+ @name = name
606
+ super(engine)
607
+ end
608
+
609
+ def allowed?
610
+ true
611
+ end
612
+
613
+ # For now, empty. Later we'll want it to honor
614
+ # the offer setting of the underlying action.
615
+ def offer
616
+ end
617
+
618
+ # Shouldn't normally happen, but just in case...
619
+ def cancel_notification
620
+ # "Silent" notifications are things like an agent's action queue
621
+ # being empty so it cancels its intention. These are normal
622
+ # operation and nobody is likely to need notification every
623
+ # tick that they didn't ask to do anything so they didn't.
624
+ return if @cancelled_info && @cancelled_info["silent"]
625
+ item = @engine.item_by_name(@name)
626
+ @engine.send_notification({ reason: @cancelled_reason, by: @cancelled_by, id: @intention_id, intention_type: self.class.to_s },
627
+ type: Demiurge::Notifications::IntentionCancelled, zone: item.zone_name, location: item.location_name, actor: item.name,
628
+ include_context: true)
629
+ end
630
+
631
+ def apply
632
+ item = @engine.item_by_name(@name)
633
+ everies = item.state["everies"]
634
+ everies.each do |every|
635
+ every["counter"] += 1
636
+ if every["counter"] >= every["every"]
637
+ item.run_action(every["action"])
638
+ every["counter"] = 0
639
+ end
640
+ end
641
+ end
642
+ end
643
+ end