Falling sand game (WIP)

Miscellaneous Forums/Blitz Showcase/Falling sand game (WIP)

After wasting too much time playing this : http://dan-ball.jp/en/javagame/dust/
I decided to try my hand at a blitzmax version...

Still a work in progress, but I'd be interested to hear what FPS people get (trying to cope with a lot of particles at once is a bit of a challenge). It's only using OpenGL at the moment as I don't currently have a working Windows box for directx dev. Physics are a bit fudged and the codes a bit of a mess but it's a start.

Updated Version 0.3 - 13th April 2008

Download v.0.3 Windows Exe http://minimono.net/downloads/elemental/elemental-windows/elemental.exe
Download v.0.3 Mac OS X (PPC) zip http://minimono.net/downloads/elemental/elemental-osx-ppc/elemental.zip
Download v.0.3 Max OS X (Intel) zip http://minimono.net/downloads/elemental/elemental-osx-intel/elemental.zip





Blitzmax Source

version 0.3

' Elemental
' (c) Tesuji 2008
SuperStrict

Framework BRL.blitz
Import BRL.glmax2d
'Import BRL.D3D7Max2d
Import BRL.max2d
Import BRL.pixmap
Import BRL.pngloader
Import BRL.jpgloader
Import BRL.retro

SetGraphicsDriver GLMax2DDriver()
'Graphics 800,600,0
Graphics 800,600,32,0

SeedRnd MilliSecs()

Global width:Int=512,height:Int=512
Global px:Particle[width+2,height+2]
Global centerX:Int = GraphicsWidth()/2
Global centerY:Int = GraphicsHeight()/2
Global halfWidth:Int = width/2
Global halfHeight:Int = height/2
Global xOrigin:Int = centerX-halfWidth
Global yOrigin:Int = centerY-halfHeight
Global particles:TList = CreateList()
Global menuItems:TList = CreateList()

Const OUTPUT_DIRECTORY:String = "images"

Const RENDERMODE_PIXEL:Int = 0
Const RENDERMODE_BLOB:Int = 1
Const RENDERMODE_BLOB2:Int = 2
Const RENDERMODE_IMAGE:Int = 3
Const MAX_RENDERMODES:Int = 4

Const DRAWMODE_PEN:Int = 0
Const DRAWMODE_LINE:Int = 1
Const DRAWMODE_CIRCLE:Int = 2
Const MAX_DRAWMODES:Int = 3

Const ITEMGROUP_ELEMENTS:Int = 0
Const ITEMGROUP_BRUSH:Int = 1
Const ITEMGROUP_CONTROL:Int = 2

Const MAX_FPS_HISTORY:Int = 60


Const ELEMENT_EMPTY:Int = 0
Const ELEMENT_WATER:Int = 1
Const ELEMENT_CONCRETE:Int = 2
Const ELEMENT_SALT:Int = 3
Const ELEMENT_MAGMA:Int = 4
Const ELEMENT_FIRE:Int = 5
Const ELEMENT_OIL:Int = 6
Const ELEMENT_SAND:Int = 7
Const ELEMENT_SMOKE:Int = 8
Const ELEMENT_GRAPHITE:Int = 9
Const ELEMENT_EMBER:Int = 10
Const ELEMENT_STEAM:Int = 11
Const ELEMENT_ACID:Int = 12
Const ELEMENT_WOOD:Int = 13
Const ELEMENT_SEED:Int = 14
Const ELEMENT_SPROUT:Int = 15
Const ELEMENT_GUNPOWDER:Int = 16
Const ELEMENT_MISSILE:Int = 17
Const ELEMENT_GRASS:Int = 18

Const MAX_ELEMENTS:Int = 19
'                                 0              1              2              3              4              5              6              7              8              9              10            11             12             13             14              15           16
Global elementLabels:String[] = [ "Air",         "Water",       "Concrete",    "Salt",        "Magma",       "Fire",        "Oil",         "Sand",        "Smoke",       "Graphite",    "Ember",      "Steam",       "Acid",        "Wood",        "Seed",        "Sprout",     "Gunpowder",    "Missile",      "Grass"         ]
Global elementCreatable:Int[] = [ True,          True,          True,          True,          True,          True,          True,          True,          False,         True,          False,        False,         True,          True,          True,          False,        True,           False,          False           ]
Global elementBitmask:Int[]   = [ $00000000,     $00000001,     $00000002,     $00000004,     $00000008,     $00000010,     $00000020,     $00000040,     $00000080,     $00000100,     $00000200,    $00000400,     $00000800,     $00001000,     $00002000,     $00004000,    $00008000,      $00010000,      $00020000       ] 
Global lifespan:Int[]         = [ -1,            -1,            -1,            -1,            -1,            128,           -1,            -1,            128,           -1,            64,           255,           -1,            128,           -1,            300,          -1,             1255,           -1              ]
Global elementAfterlife:Int[] = [ ELEMENT_EMPTY, ELEMENT_STEAM, ELEMENT_EMPTY, ELEMENT_EMPTY, ELEMENT_EMPTY, ELEMENT_SMOKE, ELEMENT_EMPTY, ELEMENT_EMPTY, ELEMENT_EMPTY, ELEMENT_EMPTY, ELEMENT_FIRE, ELEMENT_EMPTY, ELEMENT_EMPTY, ELEMENT_WOOD,  ELEMENT_EMPTY, ELEMENT_SEED, ELEMENT_EMPTY,  ELEMENT_EMPTY,  ELEMENT_SEED    ] 
Global elementImmovable:Int[] = [ False,         False,         True,          False,         False,         False,         False,         False,         False,         True,          True,         False,         False,         True,          False,         False,        False,          False,          True            ]
Global elementDrag:Float[]    = [ 1.0,           0.95,          0.0,           0.1,           0.8,           0.9,           0.85,          0.5,           0.995,         0.0,           0.0,          0.395,         0.87,          0.0,           0.85,          0.85,         0.58,           0.0,            0.0             ] ' elementDrag
Global elementMass:Float[]    = [ 0.1,           0.3,           1.0,           1.8,           3.4,           0.52,          0.3,           3.2,           2.01,          1.0,           1.0,          2.01,          0.32,          1.0,           0.13,          0.013,        2.2,            0.5,            1.0             ] ' elementMass
Global gravity:Float[]        = [ 1.0,           2.0,           0.0,           2.0,           2.0,          -2.0,           1.0,           2.0,          -1.0,           0.0,           0.0,         -1.0,           1.0,           1.0,           0.4,           -0.01,        2.0,            0.1,            1.0             ] ' gravity
Global slippyness:Float[]     = [ 0.0,           1.0,           0.0,           0.9,           1.0,           0.7,           1.0,           0.9,           1.0,           0.0,           0.0,          1.0,           1.0,           0.0,           0.995,         0.35,         0.9,            1.0,            0.0             ]
Global airDamp:Float[]        = [ 1.0,           0.0,           0.0,           0.65,          0.0,           0.5,           0.0,           0.35,          0.85,          0.0,           0.0,          0.59,          0.0,           0.0,           0.79,          0.93,         0.35,           0.99,           0.0             ]
Global elementBounce:Float[]  = [ 0.0,           0.0,           0.0,           0.5,           0.0,           0.75,          0.0,           0.35,          0.85,          0.0,           0.0,          0.59,          0.0,           0.0,           0.40,          0.93,         0.35,           0.99,           0.0             ]
Global elementRed:Int[]       = [ $00,           $10,           $80,           $F0,           $C0,           $FF,           $75,           $BA,           $C0,           $30,           $8F,          $C0,           $20,           $65,           $65,           $75,          $10,            $F0,            $45             ]
Global elementGreen:Int[]     = [ $00,           $9A,           $80,           $F0,           $20,           $40,           $2A,           $A2,           $C0,           $30,           $00,          $C0,           $B0,           $3E,           $7E,           $9E,          $10,            $F0,            $8E             ]
Global elementBlue:Int[]      = [ $80,           $CE,           $80,           $F0,           $10,           $10,           $02,           $30,           $C0,           $30,           $00,          $E0,           $08,           $22,           $22,           $22,          $10,            $D0,            $12             ]


Global elementInteractions:Interaction[MAX_ELEMENTS,MAX_ELEMENTS]

'                  element           reacts with       to form           leaving behind  with a probability of n in 100000
Interaction.Create(ELEMENT_WATER,    ELEMENT_SALT,     ELEMENT_WATER,    ELEMENT_WATER,  185)
Interaction.Create(ELEMENT_WATER,    ELEMENT_MAGMA,    ELEMENT_STEAM,    ELEMENT_SAND,   2000)
Interaction.Create(ELEMENT_WATER,    ELEMENT_ACID,     ELEMENT_STEAM,    ELEMENT_WATER,  200)
Interaction.Create(ELEMENT_WATER,    ELEMENT_EMBER,    ELEMENT_STEAM,    ELEMENT_WATER,  10000)
Interaction.Create(ELEMENT_WATER,    ELEMENT_FIRE,     ELEMENT_EMPTY,    ELEMENT_STEAM,  5000)
Interaction.Create(ELEMENT_WATER,    ELEMENT_MISSILE,  ELEMENT_STEAM,    ELEMENT_STEAM,  50000)

Interaction.Create(ELEMENT_SALT,     ELEMENT_WATER,    ELEMENT_SALT,     ELEMENT_WATER,  105)

Interaction.Create(ELEMENT_GRAPHITE, ELEMENT_FIRE,     ELEMENT_FIRE,     ELEMENT_EMBER,  500)
Interaction.Create(ELEMENT_GRAPHITE, ELEMENT_MAGMA,    ELEMENT_MAGMA,    ELEMENT_FIRE,   2000)
Interaction.Create(ELEMENT_GRAPHITE, ELEMENT_EMBER,    ELEMENT_EMBER,    ELEMENT_EMBER,  25000)
Interaction.Create(ELEMENT_GRAPHITE, ELEMENT_MISSILE,  ELEMENT_SMOKE,    ELEMENT_EMBER,  25000)

Interaction.Create(ELEMENT_EMBER,    ELEMENT_GRAPHITE, ELEMENT_EMBER,    ELEMENT_EMBER,  25000)
Interaction.Create(ELEMENT_EMBER,    ELEMENT_OIL,      ELEMENT_FIRE,     ELEMENT_SMOKE,  50000)
Interaction.Create(ELEMENT_EMBER,    ELEMENT_SAND,     ELEMENT_MAGMA,    ELEMENT_EMBER,  250)
Interaction.Create(ELEMENT_EMBER,    ELEMENT_WOOD,     ELEMENT_FIRE,     ELEMENT_EMBER,  2500)
Interaction.Create(ELEMENT_EMBER,    ELEMENT_SEED,     ELEMENT_FIRE,     ELEMENT_FIRE,   2500)
Interaction.Create(ELEMENT_EMBER,    ELEMENT_GRASS,    ELEMENT_FIRE,     ELEMENT_FIRE,   2500)

Interaction.Create(ELEMENT_MAGMA,    ELEMENT_WATER,    ELEMENT_STEAM,    ELEMENT_MAGMA,  10000)
Interaction.Create(ELEMENT_MAGMA,    ELEMENT_ACID,     ELEMENT_EMBER,    ELEMENT_MAGMA,  150)
Interaction.Create(ELEMENT_MAGMA,    ELEMENT_WOOD,     ELEMENT_FIRE,     ELEMENT_MAGMA,  2150)
Interaction.Create(ELEMENT_MAGMA,    ELEMENT_SEED,     ELEMENT_FIRE,     ELEMENT_MAGMA,  3150)
Interaction.Create(ELEMENT_MAGMA,    ELEMENT_GRASS,    ELEMENT_FIRE,     ELEMENT_MAGMA,  4150)
Interaction.Create(ELEMENT_MAGMA,    ELEMENT_GUNPOWDER,ELEMENT_FIRE,     ELEMENT_MAGMA,  10000)
Interaction.Create(ELEMENT_MAGMA,    ELEMENT_MISSILE,  ELEMENT_SMOKE,    ELEMENT_MAGMA,  5000)

Interaction.Create(ELEMENT_FIRE,     ELEMENT_WATER,    ELEMENT_EMPTY,    ELEMENT_STEAM,  10000)
Interaction.Create(ELEMENT_FIRE,     ELEMENT_GRAPHITE, ELEMENT_EMBER,    ELEMENT_SMOKE,  2500)
Interaction.Create(ELEMENT_FIRE,     ELEMENT_SAND,     ELEMENT_MAGMA,    ELEMENT_SMOKE,  150)
Interaction.Create(ELEMENT_FIRE,     ELEMENT_WOOD,     ELEMENT_FIRE,     ELEMENT_GRAPHITE,  2000)
Interaction.Create(ELEMENT_FIRE,     ELEMENT_GRASS,    ELEMENT_FIRE,     ELEMENT_FIRE,  1500)
Interaction.Create(ELEMENT_FIRE,     ELEMENT_SEED,     ELEMENT_FIRE,     ELEMENT_EMBER,  500)
Interaction.Create(ELEMENT_FIRE,     ELEMENT_GUNPOWDER,ELEMENT_FIRE,     ELEMENT_FIRE,   15000)

Interaction.Create(ELEMENT_OIL,      ELEMENT_FIRE,     ELEMENT_FIRE,     ELEMENT_FIRE,   15000)
Interaction.Create(ELEMENT_OIL,      ELEMENT_MAGMA,    ELEMENT_FIRE,     ELEMENT_FIRE,   5000)
Interaction.Create(ELEMENT_OIL,      ELEMENT_MISSILE,  ELEMENT_SMOKE,    ELEMENT_FIRE,   25000)

