gamefic 2.4.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +41 -40
  3. data/.rspec-opal +2 -0
  4. data/.solargraph.yml +20 -3
  5. data/CHANGELOG.md +18 -0
  6. data/Rakefile +11 -1
  7. data/bin/console +14 -0
  8. data/bin/setup +8 -0
  9. data/gamefic.gemspec +5 -2
  10. data/lib/gamefic/action.rb +58 -181
  11. data/lib/gamefic/active/cue.rb +25 -0
  12. data/lib/gamefic/active/epic.rb +73 -0
  13. data/lib/gamefic/active/messaging.rb +43 -0
  14. data/lib/gamefic/active/take.rb +69 -0
  15. data/lib/gamefic/active.rb +111 -191
  16. data/lib/gamefic/actor.rb +2 -0
  17. data/lib/gamefic/block.rb +28 -0
  18. data/lib/gamefic/command.rb +6 -7
  19. data/lib/gamefic/composer.rb +68 -0
  20. data/lib/gamefic/core_ext/array.rb +4 -4
  21. data/lib/gamefic/core_ext/string.rb +10 -5
  22. data/lib/gamefic/describable.rb +39 -65
  23. data/lib/gamefic/dispatcher.rb +79 -41
  24. data/lib/gamefic/entity.rb +42 -19
  25. data/lib/gamefic/expression.rb +31 -0
  26. data/lib/gamefic/logging.rb +32 -0
  27. data/lib/gamefic/messenger.rb +66 -0
  28. data/lib/gamefic/narrative.rb +104 -0
  29. data/lib/gamefic/node.rb +44 -53
  30. data/lib/gamefic/plot.rb +60 -93
  31. data/lib/gamefic/props/default.rb +49 -0
  32. data/lib/gamefic/props/multiple_choice.rb +65 -0
  33. data/lib/gamefic/props/output.rb +82 -0
  34. data/lib/gamefic/props/pause.rb +11 -0
  35. data/lib/gamefic/props/yes_or_no.rb +21 -0
  36. data/lib/gamefic/props.rb +11 -0
  37. data/lib/gamefic/query/base.rb +64 -121
  38. data/lib/gamefic/query/general.rb +50 -0
  39. data/lib/gamefic/query/result.rb +20 -0
  40. data/lib/gamefic/query/scoped.rb +46 -0
  41. data/lib/gamefic/query/text.rb +43 -33
  42. data/lib/gamefic/query.rb +7 -15
  43. data/lib/gamefic/response.rb +112 -0
  44. data/lib/gamefic/rulebook/calls.rb +90 -0
  45. data/lib/gamefic/rulebook/events.rb +79 -0
  46. data/lib/gamefic/rulebook/hooks.rb +57 -0
  47. data/lib/gamefic/rulebook/scenes.rb +68 -0
  48. data/lib/gamefic/rulebook.rb +139 -0
  49. data/lib/gamefic/scanner.rb +130 -0
  50. data/lib/gamefic/scene/activity.rb +9 -17
  51. data/lib/gamefic/scene/conclusion.rb +6 -5
  52. data/lib/gamefic/scene/default.rb +88 -0
  53. data/lib/gamefic/scene/multiple_choice.rb +14 -69
  54. data/lib/gamefic/scene/pause.rb +9 -13
  55. data/lib/gamefic/scene/yes_or_no.rb +6 -46
  56. data/lib/gamefic/scene.rb +11 -7
  57. data/lib/gamefic/scope/base.rb +44 -0
  58. data/lib/gamefic/scope/children.rb +16 -0
  59. data/lib/gamefic/scope/family.rb +20 -0
  60. data/lib/gamefic/scope/myself.rb +13 -0
  61. data/lib/gamefic/scope/parent.rb +13 -0
  62. data/lib/gamefic/scope/siblings.rb +14 -0
  63. data/lib/gamefic/scope.rb +8 -0
  64. data/lib/gamefic/scriptable/actions.rb +156 -0
  65. data/lib/gamefic/scriptable/entities.rb +79 -0
  66. data/lib/gamefic/scriptable/events.rb +65 -0
  67. data/lib/gamefic/scriptable/proxy.rb +66 -0
  68. data/lib/gamefic/scriptable/queries.rb +73 -0
  69. data/lib/gamefic/scriptable/scenes.rb +162 -0
  70. data/lib/gamefic/scriptable.rb +167 -73
  71. data/lib/gamefic/snapshot.rb +44 -0
  72. data/lib/gamefic/stage.rb +51 -0
  73. data/lib/gamefic/subplot.rb +51 -79
  74. data/lib/gamefic/syntax/template.rb +67 -0
  75. data/lib/gamefic/syntax.rb +102 -83
  76. data/lib/gamefic/vault.rb +50 -0
  77. data/lib/gamefic/version.rb +1 -1
  78. data/lib/gamefic.rb +28 -15
  79. data/spec-opal/spec_helper.rb +24 -0
  80. metadata +94 -29
  81. data/lib/gamefic/element.rb +0 -46
  82. data/lib/gamefic/keywords.rb +0 -52
  83. data/lib/gamefic/messaging.rb +0 -43
  84. data/lib/gamefic/plot/darkroom.rb +0 -120
  85. data/lib/gamefic/plot/host.rb +0 -42
  86. data/lib/gamefic/plot/snapshot.rb +0 -27
  87. data/lib/gamefic/query/children.rb +0 -9
  88. data/lib/gamefic/query/descendants.rb +0 -15
  89. data/lib/gamefic/query/external.rb +0 -39
  90. data/lib/gamefic/query/family.rb +0 -18
  91. data/lib/gamefic/query/itself.rb +0 -13
  92. data/lib/gamefic/query/matches.rb +0 -75
  93. data/lib/gamefic/query/parent.rb +0 -9
  94. data/lib/gamefic/query/siblings.rb +0 -13
  95. data/lib/gamefic/query/tree.rb +0 -17
  96. data/lib/gamefic/scene/base.rb +0 -142
  97. data/lib/gamefic/scene/multiple_scene.rb +0 -29
  98. data/lib/gamefic/serialize.rb +0 -196
  99. data/lib/gamefic/world/callbacks.rb +0 -135
  100. data/lib/gamefic/world/commands.rb +0 -181
  101. data/lib/gamefic/world/entities.rb +0 -98
  102. data/lib/gamefic/world/playbook.rb +0 -233
  103. data/lib/gamefic/world/players.rb +0 -37
  104. data/lib/gamefic/world/scenes.rb +0 -228
  105. data/lib/gamefic/world.rb +0 -18
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ class Rulebook
5
+ # Blocks of code to be executed for various narrative events, such as
6
+ # on_ready and on_update.
7
+ #
8
+ class Events
9
+ attr_reader :player_output_blocks
10
+
11
+ attr_reader :player_conclude_blocks
12
+
13
+ attr_reader :ready_blocks
14
+
15
+ attr_reader :update_blocks
16
+
17
+ attr_reader :conclude_blocks
18
+
19
+ def initialize
20
+ @ready_blocks = []
21
+ @update_blocks = []
22
+ @conclude_blocks = []
23
+ @player_conclude_blocks = []
24
+ @player_output_blocks = []
25
+ end
26
+
27
+ def empty?
28
+ [player_output_blocks, player_conclude_blocks, ready_blocks, update_blocks, conclude_blocks].all?(&:empty?)
29
+ end
30
+
31
+ def freeze
32
+ super
33
+ instance_variables.each { |k| instance_variable_get(k).freeze }
34
+ self
35
+ end
36
+
37
+ # @return [void]
38
+ def on_ready &block
39
+ @ready_blocks.push block
40
+ end
41
+
42
+ # @yieldparam [Actor]
43
+ # @return [void]
44
+ def on_player_ready &block
45
+ @ready_blocks.push(proc do
46
+ players.each { |plyr| instance_exec plyr, &block }
47
+ end)
48
+ end
49
+
50
+ def on_update &block
51
+ @update_blocks.push block
52
+ end
53
+
54
+ def on_player_update &block
55
+ @update_blocks.push(proc do
56
+ players.each { |plyr| instance_exec plyr, &block }
57
+ end)
58
+ end
59
+
60
+ # @return [void]
61
+ def on_conclude &block
62
+ @conclude_blocks.push block
63
+ end
64
+
65
+ # @yieldparam [Actor]
66
+ # @return [void]
67
+ def on_player_conclude &block
68
+ @player_conclude_blocks.push block
69
+ end
70
+
71
+ # @yieldparam [Actor]
72
+ # @yieldparam [Hash]
73
+ # @return [void]
74
+ def on_player_output &block
75
+ @player_output_blocks.push block
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ class Rulebook
5
+ # A collection of hooks that can be executed before and after an action.
6
+ #
7
+ class Hooks
8
+ attr_reader :before_actions
9
+
10
+ attr_reader :after_actions
11
+
12
+ def initialize
13
+ @before_actions = []
14
+ @after_actions = []
15
+ end
16
+
17
+ def freeze
18
+ super
19
+ @before_actions.freeze
20
+ @after_actions.freeze
21
+ self
22
+ end
23
+
24
+ def before_action *verbs, &block
25
+ before_actions.push Action::Hook.new(*verbs, &block)
26
+ end
27
+
28
+ def after_action *verbs, &block
29
+ after_actions.push Action::Hook.new(*verbs, &block)
30
+ end
31
+
32
+ def empty?
33
+ before_actions.empty? && after_actions.empty?
34
+ end
35
+
36
+ def run_before action, narrative
37
+ run_action_hooks action, narrative, before_actions
38
+ end
39
+
40
+ def run_after action, narrative
41
+ run_action_hooks action, narrative, after_actions
42
+ end
43
+
44
+ private
45
+
46
+ def run_action_hooks action, narrative, hooks
47
+ hooks.each do |hook|
48
+ break if action.cancelled?
49
+
50
+ next unless hook.match?(action.verb)
51
+
52
+ Stage.run(narrative) { instance_exec(action, &hook.block) }
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ class Rulebook
5
+ # The scene manager for rulebooks.
6
+ #
7
+ class Scenes
8
+ attr_reader :introductions
9
+
10
+ def initialize
11
+ @scene_map = {}
12
+ @introductions = []
13
+ end
14
+
15
+ def freeze
16
+ super
17
+ @scene_map.freeze
18
+ @introductions.freeze
19
+ self
20
+ end
21
+
22
+ # Add a scene to the scenebook.
23
+ #
24
+ # @param [Scene]
25
+ def add scene
26
+ raise ArgumentError, "A scene named `#{scene.name} already exists" if @scene_map.key?(scene.name)
27
+
28
+ @scene_map[scene.name] = scene
29
+ end
30
+
31
+ def scene? name
32
+ @scene_map.key? name
33
+ end
34
+
35
+ # @return [Scene, nil]
36
+ def [](name)
37
+ @scene_map[name]
38
+ end
39
+
40
+ # @return [Array<Symbol>]
41
+ def names
42
+ @scene_map.keys
43
+ end
44
+
45
+ # @return [Array<Scene>]
46
+ def all
47
+ @scene_map.values
48
+ end
49
+
50
+ def introduction scene
51
+ introductions.push scene
52
+ end
53
+
54
+ def with_defaults narrative
55
+ maybe_add :default_scene, Scene::Activity, narrative
56
+ maybe_add :default_conclusion, Scene::Conclusion, narrative
57
+ end
58
+
59
+ def maybe_add name, klass, narrative
60
+ add klass.new(name, narrative) unless names.include?(name)
61
+ end
62
+
63
+ def empty?
64
+ @scene_map.empty? && introductions.empty?
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gamefic/rulebook/calls'
4
+ require 'gamefic/rulebook/events'
5
+ require 'gamefic/rulebook/hooks'
6
+ require 'gamefic/rulebook/scenes'
7
+
8
+ module Gamefic
9
+ # A collection of rules that define the behavior of a narrative.
10
+ #
11
+ # Rulebooks provide a way to separate narrative data from code. This
12
+ # separation is necessary to ensure that the game state can be serialized in
13
+ # snapshots.
14
+ #
15
+ class Rulebook
16
+ # @return [Calls]
17
+ attr_reader :calls
18
+
19
+ # @return [Events]
20
+ attr_reader :events
21
+
22
+ # @return [Hooks]
23
+ attr_reader :hooks
24
+
25
+ # @return [Scenes]
26
+ attr_reader :scenes
27
+
28
+ # @return [Narrative]
29
+ attr_reader :narrative
30
+
31
+ # @param narrative [Narrative]
32
+ def initialize(narrative)
33
+ @narrative = narrative
34
+ @calls = Calls.new
35
+ @events = Events.new
36
+ @hooks = Hooks.new
37
+ @scenes = Scenes.new
38
+ end
39
+
40
+ def freeze
41
+ super
42
+ [@calls, @events, @hooks, @scenes].each(&:freeze)
43
+ self
44
+ end
45
+
46
+ # @return [Array<Response>]
47
+ def responses
48
+ @calls.responses
49
+ end
50
+
51
+ # @return [Array<Syntax>]
52
+ def syntaxes
53
+ @calls.syntaxes
54
+ end
55
+
56
+ # An array of all the verbs available in the rulebook. This list only
57
+ # includes verbs that are explicitly defined in reponses. It excludes
58
+ # synonyms that might be defined in syntaxes (see #synonyms).
59
+ #
60
+ # @example
61
+ # rulebook.respond :verb { |_| nil }
62
+ # rulebook.interpret 'synonym', 'verb'
63
+ # rulebook.verbs #=> [:verb]
64
+ #
65
+ # @return [Array<Symbol>]
66
+ def verbs
67
+ @calls.verbs
68
+ end
69
+
70
+ # An array of all the verbs defined in responses and any synonyms defined
71
+ # in syntaxes.
72
+ #
73
+ # @example
74
+ # rulebook.respond :verb { |_| nil }
75
+ # rulebook.interpret 'synonym', 'verb'
76
+ # rulebook.synonyms #=> [:synonym, :verb]
77
+ #
78
+ def synonyms
79
+ @calls.synonyms
80
+ end
81
+
82
+ # Get an array of all the responses that match a list of verbs.
83
+ #
84
+ # @param verbs [Array<Symbol>]
85
+ # @return [Array<Response>]
86
+ def responses_for *verbs
87
+ @calls.responses_for *verbs
88
+ end
89
+
90
+ # Get an array of all the syntaxes that match a lit of verbs.
91
+ #
92
+ # @param words [Array<Symbol>]
93
+ # @return [Array<Syntax>]
94
+ def syntaxes_for *synonyms
95
+ @calls.syntaxes_for *synonyms
96
+ end
97
+
98
+ def run_ready_blocks
99
+ events.ready_blocks.each { |blk| Stage.run narrative, &blk }
100
+ end
101
+
102
+ def run_update_blocks
103
+ events.update_blocks.each { |blk| Stage.run narrative, &blk }
104
+ end
105
+
106
+ def run_before_actions action
107
+ hooks.run_before action, narrative
108
+ end
109
+
110
+ def run_after_actions action
111
+ hooks.run_after action, narrative
112
+ end
113
+
114
+ def run_conclude_blocks
115
+ events.conclude_blocks.each { |blk| Stage.run narrative, &blk }
116
+ end
117
+
118
+ def run_player_conclude_blocks player
119
+ events.player_conclude_blocks.each { |blk| Stage.run(narrative, player, &blk) }
120
+ end
121
+
122
+ def run_player_output_blocks player, output
123
+ events.player_output_blocks.each { |blk| Stage.run(narrative, player, output, &blk) }
124
+ end
125
+
126
+ def empty?
127
+ calls.empty? && hooks.empty? && scenes.empty? && events.empty?
128
+ end
129
+
130
+ def script
131
+ narrative.class.included_blocks.select(&:script?).each { |blk| Stage.run(narrative, &blk.code) }
132
+ end
133
+
134
+ def script_with_defaults
135
+ script
136
+ scenes.with_defaults narrative
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,130 @@
1
+ module Gamefic
2
+ # A module for matching objects to tokens.
3
+ #
4
+ module Scanner
5
+ NEST_REGEXP = / in | on | of | from | inside | from inside /
6
+
7
+ # The result of an attempt to scan objects against a token in a Scanner. It
8
+ # provides an array of matching objects, the text that matched them, and the
9
+ # text that remains unmatched.
10
+ #
11
+ class Result
12
+ # The scanned objects
13
+ #
14
+ # @return [Array<Entity>, String, Regexp]
15
+ attr_reader :scanned
16
+
17
+ # The scanned token
18
+ #
19
+ # @return [String]
20
+ attr_reader :token
21
+
22
+ # The matched objects
23
+ #
24
+ # @return [Array<Entity>, String]
25
+ attr_reader :matched
26
+
27
+ # The remaining (unmatched) portion of the token
28
+ #
29
+ # @return [String]
30
+ attr_reader :remainder
31
+
32
+ def initialize scanned, token, matched, remainder
33
+ @scanned = scanned
34
+ @token = token
35
+ @matched = matched
36
+ @remainder = remainder
37
+ end
38
+ end
39
+
40
+ # Scan entities against a token.
41
+ #
42
+ # @param selection [Array<Entity>, String, Regexp]
43
+ # @param token [String]
44
+ # @return [Result]
45
+ def self.scan selection, token
46
+ strict_result = strict(selection, token)
47
+ strict_result.matched.empty? ? fuzzy(selection, token) : strict_result
48
+ end
49
+
50
+ # @param selection [Array<Entity>, String, Regexp]
51
+ # @param token [String]
52
+ # @return [Result]
53
+ def self.strict selection, token
54
+ return Result.new(selection, token, '', token) unless selection.is_a?(Array)
55
+
56
+ scan_strict_or_fuzzy(selection, token, :select_strict)
57
+ end
58
+
59
+ # @param selection [Array<Entity>, String, Regexp]
60
+ # @param token [String]
61
+ # @return [Result]
62
+ def self.fuzzy selection, token
63
+ return scan_text(selection, token) unless selection.is_a?(Array)
64
+
65
+ scan_strict_or_fuzzy(selection, token, :select_fuzzy)
66
+ end
67
+
68
+ class << self
69
+ private
70
+
71
+ def scan_strict_or_fuzzy objects, token, method
72
+ if nested?(token) && objects.all?(&:children)
73
+ denest(objects, token)
74
+ else
75
+ words = token.keywords
76
+ available = objects.clone
77
+ filtered = []
78
+ words.each_with_index do |word, idx|
79
+ tested = send(method, available, word)
80
+ return Result.new(objects, token, filtered, words[idx..].join(' ')) if tested.empty?
81
+
82
+ filtered = tested
83
+ available = filtered
84
+ end
85
+ Result.new(objects, token, filtered, '')
86
+ end
87
+ end
88
+
89
+ def select_strict available, word
90
+ available.select { |obj| obj.keywords.include?(word) }
91
+ end
92
+
93
+ def select_fuzzy available, word
94
+ available.select { |obj| obj.keywords.any? { |wrd| wrd.start_with?(word) } }
95
+ end
96
+
97
+ def nested?(token)
98
+ token.match(NEST_REGEXP)
99
+ end
100
+
101
+ def scan_text selection, token
102
+ case selection
103
+ when Regexp
104
+ return Result.new(selection, token, token, '') if token =~ selection
105
+ else
106
+ return Result.new(selection, token, selection, token[selection.length..]) if token.start_with?(selection)
107
+ end
108
+ Result.new(selection, token, '', token)
109
+ end
110
+
111
+ def denest(objects, token)
112
+ parts = token.split(NEST_REGEXP)
113
+ current = parts.pop
114
+ last_result = scan(objects, current)
115
+ until parts.empty?
116
+ current = "#{parts.last} #{current}"
117
+ result = scan(last_result.matched, current)
118
+ break if result.matched.empty?
119
+
120
+ parts.pop
121
+ last_result = result
122
+ end
123
+ return Result.new(objects, token, [], '') if last_result.matched.empty? || last_result.matched.length > 1
124
+ return last_result if parts.empty?
125
+
126
+ denest(last_result.matched.first.children, parts.join(' '))
127
+ end
128
+ end
129
+ end
130
+ end
@@ -1,21 +1,13 @@
1
- module Gamefic
2
- # Active Scenes handle the default command prompt, where input is parsed
3
- # into an Action performed by the Character. This is the default scene in
4
- # a Plot.
5
- #
6
- class Scene::Activity < Scene::Base
7
- def post_initialize
8
- self.type = 'Activity'
9
- end
1
+ # frozen_string_literal: true
10
2
 
