From d68efbbf458a95a14232bdc0c16e4dafdf703943 Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:58:37 +0200 Subject: [PATCH] Fixes transforming items getting their force incorrectly modified when metalgenned multiple times (#95739) ## About The Pull Request Transforming weapons could change their sharpness which would break material force modification as materials modify the force differently based on item sharpness. Also the force was hard reset every time you switched it on or off, which would also break the math. I've rewritten the transforming component to preserve force and sharpness, and account for different sharpness of the item when switched on/off. Additionally, I've updated the two-handed component to use the new system to make it easier to work with/less prone to breaking. ## Why It's Good For The Game image ## Changelog :cl: fix: Transforming items like eswords should no longer get absurd stats when metalgenned multiple times in a row /:cl: --- .../signals/signals_atom/signals_atom_main.dm | 4 + code/__DEFINES/dcs/signals/signals_object.dm | 1 + code/datums/components/transforming.dm | 89 ++++++++++++--- code/datums/components/twohanded.dm | 23 +++- code/game/atom/atom_materials.dm | 2 + code/game/objects/items.dm | 107 +++++++++++------- code/game/objects/items/sharpener.dm | 3 +- 7 files changed, 166 insertions(+), 63 deletions(-) diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm index b6cc94584be..6e70a9b36b9 100644 --- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm +++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm @@ -151,6 +151,10 @@ #define COMSIG_ATOM_FINALIZE_MATERIAL_EFFECTS "atom_finalize_material_effects" /// From /atom/finalize_remove_material_effects(): (list/materials, datum/material/main_material) #define COMSIG_ATOM_FINALIZE_REMOVE_MATERIAL_EFFECTS "atom_finalize_remove_material_effects" +/// /atom/proc/apply_single_mat_effect(): (datum/material/material, amount, multiplier) +#define COMSIG_ATOM_SINGLE_MATERIAL_EFFECT_APPLY "atom_single_material_effect_apply" +/// /atom/proc/remove_single_mat_effect(): (datum/material/material, amount, multiplier) +#define COMSIG_ATOM_SINGLE_MATERIAL_EFFECT_REMOVE "atom_single_material_effect_remove" /// From /atom/proc/update_atom_colour() : (color_changed) #define COMSIG_ATOM_COLOR_UPDATED "atom_color_updated" diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index f5e8f847a7c..3081b60449f 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -195,6 +195,7 @@ #define COMPONENT_BLOCK_SHARPEN_BLOCKED (1<<1) #define COMPONENT_BLOCK_SHARPEN_ALREADY (1<<2) #define COMPONENT_BLOCK_SHARPEN_MAXED (1<<3) + #define COMPONENT_BLOCK_SHARPEN_SHARPNESS (1<<4) ///Called when an armor plate is successfully applied to an object #define COMSIG_ARMOR_PLATED "armor_plated" diff --git a/code/datums/components/transforming.dm b/code/datums/components/transforming.dm index 81913616bd8..6c51d0d102d 100644 --- a/code/datums/components/transforming.dm +++ b/code/datums/components/transforming.dm @@ -24,6 +24,16 @@ var/sharpness_on /// Hitsound played when active var/hitsound_on + /// Force when the weapon is inactive + var/force_off + /// Throwforce when the weapon is inactive + var/throwforce_off + /// Throw speed of the weapon when inactive + var/throw_speed_off + /// Weight class of the weapon when inactive + var/w_class_off + /// The sharpness of the weapon when inactive + var/sharpness_off /// List of the original continuous attack verbs the item has. var/list/attack_verb_continuous_off /// List of the original simple attack verbs the item has. @@ -75,6 +85,12 @@ src.clumsy_damage = clumsy_damage src.inhand_icon_change = inhand_icon_change + src.force_off = item_parent.force + src.throwforce_off = item_parent.throwforce + src.throw_speed_off = item_parent.throw_speed + src.sharpness_off = item_parent.sharpness // Raw value, not via the getter + src.w_class_off = item_parent.w_class + if(attack_verb_continuous_on) src.attack_verb_continuous_on = attack_verb_continuous_on attack_verb_continuous_off = item_parent.attack_verb_continuous @@ -95,6 +111,10 @@ RegisterSignal(parent, COMSIG_DETECTIVE_SCANNED, PROC_REF(on_scan)) RegisterSignal(parent, COMSIG_ITEM_APPLY_FANTASY_BONUSES, PROC_REF(apply_fantasy_bonuses)) RegisterSignal(parent, COMSIG_ITEM_REMOVE_FANTASY_BONUSES, PROC_REF(remove_fantasy_bonuses)) + RegisterSignal(parent, COMSIG_ATOM_FINALIZE_MATERIAL_EFFECTS, PROC_REF(on_materials_updated)) + RegisterSignal(parent, COMSIG_ATOM_FINALIZE_REMOVE_MATERIAL_EFFECTS, PROC_REF(on_materials_updated)) + RegisterSignal(parent, COMSIG_ATOM_SINGLE_MATERIAL_EFFECT_APPLY, PROC_REF(on_material_apply)) + RegisterSignal(parent, COMSIG_ATOM_SINGLE_MATERIAL_EFFECT_REMOVE, PROC_REF(on_material_remove)) /datum/component/transforming/proc/apply_fantasy_bonuses(obj/item/source, bonus) SIGNAL_HANDLER @@ -110,9 +130,38 @@ force_on = source.reset_fantasy_variable("force_on", force_on) throwforce_on = source.reset_fantasy_variable("throwforce_on", throwforce_on) +/datum/component/transforming/proc/on_material_apply(obj/item/source, datum/material/material, amount, multiplier) + SIGNAL_HANDLER + // Opposite state's force needs to be calculated for each material's effect + if (active) + force_off *= GET_MATERIAL_MODIFIER(source.get_material_force_modifier(material, initial(source.sharpness)), multiplier) + throwforce_off *= GET_MATERIAL_MODIFIER(source.get_material_throwforce_modifier(material, initial(source.sharpness)), multiplier) + else + force_on *= GET_MATERIAL_MODIFIER(source.get_material_force_modifier(material, sharpness_on), multiplier) + throwforce_on *= GET_MATERIAL_MODIFIER(source.get_material_throwforce_modifier(material, sharpness_on), multiplier) + +/datum/component/transforming/proc/on_material_remove(obj/item/source, datum/material/material, amount, multiplier) + SIGNAL_HANDLER + // Same as appliation but inversed + if (active) + force_off /= GET_MATERIAL_MODIFIER(source.get_material_force_modifier(material, initial(source.sharpness)), multiplier) + throwforce_off /= GET_MATERIAL_MODIFIER(source.get_material_throwforce_modifier(material, initial(source.sharpness)), multiplier) + else + force_on /= GET_MATERIAL_MODIFIER(source.get_material_force_modifier(material, sharpness_on), multiplier) + throwforce_on /= GET_MATERIAL_MODIFIER(source.get_material_throwforce_modifier(material, sharpness_on), multiplier) + +/datum/component/transforming/proc/on_materials_updated(obj/item/source, list/materials, datum/material/main_material) + SIGNAL_HANDLER + // Current force can be set directly + if (active) + force_on = source.force + throwforce_on = source.throwforce + else + force_off = source.force + throwforce_off = source.throwforce /datum/component/transforming/UnregisterFromParent() - UnregisterSignal(parent, list(COMSIG_ITEM_ATTACK_SELF, COMSIG_ITEM_SHARPEN_ACT, COMSIG_DETECTIVE_SCANNED)) + UnregisterSignal(parent, list(COMSIG_ITEM_ATTACK_SELF, COMSIG_ITEM_SHARPEN_ACT, COMSIG_DETECTIVE_SCANNED, COMSIG_ATOM_FINALIZE_MATERIAL_EFFECTS, COMSIG_ATOM_FINALIZE_REMOVE_MATERIAL_EFFECTS)) /datum/component/transforming/proc/on_scan(datum/source, mob/user, datum/detective_scanner_log/entry) SIGNAL_HANDLER @@ -198,13 +247,13 @@ */ /datum/component/transforming/proc/set_active(obj/item/source) ADD_TRAIT(source, TRAIT_TRANSFORM_ACTIVE, REF(src)) - if(sharpness_on) + if(!isnull(sharpness_on)) source.sharpness = sharpness_on - if(force_on) - source.force = force_on + (source.sharpness ? sharpened_bonus : 0) - if(throwforce_on) - source.throwforce = throwforce_on + (source.sharpness ? sharpened_bonus : 0) - if(throw_speed_on) + if(!isnull(force_on)) + source.force = force_on + if(!isnull(throwforce_on)) + source.throwforce = throwforce_on + if(!isnull(throw_speed_on)) source.throw_speed = throw_speed_on if(LAZYLEN(attack_verb_continuous_on)) @@ -228,14 +277,14 @@ */ /datum/component/transforming/proc/set_inactive(obj/item/source) REMOVE_TRAIT(source, TRAIT_TRANSFORM_ACTIVE, REF(src)) - if(sharpness_on) - source.sharpness = initial(source.sharpness) - if(force_on) - source.force = initial(source.force) + (source.sharpness ? sharpened_bonus : 0) - if(throwforce_on) - source.throwforce = initial(source.throwforce) + (source.sharpness ? sharpened_bonus : 0) - if(throw_speed_on) - source.throw_speed = initial(source.throw_speed) + if(!isnull(sharpness_on)) + source.sharpness = sharpness_off + if(!isnull(force_on)) + source.force = force_off + if(!isnull(throwforce_on)) + source.throwforce = throwforce_off + if(!isnull(throw_speed_on)) + source.throw_speed = throwforce_off if(LAZYLEN(attack_verb_continuous_on)) source.attack_verb_continuous = attack_verb_continuous_off @@ -243,7 +292,7 @@ source.attack_verb_simple = attack_verb_simple_off source.hitsound = initial(source.hitsound) - source.update_weight_class(initial(source.w_class)) + source.update_weight_class(w_class_off) source.icon_state = initial(source.icon_state) source.inhand_icon_state = initial(source.inhand_icon_state) source.update_appearance() @@ -307,3 +356,11 @@ if(force_on + increment > max) return COMPONENT_BLOCK_SHARPEN_MAXED sharpened_bonus = increment + force_on += sharpened_bonus + throwforce_on += sharpened_bonus + force_off += sharpened_bonus + throwforce_off += sharpened_bonus + // Mimics base whetstone effect for the on state + sharpness_on = SHARP_EDGED + if (!active) + return COMPONENT_BLOCK_SHARPEN_SHARPNESS diff --git a/code/datums/components/twohanded.dm b/code/datums/components/twohanded.dm index a65d8c4f3c7..0db3642eb60 100644 --- a/code/datums/components/twohanded.dm +++ b/code/datums/components/twohanded.dm @@ -149,6 +149,8 @@ RegisterSignal(parent, COMSIG_ITEM_REMOVE_FANTASY_BONUSES, PROC_REF(remove_fantasy_bonuses)) RegisterSignal(parent, COMSIG_ATOM_FINALIZE_MATERIAL_EFFECTS, PROC_REF(on_materials_updated)) RegisterSignal(parent, COMSIG_ATOM_FINALIZE_REMOVE_MATERIAL_EFFECTS, PROC_REF(on_materials_updated)) + RegisterSignal(parent, COMSIG_ATOM_SINGLE_MATERIAL_EFFECT_APPLY, PROC_REF(on_material_apply)) + RegisterSignal(parent, COMSIG_ATOM_SINGLE_MATERIAL_EFFECT_REMOVE, PROC_REF(on_material_remove)) // Remove all siginals registered to the parent item /datum/component/two_handed/UnregisterFromParent() @@ -427,15 +429,26 @@ SIGNAL_HANDLER // With materials assigned we need to update our forces. if (wielded) - // Materials modify force multiplicatively! Most of the time, for snowflakes they gotta handle it themselves - if (!isnull(force_wielded)) - force_unwielded *= source.force / force_wielded force_wielded = source.force else - if (!isnull(force_unwielded)) - force_wielded *= source.force / force_unwielded force_unwielded = source.force +/datum/component/two_handed/proc/on_material_apply(obj/item/source, datum/material/material, amount, multiplier) + SIGNAL_HANDLER + // Opposite state's force needs to be calculated for each material's effect + if (wielded) + force_unwielded *= GET_MATERIAL_MODIFIER(source.get_material_force_modifier(material, source.sharpness), multiplier) + else + force_wielded *= GET_MATERIAL_MODIFIER(source.get_material_force_modifier(material, source.sharpness), multiplier) + +/datum/component/two_handed/proc/on_material_remove(obj/item/source, datum/material/material, amount, multiplier) + SIGNAL_HANDLER + // Same as appliation but inversed + if (wielded) + force_unwielded /= GET_MATERIAL_MODIFIER(source.get_material_force_modifier(material, source.sharpness), multiplier) + else + force_wielded /= GET_MATERIAL_MODIFIER(source.get_material_force_modifier(material, source.sharpness), multiplier) + /** * The offhand dummy item for two handed items */ diff --git a/code/game/atom/atom_materials.dm b/code/game/atom/atom_materials.dm index 05a1801ee6f..1f787d217bd 100644 --- a/code/game/atom/atom_materials.dm +++ b/code/game/atom/atom_materials.dm @@ -343,6 +343,7 @@ ///Apply material effects of a single material. /atom/proc/apply_single_mat_effect(datum/material/material, amount, multiplier) SHOULD_CALL_PARENT(TRUE) + SEND_SIGNAL(src, COMSIG_ATOM_SINGLE_MATERIAL_EFFECT_APPLY, material, amount, multiplier) // Derived and not optional, so this needs to be on base and not in the property code itself var/beauty_modifier = material.get_property(MATERIAL_BEAUTY) @@ -432,6 +433,7 @@ ///Remove material effects of a single material. /atom/proc/remove_single_mat_effect(datum/material/material, amount, multiplier) SHOULD_CALL_PARENT(TRUE) + SEND_SIGNAL(src, COMSIG_ATOM_SINGLE_MATERIAL_EFFECT_REMOVE, material, amount, multiplier) var/beauty_modifier = material.get_property(MATERIAL_BEAUTY) if(beauty_modifier) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index fd7a3f3058c..f45cc864750 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -2066,47 +2066,8 @@ obj_flags |= CONDUCTS_ELECTRICITY /obj/item/change_material_strength(datum/material/material, mat_amount, multiplier, remove = FALSE) - var/density = material.get_property(MATERIAL_DENSITY) - var/hardness = material.get_property(MATERIAL_HARDNESS) - var/flexibility = material.get_property(MATERIAL_FLEXIBILITY) - - // Item force calculation depends on its initial (assumed to be main) sharpness - // Transforming component doesn't work with materials at all and will need a refactor to change that, so we don't care about it here. - - var/force_mod = 1 - var/throwforce_mod = 1 - - switch (sharpness) - if (NONE) - // Blunt items are really hurt by all the flexing - force_mod = (1 + (density - 4) * 0.1) / (1 + flexibility * 0.1) - throwforce_mod = 1 + (density - 4) * 0.1 - flexibility * 0.1 - - if (SHARP_EDGED) - // Sharp items don't care about density and need high hardness to get a real bonus, but can tolerate (and benefit from) some flex - force_mod = 1 + (hardness - 4) * 0.1 - throwforce_mod = 1 + (hardness - 4) * 0.1 - - // Peaks out at 20% at flexibility of 1, drops off up to -80% at 10 - if (flexibility < 2) - force_mod *= 1 + (1 - abs(1 - flexibility)) * 0.2 - throwforce_mod += (1 - abs(1 - flexibility)) * 0.2 - else - force_mod *= 1 - (flexibility - 2) * 0.1 - throwforce_mod -= (flexibility - 2) * 0.1 - - if (SHARP_POINTY) - // Pointy items care about both density and hardness - force_mod = 1 + MATERIAL_PROPERTY_DIVERGENCE(density, 4, 6) * 0.05 + (hardness - 4) * 0.1 - throwforce_mod = 1 + MATERIAL_PROPERTY_DIVERGENCE(density, 4, 6) * 0.05 * 0.05 + (hardness - 4) * 0.1 - // But are not affected by flexibility until higher values, although they don't benefit from it either - if (flexibility > 4) - force_mod *= (1 - (flexibility - 4) * 0.2) - throwforce_mod -= (flexibility - 4) * 0.2 - - // Just for sanity in case something breaks - force_mod = round(clamp(force_mod, MATERIAL_MIN_FORCE_MULTIPLIER, MATERIAL_MAX_FORCE_MULTIPLIER), 0.01) - throwforce_mod = round(clamp(throwforce_mod, MATERIAL_MIN_FORCE_MULTIPLIER, MATERIAL_MAX_FORCE_MULTIPLIER), 0.01) + var/force_mod = get_material_force_modifier(material) + var/throwforce_mod = get_material_throwforce_modifier(material) if (!remove) force *= GET_MATERIAL_MODIFIER(force_mod, multiplier) @@ -2115,6 +2076,70 @@ force /= GET_MATERIAL_MODIFIER(force_mod, multiplier) throwforce /= GET_MATERIAL_MODIFIER(throwforce_mod, multiplier) +/// Returns a force multiplier from a material for a given sharpness +/obj/item/proc/get_material_force_modifier(datum/material/material, item_sharpness = get_sharpness()) + var/density = material.get_property(MATERIAL_DENSITY) + var/hardness = material.get_property(MATERIAL_HARDNESS) + var/flexibility = material.get_property(MATERIAL_FLEXIBILITY) + var/force_mod = 1 + switch (item_sharpness) + if (NONE) + // Blunt items are really hurt by all the flexing + force_mod = (1 + (density - 4) * 0.1) / (1 + flexibility * 0.1) + + if (SHARP_EDGED) + // Sharp items don't care about density and need high hardness to get a real bonus, but can tolerate (and benefit from) some flex + force_mod = 1 + (hardness - 4) * 0.1 + + // Peaks out at 20% at flexibility of 1, drops off up to -80% at 10 + if (flexibility < 2) + force_mod *= 1 + (1 - abs(1 - flexibility)) * 0.2 + else + force_mod *= 1 - (flexibility - 2) * 0.1 + + if (SHARP_POINTY) + // Pointy items care about both density and hardness + force_mod = 1 + MATERIAL_PROPERTY_DIVERGENCE(density, 4, 6) * 0.05 + (hardness - 4) * 0.1 + // But are not affected by flexibility until higher values, although they don't benefit from it either + if (flexibility > 4) + force_mod *= (1 - (flexibility - 4) * 0.2) + + // Just for sanity in case something breaks + force_mod = round(clamp(force_mod, MATERIAL_MIN_FORCE_MULTIPLIER, MATERIAL_MAX_FORCE_MULTIPLIER), 0.01) + return force_mod + +/// Returns a force multiplier from a material for a given sharpness +/obj/item/proc/get_material_throwforce_modifier(datum/material/material, item_sharpness = get_sharpness()) + var/density = material.get_property(MATERIAL_DENSITY) + var/hardness = material.get_property(MATERIAL_HARDNESS) + var/flexibility = material.get_property(MATERIAL_FLEXIBILITY) + var/throwforce_mod = 1 + switch (item_sharpness) + if (NONE) + // Blunt items are really hurt by all the flexing + throwforce_mod = 1 + (density - 4) * 0.1 - flexibility * 0.1 + + if (SHARP_EDGED) + // Sharp items don't care about density and need high hardness to get a real bonus, but can tolerate (and benefit from) some flex + throwforce_mod = 1 + (hardness - 4) * 0.1 + + // Peaks out at 20% at flexibility of 1, drops off up to -80% at 10 + if (flexibility < 2) + throwforce_mod += (1 - abs(1 - flexibility)) * 0.2 + else + throwforce_mod -= (flexibility - 2) * 0.1 + + if (SHARP_POINTY) + // Pointy items care about both density and hardness + throwforce_mod = 1 + MATERIAL_PROPERTY_DIVERGENCE(density, 4, 6) * 0.05 * 0.05 + (hardness - 4) * 0.1 + // But are not affected by flexibility until higher values, although they don't benefit from it either + if (flexibility > 4) + throwforce_mod -= (flexibility - 4) * 0.2 + + // Just for sanity in case something breaks + throwforce_mod = round(clamp(throwforce_mod, MATERIAL_MIN_FORCE_MULTIPLIER, MATERIAL_MAX_FORCE_MULTIPLIER), 0.01) + return throwforce_mod + /** * Returns the atom(either itself or an internal module) that will interact/attack the target on behalf of us * For example an object can have different `tool_behaviours` (e.g borg omni tool) but will return an internal reference of that tool to attack for us diff --git a/code/game/objects/items/sharpener.dm b/code/game/objects/items/sharpener.dm index daf1bf0ba96..f01ca917f8c 100644 --- a/code/game/objects/items/sharpener.dm +++ b/code/game/objects/items/sharpener.dm @@ -53,7 +53,8 @@ I.wound_bonus = I.wound_bonus + increment //wound_bonus has no cap user.visible_message(span_notice("[user] sharpens [I] with [src]!"), span_notice("You sharpen [I], making it much more deadly than before.")) playsound(src, 'sound/items/unsheath.ogg', 25, TRUE) - I.sharpness = SHARP_EDGED //When you whetstone something, it becomes an edged weapon, even if it was previously dull or pointy + if(!(signal_out & COMPONENT_BLOCK_SHARPEN_SHARPNESS)) + I.sharpness = SHARP_EDGED //When you whetstone something, it becomes an edged weapon, even if it was previously dull or pointy I.throwforce = clamp(I.throwforce + increment, 0, max) I.name = "[prefix] [I.name]" //This adds a prefix and a space to the item's name regardless of what the prefix is desc = "[desc] At least, it used to."