Bug fixes

Bug fixes

Strict

Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_8_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_8_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_9_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_9_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_10_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_10_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_11_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_11_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_12_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_12_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_13_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_13_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_14_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_14_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_15_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_15_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_16_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_16_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_17_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_17_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_18_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_18_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_19_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_19_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_20_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_20_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_21_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_21_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_22_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_22_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_23_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_23_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_24_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_24_flx.fnt"

Import flxextern
Import flxsprite
Import flxcamera
Import flxg
Import system.flxcolor
Import system.flxassetsmanager

#If FLX_TEXT_DRIVER = "angelfont"
	Import vendor.angelfont
#End

Class FlxText Extends FlxSprite Implements FlxSpriteRenderer

	Global ClassObject:Object

	Const ALIGN_LEFT:Float = 0
	Const ALIGN_CENTER:Float = 0.5
	Const ALIGN_RIGHT:Float = 1

	Const SYSTEM_FONT:String = "system"

Private
	Global _FontLoader:FlxFontLoader = New FlxFontLoader()

	Global _FontsManager:FlxFontsManager = New FlxFontsManager()

	Field _shadow:FlxColor

	Field _value:String

	Field _lines:FlxTextLine[]

	Field _countLines:Int

	Field _alignment:Float

	Field _fontFamily:String

	Field _fontSize:Int

	Field _fontHeight:Int

#If FLX_TEXT_DRIVER = "angelfont"
	Field _fontObject:AngelFont
#End

Public
	Method New(x:Float, y:Float, width:Int = 0, text:String = "")
		Super.New(x, y)

		SetRenderer(Self)
		_shadow = New FlxColor(0)

		Self.width = width
		frameWidth = Self.width
		moves = False

		SetFormat(SYSTEM_FONT)
		Text = text
	End Method

	Method Destroy:Void()
		Local i:Int = 0, l:Int = _lines.Length()

		While (i < l)
			_lines[i] = Null
			i += 1
		Wend

		_fontObject = Null
		_shadow = Null

		Super.Destroy()
	End Method

	Method SetFormat:FlxText(font:String = "", size:Int = 0, color:Int = FlxG.WHITE, alignment:Float = ALIGN_LEFT, shadowColor:Int = 0)
		_alignment = alignment
		_fontFamily = font
		_fontSize = FlxAssetsManager.GetFont(font).GetValidSize(size)

		Self.Color = color
		Shadow = shadowColor

		_ResetFont()
		_ResetHelpers()

		Return Self
	End Method

	Method SetWidth:Void(width:Float)
		Self.width = width
		frameWidth = Self.width
		_ResetHelpers()
	End Method

	Method Text:String() Property
		Return _value
	End Method

	Method Text:Void(text:String) Property
		_value = text
		_ResetHelpers()
	End Method

	Method Size:Void(size:Int) Property
		_fontSize = FlxAssetsManager.GetFont(_fontFamily).GetValidSize(size)
		_ResetFont()
		_ResetHelpers()
	End Method

	Method Size:Int() Property
		Return _fontSize
	End Method

	Method Font:String() Property
		Return _fontFamily
	End Method

	Method Font:Void(font:String) Property
		_fontFamily = font
		_ResetFont()
		_ResetHelpers()
	End Method

	Method Alignment:Float() Property
		Return _alignment
	End Method

	Method Alignment:Void(alignment:Float) Property
		If (_alignment = alignment) Return
		_alignment = alignment
		_ResetAlignment()
	End Method

	Method Shadow:Int() Property
		Return _shadow.argb
	End Method

	Method Shadow:Void(color:Int) Property
		_shadow.SetARGB(color)
	End Method

	Method OnSpriteRender:Void(x:Float, y:Float)
		If (_shadow.argb <> 0) Then
			Local oldColor:Int = FlxG._LastDrawingColor
			Local oldAlpha:Int = FlxG._LastDrawingAlpha

			If (_camera.Color <> FlxG.WHITE) Then
				_mixedColor.MixRGB(_shadow, _camera._color)

				If (FlxG._LastDrawingColor <> _mixedColor.argb) Then
					SetColor(_mixedColor.r, _mixedColor.g, _mixedColor.b)
				End If
			Else
				If (FlxG._LastDrawingColor <> _shadow.argb) Then
					SetColor(_shadow.r, _shadow.g, _shadow.b)
				End If
			End If

			If (_camera.Alpha < 1) Then
				Local _mixedAlpha:Float = _camera.Alpha * _shadow.a

				If (FlxG._LastDrawingAlpha <> _mixedAlpha) Then
					SetAlpha(_mixedAlpha)
				End If
			Else
				If (FlxG._LastDrawingAlpha <> _shadow.a) Then
					SetAlpha(_shadow.a)
				End If
			End If

			_fontObject.DrawText(Self, x + 1, y + 1)

			_mixedColor.SetRGB(oldColor)
			SetColor(_mixedColor.r, _mixedColor.g, _mixedColor.b)
			SetAlpha(oldAlpha)
		End If

		_fontObject.DrawText(Self, x, y)
	End Method

Private
	Method _ResetHelpers:Void()
		If (_value.Length() > 0) Then
			If (width <> 0) Then
				_ParseText()
			Else
				_countLines = 0
				_AddLine(0, _value.Length(), _fontObject.GetTextWidth(Self, 0, _value.Length()))
				_ResetAlignment()
			End If

			Self.height = _GetHeight()
			frameHeight = Self.height
		Else
			Self.height = 0
			frameHeight = 0
		End If

		Super._ResetHelpers()
	End Method

	Method _ResetFont:Void()
		_FontLoader.fontFamily = _fontFamily
		_FontLoader.fontSize = _fontSize

		_fontObject = _FontsManager.GetResource(_fontFamily + _fontSize, _FontLoader)
		_fontHeight = _fontObject.GetTextHeight(Self)
	End Method

	Method _ResetAlignment:Void()
		If (_value.Length() = 0) Return

		For Local line:Int = 0 Until _countLines
			_lines[line].x = (width - _lines[line].width) * _alignment
			_lines[line].y = (_fontObject.lineGap + _fontHeight) * line
		Next
	End Method

	Method _GetHeight:Int()
		If (width = 0) Then
			Return _fontHeight
		Else
			Return _fontHeight + _fontObject.lineGap * (_countLines - 1)
		End If
	End Method

	Method _ParseText:Void()
		_countLines = 0

		Local prevOffset:Int = 0
		Local offsetN:Int = _value.Find("~n", 0)
		Local offsetR:Int = _value.Find("~r", 0)
		Local offset:Int = 0

		If (offsetN >= 0 And offsetR >= 0) Then
			offset = Min(offsetN, offsetR)
		Else
			offset = Max(offsetN, offsetR)
		End if

		If (offset >= 0) Then
			While (offset >= 0)
				_BuildLines(prevOffset, offset)

				prevOffset = offset + 1

				offsetN = _value.Find("~n", prevOffset)
				offsetR = _value.Find("~r", prevOffset)

				If (offsetN >= 0 And offsetR >= 0) Then
					offset = Min(offsetN, offsetR)
				Else
					offset = Max(offsetN, offsetR)
				End if
			Wend

			_BuildLines(prevOffset, _value.Length())
		Else
			_BuildLines(0, _value.Length())
		End If

		_ResetAlignment()
	End Method

	Method _BuildLines:Void(startPos:Int, endPos:Int)
		Local textWidth:Int = _fontObject.GetTextWidth(Self, startPos, endPos)

		If (width < textWidth) Then
			Local textLength:Int = endPos - startPos

			Local range:Int = Ceil(textLength / Float(Floor(textWidth / Float(width)) + 1))

			Repeat
				range += 1
				textWidth = _fontObject.GetTextWidth(Self, 0, range)
			Until (textWidth >= width)

			Local maxOffset:Int = range
			Local minOffset:Int = 0
			Local offset:Int = maxOffset
			Local tmpOffset:Int = 0
			Local dirty:Bool = False
			Local finalTextWidth:Int = 0

			Repeat
				Repeat
					offset -= 1
					If (offset - minOffset <= 1) Then
						offset = minOffset + 1
						Exit
					End if

					tmpOffset = -1
					For Local i:Int = offset - 1 To minOffset Step - 1
						If (_value[i] = KEY_SPACE) tmpOffset = i
					Next

					If (tmpOffset < 0) Then
						tmpOffset = _GetMinOffset(minOffset, offset)
					Else
						If (offset - minOffset > 1 And _value[offset] = KEY_SPACE) Then
							minOffset += 1
							offset +=  Min(offset + 2, maxOffset)
							Continue
						EndIf
					End If

					offset = tmpOffset + minOffset
					textWidth = _fontObject.GetTextWidth(Self, minOffset, offset)
				Until (textWidth <= width)

				dirty = False
				finalTextWidth = _fontObject.GetTextWidth(Self, minOffset)

				If (finalTextWidth > width And textLength - minOffset > 1) Then
					For Local i:Int = minOffset Until offset
						If (_value[i] = KEY_SPACE Or _value[i] = KEY_TAB) Then
							minOffset += 1
							dirty = True
						Else
							Exit
						End If
					Next

					For Local i:Int = offset - 1 To minOffset Step - 1
						If (_value[i] = KEY_SPACE Or _value[i] = KEY_TAB) Then
							offset -= 1
							dirty = True
						Else
							Exit
						End If
					Next

					If (offset - minOffset = 0) Continue

					If ( Not dirty) Then
						_AddLine(minOffset, offset, textWidth)
					Else
						_AddLine(minOffset, offset, _fontObject.GetTextWidth(Self, minOffset, offset))
					End If

					minOffset = offset
					maxOffset = minOffset + range
					offset = maxOffset
				Else
					Local l:Int = _value.Length()

					For Local i:Int = minOffset Until l
						If (_value[i] = KEY_SPACE Or _value[i] = KEY_TAB) Then
							minOffset += 1
							dirty = True
						Else
							Exit
						End If
					Next

					For Local i:Int = l - 1 To minOffset Step - 1
						If (_value[i] = KEY_SPACE Or _value[i] = KEY_TAB) Then
							l -= 1
							dirty = True
						Else
							Exit
						End If
					Next

					If (offset - l = 0) Continue

					If ( Not dirty) Then
						_AddLine(minOffset, l, finalTextWidth)
					Else
						_AddLine(minOffset, offset, _fontObject.GetTextWidth(Self, minOffset, l))
					End If

					Exit
				End If
			Forever
		Else
			_AddLine(startPos, endPos, textWidth)
		End If
	End Method

	Method _GetMinOffset:Int(startPos:Int, endPos:Int)
		Local offset:Int = startPos - endPos

		While (_fontObject.GetTextWidth(Self, 0, offset) > width)
			offset -= 1
			If (offset = 0) Return offset
		Wend

		Return offset
	End Method

	Method _AddLine:FlxTextLine(startPos:Int, endPos:Int, width:Int)
		Local line:FlxTextLine

		If (_countLines = _lines.Length()) Then
			_lines = _lines.Resize(_countLines * 2 + 10)
			line = New FlxTextLine(startPos, endPos, width)
			_lines[_countLines] = line
		Else
			line = _lines[_countLines]
			If (line = Null) Then
				line = New FlxTextLine(startPos, endPos, width)
				_lines[_countLines] = line
			Else
				line.Reset(startPos, endPos, width)
			End If
		End If

		_countLines += 1
		Return line
	End Method

End Class

Private
Class FlxTextLine

	Field x:Float

	Field y:Float

	Field startPos:Int

	Field endPos:Int

	Field width:Int

	Method New(startPos:Int, endPos:Int, width:Int)
		Reset(startPos, endPos, width)
	End Method

	Method Reset:Void(startPos:Int, endPos:Int, width:Int)
		Self.startPos = startPos
		Self.endPos = endPos
	End Method

End Class

