gamefic 3.6.0 → 4.0.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.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +0 -3
  3. data/CHANGELOG.md +19 -0
  4. data/Rakefile +1 -0
  5. data/gamefic.gemspec +1 -1
  6. data/lib/gamefic/action.rb +68 -54
  7. data/lib/gamefic/active/cue.rb +84 -6
  8. data/lib/gamefic/active/messaging.rb +8 -0
  9. data/lib/gamefic/active/narratives.rb +101 -0
  10. data/lib/gamefic/active.rb +80 -92
  11. data/lib/gamefic/binding.rb +44 -0
  12. data/lib/gamefic/chapter.rb +30 -46
  13. data/lib/gamefic/command.rb +22 -40
  14. data/lib/gamefic/core_ext/array.rb +7 -7
  15. data/lib/gamefic/core_ext/string.rb +2 -2
  16. data/lib/gamefic/describable.rb +13 -0
  17. data/lib/gamefic/dispatcher.rb +35 -55
  18. data/lib/gamefic/entity.rb +6 -5
  19. data/lib/gamefic/expression.rb +1 -11
  20. data/lib/gamefic/logging.rb +3 -10
  21. data/lib/gamefic/match.rb +23 -0
  22. data/lib/gamefic/messenger.rb +1 -1
  23. data/lib/gamefic/narrative.rb +38 -74
  24. data/lib/gamefic/narrator.rb +77 -0
  25. data/lib/gamefic/node.rb +40 -8
  26. data/lib/gamefic/order.rb +53 -0
  27. data/lib/gamefic/plot.rb +41 -59
  28. data/lib/gamefic/props/default.rb +5 -17
  29. data/lib/gamefic/props/multiple_choice.rb +5 -2
  30. data/lib/gamefic/props/multiple_partial.rb +16 -0
  31. data/lib/gamefic/props/output.rb +7 -5
  32. data/lib/gamefic/props/yes_or_no.rb +2 -2
  33. data/lib/gamefic/props.rb +1 -0
  34. data/lib/gamefic/proxy/attr.rb +11 -0
  35. data/lib/gamefic/proxy/base.rb +3 -15
  36. data/lib/gamefic/proxy/config.rb +2 -2
  37. data/lib/gamefic/proxy/pick.rb +3 -3
  38. data/lib/gamefic/proxy/pick_ex.rb +11 -0
  39. data/lib/gamefic/proxy.rb +3 -71
  40. data/lib/gamefic/query/ascendants.rb +16 -0
  41. data/lib/gamefic/query/base.rb +47 -73
  42. data/lib/gamefic/query/children.rb +15 -0
  43. data/lib/gamefic/query/descendants.rb +17 -0
  44. data/lib/gamefic/query/extended.rb +20 -0
  45. data/lib/gamefic/query/family.rb +27 -0
  46. data/lib/gamefic/query/global.rb +22 -0
  47. data/lib/gamefic/query/integer.rb +32 -0
  48. data/lib/gamefic/query/myself.rb +13 -0
  49. data/lib/gamefic/query/parent.rb +13 -0
  50. data/lib/gamefic/query/result.rb +1 -1
  51. data/lib/gamefic/query/siblings.rb +12 -0
  52. data/lib/gamefic/query/subqueries.rb +17 -0
  53. data/lib/gamefic/query/text.rb +8 -9
  54. data/lib/gamefic/query.rb +11 -3
  55. data/lib/gamefic/request.rb +60 -0
  56. data/lib/gamefic/response.rb +46 -72
  57. data/lib/gamefic/scanner/nesting.rb +6 -6
  58. data/lib/gamefic/scanner/result.rb +3 -0
  59. data/lib/gamefic/scanner/strict.rb +14 -4
  60. data/lib/gamefic/scanner.rb +11 -6
  61. data/lib/gamefic/scene/active_choice.rb +75 -0
  62. data/lib/gamefic/scene/activity.rb +7 -3
  63. data/lib/gamefic/scene/base.rb +123 -0
  64. data/lib/gamefic/scene/conclusion.rb +4 -1
  65. data/lib/gamefic/scene/multiple_choice.rb +14 -11
  66. data/lib/gamefic/scene/pause.rb +5 -1
  67. data/lib/gamefic/scene/yes_or_no.rb +9 -0
  68. data/lib/gamefic/scene.rb +2 -1
  69. data/lib/gamefic/scriptable/hooks.rb +161 -0
  70. data/lib/gamefic/scriptable/queries.rb +38 -29
  71. data/lib/gamefic/scriptable/responses.rb +70 -0
  72. data/lib/gamefic/scriptable/scenes.rb +88 -115
  73. data/lib/gamefic/scriptable/seeds.rb +69 -0
  74. data/lib/gamefic/scriptable/syntaxes.rb +29 -0
  75. data/lib/gamefic/scriptable.rb +14 -199
  76. data/lib/gamefic/{scriptable → scripting}/entities.rb +22 -22
  77. data/lib/gamefic/scripting/hooks.rb +45 -0
  78. data/lib/gamefic/{scriptable → scripting}/proxies.rb +5 -3
  79. data/lib/gamefic/scripting/responses.rb +21 -0
  80. data/lib/gamefic/scripting/scenes.rb +57 -0
  81. data/lib/gamefic/scripting/seeds.rb +10 -0
  82. data/lib/gamefic/scripting/syntaxes.rb +13 -0
  83. data/lib/gamefic/scripting.rb +43 -0
  84. data/lib/gamefic/subplot.rb +11 -22
  85. data/lib/gamefic/syntax.rb +39 -24
  86. data/lib/gamefic/version.rb +1 -1
  87. data/lib/gamefic.rb +6 -7
  88. metadata +38 -41
  89. data/lib/gamefic/active/epic.rb +0 -74
  90. data/lib/gamefic/active/take.rb +0 -67
  91. data/lib/gamefic/block.rb +0 -28
  92. data/lib/gamefic/callback.rb +0 -16
  93. data/lib/gamefic/proxy/plot_pick.rb +0 -11
  94. data/lib/gamefic/query/abstract.rb +0 -12
  95. data/lib/gamefic/query/general.rb +0 -41
  96. data/lib/gamefic/query/scoped.rb +0 -27
  97. data/lib/gamefic/rulebook/calls.rb +0 -86
  98. data/lib/gamefic/rulebook/events.rb +0 -65
  99. data/lib/gamefic/rulebook/hooks.rb +0 -57
  100. data/lib/gamefic/rulebook/scenes.rb +0 -68
  101. data/lib/gamefic/rulebook.rb +0 -125
  102. data/lib/gamefic/scene/default.rb +0 -88
  103. data/lib/gamefic/scope/base.rb +0 -44
  104. data/lib/gamefic/scope/children.rb +0 -16
  105. data/lib/gamefic/scope/descendants.rb +0 -16
  106. data/lib/gamefic/scope/family.rb +0 -43
  107. data/lib/gamefic/scope/myself.rb +0 -13
  108. data/lib/gamefic/scope/parent.rb +0 -13
  109. data/lib/gamefic/scope/siblings.rb +0 -14
  110. data/lib/gamefic/scope.rb +0 -9
  111. data/lib/gamefic/scriptable/actions.rb +0 -137
  112. data/lib/gamefic/scriptable/events.rb +0 -71
  113. data/lib/gamefic/scriptable/plot_proxies.rb +0 -29
  114. data/lib/gamefic/snapshot.rb +0 -44
  115. data/lib/gamefic/stage.rb +0 -51
  116. data/lib/gamefic/syntax/template.rb +0 -67
  117. data/lib/gamefic/vault.rb +0 -52
