Some type casts removed from the Router

Some type casts removed from the Router

Strict

Private

Import director
Import directorevents
Import keyevent
Import routerevents
Import touchevent

Public

Class Router Implements DirectorEvents
    Private

    Field handlers:StringMap<DirectorEvents> = New StringMap<DirectorEvents>()
    Field routers:StringMap<RouterEvents> = New StringMap<RouterEvents>()
    Field created:List<String> = New List<String>()
    Field _current:DirectorEvents
    Field _currentName:String
    Field _previous:DirectorEvents
    Field _previousName:String
    Field director:Director

    Public

    Method Add:Void(name:String, handler:DirectorEvents)
        If handlers.Contains(name)
            Error("Router already contains a handler named " + name)
        End

        handlers.Set(name, handler)
        routers.Set(name, RouterEvents(handler))
    End

    Method Get:DirectorEvents(name:String)
        If Not handlers.Contains(name)
            Error("Router has no handler named " + name)
        End

        Return handlers.Get(name)
    End

    Method Goto:Void(name:String)
        If name = _currentName Then Return

        _previous = _current
        _previousName = _currentName

        Local tmpRouter:RouterEvents = routers.Get(name)
        If tmpRouter Then tmpRouter.OnLeave()

        _current = Get(name)
        _currentName = name
        DispatchOnCreate()

        tmpRouter = routers.Get(name)
        If tmpRouter Then tmpRouter.OnEnter()
    End

    Method OnCreate:Void(director:Director)
        Self.director = director
        DispatchOnCreate()
    End

    Method OnLoading:Void()
        If _current Then _current.OnLoading()
    End

    Method OnUpdate:Void(delta:Float, frameTime:Float)
        If _current Then _current.OnUpdate(delta, frameTime)
    End

    Method OnRender:Void()
        If _current Then _current.OnRender()
    End

    Method OnSuspend:Void()
        If _current Then _current.OnSuspend()
    End

    Method OnResume:Void(delta:Int)
        If _current Then _current.OnResume(delta)
    End

    Method OnKeyDown:Void(event:KeyEvent)
        If _current Then _current.OnKeyDown(event)
    End

    Method OnKeyPress:Void(event:KeyEvent)
        If _current Then _current.OnKeyPress(event)
    End

    Method OnKeyUp:Void(event:KeyEvent)
        If _current Then _current.OnKeyUp(event)
    End

    Method OnTouchDown:Void(event:TouchEvent)
        If _current Then _current.OnTouchDown(event)
    End

    Method OnTouchMove:Void(event:TouchEvent)
        If _current Then _current.OnTouchMove(event)
    End

    Method OnTouchUp:Void(event:TouchEvent)
        If _current Then _current.OnTouchUp(event)
    End

    Method current:DirectorEvents() Property
        Return _current
    End

    Method currentName:String() Property
        Return _currentName
    End

    Method previous:DirectorEvents() Property
        Return _previous
    End

    Method previousName:String() Property
        Return _previousName
    End

    Private

    Method DispatchOnCreate:Void()
        If Not director Then Return
        If Not _current Then Return
        If created.Contains(_currentName) Then Return

        _current.OnCreate(director)
        created.AddLast(_currentName)
    End
End

Strict

Private

Import director
Import directorevents
Import keyevent
Import routerevents
Import touchevent

Public

Class Router Implements DirectorEvents
    Private

    Field handlers:StringMap<DirectorEvents> = New StringMap<DirectorEvents>()
    Field created:List<String> = New List<String>()
    Field _current:DirectorEvents
    Field _currentName:String
    Field _previous:DirectorEvents
    Field _previousName:String
    Field director:Director

    Public

    Method Add:Void(name:String, handler:DirectorEvents)
        If handlers.Contains(name)
            Error("Router already contains a handler named " + name)
        End

        handlers.Set(name, handler)
    End

    Method Get:DirectorEvents(name:String)
        If Not handlers.Contains(name)
            Error("Router has no handler named " + name)
        End

        Return handlers.Get(name)
    End

    Method Goto:Void(name:String)
        If name = _currentName Then Return

        _previous = _current
        _previousName = _currentName

        Local routerevents:RouterEvents = RouterEvents(_previous)
        If routerevents Then routerevents.OnLeave()

        _current = Get(name)
        _currentName = name
        DispatchOnCreate()

        routerevents = RouterEvents(_current)
        If routerevents Then routerevents.OnEnter()
    End

    Method OnCreate:Void(director:Director)
        Self.director = director
        DispatchOnCreate()
    End

    Method OnLoading:Void()
        If _current Then _current.OnLoading()
    End

    Method OnUpdate:Void(delta:Float, frameTime:Float)
        If _current Then _current.OnUpdate(delta, frameTime)
    End

    Method OnRender:Void()
        If _current Then _current.OnRender()
    End

    Method OnSuspend:Void()
        If _current Then _current.OnSuspend()
    End

    Method OnResume:Void(delta:Int)
        If _current Then _current.OnResume(delta)
    End

    Method OnKeyDown:Void(event:KeyEvent)
        If _current Then _current.OnKeyDown(event)
    End

    Method OnKeyPress:Void(event:KeyEvent)
        If _current Then _current.OnKeyPress(event)
    End

    Method OnKeyUp:Void(event:KeyEvent)
        If _current Then _current.OnKeyUp(event)
    End

    Method OnTouchDown:Void(event:TouchEvent)
        If _current Then _current.OnTouchDown(event)
    End

    Method OnTouchMove:Void(event:TouchEvent)
        If _current Then _current.OnTouchMove(event)
    End

    Method OnTouchUp:Void(event:TouchEvent)
        If _current Then _current.OnTouchUp(event)
    End

    Method current:DirectorEvents() Property
        Return _current
    End

    Method currentName:String() Property
        Return _currentName
    End

    Method previous:DirectorEvents() Property
        Return _previous
    End

    Method previousName:String() Property
        Return _previousName
    End

    Private

    Method DispatchOnCreate:Void()
        If Not director Then Return
        If Not _current Then return
        If created.Contains(_currentName) Then Return

        _current.OnCreate(director)
        created.AddLast(_currentName)
    End
End


Update flxcolor.monkey

Update flxcolor.monkey

Constructor is not private anymore.

Strict

Class FlxColor

Public
	Field r:Float
	Field g:Float
	Field b:Float
	Field a:Float
	Field argb:Int

Private
	Global _Color:FlxColor = New FlxColor()

	Global _HexTransform:IntMap<Int>

Public
	Method New (argb:Int = $FFFFFFFF)
		_ParseARGB(argb)
	End Method

	Method SetARGB:Void(argb:Int)
		_ParseARGB(argb)
	End Method

	Method SetARGB:Void(a:Float, r:Int, g:Int, b:Int)
		Self.a = a
		Self.r = r
		Self.g = g
		Self.b = b

		Self.argb = (a * 255 Shl 24) | (int(r) Shl 16) | (int(g) Shl 8) | int(b)
	End Method

	Method SetRGB:Void(rgb:Int)
		r = (rgb Shr 16) & $FF
		g = (rgb Shr 8) & $FF
		b = rgb & $FF
		a = 1

		Self.argb = rgb | ($FF Shl 24)
	End Method

	Method SetRGB:Void(a:Float, r:Int, g:Int, b:Int)
		SetARGB(1, r, g, b)
	End Method

	Method MixRGB:Void(fore:FlxColor)
		r = (r * fore.r) / 255.0
		g = (g * fore.g) / 255.0
		b = (b * fore.b) / 255.0
		a = 1

		Self.argb = ($FF Shl 24) | (int(r) Shl 16) | (int(g) Shl 8) | int(b)
	End Method

	Method MixRGB:Void(back:FlxColor, fore:FlxColor)
		r = (back.r * fore.r) / 255.0
		g = (back.g * fore.g) / 255.0
		b = (back.b * fore.b) / 255.0
		a = 1

		Self.argb = ($FF Shl 24) | (int(r) Shl 16) | (int(g) Shl 8) | int(b)
	End Method

	Method FromColor:FlxColor(color:FlxColor)
		r = color.r
		g = color.g
		b = color.b
		a = color.a
		argb = color.argb

		Return Self
	End Method

	Function ARGB:Int(argb:Int = $FFFFFFFF, result:FlxColor = Null)
		If (result = Null) result = _Color
		result._ParseARGB(argb)
		Return result.argb
	End Function

	Function RGB:Int(rgb:Int = $FFFFFF, result:FlxColor = Null)
		If (result = Null) result = _Color
		result.SetRGB(rgb)
		Return result.argb
	End Function

	Function FromString:Int(stringColor:String, result:FlxColor = Null)
		If (result = Null) result = _Color
		If (stringColor.Length() = 0) Return result.argb

		If (stringColor.StartsWith("$") Or stringColor.StartsWith("#")) Then
			If (_HexTransform = Null) _InitHextTransform()
			Local value:Int = 0
			Local product:Int = 1
			Local l:Int = stringColor.Length() -1

			For Local i:Int = l To 1 Step - 1
				value += _HexTransform.Get(stringColor[i]) * product
				product *= 16
			Next

			If (product = 16777216) Then
				result.SetRGB(value)
			Else
				result.SetARGB(value)
			End If
		ElseIf(stringColor.StartsWith("argb")) Then
			Local colors:String[] = stringColor[stringColor.Find("(") + 1 .. stringColor.Find(")")].Split(",")
			If (colors.Length() <> 4) Return result.argb

			Local alpha:Float

			If (colors[0].Find(".") >= 0) Then
				alpha = Float(colors[0].Trim())
			Else
				alpha = Int(colors[0].Trim()) / 255.0
			End If

			result.SetARGB(alpha, Int(colors[1].Trim()), Int(colors[2].Trim()), Int(colors[3].Trim()))

		ElseIf(stringColor.StartsWith("rgb")) Then
			Local colors:String[] = stringColor[stringColor.Find("(") + 1 .. stringColor.Find(")")].Split(",")
			If (colors.Length() <> 3) Return result.argb

			result.SetARGB(1, Int(colors[0].Trim()), Int(colors[1].Trim()), Int(colors[2].Trim()))
		End If

		Return result.argb
	End Function

Private
	Method _ParseARGB:Void(argb:Int)
		r = (argb Shr 16) & $FF
		g = (argb Shr 8) & $FF
		b = argb & $FF
		a = Float((argb Shr 24) & $FF) / 255
		Self.argb = argb
	End Method

	Function _InitHextTransform:Void()
		_HexTransform = New IntMap<Int>()

		Local value:Int = 0

		For Local i:Int = 48 To 57
			_HexTransform.Set(i, value)
			value += 1
		Next

		For Local i:Int = 65 To 70
			_HexTransform.Set(i, value)
			value += 1
		Next

		value -= 6

		For Local i:Int = 97 To 102
			_HexTransform.Set(i, value)
			value += 1
		Next
	End Function

End Class

Strict

Class FlxColor

Public
	Field r:Float
	Field g:Float
	Field b:Float
	Field a:Float
	Field argb:Int

Private
	Global _Color:FlxColor = New FlxColor()

	Global _HexTransform:IntMap<Int>

	Method New (argb:Int = $FFFFFFFF)
		_ParseARGB(argb)
	End Method

	Method SetARGB:Void(argb:Int)
		_ParseARGB(argb)
	End Method

	Method SetARGB:Void(a:Float, r:Int, g:Int, b:Int)
		Self.a = a
		Self.r = r
		Self.g = g
		Self.b = b

		Self.argb = (a * 255 Shl 24) | (int(r) Shl 16) | (int(g) Shl 8) | int(b)
	End Method

	Method SetRGB:Void(rgb:Int)
		r = (rgb Shr 16) & $FF
		g = (rgb Shr 8) & $FF
		b = rgb & $FF
		a = 1

		Self.argb = rgb | ($FF Shl 24)
	End Method

	Method SetRGB:Void(a:Float, r:Int, g:Int, b:Int)
		SetARGB(1, r, g, b)
	End Method

	Method MixRGB:Void(fore:FlxColor)
		r = (r * fore.r) / 255.0
		g = (g * fore.g) / 255.0
		b = (b * fore.b) / 255.0
		a = 1

		Self.argb = ($FF Shl 24) | (int(r) Shl 16) | (int(g) Shl 8) | int(b)
	End Method

	Method MixRGB:Void(back:FlxColor, fore:FlxColor)
		r = (back.r * fore.r) / 255.0
		g = (back.g * fore.g) / 255.0
		b = (back.b * fore.b) / 255.0
		a = 1

		Self.argb = ($FF Shl 24) | (int(r) Shl 16) | (int(g) Shl 8) | int(b)
	End Method

	Method FromColor:FlxColor(color:FlxColor)
		r = color.r
		g = color.g
		b = color.b
		a = color.a
		argb = color.argb

		Return Self
	End Method

	Function ARGB:Int(argb:Int = $FFFFFFFF, result:FlxColor = Null)
		If (result = Null) result = _Color
		result._ParseARGB(argb)
		Return result.argb
	End Function

	Function RGB:Int(rgb:Int = $FFFFFF, result:FlxColor = Null)
		If (result = Null) result = _Color
		result.SetRGB(rgb)
		Return result.argb
	End Function

	Function FromString:Int(stringColor:String, result:FlxColor = Null)
		If (result = Null) result = _Color
		If (stringColor.Length() = 0) Return result.argb

		If (stringColor.StartsWith("$") Or stringColor.StartsWith("#")) Then
			If (_HexTransform = Null) _InitHextTransform()
			Local value:Int = 0
			Local product:Int = 1
			Local l:Int = stringColor.Length() -1

			For Local i:Int = l To 1 Step - 1
				value += _HexTransform.Get(stringColor[i]) * product
				product *= 16
			Next

			If (product = 16777216) Then
				result.SetRGB(value)
			Else
				result.SetARGB(value)
			End If
		ElseIf(stringColor.StartsWith("argb")) Then
			Local colors:String[] = stringColor[stringColor.Find("(") + 1 .. stringColor.Find(")")].Split(",")
			If (colors.Length() <> 4) Return result.argb

			Local alpha:Float

			If (colors[0].Find(".") >= 0) Then
				alpha = Float(colors[0].Trim())
			Else
				alpha = Int(colors[0].Trim()) / 255.0
			End If

			result.SetARGB(alpha, Int(colors[1].Trim()), Int(colors[2].Trim()), Int(colors[3].Trim()))

		ElseIf(stringColor.StartsWith("rgb")) Then
			Local colors:String[] = stringColor[stringColor.Find("(") + 1 .. stringColor.Find(")")].Split(",")
			If (colors.Length() <> 3) Return result.argb

			result.SetARGB(1, Int(colors[0].Trim()), Int(colors[1].Trim()), Int(colors[2].Trim()))
		End If

		Return result.argb
	End Function

