gamefic 2.4.0 → 3.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 (102) 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 +9 -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 +52 -183
  11. data/lib/gamefic/active/cue.rb +25 -0
  12. data/lib/gamefic/active/epic.rb +68 -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 +95 -192
  16. data/lib/gamefic/actor.rb +2 -0
  17. data/lib/gamefic/block.rb +28 -0
  18. data/lib/gamefic/command.rb +16 -6
  19. data/lib/gamefic/core_ext/array.rb +4 -4
  20. data/lib/gamefic/core_ext/string.rb +10 -5
  21. data/lib/gamefic/describable.rb +39 -65
  22. data/lib/gamefic/dispatcher.rb +63 -32
  23. data/lib/gamefic/entity.rb +44 -19
  24. data/lib/gamefic/logging.rb +32 -0
  25. data/lib/gamefic/messenger.rb +66 -0
  26. data/lib/gamefic/narrative.rb +104 -0
  27. data/lib/gamefic/node.rb +44 -53
  28. data/lib/gamefic/plot.rb +60 -93
  29. data/lib/gamefic/props/default.rb +41 -0
  30. data/lib/gamefic/props/multiple_choice.rb +65 -0
  31. data/lib/gamefic/props/pause.rb +11 -0
  32. data/lib/gamefic/props/yes_or_no.rb +21 -0
  33. data/lib/gamefic/props.rb +10 -0
  34. data/lib/gamefic/query/base.rb +45 -126
  35. data/lib/gamefic/query/general.rb +46 -0
  36. data/lib/gamefic/query/result.rb +20 -0
  37. data/lib/gamefic/query/scoped.rb +41 -0
  38. data/lib/gamefic/query/text.rb +30 -31
  39. data/lib/gamefic/query.rb +7 -15
  40. data/lib/gamefic/response.rb +118 -0
  41. data/lib/gamefic/rulebook/calls.rb +90 -0
  42. data/lib/gamefic/rulebook/events.rb +79 -0
  43. data/lib/gamefic/rulebook/hooks.rb +57 -0
  44. data/lib/gamefic/rulebook/scenes.rb +68 -0
  45. data/lib/gamefic/rulebook.rb +139 -0
  46. data/lib/gamefic/scanner.rb +103 -0
  47. data/lib/gamefic/scene/activity.rb +9 -17
  48. data/lib/gamefic/scene/conclusion.rb +6 -5
  49. data/lib/gamefic/scene/default.rb +88 -0
  50. data/lib/gamefic/scene/multiple_choice.rb +14 -69
  51. data/lib/gamefic/scene/pause.rb +9 -13
  52. data/lib/gamefic/scene/yes_or_no.rb +6 -46
  53. data/lib/gamefic/scene.rb +11 -7
  54. data/lib/gamefic/scope/base.rb +44 -0
  55. data/lib/gamefic/scope/children.rb +16 -0
  56. data/lib/gamefic/scope/family.rb +20 -0
  57. data/lib/gamefic/scope/myself.rb +13 -0
  58. data/lib/gamefic/scope/parent.rb +13 -0
  59. data/lib/gamefic/scope/siblings.rb +14 -0
  60. data/lib/gamefic/scope.rb +8 -0
  61. data/lib/gamefic/scriptable/actions.rb +156 -0
  62. data/lib/gamefic/scriptable/entities.rb +76 -0
  63. data/lib/gamefic/scriptable/events.rb +65 -0
  64. data/lib/gamefic/scriptable/proxy.rb +55 -0
  65. data/lib/gamefic/scriptable/queries.rb +73 -0
  66. data/lib/gamefic/scriptable/scenes.rb +162 -0
  67. data/lib/gamefic/scriptable.rb +167 -73
  68. data/lib/gamefic/snapshot.rb +36 -0
  69. data/lib/gamefic/stage.rb +51 -0
  70. data/lib/gamefic/subplot.rb +51 -79
  71. data/lib/gamefic/syntax/template.rb +67 -0
  72. data/lib/gamefic/syntax.rb +102 -83
  73. data/lib/gamefic/vault.rb +50 -0
  74. data/lib/gamefic/version.rb +1 -1
  75. data/lib/gamefic.rb +26 -15
  76. data/spec-opal/spec_helper.rb +24 -0
  77. metadata +91 -29
  78. data/lib/gamefic/element.rb +0 -46
  79. data/lib/gamefic/keywords.rb +0 -52
  80. data/lib/gamefic/messaging.rb +0 -43
  81. data/lib/gamefic/plot/darkroom.rb +0 -120
  82. data/lib/gamefic/plot/host.rb +0 -42
  83. data/lib/gamefic/plot/snapshot.rb +0 -27
  84. data/lib/gamefic/query/children.rb +0 -9
  85. data/lib/gamefic/query/descendants.rb +0 -15
  86. data/lib/gamefic/query/external.rb +0 -39
  87. data/lib/gamefic/query/family.rb +0 -18
  88. data/lib/gamefic/query/itself.rb +0 -13
  89. data/lib/gamefic/query/matches.rb +0 -75
  90. data/lib/gamefic/query/parent.rb +0 -9
  91. data/lib/gamefic/query/siblings.rb +0 -13
  92. data/lib/gamefic/query/tree.rb +0 -17
  93. data/lib/gamefic/scene/base.rb +0 -142
  94. data/lib/gamefic/scene/multiple_scene.rb +0 -29
  95. data/lib/gamefic/serialize.rb +0 -196
  96. data/lib/gamefic/world/callbacks.rb +0 -135
  97. data/lib/gamefic/world/commands.rb +0 -181
  98. data/lib/gamefic/world/entities.rb +0 -98
  99. data/lib/gamefic/world/playbook.rb +0 -233
  100. data/lib/gamefic/world/players.rb +0 -37
  101. data/lib/gamefic/world/scenes.rb +0 -228
  102. data/lib/gamefic/world.rb +0 -18
