gamefic 2.3.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +41 -0
  3. data/.rspec-opal +2 -0
  4. data/.rubocop.yml +4 -1
  5. data/.solargraph.yml +20 -3
  6. data/CHANGELOG.md +15 -0
  7. data/Gemfile +0 -4
  8. data/Rakefile +11 -1
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/gamefic.gemspec +5 -2
  12. data/lib/gamefic/action.rb +53 -173
  13. data/lib/gamefic/active/cue.rb +25 -0
  14. data/lib/gamefic/active/epic.rb +68 -0
  15. data/lib/gamefic/active/messaging.rb +43 -0
  16. data/lib/gamefic/active/take.rb +69 -0
  17. data/lib/gamefic/active.rb +97 -192
  18. data/lib/gamefic/actor.rb +2 -0
  19. data/lib/gamefic/block.rb +28 -0
  20. data/lib/gamefic/command.rb +16 -6
  21. data/lib/gamefic/core_ext/array.rb +4 -4
  22. data/lib/gamefic/core_ext/string.rb +10 -5
  23. data/lib/gamefic/describable.rb +39 -65
  24. data/lib/gamefic/dispatcher.rb +67 -29
  25. data/lib/gamefic/entity.rb +44 -19
  26. data/lib/gamefic/logging.rb +32 -0
  27. data/lib/gamefic/messenger.rb +66 -0
  28. data/lib/gamefic/narrative.rb +104 -0
  29. data/lib/gamefic/node.rb +44 -53
  30. data/lib/gamefic/plot.rb +60 -93
  31. data/lib/gamefic/props/default.rb +41 -0
  32. data/lib/gamefic/props/multiple_choice.rb +65 -0
  33. data/lib/gamefic/props/pause.rb +11 -0
  34. data/lib/gamefic/props/yes_or_no.rb +21 -0
  35. data/lib/gamefic/props.rb +10 -0
  36. data/lib/gamefic/query/base.rb +45 -126
  37. data/lib/gamefic/query/general.rb +46 -0
  38. data/lib/gamefic/query/result.rb +20 -0
  39. data/lib/gamefic/query/scoped.rb +41 -0
  40. data/lib/gamefic/query/text.rb +30 -31
  41. data/lib/gamefic/query.rb +7 -15
  42. data/lib/gamefic/response.rb +118 -0
  43. data/lib/gamefic/rulebook/calls.rb +90 -0
  44. data/lib/gamefic/rulebook/events.rb +79 -0
  45. data/lib/gamefic/rulebook/hooks.rb +57 -0
  46. data/lib/gamefic/rulebook/scenes.rb +68 -0
  47. data/lib/gamefic/rulebook.rb +139 -0
  48. data/lib/gamefic/scanner.rb +103 -0
  49. data/lib/gamefic/scene/activity.rb +9 -17
  50. data/lib/gamefic/scene/conclusion.rb +6 -5
  51. data/lib/gamefic/scene/default.rb +88 -0
  52. data/lib/gamefic/scene/multiple_choice.rb +14 -69
  53. data/lib/gamefic/scene/pause.rb +9 -13
  54. data/lib/gamefic/scene/yes_or_no.rb +6 -46
  55. data/lib/gamefic/scene.rb +11 -7
  56. data/lib/gamefic/scope/base.rb +44 -0
  57. data/lib/gamefic/scope/children.rb +16 -0
  58. data/lib/gamefic/scope/family.rb +20 -0
  59. data/lib/gamefic/scope/myself.rb +13 -0
  60. data/lib/gamefic/scope/parent.rb +13 -0
  61. data/lib/gamefic/scope/siblings.rb +14 -0
  62. data/lib/gamefic/scope.rb +8 -0
  63. data/lib/gamefic/scriptable/actions.rb +156 -0
  64. data/lib/gamefic/scriptable/entities.rb +76 -0
  65. data/lib/gamefic/scriptable/events.rb +65 -0
  66. data/lib/gamefic/scriptable/proxy.rb +55 -0
  67. data/lib/gamefic/scriptable/queries.rb +73 -0
  68. data/lib/gamefic/scriptable/scenes.rb +162 -0
  69. data/lib/gamefic/scriptable.rb +167 -68
  70. data/lib/gamefic/snapshot.rb +36 -0
  71. data/lib/gamefic/stage.rb +51 -0
  72. data/lib/gamefic/subplot.rb +51 -79
  73. data/lib/gamefic/syntax/template.rb +67 -0
  74. data/lib/gamefic/syntax.rb +102 -83
  75. data/lib/gamefic/vault.rb +50 -0
  76. data/lib/gamefic/version.rb +3 -1
  77. data/lib/gamefic.rb +26 -15
  78. data/spec-opal/spec_helper.rb +24 -0
  79. metadata +92 -29
  80. data/lib/gamefic/element.rb +0 -46
  81. data/lib/gamefic/keywords.rb +0 -52
  82. data/lib/gamefic/messaging.rb +0 -43
  83. data/lib/gamefic/plot/darkroom.rb +0 -120
  84. data/lib/gamefic/plot/host.rb +0 -42
  85. data/lib/gamefic/plot/snapshot.rb +0 -27
  86. data/lib/gamefic/query/children.rb +0 -9
  87. data/lib/gamefic/query/descendants.rb +0 -15
  88. data/lib/gamefic/query/external.rb +0 -39
  89. data/lib/gamefic/query/family.rb +0 -18
  90. data/lib/gamefic/query/itself.rb +0 -13
  91. data/lib/gamefic/query/matches.rb +0 -75
  92. data/lib/gamefic/query/parent.rb +0 -9
  93. data/lib/gamefic/query/siblings.rb +0 -13
  94. data/lib/gamefic/query/tree.rb +0 -17
  95. data/lib/gamefic/scene/base.rb +0 -142
  96. data/lib/gamefic/scene/multiple_scene.rb +0 -29
  97. data/lib/gamefic/serialize.rb +0 -196
  98. data/lib/gamefic/world/callbacks.rb +0 -135
  99. data/lib/gamefic/world/commands.rb +0 -173
  100. data/lib/gamefic/world/entities.rb +0 -98
  101. data/lib/gamefic/world/playbook.rb +0 -225
  102. data/lib/gamefic/world/players.rb +0 -37
  103. data/lib/gamefic/world/scenes.rb +0 -226
  104. 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