#If FLX_TEXT_DRIVER = "angelfont"
	Class FlxFontLoader Extends FlxResourceLoader<AngelFont>

		Field fontFamily:String = FlxText.SYSTEM_FONT
		Field fontSize:Int

		Method Load:AngelFont(name:String)
			Local font:AngelFont = New AngelFont()
			font.LoadFontXml(FlxAssetsManager.GetFont(fontFamily).GetPath(fontSize))

			Return font
		End Method

	End Class

	Class FlxFontsManager Extends FlxResourcesManager<AngelFont>
	End Class

	Class AngelFont
	Private
		Field image:Image[] = New Image[1]
		Field chars:Char[256]

		Field kernPairs:IntMap<IntMap<KernPair>> = New IntMap<IntMap<KernPair>>
		Global firstKp:IntMap<KernPair>
		Global secondKp:KernPair

		Field iniText:String

		Field xOffset:Int
		Field yOffset:Int

	Public
		Field useKerning:Bool = True

		Field height:Int = 0
		Field heightOffset:Int = 9999

		Field lineGap:Int = 5

		Method LoadFontXml:Void(url:String)
			iniText = LoadString(url+".fnt")
			Local lines:String[] = iniText.Split(String.FromChar(10))
			Local firstLine:String = lines[0]
			If firstLine.Contains("<?xml")
				Local lineList:List<String> = New List<String>(lines)
				lineList.RemoveFirst()
				lines = lineList.ToArray()
				iniText = "~n".Join(lines)
			End

			Local pageCount:Int = 0

			Local config:= LoadConfig(iniText)

			Local nodes:= config.FindNodesByPath("font/chars/char")

			For Local node:= EachIn nodes
				Local id:Int = Int(node.GetAttribute("id"))
				Local page:Int = Int(node.GetAttribute("page"))
				If pageCount < page pageCount = page
				chars[id] = New Char(Int(node.GetAttribute("x")), Int(node.GetAttribute("y")), Int(node.GetAttribute("width")), Int(node.GetAttribute("height")),  Int(node.GetAttribute("xoffset")),  Int(node.GetAttribute("yoffset")),  Int(node.GetAttribute("xadvance")), page)
				Local ch := chars[id]
				If ch.height > Self.height Self.height = ch.height
				If ch.yOffset < Self.heightOffset Self.heightOffset = ch.yOffset
			Next

			nodes = config.FindNodesByPath("font/kernings/kerning")

			If (nodes <> Null) Then
				For Local node:= EachIn nodes
					Local first:Int = Int(node.GetAttribute("first"))
					firstKp = kernPairs.Get(first)
					If firstKp = Null
						kernPairs.Add(first, New IntMap<KernPair>)
						firstKp = kernPairs.Get(first)
					End

					Local second:Int = Int(node.GetAttribute("second"))
					firstKp.Add(second, New KernPair(first, second, Int(node.GetAttribute("amount"))))
				End
			Else
				useKerning = False
			End If

			If pageCount = 0
				image[0] = LoadImage(url+".png")
				If image[0] = Null image[0] = LoadImage(url+"_0.png")
			Else
				For Local page:= 0 To pageCount
					If image.Length < page+1 image = image.Resize(page+1)
					image[page] = LoadImage(url+"_"+page+".png")
				End
			End
		End

		Method GetTextWidth:Int(txt:FlxText, startPos:Int = 0, endPos:Int = -1)
			Local prevChar:Int = 0
			Local width:Int = 0

			If (endPos < 0) endPos = txt._value.Length()
			Local asc:Int, ac:Char

			For Local i:= startPos Until endPos
				asc = txt._value[i]
				ac = chars[asc]

				If ac <> Null
					If useKerning
						Local firstKp:= kernPairs.Get(prevChar)
						If firstKp <> Null
							Local secondKp:= firstKp.Get(asc)
							If secondKp <> Null
								xOffset += secondKp.amount
							End
						Endif
					EndIf

					width += ac.xAdvance
					prevChar = asc
				EndIf
			Next

			Return width
		End Method

		Method GetTextHeight:Int(txt:FlxText, startPos:Int = 0, endPos:Int = -1)
			Local h:Int = 0

			If (endPos < 0) endPos = txt._value.Length()

			For Local i:= startPos Until endPos
				Local asc:Int = txt._value[i]
				Local ac:Char = chars[asc]
				If ac.height+ac.yOffset > h h = ac.height+ac.yOffset
			Next
			Return h
		End

		Method DrawText:Void(txt:FlxText, x:Int, y:Int)
			Local lineIndex:Int = 0, countLines:Int = txt._countLines, line:FlxTextLine

			Local prevChar:Int = 0, xOffset:Int = 0
			Local i:Int = 0, l:Int
			Local asc:Int, ac:Char

			While (lineIndex < countLines)
				line = txt._lines[lineIndex]
				i = line.startPos
				l = line.endPos

				While (i < l)
					asc = txt._value[i]
					ac = chars[asc]

					If ac <> Null
						If useKerning
							firstKp = kernPairs.Get(prevChar)
							If firstKp <> Null
								secondKp = firstKp.Get(asc)
								If secondKp <> Null
									xOffset += secondKp.amount
								End
							Endif
						Endif

						ac.Draw(image[ac.page], x + xOffset + line.x, y + line.y)
						xOffset += ac.xAdvance
						prevChar = asc
					Endif

					i += 1
				Wend

				lineIndex += 1
			Wend
		End Method

	End Class
#End

Strict

Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_8_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_8_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_9_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_9_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_10_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_10_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_11_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_11_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_12_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_12_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_13_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_13_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_14_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_14_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_15_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_15_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_16_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_16_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_17_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_17_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_18_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_18_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_19_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_19_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_20_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_20_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_21_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_21_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_22_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_22_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_23_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_23_flx.fnt"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_24_flx_0.png"
Import "data/fonts/${FLX_TEXT_DRIVER}/system_font_24_flx.fnt"

Import flxextern
Import flxsprite
Import flxcamera
Import flxg
Import system.flxcolor
Import system.flxassetsmanager

#If FLX_TEXT_DRIVER = "angelfont"
	Import vendor.angelfont
#End

Class FlxText Extends FlxSprite Implements FlxSpriteRenderer

	Global ClassObject:Object

	Const ALIGN_LEFT:Float = 0
	Const ALIGN_CENTER:Float = 0.5
	Const ALIGN_RIGHT:Float = 1

	Const SYSTEM_FONT:String = "system"

Private
	Global _FontLoader:FlxFontLoader = New FlxFontLoader()

	Global _FontsManager:FlxFontsManager = New FlxFontsManager()

	Field _shadow:FlxColor

	Field _value:String

	Field _lines:FlxTextLine[]

	Field _countLines:Int

	Field _alignment:Float

	Field _fontFamily:String

	Field _fontSize:Int

	Field _fontHeight:Int

#If FLX_TEXT_DRIVER = "angelfont"
	Field _fontObject:AngelFont
#End

Public
	Method New(x:Float, y:Float, width:Int = 0, text:String = "")
		Super.New(x, y)

		SetRenderer(Self)
		_shadow = New FlxColor(0)

		Self.width = width
		frameWidth = Self.width
		moves = False

		SetFormat(SYSTEM_FONT)
		Text = text
	End Method

	Method Destroy:Void()
		Local i:Int = 0, l:Int = _lines.Length()

		While (i < l)
			_lines[i] = Null
			i += 1
		Wend

		_fontObject = Null
		_shadow = Null

		Super.Destroy()
	End Method

	Method SetFormat:FlxText(font:String = "", size:Int = 0, color:Int = FlxG.WHITE, alignment:Float = ALIGN_LEFT, shadowColor:Int = 0)
		_alignment = alignment
		_fontFamily = font
		_fontSize = FlxAssetsManager.GetFont(font).GetValidSize(size)

		Self.Color = color
		Shadow = shadowColor

		_ResetFont()
		_ResetHelpers()

		Return Self
	End Method

	Method SetWidth:Void(width:Float)
		Self.width = width
		frameWidth = Self.width
		_ResetHelpers()
	End Method

	Method Text:String() Property
		Return _value
	End Method

	Method Text:Void(text:String) Property
		_value = text
		_ResetHelpers()
	End Method

	Method Size:Void(size:Int) Property
		_fontSize = FlxAssetsManager.GetFont(_fontFamily).GetValidSize(size)
		_ResetFont()
		_ResetHelpers()
	End Method

	Method Size:Int() Property
		Return _fontSize
	End Method

	Method Font:String() Property
		Return _fontFamily
	End Method

	Method Font:Void(font:String) Property
		_fontFamily = font
		_ResetFont()
		_ResetHelpers()
	End Method

	Method Alignment:Float() Property
		Return _alignment
	End Method

	Method Alignment:Void(alignment:Float) Property
		_alignment = alignment
		_ResetAlignment()
	End Method

	Method Shadow:Int() Property
		Return _shadow.argb
	End Method

	Method Shadow:Void(color:Int) Property
		_shadow.SetARGB(color)
	End Method

	Method OnSpriteRender:Void(x:Float, y:Float)
		If (_shadow.argb <> 0) Then
			Local oldColor:Int = FlxG._LastDrawingColor
			Local oldAlpha:Int = FlxG._LastDrawingAlpha

			If (_camera.Color <> FlxG.WHITE) Then
				_mixedColor.MixRGB(_shadow, _camera._color)

				If (FlxG._LastDrawingColor <> _mixedColor.argb) Then
					SetColor(_mixedColor.r, _mixedColor.g, _mixedColor.b)
				End If
			Else
				If (FlxG._LastDrawingColor <> _shadow.argb) Then
					SetColor(_shadow.r, _shadow.g, _shadow.b)
				End If
			End If

			If (_camera.Alpha < 1) Then
				Local _mixedAlpha:Float = _camera.Alpha * _shadow.a

				If (FlxG._LastDrawingAlpha <> _mixedAlpha) Then
					SetAlpha(_mixedAlpha)
				End If
			Else
				If (FlxG._LastDrawingAlpha <> _shadow.a) Then
					SetAlpha(_shadow.a)
				End If
			End If

			_fontObject.DrawText(Self, x + 1, y + 1)

			_mixedColor.SetRGB(oldColor)
			SetColor(_mixedColor.r, _mixedColor.g, _mixedColor.b)
			SetAlpha(oldAlpha)
		End If

		_fontObject.DrawText(Self, x, y)
	End Method

Private
	Method _ResetHelpers:Void()
		If (_value.Length() > 0) Then
			If (width <> 0) Then
				_ParseText()
			Else
				_countLines = 1
			End If

			Self.height = _GetHeight()
			frameHeight = Self.height
		Else
			Self.height = 0
			frameHeight = 0
		End If

		Super._ResetHelpers()
	End Method

	Method _ResetFont:Void()
		_FontLoader.fontFamily = _fontFamily
		_FontLoader.fontSize = _fontSize

		_fontObject = _FontsManager.GetResource(_fontFamily + _fontSize, _FontLoader)
		_fontHeight = _fontObject.GetTextHeight(Self)
	End Method

	Method _ResetAlignment:Void()
		If (_value.Length() = 0) Return

		For Local line:Int = 0 Until _countLines
			_lines[line].x = (width - _lines[line].width) * _alignment
		Next
	End Method

	Method _GetHeight:Int()
		If (width = 0) Then
			Return _fontHeight
		Else
			Return _fontHeight + _fontObject.lineGap * (_countLines - 1)
		End If
	End Method

	Method _ParseText:Void()
		_countLines = 0

		Local prevOffset:Int = 0
		Local offsetN:Int = _value.Find("~n", 0)
		Local offsetR:Int = _value.Find("~r", 0)
		Local offset:Int = 0

		If (offsetN >= 0 And offsetR >= 0) Then
			offset = Min(offsetN, offsetR)
		Else
			offset = Max(offsetN, offsetR)
		End if

		If (offset >= 0) Then
			While (offset >= 0)
				_BuildLines(prevOffset, offset)

				prevOffset = offset + 1

				offsetN = _value.Find("~n", prevOffset)
				offsetR = _value.Find("~r", prevOffset)

				If (offsetN >= 0 And offsetR >= 0) Then
					offset = Min(offsetN, offsetR)
				Else
					offset = Max(offsetN, offsetR)
				End if
			Wend

			_BuildLines(prevOffset, _value.Length())
		Else
			_BuildLines(0, _value.Length())
		End If

		_ResetAlignment()
	End Method

	Method _BuildLines:Void(startPos:Int, endPos:Int)
		Local textWidth:Int = _fontObject.GetTextWidth(Self, startPos, endPos)

		If (width < textWidth) Then
			Local textLength:Int = endPos - startPos

			Local range:Int = Ceil(textLength / Float(Floor(textWidth / Float(width)) + 1))

			Repeat
				range += 1
				textWidth = _fontObject.GetTextWidth(Self, 0, range)
			Until (textWidth >= width)

			Local maxOffset:Int = range
			Local minOffset:Int = 0
			Local offset:Int = maxOffset
			Local tmpOffset:Int = 0
			Local dirty:Bool = False
			Local finalTextWidth:Int = 0

			Repeat
				Repeat
					offset -= 1
					If (offset - minOffset <= 1) Then
						offset = minOffset + 1
						Exit
					End if

					tmpOffset = -1
					For Local i:Int = offset - 1 To minOffset Step - 1
						If (_value[i] = KEY_SPACE) tmpOffset = i
					Next

					If (tmpOffset < 0) Then
						tmpOffset = _GetMinOffset(minOffset, offset)
					Else
						If (offset - minOffset > 1 And _value[offset] = KEY_SPACE) Then
							minOffset += 1
							offset +=  Min(offset + 2, maxOffset)
							Continue
						EndIf
					End If

					offset = tmpOffset + minOffset
					textWidth = _fontObject.GetTextWidth(Self, minOffset, offset)
				Until (textWidth <= width)

				dirty = False
				finalTextWidth = _fontObject.GetTextWidth(Self, minOffset)

				If (finalTextWidth > width And textLength - minOffset > 1) Then
					For Local i:Int = minOffset Until offset
						If (_value[i] = KEY_SPACE Or _value[i] = KEY_TAB) Then
							minOffset += 1
							dirty = True
						Else
							Exit
						End If
					Next

					For Local i:Int = offset - 1 To minOffset Step - 1
						If (_value[i] = KEY_SPACE Or _value[i] = KEY_TAB) Then
							offset -= 1
							dirty = True
						Else
							Exit
						End If
					Next

					If (offset - minOffset = 0) Continue

					If ( Not dirty) Then
						_AddLine(minOffset, offset, textWidth)
					Else
						_AddLine(minOffset, offset, _fontObject.GetTextWidth(Self, minOffset, offset))
					End If

					minOffset = offset
					maxOffset = minOffset + range
					offset = maxOffset
				Else
					Local l:Int = _value.Length()

					For Local i:Int = minOffset Until l
						If (_value[i] = KEY_SPACE Or _value[i] = KEY_TAB) Then
							minOffset += 1
							dirty = True
						Else
							Exit
						End If
					Next

					For Local i:Int = l - 1 To minOffset Step - 1
						If (_value[i] = KEY_SPACE Or _value[i] = KEY_TAB) Then
							l -= 1
							dirty = True
						Else
							Exit
						End If
					Next

					If (offset - l = 0) Continue

					If ( Not dirty) Then
						_AddLine(minOffset, l, finalTextWidth)
					Else
						_AddLine(minOffset, offset, _fontObject.GetTextWidth(Self, minOffset, l))
					End If

					Exit
				End If
			Forever
		Else
			_AddLine(startPos, endPos, textWidth)
		End If
	End Method

	Method _GetMinOffset:Int(startPos:Int, endPos:Int)
		Local offset:Int = startPos - endPos

		While (_fontObject.GetTextWidth(Self, 0, offset) > width)
			offset -= 1
			If (offset = 0) Return offset
		Wend

		Return offset
	End Method

	Method _AddLine:FlxTextLine(startPos:Int, endPos:Int, width:Int)
		Local line:FlxTextLine

		If (_countLines = _lines.Length()) Then
			_lines = _lines.Resize(_countLines * 2 + 10)
			line = New FlxTextLine(startPos, endPos, width)
			_lines[_countLines] = line
		Else
			line = _lines[_countLines]
			If (line = Null) Then
				line = New FlxTextLine(startPos, endPos, width)
				_lines[_countLines] = line
			Else
				line.Reset(startPos, endPos, width)
			End If
		End If

		_countLines += 1
		Return line
	End Method