@@ -2,9 +2,8 @@
2
2
 
3
3
  require 'set'
4
4
  require 'gamefic/active/cue'
5
- require 'gamefic/active/epic'
6
5
  require 'gamefic/active/messaging'
7
- require 'gamefic/active/take'
6
+ require 'gamefic/active/narratives'
8
7
 
9
8
  module Gamefic
10
9
  # The Active module gives entities the ability to perform actions and
@@ -15,33 +14,21 @@ module Gamefic
15
14
  include Logging
16
15
  include Messaging
17
16
 
18
- # The cue that will be used to create a scene at the beginning of the next
19
- # turn.
17
+ # The most recently started cue.
20
18
  #
21
- # @return [Active::Cue, nil]
22
- attr_reader :next_cue
23
-
24
- # @return [String, nil]
25
- attr_reader :last_input
26
-
27
- # @return [Symbol, nil]
28
- def next_scene
29
- next_cue&.scene
30
- end
19
+ # @return [Cue, nil]
20
+ attr_reader :last_cue
31
21
 
32
- # The rulebooks that will be used to perform commands. Every plot and
33
- # subplot has its own rulebook.
22
+ # The cue that will be started on the next turn.
34
23
  #
35
- # @return [Set<Gamefic::World::Rulebook>]
36
- # def rulebooks
37
- # @rulebooks ||= Set.new
38
- # end
24
+ # @return [Cue, nil]
25
+ attr_reader :next_cue
39
26
 
