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

#include "audio/audio.h"
#include "common_tasks.h"
#include "dialog/marisa.h"
#include "global.h"
#include "plrmodes.h"
#include "renderer/api.h"
#include "stagedraw.h"
#include "util/graphics.h"

#define SHOT_FORWARD_DAMAGE 60
#define SHOT_FORWARD_DELAY 6
#define SHOT_LASER_DAMAGE 13

#define HAKKERO_RETRACT_TIME 4

typedef struct MarisaALaser MarisaALaser;

DEFINE_ENTITY_TYPE(MarisaASlave, {
	Sprite *sprite;
	ShaderProgram *shader;
	cmplx pos;
	real lean;
	real shot_angle;
	real flare_alpha;
	uint alive;
});

DEFINE_ENTITY_TYPE(MarisaAMasterSpark, {
	cmplx pos;
	cmplx dir;
	real alpha;
});

struct MarisaALaser {
	LIST_INTERFACE(MarisaALaser);
	cmplx pos;
	struct {
		cmplx first;
		cmplx last;
	} trace_hit;
	real alpha;
};

DEFINE_ENTITY_TYPE(MarisaAController, {
	Player *plr;
	LIST_ANCHOR(MarisaALaser) lasers;

	COEVENTS_ARRAY(
		slaves_expired
	) events;
});

static void trace_laser(MarisaALaser *laser, cmplx vel, real damage) {
	ProjCollisionResult col;
	ProjectileList lproj = { .first = NULL };

	PROJECTILE(
		.dest = &lproj,
		.pos = laser->pos,
		.size = 28*(1+I),
		.type = PROJ_PLAYER,
		.damage = damage,
		.flags = PFLAG_NOSPAWNFLARE,
		.move = move_linear(vel),
	);

	bool first_found = false;
	int timeofs = 0;
	int col_types = PCOL_ENTITY;

	struct enemy_col {
		Enemy *enemy;
		EnemyFlag original_flags;
	} enemy_collisions[64] = { 0 };  // 64 collisions ought to be enough for everyone

	int num_enemy_collissions = 0;

	while(lproj.first) {
		timeofs = trace_projectile(lproj.first, &col, col_types | PCOL_VOID, timeofs);
		struct enemy_col *c = NULL;

		if(col.type & col_types) {
			RNG_ARRAY(R, 3);
			PARTICLE(
				.sprite = "flare",
				.pos = col.location,
				.move = move_linear(vrng_range(R[1], 2, 8) * vrng_dir(R[2])),
				.timeout = vrng_range(R[0], 3, 8),
				.draw_rule = pdraw_timeout_scale(2, 0.01),
				.flags = PFLAG_NOREFLECT,
				.layer = LAYER_PARTICLE_HIGH,
			);

			col.fatal = false;

			if(col.type == PCOL_ENTITY && col.entity->type == ENT_TYPE_ID(Enemy)) {
				assert(num_enemy_collissions < ARRAY_SIZE(enemy_collisions));
				if(num_enemy_collissions < ARRAY_SIZE(enemy_collisions)) {
					Enemy *e = ENT_CAST(col.entity, Enemy);
					c = enemy_collisions + num_enemy_collissions++;
					c->enemy = e;
					if(e->flags & EFLAG_IMPENETRABLE) {
						col.fatal = true;
					}
				}
			} else {
				col_types &= ~col.type;
			}
		}

		apply_projectile_collision(&lproj, lproj.first, &col);

		if(c) {
			c->original_flags = (ENT_CAST(col.entity, Enemy))->flags;
			(ENT_CAST(col.entity, Enemy))->flags |= EFLAG_NO_HIT;
		}

		if(!first_found) {
			if(lproj.first) {
				lproj.first->damage *= 0.5;
			}

			laser->trace_hit.first = col.location;
			first_found = true;
		}
	}

	assume(num_enemy_collissions < ARRAY_SIZE(enemy_collisions));

	for(int i = 0; i < num_enemy_collissions; ++i) {
		enemy_collisions[i].enemy->flags = enemy_collisions[i].original_flags;
	}

	laser->trace_hit.last = col.location;
}

static float set_alpha(Uniform *u_alpha, float a) {
	if(a) {
		r_uniform_float(u_alpha, a);
	}

	return a;
}

static float set_alpha_dimmed(Uniform *u_alpha, float a) {
	return set_alpha(u_alpha, a * a * 0.5);
}

