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