Private
	Method _ParseARGB:Void(argb:Int)
		r = (argb Shr 16) & $FF
		g = (argb Shr 8) & $FF
		b = argb & $FF
		a = Float((argb Shr 24) & $FF) / 255
		Self.argb = argb
	End Method

	Function _InitHextTransform:Void()
		_HexTransform = New IntMap<Int>()

		Local value:Int = 0

		For Local i:Int = 48 To 57
			_HexTransform.Set(i, value)
			value += 1
		Next

		For Local i:Int = 65 To 70
			_HexTransform.Set(i, value)
			value += 1
		Next

		value -= 6

		For Local i:Int = 97 To 102
			_HexTransform.Set(i, value)
			value += 1
		Next
	End Function

End Class


Added Reseed() method to RandomSource to allow the developer to customise the random spread.  Note that this is still based on the source's original seed, not a system timer.

#Rem
Copyright (c) 2011 Steve Revill and Shane Woolcock
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#End

Function LinearTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	Return diff * t / d + b
End

Function QuadTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	t /= d / 2
	If t < 1 Return diff / 2 * t * t + b
	t -= 1
	Return -diff / 2 * (t * (t - 2) - 1) + b
End

Function QuinticTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	t /= d / 2
	If (t < 1) Return diff / 2 * t * t * t * t * t + b
	t -= 2
	Return diff / 2 * (t * t * t * t * t + 2) + b
End

Function TweenCalc:Float(p1:Float, p2:Float, t:Float)
	Return p1 + t * (p2 - p1)
End

Function TweenSmooth:Float(p1:Float, p2:Float, t:Float)
	Local v:Float = SmoothStep(t)
	Return p1 + v * (p2 - p1)
End

Function SmoothStep:Float(x:Float, interpSmooth:Int = 1)
	For Local i:Int = 0 Until interpSmooth
		x *= x * (3 - 2 * x)
	Next

	Return x
End

Function TweenUp:Float(p1:Float, p2:Float, t:Float)
	Local v:Float = SmoothStep(t)
	v = Pow(v, 2) 'power of 2.
	Return p1 + v * (p2 - p1)
End

Function TweenDown:Float(p1:Float, p2:Float, t:Float)
	Local v:Float = SmoothStep(t)
	v = 1 - Pow(1 - v, 2) 'InvSquared
	Return p1 + v * (p2 - p1)
End

Function Bezier:Float(p1:Float, p2:Float, cp1:Float, cp2:Float, t:Float)
	Local v:Float = p1 * Pow( (1 - t), 3) + 3 * cp1 * Pow( (1 - t), 2) * t + 3 * cp2 * (1 - t) * Pow(t, 2) + p2 * Pow(t, 3)
	Return v
End

Function EaseInBounceTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	Return diff - EaseOutBounceTween(d - t, 0, diff, d) + b
End

Function EaseOutBounceTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	t /= d
	If (t < (1 / 2.75))
		Return diff * (7.5625 * t * t) + b
	ElseIf(t < (2 / 2.75))
		t -= (1.5 / 2.75)
		Return diff * (7.5625 * t * t + 0.75) + b
	ElseIf(t < (2.5 / 2.75))
		t -= (2.25 / 2.75)
		Return diff * (7.5625 * t * t + 0.9375) + b
	Else
		t -= (2.625 / 2.75)
		Return diff * (7.5625 * t * t + 0.984375) + b
	End
End

Function EaseInOutBounceTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	If (t < d / 2)
		Return EaseInBounceTween(t * 2, 0, diff, d) * 0.5 + b
	End
	Return EaseOutBounceTween(t * 2 - d, 0, diff, d) * 0.5 + diff * 0.5 + b
End

Function BackEaseInTween:Float(b:Float, c:Float, t:Float, d:Float = 1, s:Float = 2)
	Local diff:Float = c - b
	t /= d
	Return diff * t * t * ( (s + 1) * t - s) + b
End

Function BackEaseOutTween:Float(b:Float, c:Float, t:Float, d:Float = 1, s:Float = 2)
	Local diff:Float = c - b
	t = t / d - 1
	Return diff * (t * t * ( (s + 1) * t + s) + 1) + b
End

Function BackEaseInOutTween:Float(b:Float, c:Float, t:Float, d:Float = 1, s:Float = 2)
	Local diff:Float = c - b
	Local s2:Float = s
	t /= d / 2
	s2 *= 1.525
	If t < 1
		Return diff / 2 * (t * t * ( (s2 + 1) * t - s2)) + b
	End
	t -= 2
	Return diff / 2 * (t * t * ( (s2 + 1) * t + s2) + 2) + b
End

Function IntToRoman:String(input:Int)
	Local s:String = ""
    While input >= 1000
        s += "M"
        input -= 1000
	End
    While input >= 900
        s += "CM"
        input -= 900
	End
    While input >= 500
        s += "D"
        input -= 500
    End
    While input >= 400
        s += "CD"
        input -= 400
    End
    While input >= 100
        s += "C"
        input -= 100
    End
    While input >= 90
        s += "XC"
        input -= 90
    End
    While input >= 50
        s += "L"
        input -= 50
    End
    While input >= 40
        s += "XL"
        input -= 40
    End
    While input >= 10
        s += "X"
        input -= 10
    End
    While input >= 9
        s += "IX"
        input -= 9
    End
    While input >= 5
        s += "V"
        input -= 5
    End
    While input >= 4
        s += "IV"
        input -= 4
    End
    While input >= 1
        s += "I"
        input -= 1
    End
    Return s
End

Function RomanToInt:Int(input:String)
	Local last:Int = 1
	Local this:Int = 1
	Local rv:Int = 0
	For Local i:Int = input.Length-1 To 0 Step -1
		last = this
		Select input[i]
			Case "I"[0]
				this = 1
			Case "V"[0]
				this = 5
			Case "X"[0]
				this = 10
			Case "L"[0]
				this = 50
			Case "C"[0]
				this = 100
			Case "D"[0]
				this = 500
			Case "M"[0]
				this = 1000
		End
		If last > this Then rv -= this Else rv += this
	Next
	Return rv
End

Class RandomSource
Private
	Field currentSeed:Int
	Field snapshotSeed:Int

	Method NextRnd:Float()
		' cache global seed
		Local lastSeed:Int = Seed
		' update it to be ours if it's not the system one
		If Self <> SystemSource Then Seed = Self.currentSeed
		' call global Rnd function
		Local thisRnd:Float = Rnd()
		' update our seed
		Self.currentSeed = Seed
		' reset global seed if it's not the system one
		If Self <> SystemSource Then Seed = lastSeed
		' return the random float
		Return thisRnd
	End

Public
	Global SystemSource:RandomSource = New RandomSource

	Method CurrentSeed:Int() Property Return currentSeed End
	Method CurrentSeed:Void(currentSeed:Int) Property Self.currentSeed = currentSeed End

	Method New()
		currentSeed = Seed
		snapshotSeed = currentSeed
	End

	Method New(seed:Int)
		currentSeed = seed
		snapshotSeed = currentSeed
	End

	Method New(other:RandomSource)
		currentSeed = other.CurrentSeed
		snapshotSeed = currentSeed
	End

	Method NextInt:Int()
		Return (NextInt($10000) Shl 16) | NextInt($10000)
	End

	Method NextInt:Int(n:Int)
		Return Int(NextRnd() * n)
	End

	Method NextInt:Int(low:Int, high:Int)
		Return low + Int(NextRnd() * (high-low+1))
	End

	Method NextFloat:Float()
		Return NextRnd()
	End

	Method NextBool:Bool()
		Return NextRnd() >= 0.5
	End

	Method Snapshot:Void()
		snapshotSeed = currentSeed
	End

	Method Rollback:Void()
		currentSeed = snapshotSeed
	End

	Method Reseed:Void()
		currentSeed = NextInt()
	End
End

#Rem
Copyright (c) 2011 Steve Revill and Shane Woolcock
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#End

Function LinearTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	Return diff * t / d + b
End

Function QuadTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	t /= d / 2
	If t < 1 Return diff / 2 * t * t + b
	t -= 1
	Return -diff / 2 * (t * (t - 2) - 1) + b
End

Function QuinticTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	t /= d / 2
	If (t < 1) Return diff / 2 * t * t * t * t * t + b
	t -= 2
	Return diff / 2 * (t * t * t * t * t + 2) + b
End

Function TweenCalc:Float(p1:Float, p2:Float, t:Float)
	Return p1 + t * (p2 - p1)
End

Function TweenSmooth:Float(p1:Float, p2:Float, t:Float)
	Local v:Float = SmoothStep(t)
	Return p1 + v * (p2 - p1)
End

Function SmoothStep:Float(x:Float, interpSmooth:Int = 1)
	For Local i:Int = 0 Until interpSmooth
		x *= x * (3 - 2 * x)
	Next

	Return x
End

Function TweenUp:Float(p1:Float, p2:Float, t:Float)
	Local v:Float = SmoothStep(t)
	v = Pow(v, 2) 'power of 2.
	Return p1 + v * (p2 - p1)
End

Function TweenDown:Float(p1:Float, p2:Float, t:Float)
	Local v:Float = SmoothStep(t)
	v = 1 - Pow(1 - v, 2) 'InvSquared
	Return p1 + v * (p2 - p1)
End

Function Bezier:Float(p1:Float, p2:Float, cp1:Float, cp2:Float, t:Float)
	Local v:Float = p1 * Pow( (1 - t), 3) + 3 * cp1 * Pow( (1 - t), 2) * t + 3 * cp2 * (1 - t) * Pow(t, 2) + p2 * Pow(t, 3)
	Return v
End

Function EaseInBounceTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	Return diff - EaseOutBounceTween(d - t, 0, diff, d) + b
End

Function EaseOutBounceTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	t /= d
	If (t < (1 / 2.75))
		Return diff * (7.5625 * t * t) + b
	ElseIf(t < (2 / 2.75))
		t -= (1.5 / 2.75)
		Return diff * (7.5625 * t * t + 0.75) + b
	ElseIf(t < (2.5 / 2.75))
		t -= (2.25 / 2.75)
		Return diff * (7.5625 * t * t + 0.9375) + b
	Else
		t -= (2.625 / 2.75)
		Return diff * (7.5625 * t * t + 0.984375) + b
	End
End

Function EaseInOutBounceTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	If (t < d / 2)
		Return EaseInBounceTween(t * 2, 0, diff, d) * 0.5 + b
	End
	Return EaseOutBounceTween(t * 2 - d, 0, diff, d) * 0.5 + diff * 0.5 + b
End

Function BackEaseInTween:Float(b:Float, c:Float, t:Float, d:Float = 1, s:Float = 2)
	Local diff:Float = c - b
	t /= d
	Return diff * t * t * ( (s + 1) * t - s) + b
End

Function BackEaseOutTween:Float(b:Float, c:Float, t:Float, d:Float = 1, s:Float = 2)
	Local diff:Float = c - b
	t = t / d - 1
	Return diff * (t * t * ( (s + 1) * t + s) + 1) + b
End

Function BackEaseInOutTween:Float(b:Float, c:Float, t:Float, d:Float = 1, s:Float = 2)
	Local diff:Float = c - b
	Local s2:Float = s
	t /= d / 2
	s2 *= 1.525
	If t < 1
		Return diff / 2 * (t * t * ( (s2 + 1) * t - s2)) + b
	End
	t -= 2
	Return diff / 2 * (t * t * ( (s2 + 1) * t + s2) + 2) + b
End

Function IntToRoman:String(input:Int)
	Local s:String = ""
    While input >= 1000
        s += "M"
        input -= 1000
	End
    While input >= 900
        s += "CM"
        input -= 900
	End
    While input >= 500
        s += "D"
        input -= 500
    End
    While input >= 400
        s += "CD"
        input -= 400
    End
    While input >= 100
        s += "C"
        input -= 100
    End
    While input >= 90
        s += "XC"
        input -= 90
    End
    While input >= 50
        s += "L"
        input -= 50
    End
    While input >= 40
        s += "XL"
        input -= 40
    End
    While input >= 10
        s += "X"
        input -= 10
    End
    While input >= 9
        s += "IX"
        input -= 9
    End
    While input >= 5
        s += "V"
        input -= 5
    End
    While input >= 4
        s += "IV"
        input -= 4
    End
    While input >= 1
        s += "I"
        input -= 1
    End
    Return s
End

Function RomanToInt:Int(input:String)
	Local last:Int = 1
	Local this:Int = 1
	Local rv:Int = 0
	For Local i:Int = input.Length-1 To 0 Step -1
		last = this
		Select input[i]
			Case "I"[0]
				this = 1
			Case "V"[0]
				this = 5
			Case "X"[0]
				this = 10
			Case "L"[0]
				this = 50
			Case "C"[0]
				this = 100
			Case "D"[0]
				this = 500
			Case "M"[0]
				this = 1000
		End
		If last > this Then rv -= this Else rv += this
	Next
	Return rv
End

Class RandomSource
Private
	Field currentSeed:Int
	Field snapshotSeed:Int

	Method NextRnd:Float()
		' cache global seed
		Local lastSeed:Int = Seed
		' update it to be ours if it's not the system one
		If Self <> SystemSource Then Seed = Self.currentSeed
		' call global Rnd function
		Local thisRnd:Float = Rnd()
		' update our seed
		Self.currentSeed = Seed
		' reset global seed if it's not the system one
		If Self <> SystemSource Then Seed = lastSeed
		' return the random float
		Return thisRnd
	End

Public
	Global SystemSource:RandomSource = New RandomSource

	Method CurrentSeed:Int() Property Return currentSeed End
	Method CurrentSeed:Void(currentSeed:Int) Property Self.currentSeed = currentSeed End

	Method New()
		currentSeed = Seed
		snapshotSeed = currentSeed
	End

	Method New(seed:Int)
		currentSeed = seed
		snapshotSeed = currentSeed
	End

	Method New(other:RandomSource)
		currentSeed = other.CurrentSeed
		snapshotSeed = currentSeed
	End

	Method NextInt:Int()
		Return (NextInt($10000) Shl 16) | NextInt($10000)
	End

	Method NextInt:Int(n:Int)
		Return Int(NextRnd() * n)
	End

	Method NextInt:Int(low:Int, high:Int)
		Return low + Int(NextRnd() * (high-low+1))
	End

	Method NextFloat:Float()
		Return NextRnd()
	End

	Method NextBool:Bool()
		Return NextRnd() >= 0.5
	End

	Method Snapshot:Void()
		snapshotSeed = currentSeed
	End

	Method Rollback:Void()
		currentSeed = snapshotSeed
	End
