local Entity = require("entity") local Animation = require("animation") local Player = {} Player.__index = Player setmetatable(Player, { __index = Entity }) local MOVE_SPEED = 65 local SWIM_SPEED = 100 local JUMP_FORCE = -210 local GROUND_LAYER = "ground" function Player.new(world, spawnX, spawnY) local w, h = 16, 16 local pw, ph = 16, 16 -- physics body size local self = setmetatable(Entity:new(spawnX or 0, spawnY or 0, w, h, pw, ph), Player) self.spawnX = spawnX or 0 self.spawnY = spawnY or 0 self.pickups = {} self.directionState = { 'side', 'up', 'down', 'side_up', 'side_down' } self.directionIndex = 1 self.direction = self.directionState[self.directionIndex] self.availableJumps = 0 self:enablePhysics(world, "dynamic") self.fixture:setFriction(0) self.doubleJump = true self.jumpsUsed = 0 self.animations = { idle = Animation.new("assets/player/idle.png", 16, 16, 0.6), running = Animation.new("assets/player/running.png", 16, 16, 0.4), going_up = Animation.new("assets/player/going_up.png", 16, 16, 0.8), going_down = Animation.new("assets/player/going_down.png", 16, 16, 0.5), stop_running = Animation.new("assets/player/stop_running.png", 16, 16, 0.5), ground_hit = Animation.new("assets/player/ground_hit.png", 16, 16, 0.5), swimming = Animation.new("assets/player/swimming.png", 16, 16, 0.5) } if love and love.filesystem and love.filesystem.getDirectoryItems then local ok, items = pcall(love.filesystem.getDirectoryItems, "assets/player") if ok and items then for _, filename in ipairs(items) do if filename:match("%.png$") then local name = filename:gsub("%.png$", "") if not self.animations[name] then self.animations[name] = Animation.new("assets/player/" .. filename, 16, 16, 0.3) end end end end end self.animations.ground_hit.looping = false self.animations.stop_running.looping = false self.currentAnim = self.animations.idle self.lastFacing = 1 self.state = "idle" self.wasOnFloor = false self.vx = 0 self.vy = 0 self.grounded = false self.isInLiquid = false self.wasInLiquid = false self.waterSurfaceContact = false self.isDead = false self.respawnTimer = 0 return self end function Player:trackDirectionByKeyPressed() local left = love.keyboard.isDown("left", "a") local right = love.keyboard.isDown("right", "d") local up = love.keyboard.isDown("up", "w") local down = love.keyboard.isDown("down", "s") if up and left then self.directionIndex = 4 elseif up and right then self.directionIndex = 4 elseif down and left then self.directionIndex = 5 elseif down and right then self.directionIndex = 5 elseif up then self.directionIndex = 2 elseif down then self.directionIndex = 3 elseif left then self.directionIndex = 1 elseif right then self.directionIndex = 1 end self.direction = self.directionState[self.directionIndex] end function Player:update(dt) self:syncFromPhysicsBody() if self.isDead then self.respawnTimer = self.respawnTimer - dt if self.respawnTimer <= 0 then self:respawn() end self.currentAnim = self.animations.dead or self.animations.idle self.currentAnim:update(dt) return end self:trackDirectionByKeyPressed() local vx, vy = self.body:getLinearVelocity() self.vx = vx self.vy = vy local c = self.contact or { floor = 0, wall = 0, ceiling = 0 } local onFloor = c.floor == 1 local onWaterSurface = self.waterSurfaceContact local prevOnFloor = self.wasOnFloor local prevState = self.state local move = 0 if love.keyboard.isDown("left", "a") then move = move - 1 end if love.keyboard.isDown("right", "d") then move = move + 1 end if self.isInLiquid then local vx, vy = self.body:getLinearVelocity() local mass = self.body:getMass() local gx, gy = self.body:getWorld():getGravity() if not self.wasInLiquid then vx = vx * 0.4 vy = math.min(vy, 60) self.body:setLinearVelocity(vx, vy) end self.body:setLinearDamping(6) local buoyancy = 0.9 self.body:applyForce(0, -gy * mass * buoyancy) local moveY = 0 if love.keyboard.isDown("up", "w") then moveY = moveY - 1 end if love.keyboard.isDown("down", "s") then moveY = moveY + 1 end local inputLen = math.sqrt(move * move + moveY * moveY) if inputLen > 1 then move = move / inputLen moveY = moveY / inputLen end if move ~= 0 then self.lastFacing = move end self.body:applyForce(move * SWIM_SPEED, moveY * SWIM_SPEED) elseif onFloor or onWaterSurface then self.body:setLinearDamping(0) self.jumpsUsed = 0 if move ~= 0 then self.lastFacing = move self.body:setLinearVelocity(move * MOVE_SPEED, vy) else self.body:setLinearVelocity(0, vy) end else self.body:setLinearDamping(0) if move ~= 0 then self.lastFacing = move end self.body:setLinearVelocity(move * MOVE_SPEED * 0.7, vy) end local desiredState if self.isInLiquid then desiredState = "swimming" elseif onFloor then desiredState = (move ~= 0) and "running" or "idle" else desiredState = (vy < 0) and "jumping" or "falling" end if self.isInLiquid then self.state = desiredState elseif self.state == "ground_hit" or self.state == "stop_running" then if not onFloor or self.currentAnim:isFinished() then self.state = desiredState end else local wasAirborne = prevState == "jumping" or prevState == "falling" if onFloor and not prevOnFloor and wasAirborne then self.state = "ground_hit" self.animations.ground_hit:reset() elseif onFloor and prevState == "running" and move == 0 then self.state = "stop_running" self.animations.stop_running:reset() else self.state = desiredState end end self.wasOnFloor = onFloor self.wasInLiquid = self.isInLiquid local animMap = { idle = self.animations.idle, running = self.animations.running, jumping = self.animations.going_up, falling = self.animations.going_down, swimming = self.animations.swimming, stop_running = self.animations.stop_running, ground_hit = self.animations.ground_hit, dead = self.animations.dead or self.animations.idle, } self.currentAnim = animMap[self.state] or self.animations.idle self.currentAnim:update(dt) end function Player:die(nx, ny) if self.isDead then return end self.isDead = true self.state = "dead" self.respawnTimer = 4 local len = math.sqrt(nx * nx + ny * ny) if len < 0.01 then nx, ny = 0, -1 else nx, ny = nx / len, ny / len end local BOUNCE_SPEED = 200 self.body:setLinearVelocity(nx * BOUNCE_SPEED, ny * BOUNCE_SPEED) end function Player:respawn() self.isDead = false self.respawnTimer = 0 self.state = "idle" self.jumpsUsed = 0 local cx = self.spawnX + self.physicsWidth / 2 local cy = self.spawnY + self.height - self.physicsHeight / 2 self.body:setPosition(cx, cy) self.body:setLinearVelocity(0, 0) self:syncFromPhysicsBody() end function Player:jump() if self.isDead then return false end local c = self.contact or { floor = 0, wall = 0, ceiling = 0 } local onFloor = c.floor == 1 local onWall = c.wall == 1 local onWaterSurface = self.waterSurfaceContact if onWall and not onFloor then return false end local maxJumps = self.doubleJump and 1 or 0 if self.jumpsUsed >= maxJumps then return false end if self.isInLiquid and not onWaterSurface then return false end self.body:setLinearVelocity(self.body:getLinearVelocity(), JUMP_FORCE) self.state = "jumping" self.animations.going_up:reset() self.jumpsUsed = self.jumpsUsed + 1 return true end function Player:draw() local flip = (self.lastFacing < 0) self.currentAnim:draw(self.x, self.y, flip) end function Player:getBody() return self.body end return Player