demiurge 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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