diff options
| -rw-r--r-- | enemy.lua | 1 | ||||
| -rw-r--r-- | hud.lua | 146 | ||||
| -rw-r--r-- | main.lua | 51 | ||||
| -rw-r--r-- | player.lua | 48 | ||||
| -rw-r--r-- | spike.lua | 1 | ||||
| -rw-r--r-- | world.lua | 22 |
6 files changed, 265 insertions, 4 deletions
@@ -5,6 +5,7 @@ setmetatable(Enemy, { __index = Entity }) function Enemy.new(spawnX, spawnY, width, height, physicsWidth, physicsHeight) local self = setmetatable(Entity:new(spawnX or 0, spawnY or 0, width or 16, height or 16, physicsWidth or width or 8, physicsHeight or height or 8), Enemy) + self.isEnemy = true self.direction = math.random(1, 2) == 1 and 1 or -1 self.speed = 20 self.turnDelay = 0.35 @@ -0,0 +1,146 @@ +local Textbox = require("textbox") + +local HUD = {} +HUD.__index = HUD + +HUD.slots = { + { enabled = false }, + { enabled = false }, + { enabled = false }, + { enabled = false }, +} + +local SLOT_SIZE = 16 +local SLOT_SPACING = 8 +local LOREM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + +local function createMissingTexture() + local size = SLOT_SIZE + local half = size / 2 + local imgData = love.image.newImageData(size, size) + for y = 0, size - 1 do + for x = 0, size - 1 do + local inBlack = (x < half and y < half) or (x >= half and y >= half) + if inBlack then + imgData:setPixel(x, y, 0, 0, 0, 1) + else + imgData:setPixel(x, y, 1, 0, 1, 1) + end + end + end + local img = love.graphics.newImage(imgData) + img:setFilter("nearest", "nearest") + return img +end + +function HUD:new() + local self = setmetatable({}, HUD) + self.missingTexture = createMissingTexture() + self.textbox = Textbox:new() + self.hoveredSlot = nil + self.slotRects = {} + self.screenW = 0 + self.screenH = 0 + self.drawScale = 1 + return self +end + +function HUD:computeSlotRects(screenW, screenH, drawScale) + local slotW = SLOT_SIZE * drawScale + local slotH = SLOT_SIZE * drawScale + local spacing = SLOT_SPACING * drawScale + local totalW = 4 * slotW + 3 * spacing + local startX = math.floor((screenW - totalW) / 2) + local startY = math.floor(screenH / 2 - slotH / 2) + local rects = {} + for i = 1, 4 do + rects[i] = { + x = startX + (i - 1) * (slotW + spacing), + y = startY, + w = slotW, + h = slotH, + } + end + return rects +end + +function HUD:update(dt, screenW, screenH, drawScale) + self.screenW = screenW + self.screenH = screenH + self.drawScale = drawScale + self.slotRects = self:computeSlotRects(screenW, screenH, drawScale) + + local mx, my = love.mouse.getPosition() + local prevHovered = self.hoveredSlot + self.hoveredSlot = nil + for i, rect in ipairs(self.slotRects) do + if mx >= rect.x and mx < rect.x + rect.w + and my >= rect.y and my < rect.y + rect.h then + self.hoveredSlot = i + break + end + end + + if self.hoveredSlot and self.hoveredSlot ~= prevHovered then + local rect = self.slotRects[self.hoveredSlot] + local centerX = screenW / 2 + local topY = rect.y + rect.h + 8 * drawScale + self.textbox:show(LOREM, { centerX, topY, "center" }, "show", { + fontSize = 16, + centeredText = true, + wrapToFit = true, + maxChars = 40, + }) + self.textbox.y = topY + self.textbox.height / 2 + elseif not self.hoveredSlot and prevHovered then + self.textbox:hide() + end + + self.textbox:update(dt) +end + +function HUD:mousepressed(mx, my, button) + if button ~= 1 then return end + for i, rect in ipairs(self.slotRects) do + if mx >= rect.x and mx < rect.x + rect.w + and my >= rect.y and my < rect.y + rect.h then + HUD.slots[i].enabled = not HUD.slots[i].enabled + break + end + end +end + +function HUD:draw() + local screenW = self.screenW + local screenH = self.screenH + local drawScale = self.drawScale + + love.graphics.setColor(0, 0, 0, 0.4) + love.graphics.rectangle("fill", 0, 0, screenW, screenH) + + for i, rect in ipairs(self.slotRects) do + love.graphics.setColor(1, 1, 1, 1) + love.graphics.draw(self.missingTexture, rect.x, rect.y, 0, drawScale, drawScale) + + if HUD.slots[i].enabled then + love.graphics.setColor(0, 1, 0, 0.3) + love.graphics.rectangle("fill", rect.x, rect.y, rect.w, rect.h) + end + + if self.hoveredSlot == i then + love.graphics.setColor(1, 1, 0, 1) + love.graphics.setLineWidth(2) + else + love.graphics.setColor(1, 1, 1, 0.5) + love.graphics.setLineWidth(1) + end + love.graphics.rectangle("line", rect.x, rect.y, rect.w, rect.h) + end + + love.graphics.setLineWidth(1) + love.graphics.setColor(1, 1, 1, 1) + + self.textbox:draw() +end + +return HUD @@ -1,7 +1,7 @@ local VIRTUAL_WIDTH, VIRTUAL_HEIGHT = 16*10*5, 9*10*5 local CANVAS_PADDING = 6 -DEBUG = true +DEBUG = false local CANVAS_WIDTH = VIRTUAL_WIDTH + CANVAS_PADDING local CANVAS_HEIGHT = VIRTUAL_HEIGHT + CANVAS_PADDING @@ -18,6 +18,9 @@ local shaderEnabled = true local cameraModule = require("camera") local camera = nil local fonts = require("fonts") +local HUD = require("hud") +local hud = nil +local previousState = nil local currentState = "game" local currentMapPath = "assets/maps/tilemap.lua" @@ -30,7 +33,8 @@ local world = nil local states = { game = {}, menu = {}, - settings = {} + settings = {}, + hud = {}, } local function recalcScale(w, h) @@ -48,6 +52,7 @@ function states.game.load() local target = world:getPlayer() or { x = 0, y = 0, width = 16, height = 16 } camera = cameraModule:new(target, VIRTUAL_WIDTH/2, VIRTUAL_HEIGHT/2, true, WORLD_TO_CANVAS) world:setCamera(camera) + hud = HUD:new() end function states.game.update(dt) @@ -82,6 +87,19 @@ function states.settings.draw() love.graphics.print("Settings", 10, 10) end +function states.hud.update(dt) + if hud then + local w, h = love.graphics.getWidth(), love.graphics.getHeight() + hud:update(dt, w, h, finalScale) + end +end + +function states.hud.draw() + if camera then camera:set() end + if world then world:draw() end + if camera then camera:unset() end +end + function love.load() love.graphics.setDefaultFilter("nearest", "nearest") love.window.setTitle("Openformer") @@ -100,6 +118,8 @@ function love.load() local w, h = love.graphics.getWidth(), love.graphics.getHeight() recalcScale(w, h) + love.mouse.setVisible(false) + local state = states[currentState] if state and state.load then state.load() end end @@ -123,9 +143,28 @@ function love.keypressed(key, scancode, isrepeat) if key == "f1" and DEBUG then shaderEnabled = not shaderEnabled end + if key == "tab" and not isrepeat then + if currentState == "game" then + previousState = currentState + currentState = "hud" + love.mouse.setVisible(true) + elseif currentState == "hud" then + currentState = previousState or "game" + previousState = nil + love.mouse.setVisible(false) + end + 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 + if currentState == "game" then + local player = world and world:getPlayer() + if player then player:jump() end + end + end +end + +function love.mousepressed(x, y, button) + if currentState == "hud" and hud then + hud:mousepressed(x, y, button) end end @@ -162,6 +201,10 @@ function love.draw() love.graphics.setShader() end + if currentState == "hud" and hud then + hud:draw() + end + if DEBUG then local fps = love.timer.getFPS() local shaderLoaded = smoothCameraShader ~= nil @@ -15,6 +15,9 @@ function Player.new(world, spawnX, spawnY) 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.directionState = { 'side', 'up', 'down', 'side_up', 'side_down' } self.directionIndex = 1 self.direction = self.directionState[self.directionIndex] @@ -65,6 +68,9 @@ function Player.new(world, spawnX, spawnY) self.isInLiquid = false self.wasInLiquid = false self.waterSurfaceContact = false + + self.isDead = false + self.respawnTimer = 0 return self end @@ -98,6 +104,17 @@ 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() @@ -196,12 +213,43 @@ function Player:update(dt) 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 @@ -5,6 +5,7 @@ setmetatable(Spike, { __index = Entity }) function Spike.new(entity) local self = setmetatable(Entity:new(entity.x, entity.y, entity.width, entity.height), Spike) + self.isSpike = true return self end @@ -33,6 +33,7 @@ function World:new() self.contactCounts = {} self.contactEntity = {} self.contactKind = {} + self.pendingPlayerDeath = nil self.playerTextbox = Textbox:new() return self end @@ -193,6 +194,10 @@ function World:_isPlayerLike(ud) return type(ud) == "table" and ud.jump ~= nil end +function World:_isHazard(ud) + return type(ud) == "table" and (ud.isSpike or ud.isEnemy) +end + function World:_onBeginContact(udA, udB, nx, ny, contact) if udA == "ground" and self:_isTrackedEntity(udB) then local kind = self:_normalToContactType(nx, ny) @@ -239,6 +244,18 @@ function World:_onBeginContact(udA, udB, nx, ny, contact) self.waterContacts[udA] = (self.waterContacts[udA] or 0) + 1 doWaterSplash(udB, udA) end + + local playerEntity, hazardNx, hazardNy + if self:_isHazard(udA) and self:_isPlayerLike(udB) then + playerEntity = udB + hazardNx, hazardNy = nx, ny + elseif self:_isHazard(udB) and self:_isPlayerLike(udA) then + playerEntity = udA + hazardNx, hazardNy = -nx, -ny + end + if playerEntity and not playerEntity.isDead then + self.pendingPlayerDeath = { nx = hazardNx, ny = hazardNy } + end end function World:_onEndContact(udA, udB, contact) @@ -523,6 +540,11 @@ function World:update(dt) if not self.physicsWorld then return end self.physicsWorld:update(PHYSICS_DT) + if self.pendingPlayerDeath and self.player then + self.player:die(self.pendingPlayerDeath.nx, self.pendingPlayerDeath.ny) + self.pendingPlayerDeath = nil + end + for _, e in ipairs(self.entities) do if e.body then e:syncFromPhysicsBody() |