40
27
  # The narratives in which the entity is participating.
41
28
  #
42
- # @return [Epic]
43
- def epic
44
- @epic ||= Epic.new
29
+ # @return [Narratives]
30
+ def narratives
31
+ @narratives ||= Narratives.new
45
32
  end
46
33
 
47
34
  # An array of commands waiting to be executed.
@@ -59,14 +46,7 @@ module Gamefic
59
46
  #
60
47
  # @return [Props::Output]
61
48
  def output
62
- @output ||= Props::Output.new.freeze
63
- end
64
-
65
- # The output from the previous turn.
66
- #
67
- # @return [Props::Output]
68
- def last_output
69
- @last_output ||= output
49
+ last_cue&.output || Props::Output::EMPTY
70
50
  end
71
51
 
72
52
  # Perform a command.
@@ -77,21 +57,24 @@ module Gamefic
77
57
  # @example Send a command as a string
78
58
  # character.perform "take the key"
79
59
  #
80
- # @param command [String]
81
- # @return [Action, nil]
82
- def perform(command)
83
- dispatchers.push Dispatcher.dispatch(self, command)
84
- dispatchers.last.execute.tap { dispatchers.pop }
60
+ # @param input [String]
61
+ # @return [Command, nil]
62
+ def perform(input)
63
+ dispatchers.push Dispatcher.new(Request.new(self, input))
64
+ dispatchers.last.execute.tap do |command|
65
+ dispatchers.pop
66
+ @acting = true if command&.active?
67
+ end
85
68
  end
86
69
 
87
70
  # Quietly perform a command.
88
71
  # This method executes the command exactly as #perform does, except it
89
72
  # buffers the resulting output instead of sending it to messages.
90
73
  #
91
- # @param command [String]
74
+ # @param input [String]
92
75
  # @return [String] The output that resulted from performing the command.
93
- def quietly(command)
94
- messenger.buffer { perform command }
76
+ def quietly(input)
77
+ messenger.buffer { perform input }
95
78
  end
96
79
 
97
80
  # Perform an action.
@@ -107,10 +90,13 @@ module Gamefic
107
90
  #
108
91
  # @param verb [Symbol]
109
92
  # @param params [Array]
110
- # @return [Action, nil]
93
+ # @return [Command, nil]
111
94
  def execute(verb, *params)
112
- dispatchers.push Dispatcher.dispatch_from_params(self, verb, params)
113
- dispatchers.last.execute.tap { dispatchers.pop }
95
+ dispatchers.push Dispatcher.new(Order.new(self, verb, params))
96
+ dispatchers.last.execute.tap do |command|
97
+ dispatchers.pop
98
+ @acting = true if command&.active?
99
+ end
114
100
  end
115
101
 
116
102
  # Proceed to the next Action in the current stack.
@@ -145,89 +131,91 @@ module Gamefic
145
131
  #
146
132
  # @raise [ArgumentError] if the scene is not valid
147
133
  #
148
- # @param scene [Symbol]
134
+ # @param scene [Class<Scene::Base>, Symbol]
149
135
  # @param context [Hash] Extra data to pass to the scene's props
150
136
  # @return [Cue]
151
137
  def cue scene, **context
152
- return @next_cue if @next_cue&.scene == scene && @next_cue&.context == context
138
+ return @next_cue if @next_cue&.key == scene && @next_cue&.context == context
153
139
 
154
- logger.debug "Overwriting existing cue `#{@next_cue.scene}` with `#{scene}`" if @next_cue
140
+ logger.debug "Overwriting existing cue `#{@next_cue.key}` with `#{scene}`" if @next_cue
155
141
 
156
- @next_cue = Cue.new(scene, **context)
142
+ @next_cue = Cue.new(self, scene, current, **context)
157
143
  end
158
144
  alias prepare cue
159
145
 
