907 lines
24 KiB
C
907 lines
24 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 "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 50
|
||
#define SHOT_FORWARD_DELAY 3
|
||
#define SHOT_VOLLEY_DMG 35
|
||
#define SHOT_VOLLEY_DELAY 16
|
||
#define SHOT_SLAVE_HOMING_DMG 175
|
||
#define SHOT_SLAVE_HOMING_DELAY 21
|
||
#define SHOT_SLAVE_NEEDLE_DMG 64
|
||
#define SHOT_SLAVE_NEEDLE_DELAY 3
|
||
|
||
#define ORB_RETRACT_TIME 4
|
||
|
||
typedef struct ReimuAController {
|
||
Player *plr;
|
||
int last_homing_fire_time;
|
||
int last_needle_fire_time;
|
||
COEVENTS_ARRAY(
|
||
slaves_expired
|
||
) events;
|
||
} ReimuAController;
|
||
|
||
DEFINE_ENTITY_TYPE(ReimuASlave, {
|
||
Sprite *sprite;
|
||
ShaderProgram *shader;
|
||
cmplx pos;
|
||
Color color;
|
||
uint alive;
|
||
});
|
||
|
||
static void reimu_spirit_preload(ResourceGroup *rg) {
|
||
const int flags = RESF_DEFAULT;
|
||
|
||
res_group_preload(rg, RES_SPRITE, flags,
|
||
"yinyang",
|
||
"proj/ofuda",
|
||
"proj/needle",
|
||
"proj/glowball",
|
||
"proj/hakurei_seal",
|
||
"part/ofuda_glow",
|
||
"part/fantasyseal_impact",
|
||
NULL);
|
||
|
||
res_group_preload(rg, RES_TEXTURE, flags,
|
||
"runes",
|
||
NULL);
|
||
|
||
res_group_preload(rg, RES_SHADER_PROGRAM, flags,
|
||
"sprite_yinyang",
|
||
"reimu_bomb_bg",
|
||
NULL);
|
||
|
||
res_group_preload(rg, RES_SFX, flags | RESF_OPTIONAL,
|
||
"boom",
|
||
"bomb_reimu_a",
|
||
"bomb_marisa_b",
|
||
NULL);
|
||
}
|
||
|
||
TASK(reimu_spirit_needle, { cmplx pos; cmplx vel; real damage; ShaderProgram *shader; }) {
|
||
Projectile *p = TASK_BIND(PROJECTILE(
|
||
.proto = pp_needle,
|
||
.pos = ARGS.pos,
|
||
.color = RGBA(0.5, 0.5, 0.5, 0.5),
|
||
.move = move_linear(ARGS.vel),
|
||
.type = PROJ_PLAYER,
|
||
.damage_type = DMG_PLAYER_SHOT,
|
||
.damage = ARGS.damage,
|
||
.shader_ptr = ARGS.shader,
|
||
));
|
||
|
||
Color trail_color = p->color;
|
||
color_mul(&trail_color, RGBA_MUL_ALPHA(0.75, 0.5, 1, 0.5));
|
||
color_mul_scalar(&trail_color, 0.6);
|
||
trail_color .a = 0;
|
||
|
||
MoveParams trail_move = move_linear(p->move.velocity * 0.8);
|
||
ProjDrawRule trail_draw = pdraw_timeout_scalefade(0, 2, 1, 0);
|
||
|
||
for(;;) {
|
||
YIELD;
|
||
|
||
PARTICLE(
|
||
.sprite_ptr = p->sprite,
|
||
.color = &trail_color,
|
||
.timeout = 12,
|
||
.pos = p->pos,
|
||
.move = trail_move,
|
||
.draw_rule = trail_draw,
|
||
.layer = LAYER_PARTICLE_LOW,
|
||
.flags = PFLAG_NOREFLECT,
|
||
);
|
||
}
|
||
}
|
||
|
||
#define REIMU_SPIRIT_HOMING_SCALE 0.75
|
||
|
||
static Projectile *reimu_spirit_spawn_ofuda_particle(Projectile *p, int t, real vfactor) {
|
||
Color *c = HSLA_MUL_ALPHA(t * 0.1, 0.6, 0.7, 0.3);
|
||
c->a = 0;
|
||
|
||
return PARTICLE(
|
||
.sprite = "ofuda_glow",
|
||
// .color = rgba(0.5 + 0.5 + psin(global.frames * 0.75), psin(t*0.5), 1, 0.5),
|
||
.color = c,
|
||
.timeout = 12,
|
||
.pos = p->pos,
|
||
.angle = p->angle,
|
||
.move = move_linear(p->move.velocity * rng_range(0.6, 1.0) * vfactor),
|
||
.draw_rule = pdraw_timeout_scalefade(1, 1.5, 1, 0),
|
||
.layer = LAYER_PARTICLE_LOW,
|
||
.flags = PFLAG_NOREFLECT | PFLAG_REQUIREDPARTICLE | PFLAG_MANUALANGLE,
|
||
.scale = REIMU_SPIRIT_HOMING_SCALE,
|
||
);
|
||
}
|
||
|
||
TASK(reimu_spirit_homing_impact, { BoxedProjectile p; }) {
|
||
Projectile *ref = NOT_NULL(ENT_UNBOX(ARGS.p));
|
||
|
||
Projectile *p = TASK_BIND(PARTICLE(
|
||
.proto = ref->proto,
|
||
.color = &ref->color,
|
||
.timeout = 32,
|
||
.pos = ref->pos,
|
||
.angle = ref->angle,
|
||
.angle_delta = 0.2,
|
||
.draw_rule = pdraw_timeout_scalefade(1, 1.5, 1, 0),
|
||
.layer = LAYER_PARTICLE_HIGH,
|
||
.flags = PFLAG_NOREFLECT | PFLAG_REQUIREDPARTICLE | PFLAG_MANUALANGLE,
|
||
.scale = REIMU_SPIRIT_HOMING_SCALE,
|
||
));
|
||
|
||
for(int t = global.frames - ref->birthtime;; ++t) {
|
||
Projectile *trail = reimu_spirit_spawn_ofuda_particle(p, t, 0);
|
||
trail->timeout = 6;
|
||
trail->angle = p->angle;
|
||
trail->ent.draw_layer = LAYER_PLAYER_FOCUS - 1; // TODO: add a layer for "super high" particles?
|
||
YIELD;
|
||
}
|
||
}
|
||
|
||
static inline real reimu_spirit_homing_aimfactor(real t, real maxt) {
|
||
real q = pow(t / maxt, 3);
|
||
return 4 * q * (1 - q);
|
||
}
|
||
|
||
TASK(reimu_spirit_homing, { cmplx pos; cmplx vel; real damage; ShaderProgram *shader; }) {
|
||
Projectile *p = TASK_BIND(PROJECTILE(
|
||
.proto = pp_ofuda,
|
||
.pos = ARGS.pos,
|
||
.color = RGBA(0.7, 0.63, 0.665, 0.7),
|
||
.move = move_linear(ARGS.vel),
|
||
.type = PROJ_PLAYER,
|
||
.damage_type = DMG_PLAYER_SHOT,
|
||
.damage = ARGS.damage,
|
||
.shader_ptr = ARGS.shader,
|
||
.scale = REIMU_SPIRIT_HOMING_SCALE,
|
||
.flags = PFLAG_NOCOLLISIONEFFECT | PFLAG_NOAUTOREMOVE,
|
||
));
|
||
|
||
INVOKE_TASK_WHEN(&p->events.killed, reimu_spirit_homing_impact, ENT_BOX(p));
|
||
|
||
cmplx target = p->pos + p->move.velocity * hypot(VIEWPORT_W, VIEWPORT_H);
|
||
real speed = cabs(p->move.velocity);
|
||
real homing_time = 60;
|
||
|
||
for(real t = 0; t < homing_time; ++t) {
|
||
target = plrutil_homing_target(p->pos, target);
|
||
cmplx aim = cnormalize(target - p->pos);
|
||
real s = speed * (0.5 + 0.5 * pow((t + 1) / homing_time, 2));
|
||
aim *= s * 0.25 * reimu_spirit_homing_aimfactor(t, homing_time);
|
||
p->move.velocity = s * cnormalize(p->move.velocity + aim);
|
||
reimu_spirit_spawn_ofuda_particle(p, t, 0.25);
|
||
YIELD;
|
||
}
|
||
|
||
p->flags &= ~PFLAG_NOAUTOREMOVE;
|
||
}
|
||
|
||
static Color *reimu_spirit_orb_color(Color *c, int i) {
|
||
*c = *RGBA((0.2 + (i==0))/1.2, (0.2 + (i==1))/1.2, (0.2 + 1.5*(i==2))/1.2, 0.0);
|
||
return c;
|
||
}
|
||
|
||
static void reimu_spirit_bomb_impact_balls(cmplx pos, int count) {
|
||
real offset = rng_real();
|
||
|
||
for(int i = 0; i < count; i++) {
|
||
PARTICLE(
|
||
.sprite_ptr = res_sprite("proj/glowball"),
|
||
.shader = "sprite_bullet",
|
||
.color = HSLA(3 * (float)i / count + offset, 1, 0.5, 0),
|
||
.timeout = 60,
|
||
.pos = pos,
|
||
.angle = rng_angle(),
|
||
.move = move_linear(cdir(2 * M_PI / count * (i + offset)) * 15),
|
||
.draw_rule = pdraw_timeout_fade(1, 0),
|
||
.layer = LAYER_BOSS,
|
||
.flags = PFLAG_NOREFLECT | PFLAG_REQUIREDPARTICLE | PFLAG_MANUALANGLE,
|
||
);
|
||
}
|
||
}
|
||
|
||
TASK(reimu_spirit_bomb_orb_impact, { BoxedProjectile orb; }) {
|
||
cmplx pos = NOT_NULL(ENT_UNBOX(ARGS.orb))->pos;
|
||
|
||
play_sfx("boom");
|
||
play_sfx("spellend");
|
||
|
||
stage_shake_view(200);
|
||
|
||
real damage = 2000;
|
||
real range = 300;
|
||
|
||
ent_area_damage(pos, range, &(DamageInfo){damage, DMG_PLAYER_BOMB}, NULL, NULL);
|
||
stage_clear_hazards_at(pos, range, CLEAR_HAZARDS_ALL | CLEAR_HAZARDS_NOW);
|
||
|
||
reimu_spirit_bomb_impact_balls(pos, 21);
|
||
|
||
int num_impacts = 3;
|
||
int t = global.frames;
|
||
BoxedProjectileArray impact_effects = ENT_ARRAY(Projectile, 3);
|
||
RNG_ARRAY(rand, num_impacts);
|
||
Color base_colors[3];
|
||
|
||
for(int i = 0; i < 3; ++i) {
|
||
base_colors[i] = *reimu_spirit_orb_color(&(Color){0}, i);
|
||
|
||
PARTICLE(
|
||
.sprite = "blast",
|
||
.color = color_mul_scalar(COLOR_COPY(&base_colors[i]), 2),
|
||
.pos = pos + 30 * cexp(I*2*M_PI/num_impacts*(i+t*0.1)),
|
||
.timeout = 40,
|
||
.draw_rule = pdraw_timeout_scalefade(0, 7.5, 1, 0),
|
||
.layer = LAYER_BOSS + 2,
|
||
.flags = PFLAG_NOREFLECT | PFLAG_REQUIREDPARTICLE,
|
||
);
|
||
|
||
ENT_ARRAY_ADD(&impact_effects, PARTICLE(
|
||
.sprite = "fantasyseal_impact",
|
||
.color = reimu_spirit_orb_color(&(Color){0}, i),
|
||
.pos = pos + 2 * cexp(I*2*M_PI/num_impacts*(i+t*0.1)),
|
||
.timeout = 120,
|
||
.layer = LAYER_BOSS + 1,
|
||
.angle = -M_PI/2,
|
||
.flags = PFLAG_NOREFLECT | PFLAG_REQUIREDPARTICLE | PFLAG_MANUALANGLE,
|
||
));
|
||
}
|
||
|
||
for(;;) {
|
||
int live = 0;
|
||
|
||
ENT_ARRAY_FOREACH_COUNTER(&impact_effects, int i, Projectile *p, {
|
||
float t = (global.frames - p->birthtime) / p->timeout;
|
||
float attack = min(1, vrng_f32_range(rand[i], 7, 12) * t);
|
||
float decay = t;
|
||
|
||
Color c = base_colors[i];
|
||
color_lerp(&c, RGBA(0.2, 0.1, 0, 1.0), decay);
|
||
color_mul_scalar(&c, powf(1.0f - decay, 2.0f) * 0.75f);
|
||
p->color = c;
|
||
p->scale = (0.75f + 0.25f / (powf(decay, 3.0f) + 1.0f)) + sqrtf(5.0f * (1.0f - attack));
|
||
|
||
++live;
|
||
});
|
||
|
||
if(!live) {
|
||
break;
|
||
}
|
||
|
||
YIELD;
|
||
}
|
||
}
|
||
|
||
TASK(reimu_spirit_bomb_orb_visual_kill, { BoxedProjectileArray components; }) {
|
||
ENT_ARRAY_FOREACH(&ARGS.components, Projectile *p, {
|
||
kill_projectile(p);
|
||
});
|
||
}
|
||
|
||
TASK(reimu_spirit_bomb_orb_visual, { BoxedProjectile orb; }) {
|
||
Projectile *orb = TASK_BIND(ARGS.orb);
|
||
DECLARE_ENT_ARRAY(Projectile, components, 3);
|
||
|
||
Sprite *glowball = res_sprite("proj/glowball");
|
||
ShaderProgram *shader = res_shader("sprite_bullet");
|
||
|
||
for(int i = 0; i < components.capacity; ++i) {
|
||
ENT_ARRAY_ADD(&components, PARTICLE(
|
||
.sprite_ptr = glowball,
|
||
.shader_ptr = shader,
|
||
.color = reimu_spirit_orb_color(&(Color){0}, i),
|
||
.opacity = 0.7,
|
||
.layer = LAYER_PLAYER_FOCUS - 1,
|
||
.flags = PFLAG_NOREFLECT | PFLAG_REQUIREDPARTICLE,
|
||
));
|
||
}
|
||
|
||
INVOKE_TASK_AFTER(&orb->events.killed, reimu_spirit_bomb_orb_visual_kill, components);
|
||
CANCEL_TASK_AFTER(&orb->events.killed, THIS_TASK);
|
||
|
||
for(;;) {
|
||
cmplx pos = orb->pos;
|
||
|
||
ENT_ARRAY_FOREACH_COUNTER(&components, int i, Projectile *p, {
|
||
real t = global.frames - p->birthtime;
|
||
cmplxf offset = (10 + pow(t, 0.5)) * cdir(2.0 * M_PI / 3*i + sqrt(1 + t * t / 300));
|
||
p->pos = pos + offset;
|
||
});
|
||
|
||
YIELD;
|
||
}
|
||
}
|
||
|
||
TASK(reimu_spirit_bomb_orb, { BoxedPlayer plr; int index; real angle; }) {
|
||
int index = ARGS.index;
|
||
|
||
Player *plr = NOT_NULL(ENT_UNBOX(ARGS.plr));
|
||
Projectile *orb = TASK_BIND(PROJECTILE(
|
||
.pos = plr->pos,
|
||
.timeout = 160 + 20 * index,
|
||
.type = PROJ_PLAYER,
|
||
.damage = 1000,
|
||
.damage_type = DMG_PLAYER_BOMB,
|
||
.size = 10 * (1+I),
|
||
.layer = LAYER_NODRAW,
|
||
.flags = PFLAG_NOREFLECT | PFLAG_NOCOLLISION | PFLAG_NOMOVE | PFLAG_MANUALANGLE | PFLAG_NOAUTOREMOVE,
|
||
));
|
||
|
||
BoxedProjectile b_orb = ENT_BOX(orb);
|
||
|
||
INVOKE_TASK(reimu_spirit_bomb_orb_visual, b_orb);
|
||
INVOKE_TASK_WHEN(&orb->events.killed, reimu_spirit_bomb_orb_impact, b_orb);
|
||
CANCEL_TASK_AFTER(&orb->events.killed, THIS_TASK);
|
||
|
||
int circletime = 60 + 20 * index;
|
||
cmplx target_homing = plr->pos;
|
||
cmplx dir = cdir(ARGS.angle);
|
||
cmplx vel = 0;
|
||
|
||
for(int t = 0;; ++t) {
|
||
if(!player_is_bomb_active(plr)) {
|
||
kill_projectile(orb);
|
||
return;
|
||
}
|
||
|
||
if(t == circletime) {
|
||
target_homing = global.plr.pos - 256*I;
|
||
orb->flags &= ~PFLAG_NOCOLLISION;
|
||
play_sfx("redirect");
|
||
}
|
||
|
||
cmplx target_circle = plr->pos + 10 * sqrt(t) * dir * (1 + 0.1 * sin(0.2 * t));
|
||
dir *= cdir(0.12);
|
||
|
||
real circlestrength = 1.0 / (1 + exp(t - circletime));
|
||
|
||
target_homing = plrutil_homing_target(orb->pos, target_homing);
|
||
cmplx homing = target_homing - orb->pos;
|
||
cmplx v = 0.3 * (circlestrength * (target_circle - orb->pos) + 0.2 * (1 - circlestrength) * (homing + 2*homing/(cabs(homing)+0.01)));
|
||
vel += (v - vel) * 0.1;
|
||
orb->pos += vel;
|
||
|
||
for(int i = 0; i < 3; i++) {
|
||
cmplx trail_pos = orb->pos + 10 * cdir(2*M_PI/3*(i+t*0.1));
|
||
cmplx trail_vel = global.plr.pos - trail_pos;
|
||
trail_vel *= 3 * circlestrength / cabs(trail_vel);
|
||
|
||
PARTICLE(
|
||
.sprite_ptr = res_sprite("part/stain"),
|
||
// .color = reimu_spirit_orb_color(&(Color){0}, i),
|
||
.color = HSLA(t/orb->timeout, 0.3, 0.3, 0.0),
|
||
.pos = trail_pos,
|
||
.angle = rng_angle(),
|
||
.angle_delta = -0.05,
|
||
.timeout = 30,
|
||
.draw_rule = pdraw_timeout_scalefade(0.4, 0, 1, 0),
|
||
.move = move_linear(trail_vel),
|
||
.flags = PFLAG_NOREFLECT | PFLAG_MANUALANGLE,
|
||
);
|
||
}
|
||
|
||
YIELD;
|
||
}
|
||
}
|
||
|
||
TASK(reimu_spirit_bomb_background, { ReimuAController *ctrl; }) {
|
||
ReimuAController *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);
|
||
|
||
if(t >= 1) {
|
||
break;
|
||
}
|
||
|
||
float alpha = 0.0f;
|
||
|
||
if(t > 0) {
|
||
alpha = min(1.0f, 10.0f * t);
|
||
}
|
||
|
||
if(t > 0.7) {
|
||
alpha *= 1.0f - powf((t - 0.7f) / 0.3f, 4.0f);
|
||
}
|
||
|
||
reimu_common_bomb_bg(plr, alpha);
|
||
colorfill(0.0f, 0.05f * alpha, 0.1f * alpha, alpha * 0.5f);
|
||
}
|
||
}
|
||
|
||
TASK(reimu_spirit_bomb, { ReimuAController *ctrl; }) {
|
||
ReimuAController *ctrl = ARGS.ctrl;
|
||
Player *plr = ctrl->plr;
|
||
|
||
INVOKE_SUBTASK(reimu_spirit_bomb_background, ctrl);
|
||
YIELD;
|
||
|
||
const int orb_count = 6;
|
||
for(int i = 0; i < orb_count; i++) {
|
||
INVOKE_TASK_DELAYED(1, reimu_spirit_bomb_orb, ENT_BOX(plr), i, M_TAU/orb_count*i);
|
||
}
|
||
|
||
// keep bg renderer alive
|
||
WAIT(plr->bomb_endtime - global.frames);
|
||
}
|
||
|
||
TASK(reimu_spirit_bomb_handler, { ReimuAController *ctrl; }) {
|
||
ReimuAController *ctrl = ARGS.ctrl;
|
||
Player *plr = ctrl->plr;
|
||
|
||
BoxedTask bomb_task = { 0 };
|
||
|
||
for(;;) {
|
||
WAIT_EVENT_OR_DIE(&plr->events.bomb_used);
|
||
play_sfx("bomb_reimu_a");
|
||
play_sfx("bomb_marisa_b");
|
||
CANCEL_TASK(bomb_task);
|
||
bomb_task = cotask_box(INVOKE_SUBTASK(reimu_spirit_bomb, ctrl));
|
||
}
|
||
}
|
||
|
||
TASK(reimu_spirit_ofuda, { cmplx pos; cmplx vel; real damage; ShaderProgram *shader; }) {
|
||
Projectile *ofuda = TASK_BIND(PROJECTILE(
|
||
.proto = pp_ofuda,
|
||
.pos = ARGS.pos,
|
||
.color = RGBA_MUL_ALPHA(1, 1, 1, 0.5),
|
||
.move = move_linear(ARGS.vel),
|
||
.type = PROJ_PLAYER,
|
||
.damage = ARGS.damage,
|
||
.shader_ptr = ARGS.shader,
|
||
));
|
||
|
||
for(;;YIELD) {
|
||
reimu_common_ofuda_swawn_trail(ofuda);
|
||
}
|
||
}
|
||
|
||
static void reimu_spirit_draw_slave(EntityInterface *ent) {
|
||
ReimuASlave *slave = ENT_CAST(ent, ReimuASlave);
|
||
r_draw_sprite(&(SpriteParams) {
|
||
.color = &slave->color,
|
||
.pos.as_cmplx = slave->pos,
|
||
.rotation.angle = global.frames * -6 * DEG2RAD,
|
||
.shader_ptr = slave->shader,
|
||
.sprite_ptr = slave->sprite,
|
||
});
|
||
}
|
||
|
||
static ReimuASlave *reimu_spirit_create_slave(ReimuAController *ctrl, const Color *color) {
|
||
Player *plr = ctrl->plr;
|
||
ReimuASlave *slave = TASK_HOST_ENT(ReimuASlave);
|
||
slave->sprite = res_sprite("yinyang");
|
||
slave->shader = res_shader("sprite_yinyang");
|
||
slave->pos = plr->pos;
|
||
slave->color = *color;
|
||
slave->ent.draw_layer = LAYER_PLAYER_SLAVE;
|
||
slave->ent.draw_func = reimu_spirit_draw_slave;
|
||
slave->alive = 1;
|
||
|
||
INVOKE_SUBTASK_WHEN(&ctrl->events.slaves_expired, common_set_bitflags,
|
||
.pflags = &slave->alive,
|
||
.mask = 0, .set = 0
|
||
);
|
||
|
||
return slave;
|
||
}
|
||
|
||
TASK(reimu_slave_shot_particles, {
|
||
BoxedReimuASlave slave;
|
||
int num;
|
||
cmplx dir;
|
||
Color *color0;
|
||
Color *color1;
|
||
Sprite *sprite;
|
||
}) {
|
||
ReimuASlave *slave = TASK_BIND(ARGS.slave);
|
||
Color color0 = *ARGS.color0;
|
||
Color color1 = *ARGS.color1;
|
||
int num = ARGS.num;
|
||
Sprite *sprite = ARGS.sprite;
|
||
ProjDrawRule draw = pdraw_timeout_scalefade(0.25, 0, 1, 0);
|
||
cmplx dir = ARGS.dir;
|
||
|
||
for(int i = 0; i < num; ++i) {
|
||
MoveParams move = move_linear(dir * rng_range(5, 10));
|
||
real timeout = rng_range(8, 10);
|
||
real angle = rng_angle();
|
||
|
||
PARTICLE(
|
||
.angle = angle,
|
||
.color = COLOR_COPY(color_lerp(&color0, &color1, rng_f32())),
|
||
.draw_rule = draw,
|
||
.flags = PFLAG_NOREFLECT | PFLAG_MANUALANGLE,
|
||
.move = move,
|
||
.pos = slave->pos + move.velocity * 0.5,
|
||
.sprite_ptr = sprite,
|
||
.timeout = timeout,
|
||
);
|
||
YIELD;
|
||
}
|
||
}
|
||
|
||
TASK(reimu_spirit_slave_needle_shot, {
|
||
ReimuAController *ctrl;
|
||
BoxedReimuASlave slave;
|
||
}) {
|
||
ReimuAController *ctrl = ARGS.ctrl;
|
||
Player *plr = ctrl->plr;
|
||
ReimuASlave *slave = TASK_BIND(ARGS.slave);
|
||
WAIT(max(0, SHOT_SLAVE_HOMING_DELAY - (global.frames - ctrl->last_needle_fire_time)));
|
||
|
||
ShaderProgram *shader = res_shader("sprite_particle");
|
||
Sprite *particle_spr = res_sprite("part/stain");
|
||
|
||
real damage = SHOT_SLAVE_NEEDLE_DMG;
|
||
real delay = SHOT_SLAVE_NEEDLE_DELAY;
|
||
|
||
for(;;) {
|
||
WAIT_EVENT_OR_DIE(&plr->events.shoot);
|
||
|
||
INVOKE_SUBTASK(reimu_slave_shot_particles,
|
||
.slave = ENT_BOX(slave),
|
||
.color0 = RGBA(0.5, 0, 0, 0),
|
||
.color1 = RGBA(0.5, 0.25, 0, 0),
|
||
.num = delay,
|
||
.dir = -I,
|
||
.sprite = particle_spr
|
||
);
|
||
|
||
INVOKE_TASK(reimu_spirit_needle,
|
||
.pos = slave->pos - 25.0*I,
|
||
.vel = -20*I,
|
||
.damage = damage,
|
||
.shader = shader
|
||
);
|
||
|
||
ctrl->last_needle_fire_time = global.frames;
|
||
ctrl->last_homing_fire_time = max(ctrl->last_homing_fire_time, global.frames - SHOT_SLAVE_HOMING_DELAY / 2);
|
||
|
||
WAIT(delay);
|
||
}
|
||
}
|
||
|
||
TASK(reimu_spirit_slave_needle, {
|
||
ReimuAController *ctrl;
|
||
real distance;
|
||
real rotation;
|
||
real rotation_offset;
|
||
}) {
|
||
ReimuAController *ctrl = ARGS.ctrl;
|
||
Player *plr = ctrl->plr;
|
||
ReimuASlave *slave = reimu_spirit_create_slave(ctrl, RGB(1.0, 0.8, 0.8));
|
||
|
||
INVOKE_SUBTASK(reimu_spirit_slave_needle_shot, ctrl, ENT_BOX(slave));
|
||
|
||
real dist = ARGS.distance;
|
||
cmplx offset = dist * cdir(ARGS.rotation_offset);
|
||
cmplx rot = cdir(ARGS.rotation);
|
||
|
||
real target_speed = 0.005 * dist;
|
||
MoveParams move = move_towards(0, plr->pos + offset, 0);
|
||
|
||
do {
|
||
move.attraction = approach(re(move.attraction), target_speed, target_speed / 12.0);
|
||
move.attraction_point = plr->pos + offset;
|
||
offset *= rot;
|
||
move_update(&slave->pos, &move);
|
||
YIELD;
|
||
} while(slave->alive);
|
||
|
||
plrutil_slave_retract(ENT_BOX(plr), &slave->pos, ORB_RETRACT_TIME);
|
||
}
|
||
|
||
TASK(reimu_spirit_slave_homing_shot, {
|
||
ReimuAController *ctrl;
|
||
BoxedReimuASlave slave;
|
||
cmplx vel;
|
||
}) {
|
||
ReimuAController *ctrl = ARGS.ctrl;
|
||
Player *plr = ctrl->plr;
|
||
ReimuASlave *slave = TASK_BIND(ARGS.slave);
|
||
WAIT(max(0, SHOT_SLAVE_HOMING_DELAY - (global.frames - ctrl->last_homing_fire_time)));
|
||
|
||
ShaderProgram *shader = res_shader("sprite_particle");
|
||
Sprite *particle_spr = res_sprite("part/stain");
|
||
cmplx vel = ARGS.vel;
|
||
cmplx pdir = cnormalize(vel);
|
||
|
||
real damage = SHOT_SLAVE_HOMING_DMG;
|
||
int delay = SHOT_SLAVE_HOMING_DELAY;
|
||
|
||
for(;;) {
|
||
WAIT_EVENT_OR_DIE(&plr->events.shoot);
|
||
|
||
INVOKE_SUBTASK(reimu_slave_shot_particles,
|
||
.slave = ENT_BOX(slave),
|
||
.color0 = RGBA(0.5, 0.125, 0, 0),
|
||
.color1 = RGBA(0.5, 0.125, 0.25, 0),
|
||
.num = delay,
|
||
.dir = pdir,
|
||
.sprite = particle_spr
|
||
);
|
||
|
||
INVOKE_TASK(reimu_spirit_homing,
|
||
.pos = slave->pos,
|
||
.vel = vel,
|
||
.damage = damage,
|
||
.shader = shader
|
||
);
|
||
|
||
ctrl->last_homing_fire_time = global.frames;
|
||
|
||
WAIT(delay);
|
||
}
|
||
}
|
||
|
||
TASK(reimu_spirit_slave_homing, {
|
||
ReimuAController *ctrl;
|
||
cmplx offset;
|
||
cmplx shot_vel;
|
||
}) {
|
||
ReimuAController *ctrl = ARGS.ctrl;
|
||
Player *plr = ctrl->plr;
|
||
ReimuASlave *slave = reimu_spirit_create_slave(ctrl, RGB(0.95, 0.75, 1.0));
|
||
|
||
INVOKE_SUBTASK(reimu_spirit_slave_homing_shot, ctrl, ENT_BOX(slave), ARGS.shot_vel);
|
||
|
||
cmplx offset = ARGS.offset;
|
||
real target_speed = 0.005 * cabs(offset);
|
||
MoveParams move = move_towards(0, plr->pos + offset, 0);
|
||
|
||
do {
|
||
move.attraction = approach(re(move.attraction), target_speed, target_speed / 12.0);
|
||
move.attraction_point = plr->pos + offset;
|
||
move_update(&slave->pos, &move);
|
||
YIELD;
|
||
} while(slave->alive);
|
||
|
||
plrutil_slave_retract(ENT_BOX(plr), &slave->pos, ORB_RETRACT_TIME);
|
||
}
|
||
|
||
static void reimu_spirit_spawn_slaves_unfocused(ReimuAController *ctrl, int power_rank) {
|
||
cmplx sv = -8 * I;
|
||
|
||
switch(power_rank) {
|
||
case 0:
|
||
break;
|
||
case 1:
|
||
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, 50*I, sv);
|
||
break;
|
||
case 2:
|
||
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, +40, sv * cdir(+M_PI/24));
|
||
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, -40, sv * cdir(-M_PI/24));
|
||
break;
|
||
case 3:
|
||
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, 50*I, sv);
|
||
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, +40, sv * cdir(+M_PI/24));
|
||
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, -40, sv * cdir(-M_PI/24));
|
||
break;
|
||
case 4:
|
||
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, +40, sv * cdir(+M_PI/24));
|
||
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, -40, sv * cdir(-M_PI/24));
|
||
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, +80, sv * cdir(+M_PI/16));
|
||
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, -80, sv * cdir(-M_PI/16));
|
||
break;
|
||
default:
|
||
UNREACHABLE;
|
||
}
|
||
}
|
||
|
||
static void reimu_spirit_spawn_slaves_focused(ReimuAController *ctrl, int power_rank) {
|
||
switch(power_rank) {
|
||
case 0:
|
||
break;
|
||
case 1:
|
||
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 52, 0.12, 0);
|
||
break;
|
||
case 2:
|
||
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 52, 0.12, 0);
|
||
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 52, 0.12, M_TAU/2);
|
||
break;
|
||
case 3:
|
||
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 52, 0.12, 0*M_TAU/3);
|
||
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 52, 0.12, 1*M_TAU/3);
|
||
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 52, 0.12, 2*M_TAU/3);
|
||
break;
|
||
case 4:
|
||
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 52, 0.12, 0);
|
||
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 52, 0.12, M_PI);
|
||
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 78, -0.06, 0);
|
||
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 78, -0.06, M_PI);
|
||
break;
|
||
default:
|
||
UNREACHABLE;
|
||
}
|
||
}
|
||
|
||
static void reimu_spirit_kill_slaves(ReimuAController *ctrl) {
|
||
coevent_signal(&ctrl->events.slaves_expired);
|
||
}
|
||
|
||
static void reimu_spirit_respawn_slaves(ReimuAController *ctrl) {
|
||
Player *plr = ctrl->plr;
|
||
int power_rank = player_get_effective_power(plr) / 100;
|
||
|
||
reimu_spirit_kill_slaves(ctrl);
|
||
|
||
if(plr->inputflags & INFLAG_FOCUS) {
|
||
reimu_spirit_spawn_slaves_focused(ctrl, power_rank);
|
||
} else {
|
||
reimu_spirit_spawn_slaves_unfocused(ctrl, power_rank);
|
||
}
|
||
}
|
||
|
||
TASK(reimu_spirit_focus_handler, { ReimuAController *ctrl; }) {
|
||
ReimuAController *ctrl = ARGS.ctrl;
|
||
Player *plr = ctrl->plr;
|
||
reimu_spirit_respawn_slaves(ctrl);
|
||
|
||
uint prev_inputflags = 0;
|
||
|
||
for(;;) {
|
||
WAIT_EVENT_OR_DIE(&plr->events.inputflags_changed);
|
||
|
||
// focus state changed?
|
||
while((prev_inputflags ^ plr->inputflags) & INFLAG_FOCUS) {
|
||
reimu_spirit_kill_slaves(ctrl);
|
||
WAIT(ORB_RETRACT_TIME * 2);
|
||
|
||
// important to record these at the time of respawning
|
||
prev_inputflags = plr->inputflags;
|
||
reimu_spirit_respawn_slaves(ctrl);
|
||
|
||
// "shift-spam" protection - minimum time until next respawn
|
||
WAIT(ORB_RETRACT_TIME * 2);
|
||
}
|
||
}
|
||
}
|
||
|
||
TASK(reimu_spirit_power_handler, { ReimuAController *ctrl; }) {
|
||
ReimuAController *ctrl = ARGS.ctrl;
|
||
Player *plr = ctrl->plr;
|
||
int old_power = player_get_effective_power(plr) / 100;
|
||
|
||
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_spirit_respawn_slaves(ctrl);
|
||
old_power = new_power;
|
||
}
|
||
}
|
||
}
|
||
|
||
TASK(reimu_spirit_shot_forward, { ReimuAController *ctrl; }) {
|
||
ReimuAController *ctrl = ARGS.ctrl;
|
||
Player *plr = ctrl->plr;
|
||
real dir = 1;
|
||
ShaderProgram *shader = res_shader("sprite_particle");
|
||
|
||
for(;;) {
|
||
WAIT_EVENT_OR_DIE(&plr->events.shoot);
|
||
play_sfx_loop("generic_shot");
|
||
INVOKE_TASK(reimu_spirit_ofuda,
|
||
.pos = plr->pos + 10 * dir - 15.0*I,
|
||
.vel = -20*I,
|
||
.damage = SHOT_FORWARD_DMG,
|
||
.shader = shader
|
||
);
|
||
dir = -dir;
|
||
WAIT(SHOT_FORWARD_DELAY);
|
||
}
|
||
}
|
||
|
||
TASK(reimu_spirit_shot_volley_bullet, { Player *plr; cmplx offset; cmplx vel; real damage; ShaderProgram *shader; }) {
|
||
play_sfx_loop("generic_shot");
|
||
|
||
PROJECTILE(
|
||
.proto = pp_hakurei_seal,
|
||
.pos = ARGS.plr->pos + ARGS.offset,
|
||
.color = color_mul_scalar(RGBA(1, 1, 1, 0.5), 0.7),
|
||
.move = move_linear(ARGS.vel),
|
||
.type = PROJ_PLAYER,
|
||
.damage = ARGS.damage,
|
||
.shader_ptr = ARGS.shader,
|
||
);
|
||
}
|
||
|
||
TASK(reimu_spirit_shot_volley, { ReimuAController *ctrl; }) {
|
||
Player *plr = ARGS.ctrl->plr;
|
||
ShaderProgram *shader = res_shader("sprite_particle");
|
||
|
||
for(;;) {
|
||
WAIT_EVENT_OR_DIE(&plr->events.shoot);
|
||
|
||
int power_rank = player_get_effective_power(plr) / 100;
|
||
real damage = SHOT_VOLLEY_DMG;
|
||
|
||
for(int pwr = 0; pwr <= power_rank; ++pwr) {
|
||
int delay = 5 * pwr;
|
||
real spread = M_PI/32 * (1 + 0.35 * pwr);
|
||
|
||
INVOKE_SUBTASK_DELAYED(delay, reimu_spirit_shot_volley_bullet,
|
||
.plr = plr,
|
||
.offset = -I + 5,
|
||
.vel = -18 * I * cdir(spread),
|
||
.damage = damage,
|
||
.shader = shader
|
||
);
|
||
|
||
INVOKE_SUBTASK_DELAYED(delay, reimu_spirit_shot_volley_bullet,
|
||
.plr = plr,
|
||
.offset = -I - 5,
|
||
.vel = -18 * I * cdir(-spread),
|
||
.damage = damage,
|
||
.shader = shader
|
||
);
|
||
}
|
||
|
||
WAIT(SHOT_VOLLEY_DELAY);
|
||
}
|
||
}
|
||
|
||
TASK(reimu_spirit_controller, { BoxedPlayer plr; }) {
|
||
ReimuAController *ctrl = TASK_MALLOC(sizeof(ReimuAController));
|
||
ctrl->plr = TASK_BIND(ARGS.plr);
|
||
TASK_HOST_EVENTS(ctrl->events);
|
||
|
||
INVOKE_SUBTASK(reimu_spirit_focus_handler, ctrl);
|
||
INVOKE_SUBTASK(reimu_spirit_power_handler, ctrl);
|
||
INVOKE_SUBTASK(reimu_spirit_shot_forward, ctrl);
|
||
INVOKE_SUBTASK(reimu_spirit_shot_volley, ctrl);
|
||
INVOKE_SUBTASK(reimu_spirit_bomb_handler, ctrl);
|
||
|
||
STALL;
|
||
}
|
||
|
||
static void reimu_spirit_init(Player *plr) {
|
||
reimu_common_bomb_buffer_init();
|
||
INVOKE_TASK(reimu_spirit_controller, ENT_BOX(plr));
|
||
}
|
||
|
||
static double reimu_spirit_property(Player *plr, PlrProperty prop) {
|
||
real base_value = reimu_common_property(plr, prop);
|
||
|
||
switch(prop) {
|
||
// fallthrough
|
||
default: {
|
||
return base_value;
|
||
}
|
||
}
|
||
}
|
||
|
||
PlayerMode plrmode_reimu_a = {
|
||
.name = "Yōkai Buster",
|
||
.description = "The tried-and-true: homing amulets and sharp needles. You don’t have the luxury of time to think about aiming.",
|
||
.spellcard_name = "Spirit Sign “Fantasy Seal —Burst—”",
|
||
.character = &character_reimu,
|
||
.dialog = &dialog_tasks_reimu,
|
||
.shot_mode = PLR_SHOT_REIMU_SPIRIT,
|
||
.procs = {
|
||
.property = reimu_spirit_property,
|
||
.init = reimu_spirit_init,
|
||
.preload = reimu_spirit_preload,
|
||
},
|
||
};
|