static void marisa_laser_draw_slave(EntityInterface *ent) {
	MarisaASlave *slave = ENT_CAST(ent, MarisaASlave);

	ShaderCustomParams shader_params;
	shader_params.color = *RGBA(0.2, 0.4, 0.5, slave->flare_alpha * 0.75);
	float t = global.frames;

	r_draw_sprite(&(SpriteParams) {
		.sprite_ptr = slave->sprite,
		.shader_ptr = slave->shader,
		.pos.as_cmplx = slave->pos,
		.rotation.angle = t * 0.05f,
		.color = color_lerp(RGB(0.2, 0.4, 0.5), RGB(1.0, 1.0, 1.0), 0.25 * powf(psinf(t / 6.0f), 2.0f) * slave->flare_alpha),
		.shader_params = &shader_params,
	});
}

static void draw_laser_beam(cmplx src, cmplx dst, real size, real step, real t, Texture *tex, Uniform *u_length) {
	cmplx dir = dst - src;
	cmplx center = (src + dst) * 0.5;

	r_mat_mv_push();

	r_mat_mv_translate(re(center), im(center), 0);
	r_mat_mv_rotate(carg(dir), 0, 0, 1);
	r_mat_mv_scale(cabs(dir), size, 1);

	r_mat_tex_push_identity();
	r_mat_tex_translate(-im(src) / step + t, 0, 0);
	r_mat_tex_scale(cabs(dir) / step, 1, 1);

	r_uniform_sampler("tex", tex);
	r_uniform_float(u_length, cabs(dir) / step);
	r_draw_quad();

	r_mat_tex_pop();
	r_mat_mv_pop();
}

static void marisa_laser_draw_lasers(EntityInterface *ent) {
	MarisaAController *ctrl = ENT_CAST(ent, MarisaAController);
	real t = global.frames;

	ShaderProgram *shader = res_shader("marisa_laser");
	Uniform *u_clr0 = r_shader_uniform(shader, "color0");
	Uniform *u_clr1 = r_shader_uniform(shader, "color1");
	Uniform *u_clr_phase = r_shader_uniform(shader, "color_phase");
	Uniform *u_clr_freq = r_shader_uniform(shader, "color_freq");
	Uniform *u_alpha = r_shader_uniform(shader, "alphamod");
	Uniform *u_length = r_shader_uniform(shader, "laser_length");
	Texture *tex0 = res_texture("part/marisa_laser0");
	Texture *tex1 = res_texture("part/marisa_laser1");
	FBPair *fbp_aux = stage_get_fbpair(FBPAIR_FG_AUX);
	Framebuffer *target_fb = r_framebuffer_current();

	r_shader_ptr(shader);
	r_uniform_vec4(u_clr0,       0.5, 0.5, 0.5, 0.0);
	r_uniform_vec4(u_clr1,       0.8, 0.8, 0.8, 0.0);
	r_uniform_float(u_clr_phase, -1.5 * t/M_PI);
	r_uniform_float(u_clr_freq,  10.0);
	r_framebuffer(fbp_aux->back);
	r_clear(BUFFER_COLOR, RGBA(0, 0, 0, 0), 1);
	r_color4(1, 1, 1, 1);

	r_blend(r_blend_compose(
		BLENDFACTOR_SRC_COLOR, BLENDFACTOR_ONE, BLENDOP_MAX,
		BLENDFACTOR_SRC_COLOR, BLENDFACTOR_ONE, BLENDOP_MAX
	));

	for(MarisaALaser *laser = ctrl->lasers.first; laser; laser = laser->next) {
		if(set_alpha(u_alpha, laser->alpha)) {
			draw_laser_beam(laser->pos, laser->trace_hit.last, 32, 128, -0.02 * t, tex1, u_length);
		}
	}

	r_blend(BLEND_PREMUL_ALPHA);
	fbpair_swap(fbp_aux);

	stage_draw_begin_noshake();

	r_framebuffer(fbp_aux->back);
	r_clear(BUFFER_COLOR, RGBA(0, 0, 0, 0), 1);
	r_shader("max_to_alpha");
	draw_framebuffer_tex(fbp_aux->front, VIEWPORT_W, VIEWPORT_H);
	fbpair_swap(fbp_aux);

	r_framebuffer(target_fb);
	r_shader_standard();
	r_color4(1, 1, 1, 1);
	draw_framebuffer_tex(fbp_aux->front, VIEWPORT_W, VIEWPORT_H);
	r_shader_ptr(shader);

	stage_draw_end_noshake();

	r_uniform_vec4(u_clr0, 0.5, 0.0, 0.0, 0.0);
	r_uniform_vec4(u_clr1, 1.0, 0.0, 0.0, 0.0);

	for(MarisaALaser *laser = ctrl->lasers.first; laser; laser = laser->next) {
		if(set_alpha_dimmed(u_alpha, laser->alpha)) {
			draw_laser_beam(laser->pos, laser->trace_hit.first, 40, 128, t * -0.12, tex0, u_length);
		}
	}

	r_uniform_vec4(u_clr0, 2.0, 1.0, 2.0, 0.0);
	r_uniform_vec4(u_clr1, 0.1, 0.1, 1.0, 0.0);

	for(MarisaALaser *laser = ctrl->lasers.first; laser; laser = laser->next) {
		if(set_alpha_dimmed(u_alpha, laser->alpha)) {
			draw_laser_beam(laser->pos, laser->trace_hit.first, 42, 200, t * -0.03, tex0, u_length);
		}
	}

	SpriteParams sp = {
		.sprite_ptr = res_sprite("part/smoothdot"),
		.shader_ptr = res_shader("sprite_default"),
	};

	for(MarisaALaser *laser = ctrl->lasers.first; laser; laser = laser->next) {
		float a = laser->alpha * 0.8f;
		cmplx spread;

		sp.pos.as_cmplx = laser->trace_hit.first;

		spread = rng_dir(); spread *= rng_range(0, 3);
		sp.pos.as_cmplx += spread;
		sp.color = color_mul_scalar(RGBA(0.8, 0.3, 0.5, 0), a);
		r_draw_sprite(&sp);

		spread = rng_dir(); spread *= rng_range(0, 3);
		sp.pos.as_cmplx += spread;
		sp.color = color_mul_scalar(RGBA(0.2, 0.7, 0.5, 0), a);
		r_draw_sprite(&sp);
	}
}

