Files
SmArtKar 13814fb55b Fixes materials runtiming whenever thrown by something other than a mob (#96107)
## About The Pull Request

Closes #96062

## Changelog
🆑
fix: Fixed materials runtiming whenever thrown by something other than a
mob
/🆑
2026-05-23 17:14:03 +02:00

375 lines
17 KiB
Plaintext

/*! Material datum
Simple datum which is instanced once per type and is used for every object of said material. It has a variety of variables that define behavior. Subtyping from this makes it easier to create your own materials.
*/
/datum/material
abstract_type = /datum/material
/// What the material is referred to as IC.
var/name = "material"
/// A short description of the material. Not used anywhere, yet...
var/desc = "its..stuff."
/// What the material is indexed by in the SSmaterials.materials list. Defaults to the type of the material.
var/id = null
/// Bitflags that influence how SSmaterials handles this material.
var/init_flags = MATERIAL_INIT_MAPLOAD
/// Material behaviors, controls how the material is categorized and in what recipes it can be used
var/mat_flags = NONE
/// List of material property IDs to their values, 0 - 10
var/mat_properties = null
/// Flags for which comsigs we should track/send
/// These exist for performance reasons as to avoid unnecessary work on materials without properties that trigger off these, or doing the same checks for each property
var/track_flags = NONE
// Color values
/// Base color of the material, for items that don't have greyscale configs nor are made of multiple materials. Item isn't changed in color if this is null.
/// This can be a RGB or color matrix, but it cannot be RGBA as alpha is automatically filled in.
var/color = null
/**
* If the color is a color matrix and either the item uses greyscale configs or is made of multiple colored materials. This will be used instead because
* neither greyscale configs nor BlendRGB() support color matrices.
* Also this has to be RRGGBB, six characters, no alpha channel as it's automatically filled in.
*
* Basically, set this if the color is a color matrix (list)
*/
var/greyscale_color = null
/// Base alpha of the material
var/alpha = 255
/// Starlight color of the material
/// This is the color of light it'll emit if its turf is transparent and over space. Defaults to GLOB.starlight_color if not set
var/starlight_color = null
// Trading values
/// This is the amount of value per 1 unit of the material
var/value_per_unit = 0
/// This is the minimum value of the material, used in the stock market for any mat that isn't set to null
var/minimum_value_override = null
/// Is this material traded on the stock market?
var/tradable = FALSE
/// If this material is tradable, what is the base quantity of the material on the stock market?
var/tradable_base_quantity = 0
// Associated item types
/// The type of sheet this material creates.
var/sheet_type = null
/// What type of ore is this material associated with? Used for mining, and not every material has one.
var/obj/item/ore_type = null
/// What type of shard the material will shatter to
var/obj/item/shard_type = null
/// What type of debris the tile will leave behind when shattered.
var/obj/effect/decal/debris_type = null
/// Reagent type(s) of this material. Can be a reagent typepath or a list.
var/list/material_reagent = null
// Misc stats
/// How resistant the material is to rusting when applied to a turf
var/mat_rust_resistance = RUST_RESISTANCE_ORGANIC
/// How likely this mineral is to be found in a boulder during mining.
var/mineral_rarity = MATERIAL_RARITY_COMMON
/// How many points per units of ore does this grant? This is used by ore in the ORM, NOT for boulder machinery due to the automated nature.
var/points_per_unit = 1
/// How many points per unit does this ore grant when processed by a smelter/refinery?
var/points_per_boulder_unit = 1
// Sound/icon stats, not inherited
/// Can be used to override the sound items make, lets add some SLOSHing.
var/item_sound_override = null
/// Can be used to override the stepsound a turf makes. MORE SLOOOSH
var/turf_sound_override = null
/// What texture icon state to overlay
var/texture_layer_icon_state = null
/// A cached icon for the texture filter
var/icon/cached_texture_filter_icon = null
/** Handles initializing the material.
*
* Arguments:
* - _id: The ID the material should use. Overrides the existing ID.
*/
/datum/material/proc/Initialize(_id, ...)
if(_id)
id = _id
else if(isnull(id))
id = type
if(texture_layer_icon_state)
cached_texture_filter_icon = icon('icons/turf/composite.dmi', texture_layer_icon_state)
for (var/prop_id in mat_properties)
var/datum/material_property/property = SSmaterials.properties[prop_id]
property.attach_to(src)
return TRUE
/// This proc is called when the material is added to an object.
/// Can be called even if the material is covered by a slot
/datum/material/proc/on_applied(atom/source, mat_amount, multiplier, from_slot)
SHOULD_CALL_PARENT(TRUE)
SEND_SIGNAL(src, COMSIG_MATERIAL_APPLIED, source, mat_amount, multiplier, from_slot)
if (!(source.material_flags & MATERIAL_EFFECTS) || from_slot)
return
if (track_flags & MATERIAL_TRACK_CONTACT)
var/static/list/turf_interactions = typecacheof(list(
/turf/open,
/obj/structure/platform,
/obj/structure/table,
/obj/structure/rack,
/obj/structure/bed,
/obj/structure/closet/crate,
/obj/structure/reagent_dispensers,
/obj/structure/altar,
))
if (is_type_in_typecache(source, turf_interactions))
source.AddComponent(/datum/component/material_turf_tracking, src)
if (isclosedturf(source))
RegisterSignal(source, COMSIG_LIVING_DISARM_COLLIDE, PROC_REF(on_wall_shove_collide))
if (track_flags & MATERIAL_TRACK_IMPACT)
RegisterSignal(source, COMSIG_MOVABLE_IMPACT, PROC_REF(on_throw_impact))
RegisterSignal(source, COMSIG_MOVABLE_IMPACT_ZONE, PROC_REF(on_throw_impact_living))
RegisterSignal(source, COMSIG_ITEM_ATTACK, PROC_REF(on_item_attack))
RegisterSignal(source, COMSIG_ITEM_ATTACK_ATOM, PROC_REF(on_item_attack))
// Allow recipe crafting for stack items
if(!isstack(source))
RegisterSignal(source, COMSIG_ITEM_ATTACK_SELF, PROC_REF(on_item_attack_self))
RegisterSignal(source, COMSIG_ITEM_ATTACK_ZONE, PROC_REF(on_item_attack_living))
/// This proc is called when the material becomes the one the object is composed of the most
/// Only called when the material isn't assigned to a slot
/datum/material/proc/on_main_applied(atom/source, mat_amount, multiplier)
SHOULD_CALL_PARENT(TRUE)
SEND_SIGNAL(src, COMSIG_MATERIAL_MAIN_APPLIED, source, mat_amount, multiplier)
/// This proc is called when the material is removed from an object.
/datum/material/proc/on_removed(atom/source, mat_amount, material_flags, from_slot)
SHOULD_CALL_PARENT(TRUE)
SEND_SIGNAL(src, COMSIG_MATERIAL_REMOVED, source, mat_amount, material_flags, from_slot)
if (!(source.material_flags & MATERIAL_EFFECTS))
return
if (track_flags & MATERIAL_TRACK_CONTACT)
var/static/list/material_signals = list(
COMSIG_LIVING_DISARM_COLLIDE,
)
UnregisterSignal(source, material_signals)
var/list/tracker_components = source.GetComponents(/datum/component/material_turf_tracking)
if (istype(tracker_components, /datum/component/material_turf_tracking))
qdel(tracker_components)
else
// Delete all trackers linked to ourselves (might be multiple if we've got multiple slots going on)
for (var/datum/component/material_turf_tracking/tracker as anything in tracker_components)
if (tracker.owner_material == src)
qdel(tracker)
if (track_flags & MATERIAL_TRACK_IMPACT)
var/static/list/material_signals = list(
COMSIG_MOVABLE_IMPACT,
COMSIG_MOVABLE_IMPACT_ZONE,
COMSIG_ITEM_ATTACK,
COMSIG_ITEM_ATTACK_ATOM,
COMSIG_ITEM_ATTACK_SELF,
COMSIG_ITEM_ATTACK_ZONE,
)
UnregisterSignal(source, material_signals)
/// This proc is called when the material is no longer the one the object is composed by the most
/datum/material/proc/on_main_removed(atom/source, mat_amount, multiplier)
SHOULD_CALL_PARENT(TRUE)
SEND_SIGNAL(src, COMSIG_MATERIAL_MAIN_REMOVED, source, mat_amount, multiplier)
/datum/material/proc/on_wall_shove_collide(turf/closed/source, mob/living/shover, mob/living/target, shove_flags, obj/item/weapon)
SIGNAL_HANDLER
// If any part is exposed it makes sense it'd impact the wall
var/skin_contact = HEAD|CHEST|GROIN|LEGS|FEET|ARMS|HANDS
for (var/obj/item/worn_item in target.get_equipped_items(INCLUDE_ABSTRACT))
skin_contact &= ~worn_item.body_parts_covered
if (!skin_contact)
break
SEND_SIGNAL(src, COMSIG_MATERIAL_EFFECT_TOUCH, source, target, shover, null, !!skin_contact)
/datum/material/proc/on_item_attack(obj/item/source, mob/living/target, mob/living/user)
SIGNAL_HANDLER
impact_affect_touch(source, user, user)
// Living mobs use a different signal
if (!isliving(target))
impact_affect_target(source, target, user)
/datum/material/proc/on_item_attack_living(obj/item/source, mob/living/target, mob/living/user, def_zone)
SIGNAL_HANDLER
var/skin_contact = body_zone2cover_flags(def_zone)
for (var/obj/item/worn_item in target.get_equipped_items(INCLUDE_ABSTRACT))
skin_contact &= ~worn_item.body_parts_covered
if (!skin_contact)
break
impact_affect_target(source, target, user, def_zone, !!skin_contact)
/datum/material/proc/on_item_attack_self(obj/item/source, mob/living/user)
SIGNAL_HANDLER
impact_affect_touch(source, user, user)
/datum/material/proc/on_throw_impact(obj/item/source, atom/hit_atom, datum/thrownthing/throwing_datum, caught)
SIGNAL_HANDLER
if (caught)
impact_affect_touch(source, hit_atom, astype(throwing_datum.thrower?.resolve(), /mob/living))
else if (!isliving(hit_atom)) // Hit mobs have armor checking
impact_affect_throw_impact(source, hit_atom, astype(throwing_datum.thrower?.resolve(), /mob/living))
/datum/material/proc/on_throw_impact_living(obj/item/source, mob/living/target, def_zone, blocked, datum/thrownthing/throwing_datum)
SIGNAL_HANDLER
var/skin_contact = body_zone2cover_flags(def_zone)
for (var/obj/item/worn_item in target.get_equipped_items(INCLUDE_ABSTRACT))
skin_contact &= ~worn_item.body_parts_covered
if (!skin_contact)
break
impact_affect_throw_impact(source, target, astype(throwing_datum.thrower?.resolve(), /mob/living), def_zone, !!skin_contact)
/datum/material/proc/impact_affect_touch(obj/item/source, mob/living/user, mob/living/initiator)
var/arm_dir = IS_LEFT_INDEX(user.active_hand_index) ? BODY_ZONE_L_ARM : BODY_ZONE_R_ARM
if (!ishuman(user))
SEND_SIGNAL(src, COMSIG_MATERIAL_EFFECT_TOUCH, source, user, initiator, arm_dir, TRUE)
return
var/mob/living/carbon/human/as_human = user
var/obj/item/bodypart/hand = as_human.has_hand_for_held_index(as_human.get_held_index_of_item(source))
if (!hand)
SEND_SIGNAL(src, COMSIG_MATERIAL_EFFECT_TOUCH, source, user, initiator, arm_dir, FALSE)
return
var/list/obj/item/hand_covers = as_human.get_clothing_on_part(hand)
var/hand_covered = FALSE
for (var/obj/item/worn_item in hand_covers)
if (worn_item.body_parts_covered & HANDS)
hand_covered = TRUE
break
SEND_SIGNAL(src, COMSIG_MATERIAL_EFFECT_TOUCH, source, user, initiator, hand.body_zone, !hand_covered)
/datum/material/proc/impact_affect_target(obj/item/source, atom/target, mob/living/user, def_zone, skin_contact = TRUE)
SEND_SIGNAL(src, COMSIG_MATERIAL_EFFECT_HIT, source, target, user, def_zone, skin_contact)
/datum/material/proc/impact_affect_throw_impact(obj/item/source, atom/target, mob/living/user, def_zone, skin_contact = TRUE)
SEND_SIGNAL(src, COMSIG_MATERIAL_EFFECT_THROW_IMPACT, source, target, user, def_zone, skin_contact)
/datum/material/proc/setup_glow(turf/on)
if(GET_TURF_PLANE_OFFSET(on) != GET_LOWEST_STACK_OFFSET(on.z)) // We ain't the bottom brother
return
// We assume no parallax means no space means no light
if(SSmapping.level_trait(on.z, ZTRAIT_NOPARALLAX))
return
if(!starlight_color)
on.RegisterSignal(SSdcs, COMSIG_STARLIGHT_COLOR_CHANGED, TYPE_PROC_REF(/turf, material_starlight_changed))
RegisterSignal(on, COMSIG_QDELETING, PROC_REF(lit_turf_deleted))
on.set_light(2, 1, starlight_color || GLOB.starlight_color, l_height = LIGHTING_HEIGHT_SPACE)
/turf/proc/material_starlight_changed(datum/source, old_star, new_star)
if(light_color == old_star)
set_light_color(new_star)
/datum/material/proc/lit_turf_deleted(turf/source)
source.set_light(0, 0, null)
////Called in `/datum/component/edible/proc/on_material_effects`
/datum/material/proc/on_edible_applied(atom/source, datum/component/edible/edible)
return
////Called in `/datum/component/edible/proc/on_remove_material_effects`
/datum/material/proc/on_edible_removed(atom/source, datum/component/edible/edible)
return
/**
* This proc is called when the mat is found in an item that's consumed by accident. see /obj/item/proc/on_accidental_consumption.
* Arguments
* * M - person consuming the mat
* * S - (optional) item the mat is contained in (NOT the item with the mat itself)
*/
/datum/material/proc/on_accidental_mat_consumption(mob/living/carbon/victim, obj/item/source_item)
SHOULD_CALL_PARENT(TRUE)
if (!material_reagent)
return FALSE
var/effect_multiplier = source_item.custom_materials[type] / SHEET_MATERIAL_AMOUNT
if (!islist(material_reagent))
victim.reagents?.add_reagent(material_reagent, rand(6, 8) * effect_multiplier)
source_item?.reagents?.add_reagent(material_reagent, source_item.reagents.total_volume * MATERIAL_REAGENT_CONSUMPTION_MULT * effect_multiplier)
return TRUE
for (var/datum/reagent/reagent_type as anything in material_reagent)
var/amount_mult = material_reagent[reagent_type] / length(material_reagent)
victim.reagents?.add_reagent(material_reagent, rand(6, 8) * effect_multiplier * amount_mult)
source_item?.reagents?.add_reagent(material_reagent, source_item.reagents.total_volume * MATERIAL_REAGENT_CONSUMPTION_MULT * effect_multiplier * amount_mult)
return TRUE
/** Returns the composition of this material.
*
* Mostly used for alloys when breaking down materials.
*
* Arguments:
* - amount: The amount of the material to break down.
*/
/datum/material/proc/return_composition(amount = 1, flags)
// Yes we need the parenthesis, without them BYOND stringifies src into "src" and things break.
return list((src) = amount)
///Returns the list of armor modifiers, with each element having its assoc value multiplied by the multiplier arg
/datum/material/proc/get_armor_modifiers(multiplier)
SHOULD_NOT_OVERRIDE(TRUE)
var/density = get_property(MATERIAL_DENSITY)
var/hardness = get_property(MATERIAL_HARDNESS)
var/flexibility = get_property(MATERIAL_FLEXIBILITY)
var/reflectivity = get_property(MATERIAL_REFLECTIVITY)
var/electric = get_property(MATERIAL_ELECTRICAL)
var/thermal = get_property(MATERIAL_THERMAL)
var/chemical = get_property(MATERIAL_CHEMICAL)
var/flammability = get_property(MATERIAL_FLAMMABILITY) // Optional, might be not present
// Welcome to hell
var/list/armor_modifiers = list(
// Based on density, with a bonus/malus for matching flexibility
// We cap divergence at 4 (reduced by hardness above 6 for REALLY dense stuff) to make it not extremely punishing on light fabrics or heavy materials
// Iron at a baseline of density of 6 and flexibility of 4 has divergence of 4, so (1 + 0.2) / (0.8 + 0.4) = 1
MELEE = (1 + (density - 4) * 0.1) / (0.8 + min(4 - max(0, hardness - 6), abs(flexibility - density)) * 0.1),
// Hardness and density, with flexibility actually being detrimental
BULLET = (1 + (density - 4) * 0.025 + (hardness - 4) * 0.075) / (1 - max(0, flexibility - 2) * 0.1),
// 0.6 ~ 1 for reflectivity below 4, 1 ~ 1.4 for reflectivity above 6, reduced for transparent materials
LASER = 1 + MATERIAL_PROPERTY_DIVERGENCE(reflectivity, 4, 6) * 0.1 - (255 - alpha) / 50 * 0.2,
// Essentially laser but with contribution split between reflectivity and inverse electric conductivity
// Here reflectivity applies if its below 4 or above 8, and conductivity if its below 4 or above 6
ENERGY = 1 + MATERIAL_PROPERTY_DIVERGENCE(reflectivity, 4, 8) * 0.05 - MATERIAL_PROPERTY_DIVERGENCE(electric, 4, 6) * 0.1,
// Linearly scales from 0.2 to 1.8 with density
BOMB = 1 + (density - 4) * 0.2,
// Each level of flammability reduces FIRE armor by 20%, with thermal conductivity reducing it by further 20% for each level above 6 and increasing for each level below 2
FIRE = max(0, 1 - max(0, (thermal - 6) * 0.2) + max(0, (2 - thermal) * 0.2) - flammability * 0.2),
// Linearly scales from 0.2 to 1.8 with chemical resistance
ACID = 1 + (chemical - 4) * 0.2,
)
for (var/armor_key in armor_modifiers)
// Safety check to ensure that we don't have inverted armor values
if (armor_modifiers[armor_key] < 0)
armor_modifiers[armor_key] = 0
armor_modifiers[armor_key] = GET_MATERIAL_MODIFIER(armor_modifiers[armor_key], multiplier)
return armor_modifiers
/datum/material/proc/get_property(prop_id)
if (!isnull(mat_properties?[prop_id]))
return mat_properties[prop_id]
var/datum/material_property/derived/derived_prop = SSmaterials.properties[prop_id]
if (!istype(derived_prop))
return null // Property was not specified on the material and wasn't a derived one
return derived_prop.get_value(src)