taisei/src/item.c
2024-10-04 16:27:28 +02:00

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