430 lines
11 KiB
C
430 lines
11 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 "item.h"
|
|
|
|
#include "audio/audio.h"
|
|
#include "global.h"
|
|
#include "list.h"
|
|
#include "stage.h"
|
|
#include "stageobjects.h"
|
|
|
|
// Instant collection radius.
|
|
// This is not the same as the player's PLR_PROP_COLLECT_RADIUS property, which is the minimum
|
|
// distance to begin attracting the item towards the player.
|
|
#define ITEM_GRAB_RADIUS 10
|
|
|
|
static const char *item_sprite_name(ItemType type) {
|
|
static const char *const map[] = {
|
|
[ITEM_BOMB - ITEM_FIRST] = "item/bomb",
|
|
[ITEM_BOMB_FRAGMENT - ITEM_FIRST] = "item/bombfrag",
|
|
[ITEM_LIFE - ITEM_FIRST] = "item/life",
|
|
[ITEM_LIFE_FRAGMENT - ITEM_FIRST] = "item/lifefrag",
|
|
[ITEM_PIV - ITEM_FIRST] = "item/bullet_point",
|
|
[ITEM_POINTS - ITEM_FIRST] = "item/point",
|
|
[ITEM_POWER - ITEM_FIRST] = "item/power",
|
|
[ITEM_POWER_MINI - ITEM_FIRST] = "item/minipower",
|
|
[ITEM_SURGE - ITEM_FIRST] = "item/surge",
|
|
[ITEM_VOLTAGE - ITEM_FIRST] = "item/voltage",
|
|
};
|
|
|
|
uint index = type - 1;
|
|
|
|
assert(index < ARRAY_SIZE(map));
|
|
return map[index];
|
|
}
|
|
|
|
static const char *item_indicator_sprite_name(ItemType type) {
|
|
static const char *const map[] = {
|
|
[ITEM_BOMB - ITEM_FIRST] = "item/bomb_indicator",
|
|
[ITEM_BOMB_FRAGMENT - ITEM_FIRST] = "item/bombfrag_indicator",
|
|
[ITEM_LIFE - ITEM_FIRST] = "item/life_indicator",
|
|
[ITEM_LIFE_FRAGMENT - ITEM_FIRST] = "item/lifefrag_indicator",
|
|
[ITEM_PIV - ITEM_FIRST] = NULL,
|
|
[ITEM_POINTS - ITEM_FIRST] = "item/point_indicator",
|
|
[ITEM_POWER - ITEM_FIRST] = "item/power_indicator",
|
|
[ITEM_POWER_MINI - ITEM_FIRST] = NULL,
|
|
[ITEM_SURGE - ITEM_FIRST] = NULL,
|
|
[ITEM_VOLTAGE - ITEM_FIRST] = "item/voltage_indicator",
|
|
};
|
|
|
|
uint index = type - 1;
|
|
|
|
assert(index < ARRAY_SIZE(map));
|
|
return map[index];
|
|
}
|
|
|
|
static Sprite *item_sprite(ItemType type) {
|
|
return res_sprite(item_sprite_name(type));
|
|
}
|
|
|
|
static Sprite *item_indicator_sprite(ItemType type) {
|
|
const char *name = item_indicator_sprite_name(type);
|
|
if(name == NULL) {
|
|
return NULL;
|
|
}
|
|
return res_sprite(name);
|
|
}
|
|
|
|
void item_set_type(Item *item, ItemType type) {
|
|
if(UNLIKELY(item->type == type)) {
|
|
return;
|
|
}
|
|
|
|
item->type = type;
|
|
item->sprites.pickup = NOT_NULL(item_sprite(type));
|
|
item->sprites.indicator = item_indicator_sprite(type);
|
|
|
|
// TODO: remove dependence on sprite size
|
|
item->size = item->sprites.pickup->extent.as_cmplx;
|
|
|
|
item->ent.draw_layer = LAYER_ITEM | type;
|
|
}
|
|
|
|
static void ent_draw_item(EntityInterface *ent) {
|
|
Item *i = ENT_CAST(ent, Item);
|
|
|
|
const int indicator_display_y = 6;
|
|
float y = im(i->pos);
|
|
|
|
ShaderCustomParams shader_params = { 1.0f };
|
|
ShaderProgram *shader = res_shader("sprite_particle");
|
|
|
|
if(y < 0) {
|
|
Sprite *s = i->sprites.indicator;
|
|
|
|
if(s != NULL) {
|
|
float alpha = -tanhf(y * 0.1f) / (1 + 0.1 * fabsf(y));
|
|
r_draw_sprite(&(SpriteParams) {
|
|
.sprite_ptr = s,
|
|
.shader_ptr = shader,
|
|
.shader_params = &shader_params,
|
|
.pos = { re(i->pos), indicator_display_y },
|
|
.color = RGBA_MUL_ALPHA(1, 1, 1, alpha),
|
|
});
|
|
}
|
|
}
|
|
|
|
float alpha = 1;
|
|
if(i->type == ITEM_PIV && !i->auto_collect) {
|
|
alpha = clamp(2.0f - (global.frames - i->birthtime) / 60.0f, 0.1f, 1.0f);
|
|
}
|
|
|
|
Color *c = RGBA_MUL_ALPHA(1, 1, 1, alpha);
|
|
|
|
r_draw_sprite(&(SpriteParams) {
|
|
.sprite_ptr = i->sprites.pickup,
|
|
.shader_ptr = shader,
|
|
.shader_params = &shader_params,
|
|
.pos = { re(i->pos), y },
|
|
.color = c,
|
|
});
|
|
}
|
|
|
|
Item *create_item(cmplx pos, cmplx v, ItemType type) {
|
|
if((re(pos) < 0 || re(pos) > VIEWPORT_W)) {
|
|
// we need this because we clamp the item position to the viewport boundary during motion
|
|
// e.g. enemies that die offscreen shouldn't spawn any items inside the viewport
|
|
return NULL;
|
|
}
|
|
|
|
if(type == ITEM_POWER_MINI && player_is_powersurge_active(&global.plr)) {
|
|
type = ITEM_SURGE;
|
|
}
|
|
|
|
auto i = alist_append(&global.items, STAGE_ACQUIRE_OBJ(Item));
|
|
i->pos = pos;
|
|
i->pos0 = pos;
|
|
i->v = v;
|
|
i->birthtime = global.frames;
|
|
i->auto_collect = 0;
|
|
i->collecttime = 0;
|
|
|
|
i->ent.draw_func = ent_draw_item;
|
|
ent_register(&i->ent, ENT_TYPE_ID(Item));
|
|
|
|
item_set_type(i, type);
|
|
|
|
return i;
|
|
}
|
|
|
|
void delete_item(Item *item) {
|
|
ent_unregister(&item->ent);
|
|
STAGE_RELEASE_OBJ(alist_unlink(&global.items, item));
|
|
}
|
|
|
|
Item *create_clear_item(cmplx pos, uint clear_flags) {
|
|
ItemType type = ITEM_PIV;
|
|
|
|
if(clear_flags & CLEAR_HAZARDS_SPAWN_VOLTAGE) {
|
|
type = ITEM_VOLTAGE;
|
|
}
|
|
|
|
Item *i = create_item(pos, -10*I + 5*rng_sreal(), type);
|
|
|
|
if(i) {
|
|
PARTICLE(
|
|
.sprite = "flare",
|
|
.pos = pos,
|
|
.timeout = 30,
|
|
.draw_rule = pdraw_timeout_fade(1, 0),
|
|
.layer = LAYER_BULLET+1
|
|
);
|
|
|
|
collect_item(i, 1);
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
void delete_items(void) {
|
|
for(Item *i = global.items.first, *next; i; i = next) {
|
|
next = i->next;
|
|
delete_item(i);
|
|
}
|
|
}
|
|
|
|
static cmplx move_item(Item *i) {
|
|
int t = global.frames - i->birthtime;
|
|
cmplx lim = 0 + 2.0*I;
|
|
|
|
cmplx oldpos = i->pos;
|
|
|
|
if(i->auto_collect && i->collecttime <= global.frames && global.frames - i->birthtime > 20) {
|
|
i->pos -= (7 + i->auto_collect) * cnormalize(i->pos - global.plr.pos);
|
|
} else {
|
|
i->pos = i->pos0 + log(t/5.0 + 1)*5*(i->v + lim) + lim*t;
|
|
|
|
cmplx v = i->pos - oldpos;
|
|
double half = re(i->size) * 0.5f;
|
|
bool over = false;
|
|
|
|
if((over = re(i->pos) > VIEWPORT_W-half) || re(i->pos) < half) {
|
|
cmplx normal = over ? -1 : 1;
|
|
v -= 2 * normal * (re(normal)*re(v));
|
|
v = 1.5*re(v) - I*fabs(im(v));
|
|
|
|
i->pos = clamp(re(i->pos), half, VIEWPORT_W-half) + I*im(i->pos);
|
|
i->v = v;
|
|
i->pos0 = i->pos;
|
|
i->birthtime = global.frames;
|
|
}
|
|
}
|
|
|
|
return i->pos - oldpos;
|
|
}
|
|
|
|
static bool item_out_of_bounds(Item *item) {
|
|
double margin = max(re(item->size), im(item->size));
|
|
|
|
return (
|
|
re(item->pos) < -margin ||
|
|
re(item->pos) > VIEWPORT_W + margin ||
|
|
im(item->pos) > VIEWPORT_H + margin
|
|
);
|
|
}
|
|
|
|
bool collect_item(Item *item, float value) {
|
|
if(!player_is_alive(&global.plr)) {
|
|
return false;
|
|
}
|
|
|
|
const float speed = 10;
|
|
const int delay = 0;
|
|
|
|
if(item->auto_collect) {
|
|
item->auto_collect = max(speed, item->auto_collect);
|
|
item->pickup_value = max(clamp(value, ITEM_MIN_VALUE, ITEM_MAX_VALUE), item->pickup_value);
|
|
item->collecttime = min(global.frames + delay, item->collecttime);
|
|
} else {
|
|
item->auto_collect = speed;
|
|
item->pickup_value = clamp(value, ITEM_MIN_VALUE, ITEM_MAX_VALUE);
|
|
item->collecttime = global.frames + delay;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void collect_all_items(float value) {
|
|
for(Item *i = global.items.first; i; i = i->next) {
|
|
collect_item(i, value);
|
|
}
|
|
}
|
|
|
|
void process_items(void) {
|
|
Item *item = global.items.first, *del = NULL;
|
|
float attract_dist = player_property(&global.plr, PLR_PROP_COLLECT_RADIUS);
|
|
bool plr_alive = player_is_alive(&global.plr);
|
|
bool stage_cleared = stage_is_cleared();
|
|
bool surge_active = player_is_powersurge_active(&global.plr);
|
|
real poc = player_property(&global.plr, PLR_PROP_POC);
|
|
|
|
while(item != NULL) {
|
|
bool may_collect = true;
|
|
|
|
if(
|
|
(item->type == ITEM_POWER_MINI && global.plr.power_stored >= PLR_MAX_POWER_EFFECTIVE) ||
|
|
(item->type == ITEM_SURGE && !surge_active)
|
|
) {
|
|
item_set_type(item, ITEM_PIV);
|
|
|
|
if(collect_item(item, 1)) {
|
|
item->pos0 = item->pos;
|
|
item->birthtime = global.frames;
|
|
item->v = -20*I + 10*rng_sreal();
|
|
}
|
|
}
|
|
|
|
if(global.stage->type == STAGE_SPELL && (item->type == ITEM_LIFE || item->type == ITEM_BOMB || item->type == ITEM_LIFE_FRAGMENT || item->type == ITEM_BOMB_FRAGMENT)) {
|
|
// just in case we ever have some weird spell that spawns those...
|
|
item_set_type(item, ITEM_POINTS);
|
|
}
|
|
|
|
if(global.frames - item->birthtime < 20) {
|
|
may_collect = false;
|
|
}
|
|
|
|
bool grabbed = false;
|
|
|
|
if(may_collect) {
|
|
real item_dist2 = cabs2(global.plr.pos - item->pos);
|
|
|
|
if(plr_alive) {
|
|
if(im(global.plr.pos) < poc || stage_cleared) {
|
|
collect_item(item, 1);
|
|
} else if(item_dist2 < attract_dist * attract_dist) {
|
|
real value;
|
|
|
|
if(surge_active) {
|
|
value = 1;
|
|
} else {
|
|
value = 1 - im(global.plr.pos) / VIEWPORT_H;
|
|
}
|
|
|
|
collect_item(item, value);
|
|
item->auto_collect = 2;
|
|
}
|
|
} else if(item->auto_collect) {
|
|
item->auto_collect = 0;
|
|
item->pos0 = item->pos;
|
|
item->birthtime = global.frames;
|
|
item->v = -10*I + 5*rng_sreal();
|
|
}
|
|
|
|
grabbed = (item_dist2 < ITEM_GRAB_RADIUS * ITEM_GRAB_RADIUS);
|
|
}
|
|
|
|
cmplx deltapos = move_item(item);
|
|
|
|
if(grabbed) {
|
|
switch(item->type) {
|
|
case ITEM_POWER:
|
|
player_add_power(&global.plr, POWER_VALUE);
|
|
player_add_points(&global.plr, 25, item->pos);
|
|
player_extend_powersurge(&global.plr, PLR_POWERSURGE_POSITIVE_GAIN*3, PLR_POWERSURGE_NEGATIVE_GAIN*3);
|
|
play_sfx("item_generic");
|
|
break;
|
|
case ITEM_POWER_MINI:
|
|
player_add_power(&global.plr, POWER_VALUE_MINI);
|
|
player_add_points(&global.plr, 5, item->pos);
|
|
play_sfx("item_generic");
|
|
break;
|
|
case ITEM_SURGE:
|
|
player_extend_powersurge(&global.plr, PLR_POWERSURGE_POSITIVE_GAIN, PLR_POWERSURGE_NEGATIVE_GAIN);
|
|
player_add_points(&global.plr, 25, item->pos);
|
|
play_sfx("item_generic");
|
|
break;
|
|
case ITEM_POINTS:
|
|
player_add_points(&global.plr, round(global.plr.point_item_value * item->pickup_value), item->pos);
|
|
play_sfx("item_generic");
|
|
break;
|
|
case ITEM_PIV:
|
|
player_add_piv(&global.plr, 1, item->pos);
|
|
play_sfx("item_generic");
|
|
break;
|
|
case ITEM_VOLTAGE:
|
|
player_add_voltage(&global.plr, 1);
|
|
player_add_piv(&global.plr, 10, item->pos);
|
|
play_sfx("item_generic");
|
|
break;
|
|
case ITEM_LIFE:
|
|
player_add_lives(&global.plr, 1);
|
|
break;
|
|
case ITEM_BOMB:
|
|
player_add_bombs(&global.plr, 1);
|
|
break;
|
|
case ITEM_LIFE_FRAGMENT:
|
|
player_add_life_fragments(&global.plr, 1);
|
|
break;
|
|
case ITEM_BOMB_FRAGMENT:
|
|
player_add_bomb_fragments(&global.plr, PLR_MAX_BOMB_FRAGMENTS / 5);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(grabbed || (im(deltapos) > 0 && item_out_of_bounds(item))) {
|
|
del = item;
|
|
item = item->next;
|
|
delete_item(del);
|
|
} else {
|
|
item = item->next;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void spawn_item_internal(cmplx pos, ItemType type, float collect_value) {
|
|
cmplx v = rng_range(12, 18);
|
|
v *= cdir(3*M_PI/2 + rng_sreal() * M_PI/11);
|
|
v -= 3*I;
|
|
|
|
Item *i = create_item(pos, v, type);
|
|
|
|
if(i != NULL && collect_value >= 0) {
|
|
collect_item(i, collect_value);
|
|
}
|
|
}
|
|
|
|
void spawn_item(cmplx pos, ItemType type) {
|
|
spawn_item_internal(pos, type, -1);
|
|
}
|
|
|
|
void spawn_and_collect_item(cmplx pos, ItemType type, float collect_value) {
|
|
spawn_item_internal(pos, type, collect_value);
|
|
}
|
|
|
|
static void spawn_items_internal(cmplx pos, float collect_value, SpawnItemsArgs groups[]) {
|
|
for(SpawnItemsArgs *g = groups; g->type > 0; ++g) {
|
|
for(uint i = 0; i < g->count; ++i) {
|
|
spawn_item_internal(pos, g->type, collect_value);
|
|
}
|
|
}
|
|
}
|
|
|
|
#undef spawn_items
|
|
void spawn_items(cmplx pos, SpawnItemsArgs groups[]) {
|
|
spawn_items_internal(pos, -1, groups);
|
|
}
|
|
|
|
#undef spawn_and_collect_items
|
|
void spawn_and_collect_items(cmplx pos, float collect_value, SpawnItemsArgs groups[]) {
|
|
spawn_items_internal(pos, collect_value, groups);
|
|
}
|
|
|
|
void items_preload(ResourceGroup *rg) {
|
|
for(ItemType i = ITEM_FIRST; i <= ITEM_LAST; ++i) {
|
|
res_group_preload(rg, RES_SPRITE, 0, item_sprite_name(i), NULL);
|
|
const char *indicator = item_indicator_sprite_name(i);
|
|
if(indicator != NULL) {
|
|
res_group_preload(rg, RES_SPRITE, 0, indicator, NULL);
|
|
}
|
|
}
|
|
|
|
res_group_preload(rg, RES_SFX, RESF_OPTIONAL,
|
|
"item_generic",
|
|
NULL);
|
|
}
|