demiurge 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +5 -0
- data/.yardopts +5 -0
- data/AUTHORS.txt +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONCEPTS.md +271 -0
- data/Gemfile +4 -0
- data/HACKING.md +34 -0
- data/LICENSE.txt +21 -0
- data/README.md +181 -0
- data/RELOADING.md +94 -0
- data/Rakefile +10 -0
- data/SECURITY.md +103 -0
- data/WORLD_FILES.md +134 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/demiurge.gemspec +31 -0
- data/exe/demirun +16 -0
- data/lib/demiurge/action_item.rb +643 -0
- data/lib/demiurge/agent.rb +338 -0
- data/lib/demiurge/container.rb +194 -0
- data/lib/demiurge/dsl.rb +583 -0
- data/lib/demiurge/exception.rb +170 -0
- data/lib/demiurge/inert_state_item.rb +21 -0
- data/lib/demiurge/intention.rb +164 -0
- data/lib/demiurge/location.rb +85 -0
- data/lib/demiurge/notification_names.rb +93 -0
- data/lib/demiurge/tmx.rb +439 -0
- data/lib/demiurge/util.rb +67 -0
- data/lib/demiurge/version.rb +4 -0
- data/lib/demiurge/zone.rb +108 -0
- data/lib/demiurge.rb +812 -0
- metadata +165 -0
data/lib/demiurge/tmx.rb
ADDED
@@ -0,0 +1,439 @@
|
|
1
|
+
require "demiurge/dsl"
|
2
|
+
|
3
|
+
require "tmx"
|
4
|
+
|
5
|
+
# TMX support here includes basic/normal TMX support for products of
|
6
|
+
# the Tiled map editor (see "http://mapeditor.org" and
|
7
|
+
# "http://docs.mapeditor.org/en/latest/reference/tmx-map-format/") and
|
8
|
+
# more complex tiled map support for formats based on the ManaSource
|
9
|
+
# game engine, including variants like Source of Tales, Land of Fire,
|
10
|
+
# The Mana World and others. For more information on the ManaSource
|
11
|
+
# mapping format, see "http://doc.manasource.org/mapping.html".
|
12
|
+
|
13
|
+
# In general, Tiled and "raw" TMX try to be all things to all
|
14
|
+
# games. If you can use a tile editor for it, Tiled would like to do
|
15
|
+
# that for you. ManaSource is a more specialized engine and
|
16
|
+
# introduces new concepts like named "Fringe" layers to make it clear
|
17
|
+
# how a humanoid sprite walks through the map, named "Collision"
|
18
|
+
# layers for walkability and swimmability, known-format "objects" for
|
19
|
+
# things like doors, warps, NPCs, NPC waypoints and monster spawns.
|
20
|
+
# Not all of that will be duplicated in Demiurge, but support for such
|
21
|
+
# things belongs in the ManaSource-specific TMX parsing code.
|
22
|
+
|
23
|
+
# In the long run, it's very likely that there will be other TMX
|
24
|
+
# "dialects" like ManaSource's. Indeed, Demiurge might eventually
|
25
|
+
# specify its own TMX dialect to support non-ManaSource features like
|
26
|
+
# procedural map generation. My intention is to add them in the same
|
27
|
+
# way - they may be requested in the Demiurge World files in the DSL,
|
28
|
+
# and they will be an additional parsing pass on the result of "basic"
|
29
|
+
# TMX parsing.
|
30
|
+
|
31
|
+
module Demiurge
|
32
|
+
module DSL
|
33
|
+
# Monkeypatch to allow tmx_location in World File zones.
|
34
|
+
class ZoneBuilder
|
35
|
+
# This is currently an ugly monkeypatch to allow declaring a
|
36
|
+
# "tmx_location" separate from other kinds of declarable
|
37
|
+
# StateItems. This remains ugly until the plugin system catches up
|
38
|
+
# with the intrusiveness of what TMX needs to plug in (which isn't
|
39
|
+
# bad, but the plugin system barely exists.)
|
40
|
+
def tmx_location(name, options = {}, &block)
|
41
|
+
state = { "zone" => @name }.merge(options)
|
42
|
+
builder = TmxLocationBuilder.new(name, @engine, "type" => options["type"] || "TmxLocation", "state" => state)
|
43
|
+
builder.instance_eval(&block)
|
44
|
+
@built_item.state["contents"] << name
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Special builder for tmx_location blocks
|
50
|
+
class TmxLocationBuilder < LocationBuilder
|
51
|
+
# Constructor
|
52
|
+
def initialize(name, engine, options = {})
|
53
|
+
options["type"] ||= "TmxLocation"
|
54
|
+
super
|
55
|
+
end
|
56
|
+
|
57
|
+
# Specify a TMX file as the tile layout, but assume relatively little about the TMX format.
|
58
|
+
def tile_layout(tmx_spec)
|
59
|
+
# Make sure this loads correctly, but use the cache for efficiency.
|
60
|
+
TmxLocation.tile_cache_entry(nil, tmx_spec)
|
61
|
+
|
62
|
+
@state["tile_layout"] = tmx_spec
|
63
|
+
end
|
64
|
+
|
65
|
+
# Specify a TMX file as the tile layout, and interpret it according to ManaSource TMX conventions.
|
66
|
+
def manasource_tile_layout(tmx_spec)
|
67
|
+
# Make sure this loads correctly, but use the cache for efficiency.
|
68
|
+
TmxLocation.tile_cache_entry(tmx_spec, nil)
|
69
|
+
|
70
|
+
@state["manasource_tile_layout"] = tmx_spec
|
71
|
+
end
|
72
|
+
|
73
|
+
# Validate built_item before returning it
|
74
|
+
def built_item
|
75
|
+
raise("A TMX location (name: #{@name.inspect}) must have a tile layout!") unless @state["tile_layout"] || @state["manasource_tile_layout"]
|
76
|
+
super
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# A TmxZone can extract things like exits and collision data from
|
82
|
+
# tile structures parsed from TMX files.
|
83
|
+
class TmxZone < TileZone
|
84
|
+
# Let's resolve any exits through this zone. NOTE: cross-zone
|
85
|
+
# exits may be slightly wonky because the other zone hasn't
|
86
|
+
# necessarily performed its own finished_init yet.
|
87
|
+
def finished_init
|
88
|
+
super
|
89
|
+
exits = []
|
90
|
+
# Go through the contents looking for locations
|
91
|
+
contents = state["contents"].map { |ln| @engine.item_by_name(ln) }
|
92
|
+
contents.each do |location|
|
93
|
+
# ManaSource locations often store exits as objects in an
|
94
|
+
# object layer. They don't cope with multiple locations that
|
95
|
+
# use the same TMX file since they identify the destination by
|
96
|
+
# the TMX filename. In Demiurge, we don't let them cross zone
|
97
|
+
# boundaries to avoid unexpected behavior in other folks'
|
98
|
+
# zones.
|
99
|
+
if location.is_a?(TmxLocation) && location.state["manasource_tile_layout"]
|
100
|
+
location.tiles[:objects].select { |obj| obj[:type].downcase == "warp" }.each do |obj|
|
101
|
+
next unless obj[:properties]
|
102
|
+
dest_map_name = obj[:properties]["dest_map"]
|
103
|
+
dest_location = contents.detect { |loc| loc.is_a?(TmxLocation) && loc.tiles[:tmx_name] == dest_map_name }
|
104
|
+
if dest_location
|
105
|
+
dest_position = "#{dest_location.name}##{obj[:properties]["dest_x"]},#{obj[:properties]["dest_y"]}"
|
106
|
+
src_x_coord = obj[:x] / location.tiles[:spritesheet][:tilewidth]
|
107
|
+
src_y_coord = obj[:y] / location.tiles[:spritesheet][:tileheight]
|
108
|
+
src_position = "#{location.name}##{src_x_coord},#{src_y_coord}"
|
109
|
+
raise("Exit destination position #{dest_position.inspect} loaded from TMX location #{location.name.inspect} (TMX: #{location.tiles[:filename]}) is not valid!") unless dest_location.valid_position?(dest_position)
|
110
|
+
exits.push({ src_loc: location, src_pos: src_position, dest_pos: dest_position })
|
111
|
+
else
|
112
|
+
@engine.admin_warning "Unresolved TMX exit in #{location.name.inspect}: #{obj[:properties].inspect}!",
|
113
|
+
"location" => location.name, "properties" => obj[:properties]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
exits.each do |exit|
|
119
|
+
exit[:src_loc].add_exit(from: exit[:src_pos], to: exit[:dest_pos])
|
120
|
+
end
|
121
|
+
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
|
+
end
|
133
|
+
|
134
|
+
# A TmxLocation is a special location that is attached to a tile
|
135
|
+
# layout, a TMX file. This affects the size and shape of the room,
|
136
|
+
# and how agents may travel through it. TmxLocations have X and Y
|
137
|
+
# coordinates (grid coordinates) for their positions.
|
138
|
+
class TmxLocation < Location
|
139
|
+
# Parse a tiled position string and return the X and Y tile coordinates
|
140
|
+
def self.position_to_coords(pos)
|
141
|
+
loc, x, y = position_to_loc_coords(pos)
|
142
|
+
return x, y
|
143
|
+
end
|
144
|
+
|
145
|
+
# Parse a tiled position string and return the location name and the X and Y tile coordinates
|
146
|
+
def self.position_to_loc_coords(pos)
|
147
|
+
loc, coords = pos.split("#",2)
|
148
|
+
if coords
|
149
|
+
x, y = coords.split(",")
|
150
|
+
return loc, x.to_i, y.to_i
|
151
|
+
else
|
152
|
+
return loc, nil, nil
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# When an item changes position in a TmxLocation, check if the new
|
157
|
+
# position leads out an exit. If so, send them where the exit
|
158
|
+
# leads instead.
|
159
|
+
def item_change_position(item, old_pos, new_pos)
|
160
|
+
exit = @state["exits"].detect { |e| e["from"] == new_pos }
|
161
|
+
return super unless exit # No exit? Do what you were going to.
|
162
|
+
|
163
|
+
# Going to hit an exit? Cancel this motion and enqueue an
|
164
|
+
# intention to do so? Or just send them through? If the former,
|
165
|
+
# it's very hard to unblockably pass through an exit, even if
|
166
|
+
# that's what's wanted. If the latter, it's very hard to make
|
167
|
+
# going through an exit blockable.
|
168
|
+
|
169
|
+
# Eh, just send them through for now. We'll figure out how to
|
170
|
+
# make detecting and blocking exit intentions easy later.
|
171
|
+
|
172
|
+
item_change_location(item, old_pos, exit["to"])
|
173
|
+
end
|
174
|
+
|
175
|
+
# This just determines if the position is valid at all. It does
|
176
|
+
# *not* check walkable/swimmable or even if it's big enough for a
|
177
|
+
# humanoid to stand in.
|
178
|
+
def valid_position?(pos)
|
179
|
+
return false unless pos[0...@name.size] == @name
|
180
|
+
return false unless pos[@name.size] == "#"
|
181
|
+
x, y = pos[(@name.size + 1)..-1].split(",", 2).map(&:to_i)
|
182
|
+
valid_coordinate?(x, y)
|
183
|
+
end
|
184
|
+
|
185
|
+
# This checks the coordinate's validity, but not relative to any
|
186
|
+
# specific person/item/whatever that could occupy the space.
|
187
|
+
def valid_coordinate?(x, y)
|
188
|
+
return false if x < 0 || y < 0
|
189
|
+
return false if x >= tiles[:spritestack][:width] || y >= tiles[:spritestack][:height]
|
190
|
+
return true unless tiles[:collision]
|
191
|
+
return tiles[:collision][y][x] == 0
|
192
|
+
end
|
193
|
+
|
194
|
+
# Determine whether this position can accomodate the given agent's shape and size.
|
195
|
+
def can_accomodate_agent?(agent, position)
|
196
|
+
loc, x, y = TmxLocation.position_to_loc_coords(position)
|
197
|
+
raise "Location #{@name.inspect} asked about different location #{loc.inspect} in can_accomodate_agent!" if loc != @name
|
198
|
+
shape = agent.state["shape"] || "humanoid"
|
199
|
+
can_accomodate_shape?(x, y, shape)
|
200
|
+
end
|
201
|
+
|
202
|
+
# Determine whether this coordinate location can accomodate a
|
203
|
+
# rectangular item of the given coordinate dimensions.
|
204
|
+
def can_accomodate_dimensions?(left_x, upper_y, width, height)
|
205
|
+
return false if left_x < 0 || upper_y < 0
|
206
|
+
right_x = left_x + width - 1
|
207
|
+
lower_y = upper_y + height - 1
|
208
|
+
return false if right_x >= tiles[:spritestack][:width] || lower_y >= tiles[:spritestack][:height]
|
209
|
+
return true unless tiles[:collision]
|
210
|
+
(left_x..right_x).each do |x|
|
211
|
+
(upper_y..lower_y).each do |y|
|
212
|
+
return false if tiles[:collision][y][x] != 0
|
213
|
+
end
|
214
|
+
end
|
215
|
+
return true
|
216
|
+
end
|
217
|
+
|
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
|
+
# For a TmxLocation's legal position, find somewhere not covered
|
240
|
+
# as a collision on the collision map.
|
241
|
+
def any_legal_position
|
242
|
+
loc_tiles = self.tiles
|
243
|
+
if tiles[:collision]
|
244
|
+
# We have a collision layer? Fabulous. Scan upper-left to lower-right until we get something non-collidable.
|
245
|
+
(0...tiles[:spritestack][:width]).each do |x|
|
246
|
+
(0...tiles[:spritestack][:height]).each do |y|
|
247
|
+
if tiles[:collision][y][x] == 0
|
248
|
+
# We found a walkable spot.
|
249
|
+
return "#{@name}##{x},#{y}"
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
else
|
254
|
+
# Is there a start location? If so, return it. Guaranteed good, right?
|
255
|
+
start_loc = tiles[:objects].detect { |obj| obj[:name] == "start location" }
|
256
|
+
if start_loc
|
257
|
+
x = start_loc[:x] / tiles[:spritestack][:tilewidth]
|
258
|
+
y = start_loc[:y] / tiles[:spritestack][:tileheight]
|
259
|
+
return "#{@name}##{x},#{y}"
|
260
|
+
end
|
261
|
+
# If no start location and no collision data, is there a first location with coordinates?
|
262
|
+
if tiles[:objects].first[:x]
|
263
|
+
obj = tiles[:objects].first
|
264
|
+
return "#{@name}##{x},#{y}"
|
265
|
+
end
|
266
|
+
# Screw it, just return the upper left corner.
|
267
|
+
return "#{@name}#0,0"
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# Return the tile object for this location
|
272
|
+
def tiles
|
273
|
+
raise("A TMX location (name: #{@name.inspect}) must have a tile layout!") unless state["tile_layout"] || state["manasource_tile_layout"]
|
274
|
+
TmxLocation.tile_cache_entry(state["manasource_tile_layout"], state["tile_layout"])
|
275
|
+
end
|
276
|
+
|
277
|
+
# Return a TMX object's structure, for an object of the given name, or nil.
|
278
|
+
def tmx_object_by_name(name)
|
279
|
+
tiles[:objects].detect { |o| o[:name] == name }
|
280
|
+
end
|
281
|
+
|
282
|
+
# Return the tile coordinates of the TMX object with the given name, or nil.
|
283
|
+
def tmx_object_coords_by_name(name)
|
284
|
+
obj = tiles[:objects].detect { |o| o[:name] == name }
|
285
|
+
if obj
|
286
|
+
[ obj[:x] / tiles[:spritesheet][:tilewidth], obj[:y] / tiles[:spritesheet][:tileheight] ]
|
287
|
+
else
|
288
|
+
nil
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
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
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
Demiurge::DSL::TopLevelBuilder.register_type "TmxZone", Demiurge::TmxZone
|
316
|
+
Demiurge::DSL::TopLevelBuilder.register_type "TmxLocation", Demiurge::TmxLocation
|
317
|
+
|
318
|
+
module Demiurge
|
319
|
+
# This is to support TMX files for ManaSource, ManaWorld, Land of
|
320
|
+
# Fire, Source of Tales and other Mana Project games. It can't be
|
321
|
+
# perfect since there's some variation between them, but it can
|
322
|
+
# follow most major conventions.
|
323
|
+
|
324
|
+
# Load a TMX file and calculate the objects inside including the
|
325
|
+
# Spritesheet and Spritestack. Assume this TMX file obeys ManaSource
|
326
|
+
# conventions such as fields for exits and names for layers.
|
327
|
+
def self.sprites_from_manasource_tmx(filename)
|
328
|
+
objs = sprites_from_tmx filename
|
329
|
+
stack = objs[:spritestack]
|
330
|
+
|
331
|
+
stack_layers = stack[:layers]
|
332
|
+
|
333
|
+
# Remove the collision layer, add as separate collision top-level entry
|
334
|
+
collision_index = stack_layers.index { |l| l[:name].downcase == "collision" }
|
335
|
+
collision_layer = stack_layers.delete_at collision_index if collision_index
|
336
|
+
|
337
|
+
# Some games make this true/false, others have separate visibility
|
338
|
+
# or swimmability in it. In general, we'll just expose the data.
|
339
|
+
objs[:collision] = collision_layer[:data] if collision_layer
|
340
|
+
|
341
|
+
# Remove the heights layer, add as separate heights top-level entry
|
342
|
+
heights_index = stack_layers.index { |l| ["height", "heights"].include?(l[:name].downcase) }
|
343
|
+
heights_layer = stack_layers.delete_at heights_index if heights_index
|
344
|
+
objs[:heights] = heights_layer
|
345
|
+
|
346
|
+
fringe_index = stack_layers.index { |l| l[:name].downcase == "fringe" }
|
347
|
+
raise ::Demiurge::Errors::TmxFormatError.new("No Fringe layer found in ManaSource TMX File #{filename.inspect}!", "filename" => filename) unless fringe_index
|
348
|
+
stack_layers.each_with_index do |layer, index|
|
349
|
+
# Assign a Z value based on layer depth, with fringe = 0 as a special case
|
350
|
+
layer["z"] = (index - fringe_index) * 10.0
|
351
|
+
end
|
352
|
+
|
353
|
+
objs
|
354
|
+
end
|
355
|
+
|
356
|
+
# Load a TMX file and calculate the objects inside including the
|
357
|
+
# Spritesheet and Spritestack. Do not assume this TMX file obeys any
|
358
|
+
# particular additional conventions beyond basic TMX format.
|
359
|
+
def self.sprites_from_tmx(filename)
|
360
|
+
spritesheet = {}
|
361
|
+
spritestack = {}
|
362
|
+
|
363
|
+
# This recursively loads things like tileset .tsx files
|
364
|
+
tiles = Tmx.load filename
|
365
|
+
|
366
|
+
spritestack[:name] = tiles.name || File.basename(filename).split(".")[0]
|
367
|
+
spritestack[:width] = tiles.width
|
368
|
+
spritestack[:height] = tiles.height
|
369
|
+
spritestack[:properties] = tiles.properties
|
370
|
+
|
371
|
+
spritesheet[:tilewidth] = tiles.tilewidth
|
372
|
+
spritesheet[:tileheight] = tiles.tileheight
|
373
|
+
|
374
|
+
spritesheet[:images] = tiles.tilesets.map do |tileset|
|
375
|
+
{
|
376
|
+
firstgid: tileset.firstgid,
|
377
|
+
tileset_name: tileset.name,
|
378
|
+
image: tileset.image,
|
379
|
+
imagewidth: tileset.imagewidth,
|
380
|
+
imageheight: tileset.imageheight,
|
381
|
+
tilewidth: tileset.tilewidth,
|
382
|
+
tileheight: tileset.tileheight,
|
383
|
+
oversize: tileset.tilewidth != tiles.tilewidth || tileset.tileheight != tiles.tileheight,
|
384
|
+
spacing: tileset.spacing,
|
385
|
+
margin: tileset.margin,
|
386
|
+
imagetrans: tileset.imagetrans, # Currently unused, color to treat as transparent
|
387
|
+
properties: tileset.properties,
|
388
|
+
}
|
389
|
+
end
|
390
|
+
spritesheet[:cyclic_animations] = animations_from_tilesets tiles.tilesets
|
391
|
+
|
392
|
+
spritesheet[:properties] = spritesheet[:images].map { |i| i[:properties] }.inject({}, &:merge)
|
393
|
+
spritesheet[:name] = spritesheet[:images].map { |i| i[:tileset_name] }.join("/")
|
394
|
+
spritestack[:spritesheet] = spritesheet[:name]
|
395
|
+
|
396
|
+
spritestack[:layers] = tiles.layers.map do |layer|
|
397
|
+
data = layer.data.each_slice(layer.width).to_a
|
398
|
+
{
|
399
|
+
name: layer.name,
|
400
|
+
data: data,
|
401
|
+
visible: layer.visible,
|
402
|
+
opacity: layer.opacity,
|
403
|
+
offsetx: layer.offsetx, # Currently unused
|
404
|
+
offsety: layer.offsety, # Currently unused
|
405
|
+
properties: layer.properties,
|
406
|
+
}
|
407
|
+
end
|
408
|
+
|
409
|
+
objects = tiles.object_groups.flat_map { |og| og.objects.to_a }.map(&:to_h)
|
410
|
+
|
411
|
+
{ filename: filename, tmx_name: File.basename(filename).split(".")[0], spritesheet: spritesheet, spritestack: spritestack, objects: objects }
|
412
|
+
end
|
413
|
+
|
414
|
+
# Find the animations included in the TMX file
|
415
|
+
def self.animations_from_tilesets tilesets
|
416
|
+
tilesets.flat_map do |tileset|
|
417
|
+
(tileset.tiles || []).map do |tile|
|
418
|
+
p = tile["properties"]
|
419
|
+
if p && p["animation-frame0"]
|
420
|
+
section = 0
|
421
|
+
anim = []
|
422
|
+
|
423
|
+
while p["animation-frame#{section}"]
|
424
|
+
section_hash = {
|
425
|
+
frame: p["animation-frame#{section}"].to_i + tileset[:firstgid],
|
426
|
+
duration: p["animation-delay#{section}"].to_i
|
427
|
+
}
|
428
|
+
anim.push section_hash
|
429
|
+
section += 1
|
430
|
+
end
|
431
|
+
{ "tile_anim_#{tile["id"].to_i + tileset[:firstgid]}".to_sym => anim }
|
432
|
+
else
|
433
|
+
nil
|
434
|
+
end
|
435
|
+
end.compact
|
436
|
+
end.inject({}, &:merge)
|
437
|
+
end
|
438
|
+
|
439
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Demiurge
|
2
|
+
# Utilities for deep-copying simple JSON-serializable data structures.
|
3
|
+
module Util
|
4
|
+
extend self
|
5
|
+
|
6
|
+
# This operation duplicates standard data that can be reconstituted from
|
7
|
+
# JSON, to make a frozen copy.
|
8
|
+
def copyfreeze(items)
|
9
|
+
case items
|
10
|
+
when Hash
|
11
|
+
result = {}
|
12
|
+
items.each do |k, v|
|
13
|
+
result[k] = copyfreeze(v)
|
14
|
+
end
|
15
|
+
result.freeze
|
16
|
+
when Array
|
17
|
+
items.map { |i| copyfreeze(i) }
|
18
|
+
when Numeric
|
19
|
+
items
|
20
|
+
when NilClass
|
21
|
+
items
|
22
|
+
when TrueClass
|
23
|
+
items
|
24
|
+
when FalseClass
|
25
|
+
items
|
26
|
+
when String
|
27
|
+
if items.frozen?
|
28
|
+
items
|
29
|
+
else
|
30
|
+
items.dup.freeze
|
31
|
+
end
|
32
|
+
else
|
33
|
+
STDERR.puts "Unrecognized item type #{items.class.inspect} in copyfreeze!"
|
34
|
+
items.dup.freeze
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# This operation duplicates standard data that can be reconstituted from
|
39
|
+
# JSON, to make a non-frozen copy.
|
40
|
+
def deepcopy(items)
|
41
|
+
case items
|
42
|
+
when Hash
|
43
|
+
result = {}
|
44
|
+
items.each do |k, v|
|
45
|
+
result[k] = deepcopy(v)
|
46
|
+
end
|
47
|
+
result
|
48
|
+
when Array
|
49
|
+
items.map { |i| deepcopy(i) }
|
50
|
+
when Numeric
|
51
|
+
items
|
52
|
+
when NilClass
|
53
|
+
items
|
54
|
+
when TrueClass
|
55
|
+
items
|
56
|
+
when FalseClass
|
57
|
+
items
|
58
|
+
when String
|
59
|
+
items.dup
|
60
|
+
else
|
61
|
+
STDERR.puts "Unrecognized item type #{items.class.inspect} in copyfreeze!"
|
62
|
+
items.dup
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module Demiurge
|
2
|
+
# A Zone is a top-level location. It may (or may not) contain
|
3
|
+
# various sub-locations managed by the top-level zone, and it may be
|
4
|
+
# quite large or quite small. Zones are the "magic" by which
|
5
|
+
# Demiurge permits simulation of much larger areas than CPU allows,
|
6
|
+
# up to and including "infinite" procedural areas where only a small
|
7
|
+
# portion is continuously simulated.
|
8
|
+
|
9
|
+
# A simplistic engine may contain only a small number of top-level
|
10
|
+
# areas, each a zone in itself. A complex engine may have a small
|
11
|
+
# number of areas, but each does extensive managing of its
|
12
|
+
# sub-locations.
|
13
|
+
|
14
|
+
class Zone < Container
|
15
|
+
# A Zone isn't located "inside" somewhere else. It is located in/at itself.
|
16
|
+
#
|
17
|
+
# @return [Demiurge::StateItem] The zone's item
|
18
|
+
# @since 0.0.1
|
19
|
+
def location
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
# A Zone isn't located "inside" somewhere else. It is located in/at itself.
|
24
|
+
#
|
25
|
+
# @return [String] The zone's name
|
26
|
+
# @since 0.0.1
|
27
|
+
def location_name
|
28
|
+
@name
|
29
|
+
end
|
30
|
+
|
31
|
+
# Similarly, a Zone has no position beyond itself.
|
32
|
+
#
|
33
|
+
# @return [String] The zone's name, which is also its position string
|
34
|
+
# @since 0.0.1
|
35
|
+
def position
|
36
|
+
@name
|
37
|
+
end
|
38
|
+
|
39
|
+
# A Zone's zone is itself.
|
40
|
+
#
|
41
|
+
# @return [Demiurge::StateItem] The Zone item
|
42
|
+
# @since 0.0.1
|
43
|
+
def zone
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
# A Zone's zone is itself.
|
48
|
+
#
|
49
|
+
# @return [String] The Zone's item name
|
50
|
+
# @since 0.0.1
|
51
|
+
def zone_name
|
52
|
+
@name
|
53
|
+
end
|
54
|
+
|
55
|
+
# A Zone is, indeed, a Zone.
|
56
|
+
#
|
57
|
+
# @return [Boolean] Return true for Zone and its subclasses.
|
58
|
+
# @since 0.0.1
|
59
|
+
def zone?
|
60
|
+
true
|
61
|
+
end
|
62
|
+
|
63
|
+
# By default, a zone just passes control to all its non-agent
|
64
|
+
# items, gathering up their intentions into a list. It doesn't ask
|
65
|
+
# agents since agents located directly in zones are usually only
|
66
|
+
# for instantiation.
|
67
|
+
#
|
68
|
+
# @return [Array<Demiurge::Intention>] The array of intentions for the next tick
|
69
|
+
# @since 0.0.1
|
70
|
+
def intentions_for_next_step
|
71
|
+
intentions = @state["contents"].flat_map do |item_name|
|
72
|
+
item = @engine.item_by_name(item_name)
|
73
|
+
item.agent? ? [] : item.intentions_for_next_step
|
74
|
+
end
|
75
|
+
intentions
|
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
|
+
end
|
108
|
+
end
|