/** * This file contains all the methods and fun things actually run combat. * @author Ceres * * * TODO: Add offensive & defensive criticals -- broken limbs etc. * */ inherit "/std/living/corpse"; inherit "/std/weapon_logic"; #include <attack_messages.h> #include <tasks.h> #include <player.h> #include <living.h> #include <weapon.h> #ifdef COMBAT_SPEED #undef COMBAT_SPEED #endif #include <combat.h> #ifdef ABS #undef ABS #endif #define ABS(n) (n) < 0 ? -(n) : (n) // 2 == verbose, 1 == brief, 0 == none. #define DEBUG 1 #undef USE_SURRENDER class tactics _tactics; #ifdef USE_SURRENDER class surrender_information _surrender; #endif nosave class combat_information _combat; nosave string tmpzone; // needed for function pointer. nosave int _callout; object *query_attacker_list(); void stop_all_fight(); int query_distance(object); int set_distance(object, int); void adjust_action_defecit(int amount); private void init_tactics(); #ifdef USE_SURRENDER object *query_surrenderers(); #endif /** @ignore yes */ void create() { ::create(); init_tactics(); _combat = new(class combat_information, in_combat : 0, protectors : ({ }), defenders : ({ }), attacker_list : ([ ]), hunting_list : ([ ]), specials : ({ }) ); #ifdef USE_SURRENDER _surrender = new(class surrender_information, to : ({ }), from : ({ }), refusers : ({ })); #endif } private void init_tactics() { if(!classp(_tactics) || sizeof(_tactics) != 8) { _tactics = new(class tactics, attitude : "neutral", response : "neutral", parry : "both", attack : "both", parry_unarmed : 0, mercy : "ask", focus_zone : 0, ideal_distance : 0); } } #ifdef DEBUG class combat_information query_combat() { return _combat; } #ifdef USE_SURRENDER class surrender_information query_surrender() { return _surrender; } #endif #endif /** @ignore yes */ void combat_debug(string fmt, mixed args ...) { if(userp(this_object())) this_object()->event_inform(this_object(), sprintf("%s " + fmt, this_object()->query_name(), args ...), "combat-debug", this_object()); event(environment(this_object()), "inform", sprintf("%s " + fmt, this_object()->query_name(), args ...), "combat-debug", this_object()); } /** * Decide if we will attack this round or not. This function can be * overloaded if necessary. It also checks for the property "cannot attack" * on this object. If the property is set the object cannot attack. * * @return 1 if we want to, 0 if we don't. */ int query_can_attack() { if(this_object()->query_property(PASSED_OUT) || this_object()->query_property("dead") || this_object()->query_auto_loading() || this_object()->query_hp() < 0 || this_object()->query_casting_spell() || (userp(this_object()) && !interactive(this_object())) || this_object()->query_property("cannot attack")) return 0; return 1; } /** * Decide if we are able to defend ourselves this time or not. * This function can be overloaded if necessary. It also checks for the * property "cannot defend" * on this object. If the property is set the object cannot defend. * * @return 1 if we want to, 0 if we don't. */ int query_can_defend() { if(this_object()->query_property(PASSED_OUT) || this_object()->query_property("dead") || this_object()->query_auto_loading() || this_object()->query_hp() < 0 || this_object()->query_casting_spell() || (userp(this_object()) && !interactive(this_object())) || this_object()->query_property("cannot defend")) return 0; return 1; } /** * Choose an opponent and attack them. * * This method must fill in the opponent variable. If no opponent is chosen * the attack will be aborted. * * PRE: Attacker information has been filled in. * POST: opponent must be chosen or the attack will be aborted. * * @param class attack * @return class attack */ protected class attack choose_opponent(class attack att) { mixed *opponents, opponent; int i; #ifdef USE_DISTANCE mixed tmp; #endif // TODO: More advanced opponent selection. opponents = map(keys(_combat->attacker_list), (: $1 && objectp($1) ? $1 : find_player($1) :)); opponents = filter(opponents, (: $1 && environment($1) == environment($2) && $1->query_attackable() :), att->attacker); #ifdef USE_SURRENDER // Remove surrendered people from the opponent list so we don't kill // anyone during the surrender. foreach(opponent in query_surrenderers()) { if(userp(opponent)) opponent = opponent->query_name(); opponents -= ({ opponent }); } #endif // we pick the closest opponent. if(!sizeof(opponents)) return att; // Attack whoever we're concentrating on. if(interactive(this_object()) && objectp(att->attacker_concentrating) && member_array(att->attacker_concentrating, opponents) != -1) opponent = att->attacker_concentrating; else { #ifdef USE_DISTANCE opponent = opponents[0]; foreach(tmp in opponents) if(_combat->attacker_list[tmp] < _combat->attacker_list[opponent]) opponent = tmp; #endif // Three different NPC algorithms. good_fighter should use better // ones. switch(random(3)) { case 2: opponent = opponents[0]; for(i=0; i<sizeof(opponents); i++) if((opponent->query_hp() / 10 + random(100)) > (opponents[i]->query_hp() / 10 + random(100))) opponent = opponents[i]; break; case 1: opponent = opponents[0]; for(i=0; i<sizeof(opponents); i++) if((opponent->query_hp() / 10 + random(100)) < (opponents[i]->query_hp() / 10 + random(100))) opponent = opponents[i]; break; default: opponent = opponents[random(sizeof(opponents))]; } } #ifdef USE_DISTANCE // Set the opponent and distance. att->distance = _combat->attacker_list[opponent]; #endif if(stringp(opponent)) att->opponent = find_player(opponent); else att->opponent = opponent; // Just in case they disappeared somehow. if(environment(att->opponent) != environment(this_object())) return att; // Automatically concentrate on someone if they're the only opponent. if(sizeof(opponents) == 1) _combat->concentrating = att->opponent; return att; } /** @ignore yes */ void clear_protection(object attacker, object protector) { if(attacker->remove_protector(protector)) protector->event_combat(protector, "You stop protecting "+ attacker->the_short() + " as "+ attacker->query_pronoun() + " moves to attack "+ "you!\n", ({ }), 0); if(attacker->remove_defender(protector)) protector->event_combat(protector, "You stop defending "+ attacker->the_short() + " as "+ attacker->query_pronoun() +" moves to attack "+ "you!\n", ({ }), 0); } /** * This method is used to choose who will defend this attack. Typically this * will be the person who was attacked, however it could be someone else in * some circumstances. * * PRE: attacker, attack and defender information has been set. * POST: defender and person_hit must be set. * * @param class attack * @return class attack */ class attack choose_defender(class attack att) { object *protectors, *defenders; // If the combat system (or a special) has already chosen a protector // we won't override it. if(!att->person_hit) { // People who will protect (ie. jump infront of an attack) protectors = filter((att->opponent)->query_protectors(), (: $1 && environment($1) == environment($2) && $1->query_protect() && !pk_check($1, $2) :), att->attacker); if(sizeof(protectors)) { protectors->attack_by(att->attacker); att->person_hit = protectors[random(sizeof(protectors))]; #if DEBUG == 1 combat_debug("picked %s to protect", (att->person_hit)->query_name()); #endif } } // If the combat system (or a special) has already chosen a defender // we won't override it. if(!att->defender) { defenders = filter((att->opponent)->query_defenders(), (: $1 && environment($1) == environment($2) && !pk_check($1, $2) :), att->attacker); // Make sure local defenders join in the fight. if(sizeof(defenders)) { defenders->attack_by(att->attacker); defenders = filter(defenders, (: $1->query_defend() :)); } // See if we have someone to defend. if(sizeof(defenders)) { att->defender = defenders[random(sizeof(defenders))]; #if DEBUG == 1 combat_debug("picked %s to defend", (att->defender)->query_name()); #endif } } // Do the defaults if we haven't been protected or defended. if(!att->person_hit) att->person_hit = att->opponent; if(!att->defender) att->defender = att->opponent; return att; } /** * Is this object attackable or not? * * @return 1 or 0 for true or false. */ int query_attackable() { if(this_object()->query_property(PASSED_OUT) || this_object()->dont_attack_me() || this_object()->attack_inhibit() || this_object()->query_property("dead") || this_object()->query_auto_loading() || this_object()->query_hp() < 0 || (userp(this_object()) && !interactive(this_object()))) return 0; return 1; } /** * Is this object prepared to try to defend someone else at this time? * * @return 1 or 0 for true or false. */ int query_defend() { if(!query_attackable() || this_object()->query_casting_spell() || this_object()->query_specific_gp("fighting") < 1) return 0; return ((_tactics->response == "parry" || _tactics->response == "both") && _combat->action_defecit < (COMBAT_ACTION_TIME * 4)); } /** * Is this object prepared to try to protect someone else at this time? * @return 1 or 0 for true or false. */ int query_protect() { if(!query_attackable() || this_object()->query_casting_spell() || this_object()->query_specific_gp("fighting") < 1) return 0; return (_combat->action_defecit < (COMBAT_ACTION_TIME * 4)); } /** * Determine which attack they'll use. * * This picks a weapon, gets the attack data and the weapon skill and * determines how much the attack will cost. * * PRE: attacker info, opponent and defender info have been set. * POST: attack weapon, attack skill, attack data, and attack_cost must * be set. * * @param class attack * @return class attack */ protected class attack choose_attack(class attack att) { object weapon, *weapons; string hand, *tmp; int perc, which_attack, i; class tactics tactics; mixed *attacks; string zone; float sz; tactics = att->attacker_tactics; if(!this_object()->query_can_attack()) return att; // Depending on their tactics they will go further into defecit in order // to attack. if(!OFFENSIVE_DEFECITS[tactics->attitude]) tactics->attitude = "neutral"; if(att->attacker_defecit > OFFENSIVE_DEFECITS[tactics->attitude]) return att; weapons = (att->attacker)->query_holding(); tmp = (att->attacker)->query_limbs(); // Do they have a preferred attacking hand? hand = (att->attacker_tactics)->attack; if(hand && hand != "both") { for(i=0; i<sizeof(tmp); i++) if(tmp[i] == hand + " hand") break; if(!weapons[i]) // they're going to attack unarmed. weapons = ({ }); else if(weapons[i]->query_weapon()) // they'll attack with the weapon weapons = ({ weapons[i] }); } // Make sure we have a unique list of weapons if we're going to have to // pick one. if(sizeof(weapons) > 1) weapons = uniq_array(filter(weapons, (: $1 && $1->query_weapon() :))); // Pick the attacking weapon from what's left by using the most appropriate // for distance. switch(sizeof(weapons)) { case 0: weapon = att->attacker; break; case 1: // Tend to attack with the weapon in the left hand. weapon = weapons[0]; break; default: #ifdef USE_DISTANCE // Pick the weapon that best fits the combat distance we're at right now. weapon = weapons[0]; foreach(wtmp in weapons) if(ABS(REACH + weapon->query_length() - att->distance) > ABS(REACH + wtmp->query_length() - att->distance)) weapon = wtmp; #else weapon = weapons[random(sizeof(weapons))]; #endif } att->attack_weapon = weapon; // Calculate their attack percentage. // For the moment I've just given a simple strength bonus so that stronger // players hit slightly harder. perc = 75 + (att->attacker)->query_str() + (att->attacker)->query_dex(); // The heavier the weapon the harder it is to hit hard with it. if(weapon != att->attacker) perc -= weapon->query_weight() / 2; if(perc < 25) perc = 25; attacks = weapon->weapon_attacks(perc, att->defender); if(!sizeof(attacks)) { #if DEBUG == 2 combat_debug("No attacks with %O, terminating [%d, %O].", weapon, perc, att->defender); #endif return att; } // Choose an attack at random from the list of attacks. This should probably // be a weighted average (since each attack has a %age chance) which_attack = random(sizeof(attacks) / AT_ARRAY_SIZE); attacks = attacks[(which_attack * AT_ARRAY_SIZE)..]; // No free hands? Can't attack with your hands then. if(attacks[AT_NAME] == "hands" && !(att->attacker)->query_free_limbs()) return att; att->attack_data = attacks; att->attack_skill = ATTACK_SKILL + attacks[AT_SKILL]; // Choose an attack zone. If they've chosen one then they get a -ve modifier // If they haven't set a preference then choose one at random. if((att->attacker_tactics)->focus_zone == "upper body") { tmp = ((att->opponent)->query_race_ob())->query_target_zones(); zone = tmp[random(sizeof(tmp) / 2)]; } else if((att->attacker_tactics)->focus_zone == "lower body") { zone = tmp[random(sizeof(tmp) / 2) + sizeof(tmp)/2]; } else if((att->attacker_tactics)->focus_zone && (att->attacker_tactics)->focus_zone != "none") { // generic zone. zone = (att->attacker_tactics)->focus_zone; // map to the races zone zone = ((att->opponent)->query_race_ob())->map_target_zone(zone); att->target_zone = zone; } else { if(att->attack_data[AT_SKILL] == "unarmed") { // for humanoids punches will typically be upper body while // kicks will typically be lower body (depending on target size). if(att->attack_data[AT_NAME] == "hands") sz = ((att->attacker)->query_height() * 2) / (att->opponent)->query_height(); else if(att->attack_data[AT_NAME] == "feet") sz = ((att->attacker)->query_height() / 2) / (att->opponent)->query_height(); } else sz = to_float((att->attacker)->query_height()) / to_float((att->opponent)->query_height()); // The relative sizes of combatants lead to more attacks higher or lower // on the body. tmp = ((att->opponent)->query_race_ob())->query_target_zones(); if(sizeof(tmp)) { if(sz > 1.5) zone = tmp[random(random(sizeof(tmp)))]; if(sz < 0.75) { i = random(sizeof(tmp) + 10); if(i > sizeof(tmp)-1) i = sizeof(tmp) - 1; zone = tmp[i]; } else { zone = tmp[random(sizeof(tmp))]; } } } att->target_zone = zone; return att; } /** * Will the defender defend or not? If so, with what skill and weapon and at * what action cost? * * PRE: all attack data and opponent and defender data have been set. * POST: defense type, skill and weapon must be set. If defense_action * is "none" the defender will have a big negative modifier. * * @param class attack * @return class attack */ class attack choose_defense(class attack att) { object defender; string response, *hands, hand; object *weapons; class tactics tactics; int i, which; #ifdef USE_DISTANCE int move_cost; #endif defender = att->defender; att->defense_action = "none"; att->defense_skill = DODGE; att->defense_weapon = 0; tactics = att->defender_tactics; // Defenders (when the defender is not the opponent) have already // decided that they'll take on this action regardless of their // defecit thus only check defecits if opponent == defender. if(defender == att->opponent) { if(!defender->query_can_defend()) return att; if(!DEFENSIVE_DEFECITS[tactics->attitude]) tactics->attitude = "neutral"; if(att->defender_defecit > DEFENSIVE_DEFECITS[tactics->attitude]) return att; } #ifdef USE_DISTANCE else if(att->distance != defender->query_distance(att->attacker) && ABS(defender->query_distance(att->attacker)) > MOVE_DISTANCE[tactics->attitude]) { // If we have a defender or protector and they aren't in the right // position they need to move there and that will cost them some extra // actions. This _could_ lead to them spending all their time running // back and forth if they try to protect/defend too many people. defender->set_distance(att->attacker, att->distance); move_cost = MOVE_COST; move_cost -= defender->query_skill_bonus(MOVE_SKILL) / 50; if(move_cost < 0) move_cost = 0; (att->opponent)->event_combat(att->opponent, defender->one_short() + " moves to " + (att->person_hit == defender ? "protect" : "defend" ) + " you.\n", ({ }), 0); defender->event_combat(defender, "You move to " + (att->person_hit == defender ? "protect" : "defend") + " " + (att->opponent)->one_short() + ".\n", ({ }), 0); event(environment(att->opponent), "combat", defender->one_short() + " moves to " + (att->person_hit == defender ? "protect" : "defend") + " " + (att->opponent)->one_short() + ".\n", ({ att->opponent, defender, att->person_hit }), 1); } #endif response = tactics->response; // Defenders have two choices, parry or don't, dodging isn't any use. if(defender != att->opponent) { if(response == "dodge") return att; response = "parry"; } else if(!response || response == "neutral") // opponents get a free choice. response = ({"parry", "dodge"})[random(2)]; if(response == "parry") { weapons = (object *)defender->query_holding(); // Figure out which weapon index is the hand they prefer to parry with. hand = (att->defender_tactics)->parry; which = -1; hands = (att->defender)->query_limbs(); for(i=0; i<sizeof(hands); i++) if(hands[i][0..sizeof(hand)-1] == hand) which = i; // If we found a preferred hand then try to parry with it, or parry // unarmed if they want to. if(which != -1) { if(weapons[which]) { att->defense_action = "parry"; att->defense_skill = WEAPON_PARRY; att->defense_weapon = weapons[which]; } else if((att->defender_tactics)->parry_unarmed) { att->defense_action = "parry"; att->defense_skill = UNARMED_PARRY; att->defense_weapon = att->defender; att->defense_limb = (att->defender)->query_limbs()[which]; } } else { // If there's no preferred hand and they have weapons they parry with // whichever they have a weapon in. Otherwise, they parry unarmed if // they want to do that, otherwise it's no defense. weapons -= ({ 0 }); if(!sizeof(weapons)) { if((att->defender_tactics)->parry_unarmed) { att->defense_action = "parry"; att->defense_skill = UNARMED_PARRY; att->defense_weapon = att->defender; } } else { att->defense_action = "parry"; att->defense_skill = WEAPON_PARRY; att->defense_weapon = weapons[random(sizeof(weapons))]; } } } else { att->defense_action = "dodge"; att->defense_skill = DODGE; att->defense_weapon = 0; } return att; } /** * This method calculates the modifier to the attack. * * It will take into account all the basic stuff that makes the attack * harder or easier. * * PRE: All attack and response data has been filled in. * POST: The offensive modifier must be set. * * @param class attack * @return class attack */ protected class attack calc_attack_modifier(class attack att) { object attacker, attack_weapon, *holding; int mod, wep, hld, lght, mntd, hlth, brdn, dist, tact, targ, oth; int tmp, tmp2, limbs, dex, num, actions; string zone, *attackers; attacker = att->attacker; dex = attacker->query_dex(); attack_weapon = att->attack_weapon; mod = wep = hld = lght = mntd = hlth = brdn = dist = tact = 0; if(attack_weapon != attacker) { /* * Find out how heavy the weapon is, how many hands they're using to hold * it, how many other weapons they're holding, which hand they're * holding their many hands they're using to hold the weapon and decrease * a based on that. */ holding = attacker->query_holding(); // tmp -- how many hands they're using to hold the attacking weapon // tmp2 -- weight factor for other held items. limbs = tmp2 = 0; for(num = 0; num < sizeof(holding); num++) { if(holding[num] == attack_weapon) limbs++; else if(holding[num]) { if((holding[num])->id("shield")) tmp2 += (holding[num])->query_weight() / 5; else tmp2 += (holding[num])->query_weight(); } } // If they're holding a second weapon but aren't parrying with it // then their negative modifier for it is lower. switch((att->attacker_tactics)->response) { case "both": tmp2 = (tmp2 * 3) / 2; break; case "dodge": // To hold multiple weapons your dex must outweigh the weight of the // secondary weapons. if(tmp2 < dex) tmp2 /= 2; } // weapon weight. wep = attack_weapon->query_weight() + (tmp2 / 2); // 2/3 for one hand. 2 / 4 for two hands. 2 / 5 for three etc. wep /= (limbs + 1); wep -= (att->attacker)->query_str(); // If their weapon weight factor adds up to more than their strength // they take a negative modifier. if(wep > 0 ) { wep = - to_int(pow(wep, 1.4)); } else wep = 0; // If you're attacking with your off-hand (ie. not your prime hand) // you take a negative modifier if your dex is low. // (index 0 is your prime hand) if(dex < 14 && sizeof(holding) && holding[0] != attack_weapon) hld = dex - 14; } else { wep = (dex * 2) - attacker->query_burden(); } // light levels switch(attacker->check_dark(environment(attacker)->query_light())) { case -2: case 2: lght = -50; break; case -1: case 1: lght = -25; break; } // mountedness mntd = 0; // health (up to -50) hlth -= (50 - (attacker->query_hp() * 50) / attacker->query_max_hp()); // fatigue (up to -25) tmp = attacker->query_specific_gp("fighting"); if(tmp < -50) hlth += -25; else if(tmp < 0) hlth += (tmp / 2); // burden (up to -33) brdn -= (attacker->query_burden() / 3); #ifdef USE_DISTANCE // distance from opponent & length of weapon. if(att->attack_weapon == att->attacker) dist = -3 * ABS(REACH - att->distance); else dist = -3 * ABS(REACH + (att->attack_weapon)->query_length() - att->distance); #endif // Bonus for offensive tactics. switch((att->attacker_tactics)->attitude) { case "insane": tact = 25; break; case "offensive": tact = 15; break; case "defensive": tact = -25; break; case "wimp": tact = -50; break; } switch(att->attacker_tactics->focus_zone) { case 0: case "none": targ = 0; break; case "upper body": case "lower body": targ -= 25; break; default: tmp = 0; foreach(zone in ((att->opponent)->query_race_ob())->query_target_zones()) if(zone == att->target_zone) tmp++; num = sizeof(((att->opponent)->query_race_ob())->query_target_zones()); // between -0 & -25 depending on what %age of the body the target zone // accounts for. targ -= ((num - tmp) * 25 / num); } if(targ < -25) { debug_printf("Targ too small (%s) %d. num: %d, tmp: %d\n", (att->attacker_tactics)->focus_zone, targ, num,tmp); targ = -25; } // If you're further down the attacker list than the second person // you take a negative modifier since only the first 2-4 people // can really get a hit in on someone. attackers = filter((att->opponent)->query_attacker_list(), (: $1 && environment($1) == environment(this_object()):)); num = member_array(this_object(), attackers); if(num > 1) num += -25 * num; if((att->attacker_tactics)->attack == "both") oth += 5; mod = wep + hld + lght + mntd + hlth + brdn + dist + tact + targ + oth + num; att->attack_modifier += mod; // Calculate how much time an attack takes. actions += ATTACK_COST; // heavier weapons are slower to attack with. if(attack_weapon != att->attacker) actions += (sqrt(attack_weapon->query_weight()) * 3) / (limbs + 1); // Holding two weapons gives a slight advantage. if(sizeof((att->attacker)->query_weapons()) > 1) actions -= ATTACK_COST / 4; // more skilled opponents are more fluid in their movements. actions -= ((att->attacker)->query_skill_bonus(att->attack_skill) + (att->attacker)->query_skill_bonus(TACTICAL_SKILL)) / 50; if(actions < ATTACK_COST / 5) actions = ATTACK_COST / 5; else if(actions > ATTACK_COST * 2) actions = ATTACK_COST * 2; att->attack_cost = actions; #if DEBUG == 1 combat_debug("Attack %s (%s): wep: %d hld: %d lght: %d mntd: %d " "hlth: %d brdn: %d dist: %d tact: %d targ: %d oth: %d " "num: %d = %d [%d] (actions: %d %d)", att->attack_data[AT_NAME], (att->attack_weapon)->query_name(), wep, hld, lght, mntd, hlth, brdn, dist, tact, targ, oth, num, mod, att->attack_modifier, actions, att->attacker_defecit); #endif return att; } /** * This method calculates the modifier to the defense. * * It will take into account all the basic stuff that makes the defense * harder or easier. * * PRE: All attack and response data has been filled in. * POST: The defensive modifier must be set. * * @param class attack * @return class attack */ class attack calc_defense_modifier(class attack att) { object defender, attacker, attack_weapon, defense_weapon, *holding; int mod, wep, wght, dist, brdn, hnd, lght, hlth, tact, prot, oth; int tmp, num, dex, limbs, actions; mod = wep = wght = dist = brdn = hnd = lght = hlth = tact = prot = oth = 0; defender = att->defender; defense_weapon = att->defense_weapon; dex = defender->query_dex(); attacker = att->attacker; attack_weapon = att->attack_weapon; // Parry & Dodge are somewhat different. Obviously parrying depends on // using an object to block while dodge requires moving ones whole body. // They should each have strengths and weaknesses. switch(att->defense_action) { case "parry": if(defense_weapon != defender && defense_weapon) { holding = defender->query_holding(); // weapon weight if(defense_weapon->id("shield")) wep = defense_weapon->query_weight() / 5; else wep = defense_weapon->query_weight() * 2; /* * Find out how many hands they're using to hold the weapon/shield */ limbs = 0; for(num = 0; num < sizeof(holding); num++) if(holding[num] && holding[num] == defense_weapon) limbs++; // 2/2 for one hand. 2 / 3 for two hands. 2 / 4 for three etc. wep /= (limbs + 2); if(wep > defender->query_str()) wep = - to_int(pow(wep - defender->query_str(), 1.3)); // relative weight of attackers weapon to ours? if(attack_weapon != attacker) wght = 2 * (defense_weapon->query_weight() - attack_weapon->query_weight()); else wght = defense_weapon->query_weight() / 2; // Having a heavier defensive weapon doesn't help much, if at all. // However, having a light defensive weapon is a disadvantage. if(wght > 5) wght = 5; #ifdef USE_DISTANCE dist = - ABS(REACH + defense_weapon->query_length() - att->distance); #endif } else { #ifdef USE_DISTANCE dist = - ABS(REACH - att->distance); #endif } brdn = -(defender->query_burden() / 3); if(sizeof(holding)) { // If you're parrying with your off-hand (ie. not your prime hand) // you take a negative modifier. if(dex < 14 && holding[0] != defense_weapon) hnd = dex - 14; // If you're parrying with the same weapon you attack with // (ie. you're parrying & only holding one weapon that's a -ve. holding = uniq_array(filter(holding, (: $1 :))); if(dex < 16 && sizeof(holding) == 1) hnd += dex - 16; } break; case "dodge": // All actions get the 1/3rd burden. brdn = -(defender->query_burden() / 3); // Dodgers get a dex mitigated burden modifier if(dex < (defender->query_burden() / 2)) brdn -= dex - (defender->query_burden() / 2); if(att->attack_weapon != att->attacker) wght = ((att->attack_weapon)->query_weight() / 10); break; } // light levels / visibility of opponent if(!(att->attacker)->query_visible(defender)) lght = -100; else { switch(defender->check_dark(environment(defender)->query_light())) { case -2: case 2: lght = -50; break; case -1: case 1: lght = -25; break; } } // mountedness // health (up to -25) hlth = - (25 - (defender->query_hp() * 25) / defender->query_max_hp()); // fatigue (up to -25) tmp = defender->query_specific_gp("fighting"); if(tmp < -50) hlth += -25; else if(tmp < 0) hlth += (tmp / 2); // Bonus for defensive tactics. switch((att->defender_tactics)->attitude) { case "insane": tact = -50; break; case "offensive": tact = -25; break; case "defensive": tact = 15; break; case "wimp": tact = 25; break; } // Protecting is a -15 modifier. Defending is an extra -15. This is because // it's easier to just jump in the way than to actually parry for someone. prot = 0; if(defender != att->opponent) { if(defender != att->person_hit) { // defending someone else with a shield is very tricky since shields // are setup only to protect you. if(defense_weapon->id("shield")) prot -= 15; prot -= 15; } prot -= 15; } // Small bonus for response of both. if((att->defender_tactics)->response == "both") oth += 5; if(defender->query_casting_spell()) oth -= 25; mod = wep + wght + dist + brdn + hnd + lght + hlth + tact + prot + oth; att->defense_modifier += mod; // Calculate how much time a parry/dodge takes. if(att->defense_action != "none") { actions = DEFENSE_COST; actions -= ((att->defender)->query_skill_bonus(att->defense_skill) + (att->attacker)->query_skill_bonus(TACTICAL_SKILL)) / 50; // heavier weapons are slower to parry with. if(att->defense_weapon && att->defense_weapon != att->defender) { if((att->defense_weapon)->id("shield")) actions += sqrt(att->defense_weapon->query_weight() / 4); else { actions += sqrt((att->defense_weapon->query_weight() * 2) / (limbs + 1)); if(sizeof((att->defender)->query_weapons()) > 1) actions -= DEFENSE_COST / 4; } } else if(att->defense_action == "dodge") actions += sqrt(defender->query_burden()); if(actions < DEFENSE_COST / 5) actions = DEFENSE_COST / 5; else if(actions > DEFENSE_COST * 2) actions = DEFENSE_COST * 2; att->defense_cost = actions; } #if DEBUG == 1 combat_debug("Defense %s (%s): wep: %d wght: %d dist: %d " "brdn: %d hnd: %d lght: %d hlth: %d tact: %d prot: %d " "oth: %d = %d [%d] (actions: %d %d)", att->defense_action, att->defense_weapon ? (att->defense_weapon)->query_name() : "", wep, wght, dist, brdn, hnd, lght, hlth, tact, prot, oth, mod, att->defense_modifier, actions, att->defender_defecit); #endif return att; } /**************************************************************************** * Now we're into the damage phase. ****************************************************************************/ /** * After a successful attack (or possibly a parried one) calculate the * amount of damage done to the opponent. * * This method must fill in the amount of damage to be done and may adjust * the time taken too. * * PRE: the outcome of an attack must have been determined. * POST: damage must be filled in. */ protected class attack calc_damage(class attack att) { int damage, weapon_damage; object attacker; // Damage is a weighted average of the weapon damage and the // players skill. attacker = att->attacker; damage = att->attack_data[AT_DAMAGE]; weapon_damage = damage; // This rather hampers unarmed, perhaps it should be removed. if(att->attack_weapon != attacker) damage = sqrt(damage * attacker->query_skill_bonus(att->attack_skill)); if(damage > 3 * weapon_damage) damage = 3 * weapon_damage; // As the combat gets slower so the damage goes up to compensate otherwise // people heal faster than they take damage. damage *= COMBAT_SPEED; att->damage = damage; switch(att->result) { case OFFAWARD: case OFFWIN: // Critical attacks do 2x damage, exceptional attacks 1.5x switch(att->degree) { case TASKER_CRITICAL: att->damage *= 2; break; case TASKER_EXCEPTIONAL: att->damage = (att->damage * 3) / 2; case TASKER_MARGINAL: att->damage /= 2; break; } break; case DEFAWARD: case DEFWIN: // A Critical or Exceptional parry can cause the opponent to use up // extra combat actions since the defense pushed the attackers weapon // out of position or brought the defender into a better position. switch(att->degree) { case TASKER_CRITICAL: att->defense_cost = 0; break; case TASKER_EXCEPTIONAL: att->defense_cost /= 2; break; case TASKER_MARGINAL: att->defense_cost *= 2; break; } // All damage is always stopped. att->damage = 0; } combat_debug("Damage: %d %d", att->damage, attacker->query_skill_bonus(att->attack_skill)); return att; } /** * Calculate how much of the damage is saved by the armour. * * PRE: all information has been filled in except the amount of damage * stopped by armour, and which piece of armour did the stopping. * POST: The damage stopped and which armour did it is filled in. * * @param class attack * @return class attack */ class attack calc_armour_protection(class attack att) { object person_hit; string armour_zone, damage_type; if(!att->damage || (att->result != OFFWIN && att->result != OFFAWARD)) return att; person_hit = att->person_hit; // Specials/Criticals may change this calculation. armour_zone = (person_hit->query_race_ob())-> map_armour_zone(att->target_zone); damage_type = att->attack_data[AT_TYPE]; if(damage_type == "unarmed") damage_type = "blunt"; att->armour_stopped = (int)person_hit->query_ac(damage_type, att->damage, armour_zone); if(att->armour_stopped < 0) { debug_printf("armour stopped %d for %s %d %s", att->armour_stopped, damage_type, att->damage, armour_zone); att->armour_stopped = 0; } return att; } /** * Do damage to the attacking & defending weapons. * * PRE: all information has been filled in * POST: the weapons involved have taken appropriate damage. * * @param class attack * @return class attack */ protected class attack damage_weapon(class attack att) { int off_damage, def_damage; switch(att->result) { case OFFAWARD: case OFFWIN: // The weapon hit the opponents armour. if(att->armour_stopped && att->degree) off_damage = att->armour_stopped / att->degree; // No def_damage needed since the query_ac() call damages the armour // directly (which is bad, but there we are). break; case DEFWIN: case DEFAWARD: // The defender successfully defended. If they parried then their // parrying weapon may take some damage and the attacking // weapon too. if(att->defense_action == "parry" && att->damage && att->degree) { off_damage = att->damage * (att->degree - 1); def_damage = att->damage / att->degree; } break; } if(off_damage) { if(att->attack_weapon == att->attacker) if((att->attack_weapon)->query_hp() > (off_damage / 10)) (att->attack_weapon)->adjust_hp(- (off_damage / 10), att->defender, att->defense_weapon, "hitting someone."); else if(function_exists("hit_weapon", att->attack_weapon)) (att->attack_weapon)->hit_weapon(off_damage, att->attack_data[AT_SKILL]); else (att->attack_weapon)->do_damage(att->attack_data[AT_SKILL], off_damage); } if(def_damage) { if(att->defense_weapon == att->defender) (att->defense_weapon)->adjust_hp(- def_damage, att->attacker, att->attack_weapon, "parrying an attack with a limb."); else if(function_exists("hit_weapon", att->defense_weapon)) (att->defense_weapon)->hit_weapon(def_damage, att->attack_data[AT_SKILL]); else (att->defense_weapon)->do_damage(att->attack_data[AT_SKILL], def_damage); } return att; } /** * Prepare the result messages. * * This method prepares the attack and defense messages setting up the 5 * element arrays of att_mess & def_mess. The elements are as follows: * 0 - the attacker, 1 - the opponent, 2 - everyone else, 3 - the defender * 4 - the person hit * * PRE: The attack has been done and the amount of damage done and stopped * has been calculated. * POST: The 5 element att_mess & def_mess arrays are populated. * * @param class attack */ protected class attack prepare_messages(class attack att) { object attacker, opponent, defender, person_hit; string *verb, def_short, margin; string *def_mess, *att_mess; int damage, armour_stopped; attacker = att->attacker; opponent = att->opponent; defender = att->defender; person_hit = att->person_hit; damage = att->damage; armour_stopped = att->armour_stopped; // Set attack part of the message. switch(att->result) { case OFFAWARD: tell_object(att->attacker, "%^YELLOW%^"+ replace(({ "You feel " "that your skill with $weapon$ has increased.", "You " "feel more able to use $weapon$.", "You seem to be a " "step closer to mastering $weapon$." })[random(3)], "$weapon$", (att->attack_weapon == att->attacker ? "unarmed combat" : (string)(att->attack_weapon)->a_short())) + "%^RESET%^\n" ); case OFFWIN: if(person_hit == opponent) { att_mess = ATTACK_MESSAGE->query_message(damage - armour_stopped, att->attack_data[AT_SKILL], opponent, attacker, att->attack_data[AT_NAME], att->attack_weapon, att->target_zone); if(sizeof(att_mess) != 3) { att_mess = ({"You hit "+opponent->the_short()+"", attacker->the_short()+" hits you", attacker->the_short()+" hits "+opponent->the_short()}); } att_mess += ({ att_mess[M_SPECTATORS], att_mess[M_SPECTATORS] }); } else { att_mess = ATTACK_MESSAGE->query_message(damage - armour_stopped, att->attack_data[AT_SKILL], opponent, attacker, att->attack_data[AT_NAME], att->attack_weapon, att->target_zone); if(sizeof(att_mess) != 3) { att_mess = ({"You almost hit "+opponent->the_short()+"", attacker->the_short()+" almost hits you", attacker->the_short()+" almost hits "+ opponent->the_short()}); } att_mess += ({ att_mess[M_SPECTATORS], att_mess[M_SPECTATORS] }); att_mess[M_ATTACKER] += " but at the last second " + person_hit->the_short() + " leaps in and protects " + opponent->query_objective(); att_mess[M_OPPONENT] += " but at the last second " + person_hit->the_short() + " leaps in and protects you"; att_mess[M_SPECTATORS] += " but at the last second " + person_hit->the_short() + " leaps in and protects " + opponent->query_objective(); if(defender == person_hit) att_mess[M_DEFENDER] += " but at the last second " + person_hit->the_short() + " leap in and protect " + opponent->query_objective(); else att_mess[M_DEFENDER] += " but at the last second " + person_hit->the_short() + " leaps in and protects " + opponent->query_objective(); att_mess[M_PERSON_HIT] += " but at the last second you leap in and " "protect " + opponent->query_objective(); } break; case DEFAWARD: case DEFWIN: att_mess = ATTACK_MESSAGE->query_message(0, att->attack_data[AT_SKILL], opponent, attacker, att->attack_data[AT_NAME], att->attack_weapon, att->target_zone); if(sizeof(att_mess) != 3) { att_mess = ({"You try to hit "+opponent->the_short()+"", attacker->the_short()+" tries to hit you", attacker->the_short()+" tries to hit "+ opponent->the_short()}); } att_mess += ({ att_mess[M_SPECTATORS], att_mess[M_SPECTATORS] }); break; } def_mess = allocate(5); def_mess[M_ATTACKER] = def_mess[M_OPPONENT] = def_mess[M_SPECTATORS] = def_mess[M_DEFENDER] = def_mess[M_PERSON_HIT] = ""; // Now set the response part ie. what happened after the attack. switch(att->result) { case OFFAWARD: case OFFWIN: // The attack was successful, so we add armour messages if appropriate. if(armour_stopped && armour_stopped > damage / 3) { if(objectp(att->stopped_by)) { def_mess[M_ATTACKER] = " but " + (att->stopped_by)->poss_short(); def_mess[M_OPPONENT] = def_mess[M_ATTACKER]; def_mess[M_SPECTATORS] = def_mess[M_ATTACKER]; def_mess[M_DEFENDER] = def_mess[M_ATTACKER]; def_mess[M_PERSON_HIT] = def_mess[M_ATTACKER]; } else if(att->stopped_by) { def_mess[M_ATTACKER] = " but " + person_hit->query_possessive() + " " + att->stopped_by; if(person_hit == opponent) def_mess[M_OPPONENT] = " but your " + att->stopped_by; else def_mess[M_OPPONENT] = def_mess[M_ATTACKER]; def_mess[M_SPECTATORS] = def_mess[M_ATTACKER]; if(person_hit == defender) def_mess[M_DEFENDER] = " but your " + att->stopped_by; else def_mess[M_DEFENDER] = def_mess[M_ATTACKER]; def_mess[M_PERSON_HIT] = " but your " + att->stopped_by; } // armour protected them, at least partly if(!damage || armour_stopped >= damage) { def_mess[M_ATTACKER] += " absorbs all of"; def_mess[M_OPPONENT] += " absorbs all of"; def_mess[M_SPECTATORS] += " absorbs all of"; def_mess[M_DEFENDER] += " absorbs all of"; def_mess[M_PERSON_HIT] += " absorbs all of"; } else if(armour_stopped > (damage * 2 / 3)) { def_mess[M_ATTACKER] += " absorbs most of"; def_mess[M_OPPONENT] += " absorbs most of"; def_mess[M_SPECTATORS] += " absorbs most of"; def_mess[M_DEFENDER] += " absorbs most of"; def_mess[M_PERSON_HIT] += " absorbs most of"; } else { def_mess[M_ATTACKER] += " absorbs some of"; def_mess[M_OPPONENT] += " absorbs some of"; def_mess[M_SPECTATORS] += " absorbs some of"; def_mess[M_DEFENDER] += " absorbs some of"; def_mess[M_PERSON_HIT] += " absorbs some of"; } def_mess[M_ATTACKER] += " the blow"; def_mess[M_OPPONENT] += " the blow"; def_mess[M_SPECTATORS] += " the blow"; def_mess[M_DEFENDER] += " the blow"; def_mess[M_PERSON_HIT] += " the blow"; } break; case DEFAWARD: if(att->defense_action == "parry") { if(att->defense_weapon && (att->defense_weapon)->query_weapon()) verb = ({ "parry", "parries", "parrying" }); else verb = ({ "block", "blocks", "blocking" }); tell_object(att->defender, "%^YELLOW%^"+ replace( ({ "You move more surely " "than you thought you could in $verbing$ $attacker$'s attack", "You just manage to $verb$ $attacker$'s attack, but you'll " "know better next time", "You feel better at $verbing$ as " "you $verb$ $attacker$'s attack" })[ random( 3 ) ], ({ "$verb$", verb[ 0 ], "$verbing$", verb[ 2 ], "$attacker$", (string)(att->attacker)->the_short() }) ) +".%^RESET%^\n" ); } else if(att->defense_action == "dodge") { tell_object(att->defender, "%^YELLOW%^"+ replace( ({ "You move more nimbly " "than you thought you could in dodging $attacker$", "You " "managed to predict $attacker$'s attack, letting you dodge it " "more easily", "You feel better at dodging as you avoid " "$attacker$'s attack" })[ random( 3 ) ], "$attacker$", (string)(att->attacker)->the_short() ) +".%^RESET%^\n" ); } case DEFWIN: // The defense was successful so we print parry/dodge messages as // appropriate. def_mess = allocate(5); def_mess[M_ATTACKER] = ""; def_mess[M_OPPONENT] = ""; def_mess[M_SPECTATORS] = ""; def_mess[M_DEFENDER] = ""; def_mess[M_PERSON_HIT] = ""; switch(att->degree) { case TASKER_MARGINAL: margin = "just "; break; case TASKER_EXCEPTIONAL: margin = "deftly "; break; case TASKER_MARGINAL: margin = "easily "; break; default: margin = ""; break; } if(att->defense_action == "parry" && att->defense_weapon) { if(defender == opponent) def_mess[M_ATTACKER] = " but " + defender->query_pronoun() + " " + margin; else def_mess[M_ATTACKER] = " but " + defender->the_short() + " " + margin; if(defender == opponent) def_mess[M_OPPONENT] = " but you " + margin; else def_mess[M_OPPONENT] = def_mess[M_ATTACKER]; def_mess[M_SPECTATORS] = def_mess[M_ATTACKER]; def_mess[M_DEFENDER] = " but you " + margin; def_mess[M_PERSON_HIT] = def_mess[M_ATTACKER]; if(att->defense_weapon != defender && !(att->defense_weapon)->id("shield")) { def_mess[M_ATTACKER] += "parries"; if(defender == opponent) def_mess[M_OPPONENT] += "parry"; else def_mess[M_OPPONENT] += "parries"; def_mess[M_SPECTATORS] += "parries"; def_mess[M_DEFENDER] += "parry"; if(att->person_hit == att->defender) def_mess[M_PERSON_HIT] += "parry"; else def_mess[M_PERSON_HIT] += "parries"; } else { def_mess[M_ATTACKER] += "blocks"; if(defender == opponent) def_mess[M_OPPONENT] += "block"; else def_mess[M_OPPONENT] += "blocks"; def_mess[M_SPECTATORS] += "blocks"; def_mess[M_DEFENDER] += "block"; if(att->person_hit == att->defender) def_mess[M_PERSON_HIT] += "block"; else def_mess[M_PERSON_HIT] += "blocks"; } if(att->defense_weapon != defender) { def_short = (att->defense_weapon)->short(); } else if(att->defense_limb) { def_short = att->defense_limb; } else def_short = (defender->query_race_ob())->map_target_zone("hands"); def_mess[M_ATTACKER] += " the blow with " + defender->query_possessive() + " " + def_short; if(defender == opponent) def_mess[M_OPPONENT] += " the blow with your " + def_short; else def_mess[M_OPPONENT] += " the blow with " + defender->query_possessive() + " " + def_short; def_mess[M_SPECTATORS] += " the blow with " + defender->query_possessive() + " " + def_short; def_mess[M_DEFENDER] += " the blow with your " + def_short; if(defender == person_hit) def_mess[M_PERSON_HIT] += " the blow with your " + def_short; else def_mess[M_PERSON_HIT] += " the blow with " + defender->query_possessive() + " " + def_short; } else if(att->defense_action == "dodge") { def_mess[M_ATTACKER] = " but " + defender->query_pronoun() + " " + margin + "dodges out of the way"; def_mess[M_OPPONENT] = " but you " + margin + "dodge out of the way"; def_mess[M_SPECTATORS] = " but " + defender->query_pronoun() + " " + margin + "dodges out of the way"; } else { def_mess[M_ATTACKER] = " but, although unable to defend, " + defender->query_pronoun() + " somehow avoids the attack"; def_mess[M_OPPONENT] = " but, although unable to defend, you somehow " "avoid the attack"; def_mess[M_SPECTATORS] = " but, although unable to defend, " + defender->query_pronoun() + " somehow avoids the attack"; } } att->attack_messages = att_mess; att->defense_messages = def_mess; if(att->result == DEFAWARD || att->result == DEFWIN || !damage || armour_stopped >= damage) att->verbose = 1; return att; } /** * Write out the attack/defense messages. * * PRE: all information has been filled in * POST: the appropriate messages have been written out. * * @param class attack * @return class attack */ protected class attack write_messages(class attack att) { int verbose; verbose = att->verbose; (att->attacker)->event_combat(att->attacker, att->attack_messages[M_ATTACKER] + att->defense_messages[M_ATTACKER] + ".\n", ({ }), verbose); (att->opponent)->event_combat(att->opponent, att->attack_messages[M_OPPONENT] + att->defense_messages[M_OPPONENT] + ".\n", ({ }), verbose); if(att->defender != att->opponent && sizeof(att->attack_messages) > M_DEFENDER) (att->defender)->event_combat(att->defender, att->attack_messages[M_DEFENDER] + att->defense_messages[M_DEFENDER] + ".\n", ({ }), verbose); if(att->person_hit != att->opponent && att->person_hit != att->defender && sizeof(att->attack_messages) > M_PERSON_HIT) (att->person_hit)->event_combat(att->person_hit, att->attack_messages[M_PERSON_HIT] + att->defense_messages[M_PERSON_HIT] + ".\n", ({ }), verbose); event(environment(att->attacker), "combat", att->attack_messages[M_SPECTATORS] + att->defense_messages[M_SPECTATORS] + ".\n", ({ att->attacker, att->opponent, att->defender, att->person_hit }), verbose); return att; } /** * Adjust the time left, combat actions and gp used by this attack. * This calls adjust_action_defecit in both the attacker and defender * as appropriate. * * @param class attack */ protected void adjust_actions(class attack att) { int tmp; // Reduce the attackers time left. if((att->attacker)->query_time_left() > 0) (att->attacker)->adjust_time_left(-DEFAULT_TIME); // Reduce their actions. (att->attacker)->adjust_action_defecit(att->attack_cost); (att->opponent)->adjust_action_defecit(att->defense_cost); // Extra cost for defenders if(att->defender != att->opponent) (att->defender)->adjust_action_defecit(DEFENSE_COST); // Extra cost for protectors if(att->person_hit != att->opponent) (att->person_hit)->adjust_action_defecit(DEFENSE_COST); // Reduce their GP tmp = ATTACK_GP[(att->attacker_tactics)->attitude]; (att->attacker)->adjust_gp(-tmp); if(att->defender != att->opponent) { tmp = DEFENDER_GP[(att->defender_tactics)->attitude]; (att->defender)->adjust_gp(-tmp); } else if(att->defense_action != "none") { tmp = DEFENSE_GP[(att->defender_tactics)->attitude]; (att->opponent)->adjust_gp(-tmp); } if(att->person_hit != att->opponent) { tmp = DEFENDER_GP[(att->person_hit)->query_combat_attitude()]; (att->person_hit)->adjust_gp(-tmp); } } /** * This function is called as part of the post attack processing and performs * the checks to see if someone can move closer or further away from their * opponent. * * @param class attack * @return class attack * @see after_attack() */ protected class attack perform_movement(class attack att) { int ideal, move_cost, closer; // Figure out if they will attempt to move-in or move-away from their // opponent. ideal = DISTANCES[(att->attacker_tactics)->ideal_distance]; // If the difference between the actual and ideal distance is greater than // the acceptable difference defined for our attitude we'll attempt to // move in or out to compensate. if(ABS(ideal - att->distance) > MOVE_DISTANCE[(att->attacker_tactics)->attitude]) { if(att->distance > ideal) closer = 1; move_cost = MOVE_COST; move_cost -= (att->attacker)->query_skill_bonus(MOVE_SKILL) / 30; if(move_cost > 0) att->attack_cost += MOVE_COST; switch(TASKER->compare_skills(att->attacker, MOVE_SKILL, att->defender, MOVE_SKILL, att->attack_modifier - att->defense_modifier, TM_CONTINUOUS, TM_CONTINUOUS, 0)) { case OFFAWARD: tell_object(att->attacker, "%^YELLOW%^You feel that your tactical skill has increased." "%^RESET%^\n"); case OFFWIN: if(closer) { (att->attacker)->event_combat(att->attacker, "You move in closer to " + (att->opponent)->one_short() + ".\n", ({ }), 0); (att->opponent)->event_combat(att->opponent, (att->attacker)->one_short() + " moves in closer to you.\n", ({ }), 0); event(environment(att->attacker), "combat", (att->attacker)->one_short() + " moves in closer to " + (att->opponent)->one_short() + ".\n", ({ att->attacker, att->opponent }), 1); set_distance(att->opponent, query_distance(att->opponent) - DISTANCE_STEP); (att->opponent)->set_distance(att->attacker, query_distance(att->opponent)); } else { (att->attacker)->event_combat(att->attacker, "You move further away from " + (att->opponent)->one_short() + ".\n", ({ }), 0); (att->opponent)->event_combat(att->opponent, (att->attacker)->one_short() + " moves further away from you.\n", ({ }), 0); event(environment(att->attacker), "combat", (att->attacker)->one_short() + " moves further away from " + (att->opponent)->one_short() + ".\n", ({ att->attacker, att->opponent }), 1); set_distance(att->opponent, query_distance(att->opponent) + DISTANCE_STEP); (att->opponent)->set_distance(att->attacker, query_distance(att->opponent)); } break; case DEFAWARD: tell_object(att->opponent, "%^YELOW%^You feel that your tactical skill has increased." "%^RESET%^\n"); case DEFWIN: if(closer) { (att->attacker)->event_combat(att->attacker, "You try to move in closer to " + (att->opponent)->one_short() + " but cannot find " "an opportunity.\n", ({ }), 1); (att->opponent)->event_combat(att->opponent, (att->attacker)->one_short() + " tries to move in closer to you " "but you don't give " + (att->attacker)->query_objective() + " an opporunity.\n", ({ }), 1); event(environment(att->attacker), (att->attacker)->one_short() + " tries to move in closer to " + (att->opponent)->one_short() + " but " + (att->opponent)->query_pronoun() + " doesn't give " + (att->attacker)->query_objective() + " an opportunity.\n", ({ att->attacker, att->opponent }), 1); } else { (att->attacker)->event_combat(att->attacker, "You try to move further away from " + (att->opponent)->one_short() + " but cannot find an opportunity.\n", ({ }), 1); (att->opponent)->event_combat(att->opponent, (att->attacker)->one_short() + " tries to move further away from you " "but you don't give " + (att->attacker)->query_objective() + " an opporunity.\n", ({ }) , 1); event(environment(att->attacker), "combat", (att->attacker)->one_short() + " tries to move further away from " + (att->opponent)->one_short() + " but " + (att->opponent)->query_pronoun() + " doesn't give " + (att->attacker)->query_objective() + " an opportunity.\n", ({ att->attacker, att->opponent }), 1); } } } return att; } /** * This function is called after it's all over and done (just before the * costs are charged). It can be used to do any special outcomes that * we may decide on. * Note that if you override this function in a special be absolutely certain * you know what you're doing if you return R_ABORT or R_DONE. Use of either * of these two could cause really nasty side-effects. * * @param class attack * @return class attack */ protected class attack after_attack(class attack att) { string *locations; int total, count, lperc, hperc; #ifdef USE_DISTANCE if((att->result == OFFWIN || att->result == OFFAWARD) && (att->attacker_tactics)->ideal_distance && environment(att->attacker) == environment(att->opponent)) att = perform_movement(att); #endif adjust_actions(att); if((att->damage - att->armour_stopped) <= 0) return att; // Do weapon special attacks. if((environment(att->person_hit) == environment(att->attacker)) && (att->result == OFFWIN || att->result == OFFAWARD)) { (att->attack_weapon)->attack_function(att->attack_data[AT_NAME], att->damage - att->armour_stopped, att->person_hit, att->attacker); } // Determine special injuries if enough damage was done. locations = (att->defender->query_race_ob())->query_target_zones(); total = sizeof(locations); tmpzone = att->target_zone; locations = filter(locations, (: $1 == tmpzone :)); count = sizeof(locations); lperc = (count * 100) / total; if(att->degree == TASKER_CRITICAL) { hperc = ((att->damage - att->armour_stopped) * 100) / (att->opponent)->query_max_hp(); if(lperc < hperc) { switch(att->target_zone) { case "head": // Unconscious break; case "chest": case "stomach": break; case "arm": // broken arm. break; case "leg": // broken leg. break; } } } return att; } /** * Go through the list of the attacker & opponents specials and * call any that have registered for this stage. */ protected mixed *call_special(int stage, class attack att) { class combat_special *tmp; mixed *result; int i; if(!this_object()->query_no_specials()) { tmp = att->attacker_specials; for(i=0; i<sizeof(tmp); i++) { if((tmp[i]->type & T_OFFENSIVE) && (tmp[i]->events & stage)) { if(functionp(tmp[i]->callback)) result = evaluate(tmp[i]->callback, stage, att, tmp[i]->data); else if(arrayp(tmp[i]->callback)) { if(!tmp[i]->callback[0] || !tmp[i]->callback[1]) { (att->attacker)->remove_special(tmp[i]->id); } else result = call_other(tmp[i]->callback[0], tmp[i]->callback[1], stage, att, tmp[i]->data); } if(sizeof(result)) { if(!classp(result[1])) { debug_printf("Error, call_special received invalid return " "data. %O when calling %O stage %d\n", result, tmp[i]->callback, stage); result[0] = R_REMOVE_ME | R_ABORT; } if(result[0] & R_REMOVE_ME) { (att->attacker)->remove_special(tmp[i]->id); } else { // Set the data to be the new data in both the current attack // data & in the original info. tmp[i]->data = result[2]; (att->attacker)->set_special_data(tmp[i]->id, result[2]); } if(result[0] & R_ABORT) return result; if((result[0] & R_DONE) || (result[0] & R_CONTINUE)) break; } } } } if(att->defender && !(att->defender)->query_no_specials()) { tmp = att->defender_specials; for(i=0; i<sizeof(tmp); i++) { if((tmp[i]->type & T_DEFENSIVE) && (tmp[i]->events & stage)) { if(functionp(tmp[i]->callback)) result = evaluate(tmp[i]->callback, stage, att, tmp[i]->data); else if(arrayp(tmp[i]->callback)) { if(!tmp[i]->callback[0] || !tmp[i]->callback[1]) { (att->defender)->remove_special(tmp[i]->id); } else result = call_other(tmp[i]->callback[0], tmp[i]->callback[1], stage, att, tmp[i]->data); } if(sizeof(result)) { if(!classp(result[1])) { debug_printf("Error, call_special received invalid return " "data. %O when calling %O stage %d", result, tmp[i]->callback, stage); result[0] = R_REMOVE_ME | R_ABORT; } if(result[0] & R_REMOVE_ME) { (att->defender)->remove_special(tmp[i]->id); } else { // Set the data to be the new data. (att->defender)->set_special_data(tmp[i]->id, result[2]); } if(result[0] & R_ABORT) return result; // Done or continue we stop processing specials. // R_PASSTHRU means try the next special. if((result[0] & R_DONE) || (result[0] & R_CONTINUE)) break; } } } } return result; } /** * This performs the actual attack itself. * All attack data, everything we know about the attacker and the defender * is stored in the attack class which is passed into and back by every * function at every step of the attack. */ void do_attack() { class tasker_result result; class attack att; mixed *sp_result; int modifier, tmp; // This is required since some people have the old tactics settings :( init_tactics(); if(_callout) _callout = remove_call_out("announce_intent"); att = new(class attack, attacker : this_object(), attacker_tactics : this_object()->query_tactics(), attacker_specials : this_object()->query_specials(), attacker_concentrating : this_object()->query_concentrating(), attacker_defecit : this_object()->query_action_defecit(), attacker_last_opponent : this_object()->query_last_opponent(), attacker_last_weapon : this_object()->query_last_weapon(), attacker_last_action : this_object()->query_last_action(), attacker_last_result : this_object()->query_last_result(), repeat : 0 ); // choose opponent sp_result = call_special(E_OPPONENT_SELECTION, att); if(!sp_result || sp_result[0] & R_CONTINUE || sp_result[0] & R_PASSTHRU) att = this_object()->choose_opponent(att); else if(!(sp_result[0] & R_ABORT) && classp(sp_result[1])) att = (class attack)sp_result[1]; else return; if(!(att->opponent) || !(att->opponent)->attack_by(att->attacker)) { combat_debug("No opponent chosen, aborting attack"); return; } event(environment(att->attacker), "fight_in_progress", att->opponent); clear_protection(att->attacker, att->opponent); _combat->last_opponent = att->opponent; // This loop causes us to allow a defender to defend and if the defender // fails & isn't the opponent the opponent also gets a chance to defend. do { // Now lets figure out who will do the defense, the person being attacked // or someone/thing that wants to defend them. sp_result = call_special(E_DEFENDER_SELECTION, att); if(!sp_result || sp_result[0] & R_CONTINUE || sp_result[0] & R_PASSTHRU) att = (att->opponent)->choose_defender(att); else if(!(sp_result[0] & R_ABORT) && classp(sp_result[1])) att = (class attack)sp_result[1]; else return; att->defender_tactics = (att->defender)->query_tactics(); att->defender_concentrating = (att->defender)->query_concentrating(); att->defender_last_opponent = (att->defender)->query_last_opponent(); att->defender_last_action = (att->defender)->query_last_action(); att->defender_last_result = (att->defender)->query_last_result(); att->defender_defecit = (att->defender)->query_action_defecit(); att->defender_specials = (att->defender)->query_specials(); clear_protection(att->attacker, att->defender); // get weapon and attack data sp_result = call_special(E_ATTACK_SELECTION, att); if(!sp_result || sp_result[0] & R_CONTINUE || sp_result[0] & R_PASSTHRU) att = this_object()->choose_attack(att); else if(!(sp_result[0] & R_ABORT) && classp(sp_result[1])) att = (class attack)sp_result[1]; else return; if(!att->attack_weapon || !sizeof(att->attack_data) || !att->attack_skill){ combat_debug("Missing attack weapon, attack data or attack skill, " "aborting attack."); return; } // set the 'last' data. _combat->last_action = att->attack_data[AT_NAME]; _combat->last_weapon = att->attack_weapon; // will opponent defend? If so determine defense skill & weapon. sp_result = call_special(E_DEFENSE_SELECTION, att); if(!sp_result || sp_result[0] & R_CONTINUE || sp_result[0] & R_PASSTHRU) att = (att->defender)->choose_defense(att); else if(!(sp_result[0] & R_ABORT) && classp(sp_result[1])) att = (class attack)sp_result[1]; else return; // Set the 'last' data. (att->opponent)->set_last_opponent(att->attacker); if(att->opponent != att->defender) (att->defender)->set_last_opponent(att->attacker); (att->defender)->set_last_action(att->defense_action); (att->defender)->set_last_weapon(att->defense_weapon); if(att->person_hit != att->opponent && att->person_hit != att->defender) (att->person_hit)->set_last_opponent(att->attacker); // determine attack modifier sp_result = call_special(E_ATTACK_MODIFIER, att); if(!sp_result || sp_result[0] & R_CONTINUE || sp_result[0] & R_PASSTHRU) { att = this_object()->calc_attack_modifier(att); } else if(!(sp_result[0] & R_ABORT) && classp(sp_result[1])) { att = (class attack)sp_result[1]; } else return; // determine defense modifier // If they aren't defending they just get a big fat minus. if(!att->defense_action || att->defense_action == "none") att->defense_modifier += -1000; else { sp_result = call_special(E_DEFENSE_MODIFIER, att); if(!sp_result || sp_result[0] & R_CONTINUE || sp_result[0] & R_PASSTHRU) att = (att->defender)->calc_defense_modifier(att); else if(!(sp_result[0] & R_ABORT) && classp(sp_result[1])) att = (class attack)sp_result[1]; else return; } tmp = att->attack_modifier - att->defense_modifier + BALANCE_MOD; modifier = tmp; // This flattens the modifier out a bit preventing crazy modifiers // against extreme skills. if(modifier > 25) modifier = sqrt(modifier * 25); else if(modifier < -25) modifier = -sqrt(-modifier * 25); // compare_skills result = TASKER->compare_skills(att->attacker, att->attack_skill, att->defender, att->defense_skill, modifier, TM_CONTINUOUS, TM_CONTINUOUS, 1); att->result = result->result; att->degree = result->degree; // Set the last_result data. _combat->last_result = att->result; (att->defender)->set_last_result(att->result); // If they got hit while being defended then reduce the defenders actions // and let them defend themselves. if((att->result == OFFWIN || att->result == OFFAWARD) && att->defender != att->opponent && !att->repeat) { // charge the defender (att->defender)->adjust_action_defecit(att->defense_cost); (att->defender)->adjust_gp(-(DEFENSE_GP[att->defender_tactics->attitude])); // mark them to defend themselves. att->defender = att->opponent; att->repeat = 1; } else att->repeat = 0; } while(att->repeat); // calculate damage sp_result = call_special(E_DAMAGE_CALCULATION, att); if(!sp_result || sp_result[0] & R_CONTINUE || sp_result[0] & R_PASSTHRU) att = this_object()->calc_damage(att); else if(!(sp_result[0] & R_ABORT) && classp(sp_result[1])) att = (class attack)sp_result[1]; else return; // see how much damage was stopped by armour. sp_result = call_special(E_ARMOUR_CALCULATION, att); if(!sp_result || sp_result[0] & R_CONTINUE || sp_result[0] & R_PASSTHRU) att = (att->opponent)->calc_armour_protection(att); else if(!(sp_result[0] & R_ABORT) && classp(sp_result[1])) att = (class attack)sp_result[1]; else return; #if DEBUG == 1 combat_debug("Outcome: mod: %d, res: %d, deg: %d dam: %d arm: %d", att->attack_modifier - att->defense_modifier + BALANCE_MOD, att->result, att->degree, att->damage, att->armour_stopped); #endif // We'll only damage people if the attack succeeded. if(att->result == OFFWIN || att->result == OFFAWARD) { // and which armour stopped the attack. att->stopped_by = (att->person_hit)->query_stopped(); #if DEBUG == 2 combat_debug("hit: %s, weapon: %s damage: %d, armour: %d, reducing hp by " "%d to %d", (att->person_hit)->query_name(), (att->attack_weapon)->query_name(), att->damage, att->armour_stopped, (att->damage - att->armour_stopped), (att->person_hit)->query_hp()); #endif } // Prepare the offense/defense messages. att = this_object()->prepare_messages(att); if(sizeof(att->attack_messages) != 5 || sizeof(att->defense_messages) != 5) { debug_printf("No attack or defense messages.\n"); return; } // Write out the offense/defense messages. sp_result = call_special(E_WRITE_MESSAGES, att); if(!sp_result || sp_result[0] & R_CONTINUE || sp_result[0] & R_PASSTHRU) att = this_object()->write_messages(att); else if(!(sp_result[0] & R_ABORT) && classp(sp_result[1])) att = (class attack)sp_result[1]; else return; // Damage the person hit if(att->damage - att->armour_stopped > 0) (att->person_hit)->adjust_hp(-(att->damage - att->armour_stopped), att->attacker, att->attack_weapon, _combat->last_action); // damage the weapon. sp_result = call_special(E_WEAPON_DAMAGE, att); if(!sp_result || sp_result[0] & R_CONTINUE || sp_result[0] & R_PASSTHRU) att = this_object()->damage_weapon(att); else if(!(sp_result[0] & R_ABORT) && classp(sp_result[1])) att = (class attack)sp_result[1]; else return; // Do post attack stuff. sp_result = call_special(E_AFTER_ATTACK, att); if(!sp_result || sp_result[0] & R_CONTINUE || sp_result[0] & R_PASSTHRU) att = this_object()->after_attack(att); else if(!(sp_result[0] & R_ABORT) && classp(sp_result[1])) att = (class attack)sp_result[1]; else return; } /* * Figure out who should be in our attacker list and who should be in our * hunting list. */ void recalc_hunting_list() { mixed ob; object tmp; tmp = 0; foreach(ob in keys(_combat->attacker_list)) { if(stringp(ob) && find_player(ob)) tmp = find_player(ob); else if(ob && objectp(ob)) tmp = ob; if(!tmp) { // This removes the zeros left by dested objects. _combat->attacker_list = filter_mapping(_combat->attacker_list,(: $1 :)); } else if(tmp->query_property("dead") || !environment(tmp) || base_name(environment(tmp)) == "/room/rubbish") { map_delete(_combat->attacker_list, ob); } else if(environment(tmp) != environment() || !tmp->query_visible(this_object())) { // Add to hunting list, remove from attacker list. _combat->hunting_list[ob] = time(); map_delete(_combat->attacker_list, ob); } } tmp = 0; foreach(ob in keys(_combat->hunting_list)) { if(stringp(ob) && find_player(ob)) tmp = find_player(ob); else if(ob && objectp(ob)) tmp = ob; if(!tmp) { // This removes the zeros left by dested objects. _combat->hunting_list = filter_mapping(_combat->hunting_list, (: $1 :)); map_delete(_combat->hunting_list, ob); } else if(tmp->query_property("dead") || base_name(environment(tmp)) == "/room/rubbish") { map_delete(_combat->hunting_list, ob); } else if(environment(tmp) == environment() && tmp->query_visible(this_object())) { // If they are here and we can see them move them from the hunted // list to the attacker list. _combat->attacker_list[ob] = INITIAL_DISTANCE; map_delete(_combat->hunting_list, ob); } else if(_combat->hunting_list[ob] < time() - HUNTING_TIME) { // If we've been hunting them too long stop hunting them. this_object()->event_combat(this_object(), "You stop hunting " + tmp->one_short() + ".\n", ({ }), 1); map_delete(_combat->hunting_list, ob); } } } /** * Display the hitpoint monitor. */ void monitor_points() { int hp; int max; string colour; #ifdef UNUSED this_object()->clear_gp_info(); if ( (int)this_object()->adjust_gp( -1 ) < 0 ) return; #endif hp = (int)this_object()->query_hp(); if(hp < 0) hp = 0; max = (int)this_object()->query_max_hp(); switch ( ( hp * 100 ) / max) { case 50 .. 100 : colour = "%^GREEN%^"; break; case 20 .. 49 : colour = "%^YELLOW%^"; break; default : colour = "%^RED%^"; } tell_object( this_object(), colour +"Hp: "+ hp +" (" + max + ") Gp: "+ (int)this_object()->query_gp() +" (" + this_object()->query_max_gp() + ") Xp: " + this_object()->query_xp() + "%^RESET%^\n" ); } /** @ignore yes */ void announce_intent(object opponent) { int difficulty = 50; object *things, ob; if(environment(opponent) != environment(this_object())) return; switch(this_object()->check_dark((int)environment(this_object())->query_light())) { case -2: case 2: difficulty *= 4; break; case -1: case 1: difficulty *= 2; } things = ({ }); foreach(ob in filter(all_inventory(environment(this_object())), (: $1 &&living($1) :))) { switch(TASKER->perform_task(ob, "other.perception", difficulty, TM_FREE)){ case AWARD: tell_object(ob, "%^YELLOW%^You feel very perceptive." "%^RESET%^\n"); case SUCCEED: if(interactive(ob) && !ob->query_verbose("combat")) things += ({ ob }); break; case FAIL: things += ({ ob }); } } opponent->add_message((string)this_object()->one_short() + " $V$0=moves,move$V$ aggressively towards you!\n", ({ })); event(environment(opponent), "see", (string)this_object()->one_short() + " $V$0=moves,move$V$ aggressively towards "+ (string)opponent->one_short() +"!\n", this_object(), ({ opponent, this_object() }) + things); } /** * This method is called when combat starts. You can overload this function * to make your NPC do things at the start of combat. * You do not need to call ::start_combat() since this is just a stub function. * * @param opponent The first opponent to be attacked. */ void start_combat(object opponent) { return; } /** * This method is called when combat ends. You can overload this function * to make your NPC do things at the end of combat. * You do not need to call ::end_combat() since this is just a stub function. */ void end_combat() { return; } /** @ignore yes */ void heart_beat() { int i; // are they in combat? if(!_combat->in_combat) return; recalc_hunting_list(); if(!sizeof(keys(_combat->attacker_list)) && !sizeof(keys(_combat->hunting_list))) { // Not in combat so remove any non-continuous specials. for(i=0; i<sizeof(_combat->specials); i++) if(!((_combat->specials[i])->type & T_CONTINUOUS)) _combat->specials -= ({ (_combat->specials)[i] }); // mark as not in combat _combat->in_combat = 0; adjust_action_defecit(-MAX_ACTION_DEFECIT); end_combat(); return; } // Are they in a fit state to fight & do they have anyone to fight? if(!query_attackable() || !sizeof(keys(_combat->attacker_list))) return; // They get part of their combat actions here (assuming they aren't // trying to do something else as well) and part later. if((!userp(this_object()) || this_object()->query_time_left() == ROUND_TIME) && !this_object()->query_casting_spell()) { adjust_action_defecit(-(ACTIONS_PER_HB / (COMBAT_SPEED + 1))); } // Used to control combat speed, especially during testing. if(_combat->hbc++ % COMBAT_SPEED) return; _combat->hbc = 0; if(this_object()->query_monitor() && this_object()->query_monitor() <= ++(_combat->mbc)) { monitor_points(); _combat->mbc = 0; } do_attack(); if(!this_object()->query_casting_spell()) { adjust_action_defecit(-(ACTIONS_PER_HB / (COMBAT_SPEED + 1))); // Give a bonus attack if they have no action defecit. if(_combat->action_defecit < (OFFENSIVE_DEFECITS[_tactics->attitude] / 2)) { #if DEBUG == 1 combat_debug("Adding bonus attack for %s", this_object()->query_name()); debug_printf("Bonus attack for %s [%d, %d]", this_object()->query_name(), _combat->action_defecit, OFFENSIVE_DEFECITS[_tactics->attitude]); #endif call_out("do_attack", 1); } } } /** * Register a combat special. * * The combat system maintains a list of combat specials. These specials can * be used to override or alter any facet of a combat attack (or defense). * <p> * A special must register what type of special it is:<br> * T_OFFENSIVE - an offensive special<br> * T_DEFENSIVE - a defensive special<br> * T_CONTINUOUS - can be used with one of the above to indicate that the * special is continuous and not a one-time event.<br> * <p> * It also indicates which stages of combat it wants to be notified of:<br> * E_OPPONENT_SELECTION - selection of an opponent<br> * E_DEFENDER_SELECTION - selection of the defender<br> * E_ATTACK_SELECTION - selecting which attack the attacker will perform<br> * E_DEFENSE_SELECTION - selection of the method of defense<br> * E_ATTACK_MODIFIER - calculating the modifier for attack (how easy/hard it * is)<br> * E_DEFENSE_MODIFIER - doing the same for the defense<br> * E_DAMAGE_CALCULATION - calculating how much damage the attack can do<br> * E_ARMOUR_CALCULATION - calculating how much damage the armour will stop<br> * E_WEAPON_DAMAGE - performing damage to the weapons involved<br> * E_WRITE_MESSAGES - writing out the success/failure messages<br> * E_AFTER_ATTACK - any post-attack cleanup.<br> * <p> * The special provides the combat system with a callback function to be * called and any additional data the special wants tracked by the combat * system. * <p> * Then, when combat reaches one of the stages requested it calls the callback * function. This function is passed the stage we're at, all the data for this * attack and also the extra data the special requested. * <p> * The callback can then modify any aspect of the attack data (it could change * the attack skill, the defensive weapon, the defense modifier, anything at * all) and then return back into combat. * <p> * The return tells the combat system what to do next. The options are:<br> * R_CONTINUE - continue as though the special hadn't done anything.<br> * R_DONE - treat this step as completed.<br> * R_ABORT - abort the attack<br> * R_REMOVE_ME - do one of the above and then remove the special. * * @param type Is this an offensive or defensive special. * @param events A bitmap of the events this special is interested in. * @param the function to be called. A mixed array containing an object & a * function string. * @return the id of the special. * * @example * * This example registers a special that causes the player to be unable to * defend themselves for three rounds. * * id = player->register_special(T_DEFENSIVE | T_CONTINUOUS, * E_DEFENSE_SELECTION | E_DAMAGE_CALCULATION, * ({ base_name(this_object()), "callback" }), * ({ player, 0})); * * mixed *callback(int stage, class attack att, mixed data) { * // If it's not our player defending we won't do anything. * if(att->defender != data[1]) * return ({ R_CONTINUE, att, data }); * * switch(stage) { * case E_DEFENSE_SELECTION: // We won't let them defend themselves! * att->defense_action = "none"; * return ({ R_DONE, att, data }); * case E_DAMAGE_CALCULATION: * // If they managed to defend themself, turn it into a failure. * if(att->result == DEFWIN || att->result == DEFAWARD) * att->result = OFFWIN; * * // By using R_CONTINUE they take 500 hits _plus_ the normal damage. * att->damage = 500; * * // The R_REMOVE_ME causes our special to disappear once we've hurt * // them three times. * if(data[1] > 3) * return ({ R_CONTINUE & R_REMOVE_ME, att, data }); * * data[1]++; * return ({ R_CONTINUE, att, data }); * } * } */ int register_special(int type, int events, mixed callback, mixed data) { _combat->specials += ({ new(class combat_special, id : ++(_combat->special_id), type : type, events : events, callback : callback, data : data) }); return _combat->special_id; } /** * Remove a combat special. * * @param id The id of the special * @return 1 for success, 0 for failure */ int remove_special(int id) { int i; for(i=0; i<sizeof(_combat->specials); i++) { if((_combat->specials[i])->id == id) { _combat->specials -= ({ (_combat->specials)[i] }); return 1; } } debug_printf("Failed to remove special %d", id); return 0; } /** * List the specials currently registered for this npc/player. * @return an array of comba_special classes. */ class combat_special *query_specials() { return copy(_combat->specials); } /** * Directly set the special list. Use with care! * @param class combat_special * */ void set_specials(class combat_special *specials) { _combat->specials = specials; } /** * Update the user data for a specific special. * @param id The id of the special * @param data The data to be set * @return True or false for success or failure */ int set_special_data(int id, mixed data) { int i; for(i=0; i<sizeof(_combat->specials); i++) { if((_combat->specials[i])->id == id) { _combat->specials[i]->data = data; return 1; } } return 0; } void adjust_action_defecit(int amount) { _combat->action_defecit += amount; if(_combat->action_defecit > MAX_ACTION_DEFECIT) _combat->action_defecit = MAX_ACTION_DEFECIT; else if(_combat->action_defecit < MIN_ACTION_DEFECIT) _combat->action_defecit = MIN_ACTION_DEFECIT; } int query_action_defecit() { return _combat->action_defecit; } /** * This method returns the current tactics set using the tactics class. * @see set_tactics() * @index tactics */ class tactics query_tactics() { init_tactics(); return copy(_tactics); } /** * This sets the current attitude to use in combat. * @param attitude the new combat attitude * @see query_tactics() */ void set_tactics(class tactics new_tactics) { _tactics = new_tactics; } string query_combat_attitude() { return _tactics->attitude; } void set_combat_attitude(string attitude) { _tactics->attitude = attitude; } string query_combat_response() { return _tactics->response; } void set_combat_response(string response) { _tactics->response = response; } string query_combat_attack() { return _tactics->attack; } void set_combat_attack(string attack) { _tactics->attack = attack; } string query_combat_parry() { return _tactics->parry; } void set_combat_parry(string parry) { _tactics->parry = parry; } int query_unarmed_parry() { return _tactics->parry_unarmed; } void set_unarmed_parry(int parry) { _tactics->parry_unarmed = parry; } string query_combat_mercy() { return _tactics->mercy; } void set_combat_mercy(string mercy) { _tactics->mercy = mercy; } string query_combat_focus() { return _tactics->focus_zone; } void set_combat_focus(string focus) { _tactics->focus_zone = focus; } string query_combat_distance() { return _tactics->ideal_distance; } void set_combat_distance(string distance) { _tactics->ideal_distance = distance; } /** * This method returns the current array of protectors on the living * object. This is the people who are protecting us, so if we are hit * make them attack the hitter. * @return the current protectors array * @see add_protector() * @see remove_protector() */ object *query_protectors() { _combat->protectors -= ({ 0 }); return copy(_combat->protectors + ({ })); } /** * This method will add a protector to the current list of protectors * for this living object. * @param thing the protector to add * @see remove_protector() * @see query_protectors() */ int add_protector( object thing ) { if(thing == this_object() || thing->query_property("dead") || member_array(this_object(), thing->query_protectors() + thing->query_defenders()) != -1 || member_array(thing, query_attacker_list()) != -1) return 0; if(member_array(thing, _combat->protectors) == -1) _combat->protectors += ({ thing }); return 1; } /** * This method will remove a protector to the current list of protectors * for this living object. * @param thing the protector to remove * @return 1 for success, 0 for failure. * * @see add_protector() * @see query_protectors() */ int remove_protector(object protector) { if(member_array(protector, _combat->protectors) == -1) return 0; _combat->protectors -= ({ protector }); return 1; } /** * This method resets the protector array back to being nothing. */ void reset_protectors() { _combat->protectors = ({ }); } /** * This method returns the current array of defenders on the living * object. This is the people who are protecting us, so if we are hit * make them attack the hitter. * @return the current defenders array * @see add_protector() * @see remove_protector() */ object *query_defenders() { _combat->defenders -= ({ 0 }); return copy(_combat->defenders + ({ })); } /** * This method will add a defender to the current list of defenders * for this living object. * @param thing the defender to add * @see remove_defender() * @see query_defenders() */ int add_defender( object thing ) { if(thing == this_object() || thing->query_property("dead") || member_array(this_object(), thing->query_defenders() + thing->query_protectors()) != -1 || member_array(thing, query_attacker_list()) != -1) return 0; if(member_array(thing, _combat->defenders) == -1) _combat->defenders += ({ thing }); return 1; } /** * This method will remove a defender to the current list of defenders * for this living object. * @param thing the defender to remove * @return 1 for success, 0 for failure. * * @see add_defender() * @see query_defenders() */ int remove_defender(object defender) { if(member_array(defender, _combat->defenders) == -1) return 0; _combat->defenders -= ({ defender }); return 1; } /** * This method resets the defender array back to being nothing. */ void reset_defenders() { _combat->defenders = ({ }); } #ifdef USE_DISTANCE /** * Set the distance from this object to the given opponent. * * @param opponent the object to set the distance to. * @param distance the new distance. */ int set_distance(object opponent, int distance) { if(userp(opponent)) { if(!_combat->attacker_list[opponent->query_name()]) return 0; _combat->attacker_list[opponent->query_name()] = distance; } else { if(!_combat->attacker_list[opponent]) return 0; _combat->attacker_list[opponent] = distance; } return 1; } /** * Query the distance from this object to the given opponent. * * @param opponent the object to check */ int query_distance(object opponent) { if(userp(opponent)) { if(!_combat->attacker_list[opponent->query_name()]) return 0; return _combat->attacker_list[opponent->query_name()]; } else { if(!_combat->attacker_list[opponent]) return 0; return _combat->attacker_list[opponent]; } } #endif /** * This method returns the current list of people in the attacker list * on the object. * @return the current attacker array */ object *query_attacker_list() { return uniq_array(map(keys(_combat->attacker_list) + keys(_combat->hunting_list), (: $1 && objectp($1) ? $1 : find_player($1) :)) - ({ 0 })); } /** * This method removes someone from an attacker/hunting list. * @param ob the object to be removed. */ void remove_attacker_list(object ob) { if(userp(ob)) { map_delete(_combat->attacker_list, ob->query_name()); map_delete(_combat->hunting_list, ob->query_name()); } else { map_delete(_combat->attacker_list, ob); map_delete(_combat->hunting_list, ob); } } /** * This method is called when the living object is attacked by some opponent. * @param opponent the opponent we are attacked by * @return 0 if we cannot attack them, 1 if we can * @see query_attacker_list() * @see attack_ob() */ int attack_by(object opponent) { int starting; if(!objectp(opponent) || opponent == this_object() || !this_object()->query_attackable() || pk_check(this_object(), opponent)) return 0; // We'll stop protecting them when they attack us. if(member_array(opponent, _combat->protectors) != -1) _combat->protectors -= ({ opponent }); if(member_array(opponent, _combat->defenders) != -1) _combat->defenders -= ({ opponent }); if(!sizeof(query_attacker_list())) starting = 1; #ifdef USE_DISTANCE if(userp(opponent)) { if(!_combat->attacker_list[opponent->query_name()]) _combat->attacker_list[opponent->query_name()] = opponent->query_distance(this_object()); } else if(!_combat->attacker_list[opponent]) _combat->attacker_list[opponent] = opponent->query_distance(this_object()); #else if(userp(opponent)) { if(!_combat->attacker_list[opponent->query_name()]) _combat->attacker_list[opponent->query_name()] = 1; } else if(!_combat->attacker_list[opponent]) _combat->attacker_list[opponent] = 1; #endif if(starting) { _combat->action_defecit = (MAX_ACTION_DEFECIT - MIN_ACTION_DEFECIT) / 3; start_combat(opponent); } _combat->in_combat = 1; return 1; } /** * This method is called to make us attack someone else. * @param opponent the person to attack * @return 0 if we cannot attack them, 1 if we can * @see query_attacker_list() * @see attack_by() */ int attack_ob(object opponent) { int starting, new_opponent; if(!objectp(opponent) || opponent == this_object() || !opponent->query_attackable() || !this_object()->query_attackable() || pk_check(this_object(), opponent)) return 0; // They stop protecting you when you attack them. if(member_array(opponent, _combat->protectors) != -1) _combat->protectors -= ({ opponent }); if(member_array(opponent, _combat->defenders) != -1) _combat->defenders -= ({ opponent }); if(!sizeof(query_attacker_list())) starting = 1; else if(member_array(opponent, query_attacker_list()) == -1) new_opponent = 1; #ifdef USE_DISTANCE if(userp(opponent)) { if(!_combat->attacker_list[opponent->query_name()]) _combat->attacker_list[opponent->query_name()] = INITIAL_DISTANCE; } else { if(!_combat->attacker_list[opponent]) _combat->attacker_list[opponent] = INITIAL_DISTANCE; } #else if(userp(opponent)) { if(!_combat->attacker_list[opponent->query_name()]) _combat->attacker_list[opponent->query_name()] = 1; } else { if(!_combat->attacker_list[opponent]) _combat->attacker_list[opponent] = 1; } #endif if((new_opponent || starting) && opponent->query_visible(this_object())) _callout = call_out("announce_intent", 0, opponent); this_object()->remove_hide_invis("hiding"); if(starting) { _combat->action_defecit = (MAX_ACTION_DEFECIT - MIN_ACTION_DEFECIT) / 3; start_combat(opponent); } _combat->in_combat = 1; return 1; } void stop_fight(object opponent) { class combat_special tmp; #if DEBUG == 2 combat_debug("stop_fight called against %s", opponent->query_name()); #endif if(!objectp(opponent)) return; if(userp(opponent)) { map_delete(_combat->attacker_list, opponent->query_name()); map_delete(_combat->hunting_list, opponent->query_name()); } else { map_delete(_combat->attacker_list, opponent); map_delete(_combat->hunting_list, opponent); } #ifdef USE_SURRENDER _surrender->from -= ({ 0, opponent }); _surrender->refusers -= ({ 0, opponent }); _surrender->to -= ({ 0, opponent }); #endif if(!sizeof(query_attacker_list())) { // Not in combat so remove any non-continuous specials. foreach(tmp in _combat->specials) if(!(tmp->type & T_CONTINUOUS)) remove_special(tmp->id); end_combat(); } } void stop_all_fight() { class combat_special tmp; #if DEBUG == 2 combat_debug("stop_all_fight called"); #endif _combat->attacker_list = ([ ]); _combat->hunting_list = ([ ]); _combat->in_combat = 0; #ifdef USE_SURRENDER _surrender->from = ({ }); _surrender->refusers = ({ }); _surrender->to = ({ }); #endif // Not in combat so remove any non-continuous specials. foreach(tmp in _combat->specials) if(!(tmp->type & T_CONTINUOUS)) remove_special(tmp->id); end_combat(); } void stop_hunting(object opponent) { if(userp(opponent)) map_delete(_combat->hunting_list, opponent->query_name()); else map_delete(_combat->hunting_list, opponent); if(!sizeof(query_attacker_list())) end_combat(); } #ifdef USE_SURRENDER /** * This method will be called when we surrender. This does mean that * the opponent must have accepted our surrender plea. * @return always returns 0 * @param thing the thing which made us surrender */ object do_surrender(object thing) { mixed *attackers; mixed att; if(this_object()->query_property("dead")) return 0; death_helper(thing, 0); catch(DEATH->someone_surrendered(this_object())); attackers = query_attacker_list(); foreach(att in attackers) { att->stop_fight(this_object()); } stop_all_fight(); return 0; } int dont_attack_me() { if(sizeof(_surrender->to)) return 1; else return 0; } /** * This method tells us what the npc shouuld do in the case that it * might possibly surrender. * @param victim the person surrendering * @param attacker the person beating up the surrenderer */ void event_surrender(object victim, object *attackers) { object *offer; offer = ({ }); if (this_object() == victim) { _surrender->refusers -= ({ 0 }); _surrender->to -= ({ 0 }); offer = filter(attackers, (: member_array($1, _surrender->refusers + _surrender->to) == -1 :)); if(sizeof(offer)) { event(environment(), "combat", sprintf("%s kneels down and surrenders to %s.\n", this_object()->one_short(), query_multiple_short(offer)), ({ this_object() }) + _surrender->to + _surrender->refusers, 0); this_object()->event_combat(this_object(), sprintf("%s kneel down and surrender to %s.\n", this_object()->one_short(), query_multiple_short(offer)), ({ }), 0); offer->offered_surrender(victim); _surrender->to += offer; } } } /** * This method returns the current list of people surrendering to us. * @return the list of people surrendering * @see remove_surrenderer() */ object *query_surrenderers() { _surrender->from -= ({ 0 }); return copy(_surrender->from); } /** * This method removes a person surrendering from our current list. * @param victim the person to remove * @see query_surrenderers() */ void remove_surrenderer(object victim) { _surrender->from -= ({ 0, victim }); return; } /** * This method is called when someone offers to surrender to the living * object. It will check the current tactics for surrender and then * handle the response appropriately. * @param victim the person surrendering */ void offered_surrender(object victim) { string mercy = this_object()->query_combat_mercy(); switch(mercy) { case "ask": if (interactive(this_object())) { _surrender->from -= ({ 0, victim }); _surrender->from += ({ victim }); this_object()->event_combat(this_object(), victim->one_short() + " has surrendered to " "you. Either \"accept " + victim->query_name() + "\" or \"reject " + victim->query_name() + ".\n", ({ }), 0); } else { if (this_object()->query_accept_surrender(victim)) victim->accepted_surrender(this_object()); else victim->refused_surrender(this_object()); } break; case "always": this_object()->event_combat(this_object(), "You accept the surrender of " + victim->one_short() + ".\n", ({ }), 0); victim->accepted_surrender(this_object()); break; default: this_object()->event_combat(this_object(), sprintf("You refuse the surrender of %s.\n", victim->one_short()), ({ }), 0); victim->refused_surrender(this_object()); } } /** * This method is called if surrender was refused for the particular * person. * @param attacker the person who is surrendering * @see accepted_surrender() */ void refused_surrender(object attacker) { _surrender->to -= ({ 0 }); if (member_array(attacker, _surrender->to) == -1) return; _surrender->to -= ({ attacker }); _surrender->refusers -= ({ 0 }); _surrender->refusers += ({ attacker }); this_object()->event_combat(this_object(), attacker->one_short() + " refused your surrender\n", ({ }), 0); } /** * This method is called if surrender was accepted for the particular * person. * @param attacker the person who is surrendering * @see refused_surrender() */ void accepted_surrender(object attacker) { _surrender->to -= ({ 0 }); if(member_array(attacker, _surrender->to) == -1) { return; } _surrender->to -= ({ attacker }); this_object()->event_combat(this_object(), attacker->one_short() + " accepts your surrender.\n", ({ })); this_object()->do_surrender(attacker); } #endif /** * This method is called when the fight has stopped. It propogates the * stopped fighting event onto all the objects in the room. * @param thing the thing which stopped fighting? */ void stopped_fighting(object thing) { event( environment(), "stopped_fighting", thing ); } /** * This method is called when there is a fight in progress. It will * propogate the event onto all the objects in the room. * @param thing the person fighting */ void fight_in_progress(object thing) { event(environment(), "fight_in_progress", thing ); } /* Stuff from the old combat shadow */ int set_concentrating(object thing) { if(userp(thing)) { if(!_combat->attacker_list[thing->query_name()]) return 0; #ifdef USE_DISTANCE _combat->attacker_list[thing->query_name()] = thing->query_distance(this_object()); #else _combat->attacker_list[thing->query_name()] = 1; #endif } else { if(!_combat->attacker_list[thing]) return 0; #ifdef USE_DISTANCE _combat->attacker_list[thing] = thing->query_distance(this_object()); #else _combat->attacker_list[thing] = 1; #endif } _combat->concentrating = thing; return 1; } object query_concentrating() { object thing; thing = _combat->concentrating; if(!thing || !objectp(thing)) return 0; if(userp(thing) && !_combat->attacker_list[thing->query_name()]) return 0; else if(!_combat->attacker_list[thing]) return 0; return thing; } /** @ignore yes * This is here for backward compatibility only. Use query_last_opponent. */ object query_attacker() { return _combat->last_opponent; } /** * This method returns the last known opponent we attacked or defended * against. It is kept as up to date as the system can manage and so will * include anyone who is currently attacking us or who we are currently * attacking. * * @return object the opponent. */ object query_last_opponent() { return _combat->last_opponent; } /** @ignore yes * This is _internal_ to the combat system and should not be used doing so * may have odd side-effects. */ void set_last_opponent(object last) { _combat->last_opponent = last; } /** * This method returns the last weapon we used for an attack or defense. * * @return object the weapon. */ object query_last_weapon() { return _combat->last_weapon; } /** @ignore yes * This is _internal_ to the combat system and should not be used doing so * may have odd side-effects. */ void set_last_weapon(object last) { _combat->last_weapon = last; } /** * This method returns the last (combat) action performed. * * @return string the action. */ string query_last_action() { return _combat->last_action; } /** @ignore yes * This is _internal_ to the combat system and should not be used doing so * may have odd side-effects. */ void set_last_action(string last) { _combat->last_action = last; } /** * This method returns the result of the last combat round. * * @return int the result. */ int query_last_result() { return _combat->last_result; } /** @ignore yes * This is _internal_ to the combat system and should not be used doing so * may have odd side-effects. */ void set_last_result(int last) { _combat->last_result = last; } /** * This method determines whether or not the object is fighting another * object. * @param ob the object being tested * @param actively is the opponent being actively fought at this time? * @return 1 if it is in combat, 0 if it is not. */ varargs int is_fighting(object ob, int actively) { if(!objectp(ob)) return 0; // Only return those things we are actively fighting right now. Things // we're hunting or cannot see are excluded. if(actively) return userp(ob) ? _combat->attacker_list[ob->query_name()] : _combat->attacker_list[ob]; return userp(ob) ? _combat->attacker_list[ob->query_name()] || _combat->hunting_list[ob->query_name()] : _combat->attacker_list[ob] || _combat->hunting_list[ob]; } /** * Is the player currently fighting anyone. * * @return 1 for yes, 0 for no. */ int query_fighting() { if(!environment() || base_name(environment()) == "/room/rubbish") return 0; // Are any of our opponents here? If so we're fighting. if(sizeof(filter(query_attacker_list(), (: environment($1) == environment(this_object()) :)))) return 1; // Are any of the people here fighting us? If so we're fighting if(sizeof(filter(all_inventory(environment(this_object())), (: $1 && living($1) && $1->is_fighting(this_object(), 1) :)))) return 1; return 0; } /** @ignore yes */ mixed stats() { int i; int j; object *weapons; mixed ret; init_tactics(); ret = ({ ({ "attitude", _tactics->attitude }), ({ "response", _tactics->response }), ({ "mercy", _tactics->mercy }), ({ "parry", _tactics->parry }), ({ "attack", _tactics->attack }), ({ "unarmed parry", ({ "no", "yes" })[ _tactics->parry_unarmed ] }), ({ "attack zone", _tactics->focus_zone ? _tactics->focus_zone : "none" }), ({ "distance", _tactics->ideal_distance ? _tactics->ideal_distance : "none" }), }); weapons = (object *)this_object()->query_weapons(); if(!sizeof(weapons)) return ret + weapon_logic::stats(); for ( i = 0; i < sizeof( weapons ); i++ ) ret += ({ ({ "weapon #"+ i, weapons[ i ]->short() }) }) + (mixed)weapons[ j ]->weapon_stats(); return ret + weapon_logic::stats(); }