level end screen and back to main menu

* Add a level end screen that shows automatically when dead or level
  is finished
* Lock player controls and GUI before the end screen shows
* Loop back to the main menu after finishing a level (or dying)
* Nerf combo chain score
* Add a placeholder KO animation for the player, and associated logic
This commit is contained in:
Lephenixnoir 2023-01-02 11:50:25 +01:00
parent c1122f511f
commit 80fda55e98
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
14 changed files with 220 additions and 31 deletions

14
TODO
View File

@ -1,6 +1,13 @@
Pixel art to do
===============
- Player KO animation. The timeline of a KO is:
1. When the player's HP reaches 0, "KO" is played ("falling" animation)
2. When "KO" finishes:
(a) "IdleKO" is played in a loop once ("lying down" animation)
(b) Simultaneously, end screen appears
It is possible to include some idle frames in "KO" in order to delay the
appearance of the end screen.
- More varied attacks:
* Sharp shadow attack (see skill icon for inspiration)
* Circular slash? (We already have shock)
@ -19,9 +26,6 @@ Programming to do
=================
Core mechanics:
* Compute a score at the end of each level
- Only increases, so we can show its real-time value
- Based on: combo chains, simult-kills, one-shot kills, waves survived
* Have a deterministic mode for grinding
* Use for items in battle (potions mainly)
* Infinite mode for every level
@ -36,6 +40,10 @@ Core mechanics:
Content:
* Additional skills
Infrastructure:
* Better end screen
* Save and load scores (including with bindings)
Bugfixes:
* Fix ability to attack continuously by holding SHIFT

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -2,5 +2,5 @@ player_*.aseprite:
custom-type: aseprite-anim
name_regex: (.*)\.aseprite frames_\1
center: 11, 19
next: Idle=Idle, Walking=Walking
next: Idle=Idle, Walking=Walking, KO=IdleKO, IdleKO=IdleKO
profile: p4

Binary file not shown.

View File

@ -55,6 +55,8 @@ ANIM4(player, Idle);
ANIM4(player, Walking);
ANIM4(player, Attack);
ANIM4(player, Hit);
ANIM4(player, KO);
ANIM4(player, IdleKO);
/* HUD */
ANIM1(hud_xp_Idle);

View File

@ -99,6 +99,8 @@ extern anim_t anims_player_Idle;
extern anim_t anims_player_Walking;
extern anim_t anims_player_Attack;
extern anim_t anims_player_Hit;
extern anim_t anims_player_KO;
extern anim_t anims_player_IdleKO;
/* HUD */
extern anim_t anims_hud_xp_Idle;

View File

@ -81,7 +81,10 @@ int fighter_damage(entity_t *e, int base_damage)
visible_set_anim(e, f->enemy->id->anim_hit, 3);
}
else {
visible_set_anim(e, &anims_player_Hit, 3);
if(f->HP == 0)
visible_set_anim(e, &anims_player_KO, 4);
else
visible_set_anim(e, &anims_player_Hit, 3);
}
return damage;

View File

@ -51,6 +51,9 @@ bool game_load(game_t *g, level_t const *level)
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);
@ -216,7 +219,12 @@ void game_hud_anim_backpack_close(game_t *g)
static int game_score_combo_chain(int chain)
{
return (chain * chain) / 2;
if(chain < 3)
return 0;
else if(chain < 30)
return chain * 5;
else
return chain * 10;
}
static int game_score_simult_kills(int kills)
@ -237,6 +245,45 @@ int game_compute_score(game_t const *g)
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
//---
@ -499,6 +546,9 @@ void game_update_animations(game_t *g, fixed_t dt, fixed_t 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)

View File

