/*
 * 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) {
	if(a->type == AT_ExtraSpell || (a->info != NULL && a->info->type == AT_ExtraSpell)) {
		if(global.plr.voltage < global.voltage_threshold) {
			return true;
		}
	}

	// Skip zero-length spells. Zero-length AT_Move and AT_Normal attacks are ok.
	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)
		);
	}

	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++;
			assert(boss->current != NULL);

			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);
	}
}