@@ -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) { blk.call(player) } }
120
+ end
121
+
122
+ def run_player_output_blocks player, output
123
+ events.player_output_blocks.each { |blk| Stage.run(narrative) { blk.call(player, output) } }
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,103 @@
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<Object>]
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<Object>]
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 objects [Array<Gamefic::Entity>]
43
+ # @param token [String]
44
+ # @return [Result]
45
+ def self.scan objects, token
46
+ # @note Theoretically, scanned objects only have to implement two
47
+ # methods:
48
+ # * #keywords => [Array<String>]
49
+ # * #children => [Array<#keywords, #children>]
50
+
51
+ words = token.keywords
52
+ available = objects.clone
53
+ filtered = []
54
+ if nested?(token) && objects.all?(&:children)
55
+ denest(objects, token)
56
+ else
57
+ words.each_with_index do |word, idx|
58
+ tested = select_strict(available, word)
59
+ tested = select_fuzzy(available, word) if tested.empty?
60
+ return Result.new(objects, token, filtered, words[idx..].join(' ')) if tested.empty?
61
+
62
+ filtered = tested
63
+ available = filtered
64
+ end
65
+ Result.new(objects, token, filtered, '')
66
+ end
67
+ end
68
+
69
+ class << self
70
+ private
71
+
72
+ def select_strict available, word
73
+ available.select { |obj| obj.keywords.include?(word) }
74
+ end
75
+
76
+ def select_fuzzy available, word
77
+ available.select { |obj| obj.keywords.any? { |wrd| wrd.start_with?(word) } }
78
+ end
79
+
80
+ def nested?(token)
81
+ token.match(NEST_REGEXP)
82
+ end
83
+
84
+ def denest(objects, token)
85
+ parts = token.split(NEST_REGEXP)
86
+ current = parts.pop
87
+ last_result = scan(objects, current)
88
+ until parts.empty?
89
+ current = "#{parts.last} #{current}"
90
+ result = scan(last_result.matched, current)
91
+ break if result.matched.empty?
92
+
93
+ parts.pop
94
+ last_result = result
95
+ end
96
+ return Result.new(objects, token, [], '') if last_result.matched.empty? || last_result.matched.length > 1
97
+ return last_result if parts.empty?
98
+
99
+ denest(last_result.matched.first.children, parts.join(' '))
100
+ end
101
+ end
102
+ end
103
+ 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(name, type, **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
+ actor.output[:scene] = to_hash
49
+ actor.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
+ actor.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
@@ -1,17 +1,13 @@
1
- module Gamefic
2
- # Pause for user input.
3
- #
4
- class Scene::Pause < Scene::Base
5
- def post_initialize
6
- self.type = 'Pause'
7
- self.prompt = 'Press enter to continue...'
8
- end
1
+ # frozen_string_literal: true
9
2
 
10
- class << self
11
- def tracked?
12
- @tracked = true if @tracked.nil?
13
- @tracked
14
- end
3
+ module Gamefic
4
+ module Scene
5
+ # Pause a scene. This rig simply runs on_start and waits for user input
6
+ # before proceeding to on_finish. The user input itself is ignored by
7
+ # default.
8
+ #
9
+ class Pause < Default
10
+ use_props_class Props::Pause
15
11
  end
16
12
  end
17
13
  end
@@ -1,51 +1,11 @@
1
- module Gamefic
2
- # Prompt the user to answer "yes" or "no". The scene will accept variations
3
- # like "YES" or "n" and normalize the answer to "yes" or "no" in the finish
4
- # block. After the scene is finished, the :active scene will be cued if no
5
- # other scene has been prepared or cued.
6
- #
7
- class Scene::YesOrNo < Scene::Base
8
- attr_writer :invalid_message
9
-
10
- def post_initialize
11
- self.type = 'YesOrNo'
12
- self.prompt = 'Yes or No:'
13
- end
14
-
15
- # True if the actor's answer is Yes.
16
- # Any answer beginning with letter Y is considered Yes.
17
- #
18
- # @return [Boolean]
19
- def yes?
20
- input.to_s[0,1].downcase == 'y' or input.to_i == 1
21
- end
22
-
23
- # True if the actor's answer is No.
24
- # Any answer beginning with letter N is considered No.
25
- #
26
- # @return [Boolean]
27
- def no?
28
- input.to_s[0,1].downcase == 'n' or input.to_i == 2
29
- end
1
+ # frozen_string_literal: true
30
2
 
31
- # The message sent to the user for an invalid answer, i.e., the input
32
- # could not be resolved to either Yes or No.
3
+ module Gamefic
4
+ module Scene
5
+ # A specialized MultipleChoice scene that only accepts Yes or No.
33
6
  #
34
- # @return [String]
35
- def invalid_message
36
- @invalid_message ||= 'Please enter Yes or No.'
37
- end
38
-
39
- def finish
40
- if yes? or no?
41
- super
42
- else
43
- actor.tell invalid_message
44
- end
45
- end
46
-
47
- def state
48
- super.merge options: ['Yes', 'No']
7
+ class YesOrNo < MultipleChoice
8
+ use_props_class Props::YesOrNo
49
9
  end
50
10
  end
51
11
  end
data/lib/gamefic/scene.rb CHANGED
@@ -1,11 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gamefic
4
+ # Narratives use scenes to process game turns. The start of a scene defines
5
+ # the output to be sent to the player. The finish processes player input.
6
+ #
2
7
  module Scene
3
- autoload :Base, 'gamefic/scene/base'
4
- autoload :Activity, 'gamefic/scene/activity'
5
- autoload :Pause, 'gamefic/scene/pause'
6
- autoload :Conclusion, 'gamefic/scene/conclusion'
7
- autoload :MultipleChoice, 'gamefic/scene/multiple_choice'
8
- autoload :MultipleScene, 'gamefic/scene/multiple_scene'
9
- autoload :YesOrNo, 'gamefic/scene/yes_or_no'
8
+ require 'gamefic/scene/default'
9
+ require 'gamefic/scene/activity'
10
+ require 'gamefic/scene/multiple_choice'
11
+ require 'gamefic/scene/pause'
12
+ require 'gamefic/scene/yes_or_no'
13
+ require 'gamefic/scene/conclusion'
10
14
  end
11
15
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Scope
5
+ # The base class for a Scoped query's scope.
6
+ #
7
+ class Base
8
+ attr_reader :context
9
+
10
+ # @param [Gamefic::Entity]
11
+ def initialize context
12
+ @context = context
13
+ end
14
+
15
+ # @param [Array<Gamefic::Entity>]
16
+ def matches
17
+ []
18
+ end
19
+
20
+ # @param [Gamefic::Entity]
21
+ def self.matches context
22
+ new(context).matches
23
+ end
24
+
25
+ def self.precision
26
+ 0
27
+ end
28
+
29
+ private
30
+
31
+ # Return an array of the entity's accessible descendants.
32
+ #
33
+ # @param [Entity]
34
+ # @return [Array<Entity>]
35
+ def subquery_accessible entity
36
+ return [] unless entity&.accessible?
37
+
38
+ entity.children.flat_map do |c|
39
+ [c] + subquery_accessible(c)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Scope
5
+ # The Children scope returns an entity's children and all accessible
6
+ # descendants.
7
+ #
8
+ class Children < Base
9
+ def matches
10
+ context.children.flat_map do |c|
11
+ [c] + subquery_accessible(c)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Scope
5
+ # The Family scope returns an entity's parent, siblings, and descendants.
6
+ #
7
+ class Family < Base
8
+ def matches
9
+ result = context.parent ? [context.parent] : []
10
+ result.concat subquery_accessible(context.parent)
11
+ result.delete context
12
+ context.children.each do |c|
13
+ result.push c
14
+ result.concat subquery_accessible(c)
15
+ end
16
+ result.uniq
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Scope
5
+ # The Myself scope returns the entity itself.
6
+ #
7
+ class Myself < Base
8
+ def matches
9
+ [context]
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Scope
5
+ # A query scope that can only match the entity's parent.
6
+ #
7
+ class Parent < Base
8
+ def matches
9
+ [context.parent].compact
10
+ end
11
+ end
12
+ end
13
+ end