render.c (18424B)
1 #include "engine/battle.h" 2 #include "engine/area.h" 3 #include "engine/audio.h" 4 #include "game/content.h" 5 #include "game/presentation.h" 6 7 #include <stdio.h> 8 9 #include "config.h" 10 11 #if PH_USE_SDL 12 static int 13 ph_render_enter_portal(PhWorld *world, int destination_area_id) 14 { 15 int i; 16 17 for (i = 0; i < world->area.marker_count; ++i) { 18 const PhAreaMarker *marker = &world->area.markers[i]; 19 const PhMarkerDef *def = ph_area_catalog_marker(ph_game_world.areas, 20 world->area_id, marker->symbol); 21 22 if (!def || def->kind != PH_MARKER_PORTAL || 23 def->destination_area_id != destination_area_id) continue; 24 world->entities[world->player_index].pos = (PhVec2){ 25 PH_TILE_CENTER(marker->tile_x), PH_TILE_CENTER(marker->tile_y) }; 26 return ph_world_update_portal(world, ph_game_world.areas); 27 } 28 return -1; 29 } 30 31 int 32 main(void) 33 { 34 SDL_Surface *surface; 35 SDL_Renderer *renderer; 36 PhRenderAssets assets; 37 PhAudio audio = { 0 }; 38 PhWorld world; 39 PhInventoryUi inventory_ui; 40 PhSpellbookUi spellbook_ui; 41 PhShopUi shop_ui = { 0 }; 42 PhOptionsUi options_ui = { .music_enabled = 1, .effects_enabled = 1 }; 43 PhStartUi start_ui = { .selected = 1 }; 44 PhGameMode game = PH_GAME_WORLD; 45 PhBattle battle = { 0 }; 46 PhSpellBook spells = ph_player_spellbook; 47 PhGameContext context = { 48 .world = &world, 49 .inventory = &inventory_ui, 50 .spellbook = &spellbook_ui, 51 .shop = &shop_ui, 52 .options = &options_ui, 53 .start = &start_ui, 54 .mode = &game, 55 .battle = &battle, 56 .spells = &spells, 57 }; 58 int i; 59 int enemy_index = -1; 60 int result = 0; 61 int order[PH_BATTLE_ORDER_COUNT]; 62 PhBattleResult battle_result; 63 int first_item; 64 int second_item; 65 66 if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) { 67 fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); 68 return 1; 69 } 70 71 surface = SDL_CreateSurface(PH_VIEW_W, PH_VIEW_H, SDL_PIXELFORMAT_ARGB8888); 72 if (!surface) { 73 fprintf(stderr, "SDL_CreateSurface failed: %s\n", SDL_GetError()); 74 SDL_Quit(); 75 return 1; 76 } 77 78 renderer = SDL_CreateSoftwareRenderer(surface); 79 if (!renderer) { 80 fprintf(stderr, "SDL_CreateSoftwareRenderer failed: %s\n", SDL_GetError()); 81 SDL_DestroySurface(surface); 82 SDL_Quit(); 83 return 1; 84 } 85 86 if (ph_world_start(&world, &ph_game_world, 87 (float)PH_VIEW_W, (float)PH_VIEW_H) < 0) { 88 SDL_DestroyRenderer(renderer); 89 SDL_DestroySurface(surface); 90 SDL_Quit(); 91 return 1; 92 } 93 if (world.ground_item_count < 2) { 94 fprintf(stderr, "render smoke test failed: item setup\n"); 95 ph_world_destroy(&world); 96 SDL_DestroyRenderer(renderer); 97 SDL_DestroySurface(surface); 98 SDL_Quit(); 99 return 1; 100 } 101 first_item = world.ground_items[0].item_id; 102 second_item = world.ground_items[1].item_id; 103 104 if (ph_render_assets_load(&assets, renderer, 105 ph_asset_defs, PH_ASSET_COUNT) < 0) { 106 ph_world_destroy(&world); 107 SDL_DestroyRenderer(renderer); 108 SDL_DestroySurface(surface); 109 SDL_Quit(); 110 return 1; 111 } 112 if (ph_start_key(&start_ui, PH_KEY_MENU_ACCEPT) != PH_START_NEW) { 113 fprintf(stderr, "render smoke test failed: new-game selection\n"); 114 result = 1; 115 } 116 start_ui.can_continue = 1; 117 start_ui.selected = 0; 118 if (ph_start_key(&start_ui, PH_KEY_MENU_ACCEPT) != PH_START_CONTINUE) { 119 fprintf(stderr, "render smoke test failed: continue selection\n"); 120 result = 1; 121 } 122 context.menu = PH_MENU_START; 123 ph_present_frame(renderer, &assets, &context); 124 if (ph_audio_init(&audio, ph_audio_defs, PH_AUDIO_COUNT) < 0) { 125 fprintf(stderr, "audio smoke test failed: %s\n", SDL_GetError()); 126 ph_render_assets_destroy(&assets); 127 ph_world_destroy(&world); 128 SDL_DestroyRenderer(renderer); 129 SDL_DestroySurface(surface); 130 SDL_Quit(); 131 return 1; 132 } 133 ph_audio_music(&audio, PH_MUSIC_MEADOW); 134 ph_audio_update(&audio); 135 ph_audio_effect(&audio, PH_SOUND_ATTACK, 1.0f); 136 ph_audio_effect(&audio, PH_SOUND_HIT, 1.0f); 137 ph_audio_effect(&audio, PH_SOUND_SPELL, 1.0f); 138 ph_audio_effect(&audio, PH_SOUND_MENU, 0.5f); 139 ph_audio_set_enabled(&audio, 0, 0); 140 if (audio.music_enabled || audio.effects_enabled || 141 ph_audio_distance_gain(0.0f, 0.0f, 160.0f, 0.0f, 160.0f) != 0.0f || 142 ph_audio_distance_gain(0.0f, 0.0f, 80.0f, 0.0f, 160.0f) != 0.5f) { 143 fprintf(stderr, "audio smoke test failed: controls or distance\n"); 144 result = 1; 145 } 146 ph_audio_set_enabled(&audio, 1, 1); 147 ph_audio_music(&audio, PH_MUSIC_TOWN); 148 ph_audio_music(&audio, PH_MUSIC_BATTLE); 149 150 ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); 151 if (ph_world_item_amount(&world, first_item) != 0) { 152 fprintf(stderr, "render smoke test failed: adjacent item pickup\n"); 153 result = 1; 154 } 155 ph_world_tick(&world, (PhInput){ .move_x = 1 }, 156 (float)PH_WORLD_STEP_MILLISECONDS / 1000.0f); 157 ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); 158 ph_world_tick(&world, (PhInput){ .move_y = 1 }, 159 (float)PH_WORLD_STEP_MILLISECONDS / 1000.0f); 160 ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); 161 if (ph_world_item_amount(&world, first_item) != 1 || 162 ph_world_item_amount(&world, second_item) != 1) { 163 fprintf(stderr, "render smoke test failed: item pickup\n"); 164 result = 1; 165 } 166 ph_inventory_open(&inventory_ui, &world); 167 ph_options_open(&options_ui); 168 if (ph_options_key(&options_ui, PH_KEY_MENU_DOWN_1) != PH_OPTIONS_STAY || 169 options_ui.selected != 1 || 170 ph_options_key(&options_ui, PH_KEY_MENU_UP_1) != PH_OPTIONS_STAY || 171 options_ui.selected != 0 || 172 ph_options_key(&options_ui, PH_KEY_MENU_ACCEPT) != PH_OPTIONS_STAY || 173 options_ui.mode != PH_OPTIONS_SOUND) { 174 fprintf(stderr, "render smoke test failed: options navigation\n"); 175 result = 1; 176 } 177 ph_options_key(&options_ui, PH_KEY_MENU_ACCEPT); 178 ph_options_key(&options_ui, PH_KEY_MENU_DOWN_1); 179 ph_options_key(&options_ui, PH_KEY_MENU_ACCEPT); 180 if (options_ui.music_enabled || options_ui.effects_enabled) { 181 fprintf(stderr, "render smoke test failed: sound toggles\n"); 182 result = 1; 183 } 184 context.menu = PH_MENU_MAIN; 185 ph_present_frame(renderer, &assets, &context); 186 ph_options_key(&options_ui, PH_KEY_MENU_BACK); 187 if (ph_options_key(&options_ui, PH_KEY_MENU_BACK) != PH_OPTIONS_CLOSE) { 188 fprintf(stderr, "render smoke test failed: options cancel\n"); 189 result = 1; 190 } 191 ph_options_open(&options_ui); 192 ph_options_key(&options_ui, PH_KEY_MENU_DOWN_1); 193 if (ph_options_key(&options_ui, PH_KEY_MENU_ACCEPT) != PH_OPTIONS_EXIT) { 194 fprintf(stderr, "render smoke test failed: exit option\n"); 195 result = 1; 196 } 197 options_ui.music_enabled = 1; 198 options_ui.effects_enabled = 1; 199 context.menu = PH_MENU_NONE; 200 ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_ACCEPT); 201 if (inventory_ui.option != PH_INVENTORY_EQUIP) result = 1; 202 ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_DOWN_1); 203 if (inventory_ui.option != PH_INVENTORY_DROP) result = 1; 204 inventory_ui.mode = PH_INVENTORY_BROWSE; 205 ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_DOWN_1); 206 ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_ACCEPT); 207 if (inventory_ui.option != PH_INVENTORY_USE) result = 1; 208 ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_DOWN_1); 209 if (inventory_ui.option != PH_INVENTORY_DROP) { 210 fprintf(stderr, "render smoke test failed: inventory actions\n"); 211 result = 1; 212 } 213 inventory_ui.mode = PH_INVENTORY_EXAMINE; 214 if (ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_BACK) || 215 inventory_ui.mode != PH_INVENTORY_ACTIONS || 216 ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_BACK) || 217 inventory_ui.mode != PH_INVENTORY_BROWSE || 218 !ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_BACK)) { 219 fprintf(stderr, "render smoke test failed: inventory cancel\n"); 220 result = 1; 221 } 222 ph_inventory_open(&inventory_ui, &world); 223 for (i = 0; i < 10; ++i) { 224 inventory_ui.mode = i == 8 ? PH_INVENTORY_EXAMINE : 225 i == 9 ? PH_INVENTORY_ACTIONS : PH_INVENTORY_BROWSE; 226 context.menu = i == 7 ? PH_MENU_CHARACTER : 227 i >= 6 ? PH_MENU_INVENTORY : PH_MENU_NONE; 228 ph_present_frame(renderer, &assets, &context); 229 } 230 ph_spellbook_open(&spellbook_ui, &spells); 231 ph_spellbook_key(&spellbook_ui, &world, &spells, PH_KEY_MENU_ACCEPT); 232 if (spellbook_ui.option != PH_SPELLBOOK_EXAMINE_ACTION) { 233 fprintf(stderr, "render smoke test failed: disabled field spell\n"); 234 result = 1; 235 } 236 spellbook_ui.mode = PH_SPELLBOOK_BROWSE; 237 spellbook_ui.selected = 1; 238 ph_spellbook_key(&spellbook_ui, &world, &spells, PH_KEY_MENU_ACCEPT); 239 if (spellbook_ui.option != PH_SPELLBOOK_USE) result = 1; 240 world.entities[world.player_index].vitals.hp -= 10; 241 ph_spellbook_key(&spellbook_ui, &world, &spells, PH_KEY_MENU_ACCEPT); 242 if (ph_world_player(&world)->vitals.hp != 243 ph_world_entity_stats(&world, ph_world_player(&world)).max_hp || 244 ph_world_player(&world)->vitals.mp != 40) { 245 fprintf(stderr, "render smoke test failed: field Heal\n"); 246 result = 1; 247 } 248 world.entities[world.player_index].vitals.mp = 249 ph_world_entity_stats(&world, ph_world_player(&world)).max_mp; 250 context.menu = PH_MENU_SPELLBOOK; 251 ph_present_frame(renderer, &assets, &context); 252 spellbook_ui.mode = PH_SPELLBOOK_ACTIONS; 253 ph_present_frame(renderer, &assets, &context); 254 spellbook_ui.mode = PH_SPELLBOOK_EXAMINE; 255 ph_present_frame(renderer, &assets, &context); 256 ph_shop_open(&shop_ui, ph_shop_defs[0].id); 257 for (i = PH_SHOP_ROOT; i <= PH_SHOP_TALK; ++i) { 258 shop_ui.mode = (PhShopMode)i; 259 shop_ui.selected = i == PH_SHOP_SELL ? 260 ph_inventory_step(&world, -1, 1) : 0; 261 context.menu = PH_MENU_SHOP; 262 ph_present_frame(renderer, &assets, &context); 263 } 264 ph_shop_open(&shop_ui, PH_SHOP_MAGICK); 265 shop_ui.mode = PH_SHOP_ROOT; 266 ph_present_frame(renderer, &assets, &context); 267 ph_shop_key(&shop_ui, &world, &spells, PH_KEY_MENU_ACCEPT); 268 if (shop_ui.mode != PH_SHOP_BUY) result = 1; 269 ph_present_frame(renderer, &assets, &context); 270 ph_shop_key(&shop_ui, &world, &spells, PH_KEY_MENU_ACCEPT); 271 if (!ph_spellbook_has(&spells, PH_SPELL_EMBER) || 272 ph_world_player(&world)->gold != 16) { 273 fprintf(stderr, "render smoke test failed: magick purchase\n"); 274 result = 1; 275 } 276 ph_present_frame(renderer, &assets, &context); 277 shop_ui.mode = PH_SHOP_TALK; 278 ph_present_frame(renderer, &assets, &context); 279 for (i = 0; i < world.entity_count; ++i) { 280 const PhEntityDef *def = ph_world_entity_def(&world, world.entities[i].type_id); 281 if (def && def->kind == PH_ENTITY_MONSTER) enemy_index = i; 282 } 283 if (enemy_index < 0) { 284 fprintf(stderr, "render smoke test failed: enemy setup\n"); 285 result = 1; 286 } else { 287 world.encounter_index = enemy_index; 288 game = PH_GAME_WORLD; 289 context.menu = PH_MENU_NONE; 290 ph_battle_begin(&battle, &world, enemy_index, spells); 291 game = PH_GAME_TRANSITION; 292 ph_present_frame(renderer, &assets, &context); 293 if (game != PH_GAME_TRANSITION) result = 1; 294 context.menu = PH_MENU_NONE; 295 ph_battle_update(&battle, &world, PH_BATTLE_TRANSITION_SECONDS * 0.5f, 1); 296 ph_present_frame(renderer, &assets, &context); 297 game = PH_GAME_BATTLE; 298 ph_options_open(&options_ui); 299 context.menu = PH_MENU_MAIN; 300 ph_present_frame(renderer, &assets, &context); 301 context.menu = PH_MENU_NONE; 302 battle.phase = PH_BATTLE_ENEMY_ACTION; 303 battle.timer = PH_BATTLE_ACTION_SECONDS; 304 battle.resolved = 0; 305 ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS * 0.5f, 0); 306 if (ph_battle_take_event(&battle) != PH_BATTLE_EVENT_HIT) { 307 fprintf(stderr, "render smoke test failed: enemy sound event\n"); 308 result = 1; 309 } 310 battle.timer = 0.0f; 311 world.entities[world.player_index].vitals.hp = 0; 312 context.menu = PH_MENU_NONE; 313 battle_result = ph_battle_update(&battle, &world, 0.0f, 0); 314 if (battle_result == PH_BATTLE_DEFEAT) game = PH_GAME_OVER; 315 ph_present_frame(renderer, &assets, &context); 316 if (game != PH_GAME_OVER) result = 1; 317 world.entities[world.player_index].vitals.hp = 318 ph_world_entity_stats(&world, ph_world_player(&world)).max_hp; 319 ph_battle_begin(&battle, &world, enemy_index, spells); 320 game = PH_GAME_TRANSITION; 321 context.menu = PH_MENU_NONE; 322 if (ph_battle_update(&battle, &world, PH_BATTLE_TRANSITION_SECONDS, 1) == 323 PH_BATTLE_READY) game = PH_GAME_BATTLE; 324 ph_present_frame(renderer, &assets, &context); 325 world.entities[enemy_index].vitals.hp = 326 ph_world_entity_stats(&world, &world.entities[enemy_index]).max_hp; 327 world.entities[enemy_index].vitals.hp = 1; 328 battle.choice.command = PH_BATTLE_MAGICK; 329 ph_battle_select(&battle, &world); 330 battle.choice.spell = 1; 331 ph_battle_cast(&battle, &world); 332 if (battle.phase != PH_BATTLE_TARGET || 333 battle.choice.target_index != world.player_index) result = 1; 334 if (ph_battle_target_step(&battle, &world, 1) != enemy_index) result = 1; 335 ph_present_frame(renderer, &assets, &context); 336 ph_battle_confirm_target(&battle, &world); 337 ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS * 0.5f, 0); 338 if (ph_battle_take_event(&battle) != PH_BATTLE_EVENT_SPELL) { 339 fprintf(stderr, "render smoke test failed: healing sound event\n"); 340 result = 1; 341 } 342 if (world.entities[enemy_index].vitals.hp != 343 ph_world_entity_stats(&world, &world.entities[enemy_index]).max_hp || 344 ph_world_player(&world)->vitals.mp != 40) { 345 fprintf(stderr, "render smoke test failed: healing enemy target\n"); 346 result = 1; 347 } 348 world.entities[world.player_index].vitals.mp = 349 ph_world_entity_stats(&world, ph_world_player(&world)).max_mp; 350 ph_battle_begin(&battle, &world, enemy_index, spells); 351 ph_battle_update(&battle, &world, PH_BATTLE_TRANSITION_SECONDS, 1); 352 battle.choice.command = PH_BATTLE_FIGHT; 353 ph_battle_order(&battle, &world, order); 354 if (order[0] != world.player_index || order[1] != enemy_index) { 355 fprintf(stderr, "render smoke test failed: normal CTB order\n"); 356 result = 1; 357 } 358 battle.choice.command = PH_BATTLE_MAGICK; 359 ph_battle_order(&battle, &world, order); 360 if (order[0] != world.player_index || order[1] != enemy_index) { 361 fprintf(stderr, "render smoke test failed: command CTB order\n"); 362 result = 1; 363 } 364 ph_battle_select(&battle, &world); 365 if (battle.phase != PH_BATTLE_MAGIC || ph_world_player(&world)->vitals.mp != 50) { 366 fprintf(stderr, "render smoke test failed: Magick submenu\n"); 367 result = 1; 368 } 369 ph_battle_order(&battle, &world, order); 370 if (order[0] != world.player_index || order[1] != world.player_index) { 371 fprintf(stderr, "render smoke test failed: spell CTB order\n"); 372 result = 1; 373 } 374 context.menu = PH_MENU_NONE; 375 ph_present_frame(renderer, &assets, &context); 376 ph_battle_cast(&battle, &world); 377 if (battle.phase != PH_BATTLE_TARGET || 378 battle.choice.target_index != enemy_index) { 379 fprintf(stderr, "render smoke test failed: offensive target default\n"); 380 result = 1; 381 } 382 ph_present_frame(renderer, &assets, &context); 383 ph_battle_confirm_target(&battle, &world); 384 context.menu = PH_MENU_NONE; 385 ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS * 0.5f, 0); 386 if (ph_battle_take_event(&battle) != PH_BATTLE_EVENT_SPELL) { 387 fprintf(stderr, "render smoke test failed: spell sound event\n"); 388 result = 1; 389 } 390 ph_present_frame(renderer, &assets, &context); 391 context.menu = PH_MENU_NONE; 392 ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS * 0.5f, 0); 393 ph_present_frame(renderer, &assets, &context); 394 if (game != PH_GAME_BATTLE || battle.phase != PH_BATTLE_COMMAND || 395 battle.turn.current_actor != world.player_index || 396 ph_world_player(&world)->vitals.mp != 35 || 397 world.entities[enemy_index].accuracy.value != 20 || 398 world.entities[enemy_index].accuracy.turns != 3) { 399 fprintf(stderr, "render smoke test failed: Flash action\n"); 400 result = 1; 401 } 402 battle.choice.command = PH_BATTLE_FIGHT; 403 ph_battle_select(&battle, &world); 404 ph_battle_confirm_target(&battle, &world); 405 context.menu = PH_MENU_NONE; 406 ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS * 0.5f, 0); 407 if (ph_battle_take_event(&battle) != PH_BATTLE_EVENT_ATTACK) { 408 fprintf(stderr, "render smoke test failed: attack sound event\n"); 409 result = 1; 410 } 411 ph_present_frame(renderer, &assets, &context); 412 context.menu = PH_MENU_NONE; 413 battle_result = ph_battle_update(&battle, &world, 414 PH_BATTLE_ACTION_SECONDS * 0.5f, 0); 415 if (battle_result == PH_BATTLE_VICTORY) game = PH_GAME_VICTORY; 416 ph_present_frame(renderer, &assets, &context); 417 if (game != PH_GAME_VICTORY || 418 ph_world_player(&world)->xp != battle.reward.xp || 419 ph_world_item_amount(&world, battle.reward.item_id) != 2) { 420 fprintf(stderr, "render smoke test failed: victory rewards\n"); 421 result = 1; 422 } 423 } 424 game = PH_GAME_WORLD; 425 for (i = 0; i < world.area.marker_count; ++i) { 426 const PhMarkerDef *def = ph_area_catalog_marker(ph_game_world.areas, 427 world.area_id, world.area.markers[i].symbol); 428 429 if (!def || def->kind != PH_MARKER_PORTAL) continue; 430 world.entities[world.player_index].pos = (PhVec2){ 431 PH_TILE_CENTER(world.area.markers[i].tile_x), 432 PH_TILE_CENTER(world.area.markers[i].tile_y) }; 433 break; 434 } 435 if (i == world.area.marker_count || 436 ph_world_update_portal(&world, ph_game_world.areas) != 1) { 437 fprintf(stderr, "render smoke test failed: town portal\n"); 438 result = 1; 439 } 440 for (i = 0; i < world.area.marker_count; ++i) { 441 const PhMarkerDef *def = ph_area_catalog_marker(ph_game_world.areas, 442 world.area_id, world.area.markers[i].symbol); 443 444 if (!def || def->kind != PH_MARKER_PORTAL || 445 def->destination_area_id != PH_AREA_MIRA_SHOP) continue; 446 world.entities[world.player_index].pos = (PhVec2){ 447 PH_TILE_CENTER(world.area.markers[i].tile_x), 448 PH_TILE_CENTER(world.area.markers[i].tile_y) }; 449 break; 450 } 451 if (i == world.area.marker_count || 452 ph_world_update_portal(&world, ph_game_world.areas) != 1 || 453 world.area_id != PH_AREA_MIRA_SHOP) { 454 fprintf(stderr, "render smoke test failed: shop portal\n"); 455 result = 1; 456 } 457 ph_shop_open(&shop_ui, ph_shop_defs[0].id); 458 shop_ui.mode = PH_SHOP_TALK; 459 context.menu = PH_MENU_SHOP; 460 ph_present_frame(renderer, &assets, &context); 461 if (ph_render_enter_portal(&world, PH_AREA_STONEHOLLOW) != 1) { 462 fprintf(stderr, "render smoke test failed: return to town\n"); 463 result = 1; 464 } 465 context.menu = PH_MENU_NONE; 466 ph_present_frame(renderer, &assets, &context); 467 if (ph_render_enter_portal(&world, PH_AREA_MEADOW) != 1) { 468 fprintf(stderr, "render smoke test failed: return to meadow\n"); 469 result = 1; 470 } 471 ph_present_frame(renderer, &assets, &context); 472 473 if (!SDL_SaveBMP(surface, PH_RENDER_SMOKE_PATH)) { 474 fprintf(stderr, "SDL_SaveBMP failed: %s\n", SDL_GetError()); 475 result = 1; 476 } 477 478 ph_audio_destroy(&audio); 479 ph_render_assets_destroy(&assets); 480 ph_world_destroy(&world); 481 SDL_DestroyRenderer(renderer); 482 SDL_DestroySurface(surface); 483 SDL_Quit(); 484 return result; 485 } 486 487 488 #else 489 int 490 main(void) 491 { 492 fprintf(stderr, "SDL3 not available in this build\n"); 493 return 1; 494 } 495 #endif