summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--animation.lua68
-rw-r--r--main.lua6
-rw-r--r--player.lua181
-rw-r--r--world.lua159
4 files changed, 399 insertions, 15 deletions
diff --git a/animation.lua b/animation.lua
new file mode 100644
index 0000000..7d77d40
--- /dev/null
+++ b/animation.lua
@@ -0,0 +1,68 @@
+local Animation = {}
+Animation.__index = Animation
+
+function Animation.new(imageOrPath, width, height, duration)
+ local self = setmetatable({}, Animation)
+
+ if type(imageOrPath) == "string" then
+ self.spriteSheet = love.graphics.newImage(imageOrPath)
+ else
+ self.spriteSheet = imageOrPath
+ end
+
+ self.frameWidth = width
+ self.frameHeight = height
+ self.quads = {}
+
+ local imgW, imgH = self.spriteSheet:getDimensions()
+ for y = 0, imgH - height, height do
+ for x = 0, imgW - width, width do
+ table.insert(self.quads, love.graphics.newQuad(x, y, width, height, imgW, imgH))
+ end
+ end
+
+ self.duration = duration or 1
+ self.currentTime = 0
+
+ return self
+end
+
+function Animation:update(dt)
+ self.currentTime = self.currentTime + dt
+ if self.looping == false then
+ if self.currentTime > self.duration then
+ self.currentTime = self.duration
+ end
+ else
+ if self.currentTime >= self.duration then
+ self.currentTime = self.currentTime % self.duration
+ end
+ end
+end
+
+function Animation:reset()
+ self.currentTime = 0
+end
+
+-- Returns true only for non-looping animations that have played through once.
+function Animation:isFinished()
+ return self.looping == false and self.currentTime >= self.duration
+end
+
+function Animation:draw(x, y, flipHorizontal)
+ if #self.quads == 0 then return end
+
+ local t = self.currentTime / self.duration
+ local index = math.floor(t * #self.quads) + 1
+ if index > #self.quads then index = #self.quads end
+
+ local quad = self.quads[index]
+ local sx = (flipHorizontal and -1) or 1
+ local sy = 1
+ local ox = flipHorizontal and self.frameWidth or 0
+ local oy = 0
+
+ love.graphics.draw(self.spriteSheet, quad, x, y, 0, sx, sy, ox, oy)
+end
+
+return Animation
diff --git a/main.lua b/main.lua
index 3f25812..5a4e2f0 100644
--- a/main.lua
+++ b/main.lua
@@ -1,5 +1,7 @@
local VIRTUAL_WIDTH, VIRTUAL_HEIGHT = 16*10*3, 9*10*3
local CANVAS_PADDING = 6
+
+DEBUG = true
local CANVAS_WIDTH = VIRTUAL_WIDTH + CANVAS_PADDING
local CANVAS_HEIGHT = VIRTUAL_HEIGHT + CANVAS_PADDING
local WORLD_TO_CANVAS = 3
@@ -114,6 +116,10 @@ function love.keypressed(key, scancode, isrepeat)
if key == "f11" then
love.window.setFullscreen(not love.window.getFullscreen(), "desktop")
end
+ if (key == "space" or key == "up" or key == "w") and not isrepeat then
+ local player = world and world:getPlayer()
+ if player then player:jump() end
+ end
end
function love.draw()
diff --git a/player.lua b/player.lua
new file mode 100644
index 0000000..4e39f21
--- /dev/null
+++ b/player.lua
@@ -0,0 +1,181 @@
+local Entity = require("entity")
+local Animation = require("animation")
+
+local Player = {}
+Player.__index = Player
+setmetatable(Player, { __index = Entity })
+
+local MOVE_SPEED = 70
+local JUMP_FORCE = -200
+local GROUND_LAYER = "ground"
+
+function Player.new(world, spawnX, spawnY)
+ local w, h = 16, 16
+ local pw, ph = 8, 8 -- physics body size
+ local self = setmetatable(Entity:new(spawnX or 0, spawnY or 0, w, h, pw, ph), Player)
+
+ 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.animations = {
+ idle = Animation.new("assets/player/idle.png", 16, 16, 0.6),
+ running = Animation.new("assets/player/running.png", 16, 16, 1),
+ 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.3),
+ ground_hit = Animation.new("assets/player/ground_hit.png", 16, 16, 0.3)
+ }
+ 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
+ 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()
+ 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 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 onFloor then
+ self.availableJumps = 1
+ if move ~= 0 then
+ self.lastFacing = move
+ self.body:setLinearVelocity(move * MOVE_SPEED, vy)
+ else
+ self.body:setLinearVelocity(0, vy)
+ end
+ else
+ if move ~= 0 then self.lastFacing = move end
+ self.body:setLinearVelocity(move * MOVE_SPEED * 0.7, vy)
+ end
+
+ local desiredState
+ if onFloor then
+ desiredState = (move ~= 0) and "running" or "idle"
+ else
+ desiredState = (vy < 0) and "jumping" or "falling"
+ end
+
+ if 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
+
+ local animMap = {
+ idle = self.animations.idle,
+ running = self.animations.running,
+ jumping = self.animations.going_up,
+ falling = self.animations.going_down,
+ stop_running = self.animations.stop_running,
+ ground_hit = self.animations.ground_hit,
+ }
+ self.currentAnim = animMap[self.state] or self.animations.idle
+ self.currentAnim:update(dt)
+end
+
+function Player:jump()
+ local c = self.contact or { floor = 0, wall = 0, ceiling = 0 }
+ local canJump = (c.floor == 1 or c.wall == 1) and (self.availableJumps or 0) > 0
+ if canJump then
+ self.body:setLinearVelocity(self.body:getLinearVelocity(), JUMP_FORCE)
+ self.state = "jumping"
+ self.animations.going_up:reset()
+ self.availableJumps = self.availableJumps - 1
+ return true
+ end
+ return false
+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
diff --git a/world.lua b/world.lua
index 6890e60..b9ba01f 100644
--- a/world.lua
+++ b/world.lua
@@ -1,5 +1,6 @@
local Tilemap = require("tilemap")
local Entity = require("entity")
+local Player = require("player")
local Camera = require("camera")
local World = {}
@@ -18,6 +19,10 @@ function World:new()
self.enemies = {}
self.entities = {}
self.camera = nil
+ self.groundContacts = {}
+ self.contactCounts = {}
+ self.contactEntity = {}
+ self.contactKind = {}
return self
end
@@ -26,7 +31,17 @@ function World:load(mapPath, tilesetPath)
self.groundEntities = {}
end
self.physicsWorld = love.physics.newWorld(0, GRAVITY)
- self.physicsWorld:setCallbacks(nil, nil, nil, nil)
+ self.physicsWorld:setCallbacks(
+ function(fa, fb, contact)
+ local udA, udB = fa:getUserData(), fb:getUserData()
+ local nx, ny = contact:getNormal()
+ self:_onBeginContact(udA, udB, nx, ny, contact)
+ end,
+ function(fa, fb, contact)
+ local udA, udB = fa:getUserData(), fb:getUserData()
+ self:_onEndContact(udA, udB, contact)
+ end
+ )
self.tilemap = Tilemap:new(mapPath, tilesetPath)
self.mapData = self.tilemap:getMapData()
@@ -39,18 +54,38 @@ function World:load(mapPath, tilesetPath)
local groundLayer = self.tilemap:getGroundLayer()
if groundLayer and groundLayer.data then
- local w = self.mapData.width or 0
+ local mapW = self.mapData.width or 0
+ local mapH = self.mapData.height or 0
local data = groundLayer.data
- for i, gid in ipairs(data) do
- if gid and gid ~= 0 then
- local idx = i - 1
- local tx = idx % w
- local ty = math.floor(idx / w)
- local x = tx * tileWidth
- local y = ty * tileHeight
- local tileEntity = Entity:new(x, y, tileWidth, tileHeight)
- tileEntity:enablePhysics(self.physicsWorld, "static")
- table.insert(self.groundEntities, tileEntity)
+
+ local solid = {}
+ for row = 0, mapH - 1 do
+ solid[row] = {}
+ for col = 0, mapW - 1 do
+ local idx = row * mapW + col + 1
+ local gid = data[idx]
+ solid[row][col] = (gid and gid ~= 0)
+ end
+ end
+
+ for row = 0, mapH - 1 do
+ local col = 0
+ while col < mapW do
+ if solid[row][col] then
+ local startCol = col
+ while col < mapW and solid[row][col] do
+ col = col + 1
+ end
+ local stripWidth = (col - startCol) * tileWidth
+ local x = startCol * tileWidth
+ local y = row * tileHeight
+ local strip = Entity:new(x, y, stripWidth, tileHeight)
+ strip:enablePhysics(self.physicsWorld, "static")
+ strip.fixture:setUserData("ground")
+ table.insert(self.groundEntities, strip)
+ else
+ col = col + 1
+ end
end
end
end
@@ -62,8 +97,7 @@ function World:load(mapPath, tilesetPath)
for _, spawn in ipairs(spawns) do
local entityType = spawn:get("entity", "")
if entityType == "Player" and not self.player then
- self.player = Entity:new(spawn.x, spawn.y, spawn.width, spawn.height)
- self.player:enablePhysics(self.physicsWorld, "dynamic")
+ self.player = Player.new(self.physicsWorld, spawn.x, spawn.y)
self.player.isPlayer = true
table.insert(self.entities, self.player)
else
@@ -78,6 +112,82 @@ function World:load(mapPath, tilesetPath)
end
+function World:_addContact(entity, kind)
+ self.contactCounts[entity] = self.contactCounts[entity] or { floor = 0, wall = 0, ceiling = 0 }
+ self.contactCounts[entity][kind] = (self.contactCounts[entity][kind] or 0) + 1
+ entity.contact = entity.contact or { floor = 0, wall = 0, ceiling = 0 }
+ entity.contact[kind] = 1
+end
+
+function World:_removeContact(entity, kind)
+ self.contactCounts[entity] = self.contactCounts[entity] or { floor = 0, wall = 0, ceiling = 0 }
+ self.contactCounts[entity][kind] = math.max(0, (self.contactCounts[entity][kind] or 0) - 1)
+ entity.contact = entity.contact or { floor = 0, wall = 0, ceiling = 0 }
+ entity.contact[kind] = (self.contactCounts[entity][kind] > 0) and 1 or 0
+end
+
+function World:_normalToContactType(nx, ny)
+ local ax, ay = math.abs(nx), math.abs(ny)
+ if ay >= ax then
+ if ny < -0.3 then return "floor" end
+ if ny > 0.3 then return "ceiling" end
+ else
+ if ax > 0.3 then return "wall" end
+ end
+ return nil
+end
+
+function World:_isTrackedEntity(ud)
+ return type(ud) == "table" and (ud.body ~= nil or ud.x ~= nil)
+end
+
+function World:_isPlayerLike(ud)
+ return type(ud) == "table" and ud.jump ~= nil
+end
+
+function World:_onBeginContact(udA, udB, nx, ny, contact)
+ if udA == "ground" and self:_isTrackedEntity(udB) then
+ local kind = self:_normalToContactType(nx, ny)
+ if kind then
+ self:_addContact(udB, kind)
+ self.contactEntity[contact] = udB
+ self.contactKind[contact] = kind
+ end
+ self.groundContacts[udB] = (self.groundContacts[udB] or 0) + 1
+ if self:_isPlayerLike(udB) then udB.grounded = true end
+ elseif udB == "ground" and self:_isTrackedEntity(udA) then
+ local kind = self:_normalToContactType(-nx, -ny)
+ if kind then
+ self:_addContact(udA, kind)
+ self.contactEntity[contact] = udA
+ self.contactKind[contact] = kind
+ end
+ self.groundContacts[udA] = (self.groundContacts[udA] or 0) + 1
+ if self:_isPlayerLike(udA) then udA.grounded = true end
+ end
+end
+
+function World:_onEndContact(udA, udB, contact)
+ local ent = self.contactEntity[contact]
+ local kind = ent and self.contactKind[contact]
+ if ent and kind then
+ self:_removeContact(ent, kind)
+ self.contactEntity[contact] = nil
+ self.contactKind[contact] = nil
+ end
+ if udA == "ground" and self:_isTrackedEntity(udB) then
+ self.groundContacts[udB] = math.max(0, (self.groundContacts[udB] or 0) - 1)
+ if self:_isPlayerLike(udB) then
+ udB.grounded = (self.groundContacts[udB] or 0) > 0
+ end
+ elseif udB == "ground" and self:_isTrackedEntity(udA) then
+ self.groundContacts[udA] = math.max(0, (self.groundContacts[udA] or 0) - 1)
+ if self:_isPlayerLike(udA) then
+ udA.grounded = (self.groundContacts[udA] or 0) > 0
+ end
+ end
+end
+
function World:setCamera(camera)
self.camera = camera
self.camera:setLimits(self.tilemap:getCameraLimits())
@@ -134,10 +244,29 @@ function World:draw()
if e.draw then e:draw() else World.drawEntityDefault(e) end
end
+ if DEBUG then
+ World.drawPhysicsBodyOutlines(self.entities)
+ World.drawPhysicsBodyOutlines(self.groundEntities)
+ end
+
drawTileLayer(self.tilemap:getForegroundLayer(), tw, th, tilesetImage, tileQuads)
end
--- TODO remove. draw method handled by each entity
+function World.drawPhysicsBodyOutlines(entityList)
+ if not entityList then return end
+ love.graphics.setColor(0, 1, 0, 1)
+ for _, e in ipairs(entityList) do
+ local body = e.body
+ if body and e.physicsWidth and e.physicsHeight then
+ local cx, cy = body:getX(), body:getY()
+ local x = cx - e.physicsWidth / 2
+ local y = cy - e.physicsHeight / 2
+ love.graphics.rectangle("line", x, y, e.physicsWidth, e.physicsHeight)
+ end
+ end
+ love.graphics.setColor(1, 1, 1, 1)
+end
+
function World.drawEntityDefault(entity)
love.graphics.setColor(0.2, 0.6, 1, 1)
if entity.isPlayer then