---@diagnostic disable: undefined-global backgrounds = require("assets.backgrounds.backgrounds-data") sprite_height = 128 sprite_width = 64 deltaTimeMultiplier = 60 groundTiles = { { x = 250, y = HEIGHT - 300, width = 200, height = 30 }, { x = WIDTH - 250 - 200, y = HEIGHT - 300, width = 200, height = 30 }, { x = 0, y = HEIGHT - 100, width = WIDTH, height = 100 } } 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 end function Setup() local bgIndex = math.random(1, #backgrounds) setBgImage(backgrounds[bgIndex]) SafeInitCharacter(player1Character, 250, 150) SafeInitCharacter(player2Character, WIDTH - 250 - (250 / 2), 150) end gravity = 1.2 -- POSITIVE gravity function DrawGroundTiles() for _, tile in ipairs(groundTiles) do queueRectForRender(tile.x, tile.y, tile.width, tile.height, 100, 100, 100, 255) -- gray boxes 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 IsTouchingWall(character, direction) for _, tile in ipairs(groundTiles) do local charBottom = character.y + sprite_height local charTop = character.y local tileBottom = tile.y + tile.height local tileTop = tile.y local verticalOverlap = charBottom > tileTop and charTop < tileBottom if direction == "left" then local nextX = character.x - character.speed local willCollide = nextX < tile.x + tile.width and character.x >= tile.x + tile.width if willCollide and verticalOverlap then character.x = tile.x + tile.width return true end elseif direction == "right" then local nextX = character.x + sprite_width + character.speed local willCollide = nextX > tile.x and character.x + sprite_width <= tile.x if willCollide and verticalOverlap then character.x = tile.x - sprite_width return true end end end return false end function HandleP1Input() if Input.isKeyPressedOnce("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") then player1Character.x_velocity = player1Character.speed else player1Character.x_velocity = 0 end end if Input.isKeyDown("A") then if not IsTouchingWall(player1Character, "left") then player1Character.x_velocity = -player1Character.speed else player1Character.x_velocity = 0 end end if Input.isKeyPressedOnce("F") then PerformPunch(player1Character, player2Character) end end function HandleP2Input() if Input.isKeyPressedOnce("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") then player2Character.x_velocity = player2Character.speed else player2Character.x_velocity = 0 end end if Input.isKeyDown("LEFT") then if not IsTouchingWall(player2Character, "left") then player2Character.x_velocity = -player2Character.speed else player2Character.x_velocity = 0 end end if Input.isKeyPressedOnce("Right Alt") or Input.isKeyPressedOnce("Right Option") 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 knockbackMultiplierX = 0.47 local knockbackMultiplierY = 0.01 target.y_velocity = -10 - (target.knockback_counter * knockbackMultiplierY) local direction = 1 if target.x < attacker.x then direction = -1 end target.x_velocity = target.knockback_counter * knockbackMultiplierX * direction -- target.x = target.x + direction * (target.knockback_counter * 0.7) end function ApplyMinimalKnockback(attacker, target) local knockbackMultiplierX = 0.47 local knockbackMultiplierY = 0.01 local knockbackCountMinimal = 5 target.y_velocity = -10 - (knockbackCountMinimal * knockbackMultiplierY) local direction = 1 if target.x < attacker.x then direction = -1 end target.x_velocity = knockbackCountMinimal * knockbackMultiplierX * direction -- target.x = target.x + direction * (target.knockback_counter * 0.7) end function KillPlayer(character) character.lives = character.lives - 1 character.knockback_counter = 0 character.combo_chain = 0 if character.lives > 0 then Respawn(character) end end function RegisterHit(attacker, target, damage) target.knockback_counter = target.knockback_counter + damage 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 = (target.x > attacker.x) and "right" or "left" -- 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 RegisterHit(attacker, target, attacker.hit_strength) end end function ApplyPhysics(character) local deltaTimeMultiplier = 60 -- Target 60FPS character.y_velocity = character.y_velocity + gravity * deltaTime * deltaTimeMultiplier character.y = character.y + character.y_velocity * deltaTime * deltaTimeMultiplier character.x = character.x + character.x_velocity * deltaTime * deltaTimeMultiplier character.x_velocity = character.x_velocity * 0.99 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 not IsOnGround(character) then character.can_jump = false end if character.y > HEIGHT + 50 then KillPlayer(character) end end function Update() -- Apply physics ApplyPhysics(player1Character) ApplyPhysics(player2Character) -- Render characters queueTextureForRender(player1Character.current_sprite, math.floor(player1Character.x), math.floor(player1Character.y)) queueTextureForRender(player2Character.current_sprite, math.floor(player2Character.x), math.floor(player2Character.y)) -- Environment DrawGroundTiles() -- Input HandleP1Input() HandleP2Input() -- UI DrawUI() -- Timer Timer.update(deltaTime) end