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