Interaction.Create(ELEMENT_ACID,     ELEMENT_GRAPHITE, ELEMENT_STEAM,    ELEMENT_ACID,   200)
Interaction.Create(ELEMENT_ACID,     ELEMENT_CONCRETE, ELEMENT_GRAPHITE, ELEMENT_ACID,   225)
Interaction.Create(ELEMENT_ACID,     ELEMENT_OIL,      ELEMENT_STEAM,    ELEMENT_ACID,   900)
Interaction.Create(ELEMENT_ACID,     ELEMENT_SAND,     ELEMENT_STEAM,    ELEMENT_ACID,   2500)
Interaction.Create(ELEMENT_ACID,     ELEMENT_SALT,     ELEMENT_STEAM,    ELEMENT_ACID,   8500)
Interaction.Create(ELEMENT_ACID,     ELEMENT_WATER,    ELEMENT_STEAM,    ELEMENT_ACID,   200)
Interaction.Create(ELEMENT_ACID,     ELEMENT_WOOD,     ELEMENT_GRAPHITE, ELEMENT_ACID,   10000)
Interaction.Create(ELEMENT_ACID,     ELEMENT_MISSILE,  ELEMENT_STEAM,    ELEMENT_ACID,   40000)
Interaction.Create(ELEMENT_ACID,     ELEMENT_GUNPOWDER,ELEMENT_STEAM,    ELEMENT_ACID,   400)
Interaction.Create(ELEMENT_ACID,     ELEMENT_GRASS,    ELEMENT_STEAM,    ELEMENT_ACID,   4000)


Interaction.Create(ELEMENT_WOOD,     ELEMENT_FIRE,     ELEMENT_FIRE,     ELEMENT_EMBER,  5000)
Interaction.Create(ELEMENT_WOOD,     ELEMENT_EMBER,    ELEMENT_FIRE,     ELEMENT_EMBER,  1000)
Interaction.Create(ELEMENT_WOOD,     ELEMENT_MISSILE,  ELEMENT_FIRE,     ELEMENT_EMBER,  1000)

Interaction.Create(ELEMENT_GUNPOWDER, ELEMENT_FIRE,    ELEMENT_FIRE,     ELEMENT_FIRE,     5000)
Interaction.Create(ELEMENT_GUNPOWDER, ELEMENT_MAGMA,   ELEMENT_FIRE,     ELEMENT_FIRE,     15000)
Interaction.Create(ELEMENT_GUNPOWDER, ELEMENT_MISSILE, ELEMENT_FIRE,     ELEMENT_FIRE,     50000)


Global selectedElement:Int = ELEMENT_WATER
Global renderMode:Int = RENDERMODE_BLOB
' todo - double sized pixels seem too much for the default windows OGL drivers in my old laptop. Need a dx fast plot really.
'?Win32
'renderMode = RENDERMODE_PIXEL
'?
Global drawMode:Int = DRAWMODE_PEN
Global frame:Int = 0
Global vSync:Int = 0
Global simulationRunning:Int = True
Global displayBackground:Int = True
Global loadSelectMode:Int = False
Global saveSelectMode:Int = False

Const MAX_PEN_SIZE:Int = 3
Global penSizes:Int[] = [1,3,5]
Global selectedPenSize:Int = 1
Global alphaByte:Byte

' -----------------------------------------------------------

Global fpstime:Long
Global fpsHistory:Int[MAX_FPS_HISTORY]

Global randomNumbers1:Float[1000000]
Global randomNumbersIndex1:Int = 0
Global randomNumbers2:Int[1000000]
Global randomNumbersIndex2:Int = 0
Global randomNumbers3:Float[1000000]
Global randomNumbersIndex3:Int = 0
For Local i:Int = 0 To 999999
	randomNumbers1[i] = Rnd(-1.0,1.0)
	randomNumbers2[i] = Rand(0,100000)
	randomNumbers3[i] = Rnd(0.0,1.0)
Next

Global prevMouseX:Int, prevMouseY:Int
Global drag:Int = False, dragFromX:Int, dragFromY:Int

Global blobImage:TImage = createBlobImage()
Global backgroundImage:TImage = createGradientImage(width,height)
Global menuImage:TImage = createMenuImage()
Global zoomImage:TImage = CreateImage(64,64,1,DYNAMICIMAGE + FILTEREDIMAGE)
Global selectedImageIndex:Int = 0

Global savedGamePixmap:TPixmap
createSaveGameDir()
initParticles()
reflectStateInMenu()

' ------------------------------------------------------------
' mainloop
' ------------------------------------------------------------
While Not KeyHit(KEY_ESCAPE)

    Cls

	SetOrigin centerX, centerY
	
	If displayBackground = True
		SetColor 0,24,96
		SetBlend SOLIDBLEND
		SetImageHandle backgroundImage,halfWidth,halfHeight
		DrawImage backgroundImage,0,-1
	End If
	
	If loadSelectMode = True
		savedGamePixmap = selectSavedFile()
		If savedGamePixmap <> Null Particle.initFromPixmap(savedGamePixmap)
		loadSelectMode = False
		reflectStateInMenu()
	Else If saveSelectMode = True
		savedGamePixmap = Particle.toPixmap() 
		savePixmap(savedGamePixmap)
		saveSelectMode = False
		selectedImageIndex = 0
		reflectStateInMenu()
	Else
   
	    ' process and render particles
		Particle.preRender()
		If simulationRunning = True
			For Local p:Particle = EachIn particles		
				p.update()
				p.interact()
				p.render()
			Next
		Else		
			For Local p:Particle = EachIn particles		
				p.render()
			Next
		End If
		Particle.postRender()
	
	End If
	
   SetColor 255,255,255	
   SetOrigin 0,0
   SetScale 1,1
   SetBlend ALPHABLEND
   SetAlpha 1
 
   DrawText  "FPS: " + countFps(),16+(xOrigin),6
   DrawText  CountList(particles),(xOrigin)+width-64,6
   userInput()
   updateMenu()
   If KeyDown(KEY_LCONTROL) Or simulationRunning = False Then displayZoom()
   If KeyDown(KEY_LCONTROL) And KeyHit(KEY_G) Then 	screenshot()
   
   Flip vSync
   frame = (frame + 1) Mod 50000
	If randomNumbersIndex1 > 500000 randomNumbersIndex1 = 0
	If randomNumbersIndex2 > 500000 randomNumbersIndex2 = 0
	If randomNumbersIndex3 > 500000 randomNumbersIndex3 = 0
   Delay 1
Wend
End


' --------------------------------------------------------------------------------------

' ------------------------------------------
' Simple 2D Image Sprite with z scaling.
' ------------------------------------------
Type TSprite

	Global sprites:TList = CreateList()
	Field x:Float,y:Float,z:Float
	Field image:TImage
	Field reflection:Int = True
	
	Function Create:TSprite(image:TImage, x:Float=0,y:Float=0,z:Float=0)
	
		Local s:TSprite = New TSprite
		SetImageHandle image, ImageWidth(image)/2, ImageHeight(image)/2 
		s.image = image
		s.x = x
		s.y = y
		s.z = z
		ListAddLast sprites,s

		Return s
		
	End Function
	
	Function deleteAll()
		ClearList(sprites)
	End Function

	' render all sprites in correct z order	
	Function renderAll()
	
		SetOrigin GraphicsWidth()/2, GraphicsHeight()/2
	
		sprites.sort()
		For Local s:TSprite = EachIn sprites
			s.render()
		Next
		
		SetOrigin 0,0
	
	End Function
	
	' sort by zOrder
	Method Compare:Int(other:Object)
		Local s:TSprite = TSprite(other)
		If z < s.z Return -1
		If z = s.z Return 0
		If z > s.z Return 1
	End Method
	
	Method render()
	
		Local scale:Float = 2^z
		Local sx:Int = x*(GraphicsWidth()/2)*scale
		Local sy:Int = y*(GraphicsHeight()/2)*scale
		SetScale scale,scale
		DrawImage image,sx,sy
		
		If reflection
			Local alpha:Float = GetAlpha()
			Local blend:Int = GetBlend() 
			
			SetAlpha alpha*.5
			SetScale scale,-scale
			DrawImage image,sx,sy+((ImageHeight(image)*scale))+1
			SetBlend SHADEBLEND
			SetAlpha 1.0
			DrawImage backgroundImage, sx, sy+1+((ImageHeight(backgroundImage)*scale))
			
			SetAlpha alpha
			SetBlend blend
			
		End If
			
	End Method
	
	Method remove()
		sprites.remove(Self)
	End Method
	

End Type

' -----------------------------
' Wrapper class for Element
' -----------------------------

Type TElement
	Field id:Int
	
	Function Create:TElement(id:Int)
		Local e:TElement = New TElement
		e.id = id
		Return e
	End Function
	
End Type


' ------------------------------
' File System object
' ------------------------------
Type TFile

    Field path:String
    Field filename:String
    Field modified:Int
    Field size:Int
	Field isLoaded:Int = False

    Function Create:TFile(filepath:String,filename:String,modified:Int,size:Int)

        Local file:TFile = New TFile
        file.path = filepath
        file.filename = filename
        file.modified = modified
        file.size = size

        Return file

    End Function

	Method toString:String()
		Return path +"/"+filename+" "+modified+" "+size+" "+isLoaded
	End Method

	' sort by last modified timestamp
	Method Compare:Int(otherObject:Object)
		Local file:TFile = TFile(otherObject)
		Return file.modified - Self.modified
	End Method


End Type

' ----------------------------------
' Element Particle
' ----------------------------------

Type Particle

	Field element:Int
	Field x:Int,y:Int
	Field xp:Float, yp:Float, xv:Float, yv:Float
	Field mass:Float
	Field life:Int
	Field energy:Int
	Field link:TLink
	Field asleep:Int = 0
	
	Function Create:Particle(element:Int, x:Int, y:Int, xv:Float=0.0, yv:Float=0.0)
	
		If element = ELEMENT_EMPTY
			clear(x,y)	
		Else If x > 0 And x < width And y > 0 And y < height And px[x,y] = Null
			Local p:Particle = New Particle
			p.x = x
			p.y = y
			p.xp = x
			p.yp = y+.99
			p.energy = 0
			p.init(element,xv,yv)
			px[x,y] = p
			p.link = particles.AddLast(p)
			'Print "created particle : " + p.toString()
			Return p
		End If
		
	End Function
	
	Function reset()
		For Local x:Int = 0 To width-1
			For Local y:Int = 0 To height-1
				If px[x,y] <> Null Particle.clear(x,y)
			Next
		Next
		
		ClearList particles
	End Function
	
	Function clear(x:Int,y:Int)
		If x >= 0 And x < width And y >= 0 And y < height
			Local p:Particle = px[x,y]
			If p <> Null
				px[x,y] = Null
				p.remove() 
			End If
		End If
	End Function
	
	Function toPixmap:TPixmap()
	
		Local pixmap:TPixmap = CreatePixmap(width,height, PF_RGBA8888)
		ClearPixels(pixmap, $00000080)
		
		For Local y:Int = 0 To height-1
			Local pb:Byte Ptr = PixmapPixelPtr(pixmap,0,y)
			Local alpha:Byte = 128+(y/4) 
			For Local x:Int = 0 To width-1
				pb[(x*4)+3] = alpha
			Next
		Next 
		
		For Local p:Particle = EachIn particles
			Local pb:Byte Ptr = PixmapPixelPtr(pixmap,p.x,p.y)
			pb[0] = elementRed[p.element]
			pb[1] = elementGreen[p.element]
			pb[2] = elementBlue[p.element]
			pb[3] = 255
		Next
		
		Return pixmap
	
	End Function
	
	Function initFromPixmap(pixmap:TPixmap)

		Local red:Byte
		Local green:Byte
		Local blue:Byte
		Local colorKey:String
		Local element:Int
		Local e:TElement
	
		' create colour -> element lookup map		
		Local colorToElementMap:TMap = CreateMap()
		For element = 0 To MAX_ELEMENTS-1
			e = TElement.Create(element)
			colorKey = rgbToString(elementRed[element],elementGreen[element],elementBlue[element])
			MapInsert(colorToElementMap, colorKey, e)
		Next
	
		Particle.reset()
		For Local y:Int = 0 To height-1
			Local pb:Byte Ptr = PixmapPixelPtr(pixmap,0,y)
			For Local x:Int = 0 To width-1
				red   = pb[(x*4)+0]
				green = pb[(x*4)+1]
				blue  = pb[(x*4)+2]
				colorKey = rgbToString(red,green,blue)				
				e = TElement(MapValueForKey(colorToElementMap, colorKey))
				
				'Local approx:Int = (((red + green + blue) /3)/8)
				'If approx < MAX_ELEMENTS Then Particle.Create(approx,x,y)
				If e <> Null And e.id <> ELEMENT_EMPTY Then Particle.Create(e.id,x,y)
			Next
		Next 		
	
	End Function
	
	Function preRender() 
	
		Select renderMode
			Case RENDERMODE_PIXEL
				SetBlend ALPHABLEND
				alphaByte = 255
				glDisable GL_TEXTURE_2D
				glPointSize 1.0
				glBegin GL_POINTS

			Case RENDERMODE_BLOB
				SetBlend ALPHABLEND
				alphaByte = 128
				glDisable GL_TEXTURE_2D
				glPointSize 2.0 
				glBegin GL_POINTS

			Case RENDERMODE_BLOB2
				SetBlend ALPHABLEND
				alphaByte = 64
				glDisable GL_TEXTURE_2D
				glPointSize 2.0 
				glBegin GL_POINTS
				
			Case RENDERMODE_IMAGE
				SetBlend LIGHTBLEND
				alphaByte = 64
				SetScale .5,.5
		
		End Select
	
	End Function
	
	Function postRender()
	
		Select renderMode

			Case RENDERMODE_IMAGE
				SetScale 1,1
				
			Default
				glEnd
				glEnable GL_TEXTURE_2D
				
		End Select

	End Function
	
	Method init(targetElement:Int, xVelocity:Float=0.0, yVelocity:Float=0.0)
		element = targetElement
		life = lifespan[element]
		If life > 0 life = Rand(life/2,life)
		xv = xVelocity
		yv = yVelocity
		'mass = Rnd(elementMass[element]*.5, elementMass[element]*2.0)
		mass = (elementMass[element]*.5) + (randomNumbers3[randomNumbersIndex3] * elementMass[element]*1.5) 
		randomNumbersIndex3 :+ 1
	End Method
	
	Method remove()
		link.remove()
	End Method
	
	Method toString:String()
		Return "[element:"+element+" life:"+life+" x:"+x+" y:"+y+"]"
	End Method
	
	Method airmove()

		' debug
		If yv > 1.0 Then 
			yv = 1.0
		Else If yv < -1.0 Then 
			yv = -1.0
		End If


		If yv > 1.0 Then 
			yp :+ 1.0
		Else If yv < -1.0 Then 
			yp :- 1.0
		Else
			yp :+ yv
		End If
	
		If Int(yp) <> y
			px[x,y] = Null
			y = yp
			If y >= height Or y < 1 Then 
				remove()
			Else
				px[x,y] = Self
			End If
		End If
		' apply air friction
		yv :+ (mass * gravity[element])*.005
	
	End Method
	
	Method move(neighbour:Particle)
	
		If elementImmovable[neighbour.element]
			yv = -yv * elementBounce[element]
		Else
	
			If neighbour.element <> element 
				
				' debug