End


The tile module can now be used standalone.  When loading tilesets, it tries LoadImage if the screen framework isn't in use, or if it can't find or load the image from the image bank.

#Rem
Copyright (c) 2011 Steve Revill and Shane Woolcock
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#End

Strict

Import diddy.base64
Import monkey.map
Import diddy.containers
Import diddy.framework
Import diddy.xml

' TileMapPropertyContainer
' Classes that extend this will automatically instantiate a property container and expose it.
Class TileMapPropertyContainer
Private
	Field properties:TileMapProperties = New TileMapProperties

Public
	Method Properties:TileMapProperties() Property
		Return properties
	End
End

' ITileMapPostLoad
' Classes that implement this interface will have PostLoad automatically called once the object has been created.
Interface ITileMapPostLoad
	Method PostLoad:Void()
End

' TileMapReader
' This can be extended to handle any map file format.
Class TileMapReader Abstract
	Field tileMap:TileMap
	Field graphicsPath:String

	Method LoadMap:TileMap(filename:String) Abstract

	' override this to create a custom tilemap class
	Method CreateMap:TileMap()
		Return New TileMap
	End
End

' TiledTileMapReader
' Extends TileMapReader to add support for the Tiled map editor.
Class TiledTileMapReader Extends TileMapReader
	Field doc:XMLDocument

	' Overrides TileMapReader
	Method LoadMap:TileMap(filename:String)
		' open file and get root node
		Local parser:XMLParser = New XMLParser
		Local xmlString:String = LoadString(filename)
		' error if we couldnt load the file
		If Not xmlString
			AssertError("Cannot load tile map file " + filename + ". Ensure you have TMX in the in the allowed #TEXT_FILES")
		End
		' look for the data encoding, if we cant find it assume its RAW XML and thats just too slow!
		Local findData:Int = xmlString.Find("<data encoding")
		If findData = -1
			AssertError("Tiled Raw XML is not supported!")
		End

		doc = parser.ParseString(xmlString)
		Return ReadMap(doc.Root)
	End

	Method ReadMap:TileMap(node:XMLElement)
		tileMap = CreateMap()
		ReadProperties(node, tileMap)

		' extract map properties
		If tileMap.properties.Has(PROP_MAP_WRAP_X) Then tileMap.wrapX = tileMap.properties.Get(PROP_MAP_WRAP_X).GetBool()
		If tileMap.properties.Has(PROP_MAP_WRAP_Y) Then tileMap.wrapY = tileMap.properties.Get(PROP_MAP_WRAP_Y).GetBool()

		' read root node's attributes
		If node.HasAttribute(ATTR_MAP_VERSION) Then tileMap.version = node.GetAttribute(ATTR_MAP_VERSION)
		If node.HasAttribute(ATTR_MAP_ORIENTATION) Then tileMap.orientation = node.GetAttribute(ATTR_MAP_ORIENTATION)
		If node.HasAttribute(ATTR_MAP_WIDTH) Then tileMap.width = Int(node.GetAttribute(ATTR_MAP_WIDTH))
		If node.HasAttribute(ATTR_MAP_HEIGHT) Then tileMap.height = Int(node.GetAttribute(ATTR_MAP_HEIGHT))
		If node.HasAttribute(ATTR_MAP_TILEWIDTH) Then tileMap.tileWidth = Int(node.GetAttribute(ATTR_MAP_TILEWIDTH))
		If node.HasAttribute(ATTR_MAP_TILEHEIGHT) Then tileMap.tileHeight = Int(node.GetAttribute(ATTR_MAP_TILEHEIGHT))

		tileMap.maxTileWidth = tileMap.tileWidth
		tileMap.maxTileHeight = tileMap.tileHeight

		' parse children
		If Not node.Children.IsEmpty() Then
			For Local mapchild:XMLElement = Eachin node.Children
				' tileset
				If mapchild.Name = NODE_TILESET Then
					Local ts:TileMapTileset = ReadTileset(mapchild)
					tileMap.tilesets.Set(ts.name, ts)

				' tile layer
				Elseif mapchild.Name = NODE_LAYER Then
					Local layer:TileMapLayer = ReadTileLayer(mapchild)
					tileMap.layers.Push(layer)

				' object layer
				Elseif mapchild.Name = NODE_OBJECTGROUP Then
					Local layer:TileMapLayer = ReadObjectLayer(mapchild)
					tileMap.layers.Push(layer)
				Endif
			Next
		Endif

		DoPostLoad(tileMap)

		Return tileMap
	End

	Method DoPostLoad:Void(obj:Object)
		If ITileMapPostLoad(obj) <> Null Then ITileMapPostLoad(obj).PostLoad()
	End

	Method ReadProperties:Void(node:XMLElement, obj:Object)
		Local cont:TileMapPropertyContainer = TileMapPropertyContainer(obj)
		If cont <> Null Then
			For Local propNode:XMLElement = Eachin node.Children
				If propNode.Name = NODE_PROPERTIES Then
					For Local child:XMLElement = Eachin propNode.Children
						If child.Name = NODE_PROPERTY Then
							Local prop:TileMapProperty = ReadProperty(child)
							cont.properties.props.Set(prop.name, prop)
						Endif
					Next
					Return
				End
			Next
		End
	End

	Method ReadProperty:TileMapProperty(node:XMLElement)
		Return New TileMapProperty(node.GetAttribute(ATTR_PROPERTY_NAME, "default"), node.GetAttribute(ATTR_PROPERTY_VALUE, ""))
	End

	Method ReadTileset:TileMapTileset(node:XMLElement, target:TileMapTileset=Null)
		Local rv:TileMapTileset = target
		ReadProperties(node, rv)
		If rv = Null Then rv = tileMap.CreateTileset()
		If node.HasAttribute(ATTR_TILESET_FIRSTGID) Then rv.firstGid = Int(node.GetAttribute(ATTR_TILESET_FIRSTGID))

		If node.HasAttribute(ATTR_TILESET_SOURCE) Then
			rv.source = node.GetAttribute(ATTR_TILESET_SOURCE)
			Local parser:XMLParser = New XMLParser
			Local tilesetdoc:XMLDocument = parser.ParseFile(rv.source)
			Return ReadTileset(tilesetdoc.Root, rv)
		Else
			If node.HasAttribute(ATTR_TILESET_NAME) Then rv.name = node.GetAttribute(ATTR_TILESET_NAME)
			If node.HasAttribute(ATTR_TILESET_TILEWIDTH) Then rv.tileWidth = Int(node.GetAttribute(ATTR_TILESET_TILEWIDTH))
			If node.HasAttribute(ATTR_TILESET_TILEHEIGHT) Then rv.tileHeight = Int(node.GetAttribute(ATTR_TILESET_TILEHEIGHT))
			If node.HasAttribute(ATTR_TILESET_SPACING) Then rv.spacing = Int(node.GetAttribute(ATTR_TILESET_SPACING))
			If node.HasAttribute(ATTR_TILESET_MARGIN) Then rv.margin = Int(node.GetAttribute(ATTR_TILESET_MARGIN))

			If Not node.Children.IsEmpty() Then
				For Local child:XMLElement = Eachin node.Children
					If child.Name = NODE_IMAGE Then
						rv.imageNode = ReadImage(child)
					Elseif child.Name = NODE_TILE Then
						rv.tileNodes.Push(ReadTile(child))
					End
				Next
			End
		End
		DoPostLoad(rv)
		Return rv
	End

	Method ReadLayerAttributes:Void(node:XMLElement, layer:TileMapLayer)
		If node.HasAttribute(ATTR_LAYER_NAME) Then layer.name = node.GetAttribute(ATTR_LAYER_NAME)
		If node.HasAttribute(ATTR_LAYER_WIDTH) Then layer.width = Int(node.GetAttribute(ATTR_LAYER_WIDTH))
		If node.HasAttribute(ATTR_LAYER_HEIGHT) Then layer.height = Int(node.GetAttribute(ATTR_LAYER_HEIGHT))
		layer.visible = Not node.HasAttribute(ATTR_LAYER_VISIBLE) Or Int(node.GetAttribute(ATTR_LAYER_VISIBLE)) <> 0
		If node.HasAttribute(ATTR_LAYER_OPACITY) Then layer.opacity = Float(node.GetAttribute(ATTR_LAYER_OPACITY))
	End

	Method ReadTileLayer:TileMapTileLayer(node:XMLElement)
		Local rv:TileMapTileLayer = tileMap.CreateTileLayer()
		ReadProperties(node, rv)
		ReadLayerAttributes(node, rv)

		If rv.properties.Has(PROP_LAYER_PARALLAX_OFFSET_X) Then rv.parallaxOffsetX = rv.properties.Get(PROP_LAYER_PARALLAX_OFFSET_X).GetFloat()
		If rv.properties.Has(PROP_LAYER_PARALLAX_OFFSET_Y) Then rv.parallaxOffsetY = rv.properties.Get(PROP_LAYER_PARALLAX_OFFSET_Y).GetFloat()
		If rv.properties.Has(PROP_LAYER_PARALLAX_SCALE_X) Then rv.parallaxScaleX = rv.properties.Get(PROP_LAYER_PARALLAX_SCALE_X).GetFloat()
		If rv.properties.Has(PROP_LAYER_PARALLAX_SCALE_Y) Then rv.parallaxScaleY = rv.properties.Get(PROP_LAYER_PARALLAX_SCALE_Y).GetFloat()

		For Local child:XMLElement = Eachin node.Children
			If child.Name = NODE_DATA Then
				rv.mapData = ReadTileData(child, rv)
			End
		Next

		DoPostLoad(rv)
		Return rv
	End

	Method ReadObjectLayer:TileMapObjectLayer(node:XMLElement)
		Local rv:TileMapObjectLayer = tileMap.CreateObjectLayer()
		ReadProperties(node, rv)
		ReadLayerAttributes(node, rv)

		If node.HasAttribute(ATTR_OBJECTGROUP_COLOR) Then rv.color = ColorToInt(node.GetAttribute(ATTR_OBJECTGROUP_COLOR))

		For Local child:XMLElement = Eachin node.Children
			If child.Name = NODE_OBJECT Then
				rv.objects.Push(ReadObject(child, rv))
			End
		Next

		DoPostLoad(rv)
		Return rv
	End

	Method ReadImage:TileMapImage(node:XMLElement)
		Local rv:TileMapImage = tileMap.CreateImage()
		ReadProperties(node, rv)

		If node.HasAttribute(ATTR_IMAGE_SOURCE) Then rv.source = graphicsPath + StripDir(node.GetAttribute(ATTR_IMAGE_SOURCE))
		If node.HasAttribute(ATTR_IMAGE_WIDTH) Then rv.width = Int(node.GetAttribute(ATTR_IMAGE_WIDTH))
		If node.HasAttribute(ATTR_IMAGE_HEIGHT) Then rv.height = Int(node.GetAttribute(ATTR_IMAGE_HEIGHT))
		If node.HasAttribute(ATTR_IMAGE_TRANS) Then rv.trans = node.GetAttribute(ATTR_IMAGE_TRANS)
		If rv.trans.Length > 0 Then
			rv.transR = HexToDec(rv.trans[0..2])
			rv.transG = HexToDec(rv.trans[2..4])
			rv.transB = HexToDec(rv.trans[4..6])
		Endif

		DoPostLoad(rv)
		Return rv
	End

	Method ReadTile:TileMapTile(node:XMLElement)
		Local id:Int = Int(node.GetAttribute(ATTR_TILE_ID, "0"))
		Local rv:TileMapTile = tileMap.CreateTile(id)
		ReadProperties(node, rv)
		DoPostLoad(rv)
		Return rv
	End

	Method ReadObject:TileMapObject(node:XMLElement, layer:TileMapObjectLayer)
		Local rv:TileMapObject = tileMap.CreateObject()
		ReadProperties(node, rv)
		If node.HasAttribute(ATTR_OBJECT_NAME) Then rv.name = node.GetAttribute(ATTR_OBJECT_NAME)
		If node.HasAttribute(ATTR_OBJECT_TYPE) Then rv.objectType = node.GetAttribute(ATTR_OBJECT_TYPE)
		If node.HasAttribute(ATTR_OBJECT_X) Then rv.x = Int(node.GetAttribute(ATTR_OBJECT_X))
		If node.HasAttribute(ATTR_OBJECT_Y) Then rv.y = Int(node.GetAttribute(ATTR_OBJECT_Y))
		If node.HasAttribute(ATTR_OBJECT_WIDTH) Then rv.width = Int(node.GetAttribute(ATTR_OBJECT_WIDTH))
		If node.HasAttribute(ATTR_OBJECT_HEIGHT) Then rv.height = Int(node.GetAttribute(ATTR_OBJECT_HEIGHT))
		DoPostLoad(rv)
		Return rv
	End

	Method ReadTileData:TileMapData(node:XMLElement, layer:TileMapTileLayer)
		Local rv:TileMapData = tileMap.CreateData(layer.width, layer.height)

		' default to raw xml (ugly)
		Local encoding$ = DATA_ENCODING_RAW
		If node.HasAttribute(ATTR_DATA_ENCODING) Then encoding = node.GetAttribute(ATTR_DATA_ENCODING)
		If encoding = DATA_ENCODING_RAW Then
			' TODO: raw xml
			AssertError("Raw xml is currently not supported")
		Elseif encoding = DATA_ENCODING_CSV Then
			Local csv:String[] = node.Value.Split(",")
			For Local i% = 0 Until csv.Length
				Local gid:Int = Int(csv[i].Trim())
				rv.tiles[i] = gid
				rv.cells[i] = tileMap.CreateCell(gid, i Mod rv.width, i / rv.width)
			Next
		Elseif encoding = DATA_ENCODING_BASE64 Then
			Local bytes:Int[] = DecodeBase64Bytes(node.Value)
			If node.HasAttribute(ATTR_DATA_COMPRESSION) Then
				' TODO: compression
				AssertError("Compression is currently not supported")
			End
			For Local i% = 0 Until bytes.Length Step 4
				' little endian
				Local gid% = bytes[i]
				gid += bytes[i + 1] Shl 8
				gid += bytes[i + 2] Shl 16
				gid += bytes[i + 3] Shl 24
				rv.tiles[i / 4] = gid
				rv.cells[i / 4] = tileMap.CreateCell(gid, (i / 4) Mod rv.width, (i / 4) / rv.width)
			Next
		End
		Return rv
	End
