tmx 0.0.1

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.
@@ -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