demiurge 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,4 @@
1
+ module Demiurge
2
+ # Current version of Demiurge
3
+ VERSION = "0.2.0"
4
+ 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