RogueLife/src/game.c

467 lines
12 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>
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->time_victory = fix(0);
g->time_defeat = fix(0);
g->entities = NULL;
g->entity_count = 0;
g->level = level;
g->wave = 0;
g->wave_left = NULL;
game_next_wave(g);
g->hud_xp_anim.frame = anims_hud_xp_Idle.start[0];
g->hud_xp_anim.elapsed = 0;
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_wave_t const *game_current_wave(game_t const *g)
{
if(!g->level) return NULL;
if(g->wave < 1 || g->wave > g->level->wave_count) return NULL;
return &g->level->waves[g->wave-1];
}
bool game_current_wave_finished(game_t const *g)
{
level_wave_t const *wave = game_current_wave(g);
if(!wave || !g->wave_left) return false;
int wave_enemies = 0;
for(int i = 0; i < wave->entry_count; i++)
wave_enemies += wave->entries[i].amount;
return g->wave_spawned >= wave_enemies;
}
void game_next_wave(game_t *g)
{
if(g->wave >= g->level->wave_count) return;
g->wave++;
g->wave_spawned = 0;
g->time_wave = fix(0);
g->time_victory = fix(0);
free(g->wave_left);
/* Copy the amounts of monsters to spawn for the next wave */
level_wave_t const *wave = game_current_wave(g);
g->wave_left = malloc(wave->entry_count * sizeof *g->wave_left);
if(!g->wave_left) return;
for(int i = 0; i < wave->entry_count; i++)
g->wave_left[i] = wave->entries[i].amount;
}
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;
}
//---
// 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 */
bool lvup = player_add_xp(g->player, f->enemy->id->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;
}
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);
}
}
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;
}
#if 0
// TODO ECS: New collision/ejection system based on teleports
void game_try_move_entity(game_t *g, entity_t *e,
entity_movement_t const * next)
{
entity_movement_t *m = &e->movement;
rect hitbox = rect_translate(e->hitbox, (vec2){ next->x, next->y });
if(!map_collides(&g->map, hitbox)) {
/* Movement is allowed */
fixed_t dx = next->x - m->x;
fixed_t dy = next->y - m->y;
/* Update attached attack animation */
if(next->facing != m->facing) {
e->attack_follows_movement = false;
}
if(e->current_attack && e->attack_follows_movement) {
e->current_attack->anchor.x += dx;
e->current_attack->anchor.y += dy;
}
*m = *next;
}
else {
/* Movement is denied. Halve speed so that high-speed movement doesn't
halt at a large distance from a wall. */
m->facing = next->facing;
m->vx = next->vx / 2;
m->vy = next->vy / 2;
m->dash = next->dash;
}
}
#endif
//---
// Per-frame update functions
//---
void game_spawn_enemies(game_t *g)
{
level_t const *level = g->level;
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 current_time_ms = fround(g->time_wave);
/* 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 * wave->duration < 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]--;
/* Select a random spawner */
int s = rand() % level->spawner_count;
entity_t *e = enemy_make(wave->entries[entry].identity);
g->wave_spawned++;
if(!e) continue;
physical_t *p = getcomp(e, physical);
p->x = fix(level->spawner_x[s]) + fix(0.5);
p->y = fix(level->spawner_y[s]) + fix(0.5);
game_spawn_entity(g, e);
}
}
void game_update_animations(game_t *g, fixed_t dt)
{
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);
}
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));
}
}
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 < 5; i++) {
fixed_t *cd = &f->actions_cooldown[i];
*cd = max(*cd - dt, fix(0.0));
}
}
}