diff options
Diffstat (limited to 'not')
-rw-r--r-- | not/Button.lua | 76 | ||||
-rw-r--r-- | not/Camera.lua | 145 | ||||
-rw-r--r-- | not/Cloud.lua | 71 | ||||
-rw-r--r-- | not/Controller.lua | 201 | ||||
-rw-r--r-- | not/Decoration.lua | 33 | ||||
-rw-r--r-- | not/Effect.lua | 46 | ||||
-rw-r--r-- | not/Element.lua | 50 | ||||
-rw-r--r-- | not/Header.lua | 48 | ||||
-rw-r--r-- | not/Hero.lua | 284 | ||||
-rw-r--r-- | not/Menu.lua | 144 | ||||
-rw-r--r-- | not/Music.lua | 25 | ||||
-rw-r--r-- | not/PhysicalBody.lua | 93 | ||||
-rw-r--r-- | not/Platform.lua | 37 | ||||
-rw-r--r-- | not/Player.lua | 159 | ||||
-rw-r--r-- | not/Ray.lua | 52 | ||||
-rw-r--r-- | not/Selector.lua | 290 | ||||
-rw-r--r-- | not/Settings.lua | 72 | ||||
-rw-r--r-- | not/Sprite.lua | 152 | ||||
-rw-r--r-- | not/World.lua | 419 |
19 files changed, 2397 insertions, 0 deletions
diff --git a/not/Button.lua b/not/Button.lua new file mode 100644 index 0000000..91aca45 --- /dev/null +++ b/not/Button.lua @@ -0,0 +1,76 @@ +--- `Button` +-- Menu element that can be activated by user. +Button = { + parent = --[[not.Menu]]nil, + x = 0, + y = 0, + text = "", + focused = false, + sprite, + quads, + delay = 2, + parent, +} + +-- `Button` is a child of `Element`. +require "not.Element" +Button.__index = Button +setmetatable(Button, Element) + +function Button:new (parent) + local o = setmetatable({}, self) + o.parent = parent + o.sprite, o.quads = parent:getSheet() + return o +end + +function Button:setText (text) + self.text = text or "" + return self +end + +function Button:focus(next) + self.focused = true + return true +end +function Button:blur () + self.focused = false +end + +function Button:active () end +function Button:isEnabled () + return true +end + +function Button:draw (scale) + local x,y = self:getPosition() + local quad = self.quads + local sprite = self.sprite + if self:isEnabled() then + love.graphics.setColor(255, 255, 255, 255) + else + love.graphics.setColor(140, 140, 140, 255) + end + love.graphics.draw(sprite, quad.button.normal, x*scale, y*scale, 0, scale, scale) + if self.focused then + love.graphics.draw(sprite, quad.arrow_l, (x+54+math.floor(self.delay))*scale, (y+5)*scale, 0, scale, scale) + love.graphics.draw(sprite, quad.arrow_r, (x-2-math.floor(self.delay))*scale, (y+5)*scale, 0, scale, scale) + end + love.graphics.setFont(Font) + love.graphics.printf(self.text, (x+2)*scale, (y+4)*scale, 54, "center", 0, scale, scale) +end + +function Button:update (dt) + self.delay = self.delay + dt + if self.delay > Button.delay then -- Button.delay is initial + self.delay = self.delay - Button.delay + end +end + +function Button:controlpressed (set, action, key) + if action == "attack" and self.focused and self:isEnabled() then + self:active() + end +end + +return Button diff --git a/not/Camera.lua b/not/Camera.lua new file mode 100644 index 0000000..63489f3 --- /dev/null +++ b/not/Camera.lua @@ -0,0 +1,145 @@ +--- `Camera` +-- Used in drawing. +Camera = { + x = 0, + y = 0, + dest_x = 0, + dest_y = 0, + scale = getScale(), + scaler = getRealScale(), + 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 +function Camera:setPosition (x, y) + local x = x or 0 + local y = y or 0 + self.x, self.y = x, y +end + +function Camera:getPosition () + return self.x, self.y +end + +function Camera:getPositionScaled () + return self.x*self.scale, self.y*self.scale +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 +end + +function Camera:getDestination () + return self.dest_x, self.dest_y +end + +-- Translate points +function Camera:translatePosition (x, y) + local x = x or 0 + local y = y or 0 + return (x-self.x)*self.scale, (y-self.y)*self.scale +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) * self.scale) + else + table.insert(r, (v + y) * self.scale) + 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() +end + +-- Move follow +function Camera:follow () + 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 + end + local x = sum_x / i - love.graphics.getWidth()/self.scale/2 + local y = sum_y / i - love.graphics.getHeight()/self.scale/2 + 4*self.scale -- 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()) + 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) +end diff --git a/not/Cloud.lua b/not/Cloud.lua new file mode 100644 index 0000000..3bc5377 --- /dev/null +++ b/not/Cloud.lua @@ -0,0 +1,71 @@ +--- `Cloud` +-- That white thing moving in the background. +-- TODO: extends variables names to be readable. +Cloud = { + t = 1, -- type (sprite number) + 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 + } +} + +-- `Cloud` is a child of `Decoration`. +require "not.Decoration" +Cloud.__index = Cloud +setmetatable(Cloud, Decoration) + +-- Constructor of `Cloud`. +function Cloud:new (x, y, t, v) + local o = setmetatable({}, self) + o:init(x, y, t, v) + -- Load spritesheet statically. + if self:getImage() == nil then + self:setImage(Sprite.newImage("assets/clouds.png")) + end + return o +end + +-- Initializer of `Cloud`. +function Cloud:init (x, y, t, v) + Decoration.init(self, x, y, nil) + self:setAnimationsList(animations) + self:setVelocity(v) + self:setType(t) +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 +end +function Cloud:setVelocity (velocity) + self.v = velocity +end + +-- Update of `Cloud`, returns x for world to delete cloud after reaching right corner. +function Cloud:update (dt) + Decoration.update(self, dt) + self.x = self.x + self.v*dt + return self.x +end diff --git a/not/Controller.lua b/not/Controller.lua new file mode 100644 index 0000000..8a2a863 --- /dev/null +++ b/not/Controller.lua @@ -0,0 +1,201 @@ +--- `Controller` +-- Module to manage player input. +-- It uses `love.keypressed`, `love.keyreleased`, `love.gamepadreleased`, `love.gamepadpressed`, `love.joystickadded`, so be sure not to use them by yourself. +-- Rather than that use functions provided by this module: `Controller.controlpressed` and `Controller.controlreleased`. +Controller = { + sets = {}, + axes = {}, + deadzone = .3 +} + +-- Declared to avoid calling nil. Be sure to define yours after this line is performed. +function Controller.controlpressed(set, action, key) end +function Controller.controlreleased(set, action, key) end + +-- Create new controls set. +function Controller.registerSet(left, right, up, down, attack, jump, joystick) + if not Controller.isJoystickUnique(joystick) then return end + local set = {} + set.left = left or "left" + set.right = right or "right" + set.up = up or "up" + set.down = down or "down" + set.attack = attack or "return" + set.jump = jump or "rshift" + set.joystick = joystick + table.insert(Controller.sets, set) + print(set, left, right, up, down, attack, jump, joystick) + return set +end + +-- Reset table of controls sets. +function Controller.reset() + local t = {} + Controller.sets = t +end + +-- Get table of controls sets. +function Controller.getSets() + return Controller.sets +end + +-- Checks if given joystick is unique in current set of Controller sets +function Controller.isJoystickUnique(joystick) + if joystick ~= nil then + for _,set in pairs(Controller.sets) do + if set.joystick == joystick then return false end + end + end + return true +end + +-- Tests all sets if they have control assigned to given key and joystick. +function Controller.testSets(key, joystick) + for i,set in pairs(Controller.sets) do + local action = Controller.testControl(set, key, joystick) + if action ~= nil then + return set, action + end + end + return nil, nil +end + +-- Tests given set if it has controll assigned to given key and joystick. +function Controller.testControl(set, key, joystick) + -- First test if it is joystick and if it is correct one + if joystick == set.joystick then + if key == set.left then + return "left" + elseif key == set.right then + return "right" + elseif key == set.up then + return "up" + elseif key == set.down then + return "down" + elseif key == set.attack then + return "attack" + elseif key == set.jump then + return "jump" + end + end +end + +-- Checks if given action of given set is down +function Controller.isDown(set, action) + if set ~= nil then + if set.joystick == nil then + return love.keyboard.isDown(set[action]) + else + if not Controller.isAxis(set[action]) then + return set.joystick:isGamepadDown(set[action]) + else + return Controller.getAxisState(set.joystick, set[action]) + end + end + end +end + +-- Return key name from given axis and value +function Controller.createAxisName(axis, value) + local key = "axis:"..axis + if value == 0 then + key = key.."0" + elseif value > 0 then + key = key.."+" + else + key = key.."-" + end + return key +end + +-- Checks if given key is an axis +function Controller.isAxis(key) + if string.find(key, "axis:") then + return true + else + return false + end +end + +-- Checks state of key assigned to axis of given joystick +function Controller.getAxisState(joystick, key) + if Controller.axes[joystick] then + local state = Controller.axes[joystick][key] + if state ~= nil then + return state + else + return false + end + end +end + +-- Sets state of key assigned to axis of given joystick +function Controller.setAxisState(joystick, key, state) + if Controller.axes[joystick] == nil then + Controller.axes[joystick] = {} + end + Controller.axes[joystick][key] = state +end + +-- Simulate pressing key on an axis +function Controller.axisPress(joystick, axis, value) + local key = Controller.createAxisName(axis, value) + local set, action = Controller.testSets(key, joystick) + local state = Controller.getAxisState(joystick, key) + if not state then + print(joystick, set, action, key) + Controller.setAxisState(joystick, key, true) + Controller.controlpressed(set, action, key) + end +end + +-- Simulate releasing key on an axis +function Controller.axisRelease(joystick, axis, value) + local key = Controller.createAxisName(axis, value) + local set, action = Controller.testSets(key, joystick) + local state = Controller.getAxisState(joystick, key) + if state then + Controller.setAxisState(joystick, key,false) + Controller.controlreleased(set, action, key) + end +end + +-- Callbacks from LÖVE2D +-- Load gamepad mappings from db file and init module +function Controller.load() + love.joystick.loadGamepadMappings("gamecontrollerdb.txt") +end + +-- Gamepad input callbacks +function Controller.gamepadaxis(joystick, axis, value) + if value ~= 0 then + if math.abs(value) > Controller.deadzone then + Controller.axisPress(joystick, axis, value) + else + Controller.axisRelease(joystick, axis, value) + end + else + Controller.axisRelease(joystick, axis, 1) + Controller.axisRelease(joystick, axis, -1) + end +end +function Controller.gamepadpressed(joystick, key) + local set, action = Controller.testSets(key, joystick) + print(joystick, set, action, key) + Controller.controlpressed(set, action, key) +end +function Controller.gamepadreleased(joystick, key) + local set, action = Controller.testSets(key, joystick) + Controller.controlreleased(set, action, key) +end + +-- Keyboard input callbacks +function Controller.keypressed(key) + local set, action = Controller.testSets(key, nil) + print(nil, set, action, key) + Controller.controlpressed(set, action, key) +end +function Controller.keyreleased(key) + local set, action = Controller.testSets(key, nil) + Controller.controlreleased(set, action, key) +end
\ No newline at end of file diff --git a/not/Decoration.lua b/not/Decoration.lua new file mode 100644 index 0000000..9dc2bdd --- /dev/null +++ b/not/Decoration.lua @@ -0,0 +1,33 @@ +--- `Decoration` +-- Positioned sprite used to decorate maps with additional graphics. +Decoration = { + world = --[[not.World]]nil, + x = 0, + y = 0 +} + +-- `Decoration` is a child of `Sprite`. +require "not.Sprite" +Decoration.__index = Decoration +setmetatable(Decoration, Sprite) + +-- Constructor of `Decoration`. +function Decoration:new (x, y, imagePath) + local o = setmetatable({}, self) + o:init(x, y, imagePath) + return o +end + +-- Initializer of `Decoration`. +function Decoration:init (x, y, imagePath) + Sprite.init(self, imagePath) + self:setPosition(x, y) +end + +-- Position-related methods. +function Decoration:getPosition () + return self.x, self.y +end +function Decoration:setPosition (x, y) + self.x, self.y = x, y +end
\ No newline at end of file diff --git a/not/Effect.lua b/not/Effect.lua new file mode 100644 index 0000000..dd7570a --- /dev/null +++ b/not/Effect.lua @@ -0,0 +1,46 @@ +--- `Effect` +-- Short animation with graphics that plays in various situation. +-- TODO: animation is currently slower than it used to be, check if it is ok; if not then make it possible to change it to 0.06 delay. +Effect = { + finished = false, +} + +-- `Effect` is a child of `Decoration`. +require "not.Decoration" +Effect.__index = Effect +setmetatable(Effect, Decoration) + +-- Constructor of `Effect`. +function Effect:new (name, x, y) + local o = setmetatable({}, self) + o:init(name, x, y) + -- Load spritesheet statically. + if self:getImage() == nil then + self:setImage(Sprite.newImage("assets/effects.png")) + end + return o +end + +-- Initializer of `Effect`. +function Effect:init (name, x, y) + Decoration.init(self, x, y, nil) + self:setAnimationsList(require("config.animations.effects")) + self:setAnimation(name) +end + +-- Update of `Effect`. +-- Returns true if animation is finished and effect is ready to be deleted. +function Effect:update (dt) + Decoration.update(self, dt) + return self.finished +end + +-- Overridden from `not.Sprite`. +-- Sets finished flag if reached last frame of played animation. +function Effect:goToNextFrame () + if not (self.frame == self.current.frames) then + self.frame = (self.frame % self.current.frames) + 1 + else + self.finished = true + end +end diff --git a/not/Element.lua b/not/Element.lua new file mode 100644 index 0000000..e6d91da --- /dev/null +++ b/not/Element.lua @@ -0,0 +1,50 @@ +--- `Element` +-- Empty element used inside `Menu`. +Element = { + parent = --[[not.Menu]]nil, + x = 0, + y = 0 +} + +Element.__index = Element + +function Element:new (parent) + local o = setmetatable({}, self) + o.parent = parent + return o +end + +function Element:delete () end -- deletes Element + +function Element:getPosition () + return self.x, self.y +end +function Element:setPosition (x, y) + self.x = x or 0 + self.y = y or 0 + return self +end + +function Element:set (name, func) + if type(name) == "string" and func ~= nil then + self[name] = func + end + return self +end + +-- 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. + +-- LÖVE2D callbacks +function Element:draw (scale) end +function Element:update (dt) end + +-- Controller callbacks +function Element:controlpressed (set, action, key) end +function Element:controlreleased (set, action, key) end + +return Element diff --git a/not/Header.lua b/not/Header.lua new file mode 100644 index 0000000..a563ab2 --- /dev/null +++ b/not/Header.lua @@ -0,0 +1,48 @@ +--- `Header` +-- Swinging title. +Header = { + parent = --[[not.Menu]]nil, + x = 0, + y = 0, + text = "", + bounce = 2, +} + +-- `Header` is a child of `Element`. +require "not.Element" +Header.__index = Header +setmetatable(Header, Element) + +function Header:new (parent) + local o = setmetatable({}, self) + o.parent = parent + return o +end + +function Header:setText (text) + self.text = text or "" + return self +end + +function Header:getBounce (f) + local f = f or 1 + return math.sin(self.bounce*f*math.pi) +end + +-- LÖVE2D callbacks +function Header:draw (scale) + local angle = self:getBounce(2) + local dy = self:getBounce()*4 + local x,y = self:getPosition() + love.graphics.setColor(255,255,255,255) + love.graphics.setFont(Bold) + love.graphics.printf(string.upper(self.text),x*scale,(y+dy)*scale,400,"center",(angle*5)*math.pi/180,scale,scale,200,12) +end +function Header:update (dt) + self.bounce = self.bounce + dt*0.7 + if self.bounce > Header.bounce then -- Header.bounce is initial + self.bounce = self.bounce - Header.bounce + end +end + +return Header diff --git a/not/Hero.lua b/not/Hero.lua new file mode 100644 index 0000000..feb61da --- /dev/null +++ b/not/Hero.lua @@ -0,0 +1,284 @@ +--- `Hero` +-- Hero (often referred to as: "naut") entity that exists in a game world. +-- Collision category: [2] +Hero = { + -- General and physics + name = "empty", + angle = 0, + facing = 1, + max_velocity = 105, + world = --[[not.World]]nil, + group = nil, + -- Combat + combo = 0, + lives = 3, + spawntimer = 2, + isAlive = true, + punchCooldown = 0.25, + punchdir = 0, -- a really bad thing + -- Movement + inAir = true, + salto = false, + isJumping = false, + isWalking = false, + jumpTimer = 0.16, + jumpCounter = 2, + -- Statics + portrait_sprite = nil, + portrait_frame = nil, + portrait_sheet = getNautsIconsList(), + portrait_box = love.graphics.newQuad( 0, 15, 32,32, 80,130), + sfx = require "config.sounds", +} + +-- `Hero` is a child of `PhysicalBody`. +require "not.PhysicalBody" +Hero.__index = Hero +setmetatable(Hero, PhysicalBody) + +-- Constructor of `Hero`. +function Hero:new (game, world, x, y, name) + local o = setmetatable({}, self) + o:init(name, game, x, y) + -- Load portraits statically. + if self.portrait_sprite == nil then + self.portrait_sprite = love.graphics.newImage("assets/portraits.png") + self.portrait_frame = love.graphics.newImage("assets/menu.png") + end + return o +end + +-- Initializer of `Hero`. +function Hero:init (name, world, x, y) + -- Find imagePath based on hero name. + local fileName = name or Hero.name -- INITIAL from metatable + local imagePath = string.format("assets/nauts/%s.png", fileName) + -- `PhysicalBody` initialization. + PhysicalBody.init(self, world, x, y, imagePath) + self:setBodyType("dynamic") + self:setBodyFixedRotation(true) + self.group = -1-#world.Nauts + -- Main fixture initialization. + local fixture = self:addFixture({-5,-8, 5,-8, 5,8, -5,8}, 8) + fixture:setUserData(self) + fixture:setCategory(2) + fixture:setMask(2) + fixture:setGroupIndex(self.group) + -- Actual `Hero` initialization. + self.world = world + self.punchCooldown = 0 + self.name = name + self:setAnimationsList(require("config.animations.hero")) + self:createEffect("respawn") +end + +-- Update callback of `Hero` +function Hero:update (dt) + PhysicalBody.update(self, dt) + if self.body:isDestroyed() then return end + + -- Salto + if self.salto and (self.current == self.animations.walk or self.current == self.animations.default) then + self.angle = (self.angle + 17 * dt * self.facing) % 360 + elseif self.angle ~= 0 then + self.angle = 0 + end + + -- Custom linear damping. + if not self.isWalking then + local face = nil + local x, y = self:getLinearVelocity() + if x < -12 then + face = 1 + elseif x > 12 then + face = -1 + else + face = 0 + end + self:applyForce(40*face,0) + if not self.inAir then + self:applyForce(80*face,0) + end + end + + -- Could you please die? + -- 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 + self.isAlive + then + self:die() + end + + -- Respawn timer. + if self.spawntimer > 0 then + self.spawntimer = self.spawntimer - dt + end + if self.spawntimer <= 0 and not self.isAlive and self.lives >= 0 then + self:respawn() + end + + -- # PUNCH + -- Cooldown + self.punchCooldown = self.punchCooldown - dt + if not self.body:isDestroyed() then -- TODO: This is weird + for _,fixture in pairs(self.body:getFixtureList()) do -- TODO: getFixtures from `PhysicalBody` or similar. + if fixture:getUserData() ~= self then + fixture:setUserData({fixture:getUserData()[1] - dt, fixture:getUserData()[2]}) + if fixture:getUserData()[1] < 0 then + fixture:destroy() + end + end + end + end + + -- Stop vertical + local c,a = self.current, self.animations + if (c == a.attack_up or c == a.attack_down or c == a.attack) and self.frame < c.frames then + if self.punchdir == 0 then + self:setLinearVelocity(0,0) + else + self:setLinearVelocity(38*self.facing,0) + end + end + + if self.punchCooldown <= 0 and self.punchdir == 1 then + self.punchdir = 0 + end +end + +-- TODO: comment them and place them somewhere properly +function Hero:getAngle () + return self.angle +end +function Hero:getHorizontalMirror () + return self.facing +end +function Hero:getOffset () + return 12,15 +end + +-- Draw of `Hero` +function Hero:draw (offset_x, offset_y, scale, debug) + if not self.isAlive then return end + PhysicalBody.draw(self, offset_x, offset_y, scale, debug) +end + +-- Draw HUD of `Hero` +-- elevation: 1 bottom, 0 top +function Hero:drawHUD (x,y,scale,elevation) + -- hud displays only if player is alive + if self.isAlive then + love.graphics.setColor(255,255,255,255) + love.graphics.draw(self.portrait_frame, self.portrait_box, (x)*scale, (y)*scale, 0, scale, scale) + love.graphics.draw(self.portrait_sprite, self.portrait_sheet[self.name], (x+2)*scale, (y+3)*scale, 0, scale, scale) + local dy = 30 * elevation + love.graphics.setFont(Font) + love.graphics.print((self.combo).."%",(x+2)*scale,(y-3+dy)*scale,0,scale,scale) + love.graphics.print(math.max(0, self.lives),(x+24)*scale,(y-3+dy)*scale,0,scale,scale) + end +end + +-- Change animation of `Hero` +-- default, walk, attack, attack_up, attack_down, damage +function Hero:goToNextFrame () + if self.current.repeated or not (self.frame == self.current.frames) then + self.frame = (self.frame % self.current.frames) + 1 + elseif self.isWalking then + self:setAnimation("walk") + elseif self.current == self.animations.damage then + self:setAnimation("default") + end +end + +-- Spawn `Effect` relative to `Hero` +function Hero:createEffect (name) + if name == "trail" or name == "hit" then + -- 16px effect: -7 -7 + self.world:createEffect(name, self.body:getX()-8, self.body:getY()-8) + elseif name ~= nil then + -- 24px effect: -12 -15 + self.world:createEffect(name, self.body:getX()-12, self.body:getY()-15) + end +end + +-- Creates temporary fixture for hero's body that acts as sensor. +-- direction: ("left", "right", "up", "down") +-- Sensor fixture is deleted after time set in UserData[1]; deleted by `not.Hero.update`. +-- TODO: Magic numbers present in `not.Hero.punch`. +function Hero:punch (direction) + self.punchCooldown = Hero.punchCooldown -- INITIAL from metatable + -- Choose shape based on punch direction. + local shape + if direction == "left" then shape = {-2,-6, -20,-6, -20,6, -2,6} end + if direction == "right" then shape = {2,-6, 20,-6, 20,6, 2,6} end + if direction == "up" then shape = {-8,-4, -8,-20, 8,-20, 8,-4} end + if direction == "down" then shape = {-8,4, -8,20, 8,20, 8,4} end + -- Create and set sensor fixture. + local fixture = self:addFixture(shape, 0) + fixture:setSensor(true) + fixture:setCategory(3) + fixture:setMask(1,3) + fixture:setGroupIndex(self.group) + fixture:setUserData({0.08, direction}) + self:playSound(4) +end + +-- Taking damage of `Hero` by successful hit test +-- currently called from World's startContact +-- TODO: attack functions needs to be renamed, because even I have problems understanding them. +function Hero:damage (direction) + local horizontal, vertical = 0, 0 + if direction == "left" then + horizontal = -1 + end + if direction == "right" then + horizontal = 1 + end + if direction == "up" then + vertical = -1 + end + if direction == "down" then + vertical = 1 + end + self:createEffect("hit") + local x,y = self:getLinearVelocity() + self:setLinearVelocity(x,0) + self:applyLinearImpulse((42+self.combo)*horizontal, (68+self.combo)*vertical + 15) + self:setAnimation("damage") + self.combo = math.min(999, self.combo + 10) + self.punchCooldown = 0.08 + self.combo*0.0006 + self:playSound(2) +end + +-- DIE +function Hero:die () + self:playSound(1) + self.combo = Hero.combo -- INITIAL from metatable + self.lives = self.lives - 1 + self.isAlive = false + self.spawntimer = Hero.spawntimer -- INITIAL from metatable + self:setBodyActive(false) + self.world:onNautKilled(self) +end + +-- And then respawn. Like Jon Snow. +function Hero:respawn () + self.isAlive = true + self:setLinearVelocity(0,0) + self:setPosition(self.world:getSpawnPosition()) -- TODO: I'm not convinced about getting new position like this. + self:setBodyActive(true) + self:createEffect("respawn") + self:playSound(7) +end + +-- Sounds +-- TODO: Possibly export to nonexistent SoundEmitter class. Can be used by World (Stage), too. +function Hero:playSound (sfx, force) + if self.isAlive or force then + local source = love.audio.newSource(self.sfx[sfx]) + source:play() + end +end diff --git a/not/Menu.lua b/not/Menu.lua new file mode 100644 index 0000000..8ef1861 --- /dev/null +++ b/not/Menu.lua @@ -0,0 +1,144 @@ +--- `Menu` +-- It creates single screen of a menu +-- I do know that model I used here and in `World` loading configuration files is not flawless but I did not want to rewrite `World`s one but wanted to keep things similar at least in project scope. +Menu = { + scale = getScale(), + elements = --[[{not.Element}]]nil, + active = 1, + music = --[[not.Music]]nil, + sprite = --[[love.graphics.newImage]]nil, + background = --[[love.graphics.newImage]]nil, + asteroids = --[[love.graphics.newImage]]nil, + stars = --[[love.graphics.newImage]]nil, + asteroids_bounce = 0, + stars_frame = 1, + stars_delay = 0.8, + allowMove = true, + quads = { -- TODO: Could be moved to config file or perhaps QuadManager to manage all quads for animations etc. + button = { + normal = love.graphics.newQuad(0, 0, 58,15, 80,130), + active = love.graphics.newQuad(0, 0, 58,15, 80,130) + }, + portrait = { + normal = love.graphics.newQuad( 0, 15, 32,32, 80,130), + active = love.graphics.newQuad(32, 15, 32,32, 80,130) + }, + panorama = { + normal = love.graphics.newQuad(0,47, 80,42, 80,130), + active = love.graphics.newQuad(0,88, 80,42, 80,130) + }, + arrow_l = love.graphics.newQuad(68, 0, 6, 6, 80,130), + arrow_r = love.graphics.newQuad(74, 0, 6, 6, 80,130), + stars = { + love.graphics.newQuad( 0, 0, 320, 200, 640,200), + love.graphics.newQuad(320, 0, 320, 200, 640,200) + }, + } +} + +Menu.__index = Menu + +require "not.Music" + +function Menu:new (name) + local o = setmetatable({}, self) + -- Load statically. + if self.sprite == nil then + self.sprite = love.graphics.newImage("assets/menu.png") + self.background = love.graphics.newImage("assets/backgrounds/menu.png") + self.asteroids = love.graphics.newImage("assets/asteroids.png") + self.stars = love.graphics.newImage("assets/stars.png") + end + o:init(name) + return o +end + +function Menu:init (name) + self.music = Music:new("menu.ogg") + self:open(name) +end + +function Menu:delete () + self.music:delete() +end + +function Menu:open (name) + local name = name or "main" + self.active = Menu.active --Menu.active is initial + self.elements = love.filesystem.load(string.format("config/menus/%s.lua", name))(self) + self.elements[self.active]:focus() +end + +-- Return reference to quads table and menu sprite +function Menu:getSheet () + return self.sprite, self.quads +end + +-- Cycle elements +function Menu:next () + self.elements[self.active]:blur() + self.active = (self.active%#self.elements)+1 + if not self.elements[self.active]:focus() then + self:next() + end +end +function Menu:previous () + self.elements[self.active]:blur() + if self.active == 1 then + self.active = #self.elements + else + self.active = self.active - 1 + end + if not self.elements[self.active]:focus() then + self:previous() + end +end + +-- LÖVE2D callbacks +function Menu:update (dt) + for _,element in pairs(self.elements) do + element:update(dt) + end + self.asteroids_bounce = self.asteroids_bounce + dt*0.1 + if self.asteroids_bounce > 2 then self.asteroids_bounce = self.asteroids_bounce - 2 end + self.stars_delay = self.stars_delay - dt + if self.stars_delay < 0 then + self.stars_delay = self.stars_delay + Menu.stars_delay --Menu.stars_delay is initial + if self.stars_frame == 2 then + self.stars_frame = 1 + else + self.stars_frame = 2 + end + end +end +function Menu:draw () + local scale = self.scale + local scaler = getRealScale() + love.graphics.draw(self.background, 0, 0, 0, scaler, scaler) + love.graphics.draw(self.stars, self.quads.stars[self.stars_frame], 0, 0, 0, scaler, scaler) + love.graphics.draw(self.asteroids, 0, math.floor(64+math.sin(self.asteroids_bounce*math.pi)*4)*scaler, 0, scaler, scaler) + love.graphics.setFont(Font) + for _,element in pairs(self.elements) do + element:draw(scale) + end +end + +-- Controller callbacks +function Menu:controlpressed (set, action, key) + if self.allowMove then + if action == "down" then + self:next() + end + if action == "up" then + self:previous() + end + end + for _,element in pairs(self.elements) do + element:controlpressed(set, action, key) + end +end +function Menu:controlreleased (set, action, key) + for _,element in pairs(self.elements) do + element:controlreleased(set, action, key) + end +end
\ No newline at end of file diff --git a/not/Music.lua b/not/Music.lua new file mode 100644 index 0000000..ee930f4 --- /dev/null +++ b/not/Music.lua @@ -0,0 +1,25 @@ +--- `Music` +-- Simple music player object that plays and loops selected track in single Scene. +Music = { + source = --[[love.audio.newSource]]nil +} + +Music.__index = Music + +function Music:new (trackName) + local o = setmetatable({}, self) + o:init(trackName) + return o +end + +-- TODO: trackName should be passed without file extension. +function Music:init (trackName) + self.source = love.audio.newSource("assets/music/" .. trackName) + self.source:setLooping(true) + self.source:setVolume(.7) + self.source:play() +end + +function Music:delete () + self.source:stop() +end
\ No newline at end of file diff --git a/not/PhysicalBody.lua b/not/PhysicalBody.lua new file mode 100644 index 0000000..e9625fa --- /dev/null +++ b/not/PhysicalBody.lua @@ -0,0 +1,93 @@ +--- `PhysicalBody` +-- Abstract class for drawable entity existing in `not.World`. +PhysicalBody = { + body =--[[love.physics.newBody]]nil, +} + +-- `PhysicalBody` is a child of `Sprite`. +require "not.Sprite" +PhysicalBody.__index = PhysicalBody +setmetatable(PhysicalBody, Sprite) + +--[[ Constructor of `PhysicalBody`. +function PhysicalBody:new (world, x, y, imagePath) + local o = setmetatable({}, self) + o:init(world, x, y, imagePath) + return o +end +]] + +-- Initializer of `PhysicalBody`. +function PhysicalBody:init (world, x, y, imagePath) + Sprite.init(self, imagePath) + self.body = love.physics.newBody(world.world, x, y) +end + +-- Add new fixture to body. +function PhysicalBody:addFixture (shape, density) + local shape = love.physics.newPolygonShape(shape) + local fixture = love.physics.newFixture(self.body, shape, density) + return fixture +end + +-- Position-related methods. +function PhysicalBody:getPosition () + return self.body:getPosition() +end +function PhysicalBody:setPosition (x, y) + self.body:setPosition(x, y) +end + +-- Velocity-related methods. +function PhysicalBody:setLinearVelocity (x, y) + self.body:setLinearVelocity(x, y) +end +function PhysicalBody:getLinearVelocity () + return self.body:getLinearVelocity() +end + +-- Various setters from Body. +-- type: BodyType ("static", "dynamic", "kinematic") +function PhysicalBody:setBodyType (type) + self.body:setType(type) +end +function PhysicalBody:setBodyFixedRotation (bool) + self.body:setFixedRotation(bool) +end +function PhysicalBody:setBodyActive (bool) + self.body:setActive(bool) +end + +-- Physical influence methods. +function PhysicalBody:applyLinearImpulse (x, y) + self.body:applyLinearImpulse(x, y) +end +function PhysicalBody:applyForce (x, y) + self.body:applyForce(x, y) +end + +-- Update of `PhysicalBody`. +function PhysicalBody:update (dt) + Sprite.update(self, dt) +end + +-- Draw of `PhysicalBody`. +function PhysicalBody:draw (offset_x, offset_y, scale, debug) + Sprite.draw(self, offset_x, offset_y, scale) + if debug then + for _,fixture in pairs(self.body:getFixtureList()) do + local category = fixture:getCategory() + if category == 1 then + love.graphics.setColor(255, 69, 0, 140) + end + if category == 2 then + love.graphics.setColor(137, 255, 0, 120) + end + if category == 3 then + love.graphics.setColor(137, 0, 255, 40) + end + -- TODO: `world` is not a member of `PhysicalBody` or its instance normally. + love.graphics.polygon("fill", self.world.camera:translatePoints(self.body:getWorldPoints(fixture:getShape():getPoints()))) + end + end +end diff --git a/not/Platform.lua b/not/Platform.lua new file mode 100644 index 0000000..3748c47 --- /dev/null +++ b/not/Platform.lua @@ -0,0 +1,37 @@ +--- `Platform` +-- Static platform physical object with a sprite. `Players` can walk on it. +-- Collision category: [1] +-- TODO: reformat code to follow new code patterns +-- TODO: comment uncovered code parts +Platform = { + world = --[[not.World]]nil, +} + +-- `Platform` is a child of `PhysicalBody`. +require "not.PhysicalBody" +Platform.__index = Platform +setmetatable(Platform, PhysicalBody) + +-- Constructor of `Platform` +function Platform:new (animations, shape, game, x, y, sprite) + local o = setmetatable({}, self) + o:init(animations, shape, game, x, y, sprite) + return o +end + +-- Initializer of `Platform`. +function Platform:init (animations, shape, world, x, y, imagePath) + PhysicalBody.init(self, world, x, y, imagePath) + self:setAnimationsList(animations) + self.world = world + -- Create table of shapes if single shape is passed. + if type(shape[1]) == "number" then + shape = {shape} + end + -- Add all shapes from as fixtures to body. + for _,single in pairs(shape) do + local fixture = self:addFixture(single) + fixture:setCategory(1) + fixture:setFriction(0.2) + end +end
\ No newline at end of file diff --git a/not/Player.lua b/not/Player.lua new file mode 100644 index 0000000..2a4b2e6 --- /dev/null +++ b/not/Player.lua @@ -0,0 +1,159 @@ +--- `Player` +-- Special `not.Hero` controllable by a player. +Player = { + -- TODO: move functions and properties related to controls from `not.Hero`. + controllerSet = --[[Controller.sets.*]]nil, +} + +-- `Player` is a child of `Hero`. +require "not.Hero" +Player.__index = Player +setmetatable(Player, Hero) + +-- Constructor of `Player`. +function Player:new (name, game, x, y) + local o = setmetatable({}, self) + o:init(name, game, x, y) + -- Load portraits statically to `not.Hero`. + -- TODO: this is heresy, put it into `load` method or something similar. + if Hero.portrait_sprite == nil then + Hero.portrait_sprite = love.graphics.newImage("assets/portraits.png") + Hero.portrait_frame = love.graphics.newImage("assets/menu.png") + end + return o +end + +-- Initializer of `Player`. +function Player:init (...) + Hero.init(self, ...) +end + +-- Controller set manipulation. +function Player:assignControllerSet (set) + self.controllerSet = set +end +function Player:getControllerSet () + return self.controllerSet +end + +-- Check if control of assigned controller is pressed. +function Player:isControlDown (control) + return Controller.isDown(self:getControllerSet(), control) +end + +-- Update of `Player`. +function Player:update (dt) + Hero.update(self, dt) -- TODO: It would be probably a good idea to add return to update functions to terminate if something goes badly in parent's update. + if self.body:isDestroyed() then return end + local x, y = self:getLinearVelocity() + -- Jumping. + if self.isJumping and self.jumpTimer > 0 then + self:setLinearVelocity(x,-160) + self.jumpTimer = self.jumpTimer - dt + end + + -- Walking. + if self:isControlDown("left") then + self.facing = -1 + self:applyForce(-250, 0) + -- Controlled speed limit + if x < -self.max_velocity then + self:applyForce(250, 0) + end + end + if self:isControlDown("right") then + self.facing = 1 + self:applyForce(250, 0) + -- Controlled speed limit + if x > self.max_velocity then + self:applyForce(-250, 0) + end + end +end + +-- Controller callbacks. +function Player:controlpressed (set, action, key) + if set ~= self:getControllerSet() then return end + -- Jumping + if action == "jump" then + if self.jumpCounter > 0 then + -- General jump logics + self.isJumping = true + --self:playSound(6) + -- Spawn proper effect + if not self.inAir then + self:createEffect("jump") + else + self:createEffect("doublejump") + end + -- Start salto if last jump + if self.jumpCounter == 1 then + self.salto = true + end + -- Animation clear + if (self.current == self.animations.attack) or + (self.current == self.animations.attack_up) or + (self.current == self.animations.attack_down) then + self:setAnimation("default") + end + -- Remove jump + self.jumpCounter = self.jumpCounter - 1 + end + end + + -- Walking + if (action == "left" or action == "right") then + self.isWalking = true + if (self.current ~= self.animations.attack) and + (self.current ~= self.animations.attack_up) and + (self.current ~= self.animations.attack_down) then + self:setAnimation("walk") + end + end + + -- Punching + if action == "attack" and self.punchCooldown <= 0 then + local f = self.facing + self.salto = false + if self:isControlDown("up") then + -- Punch up + if self.current ~= self.animations.damage then + self:setAnimation("attack_up") + end + self:punch("up") + elseif self:isControlDown("down") then + -- Punch down + if self.current ~= self.animations.damage then + self:setAnimation("attack_down") + end + self:punch("down") + else + -- Punch horizontal + if self.current ~= self.animations.damage then + self:setAnimation("attack") + end + if f == 1 then + self:punch("right") + else + self:punch("left") + end + self.punchdir = 1 + end + end +end +function Player:controlreleased (set, action, key) + if set ~= self:getControllerSet() then return end + -- Jumping + if action == "jump" then + self.isJumping = false + self.jumpTimer = Hero.jumpTimer -- take initial from metatable + end + -- Walking + if (action == "left" or action == "right") then + self.isWalking = false + if not (self:isControlDown("left") or self:isControlDown("right")) and + self.current == self.animations.walk then + self:setAnimation("default") + end + end +end diff --git a/not/Ray.lua b/not/Ray.lua new file mode 100644 index 0000000..bbe11c1 --- /dev/null +++ b/not/Ray.lua @@ -0,0 +1,52 @@ +-- `Ray` +-- That awesome effect that blinks when player dies! + +-- WHOLE CODE HAS FLAG OF "need a cleanup" + +Ray = { + naut = nil, + world = nil, + canvas = nil, + delay = 0.3 +} +function Ray:new(naut, world) + -- Meta + local o = {} + setmetatable(o, self) + self.__index = self + -- Init + o.naut = naut + o.world = world + -- Cavas, this is temporary, I believe. + local scale = o.world.camera.scale + local w, h = love.graphics.getWidth(), love.graphics.getHeight() + o.canvas = love.graphics.newCanvas(w/scale, h/scale) + return o +end +function Ray:update(dt) + self.delay = self.delay - dt + if self.delay < 0 then + return true -- delete + end + return false +end +function Ray:draw(offset_x, offset_y, scale) + love.graphics.setCanvas(self.canvas) + love.graphics.clear() + 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) +end diff --git a/not/Selector.lua b/not/Selector.lua new file mode 100644 index 0000000..8e03457 --- /dev/null +++ b/not/Selector.lua @@ -0,0 +1,290 @@ +--- `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 = { + parent = --[[not.Menu]]nil, + x = 0, + y = 0, + width = 0, + height = 0, + margin = 0, + focused = false, + global = false, + delay = 2, + first = false, + list, + sets, + locks, + selections, + shape = "portrait", + sprite, + quads, + icons_i, + icons_q +} + +-- `Selector` is a child of `Element`. +require "not.Element" +Selector.__index = Selector +setmetatable(Selector, Element) + +-- Constructor +function Selector:new (parent) + local o = setmetatable({}, self) + o.parent = parent + o.sprite, o.quads = parent:getSheet() + return o +end + +-- Size of single block +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 +end + +-- Initialize Selector with current settings. +function Selector:init () + -- Make sure that there is list present + if self.list == nil then + self.list = {} + 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 + end + return self +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) +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 + end +end + +-- Check if given number is locked +function Selector:isLocked (n) + local n = n or 1 + return self.locks[n] +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 + 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] +end + +-- Get value from list by selection +function Selector:getListValue (i) + return self.list[i] +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 + 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 +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 + end + if not eligible or self:isLocked(random) then + table.insert(avoids, random) + return self:rollRandom(avoid) + else + return random + 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) + 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) + end + love.graphics.draw(icon, iconq, (x+2)*scale, (y+3)*scale, 0, scale, scale) + 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) + 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) + end +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 + 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 + end + end +end + +return Selector diff --git a/not/Settings.lua b/not/Settings.lua new file mode 100644 index 0000000..e3316f9 --- /dev/null +++ b/not/Settings.lua @@ -0,0 +1,72 @@ +--- `Settings` +-- Stores, loads, saves and changes game settings including Controller sets. +Settings = { + current = {} +} + +function Settings.load() + if Controller then + if not love.filesystem.exists("settings") then + local def = love.filesystem.newFile("settings.default") + local new = love.filesystem.newFile("settings") + new:open("w") def:open("r") + new:write(def:read()) + new:close() def:close() + end + local getSettings = love.filesystem.load("settings") + Settings.current = getSettings() + Controller.reset() + local joysticksList = love.joystick.getJoysticks() -- local list for editing + for _,set in pairs(Settings.current) do + local isJoystick = set[7] + local joystick + if isJoystick then + -- take and remove first joystick from list + joystick = joysticksList[1] + table.remove(joysticksList, 1) + end + if not isJoystick or joystick then + Controller.registerSet(set[1], set[2], set[3], set[4], set[5], set[6], joystick) + end + end + end +end + +function Settings.save() + local new = love.filesystem.newFile("settings") + local sets = Settings.current + local string = "return {\n" + for i,set in pairs(sets) do + string = string .. "\t{" + for j,word in pairs(set) do + if j ~= 7 then + string = string .. "\"" .. word .. "\", " + else + if word then + string = string .. "true" + else + string = string .. "false" + end + end + end + string = string .. "},\n" + end + string = string .. "}" + new:open("w") + new:write(string) + new:close() +end + +function Settings.change(n, left, right, up, down, attack, jump, joystick) + local bool + if joystick then + bool = true + else + bool = false + end + -- Save current settings + Settings.current[n] = {left, right, up, down, attack, jump, bool} + Settings.save() + -- Load settings + Settings.load() +end diff --git a/not/Sprite.lua b/not/Sprite.lua new file mode 100644 index 0000000..25d85f1 --- /dev/null +++ b/not/Sprite.lua @@ -0,0 +1,152 @@ +--- `Sprite` +-- Abstract class for drawable animated entities. +Sprite = { + animations =--[[table with animations]]nil, + current =--[[animations.default]]nil, + image =--[[love.graphics.newImage]]nil, + frame = 1, + delay = .1, +} +Sprite.__index = Sprite + +--[[ Constructor of `Sprite`. +function Sprite:new (imagePath) + local o = setmetatable({}, self) + o:init(imagePath) + return o +end +]] + +-- Cleans up reference to image on deletion. +function Sprite:delete () + self.image = nil +end + +-- Initializes new Sprite instance. +function Sprite:init (imagePath) + if type(imagePath) == "string" then + self:setImage(Sprite.newImage(imagePath)) + end +end + +-- Creates new Image object from path. Key-colours two shades of green. Static. +function Sprite.newImage (path) + local imagedata = love.image.newImageData(path) + local transparency = function(x, y, r, g, b, a) + if (r == 0 and g == 128 and b == 64) or + (r == 0 and g == 240 and b == 6) then + a = 0 + end + return r, g, b, a + end + imagedata:mapPixel(transparency) + local image = love.graphics.newImage(imagedata) + return image +end + +-- Sets an Image as an image. +function Sprite:setImage (image) + self.image = image +end +-- Returns current image Image. +function Sprite:getImage () + return self.image +end + +-- Sets new animations list. +function Sprite:setAnimationsList (t) + if t then + self.animations = t + self:setAnimation("default") + end +end + +-- Sets current animation by table key. +function Sprite:setAnimation (animation) + self.frame = 1 + self.delay = Sprite.delay -- INITIAL from metatable + self.current = self.animations[animation] +end +-- Returns current animation table. +function Sprite:getAnimation () + return self.current +end + +-- Get frame quad for drawing. +function Sprite:getQuad () + if self.animations and self.current then + return self.current[self.frame] + end +end + +-- TODO: Following five methods are stupid, do something about them! +-- Sprite can't be moved by itself. Positioning should be handled by children's methods. +function Sprite:getPosition () + return 0,0 +end +-- Sprite can't be rotated by itself. Rotation should be handled by children's methods. +function Sprite:getAngle () + return 0 +end +-- Sprite can't be mirrored by itself. Mirroring should be handled by children's methods. +function Sprite:getHorizontalMirror () + return 1 +end +function Sprite:getVerticalMirror () + return 1 +end +-- Sprite can't be offset by itself. Offsetting should be handled by children's methods. +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: 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 + + 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) + + -- 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 + + if i 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()) + else + love.graphics.draw(i, draw_x, draw_y, angle, scaleX, scaleY, self:getOffset()) + end + end +end + +-- Animation updating. +function Sprite:update (dt) + if self.animations and self.current then + self.delay = self.delay - dt + if self.delay < 0 then + self.delay = self.delay + Sprite.delay -- INITIAL from metatable + self:goToNextFrame() + end + end +end + +-- Moving to the next frame. +function Sprite:goToNextFrame () + if self.current.repeated or not (self.frame == self.current.frames) then + self.frame = (self.frame % self.current.frames) + 1 + else + self:setAnimation("default") + end +end
\ No newline at end of file diff --git a/not/World.lua b/not/World.lua new file mode 100644 index 0000000..bbceec4 --- /dev/null +++ b/not/World.lua @@ -0,0 +1,419 @@ +--- `World` +-- 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 = { + world = --[[love.physics.newWorld]]nil, + Nauts = --[[{not.Hero}]]nil, + Platforms = --[[{not.Platform}]]nil, + Clouds = --[[{not.Cloud}]]nil, + Decorations = --[[{not.Decoration}]]nil, + Effects = --[[{not.Effect}]]nil, + Rays = --[[{not.Ray}]]nil, + camera = --[[not.Camera]]nil, + -- cloud generator + clouds_delay = 5, + -- Map + map = nil, + background = nil, + -- Gameplay status + lastNaut = false, + -- "WINNER" + win_move = 0, + -- Music + music = nil +} + +World.__index = World + +require "not.Platform" +require "not.Player" +require "not.Cloud" +require "not.Effect" +require "not.Decoration" +require "not.Ray" +require "not.Music" + +-- Constructor of `World` ZA WARUDO! +function World:new (map, nauts) + local o = setmetatable({}, self) + o:init(map, nauts) + return o +end + +-- Init za warudo +function World:init (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 = {} + -- Random init; TODO: use LOVE2D's random. + math.randomseed(os.time()) + -- Map and misc. + local map = map or "default" + self:loadMap(map) + self:spawnNauts(nauts) + self.camera = Camera:new(self) + self.music = Music:new(self.map.theme) +end + +-- The end of the world +function World:delete () + for _,platform in pairs(self.Platforms) do + platform:delete() + end + for _,naut in pairs(self.Nauts) do + naut:delete() + end + self.music:delete() + self.world:destroy() +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) + end + end +end + +-- Spawn all the nauts for the round +function World:spawnNauts (nauts) + for _,naut in pairs(nauts) do + local x,y = self:getSpawnPosition() + local spawn = self:createNaut(x, y, naut[1]) + spawn:assignControllerSet(naut[2]) + end +end + +-- Get respawn location +function World:getSpawnPosition () + local n = math.random(1, #self.map.respawns) + 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:new(animations, polygon, self, x, y, sprite)) +end + +-- Add new naut to the world +-- TODO: separate two methods for `not.Hero` and `not.Player`. +function World:createNaut (x, y, name) + local naut = Player:new(name, self, x, y) + 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:new(x, y, sprite)) +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:new(x, y, t, v)) +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+math.random(-50,20) + else + x = math.random(m.center_x-m.width/2,m.center_x+m.width/2) + end + y = math.random(m.center_y-m.height/2, m.center_y+m.height/2) + t = math.random(1,3) + v = math.random(8,18) + self:createCloud(x, y, t, v) +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:new(name, x, y)) +end + +-- Add a ray +function World:createRay (naut) + table.insert(self.Rays, Ray:new(naut, self)) +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) + end + end + return nauts +end +-- are alive +function World:getNautsAlive () + local nauts = {} + for _,naut in self.Nauts do + if naut.isAlive then + table.insert(nauts, naut) + end + end + return nauts +end +-- all of them +function World:getNautsAll () + return self.Nauts +end + +-- get Map name +function World:getMapName () + return self.map.name +end + +-- Event: when player is killed +function World:onNautKilled (naut) + self.camera:startShake() + self:createRay(naut) + local nauts = self:getNautsPlayable() + if self.lastNaut then + changeScene(Menu:new()) + elseif #nauts < 2 then + self.lastNaut = true + naut:playSound(5, true) + end +end + +function World:getBounce (f) + local f = f or 1 + return math.sin(self.win_move*f*math.pi) +end + +-- LÖVE2D callbacks +-- Update ZU WARUDO +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, _) + end + end + -- Rays + for _,ray in pairs(self.Rays) do + if ray:update(dt) then + table.remove(self.Rays, _) + end + end + -- Bounce `winner` + self.win_move = self.win_move + dt + if self.win_move > 2 then + self.win_move = self.win_move - 2 + end +end +-- Draw +function World:draw () + -- Camera stuff + local offset_x, offset_y = self.camera:getOffsets() + local scale = self.camera.scale + local scaler = self.camera.scaler + + -- 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) + end + + -- Draw effects + for _,effect in pairs(self.Effects) do + effect:draw(offset_x,offset_y, scale) + end + + -- Draw player + for _,naut in pairs(self.Nauts) do + naut:draw(offset_x, offset_y, scale, debug) + end + + -- Draw ground + for _,platform in pairs(self.Platforms) do + platform:draw(offset_x, offset_y, scale, debug) + end + + -- Draw rays + for _,ray in pairs(self.Rays) do + ray:draw(offset_x, offset_y, scale) + end + + -- 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 + + -- Draw HUDs + for _,naut in pairs(self.Nauts) 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 + local y, e = 1, 1 + if _ < 3 then y, e = h-33, 0 end + naut:drawHUD(1+(_%2)*(w-34), y, scale, e) + end + + -- Draw winner + if self.lastNaut then + local w, h = love.graphics.getWidth()/scale, love.graphics.getHeight()/scale + local angle = self:getBounce(2) + local dy = self:getBounce()*3 + love.graphics.setFont(Bold) + love.graphics.printf("WINNER",(w/2)*scale,(42+dy)*scale,336,"center",(angle*5)*math.pi/180,scale,scale,168,12) + love.graphics.setFont(Font) + love.graphics.printf("rofl, now kill yourself", w/2*scale, 18*scale, 160, "center", 0, scale, scale, 80, 3) + end +end + +-- Box2D callbacks +-- beginContact +function World.beginContact (a, b, coll) + if a:getCategory() == 1 then + local x,y = coll:getNormal() + if y < -0.6 then + -- TODO: move landing to `not.Hero` + -- Move them to Hero + b:getUserData().inAir = false + b:getUserData().jumpCounter = 2 + b:getUserData().salto = false + b:getUserData():createEffect("land") + end + local vx, vy = b:getUserData().body:getLinearVelocity() + if math.abs(x) == 1 or (y < -0.6 and x == 0) then + b:getUserData():playSound(3) + end + end + if a:getCategory() == 3 then + b:getUserData():damage(a:getUserData()[2]) + end + if b:getCategory() == 3 then + a:getUserData():damage(b:getUserData()[2]) + end +end +-- endContact +function World.endContact (a, b, coll) + if a:getCategory() == 1 then + -- Move them to Hero + 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 nauts = {} + for _,naut in pairs(self:getNautsAll()) do + table.insert(nauts, {naut.name, naut:getControlSet()}) + end + local new = World:new(map, nauts) + changeScene(new) + end + for k,naut in pairs(self:getNautsAll()) do + naut:controlpressed(set, action, key) + end +end +function World:controlreleased (set, action, key) + for k,naut in pairs(self:getNautsAll()) do + naut:controlreleased(set, action, key) + end +end
\ No newline at end of file |