static void marisa_laser_flash_draw(Projectile *p, int t, ProjDrawRuleArgs args) {
	SpriteParamsBuffer spbuf;
	SpriteParams sp = projectile_sprite_params(p, &spbuf);
	float o = 1 - t / p->timeout;
	color_mul_scalar(&spbuf.color, o);
	spbuf.color.r *= o;
	r_draw_sprite(&sp);
}

static void marisa_laser_show_laser(MarisaAController *ctrl, MarisaALaser *laser) {
	alist_append(&ctrl->lasers, laser);
}

static void marisa_laser_hide_laser(MarisaAController *ctrl, MarisaALaser *laser) {
	alist_unlink(&ctrl->lasers, laser);
}

TASK(marisa_laser_fader, {
	MarisaAController *ctrl;
	const MarisaALaser *ref_laser;
}) {
	MarisaAController *ctrl = ARGS.ctrl;
	MarisaALaser fader = *ARGS.ref_laser;

	marisa_laser_show_laser(ctrl, &fader);

	while(fader.alpha > 0) {
		YIELD;
		approach_p(&fader.alpha, 0, 0.1);
	}

	marisa_laser_hide_laser(ctrl, &fader);
}

static void marisa_laser_fade_laser(MarisaAController *ctrl, MarisaALaser *laser) {
	marisa_laser_hide_laser(ctrl, laser);
	INVOKE_TASK(marisa_laser_fader, ctrl, laser);
}

TASK(marisa_laser_slave_shot_cleanup, {
	MarisaAController *ctrl;
	MarisaALaser **active_laser;
}) {
	MarisaALaser *active_laser = *ARGS.active_laser;
	if(active_laser) {
		marisa_laser_fade_laser(ARGS.ctrl, active_laser);
	}
}

