diff options
Diffstat (limited to 'not/Hero.lua')
-rw-r--r-- | not/Hero.lua | 284 |
1 files changed, 284 insertions, 0 deletions
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 |