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
@@ -7,7 +7,7 @@ module Gamefic
7
7
  class Text < Base
8
8
  # @param argument [String, Regexp]
9
9
  # @param name [String, nil]
10
- def initialize argument = /.*/, name: self.class.name
10
+ def initialize(argument = /.*/, name: self.class.name)
11
11
  super(argument, name: name)
12
12
  validate_argument
13
13
  end
@@ -21,33 +21,32 @@ module Gamefic
21
21
  argument
22
22
  end
23
23
 
24
- def query _subject, token
24
+ def filter(_subject, token)
25
25
  if match? token
26
26
  Result.new(token, '')
27
27
  else
28
28
  Result.new(nil, token)
29
29
  end
30
30
  end
31
- alias filter query
32
31
 
33
32
  def precision
34
- 0
33
+ -10_000
35
34
  end
36
35
 
37
- def accept? _subject, argument
38
- match? argument
36
+ def accept?(_subject, token)
37
+ match?(token)
39
38
  end
40
39
 
41
40
  private
42
41
 
43
- def match? token
42
+ def match?(token)
44
43
  return false unless token.is_a?(String) && !token.empty?
45
44
 
46
45
  case argument
47
46
  when Regexp
48
- token =~ argument
47
+ token.match?(argument)
49
48
  else
50
- token == argument
49
+ argument == token
51
50
  end
52
51
  end
53
52
 
data/lib/gamefic/query.rb CHANGED
@@ -1,8 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'gamefic/query/base'
4
- require 'gamefic/query/general'
5
- require 'gamefic/query/abstract'
6
- require 'gamefic/query/scoped'
7
4
  require 'gamefic/query/text'
5
+ require 'gamefic/query/integer'
8
6
  require 'gamefic/query/result'
7
+ require 'gamefic/query/subqueries'
8
+ require 'gamefic/query/global'
9
+ require 'gamefic/query/children'
10
+ require 'gamefic/query/myself'
11
+ require 'gamefic/query/descendants'
12
+ require 'gamefic/query/ascendants'
13
+ require 'gamefic/query/parent'
14
+ require 'gamefic/query/siblings'
15
+ require 'gamefic/query/extended'
16
+ require 'gamefic/query/family'
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ # Build actions from text.
5
+ #
6
+ # Active#perform uses Request to parse user input into actions for execution
7
+ # by the Dispatcher.
8
+ #
9
+ class Request
10
+ # @param actor [Actor]
11
+ # @param input [String]
12
+ def initialize(actor, input)
13
+ @actor = actor
14
+ @input = input
15
+ end
16
+
17
+ # @return [Array<Action>]
18
+ def to_actions
19
+ Action.sort(
20
+ Syntax.tokenize(input, actor.narratives.syntaxes)
21
+ .flat_map { |expression| expression_to_actions(expression) }
22
+ )
23
+ end
24
+
25
+ private
26
+
27
+ # @return [Actor]
28
+ attr_reader :actor
29
+
30
+ # @return [String]
31
+ attr_reader :input
32
+
33
+ def expression_to_actions(expression)
34
+ Gamefic.logger.info "Evaluating #{expression.inspect}"
35
+ actor.narratives
36
+ .responses_for(expression.verb)
37
+ .map { |response| match_expression response, expression }
38
+ .compact
39
+ end
40
+
41
+ def match_expression(response, expression)
42
+ return nil if expression.tokens.length > response.queries.length
43
+
44
+ remainder = ''
45
+ matches = response.queries
46
+ .zip(expression.tokens)
47
+ .each_with_object([]) do |zipped, results|
48
+ query, token = zipped
49
+ result = query.filter(actor, "#{remainder} #{token}".strip)
50
+ return nil unless result.match
51
+
52
+ results.push Match.new(result.match, token.to_s[0..-result.remainder.length - 1], result.strictness)
53
+ remainder = result.remainder
54
+ end
55
+ return nil unless remainder.empty?
56
+
57
+ Action.new(actor, response, matches, input)
58
+ end
59
+ end
60
+ end
@@ -1,26 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'gamefic/scriptable'
4
+
3
5
  module Gamefic
4
6
  # A proc to be executed in response to a command that matches its verb and
5
7
  # queries.
6
8
  #