TASK(marisa_laser_slave_shot, {
	MarisaAController *ctrl;
	BoxedMarisaASlave slave;
}) {
	MarisaAController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;
	MarisaASlave *slave = TASK_BIND(ARGS.slave);

	Animation *fire_anim = res_anim("fire");
	AniSequence *fire_anim_seq = get_ani_sequence(fire_anim, "main");

	MarisaALaser *active_laser = NULL;
	INVOKE_TASK_AFTER(&TASK_EVENTS(THIS_TASK)->finished, marisa_laser_slave_shot_cleanup, ctrl, &active_laser);

	for(;;) {
		WAIT_EVENT_OR_DIE(&plr->events.shoot);
		assert(player_should_shoot(plr));

		// We started shooting - register a laser for rendering

		MarisaALaser laser = { 0 };
		marisa_laser_show_laser(ctrl, &laser);
		active_laser = &laser;

		while(player_should_shoot(plr)) {
			real angle = slave->shot_angle * (1.0 + 0.7 * psin(global.frames/15.0));

			laser.pos = slave->pos;
			approach_p(&laser.alpha, 1.0, 0.2);

			cmplx dir = -cdir(angle + slave->lean + M_PI/2);
			trace_laser(&laser, 5 * dir, SHOT_LASER_DAMAGE);

			Sprite *spr = animation_get_frame(fire_anim, fire_anim_seq, global.frames);

			PARTICLE(
				.sprite_ptr = spr,
				.size = 1+I,
				.pos = laser.pos + dir * 10,
				.color = color_mul_scalar(RGBA(2, 0.2, 0.5, 0), 0.2),
				.draw_rule = marisa_laser_flash_draw,
				.move = move_linear(dir),
				.timeout = 8,
				.flags = PFLAG_NOREFLECT | PFLAG_REQUIREDPARTICLE,
				.scale = 0.4,
			);

			YIELD;
		}

		// We stopped shooting - remove the laser, spawn fader

		marisa_laser_fade_laser(ctrl, &laser);
		active_laser = NULL;
	}
}

TASK(marisa_laser_slave, {
	MarisaAController *ctrl;
	cmplx unfocused_offset;
	cmplx focused_offset;
	real shot_angle;
}) {
	MarisaAController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;
	MarisaASlave *slave = TASK_HOST_ENT(MarisaASlave);
	slave->alive = 1;
	slave->ent.draw_func = marisa_laser_draw_slave;
	slave->ent.draw_layer = LAYER_PLAYER_SLAVE;
	slave->pos = plr->pos;
	slave->shader = res_shader("sprite_hakkero");
	slave->sprite = res_sprite("hakkero");

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

	BoxedTask shot_task = cotask_box(INVOKE_SUBTASK(marisa_laser_slave_shot,
		.ctrl = ctrl,
		.slave = ENT_BOX(slave)
	));

	/*                     unfocused              focused              */
	cmplx offsets[]    = { ARGS.unfocused_offset, ARGS.focused_offset, };
	real shot_angles[] = { ARGS.shot_angle,       0                    };

	real formation_switch_rate = 0.3;
	real lean_rate = 0.1;
	real follow_rate = 0.5;
	real lean_strength = 0.01;
	real epsilon = 1e-5;

	cmplx offset = 0;
	cmplx prev_pos = slave->pos;

	while(slave->alive) {
		bool focused = plr->inputflags & INFLAG_FOCUS;

		approach_asymptotic_p(&slave->shot_angle, shot_angles[focused], formation_switch_rate, epsilon);
		capproach_asymptotic_p(&offset, offsets[focused], formation_switch_rate, epsilon);

		capproach_asymptotic_p(&slave->pos, plr->pos + offset, follow_rate, epsilon);
		approach_asymptotic_p(&slave->lean, -lean_strength * re(slave->pos - prev_pos), lean_rate, epsilon);
		prev_pos = slave->pos;

		if(player_should_shoot(plr)) {
			approach_asymptotic_p(&slave->flare_alpha, 1.0, 0.2, epsilon);
		} else {
			approach_asymptotic_p(&slave->flare_alpha, 0.0, 0.08, epsilon);
		}

		YIELD;
	}

	CANCEL_TASK(shot_task);
	plrutil_slave_retract(ENT_BOX(plr), &slave->pos, HAKKERO_RETRACT_TIME);
}

static real marisa_laser_masterspark_width(real progress) {
	real w = 1;

	if(progress < 1./6) {
		w = progress * 6;
		w = pow(w, 1.0/3.0);
	}

	if(progress > 4./5) {
		w = 1 - progress * 5 + 4;
		w = pow(w, 5);
	}

	return w;
}