End Class

Private
Class FlxTextLine

	Field x:Float

	Field y:Float

	Field startPos:Int

	Field endPos:Int

	Field width:Int

	Method New(startPos:Int, endPos:Int, width:Int)
		Reset(startPos, endPos, width)
	End Method

	Method Reset:Void(startPos:Int, endPos:Int, width:Int)
		Self.startPos = startPos
		Self.endPos = endPos
	End Method

End Class

#If FLX_TEXT_DRIVER = "angelfont"
	Class FlxFontLoader Extends FlxResourceLoader<AngelFont>

		Field fontFamily:String = FlxText.SYSTEM_FONT
		Field fontSize:Int

		Method Load:AngelFont(name:String)
			Local font:AngelFont = New AngelFont()
			font.LoadFontXml(FlxAssetsManager.GetFont(fontFamily).GetPath(fontSize))

			Return font
		End Method

	End Class

	Class FlxFontsManager Extends FlxResourcesManager<AngelFont>
	End Class

	Class AngelFont
	Private
		Field image:Image[] = New Image[1]
		Field chars:Char[256]

		Field kernPairs:IntMap<IntMap<KernPair>> = New IntMap<IntMap<KernPair>>
		Global firstKp:IntMap<KernPair>
		Global secondKp:KernPair

		Field iniText:String

		Field xOffset:Int
		Field yOffset:Int

	Public
		Field useKerning:Bool = True

		Field height:Int = 0
		Field heightOffset:Int = 9999

		Field lineGap:Int = 5

		Method LoadFontXml:Void(url:String)
			iniText = LoadString(url+".fnt")
			Local lines:String[] = iniText.Split(String.FromChar(10))
			Local firstLine:String = lines[0]
			If firstLine.Contains("<?xml")
				Local lineList:List<String> = New List<String>(lines)
				lineList.RemoveFirst()
				lines = lineList.ToArray()
				iniText = "~n".Join(lines)
			End

			Local pageCount:Int = 0

			Local config:= LoadConfig(iniText)

			Local nodes:= config.FindNodesByPath("font/chars/char")

			For Local node:= EachIn nodes
				Local id:Int = Int(node.GetAttribute("id"))
				Local page:Int = Int(node.GetAttribute("page"))
				If pageCount < page pageCount = page
				chars[id] = New Char(Int(node.GetAttribute("x")), Int(node.GetAttribute("y")), Int(node.GetAttribute("width")), Int(node.GetAttribute("height")),  Int(node.GetAttribute("xoffset")),  Int(node.GetAttribute("yoffset")),  Int(node.GetAttribute("xadvance")), page)
				Local ch := chars[id]
				If ch.height > Self.height Self.height = ch.height
				If ch.yOffset < Self.heightOffset Self.heightOffset = ch.yOffset
			Next

			nodes = config.FindNodesByPath("font/kernings/kerning")

			If (nodes <> Null) Then
				For Local node:= EachIn nodes
					Local first:Int = Int(node.GetAttribute("first"))
					firstKp = kernPairs.Get(first)
					If firstKp = Null
						kernPairs.Add(first, New IntMap<KernPair>)
						firstKp = kernPairs.Get(first)
					End

					Local second:Int = Int(node.GetAttribute("second"))
					firstKp.Add(second, New KernPair(first, second, Int(node.GetAttribute("amount"))))
				End
			Else
				useKerning = False
			End If

			If pageCount = 0
				image[0] = LoadImage(url+".png")
				If image[0] = Null image[0] = LoadImage(url+"_0.png")
			Else
				For Local page:= 0 To pageCount
					If image.Length < page+1 image = image.Resize(page+1)
					image[page] = LoadImage(url+"_"+page+".png")
				End
			End
		End

		Method GetTextWidth:Int(txt:FlxText, startPos:Int = 0, endPos:Int = -1)
			Local prevChar:Int = 0
			Local width:Int = 0

			If (endPos < 0) endPos = txt._value.Length()

			For Local i:= startPos Until endPos
				Local asc:Int = txt._value[i]
				Local ac:Char = chars[asc]
				Local thisChar:Int = asc
				If ac  <> Null
					If useKerning
						Local firstKp:= kernPairs.Get(prevChar)
						If firstKp <> Null
							Local secondKp:= firstKp.Get(thisChar)
							If secondKp <> Null
								xOffset += secondKp.amount
							End
						Endif
					Endif
					width += ac.xAdvance
					prevChar = thisChar
				EndIf
			Next

			Return width
		End Method

		Method GetTextHeight:Int(txt:FlxText, startPos:Int = 0, endPos:Int = -1)
			Local h:Int = 0

			If (endPos < 0) endPos = txt._value.Length()

			For Local i:= startPos Until endPos
				Local asc:Int = txt._value[i]
				Local ac:Char = chars[asc]
				If ac.height+ac.yOffset > h h = ac.height+ac.yOffset
			Next
			Return h
		End

		Method DrawText:Void(txt:FlxText, x:Int, y:Int)
			If (txt._countLines = 1) Then
				Local prevChar:Int = 0
				xOffset = 0

				Local i:Int = 0, l:Int = txt._value.Length
				Local asc:Int, ac:Char

				While (i < l)
					asc = txt._value[i]
					ac = chars[asc]

					If ac <> Null
						If useKerning
							firstKp = kernPairs.Get(prevChar)
							If firstKp <> Null
								secondKp = firstKp.Get(asc)
								If secondKp <> Null
									xOffset += secondKp.amount
								End
							Endif
						Endif
						ac.Draw(image[ac.page], x + xOffset, y)
						xOffset += ac.xAdvance
						prevChar = asc
					Endif

					i += 1
				Wend
			Else
				Local line:Int, countLines:Int = txt._countLines

				Local prevChar:Int = 0, xOffset:Int = 0, yOffset:Int = 0

				Local i:Int = 0, l:Int = txt._value.Length
				Local asc:Int, ac:Char

				While (line < countLines)
					While (i < l)
						asc = txt._value[i]
						ac = chars[asc]

						If ac <> Null
							If useKerning
								firstKp = kernPairs.Get(prevChar)
								If firstKp <> Null
									secondKp = firstKp.Get(asc)
									If secondKp <> Null
										xOffset += secondKp.amount
									End
								Endif
							Endif
							ac.Draw(image[ac.page], x + xOffset, y + yOffset)
							xOffset += ac.xAdvance
							prevChar = asc
						Endif

						i += 1
					Wend

					yOffset += txt._fontHeight + lineGap
					line += 1
				Wend
			End If
		End Method

	End Class
#End


Added image support, customisable death emitter targets, and speed optimisations.

--HG--
extra : convert_revision : svn%3Ae934d3cd-0d57-f34a-94d0-9e8d31ae1c29/trunk%40280

Import diddy

Class ParticleSystem
Private
' Private fields
	Field groups:ArrayList<ParticleGroup>

Public
' Properties
	Method Groups:ArrayList<ParticleGroup>() Property
		Return groups
	End

' Constructors
	Method New()
		groups = New ArrayList<ParticleGroup>
	End

' Public methods
	Method Render:Void()
		Local rgb:Float[] = GetColor()
		Local alpha:Float = GetAlpha()
		For Local i:Int = 0 Until groups.Size
			groups.Get(i).Render()
		Next
		SetAlpha(alpha)
		SetColor(rgb[0], rgb[1], rgb[2])
	End

	Method Update:Void(delta:Float)
		For Local i:Int = 0 Until groups.Size
			groups.Get(i).Update(delta)
		Next
	End
End

Class Emitter
Private
' Property fields
	Field velocityX:Float                       ' the default X velocity
	Field velocityXSpread:Float                 ' the X velocity random spread
	Field velocityY:Float                       ' the default Y velocity
	Field velocityYSpread:Float                 ' the Y velocity random spread
	Field polarVelocityAmplitude:Float          ' the default polar velocity amplitude
	Field polarVelocityAmplitudeSpread:Float    ' the polar velocity amplitude random spread
	Field polarVelocityAngle:Float              ' the default polar velocity angle
	Field polarVelocityAngleSpread:Float        ' the polar velocity angle random spread
	Field usePolar:Bool                         ' whether we should use a polar velocity
	Field spawnMinRange:Float                   ' the minimum distance to spawn from the emit point
	Field spawnMaxRange:Float                   ' the maximum distance to spawn from the emit point
	Field life:Float                            ' the default life of the particle in seconds
	Field lifeSpread:Float                      ' the life spread in seconds

	' colours
	Field redInterpolation:Int = INTERPOLATION_NONE     ' interpolates the particle's red based on life
	Field redInterpolationTime:Float = -1               ' the number of seconds to interpolate across (if <0, defaults to life)
	Field greenInterpolation:Int = INTERPOLATION_NONE   ' interpolates the particle's green based on life
	Field greenInterpolationTime:Float = -1             ' the number of seconds to interpolate across (if <0, defaults to life)
	Field blueInterpolation:Int = INTERPOLATION_NONE    ' interpolates the particle's blue based on life
	Field blueInterpolationTime:Float = -1              ' the number of seconds to interpolate across (if <0, defaults to life)
	Field alphaInterpolation:Int = INTERPOLATION_LINEAR ' interpolates the particle's alpha based on life
	Field alphaInterpolationTime:Float = -1             ' the number of seconds to interpolate across (if <0, defaults to life)

	Field minStartRed:Int = 255, maxStartRed:Int = 255
	Field minStartGreen:Int = 255, maxStartGreen:Int = 255
	Field minStartBlue:Int = 255, maxStartBlue:Int = 255
	Field minStartAlpha:Float = 1, maxStartAlpha:Float = 1

	Field minEndRed:Int = 255, maxEndRed:Int = 255
	Field minEndGreen:Int = 255, maxEndGreen:Int = 255
	Field minEndBlue:Int = 255, maxEndBlue:Int = 255
	Field minEndAlpha:Float = 0, maxEndAlpha:Float = 0

	Field particleImage:Image

' Emitter info
	Field x:Float
	Field y:Float
	Field amplitude:Float = 10
	Field angle:Float
	Field group:ParticleGroup

' Death emitters
	Field deathEmitters:ArrayList<Emitter>      ' the death emitters will fire at the particle's point of death
	                                            ' using the particle's normalised velocity
	Field deathEmitterChances:FloatArrayList    ' 0-1 random chance as to whether the death emitter will fire

