834 lines
24 KiB
Lua
834 lines
24 KiB
Lua
dofile_once( "data/scripts/lib/coroutines.lua" )
|
|
dofile_once( "data/scripts/lib/utilities.lua" )
|
|
|
|
-- Description of overall boss behavior:
|
|
-- Boss waits for player to pick up item.
|
|
-- When item is collected, combat starts.
|
|
-- During combat if player is nearby, boss performs attacks and mauneuvers randomly (next_phase() sets phase variable).
|
|
-- Some attacks are performed several times in a row (phase_repeats).
|
|
-- If boss suffers enough damage it enters "aggro mode" with a different set of attacks.
|
|
-- If player moves away from the arena, boss gains cell eating ability and starts chasing.
|
|
-- Boss health and abilities vary based on how many orbs player has collected (search for "orbcount").
|
|
|
|
-- enum for changing C++ logic state. keep this in sync with the values in limbboss_system.cpp
|
|
local states = {
|
|
MoveAroundNest = 0,
|
|
FollowPlayer = 1,
|
|
Escape = 2,
|
|
DontMove = 3,
|
|
MoveTo = 4,
|
|
MoveDirectlyTowardsPlayer = 5,
|
|
}
|
|
|
|
local is_dead = false
|
|
local boss_chase = 0
|
|
local is_eye_open = false
|
|
local is_aggro = false
|
|
local orbcount = 0
|
|
local shield_enabled = false
|
|
local did_wait = false
|
|
local death_sound_started = false
|
|
|
|
function init_boss()
|
|
local entity = GetUpdatedEntityID()
|
|
local pos_x, pos_y = EntityGetTransform( entity )
|
|
|
|
local init_comp = get_variable_storage_component(entity, "initialized")
|
|
local orbcount_comp = get_variable_storage_component(entity, "orbcount")
|
|
|
|
-- init boss only once
|
|
if init_comp and ComponentGetValue2(init_comp, "value_bool") == false then
|
|
ComponentSetValue2(init_comp, "value_bool", true)
|
|
|
|
-- Get the amount of orbs to adjust boss difficulty
|
|
-- orbcount = orbcount + new game plus count
|
|
local newgame_n = tonumber( SessionNumbersGetValue("NEW_GAME_PLUS_COUNT") )
|
|
orbcount = GameGetOrbCountThisRun()
|
|
orbcount = orbcount + newgame_n
|
|
|
|
ComponentSetValue2(orbcount_comp, "value_int", orbcount) -- store orbcount for savegames
|
|
|
|
-- Set boss HP based on orbs
|
|
local boss_hp = 46.0 + ( 2.0 ^ (orbcount + 1.3) ) + (orbcount*15.5)
|
|
local comp = EntityGetFirstComponent( entity, "DamageModelComponent" )
|
|
if( comp ~= nil ) then
|
|
ComponentSetValue( comp, "max_hp", tostring(boss_hp) )
|
|
ComponentSetValue( comp, "hp", tostring(boss_hp) )
|
|
end
|
|
|
|
-- no orbs = weaker shield
|
|
if orbcount == 0 then
|
|
EntityAddChild( entity, EntityLoad("data/entities/animals/boss_centipede/boss_centipede_shield_weak.xml", pos_x, pos_y) )
|
|
else
|
|
EntityAddChild( entity, EntityLoad("data/entities/animals/boss_centipede/boss_centipede_shield_strong.xml", pos_x, pos_y) )
|
|
end
|
|
|
|
-- orbs boost damage resistances
|
|
local damagemodel_comp = EntityGetFirstComponent( entity, "DamageModelComponent" )
|
|
if damagemodel_comp ~= nil then
|
|
if orbcount >= 3 then
|
|
ComponentObjectSetValue2(damagemodel_comp, "damage_multipliers", "melee", 1.5)
|
|
ComponentObjectSetValue2(damagemodel_comp, "damage_multipliers", "drill", 0.25)
|
|
ComponentObjectSetValue2(damagemodel_comp, "damage_multipliers", "projectile", 0.2)
|
|
ComponentObjectSetValue2(damagemodel_comp, "damage_multipliers", "fire", 0)
|
|
ComponentObjectSetValue2(damagemodel_comp, "damage_multipliers", "ice", 0)
|
|
ComponentObjectSetValue2(damagemodel_comp, "damage_multipliers", "electricity", 0)
|
|
end
|
|
if( orbcount >= 4 ) then
|
|
EntitySetDamageFromMaterial( entity, "acid", 0.01)
|
|
end
|
|
if orbcount >= 5 then
|
|
ComponentObjectSetValue2(damagemodel_comp, "damage_multipliers", "melee", 1.0)
|
|
ComponentObjectSetValue2(damagemodel_comp, "damage_multipliers", "drill", 0.01)
|
|
end
|
|
if orbcount >= 7 then
|
|
EntitySetDamageFromMaterial( entity, "acid", 0.0)
|
|
end
|
|
if orbcount >= 9 then
|
|
ComponentObjectSetValue2(damagemodel_comp, "damage_multipliers", "slice", 0.5)
|
|
ComponentObjectSetValue2(damagemodel_comp, "damage_multipliers", "physics_hit", 0.5)
|
|
ComponentObjectSetValue2(damagemodel_comp, "damage_multipliers", "radioactive", 0.5)
|
|
ComponentObjectSetValue2(damagemodel_comp, "damage_multipliers", "poison", 0.5)
|
|
end
|
|
if( orbcount >= 11 ) then
|
|
ComponentObjectSetValue2(damagemodel_comp, "damage_multipliers", "melee", 0.5)
|
|
end
|
|
end
|
|
|
|
-- orbs determine material damages
|
|
if( orbcount > 0 ) then
|
|
|
|
-- material weaknesses based on orb count
|
|
if( orbcount == 12 ) then EntitySetDamageFromMaterial( entity, "magic_liquid_polymorph", 0.35) end
|
|
if( orbcount == 13 ) then EntitySetDamageFromMaterial( entity, "magic_liquid_random_polymorph", 0.35) end
|
|
if( orbcount == 14 ) then EntitySetDamageFromMaterial( entity, "magic_liquid_teleportation", 0.35) end
|
|
if( orbcount == 15 ) then EntitySetDamageFromMaterial( entity, "magic_liquid_movement_faster", 0.35) end
|
|
if( orbcount == 16 ) then EntitySetDamageFromMaterial( entity, "material_confusion", 0.35) end
|
|
end
|
|
|
|
-- over 30 orbs immunity to touch spells
|
|
if( orbcount > 30 ) then
|
|
EntityAddTag( entity, "touchmagic_immunity")
|
|
end
|
|
else
|
|
-- make sure orbcount is not lost on game load
|
|
orbcount = ComponentGetValue2(orbcount_comp, "value_int")
|
|
end
|
|
|
|
-- Turn on the limbs
|
|
local children = EntityGetAllChildren( entity )
|
|
|
|
if children ~= nil then
|
|
for _,it in ipairs(children) do
|
|
EntitySetComponentsWithTagEnabled( it, "disabled_at_start", true )
|
|
end
|
|
end
|
|
|
|
-- check if boss was in aggro mode
|
|
is_aggro = get_variable_storage_component(entity, "aggro") ~= nil
|
|
end
|
|
|
|
init_boss()
|
|
|
|
|
|
-- gather some data we're gonna reuse --------------
|
|
local herd_id = get_herd_id( GetUpdatedEntityID() )
|
|
local subphase = 0
|
|
local phase_repeats = 0
|
|
|
|
-- Phase logic
|
|
function phase_chase_slow()
|
|
close_eye()
|
|
set_logic_state( states.FollowPlayer )
|
|
--boss_wait(2 * 60)
|
|
boss_wait(60)
|
|
next_phase()
|
|
end
|
|
|
|
function phase_chase()
|
|
close_eye()
|
|
set_logic_state( states.FollowPlayer )
|
|
boss_wait(140)
|
|
next_phase()
|
|
end
|
|
|
|
function phase_chase_direct()
|
|
close_eye()
|
|
set_logic_state( states.MoveDirectlyTowardsPlayer )
|
|
boss_wait(2 * 80)
|
|
next_phase()
|
|
end
|
|
|
|
function phase_circleshot()
|
|
move_to_reference()
|
|
boss_wait(50) -- a brief pause if shield was previously on
|
|
|
|
shield_on()
|
|
boss_wait(10)
|
|
open_eye()
|
|
GameEntityPlaySound( GetUpdatedEntityID(), "phase_circleshot_start" )
|
|
|
|
local shot_count = 10 + math.floor(orbcount / 3)
|
|
local frame = GameGetFrameNum()
|
|
local r = ProceduralRandomf(frame, frame)
|
|
local spiral_speed = 25 * (r*2-1) -- random turn speed & direction with each attack
|
|
|
|
for i=1,shot_count do
|
|
circleshot(r * 180 + i*spiral_speed)
|
|
boss_wait(12)
|
|
end
|
|
|
|
close_eye()
|
|
shield_off()
|
|
boss_wait(60)
|
|
|
|
set_logic_state( states.FollowPlayer )
|
|
next_phase()
|
|
end
|
|
|
|
function phase_spawn_minion()
|
|
set_logic_state( states.FollowPlayer )
|
|
|
|
local spawn_count = 1 + math.floor(orbcount / 2)
|
|
for i=1,spawn_count do
|
|
if not minion_check_maxcount() then
|
|
open_eye()
|
|
GameEntityPlaySound( GetUpdatedEntityID(), "spawn_minion" )
|
|
spawn_minion()
|
|
boss_wait(30)
|
|
end
|
|
end
|
|
|
|
close_eye()
|
|
next_phase()
|
|
end
|
|
|
|
function phase_firepillar()
|
|
move_to_reference()
|
|
boss_wait(50) -- a brief pause if shield was previously on
|
|
|
|
shield_on()
|
|
boss_wait(10)
|
|
open_eye()
|
|
|
|
GameEntityPlaySound( GetUpdatedEntityID(), "shoot_fire" )
|
|
firepillar()
|
|
|
|
close_eye()
|
|
boss_wait(40)
|
|
--shield_off()
|
|
--boss_wait(20)
|
|
|
|
set_logic_state( states.FollowPlayer )
|
|
next_phase()
|
|
end
|
|
|
|
function phase_explosion()
|
|
set_logic_state( states.FollowPlayer )
|
|
open_eye()
|
|
|
|
GameEntityPlaySound( GetUpdatedEntityID(), "shoot_explosion" )
|
|
if (orbcount < 6) then
|
|
explode()
|
|
else
|
|
explode(true)
|
|
end
|
|
|
|
boss_wait(20)
|
|
close_eye()
|
|
|
|
set_logic_state( states.FollowPlayer )
|
|
next_phase()
|
|
end
|
|
|
|
function phase_homingshot()
|
|
open_eye()
|
|
|
|
local shot_count = 4 + math.floor(orbcount * 0.5)
|
|
for i=1,shot_count do
|
|
homingshot()
|
|
GameEntityPlaySound( GetUpdatedEntityID(), "shoot_homingshot" )
|
|
boss_wait(20)
|
|
end
|
|
|
|
close_eye()
|
|
next_phase()
|
|
end
|
|
|
|
function phase_polymorph()
|
|
open_eye()
|
|
boss_wait(30)
|
|
|
|
polymorphshot()
|
|
GameEntityPlaySound( GetUpdatedEntityID(), "shoot_homingshot" )
|
|
boss_wait(20)
|
|
|
|
close_eye()
|
|
next_phase()
|
|
end
|
|
|
|
function phase_melee()
|
|
local this = GetUpdatedEntityID()
|
|
local pos_x, pos_y = EntityGetTransform( this )
|
|
|
|
open_eye()
|
|
explosion_attack()
|
|
|
|
GameEntityPlaySound( GetUpdatedEntityID(), "phase_rush_start" )
|
|
|
|
-- flinch away so boss won't hang around on top of player
|
|
local dir_x = 0
|
|
local dir_y = -70
|
|
dir_x, dir_y = vec_rotate(dir_x, dir_y, ProceduralRandomf(pos_x, GameGetFrameNum(), -math.pi * 0.5, math.pi * 0.5))
|
|
set_force_coeff_mult(20)
|
|
move_to(pos_x + dir_x, pos_y + dir_y)
|
|
boss_wait(50)
|
|
set_force_coeff_mult(1)
|
|
|
|
close_eye()
|
|
next_phase()
|
|
end
|
|
|
|
function phase_clean_materials()
|
|
set_logic_state( states.FollowPlayer )
|
|
boss_wait(25)
|
|
GameEntityPlaySound( GetUpdatedEntityID(), "shoot_clean_materials" )
|
|
clear_materials()
|
|
next_phase()
|
|
end
|
|
|
|
function phase_aggro()
|
|
set_force_coeff_mult(5)
|
|
set_main_animation("aggro", "aggro")
|
|
|
|
boss_wait(60)
|
|
|
|
-- circle shots to random directions
|
|
local shot_count = 12 + math.floor(orbcount / 3)
|
|
local frame = GameGetFrameNum()
|
|
for i=1,shot_count do
|
|
circleshot_aggro()
|
|
boss_wait(12)
|
|
end
|
|
|
|
boss_wait(20)
|
|
explosion_attack()
|
|
|
|
boss_wait(60)
|
|
|
|
set_logic_state( states.FollowPlayer )
|
|
next_phase()
|
|
end
|
|
|
|
function next_phase()
|
|
shield_off()
|
|
|
|
-- set a new phase
|
|
local entity_id = GetUpdatedEntityID()
|
|
local players = EntityGetWithTag( "player_unit" )
|
|
if #players > 0 then
|
|
local player_id = players[1]
|
|
|
|
local xe,ye = EntityGetTransform( entity_id )
|
|
local xp,yp = EntityGetTransform( player_id )
|
|
|
|
local dist = get_distance(xp,yp,xe,ye)
|
|
|
|
local phases = -- { phase, repeat amount }
|
|
{
|
|
{ phase_chase_slow, 0 },
|
|
{ phase_circleshot, 1 },
|
|
{ phase_spawn_minion, 0 },
|
|
{ phase_firepillar, 2 },
|
|
}
|
|
|
|
-- additional attack mode on higher orbcount
|
|
if orbcount >= 2 then phases[#phases+1] = { phase_homingshot, 0 } end
|
|
|
|
if is_aggro then
|
|
-- aggro mode, replace the usual phases with a more difficult selection
|
|
phases =
|
|
{
|
|
--{ phase_chase, 0 },
|
|
{ phase_aggro, 0 },
|
|
}
|
|
end
|
|
|
|
-- polymorph shots
|
|
if orbcount >= 11 then phases[#phases+1] = { phase_polymorph, 0 } end
|
|
|
|
if dist < 65 and not is_aggro then
|
|
-- do a melee attack
|
|
phase = phase_melee
|
|
phase_repeats = 0
|
|
elseif dist < 700 then -- use attack phases
|
|
-- see if phase has repeats queued before picking a new one
|
|
if phase_repeats > 0 then
|
|
phase_repeats = phase_repeats - 1
|
|
return
|
|
end
|
|
|
|
-- Every phase increases a 'subphase' variable, and at 6 subphase forces the boss to use the clean_materials phase
|
|
if subphase < 6 then
|
|
SetRandomSeed(xe+xp+subphase, ye+yp)
|
|
local next_phase = random_from_array(phases)
|
|
phase = next_phase[1]
|
|
phase_repeats = next_phase[2]
|
|
|
|
subphase = subphase + 1
|
|
else
|
|
subphase = 0
|
|
phase_repeats = 0
|
|
phase = phase_clean_materials
|
|
end
|
|
else -- further away: start chasin'
|
|
phase = phase_chase_direct
|
|
end
|
|
|
|
-- If player is way too far, boss assumes they have left the arena entirely and sets up phase 2
|
|
if (dist > 1300) and (boss_chase == 0) then
|
|
boss_chase = 1
|
|
|
|
local celleater = EntityGetFirstComponent( GetUpdatedEntityID(), "CellEaterComponent" )
|
|
|
|
if (celleater ~= nil) then
|
|
ComponentSetValue( celleater, "eat_probability", tostring(100.0) )
|
|
ComponentSetValue( celleater, "radius", tostring(64.0) )
|
|
end
|
|
|
|
set_force_coeff_mult(5)
|
|
|
|
|
|
phase = phase_chase_direct
|
|
end
|
|
|
|
--print("Boss distance: " .. tostring(dist))
|
|
end
|
|
end
|
|
|
|
|
|
-- actual boss attacks -----------------
|
|
|
|
function circleshot(angle)
|
|
local this = GetUpdatedEntityID()
|
|
local pos_x, pos_y = EntityGetTransform( this )
|
|
|
|
local branches = 6 + orbcount - phase_repeats
|
|
local space = math.floor(360 / branches)
|
|
local speed = 80
|
|
|
|
-- spawn projectiles on each branch
|
|
for i=1,branches do
|
|
local vel_x = math.cos( math.rad(angle) ) * speed
|
|
local vel_y = math.sin( math.rad(angle) ) * speed
|
|
shoot_projectile( this, "data/entities/animals/boss_centipede/orb_circleshot.xml", pos_x, pos_y, vel_x, vel_y )
|
|
angle = angle + space
|
|
end
|
|
|
|
-- muzzle flash & audio
|
|
EntityLoad( "data/entities/particles/muzzle_flashes/muzzle_flash_circular_pink.xml", pos_x, pos_y)
|
|
GamePlaySound( "data/audio/Desktop/projectiles.bank", "projectiles/magic/create", pos_x, pos_y )
|
|
end
|
|
|
|
function circleshot_aggro()
|
|
local this = GetUpdatedEntityID()
|
|
local pos_x, pos_y = EntityGetTransform( this )
|
|
|
|
local branches = 4 + orbcount
|
|
local speed = 120
|
|
local frame = GameGetFrameNum()
|
|
|
|
-- spawn projectiles to random directions
|
|
for i=1,branches do
|
|
local r = ProceduralRandomf(frame + i, frame)
|
|
local angle = r * 360
|
|
local vel_x = math.cos( math.rad(angle) ) * speed
|
|
local vel_y = math.sin( math.rad(angle) ) * speed
|
|
shoot_projectile( this, "data/entities/animals/boss_centipede/orb_circleshot.xml", pos_x, pos_y, vel_x, vel_y )
|
|
end
|
|
|
|
GameCreateParticle( "slime_green", pos_x, pos_y, 50, 0, -20, true, false )
|
|
|
|
-- muzzle flash & audio
|
|
EntityLoad( "data/entities/particles/muzzle_flashes/muzzle_flash_circular_pink.xml", pos_x, pos_y)
|
|
GamePlaySound( "data/audio/Desktop/projectiles.bank", "projectiles/magic/create", pos_x, pos_y )
|
|
end
|
|
|
|
function homingshot()
|
|
local this = GetUpdatedEntityID()
|
|
local pos_x, pos_y = EntityGetTransform( this )
|
|
|
|
local vel_x = 0
|
|
local vel_y = ProceduralRandomf(pos_x, pos_y + GameGetFrameNum(), -200, 50)
|
|
|
|
shoot_projectile( this, "data/entities/animals/boss_centipede/orb_homing.xml", pos_x, pos_y, vel_x, vel_y )
|
|
end
|
|
|
|
function polymorphshot()
|
|
local this = GetUpdatedEntityID()
|
|
local pos_x, pos_y = EntityGetTransform( this )
|
|
|
|
-- triangular launch shape
|
|
shoot_projectile( this, "data/entities/animals/boss_centipede/orb_polymorph.xml", pos_x, pos_y - 10, 0, -50 )
|
|
shoot_projectile( this, "data/entities/animals/boss_centipede/orb_polymorph.xml", pos_x - 5, pos_y, -30, 20 )
|
|
shoot_projectile( this, "data/entities/animals/boss_centipede/orb_polymorph.xml", pos_x - 5, pos_y, -30, 20 )
|
|
end
|
|
|
|
function firepillar()
|
|
local this = GetUpdatedEntityID()
|
|
local pos_x, pos_y = EntityGetTransform( this )
|
|
|
|
local amount = 10 + orbcount - phase_repeats * 2
|
|
local space = math.floor(180 / amount)
|
|
local speed = 150
|
|
local angle = space * 0.5
|
|
|
|
for i=1,amount do
|
|
local vel_x = math.cos( math.rad(angle) ) * speed
|
|
local vel_y = math.sin( math.rad(angle) ) * speed
|
|
vel_y = vel_y - 200
|
|
shoot_projectile( this, "data/entities/animals/boss_centipede/firepillar.xml", pos_x, pos_y, vel_x, vel_y )
|
|
|
|
angle = angle + space
|
|
end
|
|
|
|
-- muzzle flash & audio
|
|
EntityLoad( "data/entities/particles/muzzle_flashes/muzzle_flash_circular.xml", pos_x, pos_y)
|
|
GamePlaySound( "data/audio/Desktop/projectiles.bank", "projectiles/magic/create", pos_x, pos_y )
|
|
end
|
|
|
|
function explode(big_explosion_)
|
|
local big_explosion = big_explosion_ or false
|
|
|
|
local this = GetUpdatedEntityID()
|
|
local pos_x, pos_y = EntityGetTransform( this )
|
|
|
|
if big_explosion then
|
|
shoot_projectile( this, "data/entities/animals/boss_centipede/boss_centipede_explosion_large.xml", pos_x, pos_y, 0, 0 )
|
|
else
|
|
shoot_projectile( this, "data/entities/animals/boss_centipede/boss_centipede_explosion.xml", pos_x, pos_y, 0, 0 )
|
|
end
|
|
end
|
|
|
|
function explosion_attack()
|
|
local this = GetUpdatedEntityID()
|
|
local pos_x, pos_y = EntityGetTransform( this )
|
|
|
|
local attack = shoot_projectile( this, "data/entities/animals/boss_centipede/melee.xml", pos_x, pos_y, 0, 0 )
|
|
end
|
|
|
|
function clear_materials()
|
|
local this = GetUpdatedEntityID()
|
|
local pos_x, pos_y = EntityGetTransform( this )
|
|
|
|
shoot_projectile( this, "data/entities/animals/boss_centipede/clear_materials.xml", pos_x, pos_y, 0, 0 )
|
|
end
|
|
|
|
function minion_check_maxcount()
|
|
return #EntityGetWithTag("boss_centipede_minion") >= 3 + orbcount
|
|
end
|
|
|
|
function spawn_minion()
|
|
-- check that we only have less than N minions
|
|
--if minion_check_maxcount() then return end
|
|
|
|
-- spawn
|
|
local x, y = EntityGetTransform( GetUpdatedEntityID() )
|
|
EntityLoad( "data/entities/animals/boss_centipede/boss_centipede_minion.xml", x, y )
|
|
EntityLoad( "data/entities/particles/muzzle_flashes/muzzle_flash_circular_blue.xml", x, y)
|
|
end
|
|
|
|
function shield_on()
|
|
if shield_enabled then return end -- avoid playing unnecessary transitions
|
|
|
|
local entity_id = GetUpdatedEntityID()
|
|
local children = EntityGetAllChildren(entity_id)
|
|
if children == nil then return end
|
|
for _,v in ipairs(children) do
|
|
if EntityGetName(v) == "shield_entity" then
|
|
EntitySetComponentsWithTagEnabled( v, "shield", true )
|
|
-- muzzle flash
|
|
local x, y = EntityGetTransform(entity_id)
|
|
EntityLoad( "data/entities/particles/muzzle_flashes/muzzle_flash_circular_large_pink_reverse.xml", x, y)
|
|
GameEntityPlaySound( v, "activate" )
|
|
shield_enabled = true
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
function shield_off()
|
|
if not shield_enabled then return end -- avoid playing unnecessary transitions
|
|
|
|
local entity_id = GetUpdatedEntityID()
|
|
local children = EntityGetAllChildren(entity_id)
|
|
if children == nil then return end
|
|
for _,v in ipairs(children) do
|
|
if EntityGetName(v) == "shield_entity" then
|
|
EntitySetComponentsWithTagEnabled( v, "shield", false )
|
|
-- muzzle flash
|
|
local x, y = EntityGetTransform(entity_id)
|
|
EntityLoad( "data/entities/particles/muzzle_flashes/muzzle_flash_circular_large_pink.xml", x, y)
|
|
GameEntityPlaySound( v, "deactivate" )
|
|
shield_enabled = false
|
|
return
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
function chase_start()
|
|
set_force_coeff_mult(5)
|
|
|
|
local celleater = EntityGetFirstComponent( GetUpdatedEntityID(), "CellEaterComponent" )
|
|
|
|
if (celleater ~= nil) then
|
|
--ComponentSetValue( celleater, "eat_probability", tostring(100.0) )
|
|
end
|
|
end
|
|
|
|
function chase_stop()
|
|
set_force_coeff_mult(1)
|
|
|
|
local celleater = EntityGetFirstComponent( GetUpdatedEntityID(), "CellEaterComponent" )
|
|
|
|
if (celleater ~= nil) then
|
|
--ComponentSetValue( celleater, "eat_probability", tostring(0.0) )
|
|
end
|
|
end
|
|
|
|
-- Setting the boss movement logic state
|
|
function set_logic_state( state )
|
|
edit_component( GetUpdatedEntityID(), "LimbBossComponent", function(comp,vars)
|
|
vars.state = state
|
|
end)
|
|
|
|
-- DEBUG:
|
|
local ai = EntityGetFirstComponent( GetUpdatedEntityID(), "LimbBossComponent" )
|
|
local old = 0
|
|
|
|
if (ai ~= nil) then
|
|
old = tonumber(ComponentGetValue( ai, "state" ))
|
|
end
|
|
|
|
local on,nn = "",""
|
|
|
|
for i,v in pairs(states) do
|
|
if (v == old) then
|
|
on = i
|
|
end
|
|
|
|
if (v == state) then
|
|
nn = i
|
|
end
|
|
end
|
|
|
|
--print("Changing state from " .. tostring(on) .. " to " .. tostring(nn))
|
|
end
|
|
|
|
-- Special code for moving to a set position
|
|
function move_to_reference()
|
|
if ( boss_chase == 0 ) then
|
|
local reference = EntityGetWithTag( "reference" )
|
|
if ( #reference > 0 ) then
|
|
local reference_id = reference[1]
|
|
local x,y = EntityGetTransform( reference_id )
|
|
move_to( x, y )
|
|
else
|
|
print("Boss - NO REFERENCE FOUND!!!")
|
|
end
|
|
else
|
|
set_logic_state( states.FollowPlayer )
|
|
end
|
|
end
|
|
|
|
function move_to( x, y )
|
|
set_logic_state( states.MoveTo )
|
|
|
|
edit_component( GetUpdatedEntityID(), "LimbBossComponent", function(comp,vars)
|
|
vars.mMoveToPositionX = x
|
|
vars.mMoveToPositionY = y
|
|
end)
|
|
end
|
|
|
|
function open_eye()
|
|
if is_eye_open then return end
|
|
set_main_animation( "open", "opened" )
|
|
GameEntityPlaySound( GetUpdatedEntityID(), "open_mouth" )
|
|
is_eye_open = true
|
|
boss_wait(55)
|
|
end
|
|
|
|
function close_eye()
|
|
-- don't close eye if there's still attacks lined up
|
|
if not is_eye_open or phase_repeats > 0 then return end
|
|
set_main_animation("close", get_idle_animation_name())
|
|
GameEntityPlaySound( GetUpdatedEntityID(), "close_mouth" )
|
|
is_eye_open = false
|
|
boss_wait(50)
|
|
end
|
|
|
|
-- The boss can't die normally; if their HP is zero, this does stuff instead
|
|
function check_death()
|
|
if is_dead then return end
|
|
|
|
local entity_id = GetUpdatedEntityID()
|
|
SetRandomSeed( GameGetFrameNum(), GameGetFrameNum() )
|
|
|
|
local comp = EntityGetFirstComponent( entity_id, "DamageModelComponent" )
|
|
if( comp ~= nil ) then
|
|
local hp = ComponentGetValueFloat( comp, "hp" )
|
|
|
|
-- check aggro
|
|
if not is_aggro then
|
|
local max_hp = ComponentGetValueInt( comp, "max_hp")
|
|
is_aggro = hp <= clamp(max_hp * 0.1, 9, 40)
|
|
--print("hp/max: " .. hp .. " / " .. max_hp)
|
|
|
|
-- enter aggro mode
|
|
if is_aggro then
|
|
-- store aggro to entity
|
|
EntityAddComponent( entity_id, "VariableStorageComponent",
|
|
{
|
|
name = "aggro",
|
|
})
|
|
|
|
set_main_animation("aggro", "aggro")
|
|
|
|
-- spawn body chunks
|
|
GameEntityPlaySound( entity_id, "destroy_face" )
|
|
local x,y = EntityGetTransform( entity_id )
|
|
local o = EntityLoad( "data/entities/animals/boss_centipede/body_chunks.xml", x, y)
|
|
PhysicsApplyForce( o, 0, -600)
|
|
PhysicsApplyTorque( o, 200)
|
|
GameCreateParticle( "slime_green", x+20, y, 250, 0, -20, true, false )
|
|
|
|
phase_repeats = 0
|
|
next_phase()
|
|
end
|
|
end
|
|
|
|
-- check death
|
|
if ( hp <= 0.0 ) then
|
|
move_to_reference()
|
|
GameTriggerMusicFadeOutAndDequeueAll()
|
|
if death_sound_started == false then
|
|
GameEntityPlaySound( GetUpdatedEntityID(), "dying" )
|
|
death_sound_started = true
|
|
end
|
|
|
|
local o = EntityLoad( "data/entities/animals/boss_centipede/body_chunks.xml", x, y)
|
|
PhysicsApplyForce( o, 0, -600)
|
|
PhysicsApplyTorque( o, 200)
|
|
|
|
for i = 1,40 do
|
|
local rand = function() return Random( -10, 10 ) end
|
|
local x,y = EntityGetTransform( entity_id )
|
|
GameScreenshake( i * 1, x, y )
|
|
GameCreateParticle( "slime_green", x + rand(), y + rand(), 10, i * 5.5, i * 5.5, true, false )
|
|
if i > 20 then
|
|
GameCreateParticle( "gunpowder_unstable", x + rand(), y + rand(), 3, 40.0, 40.0, true, false )
|
|
end
|
|
|
|
wait( 3 )
|
|
end
|
|
|
|
local reference = EntityGetWithTag( "reference" )
|
|
local x_portal,y_portal = EntityGetTransform( GetUpdatedEntityID() )
|
|
|
|
if ( #reference > 0 ) then
|
|
local ref_id = reference[1]
|
|
x_portal,y_portal = EntityGetTransform( ref_id )
|
|
EntityKill( ref_id )
|
|
end
|
|
|
|
-- fix for spider legs, looks visually quite bad (what? how's this related to spider legs)
|
|
-- 150 puts it right under the bridge, which kinda cool
|
|
y_portal = y_portal + 50
|
|
|
|
EntityLoad( "data/entities/buildings/teleport_ending_victory_delay.xml", x_portal, y_portal )
|
|
|
|
-- kill
|
|
comp = EntityGetFirstComponent( GetUpdatedEntityID(), "DamageModelComponent" )
|
|
if( comp ~= nil ) then
|
|
ComponentSetValue( comp, "kill_now", "1" )
|
|
end
|
|
|
|
GlobalsSetValue( "FINAL_BOSS_ACTIVE", "0" )
|
|
|
|
local boss_kill_count = tonumber( GlobalsGetValue( "GLOBAL_BOSS_KILL_COUNT", "0" ) )
|
|
boss_kill_count = boss_kill_count + 1
|
|
print( "boss_kill_count: " .. boss_kill_count )
|
|
GlobalsSetValue( "GLOBAL_BOSS_KILL_COUNT", tostring( boss_kill_count ) )
|
|
|
|
AddFlagPersistent( "boss_centipede" )
|
|
StatsLogPlayerKill( GetUpdatedEntityID() ) -- this is needed because otherwise the boss kill doesn't get registered as a kill for the player
|
|
is_dead = true
|
|
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Basic idle function
|
|
function boss_wait( frames )
|
|
check_death()
|
|
wait( frames )
|
|
check_death()
|
|
did_wait = true
|
|
end
|
|
|
|
function animate_sprite( sprite, current_name, next_name )
|
|
GamePlayAnimation( GetUpdatedEntityID(), current_name, 0, next_name, 0 )
|
|
end
|
|
|
|
function get_idle_animation_name()
|
|
return "stand"
|
|
end
|
|
|
|
function set_main_animation( current_name, next_name )
|
|
local sprite = EntityGetFirstComponent( GetUpdatedEntityID(), "SpriteComponent" )
|
|
if ( sprite ~= nil ) then
|
|
if not is_aggro then
|
|
animate_sprite( sprite, current_name, next_name )
|
|
else
|
|
-- aggro overrides animations
|
|
animate_sprite( sprite, "aggro", "aggro" )
|
|
end
|
|
end
|
|
end
|
|
|
|
function set_force_coeff_mult( mult )
|
|
local entity_id = GetUpdatedEntityID()
|
|
local physics_ai = EntityGetFirstComponent( entity_id, "PhysicsAIComponent" )
|
|
|
|
if (physics_ai ~= nil) then
|
|
-- use a hardcoded value instead of getting from comp to avoid varying the value across save/load serialization
|
|
-- remember to update value here if changed in the component
|
|
local force_coeff_orig = 14.0
|
|
ComponentSetValue( physics_ai, "force_coeff", tostring( force_coeff_orig * mult ) )
|
|
end
|
|
end
|
|
|
|
-- run phase state machine -----------------
|
|
|
|
next_phase()
|
|
-- always start combat with circle shots
|
|
--if get_initialized() then
|
|
-- phase = phase_circleshot
|
|
-- phase_repeats = 2
|
|
--end
|
|
|
|
async_loop(function()
|
|
|
|
if is_dead then
|
|
wait(60 * 10)
|
|
else
|
|
did_wait = false
|
|
phase()
|
|
if did_wait == false then -- ensure the coroutine doesn't get stuck in an infinite loop if the states never wait
|
|
boss_wait(1)
|
|
end
|
|
end
|
|
|
|
end) |