static void marisa_laser_draw_masterspark(EntityInterface *ent) {
	MarisaAMasterSpark *ms = ENT_CAST(ent, MarisaAMasterSpark);
	marisa_common_masterspark_draw(1, &(MarisaBeamInfo) {
		ms->pos,
		800 + I * VIEWPORT_H * 1.25,
		carg(ms->dir),
		global.frames
	}, ms->alpha);
}

static void marisa_laser_masterspark_damage(MarisaAMasterSpark *ms) {
	// lazy inefficient approximation of the beam parabola

	float r = 96 * ms->alpha;
	float growth = 0.25;
	cmplx v = ms->dir * cdir(M_PI * -0.5);
	cmplx p = ms->pos + v * r;

	Rect vp_rect, seg_rect;
	vp_rect.top_left = 0;
	vp_rect.bottom_right = CMPLX(VIEWPORT_W, VIEWPORT_H);

	int iter = 0;
	int maxiter = 10;

	do {
		stage_clear_hazards_at(p, r, CLEAR_HAZARDS_ALL | CLEAR_HAZARDS_NOW);
		ent_area_damage(p, r, &(DamageInfo) { 80, DMG_PLAYER_BOMB }, NULL, NULL);

		p += v * r;
		r *= 1 + growth;
		growth *= 0.75;

		cmplx o = (1 + I);
		seg_rect.top_left = p - o * r;
		seg_rect.bottom_right = p + o * r;

		++iter;
	} while(rect_rect_intersect(seg_rect, vp_rect, true, true) && iter < maxiter);
	// log_debug("%i", iter);
}

TASK(marisa_laser_bomb_background, { MarisaAController *ctrl; }) {
	MarisaAController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;
	CoEvent *draw_event = &stage_get_draw_events()->background_drawn;

	for(;;) {
		WAIT_EVENT_OR_DIE(draw_event);
		float t = player_get_bomb_progress(plr);
		float fade = 1;

		if(t < 1.0f / 6.0f) {
			fade = t * 6.0f;
		}

		if(t > 3.0f / 4.0f) {
			fade = 1.0f - t * 4.0f + 3.0f;
		}

		if(fade <= 0.0f) {
			break;
		}

		r_state_push();
		r_color4(0.8f * fade, 0.8f * fade, 0.8f * fade, 0.8f * fade);
		fill_viewport(sinf(t * 0.3f), t * 3.0f * (1.0f + t * 3.0f), 1.0f, "marisa_bombbg");
		r_state_pop();
	};
}

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

	MarisaAMasterSpark *ms = TASK_HOST_ENT(MarisaAMasterSpark);
	ms->dir = 1;
	ms->ent.draw_func = marisa_laser_draw_masterspark;
	ms->ent.draw_layer = LAYER_PLAYER_FOCUS - 1;

	int t = 0;

	Sprite *star_spr = res_sprite("part/maristar_orbit");
	Sprite *smoke_spr = res_sprite("part/smoke");

	BoxedTask bg_task = cotask_box(INVOKE_SUBTASK(marisa_laser_bomb_background, ctrl));

	YIELD;

	do {
		real bomb_progress = player_get_bomb_progress(plr);
		ms->alpha = marisa_laser_masterspark_width(bomb_progress);
		ms->dir *= cdir(0.005 * (re(plr->velocity) + 2 * rng_sreal()));
		ms->pos = plr->pos - 30 * I;

		marisa_laser_masterspark_damage(ms);

		if(bomb_progress >= 3.0/4.0) {
			stage_shake_view(8 * (1 - bomb_progress * 4 + 3));
			goto skip_particles;
		}

		stage_shake_view(8);

		uint pflags = PFLAG_NOREFLECT | PFLAG_MANUALANGLE;

		if(t % 4 == 0) {
			pflags |= PFLAG_REQUIREDPARTICLE;
		}

		cmplx dir = -cdir(1.5 * sin(t * M_PI * 1.12)) * I;
		Color *c = HSLA(-bomb_progress * 5.321, 1, 0.5, rng_range(0, 0.5));
		cmplx pos = plr->pos + 40 * dir;

		for(int i = 0; i < 2; ++i) {
			cmplx v = 10 * (dir - I);
			PARTICLE(
				.angle = rng_angle(),
				.angle_delta = 0.1,
				.color = c,
				.draw_rule = pdraw_timeout_scalefade(0, 5, 1, 0),
				.flags = pflags,
				.move = move_accelerated(v, 0.1 * cnormalize(v)),
				.pos = pos,
				.sprite_ptr = star_spr,
				.timeout = 50,
			);
			dir = -conj(dir);
		}

		PARTICLE(
			.sprite_ptr = smoke_spr,
			.pos = plr->pos - 40*I,
			.color = HSLA(2 * bomb_progress, 1, 2, 0),
			.timeout = 50,
			.move = move_linear(7 * (I - dir)),
			.angle = rng_angle(),
			.draw_rule = pdraw_timeout_scalefade(0, 7, 1, 0),
			.flags = pflags,
		);

skip_particles:
		++t;
		YIELD;
	} while(player_is_bomb_active(plr));

	// should have faded out by now, but just in case…
	CANCEL_TASK(bg_task);

	while(ms->alpha > 0) {
		approach_p(&ms->alpha, 0, 0.2);
		YIELD;
	}
}