End

' TileMapProperties
' Container for properties.
Class TileMapProperties
	Field props:StringMap<TileMapProperty> = New StringMap<TileMapProperty>

	Method Has:Bool(name:String)
		Return props.Contains(name)
	End

	Method Get:TileMapProperty(name:String)
		Return props.Get(name)
	End

	Method Set:Void(name:String, prop:TileMapProperty)
		props.Set(name, prop)
	End
End

' TileMap
' The main Map class.
Class TileMap Extends TileMapPropertyContainer Implements ITileMapPostLoad

	' attributes
	Field version:String = "1.0"
	Field orientation:String = MAP_ORIENTATION_ORTHOGONAL
	Field width:Int
	Field height:Int
	Field tileWidth:Int = 32
	Field tileHeight:Int = 32

	' children
	Field tilesets:StringMap<TileMapTileset> = New StringMap<TileMapTileset>
	Field layers:DiddyStack<TileMapLayer> = New DiddyStack<TileMapLayer>

	' post-load
	Field layerNames:StringMap<TileMapLayer> = New StringMap<TileMapLayer>
	Field tiles:TileMapTile[]
	Field maxTileWidth:Int
	Field maxTileHeight:Int
	Field wrapX:Bool = False
	Field wrapY:Bool = False

	' optimisation
	Field layerArray:TileMapLayer[] = []
	Field animatedTiles:TileMapTile[]

	' summary: override this to do something before a layer is rendered
	Method PreRenderLayer:Void(tileLayer:TileMapLayer)
		ConfigureLayer(tileLayer) ' call the deprecated one
	End

	' summary: override this to do something after a layer is rendered
	Method PostRenderLayer:Void(tileLayer:TileMapLayer)
	End

	' summary: override this to do something before the map is rendered
	Method PreRenderMap:Void()
	End

	' summary: override this to do something after the map is rendered
	Method PostRenderMap:Void()
	End

	' summary: override this to configure a layer (called on every render)
	' deprecated, override PreRenderLayer instead
	Method ConfigureLayer:Void(tileLayer:TileMapLayer)
	End

	' summary: override this to draw a tile
	Method DrawTile:Void(tileLayer:TileMapTileLayer, mapTile:TileMapTile, x:Int, y:Int)
		' default draw call
		If mapTile.image Then
			mapTile.image.DrawTile(x, y, mapTile.id, 0, 1, 1)
		ElseIf mapTile.rawImage Then
			DrawImageRect(mapTile.rawImage, x, y, mapTile.srcX, mapTile.srcY, mapTile.width, mapTile.height, 0, 1, 1)
		End
	End

	' summary: override this to create a custom tile class
	Method CreateTile:TileMapTile(id:Int)
		Return New TileMapTile(id)
	End

	' summary: override this to create a custom tileset class
	Method CreateTileset:TileMapTileset()
		Return New TileMapTileset
	End

	' summary: override this to create a custom tile layer class
	Method CreateTileLayer:TileMapTileLayer()
		Return New TileMapTileLayer
	End

	' summary: override this to create a custom object layer class
	Method CreateObjectLayer:TileMapObjectLayer()
		Return New TileMapObjectLayer
	End

	' summary: override this to create a custom image class
	Method CreateImage:TileMapImage()
		Return New TileMapImage
	End

	' summary: override this to create a custom object class
	Method CreateObject:TileMapObject()
		Return New TileMapObject
	End

	' summary: override this to create a custom cell class
	Method CreateCell:TileMapCell(gid:Int, x:Int, y:Int)
		Return New TileMapCell(gid, x, y)
	End

	' summary: override this to create a custom data class
	Method CreateData:TileMapData(width:Int, height:Int)
		Return New TileMapData(width, height)
	End

	' summary: override this to perform additional post-loading functionality (remember to call Super.PostLoad() first)
	Method PostLoad:Void()
		Local totaltiles% = 0, ts:TileMapTileset
		Local alltiles:DiddyStack<TileMapTile> = New DiddyStack<TileMapTile>
		For Local ts:TileMapTileset = Eachin tilesets.Values()
			' try to load the image from the image bank if we're using the framework
			If diddyGame Then ts.image = diddyGame.images.LoadTileset(ts.imageNode.source, ts.tileWidth, ts.tileHeight, ts.margin, ts.spacing, "", False, True)
			If ts.image Then
				' get the cell count
				ts.tileCount = ts.image.tileCount
				ts.tileCountX = ts.image.tileCountX
				ts.tileCountY = ts.image.tileCountY
			Else
				' if we couldn't get the image, load it with LoadImage
				ts.rawImage = LoadImage(ts.imageNode.source)
				If ts.rawImage Then
					ts.tileCountX = (ts.rawImage.Width() - ts.margin) / (ts.tileWidth + ts.spacing)
					ts.tileCountY = (ts.rawImage.Height() - ts.margin) / (ts.tileHeight + ts.spacing)
					ts.tileCount = ts.tileCountX * ts.tileCountY
				Else
					Print "Couldn't find tileset image: "+ts.imageNode.source
					Return
				End
			End

			' update max tile size
			If maxTileWidth < ts.tileWidth Then maxTileWidth = ts.tileWidth
			If maxTileHeight < ts.tileHeight Then maxTileHeight = ts.tileHeight

			' build tile list
			ts.tiles = New TileMapTile[ts.tileCount]
			For Local t:TileMapTile = Eachin ts.tileNodes
				ts.tiles[t.id] = t
			Next
			For Local i% = 0 Until ts.tiles.Length
				If ts.tiles[i] = Null Then
					ts.tiles[i] = CreateTile(i)
				End
				ts.tiles[i].gid = ts.firstGid + i
				ts.tiles[i].image = ts.image
				ts.tiles[i].rawImage = ts.rawImage
				ts.tiles[i].width = ts.tileWidth
				ts.tiles[i].height = ts.tileHeight
				' if we're using the raw image we need to precache some stuff
				If ts.rawImage Then
					ts.tiles[i].srcX = ts.margin + (ts.tileWidth + ts.spacing) * (i Mod ts.tileCountX)
					ts.tiles[i].srcY = ts.margin + (ts.tileHeight + ts.spacing) * (i / ts.tileCountX)
				End
				alltiles.Push(ts.tiles[i])
			Next
			' update total tiles
			totaltiles += ts.tileCount
		Next

		' make our cache
		tiles = New TileMapTile[totaltiles]
		For Local t:TileMapTile = Eachin alltiles
			tiles[t.gid - 1] = t
		Next

		' calculate the max tile size per layer
		For Local l:TileMapLayer = Eachin layers
			If TileMapTileLayer(l) <> Null Then
				Local tl:TileMapTileLayer = TileMapTileLayer(l)
				For Local i% = 0 Until tl.mapData.tiles.Length
					If tl.mapData.tiles[i] > 0 Then
						If tl.maxTileWidth < tiles[tl.mapData.tiles[i] - 1].width Then tl.maxTileWidth = tiles[tl.mapData.tiles[i] - 1].width
						If tl.maxTileHeight < tiles[tl.mapData.tiles[i] - 1].height Then tl.maxTileHeight = tiles[tl.mapData.tiles[i] - 1].height
					End
				Next
			End
		Next
	End

	Method GetAllObjects:DiddyStack<TileMapObject>()
		Local rv:DiddyStack<TileMapObject> = New DiddyStack<TileMapObject>
		For Local layer:TileMapLayer = EachIn layers
			If TileMapObjectLayer(layer) <> Null Then
				For Local obj:TileMapObject = EachIn TileMapObjectLayer(layer).objects
					rv.Push(obj)
				Next
			End
		Next
		Return rv
	End

	Method FindObjectByName:TileMapObject(name:String)
		For Local layer:TileMapLayer = EachIn layers
			If TileMapObjectLayer(layer) <> Null Then
				For Local obj:TileMapObject = EachIn TileMapObjectLayer(layer).objects
					If obj.name = name Then Return obj
				Next
			EndIf
		Next
		Return Null
	End

	Method RenderLayer:Void(layerName:String, bx:Int, by:Int, bw:Int, bh:Int, sx:Float = 1, sy:Float = 1)
		Local layer:TileMapLayer
		For Local l:TileMapLayer = EachIn layers
			If l.name.ToUpper() = layerName.ToUpper()
				layer = l
				Exit
			End
		End
		RenderLayer(layer, bx, by, bw, bh, sx, sy)
	End

	Method RenderLayer:Void(layer:TileMapLayer, bx:Int, by:Int, bw:Int, bh:Int, sx:Float = 1, sy:Float = 1)
		Local x:Int, y:Int, rx:Int, ry:Int, mx:Int, my:Int, mx2:Int, my2:Int, modx:Int, mody:Int
		If layer.visible And TileMapTileLayer(layer) <> Null Then
			Local tl:TileMapTileLayer = TileMapTileLayer(layer)
			Local mapTile:TileMapTile, gid%
			PreRenderLayer(layer)
			' ortho
			If orientation = MAP_ORIENTATION_ORTHOGONAL Then
				modx = (bx * tl.parallaxScaleX) Mod tileWidth
				mody = (by * tl.parallaxScaleY) Mod tileHeight
				y = by + tileHeight - tl.maxTileHeight
				my = Int(Floor(Float(by * tl.parallaxScaleY) / Float(tileHeight)))
				While y < by + bh + tl.maxTileHeight
					x = bx + tileWidth - tl.maxTileWidth
					mx = Int(Floor(Float(bx * tl.parallaxScaleX) / Float(tileWidth)))
					While x < bx + bw + tl.maxTileWidth
						If (wrapX Or (mx >= 0 And mx < width)) And (wrapY Or (my >= 0 And my < height)) Then
							mx2 = mx
							my2 = my
							While mx2 < 0
								mx2 += width
							End
							While mx2 >= width
								mx2 -= width
							End
							While my2 < 0
								my2 += height
							End
							While my2 >= height
								my2 -= height
							End
							gid = tl.mapData.cells[mx2 + my2*tl.mapData.width].gid
							If gid > 0 Then
								mapTile = tiles[gid - 1]

								If modx < 0 Then modx += tileWidth
								If mody < 0 Then mody += tileHeight
								rx = x - modx - bx
								ry = y - mody - by

								DrawTile(tl, mapTile, rx, ry)
							End
						End
						x += tileWidth
						mx += 1
					End
					y += tileHeight
					my += 1
				End

			' iso
			Elseif orientation = MAP_ORIENTATION_ISOMETRIC Then
				' TODO: wrapping
				For y = 0 Until tl.width + tl.height
					ry = y
					rx = 0
					While ry >= tl.height
						ry -= 1
						rx += 1
					Wend
					While ry >= 0 And rx < tl.width
						gid = tl.mapData.cells[rx + ry*tl.mapData.width].gid
						If gid > 0 Then
							mapTile = tiles[gid - 1]
							DrawTile(tl, mapTile, (rx - ry - 1) * tileWidth / 2 - bx, (rx + ry + 2) * tileHeight / 2 - mapTile.height - by)
						Endif
						ry -= 1
						rx += 1
					End
				Next
			End
			PostRenderLayer(layer)
		End
	End

	' bx,by,bw,bh = render bounds (screen)
	' sx,sy = scale x/y (float, defaults to 1) i'll do this later
	' wx,wy = wrap x/y (boolean, defaults to false)
	Method RenderMap:Void(bx:Int, by:Int, bw:Int, bh:Int, sx:Float = 1, sy:Float = 1, layerName:String = "ALL")
		PreRenderMap()

		If layerName = "ALL" Then
			For Local layer:TileMapLayer = EachIn layers
				RenderLayer(layer, bx, by, bw, bh, sx, sy)
			Next
		Else
			Local layer:TileMapLayer
			For Local l:TileMapLayer = EachIn layers
				If l.name.ToUpper() = layerName.ToUpper()
					layer = l
					Exit
				End
			End
			RenderLayer(layer, bx, by, bw, bh, sx, sy)
		End

		PostRenderMap()
	End

	Method GetBounds:TileMapRect()
		Local rv:TileMapRect = New TileMapRect
		If orientation = MAP_ORIENTATION_ORTHOGONAL Then
			rv.x = 0
			rv.y = tileHeight - maxTileHeight
			rv.w = (width - 1) * tileWidth + maxTileWidth
			rv.h = (height - 1) * tileHeight + maxTileHeight
		Elseif orientation = MAP_ORIENTATION_ISOMETRIC Then
			rv.x = -height * tileWidth / 2
			rv.y = tileHeight - maxTileHeight
			rv.w = (width - 2) * tileWidth / 2 + maxTileWidth - rv.x
			rv.h = (width + height) * tileHeight / 2 - rv.y
		Endif
		Return rv
	End

	Method UpdateAnimation:Void(timePassed:Int)
		Local layer:TileMapLayer, tl:TileMapTileLayer, cell:TileMapCell, t:TileMapTile
		Local cellCount:Int, i:Int, j:Int

		' get the layers as an array
		Local layerCount:Int = layers.Count()
		If layerArray.Length < layerCount Then
			layerArray = layers.ToArray()
			layerCount = layerArray.Length
		Else
			layerCount = layers.FillArray(layerArray)
		End

		' loop on each layer
		For i = 0 Until layerCount
			' cast
			tl = TileMapTileLayer(layerArray[i])

			' if the layer is a tile layer
			If tl <> Null Then
				' loop on each cell
				cellCount = tl.mapData.cells.Length
				For j = 0 Until cellCount
					cell = tl.mapData.cells[j]
					' if the cell exists and has a value
					If cell <> Null And cell.gid > 0 Then
						' get the tile
						t = tiles[cell.gid-1]

						' if the direction is 0 (paused), do nothing
						If t <> Null And cell.direction <> 0 Then
							' add our time to the time passed for the cell
							cell.timePassed += timePassed

							' if this tile has animation information
							If t.animated Then
								' get the new direction if we have one
								If t.hasAnimDirection Then cell.direction = t.animDirection

								' while it's not paused and we're not up to the current frame
								While cell.direction <> 0 And cell.timePassed >= t.animDelay
									' move to the next frame
									cell.timePassed -= t.animDelay
									cell.gid += cell.direction * t.animNext

									' get the tile for the new frame
									t = tiles[cell.gid-1]

									' if there's animation information, get it
									If t <> Null And t.animated Then
										' if the tile has a direction assigned to it, use it (we might be turning around)
										If t.hasAnimDirection Then cell.direction = t.animDirection

									' otherwise, pause and die
									Else
										cell.direction = 0
										Exit
									End
								End

							' no animation information, so pause
							Else
								cell.direction = 0
							End
						End
					End
				Next
			End
		Next
	End

	'summary: Check a collision using ray casting
	Method CheckCollision:TileCollisionData(startX:Float, startY:Float, endX:Float, endY:Float, layerName:String)
		Local layer:TileMapTileLayer = FindLayerByName(layerName)
		Local x1:Int = startX
		Local y1:Int = startY
		Local x2:Int = endX
		Local y2:Int = endY

		Local deltax:Int = Abs(x2 - x1)
		Local deltay:Int = Abs(y2 - y1)

		Local numpixels:Int, d:Int, dinc1:Int, dinc2:Int, xinc1:Int, xinc2:Int, yinc1:Int, yinc2:Int, x:Int, y:Int, i:Int
		Local xx:Int, cx:Int
		Local yy:Int, cy:Int
		Local tile:Int

		If deltax >= deltay
			numpixels = deltax + 1
			d = (2 * deltay) - deltax
			dinc1 = deltay Shl 1
			dinc2 = (deltay - deltax) Shl 1
			xinc1 = 1
			xinc2 = 1
			yinc1 = 0
			yinc2 = 1
		Else
			numpixels = deltay + 1
			d = (2 * deltax) - deltay
			dinc1 = deltax Shl 1
			dinc2 = (deltax - deltay) Shl 1
			xinc1 = 0
			xinc2 = 1
			yinc1 = 1
			yinc2 = 1
		End

		If x1 > x2
			xinc1 = -xinc1
			xinc2 = -xinc2
		End

		If y1 > y2
			yinc1 = -yinc1
			yinc2 = -yinc2
		End

		x = x1
		y = y1

		For i = 1 To numpixels
			xx = Floor(x / tileWidth)
			yy = Floor(y / tileHeight)
			tile = layer.mapData.Get(xx, yy)
			' exit when we have a collision
			if tile <> 0 Then
				cx = x
				cy = y
				Exit
			End

			If d < 0
				d = d + dinc1
				x = x + xinc1
				y = y + yinc1
			Else
				d = d + dinc2
				x = x + xinc2
				y = y + yinc2
			End

		Next
		Return New TileCollisionData(cx, cy, tile, xx, yy)
	End

	'summary:Return the layer for a set name
	Method FindLayerByName:TileMapTileLayer(layerName:String)
		Local layer:TileMapTileLayer
		For Local tl:TileMapLayer = Eachin layers
			If TileMapTileLayer(tl)
				If tl.name = layerName Then layer = TileMapTileLayer(tl); Exit
			End
		Next
		AssertNotNull(layer, "Cannot find layer " + layerName)
		Return layer
	End

	'summary: Check to see if a tile is under x and y on layername
	Method CollisionTile:Int(x:Float, y:Float, layerName:String)
		Local layer:TileMapTileLayer = FindLayerByName(layerName)
		If layer.name <> layerName Then Return 0

		If x < 0 Or x >= layer.width * tileWidth Or y < 0 Or y >= layer.height * tileHeight Then Return 0

		local xx:Int = (Floor(x / tileWidth))
		local yy:Int = (Floor(y / tileHeight))

		Return layer.mapData.Get(xx, yy)
	End

	'summary: Set a tile cell
	Method SetTile:Void(x:Float, y:Float, tile:Int, layerName:String)
		Local layer:TileMapTileLayer = FindLayerByName(layerName)
		If layer.name <> layerName Then Return
		If x < 0 Or x >= layer.width * tileWidth Or y < 0 Or y >= layer.height * tileHeight Then Return
		Local xx:Int = (Floor(x / tileWidth))
		Local yy:Int = (Floor(y / tileHeight))
		layer.mapData.Set(xx, yy, tile)
	End

	'summary: Change a tile cell and its frame image
	Method ChangeTile:Void(x:Float, y:Float, tile:Int, layerName:String)
		Local layer:TileMapTileLayer = FindLayerByName(layerName)

		If layer.name <> layerName Then Return
		If x < 0 Or x >= layer.width * tileWidth Or y < 0 Or y >= layer.height * tileHeight Then Return
		Local xx:Int = (Floor(x / tileWidth))
		Local yy:Int = (Floor(y / tileHeight))
		layer.mapData.Set(xx, yy, tile)
		Local tmc:TileMapCell = layer.mapData.GetCell(xx, yy)
		tmc.gid = tile
	End

	'summary: Scrolls the map based on the changeX and changeY
	Method Scroll:Void(changeX:Float, changeY:Float)
		If diddyGame Then
			diddyGame.scrollX += changeX
			diddyGame.scrollY += changeY

			If diddyGame.scrollX < 0 Then
				diddyGame.scrollX = 0
			Else
				Local maxX:Int = width * tileWidth - SCREEN_WIDTH
				If diddyGame.scrollX > maxX Then diddyGame.scrollX = maxX
			End

			If diddyGame.scrollY < 0 Then
				diddyGame.scrollY = 0
			Else
				Local maxY:Int = height * tileHeight - SCREEN_HEIGHT
				If diddyGame.scrollY > maxY Then diddyGame.scrollY = maxY
			End
		End
	End