'				If yv > 1.0 Then 
'					yv = 1.0
'				Else If yv < -1.0 Then 
'					yv = -1.0
'				End If
	
				
				If yv > 1.0 Then 
					yp :+ 1.0
				Else If yv < -1.0 Then 
					yp :- 1.0
				Else
					yp :+ yv
				End If
				If Int(yp) <> y
					px[x,y] = neighbour
					neighbour.y = y
					neighbour.yp = y
					y = yp
					If y >= height Or y < 1 Then 
						remove()
					Else
						px[x,y] = Self
					End If
				End If
				yv = (((mass * gravity[element])*.05) * elementDrag[neighbour.element])
	
			Else
				neighbour.yv :+ (yv*.05) ' impart velocity to neighbour 
				yv = yv * .95 ' slow down if next to same element
			End If
		
		End If
	
	End Method

	
	Method update()

		If life >= 0 Then 
			life :- 1
			If life <= 0 Then
				If elementAfterlife[element] = ELEMENT_EMPTY 
					Particle.clear(x,y)
				Else If element <> elementAfterlife[element]
					changeElement(Self, elementAfterlife[element], xv, yv)
				End If 
				Return
			End If
		End If

		Local neighbour:Particle
		Local nextTo:Particle	

		
		Select element
		
			' solids don't move
			Case ELEMENT_CONCRETE
			Case ELEMENT_GRAPHITE
			Case ELEMENT_EMBER
			Case ELEMENT_WOOD
			Case ELEMENT_GRASS
			
			Case ELEMENT_MISSILE
			
				Local oyp:Float = yp
				If yv > 1.0 Then 
					yp :+ 1.0
				Else If yv < -1.0 Then 
					yp :- 1.0
				Else
					yp :+ yv
				End If
				
				Local oxp:Float = xp
				If xv > 1.0 Then 
					xp :+ 1.0
				Else If xv < -1.0 Then 
					xp :- 1.0
				Else
					xp :+ xv
				End If
				
				If Int(yp) <> y
					If px[x,yp] = Null
						px[x,y] = Null
						If life < lifespan[element]/2
							If life Mod 3 = 0 Then 							Particle.Create(ELEMENT_FIRE,oxp,oyp,-xv*.25,-yv*.25)
						Else
							Particle.Create(ELEMENT_FIRE,oxp,oyp,-xv*.25,-yv*.25)
						End If
						y = yp
						px[x,y] = Self
					Else
						yp = oyp
						'yv = -yv
					End If
					
				End If

				If Int(xp) <> x
					If px[xp,y] = Null
						px[x,y] = Null
						'Particle.Create(ELEMENT_EMBER,oxp,oyp,xv,yv)
						x = xp
						px[x,y] = Self
					Else
						xp = oxp
						'xv = -xv
					End If
				End If
				
				yv :+ .005
				' friction
				xv :* .998
				yv :* .998
			
					
			Default
							
				If yv > 0 
					If px[x,y+1] = Null Then 
						airmove()				
					Else 
						move(px[x,y+1])
					End If			
					
				Else If yv < 0 
					If  px[x,y-1] = Null Then 
						airmove()
					Else 
						move(px[x,y-1])
					End If
				Else 
					yv :+ ((mass * gravity[element])*.005)
				End If
				
		
				If px[x,y+1] <> Null
					' element below
				 	randomNumbersIndex2 :+1
					If xv = 0 Or (slippyness[element] = 1.0 And randomNumbers2[randomNumbersIndex2] < 10000)
						randomNumbersIndex1 :+1
						Local slip:Float = Sgn(randomNumbers1[randomNumbersIndex1])
						If px[x+slip,y] = Null Then xv = slip
					End If
					xv :* slippyness[element]

				Else
					' air below
					randomNumbersIndex1 :+1		
					xv :+ randomNumbers1[randomNumbersIndex1] * slippyness[element]
					xv :* airDamp[element]

				End If	 

				' slide
				
				' debug
'				If xv > 1.0 Then 
'					xv = 1.0
'				Else If xv < -1.0 Then 
'					xv = -1.0
'				End If

				
				If xv > 1.0 Then 
					xp :+ 1.0
					If xv > 8.0 xv = 8.0 
				Else If xv < -1.0 Then 
					xp :- 1.0
					If xv < -8.0 xv = -8.0
				Else
					xp :+ xv
				End If											

				'xp :+ xv
				If Int(xp) <> x
					nextTo = px[xp,y]
					If nextTo = Null
						px[x,y] = Null
						x = xp
						yp = y+.5
						If px[x,y+1] = Null Then 
							yv :+  slippyness[element]	 * Sgn gravity[element] * .15 
						End If
						If x > 1 And x < width-1
							px[x,y] = Self
						Else
							remove()
						End If
					Else
						If nextTo.element <> element
							nextTo.xv :+ (xv*elementDrag[nextTo.element]*2.0)
						End If
						xv = -xv						
						xp = x
					End If			
				End If
				
		
		End Select
		
	
	End Method

	Method interact()
			
		Select element
		
            Case ELEMENT_WATER
				interactWithNeighbours()								
			Case ELEMENT_GRAPHITE
				interactWithNeighbours()				
			Case ELEMENT_EMBER
				interactWithNeighbours()			
            Case ELEMENT_MAGMA
				interactWithNeighbours()				
            Case ELEMENT_FIRE
				interactWithNeighbours()
			Case ELEMENT_GUNPOWDER
				interactWithNeighbours()
				If element = ELEMENT_FIRE
					' explode
					Local a:Int = Rand(0,360)
					Particle.Create(ELEMENT_MISSILE,x,y-1,Sin(a),Cos(a)*2.0)
					xv = Rnd(-1.0,1.0)*4.0
					yv = Rnd(-1.0,1.0)*4.0
				End If
            Case ELEMENT_OIL
				interactWithNeighbours()
            Case ELEMENT_ACID
				interactWithNeighbours()
            Case ELEMENT_WOOD
				If getNeighbourElements() <> 0
					If px[x,y+1] <> Null
						Local p:Particle = px[x,y+1]
						If p.element = ELEMENT_WATER And Rand(0,200) = 0 Then changeElement(p,ELEMENT_WOOD,0,0)
					End If
					interactWithNeighbours()
				Else If life > 0
					Particle.Create(ELEMENT_GRAPHITE,x-1,y)
					Particle.Create(ELEMENT_GRAPHITE,x+1,y)
				Else
					changeElement(Self, ELEMENT_SMOKE, 0, 0)
				End If
				
			Case ELEMENT_SEED

				If frame & 1 = 0
					Local match:Int = elementBitmask[ELEMENT_SAND] | elementBitmask[ELEMENT_WATER]
					If (getNeighbourElements() & match) = match Then
						If Rand(0,10) = 0
							changeElement(Self, ELEMENT_SPROUT, 0, 0)
							energy = Rand(296,400)
						Else
							changeElement(Self, ELEMENT_GRASS, Rnd(-.2,.2), -1.0)
							energy = Rand(10,20) 
						End If
					End If
				Else
					Local match:Int = elementBitmask[ELEMENT_WOOD] | elementBitmask[ELEMENT_WATER]
					If (getNeighbourElements() & match) = match Then
						If Rand(0,2) = 0
							changeElement(Self, ELEMENT_SPROUT, 0, 0)
							energy = Rand(100,200)
						Else
							changeElement(Self, ELEMENT_GRASS, Rnd(-.5,.5), -.5)
							energy = Rand(5,10)
						End If
					End If

				End If
			
			Case ELEMENT_SPROUT
				
				If energy > 0 And life > 0
					If energy Mod 24 = 13
						' branch
						Local p:Particle = Particle.Create(ELEMENT_SPROUT,x+xv,y-1,-xv,-(energy/300.0))
						If p <> Null 
							p.life = life
							p.energy = energy-1
						End If	
					End If
		
					Particle.Create(ELEMENT_WOOD,x,y+1) ; energy :- 1
					If energy > 196 Then Particle.Create(ELEMENT_WOOD,x-1,y+1) ; energy :- 1
					If energy > 128 Then Particle.Create(ELEMENT_WOOD,x+1,y+1) ; energy :- 1
					
				End If
				
			Case ELEMENT_GRASS
			
				If energy > 0 And Rand(0,10) = 0
					
					If xv > 1.0 xv = 1.0
					If xv < -1.0 xv = -1.0
					xp :+ xv
					If yv > 1.0 yv = 1.0
					If yv < -1.0 yv = -1.0
					yp :+ yv

					If px[xp,yp] = Null
						Local p:Particle = Particle.Create(ELEMENT_GRASS, xp, yp, xv*1.05, yv*.95)
						If p <> Null 
							p.energy = energy-1
							p.xp = xp
							p.yp = yp
						End If	
						xp = x
						yp = y
						energy = 0
					Else
						If Int(xp) <> x Then xp :- xv
						If Int(yp) <> y Then yp :- yv
						energy :-1
					End If

					
				End If
							
		End Select
	
	End Method
	
	Method getNeighbourElements:Int()
	
		Local pTop:Particle = px[x,y-1]
		Local pBottom:Particle = px[x,y+1]
		Local pLeft:Particle = px[x-1,y]
		Local pRight:Particle = px[x+1,y]
		
		Local bitmask:Int = 0
		If pTop    <> Null bitmask :| elementBitmask[pTop.element]
		If pBottom <> Null bitmask :| elementBitmask[pBottom.element]
		If pLeft   <> Null bitmask :| elementBitmask[pLeft.element]
		If pRight  <> Null bitmask :| elementBitmask[pRight.element]

		Return bitmask
	
	End Method
	
	Method interactWithNeighbours()
		Local pTop:Particle = px[x,y-1]
		Local pBottom:Particle = px[x,y+1]
		Local pLeft:Particle = px[x-1,y]
		Local pRight:Particle = px[x+1,y]
		
		If pTop <> Null And pTop.element <> element Then interactWith(pTop)	
		If pBottom <> Null And pBottom.element <> element Then interactWith(pBottom)
		If pLeft <> Null And pLeft.element <> element Then interactWith(pLeft)
		If pRight <> Null And pRight.element <> element Then interactWith(pRight) 				
	End Method
	
	Method interactWith(p:Particle)

		If elementInteractions[element, p.element] <> Null
			Local i:Interaction = elementInteractions[element, p.element]
			randomNumbersIndex2 :+1
			If randomNumbers2[randomNumbersIndex2] < i.spreadChance Then 
				changeElement(p, i.spreadElement)
				changeElement(Self, i.residueElement)
			End If
		End If
	
	End Method
		
	Function changeElement(p:Particle, targetElement:Int, xVelocity:Float=0.0, yVelocity:Float=0.0)
		If targetElement = ELEMENT_EMPTY Then
				Particle.clear(p.x,p.y) 
		Else	
				p.init(targetElement,xVelocity,yVelocity)
		End If
	End Function		
				
	Method render()
	
	    Select element
	
			Case ELEMENT_EMBER
				glColor4ub life*5,life*2,life,alphaByte

            Case ELEMENT_FIRE
				glColor4ub 255,life*2,life/2,alphaByte
				
			Case ELEMENT_SMOKE
				glColor4ub life,life,life,alphaByte

			Case ELEMENT_STEAM
				glColor4ub 250,250,255,life
				
			Case ELEMENT_WOOD
				glColor4ub elementRed[element], elementGreen[element]+(life/2), elementBlue[element],((y*y)+x) | $60

			Case ELEMENT_GUNPOWDER
				glColor4ub $10+(160*yv*xv), $10+(160*yv*xv), $10+(160*yv*xv),alphaByte

			Case ELEMENT_MISSILE
				glColor4ub elementRed[element], elementGreen[element], elementBlue[element],(life/Float(lifespan[element]))*255

			

			Default 
				'glColor4ub elementRed[element]+(255*yv), elementGreen[element]+(255*yv), elementBlue[element]+(255*yv),alphaByte
				'glColor4ub elementRed[element]+(255*xv), elementGreen[element]+(255*xv), elementBlue[element]+(255*xv),alphaByte
				'glColor4ub elementRed[element]-asleep, elementGreen[element]-asleep, elementBlue[element]-asleep,alphaByte

				glColor4ub elementRed[element], elementGreen[element], elementBlue[element], alphaByte 				 
         End Select
	
		Select renderMode
		
			Case RENDERMODE_PIXEL
				glVertex2i x+xOrigin,y+yOrigin
			Case RENDERMODE_BLOB
				glVertex2i x+xOrigin,y+yOrigin
			Case RENDERMODE_BLOB2
				glVertex2i x+xOrigin,y+yOrigin
				glVertex2i x+xOrigin+1,y+yOrigin+1
				glVertex2i x+xOrigin,y+yOrigin+1
				glVertex2i x+xOrigin+1,y+yOrigin
			Case RENDERMODE_IMAGE
				DrawImage blobImage,x-halfWidth,y-halfHeight
		End Select
				
	End Method
	
End Type

