battle.c (15172B)
1 #include "engine/battle.h" 2 3 #include <stdio.h> 4 #include <string.h> 5 6 #include "config.h" 7 8 #if PH_BATTLE_CTB_BASE <= 0 || PH_BATTLE_RESOLVE_PERCENT < 0 || \ 9 PH_BATTLE_RESOLVE_PERCENT > 100 10 #error "invalid CTB configuration" 11 #endif 12 13 static int 14 ph_battle_actor_delay(const PhWorld *world, int actor) 15 { 16 PhStats stats = ph_world_entity_stats(world, &world->entities[actor]); 17 int delay = PH_BATTLE_CTB_BASE / (stats.agility > 0 ? stats.agility : 1); 18 19 return delay > 0 ? delay : 1; 20 } 21 22 static int 23 ph_battle_next_actor(const PhBattle *battle, const PhWorld *world, 24 int player_ctb, int enemy_ctb, int previous) 25 { 26 if (player_ctb < enemy_ctb) return world->player_index; 27 if (enemy_ctb < player_ctb) return battle->enemy_index; 28 return previous == world->player_index ? 29 battle->enemy_index : world->player_index; 30 } 31 32 static void 33 ph_battle_start_turn(PhBattle *battle, const PhWorld *world, int actor) 34 { 35 battle->turn.current_actor = actor; 36 battle->turn.action_delay = ph_battle_actor_delay(world, actor); 37 battle->resolved = 0; 38 if (actor == world->player_index) { 39 battle->phase = PH_BATTLE_COMMAND; 40 battle->timer = 0.0f; 41 } else { 42 battle->phase = PH_BATTLE_ENEMY_ACTION; 43 battle->timer = PH_BATTLE_ACTION_SECONDS; 44 } 45 } 46 47 static void 48 ph_battle_next_turn(PhBattle *battle, const PhWorld *world) 49 { 50 int previous = battle->turn.current_actor; 51 52 if (previous == world->player_index) 53 battle->turn.player_ctb += battle->turn.action_delay; 54 else battle->turn.enemy_ctb += battle->turn.action_delay; 55 ph_battle_start_turn(battle, world, ph_battle_next_actor(battle, world, 56 battle->turn.player_ctb, battle->turn.enemy_ctb, previous)); 57 } 58 59 static int 60 ph_battle_item(const PhWorld *world) 61 { 62 const PhEntity *player = ph_world_player(world); 63 PhStats stats = ph_world_entity_stats(world, player); 64 int i; 65 66 for (i = 0; i < world->content.item_count; ++i) 67 if (world->inventory[i] > 0 && player && 68 ((world->content.items[i].use == PH_ITEM_USE_HEAL && 69 player->vitals.hp < stats.max_hp) || 70 (world->content.items[i].use == PH_ITEM_USE_RESTORE_MP && 71 player->vitals.mp < stats.max_mp))) return world->content.items[i].id; 72 return 0; 73 } 74 75 const PhSpellDef * 76 ph_spellbook_def(const PhSpellBook *spells, int id) 77 { 78 int i; 79 80 if (!spells || spells->def_count < 0 || 81 (spells->def_count > 0 && !spells->defs)) return NULL; 82 for (i = 0; i < spells->def_count; ++i) 83 if (spells->defs[i].id == id) return &spells->defs[i]; 84 return NULL; 85 } 86 87 const PhSpellDef * 88 ph_spell_def(const PhBattle *battle, int id) 89 { 90 return ph_spellbook_def(&battle->spells, id); 91 } 92 93 int 94 ph_spellbook_has(const PhSpellBook *spells, int id) 95 { 96 int i; 97 98 if (!spells || spells->learned_count < 0 || 99 spells->learned_count > PH_SPELL_MAX) return 0; 100 for (i = 0; i < spells->learned_count; ++i) 101 if (spells->learned[i] == id) return 1; 102 return 0; 103 } 104 105 int 106 ph_spellbook_learn(PhSpellBook *spells, int id) 107 { 108 if (!spells || !ph_spellbook_def(spells, id) || ph_spellbook_has(spells, id) || 109 spells->learned_count < 0 || spells->learned_count >= PH_SPELL_MAX) 110 return -1; 111 spells->learned[spells->learned_count++] = id; 112 return 0; 113 } 114 115 int 116 ph_spellbook_buy(PhSpellBook *spells, PhWorld *world, int entity_index, 117 int spell_id) 118 { 119 const PhSpellDef *spell = ph_spellbook_def(spells, spell_id); 120 PhEntity *entity; 121 122 if (!spell || !world || entity_index < 0 || entity_index >= world->entity_count || 123 ph_spellbook_has(spells, spell_id)) return -1; 124 entity = &world->entities[entity_index]; 125 if (!entity->active || spell->buy_value < 0 || entity->gold < spell->buy_value || 126 ph_spellbook_learn(spells, spell_id) < 0) return -1; 127 entity->gold -= spell->buy_value; 128 ph_world_emit_event(world, PH_WORLD_EVENT_BUY, entity->pos, entity_index); 129 return 0; 130 } 131 132 static int 133 ph_spell_heal(PhWorld *world, int caster_index, int target_index, 134 const PhSpellDef *spell) 135 { 136 PhEntity *caster; 137 PhEntity *target; 138 PhStats stats; 139 int amount; 140 141 if (!world || !spell || spell->power <= 0 || spell->mp_cost < 0 || 142 caster_index < 0 || caster_index >= world->entity_count || 143 target_index < 0 || target_index >= world->entity_count) return -1; 144 caster = &world->entities[caster_index]; 145 target = &world->entities[target_index]; 146 stats = ph_world_entity_stats(world, target); 147 if (!caster->active || !target->active || caster->vitals.mp < spell->mp_cost || 148 target->vitals.hp >= stats.max_hp) return -1; 149 amount = stats.max_hp - target->vitals.hp; 150 if (amount > spell->power) amount = spell->power; 151 target->vitals.hp += amount; 152 caster->vitals.mp -= spell->mp_cost; 153 return amount; 154 } 155 156 int 157 ph_spellbook_use(const PhSpellBook *spells, PhWorld *world, int entity_index, 158 int spell_id) 159 { 160 const PhSpellDef *spell = ph_spellbook_def(spells, spell_id); 161 162 if (!spell || !ph_spellbook_has(spells, spell_id)) return -1; 163 if (spell->field_use == PH_SPELL_FIELD_HEAL) 164 return ph_spell_heal(world, entity_index, entity_index, spell); 165 return -1; 166 } 167 168 const PhSpellDef * 169 ph_battle_spell(const PhBattle *battle) 170 { 171 if (battle->choice.spell < 0 || battle->choice.spell >= battle->spells.learned_count) 172 return NULL; 173 return ph_spell_def(battle, battle->spells.learned[battle->choice.spell]); 174 } 175 176 int 177 ph_spell_battle_usable(const PhSpellDef *spell) 178 { 179 return spell && 180 (spell->battle_use == PH_SPELL_BATTLE_DAMAGE || 181 spell->battle_use == PH_SPELL_BATTLE_HEAL) && 182 (spell->target == PH_SPELL_TARGET_OPPONENT || 183 spell->target == PH_SPELL_TARGET_ALLY); 184 } 185 186 int 187 ph_battle_spell_step(PhBattle *battle, int direction) 188 { 189 int index; 190 int step; 191 192 if (!battle || direction == 0 || battle->spells.learned_count <= 0) return -1; 193 index = battle->choice.spell; 194 for (step = 0; step < battle->spells.learned_count; ++step) { 195 index += direction < 0 ? -1 : 1; 196 if (index < 0) index = battle->spells.learned_count - 1; 197 if (index >= battle->spells.learned_count) index = 0; 198 if (ph_spell_battle_usable(ph_spell_def(battle, 199 battle->spells.learned[index]))) { 200 battle->choice.spell = index; 201 return index; 202 } 203 } 204 return -1; 205 } 206 207 void 208 ph_battle_begin(PhBattle *battle, PhWorld *world, int enemy_index, 209 PhSpellBook spells) 210 { 211 const PhEntity *enemy = &world->entities[enemy_index]; 212 const PhEntityDef *def = ph_world_entity_def(world, enemy->type_id); 213 214 memset(battle, 0, sizeof(*battle)); 215 world->combat_active = 1; 216 battle->spells = spells; 217 battle->enemy_index = enemy_index; 218 battle->turn.current_actor = world->player_index; 219 battle->turn.enemy_ctb = ph_battle_actor_delay(world, enemy_index); 220 battle->timer = PH_BATTLE_TRANSITION_SECONDS; 221 if (def) { 222 battle->reward = def->reward; 223 snprintf(battle->message, sizeof(battle->message), "%s approaches!", def->name); 224 } 225 } 226 227 void 228 ph_battle_select(PhBattle *battle, PhWorld *world) 229 { 230 if (battle->phase != PH_BATTLE_COMMAND || !ph_world_player(world) || 231 battle->turn.current_actor != world->player_index) return; 232 if (battle->choice.command == PH_BATTLE_MAGICK) { 233 battle->choice.spell = -1; 234 if (ph_battle_spell_step(battle, 1) < 0) { 235 snprintf(battle->message, sizeof(battle->message), 236 "No combat spells."); 237 return; 238 } 239 battle->phase = PH_BATTLE_MAGIC; 240 snprintf(battle->message, sizeof(battle->message), "Choose a spell."); 241 return; 242 } 243 if (battle->choice.command == PH_BATTLE_ITEM && 244 !(battle->choice.item_id = ph_battle_item(world))) { 245 snprintf(battle->message, sizeof(battle->message), "No usable item."); 246 return; 247 } 248 battle->phase = PH_BATTLE_TARGET; 249 battle->choice.target_index = battle->choice.command == PH_BATTLE_FIGHT ? 250 battle->enemy_index : world->player_index; 251 snprintf(battle->message, sizeof(battle->message), "Choose a target."); 252 } 253 254 void 255 ph_battle_cast(PhBattle *battle, PhWorld *world) 256 { 257 const PhEntity *player = ph_world_player(world); 258 const PhSpellDef *spell = ph_battle_spell(battle); 259 260 if (battle->phase != PH_BATTLE_MAGIC || !player || 261 !ph_spell_battle_usable(spell)) return; 262 if (player->vitals.mp < spell->mp_cost) { 263 snprintf(battle->message, sizeof(battle->message), "Not enough MP."); 264 return; 265 } 266 battle->phase = PH_BATTLE_TARGET; 267 battle->choice.target_index = spell->target == PH_SPELL_TARGET_ALLY ? 268 world->player_index : battle->enemy_index; 269 snprintf(battle->message, sizeof(battle->message), "Choose a target."); 270 } 271 272 static int 273 ph_battle_target_valid(const PhBattle *battle, const PhWorld *world, int index) 274 { 275 const PhEntityDef *def; 276 277 if (index < 0 || index >= world->entity_count || 278 !world->entities[index].active || world->entities[index].vitals.hp <= 0) 279 return 0; 280 if (index == battle->enemy_index) return 1; 281 def = ph_world_entity_def(world, world->entities[index].type_id); 282 return def && def->kind == PH_ENTITY_PLAYER && 283 world->entities[index].area_id == world->area_id; 284 } 285 286 int 287 ph_battle_target_step(PhBattle *battle, const PhWorld *world, int direction) 288 { 289 int index; 290 int step; 291 292 if (!battle || !world || battle->phase != PH_BATTLE_TARGET || 293 direction == 0) return -1; 294 index = battle->choice.target_index; 295 for (step = 0; step < world->entity_count; ++step) { 296 index += direction < 0 ? -1 : 1; 297 if (index < 0) index = world->entity_count - 1; 298 if (index >= world->entity_count) index = 0; 299 if (ph_battle_target_valid(battle, world, index)) { 300 battle->choice.target_index = index; 301 return index; 302 } 303 } 304 return -1; 305 } 306 307 void 308 ph_battle_confirm_target(PhBattle *battle, PhWorld *world) 309 { 310 const PhSpellDef *spell = ph_battle_spell(battle); 311 312 if (!battle || !world || battle->phase != PH_BATTLE_TARGET || 313 !ph_battle_target_valid(battle, world, 314 battle->choice.target_index)) return; 315 battle->phase = PH_BATTLE_PLAYER_ACTION; 316 battle->timer = PH_BATTLE_ACTION_SECONDS; 317 battle->resolved = 0; 318 battle->turn.action_delay = ph_battle_actor_delay(world, world->player_index); 319 if (battle->choice.command == PH_BATTLE_MAGICK && spell) 320 battle->turn.action_delay = battle->turn.action_delay * 321 spell->delay_percent / 100; 322 if (battle->turn.action_delay < 1) battle->turn.action_delay = 1; 323 } 324 325 void 326 ph_battle_cancel(PhBattle *battle) 327 { 328 if (battle->phase == PH_BATTLE_TARGET) 329 battle->phase = battle->choice.command == PH_BATTLE_MAGICK ? 330 PH_BATTLE_MAGIC : PH_BATTLE_COMMAND; 331 else if (battle->phase == PH_BATTLE_MAGIC) 332 battle->phase = PH_BATTLE_COMMAND; 333 } 334 335 void 336 ph_battle_order(const PhBattle *battle, const PhWorld *world, 337 int order[PH_BATTLE_ORDER_COUNT]) 338 { 339 int player_ctb = battle->turn.player_ctb; 340 int enemy_ctb = battle->turn.enemy_ctb; 341 int actor = battle->turn.current_actor; 342 int i; 343 344 for (i = 0; i < PH_BATTLE_ORDER_COUNT; ++i) { 345 int delay = ph_battle_actor_delay(world, actor); 346 const PhSpellDef *spell = ph_battle_spell(battle); 347 348 order[i] = actor; 349 if (i == 0 && actor == world->player_index) { 350 if (battle->phase == PH_BATTLE_PLAYER_ACTION) 351 delay = battle->turn.action_delay; 352 else if (spell && (battle->phase == PH_BATTLE_MAGIC || 353 battle->phase == PH_BATTLE_TARGET)) 354 delay = delay * spell->delay_percent / 100; 355 if (delay < 1) delay = 1; 356 } 357 if (actor == world->player_index) player_ctb += delay; 358 else enemy_ctb += delay; 359 actor = ph_battle_next_actor(battle, world, player_ctb, enemy_ctb, actor); 360 } 361 } 362 363 static void 364 ph_battle_resolve(PhBattle *battle, PhWorld *world) 365 { 366 const PhEntityDef *def; 367 const PhSpellDef *spell = NULL; 368 int damage; 369 370 if (battle->phase == PH_BATTLE_PLAYER_ACTION) { 371 def = ph_world_entity_def(world, 372 world->entities[battle->choice.target_index].type_id); 373 if (battle->choice.command == PH_BATTLE_FIGHT) { 374 battle->event = PH_BATTLE_EVENT_ATTACK; 375 damage = ph_world_physical_attack(world, world->player_index, 376 battle->choice.target_index); 377 } else if (battle->choice.command == PH_BATTLE_MAGICK) { 378 battle->event = PH_BATTLE_EVENT_SPELL; 379 spell = ph_battle_spell(battle); 380 if (!spell) return; 381 if (spell->battle_use == PH_SPELL_BATTLE_DAMAGE) { 382 damage = ph_world_magic_attack(world, world->player_index, 383 battle->choice.target_index, spell->mp_cost, spell->power); 384 if (damage >= 0 && spell->accuracy.value > 0 && 385 spell->accuracy.turns > 0) 386 ph_world_apply_accuracy_penalty(world, battle->choice.target_index, 387 spell->accuracy.value, spell->accuracy.turns); 388 } else if (spell->battle_use == PH_SPELL_BATTLE_HEAL) { 389 damage = ph_spell_heal(world, world->player_index, 390 battle->choice.target_index, spell); 391 snprintf(battle->message, sizeof(battle->message), 392 damage >= 0 ? "%s recovers %d HP." : "%s is unaffected.", 393 def && def->name ? def->name : "Target", damage); 394 return; 395 } else return; 396 } else { 397 const PhItemDef *item = ph_world_item_def(world, battle->choice.item_id); 398 399 damage = ph_world_use_inventory_item(world, battle->choice.target_index, 400 battle->choice.item_id) < 0 ? -1 : 0; 401 snprintf(battle->message, sizeof(battle->message), "Used %s.", 402 item && item->name ? item->name : "item"); 403 return; 404 } 405 if (battle->choice.command == PH_BATTLE_MAGICK && damage >= 0) 406 snprintf(battle->message, sizeof(battle->message), 407 "%s deals %d damage.", spell->name, damage); 408 else 409 snprintf(battle->message, sizeof(battle->message), damage > 0 ? 410 "%s takes %d damage." : "The attack misses.", 411 def && def->name ? def->name : "Enemy", damage); 412 return; 413 } 414 415 def = ph_world_entity_def(world, world->entities[battle->enemy_index].type_id); 416 battle->event = PH_BATTLE_EVENT_HIT; 417 damage = ph_world_physical_attack(world, battle->enemy_index, world->player_index); 418 ph_world_advance_effects(world, battle->enemy_index); 419 snprintf(battle->message, sizeof(battle->message), damage > 0 ? 420 "%s deals %d damage." : "%s misses.", 421 def && def->name ? def->name : "Enemy", damage); 422 } 423 424 PhBattleEvent 425 ph_battle_take_event(PhBattle *battle) 426 { 427 PhBattleEvent event; 428 429 if (!battle) return PH_BATTLE_EVENT_NONE; 430 event = battle->event; 431 battle->event = PH_BATTLE_EVENT_NONE; 432 return event; 433 } 434 435 PhBattleResult 436 ph_battle_update(PhBattle *battle, PhWorld *world, float dt, int transition) 437 { 438 if (transition) { 439 battle->timer -= dt; 440 if (battle->timer > 0.0f) return PH_BATTLE_CONTINUE; 441 battle->timer = 0.0f; 442 ph_battle_start_turn(battle, world, world->player_index); 443 return PH_BATTLE_READY; 444 } 445 if (battle->phase == PH_BATTLE_COMMAND || battle->phase == PH_BATTLE_MAGIC || 446 battle->phase == PH_BATTLE_TARGET) 447 return PH_BATTLE_CONTINUE; 448 battle->timer -= dt; 449 if (!battle->resolved && battle->timer <= PH_BATTLE_ACTION_SECONDS * 450 PH_BATTLE_RESOLVE_PERCENT / 100.0f) { 451 ph_battle_resolve(battle, world); 452 battle->resolved = 1; 453 } 454 if (battle->timer > 0.0f) return PH_BATTLE_CONTINUE; 455 if (battle->phase == PH_BATTLE_PLAYER_ACTION && 456 world->entities[battle->enemy_index].vitals.hp <= 0) { 457 if (ph_world_finish_encounter(world, battle->enemy_index) == 0) 458 return PH_BATTLE_VICTORY; 459 return PH_BATTLE_CONTINUE; 460 } 461 if (battle->phase == PH_BATTLE_ENEMY_ACTION && 462 world->entities[world->player_index].vitals.hp <= 0) 463 return PH_BATTLE_DEFEAT; 464 ph_battle_next_turn(battle, world); 465 return PH_BATTLE_CONTINUE; 466 } 467 468 float 469 ph_battle_lunge(const PhBattle *battle, PhBattlePhase phase) 470 { 471 float progress; 472 473 if (battle->phase != phase || PH_BATTLE_ACTION_SECONDS <= 0.0f) return 0.0f; 474 progress = 1.0f - battle->timer / PH_BATTLE_ACTION_SECONDS; 475 return (progress < 0.5f ? progress : 1.0f - progress) * 476 PH_BATTLE_LUNGE_DISTANCE; 477 }