mirror of
https://github.com/tbamud/tbamud.git
synced 2026-03-22 20:26:33 +01:00
1044 lines
32 KiB
C
1044 lines
32 KiB
C
/**************************************************************************
|
||
* File: fight.c Part of tbaMUD *
|
||
* Usage: Combat system. *
|
||
* *
|
||
* All rights reserved. See license for complete information. *
|
||
* *
|
||
* Copyright (C) 1993, 94 by the Trustees of the Johns Hopkins University *
|
||
* CircleMUD is based on DikuMUD, Copyright (C) 1990, 1991. *
|
||
**************************************************************************/
|
||
|
||
#include "conf.h"
|
||
#include "sysdep.h"
|
||
#include "structs.h"
|
||
#include "utils.h"
|
||
#include "comm.h"
|
||
#include "handler.h"
|
||
#include "interpreter.h"
|
||
#include "db.h"
|
||
#include "spells.h"
|
||
#include "screen.h"
|
||
#include "constants.h"
|
||
#include "dg_scripts.h"
|
||
#include "act.h"
|
||
#include "class.h"
|
||
#include "fight.h"
|
||
#include "shop.h"
|
||
#include "quest.h"
|
||
|
||
|
||
/* locally defined global variables, used externally */
|
||
/* head of l-list of fighting chars */
|
||
struct char_data *combat_list = NULL;
|
||
/* Weapon attack texts */
|
||
struct attack_hit_type attack_hit_text[] =
|
||
{
|
||
{"hit", "hits"}, /* 0 */
|
||
{"sting", "stings"},
|
||
{"whip", "whips"},
|
||
{"slash", "slashes"},
|
||
{"bite", "bites"},
|
||
{"bludgeon", "bludgeons"}, /* 5 */
|
||
{"crush", "crushes"},
|
||
{"pound", "pounds"},
|
||
{"claw", "claws"},
|
||
{"maul", "mauls"},
|
||
{"thrash", "thrashes"}, /* 10 */
|
||
{"pierce", "pierces"},
|
||
{"blast", "blasts"},
|
||
{"punch", "punches"},
|
||
{"stab", "stabs"}
|
||
};
|
||
|
||
/* local (file scope only) variables */
|
||
static struct char_data *next_combat_list = NULL;
|
||
|
||
/* local file scope utility functions */
|
||
static void perform_group_gain(struct char_data *ch, int base, struct char_data *victim);
|
||
static void dam_message(int dam, struct char_data *ch, struct char_data *victim, int w_type);
|
||
static void make_corpse(struct char_data *ch);
|
||
static void change_alignment(struct char_data *ch, struct char_data *victim);
|
||
static void group_gain(struct char_data *ch, struct char_data *victim);
|
||
static void solo_gain(struct char_data *ch, struct char_data *victim);
|
||
/** @todo refactor this function name */
|
||
static char *replace_string(const char *str, const char *weapon_singular, const char *weapon_plural);
|
||
static int roll_damage(struct char_data *ch, struct char_data *victim,
|
||
struct obj_data *wielded, int w_type);
|
||
|
||
/* Base damage roller; STR-based while there are no ranged types. */
|
||
static int roll_damage(struct char_data *ch, struct char_data *victim,
|
||
struct obj_data *wielded, int w_type)
|
||
{
|
||
int dam = 0;
|
||
|
||
if (wielded && GET_OBJ_TYPE(wielded) == ITEM_WEAPON) {
|
||
int ndice = GET_OBJ_VAL(wielded, 1); /* #dice */
|
||
int sdice = GET_OBJ_VAL(wielded, 2); /* sides */
|
||
dam = dice(ndice, sdice);
|
||
dam += GET_ABILITY_MOD(GET_STR(ch)); /* STR adds to weapon damage */
|
||
} else {
|
||
/* unarmed */
|
||
dam = dice(1, 2);
|
||
dam += GET_ABILITY_MOD(GET_STR(ch));
|
||
}
|
||
|
||
if (dam < 0) dam = 0;
|
||
return dam;
|
||
}
|
||
|
||
/* Map the current attack (unarmed/weapon damage type) to SKILL_* constant. */
|
||
static int weapon_family_skill_num(struct char_data *ch, struct obj_data *wielded, int w_type) {
|
||
/* Unarmed? */
|
||
if (!wielded || GET_OBJ_TYPE(wielded) != ITEM_WEAPON)
|
||
return SKILL_UNARMED;
|
||
|
||
/* NOTE: w_type here is TYPE_HIT + GET_OBJ_VAL(wielded, 3) or mob attack type + TYPE_HIT.
|
||
Adjust the cases below to match your game's TYPE_* values. */
|
||
switch (w_type) {
|
||
/* --- Piercing family --- */
|
||
case TYPE_STAB:
|
||
case TYPE_PIERCE:
|
||
case TYPE_WHIP:
|
||
case TYPE_STING:
|
||
return SKILL_PIERCING_WEAPONS;
|
||
|
||
/* --- Slashing family --- */
|
||
case TYPE_SLASH:
|
||
case TYPE_MAUL:
|
||
case TYPE_CLAW:
|
||
return SKILL_SLASHING_WEAPONS;
|
||
|
||
/* --- Bludgeoning family --- */
|
||
case TYPE_BLUDGEON:
|
||
case TYPE_CRUSH:
|
||
case TYPE_THRASH:
|
||
case TYPE_POUND:
|
||
return SKILL_BLUDGEONING_WEAPONS;
|
||
|
||
/* Fallback */
|
||
default:
|
||
return SKILL_UNARMED;
|
||
}
|
||
}
|
||
|
||
/* Map SKILL_* constants to the strings your gain_skill(name, failure) expects. */
|
||
static const char *skill_name_for_gain(int skillnum) {
|
||
switch (skillnum) {
|
||
case SKILL_UNARMED: return "unarmed";
|
||
case SKILL_PIERCING_WEAPONS: return "piercing weapons";
|
||
case SKILL_SLASHING_WEAPONS: return "slashing weapons";
|
||
case SKILL_BLUDGEONING_WEAPONS: return "bludgeoning weapons";
|
||
case SKILL_SHIELD_USE: return "shield use";
|
||
default: return "unarmed";
|
||
}
|
||
}
|
||
|
||
#define IS_WEAPON(type) (((type) >= TYPE_HIT) && ((type) < TYPE_SUFFERING))
|
||
/* The Fight related routines */
|
||
void appear(struct char_data *ch)
|
||
{
|
||
if (affected_by_spell(ch, SPELL_INVISIBLE))
|
||
affect_from_char(ch, SPELL_INVISIBLE);
|
||
|
||
REMOVE_BIT_AR(AFF_FLAGS(ch), AFF_INVISIBLE);
|
||
REMOVE_BIT_AR(AFF_FLAGS(ch), AFF_HIDE);
|
||
|
||
if (GET_LEVEL(ch) < LVL_IMMORT)
|
||
act("$n slowly fades into existence.", FALSE, ch, 0, 0, TO_ROOM);
|
||
else
|
||
act("You feel a strange presence as $n appears, seemingly from nowhere.",
|
||
FALSE, ch, 0, 0, TO_ROOM);
|
||
}
|
||
|
||
int compute_armor_class(struct char_data *ch)
|
||
{
|
||
int armorclass = GET_AC(ch);
|
||
|
||
if (AWAKE(ch))
|
||
armorclass += GET_ABILITY_MOD(GET_DEX(ch)) * 10;
|
||
|
||
return (MAX(-100, armorclass)); /* -100 is lowest */
|
||
}
|
||
|
||
void update_pos(struct char_data *victim)
|
||
{
|
||
if ((GET_HIT(victim) > 0) && (GET_POS(victim) > POS_STUNNED))
|
||
return;
|
||
else if (GET_HIT(victim) > 0)
|
||
GET_POS(victim) = POS_STANDING;
|
||
else if (GET_HIT(victim) <= -11)
|
||
GET_POS(victim) = POS_DEAD;
|
||
else if (GET_HIT(victim) <= -6)
|
||
GET_POS(victim) = POS_MORTALLYW;
|
||
else if (GET_HIT(victim) <= -3)
|
||
GET_POS(victim) = POS_INCAP;
|
||
else
|
||
GET_POS(victim) = POS_STUNNED;
|
||
}
|
||
|
||
void check_killer(struct char_data *ch, struct char_data *vict)
|
||
{
|
||
if (PLR_FLAGGED(vict, PLR_KILLER) || PLR_FLAGGED(vict, PLR_THIEF))
|
||
return;
|
||
if (PLR_FLAGGED(ch, PLR_KILLER) || IS_NPC(ch) || IS_NPC(vict) || ch == vict)
|
||
return;
|
||
|
||
SET_BIT_AR(PLR_FLAGS(ch), PLR_KILLER);
|
||
send_to_char(ch, "If you want to be a PLAYER KILLER, so be it...\r\n");
|
||
mudlog(BRF, MAX(LVL_IMMORT, MAX(GET_INVIS_LEV(ch), GET_INVIS_LEV(vict))),
|
||
TRUE, "PC Killer bit set on %s for initiating attack on %s at %s.",
|
||
GET_NAME(ch), GET_NAME(vict), world[IN_ROOM(vict)].name);
|
||
}
|
||
|
||
/* start one char fighting another (yes, it is horrible, I know... ) */
|
||
void set_fighting(struct char_data *ch, struct char_data *vict)
|
||
{
|
||
if (ch == vict)
|
||
return;
|
||
|
||
if (FIGHTING(ch)) {
|
||
core_dump();
|
||
return;
|
||
}
|
||
|
||
ch->next_fighting = combat_list;
|
||
combat_list = ch;
|
||
|
||
if (AFF_FLAGGED(ch, AFF_SLEEP))
|
||
affect_from_char(ch, SPELL_SLEEP);
|
||
|
||
FIGHTING(ch) = vict;
|
||
GET_POS(ch) = POS_FIGHTING;
|
||
|
||
if (!CONFIG_PK_ALLOWED)
|
||
check_killer(ch, vict);
|
||
}
|
||
|
||
/* remove a char from the list of fighting chars */
|
||
void stop_fighting(struct char_data *ch)
|
||
{
|
||
struct char_data *temp;
|
||
|
||
if (ch == next_combat_list)
|
||
next_combat_list = ch->next_fighting;
|
||
|
||
REMOVE_FROM_LIST(ch, combat_list, next_fighting);
|
||
ch->next_fighting = NULL;
|
||
FIGHTING(ch) = NULL;
|
||
GET_POS(ch) = POS_STANDING;
|
||
update_pos(ch);
|
||
}
|
||
|
||
static void make_corpse(struct char_data *ch)
|
||
{
|
||
char buf2[MAX_NAME_LENGTH + 64];
|
||
struct obj_data *corpse, *o;
|
||
int i, x, y;
|
||
|
||
corpse = create_obj();
|
||
|
||
corpse->corpse_mob_vnum = IS_NPC(ch) ? GET_MOB_VNUM(ch) : 0;
|
||
|
||
corpse->item_number = NOTHING;
|
||
IN_ROOM(corpse) = NOWHERE;
|
||
corpse->name = strdup("corpse");
|
||
|
||
/* Use short description if available, otherwise fall back to name */
|
||
const char *who = NULL;
|
||
|
||
if (GET_SHORT_DESC(ch) && *GET_SHORT_DESC(ch))
|
||
who = GET_SHORT_DESC(ch);
|
||
else
|
||
who = GET_NAME(ch);
|
||
|
||
snprintf(buf2, sizeof(buf2), "The corpse of %s is lying here.", who);
|
||
corpse->description = strdup(buf2);
|
||
|
||
snprintf(buf2, sizeof(buf2), "the corpse of %s", who);
|
||
corpse->short_description = strdup(buf2);
|
||
|
||
GET_OBJ_TYPE(corpse) = ITEM_CONTAINER;
|
||
for (x = y = 0; x < EF_ARRAY_MAX || y < TW_ARRAY_MAX; x++, y++) {
|
||
if (x < EF_ARRAY_MAX)
|
||
GET_OBJ_EXTRA_AR(corpse, x) = 0;
|
||
if (y < TW_ARRAY_MAX)
|
||
corpse->obj_flags.wear_flags[y] = 0;
|
||
}
|
||
SET_BIT_AR(GET_OBJ_WEAR(corpse), ITEM_WEAR_TAKE);
|
||
SET_BIT_AR(GET_OBJ_EXTRA(corpse), ITEM_NODONATE);
|
||
GET_OBJ_VAL(corpse, 0) = 0; /* You can't store stuff in a corpse */
|
||
GET_OBJ_VAL(corpse, 3) = 1; /* corpse identifier */
|
||
GET_OBJ_WEIGHT(corpse) = GET_WEIGHT(ch) + IS_CARRYING_W(ch);
|
||
if (IS_NPC(ch))
|
||
GET_OBJ_TIMER(corpse) = CONFIG_MAX_NPC_CORPSE_TIME;
|
||
else
|
||
GET_OBJ_TIMER(corpse) = CONFIG_MAX_PC_CORPSE_TIME;
|
||
|
||
/* transfer character's inventory to the corpse */
|
||
corpse->contains = ch->carrying;
|
||
for (o = corpse->contains; o != NULL; o = o->next_content)
|
||
o->in_obj = corpse;
|
||
object_list_new_owner(corpse, NULL);
|
||
|
||
/* transfer character's equipment to the corpse */
|
||
for (i = 0; i < NUM_WEARS; i++)
|
||
if (GET_EQ(ch, i)) {
|
||
remove_otrigger(GET_EQ(ch, i), ch);
|
||
obj_to_obj(unequip_char(ch, i), corpse);
|
||
}
|
||
|
||
/* transfer coins */
|
||
if (GET_COINS(ch) > 0)
|
||
GET_COINS(ch) = 0;
|
||
ch->carrying = NULL;
|
||
IS_CARRYING_N(ch) = 0;
|
||
IS_CARRYING_W(ch) = 0;
|
||
|
||
obj_to_room(corpse, IN_ROOM(ch));
|
||
}
|
||
|
||
/* When ch kills victim */
|
||
static void change_alignment(struct char_data *ch, struct char_data *victim)
|
||
{
|
||
/* new alignment change algorithm: if you kill a monster with alignment A,
|
||
* you move 1/16th of the way to having alignment -A. Simple and fast. */
|
||
GET_ALIGNMENT(ch) += (-GET_ALIGNMENT(victim) - GET_ALIGNMENT(ch)) / 16;
|
||
}
|
||
|
||
void death_cry(struct char_data *ch)
|
||
{
|
||
int door;
|
||
|
||
act("Your blood freezes as you hear $n's death cry.", FALSE, ch, 0, 0, TO_ROOM);
|
||
|
||
for (door = 0; door < DIR_COUNT; door++)
|
||
if (CAN_GO(ch, door))
|
||
send_to_room(world[IN_ROOM(ch)].dir_option[door]->to_room, "Your blood freezes as you hear someone's death cry.\r\n");
|
||
}
|
||
|
||
void raw_kill(struct char_data * ch, struct char_data * killer)
|
||
{
|
||
struct char_data *i;
|
||
|
||
if (FIGHTING(ch))
|
||
stop_fighting(ch);
|
||
|
||
while (ch->affected)
|
||
affect_remove(ch, ch->affected);
|
||
|
||
/* To make ordinary commands work in scripts. welcor*/
|
||
GET_POS(ch) = POS_STANDING;
|
||
|
||
if (killer) {
|
||
if (death_mtrigger(ch, killer))
|
||
death_cry(ch);
|
||
} else
|
||
death_cry(ch);
|
||
|
||
if (killer) {
|
||
if (killer->group) {
|
||
while ((i = (struct char_data *) simple_list(killer->group->members)) != NULL)
|
||
if(IN_ROOM(i) == IN_ROOM(ch) || (world[IN_ROOM(i)].zone == world[IN_ROOM(ch)].zone))
|
||
autoquest_trigger_check(i, ch, NULL, AQ_MOB_KILL);
|
||
} else
|
||
autoquest_trigger_check(killer, ch, NULL, AQ_MOB_KILL);
|
||
}
|
||
|
||
/* Alert Group if Applicable */
|
||
if (GROUP(ch))
|
||
send_to_group(ch, GROUP(ch), "%s has died.\r\n", GET_NAME(ch));
|
||
|
||
update_pos(ch);
|
||
GET_POS(ch) = POS_DEAD;
|
||
|
||
make_corpse(ch);
|
||
extract_char(ch);
|
||
|
||
if (killer) {
|
||
autoquest_trigger_check(killer, NULL, NULL, AQ_MOB_SAVE);
|
||
autoquest_trigger_check(killer, NULL, NULL, AQ_ROOM_CLEAR);
|
||
}
|
||
}
|
||
|
||
void die(struct char_data * ch, struct char_data * killer)
|
||
{
|
||
if (!IS_NPC(ch)) {
|
||
REMOVE_BIT_AR(PLR_FLAGS(ch), PLR_KILLER);
|
||
REMOVE_BIT_AR(PLR_FLAGS(ch), PLR_THIEF);
|
||
}
|
||
raw_kill(ch, killer);
|
||
}
|
||
|
||
static void perform_group_gain(struct char_data *ch, int base,
|
||
struct char_data *victim)
|
||
{
|
||
int share;
|
||
|
||
share = MIN(CONFIG_MAX_EXP_GAIN, MAX(1, base));
|
||
|
||
change_alignment(ch, victim);
|
||
}
|
||
|
||
static void group_gain(struct char_data *ch, struct char_data *victim)
|
||
{
|
||
int tot_members = 0, base, tot_gain;
|
||
struct char_data *k;
|
||
|
||
while ((k = (struct char_data *) simple_list(GROUP(ch)->members)) != NULL)
|
||
if (IN_ROOM(ch) == IN_ROOM(k))
|
||
tot_members++;
|
||
|
||
/* round up to the nearest tot_members */
|
||
tot_gain = (GET_EXP(victim) / 3) + tot_members - 1;
|
||
|
||
/* prevent illegal xp creation when killing players */
|
||
if (!IS_NPC(victim))
|
||
tot_gain = MIN(CONFIG_MAX_EXP_LOSS * 2 / 3, tot_gain);
|
||
|
||
if (tot_members >= 1)
|
||
base = MAX(1, tot_gain / tot_members);
|
||
else
|
||
base = 0;
|
||
|
||
while ((k = (struct char_data *) simple_list(GROUP(ch)->members)) != NULL)
|
||
if (IN_ROOM(k) == IN_ROOM(ch))
|
||
perform_group_gain(k, base, victim);
|
||
}
|
||
|
||
static void solo_gain(struct char_data *ch, struct char_data *victim)
|
||
{
|
||
int exp;
|
||
|
||
exp = MIN(CONFIG_MAX_EXP_GAIN, GET_EXP(victim) / 3);
|
||
|
||
/* Calculate level-difference bonus */
|
||
if (IS_NPC(ch))
|
||
exp += MAX(0, (exp * MIN(4, (GET_LEVEL(victim) - GET_LEVEL(ch)))) / 8);
|
||
else
|
||
exp += MAX(0, (exp * MIN(8, (GET_LEVEL(victim) - GET_LEVEL(ch)))) / 8);
|
||
|
||
exp = MAX(exp, 1);
|
||
|
||
change_alignment(ch, victim);
|
||
}
|
||
|
||
static char *replace_string(const char *str, const char *weapon_singular, const char *weapon_plural)
|
||
{
|
||
static char buf[256];
|
||
char *cp = buf;
|
||
|
||
for (; *str; str++) {
|
||
if (*str == '#') {
|
||
switch (*(++str)) {
|
||
case 'W':
|
||
for (; *weapon_plural; *(cp++) = *(weapon_plural++));
|
||
break;
|
||
case 'w':
|
||
for (; *weapon_singular; *(cp++) = *(weapon_singular++));
|
||
break;
|
||
default:
|
||
*(cp++) = '#';
|
||
break;
|
||
}
|
||
} else
|
||
*(cp++) = *str;
|
||
|
||
*cp = 0;
|
||
} /* For */
|
||
|
||
return (buf);
|
||
}
|
||
|
||
/* message for doing damage with a weapon */
|
||
static void dam_message(int dam, struct char_data *ch, struct char_data *victim,
|
||
int w_type)
|
||
{
|
||
char *buf;
|
||
int msgnum;
|
||
|
||
static struct dam_weapon_type {
|
||
const char *to_room;
|
||
const char *to_char;
|
||
const char *to_victim;
|
||
} dam_weapons[] = {
|
||
|
||
/* use #w for singular (i.e. "slash") and #W for plural (i.e. "slashes") */
|
||
|
||
{
|
||
"$n tries to #w $N, but misses.", /* 0: 0 */
|
||
"You try to #w $N, but miss.",
|
||
"$n tries to #w you, but misses."
|
||
},
|
||
|
||
{
|
||
"$n tickles $N as $e #W $M.", /* 1: 1..2 */
|
||
"You tickle $N as you #w $M.",
|
||
"$n tickles you as $e #W you."
|
||
},
|
||
|
||
{
|
||
"$n barely #W $N.", /* 2: 3..4 */
|
||
"You barely #w $N.",
|
||
"$n barely #W you."
|
||
},
|
||
|
||
{
|
||
"$n #W $N.", /* 3: 5..6 */
|
||
"You #w $N.",
|
||
"$n #W you."
|
||
},
|
||
|
||
{
|
||
"$n #W $N hard.", /* 4: 7..10 */
|
||
"You #w $N hard.",
|
||
"$n #W you hard."
|
||
},
|
||
|
||
{
|
||
"$n #W $N very hard.", /* 5: 11..14 */
|
||
"You #w $N very hard.",
|
||
"$n #W you very hard."
|
||
},
|
||
|
||
{
|
||
"$n #W $N extremely hard.", /* 6: 15..19 */
|
||
"You #w $N extremely hard.",
|
||
"$n #W you extremely hard."
|
||
},
|
||
|
||
{
|
||
"$n massacres $N to small fragments with $s #w.", /* 7: 19..23 */
|
||
"You massacre $N to small fragments with your #w.",
|
||
"$n massacres you to small fragments with $s #w."
|
||
},
|
||
|
||
{
|
||
"$n OBLITERATES $N with $s deadly #w!!", /* 8: > 23 */
|
||
"You OBLITERATE $N with your deadly #w!!",
|
||
"$n OBLITERATES you with $s deadly #w!!"
|
||
}
|
||
};
|
||
|
||
w_type -= TYPE_HIT; /* Change to base of table with text */
|
||
|
||
if (dam == 0) msgnum = 0;
|
||
else if (dam <= 2) msgnum = 1;
|
||
else if (dam <= 4) msgnum = 2;
|
||
else if (dam <= 6) msgnum = 3;
|
||
else if (dam <= 10) msgnum = 4;
|
||
else if (dam <= 14) msgnum = 5;
|
||
else if (dam <= 19) msgnum = 6;
|
||
else if (dam <= 23) msgnum = 7;
|
||
else msgnum = 8;
|
||
|
||
/* damage message to onlookers */
|
||
buf = replace_string(dam_weapons[msgnum].to_room,
|
||
attack_hit_text[w_type].singular, attack_hit_text[w_type].plural);
|
||
act(buf, FALSE, ch, NULL, victim, TO_NOTVICT);
|
||
|
||
/* damage message to damager */
|
||
if (GET_LEVEL(ch) >= LVL_IMMORT)
|
||
send_to_char(ch, "(%d) ", dam);
|
||
buf = replace_string(dam_weapons[msgnum].to_char,
|
||
attack_hit_text[w_type].singular, attack_hit_text[w_type].plural);
|
||
act(buf, FALSE, ch, NULL, victim, TO_CHAR);
|
||
send_to_char(ch, CCNRM(ch, C_CMP));
|
||
|
||
/* damage message to damagee */
|
||
if (GET_LEVEL(victim) >= LVL_IMMORT)
|
||
send_to_char(victim, "\tR(%d)", dam);
|
||
buf = replace_string(dam_weapons[msgnum].to_victim,
|
||
attack_hit_text[w_type].singular, attack_hit_text[w_type].plural);
|
||
act(buf, FALSE, ch, NULL, victim, TO_VICT | TO_SLEEP);
|
||
send_to_char(victim, CCNRM(victim, C_CMP));
|
||
}
|
||
|
||
/* message for doing damage with a spell or skill. Also used for weapon
|
||
* damage on miss and death blows. */
|
||
int skill_message(int dam, struct char_data *ch, struct char_data *vict,
|
||
int attacktype)
|
||
{
|
||
int i, j, nr;
|
||
struct message_type *msg;
|
||
|
||
struct obj_data *weap = GET_EQ(ch, WEAR_WIELD);
|
||
|
||
/* @todo restructure the messages library to a pointer based system as
|
||
* opposed to the current cyclic location system. */
|
||
for (i = 0; i < MAX_MESSAGES; i++) {
|
||
if (fight_messages[i].a_type == attacktype) {
|
||
nr = dice(1, fight_messages[i].number_of_attacks);
|
||
for (j = 1, msg = fight_messages[i].msg; (j < nr) && msg; j++)
|
||
msg = msg->next;
|
||
|
||
if (!IS_NPC(vict) && (GET_LEVEL(vict) >= LVL_IMPL)) {
|
||
act(msg->god_msg.attacker_msg, FALSE, ch, weap, vict, TO_CHAR);
|
||
act(msg->god_msg.victim_msg, FALSE, ch, weap, vict, TO_VICT);
|
||
act(msg->god_msg.room_msg, FALSE, ch, weap, vict, TO_NOTVICT);
|
||
} else if (dam != 0) {
|
||
/*
|
||
* Don't send redundant color codes for TYPE_SUFFERING & other types
|
||
* of damage without attacker_msg.
|
||
*/
|
||
if (GET_POS(vict) == POS_DEAD) {
|
||
if (msg->die_msg.attacker_msg) {
|
||
send_to_char(ch, CCYEL(ch, C_CMP));
|
||
act(msg->die_msg.attacker_msg, FALSE, ch, weap, vict, TO_CHAR);
|
||
send_to_char(ch, CCNRM(ch, C_CMP));
|
||
}
|
||
|
||
send_to_char(vict, CCRED(vict, C_CMP));
|
||
act(msg->die_msg.victim_msg, FALSE, ch, weap, vict, TO_VICT | TO_SLEEP);
|
||
send_to_char(vict, CCNRM(vict, C_CMP));
|
||
|
||
act(msg->die_msg.room_msg, FALSE, ch, weap, vict, TO_NOTVICT);
|
||
} else {
|
||
if (msg->hit_msg.attacker_msg) {
|
||
send_to_char(ch, CCYEL(ch, C_CMP));
|
||
act(msg->hit_msg.attacker_msg, FALSE, ch, weap, vict, TO_CHAR);
|
||
send_to_char(ch, CCNRM(ch, C_CMP));
|
||
}
|
||
|
||
send_to_char(vict, CCRED(vict, C_CMP));
|
||
act(msg->hit_msg.victim_msg, FALSE, ch, weap, vict, TO_VICT | TO_SLEEP);
|
||
send_to_char(vict, CCNRM(vict, C_CMP));
|
||
|
||
act(msg->hit_msg.room_msg, FALSE, ch, weap, vict, TO_NOTVICT);
|
||
}
|
||
} else if (ch != vict) { /* Dam == 0 */
|
||
if (msg->miss_msg.attacker_msg) {
|
||
send_to_char(ch, CCYEL(ch, C_CMP));
|
||
act(msg->miss_msg.attacker_msg, FALSE, ch, weap, vict, TO_CHAR);
|
||
send_to_char(ch, CCNRM(ch, C_CMP));
|
||
}
|
||
|
||
send_to_char(vict, CCRED(vict, C_CMP));
|
||
act(msg->miss_msg.victim_msg, FALSE, ch, weap, vict, TO_VICT | TO_SLEEP);
|
||
send_to_char(vict, CCNRM(vict, C_CMP));
|
||
|
||
act(msg->miss_msg.room_msg, FALSE, ch, weap, vict, TO_NOTVICT);
|
||
}
|
||
return (1);
|
||
}
|
||
}
|
||
return (0);
|
||
}
|
||
|
||
/* This function returns the following codes:
|
||
* < 0 Victim died.
|
||
* = 0 No damage.
|
||
* > 0 How much damage done. */
|
||
int damage(struct char_data *ch, struct char_data *victim, int dam, int attacktype)
|
||
{
|
||
long local_coins = 0;
|
||
char local_buf[256];
|
||
struct char_data *tmp_char;
|
||
struct obj_data *corpse_obj;
|
||
|
||
if (GET_POS(victim) <= POS_DEAD) {
|
||
/* This is "normal"-ish now with delayed extraction. -gg 3/15/2001 */
|
||
if (PLR_FLAGGED(victim, PLR_NOTDEADYET) || MOB_FLAGGED(victim, MOB_NOTDEADYET))
|
||
return (-1);
|
||
|
||
log("SYSERR: Attempt to damage corpse '%s' in room #%d by '%s'.",
|
||
GET_NAME(victim), GET_ROOM_VNUM(IN_ROOM(victim)), GET_NAME(ch));
|
||
die(victim, ch);
|
||
return (-1); /* -je, 7/7/92 */
|
||
}
|
||
|
||
/* peaceful rooms */
|
||
if (ch->nr != real_mobile(DG_CASTER_PROXY) &&
|
||
ch != victim && ROOM_FLAGGED(IN_ROOM(ch), ROOM_PEACEFUL)) {
|
||
send_to_char(ch, "This room just has such a peaceful, easy feeling...\r\n");
|
||
return (0);
|
||
}
|
||
|
||
/* shopkeeper and MOB_NOKILL protection */
|
||
if (!ok_damage_shopkeeper(ch, victim) || MOB_FLAGGED(victim, MOB_NOKILL)) {
|
||
send_to_char(ch, "This mob is protected.\r\n");
|
||
return (0);
|
||
}
|
||
|
||
/* You can't damage an immortal! */
|
||
if (!IS_NPC(victim) && ((GET_LEVEL(victim) >= LVL_IMMORT) && PRF_FLAGGED(victim, PRF_NOHASSLE)))
|
||
dam = 0;
|
||
|
||
dam = damage_mtrigger(ch, victim, dam, attacktype);
|
||
if (dam == -1) {
|
||
return (0);
|
||
}
|
||
|
||
if (victim != ch) {
|
||
/* Start the attacker fighting the victim */
|
||
if (GET_POS(ch) > POS_STUNNED && (FIGHTING(ch) == NULL))
|
||
set_fighting(ch, victim);
|
||
|
||
/* Start the victim fighting the attacker */
|
||
if (GET_POS(victim) > POS_STUNNED && (FIGHTING(victim) == NULL)) {
|
||
set_fighting(victim, ch);
|
||
if (MOB_FLAGGED(victim, MOB_MEMORY) && !IS_NPC(ch))
|
||
remember(victim, ch);
|
||
}
|
||
}
|
||
|
||
/* If you attack a pet, it hates your guts */
|
||
if (victim->master == ch)
|
||
stop_follower(victim);
|
||
|
||
/* If the attacker is invisible, he becomes visible */
|
||
if (AFF_FLAGGED(ch, AFF_INVISIBLE) || AFF_FLAGGED(ch, AFF_HIDE))
|
||
appear(ch);
|
||
|
||
/* Cut damage in half if victim has sanct, to a minimum 1 */
|
||
if (AFF_FLAGGED(victim, AFF_SANCTUARY) && dam >= 2)
|
||
dam /= 2;
|
||
|
||
/* Check for PK if this is not a PK MUD */
|
||
if (!CONFIG_PK_ALLOWED) {
|
||
check_killer(ch, victim);
|
||
if (PLR_FLAGGED(ch, PLR_KILLER) && (ch != victim))
|
||
dam = 0;
|
||
}
|
||
|
||
/* Set the maximum damage per round and subtract the hit points */
|
||
dam = MAX(MIN(dam, 100), 0);
|
||
GET_HIT(victim) -= dam;
|
||
|
||
update_pos(victim);
|
||
|
||
/* skill_message sends a message from the messages file in lib/misc.
|
||
* dam_message just sends a generic "You hit $n extremely hard.".
|
||
* skill_message is preferable to dam_message because it is more
|
||
* descriptive.
|
||
*
|
||
* If we are _not_ attacking with a weapon (i.e. a spell), always use
|
||
* skill_message. If we are attacking with a weapon: If this is a miss or a
|
||
* death blow, send a skill_message if one exists; if not, default to a
|
||
* dam_message. Otherwise, always send a dam_message. */
|
||
if (!IS_WEAPON(attacktype))
|
||
skill_message(dam, ch, victim, attacktype);
|
||
else {
|
||
if (GET_POS(victim) == POS_DEAD || dam == 0) {
|
||
if (!skill_message(dam, ch, victim, attacktype))
|
||
dam_message(dam, ch, victim, attacktype);
|
||
} else {
|
||
dam_message(dam, ch, victim, attacktype);
|
||
}
|
||
}
|
||
|
||
/* Use send_to_char -- act() doesn't send message if you are DEAD. */
|
||
switch (GET_POS(victim)) {
|
||
case POS_MORTALLYW:
|
||
act("$n is mortally wounded, and will die soon, if not aided.", TRUE, victim, 0, 0, TO_ROOM);
|
||
send_to_char(victim, "You are mortally wounded, and will die soon, if not aided.\r\n");
|
||
break;
|
||
case POS_INCAP:
|
||
act("$n is incapacitated and will slowly die, if not aided.", TRUE, victim, 0, 0, TO_ROOM);
|
||
send_to_char(victim, "You are incapacitated and will slowly die, if not aided.\r\n");
|
||
break;
|
||
case POS_STUNNED:
|
||
act("$n is stunned, but will probably regain consciousness again.", TRUE, victim, 0, 0, TO_ROOM);
|
||
send_to_char(victim, "You're stunned, but will probably regain consciousness again.\r\n");
|
||
break;
|
||
case POS_DEAD:
|
||
act("$n is dead! R.I.P.", FALSE, victim, 0, 0, TO_ROOM);
|
||
send_to_char(victim, "You are dead! Sorry...\r\n");
|
||
break;
|
||
|
||
default: /* >= POSITION SLEEPING */
|
||
if (dam > (GET_MAX_HIT(victim) / 4))
|
||
send_to_char(victim, "That really did HURT!\r\n");
|
||
|
||
if (GET_HIT(victim) < (GET_MAX_HIT(victim) / 4)) {
|
||
send_to_char(victim, "%sYou wish that your wounds would stop BLEEDING so much!%s\r\n",
|
||
CCRED(victim, C_SPR), CCNRM(victim, C_SPR));
|
||
if (ch != victim && MOB_FLAGGED(victim, MOB_WIMPY))
|
||
do_flee(victim, NULL, 0, 0);
|
||
}
|
||
if (!IS_NPC(victim) && GET_WIMP_LEV(victim) && (victim != ch) &&
|
||
GET_HIT(victim) < GET_WIMP_LEV(victim) && GET_HIT(victim) > 0) {
|
||
send_to_char(victim, "You wimp out, and attempt to flee!\r\n");
|
||
do_flee(victim, NULL, 0, 0);
|
||
}
|
||
break;
|
||
}
|
||
|
||
/* Help out poor linkless people who are attacked */
|
||
if (!IS_NPC(victim) && !(victim->desc) && GET_POS(victim) > POS_STUNNED) {
|
||
do_flee(victim, NULL, 0, 0);
|
||
if (!FIGHTING(victim)) {
|
||
act("$n is rescued by divine forces.", FALSE, victim, 0, 0, TO_ROOM);
|
||
GET_WAS_IN(victim) = IN_ROOM(victim);
|
||
char_from_room(victim);
|
||
char_to_room(victim, 0);
|
||
}
|
||
}
|
||
|
||
/* stop someone from fighting if they're stunned or worse */
|
||
if (GET_POS(victim) <= POS_STUNNED && FIGHTING(victim) != NULL)
|
||
stop_fighting(victim);
|
||
|
||
/* Uh oh. Victim died. */
|
||
if (GET_POS(victim) == POS_DEAD) {
|
||
if (ch != victim && (IS_NPC(victim) || victim->desc)) {
|
||
if (GROUP(ch))
|
||
group_gain(ch, victim);
|
||
else
|
||
solo_gain(ch, victim);
|
||
}
|
||
|
||
if (!IS_NPC(victim)) {
|
||
/* NPC-safe invis check for killer (ch) */
|
||
int ch_invis = IS_NPC(ch) ? 0 : GET_INVIS_LEV(ch); /* <-- added */
|
||
mudlog(BRF, MAX(LVL_IMMORT, MAX(ch_invis, GET_INVIS_LEV(victim))),
|
||
TRUE, "%s killed by %s at %s",
|
||
GET_NAME(victim), GET_NAME(ch), world[IN_ROOM(victim)].name);
|
||
if (MOB_FLAGGED(ch, MOB_MEMORY))
|
||
forget(ch, victim);
|
||
}
|
||
/* Can't determine GET_COINS on corpse, so do now and store */
|
||
if (IS_NPC(victim)) {
|
||
local_coins = GET_COINS(victim);
|
||
sprintf(local_buf,"%ld", (long)local_coins);
|
||
}
|
||
|
||
die(victim, ch);
|
||
if (GROUP(ch) && (local_coins > 0) && PRF_FLAGGED(ch, PRF_AUTOSPLIT) ) {
|
||
generic_find("corpse", FIND_OBJ_ROOM, ch, &tmp_char, &corpse_obj);
|
||
if (corpse_obj) {
|
||
do_get(ch, "all.coin corpse", 0, 0);
|
||
do_split(ch, local_buf, 0, 0);
|
||
}
|
||
}
|
||
if (!IS_NPC(ch) && (ch != victim) && PRF_FLAGGED(ch, PRF_AUTOLOOT)) {
|
||
do_get(ch, "all corpse", 0, 0);
|
||
}
|
||
return (-1);
|
||
}
|
||
return (dam);
|
||
}
|
||
|
||
/*
|
||
* hit() -- one character attempts to hit another with a weapon or attack.
|
||
* Ascending AC (5e-like), nat 1/20, bounded bonuses, and skill gains.
|
||
* Since there are no ranged types yet, we always use STR for attack & damage mods.
|
||
*/
|
||
void hit(struct char_data *ch, struct char_data *victim, int type)
|
||
{
|
||
struct obj_data *wielded = GET_EQ(ch, WEAR_WIELD);
|
||
struct obj_data *shield = GET_EQ(victim, WEAR_SHIELD);
|
||
int w_type, d20, attack_mod = 0, target_ac, dam = 0;
|
||
bool hit_success = FALSE;
|
||
|
||
/* Basic sanity */
|
||
if (!ch || !victim) return;
|
||
|
||
/* Determine attack message type exactly like stock code */
|
||
if (wielded && GET_OBJ_TYPE(wielded) == ITEM_WEAPON)
|
||
w_type = GET_OBJ_VAL(wielded, 3) + TYPE_HIT;
|
||
else {
|
||
if (IS_NPC(ch) && ch->mob_specials.attack_type != 0)
|
||
w_type = ch->mob_specials.attack_type + TYPE_HIT;
|
||
else
|
||
w_type = TYPE_HIT;
|
||
} /* matches stock message mapping */
|
||
|
||
/* Roll d20 */
|
||
d20 = rand_number(1, 20);
|
||
|
||
/* Ability modifier: STR only (no ranged types yet) */
|
||
attack_mod += GET_ABILITY_MOD(GET_STR(ch));
|
||
|
||
/* Skill family & proficiency */
|
||
{
|
||
int skillnum = weapon_family_skill_num(ch, wielded, w_type);
|
||
const char *skillname = skill_name_for_gain(skillnum);
|
||
|
||
/* proficiency from current % */
|
||
if (!IS_NPC(ch))
|
||
attack_mod += GET_PROFICIENCY(GET_SKILL(ch, skillnum));
|
||
|
||
/* --- UNARMED ATTACK HANDLING --- */
|
||
if (!wielded) {
|
||
int prof_bonus = (!IS_NPC(ch)) ? GET_PROFICIENCY(GET_SKILL(ch, SKILL_UNARMED)) : 0;
|
||
int str_mod = GET_ABILITY_MOD(GET_STR(ch));
|
||
int die_size;
|
||
|
||
/* Simple scaling by proficiency tier */
|
||
switch (prof_bonus) {
|
||
case 0: die_size = 4; break; /* untrained */
|
||
case 1: die_size = 6; break; /* trained */
|
||
case 2: die_size = 8; break; /* expert */
|
||
default: die_size = 10; break; /* master or above */
|
||
}
|
||
|
||
/* NPC fallback scaling */
|
||
if (IS_NPC(ch) && prof_bonus <= 0) {
|
||
prof_bonus = MIN(6, (GET_LEVEL(ch) / 4)); /* level scaling */
|
||
}
|
||
|
||
/* base damage roll for unarmed attacks */
|
||
dam = dice(1, die_size) + str_mod + prof_bonus;
|
||
|
||
/* mark attack type for damage() messaging */
|
||
w_type = SKILL_UNARMED + TYPE_HIT;
|
||
}
|
||
|
||
/* Weapon magic (cap +3) */
|
||
if (wielded && GET_OBJ_TYPE(wielded) == ITEM_WEAPON) {
|
||
int wmag = GET_OBJ_VAL(wielded, VAL_ARMOR_MAGIC_BONUS);
|
||
if (wmag > MAX_WEAPON_MAGIC) wmag = MAX_WEAPON_MAGIC;
|
||
attack_mod += wmag;
|
||
}
|
||
|
||
/* Situational attack modifiers hook (spells, conditions) */
|
||
attack_mod += 0;
|
||
|
||
/* Cap total attack bonus to +10 */
|
||
if (attack_mod > MAX_TOTAL_ATTACK_BONUS)
|
||
attack_mod = MAX_TOTAL_ATTACK_BONUS;
|
||
|
||
/* Ascending AC target */
|
||
target_ac = compute_armor_class_asc(victim);
|
||
|
||
/* Nat 1/20, then normal resolution */
|
||
if (d20 == 1) hit_success = FALSE;
|
||
else if (d20 == 20) hit_success = TRUE;
|
||
else hit_success = ((d20 + attack_mod) >= target_ac);
|
||
|
||
/* Apply result */
|
||
if (hit_success) {
|
||
/* Roll damage up front (needed for shield durability)
|
||
* If we are unarmed, dam was already rolled above.
|
||
* If wielding a weapon, roll normally.
|
||
*/
|
||
if (wielded)
|
||
dam = roll_damage(ch, victim, wielded, w_type);
|
||
|
||
/* --- SHIELD BLOCK CHECK ---
|
||
* Only happens if an attack actually lands.
|
||
*/
|
||
if (shield) {
|
||
int def_prof = (!IS_NPC(victim)) ? GET_PROFICIENCY(GET_SKILL(victim, SKILL_SHIELD_USE)) : 0;
|
||
int block_chance = def_prof * 10; /* 0–60% total chance to block an attack */
|
||
|
||
if (block_chance > 0 && rand_number(1, 100) <= block_chance) {
|
||
/* Block succeeded! */
|
||
act("You block $N's attack with $p!", FALSE, victim, shield, ch, TO_CHAR);
|
||
act("$n blocks your attack with $s $p!", FALSE, victim, shield, ch, TO_VICT);
|
||
act("$n blocks $N's attack with $s $p!", TRUE, victim, shield, ch, TO_NOTVICT);
|
||
|
||
/* Durability reduction based on damage prevented */
|
||
int *dur = &GET_OBJ_VAL(shield, 3);
|
||
int loss = MAX(1, dam / 10); /* at least 1% per block */
|
||
*dur -= loss;
|
||
|
||
if (*dur <= 0) {
|
||
act("Your $p shatters into pieces!", FALSE, victim, shield, 0, TO_CHAR);
|
||
act("$n's $p shatters into pieces!", TRUE, victim, shield, 0, TO_ROOM);
|
||
extract_obj(shield);
|
||
}
|
||
|
||
/* Train shield use skill on success */
|
||
if (!IS_NPC(victim)) {
|
||
gain_skill(victim, "shield use", TRUE);
|
||
}
|
||
|
||
return; /* Attack nullified entirely */
|
||
}
|
||
}
|
||
|
||
/* No block: apply normal damage */
|
||
damage(ch, victim, dam, w_type);
|
||
} else {
|
||
damage(ch, victim, 0, w_type); /* miss messaging */
|
||
}
|
||
|
||
/* --- Skill gains --- */
|
||
if (!IS_NPC(ch) && skillname) {
|
||
gain_skill(ch, (char *)skillname, hit_success);
|
||
}
|
||
|
||
/* Defender shield use: every swing trains it if they’re wearing a shield.
|
||
Treat a MISS as a "success" for the shield user (they successfully defended). */
|
||
if (!IS_NPC(victim) && GET_EQ(victim, WEAR_SHIELD)) {
|
||
gain_skill(victim, "shield use", !hit_success);
|
||
}
|
||
}
|
||
|
||
/* Optional combat numbers for debugging / builders */
|
||
if (CONFIG_DEBUG_MODE >= NRM) {
|
||
const char *crit = (d20 == 20) ? " (CRIT)" : ((d20 == 1) ? " (NAT 1)" : "");
|
||
send_to_char(ch,
|
||
"\t1Attack:\tn d20=%d%s, mod=%+d \t1⇒\tn total=%d vs AC %d — %s\r\n",
|
||
d20, crit, attack_mod, d20 + attack_mod, target_ac,
|
||
hit_success ? "\t2HIT\tn" : "\t1MISS\tn");
|
||
if (!IS_NPC(victim)) {
|
||
send_to_char(victim,
|
||
"\t1Defense:\tn %s rolled total=%d vs your AC %d — %s%s\r\n",
|
||
GET_NAME(ch), d20 + attack_mod, target_ac,
|
||
hit_success ? "\t1HIT\tn" : "\t2MISS\tn",
|
||
(d20 == 20) ? " (CRIT)" : ((d20 == 1) ? " (NAT 1)" : ""));
|
||
}
|
||
}
|
||
}
|
||
|
||
/* control the fights going on. Called every 2 seconds from comm.c. */
|
||
void perform_violence(void)
|
||
{
|
||
struct char_data *ch, *tch;
|
||
|
||
for (ch = combat_list; ch; ch = next_combat_list) {
|
||
next_combat_list = ch->next_fighting;
|
||
|
||
if (FIGHTING(ch) == NULL || IN_ROOM(ch) != IN_ROOM(FIGHTING(ch))) {
|
||
stop_fighting(ch);
|
||
continue;
|
||
}
|
||
|
||
if (IS_NPC(ch)) {
|
||
if (GET_MOB_WAIT(ch) > 0) {
|
||
GET_MOB_WAIT(ch) -= PULSE_VIOLENCE;
|
||
continue;
|
||
}
|
||
GET_MOB_WAIT(ch) = 0;
|
||
if (GET_POS(ch) < POS_FIGHTING) {
|
||
GET_POS(ch) = POS_FIGHTING;
|
||
act("$n scrambles to $s feet!", TRUE, ch, 0, 0, TO_ROOM);
|
||
}
|
||
}
|
||
|
||
if (GET_POS(ch) < POS_FIGHTING) {
|
||
send_to_char(ch, "You can't fight while sitting!!\r\n");
|
||
continue;
|
||
}
|
||
|
||
if (GROUP(ch) && GROUP(ch)->members && GROUP(ch)->members->iSize) {
|
||
struct iterator_data Iterator;
|
||
|
||
tch = (struct char_data *) merge_iterator(&Iterator, GROUP(ch)->members);
|
||
for (; tch ; tch = next_in_list(&Iterator)) {
|
||
if (tch == ch)
|
||
continue;
|
||
if (!IS_NPC(tch) && !PRF_FLAGGED(tch, PRF_AUTOASSIST))
|
||
continue;
|
||
if (IN_ROOM(ch) != IN_ROOM(tch))
|
||
continue;
|
||
if (FIGHTING(tch))
|
||
continue;
|
||
if (GET_POS(tch) != POS_STANDING)
|
||
continue;
|
||
if (!CAN_SEE(tch, ch))
|
||
continue;
|
||
|
||
do_assist(tch, GET_NAME(ch), 0, 0);
|
||
}
|
||
}
|
||
|
||
hit(ch, FIGHTING(ch), TYPE_UNDEFINED);
|
||
if (MOB_FLAGGED(ch, MOB_SPEC) && GET_MOB_SPEC(ch) && !MOB_FLAGGED(ch, MOB_NOTDEADYET)) {
|
||
char actbuf[MAX_INPUT_LENGTH] = "";
|
||
(GET_MOB_SPEC(ch)) (ch, ch, 0, actbuf);
|
||
}
|
||
}
|
||
}
|