mirror of
https://github.com/tbamud/tbamud.git
synced 2026-03-26 14:16:33 +01:00
Convert system to "5e-like"
This commit is contained in:
parent
b6864f8db9
commit
75316a6702
19 changed files with 1381 additions and 317 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,6 +1,7 @@
|
|||
bin/*
|
||||
src/*.o
|
||||
src/util/*.o
|
||||
src/tests/*.o
|
||||
config.cache
|
||||
config.log
|
||||
config.status
|
||||
|
|
|
|||
|
|
@ -296,26 +296,158 @@ to list all forms. i. e. the keyword rumble could be used to cover anyone who
|
|||
types <action> rumble rumbl rumb rum ru.
|
||||
|
||||
#31
|
||||
AC-CONFIDENCE ARMOR-CLASS ARMOUR-CLASS AC-APPLY
|
||||
ACAUDIT ARMOR-AUDIT AUDIT-ARMOR IMMORTAL
|
||||
|
||||
Your Armor Class (often called AC) is an expression for how good your armor
|
||||
is at protecting you. When you don armor, any AC apply that armor has is
|
||||
subtracted from your standard (naked) AC value, modified depending on where on
|
||||
the body you are wearing the armor. Some items have a special bonus, and
|
||||
subtract from the AC directly. Negative AC is better. Modifiers: Body X3,
|
||||
head and legs X2.
|
||||
Summary: Imm-only tool that scans all ITEM\_ARMOR prototypes and reports
|
||||
per-piece fields against slot caps. Use this to catch outliers and quickly
|
||||
rebalance items to the Light/Medium/Heavy targets.
|
||||
|
||||
See also: MEDIT-AC
|
||||
#31
|
||||
AC-CONFIDENCE ARMOR-CLASS ARMOUR-CLASS AC-APPLY
|
||||
Usage:
|
||||
acaudit
|
||||
|
||||
Your Armor Class (often called AC) is an expression for how good your armor
|
||||
is at protecting you. When you don armor, any AC apply that armor has is
|
||||
subtracted from your standard (naked) AC value, modified depending on where on
|
||||
the body you are wearing the armor. Some items have a special bonus, and
|
||||
subtract from the AC directly. Negative AC is better.
|
||||
What it does:
|
||||
|
||||
See Also: ARMOR-SPELL
|
||||
Scans prototypes for armor slots (head, body, legs, arms, hands, feet) and
|
||||
prints one line per item:
|
||||
\[#VNUM] <name> slot=<slot> ac=\<piece\_ac> bulk=<bulk> mag=+<magic> flags=<mask>
|
||||
|
||||
Markers:
|
||||
|
||||
OVER : value exceeds the slot’s hard cap (e.g., piece AC > slot max, or
|
||||
magic > slot max)
|
||||
WARN : value outside 0..3 (invalid for piece AC, bulk, or magic)
|
||||
(STEALTHDISADV): flag is set (the piece imposes Stealth Disadvantage)
|
||||
|
||||
Notes:
|
||||
|
||||
• Shields are audited separately in AC calculations and are skipped here.
|
||||
• Armor magic across all worn pieces is globally capped at +3 (shield
|
||||
is separate).
|
||||
• Bulk affects the Dex cap: Light (≤5) cap +5, Medium (6–10) cap +2,
|
||||
Heavy (≥11) cap +0.
|
||||
• Heavy bulk or any piece with Stealth Disadvantage imposes stealth
|
||||
disadvantage.
|
||||
• Values are clamped on save/load; this command helps you find and fix
|
||||
prototypes.
|
||||
|
||||
Typical targets (including Dex/shield effects):
|
||||
|
||||
Light : AC \~12–16
|
||||
Medium : AC \~14–18
|
||||
Heavy : AC \~16–20
|
||||
|
||||
See Also: ARMOR PIECES, BULK, SHIELDS, MAGIC CAPS, SCORE, OEDIT ARMOR
|
||||
#32
|
||||
AC ARMOR-CLASS ASCENDING-AC DEFENSE
|
||||
|
||||
Summary: We use an ascending AC system (higher is better). A typical
|
||||
unarmored character has AC 10. Attacks roll 1d20 + attack modifiers and
|
||||
hit if the total your AC. Natural 1 always misses; natural 20 always hits.
|
||||
|
||||
How AC is built:
|
||||
|
||||
Base: 10
|
||||
|
||||
Armor pieces: each worn slot contributes 03 AC (clamped by slot caps)
|
||||
Armor magic: total armor magic across all pieces is capped at +3
|
||||
Dexterity: add min(DEX modifier, Dex cap); Dex cap depends on total armor bulk
|
||||
Shield: base +2 (tower +3 if present), plus shield magic (capped at +3), plus
|
||||
Shield Use skill proficiency
|
||||
Situational: cover (+2/+5), spells (Shield, Haste, etc.)
|
||||
|
||||
See Also: SCORE
|
||||
#0
|
||||
ARMOR SLOTS
|
||||
|
||||
Summary: Armor is split across six slots: head, body, legs, arms, hands,
|
||||
feet. Each piece has:
|
||||
|
||||
Piece AC (value[0]): 0–3 (per-slot hard cap)
|
||||
Bulk (value[1]): 0–3 (drives Dex cap & stealth)
|
||||
Magic (value[2]): 0–3 (per-slot cap; global armor magic cap +3)
|
||||
Flags (value[3]): special rules (e.g., Stealth Disadvantage)
|
||||
Slot caps (defaults)
|
||||
Head: AC ≤2, Magic ≤1
|
||||
Body: AC ≤3, Magic ≤3
|
||||
Legs: AC ≤2, Magic ≤1
|
||||
Arms/Hands/Feet: AC ≤1, Magic ≤1
|
||||
Shield is handled separately
|
||||
|
||||
SEE ALSO: SHIELDS
|
||||
#0
|
||||
BULK DEX-CAP LIGHT MEDIUM HEAVY
|
||||
|
||||
Summary: Armor bulk limits how much of your Dex modifier you can apply
|
||||
to AC.
|
||||
|
||||
Light (bulk ≤ 5): Dex cap +5
|
||||
Medium (bulk 6–10): Dex cap +2
|
||||
Heavy (bulk ≥ 11): Dex cap +0 and imposes Stealth Disadvantage
|
||||
|
||||
Bulk is computed by summing each piece’s bulk × slot weight.
|
||||
Slot weights: head 1, body 3, legs 2, arms 1, hands 1, feet 1.
|
||||
|
||||
SEE ALSO: ARMOR
|
||||
#0
|
||||
SHIELD SHIELDS SHIELD-USE
|
||||
|
||||
Summary: A shield adds to AC:
|
||||
|
||||
Base +2 (tower +3 if applicable)
|
||||
Shield magic (capped at +3)
|
||||
Shield Use proficiency (based on your skill%)
|
||||
|
||||
Shield Use proficiency (from skill%)
|
||||
<=14:+0
|
||||
<=29:+1
|
||||
<=44:+2
|
||||
<=59:+3
|
||||
<=74:+4
|
||||
<=90:+5
|
||||
<=100:+6
|
||||
|
||||
SEE ALSO: AC ARMOR SLOTS
|
||||
#0
|
||||
MAGIC-CAP ENCHANTED-ITEMS
|
||||
|
||||
Summary: Sum of magic across all worn pieces is capped at +3 (after
|
||||
slot caps)
|
||||
|
||||
Shield: magic is capped at +3 (separate from armor)
|
||||
Weapons: magic is capped at +3
|
||||
Total attack bonus (stats + proficiency + magic + situational) is gently
|
||||
capped around +10 for balance
|
||||
|
||||
These caps are enforced automatically in calculations.
|
||||
|
||||
SEE ALSO: AC ARMOR SLOTS SHIELDS
|
||||
#0
|
||||
PROFICIENCY WEAPON SKILL SAVING THROWS
|
||||
|
||||
Summary: We map your skill % to a 5e-like proficiency bonus:
|
||||
|
||||
<=14:+0
|
||||
<=29:+1
|
||||
<=44:+2
|
||||
<=59:+3
|
||||
<=74:+4
|
||||
<=90:+5
|
||||
<=100:+6
|
||||
|
||||
This is used for weapon attacks, shields, and (when applicable) saving throws.
|
||||
|
||||
SEE ALSO: SHIELDS
|
||||
#0
|
||||
STEALTH SNEAK HIDE DISADVTANGE
|
||||
|
||||
Summary: You have Stealth Disadvantage if certain conditions are met,
|
||||
such as:
|
||||
|
||||
Any worn piece has the Stealth Disadvantage armor flag, or
|
||||
Your total armor bulk puts you in Heavy (Dex cap 0)
|
||||
|
||||
With Stealth Disadvantage, Sneak and Hide roll twice and take the worse result.
|
||||
Both success and failure can grant training progress.
|
||||
#0
|
||||
ACRONYMS TERMINOLOGY VOCABULARY
|
||||
|
||||
|
|
@ -5642,17 +5774,27 @@ See Also: TRIG-TYPES
|
|||
#31
|
||||
OASIS OLC CREATION ONLINE-CREATION ON-LINE-CREATION
|
||||
|
||||
On-line creation
|
||||
Summary: OnLine Creation tool used by the game to create, modify, or delete
|
||||
zones, rooms, objects, mobiles (NPC's), shops, and triggers.
|
||||
|
||||
The OLC command will show you any unsaved, edited world files.
|
||||
|
||||
To use OLC you have to have permission from one of the greater
|
||||
gods or implementors. When you are granted the right,
|
||||
you will receive further information.
|
||||
New updates to OLC:
|
||||
|
||||
To learn more about building check the website (help building)
|
||||
OLC fields for ITEM_ARMOR):
|
||||
|
||||
@RGOTO 3@n to enter The Builder Academy.
|
||||
value[0] Piece AC: 0–3 (slot-capped)
|
||||
value[1] Bulk: 0–3 (affects Dex cap/stealth)
|
||||
value[2] Magic: 0–3 (slot-capped; global armor magic cap +3)
|
||||
value[3] Flags: armor-specific bitvector (e.g., STEALTH_DISADV, REQ_STR15)
|
||||
|
||||
Target bands (typical, including Dex/shield):
|
||||
|
||||
Light: AC ~12–16
|
||||
|
||||
Medium: AC ~14–18
|
||||
|
||||
Heavy: AC ~16–20
|
||||
|
||||
See also: REDIT, OEDIT, MEDIT, SEDIT, ZEDIT, ZRESET,
|
||||
RLIST, OLIST, MLIST, SLIST, SHOW-ZONE
|
||||
|
|
@ -8108,14 +8250,20 @@ See Also: LOOK
|
|||
#0
|
||||
SCORE
|
||||
|
||||
Usage: score
|
||||
Summary: Score provides useful information about your character that you
|
||||
would find on a traditional tabletop character sheet. Examples:
|
||||
|
||||
Provides useful information on your status such as age, hit points,
|
||||
mana, movement points, armor class, alignment, experience points, how long
|
||||
you've been playing, and your level. Your money can be viewed with the
|
||||
'gold' command.
|
||||
|
||||
See also: ARMOR-CLASS, EXPERIENCE, GOLD
|
||||
HP, Mana, and Movement points
|
||||
Strength, Dexterity, Constituion, Intelligence, Wisdom, and Charisma scores
|
||||
Armor Class with breakdown
|
||||
Stealth Disadvantage
|
||||
Age
|
||||
Carried coins
|
||||
Quest points
|
||||
Current quest
|
||||
Time played
|
||||
Current position
|
||||
Conditions and affects
|
||||
#0
|
||||
SCREENWIDTHS SCREEN_WIDTHS
|
||||
|
||||
|
|
@ -12588,12 +12736,12 @@ ban copyover freeze hcontrol reroll skillset
|
|||
thaw unban wizupdate
|
||||
|
||||
Level 32 (God):
|
||||
advance aedit checkload dc file force
|
||||
gecho hedit helpcheck hsedit last links
|
||||
mcopy mute notitle ocopy pardon plist
|
||||
qecho rcopy restore scopy send snoop
|
||||
switch tcopy tedit transfer unaffect uptime
|
||||
users zlock zunlock
|
||||
acaudit advance aedit checkload dc file
|
||||
force gecho hedit helpcheck hsedit last
|
||||
links mcopy mute notitle ocopy pardon
|
||||
plist qecho rcopy restore scopy send
|
||||
snoop switch tcopy tedit transfer unaffect
|
||||
uptime users zlock zunlock
|
||||
|
||||
Level 31 (Immortal):
|
||||
; at attach buildwalk date
|
||||
|
|
|
|||
|
|
@ -302,6 +302,7 @@ ACMD(do_wizutil);
|
|||
#define SCMD_THAW 5
|
||||
#define SCMD_UNAFFECT 6
|
||||
/* Functions without subcommands */
|
||||
ACMD(do_acaudit);
|
||||
ACMD(do_advance);
|
||||
ACMD(do_at);
|
||||
ACMD(do_checkloadstatus);
|
||||
|
|
|
|||
|
|
@ -806,10 +806,54 @@ ACMD(do_gold)
|
|||
ACMD(do_score)
|
||||
{
|
||||
struct time_info_data playing_time;
|
||||
struct ac_breakdown acb;
|
||||
|
||||
if (IS_NPC(ch))
|
||||
return;
|
||||
|
||||
/* Compute AC components using new 5e-like system */
|
||||
compute_ac_breakdown(ch, &acb);
|
||||
|
||||
send_to_char(ch,
|
||||
"\r\n"
|
||||
"====================[ Score ]====================\r\n");
|
||||
|
||||
send_to_char(ch,
|
||||
"HP: %d/%d Mana: %d/%d Move: %d/%d\r\n",
|
||||
GET_HIT(ch), GET_MAX_HIT(ch),
|
||||
GET_MANA(ch), GET_MAX_MANA(ch),
|
||||
GET_MOVE(ch), GET_MAX_MOVE(ch));
|
||||
|
||||
/* Abilities and 5e modifiers */
|
||||
send_to_char(ch,
|
||||
"STR %2d (%+d) DEX %2d (%+d) CON %2d (%+d)\r\n"
|
||||
"INT %2d (%+d) WIS %2d (%+d) CHA %2d (%+d)\r\n",
|
||||
GET_STR(ch), ability_mod(GET_STR(ch)),
|
||||
GET_DEX(ch), ability_mod(GET_DEX(ch)),
|
||||
GET_CON(ch), ability_mod(GET_CON(ch)),
|
||||
GET_INT(ch), ability_mod(GET_INT(ch)),
|
||||
GET_WIS(ch), ability_mod(GET_WIS(ch)),
|
||||
GET_CHA(ch), ability_mod(GET_CHA(ch)));
|
||||
|
||||
/* Ascending AC breakdown */
|
||||
send_to_char(ch,
|
||||
"\r\n"
|
||||
"Armor Class (ascending): %d\r\n"
|
||||
" base: %d, armor: %d, armor magic: +%d, DEX (cap %d): %+d,\r\n"
|
||||
" shield: +%d, situational: %+d, bulk score: %d\r\n",
|
||||
acb.total,
|
||||
acb.base,
|
||||
acb.armor_piece_sum,
|
||||
acb.armor_magic_sum,
|
||||
acb.dex_cap,
|
||||
acb.dex_mod_applied,
|
||||
acb.shield_bonus,
|
||||
acb.situational,
|
||||
acb.total_bulk);
|
||||
|
||||
send_to_char(ch, "Stealth Disadvantage: %s\r\n",
|
||||
has_stealth_disadv(ch) ? "Yes" : "No");
|
||||
|
||||
send_to_char(ch, "You are %d years old.", GET_AGE(ch));
|
||||
|
||||
if (age(ch)->month == 0 && age(ch)->day == 0)
|
||||
|
|
@ -817,20 +861,9 @@ ACMD(do_score)
|
|||
else
|
||||
send_to_char(ch, "\r\n");
|
||||
|
||||
send_to_char(ch, "You have %d(%d) hit, %d(%d) mana and %d(%d) movement points.\r\n",
|
||||
GET_HIT(ch), GET_MAX_HIT(ch), GET_MANA(ch), GET_MAX_MANA(ch),
|
||||
GET_MOVE(ch), GET_MAX_MOVE(ch));
|
||||
|
||||
send_to_char(ch, "Your armor class is %d/10, and your alignment is %d.\r\n",
|
||||
compute_armor_class(ch), GET_ALIGNMENT(ch));
|
||||
|
||||
send_to_char(ch, "You have %d gold coins, and %d questpoints.\r\n",
|
||||
GET_GOLD(ch), GET_QUESTPOINTS(ch));
|
||||
|
||||
send_to_char(ch, "You have earned %d quest points.\r\n", GET_QUESTPOINTS(ch));
|
||||
send_to_char(ch, "You have completed %d quest%s, ",
|
||||
GET_NUM_QUESTS(ch),
|
||||
GET_NUM_QUESTS(ch) == 1 ? "" : "s");
|
||||
if (GET_QUEST(ch) == NOTHING)
|
||||
send_to_char(ch, "and you are not on a quest at the moment.\r\n");
|
||||
else
|
||||
|
|
|
|||
|
|
@ -99,34 +99,49 @@ ACMD(do_not_here)
|
|||
ACMD(do_sneak)
|
||||
{
|
||||
struct affected_type af;
|
||||
byte percent;
|
||||
int chance;
|
||||
bool ok;
|
||||
|
||||
if (IS_NPC(ch) || !GET_SKILL(ch, SKILL_SNEAK)) {
|
||||
send_to_char(ch, "You have no idea how to do that.\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
send_to_char(ch, "Okay, you'll try to move silently for a while.\r\n");
|
||||
|
||||
if (AFF_FLAGGED(ch, AFF_SNEAK))
|
||||
affect_from_char(ch, SKILL_SNEAK);
|
||||
|
||||
percent = rand_number(1, 101); /* 101% is a complete failure */
|
||||
/* Base chance: skill % + Dex-based adjustment */
|
||||
chance = GET_SKILL(ch, SKILL_SNEAK) + dex_app_skill[GET_DEX(ch)].sneak;
|
||||
if (chance < 0) chance = 0;
|
||||
if (chance > 100) chance = 100;
|
||||
|
||||
if (percent > GET_SKILL(ch, SKILL_SNEAK) + dex_app_skill[GET_DEX(ch)].sneak){
|
||||
/* Apply disadvantage if heavy/bulky armor or flagged pieces */
|
||||
if (has_stealth_disadv(ch))
|
||||
ok = percent_success_disadv(chance);
|
||||
else
|
||||
ok = percent_success(chance);
|
||||
|
||||
if (!ok) {
|
||||
gain_skill(ch, "sneak", FALSE);
|
||||
return;
|
||||
} else {
|
||||
new_affect(&af);
|
||||
af.spell = SKILL_SNEAK;
|
||||
af.duration = GET_LEVEL(ch);
|
||||
SET_BIT_AR(af.bitvector, AFF_SNEAK);
|
||||
affect_to_char(ch, &af);
|
||||
gain_skill(ch, "sneak", TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
/* Success: apply Sneak affect */
|
||||
new_affect(&af);
|
||||
af.spell = SKILL_SNEAK;
|
||||
af.duration = GET_LEVEL(ch);
|
||||
SET_BIT_AR(af.bitvector, AFF_SNEAK);
|
||||
affect_to_char(ch, &af);
|
||||
|
||||
gain_skill(ch, "sneak", TRUE);
|
||||
}
|
||||
|
||||
ACMD(do_hide)
|
||||
{
|
||||
byte percent;
|
||||
int chance;
|
||||
bool ok;
|
||||
|
||||
if (IS_NPC(ch) || !GET_SKILL(ch, SKILL_HIDE)) {
|
||||
send_to_char(ch, "You have no idea how to do that.\r\n");
|
||||
|
|
@ -138,15 +153,26 @@ ACMD(do_hide)
|
|||
if (AFF_FLAGGED(ch, AFF_HIDE))
|
||||
REMOVE_BIT_AR(AFF_FLAGS(ch), AFF_HIDE);
|
||||
|
||||
percent = rand_number(1, 101); /* 101% is a complete failure */
|
||||
/* Base chance: skill % + Dex-based adjustment */
|
||||
chance = GET_SKILL(ch, SKILL_HIDE) + dex_app_skill[GET_DEX(ch)].hide;
|
||||
if (chance < 0) chance = 0;
|
||||
if (chance > 100) chance = 100;
|
||||
|
||||
if (percent > GET_SKILL(ch, SKILL_HIDE) + dex_app_skill[GET_DEX(ch)].hide){
|
||||
/* Apply disadvantage if heavy/bulky armor or flagged pieces */
|
||||
if (has_stealth_disadv(ch))
|
||||
ok = percent_success_disadv(chance);
|
||||
else
|
||||
ok = percent_success(chance);
|
||||
|
||||
if (!ok) {
|
||||
gain_skill(ch, "hide", FALSE);
|
||||
return;
|
||||
} else {
|
||||
SET_BIT_AR(AFF_FLAGS(ch), AFF_HIDE);
|
||||
send_to_char(ch, "You hide yourself as best you can.\r\n");
|
||||
}
|
||||
|
||||
/* Success */
|
||||
SET_BIT_AR(AFF_FLAGS(ch), AFF_HIDE);
|
||||
send_to_char(ch, "You hide yourself as best you can.\r\n");
|
||||
gain_skill(ch, "hide", TRUE);
|
||||
}
|
||||
|
||||
ACMD(do_steal)
|
||||
|
|
|
|||
185
src/act.wizard.c
185
src/act.wizard.c
|
|
@ -2878,7 +2878,6 @@ ACMD(do_show)
|
|||
{ "shops", LVL_IMMORT },
|
||||
{ "houses", LVL_IMMORT },
|
||||
{ "snoop", LVL_IMMORT }, /* 10 */
|
||||
{ "thaco", LVL_IMMORT },
|
||||
{ "exp", LVL_IMMORT },
|
||||
{ "colour", LVL_IMMORT },
|
||||
{ "\n", 0 }
|
||||
|
|
@ -3115,30 +3114,8 @@ ACMD(do_show)
|
|||
send_to_char(ch, "No one is currently snooping.\r\n");
|
||||
break;
|
||||
|
||||
/* show thaco */
|
||||
case 11:
|
||||
len = strlcpy(buf, "LvL - Mu Cl Th Wa Ba Ra Br Dr\r\n----------------\r\n", sizeof(buf));
|
||||
|
||||
for (j = 1; j < LVL_IMMORT; j++) {
|
||||
nlen = snprintf(buf + len, sizeof(buf) - len, "%-3d - %-2d %-2d %-2d %-2d %-2d %-2d %-2d %-2d\r\n", j,
|
||||
thaco(CLASS_MAGIC_USER, j),
|
||||
thaco(CLASS_CLERIC, j),
|
||||
thaco(CLASS_THIEF, j),
|
||||
thaco(CLASS_WARRIOR, j),
|
||||
thaco(CLASS_BARBARIAN, j),
|
||||
thaco(CLASS_RANGER, j),
|
||||
thaco(CLASS_BARD, j),
|
||||
thaco(CLASS_DRUID, j));
|
||||
if (len + nlen >= sizeof(buf))
|
||||
break;
|
||||
len += nlen;
|
||||
}
|
||||
|
||||
page_string(ch->desc, buf, TRUE);
|
||||
break;
|
||||
|
||||
/* show experience tables */
|
||||
case 12:
|
||||
case 11:
|
||||
len = strlcpy(buf, "LvL - Mu Cl Th Wa BA Ra Br Dr\r\n--------------------------\r\n", sizeof(buf));
|
||||
|
||||
for (i = 1; i < LVL_IMMORT; i++) {
|
||||
|
|
@ -3159,7 +3136,7 @@ ACMD(do_show)
|
|||
page_string(ch->desc, buf, TRUE);
|
||||
break;
|
||||
|
||||
case 13:
|
||||
case 12:
|
||||
len = strlcpy(buf, "Colours\r\n--------------------------\r\n", sizeof(buf));
|
||||
k = 0;
|
||||
for (r = 0; r < 6; r++)
|
||||
|
|
@ -5563,3 +5540,161 @@ ACMD(do_oset)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 5e system helpers */
|
||||
|
||||
/* Helper: map wear flags to our armor_slots[] index (-1 if not an armor slot) */
|
||||
static int armor_slot_index_from_wear(const struct obj_data *obj) {
|
||||
if (!obj) return -1;
|
||||
|
||||
/* IMPORTANT: use your project's wear flag macros.
|
||||
Typical tbaMUD macros: CAN_WEAR(obj, ITEM_WEAR_*) */
|
||||
if (CAN_WEAR(obj, ITEM_WEAR_HEAD)) return 0; /* "head" */
|
||||
if (CAN_WEAR(obj, ITEM_WEAR_BODY)) return 1; /* "body" */
|
||||
if (CAN_WEAR(obj, ITEM_WEAR_LEGS)) return 2; /* "legs" */
|
||||
if (CAN_WEAR(obj, ITEM_WEAR_ARMS)) return 3; /* "arms" */
|
||||
if (CAN_WEAR(obj, ITEM_WEAR_HANDS)) return 4; /* "hands" */
|
||||
if (CAN_WEAR(obj, ITEM_WEAR_FEET)) return 5; /* "feet" */
|
||||
|
||||
/* Shield is audited separately in AC compute; skip it here */
|
||||
if (CAN_WEAR(obj, ITEM_WEAR_SHIELD)) return -2; /* special */
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Pretty: slot name (matches armor_slots[] order) */
|
||||
static const char *slot_name_from_index(int idx) {
|
||||
switch (idx) {
|
||||
case 0: return "head";
|
||||
case 1: return "body";
|
||||
case 2: return "legs";
|
||||
case 3: return "arms";
|
||||
case 4: return "hands";
|
||||
case 5: return "feet";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/* Wizard command: scan armor prototypes, validate per-piece fields (compact, paged, 25 lines) */
|
||||
ACMD(do_acaudit)
|
||||
{
|
||||
int found = 0, warned = 0;
|
||||
|
||||
if (IS_NPC(ch) || GET_LEVEL(ch) < LVL_IMMORT) {
|
||||
send_to_char(ch, "You lack the authority to use this.\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
/* --- dynamic buffer builder --- */
|
||||
size_t cap = 8192, len = 0;
|
||||
char *out = (char *)malloc(cap);
|
||||
if (!out) { send_to_char(ch, "Memory error.\r\n"); return; }
|
||||
out[0] = '\0';
|
||||
|
||||
#define APPEND_FMT(...) do { \
|
||||
int need = snprintf(NULL, 0, __VA_ARGS__); \
|
||||
if (need < 0) need = 0; \
|
||||
if (len + (size_t)need + 1 > cap) { \
|
||||
size_t ncap = cap * 2; \
|
||||
if (ncap < len + (size_t)need + 1) ncap = len + (size_t)need + 1; \
|
||||
char *tmp = (char *)realloc(out, ncap); \
|
||||
if (!tmp) { free(out); send_to_char(ch, "Memory error.\r\n"); return; } \
|
||||
out = tmp; cap = ncap; \
|
||||
} \
|
||||
len += (size_t)snprintf(out + len, cap - len, __VA_ARGS__); \
|
||||
} while (0)
|
||||
|
||||
/* Header (short so it won’t wrap) */
|
||||
APPEND_FMT("\r\n\tY[Armor Audit]\tn ITEM_ARMOR scan\r\n");
|
||||
APPEND_FMT("Legend: \tR!\tn over-cap, \tY?\tn warn, S stealth-disadv\r\n");
|
||||
|
||||
for (obj_rnum r = 0; r <= top_of_objt; r++) {
|
||||
struct obj_data *obj = &obj_proto[r];
|
||||
char namebuf[128] = {0};
|
||||
int idx, vnum, piece_ac, bulk, magic, flags;
|
||||
|
||||
if (GET_OBJ_TYPE(obj) != ITEM_ARMOR)
|
||||
continue;
|
||||
|
||||
/* Identify slot (skip shields here) */
|
||||
idx = armor_slot_index_from_wear(obj);
|
||||
if (idx == -2) continue; /* shield handled in AC; skip */
|
||||
if (idx < 0) continue; /* not a supported armor slot */
|
||||
|
||||
vnum = GET_OBJ_VNUM(obj);
|
||||
piece_ac = GET_OBJ_VAL(obj, VAL_ARMOR_PIECE_AC);
|
||||
bulk = GET_OBJ_VAL(obj, VAL_ARMOR_BULK);
|
||||
magic = GET_OBJ_VAL(obj, VAL_ARMOR_MAGIC_BONUS);
|
||||
flags = GET_OBJ_VAL(obj, VAL_ARMOR_FLAGS);
|
||||
|
||||
/* Display name (trim to keep line width < ~78 cols) */
|
||||
if (obj->short_description)
|
||||
snprintf(namebuf, sizeof(namebuf), "%s", obj->short_description);
|
||||
else if (obj->name)
|
||||
snprintf(namebuf, sizeof(namebuf), "%s", obj->name);
|
||||
else
|
||||
snprintf(namebuf, sizeof(namebuf), "object");
|
||||
|
||||
/* Slot caps */
|
||||
const int max_piece_ac = armor_slots[idx].max_piece_ac;
|
||||
const int max_magic = armor_slots[idx].max_magic;
|
||||
|
||||
/* Validations */
|
||||
bool over_ac = (piece_ac > max_piece_ac);
|
||||
bool over_magic = (magic > max_magic);
|
||||
bool bad_ac = (piece_ac < 0 || piece_ac > 3);
|
||||
bool bad_bulk = (bulk < 0 || bulk > 3);
|
||||
bool bad_magic = (magic < 0 || magic > 3);
|
||||
|
||||
found++;
|
||||
|
||||
/* Compact, non-wrapping row (~70 cols worst case) */
|
||||
APPEND_FMT("\tc[#%5d]\tn %-24.24s sl=%-5.5s ac=%2d%s b=%d%s m=%+d%s f=%d%s\r\n",
|
||||
vnum,
|
||||
namebuf,
|
||||
slot_name_from_index(idx),
|
||||
piece_ac, over_ac ? " \tR!\tn" : (bad_ac ? " \tY?\tn" : ""),
|
||||
bulk, bad_bulk ? " \tY?\tn" : "",
|
||||
magic, over_magic ? " \tR!\tn" : (bad_magic? " \tY?\tn" : ""),
|
||||
flags, (flags & ARMF_STEALTH_DISADV) ? " S" : "");
|
||||
|
||||
if (over_ac || over_magic || bad_ac || bad_bulk || bad_magic)
|
||||
warned++;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
free(out);
|
||||
send_to_char(ch, "No ITEM_ARMOR prototypes found for the audited slots.\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
APPEND_FMT("\r\nScanned: %d items, %d with issues. Armor magic cap +%d (shield separate).\r\n",
|
||||
found, warned, MAX_TOTAL_ARMOR_MAGIC);
|
||||
|
||||
/* Page it (copy mode) and try to force 25-line pages */
|
||||
if (ch->desc) {
|
||||
int old_len = 0; bool changed = false;
|
||||
#if defined(GET_SCREEN_HEIGHT)
|
||||
old_len = GET_SCREEN_HEIGHT(ch); GET_SCREEN_HEIGHT(ch) = 25; changed = true;
|
||||
#elif defined(GET_PAGE_LENGTH)
|
||||
old_len = GET_PAGE_LENGTH(ch); GET_PAGE_LENGTH(ch) = 25; changed = true;
|
||||
#endif
|
||||
page_string(ch->desc, out, 0); /* copy; we free out */
|
||||
free(out);
|
||||
if (changed) {
|
||||
#if defined(GET_SCREEN_HEIGHT)
|
||||
GET_SCREEN_HEIGHT(ch) = old_len;
|
||||
#elif defined(GET_PAGE_LENGTH)
|
||||
GET_PAGE_LENGTH(ch) = old_len;
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
send_to_char(ch, "%s", out);
|
||||
free(out);
|
||||
}
|
||||
|
||||
#undef APPEND_FMT
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
101
src/class.c
101
src/class.c
|
|
@ -703,107 +703,6 @@ byte saving_throws(int class_num, int type, int level)
|
|||
return 100;
|
||||
}
|
||||
|
||||
/* THAC0 for classes and levels. (To Hit Armor Class 0) */
|
||||
int thaco(int class_num, int level)
|
||||
{
|
||||
switch (class_num) {
|
||||
case CLASS_MAGIC_USER:
|
||||
switch (level) {
|
||||
case 0: return 100;
|
||||
case 1: return 20;
|
||||
case 2: return 20;
|
||||
case 3: return 20;
|
||||
case 4: return 19;
|
||||
case 5: return 19;
|
||||
default:
|
||||
log("SYSERR: Missing level for mage thac0.");
|
||||
}
|
||||
case CLASS_CLERIC:
|
||||
switch (level) {
|
||||
case 0: return 100;
|
||||
case 1: return 20;
|
||||
case 2: return 20;
|
||||
case 3: return 20;
|
||||
case 4: return 18;
|
||||
case 5: return 18;
|
||||
default:
|
||||
log("SYSERR: Missing level for cleric thac0.");
|
||||
}
|
||||
case CLASS_THIEF:
|
||||
switch (level) {
|
||||
case 0: return 100;
|
||||
case 1: return 20;
|
||||
case 2: return 20;
|
||||
case 3: return 19;
|
||||
case 4: return 19;
|
||||
case 5: return 18;
|
||||
default:
|
||||
log("SYSERR: Missing level for thief thac0.");
|
||||
}
|
||||
case CLASS_WARRIOR:
|
||||
switch (level) {
|
||||
case 0: return 100;
|
||||
case 1: return 20;
|
||||
case 2: return 19;
|
||||
case 3: return 18;
|
||||
case 4: return 17;
|
||||
case 5: return 16;
|
||||
default:
|
||||
log("SYSERR: Missing level for warrior thac0.");
|
||||
}
|
||||
case CLASS_BARBARIAN:
|
||||
switch (level) {
|
||||
case 0: return 100;
|
||||
case 1: return 20;
|
||||
case 2: return 19;
|
||||
case 3: return 18;
|
||||
case 4: return 17;
|
||||
case 5: return 16;
|
||||
default:
|
||||
log("SYSERR: Missing level for barbarian thac0.");
|
||||
}
|
||||
case CLASS_RANGER:
|
||||
switch (level) {
|
||||
case 0: return 100;
|
||||
case 1: return 20;
|
||||
case 2: return 19;
|
||||
case 3: return 18;
|
||||
case 4: return 17;
|
||||
case 5: return 16;
|
||||
default:
|
||||
log("SYSERR: Missing level for ranger thac0.");
|
||||
}
|
||||
case CLASS_BARD:
|
||||
switch (level) {
|
||||
case 0: return 100;
|
||||
case 1: return 20;
|
||||
case 2: return 19;
|
||||
case 3: return 19;
|
||||
case 4: return 18;
|
||||
case 5: return 17;
|
||||
default:
|
||||
log("SYSERR: Missing level for bard thac0.");
|
||||
}
|
||||
case CLASS_DRUID:
|
||||
switch (level) {
|
||||
case 0: return 100;
|
||||
case 1: return 20;
|
||||
case 2: return 20;
|
||||
case 3: return 20;
|
||||
case 4: return 18;
|
||||
case 5: return 18;
|
||||
default:
|
||||
log("SYSERR: Missing level for druid thac0.");
|
||||
}
|
||||
default:
|
||||
log("SYSERR: Unknown class in thac0 chart.");
|
||||
}
|
||||
|
||||
/* Will not get there unless something is wrong. */
|
||||
return 100;
|
||||
}
|
||||
|
||||
|
||||
/* Roll the 6 stats for a character... each stat is made of the sum of the best
|
||||
* 3 out of 4 rolls of a 6-sided die. Each class then decides which priority
|
||||
* will be given for the best to worst stats. */
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ int level_exp(int chclass, int level);
|
|||
int parse_class(char arg);
|
||||
void roll_real_abils(struct char_data *ch);
|
||||
byte saving_throws(int class_num, int type, int level);
|
||||
int thaco(int class_num, int level);
|
||||
const char *title_female(int chclass, int level);
|
||||
const char *title_male(int chclass, int level);
|
||||
|
||||
|
|
|
|||
|
|
@ -947,6 +947,39 @@ const char *ibt_bits[] = {
|
|||
"InProgress",
|
||||
"\n"
|
||||
};
|
||||
|
||||
/* 5e system helpers */
|
||||
|
||||
/* Armor slot table for ascending AC rules */
|
||||
const struct armor_slot armor_slots[] = {
|
||||
{ "head", 2, 1, 1 },
|
||||
{ "body", 3, 3, 3 },
|
||||
{ "legs", 2, 1, 2 },
|
||||
{ "arms", 1, 1, 1 },
|
||||
{ "hands", 1, 1, 1 },
|
||||
{ "feet", 1, 1, 1 },
|
||||
/* shield handled separately in compute_ascending_ac() */
|
||||
};
|
||||
|
||||
const int NUM_ARMOR_SLOTS = sizeof(armor_slots) / sizeof(armor_slots[0]);
|
||||
|
||||
/* Wear-position mapping for armor_slots[] order */
|
||||
const int ARMOR_WEAR_POSITIONS[] = {
|
||||
WEAR_HEAD, /* "head" */
|
||||
WEAR_BODY, /* "body" */
|
||||
WEAR_LEGS, /* "legs" */
|
||||
WEAR_ARMS, /* "arms" */
|
||||
WEAR_HANDS, /* "hands" */
|
||||
WEAR_FEET /* "feet" */
|
||||
};
|
||||
|
||||
/* Armor flag names for obj->value[3] */
|
||||
const char *armor_flag_bits[] = {
|
||||
"STEALTHDISADV", /* ARMF_STEALTH_DISADV */
|
||||
"REQ_STR15", /* ARMF_REQ_STR15 */
|
||||
"\n"
|
||||
};
|
||||
|
||||
/* --- End of constants arrays. --- */
|
||||
|
||||
/* Various arrays we count so we can check the world files. These
|
||||
|
|
|
|||
|
|
@ -59,4 +59,28 @@ extern size_t affected_bits_count;
|
|||
extern size_t extra_bits_count;
|
||||
extern size_t wear_bits_count;
|
||||
|
||||
/* 5e system helpers */
|
||||
|
||||
/* Armor slot constraints for AC calculation */
|
||||
struct armor_slot {
|
||||
const char *name;
|
||||
int max_piece_ac; /* max base AC contribution from this slot */
|
||||
int max_magic; /* max magic bonus contribution from this slot */
|
||||
int bulk_weight; /* bulk contribution for encumbrance / Dex cap */
|
||||
};
|
||||
|
||||
/* Armor slot table (defined in constants.c) */
|
||||
extern const struct armor_slot armor_slots[];
|
||||
extern const int NUM_ARMOR_SLOTS;
|
||||
extern const int ARMOR_WEAR_POSITIONS[];
|
||||
/* Armor flags (obj->value[3]) */
|
||||
extern const char *armor_flag_bits[];
|
||||
|
||||
/* Bounded accuracy caps */
|
||||
#define MAX_TOTAL_ATTACK_BONUS 10 /* stats + prof + magic + situational */
|
||||
#define MAX_WEAPON_MAGIC 3
|
||||
#define MAX_SHIELD_MAGIC 3
|
||||
/* We already set this earlier: */
|
||||
#define MAX_TOTAL_ARMOR_MAGIC 3
|
||||
|
||||
#endif /* _CONSTANTS_H_ */
|
||||
|
|
|
|||
219
src/fight.c
219
src/fight.c
|
|
@ -62,7 +62,29 @@ 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 compute_thaco(struct char_data *ch, struct char_data *vict);
|
||||
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 += ability_mod(GET_STR(ch)); /* STR adds to weapon damage */
|
||||
} else {
|
||||
/* unarmed */
|
||||
dam = dice(1, 2);
|
||||
dam += 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) {
|
||||
|
|
@ -798,45 +820,21 @@ int damage(struct char_data *ch, struct char_data *victim, int dam, int attackty
|
|||
return (dam);
|
||||
}
|
||||
|
||||
/* Calculate the THAC0 of the attacker. 'victim' currently isn't used but you
|
||||
* could use it for special cases like weapons that hit evil creatures easier
|
||||
* or a weapon that always misses attacking an animal. */
|
||||
static int compute_thaco(struct char_data *ch, struct char_data *victim)
|
||||
{
|
||||
int calc_thaco;
|
||||
|
||||
if (!IS_NPC(ch))
|
||||
calc_thaco = thaco(GET_CLASS(ch), GET_LEVEL(ch));
|
||||
else /* THAC0 for monsters is set in the HitRoll */
|
||||
calc_thaco = 20;
|
||||
calc_thaco -= str_app[STRENGTH_APPLY_INDEX(ch)].tohit;
|
||||
calc_thaco -= GET_HITROLL(ch);
|
||||
calc_thaco -= (int) ((GET_INT(ch) - 13) / 1.5); /* Intelligence helps! */
|
||||
calc_thaco -= (int) ((GET_WIS(ch) - 13) / 1.5); /* So does wisdom */
|
||||
|
||||
return calc_thaco;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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);
|
||||
int w_type, victim_ac, calc_thaco, diceroll;
|
||||
int dam;
|
||||
int w_type, d20, attack_mod = 0, target_ac, dam = 0;
|
||||
bool hit_success = FALSE;
|
||||
|
||||
/* Check that the attacker and victim exist */
|
||||
/* Basic sanity */
|
||||
if (!ch || !victim) return;
|
||||
|
||||
/* check if the character has a fight trigger */
|
||||
fight_mtrigger(ch);
|
||||
|
||||
/* Do some sanity checking, in case someone flees, etc. */
|
||||
if (IN_ROOM(ch) != IN_ROOM(victim)) {
|
||||
if (FIGHTING(ch) && FIGHTING(ch) == victim)
|
||||
stop_fighting(ch);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Find the weapon type (for display purposes only) */
|
||||
/* 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 {
|
||||
|
|
@ -844,114 +842,83 @@ void hit(struct char_data *ch, struct char_data *victim, int type)
|
|||
w_type = ch->mob_specials.attack_type + TYPE_HIT;
|
||||
else
|
||||
w_type = TYPE_HIT;
|
||||
}
|
||||
} /* matches stock message mapping */ /* */
|
||||
|
||||
/* Calculate chance of hit. Lower THAC0 is better for attacker. */
|
||||
calc_thaco = compute_thaco(ch, victim);
|
||||
/* Roll d20 */
|
||||
d20 = rand_number(1, 20);
|
||||
|
||||
/* Calculate the raw armor including magic armor. Lower AC is better for defender. */
|
||||
victim_ac = compute_armor_class(victim) / 10;
|
||||
/* Ability modifier: STR only (no ranged types yet) */
|
||||
attack_mod += ability_mod(GET_STR(ch));
|
||||
|
||||
/* roll the die and take your chances... */
|
||||
diceroll = rand_number(1, 20);
|
||||
|
||||
/* report for debugging if necessary */
|
||||
if (CONFIG_DEBUG_MODE >= NRM)
|
||||
send_to_char(ch, "\t1Debug:\r\n \t2Thaco: \t3%d\r\n \t2AC: \t3%d\r\n \t2Diceroll: \t3%d\tn\r\n",
|
||||
calc_thaco, victim_ac, diceroll);
|
||||
|
||||
/* -----------------------------------------------------------
|
||||
* To-hit & Shield bonuses:
|
||||
* - Natural 20 = auto hit, 1 = auto miss (keep unchanged)
|
||||
* - Otherwise, modify the *roll* with:
|
||||
* + (attacker skill / 10) // attack bonus
|
||||
* - (defender shield / 10) // shield reduces attacker's roll
|
||||
* Bonuses < 1 count as zero.
|
||||
* ----------------------------------------------------------- */
|
||||
if (diceroll == 20 || !AWAKE(victim)) {
|
||||
dam = TRUE;
|
||||
} else if (diceroll == 1) {
|
||||
dam = FALSE;
|
||||
} else {
|
||||
int d_adj = diceroll;
|
||||
|
||||
/* Attacker’s family skill */
|
||||
int atk_skillnum = weapon_family_skill_num(ch, wielded, w_type);
|
||||
int atk_skill = (atk_skillnum > 0) ? GET_SKILL(ch, atk_skillnum) : 0;
|
||||
int atk_bonus = atk_skill / 10; /* e.g., 0..10 */
|
||||
if (atk_bonus < 1) atk_bonus = 0; /* ignore < 1 */
|
||||
|
||||
/* Defender shield */
|
||||
int sh_bonus = 0;
|
||||
if (GET_EQ(victim, WEAR_SHIELD)) {
|
||||
int sh_skill = GET_SKILL(victim, SKILL_SHIELD_USE);
|
||||
sh_bonus = sh_skill / 10;
|
||||
if (sh_bonus < 1) sh_bonus = 0;
|
||||
}
|
||||
|
||||
d_adj += atk_bonus;
|
||||
d_adj -= sh_bonus;
|
||||
|
||||
/* NOTE: do not force auto-1/20 from adjusted roll; we keep raw auto logic above. */
|
||||
dam = (calc_thaco - d_adj <= victim_ac);
|
||||
|
||||
if (CONFIG_DEBUG_MODE >= NRM) {
|
||||
send_to_char(ch, " \t2Atk bonus: \t3%d\t2 Shield red: \t3%d\t2 Adj roll: \t3%d\tn\r\n",
|
||||
atk_bonus, sh_bonus, d_adj);
|
||||
}
|
||||
}
|
||||
|
||||
/* Skill gains: once per swing, after hit/miss known */
|
||||
/* Skill family & proficiency */
|
||||
{
|
||||
/* Attacker gains in the family skill used */
|
||||
int atk_skillnum = weapon_family_skill_num(ch, wielded, w_type);
|
||||
const char *atk_skill_name = skill_name_for_gain(atk_skillnum);
|
||||
gain_skill(ch, (char *)atk_skill_name, dam ? FALSE : TRUE);
|
||||
int skillnum = weapon_family_skill_num(ch, wielded, w_type);
|
||||
const char *skillname = skill_name_for_gain(skillnum); /* maps to "unarmed", "piercing weapons", etc. */
|
||||
|
||||
/* Defender gains in shield use if wearing a shield */
|
||||
if (GET_EQ(victim, WEAR_SHIELD)) {
|
||||
/* If miss → shield succeeded (failure=FALSE). If hit → shield failed (failure=TRUE). */
|
||||
gain_skill(victim, "shield use", dam ? TRUE : FALSE);
|
||||
}
|
||||
}
|
||||
/* proficiency from current % */
|
||||
attack_mod += prof_from_skill(GET_SKILL(ch, skillnum));
|
||||
|
||||
if (!dam) {
|
||||
/* the attacker missed the victim */
|
||||
damage(ch, victim, 0, (type == SKILL_BACKSTAB) ? SKILL_BACKSTAB : w_type);
|
||||
} else {
|
||||
/* okay, we know the guy has been hit. now calculate damage.
|
||||
* Start with the damage bonuses: the damroll and strength apply */
|
||||
dam = str_app[STRENGTH_APPLY_INDEX(ch)].todam;
|
||||
dam += GET_DAMROLL(ch);
|
||||
|
||||
/* Weapon or bare hands? */
|
||||
/* Weapon magic (cap +3) */
|
||||
if (wielded && GET_OBJ_TYPE(wielded) == ITEM_WEAPON) {
|
||||
dam += dice(GET_OBJ_VAL(wielded, 1), GET_OBJ_VAL(wielded, 2));
|
||||
} else {
|
||||
if (IS_NPC(ch))
|
||||
dam += dice(ch->mob_specials.damnodice, ch->mob_specials.damsizedice);
|
||||
else
|
||||
dam += rand_number(0, 2); /* Max 2 bare hand damage for players */
|
||||
int wmag = GET_OBJ_VAL(wielded, VAL_ARMOR_MAGIC_BONUS);
|
||||
if (wmag > MAX_WEAPON_MAGIC) wmag = MAX_WEAPON_MAGIC; /* was hard-coded 3 */
|
||||
attack_mod += wmag;
|
||||
}
|
||||
|
||||
/* Position-based damage multiplier (unchanged) */
|
||||
if (GET_POS(victim) < POS_FIGHTING)
|
||||
dam *= 1 + (POS_FIGHTING - GET_POS(victim)) / 3;
|
||||
/* Situational attack modifiers hook (spells, conditions) */
|
||||
attack_mod += 0;
|
||||
|
||||
/* at least 1 hp damage min per hit */
|
||||
dam = MAX(1, dam);
|
||||
/* Cap total attack bonus to +10 */
|
||||
if (attack_mod > MAX_TOTAL_ATTACK_BONUS)
|
||||
attack_mod = MAX_TOTAL_ATTACK_BONUS;
|
||||
|
||||
if (type == SKILL_BACKSTAB)
|
||||
damage(ch, victim, dam * backstab_mult(GET_LEVEL(ch)), SKILL_BACKSTAB);
|
||||
else
|
||||
/* 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) {
|
||||
dam = roll_damage(ch, victim, wielded, w_type);
|
||||
damage(ch, victim, dam, w_type);
|
||||
} else {
|
||||
damage(ch, victim, 0, w_type); /* miss messaging */
|
||||
}
|
||||
|
||||
/* --- Skill gains ---
|
||||
You specified that both success and failure attempt a skill gain. */
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/* check if the victim has a hitprcnt trigger */
|
||||
hitprcnt_mtrigger(victim);
|
||||
/* 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");
|
||||
/* Optional: show the same line to the victim if they are a player */
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
23
src/genobj.c
23
src/genobj.c
|
|
@ -593,3 +593,26 @@ bool oset_long_description(struct obj_data *obj, char * argument)
|
|||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* 5e system helpers */
|
||||
|
||||
/* Clamp 5e-like armor values to safe ranges */
|
||||
void clamp_armor_values(struct obj_data *obj) {
|
||||
if (!obj || GET_OBJ_TYPE(obj) != ITEM_ARMOR) return;
|
||||
|
||||
int v;
|
||||
|
||||
v = GET_OBJ_VAL(obj, VAL_ARMOR_PIECE_AC);
|
||||
if (v < 0) v = 0; else if (v > 3) v = 3;
|
||||
GET_OBJ_VAL(obj, VAL_ARMOR_PIECE_AC) = v;
|
||||
|
||||
v = GET_OBJ_VAL(obj, VAL_ARMOR_BULK);
|
||||
if (v < 0) v = 0; else if (v > 3) v = 3;
|
||||
GET_OBJ_VAL(obj, VAL_ARMOR_BULK) = v;
|
||||
|
||||
v = GET_OBJ_VAL(obj, VAL_ARMOR_MAGIC_BONUS);
|
||||
if (v < 0) v = 0; else if (v > 3) v = 3; /* total armor magic still capped at runtime */
|
||||
GET_OBJ_VAL(obj, VAL_ARMOR_MAGIC_BONUS) = v;
|
||||
|
||||
/* flags are a bitvector; leave as-is (OLC will manage legal bits) */
|
||||
}
|
||||
|
|
@ -79,6 +79,7 @@ cpp_extern const struct command_info cmd_info[] = {
|
|||
{ "sw" , "sw" , POS_STANDING, do_move , 0, SCMD_SW },
|
||||
|
||||
/* now, the main list */
|
||||
{ "acaudit" , "acaudi" , POS_DEAD , do_acaudit , LVL_IMMORT, 0 },
|
||||
{ "at" , "at" , POS_DEAD , do_at , LVL_IMMORT, 0 },
|
||||
{ "advance" , "adv" , POS_DEAD , do_advance , LVL_GRGOD, 0 },
|
||||
{ "aedit" , "aed" , POS_DEAD , do_oasis_aedit, LVL_GOD, 0 },
|
||||
|
|
|
|||
|
|
@ -1279,6 +1279,28 @@ struct recent_player
|
|||
struct recent_player *next; /* Pointer to the next instance */
|
||||
};
|
||||
|
||||
/* 5e system helpers */
|
||||
|
||||
/* Armor item values (for ITEM_ARMOR objects)
|
||||
* value[0] = piece_ac (0–3)
|
||||
* value[1] = bulk (0–3)
|
||||
* value[2] = magic_bonus (0–3, capped globally later)
|
||||
* value[3] = armor flags (bitvector, see ARMF_*)
|
||||
*/
|
||||
#define VAL_ARMOR_PIECE_AC 0
|
||||
#define VAL_ARMOR_BULK 1
|
||||
#define VAL_ARMOR_MAGIC_BONUS 2
|
||||
#define VAL_ARMOR_FLAGS 3
|
||||
|
||||
/* Armor flags (value[3]) */
|
||||
#define ARMF_STEALTH_DISADV (1 << 0) /* Disadvantage on Stealth */
|
||||
|
||||
/* Armor-specific flags stored in obj->value[3] */
|
||||
#define ARMF_STEALTH_DISADV (1 << 0) /* Disadvantage on Stealth checks */
|
||||
#define ARMF_REQ_STR15 (1 << 1) /* Requires STR 15 to wear */
|
||||
#define ARMF_RESERVED2 (1 << 2) /* Reserved for future use */
|
||||
#define ARMF_RESERVED3 (1 << 3) /* Reserved */
|
||||
|
||||
/* Config structs */
|
||||
|
||||
/** The game configuration structure used for configurating the game play
|
||||
|
|
|
|||
211
src/tests/sim_5e.c
Normal file
211
src/tests/sim_5e.c
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
/* tests/sim_5e.c — quick simulations for 5e-like tuning (fixed RNG + bounds) */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
#include "conf.h"
|
||||
#include "sysdep.h"
|
||||
|
||||
#include "structs.h"
|
||||
#include "utils.h"
|
||||
#include "constants.h"
|
||||
|
||||
/* ---------- local RNG for the sim (do NOT use MUD's rand_number here) ---------- */
|
||||
static inline int randi_closed(int lo, int hi) {
|
||||
/* inclusive [lo, hi] using C RNG; assumes lo <= hi */
|
||||
return lo + (rand() % (hi - lo + 1));
|
||||
}
|
||||
|
||||
static inline int d20_local(void) { return randi_closed(1, 20); }
|
||||
|
||||
static inline int dice_local(int ndice, int sdice) {
|
||||
int sum = 0;
|
||||
for (int i = 0; i < ndice; ++i) sum += randi_closed(1, sdice);
|
||||
return sum;
|
||||
}
|
||||
|
||||
/* ---------- minimal helpers (same style as tests_5e) ---------- */
|
||||
static void init_test_char(struct char_data *ch) {
|
||||
memset(ch, 0, sizeof(*ch));
|
||||
/* ensure skills and other saved fields exist if GET_SKILL() dereferences */
|
||||
ch->player_specials = calloc(1, sizeof(struct player_special_data));
|
||||
/* if your tree uses player_specials->saved, calloc above keeps it zeroed */
|
||||
ch->in_room = 0; /* park them in room #0 (we'll make a stub room below) */
|
||||
}
|
||||
|
||||
static struct obj_data *make_armor(int piece_ac, int bulk, int magic, int flags) {
|
||||
struct obj_data *o = calloc(1, sizeof(*o));
|
||||
GET_OBJ_TYPE(o) = ITEM_ARMOR;
|
||||
GET_OBJ_VAL(o, VAL_ARMOR_PIECE_AC) = piece_ac;
|
||||
GET_OBJ_VAL(o, VAL_ARMOR_BULK) = bulk;
|
||||
GET_OBJ_VAL(o, VAL_ARMOR_MAGIC_BONUS) = magic;
|
||||
GET_OBJ_VAL(o, VAL_ARMOR_FLAGS) = flags;
|
||||
return o;
|
||||
}
|
||||
static void equip_at(struct char_data *ch, int wear_pos, struct obj_data *o) {
|
||||
if (wear_pos < 0 || wear_pos >= NUM_WEARS) {
|
||||
fprintf(stderr, "equip_at: wear_pos %d out of bounds (NUM_WEARS=%d)\n", wear_pos, NUM_WEARS);
|
||||
abort();
|
||||
}
|
||||
ch->equipment[wear_pos] = o;
|
||||
}
|
||||
static void set_ability_scores(struct char_data *ch, int str, int dex, int con, int intel, int wis, int cha) {
|
||||
ch->real_abils.str = str;
|
||||
ch->real_abils.dex = dex;
|
||||
ch->real_abils.con = con;
|
||||
ch->real_abils.intel = intel;
|
||||
ch->real_abils.wis = wis;
|
||||
ch->real_abils.cha = cha;
|
||||
ch->aff_abils = ch->real_abils;
|
||||
}
|
||||
|
||||
/* d20 hit sim with nat 1/20 using local RNG */
|
||||
static double hit_rate(int attack_mod, int target_ac, int trials) {
|
||||
int hits = 0;
|
||||
for (int i=0;i<trials;++i) {
|
||||
int d20 = d20_local();
|
||||
int hit = (d20==20) || (d20!=1 && (d20 + attack_mod) >= target_ac);
|
||||
hits += hit;
|
||||
}
|
||||
return (double)hits / (double)trials;
|
||||
}
|
||||
|
||||
/* simple DPR per swing: roll dice + STR mod; 0 on miss (local RNG) */
|
||||
static int swing_damage(int ndice, int sdice, int str_mod) {
|
||||
return dice_local(ndice, sdice) + str_mod;
|
||||
}
|
||||
|
||||
/* duel until someone hits 0 HP; return rounds elapsed (attacker first each round) */
|
||||
static int duel_rounds(int atk_mod, int ndice, int sdice, int att_str_mod,
|
||||
struct char_data *def, int def_hp, int trials)
|
||||
{
|
||||
int rounds_sum = 0;
|
||||
int def_ac = compute_ascending_ac(def);
|
||||
|
||||
for (int t=0;t<trials;++t) {
|
||||
int hp = def_hp;
|
||||
int rounds = 0;
|
||||
while (hp > 0) {
|
||||
int d20 = d20_local();
|
||||
int hit = (d20==20) || (d20!=1 && (d20 + atk_mod) >= def_ac);
|
||||
if (hit) hp -= swing_damage(ndice, sdice, att_str_mod);
|
||||
rounds++;
|
||||
if (rounds > 1000) break; /* safety */
|
||||
}
|
||||
rounds_sum += rounds;
|
||||
}
|
||||
return (int) floor((double)rounds_sum / (double)trials);
|
||||
}
|
||||
|
||||
/* build three defenders with your real slot weights and caps */
|
||||
static void build_light(struct char_data *ch) {
|
||||
init_test_char(ch);
|
||||
set_ability_scores(ch, 10, 18, 10, 10, 10, 10);
|
||||
equip_at(ch, WEAR_HEAD, make_armor(1,1,1,0));
|
||||
equip_at(ch, WEAR_BODY, make_armor(2,1,2,0));
|
||||
}
|
||||
|
||||
static void build_medium(struct char_data *ch) {
|
||||
init_test_char(ch);
|
||||
set_ability_scores(ch, 10, 18, 10, 10, 10, 10);
|
||||
equip_at(ch, WEAR_LEGS, make_armor(2,2,2,0));
|
||||
equip_at(ch, WEAR_HANDS, make_armor(1,1,0,0));
|
||||
equip_at(ch, WEAR_FEET, make_armor(1,1,0,0));
|
||||
}
|
||||
|
||||
static void build_heavy(struct char_data *ch, int shield_magic) {
|
||||
init_test_char(ch);
|
||||
set_ability_scores(ch, 10, 18, 10, 10, 10, 10);
|
||||
equip_at(ch, WEAR_BODY, make_armor(3,3,3,0));
|
||||
equip_at(ch, WEAR_LEGS, make_armor(2,2,3,0));
|
||||
struct obj_data *shield = make_armor(0,0,shield_magic,0);
|
||||
equip_at(ch, WEAR_SHIELD, shield);
|
||||
}
|
||||
|
||||
|
||||
/* attacker profiles: compute attack_mod = STRmod + prof(skill%) + weapon_magic */
|
||||
static int compute_attack_mod(int str_score, int skill_pct, int weapon_magic) {
|
||||
int mod = ability_mod(str_score);
|
||||
mod += prof_from_skill(skill_pct);
|
||||
if (weapon_magic > MAX_WEAPON_MAGIC) weapon_magic = MAX_WEAPON_MAGIC;
|
||||
mod += weapon_magic;
|
||||
if (mod > MAX_TOTAL_ATTACK_BONUS) mod = MAX_TOTAL_ATTACK_BONUS;
|
||||
return mod;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
/* seed local RNG (do not rely on MUD RNG here) */
|
||||
srand(42);
|
||||
|
||||
/* 1) Hit-rate grid: atk_mod 0..10 vs AC 12..20 */
|
||||
printf("Hit-rate grid (trials=50000):\n ");
|
||||
for (int ac=12; ac<=20; ++ac) printf(" AC%2d ", ac);
|
||||
printf("\n");
|
||||
for (int atk=0; atk<=10; ++atk) {
|
||||
printf("atk%2d ", atk);
|
||||
for (int ac=12; ac<=20; ++ac) {
|
||||
double p = hit_rate(atk, ac, 50000);
|
||||
printf(" %5.1f", p*100.0);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
/* 2) Real defenders AC using your compute_ac_breakdown */
|
||||
struct char_data light, medium, heavy0, heavy3;
|
||||
build_light(&light);
|
||||
build_medium(&medium);
|
||||
build_heavy(&heavy0, 0);
|
||||
build_heavy(&heavy3, 5); /* requests +5 but shield path clamps to +3 */
|
||||
|
||||
struct ac_breakdown bl, bm, bh0, bh3;
|
||||
compute_ac_breakdown(&light, &bl);
|
||||
compute_ac_breakdown(&medium, &bm);
|
||||
compute_ac_breakdown(&heavy0, &bh0);
|
||||
compute_ac_breakdown(&heavy3, &bh3);
|
||||
|
||||
printf("Defender AC breakdowns:\n");
|
||||
printf(" Light : total=%d (base=%d armor=%d magic=%d dexCap=%d dex=%+d shield=%d situ=%+d bulk=%d)\n",
|
||||
bl.total, bl.base, bl.armor_piece_sum, bl.armor_magic_sum, bl.dex_cap,
|
||||
bl.dex_mod_applied, bl.shield_bonus, bl.situational, bl.total_bulk);
|
||||
printf(" Medium: total=%d (base=%d armor=%d magic=%d dexCap=%d dex=%+d shield=%d situ=%+d bulk=%d)\n",
|
||||
bm.total, bm.base, bm.armor_piece_sum, bm.armor_magic_sum, bm.dex_cap,
|
||||
bm.dex_mod_applied, bm.shield_bonus, bm.situational, bm.total_bulk);
|
||||
printf(" Heavy : total=%d (base=%d armor=%d magic=%d dexCap=%d dex=%+d shield=%d situ=%+d bulk=%d)\n",
|
||||
bh0.total, bh0.base, bh0.armor_piece_sum, bh0.armor_magic_sum, bh0.dex_cap,
|
||||
bh0.dex_mod_applied, bh0.shield_bonus, bh0.situational, bh0.total_bulk);
|
||||
printf(" Heavy+(sh+3 cap): total=%d (base=%d armor=%d magic=%d dexCap=%d dex=%+d shield=%d situ=%+d bulk=%d)\n\n",
|
||||
bh3.total, bh3.base, bh3.armor_piece_sum, bh3.armor_magic_sum, bh3.dex_cap,
|
||||
bh3.dex_mod_applied, bh3.shield_bonus, bh3.situational, bh3.total_bulk);
|
||||
|
||||
/* 3) Attacker profiles vs defenders (TTK & hit%, 1d8 weapon) */
|
||||
struct { int str; int skill; int wmag; const char *name; } atk[] = {
|
||||
{14, 30, 0, "Novice (STR14, skill30, wm+0)"},
|
||||
{16, 60, 1, "Skilled (STR16, skill60, wm+1)"},
|
||||
{18, 90, 3, "Expert (STR18, skill90, wm+3)"},
|
||||
};
|
||||
struct { struct char_data *def; const char *name; int hp; } def[] = {
|
||||
{ &light, "Light", 40 },
|
||||
{ &medium, "Medium", 50 },
|
||||
{ &heavy0, "Heavy", 60 },
|
||||
{ &heavy3, "Heavy+Shield+3", 60 },
|
||||
};
|
||||
|
||||
printf("Matchups (trials=20000, 1d8 weapon):\n");
|
||||
for (size_t i=0;i<sizeof(atk)/sizeof(atk[0]);++i) {
|
||||
int str_mod = ability_mod(atk[i].str);
|
||||
int atk_mod = compute_attack_mod(atk[i].str, atk[i].skill, atk[i].wmag);
|
||||
printf(" Attacker: %-28s => attack_mod %+d (STRmod %+d, prof %+d, wm %+d)\n",
|
||||
atk[i].name, atk_mod, str_mod, prof_from_skill(atk[i].skill), MIN(atk[i].wmag, MAX_WEAPON_MAGIC));
|
||||
for (size_t j=0;j<sizeof(def)/sizeof(def[0]);++j) {
|
||||
int ac = compute_ascending_ac(def[j].def);
|
||||
double p = hit_rate(atk_mod, ac, 20000);
|
||||
int r = duel_rounds(atk_mod, 1, 8, str_mod, def[j].def, def[j].hp, 20000);
|
||||
printf(" vs %-16s AC=%2d hit%%=%5.1f avg rounds-to-kill ~ %d\n", def[j].name, ac, p*100.0, r);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
56
src/tests/stubs_unit.c
Normal file
56
src/tests/stubs_unit.c
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/* tests/stubs_unit.c – minimal stubs to satisfy utils.o linkage for unit tests */
|
||||
#include "conf.h"
|
||||
#include "sysdep.h"
|
||||
#include "structs.h"
|
||||
#include "utils.h"
|
||||
|
||||
/* --- Globals expected by utils.c --- */
|
||||
FILE *logfile = NULL; /* used by mudlog/basic_mud_vlog */
|
||||
struct descriptor_data *descriptor_list = NULL;
|
||||
struct char_data dummy_mob; /* used as a safe send_to_char target */
|
||||
|
||||
/* --- Minimal world so in_room lookups are safe in tests/sims --- */
|
||||
struct room_data stub_room; /* zeroed room */
|
||||
struct room_data *world = &stub_room;
|
||||
int top_of_world = 0; /* room/world info */
|
||||
struct weather_data weather_info;
|
||||
|
||||
/* A few arrays symbols utils.c references in helpers (keep minimal) */
|
||||
const char *pc_class_types[] = { "class", NULL };
|
||||
|
||||
/* --- Functions utils.c references that we don't need in tests --- */
|
||||
void send_to_char(struct char_data *ch, const char *messg, ...) {
|
||||
/* no-op for tests */
|
||||
(void)ch; (void)messg;
|
||||
}
|
||||
|
||||
void act(const char *str, int hide_invisible, struct char_data *ch,
|
||||
struct obj_data *obj, void *vict_obj, int type) {
|
||||
/* no-op */
|
||||
(void)str; (void)hide_invisible; (void)ch; (void)obj; (void)vict_obj; (void)type;
|
||||
}
|
||||
|
||||
int affected_by_spell(struct char_data *ch, int skill) {
|
||||
(void)ch; (void)skill; return 0;
|
||||
}
|
||||
|
||||
void affect_from_char(struct char_data *ch, int type) {
|
||||
(void)ch; (void)type;
|
||||
}
|
||||
|
||||
void page_string(struct descriptor_data *d, char *str, int keep_internal) {
|
||||
(void)d; (void)str; (void)keep_internal;
|
||||
}
|
||||
|
||||
int is_abbrev(const char *arg1, const char *arg2) {
|
||||
(void)arg1; (void)arg2; return 0;
|
||||
}
|
||||
|
||||
void parse_tab(const char *buf, char *out, size_t outlen) {
|
||||
/* trivial passthrough */
|
||||
if (outlen) {
|
||||
size_t i = 0;
|
||||
for (; buf[i] && i + 1 < outlen; ++i) out[i] = buf[i];
|
||||
out[i] = '\0';
|
||||
}
|
||||
}
|
||||
224
src/tests/tests_5e.c
Normal file
224
src/tests/tests_5e.c
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
/* tests_5e.c — unit tests for 5e-like rules
|
||||
*
|
||||
* Build suggestion (see Makefile note below):
|
||||
* cc -g -O2 -Wall -Wextra -o tests_5e \
|
||||
* tests_5e.c utils.o constants.o handler.o db.o random.o \
|
||||
* (plus whatever .o your build needs for GET_EQ/GET_SKILL, etc.)
|
||||
*
|
||||
* If you keep it simple and link against your normal object files,
|
||||
* memset(0) on char_data is enough for GET_SKILL == 0, etc.
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
#include "conf.h"
|
||||
#include "sysdep.h"
|
||||
|
||||
#include "structs.h"
|
||||
#include "utils.h"
|
||||
#include "constants.h"
|
||||
|
||||
/* ---------- Tiny test framework ---------- */
|
||||
static int tests_run = 0, tests_failed = 0;
|
||||
|
||||
#define T_ASSERT(cond, ...) \
|
||||
do { tests_run++; if (!(cond)) { \
|
||||
tests_failed++; \
|
||||
fprintf(stderr, "[FAIL] %s:%d: ", __FILE__, __LINE__); \
|
||||
fprintf(stderr, __VA_ARGS__); \
|
||||
fprintf(stderr, "\n"); \
|
||||
} } while (0)
|
||||
|
||||
#define T_EQI(actual, expect, label) \
|
||||
T_ASSERT((actual) == (expect), "%s: got %d, expect %d", (label), (int)(actual), (int)(expect))
|
||||
|
||||
#define T_IN_RANGE(val, lo, hi, label) \
|
||||
T_ASSERT((val) >= (lo) && (val) <= (hi), "%s: got %.3f, expect in [%.3f, %.3f]", (label), (double)(val), (double)(lo), (double)(hi))
|
||||
|
||||
/* ---------- Helpers for test setup ---------- */
|
||||
|
||||
/* Make a simple armor object with given per-piece fields. */
|
||||
static struct obj_data *make_armor(int piece_ac, int bulk, int magic, int flags) {
|
||||
struct obj_data *o = calloc(1, sizeof(*o));
|
||||
GET_OBJ_TYPE(o) = ITEM_ARMOR;
|
||||
GET_OBJ_VAL(o, VAL_ARMOR_PIECE_AC) = piece_ac;
|
||||
GET_OBJ_VAL(o, VAL_ARMOR_BULK) = bulk;
|
||||
GET_OBJ_VAL(o, VAL_ARMOR_MAGIC_BONUS) = magic;
|
||||
GET_OBJ_VAL(o, VAL_ARMOR_FLAGS) = flags;
|
||||
return o;
|
||||
}
|
||||
|
||||
/* Equip an object at a wear position. */
|
||||
static void equip_at(struct char_data *ch, int wear_pos, struct obj_data *o) {
|
||||
/* Most Circle/tbaMUD trees have ch->equipment[POS] */
|
||||
ch->equipment[wear_pos] = o;
|
||||
}
|
||||
|
||||
/* Set an ability score (helpers for readability) — adjust if your tree uses different fields. */
|
||||
static void set_ability_scores(struct char_data *ch, int str, int dex, int con, int intel, int wis, int cha) {
|
||||
/* real_abils is standard in Circle; if your tree differs, tweak these lines */
|
||||
ch->real_abils.str = str;
|
||||
ch->real_abils.dex = dex;
|
||||
ch->real_abils.con = con;
|
||||
ch->real_abils.intel= intel;
|
||||
ch->real_abils.wis = wis;
|
||||
ch->real_abils.cha = cha;
|
||||
ch->aff_abils = ch->real_abils; /* common pattern */
|
||||
}
|
||||
|
||||
/* For hit probability sanity tests: simulate pure d20 vs ascending AC with nat 1/20. */
|
||||
static double simulate_hit_rate(int attack_mod, int target_ac, int trials) {
|
||||
int hits = 0;
|
||||
for (int i = 0; i < trials; ++i) {
|
||||
int d20 = rand_number(1, 20);
|
||||
bool hit;
|
||||
if (d20 == 1) hit = FALSE;
|
||||
else if (d20 == 20) hit = TRUE;
|
||||
else hit = (d20 + attack_mod) >= target_ac;
|
||||
hits += hit ? 1 : 0;
|
||||
}
|
||||
return (double)hits / (double)trials;
|
||||
}
|
||||
|
||||
/* Dump a breakdown (useful when a test fails) */
|
||||
static void dbg_dump_ac(const char *label, struct ac_breakdown *b) {
|
||||
fprintf(stderr, "%s: total=%d (base=%d armor=%d magic=%d dexCap=%d dex=%d shield=%d situ=%d bulk=%d)\n",
|
||||
label, b->total, b->base, b->armor_piece_sum, b->armor_magic_sum,
|
||||
b->dex_cap, b->dex_mod_applied, b->shield_bonus, b->situational, b->total_bulk);
|
||||
}
|
||||
|
||||
/* ---------- TESTS ---------- */
|
||||
|
||||
static void test_ability_mod(void) {
|
||||
/* Spot-check classic 5e values and a sweep */
|
||||
T_EQI(ability_mod(10), 0, "ability_mod(10)");
|
||||
T_EQI(ability_mod(8), -1, "ability_mod(8)");
|
||||
T_EQI(ability_mod(12), 1, "ability_mod(12)");
|
||||
T_EQI(ability_mod(18), 4, "ability_mod(18)");
|
||||
T_EQI(ability_mod(1), -5, "ability_mod(1)");
|
||||
/* sweep 1..30 vs floor((s-10)/2) */
|
||||
for (int s = 1; s <= 30; ++s) {
|
||||
int expect = (int)floor((s - 10) / 2.0);
|
||||
T_EQI(ability_mod(s), expect, "ability_mod sweep");
|
||||
}
|
||||
}
|
||||
|
||||
static void test_prof_from_skill(void) {
|
||||
/* Boundaries for your <= mapping: 0..14→0, 15..29→+1, ... 91..100→+6 */
|
||||
struct { int pct, expect; const char *lbl; } cases[] = {
|
||||
{ 0, 0, "0→0"}, {14,0,"14→0"},
|
||||
{15,1,"15→1"}, {29,1,"29→1"},
|
||||
{30,2,"30→2"}, {44,2,"44→2"},
|
||||
{45,3,"45→3"}, {59,3,"59→3"},
|
||||
{60,4,"60→4"}, {74,4,"74→4"},
|
||||
{75,5,"75→5"}, {90,5,"90→5"},
|
||||
{91,6,"91→6"}, {100,6,"100→6"}
|
||||
};
|
||||
for (size_t i=0;i<sizeof(cases)/sizeof(cases[0]);++i) {
|
||||
T_EQI(prof_from_skill(cases[i].pct), cases[i].expect, cases[i].lbl);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_ac_light_medium_heavy(void) {
|
||||
/* Fresh character */
|
||||
struct char_data ch;
|
||||
memset(&ch, 0, sizeof(ch));
|
||||
|
||||
/* LIGHT SETUP:
|
||||
* Bulk target: Light (<=5).
|
||||
* Use head bulk 1 (wt 1 => +1), body bulk 1 (wt 3 => +3), total 4 => Light.
|
||||
* Dex 18 => +4 fully applied.
|
||||
* Armor piece AC: head=1, body=2 => sum = 3.
|
||||
* Magic: head+1, body+2 => per-slot ok, total 3 (at global cap).
|
||||
* No shield.
|
||||
* Expect: base 10 + piece 3 + magic 3 + Dex 4 = 20.
|
||||
*/
|
||||
set_ability_scores(&ch, 10, 18, 10, 10, 10, 10);
|
||||
equip_at(&ch, WEAR_HEAD, make_armor(1, 1, 1, 0));
|
||||
equip_at(&ch, WEAR_BODY, make_armor(2, 1, 2, 0));
|
||||
|
||||
struct ac_breakdown b1; compute_ac_breakdown(&ch, &b1);
|
||||
/* Sanity checks */
|
||||
if (b1.total != 20) dbg_dump_ac("LIGHT", &b1);
|
||||
T_EQI(b1.dex_cap, 5, "Light dex cap 5");
|
||||
T_EQI(b1.dex_mod_applied, 4, "Light dex +4 applied");
|
||||
T_EQI(b1.armor_magic_sum, 3, "Light magic cap (global) 3");
|
||||
T_EQI(b1.total, 20, "Light total AC");
|
||||
|
||||
/* MEDIUM SETUP:
|
||||
* Clear equipment and rebuild:
|
||||
* Bulk target: Medium (6..10).
|
||||
* Use legs bulk 2 (wt 2 => +4), hands bulk 1 (wt 1 => +1), feet bulk 1 (wt 1 => +1), total 6 => Medium.
|
||||
* Dex 18 => +4, but cap at +2.
|
||||
* Armor piece AC: legs=2, hands=1, feet=1 => sum = 4.
|
||||
* Magic: legs +2 only (still under global cap).
|
||||
* Expect: base 10 + piece 4 + magic 2 + Dex 2 = 18.
|
||||
*/
|
||||
memset(ch.equipment, 0, sizeof(ch.equipment));
|
||||
equip_at(&ch, WEAR_LEGS, make_armor(2, 2, 2, 0)); /* wt 2 -> bulk 4 */
|
||||
equip_at(&ch, WEAR_HANDS, make_armor(1, 1, 0, 0)); /* wt 1 -> bulk 1 */
|
||||
equip_at(&ch, WEAR_FEET, make_armor(1, 1, 0, 0)); /* wt 1 -> bulk 1 */
|
||||
|
||||
struct ac_breakdown b2; compute_ac_breakdown(&ch, &b2);
|
||||
if (b2.total != 18) dbg_dump_ac("MEDIUM", &b2);
|
||||
T_EQI(b2.dex_cap, 2, "Medium dex cap 2");
|
||||
T_EQI(b2.dex_mod_applied, 2, "Medium dex +2 applied");
|
||||
T_EQI(b2.total_bulk, 6, "Medium bulk score 6");
|
||||
T_EQI(b2.total, 18, "Medium total AC");
|
||||
|
||||
/* HEAVY SETUP:
|
||||
* Bulk target: Heavy (>=11).
|
||||
* Use body bulk 3 (wt 3 => +9), legs bulk 2 (wt 2 => +4), total 13 => Heavy.
|
||||
* Dex 18 => +4 but cap 0.
|
||||
* Armor piece AC: body=3, legs=2 => sum = 5.
|
||||
* Magic: body +3 (per-slot allows up to 3), legs +3 (per-slot 1 -> runtime clamps to 1), global cap 3 -> total 3.
|
||||
* Shield: base 2 + magic +5 (clamped to +3) + prof 0 => +5 total.
|
||||
* Expect: base 10 + piece 5 + armorMagic 3 + Dex 0 + shield 5 = 23.
|
||||
*/
|
||||
memset(ch.equipment, 0, sizeof(ch.equipment));
|
||||
equip_at(&ch, WEAR_BODY, make_armor(3, 3, 3, 0)); /* bulk 3*wt3 => 9 */
|
||||
equip_at(&ch, WEAR_LEGS, make_armor(2, 2, 3, 0)); /* magic will clamp via slot+global; bulk 2*wt2 => 4 */
|
||||
/* Shield: test magic cap on shield and zero prof */
|
||||
struct obj_data *shield = make_armor(0, 0, 5, 0);
|
||||
equip_at(&ch, WEAR_SHIELD, shield);
|
||||
|
||||
struct ac_breakdown b3; compute_ac_breakdown(&ch, &b3);
|
||||
if (b3.total != 23) dbg_dump_ac("HEAVY", &b3);
|
||||
T_EQI(b3.dex_cap, 0, "Heavy dex cap 0");
|
||||
T_EQI(b3.dex_mod_applied, 0, "Heavy dex applied 0");
|
||||
T_EQI(b3.total_bulk, 13, "Heavy bulk score 13");
|
||||
T_EQI(b3.armor_piece_sum, 5, "Heavy piece sum 5");
|
||||
T_EQI(b3.armor_magic_sum, 3, "Heavy armor magic at global cap 3");
|
||||
T_EQI(b3.shield_bonus, 5, "Shield: base 2 + magic 3 (cap) + prof 0 = 5");
|
||||
T_EQI(b3.total, 23, "Heavy total AC");
|
||||
}
|
||||
|
||||
static void test_hit_probability_sanity(void) {
|
||||
/* Sanity envelope checks (Monte Carlo with seed) */
|
||||
srand(42);
|
||||
|
||||
/* Even-ish fight: attack_mod = +5 vs AC 16 => expect about 55–65% */
|
||||
double p1 = simulate_hit_rate(/*atk*/5, /*AC*/16, 200000);
|
||||
T_IN_RANGE(p1, 0.55, 0.65, "Hit rate ~60% (atk+5 vs AC16)");
|
||||
|
||||
/* Slightly behind: atk +3 vs AC 17 => expect about 35–50% */
|
||||
double p2 = simulate_hit_rate(3, 17, 200000);
|
||||
T_IN_RANGE(p2, 0.35, 0.50, "Hit rate ~40% (atk+3 vs AC17)");
|
||||
|
||||
/* Way ahead: atk +8 vs AC 14 => expect ≳80% but < 95% (nat1 auto-miss) */
|
||||
double p3 = simulate_hit_rate(8, 14, 200000);
|
||||
T_IN_RANGE(p3, 0.80, 0.95, "Hit rate high (atk+8 vs AC14)");
|
||||
|
||||
/* Way behind: atk +0 vs AC 20 => expect ≲15% but > 5% (nat20 auto-hit) */
|
||||
double p4 = simulate_hit_rate(0, 20, 200000);
|
||||
T_IN_RANGE(p4, 0.05, 0.15, "Hit rate low (atk+0 vs AC20)");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_ability_mod();
|
||||
test_prof_from_skill();
|
||||
test_ac_light_medium_heavy();
|
||||
test_hit_probability_sanity();
|
||||
|
||||
printf("Tests run: %d, failures: %d\n", tests_run, tests_failed);
|
||||
return tests_failed ? 1 : 0;
|
||||
}
|
||||
225
src/utils.c
225
src/utils.c
|
|
@ -22,7 +22,7 @@
|
|||
#include "handler.h"
|
||||
#include "interpreter.h"
|
||||
#include "class.h"
|
||||
|
||||
#include "constants.h"
|
||||
|
||||
/** Aportable random number function.
|
||||
* @param from The lower bounds of the random number.
|
||||
|
|
@ -1554,3 +1554,226 @@ void remove_from_string(char *string, const char *to_remove)
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
/* 5e system helpers */
|
||||
|
||||
extern const struct armor_slot armor_slots[]; /* in constants.c */
|
||||
extern const int NUM_ARMOR_SLOTS; /* in constants.c */
|
||||
extern const int ARMOR_WEAR_POSITIONS[]; /* in constants.c */
|
||||
|
||||
/* --- Advantage/Disadvantage rollers --- */
|
||||
int roll_d20(void) { return rand_number(1, 20); }
|
||||
int roll_d20_adv(void) { int a=roll_d20(), b=roll_d20(); return (a>b)?a:b; }
|
||||
int roll_d20_disadv(void) { int a=roll_d20(), b=roll_d20(); return (a<b)?a:b; }
|
||||
|
||||
/* Percent style (for legacy percent-based skill checks) */
|
||||
bool percent_success(int chance_pct) {
|
||||
if (chance_pct <= 0) return FALSE;
|
||||
if (chance_pct >= 100) return TRUE;
|
||||
return rand_number(1, 100) <= chance_pct;
|
||||
}
|
||||
bool percent_success_adv(int chance_pct) {
|
||||
/* better of two tries */
|
||||
int r1 = rand_number(1, 100), r2 = rand_number(1, 100);
|
||||
int best = (r1 < r2) ? r1 : r2;
|
||||
return best <= chance_pct;
|
||||
}
|
||||
bool percent_success_disadv(int chance_pct) {
|
||||
/* worse of two tries */
|
||||
int r1 = rand_number(1, 100), r2 = rand_number(1, 100);
|
||||
int worst = (r1 > r2) ? r1 : r2;
|
||||
return worst <= chance_pct;
|
||||
}
|
||||
|
||||
/* Helper: derive Dex cap from total bulk */
|
||||
static int dex_cap_from_bulk(int total_bulk) {
|
||||
if (total_bulk <= 5) /* Light */
|
||||
return 5;
|
||||
else if (total_bulk <= 10) /* Medium */
|
||||
return 2;
|
||||
else /* Heavy */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- Stealth disadvantage detector ---
|
||||
* Returns TRUE if:
|
||||
* - Any worn armor piece has ARMF_STEALTH_DISADV, or
|
||||
* - Total bulk category is Heavy (Dex cap == 0).
|
||||
*/
|
||||
bool has_stealth_disadv(struct char_data *ch) {
|
||||
if (!ch) return FALSE;
|
||||
|
||||
int total_bulk = 0;
|
||||
bool piece_imposes = FALSE;
|
||||
|
||||
for (int i = 0; i < NUM_ARMOR_SLOTS; i++) {
|
||||
int wear_pos = ARMOR_WEAR_POSITIONS[i];
|
||||
struct obj_data *obj = GET_EQ(ch, wear_pos);
|
||||
if (!obj || GET_OBJ_TYPE(obj) != ITEM_ARMOR)
|
||||
continue;
|
||||
|
||||
/* flags in value[3] */
|
||||
int flags = GET_OBJ_VAL(obj, VAL_ARMOR_FLAGS);
|
||||
if (flags & ARMF_STEALTH_DISADV)
|
||||
piece_imposes = TRUE;
|
||||
|
||||
/* accumulate bulk */
|
||||
int piece_bulk = GET_OBJ_VAL(obj, VAL_ARMOR_BULK);
|
||||
if (piece_bulk < 0) piece_bulk = 0;
|
||||
total_bulk += piece_bulk * armor_slots[i].bulk_weight;
|
||||
}
|
||||
|
||||
/* Heavy armor bulk ⇒ Dex cap 0 ⇒ stealth disadvantage */
|
||||
int cap = dex_cap_from_bulk(total_bulk); /* Light<=5:5 / <=10:2 / else:0 */
|
||||
if (cap == 0) return TRUE;
|
||||
|
||||
return piece_imposes;
|
||||
}
|
||||
|
||||
/* Returns the 5e-style ability modifier for a given ability score. */
|
||||
int ability_mod(int score) {
|
||||
int mod = (score - 10) / 2;
|
||||
if ((score - 10) < 0 && ((score - 10) % 2 != 0))
|
||||
mod -= 1; /* adjust for C truncation toward zero */
|
||||
return mod;
|
||||
}
|
||||
|
||||
/* Converts a skill percentage (0-100) into a 5e-like proficiency bonus. */
|
||||
int prof_from_skill(int pct) {
|
||||
if (pct <= 14) return 0;
|
||||
if (pct <= 29) return 1;
|
||||
if (pct <= 44) return 2;
|
||||
if (pct <= 59) return 3;
|
||||
if (pct <= 74) return 4;
|
||||
if (pct <= 90) return 5;
|
||||
return 6; /* 91–100 (inclusive) */
|
||||
}
|
||||
|
||||
/* Forward declaration */
|
||||
int situational_ac_mods(struct char_data *ch);
|
||||
|
||||
void compute_ac_breakdown(struct char_data *ch, struct ac_breakdown *out)
|
||||
{
|
||||
int total_magic = 0;
|
||||
|
||||
if (!out) return;
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->base = 10;
|
||||
|
||||
/* Armor pieces: head/body/legs/arms/hands/feet (no shield here) */
|
||||
for (int i = 0; i < NUM_ARMOR_SLOTS; i++) {
|
||||
int wear_pos = ARMOR_WEAR_POSITIONS[i];
|
||||
struct obj_data *obj = GET_EQ(ch, wear_pos);
|
||||
if (!obj || GET_OBJ_TYPE(obj) != ITEM_ARMOR)
|
||||
continue;
|
||||
|
||||
/* piece AC */
|
||||
int piece_ac = GET_OBJ_VAL(obj, VAL_ARMOR_PIECE_AC);
|
||||
if (piece_ac < 0) piece_ac = 0;
|
||||
if (piece_ac > armor_slots[i].max_piece_ac)
|
||||
piece_ac = armor_slots[i].max_piece_ac;
|
||||
out->armor_piece_sum += piece_ac;
|
||||
|
||||
/* piece magic (slot-capped; total cap applied after loop) */
|
||||
int piece_magic = GET_OBJ_VAL(obj, VAL_ARMOR_MAGIC_BONUS);
|
||||
if (piece_magic < 0) piece_magic = 0;
|
||||
if (piece_magic > armor_slots[i].max_magic)
|
||||
piece_magic = armor_slots[i].max_magic;
|
||||
total_magic += piece_magic;
|
||||
|
||||
/* bulk contribution */
|
||||
int piece_bulk = GET_OBJ_VAL(obj, VAL_ARMOR_BULK);
|
||||
if (piece_bulk < 0) piece_bulk = 0;
|
||||
out->total_bulk += piece_bulk * armor_slots[i].bulk_weight;
|
||||
}
|
||||
|
||||
/* global armor magic cap (armor only; shield handled separately) */
|
||||
if (total_magic > MAX_TOTAL_ARMOR_MAGIC)
|
||||
total_magic = MAX_TOTAL_ARMOR_MAGIC;
|
||||
out->armor_magic_sum = total_magic;
|
||||
|
||||
/* Dex cap from bulk and applied dex mod */
|
||||
{
|
||||
int dexmod = ability_mod(GET_DEX(ch));
|
||||
out->dex_cap = dex_cap_from_bulk(out->total_bulk); /* Light<=5:5 / <=10:2 / else:0 */
|
||||
out->dex_mod_applied = (dexmod > out->dex_cap) ? out->dex_cap : dexmod;
|
||||
}
|
||||
|
||||
/* Shield: base +2, magic (capped), +Shield Use proficiency */
|
||||
{
|
||||
struct obj_data *shield = GET_EQ(ch, WEAR_SHIELD);
|
||||
if (shield && GET_OBJ_TYPE(shield) == ITEM_ARMOR) {
|
||||
int shield_bonus = 2;
|
||||
int shield_magic = GET_OBJ_VAL(shield, VAL_ARMOR_MAGIC_BONUS);
|
||||
if (shield_magic < 0) shield_magic = 0;
|
||||
if (shield_magic > MAX_SHIELD_MAGIC) shield_magic = MAX_SHIELD_MAGIC;
|
||||
shield_bonus += shield_magic;
|
||||
shield_bonus += prof_from_skill(GET_SKILL(ch, SKILL_SHIELD_USE));
|
||||
out->shield_bonus = shield_bonus;
|
||||
}
|
||||
}
|
||||
|
||||
/* Situational */
|
||||
out->situational = situational_ac_mods(ch);
|
||||
|
||||
/* Total */
|
||||
out->total = out->base
|
||||
+ out->armor_piece_sum
|
||||
+ out->armor_magic_sum
|
||||
+ out->dex_mod_applied
|
||||
+ out->shield_bonus
|
||||
+ out->situational;
|
||||
}
|
||||
|
||||
/* Compute ascending AC using 5e-like rules */
|
||||
int compute_ascending_ac(struct char_data *ch)
|
||||
{
|
||||
struct ac_breakdown b;
|
||||
compute_ac_breakdown(ch, &b);
|
||||
return b.total;
|
||||
}
|
||||
|
||||
/* Stub: situational AC mods */
|
||||
int situational_ac_mods(struct char_data *ch)
|
||||
{
|
||||
int mod = 0;
|
||||
|
||||
/* --- Shield spell (5e-style +5 AC while active) --- */
|
||||
#if defined(AFF_SHIELD_SPELL)
|
||||
if (AFF_FLAGGED(ch, AFF_SHIELD_SPELL)) mod += 5;
|
||||
#elif defined(SPELL_SHIELD)
|
||||
if (affected_by_spell(ch, SPELL_SHIELD)) mod += 5;
|
||||
#endif
|
||||
|
||||
/* --- Haste (small defensive bump; tune as desired) --- */
|
||||
#if defined(AFF_HASTE)
|
||||
if (AFF_FLAGGED(ch, AFF_HASTE)) mod += 2;
|
||||
#elif defined(SPELL_HASTE)
|
||||
if (affected_by_spell(ch, SPELL_HASTE)) mod += 2;
|
||||
#endif
|
||||
|
||||
/* --- Cover (if your codebase models it as affects) --- */
|
||||
#if defined(AFF_HALF_COVER)
|
||||
if (AFF_FLAGGED(ch, AFF_HALF_COVER)) mod += 2;
|
||||
#endif
|
||||
#if defined(AFF_THREEQ_COVER)
|
||||
if (AFF_FLAGGED(ch, AFF_THREEQ_COVER)) mod += 5;
|
||||
#endif
|
||||
|
||||
/* Add more here as you formalize effects:
|
||||
- Blur/Protection, Barkskin, Stoneskin, etc.
|
||||
Example pattern:
|
||||
#if defined(AFF_BLUR)
|
||||
if (AFF_FLAGGED(ch, AFF_BLUR)) mod += 2;
|
||||
#elif defined(SPELL_BLUR)
|
||||
if (affected_by_spell(ch, SPELL_BLUR)) mod += 2;
|
||||
#endif
|
||||
*/
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
/* Shim: ascending AC wrapper for migration */
|
||||
int compute_armor_class_asc(struct char_data *ch) {
|
||||
return compute_ascending_ac(ch);
|
||||
}
|
||||
|
|
|
|||
38
src/utils.h
38
src/utils.h
|
|
@ -73,6 +73,44 @@ int count_non_protocol_chars(char * str);
|
|||
char *right_trim_whitespace(const char *string);
|
||||
void remove_from_string(char *string, const char *to_remove);
|
||||
|
||||
/* 5e system helpers */
|
||||
|
||||
/* --- Ascending AC breakdown --- */
|
||||
struct ac_breakdown {
|
||||
int base; /* always 10 */
|
||||
int armor_piece_sum; /* sum of clamped per-piece AC (no shield) */
|
||||
int armor_magic_sum; /* sum of clamped per-piece magic (capped globally) */
|
||||
int total_bulk; /* sum of bulk * weight across armor pieces */
|
||||
int dex_cap; /* cap derived from bulk (Light 5 / Med 2 / Heavy 0) */
|
||||
int dex_mod_applied; /* min(DEX_mod, dex_cap) */
|
||||
int shield_bonus; /* base 2 + magic (cap) + Shield Use proficiency */
|
||||
int situational; /* cover, spells, etc. */
|
||||
int total; /* final AC */
|
||||
};
|
||||
|
||||
int ability_mod(int score);
|
||||
int prof_from_skill(int pct);
|
||||
int ability_mod(int score);
|
||||
int prof_from_skill(int pct);
|
||||
int compute_ascending_ac(struct char_data *ch);
|
||||
int situational_ac_mods(struct char_data *ch);
|
||||
int compute_armor_class_asc(struct char_data *ch);
|
||||
void compute_ac_breakdown(struct char_data *ch, struct ac_breakdown *out);
|
||||
int compute_ascending_ac(struct char_data *ch); /* still available */
|
||||
|
||||
/* Advantage/Disadvantage helpers */
|
||||
int roll_d20(void);
|
||||
int roll_d20_adv(void);
|
||||
int roll_d20_disadv(void);
|
||||
|
||||
/* Percent-based checks (for existing percent skill flows) */
|
||||
bool percent_success(int chance_pct); /* 0..100 */
|
||||
bool percent_success_adv(int chance_pct); /* roll twice, take better */
|
||||
bool percent_success_disadv(int chance_pct); /* roll twice, take worse */
|
||||
|
||||
/* Stealth disadvantage detector */
|
||||
bool has_stealth_disadv(struct char_data *ch);
|
||||
|
||||
/* Public functions made available form weather.c */
|
||||
void weather_and_time(int mode);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue