phantasia

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

presentation.c (41870B)


      1 #include "game/presentation.h"
      2 
      3 #include <stdio.h>
      4 #include <string.h>
      5 
      6 #include "config.h"
      7 
      8 #define LEN(a) (sizeof(a) / sizeof(0[a]))
      9 
     10 #if PH_USE_SDL
     11 enum { PH_UI_FONT_W = 8 };
     12 
     13 static int
     14 ph_menu_back(SDL_Scancode key)
     15 {
     16 	return key == PH_KEY_MENU_CANCEL || key == PH_KEY_MENU_BACK;
     17 }
     18 
     19 static float
     20 ph_ui_right(float inset)
     21 {
     22 	return (float)PH_VIEW_W - inset;
     23 }
     24 
     25 static float
     26 ph_ui_bottom(float inset)
     27 {
     28 	return (float)PH_VIEW_H - inset;
     29 }
     30 
     31 static float
     32 ph_ui_center_text(const char *text)
     33 {
     34 	return ((float)PH_VIEW_W - (float)strlen(text) * PH_UI_FONT_W) * 0.5f;
     35 }
     36 
     37 static float
     38 ph_draw_wrapped_text(SDL_Renderer *renderer, float x, float y,
     39 	float width, float line_height, const char *text)
     40 {
     41 	char line[128];
     42 	size_t columns = width > PH_UI_FONT_W ? (size_t)(width / PH_UI_FONT_W) : 1;
     43 
     44 	while (text && *text) {
     45 		size_t remaining;
     46 		size_t take;
     47 		size_t i;
     48 
     49 		while (*text == ' ') ++text;
     50 		remaining = strlen(text);
     51 		take = remaining < columns ? remaining : columns;
     52 		if (take < remaining)
     53 			for (i = take; i > 0; --i)
     54 				if (text[i] == ' ') {
     55 					take = i;
     56 					break;
     57 				}
     58 		if (take >= sizeof(line)) take = sizeof(line) - 1;
     59 		memcpy(line, text, take);
     60 		line[take] = '\0';
     61 		SDL_RenderDebugText(renderer, x, y, line);
     62 		text += take;
     63 		y += line_height;
     64 	}
     65 	return y;
     66 }
     67 #endif
     68 
     69 #if PH_USE_SDL
     70 static void
     71 ph_append_bonus(char *text, size_t size, size_t *used, int value, const char *label)
     72 {
     73 	int written;
     74 
     75 	if (!value || *used >= size) return;
     76 	written = snprintf(text + *used, size - *used, "%s%+d %s",
     77 		*used ? "  " : "", value, label);
     78 	if (written > 0) *used += (size_t)written;
     79 }
     80 
     81 static void
     82 ph_format_bonuses(char *text, size_t size, PhStats bonuses)
     83 {
     84 	size_t used = 0;
     85 
     86 	text[0] = '\0';
     87 	ph_append_bonus(text, size, &used, bonuses.max_hp, "HP");
     88 	ph_append_bonus(text, size, &used, bonuses.max_mp, "MP");
     89 	ph_append_bonus(text, size, &used, bonuses.strength, "STR");
     90 	ph_append_bonus(text, size, &used, bonuses.defense, "DEF");
     91 	ph_append_bonus(text, size, &used, bonuses.magic, "MAG");
     92 	ph_append_bonus(text, size, &used, bonuses.magic_defense, "MDEF");
     93 	ph_append_bonus(text, size, &used, bonuses.agility, "AGI");
     94 	ph_append_bonus(text, size, &used, bonuses.luck, "LCK");
     95 	ph_append_bonus(text, size, &used, bonuses.evasion, "EVA");
     96 	ph_append_bonus(text, size, &used, bonuses.accuracy, "ACC");
     97 }
     98 
     99 static void
    100 ph_draw_menu(SDL_Renderer *renderer, const char *title, SDL_Scancode key)
    101 {
    102 	SDL_FRect panel = { 12.0f, 12.0f, PH_VIEW_W - 24.0f, PH_VIEW_H - 24.0f };
    103 	char text[32];
    104 	float x;
    105 
    106 	SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR);
    107 	SDL_RenderFillRect(renderer, &panel);
    108 	SDL_SetRenderDrawColor(renderer, PH_MENU_BORDER_COLOR);
    109 	SDL_RenderRect(renderer, &panel);
    110 	SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
    111 	SDL_RenderDebugText(renderer, 24.0f, 22.0f, title);
    112 	snprintf(text, sizeof(text), "CLOSE: %s / %s", SDL_GetScancodeName(key),
    113 		SDL_GetScancodeName(PH_KEY_MENU_BACK));
    114 	x = ph_ui_right(20.0f) - (float)strlen(text) * PH_UI_FONT_W;
    115 	if (x < 24.0f) x = 24.0f;
    116 	SDL_RenderDebugText(renderer, x, (float)(PH_VIEW_H - 22), text);
    117 }
    118 
    119 void
    120 ph_options_open(PhOptionsUi *ui)
    121 {
    122 	if (!ui) return;
    123 	ui->selected = 0;
    124 	ui->mode = PH_OPTIONS_ROOT;
    125 }
    126 
    127 PhOptionsAction
    128 ph_options_key(PhOptionsUi *ui, SDL_Scancode key)
    129 {
    130 	int previous = key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 ||
    131 		key == PH_KEY_MENU_LEFT_1 || key == PH_KEY_MENU_LEFT_2;
    132 	int next = key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2 ||
    133 		key == PH_KEY_MENU_RIGHT_1 || key == PH_KEY_MENU_RIGHT_2;
    134 
    135 	if (!ui) return PH_OPTIONS_CLOSE;
    136 	if (ph_menu_back(key)) {
    137 		if (ui->mode == PH_OPTIONS_SOUND) {
    138 			ui->mode = PH_OPTIONS_ROOT;
    139 			ui->selected = 0;
    140 			return PH_OPTIONS_STAY;
    141 		}
    142 		return PH_OPTIONS_CLOSE;
    143 	}
    144 	if (previous || next) {
    145 		ui->selected = (ui->selected + 1) % 2;
    146 		return PH_OPTIONS_STAY;
    147 	}
    148 	if (key != PH_KEY_MENU_ACCEPT) return PH_OPTIONS_STAY;
    149 	if (ui->mode == PH_OPTIONS_ROOT) {
    150 		if (ui->selected == 1) return PH_OPTIONS_EXIT;
    151 		ui->mode = PH_OPTIONS_SOUND;
    152 		ui->selected = 0;
    153 	} else if (ui->selected == 0) {
    154 		ui->music_enabled = !ui->music_enabled;
    155 	} else {
    156 		ui->effects_enabled = !ui->effects_enabled;
    157 	}
    158 	return PH_OPTIONS_STAY;
    159 }
    160 
    161 static void
    162 ph_draw_options(SDL_Renderer *renderer, const PhOptionsUi *ui)
    163 {
    164 	static const char *root_options[] = { "SOUND", "EXIT GAME" };
    165 	SDL_FRect panel = { PH_VIEW_W * 0.5f - 84.0f,
    166 		PH_VIEW_H * 0.5f - 58.0f, 168.0f, 116.0f };
    167 	char text[48];
    168 	int i;
    169 
    170 	if (!ui) return;
    171 	SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR);
    172 	SDL_RenderFillRect(renderer, &panel);
    173 	SDL_SetRenderDrawColor(renderer, PH_MENU_BORDER_COLOR);
    174 	SDL_RenderRect(renderer, &panel);
    175 	SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
    176 	SDL_RenderDebugText(renderer, panel.x + 12.0f, panel.y + 10.0f,
    177 		ui->mode == PH_OPTIONS_SOUND ? "SOUND" : "OPTIONS");
    178 	for (i = 0; i < 2; ++i) {
    179 		if (ui->mode == PH_OPTIONS_SOUND)
    180 			snprintf(text, sizeof(text), "%c %-8s %s",
    181 				ui->selected == i ? '>' : ' ', i == 0 ? "MUSIC" : "EFFECTS",
    182 				(i == 0 ? ui->music_enabled : ui->effects_enabled) ? "ON" : "OFF");
    183 		else snprintf(text, sizeof(text), "%c %s",
    184 			ui->selected == i ? '>' : ' ', root_options[i]);
    185 		SDL_RenderDebugText(renderer, panel.x + 20.0f,
    186 			panel.y + 38.0f + (float)i * 20.0f, text);
    187 	}
    188 	SDL_RenderDebugText(renderer, panel.x + 12.0f, panel.y + 94.0f,
    189 		"ESC/BKSP BACK");
    190 }
    191 
    192 PhStartAction
    193 ph_start_key(PhStartUi *ui, SDL_Scancode key)
    194 {
    195 	int direction = key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 ||
    196 		key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2;
    197 
    198 	if (!ui) return PH_START_STAY;
    199 	if (direction) ui->selected = ui->can_continue ? !ui->selected : 1;
    200 	if (key != PH_KEY_MENU_ACCEPT) return PH_START_STAY;
    201 	return ui->selected == 0 && ui->can_continue ?
    202 		PH_START_CONTINUE : PH_START_NEW;
    203 }
    204 
    205 static void
    206 ph_draw_start(SDL_Renderer *renderer, const PhStartUi *ui)
    207 {
    208 	SDL_FRect panel = { PH_VIEW_W * 0.5f - 82.0f,
    209 		PH_VIEW_H * 0.5f - 52.0f, 164.0f, 104.0f };
    210 	char text[32];
    211 	int i;
    212 
    213 	if (!ui) return;
    214 	SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR);
    215 	SDL_RenderFillRect(renderer, &panel);
    216 	SDL_SetRenderDrawColor(renderer, PH_MENU_BORDER_COLOR);
    217 	SDL_RenderRect(renderer, &panel);
    218 	SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
    219 	SDL_RenderDebugText(renderer, panel.x + 42.0f, panel.y + 12.0f, "PHANTASIA");
    220 	for (i = 0; i < 2; ++i) {
    221 		snprintf(text, sizeof(text), "%c %s", ui->selected == i ? '>' : ' ',
    222 			i == 0 ? "CONTINUE" : "NEW GAME");
    223 		if (i == 0 && !ui->can_continue)
    224 			SDL_SetRenderDrawColor(renderer, PH_MENU_DISABLED_COLOR);
    225 		else SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
    226 		SDL_RenderDebugText(renderer, panel.x + 36.0f,
    227 			panel.y + 42.0f + (float)i * 18.0f, text);
    228 	}
    229 }
    230 
    231 static void
    232 ph_draw_character_sheet(SDL_Renderer *renderer, const PhRenderAssets *assets,
    233 	const PhWorld *world)
    234 {
    235 	static const char *slot_names[PH_EQUIP_COUNT] = {
    236 		[PH_EQUIP_WEAPON] = "WEAPON",
    237 		[PH_EQUIP_ARMOR] = "ARMOR",
    238 		[PH_EQUIP_CHARM] = "CHARM",
    239 	};
    240 	const PhEntity *player = ph_world_player(world);
    241 	const PhEntityDef *def;
    242 	PhStats stats;
    243 	SDL_Texture *texture;
    244 	SDL_FRect avatar = { 32.0f, 46.0f, PH_TILE_SIZE * 3.0f,
    245 		PH_TILE_SIZE * 3.0f };
    246 	char text[128];
    247 	int slot;
    248 
    249 	if (!player || !(def = ph_world_entity_def(world, player->type_id))) return;
    250 	stats = ph_world_entity_stats(world, player);
    251 	ph_draw_menu(renderer, "CHARACTER", PH_KEY_CHARACTER);
    252 	SDL_RenderDebugText(renderer, 104.0f, 38.0f, def->name);
    253 
    254 	texture = ph_render_asset(assets, def->asset_id);
    255 	if (texture)
    256 		ph_render_sprite(renderer, texture,
    257 			def->sprite_tiles[PH_SPRITE_DOWN][0][0],
    258 			def->sprite_tiles[PH_SPRITE_DOWN][0][1],
    259 			avatar, SDL_FLIP_NONE);
    260 
    261 	snprintf(text, sizeof(text), "HP      %d / %d", player->vitals.hp, stats.max_hp);
    262 	SDL_RenderDebugText(renderer, 104.0f, 54.0f, text);
    263 	snprintf(text, sizeof(text), "MP      %d / %d", player->vitals.mp, stats.max_mp);
    264 	SDL_RenderDebugText(renderer, 104.0f, 66.0f, text);
    265 	snprintf(text, sizeof(text), "STR     %d", stats.strength);
    266 	SDL_RenderDebugText(renderer, 104.0f, 78.0f, text);
    267 	snprintf(text, sizeof(text), "DEF     %d", stats.defense);
    268 	SDL_RenderDebugText(renderer, 104.0f, 90.0f, text);
    269 	snprintf(text, sizeof(text), "MAG  %d", stats.magic);
    270 	SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 54.0f, text);
    271 	snprintf(text, sizeof(text), "MDEF %d", stats.magic_defense);
    272 	SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 66.0f, text);
    273 	snprintf(text, sizeof(text), "AGI  %d", stats.agility);
    274 	SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 78.0f, text);
    275 	snprintf(text, sizeof(text), "LCK  %d", stats.luck);
    276 	SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 90.0f, text);
    277 	snprintf(text, sizeof(text), "EVA  %d", stats.evasion);
    278 	SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 102.0f, text);
    279 	snprintf(text, sizeof(text), "ACC  %d", stats.accuracy);
    280 	SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 114.0f, text);
    281 	SDL_RenderDebugText(renderer, 24.0f, 128.0f, "EQUIPPED");
    282 
    283 	for (slot = PH_EQUIP_WEAPON; slot < PH_EQUIP_COUNT; ++slot) {
    284 		const PhItemDef *item = ph_world_item_def(world, player->equipment[slot]);
    285 		SDL_Texture *icon_texture;
    286 		float y = 144.0f + (float)(slot - PH_EQUIP_WEAPON) * 25.0f;
    287 
    288 		snprintf(text, sizeof(text), "%s  %s", slot_names[slot], item ? item->name : "-");
    289 		SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
    290 		SDL_RenderDebugText(renderer, 48.0f, y, text);
    291 		if (!item) continue;
    292 		icon_texture = ph_render_asset(assets, item->asset_id);
    293 		if (icon_texture) {
    294 			SDL_FRect icon = { 24.0f, y - 4.0f, PH_TILE_SIZE, PH_TILE_SIZE };
    295 			ph_render_sprite(renderer, icon_texture,
    296 				item->sprite_tile_x, item->sprite_tile_y, icon, SDL_FLIP_NONE);
    297 		}
    298 		ph_format_bonuses(text, sizeof(text), item->bonuses);
    299 		SDL_SetRenderDrawColor(renderer, PH_MENU_BONUS_COLOR);
    300 		SDL_RenderDebugText(renderer, 48.0f, y + 10.0f, text);
    301 	}
    302 }
    303 
    304 int
    305 ph_inventory_step(const PhWorld *world, int selected, int direction)
    306 {
    307 	int i = selected < 0 ? (direction > 0 ? -1 : 0) : selected;
    308 	int step;
    309 
    310 	for (step = 0; step < world->content.item_count; ++step) {
    311 		i += direction;
    312 		if (i < 0) i = world->content.item_count - 1;
    313 		if (i >= world->content.item_count) i = 0;
    314 		if (world->inventory[i] > 0) return i;
    315 	}
    316 	return -1;
    317 }
    318 
    319 void
    320 ph_inventory_open(PhInventoryUi *ui, const PhWorld *world)
    321 {
    322 	memset(ui, 0, sizeof(*ui));
    323 	ui->selected = ph_inventory_step(world, -1, 1);
    324 }
    325 
    326 static void
    327 ph_inventory_reselect(PhInventoryUi *ui, const PhWorld *world)
    328 {
    329 	if (ui->selected >= 0 && world->inventory[ui->selected] > 0) return;
    330 	ui->selected = ph_inventory_step(world, ui->selected, 1);
    331 }
    332 
    333 static void
    334 ph_inventory_action(PhInventoryUi *ui, PhWorld *world, int action)
    335 {
    336 	const PhItemDef *item;
    337 	const char *name;
    338 	int result = -1;
    339 
    340 	if (ui->selected < 0) return;
    341 	item = &world->content.items[ui->selected];
    342 	name = item->name ? item->name : "Item";
    343 	if (action == PH_INVENTORY_EQUIP)
    344 		result = ph_world_equip_inventory_item(world, world->player_index, item->id);
    345 	else if (action == PH_INVENTORY_USE)
    346 		result = ph_world_use_inventory_item(world, world->player_index, item->id);
    347 	else if (action == PH_INVENTORY_DROP)
    348 		result = ph_world_drop_inventory_item(world, world->player_index, item->id);
    349 	else {
    350 		ui->mode = PH_INVENTORY_EXAMINE;
    351 		ui->status[0] = '\0';
    352 		return;
    353 	}
    354 
    355 	if (result < 0) {
    356 		snprintf(ui->status, sizeof(ui->status), "Cannot %s %s.",
    357 			action == PH_INVENTORY_EQUIP ? "equip" :
    358 			action == PH_INVENTORY_USE ? "use" : "drop", name);
    359 		return;
    360 	}
    361 	snprintf(ui->status, sizeof(ui->status), "%s %s.",
    362 		action == PH_INVENTORY_EQUIP ? "Equipped" :
    363 		action == PH_INVENTORY_USE ? "Used" : "Dropped", name);
    364 	ui->mode = PH_INVENTORY_BROWSE;
    365 	ph_inventory_reselect(ui, world);
    366 }
    367 
    368 static int
    369 ph_inventory_action_enabled(const PhItemDef *item, int action)
    370 {
    371 	if (action == PH_INVENTORY_EQUIP) return item->equip_slot != PH_EQUIP_NONE;
    372 	if (action == PH_INVENTORY_USE) return item->use != PH_ITEM_USE_NONE;
    373 	return 1;
    374 }
    375 
    376 static void
    377 ph_inventory_action_order(const PhItemDef *item, int actions[PH_INVENTORY_ACTION_COUNT])
    378 {
    379 	if (item->equip_slot != PH_EQUIP_NONE) {
    380 		actions[0] = PH_INVENTORY_EQUIP;
    381 		actions[1] = PH_INVENTORY_USE;
    382 	} else if (item->use != PH_ITEM_USE_NONE) {
    383 		actions[0] = PH_INVENTORY_USE;
    384 		actions[1] = PH_INVENTORY_EQUIP;
    385 	} else {
    386 		actions[0] = PH_INVENTORY_DROP;
    387 		actions[1] = PH_INVENTORY_EXAMINE_ACTION;
    388 	}
    389 	if (actions[0] < PH_INVENTORY_DROP) {
    390 		actions[2] = PH_INVENTORY_DROP;
    391 		actions[3] = PH_INVENTORY_EXAMINE_ACTION;
    392 	} else {
    393 		actions[2] = PH_INVENTORY_EQUIP;
    394 		actions[3] = PH_INVENTORY_USE;
    395 	}
    396 }
    397 
    398 static int
    399 ph_inventory_action_step(const PhItemDef *item, int selected, int direction)
    400 {
    401 	int actions[PH_INVENTORY_ACTION_COUNT];
    402 	int position = 0;
    403 	int step;
    404 
    405 	ph_inventory_action_order(item, actions);
    406 	while (position < PH_INVENTORY_ACTION_COUNT && actions[position] != selected) ++position;
    407 	for (step = 0; step < PH_INVENTORY_ACTION_COUNT; ++step) {
    408 		position = (position + (direction < 0 ?
    409 			PH_INVENTORY_ACTION_COUNT - 1 : 1)) % PH_INVENTORY_ACTION_COUNT;
    410 		if (ph_inventory_action_enabled(item, actions[position])) return actions[position];
    411 	}
    412 	return selected;
    413 }
    414 
    415 static void
    416 ph_draw_inventory_popup(SDL_Renderer *renderer, const PhWorld *world,
    417 	const PhInventoryUi *ui)
    418 {
    419 	static const char *action_names[] = { "EQUIP", "USE", "DROP", "EXAMINE" };
    420 	const PhItemDef *item = &world->content.items[ui->selected];
    421 	int actions[PH_INVENTORY_ACTION_COUNT];
    422 	SDL_FRect popup = ui->mode == PH_INVENTORY_EXAMINE ?
    423 		(SDL_FRect){ 40.0f, 48.0f, PH_VIEW_W - 64.0f, 124.0f } :
    424 		(SDL_FRect){ PH_VIEW_W - 164.0f, 48.0f, 140.0f, 124.0f };
    425 	char text[128];
    426 	float x = ui->mode == PH_INVENTORY_EXAMINE ? 52.0f : ph_ui_right(152.0f);
    427 	int i;
    428 
    429 	SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR);
    430 	SDL_RenderFillRect(renderer, &popup);
    431 	SDL_SetRenderDrawColor(renderer, PH_MENU_BORDER_COLOR);
    432 	SDL_RenderRect(renderer, &popup);
    433 	SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
    434 	if (ui->mode == PH_INVENTORY_EXAMINE) {
    435 		SDL_RenderDebugText(renderer, x, 58.0f,
    436 			item->name ? item->name : "ITEM");
    437 		SDL_RenderDebugText(renderer, x, 76.0f,
    438 			item->description ? item->description : "No description.");
    439 		snprintf(text, sizeof(text), "SELL %d G", item->sell_value);
    440 		SDL_RenderDebugText(renderer, x, 96.0f, text);
    441 		ph_format_bonuses(text, sizeof(text), item->bonuses);
    442 		if (item->use == PH_ITEM_USE_HEAL)
    443 			snprintf(text, sizeof(text), "HEALS %d HP", item->use_power);
    444 		else if (item->use == PH_ITEM_USE_RESTORE_MP)
    445 			snprintf(text, sizeof(text), "RESTORES %d MP", item->use_power);
    446 		if (text[0]) SDL_RenderDebugText(renderer, x, 112.0f, text);
    447 		SDL_RenderDebugText(renderer, x, 148.0f, "ENTER BACK");
    448 		return;
    449 	}
    450 
    451 	SDL_RenderDebugText(renderer, ph_ui_right(152.0f), 58.0f, "ITEM ACTION");
    452 	ph_inventory_action_order(item, actions);
    453 	for (i = 0; i < PH_INVENTORY_ACTION_COUNT; ++i) {
    454 		int action = actions[i];
    455 
    456 		if (ph_inventory_action_enabled(item, action))
    457 			SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
    458 		else
    459 			SDL_SetRenderDrawColor(renderer, PH_MENU_DISABLED_COLOR);
    460 		snprintf(text, sizeof(text), "%c %s",
    461 			action == ui->option ? '>' : ' ', action_names[action]);
    462 		SDL_RenderDebugText(renderer, ph_ui_right(152.0f),
    463 			78.0f + (float)i * 18.0f, text);
    464 	}
    465 }
    466 
    467 static void
    468 ph_draw_inventory(SDL_Renderer *renderer, const PhRenderAssets *assets,
    469 	const PhWorld *world, const PhInventoryUi *ui)
    470 {
    471 	const PhEntity *player = ph_world_player(world);
    472 	char text[128];
    473 	int selected_rank = 0;
    474 	int total = 0;
    475 	int rank = 0;
    476 	int page;
    477 	int i;
    478 
    479 	for (i = 0; i < world->content.item_count; ++i)
    480 		if (world->inventory[i] > 0) {
    481 			if (i == ui->selected) selected_rank = total;
    482 			++total;
    483 		}
    484 	page = selected_rank / 5;
    485 	ph_draw_menu(renderer, "INVENTORY", PH_KEY_INVENTORY);
    486 	if (player) {
    487 		snprintf(text, sizeof(text), "GOLD %d", player->gold);
    488 		SDL_RenderDebugText(renderer, ph_ui_right(96.0f), 22.0f, text);
    489 	}
    490 	for (i = 0; i < world->content.item_count; ++i) {
    491 		const PhItemDef *item = &world->content.items[i];
    492 		SDL_Texture *texture;
    493 		float y;
    494 
    495 		if (world->inventory[i] <= 0) continue;
    496 		if (rank < page * 5 || rank >= page * 5 + 5) {
    497 			++rank;
    498 			continue;
    499 		}
    500 		y = 48.0f + (float)(rank++ - page * 5) * 27.0f;
    501 		SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
    502 		SDL_RenderDebugText(renderer, 20.0f, y, i == ui->selected ? ">" : " ");
    503 		texture = ph_render_asset(assets, item->asset_id);
    504 		if (texture) {
    505 			SDL_FRect icon = { 32.0f, y - 4.0f, PH_TILE_SIZE, PH_TILE_SIZE };
    506 			ph_render_sprite(renderer, texture,
    507 				item->sprite_tile_x, item->sprite_tile_y, icon, SDL_FLIP_NONE);
    508 		}
    509 		snprintf(text, sizeof(text), "%s  x%d",
    510 			item->name ? item->name : "Unknown", world->inventory[i]);
    511 		SDL_RenderDebugText(renderer, 56.0f, y, text);
    512 		ph_format_bonuses(text, sizeof(text), item->bonuses);
    513 		if (!text[0] && item->use == PH_ITEM_USE_HEAL)
    514 			snprintf(text, sizeof(text), "HEALS %d HP", item->use_power);
    515 		if (!text[0] && item->use == PH_ITEM_USE_RESTORE_MP)
    516 			snprintf(text, sizeof(text), "RESTORES %d MP", item->use_power);
    517 		if (!text[0]) snprintf(text, sizeof(text), "SELL %d G", item->sell_value);
    518 		SDL_SetRenderDrawColor(renderer, PH_MENU_BONUS_COLOR);
    519 		SDL_RenderDebugText(renderer, 56.0f, y + 10.0f, text);
    520 	}
    521 	SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
    522 	if (!total) SDL_RenderDebugText(renderer, 24.0f, 48.0f, "EMPTY");
    523 	if (total > 5) {
    524 		snprintf(text, sizeof(text), "PAGE %d/%d", page + 1, (total + 4) / 5);
    525 		SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(56.0f), text);
    526 	}
    527 	if (ui->status[0])
    528 		SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(40.0f), ui->status);
    529 	snprintf(text, sizeof(text), "%s SELECT",
    530 		SDL_GetScancodeName(PH_KEY_MENU_ACCEPT));
    531 	SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(24.0f), text);
    532 	if (ui->selected >= 0 && ui->mode != PH_INVENTORY_BROWSE)
    533 		ph_draw_inventory_popup(renderer, world, ui);
    534 }
    535 
    536 int
    537 ph_inventory_key(PhInventoryUi *ui, PhWorld *world, SDL_Scancode key)
    538 {
    539 	int previous = key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 ||
    540 		key == PH_KEY_MENU_LEFT_1 || key == PH_KEY_MENU_LEFT_2;
    541 	int next = key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2 ||
    542 		key == PH_KEY_MENU_RIGHT_1 || key == PH_KEY_MENU_RIGHT_2;
    543 
    544 	if (ph_menu_back(key)) {
    545 		if (ui->mode == PH_INVENTORY_EXAMINE)
    546 			ui->mode = PH_INVENTORY_ACTIONS;
    547 		else if (ui->mode == PH_INVENTORY_ACTIONS)
    548 			ui->mode = PH_INVENTORY_BROWSE;
    549 		else
    550 			return 1;
    551 		ui->status[0] = '\0';
    552 		return 0;
    553 	}
    554 	if (key == PH_KEY_MENU_ACCEPT) {
    555 		if (ui->mode == PH_INVENTORY_BROWSE && ui->selected >= 0) {
    556 			int actions[PH_INVENTORY_ACTION_COUNT];
    557 
    558 			ph_inventory_action_order(&world->content.items[ui->selected], actions);
    559 			ui->mode = PH_INVENTORY_ACTIONS;
    560 			ui->option = actions[0];
    561 			ui->status[0] = '\0';
    562 		} else if (ui->mode == PH_INVENTORY_ACTIONS) {
    563 			ph_inventory_action(ui, world, ui->option);
    564 		} else if (ui->mode == PH_INVENTORY_EXAMINE) {
    565 			ui->mode = PH_INVENTORY_ACTIONS;
    566 		}
    567 		return 0;
    568 	}
    569 	if ((!previous && !next) || ui->mode == PH_INVENTORY_EXAMINE) return 0;
    570 	if (ui->mode == PH_INVENTORY_ACTIONS) {
    571 		ui->option = ph_inventory_action_step(&world->content.items[ui->selected],
    572 			ui->option, previous ? -1 : 1);
    573 	} else {
    574 		ui->selected = ph_inventory_step(world, ui->selected, previous ? -1 : 1);
    575 		ui->status[0] = '\0';
    576 	}
    577 	return 0;
    578 }
    579 
    580 void
    581 ph_spellbook_open(PhSpellbookUi *ui, const PhSpellBook *spells)
    582 {
    583 	memset(ui, 0, sizeof(*ui));
    584 	ui->selected = spells && spells->learned_count > 0 ? 0 : -1;
    585 }
    586 
    587 static int
    588 ph_spellbook_action_enabled(const PhSpellDef *spell, int action)
    589 {
    590 	return action == PH_SPELLBOOK_EXAMINE_ACTION ||
    591 		(spell && spell->field_use != PH_SPELL_FIELD_NONE);
    592 }
    593 
    594 static int
    595 ph_spellbook_action_step(const PhSpellDef *spell, int selected, int direction)
    596 {
    597 	int action;
    598 	int step;
    599 
    600 	for (step = 1; step <= PH_SPELLBOOK_ACTION_COUNT; ++step) {
    601 		action = (selected + (direction < 0 ?
    602 			PH_SPELLBOOK_ACTION_COUNT - step : step)) % PH_SPELLBOOK_ACTION_COUNT;
    603 		if (ph_spellbook_action_enabled(spell, action)) return action;
    604 	}
    605 	return selected;
    606 }
    607 
    608 static void
    609 ph_spellbook_action(PhSpellbookUi *ui, PhWorld *world, PhSpellBook *spells)
    610 {
    611 	const PhSpellDef *spell;
    612 	int amount;
    613 
    614 	if (ui->selected < 0 || ui->selected >= spells->learned_count) return;
    615 	spell = ph_spellbook_def(spells, spells->learned[ui->selected]);
    616 	if (!spell) return;
    617 	if (ui->option == PH_SPELLBOOK_EXAMINE_ACTION) {
    618 		ui->mode = PH_SPELLBOOK_EXAMINE;
    619 		ui->status[0] = '\0';
    620 		return;
    621 	}
    622 	amount = ph_spellbook_use(spells, world, world->player_index, spell->id);
    623 	if (amount < 0)
    624 		snprintf(ui->status, sizeof(ui->status), "Cannot use %s.", spell->name);
    625 	else {
    626 		snprintf(ui->status, sizeof(ui->status), "Restored %d HP.", amount);
    627 		ui->spell_used = 1;
    628 	}
    629 	ui->mode = PH_SPELLBOOK_BROWSE;
    630 }
    631 
    632 static void
    633 ph_draw_spellbook_popup(SDL_Renderer *renderer, const PhSpellBook *spells,
    634 	const PhSpellbookUi *ui)
    635 {
    636 	const PhSpellDef *spell = ph_spellbook_def(spells,
    637 		spells->learned[ui->selected]);
    638 	SDL_FRect popup = ui->mode == PH_SPELLBOOK_EXAMINE ?
    639 		(SDL_FRect){ 40.0f, 48.0f, PH_VIEW_W - 64.0f, 124.0f } :
    640 		(SDL_FRect){ PH_VIEW_W - 164.0f, 48.0f, 140.0f, 82.0f };
    641 	char text[96];
    642 	int action;
    643 
    644 	if (!spell) return;
    645 	SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR);
    646 	SDL_RenderFillRect(renderer, &popup);
    647 	SDL_SetRenderDrawColor(renderer, PH_MENU_BORDER_COLOR);
    648 	SDL_RenderRect(renderer, &popup);
    649 	SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
    650 	if (ui->mode == PH_SPELLBOOK_EXAMINE) {
    651 		SDL_RenderDebugText(renderer, 52.0f, 58.0f, spell->name);
    652 		ph_draw_wrapped_text(renderer, 52.0f, 76.0f, popup.w - 24.0f, 12.0f,
    653 			spell->description ? spell->description : "No description.");
    654 		snprintf(text, sizeof(text), "%d MP   POWER %d   DELAY %d%%",
    655 			spell->mp_cost, spell->power, spell->delay_percent);
    656 		SDL_RenderDebugText(renderer, 52.0f, 126.0f, text);
    657 		SDL_RenderDebugText(renderer, 52.0f, 148.0f, "ENTER BACK");
    658 		return;
    659 	}
    660 	SDL_RenderDebugText(renderer, ph_ui_right(152.0f), 58.0f, "SPELL ACTION");
    661 	for (action = 0; action < PH_SPELLBOOK_ACTION_COUNT; ++action) {
    662 		if (ph_spellbook_action_enabled(spell, action))
    663 			SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
    664 		else
    665 			SDL_SetRenderDrawColor(renderer, PH_MENU_DISABLED_COLOR);
    666 		snprintf(text, sizeof(text), "%c %s", ui->option == action ? '>' : ' ',
    667 			action == PH_SPELLBOOK_USE ? "USE" : "EXAMINE");
    668 		SDL_RenderDebugText(renderer, ph_ui_right(152.0f),
    669 			78.0f + (float)action * 18.0f, text);
    670 	}
    671 }
    672 
    673 static void
    674 ph_draw_spellbook(SDL_Renderer *renderer, const PhWorld *world,
    675 	const PhSpellBook *spells, const PhSpellbookUi *ui)
    676 {
    677 	const PhEntity *player = ph_world_player(world);
    678 	PhStats stats = ph_world_entity_stats(world, player);
    679 	char text[96];
    680 	int page = ui->selected < 0 ? 0 : ui->selected / 6;
    681 	int i;
    682 
    683 	ph_draw_menu(renderer, "MAGICK BOOK", PH_KEY_SPELLBOOK);
    684 	if (player) {
    685 		snprintf(text, sizeof(text), "MP %d/%d", player->vitals.mp, stats.max_mp);
    686 		SDL_RenderDebugText(renderer, ph_ui_right(96.0f), 22.0f, text);
    687 	}
    688 	for (i = page * 6; i < spells->learned_count && i < page * 6 + 6; ++i) {
    689 		const PhSpellDef *spell = ph_spellbook_def(spells, spells->learned[i]);
    690 		float y = 48.0f + (float)(i - page * 6) * 24.0f;
    691 
    692 		if (!spell) continue;
    693 		SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
    694 		snprintf(text, sizeof(text), "%c %-14s %2d MP",
    695 			ui->selected == i ? '>' : ' ', spell->name, spell->mp_cost);
    696 		SDL_RenderDebugText(renderer, 24.0f, y, text);
    697 		SDL_SetRenderDrawColor(renderer, PH_MENU_BONUS_COLOR);
    698 		snprintf(text, sizeof(text), "%s  POWER %d  DELAY %d%%",
    699 			spell->field_use != PH_SPELL_FIELD_NONE ? "FIELD" : "BATTLE",
    700 			spell->power, spell->delay_percent);
    701 		SDL_RenderDebugText(renderer, 40.0f, y + 10.0f, text);
    702 	}
    703 	SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
    704 	if (spells->learned_count <= 0)
    705 		SDL_RenderDebugText(renderer, 24.0f, 48.0f, "NO SPELLS LEARNED");
    706 	if (spells->learned_count > 6) {
    707 		snprintf(text, sizeof(text), "PAGE %d/%d", page + 1,
    708 			(spells->learned_count + 5) / 6);
    709 		SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(56.0f), text);
    710 	}
    711 	if (ui->status[0])
    712 		SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(40.0f), ui->status);
    713 	SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(24.0f), "ENTER SELECT");
    714 	if (ui->selected >= 0 && ui->mode != PH_SPELLBOOK_BROWSE)
    715 		ph_draw_spellbook_popup(renderer, spells, ui);
    716 }
    717 
    718 int
    719 ph_spellbook_key(PhSpellbookUi *ui, PhWorld *world, PhSpellBook *spells,
    720 	SDL_Scancode key)
    721 {
    722 	int previous = key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 ||
    723 		key == PH_KEY_MENU_LEFT_1 || key == PH_KEY_MENU_LEFT_2;
    724 	int next = key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2 ||
    725 		key == PH_KEY_MENU_RIGHT_1 || key == PH_KEY_MENU_RIGHT_2;
    726 	const PhSpellDef *spell = ui->selected >= 0 &&
    727 		ui->selected < spells->learned_count ? ph_spellbook_def(spells,
    728 		spells->learned[ui->selected]) : NULL;
    729 
    730 	if (ph_menu_back(key)) {
    731 		if (ui->mode == PH_SPELLBOOK_EXAMINE)
    732 			ui->mode = PH_SPELLBOOK_ACTIONS;
    733 		else if (ui->mode == PH_SPELLBOOK_ACTIONS)
    734 			ui->mode = PH_SPELLBOOK_BROWSE;
    735 		else return 1;
    736 		ui->status[0] = '\0';
    737 		return 0;
    738 	}
    739 	if (key == PH_KEY_MENU_ACCEPT) {
    740 		if (ui->mode == PH_SPELLBOOK_BROWSE && spell) {
    741 			ui->mode = PH_SPELLBOOK_ACTIONS;
    742 			ui->option = ph_spellbook_action_enabled(spell, PH_SPELLBOOK_USE) ?
    743 				PH_SPELLBOOK_USE : PH_SPELLBOOK_EXAMINE_ACTION;
    744 			ui->status[0] = '\0';
    745 		} else if (ui->mode == PH_SPELLBOOK_ACTIONS) {
    746 			ph_spellbook_action(ui, world, spells);
    747 		} else if (ui->mode == PH_SPELLBOOK_EXAMINE) {
    748 			ui->mode = PH_SPELLBOOK_ACTIONS;
    749 		}
    750 		return 0;
    751 	}
    752 	if ((!previous && !next) || ui->mode == PH_SPELLBOOK_EXAMINE) return 0;
    753 	if (ui->mode == PH_SPELLBOOK_ACTIONS) {
    754 		ui->option = ph_spellbook_action_step(spell, ui->option,
    755 			previous ? -1 : 1);
    756 	} else if (spells->learned_count > 0) {
    757 		ui->selected = (ui->selected + (previous ?
    758 			spells->learned_count - 1 : 1)) % spells->learned_count;
    759 		ui->status[0] = '\0';
    760 	}
    761 	return 0;
    762 }
    763 
    764 void
    765 ph_shop_open(PhShopUi *ui, int shop_id)
    766 {
    767 	memset(ui, 0, sizeof(*ui));
    768 	ui->shop_id = shop_id;
    769 }
    770 
    771 PhMenu
    772 ph_interaction_open(PhShopUi *shop_ui, int interaction_id)
    773 {
    774 	const PhInteractionDef *interaction = ph_game_interaction_def(interaction_id);
    775 
    776 	if (!interaction) return PH_MENU_NONE;
    777 	if (interaction->kind == PH_INTERACTION_SHOP &&
    778 			ph_game_shop_def(interaction->content_id)) {
    779 		ph_shop_open(shop_ui, interaction->content_id);
    780 		return PH_MENU_SHOP;
    781 	}
    782 	return PH_MENU_NONE;
    783 }
    784 
    785 int
    786 ph_shop_key(PhShopUi *ui, PhWorld *world, PhSpellBook *spells, SDL_Scancode key)
    787 {
    788 	const PhShopDef *shop = ph_game_shop_def(ui->shop_id);
    789 	int previous = key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 ||
    790 		key == PH_KEY_MENU_LEFT_1 || key == PH_KEY_MENU_LEFT_2;
    791 	int next = key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2 ||
    792 		key == PH_KEY_MENU_RIGHT_1 || key == PH_KEY_MENU_RIGHT_2;
    793 
    794 	if (!shop) return 1;
    795 	if (ph_menu_back(key)) {
    796 		if (ui->mode == PH_SHOP_ROOT) return 1;
    797 		ui->mode = PH_SHOP_ROOT;
    798 		ui->selected = 0;
    799 		ui->status[0] = '\0';
    800 		return 0;
    801 	}
    802 	if (ui->mode == PH_SHOP_TALK) {
    803 		if (key == PH_KEY_MENU_ACCEPT) ui->mode = PH_SHOP_ROOT;
    804 		return 0;
    805 	}
    806 	if (previous || next) {
    807 		int count = ui->mode == PH_SHOP_ROOT ?
    808 			(shop->kind == PH_SHOP_ITEMS ? 3 : 2) :
    809 			ui->mode == PH_SHOP_BUY ? shop->stock_count : world->content.item_count;
    810 
    811 		if (ui->mode == PH_SHOP_SELL) {
    812 			ui->selected = ph_inventory_step(world, ui->selected,
    813 				previous ? -1 : 1);
    814 		} else if (count > 0) {
    815 			ui->selected = (ui->selected + (previous ? count - 1 : 1)) % count;
    816 		}
    817 		ui->status[0] = '\0';
    818 		return 0;
    819 	}
    820 	if (key != PH_KEY_MENU_ACCEPT) return 0;
    821 	if (ui->mode == PH_SHOP_ROOT) {
    822 		ui->mode = shop->kind == PH_SHOP_ITEMS ?
    823 			(PhShopMode)(ui->selected + 1) :
    824 			ui->selected == 0 ? PH_SHOP_BUY : PH_SHOP_TALK;
    825 		ui->selected = ui->mode == PH_SHOP_SELL ?
    826 			ph_inventory_step(world, -1, 1) : 0;
    827 		return 0;
    828 	}
    829 	if (ui->mode == PH_SHOP_BUY && ui->selected < shop->stock_count) {
    830 		if (shop->kind == PH_SHOP_SPELLS) {
    831 			const PhSpellDef *spell = ph_spellbook_def(spells,
    832 				shop->stock[ui->selected]);
    833 
    834 			if (!spell) return 0;
    835 			if (ph_spellbook_has(spells, spell->id))
    836 				snprintf(ui->status, sizeof(ui->status), "Already learned.");
    837 			else if (ph_spellbook_buy(spells, world, world->player_index,
    838 					spell->id) < 0)
    839 				snprintf(ui->status, sizeof(ui->status), "Not enough gold.");
    840 			else
    841 				snprintf(ui->status, sizeof(ui->status), "Learned %s.", spell->name);
    842 		} else {
    843 			const PhItemDef *item = ph_world_item_def(world,
    844 				shop->stock[ui->selected]);
    845 
    846 			if (!item || ph_world_buy_item(world, world->player_index, item->id) < 0)
    847 				snprintf(ui->status, sizeof(ui->status), "Not enough gold.");
    848 			else
    849 				snprintf(ui->status, sizeof(ui->status), "Bought %s.", item->name);
    850 		}
    851 	} else if (ui->mode == PH_SHOP_SELL && ui->selected >= 0) {
    852 		const PhItemDef *item = &world->content.items[ui->selected];
    853 
    854 		if (ph_world_sell_item(world, world->player_index, item->id) < 0)
    855 			snprintf(ui->status, sizeof(ui->status), "Cannot sell that.");
    856 		else {
    857 			snprintf(ui->status, sizeof(ui->status), "Sold %s.", item->name);
    858 			if (world->inventory[ui->selected] <= 0)
    859 				ui->selected = ph_inventory_step(world, ui->selected, 1);
    860 		}
    861 	}
    862 	return 0;
    863 }
    864 
    865 static void
    866 ph_draw_shop(SDL_Renderer *renderer, const PhWorld *world,
    867 	const PhSpellBook *spells, const PhShopUi *ui)
    868 {
    869 	static const char *item_options[] = { "BUY", "SELL", "TALK" };
    870 	static const char *magick_options[] = { "LEARN", "TALK" };
    871 	const PhShopDef *shop = ph_game_shop_def(ui->shop_id);
    872 	const PhEntity *player = ph_world_player(world);
    873 	char text[128];
    874 	int i;
    875 
    876 	if (!shop || !player) return;
    877 	ph_draw_menu(renderer, shop->name, PH_KEY_MENU_CANCEL);
    878 	snprintf(text, sizeof(text), "GOLD %d", player->gold);
    879 	SDL_RenderDebugText(renderer, ph_ui_right(96.0f), 38.0f, text);
    880 	if (ui->mode == PH_SHOP_ROOT) {
    881 		const char **options = shop->kind == PH_SHOP_ITEMS ?
    882 			item_options : magick_options;
    883 		int option_count = shop->kind == PH_SHOP_ITEMS ?
    884 			(int)LEN(item_options) : (int)LEN(magick_options);
    885 
    886 		for (i = 0; i < option_count; ++i) {
    887 			snprintf(text, sizeof(text), "%c %s", ui->selected == i ? '>' : ' ',
    888 				options[i]);
    889 			SDL_RenderDebugText(renderer, 32.0f, 56.0f + (float)i * 20.0f, text);
    890 		}
    891 		SDL_RenderDebugText(renderer, 32.0f, 132.0f,
    892 			shop->kind == PH_SHOP_ITEMS ? "Welcome, traveler." :
    893 			"Choose your lesson.");
    894 	} else if (ui->mode == PH_SHOP_BUY) {
    895 		SDL_RenderDebugText(renderer, 24.0f, 42.0f,
    896 			shop->kind == PH_SHOP_ITEMS ? "BUY" : "MAGICK");
    897 		for (i = 0; i < shop->stock_count; ++i) {
    898 			if (shop->kind == PH_SHOP_SPELLS) {
    899 				const PhSpellDef *spell = ph_spellbook_def(spells, shop->stock[i]);
    900 
    901 				if (!spell) continue;
    902 				if (ph_spellbook_has(spells, spell->id))
    903 					snprintf(text, sizeof(text), "%c %-12s LEARNED",
    904 						ui->selected == i ? '>' : ' ', spell->name);
    905 				else
    906 					snprintf(text, sizeof(text), "%c %-12s %4d G %2d MP",
    907 						ui->selected == i ? '>' : ' ', spell->name,
    908 						spell->buy_value, spell->mp_cost);
    909 			} else {
    910 				const PhItemDef *item = ph_world_item_def(world, shop->stock[i]);
    911 
    912 				if (!item) continue;
    913 				snprintf(text, sizeof(text), "%c %-18s %d G",
    914 					ui->selected == i ? '>' : ' ', item->name, item->buy_value);
    915 			}
    916 			SDL_RenderDebugText(renderer, 24.0f, 62.0f + (float)i * 20.0f, text);
    917 		}
    918 	} else if (ui->mode == PH_SHOP_SELL) {
    919 		int row = 0;
    920 
    921 		SDL_RenderDebugText(renderer, 24.0f, 42.0f, "SELL");
    922 		for (i = 0; i < world->content.item_count; ++i) {
    923 			const PhItemDef *item = &world->content.items[i];
    924 
    925 			if (world->inventory[i] <= 0) continue;
    926 			snprintf(text, sizeof(text), "%c %-16s x%d  %d G",
    927 				ui->selected == i ? '>' : ' ', item->name, world->inventory[i],
    928 				item->sell_value);
    929 			SDL_RenderDebugText(renderer, 24.0f, 62.0f + (float)row++ * 18.0f, text);
    930 		}
    931 		if (!row) SDL_RenderDebugText(renderer, 24.0f, 62.0f, "Nothing to sell.");
    932 	} else {
    933 		float y = 48.0f;
    934 		float width = ph_ui_right(24.0f) - 24.0f;
    935 
    936 		for (i = 0; i < PH_SHOP_TALK_LINES; ++i) {
    937 			y = ph_draw_wrapped_text(renderer, 24.0f, y, width, 12.0f,
    938 				shop->talk[i]);
    939 			y += 6.0f;
    940 		}
    941 		SDL_RenderDebugText(renderer, 24.0f, y + 8.0f, "ENTER / ESC BACK");
    942 	}
    943 	if (ui->status[0])
    944 		SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(36.0f), ui->status);
    945 }
    946 
    947 static void
    948 ph_draw_battle_entity(SDL_Renderer *renderer, const PhRenderAssets *assets,
    949 	const PhWorld *world, int entity_index, SDL_FRect rect)
    950 {
    951 	const PhEntity *entity = &world->entities[entity_index];
    952 	const PhEntityDef *def = ph_world_entity_def(world, entity->type_id);
    953 	SDL_Texture *texture = NULL;
    954 	int x;
    955 	int y;
    956 	SDL_FlipMode flip;
    957 
    958 	if (!def) return;
    959 	texture = ph_render_asset(assets, def->asset_id);
    960 	if (def->kind == PH_ENTITY_PLAYER) {
    961 		x = def->sprite_tiles[PH_SPRITE_SIDE][0][0];
    962 		y = def->sprite_tiles[PH_SPRITE_SIDE][0][1];
    963 		flip = SDL_FLIP_NONE;
    964 	} else {
    965 		ph_render_entity_frame(def, entity, &x, &y, &flip);
    966 	}
    967 	if (texture) ph_render_sprite(renderer, texture, x, y, rect, flip);
    968 	else {
    969 		if (def->kind == PH_ENTITY_MONSTER)
    970 			SDL_SetRenderDrawColor(renderer, PH_MONSTER_COLOR);
    971 		else
    972 			SDL_SetRenderDrawColor(renderer, PH_NPC_COLOR);
    973 		SDL_RenderFillRect(renderer, &rect);
    974 	}
    975 }
    976 
    977 static void
    978 ph_draw_battle(SDL_Renderer *renderer, const PhRenderAssets *assets,
    979 	const PhWorld *world, const PhBattle *battle)
    980 {
    981 	static const char *commands[PH_BATTLE_COMMAND_COUNT] = {
    982 		[PH_BATTLE_FIGHT] = "FIGHT",
    983 		[PH_BATTLE_MAGICK] = "MAGICK",
    984 		[PH_BATTLE_ITEM] = "ITEM",
    985 	};
    986 	const PhEntity *player = ph_world_player(world);
    987 	const PhEntity *enemy = &world->entities[battle->enemy_index];
    988 	const PhEntityDef *enemy_def = ph_world_entity_def(world, enemy->type_id);
    989 	PhStats player_stats = ph_world_entity_stats(world, player);
    990 	PhStats enemy_stats = ph_world_entity_stats(world, enemy);
    991 	int order[PH_BATTLE_ORDER_COUNT];
    992 	SDL_FRect ground = { 0.0f, PH_VIEW_H - 136.0f, PH_VIEW_W, 72.0f };
    993 	SDL_FRect panel = { 8.0f, PH_VIEW_H - 64.0f, PH_VIEW_W - 16.0f, 56.0f };
    994 	SDL_FRect spell_panel = { PH_VIEW_W * 0.5f - 88.0f,
    995 		PH_VIEW_H - 116.0f, 176.0f,
    996 		24.0f + (float)battle->spells.learned_count * 12.0f };
    997 	SDL_FRect enemy_rect = { 56.0f + ph_battle_lunge(battle,
    998 		PH_BATTLE_ENEMY_ACTION), PH_VIEW_H - 172.0f,
    999 		PH_TILE_SIZE * 3.0f, PH_TILE_SIZE * 3.0f };
   1000 	SDL_FRect player_rect = { PH_VIEW_W - 88.0f - ph_battle_lunge(battle,
   1001 		PH_BATTLE_PLAYER_ACTION), PH_VIEW_H - 120.0f,
   1002 		PH_TILE_SIZE * 3.0f, PH_TILE_SIZE * 3.0f };
   1003 	char text[96];
   1004 	int i;
   1005 
   1006 	SDL_SetRenderDrawColor(renderer, PH_BATTLE_BG_COLOR);
   1007 	SDL_RenderClear(renderer);
   1008 	SDL_SetRenderDrawColor(renderer, PH_BATTLE_GROUND_COLOR);
   1009 	SDL_RenderFillRect(renderer, &ground);
   1010 	ph_draw_battle_entity(renderer, assets, world, battle->enemy_index, enemy_rect);
   1011 	ph_draw_battle_entity(renderer, assets, world, world->player_index, player_rect);
   1012 	if (battle->phase == PH_BATTLE_TARGET) {
   1013 		SDL_FRect target_rect = battle->choice.target_index == world->player_index ?
   1014 			player_rect : enemy_rect;
   1015 
   1016 		target_rect.x -= 3.0f;
   1017 		target_rect.y -= 3.0f;
   1018 		target_rect.w += 6.0f;
   1019 		target_rect.h += 6.0f;
   1020 		SDL_SetRenderDrawColor(renderer, PH_MENU_BONUS_COLOR);
   1021 		SDL_RenderRect(renderer, &target_rect);
   1022 	}
   1023 	SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
   1024 	SDL_RenderDebugText(renderer, 16.0f, 18.0f,
   1025 		enemy_def && enemy_def->name ? enemy_def->name : "ENEMY");
   1026 	snprintf(text, sizeof(text), "HP %d/%d", enemy->vitals.hp, enemy_stats.max_hp);
   1027 	SDL_RenderDebugText(renderer, 16.0f, 30.0f, text);
   1028 	if (enemy->accuracy.turns > 0) {
   1029 		snprintf(text, sizeof(text), "ACC -%d%% (%d)", enemy->accuracy.value,
   1030 			enemy->accuracy.turns);
   1031 		SDL_RenderDebugText(renderer, 16.0f, 42.0f, text);
   1032 	}
   1033 	ph_battle_order(battle, world, order);
   1034 	SDL_RenderDebugText(renderer, ph_ui_right(96.0f), 18.0f, "TURN ORDER");
   1035 	for (i = 0; i < PH_BATTLE_ORDER_COUNT; ++i) {
   1036 		snprintf(text, sizeof(text), "%d %s", i + 1,
   1037 			order[i] == world->player_index ? "YOU" : "ENEMY");
   1038 		SDL_RenderDebugText(renderer, ph_ui_right(80.0f),
   1039 			30.0f + (float)i * 11.0f, text);
   1040 	}
   1041 	SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR);
   1042 	SDL_RenderFillRect(renderer, &panel);
   1043 	SDL_SetRenderDrawColor(renderer, PH_MENU_BORDER_COLOR);
   1044 	SDL_RenderRect(renderer, &panel);
   1045 	for (i = 0; i < (int)LEN(commands); ++i) {
   1046 		SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
   1047 		snprintf(text, sizeof(text), "%c %s",
   1048 			(battle->phase == PH_BATTLE_COMMAND || battle->phase == PH_BATTLE_MAGIC ||
   1049 				battle->phase == PH_BATTLE_TARGET) &&
   1050 				battle->choice.command == (PhBattleCommand)i ? '>' : ' ',
   1051 			commands[i]);
   1052 		SDL_RenderDebugText(renderer, 18.0f,
   1053 			panel.y + 8.0f + (float)i * 14.0f, text);
   1054 	}
   1055 	SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
   1056 	SDL_RenderDebugText(renderer, 104.0f, panel.y + 8.0f, battle->message);
   1057 	if (battle->phase == PH_BATTLE_TARGET) {
   1058 		const PhEntityDef *target_def = ph_world_entity_def(world,
   1059 			world->entities[battle->choice.target_index].type_id);
   1060 
   1061 		snprintf(text, sizeof(text), "TARGET: %s   ENTER CONFIRM",
   1062 			target_def && target_def->name ? target_def->name : "UNKNOWN");
   1063 		SDL_RenderDebugText(renderer, 104.0f, panel.y + 24.0f, text);
   1064 	}
   1065 	snprintf(text, sizeof(text), "HP %d/%d", player->vitals.hp, player_stats.max_hp);
   1066 	SDL_RenderDebugText(renderer, ph_ui_right(80.0f), ph_ui_bottom(36.0f), text);
   1067 	snprintf(text, sizeof(text), "MP %d/%d", player->vitals.mp, player_stats.max_mp);
   1068 	SDL_RenderDebugText(renderer, ph_ui_right(80.0f), ph_ui_bottom(24.0f), text);
   1069 	if (battle->phase == PH_BATTLE_MAGIC) {
   1070 		SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR);
   1071 		SDL_RenderFillRect(renderer, &spell_panel);
   1072 		SDL_SetRenderDrawColor(renderer, PH_MENU_BORDER_COLOR);
   1073 		SDL_RenderRect(renderer, &spell_panel);
   1074 		SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
   1075 		SDL_RenderDebugText(renderer, spell_panel.x + 8.0f,
   1076 			spell_panel.y + 6.0f, "MAGICK");
   1077 		for (i = 0; i < battle->spells.learned_count; ++i) {
   1078 			const PhSpellDef *choice = ph_spell_def(battle,
   1079 				battle->spells.learned[i]);
   1080 
   1081 			if (!choice) continue;
   1082 			if (ph_spell_battle_usable(choice))
   1083 				SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
   1084 			else
   1085 				SDL_SetRenderDrawColor(renderer, PH_MENU_DISABLED_COLOR);
   1086 			snprintf(text, sizeof(text), "%c %s %dMP%s",
   1087 				battle->choice.spell == i ? '>' : ' ', choice->name, choice->mp_cost,
   1088 				choice->delay_percent < 100 ? " QUICK" : "");
   1089 			SDL_RenderDebugText(renderer, spell_panel.x + 8.0f,
   1090 				spell_panel.y + 20.0f + (float)i * 12.0f, text);
   1091 		}
   1092 	}
   1093 }
   1094 
   1095 static void
   1096 ph_draw_battle_end(SDL_Renderer *renderer, const PhWorld *world,
   1097 	const PhBattle *battle, PhGameMode game)
   1098 {
   1099 	const PhItemDef *loot = ph_world_item_def(world, battle->reward.item_id);
   1100 	char text[96];
   1101 
   1102 	if (game == PH_GAME_VICTORY)
   1103 		SDL_SetRenderDrawColor(renderer, PH_BATTLE_BG_COLOR);
   1104 	else
   1105 		SDL_SetRenderDrawColor(renderer, PH_CLEAR_COLOR);
   1106 	SDL_RenderClear(renderer);
   1107 	SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
   1108 	if (game == PH_GAME_OVER) {
   1109 		SDL_RenderDebugText(renderer, ph_ui_center_text("GAME OVER"),
   1110 			PH_VIEW_H * 0.5f - 12.0f, "GAME OVER");
   1111 		return;
   1112 	}
   1113 	SDL_RenderDebugText(renderer, ph_ui_center_text("VICTORY!"),
   1114 		PH_VIEW_H * 0.5f - 44.0f, "VICTORY!");
   1115 	snprintf(text, sizeof(text), "XP GAINED  %d", battle->reward.xp);
   1116 	SDL_RenderDebugText(renderer, ph_ui_center_text(text),
   1117 		PH_VIEW_H * 0.5f - 14.0f, text);
   1118 	if (loot && battle->reward.amount > 0)
   1119 		snprintf(text, sizeof(text), "LOOT  %s x%d", loot->name,
   1120 			battle->reward.amount);
   1121 	else
   1122 		snprintf(text, sizeof(text), "LOOT  NONE");
   1123 	SDL_RenderDebugText(renderer, ph_ui_center_text(text),
   1124 		PH_VIEW_H * 0.5f + 4.0f, text);
   1125 	SDL_RenderDebugText(renderer, ph_ui_center_text("ENTER TO CONTINUE"),
   1126 		PH_VIEW_H * 0.5f + 44.0f, "ENTER TO CONTINUE");
   1127 }
   1128 
   1129 void
   1130 ph_present_frame(SDL_Renderer *renderer, const PhRenderAssets *assets,
   1131 	const PhGameContext *context)
   1132 {
   1133 	const PhWorld *world = context->world;
   1134 	const PhBattle *battle = context->battle;
   1135 	PhGameMode game = *context->mode;
   1136 
   1137 	if (game == PH_GAME_BATTLE) {
   1138 		ph_draw_battle(renderer, assets, world, battle);
   1139 		if (context->menu == PH_MENU_MAIN)
   1140 			ph_draw_options(renderer, context->options);
   1141 		SDL_RenderPresent(renderer);
   1142 		return;
   1143 	}
   1144 	if (game == PH_GAME_VICTORY || game == PH_GAME_OVER) {
   1145 		ph_draw_battle_end(renderer, world, battle, game);
   1146 		if (context->menu == PH_MENU_MAIN)
   1147 			ph_draw_options(renderer, context->options);
   1148 		SDL_RenderPresent(renderer);
   1149 		return;
   1150 	}
   1151 
   1152 	SDL_SetRenderDrawColor(renderer, PH_CLEAR_COLOR);
   1153 	SDL_RenderClear(renderer);
   1154 	ph_render_world(renderer, assets, world);
   1155 	if (context->menu == PH_MENU_CHARACTER) {
   1156 		ph_draw_character_sheet(renderer, assets, world);
   1157 	} else if (context->menu == PH_MENU_INVENTORY) {
   1158 		ph_draw_inventory(renderer, assets, world, context->inventory);
   1159 	} else if (context->menu == PH_MENU_SPELLBOOK) {
   1160 		ph_draw_spellbook(renderer, world, context->spells, context->spellbook);
   1161 	} else if (context->menu == PH_MENU_SHOP) {
   1162 		ph_draw_shop(renderer, world, context->spells, context->shop);
   1163 	} else if (context->menu == PH_MENU_MAIN) {
   1164 		ph_draw_options(renderer, context->options);
   1165 	} else if (context->menu == PH_MENU_START) {
   1166 		ph_draw_start(renderer, context->start);
   1167 	} else if (world->notice.seconds > 0.0f && world->notice.text[0] != '\0') {
   1168 		SDL_FRect notice_bg = {
   1169 			.x = 8.0f,
   1170 			.y = (float)(PH_VIEW_H - 28),
   1171 			.w = (float)(PH_VIEW_W - 16),
   1172 			.h = 20.0f,
   1173 		};
   1174 
   1175 		SDL_SetRenderDrawColor(renderer, PH_NOTICE_COLOR);
   1176 		SDL_RenderFillRect(renderer, &notice_bg);
   1177 		SDL_SetRenderDrawColor(renderer, PH_NOTICE_TEXT_COLOR);
   1178 		SDL_RenderDebugText(renderer, 14.0f, (float)(PH_VIEW_H - 22),
   1179 			world->notice.text);
   1180 	}
   1181 	if (game == PH_GAME_TRANSITION) {
   1182 		float progress = 1.0f - battle->timer / PH_BATTLE_TRANSITION_SECONDS;
   1183 		float height = progress * PH_VIEW_H * 0.5f;
   1184 		SDL_FRect top = { 0.0f, 0.0f, PH_VIEW_W, height };
   1185 		SDL_FRect bottom = { 0.0f, PH_VIEW_H - height, PH_VIEW_W, height };
   1186 
   1187 		SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
   1188 		SDL_RenderFillRect(renderer, &top);
   1189 		SDL_RenderFillRect(renderer, &bottom);
   1190 	}
   1191 	SDL_RenderPresent(renderer);
   1192 }
   1193 
   1194 #endif