160
- # @return [void]
161
- def start_take
162
- ensure_cue
163
- @last_cue = @next_cue
164
- cue :default_scene
165
- @props = Take.start(self, @last_cue)
166
- @last_output = self.output
167
- @props.output[:last_prompt] = @last_output.prompt
168
- @props.output[:last_input] = @last_input
169
- @output = @props.output.dup.freeze
170
- end
171
-
172
- # @return [void]
173
- def finish_take
174
- return unless @last_cue
175
-
176
- Take.finish(self, @last_cue, @props)
177
- @last_input = @props.input
178
- end
179
-
180
146
  # Restart the scene from the most recent cue.
181
147
  #
182
148
  # @return [Cue, nil]
183
149
  def recue
184
- logger.warn "No scene to recue" unless @last_cue
185
-
186
- @next_cue = @last_cue
150
+ (@next_cue = @last_cue&.restart) || warn_nil('No scene to recue')
187
151
  end
188
152
 
189
- # Cue a conclusion. This method works like #cue, except it will raise an
190
- # error if the scene is not a conclusion.
191
- #
192
- # @raise [ArgumentError] if the requested scene is not a conclusion
153
+ # True if the actor is ready to leave the game.
193
154
  #
194
- # @param new_scene [Symbol]
195
- # @oaram context [Hash] Additional scene data
196
- # @return [Cue]
197
- def conclude scene, **context
198
- cue scene, **context
199
- available = epic.select_scene(scene)
200
- raise ArgumentError, "`#{scene}` is not a conclusion" unless available.conclusion?
155
+ def concluding?
156
+ narratives.empty? || last_cue&.type == 'Conclusion'
157
+ end
201
158
 
202
- @next_cue
159
+ def accessible
160
+ []
203
161
  end
204
162
 
205
- # True if the actor is ready to leave the game.
163
+ # True if the actor is participating in any narratives.
206
164
  #
207
- def concluding?
208
- epic.empty? || @props&.scene&.fetch(:type) == 'Conclusion'
165
+ def participating?
166
+ !narratives.empty?
209
167
  end
210
168
 
211
- def accessible?
212
- false
169
+ # True if the actor can perform the verb (i.e., an active narrative
170
+ # understands it).
171
+ #
172
+ # @param verb [String, Symbol]
173
+ def can?(verb)
174
+ narratives.understand?(verb)
213
175
  end
214
176
 
177
+ # Move next_cue into last_cue. This method is typically called by the
178
+ # narrator at the start of a turn. It returns the last cue.
179
+ #
180
+ # @return [Cue, nil]
181
+ def rotate_cue
182
+ @acting = false
183
+ @last_cue = @next_cue
184
+ @next_cue = nil
185
+ @last_cue
186
+ end
187
+
188
+ # True if the actor performed a command this turn. False if the actor has
189
+ # not performed a command yet or has only performed meta commands.
190
+ #
215
191
  def acting?
216
- !epic.empty?
192
+ @acting ||= false
193
+ end
194
+
195
+ # The input from the last finished cue.
196
+ #
197
+ # @return [String, nil]
198
+ def last_input
199
+ output.last_input
217
200
  end
218
201
 
219
202
  private
220
203
 
204
+ # Get the currently bound or primary narrative.
205
+ #
206
+ # @return [Narrative, nil]
207
+ def current
208
+ Binding.for(self) || narratives.first
209
+ end
210
+
221
211
  # @return [Array<Dispatcher>]
222
212
  def dispatchers
223
213
  @dispatchers ||= []
224
214
  end
225
215
 
226
- def ensure_cue
227
- return if next_cue
228
-
229
- logger.debug "Using default scene for actor without cue"
230
- cue :default_scene
216
+ def warn_nil(message)
217
+ logger.warn message
218
+ nil
231
219
  end
232
220
  end
