tmx 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ pkg
2
+ *.swp
3
+ *.log
4
+ doc
5
+ coverage
6
+ .DS_Store
7
+ .yardoc
8
+ tmp
9
+ tags
10
+ ctags
11
+ .bundle
12
+ *.lock
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in gosu-tmx.gemspec
4
+ gemspec
@@ -0,0 +1,92 @@
1
+ # README
2
+ ## WHAT IS THIS
3
+
4
+ Eventually, a usable TMX map loader that works with Gosu and doesn't care
5
+ whether you're using Chingu or some home-grown game engine of your own
6
+ devising.
7
+
8
+ ### WHY ON EARTH
9
+
10
+ I like Chingu and a TMX loader already exists for it, but it's just not the
11
+ right tool for what I want to do. Hopefully others will find this useful as
12
+ well. :)
13
+
14
+ ### WHAT IS MISSING
15
+
16
+ Here's what's not: so far, map data is loaded and layers, object groups and
17
+ tile sets are created.
18
+
19
+ Validating the XML document to its DTD would be nice too, but I'll be buggered
20
+ if I can get Nokogiri to actually load the DTD. There is inadequate or no
21
+ documentation on this topic. Probably we'll just have mysterious failures on
22
+ unsupported or erroneous TMX files, which is not ideal.
23
+
24
+ Possible consideration for the future: move the dependency on Gosu into a
25
+ separate, mixable, matchable module. Maybe add explicit Chingu support too.
26
+ Handle tile set creation and drawing ops the same way we do object creation:
27
+ define a callback hook and let the user take care of it. Awesome.
28
+
29
+ Help is welcome, obviously.
30
+
31
+ ## INSTALL
32
+
33
+ Don't do it yet. The API is so unstable it does not have a half life but a
34
+ quarter life.
35
+
36
+ ### PREREQUISITES
37
+
38
+ * ruby >= 1.9.1 (probably)
39
+ * nokogiri
40
+
41
+ ## LICENSE
42
+
43
+ This software is available under the terms of the MIT license for no better
44
+ reason than that this is the license of Gosu itself. This means that you are
45
+ technically allowed to use my hard work as part of your proprietary,
46
+ commercial product with no obligation to give anything back but credit where
47
+ it's due. Use your discretion on that one.
48
+
49
+ The full license is reproduced here for posterity:
50
+
51
+ > Copyright © 2009–2010 Eris
52
+ >
53
+ > Permission is hereby granted, free of charge, to any person
54
+ > obtaining a copy of this software and associated documentation
55
+ > files (the "Software"), to deal in the Software without
56
+ > restriction, including without limitation the rights to use,
57
+ > copy, modify, merge, publish, distribute, sublicense, and/or sell
58
+ > copies of the Software, and to permit persons to whom the
59
+ > Software is furnished to do so, subject to the following
60
+ > conditions:
61
+ >
62
+ > The above copyright notice and this permission notice shall be
63
+ > included in all copies or substantial portions of the Software.
64
+ >
65
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
66
+ > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
67
+ > OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
68
+ > NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
69
+ > HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
70
+ > WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
71
+ > FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
72
+ > OTHER DEALINGS IN THE SOFTWARE.
73
+
74
+ ## AUTHORS
75
+
76
+ * Eris <eris.discord@gmail.com>
77
+ * your name here!
78
+
79
+ ## SEE ALSO
80
+
81
+ * [Gosu][], a 2D game development library for Ruby and C++
82
+ * [Chingu][], a higher level game library built on top of Gosu
83
+ * [Chipmunk][], a 2D rigid body physics engine in C
84
+ * [chipmunk-ffi][], more up-to-date Ruby bindings for Chipmunk
85
+ * [Tiled][], a flexible tile map editor and the origin of the TMX format (I
86
+ think).
87
+
88
+ [chingu]: http://github.com/ippa/chingu
89
+ [chipmunk]: http://code.google.com/p/chipmunk-physics
90
+ [chipmunk-ffi]: http://github.com/shawn42/chipmunk-ffi
91
+ [gosu]: http://libgosu.org
92
+ [tiled]: http://mapeditor.org/
@@ -0,0 +1,11 @@
1
+ require 'rspec/core/rake_task'
2
+ require "bundler/gem_tasks"
3
+
4
+ desc 'Default: run specs.'
5
+ task :default => :spec
6
+
7
+ desc "Run specs"
8
+ RSpec::Core::RakeTask.new do |t|
9
+ # t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
10
+ # Put spec opts in a file named .rspec in root
11
+ end
Binary file
Binary file
Binary file
@@ -0,0 +1,62 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <map version="1.0" orientation="orthogonal" width="16" height="16" tilewidth="16" tileheight="16">
3
+ <properties>
4
+ <property name="bg1" value="#402010"/>
5
+ <property name="bg2" value="#100000"/>
6
+ <property name="fg1" value="#00ff0000"/>
7
+ <property name="fg2" value="#10ff0000"/>
8
+ <property name="display_name" value="Test Map"/>
9
+ </properties>
10
+ <tileset firstgid="1" name="debug" tilewidth="16" tileheight="16">
11
+ <image source="test-tiles.png"/>
12
+ </tileset>
13
+ <layer name="background" width="16" height="16" opacity="0.75">
14
+ <data encoding="base64" compression="gzip">
15
+ H4sIAAAAAAAAA2NgGAXogInK5rGh0aNgFAwWAADxvIrYAAQAAA==
16
+ </data>
17
+ </layer>
18
+ <layer name="obstacle" width="16" height="16">
19
+ <data encoding="base64" compression="gzip">
20
+ H4sIAAAAAAAAA82T4QqAIAyEZ1Jakb3/27YjB2MMMfvjwSHMfTp1BiIqg47sTOPKk/EXvWeCAnupLmqMDR7aa95RnVQumfye+vV6GM+PfEtgV/b9g5/p/UR4u+jEe3ncSTIx9Ia9J4/H3pvDezUmwwvbw0uN0h+658Stv4t59NIDVh+CwAAEAAA=
21
+ </data>
22
+ </layer>
23
+ <objectgroup name="doors" width="16" height="16">
24
+ <object name="door1" type="Door" x="128" y="208" width="16" height="32">
25
+ <properties>
26
+ <property name="image" value="door.png"/>
27
+ <property name="target" value="door2"/>
28
+ </properties>
29
+ </object>
30
+ <object name="door2" type="Door" x="64" y="32" width="16" height="32">
31
+ <properties>
32
+ <property name="image" value="door.png"/>
33
+ <property name="target" value="door1"/>
34
+ </properties>
35
+ </object>
36
+ </objectgroup>
37
+ <objectgroup name="objects" width="16" height="16">
38
+ <properties>
39
+ <property name="what_is_this" value="it's boxes ok"/>
40
+ </properties>
41
+ <object name="cardboard_box" type="Box" x="160" y="160" width="32" height="32">
42
+ <properties>
43
+ <property name="image" value="tid:16"/>
44
+ </properties>
45
+ </object>
46
+ <object name="stone_block" type="Box" x="192" y="32" width="32" height="32">
47
+ <properties>
48
+ <property name="image" value="tid:16"/>
49
+ </properties>
50
+ </object>
51
+ <object name="dude" type="Dude" x="80" y="144" width="16" height="32">
52
+ <properties>
53
+ <property name="image" value="dude.png"/>
54
+ </properties>
55
+ </object>
56
+ </objectgroup>
57
+ <layer name="foreground" width="16" height="16" opacity="0.75">
58
+ <data encoding="base64" compression="gzip">
59
+ H4sIAAAAAAAAA2NgGAWjYPABRiBmIoAJATYCeCD1EwMo1U8MAADF4i9MAAQAAA==
60
+ </data>
61
+ </layer>
62
+ </map>
@@ -0,0 +1,114 @@
1
+ require 'chingu'
2
+ require 'tmx'
3
+ require 'opengl' # Chingu needs it for retrofy
4
+
5
+ class Float
6
+ INFINITY = 1.0 / 0.0 unless const_defined? :INFINITY
7
+ end
8
+
9
+ class Box < Chingu::GameObject
10
+ # "It's just a box."
11
+
12
+ # (Chingu takes care of everything we need here (it's magic))
13
+ end
14
+
15
+ class Door < Chingu::GameObject
16
+ # this ought to take you somewhere else
17
+ end
18
+
19
+ class Dude < Chingu::GameObject
20
+ # maybe your player logic goes here
21
+ end
22
+
23
+ class MapState < Chingu::GameState
24
+ has_trait :viewport
25
+
26
+ def initialize map_name, *rest
27
+ super *rest
28
+
29
+ # load our map
30
+ @map = TMX::Map.new $window, map_name,
31
+ :on_object => method(:create_chingu_object)
32
+ @banner = Gosu::Image.from_text $window,
33
+ @map.properties[:display_name],
34
+ 'Helvetica', 24
35
+
36
+ $window.caption = 'tmx demo - %s' % @map.properties[:display_name]
37
+ end
38
+
39
+ def create_chingu_object name, group, properties
40
+ map = group.map
41
+ obj_class = Kernel.const_get properties[:type] rescue nil
42
+
43
+ # assert that the object is a valid type
44
+ raise TypeError, "#{properties[:type]} is not a game object" \
45
+ unless obj_class.is_a? Class \
46
+ and obj_class.ancestors.include? Chingu::BasicGameObject
47
+
48
+ # load image
49
+ image_name = properties[:image]
50
+ image = case image_name
51
+ when /^tid:(\d+)$/
52
+ # a tile from the map's tile set
53
+ map.tile_set[Integer($1)]
54
+ when nil
55
+ # default image
56
+ Gosu::Image['default']
57
+ else
58
+ # image as named
59
+ Gosu::Image[File.join 'data', image_name]
60
+ end
61
+
62
+ # convert TMX properties to what Chingu is expecting
63
+ properties.merge! \
64
+ :image => image.retrofy,
65
+ :x => properties[:x] + properties[:width] / 2,
66
+ :y => properties[:y] + properties[:height] / 2,
67
+ :factor_x => properties[:width].to_f / image.width,
68
+ :factor_y => properties[:height].to_f / image.height,
69
+ :zorder => 1
70
+
71
+ # create and return the game object
72
+ obj_class.create properties
73
+ end
74
+
75
+ def update
76
+ super
77
+ # move the map opposite the mouse pointer, with lag
78
+ viewport.x = 0.9 * viewport.x + 0.1 * ($window.mouse_x - $window.width + @map.width / 2)
79
+ viewport.y = 0.9 * viewport.y + 0.1 * ($window.mouse_y - $window.height + @map.height / 2)
80
+ end
81
+
82
+ def draw
83
+ super
84
+
85
+ fill_gradient \
86
+ :from => @map.properties[:bg1],
87
+ :to => @map.properties[:bg2],
88
+ :zorder => -Float::INFINITY
89
+
90
+ # map is not a game object
91
+ @map.draw -viewport.x, -viewport.y
92
+
93
+ fill_gradient \
94
+ :from => @map.properties[:fg1],
95
+ :to => @map.properties[:fg2],
96
+ :zorder => Float::INFINITY
97
+
98
+ @banner.draw 8, 8, Float::INFINITY
99
+ end
100
+ end
101
+
102
+ class MapWindow < Chingu::Window
103
+ def initialize
104
+ super 400, 200, false
105
+
106
+ # global inputs for our demo
107
+ self.input = { :escape => :close }
108
+
109
+ # load that map
110
+ push_game_state MapState.new('data/test.tmx')
111
+ end
112
+ end
113
+
114
+ MapWindow.new.show
@@ -0,0 +1,16 @@
1
+ autoload :Base64, 'base64'
2
+ autoload :Nokogiri, 'nokogiri'
3
+ autoload :WeakRef, 'weakref'
4
+ # autoload :Zlib, 'zlib'
5
+
6
+ # mysterious autoload failure
7
+ require 'zlib'
8
+
9
+ require 'tmx/nokogiri_additions'
10
+
11
+ module Tmx
12
+ autoload :Coder, 'tmx/coder'
13
+ autoload :Layer, 'tmx/layer'
14
+ autoload :Map, 'tmx/map'
15
+ autoload :ObjectGroup, 'tmx/object_group'
16
+ end
@@ -0,0 +1,39 @@
1
+ module Tmx
2
+ module Coder
3
+ # default coders
4
+ autoload :Base64, 'tmx/coder/base64'
5
+ autoload :Gzip, 'tmx/coder/gzip'
6
+
7
+ def self.encode str, *encodings
8
+ encodings.reject(&:nil?).reduce(str) do |data, encoding|
9
+ find_coder(encoding).encode(data)
10
+ end
11
+ end # encode
12
+
13
+ def self.decode str, *encodings
14
+ encodings.reject(&:nil?).reverse.reduce(str) do |data, encoding|
15
+ find_coder(encoding).decode(data)
16
+ end
17
+ end # decode
18
+
19
+ def self.find_coder name
20
+ const_name = name.to_s.capitalize.gsub /(?:\b|_)([a-z])/, &:upcase
21
+ if const_defined? const_name
22
+ const_get const_name
23
+ else
24
+ raise NameError, "unknown coder #{name}"
25
+ end
26
+ end # find_coder
27
+
28
+ def self.register_coder name, mod
29
+ const_name = name.to_s.capitalize.gsub /(?:\b|_)([a-z])/, &:upcase
30
+ if not const_defined? const_name
31
+ const_set const_name, mod
32
+ else
33
+ raise NameError, "coder #{name} already registered"
34
+ end
35
+ end # register_coder
36
+
37
+ end # Coder
38
+
39
+ end
@@ -0,0 +1,13 @@
1
+ module Tmx
2
+ module Coder
3
+ module Base64
4
+ def self.encode str
5
+ ::Base64.strict_encode64 str
6
+ end
7
+
8
+ def self.decode str
9
+ ::Base64.decode64 str
10
+ end
11
+ end # Base64
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ module Tmx
2
+ module Coder
3
+ module Gzip
4
+ def self.encode str
5
+ buffer = String.new
6
+ writer = Zlib::GzipWriter.new(StringIO.new(buffer))
7
+ writer << str
8
+ writer.close
9
+ buffer
10
+ end
11
+
12
+ def self.decode str
13
+ reader = Zlib::GzipReader.new(StringIO.new(str))
14
+ output = reader.read
15
+ reader.close
16
+ output
17
+ end
18
+
19
+ end # Gzip
20
+ end
21
+ end
@@ -0,0 +1,74 @@
1
+ module Tmx
2
+ class Layer
3
+ attr_reader :properties
4
+ attr_reader :columns, :rows
5
+
6
+ def initialize map, data, properties
7
+ @map = WeakRef.new map
8
+ @properties = properties.dup
9
+
10
+ @columns = @properties.delete(:width) or raise ArgumentError, "layer width is required"
11
+ @rows = @properties.delete(:height) or raise ArgumentError, "layer height is required"
12
+
13
+ @tile_ids = case data
14
+ when String then data.unpack('V*')
15
+ when Array then data.dup
16
+ when nil then Array.new @columns * @rows, 0
17
+ else raise ArgumentError, "data must be a binary string or an array of integers"
18
+ end
19
+ end # initialize
20
+
21
+ def map; @map.__getobj__ end
22
+
23
+ def [] x, y
24
+ raise IndexError unless x_range.include? x and y_range.include? y
25
+ @tile_ids[offset(x, y)]
26
+ end
27
+
28
+ def []= x, y, id
29
+ raise IndexError unless x_range.include? x and y_range.include? y
30
+ @tile_ids[offset(x, y)] = id
31
+ end
32
+
33
+ def each_tile_id &block
34
+ y_range.each do |y|
35
+ x_range.each do |x|
36
+ yield x, y, @tile_ids[offset(x, y)]
37
+ end
38
+ end
39
+ end # each_tile_id
40
+
41
+ def x_range; 0...@columns end
42
+ def y_range; 0...@rows end
43
+
44
+ # def draw x_off, y_off, z_off, x_range, y_range
45
+ # x_range = [x_range.min, 0].max .. [x_range.max, @columns - 1].min
46
+ # y_range = [y_range.min, 0].max .. [y_range.max, @rows - 1].min
47
+ #
48
+ # tile_set = @map.tile_set
49
+ # tile_width = @map.tile_width
50
+ # tile_height = @map.tile_height
51
+ #
52
+ # y_range.each do |y|
53
+ # tile_y_off = y_off + y * tile_height
54
+ #
55
+ # x_range.each do |x|
56
+ # tile_x_off = x_off + x * tile_width
57
+ # tile_index = @tile_ids[offset(x, y)]
58
+ #
59
+ # image = tile_set[tile_index]
60
+ # next if image.nil?
61
+ #
62
+ # image.draw tile_x_off, tile_y_off, z_off, 1, 1, @color
63
+ # end
64
+ # end
65
+ # end
66
+
67
+ private
68
+
69
+ def offset x, y
70
+ x + y * @columns
71
+ end
72
+
73
+ end # Layer
74
+ end
@@ -0,0 +1,147 @@
1
+ module Tmx
2
+ class Map
3
+ # autoload :TileCache, 'tmx/map/tile_cache'
4
+ autoload :XMLLoader, 'tmx/map/xml_loader'
5
+
6
+ include XMLLoader
7
+
8
+ attr_reader :properties
9
+ attr_reader :columns, :rows
10
+ attr_reader :width, :height
11
+ attr_reader :tile_width, :tile_height
12
+
13
+ attr_reader :layers, :object_groups#, :tile_set
14
+
15
+ DEFAULT_OPTIONS = {
16
+ # Scales pixel units to tile units (if true) or user-defined scale (if
17
+ # numeric) when passing them to callbacks.
18
+ :scale_units => false,
19
+
20
+ # Hooks for object, layer and tile set creation. Only on_object is
21
+ # implemented so far.
22
+ :on_tile_set => nil,
23
+ :on_layer => nil,
24
+ :on_object => nil,
25
+
26
+ # This option discards all layer, object group and tile set info after
27
+ # building the tile cache; uses less memory if you don't intend to
28
+ # modify the map at run time.
29
+ :discard_structure => false,
30
+
31
+ # These three options allow finer grained control of what to throw away
32
+ # in case you intend to modify only certain aspects of the map.
33
+ :discard_layer_info => false,
34
+ :discard_object_info => false,
35
+ }
36
+
37
+ def initialize file_name, options = {}
38
+ options = DEFAULT_OPTIONS.merge options
39
+
40
+ # TODO move this XML code to an external module
41
+ # TODO allow file name or xml document?
42
+ # TODO allow other map formats?
43
+
44
+ mapdef = File.open(file_name) do |io|
45
+ doc = Nokogiri::XML(io) { |conf| conf.noent.noblanks }
46
+
47
+ # TODO figure out why this always fails
48
+ # errors = doc.validate
49
+
50
+ doc.root
51
+ end
52
+
53
+ # TODO proper version check; learn about Tmx versions if there are any
54
+ raise "Only version 1.0 maps are currently supported" unless mapdef['version'] == '1.0'
55
+ raise "Only orthogonal maps are currently supported" unless mapdef['orientation'] == 'orthogonal'
56
+
57
+ # @cache = TileCache.new self
58
+
59
+ @tile_width = mapdef['tilewidth'].to_i
60
+ @tile_height = mapdef['tileheight'].to_i
61
+
62
+ @columns = mapdef['width'].to_i
63
+ @rows = mapdef['height'].to_i
64
+
65
+ @scale_units = case options[:scale_units]
66
+ when Numeric then options[:scale_units].to_f
67
+ when :tile_width then 1.0 / @tile_width
68
+ when :tile_height then 1.0 / @tile_height
69
+ when true then 1.0 / [@tile_width, @tile_height].min
70
+ else false
71
+ end
72
+
73
+ if @scale_units
74
+ @width = @columns.to_f * @scale_units
75
+ @height = @rows.to_f * @scale_units
76
+ else
77
+ @width = @columns * @tile_width
78
+ @height = @rows * @tile_height
79
+ end
80
+
81
+ @properties = mapdef.tmx_parse_properties
82
+
83
+ # @tile_set = TileSet.new self
84
+
85
+ @layers = Hash[]
86
+ @object_groups = Hash[]
87
+
88
+ # callback for custom object creation
89
+ @on_object = options[:on_object]
90
+
91
+ # mapdef.xpath('tileset').each do |xml|
92
+ # @tile_set.load_tiles *parse_tile_set_def(xml)
93
+ # end
94
+
95
+ mapdef.xpath('layer').each do |xml|
96
+ layer = parse_layer_def xml
97
+ name = layer.properties[:name]
98
+ @layers[name] = layer
99
+ end # layers
100
+
101
+
102
+ mapdef.xpath('objectgroup').each do |xml|
103
+ group = parse_object_group_def xml
104
+ name = group.name
105
+ @object_groups[name] = group
106
+ end # object groups
107
+
108
+ # @cache.rebuild!
109
+
110
+ discard_structure = @properties.delete(:discard_structure)
111
+
112
+ @layers = nil if @properties.delete(:discard_layer_info) || discard_structure
113
+ # @tile_sets = nil if @properties.delete(:discard_tile_info) || discard_structure
114
+ @object_groups = nil if @properties.delete(:discard_object_info) || discard_structure
115
+
116
+ end # initialize
117
+
118
+ # def create_tile_set name, file_name_or_images, properties
119
+ # raise NotImplementedError
120
+ # end
121
+
122
+ def create_layer name, data, properties
123
+ raise NotImplementedError
124
+ end
125
+
126
+ def create_object_group name, properties
127
+ raise NotImplementedError
128
+ end
129
+
130
+ def draw x_off, y_off, z_off = 0, x_range = 0...@columns, y_range = 0...@rows
131
+ # @cache.draw x_off, y_off, z_off, x_range, y_range
132
+ @layers.each_value.with_index do |layer, index|
133
+ layer.draw x_off, y_off, z_off + index, x_range, y_range
134
+ end
135
+ end # draw
136
+
137
+ protected
138
+
139
+ def on_object name, group, properties
140
+ if @on_object
141
+ @on_object.call name, group, properties
142
+ else
143
+ properties
144
+ end
145
+ end # on_object
146
+ end # Map
147
+ end
@@ -0,0 +1,83 @@
1
+ module Tmx
2
+ # WARNING this is currently deprecated and not actually used
3
+ class Map::TileCache
4
+ def initialize map
5
+ @map = WeakRef.new map
6
+
7
+ @tile_cache = []
8
+ @map_cache = []
9
+
10
+ @layer_count = 0
11
+
12
+ @columns, @rows = 0, 0
13
+ @tile_width, @tile_height = 0, 0
14
+ end # initialize
15
+
16
+ def rebuild!
17
+ rebuild_tile_set!
18
+ rebuild_map!
19
+ end
20
+
21
+ def rebuild_tile_set!
22
+ raise RuntimeError, "tile set information has been discarded" if @map.tile_sets.nil?
23
+
24
+ @tile_cache.clear
25
+ @map.tile_sets.each_value do |tile_set|
26
+ @tile_cache[tile_set.range] = tile_set.tiles
27
+ end
28
+ end
29
+
30
+ def rebuild_map!
31
+ raise RuntimeError, "layer information has been discarded" if @map.layers.nil?
32
+
33
+ @map_cache.clear
34
+
35
+ @layer_count = @map.layers.count
36
+
37
+ @columns, @rows = @map.columns, @map.rows
38
+ @tile_width, @tile_height = @map.tile_width, @map.tile_height
39
+
40
+ @map.layers.each_value.with_index do |layer, layer_index|
41
+ (0...@rows).each do |y|
42
+ (0...@columns).each do |x|
43
+ index = _tile_index layer_index, x, y
44
+ tile_id = layer[x, y]
45
+ @map_cache[index] = @tile_cache[tile_id]
46
+ end
47
+ end
48
+ end
49
+ end # rebuild_map!
50
+
51
+ def draw x_off, y_off, z_off, x_range, y_range
52
+ x_range = [x_range.min, 0].max .. [x_range.max, @columns - 1].min
53
+ y_range = [y_range.min, 0].max .. [y_range.max, @rows - 1].min
54
+
55
+ y_range.each do |y|
56
+ tile_y_off = y_off + y * @tile_height
57
+
58
+ x_range.each do |x|
59
+ tile_x_off = x_off + x * @tile_width
60
+
61
+ range = _tile_range x, y
62
+ @map_cache[range].each.with_index do |image, z|
63
+ next if image.nil?
64
+ image.draw tile_x_off, tile_y_off, z_off + z
65
+ end
66
+
67
+ end
68
+ end
69
+ end # draw
70
+
71
+ protected
72
+
73
+ def _tile_index layer_index, x, y
74
+ y * @columns * @layer_count + x * @layer_count + layer_index
75
+ end
76
+
77
+ def _tile_range x, y
78
+ first = _tile_index 0, x, y
79
+ first...(first + @layer_count)
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,41 @@
1
+ module Tmx
2
+ module Map::XMLLoader
3
+ protected
4
+
5
+ def parse_tile_set_def xml
6
+ properties = xml.tmx_parse_attributes
7
+ image_path = File.absolute_path xml.xpath('image/@source').first.value, File.dirname(xml.document.url)
8
+ [ image_path, properties ]
9
+ end
10
+
11
+ def parse_layer_def xml
12
+ properties = xml.tmx_parse_properties.merge! xml.tmx_parse_attributes
13
+ Layer.new self, xml.tmx_data, properties
14
+ end
15
+
16
+ def parse_object_group_def xml
17
+ properties = xml.tmx_parse_properties.merge! xml.tmx_parse_attributes
18
+ group = ObjectGroup.new self, properties
19
+
20
+ xml.xpath('object').each do |child|
21
+ obj = parse_object_def(child, group)
22
+ group.add obj if obj
23
+ end
24
+
25
+ group
26
+ end # parse_object_group_def
27
+
28
+ def parse_object_def xml, group
29
+ properties = xml.tmx_parse_properties.merge! xml.tmx_parse_attributes
30
+ # name = properties[:name]
31
+ name = properties[:name]
32
+
33
+ [:x, :y, :width, :height].each do |key|
34
+ properties[key] = properties[key] * @scale_units
35
+ end if @scale_units
36
+
37
+ on_object name, group, properties
38
+ end
39
+
40
+ end # Map::XMLLoader
41
+ end
@@ -0,0 +1,56 @@
1
+ # TODO investigate writing Nokogiri::TMX class hierarchy (yeah right)
2
+
3
+ class Nokogiri::XML::Node
4
+ def to_sym; self.to_s.to_sym end
5
+
6
+ def to_i; self.to_s.to_i end
7
+ def to_f; self.to_s.to_f end
8
+ def to_c; self.to_s.to_c end
9
+ def to_r; self.to_s.to_r end
10
+
11
+ def tmx_parse
12
+ str = to_s
13
+ case str
14
+ when '' then nil
15
+ when %r(^ (?: false | no | off ) $)ix then false
16
+ when %r(^ (?: true | yes | on ) $)ix then true
17
+ when %r(^ [+-]? \d+ / [+-]? \d+ $)ix then str.to_r
18
+ when %r(^ [+-]? (?: \d+ \. \d+ [+-] ) \d+ i $)ix then str.to_c
19
+ when %r(^ [+-]? \d+ \. \d+ $)ix then str.to_f
20
+ when %r(^ [+-]? \d+ $)ix then str.to_i
21
+ when %r(^ \# [0-9a-f]{6} $)ix
22
+ 0xff000000 | str[1..6].to_i(16)
23
+ when %r(^ \# [0-9a-f]{8} $)ix
24
+ str[1..8].to_i(16)
25
+ else str
26
+ end
27
+ end
28
+ end
29
+
30
+ class Nokogiri::XML::Element
31
+ def tmx_parse_attributes
32
+ attributes.reduce({}) do |h, pair|
33
+ name = pair[0].to_sym
34
+ value = pair[1].tmx_parse
35
+ h.merge name => value
36
+ end
37
+ end
38
+
39
+ def tmx_parse_properties
40
+ properties_element = xpath('properties').first or return {}
41
+ properties_element.xpath('property').reduce({}) do |h, property|
42
+ name = property.attribute('name').to_sym
43
+ value = property.attribute('value').tmx_parse
44
+ h.merge! name => value
45
+ end
46
+ end
47
+
48
+ def tmx_data
49
+ data = ''
50
+ xpath('data').each do |data_element|
51
+ attrs = data_element.tmx_parse_attributes
52
+ data << Tmx::Coder.decode(data_element.text, attrs[:compression], attrs[:encoding])
53
+ end
54
+ data
55
+ end
56
+ end
@@ -0,0 +1,30 @@
1
+ module Tmx
2
+ class ObjectGroup
3
+ include Enumerable
4
+
5
+ attr_reader :properties
6
+ attr_reader :name
7
+
8
+ def initialize map, properties = {}
9
+ @map = WeakRef.new map
10
+ @properties = properties
11
+ @objects = Hash[]
12
+
13
+ @name = properties.delete :name
14
+ end
15
+
16
+ def map; @map.__getobj__ end
17
+
18
+ def add obj
19
+ @objects[obj.object_id] = obj
20
+ end
21
+
22
+ def remove obj
23
+ @objects.delete obj.object_id
24
+ end
25
+
26
+ def each &block
27
+ @objects.each_value &block
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ module Tmx
2
+ # class TileSet
3
+ # def initialize map
4
+ # @map = WeakRef.new map
5
+ # @tiles = [ nil ]
6
+ # end
7
+ #
8
+ # def load_tiles file_name_or_images, properties = {}
9
+ # properties = properties.dup
10
+ # first_id = properties.delete :firstgid
11
+ # tile_width = properties.delete :tilewidth
12
+ # tile_height = properties.delete :tileheight
13
+ #
14
+ # @tiles[first_id..-1] = case file_name_or_images
15
+ # when String then Gosu::Image.load_tiles(@map.window, file_name_or_images, tile_width, tile_height, true).freeze
16
+ # when Array then file_name_or_images.dup
17
+ # else raise ArgumentError, "must supply a file name or an array of images"
18
+ # end
19
+ #
20
+ # end # load_tiles
21
+ #
22
+ # def map; @map.__getobj__ end
23
+ #
24
+ # def [] tile_id
25
+ # raise IndexError unless (0...@tiles.length).include? tile_id
26
+ # @tiles[tile_id]
27
+ # end
28
+ #
29
+ # def []= tile_id, image
30
+ # raise IndexError unless (1...@tiles.length).include? tile_id
31
+ # @tiles[tile_id] = image
32
+ # end
33
+ #
34
+ # end
35
+ end
@@ -0,0 +1,3 @@
1
+ module Tmx
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,29 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+ describe 'Coder' do
3
+ it 'can find the default coders' do
4
+ Tmx::Coder.find_coder(:base64).should_not == nil
5
+ Tmx::Coder.find_coder(:gzip).should_not == nil
6
+ end
7
+
8
+ CODER_TEST_STRING = <<-EOS
9
+ Lorem ipsum dolor sit amet, consecteteur adapiscing elit.
10
+ EOS
11
+
12
+ def coder_round_trip *encodings
13
+ encoded = Tmx::Coder.encode CODER_TEST_STRING, *encodings
14
+ decoded = Tmx::Coder.decode encoded, *encodings
15
+ decoded.should == CODER_TEST_STRING
16
+ end
17
+
18
+ it 'can round trip base64' do
19
+ coder_round_trip :base64
20
+ end
21
+
22
+ it 'can round trip gzip' do
23
+ coder_round_trip :gzip
24
+ end
25
+
26
+ it 'can round trip multiple encodings' do
27
+ coder_round_trip :gzip, :base64
28
+ end
29
+ end
@@ -0,0 +1,50 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+ describe 'Map' do
3
+ it 'can load the test map' do
4
+ $map = Tmx::Map.new File.join($data_dir, 'test.tmx'),
5
+ :scale_units => false
6
+ $map.should.is_a? Tmx::Map
7
+ end
8
+
9
+ it 'has the correct dimensions' do
10
+ $map.columns.should == 16
11
+ $map.rows.should == 16
12
+
13
+ $map.width.should == 16 * $map.tile_width
14
+ $map.height.should == 16 * $map.tile_height
15
+ end
16
+
17
+ it 'has the correct tile dimensions' do
18
+ $map.tile_width.should == 16
19
+ $map.tile_height.should == 16
20
+ end
21
+
22
+ # it 'loads all tile set definitions into one flat set' do
23
+ # $map.tile_set.should.is_a? Tmx::TileSet
24
+ # $map.tile_set[0].should == nil
25
+ # $map.tile_set[1].should.is_a? Gosu::Image
26
+ # end
27
+
28
+ it 'loads all layers' do
29
+ $map.layers.count.should == 3
30
+ $map.layers.each do |name, layer|
31
+ layer.should.is_a? Tmx::Layer
32
+ end
33
+ end
34
+
35
+ it 'loads all object groups' do
36
+ $map.object_groups.count.should == 2
37
+ $map.object_groups.each do |name, group|
38
+ Tmx::ObjectGroup.should === group
39
+ end
40
+ end
41
+
42
+ # it 'can draw itself' do
43
+ # $window.run_test\
44
+ # :time => 1.0,
45
+ # :before => proc { @x, @y = 0, 0 },
46
+ # :update => proc { @y -= 1 },
47
+ # :draw => proc { $map.draw @x, @y },
48
+ # end
49
+
50
+ end
@@ -0,0 +1,5 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+ describe 'nokogiri additions' do
3
+ before(:all) { $doc = File.join($data_dir, 'test.tmx') }
4
+ after(:all) { $doc = nil }
5
+ end
@@ -0,0 +1,41 @@
1
+
2
+ require File.join(File.dirname(__FILE__), 'spec_helper')
3
+ describe 'object groups' do
4
+
5
+ # {:image=>"door.png", :target=>"door2", :name=>"door1", :type=>"Door", :x=>128, :y=>208, :width=>16, :height=>32}
6
+ # {:image=>"door.png", :target=>"door1", :name=>"door2", :type=>"Door", :x=>64, :y=>32, :width=>16, :height=>32}
7
+
8
+ it 'loads all object group properties' do
9
+ map = Tmx::Map.new File.join($data_dir, 'test.tmx'), :scale_units => false
10
+ object_group = map.object_groups["doors"]
11
+ object_group.should_not be_nil
12
+
13
+ captured = []
14
+ object_group.each do |obj|
15
+ captured << obj
16
+ end
17
+
18
+ obj = captured.first
19
+
20
+ obj[:target].should == "door2"
21
+ obj[:image].should == "door.png"
22
+ obj[:type].should == "Door"
23
+ obj[:x].should == 128
24
+ obj[:y].should == 208
25
+ obj[:width].should == 16
26
+ obj[:height].should == 32
27
+ obj[:name].should == "door1"
28
+
29
+ obj = captured.last
30
+ obj[:target].should == "door1"
31
+ obj[:image].should == "door.png"
32
+ obj[:type].should == "Door"
33
+ obj[:x].should == 64
34
+ obj[:y].should == 32
35
+ obj[:width].should == 16
36
+ obj[:height].should == 32
37
+ obj[:name].should == "door2"
38
+ end
39
+
40
+
41
+ end
@@ -0,0 +1,52 @@
1
+
2
+ tmx_root = File.dirname(File.dirname(__FILE__))
3
+
4
+ $:.unshift File.join(tmx_root, 'lib')
5
+ $data_dir = File.join(tmx_root, 'data')
6
+
7
+ require 'gosu'
8
+ require 'tmx'
9
+
10
+ class TMXSpecWindow < Gosu::Window
11
+ def initialize
12
+ super 400, 200, false
13
+ end
14
+
15
+ def run_test options = {}
16
+ @before = options.delete :before
17
+ @update = options.delete :update
18
+ @draw = options.delete :draw
19
+ @after = options.delete :after
20
+
21
+ @time_limit = options.delete(:time) || 1.0
22
+ @start_time = Gosu.milliseconds / 1000.0
23
+
24
+ @before.call if @before
25
+
26
+ show
27
+ end
28
+
29
+ def end_test
30
+ @after.call if @after
31
+ close
32
+ end
33
+
34
+ def update
35
+ this_time = Gosu.milliseconds / 1000.0
36
+ @start_time
37
+
38
+ @update.call(Gosu.milliseconds) if @update
39
+
40
+ if this_time - @start_time > @time_limit
41
+ end_test
42
+ end
43
+
44
+ @last_time = this_time
45
+ end
46
+
47
+ def draw
48
+ @draw.call(Gosu.milliseconds) if @draw
49
+ end
50
+ end
51
+
52
+ $window = TMXSpecWindow.new
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "tmx/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "tmx"
7
+ s.version = Tmx::VERSION
8
+ s.authors = ["erisdiscord"]
9
+ s.email = ["eris.discord@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Eventually, a usable TMX map loader that works with Gosu and doesn't care whether you're using Chingu or some home-grown game engine of your own devising.}
12
+ s.description = %q{Eventually, a usable TMX map loader that works with Gosu and doesn't care whether you're using Chingu or some home-grown game engine of your own devising.}
13
+
14
+ s.rubyforge_project = "tmx"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+
22
+ s.add_development_dependency "rake"
23
+ s.add_development_dependency "rspec"
24
+ s.add_development_dependency "gosu"
25
+
26
+ s.add_runtime_dependency "nokogiri"
27
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tmx
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - erisdiscord
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-10-04 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &2161052740 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *2161052740
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &2161052300 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *2161052300
36
+ - !ruby/object:Gem::Dependency
37
+ name: gosu
38
+ requirement: &2161051880 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *2161051880
47
+ - !ruby/object:Gem::Dependency
48
+ name: nokogiri
49
+ requirement: &2161051440 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *2161051440
58
+ description: Eventually, a usable TMX map loader that works with Gosu and doesn't
59
+ care whether you're using Chingu or some home-grown game engine of your own devising.
60
+ email:
61
+ - eris.discord@gmail.com
62
+ executables: []
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - .gitignore
67
+ - Gemfile
68
+ - README.markdown
69
+ - Rakefile
70
+ - data/door.png
71
+ - data/dude.png
72
+ - data/test-tiles.png
73
+ - data/test.tmx
74
+ - examples/example-chingu.rb
75
+ - lib/tmx.rb
76
+ - lib/tmx/coder.rb
77
+ - lib/tmx/coder/base64.rb
78
+ - lib/tmx/coder/gzip.rb
79
+ - lib/tmx/layer.rb
80
+ - lib/tmx/map.rb
81
+ - lib/tmx/map/tile_cache.rb
82
+ - lib/tmx/map/xml_loader.rb
83
+ - lib/tmx/nokogiri_additions.rb
84
+ - lib/tmx/object_group.rb
85
+ - lib/tmx/tile_set.rb
86
+ - lib/tmx/version.rb
87
+ - spec/coder_spec.rb
88
+ - spec/map_spec.rb
89
+ - spec/nokogiri_spec.rb
90
+ - spec/object_group_spec.rb
91
+ - spec/spec_helper.rb
92
+ - tmx.gemspec
93
+ homepage: ''
94
+ licenses: []
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ! '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project: tmx
113
+ rubygems_version: 1.8.6
114
+ signing_key:
115
+ specification_version: 3
116
+ summary: Eventually, a usable TMX map loader that works with Gosu and doesn't care
117
+ whether you're using Chingu or some home-grown game engine of your own devising.
118
+ test_files:
119
+ - spec/coder_spec.rb
120
+ - spec/map_spec.rb
121
+ - spec/nokogiri_spec.rb
122
+ - spec/object_group_spec.rb
123
+ - spec/spec_helper.rb