presentation.c (30023B)
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 static float 11 ph_ui_right(float inset) 12 { 13 return (float)PH_VIEW_W - inset; 14 } 15 16 static float 17 ph_ui_bottom(float inset) 18 { 19 return (float)PH_VIEW_H - inset; 20 } 21 22 static float 23 ph_ui_center_text(const char *text) 24 { 25 return ((float)PH_VIEW_W - (float)strlen(text) * PH_RENDER_FONT_W) * 0.5f; 26 } 27 28 static float 29 ph_draw_wrapped_text(PhRenderer *renderer, float x, float y, 30 float width, float line_height, const char *text) 31 { 32 char line[128]; 33 size_t columns = width > PH_RENDER_FONT_W ? (size_t)(width / PH_RENDER_FONT_W) : 1; 34 35 while (text && *text) { 36 size_t remaining; 37 size_t take; 38 size_t i; 39 40 while (*text == ' ') ++text; 41 remaining = strlen(text); 42 take = remaining < columns ? remaining : columns; 43 if (take < remaining) 44 for (i = take; i > 0; --i) 45 if (text[i] == ' ') { 46 take = i; 47 break; 48 } 49 if (take >= sizeof(line)) take = sizeof(line) - 1; 50 memcpy(line, text, take); 51 line[take] = '\0'; 52 ph_render_text(renderer, x, y, line); 53 text += take; 54 y += line_height; 55 } 56 return y; 57 } 58 59 static void 60 ph_append_bonus(char *text, size_t size, size_t *used, int value, const char *label) 61 { 62 int written; 63 64 if (!value || *used >= size) return; 65 written = snprintf(text + *used, size - *used, "%s%+d %s", 66 *used ? " " : "", value, label); 67 if (written > 0) *used += (size_t)written; 68 } 69 70 static void 71 ph_format_bonuses(char *text, size_t size, PhStats bonuses) 72 { 73 size_t used = 0; 74 75 text[0] = '\0'; 76 ph_append_bonus(text, size, &used, bonuses.max_hp, "HP"); 77 ph_append_bonus(text, size, &used, bonuses.max_mp, "MP"); 78 ph_append_bonus(text, size, &used, bonuses.strength, "STR"); 79 ph_append_bonus(text, size, &used, bonuses.defense, "DEF"); 80 ph_append_bonus(text, size, &used, bonuses.magic, "MAG"); 81 ph_append_bonus(text, size, &used, bonuses.magic_defense, "MDEF"); 82 ph_append_bonus(text, size, &used, bonuses.agility, "AGI"); 83 ph_append_bonus(text, size, &used, bonuses.luck, "LCK"); 84 ph_append_bonus(text, size, &used, bonuses.evasion, "EVA"); 85 ph_append_bonus(text, size, &used, bonuses.accuracy, "ACC"); 86 } 87 88 static void 89 ph_draw_menu(PhRenderer *renderer, const char *title, const char *key, const char *back) 90 { 91 PhRect panel = { 12.0f, 12.0f, PH_VIEW_W - 24.0f, PH_VIEW_H - 24.0f }; 92 char text[32]; 93 float x; 94 95 ph_render_color(renderer, (PhColor){ PH_MENU_COLOR }); 96 ph_render_fill(renderer, panel); 97 ph_render_color(renderer, (PhColor){ PH_MENU_BORDER_COLOR }); 98 ph_render_outline(renderer, panel); 99 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 100 ph_render_text(renderer, 24.0f, 22.0f, title); 101 snprintf(text, sizeof(text), "CLOSE: %s / %s", key, 102 back); 103 x = ph_ui_right(20.0f) - (float)strlen(text) * PH_RENDER_FONT_W; 104 if (x < 24.0f) x = 24.0f; 105 ph_render_text(renderer, x, (float)(PH_VIEW_H - 22), text); 106 } 107 108 static void 109 ph_draw_options(PhRenderer *renderer, const PhOptionsUi *ui) 110 { 111 static const char *root_options[] = { "SOUND", "EXIT GAME" }; 112 PhRect panel = { PH_VIEW_W * 0.5f - 84.0f, 113 PH_VIEW_H * 0.5f - 58.0f, 168.0f, 116.0f }; 114 char text[48]; 115 int i; 116 117 if (!ui) return; 118 ph_render_color(renderer, (PhColor){ PH_MENU_COLOR }); 119 ph_render_fill(renderer, panel); 120 ph_render_color(renderer, (PhColor){ PH_MENU_BORDER_COLOR }); 121 ph_render_outline(renderer, panel); 122 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 123 ph_render_text(renderer, panel.x + 12.0f, panel.y + 10.0f, 124 ui->mode == PH_OPTIONS_SOUND ? "SOUND" : "OPTIONS"); 125 for (i = 0; i < 2; ++i) { 126 if (ui->mode == PH_OPTIONS_SOUND) 127 snprintf(text, sizeof(text), "%c %-8s %s", 128 ui->selected == i ? '>' : ' ', i == 0 ? "MUSIC" : "EFFECTS", 129 (i == 0 ? ui->music_enabled : ui->effects_enabled) ? "ON" : "OFF"); 130 else snprintf(text, sizeof(text), "%c %s", 131 ui->selected == i ? '>' : ' ', root_options[i]); 132 ph_render_text(renderer, panel.x + 20.0f, 133 panel.y + 38.0f + (float)i * 20.0f, text); 134 } 135 ph_render_text(renderer, panel.x + 12.0f, panel.y + 94.0f, 136 "ESC/BKSP BACK"); 137 } 138 139 static void 140 ph_draw_start(PhRenderer *renderer, const PhStartUi *ui) 141 { 142 PhRect panel = { PH_VIEW_W * 0.5f - 82.0f, 143 PH_VIEW_H * 0.5f - 52.0f, 164.0f, 104.0f }; 144 char text[32]; 145 int i; 146 147 if (!ui) return; 148 ph_render_color(renderer, (PhColor){ PH_MENU_COLOR }); 149 ph_render_fill(renderer, panel); 150 ph_render_color(renderer, (PhColor){ PH_MENU_BORDER_COLOR }); 151 ph_render_outline(renderer, panel); 152 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 153 ph_render_text(renderer, panel.x + 42.0f, panel.y + 12.0f, "PHANTASIA"); 154 for (i = 0; i < 2; ++i) { 155 snprintf(text, sizeof(text), "%c %s", ui->selected == i ? '>' : ' ', 156 i == 0 ? "CONTINUE" : "NEW GAME"); 157 if (i == 0 && !ui->can_continue) 158 ph_render_color(renderer, (PhColor){ PH_MENU_DISABLED_COLOR }); 159 else ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 160 ph_render_text(renderer, panel.x + 36.0f, 161 panel.y + 42.0f + (float)i * 18.0f, text); 162 } 163 } 164 165 static void 166 ph_draw_character_sheet(PhRenderer *renderer, 167 const PhWorld *world, const PhControlLabels *labels) 168 { 169 static const char *slot_names[PH_EQUIP_COUNT] = { 170 [PH_EQUIP_WEAPON] = "WEAPON", 171 [PH_EQUIP_ARMOR] = "ARMOR", 172 [PH_EQUIP_CHARM] = "CHARM", 173 }; 174 const PhEntity *player = ph_world_player(world); 175 const PhEntityDef *def; 176 PhStats stats; 177 int asset_id; 178 PhRect avatar = { 32.0f, 46.0f, PH_TILE_SIZE * 3.0f, 179 PH_TILE_SIZE * 3.0f }; 180 char text[128]; 181 int slot; 182 183 if (!player || !(def = ph_world_entity_def(world, player->type_id))) return; 184 stats = ph_world_entity_stats(world, player); 185 ph_draw_menu(renderer, "CHARACTER", labels->character, labels->back); 186 ph_render_text(renderer, 104.0f, 38.0f, def->name); 187 188 asset_id = def->asset_id; 189 if (ph_render_has_asset(renderer, asset_id)) 190 ph_render_sprite(renderer, asset_id, 191 def->sprite_tiles[PH_SPRITE_DOWN][0][0], 192 def->sprite_tiles[PH_SPRITE_DOWN][0][1], 193 avatar, PH_FLIP_NONE); 194 195 snprintf(text, sizeof(text), "HP %d / %d", player->vitals.hp, stats.max_hp); 196 ph_render_text(renderer, 104.0f, 54.0f, text); 197 snprintf(text, sizeof(text), "MP %d / %d", player->vitals.mp, stats.max_mp); 198 ph_render_text(renderer, 104.0f, 66.0f, text); 199 snprintf(text, sizeof(text), "STR %d", stats.strength); 200 ph_render_text(renderer, 104.0f, 78.0f, text); 201 snprintf(text, sizeof(text), "DEF %d", stats.defense); 202 ph_render_text(renderer, 104.0f, 90.0f, text); 203 snprintf(text, sizeof(text), "MAG %d", stats.magic); 204 ph_render_text(renderer, ph_ui_right(88.0f), 54.0f, text); 205 snprintf(text, sizeof(text), "MDEF %d", stats.magic_defense); 206 ph_render_text(renderer, ph_ui_right(88.0f), 66.0f, text); 207 snprintf(text, sizeof(text), "AGI %d", stats.agility); 208 ph_render_text(renderer, ph_ui_right(88.0f), 78.0f, text); 209 snprintf(text, sizeof(text), "LCK %d", stats.luck); 210 ph_render_text(renderer, ph_ui_right(88.0f), 90.0f, text); 211 snprintf(text, sizeof(text), "EVA %d", stats.evasion); 212 ph_render_text(renderer, ph_ui_right(88.0f), 102.0f, text); 213 snprintf(text, sizeof(text), "ACC %d", stats.accuracy); 214 ph_render_text(renderer, ph_ui_right(88.0f), 114.0f, text); 215 ph_render_text(renderer, 24.0f, 128.0f, "EQUIPPED"); 216 217 for (slot = PH_EQUIP_WEAPON; slot < PH_EQUIP_COUNT; ++slot) { 218 const PhItemDef *item = ph_world_item_def(world, player->equipment[slot]); 219 int icon_id; 220 float y = 144.0f + (float)(slot - PH_EQUIP_WEAPON) * 25.0f; 221 222 snprintf(text, sizeof(text), "%s %s", slot_names[slot], item ? item->name : "-"); 223 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 224 ph_render_text(renderer, 48.0f, y, text); 225 if (!item) continue; 226 icon_id = item->asset_id; 227 if (ph_render_has_asset(renderer, icon_id)) { 228 PhRect icon = { 24.0f, y - 4.0f, PH_TILE_SIZE, PH_TILE_SIZE }; 229 ph_render_sprite(renderer, icon_id, 230 item->sprite_tile_x, item->sprite_tile_y, icon, PH_FLIP_NONE); 231 } 232 ph_format_bonuses(text, sizeof(text), item->bonuses); 233 ph_render_color(renderer, (PhColor){ PH_MENU_BONUS_COLOR }); 234 ph_render_text(renderer, 48.0f, y + 10.0f, text); 235 } 236 } 237 238 static void 239 ph_draw_inventory_popup(PhRenderer *renderer, const PhWorld *world, 240 const PhInventoryUi *ui) 241 { 242 static const char *action_names[] = { "EQUIP", "USE", "DROP", "EXAMINE" }; 243 const PhItemDef *item = &world->content.items[ui->selected]; 244 int actions[PH_INVENTORY_ACTION_COUNT]; 245 PhRect popup = ui->mode == PH_INVENTORY_EXAMINE ? 246 (PhRect){ 40.0f, 48.0f, PH_VIEW_W - 64.0f, 124.0f } : 247 (PhRect){ PH_VIEW_W - 164.0f, 48.0f, 140.0f, 124.0f }; 248 char text[128]; 249 float x = ui->mode == PH_INVENTORY_EXAMINE ? 52.0f : ph_ui_right(152.0f); 250 int i; 251 252 ph_render_color(renderer, (PhColor){ PH_MENU_COLOR }); 253 ph_render_fill(renderer, popup); 254 ph_render_color(renderer, (PhColor){ PH_MENU_BORDER_COLOR }); 255 ph_render_outline(renderer, popup); 256 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 257 if (ui->mode == PH_INVENTORY_EXAMINE) { 258 ph_render_text(renderer, x, 58.0f, 259 item->name ? item->name : "ITEM"); 260 ph_render_text(renderer, x, 76.0f, 261 item->description ? item->description : "No description."); 262 snprintf(text, sizeof(text), "SELL %d G", item->sell_value); 263 ph_render_text(renderer, x, 96.0f, text); 264 ph_format_bonuses(text, sizeof(text), item->bonuses); 265 if (item->use == PH_ITEM_USE_HEAL) 266 snprintf(text, sizeof(text), "HEALS %d HP", item->use_power); 267 else if (item->use == PH_ITEM_USE_RESTORE_MP) 268 snprintf(text, sizeof(text), "RESTORES %d MP", item->use_power); 269 if (text[0]) ph_render_text(renderer, x, 112.0f, text); 270 ph_render_text(renderer, x, 148.0f, "ENTER BACK"); 271 return; 272 } 273 274 ph_render_text(renderer, ph_ui_right(152.0f), 58.0f, "ITEM ACTION"); 275 ph_inventory_action_order(item, actions); 276 for (i = 0; i < PH_INVENTORY_ACTION_COUNT; ++i) { 277 int action = actions[i]; 278 279 if (ph_inventory_action_enabled(item, action)) 280 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 281 else 282 ph_render_color(renderer, (PhColor){ PH_MENU_DISABLED_COLOR }); 283 snprintf(text, sizeof(text), "%c %s", 284 action == ui->option ? '>' : ' ', action_names[action]); 285 ph_render_text(renderer, ph_ui_right(152.0f), 286 78.0f + (float)i * 18.0f, text); 287 } 288 } 289 290 static void 291 ph_draw_inventory(PhRenderer *renderer, 292 const PhWorld *world, const PhInventoryUi *ui, const PhControlLabels *labels) 293 { 294 const PhEntity *player = ph_world_player(world); 295 char text[128]; 296 int selected_rank = 0; 297 int total = 0; 298 int rank = 0; 299 int page; 300 int i; 301 302 for (i = 0; i < world->content.item_count; ++i) 303 if (world->inventory[i] > 0) { 304 if (i == ui->selected) selected_rank = total; 305 ++total; 306 } 307 page = selected_rank / 5; 308 ph_draw_menu(renderer, "INVENTORY", labels->inventory, labels->back); 309 if (player) { 310 snprintf(text, sizeof(text), "GOLD %d", player->gold); 311 ph_render_text(renderer, ph_ui_right(96.0f), 22.0f, text); 312 } 313 for (i = 0; i < world->content.item_count; ++i) { 314 const PhItemDef *item = &world->content.items[i]; 315 int asset_id; 316 float y; 317 318 if (world->inventory[i] <= 0) continue; 319 if (rank < page * 5 || rank >= page * 5 + 5) { 320 ++rank; 321 continue; 322 } 323 y = 48.0f + (float)(rank++ - page * 5) * 27.0f; 324 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 325 ph_render_text(renderer, 20.0f, y, i == ui->selected ? ">" : " "); 326 asset_id = item->asset_id; 327 if (ph_render_has_asset(renderer, asset_id)) { 328 PhRect icon = { 32.0f, y - 4.0f, PH_TILE_SIZE, PH_TILE_SIZE }; 329 ph_render_sprite(renderer, asset_id, 330 item->sprite_tile_x, item->sprite_tile_y, icon, PH_FLIP_NONE); 331 } 332 snprintf(text, sizeof(text), "%s x%d", 333 item->name ? item->name : "Unknown", world->inventory[i]); 334 ph_render_text(renderer, 56.0f, y, text); 335 ph_format_bonuses(text, sizeof(text), item->bonuses); 336 if (!text[0] && item->use == PH_ITEM_USE_HEAL) 337 snprintf(text, sizeof(text), "HEALS %d HP", item->use_power); 338 if (!text[0] && item->use == PH_ITEM_USE_RESTORE_MP) 339 snprintf(text, sizeof(text), "RESTORES %d MP", item->use_power); 340 if (!text[0]) snprintf(text, sizeof(text), "SELL %d G", item->sell_value); 341 ph_render_color(renderer, (PhColor){ PH_MENU_BONUS_COLOR }); 342 ph_render_text(renderer, 56.0f, y + 10.0f, text); 343 } 344 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 345 if (!total) ph_render_text(renderer, 24.0f, 48.0f, "EMPTY"); 346 if (total > 5) { 347 snprintf(text, sizeof(text), "PAGE %d/%d", page + 1, (total + 4) / 5); 348 ph_render_text(renderer, 24.0f, ph_ui_bottom(56.0f), text); 349 } 350 if (ui->status[0]) 351 ph_render_text(renderer, 24.0f, ph_ui_bottom(40.0f), ui->status); 352 snprintf(text, sizeof(text), "%s SELECT", 353 labels->accept); 354 ph_render_text(renderer, 24.0f, ph_ui_bottom(24.0f), text); 355 if (ui->selected >= 0 && ui->mode != PH_INVENTORY_BROWSE) 356 ph_draw_inventory_popup(renderer, world, ui); 357 } 358 359 static void 360 ph_draw_spellbook_popup(PhRenderer *renderer, const PhSpellBook *spells, 361 const PhSpellbookUi *ui) 362 { 363 const PhSpellDef *spell = ph_spellbook_def(spells, 364 spells->learned[ui->selected]); 365 PhRect popup = ui->mode == PH_SPELLBOOK_EXAMINE ? 366 (PhRect){ 40.0f, 48.0f, PH_VIEW_W - 64.0f, 124.0f } : 367 (PhRect){ PH_VIEW_W - 164.0f, 48.0f, 140.0f, 82.0f }; 368 char text[96]; 369 int action; 370 371 if (!spell) return; 372 ph_render_color(renderer, (PhColor){ PH_MENU_COLOR }); 373 ph_render_fill(renderer, popup); 374 ph_render_color(renderer, (PhColor){ PH_MENU_BORDER_COLOR }); 375 ph_render_outline(renderer, popup); 376 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 377 if (ui->mode == PH_SPELLBOOK_EXAMINE) { 378 ph_render_text(renderer, 52.0f, 58.0f, spell->name); 379 ph_draw_wrapped_text(renderer, 52.0f, 76.0f, popup.w - 24.0f, 12.0f, 380 spell->description ? spell->description : "No description."); 381 snprintf(text, sizeof(text), "%d MP POWER %d DELAY %d%%", 382 spell->mp_cost, spell->power, spell->delay_percent); 383 ph_render_text(renderer, 52.0f, 126.0f, text); 384 ph_render_text(renderer, 52.0f, 148.0f, "ENTER BACK"); 385 return; 386 } 387 ph_render_text(renderer, ph_ui_right(152.0f), 58.0f, "SPELL ACTION"); 388 for (action = 0; action < PH_SPELLBOOK_ACTION_COUNT; ++action) { 389 if (ph_spellbook_action_enabled(spell, action)) 390 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 391 else 392 ph_render_color(renderer, (PhColor){ PH_MENU_DISABLED_COLOR }); 393 snprintf(text, sizeof(text), "%c %s", ui->option == action ? '>' : ' ', 394 action == PH_SPELLBOOK_USE ? "USE" : "EXAMINE"); 395 ph_render_text(renderer, ph_ui_right(152.0f), 396 78.0f + (float)action * 18.0f, text); 397 } 398 } 399 400 static void 401 ph_draw_spellbook(PhRenderer *renderer, const PhWorld *world, 402 const PhSpellBook *spells, const PhSpellbookUi *ui, const PhControlLabels *labels) 403 { 404 const PhEntity *player = ph_world_player(world); 405 PhStats stats = ph_world_entity_stats(world, player); 406 char text[96]; 407 int page = ui->selected < 0 ? 0 : ui->selected / 6; 408 int i; 409 410 ph_draw_menu(renderer, "MAGICK BOOK", labels->spellbook, labels->back); 411 if (player) { 412 snprintf(text, sizeof(text), "MP %d/%d", player->vitals.mp, stats.max_mp); 413 ph_render_text(renderer, ph_ui_right(96.0f), 22.0f, text); 414 } 415 for (i = page * 6; i < spells->learned_count && i < page * 6 + 6; ++i) { 416 const PhSpellDef *spell = ph_spellbook_def(spells, spells->learned[i]); 417 float y = 48.0f + (float)(i - page * 6) * 24.0f; 418 419 if (!spell) continue; 420 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 421 snprintf(text, sizeof(text), "%c %-14s %2d MP", 422 ui->selected == i ? '>' : ' ', spell->name, spell->mp_cost); 423 ph_render_text(renderer, 24.0f, y, text); 424 ph_render_color(renderer, (PhColor){ PH_MENU_BONUS_COLOR }); 425 snprintf(text, sizeof(text), "%s POWER %d DELAY %d%%", 426 spell->field_use != PH_SPELL_FIELD_NONE ? "FIELD" : "BATTLE", 427 spell->power, spell->delay_percent); 428 ph_render_text(renderer, 40.0f, y + 10.0f, text); 429 } 430 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 431 if (spells->learned_count <= 0) 432 ph_render_text(renderer, 24.0f, 48.0f, "NO SPELLS LEARNED"); 433 if (spells->learned_count > 6) { 434 snprintf(text, sizeof(text), "PAGE %d/%d", page + 1, 435 (spells->learned_count + 5) / 6); 436 ph_render_text(renderer, 24.0f, ph_ui_bottom(56.0f), text); 437 } 438 if (ui->status[0]) 439 ph_render_text(renderer, 24.0f, ph_ui_bottom(40.0f), ui->status); 440 ph_render_text(renderer, 24.0f, ph_ui_bottom(24.0f), "ENTER SELECT"); 441 if (ui->selected >= 0 && ui->mode != PH_SPELLBOOK_BROWSE) 442 ph_draw_spellbook_popup(renderer, spells, ui); 443 } 444 445 static void 446 ph_draw_shop(PhRenderer *renderer, const PhWorld *world, 447 const PhSpellBook *spells, const PhShopUi *ui, const PhControlLabels *labels) 448 { 449 static const char *item_options[] = { "BUY", "SELL", "TALK" }; 450 static const char *magick_options[] = { "LEARN", "TALK" }; 451 const PhShopDef *shop = ph_game_shop_def(ui->shop_id); 452 const PhEntity *player = ph_world_player(world); 453 char text[128]; 454 int i; 455 456 if (!shop || !player) return; 457 ph_draw_menu(renderer, shop->name, labels->cancel, labels->back); 458 snprintf(text, sizeof(text), "GOLD %d", player->gold); 459 ph_render_text(renderer, ph_ui_right(96.0f), 38.0f, text); 460 if (ui->mode == PH_SHOP_ROOT) { 461 const char **options = shop->kind == PH_SHOP_ITEMS ? 462 item_options : magick_options; 463 int option_count = shop->kind == PH_SHOP_ITEMS ? 464 (int)LEN(item_options) : (int)LEN(magick_options); 465 466 for (i = 0; i < option_count; ++i) { 467 snprintf(text, sizeof(text), "%c %s", ui->selected == i ? '>' : ' ', 468 options[i]); 469 ph_render_text(renderer, 32.0f, 56.0f + (float)i * 20.0f, text); 470 } 471 ph_render_text(renderer, 32.0f, 132.0f, 472 shop->kind == PH_SHOP_ITEMS ? "Welcome, traveler." : 473 "Choose your lesson."); 474 } else if (ui->mode == PH_SHOP_BUY) { 475 ph_render_text(renderer, 24.0f, 42.0f, 476 shop->kind == PH_SHOP_ITEMS ? "BUY" : "MAGICK"); 477 for (i = 0; i < shop->stock_count; ++i) { 478 if (shop->kind == PH_SHOP_SPELLS) { 479 const PhSpellDef *spell = ph_spellbook_def(spells, shop->stock[i]); 480 481 if (!spell) continue; 482 if (ph_spellbook_has(spells, spell->id)) 483 snprintf(text, sizeof(text), "%c %-12s LEARNED", 484 ui->selected == i ? '>' : ' ', spell->name); 485 else 486 snprintf(text, sizeof(text), "%c %-12s %4d G %2d MP", 487 ui->selected == i ? '>' : ' ', spell->name, 488 spell->buy_value, spell->mp_cost); 489 } else { 490 const PhItemDef *item = ph_world_item_def(world, shop->stock[i]); 491 492 if (!item) continue; 493 snprintf(text, sizeof(text), "%c %-18s %d G", 494 ui->selected == i ? '>' : ' ', item->name, item->buy_value); 495 } 496 ph_render_text(renderer, 24.0f, 62.0f + (float)i * 20.0f, text); 497 } 498 } else if (ui->mode == PH_SHOP_SELL) { 499 int row = 0; 500 501 ph_render_text(renderer, 24.0f, 42.0f, "SELL"); 502 for (i = 0; i < world->content.item_count; ++i) { 503 const PhItemDef *item = &world->content.items[i]; 504 505 if (world->inventory[i] <= 0) continue; 506 snprintf(text, sizeof(text), "%c %-16s x%d %d G", 507 ui->selected == i ? '>' : ' ', item->name, world->inventory[i], 508 item->sell_value); 509 ph_render_text(renderer, 24.0f, 62.0f + (float)row++ * 18.0f, text); 510 } 511 if (!row) ph_render_text(renderer, 24.0f, 62.0f, "Nothing to sell."); 512 } else { 513 float y = 48.0f; 514 float width = ph_ui_right(24.0f) - 24.0f; 515 516 for (i = 0; i < PH_SHOP_TALK_LINES; ++i) { 517 y = ph_draw_wrapped_text(renderer, 24.0f, y, width, 12.0f, 518 shop->talk[i]); 519 y += 6.0f; 520 } 521 ph_render_text(renderer, 24.0f, y + 8.0f, "ENTER / ESC BACK"); 522 } 523 if (ui->status[0]) 524 ph_render_text(renderer, 24.0f, ph_ui_bottom(36.0f), ui->status); 525 } 526 527 static void 528 ph_draw_battle_entity(PhRenderer *renderer, 529 const PhWorld *world, int entity_index, PhRect rect) 530 { 531 const PhEntity *entity = &world->entities[entity_index]; 532 const PhEntityDef *def = ph_world_entity_def(world, entity->type_id); 533 int asset_id = -1; 534 int x; 535 int y; 536 PhFlip flip; 537 538 if (!def) return; 539 asset_id = def->asset_id; 540 if (def->kind == PH_ENTITY_PLAYER) { 541 x = def->sprite_tiles[PH_SPRITE_SIDE][0][0]; 542 y = def->sprite_tiles[PH_SPRITE_SIDE][0][1]; 543 flip = PH_FLIP_NONE; 544 } else { 545 ph_render_entity_frame(def, entity, &x, &y, &flip); 546 } 547 if (ph_render_has_asset(renderer, asset_id)) ph_render_sprite(renderer, asset_id, x, y, rect, flip); 548 else { 549 if (def->kind == PH_ENTITY_MONSTER) 550 ph_render_color(renderer, (PhColor){ PH_MONSTER_COLOR }); 551 else 552 ph_render_color(renderer, (PhColor){ PH_NPC_COLOR }); 553 ph_render_fill(renderer, rect); 554 } 555 } 556 557 static void 558 ph_draw_battle(PhRenderer *renderer, 559 const PhWorld *world, const PhBattle *battle) 560 { 561 static const char *commands[PH_BATTLE_COMMAND_COUNT] = { 562 [PH_BATTLE_FIGHT] = "FIGHT", 563 [PH_BATTLE_MAGICK] = "MAGICK", 564 [PH_BATTLE_ITEM] = "ITEM", 565 }; 566 const PhEntity *player = ph_world_player(world); 567 const PhEntity *enemy = &world->entities[battle->enemy_index]; 568 const PhEntityDef *enemy_def = ph_world_entity_def(world, enemy->type_id); 569 PhStats player_stats = ph_world_entity_stats(world, player); 570 PhStats enemy_stats = ph_world_entity_stats(world, enemy); 571 int order[PH_BATTLE_ORDER_COUNT]; 572 PhRect ground = { 0.0f, PH_VIEW_H - 136.0f, PH_VIEW_W, 72.0f }; 573 PhRect panel = { 8.0f, PH_VIEW_H - 64.0f, PH_VIEW_W - 16.0f, 56.0f }; 574 PhRect spell_panel = { PH_VIEW_W * 0.5f - 88.0f, 575 PH_VIEW_H - 116.0f, 176.0f, 576 24.0f + (float)battle->spells.learned_count * 12.0f }; 577 PhRect enemy_rect = { 56.0f + ph_battle_lunge(battle, 578 PH_BATTLE_ENEMY_ACTION), PH_VIEW_H - 172.0f, 579 PH_TILE_SIZE * 3.0f, PH_TILE_SIZE * 3.0f }; 580 PhRect player_rect = { PH_VIEW_W - 88.0f - ph_battle_lunge(battle, 581 PH_BATTLE_PLAYER_ACTION), PH_VIEW_H - 120.0f, 582 PH_TILE_SIZE * 3.0f, PH_TILE_SIZE * 3.0f }; 583 char text[96]; 584 int i; 585 586 ph_render_color(renderer, (PhColor){ PH_BATTLE_BG_COLOR }); 587 ph_render_clear(renderer); 588 ph_render_color(renderer, (PhColor){ PH_BATTLE_GROUND_COLOR }); 589 ph_render_fill(renderer, ground); 590 ph_draw_battle_entity(renderer, world, battle->enemy_index, enemy_rect); 591 ph_draw_battle_entity(renderer, world, world->player_index, player_rect); 592 if (battle->phase == PH_BATTLE_TARGET) { 593 PhRect target_rect = battle->choice.target_index == world->player_index ? 594 player_rect : enemy_rect; 595 596 target_rect.x -= 3.0f; 597 target_rect.y -= 3.0f; 598 target_rect.w += 6.0f; 599 target_rect.h += 6.0f; 600 ph_render_color(renderer, (PhColor){ PH_MENU_BONUS_COLOR }); 601 ph_render_outline(renderer, target_rect); 602 } 603 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 604 ph_render_text(renderer, 16.0f, 18.0f, 605 enemy_def && enemy_def->name ? enemy_def->name : "ENEMY"); 606 snprintf(text, sizeof(text), "HP %d/%d", enemy->vitals.hp, enemy_stats.max_hp); 607 ph_render_text(renderer, 16.0f, 30.0f, text); 608 if (enemy->accuracy.turns > 0) { 609 snprintf(text, sizeof(text), "ACC -%d%% (%d)", enemy->accuracy.value, 610 enemy->accuracy.turns); 611 ph_render_text(renderer, 16.0f, 42.0f, text); 612 } 613 ph_battle_order(battle, world, order); 614 ph_render_text(renderer, ph_ui_right(96.0f), 18.0f, "TURN ORDER"); 615 for (i = 0; i < PH_BATTLE_ORDER_COUNT; ++i) { 616 snprintf(text, sizeof(text), "%d %s", i + 1, 617 order[i] == world->player_index ? "YOU" : "ENEMY"); 618 ph_render_text(renderer, ph_ui_right(80.0f), 619 30.0f + (float)i * 11.0f, text); 620 } 621 ph_render_color(renderer, (PhColor){ PH_MENU_COLOR }); 622 ph_render_fill(renderer, panel); 623 ph_render_color(renderer, (PhColor){ PH_MENU_BORDER_COLOR }); 624 ph_render_outline(renderer, panel); 625 for (i = 0; i < (int)LEN(commands); ++i) { 626 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 627 snprintf(text, sizeof(text), "%c %s", 628 (battle->phase == PH_BATTLE_COMMAND || battle->phase == PH_BATTLE_MAGIC || 629 battle->phase == PH_BATTLE_TARGET) && 630 battle->choice.command == (PhBattleCommand)i ? '>' : ' ', 631 commands[i]); 632 ph_render_text(renderer, 18.0f, 633 panel.y + 8.0f + (float)i * 14.0f, text); 634 } 635 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 636 ph_render_text(renderer, 104.0f, panel.y + 8.0f, battle->message); 637 if (battle->phase == PH_BATTLE_TARGET) { 638 const PhEntityDef *target_def = ph_world_entity_def(world, 639 world->entities[battle->choice.target_index].type_id); 640 641 snprintf(text, sizeof(text), "TARGET: %s ENTER CONFIRM", 642 target_def && target_def->name ? target_def->name : "UNKNOWN"); 643 ph_render_text(renderer, 104.0f, panel.y + 24.0f, text); 644 } 645 snprintf(text, sizeof(text), "HP %d/%d", player->vitals.hp, player_stats.max_hp); 646 ph_render_text(renderer, ph_ui_right(80.0f), ph_ui_bottom(36.0f), text); 647 snprintf(text, sizeof(text), "MP %d/%d", player->vitals.mp, player_stats.max_mp); 648 ph_render_text(renderer, ph_ui_right(80.0f), ph_ui_bottom(24.0f), text); 649 if (battle->phase == PH_BATTLE_MAGIC) { 650 ph_render_color(renderer, (PhColor){ PH_MENU_COLOR }); 651 ph_render_fill(renderer, spell_panel); 652 ph_render_color(renderer, (PhColor){ PH_MENU_BORDER_COLOR }); 653 ph_render_outline(renderer, spell_panel); 654 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 655 ph_render_text(renderer, spell_panel.x + 8.0f, 656 spell_panel.y + 6.0f, "MAGICK"); 657 for (i = 0; i < battle->spells.learned_count; ++i) { 658 const PhSpellDef *choice = ph_spell_def(battle, 659 battle->spells.learned[i]); 660 661 if (!choice) continue; 662 if (ph_spell_battle_usable(choice)) 663 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 664 else 665 ph_render_color(renderer, (PhColor){ PH_MENU_DISABLED_COLOR }); 666 snprintf(text, sizeof(text), "%c %s %dMP%s", 667 battle->choice.spell == i ? '>' : ' ', choice->name, choice->mp_cost, 668 choice->delay_percent < 100 ? " QUICK" : ""); 669 ph_render_text(renderer, spell_panel.x + 8.0f, 670 spell_panel.y + 20.0f + (float)i * 12.0f, text); 671 } 672 } 673 } 674 675 static void 676 ph_draw_battle_end(PhRenderer *renderer, const PhWorld *world, 677 const PhBattle *battle, PhGameMode game) 678 { 679 const PhItemDef *loot = ph_world_item_def(world, battle->reward.item_id); 680 char text[96]; 681 682 if (game == PH_GAME_VICTORY) 683 ph_render_color(renderer, (PhColor){ PH_BATTLE_BG_COLOR }); 684 else 685 ph_render_color(renderer, (PhColor){ PH_CLEAR_COLOR }); 686 ph_render_clear(renderer); 687 ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR }); 688 if (game == PH_GAME_OVER) { 689 ph_render_text(renderer, ph_ui_center_text("GAME OVER"), 690 PH_VIEW_H * 0.5f - 12.0f, "GAME OVER"); 691 return; 692 } 693 ph_render_text(renderer, ph_ui_center_text("VICTORY!"), 694 PH_VIEW_H * 0.5f - 44.0f, "VICTORY!"); 695 snprintf(text, sizeof(text), "XP GAINED %d", battle->reward.xp); 696 ph_render_text(renderer, ph_ui_center_text(text), 697 PH_VIEW_H * 0.5f - 14.0f, text); 698 if (loot && battle->reward.amount > 0) 699 snprintf(text, sizeof(text), "LOOT %s x%d", loot->name, 700 battle->reward.amount); 701 else 702 snprintf(text, sizeof(text), "LOOT NONE"); 703 ph_render_text(renderer, ph_ui_center_text(text), 704 PH_VIEW_H * 0.5f + 4.0f, text); 705 ph_render_text(renderer, ph_ui_center_text("ENTER TO CONTINUE"), 706 PH_VIEW_H * 0.5f + 44.0f, "ENTER TO CONTINUE"); 707 } 708 709 static void 710 ph_draw_debug_stats(PhRenderer *renderer, const PhWorld *world, 711 const PhDebugView *debug) 712 { 713 char text[96]; 714 715 if (!debug || !debug->enabled) return; 716 ph_render_color(renderer, (PhColor){ PH_DEBUG_BG_COLOR }); 717 ph_render_fill(renderer, (PhRect){ 0, 0, PH_VIEW_W, 40 }); 718 ph_render_color(renderer, (PhColor){ PH_DEBUG_TEXT_COLOR }); 719 snprintf(text, sizeof(text), "FRAME %.2f MS TICKS %d", debug->frame_ms, debug->steps); 720 ph_render_text(renderer, 4, 2, text); 721 snprintf(text, sizeof(text), "UPDATE %.2f DRAW %.2f MS", debug->update_ms, debug->draw_ms); 722 ph_render_text(renderer, 4, 14, text); 723 snprintf(text, sizeof(text), "EVENTS %d/%d LOST %u", world->event_count, 724 PH_MAX_WORLD_EVENTS, world->event_dropped); 725 ph_render_text(renderer, 4, 26, text); 726 } 727 728 void 729 ph_present_frame(PhRenderer *renderer, 730 const PhGameContext *context, const PhControlLabels *labels, const PhDebugView *debug) 731 { 732 const PhWorld *world = context->world; 733 const PhBattle *battle = context->battle; 734 PhGameMode game = *context->mode; 735 PhCamera camera = { .viewport_w = PH_VIEW_W, .viewport_h = PH_VIEW_H }; 736 737 if (game == PH_GAME_BATTLE) { 738 ph_draw_battle(renderer, world, battle); 739 if (context->menu == PH_MENU_MAIN) 740 ph_draw_options(renderer, context->options); 741 ph_draw_debug_stats(renderer, world, debug); 742 ph_render_present(renderer); 743 return; 744 } 745 if (game == PH_GAME_VICTORY || game == PH_GAME_OVER) { 746 ph_draw_battle_end(renderer, world, battle, game); 747 if (context->menu == PH_MENU_MAIN) 748 ph_draw_options(renderer, context->options); 749 ph_draw_debug_stats(renderer, world, debug); 750 ph_render_present(renderer); 751 return; 752 } 753 754 ph_render_color(renderer, (PhColor){ PH_CLEAR_COLOR }); 755 ph_render_clear(renderer); 756 ph_camera_follow_player(&camera, world); 757 ph_render_world(renderer, world, &camera); 758 if (debug && debug->enabled) ph_render_world_debug(renderer, world, &camera); 759 if (context->menu == PH_MENU_CHARACTER) { 760 ph_draw_character_sheet(renderer, world, labels); 761 } else if (context->menu == PH_MENU_INVENTORY) { 762 ph_draw_inventory(renderer, world, context->inventory, labels); 763 } else if (context->menu == PH_MENU_SPELLBOOK) { 764 ph_draw_spellbook(renderer, world, context->spells, context->spellbook, labels); 765 } else if (context->menu == PH_MENU_SHOP) { 766 ph_draw_shop(renderer, world, context->spells, context->shop, labels); 767 } else if (context->menu == PH_MENU_MAIN) { 768 ph_draw_options(renderer, context->options); 769 } else if (context->menu == PH_MENU_START) { 770 ph_draw_start(renderer, context->start); 771 } else if (world->notice.seconds > 0.0f && world->notice.text[0] != '\0') { 772 PhRect notice_bg = { 773 .x = 8.0f, 774 .y = (float)(PH_VIEW_H - 28), 775 .w = (float)(PH_VIEW_W - 16), 776 .h = 20.0f, 777 }; 778 779 ph_render_color(renderer, (PhColor){ PH_NOTICE_COLOR }); 780 ph_render_fill(renderer, notice_bg); 781 ph_render_color(renderer, (PhColor){ PH_NOTICE_TEXT_COLOR }); 782 ph_render_text(renderer, 14.0f, (float)(PH_VIEW_H - 22), 783 world->notice.text); 784 } 785 if (game == PH_GAME_TRANSITION) { 786 float progress = 1.0f - battle->timer / PH_BATTLE_TRANSITION_SECONDS; 787 float height = progress * PH_VIEW_H * 0.5f; 788 PhRect top = { 0.0f, 0.0f, PH_VIEW_W, height }; 789 PhRect bottom = { 0.0f, PH_VIEW_H - height, PH_VIEW_W, height }; 790 791 ph_render_color(renderer, (PhColor){ 0, 0, 0, 255 }); 792 ph_render_fill(renderer, top); 793 ph_render_fill(renderer, bottom); 794 } 795 ph_draw_debug_stats(renderer, world, debug); 796 ph_render_present(renderer); 797 }