233
221
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ class Binding
5
+ class << self
6
+ def registry
7
+ @registry ||= {}
8
+ end
9
+
10
+ def push(object, narrative)
11
+ registry[object] ||= []
12
+ registry[object].push narrative
13
+ end
14
+
15
+ def pop(object)
16
+ registry[object].pop
17
+ registry.delete(object) if registry[object].empty?
18
+ end
19
+
20
+ def for(object)
21
+ registry.fetch(object, []).last
22
+ end
23
+ end
24
+
25
+ attr_reader :narrative
26
+
27
+ attr_reader :code
28
+
29
+ # @param narrative [Narrative]
30
+ # @param code [Proc]
31
+ def initialize(narrative, code)
32
+ @narrative = narrative
33
+ @code = code
34
+ end
35
+
36
+ def call(*args)
37
+ args.each { |arg| Binding.push arg, @narrative }
38
+ @narrative.instance_exec(*args, &@code)
39
+ ensure
40
+ args.each { |arg| Binding.pop arg }
41
+ end
42
+ alias [] call
43
+ end
44
+ end
@@ -1,71 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gamefic
4
- # Chapters are plot extensions that manage their own namespaces. Authors can
5
- # use them to encapsulate related content in a separate object instead of
6
- # adding the required instance variables, methods, and attributes to the
7
- # plot.
8
- #
9
- # Chapters are similar to subplots with three important exceptions:
10
- # * Chapters normally persist for the duration of a plot.
11
- # * Players do not need to be introduced to a chapter.
12
- # * Chapters share their plot's entities, players, and rulebook.
13
- #
14
- # @example
15
- # class MyChapter < Gamefic::Chapter
16
- # def thing
17
- # @thing ||= make Gamefic::Entity, name: 'chapter thing'
18
- # end
19
- # end
20
- #
21
- # class MyPlot < Gamefic::Plot
22
- # append MyChapter
23
- # end
24
- #
25
- # plot = MyPlot.new
26
- # plot.entities #=> [<#Gamefic::Entity a chapter thing>]
27
- # plot.thing # raises NoMethodError
28
- # plot.chapters.first.thing #=> <#Gamefic::Entity a chapter thing>
29
- #
30
4
  class Chapter < Narrative
31
- extend Scriptable::PlotProxies
32
-
33
5
  # @return [Plot]
34
6
  attr_reader :plot
35
7
 
8
+ # @return [Hash]
9
+ attr_reader :config
10
+
36
11
  # @param plot [Plot]
37
- def initialize(plot)
12
+ def initialize(plot, **config)
38
13
  @plot = plot
39
- # The plot is responsible for hydrating chapters
40
- super(hydrate: false)
14
+ @concluding = false
15
+ @config = config
16
+ configure
17
+ @config.freeze
18
+ super()
41
19
  end
42
20
 
43
- def script
44
- included_blocks.select(&:script?).each { |blk| Stage.run self, &blk.code }
21
+ def players
22
+ plot.players
45
23
  end
46
24
 
47
- def included_blocks
48
- self.class.included_blocks - plot.included_blocks
25
+ def conclude
26
+ # @todo Void entities?
27
+ @concluding = true
49
28
  end
50
29
 
51
- def rulebook
52
- plot.rulebook
30
+ def concluding?
31
+ @concluding
53
32
  end
54
33
 
55
- def entity_vault
56
- plot.entity_vault
34
+ def self.bind_from_plot *methods
35
+ methods.flatten.each do |method|
36
+ define_method(method) { plot.send(method) }
37
+ define_singleton_method(method) { Proxy::Attr.new(method) }
38
+ end
57
39
  end
58
40
 
59
- def player_vault
60
- plot.player_vault
41
+ def included_scripts
42
+ super - plot.included_scripts
61
43
  end
62
44
 
63
- def subplots
64
- plot.subplots
65
- end
45
+ # Subclasses can override this method to handle additional configuration.
46
+ #
47
+ def configure; end
66
48
 
67
- def branch(...)
68
- plot.branch(...)
49
+ class << self
50
+ def config
51
+ Proxy::Config.new
52
+ end
69
53
  end
70
54
  end
71
55
  end
@@ -10,59 +10,41 @@ module Gamefic
10
10
  # @return [Array<Array<Entity>, Entity, String>]
11
11
  attr_reader :arguments
12
12
 
13
- # @return [Integer]
14
- attr_reader :strictness
15
-
16
- # @return [Integer]
17
- attr_reader :precision
13
+ # @return [String, nil]
14
+ attr_reader :input
18
15
 
19
16
  # @param verb [Symbol]
20
17
  # @param arguments [Array<Array<Entity>, Entity, String>]
21
- # @param strictness [Integer]
22
- # @param precision [Integer]
23
- #
24
- # @todo Consider making strictness and precision required or providing
25
- # another generator
26
- def initialize verb, arguments, strictness = 0, precision = 0
18
+ # @param meta [Boolean]
19
+ # @param input [String, nil]
20
+ def initialize(verb, arguments, meta = false, input = nil)
27
21
  @verb = verb
