diff --git a/.gitignore b/.gitignore index 4611369..f5d243e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ bin/* src/*.o src/util/*.o +src/tests/*.o config.cache config.log config.status diff --git a/lib/text/help/help.hlp b/lib/text/help/help.hlp index 65c30a9..445f8e5 100644 --- a/lib/text/help/help.hlp +++ b/lib/text/help/help.hlp @@ -296,26 +296,158 @@ to list all forms. i. e. the keyword rumble could be used to cover anyone who types 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] slot= ac=\ bulk= mag=+ flags= + +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 diff --git a/src/act.h b/src/act.h index d6b87cf..01859a8 100644 --- a/src/act.h +++ b/src/act.h @@ -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); diff --git a/src/act.informative.c b/src/act.informative.c index 705982e..86498cc 100644 --- a/src/act.informative.c +++ b/src/act.informative.c @@ -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 diff --git a/src/act.other.c b/src/act.other.c index 0b8b894..ec5e6c0 100644 --- a/src/act.other.c +++ b/src/act.other.c @@ -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) diff --git a/src/act.wizard.c b/src/act.wizard.c index c2b43a4..4ccc6a8 100644 --- a/src/act.wizard.c +++ b/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 +} + + diff --git a/src/class.c b/src/class.c index 55e2b69..9496e9f 100644 --- a/src/class.c +++ b/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. */ diff --git a/src/class.h b/src/class.h index a2a45aa..06266b6 100644 --- a/src/class.h +++ b/src/class.h @@ -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); diff --git a/src/constants.c b/src/constants.c index 961147c..394c59e 100644 --- a/src/constants.c +++ b/src/constants.c @@ -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 diff --git a/src/constants.h b/src/constants.h index 660c780..962778c 100644 --- a/src/constants.h +++ b/src/constants.h @@ -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_ */ diff --git a/src/fight.c b/src/fight.c index 1779641..9c81131 100644 --- a/src/fight.c +++ b/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) { diff --git a/src/genobj.c b/src/genobj.c index 6420651..34c7b24 100644 --- a/src/genobj.c +++ b/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) */ +} \ No newline at end of file diff --git a/src/interpreter.c b/src/interpreter.c index 893c3cb..85856c4 100644 --- a/src/interpreter.c +++ b/src/interpreter.c @@ -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 }, diff --git a/src/structs.h b/src/structs.h index 877cfc0..72b40f7 100644 --- a/src/structs.h +++ b/src/structs.h @@ -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 diff --git a/src/tests/sim_5e.c b/src/tests/sim_5e.c new file mode 100644 index 0000000..a980b03 --- /dev/null +++ b/src/tests/sim_5e.c @@ -0,0 +1,211 @@ +/* tests/sim_5e.c — quick simulations for 5e-like tuning (fixed RNG + bounds) */ +#include +#include +#include +#include + +#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= 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 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 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 +#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 +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; +} diff --git a/src/utils.c b/src/utils.c index fc3a277..7e4b7d4 100644 --- a/src/utils.c +++ b/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= 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); +} diff --git a/src/utils.h b/src/utils.h index e23eb56..1314861 100644 --- a/src/utils.h +++ b/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);