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.
- checksums.yaml +4 -4
- data/.github/workflows/rspec.yml +41 -40
- data/.rspec-opal +2 -0
- data/.solargraph.yml +20 -3
- data/CHANGELOG.md +9 -0
- data/Rakefile +11 -1
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/gamefic.gemspec +5 -2
- data/lib/gamefic/action.rb +52 -183
- data/lib/gamefic/active/cue.rb +25 -0
- data/lib/gamefic/active/epic.rb +68 -0
- data/lib/gamefic/active/messaging.rb +43 -0
- data/lib/gamefic/active/take.rb +69 -0
- data/lib/gamefic/active.rb +95 -192
- data/lib/gamefic/actor.rb +2 -0
- data/lib/gamefic/block.rb +28 -0
- data/lib/gamefic/command.rb +16 -6
- data/lib/gamefic/core_ext/array.rb +4 -4
- data/lib/gamefic/core_ext/string.rb +10 -5
- data/lib/gamefic/describable.rb +39 -65
- data/lib/gamefic/dispatcher.rb +63 -32
- data/lib/gamefic/entity.rb +44 -19
- data/lib/gamefic/logging.rb +32 -0
- data/lib/gamefic/messenger.rb +66 -0
- data/lib/gamefic/narrative.rb +104 -0
- data/lib/gamefic/node.rb +44 -53
- data/lib/gamefic/plot.rb +60 -93
- data/lib/gamefic/props/default.rb +41 -0
- data/lib/gamefic/props/multiple_choice.rb +65 -0
- data/lib/gamefic/props/pause.rb +11 -0
- data/lib/gamefic/props/yes_or_no.rb +21 -0
- data/lib/gamefic/props.rb +10 -0
- data/lib/gamefic/query/base.rb +45 -126
- data/lib/gamefic/query/general.rb +46 -0
- data/lib/gamefic/query/result.rb +20 -0
- data/lib/gamefic/query/scoped.rb +41 -0
- data/lib/gamefic/query/text.rb +30 -31
- data/lib/gamefic/query.rb +7 -15
- data/lib/gamefic/response.rb +118 -0
- data/lib/gamefic/rulebook/calls.rb +90 -0
- data/lib/gamefic/rulebook/events.rb +79 -0
- data/lib/gamefic/rulebook/hooks.rb +57 -0
- data/lib/gamefic/rulebook/scenes.rb +68 -0
- data/lib/gamefic/rulebook.rb +139 -0
- data/lib/gamefic/scanner.rb +103 -0
- data/lib/gamefic/scene/activity.rb +9 -17
- data/lib/gamefic/scene/conclusion.rb +6 -5
- data/lib/gamefic/scene/default.rb +88 -0
- data/lib/gamefic/scene/multiple_choice.rb +14 -69
- data/lib/gamefic/scene/pause.rb +9 -13
- data/lib/gamefic/scene/yes_or_no.rb +6 -46
- data/lib/gamefic/scene.rb +11 -7
- data/lib/gamefic/scope/base.rb +44 -0
- data/lib/gamefic/scope/children.rb +16 -0
- data/lib/gamefic/scope/family.rb +20 -0
- data/lib/gamefic/scope/myself.rb +13 -0
- data/lib/gamefic/scope/parent.rb +13 -0
- data/lib/gamefic/scope/siblings.rb +14 -0
- data/lib/gamefic/scope.rb +8 -0
- data/lib/gamefic/scriptable/actions.rb +156 -0
- data/lib/gamefic/scriptable/entities.rb +76 -0
- data/lib/gamefic/scriptable/events.rb +65 -0
- data/lib/gamefic/scriptable/proxy.rb +55 -0
- data/lib/gamefic/scriptable/queries.rb +73 -0
- data/lib/gamefic/scriptable/scenes.rb +162 -0
- data/lib/gamefic/scriptable.rb +167 -73
- data/lib/gamefic/snapshot.rb +36 -0
- data/lib/gamefic/stage.rb +51 -0
- data/lib/gamefic/subplot.rb +51 -79
- data/lib/gamefic/syntax/template.rb +67 -0
- data/lib/gamefic/syntax.rb +102 -83
- data/lib/gamefic/vault.rb +50 -0
- data/lib/gamefic/version.rb +1 -1
- data/lib/gamefic.rb +26 -15
- data/spec-opal/spec_helper.rb +24 -0
- metadata +91 -29
- data/lib/gamefic/element.rb +0 -46
- data/lib/gamefic/keywords.rb +0 -52
- data/lib/gamefic/messaging.rb +0 -43
- data/lib/gamefic/plot/darkroom.rb +0 -120
- data/lib/gamefic/plot/host.rb +0 -42
- data/lib/gamefic/plot/snapshot.rb +0 -27
- data/lib/gamefic/query/children.rb +0 -9
- data/lib/gamefic/query/descendants.rb +0 -15
- data/lib/gamefic/query/external.rb +0 -39
- data/lib/gamefic/query/family.rb +0 -18
- data/lib/gamefic/query/itself.rb +0 -13
- data/lib/gamefic/query/matches.rb +0 -75
- data/lib/gamefic/query/parent.rb +0 -9
- data/lib/gamefic/query/siblings.rb +0 -13
- data/lib/gamefic/query/tree.rb +0 -17
- data/lib/gamefic/scene/base.rb +0 -142
- data/lib/gamefic/scene/multiple_scene.rb +0 -29
- data/lib/gamefic/serialize.rb +0 -196
- data/lib/gamefic/world/callbacks.rb +0 -135
- data/lib/gamefic/world/commands.rb +0 -181
- data/lib/gamefic/world/entities.rb +0 -98
- data/lib/gamefic/world/playbook.rb +0 -233
- data/lib/gamefic/world/players.rb +0 -37
- data/lib/gamefic/world/scenes.rb +0 -228
- data/lib/gamefic/world.rb +0 -18
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
module Query
|
5
|
+
# A General query accepts an array of entities to filter. Unlike Scoped
|
6
|
+
# queries, the resulting entities will not necessarily be in the actor's
|
7
|
+
# immediate vicinity.
|
8
|
+
#
|
9
|
+
# General queries can also be passed a Proc that returns an array of
|
10
|
+
# entities. If the Proc accepts an argument, it will be given the subject
|
11
|
+
# of the query.
|
12
|
+
#
|
13
|
+
class General < Base
|
14
|
+
# @param entities [Array, Proc]
|
15
|
+
# @param arguments [Array<Object>]
|
16
|
+
# @param ambiguous [Boolean]
|
17
|
+
def initialize entities, *arguments, ambiguous: false
|
18
|
+
super(*arguments, ambiguous: ambiguous)
|
19
|
+
@entities = entities
|
20
|
+
end
|
21
|
+
|
22
|
+
def query subject, token
|
23
|
+
filtered = available_entities(subject).that_are(*@arguments)
|
24
|
+
return Result.new(token, nil) if filtered.include?(token)
|
25
|
+
|
26
|
+
scan = Scanner.scan(filtered, token)
|
27
|
+
|
28
|
+
ambiguous? ? ambiguous_result(scan) : unambiguous_result(scan)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def available_entities(subject)
|
34
|
+
if @entities.is_a?(Proc)
|
35
|
+
if @entities.arity.zero?
|
36
|
+
@entities.call
|
37
|
+
else
|
38
|
+
@entities.call(subject)
|
39
|
+
end
|
40
|
+
else
|
41
|
+
@entities
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
module Query
|
5
|
+
# The result of a query.
|
6
|
+
#
|
7
|
+
class Result
|
8
|
+
# @return [Entity, Array<Entity>, String, nil]
|
9
|
+
attr_reader :match
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
attr_reader :remainder
|
13
|
+
|
14
|
+
def initialize match, remainder
|
15
|
+
@match = match
|
16
|
+
@remainder = remainder
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
module Query
|
5
|
+
# A Scoped query uses a Scope to select entities to filter based on their
|
6
|
+
# relationship to the entity performing the query. For example,
|
7
|
+
# Scope::Children would filter from an array of the entity's descendants.
|
8
|
+
#
|
9
|
+
# @return [Class<Gamefic::Scope::Base>]
|
10
|
+
class Scoped < Base
|
11
|
+
attr_reader :scope
|
12
|
+
|
13
|
+
# @param scope [Class<Gamefic::Scope::Base>]
|
14
|
+
def initialize scope, *arguments, ambiguous: false
|
15
|
+
super(*arguments, ambiguous: ambiguous)
|
16
|
+
@scope = scope
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Result]
|
20
|
+
def query(subject, token)
|
21
|
+
available = @scope.matches(subject)
|
22
|
+
.that_are(*@arguments)
|
23
|
+
return Result.new(token, nil) if available.include?(token)
|
24
|
+
|
25
|
+
scan = Scanner.scan(available, token)
|
26
|
+
|
27
|
+
return ambiguous_result(scan) if ambiguous?
|
28
|
+
|
29
|
+
unambiguous_result(scan)
|
30
|
+
end
|
31
|
+
|
32
|
+
def precision
|
33
|
+
@precision ||= @scope.precision + calculate_precision
|
34
|
+
end
|
35
|
+
|
36
|
+
def ambiguous?
|
37
|
+
@ambiguous
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/gamefic/query/text.rb
CHANGED
@@ -1,46 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Gamefic
|
2
4
|
module Query
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
super
|
5
|
+
# A special query that handles text instead of entities.
|
6
|
+
#
|
7
|
+
class Text
|
8
|
+
# @param argument [String, Regexp, nil]
|
9
|
+
def initialize argument = nil
|
10
|
+
@argument = argument
|
11
|
+
validate
|
11
12
|
end
|
12
13
|
|
13
|
-
def
|
14
|
-
|
15
|
-
|
16
|
-
cursor = []
|
17
|
-
matches = []
|
18
|
-
i = 0
|
19
|
-
parts.each { |w|
|
20
|
-
cursor.push w
|
21
|
-
matches = cursor if accept?(cursor.join(' '))
|
22
|
-
i += 1
|
23
|
-
}
|
24
|
-
if continued
|
25
|
-
Matches.new([matches.join(' ')], matches.join(' '), parts[i..-1].join(' '))
|
26
|
-
elsif matches.length == parts.length
|
27
|
-
Matches.new([matches.join(' ')], matches.join(' '), '')
|
14
|
+
def query _subject, token
|
15
|
+
if match? token
|
16
|
+
Result.new(token, '')
|
28
17
|
else
|
29
|
-
|
18
|
+
Result.new(nil, token)
|
30
19
|
end
|
31
20
|
end
|
32
21
|
|
33
|
-
def
|
34
|
-
|
22
|
+
def precision
|
23
|
+
0
|
35
24
|
end
|
36
25
|
|
37
|
-
|
38
|
-
|
39
|
-
|
26
|
+
private
|
27
|
+
|
28
|
+
def match? token
|
29
|
+
return true if @argument.nil?
|
30
|
+
|
31
|
+
case @argument
|
32
|
+
when Regexp
|
33
|
+
token =~ @argument
|
34
|
+
else
|
35
|
+
token == @argument
|
36
|
+
end
|
40
37
|
end
|
41
38
|
|
42
|
-
def
|
43
|
-
|
39
|
+
def validate
|
40
|
+
return if @argument.nil? || @argument.is_a?(String) || @argument.is_a?(Regexp)
|
41
|
+
|
42
|
+
raise ArgumentError, 'Invalid text query argument'
|
44
43
|
end
|
45
44
|
end
|
46
45
|
end
|
data/lib/gamefic/query.rb
CHANGED
@@ -1,15 +1,7 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
autoload :Tree, 'gamefic/query/tree'
|
9
|
-
autoload :Itself, 'gamefic/query/itself'
|
10
|
-
autoload :Matches, 'gamefic/query/matches'
|
11
|
-
autoload :Parent, 'gamefic/query/parent'
|
12
|
-
autoload :Siblings, 'gamefic/query/siblings'
|
13
|
-
autoload :Text, 'gamefic/query/text'
|
14
|
-
end
|
15
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gamefic/query/base'
|
4
|
+
require 'gamefic/query/general'
|
5
|
+
require 'gamefic/query/scoped'
|
6
|
+
require 'gamefic/query/text'
|
7
|
+
require 'gamefic/query/result'
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_literal_string: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
# A proc to be executed in response to a command that matches its verb and
|
5
|
+
# queries.
|
6
|
+
#
|
7
|
+
class Response
|
8
|
+
# @return [Symbol]
|
9
|
+
attr_reader :verb
|
10
|
+
|
11
|
+
# @return [Array<Query::Base>]
|
12
|
+
attr_reader :queries
|
13
|
+
|
14
|
+
# @return [Proc]
|
15
|
+
# attr_reader :block
|
16
|
+
|
17
|
+
# @param verb [Symbol]
|
18
|
+
# @param stage [Object]
|
19
|
+
# @param queries [Array<Query::Base>]
|
20
|
+
# @param meta [Boolean]
|
21
|
+
# @param block [Proc]
|
22
|
+
def initialize verb, stage, *queries, meta: false, &block
|
23
|
+
@verb = verb
|
24
|
+
@stage = stage
|
25
|
+
@queries = map_queryable_objects(queries)
|
26
|
+
@meta = meta
|
27
|
+
@block = block
|
28
|
+
end
|
29
|
+
|
30
|
+
# The `meta?` flag is just a way for authors to identify responses that
|
31
|
+
# serve a purpose other than performing in-game actions. Out-of-game
|
32
|
+
# responses can include features like displaying help documentation or
|
33
|
+
# listing credits.
|
34
|
+
#
|
35
|
+
def meta?
|
36
|
+
@meta
|
37
|
+
end
|
38
|
+
|
39
|
+
def hidden?
|
40
|
+
@hidden ||= verb.to_s.start_with?('_')
|
41
|
+
end
|
42
|
+
|
43
|
+
def syntax
|
44
|
+
@syntax ||= generate_default_syntax
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return an Action if the Response can accept the actor's command.
|
48
|
+
#
|
49
|
+
# @param actor [Entity]
|
50
|
+
# @param command [Command]
|
51
|
+
# @param with_hooks [Boolean]
|
52
|
+
# @return [Action, nil]
|
53
|
+
def attempt actor, command
|
54
|
+
return nil if command.verb != verb
|
55
|
+
|
56
|
+
tokens = command.arguments.clone
|
57
|
+
result = []
|
58
|
+
remainder = ''
|
59
|
+
|
60
|
+
queries.each do |qd|
|
61
|
+
token = tokens.shift
|
62
|
+
txt = "#{remainder} #{token}".strip
|
63
|
+
return nil if txt.empty?
|
64
|
+
|
65
|
+
response = qd.query(actor, txt)
|
66
|
+
return nil if response.match.nil?
|
67
|
+
|
68
|
+
result.push response.match
|
69
|
+
|
70
|
+
remainder = response.remainder
|
71
|
+
end
|
72
|
+
|
73
|
+
return nil unless tokens.empty? && remainder.empty?
|
74
|
+
|
75
|
+
Action.new(actor, result, self)
|
76
|
+
end
|
77
|
+
|
78
|
+
def execute *args
|
79
|
+
Stage.run(@stage, *args, &@block)
|
80
|
+
end
|
81
|
+
|
82
|
+
def precision
|
83
|
+
@precision ||= calculate_precision
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def generate_default_syntax
|
89
|
+
user_friendly = verb.to_s.gsub(/_/, ' ')
|
90
|
+
args = []
|
91
|
+
used_names = []
|
92
|
+
queries.each do |_c|
|
93
|
+
num = 1
|
94
|
+
new_name = ":var"
|
95
|
+
while used_names.include? new_name
|
96
|
+
num += 1
|
97
|
+
new_name = ":var#{num}"
|
98
|
+
end
|
99
|
+
used_names.push new_name
|
100
|
+
user_friendly += " #{new_name}"
|
101
|
+
args.push new_name
|
102
|
+
end
|
103
|
+
Syntax.new(user_friendly.strip, "#{verb} #{args.join(' ')}".strip)
|
104
|
+
end
|
105
|
+
|
106
|
+
def calculate_precision
|
107
|
+
total = 0
|
108
|
+
queries.each { |q| total += q.precision }
|
109
|
+
total -= 1000 if verb.nil?
|
110
|
+
total
|
111
|
+
end
|
112
|
+
|
113
|
+
def map_queryable_objects queries
|
114
|
+
# @todo Considering moving mapping from Actions to here
|
115
|
+
queries
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
class Rulebook
|
5
|
+
# A collection of responses and syntaxes that constitute the actions
|
6
|
+
# available to actors.
|
7
|
+
#
|
8
|
+
class Calls
|
9
|
+
def initialize
|
10
|
+
@verb_response_map = Hash.new { |hash, key| hash[key] = [] }
|
11
|
+
@synonym_syntax_map = Hash.new { |hash, key| hash[key] = [] }
|
12
|
+
end
|
13
|
+
|
14
|
+
def freeze
|
15
|
+
super
|
16
|
+
@verb_response_map.freeze
|
17
|
+
@verb_response_map.values.map(&:freeze)
|
18
|
+
@synonym_syntax_map.freeze
|
19
|
+
@synonym_syntax_map.values.map(&:freeze)
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def syntaxes
|
24
|
+
synonym_syntax_map.values.flatten
|
25
|
+
end
|
26
|
+
|
27
|
+
def synonyms
|
28
|
+
synonym_syntax_map.keys.compact.sort
|
29
|
+
end
|
30
|
+
|
31
|
+
def responses
|
32
|
+
verb_response_map.values.flatten
|
33
|
+
end
|
34
|
+
|
35
|
+
def verbs
|
36
|
+
verb_response_map.keys.compact.sort
|
37
|
+
end
|
38
|
+
|
39
|
+
def responses_for *verbs
|
40
|
+
verbs.flat_map { |verb| verb_response_map.fetch(verb, []) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def syntaxes_for *synonyms
|
44
|
+
synonyms.flat_map { |syn| synonym_syntax_map.fetch(syn, []) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_response response
|
48
|
+
verb_response_map[response.verb].unshift response
|
49
|
+
sort_responses_for response.verb
|
50
|
+
add_syntax response.syntax unless response.verb.to_s.start_with?('_')
|
51
|
+
response
|
52
|
+
end
|
53
|
+
|
54
|
+
# @param syntax [Syntax]
|
55
|
+
# @return [Syntax]
|
56
|
+
def add_syntax syntax
|
57
|
+
raise "No responses exist for \"#{syntax.verb}\"" unless verb_response_map.key?(syntax.verb)
|
58
|
+
|
59
|
+
return if synonym_syntax_map[syntax.synonym].include?(syntax)
|
60
|
+
|
61
|
+
synonym_syntax_map[syntax.synonym].unshift syntax
|
62
|
+
sort_syntaxes_for syntax.synonym
|
63
|
+
syntax
|
64
|
+
end
|
65
|
+
|
66
|
+
def empty?
|
67
|
+
verb_response_map.empty? && synonym_syntax_map.empty?
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.new_array_map
|
71
|
+
Hash.new { |hash, key| hash[key] = [] }
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
attr_reader :verb_response_map
|
77
|
+
|
78
|
+
attr_reader :synonym_syntax_map
|
79
|
+
|
80
|
+
# @param responses [Array<Response>]
|
81
|
+
def sort_responses_for verb
|
82
|
+
verb_response_map[verb].sort_by!.with_index { |a, i| [a.precision, -i] }.reverse!
|
83
|
+
end
|
84
|
+
|
85
|
+
def sort_syntaxes_for synonym
|
86
|
+
synonym_syntax_map[synonym].sort! { |a, b| a.compare b }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -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 [Proc]
|
38
|
+
def on_ready &block
|
39
|
+
@ready_blocks.push block
|
40
|
+
end
|
41
|
+
|
42
|
+
# @yieldparam [Actor]
|
43
|
+
# @return [Proc]
|
44
|
+
def on_player_ready &block
|
45
|
+
@ready_blocks.push(proc do
|
46
|
+
players.each { |plyr| block.call plyr }
|
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| block.call plyr }
|
57
|
+
end)
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Proc]
|
61
|
+
def on_conclude &block
|
62
|
+
@conclude_blocks.push block
|
63
|
+
end
|
64
|
+
|
65
|
+
# @yieldparam [Actor]
|
66
|
+
# @return [Proc]
|
67
|
+
def on_player_conclude &block
|
68
|
+
@player_conclude_blocks.push block
|
69
|
+
end
|
70
|
+
|
71
|
+
# @yieldparam [Actor]
|
72
|
+
# @yieldparam [Hash]
|
73
|
+
# @return [Proc]
|
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
|