/* * This software is licensed under the terms of the MIT License. * See COPYING for further information. * --- * Copyright (c) 2011-2024, Lukas Weber . * Copyright (c) 2012-2024, Andrei Alexeyev . */ #include "mainmenu.h" #include "charselect.h" #include "common.h" #include "menu.h" #include "submenus.h" #include "audio/audio.h" #include "events.h" #include "global.h" #include "resource/font.h" #include "util/graphics.h" #include "version.h" #include "video.h" #include "watchdog.h" static MenuEntry *spell_practice_entry; static MenuEntry *stage_practice_entry; void main_menu_update_practice_menus(void) { spell_practice_entry->action = NULL; stage_practice_entry->action = NULL; int n = stageinfo_get_num_stages(); for(int i = 0; i < n; ++i) { StageInfo *stg = stageinfo_get_by_index(i); if(stg->type == STAGE_SPELL) { StageProgress *p = stageinfo_get_progress(stg, D_Any, false); if(p && p->unlocked) { spell_practice_entry->action = menu_action_enter_spellpractice; if(stage_practice_entry->action) { break; } } } else if(stg->type == STAGE_STORY) { for(Difficulty d = D_Easy; d <= D_Lunatic; ++d) { StageProgress *p = stageinfo_get_progress(stg, d, false); if(p && p->unlocked) { stage_practice_entry->action = menu_action_enter_stagepractice; if(spell_practice_entry->action) { break; } } } } } } static void begin_main_menu(MenuData *m) { audio_bgm_play(res_bgm("menu"), true, 0, 0); } static void update_main_menu(MenuData *menu) { if(watchdog_signaled()) { menu->cursor = 0; } watchdog_reset(); menu->drawdata[1] += 0.1*(menu->cursor-menu->drawdata[1]); dynarray_foreach(&menu->entries, int i, MenuEntry *e, { e->drawdata += 0.2 * ((i == menu->cursor) - e->drawdata); }); } attr_unused static bool main_menu_input_handler(SDL_Event *event, void *arg) { MenuData *m = arg; TaiseiEvent te = TAISEI_EVENT(event->type); static hrtime_t last_abort_time = 0; if(te == TE_MENU_ABORT && dynarray_get(&m->entries, m->entries.num_elements - 1).action) { play_sfx_ui("hit"); m->cursor = m->entries.num_elements - 1; hrtime_t t = time_get(); if(t - last_abort_time < HRTIME_RESOLUTION/5 && last_abort_time > 0) { m->selected = m->cursor; close_menu(m); } last_abort_time = t; return true; } return menu_input_handler(event, arg); } attr_unused static void main_menu_input(MenuData *m) { events_poll((EventHandler[]){ { .proc = main_menu_input_handler, .arg = m }, { NULL } }, EFLAG_MENU); } MenuData* create_main_menu(void) { MenuData *m = alloc_menu(); m->begin = begin_main_menu; m->draw = draw_main_menu; m->logic = update_main_menu; ptrdiff_t stage_practice_idx, spell_practice_idx; add_menu_entry(m, "Start Story", start_game, NULL); add_menu_entry(m, "Start Extra", NULL, NULL); stage_practice_entry = add_menu_entry(m, "Stage Practice", menu_action_enter_stagepractice, NULL); stage_practice_idx = dynarray_indexof(&m->entries, stage_practice_entry); spell_practice_entry = add_menu_entry(m, "Spell Practice", menu_action_enter_spellpractice, NULL); spell_practice_idx = dynarray_indexof(&m->entries, spell_practice_entry); #ifdef DEBUG add_menu_entry(m, "Select Stage", menu_action_enter_stagemenu, NULL); #endif add_menu_entry(m, "Replays", menu_action_enter_replayview, NULL); add_menu_entry(m, "Media Room", menu_action_enter_media, NULL); add_menu_entry(m, "Options", menu_action_enter_options, NULL); if(!taisei_is_quit_hidden()) { add_menu_entry(m, "Quit", menu_action_close, NULL)->transition = TransFadeBlack; m->input = main_menu_input; } stage_practice_entry = dynarray_get_ptr(&m->entries, stage_practice_idx); spell_practice_entry = dynarray_get_ptr(&m->entries, spell_practice_idx); main_menu_update_practice_menus(); progress_unlock_bgm("menu"); audio_bgm_play(res_bgm("menu"), true, 0, 0); return m; } void draw_main_menu_bg(MenuData* menu, double center_x, double center_y, double R, const char *tex1, const char *tex2) { r_color4(1, 1, 1, 1); r_shader("mainmenubg"); r_uniform_float("R", R/(1-menu_fade(menu))); r_uniform_vec2("bg_translation", 0.001*menu->frames, 0); r_uniform_vec2("center", 0.5 + center_x / SCREEN_W, 0.5 + center_y / SCREEN_H); r_uniform_float("inv_aspect_ratio", 1.0 / VIDEO_ASPECT_RATIO); r_uniform_sampler("blend_mask", "cell_noise"); r_uniform_sampler("tex2", tex2); fill_screen(tex1); r_shader_standard(); } void draw_main_menu(MenuData *menu) { draw_main_menu_bg(menu, 0, 0, 0.05, "menu/mainmenubg", "stage1/cirnobg"); r_state_push(); float rot = sqrt(menu->frames/120.0); float rotfac = (1 - pow(menu_fade(menu), 2.0)); r_draw_sprite(&(SpriteParams) { .sprite_ptr = res_sprite("menu/logo"), .shader_ptr = res_shader("sprite_default"), .pos = { SCREEN_W/2, SCREEN_H/2 }, .rotation.vector = { 0, -1, 0 }, .rotation.angle = max(0, M_PI/1.5 - min(M_PI/1.5, rot) * rotfac), .color = color_mul_scalar(RGBA(1, 1, 1, 1), min(1, rot) * rotfac), }); r_mat_mv_push(); r_mat_mv_translate(0, SCREEN_H/2, 0); r_shader("text_default"); float o = 0.7; dynarray_foreach(&menu->entries, int i, MenuEntry *e, { if(e->action == NULL) { r_color4(0.2 * o, 0.3 * o, 0.5 * o, o); } else { float a = 1 - e->drawdata; r_color4(o, min(1, 0.7 + a) * o, min(1, 0.4 + a) * o, o); } text_draw(e->name, &(TextParams) { .pos = { 50 - 15 * e->drawdata, 20 * (i - menu->drawdata[1]) }, .font = "standard", }); }); r_mat_mv_pop(); r_disable(RCAP_CULL_FACE); r_shader("sprite_default"); for(int i = 0; i < 50; i++) { // who needs persistent state for a particle system? int period = 900; int t = menu->frames+100*i + 30*sin(35*i); int cycle = t/period; float posx = SCREEN_W+300+100*sin(100345*i)+200*sin(1003*i+13537*cycle)-(0.6+0.01*sin(35*i))*(t%period); float posy = 50+ 50*sin(503*i+14677*cycle)+0.8*(t%period); float rx = sin(56*i+2147*cycle); float ry = sin(913*i+137*cycle); float rz = sin(1303*i+89631*cycle); float r = sqrt(rx*rx+ry*ry+rz*rz); if(!r) { continue; } rx /= r; ry /= r; rz /= r; if(posx > SCREEN_W+20 || posy < -20 || posy > SCREEN_H+20) continue; r_draw_sprite(&(SpriteParams) { .sprite_ptr = res_sprite("part/petal"), .color = RGBA(1, 1, 1, 0), .pos = { posx, posy }, .scale.both = 0.2, .rotation.angle = DEG2RAD * 2 * (t % period), .rotation.vector = { rx, ry, rz }, }); } r_enable(RCAP_CULL_FACE); char version[32]; snprintf(version, sizeof(version), "v%s", TAISEI_VERSION); text_draw(TAISEI_VERSION, &(TextParams) { .align = ALIGN_RIGHT, .pos = { SCREEN_W-5, SCREEN_H-10 }, .font = "small", .shader_ptr = res_shader("text_default"), .color = RGBA(1, 1, 1, 1), }); r_state_pop(); } void draw_loading_screen(void) { ResourceGroup rg; res_group_init(&rg); res_group_preload(&rg, RES_TEXTURE, RESF_DEFAULT, "loading", NULL); res_group_preload(&rg, RES_SHADER_PROGRAM, RESF_DEFAULT, "text_default", NULL); set_ortho(SCREEN_W, SCREEN_H); fill_screen("loading"); text_draw("Please wait warmly…", &(TextParams) { .align = ALIGN_CENTER, .pos = { SCREEN_W/2, SCREEN_H-20 }, .font = "standard", .shader_ptr = res_shader("text_default"), .color = RGBA(0.35, 0.35, 0.35, 0.35), }); video_swap_buffers(); res_group_release(&rg); } void menu_preload(ResourceGroup *rg) { difficulty_preload(rg); res_group_preload(rg, RES_FONT, RESF_DEFAULT, "big", "small", NULL); res_group_preload(rg, RES_TEXTURE, RESF_DEFAULT, "abstract_brown", "cell_noise", "stage1/cirnobg", "menu/mainmenubg", "loading", NULL); res_group_preload(rg, RES_SPRITE, RESF_DEFAULT, "part/smoke", "part/petal", "menu/logo", "menu/arrow", "star", NULL); res_group_preload(rg, RES_SHADER_PROGRAM, RESF_DEFAULT, "gamepad_circle", "mainmenubg", "sprite_circleclipped_indicator", NULL); res_group_preload(rg, RES_SFX, RESF_OPTIONAL, "generic_shot", "shot_special1", "hit", NULL); res_group_preload(rg, RES_BGM, RESF_OPTIONAL, "menu", NULL); preload_char_menu(rg); }