11
- def finish
12
- super
13
- actor.perform input.strip unless input.to_s.strip.empty?
14
- end
15
-
16
- class << self
17
- def type
18
- 'Activity'
3
+ module Gamefic
4
+ module Scene
5
+ # A scene that accepts player commands for actors to perform.
6
+ #
7
+ class Activity < Default
8
+ def finish actor, props
9
+ super
10
+ actor.perform props.input
19
11
  end
20
12
  end
21
13
  end
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gamefic
2
- # A Conclusion ends the Plot (or the character's participation in it).
3
- #
4
- class Scene::Conclusion < Scene::Base
5
- def type
6
- @type ||= 'Conclusion'
4
+ module Scene
5
+ # A scene that ends an actor's participation in a narrative.
6
+ #
7
+ class Conclusion < Default
7
8
  end
8
9
  end
9
10
  end
@@ -0,0 +1,88 @@
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 Default
9
+ # @return [Symbol]
10
+ attr_reader :name
11
+
12
+ # @param name [Symbol]
13
+ # @param narrative [Narrative]
14
+ # @param on_start [Proc, nil]
15
+ # @param on_finish [Proc, nil]
16
+ # @yieldparam [self]
17
+ def initialize name, narrative, on_start: nil, on_finish: nil
18
+ @name = name
19
+ @narrative = narrative
20
+ @start_blocks = []
21
+ @finish_blocks = []
22
+ @start_blocks.push on_start if on_start
23
+ @finish_blocks.push on_finish if on_finish
24
+ yield(self) if block_given?
25
+ end
26
+
27
+ # @return [String]
28
+ def type
29
+ @type ||= self.class.to_s.sub(/^Gamefic::Scene::/, '')
30
+ end
31
+
32
+ def new_props(**context)
33
+ self.class.props_class.new(self, **context)
34
+ end
35
+
36
+ def on_start &block
37
+ @start_blocks.push block
38
+ end
39
+
40
+ def on_finish &block
41
+ @finish_blocks.push block
42
+ end
43
+
44
+ # @param actor [Gamefic::Actor]
45
+ # @param props [Props::Default]
46
+ # @return [void]
47
+ def start actor, props
48
+ props.output[:scene] = to_hash
49
+ props.output[:prompt] = props.prompt
50
+ end
51
+
52
+ # @param actor [Gamefic::Actor]
53
+ # @param props [Props::Default]
54
+ # @return [void]
55
+ def finish actor, props
56
+ props.input = actor.queue.shift
57
+ end
58
+
59
+ def run_start_blocks actor, props
60
+ @start_blocks.each { |blk| Stage.run(@narrative, actor, props, &blk) }
61
+ end
62
+
63
+ def run_finish_blocks actor, props
64
+ @finish_blocks.each { |blk| Stage.run(@narrative, actor, props, &blk) }
65
+ end
66
+
67
+ def self.props_class
68
+ @props_class ||= Props::Default
69
+ end
70
+
71
+ def conclusion?
72
+ is_a?(Conclusion)
73
+ end
74
+
75
+ def to_hash
76
+ { name: name, type: type }
77
+ end
78
+
79
+ class << self
80
+ protected
81
+
82
+ def use_props_class klass
83
+ @props_class = klass
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -1,79 +1,24 @@
1
- module Gamefic
2
- # Provide a list of options and process the selection in the scene's finish
3
- # block. After the scene is finished, the :active scene will be cued unless
4
- # some other scene has already been prepared or cued.
5
- #
6
- # The finish block's input parameter receives a MultipleChoice::Input object
7
- # instead of a String.
8
- #
9
- class Scene::MultipleChoice < Scene::Base
10
- # The zero-based index of the selected option.
11
- #
12
- # @return [Integer]
13
- attr_reader :index
1
+ # frozen_string_literal: true
14
2
 
