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:
Andrei Alexeyev 2018-05-13 16:08:58 +03:00 committed by GitHub
parent 280b0739d8
commit ef37b22d6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 351 additions and 164 deletions

View file

@ -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) // its 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) {

View file

@ -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);

View file

@ -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,
);
}
}

View file

@ -62,6 +62,7 @@ struct Player {
ENTITY_INTERFACE_NAMED(Player, ent);
complex pos;
complex velocity;
complex deathpos;
short focus;

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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

View file

@ -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

View 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

View file

@ -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);

View file

@ -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);

View file

@ -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),

View file

@ -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)

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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) { // its 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
//

View file

@ -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, \