718 lines
19 KiB
C
718 lines
19 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 "youmu.h"
|
|
|
|
#include "audio/audio.h"
|
|
#include "dialog/youmu.h"
|
|
#include "global.h"
|
|
#include "plrmodes.h"
|
|
#include "renderer/api.h"
|
|
#include "stagedraw.h"
|
|
#include "util/graphics.h"
|
|
|
|
#define SHOT_SELF_DELAY 6
|
|
#define SHOT_SELF_DAMAGE 60
|
|
|
|
#define SHOT_MYON_HALFDELAY 3
|
|
#define SHOT_MYON_DAMAGE 25
|
|
|
|
typedef struct YoumuAController YoumuAController;
|
|
typedef struct YoumuAMyon YoumuAMyon;
|
|
|
|
struct YoumuAMyon {
|
|
struct {
|
|
Sprite *trail;
|
|
Sprite *smoke;
|
|
Sprite *stardust;
|
|
} sprites;
|
|
|
|
cmplx pos;
|
|
cmplx dir;
|
|
real focus_factor;
|
|
};
|
|
|
|
struct YoumuAController {
|
|
struct {
|
|
Sprite *arc;
|
|
Sprite *blast_huge_halo;
|
|
Sprite *petal;
|
|
Sprite *stain;
|
|
} sprites;
|
|
|
|
Player *plr;
|
|
YoumuAMyon myon;
|
|
YoumuBombBGData bomb_bg;
|
|
};
|
|
|
|
static Color *myon_color(Color *c, float f, float opacity, float alpha) {
|
|
*c = *RGBA_MUL_ALPHA(0.8+0.2*f, 0.9-0.4*sqrt(f), 1.0-0.35*f*f, opacity);
|
|
c->a *= alpha;
|
|
return c;
|
|
}
|
|
|
|
static cmplx myon_tail_dir(YoumuAMyon *myon) {
|
|
cmplx dir = myon->dir * cdir(0.1 * sin(global.frames * 0.05));
|
|
real f = myon->focus_factor;
|
|
return lerp(f * f, 1, 0.5) * dir;
|
|
}
|
|
|
|
static void myon_draw_trail_func(Projectile *p, int t, ProjDrawRuleArgs args) {
|
|
float focus_factor = args[0].as_float[0];
|
|
float opacity = args[0].as_float[1];
|
|
|
|
float fadein = clamp(t / 10.0, 0, 1);
|
|
float s = 1 - projectile_timeout_factor(p);
|
|
|
|
SpriteParamsBuffer spbuf;
|
|
SpriteParams sp = projectile_sprite_params(p, &spbuf);
|
|
|
|
float a = opacity * fadein;
|
|
myon_color(&spbuf.color, focus_factor, a * s * s, 0);
|
|
sp.scale.as_cmplx *= fadein * (2 - s);
|
|
|
|
r_draw_sprite(&sp);
|
|
}
|
|
|
|
static ProjDrawRule myon_draw_trail(float focus_factor, float opacity) {
|
|
return (ProjDrawRule) {
|
|
.func = myon_draw_trail_func,
|
|
.args[0].as_float = { focus_factor, opacity },
|
|
};
|
|
}
|
|
|
|
TASK(youmu_mirror_myon_trail, { YoumuAMyon *myon; cmplx pos; }) {
|
|
YoumuAMyon *myon = ARGS.myon;
|
|
|
|
Projectile *p = TASK_BIND(PARTICLE(
|
|
.angle = rng_angle(),
|
|
.draw_rule = pdraw_timeout_scale(2, 0.01),
|
|
.flags = PFLAG_NOREFLECT | PFLAG_REQUIREDPARTICLE | PFLAG_MANUALANGLE | PFLAG_PLRSPECIALPARTICLE,
|
|
.layer = LAYER_PARTICLE_LOW,
|
|
.move = move_linear(0),
|
|
.pos = ARGS.pos,
|
|
.sprite_ptr = myon->sprites.trail,
|
|
.timeout = 40,
|
|
));
|
|
|
|
p->angle_delta = 0.03 * (1 - 2 * (p->birthtime & 1));
|
|
|
|
for(int t = 0;; ++t) {
|
|
real f = myon->focus_factor;
|
|
myon_color(&p->color, f, powf(1 - min(1, t / p->timeout), 2), 0.95f);
|
|
p->pos += 0.05 * (myon->pos - p->pos) * cdir(sin((t - global.frames * 2) * 0.1) * M_PI/8);
|
|
p->move.velocity = 3 * myon_tail_dir(myon);
|
|
YIELD;
|
|
}
|
|
}
|
|
|
|
static void myon_spawn_trail(YoumuAMyon *myon, int t) {
|
|
cmplx pos = myon->pos + 3 * cdir(global.frames * 0.07);
|
|
real f = myon->focus_factor;
|
|
cmplx stardust_v = (2 + f) * myon_tail_dir(myon) * cdir(M_PI/16*sin(1.33*t));
|
|
|
|
if(player_should_shoot(&global.plr)) {
|
|
RNG_ARRAY(R, 7);
|
|
|
|
PARTICLE(
|
|
.angle = vrng_angle(R[2]),
|
|
.angle_delta = 0.03 * (1 - 2 * (global.frames & 1)),
|
|
.draw_rule = myon_draw_trail(f, 0.7),
|
|
.flags = PFLAG_NOREFLECT | PFLAG_MANUALANGLE,
|
|
.pos = pos + vrng_range(R[0], 0, 10) * vrng_dir(R[1]),
|
|
.scale = 0.2,
|
|
.sprite_ptr = myon->sprites.smoke,
|
|
.timeout = 30,
|
|
);
|
|
}
|
|
|
|
INVOKE_TASK(youmu_mirror_myon_trail, myon, pos);
|
|
|
|
RNG_ARRAY(R, 4);
|
|
|
|
PARTICLE(
|
|
.angle = vrng_angle(R[3]),
|
|
.angle_delta = 0.03 * (1 - 2 * (global.frames & 1)),
|
|
.draw_rule = myon_draw_trail(f, 0.5),
|
|
.flags = PFLAG_NOREFLECT | PFLAG_MANUALANGLE,
|
|
.layer = LAYER_PARTICLE_LOW | 1,
|
|
.move = move_linear(stardust_v),
|
|
.pos = pos + vrng_range(R[0], 0, 5) * vrng_dir(R[1]),
|
|
.scale = vrng_range(R[2], 0.2, 0.3),
|
|
.sprite_ptr = myon->sprites.stardust,
|
|
.timeout = 40,
|
|
);
|
|
}
|
|
|
|
static void myon_draw_proj_trail(Projectile *p, int t, ProjDrawRuleArgs args) {
|
|
float time_progress = projectile_timeout_factor(p);
|
|
float s = 2 * time_progress;
|
|
float a = min(1, s) * (1 - time_progress);
|
|
|
|
SpriteParamsBuffer spbuf;
|
|
SpriteParams sp = projectile_sprite_params(p, &spbuf);
|
|
color_mul_scalar(&spbuf.color, a);
|
|
sp.scale.as_cmplx *= s;
|
|
r_draw_sprite(&sp);
|
|
}
|
|
|
|
TASK(youmu_mirror_myon_proj, { cmplx pos; cmplx vel; real dmg; const Color *clr; ShaderProgram *shader; }) {
|
|
Projectile *p = TASK_BIND(PROJECTILE(
|
|
.color = ARGS.clr,
|
|
.damage = ARGS.dmg,
|
|
.layer = LAYER_PLAYER_SHOT | 0x10,
|
|
.move = move_linear(ARGS.vel),
|
|
.pos = ARGS.pos,
|
|
.proto = pp_youmu,
|
|
.shader_ptr = ARGS.shader,
|
|
.type = PROJ_PLAYER,
|
|
));
|
|
|
|
Color trail_color = p->color;
|
|
trail_color.a = 0;
|
|
color_mul_scalar(&trail_color, 0.075);
|
|
|
|
Sprite *trail_sprite = res_sprite("part/boss_shadow");
|
|
MoveParams trail_move = move_linear(ARGS.vel * 0.8);
|
|
|
|
for(int t = 1;; ++t) {
|
|
YIELD;
|
|
|
|
// TODO: Optimize this. The trail can be made static, either pre-rendered
|
|
// or drawn in the projectile's custom draw rule. The opacity change can
|
|
// live in the draw rule as well. Then a separate task per shot is not needed.
|
|
|
|
p->opacity = 1.0f - powf(1.0f - min(1.0f, t / 10.0f), 2.0f);
|
|
|
|
PARTICLE(
|
|
.sprite_ptr = trail_sprite,
|
|
.pos = p->pos,
|
|
.color = &trail_color,
|
|
.draw_rule = myon_draw_proj_trail,
|
|
.timeout = 10,
|
|
.move = trail_move,
|
|
.flags = PFLAG_NOREFLECT | PFLAG_MANUALANGLE,
|
|
.angle = p->angle,
|
|
.scale = 0.6,
|
|
);
|
|
}
|
|
}
|
|
|
|
static inline void youmu_mirror_myon_proj(cmplx pos, cmplx vel, real dmg, const Color *clr, ShaderProgram *shader) {
|
|
INVOKE_TASK(youmu_mirror_myon_proj, pos, vel, dmg, clr, shader);
|
|
}
|
|
|
|
static void myon_proj_color(Color *clr, real focus_factor) {
|
|
Color intermediate = { 1.0, 1.0, 1.0, 1.0 };
|
|
focus_factor = smooth(focus_factor);
|
|
|
|
if(focus_factor < 0.5) {
|
|
*clr = *RGB(0.4, 0.6, 0.6);
|
|
color_lerp(
|
|
clr,
|
|
&intermediate,
|
|
focus_factor * 2
|
|
);
|
|
} else {
|
|
Color mc;
|
|
*clr = intermediate;
|
|
color_lerp(
|
|
clr,
|
|
myon_color(&mc, focus_factor, 1, 1),
|
|
(focus_factor - 0.5) * 2
|
|
);
|
|
}
|
|
}
|
|
|
|
TASK(youmu_mirror_myon_shot, { YoumuAController *ctrl; }) {
|
|
YoumuAController *ctrl = ARGS.ctrl;
|
|
YoumuAMyon *myon = &ctrl->myon;
|
|
Player *plr = ctrl->plr;
|
|
ShaderProgram *shader = res_shader("sprite_youmu_myon_shot");
|
|
|
|
for(;;) {
|
|
WAIT_EVENT_OR_DIE(&plr->events.shoot);
|
|
|
|
const real dmg_center = SHOT_MYON_DAMAGE;
|
|
const real dmg_side = SHOT_MYON_DAMAGE;
|
|
const real speed = -10;
|
|
const int power_rank = player_get_effective_power(plr) / 100;
|
|
|
|
real spread;
|
|
cmplx forward;
|
|
Color clr;
|
|
|
|
{
|
|
myon_proj_color(&clr, myon->focus_factor);
|
|
forward = speed * myon->dir;
|
|
spread = (psin(global.frames * 1.2) * 0.5 + 0.5) * 0.1;
|
|
|
|
youmu_mirror_myon_proj(myon->pos, forward, dmg_center, &clr, shader);
|
|
|
|
if(power_rank >= 2) {
|
|
youmu_mirror_myon_proj(myon->pos, forward * cdir(+2 * spread), dmg_side, &clr, shader);
|
|
youmu_mirror_myon_proj(myon->pos, forward * cdir(-2 * spread), dmg_side, &clr, shader);
|
|
}
|
|
|
|
if(power_rank >= 4) {
|
|
youmu_mirror_myon_proj(myon->pos, forward * cdir(+4 * spread), dmg_side, &clr, shader);
|
|
youmu_mirror_myon_proj(myon->pos, forward * cdir(-4 * spread), dmg_side, &clr, shader);
|
|
}
|
|
}
|
|
|
|
WAIT(SHOT_MYON_HALFDELAY);
|
|
|
|
{
|
|
myon_proj_color(&clr, myon->focus_factor);
|
|
forward = speed * myon->dir;
|
|
spread = (psin(global.frames * 2.0) * 0.5 + 0.5) * 0.1;
|
|
|
|
if(power_rank >= 1) {
|
|
youmu_mirror_myon_proj(myon->pos, forward * cdir(+1 * spread), dmg_side, &clr, shader);
|
|
youmu_mirror_myon_proj(myon->pos, forward * cdir(-1 * spread), dmg_side, &clr, shader);
|
|
}
|
|
|
|
if(power_rank >= 3) {
|
|
youmu_mirror_myon_proj(myon->pos, forward * cdir(+3 * spread), dmg_side, &clr, shader);
|
|
youmu_mirror_myon_proj(myon->pos, forward * cdir(-3 * spread), dmg_side, &clr, shader);
|
|
}
|
|
}
|
|
|
|
WAIT(SHOT_MYON_HALFDELAY);
|
|
}
|
|
}
|
|
|
|
static cmplx fit_velocity(cmplx smoothed, int entries, cmplx history[entries]) {
|
|
cmplx bestfit = history[0];
|
|
real mindst = cabs2(bestfit - smoothed);
|
|
|
|
for(int i = 1; i < entries; ++i) {
|
|
real d = cabs2(history[i] - smoothed);
|
|
if(d < mindst) {
|
|
bestfit = history[i];
|
|
mindst = d;
|
|
}
|
|
}
|
|
|
|
return bestfit;
|
|
}
|
|
|
|
TASK(youmu_mirror_myon, { YoumuAController *ctrl; }) {
|
|
YoumuAController *ctrl = ARGS.ctrl;
|
|
YoumuAMyon *myon = &ctrl->myon;
|
|
Player *plr = ctrl->plr;
|
|
|
|
myon->sprites.trail = res_sprite("part/myon");
|
|
myon->sprites.smoke = res_sprite("part/smoke");
|
|
myon->sprites.stardust = res_sprite("part/stardust");
|
|
|
|
const real distance = 40;
|
|
const real focus_rate = 1.0/30.0;
|
|
|
|
real focus_factor = 0.0;
|
|
bool fixed_position = false;
|
|
|
|
/*
|
|
* Myon's target offset is based on a weighted average of the player's inputs
|
|
* in the last N frames. When the player stops moving, we snap the smoothed value
|
|
* to a real recent input. This allows keyboard players to aim it diagonally
|
|
* without frame-perfect inputs.
|
|
*
|
|
* This algorithm was pulled out of my ass without any theoretical justification,
|
|
* and it's probably dumb, but it works well enough so i don't care.
|
|
*
|
|
* Numpy code to generate the weights:
|
|
*
|
|
* w = np.array(range(N - 1, -1, -1))
|
|
* w = 0.5 * (np.tanh( 2*np.pi * (w/len(w) - 0.5) ) + 1)
|
|
* w /= np.sum(w)
|
|
*/
|
|
static const cmplx pvel_history_weights[12] = {
|
|
0.1807944744847358, 0.1790414880132086, 0.1742275298832384,
|
|
0.1618282865310685, 0.1345428375193760, 0.0908782920594558,
|
|
0.0472137465995357, 0.0199282975878431, 0.0075290542356732,
|
|
0.0027150961057031, 0.0009621096341759, 0.0003387873459861,
|
|
};
|
|
cmplx pvel_history[ARRAY_SIZE(pvel_history_weights)] = { };
|
|
|
|
cmplx offset_dir = -I;
|
|
|
|
myon->pos = plr->pos;
|
|
myon->dir = I;
|
|
|
|
INVOKE_SUBTASK(youmu_mirror_myon_shot, ctrl);
|
|
|
|
for(int t = 0;; ++t) {
|
|
for(int i = ARRAY_SIZE(pvel_history) - 1; i > 0; --i) {
|
|
pvel_history[i] = pvel_history[i - 1];
|
|
}
|
|
|
|
pvel_history[0] = cnormalize(plr->uncapped_velocity);
|
|
|
|
if(pvel_history[0]) {
|
|
cmplx smoothed = 0;
|
|
|
|
for(int i = 0; i < ARRAY_SIZE(pvel_history); ++i) {
|
|
smoothed += pvel_history[i] * pvel_history_weights[1];
|
|
}
|
|
|
|
smoothed = cnormalize(smoothed);
|
|
|
|
offset_dir = -smoothed;
|
|
} else if(pvel_history[1]) {
|
|
// just stopped - snap to a real recent input
|
|
offset_dir = -fit_velocity(-offset_dir, ARRAY_SIZE(pvel_history), pvel_history);
|
|
}
|
|
|
|
real follow_factor = 0.15;
|
|
|
|
if(plr->inputflags & INFLAG_FOCUS) {
|
|
fixed_position = true;
|
|
approach_p(&focus_factor, 1, focus_rate);
|
|
} else {
|
|
approach_p(&focus_factor, 0, focus_rate);
|
|
|
|
if(fixed_position) {
|
|
focus_factor = 0;
|
|
offset_dir = -I;
|
|
follow_factor *= 3;
|
|
|
|
if(plr->inputflags & INFLAGS_MOVE) {
|
|
fixed_position = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
cmplx target = plr->pos + distance * cnormalize(offset_dir);
|
|
real follow_speed = smoothmin(10, follow_factor * max(0, cabs(target - myon->pos)), 10);
|
|
cmplx v = cnormalize(target - myon->pos) * follow_speed * (1 - focus_factor) * (1 - focus_factor);
|
|
|
|
real s = sign(re(myon->pos) - re(plr->pos));
|
|
if(!s) {
|
|
s = sign(sin(t / 10.0));
|
|
}
|
|
|
|
real rot = clamp(0.005 * cabs(plr->pos - myon->pos) - M_PI/6, 0, M_PI/8);
|
|
v *= cdir(rot * s);
|
|
|
|
myon->pos += v;
|
|
myon->focus_factor = focus_factor;
|
|
|
|
if(!(plr->inputflags & INFLAG_SHOT) || !(plr->inputflags & INFLAG_FOCUS)) {
|
|
if(plr->pos != myon->pos) {
|
|
myon->dir = cnormalize(plr->pos - myon->pos);
|
|
}
|
|
}
|
|
|
|
myon_spawn_trail(myon, t);
|
|
YIELD;
|
|
}
|
|
}
|
|
|
|
static void youmu_mirror_bomb_damage_callback(EntityInterface *victim, cmplx victim_origin, void *arg) {
|
|
YoumuAController *ctrl = arg;
|
|
|
|
cmplx ofs_dir = rng_dir();
|
|
victim_origin += ofs_dir * rng_range(0, 15);
|
|
|
|
RNG_ARRAY(R, 6);
|
|
|
|
PARTICLE(
|
|
.sprite_ptr = ctrl->sprites.blast_huge_halo,
|
|
.pos = victim_origin,
|
|
.color = RGBA(vrng_range(R[0], 0.6, 0.7), 0.8, vrng_range(R[1], 0.7, 0.775), vrng_range(R[2], 0, 0.5)),
|
|
.timeout = 30,
|
|
.draw_rule = pdraw_timeout_scalefade(0, 0.5, 1, 0),
|
|
.layer = LAYER_PARTICLE_HIGH | 0x4,
|
|
.angle = vrng_angle(R[3]),
|
|
.flags = PFLAG_REQUIREDPARTICLE | PFLAG_MANUALANGLE,
|
|
);
|
|
|
|
if(global.frames & 2) {
|
|
return;
|
|
}
|
|
|
|
real t = rng_real();
|
|
RNG_NEXT(R);
|
|
|
|
PARTICLE(
|
|
.sprite_ptr = ctrl->sprites.petal,
|
|
.pos = victim_origin,
|
|
.draw_rule = pdraw_petal_random(),
|
|
.color = RGBA(sin(5*t) * t, cos(5*t) * t, 0.5 * t, 0),
|
|
.move = move_asymptotic_simple(
|
|
vrng_sign(R[0]) * vrng_range(R[1], 3, 3 + 5 * t) * cdir(M_PI*8*t),
|
|
5
|
|
),
|
|
.layer = LAYER_PARTICLE_PETAL,
|
|
);
|
|
}
|
|
|
|
static void youmu_mirror_bomb_particles(YoumuAController *ctrl, cmplx pos, cmplx vel, int t, ProjFlags pflags) {
|
|
if(t >= 240) {
|
|
return;
|
|
}
|
|
|
|
PARTICLE(
|
|
.color = RGBA(0.9, 0.8, 1.0, 0.0),
|
|
.draw_rule = pdraw_timeout_fade(1, 0),
|
|
.flags = pflags,
|
|
.move = move_linear(2 * rng_dir()),
|
|
.pos = pos,
|
|
.sprite_ptr = ctrl->sprites.arc,
|
|
.timeout = 30,
|
|
);
|
|
|
|
RNG_ARRAY(R, 2);
|
|
|
|
PARTICLE(
|
|
.angle = vrng_angle(R[0]),
|
|
.color = RGBA(0.2, 0.1, 1.0, 0.0),
|
|
.draw_rule = pdraw_timeout_scalefade(0, 3, 1, 0),
|
|
.flags = pflags,
|
|
.move = move_accelerated(
|
|
-1 * vel * cdir(0.2 * vrng_real(R[1])) / 30,
|
|
0.1 * vel * I * sin(t/4.0) / 30
|
|
),
|
|
.pos = pos,
|
|
.sprite_ptr = ctrl->sprites.stain,
|
|
.timeout = 50,
|
|
);
|
|
}
|
|
|
|
static void youmu_mirror_draw_speed_trail(Projectile *p, int t, ProjDrawRuleArgs args) {
|
|
SpriteParamsBuffer spbuf;
|
|
SpriteParams sp = projectile_sprite_params(p, &spbuf);
|
|
float nt = 1 - projectile_timeout_factor(p);
|
|
float s = 1 + (1 - nt);
|
|
sp.scale.as_cmplx *= s;
|
|
sp.rotation.angle -= M_PI/2;
|
|
spbuf.shader_params.vector[0] = -2 * nt * nt;
|
|
spbuf.color.r *= nt * nt;
|
|
spbuf.color.g *= nt * nt;
|
|
spbuf.color.b *= nt;
|
|
r_draw_sprite(&sp);
|
|
}
|
|
|
|
TASK(youmu_mirror_bomb_postprocess, { YoumuAMyon *myon; }) {
|
|
YoumuAMyon *myon = ARGS.myon;
|
|
CoEvent *pp_event = &stage_get_draw_events()->postprocess_before_overlay;
|
|
|
|
ShaderProgram *shader = res_shader("youmua_bomb");
|
|
Uniform *u_tbomb = r_shader_uniform(shader, "tbomb");
|
|
Uniform *u_myon = r_shader_uniform(shader, "myon");
|
|
Uniform *u_fill_overlay = r_shader_uniform(shader, "fill_overlay");
|
|
|
|
for(;;) {
|
|
WAIT_EVENT_OR_DIE(pp_event);
|
|
|
|
float t = player_get_bomb_progress(&global.plr);
|
|
float f = max(0, 1 - 10 * t);
|
|
cmplx myonpos = CMPLX(re(myon->pos)/VIEWPORT_W, 1 - im(myon->pos)/VIEWPORT_H);
|
|
|
|
FBPair *fbpair = stage_get_postprocess_fbpair();
|
|
r_framebuffer(fbpair->back);
|
|
|
|
r_state_push();
|
|
r_shader_ptr(shader);
|
|
r_uniform_float(u_tbomb, t);
|
|
r_uniform_vec2_complex(u_myon, myonpos);
|
|
r_uniform_vec4(u_fill_overlay, f, f, f, f);
|
|
draw_framebuffer_tex(fbpair->front, VIEWPORT_W, VIEWPORT_H);
|
|
r_state_pop();
|
|
|
|
fbpair_swap(fbpair);
|
|
}
|
|
}
|
|
|
|
TASK(youmu_mirror_bomb, { YoumuAController *ctrl; }) {
|
|
YoumuAController *ctrl = ARGS.ctrl;
|
|
YoumuAMyon *myon = &ctrl->myon;
|
|
Player *plr = ctrl->plr;
|
|
|
|
INVOKE_SUBTASK(youmu_common_bomb_background, ENT_BOX(plr), &ctrl->bomb_bg);
|
|
INVOKE_SUBTASK(youmu_mirror_bomb_postprocess, myon);
|
|
|
|
cmplx vel = -30 * myon->dir;
|
|
cmplx pos = myon->pos;
|
|
|
|
ProjFlags pflags = PFLAG_MANUALANGLE | PFLAG_NOREFLECT;
|
|
int t = 0;
|
|
|
|
ShaderProgram *silhouette_shader = res_shader("sprite_silhouette");
|
|
|
|
YIELD;
|
|
|
|
do {
|
|
pos += vel;
|
|
|
|
cmplx aim = cclampabs((plr->pos - pos) * 0.01, 1);
|
|
vel += aim;
|
|
vel *= 1 - cabs(plr->pos - pos) * 0.0001;
|
|
|
|
youmu_mirror_bomb_particles(ctrl, pos, vel, t, pflags);
|
|
pflags ^= PFLAG_REQUIREDPARTICLE;
|
|
|
|
// roughly matches the shader effect
|
|
real bombtime = player_get_bomb_progress(plr);
|
|
real envelope = bombtime * (1 - bombtime);
|
|
real range = 200 / (1 + pow(0.08 / envelope, 5));
|
|
|
|
ent_area_damage(pos, range, &(DamageInfo) { 200, DMG_PLAYER_BOMB }, youmu_mirror_bomb_damage_callback, ctrl);
|
|
stage_clear_hazards_at(pos, range, CLEAR_HAZARDS_ALL | CLEAR_HAZARDS_NOW);
|
|
|
|
myon->pos = pos;
|
|
myon->dir = cnormalize(vel);
|
|
++t;
|
|
|
|
PARTICLE(
|
|
.color = RGBA(0.5, 1, 1, 0),
|
|
.draw_rule = youmu_mirror_draw_speed_trail,
|
|
.flags = PFLAG_NOREFLECT,
|
|
.layer = LAYER_PARTICLE_HIGH,
|
|
.pos = plr->pos,
|
|
.shader_ptr = silhouette_shader,
|
|
.sprite_ptr = aniplayer_get_frame(&plr->ani),
|
|
.timeout = 15,
|
|
);
|
|
|
|
YIELD;
|
|
} while(player_is_bomb_active(plr));
|
|
}
|
|
|
|
TASK(youmu_mirror_bomb_handler, { YoumuAController *ctrl; }) {
|
|
YoumuAController *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_youmu_b");
|
|
bomb_task = cotask_box(INVOKE_SUBTASK(youmu_mirror_bomb, ctrl));
|
|
}
|
|
}
|
|
|
|
TASK(youmu_mirror_shot_forward, { YoumuAController *ctrl; }) {
|
|
YoumuAController *ctrl = ARGS.ctrl;
|
|
Player *plr = ctrl->plr;
|
|
ShaderProgram *shader = res_shader("sprite_particle");
|
|
|
|
for(int t = 0;;) {
|
|
WAIT_EVENT_OR_DIE(&plr->events.shoot);
|
|
play_sfx_loop("generic_shot");
|
|
|
|
cmplx v = -20 * I;
|
|
int power_rank = player_get_effective_power(plr) / 100;
|
|
|
|
real spread = M_PI/64 * (1 + 0.5 * psin(t/15.0));
|
|
|
|
for(int side = -1; side < 2; side += 2) {
|
|
cmplx origin = plr->pos + 10*side + 5*I;
|
|
|
|
youmu_common_shot(origin, move_linear(v), SHOT_SELF_DAMAGE, shader);
|
|
|
|
for(int p = 0; p < power_rank; ++p) {
|
|
cmplx v2 = -(20 - p) * I * cdir(side * (1 + p) * spread);
|
|
|
|
youmu_common_shot(
|
|
origin,
|
|
move_asymptotic_halflife(
|
|
0.2 * v2 * cdir(M_PI * 0.25 * side),
|
|
v2 * cdir(M_PI * -0.02 * side),
|
|
5
|
|
), SHOT_SELF_DAMAGE, shader
|
|
);
|
|
}
|
|
}
|
|
|
|
t += WAIT(SHOT_SELF_DELAY);
|
|
}
|
|
|
|
}
|
|
|
|
TASK(youmu_mirror_controller, { BoxedPlayer plr; }) {
|
|
YoumuAController *ctrl = TASK_MALLOC(sizeof(*ctrl));
|
|
ctrl->plr = TASK_BIND(ARGS.plr);
|
|
ctrl->sprites.arc = res_sprite("part/arc");
|
|
ctrl->sprites.stain = res_sprite("part/stain");
|
|
ctrl->sprites.petal = res_sprite("part/petal");
|
|
ctrl->sprites.blast_huge_halo = res_sprite("part/blast_huge_halo");
|
|
|
|
youmu_common_init_bomb_background(&ctrl->bomb_bg);
|
|
|
|
INVOKE_SUBTASK(youmu_mirror_shot_forward, ctrl);
|
|
INVOKE_SUBTASK(youmu_mirror_myon, ctrl);
|
|
INVOKE_SUBTASK(youmu_mirror_bomb_handler, ctrl);
|
|
|
|
STALL;
|
|
}
|
|
|
|
static void youmu_mirror_init(Player *plr) {
|
|
INVOKE_TASK(youmu_mirror_controller, ENT_BOX(plr));
|
|
}
|
|
|
|
static void youmu_mirror_preload(ResourceGroup *rg) {
|
|
const int flags = RESF_DEFAULT;
|
|
|
|
res_group_preload(rg, RES_SPRITE, flags,
|
|
"part/arc",
|
|
"part/blast_huge_halo",
|
|
"part/myon",
|
|
"part/petal",
|
|
"part/stain",
|
|
"part/stardust",
|
|
"proj/youmu",
|
|
NULL);
|
|
|
|
res_group_preload(rg, RES_TEXTURE, flags,
|
|
"youmu_bombbg1",
|
|
NULL);
|
|
|
|
res_group_preload(rg, RES_SHADER_PROGRAM, flags,
|
|
"sprite_youmu_myon_shot",
|
|
"youmu_bomb_bg",
|
|
"youmua_bomb",
|
|
NULL);
|
|
|
|
res_group_preload(rg, RES_SFX, flags | RESF_OPTIONAL,
|
|
"bomb_youmu_b",
|
|
NULL);
|
|
}
|
|
|
|
static double youmu_mirror_property(Player *plr, PlrProperty prop) {
|
|
switch(prop) {
|
|
case PLR_PROP_SPEED: {
|
|
double s = youmu_common_property(plr, prop);
|
|
|
|
if(player_is_bomb_active(plr)) {
|
|
s *= 2.0;
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
default:
|
|
return youmu_common_property(plr, prop);
|
|
}
|
|
}
|
|
|
|
PlayerMode plrmode_youmu_a = {
|
|
.name = "Soul Reflection",
|
|
.description = "Human and phantom act together towards a singular purpose. Your inner duality shall lend you a hand… or a tail.",
|
|
.spellcard_name = "Soul Sign “Reality-Piercing Apparition”",
|
|
.character = &character_youmu,
|
|
.dialog = &dialog_tasks_youmu,
|
|
.shot_mode = PLR_SHOT_YOUMU_MIRROR,
|
|
.procs = {
|
|
.init = youmu_mirror_init,
|
|
.preload = youmu_mirror_preload,
|
|
.property = youmu_mirror_property,
|
|
},
|
|
};
|