Type Interaction

	Field spreadElement:Int
	Field residueElement:Int
	Field spreadChance:Int
	
	Function Create:Interaction(sourceElement:Int, neighbourElement:Int, spreadElement:Int, residueElement:Int, spreadChance:Int)
		
		Local i:Interaction = New Interaction
		i.spreadChance = spreadChance
		i.spreadElement = spreadElement
	    i.residueElement = residueElement
		elementInteractions[sourceElement, neighbourElement] = i
		Return i
		
	End Function

End Type

Type MenuItem

	Field x:Int, y:Int, w:Int, h:Int
	Field id:Int
	Field group:Int
	Field selected:Int = False
	Field recentlySelected:Int = False
	Field selectFade:Float = 0.0
	Field image:Timage
	Field selectedImage:Timage
	Field selectedImageXOffset:Int
	Field selectedImageScale:Float
	Field selectedReflectionImage:TImage
	Field buttonReleased:Int = True

	
	Function Create:MenuItem(x:Int,y:Int,w:Int,h:Int, id:Int, image:TImage, group:Int=0, selectedImage:TImage=Null, selectedImageScale:Float=.25, selectedImageXOffset:Int=0, selectedReflectionImage:TImage = Null )
		Local mi:MenuItem = New MenuItem
		mi.x = x
		mi.y = y
		mi.w = w
		mi.h = h
		mi.id = id
		mi.image = image
		mi.group = group
		menuItems.addLast(mi)
		If selectedImage = Null 
			mi.selectedImage = image
		Else
			mi.selectedImage = selectedImage
		End If
		mi.selectedImageScale = selectedImageScale
		mi.selectedImageXOffset = selectedImageXOffset
		mi.selectedReflectionImage = selectedReflectionImage

'		Print "created MenuItem [ x:"+x+" y:"+y+" w:"+w+" h:"+h+" id:"+id+" group:"+group+"]"
		Return mi
	End Function
	
	Function setSelectedItem(group:Int, id:Int)
		For Local mi:MenuItem = EachIn menuItems
			If mi.group = group And mi.id = id Then mi.selectItem()	
		Next 		
	End Function
	
	Method update()
		If buttonReleased = False
			If Not MouseDown(1) Then buttonReleased = True
		End If
		If selected = True Or selectFade > 0 Then highlightSelected()
		If MouseX() >= x And MouseX() < x+w And MouseY() >= y And MouseY() < y+h
			If buttonReleased = True And MouseDown(1)
				selectItem()
				recentlySelected = True
			End If
			highlightHover()
		End If
	End Method
	
	Method hasJustBeenSelected:Int()
		If recentlySelected = True Then
			recentlySelected = False
			buttonReleased = False
			Return True
		End If
	End Method
	
	Method highlightHover()
		SetBlend LIGHTBLEND
		SetColor 64,255,255
		SetAlpha .25
		SetScale .25,.25
		DrawImage image,x,y
		SetScale .25,-.25
		DrawImage image,x,y+(ImageHeight(image)/4)
		SetScale 1,1	
	End Method
	
	Method highlightSelected()
		SetBlend LIGHTBLEND
		SetColor 255,255,0
		
		If selectFade > 0 
			SetAlpha selectFade * .5
			selectFade :- .04
		Else
			SetAlpha .5
		End If
		SetScale selectedImageScale,-selectedImageScale
		DrawImage selectedImage,x+selectedImageXOffset,y+(ImageHeight(selectedImage)*selectedImageScale)

		' reflection
		SetAlpha (GetAlpha() *.5)
		SetScale selectedImageScale,selectedImageScale*.5
		If selectedReflectionImage <> Null
			DrawImage selectedReflectionImage,x+selectedImageXOffset,y+(ImageHeight(selectedReflectionImage)*selectedImageScale)+1
		Else
			DrawImage selectedImage,x+selectedImageXOffset,y+(ImageHeight(selectedImage)*selectedImageScale)+1
		End If
		
		
		SetScale 1,1	
	End Method
	
	Method selectItem()
		If selected = False
			' deselect any other selected menuItem in same group
			For Local mi:MenuItem = EachIn menuItems
				If mi.group = group Then mi.deselectItem()	
			Next 
			selected = True
		End If
	End Method
	
	Method deselectItem()
		If selected = True Then 
			selected = False
			selectFade = 1.0
		End If
	End Method

End Type


' -----------------------------------------

Function checkCorruption()
	For Local x:Int = 0 To width-1
		For Local y:Int = 0 To height-1
			Local p:Particle = px[x,y]
			If p <> Null
				If p.x <> x Or p.y <> y Then 				RuntimeError("corruption detected @ " + x + " "+ y + " " + p.toString())
			End If
		Next
	Next
End Function

' -----------------------------------------
Function userInput()

	If MouseX() > (xOrigin)-20 And MouseX() < (xOrigin+width)+20 
		draw(selectedElement)
	End If

	Local oldSelectedElement:Int = selectedElement	
	If KeyHit(KEY_1)
		selectedElement = ELEMENT_EMPTY
	Else If KeyHit(KEY_2)
		selectedElement = ELEMENT_WATER
	Else If KeyHit(KEY_3)
			selectedElement = ELEMENT_CONCRETE
	Else If KeyHit(KEY_4)
		    selectedElement = ELEMENT_SALT
	Else If KeyHit(KEY_5)
		    selectedElement = ELEMENT_MAGMA
	Else If KeyHit(KEY_6)
		  	selectedElement = ELEMENT_FIRE
	Else If KeyHit(KEY_7)
		    selectedElement = ELEMENT_OIL
	Else If KeyHit(KEY_8)
		    selectedElement = ELEMENT_SAND
	Else If KeyHit(KEY_9)
			selectedElement = ELEMENT_SMOKE
	End If
	If selectedElement <> oldSelectedElement Then reflectStateInMenu()
	
	Local modifier:Int = False
	If KeyDown(KEY_LCONTROL) Or KeyDown(KEY_LSYS) Or KeyDown(KEY_RSYS)
		modifier = True
	End If
	
	If modifier And KeyHit(KEY_N)
		initParticles() 
	Else If modifier And KeyHit(KEY_O)
		MenuItem.setSelectedItem(ITEMGROUP_CONTROL, 0)
		loadSelectMode = True
	Else If modifier And KeyHit(KEY_S)
		MenuItem.setSelectedItem(ITEMGROUP_CONTROL, 1)
		saveSelectMode = True
	Else If KeyHit(KEY_D)
			drawMode = (drawMode + 1) Mod MAX_DRAWMODES
			reflectStateInMenu()
	Else If KeyHit(KEY_R)
			renderMode = (renderMode + 1) Mod MAX_RENDERMODES
	Else If KeyHit(KEY_S)
			selectedPenSize = (selectedPenSize + 1) Mod MAX_PEN_SIZE
			reflectStateInMenu()
	Else If KeyHit(KEY_SPACE)
			simulationRunning = Not simulationRunning
			reflectStateInMenu()
	Else If KeyHit(KEY_G)  reverseGravity()
	Else If KeyHit(KEY_V)  vSync = vSync ~ 1
	Else If KeyHit(KEY_B)  displayBackground = Not displayBackground
	Else If KeyHit(KEY_F1) initParticles()
	Else If KeyHit(KEY_F2) drawPreset(1)
	Else If KeyHit(KEY_F3) drawPreset(0)
	Else If KeyHit(KEY_F12) And savedGamePixmap <> Null Then Particle.initFromPixmap(savedGamePixmap)
	Else If KeyHit(KEY_C) checkCorruption()
	End If
	
End Function

Function displayZoom()

	Local cursorX:Int = MouseX()-xOrigin
	Local cursorY:Int = MouseY()-yOrigin

	Local tmpXOrigin:Int = xOrigin
	Local tmpYOrigin:Int = yOrigin
	Local tmpRenderMode:Int = renderMode
	
	'xOrigin = (xOrigin + width + 10)-cursorX
	'yOrigin = (GraphicsHeight()-yOrigin-128)-cursorY
	renderMode = RENDERMODE_BLOB
	
	SetColor 0,0,255
	SetScale .25,.25
	DrawImage backgroundImage,GraphicsWidth()-(xOrigin/2), GraphicsHeight()-(yOrigin+64)
	SetColor 64,64,64
	SetScale .25,-.25
	DrawImage backgroundImage,GraphicsWidth()-(xOrigin/2), GraphicsHeight()-(yOrigin+64)

	
	Particle.preRender()
	
	Local xoffset:Int = GraphicsWidth() - (cursorX+30)
	Local yoffset:Int = (GraphicsHeight()-(centerY-halfHeight))-(cursorY+22)
	
	For Local x:Int = cursorX-21 To cursorX+21
		For Local y:Int = cursorY-21 To cursorY+21
			If x >= 0 And x < width And y >= 0 And y < height
				If px[x,y] <> Null
					xOrigin = xoffset + ((x-cursorX-21)*2)
					yOrigin = yoffset + ((y-cursorY-21)*2)
					Local p:Particle = px[x,y]
					p.render()
				End If
			End If
		Next
	Next
	
	Particle.postRender()
	SetScale 1,1
	SetColor 255,255,255
	xOrigin = tmpXOrigin
	yOrigin = tmpYOrigin
	renderMode = tmpRenderMode
	
	SetAlpha .25 + ((frame Mod 8)/16.0)
	DrawRect xOrigin+width+10+62-(penSizes[selectedPenSize]*1.25),GraphicsHeight()-yOrigin-64-(penSizes[selectedPenSize]*1.25),(penSizes[selectedPenSize]*2.5),(penSizes[selectedPenSize]*2.5)


	If MouseX() > xOrigin And MouseX() < xOrigin+width And MouseY() > yOrigin And MouseY() < yOrigin+height
		Local p:Particle = px[MouseX()-xOrigin,MouseY()-yOrigin]
		Local eType:String = "Air"
		If p <> Null eType = elementLabels[p.element]
		
		SetAlpha .5
		SetBlend LIGHTBLEND
		DrawText eType, GraphicsWidth()-xOrigin+12,GraphicsHeight()-yOrigin-126
		'If p <> Null DrawText p.toString(), 0,30
	End If	 

	

End Function

Function oldDisplayZoom()
	GrabImage(zoomImage,MouseX()-32,MouseY()-32)
	SetScale 2,2
	SetBlend SOLIDBLEND
	SetColor 255,255,255
	DrawImage zoomImage,xOrigin+width+10,GraphicsHeight()-yOrigin-128
	SetScale 1,1
	
	If MouseX() > xOrigin And MouseX() < xOrigin+width And MouseY() > yOrigin And MouseY() < yOrigin+height
		Local p:Particle = px[MouseX()-xOrigin,MouseY()-yOrigin]
		Local eType:String = "Air"
		If p <> Null eType = elementLabels[p.element]
		
		SetAlpha .5
		SetBlend LIGHTBLEND
		DrawText eType, GraphicsWidth()-xOrigin+12,GraphicsHeight()-yOrigin-126
		'If p <> Null DrawText p.toString(), 0,30
	End If	 
	SetAlpha 1
	SetBlend SOLIDBLEND

End Function

Function updateMenu()
	
	SetColor 255,255,255	
   SetOrigin 0,0
   SetScale 1,1
   SetBlend ALPHABLEND
   SetAlpha 1
   DrawImage menuImage,0,yOrigin

	
	For Local mi:MenuItem = EachIn menuItems
		mi.update()
		If mi.hasJustBeenSelected()
			Select mi.group
			
				Case ITEMGROUP_ELEMENTS
					 selectedElement = mi.id
					
				Case ITEMGROUP_BRUSH
				    selectedPenSize = mi.id Mod 3
					drawMode = mi.id / 3
					
				Case ITEMGROUP_CONTROL
					Select mi.id
					    Case 0
							loadSelectMode = True
					    Case 1
							saveSelectMode = True
					    Case 2
						Case 3 
							initParticles()
							reflectStateInMenu()
						Case 4 simulationRunning = False
						Case 5 simulationRunning = True
					End Select					
			End Select 
		End If
	Next

End Function

Function reverseGravity()

	For Local i:Int = 0 To MAX_ELEMENTS-1
		gravity[i] = -gravity[i]
	Next

End Function

' --------------------------------------------------------------------

Function createGradientImage:TImage(w:Int,h:Int)
	Local image:TImage = CreateImage(w,h)
	Local pix:TPixmap = LockImage(image)
	
	Cls
	SetBlend SOLIDBLEND
	For Local y:Int = 0 To h
		Local c:Int = 255*(y/Float(h))
		SetColor c,c,c
		DrawLine 0,y,w,y
	Next
	
	GrabImage image,0,0
	UnlockImage image
	Return image

End Function

Function createBlobImage:TImage()
	Local image:TImage = CreateImage(8,8)
	Local pix:TPixmap = LockImage(image)
	Cls
	SetColor 255,255,255
	SetBlend LIGHTBLEND
	SetAlpha .2
	SetScale 1,1
	DrawOval 0,0,8,8
	DrawOval 1,1,6,6
	DrawOval 2,2,4,4
	DrawOval 3,3,2,2 
	
	GrabImage image,0,0
	UnlockImage image
	SetImageHandle image,4,4
	Return image

End Function

Function createButtonBackgroundImage:TImage(buttonWidth:Int=100, buttonHeight:Int=24, lowerCMult:Float = 1.0)

	Local image:TImage = CreateImage(buttonWidth,buttonHeight)
	Local pix:TPixmap = LockImage(image)
    Cls
	SetColor 255,255,255
	SetBlend SOLIDBLEND
	DrawOval 0,0,buttonWidth/4,buttonHeight
	DrawOval buttonWidth - (buttonWidth/4),0,buttonWidth/4,buttonHeight
	DrawRect buttonWidth/8,0,buttonWidth - (buttonWidth/4),buttonHeight 
	
	SetBlend SHADEBLEND
	
	For Local y:Int = 0 To buttonHeight/1.65
		Local c:Int = 225-(y*3)
		SetColor c,c,c
		For Local x:Int = 0 To buttonWidth/8
			Plot buttonWidth/8-x, y-((x*x)*.004)
			Plot (buttonWidth-buttonWidth/8)+x,y-((x*x)*.004)
		Next
		DrawLine 1+(buttonWidth/8),y,buttonWidth-(buttonWidth/8)-1,y
	Next

	For Local y:Int = buttonHeight/1.65 To buttonHeight
		Local c:Int = ((y*4)-205)*lowerCMult
		SetColor c,c,c
		For Local x:Int = 0 To buttonWidth/8
			Plot buttonWidth/8-x, y-((x*x)*.004)
			Plot (buttonWidth-buttonWidth/8)+x,y-((x*x)*.004)
		Next
		DrawLine 1+(buttonWidth/8),y,buttonWidth-(buttonWidth/8)-1,y
	Next
	
	GrabImage image,0,0
	UnlockImage image
	Return image

