Improve (totally rewrite) projectile collision detection and graze mechanics (#122)
* improve (totally rewrite) projectile collision detection and graze mechanics * fix lineseg_circle_intersect as per lao's suggestion * adjust hit"boxes" * fix false hits on inductive resonanse (bad lerp) * initialize projectile prevpos on spawn * pp_basic_init_projectile: always override the projectile's sprite when setting the prototype
This commit is contained in:
parent
280b0739d8
commit
ef37b22d6d
18 changed files with 351 additions and 164 deletions
95
src/laser.c
95
src/laser.c
|
@ -246,6 +246,8 @@ bool clear_laser(Laser **laserlist, Laser *l, bool force, bool now) {
|
|||
return true;
|
||||
}
|
||||
|
||||
static bool collision_laser_curve(Laser *l);
|
||||
|
||||
void process_lasers(void) {
|
||||
Laser *laser = global.lasers, *del = NULL;
|
||||
|
||||
|
@ -294,96 +296,53 @@ void process_lasers(void) {
|
|||
}
|
||||
}
|
||||
|
||||
// what this terrible function probably does:
|
||||
// is the point of shortest distance between the line through a and b
|
||||
// and a point c between a and b and closer than r? if yes return f so that a+f*(b-a) is that point.
|
||||
// otherwise return -1.
|
||||
double collision_line(complex a, complex b, complex c, float r) {
|
||||
complex m,v;
|
||||
double projection, lv, lm, distance;
|
||||
|
||||
m = b-a; // vector pointing along the line
|
||||
v = a-c; // vector from collider to point A
|
||||
|
||||
lv = cabs(v);
|
||||
lm = cabs(m);
|
||||
if(lm == 0) {
|
||||
return -1;
|
||||
static bool collision_laser_curve(Laser *l) {
|
||||
if(l->width <= 3.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
projection = -creal(v*conj(m))/lm; // project v onto the line
|
||||
|
||||
// now the distance can be calculated by Pythagoras
|
||||
distance = sqrt(lv*lv-projection*projection);
|
||||
if(distance <= r) {
|
||||
double f = projection/lm;
|
||||
if(f >= 0 && f <= 1) // it’s on the line!
|
||||
return f;
|
||||
}
|
||||
|
||||
// now here is an additional check that becomes important if we use
|
||||
// this to check collisions along a curve made out of multiple straight
|
||||
// line segments.
|
||||
// Imagine approaching a convex corner of the curve from outside. With
|
||||
// just the above check there is a triangular death zone. This check
|
||||
// here puts a circle in that zone.
|
||||
if(lv < r)
|
||||
return 0;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int collision_laser_curve(Laser *l) {
|
||||
float t_end = (global.frames - l->birthtime)*l->speed + l->timeshift; // end of the laser based on length
|
||||
float t_death = l->deathtime*l->speed+l->timeshift; // end of the laser based on lifetime
|
||||
float t_end = (global.frames - l->birthtime) * l->speed + l->timeshift; // end of the laser based on length
|
||||
float t_death = l->deathtime * l->speed + l->timeshift; // end of the laser based on lifetime
|
||||
float t = t_end - l->timespan;
|
||||
complex last, pos;
|
||||
|
||||
bool grazed = false;
|
||||
|
||||
if(l->width <= 3.0)
|
||||
return 0;
|
||||
|
||||
if(t < 0)
|
||||
if(t < 0) {
|
||||
t = 0;
|
||||
}
|
||||
|
||||
//float t_start = t;
|
||||
|
||||
last = l->prule(l,t);
|
||||
LineSegment segment = { .a = l->prule(l,t) };
|
||||
Circle collision_area = { .origin = global.plr.pos };
|
||||
|
||||
for(t += l->collision_step; t <= min(t_end,t_death); t += l->collision_step) {
|
||||
pos = l->prule(l,t);
|
||||
float t1 = t-l->timespan/2; // i have no idea
|
||||
float tail = l->timespan/1.9;
|
||||
float t1 = t - l->timespan / 2; // i have no idea
|
||||
float tail = l->timespan / 1.9;
|
||||
float widthfac = -0.75 / pow(tail, 2) * (t1 - tail) * (t1 + tail);
|
||||
widthfac = max(0.25, pow(widthfac, l->width_exponent));
|
||||
|
||||
float widthfac = -0.75/pow(tail,2)*(t1-tail)*(t1+tail);
|
||||
segment.b = l->prule(l, t);
|
||||
collision_area.radius = widthfac * l->width * 0.5 + 1;
|
||||
|
||||
//float widthfac = -(t-t_start)*(t-min(t_end,t_death))/pow((min(t_end,t_death)-t_start)/2.,2);
|
||||
widthfac = max(0.25,pow(widthfac, l->width_exponent));
|
||||
|
||||
float collision_width = widthfac*l->width*0.5+1;
|
||||
|
||||
if(collision_line(last, pos, global.plr.pos, collision_width) >= 0) {
|
||||
return 1;
|
||||
if(lineseg_circle_intersect(segment, collision_area) >= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(!grazed && !(global.frames % 7) && global.frames - abs(global.plr.recovery) > 0) {
|
||||
float f = collision_line(last, pos, global.plr.pos, l->width*2+8);
|
||||
collision_area.radius = l->width * 2+8;
|
||||
float f = lineseg_circle_intersect(segment, collision_area);
|
||||
|
||||
if(f >= 0) {
|
||||
player_graze(&global.plr, last+f*(pos-last), 7, 5);
|
||||
player_graze(&global.plr, segment.a + f * (segment.b - segment.a), 7, 5);
|
||||
grazed = true;
|
||||
}
|
||||
}
|
||||
|
||||
last = pos;
|
||||
segment.a = segment.b;
|
||||
}
|
||||
|
||||
pos = l->prule(l, min(t_end, t_death));
|
||||
segment.b = l->prule(l, min(t_end, t_death));
|
||||
collision_area.radius = l->width * 0.5; // WTF: what is this sorcery?
|
||||
|
||||
if(collision_line(last, pos, global.plr.pos, l->width*0.5) >= 0)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
return lineseg_circle_intersect(segment, collision_area) >= 0;
|
||||
}
|
||||
|
||||
complex las_linear(Laser *l, float t) {
|
||||
|
|
|
@ -64,9 +64,6 @@ Laser *create_laser(complex pos, float time, float deathtime, Color color, Laser
|
|||
void delete_lasers(void);
|
||||
void process_lasers(void);
|
||||
|
||||
int collision_laser_line(Laser *l);
|
||||
int collision_laser_curve(Laser *l);
|
||||
|
||||
bool clear_laser(Laser **laserlist, Laser *l, bool force, bool now);
|
||||
|
||||
complex las_linear(Laser *l, float t);
|
||||
|
|
11
src/player.c
11
src/player.c
|
@ -114,6 +114,8 @@ void player_move(Player *plr, complex delta) {
|
|||
if(cabs(realdir)) {
|
||||
plr->lastmovedir = realdir / cabs(realdir);
|
||||
}
|
||||
|
||||
plr->velocity = realdir;
|
||||
}
|
||||
|
||||
static void ent_draw_player(EntityInterface *ent) {
|
||||
|
@ -704,6 +706,8 @@ static void player_ani_moving(Player *plr, int dir) {
|
|||
}
|
||||
|
||||
void player_applymovement(Player *plr) {
|
||||
plr->velocity = 0;
|
||||
|
||||
if(plr->deathtime < -1)
|
||||
return;
|
||||
|
||||
|
@ -722,8 +726,10 @@ void player_applymovement(Player *plr) {
|
|||
} else {
|
||||
player_ani_moving(plr,0);
|
||||
}
|
||||
if(gamepad)
|
||||
|
||||
if(gamepad) {
|
||||
return;
|
||||
}
|
||||
|
||||
complex direction = 0;
|
||||
|
||||
|
@ -799,8 +805,9 @@ void player_graze(Player *plr, complex pos, int pts, int effect_intensity) {
|
|||
.rule = linear,
|
||||
.timeout = 5 + 5 * afrand(2),
|
||||
.draw_rule = Shrink,
|
||||
.args = { 0, (1+afrand(0)*5)*cexp(I*M_PI*2*afrand(1)) },
|
||||
.args = { (1+afrand(0)*5)*cexp(I*M_PI*2*afrand(1)) },
|
||||
.flags = PFLAG_NOREFLECT,
|
||||
.layer = LAYER_PARTICLE_LOW,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@ struct Player {
|
|||
ENTITY_INTERFACE_NAMED(Player, ent);
|
||||
|
||||
complex pos;
|
||||
complex velocity;
|
||||
complex deathpos;
|
||||
short focus;
|
||||
|
||||
|
|
|
@ -159,6 +159,15 @@ void projectile_set_prototype(Projectile *p, ProjPrototype *proto) {
|
|||
p->proto = proto;
|
||||
}
|
||||
|
||||
complex projectile_graze_size(Projectile *p) {
|
||||
if(p->type == EnemyProj && !(p->flags & PFLAG_NOGRAZE) && p->graze_counter < 5) {
|
||||
complex s = (p->size * 420 /* graze it */) / (2 * p->graze_counter + 1);
|
||||
return sqrt(creal(s)) + sqrt(cimag(s)) * I;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Projectile* _create_projectile(ProjArgs *args) {
|
||||
if(IN_DRAW_CODE) {
|
||||
log_fatal("Tried to spawn a projectile while in drawing code");
|
||||
|
@ -167,7 +176,7 @@ static Projectile* _create_projectile(ProjArgs *args) {
|
|||
Projectile *p = (Projectile*)objpool_acquire(stage_object_pools.projectiles);
|
||||
|
||||
p->birthtime = global.frames;
|
||||
p->pos = p->pos0 = args->pos;
|
||||
p->pos = p->pos0 = p->prevpos = args->pos;
|
||||
p->angle = args->angle;
|
||||
p->rule = args->rule;
|
||||
p->draw_rule = args->draw_rule;
|
||||
|
@ -177,9 +186,9 @@ static Projectile* _create_projectile(ProjArgs *args) {
|
|||
p->sprite = args->sprite_ptr;
|
||||
p->type = args->type;
|
||||
p->color = args->color;
|
||||
p->grazed = (bool)(args->flags & PFLAG_NOGRAZE);
|
||||
p->max_viewport_dist = args->max_viewport_dist;
|
||||
p->size = args->size;
|
||||
p->collision_size = args->collision_size;
|
||||
p->flags = args->flags;
|
||||
p->timeout = args->timeout;
|
||||
|
||||
|
@ -189,6 +198,8 @@ static Projectile* _create_projectile(ProjArgs *args) {
|
|||
p->ent.draw_func = ent_draw_projectile;
|
||||
|
||||
projectile_set_prototype(p, args->proto);
|
||||
// p->collision_size *= 10;
|
||||
// p->size *= 5;
|
||||
|
||||
if((p->type == EnemyProj || p->type >= PlrProj) && (creal(p->size) <= 0 || cimag(p->size) <= 0)) {
|
||||
// FIXME: debug info is not actually attached at this point!
|
||||
|
@ -283,23 +294,28 @@ void calc_projectile_collision(Projectile *p, ProjCollisionResult *out_col) {
|
|||
out_col->damage = 0;
|
||||
|
||||
if(p->type == EnemyProj) {
|
||||
double w, h;
|
||||
projectile_size(p, &w, &h);
|
||||
Ellipse e_proj = {
|
||||
.axes = p->collision_size,
|
||||
.angle = p->angle + M_PI/2,
|
||||
};
|
||||
|
||||
double angle = carg(global.plr.pos - p->pos) + p->angle;
|
||||
double projr = sqrt(pow(w/2*cos(angle), 2) + pow(h/2*sin(angle), 2)) * 0.45;
|
||||
double grazer = max(w, h);
|
||||
double dst = cabs(global.plr.pos - p->pos);
|
||||
grazer = (0.9 * sqrt(grazer) + 0.1 * grazer) * 6;
|
||||
LineSegment seg = {
|
||||
.a = global.plr.pos - global.plr.velocity - p->prevpos,
|
||||
.b = global.plr.pos - p->pos
|
||||
};
|
||||
|
||||
if(dst < projr + 1) {
|
||||
if(lineseg_ellipse_intersect(seg, e_proj)) {
|
||||
out_col->type = PCOL_PLAYER;
|
||||
out_col->entity = &global.plr;
|
||||
out_col->fatal = true;
|
||||
} else if(!p->grazed && dst < grazer && global.frames - abs(global.plr.recovery) > 0) {
|
||||
out_col->location = p->pos - grazer * 0.3 * cexp(I*carg(p->pos - global.plr.pos));
|
||||
out_col->type = PCOL_PLAYER_GRAZE;
|
||||
out_col->entity = &global.plr;
|
||||
} else {
|
||||
e_proj.axes = projectile_graze_size(p);
|
||||
|
||||
if(creal(e_proj.axes) > 1 && lineseg_ellipse_intersect(seg, e_proj)) {
|
||||
out_col->type = PCOL_PLAYER_GRAZE;
|
||||
out_col->entity = &global.plr;
|
||||
out_col->location = global.plr.pos;
|
||||
}
|
||||
}
|
||||
} else if(p->type >= PlrProj) {
|
||||
int damage = p->type - PlrProj;
|
||||
|
@ -346,14 +362,15 @@ void apply_projectile_collision(Projectile **projlist, Projectile *p, ProjCollis
|
|||
}
|
||||
|
||||
case PCOL_PLAYER_GRAZE: {
|
||||
p->grazed = true;
|
||||
|
||||
if(p->flags & PFLAG_GRAZESPAM) {
|
||||
player_graze(col->entity, col->location, 10, 2);
|
||||
} else {
|
||||
player_graze(col->entity, col->location, 50, 5);
|
||||
player_graze(col->entity, col->location, 10 + 10 * p->graze_counter, 3 + p->graze_counter);
|
||||
}
|
||||
|
||||
p->graze_counter++;
|
||||
p->graze_counter_reset_timer = global.frames;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -507,8 +524,14 @@ void process_projectiles(Projectile **projs, bool collision) {
|
|||
|
||||
for(Projectile *proj = *projs, *next; proj; proj = next) {
|
||||
next = proj->next;
|
||||
proj->prevpos = proj->pos;
|
||||
action = proj_call_rule(proj, global.frames - proj->birthtime);
|
||||
|
||||
if(proj->graze_counter && proj->graze_counter_reset_timer - global.frames <= -90) {
|
||||
proj->graze_counter--;
|
||||
proj->graze_counter_reset_timer = global.frames;
|
||||
}
|
||||
|
||||
if(proj->type == DeadProj && killed < 5) {
|
||||
killed++;
|
||||
action = ACTION_DESTROY;
|
||||
|
|
|
@ -69,7 +69,9 @@ struct Projectile {
|
|||
|
||||
complex pos;
|
||||
complex pos0;
|
||||
complex size;
|
||||
complex prevpos; // used to lerp trajectory for collision detection; set this to pos if you intend to "teleport" the projectile in the rule!
|
||||
complex size; // affects out-of-viewport culling and grazing
|
||||
complex collision_size; // affects collision with player (TODO: make this work for player projectiles too?)
|
||||
complex args[RULE_ARGC];
|
||||
ProjRule rule;
|
||||
ProjDrawRule draw_rule;
|
||||
|
@ -84,7 +86,9 @@ struct Projectile {
|
|||
ProjType type;
|
||||
int max_viewport_dist;
|
||||
ProjFlags flags;
|
||||
bool grazed;
|
||||
|
||||
short graze_counter;
|
||||
int graze_counter_reset_timer;
|
||||
|
||||
// XXX: this is in frames of course, but needs to be float
|
||||
// to avoid subtle truncation and integer division gotchas.
|
||||
|
@ -112,7 +116,8 @@ typedef struct ProjArgs {
|
|||
Projectile **dest;
|
||||
ProjType type;
|
||||
Sprite *sprite_ptr;
|
||||
complex size;
|
||||
complex size; // affects default draw order, out-of-viewport culling, and grazing
|
||||
complex collision_size; // affects collision with player (TODO: make this work for player projectiles too?)
|
||||
int max_viewport_dist;
|
||||
drawlayer_t layer;
|
||||
|
||||
|
@ -207,5 +212,4 @@ void Blast(Projectile *p, int t);
|
|||
|
||||
void projectiles_preload(void);
|
||||
|
||||
List* proj_insert_sizeprio(List **dest, List *elem) attr_hot;
|
||||
List* proj_insert_colorprio(List **dest, List *elem);
|
||||
complex projectile_graze_size(Projectile *p);
|
||||
|
|
|
@ -16,6 +16,7 @@ typedef struct PPBasicPriv {
|
|||
const char *sprite_name;
|
||||
Sprite *sprite;
|
||||
complex size;
|
||||
complex collision_size;
|
||||
} PPBasicPriv;
|
||||
|
||||
static void pp_basic_preload(ProjPrototype *proto) {
|
||||
|
@ -26,27 +27,32 @@ static void pp_basic_preload(ProjPrototype *proto) {
|
|||
static void pp_basic_init_projectile(ProjPrototype *proto, Projectile *p) {
|
||||
PPBasicPriv *pdata = proto->private;
|
||||
|
||||
if(!p->sprite) {
|
||||
p->sprite = pdata->sprite
|
||||
? pdata->sprite
|
||||
: (pdata->sprite = get_sprite(pdata->sprite_name));
|
||||
}
|
||||
p->sprite = pdata->sprite
|
||||
? pdata->sprite
|
||||
: (pdata->sprite = get_sprite(pdata->sprite_name));
|
||||
|
||||
p->size = pdata->size;
|
||||
p->collision_size = pdata->collision_size;
|
||||
}
|
||||
|
||||
#define PP_BASIC(sprite, width, height) \
|
||||
#define _PP_BASIC(sprite, width, height, colwidth, colheight) \
|
||||
ProjPrototype _pp_##sprite = { \
|
||||
.preload = pp_basic_preload, \
|
||||
.init_projectile = pp_basic_init_projectile, \
|
||||
.private = (&(PPBasicPriv) { \
|
||||
.sprite_name = "proj/" #sprite, \
|
||||
.size = (width) + (height) * I, \
|
||||
.collision_size = (colwidth) + (colheight) * I, \
|
||||
}), \
|
||||
}, *pp_##sprite = &_pp_##sprite; \
|
||||
|
||||
#define PP_BASIC(sprite, width, height, colwidth, colheight) _PP_BASIC(sprite, width, height, colwidth, colheight)
|
||||
#include "projectile_prototypes/basic.inc.h"
|
||||
|
||||
// TODO: customize defaults
|
||||
#define PP_PLAYER(sprite, width, height) _PP_BASIC(sprite, width, height, 0, 0)
|
||||
#include "projectile_prototypes/player.inc.h"
|
||||
|
||||
static void pp_blast_init_projectile(ProjPrototype *proto, Projectile *p) {
|
||||
pp_basic_init_projectile(proto, p);
|
||||
assert(p->rule == NULL);
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
#define PP_BASIC(name, w, h) PP(name)
|
||||
|
||||
#define PP_BASIC(name, w, h, cw, ch) PP(name)
|
||||
#include "basic.inc.h"
|
||||
|
||||
#define PP_PLAYER(name, w, h) PP(name)
|
||||
#include "player.inc.h"
|
||||
|
||||
PP(blast)
|
||||
|
||||
#undef PP
|
||||
|
|
|
@ -1,23 +1,16 @@
|
|||
PP_BASIC(apple, 32, 32)
|
||||
PP_BASIC(ball, 28, 28)
|
||||
PP_BASIC(bigball, 40, 40)
|
||||
PP_BASIC(bullet, 12, 20)
|
||||
PP_BASIC(card, 17, 20)
|
||||
PP_BASIC(crystal, 10, 20)
|
||||
PP_BASIC(flea, 12, 12)
|
||||
PP_BASIC(plainball, 25, 25)
|
||||
PP_BASIC(rice, 10, 20)
|
||||
PP_BASIC(soul, 80, 80)
|
||||
PP_BASIC(thickrice, 10, 14)
|
||||
PP_BASIC(wave, 25, 25)
|
||||
|
||||
// FIXME: these are player projectiles, and should use a different shader by default.
|
||||
// It's not really appropriate to consider these "basic". Instead, we should add a
|
||||
// different prototype for them, with slightly different defaults.
|
||||
PP_BASIC(hghost, 22, 23)
|
||||
PP_BASIC(marisa, 15, 50)
|
||||
PP_BASIC(maristar, 25, 25)
|
||||
PP_BASIC(youhoming, 50, 50)
|
||||
PP_BASIC(youmu, 16, 24)
|
||||
// sprite w h cw ch
|
||||
PP_BASIC(apple, 32, 32, 14.40, 14.40)
|
||||
PP_BASIC(ball, 28, 28, 12.60, 12.60)
|
||||
PP_BASIC(bigball, 40, 40, 18.00, 18.00)
|
||||
PP_BASIC(bullet, 12, 20, 6.75, 13.75)
|
||||
PP_BASIC(card, 17, 20, 9.00, 10.00)
|
||||
PP_BASIC(crystal, 10, 20, 5.62, 11.25)
|
||||
PP_BASIC(flea, 12, 12, 6.00, 6.00)
|
||||
PP_BASIC(plainball, 25, 25, 11.25, 11.25)
|
||||
PP_BASIC(rice, 10, 20, 5.62, 11.25)
|
||||
PP_BASIC(soul, 80, 80, 36.00, 36.00)
|
||||
PP_BASIC(thickrice, 10, 14, 5.62, 7.87)
|
||||
PP_BASIC(wave, 25, 25, 8.00, 11.00)
|
||||
|
||||
#undef PP_BASIC
|
||||
|
|
9
src/projectile_prototypes/player.inc.h
Normal file
9
src/projectile_prototypes/player.inc.h
Normal file
|
@ -0,0 +1,9 @@
|
|||
|
||||
// sprite w h
|
||||
PP_PLAYER(hghost, 22, 23)
|
||||
PP_PLAYER(marisa, 15, 50)
|
||||
PP_PLAYER(maristar, 25, 25)
|
||||
PP_PLAYER(youhoming, 50, 50)
|
||||
PP_PLAYER(youmu, 16, 24)
|
||||
|
||||
#undef PP_PLAYER
|
|
@ -118,20 +118,43 @@ static void stage_draw_collision_areas(void) {
|
|||
|
||||
r_shader("sprite_filled_circle");
|
||||
r_uniform_vec4("color_inner", 0, 0, 0, 1);
|
||||
r_uniform_vec4("color_outer", 1, 1, 1, 1);
|
||||
r_uniform_vec4("color_outer", 1, 1, 1, 0.1);
|
||||
|
||||
float scale = 0.45; // TODO: bake this multiplier into the size itself
|
||||
for(Projectile *p = global.projs; p; p = p->next) {
|
||||
complex gsize = projectile_graze_size(p);
|
||||
|
||||
if(creal(gsize)) {
|
||||
r_draw_sprite(&(SpriteParams) {
|
||||
.color = rgb(0, 0.5, 0.5),
|
||||
.sprite_ptr = &stagedraw.dummy,
|
||||
.pos = { creal(p->pos), cimag(p->pos) },
|
||||
.rotation.angle = p->angle + M_PI/2,
|
||||
.scale = { .x = creal(gsize), .y = cimag(gsize) },
|
||||
.blend = BLEND_SUB,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
r_flush_sprites();
|
||||
r_uniform_vec4("color_inner", 0.0, 1.0, 0.0, 0.75);
|
||||
r_uniform_vec4("color_outer", 0.0, 0.5, 0.5, 0.75);
|
||||
|
||||
for(Projectile *p = global.projs; p; p = p->next) {
|
||||
r_draw_sprite(&(SpriteParams) {
|
||||
.sprite_ptr = &stagedraw.dummy,
|
||||
.pos = { creal(p->pos), cimag(p->pos) },
|
||||
.rotation.angle = p->angle + M_PI/2,
|
||||
.scale = { .x = creal(p->size) * scale, .y = cimag(p->size) * scale },
|
||||
.blend = BLEND_SUB,
|
||||
.scale = { .x = creal(p->collision_size), .y = cimag(p->collision_size) },
|
||||
.blend = BLEND_ALPHA,
|
||||
});
|
||||
}
|
||||
|
||||
r_draw_sprite(&(SpriteParams) {
|
||||
.sprite_ptr = &stagedraw.dummy,
|
||||
.pos = { creal(global.plr.pos), cimag(global.plr.pos) },
|
||||
.scale.both = 2, // NOTE: actual player is a singular point
|
||||
});
|
||||
|
||||
// TODO: handle other objects the player may collide with (enemies, bosses...)
|
||||
|
||||
r_flush_sprites();
|
||||
|
@ -399,6 +422,7 @@ void stage_draw_foreground(void) {
|
|||
r_mat_scale(1/facw,1/fach,1);
|
||||
r_mat_translate(floorf(facw*VIEWPORT_X), floorf(fach*VIEWPORT_Y), 0);
|
||||
r_mat_scale(floorf(scale*VIEWPORT_W)/VIEWPORT_W,floorf(scale*VIEWPORT_H)/VIEWPORT_H,1);
|
||||
|
||||
// apply the screenshake effect
|
||||
if(global.shake_view) {
|
||||
r_mat_translate(global.shake_view*sin(global.frames),global.shake_view*sin(global.frames*1.1+3),0);
|
||||
|
|
|
@ -849,7 +849,7 @@ void cirno_benchmark(Boss* b, int t) {
|
|||
p->flags |= PFLAG_DRAWADD;
|
||||
|
||||
if(t > 700 && frand() > 0.5)
|
||||
p->sprite = get_sprite("proj/plainball");
|
||||
projectile_set_prototype(p, pp_plainball);
|
||||
|
||||
if(t > 1200 && frand() > 0.5)
|
||||
p->color = rgb(1.0,0.2,0.8);
|
||||
|
@ -1142,6 +1142,14 @@ int stage1_tritoss(Enemy *e, int t) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
#ifdef BULLET_TEST
|
||||
static int proj_rotate(Projectile *p, int t) {
|
||||
p->angle = global.frames / 60.0;
|
||||
// p->angle = M_PI/2;
|
||||
return ACTION_NONE;
|
||||
}
|
||||
#endif
|
||||
|
||||
void stage1_events(void) {
|
||||
TIMER(&global.timer);
|
||||
|
||||
|
@ -1169,6 +1177,47 @@ void stage1_events(void) {
|
|||
return;
|
||||
#endif
|
||||
|
||||
#ifdef BULLET_TEST
|
||||
if(!global.projs) {
|
||||
PROJECTILE(
|
||||
.proto = pp_rice,
|
||||
.pos = (VIEWPORT_W + VIEWPORT_H * I) * 0.5,
|
||||
.color = hsl(0.5, 1.0, 0.5),
|
||||
.rule = proj_rotate,
|
||||
);
|
||||
}
|
||||
|
||||
if(!(global.frames % 36)) {
|
||||
ProjPrototype *projs[] = {
|
||||
pp_thickrice,
|
||||
pp_rice,
|
||||
// pp_ball,
|
||||
// pp_plainball,
|
||||
// pp_bigball,
|
||||
// pp_soul,
|
||||
pp_wave,
|
||||
pp_card,
|
||||
pp_bigball,
|
||||
pp_plainball,
|
||||
pp_ball,
|
||||
};
|
||||
int numprojs = sizeof(projs)/sizeof(*projs);
|
||||
|
||||
for(int i = 0; i < numprojs; ++i) {
|
||||
PROJECTILE(
|
||||
.proto = projs[i],
|
||||
.pos = ((0.5 + i) * VIEWPORT_W/numprojs + 0 * I),
|
||||
.color = hsl(0.5, 1.0, 0.5),
|
||||
.rule = linear,
|
||||
.args = { 1*I },
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return;
|
||||
#endif
|
||||
|
||||
// opening. projectile bursts
|
||||
FROM_TO(100, 160, 25) {
|
||||
create_enemy1c(VIEWPORT_W/2 + 70, 700, Fairy, stage1_burst, 1 + 0.6*I);
|
||||
|
|
|
@ -868,6 +868,7 @@ static int wriggle_spell_slave(Enemy *e, int time) {
|
|||
// or just replace this with some thing else
|
||||
.sprite_ptr = get_sprite("part/smoothdot"),
|
||||
.size = 16 + 16*I,
|
||||
.collision_size = 7.2 + 7.2*I,
|
||||
|
||||
.pos = e->pos,
|
||||
.color = rgb(1.0 - c, 0.5, 0.5 + c),
|
||||
|
|
|
@ -866,7 +866,7 @@ static int kdanmaku_proj(Projectile *p, int t) {
|
|||
|
||||
if(t == time) {
|
||||
p->color = rgb(0.6,0.3,1.0);
|
||||
p->sprite = get_sprite("proj/bullet");
|
||||
projectile_set_prototype(p, pp_bullet);
|
||||
p->args[1] = (global.plr.pos - p->pos) * 0.001;
|
||||
|
||||
if(frand()<0.5)
|
||||
|
|
|
@ -452,6 +452,7 @@ static void cloud_common(void) {
|
|||
// or just replace this with some thing else
|
||||
.sprite_ptr = get_sprite("part/lightningball"),
|
||||
.size = 48 * (1+I),
|
||||
.collision_size = 21.6 * (1+I),
|
||||
|
||||
.pos = VIEWPORT_W*afrand(0)-15.0*I,
|
||||
.color = rgba(0.2, 0.0, 0.4, 0.6),
|
||||
|
@ -784,7 +785,15 @@ int induction_bullet(Projectile *p, int time) {
|
|||
if(t < 0)
|
||||
return ACTION_DESTROY;
|
||||
}
|
||||
|
||||
p->pos = induction_bullet_traj(p,t);
|
||||
|
||||
// FIXME: projectiles really should get called with EVENT_BIRTH and/or with time==0 consistently.
|
||||
if(time == 1) {
|
||||
// don't lerp; the spawn position is very different on hard/lunatic and would cause false hits
|
||||
p->prevpos = p->pos;
|
||||
}
|
||||
|
||||
p->angle = carg(p->args[0]*cexp(p->args[1]*t)*(1+p->args[1]*t));
|
||||
return 1;
|
||||
}
|
||||
|
|
|
@ -502,26 +502,28 @@ int scythe_kepler(Enemy *e, int t) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
static ProjPrototype* kepler_pick_bullet(int tier) {
|
||||
switch(tier) {
|
||||
case 0: return pp_soul;
|
||||
case 1: return pp_bigball;
|
||||
case 2: return pp_ball;
|
||||
default: return pp_flea;
|
||||
}
|
||||
}
|
||||
|
||||
int kepler_bullet(Projectile *p, int t) {
|
||||
TIMER(&t);
|
||||
int tier = round(creal(p->args[1]));
|
||||
AT(1) {
|
||||
char *tex;
|
||||
switch(tier) {
|
||||
case 0: tex = "proj/soul"; break;
|
||||
case 1: tex = "proj/bigball"; break;
|
||||
case 2: tex = "proj/ball"; break;
|
||||
default: tex = "proj/flea";
|
||||
}
|
||||
p->sprite = get_sprite(tex);
|
||||
|
||||
}
|
||||
AT(EVENT_DEATH) {
|
||||
if(tier != 0)
|
||||
if(tier != 0) {
|
||||
free_ref(p->args[2]);
|
||||
}
|
||||
}
|
||||
if(t < 0)
|
||||
|
||||
if(t < 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
complex pos = p->pos0;
|
||||
|
||||
|
@ -553,7 +555,7 @@ int kepler_bullet(Projectile *p, int t) {
|
|||
play_sound("redirect");
|
||||
if(tier <= 1+(global.diff>D_Hard) && cimag(p->args[1])*(tier+1) < n) {
|
||||
PROJECTILE(
|
||||
.sprite = "flea",
|
||||
.proto = kepler_pick_bullet(tier + 1),
|
||||
.pos = p->pos,
|
||||
.color = rgb(0.3 + 0.3 * tier, 0.6 - 0.3 * tier, 1.0),
|
||||
.rule = kepler_bullet,
|
||||
|
@ -590,11 +592,14 @@ void elly_kepler(Boss *b, int t) {
|
|||
play_sound("shot_special1");
|
||||
for(int i = 0; i < c; i++) {
|
||||
complex n = cexp(I*2*M_PI/c*i+I*0.6*_i);
|
||||
PROJECTILE("soul", b->pos, rgb(0.3,0.8,1), kepler_bullet, {
|
||||
50*n,
|
||||
0,
|
||||
(1.4+0.1*global.diff)*n
|
||||
});
|
||||
|
||||
PROJECTILE(
|
||||
.proto = kepler_pick_bullet(0),
|
||||
.pos = b->pos,
|
||||
.color = rgb(0.3,0.8,1),
|
||||
.rule = kepler_bullet,
|
||||
.args = { 50*n, 0, (1.4+0.1*global.diff)*n }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1925,6 +1930,8 @@ static int elly_toe_boson(Projectile *p, int t) {
|
|||
int warps_initial = cimag(p->args[1]);
|
||||
|
||||
if(wrap_around(&p->pos) != 0) {
|
||||
p->prevpos = p->pos; // don't lerp
|
||||
|
||||
if(warps_left-- < 1) {
|
||||
p->type = FakeProj; // prevent invisible collision at would-be warp location
|
||||
return ACTION_DESTROY;
|
||||
|
@ -2072,7 +2079,7 @@ static int elly_toe_fermion(Projectile *p, int t) {
|
|||
|
||||
if(elly_toe_its_yukawatime(p->pos)) {
|
||||
if(!p->args[3]) {
|
||||
p->sprite = get_sprite("proj/bigball");
|
||||
projectile_set_prototype(p, pp_bigball);
|
||||
p->args[3]=1;
|
||||
play_sound_ex("shot_special1", 5, false);
|
||||
|
||||
|
@ -2091,7 +2098,7 @@ static int elly_toe_fermion(Projectile *p, int t) {
|
|||
|
||||
p->pos0*=1.01;
|
||||
} else if(p->args[3]) {
|
||||
p->sprite = get_sprite("proj/ball");
|
||||
projectile_set_prototype(p, pp_ball);
|
||||
p->args[3]=0;
|
||||
}
|
||||
|
||||
|
@ -2104,12 +2111,12 @@ static int elly_toe_higgs(Projectile *p, int t) {
|
|||
|
||||
if(elly_toe_its_yukawatime(p->pos)) {
|
||||
if(!p->args[3]) {
|
||||
p->sprite = get_sprite("proj/rice");
|
||||
projectile_set_prototype(p, pp_rice);
|
||||
p->args[3]=1;
|
||||
}
|
||||
p->args[0]*=1.01;
|
||||
} else if(p->args[3]) {
|
||||
p->sprite = get_sprite("proj/flea");
|
||||
projectile_set_prototype(p, pp_flea);
|
||||
p->args[3]=0;
|
||||
}
|
||||
|
||||
|
|
67
src/util.c
67
src/util.c
|
@ -344,6 +344,73 @@ float sanitize_scale(float scale) {
|
|||
return max(0.1, scale);
|
||||
}
|
||||
|
||||
bool point_in_ellipse(complex p, Ellipse e) {
|
||||
double Xp = creal(p);
|
||||
double Yp = cimag(p);
|
||||
double Xe = creal(e.origin);
|
||||
double Ye = cimag(e.origin);
|
||||
double a = e.angle;
|
||||
|
||||
return (
|
||||
pow(cos(a) * (Xp - Xe) + sin(a) * (Yp - Ye), 2) / pow(creal(e.axes)/2, 2) +
|
||||
pow(sin(a) * (Xp - Xe) - cos(a) * (Yp - Ye), 2) / pow(cimag(e.axes)/2, 2)
|
||||
) <= 1;
|
||||
}
|
||||
|
||||
// Is the point of shortest distance between the line through a and b
|
||||
// and a point c between a and b and closer than r?
|
||||
// if yes, return f so that a+f*(b-a) is that point.
|
||||
// otherwise return -1.
|
||||
double lineseg_circle_intersect(LineSegment seg, Circle c) {
|
||||
complex m, v;
|
||||
double projection, lv, lm, distance;
|
||||
|
||||
m = seg.b - seg.a; // vector pointing along the line
|
||||
v = seg.a - c.origin; // vector from circle to point A
|
||||
|
||||
lv = cabs(v);
|
||||
lm = cabs(m);
|
||||
|
||||
if(lv < c.radius) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(lm == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
projection = -creal(v*conj(m)) / lm; // project v onto the line
|
||||
|
||||
// now the distance can be calculated by Pythagoras
|
||||
distance = sqrt(pow(lv, 2) - pow(projection, 2));
|
||||
|
||||
if(distance <= c.radius) {
|
||||
double f = projection/lm;
|
||||
|
||||
if(f >= 0 && f <= 1) { // it’s on the line!
|
||||
return f;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool lineseg_ellipse_intersect(LineSegment seg, Ellipse e) {
|
||||
// Transform the coordinate system so that the ellipse becomes a circle
|
||||
// with origin at (0, 0) and diameter equal to its X axis. Then we can
|
||||
// calculate the segment-circle intersection.
|
||||
|
||||
double ratio = creal(e.axes) / cimag(e.axes);
|
||||
complex rotation = cexp(I * -e.angle);
|
||||
seg.a *= rotation;
|
||||
seg.b *= rotation;
|
||||
seg.a = creal(seg.a) + I * ratio * cimag(seg.a);
|
||||
seg.b = creal(seg.b) + I * ratio * cimag(seg.b);
|
||||
|
||||
Circle c = { .radius = creal(e.axes) / 2 };
|
||||
return lineseg_circle_intersect(seg, c) >= 0;
|
||||
}
|
||||
|
||||
//
|
||||
// gl/video utils
|
||||
//
|
||||
|
|
55
src/util.h
55
src/util.h
|
@ -63,29 +63,17 @@ char* ucs4_to_utf8(const uint32_t *ucs4);
|
|||
// math utils
|
||||
//
|
||||
|
||||
typedef struct FloatRect {
|
||||
float x;
|
||||
float y;
|
||||
float w;
|
||||
float h;
|
||||
} FloatRect;
|
||||
|
||||
typedef struct IntRect {
|
||||
int x;
|
||||
int y;
|
||||
int w;
|
||||
int h;
|
||||
} IntRect;
|
||||
|
||||
#include <complex.h>
|
||||
|
||||
// These definitions are common but non-standard, so we provide our own
|
||||
#undef M_PI
|
||||
#undef M_PI_2
|
||||
#undef M_PI_4
|
||||
#undef M_E
|
||||
#define M_PI 3.14159265358979323846
|
||||
#define M_PI_2 1.57079632679489661923
|
||||
#define M_PI_4 0.78539816339744830962
|
||||
#define M_E 2.7182818284590452354
|
||||
|
||||
#define DEG2RAD (M_PI/180.0)
|
||||
#define RAD2DEG (180.0/M_PI)
|
||||
|
@ -100,6 +88,41 @@ typedef struct IntRect {
|
|||
#undef min
|
||||
#undef max
|
||||
|
||||
typedef struct FloatRect {
|
||||
float x;
|
||||
float y;
|
||||
float w;
|
||||
float h;
|
||||
} FloatRect;
|
||||
|
||||
typedef struct IntRect {
|
||||
int x;
|
||||
int y;
|
||||
int w;
|
||||
int h;
|
||||
} IntRect;
|
||||
|
||||
typedef struct Ellipse {
|
||||
complex origin;
|
||||
complex axes; // NOT half-axes!
|
||||
double angle;
|
||||
} Ellipse;
|
||||
|
||||
typedef struct LineSegment {
|
||||
complex a;
|
||||
complex b;
|
||||
} LineSegment;
|
||||
|
||||
typedef struct Circle {
|
||||
complex origin;
|
||||
double radius;
|
||||
} Circle;
|
||||
|
||||
typedef struct Rect {
|
||||
complex top_left;
|
||||
complex bottom_right;
|
||||
} Rect;
|
||||
|
||||
double min(double, double) attr_const;
|
||||
double max(double, double) attr_const;
|
||||
double clamp(double, double, double) attr_const;
|
||||
|
@ -114,6 +137,10 @@ float smooth(float x) attr_const;
|
|||
float smoothreclamp(float x, float old_min, float old_max, float new_min, float new_max) attr_const;
|
||||
float sanitize_scale(float scale) attr_const;
|
||||
|
||||
bool point_in_ellipse(complex p, Ellipse e) attr_const;
|
||||
double lineseg_circle_intersect(LineSegment seg, Circle c) attr_const;
|
||||
bool lineseg_ellipse_intersect(LineSegment seg, Ellipse e) attr_const;
|
||||
|
||||
#define topow2(x) (_Generic((x), \
|
||||
uint32_t: topow2_u32, \
|
||||
uint64_t: topow2_u64, \
|
||||
|
|
Loading…
Reference in a new issue