phantasia

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

game.c (19369B)


      1 #include "game/game.h"
      2 #include "engine/save.h"
      3 
      4 #include <stdio.h>
      5 #include <string.h>
      6 
      7 #include "config.h"
      8 
      9 #if PH_CONFIG_VERSION != PH_CONFIG_API_VERSION
     10 #error "config.h version does not match the engine; merge config.def.h"
     11 #endif
     12 #if PH_SIMULATION_HZ <= 0 || PH_MAX_CATCHUP_STEPS <= 0
     13 #error "simulation tick rate and catch-up limit must be positive"
     14 #endif
     15 
     16 static int
     17 ph_menu_direction(PhAction key)
     18 {
     19 	return key == PH_ACTION_UP ||
     20 		key == PH_ACTION_DOWN ||
     21 		key == PH_ACTION_LEFT ||
     22 		key == PH_ACTION_RIGHT;
     23 }
     24 
     25 static int
     26 ph_menu_back(PhAction key)
     27 {
     28 	return key == PH_ACTION_CANCEL || key == PH_ACTION_BACK;
     29 }
     30 
     31 void
     32 ph_options_open(PhOptionsUi *ui)
     33 {
     34 	if (!ui) return;
     35 	ui->selected = 0;
     36 	ui->mode = PH_OPTIONS_ROOT;
     37 }
     38 
     39 PhOptionsAction
     40 ph_options_input(PhOptionsUi *ui, PhAction key)
     41 {
     42 	int previous = key == PH_ACTION_UP ||
     43 		key == PH_ACTION_LEFT;
     44 	int next = key == PH_ACTION_DOWN ||
     45 		key == PH_ACTION_RIGHT;
     46 
     47 	if (!ui) return PH_OPTIONS_CLOSE;
     48 	if (ph_menu_back(key)) {
     49 		if (ui->mode == PH_OPTIONS_SOUND) {
     50 			ui->mode = PH_OPTIONS_ROOT;
     51 			ui->selected = 0;
     52 			return PH_OPTIONS_STAY;
     53 		}
     54 		return PH_OPTIONS_CLOSE;
     55 	}
     56 	if (previous || next) {
     57 		ui->selected = (ui->selected + 1) % 2;
     58 		return PH_OPTIONS_STAY;
     59 	}
     60 	if (key != PH_ACTION_ACCEPT) return PH_OPTIONS_STAY;
     61 	if (ui->mode == PH_OPTIONS_ROOT) {
     62 		if (ui->selected == 1) return PH_OPTIONS_EXIT;
     63 		ui->mode = PH_OPTIONS_SOUND;
     64 		ui->selected = 0;
     65 	} else if (ui->selected == 0) {
     66 		ui->music_enabled = !ui->music_enabled;
     67 	} else {
     68 		ui->effects_enabled = !ui->effects_enabled;
     69 	}
     70 	return PH_OPTIONS_STAY;
     71 }
     72 
     73 PhStartAction
     74 ph_start_input(PhStartUi *ui, PhAction key)
     75 {
     76 	int direction = key == PH_ACTION_UP ||
     77 		key == PH_ACTION_DOWN;
     78 
     79 	if (!ui) return PH_START_STAY;
     80 	if (direction) ui->selected = ui->can_continue ? !ui->selected : 1;
     81 	if (key != PH_ACTION_ACCEPT) return PH_START_STAY;
     82 	return ui->selected == 0 && ui->can_continue ?
     83 		PH_START_CONTINUE : PH_START_NEW;
     84 }
     85 
     86 int
     87 ph_inventory_step(const PhWorld *world, int selected, int direction)
     88 {
     89 	int i = selected < 0 ? (direction > 0 ? -1 : 0) : selected;
     90 	int step;
     91 
     92 	for (step = 0; step < world->content.item_count; ++step) {
     93 		i += direction;
     94 		if (i < 0) i = world->content.item_count - 1;
     95 		if (i >= world->content.item_count) i = 0;
     96 		if (world->inventory[i] > 0) return i;
     97 	}
     98 	return -1;
     99 }
    100 
    101 void
    102 ph_inventory_open(PhInventoryUi *ui, const PhWorld *world)
    103 {
    104 	memset(ui, 0, sizeof(*ui));
    105 	ui->selected = ph_inventory_step(world, -1, 1);
    106 }
    107 
    108 static void
    109 ph_inventory_reselect(PhInventoryUi *ui, const PhWorld *world)
    110 {
    111 	if (ui->selected >= 0 && world->inventory[ui->selected] > 0) return;
    112 	ui->selected = ph_inventory_step(world, ui->selected, 1);
    113 }
    114 
    115 static void
    116 ph_inventory_action(PhInventoryUi *ui, PhWorld *world, int action)
    117 {
    118 	const PhItemDef *item;
    119 	const char *name;
    120 	int result = -1;
    121 
    122 	if (ui->selected < 0) return;
    123 	item = &world->content.items[ui->selected];
    124 	name = item->name ? item->name : "Item";
    125 	if (action == PH_INVENTORY_EQUIP)
    126 		result = ph_world_equip_inventory_item(world, world->player_index, item->id);
    127 	else if (action == PH_INVENTORY_USE)
    128 		result = ph_world_use_inventory_item(world, world->player_index, item->id);
    129 	else if (action == PH_INVENTORY_DROP)
    130 		result = ph_world_drop_inventory_item(world, world->player_index, item->id);
    131 	else {
    132 		ui->mode = PH_INVENTORY_EXAMINE;
    133 		ui->status[0] = '\0';
    134 		return;
    135 	}
    136 
    137 	if (result < 0) {
    138 		snprintf(ui->status, sizeof(ui->status), "Cannot %s %s.",
    139 			action == PH_INVENTORY_EQUIP ? "equip" :
    140 			action == PH_INVENTORY_USE ? "use" : "drop", name);
    141 		return;
    142 	}
    143 	snprintf(ui->status, sizeof(ui->status), "%s %s.",
    144 		action == PH_INVENTORY_EQUIP ? "Equipped" :
    145 		action == PH_INVENTORY_USE ? "Used" : "Dropped", name);
    146 	ui->mode = PH_INVENTORY_BROWSE;
    147 	ph_inventory_reselect(ui, world);
    148 }
    149 
    150 int
    151 ph_inventory_action_enabled(const PhItemDef *item, int action)
    152 {
    153 	if (action == PH_INVENTORY_EQUIP) return item->equip_slot != PH_EQUIP_NONE;
    154 	if (action == PH_INVENTORY_USE) return item->use != PH_ITEM_USE_NONE;
    155 	return 1;
    156 }
    157 
    158 void
    159 ph_inventory_action_order(const PhItemDef *item, int actions[PH_INVENTORY_ACTION_COUNT])
    160 {
    161 	if (item->equip_slot != PH_EQUIP_NONE) {
    162 		actions[0] = PH_INVENTORY_EQUIP;
    163 		actions[1] = PH_INVENTORY_USE;
    164 	} else if (item->use != PH_ITEM_USE_NONE) {
    165 		actions[0] = PH_INVENTORY_USE;
    166 		actions[1] = PH_INVENTORY_EQUIP;
    167 	} else {
    168 		actions[0] = PH_INVENTORY_DROP;
    169 		actions[1] = PH_INVENTORY_EXAMINE_ACTION;
    170 	}
    171 	if (actions[0] < PH_INVENTORY_DROP) {
    172 		actions[2] = PH_INVENTORY_DROP;
    173 		actions[3] = PH_INVENTORY_EXAMINE_ACTION;
    174 	} else {
    175 		actions[2] = PH_INVENTORY_EQUIP;
    176 		actions[3] = PH_INVENTORY_USE;
    177 	}
    178 }
    179 
    180 static int
    181 ph_inventory_action_step(const PhItemDef *item, int selected, int direction)
    182 {
    183 	int actions[PH_INVENTORY_ACTION_COUNT];
    184 	int position = 0;
    185 	int step;
    186 
    187 	ph_inventory_action_order(item, actions);
    188 	while (position < PH_INVENTORY_ACTION_COUNT && actions[position] != selected) ++position;
    189 	for (step = 0; step < PH_INVENTORY_ACTION_COUNT; ++step) {
    190 		position = (position + (direction < 0 ?
    191 			PH_INVENTORY_ACTION_COUNT - 1 : 1)) % PH_INVENTORY_ACTION_COUNT;
    192 		if (ph_inventory_action_enabled(item, actions[position])) return actions[position];
    193 	}
    194 	return selected;
    195 }
    196 
    197 int
    198 ph_inventory_input(PhInventoryUi *ui, PhWorld *world, PhAction key)
    199 {
    200 	int previous = key == PH_ACTION_UP ||
    201 		key == PH_ACTION_LEFT;
    202 	int next = key == PH_ACTION_DOWN ||
    203 		key == PH_ACTION_RIGHT;
    204 
    205 	if (ph_menu_back(key)) {
    206 		if (ui->mode == PH_INVENTORY_EXAMINE)
    207 			ui->mode = PH_INVENTORY_ACTIONS;
    208 		else if (ui->mode == PH_INVENTORY_ACTIONS)
    209 			ui->mode = PH_INVENTORY_BROWSE;
    210 		else
    211 			return 1;
    212 		ui->status[0] = '\0';
    213 		return 0;
    214 	}
    215 	if (key == PH_ACTION_ACCEPT) {
    216 		if (ui->mode == PH_INVENTORY_BROWSE && ui->selected >= 0) {
    217 			int actions[PH_INVENTORY_ACTION_COUNT];
    218 
    219 			ph_inventory_action_order(&world->content.items[ui->selected], actions);
    220 			ui->mode = PH_INVENTORY_ACTIONS;
    221 			ui->option = actions[0];
    222 			ui->status[0] = '\0';
    223 		} else if (ui->mode == PH_INVENTORY_ACTIONS) {
    224 			ph_inventory_action(ui, world, ui->option);
    225 		} else if (ui->mode == PH_INVENTORY_EXAMINE) {
    226 			ui->mode = PH_INVENTORY_ACTIONS;
    227 		}
    228 		return 0;
    229 	}
    230 	if ((!previous && !next) || ui->mode == PH_INVENTORY_EXAMINE) return 0;
    231 	if (ui->mode == PH_INVENTORY_ACTIONS) {
    232 		ui->option = ph_inventory_action_step(&world->content.items[ui->selected],
    233 			ui->option, previous ? -1 : 1);
    234 	} else {
    235 		ui->selected = ph_inventory_step(world, ui->selected, previous ? -1 : 1);
    236 		ui->status[0] = '\0';
    237 	}
    238 	return 0;
    239 }
    240 
    241 void
    242 ph_spellbook_open(PhSpellbookUi *ui, const PhSpellBook *spells)
    243 {
    244 	memset(ui, 0, sizeof(*ui));
    245 	ui->selected = spells && spells->learned_count > 0 ? 0 : -1;
    246 }
    247 
    248 int
    249 ph_spellbook_action_enabled(const PhSpellDef *spell, int action)
    250 {
    251 	return action == PH_SPELLBOOK_EXAMINE_ACTION ||
    252 		(spell && spell->field_use != PH_SPELL_FIELD_NONE);
    253 }
    254 
    255 static int
    256 ph_spellbook_action_step(const PhSpellDef *spell, int selected, int direction)
    257 {
    258 	int action;
    259 	int step;
    260 
    261 	for (step = 1; step <= PH_SPELLBOOK_ACTION_COUNT; ++step) {
    262 		action = (selected + (direction < 0 ?
    263 			PH_SPELLBOOK_ACTION_COUNT - step : step)) % PH_SPELLBOOK_ACTION_COUNT;
    264 		if (ph_spellbook_action_enabled(spell, action)) return action;
    265 	}
    266 	return selected;
    267 }
    268 
    269 static void
    270 ph_spellbook_action(PhSpellbookUi *ui, PhWorld *world, PhSpellBook *spells)
    271 {
    272 	const PhSpellDef *spell;
    273 	int amount;
    274 
    275 	if (ui->selected < 0 || ui->selected >= spells->learned_count) return;
    276 	spell = ph_spellbook_def(spells, spells->learned[ui->selected]);
    277 	if (!spell) return;
    278 	if (ui->option == PH_SPELLBOOK_EXAMINE_ACTION) {
    279 		ui->mode = PH_SPELLBOOK_EXAMINE;
    280 		ui->status[0] = '\0';
    281 		return;
    282 	}
    283 	amount = ph_spellbook_use(spells, world, world->player_index, spell->id);
    284 	if (amount < 0)
    285 		snprintf(ui->status, sizeof(ui->status), "Cannot use %s.", spell->name);
    286 	else {
    287 		snprintf(ui->status, sizeof(ui->status), "Restored %d HP.", amount);
    288 		ui->spell_used = 1;
    289 	}
    290 	ui->mode = PH_SPELLBOOK_BROWSE;
    291 }
    292 
    293 int
    294 ph_spellbook_input(PhSpellbookUi *ui, PhWorld *world, PhSpellBook *spells,
    295 	PhAction key)
    296 {
    297 	int previous = key == PH_ACTION_UP ||
    298 		key == PH_ACTION_LEFT;
    299 	int next = key == PH_ACTION_DOWN ||
    300 		key == PH_ACTION_RIGHT;
    301 	const PhSpellDef *spell = ui->selected >= 0 &&
    302 		ui->selected < spells->learned_count ? ph_spellbook_def(spells,
    303 		spells->learned[ui->selected]) : NULL;
    304 
    305 	if (ph_menu_back(key)) {
    306 		if (ui->mode == PH_SPELLBOOK_EXAMINE)
    307 			ui->mode = PH_SPELLBOOK_ACTIONS;
    308 		else if (ui->mode == PH_SPELLBOOK_ACTIONS)
    309 			ui->mode = PH_SPELLBOOK_BROWSE;
    310 		else return 1;
    311 		ui->status[0] = '\0';
    312 		return 0;
    313 	}
    314 	if (key == PH_ACTION_ACCEPT) {
    315 		if (ui->mode == PH_SPELLBOOK_BROWSE && spell) {
    316 			ui->mode = PH_SPELLBOOK_ACTIONS;
    317 			ui->option = ph_spellbook_action_enabled(spell, PH_SPELLBOOK_USE) ?
    318 				PH_SPELLBOOK_USE : PH_SPELLBOOK_EXAMINE_ACTION;
    319 			ui->status[0] = '\0';
    320 		} else if (ui->mode == PH_SPELLBOOK_ACTIONS) {
    321 			ph_spellbook_action(ui, world, spells);
    322 		} else if (ui->mode == PH_SPELLBOOK_EXAMINE) {
    323 			ui->mode = PH_SPELLBOOK_ACTIONS;
    324 		}
    325 		return 0;
    326 	}
    327 	if ((!previous && !next) || ui->mode == PH_SPELLBOOK_EXAMINE) return 0;
    328 	if (ui->mode == PH_SPELLBOOK_ACTIONS) {
    329 		ui->option = ph_spellbook_action_step(spell, ui->option,
    330 			previous ? -1 : 1);
    331 	} else if (spells->learned_count > 0) {
    332 		ui->selected = (ui->selected + (previous ?
    333 			spells->learned_count - 1 : 1)) % spells->learned_count;
    334 		ui->status[0] = '\0';
    335 	}
    336 	return 0;
    337 }
    338 
    339 void
    340 ph_shop_open(PhShopUi *ui, int shop_id)
    341 {
    342 	memset(ui, 0, sizeof(*ui));
    343 	ui->shop_id = shop_id;
    344 }
    345 
    346 PhMenu
    347 ph_interaction_open(PhShopUi *shop_ui, int interaction_id)
    348 {
    349 	const PhInteractionDef *interaction = ph_game_interaction_def(interaction_id);
    350 
    351 	if (!interaction) return PH_MENU_NONE;
    352 	if (interaction->kind == PH_INTERACTION_SHOP &&
    353 			ph_game_shop_def(interaction->content_id)) {
    354 		ph_shop_open(shop_ui, interaction->content_id);
    355 		return PH_MENU_SHOP;
    356 	}
    357 	return PH_MENU_NONE;
    358 }
    359 
    360 int
    361 ph_shop_input(PhShopUi *ui, PhWorld *world, PhSpellBook *spells, PhAction key)
    362 {
    363 	const PhShopDef *shop = ph_game_shop_def(ui->shop_id);
    364 	int previous = key == PH_ACTION_UP ||
    365 		key == PH_ACTION_LEFT;
    366 	int next = key == PH_ACTION_DOWN ||
    367 		key == PH_ACTION_RIGHT;
    368 
    369 	if (!shop) return 1;
    370 	if (ph_menu_back(key)) {
    371 		if (ui->mode == PH_SHOP_ROOT) return 1;
    372 		ui->mode = PH_SHOP_ROOT;
    373 		ui->selected = 0;
    374 		ui->status[0] = '\0';
    375 		return 0;
    376 	}
    377 	if (ui->mode == PH_SHOP_TALK) {
    378 		if (key == PH_ACTION_ACCEPT) ui->mode = PH_SHOP_ROOT;
    379 		return 0;
    380 	}
    381 	if (previous || next) {
    382 		int count = ui->mode == PH_SHOP_ROOT ?
    383 			(shop->kind == PH_SHOP_ITEMS ? 3 : 2) :
    384 			ui->mode == PH_SHOP_BUY ? shop->stock_count : world->content.item_count;
    385 
    386 		if (ui->mode == PH_SHOP_SELL) {
    387 			ui->selected = ph_inventory_step(world, ui->selected,
    388 				previous ? -1 : 1);
    389 		} else if (count > 0) {
    390 			ui->selected = (ui->selected + (previous ? count - 1 : 1)) % count;
    391 		}
    392 		ui->status[0] = '\0';
    393 		return 0;
    394 	}
    395 	if (key != PH_ACTION_ACCEPT) return 0;
    396 	if (ui->mode == PH_SHOP_ROOT) {
    397 		ui->mode = shop->kind == PH_SHOP_ITEMS ?
    398 			(PhShopMode)(ui->selected + 1) :
    399 			ui->selected == 0 ? PH_SHOP_BUY : PH_SHOP_TALK;
    400 		ui->selected = ui->mode == PH_SHOP_SELL ?
    401 			ph_inventory_step(world, -1, 1) : 0;
    402 		return 0;
    403 	}
    404 	if (ui->mode == PH_SHOP_BUY && ui->selected < shop->stock_count) {
    405 		if (shop->kind == PH_SHOP_SPELLS) {
    406 			const PhSpellDef *spell = ph_spellbook_def(spells,
    407 				shop->stock[ui->selected]);
    408 
    409 			if (!spell) return 0;
    410 			if (ph_spellbook_has(spells, spell->id))
    411 				snprintf(ui->status, sizeof(ui->status), "Already learned.");
    412 			else if (ph_spellbook_buy(spells, world, world->player_index,
    413 					spell->id) < 0)
    414 				snprintf(ui->status, sizeof(ui->status), "Not enough gold.");
    415 			else
    416 				snprintf(ui->status, sizeof(ui->status), "Learned %s.", spell->name);
    417 		} else {
    418 			const PhItemDef *item = ph_world_item_def(world,
    419 				shop->stock[ui->selected]);
    420 
    421 			if (!item || ph_world_buy_item(world, world->player_index, item->id) < 0)
    422 				snprintf(ui->status, sizeof(ui->status), "Not enough gold.");
    423 			else
    424 				snprintf(ui->status, sizeof(ui->status), "Bought %s.", item->name);
    425 		}
    426 	} else if (ui->mode == PH_SHOP_SELL && ui->selected >= 0) {
    427 		const PhItemDef *item = &world->content.items[ui->selected];
    428 
    429 		if (ph_world_sell_item(world, world->player_index, item->id) < 0)
    430 			snprintf(ui->status, sizeof(ui->status), "Cannot sell that.");
    431 		else {
    432 			snprintf(ui->status, sizeof(ui->status), "Sold %s.", item->name);
    433 			if (world->inventory[ui->selected] <= 0)
    434 				ui->selected = ph_inventory_step(world, ui->selected, 1);
    435 		}
    436 	}
    437 	return 0;
    438 }
    439 
    440 int
    441 ph_game_input(PhGameContext *context, PhAction key)
    442 {
    443 	PhMenu menu = context->menu;
    444 	PhGameMode mode = *context->mode;
    445 	PhBattlePhase phase = context->battle->phase;
    446 	int result = 0;
    447 
    448 	if (context->menu == PH_MENU_START) {
    449 		PhStartAction action = ph_start_input(context->start, key);
    450 
    451 		if (action == PH_START_CONTINUE) context->menu = PH_MENU_NONE;
    452 		else if (action == PH_START_NEW) {
    453 			context->time_reset = 1;
    454 			return PH_GAME_REQUEST_NEW;
    455 		}
    456 	} else if (context->menu == PH_MENU_MAIN) {
    457 		PhOptionsAction action;
    458 
    459 		if (ph_menu_direction(key)) result |= PH_GAME_MENU_SOUND;
    460 		action = ph_options_input(context->options, key);
    461 		if (action == PH_OPTIONS_CLOSE) context->menu = PH_MENU_NONE;
    462 		else if (action == PH_OPTIONS_EXIT) result |= PH_GAME_REQUEST_EXIT;
    463 	} else if (context->menu == PH_MENU_NONE && key == PH_ACTION_CANCEL &&
    464 			(*context->mode == PH_GAME_WORLD || *context->mode == PH_GAME_VICTORY ||
    465 			*context->mode == PH_GAME_OVER || (*context->mode == PH_GAME_BATTLE &&
    466 			context->battle->phase != PH_BATTLE_MAGIC &&
    467 			context->battle->phase != PH_BATTLE_TARGET))) {
    468 		ph_options_open(context->options);
    469 		context->menu = PH_MENU_MAIN;
    470 	} else if (context->menu == PH_MENU_NONE && *context->mode == PH_GAME_BATTLE &&
    471 			(context->battle->phase == PH_BATTLE_COMMAND ||
    472 			context->battle->phase == PH_BATTLE_MAGIC ||
    473 			context->battle->phase == PH_BATTLE_TARGET)) {
    474 		int previous = key == PH_ACTION_UP ||
    475 			key == PH_ACTION_LEFT;
    476 		int next = key == PH_ACTION_DOWN ||
    477 			key == PH_ACTION_RIGHT;
    478 
    479 		if (context->battle->phase == PH_BATTLE_TARGET) {
    480 			if (previous || next) {
    481 				if (ph_battle_target_step(context->battle, context->world,
    482 						previous ? -1 : 1) >= 0) result |= PH_GAME_MENU_SOUND;
    483 			}
    484 			else if (key == PH_ACTION_ACCEPT)
    485 				ph_battle_confirm_target(context->battle, context->world);
    486 			else if (ph_menu_back(key))
    487 				ph_battle_cancel(context->battle);
    488 		} else if (context->battle->phase == PH_BATTLE_MAGIC) {
    489 			if ((previous || next) && context->battle->spells.learned_count > 0) {
    490 				if (ph_battle_spell_step(context->battle, previous ? -1 : 1) >= 0)
    491 					result |= PH_GAME_MENU_SOUND;
    492 			}
    493 			else if (key == PH_ACTION_ACCEPT)
    494 				ph_battle_cast(context->battle, context->world);
    495 			else if (ph_menu_back(key))
    496 				ph_battle_cancel(context->battle);
    497 		} else if (previous || next) {
    498 			context->battle->choice.command = (PhBattleCommand)((context->battle->choice.command + (previous ?
    499 				PH_BATTLE_COMMAND_COUNT - 1 : 1)) % PH_BATTLE_COMMAND_COUNT);
    500 			result |= PH_GAME_MENU_SOUND;
    501 		} else if (key == PH_ACTION_ACCEPT) {
    502 			ph_battle_select(context->battle, context->world);
    503 		}
    504 	} else if (context->menu == PH_MENU_NONE && *context->mode == PH_GAME_VICTORY &&
    505 			key == PH_ACTION_ACCEPT) {
    506 		*context->mode = PH_GAME_WORLD;
    507 		context->menu = PH_MENU_NONE;
    508 	} else if (*context->mode == PH_GAME_WORLD && context->menu == PH_MENU_CHARACTER &&
    509 			ph_menu_back(key)) {
    510 		context->menu = PH_MENU_NONE;
    511 	} else if (*context->mode == PH_GAME_WORLD &&
    512 			(context->menu == PH_MENU_NONE || context->menu == PH_MENU_CHARACTER) &&
    513 			key == PH_ACTION_CHARACTER) {
    514 		context->menu = context->menu == PH_MENU_CHARACTER ? PH_MENU_NONE : PH_MENU_CHARACTER;
    515 	} else if (*context->mode == PH_GAME_WORLD &&
    516 			(context->menu == PH_MENU_NONE || context->menu == PH_MENU_INVENTORY) &&
    517 			key == PH_ACTION_INVENTORY) {
    518 		if (context->menu == PH_MENU_INVENTORY) context->menu = PH_MENU_NONE;
    519 		else {
    520 			context->menu = PH_MENU_INVENTORY;
    521 			ph_inventory_open(context->inventory, context->world);
    522 		}
    523 	} else if (*context->mode == PH_GAME_WORLD && context->menu == PH_MENU_INVENTORY) {
    524 		if (ph_menu_direction(key) &&
    525 				context->inventory->mode != PH_INVENTORY_EXAMINE)
    526 			result |= PH_GAME_MENU_SOUND;
    527 		if (ph_inventory_input(context->inventory, context->world, key))
    528 			context->menu = PH_MENU_NONE;
    529 	} else if (*context->mode == PH_GAME_WORLD &&
    530 			(context->menu == PH_MENU_NONE || context->menu == PH_MENU_SPELLBOOK) &&
    531 			key == PH_ACTION_SPELLBOOK) {
    532 		if (context->menu == PH_MENU_SPELLBOOK) context->menu = PH_MENU_NONE;
    533 		else {
    534 			context->menu = PH_MENU_SPELLBOOK;
    535 			ph_spellbook_open(context->spellbook, context->spells);
    536 		}
    537 	} else if (*context->mode == PH_GAME_WORLD && context->menu == PH_MENU_SPELLBOOK) {
    538 		if (ph_menu_direction(key) &&
    539 				context->spellbook->mode != PH_SPELLBOOK_EXAMINE)
    540 			result |= PH_GAME_MENU_SOUND;
    541 		if (ph_spellbook_input(context->spellbook, context->world, context->spells,
    542 				key)) context->menu = PH_MENU_NONE;
    543 	} else if (*context->mode == PH_GAME_WORLD && context->menu == PH_MENU_SHOP) {
    544 		if (ph_menu_direction(key) &&
    545 				context->shop->mode != PH_SHOP_TALK) result |= PH_GAME_MENU_SOUND;
    546 		if (ph_shop_input(context->shop, context->world, context->spells, key))
    547 			context->menu = PH_MENU_NONE;
    548 	}
    549 	if (menu != context->menu || mode != *context->mode || phase != context->battle->phase)
    550 		context->time_reset = 1;
    551 	return result;
    552 }
    553 
    554 int
    555 ph_game_update(PhGameContext *context, PhInput input, float dt, const char *save_path)
    556 {
    557 	PhWorld *world = context->world;
    558 	PhGameMode *game = context->mode;
    559 	PhBattleResult result = PH_BATTLE_CONTINUE;
    560 	int old_area = world->area_id;
    561 	int save_requested = 0;
    562 
    563 	if (*game == PH_GAME_WORLD && context->menu == PH_MENU_NONE) {
    564 		ph_world_tick(world, input, dt);
    565 		if (ph_world_update_portal(world, ph_game_world.areas) < 0)
    566 			fprintf(stderr, "failed to enter area\n");
    567 		if (world->encounter_index >= 0) {
    568 			if (save_path && ph_save_write(save_path, world, context->spells) < 0)
    569 				fprintf(stderr, "failed to save pre-combat checkpoint\n");
    570 			ph_battle_begin(context->battle, world, world->encounter_index,
    571 				*context->spells);
    572 			*game = PH_GAME_TRANSITION;
    573 		}
    574 	}
    575 	if ((*game == PH_GAME_TRANSITION || *game == PH_GAME_BATTLE) &&
    576 			context->menu == PH_MENU_NONE)
    577 		result = ph_battle_update(context->battle, world, dt,
    578 			*game == PH_GAME_TRANSITION);
    579 	if (result == PH_BATTLE_READY) *game = PH_GAME_BATTLE;
    580 	else if (result == PH_BATTLE_VICTORY) {
    581 		*game = PH_GAME_VICTORY;
    582 		save_requested = 1;
    583 	}
    584 	else if (result == PH_BATTLE_DEFEAT) *game = PH_GAME_OVER;
    585 	return save_requested || world->area_id != old_area;
    586 }
    587 
    588 void
    589 ph_game_interaction(PhGameContext *context)
    590 {
    591 	if (*context->mode == PH_GAME_WORLD && context->menu == PH_MENU_NONE &&
    592 			context->world->interaction_id > 0) {
    593 		context->menu = ph_interaction_open(context->shop, context->world->interaction_id);
    594 		context->world->interaction_id = 0;
    595 	}
    596 }
    597 
    598 int
    599 ph_game_steps(PhGameContext *context, uint64_t elapsed_ns)
    600 {
    601 	if (context->time_reset || context->menu != PH_MENU_NONE ||
    602 			*context->mode == PH_GAME_VICTORY || *context->mode == PH_GAME_OVER) {
    603 		context->clock.credit = 0;
    604 		context->time_reset = 0;
    605 		return 0;
    606 	}
    607 	return ph_clock_steps(&context->clock, elapsed_ns,
    608 		PH_SIMULATION_HZ, PH_MAX_CATCHUP_STEPS);
    609 }