Convert system to "5e-like"

This commit is contained in:
kinther 2025-08-20 15:27:14 -07:00
parent b6864f8db9
commit 75316a6702
19 changed files with 1381 additions and 317 deletions

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
bin/*
src/*.o
src/util/*.o
src/tests/*.o
config.cache
config.log
config.status

View file

@ -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 slots 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 (610) 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 \~1216
Medium : AC \~1418
Heavy : AC \~1620
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]): 03 (per-slot hard cap)
Bulk (value[1]): 03 (drives Dex cap & stealth)
Magic (value[2]): 03 (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 610): Dex cap +2
Heavy (bulk ≥ 11): Dex cap +0 and imposes Stealth Disadvantage
Bulk is computed by summing each pieces 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: 03 (slot-capped)
value[1] Bulk: 03 (affects Dex cap/stealth)
value[2] Magic: 03 (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 ~1216
Medium: AC ~1418
Heavy: AC ~1620
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

View file

@ -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);

View file

@ -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

View file

@ -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)

View file

@ -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 wont 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
}

View file

@ -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. */

View file

@ -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);

View file

@ -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

View file

@ -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_ */

View file

@ -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;
/* Attackers 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 theyre 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)
{

View file

@ -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) */
}

View file

@ -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 },

View file

@ -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 (03)
* value[1] = bulk (03)
* value[2] = magic_bonus (03, 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
View 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
View 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
View 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 5565% */
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 3550% */
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;
}

View file

@ -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; /* 91100 (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);
}

View file

@ -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);