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 +4 -4
- data/.gitignore +1 -0
- data/Gemfile +3 -0
- data/WORLD_FILES.md +1 -1
- data/lib/demiurge.rb +3 -1
- data/lib/demiurge/action_item.rb +4 -3
- data/lib/demiurge/agent.rb +5 -4
- data/lib/demiurge/container.rb +8 -0
- data/lib/demiurge/dsl.rb +0 -2
- data/lib/demiurge/location.rb +156 -0
- data/lib/demiurge/tmx.rb +323 -328
- data/lib/demiurge/version.rb +1 -1
- data/lib/demiurge/zone.rb +0 -30
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 499186009cfdb7fdec206e4f4d4e900629f4dea0
|
4
|
+
data.tar.gz: 299ca77666a7b07d559f11f15bbe53e812318550
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e47ef0f89c8ee90df35d167ff802e38977b73fd533eaad911e742bb54dcda86896cfffd05ee5a22cfeca543c5ff131bd269e13291f309b8f11ee97fbbb27df45
|
7
|
+
data.tar.gz: f397a906555572b4e34369edd098ca9b9e9f3eafc4a35c0bf6e009e8e60c56b1b1b70d3035f8ad346d341d09384ae4ced796131678c78968bfd37221c6563877
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/WORLD_FILES.md
CHANGED
@@ -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"
|
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"
|
data/lib/demiurge.rb
CHANGED
@@ -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.
|
366
|
+
# @since 0.3.0
|
365
367
|
def push_context(context)
|
366
368
|
@execution_context.push(context)
|
367
369
|
yield
|
data/lib/demiurge/action_item.rb
CHANGED
@@ -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::
|
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 =
|
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
|
data/lib/demiurge/agent.rb
CHANGED
@@ -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.
|
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
|
data/lib/demiurge/container.rb
CHANGED
@@ -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.
|
data/lib/demiurge/dsl.rb
CHANGED
data/lib/demiurge/location.rb
CHANGED
@@ -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
|
data/lib/demiurge/tmx.rb
CHANGED
@@ -1,34 +1,52 @@
|
|
1
|
-
require "
|
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
|
-
|
60
|
-
|
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
|
-
|
68
|
-
|
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["
|
76
|
-
super
|
77
|
-
|
78
|
-
|
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
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
-
#
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
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
|
-
#
|
157
|
-
#
|
158
|
-
#
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
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
|
-
|
170
|
-
|
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
|
-
|
173
|
-
|
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
|
-
|
176
|
-
|
177
|
-
|
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 >=
|
190
|
-
return true unless
|
191
|
-
return
|
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
|
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 >=
|
209
|
-
return true unless
|
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
|
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
|
-
|
243
|
-
if
|
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...
|
246
|
-
(0...
|
247
|
-
if
|
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
|
-
|
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
|
273
|
-
raise("A TMX location (name: #{@name.inspect}) must have a tile layout!") unless state["
|
274
|
-
|
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
|
-
|
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 =
|
285
|
-
|
286
|
-
|
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 "
|
316
|
-
|
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
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
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
|
-
|
354
|
-
|
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
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
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
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
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
|
-
|
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
|
-
|
412
|
-
|
388
|
+
tiles = cache_entry["map"]
|
389
|
+
cache_entry["tilesets"] = tiles["tilesets"]
|
413
390
|
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
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
|
-
|
432
|
-
|
433
|
-
|
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
|
data/lib/demiurge/version.rb
CHANGED
data/lib/demiurge/zone.rb
CHANGED
@@ -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.
|
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-
|
11
|
+
date: 2018-01-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: multi_json
|