demiurge 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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