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 +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
|