phantasia

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

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