7
9
  class Response
10
+ include Scriptable::Queries
11
+
8
12
  # @return [Symbol]
9
13
  attr_reader :verb
10
14
 
11
15
  # @return [Array<Query::Base, Query::Text>]
12
16
  attr_reader :queries
13
17
 
18
+ # @return [Proc]
19
+ attr_reader :block
20
+
14
21
  # @param verb [Symbol]
15
- # @param narrative [Narrative]
16
- # @param args [Array<Object>]
22
+ # @param queries [Array<Object>]
17
23
  # @param meta [Boolean]
18
- # @param block [Proc]
19
- def initialize verb, narrative, *args, meta: false, &block
20
- @verb = verb
21
- @queries = map_queries(args, narrative)
24
+ def initialize verb, *queries, meta: false, &block
25
+ @verb = verb&.to_sym
22
26
  @meta = meta
23
- @callback = Callback.new(narrative, block)
27
+ @block = block
28
+ @queries = map_queries(queries)
24
29
  end
25
30
 
26
31
  # The `meta?` flag is just a way for authors to identify responses that
@@ -32,115 +37,84 @@ module Gamefic
32
37
  @meta
33
38
  end
34
39
 
35
- def hidden?
36
- @hidden ||= verb.to_s.start_with?('_')
37
- end
38
-
39
40
  def syntax
40
41
  @syntax ||= generate_default_syntax
41
42
  end
42
43
 
43
- # Return an Action if the Response can accept the actor's command.
44
- #
45
- # @param actor [Entity]
46
- # @param command [Command]
47
- # @return [Action, nil]
48
- def attempt actor, command
49
- return nil unless accept?(actor, command)
50
-
51
- Action.new(actor, command.arguments, self)
52
- end
53
-
54
44
  # True if the Response can be executed for the given actor and command.
55
45
  #
56
46
  # @param actor [Active]
57
47
  # @param command [Command]
58
- def accept? actor, command
48
+ def accept?(actor, command)
59
49
  command.verb == verb &&
60
50
  command.arguments.length == queries.length &&
61
51
  queries.zip(command.arguments).all? { |query, argument| query.accept?(actor, argument) }
62
52
  end
63
53
 
64
54
  def execute *args
65
- @callback.run(*args)
55
+ Gamefic.logger.warn "Executing unbound response #{inspect}" unless bound?
56
+ gamefic_binding.call(*args)
66
57
  end
67
58
 
59
+ # The total precision of all the response's queries.
60
+ #
61
+ # @note Precision is decreased if the response has a nil verb.
62
+ #
63
+ # @return [Integer]
68
64
  def precision
69
65
  @precision ||= calculate_precision
70
66
  end
71
67
 
72
- # Turn an actor and an expression into a command by matching the
73
- # expression's tokens to queries. Return nil if the expression
74
- # could not be matched.
75
- #
76
- # @param actor [Actor]
77
- # @param expression [Expression]
78
- # @return [Command, nil]
79
- def to_command actor, expression
80
- return log_and_discard unless expression.verb == verb && expression.tokens.length <= queries.length
81
-
82
- results = filter(actor, expression)
83
- return log_and_discard unless results
84
-
85
- Gamefic.logger.info "Accepted #{inspect}"
86
- Command.new(
87
- verb,
88
- results.map(&:match),
89
- results.sum(&:strictness),
90
- precision
91
- )
92
- end
93
-
94
68
  def inspect
95
69
  "#<#{self.class} #{([verb] + queries).map(&:inspect).join(', ')}>"
96
70
  end
97
71
 
98
- private
72
+ def bound?
73
+ !!gamefic_binding.narrative
74
+ end
99
75
 
100
- def log_and_discard
101
- Gamefic.logger.info "Discarded #{inspect}"
102
- nil
76
+ def bind(narrative)
77
+ clone.inject_binding narrative
103
78
  end
104
79
 
105
- def filter actor, expression
106
- remainder = ''
107
- result = queries.zip(expression.tokens)
108
- .map do |query, token|
109
- token = "#{remainder} #{token}".strip
110
- result = query.filter(actor, token)
111
- return nil unless result.match
112
-
113
- remainder = result.remainder
114
- result
115
- end
116
- result if remainder.empty?
80
+ protected
81
+
82
+ def inject_binding(narrative)
83
+ @queries = map_queries(narrative.unproxy(@queries))
84
+ @gamefic_binding = Binding.new(narrative, @block)
85
+ self
86
+ end
87
+
88
+ private
89
+
90
+ def gamefic_binding
91
+ @gamefic_binding ||= Binding.new(nil, @block)
117
92
  end
