demiurge 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 16771a3630ea74f9412194ba68ec9ebc5181380c
4
- data.tar.gz: 7bd716fe644a7be1e32bd49ab0d39d4cc66a0e97
3
+ metadata.gz: 499186009cfdb7fdec206e4f4d4e900629f4dea0
4
+ data.tar.gz: 299ca77666a7b07d559f11f15bbe53e812318550
5
5
  SHA512:
6
- metadata.gz: b10caf567ea6f50864c594e22ac16708407e325ec72a543ec403a7439ede2c512c82f6bb383dc019ba343cd71dbd131fdc87b99a4a5108be7e3c7dc2fd6bac62
7
- data.tar.gz: 4ec6595a79d26d53fb40c04725820239b434195b3979583f589a0615d07339cf104680b62cb5669bdb3851b2588f560a9d7b3f39a3fa6f97d152eac4a33c6226
6
+ metadata.gz: e47ef0f89c8ee90df35d167ff802e38977b73fd533eaad911e742bb54dcda86896cfffd05ee5a22cfeca543c5ff131bd269e13291f309b8f11ee97fbbb27df45
7
+ data.tar.gz: f397a906555572b4e34369edd098ca9b9e9f3eafc4a35c0bf6e009e8e60c56b1b1b70d3035f8ad346d341d09384ae4ced796131678c78968bfd37221c6563877
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ demiurge-*.gem
data/Gemfile CHANGED
@@ -1,4 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
+ # We're using new experimental TMX features at the moment - use the gem from Git
4
+ gem "tmx", :git => "git@github.com:shawn42/tmx.git"
5
+
3
6
  # Specify your gem's dependencies in demiurge.gemspec
4
7
  gemspec
@@ -45,7 +45,7 @@ with this example, but you'll need to change it later.
45
45
  Here's an example file containing a very simple zone for use with
46
46
  Demiurge-CreateJS:
47
47
 
48
- zone "trackless island", "type" => "TmxZone" do
48
+ zone "trackless island" do
49
49
  tmx_location "start location" do
50
50
  manasource_tile_layout "tmx/trackless_island_main.tmx"
51
51
  description "A Mysterious Island"
@@ -16,6 +16,7 @@ require "demiurge/zone"
16
16
  require "demiurge/location"
17
17
  require "demiurge/agent"
18
18
  require "demiurge/dsl"
19
+ require "demiurge/tmx"
19
20
 
20
21
  require "multi_json"
21
22
 
@@ -43,6 +44,7 @@ module Demiurge
43
44
  attr_reader :ticks
44
45
 
45
46
  # @return [Hash{String=>String}] The current execution context for notifications and logging.
47
+ # @since 0.3.0
46
48
  attr_reader :execution_context
47
49
 
48
50
  # This is the constructor for a new Engine object. Most frequently
@@ -361,7 +363,7 @@ module Demiurge
361
363
  # @yield Evaluate the following block with the given context set
362
364
  # @api private
363
365
  # @return [void]
364
- # @since 0.2.0
366
+ # @since 0.3.0
365
367
  def push_context(context)
366
368
  @execution_context.push(context)
367
369
  yield
@@ -326,7 +326,7 @@ module Demiurge
326
326
  # @return [String, Integer, Integer] The location string, the X coordinate and the Y coordinate
327
327
  # @since 0.0.1
328
328
  def position_to_location_and_tile_coords(position)
329
- ::Demiurge::TmxLocation.position_to_loc_coords(position)
329
+ ::Demiurge::TiledLocation.position_to_loc_coords(position)
330
330
  end
331
331
 
332
332
  # Cancel the current intention. Raise a NoCurrentIntentionError if there isn't one.
@@ -362,13 +362,14 @@ module Demiurge
362
362
  # walking animation or anything. Just put it where it needs to be.
363
363
  #
364
364
  # @param position [String] The position to move to
365
+ # @param options [Hash] A Hash of *how* the item moves there; this can be checked by your World Files or display library, though Demiurge won't use it directly.
365
366
  # @return [void]
366
367
  # @since 0.0.1
367
- def move_to_instant(position)
368
+ def move_to_instant(position, options = {})
368
369
  # TODO: We don't have a great way to do this for non-agent entities. How does "accomodate" work for non-agents?
369
370
  # This may be app-specific.
370
371
 
371
- loc_name, next_x, next_y = TmxLocation.position_to_loc_coords(position)
372
+ loc_name, next_x, next_y = TiledLocation.position_to_loc_coords(position)
372
373
  location = @item.engine.item_by_name(loc_name)
373
374
  if !location
374
375
  cancel_intention_if_present "Location #{loc_name.inspect} doesn't exist.", "position" => position, "mover" => @item.name
@@ -43,9 +43,10 @@ module Demiurge
43
43
  # happen, has happened already.
44
44
  #
45
45
  # @param pos [String] A position string to move to
46
+ # @param options [Hash] A hash of how to do the movement; Demiurge doesn't internally use this hash, but your World Files or display library may do so
46
47
  # @return [void]
47
48
  # @since 0.0.1
48
- def move_to_position(pos)
49
+ def move_to_position(pos, options = {})
49
50
  old_pos = self.position
50
51
  old_loc = self.location_name
51
52
  old_zone_name = self.zone_name
@@ -64,7 +65,7 @@ module Demiurge
64
65
 
65
66
  @engine.send_notification({ old_position: old_pos, old_location: old_loc, new_position: self.position, new_location: new_loc },
66
67
  type: Demiurge::Notifications::MoveFrom, zone: old_zone_name, location: old_loc, actor: @name, include_context: true)
