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 | |
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')
-rw-r--r-- | not/Button.lua | 4 | ||||
-rw-r--r-- | not/Camera.lua | 221 | ||||
-rw-r--r-- | not/Cloud.lua | 84 | ||||
-rw-r--r-- | not/CloudGenerator.lua | 73 | ||||
-rw-r--r-- | not/Element.lua | 24 | ||||
-rw-r--r-- | not/Group.lua | 116 | ||||
-rw-r--r-- | not/Hero.lua | 17 | ||||
-rw-r--r-- | not/Layer.lua | 45 | ||||
-rw-r--r-- | not/MusicPlayer.lua | 15 | ||||
-rw-r--r-- | not/Object.lua | 7 | ||||
-rw-r--r-- | not/PhysicalBody.lua | 22 | ||||
-rw-r--r-- | not/Ray.lua | 44 | ||||
-rw-r--r-- | not/SceneManager.lua | 8 | ||||
-rw-r--r-- | not/Selector.lua | 365 | ||||
-rw-r--r-- | not/Sprite.lua | 17 | ||||
-rw-r--r-- | not/Timer.lua | 30 | ||||
-rw-r--r-- | not/Trap.lua | 51 | ||||
-rw-r--r-- | not/Trigger.lua | 18 | ||||
-rw-r--r-- | not/World.lua | 565 |
19 files changed, 1020 insertions, 706 deletions
diff --git a/not/Button.lua b/not/Button.lua index a2f7a19..3493a84 100644 --- a/not/Button.lua +++ b/not/Button.lua @@ -15,6 +15,10 @@ function Button:new (parent) self.sprite, self.quads = parent:getSheet() end +function Button:getSize () + return 58, 15 +end + function Button:setText (text) self.text = text or "" return self diff --git a/not/Camera.lua b/not/Camera.lua index aa4df5b..65395cb 100644 --- a/not/Camera.lua +++ b/not/Camera.lua @@ -1,37 +1,46 @@ ---- `Camera` --- Used in drawing. -Camera = { - x = 0, - y = 0, - dest_x = 0, - dest_y = 0, - shake = 0, - timer = 0, - delay = 0, - origin_x = 0, - origin_y = 0, - shake_x = 0, - shake_y = 0, - world = --[[not.World]]nil, -} - --- Constructor of `Camera` -function Camera:new (world) - local o = {} - setmetatable(o, self) - self.__index = self - o.world = world - o:setPosition(o:follow()) - o:setDestination(o:follow()) - return o -end - --- Drawing offsets -function Camera:getOffsets () - return -self.x,-self.y -end - --- Position +--- Used in drawing other stuff in places. +-- TODO: Camera is missing documentation on every important method. +Camera = require "not.Object":extends() + +Camera.SHAKE_LENGTH = 0.6 +Camera.SHAKE_INTERVAL = 0.03 + +-- TODO: Camera would really make use of vec2s (other classes would use them too). +function Camera:new (x, y, world) + self.world = world + self:setPosition(x, y) + self:resetSum() + self:initShake() +end + +function Camera:initShake () + self.shakeTime = 0 + self.shakeInterval = 0 + self.shakeShift = { + theta = love.math.random() * 2, + radius = 0 + } +end + +function Camera:push () + love.graphics.push() +end + +function Camera:transform (scale, ratio, vw, vh) + local px, py = self:getPosition() + local sx, sy = self:getShake() + local dx, dy = (px + sx) * ratio, (py + sy) * ratio + + vw, vh = vw / scale / 2, vh / scale / 2 + + love.graphics.scale(scale, scale) + love.graphics.translate(vw - dx, vh - dy) +end + +function Camera:pop () + love.graphics.pop() +end + function Camera:setPosition (x, y) local x = x or 0 local y = y or 0 @@ -42,102 +51,86 @@ function Camera:getPosition () return self.x, self.y end -function Camera:getPositionScaled () - return self.x*getScale(), self.y*getScale() -end - --- Destination -function Camera:setDestination (x, y) - local x = x or 0 - local y = y or 0 - self.dest_x, self.dest_y = x, y +function Camera:getBoundaries (scale, vw, vh) + local x, y = self:getPosition() + local width, height = vw / scale / 2, vh / scale / 2 + return x - width, y - height, x + width, y + height end -function Camera:getDestination () - return self.dest_x, self.dest_y +function Camera:startShake () + self.shakeTime = Camera.SHAKE_LENGTH end --- Translate points -function Camera:translatePosition (x, y) - local x = x or 0 - local y = y or 0 - return (x-self.x)*getScale(), (y-self.y)*getScale() +local +function limit (theta) + if theta > 2 then + return limitAngle(theta - 2) + end + if theta < 0 then + return limitAngle(theta + 2) + end + return theta end -function Camera:translatePoints(...) - local a = {...} - local r = {} - local x,y = self:getOffsets() - for k,v in pairs(a) do - if k%2 == 1 then - table.insert(r, (v + x) * getScale()) +-- TODO: Magic numbers present in Camera's shake. +function Camera:shake (dt) + if self.shakeTime > 0 then + self.shakeTime = self.shakeTime - dt + if self.shakeInterval < 0 then + self.shakeShift.theta = self.shakeShift.theta - 1.3 + love.math.random() * 0.6 + self.shakeShift.radius = 50 * self.shakeTime + self.shakeInterval = Camera.SHAKE_INTERVAL else - table.insert(r, (v + y) * getScale()) + self.shakeShift.radius = self.shakeShift.radius * 0.66 + self.shakeInterval = self.shakeInterval - dt + end + if self.shakeTime < 0 then + self.shakeShift.radius = 0 end end - return r -end - --- Shake it --- Really bad script, but for now it works -function Camera:shake () - if self.shake_x == 0 then - self.shake_x = math.random(-10, 10) * 2 - elseif self.shake_x > 0 then - self.shake_x = math.random(-10, -1) * 2 - elseif self.shake_x < 0 then - self.shake_x = math.random(10, 1) * 2 - end - if self.shake_y == 0 then - self.shake_y = math.random(-10, 10) * 2 - elseif self.shake_y > 0 then - self.shake_y = math.random(-10, -1) * 2 - elseif self.shake_y < 0 then - self.shake_y = math.random(10, 1) * 2 - end - local x = self.origin_x + self.shake_x - local y = self.origin_y + self.shake_y - self:setDestination(x, y) end -function Camera:startShake () - self.timer = 0.3 - self.origin_x, self.origin_y = self:getPosition() +function Camera:getShake () + local radius = self.shakeShift.radius + local theta = self.shakeShift.theta * math.pi + return radius * math.cos(theta), radius * math.sin(theta) end --- Move follow -function Camera:follow () +function Camera:resetSum () + self.sumX = 0 + self.sumY = 0 + self.sumI = 0 +end + +function Camera:sum (x, y) local map = self.world.map - local sum_x,sum_y,i = map.center_x, map.center_y, 1 - for k,naut in pairs(self.world.Nauts) do - local naut_x,naut_y = naut:getPosition() - if math.abs(naut_x - map.center_x) < map.width/2 and - math.abs(naut_y - map.center_y) < map.height/2 then - i = i + 1 - sum_x = naut_x + sum_x - sum_y = naut_y + sum_y - end + if math.abs(x - map.center.x) < map.width/2 and + math.abs(y - map.center.y) < map.height/2 then + self.sumX = self.sumX + x + self.sumY = self.sumY + y + self.sumI = self.sumI + 1 end - local x = sum_x / i - love.graphics.getWidth()/getScale()/2 - local y = sum_y / i - love.graphics.getHeight()/getScale()/2 + 4*getScale() -- hotfix - return x,y end --- Update -function Camera:update (dt) - if self.timer > 0 then - self.timer = self.timer - dt - if self.delay <= 0 then - self:shake() - self.delay = 0.02 - else - self.delay = self.delay - dt - end - else - self:setDestination(self:follow()) +function Camera:getSumPostion () + if self.sumI > 0 then + return self.sumX / self.sumI, self.sumY / self.sumI end - local dx, dy = self:getDestination() - dx = (dx - self.x) * 6 * dt - dy = (dy - self.y) * 6 * dt - self:setPosition(self.x + dx, self.y + dy) + return 0, 0 +end + +function Camera:step (dt) + local x, y = self:getSumPostion() + local dx, dy = (x - self.x), (y - self.y) + if math.abs(dx) > 0.4 or math.abs(dy) > 0.4 then + x = self.x + (x - self.x) * dt * 6 + y = self.y + (y - self.y) * dt * 6 + end + self:setPosition(x, y) +end + +function Camera:update (dt) + self:step(dt) + self:shake(dt) + self:resetSum() end diff --git a/not/Cloud.lua b/not/Cloud.lua index 25169c0..4851042 100644 --- a/not/Cloud.lua +++ b/not/Cloud.lua @@ -1,61 +1,45 @@ -require "not.Decoration" - --- `Cloud` --- That white thing moving in the background. --- TODO: extends variables names to be readable. -Cloud = Decoration:extends() -Cloud.t = 1 -- type (sprite number) -Cloud.v = 13 -- velocity - --- TODO: allow maps to use other quads and sprites for clouds --- TODO: you know this isn't right, don't you? -local animations = { - default = { - [1] = love.graphics.newQuad( 1, 1, 158,47, 478,49), - frames = 1, - repeated = true - }, - default2 = { - [1] = love.graphics.newQuad(160, 1, 158,47, 478,49), - frames = 1, - repeated = true - }, - default3 = { - [1] = love.graphics.newQuad(319, 1, 158,47, 478,49), - frames = 1, - repeated = true - } -} - --- Constructor of `Cloud`. -function Cloud:new (x, y, t, v, world) - if self:getImage() == nil then - self:setImage(Sprite.newImage("assets/clouds.png")) - end - Cloud.__super.new(self, x, y, world, nil) - self:setAnimationsList(animations) - self:setVelocity(v) - self:setType(t) +-- Moving decorations with limited lifespan. +Cloud = require "not.Decoration":extends() + +function Cloud:new (x, y, world, imagePath) + Cloud.__super.new(self, x, y, world, imagePath) + self.velocity_x = 0 + self.velocity_y = 0 + self.boundary_x = 0 + self.boundary_y = 0 end --- Setters for cloud type and velocity. -function Cloud:setType (type) - local animation = "default" - if type > 1 then - animation = animation .. type - end - self:setAnimation(animation) - self.t = type +function Cloud:setVelocity (x, y) + self.velocity_x = x + self.velocity_y = y +end + +function Cloud:setBoundary (x, y) + self.boundary_x = x + self.boundary_y = y end -function Cloud:setVelocity (velocity) - self.v = velocity + +function Cloud:setStyle (style) + self:setAnimation(style) +end + +function Cloud:getStyle () + return self:getAnimation() +end + +function Cloud:testPosition () + if self.x > self.boundary_x or self.y > self.boundary_y then + return true + end end --- Update of `Cloud`, returns x for world to delete cloud after reaching right corner. +-- Cloud will get deleted if this function returns true. function Cloud:update (dt) Cloud.__super.update(self, dt) - self.x = self.x + self.v*dt - return self.x + self.x = self.x + self.velocity_x * dt + self.y = self.y + self.velocity_y * dt + return self:testPosition() end return Cloud diff --git a/not/CloudGenerator.lua b/not/CloudGenerator.lua new file mode 100644 index 0000000..e72514b --- /dev/null +++ b/not/CloudGenerator.lua @@ -0,0 +1,73 @@ +--- Generates clouds over time with randomized positions and styles. +-- Also used as factory for Clouds. +CloudGenerator = require "not.Object":extends() + +require "not.Cloud" + +function CloudGenerator:new (atlas, animations, count, world) + self.world = world + self.atlas = atlas + self.quads = animations + self.count = count + self.interval = 12 + self.timer = self.interval + self.layer = false +end + +-- TODO: This was a bad idea. Move Cloud creation back to World, pass created Cloud here for configuration. +function CloudGenerator:createCloud (x, y, style) + local cloud = Cloud(x, y, self.world, self.atlas) + cloud:setAnimationsList(self.quads) + cloud:setAnimation(style) + cloud:setVelocity(13, 0) + cloud:setBoundary(340, 320) + cloud.generator = self + cloud.layer = self.layer + return cloud +end + +-- TODO: CloudGen's randomization methods are too static (not configurable). +-- TODO: Random position for Clouds inside map shouldn't be random. Make them place them where no clouds are present. +function CloudGenerator:getRandomPosition (inside) + local x, y + local map = self.world.map + if not inside then + x = map.center.x - map.width*1.2 + love.math.random(-50, 20) + else + x = love.math.random(map.center.x - map.width / 2, map.center.x + map.width / 2) + end + y = love.math.random(map.center.y - map.height / 2, map.center.y + map.height / 2) + return x, y +end + +function CloudGenerator:getRandomStyle () + local num = love.math.random(1, 3) + local style = "default" + if num > 1 then + style = style .. tostring(num) + end + return style +end + +function CloudGenerator:run (count, inside) + count = count or 1 + for i=1,count do + local x, y = self:getRandomPosition(inside) + local style = self:getRandomStyle() + self.world:insertCloud(self:createCloud(x, y, style)) + end +end + +function CloudGenerator:update (dt) + local count = self.world:getCloudsCountFrom(self) + if self.timer < 0 then + if self.count > count then + self.timer = self.timer + self.interval + self:run() + end + else + self.timer = self.timer - dt + end +end + +return CloudGenerator diff --git a/not/Element.lua b/not/Element.lua index 24576e6..3b0d13a 100644 --- a/not/Element.lua +++ b/not/Element.lua @@ -1,22 +1,22 @@ -require "not.Object" - --- `Element` -- Empty element used inside `Menu`. -Element = Object:extends() - -Element.parent = --[[not.Menu]]nil -Element.x = 0 -Element.y = 0 +Element = require "not.Object":extends() function Element:new (parent) self.parent = parent + self.x = 0 + self.y = 0 end -function Element:delete () end -- deletes Element +-- TODO: Element's getSize is temporary. Create BoxElement and move it there. +function Element:getSize () + return 0, 0 +end function Element:getPosition () return self.x, self.y end + function Element:setPosition (x, y) self.x = x or 0 self.y = y or 0 @@ -30,12 +30,14 @@ function Element:set (name, func) return self end --- Called when menu tries to focus on this element. +--- Called when menu tries to focus on this element. -- If it will return false then menu will skip element and go to next in list. function Element:focus () return false -end -function Element:blur () end -- Called when Element loses focus. +end + +--- Called when Element loses focus. +function Element:blur () end -- LÖVE2D callbacks function Element:draw (scale) end diff --git a/not/Group.lua b/not/Group.lua new file mode 100644 index 0000000..970d3bc --- /dev/null +++ b/not/Group.lua @@ -0,0 +1,116 @@ +--- Element used for grouping elements and passing input to selected child based on controller set. +Group = require "not.Element":extends() + +function Group:new (parent) + Group.__super.new(self, parent) + self.children = {} + self.margin = 0 +end + +function Group:addChild (element) + table.insert(self.children, element) + return element +end + +-- TODO: Missing semi-important docs on Group's setPosition. +function Group:setPosition (x, y) + local dx = 0 + for _,child in ipairs(self.children) do + child:setPosition(x + dx, y) + dx = dx + child:getSize() + self.margin + end + return Group.__super.setPosition(self, x, y) +end + +function Group:getSize () + local twidth = -self.margin + local theight = 0 + for _,child in ipairs(self.children) do + local cwidth, cheight = child:getSize() + twidth = twidth + child:getSize() + self.margin + if theight < cheight then + theight = cheight + end + end + return twidth, theight +end + +--- Calls function with parameters for each child. +-- @param func key of function to call +-- @param ... parameters passed to function +-- @return table with calls' results +function Group:callEach (func, ...) + local results = {} + for _,child in ipairs(self.children) do + if type(child[func]) == "function" then + table.insert(results, child[func](child, ...) or nil) + end + end + return results +end + +--- Calls function with parameters for each but one child. +-- @param avoid child to avoid calling +-- @param func key of function to call +-- @param ... parameters passed to function +-- @return table with calls' results +function Group:callEachBut (avoid, func, ...) + local results = {} + for _,child in ipairs(self.children) do + if child ~= avoid then + if type(child[func]) == "function" then + table.insert(results, child[func](child, ...) or nil) + end + end + end + return results +end + +--- Calls function with parameters for one child based on controller set. +-- @param set controller set +-- @param func key of function to call +-- @param ... parameters passed to function +-- @return results of called function +function Group:callWithSet (set, func, ...) + for i,test in ipairs(Controller.getSets()) do + if test == set then + local child = self.children[i] + if child then + return child[func](child, ...) + end + end + end +end + +function Group:focus () + self:callEach("focus") + self.focused = true + return true +end + +function Group:blur () + self:callEach("blur") + self.focused = false +end + +function Group:draw (scale) + self:callEach("draw", scale) +end + +function Group:update (dt) + self:callEach("update", dt) +end + +function Group:controlpressed (set, action, key) + if self.focused then + self:callWithSet(set, "controlpressed", set, action, key) + end +end + +function Group:controlreleased (set, action, key) + if self.focused then + self:callWithSet(set, "controlreleased", set, action, key) + end +end + +return Group diff --git a/not/Hero.lua b/not/Hero.lua index 039aeb8..66bc511 100644 --- a/not/Hero.lua +++ b/not/Hero.lua @@ -27,7 +27,7 @@ function Hero:new (name, x, y, world) Hero.load() Hero.__super.new(self, x, y, world, imagePath) -- Physics - self.group = -1-#world.Nauts + self.group = -1-#world:getNautsAll() self:setBodyType("dynamic") self:setBodyFixedRotation(true) self:newFixture() @@ -87,8 +87,8 @@ function Hero:update (dt) -- TODO: World/Map function for testing if Point is inside playable area. local m = self.world.map local x, y = self:getPosition() - if (x < m.center_x - m.width*1.5 or x > m.center_x + m.width*1.5 or - y < m.center_y - m.height*1.5 or y > m.center_y + m.height*1.5) and + if (x < m.center.x - m.width*1.5 or x > m.center.x + m.width*1.5 or + y < m.center.y - m.height*1.5 or y > m.center.y + m.height*1.5) and self.isAlive then self:die() @@ -165,16 +165,17 @@ function Hero:getOffset () return 12,15 end --- Draw of `Hero` -function Hero:draw (offset_x, offset_y, scale, debug) +function Hero:draw (debug) if not self.isAlive then return end - Hero.__super.draw(self, offset_x, offset_y, scale, debug) + Hero.__super.draw(self, debug) end -function Hero:drawTag (offset_x, offset_y, scale) +-- TODO: Hero@drawTag's printf is not readable. +function Hero:drawTag () local x,y = self:getPosition() love.graphics.setFont(Font) - love.graphics.printf(string.format("Player %d", math.abs(self.group)), (math.floor(x)+offset_x)*scale, (math.floor(y)+offset_y-26)*scale,100,'center',0,scale,scale,50,0) + love.graphics.setColor(255, 255, 255) + love.graphics.printf(string.format("Player %d", math.abs(self.group)), math.floor(x), math.floor(y)-26 ,100,'center',0,1,1,50,0) end -- Draw HUD of `Hero` diff --git a/not/Layer.lua b/not/Layer.lua new file mode 100644 index 0000000..14dac32 --- /dev/null +++ b/not/Layer.lua @@ -0,0 +1,45 @@ +--- A little bit more than just a Canvas. +Layer = require "not.Object":extends() + +function Layer:new (width, height) + self.canvas = love.graphics.newCanvas(width, height) + self.transformScale = getScale() + self.transformRatio = 1 + self.drawScale = 1 +end + +function Layer:delete () + self.canvas = nil +end + +--- Sets this layer as current canvas for drawing with love.graphics functions. +-- @return old canvas used by love +function Layer:setAsCanvas () + local c = love.graphics.getCanvas() + love.graphics.setCanvas(self.canvas) + return c +end + +function Layer:renderTo (func, ...) + local c = self:setAsCanvas() + func(...) + love.graphics.setCanvas(c) +end + +function Layer:renderToWith (camera, func, ...) + camera:push() + camera:transform(self.transformScale, self.transformRatio, self.canvas:getDimensions()) + self:renderTo(func, ...) + camera:pop() +end + +function Layer:clear () + self:renderTo(love.graphics.clear) +end + +function Layer:draw () + love.graphics.setColor(255, 255, 255, 255) + love.graphics.draw(self.canvas, nil, nil, nil, self.drawScale, self.drawScale) +end + +return Layer diff --git a/not/MusicPlayer.lua b/not/MusicPlayer.lua index 4634ed9..17beda4 100644 --- a/not/MusicPlayer.lua +++ b/not/MusicPlayer.lua @@ -1,8 +1,5 @@ -require "not.Object" - ---- `MusicPlayer` --- Simple music player object that plays and loops selected track. -MusicPlayer = Object:extends() +--- Simple music player object that stores, playes and loops tracks.. +MusicPlayer = require "not.Object":extends() function MusicPlayer:new (trackName) self.tracks = {} @@ -13,8 +10,9 @@ function MusicPlayer:new (trackName) end function MusicPlayer:delete () - self.tracks = nil self:stop() + self.tracks = nil + self.source = nil end function MusicPlayer:setTrack (trackName) @@ -40,7 +38,10 @@ function MusicPlayer:getCurrentTrack () end end -function MusicPlayer:play () +function MusicPlayer:play (trackName) + if trackName then + self:setTrack(trackName) + end self.source:play() end diff --git a/not/Object.lua b/not/Object.lua index 30b91b5..352e9e3 100644 --- a/not/Object.lua +++ b/not/Object.lua @@ -1,3 +1,8 @@ --- Wrapping library to game's hierarchy in a shameless way. +--- Wrapping library to game's hierarchy in a shameless way. Object = require "lib.object.Object" + +--- Called before Object references are removed from parent. +-- This is not called when Object is garbage collected. +function Object:delete () end + return Object diff --git a/not/PhysicalBody.lua b/not/PhysicalBody.lua index 804c706..5081836 100644 --- a/not/PhysicalBody.lua +++ b/not/PhysicalBody.lua @@ -62,22 +62,30 @@ function PhysicalBody:update (dt) PhysicalBody.__super.update(self, dt) end --- Draw of `PhysicalBody`. -function PhysicalBody:draw (offset_x, offset_y, scale, debug) - PhysicalBody.__super.draw(self, offset_x, offset_y, scale) +function PhysicalBody:draw (debug) + PhysicalBody.__super.draw(self, debug) if debug then for _,fixture in pairs(self.body:getFixtureList()) do local category = fixture:getCategory() + -- TODO: Fixture drawing of PhysicalBodies could take activity into account in every case. if category == 1 then - love.graphics.setColor(255, 69, 0, 140) + love.graphics.setColor(255, 69, 0, 150) end if category == 2 then - love.graphics.setColor(137, 255, 0, 120) + love.graphics.setColor(137, 255, 0, 150) end if category == 3 then - love.graphics.setColor(137, 0, 255, 40) + love.graphics.setColor(137, 0, 255, 50) end - love.graphics.polygon("fill", self.world.camera:translatePoints(self.body:getWorldPoints(fixture:getShape():getPoints()))) + if category == 4 then + if self.body:isActive() then + love.graphics.setColor(255, 230, 0, 50) + else + love.graphics.setColor(255, 230, 0, 10) + end + end + local camera = self.world.camera + love.graphics.polygon("fill", self.body:getWorldPoints(fixture:getShape():getPoints())) end end end diff --git a/not/Ray.lua b/not/Ray.lua index 16a9bee..4ae640a 100644 --- a/not/Ray.lua +++ b/not/Ray.lua @@ -1,21 +1,10 @@ -require "not.Object" +--- That awesome effect that blinks when player dies! +Ray = require "not.Object":extends() ---- `Ray` --- That awesome effect that blinks when player dies! -Ray = Object:extends() - -Ray.naut =--[[not.Hero]]nil -Ray.world =--[[not.World]]nil -Ray.canvas =--[[love.graphics.newCanvas]]nil -Ray.delay = 0.3 - -function Ray:new (naut, world) - self.naut = naut +function Ray:new (source, world) + self.source = source self.world = world - -- Cavas, this is temporary, I believe. - local scale = getScale() - local w, h = love.graphics.getWidth(), love.graphics.getHeight() - self.canvas = love.graphics.newCanvas(w/scale, h/scale) + self.delay = 0.3 end function Ray:update (dt) @@ -26,25 +15,16 @@ function Ray:update (dt) return false end -function Ray:draw (offset_x, offset_y, scale) - love.graphics.setCanvas(self.canvas) - love.graphics.clear() +-- TODO: Ray should use Camera boundaries just-in-case. +-- TODO: Ray uses magic numbers. +function Ray:draw () love.graphics.setColor(255, 247, 228, 247) love.graphics.setLineStyle("rough") love.graphics.setLineWidth(self.delay*160) - local x, y = self.naut:getPosition() - local m = self.world.map - local dy = m.height - if y > m.center_y then - dy = -dy - end - love.graphics.line(-x+offset_x,-y+offset_y-dy*0.7,x+offset_x,y+dy*0.7+offset_y) - -- reset - love.graphics.setCanvas() - love.graphics.setLineWidth(1) - love.graphics.setColor(255,255,255,255) - -- draw on screen - love.graphics.draw(self.canvas, 0, 0, 0, scale, scale) + + local x, y = self.source:getPosition() + + love.graphics.line(x, y, -x, -y) end return Ray diff --git a/not/SceneManager.lua b/not/SceneManager.lua index c076448..4f9edfd 100644 --- a/not/SceneManager.lua +++ b/not/SceneManager.lua @@ -9,7 +9,10 @@ end -- This function should be removed when multiple scenes will be handled properly by SceneManager and other things. function SceneManager:changeScene (scene) - table.remove(self.scenes, #self.scenes) + local removed = table.remove(self.scenes, #self.scenes) + if removed then + removed:delete() + end return self:addScene(scene) end @@ -20,7 +23,8 @@ end -- Not nice, not nice. function SceneManager:removeTopScene () - table.remove(self.scenes, #self.scenes) + local scene = table.remove(self.scenes, #self.scenes) + scene:delete() end function SceneManager:getAllScenes () diff --git a/not/Selector.lua b/not/Selector.lua index ef78778..ee6f0e3 100644 --- a/not/Selector.lua +++ b/not/Selector.lua @@ -1,282 +1,195 @@ -require "not.Element" - --- `Selector` --- Used in Menu for selecting various things from list. Works for each Controller set or globally. ---[[ -How to use `Selector` in `Menu` config file? -selector:new(menu) - :setPosition(x, y) - :setMargin(8) -- each block has marigin on both sides; they do stack - :setSize(32, 32) -- size of single graphics frame - :set("list", require "nautslist") - :set("icons_i", love.graphics.newImage("assets/portraits.png")) - :set("icons_q", require "portraits") - :set("global", false) -- true: single selector; false: selector for each controller set present - :init() -]] -Selector = Element:extends() +-- Element for selecting variable from list. +Selector = require "not.Element":extends() -Selector.width = 0 -Selector.height = 0 -Selector.margin = 0 -Selector.focused = false -Selector.global = false -Selector.delay = 2 -Selector.first = false -Selector.list = --[[]]nil -Selector.sets = --[[]]nil -Selector.locks = --[[]]nil -Selector.selections = --[[]]nil -Selector.shape = "portrait" -Selector.sprite = --[[]]nil -Selector.quads = --[[]]nil -Selector.icons_i = --[[]]nil -Selector.icons_q = --[[]]nil +Selector.DEFAULT_DELAY = 2 +Selector.SHAPE_PORTRAIT = 1 +Selector.SHAPE_PANORAMA = 2 --- Constructor -function Selector:new (parent) +function Selector:new (list, group, parent) Selector.__super.new(self, parent) - self.sprite, self.quads = parent:getSheet() + self.atlas, self.quads = parent:getSheet() + self.group = group + self.list = list + self.delay = Selector.DEFAULT_DELAY + self.shape = Selector.SHAPE_PORTRAIT + self.focused = false + self.lock = false + self.index = 1 end --- Size of single block +-- TODO: See `not/Element@getSize`. function Selector:getSize () - return self.width, self.height -end -function Selector:setSize (width, height) - self.width, self.height = width, height - return self -end - --- Spacing between two blocks -function Selector:getMargin () - return self.margin -end -function Selector:setMargin (margin) - self.margin = margin - return self + if self.shape == Selector.SHAPE_PORTRAIT then + return 32, 32 + end + if self.shape == Selector.SHAPE_PANORAMA then + return 80, 42 + end end --- Initialize Selector with current settings. -function Selector:init () - -- Make sure that there is list present - if self.list == nil then - self.list = {} +--- Makes sure that n is in <1, total> range. +local +function limit (n, total) + if n > total then + return limit(n - total, total) end - -- Initialize global Selector - if self.global then - self.sets = {} - self.locks = {false} - self.selections = {1} - -- Initialize Selector for Controllers - else - self.sets = Controller.getSets() - self.locks = {} - self.selections = {} - for n=1,#self.sets do - self.locks[n] = false - self.selections[n] = 1 - end + if n < 1 then + return limit(n + total, total) end - return self + return n end --- Cycle through list on given number -function Selector:next (n) - local current = self.selections[n] - self:setSelection(n, current + 1) -end -function Selector:previous (n) - local current = self.selections[n] - self:setSelection(n, current - 1) +--- Chooses item with an index. +-- @param index selected item's index +-- @return old index +function Selector:setIndex (index) + local old = self.index + self.index = limit(index, #self.list) + return old end --- Get number associated with a given set -function Selector:checkNumber (set) - if self.global then return 1 end -- For global Selector - for n,check in pairs(self.sets) do - if check == set then return n end +function Selector:rollRandom (exclude) + local exclude = exclude or {} + local index = love.math.random(1, #self.list) + local elgible = true + for _,i in ipairs(exclude) do + if index == i then + elgible = false + break + end + end + if not elgible or not self:isUnique(self.list[index]) then + table.insert(exclude, index) + return self:rollRandom(exclude) end + return index end --- Check if given number is locked -function Selector:isLocked (n) - local n = n or 1 - return self.locks[n] +--- Returns selected item's value. +-- @return item selected from the list +function Selector:getSelected () + return self.list[self.index] end --- Sets value of selection of given number. Returns old. -function Selector:setSelection (n, new) - -- Functception. It sounds like fun but it isn't. - local function limit(new, total) - if new > total then - return limit(new - total, total) - elseif new < 1 then - return limit(total + new, total) - else - return new - end +--- Checks if selection is locked and returns item's value. +-- @return item selected from the list if Selector is locked, nil otherwise +function Selector:getLocked () + if self.lock then + return self:getSelected() end - local n = n or 1 - local old = self.selections[n] - self.selections[n] = limit(new, #self.list) - return old end --- Get value of selection of given number -function Selector:getSelection (n) - local n = n or 1 - return self.selections[n] +--- Checks if Selected value is unique in group's scope. +-- @param index optional parameter to fill in place of currently selected item +-- @return boolean answering question +function Selector:isUnique (item) + local item = item or self:getSelected() + if self.group then + local locked = self.group:callEachBut(self, "getLocked") + for _,value in pairs(locked) do + if value == item then + return false + end + end + end + return true end --- Get value from list by selection -function Selector:getListValue (i) - return self.list[i] +function Selector:getText () + return tostring(self:getSelected()) end --- Checks if selection of given number is unique within Selector scope. -function Selector:isUnique (n) - local selection = self:getSelection(n) - for fn,v in pairs(self.selections) do - if fn ~= n and self:isLocked(fn) and v == selection then - return false - end - end +function Selector:focus () + self.focused = true return true end --- Get list of selections, checks if not locked are allowed. -function Selector:getFullSelection (allowed) - local allowed = allowed - if allowed == nil then allowed = false end - local t = {} - for n,v in pairs(self.selections) do - local name = self:getListValue(self:getSelection(n)) - local locked = self:isLocked(n) - if locked or allowed then - local a = {name} - if self.sets[n] then table.insert(a, self.sets[n]) end - table.insert(t, a) - end - end - return t +function Selector:blur () + self.focused = false end --- Rolls and returns random selection from list that is not locked. -function Selector:rollRandom (avoids) - -- Me: You should make it simpler. - -- Inner me: Nah, it works. Leave it. - -- Me: Ok, let's leave it as it is. - local avoids = avoids or {} - local total = #self.list - local random = love.math.random(1, total) - local eligible = true - for _,avoid in ipairs(avoids) do - if random == avoid then - eligible = false - break - end +-- TODO: Temporary function to determine quad to use. Will be obsolete when BoxElement will be done. See also `not/Element@getSize`. +function Selector:getShapeString () + if self.shape == Selector.SHAPE_PORTRAIT then + return "portrait" end - if not eligible or self:isLocked(random) then - table.insert(avoids, random) - return self:rollRandom(avoid) - else - return random + if self.shape == Selector.SHAPE_PANORAMA then + return "panorama" end end --- Draw single block of Selector -function Selector:drawBlock (n, x, y, scale) - if self.quads == nil or self.sprite == nil then return end - local x, y = x or 0, y or 0 - local name = self:getListValue(self:getSelection(n)) - local locked = self:isLocked(n) - local sprite = self.sprite - local quad = self.quads - local icon = self.icons_i - local iconq = self.icons_q[name] - local w,h = self:getSize() - local unique = self:isUnique(n) - if unique then - love.graphics.setColor(255, 255, 255, 255) - else - love.graphics.setColor(140, 140, 140, 255) +function Selector:draw (scale) + local x, y = self:getPosition() + local w, h = self:getSize() + + local boxType = "normal" + if self:getLocked() then + boxType = "active" + end + + love.graphics.setColor(255, 255, 255, 255) + if not self:isUnique() then + love.graphics.setColor(120, 120, 120, 255) end - if not locked then - love.graphics.draw(sprite, quad[self.shape].normal, x*scale, y*scale, 0, scale, scale) - else - love.graphics.draw(sprite, quad[self.shape].active, x*scale, y*scale, 0, scale, scale) + love.graphics.draw(self.atlas, self.quads[self:getShapeString()][boxType], x*scale, y*scale, 0, scale, scale) + -- TODO: That is one way to draw icon for selected value. Find better one. See: `config/menus/host`. + if self.icons_atlas and self.icons_quads then + love.graphics.draw(self.icons_atlas, self.icons_quads[self.index], (x+2)*scale, (y+3)*scale, 0, scale, scale) end - love.graphics.draw(icon, iconq, (x+2)*scale, (y+3)*scale, 0, scale, scale) + + love.graphics.setColor(255, 255, 255, 255) + if self.focused then local dy = (h-6)/2 - if not locked then - love.graphics.draw(sprite, quad.arrow_l, (x+0-2-math.floor(self.delay))* scale, (y+dy)*scale, 0, scale, scale) - love.graphics.draw(sprite, quad.arrow_r, (x+w-4+math.floor(self.delay))*scale, (y+dy)*scale, 0, scale, scale) - else - love.graphics.draw(sprite, quad.arrow_r, (x+0-2-math.floor(self.delay))* scale, (y+dy)*scale, 0, scale, scale) - love.graphics.draw(sprite, quad.arrow_l, (x+w-4+math.floor(self.delay))*scale, (y+dy)*scale, 0, scale, scale) + local al, ar = self.quads.arrow_r, self.quads.arrow_l + if self.lock then + al, ar = ar, al end - end - if (self:getSelection(n) ~= 1 or self.first) then - love.graphics.setFont(Font) - love.graphics.setColor(255, 255, 255, 255) - love.graphics.printf(string.upper(name), (x-w)*scale, (y+h+1)*scale, w*3, "center", 0, scale, scale) - end -end --- Menu callbacks -function Selector:focus () -- Called when Element gains focus - self.focused = true - return true -end -function Selector:blur () -- Called when Element loses focus - self.focused = false -end - --- LÖVE2D callbacks -function Selector:draw (scale) - local x,y = self:getPosition() - local margin = self:getMargin() - local width = self:getSize() - x = x - #self.selections*0.5*(margin+margin+width) - for n=1,#self.selections do - self:drawBlock(n, x+(margin+width)*(n-1)+margin*n, y, scale) + love.graphics.draw(self.atlas, ar, (x+0-2-math.floor(self.delay))*scale, (y+dy)*scale, 0, scale, scale) + love.graphics.draw(self.atlas, al, (x+w-4+math.floor(self.delay))*scale, (y+dy)*scale, 0, scale, scale) end + + love.graphics.setFont(Font) + love.graphics.printf(self:getText(), (x-w)*scale, (y+h+1)*scale, w*3, "center", 0, scale, scale) end + function Selector:update (dt) self.delay = self.delay + dt - if self.delay > Selector.delay then -- Selector.delay is initial - self.delay = self.delay - Selector.delay + if self.delay > Selector.DEFAULT_DELAY then + self.delay = self.delay - Selector.DEFAULT_DELAY end end --- Controller callbacks --- TODO: Add action to perform when key is pressed and selector is locked in e.g. to move into character selection from map selection. function Selector:controlpressed (set, action, key) if set and self.focused then - local n = self:checkNumber(set) - local locked = self:isLocked(n) - if action == "left" and not locked then self:previous(n) end - if action == "right" and not locked then self:next(n) end - if action == "attack" then - local name = self:getListValue(self:getSelection(n)) - if name == "random" then - self:setSelection(n, self:rollRandom({1,2})) -- avoid empty naut - self.locks[n] = true - else - -- If not empty or if first is allowed. Additionaly must be unique selection. - if (self:getSelection(n) ~= 1 or self.first) and self:isUnique(n) then - self.locks[n] = true - end - end - end - if action == "jump" then - if locked then - self.locks[n] = false - end + local handler = self[action] + if handler then + handler(self) end end end +function Selector:left () + if not self.lock then + self:setIndex(self.index - 1) + end +end + +function Selector:right () + if not self.lock then + self:setIndex(self.index + 1) + end +end + +function Selector:attack () + self.lock = true +end + +-- Selector doesn't actually jump, haha, I tricked you! +function Selector:jump () + self.lock = false +end + return Selector diff --git a/not/Sprite.lua b/not/Sprite.lua index 3951e6e..ec23eac 100644 --- a/not/Sprite.lua +++ b/not/Sprite.lua @@ -11,6 +11,7 @@ Sprite.frame = 1 Sprite.delay = .1 -- Constructor of `Sprite`. +-- TODO: Sprites' in general don't take actual Image in constructor. That is not only case of Decoration. function Sprite:new (imagePath) if type(imagePath) == "string" then self:setImage(Sprite.newImage(imagePath)) @@ -84,26 +85,24 @@ function Sprite:getOffset () return 0,0 end -- Drawing self to LOVE2D buffer. -- If there is no Quad, it will draw entire image. It won't draw anything if there is no image. +-- TODO: Sprite@draw requires a serious review! -- TODO: it doesn't follow same pattern as `not.Hero.draw`. It should implement so it can be called from `not.World`. -- TODO: change children if above changes are in effect: `not.Platform`, `not.Decoration`. -function Sprite:draw (offset_x, offset_y, scale) - local offset_x = offset_x or 0 - local offset_y = offset_y or 0 - +function Sprite:draw (debug) local i, q = self:getImage(), self:getQuad() local x, y = self:getPosition() local angle = self:getAngle() - local scaleX = self:getHorizontalMirror()*(scale or 1) - local scaleY = self:getVerticalMirror()*(scale or 1) + local scaleX = self:getHorizontalMirror() + local scaleY = self:getVerticalMirror() -- pixel grid ; `approx` selected to prevent floating characters on certain conditions local approx = math.floor if (y - math.floor(y)) > 0.5 then approx = math.ceil end - local draw_y = (approx(y) + offset_y) * scale - local draw_x = (math.floor(x) + offset_x) * scale + local draw_y = approx(y) + local draw_x = math.floor(x) - if i then + if i and not self.hidden then love.graphics.setColor(255,255,255,255) if q then love.graphics.draw(i, q, draw_x, draw_y, angle, scaleX, scaleY, self:getOffset()) diff --git a/not/Timer.lua b/not/Timer.lua new file mode 100644 index 0000000..9ae0de8 --- /dev/null +++ b/not/Timer.lua @@ -0,0 +1,30 @@ +Timer = require "not.Trigger":extends() + +function Timer:new (delay) + Timer.__super.new(self) + self.delay = delay + self.left = 0 + self.active = false + self.restart = false +end + +function Timer:start () + self.left = self.delay + self.active = true +end + +function Timer:update (dt) + if self.active then + if self.left < 0 then + self:emit() + self.active = false + if self.restart then + self:start() + end + else + self.left = self.left - dt + end + end +end + +return Timer diff --git a/not/Trap.lua b/not/Trap.lua new file mode 100644 index 0000000..0867a36 --- /dev/null +++ b/not/Trap.lua @@ -0,0 +1,51 @@ +Trap = require "not.PhysicalBody":extends() + +function Trap:new (direction, x, y, world, imagePath) + Trap.__super.new(self, x, y, world, imagePath) + self:setAnimationsList(require("config.animations.flames")) + self:setBodyType("static") + + local mirror = 1 + if direction == "left" then mirror = -1 end + local fixture = self:addFixture({0, 0, 41 * mirror, 0, 41 * mirror, 18, 0, 18}) + fixture:setCategory(4) + fixture:setMask(1,3,4) + fixture:setUserData({direction}) + fixture:setSensor(true) + + self.mirror = mirror +end + +function Trap:fadeIn () + self.hidden = false + self:setBodyActive(true) + if self.animations.fadein then + self:setAnimation("fadein") + end +end + +function Trap:fadeOut () + self:setBodyActive(false) + if self.animations.fadeout then + self:setAnimation("fadeout") + else + self.hidden = true + end +end + +function Trap:getHorizontalMirror () + return self.mirror +end + +function Trap:goToNextFrame () + if self.current.repeated or not (self.frame == self.current.frames) then + self.frame = (self.frame % self.current.frames) + 1 + elseif self.current == self.animations.fadeout then + self:setAnimation("default") + self.hidden = true + else + self:setAnimation("default") + end +end + +return Trap diff --git a/not/Trigger.lua b/not/Trigger.lua new file mode 100644 index 0000000..c6ef7c7 --- /dev/null +++ b/not/Trigger.lua @@ -0,0 +1,18 @@ +Trigger = require "not.Object":extends() + +function Trigger:new () + self.calls = {} +end + +function Trigger:register (func, ...) + local call = {func = func, params = {...}} + table.insert(self.calls, call) +end + +function Trigger:emit () + for _,call in pairs(self.calls) do + call.func(unpack(call.params)) + end +end + +return Trigger 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()}) |