Files
Bubberstation/code/modules/mob/inventory.dm
df7832aa43 Replaced our NPC AI with Behavior Trees. (#96628)
This PR replaces our current NPC AI with a [behavior tree
system](https://en.wikipedia.org/wiki/Behavior_tree_(artificial_intelligence,_robotics_and_control)).
Behavior trees are a common way of creating AI in which you place nodes
in a tree structure to define what actions an AI should take.

AI controllers defined a list of /datum/ai_planning_subtree types in
behavior_nodes. Each subtree was a self-contained unit that could call
queue_behavior() to fire off /datum/ai_behavior actions. The controller
iterated subtrees in order, each one deciding independently whether to
queue something and deciding whether the next subtree would run.

This has a few issues:
1. There's no real structure; you are just defining a list of things to
try in order.
2. There was a loooot of subtrees that were basically the same as
another but with some slight modification
3. It was hard to understand.

Controllers now define a single json file describing a tree of nodes.
The tree is composed of structural composites:

Sequence - do A, then B, then C (and so on)
Selector - try A, if it fails try B, then C (and so on)
Parallel - run A and B simultaneously, with configurable failure/success
policies and or looping behavior
Subplan - loop a child continiously

Along that we also have "Decorators". These are nodes that basically
check a condition (E.g.; do we have a combat target). These decorators
can be used to gate behavior and are re-useable across behavior trees.
They also have a concept known as "Observers". Which lets them cancel
lower priority behavior in case their condition changes (Which we check
whenever a signal fires that fits that specific decorator). This makes
the AI much more responsive to change in environment.

For behaviors, we still use the ai_behavior datums. These are the actual
behaviors such as "Move to X", "Attack X". The only major change is that
these can no longer sleep() since they now run in the ai_controller.

Lastly, we now also have subtrees, except now they are essentially
pieces of behavior tree that can be re-used, or even overriden at
runtime or as a variable. Allowing for making modular AI made out of
several smaller trees.

You can set variables on these nodes directly via the extension (see
below), which should reduce the need to make subtypes of behaviors by a
lot. All of these vars are saved on the JSON and will be applied at
runtime.

If you are using subtrees, you can also assign "bindings" to these
variables, which will allow instances of the subtree to override those
variables.

Since a tree structure with variables becomes hard to parse in a JSON,
I've made a VSCode extension to edit these JSONs:

https://marketplace.visualstudio.com/items?itemName=BehaviorTreeG.behaviortreeg
https://github.com/CabinetOnFire/BehaviorTreeG

<img width="1795" height="1268" alt="image"
src="https://github.com/user-attachments/assets/56aa2f0b-3cf9-449f-bca4-8281fca82db6"
/>

This extension allows you to edit the behavior tree JSONs, and browse
through all the behaviors/decorators/subtrees we have

If you'd like more info on how to build these AI check out the
learn_ai.md. I will also make a tutorial to go over more depth on what
the system offers because I kind of suck at doing technical write-ups.

Targetting has been changed to. I've made a new acquire_targets behavior
that takes a target_source (what am I targetting) and
targetting_strategy (what does the candidate need to fulfill to be
considered a target). This allows us to make composites targetting
combinations to reduce the amount of specific find_and_set esque
behaviors we had before. Not everything is ported to this system but
that would be a longer term goal.

I've added a new build_bt script that converts all the behavior tree
JSONs into compiled versions. Why is this needed? Because I wanted to
keep using defines in behavior trees, so we need a way to convert this
into literal values before we send it to DM. This script runs on compile
and should also run in CI (If I didn't fuck that up!). This saves to a
new build/ folder.

I've ported every single AI in the game to this system (except raptors,
Kobsa is working on those so should be in soon!), so I do expect some
bugs to come out of this. But I also fixed some issues that have
probably been in the game for a long time such as:
- Fixed penguins being unable to fish
- Fixed bileworms not being able to devour people
- Fixes goldgrubs not grubbing gold (they could not mine!)
- Lizards actually eat food they find

Either way, I'd reccomend a long TM on this.

1. (Hopefully) a better development experience for making AI
2. Less copy-paste for behaviors, we should be able to re-use more
pieces to make behavior
3. Behavior trees is a more common pattern in making AI, so it should be
easier to find resources to find out how to do things.

🆑 CabinetOnFire, Iamgoofball, SmartKar, Ben10omintrix
refactor: Replaces our AI system with behavior trees, porting all
datum/ai to it
/🆑

I will add this PR with more details down the line. I think I got the
big picture but its a big PR, so sorry if I missed something important.

---------

Co-authored-by: Iamgoofball <iamgoofball@gmail.com>
Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com>
Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
Co-authored-by: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com>
Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
2026-08-15 10:33:57 -06:00

740 lines
27 KiB
Plaintext

//These procs handle putting stuff in your hands
//as they handle all relevant stuff like adding it to the player's screen and updating their overlays.
///Returns the thing we're currently holding
/mob/proc/get_active_held_item()
return get_item_for_held_index(active_hand_index)
//Finds the opposite limb for the active one (eg: upper left arm will find the item in upper right arm)
//So we're treating each "pair" of limbs as a team, so "both" refers to them
/mob/proc/get_inactive_held_item()
return get_item_for_held_index(get_inactive_hand_index())
//Finds the opposite index for the active one (eg: upper left arm will find the item in upper right arm)
//So we're treating each "pair" of limbs as a team, so "both" refers to them
/mob/proc/get_inactive_hand_index()
var/other_hand = 0
if(IS_RIGHT_INDEX(active_hand_index))
other_hand = active_hand_index-1 //finding the matching "left" limb
else
other_hand = active_hand_index+1 //finding the matching "right" limb
if(other_hand < 0 || other_hand > held_items.len)
other_hand = 0
return other_hand
/mob/proc/get_item_for_held_index(i)
if(i > 0 && i <= held_items.len)
return held_items[i]
return null
//Odd = left. Even = right
/mob/proc/held_index_to_dir(i)
if(IS_RIGHT_INDEX(i))
return "r"
return "l"
//Check we have an organ for this hand slot (Dismemberment), Only relevant for humans
/mob/proc/has_hand_for_held_index(i)
return TRUE
//Check we have an organ for our active hand slot (Dismemberment),Only relevant for humans
/mob/proc/has_active_hand()
return has_hand_for_held_index(active_hand_index)
//Finds the first available (null) index OR all available (null) indexes in held_items based on a side.
//Lefts: 1, 3, 5, 7...
//Rights:2, 4, 6, 8...
/mob/proc/get_empty_held_index_for_side(side = LEFT_HANDS, all = FALSE)
var/list/empty_indexes = all ? list() : null
for(var/i in (side == LEFT_HANDS) ? 1 : 2 to held_items.len step 2)
if(!held_items[i])
if(!all)
return i
empty_indexes += i
return empty_indexes
//Same as the above, but returns the first or ALL held *ITEMS* for the side
/mob/proc/get_held_items_for_side(side = LEFT_HANDS, all = FALSE)
var/list/holding_items = all ? list() : null
for(var/i in (side == LEFT_HANDS) ? 1 : 2 to held_items.len step 2)
var/obj/item/I = held_items[i]
if(I)
if(!all)
return I
holding_items += I
return holding_items
/mob/proc/get_empty_held_indexes()
var/list/L
for(var/i in 1 to held_items.len)
if(!held_items[i])
LAZYADD(L, i)
return L
/mob/proc/get_held_index_of_item(obj/item/I)
return held_items.Find(I)
/// Returns what body zone is holding the passed item
/mob/proc/get_hand_zone_of_item(obj/item/I)
var/hand_index = get_held_index_of_item(I)
if(!hand_index)
return null
if(IS_RIGHT_INDEX(hand_index))
return BODY_ZONE_R_ARM
return BODY_ZONE_L_ARM
///Find number of held items, multihand compatible
/mob/proc/get_num_held_items()
. = 0
for(var/i in 1 to held_items.len)
if(!held_items[i])
continue
var/obj/item/gripped_item = held_items[i]
if(HAS_TRAIT_FROM(gripped_item, TRAIT_NODROP, HAND_REPLACEMENT_TRAIT)) //prostetic limbs are not held items, they are part of the body.
continue
if(gripped_item.item_flags & ABSTRACT) //not really flavoured as items
continue
.++
//Sad that this will cause some overhead, but the alias seems necessary
//*I* may be happy with a million and one references to "indexes" but others won't be
/mob/proc/is_holding(obj/item/I)
return get_held_index_of_item(I)
//Checks if we're holding an item of type: typepath
/mob/proc/is_holding_item_of_type(typepath)
for(var/obj/item/I in held_items)
if(istype(I, typepath))
return I
return FALSE
// List version of above proc
// Returns ret_item, which is either the successfully located item or null
/mob/proc/is_holding_item_of_types(list/typepaths)
for(var/typepath in typepaths)
var/ret_item = is_holding_item_of_type(typepath)
return ret_item
//Checks if we're holding a tool that has given quality
//Returns the tool that has the best version of this quality
/mob/proc/is_holding_tool_quality(quality)
var/obj/item/best_item
var/best_quality = INFINITY
for(var/obj/item/I in held_items)
if(I.tool_behaviour == quality && I.toolspeed < best_quality)
best_item = I
best_quality = I.toolspeed
return best_item
//To appropriately fluff things like "they are holding [I] in their [get_held_index_name(get_held_index_of_item(I))]"
//Can be overridden to pass off the fluff to something else (eg: science allowing people to add extra robotic limbs, and having this proc react to that
// with say "they are holding [I] in their Nanotrasen Brand Utility Arm - Right Edition" or w/e
/mob/proc/get_held_index_name(i)
var/list/hand = list()
if(i > 2)
hand += "upper "
var/num = 0
if(IS_RIGHT_INDEX(i))
num = i-2
hand += "right hand"
else
num = i-1
hand += "left hand"
num -= (num*0.5)
if(num > 1) //"upper left hand #1" seems weird, but "upper left hand #2" is A-ok
hand += " #[num]"
return hand.Join()
//Returns if a certain item can be equipped to a certain slot.
// Currently invalid for two-handed items - call obj/item/mob_can_equip() instead.
/mob/proc/can_equip(obj/item/I, slot, disable_warning = FALSE, bypass_equip_delay_self = FALSE, ignore_equipped = FALSE, indirect_action = FALSE)
return FALSE
/mob/proc/can_put_in_hand(I, hand_index)
if(hand_index > held_items.len)
return FALSE
if(!put_in_hand_check(I))
return FALSE
if(!has_hand_for_held_index(hand_index))
return FALSE
return !held_items[hand_index]
/mob/proc/put_in_hand(obj/item/I, hand_index, forced = FALSE, ignore_anim = TRUE, visuals_only = FALSE)
if(hand_index == null || !held_items.len || (!forced && !can_put_in_hand(I, hand_index)))
return FALSE
if(isturf(I.loc) && !ignore_anim)
I.do_pickup_animation(src)
if(get_item_for_held_index(hand_index))
dropItemToGround(get_item_for_held_index(hand_index), force = TRUE)
I.forceMove(src) //this has to come before has_equipped() is called
held_items[hand_index] = I
SET_PLANE_EXPLICIT(I, ABOVE_HUD_PLANE, src)
if(I.pulledby)
I.pulledby.stop_pulling()
if(!has_equipped(I, ITEM_SLOT_HANDS, initial = visuals_only))
return FALSE
update_held_items()
I.pixel_x = I.base_pixel_x
I.pixel_y = I.base_pixel_y
if(QDELETED(I)) // this is here because some ABSTRACT items like slappers and circle hands could be moved from hand to hand then delete, which meant you'd have a null in your hand until you cleared it (say, by dropping it)
held_items[hand_index] = null
return FALSE
SEND_SIGNAL(I, COMSIG_ITEM_ENTERED_HANDS, src, hand_index)
return hand_index
//Puts the item into the first available left hand if possible and calls all necessary triggers/updates. returns 1 on success.
/mob/proc/put_in_l_hand(obj/item/I, visuals_only = FALSE)
return put_in_hand(I, get_empty_held_index_for_side(LEFT_HANDS), visuals_only = visuals_only)
//Puts the item into the first available right hand if possible and calls all necessary triggers/updates. returns 1 on success.
/mob/proc/put_in_r_hand(obj/item/I, visuals_only = FALSE)
return put_in_hand(I, get_empty_held_index_for_side(RIGHT_HANDS), visuals_only = visuals_only)
/mob/proc/put_in_hand_check(obj/item/I)
return FALSE //nonliving mobs don't have hands
/mob/living/put_in_hand_check(obj/item/I)
if(istype(I) && ((mobility_flags & MOBILITY_PICKUP) || (I.item_flags & ABSTRACT)) \
&& !(SEND_SIGNAL(src, COMSIG_LIVING_TRY_PUT_IN_HAND, I) & COMPONENT_LIVING_CANT_PUT_IN_HAND))
return TRUE
return FALSE
//Puts the item into our active hand if possible. returns TRUE on success.
/mob/proc/put_in_active_hand(obj/item/I, forced = FALSE, ignore_animation = TRUE, visuals_only = FALSE)
return put_in_hand(I, active_hand_index, forced, ignore_animation, visuals_only)
//Puts the item into our inactive hand if possible, returns TRUE on success
/mob/proc/put_in_inactive_hand(obj/item/I, forced = FALSE, visuals_only = FALSE)
return put_in_hand(I, get_inactive_hand_index(), forced, visuals_only = visuals_only)
//Puts the item our active hand if possible. Failing that it tries other hands. Returns TRUE on success.
//If both fail it drops it on the floor (or nearby tables if germ sensitive) and returns FALSE.
//This is probably the main one you need to know :)
/mob/proc/put_in_hands(obj/item/I, del_on_fail = FALSE, merge_stacks = TRUE, forced = FALSE, ignore_animation = TRUE, visuals_only = FALSE)
if(QDELETED(I))
return FALSE
// If the item is a stack and we're already holding a stack then merge
if (isstack(I))
var/obj/item/stack/item_stack = I
var/obj/item/stack/active_stack = get_active_held_item()
if (item_stack.is_zero_amount(delete_if_zero = TRUE))
return FALSE
if (merge_stacks)
if (istype(active_stack) && active_stack.can_merge(item_stack, inhand = TRUE))
if (item_stack.merge(active_stack))
to_chat(usr, span_notice("Your [active_stack.name] stack now contains [active_stack.get_amount()] [active_stack.singular_name]\s."))
return TRUE
else
var/obj/item/stack/inactive_stack = get_inactive_held_item()
if (istype(inactive_stack) && inactive_stack.can_merge(item_stack, inhand = TRUE))
if (item_stack.merge(inactive_stack))
to_chat(usr, span_notice("Your [inactive_stack.name] stack now contains [inactive_stack.get_amount()] [inactive_stack.singular_name]\s."))
return TRUE
if(put_in_active_hand(I, forced, ignore_animation, visuals_only))
return TRUE
var/hand = get_empty_held_index_for_side(LEFT_HANDS)
if(!hand)
hand = get_empty_held_index_for_side(RIGHT_HANDS)
if(hand)
if(put_in_hand(I, hand, forced, ignore_animation, visuals_only))
return TRUE
if(del_on_fail)
qdel(I)
return FALSE
// Failed to put in hands - drop the item
var/atom/location = drop_location()
// Try dropping on nearby tables if germ sensitive (except table behind you)
if(HAS_TRAIT(I, TRAIT_GERM_SENSITIVE))
var/list/dirs = list( // All dirs in clockwise order
NORTH,
NORTHEAST,
EAST,
SOUTHEAST,
SOUTH,
SOUTHWEST,
WEST,
NORTHWEST,
)
var/dir_count = dirs.len
var/facing_dir_index = dirs.Find(dir)
var/cw_index = facing_dir_index
var/ccw_index = facing_dir_index
var/list/turfs_ordered = list(get_step(src, dir))
// Build ordered list of turfs starting from the front facing
for(var/i in 1 to ROUND_UP(dir_count/2) - 1)
cw_index++
if(cw_index > dir_count)
cw_index = 1
turfs_ordered += get_step(src, dirs[cw_index]) // Add next tile on your right
ccw_index--
if(ccw_index <= 0)
ccw_index = dir_count
turfs_ordered += get_step(src, dirs[ccw_index]) // Add next tile on your left
// Check tables on these turfs
for(var/turf in turfs_ordered)
if(locate(/obj/structure/table) in turf || locate(/obj/structure/rack) in turf || locate(/obj/machinery/icecream_vat) in turf)
location = turf
break
transferItemToLoc(I, location, force = TRUE, silent = TRUE, animated = !ignore_animation)
return FALSE
/// Returns true if a mob is holding something
/mob/proc/is_holding_items()
return !!locate(/obj/item) in held_items
/**
* Returns a list of all dropped held items.
* If none were dropped, returns an empty list.
*/
/mob/proc/drop_all_held_items()
. = list()
for(var/obj/item/I in held_items)
. |= dropItemToGround(I)
//Here lie drop_from_inventory and before_item_take, already forgotten and not missed.
/mob/proc/canUnEquip(obj/item/I, force)
if(!I)
return TRUE
if(HAS_TRAIT(I, TRAIT_NODROP) && !force)
return FALSE
return TRUE
/mob/proc/putItemFromInventoryInHandIfPossible(obj/item/I, hand_index, force_removal = FALSE)
if(!can_put_in_hand(I, hand_index))
return FALSE
if(!temporarilyRemoveItemFromInventory(I, force_removal))
return FALSE
I.remove_item_from_storage(src)
if(!put_in_hand(I, hand_index))
qdel(I)
CRASH("Assertion failure: putItemFromInventoryInHandIfPossible") //should never be possible
return TRUE
//The following functions are the same save for one small difference
/**
* Used to drop an item (if it exists) to the ground.
* * Will return null if the item wasn't dropped.
* * If it was, returns the item.
* If the item can be dropped, it will be forceMove()'d to the ground and the turf's Entered() will be called.
*/
/mob/proc/dropItemToGround(obj/item/to_drop, force = FALSE, silent = FALSE, invdrop = TRUE)
if(isnull(to_drop))
return
var/x_offset = 0
var/y_offset = 0
if(!(to_drop.item_flags & NO_PIXEL_RANDOM_DROP))
x_offset += rand(-6, 6)
y_offset += rand(-6, 6)
if(!transfer_item_to_turf(to_drop, drop_location(), x_offset, y_offset, force, silent, invdrop))
return
return to_drop
/// Unequips and transfers an item to a given turf, if possible.
/mob/proc/transfer_item_to_turf(
obj/item/to_transfer,
turf/new_loc,
x_offset = 0,
y_offset = 0,
force = FALSE,
silent = FALSE,
drop_item_inventory = TRUE,
)
SEND_SIGNAL(src, COMSIG_MOB_DROPPING_ITEM)
if(!doUnEquip(to_transfer, force, new_loc, no_move = FALSE, invdrop = drop_item_inventory, silent = silent))
return FALSE
if(QDELETED(to_transfer)) // Some items may get deleted upon getting unequipped.
return FALSE
to_transfer.pixel_x = to_transfer.base_pixel_x + x_offset
to_transfer.pixel_y = to_transfer.base_pixel_y + y_offset
to_transfer.do_drop_animation(src)
return TRUE
//for when the item will be immediately placed in a loc other than the ground
/mob/proc/transferItemToLoc(obj/item/I, newloc = null, force = FALSE, silent = TRUE, animated = null)
. = doUnEquip(I, force, newloc, FALSE, silent = silent)
if(!.)
return
// This proc wears a lot of hats for moving items around in different ways,
// so we assume unhandled cases for checking to animate can safely be handled
// with the same logic we handle animating putting items in container (container on your person isn't animated)
if(isnull(animated))
// If the item's ultimate location is us, we don't animate putting it wherever
animated = !(get(newloc, /mob) == src)
if(animated)
I.do_pickup_animation(newloc, src)
//visibly unequips I but it is NOT MOVED AND REMAINS IN SRC, newloc is for signal handling checks only which hints where you want to move the object after removal
//item MUST BE FORCEMOVE'D OR QDEL'D
/mob/proc/temporarilyRemoveItemFromInventory(obj/item/item_dropping, force = FALSE, idrop = TRUE, atom/newloc = src)
return doUnEquip(item_dropping, force, newloc, TRUE, idrop, silent = TRUE)
/**
* ## doUnEquip
* First and most importantly, DO NOT CALL THIS PROC
* Use one of the above 4 helper procs instead!!
* you may override it, but do not modify the args.
* Returns TRUE if it managed to unequip, FALSE if it can't.
* Args:
* item_dropping - The item that we're unequipping.
* force - overrides TRAIT_NODROP for things like wizarditis and admin undress.
* newloc - The location we're dropping the item into, this could be a turf, storage, mob, etc.
* no_move - if the item is just gonna be immediately moved afterward
* invdrop - Arg passed to signals, prevents stuff in pockets dropping. Only set to false if it's going to immediately be replaced
* silent - Arg passed to dropped() and signals, muting things like drop sound.
*/
/mob/proc/doUnEquip(obj/item/item_dropping, force, atom/newloc, no_move, invdrop = TRUE, silent = FALSE)
PROTECTED_PROC(TRUE)
if(!item_dropping) //If there's nothing to drop, the drop is automatically successful. If(unEquip) should generally be used to check for TRAIT_NODROP.
return TRUE
if(HAS_TRAIT(item_dropping, TRAIT_NODROP) && !force)
return FALSE
if((SEND_SIGNAL(item_dropping, COMSIG_ITEM_PRE_UNEQUIP, force, newloc, no_move, invdrop, silent) & COMPONENT_ITEM_BLOCK_UNEQUIP) && !force)
return FALSE
var/hand_index = get_held_index_of_item(item_dropping)
if(hand_index)
held_items[hand_index] = null
update_held_items()
if(!item_dropping)
return FALSE
client?.screen -= item_dropping
for(var/mob/dead/observe as anything in observers)
observe.client?.screen -= item_dropping
item_dropping.layer = initial(item_dropping.layer)
SET_PLANE_EXPLICIT(item_dropping, initial(item_dropping.plane), newloc)
item_dropping.appearance_flags &= ~NO_CLIENT_COLOR
item_dropping.item_flags &= ~IN_INVENTORY //This has to come before MoveToNullspace/forceMove is called
if(!no_move && !(item_dropping.item_flags & DROPDEL)) //item may be moved/qdel'd immedietely, don't bother moving it
if (isnull(newloc))
item_dropping.moveToNullspace()
else
item_dropping.forceMove(newloc)
has_unequipped(item_dropping, silent)
SEND_SIGNAL(item_dropping, COMSIG_ITEM_POST_UNEQUIP, force, newloc, no_move, invdrop, silent)
SEND_SIGNAL(src, COMSIG_MOB_UNEQUIPPED_ITEM, item_dropping, force, newloc, no_move, invdrop, silent)
return TRUE
/**
* Used to return a list of equipped items on a mob; does not include held items (use get_all_gear)
*
* Argument(s):
* * Optional - include_flags, (see obj.flags.dm) describes which optional things to include or not (pockets, accessories, held items)
*/
/mob/proc/get_equipped_items(include_flags = NONE)
var/list/items = list()
for(var/obj/item/item_contents in contents)
if(item_contents.item_flags & IN_INVENTORY)
if(!(include_flags & INCLUDE_PROSTHETICS) && HAS_TRAIT_FROM(item_contents, TRAIT_NODROP, HAND_REPLACEMENT_TRAIT)) //prostetic limbs are not equipped items, they are part of the body.
continue
if(!(include_flags & INCLUDE_ABSTRACT) && (item_contents.item_flags & ABSTRACT)) //not really flavoured as items
continue
items += item_contents
if (!(include_flags & INCLUDE_HELD))
items -= held_items
return items
///Get all items in our possession that should affect our movespeed
/mob/proc/get_equipped_speed_mod_items()
. = get_equipped_items(INCLUDE_ABSTRACT|INCLUDE_PROSTHETICS)
for(var/obj/item/thing in held_items)
if(thing.item_flags & SLOWS_WHILE_IN_HAND)
. += thing
/**
* Returns the items that were successfully unequipped.
*/
/mob/living/proc/unequip_everything()
var/list/items = list()
items |= get_equipped_items(INCLUDE_POCKETS)
// In case something isn't actually unequipped somehow
var/list/dropped_items = list()
for(var/I in items)
var/return_val = dropItemToGround(I)
if(!isitem(return_val))
continue
dropped_items |= return_val
var/return_val = drop_all_held_items()
if(islist(return_val))
dropped_items |= return_val
return dropped_items
/**
* Try to equip an item to a slot on the mob
*
* This is a SAFE proc. Use this instead of equip_to_slot()!
*
* set qdel_on_fail to have it delete W if it fails to equip
*
* set disable_warning to disable the 'you are unable to equip that' warning.
*
* unset redraw_mob to prevent the mob icons from being redrawn at the end.
*
* Initial is used to indicate whether or not this is the initial equipment (job datums etc) or just a player doing it
*
* set indirect_action to allow insertions into "soft" locked objects, things that are easily opened by the owning mob
*/
/mob/proc/equip_to_slot_if_possible(obj/item/W, slot, qdel_on_fail = FALSE, disable_warning = FALSE, redraw_mob = TRUE, bypass_equip_delay_self = FALSE, initial = FALSE, indirect_action = FALSE)
if(!istype(W) || QDELETED(W)) //This qdeleted is to prevent stupid behavior with things that qdel during init, like say stacks
return FALSE
if(!W.mob_can_equip(src, slot, disable_warning, bypass_equip_delay_self, indirect_action = indirect_action))
if(qdel_on_fail)
qdel(W)
else if(!disable_warning)
to_chat(src, span_warning("You are unable to equip that!"))
return FALSE
equip_to_slot(W, slot, initial, redraw_mob, indirect_action = indirect_action) //This proc should not ever fail.
SEND_SIGNAL(src, COMSIG_MOB_POST_EQUIP, W, slot) //BUBBER EDIT ADDITION
return TRUE
/**
* Actually equips an item to a slot (UNSAFE)
*
* This is an UNSAFE proc. It merely handles the actual job of equipping. All the checks on
* whether you can or can't equip need to be done before! Use mob_can_equip() for that task.
*
*In most cases you will want to use equip_to_slot_if_possible()
*/
/mob/proc/equip_to_slot(obj/item/equipping, slot, initial = FALSE, redraw_mob = FALSE, indirect_action = FALSE)
return
/// This proc is called after an item has been successfully handled and equipped to a slot.
/mob/proc/has_equipped(obj/item/item, slot, initial = FALSE)
SHOULD_CALL_PARENT(TRUE)
item.item_flags |= IN_INVENTORY
. = item.on_equipped(src, slot, initial)
if(.)
update_equipment_speed_mods()
/// This proc is called after an item has been removed from a mob but before it has been officially deslotted.
/mob/proc/has_unequipped(obj/item/item, silent = FALSE)
SHOULD_CALL_PARENT(TRUE)
item.dropped(src, silent)
update_equipment_speed_mods()
return TRUE
/**
* Equip an item to the slot or delete
*
* This is just a commonly used configuration for the equip_to_slot_if_possible() proc, used to
* equip people when the round starts and when events happen and such.
*
* Also bypasses equip delay checks, since the mob isn't actually putting it on.
* Initial is used to indicate whether or not this is the initial equipment (job datums etc) or just a player doing it
* set indirect_action to allow insertions into "soft" locked objects, things that are easily opened by the owning mob
*/
/mob/proc/equip_to_slot_or_del(obj/item/W, slot, initial = FALSE, indirect_action = FALSE)
return equip_to_slot_if_possible(W, slot, TRUE, TRUE, FALSE, TRUE, initial, indirect_action)
/**
* Auto equip the passed in item the appropriate slot based on equipment priority
*
* puts the item "W" into an appropriate slot in a human's inventory
*
* returns 0 if it cannot, 1 if successful
*/
/mob/proc/equip_to_appropriate_slot(obj/item/W, qdel_on_fail = FALSE, indirect_action = FALSE, blacklist, initial) // SKYRAT EDIT CHANGE - ORIGINAL: /mob/proc/equip_to_appropriate_slot(obj/item/W, qdel_on_fail = FALSE, indirect_action = FALSE)
if(!istype(W))
return FALSE
var/slot_priority = W.slot_equipment_priority
if(!slot_priority)
slot_priority = list( \
ITEM_SLOT_BACK, ITEM_SLOT_ID,\
ITEM_SLOT_ICLOTHING, ITEM_SLOT_OCLOTHING,\
ITEM_SLOT_MASK, ITEM_SLOT_HEAD, ITEM_SLOT_NECK,\
ITEM_SLOT_FEET, ITEM_SLOT_GLOVES,\
ITEM_SLOT_EARS, ITEM_SLOT_EYES,\
ITEM_SLOT_BELT, ITEM_SLOT_SUITSTORE,\
ITEM_SLOT_LPOCKET, ITEM_SLOT_RPOCKET,\
ITEM_SLOT_DEX_STORAGE\
)
// SKYRAT EDIT ADDITION START - CUSTOMIZATION
if (blacklist)
slot_priority -= blacklist
// SKYRAT EDIT ADDITION END
for(var/slot in slot_priority)
if(equip_to_slot_if_possible(W, slot, disable_warning = TRUE, redraw_mob = TRUE, indirect_action = indirect_action, initial = initial)) // SKYRAT EDIT CHANGE - ORIGINAL: if(equip_to_slot_if_possible(W, slot, disable_warning = TRUE, redraw_mob = TRUE, indirect_action = indirect_action))
return TRUE
if(qdel_on_fail)
qdel(W)
return FALSE
/// Tries to equip an item, store it in open storage, or in next best storage
/obj/item/proc/equip_to_best_slot(mob/user)
if(user.equip_to_appropriate_slot(src))
user.update_held_items()
return TRUE
else
if(equip_delay_self)
return
if(user.active_storage?.attempt_insert(src, user, messages = FALSE))
return TRUE
var/static/list/equip_priorities = list(
ITEM_SLOT_BELT,
ITEM_SLOT_BACK,
ITEM_SLOT_DEX_STORAGE,
ITEM_SLOT_OCLOTHING,
ITEM_SLOT_ICLOTHING,
)
var/list/possible_storages = user.held_items.Copy()
var/obj/item/active_held = user.get_active_held_item()
possible_storages -= active_held
if(active_held != src)
// If something else is equipping us, just in case, do it into the held item
possible_storages.Insert(1, active_held)
for(var/slot in equip_priorities)
possible_storages += user.get_item_by_slot(slot)
for(var/obj/item/gear as anything in possible_storages)
if(gear?.atom_storage?.attempt_insert(src, user, messages = FALSE))
return TRUE
to_chat(user, span_warning("You are unable to equip that!"))
return FALSE
/// Attempts to put an item into storage located in a given slot
/// indirect_action - ignore "soft-locked" storages that can be easily opened
/// del_on_fail - delete the item upon failure
/mob/proc/equip_to_storage(obj/item/item, slot, indirect_action = FALSE, del_on_fail = FALSE, initial = FALSE)
var/obj/item/worn_item = get_item_by_slot(slot)
if (worn_item?.atom_storage?.attempt_insert(item, src, override = TRUE, force = indirect_action ? STORAGE_SOFT_LOCKED : STORAGE_NOT_LOCKED, messages = FALSE))
return TRUE
if (del_on_fail)
qdel(item)
return FALSE
GAME_VERB_HIDDEN(/mob, quick_equip, "quick-equip")
DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(execute_quick_equip)))
///proc extender of [/mob/verb/quick_equip] used to make the verb queuable if the server is overloaded
/mob/proc/execute_quick_equip()
var/obj/item/I = get_active_held_item()
if(!I)
to_chat(src, span_warning("You are not holding anything to equip!"))
return
if(!QDELETED(I))
I.equip_to_best_slot(src)
//used in code for items usable by both carbon and drones, this gives the proper back slot for each mob.(defibrillator, backpack watertank, ...)
/mob/proc/getBackSlot()
return ITEM_SLOT_BACK
//Inventory.dm is -kind of- an ok place for this I guess
//This is NOT for dismemberment, as the user still technically has 2 "hands"
//This is for multi-handed mobs, such as a human with a third limb installed
//This is a very rare proc to call (besides admin fuckery) so
//any cost it has isn't a worry
/mob/proc/change_number_of_hands(amt)
if(amt < held_items.len)
for(var/i in held_items.len to amt step -1)
dropItemToGround(held_items[i])
held_items.len = amt
if(!hud_used)
return
hud_used.build_hand_slots(update_hud = TRUE)
var/atom/movable/screen/healthdoll/doll = hud_used.screen_objects[HUD_MOB_HEALTHDOLL]
if(doll)
doll.update_body_zones()
//GetAllContents that is reasonable and not stupid
/mob/living/proc/get_all_gear(equipment_flags = INCLUDE_ACCESSORIES|INCLUDE_PROSTHETICS, recursive = TRUE)
var/list/processing_list = get_equipped_items(INCLUDE_POCKETS|INCLUDE_HELD|equipment_flags)
list_clear_nulls(processing_list) // handles empty hands
var/i = 0
while(i < length(processing_list))
var/atom/A = processing_list[++i]
if(A.atom_storage && recursive)
processing_list += A.atom_storage.return_inv()
return processing_list
/// Returns a list of things that the provided mob has, including any storage-capable implants.
/mob/living/proc/gather_belongings(equipment_flags = INCLUDE_ACCESSORIES|INCLUDE_PROSTHETICS, recursive = TRUE)
var/list/belongings = get_all_gear(equipment_flags, recursive)
for (var/obj/item/implant/storage/internal_bag in implants)
belongings += internal_bag.contents
return belongings
/// Safely drop everything, without deconstructing the mob
/mob/living/proc/drop_everything(del_on_drop, force, del_if_nodrop)
. = list() //list of items that were successfully dropped
var/list/all_gear = get_all_gear(INCLUDE_ACCESSORIES, recursive = FALSE)
for(var/obj/item/item in all_gear)
if(dropItemToGround(item, force))
if(QDELETED(item)) //DROPDEL can cause this item to be deleted
continue
if(del_on_drop)
qdel(item)
continue
. += item
else if(del_if_nodrop)
qdel(item)
/**
* Iterates over all contents of the mob to find all items with a cell (or loose cells)
* Useful instead of iterating contents for cells, as it recurses storage and avoids returning abstract cells (like Ethereals)
*
* * max_percent: The maximum charge percent (0.0-1.0) the cell can have to be included in the results
*
* Returns an assoc list of item - its cell
*/
/mob/living/proc/get_all_cells(max_percent = 1.0)
var/list/cell_items = list()
for(var/obj/item/stored in get_all_gear())
var/obj/item/stock_parts/power_store/stored_cell = stored.get_cell()
if(stored_cell && stored_cell.charge <= (stored_cell.maxcharge * max_percent))
cell_items[stored] = stored_cell
return cell_items