Public
' Properties
	' velocityX
	Method VelocityX:Float() Property
		Return velocityX
	End
	Method VelocityX:Void(velocityX:Float) Property
		Self.velocityX = velocityX
	End

	' velocityXSpread
	Method VelocityXSpread:Float() Property
		Return velocityXSpread
	End
	Method VelocityX:Void(velocityXSpread:Float) Property
		Self.velocityXSpread = velocityXSpread
	End

	' velocityY
	Method VelocityY:Float() Property
		Return velocityY
	End
	Method VelocityY:Void(velocityY:Float) Property
		Self.velocityY = velocityY
	End

	' velocityYSpread
	Method VelocityYSpread:Float() Property
		Return velocityYSpread
	End
	Method VelocityY:Void(velocityYSpread:Float) Property
		Self.velocityYSpread = velocityYSpread
	End

	' polarVelocityAmplitude
	Method PolarVelocityAmplitude:Float() Property
		Return polarVelocityAmplitude
	End
	Method PolarVelocityAmplitude:Void(polarVelocityAmplitude:Float) Property
		Self.polarVelocityAmplitude = polarVelocityAmplitude
	End

	' polarVelocityAmplitudeSpread
	Method PolarVelocityAmplitudeSpread:Float() Property
		Return polarVelocityAmplitudeSpread
	End
	Method PolarVelocityAmplitudeSpread:Void(polarVelocityAmplitudeSpread:Float) Property
		Self.polarVelocityAmplitudeSpread = polarVelocityAmplitudeSpread
	End

	' polarVelocityAngle
	Method PolarVelocityAngle:Float() Property
		Return polarVelocityAngle
	End
	Method PolarVelocityAngle:Void(polarVelocityAngle:Float) Property
		Self.polarVelocityAngle = polarVelocityAngle
	End

	' polarVelocityAngleSpread
	Method PolarVelocityAngleSpread:Float() Property
		Return polarVelocityAngleSpread
	End
	Method PolarVelocityAngleSpread:Void(polarVelocityAngleSpread:Float) Property
		Self.polarVelocityAngleSpread = polarVelocityAngleSpread
	End

	' usePolar
	Method UsePolar:Bool() Property
		Return usePolar
	End
	Method UsePolar:Void(usePolar:Bool) Property
		Self.usePolar = usePolar
	End

	' spawnMinRange
	Method SpawnMinRange:Float() Property
		Return spawnMinRange
	End
	Method SpawnMinRange:Void(spawnMinRange:Float) Property
		Self.spawnMinRange = spawnMinRange
	End

	' spawnMaxRange
	Method SpawnMaxRange:Float() Property
		Return spawnMaxRange
	End
	Method SpawnMaxRange:Void(spawnMaxRange:Float) Property
		Self.spawnMaxRange = spawnMaxRange
	End

	' life
	Method Life:Float() Property
		Return life
	End
	Method Life:Void(life:Float) Property
		Self.life = life
	End

	' lifeSpread
	Method LifeSpread:Float() Property
		Return lifeSpread
	End
	Method LifeSpread:Void(lifeSpread:Float) Property
		Self.lifeSpread = lifeSpread
	End

	' redInterpolation
	Method RedInterpolation:Int() Property
		Return redInterpolation
	End
	Method RedInterpolation:Void(redInterpolation:Int) Property
		AssertRangeInt(redInterpolation, INTERPOLATION_NONE, INTERPOLATION_COUNT, "Invalid RedInterpolation")
		Self.redInterpolation = redInterpolation
	End

	' redInterpolationTime
	Method RedInterpolationTime:Float() Property
		Return redInterpolationTime
	End
	Method RedInterpolationTime:Void(redInterpolationTime:Float) Property
		Self.redInterpolationTime = redInterpolationTime
	End

	' greenInterpolation
	Method GreenInterpolation:Int() Property
		Return greenInterpolation
	End
	Method GreenInterpolation:Void(greenInterpolation:Int) Property
		AssertRangeInt(greenInterpolation, INTERPOLATION_NONE, INTERPOLATION_COUNT, "Invalid GreenInterpolation")
		Self.greenInterpolation = greenInterpolation
	End

	' greenInterpolationTime
	Method GreenInterpolationTime:Float() Property
		Return greenInterpolationTime
	End
	Method GreenInterpolationTime:Void(greenInterpolationTime:Float) Property
		Self.greenInterpolationTime = greenInterpolationTime
	End

	' blueInterpolation
	Method BlueInterpolation:Int() Property
		Return blueInterpolation
	End
	Method BlueInterpolation:Void(blueInterpolation:Int) Property
		AssertRangeInt(blueInterpolation, INTERPOLATION_NONE, INTERPOLATION_COUNT, "Invalid BlueInterpolation")
		Self.blueInterpolation = blueInterpolation
	End

	' blueInterpolationTime
	Method BlueInterpolationTime:Float() Property
		Return blueInterpolationTime
	End
	Method BlueInterpolationTime:Void(blueInterpolationTime:Float) Property
		Self.blueInterpolationTime = blueInterpolationTime
	End

	' alphaInterpolation
	Method AlphaInterpolation:Int() Property
		Return alphaInterpolation
	End
	Method AlphaInterpolation:Void(alphaInterpolation:Int) Property
		AssertRangeInt(alphaInterpolation, INTERPOLATION_NONE, INTERPOLATION_COUNT, "Invalid AlphaInterpolation")
		Self.alphaInterpolation = alphaInterpolation
	End

	' alphaInterpolationTime
	Method AlphaInterpolationTime:Float() Property
		Return alphaInterpolationTime
	End
	Method AlphaInterpolationTime:Void(alphaInterpolationTime:Float) Property
		Self.alphaInterpolationTime = alphaInterpolationTime
	End

	' minStartRed
	Method MinStartRed:Int() Property
		Return minStartRed
	End
	Method MinStartRed:Void(minStartRed:Int) Property
		Self.minStartRed = minStartRed
	End

	' maxStartRed
	Method MaxStartRed:Int() Property
		Return maxStartRed
	End
	Method MaxStartRed:Void(maxStartRed:Int) Property
		Self.maxStartRed = maxStartRed
	End

	' minStartGreen
	Method MinStartGreen:Int() Property
		Return minStartGreen
	End
	Method MinStartGreen:Void(minStartGreen:Int) Property
		Self.minStartGreen = minStartGreen
	End

	' maxStartGreen
	Method MaxStartGreen:Int() Property
		Return maxStartGreen
	End
	Method MaxStartGreen:Void(maxStartGreen:Int) Property
		Self.maxStartGreen = maxStartGreen
	End

	' minStartBlue
	Method MinStartBlue:Int() Property
		Return minStartBlue
	End
	Method MinStartBlue:Void(minStartBlue:Int) Property
		Self.minStartBlue = minStartBlue
	End

	' maxStartBlue
	Method MaxStartBlue:Int() Property
		Return maxStartBlue
	End
	Method MaxStartBlue:Void(maxStartBlue:Int) Property
		Self.maxStartBlue = maxStartBlue
	End

	' minStartAlpha
	Method MinStartAlpha:Float() Property
		Return minStartAlpha
	End
	Method MinStartAlpha:Void(minStartAlpha:Float) Property
		Self.minStartAlpha = minStartAlpha
	End

	' maxStartAlpha
	Method MaxStartAlpha:Float() Property
		Return maxStartAlpha
	End
	Method MaxStartAlpha:Void(maxStartAlpha:Float) Property
		Self.maxStartAlpha = maxStartAlpha
	End

	' minEndRed
	Method MinEndRed:Int() Property
		Return minEndRed
	End
	Method MinEndRed:Void(minEndRed:Int) Property
		Self.minEndRed = minEndRed
	End

	' maxEndRed
	Method MaxEndRed:Int() Property
		Return maxEndRed
	End
	Method MaxEndRed:Void(maxEndRed:Int) Property
		Self.maxEndRed = maxEndRed
	End

	' minEndGreen
	Method MinEndGreen:Int() Property
		Return minEndGreen
	End
	Method MinEndGreen:Void(minEndGreen:Int) Property
		Self.minEndGreen = minEndGreen
	End

	' maxEndGreen
	Method MaxEndGreen:Int() Property
		Return maxEndGreen
	End
	Method MaxEndGreen:Void(maxEndGreen:Int) Property
		Self.maxEndGreen = maxEndGreen
	End

	' minEndBlue
	Method MinEndBlue:Int() Property
		Return minEndBlue
	End
	Method MinEndBlue:Void(minEndBlue:Int) Property
		Self.minEndBlue = minEndBlue
	End

	' maxEndBlue
	Method MaxEndBlue:Int() Property
		Return maxEndBlue
	End
	Method MaxEndBlue:Void(maxEndBlue:Int) Property
		Self.maxEndBlue = maxEndBlue
	End

	' minEndAlpha
	Method MinEndAlpha:Float() Property
		Return minEndAlpha
	End
	Method MinEndAlpha:Void(minEndAlpha:Float) Property
		Self.minEndAlpha = minEndAlpha
	End

	' maxEndAlpha
	Method MaxEndAlpha:Float() Property
		Return maxEndAlpha
	End
	Method MaxEndAlpha:Void(maxEndAlpha:Float) Property
		Self.maxEndAlpha = maxEndAlpha
	End

	' particleImage
	Method ParticleImage:Image() Property
		Return particleImage
	End
	Method ParticleImage:Void(particleImage:Image) Property
		Self.particleImage = particleImage
	End

	' x
	Method X:Float() Property
		Return x
	End
	Method X:Void(x:Float) Property
		Self.x = x
	End

	' y
	Method Y:Float() Property
		Return y
	End
	Method Y:Void(y:Float) Property
		Self.y = y
	End

	' angle
	Method Angle:Float() Property
		Return angle
	End
	Method Angle:Void(angle:Float) Property
		Self.angle = angle
	End

	' amplitude
	Method Amplitude:Float() Property
		Return amplitude
	End
	Method Amplitude:Void(amplitude:Float) Property
		Self.amplitude = amplitude
	End

	' group
	Method Group:ParticleGroup() Property
		Return group
	End
	Method Group:Void(group:ParticleGroup) Property
		Self.group = group
	End

' Constructors
	Method New()
		deathEmitters = New ArrayList<Emitter>
		deathEmitterChances = New FloatArrayList
	End

' Convenience setters
	Method SetParticleRGBInterpolated:Void(startRed:Int, startGreen:Int, startBlue:Int, endRed:Int, endGreen:Int, endBlue:Int)
		minStartRed = startRed
		maxStartRed = startRed
		minEndRed = endRed
		maxEndRed = endRed
		If startRed <> endRed Then
			redInterpolation = INTERPOLATION_LINEAR
			redInterpolationTime = -1
		Else
			redInterpolation = INTERPOLATION_NONE
		End

		minStartGreen = startGreen
		maxStartGreen = startGreen
		minEndGreen = endGreen
		maxEndGreen = endGreen
		If startGreen <> endGreen Then
			greenInterpolation = INTERPOLATION_LINEAR
			greenInterpolationTime = -1
		Else
			greenInterpolation = INTERPOLATION_NONE
		End

		minStartBlue = startBlue
		maxStartBlue = startBlue
		minEndBlue = endBlue
		maxEndBlue = endBlue
		If startBlue <> endBlue Then
			blueInterpolation = INTERPOLATION_LINEAR
			blueInterpolationTime = -1
		Else
			blueInterpolation = INTERPOLATION_NONE
		End
	End

	Method SetParticleRGB:Void(red:Int, green:Int, blue:Int)
		minStartRed = red
		maxStartRed = red
		minEndRed = red
		maxEndRed = red
		redInterpolation = INTERPOLATION_NONE
		minStartGreen = green
		maxStartGreen = green
		minEndGreen = green
		maxEndGreen = green
		greenInterpolation = INTERPOLATION_NONE
		minStartBlue = blue
		maxStartBlue = blue
		minEndBlue = blue
		maxEndBlue = blue
		blueInterpolation = INTERPOLATION_NONE
	End

	Method SetParticleAlpha:Void(alpha:Float, time:Int=-1)
		minStartAlpha = alpha
		maxStartAlpha = alpha
		minEndAlpha = 0
		maxEndAlpha = 0
		alphaInterpolation = INTERPOLATION_LINEAR
		If time < 0 Then time = life
		alphaInterpolationTime = life
	End

	Method SetVelocity:Void(velocityX:Float, velocityY:Float)
		SetVelocity(velocityX, 0, velocityY, 0)
	End

	Method SetVelocity:Void(velocityX:Float, velocityXSpread:Float, velocityY:Float, velocityYSpread:Float)
		usePolar = False
		Self.velocityX = velocityX
		Self.velocityXSpread = velocityXSpread
		Self.velocityY = velocityY
		Self.velocityYSpread = velocityYSpread
	End

	Method SetPolarVelocity:Void(polarVelocityAngle:Float, polarVelocityAmplitude:Float)
		SetPolarVelocity(polarVelocityAngle, 0, polarVelocityAmplitude, 0)
	End

	Method SetPolarVelocity:Void(polarVelocityAngle:Float, polarVelocityAngleSpread:Float, polarVelocityAmplitude:Float, polarVelocityAmplitudeSpread:Float)
		usePolar = True
		Self.polarVelocityAngle = polarVelocityAngle
		Self.polarVelocityAngleSpread = polarVelocityAngleSpread
		Self.polarVelocityAmplitude = polarVelocityAmplitude
		Self.polarVelocityAmplitudeSpread = polarVelocityAmplitudeSpread
	End