@ -89,6 +89,12 @@ typedef struct game {
bool menu_open;
/* Cursor within the inventory menu */
int menu_cursor;
/* Time when the game was finished (0 while playing) */
fixed_t finish_time;
/* Whether the game was won (valid when finish_time > 0) */
bool victory;
/* Final screen time (negative while playing, >= 0 when game finished) */
fixed_t final_screen_time;
/* Current UI message, and how long it stays on (if not overwritten) */
char const *message;
@ -130,6 +136,11 @@ void game_hud_anim_backpack_close(game_t *g);
/* Compute total score */
int game_compute_score(game_t const *g);
/* Determine whethey victory was achieved */
bool game_victory_achieved(game_t const *g);
/* Determine whether players have lost */
bool game_defeated(game_t const *g);
//---
// Managing dynamic game elements
//---

View File

@ -38,20 +38,8 @@
/* Record USB frames (used by main menu and game loop) */
bool rogue_life_video_capture = false;
int main(void)
static int menu_select_play_repeat(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;
@ -356,7 +344,8 @@ int main(void)
if(ev.key == KEY_MENU)
gint_osmenu();
if(ev.key == KEY_EXIT)
if(ev.key == KEY_EXIT && game.finish_time > 0 &&
game.time_total >= game.finish_time + fix(2.0))
stop = true;
/* Debug settings */
@ -454,13 +443,13 @@ int main(void)
camera_zoom(c, c->zoom - 1);
#endif
if(!debug.paused && !game.menu_open && ev.key == KEY_SHIFT
&& !debug.dev_menu)
if(!debug.paused && !game.menu_open && !(game.finish_time > 0)
&& ev.key == KEY_SHIFT && !debug.dev_menu)
attack = true;
/* Menus */
if(!debug.paused && ev.key == KEY_F6 && !debug.dev_menu
&& !keydown(KEY_ALPHA)) {
if(!debug.paused && !debug.dev_menu && !(game.finish_time > 0)
&& ev.key == KEY_F6 && !keydown(KEY_ALPHA)) {
if(game.menu_open)
game_hud_anim_backpack_close(&game);
else
@ -469,7 +458,7 @@ int main(void)
}
/* Inventory movement */
if(!debug.paused && game.menu_open) {
if(!debug.paused && game.menu_open && !(game.finish_time > 0)) {
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);
@ -479,7 +468,8 @@ int main(void)
}
/* Using and equipping items */
if(!debug.paused && game.menu_open && ev.key == KEY_SHIFT) {
if(!debug.paused && game.menu_open && !(game.finish_time > 0)
&& ev.key == KEY_SHIFT) {
int item = player_data.inventory[game.menu_cursor];
int slot = item_equipment_slot(item);
@ -525,17 +515,23 @@ int main(void)
/* Player movement input */
int input_dir = -1, next_dir = -1;
if(!debug.paused && !game.menu_open && player_f->HP > 0) {
if(!debug.paused && !game.menu_open && !(game.finish_time > 0)
&& 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;
if(game.finish_time > 0)
next_dir = -1;
else if(input_dir >= 0)
next_dir = input_dir;
else
next_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;
&& !(game.finish_time > 0) && player_f->HP > 0 && !debug.dev_menu;
if(can_use_skill && keydown(KEY_F1))
skill_use(&game, player, 1, fdir(next_dir));
@ -580,8 +576,8 @@ int main(void)
}
/* Player attack */
if(!debug.paused && !game.menu_open && player_f->HP > 0 && attack
&& !player_f->current_attack) {
if(!debug.paused && !game.menu_open && !(game.finish_time > 0)
&& player_f->HP > 0 && attack && !player_f->current_attack) {
if(player_f->skills[0] == AOE_SLASH) {
int hit_number=0, effect=AOE_SLASH;
@ -676,6 +672,23 @@ int main(void)
if(player_f->HP > 0 && game_current_event_finished(&game))
game_next_event(&game);
/* Victory or game over */
if(!game.finish_time && game_defeated(&game)) {
game.finish_time = game.time_total;
game.victory = false;
game.menu_open = false;
}
else if(!game.finish_time && game_victory_achieved(&game)) {
game.finish_time = game.time_total;
game.victory = true;
game.menu_open = false;
}
/* Start the final screen when everything has settled */
if(game.finish_time > 0 && !game.menu_time
&& game.final_screen_time < 0) {
game.final_screen_time = fix(0);
}
/* Visual pathfinding debug */
if(debug.show_path) {
pfg_path_free(&debug.grid_path);
@ -693,6 +706,27 @@ int main(void)
}
timer_stop(timer_id);
return 0;
}
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);
while(1) {
menu_select_play_repeat();
}
prof_quit();
usb_close();
return 1;

View File

@ -418,6 +418,42 @@ static void render_panel(int x, int y, bool hflip)
dsubimage(x, y, &img_hud_panel, sx, sy, w, 30, DIMAGE_NONE);
}
void render_full_panel(int x, int y, int w, int h)
{
extern bopti_image_t img_hud_panel;
w = max(w, 100);
h = max(h, 60);
/* Top row */
dsubimage(x, y, &img_hud_panel, 0, 0, 50, 30, DIMAGE_NONE);
for(int sx = 50; sx < w-50;) {
int frame_w = min(w-50-sx, 44);
dsubimage(x+sx, y, &img_hud_panel, 50, 0, frame_w, 30, DIMAGE_NONE);
sx += frame_w;
}
dsubimage(x+w-50, y, &img_hud_panel, 94, 0, 50, 30, DIMAGE_NONE);
/* Middle row */
for(int sy = 30; sy < h-30;) {
int frame_h = min(h-30-sy, 10);
dsubimage(x, y+sy, &img_hud_panel, 0, 30, 50, 10, DIMAGE_NONE);
dsubimage(x+w-50, y+sy, &img_hud_panel, 94, 30, 50, 10, DIMAGE_NONE);
sy += frame_h;
}
if(w > 100 && h > 60)
drect(x+50, y+30, x+w-51, y+h-31, RGB24(0x202828));
/* Bottom row */
dsubimage(x, y+h-30, &img_hud_panel, 0, 40, 50, 30, DIMAGE_NONE);
for(int sx = 50; sx < w-50;) {
int frame_w = min(w-50-sx, 44);
dsubimage(x+sx, y+h-30, &img_hud_panel, 50, 40, frame_w, 30,
DIMAGE_NONE);
sx += frame_w;
}
dsubimage(x+w-50, y+h-30, &img_hud_panel, 94, 40, 50, 30, DIMAGE_NONE);
}
uint32_t time_render_map = 0;
uint32_t time_render_hud = 0;
@ -837,6 +873,49 @@ void render_game(game_t const *g, bool show_hitboxes)
}
}
/* Render final panel */
if(g->final_screen_time >= 0 && g->victory) {
int PANEL_Y = cubic(-(DHEIGHT-30), 30, g->final_screen_time, fix(2.0));
render_full_panel(30, PANEL_Y, DWIDTH-30*2, DHEIGHT-30-40);
dtext_opt(DWIDTH/2, PANEL_Y+12, C_WHITE, C_NONE, DTEXT_CENTER,
DTEXT_TOP, "Victory!", -1);
dprint(45, PANEL_Y+30, 0x5555, "Score");
dprint(300, PANEL_Y+30, 0x5555, "%d", game_compute_score(g));
dprint(45, PANEL_Y+45, C_WHITE, "Time survived");
dprint(300, PANEL_Y+45, C_WHITE, "%d",
g->score.waves_survived);
dprint(45, PANEL_Y+60, C_WHITE, "Kills (%d one-shot)",
g->score.one_shot_kills);
dprint(300, PANEL_Y+60, C_WHITE, "%d",
g->score.kill_number);
dprint(45, PANEL_Y+75, C_WHITE, "Combos (longest: %d)",
g->score.longest_combo_chain);
dprint(300, PANEL_Y+75, C_WHITE, "%d",
g->score.combo_chains);
dprint(45, PANEL_Y+90, C_WHITE, "Simult. kills (largest: %d)",
g->score.largest_simult_kill);
dprint(300, PANEL_Y+90, C_WHITE, "%d",
g->score.simult_kills);
dtext_opt(DWIDTH/2, PANEL_Y+120, C_WHITE, C_NONE, DTEXT_CENTER,
DTEXT_TOP, "EXIT: Back to menu", -1);
}
if(g->final_screen_time >= 0 && !g->victory) {
int PANEL_Y = cubic(-60, DHEIGHT/2-30, g->final_screen_time, fix(2.0));
render_full_panel(DWIDTH/2-90, PANEL_Y, 180, 60);
dtext_opt(DWIDTH/2, PANEL_Y+15, C_RED, C_NONE, DTEXT_CENTER,
DTEXT_TOP, "Defeat!", -1);
dtext_opt(DWIDTH/2, PANEL_Y+30, C_WHITE, C_NONE, DTEXT_CENTER,
DTEXT_TOP, "EXIT: Back to menu", -1);
}
prof_leave(ctx);
time_render_hud = prof_time(ctx);
dfont(old_font);