diff options
author | Aki <nthirtyone@gmail.com> | 2017-09-21 21:05:37 +0200 |
---|---|---|
committer | Aki <nthirtyone@gmail.com> | 2017-09-21 21:05:37 +0200 |
commit | e9a450d65d4fb564691cdf651ef5771dd88303ae (patch) | |
tree | f49d29582dd6877f3b3c807c3f7d9d92d368f798 /not/World.lua | |
parent | eb8302723cd85adca0fbaf505cfb315f1db0299a (diff) | |
parent | b97985def64b8bd8f93a7b391b12333595432e52 (diff) | |
download | roflnauts-e9a450d65d4fb564691cdf651ef5771dd88303ae.zip roflnauts-e9a450d65d4fb564691cdf651ef5771dd88303ae.tar.gz roflnauts-e9a450d65d4fb564691cdf651ef5771dd88303ae.tar.bz2 |
Merge branch 'maps'
Diffstat (limited to 'not/World.lua')
-rw-r--r-- | not/World.lua | 565 |
1 files changed, 326 insertions, 239 deletions
diff --git a/not/World.lua b/not/World.lua index c73a3da..4523efa 100644 --- a/not/World.lua +++ b/not/World.lua @@ -1,82 +1,183 @@ ---- `World` --- Used to manage physical world and everything inside it: clouds, platforms, nauts, background etc. +--- Used to manage physical world and everything inside it: clouds, platforms, nauts, background etc. -- TODO: Possibly move common parts of `World` and `Menu` to abstract class `Scene`. World = require "not.Scene":extends() -World.world =--[[love.physics.newWorld]]nil -World.Nauts =--[[{not.Hero}]]nil -World.Platforms =--[[{not.Platform}]]nil -World.Clouds =--[[{not.Cloud}]]nil -World.Decorations =--[[{not.Decoration}]]nil -World.Effects =--[[{not.Effect}]]nil -World.Rays =--[[{not.Ray}]]nil -World.camera =--[[not.Camera]]nil -World.music =--[[not.Music]]nil -World.clouds_delay = 5 -World.map =--[[config.maps.*]]nil -World.background =--[[image?]]nil -World.lastNaut = false - require "not.Platform" require "not.Player" -require "not.Cloud" require "not.Effect" require "not.Decoration" require "not.Ray" +require "not.Cloud" +require "not.CloudGenerator" +require "not.Layer" +require "not.Timer" +require "not.Trap" +require "not.Entity" --- Constructor of `World` ZA WARUDO! +--- ZA WARUDO! +-- TODO: Missing documentation on most of World's methods. function World:new (map, nauts) - -- Box2D physical world. love.physics.setMeter(64) self.world = love.physics.newWorld(0, 9.81*64, true) - self.world:setCallbacks(self.beginContact, self.endContact) - -- Tables for entities. TODO: It is still pretty bad! - self.Nauts = {} - self.Platforms = {} - self.Clouds = {} - self.Effects = {} - self.Decorations = {} - self.Rays = {} - -- Map and misc. - local map = map or "default" - self:loadMap(map) + self.world:setCallbacks(self:getContactCallbacks()) + + self.lastNaut = false + self.entities = {} + self.map = map + + self.camera = Camera(self.map.center.x, self.map.center.y, self) + + self:initLayers() + self:buildMap() self:spawnNauts(nauts) - self.camera = Camera:new(self) - musicPlayer:setTrack(self.map.theme) - musicPlayer:play() + + musicPlayer:play(self.map.theme) end -- The end of the world function World:delete () - for _,platform in pairs(self.Platforms) do - platform:delete() + for _,entity in pairs(self.entities) do + entity:delete() end - for _,naut in pairs(self.Nauts) do - naut:delete() + for layer in self.layers() do + layer:delete() end self.world:destroy() + collectgarbage() +end + +--- Custom iterator for layers table. +-- Iterates over elements in reversed order. Doesn't pay attention to any changes in table. +local +function layersIterator (layers) + local i = layers.n + 1 + return function () + i = i - 1 + return layers[i] + end +end + +--- Layers in World may exists as two references. Every reference is stored inside `instance.layers`. +-- First reference is indexed with number, it exists for every layer. +-- Second reference is indexed with string, it exists only for selected layers. +-- Mentioned special layers are initialized in this method. +-- Additionally layer count is stored inside `instance.layers.n`. +-- Layers are drawn in reverse order, meaning that `instance.layers[1]` will be on the top. +-- Calling `instance.layers` will return iterator. +function World:initLayers () + self.layers = setmetatable({}, {__call = layersIterator}) + self.layers.n = 0 + do + local width, height = love.graphics.getWidth() / getScale(), love.graphics.getHeight() / getScale() + local rays = self:addLayer(width, height) + rays.transformScale = 1 + rays.transformRatio = 0 + rays.drawScale = getScale() + self.layers.rays = rays + end + do + local width, height = love.graphics.getDimensions() + self.layers.tags = self:addLayer(width, height) + self.layers.platforms = self:addLayer(width, height) + self.layers.effects = self:addLayer(width, height) + self.layers.heroes = self:addLayer(width, height) + self.layers.decorations = self:addLayer(width, height) + self.layers.clouds = self:addLayer(width, height) + end +end + +-- TODO: Make collisions for category 3 more customizable or create new category for traps/area effects. +local +function createFlame (self, x, y, direction, timerIn, timerOut) + local trap = Trap(direction, x, y, self, "assets/decorations/205-flames.png") + + trap.layer = self.layers.platforms + + timerIn:register(trap.fadeIn, trap) + timerOut:register(trap.fadeOut, trap) + + self:insertEntity(trap) end --- Load map from file --- TODO: Change current map model to function-based one. -function World:loadMap (name) - local name = name or "default" - local map = love.filesystem.load(string.format("config/maps/%s.lua", name)) - self.map = map() - -- Platforms - for _,platform in pairs(self.map.platforms) do - self:createPlatform(platform.x, platform.y, platform.shape, platform.sprite, platform.animations) - end - -- Decorations - for _,decoration in pairs(self.map.decorations) do - self:createDecoration(decoration.x, decoration.y, decoration.sprite) - end - -- Background - self.background = love.graphics.newImage(self.map.background) - -- Clouds - if self.map.clouds then - for i=1,6 do - self:randomizeCloud(false) +local +function getAnimations (a) + if type(a) == "string" then + return require("config.animations." .. a) + end + if type(a) == "table" then + return a + end +end + +--- Builds map using one of tables frin config files located in `config/maps/` directory. +-- TODO: Clean World@buildMap. Possibly explode into more methods. +-- TODO: Move buildMap along with getAnimations to Factory. +function World:buildMap () + local width, height = love.graphics.getDimensions() + + for _,op in pairs(self.map.create) do + if op.platform then + -- TODO: Merge configs imported from other files to currently processed element. + local config = love.filesystem.load(string.format("config/platforms/%s.lua", op.platform))() + local platform = Platform(config.animations, config.shape, op.x, op.y, self, config.sprite) + platform.layer = self.layers.platforms + self:insertEntity(platform) + end + if op.decoration or op.background then + local imagePath = op.decoration or op.background + local entity = Decoration(0, 0, self, imagePath) + + local x, y = 0, 0 + if op.x and op.y then + x = op.x + y = op.y + elseif op.animations then + entity:setAnimationsList(getAnimations(op.animations)) + _,_,x,y = bg:getAnimation()[1]:getViewport() + bg:setPosition(x / -2, y / -2) + else + local image = love.graphics.newImage(imagePath) + x = image:getWidth() / -2 + y = image:getHeight() / -2 + end + entity:setPosition(x, y) + + local layer = self.layers.decorations + if op.ratio then + layer = self:addLayer(width, height) + layer.transformRatio = op.ratio + if op.background then + layer.transformScale = getRealScale() + end + end + entity.layer = layer + + self:insertEntity(entity) + end + if op.clouds then + local animations = getAnimations(op.animations) + local cg = CloudGenerator(op.clouds, animations, op.count, self) + if op.ratio then + cg.layer = self:addLayer(width, height) + cg.layer.transformRatio = op.ratio + end + self:insertEntity(cg) + cg:run(op.count, true) + end + -- TODO: Make flames and other traps more configurable through map config file. + if op.flames then + local timerIn = Timer(10) + local timerOut = Timer(5) + + timerIn:register(timerOut.start, timerOut) + timerOut:register(timerIn.start, timerIn) + + createFlame(self, -62, 16, "right", timerIn, timerOut) + createFlame(self, 63, 16, "left", timerIn, timerOut) + + self:insertEntity(timerIn) + self:insertEntity(timerOut) + timerOut:start() end end end @@ -96,89 +197,107 @@ function World:getSpawnPosition () return self.map.respawns[n].x, self.map.respawns[n].y end --- Add new platform to the world --- TODO: it would be nice if function parameters would be same as `not.Platform.new`. -function World:createPlatform (x, y, polygon, sprite, animations) - table.insert(self.Platforms, Platform(animations, polygon, x, y, self, sprite)) +function World:addLayer (width, height) + local layer = Layer(width, height) + local n = self.layers.n + 1 + self.layers[n] = layer + self.layers.n = n + return layer end --- Add new naut to the world --- TODO: separate two methods for `not.Hero` and `not.Player`. +-- TODO: Standardize `create*` methods with corresponding constructors. Pay attention to both params' order and names. function World:createNaut (x, y, name) - local naut = Player(name, x, y, self) - table.insert(self.Nauts, naut) - return naut -end - --- Add new decoration to the world --- TODO: `not.World.create*` functions often have different naming for parameters. It is not ground-breaking but it makes reading code harder for no good reason. -function World:createDecoration (x, y, sprite) - table.insert(self.Decorations, Decoration(x, y, self, sprite)) + local h = Player(name, x, y, self) + table.insert(self.entities, h) + h.layer = self.layers.heroes + return h end --- Add new cloud to the world --- TODO: extend variables names to provide better readability. --- TODO: follow new parameters in `not.Cloud.new` based on `not.Cloud.init`. -function World:createCloud (x, y, t, v) - table.insert(self.Clouds, Cloud(x, y, t, v, self)) +function World:createEffect (name, x, y) + local e = Effect(name, x, y, self) + table.insert(self.entities, e) + e.layer = self.layers.effects + return e end --- Randomize Cloud creation -function World:randomizeCloud (outside) - if outside == nil then - outside = true - else - outside = outside - end - local x,y,t,v - local m = self.map - if outside then - x = m.center_x-m.width*1.2+love.math.random(-50,20) - else - x = love.math.random(m.center_x-m.width/2,m.center_x+m.width/2) - end - y = love.math.random(m.center_y-m.height/2, m.center_y+m.height/2) - t = love.math.random(1,3) - v = love.math.random(8,18) - self:createCloud(x, y, t, v) +function World:createRay (naut) + local r = Ray(naut, self) + table.insert(self.entities, r) + r.layer = self.layers.rays + return r end --- Add an effect behind nauts --- TODO: follow new parameters in `not.Effect.new` based on `not.Effect.init`. --- TODO: along with `createRay` move this nearer reast of `create*` methods for readability. -function World:createEffect (name, x, y) - table.insert(self.Effects, Effect(name, x, y, self)) +function World:insertCloud (cloud) + table.insert(self.entities, cloud) + if not cloud.layer then + cloud.layer = self.layers.clouds + end + return cloud end --- Add a ray -function World:createRay (naut) - table.insert(self.Rays, Ray(naut, self)) +--- Verbose wrapper for inserting entities into entities table. +-- @param entity entity to insert +function World:insertEntity (entity) + if entity then + table.insert(self.entities, entity) + return entity + end end --- get Nauts functions --- more than -1 lives -function World:getNautsPlayable () - local nauts = {} - for _,naut in pairs(self.Nauts) do - if naut.lives > -1 then - table.insert(nauts, naut) +--- Searches entities for those which return true with filtering function. +-- @param filter function with entity as parameter +-- @return table containing results of search +function World:getEntities (filter) + local result = {} + for _,entity in pairs(self.entities) do + if filter(entity) then + table.insert(result, entity) end end - return nauts + return result end --- are alive -function World:getNautsAlive () - local nauts = {} - for _,naut in self.Nauts do - if naut.isAlive then - table.insert(nauts, naut) + +--- Counts entities returning true with filtering function. +-- @param filter function with entity as parameter +-- @return entity count +function World:countEntities (filter) + local count = 0 + for _,entity in pairs(self.entities) do + if filter(entity) then + count = count + 1 end end - return nauts + return count +end + +function World:getCloudsCount () + return self:countEntities(function (entity) + return entity:is(Cloud) + end) end --- all of them + +function World:getCloudsCountFrom (generator) + return self:countEntities(function (entity) + return entity:is(Cloud) and entity.generator == generator + end) +end + function World:getNautsAll () - return self.Nauts + return self:getEntities(function (entity) + return entity:is(require("not.Hero")) and not entity.body:isDestroyed() + end) +end + +function World:getNautsPlayable () + return self:getEntities(function (entity) + return entity:is(require("not.Hero")) and entity.lives > -1 + end) +end + +function World:getNautsAlive () + return self:getEntities(function (entity) + return entity:is(require("not.Hero")) and entity.isAlive + end) end -- get Map name @@ -206,121 +325,63 @@ end function World:update (dt) self.world:update(dt) self.camera:update(dt) - -- Engine world: Nauts, Grounds (kek) and Decorations - all Animateds (top kek) - for _,naut in pairs(self.Nauts) do - naut:update(dt) - end - for _,platform in pairs(self.Platforms) do - platform:update(dt) - end - for _,decoration in pairs(self.Decorations) do - decoration:update(dt) - end - -- Clouds - if self.map.clouds then - -- generator - local n = table.getn(self.Clouds) - self.clouds_delay = self.clouds_delay - dt - if self.clouds_delay < 0 and - n < 18 - then - self:randomizeCloud() - self.clouds_delay = self.clouds_delay + World.clouds_delay -- World.clouds_delay is initial - end - -- movement - for _,cloud in pairs(self.Clouds) do - if cloud:update(dt) > 340 then - table.remove(self.Clouds, _) - end - end - end - -- Effects - for _,effect in pairs(self.Effects) do - if effect:update(dt) then - table.remove(self.Effects, _) + + for key,entity in pairs(self.entities) do + if entity:update(dt) then + table.remove(self.entities, key):delete() end end - -- Rays - for _,ray in pairs(self.Rays) do - if ray:update(dt) then - table.remove(self.Rays, _) - end + + -- TODO: Possibly rename Camera@sum because this code part in World doesn't make sense without reading further. + self.camera:sum(self.map.center.x, self.map.center.y) + for _,hero in pairs(self:getNautsAll()) do + self.camera:sum(hero:getPosition()) end + + -- Some additional debug info. + local stats = love.graphics.getStats() + dbg_msg = string.format("%sMap: %s\nClouds: %d\nLoaded: %d\nMB: %.2f", dbg_msg, self.map.filename, self:getCloudsCount(), stats.images, stats.texturememory / 1024 / 1024) end --- Draw -function World:draw () - -- Camera stuff - local offset_x, offset_y = self.camera:getOffsets() - local scale = getScale() - local scaler = getRealScale() - - -- Background - love.graphics.draw(self.background, 0, 0, 0, scaler, scaler) - - -- TODO: this needs to be reworked! - -- Draw clouds - for _,cloud in pairs(self.Clouds) do - cloud:draw(offset_x, offset_y, scale) - end - -- Draw decorations - for _,decoration in pairs(self.Decorations) do - decoration:draw(offset_x, offset_y, scale) +function World:draw () + for _,entity in pairs(self.entities) do + if entity.draw and entity.layer then + entity.layer:renderToWith(self.camera, entity.draw, entity, debug) + end + if entity.drawTag then + self.layers.tags:renderToWith(self.camera, entity.drawTag, entity, debug) + end end - -- Draw effects - for _,effect in pairs(self.Effects) do - effect:draw(offset_x,offset_y, scale) + for layer in self.layers() do + layer:draw() + layer:clear() end - -- Draw player - for _,naut in pairs(self.Nauts) do - naut:draw(offset_x, offset_y, scale, debug) - end + -- TODO: Debug information could possibly get its own layer so it could follow flow of draw method. + if debug then + local center = self.map.center + local ax, ay, bx, by = self.camera:getBoundaries(getScale(), love.graphics.getDimensions()) - -- Draw ground - for _,platform in pairs(self.Platforms) do - platform:draw(offset_x, offset_y, scale, debug) - end + love.graphics.setLineWidth(1 / getScale()) + love.graphics.setLineStyle("rough") - -- Draw rays - for _,ray in pairs(self.Rays) do - ray:draw(offset_x, offset_y, scale) - end + self.camera:push() + self.camera:transform(getScale(), 1, love.graphics.getDimensions()) - -- draw center - if debug then - local c = self.camera - local w, h = love.graphics.getWidth(), love.graphics.getHeight() - -- draw map center love.graphics.setColor(130,130,130) - love.graphics.setLineWidth(1) - love.graphics.setLineStyle("rough") - local cx, cy = c:getPositionScaled() - local x1, y1 = c:translatePosition(self.map.center_x, cy) - local x2, y2 = c:translatePosition(self.map.center_x, cy+h) - love.graphics.line(x1,y1,x2,y2) - local x1, y1 = c:translatePosition(cx, self.map.center_y) - local x2, y2 = c:translatePosition(cx+w, self.map.center_y) - love.graphics.line(x1,y1,x2,y2) - -- draw ox, oy - love.graphics.setColor(200,200,200) - love.graphics.setLineStyle("rough") - local cx, cy = c:getPositionScaled() - local x1, y1 = c:translatePosition(0, cy) - local x2, y2 = c:translatePosition(0, cy+h) - love.graphics.line(x1,y1,x2,y2) - local x1, y1 = c:translatePosition(cx, 0) - local x2, y2 = c:translatePosition(cx+w, 0) - love.graphics.line(x1,y1,x2,y2) - end + love.graphics.line(ax,center.y,bx,center.y) + love.graphics.line(center.x,ay,center.x,by) - for _,naut in pairs(self.Nauts) do - naut:drawTag(offset_x, offset_y, scale) + love.graphics.setColor(200,200,200) + love.graphics.line(ax,0,bx,0) + love.graphics.line(0,ay,0,by) + self.camera:pop() end - - -- Draw HUDs - for _,naut in pairs(self.Nauts) do + + -- TODO: Draw method beyond this point is a very, very dark place (portraits drawing to review). + local scale = getScale() + for _,naut in pairs(self:getNautsAll()) do -- I have no idea where to place them T_T -- let's do: bottom-left, bottom-right, top-left, top-right local w, h = love.graphics.getWidth()/scale, love.graphics.getHeight()/scale @@ -330,15 +391,29 @@ function World:draw () end end --- Box2D callbacks --- TODO: Rather than here, these contacts should be in `Hero` (most likely). --- TODO: Explode these into more functions.\ --- TODO: Stop using magical numbers: --- [1] -> Platform --- [2] -> Hero --- [3] -> Punch sensor -function World.beginContact (a, b, coll) - if a:getCategory() == 1 then +--- Wraps World's beginContact and endContact to functions usable as callbacks for Box2D's world. +-- Only difference in new functions is absence of self as first argument. +-- @return wrapper for beginContact +-- @return wrapper for endContact +function World:getContactCallbacks () + local b = function (a, b, coll) + self:beginContact(a, b, coll) + end + local e = function (a, b, coll) + self:endContact(a, b, coll) + end + return b, e +end + +-- TODO: Move these constants to a proper place (I have no idea where proper place is). +local COLL_HERO = 2 +local COLL_PLATFORM = 1 +local COLL_PUNCH = 3 +local COLL_TRAP = 4 + +-- TODO: Review current state of both Box2D callbacks (again). +function World:beginContact (a, b, coll) + if a:getCategory() == COLL_PLATFORM then local x,y = coll:getNormal() if y < -0.6 then b:getUserData():land() @@ -348,37 +423,49 @@ function World.beginContact (a, b, coll) b:getUserData():playSound(3) end end - if a:getCategory() == 3 then - if b:getCategory() == 2 then + if a:getCategory() == COLL_PUNCH then + if b:getCategory() == COLL_HERO then b:getUserData():damage(a:getUserData()[2]) end - if b:getCategory() == 3 then + if b:getCategory() == COLL_PUNCH then a:getBody():getUserData():damage(b:getUserData()[2]) b:getBody():getUserData():damage(a:getUserData()[2]) local x1,y1 = b:getBody():getUserData():getPosition() local x2,y2 = a:getBody():getUserData():getPosition() local x = (x2 - x1) / 2 + x1 - 12 local y = (y2 - y1) / 2 + y1 - 15 - a:getBody():getUserData().world:createEffect("clash", x, y) + self:createEffect("clash", x, y) end end - if b:getCategory() == 3 then - if a:getCategory() == 2 then + if b:getCategory() == COLL_PUNCH then + if a:getCategory() == COLL_HERO then a:getUserData():damage(b:getUserData()[2]) end end + if a:getCategory() == COLL_TRAP then + if b:getCategory() == COLL_HERO then + b:getUserData():damage(a:getUserData()[1]) + end + end + if b:getCategory() == COLL_TRAP then + if a:getCategory() == COLL_HERO then + a:getUserData():damage(b:getUserData()[1]) + end + end end -function World.endContact (a, b, coll) - if a:getCategory() == 1 then + +function World:endContact (a, b, coll) + if a:getCategory() == COLL_PLATFORM then b:getUserData().inAir = true end end -- Controller callbacks --- TODO: names of this methods don't follow naming patterns in this project. See `Controller` and change it. function World:controlpressed (set, action, key) if key == "f6" and debug then - local map = self:getMapName() + local filename = self.map.filename + local map = love.filesystem.load(filename)() + map.filename = filename local nauts = {} for _,naut in pairs(self:getNautsAll()) do table.insert(nauts, {naut.name, naut:getControllerSet()}) |