Coroutinize ReimuA (#199)

This commit is contained in:
Andrei Alexeyev 2020-03-13 22:54:07 +02:00 committed by GitHub
parent 76a60d92a9
commit 01e7ea8a33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 557 additions and 348 deletions

View file

@ -355,9 +355,13 @@ bool enemy_in_viewport(Enemy *enemy) {
cimag(enemy->pos) <= VIEWPORT_H + s;
}
void enemy_kill(Enemy *enemy) {
enemy->hp = ENEMY_KILLED;
}
void enemy_kill_all(EnemyList *enemies) {
for(Enemy *e = enemies->first; e; e = e->next) {
e->hp = ENEMY_KILLED;
enemy_kill(e);
}
}

View file

@ -91,6 +91,7 @@ void process_enemies(EnemyList *enemies);
bool enemy_is_vulnerable(Enemy *enemy);
bool enemy_in_viewport(Enemy *enemy);
void enemy_kill(Enemy *enemy);
void enemy_kill_all(EnemyList *enemies);
void Fairy(Enemy*, int t, bool render);

View file

@ -49,6 +49,8 @@ static DamageResult ent_damage_player(EntityInterface *ent, const DamageInfo *dm
static void player_spawn_focus_circle(Player *plr);
DECLARE_TASK(player_logic, { BoxedPlayer plr; });
void player_stage_post_init(Player *plr) {
assert(plr->mode != NULL);
@ -61,10 +63,6 @@ void player_stage_post_init(Player *plr) {
delete_enemies(&global.plr.slaves);
if(plr->mode->procs.init != NULL) {
plr->mode->procs.init(plr);
}
plrchar_make_bomb_portrait(plr->mode->character, &plr->bomb_portrait);
aniplayer_create(&plr->ani, get_ani(plr->mode->character->player_sprite_name), "main");
@ -73,6 +71,14 @@ void player_stage_post_init(Player *plr) {
plr->ent.damage_func = ent_damage_player;
ent_register(&plr->ent, ENT_PLAYER);
COEVENT_INIT_ARRAY(plr->events);
INVOKE_TASK_DELAYED(1, player_logic, ENT_BOX(plr));
if(plr->mode->procs.init != NULL) {
plr->mode->procs.init(plr);
}
player_spawn_focus_circle(plr);
plr->extralife_threshold = player_next_extralife_threshold(plr->extralives_given);
@ -83,6 +89,8 @@ void player_stage_post_init(Player *plr) {
}
void player_free(Player *plr) {
COEVENT_CANCEL_ARRAY(plr->events);
if(plr->mode->procs.free) {
plr->mode->procs.free(plr);
}
@ -120,7 +128,13 @@ bool player_set_power(Player *plr, short npow) {
player_full_power(plr);
}
return (oldpow + oldpow_over) != (plr->power + plr->power_overflow);
bool change = (oldpow + oldpow_over) != (plr->power + plr->power_overflow);
if(change) {
coevent_signal(&plr->events.power_changed);
}
return change;
}
bool player_add_power(Player *plr, short pdelta) {
@ -553,61 +567,77 @@ static void player_powersurge_logic(Player *plr) {
plr->powersurge.power += plr->powersurge.bonus.gain_rate;
}
DEFINE_TASK(player_logic) {
Player *plr = TASK_BIND(ARGS.plr);
uint prev_inputflags = 0;
for(;;) {
YIELD;
if(prev_inputflags != plr->inputflags) {
coevent_signal(&plr->events.inputflags_changed);
prev_inputflags = plr->inputflags;
}
fapproach_p(&plr->bomb_cutin_alpha, 0, 1/200.0);
if(plr->respawntime - PLR_RESPAWN_TIME/2 == global.frames && plr->lives < 0 && global.replaymode != REPLAY_PLAY) {
stage_gameover();
}
if(plr->continuetime == global.frames) {
plr->lives = PLR_START_LIVES;
plr->bombs = PLR_START_BOMBS;
plr->point_item_value = PLR_START_PIV;
plr->life_fragments = 0;
plr->bomb_fragments = 0;
plr->continues_used += 1;
player_set_power(plr, 0);
stage_clear_hazards(CLEAR_HAZARDS_ALL);
spawn_items(plr->deathpos, ITEM_POWER, (int)ceil(PLR_MAX_POWER/(double)POWER_VALUE));
}
process_enemies(&plr->slaves);
aniplayer_update(&plr->ani);
if(plr->lives < 0) {
continue;
}
if(plr->respawntime > global.frames) {
double x = PLR_SPAWN_POS_X;
double y = lerp(PLR_SPAWN_POS_Y, VIEWPORT_H + 64, smoothstep(0.0, 1.0, (plr->respawntime - global.frames) / (double)PLR_RESPAWN_TIME));
plr->pos = CMPLX(x, y);
stage_clear_hazards(CLEAR_HAZARDS_ALL | CLEAR_HAZARDS_NOW);
continue;
}
if(player_is_powersurge_active(plr)) {
player_powersurge_logic(plr);
}
plr->focus = approach(plr->focus, (plr->inputflags & INFLAG_FOCUS) ? 30 : 0, 1);
plr->focus_circle.first->pos = plr->pos;
process_enemies(&plr->focus_circle);
if(plr->mode->procs.think) {
plr->mode->procs.think(plr);
}
if(player_should_shoot(plr, false)) {
coevent_signal(&plr->events.shoot);
plr->mode->procs.shot(plr);
}
if(global.frames == plr->deathtime) {
player_realdeath(plr);
} else if(plr->deathtime > global.frames) {
stage_clear_hazards(CLEAR_HAZARDS_ALL | CLEAR_HAZARDS_NOW);
}
}
}
void player_logic(Player* plr) {
fapproach_p(&plr->bomb_cutin_alpha, 0, 1/200.0);
if(plr->respawntime - PLR_RESPAWN_TIME/2 == global.frames && plr->lives < 0 && global.replaymode != REPLAY_PLAY) {
stage_gameover();
}
if(plr->continuetime == global.frames) {
plr->lives = PLR_START_LIVES;
plr->bombs = PLR_START_BOMBS;
plr->point_item_value = PLR_START_PIV;
plr->life_fragments = 0;
plr->bomb_fragments = 0;
plr->continues_used += 1;
player_set_power(plr, 0);
stage_clear_hazards(CLEAR_HAZARDS_ALL);
spawn_items(plr->deathpos, ITEM_POWER, (int)ceil(PLR_MAX_POWER/(double)POWER_VALUE));
}
process_enemies(&plr->slaves);
aniplayer_update(&plr->ani);
if(plr->lives < 0) {
return;
}
if(plr->respawntime > global.frames) {
double x = PLR_SPAWN_POS_X;
double y = lerp(PLR_SPAWN_POS_Y, VIEWPORT_H + 64, smoothstep(0.0, 1.0, (plr->respawntime - global.frames) / (double)PLR_RESPAWN_TIME));
plr->pos = CMPLX(x, y);
stage_clear_hazards(CLEAR_HAZARDS_ALL | CLEAR_HAZARDS_NOW);
return;
}
if(player_is_powersurge_active(plr)) {
player_powersurge_logic(plr);
}
plr->focus = approach(plr->focus, (plr->inputflags & INFLAG_FOCUS) ? 30 : 0, 1);
plr->focus_circle.first->pos = plr->pos;
process_enemies(&plr->focus_circle);
if(plr->mode->procs.think) {
plr->mode->procs.think(plr);
}
if(player_should_shoot(plr, false)) {
plr->mode->procs.shot(plr);
}
if(global.frames == plr->deathtime) {
player_realdeath(plr);
} else if(plr->deathtime > global.frames) {
stage_clear_hazards(CLEAR_HAZARDS_ALL | CLEAR_HAZARDS_NOW);
}
}
static bool player_bomb(Player *plr) {

View file

@ -115,6 +115,12 @@ struct Player {
PowerSurgeBonus bonus;
} powersurge;
COEVENTS_ARRAY(
shoot,
inputflags_changed,
power_changed
) events;
uint64_t points;
uint64_t extralife_threshold;
@ -138,7 +144,8 @@ struct Player {
int respawntime;
int bombtotaltime;
int inputflags;
uint inputflags;
int lastmovesequence; // used for animation
int axis_ud;
int axis_lr;

View file

@ -12,12 +12,14 @@
#include "plrmodes.h"
#include "reimu.h"
// FIXME: We probably need a better way to store shot-specific state.
// See also MarisaA.
static struct {
uint prev_inputflags;
bool respawn_slaves;
} reimu_spirit_state;
#define ORB_RETRACT_TIME 4
typedef struct ReimuAController {
Player *plr;
COEVENTS_ARRAY(
slaves_expired
) events;
} ReimuAController;
static void reimu_spirit_preload(void) {
const int flags = RESF_DEFAULT;
@ -48,30 +50,40 @@ static void reimu_spirit_preload(void) {
NULL);
}
static int reimu_spirit_needle(Projectile *p, int t) {
int r = linear(p, t);
TASK(reimu_spirit_needle, { cmplx pos; cmplx vel; real damage; ShaderProgram *shader; }) {
Projectile *p = TASK_BIND_UNBOXED(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,
));
if(t < 0) {
return r;
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,
);
}
Color c = p->color;
color_mul(&c, RGBA_MUL_ALPHA(0.75, 0.5, 1, 0.5));
color_mul_scalar(&c, 0.6);
c.a = 0;
PARTICLE(
.sprite_ptr = p->sprite,
.color = &c,
.timeout = 12,
.pos = p->pos,
.move = move_linear(p->args[0] * 0.8),
.draw_rule = pdraw_timeout_scalefade(0, 2, 1, 0),
.layer = LAYER_PARTICLE_LOW,
.flags = PFLAG_NOREFLECT,
);
return r;
}
#define REIMU_SPIRIT_HOMING_SCALE 0.75
@ -87,7 +99,7 @@ static Projectile *reimu_spirit_spawn_ofuda_particle(Projectile *p, int t, doubl
.timeout = 12,
.pos = p->pos,
.angle = p->angle,
.move = move_linear(p->args[0] * rng_range(0.6, 1.0) * vfactor),
.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,
@ -95,67 +107,65 @@ static Projectile *reimu_spirit_spawn_ofuda_particle(Projectile *p, int t, doubl
);
}
static int reimu_spirit_homing_impact(Projectile *p, int t) {
if(t < 0) {
return ACTION_ACK;
}
TASK(reimu_spirit_homing_impact, { BoxedProjectile p; }) {
Projectile *ref = NOT_NULL(ENT_UNBOX(ARGS.p));
Projectile *trail = reimu_spirit_spawn_ofuda_particle(p, global.frames, 1);
trail->rule = NULL;
trail->timeout = 6;
trail->angle = p->angle;
trail->ent.draw_layer = LAYER_PLAYER_FOCUS - 1; // TODO: add a layer for "super high" particles?
trail->args[2] *= 1.5 * (1 - t/p->timeout);
p->angle += 0.2;
return ACTION_NONE;
}
static Projectile* reimu_spirit_spawn_homing_impact(Projectile *p, int t) {
return PARTICLE(
.proto = p->proto,
.color = &p->color,
Projectile *p = TASK_BIND_UNBOXED(PARTICLE(
.proto = ref->proto,
.color = &ref->color,
.timeout = 32,
.pos = p->pos,
.angle = p->angle,
.rule = reimu_spirit_homing_impact,
.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,
.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 double reimu_spirit_homing_aimfactor(double t, double maxt) {
t = clamp(t, 0, maxt);
double q = pow(1 - t / maxt, 3);
static inline real reimu_spirit_homing_aimfactor(real t, real maxt) {
real q = pow(t / maxt, 3);
return 4 * q * (1 - q);
}
static int reimu_spirit_homing(Projectile *p, int t) {
if(t < 0) {
if(t == EVENT_DEATH && !global.gameover && projectile_in_viewport(p)) {
reimu_spirit_spawn_homing_impact(p, t);
}
TASK(reimu_spirit_homing, { cmplx pos; cmplx vel; real damage; ShaderProgram *shader; }) {
Projectile *p = TASK_BIND_UNBOXED(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,
));
return ACTION_ACK;
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->args[3] = plrutil_homing_target(p->pos, p->args[3]);
double v = cabs(p->args[0]);
cmplx aimdir = cexp(I*carg(p->args[3] - p->pos));
double aim = reimu_spirit_homing_aimfactor(t, p->args[1]);
p->args[0] += v * 0.25 * aim * aimdir;
p->args[0] = v * cexp(I*carg(p->args[0]));
p->angle = carg(p->args[0]);
double s = 1;// max(pow(2*t/creal(p->args[1]), 2), 0.1); //(0.25 + 0.75 * (1 - aim));
p->pos += p->args[0] * s;
reimu_spirit_spawn_ofuda_particle(p, t, 0.5);
return ACTION_NONE;
}
static Color *reimu_spirit_orb_color(Color *c, int i) {
@ -163,19 +173,6 @@ static Color *reimu_spirit_orb_color(Color *c, int i) {
return c;
}
static int reimu_spirit_bomb_orb_trail(Projectile *p, int t) {
if(t < 0) {
return ACTION_ACK;
}
p->pos += p->args[0];
p->angle -= 0.05;
// p->color = *HSLA(2.0*t/p->timeout, 0.5, 0.5, 0.0);
return ACTION_NONE;
}
static void reimu_spirit_bomb_impact_balls(cmplx pos, int count) {
real offset = rng_real();
@ -186,12 +183,11 @@ static void reimu_spirit_bomb_impact_balls(cmplx pos, int count) {
.color = HSLA(3 * (float)i / count + offset, 1, 0.5, 0),
.timeout = 60,
.pos = pos,
.args = { cdir(2 * M_PI / count * (i + offset)) * 15 },
.angle = rng_angle(),
.rule = linear,
.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,
.flags = PFLAG_NOREFLECT | PFLAG_REQUIREDPARTICLE | PFLAG_MANUALANGLE,
);
}
}
@ -368,11 +364,11 @@ TASK(reimu_spirit_bomb_orb, { BoxedPlayer plr; int index; real angle; }) {
.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),
.rule = reimu_spirit_bomb_orb_trail,
.args = { trail_vel },
.flags = PFLAG_NOREFLECT,
.move = move_linear(trail_vel),
.flags = PFLAG_NOREFLECT | PFLAG_MANUALANGLE,
);
}
@ -439,245 +435,418 @@ TASK(reimu_spirit_ofuda, { cmplx pos; cmplx vel; real damage; }) {
}
static void reimu_spirit_shot(Player *p) {
play_loop("generic_shot");
if(!(global.frames % 3)) {
int i = 1 - 2 * (bool)(global.frames % 6);
INVOKE_TASK(reimu_spirit_ofuda, p->pos + 10 * i - 15.0*I, -20*I, 100 - 8 * (p->power / 100));
}
for(int pwr = 0; pwr <= p->power/100; ++pwr) {
int t = (global.frames - 5 * pwr);
if(!(t % 16)) {
for(int i = -1; i < 2; i += 2) {
float spread = i * M_PI/32 * (1 + 0.35 * pwr);
PROJECTILE(
.proto = pp_hakurei_seal,
.pos = p->pos - I + 5 * i,
.color = color_mul_scalar(RGBA(1, 1, 1, 0.5), 0.7),
.rule = linear,
.args = { -18.0*I*cexp(I*spread) },
.type = PROJ_PLAYER,
.damage = 60 - 5 * (p->power / 100),
.shader = "sprite_particle",
);
}
}
}
}
static void reimu_spirit_slave_shot(Enemy *e, int t) {
int st = t;
if(st % 3) {
return;
}
if(global.plr.inputflags & INFLAG_FOCUS) {
PROJECTILE(
.proto = pp_needle,
.pos = e->pos - 25.0*I,
.color = RGBA_MUL_ALPHA(1, 1, 1, 0.5),
.rule = reimu_spirit_needle,
.args = { -20.0*I },
.type = PROJ_PLAYER,
.damage_type = DMG_PLAYER_SHOT,
.damage = cimag(e->args[2]),
.shader = "sprite_particle",
);
} else if(!(st % 12)) {
cmplx v = -10 * I * cexp(I*cimag(e->args[0]));
PROJECTILE(
.proto = pp_ofuda,
.pos = e->pos,
.color = RGBA_MUL_ALPHA(1, 0.9, 0.95, 0.7),
.rule = reimu_spirit_homing,
.args = { v , 60, 0, e->pos + v * VIEWPORT_H * VIEWPORT_W /*creal(e->pos)*/ },
.type = PROJ_PLAYER,
.damage_type = DMG_PLAYER_SHOT,
.damage = creal(e->args[2]),
// .timeout = 60,
.shader = "sprite_particle",
.scale = REIMU_SPIRIT_HOMING_SCALE,
.flags = PFLAG_NOCOLLISIONEFFECT,
);
}
}
static int reimu_spirit_slave(Enemy *e, int t) {
TIMER(&t);
AT(EVENT_BIRTH) {
e->pos = global.plr.pos;
return ACTION_NONE;
}
if(t < 0) {
return ACTION_NONE;
}
if(player_should_shoot(&global.plr, true)) {
reimu_spirit_slave_shot(e, t);
}
if(creal(e->args[3]) != 0) {
int death_begin_time = creal(e->args[3]);
int death_duration = cimag(e->args[3]);
double death_progress = (t - death_begin_time) / (double)death_duration;
e->pos = global.plr.pos * death_progress + e->pos0 * (1 - death_progress);
if(death_progress >= 1) {
return ACTION_DESTROY;
}
return ACTION_NONE;
}
double speed = 0.005 * min(1, t / 12.0);
if(global.plr.inputflags & INFLAG_FOCUS) {
GO_TO(e, global.plr.pos + cimag(e->args[1]) * cexp(I*(creal(e->args[0]) + t * creal(e->args[1]))), speed * cabs(e->args[1]));
} else {
GO_TO(e, global.plr.pos + e->pos0, speed * cabs(e->pos0));
}
return ACTION_NONE;
}
static void reimu_spirit_yinyang_focused_visual(Enemy *e, int t, bool render) {
if(!render && player_should_shoot(&global.plr, true)) {
RNG_ARRAY(R, 4);
PARTICLE(
.sprite = "stain",
.color = RGBA(0.5, vrng_range(R[0], 0, 0.25), 0, 0),
.timeout = vrng_range(R[1], 8, 10),
.pos = e->pos,
.angle = vrng_angle(R[3]),
.move = move_linear(-I * vrng_range(R[2], 5, 10)),
.draw_rule = pdraw_timeout_scalefade(0.25, 0, 1, 0),
.flags = PFLAG_NOREFLECT | PFLAG_MANUALANGLE,
);
}
if(render) {
reimu_common_draw_yinyang(e, t, RGB(1.0, 0.8, 0.8));
}
}
static void reimu_spirit_yinyang_unfocused_visual(Enemy *e, int t, bool render) {
if(!render && player_should_shoot(&global.plr, true)) {
RNG_ARRAY(R, 4);
PARTICLE(
.sprite = "stain",
.color = RGBA(0.5, 0.125, vrng_range(R[0], 0, 0.25), 0),
.timeout = vrng_range(R[1], 8, 10),
.pos = e->pos,
.args = { -I * vrng_range(R[2], 5, 10), 0, 0.25 + 0*I },
.angle = vrng_angle(R[3]),
.move = move_linear(-I * vrng_range(R[2], 5, 10)),
.draw_rule = pdraw_timeout_scalefade(0.25, 0, 1, 0),
.flags = PFLAG_NOREFLECT | PFLAG_MANUALANGLE,
);
}
if(render) {
reimu_common_draw_yinyang(e, t, RGB(0.95, 0.75, 1.0));
}
}
static inline Enemy* reimu_spirit_spawn_slave(Player *plr, cmplx pos, cmplx a0, cmplx a1, cmplx a2, cmplx a3, EnemyVisualRule visual) {
Enemy *e = create_enemy_p(&plr->slaves, pos, ENEMY_IMMUNE, visual, reimu_spirit_slave, a0, a1, a2, a3);
TASK(reimu_spirit_slave_expire, { ReimuAController *ctrl; BoxedEnemy e; BoxedTask main_task; }) {
Enemy *e = TASK_BIND(ARGS.e);
Player *plr = ARGS.ctrl->plr;
cotask_cancel(cotask_unbox(ARGS.main_task));
cmplx pos0 = e->pos;
real retract_time = ORB_RETRACT_TIME;
e->move = (MoveParams) { 0 };
for(int i = 1; i <= retract_time; ++i) {
YIELD;
e->pos = clerp(pos0, plr->pos, i / retract_time);
}
delete_enemy(&plr->slaves, e);
}
static Enemy *reimu_spirit_create_slave(ReimuAController *ctrl, EnemyVisualRule visual) {
Player *plr = ctrl->plr;
Enemy *e = create_enemy_p(
&plr->slaves,
plr->pos,
ENEMY_IMMUNE,
visual,
NULL, 0, 0, 0, 0
);
e->ent.draw_layer = LAYER_PLAYER_SLAVE;
INVOKE_TASK_WHEN(&ctrl->events.slaves_expired, reimu_spirit_slave_expire, ctrl, ENT_BOX(e), THIS_TASK);
return e;
}
static void reimu_spirit_kill_slaves(EnemyList *slaves) {
for(Enemy *e = slaves->first, *next; e; e = next) {
next = e->next;
TASK(reimu_slave_shot_particles, {
BoxedEnemy e;
int num;
cmplx dir;
Color *color0;
Color *color1;
Sprite *sprite;
}) {
Enemy *e = TASK_BIND(ARGS.e);
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;
if(e->hp == ENEMY_IMMUNE && creal(e->args[3]) == 0) {
// delete_enemy(slaves, e);
// e->args[3] = 1;
e->args[3] = global.frames - e->birthtime + 3 * I;
e->pos0 = e->pos;
}
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 = e->pos,
.sprite_ptr = sprite,
.timeout = timeout,
);
YIELD;
}
}
static void reimu_spirit_respawn_slaves(Player *plr, short npow, cmplx param) {
double dmg_homing = 120 - 12 * plr->power / 100; // every 12 frames
double dmg_needle = 92 - 10 * plr->power / 100; // every 3 frames
cmplx dmg = dmg_homing + I * dmg_needle;
EnemyVisualRule visual;
TASK(reimu_spirit_slave_needle_shot, {
ReimuAController *ctrl;
BoxedEnemy e;
real damage;
}) {
Player *plr = ARGS.ctrl->plr;
Enemy *e = TASK_BIND(ARGS.e);
if(plr->inputflags & INFLAG_FOCUS) {
visual = reimu_spirit_yinyang_focused_visual;
} else {
visual = reimu_spirit_yinyang_unfocused_visual;
ShaderProgram *shader = r_shader_get("sprite_particle");
Sprite *particle_spr = get_sprite("part/stain");
real damage = ARGS.damage;
real delay = 3;
for(;;) {
WAIT_EVENT_OR_DIE(&plr->events.shoot);
INVOKE_SUBTASK(reimu_slave_shot_particles,
.e = ENT_BOX(e),
.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 = e->pos - 25.0*I,
.vel = -20*I,
.damage = damage,
.shader = shader
);
WAIT(delay);
}
}
reimu_spirit_kill_slaves(&plr->slaves);
TASK(reimu_spirit_slave_needle, {
ReimuAController *ctrl;
real distance;
real rotation;
real rotation_offset;
real shot_damage;
}) {
ReimuAController *ctrl = ARGS.ctrl;
Player *plr = ctrl->plr;
Enemy *e = TASK_BIND_UNBOXED(reimu_spirit_create_slave(ctrl, reimu_spirit_yinyang_focused_visual));
switch(npow / 100) {
INVOKE_SUBTASK(reimu_spirit_slave_needle_shot, ctrl, ENT_BOX(e), ARGS.shot_damage);
real dist = ARGS.distance;
cmplx offset = dist * cdir(ARGS.rotation_offset);
cmplx rot = cdir(ARGS.rotation);
real target_speed = 0.005 * dist;
e->move = move_towards(plr->pos + offset, 0);
for(;;) {
YIELD;
e->move.attraction = approach(e->move.attraction, target_speed, target_speed / 12.0);
e->move.attraction_point = plr->pos + offset;
offset *= rot;
}
}
TASK(reimu_spirit_slave_homing_shot, {
ReimuAController *ctrl;
BoxedEnemy e;
cmplx vel;
real damage;
}) {
Player *plr = ARGS.ctrl->plr;
Enemy *e = TASK_BIND(ARGS.e);
ShaderProgram *shader = r_shader_get("sprite_particle");
Sprite *particle_spr = get_sprite("part/stain");
cmplx vel = ARGS.vel;
cmplx pdir = cnormalize(vel);
real damage = ARGS.damage;
real delay = 12;
for(;;) {
WAIT_EVENT_OR_DIE(&plr->events.shoot);
INVOKE_SUBTASK(reimu_slave_shot_particles,
.e = ENT_BOX(e),
.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 = e->pos,
.vel = vel,
.damage = damage,
.shader = shader
);
WAIT(delay);
}
}
TASK(reimu_spirit_slave_homing, {
ReimuAController *ctrl;
cmplx offset;
cmplx shot_vel;
real shot_damage;
}) {
ReimuAController *ctrl = ARGS.ctrl;
Player *plr = ctrl->plr;
Enemy *e = TASK_BIND_UNBOXED(reimu_spirit_create_slave(ctrl, reimu_spirit_yinyang_unfocused_visual));
INVOKE_SUBTASK(reimu_spirit_slave_homing_shot, ctrl, ENT_BOX(e), ARGS.shot_vel, ARGS.shot_damage);
cmplx offset = ARGS.offset;
real target_speed = 0.005 * cabs(offset);
e->move = move_towards(plr->pos + offset, 0);
for(;;) {
YIELD;
e->move.attraction = approach(e->move.attraction, target_speed, target_speed / 12.0);
e->move.attraction_point = plr->pos + offset;
}
}
static void reimu_spirit_spawn_slaves_unfocused(ReimuAController *ctrl, int power_rank) {
real dmg_homing = 100; // 120 - 12 * power_rank;
cmplx sv = -10 * I;
switch(power_rank) {
case 0:
break;
case 1:
reimu_spirit_spawn_slave(plr, 50.0*I, 0 , +0.10 + 60*I, dmg, 0, visual);
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, 50*I, sv, dmg_homing);
break;
case 2:
reimu_spirit_spawn_slave(plr, +40, 0 +M_PI/24*I, +0.10 + 60*I, dmg, 0, visual);
reimu_spirit_spawn_slave(plr, -40, M_PI -M_PI/24*I, +0.10 + 60*I, dmg, 0, visual);
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, +40, sv * cdir(+M_PI/24), dmg_homing);
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, -40, sv * cdir(-M_PI/24), dmg_homing);
break;
case 3:
reimu_spirit_spawn_slave(plr, 50.0*I, 0*2*M_PI/3 , +0.10 + 60*I, dmg, 0, visual);
reimu_spirit_spawn_slave(plr, +40, 1*2*M_PI/3 +M_PI/24*I, +0.10 + 60*I, dmg, 0, visual);
reimu_spirit_spawn_slave(plr, -40, 2*2*M_PI/3 -M_PI/24*I, +0.10 + 60*I, dmg, 0, visual);
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, 50*I, sv, dmg_homing);
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, +40, sv * cdir(+M_PI/24), dmg_homing);
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, -40, sv * cdir(-M_PI/24), dmg_homing);
break;
case 4:
reimu_spirit_spawn_slave(plr, +40, 0 +M_PI/32*I, +0.10 + 60*I, dmg, 0, visual);
reimu_spirit_spawn_slave(plr, -40, M_PI -M_PI/32*I, +0.10 + 60*I, dmg, 0, visual);
reimu_spirit_spawn_slave(plr, +80, 0 +M_PI/16*I, -0.05 + 80*I, dmg, 0, visual);
reimu_spirit_spawn_slave(plr, -80, M_PI -M_PI/16*I, -0.05 + 80*I, dmg, 0, visual);
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, +40, sv * cdir(+M_PI/24), dmg_homing);
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, -40, sv * cdir(-M_PI/24), dmg_homing);
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, +80, sv * cdir(+M_PI/16), dmg_homing);
INVOKE_TASK(reimu_spirit_slave_homing, ctrl, -80, sv * cdir(-M_PI/16), dmg_homing);
break;
default:
UNREACHABLE;
}
}
static void reimu_spirit_init(Player *plr) {
memset(&reimu_spirit_state, 0, sizeof(reimu_spirit_state));
reimu_spirit_state.prev_inputflags = plr->inputflags;
reimu_spirit_respawn_slaves(plr, plr->power, 0);
reimu_common_bomb_buffer_init();
static void reimu_spirit_spawn_slaves_focused(ReimuAController *ctrl, int power_rank) {
real dmg_needle = 90; // 92 - 10 * power_rank;
switch(power_rank) {
case 0:
break;
case 1:
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 60, 0.10, 0, dmg_needle);
break;
case 2:
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 60, 0.10, 0, dmg_needle);
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 60, 0.10, M_TAU/2, dmg_needle);
break;
case 3:
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 60, 0.10, 0*M_TAU/3, dmg_needle);
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 60, 0.10, 1*M_TAU/3, dmg_needle);
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 60, 0.10, 2*M_TAU/3, dmg_needle);
break;
case 4:
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 60, 0.10, 0, dmg_needle);
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 60, 0.10, M_PI, dmg_needle);
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 80, -0.05, 0, dmg_needle);
INVOKE_TASK(reimu_spirit_slave_needle, ctrl, 80, -0.05, M_PI, dmg_needle);
break;
default:
UNREACHABLE;
}
}
static void reimu_spirit_think(Player *plr) {
if((bool)(reimu_spirit_state.prev_inputflags & INFLAG_FOCUS) ^ (bool)(plr->inputflags & INFLAG_FOCUS)) {
reimu_spirit_state.respawn_slaves = true;
}
static void reimu_spirit_kill_slaves(ReimuAController *ctrl) {
coevent_signal(&ctrl->events.slaves_expired);
}
if(reimu_spirit_state.respawn_slaves) {
if(plr->slaves.first) {
reimu_spirit_kill_slaves(&plr->slaves);
} else {
reimu_spirit_respawn_slaves(plr, plr->power, 0);
reimu_spirit_state.respawn_slaves = false;
static void reimu_spirit_respawn_slaves(ReimuAController *ctrl) {
Player *plr = ctrl->plr;
int power_rank = plr->power / 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) {
if(plr->slaves.first) {
reimu_spirit_kill_slaves(ctrl);
WAIT(ORB_RETRACT_TIME);
}
// 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);
}
}
reimu_spirit_state.prev_inputflags = plr->inputflags;
}
static void reimu_spirit_power(Player *plr, short npow) {
if(plr->power / 100 != npow / 100) {
reimu_spirit_respawn_slaves(plr, npow, 0);
TASK(reimu_spirit_power_handler, { ReimuAController *ctrl; }) {
ReimuAController *ctrl = ARGS.ctrl;
Player *plr = ctrl->plr;
int old_power = plr->power / 100;
for(;;) {
WAIT_EVENT_OR_DIE(&plr->events.power_changed);
int new_power = plr->power / 100;
if(old_power != new_power) {
reimu_spirit_respawn_slaves(ctrl);
old_power = new_power;
}
}
}
TASK(reimu_spirit_shot_forward, { ReimuAController *ctrl; }) {
Player *plr = ARGS.ctrl->plr;
real dir = 1;
for(;;) {
WAIT_EVENT_OR_DIE(&plr->events.shoot);
play_loop("generic_shot");
INVOKE_TASK(reimu_spirit_ofuda,
.pos = plr->pos + 10 * dir - 15.0*I,
.vel = -20*I,
.damage = 100 - 8 * (plr->power / 100)
);
dir = -dir;
WAIT(3);
}
}
TASK(reimu_spirit_shot_volley_bullet, { Player *plr; cmplx offset; cmplx vel; real damage; ShaderProgram *shader; }) {
play_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 = "sprite_particle",
.shader_ptr = ARGS.shader,
);
}
TASK(reimu_spirit_shot_volley, { ReimuAController *ctrl; }) {
Player *plr = ARGS.ctrl->plr;
ShaderProgram *shader = r_shader_get("sprite_particle");
for(;;) {
WAIT_EVENT_OR_DIE(&plr->events.shoot);
int power_rank = plr->power / 100;
real damage = 60 - 5 * power_rank;
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,
.offset = -I + 5,
.vel = -18 * I * cdir(spread),
.damage = damage,
.shader = shader
);
INVOKE_SUBTASK_DELAYED(delay, reimu_spirit_shot_volley_bullet,
plr,
.offset = -I - 5,
.vel = -18 * I * cdir(-spread),
.damage = damage,
.shader = shader
);
}
WAIT(16);
}
}
TASK(reimu_spirit_controller, { BoxedPlayer plr; }) {
ReimuAController ctrl;
ctrl.plr = TASK_BIND(ARGS.plr);
COEVENT_INIT_ARRAY(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);
WAIT_EVENT(&TASK_EVENTS(THIS_TASK)->finished);
COEVENT_CANCEL_ARRAY(ctrl.events);
}
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) {
double base_value = reimu_common_property(plr, prop);
@ -707,11 +876,9 @@ PlayerMode plrmode_reimu_a = {
.procs = {
.property = reimu_spirit_property,
.init = reimu_spirit_init,
.think = reimu_spirit_think,
.bomb = reimu_spirit_bomb,
.bombbg = reimu_spirit_bomb_bg,
.shot = reimu_spirit_shot,
.power = reimu_spirit_power,
.preload = reimu_spirit_preload,
},
};