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/describable.rb
CHANGED
@@ -1,19 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Gamefic
|
2
|
-
#
|
4
|
+
# A variety of text properties for naming, describing, and referencing
|
3
5
|
# objects.
|
6
|
+
#
|
4
7
|
module Describable
|
5
|
-
|
6
|
-
|
7
|
-
#
|
8
|
-
# The name is usually presented without articles (e.g., "object" instead
|
9
|
-
# of "an object" or "the object" unless the article is part of a proper
|
8
|
+
# The object's name.
|
9
|
+
# Names are usually presented without articles (e.g., "object" instead
|
10
|
+
# of "an object" or "the object") unless the article is part of a proper
|
10
11
|
# name (e.g., "The Ohio State University").
|
11
12
|
#
|
12
13
|
# @return [String]
|
13
14
|
attr_reader :name
|
14
15
|
|
15
|
-
# Alternate words that can
|
16
|
-
#
|
16
|
+
# Alternate words that can reference the object. Synonyms are used in
|
17
|
+
# conjunction with the object's name when scanning tokens.
|
17
18
|
#
|
18
19
|
# @return [String]
|
19
20
|
attr_reader :synonyms
|
@@ -21,62 +22,42 @@ module Gamefic
|
|
21
22
|
# The object's indefinite article (usually "a" or "an").
|
22
23
|
#
|
23
24
|
# @return [String]
|
24
|
-
|
25
|
+
attr_accessor :indefinite_article
|
25
26
|
|
26
27
|
# The object's definite article (usually "the").
|
27
28
|
#
|
28
29
|
# @return [String]
|
29
|
-
|
30
|
+
attr_writer :definite_article
|
30
31
|
|
31
|
-
# Get a set of keywords associated with the object.
|
32
|
-
# Keywords are typically the words in the object's name plus its synonyms.
|
33
|
-
#
|
34
|
-
# @return [Array<String>]
|
35
32
|
def keywords
|
36
|
-
|
33
|
+
"#{name} #{synonyms}".keywords
|
37
34
|
end
|
38
35
|
|
39
|
-
#
|
36
|
+
# The name of the object with an indefinite article.
|
40
37
|
# Note: proper-named objects never append an article, though an article
|
41
38
|
# may be included in its proper name.
|
42
39
|
#
|
43
40
|
# @return [String]
|
44
41
|
def indefinitely
|
45
|
-
(
|
42
|
+
(proper_named? || indefinite_article == '' ? '' : "#{indefinite_article} ") + name.to_s
|
46
43
|
end
|
47
44
|
|
48
|
-
#
|
45
|
+
# The name of the object with a definite article.
|
49
46
|
# Note: proper-named objects never append an article, though an article
|
50
47
|
# may be included in its proper name.
|
51
48
|
#
|
52
49
|
# @return [String]
|
53
50
|
def definitely
|
54
|
-
(
|
51
|
+
(proper_named? || definite_article == '' ? '' : "#{definite_article} ") + name.to_s
|
55
52
|
end
|
56
53
|
|
57
|
-
#
|
54
|
+
# Tefinite article for this object (usually "the").
|
58
55
|
#
|
59
56
|
# @return [String]
|
60
57
|
def definite_article
|
61
58
|
@definite_article || "the"
|
62
59
|
end
|
63
60
|
|
64
|
-
# Set the definite article.
|
65
|
-
#
|
66
|
-
# @param [String] article
|
67
|
-
def definite_article= article
|
68
|
-
@keywords = nil
|
69
|
-
@definite_article = article
|
70
|
-
end
|
71
|
-
|
72
|
-
# Set the indefinite article.
|
73
|
-
#
|
74
|
-
# @param [String] article
|
75
|
-
def indefinite_article= article
|
76
|
-
@keywords = nil
|
77
|
-
@indefinite_article = article
|
78
|
-
end
|
79
|
-
|
80
61
|
# Is the object proper-named?
|
81
62
|
# Proper-named objects typically do not add articles to their names when
|
82
63
|
# referenced #definitely or #indefinitely, e.g., "Jane Doe" instead of
|
@@ -84,18 +65,16 @@ module Gamefic
|
|
84
65
|
#
|
85
66
|
# @return [Boolean]
|
86
67
|
def proper_named?
|
87
|
-
|
68
|
+
@proper_named == true
|
88
69
|
end
|
89
70
|
|
90
71
|
# Set whether the object has a proper name.
|
91
72
|
#
|
92
73
|
# @param bool [Boolean]
|
93
74
|
def proper_named=(bool)
|
94
|
-
if bool
|
95
|
-
|
96
|
-
|
97
|
-
@definite_article = nil
|
98
|
-
end
|
75
|
+
if bool && @definite_article
|
76
|
+
@name = "#{@definite_article} #{@name}".strip
|
77
|
+
@definite_article = nil
|
99
78
|
end
|
100
79
|
@proper_named = bool
|
101
80
|
end
|
@@ -106,27 +85,26 @@ module Gamefic
|
|
106
85
|
#
|
107
86
|
# @param value [String]
|
108
87
|
def name=(value)
|
109
|
-
|
110
|
-
|
111
|
-
if ['a','an'].include?(words[0].downcase)
|
88
|
+
words = value.split
|
89
|
+
if %w[a an].include?(words[0].downcase)
|
112
90
|
@indefinite_article = words[0].downcase
|
113
91
|
@definite_article = 'the'
|
114
|
-
value = value[words[0].length+1
|
92
|
+
value = value[words[0].length + 1..].strip
|
115
93
|
else
|
116
94
|
if words[0].downcase == 'the'
|
117
95
|
if proper_named?
|
118
96
|
@definite_article = nil
|
119
97
|
else
|
120
98
|
@definite_article = 'the'
|
121
|
-
value = value[4
|
99
|
+
value = value[4..].strip
|
122
100
|
end
|
123
101
|
end
|
124
102
|
# Try to guess the indefinite article
|
125
|
-
if [
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
103
|
+
@indefinite_article = if %w[a e i o u].include?(value[0, 1].downcase)
|
104
|
+
'an'
|
105
|
+
else
|
106
|
+
'a'
|
107
|
+
end
|
130
108
|
end
|
131
109
|
@name = value
|
132
110
|
end
|
@@ -134,30 +112,26 @@ module Gamefic
|
|
134
112
|
# Does the object have a description?
|
135
113
|
#
|
136
114
|
# @return [Boolean]
|
137
|
-
def
|
138
|
-
|
115
|
+
def description?
|
116
|
+
@description.to_s != ''
|
139
117
|
end
|
118
|
+
alias has_description? description?
|
140
119
|
|
141
120
|
# Get the object's description.
|
142
121
|
#
|
143
122
|
# @return [String]
|
144
123
|
def description
|
145
|
-
@description || (Describable.default_description
|
124
|
+
@description || format(Describable.default_description, name: definitely, Name: definitely.capitalize_first)
|
146
125
|
end
|
147
126
|
|
148
127
|
# Set the object's description.
|
149
128
|
#
|
150
129
|
# @param text [String]
|
151
130
|
def description=(text)
|
152
|
-
if text != (Describable.default_description
|
153
|
-
@description = text
|
154
|
-
else
|
155
|
-
@description = nil
|
156
|
-
end
|
131
|
+
@description = (text if text != (format(Describable.default_description, name: definitely, Name: definitely.capitalize_first)))
|
157
132
|
end
|
158
133
|
|
159
134
|
def synonyms= text
|
160
|
-
@keywords = nil
|
161
135
|
@synonyms = text
|
162
136
|
end
|
163
137
|
|
@@ -175,12 +149,12 @@ module Gamefic
|
|
175
149
|
#
|
176
150
|
# @return [String]
|
177
151
|
def self.default_description
|
178
|
-
@default_description || "There's nothing special about
|
152
|
+
@default_description || "There's nothing special about %<name>s."
|
179
153
|
end
|
180
154
|
|
181
|
-
# Get a String representation of the object. By default, this is
|
182
|
-
# object's name with an indefinite article, e.g., "a person" or "a red
|
183
|
-
# dog."
|
155
|
+
# Get a String representation of the object. By default, this is either
|
156
|
+
# the object's name with an indefinite article, e.g., "a person" or "a red
|
157
|
+
# dog"; or its proper name, e.g., "Mr. Smith".
|
184
158
|
#
|
185
159
|
# @return [String]
|
186
160
|
def to_s
|
data/lib/gamefic/dispatcher.rb
CHANGED
@@ -6,47 +6,58 @@ module Gamefic
|
|
6
6
|
class Dispatcher
|
7
7
|
# @param actor [Actor]
|
8
8
|
# @param commands [Array<Command>]
|
9
|
-
# @param
|
10
|
-
def initialize actor, commands = [],
|
9
|
+
# @param responses [Array<Response>]
|
10
|
+
def initialize actor, commands = [], responses = []
|
11
11
|
@actor = actor
|
12
12
|
@commands = commands
|
13
|
-
@
|
14
|
-
@
|
13
|
+
@responses = responses
|
14
|
+
@executed = false
|
15
15
|
end
|
16
16
|
|
17
|
-
#
|
18
|
-
#
|
19
|
-
def
|
20
|
-
|
21
|
-
|
17
|
+
# Run the dispatcher.
|
18
|
+
#
|
19
|
+
def execute
|
20
|
+
return if @executed
|
21
|
+
|
22
|
+
action = proceed
|
23
|
+
return unless action
|
24
|
+
|
25
|
+
@executed = action.arguments
|
26
|
+
run_before_action_hooks action
|
27
|
+
return if action.cancelled?
|
28
|
+
|
29
|
+
action.execute
|
30
|
+
run_after_action_hooks action
|
22
31
|
end
|
23
32
|
|
24
33
|
# Get the next executable action.
|
25
34
|
#
|
26
|
-
# @return [Action]
|
27
|
-
def
|
28
|
-
|
29
|
-
while instance.nil? && !@actions.empty?
|
30
|
-
action = actions.shift
|
35
|
+
# @return [Action, nil]
|
36
|
+
def proceed
|
37
|
+
while (response = responses.shift)
|
31
38
|
commands.each do |cmd|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
39
|
+
action = response.attempt(actor, cmd)
|
40
|
+
next unless action && arguments_match?(action.arguments)
|
41
|
+
|
42
|
+
return action
|
37
43
|
end
|
38
44
|
end
|
39
|
-
|
45
|
+
nil # Without this, return value in Opal is undefined
|
40
46
|
end
|
41
47
|
|
42
48
|
# @param actor [Active]
|
43
|
-
# @param
|
49
|
+
# @param input [String]
|
44
50
|
# @return [Dispatcher]
|
45
|
-
def self.dispatch actor,
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
51
|
+
def self.dispatch actor, input
|
52
|
+
commands = Syntax.tokenize(input, actor.epic.rulebooks.flat_map(&:syntaxes))
|
53
|
+
verbs = commands.map(&:verb).uniq
|
54
|
+
responses = actor.epic
|
55
|
+
.rulebooks
|
56
|
+
.to_a
|
57
|
+
.reverse
|
58
|
+
.flat_map { |pb| pb.responses_for(*verbs) }
|
59
|
+
.reject(&:hidden?)
|
60
|
+
new(actor, commands, responses)
|
50
61
|
end
|
51
62
|
|
52
63
|
# @param actor [Active]
|
@@ -54,10 +65,13 @@ module Gamefic
|
|
54
65
|
# @param params [Array<Object>]
|
55
66
|
# @return [Dispatcher]
|
56
67
|
def self.dispatch_from_params actor, verb, params
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
68
|
+
command = Command.new(verb, params)
|
69
|
+
responses = actor.epic
|
70
|
+
.rulebooks
|
71
|
+
.to_a
|
72
|
+
.reverse
|
73
|
+
.flat_map { |pb| pb.responses_for(verb) }
|
74
|
+
new(actor, [command], responses)
|
61
75
|
end
|
62
76
|
|
63
77
|
protected
|
@@ -68,7 +82,24 @@ module Gamefic
|
|
68
82
|
# @return [Array<Command>]
|
69
83
|
attr_reader :commands
|
70
84
|
|
71
|
-
# @return [Array<
|
72
|
-
attr_reader :
|
85
|
+
# @return [Array<Response>]
|
86
|
+
attr_reader :responses
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# After the first action gets selected, subsequent actions need to use the
|
91
|
+
# same arguments.
|
92
|
+
#
|
93
|
+
def arguments_match? arguments
|
94
|
+
!@executed || arguments == @executed
|
95
|
+
end
|
96
|
+
|
97
|
+
def run_before_action_hooks action
|
98
|
+
actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_before_actions action }
|
99
|
+
end
|
100
|
+
|
101
|
+
def run_after_action_hooks action
|
102
|
+
actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_after_actions action }
|
103
|
+
end
|
73
104
|
end
|
74
105
|
end
|
data/lib/gamefic/entity.rb
CHANGED
@@ -1,26 +1,35 @@
|
|
1
|
-
|
2
|
-
require "gamefic/describable"
|
3
|
-
require 'gamefic/messaging'
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
3
|
module Gamefic
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
4
|
+
# Entities are the people, places, and things that exist in a Gamefic
|
5
|
+
# narrative. Authors are encouraged to define Entity subclasses to create
|
6
|
+
# entity types that have additional features or need special handling in
|
7
|
+
# actions.
|
9
8
|
#
|
10
|
-
class Entity
|
9
|
+
class Entity
|
10
|
+
include Describable
|
11
11
|
include Node
|
12
|
-
include Messaging
|
12
|
+
# include Messaging
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
14
|
+
def initialize **args
|
15
|
+
klass = self.class
|
16
|
+
defaults = {}
|
17
|
+
while klass <= Entity
|
18
|
+
defaults = klass.default_attributes.merge(defaults)
|
19
|
+
klass = klass.superclass
|
20
20
|
end
|
21
|
-
|
21
|
+
defaults.merge(args).each_pair { |k, v| send "#{k}=", v }
|
22
|
+
|
23
|
+
yield(self) if block_given?
|
24
|
+
|
25
|
+
post_initialize
|
22
26
|
end
|
23
27
|
|
28
|
+
# This method can be overridden for additional processing after the entity
|
29
|
+
# has been created.
|
30
|
+
#
|
31
|
+
def post_initialize; end
|
32
|
+
|
24
33
|
# A freeform property dictionary.
|
25
34
|
# Authors can use the session hash to assign custom properties to the
|
26
35
|
# entity. It can also be referenced directly using [] without the method
|
@@ -31,20 +40,36 @@ module Gamefic
|
|
31
40
|
@session ||= {}
|
32
41
|
end
|
33
42
|
|
34
|
-
# Get a custom property.
|
35
|
-
#
|
36
43
|
# @param key [Symbol] The property's name
|
37
44
|
# @return The value of the property
|
38
45
|
def [](key)
|
39
46
|
session[key]
|
40
47
|
end
|
41
48
|
|
42
|
-
# Set a custom property.
|
43
|
-
#
|
44
49
|
# @param key [Symbol] The property's name
|
45
50
|
# @param value The value to set
|
46
51
|
def []=(key, value)
|
47
52
|
session[key] = value
|
48
53
|
end
|
54
|
+
|
55
|
+
def inspect
|
56
|
+
"#<#{self.class} #{name}>"
|
57
|
+
end
|
58
|
+
|
59
|
+
class << self
|
60
|
+
# Set or update the default values for new instances.
|
61
|
+
#
|
62
|
+
# @param attrs [Hash] The attributes to be merged into the defaults.
|
63
|
+
def set_default attrs = {}
|
64
|
+
default_attributes.merge! attrs
|
65
|
+
end
|
66
|
+
|
67
|
+
# A hash of default values for attributes when creating an instance.
|
68
|
+
#
|
69
|
+
# @return [Hash]
|
70
|
+
def default_attributes
|
71
|
+
@default_attributes ||= {}
|
72
|
+
end
|
73
|
+
end
|
49
74
|
end
|
50
75
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Gamefic
|
6
|
+
# A simple logger.
|
7
|
+
#
|
8
|
+
module Logging
|
9
|
+
module_function
|
10
|
+
|
11
|
+
# @return [Logger]
|
12
|
+
def logger
|
13
|
+
Gamefic.logger
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def logger
|
19
|
+
@logger ||= select_logger.tap do |l|
|
20
|
+
l.formatter = proc { |sev, _dt, _prog, msg| "[#{sev}] #{msg}\n" }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def select_logger
|
27
|
+
# We use #tap here because `Logger.new(STDERR, level: Logger::WARN)`
|
28
|
+
# fails in Opal
|
29
|
+
Logger.new($stderr).tap { |log| log.level = Logger::WARN }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
# Message formatting and buffering.
|
5
|
+
#
|
6
|
+
class Messenger
|
7
|
+
def initialize
|
8
|
+
@buffers = ['']
|
9
|
+
end
|
10
|
+
|
11
|
+
# Create a temporary buffer while yielding the given block and return the
|
12
|
+
# buffered text.
|
13
|
+
#
|
14
|
+
# @return [String]
|
15
|
+
def buffer
|
16
|
+
@buffers.push('')
|
17
|
+
yield if block_given?
|
18
|
+
@buffers.pop
|
19
|
+
end
|
20
|
+
|
21
|
+
# Add a formatted message to the current buffer.
|
22
|
+
#
|
23
|
+
# This method will automatically wrap the message in HTML paragraphs.
|
24
|
+
# To send a message without formatting, use #stream instead.
|
25
|
+
#
|
26
|
+
# @param message [String, #to_s]
|
27
|
+
# @return [String] The messages in the current buffer
|
28
|
+
def tell(message)
|
29
|
+
@buffers.push(@buffers.pop + format(message.to_s))
|
30
|
+
.last
|
31
|
+
end
|
32
|
+
|
33
|
+
# Add a raw text message to the current buffer.
|
34
|
+
#
|
35
|
+
# Unlike #tell, this method will not wrap the message in HTML paragraphs.
|
36
|
+
#
|
37
|
+
# @param message [String, #to_s]
|
38
|
+
# @return [String] The messages in the current buffer
|
39
|
+
def stream(message)
|
40
|
+
@buffers.push(@buffers.pop + message.to_s)
|
41
|
+
.last
|
42
|
+
end
|
43
|
+
|
44
|
+
# Get the currently buffered messages.
|
45
|
+
#
|
46
|
+
# @return [String]
|
47
|
+
def messages
|
48
|
+
@buffers.last
|
49
|
+
end
|
50
|
+
|
51
|
+
# Clear the buffered messages.
|
52
|
+
#
|
53
|
+
# @return [String] The flushed message
|
54
|
+
def flush
|
55
|
+
@buffers.pop.tap { @buffers.push '' }
|
56
|
+
end
|
57
|
+
|
58
|
+
def format(message)
|
59
|
+
"<p>#{message.strip}</p>"
|
60
|
+
.gsub(/[ \t\r]*\n[ \t\r]*\n[ \t\r]*/, "</p><p>")
|
61
|
+
.gsub(/[ \t]*\n[ \t]*/, ' ')
|
62
|
+
.gsub(/<p>\s*<p>/, '<p>')
|
63
|
+
.gsub(%r{</p>\s*</p>}, '</p>')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
# A base class for building and managing the resources that compose a story.
|
5
|
+
# The Plot and Subplot classes inherit from Narrative and provide additional
|
6
|
+
# functionality.
|
7
|
+
#
|
8
|
+
class Narrative
|
9
|
+
extend Scriptable
|
10
|
+
|
11
|
+
include Logging
|
12
|
+
include Scriptable::Actions
|
13
|
+
include Scriptable::Entities
|
14
|
+
include Scriptable::Events
|
15
|
+
include Scriptable::Proxy
|
16
|
+
include Scriptable::Queries
|
17
|
+
include Scriptable::Scenes
|
18
|
+
|
19
|
+
attr_reader :rulebook
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
self.class.included_blocks.select(&:seed?).each { |blk| Stage.run self, &blk.code }
|
23
|
+
entity_vault.lock
|
24
|
+
@rulebook = nil
|
25
|
+
hydrate
|
26
|
+
end
|
27
|
+
|
28
|
+
def scenes
|
29
|
+
rulebook.scenes.names
|
30
|
+
end
|
31
|
+
|
32
|
+
# Introduce an actor to the story.
|
33
|
+
#
|
34
|
+
# @param player [Gamefic::Actor]
|
35
|
+
# @return [Gamefic::Actor]
|
36
|
+
def introduce(player = Gamefic::Actor.new)
|
37
|
+
cast player
|
38
|
+
rulebook.scenes.introductions.each do |scene|
|
39
|
+
scene.run_start_blocks player, nil
|
40
|
+
end
|
41
|
+
player
|
42
|
+
end
|
43
|
+
|
44
|
+
# A narrative is considered to be concluding when all of its players are in
|
45
|
+
# a conclusion scene. Engines can use this method to determine whether the
|
46
|
+
# game is ready to end.
|
47
|
+
#
|
48
|
+
def concluding?
|
49
|
+
players.empty? || players.all?(&:concluding?)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Add an active entity to the narrative.
|
53
|
+
#
|
54
|
+
# @param [Gamefic::Active]
|
55
|
+
# @return [Gamefic::Active]
|
56
|
+
def cast active
|
57
|
+
active.epic.add self
|
58
|
+
player_vault.add active
|
59
|
+
entity_vault.add active
|
60
|
+
active
|
61
|
+
end
|
62
|
+
|
63
|
+
# Remove an active entity from the narrative.
|
64
|
+
#
|
65
|
+
# @param [Gamefic::Active]
|
66
|
+
# @return [Gamefic::Active]
|
67
|
+
def uncast active
|
68
|
+
active.epic.delete self
|
69
|
+
player_vault.delete active
|
70
|
+
entity_vault.delete active
|
71
|
+
active
|
72
|
+
end
|
73
|
+
|
74
|
+
def ready
|
75
|
+
rulebook.run_ready_blocks
|
76
|
+
end
|
77
|
+
|
78
|
+
def update
|
79
|
+
rulebook.run_update_blocks
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [Object]
|
83
|
+
def detach
|
84
|
+
cache = @rulebook
|
85
|
+
@rulebook = nil
|
86
|
+
cache
|
87
|
+
end
|
88
|
+
|
89
|
+
def attach cache
|
90
|
+
@rulebook = cache
|
91
|
+
end
|
92
|
+
|
93
|
+
def hydrate
|
94
|
+
@rulebook = Rulebook.new(self)
|
95
|
+
@rulebook.script_with_defaults
|
96
|
+
@rulebook.freeze
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.inherited klass
|
100
|
+
super
|
101
|
+
klass.blocks.concat blocks
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|