End

'summary: Simple Collision Data Class
Class TileCollisionData
	Field x:Float, y:Float
	Field tileX:Float, tileY:Float
	Field tile:Int

	Method New(x:Float, y:Float, t:Int, tx:Float, ty:Float)
		Self.x = x
		Self.y = y
		Self.tile = t
		Self.tileX = tx
		Self.tileY = ty
	End
End

Class TileMapTileset Implements ITileMapPostLoad
	' attributes
	Field firstGid:Int
	Field name:String
	Field tileWidth:Int
	Field tileHeight:Int
	Field spacing:Int
	Field margin:Int
	Field source:String

	' children
	Field imageNode:TileMapImage
	Field tileNodes:DiddyStack<TileMapTile> = New DiddyStack<TileMapTile>

	' post load
	Field tiles:TileMapTile[]
	Field image:GameImage
	Field rawImage:Image
	Field tileCount:Int
	Field tileCountX:Int
	Field tileCountY:Int

	Method PostLoad:Void()
	End
End

Class TileMapImage Implements ITileMapPostLoad
	' attributes
	Field source$
	Field width%
	Field height%
	Field trans$ = ""

	' post-load
	Field transR%, transG%, transB%

	Method PostLoad:Void()
	End
End

'<layer> and <objectgroup>
Class TileMapLayer Extends TileMapPropertyContainer Implements ITileMapPostLoad Abstract
	' attributes
	Field name$
	Field width%
	Field height%
	Field visible% = True
	Field opacity:Float = 1

	Method PostLoad:Void()
	End
End

Class TileMapData
	Field width%
	Field height%
	Field tiles:Int[]
	Field cells:TileMapCell[]

	Method New(width%, height%)
		Self.width = width
		Self.height = height
		Self.tiles = New Int[width * height]
		Self.cells = New TileMapCell[width * height]
	End Method

	Method Get%(x%, y%)
		Return tiles[x + y * width]
	End

	Method Set:Void(x%, y%, gid%)
		tiles[x + y * width] = gid
	End

	Method GetCell:TileMapCell(x%, y%)
		Return cells[x + y * width]
	End

	Method SetCell:Void(x%, y%, cell:TileMapCell)
		cells[x + y * width] = cell
	End
End

Class TileMapTileLayer Extends TileMapLayer
	Field mapData:TileMapData
	Field maxTileWidth%
	Field maxTileHeight%
	Field parallaxOffsetX# = 0
	Field parallaxOffsetY# = 0
	Field parallaxScaleX# = 1
	Field parallaxScaleY# = 1
End

Class TileMapObjectLayer Extends TileMapLayer
	' attributes
	Field color%
	' children
	Field objects:DiddyStack<TileMapObject> = New DiddyStack<TileMapObject>
End

Class TileMapObject Extends TileMapPropertyContainer
	' attributes
	Field name:String
	Field objectType:String
	Field x:Int
	Field y:Int
	Field width:Int
	Field height:Int
End

Class TileMapProperty
Private
	Field valueType% = 0 ' 0=int, 1=float, 2=bool, 3=string
	Field name$ = ""
	Field rawValue$ = ""

Public
	Method New(name:String="default", value:String="")
		Self.name = name
		Self.rawValue = value
		Self.valueType = 3
	End

	Method Set:Void(value:Int)
		rawValue = value
		valueType = 0
	End

	Method Set:Void(value:Float)
		rawValue = value
		valueType = 1
	End

	Method Set:Void(value:Bool)
		If value Then
			rawValue = "true"
		Else
			rawValue = "false"
		Endif
		valueType = 2
	End

	Method Set:Void(value:String)
		rawValue = value
		valueType = 3
	End

	Method GetInt:Int()
		Return Int(rawValue)
	End

	Method GetFloat:Float()
		Return Float(rawValue)
	End

	Method GetBool:Bool()
		Local val:String = rawValue.ToLower()
		If val = "true" Or val = "t" Or val = "y" Then Return True
		Return False
	End

	Method GetString:String()
		Return rawValue
	End
End

Class TileMapTile Extends TileMapPropertyContainer Implements ITileMapPostLoad
	Field id%
	Field width:Int
	Field height:Int
	Field gid:Int

	Field animDelay:Int
	Field animNext:Int
	Field animDirection:Int
	Field hasAnimDirection:Bool
	Field animated:Bool

	' assigned from the tileset
	Field image:GameImage
	Field rawImage:Image
	Field srcX:Int
	Field srcY:Int

	Method New(id:Int)
		Self.id = id
	End

	Method PostLoad:Void()
		If properties.Has(PROP_TILE_ANIM_DELAY) Then
			If diddyGame Then
				animDelay = diddyGame.CalcAnimLength(properties.Get(PROP_TILE_ANIM_DELAY).GetInt())
			Else
				animDelay = properties.Get(PROP_TILE_ANIM_DELAY).GetInt()
			End
			animated = True
		End
		If properties.Has(PROP_TILE_ANIM_NEXT) Then animNext = properties.Get(PROP_TILE_ANIM_NEXT).GetInt()
		If properties.Has(PROP_TILE_ANIM_DIRECTION) Then
			animDirection = properties.Get(PROP_TILE_ANIM_DIRECTION).GetInt()
			hasAnimDirection = True
		End
	End
End

Class TileMapCell
	' mandatory fields
	Field gid%
	Field x%
	Field y%

	' animation stuff
	Field originalGid%
	Field timePassed:Int = 0 ' the time that has passed since the last frame change
	Field direction% = 1 ' >0 = forward, <0 = backward, 0 = paused

	Method New(gid%, x%, y%)
		Self.gid = gid
		Self.x = x
		Self.y = y
		originalGid = gid
	End
End

Class TileMapRect
	Field x%, y%, w%, h%
End

Function ColorToInt%(str$)
	' TODO: convert a hex color string #rrggbb to a number
	Return 0
End

Function HexToDec%(hexstr$)
	Local chars$ = "0123456789abcdef"
	Local rv% = 0
	hexstr = hexstr.ToLower()
	For Local i% = 0 To hexstr.Length - 1
		rv = rv Shl 4
		Local idx% = chars.Find(hexstr[i])
		If idx >= 0 Then rv += idx
	Next
	Return rv
End

Const PROP_MAP_WRAP_X$ = "wrap_x"
Const PROP_MAP_WRAP_Y$ = "wrap_y"

Const PROP_LAYER_PARALLAX_OFFSET_X$ = "parallax_offset_x"
Const PROP_LAYER_PARALLAX_OFFSET_Y$ = "parallax_offset_y"
Const PROP_LAYER_PARALLAX_SCALE_X$  = "parallax_scale_x"
Const PROP_LAYER_PARALLAX_SCALE_Y$  = "parallax_scale_y"

Const PROP_TILE_ANIM_DELAY$     = "anim_delay"
Const PROP_TILE_ANIM_NEXT$      = "anim_next"
Const PROP_TILE_ANIM_DIRECTION$ = "anim_direction"

Const DATA_ENCODING_RAW$    = ""
Const DATA_ENCODING_CSV$    = "csv"
Const DATA_ENCODING_BASE64$ = "base64"

Const MAP_ORIENTATION_ORTHOGONAL$   = "orthogonal"
Const MAP_ORIENTATION_ISOMETRIC$    = "isometric"

Const ATTR_MAP_VERSION$         = "version"
Const ATTR_MAP_ORIENTATION$     = "orientation"
Const ATTR_MAP_WIDTH$           = "width"
Const ATTR_MAP_HEIGHT$          = "height"
Const ATTR_MAP_TILEWIDTH$       = "tilewidth"
Const ATTR_MAP_TILEHEIGHT$      = "tileheight"
Const ATTR_TILESET_FIRSTGID$    = "firstgid"
Const ATTR_TILESET_NAME$        = "name"
Const ATTR_TILESET_TILEWIDTH$   = "tilewidth"
Const ATTR_TILESET_TILEHEIGHT$  = "tileheight"
Const ATTR_TILESET_SPACING$     = "spacing"
Const ATTR_TILESET_MARGIN$      = "margin"
Const ATTR_TILESET_SOURCE$      = "source"
Const ATTR_IMAGE_SOURCE$        = "source"
Const ATTR_IMAGE_WIDTH$         = "width"
Const ATTR_IMAGE_HEIGHT$        = "height"
Const ATTR_IMAGE_TRANS$         = "trans"
Const ATTR_TILE_ID$             = "id"
Const ATTR_LAYER_NAME$          = "name"
Const ATTR_LAYER_WIDTH$         = "width"
Const ATTR_LAYER_HEIGHT$        = "height"
Const ATTR_LAYER_VISIBLE$       = "visible"
Const ATTR_LAYER_OPACITY$       = "opacity"
Const ATTR_DATA_ENCODING$       = "encoding"
Const ATTR_DATA_COMPRESSION$    = "compression"
Const ATTR_PROPERTY_NAME$       = "name"
Const ATTR_PROPERTY_VALUE$      = "value"
Const ATTR_OBJECTGROUP_COLOR$   = "color"
Const ATTR_OBJECTGROUP_NAME$    = "name"
Const ATTR_OBJECTGROUP_WIDTH$   = "width"
Const ATTR_OBJECTGROUP_HEIGHT$  = "height"
Const ATTR_OBJECT_NAME$         = "name"
Const ATTR_OBJECT_TYPE$         = "type"
Const ATTR_OBJECT_X$            = "x"
Const ATTR_OBJECT_Y$            = "y"
Const ATTR_OBJECT_WIDTH$        = "width"
Const ATTR_OBJECT_HEIGHT$       = "height"

