-- PlayerBehavior: input-driven logic + rendering for team-1 combatants local BaseBehavior = require("behaviors.base") local Behavior = {} Behavior.__index = Behavior setmetatable(Behavior, { __index = BaseBehavior }) function Behavior:new(combatant) self = setmetatable(BaseBehavior:new(combatant), Behavior) self.playerIdx = self.combatant.playerIdx or 0 self.inputManager = self.context.inputManager self.spawnTimer = Config.PLAYER_SPAWN_TIME return self end function Behavior:update(dt) BaseBehavior.update(self, dt) local moveX = 0 if self.spawnTimer > 0 then self.combatant.sprite:setAnimation("walk") self.combatant.velX = self.combatant.speed / 4 self.spawnTimer = self.spawnTimer - dt if self.spawnTimer <= 0 then self.combatant.velX = 0 end end if not self.inputManager or self:isDisabled() then return end if self.inputManager:isDown(self.playerIdx, self.inputManager.ACTIONS.MOVE_LEFT) then moveX = -1 elseif self.inputManager:isDown(self.playerIdx, self.inputManager.ACTIONS.MOVE_RIGHT) then moveX = 1 end -- Block movement and actions during knockback if self.combatant.knockbackTimer <= 0 then self.combatant.moveX = moveX if moveX ~= 0 then self.combatant.facing = moveX > 0 and 1 or -1 end -- Jump buffer if self.inputManager:wasPressed(self.playerIdx, self.inputManager.ACTIONS.JUMP) then self.combatant.jumpBufferTimer = Config.JUMP_BUFFER_TIME end self.combatant.jumpBufferTimer = self.combatant.jumpBufferTimer - dt if self.combatant.jumpBufferTimer < 0 then self.combatant.jumpBufferTimer = 0 end -- Coyote time if self.combatant.grounded then self.combatant.coyoteTimer = Config.COYOTE_TIME else self.combatant.coyoteTimer = self.combatant.coyoteTimer - dt if self.combatant.coyoteTimer < 0 then self.combatant.coyoteTimer = 0 end end -- Jump if self.combatant.jumpBufferTimer > 0 and self.combatant.coyoteTimer > 0 then self.combatant.velY = Config.JUMP_SPEED self.combatant.coyoteTimer = 0 self.combatant.jumpBufferTimer = 0 Sound.play("jump") end -- Attack charge mechanic local chargeCooldown = self.combatant.skillCooldowns[self.combatant.skillAssets[PlayerSkillSlot.CHARGE].name] or 0 local chargeTimeConfig = self.combatant.skillAssets[PlayerSkillSlot.CHARGE].config.chargeTime or 1.0 if self.inputManager:isDown(self.playerIdx, self.inputManager.ACTIONS.ATTACK) then self.combatant.chargeTime = ((chargeCooldown <= 0 and self.combatant.chargeTime) or 0) + dt if chargeCooldown <= 0 and self.combatant.chargeTime >= Config.PLAYER_CHARGE_START_TIME and not self.combatant.isCharging then self.combatant.isCharging = true Sound.play("charge") self.combatant.chargeEffect = ParticlePlayer:play("charge_up", 0, 0, { duration = chargeTimeConfig - .5 }) end else if self.combatant.isCharging and self.combatant.weapon and self.combatant.weapon.chargeSkill then if self.combatant.chargeTime >= chargeTimeConfig then self.combatant:activateSkillBySlot(PlayerSkillSlot.CHARGE) end elseif self.combatant.chargeTime > 0 and self.combatant.chargeTime < Config.PLAYER_CHARGE_START_TIME then self.combatant:activateSkillBySlot(PlayerSkillSlot.ATTACK) end self.combatant.isCharging = false if self.combatant.chargeTime < chargeTimeConfig then ParticlePlayer:stop(self.combatant.chargeEffect) end self.combatant.chargeTime = 0 end -- Consumable if self.inputManager:wasPressed(self.playerIdx, self.inputManager.ACTIONS.ITEM) then self.combatant:activateSkillBySlot(PlayerSkillSlot.CONSUMABLE) self.combatant.skillAssets[PlayerSkillSlot.CONSUMABLE] = nil end -- Defensive skill if self.inputManager:wasPressed(self.playerIdx, self.inputManager.ACTIONS.DEFEND) then self.combatant:activateSkillBySlot(PlayerSkillSlot.DEFENSIVE) end end -- Pickup / swap drops on INTERACT if self.inputManager:wasPressed(self.playerIdx, self.inputManager.ACTIONS.INTERACT) then local pickupRange = 24 local closestDrop = nil local closestDist = math.huge for _, drop in ipairs(self.context.drops) do if drop.state == DropState.IDLE then local dist = Utils.getDistance( self.combatant.x + self.combatant.width / 2, self.combatant.y + self.combatant.height / 2, drop.x + drop.width / 2, drop.y + drop.height / 2 ) if dist < pickupRange and dist < closestDist then closestDist = dist closestDrop = drop end end end if closestDrop then local dropX = self.combatant.x + self.combatant.width / 2 - (Config.DROP_SPRITE_SIZE[1] / 2) + self.combatant.facing * (Config.DROP_SPRITE_SIZE[1] / 2) local dropY = self.combatant.y + self.combatant.height / 2 - (Config.DROP_SPRITE_SIZE[2] / 2) closestDrop.state = DropState.QUEUED_PICKUP closestDrop.targetX = dropX closestDrop.targetY = dropY local oldDrop = nil if closestDrop.type == DropType.WEAPON then local oldWeapon = self.combatant.weapon self.combatant:equipWeapon(closestDrop.item) if oldWeapon and oldWeapon.drop then oldDrop = Drop:new(oldWeapon.drop, { type = DropType.WEAPON, item = oldWeapon, state = DropState.DROP, x = dropX, y = dropY, }) oldDrop.onComplete = closestDrop table.insert(self.context.drops, oldDrop) else closestDrop:setPickup(dropX, dropY) end elseif closestDrop.type == DropType.CONSUMABLE then local oldConsumableAsset = self.combatant.skillAssets[PlayerSkillSlot.CONSUMABLE] self.combatant:equipSkill(PlayerSkillSlot.CONSUMABLE, closestDrop.item) if oldConsumableAsset then oldDrop = Drop:new(oldConsumableAsset.drop, { type = DropType.CONSUMABLE, item = oldConsumableAsset.name, state = DropState.DROP, x = dropX, y = dropY, }) oldDrop.onComplete = closestDrop table.insert(self.context.drops, oldDrop) else closestDrop:setPickup(dropX, dropY) end end end end -- Landing sound if not self.combatant.prevGrounded and self.combatant.grounded and self.combatant.velY >= 0 then Sound.play("land") end self.combatant.prevGrounded = self.combatant.grounded -- Charging particles if self.combatant.isCharging then self.combatant.velX = self.combatant.velX / 10 local cx = self.combatant.x + self.combatant.width / 2 local cy = self.combatant.y + (self.combatant.visualOffsetY or 0) + self.combatant.height / 2 if self.combatant.chargeEffect then self.combatant.chargeEffect.x = cx self.combatant.chargeEffect.y = cy end end -- Animation: skill > charging > jump > walk > idle local targetAnim = "idle" if moveX ~= 0 then targetAnim = "walk" end if not self.combatant.grounded then targetAnim = "jump" end if self.combatant.isCharging then targetAnim = "charging" end if self.combatant.skillAnimation then targetAnim = self.combatant.skillAnimation end self.combatant.sprite:setAnimation(targetAnim) end function Behavior:draw() local oy = self.combatant.visualOffsetY or 0 love.graphics.setColor(1, 1, 1, 1 - (self.spawnTimer / Config.PLAYER_SPAWN_TIME)) self.combatant:drawSprite() love.graphics.setColor(1, 1, 1, 1) -- Charge gauge if self.combatant.isCharging and self.combatant.weapon and self.combatant.weapon.chargeSkill then local barW = 32 local barH = 4 local barX = self.combatant.x + self.combatant.width / 2 - barW / 2 local barY = self.combatant.y + oy - 10 local chargeTime = self.combatant.skillAssets[PlayerSkillSlot.CHARGE].config.chargeTime or 1.0 local pct = math.min(1, self.combatant.chargeTime / chargeTime) love.graphics.setColor(0.3, 0.3, 0.3, 1) love.graphics.rectangle("fill", barX, barY, barW, barH) love.graphics.setColor(0.9, 0.7, 0.2, 1) love.graphics.rectangle("fill", barX, barY, barW * pct, barH) love.graphics.setColor(0.1, 0.1, 0.1, 1) love.graphics.rectangle("line", barX, barY, barW, barH) love.graphics.setColor(1, 1, 1, 1) end end return Behavior