TASK(marisa_laser_bomb_handler, { MarisaAController *ctrl; }) {
	MarisaAController *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(marisa_laser_bomb_masterspark, ctrl));
	}
}

static void marisa_laser_respawn_slaves(MarisaAController *ctrl, int power_rank) {
	coevent_signal(&ctrl->events.slaves_expired);

	if(power_rank == 1) {
		INVOKE_TASK(marisa_laser_slave, ctrl, -40.0*I, -40.0*I, 0);
	}

	if(power_rank >= 2) {
		INVOKE_TASK(marisa_laser_slave, ctrl,  25-5.0*I,  9-40.0*I,  M_PI/30);
		INVOKE_TASK(marisa_laser_slave, ctrl, -25-5.0*I, -9-40.0*I, -M_PI/30);
	}

	if(power_rank == 3) {
		INVOKE_TASK(marisa_laser_slave, ctrl, -30.0*I, -55.0*I, 0);
	}

	if(power_rank >= 4) {
		INVOKE_TASK(marisa_laser_slave, ctrl,  17-30.0*I,  18-55.0*I,  M_PI/60);
		INVOKE_TASK(marisa_laser_slave, ctrl, -17-30.0*I, -18-55.0*I, -M_PI/60);
	}
}

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

	marisa_laser_respawn_slaves(ctrl, old_power);

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

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

	ctrl->ent.draw_func = marisa_laser_draw_lasers;
	ctrl->ent.draw_layer = LAYER_PLAYER_SHOT_HIGH;

	INVOKE_SUBTASK(marisa_laser_power_handler, ctrl);
	INVOKE_SUBTASK(marisa_laser_bomb_handler, ctrl);
	INVOKE_SUBTASK(marisa_common_shot_forward, ARGS.plr, SHOT_FORWARD_DAMAGE, SHOT_FORWARD_DELAY);

	STALL;
}

static void marisa_laser_init(Player *plr) {
	INVOKE_TASK(marisa_laser_controller, ENT_BOX(plr));
}

static double marisa_laser_property(Player *plr, PlrProperty prop) {
	switch(prop) {
		case PLR_PROP_SPEED: {
			double s = marisa_common_property(plr, prop);

			if(player_is_bomb_active(plr)) {
				s /= 5.0;
			}

			return s;
		}

		default:
			return marisa_common_property(plr, prop);
	}
}

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

	res_group_preload(rg, RES_SPRITE, flags,
		"proj/marisa",
		"part/maristar_orbit",
		"hakkero",
		"masterspark_ring",
	NULL);

	res_group_preload(rg, RES_TEXTURE, flags,
		"marisa_bombbg",
		"part/marisa_laser0",
		"part/marisa_laser1",
	NULL);

	res_group_preload(rg, RES_SHADER_PROGRAM, flags,
		"blur25",
		"blur5",
		"marisa_laser",
		"masterspark",
		"max_to_alpha",
		"sprite_hakkero",
	NULL);

	res_group_preload(rg, RES_ANIM, flags,
		"fire",
	NULL);

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

PlayerMode plrmode_marisa_a = {
	.name = "Illusion Laser",
	.description = "Magic missiles and lasers — simple and to the point. They’ve never let you down before, so why stop now?",
	.spellcard_name = "Pure Love “Galactic Spark”",
	.character = &character_marisa,
	.dialog = &dialog_tasks_marisa,
	.shot_mode = PLR_SHOT_MARISA_LASER,
	.procs = {
		.init = marisa_laser_init,
		.preload = marisa_laser_preload,
		.property = marisa_laser_property,
	},
};
