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
data/lib/gamefic/scriptable.rb
CHANGED
@@ -1,94 +1,188 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
1
5
|
module Gamefic
|
2
|
-
#
|
3
|
-
#
|
4
|
-
# @!method stage(*args, &block)
|
5
|
-
# Execute a block of code in a subset of the owner's scope.
|
6
|
+
# A class module that enables scripting.
|
6
7
|
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
8
|
+
# Narratives extend Scriptable to enable definition of scripts and seeds.
|
9
|
+
# Modules can also be extended with Scriptable to make them includable to
|
10
|
+
# other Scriptables.
|
10
11
|
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# }
|
12
|
+
# @example Include a scriptable module in a plot
|
13
|
+
# module MyScript
|
14
|
+
# extend Gamefic::Scriptable
|
15
15
|
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
16
|
+
# respond :myscript do |actor|
|
17
|
+
# actor.tell "This command was added by MyScript"
|
18
|
+
# end
|
19
|
+
# end
|
20
20
|
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
21
|
+
# class MyPlot < Gamefic::Plot
|
22
|
+
# include MyScript
|
23
|
+
# end
|
24
24
|
#
|
25
|
-
# @yieldpublic [Gamefic::Plot]
|
26
|
-
# @return [Object] The value returned by the executed code
|
27
|
-
#
|
28
|
-
# @!method theater
|
29
|
-
# The object that acts as an isolated namespace for staged code.
|
30
|
-
# @return [Object]
|
31
|
-
#
|
32
|
-
# @!parse alias cleanroom theater
|
33
25
|
module Scriptable
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
end
|
26
|
+
autoload :Actions, 'gamefic/scriptable/actions'
|
27
|
+
autoload :Entities, 'gamefic/scriptable/entities'
|
28
|
+
autoload :Events, 'gamefic/scriptable/events'
|
29
|
+
autoload :Queries, 'gamefic/scriptable/queries'
|
30
|
+
autoload :Proxy, 'gamefic/scriptable/proxy'
|
31
|
+
autoload :Scenes, 'gamefic/scriptable/scenes'
|
41
32
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
33
|
+
include Proxy
|
34
|
+
include Queries
|
35
|
+
# @!parse
|
36
|
+
# include Scriptable::Actions
|
37
|
+
# include Scriptable::Events
|
38
|
+
# include Scriptable::Scenes
|
39
|
+
|
40
|
+
# @return [Array<Block>]
|
41
|
+
def blocks
|
42
|
+
@blocks ||= []
|
43
|
+
end
|
44
|
+
alias scripts blocks
|
45
|
+
|
46
|
+
# Add a block of code to be executed during initialization.
|
47
|
+
#
|
48
|
+
# These blocks are primarily used to define actions, scenes, and hooks in
|
49
|
+
# the narrative's rulebook. Entities and game data should be initialized
|
50
|
+
# with `seed`.
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# class MyPlot < Gamefic::Plot
|
54
|
+
# script do
|
55
|
+
# introduction do |actor|
|
56
|
+
# actor.tell 'Hello, world!'
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# respond :wait do |actor|
|
60
|
+
# actor.tell 'Time passes.'
|
61
|
+
# end
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
def script &block
|
66
|
+
blocks.push Block.new(:script, block)
|
51
67
|
end
|
52
68
|
|
53
|
-
|
54
|
-
|
69
|
+
# Add a block of code to generate content after initialization.
|
70
|
+
#
|
71
|
+
# Seeds run after the initial scripts have been executed. Their primary
|
72
|
+
# use is to add entities and other data components, especially randomized
|
73
|
+
# or procedurally generated content that can vary between instances.
|
74
|
+
#
|
75
|
+
# @note Seeds do not get executed when a narrative is restored from a
|
76
|
+
# snapshot.
|
77
|
+
#
|
78
|
+
# @example
|
79
|
+
# class MyPlot < Gamefic::Plot
|
80
|
+
# seed do
|
81
|
+
# @thing = make Gamefic::Entity, name: 'a thing'
|
82
|
+
# end
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
def seed &block
|
86
|
+
blocks.push Block.new(:seed, block)
|
55
87
|
end
|
56
88
|
|
57
|
-
|
89
|
+
# @return [Array<Block>]
|
90
|
+
def included_blocks
|
91
|
+
included_modules.that_are(Scriptable)
|
92
|
+
.uniq
|
93
|
+
.reverse
|
94
|
+
.flat_map(&:blocks)
|
95
|
+
.concat(blocks)
|
96
|
+
end
|
58
97
|
|
59
|
-
#
|
98
|
+
# Seed an entity.
|
99
|
+
#
|
100
|
+
# @example
|
101
|
+
# make_seed Gamefic::Entity, name: 'thing'
|
60
102
|
#
|
61
|
-
|
62
|
-
|
103
|
+
# @param klass [Class<Gamefic::Entity>]
|
104
|
+
def make_seed klass, **opts
|
105
|
+
@count ||= 0
|
106
|
+
seed { make(klass, **opts) }
|
107
|
+
@count.tap { @count += 1 }
|
63
108
|
end
|
64
|
-
end
|
65
|
-
end
|
66
109
|
|
67
|
-
#
|
68
|
-
#
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
110
|
+
# Seed an entity with an attribute method.
|
111
|
+
#
|
112
|
+
# @example
|
113
|
+
# class Plot < Gamefic::Plot
|
114
|
+
# attr_seed :thing, Gamefic::Entity, name: 'thing'
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
# plot = Plot.new
|
118
|
+
# plot.thing #=> #<Gamefic::Entity a thing>
|
119
|
+
#
|
120
|
+
# @param klass [Class<Gamefic::Entity>]
|
121
|
+
def attr_seed name, klass, **opts
|
122
|
+
@count ||= 0
|
123
|
+
seed do
|
124
|
+
instance_variable_set("@#{name}", make(klass, **opts))
|
125
|
+
self.class.define_method(name) { instance_variable_get("@#{name}") }
|
126
|
+
end
|
127
|
+
@count.tap { @count += 1 }
|
128
|
+
end
|
129
|
+
|
130
|
+
if RUBY_ENGINE == 'opal'
|
131
|
+
# :nocov:
|
132
|
+
def method_missing method, *args, &block
|
133
|
+
return super unless respond_to_missing?(method)
|
134
|
+
|
135
|
+
script { send(method, *args, &block) }
|
136
|
+
end
|
137
|
+
# :nocov:
|
138
|
+
else
|
139
|
+
def method_missing method, *args, **kwargs, &block
|
140
|
+
return super unless respond_to_missing?(method)
|
141
|
+
|
142
|
+
script { send(method, *args, **kwargs, &block) }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def respond_to_missing?(method, _with_private = false)
|
147
|
+
[Scriptable::Actions, Scriptable::Events, Scriptable::Scenes].flat_map(&:public_instance_methods)
|
148
|
+
.include?(method)
|
149
|
+
end
|
73
150
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
151
|
+
# Create an anonymous module that includes the features of a Scriptable
|
152
|
+
# module but does not include its scripts.
|
153
|
+
#
|
154
|
+
# This can be useful when you need access to the Scriptable's constants and
|
155
|
+
# instance methods, but you don't want to duplicate its rules.
|
156
|
+
#
|
157
|
+
# @example
|
158
|
+
# # Plot and Subplot will both include the `info` method, but
|
159
|
+
# # only Plot will implement the `think` action.
|
160
|
+
#
|
161
|
+
# module Shared
|
162
|
+
# extend Gamefic::Scriptable
|
163
|
+
#
|
164
|
+
# def info
|
165
|
+
# "This method was added by the Shared module."
|
166
|
+
# end
|
167
|
+
#
|
168
|
+
# respond :think do |actor|
|
169
|
+
# actor.tell 'You ponder your predicament.'
|
170
|
+
# end
|
171
|
+
# end
|
172
|
+
#
|
173
|
+
# class Plot < Gamefic::Plot
|
174
|
+
# include Shared
|
175
|
+
# end
|
176
|
+
#
|
177
|
+
# class Subplot < Gamefic::Subplot
|
178
|
+
# include Shared.no_scripts
|
179
|
+
# end
|
180
|
+
#
|
181
|
+
# @return [Module]
|
182
|
+
def no_scripts
|
183
|
+
Module.new.tap do |mod|
|
184
|
+
append_features(mod)
|
88
185
|
end
|
89
|
-
theater.extend Gamefic::Serialize
|
90
|
-
theater
|
91
186
|
end
|
92
187
|
end
|
93
|
-
alias cleanroom theater
|
94
188
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'corelib/marshal' if RUBY_ENGINE == 'opal' # Required in browser
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
module Gamefic
|
7
|
+
# Save and restore plots.
|
8
|
+
#
|
9
|
+
module Snapshot
|
10
|
+
# Save a base64-encoded snapshot of a plot.
|
11
|
+
#
|
12
|
+
# @param plot [Plot]
|
13
|
+
# @return [String]
|
14
|
+
def self.save plot
|
15
|
+
cache = plot.detach
|
16
|
+
binary = Marshal.dump(plot)
|
17
|
+
plot.attach cache
|
18
|
+
Base64.encode64(binary)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Restore a plot from a base64-encoded string.
|
22
|
+
#
|
23
|
+
# @param snapshot [String]
|
24
|
+
# @return [Plot]
|
25
|
+
def self.restore snapshot
|
26
|
+
binary = Base64.decode64(snapshot)
|
27
|
+
Marshal.load(binary).tap do |plot|
|
28
|
+
plot.hydrate
|
29
|
+
# @todo Opal marshal dumps are not idempotent
|
30
|
+
next if RUBY_ENGINE == 'opal' || Snapshot.save(plot) == snapshot
|
31
|
+
|
32
|
+
Logging.logger.warn "Scripts modified #{plot.class} data. Snapshot may not have restored properly"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
# A safe execution environment for narrative code.
|
5
|
+
#
|
6
|
+
module Stage
|
7
|
+
module_function
|
8
|
+
|
9
|
+
# @param narrative [Narrative]
|
10
|
+
def run(narrative, *args, &code)
|
11
|
+
container = narrative.clone
|
12
|
+
narrative.instance_exec(*args, &code).tap { validate_changes narrative, container, code }
|
13
|
+
end
|
14
|
+
|
15
|
+
OVERWRITEABLE_CLASSES = [String, Numeric, Symbol].freeze
|
16
|
+
|
17
|
+
SWAPPABLE_VALUES = [true, false, nil].freeze
|
18
|
+
|
19
|
+
class << self
|
20
|
+
private
|
21
|
+
|
22
|
+
def validate_changes narrative, container, code
|
23
|
+
container.instance_variables.each do |var|
|
24
|
+
next unless narrative.instance_variables.include?(var)
|
25
|
+
|
26
|
+
cval = container.instance_variable_get(var)
|
27
|
+
|
28
|
+
nval = narrative.instance_variable_get(var)
|
29
|
+
next if cval == nval
|
30
|
+
|
31
|
+
validate_overwriteable(cval, nval, "Unsafe reassignment of #{var} in #{code}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate_overwriteable cval, nval, error
|
36
|
+
raise error unless overwriteable?(cval, nval)
|
37
|
+
end
|
38
|
+
|
39
|
+
def overwriteable? cval, nval
|
40
|
+
return true if swappable?(cval, nval)
|
41
|
+
|
42
|
+
allowed = OVERWRITEABLE_CLASSES.find { |klass| cval.is_a?(klass) }
|
43
|
+
allowed && cval.is_a?(allowed)
|
44
|
+
end
|
45
|
+
|
46
|
+
def swappable? *values
|
47
|
+
values.all? { |val| SWAPPABLE_VALUES.include?(val) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/gamefic/subplot.rb
CHANGED
@@ -1,106 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'gamefic/plot'
|
2
4
|
|
3
5
|
module Gamefic
|
4
6
|
# Subplots are disposable plots that run inside a parent plot. They can be
|
5
|
-
# started and concluded at any time during the parent plot's
|
7
|
+
# started and concluded at any time during the parent plot's runtime.
|
6
8
|
#
|
7
|
-
class Subplot
|
8
|
-
|
9
|
-
|
10
|
-
include Gamefic::Serialize
|
11
|
-
# @!parse extend Scriptable::ClassMethods
|
9
|
+
class Subplot < Narrative
|
10
|
+
# @return [Hash]
|
11
|
+
attr_reader :config
|
12
12
|
|
13
|
-
# @return [
|
13
|
+
# @return [Plot]
|
14
14
|
attr_reader :plot
|
15
15
|
|
16
16
|
# @param plot [Gamefic::Plot]
|
17
|
-
# @param introduce [Gamefic::Actor, nil]
|
18
|
-
# @param
|
19
|
-
|
20
|
-
def initialize plot, introduce: nil, next_cue: nil, **more
|
17
|
+
# @param introduce [Gamefic::Actor, Array<Gamefic::Actor>, nil]
|
18
|
+
# @param config [Hash]
|
19
|
+
def initialize plot, introduce: [], **config
|
21
20
|
@plot = plot
|
22
|
-
@
|
23
|
-
|
24
|
-
@
|
25
|
-
|
26
|
-
|
27
|
-
playbook.freeze
|
28
|
-
self.introduce introduce unless introduce.nil?
|
29
|
-
@static = [self] + scene_classes + entities
|
30
|
-
end
|
31
|
-
|
32
|
-
def static
|
33
|
-
plot.static
|
34
|
-
end
|
35
|
-
|
36
|
-
def players
|
37
|
-
@players ||= []
|
38
|
-
end
|
39
|
-
|
40
|
-
def subplot
|
41
|
-
self
|
42
|
-
end
|
43
|
-
|
44
|
-
def default_scene
|
45
|
-
plot.default_scene
|
46
|
-
end
|
47
|
-
|
48
|
-
def default_conclusion
|
49
|
-
plot.default_conclusion
|
50
|
-
end
|
51
|
-
|
52
|
-
def playbook
|
53
|
-
@playbook ||= Gamefic::Plot::Playbook.new
|
21
|
+
@config = config
|
22
|
+
configure
|
23
|
+
@config.freeze
|
24
|
+
super()
|
25
|
+
[introduce].flatten.each { |pl| self.introduce pl }
|
54
26
|
end
|
55
27
|
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
-
ent
|
60
|
-
end
|
61
|
-
|
62
|
-
def exeunt player
|
63
|
-
player_conclude_procs.each { |block| block.call player }
|
64
|
-
player.playbooks.delete playbook
|
65
|
-
player.cue (@next_cue || default_scene)
|
66
|
-
players.delete player
|
28
|
+
def ready
|
29
|
+
super
|
30
|
+
conclude if concluding?
|
67
31
|
end
|
68
32
|
|
69
33
|
def conclude
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
entities.reject(&:nil?).each { |e| destroy e }
|
77
|
-
# plot.static.remove(scene_classes + entities)
|
34
|
+
rulebook.run_conclude_blocks
|
35
|
+
players.each do |plyr|
|
36
|
+
rulebook.run_player_conclude_blocks plyr
|
37
|
+
uncast plyr
|
38
|
+
end
|
39
|
+
entities.each { |ent| destroy ent }
|
78
40
|
end
|
79
41
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
def
|
85
|
-
|
86
|
-
# might be cases where a subplot gets created with the intention of
|
87
|
-
# introducing players in a later turn.
|
88
|
-
conclude if players.empty?
|
89
|
-
return if concluded?
|
90
|
-
playbook.freeze
|
91
|
-
call_ready
|
92
|
-
call_player_ready
|
42
|
+
# Make an entity that persists in the subplot's parent plot.
|
43
|
+
#
|
44
|
+
# @see Plot#make
|
45
|
+
#
|
46
|
+
def persist klass, **args
|
47
|
+
plot.make klass, *args
|
93
48
|
end
|
94
49
|
|
95
|
-
|
96
|
-
|
97
|
-
|
50
|
+
# Start a new subplot based on the provided class.
|
51
|
+
#
|
52
|
+
# @note A subplot's host is always the base plot, regardless of whether
|
53
|
+
# it was branched from another subplot.
|
54
|
+
#
|
55
|
+
# @param subplot_class [Class<Gamefic::Subplot>] The Subplot class
|
56
|
+
# @param introduce [Gamefic::Actor, Array<Gamefic::Actor>, nil] Players to introduce
|
57
|
+
# @param config [Hash] Subplot configuration
|
58
|
+
# @return [Gamefic::Subplot]
|
59
|
+
def branch subplot_class = Gamefic::Subplot, introduce: [], **config
|
60
|
+
plot.branch subplot_class, introduce: introduce, **config
|
98
61
|
end
|
99
62
|
|
100
63
|
# Subclasses can override this method to handle additional configuration
|
101
64
|
# options.
|
102
65
|
#
|
103
|
-
def configure
|
66
|
+
def configure; end
|
67
|
+
|
68
|
+
def inspect
|
69
|
+
"#<#{self.class}>"
|
70
|
+
end
|
71
|
+
|
72
|
+
def hydrate
|
73
|
+
@rulebook = Rulebook.new(self)
|
74
|
+
@rulebook.script
|
75
|
+
@rulebook.freeze
|
104
76
|
end
|
105
77
|
end
|
106
78
|
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
class Syntax
|
5
|
+
# Template data for syntaxes.
|
6
|
+
#
|
7
|
+
class Template
|
8
|
+
PARAM_REGEXP = /^:[a-z0-9_]+$/i.freeze
|
9
|
+
|
10
|
+
# @return [String]
|
11
|
+
attr_reader :text
|
12
|
+
|
13
|
+
# @return [Array<String>]
|
14
|
+
attr_reader :params
|
15
|
+
|
16
|
+
def initialize text
|
17
|
+
@text = text.normalize
|
18
|
+
@params = @text.keywords.select { |word| word.start_with?(':') }
|
19
|
+
end
|
20
|
+
|
21
|
+
def keywords
|
22
|
+
text.keywords
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
text
|
27
|
+
end
|
28
|
+
|
29
|
+
def regexp
|
30
|
+
@regexp ||= Regexp.new("^#{make_tokens.join(' ')}$", Regexp::IGNORECASE)
|
31
|
+
end
|
32
|
+
|
33
|
+
def verb
|
34
|
+
@verb ||= Syntax.literal_or_nil(keywords.first)
|
35
|
+
end
|
36
|
+
|
37
|
+
def compare other
|
38
|
+
if keywords.length == other.keywords.length
|
39
|
+
other.verb <=> verb
|
40
|
+
else
|
41
|
+
other.keywords.length <=> keywords.length
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @param tmpl_or_str [Template, String]
|
46
|
+
# @return [Template]
|
47
|
+
def self.to_template tmpl_or_str
|
48
|
+
return tmpl_or_str if tmpl_or_str.is_a?(Template)
|
49
|
+
|
50
|
+
Template.new(tmpl_or_str)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# @return [Array<String>]
|
56
|
+
def make_tokens
|
57
|
+
keywords.map.with_index do |word, idx|
|
58
|
+
next word unless word.match?(PARAM_REGEXP)
|
59
|
+
|
60
|
+
next nil if idx.positive? && keywords[idx - 1].match?(PARAM_REGEXP)
|
61
|
+
|
62
|
+
'([\w\W\s\S]*?)'
|
63
|
+
end.compact
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|