main.c (13572B)
1 #include "engine/world.h" 2 3 #include <stdbool.h> 4 #include <stdio.h> 5 #include <string.h> 6 7 #if PH_USE_SDL 8 #include <SDL3/SDL.h> 9 #endif 10 11 enum { 12 PH_VIEW_W = 320, 13 PH_VIEW_H = 240, 14 PH_SCALE = 3, 15 }; 16 17 enum { 18 PH_DEF_PLAYER = 1, 19 PH_DEF_SLIME, 20 }; 21 22 enum { 23 PH_ITEM_TALENT_SHARD = 1, 24 }; 25 26 #define PH_START_MAP_PATH "obj/maps/ashen-meadow.phmap" 27 #define PH_TILESET_PATH "obj/assets/overworld_tileset_grass.bmp" 28 #define PH_HERO_PATH "obj/assets/hero.bmp" 29 #define PH_ITEMS_PATH "obj/assets/items.bmp" 30 #define PH_CRITTERS_PATH "obj/assets/critters.bmp" 31 #define PH_RENDER_SMOKE_PATH "obj/render-smoke.bmp" 32 33 enum { 34 PH_TILESET_COLUMNS = 12, 35 PH_ITEMS_COLUMNS = 8, 36 PH_TILE_GRASS = 13, 37 PH_TILE_BLOCKED = 61, 38 }; 39 40 #if PH_USE_SDL 41 typedef struct { 42 SDL_Texture *tileset; 43 SDL_Texture *hero; 44 SDL_Texture *items; 45 SDL_Texture *critters; 46 } PhAssets; 47 #endif 48 49 static int 50 ph_make_world(PhWorld *world) 51 { 52 PhArea area; 53 int player; 54 55 if (ph_area_load(&area, PH_START_MAP_PATH) < 0) { 56 fprintf(stderr, "failed to load %s\n", PH_START_MAP_PATH); 57 return -1; 58 } 59 60 ph_world_init(world, area, (float)PH_VIEW_W, (float)PH_VIEW_H); 61 ph_world_add_entity_def(world, (PhEntityDef){ 62 .id = PH_DEF_PLAYER, 63 .name = "Wayfarer", 64 .max_hp = 40, 65 .move_speed = 90, 66 .sprite_tile_x = 0, 67 .sprite_tile_y = 2, 68 .blocks_movement = 0, 69 .kind = PH_ENTITY_PLAYER, 70 }); 71 ph_world_add_entity_def(world, (PhEntityDef){ 72 .id = PH_DEF_SLIME, 73 .name = "Mire Slime", 74 .max_hp = 12, 75 .move_speed = 45, 76 .sprite_tile_x = 12, 77 .sprite_tile_y = 0, 78 .blocks_movement = 1, 79 .kind = PH_ENTITY_MONSTER, 80 }); 81 ph_world_add_item_def(world, (PhItemDef){ 82 .id = PH_ITEM_TALENT_SHARD, 83 .name = "Talent Shard", 84 .value = 30, 85 .power = 0, 86 .talent_points = 1, 87 .sprite_tile_x = 2, 88 .sprite_tile_y = 6, 89 }); 90 91 player = ph_world_spawn_entity(world, PH_DEF_PLAYER, (PhVec2){ 80.0f, 80.0f }); 92 ph_world_spawn_entity(world, PH_DEF_SLIME, (PhVec2){ 180.0f, 120.0f }); 93 ph_world_drop_item(world, PH_ITEM_TALENT_SHARD, (PhVec2){ 104.0f, 80.0f }, 1); 94 ph_world_set_player(world, player); 95 96 return 0; 97 } 98 99 static int 100 ph_run_smoke_test(void) 101 { 102 PhWorld world; 103 PhInput input = { .move_x = 1, .move_y = 0, .interact = 0 }; 104 const PhEntity *player; 105 int i; 106 107 if (ph_make_world(&world) < 0) { 108 return 1; 109 } 110 111 for (i = 0; i < 30; ++i) { 112 input.interact = i == 20; 113 ph_world_tick(&world, input, 1.0f / 60.0f); 114 } 115 116 player = ph_world_player(&world); 117 if (!player) { 118 fprintf(stderr, "smoke test failed: no player\n"); 119 return 1; 120 } 121 122 printf("area=%s player=(%.1f,%.1f) camera=(%.1f,%.1f) talent=%d\n", 123 world.area.name, 124 player->pos.x, 125 player->pos.y, 126 world.camera.pos.x, 127 world.camera.pos.y, 128 world.player_talent_points); 129 ph_area_free(&world.area); 130 return 0; 131 } 132 133 #if PH_USE_SDL 134 static SDL_Texture * 135 ph_load_sprite_sheet(SDL_Renderer *renderer, const char *path, Uint8 key_r, Uint8 key_g, Uint8 key_b) 136 { 137 SDL_Surface *surface; 138 SDL_Texture *texture; 139 140 surface = SDL_LoadBMP(path); 141 if (!surface) { 142 fprintf(stderr, "SDL_LoadBMP failed for %s: %s\n", path, SDL_GetError()); 143 return NULL; 144 } 145 146 SDL_SetSurfaceColorKey(surface, true, SDL_MapSurfaceRGB(surface, key_r, key_g, key_b)); 147 texture = SDL_CreateTextureFromSurface(renderer, surface); 148 SDL_DestroySurface(surface); 149 if (!texture) { 150 fprintf(stderr, "SDL_CreateTextureFromSurface failed: %s\n", SDL_GetError()); 151 } 152 return texture; 153 } 154 155 static int 156 ph_load_assets(PhAssets *assets, SDL_Renderer *renderer) 157 { 158 memset(assets, 0, sizeof(*assets)); 159 160 assets->tileset = ph_load_sprite_sheet(renderer, PH_TILESET_PATH, 0, 0, 0); 161 assets->hero = ph_load_sprite_sheet(renderer, PH_HERO_PATH, 0, 0, 0); 162 assets->items = ph_load_sprite_sheet(renderer, PH_ITEMS_PATH, 111, 119, 109); 163 assets->critters = ph_load_sprite_sheet(renderer, PH_CRITTERS_PATH, 129, 255, 94); 164 165 if (!assets->tileset || !assets->hero || !assets->items || !assets->critters) { 166 SDL_DestroyTexture(assets->tileset); 167 SDL_DestroyTexture(assets->hero); 168 SDL_DestroyTexture(assets->items); 169 SDL_DestroyTexture(assets->critters); 170 memset(assets, 0, sizeof(*assets)); 171 return -1; 172 } 173 return 0; 174 } 175 176 static void 177 ph_destroy_assets(PhAssets *assets) 178 { 179 SDL_DestroyTexture(assets->tileset); 180 SDL_DestroyTexture(assets->hero); 181 SDL_DestroyTexture(assets->items); 182 SDL_DestroyTexture(assets->critters); 183 memset(assets, 0, sizeof(*assets)); 184 } 185 186 static void 187 ph_draw_sprite(SDL_Renderer *renderer, SDL_Texture *texture, 188 int sprite_tile_x, int sprite_tile_y, SDL_FRect dst, SDL_FlipMode flip) 189 { 190 SDL_FRect src = { 191 .x = (float)(sprite_tile_x * PH_TILE_SIZE), 192 .y = (float)(sprite_tile_y * PH_TILE_SIZE), 193 .w = (float)PH_TILE_SIZE, 194 .h = (float)PH_TILE_SIZE, 195 }; 196 197 SDL_RenderTextureRotated(renderer, texture, &src, &dst, 0.0, NULL, flip); 198 } 199 200 static void 201 ph_player_sprite_frame(const PhEntityDef *def, const PhEntity *entity, 202 int *sprite_tile_x, int *sprite_tile_y, SDL_FlipMode *flip) 203 { 204 if (entity->facing_y > 0) { 205 *sprite_tile_x = def->sprite_tile_x + 4; 206 *sprite_tile_y = 0; 207 *flip = SDL_FLIP_NONE; 208 return; 209 } 210 if (entity->facing_y < 0) { 211 *sprite_tile_x = def->sprite_tile_x; 212 *sprite_tile_y = 0; 213 *flip = SDL_FLIP_NONE; 214 return; 215 } 216 217 *sprite_tile_x = def->sprite_tile_x; 218 *sprite_tile_y = 1; 219 *flip = entity->facing_x > 0 ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; 220 } 221 222 static void 223 ph_draw_area(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld *world) 224 { 225 int tx; 226 int ty; 227 228 for (ty = 0; ty < world->area.height; ++ty) { 229 for (tx = 0; tx < world->area.width; ++tx) { 230 SDL_FRect rect = { 231 .x = (float)(tx * PH_TILE_SIZE) - world->camera.pos.x, 232 .y = (float)(ty * PH_TILE_SIZE) - world->camera.pos.y, 233 .w = (float)PH_TILE_SIZE, 234 .h = (float)PH_TILE_SIZE, 235 }; 236 int tile_id = ph_area_tile_blocked(&world->area, tx, ty) ? 237 PH_TILE_BLOCKED : PH_TILE_GRASS; 238 SDL_FRect src = { 239 .x = (float)((tile_id % PH_TILESET_COLUMNS) * PH_TILE_SIZE), 240 .y = (float)((tile_id / PH_TILESET_COLUMNS) * PH_TILE_SIZE), 241 .w = (float)PH_TILE_SIZE, 242 .h = (float)PH_TILE_SIZE, 243 }; 244 245 SDL_RenderTexture(renderer, assets->tileset, &src, &rect); 246 } 247 } 248 } 249 250 static void 251 ph_draw_items(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld *world) 252 { 253 int i; 254 255 for (i = 0; i < world->ground_item_count; ++i) { 256 const PhGroundItem *drop = &world->ground_items[i]; 257 const PhItemDef *def = ph_world_item_def(world, drop->item_id); 258 SDL_FRect rect; 259 260 if (!drop->active) { 261 continue; 262 } 263 264 rect.x = drop->pos.x - world->camera.pos.x - 3.0f; 265 rect.y = drop->pos.y - world->camera.pos.y - 3.0f; 266 rect.w = 6.0f; 267 rect.h = 6.0f; 268 269 if (def && def->sprite_tile_x >= 0 && def->sprite_tile_y >= 0) { 270 rect.x = drop->pos.x - world->camera.pos.x - 8.0f; 271 rect.y = drop->pos.y - world->camera.pos.y - 8.0f; 272 rect.w = 16.0f; 273 rect.h = 16.0f; 274 ph_draw_sprite(renderer, assets->items, 275 def->sprite_tile_x, def->sprite_tile_y, rect, SDL_FLIP_NONE); 276 } else { 277 SDL_SetRenderDrawColor(renderer, 242, 214, 80, 255); 278 SDL_RenderFillRect(renderer, &rect); 279 } 280 } 281 } 282 283 static void 284 ph_draw_entities(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld *world) 285 { 286 int i; 287 288 for (i = 0; i < world->entity_count; ++i) { 289 const PhEntity *entity = &world->entities[i]; 290 const PhEntityDef *def = ph_world_entity_def(world, entity->type_id); 291 SDL_FRect rect; 292 293 if (!entity->active || !def) { 294 continue; 295 } 296 297 if (def->sprite_tile_x >= 0 && def->sprite_tile_y >= 0 && 298 (def->kind == PH_ENTITY_PLAYER || def->kind == PH_ENTITY_MONSTER)) { 299 SDL_Texture *sheet = def->kind == PH_ENTITY_PLAYER ? assets->hero : assets->critters; 300 int sprite_tile_x = def->sprite_tile_x; 301 int sprite_tile_y = def->sprite_tile_y; 302 SDL_FlipMode flip = SDL_FLIP_NONE; 303 304 rect.x = entity->pos.x - world->camera.pos.x - 8.0f; 305 rect.y = entity->pos.y - world->camera.pos.y - 12.0f; 306 rect.w = 16.0f; 307 rect.h = 16.0f; 308 309 if (def->kind == PH_ENTITY_PLAYER) { 310 ph_player_sprite_frame(def, entity, &sprite_tile_x, &sprite_tile_y, &flip); 311 } 312 ph_draw_sprite(renderer, sheet, sprite_tile_x, sprite_tile_y, rect, flip); 313 } else { 314 rect.x = entity->pos.x - world->camera.pos.x - 7.0f; 315 rect.y = entity->pos.y - world->camera.pos.y - 12.0f; 316 rect.w = 14.0f; 317 rect.h = 18.0f; 318 319 if (def->kind == PH_ENTITY_MONSTER) { 320 SDL_SetRenderDrawColor(renderer, 168, 108, 224, 255); 321 } else { 322 SDL_SetRenderDrawColor(renderer, 220, 180, 90, 255); 323 } 324 SDL_RenderFillRect(renderer, &rect); 325 } 326 } 327 } 328 329 static void 330 ph_render_frame(SDL_Renderer *renderer, const PhAssets *assets, PhWorld *world, PhInput input, float dt) 331 { 332 ph_world_tick(world, input, dt); 333 334 SDL_SetRenderDrawColor(renderer, 16, 18, 24, 255); 335 SDL_RenderClear(renderer); 336 ph_draw_area(renderer, assets, world); 337 ph_draw_items(renderer, assets, world); 338 ph_draw_entities(renderer, assets, world); 339 if (world->notice_seconds > 0.0f && world->notice[0] != '\0') { 340 SDL_FRect notice_bg = { 341 .x = 8.0f, 342 .y = (float)(PH_VIEW_H - 28), 343 .w = (float)(PH_VIEW_W - 16), 344 .h = 20.0f, 345 }; 346 347 SDL_SetRenderDrawColor(renderer, 20, 24, 34, 255); 348 SDL_RenderFillRect(renderer, ¬ice_bg); 349 SDL_SetRenderDrawColor(renderer, 232, 236, 244, 255); 350 SDL_RenderDebugText(renderer, 14.0f, (float)(PH_VIEW_H - 22), world->notice); 351 } 352 SDL_RenderPresent(renderer); 353 } 354 355 static PhInput 356 ph_read_input(const bool *keys) 357 { 358 PhInput input = { 0 }; 359 360 if (keys[SDL_SCANCODE_LEFT] || keys[SDL_SCANCODE_A]) { 361 input.move_x -= 1; 362 } 363 if (keys[SDL_SCANCODE_RIGHT] || keys[SDL_SCANCODE_D]) { 364 input.move_x += 1; 365 } 366 if (keys[SDL_SCANCODE_UP] || keys[SDL_SCANCODE_W]) { 367 input.move_y -= 1; 368 } 369 if (keys[SDL_SCANCODE_DOWN] || keys[SDL_SCANCODE_S]) { 370 input.move_y += 1; 371 } 372 if (keys[SDL_SCANCODE_E] || keys[SDL_SCANCODE_SPACE]) { 373 input.interact = 1; 374 } 375 376 return input; 377 } 378 379 static int 380 ph_create_window_renderer(SDL_Window **window, SDL_Renderer **renderer) 381 { 382 *window = SDL_CreateWindow("phantasia", 383 PH_VIEW_W * PH_SCALE, 384 PH_VIEW_H * PH_SCALE, 385 0); 386 if (!*window) { 387 fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError()); 388 return -1; 389 } 390 391 *renderer = SDL_CreateRenderer(*window, NULL); 392 if (!*renderer) { 393 fprintf(stderr, "SDL_CreateRenderer default backend failed: %s\n", SDL_GetError()); 394 *renderer = SDL_CreateRenderer(*window, "software"); 395 } 396 if (!*renderer) { 397 fprintf(stderr, "SDL_CreateRenderer software fallback failed: %s\n", SDL_GetError()); 398 SDL_DestroyWindow(*window); 399 *window = NULL; 400 return -1; 401 } 402 403 return 0; 404 } 405 406 static int 407 ph_run_render_smoke_test(void) 408 { 409 SDL_Surface *surface; 410 SDL_Renderer *renderer; 411 PhAssets assets; 412 PhWorld world; 413 int i; 414 415 if (!SDL_Init(SDL_INIT_VIDEO)) { 416 fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); 417 return 1; 418 } 419 420 surface = SDL_CreateSurface(PH_VIEW_W, PH_VIEW_H, SDL_PIXELFORMAT_ARGB8888); 421 if (!surface) { 422 fprintf(stderr, "SDL_CreateSurface failed: %s\n", SDL_GetError()); 423 SDL_Quit(); 424 return 1; 425 } 426 427 renderer = SDL_CreateSoftwareRenderer(surface); 428 if (!renderer) { 429 fprintf(stderr, "SDL_CreateSoftwareRenderer failed: %s\n", SDL_GetError()); 430 SDL_DestroySurface(surface); 431 SDL_Quit(); 432 return 1; 433 } 434 435 if (ph_make_world(&world) < 0) { 436 SDL_DestroyRenderer(renderer); 437 SDL_DestroySurface(surface); 438 SDL_Quit(); 439 return 1; 440 } 441 442 if (ph_load_assets(&assets, renderer) < 0) { 443 ph_area_free(&world.area); 444 SDL_DestroyRenderer(renderer); 445 SDL_DestroySurface(surface); 446 SDL_Quit(); 447 return 1; 448 } 449 450 for (i = 0; i < 8; ++i) { 451 ph_render_frame(renderer, &assets, &world, (PhInput){ 0 }, 1.0f / 60.0f); 452 } 453 454 if (!SDL_SaveBMP(surface, PH_RENDER_SMOKE_PATH)) { 455 fprintf(stderr, "SDL_SaveBMP failed: %s\n", SDL_GetError()); 456 } 457 458 ph_destroy_assets(&assets); 459 ph_area_free(&world.area); 460 SDL_DestroyRenderer(renderer); 461 SDL_DestroySurface(surface); 462 SDL_Quit(); 463 return 0; 464 } 465 466 static int 467 ph_run_game(int frame_limit) 468 { 469 SDL_Window *window; 470 SDL_Renderer *renderer; 471 PhAssets assets; 472 PhWorld world; 473 Uint64 last_ticks; 474 int frame_count = 0; 475 int running = 1; 476 477 if (!SDL_Init(SDL_INIT_VIDEO)) { 478 fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); 479 return 1; 480 } 481 482 if (ph_make_world(&world) < 0) { 483 SDL_Quit(); 484 return 1; 485 } 486 487 if (ph_create_window_renderer(&window, &renderer) < 0) { 488 ph_area_free(&world.area); 489 SDL_Quit(); 490 return 1; 491 } 492 493 if (ph_load_assets(&assets, renderer) < 0) { 494 SDL_DestroyRenderer(renderer); 495 SDL_DestroyWindow(window); 496 ph_area_free(&world.area); 497 SDL_Quit(); 498 return 1; 499 } 500 501 SDL_SetRenderLogicalPresentation(renderer, 502 PH_VIEW_W, 503 PH_VIEW_H, 504 SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); 505 506 last_ticks = SDL_GetTicks(); 507 while (running) { 508 SDL_Event event; 509 Uint64 now_ticks; 510 float dt; 511 PhInput input; 512 513 while (SDL_PollEvent(&event)) { 514 if (event.type == SDL_EVENT_QUIT) { 515 running = 0; 516 } 517 } 518 519 now_ticks = SDL_GetTicks(); 520 dt = (float)(now_ticks - last_ticks) / 1000.0f; 521 if (dt > 0.05f) { 522 dt = 0.05f; 523 } 524 last_ticks = now_ticks; 525 526 input = ph_read_input(SDL_GetKeyboardState(NULL)); 527 ph_render_frame(renderer, &assets, &world, input, dt); 528 ++frame_count; 529 if (frame_limit > 0 && frame_count >= frame_limit) { 530 running = 0; 531 } 532 SDL_Delay(16); 533 } 534 535 ph_destroy_assets(&assets); 536 SDL_DestroyRenderer(renderer); 537 SDL_DestroyWindow(window); 538 ph_area_free(&world.area); 539 SDL_Quit(); 540 return 0; 541 } 542 #endif 543 544 int 545 main(int argc, char **argv) 546 { 547 if (argc > 1 && strcmp(argv[1], "--smoke-test") == 0) { 548 return ph_run_smoke_test(); 549 } 550 551 #if PH_USE_SDL 552 if (argc > 1 && strcmp(argv[1], "--render-smoke-test") == 0) { 553 return ph_run_render_smoke_test(); 554 } 555 return ph_run_game(0); 556 #else 557 fprintf(stderr, "SDL3 not available in this build; run `%s --smoke-test`\n", argv[0]); 558 return 1; 559 #endif 560 }