main.c (13555B)
1 #include "engine/battle.h" 2 #include "engine/area.h" 3 #include "engine/audio.h" 4 #include "engine/save.h" 5 #include "game/content.h" 6 #include "game/presentation.h" 7 8 #include <stdbool.h> 9 #include <stdio.h> 10 11 #if PH_USE_SDL 12 #include <SDL3/SDL.h> 13 #endif 14 15 #include "config.h" 16 17 #ifndef PH_CONFIG_VERSION 18 #error "config.h is outdated; merge config.def.h or replace config.h" 19 #elif PH_CONFIG_VERSION != PH_CONFIG_API_VERSION 20 #error "config.h version does not match the engine; merge config.def.h" 21 #else 22 23 #if PH_USE_SDL 24 static int 25 ph_game_music(const PhGameContext *context) 26 { 27 return *context->mode == PH_GAME_WORLD ? 28 ph_game_area_music(context->world->area_id) : PH_MUSIC_BATTLE; 29 } 30 31 static int 32 ph_update_game(PhGameContext *context, PhInput input, float dt) 33 { 34 PhWorld *world = context->world; 35 PhGameMode *game = context->mode; 36 PhBattleResult result = PH_BATTLE_CONTINUE; 37 int old_area = world->area_id; 38 int save_requested = 0; 39 40 if (*game == PH_GAME_WORLD && context->menu == PH_MENU_NONE) { 41 ph_world_tick(world, input, dt); 42 if (ph_world_update_portal(world, ph_game_world.areas) < 0) 43 fprintf(stderr, "failed to enter area\n"); 44 if (world->encounter_index >= 0) { 45 if (ph_save_write(PH_SAVE_PATH, world, context->spells) < 0) 46 fprintf(stderr, "failed to save pre-combat checkpoint\n"); 47 ph_battle_begin(context->battle, world, world->encounter_index, 48 *context->spells); 49 *game = PH_GAME_TRANSITION; 50 } 51 } 52 if ((*game == PH_GAME_TRANSITION || *game == PH_GAME_BATTLE) && 53 context->menu == PH_MENU_NONE) 54 result = ph_battle_update(context->battle, world, dt, 55 *game == PH_GAME_TRANSITION); 56 if (result == PH_BATTLE_READY) *game = PH_GAME_BATTLE; 57 else if (result == PH_BATTLE_VICTORY) { 58 *game = PH_GAME_VICTORY; 59 save_requested = 1; 60 } 61 else if (result == PH_BATTLE_DEFEAT) *game = PH_GAME_OVER; 62 return save_requested || world->area_id != old_area; 63 } 64 65 static int 66 ph_menu_direction(SDL_Scancode key) 67 { 68 return key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 || 69 key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2 || 70 key == PH_KEY_MENU_LEFT_1 || key == PH_KEY_MENU_LEFT_2 || 71 key == PH_KEY_MENU_RIGHT_1 || key == PH_KEY_MENU_RIGHT_2; 72 } 73 74 static int 75 ph_menu_back(SDL_Scancode key) 76 { 77 return key == PH_KEY_MENU_CANCEL || key == PH_KEY_MENU_BACK; 78 } 79 80 static float 81 ph_world_sound_gain(const PhWorld *world, const PhWorldEvent *event) 82 { 83 const PhEntity *player = ph_world_player(world); 84 85 if (!player || !event || PH_SPATIAL_SOUND_DISTANCE <= 0.0f) return 0.0f; 86 return ph_audio_distance_gain(player->pos.x, player->pos.y, 87 event->pos.x, event->pos.y, PH_SPATIAL_SOUND_DISTANCE); 88 } 89 90 static PhInput 91 ph_read_input(const bool *keys) 92 { 93 PhInput input = { 0 }; 94 95 if (keys[PH_KEY_LEFT_1] || keys[PH_KEY_LEFT_2] || keys[PH_KEY_LEFT_3]) 96 input.move_x -= 1; 97 if (keys[PH_KEY_RIGHT_1] || keys[PH_KEY_RIGHT_2] || keys[PH_KEY_RIGHT_3]) 98 input.move_x += 1; 99 if (keys[PH_KEY_UP_1] || keys[PH_KEY_UP_2] || keys[PH_KEY_UP_3]) 100 input.move_y -= 1; 101 if (keys[PH_KEY_DOWN_1] || keys[PH_KEY_DOWN_2] || keys[PH_KEY_DOWN_3]) 102 input.move_y += 1; 103 if (keys[PH_KEY_INTERACT_1] || keys[PH_KEY_INTERACT_2]) input.interact = 1; 104 return input; 105 } 106 107 static int 108 ph_create_window_renderer(SDL_Window **window, SDL_Renderer **renderer) 109 { 110 *window = SDL_CreateWindow(PH_WINDOW_TITLE, PH_VIEW_W * PH_SCALE, 111 PH_VIEW_H * PH_SCALE, SDL_WINDOW_RESIZABLE); 112 if (!*window) { 113 fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError()); 114 return -1; 115 } 116 117 *renderer = SDL_CreateRenderer(*window, NULL); 118 if (!*renderer) { 119 fprintf(stderr, "SDL_CreateRenderer default backend failed: %s\n", 120 SDL_GetError()); 121 *renderer = SDL_CreateRenderer(*window, "software"); 122 } 123 if (!*renderer) { 124 fprintf(stderr, "SDL_CreateRenderer software fallback failed: %s\n", 125 SDL_GetError()); 126 SDL_DestroyWindow(*window); 127 *window = NULL; 128 return -1; 129 } 130 return 0; 131 } 132 133 static int 134 ph_run_game(void) 135 { 136 SDL_Window *window; 137 SDL_Renderer *renderer; 138 PhRenderAssets assets; 139 PhWorld world; 140 PhInventoryUi inventory_ui = { 0 }; 141 PhSpellbookUi spellbook_ui = { 0 }; 142 PhShopUi shop_ui = { 0 }; 143 PhOptionsUi options_ui = { .music_enabled = 1, .effects_enabled = 1 }; 144 PhStartUi start_ui = { 0 }; 145 PhGameMode game = PH_GAME_WORLD; 146 PhBattle battle = { 0 }; 147 PhSpellBook spells = ph_player_spellbook; 148 PhAudio audio = { 0 }; 149 PhGameContext context = { 150 .world = &world, 151 .inventory = &inventory_ui, 152 .spellbook = &spellbook_ui, 153 .shop = &shop_ui, 154 .options = &options_ui, 155 .start = &start_ui, 156 .mode = &game, 157 .battle = &battle, 158 .spells = &spells, 159 }; 160 Uint64 last_ticks; 161 int running = 1; 162 int audio_ready = 0; 163 PhMenu menu = PH_MENU_START; 164 int save_status; 165 166 if (!SDL_Init(SDL_INIT_VIDEO)) { 167 fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); 168 return 1; 169 } 170 save_status = ph_save_load(PH_SAVE_PATH, &world, &ph_game_world, &spells, 171 (float)PH_VIEW_W, (float)PH_VIEW_H); 172 if (save_status < 0) { 173 fprintf(stderr, "save is invalid; starting a new game\n"); 174 spells = ph_player_spellbook; 175 } 176 if (save_status <= 0 && ph_world_start(&world, &ph_game_world, 177 (float)PH_VIEW_W, (float)PH_VIEW_H) < 0) { 178 SDL_Quit(); 179 return 1; 180 } 181 start_ui.can_continue = save_status > 0; 182 start_ui.selected = start_ui.can_continue ? 0 : 1; 183 if (ph_create_window_renderer(&window, &renderer) < 0) { 184 ph_world_destroy(&world); 185 SDL_Quit(); 186 return 1; 187 } 188 if (ph_render_assets_load(&assets, renderer, 189 ph_asset_defs, PH_ASSET_COUNT) < 0) { 190 SDL_DestroyRenderer(renderer); 191 SDL_DestroyWindow(window); 192 ph_world_destroy(&world); 193 SDL_Quit(); 194 return 1; 195 } 196 if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) { 197 fprintf(stderr, "audio disabled: %s\n", SDL_GetError()); 198 } else if (ph_audio_init(&audio, ph_audio_defs, PH_AUDIO_COUNT) < 0) { 199 fprintf(stderr, "audio disabled: %s\n", SDL_GetError()); 200 } else audio_ready = 1; 201 202 SDL_SetRenderLogicalPresentation(renderer, PH_VIEW_W, PH_VIEW_H, 203 SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); 204 last_ticks = SDL_GetTicks(); 205 while (running) { 206 SDL_Event event; 207 Uint64 now_ticks; 208 float dt; 209 PhInput input; 210 int effect = PH_AUDIO_NONE; 211 212 while (SDL_PollEvent(&event)) { 213 if (event.type == SDL_EVENT_QUIT) { 214 running = 0; 215 } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && 216 menu == PH_MENU_START) { 217 PhStartAction action = ph_start_key(&start_ui, event.key.scancode); 218 219 if (action == PH_START_CONTINUE) menu = PH_MENU_NONE; 220 else if (action == PH_START_NEW) { 221 ph_world_destroy(&world); 222 spells = ph_player_spellbook; 223 if (ph_world_start(&world, &ph_game_world, 224 (float)PH_VIEW_W, (float)PH_VIEW_H) < 0) running = 0; 225 else { 226 remove(PH_SAVE_PATH); 227 menu = PH_MENU_NONE; 228 } 229 } 230 } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && 231 menu == PH_MENU_MAIN) { 232 PhOptionsAction action; 233 234 if (ph_menu_direction(event.key.scancode)) effect = PH_SOUND_MENU; 235 action = ph_options_key(&options_ui, event.key.scancode); 236 if (action == PH_OPTIONS_CLOSE) menu = PH_MENU_NONE; 237 else if (action == PH_OPTIONS_EXIT) running = 0; 238 } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && 239 menu == PH_MENU_NONE && event.key.scancode == PH_KEY_MENU_CANCEL && 240 (game == PH_GAME_WORLD || game == PH_GAME_VICTORY || 241 game == PH_GAME_OVER || (game == PH_GAME_BATTLE && 242 battle.phase != PH_BATTLE_MAGIC && 243 battle.phase != PH_BATTLE_TARGET))) { 244 ph_options_open(&options_ui); 245 menu = PH_MENU_MAIN; 246 } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && 247 menu == PH_MENU_NONE && game == PH_GAME_BATTLE && 248 (battle.phase == PH_BATTLE_COMMAND || 249 battle.phase == PH_BATTLE_MAGIC || 250 battle.phase == PH_BATTLE_TARGET)) { 251 SDL_Scancode key = event.key.scancode; 252 int previous = key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 || 253 key == PH_KEY_MENU_LEFT_1 || key == PH_KEY_MENU_LEFT_2; 254 int next = key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2 || 255 key == PH_KEY_MENU_RIGHT_1 || key == PH_KEY_MENU_RIGHT_2; 256 257 if (battle.phase == PH_BATTLE_TARGET) { 258 if (previous || next) { 259 if (ph_battle_target_step(&battle, &world, 260 previous ? -1 : 1) >= 0) effect = PH_SOUND_MENU; 261 } 262 else if (key == PH_KEY_MENU_ACCEPT) 263 ph_battle_confirm_target(&battle, &world); 264 else if (ph_menu_back(key)) 265 ph_battle_cancel(&battle); 266 } else if (battle.phase == PH_BATTLE_MAGIC) { 267 if ((previous || next) && battle.spells.learned_count > 0) { 268 if (ph_battle_spell_step(&battle, previous ? -1 : 1) >= 0) 269 effect = PH_SOUND_MENU; 270 } 271 else if (key == PH_KEY_MENU_ACCEPT) 272 ph_battle_cast(&battle, &world); 273 else if (ph_menu_back(key)) 274 ph_battle_cancel(&battle); 275 } else if (previous || next) { 276 battle.choice.command = (PhBattleCommand)((battle.choice.command + (previous ? 277 PH_BATTLE_COMMAND_COUNT - 1 : 1)) % PH_BATTLE_COMMAND_COUNT); 278 effect = PH_SOUND_MENU; 279 } else if (key == PH_KEY_MENU_ACCEPT) { 280 ph_battle_select(&battle, &world); 281 } 282 } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && 283 menu == PH_MENU_NONE && game == PH_GAME_VICTORY && 284 event.key.scancode == PH_KEY_MENU_ACCEPT) { 285 game = PH_GAME_WORLD; 286 menu = PH_MENU_NONE; 287 } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && 288 game == PH_GAME_WORLD && menu == PH_MENU_CHARACTER && 289 ph_menu_back(event.key.scancode)) { 290 menu = PH_MENU_NONE; 291 } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && 292 game == PH_GAME_WORLD && 293 (menu == PH_MENU_NONE || menu == PH_MENU_CHARACTER) && 294 event.key.scancode == PH_KEY_CHARACTER) { 295 menu = menu == PH_MENU_CHARACTER ? PH_MENU_NONE : PH_MENU_CHARACTER; 296 } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && 297 game == PH_GAME_WORLD && 298 (menu == PH_MENU_NONE || menu == PH_MENU_INVENTORY) && 299 event.key.scancode == PH_KEY_INVENTORY) { 300 if (menu == PH_MENU_INVENTORY) menu = PH_MENU_NONE; 301 else { 302 menu = PH_MENU_INVENTORY; 303 ph_inventory_open(&inventory_ui, &world); 304 } 305 } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && 306 game == PH_GAME_WORLD && menu == PH_MENU_INVENTORY) { 307 if (ph_menu_direction(event.key.scancode) && 308 inventory_ui.mode != PH_INVENTORY_EXAMINE) 309 effect = PH_SOUND_MENU; 310 if (ph_inventory_key(&inventory_ui, &world, event.key.scancode)) 311 menu = PH_MENU_NONE; 312 } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && 313 game == PH_GAME_WORLD && 314 (menu == PH_MENU_NONE || menu == PH_MENU_SPELLBOOK) && 315 event.key.scancode == PH_KEY_SPELLBOOK) { 316 if (menu == PH_MENU_SPELLBOOK) menu = PH_MENU_NONE; 317 else { 318 menu = PH_MENU_SPELLBOOK; 319 ph_spellbook_open(&spellbook_ui, &spells); 320 } 321 } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && 322 game == PH_GAME_WORLD && menu == PH_MENU_SPELLBOOK) { 323 if (ph_menu_direction(event.key.scancode) && 324 spellbook_ui.mode != PH_SPELLBOOK_EXAMINE) 325 effect = PH_SOUND_MENU; 326 if (ph_spellbook_key(&spellbook_ui, &world, &spells, 327 event.key.scancode)) menu = PH_MENU_NONE; 328 } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && 329 game == PH_GAME_WORLD && menu == PH_MENU_SHOP) { 330 if (ph_menu_direction(event.key.scancode) && 331 shop_ui.mode != PH_SHOP_TALK) effect = PH_SOUND_MENU; 332 if (ph_shop_key(&shop_ui, &world, &spells, event.key.scancode)) 333 menu = PH_MENU_NONE; 334 } 335 } 336 337 now_ticks = SDL_GetTicks(); 338 dt = (float)(now_ticks - last_ticks) / 1000.0f; 339 if (dt > PH_MAX_FRAME_TIME) dt = PH_MAX_FRAME_TIME; 340 last_ticks = now_ticks; 341 input = ph_read_input(SDL_GetKeyboardState(NULL)); 342 context.menu = menu; 343 if (ph_update_game(&context, input, dt) && 344 ph_save_write(PH_SAVE_PATH, &world, &spells) < 0) 345 fprintf(stderr, "failed to autosave\n"); 346 { 347 int battle_effect = ph_game_battle_sound(ph_battle_take_event(&battle)); 348 349 if (battle_effect != PH_AUDIO_NONE) effect = battle_effect; 350 if (spellbook_ui.spell_used) effect = PH_SOUND_SPELL; 351 spellbook_ui.spell_used = 0; 352 } 353 if (audio_ready) { 354 PhWorldEvent world_event; 355 356 ph_audio_music(&audio, ph_game_music(&context)); 357 ph_audio_set_enabled(&audio, options_ui.music_enabled, 358 options_ui.effects_enabled); 359 if (effect != PH_AUDIO_NONE) ph_audio_effect(&audio, effect, 1.0f); 360 while (ph_world_take_event(&world, &world_event)) { 361 int world_effect = ph_game_world_sound(&world, &world_event); 362 363 if (world_effect != PH_AUDIO_NONE) 364 ph_audio_effect(&audio, world_effect, 365 ph_world_sound_gain(&world, &world_event)); 366 } 367 ph_audio_update(&audio); 368 } else { 369 PhWorldEvent world_event; 370 371 while (ph_world_take_event(&world, &world_event)) { } 372 } 373 ph_present_frame(renderer, &assets, &context); 374 if (game == PH_GAME_WORLD && menu == PH_MENU_NONE && 375 world.interaction_id > 0) { 376 menu = ph_interaction_open(&shop_ui, world.interaction_id); 377 world.interaction_id = 0; 378 } 379 SDL_Delay(PH_FRAME_DELAY_MS); 380 } 381 382 if (audio_ready) ph_audio_destroy(&audio); 383 ph_render_assets_destroy(&assets); 384 SDL_DestroyRenderer(renderer); 385 SDL_DestroyWindow(window); 386 if ((game == PH_GAME_WORLD || game == PH_GAME_VICTORY) && 387 ph_save_write(PH_SAVE_PATH, &world, &spells) < 0) 388 fprintf(stderr, "failed to save game\n"); 389 ph_world_destroy(&world); 390 SDL_Quit(); 391 return 0; 392 } 393 #endif 394 395 int 396 main(void) 397 { 398 #if PH_USE_SDL 399 return ph_run_game(); 400 #else 401 fprintf(stderr, "SDL3 not available in this build; run `make smoke`\n"); 402 return 1; 403 #endif 404 } 405 406 #endif