/*
 * 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 "reimu.h"

#include "audio/audio.h"
#include "common_tasks.h"
#include "dialog/reimu.h"
#include "global.h"
#include "plrmodes.h"
#include "stagedraw.h"
#include "util/graphics.h"

#define SHOT_FORWARD_DMG 35
#define SHOT_FORWARD_DELAY 6

#define SHOT_SLAVE_DMG 44
#define SHOT_SLAVE_PRE_DELAY 3
#define SHOT_SLAVE_POST_DELAY 3

#define BOMB_PROJECTILE_DAMAGE 75
#define BOMB_PROJECTILE_FIRE_DELAY 4
#define BOMB_PROJECTILE_PERIODIC_AREA_DAMAGE 50
#define BOMB_PROJECTILE_PERIODIC_AREA_RADIUS (GAP_LENGTH * 0.5)
#define BOMB_PROJECTILE_PERIODIC_DELAY 3
#define BOMB_PROJECTILE_IMPACT_AREA_DAMAGE 50
#define BOMB_PROJECTILE_IMPACT_AREA_RADIUS GAP_LENGTH

#define ORB_RETRACT_TIME 4

#define GAP_LENGTH 128
#define GAP_WIDTH 16
#define GAP_OFFSET 8
#define GAP_LIMIT 10

// #define GAP_LENGTH 160
// #define GAP_WIDTH 160
// #define GAP_OFFSET 82

#define NUM_GAPS 4

typedef struct ReimuBGap ReimuBGap;

struct ReimuBGap {
	ReimuBGap *link;     // bullets entering this gap will exit from the linked gap
	cmplx pos;           // position of the gap's center in viewport space
	cmplx orientation;   // normalized, points to the side the gap is 'attached' to
	cmplx parallel_axis; // normalized, parallel the gap, perpendicular to orientation
};

DEFINE_ENTITY_TYPE(ReimuBSlave, {
	Sprite *sprite;
	ShaderProgram *shader;
	cmplx pos;
	uint alive;
});

DEFINE_ENTITY_TYPE(ReimuBController, {
	Player *plr;
	ShaderProgram *yinyang_shader;
	Sprite *yinyang_sprite;

	union {
		ReimuBGap array[NUM_GAPS];
		struct {
			ReimuBGap left, top, right, bottom;
		};
	} gaps;

	COEVENTS_ARRAY(
		slaves_expired
	) events;

	real bomb_alpha;
});

static void reimu_dream_gap_bomb_projectile_draw(Projectile *p, int t, ProjDrawRuleArgs args) {
	SpriteParamsBuffer spbuf;
	SpriteParams sp = projectile_sprite_params(p, &spbuf);
	sp.scale.as_cmplx = 0.75 * clamp(t / 5.0, 0.1, 1.0) * (1 + I);
	r_draw_sprite(&sp);
}

TASK(reimu_dream_gap_bomb_projectile_impact, { BoxedProjectile p; Sprite *impact_sprite; }) {
	Projectile *p = TASK_BIND(ARGS.p);

	if(p->collision && p->collision->type == PCOL_VOID) {
		// Auto-removed out of bounds; don't explode.
		return;
	}

	real range = BOMB_PROJECTILE_IMPACT_AREA_RADIUS;
	real damage = BOMB_PROJECTILE_IMPACT_AREA_DAMAGE;

	PARTICLE(
		.angle = rng_angle(),
		.color = &p->color,
		.draw_rule = pdraw_timeout_scalefade(0, 3 * range / ARGS.impact_sprite->w, 1, 0),
		.flags = PFLAG_NOREFLECT | PFLAG_REQUIREDPARTICLE | PFLAG_MANUALANGLE,
		.layer = LAYER_BOSS + 2,
		.pos = p->pos,
		.sprite_ptr = ARGS.impact_sprite,
		.timeout = 20,
	);

	stage_clear_hazards_at(p->pos, range, CLEAR_HAZARDS_ALL | CLEAR_HAZARDS_NOW);
	ent_area_damage(p->pos, range, &(DamageInfo) { damage, DMG_PLAYER_BOMB }, NULL, NULL);
}

TASK(reimu_dream_gap_bomb_projectile, {
	cmplx pos;
	cmplx vel;
	const Color *color;
	Sprite *sprite;
	Sprite *impact_sprite;
}) {
	Projectile *p = TASK_BIND(PROJECTILE(
		.angle = rng_angle(),
		.color = ARGS.color,
		.damage = 75,
		.damage_type = DMG_PLAYER_BOMB,
		.draw_rule = reimu_dream_gap_bomb_projectile_draw,
		.flags = PFLAG_MANUALANGLE,
		.move = move_linear(ARGS.vel),
		.pos = ARGS.pos,
		.size = 32 * (1 + I),
		.sprite_ptr = ARGS.sprite,
		.type = PROJ_PLAYER,
		.max_viewport_dist = BOMB_PROJECTILE_PERIODIC_AREA_RADIUS
	));

	INVOKE_TASK_WHEN(&p->events.killed, reimu_dream_gap_bomb_projectile_impact,
		ENT_BOX(p),
		ARGS.impact_sprite
	);

	for(;;) {
		WAIT(BOMB_PROJECTILE_PERIODIC_DELAY);
		real range = BOMB_PROJECTILE_PERIODIC_AREA_RADIUS;
		real damage = BOMB_PROJECTILE_PERIODIC_AREA_DAMAGE;
		stage_clear_hazards_at(p->pos, range, CLEAR_HAZARDS_ALL | CLEAR_HAZARDS_NOW);
		ent_area_damage(p->pos, range, &(DamageInfo) { damage, DMG_PLAYER_BOMB }, NULL, NULL);
	}
}

TASK(reimu_dream_bomb_noise) {
	for(;;WAIT(16)) {
		play_sfx("boon");
	}
}

TASK(reimu_dream_bomb_background, { ReimuBController *ctrl; }) {
	ReimuBController *ctrl = ARGS.ctrl;
	CoEvent *draw_event = &stage_get_draw_events()->background_drawn;

	for(;;) {
		WAIT_EVENT_OR_DIE(draw_event);
		reimu_common_bomb_bg(ctrl->plr, ctrl->bomb_alpha);
	}
}

TASK(reimu_dream_bomb, { ReimuBController *ctrl; }) {
	ReimuBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;

	BoxedTask noise_task = cotask_box(INVOKE_SUBTASK(reimu_dream_bomb_noise));
	INVOKE_SUBTASK(reimu_dream_bomb_background, ctrl);

	Sprite *spr_proj = res_sprite("proj/glowball");
	Sprite *spr_impact = res_sprite("part/blast");

	YIELD;

	int t = 0;
	do {
		Color *pcolor = HSLA(t/30.0, 0.5, 0.5, 0.5);

		for(int i = 0; i < NUM_GAPS; ++i) {
			ReimuBGap *gap = ctrl->gaps.array + i;
			INVOKE_TASK(reimu_dream_gap_bomb_projectile,
				.pos = gap->pos + gap->parallel_axis * GAP_LENGTH * 0.25 * rng_sreal(),
				.vel = -20 * gap->orientation,
				.color = pcolor,
				.sprite = spr_proj,
				.impact_sprite = spr_impact
			);
		}

		stage_shake_view(4);
		t += WAIT(BOMB_PROJECTILE_FIRE_DELAY);
	} while(player_is_bomb_active(plr));

	CANCEL_TASK(noise_task);

	while(ctrl->bomb_alpha > 0) {
		// keep bg renderer alive
		YIELD;
	}
}

TASK(reimu_dream_bomb_handler, { ReimuBController *ctrl; }) {
	ReimuBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;

	BoxedTask bomb_task = { 0 };

	for(;;) {
		WAIT_EVENT_OR_DIE(&plr->events.bomb_used);
		CANCEL_TASK(bomb_task);
		play_sfx("bomb_marisa_a");
		bomb_task = cotask_box(INVOKE_SUBTASK(reimu_dream_bomb, ctrl));
	}
}

static void reimu_dream_draw_gap_lights(ReimuBController *ctrl, int time, real strength) {
	if(strength <= 0) {
		return;
	}

	r_shader("reimu_gap_light");
	r_uniform_sampler("tex", "gaplight");
	r_uniform_float("time", time / 60.0f);
	r_uniform_float("strength", strength);

	real len = GAP_LENGTH * 3 * sqrt(log1p(strength + 1) / 0.693f);

	for(int i = 0; i < NUM_GAPS; ++i) {
		ReimuBGap *gap = ctrl->gaps.array + i;
		cmplx center = gap->pos - gap->orientation * (len * 0.5 - GAP_WIDTH * 0.6);

		r_mat_mv_push();
		r_mat_mv_translate(re(center), im(center), 0);
		r_mat_mv_rotate(carg(gap->orientation) + M_PI, 0, 0, 1);
		r_mat_mv_scale(len, GAP_LENGTH, 1);
		r_draw_quad();
		r_mat_mv_pop();
	}
}

static void reimu_dream_draw_gaps(EntityInterface *gap_renderer_ent) {
	ReimuBController *ctrl = ENT_CAST(gap_renderer_ent, ReimuBController);

	float gaps[NUM_GAPS][2];
	float angles[NUM_GAPS];
	int links[NUM_GAPS];

	for(int i = 0; i < NUM_GAPS; ++i) {
		ReimuBGap *gap = ctrl->gaps.array + i;
		gaps[i][0] = re(gap->pos);
		gaps[i][1] = im(gap->pos);
		angles[i] = -carg(gap->orientation);
		links[i] = gap->link - ctrl->gaps.array;
	}

	FBPair *framebuffers = stage_get_fbpair(FBPAIR_FG);
	bool render_to_fg = r_framebuffer_current() == framebuffers->back;

	if(render_to_fg) {
		fbpair_swap(framebuffers);
		Framebuffer *target_fb = framebuffers->back;

		// This change must propagate
		r_state_pop();
		r_framebuffer(target_fb);
		r_state_push();

		r_shader("reimu_gap");
		r_uniform_int("draw_background", true);
		r_blend(BLEND_NONE);
		r_clear(BUFFER_COLOR, RGBA(0, 0, 0, 0), 1);
	} else {
		r_shader("reimu_gap");
		r_uniform_int("draw_background", false);
		r_blend(BLEND_PREMUL_ALPHA);
	}

	r_uniform_vec2("viewport", VIEWPORT_W, VIEWPORT_H);
	r_uniform_float("time", global.frames / 60.0f);
	r_uniform_vec2("gap_size", GAP_WIDTH/2.0f, GAP_LENGTH/2.0f);
	r_uniform_vec2_array("gaps", 0, NUM_GAPS, gaps);
	r_uniform_float_array("gap_angles", 0, NUM_GAPS, angles);
	r_uniform_int_array("gap_links", 0, NUM_GAPS, links);
	draw_framebuffer_tex(framebuffers->front, VIEWPORT_W, VIEWPORT_H);

	r_blend(BLEND_PREMUL_ALPHA);

	SpriteParams yinyang = {
		.sprite_ptr = ctrl->yinyang_sprite,
		.shader_ptr = ctrl->yinyang_shader,
		.rotation.angle = global.frames * -6 * DEG2RAD,
		.color = RGB(0.95, 0.75, 1.0),
		.scale.both = 0.5,
	};

	for(int i = 0; i < NUM_GAPS; ++i) {
		ReimuBGap *gap = ctrl->gaps.array + i;
		yinyang.pos.as_cmplx = gap->pos + gap->parallel_axis * -0.5 * GAP_LENGTH;
		r_draw_sprite(&yinyang);
		yinyang.pos.as_cmplx = gap->pos + gap->parallel_axis * +0.5 * GAP_LENGTH;
		r_draw_sprite(&yinyang);
	}

	reimu_dream_draw_gap_lights(ctrl, global.frames, ctrl->bomb_alpha * ctrl->bomb_alpha);
}

static void reimu_dream_spawn_warp_effect(cmplx pos, bool exit) {
	PARTICLE(
		.sprite = "myon",
		.pos = pos,
		.color = RGBA(0.5, 0.5, 0.5, 0.5),
		.timeout = 20,
		.angle = rng_angle(),
		.draw_rule = pdraw_timeout_scalefade(0.2, 1, 1, 0),
		.layer = LAYER_PLAYER_FOCUS,
		.flags = PFLAG_MANUALANGLE,
	);

	Color *clr = color_mul_scalar(RGBA(0.75, rng_range(0, 0.4), 0.4, 0), 0.8-0.4*exit);
	PARTICLE(
		.sprite = exit ? "stain" : "stardust",
		.pos = pos,
		.color = clr,
		.timeout = 20,
		.angle = rng_angle(),
		.draw_rule = pdraw_timeout_scalefade(0.1, 0.6, 1, 0),
		.layer = LAYER_PLAYER_FOCUS,
		.flags = PFLAG_MANUALANGLE,
	);
}

static void reimu_dream_bullet_warp(ReimuBController *ctrl, Projectile *p, int *warp_count) {
	if(*warp_count < 1) {
		return;
	}

	real p_long_side = max(re(p->size), im(p->size));
	cmplx half = 0.25 * (1 + I);
	Rect p_bbox = { p->pos - p_long_side * half, p->pos + p_long_side * half };

	for(int i = 0; i < NUM_GAPS; ++i) {
		ReimuBGap *gap = ctrl->gaps.array + i;
		real a = carg(-gap->orientation/p->move.velocity);

		if(fabs(a) < M_TAU/3) {
			continue;
		}

		Rect gap_bbox, overlap = { };
		cmplx gap_size = (GAP_LENGTH + I * GAP_WIDTH) * gap->parallel_axis;
		cmplx p0 = gap->pos - gap_size * 0.5;
		cmplx p1 = gap->pos + gap_size * 0.5;
		gap_bbox.top_left = min(re(p0), re(p1)) + I * min(im(p0), im(p1));
		gap_bbox.bottom_right = max(re(p0), re(p1)) + I * max(im(p0), im(p1));

		if(rect_rect_intersection(p_bbox, gap_bbox, true, false, &overlap)) {
			cmplx o = (overlap.top_left + overlap.bottom_right) / 2;
			real fract;

			if(re(gap_size) > im(gap_size)) {
				fract = 1 - re(o - gap_bbox.top_left) / re(gap_size);
			} else {
				fract = 1 - im(o - gap_bbox.top_left) / im(gap_size);
			}

			ReimuBGap *ngap = gap->link;
			o = ngap->pos + ngap->parallel_axis * GAP_LENGTH * (1 - fract - 0.5);

			reimu_dream_spawn_warp_effect(gap->pos + gap->parallel_axis * GAP_LENGTH * (fract - 0.5), false);
			reimu_dream_spawn_warp_effect(o, true);

			cmplx new_vel = cabs(p->move.velocity) * -ngap->orientation;

			p->move.acceleration *= cnormalize(new_vel/p->move.velocity);
			p->move.velocity = new_vel;
			p->pos = o - p->move.velocity;

			--*warp_count;
		}
	}
}

static void reimu_dream_draw_slave(EntityInterface *ent) {
	ReimuBSlave *slave = ENT_CAST(ent, ReimuBSlave);
	r_draw_sprite(&(SpriteParams) {
		.sprite_ptr = slave->sprite,
		.shader_ptr = slave->shader,
		.pos.as_cmplx = slave->pos,
		.rotation.angle = global.frames * -6 * DEG2RAD,
		.color = RGB(0.95, 0.75, 1.0),
		.scale.both = 0.5,
	});
}

TASK(reimu_dream_needle, {
	ReimuBController *ctrl;
	cmplx pos;
	cmplx vel;
	ShaderProgram *shader;
}) {
	ReimuBController *ctrl = ARGS.ctrl;
	Projectile *p = TASK_BIND(PROJECTILE(
		.proto = pp_needle2,
		.pos = ARGS.pos,
		.color = RGBA_MUL_ALPHA(1, 1, 1, 0.35),
		.move = move_linear(ARGS.vel),
		.type = PROJ_PLAYER,
		.damage = SHOT_SLAVE_DMG,
		.shader_ptr = ARGS.shader,
	));

	Color *trail_color = color_mul_scalar(RGBA(0.75, 0.5, 1, 0), 0.15);
	int warp_cnt = 1;

	for(;;) {
		reimu_dream_bullet_warp(ctrl, p, &warp_cnt);

		PARTICLE(
			.sprite_ptr = p->sprite,
			.color = trail_color,
			.timeout = 12,
			.pos = p->pos,
			.move = move_linear(p->move.velocity * 0.8),
			.draw_rule = pdraw_timeout_scalefade(0, 3, 1, 0),
			.layer = LAYER_PARTICLE_LOW,
			.flags = PFLAG_NOREFLECT,
		);

		YIELD;
	}
}

TASK(reimu_dream_slave_shot, {
	ReimuBController *ctrl;
	BoxedReimuBSlave slave;
	cmplx vel;
}) {
	ReimuBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;
	ReimuBSlave *slave = TASK_BIND(ARGS.slave);
	cmplx vel = ARGS.vel;
	ShaderProgram *shader = res_shader("sprite_particle");

	for(;;) {
		WAIT_EVENT_OR_DIE(&plr->events.shoot);
		WAIT(SHOT_SLAVE_PRE_DELAY);
		INVOKE_TASK(reimu_dream_needle,
			.ctrl = ctrl,
			.pos = slave->pos,
			.vel = (plr->inputflags & INFLAG_FOCUS) ? cswap(vel) : vel,
			.shader = shader
		);
		WAIT(SHOT_SLAVE_POST_DELAY);
	}
}

TASK(reimu_dream_slave, {
	ReimuBController *ctrl;
	cmplx offset;
	real angle_offset;
	cmplx shot_dir;
}) {
	ReimuBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;
	ReimuBSlave *slave = TASK_HOST_ENT(ReimuBSlave);
	slave->ent.draw_layer = LAYER_PLAYER_SLAVE;
	slave->ent.draw_func = reimu_dream_draw_slave;
	slave->sprite = res_sprite("yinyang"),
	slave->shader = res_shader("sprite_yinyang"),
	slave->pos = plr->pos;
	slave->alive = 1;

	INVOKE_SUBTASK_WHEN(&ctrl->events.slaves_expired, common_set_bitflags,
		.pflags = &slave->alive,
		.mask = 0, .set = 0
	);

	real angle = ARGS.angle_offset + M_PI/2;
	cmplx offset = ARGS.offset;

	INVOKE_SUBTASK(reimu_dream_slave_shot,
		.ctrl = ctrl,
		.slave = ENT_BOX(slave),
		.vel = 20 * ARGS.shot_dir
	);

	do {
		cmplx ofs = (plr->inputflags & INFLAG_FOCUS) ? cswap(offset) : offset;
		ofs = cwmul(ofs, cdir(angle));
		capproach_asymptotic_p(&slave->pos, plr->pos + ofs, 0.5, 1e-5);
		angle += 0.1;
		YIELD;
	} while(slave->alive);
}

static void reimu_dream_respawn_slaves(ReimuBController *ctrl, int num_slaves) {
	coevent_signal(&ctrl->events.slaves_expired);
	cmplx shot_dir = -I;

	for(int i = 0; i < num_slaves; ++i, shot_dir *= -1) {
		INVOKE_TASK(reimu_dream_slave,
			.ctrl = ctrl,
			.offset = 48 + 32*I,
			.angle_offset = (i * M_TAU) / num_slaves,
			.shot_dir = shot_dir
		);
	}
}

static cmplx reimu_dream_gaps_reference_pos(ReimuBController *ctrl, cmplx vp) {
	cmplx rp = ctrl->plr->pos;

	if(!(ctrl->plr->inputflags & INFLAG_FOCUS)) {
		rp = CMPLX(re(rp), im(vp) - im(rp));
	} else {
		// rp = CMPLX(re(vp) - re(rp), im(rp));
	}

	return rp;
}

static cmplx reimu_dream_gap_pos(cmplx vp, cmplx ref_pos, cmplx orientation, cmplx parallel_axis, cmplx ofs_factors) {
	cmplx p = cwmul(vp, orientation) + cwmul(ref_pos, parallel_axis);
	cmplx ofs = (1 + I) * GAP_OFFSET + (GAP_LENGTH * 0.5 + GAP_LIMIT) * cwmul(ofs_factors, parallel_axis);
	return cwclamp(p, ofs, vp - ofs);
}

TASK(reimu_dream_process_gaps, { ReimuBController *ctrl; }) {
	ReimuBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;

	cmplx vp = VIEWPORT_W + VIEWPORT_H * I;
	cmplx ref_pos = reimu_dream_gaps_reference_pos(ctrl, vp);
	cmplx ofs_factors = 1 + I;

	for(int i = 0; i < NUM_GAPS; ++i) {
		ReimuBGap *gap = ctrl->gaps.array + i;
		gap->pos = reimu_dream_gap_pos(vp, ref_pos, gap->orientation, gap->parallel_axis, ofs_factors);
		gap->pos += (2 * GAP_WIDTH + GAP_OFFSET) * gap->orientation;
	}

	for(;;) {
		YIELD;
		bool is_focused = plr->inputflags & INFLAG_FOCUS;
		real pos_approach_rate = 0.1 * (0.1 + 0.9 * (1 - sqrt(ctrl->bomb_alpha)));

		ref_pos = reimu_dream_gaps_reference_pos(ctrl, vp);
		ofs_factors = 1 + I * !is_focused;  // no extra clamping of vertical gaps if focused

		for(int i = 0; i < NUM_GAPS; ++i) {
			ReimuBGap *gap = ctrl->gaps.array + i;
			cmplx target_pos = reimu_dream_gap_pos(vp, ref_pos, gap->orientation, gap->parallel_axis, ofs_factors);
			capproach_asymptotic_p(&gap->pos, target_pos, pos_approach_rate, 1e-5);
		}
	}
}

TASK(reimu_dream_ofuda, { ReimuBController *ctrl; cmplx pos; cmplx vel; ShaderProgram *shader; }) {
	ReimuBController *ctrl = ARGS.ctrl;
	Projectile *ofuda = TASK_BIND(PROJECTILE(
		.proto = pp_ofuda,
		.pos = ARGS.pos,
		.color = RGBA_MUL_ALPHA(1, 1, 1, 0.5),
		.move = move_asymptotic(1.5 * ARGS.vel, ARGS.vel, 0.8),
		.type = PROJ_PLAYER,
		.damage = SHOT_FORWARD_DMG,
		.shader_ptr = ARGS.shader,
	));

	int warp_cnt = 1;

	for(;;) {
		reimu_dream_bullet_warp(ctrl, ofuda, &warp_cnt);
		reimu_common_ofuda_swawn_trail(ofuda);
		YIELD;
	}
}

TASK(reimu_dream_shot_forward, { ReimuBController *ctrl; }) {
	ReimuBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;
	ShaderProgram *ofuda_shader = res_shader("sprite_particle");

	for(;;) {
		WAIT_EVENT_OR_DIE(&plr->events.shoot);
		play_sfx_loop("generic_shot");

		for(int i = -1; i < 2; i += 2) {
			cmplx shot_dir = i * ((plr->inputflags & INFLAG_FOCUS) ? 1 : I);
			cmplx spread_dir = shot_dir * I;

			for(int j = -1; j < 2; j += 2) {
				INVOKE_TASK(reimu_dream_ofuda,
					.ctrl = ctrl,
					.pos = plr->pos + 10 * j * spread_dir,
					.vel = 20.0 * shot_dir,
					.shader = ofuda_shader
   				);
			}
		}

		WAIT(SHOT_FORWARD_DELAY);
	}
}

TASK(reimu_dream_power_handler, { ReimuBController *ctrl; }) {
	ReimuBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;
	int old_power = player_get_effective_power(plr) / 100;

	reimu_dream_respawn_slaves(ctrl, old_power * 2);

	for(;;) {
		WAIT_EVENT_OR_DIE(&plr->events.effective_power_changed);
		int new_power = player_get_effective_power(plr) / 100;
		if(old_power != new_power) {
			reimu_dream_respawn_slaves(ctrl, new_power * 2);
			old_power = new_power;
		}
	}
}

TASK(reimu_dream_controller_tick, { ReimuBController *ctrl; }) {
	ReimuBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;

	for(;;) {
		if(player_is_bomb_active(plr)) {
			stage_shake_view(1);
			approach_p(&ctrl->bomb_alpha, 1.0, 0.1);
		} else {
			approach_p(&ctrl->bomb_alpha, 0.0, 0.025);
		}

		YIELD;
	}
}

TASK(reimu_dream_controller, { BoxedPlayer plr; }) {
	ReimuBController *ctrl = TASK_HOST_ENT(ReimuBController);
	ctrl->plr = TASK_BIND(ARGS.plr);
	TASK_HOST_EVENTS(ctrl->events);

	ctrl->yinyang_shader = res_shader("sprite_yinyang");
	ctrl->yinyang_sprite = res_sprite("yinyang");

	ctrl->ent.draw_func = reimu_dream_draw_gaps;
	ctrl->ent.draw_layer = LAYER_PLAYER_FOCUS;

	ctrl->gaps.left.link = &ctrl->gaps.top;
	ctrl->gaps.top.link = &ctrl->gaps.left;
	ctrl->gaps.right.link = &ctrl->gaps.bottom;
	ctrl->gaps.bottom.link = &ctrl->gaps.right;

	ctrl->gaps.left.orientation = -1;
	ctrl->gaps.right.orientation = 1;
	ctrl->gaps.top.orientation = -I;
	ctrl->gaps.bottom.orientation = I;

	ctrl->gaps.left.parallel_axis = ctrl->gaps.right.parallel_axis = I;
	ctrl->gaps.top.parallel_axis = ctrl->gaps.bottom.parallel_axis = 1;

	INVOKE_SUBTASK(reimu_dream_controller_tick, ctrl);
	INVOKE_SUBTASK(reimu_dream_process_gaps, ctrl);
	INVOKE_SUBTASK(reimu_dream_shot_forward, ctrl);
	INVOKE_SUBTASK(reimu_dream_power_handler, ctrl);
	INVOKE_SUBTASK(reimu_dream_bomb_handler, ctrl);

	STALL;
}

static void reimu_dream_init(Player *plr) {
	reimu_common_bomb_buffer_init();
	INVOKE_TASK(reimu_dream_controller, ENT_BOX(plr));
}

static void reimu_dream_preload(ResourceGroup *rg) {
	const int flags = RESF_DEFAULT;

	res_group_preload(rg, RES_SPRITE, flags,
		"yinyang",
		"proj/ofuda",
		"proj/needle2",
		"proj/glowball",
		"part/myon",
		"part/stardust",
	NULL);

	res_group_preload(rg, RES_TEXTURE, flags,
		"runes",
		"gaplight",
	NULL);

	res_group_preload(rg, RES_SHADER_PROGRAM, flags,
		"sprite_yinyang",
		"reimu_gap",
		"reimu_gap_light",
		"reimu_bomb_bg",
	NULL);

	res_group_preload(rg, RES_SFX, flags | RESF_OPTIONAL,
		"bomb_marisa_a",
		"boon",
	NULL);
}

PlayerMode plrmode_reimu_b = {
	.name = "Dream Shaper",
	.description = "Turn your understanding of reality upside-down, and tie the boundaries of spacetime into knots as easily as the ribbons in your hair.",
	.spellcard_name = "Dream Sign “Ethereal Boundary Rupture”",
	.character = &character_reimu,
	.dialog = &dialog_tasks_reimu,
	.shot_mode = PLR_SHOT_REIMU_DREAM,
	.procs = {
		.property = reimu_common_property,
		.init = reimu_dream_init,
		.preload = reimu_dream_preload,
	},
};
