Created
October 13, 2012 17:30
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--# 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