118
93
 
119
94
  def generate_default_syntax
120
95
  args = queries.length.times.map { |num| num.zero? ? ':var' : ":var#{num + 1}" }
121
96
  tmpl = "#{verb} #{args.join(' ')}".strip
122
- Syntax.new(tmpl.gsub('_', ' '), tmpl)
97
+ Syntax.new(tmpl, tmpl)
123
98
  end
124
99
 
100
+ # @return [Integer]
125
101
  def calculate_precision
126
102
  total = queries.sum(&:precision)
127
103
  total -= 1000 unless verb
128
104
  total
129
105
  end
130
106
 
131
- def map_queries args, narrative
132
- args.map do |arg|
133
- select_query(arg, narrative).tap { |qry| qry.narrative = narrative }
134
- end
107
+ def map_queries(args)
108
+ args.map { |arg| select_query(arg) }
135
109
  end
136
110
 
137
- def select_query arg, narrative
111
+ def select_query(arg)
138
112
  case arg
139
- when Entity, Class, Module, Proc, Proxy, Proxy::Base
140
- narrative.available(arg)
113
+ when Entity, Class, Module, Proc, Proxy::Base
114
+ available(arg)
141
115
  when String, Regexp
142
- narrative.plaintext(arg)
143
- when Query::Base, Query::Text
116
+ plaintext(arg)
117
+ when Query::Base
144
118
  arg
145
119
  else
146
120
  raise ArgumentError, "invalid argument in response: #{arg.inspect}"
@@ -14,14 +14,14 @@ module Gamefic
14
14
  def scan
15
15
  return unmatched_result unless token =~ NEST_REGEXP
16
16
 
17
- denest selection, token
17
+ denest
18
18
  end
19
19
 
20
20
  private
21
21
 
22
- def denest objects, token
23
- near = objects
24
- far = objects
22
+ def denest
23
+ near = selection
24
+ far = selection
25
25
  parts = token.split(NEST_REGEXP)
26
26
  until parts.empty?
27
27
  current = parts.pop
@@ -29,8 +29,8 @@ module Gamefic
29
29
  last_result = subprocessor.scan(far, current) if last_result.matched.empty? && near != far
30
30
  return unmatched_result if last_result.matched.empty? || last_result.matched.length > 1
31
31
 
32
- near = last_result.matched.first.children & objects
33
- far = last_result.matched.first.flatten & objects
32
+ near = last_result.matched.first.children & selection
33
+ far = last_result.matched.first.flatten & selection
34
34
  end
35
35
  last_result
36
36
  end
@@ -38,6 +38,9 @@ module Gamefic
38
38
  @processor = processor
39
39
  end
40
40
 
41
+ # The strictness of the scanner that produced this result.
42
+ #
43
+ # @return [Integer]
41
44
  def strictness
42
45
  @strictness ||= Scanner.strictness(processor)
43
46
  end
@@ -8,23 +8,33 @@ module Gamefic
8
8
  # matches one of the entity's keywords.
9
9
  #
10
10
  class Strict < Base
11
+ NOISE = %w[
12
+ a an the of some
13
+ ].freeze
14
+
11
15
  # @return [Result]
12
16
  def scan
13
17
  words = token.keywords
14
18
  available = selection.clone
15
19
  filtered = []
16
20
  words.each_with_index do |word, idx|
17
- tested = match_word(available, word)
18
- return matched_result(filtered, words[idx..].join(' ')) if tested.empty?
21
+ # @todo This might not be the best way to filter articles, but it works for now
22
+ tested = %w[a an the].include?(word) ? available : match_word(available, word)
23
+ return matched_result(reduce_noise(filtered, words[0, idx]), words[idx..].join(' ')) if tested.empty?
19
24
 
20
25
  filtered = tested
21
26
  available = filtered
22
27
  end
23
- matched_result filtered, ''
28
+ matched_result(reduce_noise(filtered, words), '')
24
29
  end
25
30
 
26
31
  def match_word available, word
27
- available.select { |obj| obj.keywords.include?(word) }
32
+ available.select { |obj| (obj.keywords + obj.nuance.keywords).include?(word) }
33
+ end
34
+
35
+ def reduce_noise entities, keywords
36
+ noiseless = keywords - NOISE
37
+ entities.reject { |entity| (noiseless - entity.nuance.keywords).empty? }
28
38
  end
29
39
  end
30
40
  end
@@ -17,12 +17,13 @@ module Gamefic
17
17
  #
18
18
  # @param selection [Array<Entity>]
19
19
  # @param token [String]
20
+ # @param use [Array<Scanner::Base>]
20
21
  # @return [Result]
21
- def self.scan selection, token
22
+ def self.scan(selection, token, use = processors)
22
23
  result = nil
23
- processors.each do |processor|
24
+ use.each do |processor|
24
25
  result = processor.scan(selection, token)
25
- break unless result.matched.empty?
26
+ break result unless result.matched.empty?
26
27
  end
27
28
  result
28
29
  end
@@ -34,8 +35,8 @@ module Gamefic
34
35
  # Processor classes should be in order from most to least strict rules
35
36
  # for matching tokens to entities.
36
37
  #
37
- # @param klasses [Array<Class<Base>>]
38
- # @return [Array<Class<Base>>]
38
+ # @param klasses [Array<Class<Scanner::Base>>]
39
+ # @return [Array<Class<Scanner::Base>>]
39
40
  def self.use *klasses
40
41
  processors.replace klasses.flatten
41
42
  end
@@ -45,7 +46,11 @@ module Gamefic
45
46
  @processors ||= []
46
47
  end
47
48
 
48
- def self.strictness processor
49
+ # A measure of a scan processor's strictness based on its order of use.
50
+ # Higher values indicate higher strictness.
51
+ #
52
+ # @return [Integer]
53
+ def self.strictness(processor)
49
54
  (processors.length - (processors.find_index(processor) || processors.length)) * 100
50
55
  end
51
56
 
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Scene
5
+ # A scene that presents a list of optional choices. The scene can still
6
+ # attempt to process input that does not match any of the options.
7
+ #
8
+ # Authors can use the `without_selection` class method to select one of
9
+ # three actions to take when the user does not enter one of the options:
10
+ # `:perform`, `:recue`, or `:continue`.
11
+ #
12
+ class ActiveChoice < MultipleChoice
13
+ WITHOUT_SELECTION_ACTIONS = %i[perform recue continue].freeze
14
+
15
+ use_props_class Props::MultipleChoice
16
+
17
+ def finish
18
+ return super if props.selected?
19
+
20
+ send(self.class.without_selection_action)
21
+ end
22
+
23
+ def without_selection_action
24
+ self.class.without_selection_action
25
+ end
26
+
27
+ def self.type
28
+ 'ActiveChoice'
29
+ end
30
+
31
+ # Select the behavior for input that does not match a selectable option.
32
+ # The available settings are `:perform`, `:recue`, and `:continue`.
33
+ #
34
+ # * `:perform` - Skip the `on_finish` blocks and try to perform the input
35
+ # as a command. This is the default behavior.
36
+ # * `:recue` - Restart the scene until the user makes a valid selection.
37
+ # This is the same behavior as a `MultipleChoice` scene.
38
+ # * `:continue` - Execute the `on_finish` blocks regardless of whether the
39
+ # input matches an option.
40
+ #
41
+ # @param action [Symbol]
42
+ def self.without_selection(action)
43
+ WITHOUT_SELECTION_ACTIONS.include?(action) ||
44
+ raise(ArgumentError, "without_selection_action must be one of #{WITHOUT_SELECTION_ACTIONS.map(&:inspect).join_or}")
45
+
46
+ @without_selection_action = action
47
+ end
48
+
49
+ # @return [Symbol]
50
+ def self.without_selection_action
51
+ @without_selection_action ||= :perform
52
+ end
53
+
54
+ def self.inherited(klass)
55
+ super
56
+ klass.without_selection without_selection_action
57
+ end
58
+
59
+ private
60
+
61
+ def perform
62
+ actor.perform props.input
63
+ end
64
+
65
+ def recue
66
+ actor.tell props.invalid_message
67
+ actor.recue
68
+ end
69
+
70
+ def continue
71
+ run_finish_blocks
72
+ end
73
+ end
74
+ end
75
+ end
@@ -4,10 +4,14 @@ module Gamefic
4
4
  module Scene
