From 06d581b0117280a0f389398ed4e44040bc12aa89 Mon Sep 17 00:00:00 2001 From: kinther Date: Mon, 15 Dec 2025 14:34:45 -0800 Subject: [PATCH] Stealth skill update --- src/act.h | 1 + src/act.informative.c | 4 +- src/act.movement.c | 9 +- src/act.other.c | 191 ++++++++++++++++++++++++++++++++---------- src/class.c | 12 +-- src/players.c | 16 +++- src/spell_parser.c | 2 +- src/spells.h | 1 + src/utils.c | 15 +++- src/utils.h | 10 +-- 10 files changed, 194 insertions(+), 67 deletions(-) diff --git a/src/act.h b/src/act.h index 6e9c859..d8ebc22 100644 --- a/src/act.h +++ b/src/act.h @@ -242,6 +242,7 @@ bool perform_scan_sweep(struct char_data *ch); void clear_scan_results(struct char_data *ch); bool scan_can_target(struct char_data *ch, struct char_data *tch); bool scan_confirm_target(struct char_data *ch, struct char_data *tch); +void stealth_process_room_movement(struct char_data *ch, room_rnum room, int dir, bool leaving); /***************************************************************************** diff --git a/src/act.informative.c b/src/act.informative.c index 8d85fbe..af492c5 100644 --- a/src/act.informative.c +++ b/src/act.informative.c @@ -412,12 +412,12 @@ static void list_one_char(struct char_data *i, struct char_data *ch) /* Otherwise, use short description (PC or NPC) if present, else name. */ { - const char *sdesc = GET_SHORT_DESC(i); + const char *sdesc = get_char_sdesc(i); if (sdesc && *sdesc) { /* Capitalize first letter for room list */ send_to_char(ch, "%c%s", UPPER(*sdesc), sdesc + 1); } else { - send_to_char(ch, "%s", GET_NAME(i)); + send_to_char(ch, "Someone"); } } diff --git a/src/act.movement.c b/src/act.movement.c index d845585..6f684c7 100644 --- a/src/act.movement.c +++ b/src/act.movement.c @@ -274,8 +274,9 @@ int do_simple_move(struct char_data *ch, int dir, int need_specials_check) GET_MOVE(ch) -= need_movement; /* Generate the leave message and display to others in the was_in room. */ - if (!AFF_FLAGGED(ch, AFF_SNEAK)) - { + if (AFF_FLAGGED(ch, AFF_SNEAK)) { + stealth_process_room_movement(ch, was_in, dir, TRUE); + } else { snprintf(leave_message, sizeof(leave_message), "$n leaves %s.", dirs[dir]); act(leave_message, TRUE, ch, 0, 0, TO_ROOM); } @@ -299,7 +300,9 @@ int do_simple_move(struct char_data *ch, int dir, int need_specials_check) } /* Display arrival information to anyone in the destination room... */ - if (!AFF_FLAGGED(ch, AFF_SNEAK)) + if (AFF_FLAGGED(ch, AFF_SNEAK)) + stealth_process_room_movement(ch, going_to, dir, FALSE); + else act("$n has arrived.", TRUE, ch, 0, 0, TO_ROOM); /* ... and the room description to the character. */ diff --git a/src/act.other.c b/src/act.other.c index 9916669..855a904 100644 --- a/src/act.other.c +++ b/src/act.other.c @@ -189,13 +189,74 @@ ACMD(do_not_here) send_to_char(ch, "Sorry, but you cannot do that here!\r\n"); } +#define STEALTH_BASE_DC 10 + +static int get_stealth_skill_value(struct char_data *ch) +{ + int skill = GET_SKILL(ch, SKILL_STEALTH); + int legacy = MAX(GET_SKILL(ch, SKILL_HIDE), GET_SKILL(ch, SKILL_SNEAK)); + + if (skill <= 0 && legacy > 0) { + skill = MIN(legacy, 100); + SET_SKILL(ch, SKILL_STEALTH, skill); + } + + return skill; +} + +static int roll_stealth_check(struct char_data *ch) +{ + int skill = get_stealth_skill_value(ch); + int bonus = GET_ABILITY_MOD(GET_DEX(ch)) + GET_PROFICIENCY(skill); + bool disadv = has_stealth_disadv(ch) ? TRUE : FALSE; + int rolla = rand_number(1, 20); + int rollb = rand_number(1, 20); + int roll = disadv ? MIN(rolla, rollb) : rolla; + + return roll + bonus; +} + +static int sneak_effect_duration(struct char_data *ch) +{ + int skill = get_stealth_skill_value(ch); + if (skill <= 0) + return 1; + + return MAX(1, skill / 10); +} + +static bool can_scan_for_sneak(struct char_data *ch) +{ + if (!AFF_FLAGGED(ch, AFF_SCAN)) + return FALSE; + if (!GET_SKILL(ch, SKILL_PERCEPTION)) + return FALSE; + if (AFF_FLAGGED(ch, AFF_BLIND)) + return FALSE; + if (IS_DARK(IN_ROOM(ch)) && !CAN_SEE_IN_DARK(ch)) + return FALSE; + return TRUE; +} + +static int roll_scan_perception(struct char_data *ch) +{ + int bonus = GET_ABILITY_MOD(GET_WIS(ch)) + + GET_PROFICIENCY(GET_SKILL(ch, SKILL_PERCEPTION)); + int total = rand_number(1, 20) + bonus; + + if (FIGHTING(ch)) + total -= 4; + + return total; +} + ACMD(do_sneak) { struct affected_type af; - int rolla, rollb, roll, bonus, total, dc; - bool disadv = FALSE; + int total, dc; + int stealth_skill = get_stealth_skill_value(ch); - if (!GET_SKILL(ch, SKILL_SNEAK)) { + if (!stealth_skill) { send_to_char(ch, "You have no idea how to do that.\r\n"); return; } @@ -212,25 +273,11 @@ ACMD(do_sneak) affect_from_char(ch, SKILL_SNEAK); /* --- 5e-style Stealth check (DEX + proficiency) --- */ - bonus = GET_ABILITY_MOD(GET_DEX(ch)) + - GET_PROFICIENCY(GET_SKILL(ch, SKILL_SNEAK)); - - dc = 10; - - disadv = has_stealth_disadv(ch) ? TRUE : FALSE; - - rolla = rand_number(1, 20); - if (disadv) { - rollb = rand_number(1, 20); - roll = MIN(rolla, rollb); /* disadvantage: take lower roll */ - } else { - roll = rolla; - } - - total = roll + bonus; + total = roll_stealth_check(ch); + dc = STEALTH_BASE_DC; if (total < dc) { - gain_skill(ch, "sneak", FALSE); + gain_skill(ch, "stealth", FALSE); WAIT_STATE(ch, PULSE_VIOLENCE / 2); GET_MOVE(ch) -= 10; return; @@ -241,7 +288,7 @@ ACMD(do_sneak) af.spell = SKILL_SNEAK; af.location = APPLY_NONE; af.modifier = 0; - af.duration = GET_LEVEL(ch); /* keep stock duration; adjust if desired */ + af.duration = sneak_effect_duration(ch); memset(af.bitvector, 0, sizeof(af.bitvector)); SET_BIT_AR(af.bitvector, AFF_SNEAK); affect_to_char(ch, &af); @@ -250,16 +297,16 @@ ACMD(do_sneak) /* If you’ve already hidden with a higher roll, keep the stronger value. */ SET_STEALTH_CHECK(ch, MAX(GET_STEALTH_CHECK(ch), total)); - gain_skill(ch, "sneak", TRUE); + gain_skill(ch, "stealth", TRUE); GET_MOVE(ch) -= 10; } ACMD(do_hide) { - int rolla, rollb, roll, bonus, total, dc; - bool disadv = FALSE; + int total, dc; + int stealth_skill = get_stealth_skill_value(ch); - if (!GET_SKILL(ch, SKILL_HIDE)) { + if (!stealth_skill) { send_to_char(ch, "You have no idea how to do that.\r\n"); return; } @@ -278,28 +325,14 @@ ACMD(do_hide) } /* you can't hide while in active melee */ /* --- 5e Stealth (DEX) ability check --- */ - bonus = GET_ABILITY_MOD(GET_DEX(ch)) + GET_PROFICIENCY(GET_SKILL(ch, SKILL_HIDE)); - /* Baseline difficulty: hiding in general */ /* TODO: Maybe change dc based on terrain/populated rooms in the future */ - dc = 10; - - /* Armor/gear can impose disadvantage */ - disadv = has_stealth_disadv(ch) ? TRUE : FALSE; - - rolla = rand_number(1, 20); - if (disadv) { - rollb = rand_number(1, 20); - roll = MIN(rolla, rollb); /* disadvantage: take the lower */ - } else { - roll = rolla; - } - - total = roll + bonus; + dc = STEALTH_BASE_DC; + total = roll_stealth_check(ch); if (total < dc) { /* Failure */ - gain_skill(ch, "hide", FALSE); + gain_skill(ch, "stealth", FALSE); WAIT_STATE(ch, PULSE_VIOLENCE / 2); GET_MOVE(ch) -= 10; return; @@ -310,7 +343,7 @@ ACMD(do_hide) GET_STEALTH_CHECK(ch) = total; send_to_char(ch, "You hide yourself as best you can.\r\n"); - gain_skill(ch, "hide", TRUE); + gain_skill(ch, "stealth", TRUE); WAIT_STATE(ch, PULSE_VIOLENCE / 2); GET_MOVE(ch) -= 10; } @@ -365,6 +398,78 @@ static void forget_scan_target(struct char_data *ch, struct char_data *tch) } } +void stealth_process_room_movement(struct char_data *ch, room_rnum room, int dir, bool leaving) +{ + struct char_data *viewer; + int stealth_total; + bool base_failure; + const char *dir_word; + char msg[MAX_INPUT_LENGTH]; + char sdesc_buf[MAX_INPUT_LENGTH]; + const char *name_desc; + const char *format; + + if (!ch || room == NOWHERE) + return; + + stealth_total = roll_stealth_check(ch); + base_failure = (stealth_total < STEALTH_BASE_DC); + + if (dir >= 0 && dir < NUM_OF_DIRS) { + dir_word = leaving ? dirs[dir] : dirs[rev_dir[dir]]; + } else { + dir_word = "somewhere"; + } + + if (get_char_sdesc(ch) && *get_char_sdesc(ch)) { + strlcpy(sdesc_buf, get_char_sdesc(ch), sizeof(sdesc_buf)); + if (*sdesc_buf) + sdesc_buf[0] = UPPER(sdesc_buf[0]); + name_desc = sdesc_buf; + } else { + name_desc = "Someone"; + } + + format = leaving ? + "%s tries to stealthily move to the %s." : + "%s stealthily moves in from the %s."; + snprintf(msg, sizeof(msg), format, name_desc, dir_word); + + for (viewer = world[room].people; viewer; viewer = viewer->next_in_room) { + bool viewer_can_scan, saw_with_scan = FALSE, send_echo = FALSE; + + if (viewer == ch) + continue; + + viewer_can_scan = can_scan_for_sneak(viewer); + + if (viewer_can_scan) { + int perception_total = roll_scan_perception(viewer); + + if (perception_total >= stealth_total) { + saw_with_scan = TRUE; + send_echo = TRUE; + remember_scan_target(viewer, ch); + } else if (!base_failure) { + forget_scan_target(viewer, ch); + } + + gain_skill(viewer, "perception", saw_with_scan ? TRUE : FALSE); + } + + if (!send_echo && base_failure) { + if (!viewer_can_scan && !CAN_SEE(viewer, ch)) + continue; + send_echo = TRUE; + if (viewer_can_scan) + remember_scan_target(viewer, ch); + } + + if (send_echo) + send_to_char(viewer, "%s\r\n", msg); + } +} + void clear_scan_results(struct char_data *ch) { struct scan_result_data *node, *next; diff --git a/src/class.c b/src/class.c index c8bbfa3..263f026 100644 --- a/src/class.c +++ b/src/class.c @@ -349,8 +349,7 @@ void grant_class_skills(struct char_data *ch, bool reset) break; case CLASS_ROGUE: - SET_SKILL(ch, SKILL_SNEAK, 5); - SET_SKILL(ch, SKILL_HIDE, 5); + SET_SKILL(ch, SKILL_STEALTH, 5); SET_SKILL(ch, SKILL_TRACK, 5); SET_SKILL(ch, SKILL_STEAL, 5); SET_SKILL(ch, SKILL_BACKSTAB, 5); @@ -386,8 +385,7 @@ void grant_class_skills(struct char_data *ch, bool reset) break; case CLASS_RANGER: - SET_SKILL(ch, SKILL_SNEAK, 5); - SET_SKILL(ch, SKILL_HIDE, 5); + SET_SKILL(ch, SKILL_STEALTH, 5); SET_SKILL(ch, SKILL_BANDAGE, 5); SET_SKILL(ch, SKILL_TRACK, 5); SET_SKILL(ch, SKILL_BASH, 5); @@ -642,16 +640,15 @@ void init_spell_levels(void) spell_level(SKILL_SHIELD_USE, CLASS_CLERIC, 1); /* ROGUES */ - spell_level(SKILL_SNEAK, CLASS_ROGUE, 1); spell_level(SKILL_PICK_LOCK, CLASS_ROGUE, 1); spell_level(SKILL_BACKSTAB, CLASS_ROGUE, 1); spell_level(SKILL_STEAL, CLASS_ROGUE, 1); - spell_level(SKILL_HIDE, CLASS_ROGUE, 1); spell_level(SKILL_TRACK, CLASS_ROGUE, 1); spell_level(SKILL_UNARMED, CLASS_ROGUE, 1); spell_level(SKILL_PIERCING_WEAPONS, CLASS_ROGUE, 1); spell_level(SKILL_SHIELD_USE, CLASS_ROGUE, 1); spell_level(SKILL_PERCEPTION, CLASS_ROGUE, 1); + spell_level(SKILL_STEALTH, CLASS_ROGUE, 1); /* FIGHTERS */ spell_level(SKILL_KICK, CLASS_FIGHTER, 1); @@ -676,8 +673,6 @@ void init_spell_levels(void) spell_level(SKILL_PERCEPTION, CLASS_BARBARIAN, 1); /* RANGERS */ - spell_level(SKILL_SNEAK, CLASS_RANGER, 1); - spell_level(SKILL_HIDE, CLASS_RANGER, 1); spell_level(SKILL_BANDAGE, CLASS_RANGER, 1); spell_level(SKILL_TRACK, CLASS_RANGER, 1); spell_level(SKILL_BASH, CLASS_RANGER, 1); @@ -686,6 +681,7 @@ void init_spell_levels(void) spell_level(SKILL_PIERCING_WEAPONS, CLASS_RANGER, 1); spell_level(SKILL_SHIELD_USE, CLASS_RANGER, 1); spell_level(SKILL_PERCEPTION, CLASS_RANGER, 1); + spell_level(SKILL_STEALTH, CLASS_RANGER, 1); /* BARDS */ spell_level(SPELL_ARMOR, CLASS_BARD, 1); diff --git a/src/players.c b/src/players.c index 811aaba..a116b92 100644 --- a/src/players.c +++ b/src/players.c @@ -442,10 +442,22 @@ int load_char(const char *name, struct char_data *ch) else if (!strcmp(tag, "ScrW")) GET_SCREEN_WIDTH(ch) = atoi(line); else if (!strcmp(tag, "Skil")) load_skills(fl, ch); else if (!strcmp(tag, "SkGt")) { /* Skill Gain Timers */ + char *p = line; for (int i = 1; i <= MAX_SKILLS; i++) { long t = 0; - if (fscanf(fl, " %ld", &t) != 1) - t = 0; + + while (*p && isspace((unsigned char)*p)) + ++p; + + if (*p) { + char *endptr = p; + t = strtol(p, &endptr, 10); + if (endptr == p) + t = 0; + else + p = endptr; + } + GET_SKILL_NEXT_GAIN(ch, i) = (time_t)t; } } diff --git a/src/spell_parser.c b/src/spell_parser.c index ebf54f9..42de079 100644 --- a/src/spell_parser.c +++ b/src/spell_parser.c @@ -935,5 +935,5 @@ void mag_assign_spells(void) { skillo(SKILL_SLASHING_WEAPONS, "slashing weapons"); skillo(SKILL_BLUDGEONING_WEAPONS, "bludgeoning weapons"); skillo(SKILL_PERCEPTION, "perception"); + skillo(SKILL_STEALTH, "stealth"); } - diff --git a/src/spells.h b/src/spells.h index 4e9e0bd..98e9d5d 100644 --- a/src/spells.h +++ b/src/spells.h @@ -117,6 +117,7 @@ #define SKILL_SLASHING_WEAPONS 145 /* Reserved Skill[] DO NOT CHANGE */ #define SKILL_BLUDGEONING_WEAPONS 146 /* Reserved Skill[] DO NOT CHANGE */ #define SKILL_PERCEPTION 147 /* Reserved Skill[] DO NOT CHANGE */ +#define SKILL_STEALTH 148 /* Shared stealth skill for hide/sneak */ /* New skills may be added here up to MAX_SKILLS (200) */ diff --git a/src/utils.c b/src/utils.c index bff2916..a73bb5a 100644 --- a/src/utils.c +++ b/src/utils.c @@ -1642,7 +1642,20 @@ void remove_from_string(char *string, const char *to_remove) i--; } } - +} + +const char *get_char_sdesc(const struct char_data *ch) +{ + if (!ch) + return "someone"; + + if (GET_SHORT_DESC(ch) && *GET_SHORT_DESC(ch)) + return GET_SHORT_DESC(ch); + + if (GET_NAME(ch)) + return GET_NAME(ch); + + return "someone"; } /* 5e system helpers */ diff --git a/src/utils.h b/src/utils.h index 7c9e50e..0afd22f 100644 --- a/src/utils.h +++ b/src/utils.h @@ -79,6 +79,7 @@ int count_non_protocol_chars(char * str); char *right_trim_whitespace(const char *string); void remove_from_string(char *string, const char *to_remove); const char *const *obj_value_labels(int item_type); +const char *get_char_sdesc(const struct char_data *ch); /* 5e system helpers */ @@ -892,15 +893,10 @@ do \ /* Display name for a character as seen by 'vict'. * - If vict can’t see ch: "someone" - * - If NPC: use short_descr if set, else personal name - * - If PC: use short_descr if set, else personal name + * - Otherwise: prefer short_descr, fall back to NPC name or a generic label */ #define PERS(ch, vict) \ - (CAN_SEE((vict), (ch)) ? \ - ((GET_SHORT_DESC(ch) && *GET_SHORT_DESC(ch)) ? \ - GET_SHORT_DESC(ch) : \ - GET_NAME(ch)) : \ - "someone") + (CAN_SEE((vict), (ch)) ? get_char_sdesc(ch) : "someone") /** If vict can see obj, return obj short description, else return * "something". */