Files
Leland Kemble 9ebd7c3cf1 fixes mouse stored inside holder when killed (#92232)
## About The Pull Request

Makes mice go where they were when you kill them, rather than being
stored inside whatever mob was holding them. Mice on your shoulder fall
off, instead.
Also fixes unrelated runtime happening in parallel

## Why It's Good For The Game

fixes  #91057

## Changelog
🆑

fix: mice stored on your person will no longer disappear when killed
/🆑

---------

Co-authored-by: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com>
2025-09-10 19:47:11 -04:00

489 lines
18 KiB
Plaintext

/mob/living/basic/mouse
name = "mouse"
desc = "This cute little guy just loves the taste of insulated electrical cables. Isn't he adorable?"
icon_state = "mouse_gray"
icon_living = "mouse_gray"
icon_dead = "mouse_gray_dead"
held_state = "mouse_gray"
maxHealth = 5
health = 5
density = FALSE
pass_flags = PASSTABLE|PASSGRILLE|PASSMOB
mob_size = MOB_SIZE_TINY
can_be_held = TRUE
held_w_class = WEIGHT_CLASS_TINY
mob_biotypes = MOB_ORGANIC|MOB_BEAST
gold_core_spawnable = FRIENDLY_SPAWN
faction = list(FACTION_RAT, FACTION_MAINT_CREATURES)
butcher_results = list(/obj/item/food/meat/slab/mouse = 1)
speak_emote = list("squeaks")
response_help_continuous = "pets"
response_help_simple = "pet"
response_disarm_continuous = "gently pushes aside"
response_disarm_simple = "gently push aside"
response_harm_continuous = "splats"
response_harm_simple = "splat"
ai_controller = /datum/ai_controller/basic_controller/mouse
/// Whether this rat is friendly to players
var/tame = FALSE
/// What color our mouse is. Brown, gray and white - leave blank for random.
var/body_color
/// Does this mouse contribute to the ratcap?
var/contributes_to_ratcap = TRUE
/// Probability that, if we successfully bite a shocked cable, that we will die to it.
var/cable_zap_prob = 85
///list of pet commands we follow
var/static/list/pet_commands = list(
/datum/pet_command/idle,
/datum/pet_command/free,
/datum/pet_command/follow/start_active,
/datum/pet_command/perform_trick_sequence,
)
/datum/emote/mouse
mob_type_allowed_typecache = /mob/living/basic/mouse
mob_type_blacklist_typecache = list()
/datum/emote/mouse/squeak
key = "squeak"
key_third_person = "squeaks"
message = "squeak!"
emote_type = EMOTE_VISIBLE | EMOTE_AUDIBLE
vary = TRUE
sound = 'sound/mobs/non-humanoids/mouse/mousesqueek.ogg'
/mob/living/basic/mouse/Initialize(mapload, tame = FALSE, new_body_color)
. = ..()
if(contributes_to_ratcap)
SSmobs.cheeserats |= src
ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
src.tame = tame
if(!isnull(new_body_color))
body_color = new_body_color
if(isnull(body_color))
body_color = pick("brown", "gray", "white")
held_state = "mouse_[body_color]" // not handled by variety element
AddElement(/datum/element/animal_variety, "mouse", body_color, FALSE)
AddElement(/datum/element/swabable, CELL_LINE_TABLE_MOUSE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 10)
AddComponent(/datum/component/squeak, list('sound/mobs/non-humanoids/mouse/mousesqueek.ogg' = 1), 100, extrarange = SHORT_RANGE_SOUND_EXTRARANGE) //as quiet as a mouse or whatever
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
)
AddComponent(/datum/component/obeys_commands, pet_commands)
AddElement(/datum/element/connect_loc, loc_connections)
make_tameable()
AddComponent(/datum/component/swarming, 16, 16) //max_x, max_y
/mob/living/basic/mouse/proc/make_tameable()
if (tame)
faction |= FACTION_NEUTRAL
else
var/static/list/food_types = list(/obj/item/food/cheese)
AddComponent(/datum/component/tameable, food_types = food_types, tame_chance = 100)
AddElement(/datum/element/regal_rat_minion, converted_path = /mob/living/basic/mouse/rat, pet_commands = GLOB.regal_rat_minion_commands)
/mob/living/basic/mouse/Destroy()
SSmobs.cheeserats -= src
return ..()
/mob/living/basic/mouse/examine(mob/user)
. = ..()
var/sameside = user.faction_check_atom(src, exact_match = TRUE)
if(isregalrat(user))
if(sameside)
. += span_notice("This rat serves under you.")
else
. += span_warning("This peasant serves a different king! Strike [p_them()] down!")
else if(user != src && ismouse(user))
if(sameside)
. += span_notice("You both serve the same king.")
else
. += span_warning("This fool serves a different king!")
/// Kills the rat and changes its icon state to be splatted (bloody).
/mob/living/basic/mouse/proc/splat()
icon_dead = "mouse_[body_color]_splat"
adjust_health(maxHealth)
// On revival, re-add the mouse to the ratcap, or block it if we're at it
/mob/living/basic/mouse/revive(full_heal_flags = NONE, excess_healing = 0, force_grab_ghost = FALSE)
if(!contributes_to_ratcap)
return ..()
var/aheal_included = full_heal_flags & HEAL_ADMIN
var/cap = CONFIG_GET(number/ratcap)
if(!aheal_included && !ckey && length(SSmobs.cheeserats) >= cap)
visible_message(span_warning("[src] twitches, but does not continue moving \
due to the overwhelming rodent population on the station!"))
return
. = ..()
if(stat != DEAD)
SSmobs.cheeserats |= src
// On death, remove the mouse from the ratcap, and turn it into an item if applicable
/mob/living/basic/mouse/death(gibbed)
SSmobs.cheeserats -= src
// Rats with a mind will not turn into a lizard snack on death
if(mind)
return ..()
// Call parent with gibbed = TRUE, becuase we're getting rid of the body
. = ..(TRUE)
// Now if we were't ACTUALLY gibbed, spawn the dead mouse
if(!gibbed)
var/make_a_corpse = TRUE
var/place_to_make_corpse = loc
var/must_equip = FALSE
var/equip_slot
var/mob/holding_mob
var/obj/item/mob_holder/found_holder
if(istype(loc, /obj/item/mob_holder))//If our mouse is dying in place holder we want to put the dead mouse where the place holder was
found_holder = loc
place_to_make_corpse = found_holder.loc
if(istype(found_holder.loc,/mob/living/carbon))
holding_mob = found_holder.loc
place_to_make_corpse = get_turf(holding_mob)
equip_slot = holding_mob.get_slot_by_item(found_holder)
if(equip_slot == ITEM_SLOT_HANDS || equip_slot == ITEM_SLOT_RPOCKET || equip_slot == ITEM_SLOT_LPOCKET)
must_equip = TRUE
if(istype(found_holder.loc, /obj/machinery/microwave))//Microwaves gib things that die when cooked, so we don't need to make a dead body too
make_a_corpse = FALSE
if(make_a_corpse)
var/obj/item/food/deadmouse/mouse = new(place_to_make_corpse)
mouse.copy_corpse(src)
if(HAS_TRAIT(src, TRAIT_BEING_SHOCKED))
mouse.desc = "They're toast."
mouse.add_atom_colour("#3A3A3A", FIXED_COLOUR_PRIORITY)
found_holder?.release(FALSE)
if(must_equip)
if(equip_slot == ITEM_SLOT_HANDS)
holding_mob.dropItemToGround(found_holder)
holding_mob.equip_to_slot(mouse,equip_slot)
qdel(src)
/mob/living/basic/mouse/UnarmedAttack(atom/attack_target, proximity_flag, list/modifiers)
. = ..()
if(!.)
return
if(!proximity_flag)
return
if(istype(attack_target, /obj/item/food/cheese))
try_consume_cheese(attack_target)
return TRUE
if(istype(attack_target, /obj/structure/cable))
try_bite_cable(attack_target)
return TRUE
/// Signal proc for [COMSIG_ATOM_ENTERED]. Sends a lil' squeak to chat when someone walks over us.
/mob/living/basic/mouse/proc/on_entered(datum/source, atom/movable/entered)
SIGNAL_HANDLER
if(ishuman(entered) && stat == CONSCIOUS)
to_chat(entered, span_notice("[icon2html(src, entered)] Squeak!"))
/// Called when a mouse is hand-fed some cheese, it will stop being afraid of humans
/mob/living/basic/mouse/tamed(mob/living/tamer, obj/item/food/cheese/cheese)
new /obj/effect/temp_visual/heart(loc)
faction |= FACTION_NEUTRAL
tame = TRUE
try_consume_cheese(cheese)
ai_controller.CancelActions() // Interrupt any current fleeing
/// Attempts to consume a piece of cheese, causing a few effects.
/mob/living/basic/mouse/proc/try_consume_cheese(obj/item/food/cheese/cheese)
// Royal cheese will evolve us into a regal rat
if(istype(cheese, /obj/item/food/cheese/royal))
visible_message(
span_warning("[src] devours [cheese]! They morph into something... greater!"),
span_notice("You devour [cheese], and start morphing into something... greater!"),
)
evolve_into_regal_rat()
qdel(cheese)
return
var/cap = CONFIG_GET(number/ratcap)
// Normal cheese will either heal us
if(prob(90) || health < maxHealth)
visible_message(
span_notice("[src] nibbles [cheese]."),
span_notice("You nibble [cheese][health < maxHealth ? ", restoring your health" : ""].")
)
adjust_health(-maxHealth)
// Or, if we're at full health, there's a 10% chance that normal cheese will spawn a new mouse
// ...if the rat cap allows us, that is
else if(length(SSmobs.cheeserats) >= cap)
visible_message(
span_warning("[src] carefully eats [cheese], hiding it from the [cap] mice on the station!"),
span_notice("You carefully nibble [cheese], hiding it from the [cap] other mice on board the station.")
)
else
visible_message(
span_notice("[src] nibbles through [cheese], attracting another mouse!"),
span_notice("You nibble through [cheese], attracting another mouse!")
)
create_a_new_rat()
qdel(cheese)
/// Evolves this rat into a regal rat
/mob/living/basic/mouse/proc/evolve_into_regal_rat()
var/mob/living/basic/regal_rat/controlled/regalrat = new(loc)
mind?.transfer_to(regalrat)
INVOKE_ASYNC(regalrat, TYPE_PROC_REF(/atom/movable, say), "RISE, MY SUBJECTS! SCREEEEEEE!")
qdel(src)
/// Creates a new mouse based on this mouse's subtype.
/mob/living/basic/mouse/proc/create_a_new_rat()
new /mob/living/basic/mouse(loc, /* tame = */ tame)
/// Biting into a cable will cause a mouse to get shocked and die if applicable. Or do nothing if they're lucky.
/mob/living/basic/mouse/proc/try_bite_cable(obj/structure/cable/cable)
if(cable.avail() && !HAS_TRAIT(src, TRAIT_SHOCKIMMUNE) && prob(cable_zap_prob))
visible_message(
span_warning("[src] chews through \the [cable]. It's toast!"),
span_userdanger("As you bite deeply into [cable], you suddenly realize this may have been a bad idea."),
span_hear("You hear electricity crack."),
)
// Finely toasted
ADD_TRAIT(src, TRAIT_BEING_SHOCKED, TRAIT_GENERIC)
// Unfortunately we can't check the return value of electrocute_act before displaying a message,
// as it's possible the damage from electrocution results in our hunter being deleted.
// But what are the odds of the shock failing? Hahaha...
electrocute_act(maxHealth * 2, cable, flags = SHOCK_SUPPRESS_MESSAGE)
else
visible_message(
span_warning("[src] chews through \the [cable]."),
span_notice("You chew through \the [cable]."),
)
playsound(cable, 'sound/effects/sparks/sparks2.ogg', 100, TRUE)
cable.deconstruct()
/mob/living/basic/mouse/white
body_color = "white"
icon_state = "mouse_white"
held_state = "mouse_white"
/mob/living/basic/mouse/gray
body_color = "gray"
icon_state = "mouse_gray"
/mob/living/basic/mouse/brown
body_color = "brown"
icon_state = "mouse_brown"
held_state = "mouse_brown"
//TOM IS ALIVE! SQUEEEEEEEE~K :)
/mob/living/basic/mouse/brown/tom
name = "Tom"
desc = "Jerry the cat is not amused."
response_help_continuous = "pets"
response_help_simple = "pet"
response_disarm_continuous = "gently pushes aside"
response_disarm_simple = "gently push aside"
response_harm_continuous = "splats"
response_harm_simple = "splat"
gold_core_spawnable = NO_SPAWN
contributes_to_ratcap = FALSE
/mob/living/basic/mouse/brown/tom/make_tameable()
tame = TRUE
return ..()
/mob/living/basic/mouse/brown/tom/Initialize(mapload)
. = ..()
// Tom fears no cable.
ADD_TRAIT(src, TRAIT_SHOCKIMMUNE, INNATE_TRAIT)
AddElement(/datum/element/pet_bonus, "squeak")
/mob/living/basic/mouse/brown/tom/create_a_new_rat()
new /mob/living/basic/mouse/brown(loc, /* tame = */ tame) // dominant gene
/mob/living/basic/mouse/rat
name = "rat"
desc = "They're a nasty, ugly, evil, disease-ridden rodent with anger issues."
gold_core_spawnable = HOSTILE_SPAWN
melee_damage_lower = 3
melee_damage_upper = 5
obj_damage = 5
maxHealth = 15
health = 15
ai_controller = /datum/ai_controller/basic_controller/mouse/rat
/mob/living/basic/mouse/rat/make_tameable()
return // Unlike in real life, space rats are horrible creatures who don't like you
/mob/living/basic/mouse/rat/create_a_new_rat()
new /mob/living/basic/mouse/rat(loc)
/// Mice turn into food when they die
/obj/item/food/deadmouse
name = "dead mouse"
desc = "They look like somebody dropped the bass on it. A lizard's favorite meal."
icon = 'icons/mob/simple/animal.dmi'
icon_state = "mouse_gray_dead"
bite_consumption = 3
eatverbs = list("devour")
food_reagents = list(/datum/reagent/consumable/nutriment = 3, /datum/reagent/consumable/nutriment/vitamin = 2)
foodtypes = GORE | MEAT | RAW
grind_results = list(/datum/reagent/blood = 20, /datum/reagent/consumable/liquidgibs = 5)
decomp_req_handle = TRUE
ant_attracting = FALSE
decomp_type = /obj/item/food/deadmouse/moldy
var/body_color = "gray"
var/critter_type = /mob/living/basic/mouse
/obj/item/food/deadmouse/Initialize(mapload)
. = ..()
AddElement(/datum/element/swabable, CELL_LINE_TABLE_MOUSE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 10)
RegisterSignal(src, COMSIG_ATOM_ON_LAZARUS_INJECTOR, PROC_REF(use_lazarus))
/// Copy properties from an imminently dead mouse
/obj/item/food/deadmouse/proc/copy_corpse(mob/living/basic/mouse/dead_critter)
body_color = dead_critter.body_color
critter_type = dead_critter.type
name = dead_critter.name
icon_state = dead_critter.icon_dead
/obj/item/food/deadmouse/examine(mob/user)
. = ..()
if (reagents?.has_reagent(/datum/reagent/yuck) || reagents?.has_reagent(/datum/reagent/fuel))
. += span_warning("[p_Theyre()] dripping with fuel and smells terrible.")
///Spawn a new mouse from this dead mouse item when hit by a lazarus injector and conditions are met.
/obj/item/food/deadmouse/proc/use_lazarus(datum/source, obj/item/lazarus_injector/injector, mob/user)
SIGNAL_HANDLER
if(injector.revive_type != SENTIENCE_ORGANIC)
balloon_alert(user, "invalid creature!")
return
var/mob/living/basic/mouse/revived_critter = new critter_type (drop_location(), FALSE, body_color)
revived_critter.name = name
revived_critter.lazarus_revive(user, injector.malfunctioning)
injector.expend(revived_critter, user)
qdel(src)
return LAZARUS_INJECTOR_USED
/obj/item/food/deadmouse/attackby(obj/item/attacking_item, mob/user, list/modifiers, list/attack_modifiers)
var/mob/living/living_user = user
if(istype(living_user) && attacking_item.get_sharpness() && living_user.combat_mode)
if(!isturf(loc))
balloon_alert(user, "can't butcher here!")
return
balloon_alert(user, "butchering...")
if(!do_after(user, 0.75 SECONDS, src))
balloon_alert(user, "interrupted!")
return
loc.balloon_alert(user, "butchered")
new /obj/item/food/meat/slab/mouse(loc)
qdel(src)
return
return ..()
/obj/item/food/deadmouse/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
if(isnull(reagents) || !interacting_with.is_open_container())
return NONE
// is_open_container will not return truthy if target.reagents doesn't exist
var/datum/reagents/target_reagents = interacting_with.reagents
var/trans_amount = reagents.maximum_volume - reagents.total_volume * (4 / 3)
if(target_reagents.has_reagent(/datum/reagent/fuel) && target_reagents.trans_to(src, trans_amount))
to_chat(user, span_notice("You dip [src] into [interacting_with]."))
return ITEM_INTERACT_SUCCESS
/obj/item/food/deadmouse/moldy
name = "moldy dead mouse"
desc = "A dead rodent, consumed by mold and rot. There is a slim chance that a lizard might still eat it."
icon_state = "mouse_gray_dead"
food_reagents = list(/datum/reagent/consumable/nutriment = 3, /datum/reagent/consumable/nutriment/vitamin = 2, /datum/reagent/consumable/mold = 10)
foodtypes = GORE | MEAT | RAW | GROSS
grind_results = list(/datum/reagent/blood = 20, /datum/reagent/consumable/liquidgibs = 5, /datum/reagent/consumable/mold = 10)
preserved_food = TRUE
/// The mouse AI controller
/datum/ai_controller/basic_controller/mouse
blackboard = list( // Always cowardly
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, // Use this to find people to run away from
BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends,
BB_BASIC_MOB_FLEE_DISTANCE = 3,
BB_SONG_LINES = MOUSE_SONG,
)
ai_traits = PASSIVE_AI_FLAGS
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
// Try to speak, because it's cute
/datum/ai_planning_subtree/random_speech/mouse,
// Follow the boss's orders
/datum/ai_planning_subtree/pet_planning,
// Look for and execute hunts for cheese even if someone is looking at us
/datum/ai_planning_subtree/find_and_hunt_target/look_for_cheese,
// Next priority is to try and appreoach a keyboard
/datum/ai_planning_subtree/approach_synthesizer,
// And play it if we are near it
/datum/ai_planning_subtree/generic_play_instrument/end_planning,
// Next priority is see if anyone is looking at us
/datum/ai_planning_subtree/simple_find_nearest_target_to_flee,
// Skedaddle
/datum/ai_planning_subtree/flee_target/mouse,
// Otherwise, look for and execute hunts for cabling
/datum/ai_planning_subtree/find_and_hunt_target/look_for_cables,
)
/// Don't look for anything to run away from if you are distracted by being adjacent to cheese
/datum/ai_planning_subtree/flee_target/mouse
/datum/ai_planning_subtree/flee_target/mouse/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/atom/hunted_cheese = controller.blackboard[BB_CURRENT_HUNTING_TARGET]
if (!isnull(hunted_cheese))
return // We see some cheese, which is more important than our life
return ..()
/// AI controller for rats, slightly more complex than mice becuase they attack people
/datum/ai_controller/basic_controller/mouse/rat
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends,
BB_BASIC_MOB_CURRENT_TARGET = null, // heathen
BB_CURRENT_HUNTING_TARGET = null, // cheese
BB_LOW_PRIORITY_HUNTING_TARGET = null, // cable
BB_OWNER_SELF_HARM_RESPONSES = list(
"*me cleans its whiskers in disapproval.",
"*me squeaks sadly.",
"*me sheds a single small tear."
)
)
ai_traits = DEFAULT_AI_FLAGS | STOP_MOVING_WHEN_PULLED
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/pet_planning,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/find_and_hunt_target/look_for_cheese,
/datum/ai_planning_subtree/random_speech/mouse,
/datum/ai_planning_subtree/find_and_hunt_target/look_for_cables,
)