RogueLife/src/game.c

678 lines
18 KiB
C

#include "game.h"
#include "util.h"
#include "enemies.h"
#include "player.h"
#include "comp/fighter.h"
#include "comp/physical.h"
#include "comp/visible.h"
#include "comp/particle.h"
#include "aoe.h"
#include <gint/defs/util.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
bool game_load(game_t *g, level_t const *level)
{
game_unload(g);
g->map = level->map;
g->occupation = malloc(g->map->width * g->map->height *
sizeof *g->occupation);
g->map_anim = malloc(g->map->width * g->map->height *
sizeof *g->map_anim);
for(int i = 0; i < g->map->width * g->map->height; i++)
g->map_anim[i] = rand() & 4095;
camera_init(&g->camera, g->map);
g->time_total = fix(0);
g->entities = NULL;
g->entity_count = 0;
g->level = level;
g->event = -1;
g->event_time = fix(0);
g->freeze_time = fix(0);
g->wave_number = 0;
g->wave_spawned = 0;
g->wave_left = NULL;
g->combo = 0;
g->combo_health = fix(0);
g->hud_xp_anim.frame = anims_hud_xp_Idle.start[0];
g->hud_xp_anim.elapsed = 0;
g->hud_backpack_anim.frame = anims_hud_backpack_Idle.start[0];
g->hud_backpack_anim.elapsed = 0;
g->menu_time = fix(0);
g->menu_open = false;
g->menu_cursor = 0;
g->finish_time = fix(0);
g->victory = false;
g->final_screen_time = fix(-1);
g->hud_wave_number_timer = fix(0);
memset(&g->score, 0, sizeof g->score);
game_next_event(g);
return true;
}
void game_unload(game_t *g)
{
g->map = NULL;
free(g->occupation);
g->occupation = NULL;
free(g->map_anim);
g->map_anim = NULL;
for(int i = 0; i < g->entity_count; i++)
entity_destroy(g->entities[i]);
free(g->entities);
g->entities = NULL;
g->entity_count = 0;
free(g->wave_left);
g->wave_left = NULL;
}
level_event_t const *game_current_event(game_t const *g)
{
if(!g->level) return NULL;
if(g->event < 0 || g->event >= g->level->event_count) return NULL;
return &g->level->events[g->event];
}
level_wave_t const *game_current_wave(game_t const *g)
{
level_event_t const *event = game_current_event(g);
if(!event)
return NULL;
return (event->type == LEVEL_EVENT_WAVE) ? event->wave : NULL;
}
bool game_current_event_finished(game_t const *g)
{
level_event_t const *event = game_current_event(g);
/* The implicit event #level->event_count+1 never finishes */
if(!event)
return false;
if(event->type == LEVEL_EVENT_DELAY) {
return g->event_time >= event->duration;
}
if(event->type == LEVEL_EVENT_WAVE) {
int wave_enemies = 0;
for(int i = 0; i < event->wave->entry_count; i++)
wave_enemies += event->wave->entries[i].amount;
return g->wave_spawned >= wave_enemies;
}
if(event->type == LEVEL_EVENT_ITEM) {
return g->event_time >= event->duration;
}
return false;
}
void game_next_event(game_t *g)
{
if(g->event >= 0 && g->level
&& g->level->events[g->event].type == LEVEL_EVENT_WAVE) {
g->score.waves_survived++;
}
if(g->event >= g->level->event_count) return;
g->event++;
g->event_time = fix(0);
g->wave_spawned = 0;
free(g->wave_left);
g->wave_left = NULL;
level_event_t const *event = game_current_event(g);
if(event && event->type == LEVEL_EVENT_WAVE) {
g->wave_number++;
g->hud_wave_number_timer = fix(1);
/* Copy the amounts of monsters to spawn for the next wave */
g->wave_left = malloc(event->wave->entry_count * sizeof *g->wave_left);
if(!g->wave_left) return;
for(int i = 0; i < event->wave->entry_count; i++)
g->wave_left[i] = event->wave->entries[i].amount;
}
if(event && event->type == LEVEL_EVENT_ITEM) {
int x=-1, y=-1;
for(int i = 0; i < 100; i++) {
int cx = rand() % g->map->width;
int cy = rand() % g->map->height;
map_cell_t const *cell = map_cell(g->map, cx, cy);
__auto_type tiles = g->map->tileset->tiles;
if(!tiles[cell->base].solid &&
(!cell->decor || !tiles[cell->decor].solid)) {
x = cx;
y = cy;
break;
}
}
/* If a position can't be found, place the item on the player */
if(x < 0 || y < 0) {
physical_t *p = getcomp(g->player, physical);
x = ffloor(p->x);
y = ffloor(p->y);
}
entity_t *item = item_make(event->item,
(vec2){ fix(x)+fix(0.5), fix(y)+fix(0.5) });
game_add_entity(g, item);
game_message(g, fix(2.0), "A %s dropped!", item_name(event->item));
}
}
void game_shake(game_t *g, int amplitude, fixed_t duration)
{
if(g->screenshake_amplitude > amplitude)
return;
g->screenshake_duration = max(g->screenshake_duration, duration);
g->screenshake_amplitude = amplitude;
}
void game_freeze(game_t *g, fixed_t duration)
{
g->freeze_time = max(g->freeze_time, duration);
}
void game_message(game_t *g, fixed_t duration, char const *fmt, ...)
{
static char message[128];
va_list args;
va_start(args, fmt);
vsnprintf(message, sizeof message, fmt, args);
va_end(args);
g->message = message;
g->message_time = duration;
}
void game_hud_anim_backpack_item(game_t *g)
{
g->hud_backpack_anim.frame = anims_hud_backpack_Open.start[0];
g->hud_backpack_anim.elapsed = 0;
}
void game_hud_anim_backpack_open(game_t *g)
{
g->hud_backpack_anim.frame = anims_hud_backpack_InventoryOpen.start[0];
g->hud_backpack_anim.elapsed = 0;
}
void game_hud_anim_backpack_close(game_t *g)
{
g->hud_backpack_anim.frame = anims_hud_backpack_InventoryClose.start[0];
g->hud_backpack_anim.elapsed = 0;
}
static int game_score_combo_chain(int chain)
{
if(chain < 3)
return 0;
else if(chain < 30)
return chain * 5;
else
return chain * 10;
}
static int game_score_simult_kills(int kills)
{
return (kills < 3) ? 0 : kills * kills;
}
int game_compute_score(game_t const *g)
{
score_t const *s = &g->score;
return
s->kill_number * 3 +
s->longest_combo_chain * 8 +
s->combo_chains +
s->largest_simult_kill * 17 +
s->simult_kills +
s->one_shot_kills * 4 +
s->waves_survived * 16;
}
bool game_victory_achieved(game_t const *g)
{
fighter_t *player_f = getcomp(g->player, fighter);
/* Must be alive. */
if(player_f->HP <= 0)
return false;
/* All waves must have finished. */
if(!g->level || g->event < g->level->event_count)
return false;
/* All enemies must be dead. */
for(int i = 0; i < g->entity_count; i++) {
fighter_t *f = getcomp(g->entities[i], fighter);
if(f && f->enemy)
return false;
}
/* Combo must be finished. */
if(g->combo_health > 0)
return false;
return true;
}
bool game_defeated(game_t const *g)
{
fighter_t *player_f = getcomp(g->player, fighter);
visible_t *player_v = getcomp(g->player, visible);
/* Must be dead. */
if(player_f->HP > 0)
return false;
/* Must have finished the KO animation. */
for(int i = 0; i < 4; i++) {
if(anim_in(player_v->anim.frame, &anims_player_IdleKO, i))
return true;
}
return false;
}
//---
// Object management functions
//---
void game_compute_occupation(game_t *g)
{
memset(g->occupation, 0, g->map->width * g->map->height *
sizeof *g->occupation);
for(int i = 0; i < g->entity_count; i++) {
if(getcomp(g->entities[i], fighter) == NULL)
continue;
ivec2 position = vec_f2i(physical_pos(g->entities[i]));
g->occupation[position.y * g->map->width + position.x]++;
}
}
void game_add_entity(game_t *g, entity_t *entity)
{
size_t new_size = (g->entity_count + 1) * sizeof *g->entities;
entity_t **new_entities = realloc(g->entities, new_size);
if(!new_entities) return;
g->entities = new_entities;
g->entities[g->entity_count] = entity;
g->entity_count++;
}
void game_spawn_entity(game_t *g, entity_t *e)
{
game_add_entity(g, e);
fighter_t *f = getcomp(e, fighter);
if(!f || f->current_attack) return;
/* Teleport hitbox */
rect hitbox = {
-fix(8)/16, fix(7)/16, -fix(20)/16, fix(4)/16,
};
entity_t *spawn = aoe_make(EFFECT_SPAWN, physical_pos(e), fix(0));
getcomp(spawn, physical)->hitbox = hitbox;
aoe_t *aoe = getcomp(spawn, aoe);
aoe->lifetime = anim_duration(&anims_skill_teleport);
aoe->repeat_delay = 0;
aoe->origin = e;
visible_set_anim(spawn, &anims_skill_teleport, 2);
game_add_entity(g, spawn);
f->current_attack = spawn;
f->attack_follows_movement = true;
}
/* Remove an entity and rearrange the array. */
static void game_remove_entity(game_t *g, int i)
{
if(i < 0 || i >= g->entity_count) return;
entity_destroy(g->entities[i]);
g->entities[i] = g->entities[--g->entity_count];
}
void game_remove_dead_entities(game_t *g)
{
/* Mark dead fighters for deletion */
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
fighter_t *f = getcomp(e, fighter);
visible_t *v = getcomp(e, visible);
bool anim_finished = !v || (v->anim.frame == NULL);
if(f && f->HP == 0 && f->enemy != NULL && anim_finished) {
/* Give XP points to player based on combo */
int mult_percent = 100 + 2 * min(g->combo, 100);
int xp = f->enemy->id->xp * mult_percent / 100;
bool lvup = player_add_xp(g->player, xp);
if(lvup) {
g->hud_xp_anim.frame = anims_hud_xp_Explode.start[0];
g->hud_xp_anim.elapsed = 0;
}
else if(!anim_in(g->hud_xp_anim.frame, &anims_hud_xp_Explode, 0)) {
g->hud_xp_anim.frame = anims_hud_xp_Shine.start[0];
g->hud_xp_anim.elapsed = 0;
}
/* Update player combo */
g->combo++;
g->combo_health = fix(1);
/* Update score-related metrics */
g->score.kill_number++;
g->score.one_shot_kills += (f->one_shot_killed == true);
g->score.longest_combo_chain = max(g->score.longest_combo_chain,
g->combo);
g->score.current_simult_kills++;
g->score.largest_simult_kill = max(g->score.largest_simult_kill,
g->score.current_simult_kills);
g->score.current_simult_kill_timer = fix(1);
entity_mark_to_delete(e);
}
}
/* Disown areas of effect linked to dead fighters, and remove the ones with
expired lifetimes */
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
aoe_t *aoe = getcomp(e, aoe);
if(!aoe)
continue;
if(aoe->origin && aoe->origin->deleted)
aoe->origin = NULL;
if(aoe->lifetime <= 0) {
/* Notify origin of area removal */
if(aoe->origin) {
fighter_t *f = getcomp(aoe->origin, fighter);
if(f && f->current_attack == e) {
f->current_attack = NULL;
f->attack_follows_movement = false;
}
}
entity_mark_to_delete(e);
}
}
/* Remove particles bound to dead fighters */
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
particle_t *p = getcomp(e, particle);
if(p && p->bound_to_entity && p->bound_entity->deleted)
entity_mark_to_delete(e);
}
int i = 0;
while(i < g->entity_count) {
if(g->entities[i]->deleted)
game_remove_entity(g, i);
else i++;
}
}
//---
// Generic entity functions
//---
int game_count_entities(game_t const *g, entity_predicate_t *predicate)
{
int total = 0;
for(int i = 0; i < g->entity_count; i++)
total += (predicate(g->entities[i]) != 0);
return total;
}
/* Not re-entrant, but we can deal with that */
static entity_measure_t *gse_measure = NULL;
static entity_t **gse_entities = NULL;
static int gse_compare(const void *ptr1, const void *ptr2)
{
int i1 = *(uint16_t *)ptr1;
int i2 = *(uint16_t *)ptr2;
return gse_measure(gse_entities[i1]) - gse_measure(gse_entities[i2]);
}
int game_sort_entities(game_t const *g, entity_measure_t *measure,
uint16_t **result)
{
int count = 0;
*result = NULL;
for(int i = 0; i < g->entity_count; i++)
count += (measure(g->entities[i]) >= 0);
if(count == 0)
return 0;
/* Initialize array with matching entities in storage order */
uint16_t *array = malloc(count * sizeof *array);
*result = array;
if(array == NULL)
return -1;
for(int i=0, j=0; i < g->entity_count; i++) {
if(measure(g->entities[i]) >= 0)
array[j++] = i;
}
/* Sort and return */
gse_measure = measure;
gse_entities = g->entities;
qsort(array, count, sizeof *array, gse_compare);
return count;
}
//---
// Per-frame update functions
//---
void game_spawn_enemy(game_t *g, int identity, int spawner)
{
level_t const *level = g->level;
/* Select a random spawner if none is specified */
if(spawner < 0)
spawner = rand() % level->spawner_count;
entity_t *e = enemy_make(identity);
g->wave_spawned++;
if(!e) return;
physical_t *p = getcomp(e, physical);
p->x = fix(level->spawner_x[spawner]) + fix(0.5);
p->y = fix(level->spawner_y[spawner]) + fix(0.5);
game_spawn_entity(g, e);
}
void game_spawn_enemies(game_t *g)
{
level_event_t const *event = game_current_event(g);
level_wave_t const *wave = game_current_wave(g);
if(!wave || !g->wave_left) return;
int wave_enemies = 0;
for(int i = 0; i < wave->entry_count; i++)
wave_enemies += wave->entries[i].amount;
int ev_duration_ms = fround(event->duration);
int current_time_ms = fround(g->event_time);
/* Keep spawning enemies until we're up-to-date. The time-based test
makes sure enemies are spawned somewhat regularly and the count-based
test makes sure we stop at the limit even if the delays don't align
exactly at the unit */
while(g->wave_spawned * ev_duration_ms < wave_enemies * current_time_ms
&& g->wave_spawned < wave_enemies) {
/* Select one random enemy from those remaining */
int r = rand() % (wave_enemies - g->wave_spawned);
int entry = -1;
while(r >= 0 && entry < wave->entry_count)
r -= g->wave_left[++entry];
if(entry >= wave->entry_count)
return; /* just in case */
g->wave_left[entry]--;
game_spawn_enemy(g, wave->entries[entry].identity, -1);
}
}
void game_update_animations(game_t *g, fixed_t dt, fixed_t dt_rt)
{
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
if(getcomp(e, visible))
visible_update(e, dt);
}
for(int i = 0; i < g->map->width * g->map->height; i++)
g->map_anim[i] += fround(dt * 1000);
anim_state_update(&g->hud_xp_anim, dt_rt);
anim_state_update(&g->hud_backpack_anim, dt_rt);
g->hud_wave_number_timer = max(0, g->hud_wave_number_timer - dt_rt);
if(g->final_screen_time >= 0)
g->final_screen_time += dt_rt;
}
void game_update_effects(game_t *g, fixed_t dt)
{
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
fighter_t *f = getcomp(e, fighter);
if(!f)
continue;
f->stun_delay = max(f->stun_delay - dt, fix(0));
f->invulnerability_delay = max(f->invulnerability_delay - dt, fix(0));
f->speed_delay = max(f->speed_delay - dt, fix(0));
}
g->combo_health -= dt / 5;
if(g->combo_health <= 0) {
g->combo_health = 0;
if(g->combo >= 30)
game_message(g, fix(2.0), "Fabulous %d-combo!", g->combo);
else if(g->combo >= 10)
game_message(g, fix(2.0), "Excellent %d-combo!", g->combo);
g->score.combo_chains += game_score_combo_chain(g->combo);
g->combo = 0;
}
g->score.current_simult_kill_timer -= dt * 4;
if(g->score.current_simult_kill_timer <= 0) {
g->score.current_simult_kill_timer = 0;
if(g->score.current_simult_kills >= 8)
game_message(g, fix(2.0), "\"This is a massacre!\"");
else if(g->score.current_simult_kills >= 5)
game_message(g, fix(2.0), "\"Begone, idiots!\"");
else if(g->score.current_simult_kills >= 3)
game_message(g, fix(2.0), "\"Come and get me!\"");
g->score.simult_kills +=
game_score_simult_kills(g->score.current_simult_kills);
g->score.current_simult_kills = 0;
}
}
void game_update_aoes(game_t *g, fixed_t dt)
{
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
aoe_t *aoe = getcomp(e, aoe);
if(aoe == NULL)
continue;
/* Movement and collisions, when relevant */
aoe_update(g, e, dt);
rect hitbox = physical_abs_hitbox(e);
// TODO ECS: Move collisions in a proper system
// TODO ECS: Quadratic collision check is a no-no for high performance
for(int i = 0; i < g->entity_count; i++) {
entity_t *target = g->entities[i];
physical_t *p = getcomp(target, physical);
if(p && rect_collide(hitbox, rect_translate(p->hitbox,
(vec2){ p->x, p->y })))
aoe_apply(g, e, target);
}
aoe->lifetime -= dt;
}
}
void game_update_particles(game_t *g, fixed_t dt)
{
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
particle_t *p = getcomp(e, particle);
if(p == NULL)
continue;
bool remove = particle_update(p, dt);
if(remove) entity_mark_to_delete(e);
}
/* Spawn dash particles */
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
mechanical_t *m = getcomp(e, mechanical);
if(m && mechanical_dashing(e)) {
entity_t *p = particle_make_dash(e);
game_add_entity(g, p);
}
}
}
void game_run_cooldowns(game_t *g, fixed_t dt)
{
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
fighter_t *f = getcomp(e, fighter);
if(f == NULL)
continue;
for(int i = 0; i < 6; i++) {
fixed_t *cd = &f->actions_cooldown[i];
*cd = max(*cd - dt, fix(0.0));
}
}
}