#include #include #include #include #include #include #include #include #include #include #include "duet.h" /* Game state */ typedef enum { State_Playing, State_Dead, State_Rewind, State_Transition, State_EpisodeTransition, State_Finale, } state_t; /* All levels, classified by episode */ episode_t episodes[] = { { .name = "ignorance", .level_count = 5, .levels = (level_t *[]){ &level1_1, &level1_2, &level1_3, &level1_4, &level1_5 }, }, { .name = "denial", .level_count = 6, .levels = (level_t *[]){ &level2_1, &level2_2, &level2_3, &level2_4, &level2_5, &level2_6 }, }, { .name = "anger", .level_count = 5, .levels = (level_t *[]){ &level3_1, &level3_2, &level3_3, &level3_4, &level3_5 }, }, { .name = "bargaining", .level_count = 6, .levels = (level_t *[]){ &level4_1, &level4_2, &level4_3, &level4_4, &level4_5, &level4_6 }, }, { .name = "guilt", .level_count = 6, .levels = (level_t *[]) { &level5_1, &level5_2, &level5_3, &level5_4, &level5_5, &level5_6 }, }, { .name = "depression", .level_count = 6, .levels = (level_t *[]){ &level6_1, &level6_2, &level6_3, &level6_4, &level6_5, &level6_6 }, }, { .name = "hope", .level_count = 6, .levels = (level_t *[]){ &level7_1, &level7_2, &level7_3, &level7_4, &level7_5, &level7_6 }, }, { .name = "acceptance", .level_count = 9, .levels = (level_t *[]){ &level8_1, &level8_2, &level8_3, &level8_4, &level8_5, &level8_6, &level8_7, &level8_8, &level8_9 }, }, }; int episode_count = sizeof(episodes) / sizeof(episodes[0]); void load_level(game_t *game, level_t const *lv) { game->level = lv; game->time = -11.0; game->rects = realloc(game->rects, lv->block_count * sizeof *game->rects); if(!game->rects) exit(1); for(int i = 0; i < lv->block_count; i++) { game->rects[i].meta = &lv->blocks[i]; rect_load(&game->rects[i], &lv->blocks[i]); } game->rect_count = lv->block_count; } int strcount(char const *str, int c) { int count = 0; for(int i = 0; str[i]; i++) count += (str[i] == c); return count; } /* Round player rotation to closest multiple of M_PI */ float rpr(float rota) { if(fabs(rota) <= M_PI_2) return 0; return (rota < 0) ? -M_PI : M_PI; } /* Forced player rotation to go back to an even position */ float fpr(float rota, float duration) { return (rpr(rota) - rota) / duration; } void game_loop(int current_episode, int current_level) { game_t game; memset(&game, 0x00, sizeof game); load_level(&game, episodes[current_episode].levels[current_level]); state_t state = State_Playing; /* Direction of time (with a factor regulating game speed) */ float const time_direction_forward = 2.7; float const time_direction_rewind = -25; /* Level time to start (negative to leave time before blocks arrive) */ float const LEVEL_START = -14.0; /* Level time until which message is shown */ float const MESSAGE_END = -5.0; /* Duration of death pause (seconds) */ float const DEATH_PAUSE_DURATION = 0.7; /* Level time to restarting playing at after rewind */ float const REWIND_END = -5.0; /* Duration of level transitions (seconds) */ float const TRANSITION_DURATION = 1.0; /* Duration of episode transitions (seconds) Warning: overlaps with pre-level, will lock player if too long x_x */ float const EPISODE_TRANSITION_DURATION = 3.0; volatile int need_frame = 1; int timer = timer_configure(TIMER_ANY, 33000, GINT_CALL_SET(&need_frame)); if(timer >= 0) timer_start(timer); /* Frame duration in real time */ float const FRAME_DURATION = 1.0 / 30; if(current_level == 0) { state = State_EpisodeTransition; game.time_episode_transition = 0; game.time = LEVEL_START; } while (1) { level_t const *lv = game.level; while (need_frame == 0) sleep(); need_frame = 0; /* Time management */ float time_direction = time_direction_forward; if(state == State_Rewind) time_direction = time_direction_rewind; if(state == State_Playing) time_direction *= lv->speed_factor; if(state == State_Dead) { game.time_dead += FRAME_DURATION; if(game.time_dead >= DEATH_PAUSE_DURATION) { game.time_dead = 0; game.forced_player_rota = fpr(game.player_rota, REWIND_END - game.time); state = State_Rewind; } continue; /* Pause effect */ } if(state == State_Transition) { time_direction = 1.0; game.time_transition += FRAME_DURATION; if(game.time_transition >= TRANSITION_DURATION) { game.time_transition = 0; game.forced_player_rota = 0; game.player_rota = rpr(game.player_rota); state = State_Playing; } } if(state == State_EpisodeTransition) { time_direction = 1.0; game.time_episode_transition += FRAME_DURATION; if(game.time_episode_transition >= TRANSITION_DURATION) { game.forced_player_rota = 0; game.player_rota = rpr(game.player_rota); } if(game.time_episode_transition >= EPISODE_TRANSITION_DURATION) { game.time_episode_transition = 0; state = State_Playing; } } float dt = FRAME_DURATION * time_direction; game.time += dt; /* Finale */ if(state == State_Finale) { game.time_finale += dt; if(game.time_finale >= 0.5) { float t = game.time_finale - 0.5; game.player_rota -= 1.57 * t / 3 * dt; } } /* Input analysis */ bool rotate_left=false, rotate_right=false; key_event_t e; bool stop_playing=false; while ((e = pollevent()).type != KEYEV_NONE) { if (e.type == KEYEV_DOWN && e.key == KEY_MENU) gint_osmenu(); if (e.type == KEYEV_DOWN && e.key == KEY_EXIT) stop_playing = true; } if (keydown(KEY_7)) { rotate_left = true; } if (keydown(KEY_0)) { rotate_right = true; } if ((keydown(KEY_0) || keydown(KEY_7)) && state == State_Finale && game.time_finale >= 16.0) { stop_playing = true; } if (stop_playing) break; /* Level transitions */ // End of level if(state == State_Playing && game.time >= lv->blocks[lv->block_count-1].time + 5) { bool changed_episode = false; current_level++; if(current_level >= episodes[current_episode].level_count) { current_episode++; current_level = 0; changed_episode = true; } if(current_episode >= episode_count) { state = State_Finale; continue; } load_level(&game, episodes[current_episode].levels[current_level]); if(changed_episode) { state = State_EpisodeTransition; game.time_episode_transition = 0; game.forced_player_rota = fpr(game.player_rota, TRANSITION_DURATION); game.time = LEVEL_START; continue; } else { state = State_Transition; game.time_transition = 0; game.forced_player_rota = fpr(game.player_rota, TRANSITION_DURATION); continue; } } // End of rewind if(state == State_Rewind && game.time <= REWIND_END) { game.time = REWIND_END; game.forced_player_rota = 0; game.player_rota = rpr(game.player_rota); state = State_Playing; continue; } /* Physics */ if(state == State_Playing && player_collision(&game)) { state = State_Dead; game.time_dead = 0; continue; } for(int i = 0; i < game.rect_count; i++) { rect_physics(&game.rects[i], game.rects[i].meta, game.time); } if(game.forced_player_rota) game.player_rota += game.forced_player_rota * dt; else { if(state == State_Playing && rotate_left) game.player_rota += 1.57 * dt; if(state == State_Playing && rotate_right) game.player_rota -= 1.57 * dt; } if(game.player_rota > M_PI) game.player_rota -= 2 * M_PI; if(game.player_rota < -M_PI) game.player_rota += 2 * M_PI; /* Rendering */ dclear(C_BLACK); // Cool animated background int bg_color = 4; if(state == State_Finale) bg_color = (game.time_finale >= 2) ? 0 : 4 - 2 * game.time_finale; for(int i = 0; i < 10; i++) { int x=DWIDTH+2-40*i-20, y=20; rect_t rect = { .x=x, .y=y, .w=20, .h=20, .r=1.0*game.time, .opacity=256 }; drectoid(&rect, 0, C_RGB(bg_color, bg_color, bg_color)); } for(int j = 1; j < 6; j++) { int lines = (j == 5) ? 24 : 40; dma_memcpy(gint_vram + 396*(40*j), gint_vram, 396*2*lines); } if(state == State_Playing && game.time < MESSAGE_END && lv->message) { int x = DWIDTH/2 + 8 * (strcount(lv->message, '\n') + 1); /* Split at newlines */ char const *str = lv->message; while(*str) { char const *end = strchrnul(str, '\n'); duet_text_opt(x, DHEIGHT/2, C_WHITE, C_NONE, DTEXT_CENTER, DTEXT_MIDDLE, str, end - str); x -= 16; str = end + (*end != 0); } } if(state == State_EpisodeTransition) { duet_text_opt(DWIDTH/2, DHEIGHT/2, C_WHITE, C_NONE, DTEXT_CENTER, DTEXT_MIDDLE, episodes[current_episode].name, -1); } if(state == State_Finale && game.time_finale >= 11.0) { float t = game.time_finale - 11.0; float rt = fminf(1.0, t / 4.0); float opacity = fminf(1.0, t / 2.0); render_glow(DWIDTH/2, DHEIGHT/2, 24*rt, 20*rt, C_RGB(15,12,13), C_RGB(10,5,8), -t / 2.0, opacity); } float opacity = 1.0; int x = PLAYER_X; int r = PLAYER_R; if(state == State_Finale) { opacity = fmaxf(1 - game.time_finale / 2, 0); /* Interpolate between x1 and x2 in 10.0 seconds along (1-x)² */ int x1 = PLAYER_X, x2 = DWIDTH / 2; float t = fminf(game.time_finale / 10.0, 1.0); t = 1 - (1-t)*(1-t); x = (1 - t) * x1 + t * x2; /* Interpolate radius between PLAYER_R and 0 in 10.0 seconds */ t = fmaxf(0.0, fminf(1.0, (game.time_finale - 2.0) / 10.0)); r = PLAYER_R * (1 - t); } render_player(x, DHEIGHT/2, game.player_rota, r, opacity); /* Radial fade out */ if(state == State_Finale && game.time_finale >= 12.0) { float t = fminf(1.0, (game.time_finale - 11.0) / 4.0); int r1 = PLAYER_SIZE + (12 - PLAYER_SIZE) * t; int r2 = PLAYER_SIZE + (24 - PLAYER_SIZE) * t; radial_fadeout(DWIDTH/2, DHEIGHT/2, r1, r2, C_BLACK); } /* Final text */ if(state == State_Finale && game.time_finale >= 14.0) { float t = fminf(1.0, (game.time_finale - 14.0) / 2.0); int c = 31*t; c = C_RGB(c, c, c); duet_text_opt(DWIDTH/2 - 30, DHEIGHT/2, c, C_NONE, DTEXT_CENTER, DTEXT_MIDDLE, "I am here", -1); } float extra_size = 1 + sinf(6 * game.time); for(int i = 0; i < game.rect_count; i++) drectoid(&game.rects[i], extra_size, C_WHITE); dupdate(); } timer_stop(timer); } int main(void) { __printf_enable_fp(); /* Azur trickz for less tearing */ r61524_set(0x010, 0x0010); int current_episode = 0; int current_level = 0; while(1) { main_menu(¤t_episode, ¤t_level); game_loop(current_episode, current_level); } return 1; }