phantasia

Phantasia - 2D SDL3 RPG prototype.
git clone git://git.beep.wimdupont.com/phantasia.git
Log | Files | Refs | README | LICENSE

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, &notice_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 }