/* * Copyright (C) 2002-2007 The Warp Rogue Team * Part of the Warp Rogue Project * * This software is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License. * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY. * * See the license.txt file for more details. */ /* * Module Name: AI * Description: AI routines */ #define Uses_Character #define Uses_Stats #define Uses_Movement #define Uses_Util #define Uses_Pathfinder #define Uses_Actions #define Uses_Area #define Uses_Sector #define Uses_Object #define Uses_Inventory #define Uses_Faction #define Uses_Combat #define Uses_Perception #define Uses_Random #define Uses_Psychic #define Uses_ProgramManager #include "mheader.h" #include "ai.h" /* * number of AI states */ #define MAX_AI_STATES 7 /* * AI decision probabilities (1% - 100% chance) */ #define AI_SWITCH_TO_GUARD_PROBABILITY 1 #define AI_SWITCH_TO_PATROL_PROBABILITY 2 /* * the maximal distance between the AI and its target the AI will * tolerate while in "Follow" mode */ #define AI_MAX_FOLLOW_DISTANCE 1 static void ai_reset_data(void); static void ai_state_wait(CHARACTER *); static void ai_state_guard(CHARACTER *); static void ai_state_patrol(CHARACTER *); static void ai_state_follow(CHARACTER *); static void ai_state_flee(CHARACTER *); static void ai_state_search(CHARACTER *); static void ai_state_combat(CHARACTER *); static void ai_set_target(CHARACTER *, const AREA_POINT *); static bool ai_has_target(const CHARACTER *); static void ai_erase_target(CHARACTER *); static AREA_DISTANCE ai_target_distance(const CHARACTER *); static void ai_fix_self(CHARACTER *); static void ai_use_drugs_to_fix_self(CHARACTER *); static void ai_use_stealth(CHARACTER *); static void ai_get_ready(CHARACTER *); static bool ai_attack(CHARACTER *); static bool ai_attack_type_choice(const CHARACTER *); static bool ai_run_towards_target(CHARACTER *); static void ai_switch_weapon(CHARACTER *, bool); static void ai_switch_weapon_ranged_combat(CHARACTER *); static void ai_switch_weapon_close_combat(CHARACTER *); static void ai_fix_weapon(CHARACTER *); static void ai_ranged_attack(CHARACTER *); static void ai_choose_firing_mode(const CHARACTER *, FIRING_DATA * ); static void ai_close_combat_attack(CHARACTER *); static bool ai_is_close_to_pcc(const CHARACTER *); static void ai_psyker(CHARACTER *); static PSY_POWER ai_choose_psy_power(CHARACTER *); static bool ai_power_is_sensible_choice(PSY_POWER, const AREA_POINT * ); static int ai_psychic_action_probability(const CHARACTER *); static void ai_flee(AREA_POINT *, const AREA_POINT *); /* * AI states */ static void (* const AiState[MAX_AI_STATES])(CHARACTER *) = { ai_state_wait, ai_state_guard, ai_state_patrol, ai_state_follow, ai_state_flee, ai_state_search, ai_state_combat }; /* * AI tactic names */ static const char * AiTacticName[MAX_AI_TACTICS] = { "Plan A!", "Get ready!", "Stay close!", "Hold position!" }; /* * AI data */ static bool AiFollowFailed; /* * lets the AI control the passed character */ void ai_control(CHARACTER *character) { ai_reset_data(); ai_fix_self(character); while (!character->action_spent) { (*AiState[character->ai.state])(character); } } /* * sets the state of the AI */ void ai_set_state(CHARACTER *character, AI_STATE new_state) { if (character->ai.state == AI_STATE_COMBAT && new_state == AI_STATE_SEARCH) { /* DO NOTHING */ } else { ai_erase_target(character); } character->ai.state = new_state; } /* * sets the default state of the AI */ void ai_set_default_state(CHARACTER *character, AI_STATE new_state) { character->ai.default_state = new_state; } /* * sets the tactic of the AI */ void ai_set_tactic(CHARACTER *character, AI_TACTIC tactic) { character->ai.tactic = tactic; if (tactic == TACTIC_GET_READY || tactic == TACTIC_HOLD_POSITION) { ai_set_state(character, AI_STATE_GUARD); ai_set_default_state(character, AI_STATE_GUARD); } else { ai_set_state(character, AI_STATE_FOLLOW); ai_set_default_state(character, AI_STATE_FOLLOW); } } /* * returns the name of the passed AI tactic */ const char * ai_tactic_name(AI_TACTIC tactic) { return AiTacticName[tactic]; } /* * AI: reset data */ static void ai_reset_data(void) { AiFollowFailed = false; } /* * AI state: wait */ static void ai_state_wait(CHARACTER *character) { if (enemies_noticed(character)) { ai_set_state(character, AI_STATE_COMBAT); return; } ai_get_ready(character); } /* * AI state: guard */ static void ai_state_guard(CHARACTER *character) { if (enemies_noticed(character)) { ai_set_state(character, AI_STATE_COMBAT); return; } if (character->party != PARTY_PLAYER && random_choice(AI_SWITCH_TO_PATROL_PROBABILITY)) { ai_set_state(character, AI_STATE_PATROL); ai_set_default_state(character, AI_STATE_PATROL); return; } ai_get_ready(character); } /* * AI state: patrol * */ static void ai_state_patrol(CHARACTER *character) { const PATH_NODE *path; if (enemies_noticed(character)) { ai_set_state(character, AI_STATE_COMBAT); return; } if (random_choice(AI_SWITCH_TO_GUARD_PROBABILITY)) { ai_set_state(character, AI_STATE_GUARD); ai_set_default_state(character, AI_STATE_GUARD); return; } /* * non-hostile characters patrol slowly, this makes * talking to them easier */ if (!character->is_hostile && random_choice(50)) { ai_get_ready(character); return; } if (ai_target_distance(character) == 0) { ai_erase_target(character); } if (!ai_has_target(character)) { AREA_POINT target_point; if (sector_random(&target_point, SC_MOVE_TARGET_SAFE) == NULL) { ai_get_ready(character); return; } ai_set_target(character, &target_point); } path = find_safe_ai_path(character, &character->ai.target_point); if (path != NULL && character_blocks_path(character, path)) { path = find_bypass_characters_path(character, &character->ai.target_point ); } if (path == NULL || !action_move(character, path_first_step(path))) { ai_erase_target(character); ai_get_ready(character); } } /* * AI state: follow */ static void ai_state_follow(CHARACTER *character) { if (!ai_is_close_to_pcc(character)) { const AREA_POINT *pcc_location; pcc_location = &(player_controlled_character()->location); ai_set_target(character, pcc_location); if (ai_run_towards_target(character)) { return; } AiFollowFailed = true; } if (enemies_noticed(character)) { ai_set_state(character, AI_STATE_COMBAT); return; } ai_get_ready(character); } /* * AI state: flee */ static void ai_state_flee(CHARACTER *character) { CHARACTER *nearest_enemy; AREA_POINT step; int i; if (!character_has_flag(character, CF_BROKEN)) { ai_set_state(character, character->ai.default_state); return; } nearest_enemy = nearest_noticed_enemy(character); if (nearest_enemy == NULL) { action_do_nothing(character); return; } step = character->location; for (i = run_factor(character); i > 0; --i) { ai_flee(&step, &nearest_enemy->location); if (!action_move(character, &step)) { break; } } if (!character->action_spent) { action_do_nothing(character); } } /* * AI state: search */ static void ai_state_search(CHARACTER *character) { if (ai_target_distance(character) == 0) { ai_set_state(character, character->ai.default_state); return; } if (enemies_noticed(character)) { ai_set_state(character, AI_STATE_COMBAT); return; } if (!ai_run_towards_target(character)) { ai_get_ready(character); } } /* * AI state: combat */ static void ai_state_combat(CHARACTER *character) { const CHARACTER *nearest_enemy; if (!enemies_noticed(character)) { if (character->party == PARTY_PLAYER) { ai_set_state(character, character->ai.default_state); } else { ai_set_state(character, AI_STATE_SEARCH); } return; } if (character->ai.tactic == TACTIC_GET_READY) { ai_set_tactic(character, TACTIC_PLAN_A); } else if (character->ai.tactic == TACTIC_STAY_CLOSE && !AiFollowFailed && !ai_is_close_to_pcc(character)) { ai_set_state(character, AI_STATE_FOLLOW); return; } nearest_enemy = nearest_noticed_enemy(character); ai_set_target(character, &nearest_enemy->location); if (is_able_to_evoke_psy_powers(character)) { ai_psyker(character); if (character->action_spent) return; } if (ai_attack(character)) { return; } ai_get_ready(character); } /* * sets the target of the AI */ static void ai_set_target(CHARACTER *character, const AREA_POINT *target_point) { character->ai.target_point = *target_point; } /* * returns true if the AI has a target */ static bool ai_has_target(const CHARACTER *character) { if (AREA_POINT_NOT_DEFINED(&character->ai.target_point)) { return false; } return true; } /* * sets the target of the AI */ static void ai_erase_target(CHARACTER *character) { character->ai.target_point = *area_point_nil(); } /* * returns the distance between the AI and its target */ static AREA_DISTANCE ai_target_distance(const CHARACTER *character) { return area_distance(&character->location, &character->ai.target_point ); } /* * AI: fix self * * the AI attempts to "fix" itself * */ static void ai_fix_self(CHARACTER *character) { ai_use_drugs_to_fix_self(character); ai_use_stealth(character); } /* * AI: use drugs to fix self * * the AI uses drugs to remove the status flags "Poisoned" and "Broken" * if the needed drugs are available * */ static void ai_use_drugs_to_fix_self(CHARACTER *character) { OBJECT *drug; if (character_has_flag(character, CF_POISONED)) { drug = inventory_find_object(character, "Antidote"); if (drug != NULL) { action_use_drug(character, drug); } } if (character_has_flag(character, CF_BROKEN)) { drug = inventory_find_object(character, "Stoic"); if (drug != NULL) { action_use_drug(character, drug); } } } /* * AI: use stealth * * the AI uses the "Stealth" perk if its available * */ static void ai_use_stealth(CHARACTER *character) { if (!character->perk[PK_STEALTH] || character_unnoticed(character)) { return; } action_stealth(character); } /* * AI: get ready * * the AI will ready itself for combat * * if there is nothing left to do it will just do nothing * */ static void ai_get_ready(CHARACTER *character) { if (character->weapon != NULL) { OBJECT *weapon; const OBJECT_DATA *weapon_data; weapon = character->weapon; weapon_data = object_static_data(weapon); if (!weapon->functional) { action_unjam_weapon(character); return; } if (weapon->charge < weapon_data->charge_max && !weapon_data->attribute[OA_AUTOMATIC_RECHARGE]) { action_reload_weapon(character); return; } } if (character_must_recover(character)) { action_recover(character); return; } action_do_nothing(character); } /* * AI: attack */ static bool ai_attack(CHARACTER *character) { bool ranged_attack; ranged_attack = ai_attack_type_choice(character); if (!ranged_attack && ai_target_distance(character) > 1) { if (character->ai.tactic == TACTIC_STAY_CLOSE || character->ai.tactic == TACTIC_HOLD_POSITION) { return false; } if (ai_run_towards_target(character)) { return true; } return false; } ai_switch_weapon(character, ranged_attack); ai_fix_weapon(character); if (ranged_attack) { ai_ranged_attack(character); } else { ai_close_combat_attack(character); } return true; } /* * AI: attack type choice * * returns true if the AI has chosen to use a ranged attack * */ static bool ai_attack_type_choice(const CHARACTER *character) { AREA_DISTANCE target_distance; target_distance = ai_target_distance(character); if (target_distance == 1 || !has_ranged_attack(character)) { return false; } if (character->ai.tactic == TACTIC_STAY_CLOSE || character->ai.tactic == TACTIC_HOLD_POSITION) { return true; } if (target_distance > MAX_LONG_RANGE && !character->perk[PK_MARKSMAN]) { return false; } return true; } /* * AI: run towards target */ static bool ai_run_towards_target(CHARACTER *character) { const PATH_NODE *path; path = find_ai_path(character, &character->ai.target_point); if (path != NULL && character_blocks_path(character, path)) { path = find_bypass_characters_path(character, &character->ai.target_point ); } if (path == NULL) { return false; } if (destructable_obstacle_at(path_first_step(path))) { ai_set_target(character, path_first_step(path)); ai_attack(character); return true; } if (action_run(character, path, run_factor(character))) { return true; } return false; } /* * AI: switch weapon * * the AI switches to the most appropriate weapon * */ static void ai_switch_weapon(CHARACTER *character, bool ranged_attack) { if (ranged_attack) { ai_switch_weapon_ranged_combat(character); } else { ai_switch_weapon_close_combat(character); } } /* * AI: switch weapon / ranged combat * * the AI switches to the most appropriate weapon for ranged combat * */ static void ai_switch_weapon_ranged_combat(CHARACTER *character) { if (character->weapon != NULL) { if (object_static_data(character->weapon)->type == OTYPE_RANGED_COMBAT_WEAPON) { return; } } if (character->secondary_weapon != NULL) { if (object_static_data(character->secondary_weapon)->type == OTYPE_RANGED_COMBAT_WEAPON) { action_switch_weapons(character); } } } /* * AI: switch weapon / close combat * * the AI switches to the most appropriate weapon for ranged combat * */ static void ai_switch_weapon_close_combat(CHARACTER *character) { if (character->weapon != NULL) { const OBJECT_DATA *weapon_data; weapon_data = object_static_data(character->weapon); if (weapon_data->type == OTYPE_CLOSE_COMBAT_WEAPON || weapon_data->subtype == OSTYPE_PISTOL) { return; } } if (character->secondary_weapon != NULL) { const OBJECT_DATA *weapon_data; weapon_data = object_static_data(character->secondary_weapon); if (weapon_data->type == OTYPE_CLOSE_COMBAT_WEAPON || weapon_data->subtype == OSTYPE_PISTOL) { action_switch_weapons(character); return; } } if (character->weapon != NULL) { if (character->secondary_weapon == NULL) { action_switch_weapons(character); return; } action_unequip_object(character, character->weapon); } } /* * AI: fix weapon * * the AI tries to fix (unjam/reload) its weapon if necessary * * if the AI uses an automatic recharge weapon, it will just * "get ready" while the weapon recharges * */ static void ai_fix_weapon(CHARACTER *character) { if (character->weapon == NULL) { return; } if (!character->weapon->functional) { action_unjam_weapon(character); return; } if (character->weapon->charge == 0) { if (object_has_attribute(character->weapon, OA_AUTOMATIC_RECHARGE)) { ai_get_ready(character); return; } action_reload_weapon(character); } } /* * AI: ranged attack */ static void ai_ranged_attack(CHARACTER *character) { FIRING_DATA firing_data; ai_choose_firing_mode(character, &firing_data); action_shoot(character, character->weapon, &firing_data, &character->ai.target_point ); } /* * AI: choose firing mode */ static void ai_choose_firing_mode(const CHARACTER *character, FIRING_DATA *firing_data ) { const FIRING_MODE_DATA *firing_mode; firing_mode = &(object_static_data(character->weapon)->firing_mode); if (firing_mode->has[FMODE_A]) { firing_data->firing_mode = FMODE_A; firing_data->n_shots = firing_mode->a_shots; firing_data->spread = random_int( A_MODE_SPREAD_MIN, A_MODE_SPREAD_MAX ); } else if (firing_mode->has[FMODE_SA]) { firing_data->firing_mode = FMODE_SA; firing_data->n_shots = random_int( firing_mode->min_sa_shots, firing_mode->max_sa_shots ); } else if (firing_mode->has[FMODE_S]) { firing_data->firing_mode = FMODE_S; firing_data->n_shots = 1; } else { die("*** CORE ERROR *** weapon without a firing mode"); } } /* * AI: close combat attack */ static void ai_close_combat_attack(CHARACTER *character) { if (character->weapon != NULL && object_static_data(character->weapon)->subtype == OSTYPE_PISTOL ) { FIRING_DATA firing_data; ai_choose_firing_mode(character, &firing_data); action_shoot(character, character->weapon, &firing_data, &character->ai.target_point ); return; } action_strike(character, character->weapon, &character->ai.target_point ); } /* * AI: is close to PCC * * returns true if the passed character is close to the player * controlled character * */ static bool ai_is_close_to_pcc(const CHARACTER *character) { const AREA_POINT *pcc_location; pcc_location = &(player_controlled_character()->location); if (area_distance(&character->location, pcc_location) <= AI_MAX_FOLLOW_DISTANCE) { return true; } return false; } /* * AI: psyker */ static void ai_psyker(CHARACTER *character) { PSY_POWER chosen_power; if (!random_choice(ai_psychic_action_probability(character))) { return; } chosen_power = ai_choose_psy_power(character); if (chosen_power == PSY_NIL) { return; } action_evoke_psy_power(character, chosen_power, &character->ai.target_point ); } /* * AI: choose psy power */ static PSY_POWER ai_choose_psy_power(CHARACTER *character) { PSY_POWER chosen_power, power; bool choice_valid; power = chosen_power = random_int(0, MAX_PSY_POWERS - 1); choice_valid = false; do { if (character->psy_power[power]) { const AREA_POINT *target_point; if (psy_power_is_hostile(power)) { target_point = &character->ai.target_point; } else { target_point = &character->location; } if (ai_power_is_sensible_choice(power, target_point)) { chosen_power = power; choice_valid = true; ai_set_target(character, target_point); break; } } ++power; if (power == MAX_PSY_POWERS) { power = 0; } } while (power != chosen_power); if (choice_valid) { return chosen_power; } return PSY_NIL; } /* * returns true if the passed psychic power is a sensible choice */ static bool ai_power_is_sensible_choice(PSY_POWER power, const AREA_POINT *target_point ) { if (psy_power_is_tricky(power)) { return false; } if (!psy_power_valid_target(power, target_point)) { return false; } if (psy_power_in_effect(power, target_point)) { return false; } if (psy_power_target_is_immune(power, target_point)) { return false; } return true; } /* * returns the psychic action probability of the passed character */ static int ai_psychic_action_probability(const CHARACTER *character) { int probability; if (character_has_flag(character, CF_PSYCHIC_OVERLOAD)) { return 0; } probability = character->stat[S_FR].current; if (probability > 100) { probability = 100; } else if (probability < 0) { probability = 0; } return probability; } /* * changes the coordinates of area point 'a' by moving it * one step away from area point 'b' */ static void ai_flee(AREA_POINT *a, const AREA_POINT *b) { if (a->y > b->y) { ++a->y; } else if (a->y < b->y) { --a->y; } if (a->x > b->x) { ++a->x; } else if (a->x < b->x) { --a->x; } }