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
@@ -1,19 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gamefic
2
- # Add a variety of text properties for naming, describing, and referencing
4
+ # A variety of text properties for naming, describing, and referencing
3
5
  # objects.
6
+ #
4
7
  module Describable
5
- include Keywords
6
-
7
- # Get the name of the object.
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 be used to describe the object. Synonyms are
16
- # used in conjunction with the object's name when generating keywords.
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
- attr_reader :indefinite_article
25
+ attr_accessor :indefinite_article
25
26
 
26
27
  # The object's definite article (usually "the").
27
28
  #
28
29
  # @return [String]
29
- attr_reader :definite_article
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
- @keywords ||= "#{definite_article} #{indefinite_article} #{name} #{synonyms}".downcase.split(Keywords::SPLIT_REGEXP).uniq
33
+ "#{name} #{synonyms}".keywords
37
34
  end
38
35
 
39
- # Get the name of the object with an indefinite article.
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
- ((proper_named? or indefinite_article == '') ? '' : "#{indefinite_article} ") + name.to_s
42
+ (proper_named? || indefinite_article == '' ? '' : "#{indefinite_article} ") + name.to_s
46
43
  end
47
44
 
48
- # Get the name of the object with a definite article.
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
- ((proper_named? or definite_article == '') ? '' : "#{definite_article} ") + name.to_s
51
+ (proper_named? || definite_article == '' ? '' : "#{definite_article} ") + name.to_s
55
52
  end
56
53
 
57
- # Get the definite article for this object (usually "the").
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
- (@proper_named == true)
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 == true
95
- if @definite_article != nil
96
- @name = "#{@definite_article} #{@name}"
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
- @keywords = nil
110
- words = value.split_words
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..-1].strip
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..-1].strip
99
+ value = value[4..].strip
122
100
  end
123
101
  end
124
102
  # Try to guess the indefinite article
125
- if ['a','e','i','o','u'].include?(value[0,1].downcase)
126
- @indefinite_article = 'an'
127
- else
128
- @indefinite_article = 'a'
129
- end
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 has_description?
138
- (@description.to_s != '')
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 % { :name => self.definitely, :Name => self.definitely.capitalize_first })
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 % { :name => self.definitely, :Name => self.definitely.capitalize_first })
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 %{name}."
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 the
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
@@ -6,47 +6,58 @@ module Gamefic
6
6
  class Dispatcher
7
7
  # @param actor [Actor]
8
8
  # @param commands [Array<Command>]
9
- # @param actions [Array<Action>]
10
- def initialize actor, commands = [], actions = []
9
+ # @param responses [Array<Response>]
10
+ def initialize actor, commands = [], responses = []
11
11
  @actor = actor
12
12
  @commands = commands
13
- @actions = actions
14
- @started = false
13
+ @responses = responses
14
+ @executed = false
15
15
  end
16
16
 
17
- # @param dispatcher [Dispatcher]
18
- # @return [void]
19
- def merge dispatcher
20
- commands.concat dispatcher.commands
21
- actions.concat dispatcher.actions
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 next
28
- instance = nil
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
- instance = action.attempt(actor, cmd, !@started)
33
- if instance
34
- @started = true
35
- break
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
- instance
45
+ nil # Without this, return value in Opal is undefined
40
46
  end
41
47
 
42
48
  # @param actor [Active]
43
- # @param command [String]
49
+ # @param input [String]
44
50
  # @return [Dispatcher]
45
- def self.dispatch actor, command
46
- group = actor.playbooks.reverse.map { |p| p.dispatch(actor, command) }
47
- dispatcher = Dispatcher.new(actor)
48
- group.each { |d| dispatcher.merge d }
49
- dispatcher
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
- group = actor.playbooks.reverse.map { |p| p.dispatch_from_params(actor, verb, params) }
58
- dispatcher = Dispatcher.new(actor)
59
- group.each { |d| dispatcher.merge d }
60
- dispatcher
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<Action>]
72
- attr_reader :actions
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
@@ -1,26 +1,35 @@
1
- require "gamefic/node"
2
- require "gamefic/describable"
3
- require 'gamefic/messaging'
1
+ # frozen_string_literal: true
4
2
 
5
3
  module Gamefic
6
- # A physical object that can exist in a plot. Most objects with which
7
- # players interact are entities. Player characters themselves typically
8
- # derive from entities, e.g., the Gamefic::Actor class.
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 < Element
9
+ class Entity
10
+ include Describable
11
11
  include Node
12
- include Messaging
12
+ # include Messaging
13
13
 
14
- # Set the Entity's parent.
15
- #
16
- # @param node [Gamefic::Entity, nil] The new parent.
17
- def parent=(node)
18
- if node && node.is_a?(Entity) == false
19
- raise ArgumentError, "Entity's parent must be an Entity"
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
- super
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