Stage 2 revamp (#229)

* stage2: remake wriggle non

* stage2: coroutinize first enemy

* stage2: coroutinize small_spin_circle (renamed redwall_fairy)

* stage2: remake aimshot fairies

* stage2: remove dead code

* stage2: blind-coroutinize flea-shooting fairy; not tested at all

* stage2: some hina nonspell shenanigans

* stage2: coroutinize amulet of harm (very lazily)

* stage2: lame hina nons (no balance)

* stage2: remake wheel of fortune (lunatic tuning only for now)

* stage2: Add raz's Hina nonspell

* coroutinize bad pick

* bad pick tweaks

* coroutinize and remake Monty Hall Danmaku

* stage2: coroutinize turning fairies

* it's big brain time

* look ma, i'm doing level design!

* stage2 second half (WIP)

* stage2: use new enemy spawners

* stage2: remove dead code

* stage2: fix post-midboss turning fairies

* stage2: swap post-midboss red/blue aimshot fairies order

* stage2: amulet of harm redesign prototype

* Amulet of Harm tweaks:

	* Less erratic boss movement
	* Improved "amulet" visuals

* stage2: very bare-bones difficulty scaling (enemies only)

* stage2: wheel of fortune balance tweaks

* stage2: amulet of harm difficulty balancing

* stage2: add hina spin animation to amulet of harm

* stage2: remove dead code
This commit is contained in:
Andrei Alexeyev 2020-12-01 19:25:00 +02:00 committed by GitHub
parent 9b5d515721
commit 48f0a21e81
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1799 additions and 727 deletions

View file

@ -254,6 +254,14 @@ DEFINE_EXTERN_TASK(common_set_bitflags) {
*ARGS.pflags = ((*ARGS.pflags & ARGS.mask) | ARGS.set);
}
DEFINE_EXTERN_TASK(common_kill_projectile) {
kill_projectile(TASK_BIND(ARGS.proj));
}
DEFINE_EXTERN_TASK(common_kill_enemy) {
enemy_kill(TASK_BIND(ARGS.enemy));
}
cmplx common_wander(cmplx origin, double dist, Rect bounds) {
int attempts = 32;
double angle;

View file

@ -98,4 +98,18 @@ DECLARE_EXTERN_TASK(
}
);
DECLARE_EXTERN_TASK(
common_kill_projectile,
{
BoxedProjectile proj;
}
);
DECLARE_EXTERN_TASK(
common_kill_enemy,
{
BoxedEnemy enemy;
}
);
#endif // IGUARD_common_tasks_h

View file

@ -16,5 +16,6 @@
DECLARE_EXTERN_TASK_WITH_INTERFACE(stage1_midboss_nonspell_1, BossAttack);
DECLARE_EXTERN_TASK_WITH_INTERFACE(stage1_boss_nonspell_1, BossAttack);
DECLARE_EXTERN_TASK_WITH_INTERFACE(stage1_boss_nonspell_2, BossAttack);
DECLARE_EXTERN_TASK_WITH_INTERFACE(stage1_boss_nonspell_3, BossAttack);
#endif // IGUARD_stages_stage1_nonspells_nonspells_h

View file

@ -5,6 +5,7 @@ stage2_src = files(
'hina.c',
'nonspells/boss_nonspell_1.c',
'nonspells/boss_nonspell_2.c',
'nonspells/boss_nonspell_3.c',
'nonspells/midboss_nonspell_1.c',
'spells/amulet_of_harm.c',
'spells/bad_pick.c',

View file

@ -11,20 +11,55 @@
#include "nonspells.h"
#include "global.h"
#include "common_tasks.h"
void hina_cards1(Boss *h, int time) {
int t = time % 500;
TIMER(&t);
TASK(wander, { BoxedBoss boss; }) {
Rect wander_bounds = viewport_bounds(80);
wander_bounds.bottom = 130;
if(time < 0)
return;
Boss *boss = TASK_BIND(ARGS.boss);
boss->move = move_towards(boss->pos, 0.03);
AT(0) {
aniplayer_queue(&h->ani, "guruguru", 0);
}
FROM_TO(0, 500, 2-(global.diff > D_Normal)) {
play_sound_ex("shot1", 4, false);
PROJECTILE(.proto = pp_card, .pos = h->pos+50*cexp(I*t/10), .color = RGB(0.8,0.0,0.0), .rule = asymptotic, .args = { (1.6+0.4*global.diff)*cexp(I*t/5.0), 3 });
PROJECTILE(.proto = pp_card, .pos = h->pos-50*cexp(I*t/10), .color = RGB(0.0,0.0,0.8), .rule = asymptotic, .args = {-(1.6+0.4*global.diff)*cexp(I*t/5.0), 3 });
for(;;WAIT(300)) {
boss->move.attraction_point = common_wander(boss->pos, 60, wander_bounds);
}
}
DEFINE_EXTERN_TASK(stage2_boss_nonspell_1) {
STAGE_BOOKMARK(boss-non1);
Boss *boss = INIT_BOSS_ATTACK();
BEGIN_BOSS_ATTACK();
INVOKE_SUBTASK_DELAYED(420, wander, ENT_BOX(boss));
aniplayer_queue(&boss->ani, "guruguru", 0);
int step = difficulty_value(3, 2, 1, 1);
real speed0 = difficulty_value(2.0, 2.4, 2.8, 3.2);
real speed1 = 0.2; // difficulty_value(0.1, 0.1, 0.1, 0.2);
for(int t = 0;; t += WAIT(step)) {
// play_sfx_ex("shot1", 4, false);
play_sfx_loop("shot1_loop");
cmplx dir = cdir(t / 5.0);
cmplx ofs = 50 * cdir(t / 10.0);
real hl = 30 + 50 * psin(t/60.0);
PROJECTILE(
.proto = pp_card,
.pos = boss->pos + ofs,
.color = RGB(0.8, 0.0, 0.0),
.move = move_asymptotic_halflife(speed1 * dir, speed0 * dir * 2, hl),
);
PROJECTILE(
.proto = pp_card,
.pos = boss->pos - ofs,
.color = RGB(0.0, 0.0, 0.8),
.move = move_asymptotic_halflife(speed0 * -dir, speed1 * -dir, hl),
);
}
}

View file

@ -11,34 +11,93 @@
#include "nonspells.h"
#include "global.h"
#include "common_tasks.h"
void hina_cards2(Boss *h, int time) {
int t = time % 500;
TIMER(&t);
TASK(speen, { BoxedBoss boss; }) {
Boss *boss = TASK_BIND(ARGS.boss);
aniplayer_queue(&boss->ani, "guruguru", 0);
if(time < 0)
return;
boss->move = (MoveParams) { 0 };
cmplx opos = boss->pos;
hina_cards1(h, time);
for(int t = 0;; t += WAIT(1)) {
cmplx target = VIEWPORT_W/2 + 150*I + cwmul(cdir(t*M_TAU/150), 200 + 42*I);
boss->pos = clerp(opos, target, smoothmin(t / 300.0, 1.0, 0.5));
}
}
GO_AT(h, 100, 200, 2);
GO_AT(h, 260, 460, -2);
GO_AT(h, 460, 500, 5);
TASK(balls, { BoxedBoss boss; }) {
Boss *boss = TASK_BIND(ARGS.boss);
for(;;) {
WAIT(75);
play_sound("shot_special1");
play_sound("redirect");
cmplx aim = cnormalize(global.plr.pos - boss->pos);
int n = 12;
for(int i = 0; i < n; ++i) {
cmplx d = aim * cdir(i * M_TAU / n);
AT(100) {
int i;
for(i = 0; i < 30; i++) {
play_sound("shot_special1");
PROJECTILE(
.proto = pp_bigball,
.pos = h->pos,
.color = RGB(0.7, 0, 0.7),
.rule = asymptotic,
.args = {
2*cexp(I*2*M_PI*i/20.0),
3
}
.pos = boss->pos,
.color = RGB(0.8, 0.0, 0.8),
.move = move_asymptotic_halflife(8 * d, 3 * d, 60),
);
int m = 8;
for(int j = 0; j < m; ++j) {
real s = (m - j) / (real)m;
PROJECTILE(
.proto = pp_ball,
.pos = boss->pos,
.color = j & 1 ? RGBA(0.8, 0.0, 0.0, 0.0) : RGBA(0.0, 0.0, 0.8, 0.0),
.move = move_asymptotic_halflife((8 - 3 * s) * d, 3 * d, 60),
);
}
}
}
}
DEFINE_EXTERN_TASK(stage2_boss_nonspell_2) {
STAGE_BOOKMARK(boss-non2);
Boss *boss = INIT_BOSS_ATTACK();
boss->move = move_towards(VIEWPORT_W/2 + 100*I, 0.01);
BEGIN_BOSS_ATTACK();
INVOKE_SUBTASK(speen, ENT_BOX(boss));
INVOKE_SUBTASK_DELAYED(150, balls, ENT_BOX(boss));
int step = difficulty_value(3, 2, 1, 1);
real speed0 = difficulty_value(2.0, 2.4, 2.8, 3.2);
real speed1 = 0.2; // difficulty_value(0.1, 0.1, 0.1, 0.2);
for(int t = 0;; t += WAIT(step)) {
// play_sfx_ex("shot1", 4, false);
play_sfx_loop("shot1_loop");
cmplx dir = cdir(t / 5.0);
cmplx ofs = 50 * cdir(t / 10.0);
real hl = 30 + 50 * psin(t/60.0);
PROJECTILE(
.proto = pp_card,
.pos = boss->pos + ofs,
.color = RGB(0.8, 0.0, 0.0),
.move = move_asymptotic_halflife(speed1 * dir, speed0 * dir * 2, hl),
);
PROJECTILE(
.proto = pp_card,
.pos = boss->pos - ofs,
.color = RGB(0.0, 0.0, 0.8),
.move = move_asymptotic_halflife(speed1 * -dir, speed0 * -dir * 2, hl),
);
}
STALL;
}

View file

@ -0,0 +1,216 @@
/*
* This software is licensed under the terms of the MIT License.
* See COPYING for further information.
* ---
* Copyright (c) 2011-2019, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2019, Andrei Alexeyev <akari@taisei-project.org>.
*/
#include "taisei.h"
#include "nonspells.h"
#include "../hina.h"
#include "common_tasks.h"
#include "global.h"
// Pattern contributed by raz, originally written for danmakufu
// The port is almost direct, and a bit rough
TASK(TColumn, { BoxedBoss boss; cmplx offset; cmplx aim; int num; real rank; }) {
Boss *boss = TASK_BIND(ARGS.boss);
// NOTE: bullet flash warning dummied out, replaced with simple wait
WAIT(20);
int num = ARGS.num;
real rank = ARGS.rank;
cmplx shot_origin = boss->pos + ARGS.offset;
for(int i = 0; i < num; ++i) {
real spd = rank * (3.75 + (1.875 * i) / num);
cmplx aim = ARGS.aim * cdir(M_PI/180 * rng_sreal());
play_sfx_loop("shot1_loop");
PROJECTILE(
.proto = pp_card,
.color = RGB(1, 0, 0),
.move = move_asymptotic_simple(spd * aim, 2),
.pos = shot_origin,
);
WAIT(5);
}
}
TASK(XPattern, { BoxedBoss boss; real dir_sign; int num2; int num3; }) {
Boss *boss = TASK_BIND(ARGS.boss);
real rank = difficulty_value(0.4, 0.65, 0.85, 1);
int rof = lround(3 / rank);
int num = 180 * rank;
int num2 = ARGS.num2 * rank;
int num3 = ARGS.num3 * rank;
real odd = 1;
real dir_sign = ARGS.dir_sign;
cmplx rot = cnormalize(global.plr.pos - boss->pos) * cdir(M_PI/2 * -dir_sign);
for(int i = 0; i < num2; ++i) {
for(int j = 0; j < 3; ++j) {
cmplx dir = rot * cdir(dir_sign * (i * M_TAU*2 / (num - 1.0) - odd * (M_PI/6 + M_PI/3 * j)));
INVOKE_SUBTASK(TColumn,
.boss = ENT_BOX(boss),
.offset = 80 * dir,
.aim = dir * cdir(M_PI/4 * dir_sign * odd),
.num = num3,
.rank = rank
);
}
odd = -odd;
WAIT(rof);
}
}
TASK(TShoot, { BoxedBoss boss; real ang; real ang_s; real dist; real spd_inc; real dir_sign; int t; int time; int num; }) {
Boss *boss = TASK_BIND(ARGS.boss);
real ang = ARGS.ang;
real ang_s = ARGS.ang_s;
real dist = ARGS.dist;
real spd_inc = ARGS.spd_inc;
real dir_sign = ARGS.dir_sign;
int t = ARGS.t;
int time = ARGS.time;
int num = ARGS.num;
for(int i = 0; i < num; ++i) {
real ang2 = ang - ang_s * i * dir_sign;
cmplx shot_origin = boss->pos + cdir(ang2) * dist;
real spd = (1.875 - 0.625 * (i / num)) + (spd_inc * t) / time;
cmplx dir = cdir(ang2 - M_PI/2 * dir_sign);
bool phase2 = t > 0.6 * time;
play_sfx(phase2 ? "shot1" : "shot2");
PROJECTILE(
.proto = phase2 ? pp_crystal : pp_card,
.color = RGB(t / (real)time, 0, 1),
.pos = shot_origin,
.move = move_linear(spd * dir),
);
WAIT(3);
}
}
TASK(SpinPattern, { BoxedBoss boss; real dir_sign; }) {
Boss *boss = TASK_BIND(ARGS.boss);
int rof = difficulty_value(12, 5, 4, 3);
real spd_inc = difficulty_value(1.25, 1.875, 2.5, 3.125);
int time = 240;
int num = 3;
real dir_sign = ARGS.dir_sign;
real ang = M_PI/2 * (1 - dir_sign);
real ang_d = dir_sign * rof * (2*M_TAU - M_PI/2) / time;
real ang_s = M_TAU/3;
real ang_s_d = -(M_TAU/3) / (time / (real)rof);
real dist = 20;
real dist_d = 320.0 / time;
// NOTE: bullet flash warning dummied out, replaced with simple wait
WAIT(20);
for(int t = 0; t < time;) {
INVOKE_SUBTASK(TShoot,
.boss = ENT_BOX(boss),
.ang = ang,
.ang_s = ang_s,
.dist = dist,
.spd_inc = spd_inc,
.dir_sign = dir_sign,
.t = t,
.time = time,
.num = num
);
ang_s += ang_s_d;
dist += dist_d;
ang += ang_d;
t += WAIT(rof);
}
}
static cmplx random_boss_pos(Boss *boss) {
Rect bounds = viewport_bounds(140);
bounds.top = 140;
bounds.bottom = 200;
return common_wander(boss->pos, 60, bounds);
// return VIEWPORT_W/2 + (rng_sreal() * 40 + I * rng_range(165, 180));
}
static void random_move(Boss *boss) {
boss->move.attraction_point = random_boss_pos(boss);
aniplayer_soft_switch(&boss->ani, "guruguru", 1);
aniplayer_queue(&boss->ani, "main", 0);
}
DEFINE_EXTERN_TASK(stage2_boss_nonspell_3) {
Boss *boss = INIT_BOSS_ATTACK();
boss->move = move_towards(VIEWPORT_W/2.0 + 100.0*I, 0.02);
BEGIN_BOSS_ATTACK();
real dir_sign = rng_sign();
random_move(boss);
WAIT(20);
INVOKE_SUBTASK(SpinPattern, ENT_BOX(boss), dir_sign);
WAIT(30);
INVOKE_SUBTASK(SpinPattern, ENT_BOX(boss), dir_sign);
WAIT(30);
INVOKE_SUBTASK(SpinPattern, ENT_BOX(boss), dir_sign);
WAIT(210);
INVOKE_SUBTASK(XPattern,
.boss = ENT_BOX(boss),
.dir_sign = dir_sign,
.num2 = 42,
.num3 = 6
);
for(;;) {
dir_sign = -dir_sign;
random_move(boss);
WAIT(80);
INVOKE_SUBTASK(XPattern,
.boss = ENT_BOX(boss),
.dir_sign = dir_sign,
.num2 = 42,
.num3 = 6
);
INVOKE_SUBTASK(SpinPattern, ENT_BOX(boss), dir_sign);
WAIT(30);
INVOKE_SUBTASK(SpinPattern, ENT_BOX(boss), dir_sign);
WAIT(30);
INVOKE_SUBTASK(SpinPattern, ENT_BOX(boss), dir_sign);
WAIT(180);
INVOKE_SUBTASK(XPattern,
.boss = ENT_BOX(boss),
.dir_sign = dir_sign,
.num2 = 42,
.num3 = 9
);
}
}

View file

@ -11,61 +11,138 @@
#include "nonspells.h"
#include "global.h"
#include "common_tasks.h"
static int wriggle_bug(Projectile *p, int t) {
if(t < 0) {
return ACTION_ACK;
TASK(spawn_bugs, { BoxedBoss boss; BoxedProjectileArray *bugs; }) {
Boss *boss = TASK_BIND(ARGS.boss);
real speed = difficulty_value(2, 2, 3, 2.25);
int delay = difficulty_value(3, 2, 1, 1);
for(int i = 0;; ++i) {
play_sfx_loop("shot1_loop");
for(int j = 0; j < 2; ++j) {
cmplx dir = cdir(i * M_TAU / 20 + M_PI * j);
ENT_ARRAY_ADD(ARGS.bugs, PROJECTILE(
.proto = pp_rice,
.pos = boss->pos + dir * 60,
.color = RGB(1.0, 0.5, 0.2),
.move = move_linear(speed * dir),
.max_viewport_dist = 120,
));
}
WAIT(delay);
}
p->pos += p->args[0];
p->angle = carg(p->args[0]);
if(global.boss && global.boss->current && !((global.frames - global.boss->current->starttime - 30) % 200)) {
play_sound("redirect");
p->args[0] *= cexp(I*(M_PI/3)*nfrand());
spawn_projectile_highlight_effect(p);
}
return ACTION_NONE;
}
void wriggle_small_storm(Boss *w, int time) {
int t = time % 400;
TIMER(&t);
TASK(scatter_bugs, { BoxedProjectileArray *bugs; }) {
DECLARE_ENT_ARRAY(Projectile, bugs, ARGS.bugs->size);
ENT_ARRAY_FOREACH(ARGS.bugs, Projectile *bug, {
ENT_ARRAY_ADD(&bugs, bug);
});
if(time < 0)
return;
int p = difficulty_value(3, 4, 6, 8);
cmplx rot = cdir(M_TAU / difficulty_value(20, 20, 15, 10));
FROM_TO_SND("shot1_loop", 0,400,5-global.diff) {
PROJECTILE(.proto = pp_rice, .pos = w->pos, .color = RGB(1,0.5,0.2), .rule = wriggle_bug, .args = { 2*cexp(I*_i*2*M_PI/20) });
PROJECTILE(.proto = pp_rice, .pos = w->pos, .color = RGB(1,0.5,0.2), .rule = wriggle_bug, .args = { 2*cexp(I*_i*2*M_PI/20+I*M_PI) });
}
for(int i = bugs.size - 1; i >= 0; --i) {
Projectile *bug = ENT_UNBOX(bugs.array[i]);
GO_AT(w, 60, 120, 1)
GO_AT(w, 180, 240, -1)
if(bug) {
cmplx v = 1.1 * bug->move.velocity;
if(!((t+200-15)%200)) {
aniplayer_queue(&w->ani,"specialshot_charge",1);
aniplayer_queue(&w->ani,"specialshot_hold",20);
aniplayer_queue(&w->ani,"specialshot_release",1);
aniplayer_queue(&w->ani,"main",0);
}
if(!(t%200)) {
int i;
play_sound("shot_special1");
for(i = 0; i < 10+global.diff; i++) {
PROJECTILE(
.proto = pp_bigball,
.pos = w->pos,
.color = RGB(0.1,0.3,0.0),
.rule = asymptotic,
.args = {
2*cexp(I*i*2*M_PI/(10+global.diff)),
2
}
.proto = pp_thickrice,
.pos = bug->pos,
.color = RGB(0.2, 1.0, 0.2),
.move = move_asymptotic_simple(v * rot, -4)
);
PROJECTILE(
.proto = pp_thickrice,
.pos = bug->pos,
.color = RGB(0.2, 1.0, 0.2),
.move = move_asymptotic_simple(v / rot, -4)
);
kill_projectile(bug);
}
if(i % p == 0) {
play_sound("redirect");
YIELD;
}
}
}
static void wriggle_anim_begin_charge(Boss *boss) {
aniplayer_soft_switch(&boss->ani, "specialshot_charge", 1);
aniplayer_queue(&boss->ani, "specialshot_hold", 0);
}
static void wriggle_anim_end_charge(Boss *boss) {
aniplayer_soft_switch(&boss->ani, "specialshot_release", 1);
aniplayer_queue(&boss->ani, "main", 0);
}
DEFINE_EXTERN_TASK(stage2_midboss_nonspell_1) {
STAGE_BOOKMARK(non1);
Boss *boss = INIT_BOSS_ATTACK();
boss->move = move_towards(VIEWPORT_W/2 + 100.0*I, 0.02);
BEGIN_BOSS_ATTACK();
DECLARE_ENT_ARRAY(Projectile, bugs, 512);
INVOKE_SUBTASK(spawn_bugs, ENT_BOX(boss), &bugs);
Rect wander_bounds = viewport_bounds(80);
wander_bounds.bottom = 180;
for(;;) {
// boss->move.attraction_point = common_wander(boss->pos, 20, wander_bounds);
WAIT(60);
boss->move.attraction_point = common_wander(boss->pos, 20, wander_bounds);
int delay = 110;
int charge_time = 60;
WAIT(delay - charge_time);
wriggle_anim_begin_charge(boss);
INVOKE_SUBTASK(common_charge,
.anchor = &boss->pos,
.color = RGBA(0.2, 1.0, 0.2, 0),
.time = charge_time,
.sound = COMMON_CHARGE_SOUNDS
);
WAIT(charge_time);
wriggle_anim_end_charge(boss);
INVOKE_TASK(scatter_bugs, &bugs);
ENT_ARRAY_CLEAR(&bugs);
WAIT(30);
play_sound("shot_special1");
int n = 20;
int m = difficulty_value(1, 3, 4, 5);
real a = 0.05;
for(int i = 0; i < n; i++) {
for(int j = 0; j < m; ++j) {
PROJECTILE(
.proto = pp_bigball,
.pos = boss->pos,
.color = RGB(0.1, 0.3, 0.0),
.move = move_accelerated(0, a * (1 + j) * cdir((i + 0.5) * M_TAU/n - M_PI/2)),
);
}
}
boss->move.attraction_point = common_wander(boss->pos, 120, wander_bounds);
}
STALL;
}

View file

@ -13,8 +13,12 @@
#include "boss.h"
void wriggle_small_storm(Boss *w, int time);
void hina_cards1(Boss *h, int time);
void hina_cards2(Boss *h, int time);
DECLARE_EXTERN_TASK_WITH_INTERFACE(stage2_midboss_nonspell_1, BossAttack);
DECLARE_EXTERN_TASK_WITH_INTERFACE(stage2_boss_nonspell_1, BossAttack);
DECLARE_EXTERN_TASK_WITH_INTERFACE(stage2_boss_nonspell_2, BossAttack);
DECLARE_EXTERN_TASK_WITH_INTERFACE(stage2_boss_nonspell_3, BossAttack);
#endif // IGUARD_stages_stage2_nonspells_nonspells_h

View file

@ -11,36 +11,206 @@
#include "spells.h"
#include "global.h"
#include "common_tasks.h"
void hina_amulet(Boss *h, int time) {
int t = time % 200;
TASK(spinner_bullet_redirect, { BoxedProjectile p; MoveParams move; }) {
Projectile *p = TASK_BIND(ARGS.p);
cmplx ov = p->move.velocity;
p->move = ARGS.move;
p->move.velocity += ov;
}
if(time < 0)
return;
static void amulet_visual(Enemy *e, int t, bool render) {
if(render) {
r_draw_sprite(&(SpriteParams) {
.color = RGBA(2, 1, 1, 0),
.sprite = "fairy_circle_big",
.pos.as_cmplx = e->pos,
.rotation.angle = t * 5 * DEG2RAD,
});
if(time < 100)
GO_TO(h, VIEWPORT_W/2 + 200.0*I, 0.02);
TIMER(&t);
cmplx d = global.plr.pos - h->pos;
int loopduration = 200*(global.diff+0.5)/(D_Lunatic+0.5);
AT(0) {
aniplayer_queue_frames(&h->ani,"guruguru",loopduration);
}
FROM_TO_SND("shot1_loop", 0,loopduration,1) {
float f = _i/30.0;
cmplx n = cexp(I*2*M_PI*f+I*carg(d)+0.7*time/200*I)/sqrt(0.5+global.diff);
float speed = 1.0 + 0.75 * imax(0, global.diff - D_Normal);
float accel = 1.0 + 1.20 * imax(0, global.diff - D_Normal);
cmplx p = h->pos+30*log(1+_i/2.0)*n;
ProjPrototype *t0 = pp_ball;
ProjPrototype *t1 = global.diff == D_Easy ? t0 : pp_crystal;
PROJECTILE(.proto = t0, .pos = p, .color = RGB(0.8, 0.0, 0.0), .rule = accelerated, .args = { speed * 2*n*I, accel * -0.01*n });
PROJECTILE(.proto = t1, .pos = p, .color = RGB(0.8, 0.0, 0.5), .rule = accelerated, .args = { speed * -2*n*I, accel * -0.01*n });
r_draw_sprite(&(SpriteParams) {
.sprite = "enemy/swirl",
.pos.as_cmplx = e->pos,
.rotation.angle = t * -10 * DEG2RAD,
});
}
}
TASK(amulet_fire_spinners, { BoxedEnemy core; BoxedProjectileArray *spinners; }) {
Enemy *core = TASK_BIND(ARGS.core);
int nshots = difficulty_value(1, 1, 3, 69);
int charge_time = difficulty_value(90, 75, 60, 60);
real accel = difficulty_value(0.05, 0.06, 0.085, 0.085);
for(int i = 0; i < nshots; ++i) {
WAIT(60);
common_charge(charge_time, &core->pos, 0, RGBA(0.6, 0.2, 0.5, 0));
ENT_ARRAY_FOREACH(ARGS.spinners, Projectile *p, {
int cnt = difficulty_value(12, 16, 22, 24);
for(int i = 0; i < cnt; ++i) {
cmplx ca = circle_dir(i, cnt);
cmplx o = p->pos + 42 * ca;
cmplx aim = cnormalize(o - core->pos);
Projectile *c = PROJECTILE(
.proto = pp_crystal,
.pos = p->pos,
.color = RGB(0.2 + 0.8 * tanh(cabs2(o - core->pos) / 4200), 0.2, 1.0),
.max_viewport_dist = p->max_viewport_dist,
.move = move_towards(o, 0.02),
);
INVOKE_TASK_DELAYED(42, spinner_bullet_redirect, ENT_BOX(c), move_accelerated(0, aim * accel));
}
});
}
WAIT(60);
enemy_kill(core);
}
TASK(amulet, {
cmplx pos;
MoveParams move;
CoEvent *death_event;
}) {
Enemy *core = create_enemy_p(&global.enemies, ARGS.pos, 2000, amulet_visual, NULL, 0, 0, 0, 0);
core->hurt_radius = 18;
core->hit_radius = 36;
core->flags |= EFLAG_NO_VISUAL_CORRECTION;
core->move = ARGS.move;
INVOKE_TASK_AFTER(NOT_NULL(ARGS.death_event), common_kill_enemy, ENT_BOX(core));
int num_spinners = difficulty_value(2, 3, 4, 4);
real spinner_ofs = 32;
cmplx spin = cdir(M_PI/8);
DECLARE_ENT_ARRAY(Projectile, spinners, num_spinners);
cmplx spinner_offsets[num_spinners];
cmplx init_orientation = cnormalize(core->move.velocity);
for(int i = 0; i < num_spinners; ++i) {
spinner_offsets[i] = spinner_ofs * cdir(i * M_TAU / num_spinners) * init_orientation;
Projectile *p = PROJECTILE(
.proto = pp_ball,
.pos = core->pos,
.color = RGB(0.8, 0.2, 1.0),
.max_viewport_dist = spinner_ofs * 2,
.flags = PFLAG_NOCLEAR,
);
INVOKE_TASK_AFTER(&core->events.killed, common_kill_projectile, ENT_BOX(p));
ENT_ARRAY_ADD(&spinners, p);
}
INVOKE_SUBTASK(amulet_fire_spinners, ENT_BOX(core), &spinners);
for(;;YIELD) {
int live = 0;
ENT_ARRAY_FOREACH_COUNTER(&spinners, int i, Projectile *p, {
p->pos = core->pos + spinner_offsets[i];
spinner_offsets[i] *= spin;
++live;
});
if(!live) {
enemy_kill(core);
return;
}
real s = approach_asymptotic(carg(spin), -M_PI/32, 0.02, 1e-5);
spin = cabs(spin) * cdir(s);
}
}
static void fan_burst_side(cmplx o, cmplx ofs, real x) {
int cnt = difficulty_value(2, 3, 3, 3);
real speed = difficulty_value(2, 2.5, 3, 3);
real sat = 0.75;
for(int i = 0; i < cnt; ++i) {
real s = speed * (1 + 0.1 * i);
PROJECTILE(
.pos = o + ofs,
.color = RGB(sat * 1, sat * 0.25 * i / (cnt - 1.0), 0),
.proto = pp_card,
.move = move_asymptotic_simple(s * I*cnormalize(ofs) * cdir(x * (i+1) * -0.3), 3 - 0.1 * i),
);
PROJECTILE(
.pos = o + ofs,
.color = RGB(0, sat * 0.25 * i / (cnt - 1.0), sat * 1),
.proto = pp_card,
.move = move_asymptotic_simple(s * cnormalize(ofs)/I * cdir(x * (i+1) * 0.3), 3 - 0.1 * i),
);
}
}
TASK(fan_burst, { Boss *boss; }) {
int nbursts = difficulty_value(1, 1, 2, 2);
for(int burst = 0; burst < nbursts; ++burst) {
int cnt = 30;
for(int i = 0; i < cnt; ++i, WAIT(3)) {
play_sfx_loop("shot1_loop");
cmplx o = 32;
real u = 32;
real x = i / (cnt - 1.0);
o += sin(4 * -x * M_PI) * u;
o += cos(4 * -x * 1.23 * M_PI) * I * u;
fan_burst_side(ARGS.boss->pos, o, x);
fan_burst_side(ARGS.boss->pos, -conj(o), x);
}
WAIT(30);
}
}
DEFINE_EXTERN_TASK(stage2_spell_amulet_of_harm) {
Boss *boss = INIT_BOSS_ATTACK();
boss->move = move_towards(VIEWPORT_W/2 + 200.0*I, 0.02);
BEGIN_BOSS_ATTACK();
Rect wander_bounds = viewport_bounds(64);
wander_bounds.top += 64;
wander_bounds.bottom = VIEWPORT_H * 0.4;
for(;;) {
boss->move.attraction_point = common_wander(boss->pos, VIEWPORT_W * 0.3, wander_bounds);
WAIT(20);
int cnt = 3;
real arange = M_PI/3;
for(int i = 0; i < cnt; ++i) {
real s = 4 * fmax(2, log1p(cabs(boss->move.velocity)));
cmplx dir = cnormalize(-boss->move.velocity) * cdir(arange * (i / (cnt - 1.0) - 0.5));
play_sfx("redirect");
INVOKE_TASK(amulet, boss->pos, move_asymptotic_halflife(s * dir, 0, 12), &ARGS.attack->events.finished);
WAIT(15);
}
WAIT(40);
aniplayer_soft_switch(&boss->ani, "guruguru", 0);
WAIT(20);
boss->move.attraction_point = common_wander(boss->pos, VIEWPORT_W * 0.25, wander_bounds);
play_sfx("shot_special1");
INVOKE_SUBTASK(fan_burst, boss);
WAIT(120);
aniplayer_soft_switch(&boss->ani, "main", 0);
}
}

View file

@ -11,99 +11,155 @@
#include "spells.h"
#include "global.h"
#include "common_tasks.h"
#define SLOTS 5
static int bad_pick_bullet(Projectile *p, int t) {
if(t < 0) {
return ACTION_ACK;
}
p->pos += p->args[0];
p->args[0] += p->args[1];
// reflection
int slot = (int)(creal(p->pos)/(VIEWPORT_W/SLOTS)+1)-1; // correct rounding for slot == -1
int targetslot = creal(p->args[2])+0.5;
if(slot != targetslot)
p->args[0] = copysign(creal(p->args[0]),targetslot-slot)+I*cimag(p->args[0]);
return ACTION_NONE;
static int slot_of_position(cmplx pos) {
return (int)(creal(pos) / (VIEWPORT_W / SLOTS) + 1) - 1; // correct rounding for slot == -1
}
void hina_bad_pick(Boss *h, int time) {
int t = time % 500;
int i, j;
TASK(bad_pick_bullet, { cmplx pos; cmplx vel; cmplx accel; int target_slot; }) {
Projectile *p = TASK_BIND(PROJECTILE(
.proto = pp_ball,
.pos = ARGS.pos,
.color = RGBA(0.7, 0.0, 0.0, 0.0),
.move = move_accelerated(ARGS.vel, ARGS.accel),
));
TIMER(&t);
for(;;YIELD) {
// reflection
int slot = slot_of_position(p->pos);
if(time < 0)
return;
if(slot != ARGS.target_slot) {
p->move.velocity = copysign(creal(p->move.velocity), ARGS.target_slot - slot) + I*cimag(p->move.velocity);
}
}
}
GO_TO(h, VIEWPORT_W/SLOTS*((113*(time/500))%SLOTS+0.5)+ 100.0*I, 0.02);
static cmplx pick_boss_position(void) {
return VIEWPORT_W/SLOTS * (rng_irange(0, SLOTS) + 0.5) + 100 * I;
}
FROM_TO(100, 500, 5) {
play_sound_ex("shot1", 4, false);
TASK(walls, { int duration; }) {
for(int t = 0; t < ARGS.duration; t += WAIT(5)) {
play_sfx_loop("shot1_loop");
for(i = 1; i < SLOTS; i++) {
for(int i = 1; i < SLOTS; i++) {
PROJECTILE(
.proto = pp_crystal,
.pos = VIEWPORT_W/SLOTS*i,
.color = RGB(0.5,0,0.6),
.rule = linear,
.args = { 7.0*I }
.pos = VIEWPORT_W / SLOTS * i,
.color = RGB(0.5, 0, 0.6),
.move = move_linear(7*I),
);
}
if(global.diff >= D_Hard) {
double shift = 0;
if(global.diff == D_Lunatic)
shift = 0.3*fmax(0,t-200);
for(i = 1; i < SLOTS; i++) {
double height = VIEWPORT_H/SLOTS*i+shift;
if(height > VIEWPORT_H-40)
height -= VIEWPORT_H-40;
real shift = 0;
if(global.diff == D_Lunatic) {
shift = 0.3 * fmax(0, t - 100);
}
for(int i = 1; i < SLOTS; i++) {
real height = VIEWPORT_H / SLOTS * i + shift;
if(height > VIEWPORT_H - 40) {
height -= VIEWPORT_H -40;
}
PROJECTILE(
.proto = pp_crystal,
.pos = (i&1)*VIEWPORT_W+I*height,
.color = RGB(0.5,0,0.6),
.rule = linear,
.args = { 5.0*(1-2*(i&1)) }
);
}
}
}
AT(190) {
aniplayer_queue(&h->ani,"guruguru",2);
aniplayer_queue(&h->ani,"main",0);
}
AT(200) {
play_sound("shot_special1");
int win = tsrand()%SLOTS;
for(i = 0; i < SLOTS; i++) {
if(i == win)
continue;
float cnt = (1+imin(D_Normal,global.diff)) * 5;
for(j = 0; j < cnt; j++) {
cmplx o = VIEWPORT_W/SLOTS*(i + j/(cnt-1));
PROJECTILE(
.proto = pp_ball,
.pos = o,
.color = RGBA(0.7, 0.0, 0.0, 0.0),
.rule = bad_pick_bullet,
.args = {
0,
0.005*nfrand() + 0.005*I * (1 + psin(i + j + global.frames)),
i
},
.pos = (i & 1) * VIEWPORT_W + I * height,
.color = RGB(0.5, 0, 0.6),
.move = move_linear(5.0 * (1 - 2 * (i & 1))),
);
}
}
}
}
DEFINE_EXTERN_TASK(stage2_spell_bad_pick) {
Boss *boss = INIT_BOSS_ATTACK();
boss->move = move_towards(pick_boss_position(), 0.02);
BEGIN_BOSS_ATTACK();
int balls_per_slot = difficulty_value(15, 15, 20, 25);
for(int t = 0;;) {
t += WAIT(100);
INVOKE_SUBTASK(walls, 400);
t += WAIT(30);
int win;
int plr_slot = slot_of_position(global.plr.pos);
do {
win = rng_irange(0, SLOTS);
} while(win == plr_slot && global.diff > D_Easy);
for(int i = 0; i < SLOTS; i++) {
if(i == win) {
continue;
}
INVOKE_SUBTASK(common_charge,
.pos = VIEWPORT_W / (real)SLOTS * (i + 0.5),
.color = RGBA(1.0, 0.1, 0.1, 0),
.time = 65,
.sound = COMMON_CHARGE_SOUNDS
);
if(i == plr_slot && global.diff > D_Normal) {
INVOKE_SUBTASK(common_charge,
.pos = VIEWPORT_W / (real)SLOTS * (i + 0.5) + VIEWPORT_H*I,
.color = RGBA(0.2, 0.2, 1.0, 0),
.time = 65,
.sound = COMMON_CHARGE_SOUNDS
);
}
}
t += WAIT(60);
aniplayer_queue(&boss->ani, "guruguru", 2);
aniplayer_queue(&boss->ani, "main", 0);
t += WAIT(10);
play_sfx("shot_special1");
for(int i = 0; i < SLOTS; i++) {
if(i == win) {
continue;
}
for(int j = 0; j < balls_per_slot; j++) {
INVOKE_TASK(bad_pick_bullet,
.pos = VIEWPORT_W / (real)SLOTS * (i + 0.1 + 0.8 * j / (balls_per_slot - 1.0)),
.vel = 0,
.accel = 0.005 * (rng_sreal() + I * (1 + psin(i + j + t))),
.target_slot = i
);
}
if(i == plr_slot && global.diff > D_Normal) {
for(int j = 0; j < balls_per_slot; j++) {
Projectile *p = PROJECTILE(
.proto = pp_ball,
.pos = VIEWPORT_W / (real)SLOTS * (i + 0.1 + 0.8 * j / (balls_per_slot - 1.0)) + VIEWPORT_H*I,
.color = RGBA(0.2, 0.2, 0.7, 0.0),
.move = move_accelerated(0, 0),
);
INVOKE_TASK(common_move,
.ent = ENT_BOX(p).as_generic,
.pos = &p->move.acceleration,
.move_params = move_linear(0.000001 * (rng_sreal() - I * (1 + psin(i + j + t))))
);
}
}
}
t += WAIT(300);
boss->move.attraction_point = pick_boss_position();
}
}

View file

@ -11,239 +11,198 @@
#include "spells.h"
#include "global.h"
#include "common_tasks.h"
static int timeout_deadproj_linear(Projectile *p, int time) {
// TODO: maybe add a "clear on timeout" flag?
#define NUM_SLOTS 3
#define SLOT_WIDTH (VIEWPORT_W / (real)NUM_SLOTS)
if(time < 0) {
return ACTION_ACK;
}
static void goat_bullets(
int slot,
int t,
int cnt,
bool top,
ProjFlags flags,
BoxedProjectileArray *array
) {
for(int i = 0; i < cnt; i++) {
cmplx o = !top*VIEWPORT_H*I + SLOT_WIDTH * (slot + i/(cnt - 1.0));
cmplx a = 0.007 * (sin((M_PI * 4 * i / (cnt - 1))) * 0.1 * global.diff - I * (1 + psin(i + t)));
if(time > creal(p->args[0])) {
p->type = PROJ_DEAD;
}
if(top) {
a *= -0.5;
}
p->pos += p->args[1];
p->angle = carg(p->args[1]);
return ACTION_NONE;
}
static int hina_monty_slave(Enemy *s, int time) {
if(time < 0) {
return 1;
}
if(time > 60 && time < 720-140 + 20*(global.diff-D_Lunatic) && !(time % (int)(fmax(2 + (global.diff < D_Normal), (120 - 0.5 * time))))) {
play_sfx_loop("shot1_loop");
PROJECTILE(
.proto = pp_crystal,
.pos = s->pos,
.color = RGB(0.5 + 0.5 * psin(time*0.2), 0.3, 1.0 - 0.5 * psin(time*0.2)),
.rule = asymptotic,
.args = {
5*I + 1 * (sin(time) + I * cos(time)),
4
}
Projectile *p = PROJECTILE(
.proto = pp_ball,
.pos = o,
.color = top ? RGBA(0, 0, 0.7, 0) : RGBA(0.7, 0, 0, 0),
.move = move_accelerated(a, a),
.flags = flags,
);
if(global.diff > D_Easy) {
PROJECTILE(
.proto = pp_crystal,
.pos = s->pos,
.color = RGB(0.5 + 0.5 * psin(time*0.2), 0.3, 1.0 - 0.5 * psin(time*0.2)),
.rule = timeout_deadproj_linear,
.args = {
500,
-0.5*I + 1 * (sin(time) + I * cos(time))
}
);
if(array) {
ENT_ARRAY_ADD(array, p);
}
}
return 1;
}
static void hina_monty_slave_visual(Enemy *s, int time, bool render) {
Swirl(s, time, render);
TASK(goat, { int slot; CoEvent *activation_event; }) {
int cnt = difficulty_value(15, 20, 25, 30);
int slot = ARGS.slot;
int charge_time = 60;
int period = 60;
int timeout = 300;
if(!render) {
return;
bool top_enabled = global.diff > D_Hard;
INVOKE_SUBTASK(common_charge,
.pos = SLOT_WIDTH * (slot + 0.5) + VIEWPORT_H*I,
.color = RGBA(1.0, 0.2, 0.2, 0),
.time = charge_time,
.sound = COMMON_CHARGE_SOUNDS
);
if(top_enabled) {
INVOKE_SUBTASK(common_charge,
.pos = SLOT_WIDTH * (slot + 0.5),
.color = RGBA(0.2, 0.2, 1.0, 0),
.time = charge_time,
.sound = COMMON_CHARGE_SOUNDS
);
}
Sprite *soul = get_sprite("proj/soul");
double scale = fabs(swing(clamp(time / 60.0, 0, 1), 3)) * 1.25;
WAIT(charge_time);
play_sfx("shot_special1");
Color *clr1 = RGBA(1.0, 0.0, 0.0, 0.0);
Color *clr2 = RGBA(0.0, 0.0, 1.0, 0.0);
Color *clr3 = RGBA(psin(time*0.05), 0.0, 1.0 - psin(time*0.05), 0.0);
DECLARE_ENT_ARRAY(Projectile, initial_bullets_bottom, cnt);
DECLARE_ENT_ARRAY(Projectile, initial_bullets_top, cnt);
r_mat_mv_push();
r_mat_mv_translate(creal(s->pos), cimag(s->pos), 0);
r_shader("sprite_bullet");
goat_bullets(slot, 0, cnt, false, PFLAG_NOMOVE, &initial_bullets_bottom);
r_draw_sprite(&(SpriteParams) {
.sprite_ptr = soul,
.rotation.angle = time * DEG2RAD,
.scale.x = scale * (0.6 + 0.6 * psin(time*0.1)),
.scale.y = scale * (0.7 + 0.5 * psin(time*0.1 + M_PI)),
.color = clr1,
if(top_enabled) {
goat_bullets(slot, period, cnt, true, PFLAG_NOMOVE, &initial_bullets_top);
}
WAIT_EVENT(ARGS.activation_event);
int t = 0;
play_sfx("redirect");
ENT_ARRAY_FOREACH(&initial_bullets_bottom, Projectile *p, {
p->flags &= ~PFLAG_NOMOVE;
});
r_draw_sprite(&(SpriteParams) {
.sprite_ptr = soul,
.rotation.angle = time * DEG2RAD,
.scale.x = scale * (0.7 + 0.5 * psin(time*0.1 + M_PI)),
.scale.y = scale * (0.6 + 0.6 * psin(time*0.1)),
.color = clr2,
});
t += WAIT(period);
r_draw_sprite(&(SpriteParams) {
.sprite_ptr = soul,
.rotation.angle = -time * DEG2RAD,
.scale.both = scale,
.color = clr3,
});
if(top_enabled) {
play_sfx("redirect");
ENT_ARRAY_FOREACH(&initial_bullets_top, Projectile *p, {
p->flags &= ~PFLAG_NOMOVE;
});
r_mat_mv_pop();
t += WAIT(period);
}
for(bool top = false; t < timeout; t += WAIT(period), top = !top) {
play_sfx("shot_special1");
goat_bullets(slot, t, cnt, top && top_enabled, 0, NULL);
}
}
void hina_monty(Boss *h, int time) {
int t = time % 720;
TIMER(&t);
TASK(cards, { BoxedBoss boss; }) {
Boss *boss = TASK_BIND(ARGS.boss);
static short slave_pos, bad_pos, good_pos, plr_pos;
static int cwidth = VIEWPORT_W / 3.0;
static cmplx targetpos;
int cnt = 10;
int period = 30;
int period_reduction = global.diff;
real side = rng_sign();
real w = SLOT_WIDTH * 0.5 * difficulty_value(1.1, 1.25, 1.5, 1.5);
real ofs = (SLOT_WIDTH * 0.5 - w);
if(time == EVENT_DEATH) {
enemy_kill_all(&global.enemies);
return;
for(int t = 0; t < 360;) {
for(int i = 0; i < cnt; ++i) {
play_sfx("shot3");
cmplx o = creal(boss->pos) + side * (ofs + (w*i)/cnt) + I*cimag(boss->pos);
PROJECTILE(
.proto = pp_card,
.pos = o,
.color = RGB(1.0, 0.0, 1.0),
.move = move_accelerated(-3*I, 0.06*I),
);
t += WAIT(1);
}
side = -side;
t += WAIT(period);
period = imax(1, period - period_reduction);
}
}
if(time < 0) {
targetpos = VIEWPORT_W/2.0 + VIEWPORT_H/2.0 * I;
return;
}
DEFINE_EXTERN_TASK(stage2_spell_monty_hall_danmaku) {
Boss *boss = INIT_BOSS_ATTACK();
AT(0) {
plr_pos = creal(global.plr.pos) / cwidth;
bad_pos = tsrand() % 3;
do good_pos = tsrand() % 3; while(good_pos == bad_pos);
COEVENTS_ARRAY(goat_trigger) events;
TASK_HOST_EVENTS(events);
play_sound("laser1");
boss->move = move_towards(VIEWPORT_W/2.0 + VIEWPORT_H/2.0 * I, 0.06);
BEGIN_BOSS_ATTACK();
int plr_slot;
int goat1_slot;
int goat2_slot;
int win_slot;
for(;;) {
boss->move.attraction_point = VIEWPORT_W/2.0 + VIEWPORT_H/2.0 * I;
goat1_slot = rng_irange(0, 3);
do {
win_slot = rng_irange(0, 3);
} while(win_slot == goat1_slot);
goat2_slot = 0;
while(goat2_slot == win_slot || goat2_slot == goat1_slot) {
goat2_slot++;
}
assert(goat2_slot < 3);
for(int i = 0; i < 2; ++i) {
int x = cwidth * (1 + i);
create_laserline_ab(x, x + VIEWPORT_H