Files
b4a124e557 [IDB IGNORE] cyborg refactor: icons, chassis, modules base (#6929)
- separates the composition of cyborgs into modules, iconsets, chassis,
upgrades
- modules / chassis atm can provide items, so can upgrades; in the
future we will want to probably generalize the item API a bit so
components can work
- chassis now can (unimplemented) easily be made to give different stats
- reorganizes all the cyborg sprites
- adds generalized item mounts with redirection support and tears out
legacy cyborg items
- entirely rebalances modules & writes some new lawsets in for future
use

---------

Co-authored-by: silicons <silicons@silicons.dev>
2026-01-13 18:39:38 +00:00

627 lines
18 KiB
Plaintext

/**
* automatically splitting stack spawns
*
* supports /datum/prototype/material as well
*
* @return **amount of objects created** (not total stack/sheet amount made!)
*/
/proc/spawn_stacks_at(atom/location, stack_path, amount)
. = 0
var/safety = 50
if(ispath(stack_path, /datum/prototype/material))
var/datum/prototype/material/resolved = RSmaterials.fetch(stack_path)
// todo: ugh
resolved.place_sheet(location, amount)
return 1
else if(istype(stack_path, /obj/item/stack))
stack_path = stack_path:type
var/obj/item/stack/casted_path = stack_path
var/max_amount = initial(casted_path.max_amount)
while(amount > 0)
var/creating = min(amount, max_amount)
new stack_path(location, creating)
.++
amount -= creating
--safety
if(!safety)
CRASH("ran out of safety")
/proc/__construct_legacy_stack_provider_material_map()
return list(
/obj/item/stack/rods = list(
/datum/prototype/material/steel::id = SHEET_MATERIAL_AMOUNT / 2,
),
/obj/item/stack/tile/floor = list(
/datum/prototype/material/steel::id = SHEET_MATERIAL_AMOUNT / 4,
),
/obj/item/stack/material/glass/reinforced = list(
/datum/prototype/material/glass::id = SHEET_MATERIAL_AMOUNT / 1,
/datum/prototype/material/steel::id = SHEET_MATERIAL_AMOUNT / 2,
),
)
/**
* Items that can stack, tracking the number of which is in it
*
* * [worth_intrinsic] is the only thing used on this path for detecting economic value. Normal get_worth() is not considered.
*/
/obj/item/stack
gender = PLURAL
origin_tech = list(TECH_MATERIAL = 1)
icon = 'icons/obj/stacks.dmi'
item_flags = ITEM_CAREFUL_BLUDGEON | ITEM_ENCUMBERS_WHILE_HELD
/// Total number of states used in updating icons.
/// todo: all stacks should use this, remove `use_new_icon_update
///
/// * Only active when [use_new_icon_update] is set on
/// * This counts up from 1.
/// * If null, we don't do icon updates based on amount.
var/icon_state_count
/// Our effective stack type. Defaults to our type.
/// * Used to determine our identity when calling into providers.
/// * Used to determine what can merge into what (so colored subtypes of cable can all be considered cable).
var/stack_type
/// What type we split into. Useful if you have an infinite stack you don't want to be split into another infinite stack.
/// * Use `|| stack_type || type` to default to stack type if this is not set.
var/split_type
/// Current amount
var/amount = 1
/// Maximum amount
var/max_amount = 50
/// this is admittedly shitcode but this allows for automatic
/// stack provider support for non /material stacks that are directly derived from
/// materials, like reinf glass and metal tiles
///
/// todo: please find a better way.
var/static/list/legacy_stack_provider_material_map = __construct_legacy_stack_provider_material_map()
/// explicit recipes, lazy-list. this is typelist'd
var/list/datum/stack_recipe/explicit_recipes
//! legacy - re-evaluate these at some point!
var/singular_name
/// Determines whether the item should update it's sprites based on amount.
var/no_variants = TRUE
/// skip default / old update_icon() handling
var/skip_legacy_icon_update = FALSE
/// use new update icon system
/// * this is mandatory for all new stacks
var/use_new_icon_update = FALSE
/// Will the item pass its own color var to the created item? Dyed cloth, wood, etc.
var/pass_color = FALSE
/// Will the stack merge with other stacks that are different colors? (Dyed cloth, wood, etc).
var/strict_color_stacking = FALSE
//! end
/obj/item/stack/Initialize(mapload, new_amount, merge = TRUE)
if(has_typelist(explicit_recipes))
explicit_recipes = get_typelist(explicit_recipes)
else
explicit_recipes = typelist(NAMEOF(src, explicit_recipes), generate_explicit_recipes())
if(!isnull(new_amount))
amount = new_amount
// todo: lint this to make sure everyone sets this, don't set in initialize as a safety net
if(!stack_type)
stack_type = type
. = ..()
// only merge 1. outside of mapload and 2. if we had amount
if(merge && !mapload && amount)
for(var/obj/item/stack/S in loc)
if(can_merge_into(S))
merge_into_other(S, TRUE)
// and if we did and no longer have amount, delete ourselves
if(amount == 0)
return INITIALIZE_HINT_QDEL
update_icon()
/obj/item/stack/update_icon_state()
if(!use_new_icon_update)
return ..()
if(!icon_state_count)
return ..()
icon_state = "[base_icon_state || initial(icon_state)]-[get_amount_icon_notch(get_amount())]"
return ..()
/obj/item/stack/update_icon()
. = ..()
if(skip_legacy_icon_update)
return
if(no_variants)
icon_state = initial(icon_state)
else
var/amount = get_amount()
if(amount <= (max_amount * (1/3)))
icon_state = initial(icon_state)
else if (amount <= (max_amount * (2/3)))
icon_state = "[initial(icon_state)]_2"
else
icon_state = "[initial(icon_state)]_3"
item_state = initial(icon_state)
/obj/item/stack/examine(mob/user, dist)
. = ..()
if(!has_stack_provider())
. += "There are [get_amount()] [singular_name]\s in the stack."
else
. += "There are [get_amount()] [singular_name]\s in \the [get_provider_name()]."
/**
* Get the explicit recipes of this stack type
*/
/obj/item/stack/proc/generate_explicit_recipes()
return list()
/obj/item/stack/ui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui)
if(!stackcrafting_makes_sense())
return
ui = SStgui.try_update_ui(user, src, ui)
if(isnull(ui))
ui = new(user, src, "StackCrafting")
ui.open()
/obj/item/stack/ui_static_data(mob/user, datum/tgui/ui)
. = ..()
.["recipes"] = tgui_recipes()
.["maxAmount"] = max_amount
.["name"] = name
/obj/item/stack/proc/stackcrafting_makes_sense()
return length(explicit_recipes)
/obj/item/stack/proc/tgui_recipes()
var/list/assembled = list()
for(var/datum/stack_recipe/recipe as anything in explicit_recipes)
assembled[++assembled.len] = recipe.tgui_recipe_data()
return assembled
/obj/item/stack/ui_data(mob/user, datum/tgui/ui)
. = ..()
.["amount"] = get_amount()
/obj/item/stack/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state, datum/event_args/actor/actor)
. = ..()
if(.)
return
switch(action)
if("craft")
var/recipe_ref = params["recipe"]
if(!istext(recipe_ref))
return TRUE
var/datum/stack_recipe/recipe = locate(recipe_ref)
if(!can_craft_recipe(recipe))
return TRUE
var/make_amount = params["amount"]
craft_recipe(recipe, usr, make_amount)
return TRUE
/obj/item/stack/proc/can_craft_recipe(datum/stack_recipe/recipe)
if(recipe in explicit_recipes)
return TRUE
return FALSE
/obj/item/stack/proc/craft_recipe(datum/stack_recipe/recipe, mob/user, make_amount)
if(make_amount > (isnull(recipe.max_amount)? (recipe.result_is_stack? INFINITY : 1) : recipe.max_amount))
return FALSE
var/needed = recipe.cost * (make_amount / recipe.result_amount)
if(FLOOR(needed, 1) != needed) // no decimals, thank you!
return FALSE
if(needed > get_amount())
return FALSE
var/turf/where = get_turf(user)
if(!do_after(user, recipe.time, src, progress_anchor = user))
return FALSE
if(needed > get_amount())
return FALSE
if(!recipe.craft(where, make_amount, src, user, FALSE, user.dir))
return FALSE
log_stackcrafting(user, src, recipe.name, make_amount, needed, where)
use(needed)
/**
* Can we merge with this stack?
*/
/obj/item/stack/proc/can_merge_into(obj/item/stack/other)
if(!istype(other))
return FALSE
if((strict_color_stacking || other.strict_color_stacking) && (color != other.color))
return FALSE
return can_merge_into_type(other.type)
// todo: deprecated
/obj/item/stack/proc/can_use(used)
return has_amount(used)
// todo: deprecated
/obj/item/stack/proc/use(used, no_delete)
return use_amount(used, no_delete)
// todo: deprecated
/obj/item/stack/proc/add(extra, force)
return give_amount(extra, force)
/**
* The transfer and split procs work differently than use() and add().
* Whereas those procs take no action if the desired amount cannot be added or removed these procs will try to transfer whatever they can.
* They also remove an equal amount from the source stack.
*/
/// Attempts to transfer amount to S, and returns the amount actually transferred.
/obj/item/stack/proc/transfer_to(obj/item/stack/S, tamount=null, type_verified)
if (!get_amount())
return 0
if (!can_merge_into(S) && !type_verified)
return 0
if (isnull(tamount))
tamount = src.get_amount()
var/transfer = max(min(tamount, src.get_amount(), (S.get_max_amount() - S.get_amount())), 0)
var/orig_amount = src.get_amount()
if (transfer && src.use(transfer))
S.add(transfer)
if (prob(transfer/orig_amount * 100))
transfer_fingerprints_to(S)
if(blood_DNA)
S.blood_DNA |= blood_DNA
return transfer
return 0
/// Creates a new stack with the specified amount.
// todo: refactor and combine /change_stack into here
/obj/item/stack/proc/split(tamount, atom/where, force)
if (!amount)
return null
var/transfer = max(min(tamount, src.amount, force? INFINITY : initial(max_amount)), 0)
var/orig_amount = src.amount
if (transfer && src.use(transfer))
var/make_type = isnull(split_type)? type : split_type
var/obj/item/stack/newstack = new make_type(where, transfer, FALSE)
newstack.color = color
if (prob(transfer/orig_amount * 100))
transfer_fingerprints_to(newstack)
if(blood_DNA)
newstack.blood_DNA |= blood_DNA
return newstack
return null
/obj/item/stack/attack_hand(mob/user, datum/event_args/actor/clickchain/e_args)
if(user.get_inactive_held_item() == src)
change_stack(user, 1)
else
return ..()
/obj/item/stack/Crossed(atom/movable/AM)
. = ..()
// if we're in a mob, do not automerge
if(!ismob(loc) && !AM.throwing && can_merge_into(AM))
merge_into_other(AM)
/**
* Merge self into other
*
* * Can delete ourselves.
*
* @return amount merged
*/
/obj/item/stack/proc/merge_into_other(obj/item/stack/other, no_delete)
if(QDELETED(other) || QDELETED(src) || (other == src)) //amusingly this can cause a stack to consume itself, let's not allow that.
return
var/transfer = get_amount()
transfer = min(transfer, other.max_amount - other.amount)
if(pulledby)
pulledby.start_pulling(other)
other.copy_evidences(src)
use(transfer, no_delete)
other.add(transfer)
return transfer
/obj/item/stack/attackby(obj/item/I, mob/living/user, list/params, clickchain_flags, damage_multiplier)
if (istype(I, /obj/item/stack))
var/obj/item/stack/S = I
src.transfer_to(S)
return
return ..()
/obj/item/stack/context_menu_query(datum/event_args/actor/e_args)
. = ..()
.["split"] = create_context_menu_tuple("split", src, 1, MOBILITY_CAN_USE | MOBILITY_CAN_PICKUP, TRUE)
/obj/item/stack/context_menu_act(datum/event_args/actor/e_args, key)
. = ..()
if(.)
return
switch(key)
if("split")
// TODO: e-args support
attempt_split_stack(e_args.initiator)
/obj/item/stack/alt_clicked_on(mob/user, location, control, list/params)
. = ..()
if(.)
return
if(user.Reachability(src) && CHECK_MOBILITY(user, MOBILITY_CAN_PICKUP))
attempt_split_stack(user)
return TRUE
// TODO: THERE HAS TO BE A BETTER FUCKING WAY
/obj/item/stack/should_list_turf_on_alt_click(mob/user)
return FALSE
/obj/item/stack/proc/attempt_split_stack(mob/living/user)
//get amount from user
var/max = get_amount()
var/stackmaterial = tgui_input_number(user, "How many sheets do you wish to take out of this stack?", "Stack", max, max, 1, round_value=TRUE)
if(QDELETED(src))
return
max = get_amount() // Not sure why this is done twice but whatever.
stackmaterial = min(max, stackmaterial)
if(stackmaterial == null || stackmaterial <= 0 || !in_range(user, src) || !CHECK_MOBILITY(user, MOBILITY_CAN_PICKUP))
return TRUE
else
change_stack(user, stackmaterial)
to_chat(user, SPAN_NOTICE("You take [stackmaterial] sheets out of the stack"))
return TRUE
// todo: refactor and combine with /split
/obj/item/stack/proc/change_stack(mob/user, amount)
var/atom/our_current_location = drop_location()
if(!use(amount, TRUE, FALSE))
return FALSE
var/make_type = isnull(split_type)? type : split_type
var/obj/item/stack/F = new make_type(user? user : our_current_location, amount, FALSE)
. = F
F.copy_evidences(src)
if(user)
if(!user.put_in_hands(F, INV_OP_NO_MERGE_STACKS))
F.forceMove(user.drop_location())
add_fingerprint(user)
F.add_fingerprint(user)
/obj/item/stack/proc/copy_evidences(obj/item/stack/from)
if(from.blood_DNA)
blood_DNA = from.blood_DNA.Copy()
if(from.fingerprints)
fingerprints = from.fingerprints.Copy()
if(from.fingerprintshidden)
fingerprintshidden = from.fingerprintshidden.Copy()
if(from.fingerprintslast)
fingerprintslast = from.fingerprintslast
/obj/item/stack/proc/legacy_add_to_stacks_please_refactor_me(mob/user)
for (var/obj/item/stack/item in user.loc)
if (item==src)
continue
var/transfer = src.transfer_to(item)
if (transfer)
to_chat(user, SPAN_NOTICE("You add a new [item.singular_name] to the stack. It now contains [item.amount] [item.singular_name]\s."))
if(!amount)
break
//* Access *//
/**
* Gets how many sheets we have.
*/
/obj/item/stack/proc/get_amount()
if(item_mount)
return check_provider_remaining()
return amount
/**
* Gets how many sheets we can carry.
*/
/obj/item/stack/proc/get_max_amount()
if(item_mount)
return check_provider_capacity()
return max_amount
/**
* Gets how many sheets we can accept.
*/
/obj/item/stack/proc/get_missing_amount()
return get_max_amount() - get_amount()
/**
* Checks if we have an amount.
*
* @return TRUE / FALSE
*/
/obj/item/stack/proc/has_amount(amount)
return check_provider_remaining() >= amount
/**
* Use an amount if we have the whole amount.
*
* @return amount used
*/
/obj/item/stack/proc/use_checked_amount(amount)
if(!has_amount(amount))
return 0
return use_amount(amount)
/**
* Use an amount. Will use less if we don't have that much.
*
* @return amount used
*/
/obj/item/stack/proc/use_amount(amount, no_delete)
if(item_mount)
return pull_from_provider(amount)
if(amount <= 0)
return 0
amount = min(amount, src.amount)
. = amount
src.amount -= amount
update_amount(no_delete)
/**
* Gives a number of sheets back to us.
*
* @return amount added
*/
/obj/item/stack/proc/give_amount(amount, force)
if(item_mount)
return push_to_provider(amount, force)
if(!force)
amount = min(amount, max_amount - src.amount)
if(amount <= 0)
return 0
. = amount
src.amount += amount
update_amount()
/**
* Sets our amount to a specific amount.
*
* * If we use a stack provider, we'll push/pull automatically. This can be weird, because providers don't act the same as stacks.
* * Setting us to 0 will delete us immediately.
*
* @return amount changed
*/
/obj/item/stack/proc/set_amount(new_amount, force)
if(new_amount < 0 || (new_amount != floor(amount)))
stack_trace("Tried to set a bad stack amount: [new_amount]")
return 0
if(item_mount)
var/current_amount = check_provider_remaining()
var/amount_to_push_or_pull = new_amount - current_amount
if(!amount_to_push_or_pull)
return 0
return amount_to_push_or_pull > 0 ? push_to_provider(amount_to_push_or_pull, force) : pull_from_provider(amount_to_push_or_pull)
if(!force)
new_amount = min(amount, max_amount)
. = new_amount - amount
amount = new_amount
update_amount()
/**
* This can destroy the stack.
*/
/obj/item/stack/proc/update_amount(no_delete)
if(QDELING(src))
return
if(item_mount)
else
if(amount <= 0)
if(!no_delete)
qdel(src)
return
update_icon()
//* Getters *//
/**
* Get the number for `iconstate-[n]` icon state rendering.
*
* @return number, or null if `icon_state_count` isn't set.
*/
/obj/item/stack/proc/get_amount_icon_notch(the_amount)
if(!icon_state_count)
return null
return CEILING(the_amount / max_amount * icon_state_count, 1)
//* Stack Providers *//
/obj/item/stack/proc/has_stack_provider()
return !!item_mount
/**
* Get the name of our stack provider.
* * You must check if stack provider exists.
*/
/obj/item/stack/proc/get_provider_name()
return item_mount.stack_get_provider_name(src, null, stack_type)
/**
* * You must check if stack provider exists.
* @return amount left
*/
/obj/item/stack/proc/check_provider_remaining()
var/list/legacy_remap = legacy_stack_provider_material_map[type]
if(legacy_remap)
. = INFINITY
for(var/mat_id in legacy_remap)
. = min(., (item_mount.material_get_amount(src, null, mat_id) / legacy_remap[mat_id]))
else
. = item_mount.stack_get_amount(src, null, stack_type)
/**
* * You must check if stack provider exists.
* @return max volume
*/
/obj/item/stack/proc/check_provider_capacity()
var/list/legacy_remap = legacy_stack_provider_material_map[type]
if(legacy_remap)
. = INFINITY
for(var/mat_id in legacy_remap)
. = min(., (item_mount.material_get_capacity(src, null, mat_id) / legacy_remap[mat_id]))
else
. = item_mount.stack_get_capacity(src, null, stack_type)
/**
* * You must check if stack provider exists.
* @return amount pushed
*/
/obj/item/stack/proc/push_to_provider(amount, force)
var/list/legacy_remap = legacy_stack_provider_material_map[type]
if(legacy_remap)
// we have to be atomic, so do an expensive check first
if(!force)
var/has_remaining_capacity = check_provider_capacity() - check_provider_remaining()
if(has_remaining_capacity <= 0)
return 0
amount = min(amount, has_remaining_capacity)
. = amount
for(var/mat_id in legacy_remap)
item_mount.material_give_amount(src, null, mat_id, amount * legacy_remap[mat_id] * SHEET_MATERIAL_AMOUNT)
else
. = item_mount.stack_give_amount(src, null, stack_type, amount * SHEET_MATERIAL_AMOUNT, force)
/**
* * You must check if stack provider exists.
* @return amount pulled
*/
/obj/item/stack/proc/pull_from_provider(amount)
var/list/legacy_remap = legacy_stack_provider_material_map[type]
if(legacy_remap)
// we have to be atomic, so do an expensive check first
var/has_remaining = check_provider_remaining()
for(var/mat_id in legacy_remap)
item_mount.material_use_amount(src, null, mat_id, has_remaining * legacy_remap[mat_id] * SHEET_MATERIAL_AMOUNT)
else
. = item_mount.stack_use_amount(src, null, stack_type, amount * SHEET_MATERIAL_AMOUNT)
//* Types *//
/**
* Get our 'use as type'.
*/
/obj/item/stack/proc/get_use_as_type()
return stack_type || type
/**
* We can be used as a specific stack type.
*/
/obj/item/stack/proc/can_use_as_type(path)
return stack_type == path || type == path
/**
* Can merge into a type
*/
/obj/item/stack/proc/can_merge_into_type(path)
return path == (stack_type || type)