443 lines
13 KiB
Lua
443 lines
13 KiB
Lua
--------------------------------------------------------------------------------
|
|
function init()
|
|
self.timers = createTimers()
|
|
|
|
self.state = stateMachine.create({
|
|
"smashAttack",
|
|
"riseState",
|
|
"followState",
|
|
"rangedAttack",
|
|
"followState",
|
|
"meleeAttack",
|
|
"followState",
|
|
})
|
|
|
|
self.state.enteringState = function(stateName)
|
|
self.state.moveStateToEnd(stateName)
|
|
end
|
|
|
|
self.state.leavingState = function(stateName)
|
|
self.hoverTimer = nil
|
|
end
|
|
|
|
entity.setAggressive(true)
|
|
|
|
-- Setup blinking
|
|
for i = 1, 3 do
|
|
local animationStateName = "eye" .. i
|
|
local blinkDelay = entity.randomizeParameterRange("blinkTimeRange")
|
|
self.timers.start(blinkDelay, function()
|
|
local animation = entity.animationState(animationStateName)
|
|
if animation == "open" then
|
|
entity.setAnimationState(animationStateName, "blink")
|
|
end
|
|
return blinkDelay
|
|
end)
|
|
end
|
|
|
|
self.state.pickState({ requestedState = "entrance" })
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
function update(dt)
|
|
if not self.state.update(dt) then
|
|
mcontroller.controlFly({ 0, 0 })
|
|
end
|
|
|
|
self.timers.tick(dt)
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
function moveTo(destination, maxSpeed)
|
|
moveBy(world.distance(destination, mcontroller.position()), maxSpeed)
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
function moveBy(delta, maxSpeed)
|
|
if maxSpeed ~= nil then
|
|
local speed = world.magnitude(delta)
|
|
delta = vec2.mul(vec2.norm(delta), math.min(speed, maxSpeed))
|
|
end
|
|
|
|
mcontroller.setVelocity(vec2.div(delta, script.updateDt()))
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
function hoverOffset(dt)
|
|
if self.hoverTimer == nil then
|
|
self.hoverTimer = 0
|
|
else
|
|
self.hoverTimer = self.hoverTimer + dt
|
|
end
|
|
|
|
local angle = 2.0 * math.pi * entity.configParameter("hoverFrequency") * self.hoverTimer
|
|
return entity.configParameter("hoverAmplitude") * math.sin(angle)
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
function findTargets(radius)
|
|
local position = mcontroller.position()
|
|
local targetIds = world.entityQuery(position, radius, {
|
|
-- validTargetOf = entity.id(), -- deprecated, but so is this whole behavior file
|
|
includedTypes = {"creature"},
|
|
order = "nearest"
|
|
})
|
|
|
|
-- Prefer an existing target as long as it stays within the search radius
|
|
if self.targetId ~= nil then
|
|
local targetPosition = world.entityPosition(self.targetId)
|
|
if targetPosition ~= nil then
|
|
local distance = world.magnitude(targetPosition, position)
|
|
if distance < radius then
|
|
table.insert(targetIds, self.targetId)
|
|
else
|
|
self.targetId = nil
|
|
end
|
|
else
|
|
self.targetId = nil
|
|
end
|
|
end
|
|
|
|
return targetIds
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
function closestCraterOffset(targetPosition)
|
|
local minDistance, bestOffset, bestOffsetIndex = nil, nil, nil
|
|
|
|
for index, offset in ipairs(entity.configParameter("craterOffsets")) do
|
|
local distance = world.magnitude(entity.toAbsolutePosition(offset), targetPosition)
|
|
if minDistance == nil or distance < minDistance then
|
|
minDistance = distance
|
|
bestOffset = offset
|
|
bestOffsetIndex = index
|
|
end
|
|
end
|
|
|
|
return bestOffset, bestOffsetIndex
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
riseState = {
|
|
riseSpeedFraction = 0.15,
|
|
riseHeight = 18,
|
|
maxRiseTime = 4,
|
|
pauseTime = 2,
|
|
}
|
|
|
|
function riseState.enterWith(params)
|
|
if params.requestedState ~= "rise" then return nil end
|
|
return { initialPosition = mcontroller.position(), timer = 0 }
|
|
end
|
|
|
|
function riseState.update(dt, stateData)
|
|
-- Pause for a bit after rising up
|
|
if stateData.pauseTimer ~= nil then
|
|
if stateData.hoverPosition == nil then
|
|
stateData.hoverPosition = mcontroller.position()
|
|
end
|
|
moveTo(vec2.add(stateData.hoverPosition, { 0, hoverOffset(dt) }))
|
|
|
|
if entity.animationState("eye1") == "closed" then
|
|
entity.setAnimationState("eye1", "open")
|
|
entity.setAnimationState("eye2", "open")
|
|
entity.setAnimationState("eye3", "open")
|
|
end
|
|
|
|
stateData.pauseTimer = stateData.pauseTimer - dt
|
|
return stateData.pauseTimer <= 0
|
|
end
|
|
|
|
local distance = world.magnitude(mcontroller.position(), stateData.initialPosition)
|
|
stateData.timer = stateData.timer + dt
|
|
if distance >= riseState.riseHeight or stateData.timer >= riseState.maxRiseTime then
|
|
stateData.pauseTimer = riseState.pauseTime
|
|
end
|
|
|
|
moveBy({ 0, entity.flySpeed() * riseState.riseSpeedFraction })
|
|
|
|
return false
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
followState = {
|
|
targetSearchRadius = 30,
|
|
duration = 10,
|
|
heightOffset = 15,
|
|
trackTargetXInterval = 0.25,
|
|
trackTargetYInterval = 1,
|
|
moveSpeedFraction = 0.1,
|
|
}
|
|
|
|
followState.enter = function()
|
|
local targetIds = findTargets(followState.targetSearchRadius)
|
|
if #targetIds > 0 then
|
|
return {
|
|
timer = followState.duration,
|
|
targetId = targetIds[1],
|
|
targetPosition = world.entityPosition(targetIds[1]),
|
|
trackTargetXTimer = followState.trackTargetXInterval,
|
|
trackTargetYTimer = followState.trackTargetYInterval,
|
|
}
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
followState.update = function(dt, stateData)
|
|
local targetPosition = world.entityPosition(stateData.targetId)
|
|
if targetPosition == nil then return true end
|
|
|
|
stateData.targetPosition[1] = targetPosition[1]
|
|
stateData.trackTargetXTimer = stateData.trackTargetYTimer - dt
|
|
if stateData.trackTargetXTimer <= 0 then
|
|
stateData.targetPosition[1] = targetPosition[1]
|
|
stateData.trackTargetXTimer = followState.trackTargetXInterval
|
|
end
|
|
|
|
stateData.trackTargetYTimer = stateData.trackTargetYTimer - dt
|
|
if stateData.trackTargetYTimer <= 0 then
|
|
stateData.targetPosition[2] = targetPosition[2]
|
|
stateData.trackTargetYTimer = followState.trackTargetYInterval
|
|
end
|
|
|
|
local followPosition = {
|
|
stateData.targetPosition[1],
|
|
stateData.targetPosition[2] + followState.heightOffset + hoverOffset(dt)
|
|
}
|
|
moveTo(followPosition, entity.flySpeed() * followState.moveSpeedFraction)
|
|
|
|
stateData.timer = stateData.timer - dt
|
|
return stateData.timer <= 0
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Rise up a bit (optional), then smash down in the target's vicinity
|
|
smashAttack = {
|
|
entranceTargetSearchRadius = 100,
|
|
targetSearchRadius = 50,
|
|
|
|
riseSpeedFraction = 0.2,
|
|
riseHeight = 22,
|
|
riseMaxTime = 4,
|
|
|
|
smashSpeedFraction = 0.5,
|
|
smashMaxTime = 3,
|
|
smashTargetOffset = { 0, -2 },
|
|
|
|
cooldown = 1,
|
|
|
|
explosionOffset = { 0, -6.25 },
|
|
}
|
|
|
|
function smashAttack.enter()
|
|
local targetIds = findTargets(smashAttack.targetSearchRadius)
|
|
if #targetIds == 0 then return nil end
|
|
|
|
return {
|
|
targetId = targetIds[1],
|
|
targetPosition = world.entityPosition(targetIds[1]),
|
|
riseTimer = smashAttack.riseMaxTime,
|
|
}
|
|
end
|
|
|
|
function smashAttack.enterWith(params)
|
|
-- Called on initial spawn, this is just a version of this state with some
|
|
-- additional effects/options
|
|
if params.requestedState ~= "entrance" then return nil end
|
|
|
|
local direction = { 0, -1 }
|
|
|
|
local targetIds = findTargets(smashAttack.entranceTargetSearchRadius)
|
|
if #targetIds > 0 then
|
|
direction = smashAttack.targetDirection(targetIds[1])
|
|
end
|
|
|
|
return { direction = direction }
|
|
end
|
|
|
|
function smashAttack.update(dt, stateData)
|
|
-- Delay one update after crashing to allow the explosion to trigger
|
|
if stateData.landed then
|
|
self.state.pickState({ requestedState = "rise" })
|
|
return true, smashAttack.cooldown
|
|
end
|
|
|
|
local position = mcontroller.position()
|
|
|
|
if stateData.riseTimer ~= nil then
|
|
-- Rising up
|
|
if not world.entityExists(stateData.targetId) then return true, smashAttack.cooldown end
|
|
local delta = world.distance(stateData.targetPosition, mcontroller.position())
|
|
|
|
if delta[2] < -smashAttack.riseHeight or stateData.riseTimer <= 0 then
|
|
stateData.direction = smashAttack.targetDirection(stateData.targetId)
|
|
stateData.riseTimer = nil
|
|
else
|
|
moveBy({ 0, entity.flySpeed() * smashAttack.riseSpeedFraction })
|
|
stateData.riseTimer = stateData.riseTimer - dt
|
|
end
|
|
else
|
|
-- Smashing down
|
|
local angle = vec2.angle(stateData.direction)
|
|
if stateData.direction[1] < 0 then angle = math.pi - angle end
|
|
entity.rotateGroup("flames", angle + math.pi / 2, true)
|
|
entity.setAnimationState("flames", "idle")
|
|
|
|
local delta = vec2.mul(stateData.direction, entity.flySpeed() * smashAttack.smashSpeedFraction)
|
|
|
|
moveBy(vec2.mul(vec2.norm(delta), entity.flySpeed() * smashAttack.smashSpeedFraction))
|
|
|
|
local bounds = entity.configParameter("metaBoundBox")
|
|
bounds = {
|
|
bounds[1] + position[1] + delta[1],
|
|
bounds[2] + position[2] + delta[2],
|
|
bounds[3] + position[1] + delta[1],
|
|
bounds[4] + position[2] + delta[2]
|
|
}
|
|
moveBy(delta)
|
|
|
|
if stateData.smashTimer == nil then
|
|
stateData.smashTimer = smashAttack.smashMaxTime
|
|
end
|
|
stateData.smashTimer = stateData.smashTimer - dt
|
|
if stateData.smashTimer <= 0 then
|
|
self.state.pickState({ requestedState = "rise" })
|
|
return true, smashAttack.cooldown
|
|
end
|
|
|
|
if world.rectTileCollision(bounds) then
|
|
-- entity.setFireDirection(smashAttack.explosionOffset, { 0, 0 })
|
|
-- entity.startFiring("crashexplosion")
|
|
stateData.landed = true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function smashAttack.leavingState(stateData)
|
|
-- entity.stopFiring()
|
|
entity.setAnimationState("flames", "hidden")
|
|
end
|
|
|
|
function smashAttack.targetDirection(targetId)
|
|
local targetPosition = vec2.add(world.entityPosition(targetId), smashAttack.smashTargetOffset)
|
|
local toTarget = world.distance(targetPosition, mcontroller.position())
|
|
return vec2.norm(toTarget)
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
rangedAttack = {
|
|
targetSearchRadius = 30,
|
|
duration = 1,
|
|
cooldown = 1,
|
|
}
|
|
|
|
function rangedAttack.enter()
|
|
local targetIds = findTargets(rangedAttack.targetSearchRadius)
|
|
if #targetIds == 0 then return nil end
|
|
|
|
return {
|
|
targetId = targetIds[1],
|
|
targetPosition = world.entityPosition(targetIds[1]),
|
|
timer = rangedAttack.duration,
|
|
}
|
|
end
|
|
|
|
function rangedAttack.enteringState(stateData)
|
|
local fireOffset = closestCraterOffset(stateData.targetPosition)
|
|
-- entity.setFireDirection(fireOffset, vec2.norm(fireOffset))
|
|
-- entity.startFiring("meteor")
|
|
end
|
|
|
|
function rangedAttack.update(dt, stateData)
|
|
mcontroller.controlFly({ 0, 0 })
|
|
|
|
stateData.timer = stateData.timer - dt
|
|
return stateData.timer <= 0, rangedAttack.cooldown
|
|
end
|
|
|
|
function rangedAttack.leavingState(stateData)
|
|
-- entity.stopFiring()
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
meleeAttack = {
|
|
targetSearchRadius = 30,
|
|
targetOffset = { 0, 6 },
|
|
moveSpeedFraction = 0.3,
|
|
tentacleSearchRadius = 12,
|
|
maxTime = 4,
|
|
pauseTime = 2,
|
|
cooldown = 1,
|
|
minDistance = 7,
|
|
|
|
projectileLifetime = 0.5,
|
|
}
|
|
|
|
function meleeAttack.enter()
|
|
local targetIds = findTargets(meleeAttack.targetSearchRadius)
|
|
if #targetIds == 0 then return nil end
|
|
|
|
return {
|
|
targetId = targetIds[1],
|
|
targetPosition = vec2.add(world.entityPosition(targetIds[1]), meleeAttack.targetOffset),
|
|
timer = meleeAttack.maxTime,
|
|
tentacleProjectileEntityIds = { nil, nil, nil, nil, nil, nil, nil, nil }
|
|
}
|
|
end
|
|
|
|
function meleeAttack.update(dt, stateData)
|
|
local targetIds = findTargets(meleeAttack.tentacleSearchRadius)
|
|
for _, targetId in pairs(targetIds) do
|
|
local offset, index = closestCraterOffset(world.entityPosition(targetId))
|
|
local animationStateName = "tentacle" .. index
|
|
if entity.animationState(animationStateName) == "hidden" then
|
|
entity.setAnimationState(animationStateName, "extend")
|
|
end
|
|
end
|
|
|
|
local craterOffsets = entity.configParameter("craterOffsets")
|
|
for i = 1, 8 do
|
|
local animationStateName = "tentacle" .. i
|
|
local animation = entity.animationState(animationStateName)
|
|
if animation == "idle" then
|
|
if stateData.tentacleProjectileEntityIds[i] == nil or not world.entityExists(stateData.tentacleProjectileEntityIds[i]) then
|
|
stateData.tentacleProjectileEntityIds[i] = world.spawnProjectile("tentaclecomet" .. i, mcontroller.position(), entity.id(), craterOffsets[i], true, {
|
|
speed = 0,
|
|
power = 20,
|
|
timeToLive = meleeAttack.projectileLifetime
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
if stateData.pauseTimer ~= nil then
|
|
if stateData.hoverPosition == nil then
|
|
stateData.hoverPosition = mcontroller.position()
|
|
end
|
|
moveTo(vec2.add(stateData.hoverPosition, { 0, hoverOffset(dt) }))
|
|
|
|
stateData.pauseTimer = stateData.pauseTimer - dt
|
|
return stateData.pauseTimer <= 0, meleeAttack.cooldown
|
|
end
|
|
|
|
local distance = world.magnitude(stateData.targetPosition, mcontroller.position())
|
|
if distance <= meleeAttack.minDistance or stateData.timer <= 0 then
|
|
stateData.pauseTimer = meleeAttack.pauseTime
|
|
else
|
|
moveTo(stateData.targetPosition, entity.flySpeed() * meleeAttack.moveSpeedFraction)
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function meleeAttack.leavingState(stateData)
|
|
for i = 1, 8 do
|
|
local animationStateName = "tentacle" .. i
|
|
if entity.animationState(animationStateName) ~= "hidden" then
|
|
entity.setAnimationState(animationStateName, "retract")
|
|
end
|
|
end
|
|
end
|