Const NODE_MAP$                 = "map"
Const NODE_TILESET$             = "tileset"
Const NODE_IMAGE$               = "image"
Const NODE_TILE$                = "tile"
Const NODE_LAYER$               = "layer"
Const NODE_DATA$                = "data"
Const NODE_PROPERTIES$          = "properties"
Const NODE_PROPERTY$            = "property"
Const NODE_OBJECTGROUP$         = "objectgroup"
Const NODE_OBJECT$              = "object"

#Rem
Copyright (c) 2011 Steve Revill and Shane Woolcock
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#End

Strict

Import diddy.base64
Import monkey.map
Import diddy.containers
Import diddy.framework
Import diddy.xml

' TileMapPropertyContainer
' Classes that extend this will automatically instantiate a property container and expose it.
Class TileMapPropertyContainer
Private
	Field properties:TileMapProperties = New TileMapProperties

Public
	Method Properties:TileMapProperties() Property
		Return properties
	End
End

' ITileMapPostLoad
' Classes that implement this interface will have PostLoad automatically called once the object has been created.
Interface ITileMapPostLoad
	Method PostLoad:Void()
End

' TileMapReader
' This can be extended to handle any map file format.
Class TileMapReader Abstract
	Field tileMap:TileMap
	Field graphicsPath:String

	Method LoadMap:TileMap(filename:String) Abstract

	' override this to create a custom tilemap class
	Method CreateMap:TileMap()
		Return New TileMap
	End
End

' TiledTileMapReader
' Extends TileMapReader to add support for the Tiled map editor.
Class TiledTileMapReader Extends TileMapReader
	Field doc:XMLDocument

	' Overrides TileMapReader
	Method LoadMap:TileMap(filename:String)
		' open file and get root node
		Local parser:XMLParser = New XMLParser
		Local xmlString:String = LoadString(filename)
		' error if we couldnt load the file
		If Not xmlString
			AssertError("Cannot load tile map file " + filename + ". Ensure you have TMX in the in the allowed #TEXT_FILES")
		End
		' look for the data encoding, if we cant find it assume its RAW XML and thats just too slow!
		Local findData:Int = xmlString.Find("<data encoding")
		If findData = -1
			AssertError("Tiled Raw XML is not supported!")
		End

		doc = parser.ParseString(xmlString)
		Return ReadMap(doc.Root)
	End

	Method ReadMap:TileMap(node:XMLElement)
		tileMap = CreateMap()
		ReadProperties(node, tileMap)

		' extract map properties
		If tileMap.properties.Has(PROP_MAP_WRAP_X) Then tileMap.wrapX = tileMap.properties.Get(PROP_MAP_WRAP_X).GetBool()
		If tileMap.properties.Has(PROP_MAP_WRAP_Y) Then tileMap.wrapY = tileMap.properties.Get(PROP_MAP_WRAP_Y).GetBool()

		' read root node's attributes
		If node.HasAttribute(ATTR_MAP_VERSION) Then tileMap.version = node.GetAttribute(ATTR_MAP_VERSION)
		If node.HasAttribute(ATTR_MAP_ORIENTATION) Then tileMap.orientation = node.GetAttribute(ATTR_MAP_ORIENTATION)
		If node.HasAttribute(ATTR_MAP_WIDTH) Then tileMap.width = Int(node.GetAttribute(ATTR_MAP_WIDTH))
		If node.HasAttribute(ATTR_MAP_HEIGHT) Then tileMap.height = Int(node.GetAttribute(ATTR_MAP_HEIGHT))
		If node.HasAttribute(ATTR_MAP_TILEWIDTH) Then tileMap.tileWidth = Int(node.GetAttribute(ATTR_MAP_TILEWIDTH))
		If node.HasAttribute(ATTR_MAP_TILEHEIGHT) Then tileMap.tileHeight = Int(node.GetAttribute(ATTR_MAP_TILEHEIGHT))

		tileMap.maxTileWidth = tileMap.tileWidth
		tileMap.maxTileHeight = tileMap.tileHeight

		' parse children
		If Not node.Children.IsEmpty() Then
			For Local mapchild:XMLElement = Eachin node.Children
				' tileset
				If mapchild.Name = NODE_TILESET Then
					Local ts:TileMapTileset = ReadTileset(mapchild)
					tileMap.tilesets.Set(ts.name, ts)

				' tile layer
				Elseif mapchild.Name = NODE_LAYER Then
					Local layer:TileMapLayer = ReadTileLayer(mapchild)
					tileMap.layers.Push(layer)

				' object layer
				Elseif mapchild.Name = NODE_OBJECTGROUP Then
					Local layer:TileMapLayer = ReadObjectLayer(mapchild)
					tileMap.layers.Push(layer)
				Endif
			Next
		Endif

		DoPostLoad(tileMap)

		Return tileMap
	End

	Method DoPostLoad:Void(obj:Object)
		If ITileMapPostLoad(obj) <> Null Then ITileMapPostLoad(obj).PostLoad()
	End

	Method ReadProperties:Void(node:XMLElement, obj:Object)
		Local cont:TileMapPropertyContainer = TileMapPropertyContainer(obj)
		If cont <> Null Then
			For Local propNode:XMLElement = Eachin node.Children
				If propNode.Name = NODE_PROPERTIES Then
					For Local child:XMLElement = Eachin propNode.Children
						If child.Name = NODE_PROPERTY Then
							Local prop:TileMapProperty = ReadProperty(child)
							cont.properties.props.Set(prop.name, prop)
						Endif
					Next
					Return
				End
			Next
		End
	End

	Method ReadProperty:TileMapProperty(node:XMLElement)
		Return New TileMapProperty(node.GetAttribute(ATTR_PROPERTY_NAME, "default"), node.GetAttribute(ATTR_PROPERTY_VALUE, ""))
	End

	Method ReadTileset:TileMapTileset(node:XMLElement, target:TileMapTileset=Null)
		Local rv:TileMapTileset = target
		ReadProperties(node, rv)
		If rv = Null Then rv = tileMap.CreateTileset()
		If node.HasAttribute(ATTR_TILESET_FIRSTGID) Then rv.firstGid = Int(node.GetAttribute(ATTR_TILESET_FIRSTGID))

		If node.HasAttribute(ATTR_TILESET_SOURCE) Then
			rv.source = node.GetAttribute(ATTR_TILESET_SOURCE)
			Local parser:XMLParser = New XMLParser
			Local tilesetdoc:XMLDocument = parser.ParseFile(rv.source)
			Return ReadTileset(tilesetdoc.Root, rv)
		Else
			If node.HasAttribute(ATTR_TILESET_NAME) Then rv.name = node.GetAttribute(ATTR_TILESET_NAME)
			If node.HasAttribute(ATTR_TILESET_TILEWIDTH) Then rv.tileWidth = Int(node.GetAttribute(ATTR_TILESET_TILEWIDTH))
			If node.HasAttribute(ATTR_TILESET_TILEHEIGHT) Then rv.tileHeight = Int(node.GetAttribute(ATTR_TILESET_TILEHEIGHT))
			If node.HasAttribute(ATTR_TILESET_SPACING) Then rv.spacing = Int(node.GetAttribute(ATTR_TILESET_SPACING))
			If node.HasAttribute(ATTR_TILESET_MARGIN) Then rv.margin = Int(node.GetAttribute(ATTR_TILESET_MARGIN))

			If Not node.Children.IsEmpty() Then
				For Local child:XMLElement = Eachin node.Children
					If child.Name = NODE_IMAGE Then
						rv.imageNode = ReadImage(child)
					Elseif child.Name = NODE_TILE Then
						rv.tileNodes.Push(ReadTile(child))
					End
				Next
			End
		End
		DoPostLoad(rv)
		Return rv
	End

	Method ReadLayerAttributes:Void(node:XMLElement, layer:TileMapLayer)
		If node.HasAttribute(ATTR_LAYER_NAME) Then layer.name = node.GetAttribute(ATTR_LAYER_NAME)
		If node.HasAttribute(ATTR_LAYER_WIDTH) Then layer.width = Int(node.GetAttribute(ATTR_LAYER_WIDTH))
		If node.HasAttribute(ATTR_LAYER_HEIGHT) Then layer.height = Int(node.GetAttribute(ATTR_LAYER_HEIGHT))
		layer.visible = Not node.HasAttribute(ATTR_LAYER_VISIBLE) Or Int(node.GetAttribute(ATTR_LAYER_VISIBLE)) <> 0
		If node.HasAttribute(ATTR_LAYER_OPACITY) Then layer.opacity = Float(node.GetAttribute(ATTR_LAYER_OPACITY))
	End

	Method ReadTileLayer:TileMapTileLayer(node:XMLElement)
		Local rv:TileMapTileLayer = tileMap.CreateTileLayer()
		ReadProperties(node, rv)
		ReadLayerAttributes(node, rv)

		If rv.properties.Has(PROP_LAYER_PARALLAX_OFFSET_X) Then rv.parallaxOffsetX = rv.properties.Get(PROP_LAYER_PARALLAX_OFFSET_X).GetFloat()
		If rv.properties.Has(PROP_LAYER_PARALLAX_OFFSET_Y) Then rv.parallaxOffsetY = rv.properties.Get(PROP_LAYER_PARALLAX_OFFSET_Y).GetFloat()
		If rv.properties.Has(PROP_LAYER_PARALLAX_SCALE_X) Then rv.parallaxScaleX = rv.properties.Get(PROP_LAYER_PARALLAX_SCALE_X).GetFloat()
		If rv.properties.Has(PROP_LAYER_PARALLAX_SCALE_Y) Then rv.parallaxScaleY = rv.properties.Get(PROP_LAYER_PARALLAX_SCALE_Y).GetFloat()

		For Local child:XMLElement = Eachin node.Children
			If child.Name = NODE_DATA Then
				rv.mapData = ReadTileData(child, rv)
			End
		Next

		DoPostLoad(rv)
		Return rv
	End

	Method ReadObjectLayer:TileMapObjectLayer(node:XMLElement)
		Local rv:TileMapObjectLayer = tileMap.CreateObjectLayer()
		ReadProperties(node, rv)
		ReadLayerAttributes(node, rv)

		If node.HasAttribute(ATTR_OBJECTGROUP_COLOR) Then rv.color = ColorToInt(node.GetAttribute(ATTR_OBJECTGROUP_COLOR))

		For Local child:XMLElement = Eachin node.Children
			If child.Name = NODE_OBJECT Then
				rv.objects.Push(ReadObject(child, rv))
			End
		Next

		DoPostLoad(rv)
		Return rv
	End

	Method ReadImage:TileMapImage(node:XMLElement)
		Local rv:TileMapImage = tileMap.CreateImage()
		ReadProperties(node, rv)

		If node.HasAttribute(ATTR_IMAGE_SOURCE) Then rv.source = graphicsPath + StripDir(node.GetAttribute(ATTR_IMAGE_SOURCE))
		If node.HasAttribute(ATTR_IMAGE_WIDTH) Then rv.width = Int(node.GetAttribute(ATTR_IMAGE_WIDTH))
		If node.HasAttribute(ATTR_IMAGE_HEIGHT) Then rv.height = Int(node.GetAttribute(ATTR_IMAGE_HEIGHT))
		If node.HasAttribute(ATTR_IMAGE_TRANS) Then rv.trans = node.GetAttribute(ATTR_IMAGE_TRANS)
		If rv.trans.Length > 0 Then
			rv.transR = HexToDec(rv.trans[0..2])
			rv.transG = HexToDec(rv.trans[2..4])
			rv.transB = HexToDec(rv.trans[4..6])
		Endif

		DoPostLoad(rv)
		Return rv
	End

	Method ReadTile:TileMapTile(node:XMLElement)
		Local id:Int = Int(node.GetAttribute(ATTR_TILE_ID, "0"))
		Local rv:TileMapTile = tileMap.CreateTile(id)
		ReadProperties(node, rv)
		DoPostLoad(rv)
		Return rv
	End

	Method ReadObject:TileMapObject(node:XMLElement, layer:TileMapObjectLayer)
		Local rv:TileMapObject = tileMap.CreateObject()
		ReadProperties(node, rv)
		If node.HasAttribute(ATTR_OBJECT_NAME) Then rv.name = node.GetAttribute(ATTR_OBJECT_NAME)
		If node.HasAttribute(ATTR_OBJECT_TYPE) Then rv.objectType = node.GetAttribute(ATTR_OBJECT_TYPE)
		If node.HasAttribute(ATTR_OBJECT_X) Then rv.x = Int(node.GetAttribute(ATTR_OBJECT_X))
		If node.HasAttribute(ATTR_OBJECT_Y) Then rv.y = Int(node.GetAttribute(ATTR_OBJECT_Y))
		If node.HasAttribute(ATTR_OBJECT_WIDTH) Then rv.width = Int(node.GetAttribute(ATTR_OBJECT_WIDTH))
		If node.HasAttribute(ATTR_OBJECT_HEIGHT) Then rv.height = Int(node.GetAttribute(ATTR_OBJECT_HEIGHT))
		DoPostLoad(rv)
		Return rv
	End

	Method ReadTileData:TileMapData(node:XMLElement, layer:TileMapTileLayer)
		Local rv:TileMapData = tileMap.CreateData(layer.width, layer.height)

		' default to raw xml (ugly)
		Local encoding$ = DATA_ENCODING_RAW
		If node.HasAttribute(ATTR_DATA_ENCODING) Then encoding = node.GetAttribute(ATTR_DATA_ENCODING)
		If encoding = DATA_ENCODING_RAW Then
			' TODO: raw xml
			AssertError("Raw xml is currently not supported")
		Elseif encoding = DATA_ENCODING_CSV Then
			Local csv:String[] = node.Value.Split(",")
			For Local i% = 0 Until csv.Length
				Local gid:Int = Int(csv[i].Trim())
				rv.tiles[i] = gid
				rv.cells[i] = tileMap.CreateCell(gid, i Mod rv.width, i / rv.width)
			Next
		Elseif encoding = DATA_ENCODING_BASE64 Then
			Local bytes:Int[] = DecodeBase64Bytes(node.Value)
			If node.HasAttribute(ATTR_DATA_COMPRESSION) Then
				' TODO: compression
				AssertError("Compression is currently not supported")
			End
			For Local i% = 0 Until bytes.Length Step 4
				' little endian
				Local gid% = bytes[i]
				gid += bytes[i + 1] Shl 8
				gid += bytes[i + 2] Shl 16
				gid += bytes[i + 3] Shl 24
				rv.tiles[i / 4] = gid
				rv.cells[i / 4] = tileMap.CreateCell(gid, (i / 4) Mod rv.width, (i / 4) / rv.width)
			Next
		End
		Return rv
	End
