RogueLife/src/main.c

641 lines
22 KiB
C

#include "comp/entity.h"
#include "comp/physical.h"
#include "comp/visible.h"
#include "comp/mechanical.h"
#include "comp/fighter.h"
#include "comp/particle.h"
#include "anim.h"
#include "aoe.h"
#include "enemies.h"
#include "game.h"
#include "geometry.h"
#include "item.h"
#include "level.h"
#include "map.h"
#include "menu.h"
#include "pathfinding.h"
#include "player.h"
#include "render.h"
#include "skills.h"
#include <gint/display.h>
#include <gint/keyboard.h>
#include <gint/usb.h>
#include <gint/usb-ff-bulk.h>
#include <gint/cpu.h>
#include <gint/timer.h>
#include <gint/kmalloc.h>
#include <gint/defs/util.h>
#include <gint/rtc.h>
#include <stdlib.h>
#include <string.h>
#include <fxlibc/printf.h>
#include <libprof.h>
/* Record USB frames (used by main menu and game loop) */
bool rogue_life_video_capture = false;
int main(void)
{
/* Enable %f etc. in printf()-like functions */
__printf_enable_fp();
/* Enable %D for decimal fixed-point in printf()-like functions */
__printf_enable_fixed();
/* Initialize the PRNG */
srand(rtc_ticks());
/* Initialize the benchmarking/profiling library */
prof_init();
/* Open the USB connection (for screenshots with fxlink) */
usb_interface_t const *interfaces[] = { &usb_ff_bulk, NULL };
usb_open(interfaces, GINT_CALL_NULL);
int lv = menu_level_select(0);
if(lv == -1) return 1;
game_t game = { 0 };
camera_t *c = &game.camera;
game_load(&game, level_all[lv]);
struct {
/* Developer menu is open */
bool dev_menu;
/* Show developer submenus */
bool dev_menu_view;
bool dev_menu_spawn;
/* Show variables */
bool show_vars;
/* Show hitboxes */
bool show_hitboxes;
/* Show BFS field around player */
bool show_bfs_field;
pfg_all2one_t bfs_field;
/* Show path to some fixed cell */
bool show_path;
pfg_path_t grid_path;
pfc_path_t continuous_path;
/* Show performance metrics */
bool show_perf;
/* Coordinates of a point of interest */
int some_x, some_y;
/* Game is paused */
bool paused;
} debug;
memset(&debug, 0, sizeof debug);
//---
// Spawn player
//---
player_data_t player_data = {
.entity = NULL,
.mechanical_limits = {
.max_speed = fix(4.5),
.friction = fix(0.7),
.dash_speed = fix(20),
.dash_duration = fix(1) / 8,
.max_disruption_speed = fix(3.0),
},
.stats = { .HP=60, .ATK=13, .MAG=10, .DEF=6 },
.stats_growth = { .HP=25, .ATK=1, .MAG=1, .DEF=1 },
.xp_level = 0,
.xp_to_next_level = 0,
.xp_current = 0,
// .inventory = { -1, -1, -1, -1, -1, -1, -1, -1 },
.inventory = { 1, 2, 3, 5, 101, -1, -1, -1 },
.equipment = { -1, -1, -1 },
};
entity_t *player = entity_make(physical, visible, mechanical, fighter);
player_data.entity = player;
physical_t *player_p = getcomp(player, physical);
visible_t *player_v = getcomp(player, visible);
mechanical_t *player_m = getcomp(player, mechanical);
fighter_t *player_f = getcomp(player, fighter);
player_f->combo_length = 2;
player_f->combo_next = 0;
player_f->combo_delay = fix(0);
player_f->enemy = NULL;
player_f->player = &player_data;
for(int i = 0; i < 6; i++)
player_f->skills[i] = -1;
for(int i = 0; i < 6; i++)
player_f->actions_cooldown[i] = fix(0.0);
/* Initialize stats. This will level up to level 1 */
player_f->HP_max = player_data.stats.HP;
player_f->ATK = player_data.stats.ATK;
player_f->MAG = player_data.stats.MAG;
player_f->DEF = player_data.stats.DEF;
player_add_xp(player, 0);
player_f->HP = player_f->HP_max;
player_p->x = fix(game.level->player_spawn_x) + fix(0.5);
player_p->y = fix(game.level->player_spawn_y) + fix(0.5);
player_p->facing = DOWN;
player_p->hitbox = (rect){ -fix(5)/16, fix(5)/16, -fix(2)/16, fix(4)/16 };
player_m->limits = &player_data.mechanical_limits;
player_v->sprite_plane = VERTICAL;
player_v->shadow_size = 4;
visible_set_anim(player, &anims_player_Idle, 1);
game_add_entity(&game, player);
game.player = player;
//---
// Main loop
//---
/* Start a timer at the FRAME_RATE setting to schedule new frames */
volatile int frame_tick = 1;
int timer_id = timer_configure(TIMER_ANY, 1000000 / FRAME_RATE,
GINT_CALL_SET(&frame_tick));
if(timer_id >= 0)
timer_start(timer_id);
bool stop = false;
prof_t perf_render, perf_simul;
uint32_t time_render=0, time_simul=0;
while(!stop) {
while(!frame_tick) sleep();
frame_tick = 0;
bool attack = false;
bool bullet_time = game.menu_open;
/* This assumes the frame is not late; can do better with libprof */
fixed_t dt = fix(1) / FRAME_RATE;
fixed_t dt_rt = dt;
if(bullet_time)
dt >>= 2;
if(debug.paused || debug.dev_menu)
dt = dt_rt = 0;
perf_render = prof_make();
prof_enter(perf_render);
render_game(&game, debug.show_hitboxes);
/* kmalloc_arena_t *_uram = kmalloc_get_arena("_uram");
kmalloc_gint_stats_t *_uram_stats = kmalloc_get_gint_stats(_uram);
dprint(1, 15, C_WHITE, "Memory: %d", _uram_stats->used_memory); */
/* Developer/tweaking menu */
if(debug.show_vars) {
uint32_t *vram = (void *)gint_vram;
for(int y = 0; y < 224; y++) {
for(int i = 0; i < 396/4; i++)
vram[i] = (vram[i] & 0xf7def7de) >> 1;
vram += 396/2;
}
uint16_t gray = C_RGB(16, 16, 16);
dprint(3, 40, C_WHITE, "Player speed: %g tiles/s",
f2double(player_data.mechanical_limits.max_speed));
dprint(15, 55, gray, "[frac] -/+ [X,0,T]");
dprint(3, 70, C_WHITE, "Friction: %g",
f2double(player_data.mechanical_limits.friction));
dprint(15, 85, gray, "[F<>D] -/+ [log]");
dprint(3, 115, C_WHITE, "Dash speed: %g tiles/s",
f2double(player_data.mechanical_limits.dash_speed));
dprint(15, 130, gray, "[(] -/+ [ln]");
dprint(3, 145, C_WHITE, "Dash duration: %g s",
f2double(player_data.mechanical_limits.dash_duration));
dprint(15, 160, gray, "[)] -/+ [sin]");
}
if(debug.dev_menu_view) {
uint32_t *vram = (void *)gint_vram;
for(int i = 0; i < DWIDTH * DHEIGHT / 2; i++)
vram[i] = (vram[i] & 0xf7def7de) >> 1;
int fg = C_RGB(31, 31, 0);
dprint(3, 25, debug.show_hitboxes ? fg : C_WHITE,
"[1] Show hitboxes");
dprint(3, 40, debug.show_bfs_field ? fg : C_WHITE,
"[2] Show BFS field");
dprint(3, 55, debug.show_path ? fg : C_WHITE,
"[3] Show pathfinding to spawner");
}
if(debug.dev_menu_spawn) {
uint32_t *vram = (void *)gint_vram;
for(int i = 0; i < DWIDTH * DHEIGHT / 2; i++)
vram[i] = (vram[i] & 0xf7def7de) >> 1;
dprint(3, 25, C_WHITE, "Spawn an enemy");
for(int i = 0; i < 16; i++) {
int x=12+186*(i/8), y=40+18*(i%8);
enemy_t const *enemy = enemy_data(i+1);
if(!enemy)
continue;
anim_frame_t *frame = enemy->anim_idle->start[0];
dprint(x, y, C_WHITE, "[%d]", i+1);
dsubimage(x+(20+44)/2 - frame->w / 2, y+4 - frame->h / 2,
frame->sheet, frame->x, frame->y, frame->w, frame->h,
DIMAGE_NONE);
dprint(x+44, y, C_WHITE, "%s (lv. %d)", enemy->name,
enemy->level);
}
}
if(debug.show_path) {
if(debug.grid_path.points)
for(int i = 0; i < debug.grid_path.length; i++) {
ivec2 j = camera_map2screen(c,
vec_i2f_center(debug.grid_path.points[i]));
ivec2 k = camera_map2screen(c,
vec_i2f_center(debug.grid_path.points[i+1]));
dline(j.x, j.y, k.x, k.y, C_RGB(31, 0, 31));
}
if(debug.continuous_path.points)
for(int i = 0; i < debug.continuous_path.length; i++) {
ivec2 j = camera_map2screen(c,
debug.continuous_path.points[i]);
ivec2 k = camera_map2screen(c,
debug.continuous_path.points[i+1]);
dline(j.x, j.y, k.x, k.y, C_RGB(0, 31, 31));
}
debug.some_x = game.level->spawner_count > 0 ?
game.level->spawner_x[0] : game.map->width / 2;
debug.some_y = game.level->spawner_count > 0 ?
game.level->spawner_y[0] : game.map->height / 2;
vec2 p = physical_pos(player);
vec2 q = vec_i2f_center((ivec2){ debug.some_x, debug.some_y });
bool clear = raycast_clear_hitbox(game.map,p,q,player_p->hitbox);
ivec2 j = camera_map2screen(c, p);
ivec2 k = camera_map2screen(c, q);
dline(j.x, j.y, k.x, k.y, clear ? C_GREEN : C_RED);
}
if(debug.show_bfs_field && game.paths_to_player.direction) {
render_pfg_all2one(&game.paths_to_player, c, game.occupation);
}
if(debug.show_perf) {
extern uint32_t time_render_map, time_render_hud;
dprint(1, 15, C_WHITE, "Render: map %.3D, hud %.3D, total %.3D ms",
time_render_map, time_render_hud, time_render);
dprint(1, 29, C_WHITE, "Simul: %.3D ms", time_simul);
}
if(debug.dev_menu) {
int fg = C_RGB(31, 31, 0);
fkey_button(1, "PARAMS", debug.show_vars ? fg : C_WHITE);
fkey_button(2, "VIEW", debug.dev_menu_view ? fg : C_WHITE);
fkey_button(3, "SPAWN", debug.dev_menu_spawn ? fg : C_WHITE);
fkey_button(4, "GOD", C_WHITE);
fkey_button(5, "PERF", debug.show_perf ? fg : C_WHITE);
fkey_button(6, "PAUSE", debug.paused ? fg : C_WHITE);
}
if(keydown(KEY_F6) && keydown(KEY_ALPHA) && debug.dev_menu
&& usb_is_open()) {
rogue_life_video_capture = !rogue_life_video_capture;
}
if(rogue_life_video_capture) {
usb_fxlink_videocapture(false);
}
/* Instead of dupdate(); for accurate performance measurements. Leaving
the DMA running during the simulation affects the results in extreme
proportions (can turn 1 ms of simulation into 11 ms measured) */
dupdate();
prof_leave(perf_render);
time_render = prof_time(perf_render);
//---
perf_simul = prof_make();
prof_enter(perf_simul);
game_spawn_enemies(&game);
key_event_t ev;
while((ev = pollevent()).type != KEYEV_NONE) {
if(ev.type == KEYEV_UP) continue;
if(ev.key == KEY_MENU)
gint_osmenu();
if(ev.key == KEY_EXIT)
stop = true;
/* Debug settings */
if(ev.key == KEY_OPTN) {
debug.dev_menu ^= 1;
if(!debug.dev_menu) {
debug.dev_menu_view = 0;
debug.dev_menu_spawn = 0;
}
}
/* Main debug menu entries */
if(ev.key == KEY_F1 && debug.dev_menu)
debug.show_vars ^= 1;
if(ev.key == KEY_F2 && debug.dev_menu) {
debug.dev_menu_view ^= 1;
if(debug.dev_menu_view)
debug.dev_menu_spawn = 0;
}
if(ev.key == KEY_F3 && debug.dev_menu) {
debug.dev_menu_spawn ^= 1;
if(debug.dev_menu_spawn)
debug.dev_menu_view = 0;
}
if(ev.key == KEY_F4 && debug.dev_menu) {
player_add_xp(game.player, 1000);
// fighter_effect_invulnerability(game.player, fix(999.0));
player_f->skills[1] = AOE_SHOCK;
player_f->skills[2] = AOE_JUDGEMENT;
player_f->skills[3] = SKILL_DASH;
player_f->skills[4] = AOE_BULLET;
}
if(ev.key == KEY_F5 && debug.dev_menu)
debug.show_perf ^= 1;
if(ev.key == KEY_F6 && debug.dev_menu)
debug.paused ^= 1;
if(ev.key == KEY_F2 && debug.dev_menu) {
}
/* Debug menu: View */
if(ev.key == KEY_1 && debug.dev_menu_view)
debug.show_hitboxes ^= 1;
if(ev.key == KEY_2 && debug.dev_menu_view)
debug.show_bfs_field ^= 1;
if(ev.key == KEY_3 && debug.dev_menu_view)
debug.show_path ^= 1;
/* Debug menu: Spawn */
if(keycode_digit(ev.key) >= 0 && debug.dev_menu_spawn) {
int id = keycode_digit(ev.key);
game_spawn_enemy(&game, id, -1);
}
#if 0
if(ev.key == KEY_XOT)
player_data.mechanical_limits.max_speed += fix(1)/8;
if(ev.key == KEY_FRAC) {
player_data.mechanical_limits.max_speed -= fix(1)/8;
if(player_data.mechanical_limits.max_speed < 0)
player_data.mechanical_limits.max_speed = 0;
}
if(ev.key == KEY_LOG) {
player_data.mechanical_limits.friction += fix(1) / 32;
if(player_data.mechanical_limits.friction > 1)
player_data.mechanical_limits.friction = 1;
}
if(ev.key == KEY_FD) {
player_data.mechanical_limits.friction -= fix(1) / 32;
if(player_data.mechanical_limits.friction <= 0)
player_data.mechanical_limits.friction = 0;
}
if(ev.key == KEY_LN)
player_data.mechanical_limits.dash_speed += fix(0.5);
if(ev.key == KEY_LEFTP) {
player_data.mechanical_limits.dash_speed -= fix(0.5);
if(player_data.mechanical_limits.dash_speed <= 0)
player_data.mechanical_limits.dash_speed = 0;
}
if(ev.key == KEY_SIN)
player_data.mechanical_limits.dash_duration += fix(1) / 64;
if(ev.key == KEY_RIGHTP) {
player_data.mechanical_limits.dash_duration -= fix(1) / 64;
if(player_data.mechanical_limits.dash_duration <= 0)
player_data.mechanical_limits.dash_duration = 0;
}
if(ev.key == KEY_PLUS)
camera_zoom(c, c->zoom + 1);
if(ev.key == KEY_MINUS)
camera_zoom(c, c->zoom - 1);
#endif
if(!debug.paused && !game.menu_open && ev.key == KEY_SHIFT
&& !debug.dev_menu)
attack = true;
/* Menus */
if(!debug.paused && ev.key == KEY_F6 && !debug.dev_menu
&& !keydown(KEY_ALPHA))
game.menu_open = !game.menu_open;
/* Inventory movement */
if(!debug.paused && game.menu_open) {
int y = game.menu_cursor / 4, x = game.menu_cursor % 4;
y = y + (ev.key == KEY_DOWN) - (ev.key == KEY_UP);
x = x + (ev.key == KEY_RIGHT) - (ev.key == KEY_LEFT);
y = max(0, min(y, 1));
x = max(0, min(x, 3));
game.menu_cursor = 4 * y + x;
}
/* Equipping and unequipping items */
if(!debug.paused && game.menu_open && ev.key == KEY_SHIFT) {
int item = player_data.inventory[game.menu_cursor];
int slot = item_equipment_slot(item);
if(item >= 0 && slot >= 0) {
if(player_data.equipment[slot] == game.menu_cursor)
player_data.equipment[slot] = -1;
else
player_data.equipment[slot] = game.menu_cursor;
/* Update skills */
player_compute_skills(player, player_data.equipment,
player_f->skills);
}
}
}
/* Camera movement */
#if 0
fixed_t vx = CAMERA_SPEED_X;
fixed_t vy = CAMERA_SPEED_Y;
if(keydown(KEY_4) || keydown(KEY_7) || keydown(KEY_1))
camera_move(c, -fmul(dt, vx), 0);
if(keydown(KEY_6) || keydown(KEY_9) || keydown(KEY_3))
camera_move(c, fmul(dt, vx), 0);
if(keydown(KEY_8) || keydown(KEY_7) || keydown(KEY_9))
camera_move(c, 0, -fmul(dt, vy));
if(keydown(KEY_2) || keydown(KEY_1) || keydown(KEY_3))
camera_move(c, 0, fmul(dt, vy));
#endif
/* Player movement input */
int input_dir = -1, next_dir = -1;
if(!debug.paused && !game.menu_open && player_f->HP > 0) {
if(keydown(KEY_UP)) input_dir = UP;
if(keydown(KEY_DOWN)) input_dir = DOWN;
if(keydown(KEY_LEFT)) input_dir = LEFT;
if(keydown(KEY_RIGHT)) input_dir = RIGHT;
}
next_dir = (input_dir >= 0) ? input_dir : player_p->facing;
/* Player skills (including movement skills) */
bool can_use_skill = !debug.paused && !game.menu_open
&& player_f->HP > 0 && !debug.dev_menu;
if(can_use_skill && keydown(KEY_F1))
skill_use(&game, player, 1, fdir(next_dir));
if(can_use_skill && keydown(KEY_F2))
skill_use(&game, player, 2, fdir(next_dir));
if(can_use_skill && keydown(KEY_F3))
skill_use(&game, player, 3, fdir(next_dir));
if(can_use_skill && keydown(KEY_F4))
skill_use(&game, player, 4, fdir(next_dir));
/* Player movement */
if(!debug.paused && !game.menu_open && player_f->HP > 0) {
mechanical_move4(player, input_dir, dt, game.map);
if(input_dir >= 0)
visible_set_anim(player, &anims_player_Walking, 1);
else
visible_set_anim(player, &anims_player_Idle, 1);
}
/* Directions to reach the player from anywhere on the grid */
game_compute_occupation(&game);
pfg_all2one_free(&game.paths_to_player);
game.paths_to_player = pfg_dijkstra(game.map,
vec_f2i(physical_pos(player)), game.occupation);
/* Enemy AI */
if(player_f->HP > 0)
for(int i = 0; i < game.entity_count; i++) {
entity_t *e = game.entities[i];
fighter_t *f = getcomp(e, fighter);
mechanical_t *m = getcomp(e, mechanical);
if(!f || !m || !f->enemy || f->HP == 0)
continue;
enemy_ai(&game, e, dt);
if(mechanical_moving(e))
visible_set_anim(e, f->enemy->id->anim_walking, 1);
else
visible_set_anim(e, f->enemy->id->anim_idle, 1);
}
/* Player attack */
if(!debug.paused && !game.menu_open && player_f->HP > 0 && attack
&& !player_f->current_attack) {
int hit_number=0, effect=AOE_SLASH;
/* If hitting within .25s of the previous hit ending, combo! */
if(abs(player_f->combo_delay) < fix(0.25))
hit_number = player_f->combo_next;
player_f->combo_next = (hit_number + 1) % player_f->combo_length;
if(hit_number == 0) effect = AOE_SLASH;
if(hit_number == 1) effect = AOE_IMPALE;
entity_t *aoe = aoe_make_attack_4(effect, player,player_p->facing);
getcomp(aoe, visible)->z = fix(0.25);
game_add_entity(&game, aoe);
visible_set_anim(player, &anims_player_Attack, 2);
player_f->current_attack = aoe;
player_f->attack_follows_movement = true;
player_f->combo_delay = getcomp(aoe, aoe)->lifetime;
}
/* Ideas for additional skills:
- Freeze enemies
- Barrier around player
- Teleport
- Time manipulation
- Player buffs (short but strong) or wide area debuffs
Ideas for items:
- Healing potion
- Equipment
- XP boosts
- Weaker but longer-lasting buffs */
game_update_animations(&game, dt);
game_update_effects(&game, dt);
game_update_aoes(&game, dt);
game_update_particles(&game, dt);
// TODO: Kill out-of-bounds entities
game_remove_dead_entities(&game);
/* Update combo and action cooldowns */
player_f->combo_delay -= dt;
game_run_cooldowns(&game, dt);
/* Reset default anims */
if(!player_v->anim.frame)
visible_set_anim(player, &anims_player_Idle, 1);
for(int i = 0; i < game.entity_count; i++) {
entity_t *e = game.entities[i];
fighter_t *f = getcomp(e, fighter);
visible_t *v = getcomp(e, visible);
if(f && f->enemy && v && !v->anim.frame) {
visible_set_anim(e, f->enemy->id->anim_idle, 1);
}
}
/* Reduce screenshake time */
game.screenshake_duration -= dt;
if(game.screenshake_duration < 0) {
game.screenshake_duration = 0;
game.screenshake_amplitude = 0;
}
/* Menu animation */
if(game.menu_open && game.menu_time < fix(1)) {
game.menu_time = min(game.menu_time + 2 * dt_rt, fix(1));
}
else if(!game.menu_open && game.menu_time > fix(0)) {
game.menu_time = max(game.menu_time - 2 * dt_rt, fix(0));
}
game.time_total += dt;
game.event_time += dt;
/* Next wave */
if(player_f->HP > 0 && game_current_event_finished(&game))
game_next_event(&game);
/* Visual pathfinding debug */
if(debug.show_path) {
pfg_path_free(&debug.grid_path);
pfc_path_free(&debug.continuous_path);
vec2 target = vec_i2f_center((ivec2){ debug.some_x,debug.some_y });
debug.grid_path = pfg_outwards(&game.paths_to_player,
vec_f2i(target));
debug.continuous_path = pfc_shortcut_full(&debug.grid_path,
physical_pos(player), target, player_p->hitbox);
}
prof_leave(perf_simul);
time_simul = prof_time(perf_simul);
}
timer_stop(timer_id);
prof_quit();
usb_close();
return 1;
}