End Function

Function createMenuImage:TImage()

    Local buttonImage:TImage = createButtonBackgroundImage((xOrigin-40)*4,22*4)
	Local smallButtonImage:TImage = createButtonBackgroundImage((xOrigin-40),22*4)
	Local smallButtonReflectionImage:TImage = createButtonBackgroundImage((xOrigin-40),22*4,0.0)
	Local image:TImage = CreateImage(xOrigin,GraphicsHeight())
	Local pix:TPixmap = LockImage(image)
	Cls

	' elements menu	
	SetColor 255,255,255
	SetBlend SOLIDBLEND
	DrawText "ELEMENTS",(xOrigin-TextWidth("ELEMENTS"))/2,6

	Local y:Int = 30	
	Local i:Int = 0
	For Local label:String = EachIn elementLabels
	
		If elementCreatable[i] = True
		    SetScale .25,-.25
			SetBlend SOLIDBLEND
			SetColor elementRed[i]*.75,elementGreen[i]*.75,elementBlue[i]*.75
			DrawImage buttonImage,20,y-4+22
			SetColor 255,255,255
			SetScale .25,.25
			SetAlpha .25
			SetBlend LIGHTBLEND
			DrawImage buttonImage,20,y-4
	
			SetScale 1,1
			SetBlend ALPHABLEND
			SetAlpha .5		
			SetColor 0,0,0
			DrawText label, 1+((xOrigin) - TextWidth(label))/2, y+1
			SetAlpha .25
			DrawText label, 2+((xOrigin) - TextWidth(label))/2, y+2
	
			SetAlpha .75
			SetColor 225,225,225
			DrawText label, ((xOrigin) - TextWidth(label))/2, y
	
			MenuItem.Create(20, y+40, xOrigin-40, 24, i, buttonImage, ITEMGROUP_ELEMENTS, blobImage, 1.5, xOrigin-30)
			y:+24
		End If
		i:+1
	Next

	' brush menu	
	Local buttonWidth:Int = ImageWidth(smallButtonImage)/4
	Local buttonHeight:Int = ImageHeight(smallButtonImage)/4
	Local x:Int = (xOrigin-((buttonWidth+2)*3))/2

	For Local i:Int = 0 To MAX_PEN_SIZE-1

        ' small button background
		SetBlend ALPHABLEND
		SetAlpha .75
		SetColor 228,168,228
		SetScale .25,.25
		DrawImage smallButtonImage, x+((2+buttonWidth)*i),y+24
		DrawImage smallButtonImage, x+((2+buttonWidth)*i),y+48
		DrawImage smallButtonImage, x+((2+buttonWidth)*i),y+72
		
		SetAlpha .45
		DrawImage smallButtonImage, x+((2+buttonWidth)*i),y+120
		DrawImage smallButtonImage, x+((2+buttonWidth)*i),y+144

		' small button label shadows
		SetScale 1,1
		SetColor 0,0,0
		SetAlpha .5
		drawButtonLabels(i, x+1,y+1, buttonWidth, buttonHeight)
		SetColor 0,0,0
		drawButtonLabels(i, x+2,y+2, buttonWidth, buttonHeight)

		' small button label 
		SetColor 220,220,220
		SetAlpha .75
		drawButtonLabels(i, x,y, buttonWidth, buttonHeight)
		
		' small button 
		SetBlend ALPHABLEND
		SetAlpha .35
		SetColor 255,0,0
		SetScale .25,-.25
		DrawImage smallButtonImage, x+((2+buttonWidth)*i),y+24+22
		DrawImage smallButtonImage, x+((2+buttonWidth)*i),y+48+22
		DrawImage smallButtonImage, x+((2+buttonWidth)*i),y+72+22

		SetColor 128,255,0
		DrawImage smallButtonImage, x+((2+buttonWidth)*i),y+120+22
		DrawImage smallButtonImage, x+((2+buttonWidth)*i),y+144+22
		
		MenuItem.Create(x+(i*(buttonWidth+2)), y+24+44, buttonWidth-2, buttonHeight, (DRAWMODE_PEN*3)+i, smallButtonImage, ITEMGROUP_BRUSH)
		MenuItem.Create(x+(i*(buttonWidth+2)), y+48+44, buttonWidth-2, buttonHeight, (DRAWMODE_LINE*3)+i, smallButtonImage, ITEMGROUP_BRUSH)
		MenuItem.Create(x+(i*(buttonWidth+2)), y+72+44, buttonWidth-2, buttonHeight, (DRAWMODE_CIRCLE*3)+i, smallButtonImage, ITEMGROUP_BRUSH,Null, .25, 0, smallButtonReflectionImage)

		MenuItem.Create(x+(i*(buttonWidth+2)), y+120+44, buttonWidth-2, buttonHeight, i, smallButtonImage, ITEMGROUP_CONTROL)
		MenuItem.Create(x+(i*(buttonWidth+2)), y+144+44, buttonWidth-2, buttonHeight, i+3, smallButtonImage, ITEMGROUP_CONTROL, Null, .25, 0, smallButtonReflectionImage)

	Next

	GrabImage image,0,0
	UnlockImage image
	Return image


End Function

Function reflectStateInMenu()
	
	MenuItem.setSelectedItem(ITEMGROUP_ELEMENTS, selectedElement)
	MenuItem.setSelectedItem(ITEMGROUP_BRUSH, (drawMode*3) + selectedPenSize)
	If simulationRunning = False Then 
		MenuItem.setSelectedItem(ITEMGROUP_CONTROL,4)
	Else
		MenuItem.setSelectedItem(ITEMGROUP_CONTROL,5)
	End If
End Function

		
Function drawButtonLabels(i:Int, x:Int,y:Int, w:Int, h:Int)

	Local red:Int,green:Int,blue:Int
	GetColor(red,green,blue)

	' pen
	DrawRect x+(w/2)+(i*(w+2))-(penSizes[i]/2), y+24+(h/2)-(penSizes[i]/2), penSizes[i],penSizes[i]
	' line
	SetLineWidth penSizes[i]	
	DrawLine x+(w/4)+(i*(w+2)), y+48+(h-(h/4))-2, x+(w-(w/4))+(i*(w+2)), y+48+(h/4)+2
	
	
	Select i
		Case 0
		
			' load
			DrawPoly([ Float(x+8),Float(y+120+(h-12)), Float(x+(w-8)),Float(y+120+(h-12)), Float(x+(w/2)),Float(y+120+4)]) 
			DrawPoly([ Float(x+4),Float(y+120+(h-4)), Float(x+(w-4)),Float(y+120+(h-4)), Float(x+(w-6)),Float(y+120+(h-8)), Float(x+6),Float(y+120+(h-8))  ]) 
			SetColor 0,0,0
			DrawPoly([ Float(x+6),Float(y+120+(h-5)), Float(x+(w-6)),Float(y+120+(h-5)), Float(x+(w-7)),Float(y+120+(h-7)), Float(x+7),Float(y+120+(h-7))  ])
			SetColor red,green,blue
			DrawRect  x+11, y+120+(h-11), 4,5

			
			' circle
			SetScale 1+(penSizes[i]/2),1+(penSizes[i]/2)
			DrawCircle( x+(w/2)+(i*(w+2))-(penSizes[i]/2)-1, y+72+(h/2)-(penSizes[i]/2), w/4 )
			SetScale 1,1

			' new
			SetLineWidth 1
			SetColor 0,0,128
			DrawRect x+((w+2)*0)+6,y+144+5,12,10
			SetColor 225,225,225
			DrawLine x+((w+2)*0)+6,  y+144+5,  x+((w+2)*1)-10, y+144+5
			DrawLine x+((w+2)*0)+6,  y+144+15, x+((w+2)*1)-10, y+144+15
			DrawLine x+((w+2)*1)-10, y+144+5,  x+((w+2)*1)-10, y+144+15
			DrawLine x+((w+2)*0)+6,  y+144+5,  x+((w+2)*0)+6, y+144+15

		Case 1
			' save
			DrawPoly([ Float(x+w+2+4),Float(y+120+(h-4)), Float(x+w+2+(w-4)),Float(y+120+(h-4)), Float(x+w+2+(w-6)),Float(y+120+(h-8)), Float(x+w+2+6),Float(y+120+(h-8))  ]) 
			SetColor 0,0,0
			DrawPoly([ Float(x+w+2+6),Float(y+120+(h-5)), Float(x+w+2+(w-6)),Float(y+120+(h-5)), Float(x+w+2+(w-7)),Float(y+120+(h-7)), Float(x+w+2+7),Float(y+120+(h-7))  ])
			SetColor red,green,blue
			DrawRect  x+w+2+11, y+120+5, 4,5
			DrawPoly([Float(x+w+2+8),Float(y+120+11), Float(x+w+2+(w-8)),Float(y+120+11), Float(x+w+2+(w/2)),Float(y+120+(h-5))]) 


			'circle
			SetScale 1+(penSizes[i]/2),1+(penSizes[i]/2)
			DrawCircle( x+(w/2)+(i*(w+2))-(penSizes[i]/2), y+72+(h/2)-(penSizes[i]/2), w/4 )
			SetScale 1,1

			' pause
			DrawRect x+w+2+(w/4)+1, y+144+(h/4), 5,h/2
			DrawRect x+w+2+(w-(w/2))+1, y+144+(h/4), 5,h/2
		Case 2
			'circle
			SetScale 1+(penSizes[i]/2),1+(penSizes[i]/2)
			DrawCircle( x+(w/2)+(i*(w+2))-(penSizes[i]/2), y+72+(h/2)-(penSizes[i]/2), w/4 )
			SetScale 1,1
			
			' play
			DrawPoly([Float(x+w+w+2+2+(w/4)+3), Float(y+144+(h/4)), Float(x+w+w+2+2+(w/4)+3), Float(y+144+(h-(h/4))),Float( x+w+w+2+2+(w-(w/2))+6), Float(y+144+(h/2))]) 
			 
	End Select
	
		
End Function

Function DrawCircle(x:Int,y:Int,radius:Int, stepinc:Float=6.0)
	Local angle:Float = 0
	While (angle < 360.0)
		DrawRect x+(Sin(angle)*radius),y+(Cos(angle)*radius),1,1
		angle :+ stepinc 
	Wend
End Function


Function drawPreset(preset:Int)
	Select preset
		Case 1
			For Local x:Int = width/4 To width-(width/4)
				For Local y:Int = 50 To 100
					Particle.Create(selectedElement,x,y)
				Next
			Next

		Case 0
			For Local x:Int = 0 To width-1 Step 8
				For Local y:Int = 0 To height -1 Step 8
					If Rand(0,8) = 0
						For Local z:Int = 0 To 7
							Particle.Create(selectedElement,x+z,y+z)
							Particle.Create(selectedElement,x+z+1,y+z)
						Next
						'For Local z:Int = 0 To 7
						'	Particle.Create(ELEMENT_GRAPHITE,x+z,y+3)
						'Next
					Else If Rand(0,8) = 0
						For Local z:Int = 0 To 7
							Particle.Create(selectedElement,(8+x)-z,y+z)
							Particle.Create(selectedElement,(9+x)-z,y+z)
						Next
						'For Local z:Int = 0 To 7
						'	Particle.Create(ELEMENT_GRAPHITE,x,y+z)
						'Next
					End If
				Next
			Next
	End Select
End Function

Function countFps:Int()
	fpsHistory[frame Mod MAX_FPS_HISTORY] = 1000.0 / (MilliSecs() - fpstime)
	fpstime = MilliSecs()
	Local fpsTotal:Int
	For Local fps:Int = EachIn fpsHistory
		fpsTotal :+ fps
	Next
	Return fpsTotal / MAX_FPS_HISTORY
End Function