' Death emitter methods
	Method AddDeathEmitter(emitter:Emitter, chance:Float)
		deathEmitters.Add(emitter)
		deathEmitterChances.AddFloat(chance)
	End

	Method RemoveDeathEmitter(emitter:Emitter)
		Local idx:Int = deathEmitters.IndexOf(emitter)
		If idx >= 0 Then
			deathEmitters.RemoveAt(idx)
			deathEmitterChances.RemoveAt(idx)
		End
	End

	Method SetDeathEmitterChance(emitter:Emitter, chance:Float)
		Local idx:Int = deathEmitters.IndexOf(emitter)
		If idx >= 0 Then
			deathEmitterChances.SetFloat(idx, chance)
		End
	End

' Emits
	Method Emit:Void(amount:Int, group:ParticleGroup=Null)
		EmitAtAngle(amount, x, y, angle, group)
	End

	Method EmitAt:Void(amount:Int, emitX:Float, emitY:Float, group:ParticleGroup=Null)
		EmitAtAngle(amount, emitX, emitY, angle, group)
	End

	Method EmitAngle:Void(amount:Int, emitAngle:Float, emitAmplitude:Float, group:ParticleGroup=Null)
		'EmitAtAngle(group, amount, x, y, emitAngle, group)
	End

	Method EmitAtAngle:Void(amount:Int, emitX:Float, emitY:Float, emitAngle:Float, group:ParticleGroup=Null)
		' if not passed a group, use the assigned one
		If group = Null Then group = Self.group
		' create "amount" number of particles
		For Local i:Int = 0 Until amount
			' get an index for a new particle
			Local index:Int = group.CreateParticle()
			' die if we're full
			If index < 0 Then Exit

			Local spawnAngle:Float = Rnd() * 2 * PI
			Local spawnDistance:Float = spawnMinRange + Rnd() * (spawnMaxRange-spawnMinRange)
			group.x[index] = emitX + Cosr(spawnAngle) * spawnDistance
			group.y[index] = emitY + Sinr(spawnAngle) * spawnDistance
			group.usePolar[index] = usePolar
			If usePolar Then
				' TODO: adjust for src speed
				group.polarVelocityAngle[index] = emitAngle + polarVelocityAngle - polarVelocityAngleSpread/2 + Rnd() * polarVelocityAngleSpread
				group.polarVelocityAmplitude[index] = polarVelocityAmplitude - polarVelocityAmplitudeSpread/2 + Rnd() * polarVelocityAmplitudeSpread
				group.UpdateCartesian(index)
			Else
				' TODO: adjust for src angle and speed
				group.velocityX[index] = velocityX - velocityXSpread/2 + Rnd() * velocityXSpread
				group.velocityY[index] = velocityY - velocityYSpread/2 + Rnd() * velocityYSpread
			End
			group.sourceEmitter[index] = Self
			group.alive[index] = True
			group.life[index] = life - lifeSpread/2 + Rnd() * lifeSpread

			' image
			group.particleImage[index] = particleImage

			' start colours
			If minStartRed <> maxStartRed Then
				group.startRed[index] = Max(0,Min(255,Int(minStartRed + Rnd() * (maxStartRed-minStartRed))))
			Else
				group.startRed[index] = minStartRed
			End
			If minStartGreen <> maxStartGreen Then
				group.startGreen[index] = Max(0,Min(255,Int(minStartGreen + Rnd() * (maxStartRed-minStartGreen))))
			Else
				group.startGreen[index] = minStartGreen
			End
			If minStartBlue <> maxStartBlue Then
				group.startBlue[index] = Max(0,Min(255,Int(minStartBlue + Rnd() * (maxStartBlue-minStartBlue))))
			Else
				group.startBlue[index] = minStartBlue
			End
			If minStartAlpha <> maxStartAlpha Then
				group.startAlpha[index] = Max(0.0,Min(1.0,minStartAlpha + Rnd() * (maxStartAlpha-minStartAlpha)))
			Else
				group.startAlpha[index] = minStartAlpha
			End

			group.red[index] = group.startRed[index]
			group.green[index] = group.startGreen[index]
			group.blue[index] = group.startBlue[index]
			group.alpha[index] = group.startAlpha[index]

			' end colours
			If minEndRed <> maxEndRed Then
				group.endRed[index] = Max(0,Min(255,Int(minEndRed + Rnd() * (maxEndRed-minEndRed))))
			Else
				group.endRed[index] = minEndRed
			End
			If minEndGreen <> maxEndGreen Then
				group.endGreen[index] = Max(0,Min(255,Int(minEndGreen + Rnd() * (maxEndRed-minEndGreen))))
			Else
				group.endGreen[index] = minEndGreen
			End
			If minEndBlue <> maxEndBlue Then
				group.endBlue[index] = Max(0,Min(255,Int(minEndBlue + Rnd() * (maxEndBlue-minEndBlue))))
			Else
				group.endBlue[index] = minEndBlue
			End
			If minEndAlpha <> maxEndAlpha Then
				group.endAlpha[index] = Max(0.0,Min(1.0,minEndAlpha + Rnd() * (maxEndAlpha-minEndAlpha)))
			Else
				group.endAlpha[index] = minEndAlpha
			End

			' interpolation
			If group.startRed[index] = group.endRed[index] Then
				group.redInterpolation[index] = INTERPOLATION_NONE
			Else
				group.redInterpolation[index] = redInterpolation
				group.redInterpolationTime[index] = redInterpolationTime
				If group.redInterpolationTime[index] < 0 Then group.redInterpolationTime[index] = group.life[index]
			End
			If group.startGreen[index] = group.endGreen[index] Then
				group.greenInterpolation[index] = INTERPOLATION_NONE
			Else
				group.greenInterpolation[index] = greenInterpolation
				group.greenInterpolationTime[index] = greenInterpolationTime
				If group.greenInterpolationTime[index] < 0 Then group.greenInterpolationTime[index] = group.life[index]
			End
			If group.startBlue[index] = group.endBlue[index] Then
				group.blueInterpolation[index] = INTERPOLATION_NONE
			Else
				group.blueInterpolation[index] = blueInterpolation
				group.blueInterpolationTime[index] = blueInterpolationTime
				If group.blueInterpolationTime[index] < 0 Then group.blueInterpolationTime[index] = group.life[index]
			End
			If group.startAlpha[index] = group.endAlpha[index] Then
				group.alphaInterpolation[index] = INTERPOLATION_NONE
			Else
				group.alphaInterpolation[index] = alphaInterpolation
				group.alphaInterpolationTime[index] = alphaInterpolationTime
				If group.alphaInterpolationTime[index] < 0 Then group.alphaInterpolationTime[index] = group.life[index]
			End
		Next
	End
End

Class ParticleGroup
Private
	' fields for each particle (more efficient to have multiple large arrays than incredible amounts of Particle objects
	Field x:Float[]
	Field y:Float[]
	Field velocityX:Float[]
	Field velocityY:Float[]
	Field polarVelocityAmplitude:Float[]
	Field polarVelocityAngle:Float[] ' radians
	Field usePolar:Bool[]
	Field mass:Float[]

	Field red:Int[]
	Field green:Int[]
	Field blue:Int[]
	Field alpha:Float[]

	Field particleImage:Image[]

	Field startRed:Int[]
	Field startGreen:Int[]
	Field startBlue:Int[]
	Field startAlpha:Float[]

	Field endRed:Int[]
	Field endGreen:Int[]
	Field endBlue:Int[]
	Field endAlpha:Float[]

	Field redInterpolation:Int[]
	Field redInterpolationTime:Float[]
	Field greenInterpolation:Int[]
	Field greenInterpolationTime:Float[]
	Field blueInterpolation:Int[]
	Field blueInterpolationTime:Float[]
	Field alphaInterpolation:Int[]
	Field alphaInterpolationTime:Float[]

	Field life:Float[]
	Field alive:Bool[]
	Field reversePointer:Int[]
	Field sourceEmitter:Emitter[]

	Field maxParticles:Int ' read only property
	Field alivePointers:Int[]
	Field aliveParticles:Int ' read only property

	' acceleration, etc.
	Field accelerationX:Float
	Field accelerationY:Float

	' cached arrays for clearing out dead particles
	Field deadParticles:Int[]
	Field deadEmitters:Emitter[]
	Field deadX:Float[]
	Field deadY:Float[]
	Field deadVelocityX:Float[]
	Field deadVelocityY:Float[]
	Field deadCount:Int = 0

	' constants for the group
	Field forces:ArrayList<Force>

Public
' Properties
	Method AliveParticles:Int() Property
		Return aliveParticles
	End

	Method MaxParticles:Int() Property
		Return maxParticles
	End

	Method Forces:ArrayList<Force>() Property
		Return forces
	End

' Constructors
	Method New(maxParticles:Int)
		Self.maxParticles = maxParticles
		forces = New ArrayList<Force>

		x = New Float[maxParticles]
		y = New Float[maxParticles]
		velocityX = New Float[maxParticles]
		velocityY = New Float[maxParticles]
		polarVelocityAmplitude = New Float[maxParticles]
		polarVelocityAngle = New Float[maxParticles]
		usePolar = New Bool[maxParticles]
		sourceEmitter = New Emitter[maxParticles]
		life = New Float[maxParticles]
		alive = New Bool[maxParticles]
		mass = New Float[maxParticles]
		particleImage = New Image[maxParticles]

		red = New Int[maxParticles]
		green = New Int[maxParticles]
		blue = New Int[maxParticles]
		alpha = New Float[maxParticles]

		startRed = New Int[maxParticles]
		startGreen = New Int[maxParticles]
		startBlue = New Int[maxParticles]
		startAlpha = New Float[maxParticles]

		endRed = New Int[maxParticles]
		endGreen = New Int[maxParticles]
		endBlue = New Int[maxParticles]
		endAlpha = New Float[maxParticles]

		redInterpolation = New Int[maxParticles]
		redInterpolationTime = New Float[maxParticles]
		greenInterpolation = New Int[maxParticles]
		greenInterpolationTime = New Float[maxParticles]
		blueInterpolation = New Int[maxParticles]
		blueInterpolationTime = New Float[maxParticles]
		alphaInterpolation = New Int[maxParticles]
		alphaInterpolationTime = New Float[maxParticles]

		reversePointer = New Int[maxParticles]
		alivePointers = New Int[maxParticles]

		' these are populated before a dead particle is cleared
		deadParticles = New Int[maxParticles]
		deadEmitters = New Emitter[maxParticles]
		deadX = New Float[maxParticles]
		deadY = New Float[maxParticles]
		deadVelocityX = New Float[maxParticles]
		deadVelocityY = New Float[maxParticles]

		ResetParticles()
	End

	Method Update:Void(delta:Float)
		' convert milliseconds to seconds
		delta = delta/1000
		deadCount = 0
		' loop through all living particles
		For Local i:Int = 0 Until aliveParticles
			Local index:Int = alivePointers[i]
			life[index] -= delta
			If life[index] > 0 Then
				' apply acceleration
				If Not forces.IsEmpty() Then
					' apply forces
					For Local fi:Int = 0 Until forces.Size
						Local f:Force = forces.Get(fi)
						velocityX[index] += f.ApplyX(velocityX[index], mass[index]) * delta
						velocityY[index] += f.ApplyY(velocityY[index], mass[index]) * delta
					Next
					' TODO: terminal velocity
				End
				' update position
				x[index] += velocityX[index] * delta
				y[index] += velocityY[index] * delta
				' interpolate colours
				If startRed[index] <> endRed[index] And life[index] < redInterpolationTime[index] Then
					red[index] = Int(Interpolate(redInterpolation[index], startRed[index], endRed[index], 1 - life[index]/redInterpolationTime[index]))
				End
				If startGreen[index] <> endGreen[index] And life[index] < greenInterpolationTime[index] Then
					green[index] = Int(Interpolate(greenInterpolation[index], startGreen[index], endGreen[index], 1 - life[index]/greenInterpolationTime[index]))
				End
				If startBlue[index] <> endBlue[index] And life[index] < blueInterpolationTime[index] Then
					blue[index] = Int(Interpolate(blueInterpolation[index], startBlue[index], endBlue[index], 1 - life[index]/blueInterpolationTime[index]))
				End
				If startAlpha[index] <> endAlpha[index] And life[index] < alphaInterpolationTime[index] Then
					alpha[index] = Interpolate(alphaInterpolation[index], startAlpha[index], endAlpha[index], 1 - life[index]/alphaInterpolationTime[index])
				End
			Else
				alive[index] = False
				' cache info from dead particle
				deadParticles[deadCount] = index
				deadEmitters[deadCount] = sourceEmitter[index]
				deadX[deadCount] = x[index]
				deadY[deadCount] = y[index]
				deadVelocityX[deadCount] = velocityX[index]
				deadVelocityY[deadCount] = velocityY[index]
				deadCount += 1
			End
		Next
		' remove all the dead particles from the pointer array
		For Local i:Int = 0 Until deadCount
			RemoveParticle(deadParticles[i])
		Next
		' now fire off any emitters
		For Local i:Int = 0 Until deadCount
			For Local j:Int = 0 Until deadEmitters[i].deathEmitterChances.Size
				Local c:Float = deadEmitters[i].deathEmitterChances.Get(j)
				If c = 1 Or c > 0 And Rnd() <= c Then
					Local e:Emitter = deadEmitters[i].deathEmitters.Get(j)
					' if the emitter has no group assigned, we use Self
					If e.group = Null Then
						e.EmitAtAngle(30, deadX[i], deadY[i], SafeATanr(deadVelocityX[i], deadVelocityY[i]), Self)
					Else
						e.EmitAtAngle(30, deadX[i], deadY[i], SafeATanr(deadVelocityX[i], deadVelocityY[i]))
					End
				End
			Next
			deadEmitters[i] = Null
		Next
	End

	Method UpdatePolar(index:Int)
		Local angle:Float = SafeATanr(velocityX[index], velocityY[index], polarVelocityAngle[index])
		polarVelocityAngle[index] = angle
		polarVelocityAmplitude[index] = Sqrt(velocityX[index]*velocityX[index] + velocityY[index]*velocityY[index])
	End

	Method UpdateCartesian(index:Int)
		velocityX[index] = Cosr(polarVelocityAngle[index]) * polarVelocityAmplitude[index]
		velocityY[index] = Sinr(polarVelocityAngle[index]) * polarVelocityAmplitude[index]
	End

	Method CreateParticle:Int()
		If aliveParticles >= maxParticles Then Return -1
		aliveParticles += 1
		ClearParticle(alivePointers[aliveParticles-1])
		Return alivePointers[aliveParticles-1]
	End

	Method ResetParticles:Void()
		aliveParticles = 0
		For Local i:Int = 0 Until maxParticles
			alivePointers[i] = i
			reversePointer[i] = i
			ClearParticle(i)
		Next
	End

	Method ClearParticle:Void(index:Int)
		x[index] = 0
		y[index] = 0
		velocityX[index] = 0
		velocityY[index] = 0
		polarVelocityAmplitude[index] = 0
		polarVelocityAngle[index] = 0
		usePolar[index] = False
		sourceEmitter[index] = Null
		alive[index] = False
	End

	' index points to the main particle arrays, not alivePointers[]
	Method RemoveParticle:Void(index:Int)
		Local indexOfLastParticle:Int = alivePointers[aliveParticles-1]
		' switch the alivePointers array
		alivePointers[reversePointer[index]] = indexOfLastParticle
		alivePointers[aliveParticles-1] = index
		' switch the reversePointer array
		reversePointer[indexOfLastParticle] = reversePointer[index]
		reversePointer[index] = aliveParticles-1
		' reduce alive pointers
		aliveParticles -= 1
	End

	Method Render:Void()
		For Local i:Int = 0 Until aliveParticles
			Local index:Int = alivePointers[i]
			SetColor(red[index], green[index], blue[index])
			SetAlpha(alpha[index])
			If particleImage[index] <> Null Then
				DrawImage(particleImage[index], x[index], SCREEN_HEIGHT-y[index])
			Else
				DrawRect(x[index]-1, SCREEN_HEIGHT-y[index]-1, 3, 3)
			End
		Next
	End