28
22
  @arguments = arguments
29
- @strictness = strictness
30
- @precision = precision
23
+ @meta = meta
24
+ @input = input
25
+ @cancelled = false
31
26
  end
32
27
 
33
- def substantiality
34
- @substantiality ||= arguments.that_are(Entity).length + (verb ? 1 : 0)
28
+ def cancel
29
+ @cancelled = true
35
30
  end
31
+ alias stop cancel
36
32
 
37
- def inspect
38
- "#<#{self.class} #{([verb] + arguments).map(&:inspect).join(', ')}>"
33
+ def cancelled?
34
+ @cancelled
39
35
  end
36
+ alias stopped? cancelled?
40
37
 
41
- class << self
42
- # Compose a command from input.
43
- #
44
- # @param actor [Actor]
45
- # @param input [String]
46
- # @return [Command]
47
- def compose actor, input
48
- expressions = Syntax.tokenize(input, actor.epic.syntaxes)
49
- expressions.flat_map { |expression| expression_to_commands(actor, expression) }
50
- .first || Command.new(nil, [])
51
- end
38
+ def meta?
39
+ @meta
40
+ end
52
41
 
53
- private
42
+ def active?
43
+ !meta?
44
+ end
54
45
 
55
- # @param actor [Actor]
56
- # @param expression [Expression]
57
- # @return [Array<Command>]
58
- def expression_to_commands actor, expression
59
- Gamefic.logger.info "Evaluating #{expression.inspect}"
60
- actor.epic
61
- .responses_for(expression.verb)
62
- .map { |response| response.to_command(actor, expression) }
63
- .compact
64
- .sort_by.with_index { |result, idx| [-result.substantiality, -result.strictness, -result.precision, idx] }
65
- end
46
+ def inspect
47
+ "#<#{self.class} #{([verb] + arguments).map(&:inspect).join(', ')}>"
66
48
  end
67
49
  end
68
50
  end
@@ -46,24 +46,24 @@ class Array
46
46
  # animals = ['a dog', 'a cat', 'a mouse']
47
47
  # animals.join_and #=> 'a dog, a cat, and a mouse'
48
48
  #
49
- # @param sep [String] The separator for all but the last element
50
- # @param andSep [String] The separator for the last element
49
+ # @param separator [String] The separator for all but the last element
50
+ # @param and_separator [String] The separator for the last element
51
51
  # @param serial [Boolean] Use serial separators (e.g., serial commas)
52
52
  # @return [String]
53
- def join_and(sep = ', ', and_sep = ' and ', serial = true)
53
+ def join_and(separator: ', ', and_separator: ' and ', serial: true)
54
54
  if length < 3
55
- join(and_sep)
55
+ join(and_separator)
56
56
  else
57
57
  start = self[0..-2]
58
- start.join(sep) + "#{serial ? sep.strip : ''}#{and_sep}#{last}"
58
+ start.join(separator) + "#{serial ? separator.strip : ''}#{and_separator}#{last}"
59
59
  end
60
60
  end
61
61
 
62
62
  # @see Array#join_and
63
63
  #
64
64
  # @return [String]
65
- def join_or(sep = ', ', or_sep = ' or ', serial = true)
66
- join_and(sep, or_sep, serial)
65
+ def join_or(separator: ', ', or_separator: ' or ', serial: true)
66
+ join_and(separator: separator, and_separator: or_separator, serial: serial)
67
67
  end
68
68
 
69
69
  private
@@ -12,9 +12,9 @@ class String
12
12
 
13
13
  # Get an array of words split by any whitespace.
14
14
  #
15
- # @return [Array]
15
+ # @return [Array<String>]
16
16
  def keywords
17
- gsub(/[\s-]+/, ' ').strip.downcase.split - %w[a an the]
17
+ gsub(/[\s-]+/, ' ').strip.downcase.split
18
18
  end
19
19
 
20
20
  # @return [String]
@@ -19,6 +19,9 @@ module Gamefic
19
19
  # @return [String]
20
20
  attr_reader :synonyms
21
21
 
22
+ # @return [String]
23
+ attr_writer :nuance
24
+
22
25
  # The object's indefinite article (usually "a" or "an").
23
26
  #
24
27
  # @return [String]
