From 219d59787fce12df15822523ea74800c86996223 Mon Sep 17 00:00:00 2001 From: kinther Date: Mon, 15 Dec 2025 13:39:03 -0800 Subject: [PATCH] Scan/hide update --- src/act.h | 5 +- src/act.informative.c | 99 +-------------- src/act.other.c | 289 ++++++++++++++++++++++++++++++++---------- src/act.wizard.c | 15 ++- src/constants.c | 5 +- src/db.c | 3 + src/handler.c | 60 ++++++++- src/interpreter.c | 4 +- src/structs.h | 9 +- src/utils.h | 1 + 10 files changed, 315 insertions(+), 175 deletions(-) diff --git a/src/act.h b/src/act.h index de28379..6e9c859 100644 --- a/src/act.h +++ b/src/act.h @@ -235,10 +235,13 @@ ACMD(do_report); ACMD(do_save); ACMD(do_skills); ACMD(do_sneak); -ACMD(do_perception); ACMD(do_split); ACMD(do_steal); ACMD(do_visible); +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); /***************************************************************************** diff --git a/src/act.informative.c b/src/act.informative.c index ea3f270..8d85fbe 100644 --- a/src/act.informative.c +++ b/src/act.informative.c @@ -625,6 +625,7 @@ void look_at_room(struct char_data *ch, int ignore_brief) /* now list characters & objects */ list_obj_to_char(world[IN_ROOM(ch)].contents, ch, SHOW_OBJ_LONG, FALSE); list_char_to_char(world[IN_ROOM(ch)].people, ch); + perform_scan_sweep(ch); } static void look_in_direction(struct char_data *ch, int dir) @@ -2658,101 +2659,3 @@ ACMD(do_areas) else page_string(ch->desc, buf, TRUE); } - -static void list_scanned_chars(struct char_data * list, struct char_data * ch, int -distance, int door) -{ - char buf[MAX_STRING_LENGTH], buf2[MAX_STRING_LENGTH - 1]; - - const char *how_far[] = { - "close by", - "a ways off", - "far off to the" - }; - - struct char_data *i; - int count = 0; - *buf = '\0'; - -/* this loop is a quick, easy way to help make a grammatical sentence - (i.e., "You see x, x, y, and z." with commas, "and", etc.) */ - - for (i = list; i; i = i->next_in_room) - -/* put any other conditions for scanning someone in this if statement - - i.e., if (CAN_SEE(ch, i) && condition2 && condition3) or whatever */ - - if (CAN_SEE(ch, i)) - count++; - - if (!count) - return; - - for (i = list; i; i = i->next_in_room) { - -/* make sure to add changes to the if statement above to this one also, using - or's to join them.. i.e., - if (!CAN_SEE(ch, i) || !condition2 || !condition3) */ - - if (!CAN_SEE(ch, i)) - continue; - if (!*buf) - snprintf(buf, sizeof(buf), "You see %s", GET_NAME(i)); - else - strncat(buf, GET_NAME(i), sizeof(buf) - strlen(buf) - 1); - if (--count > 1) - strncat(buf, ", ", sizeof(buf) - strlen(buf) - 1); - else if (count == 1) - strncat(buf, " and ", sizeof(buf) - strlen(buf) - 1); - else { - snprintf(buf2, sizeof(buf2), " %s %s.\r\n", how_far[distance], dirs[door]); - strncat(buf, buf2, sizeof(buf) - strlen(buf) - 1); - } - - } - send_to_char(ch, "%s", buf); -} - -ACMD(do_scan) -{ - int door; - bool found=FALSE; - - int range; - int maxrange = 3; - - room_rnum scanned_room = IN_ROOM(ch); - - if (IS_AFFECTED(ch, AFF_BLIND)) { - send_to_char(ch, "You can't see a damned thing, you're blind!\r\n"); - return; - } - - for (door = 0; door < DIR_COUNT; door++) { - for (range = 1; range<= maxrange; range++) { - if (world[scanned_room].dir_option[door] && world[scanned_room].dir_option[door]->to_room != NOWHERE && - !IS_SET(world[scanned_room].dir_option[door]->exit_info, EX_CLOSED) && - !IS_SET(world[scanned_room].dir_option[door]->exit_info, EX_HIDDEN)) { - scanned_room = world[scanned_room].dir_option[door]->to_room; - if (IS_DARK(scanned_room) && !CAN_SEE_IN_DARK(ch)) { - if (world[scanned_room].people) - send_to_char(ch, "%s: It's too dark to see, but you can hear shuffling.\r\n", dirs[door]); - else - send_to_char(ch, "%s: It is too dark to see anything.\r\n", dirs[door]); - found=TRUE; - } else { - if (world[scanned_room].people) { - list_scanned_chars(world[scanned_room].people, ch, range - 1, door); - found=TRUE; - } - } - } // end of if - else - break; - } // end of range - scanned_room = IN_ROOM(ch); - } // end of directions - if (!found) { - send_to_char(ch, "You don't see anything nearby!\r\n"); - } -} // end of do_scan diff --git a/src/act.other.c b/src/act.other.c index c4d5060..9916669 100644 --- a/src/act.other.c +++ b/src/act.other.c @@ -315,84 +315,241 @@ ACMD(do_hide) GET_MOVE(ch) -= 10; } -/* Perception: scan the room for hidden creatures and objects */ -ACMD(do_perception) +static void remember_scan_target(struct char_data *ch, struct char_data *tch) +{ + struct scan_result_data *node; + long uid; + + if (!ch || !tch) + return; + if (IS_NPC(ch)) + return; + if (!ch->player_specials || ch->player_specials == &dummy_mob) + return; + + uid = char_script_id(tch); + for (node = GET_SCAN_RESULTS(ch); node; node = node->next) { + if (node->target_uid == uid) { + node->room = IN_ROOM(ch); + return; + } + } + + CREATE(node, struct scan_result_data, 1); + node->target_uid = uid; + node->room = IN_ROOM(ch); + node->next = GET_SCAN_RESULTS(ch); + GET_SCAN_RESULTS(ch) = node; +} + +static void forget_scan_target(struct char_data *ch, struct char_data *tch) +{ + struct scan_result_data **node; + long uid; + + if (!ch || !tch) + return; + if (IS_NPC(ch)) + return; + if (!ch->player_specials || ch->player_specials == &dummy_mob) + return; + + uid = char_script_id(tch); + for (node = &GET_SCAN_RESULTS(ch); *node; node = &((*node)->next)) { + if ((*node)->target_uid == uid) { + struct scan_result_data *old = *node; + *node = old->next; + free(old); + return; + } + } +} + +void clear_scan_results(struct char_data *ch) +{ + struct scan_result_data *node, *next; + + if (!ch || IS_NPC(ch)) + return; + if (!ch->player_specials || ch->player_specials == &dummy_mob) + return; + + for (node = GET_SCAN_RESULTS(ch); node; node = next) { + next = node->next; + free(node); + } + + GET_SCAN_RESULTS(ch) = NULL; +} + +bool scan_can_target(struct char_data *ch, struct char_data *tch) +{ + struct scan_result_data *node; + long uid; + + if (!ch || !tch) + return FALSE; + if (IS_NPC(ch)) + return FALSE; + if (!ch->player_specials || ch->player_specials == &dummy_mob) + return FALSE; + if (!AFF_FLAGGED(ch, AFF_SCAN)) + return FALSE; + + uid = char_script_id(tch); + + for (node = GET_SCAN_RESULTS(ch); node; node = node->next) + if (node->target_uid == uid && node->room == IN_ROOM(ch)) + return TRUE; + + return FALSE; +} + +static int scan_target_dc(struct char_data *tch) +{ + if (GET_STEALTH_CHECK(tch) <= 0) + SET_STEALTH_CHECK(tch, 5); + + /* Give hiders a modest buffer so high skill matters but success remains possible. */ + return GET_STEALTH_CHECK(tch) + 2; +} + +bool scan_confirm_target(struct char_data *ch, struct char_data *tch) +{ + int roll, bonus, total; + + if (!ch || !tch) + return FALSE; + if (!AFF_FLAGGED(ch, AFF_SCAN)) + return FALSE; + if (!GET_SKILL(ch, SKILL_PERCEPTION)) + return FALSE; + + bonus = GET_ABILITY_MOD(GET_WIS(ch)) + + GET_PROFICIENCY(GET_SKILL(ch, SKILL_PERCEPTION)); + roll = rand_number(1, 20); + total = roll + bonus; + + if (FIGHTING(ch)) + total -= 4; + + if (total >= scan_target_dc(tch)) { + remember_scan_target(ch, tch); + return TRUE; + } + + return FALSE; +} + +static int scan_effect_duration(struct char_data *ch) +{ + int skill = GET_SKILL(ch, SKILL_PERCEPTION); + int minutes; + + if (skill < 20) + minutes = 15; + else if (skill < 40) + minutes = 20; + else if (skill < 60) + minutes = 25; + else if (skill < 80) + minutes = 30; + else + minutes = 45; + + /* Affect durations tick once per mud hour (75 seconds). */ + return MAX(1, (minutes * SECS_PER_REAL_MIN) / SECS_PER_MUD_HOUR); +} + +bool perform_scan_sweep(struct char_data *ch) { struct char_data *tch; - struct obj_data *obj, *next_obj; int roll, bonus, total; - int found_chars = 0, found_objs = 0; + bool had_targets = FALSE; + bool found_any = FALSE; + + if (ch == NULL || IN_ROOM(ch) == NOWHERE) + return FALSE; + 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; + + for (tch = world[IN_ROOM(ch)].people; tch; tch = tch->next_in_room) { + if (tch == ch) + continue; + if (!AFF_FLAGGED(tch, AFF_HIDE)) + continue; + if (IS_NPC(tch)) + continue; + had_targets = TRUE; + } + + if (!had_targets) + return FALSE; + + bonus = GET_ABILITY_MOD(GET_WIS(ch)) + + GET_PROFICIENCY(GET_SKILL(ch, SKILL_PERCEPTION)); + roll = rand_number(1, 20); + total = roll + bonus; + + if (FIGHTING(ch)) + total -= 4; + + for (tch = world[IN_ROOM(ch)].people; tch; tch = tch->next_in_room) { + if (tch == ch) + continue; + if (!AFF_FLAGGED(tch, AFF_HIDE)) + continue; + if (IS_NPC(tch)) + continue; + + if (total >= scan_target_dc(tch)) { + send_to_char(ch, "A shadowy figure.\r\n"); + remember_scan_target(ch, tch); + found_any = TRUE; + } else { + forget_scan_target(ch, tch); + } + } + + gain_skill(ch, "perception", found_any ? TRUE : FALSE); + return found_any; +} + +/* Scan: apply a perception-based buff that auto-checks rooms while it lasts */ +ACMD(do_scan) +{ + struct affected_type af; if (!GET_SKILL(ch, SKILL_PERCEPTION)) { send_to_char(ch, "You have no idea how to do that.\r\n"); return; } - /* Roll once for this scan (active check) */ - bonus = GET_ABILITY_MOD(GET_WIS(ch)) + - GET_PROFICIENCY(GET_SKILL(ch, SKILL_PERCEPTION)); - - roll = rand_number(1, 20); - total = roll + bonus; - - /* Optional: it’s harder to actively scan while in melee */ - if (FIGHTING(ch)) - total -= 4; - - /* --- Scan characters in the room (PCs & NPCs) --- */ - for (tch = world[IN_ROOM(ch)].people; tch; tch = tch->next_in_room) { - if (tch == ch) - continue; - if (!AFF_FLAGGED(tch, AFF_HIDE)) - continue; - - /* Safety default if some legacy code set AFF_HIDE without a stored check */ - if (GET_STEALTH_CHECK(tch) <= 0) - SET_STEALTH_CHECK(tch, 5); - - if (total >= GET_STEALTH_CHECK(tch)) { - /* Spotted! Reveal them. */ - REMOVE_BIT_AR(AFF_FLAGS(tch), AFF_HIDE); - SET_STEALTH_CHECK(tch, 0); - ++found_chars; - - act("You spot $N hiding!", FALSE, ch, 0, tch, TO_CHAR); - act("$n seems to look right at you — you've been spotted!", FALSE, ch, 0, tch, TO_VICT); - act("$n spots $N hiding nearby!", FALSE, ch, 0, tch, TO_NOTVICT); - } + if (AFF_FLAGGED(ch, AFF_SCAN)) { + affect_from_char(ch, SKILL_PERCEPTION); + send_to_char(ch, "You lower your guard and stop scanning the area.\r\n"); + act("$n relaxes, no longer scanning so intently.", TRUE, ch, 0, 0, TO_ROOM); + return; } - /* --- Scan objects in the room (requires an ITEM_HIDDEN extra flag) --- */ - for (obj = world[IN_ROOM(ch)].contents; obj; obj = next_obj) { - next_obj = obj->next_content; + new_affect(&af); + af.spell = SKILL_PERCEPTION; + af.location = APPLY_NONE; + af.modifier = 0; + af.duration = scan_effect_duration(ch); + memset(af.bitvector, 0, sizeof(af.bitvector)); + SET_BIT_AR(af.bitvector, AFF_SCAN); + affect_to_char(ch, &af); - /* If you don't have ITEM_HIDDEN yet, add it to your extra flags table and OBJ flag names. */ -#ifdef ITEM_HIDDEN - if (OBJ_FLAGGED(obj, ITEM_HIDDEN)) { - /* Simple baseline DC for hidden objects; tune as desired or add per-object difficulty. */ - int obj_dc = 12; - if (FIGHTING(ch)) obj_dc += 2; + send_to_char(ch, "You sharpen your senses and begin scanning for hidden threats.\r\n"); + act("$n studies $s surroundings with a wary gaze.", TRUE, ch, 0, 0, TO_ROOM); - if (total >= obj_dc) { - /* Reveal the object. */ - REMOVE_BIT_AR(GET_OBJ_EXTRA(obj), ITEM_HIDDEN); - ++found_objs; - - act("You spot $p tucked out of sight.", FALSE, ch, obj, 0, TO_CHAR); - act("$n notices $p tucked out of sight.", FALSE, ch, obj, 0, TO_ROOM); - } - } -#endif - } - - if (!found_chars && !found_objs) { - send_to_char(ch, "You search carefully but don’t uncover anything hidden.\r\n"); - gain_skill(ch, "perception", FALSE); - } else { - gain_skill(ch, "perception", TRUE); - } - - /* Small action taxes do disincline players from spamming perception */ WAIT_STATE(ch, PULSE_VIOLENCE / 2); GET_MOVE(ch) -= 10; } @@ -1058,4 +1215,4 @@ ACMD(do_gen_tog) send_to_char(ch, "%s", tog_messages[subcmd][TOG_OFF]); return; -} \ No newline at end of file +} diff --git a/src/act.wizard.c b/src/act.wizard.c index 5c856d2..82af0a0 100644 --- a/src/act.wizard.c +++ b/src/act.wizard.c @@ -408,9 +408,20 @@ static void stat_format_char_effects(struct char_data *k, char *buf, size_t buf_ if (len) stat_appendf(buf, buf_size, &len, "\n"); - stat_appendf(buf, buf_size, &len, "%s (%dhr)", + const char *dur_unit = "hr"; + int display_dur = aff->duration + 1; + + if (aff->spell == SKILL_PERCEPTION) { + /* convert mud-hours -> real minutes (round up) */ + int total_seconds = display_dur * SECS_PER_MUD_HOUR; + display_dur = (total_seconds + SECS_PER_REAL_MIN - 1) / SECS_PER_REAL_MIN; + dur_unit = "min"; + } + + stat_appendf(buf, buf_size, &len, "%s (%d%s)", skill_name(aff->spell), - aff->duration + 1); + display_dur, + dur_unit); if (aff->modifier) stat_appendf(buf, buf_size, &len, " %+d %s", diff --git a/src/constants.c b/src/constants.c index 2164c80..36cd023 100644 --- a/src/constants.c +++ b/src/constants.c @@ -308,8 +308,9 @@ const char *affected_bits[] = "SCUBA", "SNEAK", "HIDE", - "UNUSED", + "SCAN", "CHARM", + "BANDAGED", "\n" }; @@ -855,4 +856,4 @@ const char *armor_flag_bits[] = { /** Number of defined extra bit descriptions. */ extra_bits_count = sizeof(extra_bits) / sizeof(extra_bits[0]) - 1, /** Number of defined wear bit descriptions. */ - wear_bits_count = sizeof(wear_bits) / sizeof(wear_bits[0]) - 1; \ No newline at end of file + wear_bits_count = sizeof(wear_bits) / sizeof(wear_bits[0]) - 1; diff --git a/src/db.c b/src/db.c index fbe7443..86523b8 100644 --- a/src/db.c +++ b/src/db.c @@ -3402,6 +3402,9 @@ void free_char(struct char_data *ch) int i; struct alias_data *a; + if (!IS_NPC(ch) && ch->player_specials && ch->player_specials != &dummy_mob) + clear_scan_results(ch); + if (ch->player_specials != NULL && ch->player_specials != &dummy_mob) { while ((a = GET_ALIASES(ch)) != NULL) { GET_ALIASES(ch) = (GET_ALIASES(ch))->next; diff --git a/src/handler.c b/src/handler.c index 127b2f9..b6f72c0 100644 --- a/src/handler.c +++ b/src/handler.c @@ -33,6 +33,7 @@ static int extractions_pending = 0; static int apply_ac(struct char_data *ch, int eq_pos); static void update_object(struct obj_data *obj, int use); static void affect_modify_ar(struct char_data * ch, byte loc, sbyte mod, int bitv[], bool add); +static bool is_shadow_keyword(const char *name); char *fname(const char *namelist) { @@ -110,6 +111,13 @@ int isname(const char *str, const char *namelist) return 0; } +static bool is_shadow_keyword(const char *name) +{ + if (!name || !*name) + return FALSE; + return isname(name, "shadow shadowy figure"); +} + static void aff_apply_modify(struct char_data *ch, byte loc, sbyte mod, char *msg) { switch (loc) { @@ -264,6 +272,8 @@ void affect_remove(struct char_data *ch, struct affected_type *af) affect_modify_ar(ch, af->location, af->modifier, af->bitvector, FALSE); REMOVE_FROM_LIST(af, ch->affected, next); + if (af->spell == SKILL_PERCEPTION) + clear_scan_results(ch); free(af); affect_total(ch); } @@ -1132,9 +1142,39 @@ struct char_data *get_char_room_vis(struct char_data *ch, char *name, int *numbe } } - if (match && CAN_SEE(ch, i)) - if (--(*number) == 0) + if (match) { + bool can_target = CAN_SEE(ch, i); + + if (!can_target && GET_LEVEL(ch) >= LVL_IMMORT) + can_target = TRUE; + + if (!can_target && + AFF_FLAGGED(ch, AFF_SCAN) && + AFF_FLAGGED(i, AFF_HIDE) && + scan_can_target(ch, i)) { + can_target = scan_confirm_target(ch, i); + } + + if (can_target && --(*number) == 0) return i; + } + } + + if ((AFF_FLAGGED(ch, AFF_SCAN) || GET_LEVEL(ch) >= LVL_IMMORT) && is_shadow_keyword(name)) { + for (i = world[IN_ROOM(ch)].people; i && *number; i = i->next_in_room) { + if (i == ch) + continue; + if (!AFF_FLAGGED(i, AFF_HIDE)) + continue; + if (GET_LEVEL(ch) < LVL_IMMORT && !scan_can_target(ch, i)) + continue; + + if (--(*number) == 0) { + if (GET_LEVEL(ch) >= LVL_IMMORT || scan_confirm_target(ch, i)) + return i; + return NULL; + } + } } return NULL; @@ -1177,8 +1217,22 @@ struct char_data *get_char_world_vis(struct char_data *ch, char *name, int *numb if (!match) continue; - if (!CAN_SEE(ch, i)) + + bool can_target = CAN_SEE(ch, i); + + if (!can_target && GET_LEVEL(ch) >= LVL_IMMORT) + can_target = TRUE; + + if (!can_target && + AFF_FLAGGED(ch, AFF_SCAN) && + AFF_FLAGGED(i, AFF_HIDE) && + scan_can_target(ch, i)) { + can_target = scan_confirm_target(ch, i); + } + + if (!can_target) continue; + if (--(*number) != 0) continue; diff --git a/src/interpreter.c b/src/interpreter.c index 6686e57..f5fc96a 100644 --- a/src/interpreter.c +++ b/src/interpreter.c @@ -226,7 +226,7 @@ cpp_extern const struct command_info cmd_info[] = { { "pick" , "pi" , POS_STANDING, do_gen_door , 1, SCMD_PICK }, { "page" , "pag" , POS_DEAD , do_page , 1, 0 }, { "pardon" , "pardon" , POS_DEAD , do_wizutil , LVL_GOD, SCMD_PARDON }, - { "perception","per" , POS_RESTING , do_perception, 0, 0 }, +{ "perception","per" , POS_RESTING , do_scan , 0, 0 }, { "plist" , "plist" , POS_DEAD , do_plist , LVL_GOD, 0 }, { "policy" , "pol" , POS_DEAD , do_gen_ps , 0, SCMD_POLICIES }, { "pour" , "pour" , POS_STANDING, do_pour , 0, SCMD_POUR }, @@ -263,7 +263,7 @@ cpp_extern const struct command_info cmd_info[] = { { "say" , "s" , POS_RESTING , do_say , 0, 0 }, { "score" , "sc" , POS_DEAD , do_score , 0, 0 }, - { "scan" , "sca" , POS_RESTING , do_scan , 0, 0 }, +{ "scan" , "sca" , POS_RESTING , do_scan , 0, 0 }, { "scopy" , "scopy" , POS_DEAD , do_oasis_copy, LVL_GOD, CON_SEDIT }, { "sit" , "si" , POS_RESTING , do_sit , 0, 0 }, { "'" , "'" , POS_RESTING , do_say , 0, 0 }, diff --git a/src/structs.h b/src/structs.h index 977c2eb..dcb2e90 100644 --- a/src/structs.h +++ b/src/structs.h @@ -298,7 +298,7 @@ #define AFF_SCUBA 18 /**< Room for future expansion */ #define AFF_SNEAK 19 /**< Char can move quietly */ #define AFF_HIDE 20 /**< Char is hidden */ -#define AFF_FREE 21 /**< Room for future expansion */ +#define AFF_SCAN 21 /**< Actively scanning for hidden threats */ #define AFF_CHARM 22 /**< Char is charmed */ #define AFF_BANDAGED 23 /**< Character was bandaged recently */ /** Total number of affect flags */ @@ -1008,6 +1008,7 @@ struct player_special_data int last_olc_mode; /**< ? Currently Unused ? */ char *host; /**< Resolved hostname, or ip, for player. */ int buildwalk_sector; /**< Default sector type for buildwalk */ + struct scan_result_data *scan_results; /**< Hidden figures this player has spotted */ }; /** Special data used by NPCs, not PCs */ @@ -1047,6 +1048,12 @@ struct mob_loadout { struct mob_loadout *next; }; +struct scan_result_data { + long target_uid; /* char_script_id() value when spotted */ + room_rnum room; /* room where the target was seen */ + struct scan_result_data *next; +}; + /** Master structure for PCs and NPCs. */ struct char_data { diff --git a/src/utils.h b/src/utils.h index 68c62cc..7c9e50e 100644 --- a/src/utils.h +++ b/src/utils.h @@ -659,6 +659,7 @@ do \ #define GET_PREF(ch) ((ch)->pref) /** Get host name or ip of ch. */ #define GET_HOST(ch) CHECK_PLAYER_SPECIAL((ch), ((ch)->player_specials->host)) +#define GET_SCAN_RESULTS(ch) CHECK_PLAYER_SPECIAL((ch), ((ch)->player_specials->scan_results)) #define GET_LAST_MOTD(ch) CHECK_PLAYER_SPECIAL((ch), ((ch)->player_specials->saved.lastmotd)) #define GET_LAST_NEWS(ch) CHECK_PLAYER_SPECIAL((ch), ((ch)->player_specials->saved.lastnews)) /** Get channel history i for ch. */