eitmer/bundle/main.lua

1223 lines
36 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

local debug = false
local cavern = {}
local tiletypes = {unknown = 0, empty = 1, wall = 2, orb = 3, slime = 4}
local visibility_map = {}
local remembered_cavern = {}
local spawn_x = nil
local spawn_y = nil
local directions = {up = 0, upleft = 1, left = 2, downleft = 3, down = 4, downright = 5, right = 6, upright = 7, inplace = 8}
local player_x = nil
local player_y = nil
local player_direction = nil
local hp = nil
local flash_player_crit_counter = 0
local flash_player_damaged_counter = 0
local player_sprite = nil
local player_sprite_crit = nil
local player_sprite_damaged = nil
local player_animation_flip = false
local heart_sprite = nil
local slimes = {}
local last_key_pressed = nil
local move_repeat_counter = nil
local gamemodes = {normal = 0, won = 1, lost = 2, config = 3}
local game_mode = nil
local configuration_steps = {quit = -4, restart = -3, configure = -2, autoexplore = -1}
local configuration_step = nil
local direction_keys = {}
direction_keys['i'] = directions.up
direction_keys['u'] = directions.upleft
direction_keys['j'] = directions.left
direction_keys['m'] = directions.downleft
direction_keys['k'] = directions.down
direction_keys['.'] = directions.downright
direction_keys['l'] = directions.right
direction_keys['o'] = directions.upright
direction_keys['space'] = directions.inplace
local control_keys = {quit = 'q', restart = 'r', configure = 'c', autoexplore = 'a'}
local win_image = nil
local win_is_raster = true
-- ------------------------------------------------------------------
-- Cavern generation
-- ------------------------------------------------------------------
function generateCavern()
repeat
cavern = {}
cavern.width = 80
cavern.height = 60
-- Create a cavern.width × cavern.height array filled with tiletypes.unknown
for x = 1, cavern.width do
local list = {}
for y = 1, cavern.height do
table.insert(list, tiletypes.unknown)
end
table.insert(cavern, list)
end
-- Fill top and bottom edges with walls
for x = 1, cavern.width do
cavern[x][1] = tiletypes.wall
cavern[x][cavern.height] = tiletypes.wall
end
-- Fill the left and right edges with walls
for y = 1, cavern.height do
cavern[1][y] = tiletypes.wall
cavern[cavern.width][y] = tiletypes.wall
end
local queue = {}
local size = 1
-- Limit spawn to locations not on the edge walls
spawn_x = math.random(2, cavern.width - 1)
spawn_y = math.random(2, cavern.height - 1)
cavern[spawn_x][spawn_y] = tiletypes.empty
table.insert(queue, {x = spawn_x, y = spawn_y - 1})
table.insert(queue, {x = spawn_x, y = spawn_y + 1})
table.insert(queue, {x = spawn_x - 1, y = spawn_y})
table.insert(queue, {x = spawn_x + 1, y = spawn_y})
repeat
queue, size = stepCavernGeneration(queue, size)
until #queue == 0
until size > 200
end
function stepCavernGeneration(queue, size)
-- Tuning this parameter will change how open and cavernous vs. closed and corridory the
-- cavern will be
local spread_probability = 0.6
local new_queue = {}
local new_size = size
for i, location in ipairs(queue) do
if cavern[location.x][location.y] == tiletypes.unknown then
-- We haven't covered this tile yet
if math.random() < spread_probability then
-- Empty
cavern[location.x][location.y] = tiletypes.empty
size = size + 1
-- Up
if location.y - 1 >= 1 then
table.insert(new_queue, {x = location.x, y = location.y - 1})
end
-- Down
if location.y + 1 <= cavern.height then
table.insert(new_queue, {x = location.x, y = location.y + 1})
end
-- Left
if location.x - 1 >= 1 then
table.insert(new_queue, {x = location.x - 1, y = location.y})
end
-- Right
if location.x + 1 <= cavern.width then
table.insert(new_queue, {x = location.x + 1, y = location.y})
end
else
-- Wall
cavern[location.x][location.y] = tiletypes.wall
end
end
end
return new_queue, size
end
-- ------------------------------------------------------------------
-- Spawning objects
-- ------------------------------------------------------------------
function spawnPlayer()
player_x = spawn_x
player_y = spawn_y
-- Check what direction we can spawn the player
if passable(player_x, player_y + 1) then
-- There's space under, spawn facing up
player_direction = directions.up
elseif passable(player_x + 1, player_y) then
-- There's space to the right, spawn facing left
player_direction = directions.left
elseif passable(player_x - 1, player_y) then
-- There's space to the left, spawn facing right
player_direction = directions.right
elseif passable(player_x, player_y - 1) then
-- There's space above, spawn facing down
player_direction = directions.down
else
error("Cavern is weirdly generated and player cannot spawn")
end
hp = 3
end
function spawnOrb()
local orb_x = nil
local orb_y = nil
repeat
orb_x = math.random(2, cavern.width - 1)
orb_y = math.random(2, cavern.height - 1)
until cavern[orb_x][orb_y] == tiletypes.empty
cavern[orb_x][orb_y] = tiletypes.orb
end
function spawnSlimes()
-- Try spawning 20 slimes, but if they don't hit any location then don't care
slimes = {}
for i = 1, 20 do
local x = math.random(2, cavern.width - 1)
local y = math.random(2, cavern.height - 1)
if cavern[x][y] == tiletypes.empty then
cavern[x][y] = tiletypes.slime
table.insert(slimes, {x = x, y = y})
end
end
end
-- ------------------------------------------------------------------
-- Visibility
-- ------------------------------------------------------------------
function generateVisibilityMap()
-- Start by creating a new visibility map (since we'll have to recompute everything anyways)
visibility_map = {}
for x = 1, cavern.width do
local list = {}
for y = 1, cavern.height do
table.insert(list, false)
end
table.insert(visibility_map, list)
end
-- Handle visibility from the head
local queue = {}
visibility_map[player_x][player_y] = true
if player_direction == directions.up then
-- ⌜^⌝
-- <o>
table.insert(queue, {x = player_x - 1, y = player_y - 1, direction = directions.upleft})
table.insert(queue, {x = player_x, y = player_y - 1, direction = directions.up})
table.insert(queue, {x = player_x + 1, y = player_y - 1, direction = directions.upright})
table.insert(queue, {x = player_x - 1, y = player_y, direction = directions.left})
table.insert(queue, {x = player_x + 1, y = player_y, direction = directions.right})
elseif player_direction == directions.left then
-- ⌜^
-- <o
-- ⌞v
table.insert(queue, {x = player_x - 1, y = player_y - 1, direction = directions.upleft})
table.insert(queue, {x = player_x, y = player_y - 1, direction = directions.up})
table.insert(queue, {x = player_x - 1, y = player_y, direction = directions.left})
table.insert(queue, {x = player_x - 1, y = player_y + 1, direction = directions.downleft})
table.insert(queue, {x = player_x, y = player_y + 1, direction = directions.down})
elseif player_direction == directions.down then
-- <o>
-- ⌞v⌟
table.insert(queue, {x = player_x - 1, y = player_y, direction = directions.left})
table.insert(queue, {x = player_x + 1, y = player_y, direction = directions.right})
table.insert(queue, {x = player_x - 1, y = player_y + 1, direction = directions.downleft})
table.insert(queue, {x = player_x, y = player_y + 1, direction = directions.down})
table.insert(queue, {x = player_x + 1, y = player_y + 1, direction = directions.downright})
elseif player_direction == directions.right then
-- ^⌝
-- o>
-- v⌟
table.insert(queue, {x = player_x, y = player_y - 1, direction = directions.up})
table.insert(queue, {x = player_x + 1, y = player_y - 1, direction = directions.upright})
table.insert(queue, {x = player_x + 1, y = player_y, direction = directions.right})
table.insert(queue, {x = player_x, y = player_y + 1, direction = directions.down})
table.insert(queue, {x = player_x + 1, y = player_y + 1, direction = directions.downright})
else
error("Player facing an impossible direction")
end
repeat
queue = stepVisibilityMapGeneration(queue)
until #queue == 0
-- Handle "visibility" from the feet
local body_x, body_y = getBodyLocation()
visibility_map[body_x][body_y] = true
if player_direction == directions.up then
-- <x>
-- v
visibility_map[body_x - 1][body_y] = true
visibility_map[body_x + 1][body_y] = true
visibility_map[body_x][body_y + 1] = true
elseif player_direction == directions.left then
-- ^
-- x>
-- v
visibility_map[body_x][body_y - 1] = true
visibility_map[body_x + 1][body_y] = true
visibility_map[body_x][body_y + 1] = true
elseif player_direction == directions.down then
-- ^
-- <x>
visibility_map[body_x][body_y - 1] = true
visibility_map[body_x - 1][body_y] = true
visibility_map[body_x + 1][body_y] = true
elseif player_direction == directions.right then
-- ^
-- <x
-- v
visibility_map[body_x][body_y - 1] = true
visibility_map[body_x - 1][body_y] = true
visibility_map[body_x][body_y + 1] = true
else
error("Player facing an impossible direction")
end
end
function stepVisibilityMapGeneration(queue)
-- Do a modified floodfill, where each cell will only expand in its given direction, not
-- every direction like in a normal floodfill
local new_queue = {}
for i, item in ipairs(queue) do
if item.direction == directions.up then
visibility_map[item.x][item.y] = true
if seethrough(item.x, item.y) then
table.insert(new_queue, {x = item.x, y = item.y - 1, direction = directions.up})
end
elseif item.direction == directions.left then
visibility_map[item.x][item.y] = true
if seethrough(item.x, item.y) then
table.insert(new_queue, {x = item.x - 1, y = item.y, direction = directions.left})
end
elseif item.direction == directions.down then
visibility_map[item.x][item.y] = true
if seethrough(item.x, item.y) then
table.insert(new_queue, {x = item.x, y = item.y + 1, direction = directions.down})
end
elseif item.direction == directions.right then
visibility_map[item.x][item.y] = true
if seethrough(item.x, item.y) then
table.insert(new_queue, {x = item.x + 1, y = item.y, direction = directions.right})
end
elseif item.direction == directions.upleft then
-- Check that we could have gotten here also by moving only 4-ways
--
-- The possible cases are:
-- (o represents old position, x is new, and # is a wall)
--
-- x x# x x#
-- o o #o #o
--
-- In the first three, the movement is possible, but in the last one
-- it is not. We can therefore check whether the movement is valid
-- by testing if either the cell to our right or below us (the
-- directions opposite to the ones in an up.left movement) is empty
if seethrough(item.x + 1, item.y) or seethrough(item.x, item.y + 1) then
visibility_map[item.x][item.y] = true
if seethrough(item.x, item.y) then
-- ⌜^
-- ⌜ <x
table.insert(new_queue, {x = item.x - 1, y = item.y - 1, direction = directions.upleft})
table.insert(new_queue, {x = item.x, y = item.y - 1, direction = directions.up})
table.insert(new_queue, {x = item.x - 1, y = item.y, direction = directions.left})
end
end
elseif item.direction == directions.downleft then
-- See under item.direction == directions.upleft
if seethrough(item.x + 1, item.y) or seethrough(item.x, item.y - 1) then
visibility_map[item.x][item.y] = true
if seethrough(item.x, item.y) then
-- ⌞ <x
-- ⌞v
table.insert(new_queue, {x = item.x - 1, y = item.y, direction = directions.left})
table.insert(new_queue, {x = item.x - 1, y = item.y + 1, direction = directions.downleft})
table.insert(new_queue, {x = item.x, y = item.y + 1, direction = directions.down})
end
end
elseif item.direction == directions.downright then
-- See under item.direction == directions.upleft
if seethrough(item.x - 1, item.y) or seethrough(item.x, item.y - 1) then
visibility_map[item.x][item.y] = true
if seethrough(item.x, item.y) then
-- ⌟ x>
-- v⌟
table.insert(new_queue, {x = item.x + 1, y = item.y, direction = directions.right})
table.insert(new_queue, {x = item.x, y = item.y + 1, direction = directions.down})
table.insert(new_queue, {x = item.x + 1, y = item.y + 1, direction = directions.downright})
end
end
elseif item.direction == directions.upright then
-- See under item.direction == directions.upleft
if seethrough(item.x - 1, item.y) or seethrough(item.x, item.y + 1) then
visibility_map[item.x][item.y] = true
if seethrough(item.x, item.y) then
-- ^⌝
-- ⌝ x>
table.insert(new_queue, {x = item.x, y = item.y - 1, direction = directions.up})
table.insert(new_queue, {x = item.x + 1, y = item.y - 1, direction = directions.upright})
table.insert(new_queue, {x = item.x + 1, y = item.y, direction = directions.right})
end
end
else
error("Visibility floodfill item travelling in an impossible direction")
end
end
return new_queue
end
function initializeRememberedCavern()
remembered_cavern = {}
for x = 1, cavern.width do
local list = {}
for y = 1, cavern.height do
table.insert(list, nil)
end
table.insert(remembered_cavern, list)
end
end
function rememberVisible()
for x, list in ipairs(visibility_map) do
for y, visible in ipairs(list) do
if visible then
remembered_cavern[x][y] = cavern[x][y]
end
end
end
end
-- ------------------------------------------------------------------
-- Collision
-- ------------------------------------------------------------------
function passable(x, y)
return cavern[x][y] == tiletypes.empty or cavern[x][y] == tiletypes.orb
end
function seethrough(x, y)
return cavern[x][y] == tiletypes.empty or cavern[x][y] == tiletypes.orb or cavern[x][y] == tiletypes.slime
end
function collidedPlayerOrb()
local body_x, body_y = getBodyLocation()
return cavern[player_x][player_y] == tiletypes.orb or cavern[body_x][body_y] == tiletypes.orb
end
-- ------------------------------------------------------------------
-- Player helper functions
-- ------------------------------------------------------------------
function getBodyLocation(x, y, facing)
if x == nil then x = player_x end
if y == nil then y = player_y end
if facing == nil then facing = player_direction end
if facing == directions.up then
-- Facing up, body is down
return x, y + 1
elseif facing == directions.left then
-- Facing left, body is right
return x + 1, y
elseif facing == directions.down then
-- Facing down, body is up
return x, y - 1
elseif facing == directions.right then
-- Facing right, body is left
return x - 1, y
else
error("Player facing an impossible direction")
end
end
function movePlayer(direction)
local dx = 0
local dy = 0
local new_direction = direction
-- If we are moving backwards, keep the old direction of facing
if player_direction == directions.up and direction == directions.down or
player_direction == directions.down and direction == directions.up or
player_direction == directions.left and direction == directions.right or
player_direction == directions.right and direction == directions.left then
new_direction = player_direction
end
if direction == directions.up then
dy = -1
elseif direction == directions.down then
dy = 1
elseif direction == directions.left then
dx = -1
elseif direction == directions.right then
dx = 1
elseif direction == directions.upleft then
if player_direction == directions.up then
dx = -1
new_direction = directions.left
elseif player_direction == directions.left then
dy = -1
new_direction = directions.up
elseif player_direction == directions.down then
-- ## ##
-- x# xo#
-- #o# # #
dy = -1
new_direction = directions.right
elseif player_direction == directions.right then
-- # # #x#
-- #xo #o
-- ## ##
dx = -1
new_direction = directions.down
else
error("Player facing an impossible direction")
end
elseif direction == directions.downleft then
if player_direction == directions.up then
-- #o# # #
-- x# xo#
-- ## ##
dy = 1
new_direction = directions.right
elseif player_direction == directions.left then
dy = 1
new_direction = directions.down
elseif player_direction == directions.down then
dx = -1
new_direction = directions.left
elseif player_direction == directions.right then
-- ## ##
-- #xo #o
-- # # #x#
dx = -1
new_direction = directions.up
else
error("Player facing an impossible direction")
end
elseif direction == directions.downright then
if player_direction == directions.up then
-- #o# # #
-- #x #ox
-- ## ##
dy = 1
new_direction = directions.left
elseif player_direction == directions.left then
-- ## ##
-- ox# o#
-- # # #x#
dx = 1
new_direction = directions.up
elseif player_direction == directions.down then
dx = 1
new_direction = directions.right
elseif player_direction == directions.right then
dy = 1
new_direction = directions.down
else
error("Player facing an impossible direction")
end
elseif direction == directions.upright then
if player_direction == directions.up then
dx = 1
new_direction = directions.right
elseif player_direction == directions.left then
-- # # #x#
-- ox# o#
-- ## ##
dx = 1
new_direction = directions.down
elseif player_direction == directions.down then
-- ## ##
-- #x #ox
-- #o# # #
dy = -1
new_direction = directions.left
elseif player_direction == directions.right then
dy = -1
new_direction = directions.up
else
error("Player facing an impossible direction")
end
else
error("Player moving in an impossible direction")
end
local body_x, body_y = getBodyLocation(player_x + dx, player_y + dy, new_direction)
if passable(player_x + dx, player_y + dy) and passable(body_x, body_y) then
player_x = player_x + dx
player_y = player_y + dy
player_direction = new_direction
end
end
-- ------------------------------------------------------------------
-- Gameloop functions
-- ------------------------------------------------------------------
function newGame()
game_mode = gamemodes.normal
generateCavern()
spawnPlayer()
spawnOrb()
spawnSlimes()
generateVisibilityMap()
initializeRememberedCavern()
if collidedPlayerOrb() then
game_mode = gamemodes.won
end
end
function moveSlimes()
for i, slime in pairs(slimes) do
local dx = 0
local dy = 0
local to_player_dx = player_x - slime.x
local to_player_dy = player_y - slime.y
if math.abs(to_player_dx) + math.abs(to_player_dy) <= 5 and math.random() < 0.7 then
-- If at max 5 moves away from player, book for it (likely)
if math.abs(to_player_dx) > math.abs(to_player_dy) then
-- Needs to move more in the horizontal direction → move horizontally
if to_player_dx < 0 then
dx = -1
else
dx = 1
end
else
-- Otherwise move vertically
if to_player_dy < 0 then
dy = -1
else
dy = 1
end
end
else
local movement = math.random(0, 3)
if movement == 0 then
-- Up
dy = -1
elseif movement == 1 then
-- Left
dx = -1
elseif movement == 2 then
-- Down
dy = 1
elseif movement == 3 then
-- Right
dx = 1
end
end
-- We test for empty instead of using passable(), since we are overwriting a tile
if cavern[slime.x + dx][slime.y + dy] == tiletypes.empty then
cavern[slime.x + dx][slime.y + dy] = tiletypes.slime
cavern[slime.x][slime.y] = tiletypes.empty
slimes[i] = {x = slime.x + dx, y = slime.y + dy}
end
end
end
function killSlime()
local body_x, body_y = getBodyLocation()
for i, slime in pairs(slimes) do
if slime.x == player_x and slime.y == player_y or slime.x == body_x and slime.y == body_y then
cavern[slime.x][slime.y] = tiletypes.empty
slimes[i] = nil
end
end
end
function step(direction)
if game_mode == gamemodes.normal then
rememberVisible()
if direction ~= directions.inplace then
movePlayer(direction)
end
moveSlimes()
generateVisibilityMap()
if collidedPlayerOrb() then
game_mode = gamemodes.won
end
local body_x, body_y = getBodyLocation()
if cavern[body_x][body_y] == tiletypes.slime then
killSlime()
hp = hp - 1
if hp == 0 then
flash_player_damaged_counter = 1
else
flash_player_damaged_counter = 0.2
end
elseif cavern[player_x][player_y] == tiletypes.slime then
killSlime()
if math.random() < 0.2 then
flash_player_crit_counter = 0.2
else
hp = hp - 1
if hp == 0 then
flash_player_damaged_counter = 1
else
flash_player_damaged_counter = 0.2
end
end
end
player_animation_flip = not player_animation_flip
end
end
-- ------------------------------------------------------------------
-- Autoexplore
-- ------------------------------------------------------------------
function autoexplore()
-- Use a pathfinding algo where each unknown but accessible space gets labelled 0, and
-- then each known accessible space gets labelled with min(neighbours) + 1. Once we have
-- computed that table, we can then move towards closest unknown accessible space by
-- moving to our neighbouring space with lowest score
local distances = {}
for x = 1, cavern.width do
local list = {}
for y = 1, cavern.height do
table.insert(list, {value = nil, direction = directions.inplace})
end
table.insert(distances, list)
end
local queue = {}
function enqueue(queue, x, y, value, direction)
if 1 < x and x < cavern.width and
1 < y and y < cavern.height and
cavern[x][y] ~= tiletypes.wall then
table.insert(queue, {x = x, y = y, value = value, direction = direction})
end
end
function neighbors(q, x, y, v)
-- The directions are reversed, because they tell where to go to get to the
-- unexplored areas
enqueue(q, x, y - 1, v, directions.down)
enqueue(q, x - 1, y, v, directions.right)
enqueue(q, x, y + 1, v, directions.up)
enqueue(q, x + 1, y, v, directions.left)
end
for x, list in ipairs(visibility_map) do
for y, visible in ipairs(list) do
if not visible and remembered_cavern[x][y] == nil then
distances[x][y] = {value = 0, direction = directions.inplace}
neighbors(queue, x, y, 1)
end
end
end
repeat
local new_queue = {}
for i, item in ipairs(queue) do
-- Write the lowest distance to each tile
if distances[item.x][item.y].value == nil or distances[item.x][item.y].value > item.value then
distances[item.x][item.y] = {value = item.value, direction = item.direction}
neighbors(new_queue, item.x, item.y, item.value + 1)
end
end
queue = new_queue
until #queue == 0
local direction = distances[player_x][player_y].direction
local body_x, body_y = getBodyLocation()
-- If we are backing up, consider feet
if player_direction == directions.up and direction == directions.down then
direction = distances[body_x][body_y].direction
if direction == directions.left then
direction = directions.downleft
elseif direction == directions.right then
direction = directions.downright
else
direction = directions.down
end
elseif player_direction == directions.left and direction == directions.right then
direction = distances[body_x][body_y].direction
if direction == directions.up then
direction = directions.upright
elseif direction == directions.down then
direction = directions.downright
else
direction = directions.right
end
elseif player_direction == directions.down and direction == directions.up then
direction = distances[body_x][body_y].direction
if direction == directions.left then
direction = directions.upleft
elseif direction == directions.right then
direction = directions.upright
else
direction = directions.up
end
elseif player_direction == directions.right and direction == directions.left then
direction = distances[body_x][body_y].direction
if direction == directions.up then
direction = directions.upleft
elseif direction == directions.down then
direction = directions.downleft
else
direction = directions.left
end
end
step(direction)
end
-- ------------------------------------------------------------------
-- Drawing
-- ------------------------------------------------------------------
function drawCavern()
local screen_width = love.graphics.getWidth()
local screen_height = love.graphics.getHeight()
local width_scale = screen_width / cavern.width
local height_scale = screen_height / cavern.height
local scale = math.min(width_scale, height_scale)
local x_offset = (screen_width - cavern.width * scale) / 2
local y_offset = (screen_height - cavern.height * scale) / 2
for tile_x, list in ipairs(visibility_map) do
local x = (tile_x - 1) * scale + x_offset
for tile_y, visible in ipairs(list) do
local y = (tile_y - 1) * scale + y_offset
if visible or debug then
tile = cavern[tile_x][tile_y]
if tile == tiletypes.empty then
love.graphics.setColor(0, 0, 0)
love.graphics.rectangle('fill', x, y, scale, scale)
elseif tile == tiletypes.wall then
love.graphics.setColor(1, 1, 1)
love.graphics.rectangle('fill', x, y, scale, scale)
elseif tile == tiletypes.orb then
love.graphics.setColor(0.5, 0.3, 0.3)
love.graphics.rectangle('fill', x, y, scale, scale)
love.graphics.setColor(0, 1, 0)
love.graphics.circle('fill', x + 0.5 * scale, y + 0.5 * scale, 0.7 * scale/2)
elseif tile == tiletypes.slime then
love.graphics.setColor(0, 0, 0)
love.graphics.rectangle('fill', x, y, scale, scale)
love.graphics.setColor(1, 1, 0)
love.graphics.circle('fill', x + 0.5 * scale, y + 0.5 * scale, 0.8 * scale/2)
love.graphics.rectangle('fill', x + 0.1 * scale , y + 0.5 * scale, 0.8 * scale, 0.8 * scale/2)
elseif tile == tiletypes.unknown then
love.graphics.setColor(1, 0.5, 0.5)
love.graphics.rectangle('fill', x, y, scale, scale)
else
print("Impossible tile!!!")
love.graphics.setColor(1, 1, 0)
love.graphics.rectangle('fill', x, y, scale, scale)
end
else
tile = remembered_cavern[tile_x][tile_y]
if tile == tiletypes.empty then
love.graphics.setColor(0.2, 0.2, 0.2)
love.graphics.rectangle('fill', x, y, scale, scale)
elseif tile == tiletypes.wall then
love.graphics.setColor(0.9, 0.9, 0.9)
love.graphics.rectangle('fill', x, y, scale, scale)
elseif tile == tiletypes.orb then
love.graphics.setColor(0.4, 0.4, 0.4)
love.graphics.rectangle('fill', x, y, scale, scale)
love.graphics.setColor(0.2, 0.7, 0.2)
love.graphics.circle('fill', x + 0.5 * scale, y + 0.5 * scale, 0.7 * scale/2)
elseif tile == tiletypes.slime then
love.graphics.setColor(0.2, 0.2, 0.2)
love.graphics.rectangle('fill', x, y, scale, scale)
love.graphics.setColor(0.7, 0.7, 0.2)
love.graphics.circle('fill', x + 0.5 * scale, y + 0.5 * scale, 0.8 * scale/2)
love.graphics.rectangle('fill', x + 0.1 * scale , y + 0.5 * scale, 0.8 * scale, 0.8 * scale/2)
else
love.graphics.setColor(0.7, 0.5, 0.5)
love.graphics.rectangle('fill', x, y, scale, scale)
end
end
if debug and tile_x == player_x and tile_y == player_y then
love.graphics.setColor(1, 1, 0)
love.graphics.rectangle('fill', x + 0.5 * scale, y + 0.5 * scale, scale/2, scale/2)
end
end
end
end
function drawPlayer()
local screen_width = love.graphics.getWidth()
local screen_height = love.graphics.getHeight()
local width_scale = screen_width / cavern.width
local height_scale = screen_height / cavern.height
local scale = math.min(width_scale, height_scale)
local x_offset = (screen_width - cavern.width * scale) / 2
local y_offset = (screen_height - cavern.height * scale) / 2
local x = (player_x - 1) * scale + x_offset
local y = (player_y - 1) * scale + y_offset
local sprite = nil
if flash_player_damaged_counter > 0 then
sprite = player_sprite_damaged
elseif flash_player_crit_counter > 0 then
sprite = player_sprite_crit
else
sprite = player_sprite
end
local rotation = nil
if player_direction == directions.up then
rotation = 0
if player_animation_flip then
x = x + scale
end
elseif player_direction == directions.left then
rotation = math.pi * 1.5
if not player_animation_flip then
y = y + scale
end
elseif player_direction == directions.down then
rotation = math.pi
if not player_animation_flip then
x = x + scale
y = y + scale
else
y = y + scale
end
elseif player_direction == directions.right then
rotation = math.pi * 0.5
if not player_animation_flip then
x = x + scale
else
x = x + scale
y = y + scale
end
else
error("Player facing a direction without usable sprite")
end
-- Our sprite image is drawn assuming 16 pixels per tile
love.graphics.setColor(1, 1, 1)
if not player_animation_flip then
love.graphics.draw(sprite, x, y, rotation, scale/16, scale/16)
else
love.graphics.draw(sprite, x, y, rotation, -scale/16, scale/16)
end
end
function drawHP()
local screen_width = love.graphics.getWidth()
local screen_height = love.graphics.getHeight()
local width_scale = screen_width / cavern.width
local height_scale = screen_height / cavern.height
local scale = math.min(width_scale, height_scale)
local x_offset = (screen_width - cavern.width * scale) / 2
local y_offset = (screen_height - cavern.height * scale) / 2
for i = 1, hp do
local x = (i - 1) * scale + x_offset
local y = y_offset
love.graphics.setColor(1, 1, 1)
love.graphics.draw(heart_sprite, x, y, 0, scale/16, scale/16)
end
end
function drawWin()
if win_is_raster then
local img_width, img_height = win_image:getDimensions()
local screen_width = love.graphics.getWidth()
local screen_height = love.graphics.getHeight()
local width_scale = screen_width / img_width
local height_scale = screen_height / img_height
local scale = math.min(width_scale, height_scale)
local x_offset = (screen_width - img_width * scale) / 2
local y_offset = (screen_height - img_height * scale) / 2
love.graphics.setColor(1, 1, 1)
love.graphics.draw(win_image, x_offset, y_offset, 0, scale, scale)
else
local width = 20
local height = 20
local screen_width = love.graphics.getWidth()
local screen_height = love.graphics.getHeight()
local width_scale = screen_width / width
local height_scale = screen_height / height
local scale = math.min(width_scale, height_scale)
local x_offset = (screen_width - width * scale) / 2
local y_offset = (screen_height - height * scale) / 2
local vertices = {10, 7, 6, 8, 5, 20, 15, 20, 14, 8}
local transformed_vertices = {}
for i = 1, #vertices, 2 do
local x = vertices[i] * scale + x_offset
local y = vertices[i + 1] * scale + y_offset
transformed_vertices[i] = x
transformed_vertices[i + 1] = y
end
love.graphics.setColor(0.1, 0.1, 0.3)
love.graphics.polygon('fill', transformed_vertices)
love.graphics.setColor(0.6, 0.3, 0.2)
love.graphics.circle('fill', 10 * scale + x_offset, 5 * scale + y_offset, 3 * scale)
love.graphics.setColor(0.5, 0.2, 0.1)
love.graphics.rectangle('fill', x_offset, 15 * scale + y_offset, 20 * scale, 10 * scale)
love.graphics.setColor(0, 1, 0)
love.graphics.circle('fill', 10 * scale + x_offset, 16 * scale + y_offset, 2 * scale)
end
end
function drawLoss()
local width = 54
local height = 140
local screen_width = love.graphics.getWidth()
local screen_height = love.graphics.getHeight()
local width_scale = screen_width / width
local height_scale = screen_height / height
local scale = math.min(width_scale, height_scale)
local x_offset = (screen_width - width * scale) / 2
local y_offset = (screen_height - height * scale) / 2
local vertices = {18, 20, 0, 40, 0, 60, 6, 80, 12, 100, 18, 120, 36, 120, 42, 100, 48, 80, 54, 60, 54, 40, 36, 20}
local transformed_vertices = {}
for i = 1, #vertices, 2 do
local x = vertices[i] * scale + x_offset
local y = vertices[i + 1] * scale + y_offset
transformed_vertices[i] = x
transformed_vertices[i + 1] = y
end
love.graphics.setColor(0.5, 0.2, 0.1)
love.graphics.polygon('fill', transformed_vertices)
love.graphics.setColor(1, 1, 1)
local lines = {27, 40, 27, 80, 18, 50, 36, 50}
for i = 1, #lines, 4 do
local sx = lines[i] * scale + x_offset
local sy = lines[i + 1] * scale + y_offset
local ex = lines[i + 2] * scale + x_offset
local ey = lines[i + 3] * scale + y_offset
love.graphics.line(sx, sy, ex, ey)
end
end
function drawConfig()
local text = nil
if configuration_step == configuration_steps.quit then
text = "Quit"
elseif configuration_step == configuration_steps.restart then
text = "Restart"
elseif configuration_step == configuration_steps.configure then
text = "Button configuration"
elseif configuration_step == configuration_steps.autoexplore then
text = "Autoexplore"
elseif configuration_step == directions.up then
text = "Up"
elseif configuration_step == directions.upleft then
text = "Up-Left"
elseif configuration_step == directions.left then
text = "Left"
elseif configuration_step == directions.downleft then
text = "Down-Left"
elseif configuration_step == directions.down then
text = "Down"
elseif configuration_step == directions.downright then
text = "Down-Right"
elseif configuration_step == directions.right then
text = "Right"
elseif configuration_step == directions.upright then
text = "Up-Right"
elseif configuration_step == directions.inplace then
text = "Do nothing for a turn"
else
error("Impossible configuration step")
end
love.graphics.setColor(1, 1, 1)
love.graphics.print(text)
end
-- ------------------------------------------------------------------
-- Callbacks
-- ------------------------------------------------------------------
function love.load()
math.randomseed(os.time())
newGame()
if win_is_raster then
win_image = love.graphics.newImage("win_image.png")
end
player_sprite = love.graphics.newImage("sprite.png")
player_sprite_crit = love.graphics.newImage("sprite_crit.png")
player_sprite_damaged = love.graphics.newImage("sprite_damaged.png")
heart_sprite = love.graphics.newImage("heart.png")
end
function love.update(dt)
if move_repeat_counter ~= nil and move_repeat_counter > 0 then
move_repeat_counter = move_repeat_counter - dt
if move_repeat_counter <= 0 and last_key_pressed ~= nil then
love.keypressed(last_key_pressed)
end
end
if flash_player_crit_counter > 0 then
flash_player_crit_counter = flash_player_crit_counter - dt
end
if flash_player_damaged_counter > 0 then
flash_player_damaged_counter = flash_player_damaged_counter - dt
if hp == 0 and flash_player_damaged_counter <= 0 and game_mode ~= gamemodes.won then
game_mode = gamemodes.lost
end
end
end
function love.keypressed(key)
if game_mode == gamemodes.config then
-- This is a hack. Each "enum value" corresponds to an integer, and we are iterating
-- through them. -3 to -1 are control keys, and 0 to 8 are direction keys
if configuration_step >= directions.up and configuration_step <= directions.inplace then
direction_keys[key] = configuration_step
elseif configuration_step == configuration_steps.quit then
control_keys.quit = key
elseif configuration_step == configuration_steps.restart then
control_keys.restart = key
elseif configuration_step == configuration_steps.configure then
control_keys.configure = key
elseif configuration_step == configuration_steps.autoexplore then
control_keys.autoexplore = key
end
configuration_step = configuration_step + 1
if configuration_step > directions.inplace then
game_mode = gamemodes.normal
end
else
if last_key_pressed == key then
-- Repeat faster after the initial threshold for repetition has been met
move_repeat_counter = 0.1
else
move_repeat_counter = 0.3
end
if key == control_keys.configure and game_mode == gamemodes.normal then
direction_keys = {}
game_mode = gamemodes.config
configuration_step = configuration_steps.quit
elseif key == control_keys.quit then
love.event.quit()
elseif key == control_keys.restart then
newGame()
elseif key == control_keys.autoexplore then
autoexplore()
last_key_pressed = key
elseif direction_keys[key] ~= nil then
step(direction_keys[key])
last_key_pressed = key
elseif key == 'pause' then
debug = not debug
win_is_raster = not win_is_raster
end
end
end
function love.keyreleased(key)
if last_key_pressed == key then
last_key_pressed = nil
move_repeat_counter = nil
end
end
function love.draw()
if game_mode == gamemodes.normal then
drawCavern()
if not debug then
drawPlayer()
end
drawHP()
elseif game_mode == gamemodes.won then
drawWin()
elseif game_mode == gamemodes.lost then
drawLoss()
elseif game_mode == gamemodes.config then
drawConfig()
else
error("Impossible game mode")
end
end