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:
parent
c1122f511f
commit
80fda55e98
14
TODO
14
TODO
|
@ -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 |
|
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
52
src/game.c
52
src/game.c
|
@ -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)
|
||||
|
|
11
src/game.h
11
src/game.h
|
@ -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
|
||||
//---
|
||||
|
|
84
src/main.c
84
src/main.c
|
@ -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;
|
||||
|
|
79
src/render.c
79
src/render.c
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue