1643 lines
44 KiB
C
1643 lines
44 KiB
C
/*
|
||
* This software is licensed under the terms of the MIT License.
|
||
* See COPYING for further information.
|
||
* ---
|
||
* Copyright (c) 2011-2024, Lukas Weber <laochailan@web.de>.
|
||
* Copyright (c) 2012-2024, Andrei Alexeyev <akari@taisei-project.org>.
|
||
*/
|
||
|
||
#include "boss.h"
|
||
|
||
#include "audio/audio.h"
|
||
#include "dynstage.h"
|
||
#include "entity.h"
|
||
#include "global.h"
|
||
#include "portrait.h"
|
||
#include "stage.h"
|
||
#include "stagedraw.h"
|
||
#include "stageobjects.h"
|
||
#include "stages/stage5/stage5.h" // for unlockable bonus BGM
|
||
#include "stagetext.h"
|
||
#include "util/env.h"
|
||
#include "util/glm.h"
|
||
#include "util/graphics.h"
|
||
|
||
#define DAMAGE_PER_POWER_POINT 500.0f
|
||
#define DAMAGE_PER_POWER_ITEM (DAMAGE_PER_POWER_POINT * POWER_VALUE)
|
||
|
||
static void ent_draw_boss(EntityInterface *ent);
|
||
static DamageResult ent_damage_boss(EntityInterface *ent, const DamageInfo *dmg);
|
||
|
||
typedef struct SpellBonus {
|
||
int clear;
|
||
int time;
|
||
int survival;
|
||
int endurance;
|
||
float diff_multiplier;
|
||
int total;
|
||
bool failed;
|
||
} SpellBonus;
|
||
|
||
static void calc_spell_bonus(Attack *a, SpellBonus *bonus);
|
||
|
||
DECLARE_TASK(boss_particles, { BoxedBoss boss; });
|
||
|
||
TASK(boss_damage_to_power, { BoxedBoss boss; }) {
|
||
auto boss = TASK_BIND(ARGS.boss);
|
||
|
||
for(;;WAIT(2)) {
|
||
if(boss->damage_to_power_accum >= DAMAGE_PER_POWER_ITEM) {
|
||
spawn_item(boss->pos, ITEM_POWER);
|
||
boss->damage_to_power_accum -= DAMAGE_PER_POWER_ITEM;
|
||
}
|
||
}
|
||
}
|
||
|
||
Boss *create_boss(const char *name, char *ani, cmplx pos) {
|
||
auto boss = STAGE_ACQUIRE_OBJ(Boss);
|
||
|
||
boss->name = name;
|
||
boss->pos = pos;
|
||
|
||
char strbuf[strlen(ani) + sizeof("boss/")];
|
||
snprintf(strbuf, sizeof(strbuf), "boss/%s", ani);
|
||
aniplayer_create(&boss->ani, res_anim(strbuf), "main");
|
||
|
||
boss->birthtime = global.frames;
|
||
boss->zoomcolor = *RGBA(0.1, 0.2, 0.3, 1.0);
|
||
|
||
boss->ent.draw_layer = LAYER_BOSS;
|
||
boss->ent.draw_func = ent_draw_boss;
|
||
boss->ent.damage_func = ent_damage_boss;
|
||
ent_register(&boss->ent, ENT_TYPE_ID(Boss));
|
||
|
||
// This is not necessary because the default will be set at the start of every attack.
|
||
// But who knows. Maybe this will be triggered somewhen. If bosses without attacks start
|
||
// taking over the world, I will be the one who put in this weak point to make them vulnerable.
|
||
boss->bomb_damage_multiplier = 1.0;
|
||
boss->shot_damage_multiplier = 1.0;
|
||
|
||
COEVENT_INIT_ARRAY(boss->events);
|
||
INVOKE_TASK(boss_particles, ENT_BOX(boss));
|
||
INVOKE_TASK(boss_damage_to_power, ENT_BOX(boss));
|
||
|
||
return boss;
|
||
}
|
||
|
||
void boss_set_portrait(Boss *boss, const char *name, const char *variant, const char *face) {
|
||
if(boss->portrait.tex != NULL) {
|
||
r_texture_destroy(boss->portrait.tex);
|
||
boss->portrait.tex = NULL;
|
||
}
|
||
|
||
if(name != NULL) {
|
||
assume(face != NULL);
|
||
portrait_render_byname(name, variant, face, &boss->portrait);
|
||
} else {
|
||
assume(face == NULL);
|
||
assume(variant == NULL);
|
||
}
|
||
}
|
||
|
||
static double draw_boss_text(Alignment align, float x, float y, const char *text, Font *fnt, const Color *clr) {
|
||
return text_draw(text, &(TextParams) {
|
||
.shader = "text_hud",
|
||
.pos = { x, y },
|
||
.color = clr,
|
||
.font_ptr = fnt,
|
||
.align = align,
|
||
});
|
||
}
|
||
|
||
void draw_extraspell_bg(Boss *boss, int time) {
|
||
// overlay for all extra spells
|
||
// FIXME: Please replace this with something that doesn't look like shit.
|
||
|
||
r_state_push();
|
||
|
||
float opacity = 0.7;
|
||
r_color4(0.2 * opacity, 0.1 * opacity, 0, 0);
|
||
fill_viewport(sin(time) * 0.015, time / 50.0, 1, "stage3/wspellclouds");
|
||
r_color4(2000, 2000, 2000, 0);
|
||
r_blend(r_blend_compose(
|
||
BLENDFACTOR_SRC_COLOR, BLENDFACTOR_ONE, BLENDOP_MIN,
|
||
BLENDFACTOR_ZERO, BLENDFACTOR_ONE, BLENDOP_ADD
|
||
));
|
||
fill_viewport(cos(time) * 0.015, time / 70.0, 1, "stage4/kurumibg2");
|
||
fill_viewport(sin(time*1.1+2.1) * 0.015, time / 30.0, 1, "stage4/kurumibg2");
|
||
|
||
r_state_pop();
|
||
}
|
||
|
||
static inline bool healthbar_style_is_radial(void) {
|
||
return config_get_int(CONFIG_HEALTHBAR_STYLE) > 0;
|
||
}
|
||
|
||
static const Color *boss_healthbar_color(AttackType atype) {
|
||
static const Color colors[] = {
|
||
[AT_Normal] = { 0.50, 0.50, 0.60, 1.00 },
|
||
[AT_Move] = { 1.00, 1.00, 1.00, 1.00 },
|
||
[AT_Spellcard] = { 1.00, 0.00, 0.00, 1.00 },
|
||
[AT_SurvivalSpell] = { 0.00, 1.00, 0.00, 1.00 },
|
||
[AT_ExtraSpell] = { 1.00, 0.40, 0.00, 1.00 },
|
||
};
|
||
|
||
assert(atype >= 0 && atype < sizeof(colors)/sizeof(*colors));
|
||
return colors + atype;
|
||
}
|
||
|
||
static StageProgress *get_spellstage_progress(Attack *a, StageInfo **out_stginfo, bool write) {
|
||
if(!write || (global.replay.input.replay == NULL && global.stage->type == STAGE_STORY)) {
|
||
StageInfo *i = stageinfo_get_by_spellcard(a->info, global.diff);
|
||
if(i) {
|
||
StageProgress *p = stageinfo_get_progress(i, global.diff, write);
|
||
|
||
if(out_stginfo) {
|
||
*out_stginfo = i;
|
||
}
|
||
|
||
if(p) {
|
||
return p;
|
||
}
|
||
}
|
||
#if DEBUG
|
||
else if((a->type == AT_Spellcard || a->type == AT_ExtraSpell) && global.stage->type != STAGE_SPECIAL) {
|
||
log_warn("FIXME: spellcard '%s' is not available in spell practice mode!", a->name);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
return NULL;
|
||
}
|
||
|
||
static bool boss_should_skip_attack(Boss *boss, Attack *a) {
|
||
// Skip zero-length spells. Zero-length AT_Move and AT_Normal attacks are ok.
|
||
// FIXME: I'm really not sure what was the purpose of this, but for now I'm abusing this to
|
||
// conditionally flag the extra spell as skipped. Investigate whether we can remove simplify
|
||
// things a bit and remove this function.
|
||
if(ATTACK_IS_SPELL(a->type) && a->timeout <= 0) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
static Attack* boss_get_final_attack(Boss *boss) {
|
||
Attack *final;
|
||
for(final = boss->attacks + boss->acount - 1; final >= boss->attacks && boss_should_skip_attack(boss, final); --final);
|
||
return final >= boss->attacks ? final : NULL;
|
||
}
|
||
|
||
static bool boss_attack_is_final(Boss *boss, Attack *a) {
|
||
return boss_get_final_attack(boss) == a;
|
||
}
|
||
|
||
static void update_hud_attack_ptrs(Boss *boss) {
|
||
boss->hud.a_cur = NULL;
|
||
boss->hud.a_next = NULL;
|
||
boss->hud.a_prev = NULL;
|
||
|
||
for(Attack *a = boss->attacks, *a_end = boss->attacks + boss->acount; a < a_end; ++a) {
|
||
if(boss_should_skip_attack(boss, a) || a->type == AT_Move) {
|
||
continue;
|
||
}
|
||
|
||
if(boss->hud.a_cur != NULL) {
|
||
boss->hud.a_next = a;
|
||
break;
|
||
}
|
||
|
||
if(a == boss->current) {
|
||
boss->hud.a_cur = boss->current;
|
||
continue;
|
||
}
|
||
|
||
if(a->hp <= 0 && attack_has_finished(a)) {
|
||
if(boss->current == NULL) {
|
||
boss->hud.a_prev = boss->hud.a_cur;
|
||
boss->hud.a_cur = a;
|
||
} else {
|
||
boss->hud.a_prev = a;
|
||
}
|
||
}
|
||
}
|
||
|
||
// log_debug("prev = %s", boss->hud.a_prev ? boss->hud.a_prev->name : NULL);
|
||
// log_debug("cur = %s", boss->hud.a_cur ? boss->hud.a_cur->name : NULL);
|
||
// log_debug("next = %s", boss->hud.a_next ? boss->hud.a_next->name : NULL);
|
||
}
|
||
|
||
static void update_healthbar(Boss *boss) {
|
||
bool radial = healthbar_style_is_radial();
|
||
bool player_nearby = radial && cabs(boss->pos - global.plr.pos) < 128;
|
||
float update_speed = 0.1;
|
||
float target_opacity = 1.0;
|
||
float target_fill = 0.0;
|
||
float target_altfill = 0.0;
|
||
|
||
Attack *a_prev = boss->hud.a_prev;
|
||
Attack *a_cur = boss->hud.a_cur;
|
||
Attack *a_next = boss->hud.a_next;
|
||
|
||
if(
|
||
boss_is_dying(boss) ||
|
||
boss_is_fleeing(boss) ||
|
||
!a_cur ||
|
||
a_cur->type == AT_Move ||
|
||
dialog_is_active(global.dialog)
|
||
) {
|
||
target_opacity = 0.0;
|
||
}
|
||
|
||
if(player_nearby || boss->in_background) {
|
||
target_opacity *= 0.25;
|
||
}
|
||
|
||
if(a_cur != NULL) {
|
||
float total_maxhp = 0, total_hp = 0;
|
||
|
||
Attack *spell, *non;
|
||
|
||
if(a_cur->type == AT_Normal) {
|
||
non = a_cur;
|
||
spell = a_next;
|
||
} else {
|
||
non = a_prev;
|
||
spell = a_cur;
|
||
}
|
||
|
||
if(non && non->type == AT_Normal && non->maxhp > 0) {
|
||
total_hp += 3 * non->hp;
|
||
total_maxhp += 3 * non->maxhp;
|
||
boss->healthbar.fill_color = *boss_healthbar_color(non->type);
|
||
}
|
||
|
||
if(spell && ATTACK_IS_SPELL(spell->type)) {
|
||
float spell_hp;
|
||
float spell_maxhp;
|
||
|
||
if(spell->type == AT_SurvivalSpell) {
|
||
spell_hp = spell_maxhp = max(1, total_maxhp * 0.1f);
|
||
} else {
|
||
spell_hp = spell->hp;
|
||
spell_maxhp = spell->maxhp;
|
||
}
|
||
|
||
total_hp += spell_hp;
|
||
total_maxhp += spell_maxhp;
|
||
target_altfill = spell_maxhp / total_maxhp;
|
||
boss->healthbar.fill_altcolor = *boss_healthbar_color(spell->type);
|
||
}
|
||
|
||
if(a_cur->type == AT_SurvivalSpell) {
|
||
target_altfill = 1;
|
||
target_fill = 0;
|
||
|
||
if(radial) {
|
||
target_opacity = 0.0;
|
||
}
|
||
} else if(total_maxhp > 0) {
|
||
total_maxhp = max(0.001f, total_maxhp);
|
||
|
||
if(total_hp > 0) {
|
||
total_hp = max(0.001f, total_hp);
|
||
}
|
||
|
||
target_fill = total_hp / total_maxhp;
|
||
}
|
||
}
|
||
|
||
float opacity_update_speed = update_speed;
|
||
|
||
if(boss_is_dying(boss)) {
|
||
opacity_update_speed *= 0.25;
|
||
}
|
||
|
||
fapproach_asymptotic_p(&boss->healthbar.opacity, target_opacity, opacity_update_speed, 1e-3);
|
||
|
||
if(
|
||
boss_is_vulnerable(boss) ||
|
||
(a_cur && a_cur->type == AT_SurvivalSpell && attack_has_started(a_cur))
|
||
) {
|
||
update_speed *= (1 + 4 * pow(1 - target_fill, 2));
|
||
}
|
||
|
||
fapproach_asymptotic_p(&boss->healthbar.fill_total, target_fill, update_speed, 1e-3);
|
||
fapproach_asymptotic_p(&boss->healthbar.fill_alt, target_altfill, update_speed, 1e-3);
|
||
}
|
||
|
||
static void update_hud(Boss *boss) {
|
||
update_hud_attack_ptrs(boss);
|
||
update_healthbar(boss);
|
||
|
||
float update_speed = 0.1;
|
||
float target_opacity = 1.0;
|
||
float target_spell_opacity = 1.0;
|
||
float target_plrproximity_opacity = 1.0;
|
||
|
||
if(boss_is_dying(boss) || boss_is_fleeing(boss)) {
|
||
update_speed *= 0.25;
|
||
target_opacity = 0.0;
|
||
}
|
||
|
||
if(!boss->current || boss->current->type == AT_Move || dialog_is_active(global.dialog)) {
|
||
target_opacity = 0.0;
|
||
}
|
||
|
||
if(!boss->current || !ATTACK_IS_SPELL(boss->current->type) || attack_has_finished(boss->current)) {
|
||
target_spell_opacity = 0.0;
|
||
}
|
||
|
||
if(im(global.plr.pos) < 128) {
|
||
target_plrproximity_opacity = 0.25;
|
||
}
|
||
|
||
if(boss->current && !attack_has_finished(boss->current) && boss->current->type != AT_Move) {
|
||
int frames = boss->current->timeout + min(0, boss->current->starttime - global.frames);
|
||
boss->hud.attack_timer = clamp((frames)/(double)FPS, 0, 99.999);
|
||
}
|
||
|
||
fapproach_asymptotic_p(&boss->hud.global_opacity, target_opacity, update_speed, 1e-2);
|
||
fapproach_asymptotic_p(&boss->hud.spell_opacity, target_spell_opacity, update_speed, 1e-2);
|
||
fapproach_asymptotic_p(&boss->hud.plrproximity_opacity, target_plrproximity_opacity, update_speed, 1e-2);
|
||
}
|
||
|
||
static void draw_radial_healthbar(Boss *boss) {
|
||
if(boss->healthbar.opacity == 0) {
|
||
return;
|
||
}
|
||
|
||
r_state_push();
|
||
r_mat_mv_push();
|
||
r_mat_mv_translate(re(boss->pos), im(boss->pos), 0);
|
||
r_mat_mv_scale(220, 220, 0);
|
||
r_shader("healthbar_radial");
|
||
r_uniform_vec4_rgba("borderColor", RGBA(0.75, 0.75, 0.75, 0.75));
|
||
r_uniform_vec4_rgba("glowColor", RGBA(0.5, 0.5, 1.0, 0.75));
|
||
r_uniform_vec4_rgba("fillColor", &boss->healthbar.fill_color);
|
||
r_uniform_vec4_rgba("altFillColor", &boss->healthbar.fill_altcolor);
|
||
r_uniform_vec4_rgba("coreFillColor", RGBA(0.8, 0.8, 0.8, 0.5));
|
||
r_uniform_vec2("fill", boss->healthbar.fill_total, boss->healthbar.fill_alt);
|
||
r_uniform_float("opacity", boss->healthbar.opacity);
|
||
r_draw_quad();
|
||
r_mat_mv_pop();
|
||
r_state_pop();
|
||
}
|
||
|
||
static void draw_linear_healthbar(Boss *boss) {
|
||
float opacity = boss->healthbar.opacity * boss->hud.global_opacity * boss->hud.plrproximity_opacity;
|
||
|
||
if(opacity == 0) {
|
||
return;
|
||
}
|
||
|
||
const float width = VIEWPORT_W * 0.9;
|
||
const float height = 24;
|
||
|
||
r_state_push();
|
||
r_mat_mv_push();
|
||
r_mat_mv_translate(1 + width/2, height/2 - 3, 0);
|
||
r_mat_mv_scale(width, height, 0);
|
||
r_shader("healthbar_linear");
|
||
r_uniform_vec4_rgba("borderColor", RGBA(0.75, 0.75, 0.75, 0.75));
|
||
r_uniform_vec4_rgba("glowColor", RGBA(0.5, 0.5, 1.0, 0.75));
|
||
r_uniform_vec4_rgba("fillColor", &boss->healthbar.fill_color);
|
||
r_uniform_vec4_rgba("altFillColor", &boss->healthbar.fill_altcolor);
|
||
r_uniform_vec4_rgba("coreFillColor", RGBA(0.8, 0.8, 0.8, 0.5));
|
||
r_uniform_vec2("fill", boss->healthbar.fill_total, boss->healthbar.fill_alt);
|
||
r_uniform_float("opacity", opacity);
|
||
r_draw_quad();
|
||
r_mat_mv_pop();
|
||
r_state_pop();
|
||
}
|
||
|
||
static void draw_spell_warning(Font *font, float y_pos, float f, float opacity) {
|
||
static const char *msg = "~ Spell Card Attack! ~";
|
||
|
||
float msg_width = text_width(font, msg, 0);
|
||
float flash = 0.2 + 0.8 * pow(psin(M_PI + 5 * M_PI * f), 0.5);
|
||
f = 0.15 * f + 0.85 * (0.5 * pow(2 * f - 1, 3) + 0.5);
|
||
opacity *= 1 - 2 * fabs(f - 0.5);
|
||
cmplx pos = (VIEWPORT_W + msg_width) * f - msg_width * 0.5 + I * y_pos;
|
||
|
||
draw_boss_text(ALIGN_CENTER, re(pos), im(pos), msg, font, color_mul_scalar(RGBA(1, flash, flash, 1), opacity));
|
||
}
|
||
|
||
static void draw_spell_name(Boss *b, int time, bool healthbar_radial) {
|
||
Font *font = res_font("standard");
|
||
|
||
cmplx x0 = VIEWPORT_W/2+I*VIEWPORT_H/3.5;
|
||
float f = clamp((time - 40.0) / 60.0, 0, 1);
|
||
float f2 = clamp(time / 80.0, 0, 1);
|
||
float y_offset = 26 + healthbar_radial * -15;
|
||
float y_text_offset = 5 - font_get_metrics(font)->descent;
|
||
cmplx x = x0 + ((VIEWPORT_W - 10) + I*(y_offset + y_text_offset) - x0) * f*(f+1)*0.5;
|
||
int strw = text_width(font, b->current->name, 0);
|
||
|
||
float opacity_noplr = b->hud.spell_opacity * b->hud.global_opacity;
|
||
float opacity = opacity_noplr * b->hud.plrproximity_opacity;
|
||
|
||
r_draw_sprite(&(SpriteParams) {
|
||
.sprite_ptr = res_sprite("spell"),
|
||
.shader_ptr = res_shader("sprite_default"),
|
||
.pos = { (VIEWPORT_W - 128), y_offset * (1 - pow(1 - f2, 5)) + VIEWPORT_H * pow(1 - f2, 2) },
|
||
.color = color_mul_scalar(RGBA(1, 1, 1, f2 * 0.5), opacity * f2) ,
|
||
.scale.both = 3 - 2 * (1 - pow(1 - f2, 3)),
|
||
});
|
||
|
||
int delay = attacktype_start_delay(b->current->type);
|
||
float warn_progress = clamp((time + delay) / 120.0, 0, 1);
|
||
|
||
r_mat_mv_push();
|
||
r_mat_mv_translate(re(x), im(x),0);
|
||
float scale = f+1.*(1-f)*(1-f)*(1-f);
|
||
r_mat_mv_scale(scale,scale,1);
|
||
r_mat_mv_rotate(glm_ease_quad_out(f) * 2 * M_PI, 0.8, -0.2, 0);
|
||
|
||
float spellname_opacity_noplr = opacity_noplr * min(1, warn_progress/0.6f);
|
||
float spellname_opacity = spellname_opacity_noplr * b->hud.plrproximity_opacity;
|
||
|
||
draw_boss_text(ALIGN_RIGHT, strw/2*(1-f), 0, b->current->name, font, color_mul_scalar(RGBA(1, 1, 1, 1), spellname_opacity));
|
||
|
||
if(spellname_opacity_noplr < 1) {
|
||
r_mat_mv_push();
|
||
r_mat_mv_scale(2 - spellname_opacity_noplr, 2 - spellname_opacity_noplr, 1);
|
||
draw_boss_text(ALIGN_RIGHT, strw/2*(1-f), 0, b->current->name, font, color_mul_scalar(RGBA(1, 1, 1, 1), spellname_opacity_noplr * 0.5));
|
||
r_mat_mv_pop();
|
||
}
|
||
|
||
r_mat_mv_pop();
|
||
|
||
if(warn_progress < 1) {
|
||
draw_spell_warning(font, im(x0) - font_get_lineskip(font), warn_progress, opacity);
|
||
}
|
||
|
||
StageProgress *p = get_spellstage_progress(b->current, NULL, false);
|
||
|
||
if(p) {
|
||
char buf[32];
|
||
float a = clamp((global.frames - b->current->starttime - 60) / 60.0, 0, 1);
|
||
font = res_font("small");
|
||
|
||
float bonus_ofs = 220;
|
||
|
||
r_mat_mv_push();
|
||
r_mat_mv_translate(
|
||
bonus_ofs * pow(1 - a, 2),
|
||
font_get_lineskip(font) + y_offset + y_text_offset + 0.5,
|
||
0);
|
||
|
||
bool kern = font_get_kerning_enabled(font);
|
||
font_set_kerning_enabled(font, false);
|
||
|
||
// TODO: display plrmode-specific data?
|
||
snprintf(buf, sizeof(buf), "%u / %u", p->global.num_cleared, p->global.num_played);
|
||
|
||
draw_boss_text(ALIGN_RIGHT,
|
||
VIEWPORT_W - 10 - text_width(font, buf, 0), 0,
|
||
"History: ", font, color_mul_scalar(RGB(0.75, 0.75, 0.75), a * opacity)
|
||
);
|
||
|
||
draw_boss_text(ALIGN_RIGHT,
|
||
VIEWPORT_W - 10, 0,
|
||
buf, font, color_mul_scalar(RGB(0.50, 1.00, 0.50), a * opacity)
|
||
);
|
||
|
||
// r_mat_translate(0, 6.5, 0);
|
||
|
||
bonus_ofs -= draw_boss_text(ALIGN_LEFT,
|
||
VIEWPORT_W - bonus_ofs, 0,
|
||
"Bonus: ", font, color_mul_scalar(RGB(0.75, 0.75, 0.75), a * opacity)
|
||
);
|
||
|
||
SpellBonus bonus;
|
||
calc_spell_bonus(b->current, &bonus);
|
||
format_huge_num(0, bonus.total, sizeof(buf), buf);
|
||
|
||
draw_boss_text(ALIGN_LEFT,
|
||
VIEWPORT_W - bonus_ofs, 0,
|
||
buf, font, color_mul_scalar(
|
||
bonus.failed ? RGB(1.00, 0.50, 0.50)
|
||
: RGB(0.30, 0.60, 1.00)
|
||
, a * opacity)
|
||
);
|
||
|
||
font_set_kerning_enabled(font, kern);
|
||
|
||
r_mat_mv_pop();
|
||
}
|
||
}
|
||
|
||
static void draw_spell_portrait(Boss *b, int time) {
|
||
const int anim_time = 200;
|
||
|
||
if(time <= 0 || time >= anim_time || !b->portrait.tex) {
|
||
return;
|
||
}
|
||
|
||
if(b->current->draw_rule == NULL) {
|
||
// No spell background? Assume not cut-in is intended, either.
|
||
return;
|
||
}
|
||
|
||
// NOTE: Mostly copypasted from player.c::player_draw_overlay()
|
||
// TODO: Maybe somehow generalize and make it more intelligible
|
||
|
||
float a = time / (float)anim_time;
|
||
|
||
r_state_push();
|
||
r_shader("sprite_default");
|
||
|
||
float char_in = clamp(a * 1.5f, 0, 1);
|
||
float char_out = min(1, 2 - (2 * a));
|
||
float char_opacity_in = 0.75f * min(1, a * 5);
|
||
float char_opacity = char_opacity_in * char_out * char_out;
|
||
float char_xofs = -20 * a;
|
||
|
||
Sprite *char_spr = &b->portrait;
|
||
|
||
r_mat_mv_push();
|
||
r_mat_mv_scale(-1, 1, 1);
|
||
r_mat_mv_translate(-VIEWPORT_W, 0, 0);
|
||
r_cull(CULL_FRONT);
|
||
|
||
for(int i = 1; i <= 3; ++i) {
|
||
float t = a * 200;
|
||
float dur = 20;
|
||
float start = 200 - dur * 5;
|
||
float end = start + dur;
|
||
float ofs = 0.2 * dur * (i - 1);
|
||
float o = 1 - smoothstep(start + ofs, end + ofs, t);
|
||
|
||
r_draw_sprite(&(SpriteParams) {
|
||
.sprite_ptr = char_spr,
|
||
.pos = { char_spr->w * 0.5 + VIEWPORT_W * powf(1 - char_in, 4 - i * 0.3f) - i + char_xofs, VIEWPORT_H - char_spr->h * 0.5 },
|
||
.color = color_mul_scalar(color_add(RGBA(0.2, 0.2, 0.2, 0), RGBA(i==1, i==2, i==3, 0)), char_opacity_in * (1 - char_in * o) * o),
|
||
.flip.x = true,
|
||
.scale.both = 1.0f + 0.02f * (min(1, a * 1.2f)) + i * 0.5 * powf(1 - o, 2),
|
||
});
|
||
}
|
||
|
||
r_draw_sprite(&(SpriteParams) {
|
||
.sprite_ptr = char_spr,
|
||
.pos = { char_spr->w * 0.5f + VIEWPORT_W * powf(1 - char_in, 4) + char_xofs, VIEWPORT_H - char_spr->h * 0.5f },
|
||
.color = RGBA_MUL_ALPHA(1, 1, 1, char_opacity * min(1, char_in * 2) * (1 - min(1, (1 - char_out) * 5))),
|
||
.flip.x = true,
|
||
.scale.both = 1.0f + 0.1f * (1 - char_out),
|
||
});
|
||
|
||
r_mat_mv_pop();
|
||
r_state_pop();
|
||
}
|
||
|
||
static void boss_glow_draw(Projectile *p, int t, ProjDrawRuleArgs args) {
|
||
float s = 1.0+t/(double)p->timeout*0.5;
|
||
float fade = 1 - (1.5 - s);
|
||
float deform = 5 - 10 * fade * fade;
|
||
Color c = p->color;
|
||
|
||
c.a = 0;
|
||
color_mul_scalar(&c, 1.5 - s);
|
||
|
||
r_draw_sprite(&(SpriteParams) {
|
||
.pos = { re(p->pos), im(p->pos) },
|
||
.sprite_ptr = p->sprite,
|
||
.scale.both = s,
|
||
.color = &c,
|
||
.shader_params = &(ShaderCustomParams){{ deform }},
|
||
.shader_ptr = p->shader,
|
||
});
|
||
}
|
||
|
||
static Projectile *spawn_boss_glow(Boss *boss, const Color *clr, int timeout) {
|
||
return PARTICLE(
|
||
.sprite_ptr = aniplayer_get_frame(&boss->ani),
|
||
.pos = boss->pos + boss_get_sprite_offset(boss),
|
||
.color = clr,
|
||
.draw_rule = boss_glow_draw,
|
||
.timeout = timeout,
|
||
.layer = LAYER_PARTICLE_LOW,
|
||
.shader = "sprite_silhouette",
|
||
.flags = PFLAG_REQUIREDPARTICLE | PFLAG_NOMOVE | PFLAG_MANUALANGLE,
|
||
);
|
||
}
|
||
|
||
DEFINE_TASK(boss_particles) {
|
||
Boss *boss = TASK_BIND(ARGS.boss);
|
||
DECLARE_ENT_ARRAY(Projectile, smoke_parts, 16);
|
||
|
||
cmplx prev_pos = boss->pos;
|
||
|
||
for(;;YIELD) {
|
||
ENT_ARRAY_FOREACH(&smoke_parts, Projectile *p, {
|
||
p->pos += boss->pos - prev_pos;
|
||
});
|
||
prev_pos = boss->pos;
|
||
|
||
Color *glowcolor = &boss->glowcolor;
|
||
Color *shadowcolor = &boss->shadowcolor;
|
||
|
||
Attack *cur = boss->current;
|
||
bool is_spell = cur && ATTACK_IS_SPELL(cur->type) && !attack_has_finished(cur);
|
||
bool is_extra = cur && cur->type == AT_ExtraSpell && attack_has_started(cur);
|
||
|
||
if(!(global.frames % 13) && !is_extra) {
|
||
ENT_ARRAY_COMPACT(&smoke_parts);
|
||
ENT_ARRAY_ADD(&smoke_parts, PARTICLE(
|
||
.sprite = "smoke",
|
||
.pos = cdir(global.frames) + boss->pos,
|
||
.color = RGBA(shadowcolor->r, shadowcolor->g, shadowcolor->b, 0.0),
|
||
.timeout = 180,
|
||
.draw_rule = pdraw_timeout_scale(2, 0.01),
|
||
.angle = rng_angle(),
|
||
.flags = PFLAG_MANUALANGLE,
|
||
));
|
||
}
|
||
|
||
if(
|
||
!(global.frames % (2 + 2 * is_extra)) &&
|
||
(is_spell || boss_is_dying(boss)) &&
|
||
!boss->in_background
|
||
) {
|
||
float glowstr = 0.5;
|
||
float a = (1.0 - glowstr) + glowstr * psin(global.frames/15.0);
|
||
spawn_boss_glow(boss, color_mul_scalar(COLOR_COPY(glowcolor), a), 24);
|
||
}
|
||
}
|
||
}
|
||
|
||
void draw_boss_background(Boss *boss) {
|
||
r_mat_mv_push();
|
||
r_mat_mv_translate(re(boss->pos), im(boss->pos), 0);
|
||
r_mat_mv_rotate(global.frames * 4.0 * DEG2RAD, 0, 0, -1);
|
||
|
||
float f = 0.8+0.1*sin(global.frames/8.0);
|
||
|
||
if(boss_is_dying(boss)) {
|
||
float t = (global.frames - boss->current->endtime)/(float)BOSS_DEATH_DELAY + 1;
|
||
f -= t * (t - 0.7f) / max(0.01f, 1-t);
|
||
}
|
||
|
||
r_mat_mv_scale(f, f, 1);
|
||
r_draw_sprite(&(SpriteParams) {
|
||
.sprite_ptr = res_sprite("boss_circle"),
|
||
.shader_ptr = res_shader("sprite_particle"),
|
||
.shader_params = &(ShaderCustomParams) { 1.0f },
|
||
.color = RGBA(1, 1, 1, 0),
|
||
});
|
||
r_mat_mv_pop();
|
||
}
|
||
|
||
cmplx boss_get_sprite_offset(Boss *boss) {
|
||
return 6 * sin(global.frames/25.0) * I;
|
||
}
|
||
|
||
static void ent_draw_boss(EntityInterface *ent) {
|
||
Boss *boss = ENT_CAST(ent, Boss);
|
||
|
||
float red = 0.5*exp(-0.5*(global.frames-boss->lastdamageframe));
|
||
if(red > 1)
|
||
red = 0;
|
||
|
||
float boss_alpha = 1;
|
||
|
||
if(boss_is_dying(boss)) {
|
||
float t = (global.frames - boss->current->endtime)/(float)BOSS_DEATH_DELAY + 1;
|
||
boss_alpha = (1 - t) + 0.3;
|
||
}
|
||
|
||
Color *c = RGB(1.0f, 1.0f - red, 1.0f - red * 0.5f);
|
||
color_lerp(c, RGB(0.2f, 0.2f, 0.2f), boss->background_transition);
|
||
color_mul_scalar(c, boss_alpha);
|
||
|
||
r_draw_sprite(&(SpriteParams) {
|
||
.sprite_ptr = aniplayer_get_frame(&boss->ani),
|
||
.shader_ptr = res_shader("sprite_particle"),
|
||
.shader_params = &(ShaderCustomParams) { 1.0f },
|
||
.pos.as_cmplx = boss->pos + boss_get_sprite_offset(boss),
|
||
.color = c,
|
||
});
|
||
}
|
||
|
||
void draw_boss_fake_overlay(Boss *boss) {
|
||
if(healthbar_style_is_radial()) {
|
||
draw_radial_healthbar(boss);
|
||
}
|
||
}
|
||
|
||
void draw_boss_overlay(Boss *boss) {
|
||
bool radial_style = healthbar_style_is_radial();
|
||
|
||
float o = boss->hud.global_opacity * boss->hud.plrproximity_opacity;
|
||
|
||
if(o > 0) {
|
||
if(boss->current && ATTACK_IS_SPELL(boss->current->type)) {
|
||
int t_portrait, t_spell;
|
||
t_portrait = t_spell = global.frames - boss->current->starttime;
|
||
t_portrait += attacktype_start_delay(boss->current->type);
|
||
|
||
draw_spell_portrait(boss, t_portrait);
|
||
draw_spell_name(boss, t_spell, radial_style);
|
||
}
|
||
|
||
draw_boss_text(ALIGN_LEFT, 10, 20 + 8 * !radial_style, boss->name, res_font("standard"), RGBA(o, o, o, o));
|
||
|
||
float remaining = boss->hud.attack_timer;
|
||
Color clr_int, clr_fract;
|
||
|
||
if(remaining < 6) {
|
||
clr_int = *RGB(1.0, 0.2, 0.2);
|
||
} else if(remaining < 11) {
|
||
clr_int = *RGB(1.0, 1.0, 0.2);
|
||
} else {
|
||
clr_int = *RGB(1.0, 1.0, 1.0);
|
||
}
|
||
|
||
color_mul_scalar(&clr_int, o);
|
||
clr_fract = *RGBA(clr_int.r * 0.5, clr_int.g * 0.5, clr_int.b * 0.5, clr_int.a);
|
||
|
||
Font *f_int = res_font("standard");
|
||
Font *f_fract = res_font("small");
|
||
double pos_x, pos_y;
|
||
|
||
Alignment align;
|
||
|
||
if(radial_style) {
|
||
// pos_x = (VIEWPORT_W - w_total) * 0.5;
|
||
pos_x = VIEWPORT_W / 2;
|
||
pos_y = 64;
|
||
align = ALIGN_CENTER;
|
||
} else {
|
||
// pos_x = VIEWPORT_W - w_total - 10;
|
||
pos_x = VIEWPORT_W - 10;
|
||
pos_y = 12;
|
||
align = ALIGN_RIGHT;
|
||
}
|
||
|
||
r_shader("text_hud");
|
||
draw_fraction(remaining, align, pos_x, pos_y, f_int, f_fract, &clr_int, &clr_fract, true);
|
||
r_shader("sprite_default");
|
||
|
||
// remaining spells
|
||
Color *clr = RGBA(0.7 * o, 0.7 * o, 0.7 * o, 0.7 * o);
|
||
Sprite *star = res_sprite("star");
|
||
float x = 10 + star->w * 0.5;
|
||
bool spell_found = false;
|
||
|
||
for(Attack *a = boss->hud.a_cur, *a_end = boss->attacks + boss->acount; a && a < a_end; ++a) {
|
||
if(
|
||
ATTACK_IS_SPELL(a->type) &&
|
||
(a->type != AT_ExtraSpell) &&
|
||
!boss_should_skip_attack(boss, a)
|
||
) {
|
||
// I guess we can just always skip the first one
|
||
if(spell_found) {
|
||
r_draw_sprite(&(SpriteParams) {
|
||
.sprite_ptr = star,
|
||
.pos = { x, 40 + 8 * !radial_style },
|
||
.color = clr,
|
||
});
|
||
x += star->w * 1.1;
|
||
} else {
|
||
spell_found = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
r_color4(1, 1, 1, 1);
|
||
}
|
||
|
||
if(!radial_style) {
|
||
draw_linear_healthbar(boss);
|
||
}
|
||
}
|
||
|
||
static void boss_rule_extra(Boss *boss, float alpha) {
|
||
if(global.frames % 5 || boss->in_background) {
|
||
return;
|
||
}
|
||
|
||
// XXX: not sure why, but the cast is needed to not desync 1.4 replays
|
||
int cnt = 5 * (double)max(1, alpha);
|
||
alpha = min(2, alpha);
|
||
int lt = 1;
|
||
|
||
if(alpha == 0) {
|
||
lt += 2;
|
||
alpha = rng_real();
|
||
}
|
||
|
||
for(int i = 0; i < cnt; ++i) {
|
||
float a = i*2*M_PI/cnt + global.frames / 100.0;
|
||
cmplx dir = cdir(a+global.frames/50.0);
|
||
cmplx vel = dir * 3;
|
||
float v = max(0, alpha - 1);
|
||
float psina = psin(a);
|
||
|
||
PARTICLE(
|
||
.sprite = (rng_chance(v * 0.3) || lt > 1) ? "stain" : "arc",
|
||
.pos = boss->pos + dir * (100 + 50 * psin(alpha*global.frames/10.0+2*i)) * alpha,
|
||
.color = color_mul_scalar(RGBA(
|
||
1.0 - 0.5 * psina * v,
|
||
0.5 + 0.2 * psina * (1-v),
|
||
0.5 + 0.5 * psina * v,
|
||
0.0
|
||
), 0.8),
|
||
.timeout = 30*lt,
|
||
.draw_rule = pdraw_timeout_scalefade(0, 3.5, 1, 0),
|
||
.move = move_linear(vel * (1 - 2 * !(global.frames % 10))),
|
||
);
|
||
}
|
||
}
|
||
|
||
bool boss_is_dying(Boss *boss) {
|
||
return
|
||
boss->current &&
|
||
attack_has_finished(boss->current) &&
|
||
boss->current->type != AT_Move &&
|
||
boss_attack_is_final(boss, boss->current);
|
||
}
|
||
|
||
bool boss_is_fleeing(Boss *boss) {
|
||
return
|
||
boss->current &&
|
||
boss->current->type == AT_Move &&
|
||
boss_attack_is_final(boss, boss->current);
|
||
}
|
||
|
||
bool boss_is_vulnerable(Boss *boss) {
|
||
Attack *atk = boss->current;
|
||
|
||
return
|
||
!boss->in_background &&
|
||
atk &&
|
||
atk->type != AT_Move &&
|
||
atk->type != AT_SurvivalSpell &&
|
||
!attack_has_finished(atk);
|
||
}
|
||
|
||
bool boss_is_player_collision_active(Boss *boss) {
|
||
return
|
||
boss->current &&
|
||
boss->current->type != AT_Move &&
|
||
attack_is_active(boss->current) &&
|
||
!boss->in_background &&
|
||
boss->background_transition < 0.5f &&
|
||
!boss_is_dying(boss) &&
|
||
!boss_is_fleeing(boss);
|
||
}
|
||
|
||
static DamageResult ent_damage_boss(EntityInterface *ent, const DamageInfo *dmg) {
|
||
Boss *boss = ENT_CAST(ent, Boss);
|
||
|
||
if(
|
||
!boss_is_vulnerable(boss) ||
|
||
dmg->type == DMG_ENEMY_SHOT ||
|
||
dmg->type == DMG_ENEMY_COLLISION
|
||
) {
|
||
return DMG_RESULT_IMMUNE;
|
||
}
|
||
|
||
float factor;
|
||
|
||
if(dmg->type == DMG_PLAYER_SHOT || dmg->type == DMG_PLAYER_DISCHARGE) {
|
||
factor = boss->shot_damage_multiplier;
|
||
} else if(dmg->type == DMG_PLAYER_BOMB) {
|
||
factor = boss->bomb_damage_multiplier;
|
||
} else {
|
||
factor = 1.0f;
|
||
}
|
||
|
||
int min_damage_time = 60;
|
||
int max_damage_time = 300;
|
||
int pattern_time = global.frames - NOT_NULL(boss->current)->starttime;
|
||
|
||
if(pattern_time < max_damage_time) {
|
||
float span = max_damage_time - min_damage_time;
|
||
factor = clamp((pattern_time - min_damage_time) / span, 0.0f, 1.0f);
|
||
}
|
||
|
||
if(factor == 0) {
|
||
return DMG_RESULT_IMMUNE;
|
||
}
|
||
|
||
if(dmg->amount > 0 && global.frames-boss->lastdamageframe > 2) {
|
||
boss->lastdamageframe = global.frames;
|
||
}
|
||
|
||
float damage = min(boss->current->hp, dmg->amount * factor);
|
||
boss->current->hp -= damage;
|
||
boss->damage_to_power_accum += damage;
|
||
|
||
if(boss->current->hp < boss->current->maxhp * 0.1) {
|
||
play_sfx_loop("hit1");
|
||
} else {
|
||
play_sfx_loop("hit0");
|
||
}
|
||
|
||
return DMG_RESULT_OK;
|
||
}
|
||
|
||
static void calc_spell_bonus(Attack *a, SpellBonus *bonus) {
|
||
bool survival = a->type == AT_SurvivalSpell;
|
||
bonus->failed = attack_was_failed(a);
|
||
|
||
int time_left = clamp(a->starttime + a->timeout - global.frames, 0, a->timeout);
|
||
|
||
double piv_factor = global.plr.point_item_value / (double)PLR_START_PIV;
|
||
double base = a->bonus_base * 0.5 * (1 + piv_factor);
|
||
|
||
bonus->clear = bonus->failed ? 0 : base;
|
||
bonus->time = survival ? 0 : 0.5 * base * (time_left / (double)a->timeout);
|
||
bonus->endurance = 0;
|
||
bonus->survival = 0;
|
||
|
||
if(bonus->failed) {
|
||
bonus->time /= 4;
|
||
bonus->endurance = base * 0.1 * (max(0, a->failtime - a->starttime) / (double)a->timeout);
|
||
} else if(survival) {
|
||
bonus->survival = base * (1.0 + 0.02 * (a->timeout / (double)FPS));
|
||
}
|
||
|
||
bonus->total = bonus->clear + bonus->time + bonus->endurance + bonus->survival;
|
||
bonus->diff_multiplier = 0.6 + 0.2 * global.diff;
|
||
bonus->total *= bonus->diff_multiplier;
|
||
}
|
||
|
||
static void boss_give_spell_bonus(Boss *boss, Attack *a, Player *plr) {
|
||
SpellBonus bonus = { 0 };
|
||
calc_spell_bonus(a, &bonus);
|
||
|
||
const char *title = bonus.failed ? "Spell Card failed…" : "Spell Card captured!";
|
||
|
||
char diff_bonus_text[6];
|
||
snprintf(diff_bonus_text, sizeof(diff_bonus_text), "x%.2f", bonus.diff_multiplier);
|
||
|
||
player_add_points(plr, bonus.total, plr->pos);
|
||
|
||
StageTextTable tbl;
|
||
stagetext_begin_table(&tbl, title, RGB(1, 1, 1), RGB(1, 1, 1), VIEWPORT_W/2, 0,
|
||
ATTACK_END_DELAY_SPELL * 2, ATTACK_END_DELAY_SPELL / 2, ATTACK_END_DELAY_SPELL);
|
||
stagetext_table_add_numeric_nonzero(&tbl, "Clear bonus", bonus.clear);
|
||
stagetext_table_add_numeric_nonzero(&tbl, "Time bonus", bonus.time);
|
||
stagetext_table_add_numeric_nonzero(&tbl, "Survival bonus", bonus.survival);
|
||
stagetext_table_add_numeric_nonzero(&tbl, "Endurance bonus", bonus.endurance);
|
||
stagetext_table_add_separator(&tbl);
|
||
stagetext_table_add(&tbl, "Diff. multiplier", diff_bonus_text);
|
||
stagetext_table_add_numeric(&tbl, "Total", bonus.total);
|
||
stagetext_end_table(&tbl);
|
||
|
||
play_sfx("spellend");
|
||
|
||
if(!bonus.failed) {
|
||
play_sfx("spellclear");
|
||
}
|
||
}
|
||
|
||
int attacktype_start_delay(AttackType t) {
|
||
switch(t) {
|
||
case AT_ExtraSpell: return ATTACK_START_DELAY_EXTRA;
|
||
default: return ATTACK_START_DELAY;
|
||
}
|
||
}
|
||
|
||
int attacktype_end_delay(AttackType t) {
|
||
switch(t) {
|
||
case AT_Spellcard: return ATTACK_END_DELAY_SPELL;
|
||
case AT_SurvivalSpell: return ATTACK_END_DELAY_SURV;
|
||
case AT_ExtraSpell: return ATTACK_END_DELAY_EXTRA;
|
||
case AT_Move: return ATTACK_END_DELAY_MOVE;
|
||
default: return ATTACK_END_DELAY;
|
||
}
|
||
}
|
||
|
||
static int attack_end_delay(Boss *boss) {
|
||
if(boss_attack_is_final(boss, boss->current)) {
|
||
return BOSS_DEATH_DELAY;
|
||
}
|
||
|
||
return attacktype_end_delay(boss->current->type);
|
||
}
|
||
|
||
static bool spell_is_overload(AttackInfo *spell) {
|
||
// HACK HACK HACK
|
||
StagesExports *e = dynstage_get_exports();
|
||
struct stage5_spells_s *stage5_spells = (struct stage5_spells_s*)e->stage5.spells;
|
||
return spell == &stage5_spells->extra.overload;
|
||
}
|
||
|
||
static void clear_hazards_and_enemies(void) {
|
||
enemy_kill_all(&global.enemies);
|
||
stage_clear_hazards(CLEAR_HAZARDS_ALL | CLEAR_HAZARDS_FORCE);
|
||
}
|
||
|
||
void boss_finish_current_attack(Boss *boss) {
|
||
AttackType t = boss->current->type;
|
||
|
||
boss->in_background = false;
|
||
boss->current->hp = 0;
|
||
|
||
aniplayer_soft_switch(&boss->ani,"main",0);
|
||
|
||
if(t != AT_Move) {
|
||
clear_hazards_and_enemies();
|
||
}
|
||
|
||
if(ATTACK_IS_SPELL(t)) {
|
||
boss_give_spell_bonus(boss, boss->current, &global.plr);
|
||
|
||
if(!attack_was_failed(boss->current)) {
|
||
StageProgress *p = get_spellstage_progress(boss->current, NULL, true);
|
||
|
||
if(p) {
|
||
progress_register_stage_cleared(p, global.plr.mode);
|
||
}
|
||
|
||
// HACK
|
||
if(spell_is_overload(boss->current->info)) {
|
||
stage_unlock_bgm("bonus0");
|
||
}
|
||
} else if(boss->current->type != AT_ExtraSpell) {
|
||
boss->failed_spells++;
|
||
}
|
||
|
||
spawn_items(boss->pos,
|
||
ITEM_POWER, 14,
|
||
ITEM_POINTS, 12,
|
||
ITEM_BOMB_FRAGMENT, (attack_was_failed(boss->current) ? 0 : 1)
|
||
);
|
||
}
|
||
|
||
if(boss->current < boss->attacks + boss->acount - 1) {
|
||
// If the next attack is an extra spell, determine whether we have enough voltage now, and
|
||
// if not, skip it. This can't be done any later, because we have to know whether to start
|
||
// the death sequence this frame (since the extra spell is usually the final one).
|
||
Attack *next = boss->current + 1;
|
||
if(next->type == AT_ExtraSpell && global.plr.voltage < global.voltage_threshold) {
|
||
// see boss_should_skip_attack()
|
||
next->timeout = 0;
|
||
}
|
||
}
|
||
|
||
boss->current->endtime = global.frames + attack_end_delay(boss);
|
||
boss->current->endtime_undelayed = global.frames;
|
||
|
||
boss_reset_motion(boss);
|
||
coevent_signal_once(&boss->current->events.finished);
|
||
}
|
||
|
||
static void boss_schedule_next_attack(Boss *b, Attack *a);
|
||
|
||
void process_boss(Boss **pboss) {
|
||
Boss *boss = *pboss;
|
||
|
||
if(!boss) {
|
||
return;
|
||
}
|
||
|
||
move_update(&boss->pos, &boss->move);
|
||
aniplayer_update(&boss->ani);
|
||
update_hud(boss);
|
||
|
||
fapproach_asymptotic_p(&boss->background_transition, boss->in_background ? 1.0f : 0.0f, 0.15, 1e-3);
|
||
boss->ent.draw_layer = lerp(LAYER_BOSS, LAYER_BACKGROUND, boss->background_transition);
|
||
|
||
if(!boss->current || dialog_is_active(global.dialog)) {
|
||
return;
|
||
}
|
||
|
||
int time = global.frames - boss->current->starttime;
|
||
bool extra = boss->current->type == AT_ExtraSpell;
|
||
bool over = attack_has_finished(boss->current) && global.frames >= boss->current->endtime;
|
||
|
||
if(time == 0) {
|
||
coevent_signal_once(&boss->current->events.started);
|
||
}
|
||
|
||
if(!attack_has_finished(boss->current)) {
|
||
int remaining = boss->current->timeout - time;
|
||
|
||
if(boss->current->type != AT_Move && remaining <= 11*FPS && remaining > 0 && !(time % FPS)) {
|
||
play_sfx(remaining <= 6*FPS ? "timeout2" : "timeout1");
|
||
}
|
||
}
|
||
|
||
if(extra) {
|
||
float base = 0.2;
|
||
float ampl = 0.2;
|
||
float s = sin(time / 90.0 + M_PI*1.2);
|
||
|
||
if(attack_has_finished(boss->current)) {
|
||
float p = (boss->current->endtime - global.frames)/(float)ATTACK_END_DELAY_EXTRA;
|
||
float a = max((base + ampl * s) * p * 0.5f, 5 * powf(1 - p, 3));
|
||
if(a < 2) {
|
||
stage_shake_view(3 * a);
|
||
boss_rule_extra(boss, a);
|
||
if(a > 1) {
|
||
boss_rule_extra(boss, a * 0.5);
|
||
if(a > 1.3) {
|
||
stage_shake_view(5 * a);
|
||
if(a > 1.7)
|
||
stage_shake_view(2 * a);
|
||
boss_rule_extra(boss, 0);
|
||
boss_rule_extra(boss, 0.1);
|
||
}
|
||
}
|
||
}
|
||
} else if(time < 0) {
|
||
boss_rule_extra(boss, 1+time/(float)ATTACK_START_DELAY_EXTRA);
|
||
} else {
|
||
float o = min(0, -5 + time/30.0f);
|
||
float q = (time <= 150? 1 - powf(time/250.0f, 2) : min(1, time/60.0f));
|
||
|
||
boss_rule_extra(boss, max(1-time/300.0f, base + ampl * s) * q);
|
||
if(o) {
|
||
boss_rule_extra(boss, max(1-time/300.0f, base + ampl * s) - o);
|
||
|
||
stage_shake_view(5);
|
||
if(o > -0.05) {
|
||
stage_shake_view(10);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
bool timedout = time > boss->current->timeout;
|
||
|
||
if((boss->current->type != AT_Move && boss->current->hp <= 0) || timedout) {
|
||
if(!attack_has_finished(boss->current)) {
|
||
if(timedout && boss->current->type != AT_SurvivalSpell) {
|
||
boss->current->failtime = global.frames;
|
||
}
|
||
|
||
boss_finish_current_attack(boss);
|
||
} else if(boss->current->type != AT_Move || !boss_attack_is_final(boss, boss->current)) {
|
||
// XXX: do we actually need to call this for AT_Move attacks at all?
|
||
// it should be harmless, but probably unnecessary.
|
||
// i'll be conservative and leave it in for now.
|
||
clear_hazards_and_enemies();
|
||
}
|
||
}
|
||
|
||
bool dying = boss_is_dying(boss);
|
||
bool fleeing = boss_is_fleeing(boss);
|
||
|
||
if(dying || fleeing) {
|
||
coevent_signal_once(&boss->events.defeated);
|
||
}
|
||
|
||
if(dying) {
|
||
float t = (global.frames - boss->current->endtime)/(float)BOSS_DEATH_DELAY + 1;
|
||
RNG_ARRAY(rng, 2);
|
||
|
||
Color *clr = RGBA_MUL_ALPHA(0.1 + sin(10*t), 0.1 + cos(10*t), 0.5, t);
|
||
clr->a = 0;
|
||
|
||
PARTICLE(
|
||
.sprite = "petal",
|
||
.pos = boss->pos,
|
||
.draw_rule = pdraw_petal_random(),
|
||
.color = clr,
|
||
.move = move_asymptotic_simple(
|
||
vrng_sign(rng[0]) * (3 + t * 5 * vrng_real(rng[1])) * cdir(M_PI*8*t),
|
||
5
|
||
),
|
||
.layer = LAYER_PARTICLE_PETAL,
|
||
.flags = PFLAG_REQUIREDPARTICLE,
|
||
);
|
||
|
||
if(!extra) {
|
||
stage_shake_view(5 * (t + t*t + t*t*t));
|
||
}
|
||
|
||
if(t == 1) {
|
||
for(int i = 0; i < 10; ++i) {
|
||
spawn_boss_glow(boss, &boss->glowcolor, 60 + 20 * i);
|
||
}
|
||
|
||
for(int i = 0; i < 256; i++) {
|
||
RNG_ARRAY(rng, 3);
|
||
PARTICLE(
|
||
.sprite = "flare",
|
||
.pos = boss->pos,
|
||
.timeout = vrng_range(rng[2], 60, 70),
|
||
.draw_rule = pdraw_timeout_fade(1, 0),
|
||
.move = move_linear(vrng_range(rng[0], 3, 13) * vrng_dir(rng[1])),
|
||
);
|
||
}
|
||
|
||
PARTICLE(
|
||
.proto = pp_blast,
|
||
.pos = boss->pos,
|
||
.timeout = 60,
|
||
.draw_rule = pdraw_timeout_scalefade(0, 4, 1, 0),
|
||
);
|
||
|
||
PARTICLE(
|
||
.proto = pp_blast,
|
||
.pos = boss->pos,
|
||
.timeout = 70,
|
||
.draw_rule = pdraw_timeout_scalefade(0, 3.5, 1, 0),
|
||
);
|
||
}
|
||
|
||
play_sfx_ex("bossdeath", BOSS_DEATH_DELAY * 2, false);
|
||
}
|
||
|
||
if(boss_is_player_collision_active(boss) && cabs(boss->pos - global.plr.pos) < BOSS_HURT_RADIUS) {
|
||
ent_damage(&global.plr.ent, &(DamageInfo) { .type = DMG_ENEMY_COLLISION });
|
||
}
|
||
|
||
#ifdef DEBUG
|
||
if(env_get("TAISEI_SKIP_TO_DIALOG", 0) && gamekeypressed(KEY_FOCUS)) {
|
||
over = true;
|
||
}
|
||
#endif
|
||
|
||
if(over) {
|
||
if(
|
||
global.stage->type == STAGE_SPELL &&
|
||
boss->current->type != AT_Move &&
|
||
attack_was_failed(boss->current)
|
||
) {
|
||
stage_gameover();
|
||
}
|
||
|
||
log_debug("Current attack [%s] is over", boss->current->name);
|
||
|
||
boss_reset_motion(boss);
|
||
coevent_signal_once(&boss->current->events.completed);
|
||
COEVENT_CANCEL_ARRAY(boss->current->events);
|
||
|
||
for(;;) {
|
||
if(boss->current == boss->attacks + boss->acount - 1) {
|
||
// no more attacks, die
|
||
boss->current = NULL;
|
||
boss_death(pboss);
|
||
break;
|
||
}
|
||
|
||
boss->current++;
|
||
|
||
if(boss_should_skip_attack(boss, boss->current)) {
|
||
COEVENT_CANCEL_ARRAY(boss->current->events);
|
||
continue;
|
||
}
|
||
|
||
Attack *next = boss->current;
|
||
boss->current = NULL;
|
||
boss_schedule_next_attack(boss, next);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
void boss_reset_motion(Boss *boss) {
|
||
boss->move = move_dampen(boss->move.velocity, 0.8);
|
||
}
|
||
|
||
static void boss_death_effect_draw_overlay(Projectile *p, int t, ProjDrawRuleArgs args) {
|
||
FBPair *framebuffers = stage_get_fbpair(FBPAIR_FG);
|
||
r_framebuffer(framebuffers->front);
|
||
r_uniform_sampler("noise_tex", "static");
|
||
r_uniform_int("frames", global.frames);
|
||
r_uniform_float("progress", t / p->timeout);
|
||
r_uniform_vec2("origin", re(p->pos), VIEWPORT_H - im(p->pos));
|
||
r_uniform_vec2("clear_origin", re(p->pos), VIEWPORT_H - im(p->pos));
|
||
r_uniform_vec2("viewport", VIEWPORT_W, VIEWPORT_H);
|
||
r_uniform_float("size", hypotf(VIEWPORT_W, VIEWPORT_H));
|
||
draw_framebuffer_tex(framebuffers->back, VIEWPORT_W, VIEWPORT_H);
|
||
fbpair_swap(framebuffers);
|
||
|
||
// This change must propagate, hence the r_state salsa. Yes, pop then push, I know what I'm doing.
|
||
r_state_pop();
|
||
r_framebuffer(framebuffers->back);
|
||
r_state_push();
|
||
}
|
||
|
||
void boss_death(Boss **boss) {
|
||
Attack *a = boss_get_final_attack(*boss);
|
||
bool fleed = false;
|
||
|
||
if(!a) {
|
||
// XXX: why does this happen?
|
||
log_debug("FIXME: boss had no final attacK?");
|
||
} else {
|
||
fleed = a->type == AT_Move;
|
||
}
|
||
|
||
if((*boss)->acount && !fleed) {
|
||
petal_explosion(35, (*boss)->pos);
|
||
}
|
||
|
||
if(!fleed) {
|
||
clear_hazards_and_enemies();
|
||
stage_shake_view(100);
|
||
|
||
PARTICLE(
|
||
.pos = (*boss)->pos,
|
||
.size = 1+I,
|
||
.timeout = 120,
|
||
.draw_rule = boss_death_effect_draw_overlay,
|
||
.blend = BLEND_NONE,
|
||
.flags = PFLAG_NOREFLECT | PFLAG_REQUIREDPARTICLE,
|
||
.layer = LAYER_OVERLAY,
|
||
.shader = "boss_death",
|
||
);
|
||
}
|
||
|
||
free_boss(*boss);
|
||
*boss = NULL;
|
||
}
|
||
|
||
static void free_attack(Attack *a) {
|
||
COEVENT_CANCEL_ARRAY(a->events);
|
||
}
|
||
|
||
void free_boss(Boss *boss) {
|
||
COEVENT_CANCEL_ARRAY(boss->events);
|
||
|
||
for(int i = 0; i < boss->acount; i++) {
|
||
free_attack(&boss->attacks[i]);
|
||
}
|
||
|
||
ent_unregister(&boss->ent);
|
||
boss_set_portrait(boss, NULL, NULL, NULL);
|
||
aniplayer_free(&boss->ani);
|
||
STAGE_RELEASE_OBJ(boss);
|
||
}
|
||
|
||
static void boss_schedule_next_attack(Boss *b, Attack *a) {
|
||
assert(b->current == NULL);
|
||
assert(!a->events.initiated.num_signaled);
|
||
log_debug("[%i] %s", global.frames, a->name);
|
||
coevent_signal_once(&a->events.initiated);
|
||
}
|
||
|
||
void boss_engage(Boss *b) {
|
||
boss_schedule_next_attack(b, b->attacks);
|
||
}
|
||
|
||
void boss_start_next_attack(Boss *b, Attack *a) {
|
||
assert(b->current == NULL);
|
||
assert(a->events.initiated.num_signaled > 0);
|
||
assert(a->events.started.num_signaled == 0);
|
||
|
||
b->current = a;
|
||
log_debug("[%i] %s", global.frames, a->name);
|
||
|
||
StageInfo *i;
|
||
StageProgress *p = get_spellstage_progress(a, &i, true);
|
||
|
||
if(p) {
|
||
if(!p->unlocked) {
|
||
log_info("Spellcard unlocked! %s: %s", i->title, i->subtitle);
|
||
p->unlocked = true;
|
||
}
|
||
|
||
progress_register_stage_played(p, global.plr.mode);
|
||
}
|
||
|
||
// This should go before a->rule(b,EVENT_BIRTH), so it doesn’t reset values set by the attack rule.
|
||
b->bomb_damage_multiplier = 1.0;
|
||
b->shot_damage_multiplier = 1.0;
|
||
|
||
a->starttime = global.frames + attacktype_start_delay(a->type);
|
||
|
||
if(ATTACK_IS_SPELL(a->type)) {
|
||
play_sfx(a->type == AT_ExtraSpell ? "charge_extra" : "charge_generic");
|
||
|
||
for(int i = 0; i < 10+5*(a->type == AT_ExtraSpell); i++) {
|
||
RNG_ARRAY(rng, 4);
|
||
|
||
PARTICLE(
|
||
.sprite = "stain",
|
||
.pos = CMPLX(VIEWPORT_W/2 + vrng_sreal(rng[0]) * VIEWPORT_W/4, VIEWPORT_H/2 + vrng_sreal(rng[1]) * 30),
|
||
.color = RGBA(0.2, 0.3, 0.4, 0.0),
|
||
.timeout = 50,
|
||
.draw_rule = pdraw_timeout_scalefade(0, 1, 1, 0),
|
||
.move = move_linear(vrng_sign(rng[2]) * 10 * vrng_range(rng[3], 1, 4)),
|
||
);
|
||
}
|
||
}
|
||
|
||
clear_hazards_and_enemies();
|
||
}
|
||
|
||
Attack *boss_add_attack(Boss *boss, AttackType type, const char *name, float timeout, int hp, BossRule draw_rule) {
|
||
assert(boss->acount < BOSS_MAX_ATTACKS);
|
||
|
||
Attack *a = &boss->attacks[boss->acount];
|
||
boss->acount += 1;
|
||
memset(a, 0, sizeof(Attack));
|
||
a->type = type;
|
||
a->name = name;
|
||
a->timeout = timeout * FPS;
|
||
a->maxhp = hp;
|
||
a->hp = hp;
|
||
a->draw_rule = draw_rule;
|
||
|
||
COEVENT_INIT_ARRAY(a->events);
|
||
|
||
return a;
|
||
}
|
||
|
||
static Attack *_boss_add_attack_from_info(Boss *boss, AttackInfo *info){
|
||
Attack *a = boss_add_attack(
|
||
boss,
|
||
info->type,
|
||
info->name,
|
||
info->timeout,
|
||
info->hp,
|
||
info->draw_rule
|
||
);
|
||
|
||
a->info = info;
|
||
boss_set_attack_bonus(a, info->bonus_rank);
|
||
|
||
return a;
|
||
}
|
||
|
||
TASK(attack_task_helper_custom, {
|
||
CoEvent *evt;
|
||
BossAttackTask task;
|
||
BossAttackTaskArgs *task_args;
|
||
size_t task_args_size;
|
||
BossAttackTaskArgs task_args_header;
|
||
}) {
|
||
size_t args_size = ARGS.task_args_size;
|
||
BossAttackTaskArgs *orig_args = NOT_NULL(ARGS.task_args);
|
||
char *args = TASK_MALLOC(args_size);
|
||
size_t header_ofs = sizeof(ARGS.task_args_header);
|
||
assume(args_size >= header_ofs);
|
||
|
||
memcpy(args, &ARGS.task_args_header, sizeof(ARGS.task_args_header));
|
||
memcpy(args + header_ofs, (char*)orig_args + header_ofs, args_size - header_ofs);
|
||
|
||
WAIT_EVENT_OR_DIE(ARGS.evt);
|
||
|
||
ARGS.task._cotask_BossAttack_thunk(args, args_size);
|
||
}
|
||
|
||
static void setup_attack_task_with_custom_args(
|
||
Boss *boss, Attack *a, BossAttackTask task,
|
||
BossAttackTaskArgs *args, size_t args_size
|
||
) {
|
||
INVOKE_TASK(attack_task_helper_custom,
|
||
.evt = &a->events.initiated,
|
||
.task = task,
|
||
.task_args = args,
|
||
.task_args_size = args_size,
|
||
.task_args_header = {
|
||
.boss = ENT_BOX(boss),
|
||
.attack = a
|
||
}
|
||
);
|
||
}
|
||
|
||
TASK(attack_task_helper, {
|
||
BossAttackTask task;
|
||
BossAttackTaskArgs task_args;
|
||
}) {
|
||
ARGS.task._cotask_BossAttack_thunk(&ARGS.task_args, sizeof(ARGS.task_args));
|
||
}
|
||
|
||
static void setup_attack_task(Boss *boss, Attack *a, BossAttackTask task) {
|
||
INVOKE_TASK_WHEN(&a->events.initiated, attack_task_helper,
|
||
.task = task,
|
||
.task_args = {
|
||
.boss = ENT_BOX(boss),
|
||
.attack = a
|
||
}
|
||
);
|
||
}
|
||
|
||
Attack *boss_add_attack_task(Boss *boss, AttackType type, const char *name, float timeout, int hp, BossAttackTask task, BossRule draw_rule) {
|
||
Attack *a = boss_add_attack(boss, type, name, timeout, hp, draw_rule);
|
||
setup_attack_task(boss, a, task);
|
||
return a;
|
||
}
|
||
|
||
void boss_set_attack_bonus(Attack *a, int rank) {
|
||
if(!ATTACK_IS_SPELL(a->type)) {
|
||
return;
|
||
}
|
||
|
||
if(rank == 0) {
|
||
log_warn("Bonus rank was not set for this spell!");
|
||
}
|
||
|
||
if(a->type == AT_SurvivalSpell) {
|
||
a->bonus_base = (2000.0 + 600.0 * (a->timeout / (double)FPS)) * (9 + rank);
|
||
} else {
|
||
a->bonus_base = (2000.0 + a->hp * 0.6) * (9 + rank);
|
||
}
|
||
|
||
if(a->type == AT_ExtraSpell) {
|
||
a->bonus_base *= 1.5;
|
||
}
|
||
}
|
||
|
||
TASK(boss_generic_move_finish, { BoxedBoss boss; cmplx dest; }) {
|
||
Boss *b = TASK_BIND(ARGS.boss);
|
||
b->pos = ARGS.dest;
|
||
b->move = move_linear(0);
|
||
}
|
||
|
||
TASK_WITH_INTERFACE(boss_generic_move, BossAttack) {
|
||
Boss *b = INIT_BOSS_ATTACK(&ARGS);
|
||
cmplx dest = ARGS.attack->info->pos_dest;
|
||
b->move = move_from_towards(b->pos, dest, 0.07);
|
||
INVOKE_TASK_WHEN(&ARGS.attack->events.completed, boss_generic_move_finish, ENT_BOX(b), dest);
|
||
BEGIN_BOSS_ATTACK(&ARGS);
|
||
}
|
||
|
||
Attack *boss_add_attack_from_info(Boss *boss, AttackInfo *info, bool move) {
|
||
if(move) {
|
||
Attack *m = boss_add_attack_task(
|
||
boss, AT_Move, "Generic Move", 0.5, 0,
|
||
TASK_INDIRECT(BossAttack, boss_generic_move), NULL
|
||
);
|
||
m->info = info;
|
||
}
|
||
|
||
Attack *a = _boss_add_attack_from_info(boss, info);
|
||
|
||
if(info->task._cotask_BossAttack_thunk) {
|
||
setup_attack_task(boss, a, info->task);
|
||
}
|
||
|
||
return a;
|
||
}
|
||
|
||
Attack *boss_add_attack_from_info_with_args(
|
||
Boss *boss, AttackInfo *info, BossAttackTaskCustomArgs args
|
||
) {
|
||
Attack *a = _boss_add_attack_from_info(boss, info);
|
||
assume(info->task._cotask_BossAttack_thunk != NULL);
|
||
setup_attack_task_with_custom_args(boss, a, info->task, args.ptr, args.size);
|
||
return a;
|
||
}
|
||
|
||
Attack *boss_add_attack_task_with_args(
|
||
Boss *boss, AttackType type, const char *name, float timeout, int hp,
|
||
BossAttackTask task, BossRule draw_rule, BossAttackTaskCustomArgs args
|
||
) {
|
||
Attack *a = boss_add_attack(boss, type, name, timeout, hp, draw_rule);
|
||
setup_attack_task_with_custom_args(boss, a, task, args.ptr, args.size);
|
||
return a;
|
||
}
|
||
|
||
Boss *_init_boss_attack_task(const BossAttackTaskArgs *args) {
|
||
Boss *pboss = TASK_BIND(args->boss);
|
||
CANCEL_TASK_AFTER(&args->attack->events.finished, THIS_TASK);
|
||
return pboss;
|
||
}
|
||
|
||
void _begin_boss_attack_task(const BossAttackTaskArgs *args) {
|
||
boss_start_next_attack(NOT_NULL(ENT_UNBOX(args->boss)), args->attack);
|
||
WAIT_EVENT_OR_DIE(&args->attack->events.started);
|
||
}
|
||
|
||
void boss_preload(ResourceGroup *rg) {
|
||
res_group_preload(rg, RES_SFX, RESF_OPTIONAL,
|
||
"charge_generic",
|
||
"charge_extra",
|
||
"discharge",
|
||
"spellend",
|
||
"spellclear",
|
||
"timeout1",
|
||
"timeout2",
|
||
"bossdeath",
|
||
NULL);
|
||
|
||
res_group_preload(rg, RES_SPRITE, RESF_DEFAULT,
|
||
"boss_circle",
|
||
"boss_indicator",
|
||
"boss_spellcircle0",
|
||
"part/arc",
|
||
"part/blast_huge_rays",
|
||
"part/boss_shadow",
|
||
"spell",
|
||
NULL);
|
||
|
||
res_group_preload(rg, RES_SHADER_PROGRAM, RESF_DEFAULT,
|
||
"boss_zoom",
|
||
"boss_death",
|
||
"spellcard_intro",
|
||
"spellcard_outro",
|
||
"spellcard_walloftext",
|
||
"sprite_silhouette",
|
||
"healthbar_linear",
|
||
"healthbar_radial",
|
||
NULL);
|
||
|
||
StageInfo *s = global.stage;
|
||
|
||
if(s->type != STAGE_SPELL || s->spell->type == AT_ExtraSpell) {
|
||
res_group_preload(rg, RES_TEXTURE, RESF_DEFAULT,
|
||
"stage3/wspellclouds",
|
||
"stage4/kurumibg2",
|
||
NULL);
|
||
}
|
||
}
|