Files
Bubberstation/code/modules/mob/mob_helpers.dm
2025-06-15 15:54:32 -04:00

585 lines
20 KiB
Plaintext

// see _DEFINES/is_helpers.dm for mob type checks
///Find the mob at the bottom of a buckle chain
/mob/proc/lowest_buckled_mob()
. = src
if(buckled && ismob(buckled))
var/mob/Buckled = buckled
. = Buckled.lowest_buckled_mob()
///Convert a PRECISE ZONE into the BODY_ZONE
/proc/check_zone(zone)
if(!zone)
return BODY_ZONE_CHEST
switch(zone)
if(BODY_ZONE_PRECISE_EYES)
zone = BODY_ZONE_HEAD
if(BODY_ZONE_PRECISE_MOUTH)
zone = BODY_ZONE_HEAD
if(BODY_ZONE_PRECISE_L_HAND)
zone = BODY_ZONE_L_ARM
if(BODY_ZONE_PRECISE_R_HAND)
zone = BODY_ZONE_R_ARM
if(BODY_ZONE_PRECISE_L_FOOT)
zone = BODY_ZONE_L_LEG
if(BODY_ZONE_PRECISE_R_FOOT)
zone = BODY_ZONE_R_LEG
if(BODY_ZONE_PRECISE_GROIN)
zone = BODY_ZONE_CHEST
return zone
/**
* Return the zone or randomly, another valid zone
*
* probability controls the chance it chooses the passed in zone, or another random zone
* defaults to 80
*/
/proc/ran_zone(zone, probability = 80, list/weighted_list)
if(prob(probability))
zone = check_zone(zone)
else
zone = pick_weight(weighted_list ? weighted_list : list(BODY_ZONE_HEAD = 1, BODY_ZONE_CHEST = 1, BODY_ZONE_L_ARM = 4, BODY_ZONE_R_ARM = 4, BODY_ZONE_L_LEG = 4, BODY_ZONE_R_LEG = 4))
return zone
/**
* More or less ran_zone, but only returns bodyzones that the mob /actually/ has.
*
* * blacklisted_parts - allows you to specify zones that will not be chosen. eg: list(BODY_ZONE_CHEST, BODY_ZONE_R_LEG)
* * * !!!! blacklisting BODY_ZONE_CHEST is really risky since it's the only bodypart guarunteed to ALWAYS exists !!!!
* * * !!!! Only do that if you're REALLY CERTAIN they have limbs, otherwise we'll CRASH() !!!!
*
* * ran_zone has a base prob(80) to return the base_zone (or if null, BODY_ZONE_CHEST) vs something in our generated list of limbs.
* * this probability is overriden when either blacklisted_parts contains BODY_ZONE_CHEST and we aren't passed a base_zone (since the default fallback for ran_zone would be the chest in that scenario), or if even_weights is enabled.
* * you can also manually adjust this probability by altering base_probability
*
* * even_weights - ran_zone has a 40% chance (after the prob(80) mentioned above) of picking a limb, vs the torso & head which have an additional 10% chance.
* * Setting even_weight to TRUE will make it just a straight up pick() between all possible bodyparts.
*
*/
/mob/proc/get_random_valid_zone(base_zone, base_probability = 80, list/blacklisted_parts, even_weights, bypass_warning)
return BODY_ZONE_CHEST //even though they don't really have a chest, let's just pass the default of check_zone to be safe.
/mob/living/carbon/get_random_valid_zone(base_zone, base_probability = 80, list/blacklisted_parts, even_weights, bypass_warning)
var/list/limbs = list()
for(var/obj/item/bodypart/part as anything in bodyparts)
var/limb_zone = part.body_zone //cache the zone since we're gonna check it a ton.
if(limb_zone in blacklisted_parts)
continue
if(even_weights)
limbs[limb_zone] = 1
continue
if(limb_zone == BODY_ZONE_CHEST || limb_zone == BODY_ZONE_HEAD)
limbs[limb_zone] = 1
else
limbs[limb_zone] = 4
if(base_zone && !(check_zone(base_zone) in limbs))
base_zone = null //check if the passed zone is infact valid
var/chest_blacklisted
if((BODY_ZONE_CHEST in blacklisted_parts))
chest_blacklisted = TRUE
if(bypass_warning && !limbs.len)
CRASH("limbs is empty and the chest is blacklisted. this may not be intended!")
return (((chest_blacklisted && !base_zone) || even_weights) ? pick_weight(limbs) : ran_zone(base_zone, base_probability, limbs))
///Would this zone be above the neck
/proc/above_neck(zone)
var/list/zones = list(BODY_ZONE_HEAD, BODY_ZONE_PRECISE_MOUTH, BODY_ZONE_PRECISE_EYES)
if(zones.Find(zone))
return TRUE
else
return FALSE
/**
* Convert random parts of a passed in message to stars
*
* * phrase - the string to convert
* * probability - probability any character gets changed
*
* This proc is dangerously laggy, avoid it or die
*/
/proc/stars(phrase, probability = 25)
if(probability <= 0)
return phrase
phrase = html_decode(phrase)
var/leng = length(phrase)
. = ""
var/char = ""
for(var/i = 1, i <= leng, i += length(char))
char = phrase[i]
if(char == " " || !prob(probability))
. += char
else
. += "*"
return sanitize(.)
/**
* For when you're only able to speak a limited amount of words
* phrase - the string to convert
* definitive_limit - the amount of words to limit the phrase to, optional
*/
/proc/stifled(phrase, definitive_limit = null)
phrase = html_decode(phrase)
var/num_words = 0
var/words = splittext(phrase, " ")
if(definitive_limit > 0) // in case someone passes a negative
num_words = min(definitive_limit, length(words))
else
num_words = min(rand(3, 5), length(words))
. = ""
for(var/i = 1, i <= num_words, i++)
if(num_words == i)
. += words[i] + "..."
else
. += words[i] + " ... "
return sanitize(.)
/**
* Turn text into complete gibberish!
*
* text is the inputted message, replace_characters will cause original letters to be replaced and chance are the odds that a character gets modified.
*/
/proc/Gibberish(text, replace_characters = FALSE, chance = 50)
text = html_decode(text)
. = ""
var/rawchar = ""
var/letter = ""
var/lentext = length(text)
for(var/i = 1, i <= lentext, i += length(rawchar))
rawchar = letter = text[i]
if(prob(chance))
if(replace_characters)
letter = ""
for(var/j in 1 to rand(0, 2))
letter += pick("#", "@", "*", "&", "%", "$", "/", "<", ">", ";", "*", "*", "*", "*", "*", "*", "*")
. += letter
return sanitize(.)
#define TILES_PER_SECOND 0.7
///Shake the camera of the person viewing the mob SO REAL!
///Takes the mob to shake, the time span to shake for, and the amount of tiles we're allowed to shake by in tiles
///Duration isn't taken as a strict limit, since we don't trust our coders to not make things feel shitty. So it's more like a soft cap.
/proc/shake_camera(mob/M, duration, strength=1)
if(!M || !M.client || duration < 1)
return
var/client/C = M.client
var/oldx = C.pixel_x
var/oldy = C.pixel_y
var/max_x = strength*ICON_SIZE_X
var/max_y = strength*ICON_SIZE_Y
var/min_x = -(strength*ICON_SIZE_X)
var/min_y = -(strength*ICON_SIZE_Y)
if(C.prefs?.read_preference(/datum/preference/toggle/screen_shake_darken))
var/type = /atom/movable/screen/fullscreen/flash/black
M.overlay_fullscreen("flash", type)
addtimer(CALLBACK(M, TYPE_PROC_REF(/mob, clear_fullscreen), "flash", 3 SECONDS), 3 SECONDS)
//How much time to allot for each pixel moved
var/time_scalar = (1 / ICON_SIZE_ALL) * TILES_PER_SECOND
var/last_x = oldx
var/last_y = oldy
var/time_spent = 0
while(time_spent < duration)
//Get a random pos in our box
var/x_pos = rand(min_x, max_x) + oldx
var/y_pos = rand(min_y, max_y) + oldy
//We take the smaller of our two distances so things still have the propencity to feel somewhat jerky
var/time = round(max(min(abs(last_x - x_pos), abs(last_y - y_pos)) * time_scalar, 1))
if (time_spent == 0)
animate(C, pixel_x=x_pos, pixel_y=y_pos, time=time)
else
animate(pixel_x=x_pos, pixel_y=y_pos, time=time)
last_x = x_pos
last_y = y_pos
//We go based on time spent, so there is a chance we'll overshoot our duration. Don't care
time_spent += time
animate(pixel_x=oldx, pixel_y=oldy, time=3)
#undef TILES_PER_SECOND
///Find if the message has the real name of any user mob in the mob_list
/proc/findname(msg)
if(!istext(msg))
msg = "[msg]"
for(var/i in GLOB.mob_list)
var/mob/M = i
if(M.real_name == msg)
return M
return 0
///Returns a mob's real name between brackets. Useful when you want to display a mob's name alongside their real name
/mob/proc/get_realname_string()
if(real_name && real_name != name)
return " \[[real_name]\]"
return ""
// moved out of admins.dm because things other than admin procs were calling this.
/**
* Returns TRUE if the game has started and we're either an AI with a 0th law, or we're someone with a special role/antag datum
* If allow_fake_antags is set to FALSE, Valentines, ERTs, and any such roles with FLAG_FAKE_ANTAG won't pass.
*/
/proc/is_special_character(mob/M, allow_fake_antags = FALSE)
if(!SSticker.HasRoundStarted())
return FALSE
if(!istype(M))
return FALSE
if(iscyborg(M)) //as a borg you're now beholden to your laws rather than greentext
return FALSE
// Returns TRUE if AI has a zeroth law *and* either has a special role *or* an antag datum.
if(isAI(M))
var/mob/living/silicon/ai/A = M
return (A.laws?.zeroth && (A.mind?.special_role || !isnull(M.mind?.antag_datums)))
if(M.mind?.special_role)
return TRUE
// Turns 'faker' to TRUE if the antag datum is fake. If it's not fake, returns TRUE directly.
var/faker = FALSE
for(var/datum/antagonist/antag_datum as anything in M.mind?.antag_datums)
if((antag_datum.antag_flags & FLAG_FAKE_ANTAG))
faker = TRUE
else
return TRUE
// If 'faker' was assigned TRUE in the above loop and the argument 'allow_fake_antags' is set to TRUE, this passes.
// Else, return FALSE.
return (faker && allow_fake_antags)
/**
* Fancy notifications for ghosts
*
* The kitchen sink of notification procs
*
* Arguments:
* * message: The message displayed in chat.
* * source: The source of the notification. This is required for an icon
* * header: The title text to display on the icon tooltip.
* * alert_overlay: Optional. Create a custom overlay if you want, otherwise it will use the source
* * click_interact: If true, adds a link + clicking the icon will attack_ghost the source
* * custom_link: Optional. If you want to add a custom link to the chat notification
* * ghost_sound: sound to play
* * ignore_key: Ignore keys if they're in the GLOB.poll_ignore list
* * notify_volume: How loud the sound should be to spook the user
*/
/proc/notify_ghosts(
message,
atom/source,
header = "Something Interesting!",
mutable_appearance/alert_overlay,
click_interact = FALSE,
custom_link = "",
ghost_sound,
ignore_key,
notify_flags = NOTIFY_CATEGORY_DEFAULT,
notify_volume = 100,
)
if(notify_flags & GHOST_NOTIFY_IGNORE_MAPLOAD && SSatoms.initialized != INITIALIZATION_INNEW_REGULAR) //don't notify for objects created during a map load
return
if(source)
if(isnull(alert_overlay))
alert_overlay = get_small_overlay(source)
alert_overlay.appearance_flags |= TILE_BOUND
alert_overlay.layer = FLOAT_LAYER
alert_overlay.plane = FLOAT_PLANE
for(var/mob/dead/observer/ghost in GLOB.player_list)
if(!(notify_flags & GHOST_NOTIFY_NOTIFY_SUICIDERS) && HAS_TRAIT(ghost, TRAIT_SUICIDED))
continue
if(ignore_key && (ghost.ckey in GLOB.poll_ignore[ignore_key]))
continue
if(notify_flags & GHOST_NOTIFY_FLASH_WINDOW)
window_flash(ghost.client)
if(ghost_sound)
SEND_SOUND(ghost, sound(ghost_sound, volume = notify_volume))
if(isnull(source))
to_chat(ghost, span_ghostalert(message))
continue
var/interact_link = click_interact ? " <a href='byond://?src=[REF(ghost)];play=[REF(source)]'>(Play)</a>" : ""
var/view_link = " <a href='byond://?src=[REF(ghost)];view=[REF(source)]'>(View)</a>"
to_chat(ghost, span_ghostalert("[message][custom_link][interact_link][view_link]"))
var/atom/movable/screen/alert/notify_action/toast = ghost.throw_alert(
category = "[REF(source)]_notify_action",
type = /atom/movable/screen/alert/notify_action,
)
toast.add_overlay(alert_overlay)
toast.click_interact = click_interact
toast.desc = "Click to [click_interact ? "play" : "view"]."
toast.name = header
toast.target_ref = WEAKREF(source)
///Is the passed in mob a ghost with admin powers, doesn't check for AI interact like isAdminGhost() used to
/proc/isAdminObserver(mob/user)
if(!user) //Are they a mob? Auto interface updates call this with a null src
return
if(!user.client) // Do they have a client?
return
if(!isobserver(user)) // Are they a ghost?
return
if(!check_rights_for(user.client, R_ADMIN)) // Are they allowed?
return
return TRUE
///Returns TRUE/FALSE on whether the mob is an Admin Ghost AI.
///This requires this snowflake check because AI interact gives the access to the mob's client, rather
///than the mob like everyone else, and we keep it that way so they can't accidentally give someone Admin AI access.
/proc/isAdminGhostAI(mob/user)
if(!isAdminObserver(user))
return FALSE
if(!HAS_TRAIT_FROM(user.client, TRAIT_AI_ACCESS, ADMIN_TRAIT)) // Do they have it enabled?
return FALSE
return TRUE
/**
* Offer control of the passed in mob to dead player
*
* Automatic logging and uses poll_candidates_for_mob, how convenient
*/
/proc/offer_control(mob/M)
if(isdead(M))
to_chat(usr, "You can't give ghosts control of a ghost. They're already ghosts.")
return FALSE
to_chat(M, "Control of your mob has been offered to dead players.")
if(usr)
log_admin("[key_name(usr)] has offered control of ([key_name(M)]) to ghosts.")
message_admins("[key_name_admin(usr)] has offered control of ([ADMIN_LOOKUPFLW(M)]) to ghosts")
var/poll_message = "Do you want to play as [span_danger(M.real_name)]?"
if(M.mind)
poll_message = "[poll_message] Job: [span_notice(M.mind.assigned_role.title)]."
if(M.mind.special_role)
poll_message = "[poll_message] Status: [span_boldnotice(M.mind.special_role)]."
else
var/datum/antagonist/A = M.mind.has_antag_datum(/datum/antagonist/)
if(A)
poll_message = "[poll_message] Status: [span_boldnotice(A.name)]."
var/mob/chosen_one = SSpolling.poll_ghosts_for_target(poll_message, check_jobban = ROLE_PAI, poll_time = 10 SECONDS, checked_target = M, alert_pic = M, role_name_text = "ghost control")
if(chosen_one)
to_chat(M, "Your mob has been taken over by a ghost!")
message_admins("[key_name_admin(chosen_one)] has taken control of ([ADMIN_LOOKUPFLW(M)])")
M.ghostize(FALSE)
M.PossessByPlayer(chosen_one.key)
M.client?.init_verbs()
return TRUE
else
to_chat(M, "There were no ghosts willing to take control.")
message_admins("No ghosts were willing to take control of [ADMIN_LOOKUPFLW(M)])")
return FALSE
///Clicks a random nearby mob with the source from this mob
/mob/proc/click_random_mob()
var/list/nearby_mobs = list()
for(var/mob/living/L in range(1, src))
if(L != src)
nearby_mobs |= L
if(nearby_mobs.len)
var/mob/living/T = pick(nearby_mobs)
ClickOn(T)
///Can the mob hear
/mob/proc/can_hear()
return !HAS_TRAIT(src, TRAIT_DEAF)
/**
* Get the list of keywords for policy config
*
* This gets the type, mind assigned roles and antag datums as a list, these are later used
* to send the user relevant headadmin policy config
*/
/mob/proc/get_policy_keywords()
. = list()
. += "[type]"
if(mind)
if(mind.assigned_role.policy_index)
. += mind.assigned_role.policy_index
. += mind.assigned_role.title //A bit redunant, but both title and policy index are used
. += mind.special_role //In case there's something special leftover, try to avoid
for(var/datum/antagonist/antag_datum as anything in mind.antag_datums)
. += "[antag_datum.type]"
///Can the mob see reagents inside of containers?
/mob/proc/can_see_reagents()
return stat == DEAD || HAS_TRAIT(src, TRAIT_REAGENT_SCANNER) //Dead guys and silicons can always see reagents
///Can this mob hold items
/mob/proc/can_hold_items(obj/item/I)
return length(held_items)
/// Returns this mob's default lighting alpha
/mob/proc/default_lighting_cutoff()
if(client?.combo_hud_enabled && (client?.prefs?.toggles & COMBOHUD_LIGHTING))
return LIGHTING_CUTOFF_FULLBRIGHT
return initial(lighting_cutoff)
/// Returns a generic path of the object based on the slot
/proc/get_path_by_slot(slot_id)
switch(slot_id)
if(ITEM_SLOT_BACK)
return /obj/item/storage/backpack
if(ITEM_SLOT_MASK)
return /obj/item/clothing/mask
if(ITEM_SLOT_NECK)
return /obj/item/clothing/neck
if(ITEM_SLOT_HANDCUFFED)
return /obj/item/restraints/handcuffs
if(ITEM_SLOT_LEGCUFFED)
return /obj/item/restraints/legcuffs
if(ITEM_SLOT_BELT)
return /obj/item/storage/belt
if(ITEM_SLOT_ID)
return /obj/item/card/id/advanced
if(ITEM_SLOT_EARS)
return /obj/item/clothing/ears
if(ITEM_SLOT_EYES)
return /obj/item/clothing/glasses
if(ITEM_SLOT_GLOVES)
return /obj/item/clothing/gloves
if(ITEM_SLOT_HEAD)
return /obj/item/clothing/head
if(ITEM_SLOT_FEET)
return /obj/item/clothing/shoes
if(ITEM_SLOT_OCLOTHING)
return /obj/item/clothing/suit
if(ITEM_SLOT_ICLOTHING)
return /obj/item/clothing/under
if(ITEM_SLOT_LPOCKET)
return /obj/item
if(ITEM_SLOT_RPOCKET)
return /obj/item
if(ITEM_SLOT_SUITSTORE)
return /obj/item
return null
/// Returns a client from a mob, mind or client
/proc/get_player_client(player)
if(ismob(player))
var/mob/player_mob = player
player = player_mob.client
else if(istype(player, /datum/mind))
var/datum/mind/player_mind = player
player = player_mind.current.client
if(!istype(player, /client))
return
return player
/proc/health_percentage(mob/living/mob)
var/divided_health = mob.health / mob.maxHealth
if(iscyborg(mob) || islarva(mob))
divided_health = (mob.health + mob.maxHealth) / (mob.maxHealth * 2)
else if(iscarbon(mob) || isAI(mob) || isbrain(mob))
divided_health = abs(HEALTH_THRESHOLD_DEAD - mob.health) / abs(HEALTH_THRESHOLD_DEAD - mob.maxHealth)
return divided_health * 100
/**
* Generates a log message when a user manually changes their targeted zone.
* Only need to one of new_target or old_target, and the other will be auto populated with the current selected zone.
*/
/mob/proc/log_manual_zone_selected_update(source, new_target, old_target)
if(!new_target && !old_target)
CRASH("Called log_manual_zone_selected_update without specifying a new or old target")
old_target ||= zone_selected
new_target ||= zone_selected
if(old_target == new_target)
return
var/list/data = list(
"new_target" = new_target,
"old_target" = old_target,
)
if(mind?.assigned_role)
data["assigned_role"] = mind.assigned_role.title
if(job)
data["assigned_job"] = job
var/atom/handitem = get_active_held_item()
if(handitem)
data["active_item"] = list(
"type" = handitem.type,
"name" = handitem.name,
)
var/atom/offhand = get_inactive_held_item()
if(offhand)
data["offhand_item"] = list(
"type" = offhand.type,
"name" = offhand.name,
)
logger.Log(
LOG_CATEGORY_TARGET_ZONE_SWITCH,
"[key_name(src)] manually changed selected zone",
data,
)
/**
* Returns an associative list of the logs of a certain amount of lines spoken recently by this mob
* copy_amount - number of lines to return
* line_chance - chance to return a line, if you don't want just the most recent x lines
*/
/mob/proc/copy_recent_speech(copy_amount = LING_ABSORB_RECENT_SPEECH, line_chance = 100)
var/list/recent_speech = list()
var/list/say_log = list()
var/log_source = logging
for(var/log_type in log_source)
var/nlog_type = text2num(log_type)
if(nlog_type & LOG_SAY)
var/list/reversed = log_source[log_type]
if(islist(reversed))
say_log = reverse_range(reversed.Copy())
break
for(var/spoken_memory in say_log)
if(recent_speech.len >= copy_amount)
break
if(!prob(line_chance))
continue
recent_speech[spoken_memory] = splittext(say_log[spoken_memory], "\"", 1, 0, TRUE)[3]
var/list/raw_lines = list()
for (var/key as anything in recent_speech)
raw_lines += recent_speech[key]
return raw_lines
/// Takes in an associated list (key `/datum/action` typepaths, value is the AI blackboard key) and handles granting the action and adding it to the mob's AI controller blackboard.
/// This is only useful in instances where you don't want to store the reference to the action on a variable on the mob.
/// You can set the value to null if you don't want to add it to the blackboard (like in player controlled instances). Is also safe with null AI controllers.
/// Assumes that the action will be initialized and held in the mob itself, which is typically standard.
/mob/proc/grant_actions_by_list(list/input)
if(length(input) <= 0)
return
for(var/action in input)
var/datum/action/ability = new action(src)
ability.Grant(src)
var/blackboard_key = input[action]
if(isnull(blackboard_key))
continue
ai_controller?.set_blackboard_key(blackboard_key, ability)