RogueLife/src/game.c

380 lines
9.9 KiB
C
Raw Normal View History

2021-06-04 15:14:12 +02:00
#include "game.h"
2021-06-15 17:27:30 +02:00
#include "util.h"
2021-07-16 15:51:32 +02:00
#include "enemies.h"
#include "player.h"
2021-06-04 15:14:12 +02:00
#include "comp/fighter.h"
#include "comp/physical.h"
#include "comp/visible.h"
#include "comp/particle.h"
#include "aoe.h"
2021-06-04 15:14:12 +02:00
#include <stdlib.h>
bool game_load(game_t *g, level_t const *level)
2021-06-04 15:14:12 +02:00
{
game_unload(g);
g->map = level->map;
2021-06-04 15:14:12 +02:00
camera_init(&g->camera, g->map);
2021-06-04 15:14:12 +02:00
g->time_total = fix(0);
g->time_victory = fix(0);
g->time_defeat = fix(0);
2021-06-04 15:14:12 +02:00
g->entities = NULL;
g->entity_count = 0;
2021-07-16 15:51:32 +02:00
g->level = level;
g->wave = 0;
g->wave_left = NULL;
game_next_wave(g);
2021-07-16 15:51:32 +02:00
2021-06-04 15:14:12 +02:00
return true;
}
void game_unload(game_t *g)
{
g->map = NULL;
2021-06-04 15:14:12 +02:00
for(int i = 0; i < g->entity_count; i++)
entity_destroy(g->entities[i]);
2021-06-04 15:14:12 +02:00
free(g->entities);
g->entities = NULL;
g->entity_count = 0;
free(g->wave_left);
g->wave_left = NULL;
2021-06-04 15:14:12 +02:00
}
2021-07-16 15:51:32 +02:00
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;
}
2021-07-16 15:51:32 +02:00
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;
2021-07-16 15:51:32 +02:00
}
//---
// Object management functions
//---
2021-06-04 15:14:12 +02:00
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)
2021-07-16 13:31:14 +02:00
{
game_add_entity(g, e);
fighter_t *f = getcomp(e, fighter);
if(!f || f->current_attack) return;
2021-07-16 13:31:14 +02:00
/* Teleport hitbox */
rect hitbox = {
2021-07-16 13:31:14 +02:00
-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;
2021-07-16 13:31:14 +02:00
}
/* 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)
{
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
/* First remove dead fighters */
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_data != NULL && anim_finished) {
/* Disown remaining areas of effect */
fighter_t *f = getcomp(e, fighter);
if(f->current_attack) {
aoe_t *aoe = getcomp(f->current_attack, aoe);
aoe->origin = NULL;
}
/* Give XP points to player */
player_add_xp(g->player_data, f->enemy_data->xp);
entity_mark_to_delete(e);
}
/* Then remove areas of effect with expired lifetime */
aoe_t *aoe = getcomp(e, aoe);
if(aoe && aoe->lifetime <= 0) {
/* Notify origin of area removal */
if(aoe->origin) {
fighter_t *f = getcomp(aoe->origin, fighter);
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
//---
2021-07-16 15:51:32 +02:00
void game_spawn_enemies(game_t *g)
{
level_t const *level = g->level;
2021-07-16 15:51:32 +02:00
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;
2021-07-16 15:51:32 +02:00
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);
2021-07-16 15:51:32 +02:00
}
}
2021-06-09 20:47:39 +02:00
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];
2021-08-30 11:39:14 +02:00
if(getcomp(e, visible))
visible_update(e, dt);
}
}
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;
2021-10-17 18:43:38 +02:00
/* Movement and collisions, when relevant */
aoe_update(g, e, dt);
2021-10-17 18:43:38 +02:00
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;
}
2021-06-09 20:47:39 +02:00
}
2021-06-15 17:27:30 +02:00
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);
}
}
}