End

' TileMapProperties
' Container for properties.
Class TileMapProperties
	Field props:StringMap<TileMapProperty> = New StringMap<TileMapProperty>

	Method Has:Bool(name:String)
		Return props.Contains(name)
	End

	Method Get:TileMapProperty(name:String)
		Return props.Get(name)
	End

	Method Set:Void(name:String, prop:TileMapProperty)
		props.Set(name, prop)
	End
End

' TileMap
' The main Map class.
Class TileMap Extends TileMapPropertyContainer Implements ITileMapPostLoad

	' attributes
	Field version:String = "1.0"
	Field orientation:String = MAP_ORIENTATION_ORTHOGONAL
	Field width:Int
	Field height:Int
	Field tileWidth:Int = 32
	Field tileHeight:Int = 32

	' children
	Field tilesets:StringMap<TileMapTileset> = New StringMap<TileMapTileset>
	Field layers:DiddyStack<TileMapLayer> = New DiddyStack<TileMapLayer>

	' post-load
	Field layerNames:StringMap<TileMapLayer> = New StringMap<TileMapLayer>
	Field tiles:TileMapTile[]
	Field maxTileWidth:Int
	Field maxTileHeight:Int
	Field wrapX:Bool = False
	Field wrapY:Bool = False

	' optimisation
	Field layerArray:TileMapLayer[] = []
	Field animatedTiles:TileMapTile[]

	' summary: override this to do something before a layer is rendered
	Method PreRenderLayer:Void(tileLayer:TileMapLayer)
		ConfigureLayer(tileLayer) ' call the deprecated one
	End

	' summary: override this to do something after a layer is rendered
	Method PostRenderLayer:Void(tileLayer:TileMapLayer)
	End

	' summary: override this to do something before the map is rendered
	Method PreRenderMap:Void()
	End

	' summary: override this to do something after the map is rendered
	Method PostRenderMap:Void()
	End

	' summary: override this to configure a layer (called on every render)
	' deprecated, override PreRenderLayer instead
	Method ConfigureLayer:Void(tileLayer:TileMapLayer)
	End

	' summary: override this to draw a tile
	Method DrawTile:Void(tileLayer:TileMapTileLayer, mapTile:TileMapTile, x:Int, y:Int)
		' default draw call
		mapTile.image.DrawTile(x, y, mapTile.id, 0, 1, 1)
	End

	' summary: override this to create a custom tile class
	Method CreateTile:TileMapTile(id:Int)
		Return New TileMapTile(id)
	End

	' summary: override this to create a custom tileset class
	Method CreateTileset:TileMapTileset()
		Return New TileMapTileset
	End

	' summary: override this to create a custom tile layer class
	Method CreateTileLayer:TileMapTileLayer()
		Return New TileMapTileLayer
	End

	' summary: override this to create a custom object layer class
	Method CreateObjectLayer:TileMapObjectLayer()
		Return New TileMapObjectLayer
	End

	' summary: override this to create a custom image class
	Method CreateImage:TileMapImage()
		Return New TileMapImage
	End

	' summary: override this to create a custom object class
	Method CreateObject:TileMapObject()
		Return New TileMapObject
	End

	' summary: override this to create a custom cell class
	Method CreateCell:TileMapCell(gid:Int, x:Int, y:Int)
		Return New TileMapCell(gid, x, y)
	End

	' summary: override this to create a custom data class
	Method CreateData:TileMapData(width:Int, height:Int)
		Return New TileMapData(width, height)
	End

	' summary: override this to perform additional post-loading functionality (remember to call Super.PostLoad() first)
	Method PostLoad:Void()
		Local totaltiles% = 0, ts:TileMapTileset
		Local alltiles:DiddyStack<TileMapTile> = New DiddyStack<TileMapTile>
		For Local ts:TileMapTileset = Eachin tilesets.Values()
			' load the image
			ts.image = diddyGame.images.LoadTileset(ts.imageNode.source, ts.tileWidth, ts.tileHeight, ts.margin, ts.spacing, "", False, True)
			' get the cell count
			ts.tileCount = ts.image.tileCount

			' update max tile size
			If maxTileWidth < ts.tileWidth Then maxTileWidth = ts.tileWidth
			If maxTileHeight < ts.tileHeight Then maxTileHeight = ts.tileHeight

			' build tile list
			ts.tiles = New TileMapTile[ts.tileCount]
			For Local t:TileMapTile = Eachin ts.tileNodes
				ts.tiles[t.id] = t
			Next
			For Local i% = 0 Until ts.tiles.Length
				If ts.tiles[i] = Null Then
					ts.tiles[i] = CreateTile(i)
				End
				ts.tiles[i].gid = ts.firstGid + i
				ts.tiles[i].image = ts.image
				ts.tiles[i].width = ts.tileWidth
				ts.tiles[i].height = ts.tileHeight
				alltiles.Push(ts.tiles[i])
			Next
			' update total tiles
			totaltiles += ts.tileCount
		Next

		' make our cache
		tiles = New TileMapTile[totaltiles]
		For Local t:TileMapTile = Eachin alltiles
			tiles[t.gid - 1] = t
		Next

		' calculate the max tile size per layer
		For Local l:TileMapLayer = Eachin layers
			If TileMapTileLayer(l) <> Null Then
				Local tl:TileMapTileLayer = TileMapTileLayer(l)
				For Local i% = 0 Until tl.mapData.tiles.Length
					If tl.mapData.tiles[i] > 0 Then
						If tl.maxTileWidth < tiles[tl.mapData.tiles[i] - 1].width Then tl.maxTileWidth = tiles[tl.mapData.tiles[i] - 1].width
						If tl.maxTileHeight < tiles[tl.mapData.tiles[i] - 1].height Then tl.maxTileHeight = tiles[tl.mapData.tiles[i] - 1].height
					End
				Next
			End
		Next
	End

	Method GetAllObjects:DiddyStack<TileMapObject>()
		Local rv:DiddyStack<TileMapObject> = New DiddyStack<TileMapObject>
		For Local layer:TileMapLayer = EachIn layers
			If TileMapObjectLayer(layer) <> Null Then
				For Local obj:TileMapObject = EachIn TileMapObjectLayer(layer).objects
					rv.Push(obj)
				Next
			End
		Next
		Return rv
	End

	Method FindObjectByName:TileMapObject(name:String)
		For Local layer:TileMapLayer = EachIn layers
			If TileMapObjectLayer(layer) <> Null Then
				For Local obj:TileMapObject = EachIn TileMapObjectLayer(layer).objects
					If obj.name = name Then Return obj
				Next
			EndIf
		Next
		Return Null
	End

	Method RenderLayer:Void(layerName:String, bx:Int, by:Int, bw:Int, bh:Int, sx:Float = 1, sy:Float = 1)
		Local layer:TileMapLayer
		For Local l:TileMapLayer = EachIn layers
			If l.name.ToUpper() = layerName.ToUpper()
				layer = l
				Exit
			End
		End
		RenderLayer(layer, bx, by, bw, bh, sx, sy)
	End

	Method RenderLayer:Void(layer:TileMapLayer, bx:Int, by:Int, bw:Int, bh:Int, sx:Float = 1, sy:Float = 1)
		Local x:Int, y:Int, rx:Int, ry:Int, mx:Int, my:Int, mx2:Int, my2:Int, modx:Int, mody:Int
		If layer.visible And TileMapTileLayer(layer) <> Null Then
			Local tl:TileMapTileLayer = TileMapTileLayer(layer)
			Local mapTile:TileMapTile, gid%
			PreRenderLayer(layer)
			' ortho
			If orientation = MAP_ORIENTATION_ORTHOGONAL Then
				modx = (bx * tl.parallaxScaleX) Mod tileWidth
				mody = (by * tl.parallaxScaleY) Mod tileHeight
				y = by + tileHeight - tl.maxTileHeight
				my = Int(Floor(Float(by * tl.parallaxScaleY) / Float(tileHeight)))
				While y < by + bh + tl.maxTileHeight
					x = bx + tileWidth - tl.maxTileWidth
					mx = Int(Floor(Float(bx * tl.parallaxScaleX) / Float(tileWidth)))
					While x < bx + bw + tl.maxTileWidth
						If (wrapX Or (mx >= 0 And mx < width)) And (wrapY Or (my >= 0 And my < height)) Then
							mx2 = mx
							my2 = my
							While mx2 < 0
								mx2 += width
							End
							While mx2 >= width
								mx2 -= width
							End
							While my2 < 0
								my2 += height
							End
							While my2 >= height
								my2 -= height
							End
							gid = tl.mapData.cells[mx2 + my2*tl.mapData.width].gid
							If gid > 0 Then
								mapTile = tiles[gid - 1]

								If modx < 0 Then modx += tileWidth
								If mody < 0 Then mody += tileHeight
								rx = x - modx - bx
								ry = y - mody - by

								DrawTile(tl, mapTile, rx, ry)
							End
						End
						x += tileWidth
						mx += 1
					End
					y += tileHeight
					my += 1
				End

			' iso
			Elseif orientation = MAP_ORIENTATION_ISOMETRIC Then
				' TODO: wrapping
				For y = 0 Until tl.width + tl.height
					ry = y
					rx = 0
					While ry >= tl.height
						ry -= 1
						rx += 1
					Wend
					While ry >= 0 And rx < tl.width
						gid = tl.mapData.cells[rx + ry*tl.mapData.width].gid
						If gid > 0 Then
							mapTile = tiles[gid - 1]
							DrawTile(tl, mapTile, (rx - ry - 1) * tileWidth / 2 - bx, (rx + ry + 2) * tileHeight / 2 - mapTile.height - by)
						Endif
						ry -= 1
						rx += 1
					End
				Next
			End
			PostRenderLayer(layer)
		End
	End

	' bx,by,bw,bh = render bounds (screen)
	' sx,sy = scale x/y (float, defaults to 1) i'll do this later
	' wx,wy = wrap x/y (boolean, defaults to false)
	Method RenderMap:Void(bx:Int, by:Int, bw:Int, bh:Int, sx:Float = 1, sy:Float = 1, layerName:String = "ALL")
		PreRenderMap()

		If layerName = "ALL" Then
			For Local layer:TileMapLayer = EachIn layers
				RenderLayer(layer, bx, by, bw, bh, sx, sy)
			Next
		Else
			Local layer:TileMapLayer
			For Local l:TileMapLayer = EachIn layers
				If l.name.ToUpper() = layerName.ToUpper()
					layer = l
					Exit
				End
			End
			RenderLayer(layer, bx, by, bw, bh, sx, sy)
		End

		PostRenderMap()
	End

	Method GetBounds:TileMapRect()
		Local rv:TileMapRect = New TileMapRect
		If orientation = MAP_ORIENTATION_ORTHOGONAL Then
			rv.x = 0
			rv.y = tileHeight - maxTileHeight
			rv.w = (width - 1) * tileWidth + maxTileWidth
			rv.h = (height - 1) * tileHeight + maxTileHeight
		Elseif orientation = MAP_ORIENTATION_ISOMETRIC Then
			rv.x = -height * tileWidth / 2
			rv.y = tileHeight - maxTileHeight
			rv.w = (width - 2) * tileWidth / 2 + maxTileWidth - rv.x
			rv.h = (width + height) * tileHeight / 2 - rv.y
		Endif
		Return rv
	End

	Method UpdateAnimation:Void(timePassed:Int)
		Local layer:TileMapLayer, tl:TileMapTileLayer, cell:TileMapCell, t:TileMapTile
		Local cellCount:Int, i:Int, j:Int

		' get the layers as an array
		Local layerCount:Int = layers.Count()
		If layerArray.Length < layerCount Then
			layerArray = layers.ToArray()
			layerCount = layerArray.Length
		Else
			layerCount = layers.FillArray(layerArray)
		End

		' loop on each layer
		For i = 0 Until layerCount
			' cast
			tl = TileMapTileLayer(layerArray[i])

			' if the layer is a tile layer
			If tl <> Null Then
				' loop on each cell
				cellCount = tl.mapData.cells.Length
				For j = 0 Until cellCount
					cell = tl.mapData.cells[j]
					' if the cell exists and has a value
					If cell <> Null And cell.gid > 0 Then
						' get the tile
						t = tiles[cell.gid-1]

						' if the direction is 0 (paused), do nothing
						If t <> Null And cell.direction <> 0 Then
							' add our time to the time passed for the cell
							cell.timePassed += timePassed

							' if this tile has animation information
							If t.animated Then
								' get the new direction if we have one
								If t.hasAnimDirection Then cell.direction = t.animDirection

								' while it's not paused and we're not up to the current frame
								While cell.direction <> 0 And cell.timePassed >= t.animDelay
									' move to the next frame
									cell.timePassed -= t.animDelay
									cell.gid += cell.direction * t.animNext

									' get the tile for the new frame
									t = tiles[cell.gid-1]

									' if there's animation information, get it
									If t <> Null And t.animated Then
										' if the tile has a direction assigned to it, use it (we might be turning around)
										If t.hasAnimDirection Then cell.direction = t.animDirection

									' otherwise, pause and die
									Else
										cell.direction = 0
										Exit
									End
								End

							' no animation information, so pause
							Else
								cell.direction = 0
							End
						End
					End
				Next
			End
		Next
	End

	'summary: Check a collision using ray casting
	Method CheckCollision:TileCollisionData(startX:Float, startY:Float, endX:Float, endY:Float, layerName:String)
		Local layer:TileMapTileLayer = FindLayerByName(layerName)
		Local x1:Int = startX
		Local y1:Int = startY
		Local x2:Int = endX
		Local y2:Int = endY

		Local deltax:Int = Abs(x2 - x1)
		Local deltay:Int = Abs(y2 - y1)

		Local numpixels:Int, d:Int, dinc1:Int, dinc2:Int, xinc1:Int, xinc2:Int, yinc1:Int, yinc2:Int, x:Int, y:Int, i:Int
		Local xx:Int, cx:Int
		Local yy:Int, cy:Int
		Local tile:Int

		If deltax >= deltay
			numpixels = deltax + 1
			d = (2 * deltay) - deltax
			dinc1 = deltay Shl 1
			dinc2 = (deltay - deltax) Shl 1
			xinc1 = 1
			xinc2 = 1
			yinc1 = 0
			yinc2 = 1
		Else
			numpixels = deltay + 1
			d = (2 * deltax) - deltay
			dinc1 = deltax Shl 1
			dinc2 = (deltax - deltay) Shl 1
			xinc1 = 0
			xinc2 = 1
			yinc1 = 1
			yinc2 = 1
		End

		If x1 > x2
			xinc1 = -xinc1
			xinc2 = -xinc2
		End

		If y1 > y2
			yinc1 = -yinc1
			yinc2 = -yinc2
		End

		x = x1
		y = y1

		For i = 1 To numpixels
			xx = Floor(x / tileWidth)
			yy = Floor(y / tileHeight)
			tile = layer.mapData.Get(xx, yy)
			' exit when we have a collision
			if tile <> 0 Then
				cx = x
				cy = y
				Exit
			End

			If d < 0
				d = d + dinc1
				x = x + xinc1
				y = y + yinc1
			Else
				d = d + dinc2
				x = x + xinc2
				y = y + yinc2
			End

		Next
		Return New TileCollisionData(cx, cy, tile, xx, yy)
	End

	'summary:Return the layer for a set name
	Method FindLayerByName:TileMapTileLayer(layerName:String)
		Local layer:TileMapTileLayer
		For Local tl:TileMapLayer = Eachin layers
			If TileMapTileLayer(tl)
				If tl.name = layerName Then layer = TileMapTileLayer(tl); Exit
			End
		Next
		AssertNotNull(layer, "Cannot find layer " + layerName)
		Return layer
	End

	'summary: Check to see if a tile is under x and y on layername
	Method CollisionTile:Int(x:Float, y:Float, layerName:String)
		Local layer:TileMapTileLayer = FindLayerByName(layerName)
		If layer.name <> layerName Then Return 0

		If x < 0 Or x >= layer.width * tileWidth Or y < 0 Or y >= layer.height * tileHeight Then Return 0

		local xx:Int = (Floor(x / tileWidth))
		local yy:Int = (Floor(y / tileHeight))

		Return layer.mapData.Get(xx, yy)
	End

	'summary: Set a tile cell
	Method SetTile:Void(x:Float, y:Float, tile:Int, layerName:String)
		Local layer:TileMapTileLayer = FindLayerByName(layerName)
		If layer.name <> layerName Then Return
		If x < 0 Or x >= layer.width * tileWidth Or y < 0 Or y >= layer.height * tileHeight Then Return
		Local xx:Int = (Floor(x / tileWidth))
		Local yy:Int = (Floor(y / tileHeight))
		layer.mapData.Set(xx, yy, tile)
	End

	'summary: Change a tile cell and its frame image
	Method ChangeTile:Void(x:Float, y:Float, tile:Int, layerName:String)
		Local layer:TileMapTileLayer = FindLayerByName(layerName)

		If layer.name <> layerName Then Return
		If x < 0 Or x >= layer.width * tileWidth Or y < 0 Or y >= layer.height * tileHeight Then Return
		Local xx:Int = (Floor(x / tileWidth))
		Local yy:Int = (Floor(y / tileHeight))
		layer.mapData.Set(xx, yy, tile)
		Local tmc:TileMapCell = layer.mapData.GetCell(xx, yy)
		tmc.gid = tile
	End

	'summary: Scrolls the map based on the changeX and changeY
	Method Scroll:Void(changeX:Float, changeY:Float)
		diddyGame.scrollX += changeX
		diddyGame.scrollY += changeY

		If diddyGame.scrollX < 0 Then
			diddyGame.scrollX = 0
		Else
			Local maxX:Int = width * tileWidth - SCREEN_WIDTH
			If diddyGame.scrollX > maxX Then diddyGame.scrollX = maxX
		End

		If diddyGame.scrollY < 0 Then
			diddyGame.scrollY = 0
		Else
			Local maxY:Int = height * tileHeight - SCREEN_HEIGHT
			If diddyGame.scrollY > maxY Then diddyGame.scrollY = maxY
		End
	End