@@ -1,45 +1,63 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gamefic
2
4
  # The action selector for character commands.
3
5
  #
4
6
  class Dispatcher
5
7
  # @param actor [Actor]
6
8
  # @param commands [Array<Command>]
7
- # @param actions [Array<Action>]
8
- def initialize actor, commands = [], actions = []
9
+ # @param responses [Array<Response>]
10
+ def initialize actor, commands = [], responses = []
9
11
  @actor = actor
10
12
  @commands = commands
11
- @actions = actions
12
- @started = false
13
+ @responses = responses
14
+ @executed = false
13
15
  end
14
16
 
15
- def merge dispatcher
16
- commands.concat dispatcher.commands
17
- 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
18
31
  end
19
32
 
20
- def next
21
- instance = nil
22
- while instance.nil? && !@actions.empty?
23
- action = actions.shift
33
+ # Get the next executable action.
34
+ #
35
+ # @return [Action, nil]
36
+ def proceed
37
+ while (response = responses.shift)
24
38
  commands.each do |cmd|
25
- instance = action.attempt(actor, cmd, !@started)
26
- if instance
27
- @started = true
28
- break
29
- end
39
+ action = response.attempt(actor, cmd)
40
+ next unless action && arguments_match?(action.arguments)
41
+
42
+ return action
30
43
  end
31
44
  end
32
- instance
45
+ nil # Without this, return value in Opal is undefined
33
46
  end
34
47
 
35
48
  # @param actor [Active]
36
- # @param command [String]
49
+ # @param input [String]
37
50
  # @return [Dispatcher]
38
- def self.dispatch actor, command
39
- group = actor.playbooks.reverse.map { |p| p.dispatch(actor, command) }
40
- dispatcher = Dispatcher.new(actor)
41
- group.each { |d| dispatcher.merge d }
42
- 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)
43
61
  end
44
62
 
45
63
  # @param actor [Active]
@@ -47,10 +65,13 @@ module Gamefic
47
65
  # @param params [Array<Object>]
48
66
  # @return [Dispatcher]
49
67
  def self.dispatch_from_params actor, verb, params
50
- group = actor.playbooks.reverse.map { |p| p.dispatch_from_params(actor, verb, params) }
51
- dispatcher = Dispatcher.new(actor)
52
- group.each { |d| dispatcher.merge d }
53
- 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)
54
75
  end
55
76
 
56
77
  protected
@@ -61,7 +82,24 @@ module Gamefic
61
82
  # @return [Array<Command>]
62
83
  attr_reader :commands
63
84
 
64
- # @return [Array<Action>]
65
- 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
66
104
  end
67
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