@@ -33,6 +36,16 @@ module Gamefic
33
36
  "#{name} #{synonyms}".keywords
34
37
  end
35
38
 
39
+ # Optional words that shouldn't match an object on their own but might be
40
+ # used in a larger phrase. For example, if you have an entity named "dog"
41
+ # and its description calls it "sleepy," you might add "sleepy" to nuance
42
+ # so the phrase "sleepy dog" matches but the word "sleepy" alone does not.
43
+ #
44
+ # @return [String]
45
+ def nuance
46
+ @nuance ||= ''
47
+ end
48
+
36
49
  # The name of the object with an indefinite article.
37
50
  # Note: proper-named objects never append an article, though an article
38
51
  # may be included in its proper name.
@@ -4,33 +4,21 @@ module Gamefic
4
4
  # The action executor for character commands.
5
5
  #
6
6
  class Dispatcher
7
- include Logging
8
-
9
- # @param actor [Actor]
10
- # @param command [Command]
11
- def initialize actor, command
12
- @actor = actor
13
- @command = command
14
- @executed = false
15
- Gamefic.logger.info "Dispatching #{command.inspect}"
7
+ # @param actionable [#to_actions]
8
+ def initialize(actionable)
9
+ @actions = actionable.to_actions
16
10
  end
17
11
 
18
- # Run the dispatcher.
12
+ # Start executing actions in the dispatcher.
19
13
  #
20
- # @return [Action, nil]
14
+ # @return [Command, nil]
21
15
  def execute
22
- return if @executed
23
-
24
- @executed = true
25
- action = next_action
26
- return unless action
16
+ return if action || actions.empty?
27
17
 
28
- actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_before_actions action }
29
- return if action.cancelled?
30
-
31
- action.execute
32
- actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_after_actions action }
33
- action
18
+ @action = actions.shift
19
+ Gamefic.logger.info "Dispatching #{actor.inspect} #{command.inspect}"
20
+ run_hooks_and_response
21
+ command
34
22
  end
35
23
 
36
24
  # Execute the next available action.
@@ -39,51 +27,43 @@ module Gamefic
39
27
  #
40
28
  # @return [Action, nil]
41
29
  def proceed
42
- return unless @executed
30
+ return if !action || command.cancelled?
43
31
 
44
- next_action&.execute
32
+ actions.shift&.execute
45
33
  end
46
34
 
47
- # @param actor [Active]
48
- # @param input [String]
49
- # @return [Dispatcher]
50
- def self.dispatch actor, input
51
- # expressions = Syntax.tokenize(input, actor.epic.syntaxes)
52
- # new(actor, Command.compose(actor, expressions))
53
- new(actor, Command.compose(actor, input))
54
- end
35
+ private
55
36
 
56
- # @param actor [Active]
57
- # @param verb [Symbol]
58
- # @param params [Array<Object>]
59
- # @return [Dispatcher]
60
- def self.dispatch_from_params actor, verb, params
61
- command = Command.new(verb, params)
62
- new(actor, command)
63
- end
37
+ # @return [Array<Action>]
38
+ attr_reader :actions
64
39
 
65
- protected
40
+ # @return [Action, nil]
41
+ attr_reader :action
66
42
 
67
- # @return [Actor]
68
- attr_reader :actor
43
+ # @return [Actor, nil]
44
+ def actor
45
+ action.actor
46
+ end
69
47
 
70
48
  # @return [Command]
71
- attr_reader :command
72
-
73
- # @return [Array<Response>]
74
- def responses
75
- @responses ||= actor.epic.responses_for(command.verb)
49
+ def command
50
+ action.command
76
51
  end
77
52
 
78
- private
53
+ def run_hooks(list)
54
+ list.each do |blk|
55
+ blk[actor, command]
56
+ break if command.cancelled?
57
+ end
58
+ end
79
59
 
80
- # @return [Action, nil]
81
- def next_action
82
- while (response = responses.shift)
83
- next if response.queries.length < @command.arguments.length
60
+ def run_hooks_and_response
61
+ run_hooks actor.narratives.before_commands
62
+ command.freeze
63
+ return if command.cancelled?
84
64
 
85
- return Action.new(actor, @command.arguments, response) if response.accept?(actor, @command)
86
- end
65
+ action.execute
66
+ run_hooks actor.narratives.after_commands
87
67
  end
88
68
  end
89
69
  end