End

'summary: Simple Collision Data Class
Class TileCollisionData
	Field x:Float, y:Float
	Field tileX:Float, tileY:Float
	Field tile:Int

	Method New(x:Float, y:Float, t:Int, tx:Float, ty:Float)
		Self.x = x
		Self.y = y
		Self.tile = t
		Self.tileX = tx
		Self.tileY = ty
	End
End

Class TileMapTileset Implements ITileMapPostLoad
	' attributes
	Field firstGid:Int
	Field name:String
	Field tileWidth:Int
	Field tileHeight:Int
	Field spacing:Int
	Field margin:Int
	Field source:String

	' children
	Field imageNode:TileMapImage
	Field tileNodes:DiddyStack<TileMapTile> = New DiddyStack<TileMapTile>

	' post load
	Field tiles:TileMapTile[]
	Field image:GameImage
	Field tileCount:Int

	Method PostLoad:Void()
	End
End

Class TileMapImage Implements ITileMapPostLoad
	' attributes
	Field source$
	Field width%
	Field height%
	Field trans$ = ""

	' post-load
	Field transR%, transG%, transB%

	Method PostLoad:Void()
	End
End

'<layer> and <objectgroup>
Class TileMapLayer Extends TileMapPropertyContainer Implements ITileMapPostLoad Abstract
	' attributes
	Field name$
	Field width%
	Field height%
	Field visible% = True
	Field opacity:Float = 1

	Method PostLoad:Void()
	End
End

Class TileMapData
	Field width%
	Field height%
	Field tiles:Int[]
	Field cells:TileMapCell[]

	Method New(width%, height%)
		Self.width = width
		Self.height = height
		Self.tiles = New Int[width * height]
		Self.cells = New TileMapCell[width * height]
	End Method

	Method Get%(x%, y%)
		Return tiles[x + y * width]
	End

	Method Set:Void(x%, y%, gid%)
		tiles[x + y * width] = gid
	End

	Method GetCell:TileMapCell(x%, y%)
		Return cells[x + y * width]
	End

	Method SetCell:Void(x%, y%, cell:TileMapCell)
		cells[x + y * width] = cell
	End
End

Class TileMapTileLayer Extends TileMapLayer
	Field mapData:TileMapData
	Field maxTileWidth%
	Field maxTileHeight%
	Field parallaxOffsetX# = 0
	Field parallaxOffsetY# = 0
	Field parallaxScaleX# = 1
	Field parallaxScaleY# = 1
End

Class TileMapObjectLayer Extends TileMapLayer
	' attributes
	Field color%
	' children
	Field objects:DiddyStack<TileMapObject> = New DiddyStack<TileMapObject>
End

Class TileMapObject Extends TileMapPropertyContainer
	' attributes
	Field name:String
	Field objectType:String
	Field x:Int
	Field y:Int
	Field width:Int
	Field height:Int
End

Class TileMapProperty
Private
	Field valueType% = 0 ' 0=int, 1=float, 2=bool, 3=string
	Field name$ = ""
	Field rawValue$ = ""

Public
	Method New(name:String="default", value:String="")
		Self.name = name
		Self.rawValue = value
		Self.valueType = 3
	End

	Method Set:Void(value:Int)
		rawValue = value
		valueType = 0
	End

	Method Set:Void(value:Float)
		rawValue = value
		valueType = 1
	End

	Method Set:Void(value:Bool)
		If value Then
			rawValue = "true"
		Else
			rawValue = "false"
		Endif
		valueType = 2
	End

	Method Set:Void(value:String)
		rawValue = value
		valueType = 3
	End

	Method GetInt:Int()
		Return Int(rawValue)
	End

	Method GetFloat:Float()
		Return Float(rawValue)
	End

	Method GetBool:Bool()
		Local val:String = rawValue.ToLower()
		If val = "true" Or val = "t" Or val = "y" Then Return True
		Return False
	End

	Method GetString:String()
		Return rawValue
	End
End

Class TileMapTile Extends TileMapPropertyContainer Implements ITileMapPostLoad
	Field id%
	Field image:GameImage
	Field width:Int
	Field height:Int
	Field gid:Int

	Field animDelay:Int
	Field animNext:Int
	Field animDirection:Int
	Field hasAnimDirection:Bool
	Field animated:Bool

	Method New(id:Int)
		Self.id = id
	End

	Method PostLoad:Void()
		If properties.Has(PROP_TILE_ANIM_DELAY) Then
			animDelay = diddyGame.CalcAnimLength(properties.Get(PROP_TILE_ANIM_DELAY).GetInt())
			animated = True
		End
		If properties.Has(PROP_TILE_ANIM_NEXT) Then animNext = properties.Get(PROP_TILE_ANIM_NEXT).GetInt()
		If properties.Has(PROP_TILE_ANIM_DIRECTION) Then
			animDirection = properties.Get(PROP_TILE_ANIM_DIRECTION).GetInt()
			hasAnimDirection = True
		End
	End
End

Class TileMapCell
	' mandatory fields
	Field gid%
	Field x%
	Field y%

	' animation stuff
	Field originalGid%
	Field timePassed:Int = 0 ' the time that has passed since the last frame change
	Field direction% = 1 ' >0 = forward, <0 = backward, 0 = paused

	Method New(gid%, x%, y%)
		Self.gid = gid
		Self.x = x
		Self.y = y
		originalGid = gid
	End
End

Class TileMapRect
	Field x%, y%, w%, h%
End

Function ColorToInt%(str$)
	' TODO: convert a hex color string #rrggbb to a number
	Return 0
End

Function HexToDec%(hexstr$)
	Local chars$ = "0123456789abcdef"
	Local rv% = 0
	hexstr = hexstr.ToLower()
	For Local i% = 0 To hexstr.Length - 1
		rv = rv Shl 4
		Local idx% = chars.Find(hexstr[i])
		If idx >= 0 Then rv += idx
	Next
	Return rv
End

Const PROP_MAP_WRAP_X$ = "wrap_x"
Const PROP_MAP_WRAP_Y$ = "wrap_y"

Const PROP_LAYER_PARALLAX_OFFSET_X$ = "parallax_offset_x"
Const PROP_LAYER_PARALLAX_OFFSET_Y$ = "parallax_offset_y"
Const PROP_LAYER_PARALLAX_SCALE_X$  = "parallax_scale_x"
Const PROP_LAYER_PARALLAX_SCALE_Y$  = "parallax_scale_y"

Const PROP_TILE_ANIM_DELAY$     = "anim_delay"
Const PROP_TILE_ANIM_NEXT$      = "anim_next"
Const PROP_TILE_ANIM_DIRECTION$ = "anim_direction"

Const DATA_ENCODING_RAW$    = ""
Const DATA_ENCODING_CSV$    = "csv"
Const DATA_ENCODING_BASE64$ = "base64"

Const MAP_ORIENTATION_ORTHOGONAL$   = "orthogonal"
Const MAP_ORIENTATION_ISOMETRIC$    = "isometric"

Const ATTR_MAP_VERSION$         = "version"
Const ATTR_MAP_ORIENTATION$     = "orientation"
Const ATTR_MAP_WIDTH$           = "width"
Const ATTR_MAP_HEIGHT$          = "height"
Const ATTR_MAP_TILEWIDTH$       = "tilewidth"
Const ATTR_MAP_TILEHEIGHT$      = "tileheight"
Const ATTR_TILESET_FIRSTGID$    = "firstgid"
Const ATTR_TILESET_NAME$        = "name"
Const ATTR_TILESET_TILEWIDTH$   = "tilewidth"
Const ATTR_TILESET_TILEHEIGHT$  = "tileheight"
Const ATTR_TILESET_SPACING$     = "spacing"
Const ATTR_TILESET_MARGIN$      = "margin"
Const ATTR_TILESET_SOURCE$      = "source"
Const ATTR_IMAGE_SOURCE$        = "source"
Const ATTR_IMAGE_WIDTH$         = "width"
Const ATTR_IMAGE_HEIGHT$        = "height"
Const ATTR_IMAGE_TRANS$         = "trans"
Const ATTR_TILE_ID$             = "id"
Const ATTR_LAYER_NAME$          = "name"
Const ATTR_LAYER_WIDTH$         = "width"
Const ATTR_LAYER_HEIGHT$        = "height"
Const ATTR_LAYER_VISIBLE$       = "visible"
Const ATTR_LAYER_OPACITY$       = "opacity"
Const ATTR_DATA_ENCODING$       = "encoding"
Const ATTR_DATA_COMPRESSION$    = "compression"
Const ATTR_PROPERTY_NAME$       = "name"
Const ATTR_PROPERTY_VALUE$      = "value"
Const ATTR_OBJECTGROUP_COLOR$   = "color"
Const ATTR_OBJECTGROUP_NAME$    = "name"
Const ATTR_OBJECTGROUP_WIDTH$   = "width"
Const ATTR_OBJECTGROUP_HEIGHT$  = "height"
Const ATTR_OBJECT_NAME$         = "name"
Const ATTR_OBJECT_TYPE$         = "type"
Const ATTR_OBJECT_X$            = "x"
Const ATTR_OBJECT_Y$            = "y"
Const ATTR_OBJECT_WIDTH$        = "width"
Const ATTR_OBJECT_HEIGHT$       = "height"

Const NODE_MAP$                 = "map"
Const NODE_TILESET$             = "tileset"
Const NODE_IMAGE$               = "image"
Const NODE_TILE$                = "tile"
Const NODE_LAYER$               = "layer"
Const NODE_DATA$                = "data"
Const NODE_PROPERTIES$          = "properties"
Const NODE_PROPERTY$            = "property"
Const NODE_OBJECTGROUP$         = "objectgroup"
Const NODE_OBJECT$              = "object"