local TMX = require('tmx') local Player = require('player') local W, H = 320, 240 local TILE = 16 -- placeholder tile colors (match assets/tiles/basic.png) local TILE_COLORS = { [1] = { 76, 175, 80 }, -- grass / green box [2] = { 139, 90, 43 }, -- dirt / brown box [3] = { 158, 158, 158 }, -- stone / grey box } -- LÖVE setColor wants 0-1 floats; keep palette values as 0-255 ints instead local function color(r, g, b) love.graphics.setColor(r / 255, g / 255, b / 255) end local characters = {} for _, dir in ipairs({ 'fighter', 'healer', 'rogue' }) do local cfg = require('assets.characters.' .. dir .. '.config') characters[#characters + 1] = { dir = dir, cfg = cfg, sheet = love.graphics.newImage('assets/characters/' .. dir .. '/sprite.png'), frameCount = nil, -- set in load animTimer = math.random() * 10, } end local fontTitle, fontText, fontSmall local state = 'select' local selIndex = 1 local selectTime = 0 local map, player, camX, camY -------------------------------------------------------------------- function love.load() love.graphics.setDefaultFilter('nearest', 'nearest') love.window.setMode(W, H, { resizable = true }) local ttf = 'assets/fonts/RasterForgeRegular-JpBgm.ttf' fontTitle = love.graphics.newFont(ttf, 14) fontText = love.graphics.newFont(ttf, 9) fontSmall = love.graphics.newFont(ttf, 8) for _, c in ipairs(characters) do c.frameCount = math.floor(c.sheet:getWidth() / 16) end end -- window size: prefer live query when the build provides it, otherwise fall -- back to values tracked via love.resize() (works on every LÖVE version) local winW, winH = W, H local function currentWindowSize() local w, h = winW, winH if love.window and type(love.window.getWidth) == 'function' then local qw, qh = love.window.getWidth(), love.window.getHeight() if type(qw) == 'number' and qw > 0 and type(qh) == 'number' and qh > 0 then w, h = qw, qh end end return math.max(1, w), math.max(1, h) end -- scale factor + centering offset so the 4:3 view fits the window, -- using whole-pixel scaling whenever the window is big enough local function viewTransform() local ww, wh = currentWindowSize() local fit = math.min(ww / W, wh / H) local s = fit < 1 and fit or math.floor(fit + 0.0001) -- +eps for float safety if s <= 0 then s = 1 end -- degenerate size; render at native scale local ox = math.floor((ww - W * s) / 2) local oy = math.floor((wh - H * s) / 2) return s, ox, oy end -------------------------------------------------------------------- -- character select screen -------------------------------------------------------------------- local function cardRect(i) local cw = 96 local x = (W - 3 * cw - 2 * 8) / 2 + (i - 1) * (cw + 8) return math.floor(x), 58, cw, 140 end -- draw text horizontally centered on x (call after setFont) local function printC(text, x, y) local w = love.graphics.getFont():getWidth(text) love.graphics.print(text, math.floor(x - w / 2), y) end local function drawCard(i) local c = characters[i] local x, y, cw, ch = cardRect(i) local selected = i == selIndex -- explicit colors: leftover state from previous draws would bleed in otherwise if selected then color(60, 56, 88) else color(38, 36, 52) end love.graphics.rectangle('fill', x, y, cw, ch, 2, 2) if selected then love.graphics.setLineWidth(2) color(255, 210, 60) else love.graphics.setLineWidth(1) color(90, 90, 110) end love.graphics.rectangle('line', x + 0.5, y + 0.5, cw - 1, ch - 1, 2, 2) -- animated sprite at 3x scale (walk loop so they feel alive) local frames = c.cfg.animations.walk local fi = math.floor(c.animTimer * 8) % #frames + 1 local frame = frames[fi] or 1 love.graphics.push() love.graphics.translate(x + cw / 2, y + 34) if math.floor(c.animTimer) % 2 == 0 then love.graphics.scale(-3, 3) -- flip facing left/right once per second else love.graphics.scale(3, 3) end local quad = love.graphics.newQuad((frame - 1) * 16, 0, 16, 16, c.sheet) love.graphics.draw(c.sheet, quad, -8, -8) love.graphics.pop() -- name + stats local cx = x + cw / 2 color(235, 235, 245) love.graphics.setFont(fontText) printC(c.cfg.name, cx, y + 60) love.graphics.setFont(fontSmall) color(170, 170, 190) local sy = y + 74 for _, line in ipairs({ 'HP ' .. c.cfg.health, 'ATK ' .. c.cfg.stats.atk .. ' DEF ' .. c.cfg.stats.def, 'MAG ' .. c.cfg.stats.mag .. ' LCK ' .. c.cfg.stats.lck, 'wep: ' .. (c.cfg.defaultWeapon and c.cfg.defaultWeapon.name or '-'), }) do printC(line, cx, sy) sy = sy + 11 end if selected then color(255, 210, 60) love.graphics.setFont(fontSmall) printC('Z / ENTER to start', W / 2, y + ch + 8) end end local function drawSelect() color(26, 24, 38) love.graphics.rectangle('fill', 0, 0, W, H) -- fake dungeon backdrop: a few dim boxes on the ground line color(38, 35, 54) for i = 0, 19 do local hgt = 20 + math.abs(math.sin(i * 7.3)) * 40 love.graphics.rectangle('fill', i * 16 + 3, H - 14 - hgt, 10, hgt) end color(255, 210, 60) love.graphics.setFont(fontTitle) printC('DUNGEON CRAWLER', W / 2, 18) color(200, 200, 220) love.graphics.setFont(fontText) printC('SELECT YOUR HERO', W / 2, 40) for i = 1, #characters do drawCard(i) end color(150, 150, 170) love.graphics.setFont(fontSmall) printC('< > or A/D: choose Q: quit', W / 2, H - 8) end -------------------------------------------------------------------- -- play state -------------------------------------------------------------------- local function startGame() local c = characters[selIndex] map = TMX.load('maps/level1.tmx') player = Player.new(c.cfg, c.sheet) player:setPosition(8 * TILE, 27 * TILE) -- open ground; ground surface is at y = 29*16 camX, camY = 0, 0 state = 'play' end local function updateCamera() local targetX = player.x + player.w / 2 - W / 2 local targetY = player.y + player.h / 2 - H / 2 camX = math.floor(math.max(0, math.min(targetX, map.width * TILE - W))) camY = math.floor(math.max(0, math.min(targetY, map.height * TILE - H))) end local function drawMap() local x0 = math.max(0, math.floor(camX / TILE)) local y0 = math.max(0, math.floor(camY / TILE)) local x1 = math.min(map.width - 1, math.ceil((camX + W) / TILE)) local y1 = math.min(map.height - 1, math.ceil((camY + H) / TILE)) for ty = y0, y1 do for tx = x0, x1 do local gid = map.tiles[ty * map.width + tx + 1] if gid > 0 then local col = TILE_COLORS[gid] or { 255, 0, 255 } -- magenta = missing tile color(col[1], col[2], col[3]) love.graphics.rectangle('fill', tx * TILE - camX, ty * TILE - camY, TILE, TILE) end end end end local function drawPlay() color(20, 18, 30) love.graphics.rectangle('fill', 0, 0, W, H) drawMap() player:draw(camX, camY) -- HUD local cfg = player.cfg love.graphics.setFont(fontText) color(235, 235, 245) love.graphics.print(cfg.name, 6, 6) local hpMax = cfg.health local barW = 80 color(60, 30, 30) love.graphics.rectangle('fill', 6, 18, barW, 7) color(200, 60, 50) love.graphics.rectangle('fill', 7, 19, (barW - 2) * math.max(0, player.hp or hpMax) / hpMax, 5) -- Love 11.4 print() has no align arg; compute the right-aligned x manually local hint = 'ESC: menu F11: fullscreen' color(150, 150, 170) love.graphics.setFont(fontSmall) love.graphics.print(hint, W - 6 - fontSmall:getWidth(hint), H - 8) end -------------------------------------------------------------------- function love.resize(w, h) winW, winH = w, h end function love.update(dt) if state == 'play' then player:update(dt, map, { left = love.keyboard.isDown('left', 'a'), right = love.keyboard.isDown('right', 'd'), jumpHeld = love.keyboard.isDown('space', 'up', 'w', 'z'), }) updateCamera() else for _, c in ipairs(characters) do c.animTimer = c.animTimer + dt end end end function love.draw() -- no offscreen canvas in this build: scale the draw transform instead. -- nearest filtering keeps pixels crisp at integer scales. local s, ox, oy = viewTransform() color(10, 8, 16) love.graphics.clear(10 / 255, 8 / 255, 16 / 255) love.graphics.push() love.graphics.translate(ox, oy) love.graphics.scale(s, s) if state == 'select' then drawSelect() else drawPlay() end love.graphics.pop() end function love.keypressed(key) if key == 'f11' then local fs = love.window.getFullscreen() love.window.setMode(W, H, { fullscreen = not fs, resizable = true }) return end if state == 'select' then if key == 'left' or key == 'a' then selIndex = (selIndex - 2) % #characters + 1 elseif key == 'right' or key == 'd' then selIndex = selIndex % #characters + 1 elseif key == 'z' or key == 'return' or key == 'space' then startGame() elseif key == 'q' or key == 'escape' then love.event.quit() end else -- play if key == 'space' or key == 'up' or key == 'w' or key == 'z' then player:queueJump() elseif key == 'escape' then state = 'select' end end end