End

Interface Force
	Method ApplyX:Float(x:Float, mass:Float=-1)
	Method ApplyY:Float(y:Float, mass:Float=-1)
End

Class ConstantForce Implements Force
Private
	Field x:Float
	Field y:Float
Public
	Method X:Float() Property
		Return x
	End
	Method X:Void(x:Float) Property
		Self.x = x
	End

	Method Y:Float() Property
		Return y
	End
	Method Y:Void(y:Float) Property
		Self.y = y
	End

	Method New(x:Float, y:Float)
		Self.x = x
		Self.y = y
	End

	Method ApplyX:Float(x:Float, mass:Float=-1)
		Return Self.x
	End

	Method ApplyY:Float(y:Float, mass:Float=-1)
		Return Self.y
	End
End

Private

Function SafeATanr:Float(dx:Float, dy:Float, def:Float=0)
	' technically a default angle shouldn't be necessary
	Local angle:Float = def
	If dy = 0 And dx >= 0 Then ' 0
		angle = 0
	ElseIf dy = 0 And dx < 0 Then ' 180
		angle = PI
	ElseIf dy > 0 And dx = 0 Then ' 90
		angle = PI/2
	ElseIf dy < 0 And dx = 0 Then ' 270
		angle = 3*PI / 2
	ElseIf dy > 0 And dx > 0 Then ' Acute
		angle = ATanr(dy / dx)
	ElseIf dy > 0 And dx < 0 Then ' Obtuse
		angle = PI - ATanr(dy / -dx)
	ElseIf dy < 0 And dx < 0 Then ' Reflex < 270
		angle = PI + ATanr(dy / dx)
	ElseIf dy < 0 And dx > 0 Then ' Reflex > 270
		angle = 2*PI - ATanr(-dy / dx)
	End
	Return angle
End

Import diddy

Class ParticleSystem
Private
' Private fields
	Field groups:ArrayList<ParticleGroup>

Public
' Properties
	Method Groups:ArrayList<ParticleGroup>() Property
		Return groups
	End

' Constructors
	Method New()
		groups = New ArrayList<ParticleGroup>
	End

' Public methods
	Method Render:Void()
		Local rgb:Float[] = GetColor()
		Local alpha:Float = GetAlpha()
		For Local i:Int = 0 Until groups.Size
			groups.Get(i).Render()
		Next
		SetAlpha(alpha)
		SetColor(rgb[0], rgb[1], rgb[2])
	End

	Method Update:Void(delta:Float)
		For Local i:Int = 0 Until groups.Size
			groups.Get(i).Update(delta)
		Next
	End
End

Class Emitter
Private
' Property fields
	Field velocityX:Float                       ' the default X velocity
	Field velocityXSpread:Float                 ' the X velocity random spread
	Field velocityY:Float                       ' the default Y velocity
	Field velocityYSpread:Float                 ' the Y velocity random spread
	Field polarVelocityAmplitude:Float          ' the default polar velocity amplitude
	Field polarVelocityAmplitudeSpread:Float    ' the polar velocity amplitude random spread
	Field polarVelocityAngle:Float              ' the default polar velocity angle
	Field polarVelocityAngleSpread:Float        ' the polar velocity angle random spread
	Field usePolar:Bool                         ' whether we should use a polar velocity
	Field spawnMinRange:Float                   ' the minimum distance to spawn from the emit point
	Field spawnMaxRange:Float                   ' the maximum distance to spawn from the emit point
	Field life:Float                            ' the default life of the particle in seconds
	Field lifeSpread:Float                      ' the life spread in seconds

	' colours
	Field redInterpolation:Int = INTERPOLATION_NONE     ' interpolates the particle's red based on life
	Field redInterpolationTime:Float = -1               ' the number of seconds to interpolate across (if <0, defaults to life)
	Field greenInterpolation:Int = INTERPOLATION_NONE   ' interpolates the particle's green based on life
	Field greenInterpolationTime:Float = -1             ' the number of seconds to interpolate across (if <0, defaults to life)
	Field blueInterpolation:Int = INTERPOLATION_NONE    ' interpolates the particle's blue based on life
	Field blueInterpolationTime:Float = -1              ' the number of seconds to interpolate across (if <0, defaults to life)
	Field alphaInterpolation:Int = INTERPOLATION_LINEAR ' interpolates the particle's alpha based on life
	Field alphaInterpolationTime:Float = -1             ' the number of seconds to interpolate across (if <0, defaults to life)

	Field minStartRed:Int = 255, maxStartRed:Int = 255
	Field minStartGreen:Int = 255, maxStartGreen:Int = 255
	Field minStartBlue:Int = 255, maxStartBlue:Int = 255
	Field minStartAlpha:Float = 1, maxStartAlpha:Float = 1

	Field minEndRed:Int = 255, maxEndRed:Int = 255
	Field minEndGreen:Int = 255, maxEndGreen:Int = 255
	Field minEndBlue:Int = 255, maxEndBlue:Int = 255
	Field minEndAlpha:Float = 0, maxEndAlpha:Float = 0

' Emitter info
	Field x:Float
	Field y:Float
	Field amplitude:Float = 10
	Field angle:Float

' Death emitters
	Field deathEmitters:ArrayList<Emitter>      ' the death emitters will fire at the particle's point of death
	                                            ' using the particle's normalised velocity
	Field deathEmitterChances:FloatArrayList    ' 0-1 random chance as to whether the death emitter will fire