Function draw(element:Int)

	Local currentMouseX:Int = MouseX() - xOrigin 
	Local currentMouseY:Int = MouseY() - yOrigin
	Local mouseReleased:Int = False

	If MouseDown(1)

		If drag = False ' start dragging
			dragFromX = currentMouseX
			dragFromY = currentMouseY
			drag = True
		End If
	
	Else 

		If drag = True
			mouseReleased = True
			drag = False
		End If
	
	End If
	
	Local halfPenSize:Int = penSizes[selectedPenSize]/2
	SetBlend LIGHTBLEND
	SetAlpha .25 + ((frame Mod 8)/16.0)
	Select renderMode	
		Case RENDERMODE_PIXEL
			DrawRect currentMouseX+xOrigin-halfPenSize,currentMouseY+yOrigin-1-halfPenSize,penSizes[selectedPenSize],penSizes[selectedPenSize]
		Default
			DrawRect currentMouseX+xOrigin-halfPenSize-1,currentMouseY+yOrigin-1-halfPenSize,penSizes[selectedPenSize]+1,penSizes[selectedPenSize]+1
	End Select	
	
   Select drawMode

		Case DRAWMODE_LINE

			If mouseReleased = True
				Local x:Float = dragFromX
				Local y:Float = dragFromY
				Local steps:Float = Sqr((Abs(currentMouseX-x) * Abs(currentMouseX-x)) + (Abs(currentMouseY-y) * Abs(currentMouseY-y)) )
				Local xInc:Float = (currentMouseX-x)/steps
				Local yInc:Float = (currentMouseY-y)/steps
			
				For Local i:Int = 0 To steps*2
					For Local n:Int = -halfPenSize To halfPenSize
		        		Particle.Create(element,x+(yInc*n),y+(-xInc*n))
					Next
					x :+ (xInc*.5)
					y :+ (yInc*.5)
			   Next
			
			Else If drag = True
				SetBlend LIGHTBLEND
				SetAlpha .25 + ((frame Mod 8)/16.0)
				SetLineWidth penSizes[selectedPenSize]
				DrawLine dragFromX+xOrigin,dragFromY+yOrigin-1,currentMouseX+xOrigin,currentMouseY+yOrigin-1
				SetLineWidth 1
			End If
	
		Case DRAWMODE_PEN
		
			If drag = True
				Local mouseXSpeed:Float = (currentMouseX-prevMouseX)*.1
				If mouseXSpeed > 1.0 mouseXSpeed = 1.0
				If mouseXSpeed < -1.0 mouseXSpeed = -1.0
				Local mouseYSpeed:Float = (currentMouseY-prevMouseY)*.1
				If mouseYSpeed > 1.0 mouseYSpeed = 1.0
				If mouseYSpeed < -1.0 mouseYSpeed = -1.0
				
				If (currentMouseX = dragFromX) And (currentMouseY = dragFromY)

					For Local xx:Int=-halfPenSize To halfPenSize
						For Local yy:Int=-halfPenSize To halfPenSize
				        	Particle.Create(element,xx+currentMouseX,yy+currentMouseY,mouseXSpeed,mouseYSpeed) 
				   		Next
					Next
				
				Else 

					Local x:Float = dragFromX
					Local y:Float = dragFromY
					Local steps:Float = Sqr((Abs(currentMouseX-x) * Abs(currentMouseX-x)) + (Abs(currentMouseY-y) * Abs(currentMouseY-y)) )
					Local xInc:Float = (currentMouseX-x)/steps
					Local yInc:Float = (currentMouseY-y)/steps
				
					For Local i:Int = 0 To steps*2
						For Local n:Int = -halfPenSize To halfPenSize
			        		Particle.Create(element,x+(yInc*n),y+(-xInc*n),mouseXSpeed,mouseYSpeed)
						Next
						x :+ (xInc*.5)
						y :+ (yInc*.5)
				   Next

				End If
								
				dragFromX = currentMouseX
				dragFromY = currentMouseY
				
				
			End If
			
		Case DRAWMODE_CIRCLE

			If mouseReleased = True
				Local x:Float = dragFromX
				Local y:Float = dragFromY
				Local radius:Float = Sqr((Abs(currentMouseX-x) * Abs(currentMouseX-x)) + (Abs(currentMouseY-y) * Abs(currentMouseY-y)) )
				Local angleInc:Float = 32/(Abs(radius)+.0001)
				Local angle:Float = 0.0
				
				While (angle < 360.0)
				
					Local xx:Float = Sin(angle) * radius
					Local yy:Float = Cos(angle) * radius
			
					For Local xxx:Int=-halfPenSize To halfPenSize
						For Local yyy:Int=-halfPenSize To halfPenSize
				        	Particle.Create(element,x+xx+xxx+1, y+yy+yyy+1) 
				   		Next
					Next
					
					angle :+ angleInc
				
				Wend 
			
			Else If drag = True
				Local radius:Float = Sqr((Abs(currentMouseX-dragFromX) * Abs(currentMouseX-dragFromX)) + (Abs(currentMouseY-dragFromY) * Abs(currentMouseY-dragFromY)) )
				SetBlend LIGHTBLEND
				SetAlpha .25 + ((frame Mod 8)/16.0)
				SetScale penSizes[selectedPenSize],penSizes[selectedPenSize]
				DrawCircle(dragFromX+xOrigin-halfPenSize,dragFromY+yOrigin-halfPenSize,radius, Min(6.0 * (64/(Abs(radius)+.0001)),6.0) )
				SetScale 1,1
			End If

	End Select
	
	prevMouseX = currentMouseX
	prevMouseY = currentMouseY


End Function

Function initParticles()

	Particle.reset()
	
	' concrete walls
	For Local x:Int = 1 To width-1
		Particle.Create(ELEMENT_CONCRETE,x,1)
		Particle.Create(ELEMENT_CONCRETE,x,height-1)
	Next
	For Local y:Int = 1 To height-1
		Particle.Create(ELEMENT_CONCRETE,1,y)
		Particle.Create(ELEMENT_CONCRETE,width-1,y)
	Next


	
End Function

'   try and create output directory If it doesn't exist already
Function createSaveGameDir()
	Local dir:Int = ReadDir(OUTPUT_DIRECTORY)
	If Not dir Then 
		CreateDir(OUTPUT_DIRECTORY,True)
	Else
		CloseDir(dir)
	End If
End Function

Function screenshot()

	Local filename:String = OUTPUT_DIRECTORY+"/elemental_"+MilliSecs()+".jpg"
	Local screengrab:TPixmap = GrabPixmap(0,0,GraphicsWidth(),GraphicsHeight())
	SavePixmapJPeg(screengrab,filename,100)

End Function


Function savePixmap(pixmap:TPixmap)

	createSaveGameDir()
	Local filename:String = OUTPUT_DIRECTORY+"/elemental_"+MilliSecs()+".png"
	SavePixmapPNG(pixmap,filename)

End Function

Function rgbToString:String(red:Byte,green:Byte,blue:Byte)
	Return Hex( (red Shl 24) + (green Shl 16) + (blue Shl 8))
End Function


Type SavedGame

	Global savedGames:TList = CreateList()
	Field pixmap:TPixmap
	Field sprite:TSprite
	Field file:TFile
	Field deleting:Float = -1
	
	Function Create:SavedGame(file:TFile)
		Local sg:SavedGame = New SavedGame
		sg.pixmap   = PixmapWindow(LoadPixmap(file.path+"/"+file.filename),0,0,width,height)
		sg.file = file
		sg.sprite   = TSprite.Create(LoadImage(sg.pixmap, MIPMAPPEDIMAGE))
		sg.file.isLoaded = True
		ListAddLast savedGames,sg
		Return sg
	End Function
	
	Function updateAll(selectedIndex:Int, lsmooth:Float, sliding:Int)
		Local i:Int = 0
		Local deletedCount:Int = 0
		For Local sg:SavedGame = EachIn savedGames
			sg.update(i, selectedIndex, deletedCount, lsmooth, sliding)
			If sg.deleting >= 0 deletedCount :+1 
			i :+ 1
		Next
	End Function
	
	Function getSelectedSavedGame:SavedGame(selectedIndex:Int)
		Local i:Int = 0
		For Local sg:SavedGame = EachIn savedGames
			If i = selectedIndex And sg.deleting < 0
				Return sg
			End If
			If sg.deleting < 0 i :+ 1
		Next	
	End Function
	
	Function deleteAll()
		TSprite.deleteAll()
		ClearList(savedGames)
	End Function
	
	Function renderAll()
		SetBlend ALPHABLEND
		SetAlpha 1
		SetColor 255,255,255
		TSprite.renderAll()
	End Function
	
	Method update(index:Int, selectedIndex:Int, deletedCount:Int, lsmooth:Float, sliding:Int)
		' calculate x,y,z coords of all sprites
		Local smooth:Float = lsmooth 
		If sliding = False smooth = 0.0
		sprite.x = ((index-deletedCount-selectedIndex+smooth)+(deletedCount*Abs(lsmooth)))*2 
		sprite.y = 0
		sprite.z = -3.5 + (Cos((sprite.x)*22.5*.5)*2)
		
		If deleting > 0
			sprite.x = 0
			sprite.z = -1.5 - ((1.0-deleting)*4)
			deleting :- .02
			If deleting <= 0 Then
				DeleteFile(file.path +"/"+ file.filename) 
				'Print "removed " + file.path + "/" + file.filename		
				remove()
			End If 
		End If 
	End Method
	
	Method remove()
		sprite.remove()
		savedGames.remove(Self)		
	End Method	

End Type

Function selectSavedFile:TPixmap()

	FlushMouse()
	FlushKeys()
	MoveMouse(centerX,centerY)

	Local selected:TPixmap = Null
	Local files:TList = readImageDirFiles(OUTPUT_DIRECTORY)
	Local fileCount:Int = CountList(files)
	If fileCount = 0 Return selected
	SortList files
	
	For Local f:TFile = EachIn files
		SavedGame.Create(f)
	Next

	Local lsmoothX:Float = 0.0
	Local selecting:Int = True
	Local sliding:Int = False
	
	While (selecting)
		Local sg:SavedGame = SavedGame.getSelectedSavedGame(selectedImageIndex)
		Cls
		SetBlend LIGHTBLEND
		SetScale 1,1
		SetAlpha .5
		SetOrigin 0,0
		SetColor 255,255,255 
		DrawText "Select File",centerX-(TextWidth("Select File")/2),yOrigin+6
		SetAlpha 0.5-Abs(lsmoothX)
		DrawText sg.file.filename, centerX-(TextWidth(sg.file.filename)/2), (halfHeight/4)+halfHeight/2

		SetAlpha .75
		SetColor 96,96,64
		SetScale 2,.125
		DrawImage backgroundImage,centerX,(centerY-(height/16))

		SavedGame.updateAll(selectedImageIndex, lsmoothX, sliding)		
		SavedGame.renderAll()
						
		If lsmoothX <> 0.0 lsmoothX :* .92
		
		If Abs(lsmoothX) < .01
			sliding = False
		End If
		If Abs(lsmoothX) < .1
			If selectedImageIndex > 0
				If KeyHit(KEY_LEFT) Then FlushKeys() ; MoveMouse(centerX,centerY) ; selectedImageIndex :-1 ; lsmoothX = -1.0 ; sliding = True 
				If KeyHit(KEY_UP) Then FlushKeys() ; MoveMouse(centerX,centerY) ; selectedImageIndex = 0 ; lsmoothX = -1.0 ; sliding = True
				If MouseX() <= 0 Then selectedImageIndex :-1 ; lsmoothX = -1.0 ; sliding = True 
			End If
			If selectedImageIndex < fileCount-1
				If KeyHit(KEY_RIGHT) Then FlushKeys() ; MoveMouse(centerX,centerY) ; selectedImageIndex :+1 ; lsmoothX = 1.0 ; sliding = True 
				If KeyHit(KEY_DOWN) Then FlushKeys() ; MoveMouse(centerX,centerY) ; selectedImageIndex = fileCount-1 ; lsmoothX = 1.0 ; sliding = True
				If MouseX() >= GraphicsWidth()-1 Then selectedImageIndex :+1 ; lsmoothX = 1.0 ; sliding = True 
			End If
		End If

		If KeyHit(KEY_DELETE) And sg.deleting < 0 Then
			sliding = False 
			sg.deleting = 1.0
			lsmoothX = 1.0
			fileCount :- 1
			If fileCount <= 0 Then 
				selecting = False
			Else If selectedImageIndex > fileCount-1 Then 
				selectedImageIndex = fileCount-1
				lsmoothX = -1.0
				sliding = True
			End If
		End If   

		If KeyHit(KEY_ENTER) Or MouseHit(1) Then selected = sg.pixmap ; selecting = False
		If KeyHit(KEY_ESCAPE) selecting = False
		If KeyDown(KEY_LCONTROL) And KeyHit(KEY_G) Then 	screenshot()

		
		Flip
		Delay 1
	Wend

	SavedGame.deleteAll()
			
	Return selected

End Function

Function readImageDirFiles:TList(path:String)

	Local files:TList = CreateList()
    Local dirFiles:String[]
    dirFiles=LoadDir(path)

    For Local filename:String = EachIn dirFiles
      
        Local fileExtension:String = Lower(Right(filename,4))
        If fileExtension = ".png"
            ListAddLast files, TFile.Create(path,filename,FileTime(path+"/"+filename),FileSize(path+"/"+filename) )
        End If  
    Next
    
	Return files
End Function




Great job!

These are a ton of fun -- you just managed to make me lose a half-hour of my day creating huge graphite/oil structures and then burning them up.

fps is all over the place, lots of negative values as well. ? ?

Brilliant - genious - a great waste of time. Thanks for sharing the code.

My god this is absolutely mind-blowing... You have to find a way to incorporate this into a game! Very smooth on my machine too.

This thing is astounding! And so fast! I was getting
a frame rate of, like 27-30 when there were over 15,000
of those granules as well as 2 or three concrete platforms!
... and how fast it was to build...
Just WOW!

Any chance of an exe for non Bmax users?

Impressive stuff. Wish I could even comprehend what's going on in the code. :)

[link to download in first post now]

yeah nice app what i realy liked was making a huge number of graphite platforms rising to the top a wide layer at the bottom comprising of magma and then letting a flow of oil over the graphite =nice fire trials

This was fantastic fun for 10mins this morning before work. Now make it into a game :-)

Thanks everyone. I'm pleased that it appears to run ok on most peoples systems. Thanks Amon for hosting an Exe.

I've got a few ideas for how to make a game out of it, but for now, it's more of an inspirational spike to play around with some ideas. Here's my current todo list:

Elemental todo

Further Optimization

* DirectX rendering for Windows version
* Replace Floats with Ints
* Convert unchanging particles into bigger 'particle' blocks thereby reducing total particle count
* rewrite in C++ (really don't want to have to do this if I can help it)

New Functionality

Menu tools

* Cut and paste
* Load and Save
* Emitters (things that generate elements automatically)

Physics

* Gunpowder Element (explosive)
* Earth Element (neutral)
* Wood Element (burns leaving behind graphite)
* Seed Element (grows into a tree when it hits water and earth)
* Ice Element (reacts with Magma/Fire/Steam to form Water and freezes water)

A better model of energy/conductivity. Energy can be transferred between
elements changing their state when thresholds are reached. e.g. ice -> water -> steam

Game Characters

Introduction of complex entities that can interact with the simulation. e.g. fish, stick people, slugs etc..

I tried the exe - very nice and fast and simple to control.

I've seen some other falling sands games but they don't seem to have the detail / interaction you've implemented. Looking forward to seeing this made into a game.

Stevie

Good job!

* Replace Floats with Ints

I thought Floats were faster than Ints?

Hey looks cool! Great framerate too (>100)

I thought Floats were faster than Ints?
No way (make a little test), unless you are trying to use an Int as a fake float and having to do fancy maths on it. Related: What I also found out a while back was that floats and doubles run at the same speed, so I have no worries about using doubles when precision required it. Also Bytes are no faster than Ints because we have 32-bit processors now.

@Stevie
Thanks. Still a long way to go to catch up with the dan-ball version however ;)

