Skip to content

Instantly share code, notes, and snippets.

@bcatcho
Created October 13, 2012 17:30
Show Gist options
  • Save bcatcho/3885466 to your computer and use it in GitHub Desktop.
Save bcatcho/3885466 to your computer and use it in GitHub Desktop.
Dramatically changing how water flow works to based off of pressure, volume, mass, density... it's in a half working state
--# Scratch
-- TRiver
-- Algorithm for flowing water:
-- Terms:
-- P = flow power. 2 power == 2 block slices
-- N = a river node
-- n.wl = water level for a node
-- n.wgl = water or ground level
-- Invariant:
-- 1. Water seeks lowest ground.
-- 2. Water will to be level.
-- 3. Water will not move without force (eg gravity or pump)
-- 4. A Water node can only have 1 parent and 4 children (north south east west)
--[[
N = root node
end_node_list = {} -- list of nodes that recieved water
Flow(N,2)
Flow( n, p, end_node_list )
wl' = n.wl + p
while wl' is > n_nwl, n_s.wgl, n_e.wl, n_w.wl do
n_low = child with lowest water/ground level
temp_list = Flow(n_low, 1)
if end_node.wl + 1 > n_low.wl then
end_node_list.remove(end
end
end --while
]]--
--# StateSimulating
StateSimulating = class(State)
function StateSimulating:initState()
self.name = "Simulating"
self.lastAnimTime = ElapsedTime
end
function StateSimulating:draw()
-- Codea does not automatically call this method
local map, session = self:shared().map, self:shared().session
map:draw()
if ElapsedTime > self.lastAnimTime +.3 then
self.lastAnimTime = ElapsedTime
if map:simulate() == false then
print(session:powerLeft().. " power generated!" )
self:goState(StatePlayerPlaying())-- self.map, self.session ))
else
session:storePower(1)
end
end
end
function StateSimulating:touched(touch)
-- Codea does not automatically call this method
end
--# StatePlayerPlaying
StatePlayerPlaying = class(State)
function StatePlayerPlaying:initState()
self.name = "PlayerPlaying"
end
function StatePlayerPlaying:draw()
self:shared().map:draw()
end
function StatePlayerPlaying:touched(touch)
if touch.state == ENDED and self:shared().session:powerLeft() > 0 then
if self:shared().map:touched(touch) then
-- if the touch actually does something to a block
self:shared().session:usePower(1)
end
self:tryToEndRound()
end
end
function StatePlayerPlaying:tryToEndRound()
if self:shared().session:powerLeft() == 0 then
self:goState( StateSimulating())--self.map, self.session) )
end
end
--# StateActorPlacement
StateActorPlacement = class(State)
function StateActorPlacement:initState(actorName)
self.name = "ActorPlacement"
self.actorName = actorName
self.actor = nil
self.touchDiffX = 0
self.touchDiffY = 0
end
function StateActorPlacement:enter()
-- because self:shared is not available until enterState
self.actor = self:getPlaceableActor(self.actorName)(3, 5, self:shared().map.blockDimensions)
end
function StateActorPlacement:draw()
-- Codea does not automatically call this method
self:shared().map:draw()
self.actor:draw()
end
function StateActorPlacement:touched(touch)
if touch.state == BEGAN then
-- the offset is so that you don't move the sprite to the top or left
-- by moving a pixel to the top or left respectively
self.ox = self.actor.x + (self.actor.w/2.0)
self.oy = self.actor.y + (self.actor.w-9)/2.0
self.actor:setCustomDrawStyle(draggingSpriteDrawStyle)
elseif touch.state == MOVING then
self.ox = self.ox + touch.deltaX
self.oy = self.oy + touch.deltaY
self.actor:setPosWithGridConstraint(self.ox, self.oy)
elseif touch.state == ENDED then
self.actor:resetCustomDrawStyle()
if touch.tapCount == 2 then
print("Placed "..self.actor.name)
self:shared().map.world:get(self.actor.gx, self.actor.gy):setObject(self.actor)
self:goState(StatePlayerPlaying())-- self.map, self.session ))
end
end
end
function draggingSpriteDrawStyle(actor)
-- scale, translate, alpha
tint(255, 255, 255, 181)
local s = 1.2
local x = actor.x - ((actor.w * 1.2) - actor.w)/2.0
local y = actor.y - (((actor.w - 9) * 1.2) - (actor.w - 9))/2.0
sprite(actor.spriteName, x, y, actor.w*1.2, actor.h*1.2)
end
function StateActorPlacement:getPlaceableActor(actorName)
local placeableActors = {
wheel = ActorWheel
}
return placeableActors[actorName]
end
--# BlockActor
BlockActor = class()
function BlockActor:init(gx,gy, blockDim)
self.z = 3
self.maxGx = blockDim.maxGx
self.maxGy = blockDim.maxGy
self.w = blockDim.w
self.h = blockDim.h
self.x, self.y = nil
-- TODO the 19 offset for bounds is really weird. not sure why this works.
-- need to play around with these values
self.bounds = { w = self.w, h = self.h - 19, x = nil, y = nil}
self:setGridXY(gx, gy)
self.lasttouchid = nil
self.spriteName = "Planet Cute:Grass Block"
self:actorInit()
self.drawSprite = nil
self:resetCustomDrawStyle()
end
function BlockActor:setCustomDrawStyle(method)
self.drawSprite = function(actor)
pushStyle()
method(actor)
popStyle()
end
end
function BlockActor:resetCustomDrawStyle()
self.drawSprite = function(actor)
sprite(actor.spriteName, actor.x, actor.y, actor.w, actor.h)
end
end
function BlockActor:draw()
self.drawSprite(self)
if debug_sprites then
pushStyle()
noFill()
strokeWidth(2)
stroke(64, 172, 205, 211)
rectMode(CORNER)
rect(self.bounds.x, self.bounds.y, self.bounds.w, self.bounds.h)
popStyle()
end
end
function BlockActor:setGridXY(gx, gy)
local xy = self:getGridXY(gx, gy)
self.gx = gx
self.gy = gy
self.x = xy.x
self.y = xy.y
self.bounds.x = self.x
self.bounds.y = self.y
self:setZ( self.z )
end
function BlockActor:getGridXY(gx, gy)
return {
x = (gx - 1) * self.w,
y = (gy* (self.w-9)) - self.w
}
end
function BlockActor:setPosWithGridConstraint(x, y)
self.gx = math.floor((x/self.w) + 1)
self.gy = math.floor((y+self.w)/(self.w-9))
self.gx = math.max(math.min(self.maxGx, self.gx), 1)
self.gy = math.max(math.min(self.maxGy, self.gy), 1)
self:setGridXY(self.gx, self.gy)
end
function BlockActor:getYforZ( z )
local xy = self:getGridXY( self.gx, self.gy)
local z2yMap = { -13, -10, -5, 0, 5 }
return xy.y + z2yMap[z + 1] -- lua arrays start at 1
end
function BlockActor:setZ( z )
self.z = z
self.y = self:getYforZ(z)
self.bounds.y = self.y
end
function BlockActor:touched(touch)
return ((touch.x >self.bounds.x
and touch.x < self.bounds.x + self.bounds.w
and touch.y > self.bounds.y
and touch.y < self.bounds.y + self.bounds.h)
and self.lasttouchid ~= touch.id
and self:touchedCallback(touch))
end
-- returns true/false and meant to be overridden
function BlockActor:touchedCallback(touch)
return true
end
--# ActorWheel
ActorWheel = class(BlockActor)
function ActorWheel:actorInit()
self.name = "wheel"
self.spriteName = "Planet Cute:Chest Lid"
self:setZ( 4 )
end
function ActorWheel:touched(touch)
-- Codea does not automatically call this method
end
--# WorldBlock
WorldBlock = class()
-- todo: change to order comps by z. this would simplify things
function WorldBlock:init(x, y)
self.x, self.y = x,y
self.comps = { object = nil, water = nil, terrain = nil }
end
function WorldBlock:doCompsB2T(method, ...)
local o, t, w = self.comps.object, self.comps.terrain, self.comps.water
if t then t[method](t, unpack(arg)) end
if w then w[method](w, unpack(arg)) end
if o then o[method](o, unpack(arg)) end
end
function WorldBlock:doComps(method, ...)
local o, t, w = self.comps.object, self.comps.terrain, self.comps.water
if o then o[method](o, unpack(arg)) end
if w then w[method](w, unpack(arg)) end
if t then t[method](t, unpack(arg)) end
end
function WorldBlock:doCompsBlock(method, ...)
local o, t, w = self.comps.object, self.comps.terrain, self.comps.water
if o then return o[method](o, unpack(arg)) end
if w then return w[method](w, unpack(arg)) end
if t then return t[method](t, unpack(arg)) end
return nil
end
-- setters to ensure precedence when invoking doComponent
function WorldBlock:setObject(object)
self.comps.object = object
end
function WorldBlock:setWater(water)
self.comps.water = water
end
function WorldBlock:setTerrain(terrain)
self.comps.terrain = terrain
end
function WorldBlock:get(compName)
return self.comps[compName]
end
-- common methods
function WorldBlock:exe(methodName, ...)
return self:doCompsBlock(methodName, unpack(arg))
end
function WorldBlock:touched(touch)
return self:doCompsBlock("touched", touch)
end
function WorldBlock:draw()
self:doCompsB2T("draw")
end
--# TRiver
-- Algorithm
-- 0. go to new dig spots and settle water
-- look around for water ordered by highest to lowest WL
-- equal WL are ordered by proximity to river root
-- let n = new water node at dug spot
-- while the n.WL > n.parent.GL+1
-- take 1 water from it and give to you.
-- establish parent/child relationship
-- update drain strenght for parent-to-node
-- let n = parent
-- propigate n.drainStrength back until root
-- 1. call PushWater(root node,X) // where X = power
-- PushWater(N, X)
-- look around N ordered by lowest-to-high WL then lowest-to-high drain strength
-- let cN = comparison node
-- if N.wl < cN.wl
-- N.wl +=1
-- ==> begin next tock with Push(N, X-1)
-- else
-- PushWater(cn, n)
TRiver = class()
function TRiver:init(x, y, world)
self.nodes = {}
self.root = TRiverNode(x, y, 3, nil, world)
self:trackNode(x,y,self.root)
self.power = 3
end
function TRiver:flow()
return self.root:flow(self, self.power)
end
function TRiver:trackNode(x,y,node)
self.nodes[x..","..y]= node
end
function TRiver:getNode(x,y)
return self.nodes[x..","..y]
end
--# TRRiver
TRRiver = class()
function TRRiver:init(x, y, world)
self.world = world
self.nodes = {}
self.root = TRNode(x, y, 1.0, self)
self.power = 3
end
function TRRiver:flow(mass)
self.root:addWater(mass)
return self.root:flow()
end
function TRRiver:trackNode(x,y,node)
self.nodes[x..","..y]= node
end
function TRRiver:getNode(x,y)
return self.nodes[x..","..y]
end
--# TerrainBlock
TerrainBlock = class(BlockActor)
function TerrainBlock:actorInit()
self.name = "terrainBlock"
self.z = 3
self.gl = 3
self.wl = 3
end
function TerrainBlock:touchedCallback(touch)
return (self.z > 0) and self.wl == self.gl
end
function TerrainBlock:tryDig()
if self.wl > self.gl then
return false
end
if self.z == 0 then
return false
end
self.gl = self.gl - 1
self.wl = self.wl - 1 -- to make a copy
self:setZ(self.z - 1)
self:setSpriteForZ()
return true
end
function TerrainBlock:setWaterLevel(wl)
print(wl)
self.wl = wl
self:setZ(wl)
self:setSpriteForZ()
--self.spriteName = "Planet Cute:Water Block"
end
function TerrainBlock:fillWithWater(layersToFill)
self:setWaterLevel(self.gl + layersToFill)
end
function TerrainBlock:setSpriteForZ()
if self.wl > self.gl then
self.spriteName = "Planet Cute:Water Block"
elseif self.z == 0 then
self.spriteName = "Planet Cute:Shadow North"
elseif self.z == 1 then
self.spriteName = "Planet Cute:Stone Block"
elseif self.z == 2 then
self.spriteName = "Planet Cute:Dirt Block"
elseif self.z == 3 then
self.spriteName = "Planet Cute:Grass Block"
end
end
--# TRNode
TRNode = class()
function TRNode:init(gx,gy, mass, river)
-- you can accept and set parameters here
self.gx, self.gy = gx, gy
self.mass = mass
self.river = river
local gridPointer = river.world:get(gx, gy)
self.gl = gridPointer:get("terrain").gl
self.v = 3.0 - self.gl
self.wl = function()
if self.v == 0 then
return self.gl
end
local fullness = math.min((self.mass/self.v)*self.v, self.v)
print(self.gx,self.gy,fullness)
return math.ceil(fullness + self.gl)
end
self.setWaterLevel = function(newWl)
gridPointer:get("terrain"):setWaterLevel(newWl)
end
self.around = river.world:getAround(self.gx, self.gy)
river:trackNode(gx, gy, self)
self.setWaterLevel(self.wl())
self.dirty = false
self.aroundR = {}
end
-- density
function TRNode:p()
return (self.v > 0) and (self.mass/self.v) or math.huge
end
-- give water molecules to this volume
function TRNode:addWater(mass)
self.mass = self.mass + mass
self.setWaterLevel(self.wl())
end
function TRNode:canAddWater(mass)
return self:p() < 1.5
end
function TRNode:debugPrint()
local output = string.format("trnode: %d,%d\n", self.gx, self.gy)
output = output.."\tvol:%d, m:%d, p:%d\n\tgl:%d, wl:%d"
print(string.format(output,self.v, self.mass, self:p(), self.gl, self.wl()))
end
function TRNode:flow()
local p = self:p()
if self:p() > 1.0 then
-- look for an escape path in via neighbors
local around = self:getNeighborsByDensityDesc()
for _,n in ipairs(around) do
if n:canAddWater(0.5) then
self.mass = self.mass - 0.5
n:addWater(0.5)
if self:p() <= 1.0 then
return true -- =librium
end
end
end
end
return p ~= self:p() -- TODO... is this good?
end
-- look n, e, w, s for neighbors
function TRNode:getNeighborsByDensityDesc()
local r_around = {}
for _,n in pairs(self.around) do
local r = self.river:getNode(n.x, n.y)
if r == nil then
r = TRNode(n.x, n.y, 0.0, self.river)
end
table.insert(r_around, r)
end
return r_around
end
--# TRiverNode
TRiverNode = class()
function TRiverNode:init(gx, gy, wl, p, world)
-- you can accept and set parameters here
self.x = gx
self.y = gy
self.up = p -- upstream nodes
self.dn = {} -- downstream nodes
self.wl = wl -- water level
self.world = world
local gridPointer = world:get(gx, gy)
self.wl = function() return gridPointer:get("terrain").wl end
self.setWl = function(newWl) gridPointer:get("terrain"):setWaterLevel(newWl) end
self.around = world:getAround(self.x, self.y)
gridPointer:get("terrain"):fillWithWater(1)
end
function TRiverNode:flow(r, power)
if self.wl() == 3 then
local didFlow = false
-- 1. flow your downstream nodes
for k, n in pairs(self.dn) do
if n:flow(r) then
didFlow = true
end
end
-- 2. look for adjacent, lower terrain to flow into (N,S,E,W)
for k, n in pairs(self.around) do
if r:getNode(n.x, n.y) == nil then
local terrain = n:get("terrain")
if terrain.gl < self.wl() then
self.dn[#self.dn+1] = TRiverNode(n.x, n.y, terrain.gl+1, self, self.world)
r:trackNode(n.x, n.y, self.dn[#self.dn])
local newN = self.dn[#self.dn]
return true
end
end
end
return didFlow
else
self.setWl(math.min(self.wl()+1, 3))
return true
end
end
--# Map
Map = class()
function Map:init(w, h, mscale)
self.w = w
self.h = h
self.world = TGrid(w, h)
self.objects = {}
self.blockDimensions = {
w = mscale * 101,
h = mscale * 171,
maxGx = w,
maxGy = h
}
for y = 1, self.h do
for x = 1, self.w do
self.world:add(x,y, WorldBlock(x, y))
self.world:get(x,y):setTerrain(TerrainBlock(x, y, self.blockDimensions))
end
end
-- this must go at end (after world is generated)
self.world:get(1,1):get("terrain"):tryDig()
self.river = TRiver(1,1, self.world)
self.tr = TRRiver(1,1, self.world)
end
function Map:draw()
for b in self.world:listTtoB() do
b:draw()
end
end
-- returns true when a block is dug
function Map:touched(touch)
for b in self.world:listBtoT() do
if b:touched(touch) then
return b:exe("tryDig",touch)
end
end
return false
end
function Map:simulate()
--return self.river:flow()
return self.tr:flow(1)
end
--# GameSession
GameSession = class()
function GameSession:init()
-- you can accept and set parameters here
self.turnsLeft = 3
self.turnCap = 3
self.power = 6
end
function GameSession:storePower(x)
self.power = self.power + x
end
function GameSession:usePower(x)
self.power = self.power - x
end
function GameSession:powerLeft()
return self.power
end
--# Main
-- protoColumny
map = nil
mscale = .4
touchtime = 0
touchbuffer = .1
lasttouch = nil
debug_showTouch = false
debug_sprites = false
iphone2padScale = (263.92/(329.65*ContentScaleFactor))
phone = {
w = iphone2padScale*640,
h = iphone2padScale*1136
}
bounds = {
x = 200, y = 200,
w = phone.w, h = phone.h
}
gameState = nil
gameSession = GameSession()
-- Use this function to perform your initial setup
function setup()
mscale = .422
map = Map(6, 11, mscale)
touchtime = ElapsedTime
touches = {}
-- setup state machine
gameState = StateMachine()
gameState:setSharedProperties( { map = map, session = gameSession } )
gameState:startWith( StateActorPlacement( "wheel") )
end
function touchedt(touch)
if touch.state == ENDED then
touches[touch.id] = nil
else
touches[touch.id] = touch
end
end
-- This function gets called once every frame
function draw()
clip(bounds.x-1, bounds.y-1, bounds.w+2, bounds.h+2)
pushMatrix()
spriteMode(CORNER)
translate(bounds.x, bounds.y)
background(44, 44, 54, 255)
gameState:current():draw()
if lasttouch ~= nil and debug_showTouch then
noStroke()
fill(246, 246, 246, 104)
ellipse(lasttouch.x, lasttouch.y, 20,20)
fill(255, 0, 216, 255)
ellipse(lasttouch.x, lasttouch.y, 2,2)
print(string.format("%f %f", lasttouch.x, lasttouch.y))
end
strokeWidth(3)
noFill()
rect(-2,-2, bounds.w+4, bounds.h+4)
popMatrix()
end
function touched(touch)
-- translate the touch to local coordinates
local t = {}
t.state = touch.state
t.id = touch.id
t.x = touch.x - bounds.x
t.y = touch.y - bounds.y
t.prevX = touch.prevX
t.prevY = touch.prevY
t.deltaX = touch.deltaX
t.deltaY = touch.deltaY
t.tapCount = touch.tapCount
lasttouch = t
gameState:current():touched(t)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment