From ff5f254fce886f81b757870c0d4876b541b9aaff Mon Sep 17 00:00:00 2001 From: kinther Date: Sun, 14 Dec 2025 14:56:09 -0800 Subject: [PATCH] Fix saving throw bonus logic --- src/act.wizard.c | 17 +++++++------ src/class.h | 1 + src/magic.c | 44 +++++++++++++++----------------- src/spells.c | 11 ++++++-- src/spells.h | 2 +- src/utils.c | 66 +++++++++++++++++++++++++++++++++++++++++++++++- src/utils.h | 4 +++ 7 files changed, 109 insertions(+), 36 deletions(-) diff --git a/src/act.wizard.c b/src/act.wizard.c index c9ccb6a..0233f4f 100644 --- a/src/act.wizard.c +++ b/src/act.wizard.c @@ -1274,14 +1274,15 @@ static void do_stat_character(struct char_data *ch, struct char_data *k) CCCYN(ch, C_NRM), GET_DEX(k), CCNRM(ch, C_NRM), CCCYN(ch, C_NRM), GET_CON(k), CCNRM(ch, C_NRM), CCCYN(ch, C_NRM), GET_CHA(k), CCNRM(ch, C_NRM)); - send_to_char(ch, "Saving Throws: Str: [%s%d%s] Int: [%s%d%s] Wis: [%s%d%s] " - "Dex: [%s%d%s] Con: [%s%d%s] Cha: [%s%d%s]\r\n", - CCCYN(ch, C_NRM), GET_SAVE(k, ABIL_STR), CCNRM(ch, C_NRM), - CCCYN(ch, C_NRM), GET_SAVE(k, ABIL_DEX), CCNRM(ch, C_NRM), - CCCYN(ch, C_NRM), GET_SAVE(k, ABIL_CON), CCNRM(ch, C_NRM), - CCCYN(ch, C_NRM), GET_SAVE(k, ABIL_INT), CCNRM(ch, C_NRM), - CCCYN(ch, C_NRM), GET_SAVE(k, ABIL_WIS), CCNRM(ch, C_NRM), - CCCYN(ch, C_NRM), GET_SAVE(k, ABIL_CHA), CCNRM(ch, C_NRM)); + send_to_char(ch, "Saving Throws: Str: [%s%+d%s (%+d)] Dex: [%s%+d%s (%+d)] " + "Con: [%s%+d%s (%+d)] Int: [%s%+d%s (%+d)] Wis: [%s%+d%s (%+d)] " + "Cha: [%s%+d%s (%+d)]\r\n", + CCCYN(ch, C_NRM), get_save_mod(k, ABIL_STR), CCNRM(ch, C_NRM), GET_SAVE(k, ABIL_STR), + CCCYN(ch, C_NRM), get_save_mod(k, ABIL_DEX), CCNRM(ch, C_NRM), GET_SAVE(k, ABIL_DEX), + CCCYN(ch, C_NRM), get_save_mod(k, ABIL_CON), CCNRM(ch, C_NRM), GET_SAVE(k, ABIL_CON), + CCCYN(ch, C_NRM), get_save_mod(k, ABIL_INT), CCNRM(ch, C_NRM), GET_SAVE(k, ABIL_INT), + CCCYN(ch, C_NRM), get_save_mod(k, ABIL_WIS), CCNRM(ch, C_NRM), GET_SAVE(k, ABIL_WIS), + CCCYN(ch, C_NRM), get_save_mod(k, ABIL_CHA), CCNRM(ch, C_NRM), GET_SAVE(k, ABIL_CHA)); send_to_char(ch, "Hit p.:[%s%d/%d+%d%s] Mana p.:[%s%d/%d+%d%s] Move p.:[%s%d/%d+%d%s]\r\n", CCGRN(ch, C_NRM), GET_HIT(k), GET_MAX_HIT(k), hit_gain(k), CCNRM(ch, C_NRM), diff --git a/src/class.h b/src/class.h index 35152ec..28df939 100644 --- a/src/class.h +++ b/src/class.h @@ -21,6 +21,7 @@ int invalid_class(struct char_data *ch, struct obj_data *obj); int level_exp(int chclass, int level); int parse_class(char arg); void roll_real_abils(struct char_data *ch); +bool has_save_proficiency(int class_num, int ability); /* Global variables */ diff --git a/src/magic.c b/src/magic.c index 326906d..1cb8fb6 100644 --- a/src/magic.c +++ b/src/magic.c @@ -29,29 +29,22 @@ static int mag_materials(struct char_data *ch, IDXTYPE item0, IDXTYPE item1, IDX static void perform_mag_groups(int level, struct char_data *ch, struct char_data *tch, int spellnum, int savetype); -/* Negative apply_saving_throw[] values make saving throws better! So do - * negative modifiers. Though people may be used to the reverse of that. - * It's due to the code modifying the target saving throw instead of the - * random number of the character as in some other systems. */ -int mag_savingthrow(struct char_data *ch, int type, int modifier) +/* Roll a 5e-style saving throw: d20 + ability mod + proficiency vs DC. */ +int mag_savingthrow(struct char_data *ch, int ability, int dc) { - /* NPCs use fighter tables according to some book */ - int class_sav = CLASS_FIGHTER; - int save; + int roll, total, target; - if (!IS_NPC(ch)) - class_sav = GET_CLASS(ch); + if (!ch) + return FALSE; - save = GET_SAVE(ch, type); - save += GET_SAVE(ch, type); - save += modifier; + roll = roll_d20(); + total = roll + get_save_mod(ch, ability); + target = MAX(1, dc); - /* Throwing a 0 is always a failure. */ - if (MAX(1, save) < rand_number(0, 99)) - return (TRUE); + if (total >= target) + return TRUE; - /* Oops, failed. Sorry. */ - return (FALSE); + return FALSE; } /* affect_update: called from comm.c (causes spells to wear off) */ @@ -287,7 +280,7 @@ int mag_damage(int level, struct char_data *ch, struct char_data *victim, /* divide damage by two if victim makes his saving throw */ - if (mag_savingthrow(victim, savetype, 0)) + if (mag_savingthrow(victim, savetype, compute_save_dc(ch, level, spellnum))) dam /= 2; /* and finally, inflict the damage */ @@ -307,11 +300,14 @@ void mag_affects(int level, struct char_data *ch, struct char_data *victim, bool accum_affect = FALSE, accum_duration = FALSE; const char *to_vict = NULL, *to_room = NULL; int i, j; + int save_dc; if (victim == NULL || ch == NULL) return; + save_dc = compute_save_dc(ch, level, spellnum); + for (i = 0; i < MAX_SPELL_AFFECTS; i++) { new_affect(&(af[i])); af[i].spell = spellnum; @@ -321,7 +317,7 @@ void mag_affects(int level, struct char_data *ch, struct char_data *victim, case SPELL_CHILL_TOUCH: af[0].location = APPLY_STR; - if (mag_savingthrow(victim, ABIL_CON, savetype)) + if (mag_savingthrow(victim, ABIL_CON, save_dc)) af[0].duration = 1; /* resisted: brief weakening */ else af[0].duration = 4; /* failed: longer effect */ @@ -354,7 +350,7 @@ void mag_affects(int level, struct char_data *ch, struct char_data *victim, case SPELL_BLINDNESS: if (MOB_FLAGGED(victim, MOB_NOBLIND) || GET_LEVEL(victim) >= LVL_IMMORT || - mag_savingthrow(victim, ABIL_CON, savetype)) { + mag_savingthrow(victim, ABIL_CON, save_dc)) { send_to_char(ch, "You fail.\r\n"); return; } @@ -374,7 +370,7 @@ void mag_affects(int level, struct char_data *ch, struct char_data *victim, break; case SPELL_CURSE: - if (mag_savingthrow(victim, ABIL_WIS, savetype)) { + if (mag_savingthrow(victim, ABIL_WIS, save_dc)) { send_to_char(ch, "%s", CONFIG_NOEFFECT); return; } @@ -445,7 +441,7 @@ void mag_affects(int level, struct char_data *ch, struct char_data *victim, break; case SPELL_POISON: - if (mag_savingthrow(victim, ABIL_CON, savetype)) { + if (mag_savingthrow(victim, ABIL_CON, save_dc)) { send_to_char(ch, "%s", CONFIG_NOEFFECT); return; } @@ -478,7 +474,7 @@ void mag_affects(int level, struct char_data *ch, struct char_data *victim, return; if (MOB_FLAGGED(victim, MOB_NOSLEEP)) return; - if (mag_savingthrow(victim, ABIL_WIS, savetype)) + if (mag_savingthrow(victim, ABIL_WIS, save_dc)) return; af[0].duration = 4 + (GET_LEVEL(ch) / 4); diff --git a/src/spells.c b/src/spells.c index 30ad842..e0dd445 100644 --- a/src/spells.c +++ b/src/spells.c @@ -106,9 +106,13 @@ ASPELL(spell_teleport) #define SUMMON_FAIL "You failed.\r\n" ASPELL(spell_summon) { + int save_dc; + if (ch == NULL || victim == NULL) return; + save_dc = compute_save_dc(ch, level, SPELL_SUMMON); + if (GET_LEVEL(victim) > MIN(LVL_IMMORT - 1, level + 3)) { send_to_char(ch, "%s", SUMMON_FAIL); return; @@ -143,7 +147,7 @@ ASPELL(spell_summon) } if (MOB_FLAGGED(victim, MOB_NOSUMMON) || - (IS_NPC(victim) && mag_savingthrow(victim, SAVING_CHA, 0))) { + (IS_NPC(victim) && mag_savingthrow(victim, SAVING_CHA, save_dc))) { send_to_char(ch, "%s", SUMMON_FAIL); return; } @@ -248,10 +252,13 @@ ASPELL(spell_locate_object) ASPELL(spell_charm) { struct affected_type af; + int save_dc; if (victim == NULL || ch == NULL) return; + save_dc = compute_save_dc(ch, level, SPELL_CHARM); + if (victim == ch) send_to_char(ch, "You like yourself even better!\r\n"); else if (!IS_NPC(victim) && !PRF_FLAGGED(victim, PRF_SUMMONABLE)) @@ -269,7 +276,7 @@ ASPELL(spell_charm) send_to_char(ch, "You fail - shouldn't be doing it anyway.\r\n"); else if (circle_follow(victim, ch)) send_to_char(ch, "Sorry, following in circles is not allowed.\r\n"); - else if (mag_savingthrow(victim, SAVING_WIS, 0)) + else if (mag_savingthrow(victim, SAVING_WIS, save_dc)) send_to_char(ch, "Your victim resists!\r\n"); else { if (victim->master) diff --git a/src/spells.h b/src/spells.h index a7321f1..4e9e0bd 100644 --- a/src/spells.h +++ b/src/spells.h @@ -286,7 +286,7 @@ void init_spell_levels(void); const char *skill_name(int num); /* From magic.c */ -int mag_savingthrow(struct char_data *ch, int type, int modifier); +int mag_savingthrow(struct char_data *ch, int ability, int dc); void affect_update(void); /* from spell_parser.c */ diff --git a/src/utils.c b/src/utils.c index cd5099f..f1d3942 100644 --- a/src/utils.c +++ b/src/utils.c @@ -1640,6 +1640,70 @@ int GET_ABILITY_MOD(int score) { return mod; } +/* Helper: derive a class-level proficiency bonus (no situational modifiers). */ +int get_level_proficiency_bonus(struct char_data *ch) +{ + int level; + int bonus; + + if (!ch) + return 0; + + level = MAX(1, GET_LEVEL(ch)); + bonus = 2 + ((level - 1) / 4); + if (bonus > 6) + bonus = 6; + return bonus; +} + +int get_total_proficiency_bonus(struct char_data *ch) +{ + if (!ch) + return 0; + return get_level_proficiency_bonus(ch) + GET_PROF_MOD(ch); +} + +static int get_ability_mod_from_index(struct char_data *ch, int ability) +{ + if (!ch) + return 0; + + switch (ability) { + case ABIL_STR: return GET_ABILITY_MOD(GET_STR(ch)); + case ABIL_DEX: return GET_ABILITY_MOD(GET_DEX(ch)); + case ABIL_CON: return GET_ABILITY_MOD(GET_CON(ch)); + case ABIL_INT: return GET_ABILITY_MOD(GET_INT(ch)); + case ABIL_WIS: return GET_ABILITY_MOD(GET_WIS(ch)); + case ABIL_CHA: return GET_ABILITY_MOD(GET_CHA(ch)); + default: return 0; + } +} + +int get_save_mod(struct char_data *ch, int ability) +{ + int mod; + + if (!ch) + return 0; + + mod = GET_SAVE(ch, ability); + mod += get_ability_mod_from_index(ch, ability); + + if (has_save_proficiency(GET_CLASS(ch), ability)) + mod += get_total_proficiency_bonus(ch); + + return mod; +} + +int compute_save_dc(struct char_data *caster, int level, int spellnum) +{ + if (caster) + return GET_SPELL_SAVE_DC(caster, spellnum, 0); + + /* Non-caster fallback for scrolls/wands/etc. */ + return MAX(1, 8 + (level / 2)); +} + /* Converts a skill percentage (0-100) into a 5e-like proficiency bonus. */ int GET_PROFICIENCY(int pct) { if (pct <= 14) return 0; @@ -1799,4 +1863,4 @@ struct mob_loadout *loadout_deep_copy(const struct mob_loadout *src) { else { tail->next = n; tail = n; } } return head; -} \ No newline at end of file +} diff --git a/src/utils.h b/src/utils.h index 83d09f0..48a05a3 100644 --- a/src/utils.h +++ b/src/utils.h @@ -95,6 +95,10 @@ struct ac_breakdown { int GET_ABILITY_MOD(int score); int GET_PROFICIENCY(int pct); +int get_level_proficiency_bonus(struct char_data *ch); +int get_total_proficiency_bonus(struct char_data *ch); +int get_save_mod(struct char_data *ch, int ability); +int compute_save_dc(struct char_data *caster, int level, int spellnum); int compute_ascending_ac(struct char_data *ch); int GET_SITUATIONAL_AC(struct char_data *ch); int compute_armor_class_asc(struct char_data *ch);