Scan/hide update

This commit is contained in:
kinther 2025-12-15 13:39:03 -08:00
parent cba4a5b0aa
commit 219d59787f
10 changed files with 315 additions and 175 deletions

View file

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

View file

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

View file

@ -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: its 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 dont 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;
}
}

View file

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

View file

@ -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;
wear_bits_count = sizeof(wear_bits) / sizeof(wear_bits[0]) - 1;

View file

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

View file

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

View file

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

View file

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

View file

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