67
- @engine.send_notification({ old_position: old_pos, old_location: old_loc, new_position: self.position, new_location: new_loc },
68
+ @engine.send_notification({ old_position: old_pos, old_location: old_loc, new_position: self.position, new_location: new_loc, options: options },
68
69
  type: Demiurge::Notifications::MoveTo, zone: self.zone_name, location: self.location_name, actor: @name, include_context: true)
69
70
  end
70
71
 
@@ -323,7 +324,7 @@ module Demiurge
323
324
  agent.state["wander_counter"] += 1
324
325
  wander_every = agent.state["wander_every"] || 3
325
326
  return if agent.state["wander_counter"] < wander_every
326
- next_coords = agent.zone.adjacent_positions(agent.position)
327
+ next_coords = agent.location.adjacent_positions(agent.position)
327
328
  if next_coords.empty?
328
329
  @engine.admin_warning("Oh no! Wandering agent #{@name.inspect} is stuck and can't get out!",
329
330
  "zone" => agent.zone_name, "location" => agent.location_name, "agent" => @name)
@@ -331,7 +332,7 @@ module Demiurge
331
332
  end
332
333
  chosen = next_coords.sample
333
334
  pos = "#{agent.location_name}##{chosen.join(",")}"
334
- agent.move_to_position(pos)
335
+ agent.move_to_position(pos, { "method" => "wander" })
335
336
  agent.state["wander_counter"] = 0
336
337
  end
337
338
  end
@@ -24,6 +24,14 @@ module Demiurge
24
24
  state["contents"]
25
25
  end
26
26
 
27
+ # Gets the contents array as items, not names.
28
+ #
29
+ # @return [Array<Demiurge::StateItem>] The array of items contained in this item
30
+ # @since 0.3.0
31
+ def contents
32
+ state["contents"].map { |name| @engine.item_by_name(name) }
33
+ end
34
+
27
35
  # The finished_init hook is called after all items are loaded. For
28
36
  # containers, this makes sure all items set as contents of this
29
37
  # container also have it correctly set as their position.
@@ -1,5 +1,3 @@
1
- require_relative "../demiurge"
2
-
3
1
  class Demiurge::Engine
4
2
  # This method loads new World File code into an existing engine. It
5
3
  # should be passed a list of filenames, normally roughly the same
@@ -81,5 +81,161 @@ module Demiurge
81
81
  @state["exits"]
82
82
  end
83
83
 
84
+ # Returns an array of position strings for positions adjacent to
85
+ # the one given. In some areas this won't be meaningful. But for
86
+ # most "plain" areas, this gives possibilities of where is
87
+ # moveable for simple AIs.
88
+ #
89
+ # @return [Array<String>] Array of position strings
90
+ # @since 0.0.1
91
+ def adjacent_positions(pos, options = {})
92
+ @state["exits"].map { |e| e["to"] }
93
+ end
94
+ end
95
+
96
+ # A TiledLocation is a location that uses #x,y format for positions
97
+ # for a 2D grid in the location. Something like a TMX file defines a
98
+ # TiledLocation, but so does an infinitely generated tiled space.
99
+ #
100
+ # @since 0.3.0
101
+ class TiledLocation < Location
102
+ # Parse a tiled position string and return the X and Y tile coordinates
103
+ #
104
+ # @param pos [String] The position string to parse
105
+ # @return [Array<Integer,Integer>] The x, y coordinates
106
+ # @since 0.2.0
107
+ def self.position_to_coords(pos)
108
+ loc, x, y = position_to_loc_coords(pos)
109
+ return x, y
110
+ end
111
+
112
+ # Parse a tiled position string and return the location name and the X and Y tile coordinates
113
+ #
114
+ # @param pos [String] The position string to parse
115
+ # @return [Array<String,Integer,Integer>] The location name and x, y coordinates
116
+ # @since 0.2.0
117
+ def self.position_to_loc_coords(pos)
118
+ loc, coords = pos.split("#",2)
119
+ if coords
120
+ x, y = coords.split(",")
121
+ return loc, x.to_i, y.to_i
122
+ else
123
+ return loc, nil, nil
124
+ end
125
+ end
126
+
127
+ # When an item changes position in a TiledLocation, check if the
128
+ # new position leads out an exit. If so, send them where the exit
129
+ # leads instead.
130
+ #
131
+ # @param item [String] The item changing position
132
+ # @param old_pos [String] The position string the item is moving *from*
133
+ # @param new_pos [String] The position string the item is moving *to*
134
+ # @return [void]
135
+ # @since 0.2.0
136
+ def item_change_position(item, old_pos, new_pos)
137
+ exit = @state["exits"].detect { |e| e["from"] == new_pos }
138
+ return super unless exit # No exit? Do what you were going to.
139
+
140
+ # Going to hit an exit? Cancel this motion and enqueue an
141
+ # intention to do so? Or just send them through? If the former,
142
+ # it's very hard to unblockably pass through an exit, even if
143
+ # that's what's wanted. If the latter, it's very hard to make
144
+ # going through an exit blockable.
145
+
146
+ # Eh, just send them through for now. We'll figure out how to
147
+ # make detecting and blocking exit intentions easy later.
148
+
149
+ item_change_location(item, old_pos, exit["to"])
150
+ end
151
+
152
+ # This just determines if the position is valid at all. It does
153
+ # *not* check walkable/swimmable or even if it's big enough for a
154
+ # humanoid to stand in.
155
+ #
156
+ # @param pos [String] The position being checked
157
+ # @return [Boolean] Whether the position is valid
158
+ # @since 0.2.0
159
+ def valid_position?(pos)
160
+ return false unless pos[0...@name.size] == @name
161
+ return false unless pos[@name.size] == "#"
162
+ x, y = pos[(@name.size + 1)..-1].split(",", 2).map(&:to_i)
163
+ valid_coordinate?(x, y)
164
+ end
165
+
166
+ # Determine whether this position can accomodate the given agent's shape and size.
167
+ #
168
+ # @param agent [Demiurge::Agent] The agent being checked
169
+ # @param position [String] The position being checked
170
+ # @return [Boolean] Whether the position can accomodate the agent
171
+ # @since 0.2.0
172
+ def can_accomodate_agent?(agent, position)
173
+ loc, x, y = TiledLocation.position_to_loc_coords(position)
174
+ raise "Location #{@name.inspect} asked about different location #{loc.inspect} in can_accomodate_agent!" if loc != @name
175
+ shape = agent.state["shape"] || "humanoid"
176
+ can_accomodate_shape?(x, y, shape)
177
+ end
178
+
179
+ # Whether the coordinate is valid
180
+ #
181
+ # @param x [Integer] The X coordinate
182
+ # @param y [Integer] The Y coordinate
183
+ # @return [Boolean] Whether the coordinate is valid
184
+ # @since 0.3.0
185
+ def valid_coordinate?(x,y)
186
+ true
187
+ end
188
+
189
+ # Whether the location can accomodate an object of this size.
190
+ #
191
+ # @param left_x [Integer] The lower (leftmost) X coordinate
192
+ # @param upper_y [Integer] The higher (upper) Y coordinate
193
+ # @param width [Integer] How wide the checked object is
194
+ # @param height [Integer] How high the checked object is
195
+ # @return [Boolean] Whether the object can be accomodated
196
+ # @since 0.3.0
197
+ def can_accomodate_dimensions?(left_x, upper_y, width, height)
198
+ true
199
+ end
200
+
201
+ # Determine whether this coordinate location can accomodate an
202
+ # item of the given shape.
203
+ #
204
+ # For now, don't distinguish between walkable/swimmable or
205
+ # whatever, just say a collision value of 0 means valid,
206
+ # everything else is invalid.
207
+ #
208
+ # @param left_x [Integer] The lower (leftmost) X coordinate
209
+ # @param upper_y [Integer] The higher (upper) Y coordinate
210
+ # @return [Boolean] Whether the object can be accomodated
211
+ # @since 0.3.0
212
+ def can_accomodate_shape?(left_x, upper_y, shape)
213
+ case shape
214
+ when "humanoid"
215
+ return can_accomodate_dimensions?(left_x, upper_y, 2, 1)
216
+ when "tiny"
217
+ return can_accomodate_dimensions?(left_x, upper_y, 1, 1)
218
+ else
219
+ raise "Unknown shape #{shape.inspect} passed to can_accomodate_shape!"
220
+ end
221
+ end
222
+
223
+ # Return a legal position in this location
224
+ #
225
+ # @return [String] A legal position string
226
+ # @since 0.3.0
227
+ def any_legal_position
228
+ "#{@name}#0,0"
229
+ end
230
+
231
+ # Return the list of valid adjacent positions from this one
232
+ def adjacent_positions(pos, options = {})
233
+ location, pos_spec = pos.split("#", 2)
234
+ loc = @engine.item_by_name(location)
235
+ x, y = pos_spec.split(",").map(&:to_i)
236
+
237
+ shape = options[:shape] || "humanoid"
238
+ [[x - 1, y], [x + 1, y], [x, y - 1], [x, y + 1]].select { |xp, yp| loc.can_accomodate_shape?(xp, yp, shape) }
239
+ end
84
240
  end
85
241
  end
@@ -1,34 +1,52 @@
1
- require "demiurge/dsl"
2
-
3
- require "tmx"
4
-
5
- # TMX support here includes basic/normal TMX support for products of
6
- # the Tiled map editor (see "http://mapeditor.org" and
7
- # "http://docs.mapeditor.org/en/latest/reference/tmx-map-format/") and
8
- # more complex tiled map support for formats based on the ManaSource
9
- # game engine, including variants like Source of Tales, Land of Fire,
10
- # The Mana World and others. For more information on the ManaSource
11
- # mapping format, see "http://doc.manasource.org/mapping.html".
12
-
13
- # In general, Tiled and "raw" TMX try to be all things to all
14
- # games. If you can use a tile editor for it, Tiled would like to do
15
- # that for you. ManaSource is a more specialized engine and
16
- # introduces new concepts like named "Fringe" layers to make it clear
17
- # how a humanoid sprite walks through the map, named "Collision"
18
- # layers for walkability and swimmability, known-format "objects" for
19
- # things like doors, warps, NPCs, NPC waypoints and monster spawns.
20
- # Not all of that will be duplicated in Demiurge, but support for such
21
- # things belongs in the ManaSource-specific TMX parsing code.
22
-
23
- # In the long run, it's very likely that there will be other TMX
24
- # "dialects" like ManaSource's. Indeed, Demiurge might eventually
25
- # specify its own TMX dialect to support non-ManaSource features like
26
- # procedural map generation. My intention is to add them in the same
27
- # way - they may be requested in the Demiurge World files in the DSL,
28
- # and they will be an additional parsing pass on the result of "basic"
29
- # TMX parsing.
1
+ require "tmx" # Require the TMX gem
30
2
 
31
3
  module Demiurge
4
+
5
+ # Demiurge::Tmx is the module for Tmx internals. This includes Tmx
6
+ # parsing, caching and general encapsulation.
7
+ #
8
+ # TMX support here includes basic/normal TMX support for products of
9
+ # the Tiled map editor (see "http://mapeditor.org" and
10
+ # "http://docs.mapeditor.org/en/latest/reference/tmx-map-format/") and
11
+ # more complex tiled map support for formats based on the ManaSource
12
+ # game engine, including variants like Source of Tales, Land of Fire,
13
+ # The Mana World and others. For more information on the ManaSource
14
+ # mapping format, see "http://doc.manasource.org/mapping.html".
15
+ #
16
+ # In general, Tiled and "raw" TMX try to be all things to all
17
+ # games. If you can use a tile editor for it, Tiled would like to do
18
+ # that for you. ManaSource is a more specialized engine and
19
+ # introduces new concepts like named "Fringe" layers to make it clear
20
+ # how a humanoid sprite walks through the map, named "Collision"
21
+ # layers for walkability and swimmability, known-format "objects" for
22
+ # things like doors, warps, NPCs, NPC waypoints and monster spawns.
23
+ # Not all of that will be duplicated in Demiurge, but support for such
24
+ # things belongs in the ManaSource-specific TMX parsing code.
25
+ #
26
+ # In the long run, it's very likely that there will be other TMX
27
+ # "dialects" like ManaSource's. Indeed, Demiurge might eventually
28
+ # specify its own TMX dialect to support non-ManaSource features like
29
+ # procedural map generation. My intention is to add them in the same
30
+ # way - they may be requested in the Demiurge World files in the DSL,
31
+ # and they will be an additional parsing pass on the result of "basic"
32
+ # TMX parsing.
33
+ #
34
+ # @todo This entire file feels like a plugin waiting to
35
+ # happen. Putting it into Demiurge and monkeypatching seems slightly
36
+ # weird and "off". There's nothing wrong with having TiledLocation
37
+ # in Demiurge, and we do. But TMX, specifically, has a lot of
38
+ # format-specific stuff that doesn't seem to belong in core
39
+ # Demiurge. That's part of why it's off by itself in a separate
40
+ # file. If we were to support a second tilemap format, that would
41
+ # definitely seem to belong in a plugin. It's not clear what makes
42
+ # TMX different other than it being the first supported map format.
43
+ # The only thing keeping this from being the "demiurge-tmx" gem is
44
+ # that I feel like the code is already diced too finely into gems
45
+ # for its current level of maturity.
46
+ #
47
+ # @since 0.3.0
48
+ module Tmx; end
49
+
32
50
  module DSL
33
51
  # Monkeypatch to allow tmx_location in World File zones.
34
52
  class ZoneBuilder
@@ -56,384 +74,361 @@ module Demiurge
56
74
 
57
75
  # Specify a TMX file as the tile layout, but assume relatively little about the TMX format.
58
76
  def tile_layout(tmx_spec)
59
- # Make sure this loads correctly, but use the cache for efficiency.
60
- TmxLocation.tile_cache_entry(nil, tmx_spec)
61
-
62
- @state["tile_layout"] = tmx_spec
77
+ @state["tile_layout_filename"] = tmx_spec
78
+ @state["tile_layout_type"] = "tmx"
63
79
  end
64
80
 
65
81
  # Specify a TMX file as the tile layout, and interpret it according to ManaSource TMX conventions.
66
82
  def manasource_tile_layout(tmx_spec)
67
- # Make sure this loads correctly, but use the cache for efficiency.
68
- TmxLocation.tile_cache_entry(tmx_spec, nil)
69
-
70
- @state["manasource_tile_layout"] = tmx_spec
83
+ @state["tile_layout_filename"] = tmx_spec
84
+ @state["tile_layout_type"] = "manasource"
71
85
  end
72
86
 
73
87
  # Validate built_item before returning it
74
88
  def built_item
75
- raise("A TMX location (name: #{@name.inspect}) must have a tile layout!") unless @state["tile_layout"] || @state["manasource_tile_layout"]
76
- super
77
- end
78
- end
79
- end
80
-
81
- # A TmxZone can extract things like exits and collision data from
82
- # tile structures parsed from TMX files.
83
- class TmxZone < TileZone
84
- # Let's resolve any exits through this zone. NOTE: cross-zone
85
- # exits may be slightly wonky because the other zone hasn't
86
- # necessarily performed its own finished_init yet.
87
- def finished_init
88
- super
89
- exits = []
90
- # Go through the contents looking for locations
91
- contents = state["contents"].map { |ln| @engine.item_by_name(ln) }
92
- contents.each do |location|
93
- # ManaSource locations often store exits as objects in an
94
- # object layer. They don't cope with multiple locations that
95
- # use the same TMX file since they identify the destination by
96
- # the TMX filename. In Demiurge, we don't let them cross zone
97
- # boundaries to avoid unexpected behavior in other folks'
98
- # zones.
99
- if location.is_a?(TmxLocation) && location.state["manasource_tile_layout"]
100
- location.tiles[:objects].select { |obj| obj[:type].downcase == "warp" }.each do |obj|
101
- next unless obj[:properties]
102
- dest_map_name = obj[:properties]["dest_map"]
103
- dest_location = contents.detect { |loc| loc.is_a?(TmxLocation) && loc.tiles[:tmx_name] == dest_map_name }
104
- if dest_location
105
- dest_position = "#{dest_location.name}##{obj[:properties]["dest_x"]},#{obj[:properties]["dest_y"]}"
106
- src_x_coord = obj[:x] / location.tiles[:spritesheet][:tilewidth]
107
- src_y_coord = obj[:y] / location.tiles[:spritesheet][:tileheight]
108
- src_position = "#{location.name}##{src_x_coord},#{src_y_coord}"
109
- raise("Exit destination position #{dest_position.inspect} loaded from TMX location #{location.name.inspect} (TMX: #{location.tiles[:filename]}) is not valid!") unless dest_location.valid_position?(dest_position)
110
- exits.push({ src_loc: location, src_pos: src_position, dest_pos: dest_position })
111
- else
112
- @engine.admin_warning "Unresolved TMX exit in #{location.name.inspect}: #{obj[:properties].inspect}!",
113
- "location" => location.name, "properties" => obj[:properties]
114
- end
115
- end
116
- end
117
- end
118
- exits.each do |exit|
119
- exit[:src_loc].add_exit(from: exit[:src_pos], to: exit[:dest_pos])
89
+ raise("A TMX location (name: #{@name.inspect}) must have a tile layout!") unless @state["tile_layout_filename"]
90
+ item = super
91
+ item.tile_cache_entry # Load the cache entry, make sure it works without error
92
+ item
120
93
  end
121
94
  end
122
-
123
- # Return the list of valid adjacent positions from this one
124
- def adjacent_positions(pos, options = {})
125
- location, pos_spec = pos.split("#", 2)
126
- loc = @engine.item_by_name(location)
127
- x, y = pos_spec.split(",").map(&:to_i)
128
-
129
- shape = options[:shape] || "humanoid"
130
- [[x - 1, y], [x + 1, y], [x, y - 1], [x, y + 1]].select { |xp, yp| loc.can_accomodate_shape?(xp, yp, shape) }
131
- end
132
95
  end
133
96
 
134
97
  # A TmxLocation is a special location that is attached to a tile
135
98
  # layout, a TMX file. This affects the size and shape of the room,
136
99
  # and how agents may travel through it. TmxLocations have X and Y
137
100
  # coordinates (grid coordinates) for their positions.
138
- class TmxLocation < Location
139
- # Parse a tiled position string and return the X and Y tile coordinates
140
- def self.position_to_coords(pos)
141
- loc, x, y = position_to_loc_coords(pos)
142
- return x, y
101
+ #
102
+ # @since 0.2.0
103
+ class Tmx::TmxLocation < TiledLocation
104
+ # Get the default tile cache for newly-created
105
+ # TmxLocations. Demiurge provides one by default which can be
106
+ # overridden.
107
+ #
108
+ # @return [Demiurge::Tmx::TileCache] The current default TileCache for newly-created TmxLocations
109
+ # @since 0.3.0
110
+ def self.default_cache
111
+ @default_cache ||= ::Demiurge::Tmx::TmxCache.new
112
+ @default_cache
143
113
  end
144
114
 
145
- # Parse a tiled position string and return the location name and the X and Y tile coordinates
146
- def self.position_to_loc_coords(pos)
147
- loc, coords = pos.split("#",2)
148
- if coords
149
- x, y = coords.split(",")
150
- return loc, x.to_i, y.to_i
151
- else
152
- return loc, nil, nil
153
- end
115
+ # Set the default cache for newly-created TmxLocations.
116
+ #
117
+ # @param cache [Demiurge::Tmx::TileCache] The tile cache for all subsequently-created TmxLocations
118
+ # @since 0.3.0
119
+ def self.set_default_cache(cache)
120
+ @default_cache = cache
154
121
  end
155
122
 
156
- # When an item changes position in a TmxLocation, check if the new
157
- # position leads out an exit. If so, send them where the exit
158
- # leads instead.
159
- def item_change_position(item, old_pos, new_pos)
160
- exit = @state["exits"].detect { |e| e["from"] == new_pos }
161
- return super unless exit # No exit? Do what you were going to.
123
+ # Set the tile cache for this specific TmxLocation
124
+ #
125
+ # @param cache [Demiurge::Tmx::TileCache]
126
+ # @since 0.3.0
127
+ def set_cache(cache)
128
+ @cache = cache
129
+ end
162
130
 
163
- # Going to hit an exit? Cancel this motion and enqueue an
164
- # intention to do so? Or just send them through? If the former,
165
- # it's very hard to unblockably pass through an exit, even if
166
- # that's what's wanted. If the latter, it's very hard to make
167
- # going through an exit blockable.
131
+ # Get the tile cache for this Tmxlocation
132
+ #
133
+ # @return [Demiurge::Tmx::TileCache]
134
+ # @since 0.3.0
135
+ def cache
136
+ @cache ||= self.class.default_cache
137
+ end
168
138
 
169
- # Eh, just send them through for now. We'll figure out how to
170
- # make detecting and blocking exit intentions easy later.
139
+ # Let's resolve any exits that go to other TMX locations.
140
+ #
141
+ # @since 0.3.0
142
+ def finished_init
143
+ super
144
+ exits = []
145
+ return unless @state["tile_layout_type"] == "manasource"
171
146
 
172
- item_change_location(item, old_pos, exit["to"])
173
- end
147
+ # Go through the contents looking for locations
148
+ zone_contents = self.zone.contents
149
+
150
+ # ManaSource locations often store exits as objects in an
151
+ # object layer. They don't cope with multiple locations that
152
+ # use the same TMX file since they identify the destination by
153
+ # the TMX filename. In Demiurge, we don't let them cross zone
154
+ # boundaries to avoid unexpected behavior in other folks'
155
+ # zones.
156
+ tile_cache_entry["objects"].select { |obj| obj["type"].downcase == "warp" }.each do |obj|
157
+ next unless obj["properties"]
158
+ dest_map_name = obj["properties"]["dest_map"]
159
+ dest_location = zone_contents.detect { |loc| loc.is_a?(::Demiurge::Tmx::TmxLocation) && loc.tile_cache_entry["tmx_name"] == dest_map_name }
160
+ if dest_location
161
+ entry = dest_location.tile_cache_entry
162
+ dest_position = "#{dest_location.name}##{obj["properties"]["dest_x"]},#{obj["properties"]["dest_y"]}"
163
+ src_x_coord = obj["x"] / tile_cache_entry["tilewidth"]
164
+ src_y_coord = obj["y"] / tile_cache_entry["tileheight"]
165
+ src_position = "#{name}##{src_x_coord},#{src_y_coord}"
166
+ raise("Exit destination position #{dest_position.inspect} loaded from TMX location #{name.inspect} (TMX: #{tile_cache_entry["filename"]}) is not valid!") unless dest_location.valid_position?(dest_position)
167
+ exits.push({ src_loc: self, src_pos: src_position, dest_pos: dest_position })
168
+ else
169
+ @engine.admin_warning "Unresolved TMX exit in #{name.inspect}: #{obj["properties"].inspect}!",
170
+ "location" => name, "properties" => obj["properties"]
171
+ end
172
+ end
174
173
 
175
- # This just determines if the position is valid at all. It does
176
- # *not* check walkable/swimmable or even if it's big enough for a
177
- # humanoid to stand in.
178
- def valid_position?(pos)
179
- return false unless pos[0...@name.size] == @name
180
- return false unless pos[@name.size] == "#"
181
- x, y = pos[(@name.size + 1)..-1].split(",", 2).map(&:to_i)
182
- valid_coordinate?(x, y)
174
+ exits.each do |exit|
175
+ exit[:src_loc].add_exit(from: exit[:src_pos], to: exit[:dest_pos])
176
+ end
183
177
  end
184
178
 
185
179
  # This checks the coordinate's validity, but not relative to any
186
180
  # specific person/item/whatever that could occupy the space.
181
+ #
182
+ # @return [Boolean] Whether the coordinate is valid
183
+ # @since 0.2.0
187
184
  def valid_coordinate?(x, y)
188
185
  return false if x < 0 || y < 0
189
- return false if x >= tiles[:spritestack][:width] || y >= tiles[:spritestack][:height]
190
- return true unless tiles[:collision]
191
- return tiles[:collision][y][x] == 0
192
- end
193
-
194
- # Determine whether this position can accomodate the given agent's shape and size.
195
- def can_accomodate_agent?(agent, position)
196
- loc, x, y = TmxLocation.position_to_loc_coords(position)
197
- raise "Location #{@name.inspect} asked about different location #{loc.inspect} in can_accomodate_agent!" if loc != @name
198
- shape = agent.state["shape"] || "humanoid"
199
- can_accomodate_shape?(x, y, shape)
186
+ return false if x >= tile_cache_entry["width"] || y >= tile_cache_entry["height"]
187
+ return true unless tile_cache_entry["collision"]
188
+ return tile_cache_entry["collision"][y * tile_cache_entry["width"] + x] == 0
200
189
  end
201
190
 
202
- # Determine whether this coordinate location can accomodate a
191
+ # Determine whether this coordinate location can accommodate a
203
192
  # rectangular item of the given coordinate dimensions.
193
+ #
194
+ # @since 0.2.0
204
195
  def can_accomodate_dimensions?(left_x, upper_y, width, height)
205
196
  return false if left_x < 0 || upper_y < 0
206
197
  right_x = left_x + width - 1
207
198
  lower_y = upper_y + height - 1
208
- return false if right_x >= tiles[:spritestack][:width] || lower_y >= tiles[:spritestack][:height]
209
- return true unless tiles[:collision]
199
+ return false if right_x >= tile_cache_entry["width"] || lower_y >= tile_cache_entry["height"]
200
+ return true unless tile_cache_entry["collision"]
210
201
  (left_x..right_x).each do |x|
211
202
  (upper_y..lower_y).each do |y|
212
- return false if tiles[:collision][y][x] != 0
203
+ return false if tile_cache_entry["collision"][y * tile_cache_entry["width"] + x] != 0
213
204
  end
214
205
  end
215
206
  return true
216
207
  end
217
208
 
218
- # Determine whether this coordinate location can accomodate an
219
- # item of the given shape.
220
- #
221
- # For now, don't distinguish between walkable/swimmable or
222
- # whatever, just say a collision value of 0 means valid,
223
- # everything else is invalid.
224
- #
225
- # TODO: figure out some configurable way to specify what tile
226
- # value means invalid for TMX maps with more complex collision
227
- # logic.
228
- def can_accomodate_shape?(left_x, upper_y, shape)
229
- case shape
230
- when "humanoid"
231
- return can_accomodate_dimensions?(left_x, upper_y, 2, 1)
232
- when "tiny"
233
- return can_accomodate_dimensions?(left_x, upper_y, 1, 1)
234
- else
235
- raise "Unknown shape #{shape.inspect} passed to can_accomodate_shape!"
236
- end
237
- end
238
-
239
209
  # For a TmxLocation's legal position, find somewhere not covered
240
210
  # as a collision on the collision map.
211
+ #
212
+ # @return [String] A legal position string within this location
213
+ # @since 0.2.0
241
214
  def any_legal_position
242
- loc_tiles = self.tiles
243
- if tiles[:collision]
215
+ entry = tile_cache_entry
216
+ if entry["collision"]
244
217
  # We have a collision layer? Fabulous. Scan upper-left to lower-right until we get something non-collidable.
245
- (0...tiles[:spritestack][:width]).each do |x|
246
- (0...tiles[:spritestack][:height]).each do |y|
247
- if tiles[:collision][y][x] == 0
218
+ (0...entry["width"]).each do |x|
219
+ (0...entry["height"]).each do |y|
220
+ if entry["collision"][y * tile_cache_entry["width"] + x] == 0
248
221
  # We found a walkable spot.
249
222
  return "#{@name}##{x},#{y}"
250
223
  end
251
224
  end
252
225
  end
253
- else
254
- # Is there a start location? If so, return it. Guaranteed good, right?
255
- start_loc = tiles[:objects].detect { |obj| obj[:name] == "start location" }
256
- if start_loc
257
- x = start_loc[:x] / tiles[:spritestack][:tilewidth]
258
- y = start_loc[:y] / tiles[:spritestack][:tileheight]
259
- return "#{@name}##{x},#{y}"
260
- end
261
- # If no start location and no collision data, is there a first location with coordinates?
262
- if tiles[:objects].first[:x]
263
- obj = tiles[:objects].first
264
- return "#{@name}##{x},#{y}"
265
- end
266
- # Screw it, just return the upper left corner.
267
- return "#{@name}#0,0"
226
+ # If we got here, there exists no walkable spot in the whole location.
268
227
  end
228
+ # Screw it, just return the upper left corner.
229
+ return "#{@name}#0,0"
269
230
  end
270
231
 
271
232
  # Return the tile object for this location
272
- def tiles
273
- raise("A TMX location (name: #{@name.inspect}) must have a tile layout!") unless state["tile_layout"] || state["manasource_tile_layout"]
274
- TmxLocation.tile_cache_entry(state["manasource_tile_layout"], state["tile_layout"])
233
+ def tile_cache_entry
234
+ raise("A TMX location (name: #{@name.inspect}) must have a tile layout!") unless state["tile_layout_filename"]
235
+ cache.tmx_entry(@state["tile_layout_type"], @state["tile_layout_filename"])
275
236
  end
276
237
 
277
238
  # Return a TMX object's structure, for an object of the given name, or nil.
278
239
  def tmx_object_by_name(name)
279
- tiles[:objects].detect { |o| o[:name] == name }
240
+ tile_cache_entry["objects"].detect { |o| o["name"] == name }
280
241
  end
281
242
 
282
243
  # Return the tile coordinates of the TMX object with the given name, or nil.
283
244
  def tmx_object_coords_by_name(name)
284
- obj = tiles[:objects].detect { |o| o[:name] == name }
285
- if obj
286
- [ obj[:x] / tiles[:spritesheet][:tilewidth], obj[:y] / tiles[:spritesheet][:tileheight] ]
287
- else
288
- nil
289
- end
245
+ obj = tmx_object_by_name(name)
246
+ return nil unless obj
247
+ [ obj["x"] / tile_cache_entry["tilewidth"], obj["y"] / tile_cache_entry["tileheight"] ]
290
248
  end
291
249
 
292
- # Technically a StateItem of any kind has to be okay with its
293
- # state changing totally out from under it at any time. One way
294
- # around that for TMX is a tile cache to parse new entries and
295
- # re-return old ones. This means if a file is changed at runtime,
296
- # its cache entry won't be reloaded.
297
- def self.tile_cache_entry(state_manasource_tile_layout, state_tile_layout)
298
- smtl = state_manasource_tile_layout
299
- stl = state_tile_layout
300
- @tile_cache ||= {
301
- "manasource_tile_layout" => {},
302
- "tile_layout" => {},
303
- }
304
- if smtl
305
- @tile_cache["manasource_tile_layout"][smtl] ||= Demiurge.sprites_from_manasource_tmx(smtl)
306
- elsif stl
307
- @tile_cache["tile_layout"][stl] ||= Demiurge.sprites_from_tmx(stl)
308
- else
309
- raise "A TMX location must have some kind of tile layout!"
310
- end
311
- end
312
250
  end
313
251
  end
314
252
 
315
- Demiurge::DSL::TopLevelBuilder.register_type "TmxZone", Demiurge::TmxZone
316
- Demiurge::DSL::TopLevelBuilder.register_type "TmxLocation", Demiurge::TmxLocation
253
+ Demiurge::DSL::TopLevelBuilder.register_type "TmxLocation", Demiurge::Tmx::TmxLocation
254
+
255
+ module Demiurge::Tmx
256
+ # A TmxCache loads and remembers TMX file maps from the tmx gem. For
257
+ # a variety of reasons, it's not great to reload TMX files every
258
+ # time we need to know about them, but it can also be a problem to
259
+ # store a copy of the parsed version every time and place we use it.
260
+ # Caching is, of course, a time-honored solution to this problem.
261
+ #
262
+ # @since 0.3.0
263
+ class TmxCache
264
+ # @return [String] Root directory the cache was created relative to
265
+ attr_reader :root_dir
266
+
267
+ # Create the TmxCache
268
+ #
269
+ # @param options [Hash] Options
270
+ # @option options [String] :root_dir The root directory to read TMX and TSX files relative to
271
+ # @return [void]
272
+ # @since 0.3.0
273
+ def initialize(options = {})
274
+ @root_dir = options[:root_dir] || Dir.pwd
275
+ end
317
276
 
318
- module Demiurge
319
- # This is to support TMX files for ManaSource, ManaWorld, Land of
320
- # Fire, Source of Tales and other Mana Project games. It can't be
321
- # perfect since there's some variation between them, but it can
322
- # follow most major conventions.
323
-
324
- # Load a TMX file and calculate the objects inside including the
325
- # Spritesheet and Spritestack. Assume this TMX file obeys ManaSource
326
- # conventions such as fields for exits and names for layers.
327
- def self.sprites_from_manasource_tmx(filename)
328
- objs = sprites_from_tmx filename
329
- stack = objs[:spritestack]
330
-
331
- stack_layers = stack[:layers]
332
-
333
- # Remove the collision layer, add as separate collision top-level entry
334
- collision_index = stack_layers.index { |l| l[:name].downcase == "collision" }
335
- collision_layer = stack_layers.delete_at collision_index if collision_index
336
-
337
- # Some games make this true/false, others have separate visibility
338
- # or swimmability in it. In general, we'll just expose the data.
339
- objs[:collision] = collision_layer[:data] if collision_layer
340
-
341
- # Remove the heights layer, add as separate heights top-level entry
342
- heights_index = stack_layers.index { |l| ["height", "heights"].include?(l[:name].downcase) }
343
- heights_layer = stack_layers.delete_at heights_index if heights_index
344
- objs[:heights] = heights_layer
345
-
346
- fringe_index = stack_layers.index { |l| l[:name].downcase == "fringe" }
347
- raise ::Demiurge::Errors::TmxFormatError.new("No Fringe layer found in ManaSource TMX File #{filename.inspect}!", "filename" => filename) unless fringe_index
348
- stack_layers.each_with_index do |layer, index|
349
- # Assign a Z value based on layer depth, with fringe = 0 as a special case
350
- layer["z"] = (index - fringe_index) * 10.0
277
+ # Clear the TMX cache. Remove all existing entries. This can
278
+ # potentially break existing TMX-based objects. Objects often
279
+ # refer repeatedly back into the cache to avoid duplicating
280
+ # structures, and to pick up new changes to those structures.
281
+ #
282
+ # @return [void]
283
+ # @since 0.3.0
284
+ def clear_cache
285
+ @tile_cache = {}
286
+ nil
351
287
  end
352
288
 
353
- objs
354
- end
289
+ # Change the root directory this cache uses. Note that any
290
+ # existing TMX files with a different root would presumably
291
+ # change, so this clears the cache as a side effect.
292
+ #
293
+ # @param new_root [String] The new directory to use as TMX root for this cache
294
+ # @return [void]
295
+ # @since 0.3.0
296
+ def root_dir=(new_root)
297
+ clear_cache
298
+ @root_dir = new_root
299
+ nil
300
+ end
301
+
302
+ # Return a TMX entry for the given layout type and filename. A
303
+ # "layout type" is something like normal TMX (called "tmx") or
304
+ # Manasource-style (called "manasource".) But additional types are
305
+ # likely to be added in future versions of Pixiurge.
306
+ #
307
+ # @param layout_type [String] The subtype of TMX file; currently may be one of "tmx" or "manasource"
308
+ # @param layout_filename [String] The path to the TMX file relative to the tile cache's TMX root directory
309
+ # @return [Hash] The TMX cache entry, in an internal and potentially changeable format
310
+ # @api private
311
+ # @since 0.3.0
312
+ def tmx_entry(layout_type, layout_filename)
313
+ @tile_cache ||= {}
314
+ @tile_cache[layout_type] ||= {}
315
+ if @tile_cache[layout_type][layout_filename]
316
+ return @tile_cache[layout_type][layout_filename]
317
+ end
355
318
 
356
- # Load a TMX file and calculate the objects inside including the
357
- # Spritesheet and Spritestack. Do not assume this TMX file obeys any
358
- # particular additional conventions beyond basic TMX format.
359
- def self.sprites_from_tmx(filename)
360
- spritesheet = {}
361
- spritestack = {}
362
-
363
- # This recursively loads things like tileset .tsx files
364
- tiles = Tmx.load filename
365
-
366
- spritestack[:name] = tiles.name || File.basename(filename).split(".")[0]
367
- spritestack[:width] = tiles.width
368
- spritestack[:height] = tiles.height
369
- spritestack[:properties] = tiles.properties
370
-
371
- spritesheet[:tilewidth] = tiles.tilewidth
372
- spritesheet[:tileheight] = tiles.tileheight
373
-
374
- spritesheet[:images] = tiles.tilesets.map do |tileset|
375
- {
376
- firstgid: tileset.firstgid,
377
- tileset_name: tileset.name,
378
- image: tileset.image,
379
- imagewidth: tileset.imagewidth,
380
- imageheight: tileset.imageheight,
381
- tilewidth: tileset.tilewidth,
382
- tileheight: tileset.tileheight,
383
- oversize: tileset.tilewidth != tiles.tilewidth || tileset.tileheight != tiles.tileheight,
384
- spacing: tileset.spacing,
385
- margin: tileset.margin,
386
- imagetrans: tileset.imagetrans, # Currently unused, color to treat as transparent
387
- properties: tileset.properties,
388
- }
319
+ if layout_type == "manasource"
320
+ @tile_cache[layout_type][layout_filename] = sprites_from_manasource_tmx(layout_filename)
321
+ elsif layout_type == "tmx"
322
+ @tile_cache[layout_type][layout_filename] = sprites_from_tmx(layout_filename)
323
+ else
324
+ raise "A TMX location must have a known type of layout (tmx or manasource), not #{layout_type.inspect}!"
325
+ end
326
+
327
+ @tile_cache[layout_type][layout_filename]
389
328
  end
390
- spritesheet[:cyclic_animations] = animations_from_tilesets tiles.tilesets
391
-
392
- spritesheet[:properties] = spritesheet[:images].map { |i| i[:properties] }.inject({}, &:merge)
393
- spritesheet[:name] = spritesheet[:images].map { |i| i[:tileset_name] }.join("/")
394
- spritestack[:spritesheet] = spritesheet[:name]
395
-
396
- spritestack[:layers] = tiles.layers.map do |layer|
397
- data = layer.data.each_slice(layer.width).to_a
398
- {
399
- name: layer.name,
400
- data: data,
401
- visible: layer.visible,
402
- opacity: layer.opacity,
403
- offsetx: layer.offsetx, # Currently unused
404
- offsety: layer.offsety, # Currently unused
405
- properties: layer.properties,
406
- }
329
+
330
+ private
331
+
332
+ # This is to support TMX files for ManaSource, ManaWorld, Land of
333
+ # Fire, Source of Tales and other Mana Project games. It can't be
334
+ # perfect since there's some variation between them, but it can
335
+ # follow most major conventions.
336
+ #
337
+ # Load a TMX file and calculate additional information like
338
+ # animations from the given properties. Assume this TMX file obeys
339
+ # ManaSource conventions such as fields for exits and names for
340
+ # layers.
341
+ #
342
+ # @since 0.1.0
343
+ def sprites_from_manasource_tmx(filename)
344
+ entry = sprites_from_tmx filename
345
+
346
+ stack_layers = entry["map"]["layers"].select { |layer| layer["type"] == "tilelayer" }
347
+
348
+ # Remove the collision layer, add as separate collision top-level entry
349
+ collision_index = stack_layers.index { |l| l["name"].downcase == "collision" }
350
+ collision_layer = stack_layers.delete_at collision_index if collision_index
351
+
352
+ # Some games make this true/false, others have separate visibility
353
+ # or swimmability in it. In general, we'll just expose the data.
354
+ entry["collision"] = collision_layer["data"] if collision_layer
355
+
356
+ # Remove the heights layer, add as separate heights top-level entry
357
+ heights_index = stack_layers.index { |l| ["height", "heights"].include?(l["name"].downcase) }
358
+ heights_layer = stack_layers.delete_at heights_index if heights_index
359
+ entry["heights"] = heights_layer
360
+
361
+ fringe_index = stack_layers.index { |l| l["name"].downcase == "fringe" }
362
+ raise ::Demiurge::Errors::TmxFormatError.new("No Fringe layer found in ManaSource TMX File #{filename.inspect}!", "filename" => filename) unless fringe_index
363
+ stack_layers.each_with_index do |layer, index|
364
+ # Assign a Z value based on layer depth, with fringe = 0 as a special case
365
+ layer["z"] = (index - fringe_index) * 10.0
366
+ end
367
+
368
+ entry
407
369
  end
408
370
 
409
- objects = tiles.object_groups.flat_map { |og| og.objects.to_a }.map(&:to_h)
371
+ # Load a TMX file and JSONify it. This includes its various
372
+ # tilesets, such as embedded tilesets and TSX files. Do not
373
+ # assume this TMX file obeys any particular additional conventions
374
+ # beyond basic TMX format.
375
+ #
376
+ # @since 0.1.0
377
+ def sprites_from_tmx(filename)
378
+ cache_entry = {}
379
+
380
+ # This recursively loads things like tileset .tsx files, so we
381
+ # change to the root dir.
382
+ Dir.chdir(@root_dir) do
383
+ tmx_map = Tmx.load filename
384
+ filename.sub!(/\.tmx\Z/, ".json")
385
+ cache_entry["map"] = MultiJson.load(tmx_map.export_to_string(:filename => filename, :format => :json))
386
+ end
410
387
 
411
- { filename: filename, tmx_name: File.basename(filename).split(".")[0], spritesheet: spritesheet, spritestack: spritestack, objects: objects }
412
- end
388
+ tiles = cache_entry["map"]
389
+ cache_entry["tilesets"] = tiles["tilesets"]
413
390
 
414
- # Find the animations included in the TMX file
415
- def self.animations_from_tilesets tilesets
416
- tilesets.flat_map do |tileset|
417
- (tileset.tiles || []).map do |tile|
418
- p = tile["properties"]
419
- if p && p["animation-frame0"]
420
- section = 0
421
- anim = []
422
-
423
- while p["animation-frame#{section}"]
424
- section_hash = {
425
- frame: p["animation-frame#{section}"].to_i + tileset[:firstgid],
426
- duration: p["animation-delay#{section}"].to_i
427
- }
428
- anim.push section_hash
429
- section += 1
391
+ # Add entries to the top-level cache entry, but not inside the "map" structure
392
+ cache_entry["tmx_name"] = File.basename(filename).split(".")[0]
393
+ cache_entry["name"] = tiles["name"] || cache_entry["tmx_name"]
394
+ cache_entry["filename"] = filename
395
+ cache_entry["dir"] = filename.split("/")[0..-2].join(".")
396
+ cache_entry["animations"] = animations_from_tilesets cache_entry["tilesets"]
397
+ cache_entry["objects"] = tiles["layers"].flat_map { |layer| layer["objects"] || [] }
398
+
399
+ # Copy most-used properties into top-level cache entry
400
+ ["width", "height", "tilewidth", "tileheight"].each do |property|
401
+ cache_entry[property] = tiles[property]
402
+ end
403
+
404
+ cache_entry
405
+ end
406
+
407
+ # Extract the animations included in the TMX file
408
+ #
409
+ # @since 0.1.0
410
+ def animations_from_tilesets tilesets
411
+ tilesets.flat_map do |tileset|
412
+ (tileset["tiles"] || []).map do |tile|
413
+ p = tile["properties"]
414
+ if p && p["animation-frame0"]
415
+ section = 0
416
+ anim = []
417
+
418
+ while p["animation-frame#{section}"]
419
+ section_hash = {
420
+ "frame" => p["animation-frame#{section}"].to_i + tileset[:firstgid],
421
+ "duration" => p["animation-delay#{section}"].to_i
422
+ }
423
+ anim.push section_hash
424
+ section += 1
425
+ end
426
+ { "tile_anim_#{tile["id"].to_i + tileset[:firstgid]}" => anim }
427
+ else
428
+ nil
430
429
  end
431
- { "tile_anim_#{tile["id"].to_i + tileset[:firstgid]}".to_sym => anim }
432
- else
433
- nil
434
- end
435
- end.compact
436
- end.inject({}, &:merge)
430
+ end.compact
431
+ end.inject({}, &:merge)
432
+ end
437
433
  end
438
-
439
434
  end
@@ -1,4 +1,4 @@
1
1
  module Demiurge
2
2
  # Current version of Demiurge
3
- VERSION = "0.2.0"
3
+ VERSION = "0.4.0"
4
4
  end
@@ -74,35 +74,5 @@ module Demiurge
74
74
  end
75
75
  intentions
76
76
  end
77
-
78
- # Returns an array of position strings for positions adjacent to
79
- # the one given. In some Zones this won't be meaningful. But for
80
- # most "plain" zones, this gives possibilities of where is
81
- # moveable for simple AIs.
82
- #
83
- # @return [Array<String>] Array of position strings
84
- # @since 0.0.1
85
- def adjacent_positions(pos, options = {})
86
- []
87
- end
88
- end
89
-
90
- # In a RoomZone, locations are simple: you are in a Location, just
91
- # one at once, and exits allow movement between them. This is
92
- # similar to old MUDs, as well as games that present a store or
93
- # conversation as a separate screen that takes over your interface.
94
- #
95
- # @since 0.0.1
96
- class RoomZone < Zone
97
- end
98
-
99
- # In a TileZone, each Location is permitted to contain a coordinate
100
- # grid of sub-locations. A given entity occupies one or more
101
- # sub-locations, but can't really be "in between" those
102
- # coordinates. Most frequently, objects take up a single
103
- # sub-location, or a small rectangle of them.
104
- #
105
- # @since 0.0.1
106
- class TileZone < Zone
107
77
  end
108
78
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: demiurge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Noah Gibbs
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-01-05 00:00:00.000000000 Z
11
+ date: 2018-01-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: multi_json