351 lines
8.4 KiB
Lua
351 lines
8.4 KiB
Lua
|
dofile( "data/scripts/lib/coroutines.lua" )
|
||
|
dofile( "data/scripts/lib/utilities.lua" )
|
||
|
|
||
|
-- enum for changing C++ logic state. keep this in sync with the values in limbboss_system.cpp
|
||
|
MoveAroundNest = 0
|
||
|
FollowPlayer = 1
|
||
|
Escape = 2
|
||
|
DontMove = 3
|
||
|
|
||
|
local details_hidden = false
|
||
|
local is_dead = false
|
||
|
local did_wait = false
|
||
|
|
||
|
|
||
|
-- gather some data we're gonna reuse --------------
|
||
|
|
||
|
local herd_id = get_herd_id( GetUpdatedEntityID() )
|
||
|
local force_coeff_orig = component_get_value_float( GetUpdatedEntityID(), "PhysicsAIComponent", "force_coeff", 10.0 )
|
||
|
|
||
|
-- animate eyes and skull randomly -----------------
|
||
|
|
||
|
async_loop(function()
|
||
|
wait(120)
|
||
|
if details_hidden == false then
|
||
|
animate_random_detail()
|
||
|
end
|
||
|
end)
|
||
|
|
||
|
|
||
|
-- do various attack patterns -----------------
|
||
|
|
||
|
function phase0()
|
||
|
-- init
|
||
|
set_logic_state( FollowPlayer )
|
||
|
-- timeline
|
||
|
boss_wait(5 * 60)
|
||
|
choose_random_phase()
|
||
|
end
|
||
|
|
||
|
function phase1()
|
||
|
-- init
|
||
|
set_logic_state( DontMove )
|
||
|
-- open and shoot
|
||
|
expose_weak_spot()
|
||
|
circleshot()
|
||
|
boss_wait(50)
|
||
|
circleshot()
|
||
|
boss_wait(50)
|
||
|
circleshot()
|
||
|
-- keep the weak spot temporarily exposed
|
||
|
boss_wait(3 * 80)
|
||
|
-- close
|
||
|
hide_weak_spot()
|
||
|
boss_wait(10)
|
||
|
choose_random_phase()
|
||
|
end
|
||
|
|
||
|
function phase2()
|
||
|
-- init
|
||
|
set_logic_state( FollowPlayer )
|
||
|
-- timeline
|
||
|
spawn_minion()
|
||
|
boss_wait(30)
|
||
|
spawn_minion()
|
||
|
boss_wait(100)
|
||
|
choose_random_phase()
|
||
|
end
|
||
|
|
||
|
function phase3()
|
||
|
-- init
|
||
|
set_logic_state( FollowPlayer )
|
||
|
-- timeline
|
||
|
prepare_chase()
|
||
|
chase_start()
|
||
|
boss_wait(1 * 80)
|
||
|
chase_stop()
|
||
|
state = phase1
|
||
|
end
|
||
|
|
||
|
function phase4()
|
||
|
-- init
|
||
|
set_logic_state( DontMove )
|
||
|
-- open and shoot
|
||
|
expose_weak_spot()
|
||
|
homingshot()
|
||
|
boss_wait(40)
|
||
|
homingshot()
|
||
|
boss_wait(2 * 60)
|
||
|
-- close
|
||
|
hide_weak_spot()
|
||
|
boss_wait(10)
|
||
|
state = phase0
|
||
|
end
|
||
|
|
||
|
|
||
|
function choose_random_phase()
|
||
|
local r = math.random(0,4)
|
||
|
if r == 0 then state = phase0
|
||
|
elseif r == 1 then state = phase1
|
||
|
elseif r == 2 then state = phase2
|
||
|
elseif r == 3 then state = phase3
|
||
|
else state = phase4
|
||
|
end
|
||
|
end
|
||
|
|
||
|
|
||
|
-- helpers -----------------
|
||
|
|
||
|
function circleshot()
|
||
|
set_main_animation( "attack_ranged", "opened" )
|
||
|
boss_wait(15)
|
||
|
|
||
|
local this = GetUpdatedEntityID()
|
||
|
local pos_x, pos_y = EntityGetTransform( this )
|
||
|
|
||
|
local angle = 0
|
||
|
local amount = 8
|
||
|
local space = math.floor(360 / amount)
|
||
|
local speed = 230
|
||
|
|
||
|
for i=1,amount do
|
||
|
local vel_x = math.cos( math.rad(angle) ) * speed
|
||
|
local vel_y = math.sin( math.rad(angle) ) * speed
|
||
|
angle = angle + space
|
||
|
|
||
|
local orb = shoot_projectile( this, "data/entities/animals/boss_limbs/orb_boss_limbs.xml", pos_x, pos_y, vel_x, vel_y )
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function homingshot()
|
||
|
set_main_animation( "attack_ranged", "opened" )
|
||
|
boss_wait(15)
|
||
|
|
||
|
local this = GetUpdatedEntityID()
|
||
|
local pos_x, pos_y = EntityGetTransform( this )
|
||
|
|
||
|
local vel_x = 0
|
||
|
local vel_y = -30
|
||
|
|
||
|
shoot_projectile( this, "data/entities/animals/boss_limbs/orb_pink_big.xml", pos_x, pos_y, vel_x, vel_y )
|
||
|
end
|
||
|
|
||
|
function spawn_minion()
|
||
|
-- check that we only have less than N minions
|
||
|
local existing_minion_count = 0
|
||
|
local existing_minions = EntityGetWithTag( "slimeshooter_boss_limbs" )
|
||
|
if ( #existing_minions > 0 ) then
|
||
|
existing_minion_count = #existing_minions
|
||
|
end
|
||
|
|
||
|
if existing_minion_count >= 2 then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
-- spawn
|
||
|
local x, y = EntityGetTransform( GetUpdatedEntityID() )
|
||
|
SetRandomSeed( GameGetFrameNum(), x + y )
|
||
|
|
||
|
local slime = EntityLoad( "data/entities/animals/boss_limbs/slimeshooter_boss_limbs.xml", x, y )
|
||
|
edit_component( slime, "VelocityComponent", function(comp,vars)
|
||
|
local vel_x = Random(-90,90)
|
||
|
local vel_y = Random(-150,25)
|
||
|
ComponentSetValueVector2( comp, "mVelocity", vel_x, vel_y )
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
function get_idle_animation_name()
|
||
|
return "stand"
|
||
|
end
|
||
|
|
||
|
function prepare_chase()
|
||
|
set_main_animation( "charge", get_idle_animation_name() )
|
||
|
boss_wait(40)
|
||
|
end
|
||
|
|
||
|
function chase_start()
|
||
|
local physics_ai = EntityGetFirstComponent( GetUpdatedEntityID(), "PhysicsAIComponent" )
|
||
|
ComponentSetValue( physics_ai, "force_coeff", tostring( force_coeff_orig * 5.0 ) )
|
||
|
|
||
|
local celleater = EntityGetFirstComponent( GetUpdatedEntityID(), "CellEaterComponent" )
|
||
|
ComponentSetValue( celleater, "eat_probability", tostring(100.0) )
|
||
|
end
|
||
|
|
||
|
function chase_stop()
|
||
|
local physics_ai = EntityGetFirstComponent( GetUpdatedEntityID(), "PhysicsAIComponent" )
|
||
|
ComponentSetValue( physics_ai, "force_coeff", tostring( force_coeff_orig ) )
|
||
|
|
||
|
local celleater = EntityGetFirstComponent( GetUpdatedEntityID(), "CellEaterComponent" )
|
||
|
ComponentSetValue( celleater, "eat_probability", tostring(0.0) )
|
||
|
end
|
||
|
|
||
|
function expose_weak_spot()
|
||
|
set_main_animation( "open", "opened" )
|
||
|
set_details_hidden( true )
|
||
|
boss_wait(10)
|
||
|
set_hitboxes_weak( true )
|
||
|
boss_wait(20)
|
||
|
end
|
||
|
|
||
|
function hide_weak_spot()
|
||
|
set_main_animation( "close", get_idle_animation_name() )
|
||
|
boss_wait(10)
|
||
|
set_hitboxes_weak( false )
|
||
|
boss_wait(30)
|
||
|
set_details_hidden( false )
|
||
|
end
|
||
|
|
||
|
|
||
|
function set_hitboxes_weak( weak_spot_exposed )
|
||
|
EntitySetComponentsWithTagEnabled( GetUpdatedEntityID(), "hitbox_weak_spot", weak_spot_exposed )
|
||
|
EntitySetComponentsWithTagEnabled( GetUpdatedEntityID(), "hitbox_default", weak_spot_exposed == false )
|
||
|
end
|
||
|
|
||
|
function set_main_animation( current_name, next_name )
|
||
|
local sprite = EntityGetFirstComponent( GetUpdatedEntityID(), "SpriteComponent" )
|
||
|
if ( sprite ~= nil ) then
|
||
|
animate_sprite( sprite, current_name, next_name )
|
||
|
end
|
||
|
end
|
||
|
|
||
|
|
||
|
function animate_random_detail()
|
||
|
local which = math.random(1, 3)
|
||
|
for_comps( "SpriteComponent", function(i,sprite)
|
||
|
if i == which + 1 then
|
||
|
animate_sprite_simple( sprite, "animate", "stand" )
|
||
|
end
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
function set_details_hidden( is_hidden )
|
||
|
if is_hidden then
|
||
|
set_detail_animation( "invisible" )
|
||
|
else
|
||
|
set_detail_animation( "stand" )
|
||
|
end
|
||
|
details_hidden = is_hidden
|
||
|
end
|
||
|
|
||
|
function set_detail_animation( current_name )
|
||
|
for_comps( "SpriteComponent", function(i,sprite)
|
||
|
if i > 1 then
|
||
|
ComponentSetValue( sprite, "rect_animation", current_name )
|
||
|
end
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
|
||
|
function animate_sprite( sprite, current_name, next_name )
|
||
|
GamePlayAnimation( GetUpdatedEntityID(), current_name, 0, next_name, 0 )
|
||
|
--ComponentSetValue( sprite, "rect_animation", current_name )
|
||
|
--ComponentSetValue( sprite, "next_rect_animation", next_name )
|
||
|
end
|
||
|
|
||
|
function animate_sprite_simple( sprite, current_name, next_name )
|
||
|
ComponentSetValue( sprite, "rect_animation", current_name )
|
||
|
ComponentSetValue( sprite, "next_rect_animation", next_name )
|
||
|
end
|
||
|
|
||
|
function set_logic_state( state )
|
||
|
local comp = EntityGetFirstComponent( GetUpdatedEntityID(), "LimbBossComponent" )
|
||
|
if( comp ~= nil ) then
|
||
|
ComponentSetValue( comp, "state", state )
|
||
|
end
|
||
|
end
|
||
|
|
||
|
|
||
|
function check_death()
|
||
|
local comp = EntityGetFirstComponent( GetUpdatedEntityID(), "DamageModelComponent" )
|
||
|
if( comp ~= nil ) then
|
||
|
--ComponentSetValue( comp, "hp", "-0.1" ) -- DEBUG: kill immediately
|
||
|
local hp = ComponentGetValueFloat( comp, "hp" )
|
||
|
if ( hp <= 0.0 ) then
|
||
|
|
||
|
-- disable the attack limb
|
||
|
local children = EntityGetAllChildren( GetUpdatedEntityID() )
|
||
|
for i,child in ipairs( children ) do
|
||
|
if EntityGetName( child ) == "limb_attacker" then
|
||
|
EntityKill( child )
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- run death sequence
|
||
|
set_hitboxes_weak( false )
|
||
|
set_details_hidden( true )
|
||
|
set_logic_state( DontMove )
|
||
|
set_main_animation( "death1", "death2" )
|
||
|
|
||
|
SetRandomSeed( GameGetFrameNum(), GameGetFrameNum() )
|
||
|
|
||
|
local rand = function() return Random( -10, 10 ) end
|
||
|
|
||
|
for i = 1,40 do
|
||
|
local x,y = EntityGetTransform( GetUpdatedEntityID() )
|
||
|
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
|
||
|
|
||
|
-- kill
|
||
|
comp = EntityGetFirstComponent( GetUpdatedEntityID(), "DamageModelComponent" )
|
||
|
if( comp ~= nil ) then
|
||
|
ComponentSetValue( comp, "kill_now", "1" )
|
||
|
end
|
||
|
|
||
|
StatsLogPlayerKill( GetUpdatedEntityID() )
|
||
|
is_dead = true
|
||
|
AddFlagPersistent( "miniboss_limbs" )
|
||
|
|
||
|
return
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function boss_wait( frames )
|
||
|
check_death()
|
||
|
wait( frames )
|
||
|
check_death()
|
||
|
did_wait = true
|
||
|
end
|
||
|
|
||
|
|
||
|
-- init --------------------------------------------
|
||
|
|
||
|
set_hitboxes_weak( false )
|
||
|
set_main_animation( get_idle_animation_name(), get_idle_animation_name() )
|
||
|
set_details_hidden( false )
|
||
|
|
||
|
|
||
|
-- run phase state machine -----------------
|
||
|
|
||
|
state = phase0
|
||
|
|
||
|
async_loop(function()
|
||
|
|
||
|
-- alive
|
||
|
if is_dead then
|
||
|
wait(60 * 10)
|
||
|
else
|
||
|
did_wait = false
|
||
|
state()
|
||
|
if did_wait == false then -- ensure the coroutine doesn't get stuck in an infinite loop if the states never wait
|
||
|
wait(1)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
end)
|