@Tachyon

You may well be correct. I think I'll have to do some testing to see if it does indeed speed it up at all. There seems to be a big penalty incurred when mixing types up (casting must be expensive).

e.g.

SuperStrict

Const iterations:Int = 1000000
Local i:Int, ms:Int

Local foo:Int= 0
Local baz:Int = 8
Local goo:Float = 0.0
Local bar:Float = 0.125


foo = 0
ms = MilliSecs()
For i = 0 To iterations
	foo :+ bar
Next
Print "Adding float to int took "+(MilliSecs() - ms) +" ms"

foo = 0
ms = MilliSecs()
For  i = 0 To iterations
	foo :+ baz
Next
Print "Adding int to int took "+(MilliSecs() - ms) +" ms"

goo = 0.0
ms = MilliSecs()
For  i = 0 To iterations
	goo :+ bar
Next
Print "Adding float to float took "+(MilliSecs() - ms) +" ms"

foo = 2000000
ms = MilliSecs()
For i = 0 To iterations
	foo :* bar
Next
Print "Multiplying int  with float took "+(MilliSecs() - ms) +" ms"

goo = 2000000.0
ms = MilliSecs()
For i = 0 To iterations
	goo :* bar
Next
Print "Multiplying float  with float took "+(MilliSecs() - ms) +" ms"

foo = 2000000
ms = MilliSecs()
For  i = 0 To iterations
	foo :/ baz
Next

Print "Dividing int by int took "+(MilliSecs() - ms) +" ms"



Adding float to int took 463 ms
Adding int to int took 3 ms
Adding float to float took 5 ms
Multiplying int with float took 471 ms
Multiplying float with float took 4 ms
Dividing int by int took 17 ms

You those casting times are horrible and worth bearing in mind! Probably worth making everything floats just to avoid that even though there is a small speed loss on general float to float operations. Hey that test could do with multiplying int by int as a comparison and also the two missing dividing by tests. Sorry for going off topic btw...

This is cool.

Thank you making me waste an hour of my time, which I could've used for something productive like... working.

;)

Cool. I'd love to see this evolve further.

Just wasted an hour of my time, good stuff:)

Definately good fun. My fave being filling a huge V shape made of Graphite with oil then tickling it with a small bit of fire at the bottom. Lovely lovely!

Is there a binary I could try out ?

Windows Exe link is in Amons post above.

When I've made a bit more progress, I'll put binaries for all platforms up on my website.

brilliant fun.... very enjoyable.

It's good fun, I do get some strange graphic glitches with the exe though.

My one beef (and this is being picky) is that the water doesn't work properly, you can't make a U bend, it fills up like sand and doesn't push the water out of the other end.

I have to post my FSG engine after this =D Much simpler.
Using only simple math and one (!) array, it has the same approximate FPS. Use the mouse wheel to scroll through the different elements. Click to place element, right click to remove.

Yay for simple yet unrealistic physics!

Global gw=320,gh=240,xx,yy

SeedRnd MilliSecs()

Dim px(gw,gh)
;0 is empty, 1 is water, 2 is wall, 3 is salt, 4 is saltwater, 5 is fire, 6 is oil, 7 is sand, 8 is plant

Graphics gw,gh,0,1
AppTitle "BlitzSand 0.5 alpha"
SetBuffer BackBuffer()
HidePointer()

Global fps%
Global fpstime#

Global drawing=1


Function draw(what)
	For xx=-2 To 2
	For yy=-2 To 2
		x=xx+MouseX()
		y=yy+MouseY()
		If x>1 And x<gw-1 And y>1 And y<gh-1 Then px(MouseX()+xx,MouseY()+yy)=what
	Next
	Next
End Function

Flip
Repeat
	Cls
	drawing=MouseZ()+1
	If drawing<0 drawing=0
	If drawing>10 drawing=10
	LockBuffer()
	
		If MouseDown(1)
			draw drawing
		EndIf
		If MouseDown(2)
			draw 0
		EndIf
		If MouseDown(3)
			draw 2
		EndIf
		
		For xx=0 To gw
		For yy=0 To gh
			If Not px(xx,yy)=0

					;fall
					If gravity(px(xx,yy))>0 And Rand(0,gravity(px(xx,yy)))=0 Then
						here=px(xx,yy)
						If yy+1<gh Then
							there=px(xx,yy+1)
							If density(here)>density(there) And gravity(there)>0 Then
								px(xx,yy)=there
								px(xx,yy+1)=here
							EndIf
						
						EndIf
					EndIf
					;negative gravity
					If gravity(px(xx,yy))<0 And Rand(gravity(px(xx,yy)),0)=0 Then
						here=px(xx,yy)
						If yy-1>0 Then
							there=px(xx,yy-1)
							If gravity(there)>0 Then
								px(xx,yy)=there
								px(xx,yy-1)=here
							EndIf
						
						EndIf
					EndIf

					;slip
					If Rand(0,slip(px(xx,yy)))=0 And slip(px(xx,yy))>0 Then
						here=px(xx,yy)
						slip=Rand(-1,1)
						If xx=<1 Then slip=-1
						If xx=>gw-1 Then slip=1
						there=px(xx+slip,yy)
						If gravity(there)>0 Then
							px(xx,yy)=there
							px(xx+slip,yy)=here
						EndIf
					EndIf
			
			;draw the element and react with others		
			Select px(xx,yy)
				Case 1
					WritePixelFast xx,yy,$208AEE
						For ax=xx-1 To xx+1 
						For ay=yy-1 To yy+1
						If ax<>0 And ay<>0 ;avoid reacting with itself
							If px(ax,ay)=3 px(xx,yy)=4:px(ax,ay)=0
						EndIf
						Next
						Next 
				Case 2
					WritePixelFast xx,yy,$808080
				Case 3
					WritePixelFast xx,yy,$F0F0F0
				Case 4
					WritePixelFast xx,yy,$041090
						For ax=xx-1 To xx+1 
						For ay=yy-1 To yy+1
						If ax<>0 And ay<>0 ;avoid reacting with itself
							If px(ax,ay)=5 px(xx,yy)=3:px(ax,ay)=1
						EndIf
						Next
						Next 
				Case 5
					WritePixelFast xx,yy,IntColor(Rand(210,255),Rand(130,150),20)
						For ax=xx-1 To xx+1 
						For ay=yy-1 To yy+1
						If ax<>0 And ay<>0 ;avoid reacting with itself
							If px(ax,ay)=1 px(xx,yy)=0:px(ax,ay)=1
						EndIf
						Next
						Next 
						For ax=xx-1 To xx+1 
						For ay=yy-1 To yy+1
						If ax<>0 And ay<>0 ;avoid reacting with itself
							If px(ax,ay)=0 And Rand(0,10)=0 Then px(xx,yy)=0:px(ax,ay)=0
						EndIf
						Next
						Next 
				Case 6
					WritePixelFast xx,yy,$750A02
						For ax=xx-1 To xx+1 
						For ay=yy-1 To yy+1
						If ax<>0 And ay<>0 ;avoid reacting with itself
							If px(ax,ay)=5 And Rand(0,2)>0 px(xx,yy)=5:px(ax,ay)=5
						EndIf
						Next
						Next 
				Case 7
					WritePixelFast xx,yy,$EAE240
						For ax=xx-1 To xx+1 
						For ay=yy-1 To yy+1
						If ax<>0 And ay<>0 ;avoid reacting with itself
							If px(ax,ay)=9 And Rand(0,2)=0 px(xx,yy)=7:px(ax,ay)=0
						EndIf
						Next
						Next 
				Case 8
						For ax=xx-1 To xx+1 
						For ay=yy-1 To yy+1
						If ax<>0 And ay<>0 ;avoid reacting with itself
							If px(ax,ay)=5 And Rand(0,2)=0 px(xx,yy)=5:px(ax,ay)=5
						EndIf
						Next
						Next
						For ax=xx-1 To xx+1 
						For ay=yy-1 To yy+1
						If ax<>0 And ay<>0 ;avoid reacting with itself
							If px(ax,ay)=1 And Rand(0,3)=0 px(xx,yy)=8:px(ax,ay)=8
						EndIf
						Next
						Next 
					WritePixelFast xx,yy,$10F108
				Case 9
					WritePixelFast xx,yy,$1090FF
						For ax=xx-1 To xx+1 
						For ay=yy-1 To yy+1
						If ax<>0 And ay<>0 ;avoid reacting with itself
							If px(ax,ay)=0 And Rand(0,3)=0 px(xx,yy)=9:px(ax,ay)=1
						EndIf
						Next
						Next
				Case 10
					WritePixelFast xx,yy,$906010
						For ax=xx-1 To xx+1 
						For ay=yy-1 To yy+1
						If ax<>0 And ay<>0 ;avoid reacting with itself
							If px(ax,ay)=0 And Rand(0,3)=0 px(xx,yy)=10:px(ax,ay)=5
						EndIf
						Next
						Next
			End Select
			
			EndIf
		Next
		Next
	
		WritePixelFast MouseX(),MouseY(),$FFFFFF

	UnlockBuffer()

	Color 255,255,255
	Text 12,12, "FPS: " + FPS
	Text 12,26,"Drawing: "+drawing
	CountFPS()
	
	If KeyDown(1) End
	Flip
Forever



Function react(e1,e2,e3,e4)

End Function

Function density(element)
	Select element
		Case 0 d=1
		Case 1 d=3
		Case 2 d=256
		Case 3 d=3
		Case 4 d=4
		Case 5 d=-4
		Case 6 d=2
		Case 7 d=5
		Case 8 d=256
		Case 9 d=256
		Case 10 d=256
	End Select
	Return d
End Function

Function gravity(element)
	Select element
		Case 0 d=1
		Case 1 d=2
		Case 2 d=0
		Case 3 d=2
		Case 4 d=2
		Case 5 d=-1
		Case 6 d=1
		Case 7 d=2
		Case 8 d=0
		Case 9 d=0
		Case 10 d=0
	End Select
	Return d
End Function

Function slip(element)
	Select element
		Case 0 d=1
		Case 1 d=3
		Case 2 d=0
		Case 3 d=2
		Case 4 d=4
		Case 5 d=2
		Case 6 d=2
		Case 7 d=1
		Case 8 d=0
		Case 9 d=0
		Case 10 d=0
	End Select
	Return d
End Function


Function CountFPS()
	fps = 1000.0 / (MilliSecs() - fpstime)
	fpstime = MilliSecs()
End Function


;;;; only here for use in future for rgb to hex for writepixel functions
Function IntColor(R,G,B,A=255)
	Return A Shl 24 Or R Shl 16 Or G Shl 8 Or B Shl 0
End Function


I think this is the coolest shared app I have seen on here. Extremely awsome particles effects, and so fast!

Thanks for sharing Tesuji

This is very good!

Oh and in the initial state i do get 19/20 fps on my Mini (G4 1.25Ghz).

Thanks for all the feedback.

I've added a new version above with some links to compiled binaries. Had to remove v 0.1 source due to maximum size of post limit. Windows version was compiled on XP so I don't know how it'll fair with vista.

v 0.2

Some minor physics speed optimizations.
Added circle tool.
Pen tool doesn't leave gaps anymore.
Added Load/Save/Delete
Nicer GUI buttons
Added 'Wood' Element (Burns)
Added 'Acid' Element (Melts just about everything)
Added 'Seed' Element (Grows into wood when in contact with Sand + Water)
Added 'Gunpowder' Element (Explodes into fire missiles)

Some dev key shortcuts
r - cycle through rendering modes
v - toggle vertical sync
F1 - reset
F2..F3 - presets
F12 - Revert to last loaded/saved image
Left CTRL - Display zoom (Seems to crash my old win xp laptop. Maybe OGL driver issues).
CTRL + o - Open image
CTRL + s - Save image

Cool, thanks for the update.

I also noticed that there already is an iPhone version around and was pretty much surprised of the speed it's running at.

-> http://revver.com/video/649900/evolution-rgb/

It's great to see someone working on this! I think you've done a great job so far. It does seem to me though, that what you are doing is much more "Sand Game" than "Powder Game". The first button I looked for in your game was "Fan" :-)

Powder Game looks like it uses a different physics algorithm entirely - based on fluid dynamics by the look of it.

I'm guessing you've done as I have and googled away for a hint of what's going on only to come up with nothing?

For me, the Powder Game dynamics are where it's really at. I'm sure the code is out there, I just don't know the right terms to search for.

Any ideas anyone?

Yeah, I agree that the Powder Game dynamics set the current benchmark for this sort of simulation.

I found this interesting PDF when doing a bit of research into how the powder game ticks : http://www.dgp.toronto.edu/people/stam/reality/Research/pdf/GDC03.pdf

I imagine the powder game started out as a bog standard sand game before it evolved into what it is today. I would really like to add fluid dynamics/wind etc to elemental at some point but wanted to get the basics down first ;)

Good info there, some amazing stuff really.

Great stuff.

One problem I've encountered is when I hit the pause button, I get a magnified version of the area around the cursor to the right of the main area (I'm running widescreen 1440x900) then the program bombs out back to windows.

Same as Geehawk, running vista here.

I get the same crash on my old laptop running XP. Thought it was just my flakey OpenGL drivers but I guess not. If you hold down left CTRL it should do the same thing. It's seems to be caused by my displayZoom() function using grabimage. Somehow it interferes with the open gl pixel plot.

I've got a workaround (i.e. not using grabimage) that should be finished at the weekend.

Thanks for letting me know.

Tried it too and this is working great and really fast, at least 100 fps on a notebook. I like it :-)

Keep on the good work tesuji!

@tesuji - Great! That paper you found is exactly what's needed.

A little further digging revealed the entire demo source as a C project.

http://www.dgp.toronto.edu/people/stam/reality/Research/zip/CDROM_GDC03.zip

