phantasia

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

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 }