5
5
  # A scene that accepts player commands for actors to perform.
6
6
  #
7
- class Activity < Default
8
- def finish actor, props
9
- super
7
+ class Activity < Base
8
+ def finish
10
9
  actor.perform props.input
10
+ super
11
+ end
12
+
13
+ def self.type
14
+ 'Activity'
11
15
  end
12
16
  end
13
17
  end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Scene
5
+ # The base class for scenes. Authors can instantiate this class directly
6
+ # and customize it with on_start and on_finish blocks.
7
+ #
8
+ class Base
9
+ # @todo Code smell
10
+ attr_writer :name
11
+
12
+ attr_reader :actor, :narrative, :props, :context
13
+
14
+ # @param actor [Actor]
15
+ # @param narrative [Narrative]
16
+ # @param props [Props::Base]
17
+ def initialize(actor, narrative = nil, props = nil, **context)
18
+ @actor = actor
19
+ @narrative = narrative
20
+ @props = props || self.class.props_class.new
21
+ @context = context
22
+ end
23
+
24
+ def name
25
+ @name ||= self.class.nickname
26
+ end
27
+
28
+ def rename(name)
29
+ @name = name
30
+ end
31
+
32
+ # @return [String]
33
+ def type
34
+ self.class.type
35
+ end
36
+
37
+ # @return [Props::Default]
38
+ def start
39
+ run_start_blocks
40
+ props
41
+ end
42
+
43
+ # @return [void]
44
+ def finish
45
+ run_finish_blocks
46
+ end
47
+
48
+ def to_hash
49
+ { name: name, type: type }
50
+ end
51
+
52
+ def self.inherited(klass)
53
+ super
54
+ klass.use_props_class props_class
55
+ klass.start_blocks.concat start_blocks
56
+ klass.finish_blocks.concat finish_blocks
57
+ end
58
+
59
+ private
60
+
61
+ def execute(block)
62
+ Binding.new(narrative, block).call(actor, props, context)
63
+ end
64
+
65
+ def run_start_blocks
66
+ self.class.start_blocks.each { |blk| execute(blk) }
67
+ end
68
+
69
+ def run_finish_blocks
70
+ self.class.finish_blocks.each { |blk| execute(blk) }
71
+ end
72
+
73
+ class << self
74
+ attr_reader :context, :nickname
75
+
76
+ def type
77
+ 'Base'
78
+ end
79
+
80
+ def props_class
81
+ @props_class ||= Props::Default
82
+ end
83
+
84
+ def rename(nickname)
85
+ @nickname = nickname
86
+ end
87
+
88
+ # @return [Array<Proc>]
89
+ def start_blocks
90
+ @start_blocks ||= []
91
+ end
92
+
93
+ # @return [Array<Proc>]
94
+ def finish_blocks
95
+ @finish_blocks ||= []
96
+ end
97
+
98
+ # @yieldparam [Actor] The scene's actor
99
+ # @yieldparam [Props::Base] The scene's props
100
+ # @yieldparam [Hash] Additional context
101
+ def on_start(&block)
102
+ start_blocks.push block
103
+ end
104
+
105
+ # @yieldparam [Actor] The scene's actor
106
+ # @yieldparam [Props::Base] The scene's props
107
+ # @yieldparam [Hash] Additional context
108
+ def on_finish(&block)
109
+ finish_blocks.push block
110
+ end
111
+
112
+ protected
113
+
114
+ attr_writer :context
115
+
116
+ # @param klass [Class<Props::Base>]
117
+ def use_props_class(klass)
118
+ @props_class = klass
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -4,7 +4,10 @@ module Gamefic
4
4
  module Scene
5
5
  # A scene that ends an actor's participation in a narrative.
6
6
  #
7
- class Conclusion < Default
7
+ class Conclusion < Base
8
+ def self.type
9
+ 'Conclusion'
10
+ end
8
11
  end
9
12
  end
10
13
  end