Public
' Properties
	' velocityX
	Method VelocityX:Float() Property
		Return velocityX
	End
	Method VelocityX:Void(velocityX:Float) Property
		Self.velocityX = velocityX
	End

	' velocityXSpread
	Method VelocityXSpread:Float() Property
		Return velocityXSpread
	End
	Method VelocityX:Void(velocityXSpread:Float) Property
		Self.velocityXSpread = velocityXSpread
	End

	' velocityY
	Method VelocityY:Float() Property
		Return velocityY
	End
	Method VelocityY:Void(velocityY:Float) Property
		Self.velocityY = velocityY
	End

	' velocityYSpread
	Method VelocityYSpread:Float() Property
		Return velocityYSpread
	End
	Method VelocityY:Void(velocityYSpread:Float) Property
		Self.velocityYSpread = velocityYSpread
	End

	' polarVelocityAmplitude
	Method PolarVelocityAmplitude:Float() Property
		Return polarVelocityAmplitude
	End
	Method PolarVelocityAmplitude:Void(polarVelocityAmplitude:Float) Property
		Self.polarVelocityAmplitude = polarVelocityAmplitude
	End

	' polarVelocityAmplitudeSpread
	Method PolarVelocityAmplitudeSpread:Float() Property
		Return polarVelocityAmplitudeSpread
	End
	Method PolarVelocityAmplitudeSpread:Void(polarVelocityAmplitudeSpread:Float) Property
		Self.polarVelocityAmplitudeSpread = polarVelocityAmplitudeSpread
	End

	' polarVelocityAngle
	Method PolarVelocityAngle:Float() Property
		Return polarVelocityAngle
	End
	Method PolarVelocityAngle:Void(polarVelocityAngle:Float) Property
		Self.polarVelocityAngle = polarVelocityAngle
	End

	' polarVelocityAngleSpread
	Method PolarVelocityAngleSpread:Float() Property
		Return polarVelocityAngleSpread
	End
	Method PolarVelocityAngleSpread:Void(polarVelocityAngleSpread:Float) Property
		Self.polarVelocityAngleSpread = polarVelocityAngleSpread
	End

	' usePolar
	Method UsePolar:Bool() Property
		Return usePolar
	End
	Method UsePolar:Void(usePolar:Bool) Property
		Self.usePolar = usePolar
	End

	' spawnMinRange
	Method SpawnMinRange:Float() Property
		Return spawnMinRange
	End
	Method SpawnMinRange:Void(spawnMinRange:Float) Property
		Self.spawnMinRange = spawnMinRange
	End

	' spawnMaxRange
	Method SpawnMaxRange:Float() Property
		Return spawnMaxRange
	End
	Method SpawnMaxRange:Void(spawnMaxRange:Float) Property
		Self.spawnMaxRange = spawnMaxRange
	End

	' life
	Method Life:Float() Property
		Return life
	End
	Method Life:Void(life:Float) Property
		Self.life = life
	End

	' lifeSpread
	Method LifeSpread:Float() Property
		Return lifeSpread
	End
	Method LifeSpread:Void(lifeSpread:Float) Property
		Self.lifeSpread = lifeSpread
	End

	' redInterpolation
	Method RedInterpolation:Int() Property
		Return redInterpolation
	End
	Method RedInterpolation:Void(redInterpolation:Int) Property
		AssertRangeInt(redInterpolation, INTERPOLATION_NONE, INTERPOLATION_COUNT, "Invalid RedInterpolation")
		Self.redInterpolation = redInterpolation
	End

	' redInterpolationTime
	Method RedInterpolationTime:Float() Property
		Return redInterpolationTime
	End
	Method RedInterpolationTime:Void(redInterpolationTime:Float) Property
		Self.redInterpolationTime = redInterpolationTime
	End

	' greenInterpolation
	Method GreenInterpolation:Int() Property
		Return greenInterpolation
	End
	Method GreenInterpolation:Void(greenInterpolation:Int) Property
		AssertRangeInt(greenInterpolation, INTERPOLATION_NONE, INTERPOLATION_COUNT, "Invalid GreenInterpolation")
		Self.greenInterpolation = greenInterpolation
	End

	' greenInterpolationTime
	Method GreenInterpolationTime:Float() Property
		Return greenInterpolationTime
	End
	Method GreenInterpolationTime:Void(greenInterpolationTime:Float) Property
		Self.greenInterpolationTime = greenInterpolationTime
	End

	' blueInterpolation
	Method BlueInterpolation:Int() Property
		Return blueInterpolation
	End
	Method BlueInterpolation:Void(blueInterpolation:Int) Property
		AssertRangeInt(blueInterpolation, INTERPOLATION_NONE, INTERPOLATION_COUNT, "Invalid BlueInterpolation")
		Self.blueInterpolation = blueInterpolation
	End

	' blueInterpolationTime
	Method BlueInterpolationTime:Float() Property
		Return blueInterpolationTime
	End
	Method BlueInterpolationTime:Void(blueInterpolationTime:Float) Property
		Self.blueInterpolationTime = blueInterpolationTime
	End

	' alphaInterpolation
	Method AlphaInterpolation:Int() Property
		Return alphaInterpolation
	End
	Method AlphaInterpolation:Void(alphaInterpolation:Int) Property
		AssertRangeInt(alphaInterpolation, INTERPOLATION_NONE, INTERPOLATION_COUNT, "Invalid AlphaInterpolation")
		Self.alphaInterpolation = alphaInterpolation
	End

	' alphaInterpolationTime
	Method AlphaInterpolationTime:Float() Property
		Return alphaInterpolationTime
	End
	Method AlphaInterpolationTime:Void(alphaInterpolationTime:Float) Property
		Self.alphaInterpolationTime = alphaInterpolationTime
	End

	' minStartRed
	Method MinStartRed:Int() Property
		Return minStartRed
	End
	Method MinStartRed:Void(minStartRed:Int) Property
		Self.minStartRed = minStartRed
	End

	' maxStartRed
	Method MaxStartRed:Int() Property
		Return maxStartRed
	End
	Method MaxStartRed:Void(maxStartRed:Int) Property
		Self.maxStartRed = maxStartRed
	End

	' minStartGreen
	Method MinStartGreen:Int() Property
		Return minStartGreen
	End
	Method MinStartGreen:Void(minStartGreen:Int) Property
		Self.minStartGreen = minStartGreen
	End

	' maxStartGreen
	Method MaxStartGreen:Int() Property
		Return maxStartGreen
	End
	Method MaxStartGreen:Void(maxStartGreen:Int) Property
		Self.maxStartGreen = maxStartGreen
	End

	' minStartBlue
	Method MinStartBlue:Int() Property
		Return minStartBlue
	End
	Method MinStartBlue:Void(minStartBlue:Int) Property
		Self.minStartBlue = minStartBlue
	End

	' maxStartBlue
	Method MaxStartBlue:Int() Property
		Return maxStartBlue
	End
	Method MaxStartBlue:Void(maxStartBlue:Int) Property
		Self.maxStartBlue = maxStartBlue
	End

	' minStartAlpha
	Method MinStartAlpha:Float() Property
		Return minStartAlpha
	End
	Method MinStartAlpha:Void(minStartAlpha:Float) Property
		Self.minStartAlpha = minStartAlpha
	End

	' maxStartAlpha
	Method MaxStartAlpha:Float() Property
		Return maxStartAlpha
	End
	Method MaxStartAlpha:Void(maxStartAlpha:Float) Property
		Self.maxStartAlpha = maxStartAlpha
	End

	' minEndRed
	Method MinEndRed:Int() Property
		Return minEndRed
	End
	Method MinEndRed:Void(minEndRed:Int) Property
		Self.minEndRed = minEndRed
	End

	' maxEndRed
	Method MaxEndRed:Int() Property
		Return maxEndRed
	End
	Method MaxEndRed:Void(maxEndRed:Int) Property
		Self.maxEndRed = maxEndRed
	End

	' minEndGreen
	Method MinEndGreen:Int() Property
		Return minEndGreen
	End
	Method MinEndGreen:Void(minEndGreen:Int) Property
		Self.minEndGreen = minEndGreen
	End

	' maxEndGreen
	Method MaxEndGreen:Int() Property
		Return maxEndGreen
	End
	Method MaxEndGreen:Void(maxEndGreen:Int) Property
		Self.maxEndGreen = maxEndGreen
	End

	' minEndBlue
	Method MinEndBlue:Int() Property
		Return minEndBlue
	End
	Method MinEndBlue:Void(minEndBlue:Int) Property
		Self.minEndBlue = minEndBlue
	End

	' maxEndBlue
	Method MaxEndBlue:Int() Property
		Return maxEndBlue
	End
	Method MaxEndBlue:Void(maxEndBlue:Int) Property
		Self.maxEndBlue = maxEndBlue
	End

	' minEndAlpha
	Method MinEndAlpha:Float() Property
		Return minEndAlpha
	End
	Method MinEndAlpha:Void(minEndAlpha:Float) Property
		Self.minEndAlpha = minEndAlpha
	End

	' maxEndAlpha
	Method MaxEndAlpha:Float() Property
		Return maxEndAlpha
	End
	Method MaxEndAlpha:Void(maxEndAlpha:Float) Property
		Self.maxEndAlpha = maxEndAlpha
	End

	' x
	Method X:Float() Property
		Return x
	End
	Method X:Void(x:Float) Property
		Self.x = x
	End

	' y
	Method Y:Float() Property
		Return y
	End
	Method Y:Void(y:Float) Property
		Self.y = y
	End

	' angle
	Method Angle:Float() Property
		Return angle
	End
	Method Angle:Void(angle:Float) Property
		Self.angle = angle
	End

	' amplitude
	Method Amplitude:Float() Property
		Return amplitude
	End
	Method Amplitude:Void(amplitude:Float) Property
		Self.amplitude = amplitude
	End

' Constructors
	Method New()
		deathEmitters = New ArrayList<Emitter>
		deathEmitterChances = New FloatArrayList
	End

' Convenience setters
	Method SetParticleRGBInterpolated:Void(startRed:Int, startGreen:Int, startBlue:Int, endRed:Int, endGreen:Int, endBlue:Int)
		minStartRed = startRed
		maxStartRed = startRed
		minEndRed = endRed
		maxEndRed = endRed
		If startRed <> endRed Then
			redInterpolation = INTERPOLATION_LINEAR
			redInterpolationTime = -1
		Else
			redInterpolation = INTERPOLATION_NONE
		End

		minStartGreen = startGreen
		maxStartGreen = startGreen
		minEndGreen = endGreen
		maxEndGreen = endGreen
		If startGreen <> endGreen Then
			greenInterpolation = INTERPOLATION_LINEAR
			greenInterpolationTime = -1
		Else
			greenInterpolation = INTERPOLATION_NONE
		End

		minStartBlue = startBlue
		maxStartBlue = startBlue
		minEndBlue = endBlue
		maxEndBlue = endBlue
		If startBlue <> endBlue Then
			blueInterpolation = INTERPOLATION_LINEAR
			blueInterpolationTime = -1
		Else
			blueInterpolation = INTERPOLATION_NONE
		End
	End

	Method SetParticleRGB:Void(red:Int, green:Int, blue:Int)
		minStartRed = red
		maxStartRed = red
		minEndRed = red
		maxEndRed = red
		redInterpolation = INTERPOLATION_NONE
		minStartGreen = green
		maxStartGreen = green
		minEndGreen = green
		maxEndGreen = green
		greenInterpolation = INTERPOLATION_NONE
		minStartBlue = blue
		maxStartBlue = blue
		minEndBlue = blue
		maxEndBlue = blue
		blueInterpolation = INTERPOLATION_NONE
	End

	Method SetParticleAlpha:Void(alpha:Float, time:Int=-1)
		minStartAlpha = alpha
		maxStartAlpha = alpha
		minEndAlpha = 0
		maxEndAlpha = 0
		alphaInterpolation = INTERPOLATION_LINEAR
		If time < 0 Then time = life
		alphaInterpolationTime = life
	End

	Method SetVelocity:Void(velocityX:Float, velocityY:Float)
		SetVelocity(velocityX, 0, velocityY, 0)
	End

	Method SetVelocity:Void(velocityX:Float, velocityXSpread:Float, velocityY:Float, velocityYSpread:Float)
		usePolar = False
		Self.velocityX = velocityX
		Self.velocityXSpread = velocityXSpread
		Self.velocityY = velocityY
		Self.velocityYSpread = velocityYSpread
	End

	Method SetPolarVelocity:Void(polarVelocityAngle:Float, polarVelocityAmplitude:Float)
		SetPolarVelocity(polarVelocityAngle, 0, polarVelocityAmplitude, 0)
	End

	Method SetPolarVelocity:Void(polarVelocityAngle:Float, polarVelocityAngleSpread:Float, polarVelocityAmplitude:Float, polarVelocityAmplitudeSpread:Float)
		usePolar = True
		Self.polarVelocityAngle = polarVelocityAngle
		Self.polarVelocityAngleSpread = polarVelocityAngleSpread
		Self.polarVelocityAmplitude = polarVelocityAmplitude
		Self.polarVelocityAmplitudeSpread = polarVelocityAmplitudeSpread
	End

' Death emitter methods
	Method AddDeathEmitter(emitter:Emitter, chance:Float)
		deathEmitters.Add(emitter)
		deathEmitterChances.AddFloat(chance)
	End

	Method RemoveDeathEmitter(emitter:Emitter)
		Local idx:Int = deathEmitters.IndexOf(emitter)
		If idx >= 0 Then
			deathEmitters.RemoveAt(idx)
			deathEmitterChances.RemoveAt(idx)
		End
	End

	Method SetDeathEmitterChance(emitter:Emitter, chance:Float)
		Local idx:Int = deathEmitters.IndexOf(emitter)
		If idx >= 0 Then
			deathEmitterChances.SetFloat(idx, chance)
		End
	End

' Emits
	Method Emit:Void(group:ParticleGroup, amount:Int)
		EmitAtAngle(group, amount, x, y, angle)
	End

	Method EmitAt:Void(group:ParticleGroup, amount:Int, emitX:Float, emitY:Float)
		EmitAtAngle(group, amount, emitX, emitY, angle)
	End

	Method EmitAngle:Void(group:ParticleGroup, amount:Int, emitAngle:Float)', emitAmplitude:Float)
		EmitAtAngle(group, amount, x, y, emitAngle)
	End

	Method EmitAtAngle:Void(group:ParticleGroup, amount:Int, emitX:Float, emitY:Float, emitAngle:Float)
		For Local i:Int = 0 Until amount
			' get an index for a new particle
			Local index:Int = group.CreateParticle()
			' die if we're full
			If index < 0 Then Exit

			Local spawnAngle:Float = Rnd() * 2 * PI
			Local spawnDistance:Float = spawnMinRange + Rnd() * (spawnMaxRange-spawnMinRange)
			group.x[index] = emitX + Cosr(spawnAngle) * spawnDistance
			group.y[index] = emitY + Sinr(spawnAngle) * spawnDistance
			group.usePolar[index] = usePolar
			If usePolar Then
				' TODO: adjust for src speed
				group.polarVelocityAngle[index] = emitAngle + polarVelocityAngle - polarVelocityAngleSpread/2 + Rnd() * polarVelocityAngleSpread
				group.polarVelocityAmplitude[index] = polarVelocityAmplitude - polarVelocityAmplitudeSpread/2 + Rnd() * polarVelocityAmplitudeSpread
				group.velocityX[index] = Cosr(group.polarVelocityAngle[index]) * group.polarVelocityAmplitude[index]
				group.velocityY[index] = Sinr(group.polarVelocityAngle[index]) * group.polarVelocityAmplitude[index]
			Else
				' TODO: adjust for src angle and speed
				group.velocityX[index] = velocityX - velocityXSpread/2 + Rnd() * velocityXSpread
				group.velocityY[index] = velocityY - velocityYSpread/2 + Rnd() * velocityYSpread
			End
			group.sourceEmitter[index] = Self
			group.alive[index] = True
			group.life[index] = life - lifeSpread/2 + Rnd() * lifeSpread

			' start colours
			If minStartRed <> maxStartRed Then
				group.startRed[index] = Max(0,Min(255,Int(minStartRed + Rnd() * (maxStartRed-minStartRed))))
			Else
				group.startRed[index] = minStartRed
			End
			If minStartGreen <> maxStartGreen Then
				group.startGreen[index] = Max(0,Min(255,Int(minStartGreen + Rnd() * (maxStartRed-minStartGreen))))
			Else
				group.startGreen[index] = minStartGreen
			End
			If minStartBlue <> maxStartBlue Then
				group.startBlue[index] = Max(0,Min(255,Int(minStartBlue + Rnd() * (maxStartBlue-minStartBlue))))
			Else
				group.startBlue[index] = minStartBlue
			End
			If minStartAlpha <> maxStartAlpha Then
				group.startAlpha[index] = Max(0.0,Min(1.0,minStartAlpha + Rnd() * (maxStartAlpha-minStartAlpha)))
			Else
				group.startAlpha[index] = minStartAlpha
			End

			group.red[index] = group.startRed[index]
			group.green[index] = group.startGreen[index]
			group.blue[index] = group.startBlue[index]
			group.alpha[index] = group.startAlpha[index]

			' end colours
			If minEndRed <> maxEndRed Then
				group.endRed[index] = Max(0,Min(255,Int(minEndRed + Rnd() * (maxEndRed-minEndRed))))
			Else
				group.endRed[index] = minEndRed
			End
			If minEndGreen <> maxEndGreen Then
				group.endGreen[index] = Max(0,Min(255,Int(minEndGreen + Rnd() * (maxEndRed-minEndGreen))))
			Else
				group.endGreen[index] = minEndGreen
			End
			If minEndBlue <> maxEndBlue Then
				group.endBlue[index] = Max(0,Min(255,Int(minEndBlue + Rnd() * (maxEndBlue-minEndBlue))))
			Else
				group.endBlue[index] = minEndBlue
			End
			If minEndAlpha <> maxEndAlpha Then
				group.endAlpha[index] = Max(0.0,Min(1.0,minEndAlpha + Rnd() * (maxEndAlpha-minEndAlpha)))
			Else
				group.endAlpha[index] = minEndAlpha
			End

			' interpolation
			If group.startRed[index] = group.endRed[index] Then
				group.redInterpolation[index] = INTERPOLATION_NONE
			Else
				group.redInterpolation[index] = redInterpolation
				group.redInterpolationTime[index] = redInterpolationTime
				If group.redInterpolationTime[index] < 0 Then group.redInterpolationTime[index] = group.life[index]
			End
			If group.startGreen[index] = group.endGreen[index] Then
				group.greenInterpolation[index] = INTERPOLATION_NONE
			Else
				group.greenInterpolation[index] = greenInterpolation
				group.greenInterpolationTime[index] = greenInterpolationTime
				If group.greenInterpolationTime[index] < 0 Then group.greenInterpolationTime[index] = group.life[index]
			End
			If group.startBlue[index] = group.endBlue[index] Then
				group.blueInterpolation[index] = INTERPOLATION_NONE
			Else
				group.blueInterpolation[index] = blueInterpolation
				group.blueInterpolationTime[index] = blueInterpolationTime
				If group.blueInterpolationTime[index] < 0 Then group.blueInterpolationTime[index] = group.life[index]
			End
			If group.startAlpha[index] = group.endAlpha[index] Then
				group.alphaInterpolation[index] = INTERPOLATION_NONE
			Else
				group.alphaInterpolation[index] = alphaInterpolation
				group.alphaInterpolationTime[index] = alphaInterpolationTime
				If group.alphaInterpolationTime[index] < 0 Then group.alphaInterpolationTime[index] = group.life[index]
			End
		Next
	End