15
- # The one-based index of the selected option.
16
- #
17
- # @return [Integer]
18
- attr_reader :number
19
-
20
- # The full text of the selected option.
3
+ module Gamefic
4
+ module Scene
5
+ # A scene that presents a list of choices and processes the player's input.
6
+ # If the input is not a valid choice, the scene gets recued.
21
7
  #
22
- # @return [String]
23
- attr_reader :selection
8
+ class MultipleChoice < Default
9
+ use_props_class Props::MultipleChoice
24
10
 
25
- attr_writer :invalid_message
26
-
27
- def post_initialize
28
- self.type = 'MultipleChoice'
29
- self.prompt = 'Enter a choice:'
30
- end
31
-
32
- def finish
33
- get_choice
34
- if selection.nil?
35
- actor.tell invalid_message
36
- else
11
+ def start actor, props
37
12
  super
13
+ props.output[:options] = props.options
38
14
  end
39
- end
40
15
 
41
- # The array of available options.
42
- #
43
- # @return [Array<String>]
44
- def options
45
- @options ||= []
46
- end
47
-
48
- # The text to display when an invalid selection is received.
49
- #
50
- # @return [String]
51
- def invalid_message
52
- @invalid_message ||= 'That is not a valid choice.'
53
- end
54
-
55
- def state
56
- super.merge options: options
57
- end
58
-
59
- private
16
+ def finish actor, props
17
+ super
18
+ return if props.index
60
19
 
61
- def get_choice
62
- if input.strip =~ /^[0-9]+$/ and input.to_i > 0
63
- @number = input.to_i
64
- @index = number - 1
65
- @selection = options[index]
66
- else
67
- i = 0
68
- options.each { |o|
69
- if o.casecmp(input).zero?
70
- @selection = o
71
- @index = i
72
- @number = index + 1
73
- break
74
- end
75
- i += 1
76
- }
20
+ actor.tell format(props.invalid_message, input: props.input)
21
+ actor.recue
77
22
  end
78
23
  end
79
24
  end