-- Game state: rooms, combatants, physics, HUD GameState = {} GameState.__index = GameState local TransitionPhase = { FADE_IN = 1, SWAP = 2, FADE_OUT = 3, } function GameState:new(stateMachine) self = setmetatable({}, GameState) self.stateMachine = stateMachine self.context = GameContext:new(self.stateMachine.assetLoader, self.stateMachine.input) self.paused = false self.canTransition = false self.transitionTimer = 0 self.transitionPhase = nil self.transitionX = 0 self.transitionSpeed = Config.SCREEN_W / 0.5 self.transitionEdgeWidth = 20 self.transitionPendingRoom = nil self.hud = nil return self end function GameState:enter() self.context.roomManager = RoomManager:new(self.stateMachine.assetLoader) local nextRoom = self.context.roomManager:selectNextRoom() self:addPlayers() self:loadRoom(nextRoom) self.paused = false self.hud = HUD:new(self.stateMachine.assetLoader, self.context.players) end function GameState:addPlayers() local slots = self.stateMachine.players or {} for _, slotInfo in ipairs(slots) do self.context:addPlayer(slotInfo.selectedChar, slotInfo.playerIdx) end end function GameState:loadRoom(room) self.context:loadRoom(room) self.canTransition = not (self.context.roomManager.currentRoomConfig.defeatAllEnemies or false) local hasEnemy = false for _, combatant in ipairs(self.context.combatants) do if combatant.team == CombatantTeam.ENEMY then hasEnemy = true end end if not self.canTransition and not hasEnemy then self.canTransition = true end end function GameState:advanceRoom() if self.transitionPhase then return end self.transitionPendingRoom = self.context.roomManager:selectNextRoom() self.transitionPhase = TransitionPhase.FADE_IN self.transitionX = 0 end function GameState:update(dt) -- Handle pause for _, player in ipairs(self.context.players) do if self.stateMachine.input:wasPressed(player.playerIdx, self.stateMachine.input.ACTIONS.PAUSE) then self.paused = not self.paused end end if self.paused then return end -- Update room self.context.roomManager.currentRoomVisual:update(dt) -- Update all combatants for _, combatant in ipairs(self.context.combatants) do combatant:update(dt) end ParticlePlayer:update(dt) -- Update projectiles for i = #self.context.projectiles, 1, -1 do local proj = self.context.projectiles[i] proj:update(dt) if not proj.alive then table.remove(self.context.projectiles, i) end end -- Remove dead combatants that finished their death animation for i = #self.context.combatants, 1, -1 do if self.context.combatants[i].dead then table.remove(self.context.combatants, i) end end -- Check if all enemies are dead or dying for room transition if self.context.roomManager.currentRoomConfig.defeatAllEnemies and not self.canTransition then local livingEnemies = 0 for _, combatant in ipairs(self.context.combatants) do if combatant.team == CombatantTeam.ENEMY and combatant.health > 0 then livingEnemies = livingEnemies + 1 end end if livingEnemies == 0 then self.canTransition = true end end -- Update drops local roomCollision = self.context.roomManager.currentRoomCollision for i = #self.context.drops, 1, -1 do local drop = self.context.drops[i] drop:update(dt, roomCollision) if drop.state == DropState.IDLE and drop.onComplete then drop.onComplete:setPickup(drop.onComplete.targetX, drop.onComplete.targetY) drop.onComplete = nil elseif drop.state == nil then table.remove(self.context.drops, i) end end -- Room transition fade if self.transitionPhase then if self.transitionPhase == TransitionPhase.FADE_IN then self.transitionX = self.transitionX + self.transitionSpeed * dt if self.transitionX >= Config.SCREEN_W then self.transitionX = Config.SCREEN_W self.transitionPhase = TransitionPhase.SWAP end elseif self.transitionPhase == TransitionPhase.SWAP then self:loadRoom(self.transitionPendingRoom) self.transitionX = 0 self.transitionPhase = TransitionPhase.FADE_OUT elseif self.transitionPhase == TransitionPhase.FADE_OUT then self.transitionX = self.transitionX + self.transitionSpeed * dt if self.transitionX >= Config.SCREEN_W then self.transitionPhase = nil end end end if self:updateTransition(dt) then self:advanceRoom() end end function GameState:draw() self.context.roomManager.currentRoomVisual:drawBack() for _, combatant in ipairs(self.context.combatants) do if combatant.state == CombatantState.ALIVE then combatant.behavior:draw() for _, skill in ipairs(combatant.activeSkills) do skill:draw() end elseif combatant.state == CombatantState.DYING then combatant:drawDying() end combatant:drawFloatingText() end for _, drop in ipairs(self.context.drops) do drop:draw() end for _, proj in ipairs(self.context.projectiles) do proj:draw() end self.context.roomManager.currentRoomVisual:drawFront() self:drawTransitionZone() ParticlePlayer:draw() if self.hud then self.hud:draw() end self:drawTransition() if self.paused then love.graphics.setColor(0, 0, 0, 0.5) love.graphics.rectangle("fill", 0, 0, Config.SCREEN_W, Config.SCREEN_H) love.graphics.setColor(1, 1, 1, 1) Utils.printWithShadow("PAUSED", 280, 170) end end function GameState:drawTransitionZone() love.graphics.setColor(0.6, 0.6, 0.6, 0.15) love.graphics.rectangle("fill", Config.ROOM_TRANSITION_ZONE_X, 0, Config.ROOM_W - Config.ROOM_TRANSITION_ZONE_X, Config.ROOM_H) love.graphics.setColor(0.6, 0.6, 0.6, 0.4) love.graphics.line(Config.ROOM_TRANSITION_ZONE_X, 0, Config.ROOM_TRANSITION_ZONE_X, Config.ROOM_H) if self.canTransition then local progress = self:getTransitionProgress() local barW = Config.ROOM_W - Config.ROOM_TRANSITION_ZONE_X love.graphics.setColor(0.3, 0.3, 0.3, 1) love.graphics.rectangle("fill", barW > 0 and Config.ROOM_TRANSITION_ZONE_X or 0, 2, barW, 4) love.graphics.setColor(0.8, 0.8, 0.3, 1) love.graphics.rectangle("fill", Config.ROOM_TRANSITION_ZONE_X, 2, barW * progress, 4) end end function GameState:drawTransition() if self.transitionPhase then local tw = self.transitionEdgeWidth local tx = self.transitionX if self.transitionPhase == TransitionPhase.FADE_IN then love.graphics.setColor(0, 0, 0, 1) love.graphics.rectangle("fill", 0, 0, tx, Config.SCREEN_H) love.graphics.setColor(1, 1, 1, 1) for i = 0, tw - 1 do local a = 1 - i / tw love.graphics.setColor(0, 0, 0, a) love.graphics.line(tx + i, 0, tx + i, Config.SCREEN_H) end love.graphics.setColor(1, 1, 1, 1) elseif self.transitionPhase == TransitionPhase.SWAP then love.graphics.setColor(0, 0, 0, 1) love.graphics.rectangle("fill", 0, 0, Config.SCREEN_W, Config.SCREEN_H) love.graphics.setColor(1, 1, 1, 1) elseif self.transitionPhase == TransitionPhase.FADE_OUT then love.graphics.setColor(0, 0, 0, 1) love.graphics.rectangle("fill", tx, 0, Config.SCREEN_W - tx, Config.SCREEN_H) love.graphics.setColor(1, 1, 1, 1) for i = 0, tw - 1 do local a = i / tw love.graphics.setColor(0, 0, 0, a) love.graphics.line(tx - i, 0, tx - i, Config.SCREEN_H) end love.graphics.setColor(1, 1, 1, 1) end end end function GameState:allPlayersInZone() for _, player in ipairs(self.context.players) do if player.x + player.width < Config.ROOM_TRANSITION_ZONE_X then return false end end return true end function GameState:updateTransition(dt) if not self.canTransition then self.transitionTimer = 0 return false end if self:allPlayersInZone() then self.transitionTimer = self.transitionTimer + dt if self.transitionTimer >= Config.ROOM_TRANSITION_TIME then return true end else self.transitionTimer = 0 end return false end function GameState:getTransitionProgress() if not self.canTransition then return 0 end if self.transitionTimer >= Config.ROOM_TRANSITION_TIME then return 1 end return self.transitionTimer / Config.ROOM_TRANSITION_TIME end function GameState:exit() self.context:reset() end return GameState