All that's required now is some kind of port/max interface :-)

Nice find DavidDC

Here's a blitzmax fluid dynamics test using the code you dug up. It was amazingly easy to integrate it. Might come in useful for all sorts of applications.

Windows exe http://minimono.net/downloads/elemental/elemental-windows/fluidTest.exe

Source

Note - save both files into the same directory :

save as fluidTest.bmx
' Fluid dynamics test
' Original C code written by Jos Stam (jstam@...)

SuperStrict

Framework BRL.blitz
Import BRL.glmax2d
Import BRL.max2d
Import BRL.retro

Import "solver.c"

Extern
	Function dens_step(N:Int, x:Float Ptr, x0:Float Ptr, u:Float Ptr, v:Float Ptr, diff:Float, dt:Float)
	Function vel_step(N:Int, u:Float Ptr, v:Float Ptr, u0:Float Ptr, v0:Float Ptr, visc:Float, dt:Float)
End Extern

SetGraphicsDriver GLMax2DDriver()
Graphics 800,600,32,0

Const gridSize:Int = 96
Const diffusion:Float = 0.0
Const deltaTime:Float = 0.1
Const viscosity:Float = .0001
Const force:Float = .2
Const source:Float = 4.0

Global bytesize:Int = (gridSize+2)*(gridSize+2)
Global densities:Float[byteSize]
Global densitiesPtr:Float Ptr = densities
Global xVelocities:Float[byteSize]
Global xVelocitiesPtr:Float Ptr = xVelocities
Global yVelocities:Float[byteSize]
Global yVelocitiesPtr:Float Ptr = yVelocities

Global densitiesPrevious:Float[byteSize]
Global densitiesPreviousPtr:Float Ptr = densitiesPrevious
Global xVelocitiesPrevious:Float[byteSize]
Global xVelocitiesPreviousPtr:Float Ptr = xVelocitiesPrevious
Global yVelocitiesPrevious:Float[byteSize]
Global yVelocitiesPreviousPtr:Float Ptr = yVelocitiesPrevious

Global omx:Int = GraphicsWidth()/2
Global omy:Int = GraphicsHeight()/2

' mainloop
While Not KeyHit(KEY_ESCAPE)

	For Local i:Int = 0 To (byteSize)-1
		densitiesPrevious[i] = 0.0
		xVelocitiesPrevious[i] = 0.0
		yVelocitiesPrevious[i] = 0.0
	Next
	
	userInput()
	vel_step(gridSize, xVelocitiesPtr, yVelocitiesPtr, xVelocitiesPreviousPtr, yVelocitiesPreviousPtr, viscosity, deltaTime)
	dens_step(gridSize, densitiesPtr, densitiesPreviousPtr, xVelocitiesPtr, yVelocitiesPtr, diffusion, deltaTime )
	renderDensity()
			
	Flip

Wend

End

Function userInput()

	Local x:Int = 0+((MouseX()/Float(GraphicsWidth()))*(gridSize-4)) 
	Local y:Int = 0+((MouseY()/Float(GraphicsHeight()))*(gridSize-4))
	Local offset:Int = x+(gridSize+2)*y

	If offset < byteSize
		If MouseDown(1)
			densities[offset] = source
		Else If KeyDown(KEY_SPACE)
			densities[offset] = -source
		End If
		xVelocities[offset] = force * (MouseX()-omx)
		yVelocities[offset] = force * (MouseY()-omy)
	End If
		
	omx = MouseX()
	omy = MouseY()
	
End Function

Function renderDensity()

	Local w:Float = GraphicsWidth()/gridSize+1
	Local h:Float = GraphicsHeight()/gridSize+1
	
	glBegin(GL_QUADS)

	For Local y:Int = 0 To gridSize-1
		Local yy:Float = (y-0.5)*h
		Local yoffset:Int = (gridSize+2)*y
		For Local x:Int = 0 To gridSize-1
			Local xx:Float = (x-0.5)*w 
	
			Local d00:Float = densities[yoffset+x]
			Local d01:Float = densities[yoffset+x+gridSize+2]
			Local d10:Float = densities[yoffset+x+1]
			Local d11:Float = densities[yoffset+x+1+gridSize+2]

			glColor3f(d00,d00,d00*2.0) ; glVertex2f(xx,yy)
			glColor3f(d10,d10,d10*2.0) ; glVertex2f(xx+w,yy)
			glColor3f(d11,d11,d11*2.0) ; glVertex2f(xx+w,yy+h)
			glColor3f(d01,d01,d01*2.0) ; glVertex2f(xx,yy+h)
			
		Next	
	Next

	glEnd()	

End Function



save as solver.c
#define IX(i,j) ((i)+(N+2)*(j))
#define SWAP(x0,x) {float * tmp=x0;x0=x;x=tmp;}
#define FOR_EACH_CELL for ( i=1 ; i<=N ; i++ ) { for ( j=1 ; j<=N ; j++ ) {
#define END_FOR }}

void add_source ( int N, float * x, float * s, float dt )
{
	int i, size=(N+2)*(N+2);
	for ( i=0 ; i<size ; i++ ) x[i] += dt*s[i];
}

void set_bnd ( int N, int b, float * x )
{
	int i;

	for ( i=1 ; i<=N ; i++ ) {
		x[IX(0  ,i)] = b==1 ? -x[IX(1,i)] : x[IX(1,i)];
		x[IX(N+1,i)] = b==1 ? -x[IX(N,i)] : x[IX(N,i)];
		x[IX(i,0  )] = b==2 ? -x[IX(i,1)] : x[IX(i,1)];
		x[IX(i,N+1)] = b==2 ? -x[IX(i,N)] : x[IX(i,N)];
	}
	x[IX(0  ,0  )] = 0.5f*(x[IX(1,0  )]+x[IX(0  ,1)]);
	x[IX(0  ,N+1)] = 0.5f*(x[IX(1,N+1)]+x[IX(0  ,N)]);
	x[IX(N+1,0  )] = 0.5f*(x[IX(N,0  )]+x[IX(N+1,1)]);
	x[IX(N+1,N+1)] = 0.5f*(x[IX(N,N+1)]+x[IX(N+1,N)]);
}

void lin_solve ( int N, int b, float * x, float * x0, float a, float c )
{
	int i, j, k;

	for ( k=0 ; k<20 ; k++ ) {
		FOR_EACH_CELL
			x[IX(i,j)] = (x0[IX(i,j)] + a*(x[IX(i-1,j)]+x[IX(i+1,j)]+x[IX(i,j-1)]+x[IX(i,j+1)]))/c;
		END_FOR
		set_bnd ( N, b, x );
	}
}

void diffuse ( int N, int b, float * x, float * x0, float diff, float dt )
{
	float a=dt*diff*N*N;
	lin_solve ( N, b, x, x0, a, 1+4*a );
}

void advect ( int N, int b, float * d, float * d0, float * u, float * v, float dt )
{
	int i, j, i0, j0, i1, j1;
	float x, y, s0, t0, s1, t1, dt0;

	dt0 = dt*N;
	FOR_EACH_CELL
		x = i-dt0*u[IX(i,j)]; y = j-dt0*v[IX(i,j)];
		if (x<0.5f) x=0.5f; if (x>N+0.5f) x=N+0.5f; i0=(int)x; i1=i0+1;
		if (y<0.5f) y=0.5f; if (y>N+0.5f) y=N+0.5f; j0=(int)y; j1=j0+1;
		s1 = x-i0; s0 = 1-s1; t1 = y-j0; t0 = 1-t1;
		d[IX(i,j)] = s0*(t0*d0[IX(i0,j0)]+t1*d0[IX(i0,j1)])+
					 s1*(t0*d0[IX(i1,j0)]+t1*d0[IX(i1,j1)]);
	END_FOR
	set_bnd ( N, b, d );
}

void project ( int N, float * u, float * v, float * p, float * div )
{
	int i, j;

	FOR_EACH_CELL
		div[IX(i,j)] = -0.5f*(u[IX(i+1,j)]-u[IX(i-1,j)]+v[IX(i,j+1)]-v[IX(i,j-1)])/N;
		p[IX(i,j)] = 0;
	END_FOR	
	set_bnd ( N, 0, div ); set_bnd ( N, 0, p );

	lin_solve ( N, 0, p, div, 1, 4 );

	FOR_EACH_CELL
		u[IX(i,j)] -= 0.5f*N*(p[IX(i+1,j)]-p[IX(i-1,j)]);
		v[IX(i,j)] -= 0.5f*N*(p[IX(i,j+1)]-p[IX(i,j-1)]);
	END_FOR
	set_bnd ( N, 1, u ); set_bnd ( N, 2, v );
}

void dens_step ( int N, float * x, float * x0, float * u, float * v, float diff, float dt )
{
	add_source ( N, x, x0, dt );
	SWAP ( x0, x ); diffuse ( N, 0, x, x0, diff, dt );
	SWAP ( x0, x ); advect ( N, 0, x, x0, u, v, dt );
}

void vel_step ( int N, float * u, float * v, float * u0, float * v0, float visc, float dt )
{
	add_source ( N, u, u0, dt ); add_source ( N, v, v0, dt );
	SWAP ( u0, u ); diffuse ( N, 1, u, u0, visc, dt );
	SWAP ( v0, v ); diffuse ( N, 2, v, v0, visc, dt );
	project ( N, u, v, u0, v0 );
	SWAP ( u0, u ); SWAP ( v0, v );
	advect ( N, 1, u, u0, u0, v0, dt ); advect ( N, 2, v, v0, u0, v0, dt );
	project ( N, u, v, u0, v0 );
}


@tesuji - Ha ha! You legend - this is awesome. Well done! Now we're definitely getting somewhere! :-)

The newer version crashes with an "Unhandled Exception: Attempt to access field or method of Null object" whenever I attempt to put some seed on top of water and sand.

Thanks for spotting that one Torrente. I don't run in debug mode often enough so it was failing silently here. I've just fixed it in the above source and will try and put up some new binaries tomorrow.

New version is up.

* Zoom box issue should now be fixed.
* Fixed seed exception.
* Added grass.

Next thing I want to do is add wind so it may be a little while before the next update...

You are doing some really fantastic stuff here tesuji

The fluid test is nice!

Clever and interesting. Keep exploring the possibilities...

V0.3: 100 - 500 fps (NVIDIA GeForce 8800 GTS 640mb, Intel Core2Quad Q6600 2.4Ghz, 2GB RAM)

Waw very promising simulation over here !
Really good work !
Thanks for the sources :)

Looks good but the source did not compile on my mac and the exe crashed after some time.

Just keeps getting better. I like building a contraption to rain seeds and water to grow a forest. Then of course I have to burn it down with gun powder!

I have to buy a faster PC!

I found a bug.

When the acid eats through the side of the screen on the right hand side, it crashes and quits.

@taumel - What version of BlitzMax are you using? I'm compiling ok on a powerbook G4 with version 1.28.

@andy_mc - Thanks. I guess it's the acid eating its way through memory once it escapes the confines of the box. Not managed to replicate it locally yet but I'll keep trying. Most likely to be one of those pesky array index violations.

1.28 on a G4 (Mini) as well.

people, post some of your best starting pictures for this, I'm having problems getting trees to grow well. I use sand and water then drop some seeds on but nothing much happens.

They grow on sand? Not tried that.

Make a flat bottomed bowl of wood. Then make two concrete cones well above it, one about an inch above the other. Fill the top one with seed and the bottom one with water, then using the thinnest line make a load of vertical holes that go through both cones. Voila- forest.

Added 'Seed' Element (Grows into wood when in contact with Sand + Water)


Seed grows without sand?

Seed grows when in contact with sand+water or wood+water. Sometimes seeds don't sprout if you've sprinkled too many as elements tend to separate from each other.

Here's a png that you can try out if you save it into your images directory (located in the same dir where you run elemental from) and load it up.



BTW, is anyone else having compilation issues? (trying to help taumel).

Btw my compilation errors were due to that BlitzMax said that certain variables have already been declared, if i remember correctly this happend in if else if structures...just had a quick try after the exe boomed here (could have been because of the acid here too).

Hmmm, could it be cut and paste is somehow messing up the SuperStrict declaration? Maybe try pasting to a plain text editor first and then re-paste it into max ide?

I've got a fix for the acid leak issue (it was a index out of bounds) which I'll try and upload asap.

Yes this fixed it. Sorry that i didn't had a look at it on my own but i just had a quick try at it...

can someone upload some more example pics, I've rubbish at making my own.

this is awesome! You lost me hours of a day playing the other sand game... unfortunately I don't like yours as much as the java one yet but you're getting there, especially with the full screen thing so you can have more room. You're on the way to making a great game, possibly a "real" game :P
here's an idea for a "campaign" thing...
So the user selects "campaign" or missions, whatever you wanna call it. It brings up a list of course, the first few being tutorials on what elements do when mixed together, but it just demonstrates one combination so there's room for testing on the user's part. Then you have missions, where you have objects/shapes made out of blocks, and the user has to do something like make a flamethrower or something. I can see how hard it could be to do this, you would have to check that they are mixing the elements inside the gun, that the elements are the right ones, that the elements are spraying out, and that they're burning up before touching the ground. You could also have shapes/objects made out of something that can be burned up, and your goal is to complete the objective WITHOUT harming the object. For these missions block would be disabled. Thanks for listening to my rambling! :lol:

Weird graphic glitches on my Vista widescreen laptop.
Oh well. It looked like fun.

Meanwhile back on a stable-ish OS (XP Home).... my two children think:



you could make a little concrete box that has gun powder in, make some wood stick lane, put some fire on the wood then wait until the fuse reaches the gun powder and there is a boom effect.

It is really fab because you can like make a beach by putting some sand on the bottom of the page and then put the water on top.




No glitches here, numerous effects on screen at once dropped the frame rate to about 6fps, (I've a 1.8Ghz athlon), but very impressive game play. Sheer genius. a WIP that is nearing fruition in my book!

I figured it out. :0
No OpenGL on my system.
you used that,right?
All of the openGL programs dont work.