End

Class ParticleGroup
Private
	' fields for each particle (more efficient to have multiple large arrays than incredible amounts of Particle objects
	Field x:Float[]
	Field y:Float[]
	Field velocityX:Float[]
	Field velocityY:Float[]
	Field polarVelocityAmplitude:Float[]
	Field polarVelocityAngle:Float[] ' radians
	Field usePolar:Bool[]
	Field mass:Float[]

	Field red:Int[]
	Field green:Int[]
	Field blue:Int[]
	Field alpha:Float[]

	Field startRed:Int[]
	Field startGreen:Int[]
	Field startBlue:Int[]
	Field startAlpha:Float[]

	Field endRed:Int[]
	Field endGreen:Int[]
	Field endBlue:Int[]
	Field endAlpha:Float[]

	Field redInterpolation:Int[]
	Field redInterpolationTime:Float[]
	Field greenInterpolation:Int[]
	Field greenInterpolationTime:Float[]
	Field blueInterpolation:Int[]
	Field blueInterpolationTime:Float[]
	Field alphaInterpolation:Int[]
	Field alphaInterpolationTime:Float[]

	Field life:Float[]
	Field alive:Bool[]
	Field reversePointer:Int[]
	Field sourceEmitter:Emitter[]

	Field maxParticles:Int ' read only property
	Field alivePointers:Int[]
	Field aliveParticles:Int ' read only property

	' acceleration, etc.
	Field accelerationX:Float
	Field accelerationY:Float

	' cached arrays for clearing out dead particles
	Field deadParticles:Int[]
	Field deadEmitters:Emitter[]
	Field deadX:Float[]
	Field deadY:Float[]
	Field deadVelocityX:Float[]
	Field deadVelocityY:Float[]
	Field deadCount:Int = 0

	' constants for the group
	Field forces:ArrayList<Force>

Public
' Properties
	Method AliveParticles:Int() Property
		Return aliveParticles
	End

	Method MaxParticles:Int() Property
		Return maxParticles
	End

	Method Forces:ArrayList<Force>() Property
		Return forces
	End

' Constructors
	Method New(maxParticles:Int)
		Self.maxParticles = maxParticles
		forces = New ArrayList<Force>

		x = New Float[maxParticles]
		y = New Float[maxParticles]
		velocityX = New Float[maxParticles]
		velocityY = New Float[maxParticles]
		polarVelocityAmplitude = New Float[maxParticles]
		polarVelocityAngle = New Float[maxParticles]
		usePolar = New Bool[maxParticles]
		sourceEmitter = New Emitter[maxParticles]
		life = New Float[maxParticles]
		alive = New Bool[maxParticles]
		mass = New Float[maxParticles]

		red = New Int[maxParticles]
		green = New Int[maxParticles]
		blue = New Int[maxParticles]
		alpha = New Float[maxParticles]

		startRed = New Int[maxParticles]
		startGreen = New Int[maxParticles]
		startBlue = New Int[maxParticles]
		startAlpha = New Float[maxParticles]

		endRed = New Int[maxParticles]
		endGreen = New Int[maxParticles]
		endBlue = New Int[maxParticles]
		endAlpha = New Float[maxParticles]

		redInterpolation = New Int[maxParticles]
		redInterpolationTime = New Float[maxParticles]
		greenInterpolation = New Int[maxParticles]
		greenInterpolationTime = New Float[maxParticles]
		blueInterpolation = New Int[maxParticles]
		blueInterpolationTime = New Float[maxParticles]
		alphaInterpolation = New Int[maxParticles]
		alphaInterpolationTime = New Float[maxParticles]

		reversePointer = New Int[maxParticles]
		alivePointers = New Int[maxParticles]

		' these are populated before a dead particle is cleared
		deadParticles = New Int[maxParticles]
		deadEmitters = New Emitter[maxParticles]
		deadX = New Float[maxParticles]
		deadY = New Float[maxParticles]
		deadVelocityX = New Float[maxParticles]
		deadVelocityY = New Float[maxParticles]

		ResetParticles()
	End

	Method Update:Void(delta:Float)
		' convert milliseconds to seconds
		delta = delta/1000
		deadCount = 0
		' loop through all living particles
		For Local i:Int = 0 Until aliveParticles
			Local index:Int = alivePointers[i]
			life[index] -= delta
			If life[index] > 0 Then
				' apply acceleration
				If Not forces.IsEmpty() Then
					' update cartesian velocity
					If usePolar[index] Then
						velocityX[index] = Cosr(polarVelocityAngle[index]) * polarVelocityAmplitude[index]
						velocityY[index] = Sinr(polarVelocityAngle[index]) * polarVelocityAmplitude[index]
					End
					' apply forces
					For Local fi:Int = 0 Until forces.Size
						Local f:Force = forces.Get(fi)
						velocityX[index] += f.ApplyX(velocityX[index], mass[index]) * delta
						velocityY[index] += f.ApplyY(velocityY[index], mass[index]) * delta
					Next
					' TODO: terminal velocity
					' update polar velocity
					If usePolar[index] Then
						Local angle:Float = SafeATanr(velocityX[index], velocityY[index], polarVelocityAngle[index])
						polarVelocityAngle[index] = angle
						polarVelocityAmplitude[index] = Sqrt(velocityX[index]*velocityX[index] + velocityY[index]*velocityY[index])
					End
				End
				' update position
				x[index] += velocityX[index] * delta
				y[index] += velocityY[index] * delta
				' interpolate colours
				If startRed[index] <> endRed[index] And life[index] < redInterpolationTime[index] Then
					red[index] = Int(Interpolate(redInterpolation[index], startRed[index], endRed[index], 1 - life[index]/redInterpolationTime[index]))
				End
				If startGreen[index] <> endGreen[index] And life[index] < greenInterpolationTime[index] Then
					green[index] = Int(Interpolate(greenInterpolation[index], startGreen[index], endGreen[index], 1 - life[index]/greenInterpolationTime[index]))
				End
				If startBlue[index] <> endBlue[index] And life[index] < blueInterpolationTime[index] Then
					blue[index] = Int(Interpolate(blueInterpolation[index], startBlue[index], endBlue[index], 1 - life[index]/blueInterpolationTime[index]))
				End
				If startAlpha[index] <> endAlpha[index] And life[index] < alphaInterpolationTime[index] Then
					alpha[index] = Interpolate(alphaInterpolation[index], startAlpha[index], endAlpha[index], 1 - life[index]/alphaInterpolationTime[index])
				End
			Else
				alive[index] = False
				' cache info from dead particle
				deadParticles[deadCount] = index
				deadEmitters[deadCount] = sourceEmitter[index]
				deadX[deadCount] = x[index]
				deadY[deadCount] = y[index]
				deadVelocityX[deadCount] = velocityX[index]
				deadVelocityY[deadCount] = velocityY[index]
				deadCount += 1
			End
		Next
		' remove all the dead particles from the pointer array
		For Local i:Int = 0 Until deadCount
			RemoveParticle(deadParticles[i])
		Next
		' now fire off any emitters
		For Local i:Int = 0 Until deadCount
			For Local j:Int = 0 Until deadEmitters[i].deathEmitterChances.Size
				Local c:Float = deadEmitters[i].deathEmitterChances.Get(j)
				If c = 1 Or c > 0 And Rnd() <= c Then
					Local e:Emitter = deadEmitters[i].deathEmitters.Get(j)
					e.EmitAtAngle(Self, 30, deadX[i], deadY[i], SafeATanr(deadVelocityX[i], deadVelocityY[i]))
				End
			Next
			deadEmitters[i] = Null
		Next
	End

	Method CreateParticle:Int()
		If aliveParticles >= maxParticles Then Return -1
		aliveParticles += 1
		ClearParticle(alivePointers[aliveParticles-1])
		Return alivePointers[aliveParticles-1]
	End

	Method ResetParticles:Void()
		aliveParticles = 0
		For Local i:Int = 0 Until maxParticles
			alivePointers[i] = i
			reversePointer[i] = i
			ClearParticle(i)
		Next
	End

	Method ClearParticle:Void(index:Int)
		x[index] = 0
		y[index] = 0
		velocityX[index] = 0
		velocityY[index] = 0
		polarVelocityAmplitude[index] = 0
		polarVelocityAngle[index] = 0
		usePolar[index] = False
		sourceEmitter[index] = Null
		alive[index] = False
	End

	' index points to the main particle arrays, not alivePointers[]
	Method RemoveParticle:Void(index:Int)
		Local indexOfLastParticle:Int = alivePointers[aliveParticles-1]
		' switch the alivePointers array
		alivePointers[reversePointer[index]] = indexOfLastParticle
		alivePointers[aliveParticles-1] = index
		' switch the reversePointer array
		reversePointer[indexOfLastParticle] = reversePointer[index]
		reversePointer[index] = aliveParticles-1
		' reduce alive pointers
		aliveParticles -= 1
	End

	Method Render:Void()
		For Local i:Int = 0 Until aliveParticles
			Local index:Int = alivePointers[i]
			SetColor(red[index], green[index], blue[index])
			SetAlpha(alpha[index])
			DrawRect(x[index]-1, SCREEN_HEIGHT-y[index]-1, 3, 3)
		Next
	End
End

Interface Force
	Method ApplyX:Float(x:Float, mass:Float=-1)
	Method ApplyY:Float(y:Float, mass:Float=-1)
End

Class ConstantForce Implements Force
Private
	Field x:Float
	Field y:Float
Public
	Method X:Float() Property
		Return x
	End
	Method X:Void(x:Float) Property
		Self.x = x
	End

	Method Y:Float() Property
		Return y
	End
	Method Y:Void(y:Float) Property
		Self.y = y
	End

	Method New(x:Float, y:Float)
		Self.x = x
		Self.y = y
	End

	Method ApplyX:Float(x:Float, mass:Float=-1)
		Return Self.x
	End

	Method ApplyY:Float(y:Float, mass:Float=-1)
		Return Self.y
	End
End

Private

Function SafeATanr:Float(dx:Float, dy:Float, def:Float=0)
	' technically a default angle shouldn't be necessary
	Local angle:Float = def
	If dy = 0 And dx >= 0 Then ' 0
		angle = 0
	ElseIf dy = 0 And dx < 0 Then ' 180
		angle = PI
	ElseIf dy > 0 And dx = 0 Then ' 90
		angle = PI/2
	ElseIf dy < 0 And dx = 0 Then ' 270
		angle = 3*PI / 2
	ElseIf dy > 0 And dx > 0 Then ' Acute
		angle = ATanr(dy / dx)
	ElseIf dy > 0 And dx < 0 Then ' Obtuse
		angle = PI - ATanr(dy / -dx)
	ElseIf dy < 0 And dx < 0 Then ' Reflex < 270
		angle = PI + ATanr(dy / dx)
	ElseIf dy < 0 And dx > 0 Then ' Reflex > 270
		angle = 2*PI - ATanr(-dy / dx)
	End
	Return angle
End