Files
Bubberstation/code/game/objects/items/stacks/stack.dm
Ghom 2531d69ff4 refactoring how materials effects are added to atoms (#86901)
## About The Pull Request
I'm "cooking" the materials system a bit, specifically the code
responsible for applying and removing effects. My goal is to move most
of the code to the objects-side, split it in smaller procs that can be
more easily overriden or called for object-specific modifiers and
effects, while also revamping things all around to better support items
made from multiple materials (the cleric mace will most likely be one in
this PR, with the handle and tip made of different materials).

PR NO LONGER WIP, TESTED AND ALL, CLERIC MACES CAN NOW BE MADE OF TWO
MATERIALS.

## Why It's Good For The Game
One of the nastiest flaws with the materials system is that it's just
unfeasable to have items made of multiple mats (with effects enabled)
right now, as they easily tend to override each other, where some of the
modifiers and effects should only be applied the main material.

Beside, the system's starting to show signs of its time, from the
several type checks used to apply different effects, the one letter
variables to the the material flags that are still being passed down as
arguments when you can access them from the atom/source arg anyway. It
would be disonhest of me if I went ahead and coded material fishing rods
or whatever fish fuckery with materials without ensuring it won't
further the technical debt the feature currently has.

## Changelog
🆑
refactor: Refactored materials code. report any issue.
add: Cleric maces (The autolathe-printable weapon design from outer
space) can now be made of two different materials.
balance: Buffed cleric maces a little.
fix: toolboxes' stats are now affected by materials again.
/🆑

---------

Co-authored-by: _0Steven <42909981+00-Steven@users.noreply.github.com>
2024-11-11 00:43:40 -08:00

731 lines
26 KiB
Plaintext

/* Stack type objects!
* Contains:
* Stacks
* Recipe datum
* Recipe list datum
*/
/*
* Stacks
*/
/obj/item/stack
icon = 'icons/obj/stack_objects.dmi'
gender = PLURAL
material_modifier = 0.05 //5%, so that a 50 sheet stack has the effect of 5k materials instead of 100k.
max_integrity = 100
item_flags = SKIP_FANTASY_ON_SPAWN
/// A list to all recipies this stack item can create.
var/list/datum/stack_recipe/recipes
/// What's the name of just 1 of this stack. You have a stack of leather, but one piece of leather
var/singular_name
/// How much is in this stack?
var/amount = 1
/// How much is allowed in this stack?
// Also see stack recipes initialisation. "max_res_amount" must be equal to this max_amount
var/max_amount = 50
/// If TRUE, this stack is a module used by a cyborg (doesn't run out like normal / etc)
var/is_cyborg = FALSE
/// Related to above. If present, the energy we draw from when using stack items, for cyborgs
var/datum/robot_energy_storage/source
/// Related to above. How much energy it costs from storage to use stack items
var/cost = 1
/// This path and its children should merge with this stack, defaults to src.type
var/merge_type = null
/// The weight class the stack has at amount > 2/3rds max_amount
var/full_w_class = WEIGHT_CLASS_NORMAL
/// Determines whether the item should update its sprites based on amount.
var/novariants = TRUE
/// List that tells you how much is in a single unit.
var/list/mats_per_unit
/// Datum material type that this stack is made of
var/material_type
// NOTE: When adding grind_results, the amounts should be for an INDIVIDUAL ITEM -
// these amounts will be multiplied by the stack size in on_grind()
/// Amount of matter given back to RCDs
var/matter_amount = 0
/// Does this stack require a unique girder in order to make a wall?
var/has_unique_girder = FALSE
/// What typepath table we create from this stack
var/obj/structure/table/tableVariant
/// What typepath stairs do we create from this stack
var/obj/structure/stairs/stairs_type
/// If TRUE, we'll use a radial instead when displaying recipes
var/use_radial = FALSE
/// If use_radial is TRUE, this is the radius of the radial
var/radial_radius = 52
// The following are all for medical treatment
// They're here instead of /stack/medical
// because sticky tape can be used as a makeshift bandage or splint
/// If set and this used as a splint for a broken bone wound,
/// This is used as a multiplier for applicable slowdowns (lower = better) (also for speeding up burn recoveries)
var/splint_factor
/// Like splint_factor but for burns instead of bone wounds. This is a multiplier used to speed up burn recoveries
var/burn_cleanliness_bonus
/// How much blood flow this stack can absorb if used as a bandage on a cut wound.
/// note that absorption is how much we lower the flow rate, not the raw amount of blood we suck up
var/absorption_capacity
/// How quickly we lower the blood flow on a cut wound we're bandaging.
/// Expected lifetime of this bandage in seconds is thus absorption_capacity/absorption_rate,
/// or until the cut heals, whichever comes first
var/absorption_rate
/obj/item/stack/Initialize(mapload, new_amount, merge = TRUE, list/mat_override=null, mat_amt=1)
if(new_amount != null)
amount = new_amount
while(amount > max_amount)
amount -= max_amount
new type(loc, max_amount, FALSE, mat_override, mat_amt)
if(!merge_type)
merge_type = type
. = ..()
var/materials_mult = amount
if(LAZYLEN(mat_override))
materials_mult *= mat_amt
mats_per_unit = mat_override
if(LAZYLEN(mats_per_unit))
initialize_materials(mats_per_unit, materials_mult)
if(merge)
for(var/obj/item/stack/item_stack in loc)
if(item_stack == src)
continue
if(can_merge(item_stack))
INVOKE_ASYNC(src, PROC_REF(merge_without_del), item_stack)
if(is_zero_amount(delete_if_zero = FALSE))
return INITIALIZE_HINT_QDEL
recipes = get_main_recipes().Copy()
if(material_type)
var/datum/material/what_are_we_made_of = GET_MATERIAL_REF(material_type) //First/main material
for(var/category in what_are_we_made_of.categories)
switch(category)
if(MAT_CATEGORY_BASE_RECIPES)
recipes |= SSmaterials.base_stack_recipes.Copy()
if(MAT_CATEGORY_RIGID)
recipes |= SSmaterials.rigid_stack_recipes.Copy()
update_weight()
update_appearance()
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_movable_entered_occupied_turf),
)
AddElement(/datum/element/connect_loc, loc_connections)
if(is_path_in_list(merge_type, GLOB.golem_stack_food_directory))
AddComponent(/datum/component/golem_food, golem_food_key = merge_type)
///Called to lazily update the materials of the item whenever the used or if more is added
/obj/item/stack/proc/update_custom_materials()
if(length(mats_per_unit))
set_custom_materials(mats_per_unit, amount)
/obj/item/stack/apply_material_effects(list/materials)
. = ..()
if(amount)
mats_per_unit = SSmaterials.FindOrCreateMaterialCombo(materials, 1/amount)
/obj/item/stack/blend_requirements()
if(is_cyborg)
to_chat(usr, span_warning("[src] is too integrated into your chassis and can't be ground up!"))
return
return TRUE
/obj/item/stack/grind(datum/reagents/target_holder, mob/user)
var/current_amount = get_amount()
if(current_amount <= 0 || QDELETED(src)) //just to get rid of this 0 amount/deleted stack we return success
return TRUE
if(on_grind() == -1)
return FALSE
if(isnull(target_holder))
return TRUE
if(reagents)
reagents.trans_to(target_holder, reagents.total_volume, transferred_by = user)
var/available_volume = target_holder.maximum_volume - target_holder.total_volume
//compute total volume of reagents that will be occupied by grind_results
var/total_volume = 0
for(var/reagent in grind_results)
total_volume += grind_results[reagent]
//compute number of pieces(or sheets) from available_volume
var/available_amount = min(current_amount, round(available_volume / total_volume))
if(available_amount <= 0)
return FALSE
//Now transfer the grind results scaled by available_amount
var/list/grind_reagents = grind_results.Copy()
for(var/reagent in grind_reagents)
grind_reagents[reagent] *= available_amount
target_holder.add_reagent_list(grind_reagents)
/**
* use available_amount of sheets/pieces, return TRUE only if all sheets/pieces of this stack were used
* we don't delete this stack when it reaches 0 because we expect the all in one grinder, etc to delete
* this stack if grinding was successful
*/
use(available_amount, check = FALSE)
return available_amount == current_amount
/obj/item/stack/proc/get_main_recipes()
RETURN_TYPE(/list)
SHOULD_CALL_PARENT(TRUE)
return list() //empty list
/obj/item/stack/proc/update_weight()
if(amount <= (max_amount * (1/3)))
update_weight_class(clamp(full_w_class-2, WEIGHT_CLASS_TINY, full_w_class))
else if (amount <= (max_amount * (2/3)))
update_weight_class(clamp(full_w_class-1, WEIGHT_CLASS_TINY, full_w_class))
else
update_weight_class(full_w_class)
/obj/item/stack/update_icon_state()
if(novariants)
return ..()
if(amount <= (max_amount * (1/3)))
icon_state = initial(icon_state)
return ..()
if (amount <= (max_amount * (2/3)))
icon_state = "[initial(icon_state)]_2"
return ..()
icon_state = "[initial(icon_state)]_3"
return ..()
/obj/item/stack/examine(mob/user)
. = ..()
if(is_cyborg)
return
if(singular_name)
if(get_amount()>1)
. += "There are [get_amount()] [singular_name]\s in the stack."
else
. += "There is [get_amount()] [singular_name] in the stack."
else if(get_amount()>1)
. += "There are [get_amount()] in the stack."
else
. += "There is [get_amount()] in the stack."
. += span_notice("<b>Right-click</b> with an empty hand to take a custom amount.")
/obj/item/stack/proc/get_amount()
if(is_cyborg)
. = round(source?.energy / cost)
else
. = (amount)
/**
* Builds all recipes in a given recipe list and returns an association list containing them
*
* Arguments:
* * recipe_to_iterate - The list of recipes we are using to build recipes
*/
/obj/item/stack/proc/recursively_build_recipes(list/recipe_to_iterate)
var/list/L = list()
for(var/recipe in recipe_to_iterate)
if(istype(recipe, /datum/stack_recipe_list))
var/datum/stack_recipe_list/R = recipe
L["[R.title]"] = recursively_build_recipes(R.recipes)
if(istype(recipe, /datum/stack_recipe))
var/datum/stack_recipe/R = recipe
L["[R.title]"] = build_recipe(R)
return L
/**
* Returns a list of properties of a given recipe
*
* Arguments:
* * R - The stack recipe we are using to get a list of properties
*/
/obj/item/stack/proc/build_recipe(datum/stack_recipe/R)
return list(
"res_amount" = R.res_amount,
"max_res_amount" = R.max_res_amount,
"req_amount" = R.req_amount,
"ref" = text_ref(R),
)
/**
* Checks if the recipe is valid to be used
*
* Arguments:
* * R - The stack recipe we are checking if it is valid
* * recipe_list - The list of recipes we are using to check the given recipe
*/
/obj/item/stack/proc/is_valid_recipe(datum/stack_recipe/R, list/recipe_list)
for(var/S in recipe_list)
if(S == R)
return TRUE
if(istype(S, /datum/stack_recipe_list))
var/datum/stack_recipe_list/L = S
if(is_valid_recipe(R, L.recipes))
return TRUE
return FALSE
/obj/item/stack/interact(mob/user)
if(use_radial)
show_construction_radial(user)
else
ui_interact(user)
/obj/item/stack/ui_state(mob/user)
return GLOB.hands_state
/obj/item/stack/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "StackCrafting", name)
ui.open()
/obj/item/stack/ui_data(mob/user)
var/list/data = list()
data["amount"] = get_amount()
return data
/obj/item/stack/ui_static_data(mob/user)
var/list/data = list()
data["recipes"] = recursively_build_recipes(recipes)
return data
/obj/item/stack/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
switch(action)
if("make")
var/datum/stack_recipe/recipe = locate(params["ref"])
var/multiplier = text2num(params["multiplier"])
return make_item(usr, recipe, multiplier)
/// The key / title for a radial option that shows the entire list of buildables (uses the old menu)
#define FULL_LIST "view full list"
/// Shows a radial consisting of every radial recipe we have in our list.
/obj/item/stack/proc/show_construction_radial(mob/builder)
var/list/options = list()
var/list/titles_to_recipes = list()
for(var/datum/stack_recipe/radial/recipe in recipes)
var/datum/radial_menu_choice/option = new()
option.image = image(
icon = initial(recipe.result_type.icon),
icon_state = initial(recipe.result_type.icon_state),
)
if(recipe.desc)
option.info = recipe.desc
options[recipe.title] = option
titles_to_recipes[recipe.title] = recipe
// After everything's been added to the radial, add an option
// that lets the user see the whole list of buildables
options[FULL_LIST] = image(
icon = 'icons/hud/radial.dmi',
icon_state = "radial_full_list",
)
var/selection = show_radial_menu(
user = builder,
anchor = builder,
choices = options,
custom_check = CALLBACK(src, PROC_REF(radial_check), builder),
radius = radial_radius,
tooltips = TRUE,
)
if(!selection)
return
// Run normal UI interact if we wanna see the full list
if(selection == FULL_LIST)
ui_interact(builder)
return
// Otherwise go straight to building
var/datum/stack_recipe/picked_recipe = titles_to_recipes[selection]
if(!istype(picked_recipe))
return
make_item(builder, picked_recipe, 1)
/// Used as a callback for radial building.
/obj/item/stack/proc/radial_check(mob/builder)
if(QDELETED(builder) || QDELETED(src))
return FALSE
if(builder.incapacitated)
return FALSE
if(!builder.is_holding(src))
return FALSE
return TRUE
#undef FULL_LIST
/// Makes the item with the given recipe.
/obj/item/stack/proc/make_item(mob/builder, datum/stack_recipe/recipe, multiplier)
if(get_amount() < 1 && !is_cyborg) //sanity check as this shouldn't happen
qdel(src)
return
if(!is_valid_recipe(recipe, recipes)) //href exploit protection
return
if(!multiplier || multiplier < 1 || !IS_FINITE(multiplier)) //href exploit protection
stack_trace("Invalid multiplier value in stack creation [multiplier], [usr] is likely attempting an exploit")
return
if(!building_checks(builder, recipe, multiplier))
return
if(recipe.time)
var/adjusted_time = 0
builder.balloon_alert(builder, "building...")
builder.visible_message(
span_notice("[builder] starts building \a [recipe.title]."),
span_notice("You start building \a [recipe.title]..."),
)
if(HAS_TRAIT(builder, recipe.trait_booster))
adjusted_time = (recipe.time * recipe.trait_modifier)
else
adjusted_time = recipe.time
var/skill_modifier = builder.mind.get_skill_modifier(/datum/skill/construction, SKILL_SPEED_MODIFIER) //SKYRAT EDIT: Construction Skill
if(!do_after(builder, adjusted_time * skill_modifier, target = builder))
builder.balloon_alert(builder, "interrupted!")
return
if(!building_checks(builder, recipe, multiplier))
return
var/atom/created
if(recipe.max_res_amount > 1) // Is it a stack?
created = new recipe.result_type(builder.drop_location(), recipe.res_amount * multiplier)
builder.balloon_alert(builder, "built items")
else if(ispath(recipe.result_type, /turf))
var/turf/covered_turf = builder.drop_location()
if(!isturf(covered_turf))
return
var/turf/created_turf = covered_turf.place_on_top(recipe.result_type, flags = CHANGETURF_INHERIT_AIR)
builder.balloon_alert(builder, "placed [ispath(recipe.result_type, /turf/open) ? "floor" : "wall"]")
if((recipe.crafting_flags & CRAFT_APPLIES_MATS) && LAZYLEN(mats_per_unit))
created_turf.set_custom_materials(mats_per_unit, recipe.req_amount / recipe.res_amount)
else
created = new recipe.result_type(builder.drop_location())
builder.balloon_alert(builder, "built item")
if(created)
created.setDir(builder.dir)
SEND_SIGNAL(created, COMSIG_ATOM_CONSTRUCTED, builder)
on_item_crafted(builder, created)
builder.mind.adjust_experience(/datum/skill/construction, 5) //SKYRAT EDIT: Construction Skill
// Use up the material
use(recipe.req_amount * multiplier)
builder.investigate_log("crafted [recipe.title]", INVESTIGATE_CRAFTING)
// Apply mat datums
if((recipe.crafting_flags & CRAFT_APPLIES_MATS) && LAZYLEN(mats_per_unit))
if(isstack(created))
var/obj/item/stack/crafted_stack = created
crafted_stack.set_custom_materials(mats_per_unit, (recipe.req_amount / recipe.res_amount) * crafted_stack.amount)
else
created.set_custom_materials(mats_per_unit, recipe.req_amount / recipe.res_amount)
// We could be qdeleted - like if it's a stack and has already been merged
if(QDELETED(created))
return TRUE
// Add fingerprints first, otherwise created might already be deleted because of stack merging
created.add_fingerprint(builder)
if(isitem(created))
builder.put_in_hands(created)
//BubbleWrap - so newly formed boxes are empty
if(istype(created, /obj/item/storage))
for (var/obj/item/thing in created)
qdel(thing)
//BubbleWrap END
return TRUE
/// Run special logic on created items after they've been successfully crafted.
/obj/item/stack/proc/on_item_crafted(mob/builder, atom/created)
return
/obj/item/stack/vv_edit_var(vname, vval)
if(vname == NAMEOF(src, amount))
add(clamp(vval, 1-amount, max_amount - amount)) //there must always be one.
return TRUE
else if(vname == NAMEOF(src, max_amount))
max_amount = max(vval, 1)
add((max_amount < amount) ? (max_amount - amount) : 0) //update icon, weight, ect
return TRUE
return ..()
/// Checks if we can build here, validly.
/obj/item/stack/proc/building_checks(mob/builder, datum/stack_recipe/recipe, multiplier)
if (get_amount() < recipe.req_amount * multiplier)
builder.balloon_alert(builder, "not enough material!")
return FALSE
var/turf/dest_turf = get_turf(builder)
if((recipe.crafting_flags & CRAFT_ONE_PER_TURF) && (locate(recipe.result_type) in dest_turf))
builder.balloon_alert(builder, "already one here!")
return FALSE
if(recipe.crafting_flags & CRAFT_CHECK_DIRECTION)
if(!valid_build_direction(dest_turf, builder.dir, is_fulltile = (recipe.crafting_flags & CRAFT_IS_FULLTILE)))
builder.balloon_alert(builder, "won't fit here!")
return FALSE
if(recipe.crafting_flags & CRAFT_ON_SOLID_GROUND)
if(isclosedturf(dest_turf))
builder.balloon_alert(builder, "cannot be made on a wall!")
return FALSE
if(is_type_in_typecache(dest_turf, GLOB.turfs_without_ground))
if(!locate(/obj/structure/thermoplastic) in dest_turf) // for tram construction
builder.balloon_alert(builder, "must be made on solid ground!")
return FALSE
if(recipe.crafting_flags & CRAFT_CHECK_DENSITY)
for(var/obj/object in dest_turf)
if(object.density && !(object.obj_flags & IGNORE_DENSITY) || object.obj_flags & BLOCKS_CONSTRUCTION)
builder.balloon_alert(builder, "something is in the way!")
return FALSE
if(recipe.placement_checks & STACK_CHECK_CARDINALS)
var/turf/nearby_turf
for(var/direction in GLOB.cardinals)
nearby_turf = get_step(dest_turf, direction)
if(locate(recipe.result_type) in nearby_turf)
to_chat(builder, span_warning("\The [recipe.title] must not be built directly adjacent to another!"))
builder.balloon_alert(builder, "can't be adjacent to another!")
return FALSE
if(recipe.placement_checks & STACK_CHECK_ADJACENT)
if(locate(recipe.result_type) in range(1, dest_turf))
builder.balloon_alert(builder, "can't be near another!")
return FALSE
if(recipe.placement_checks & STACK_CHECK_TRAM_FORBIDDEN)
if(locate(/obj/structure/transport/linear/tram) in dest_turf || locate(/obj/structure/thermoplastic) in dest_turf)
builder.balloon_alert(builder, "can't be on tram!")
return FALSE
if(recipe.placement_checks & STACK_CHECK_TRAM_EXCLUSIVE)
if(!locate(/obj/structure/transport/linear/tram) in dest_turf)
builder.balloon_alert(builder, "must be made on a tram!")
return FALSE
return TRUE
/obj/item/stack/use(used, transfer = FALSE, check = TRUE) // return 0 = borked; return 1 = had enough
if(check && is_zero_amount(delete_if_zero = TRUE))
return FALSE
if(is_cyborg)
return source.use_charge(used * cost)
if (amount < used)
return FALSE
amount -= used
if(check && is_zero_amount(delete_if_zero = TRUE))
return TRUE
update_custom_materials()
update_appearance()
update_weight()
return TRUE
/obj/item/stack/tool_use_check(mob/living/user, amount, heat_required)
if(get_amount() < amount)
// general balloon alert that says they don't have enough
user.balloon_alert(user, "not enough material!")
// then a more specific message about how much they need and what they need specifically
if(singular_name)
if(amount > 1)
to_chat(user, span_warning("You need at least [amount] [singular_name]\s to do this!"))
else
to_chat(user, span_warning("You need at least [amount] [singular_name] to do this!"))
else
to_chat(user, span_warning("You need at least [amount] to do this!"))
return FALSE
return TRUE
/**
* Returns TRUE if the item stack is the equivalent of a 0 amount item.
*
* Also deletes the item if delete_if_zero is TRUE and the stack does not have
* is_cyborg set to true.
*/
/obj/item/stack/proc/is_zero_amount(delete_if_zero = TRUE)
if(is_cyborg)
return source.energy < cost
if(amount < 1)
if(delete_if_zero)
qdel(src)
return TRUE
return FALSE
/** Adds some number of units to this stack.
*
* Arguments:
* - _amount: The number of units to add to this stack.
*/
/obj/item/stack/proc/add(_amount)
if(is_cyborg)
source.add_charge(_amount * cost)
else
amount += _amount
update_custom_materials()
update_appearance()
update_weight()
/** Checks whether this stack can merge itself into another stack.
*
* Arguments:
* - [check][/obj/item/stack]: The stack to check for mergeability.
* - [inhand][boolean]: Whether or not the stack to check should act like it's in a mob's hand.
*/
/obj/item/stack/proc/can_merge(obj/item/stack/check, inhand = FALSE)
// We don't only use istype here, since that will match subtypes, and stack things that shouldn't stack
if(!istype(check, merge_type) || check.merge_type != merge_type)
return FALSE
if(mats_per_unit ~! check.mats_per_unit) // ~! in case of lists this operator checks only keys, but not values
return FALSE
if(is_cyborg) // No merging cyborg stacks into other stacks
return FALSE
if(ismob(loc) && !inhand) // no merging with items that are on the mob
return FALSE
if(istype(loc, /obj/machinery)) // no merging items in machines that aren't both in componentparts
var/obj/machinery/machine = loc
if(!(src in machine.component_parts) || !(check in machine.component_parts))
return FALSE
if(SEND_SIGNAL(src, COMSIG_STACK_CAN_MERGE, check, inhand) & CANCEL_STACK_MERGE)
return FALSE
return TRUE
/**
* Merges as much of src into target_stack as possible. If present, the limit arg overrides target_stack.max_amount for transfer.
*
* This calls use() without check = FALSE, preventing the item from qdeling itself if it reaches 0 stack size.
*
* As a result, this proc can leave behind a 0 amount stack.
*/
/obj/item/stack/proc/merge_without_del(obj/item/stack/target_stack, limit)
// Cover edge cases where multiple stacks are being merged together and haven't been deleted properly.
// Also cover edge case where a stack is being merged into itself, which is supposedly possible.
if(QDELETED(target_stack))
CRASH("Stack merge attempted on qdeleted target stack.")
if(QDELETED(src))
CRASH("Stack merge attempted on qdeleted source stack.")
if(target_stack == src)
CRASH("Stack attempted to merge into itself.")
var/transfer = get_amount()
if(target_stack.is_cyborg)
transfer = min(transfer, round((target_stack.source.max_energy - target_stack.source.energy) / target_stack.cost))
else
transfer = min(transfer, (limit ? limit : target_stack.max_amount) - target_stack.amount)
if(pulledby)
pulledby.start_pulling(target_stack)
target_stack.copy_evidences(src)
use(transfer, transfer = TRUE, check = FALSE)
target_stack.add(transfer)
if(target_stack.mats_per_unit != mats_per_unit) // We get the average value of mats_per_unit between two stacks getting merged
var/list/temp_mats_list = list() // mats_per_unit is passed by ref into this coil, and that same ref is used in other places. If we didn't make a new list here we'd end up contaminating those other places, which leads to batshit behavior
for(var/mat_type in target_stack.mats_per_unit)
temp_mats_list[mat_type] = (target_stack.mats_per_unit[mat_type] * (target_stack.amount - transfer) + mats_per_unit[mat_type] * transfer) / target_stack.amount
target_stack.mats_per_unit = temp_mats_list
return transfer
/**
* Merges as much of src into target_stack as possible. If present, the limit arg overrides target_stack.max_amount for transfer.
*
* This proc deletes src if the remaining amount after the transfer is 0.
*/
/obj/item/stack/proc/merge(obj/item/stack/target_stack, limit)
. = merge_without_del(target_stack, limit)
is_zero_amount(delete_if_zero = TRUE)
/// Signal handler for connect_loc element. Called when a movable enters the turf we're currently occupying. Merges if possible.
/obj/item/stack/proc/on_movable_entered_occupied_turf(datum/source, atom/movable/arrived)
SIGNAL_HANDLER
// Edge case. This signal will also be sent when src has entered the turf. Don't want to merge with ourselves.
if(arrived == src)
return
if(!arrived.throwing && can_merge(arrived))
INVOKE_ASYNC(src, PROC_REF(merge), arrived)
/obj/item/stack/hitby(atom/movable/hitting, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
if(can_merge(hitting, inhand = TRUE))
merge(hitting)
. = ..()
//ATTACK HAND IGNORING PARENT RETURN VALUE
/obj/item/stack/attack_hand(mob/user, list/modifiers)
if(user.get_inactive_held_item() == src)
if(is_zero_amount(delete_if_zero = TRUE))
return
return split_stack(user, 1)
else
. = ..()
/obj/item/stack/attack_hand_secondary(mob/user, modifiers)
. = ..()
if(. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN)
return
if(is_cyborg || !user.can_perform_action(src, NEED_DEXTERITY))
return SECONDARY_ATTACK_CONTINUE_CHAIN
if(is_zero_amount(delete_if_zero = TRUE))
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
var/max = get_amount()
var/stackmaterial = tgui_input_number(user, "How many sheets do you wish to take out of this stack?", "Stack Split", max_value = max)
if(!stackmaterial || QDELETED(user) || QDELETED(src) || !usr.can_perform_action(src, FORBID_TELEKINESIS_REACH))
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
split_stack(user, stackmaterial)
to_chat(user, span_notice("You take [stackmaterial] sheets out of the stack."))
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/** Splits the stack into two stacks.
*
* Arguments:
* - [user][/mob]: The mob splitting the stack.
* - amount: The number of units to split from this stack.
*/
/obj/item/stack/proc/split_stack(mob/user, amount)
if(!use(amount, TRUE, FALSE))
return null
var/obj/item/stack/F = new type(user? user : drop_location(), amount, FALSE, mats_per_unit)
. = F
F.copy_evidences(src)
loc.atom_storage?.refresh_views()
if(user)
if(!user.put_in_hands(F, merge_stacks = FALSE))
F.forceMove(user.drop_location())
add_fingerprint(user)
F.add_fingerprint(user)
is_zero_amount(delete_if_zero = TRUE)
/obj/item/stack/attackby(obj/item/W, mob/user, params)
if(can_merge(W, inhand = TRUE))
var/obj/item/stack/S = W
if(merge(S))
to_chat(user, span_notice("Your [S.name] stack now contains [S.get_amount()] [S.singular_name]\s."))
else
. = ..()
/obj/item/stack/proc/copy_evidences(obj/item/stack/from)
add_blood_DNA(GET_ATOM_BLOOD_DNA(from))
add_fingerprint_list(GET_ATOM_FINGERPRINTS(from))
add_hiddenprint_list(GET_ATOM_HIDDENPRINTS(from))
fingerprintslast = from.fingerprintslast
//TODO bloody overlay