render.c (26083B)
1 #define _POSIX_C_SOURCE 200809L 2 #include "engine/battle.h" 3 #include "engine/area.h" 4 #include "engine/audio.h" 5 #include "engine/sdl.h" 6 #include "game/content.h" 7 #include "game/presentation.h" 8 9 #include <stdio.h> 10 #include <stdlib.h> 11 #include <string.h> 12 #include <unistd.h> 13 14 #include "config.h" 15 16 #if PH_USE_SDL 17 static int 18 ph_camera_smoke(void) 19 { 20 PhWorld world = { .area = { .width = 40, .height = 30 }, 21 .player_index = 0, .entity_count = 1 }; 22 PhWorld before; 23 PhCamera camera = { .viewport_w = 320, .viewport_h = 240 }; 24 PhCamera other = { .viewport_w = 160, .viewport_h = 120 }; 25 26 world.entities[0].active = 1; 27 world.entities[0].pos = (PhVec2){ 300, 220 }; 28 memcpy(&before, &world, sizeof(world)); 29 ph_camera_follow_player(&camera, &world); 30 ph_camera_follow_player(&other, &world); 31 if (camera.pos.x != 140 || camera.pos.y != 100 || 32 other.pos.x != 220 || other.pos.y != 160 || 33 memcmp(&before, &world, sizeof(world))) return 1; 34 world.entities[0].pos = (PhVec2){ 639, 479 }; 35 ph_camera_follow_player(&camera, &world); 36 if (camera.pos.x != 320 || camera.pos.y != 240) return 1; 37 world.entities[0].pos = (PhVec2){ 8, 8 }; 38 ph_camera_follow_player(&camera, &world); 39 if (camera.pos.x != 0 || camera.pos.y != 0) return 1; 40 world.entities[0].pos = (PhVec2){ 300, 220 }; 41 camera.viewport_w = 800; 42 camera.viewport_h = 600; 43 ph_camera_follow_player(&camera, &world); 44 if (camera.pos.x != 0 || camera.pos.y != 0) return 1; 45 /* A smaller area must discard the previous area's scroll immediately. */ 46 world.area.width = world.area.height = 4; 47 ph_camera_follow_player(&other, &world); 48 if (other.pos.x != 0 || other.pos.y != 0) return 1; 49 world.player_index = -1; 50 camera.pos = (PhVec2){ 100, 100 }; 51 ph_camera_follow_player(&camera, &world); 52 return camera.pos.x != 0 || camera.pos.y != 0; 53 } 54 55 static int 56 ph_pixel_is(SDL_Surface *surface, int x, int y, PhColor color) 57 { 58 Uint8 r, g, b, a; 59 60 return SDL_ReadSurfacePixel(surface, x, y, &r, &g, &b, &a) && 61 r == color.r && g == color.g && b == color.b && a == color.a; 62 } 63 64 static int 65 ph_debug_smoke(PhRenderer *renderer, SDL_Surface *surface) 66 { 67 unsigned char tiles[64] = { 0 }; 68 const PhTileDef defs[] = { { .symbol = '.' }, { .symbol = '#', .blocks_movement = 1 } }; 69 const PhEntityDef actor = { .id = 1, .kind = PH_ENTITY_MONSTER, .blocks_movement = 1 }; 70 PhWorld world = { 71 .area = { .width = 8, .height = 8, .tiles = tiles, .tile_defs = defs, .tile_def_count = 2 }, 72 .content = { .entities = &actor, .entity_count = 1 }, 73 .player_index = -1, .entity_count = 1, 74 }; 75 PhWorld before; 76 PhCamera camera = { .pos = { 16, 16 }, .viewport_w = PH_VIEW_W, .viewport_h = PH_VIEW_H }; 77 78 tiles[3 * 8 + 2] = 1; 79 world.entities[0] = (PhEntity){ .type_id = 1, .active = 1, .pos = { 96, 96 }, 80 .motion = { .target = { 112, 112 }, .moving = 1 } }; 81 memcpy(&before, &world, sizeof(world)); 82 ph_render_color(renderer, (PhColor){ PH_CLEAR_COLOR }); 83 ph_render_clear(renderer); 84 ph_render_world_debug(renderer, &world, &camera); 85 ph_render_present(renderer); 86 if (memcmp(&before, &world, sizeof(world)) || 87 !ph_pixel_is(surface, 16, 32, (PhColor){ PH_DEBUG_BLOCK_COLOR }) || 88 !ph_pixel_is(surface, 80 - PH_ENTITY_BLOCK_RADIUS, 80, (PhColor){ PH_DEBUG_ACTOR_COLOR }) || 89 !ph_pixel_is(surface, 96 + PH_ENTITY_BLOCK_RADIUS, 96, (PhColor){ PH_DEBUG_TARGET_COLOR }) || 90 !ph_pixel_is(surface, 94, 94, (PhColor){ PH_DEBUG_TARGET_COLOR })) return 1; 91 return 0; 92 } 93 94 static int 95 ph_media_smoke(SDL_Renderer *target, SDL_Surface *surface) 96 { 97 char directory[] = "/tmp/ph-media-XXXXXX"; 98 char path[128], missing[128]; 99 SDL_Surface *source = NULL; 100 PhRenderer *renderer = NULL; 101 PhAudio *audio = NULL; 102 PhAssetDef defs[2]; 103 PhAudioDef sounds[3] = { { 0 }, ph_audio_defs[1], { 0 } }; 104 PhColor background = { 1, 2, 3, 255 }; 105 PhColor red = { 255, 0, 0, 255 }, green = { 0, 255, 0, 255 }; 106 PhRect src = { 0, 0, 3, 1 }; 107 int music = 1, effects = 1, result = 1; 108 109 if (!mkdtemp(directory)) return 1; 110 snprintf(path, sizeof(path), "%s/sprite.bmp", directory); 111 snprintf(missing, sizeof(missing), "%s/missing", directory); 112 defs[0] = (PhAssetDef){ .path = path, .key = { 255, 0, 255 } }; 113 defs[1] = (PhAssetDef){ .path = missing }; 114 sounds[2].path = missing; 115 source = SDL_CreateSurface(3, 1, SDL_PIXELFORMAT_RGBA32); 116 if (!source || !SDL_WriteSurfacePixel(source, 0, 0, 255, 0, 0, 255) || 117 !SDL_WriteSurfacePixel(source, 1, 0, 0, 255, 0, 255) || 118 !SDL_WriteSurfacePixel(source, 2, 0, 255, 0, 255, 255) || 119 !SDL_SaveBMP(source, path)) goto done; 120 /* Fail after loading one asset, then reuse the same borrowed SDL target. */ 121 renderer = ph_render_sdl_create(target, defs, 2); 122 if (renderer) goto done; 123 audio = ph_audio_sdl_create(sounds, 3); 124 if (audio) goto done; 125 renderer = ph_render_sdl_create(target, defs, 1); 126 if (!renderer || !ph_render_has_asset(renderer, 0) || 127 ph_render_has_asset(renderer, -1) || ph_render_has_asset(renderer, 1)) goto done; 128 ph_render_color(renderer, background); 129 ph_render_clear(renderer); 130 ph_render_texture(renderer, 0, src, (PhRect){ 2, 2, 3, 1 }, PH_FLIP_NONE); 131 ph_render_texture(renderer, 0, src, (PhRect){ 2, 3, 3, 1 }, PH_FLIP_HORIZONTAL); 132 ph_render_texture(renderer, -1, src, (PhRect){ 2, 2, 3, 1 }, PH_FLIP_NONE); 133 ph_render_texture(renderer, 1, src, (PhRect){ 2, 2, 3, 1 }, PH_FLIP_NONE); 134 ph_render_color(renderer, green); 135 ph_render_fill(renderer, (PhRect){ 10, 10, 4, 4 }); 136 ph_render_color(renderer, red); 137 ph_render_outline(renderer, (PhRect){ 10, 10, 4, 4 }); 138 ph_render_present(renderer); 139 if (!ph_pixel_is(surface, 2, 2, red) || !ph_pixel_is(surface, 3, 2, green) || 140 !ph_pixel_is(surface, 4, 2, background) || 141 !ph_pixel_is(surface, 2, 3, background) || 142 !ph_pixel_is(surface, 3, 3, green) || !ph_pixel_is(surface, 4, 3, red) || 143 !ph_pixel_is(surface, 10, 10, red) || !ph_pixel_is(surface, 11, 11, green) || 144 !ph_pixel_is(surface, 14, 14, background)) goto done; 145 ph_render_destroy(renderer); 146 renderer = ph_render_sdl_create(target, NULL, 0); 147 if (!renderer || ph_render_has_asset(renderer, 0)) goto done; 148 /* Optional audio must be safe when creation fails or audio is disabled. */ 149 ph_audio_music(NULL, 1); 150 ph_audio_effect(NULL, 1, 1.0f); 151 ph_audio_set_enabled(NULL, 1, 1); 152 ph_audio_update(NULL); 153 ph_audio_get_enabled(NULL, &music, &effects); 154 if (music || effects) goto done; 155 result = 0; 156 done: 157 if (result) fprintf(stderr, "media smoke failed: pixels or resource lifecycle: %s\n", SDL_GetError()); 158 ph_render_destroy(renderer); 159 ph_audio_destroy(audio); 160 SDL_DestroySurface(source); 161 remove(path); 162 rmdir(directory); 163 return result; 164 } 165 166 static int 167 ph_render_enter_portal(PhWorld *world, int destination_area_id) 168 { 169 int i; 170 171 for (i = 0; i < world->area.marker_count; ++i) { 172 const PhAreaMarker *marker = &world->area.markers[i]; 173 const PhMarkerDef *def = ph_area_catalog_marker(ph_game_world.areas, 174 world->area_id, marker->symbol); 175 176 if (!def || def->kind != PH_MARKER_PORTAL || 177 def->destination_area_id != destination_area_id) continue; 178 world->entities[world->player_index].pos = (PhVec2){ 179 PH_TILE_CENTER(marker->tile_x), PH_TILE_CENTER(marker->tile_y) }; 180 return ph_world_update_portal(world, ph_game_world.areas); 181 } 182 return -1; 183 } 184 185 int 186 main(void) 187 { 188 SDL_Surface *surface; 189 SDL_Renderer *target; 190 PhRenderer *renderer; 191 PhAudio *audio = NULL; 192 PhControlLabels labels = { 193 .accept = SDL_GetScancodeName(PH_KEY_MENU_ACCEPT), 194 .back = SDL_GetScancodeName(PH_KEY_MENU_BACK), 195 .cancel = SDL_GetScancodeName(PH_KEY_MENU_CANCEL), 196 .character = SDL_GetScancodeName(PH_KEY_CHARACTER), 197 .inventory = SDL_GetScancodeName(PH_KEY_INVENTORY), 198 .spellbook = SDL_GetScancodeName(PH_KEY_SPELLBOOK), 199 }; 200 PhWorld world; 201 PhInventoryUi inventory_ui; 202 PhSpellbookUi spellbook_ui; 203 PhShopUi shop_ui = { 0 }; 204 PhOptionsUi options_ui = { .music_enabled = 1, .effects_enabled = 1 }; 205 PhStartUi start_ui = { .selected = 1 }; 206 PhGameMode game = PH_GAME_WORLD; 207 PhBattle battle = { 0 }; 208 PhSpellBook spells = ph_player_spellbook; 209 PhGameContext context = { 210 .world = &world, 211 .inventory = &inventory_ui, 212 .spellbook = &spellbook_ui, 213 .shop = &shop_ui, 214 .options = &options_ui, 215 .start = &start_ui, 216 .mode = &game, 217 .battle = &battle, 218 .spells = &spells, 219 }; 220 int music_enabled, effects_enabled; 221 int i; 222 int enemy_index = -1; 223 int result = 0; 224 int order[PH_BATTLE_ORDER_COUNT]; 225 PhBattleResult battle_result; 226 int first_item; 227 int second_item; 228 229 if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) { 230 fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); 231 return 1; 232 } 233 234 surface = SDL_CreateSurface(PH_VIEW_W, PH_VIEW_H, SDL_PIXELFORMAT_ARGB8888); 235 if (!surface) { 236 fprintf(stderr, "SDL_CreateSurface failed: %s\n", SDL_GetError()); 237 SDL_Quit(); 238 return 1; 239 } 240 241 target = SDL_CreateSoftwareRenderer(surface); 242 if (!target) { 243 fprintf(stderr, "SDL_CreateSoftwareRenderer failed: %s\n", SDL_GetError()); 244 SDL_DestroySurface(surface); 245 SDL_Quit(); 246 return 1; 247 } 248 249 result = ph_media_smoke(target, surface); 250 if (ph_camera_smoke()) { 251 fprintf(stderr, "camera smoke test failed\n"); 252 result = 1; 253 } 254 if (ph_world_start(&world, &ph_game_world) < 0) { 255 SDL_DestroyRenderer(target); 256 SDL_DestroySurface(surface); 257 SDL_Quit(); 258 return 1; 259 } 260 if (world.ground_item_count < 2) { 261 fprintf(stderr, "render smoke test failed: item setup\n"); 262 ph_world_destroy(&world); 263 SDL_DestroyRenderer(target); 264 SDL_DestroySurface(surface); 265 SDL_Quit(); 266 return 1; 267 } 268 first_item = world.ground_items[0].item_id; 269 second_item = world.ground_items[1].item_id; 270 271 if (!(renderer = ph_render_sdl_create(target, ph_asset_defs, PH_ASSET_COUNT))) { 272 ph_world_destroy(&world); 273 SDL_DestroyRenderer(target); 274 SDL_DestroySurface(surface); 275 SDL_Quit(); 276 return 1; 277 } 278 if (ph_debug_smoke(renderer, surface)) { 279 fprintf(stderr, "debug overlay geometry test failed\n"); 280 result = 1; 281 } 282 if (ph_start_input(&start_ui, PH_ACTION_ACCEPT) != PH_START_NEW) { 283 fprintf(stderr, "render smoke test failed: new-game selection\n"); 284 result = 1; 285 } 286 start_ui.can_continue = 1; 287 start_ui.selected = 0; 288 if (ph_start_input(&start_ui, PH_ACTION_ACCEPT) != PH_START_CONTINUE) { 289 fprintf(stderr, "render smoke test failed: continue selection\n"); 290 result = 1; 291 } 292 context.menu = PH_MENU_START; 293 ph_present_frame(renderer, &context, &labels, NULL); 294 { 295 PhWorld before; 296 PhDebugView debug = { .enabled = 1, .frame_ms = 16, .steps = 1 }; 297 PhColor baseline; 298 299 memcpy(&before, &world, sizeof(world)); 300 SDL_ReadSurfacePixel(surface, 0, 0, &baseline.r, &baseline.g, &baseline.b, &baseline.a); 301 ph_present_frame(renderer, &context, &labels, &debug); 302 if (!ph_pixel_is(surface, 0, 0, (PhColor){ PH_DEBUG_BG_COLOR }) || 303 memcmp(&before, &world, sizeof(world))) result = 1; 304 debug.enabled = 0; 305 ph_present_frame(renderer, &context, &labels, &debug); 306 if (!ph_pixel_is(surface, 0, 0, baseline)) result = 1; 307 } 308 309 if (!(audio = ph_audio_sdl_create(ph_audio_defs, PH_AUDIO_COUNT))) { 310 fprintf(stderr, "audio smoke test failed: %s\n", SDL_GetError()); 311 ph_render_destroy(renderer); 312 ph_world_destroy(&world); 313 SDL_DestroyRenderer(target); 314 SDL_DestroySurface(surface); 315 SDL_Quit(); 316 return 1; 317 } 318 ph_audio_music(audio, PH_MUSIC_MEADOW); 319 ph_audio_update(audio); 320 ph_audio_effect(audio, PH_SOUND_ATTACK, 1.0f); 321 ph_audio_effect(audio, PH_SOUND_HIT, 1.0f); 322 ph_audio_effect(audio, PH_SOUND_SPELL, 1.0f); 323 ph_audio_effect(audio, PH_SOUND_MENU, 0.5f); 324 ph_audio_set_enabled(audio, 0, 0); 325 ph_audio_get_enabled(audio, &music_enabled, &effects_enabled); 326 if (music_enabled || effects_enabled || 327 ph_audio_distance_gain(0.0f, 0.0f, 160.0f, 0.0f, 160.0f) != 0.0f || 328 ph_audio_distance_gain(0.0f, 0.0f, 80.0f, 0.0f, 160.0f) != 0.5f) { 329 fprintf(stderr, "audio smoke test failed: controls or distance\n"); 330 result = 1; 331 } 332 ph_audio_set_enabled(audio, 1, 1); 333 ph_audio_music(audio, PH_MUSIC_TOWN); 334 ph_audio_music(audio, PH_MUSIC_BATTLE); 335 336 ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); 337 if (ph_world_item_amount(&world, first_item) != 0) { 338 fprintf(stderr, "render smoke test failed: adjacent item pickup\n"); 339 result = 1; 340 } 341 ph_world_tick(&world, (PhInput){ .move_x = 1 }, 342 (float)PH_WORLD_STEP_MILLISECONDS / 1000.0f); 343 ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); 344 ph_world_tick(&world, (PhInput){ .move_y = 1 }, 345 (float)PH_WORLD_STEP_MILLISECONDS / 1000.0f); 346 ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); 347 if (ph_world_item_amount(&world, first_item) != 1 || 348 ph_world_item_amount(&world, second_item) != 1) { 349 fprintf(stderr, "render smoke test failed: item pickup\n"); 350 result = 1; 351 } 352 ph_inventory_open(&inventory_ui, &world); 353 ph_options_open(&options_ui); 354 if (ph_options_input(&options_ui, PH_ACTION_DOWN) != PH_OPTIONS_STAY || 355 options_ui.selected != 1 || 356 ph_options_input(&options_ui, PH_ACTION_UP) != PH_OPTIONS_STAY || 357 options_ui.selected != 0 || 358 ph_options_input(&options_ui, PH_ACTION_ACCEPT) != PH_OPTIONS_STAY || 359 options_ui.mode != PH_OPTIONS_SOUND) { 360 fprintf(stderr, "render smoke test failed: options navigation\n"); 361 result = 1; 362 } 363 ph_options_input(&options_ui, PH_ACTION_ACCEPT); 364 ph_options_input(&options_ui, PH_ACTION_DOWN); 365 ph_options_input(&options_ui, PH_ACTION_ACCEPT); 366 if (options_ui.music_enabled || options_ui.effects_enabled) { 367 fprintf(stderr, "render smoke test failed: sound toggles\n"); 368 result = 1; 369 } 370 context.menu = PH_MENU_MAIN; 371 ph_present_frame(renderer, &context, &labels, NULL); 372 ph_options_input(&options_ui, PH_ACTION_BACK); 373 if (ph_options_input(&options_ui, PH_ACTION_BACK) != PH_OPTIONS_CLOSE) { 374 fprintf(stderr, "render smoke test failed: options cancel\n"); 375 result = 1; 376 } 377 ph_options_open(&options_ui); 378 ph_options_input(&options_ui, PH_ACTION_DOWN); 379 if (ph_options_input(&options_ui, PH_ACTION_ACCEPT) != PH_OPTIONS_EXIT) { 380 fprintf(stderr, "render smoke test failed: exit option\n"); 381 result = 1; 382 } 383 options_ui.music_enabled = 1; 384 options_ui.effects_enabled = 1; 385 context.menu = PH_MENU_NONE; 386 ph_inventory_input(&inventory_ui, &world, PH_ACTION_ACCEPT); 387 if (inventory_ui.option != PH_INVENTORY_EQUIP) result = 1; 388 ph_inventory_input(&inventory_ui, &world, PH_ACTION_DOWN); 389 if (inventory_ui.option != PH_INVENTORY_DROP) result = 1; 390 inventory_ui.mode = PH_INVENTORY_BROWSE; 391 ph_inventory_input(&inventory_ui, &world, PH_ACTION_DOWN); 392 ph_inventory_input(&inventory_ui, &world, PH_ACTION_ACCEPT); 393 if (inventory_ui.option != PH_INVENTORY_USE) result = 1; 394 ph_inventory_input(&inventory_ui, &world, PH_ACTION_DOWN); 395 if (inventory_ui.option != PH_INVENTORY_DROP) { 396 fprintf(stderr, "render smoke test failed: inventory actions\n"); 397 result = 1; 398 } 399 inventory_ui.mode = PH_INVENTORY_EXAMINE; 400 if (ph_inventory_input(&inventory_ui, &world, PH_ACTION_BACK) || 401 inventory_ui.mode != PH_INVENTORY_ACTIONS || 402 ph_inventory_input(&inventory_ui, &world, PH_ACTION_BACK) || 403 inventory_ui.mode != PH_INVENTORY_BROWSE || 404 !ph_inventory_input(&inventory_ui, &world, PH_ACTION_BACK)) { 405 fprintf(stderr, "render smoke test failed: inventory cancel\n"); 406 result = 1; 407 } 408 ph_inventory_open(&inventory_ui, &world); 409 for (i = 0; i < 10; ++i) { 410 inventory_ui.mode = i == 8 ? PH_INVENTORY_EXAMINE : 411 i == 9 ? PH_INVENTORY_ACTIONS : PH_INVENTORY_BROWSE; 412 context.menu = i == 7 ? PH_MENU_CHARACTER : 413 i >= 6 ? PH_MENU_INVENTORY : PH_MENU_NONE; 414 ph_present_frame(renderer, &context, &labels, NULL); 415 } 416 ph_spellbook_open(&spellbook_ui, &spells); 417 ph_spellbook_input(&spellbook_ui, &world, &spells, PH_ACTION_ACCEPT); 418 if (spellbook_ui.option != PH_SPELLBOOK_EXAMINE_ACTION) { 419 fprintf(stderr, "render smoke test failed: disabled field spell\n"); 420 result = 1; 421 } 422 spellbook_ui.mode = PH_SPELLBOOK_BROWSE; 423 spellbook_ui.selected = 1; 424 ph_spellbook_input(&spellbook_ui, &world, &spells, PH_ACTION_ACCEPT); 425 if (spellbook_ui.option != PH_SPELLBOOK_USE) result = 1; 426 world.entities[world.player_index].vitals.hp -= 10; 427 ph_spellbook_input(&spellbook_ui, &world, &spells, PH_ACTION_ACCEPT); 428 if (ph_world_player(&world)->vitals.hp != 429 ph_world_entity_stats(&world, ph_world_player(&world)).max_hp || 430 ph_world_player(&world)->vitals.mp != 40) { 431 fprintf(stderr, "render smoke test failed: field Heal\n"); 432 result = 1; 433 } 434 world.entities[world.player_index].vitals.mp = 435 ph_world_entity_stats(&world, ph_world_player(&world)).max_mp; 436 context.menu = PH_MENU_SPELLBOOK; 437 ph_present_frame(renderer, &context, &labels, NULL); 438 spellbook_ui.mode = PH_SPELLBOOK_ACTIONS; 439 ph_present_frame(renderer, &context, &labels, NULL); 440 spellbook_ui.mode = PH_SPELLBOOK_EXAMINE; 441 ph_present_frame(renderer, &context, &labels, NULL); 442 ph_shop_open(&shop_ui, ph_shop_defs[0].id); 443 for (i = PH_SHOP_ROOT; i <= PH_SHOP_TALK; ++i) { 444 shop_ui.mode = (PhShopMode)i; 445 shop_ui.selected = i == PH_SHOP_SELL ? 446 ph_inventory_step(&world, -1, 1) : 0; 447 context.menu = PH_MENU_SHOP; 448 ph_present_frame(renderer, &context, &labels, NULL); 449 } 450 ph_shop_open(&shop_ui, PH_SHOP_MAGICK); 451 shop_ui.mode = PH_SHOP_ROOT; 452 ph_present_frame(renderer, &context, &labels, NULL); 453 ph_shop_input(&shop_ui, &world, &spells, PH_ACTION_ACCEPT); 454 if (shop_ui.mode != PH_SHOP_BUY) result = 1; 455 ph_present_frame(renderer, &context, &labels, NULL); 456 ph_shop_input(&shop_ui, &world, &spells, PH_ACTION_ACCEPT); 457 if (!ph_spellbook_has(&spells, PH_SPELL_EMBER) || 458 ph_world_player(&world)->gold != 16) { 459 fprintf(stderr, "render smoke test failed: magick purchase\n"); 460 result = 1; 461 } 462 ph_present_frame(renderer, &context, &labels, NULL); 463 shop_ui.mode = PH_SHOP_TALK; 464 ph_present_frame(renderer, &context, &labels, NULL); 465 for (i = 0; i < world.entity_count; ++i) { 466 const PhEntityDef *def = ph_world_entity_def(&world, world.entities[i].type_id); 467 if (def && def->kind == PH_ENTITY_MONSTER) enemy_index = i; 468 } 469 if (enemy_index < 0) { 470 fprintf(stderr, "render smoke test failed: enemy setup\n"); 471 result = 1; 472 } else { 473 world.encounter_index = enemy_index; 474 game = PH_GAME_WORLD; 475 context.menu = PH_MENU_NONE; 476 ph_battle_begin(&battle, &world, enemy_index, spells); 477 game = PH_GAME_TRANSITION; 478 ph_present_frame(renderer, &context, &labels, NULL); 479 if (game != PH_GAME_TRANSITION) result = 1; 480 context.menu = PH_MENU_NONE; 481 ph_battle_update(&battle, &world, PH_BATTLE_TRANSITION_SECONDS * 0.5f, 1); 482 ph_present_frame(renderer, &context, &labels, NULL); 483 game = PH_GAME_BATTLE; 484 ph_options_open(&options_ui); 485 context.menu = PH_MENU_MAIN; 486 ph_present_frame(renderer, &context, &labels, NULL); 487 context.menu = PH_MENU_NONE; 488 battle.phase = PH_BATTLE_ENEMY_ACTION; 489 battle.timer = PH_BATTLE_ACTION_SECONDS; 490 battle.resolved = 0; 491 ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS * 0.5f, 0); 492 if (ph_battle_take_event(&battle) != PH_BATTLE_EVENT_HIT) { 493 fprintf(stderr, "render smoke test failed: enemy sound event\n"); 494 result = 1; 495 } 496 battle.timer = 0.0f; 497 world.entities[world.player_index].vitals.hp = 0; 498 context.menu = PH_MENU_NONE; 499 battle_result = ph_battle_update(&battle, &world, 0.0f, 0); 500 if (battle_result == PH_BATTLE_DEFEAT) game = PH_GAME_OVER; 501 ph_present_frame(renderer, &context, &labels, NULL); 502 if (game != PH_GAME_OVER) result = 1; 503 world.entities[world.player_index].vitals.hp = 504 ph_world_entity_stats(&world, ph_world_player(&world)).max_hp; 505 ph_battle_begin(&battle, &world, enemy_index, spells); 506 game = PH_GAME_TRANSITION; 507 context.menu = PH_MENU_NONE; 508 if (ph_battle_update(&battle, &world, PH_BATTLE_TRANSITION_SECONDS, 1) == 509 PH_BATTLE_READY) game = PH_GAME_BATTLE; 510 ph_present_frame(renderer, &context, &labels, NULL); 511 world.entities[enemy_index].vitals.hp = 512 ph_world_entity_stats(&world, &world.entities[enemy_index]).max_hp; 513 world.entities[enemy_index].vitals.hp = 1; 514 battle.choice.command = PH_BATTLE_MAGICK; 515 ph_battle_select(&battle, &world); 516 battle.choice.spell = 1; 517 ph_battle_cast(&battle, &world); 518 if (battle.phase != PH_BATTLE_TARGET || 519 battle.choice.target_index != world.player_index) result = 1; 520 if (ph_battle_target_step(&battle, &world, 1) != enemy_index) result = 1; 521 ph_present_frame(renderer, &context, &labels, NULL); 522 ph_battle_confirm_target(&battle, &world); 523 ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS * 0.5f, 0); 524 if (ph_battle_take_event(&battle) != PH_BATTLE_EVENT_SPELL) { 525 fprintf(stderr, "render smoke test failed: healing sound event\n"); 526 result = 1; 527 } 528 if (world.entities[enemy_index].vitals.hp != 529 ph_world_entity_stats(&world, &world.entities[enemy_index]).max_hp || 530 ph_world_player(&world)->vitals.mp != 40) { 531 fprintf(stderr, "render smoke test failed: healing enemy target\n"); 532 result = 1; 533 } 534 world.entities[world.player_index].vitals.mp = 535 ph_world_entity_stats(&world, ph_world_player(&world)).max_mp; 536 ph_battle_begin(&battle, &world, enemy_index, spells); 537 ph_battle_update(&battle, &world, PH_BATTLE_TRANSITION_SECONDS, 1); 538 battle.choice.command = PH_BATTLE_FIGHT; 539 ph_battle_order(&battle, &world, order); 540 if (order[0] != world.player_index || order[1] != enemy_index) { 541 fprintf(stderr, "render smoke test failed: normal CTB order\n"); 542 result = 1; 543 } 544 battle.choice.command = PH_BATTLE_MAGICK; 545 ph_battle_order(&battle, &world, order); 546 if (order[0] != world.player_index || order[1] != enemy_index) { 547 fprintf(stderr, "render smoke test failed: command CTB order\n"); 548 result = 1; 549 } 550 ph_battle_select(&battle, &world); 551 if (battle.phase != PH_BATTLE_MAGIC || ph_world_player(&world)->vitals.mp != 50) { 552 fprintf(stderr, "render smoke test failed: Magick submenu\n"); 553 result = 1; 554 } 555 ph_battle_order(&battle, &world, order); 556 if (order[0] != world.player_index || order[1] != world.player_index) { 557 fprintf(stderr, "render smoke test failed: spell CTB order\n"); 558 result = 1; 559 } 560 context.menu = PH_MENU_NONE; 561 ph_present_frame(renderer, &context, &labels, NULL); 562 ph_battle_cast(&battle, &world); 563 if (battle.phase != PH_BATTLE_TARGET || 564 battle.choice.target_index != enemy_index) { 565 fprintf(stderr, "render smoke test failed: offensive target default\n"); 566 result = 1; 567 } 568 ph_present_frame(renderer, &context, &labels, NULL); 569 ph_battle_confirm_target(&battle, &world); 570 context.menu = PH_MENU_NONE; 571 ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS * 0.5f, 0); 572 if (ph_battle_take_event(&battle) != PH_BATTLE_EVENT_SPELL) { 573 fprintf(stderr, "render smoke test failed: spell sound event\n"); 574 result = 1; 575 } 576 ph_present_frame(renderer, &context, &labels, NULL); 577 context.menu = PH_MENU_NONE; 578 ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS * 0.5f, 0); 579 ph_present_frame(renderer, &context, &labels, NULL); 580 if (game != PH_GAME_BATTLE || battle.phase != PH_BATTLE_COMMAND || 581 battle.turn.current_actor != world.player_index || 582 ph_world_player(&world)->vitals.mp != 35 || 583 world.entities[enemy_index].accuracy.value != 20 || 584 world.entities[enemy_index].accuracy.turns != 3) { 585 fprintf(stderr, "render smoke test failed: Flash action\n"); 586 result = 1; 587 } 588 battle.choice.command = PH_BATTLE_FIGHT; 589 ph_battle_select(&battle, &world); 590 ph_battle_confirm_target(&battle, &world); 591 context.menu = PH_MENU_NONE; 592 ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS * 0.5f, 0); 593 if (ph_battle_take_event(&battle) != PH_BATTLE_EVENT_ATTACK) { 594 fprintf(stderr, "render smoke test failed: attack sound event\n"); 595 result = 1; 596 } 597 ph_present_frame(renderer, &context, &labels, NULL); 598 context.menu = PH_MENU_NONE; 599 battle_result = ph_battle_update(&battle, &world, 600 PH_BATTLE_ACTION_SECONDS * 0.5f, 0); 601 if (battle_result == PH_BATTLE_VICTORY) game = PH_GAME_VICTORY; 602 ph_present_frame(renderer, &context, &labels, NULL); 603 if (game != PH_GAME_VICTORY || 604 ph_world_player(&world)->xp != battle.reward.xp || 605 ph_world_item_amount(&world, battle.reward.item_id) != 2) { 606 fprintf(stderr, "render smoke test failed: victory rewards\n"); 607 result = 1; 608 } 609 } 610 game = PH_GAME_WORLD; 611 for (i = 0; i < world.area.marker_count; ++i) { 612 const PhMarkerDef *def = ph_area_catalog_marker(ph_game_world.areas, 613 world.area_id, world.area.markers[i].symbol); 614 615 if (!def || def->kind != PH_MARKER_PORTAL) continue; 616 world.entities[world.player_index].pos = (PhVec2){ 617 PH_TILE_CENTER(world.area.markers[i].tile_x), 618 PH_TILE_CENTER(world.area.markers[i].tile_y) }; 619 break; 620 } 621 if (i == world.area.marker_count || 622 ph_world_update_portal(&world, ph_game_world.areas) != 1) { 623 fprintf(stderr, "render smoke test failed: town portal\n"); 624 result = 1; 625 } 626 for (i = 0; i < world.area.marker_count; ++i) { 627 const PhMarkerDef *def = ph_area_catalog_marker(ph_game_world.areas, 628 world.area_id, world.area.markers[i].symbol); 629 630 if (!def || def->kind != PH_MARKER_PORTAL || 631 def->destination_area_id != PH_AREA_MIRA_SHOP) continue; 632 world.entities[world.player_index].pos = (PhVec2){ 633 PH_TILE_CENTER(world.area.markers[i].tile_x), 634 PH_TILE_CENTER(world.area.markers[i].tile_y) }; 635 break; 636 } 637 if (i == world.area.marker_count || 638 ph_world_update_portal(&world, ph_game_world.areas) != 1 || 639 world.area_id != PH_AREA_MIRA_SHOP) { 640 fprintf(stderr, "render smoke test failed: shop portal\n"); 641 result = 1; 642 } 643 ph_shop_open(&shop_ui, ph_shop_defs[0].id); 644 shop_ui.mode = PH_SHOP_TALK; 645 context.menu = PH_MENU_SHOP; 646 ph_present_frame(renderer, &context, &labels, NULL); 647 if (ph_render_enter_portal(&world, PH_AREA_STONEHOLLOW) != 1) { 648 fprintf(stderr, "render smoke test failed: return to town\n"); 649 result = 1; 650 } 651 context.menu = PH_MENU_NONE; 652 ph_present_frame(renderer, &context, &labels, NULL); 653 if (ph_render_enter_portal(&world, PH_AREA_MEADOW) != 1) { 654 fprintf(stderr, "render smoke test failed: return to meadow\n"); 655 result = 1; 656 } 657 ph_present_frame(renderer, &context, &labels, NULL); 658 659 if (!SDL_SaveBMP(surface, PH_RENDER_SMOKE_PATH)) { 660 fprintf(stderr, "SDL_SaveBMP failed: %s\n", SDL_GetError()); 661 result = 1; 662 } 663 664 ph_audio_destroy(audio); 665 ph_render_destroy(renderer); 666 ph_world_destroy(&world); 667 SDL_DestroyRenderer(target); 668 SDL_DestroySurface(surface); 669 SDL_Quit(); 670 return result; 671 } 672 673 674 #else 675 int 676 main(void) 677 { 678 fprintf(stderr, "SDL3 not available in this build\n"); 679 return 1; 680 } 681 #endif