require "/scripts/util.lua" require "/items/active/weapons/weapon.lua" -- whip primary attack plus swinging from torches like a boss SuperWhipCrack = WeaponAbility:new() function SuperWhipCrack:init() self.damageConfig.baseDamage = self.chainDps * self.fireTime self.weapon:setStance(self.stances.idle) animator.setAnimationState("attack", "idle") activeItem.setScriptedAnimationParameter("chains", nil) self.cooldownTimer = self:cooldownTime() self.weapon.onLeaveAbility = function() self.weapon:setStance(self.stances.idle) end self.projectileConfig = self.projectileConfig or {} self.chain = config.getParameter("chain") self.anchorObjects = config.getParameter("anchorObjects", {"torch"}) self.anchor = nil self.snapDistance = config.getParameter("snapDistance", 3.0) end -- Ticks on every update regardless if this is the active ability function SuperWhipCrack:update(dt, fireMode, shiftHeld) WeaponAbility.update(self, dt, fireMode, shiftHeld) self.cooldownTimer = math.max(0, self.cooldownTimer - self.dt) if self.fireMode == "primary" and self:canStartAttack() then self:setState(self.windup) self.fireHeld = true elseif self.fireMode ~= "primary" and self.fireHeld then self.fireHeld = false self:disconnect() end end function SuperWhipCrack:canStartAttack() return not self.weapon.currentAbility and self.cooldownTimer == 0 end -- State: windup function SuperWhipCrack:windup() self.weapon:setStance(self.stances.windup) animator.setAnimationState("attack", "windup") util.wait(self.stances.windup.duration) self:setState(self.extend) end -- State: extend function SuperWhipCrack:extend() self.weapon:setStance(self.stances.extend) animator.setAnimationState("attack", "extend") animator.playSound("swing") util.wait(self.stances.extend.duration) if self.fireHeld then self.anchor = self:findAnchor() end if self.anchor then animator.setAnimationState("attack", "fire") self:setState(self.swing) else animator.setAnimationState("attack", "fire") self:setState(self.fire) end end -- State: swing function SuperWhipCrack:swing() self.weapon:setStance(self.stances.swing) self.weapon:updateAim() self.cooldownTimer = self:cooldownTime() while self.anchor do if world.entityExists(self.anchor) then local chainEndPos = vec2.add(world.entityPosition(self.anchor), {0.5, 0.5}) local chainStartPos = vec2.add(mcontroller.position(), activeItem.handPosition(self.chain.startOffset)) if not world.lineCollision(chainStartPos, chainEndPos) then local aimVector = world.distance(chainEndPos, chainStartPos) local swingAngle = vec2.angle(aimVector) if not mcontroller.onGround() then mcontroller.controlApproachVelocityAlongAngle(swingAngle, 0, 1000, true) else mcontroller.controlModifiers({movementSuppressed = true}) end if self.weapon.aimDirection < 0 then self.weapon.aimAngle = vec2.angle({-aimVector[1], aimVector[2]}) else self.weapon.aimAngle = swingAngle end self.weapon:updateAim() self.chain.endPosition = chainEndPos activeItem.setScriptedAnimationParameter("chains", {self.chain}) coroutine.yield() else self:disconnect() end else self:disconnect() end end end function SuperWhipCrack:disconnect() self.anchor = nil animator.setAnimationState("attack", "idle") self.chain.endPosition = nil activeItem.setScriptedAnimationParameter("chains", nil) end -- State: fire function SuperWhipCrack:fire() self.weapon:setStance(self.stances.fire) self.weapon:updateAim() local chainStartPos = vec2.add(mcontroller.position(), activeItem.handPosition(self.chain.startOffset)) local chainLength = world.magnitude(chainStartPos, activeItem.ownerAimPosition()) chainLength = math.min(self.chain.length[2], math.max(self.chain.length[1], chainLength)) self.chain.endOffset = vec2.add(self.chain.startOffset, {chainLength, 0}) local collidePoint = world.lineCollision(chainStartPos, vec2.add(mcontroller.position(), activeItem.handPosition(self.chain.endOffset))) if collidePoint then chainLength = world.magnitude(chainStartPos, collidePoint) - 0.25 if chainLength < self.chain.length[1] then animator.setAnimationState("attack", "idle") return else self.chain.endOffset = vec2.add(self.chain.startOffset, {chainLength, 0}) end end local chainEndPos = vec2.add(mcontroller.position(), activeItem.handPosition(self.chain.endOffset)) activeItem.setScriptedAnimationParameter("chains", {self.chain}) animator.resetTransformationGroup("endpoint") animator.translateTransformationGroup("endpoint", self.chain.endOffset) animator.burstParticleEmitter("crack") animator.playSound("crack") self.projectileConfig.power = self:crackDamage() self.projectileConfig.powerMultiplier = activeItem.ownerPowerMultiplier() local projectileAngle = vec2.withAngle(self.weapon.aimAngle) if self.weapon.aimDirection < 0 then projectileAngle[1] = -projectileAngle[1] end world.spawnProjectile( self.projectileType, chainEndPos, activeItem.ownerEntityId(), projectileAngle, false, self.projectileConfig ) util.wait(self.stances.fire.duration, function() if self.damageConfig.baseDamage > 0 then self.weapon:setDamage(self.damageConfig, {self.chain.startOffset, {self.chain.endOffset[1] + 0.75, self.chain.endOffset[2]}}, self.fireTime) end end) animator.setAnimationState("attack", "idle") activeItem.setScriptedAnimationParameter("chains", nil) self.cooldownTimer = self:cooldownTime() end function SuperWhipCrack:cooldownTime() return self.fireTime - (self.stances.windup.duration + self.stances.extend.duration + self.stances.fire.duration) end function SuperWhipCrack:uninit(unloaded) self.weapon:setDamage() activeItem.setScriptedAnimationParameter("chains", nil) end function SuperWhipCrack:chainDamage() return (self.chainDps * self.fireTime) * config.getParameter("damageLevelMultiplier") end function SuperWhipCrack:crackDamage() return (self.crackDps * self.fireTime) * config.getParameter("damageLevelMultiplier") end function SuperWhipCrack:findAnchor() local objectsNearCursor = world.objectQuery(activeItem.ownerAimPosition(), 5, {boundMode = "metaboundbox", order = "nearest"}) if #objectsNearCursor > 0 then local objectName = world.entityName(objectsNearCursor[1]) local anchorValid = false for _, anchorObject in pairs(self.anchorObjects) do if anchorObject == objectName then anchorValid = true break end end if anchorValid then local pos = vec2.add(world.entityPosition(objectsNearCursor[1]), {0.5, 0.5}) local distToCursor = world.magnitude(pos, activeItem.ownerAimPosition()) local distToHand = world.magnitude(pos, vec2.add(mcontroller.position(), activeItem.handPosition(self.chain.startOffset))) if distToCursor <= self.snapDistance and distToHand <= self.chain.length[2] and distToHand >= self.chain.length[1] then return objectsNearCursor[1] end end end end