---@diagnostic disable: undefined-global, lowercase-global backgrounds = require("assets.backgrounds.backgrounds-data") sprite_height = 128 sprite_width = 64 remaining_rounds = 9 current_round = 1 gamePaused = true isKO = false isOverlay = true isNewRound = false isFight = false isGameOver = false winner = nil math.randomseed(os.time()) groundTiles = {} specialGroundTiles = { { x = -50, y = 0, width = 55, height = HEIGHT, special = true }, { x = WIDTH - 5, y = 0, width = 50, height = HEIGHT, special = true } } knockbackMultiplierX = 22.4 knockbackMultiplierY = 7.73 minimalKnockbackMultiplierY = 40.3 function SafeInitCharacter(character, default_x, default_y) character.x = default_x character.x_velocity = 1 character.y = default_y character.y_velocity = 1 character.current_sprite = character.asset_dir .. "/sprites/idle.png" character.default_sprite = character.current_sprite character.can_jump = false character.knockback_counter = 0 character.combo_chain = 0 character.can_apply_knockback = false character.facing = "right" character.isDead = false character.wins = 0 end function Setup() local bgIndex = math.random(1, #backgrounds) setBgImage(backgrounds[bgIndex]) SafeInitCharacter(player1Character, 250, 150) SafeInitCharacter(player2Character, WIDTH - 250 - (250 / 2), 150) groundTiles = {} local tileSize = 50 local gridWidth = math.floor(WIDTH / tileSize) local gridHeight = math.floor(HEIGHT / tileSize) local clusterSizeMin, clusterSizeMax = 3, 5 local clusterCount = 8 for _ = 1, clusterCount do local clusterX = math.random(1, gridWidth - clusterSizeMax - 2) local clusterY = math.random(5, gridHeight - 3) local clusterSize = math.random(clusterSizeMin, clusterSizeMax) for i = 0, clusterSize - 1 do table.insert(groundTiles, { x = (clusterX + i) * tileSize, y = clusterY * tileSize, width = tileSize, height = tileSize, special = false }) end end for _, tile in ipairs(specialGroundTiles) do table.insert(groundTiles, tile) end Timer.after(0.5, function () isOverlay = false isNewRound = true Timer.after(2, function () isNewRound = false isFight = true Timer.after(2, function () isFight = false gamePaused = false end) end) end) end gravity = 1570 function DrawGroundTiles() for _, tile in ipairs(groundTiles) do if tile.special then return end queueTextureForRender("assets/images/tile.png", tile.x, tile.y) end end function IsOnGround(character) for _, tile in ipairs(groundTiles) do local characterFeetY = character.y + sprite_height local isWithinX = character.x + sprite_width > tile.x and character.x < tile.x + tile.width local isTouchingY = characterFeetY >= tile.y and characterFeetY <= tile.y + tile.height local isFalling = character.y_velocity >= 0 if isWithinX and isTouchingY and isFalling then character.y = tile.y - sprite_height return true end end return false end function IsHittingCeiling(character) for _, tile in ipairs(groundTiles) do local characterHeadY = character.y local isWithinX = character.x + sprite_width > tile.x and character.x < tile.x + tile.width local isTouchingY = characterHeadY <= tile.y + tile.height and characterHeadY >= tile.y if isWithinX and isTouchingY then character.y = tile.y + tile.height character.y_velocity = 0 return true end end return false end function AABBOverlap(a, b) return a.x < b.x + b.width and a.x + a.width > b.x and a.y < b.y + b.height and a.y + a.height > b.y end function IsTouchingWall(character, direction, exclude_special) local velocity = character.x_velocity -- Clamp how far ahead we look local max_check_distance = 1.0 -- only check 1 pixel ahead if velocity > max_check_distance then velocity = max_check_distance end if velocity < -max_check_distance then velocity = -max_check_distance end -- Prevent missing collisions on tiny glide if math.abs(velocity) < 0.01 then velocity = (direction == "right") and 0.01 or -0.01 end local futureX = character.x + velocity local charBox = { x = futureX, y = character.y, width = sprite_width, height = sprite_height } for _, tile in ipairs(groundTiles) do if tile.special and exclude_special then goto continue end local tileBox = { x = tile.x, y = tile.y, width = tile.width, height = tile.height } if AABBOverlap(charBox, tileBox) then -- Resolve overlap if direction == "left" then character.x = tile.x + tile.width elseif direction == "right" then character.x = tile.x - sprite_width end character.x_velocity = 0 return true end ::continue:: end return false end function HandleP1Input() if player1Character.isDead then return end if Input.isKeyDown("W") and player1Character.can_jump then player1Character.y_velocity = player1Character.jump_strength * -1.0 player1Character.can_jump = false end if Input.isKeyDown("D") then if not IsTouchingWall(player1Character, "right", false) then player1Character.x_velocity = player1Character.speed player1Character.facing = "right" else player1Character.x_velocity = 0 player1Character.x = math.floor(player1Character.x) end end if Input.isKeyDown("A") then if not IsTouchingWall(player1Character, "left", false) then player1Character.x_velocity = -player1Character.speed player1Character.facing = "left" else player1Character.x_velocity = 0 player1Character.x = math.floor(player1Character.x) end end if Input.isKeyPressedOnce("F") then PerformPunch(player1Character, player2Character) end end function HandleP2Input() if player2Character.isDead then return end if Input.isKeyDown("UP") and player2Character.can_jump then player2Character.y_velocity = player2Character.jump_strength * -1.0 player2Character.can_jump = false end if Input.isKeyDown("RIGHT") then if not IsTouchingWall(player2Character, "right", false) then player2Character.x_velocity = player2Character.speed player2Character.facing = "right" else player2Character.x_velocity = 0 player2Character.x = math.floor(player2Character.x) end end if Input.isKeyDown("LEFT") then if not IsTouchingWall(player2Character, "left", false) then player2Character.x_velocity = -player2Character.speed player2Character.facing = "left" else player2Character.x_velocity = 0 player2Character.x = math.floor(player2Character.x) end end if Input.isKeyPressedOnce("L") then PerformPunch(player2Character, player1Character) end end function DrawUI() local fontFile = "assets/fonts/OpenSans-Bold.ttf" local fontSize = 24 local text = player1Character.name local x = 20 local y = 20 queueTextForRender(text, fontFile, x, y, fontSize, 0, 0, 0, 255) local text = player2Character.name local textWidth = getTextWidth(fontFile, fontSize, text) local x = WIDTH - textWidth - 20 local y = 20 queueTextForRender(text, fontFile, x, y, fontSize, 0, 0, 0, 255) local fontFile = "assets/fonts/OpenSans-MediumItalic.ttf" local fontSize = 34 local text = tonumber(player1Character.knockback_counter) local x = 40 local y = 40 queueTextForRender(text, fontFile, x, y, fontSize, 0, 0, 0, 255) local fontFile = "assets/fonts/OpenSans-MediumItalic.ttf" local fontSize = 34 local text = tostring(player2Character.knockback_counter) local textWidth = getTextWidth(fontFile, fontSize, text) local x = WIDTH - textWidth - 40 queueTextForRender(text, fontFile, x, y, fontSize, 0, 0, 0, 255) end function Respawn(character) SafeInitCharacter(character, 250, 150) end function ApplyKnockback(attacker, target) local direction = attacker.facing == "left" and -1 or 1 local scaledY = -10 - (target.knockback_counter * knockbackMultiplierY) target.y_velocity = math.max(-950, scaledY) -- clamp Y knockback target.x_velocity = target.knockback_counter * knockbackMultiplierX * direction end function ApplyMinimalKnockback(attacker, target) local knockbackCountMinimal = 5 target.y_velocity = -10 - (knockbackCountMinimal * minimalKnockbackMultiplierY) local direction = 1 if attacker.facing == "left" then direction = -1 end target.x_velocity = knockbackCountMinimal * knockbackMultiplierX * direction -- target.x = target.x + direction * (target.knockback_counter * 0.7) end function GameOver() isGameOver = true Timer.after(10, function () dofile("assets/scripts/mainMenu.lua") Setup() end) if player1Character.wins > player2Character.wins then winner = player1Character else winner = player2Character end end function KillPlayer(character) if gamePaused then return end if character.isDead then return end character.isDead = true playSound("assets/sounds/K-O.wav") gamePaused = true isKO = true Timer.after(2, function() remaining_rounds = remaining_rounds - 1 current_round = current_round + 1 character.knockback_counter = 0 character.combo_chain = 0 if character == player1Character then player2Character.wins = player2Character.wins + 1 else player1Character.wins = player1Character.wins + 1 end if remaining_rounds > 0 then Setup() else GameOver() end isKO = false if remaining_rounds > 0 then isOverlay = true Timer.after(2, function () isOverlay = false isNewRound = true Timer.after(2, function () isNewRound = false isFight = true Timer.after(2, function () gamePaused = false isFight = false end) end) end) end end) end function RegisterHit(attacker, target, damage) target.knockback_counter = target.knockback_counter + damage target.combo_chain = 0 attacker.combo_chain = attacker.combo_chain + 1 if attacker.combo_chain >= 4 then ApplyKnockback(attacker, target) attacker.combo_chain = 0 else ApplyMinimalKnockback(attacker, target) end if target.knockback_counter >= 1000 then KillPlayer(target) end end function PerformPunch(attacker, target) local punch_range = 27 local punch_height = 80 -- Determine direction of target relative to attacker local punch_direction = attacker.facing -- Set punch sprite attacker.current_sprite = attacker.asset_dir .. "/sprites/punch_" .. punch_direction .. ".png" -- Reset sprite after a short delay (e.g., 0.15s) Timer.clearAllTimers() Timer.after(0.15, function() attacker.current_sprite = attacker.default_sprite end) -- Define hitbox based on punch direction local hitbox = { x = attacker.x + (punch_direction == "right" and sprite_width or -punch_range), y = attacker.y + (sprite_height / 2) - (punch_height / 2), width = punch_range, height = punch_height } local targetHitbox = { x = target.x, y = target.y, width = sprite_width, height = sprite_height } local function isColliding(a, b) return a.x < b.x + b.width and a.x + a.width > b.x and a.y < b.y + b.height and a.y + a.height > b.y end if isColliding(hitbox, targetHitbox) then Timer.after(0.05, function () RegisterHit(attacker, target, attacker.hit_strength) end) end end function ApplyPhysics(character) if character.isDead then return end local last_x = character.x local last_y = character.y character.y_velocity = character.y_velocity + gravity * deltaTime character.y = character.y + character.y_velocity * deltaTime character.x = character.x + character.x_velocity * deltaTime character.x_velocity = character.x_velocity * 0.88 if character.y_velocity > 0 then -- Falling if IsOnGround(character) then character.y_velocity = 0 character.can_jump = true end elseif character.y_velocity < 0 then -- Going upward IsHittingCeiling(character) end if IsTouchingWall(character, "left", true) then character.x = last_x character.x_velocity = 0 end if IsTouchingWall(character, "right", true) then character.x = last_x character.x_velocity = 0 end if not IsOnGround(character) then character.can_jump = false end if character.y > HEIGHT + 60 then KillPlayer(character) end if character.x < -60 then KillPlayer(character) end if character.x > WIDTH + 60 then KillPlayer(character) end end function DrawOverlay() queueRectForRender(0, 0, 1280, 720, 0, 0, 0, 150) end function DrawKOGraphic() DrawOverlay() local fontFile = "assets/fonts/OpenSans-Bold.ttf" local fontSize = 192 local text = "K.O." local textWidth = getTextWidth(fontFile, fontSize, text) local x = (1280 - textWidth) // 2 local y = 720 // 2 - fontSize // 2 queueTextForRender(text, fontFile, x + 5, y + 5, fontSize, 255, 255, 0, 255) queueTextForRender(text, fontFile, x, y, fontSize, 255, 0, 0, 255) end function DrawNewRoundGraphic() DrawOverlay() local fontFile = "assets/fonts/OpenSans-Bold.ttf" local fontSize = 96 local text = "Round " .. current_round if remaining_rounds == 1 then text = "Final Round" end local textWidth = getTextWidth(fontFile, fontSize, text) local x = (1280 - textWidth) // 2 local y = 720 // 2 - fontSize // 2 queueTextForRender(text, fontFile, x, y, fontSize, 255, 255, 255, 255) end function DrawFightGraphic() DrawOverlay() local fontFile = "assets/fonts/OpenSans-Bold.ttf" local fontSize = 96 local text = "FIGHT!" local textWidth = getTextWidth(fontFile, fontSize, text) local x = (1280 - textWidth) // 2 local y = 720 // 2 - fontSize // 2 queueTextForRender(text, fontFile, x, y, fontSize, 255, 255, 0, 255) end function DrawGameOverGraphic() if not winner then return end setBgImage(backgrounds[1]) local fontFile = "assets/fonts/OpenSans-Bold.ttf" local fontSize = 96 local text = "WINNER" local textWidth = getTextWidth(fontFile, fontSize, text) local x = (1280 - textWidth) // 2 local y = 20 queueTextForRender(text, fontFile, x, y, fontSize, 255, 255, 0, 255) local portraitWidth = 128 local portraitHeight = 256 local portraitX = (WIDTH // 2) - (portraitWidth // 2) local portraitY = (HEIGHT // 2) - (portraitHeight // 2) queueTextureForRender(winner.asset_dir .. "/portrait.png", portraitX, portraitY) local nameFontSize = 24 local nameWidth = getTextWidth(fontFile, nameFontSize, winner.name) local nameX = (1280 - nameWidth) // 2 local nameY = portraitY + portraitHeight + 20 queueTextForRender(winner.name, fontFile, nameX, nameY, nameFontSize, 255, 255, 255, 255) end function Update() local maxDelta = 0.05 -- max 50 ms per frame if deltaTime > maxDelta then deltaTime = maxDelta end Timer.update(deltaTime) if not gamePaused then ApplyPhysics(player1Character) ApplyPhysics(player2Character) HandleP1Input() HandleP2Input() end if not isGameOver then queueTextureForRender(player1Character.current_sprite, math.ceil(player1Character.x), math.ceil(player1Character.y)) queueTextureForRender(player2Character.current_sprite, math.ceil(player2Character.x), math.ceil(player2Character.y)) DrawGroundTiles() DrawUI() end if isKO then DrawKOGraphic() end if isOverlay then DrawOverlay() end if isNewRound then DrawNewRoundGraphic() end if isFight then DrawFightGraphic() end if isGameOver then DrawGameOverGraphic() end end