Merge remote-tracking branch 'tgstation/master' into upstream-feb12-2026

# Conflicts:
#	.github/CODEOWNERS
#	.github/workflows/compile_changelogs.yml
#	.github/workflows/stale.yml
#	SQL/database_changelog.md
#	_maps/map_files/CatwalkStation/CatwalkStation_2023.dmm
#	code/__DEFINES/atom_hud.dm
#	code/__DEFINES/inventory.dm
#	code/__DEFINES/mobs.dm
#	code/__DEFINES/species_clothing_paths.dm
#	code/__DEFINES/subsystems.dm
#	code/__DEFINES/surgery.dm
#	code/__HELPERS/global_lists.dm
#	code/_globalvars/lists/maintenance_loot.dm
#	code/_globalvars/traits/_traits.dm
#	code/controllers/subsystem/minor_mapping.dm
#	code/controllers/subsystem/processing/quirks.dm
#	code/controllers/subsystem/shuttle.dm
#	code/datums/components/palette.dm
#	code/datums/components/surgery_initiator.dm
#	code/datums/diseases/advance/advance.dm
#	code/datums/hud.dm
#	code/datums/mood.dm
#	code/datums/mutations/chameleon.dm
#	code/datums/quirks/negative_quirks/nyctophobia.dm
#	code/datums/status_effects/debuffs/debuffs.dm
#	code/datums/status_effects/debuffs/drunk.dm
#	code/datums/status_effects/debuffs/slime/slime_leech.dm
#	code/datums/weather/weather.dm
#	code/game/data_huds.dm
#	code/game/objects/items.dm
#	code/game/objects/items/devices/scanners/health_analyzer.dm
#	code/game/objects/items/frog_statue.dm
#	code/game/objects/items/rcd/RLD.dm
#	code/game/objects/items/robot/items/hypo.dm
#	code/game/objects/items/stacks/medical.dm
#	code/game/objects/items/stacks/wrap.dm
#	code/game/objects/items/storage/garment.dm
#	code/game/objects/items/tools/medical/defib.dm
#	code/game/objects/items/weaponry.dm
#	code/game/objects/items/weaponry/melee/misc.dm
#	code/game/objects/structures/crates_lockers/closets/secure/security.dm
#	code/game/objects/structures/curtains.dm
#	code/game/objects/structures/dresser.dm
#	code/game/objects/structures/girders.dm
#	code/game/objects/structures/maintenance.dm
#	code/game/objects/structures/mirror.dm
#	code/modules/admin/greyscale_modify_menu.dm
#	code/modules/admin/verbs/light_debug.dm
#	code/modules/antagonists/ashwalker/ashwalker.dm
#	code/modules/antagonists/heretic/knowledge/starting_lore.dm
#	code/modules/antagonists/ninja/ninjaDrainAct.dm
#	code/modules/art/paintings.dm
#	code/modules/client/preferences.dm
#	code/modules/client/verbs/ooc.dm
#	code/modules/clothing/head/wig.dm
#	code/modules/events/disease_outbreak.dm
#	code/modules/holodeck/holo_effect.dm
#	code/modules/jobs/job_types/head_of_security.dm
#	code/modules/jobs/job_types/security_officer.dm
#	code/modules/library/skill_learning/generic_skillchips/point.dm
#	code/modules/mining/lavaland/ash_flora.dm
#	code/modules/mining/lavaland/mining_loot/megafauna/ash_drake.dm
#	code/modules/mob/dead/new_player/new_player.dm
#	code/modules/mob/living/basic/guardian/guardian.dm
#	code/modules/mob/living/basic/space_fauna/space_dragon/space_dragon.dm
#	code/modules/mob/living/carbon/carbon.dm
#	code/modules/mob/living/carbon/human/human.dm
#	code/modules/mob/living/carbon/human/human_defines.dm
#	code/modules/mob/living/carbon/life.dm
#	code/modules/mob/living/living.dm
#	code/modules/mob/living/living_defines.dm
#	code/modules/mob/mob.dm
#	code/modules/mob_spawn/ghost_roles/mining_roles.dm
#	code/modules/mod/mod_control.dm
#	code/modules/mod/modules/modules_general.dm
#	code/modules/modular_computers/computers/item/computer_ui.dm
#	code/modules/paperwork/paper.dm
#	code/modules/paperwork/paperbin.dm
#	code/modules/power/lighting/light.dm
#	code/modules/projectiles/guns/energy/kinetic_accelerator.dm
#	code/modules/projectiles/projectile.dm
#	code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
#	code/modules/reagents/chemistry/reagents/food_reagents.dm
#	code/modules/reagents/chemistry/reagents/other_reagents.dm
#	code/modules/reagents/chemistry/reagents/pyrotechnic_reagents.dm
#	code/modules/research/xenobiology/crossbreeding/_clothing.dm
#	code/modules/research/xenobiology/crossbreeding/prismatic.dm
#	code/modules/surgery/advanced/brainwashing.dm
#	code/modules/surgery/advanced/lobotomy.dm
#	code/modules/surgery/amputation.dm
#	code/modules/surgery/blood_filter.dm
#	code/modules/surgery/bodyparts/_bodyparts.dm
#	code/modules/surgery/brain_surgery.dm
#	code/modules/surgery/cavity_implant.dm
#	code/modules/surgery/coronary_bypass.dm
#	code/modules/surgery/gastrectomy.dm
#	code/modules/surgery/healing.dm
#	code/modules/surgery/limb_augmentation.dm
#	code/modules/surgery/organ_manipulation.dm
#	code/modules/surgery/revival.dm
#	code/modules/surgery/sleeper_protocol.dm
#	code/modules/surgery/surgery_helpers.dm
#	code/modules/surgery/surgery_step.dm
#	code/modules/unit_tests/_unit_tests.dm
#	code/modules/unit_tests/designs.dm
#	code/modules/unit_tests/icon_state_worn.dm
#	code/modules/unit_tests/screenshots/screenshot_antag_icons_cultist.png
#	code/modules/unit_tests/screenshots/screenshot_antag_icons_headrevolutionary.png
#	code/modules/unit_tests/screenshots/screenshot_antag_icons_provocateur.png
#	code/modules/unit_tests/screenshots/screenshot_husk_body.png
#	code/modules/unit_tests/screenshots/screenshot_husk_body_missing_limbs.png
#	icons/map_icons/clothing/head/_head.dmi
#	icons/map_icons/clothing/shoes.dmi
#	icons/map_icons/items/_item.dmi
#	icons/mob/huds/hud.dmi
#	icons/mob/inhands/64x64_lefthand.dmi
#	icons/mob/inhands/64x64_righthand.dmi
#	icons/obj/machines/computer.dmi
#	tgui/packages/tgui/interfaces/OperatingComputer.jsx
#	tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferences/MainPage.tsx
#	tgui/packages/tgui/interfaces/PreferencesMenu/types.ts
#	tgui/packages/tgui/interfaces/SurgeryInitiator.tsx
#	tools/icon_cutter/check.py
This commit is contained in:
nevimer
2026-02-12 23:50:09 -05:00
2374 changed files with 81136 additions and 52767 deletions
@@ -1,110 +0,0 @@
///Influences the difficulty of the minigame when worn or if buckled to.
/datum/component/adjust_fishing_difficulty
///The additive numerical modifier to the difficulty of the minigame
var/modifier
///For items, in which slot it has to be worn to influence the difficulty of the minigame
var/slots
/datum/component/adjust_fishing_difficulty/Initialize(modifier, slots = NONE)
if(!ismovable(parent) || !modifier)
return COMPONENT_INCOMPATIBLE
if(!isitem(parent))
var/atom/movable/movable_parent = parent
if(!movable_parent.can_buckle)
return COMPONENT_INCOMPATIBLE
src.modifier = modifier
src.slots = slots
/datum/component/adjust_fishing_difficulty/RegisterWithParent()
if(isitem(parent))
RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(on_equipped))
RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(on_dropped))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_item_examine))
else
RegisterSignal(parent, COMSIG_MOVABLE_BUCKLE, PROC_REF(on_buckle))
RegisterSignal(parent, COMSIG_MOVABLE_UNBUCKLE, PROC_REF(on_unbuckle))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_buckle_examine))
update_check()
/datum/component/adjust_fishing_difficulty/UnregisterFromParent()
UnregisterSignal(parent, list(
COMSIG_ATOM_EXAMINE,
COMSIG_MOVABLE_BUCKLE,
COMSIG_MOVABLE_UNBUCKLE,
COMSIG_ITEM_EQUIPPED,
COMSIG_ITEM_DROPPED,
))
update_check(TRUE)
/datum/component/adjust_fishing_difficulty/proc/update_check(removing = FALSE)
var/atom/movable/movable_parent = parent
for(var/mob/living/buckled_mob as anything in movable_parent.buckled_mobs)
update_user(buckled_mob, removing)
if(!isitem(movable_parent) || !isliving(movable_parent.loc))
return
var/mob/living/holder = movable_parent.loc
var/obj/item/item = parent
if(holder.get_slot_by_item(movable_parent) & (slots || item.slot_flags))
update_user(holder, removing)
/datum/component/adjust_fishing_difficulty/proc/on_item_examine(obj/item/item, mob/user, list/examine_text)
SIGNAL_HANDLER
if(!HAS_MIND_TRAIT(user, TRAIT_EXAMINE_FISH))
return
var/method = "[(slots || item.slot_flags) & ITEM_SLOT_HANDS ? "Holding" : "Wearing"] [item.p_them()]"
add_examine_line(user, examine_text, method)
/datum/component/adjust_fishing_difficulty/proc/on_buckle_examine(atom/movable/source, mob/user, list/examine_text)
SIGNAL_HANDLER
if(!HAS_MIND_TRAIT(user, TRAIT_EXAMINE_FISH))
return
add_examine_line(user, examine_text, "Buckling to [source.p_them()]")
/datum/component/adjust_fishing_difficulty/proc/add_examine_line(mob/user, list/examine_text, method)
var/percent = HAS_MIND_TRAIT(user, TRAIT_EXAMINE_DEEPER_FISH) ? "[abs(modifier)]% " : ""
var/text = "[method] will make fishing [percent][modifier < 0 ? "easier" : "harder"]."
if(modifier < 0)
examine_text += span_nicegreen(text)
else
examine_text += span_danger(text)
/datum/component/adjust_fishing_difficulty/proc/on_buckle(atom/movable/source, mob/living/buckled_mob, forced)
SIGNAL_HANDLER
update_user(buckled_mob)
/datum/component/adjust_fishing_difficulty/proc/on_unbuckle(atom/movable/source, mob/living/buckled_mob, forced)
SIGNAL_HANDLER
update_user(buckled_mob, TRUE)
/datum/component/adjust_fishing_difficulty/proc/on_equipped(obj/item/source, mob/living/wearer, slot)
SIGNAL_HANDLER
if(slot & (slots || source.slot_flags))
update_user(wearer)
/datum/component/adjust_fishing_difficulty/proc/on_dropped(obj/item/source, mob/living/dropper)
SIGNAL_HANDLER
update_user(dropper, TRUE)
/datum/component/adjust_fishing_difficulty/proc/update_user(mob/living/user, removing = FALSE)
var/datum/fishing_challenge/challenge = GLOB.fishing_challenges_by_user[user]
if(removing)
UnregisterSignal(user, COMSIG_MOB_BEGIN_FISHING)
if(challenge)
UnregisterSignal(challenge, COMSIG_FISHING_CHALLENGE_GET_DIFFICULTY)
else
RegisterSignal(user, COMSIG_MOB_BEGIN_FISHING, PROC_REF(on_minigame_started), TRUE)
if(challenge)
RegisterSignal(challenge, COMSIG_FISHING_CHALLENGE_GET_DIFFICULTY, PROC_REF(adjust_difficulty), TRUE)
challenge?.update_difficulty()
/datum/component/adjust_fishing_difficulty/proc/on_minigame_started(mob/living/source, datum/fishing_challenge/challenge)
SIGNAL_HANDLER
RegisterSignal(challenge, COMSIG_FISHING_CHALLENGE_GET_DIFFICULTY, PROC_REF(adjust_difficulty), TRUE)
/datum/component/adjust_fishing_difficulty/proc/adjust_difficulty(datum/fishing_challenge/challenge, reward_path, obj/item/fishing_rod/rod, mob/living/user, list/holder)
SIGNAL_HANDLER
holder[1] += modifier
+130
View File
@@ -0,0 +1,130 @@
/datum/component/anomaly_locked_module
var/obj/item/assembly/signaler/anomaly/core
/// Accepted types of anomaly cores.
var/list/accepted_anomalies
/// If the core is removable once socketed.
var/core_removable
/// A proc to call before the core is inserted. Returns an ITEM_INTERACT define, which the component will itself return.
var/pre_insert_callback
/// A proc to call when the core is inserted.
var/core_insert_callback
/// A proc to call when the core is removed.
var/core_remove_callback
/datum/component/anomaly_locked_module/Initialize(list/anomaly_types, prebuilt = FALSE, removable = TRUE, pre_insert_callback, insert_callback, remove_callback)
. = ..()
if(!istype(parent, /obj/item/mod/module))
return COMPONENT_INCOMPATIBLE
accepted_anomalies = typecacheof(anomaly_types)
core_removable = removable
src.pre_insert_callback = pre_insert_callback
core_insert_callback = insert_callback
core_remove_callback = remove_callback
if(!(prebuilt && length(anomaly_types)))
return
var/obj/item/assembly/signaler/anomaly/core_type = pick(anomaly_types)
core = new core_type(parent)
/datum/component/anomaly_locked_module/Destroy(force)
QDEL_NULL(core)
return ..()
/datum/component/anomaly_locked_module/RegisterWithParent()
RegisterSignal(parent, COMSIG_MODULE_TRIGGERED, PROC_REF(on_module_triggered))
RegisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION, PROC_REF(on_item_interact))
RegisterSignal(parent, COMSIG_ATOM_TOOL_ACT(TOOL_SCREWDRIVER), PROC_REF(on_screwdriver_act))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
RegisterSignal(parent, COMSIG_ATOM_UPDATE_ICON_STATE, PROC_REF(on_update_icon_state))
/datum/component/anomaly_locked_module/UnregisterFromParent()
UnregisterSignal(parent, list(
COMSIG_MODULE_TRIGGERED,
COMSIG_ATOM_ITEM_INTERACTION,
COMSIG_ATOM_TOOL_ACT(TOOL_SCREWDRIVER),
COMSIG_ATOM_EXAMINE,
COMSIG_ATOM_UPDATE_ICON_STATE,
))
/datum/component/anomaly_locked_module/proc/on_module_triggered(obj/item/mod/module/source, mob/living/wearer)
SIGNAL_HANDLER
if(!core)
source.balloon_alert(wearer, "no core!")
return MOD_ABORT_USE
/datum/component/anomaly_locked_module/proc/on_item_interact(obj/item/mod/module/source, mob/living/user, obj/item/tool, list/modifiers)
SIGNAL_HANDLER
if(!is_type_in_typecache(tool, accepted_anomalies))
return 0
if(core)
source.balloon_alert(user, "already has core!")
return ITEM_INTERACT_FAILURE
if(pre_insert_callback)
var/callback_return
if(istype(pre_insert_callback, /datum/callback))
var/datum/callback/pre_insert_callback_datum = pre_insert_callback
callback_return = pre_insert_callback_datum.Invoke(user, tool, modifiers)
else
callback_return = call(source, pre_insert_callback)(user, tool, modifiers)
if(callback_return)
return callback_return
return insert_core(source, user, tool, modifiers)
/datum/component/anomaly_locked_module/proc/insert_core(obj/item/mod/module/source, mob/living/user, obj/item/tool, list/modifiers)
if(!user.transferItemToLoc(tool, source))
return ITEM_INTERACT_FAILURE
core = tool
source.balloon_alert(user, "core inserted")
playsound(source, 'sound/machines/click.ogg', 30, TRUE)
source.update_appearance(UPDATE_ICON_STATE)
if(core_insert_callback)
if(istype(core_insert_callback, /datum/callback))
var/datum/callback/core_insert_callback_datum = core_insert_callback
core_insert_callback_datum.Invoke(core, user, modifiers)
else
call(source, core_insert_callback)(core, user, modifiers)
return ITEM_INTERACT_SUCCESS
/datum/component/anomaly_locked_module/proc/on_screwdriver_act(obj/item/mod/module/source, mob/living/user, obj/item/tool)
SIGNAL_HANDLER
if(!core)
source.balloon_alert(user, "no core!")
return ITEM_INTERACT_FAILURE
if(!core_removable)
source.balloon_alert(user, "cannot remove core!")
INVOKE_ASYNC(src, PROC_REF(try_remove_core), source, user, tool)
return ITEM_INTERACT_SUCCESS
/datum/component/anomaly_locked_module/proc/try_remove_core(obj/item/mod/module/source, mob/living/user, obj/item/tool)
if(!do_after(user, 3 SECONDS, source))
source.balloon_alert(user, "interrupted!")
return
source.balloon_alert(user, "core removed")
core.forceMove(source.drop_location())
if(source.Adjacent(user) && !issilicon(user))
user.put_in_hands(core)
core = null
source.update_appearance(UPDATE_ICON_STATE)
if(core_remove_callback)
if(istype(core_remove_callback, /datum/callback))
var/datum/callback/core_remove_callback_datum = core_remove_callback
core_remove_callback_datum.Invoke(core, user)
else
call(source, core_remove_callback)(core, user)
/datum/component/anomaly_locked_module/proc/on_examine(obj/item/mod/module/source, mob/viewer, list/examine_list)
SIGNAL_HANDLER
if(!length(accepted_anomalies))
return
if(core)
examine_list += span_notice("There is a [core.name] installed in it. [core_removable ? "You could remove it with a <b>screwdriver</b>..." : "Unfortunately, due to a design quirk, it's unremovable."]")
return
var/list/core_list = list()
for(var/atom/core_path as anything in accepted_anomalies)
core_list += initial(core_path.name)
examine_list += span_notice("You need to insert \a [english_list(core_list, and_text = " or ")] for this module to function.")
if(!core_removable)
examine_list += span_notice("Due to some design quirk, once a core is inserted, it won't be removable.")
/datum/component/anomaly_locked_module/proc/on_update_icon_state(obj/item/mod/module/source)
SIGNAL_HANDLER
source.icon_state = source::icon_state + (core ? "-core" : "")
+1 -1
View File
@@ -43,7 +43,7 @@
/datum/component/area_sound_manager/proc/react_to_z_move(datum/source, turf/old_turf, turf/new_turf)
SIGNAL_HANDLER
if(!length(accepted_zs) || (new_turf.z in accepted_zs))
if(!length(accepted_zs) || (new_turf?.z in accepted_zs))
return
qdel(src)
+32 -17
View File
@@ -3,9 +3,9 @@
/// The closed turf our object is currently linked to.
var/atom/hanging_support_atom
/datum/component/atom_mounted/Initialize(target_structure, on_drop_callback)
/datum/component/atom_mounted/Initialize(target_structure)
. = ..()
if(!isobj(parent))
if(!isobj(parent) || !isatom(target_structure))
return COMPONENT_INCOMPATIBLE
hanging_support_atom = target_structure
RegisterSignal(hanging_support_atom, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
@@ -15,19 +15,19 @@
RegisterSignal(hanging_support_atom, COMSIG_QDELETING, PROC_REF(on_structure_delete))
/datum/component/atom_mounted/RegisterWithParent()
ADD_TRAIT(parent, TRAIT_WALLMOUNTED, REF(src))
ADD_TRAIT(parent, TRAIT_WALLMOUNTED, INNATE_TRAIT)
if(is_area_shuttle(get_area(parent)))
RegisterSignal(parent, COMSIG_ATOM_BEFORE_SHUTTLE_MOVE, PROC_REF(detach))
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_move))
/datum/component/atom_mounted/UnregisterFromParent()
REMOVE_TRAIT(parent, TRAIT_WALLMOUNTED, REF(src))
UnregisterSignal(parent, COMSIG_MOVABLE_MOVED)
REMOVE_TRAIT(parent, TRAIT_WALLMOUNTED, INNATE_TRAIT)
var/list/signals = list(COMSIG_MOVABLE_MOVED)
if(is_area_shuttle(get_area(parent)))
signals += COMSIG_ATOM_BEFORE_SHUTTLE_MOVE
UnregisterSignal(parent, signals)
/datum/component/atom_mounted/Destroy(force)
UnregisterSignal(hanging_support_atom, list(COMSIG_ATOM_EXAMINE))
if(isclosedturf(hanging_support_atom))
UnregisterSignal(hanging_support_atom, COMSIG_TURF_CHANGE)
else
UnregisterSignal(hanging_support_atom, COMSIG_QDELETING)
hanging_support_atom = null
return ..()
@@ -38,7 +38,7 @@
SIGNAL_HANDLER
if (parent in view(user.client?.view || world.view, user))
examine_list += span_notice("\The [hanging_support_atom] is currently supporting [span_bold("[parent]")]. Deconstruction or excessive damage would cause it to [span_bold("fall to the ground")].")
examine_list += span_notice("\The [hanging_support_atom] is currently supporting [span_bold("\the [parent]")]. Deconstruction or excessive damage would cause it to [span_bold("fall to the ground")].")
/// When the type of turf changes, if it is changing into a floor we should drop our contents
/datum/component/atom_mounted/proc/on_turf_changing(datum/source, path, new_baseturfs, flags, post_change_callbacks)
@@ -47,6 +47,7 @@
if(ispath(path, /turf/open))
drop_wallmount()
///When the atom the object is mounted on is destroyed deconstruct
/datum/component/atom_mounted/proc/on_structure_delete(datum/source, force)
SIGNAL_HANDLER
@@ -61,6 +62,12 @@
return
drop_wallmount()
///Called when the object is about to be shuttle rotated so we have to delete ourself and mount again later
/datum/component/atom_mounted/proc/detach(datum/source, newT, rotation, move_mode, moving_dock)
SIGNAL_HANDLER
qdel(src)
/**
* Handles the dropping of the linked object. This is done via deconstruction, as that should be the most sane way to handle it for most objects.
* Except for intercoms, which are handled by creating a new wallframe intercom, as they're apparently items.
@@ -72,7 +79,6 @@
hanging_parent.visible_message(message = span_warning("\The [hanging_parent] falls apart!"), vision_distance = 5)
hanging_parent.deconstruct(FALSE)
/// Returns a list of potential turfs to mount on. This should not check if those turfs are valid but only locate them
/obj/proc/get_turfs_to_mount_on()
PROTECTED_PROC(TRUE)
@@ -107,7 +113,7 @@
return isclosedturf(target)
/// Returns an list of object types we can mount on if the turf is unmountable
/obj/proc/get_moutable_objects()
/obj/proc/get_mountable_objects()
PROTECTED_PROC(TRUE)
SHOULD_BE_PURE(TRUE)
RETURN_TYPE(/list/obj)
@@ -137,11 +143,11 @@
return TRUE
var/area/location = get_area(src)
if(!isarea(location) || istype(location, /area/shuttle))
if(!isarea(location))
return FALSE
var/msg
if(PERFORM_ALL_TESTS(focus_only/atom_mounted) && !mark_for_late_init)
if(PERFORM_ALL_TESTS(maptest_log_mapping) && !mark_for_late_init)
msg = "[type] Could not find attachable object at [location.type] "
var/list/turf/attachable_turfs = get_turfs_to_mount_on()
@@ -150,19 +156,28 @@
if(is_mountable_turf(target))
attachable_atom = target //your usual wallmount
else
var/list/obj/attachables = get_moutable_objects()
var/list/obj/attachables = get_mountable_objects()
for(var/obj/attachable in target)
if(is_type_in_list(attachable, attachables))
attachable_atom = attachable
break
if(attachable_atom)
AddComponent(/datum/component/atom_mounted, attachable_atom)
if(is_area_shuttle(location))
RegisterSignal(src, COMSIG_ATOM_AFTER_SHUTTLE_MOVE, PROC_REF(remount), override = TRUE)
return TRUE
if(msg)
msg += "([target.x],[target.y],[target.z]) "
if(msg)
stack_trace(msg)
log_mapping(msg)
if(mark_for_late_init)
obj_flags |= MOUNT_ON_LATE_INITIALIZE
return FALSE
///Used to remount an object after shuttle move
/obj/proc/remount(datum/source, oldT)
SIGNAL_HANDLER
PRIVATE_PROC(TRUE)
find_and_mount_on_atom()
+6 -2
View File
@@ -120,8 +120,7 @@
for (var/mob/living/candidate as anything in to_heal)
if (!current_alerts[candidate])
var/atom/movable/screen/alert/aura_healing/alert = candidate.throw_alert(alert_category, /atom/movable/screen/alert/aura_healing, new_master = parent)
alert.desc = "You are being healed by [parent]."
candidate.throw_alert(alert_category, /atom/movable/screen/alert/aura_healing, new_master = parent)
current_alerts[candidate] = TRUE
if (should_show_effect && candidate.health < candidate.maxHealth)
@@ -164,5 +163,10 @@
icon_state = "template"
use_user_hud_icon = TRUE
clickable_glow = TRUE
click_master = FALSE
/atom/movable/screen/alert/aura_healing/update_desc(updates)
. = ..()
desc = "You are being healed by [master_ref?.resolve()]."
#undef HEAL_EFFECT_COOLDOWN
+2 -1
View File
@@ -100,7 +100,8 @@
if (isnull(bayonet) || !user.combat_mode)
return NONE
INVOKE_ASYNC(bayonet, TYPE_PROC_REF(/obj/item, melee_attack_chain), user, target, modifiers)
// RMB is used for butchering, and bayonets are usually are knives
INVOKE_ASYNC(bayonet, TYPE_PROC_REF(/obj/item, melee_attack_chain), user, target, modifiers - RIGHT_CLICK)
return COMPONENT_CANCEL_ATTACK_CHAIN
/datum/component/bayonet_attachable/proc/on_attackby(obj/item/source, obj/item/attacking_item, mob/attacker, list/modifiers)
+2 -2
View File
@@ -72,7 +72,7 @@
/datum/component/blob_minion/RegisterWithParent()
var/mob/living/living_parent = parent
living_parent.faction |= ROLE_BLOB
living_parent.add_faction(ROLE_BLOB)
ADD_TRAIT(parent, TRAIT_BLOB_ALLY, REF(src))
remove_verb(parent, /mob/living/verb/pulled) // No dragging people into the blob
RegisterSignal(parent, COMSIG_MOB_MIND_INITIALIZED, PROC_REF(on_mind_init))
@@ -96,7 +96,7 @@
overmind.blob_mobs -= parent
var/mob/living/living_parent = parent
living_parent.pass_flags &= ~PASSBLOB
living_parent.faction -= ROLE_BLOB
living_parent.remove_faction(ROLE_BLOB)
REMOVE_TRAIT(parent, TRAIT_BLOB_ALLY, REF(src))
add_verb(parent, /mob/living/verb/pulled)
UnregisterSignal(parent, list(
+21 -6
View File
@@ -1,5 +1,5 @@
/**
* Attaches to a hostile simplemob and plays that music while they have a target.
* Attaches to a mob and plays that music while they have a target.
*/
/datum/component/boss_music
///The music track we will play to players.
@@ -12,11 +12,16 @@
///List of callback timers, used to clear out mobs listening to boss music after `track_duration`.
var/list/music_callbacks = list()
/datum/component/boss_music/Initialize(boss_track)
/// Signal that we listen to on our parent to know when to start boss music.
/// On basic mobs, the signal should be a variant of `COMSIG_AI_BLACKBOARD_KEY_SET()`, feeding in whatever key you want.
var/signal = null
/datum/component/boss_music/Initialize(boss_track, signal)
. = ..()
if(!ishostile(parent))
if(!ismob(parent) || isnull(signal))
return COMPONENT_INCOMPATIBLE
src.boss_track = boss_track
src.signal = signal
track_duration = SSsounds.get_sound_length(boss_track)
/datum/component/boss_music/Destroy(force)
@@ -31,16 +36,26 @@
/datum/component/boss_music/RegisterWithParent()
. = ..()
RegisterSignal(parent, COMSIG_HOSTILE_FOUND_TARGET, PROC_REF(on_target_found))
if(isbasicmob(parent))
RegisterSignal(parent, signal, PROC_REF(basic_target_found))
else
RegisterSignal(parent, signal, PROC_REF(on_target_found))
/datum/component/boss_music/UnregisterFromParent()
UnregisterSignal(parent, COMSIG_HOSTILE_FOUND_TARGET)
UnregisterSignal(parent, signal)
return ..()
/// Handler wrapper for basic mobs getting a target. The signal we pass to basic mobs passes along the blackboard key which we can access off parent.
/datum/component/boss_music/proc/basic_target_found(mob/source, key)
SIGNAL_HANDLER
var/mob/new_target = source.ai_controller.blackboard[key]
on_target_found(source, new_target)
///Handles giving the boss music to a new target the fauna has received.
///Keeps track of them to not repeatedly overwrite its own track.
/datum/component/boss_music/proc/on_target_found(atom/source, mob/new_target)
/datum/component/boss_music/proc/on_target_found(mob/source, mob/new_target)
SIGNAL_HANDLER
if(QDELETED(source) || !istype(new_target))
return
+415 -108
View File
@@ -27,72 +27,362 @@
src.effectiveness = effectiveness
src.bonus_modifier = bonus_modifier
src.butcher_sound = butcher_sound
if(disabled)
if (disabled)
src.butchering_enabled = FALSE
src.can_be_blunt = can_be_blunt
src.butcher_callback = butcher_callback
if(isitem(parent))
RegisterSignal(parent, COMSIG_ITEM_ATTACK, PROC_REF(onItemAttack))
/datum/component/butchering/Destroy(force)
butcher_callback = null
return ..()
/datum/component/butchering/proc/onItemAttack(obj/item/source, mob/living/M, mob/living/user)
/datum/component/butchering/RegisterWithParent()
if (!isitem(parent))
return
var/obj/item/item_parent = parent
item_parent.item_flags |= ITEM_HAS_CONTEXTUAL_SCREENTIPS
RegisterSignal(parent, COMSIG_ITEM_ATTACK, PROC_REF(on_item_attack))
RegisterSignal(parent, COMSIG_ITEM_INTERACTING_WITH_ATOM, PROC_REF(on_item_interaction))
RegisterSignal(parent, COMSIG_ITEM_REQUESTING_CONTEXT_FOR_TARGET, PROC_REF(add_item_context))
/datum/component/butchering/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_ITEM_ATTACK, COMSIG_ITEM_INTERACTING_WITH_ATOM))
/datum/component/butchering/proc/on_item_attack(obj/item/source, mob/living/victim, mob/living/user, list/modifiers)
SIGNAL_HANDLER
if(!user.combat_mode)
return
if(M.stat == DEAD && (M.butcher_results || M.guaranteed_butcher_results)) //can we butcher it?
if(butchering_enabled && (can_be_blunt || source.get_sharpness()))
INVOKE_ASYNC(src, PROC_REF(startButcher), source, M, user)
return COMPONENT_CANCEL_ATTACK_CHAIN
if(ishuman(M) && source.force && source.get_sharpness())
var/mob/living/carbon/human/H = M
if((user.pulling == H && user.grab_state >= GRAB_AGGRESSIVE) && user.zone_selected == BODY_ZONE_HEAD) // Only aggressive grabbed can be sliced.
if(HAS_TRAIT(user, TRAIT_PACIFISM))
to_chat(user, span_warning("You don't want to harm other living beings!"))
return COMPONENT_CANCEL_ATTACK_CHAIN
if(H.has_status_effect(/datum/status_effect/neck_slice))
return
INVOKE_ASYNC(src, PROC_REF(startNeckSlice), source, H, user)
return COMPONENT_CANCEL_ATTACK_CHAIN
/datum/component/butchering/proc/startButcher(obj/item/source, mob/living/M, mob/living/user)
to_chat(user, span_notice("You begin to butcher [M]..."))
playsound(M.loc, butcher_sound, 50, TRUE, -1)
if(do_after(user, speed, M) && M.Adjacent(source))
on_butchering(user, M)
/datum/component/butchering/proc/startNeckSlice(obj/item/source, mob/living/carbon/human/H, mob/living/user)
if(DOING_INTERACTION_WITH_TARGET(user, H))
to_chat(user, span_warning("You're already interacting with [H]!"))
if (!source.get_sharpness() && !can_be_blunt)
return
user.visible_message(span_danger("[user] is slitting [H]'s throat!"), \
span_danger("You start slicing [H]'s throat!"), \
span_hear("You hear a cutting noise!"), ignored_mobs = H)
H.show_message(span_userdanger("Your throat is being slit by [user]!"), MSG_VISUAL, \
span_userdanger("Something is cutting into your neck!"), NONE)
log_combat(user, H, "attempted throat slitting", source)
if (!user.combat_mode)
return
playsound(H.loc, butcher_sound, 50, TRUE, -1)
if(do_after(user, clamp(500 / source.force, 30, 100), H) && H.Adjacent(source))
if(H.has_status_effect(/datum/status_effect/neck_slice))
user.show_message(span_warning("[H]'s neck has already been already cut, you can't make the bleeding any worse!"), MSG_VISUAL, \
span_warning("Their neck has already been already cut, you can't make the bleeding any worse!"))
// Can we butcher it?
if (victim.stat == DEAD && (victim.butcher_results || victim.guaranteed_butcher_results) && butchering_enabled)
INVOKE_ASYNC(src, PROC_REF(start_butcher), source, victim, user)
return COMPONENT_CANCEL_ATTACK_CHAIN
if (!ishuman(victim) || !source.force)
return
if (LAZYACCESS(modifiers, RIGHT_CLICK) && butchering_enabled)
INVOKE_ASYNC(src, PROC_REF(butcher_human), source, victim, user)
return COMPONENT_CANCEL_ATTACK_CHAIN
// Neckslicing requires aggro grabs
if (user.pulling != victim || user.grab_state < GRAB_AGGRESSIVE || user.zone_selected != BODY_ZONE_HEAD)
return
if (HAS_TRAIT(user, TRAIT_PACIFISM))
to_chat(user, span_warning("You don't want to harm other living beings!"))
return COMPONENT_CANCEL_ATTACK_CHAIN
if (victim.has_status_effect(/datum/status_effect/neck_slice))
return
INVOKE_ASYNC(src, PROC_REF(start_neck_slice), source, victim, user)
return COMPONENT_CANCEL_ATTACK_CHAIN
/datum/component/butchering/proc/on_item_interaction(obj/item/source, mob/living/user, atom/target)
SIGNAL_HANDLER
if (!source.get_sharpness() && !can_be_blunt)
return
if (!user.combat_mode || !butchering_enabled)
return
if (isbodypart(target))
INVOKE_ASYNC(src, PROC_REF(butcher_limb), source, target, user)
return COMPONENT_CANCEL_ATTACK_CHAIN
/datum/component/butchering/proc/add_item_context(obj/item/source, list/context, atom/target, mob/living/user)
SIGNAL_HANDLER
if (!source.get_sharpness() && !can_be_blunt)
return NONE
if (!butchering_enabled)
return NONE
if (isbodypart(target))
context[SCREENTIP_CONTEXT_LMB] = "(Combat Mode) Butcher limb"
return CONTEXTUAL_SCREENTIP_SET
if (!isliving(target))
return NONE
var/mob/living/victim = target
if (victim.stat != DEAD && (victim.butcher_results || victim.guaranteed_butcher_results))
context[SCREENTIP_CONTEXT_LMB] = "(Combat Mode) Butcher"
return CONTEXTUAL_SCREENTIP_SET
if (ishuman(victim))
context[SCREENTIP_CONTEXT_RMB] = "(Combat Mode) Butcher"
return CONTEXTUAL_SCREENTIP_SET
return NONE
/datum/component/butchering/proc/butcher_limb(obj/item/source, obj/item/bodypart/target, mob/living/user)
target.add_fingerprint(user)
if (LIMB_HAS_SKIN(target) && !HAS_ANY_SURGERY_STATE(target.surgery_state, SURGERY_SKIN_CUT | SURGERY_SKIN_OPEN))
to_chat(user, span_warning("[target]'s skin is still intact!"))
return
if (LIMB_HAS_BONES(target) && !HAS_ANY_SURGERY_STATE(target.surgery_state, SURGERY_BONE_DRILLED | SURGERY_BONE_SAWED))
// We need to gut the limb before turning it into meat, otherwise just cut around the bone I guess
if (length(target.contents))
to_chat(user, span_warning("[target]'s bones are still intact!"))
return
H.visible_message(span_danger("[user] slits [H]'s throat!"), \
span_userdanger("[user] slits your throat..."))
log_combat(user, H, "wounded via throat slitting", source)
H.apply_damage(source.force, BRUTE, BODY_ZONE_HEAD, wound_bonus=CANT_WOUND) // easy tiger, we'll get to that in a sec
var/obj/item/bodypart/slit_throat = H.get_bodypart(BODY_ZONE_HEAD)
if (H.cause_wound_of_type_and_severity(WOUND_SLASH, slit_throat, WOUND_SEVERITY_CRITICAL))
H.apply_status_effect(/datum/status_effect/neck_slice)
var/speed_modifier = 1
if (!target.owner)
speed_modifier = 0.5
else if (target.owner.stat < UNCONSCIOUS)
speed_modifier = 1.5 // yeowch
var/limb_descriptor = (target.owner ? "[target.owner]'s [target.plaintext_zone]" : target)
// Okay *hopefully* they're already dead at this point
if (target.body_zone == BODY_ZONE_CHEST && target.owner)
// Butchering the chest gibs the victim
limb_descriptor = target.owner
if (target.owner)
log_combat(user, target.owner, "attempted to butcher", source)
if (length(target.contents))
user.visible_message(span_warning("[user] begins to gut [limb_descriptor]!"), span_notice("You begin to gut [limb_descriptor]..."), ignored_mobs = target.owner)
if (target.owner)
to_chat(target.owner, span_warning("[user] begins to gut your [target.plaintext_zone]!"))
playsound(target.loc, butcher_sound, 50, TRUE, -1)
if (!do_after(user, speed * speed_modifier, target.owner || target))
return
target.drop_organs(user, TRUE)
return
if (!length(target.butcher_drops))
to_chat(user, span_warning("There is nothing left inside [limb_descriptor]!"))
return
if (target.body_zone == BODY_ZONE_CHEST && target.owner)
// Cannot butcher the chest until we hack off all the other limbs
for (var/obj/item/bodypart/limb as anything in target.owner.bodyparts)
if (limb != target && limb.butcher_drops && limb.butcher_replacement)
to_chat(user, span_warning("You need to butcher all other limbs first!"))
return
user.visible_message(span_warning("[user] begins to cut [limb_descriptor] apart!"), span_notice("You begin to cut [limb_descriptor] apart..."), ignored_mobs = target.owner)
if (target.owner)
to_chat(target.owner, span_warning("[user] begins to cut your [target.plaintext_zone] apart!"))
playsound(target.loc, butcher_sound, 50, TRUE, -1)
if (!do_after(user, speed * speed_modifier, target.owner || target))
return
var/list/results = list()
var/turf/drop_loc = target.owner?.drop_location() || target.drop_location()
var/bonus_chance = max(0, (effectiveness - 100) + bonus_modifier) //so 125 total effectiveness = 25% extra chance
var/list/failures = list()
var/list/bonuses = list()
for (var/obj/item/drop_type as anything in target.butcher_drops)
var/amount = target.butcher_drops[drop_type] || 1
var/is_stack = ispath(drop_type, /obj/item/stack)
for (var/i in 1 to amount)
if (!prob(effectiveness))
failures |= drop_type::name
amount -= 1
continue
if (prob(bonus_chance))
if (!is_stack)
if (ispath(drop_type, /obj/item/food/meat))
results += new drop_type(drop_loc, target.blood_dna_info?.Copy())
else
var/obj/item/butcher_result = new drop_type(drop_loc)
if (target.blood_dna_info)
butcher_result.add_blood_DNA(target.blood_dna_info.Copy())
results += butcher_result
amount += 1
bonuses |= drop_type::name
if (is_stack)
continue
if (ispath(drop_type, /obj/item/food/meat))
results += new drop_type(drop_loc, target.blood_dna_info?.Copy())
continue
var/obj/item/butcher_result = new drop_type(drop_loc)
butcher_result.add_blood_DNA(target.blood_dna_info.Copy())
results += butcher_result
if (is_stack && amount)
var/obj/item/stack/butcher_result = null
if (ispath(drop_type, /obj/item/stack/sheet/animalhide/carbon))
butcher_result = new drop_type(drop_loc, amount, /*merge = */TRUE, /*mat_override = */null, /*mat_amount = */1, target.skin_tone || target.species_color)
else
butcher_result = new drop_type(drop_loc, amount)
if (target.blood_dna_info)
butcher_result.add_blood_DNA(target.blood_dna_info.Copy())
results += butcher_result
if (target.reagents)
var/meat_produced = 0
for (var/obj/item/food/target_meat in results)
meat_produced += 1
for (var/obj/item/food/target_meat in results)
target.reagents.trans_to(target_meat, target.reagents.total_volume / meat_produced, remove_blacklisted = TRUE)
if (target.owner)
var/reagents_in_produced = 0
for(var/obj/item/result as anything in results)
if(result.reagents)
reagents_in_produced += 1
var/list/diseases = target.owner.get_static_viruses()
for(var/obj/item/result as anything in results)
if (reagents_in_produced)
if (target.owner.reagents)
target.owner.reagents.trans_to(result, target.owner.reagents.total_volume / reagents_in_produced / length(target.owner.bodyparts), remove_blacklisted = TRUE)
result.reagents?.add_reagent(/datum/reagent/consumable/nutriment/fat, target.owner.nutrition / /datum/reagent/consumable/nutriment/fat::nutriment_factor / reagents_in_produced)
if(LAZYLEN(diseases))
var/list/datum/disease/diseases_to_add = list()
for(var/datum/disease/disease as anything in diseases)
// Admin or special viruses that should not be reproduced
if(disease.spread_flags & (DISEASE_SPREAD_SPECIAL | DISEASE_SPREAD_NON_CONTAGIOUS))
continue
diseases_to_add += disease
if(LAZYLEN(diseases_to_add))
result.AddComponent(/datum/component/infective, diseases_to_add)
for (var/obj/item/food/meat/meat in results)
meat.name = "[target.owner.real_name]'s [meat.name]"
meat.set_custom_materials(list(GET_MATERIAL_REF(/datum/material/meat/mob_meat, target.owner) = 4 * SHEET_MATERIAL_AMOUNT))
meat.subjectname = target.owner.real_name
meat.subjectjob = target.owner.job
user.visible_message(span_warning("[user] butchers [limb_descriptor]!"), span_notice("You butcher [limb_descriptor]."), ignored_mobs = target.owner)
if (!target.owner)
target.drop_organs(violent_removal = TRUE) // Should not happen, but just in case
create_replacement_limb(target, drop_loc)
qdel(target)
return
var/wound_type = null
if (source.damtype == BURN)
wound_type = WOUND_BURN
else
switch (source.get_sharpness())
if (SHARP_EDGED)
wound_type = WOUND_SLASH
if (SHARP_POINTY)
wound_type = WOUND_PIERCE
else
wound_type = WOUND_BLUNT
to_chat(target.owner, span_userdanger("[user] hacks the meat off your [target.plaintext_zone]!"))
var/mob/living/carbon/victim = target.owner
if (!target.butcher_replacement)
target.dismember(source.damtype, wound_type)
target.drop_organs(violent_removal = TRUE) // Should not happen, but just in case
qdel(target)
return
var/obj/item/bodypart/replacement = create_replacement_limb(target, drop_loc)
target.dismember(source.damtype, wound_type)
target.drop_organs(violent_removal = TRUE)
replacement.replace_limb(victim)
replacement.update_limb(is_creating = TRUE)
qdel(target)
/// Creates a replacement (usually skeleton) limb for the butchered one
/datum/component/butchering/proc/create_replacement_limb(obj/item/bodypart/target, drop_loc)
var/drop_type = target.butcher_replacement
var/obj/item/bodypart/replacement = new drop_type(drop_loc)
replacement.bodyshape = target.bodyshape
replacement.set_initial_damage(target.brute_dam, target.burn_dam)
if (IS_ORGANIC_LIMB(replacement) && target.owner)
replacement.blood_dna_info = target.owner.get_blood_dna_list()
for (var/datum/wound/wound as anything in target.wounds)
wound.remove_wound()
wound.apply_wound(replacement, silent = TRUE)
return replacement
/datum/component/butchering/proc/start_butcher(obj/item/source, mob/living/target, mob/living/user)
to_chat(user, span_notice("You begin to butcher [target]..."))
playsound(target.loc, butcher_sound, 50, TRUE, -1)
if (do_after(user, speed, target) && target.Adjacent(source))
on_butchering(user, target)
/datum/component/butchering/proc/butcher_human(obj/item/source, mob/living/carbon/human/victim, mob/living/user)
if (DOING_INTERACTION_WITH_TARGET(user, victim))
to_chat(user, span_warning("You're already interacting with [victim]!"))
return
var/static/list/butcher_spots = typecacheof(list(
/obj/structure/table,
/obj/structure/bed,
/obj/machinery/stasis,
/obj/structure/kitchenspike,
))
var/found_spot = FALSE
for (var/obj/thing in victim.loc)
if (is_type_in_typecache(thing, butcher_spots))
found_spot = TRUE
break
if (!found_spot)
to_chat(user, span_warning("You need a better spot to butcher [victim]!"))
return
var/obj/item/bodypart/limb = victim.get_bodypart(deprecise_zone(user.zone_selected))
if (!limb)
to_chat(user, span_warning("[victim] doesn't have a [parse_zone(deprecise_zone(user.zone_selected))]!"))
return
butcher_limb(source, limb, user)
/datum/component/butchering/proc/start_neck_slice(obj/item/source, mob/living/carbon/human/victim, mob/living/user)
if (DOING_INTERACTION_WITH_TARGET(user, victim))
to_chat(user, span_warning("You're already interacting with [victim]!"))
return
user.visible_message(span_danger("[user] is slitting [victim]'s throat!"), \
span_danger("You start slicing [victim]'s throat!"), \
span_hear("You hear a cutting noise!"), ignored_mobs = victim)
victim.show_message(span_userdanger("Your throat is being slit by [user]!"), MSG_VISUAL, \
span_userdanger("Something is cutting into your neck!"), NONE)
log_combat(user, victim, "attempted throat slitting", source)
playsound(victim.loc, butcher_sound, 50, TRUE, -1)
if (!do_after(user, clamp(500 / source.force, 30, 100), victim) && victim.Adjacent(source))
return
if (victim.has_status_effect(/datum/status_effect/neck_slice))
user.show_message(span_warning("[victim]'s neck has already been already cut, you can't make the bleeding any worse!"), MSG_VISUAL, \
span_warning("Their neck has already been already cut, you can't make the bleeding any worse!"))
return
victim.visible_message(span_danger("[user] slits [victim]'s throat!"), \
span_userdanger("[user] slits your throat..."))
log_combat(user, victim, "wounded via throat slitting", source)
victim.apply_damage(source.force, BRUTE, BODY_ZONE_HEAD, wound_bonus=CANT_WOUND) // easy tiger, we'll get to that in a sec
var/obj/item/bodypart/slit_throat = victim.get_bodypart(BODY_ZONE_HEAD)
if (victim.cause_wound_of_type_and_severity(WOUND_SLASH, slit_throat, WOUND_SEVERITY_CRITICAL))
victim.apply_status_effect(/datum/status_effect/neck_slice)
/**
* Handles a user butchering a target
@@ -107,67 +397,85 @@
var/final_effectiveness = effectiveness - target.butcher_difficulty
var/bonus_chance = max(0, (final_effectiveness - 100) + bonus_modifier) //so 125 total effectiveness = 25% extra chance
if(target.flags_1 & HOLOGRAM_1)
if (target.flags_1 & HOLOGRAM_1)
butcher.visible_message(span_notice("[butcher] tries to butcher [target], but it vanishes."), \
span_notice("You try to butcher [target], but it vanishes."))
qdel(target)
return
for(var/result_typepath in target.butcher_results)
var/obj/remains = result_typepath
var/amount = target.butcher_results[remains]
for(var/_i in 1 to amount)
if(!prob(final_effectiveness))
if(butcher)
to_chat(butcher, span_warning("You fail to harvest some of the [initial(remains.name)] from [target]."))
var/list/failures = list()
var/list/bonuses = list()
for (var/obj/remains as anything in target.butcher_results)
var/amount = target.butcher_results[remains] || 1
var/is_stack = ispath(remains, /obj/item/stack)
for (var/i in 1 to amount)
if (!prob(final_effectiveness))
failures |= remains::name
amount -= 1
continue
if(prob(bonus_chance))
if(butcher)
to_chat(butcher, span_info("You harvest some extra [initial(remains.name)] from [target]!"))
results += new remains (location)
results += new remains (location)
if (prob(bonus_chance))
if (!is_stack)
results += new remains(location)
amount += 1
bonuses |= remains::name
target.butcher_results.Remove(remains) //in case you want to, say, have it drop its results on gib
if (!is_stack)
results += new remains(location)
for(var/guaranteed_result_typepath in target.guaranteed_butcher_results)
var/obj/guaranteed_remains = guaranteed_result_typepath
if (is_stack && amount)
results += new remains(location, amount)
target.butcher_results?.Cut()
if (butcher)
if (length(failures))
to_chat(butcher, span_warning("You fail to harvest some of the [english_list(failures)] from [target]."))
if (length(bonuses))
to_chat(butcher, span_info("You harvest some extra [english_list(bonuses)] from [target]!"))
for (var/obj/guaranteed_remains as anything in target.guaranteed_butcher_results)
var/amount = target.guaranteed_butcher_results[guaranteed_remains]
for(var/i in 1 to amount)
results += new guaranteed_remains (location)
target.guaranteed_butcher_results.Remove(guaranteed_remains)
if (ispath(guaranteed_remains, /obj/item/stack))
results += new guaranteed_remains(location, amount)
continue
for(var/obj/item/carrion in results)
for (var/i in 1 to amount)
results += new guaranteed_remains(location)
target.guaranteed_butcher_results?.Cut()
for (var/obj/item/carrion in results)
var/list/meat_mats = carrion.has_material_type(/datum/material/meat)
if(!length(meat_mats))
if (!length(meat_mats))
continue
carrion.set_custom_materials((carrion.custom_materials - meat_mats) + list(GET_MATERIAL_REF(/datum/material/meat/mob_meat, target) = counterlist_sum(meat_mats)))
// transfer delicious reagents to meat
if(target.reagents)
// Transfer delicious reagents to meat
if (target.reagents)
var/meat_produced = 0
for(var/obj/item/food/meat/slab/target_meat in results)
for (var/obj/item/food/target_meat in results)
meat_produced += 1
for(var/obj/item/food/meat/slab/target_meat in results)
for (var/obj/item/food/target_meat in results)
target.reagents.trans_to(target_meat, target.reagents.total_volume / meat_produced, remove_blacklisted = TRUE)
// dont forget yummy diseases either!
if(iscarbon(target))
// Don't forget yummy diseases either!
if (iscarbon(target))
var/mob/living/carbon/host_target = target
var/list/diseases = host_target.get_static_viruses()
if(LAZYLEN(diseases))
if (LAZYLEN(diseases))
var/list/datum/disease/diseases_to_add = list()
for(var/datum/disease/disease as anything in diseases)
for (var/datum/disease/disease as anything in diseases)
// admin or special viruses that should not be reproduced
if(disease.spread_flags & (DISEASE_SPREAD_SPECIAL | DISEASE_SPREAD_NON_CONTAGIOUS))
continue
if (!(disease.spread_flags & (DISEASE_SPREAD_SPECIAL | DISEASE_SPREAD_NON_CONTAGIOUS)))
diseases_to_add += disease
diseases_to_add += disease
if(LAZYLEN(diseases_to_add))
for(var/obj/diseased_remains in results)
if (LAZYLEN(diseases_to_add))
for (var/obj/diseased_remains in results)
diseased_remains.AddComponent(/datum/component/infective, diseases_to_add)
if(butcher)
if (butcher)
butcher.visible_message(span_notice("[butcher] butchers [target]."), \
span_notice("You butcher [target]."))
butcher_callback?.Invoke(butcher, target)
@@ -175,20 +483,9 @@
target.log_message("has been butchered by [key_name(butcher)]", LOG_ATTACK)
target.gib(DROP_BRAIN|DROP_ORGANS)
///Enables the butchering mechanic for the mob who has equipped us.
/datum/component/butchering/proc/enable_butchering(datum/source)
SIGNAL_HANDLER
butchering_enabled = TRUE
///Disables the butchering mechanic for the mob who has dropped us.
/datum/component/butchering/proc/disable_butchering(datum/source)
SIGNAL_HANDLER
butchering_enabled = FALSE
///Special snowflake component only used for the recycler.
/// Special snowflake component only used for the recycler.
/datum/component/butchering/recycler
/datum/component/butchering/recycler/Initialize(
speed,
effectiveness,
@@ -198,10 +495,10 @@
can_be_blunt,
butcher_callback,
)
if(!istype(parent, /obj/machinery/recycler)) //EWWW
if (!istype(parent, /obj/machinery/recycler)) //EWWW
return COMPONENT_INCOMPATIBLE
. = ..()
if(. == COMPONENT_INCOMPATIBLE)
if (. == COMPONENT_INCOMPATIBLE)
return
var/static/list/loc_connections = list(
@@ -212,13 +509,13 @@
/datum/component/butchering/recycler/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
SIGNAL_HANDLER
if(!isliving(arrived))
if (!isliving(arrived))
return
var/mob/living/victim = arrived
var/obj/machinery/recycler/eater = parent
if(eater.safety_mode || (eater.machine_stat & (BROKEN|NOPOWER))) //I'm so sorry.
if (eater.safety_mode || (eater.machine_stat & (BROKEN|NOPOWER))) //I'm so sorry.
return
if(victim.stat == DEAD && (victim.butcher_results || victim.guaranteed_butcher_results))
if (victim.stat == DEAD && (victim.butcher_results || victim.guaranteed_butcher_results))
on_butchering(parent, victim)
/datum/component/butchering/mecha
@@ -237,6 +534,16 @@
COMSIG_MECHA_EQUIPMENT_DETACHED,
))
/// Enables the butchering mechanic for the mecha who has equipped us.
/datum/component/butchering/mecha/proc/enable_butchering(datum/source)
SIGNAL_HANDLER
butchering_enabled = TRUE
/// Disables the butchering mechanic for the mecha who has dropped us.
/datum/component/butchering/mecha/proc/disable_butchering(datum/source)
SIGNAL_HANDLER
butchering_enabled = FALSE
///When we are ready to drill through a mob
/datum/component/butchering/mecha/proc/on_drill(datum/source, obj/vehicle/sealed/mecha/chassis, mob/living/target)
SIGNAL_HANDLER
@@ -256,16 +563,16 @@
COMSIG_ITEM_DROPPED,
))
///Same as enable_butchering but for worn items
/// Same as enable_butchering but for worn items
/datum/component/butchering/wearable/proc/worn_enable_butchering(obj/item/source, mob/user, slot)
SIGNAL_HANDLER
//check if the item is being not worn
if(!(slot & source.slot_flags))
if (!(slot & source.slot_flags))
return
butchering_enabled = TRUE
RegisterSignal(user, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(butcher_target))
///Same as disable_butchering but for worn items
/// Same as disable_butchering but for worn items
/datum/component/butchering/wearable/proc/worn_disable_butchering(obj/item/source, mob/user)
SIGNAL_HANDLER
butchering_enabled = FALSE
@@ -273,6 +580,6 @@
/datum/component/butchering/wearable/proc/butcher_target(mob/user, atom/target, proximity)
SIGNAL_HANDLER
if(!isliving(target))
if (!isliving(target))
return NONE
return onItemAttack(parent, target, user)
return on_item_attack(parent, target, user)
+23 -9
View File
@@ -6,7 +6,7 @@
var/oblivion_message = "You stumble and stare into the abyss before you. It stares back, and you fall into the enveloping dark."
/// List of refs to falling objects -> how many levels deep we've fallen
var/static/list/falling_atoms = list()
var/static/list/falling_atoms
var/static/list/forbidden_types = typecacheof(list(
/obj/docking_port,
/obj/effect/abstract,
@@ -106,7 +106,8 @@
/datum/component/chasm/proc/droppable(atom/movable/dropped_thing)
var/datum/weakref/falling_ref = WEAKREF(dropped_thing)
// avoid an infinite loop, but allow falling a large distance
if(falling_atoms[falling_ref] && falling_atoms[falling_ref] > 30)
var/falling_atom = LAZYACCESS(falling_atoms, falling_ref)
if(falling_atom && falling_atom > 30)
return CHASM_NOT_DROPPING
if(is_type_in_typecache(dropped_thing, forbidden_types) || (!isliving(dropped_thing) && !isobj(dropped_thing)))
return CHASM_NOT_DROPPING
@@ -133,14 +134,14 @@
var/datum/weakref/falling_ref = WEAKREF(dropped_thing)
//Make sure the item is still there after our sleep
if(!dropped_thing || !falling_ref?.resolve())
falling_atoms -= falling_ref
LAZYREMOVE(falling_atoms, falling_ref)
return
falling_atoms[falling_ref] = (falling_atoms[falling_ref] || 0) + 1
LAZYSET(falling_atoms, falling_ref, (falling_atoms[falling_ref] || 0) + 1)
var/turf/below_turf = target_turf
var/atom/parent = src.parent
if(falling_atoms[falling_ref] > 1)
if(LAZYACCESS(falling_atoms, falling_ref) > 1)
return // We're already handling this
if(SEND_SIGNAL(dropped_thing, COMSIG_MOVABLE_CHASM_DROPPED, parent) & COMPONENT_NO_CHASM_DROP)
@@ -164,15 +165,28 @@
var/mob/living/fallen = dropped_thing
fallen.Paralyze(100)
fallen.adjust_brute_loss(30)
falling_atoms -= falling_ref
LAZYREMOVE(falling_atoms, falling_ref)
return
// send to oblivion
dropped_thing.visible_message(span_boldwarning("[dropped_thing] falls into [parent]!"), span_userdanger("[oblivion_message]"))
if (isliving(dropped_thing))
var/mob/living/falling_mob = dropped_thing
ADD_TRAIT(falling_mob, TRAIT_NO_TRANSFORM, REF(src))
falling_mob.Paralyze(20 SECONDS)
falling_mob.Stun(20 SECONDS, ignore_canstun = TRUE)
if (HAS_MIND_TRAIT(falling_mob, TRAIT_NAIVE))
falling_mob.do_alert_animation()
dropped_thing.visible_message(span_boldwarning("[dropped_thing] kicks [dropped_thing.p_their()] legs in the air, as if running in place!"))
dropped_thing.Shake(1, 0, 2 SECONDS, 0.3 SECONDS)
sleep(3 SECONDS)
if (get_turf(falling_mob) != get_turf(parent))
REMOVE_TRAIT(falling_mob, TRAIT_NO_TRANSFORM, REF(src))
falling_mob.Paralyze(17 SECONDS, ignore_canstun = TRUE) // Wow nice job
return
dropped_thing.visible_message(span_boldwarning("[dropped_thing] falls into [parent]!"), span_userdanger("[oblivion_message]"))
var/oldtransform = dropped_thing.transform
var/oldcolor = dropped_thing.color
@@ -222,7 +236,7 @@
fallen_mob.death(TRUE)
fallen_mob.apply_damage(300)
falling_atoms -= falling_ref
LAZYREMOVE(falling_atoms, falling_ref)
/**
* Called when something has left the chasm depths storage.
-29
View File
@@ -1,29 +0,0 @@
/// Allows objects that entered parent's tile to move freely through other objects with this component regardless of density
/datum/component/climb_walkable
/datum/component/climb_walkable/RegisterWithParent()
var/static/list/turf_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_enter),
COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON = PROC_REF(on_enter),
COMSIG_ATOM_EXITED = PROC_REF(on_exit),
)
AddComponent(/datum/component/connect_loc_behalf, parent, turf_connections)
RegisterSignal(parent, COMSIG_ATOM_TRIED_PASS, PROC_REF(can_allow_through))
/datum/component/climb_walkable/UnregisterFromParent()
UnregisterSignal(parent, COMSIG_ATOM_TRIED_PASS)
for (var/atom/movable/climber in get_turf(parent))
REMOVE_TRAIT(climber, TRAIT_ON_CLIMBABLE, REF(src))
/datum/component/climb_walkable/proc/on_enter(datum/source, atom/movable/arrived)
SIGNAL_HANDLER
ADD_TRAIT(arrived, TRAIT_ON_CLIMBABLE, REF(src))
/datum/component/climb_walkable/proc/on_exit(datum/source, atom/movable/gone, direction)
SIGNAL_HANDLER
REMOVE_TRAIT(gone, TRAIT_ON_CLIMBABLE, REF(src))
/datum/component/climb_walkable/proc/can_allow_through(datum/source, atom/movable/mover, border_dir)
SIGNAL_HANDLER
if(HAS_TRAIT(mover, TRAIT_ON_CLIMBABLE))
return COMSIG_COMPONENT_PERMIT_PASSAGE
+1 -1
View File
@@ -42,7 +42,7 @@
return
// don't skip codewords when owner speaks
if(!owner.can_hear() || !owner.has_language(hearing_args[HEARING_LANGUAGE]))
if(HAS_TRAIT(owner, TRAIT_DEAF) || !owner.has_language(hearing_args[HEARING_LANGUAGE]))
return
var/message = hearing_args[HEARING_RAW_MESSAGE]
+24 -18
View File
@@ -8,7 +8,7 @@
///type paths of items consumed associated with how many are needed
var/list/reqs = list()
///type paths of items explicitly not allowed as an ingredient
var/list/blacklist = list()
var/list/blacklist
///type path of item resulting from this craft
var/result
/// String defines of items needed but not consumed. Lazy list.
@@ -23,13 +23,13 @@
///time in seconds. Remember to use the SECONDS define!
var/time = 3 SECONDS
///type paths of items that will be forceMoved() into the result instead of being deleted
var/list/parts = list()
var/list/parts
///items, structures and machineries of types that are in this list won't transfer their materials to the result
var/list/requirements_mats_blacklist
///if set, the materials in this list and their values will be subtracted from the result.
var/list/removed_mats
///like tool_behaviors but for reagents
var/list/chem_catalysts = list()
var/list/chem_catalysts
///where it shows up in the crafting UI
var/category
///Required machines for the craft, set the assigned value of the typepath to CRAFTING_MACHINERY_CONSUME or CRAFTING_MACHINERY_USE. Lazy associative list: type_path key -> flag value.
@@ -50,19 +50,15 @@
var/delete_contents = TRUE
/// Allows you to craft so that you don't have to click the craft button many times.
var/mass_craftable = FALSE
///crafting_flags var to hold bool values
var/crafting_flags = CRAFT_CHECK_DENSITY
/datum/crafting_recipe/New()
if(!name && result)
var/atom/atom_result = result
name = initial(atom_result.name)
if(!(result in reqs))
blacklist += result
// These should be excluded from all crafting recipies
blacklist += list(
/**
* Should the recipe blacklist its result? Default behavior is to blacklist any result that isn't in reqs.
* Can be set to ALWAYS_BLACKLIST_RESULT or NEVER_BLACKLIST_RESULT to override the default behavior.
*/
var/blacklist_result = BLACKLIST_RESULT_IF_NOT_IN_REQS
/// Global crafting blacklist. These should be excluded from all crafting recipes no matter what.
var/static/list/global_blacklist = typecacheof(list(
/obj/item/cautery/augment,
/obj/item/cautery/cruel/augment,
/obj/item/circular_saw/augment,
@@ -81,15 +77,25 @@
/obj/item/weldingtool/largetank/cyborg,
/obj/item/wirecutters/cyborg,
/obj/item/wrench/cyborg,
)
))
/datum/crafting_recipe/New()
if(!name && result)
var/atom/atom_result = result
name = initial(atom_result.name)
if(result && blacklist_result == BLACKLIST_RESULT_IF_NOT_IN_REQS && !(result in reqs))
blacklist_result = ALWAYS_BLACKLIST_RESULT
if(tool_behaviors)
tool_behaviors = string_list(tool_behaviors)
if(tool_paths)
tool_paths = string_list(tool_paths)
for(var/key in parts)
if(!parts[key])
for(var/key, part in parts)
if(!part)
//ensure every single, same-type part used for the recipe will be transferred if the value is otherwise not specified
parts[key] = INFINITY
part = INFINITY
/datum/crafting_recipe/stack
abstract_type = /datum/crafting_recipe/stack
/datum/crafting_recipe/stack/New(obj/item/stack/material, datum/stack_recipe/stack_recipe)
if(!material || !stack_recipe || !stack_recipe.result_type)
+17 -13
View File
@@ -68,12 +68,17 @@
var/list/requirements_list = list()
// Process all requirements
for(var/requirement_path in recipe.reqs)
var/recipe_result
if(recipe.blacklist_result)
recipe_result = recipe.result
for(var/requirement_path, needed_amount in recipe.reqs)
// Check we have the appropriate amount available in the contents list
var/needed_amount = recipe.reqs[requirement_path]
for(var/content_item_path in contents)
// Right path and not blacklisted
if(!ispath(content_item_path, requirement_path) || recipe.blacklist.Find(content_item_path))
if(!ispath(content_item_path, requirement_path) || (content_item_path in recipe.blacklist) || is_type_in_typecache(recipe.global_blacklist, content_item_path))
continue
// If we are a recipe that is blacklisting its result, make sure we skip that path
if(recipe_result && content_item_path == recipe_result)
continue
needed_amount -= contents[content_item_path]
@@ -85,14 +90,14 @@
// Store the instances of what we will use for recipe.check_requirements() for requirement_path
var/list/instances_list = list()
for(var/instance_path in item_instances)
for(var/instance_path, item_instance in item_instances)
if(ispath(instance_path, requirement_path))
instances_list += item_instances[instance_path]
instances_list += item_instance
requirements_list[requirement_path] = instances_list
for(var/requirement_path in recipe.chem_catalysts)
if(contents[requirement_path] < recipe.chem_catalysts[requirement_path])
for(var/requirement_path, chem_amount in recipe.chem_catalysts)
if(contents[requirement_path] < chem_amount)
return FALSE
var/mech_found = FALSE
@@ -250,8 +255,7 @@
var/list/total_materials = list()
var/list/stuff_to_use = get_used_reqs(recipe, crafter, total_materials)
for(var/mat in recipe.removed_mats)
var/to_remove = recipe.removed_mats[mat]
for(var/mat, to_remove in recipe.removed_mats)
var/datum/material/ref_mat = locate(mat) in total_materials
if(!ref_mat)
continue
@@ -683,18 +687,18 @@
data["structures"] += atoms.Find(req_atom)
// Ingredients / Materials
data["reqs"] = list()
if(recipe.reqs.len)
data["reqs"] = list()
for(var/req_atom in recipe.reqs)
var/id = atoms.Find(req_atom)
data["reqs"]["[id]"] = recipe.reqs[req_atom]
// Catalysts
if(recipe.chem_catalysts.len)
if(LAZYLEN(recipe.chem_catalysts))
data["chem_catalysts"] = list()
for(var/req_atom in recipe.chem_catalysts)
for(var/req_atom, chem_amount in recipe.chem_catalysts)
var/id = atoms.Find(req_atom)
data["chem_catalysts"]["[id]"] = recipe.chem_catalysts[req_atom]
data["chem_catalysts"]["[id]"] = chem_amount
// Reaction data
if(ispath(recipe.reaction))
+18 -27
View File
@@ -10,8 +10,8 @@
category = CAT_EQUIPMENT
/datum/crafting_recipe/strobeshield/New()
..()
blacklist |= subtypesof(/obj/item/shield/riot)
LAZYADD(blacklist, typecacheof(/obj/item/shield/riot, ignore_root_path = TRUE))
return ..()
/datum/crafting_recipe/improvisedshield
name = "Improvised Shield"
@@ -33,8 +33,19 @@
time = 4 SECONDS
category = CAT_EQUIPMENT
/datum/crafting_recipe/radio_containing
abstract_type = /datum/crafting_recipe/radio_containing
/// Shared blacklist of all the radio types for anything that uses a radio in its construction, so we don't repeat it.
var/static/list/radio_types_blacklist
/datum/crafting_recipe/radiogloves
/datum/crafting_recipe/radio_containing/New()
if(isnull(radio_types_blacklist))
// because we got shit like /obj/item/radio/off ... WHY!?!
radio_types_blacklist = typecacheof(list(/obj/item/radio/headset, /obj/item/radio/intercom))
blacklist = radio_types_blacklist
return ..()
/datum/crafting_recipe/radio_containing/radiogloves
name = "Radio Gloves"
result = /obj/item/clothing/gloves/radio
time = 1.5 SECONDS
@@ -46,11 +57,6 @@
tool_behaviors = list(TOOL_WIRECUTTER)
category = CAT_EQUIPMENT
/datum/crafting_recipe/radiogloves/New()
..()
blacklist |= typesof(/obj/item/radio/headset)
blacklist |= typesof(/obj/item/radio/intercom)
/datum/crafting_recipe/wheelchair
name = "Wheelchair"
result = /obj/vehicle/ridden/wheelchair
@@ -163,8 +169,8 @@
category = CAT_EQUIPMENT
/datum/crafting_recipe/flashlight_eyes/New()
. = ..()
blacklist += typesof(/obj/item/flashlight/flare)
LAZYADD(blacklist, typecacheof(/obj/item/flashlight/flare))
return ..()
/datum/crafting_recipe/extendohand_r
name = "Extendo-Hand (Right Arm)"
@@ -248,21 +254,6 @@
result = /obj/item/clothing/gloves/tackler/offbrand
category = CAT_EQUIPMENT
/**
* Recipe used for upgrading fake N-spect scanners to bananium HONK-spect scanners
*/
/datum/crafting_recipe/clown_scanner_upgrade
name = "Bananium HONK-spect scanner"
result = /obj/item/inspector/clown/bananium
reqs = list(
/obj/item/inspector/clown = 1,
/obj/item/stack/sticky_tape = 3,
/obj/item/stack/sheet/mineral/bananium = 5,
) //the chainsaw of prank tools
tool_paths = list(/obj/item/bikehorn)
time = 40 SECONDS
category = CAT_EQUIPMENT
/datum/crafting_recipe/rebar_quiver
name = "Rebar Storage Quiver"
result = /obj/item/storage/bag/rebar_quiver
@@ -311,8 +302,8 @@
tool_behaviors = list(TOOL_WELDER, TOOL_SCREWDRIVER, TOOL_WIRECUTTER)
/datum/crafting_recipe/morbid_surgical_toolset/New()
..()
blacklist |= subtypesof(/obj/item/organ/cyberimp/arm/toolkit/surgery)
LAZYADD(blacklist, typecacheof(/obj/item/organ/cyberimp/arm/toolkit/surgery, ignore_root_path = TRUE))
return ..()
/datum/crafting_recipe/surgical_toolset
name = "Surgical Toolset Implant"
@@ -81,3 +81,40 @@
tool_behaviors = list(TOOL_SCREWDRIVER)
category = CAT_FURNITURE
time = 10 SECONDS
/datum/crafting_recipe/lamp
name = "Desk Lamp"
result = /obj/item/flashlight/lamp
reqs = list(
/obj/item/flashlight = 1,
/obj/item/stack/rods = 1,
/obj/item/light/bulb = 1,
)
crafting_flags = CRAFT_SKIP_MATERIALS_PARITY
tool_behaviors = list(TOOL_SCREWDRIVER)
category = CAT_FURNITURE
/datum/crafting_recipe/lamp/New()
. = ..()
LAZYADD(blacklist, subtypesof(/obj/item/flashlight))
/datum/crafting_recipe/lamp/green
name = "Green Desk Lamp"
result = /obj/item/flashlight/lamp/green
/datum/crafting_recipe/banana_lamp
name = "Banana Lamp"
result = /obj/item/flashlight/lamp/bananalamp
reqs = list(
/obj/item/flashlight/lamp = 1,
/obj/item/grown/bananapeel = 1,
)
blacklist = list(
/obj/item/flashlight/lamp/bananalamp,
/obj/item/flashlight/lamp/space_bubble,
)
requirements_mats_blacklist = list(/obj/item/grown/bananapeel)
tool_behaviors = list(TOOL_SCREWDRIVER)
category = CAT_FURNITURE
@@ -89,7 +89,6 @@
time = 4 SECONDS
category = CAT_WEAPON_MELEE
/datum/crafting_recipe/balloon_mallet
name = "Balloon Mallet"
result = /obj/item/balloon_mallet
@@ -255,3 +254,15 @@
)
time = 120 SECONDS
category = CAT_WEAPON_MELEE
/datum/crafting_recipe/dragonator_spear_untreated
name = "Untreated Giant-Killer Spear"
result = /obj/item/spear/dragonator_untreated
reqs = list(
/obj/item/stack/sheet/plasteel = 15,
/obj/item/stack/sheet/mineral/titanium = 5,
/obj/item/knife = 7,
)
time = 5 SECONDS
category = CAT_WEAPON_MELEE
@@ -67,8 +67,8 @@
category = CAT_WEAPON_RANGED
/datum/crafting_recipe/advancedegun/New()
..()
blacklist += subtypesof(/obj/item/gun/energy/e_gun)
LAZYADD(blacklist, typecacheof(/obj/item/gun/energy/e_gun, ignore_root_path = TRUE))
return ..()
/datum/crafting_recipe/tempgun
name = "Temperature Gun"
@@ -81,8 +81,8 @@
category = CAT_WEAPON_RANGED
/datum/crafting_recipe/tempgun/New()
..()
blacklist += subtypesof(/obj/item/gun/energy/e_gun)
LAZYADD(blacklist, typecacheof(/obj/item/gun/energy/e_gun, ignore_root_path = TRUE))
return ..()
/datum/crafting_recipe/beam_rifle
name = "Event Horizon Anti-Existential Beam Rifle"
@@ -108,7 +108,18 @@
time = 10 SECONDS
category = CAT_WEAPON_RANGED
/datum/crafting_recipe/xraylaser
/datum/crafting_recipe/laser
abstract_type = /datum/crafting_recipe/laser
/// We will use the same blacklist for every type here to avoid repeating lists
var/static/list/laser_blacklist
/datum/crafting_recipe/laser/New()
if(isnull(laser_blacklist))
laser_blacklist = typecacheof(/obj/item/gun/energy/laser, ignore_root_path = TRUE)
blacklist = laser_blacklist
return ..()
/datum/crafting_recipe/laser/xraylaser
name = "X-ray Laser Gun"
result = /obj/item/gun/energy/laser/xray
reqs = list(
@@ -118,11 +129,7 @@
time = 10 SECONDS
category = CAT_WEAPON_RANGED
/datum/crafting_recipe/xraylaser/New()
..()
blacklist += subtypesof(/obj/item/gun/energy/laser)
/datum/crafting_recipe/hellgun
/datum/crafting_recipe/laser/hellgun
name = "Hellfire Laser Gun"
result = /obj/item/gun/energy/laser/hellgun
reqs = list(
@@ -132,11 +139,7 @@
time = 10 SECONDS
category = CAT_WEAPON_RANGED
/datum/crafting_recipe/hellgun/New()
..()
blacklist += subtypesof(/obj/item/gun/energy/laser)
/datum/crafting_recipe/ioncarbine
/datum/crafting_recipe/laser/ioncarbine
name = "Ion Carbine"
result = /obj/item/gun/energy/ionrifle/carbine
reqs = list(
@@ -146,10 +149,6 @@
time = 10 SECONDS
category = CAT_WEAPON_RANGED
/datum/crafting_recipe/ioncarbine/New()
..()
blacklist += subtypesof(/obj/item/gun/energy/laser)
/datum/crafting_recipe/teslacannon
name = "Tesla Cannon"
result = /obj/item/gun/energy/tesla_cannon
@@ -241,8 +240,8 @@
/obj/item/gun/ballistic/rifle/rebarxbow = 1,
)
blacklist = list(
/obj/item/gun/ballistic/rifle/rebarxbow/forced,
/obj/item/gun/ballistic/rifle/rebarxbow/syndie,
/obj/item/gun/ballistic/rifle/rebarxbow/forced,
/obj/item/gun/ballistic/rifle/rebarxbow/syndie,
)
tool_behaviors = list(TOOL_CROWBAR)
time = 1 SECONDS
@@ -289,8 +288,8 @@
crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_MUST_BE_LEARNED
/datum/crafting_recipe/deagle_prime/New()
..()
blacklist += subtypesof(/obj/item/gun/ballistic/automatic/pistol)
LAZYADD(blacklist, typecacheof(/obj/item/gun/ballistic/automatic/pistol, ignore_root_path = TRUE))
return ..()
/datum/crafting_recipe/deagle_prime_mag
name = "Regal Condor Magazine (10mm Reaper)"
@@ -316,7 +315,7 @@
/datum/crafting_recipe/pipe_organ_gun
name = "Pipe Organ Gun"
tool_behaviors = list(TOOL_WELDER, TOOL_SCREWDRIVER)
result = /obj/structure/mounted_gun/pipe
result = /obj/structure/mounted_gun/organ_gun
reqs = list(
/obj/item/pipe = 8,
/obj/item/stack/sheet/mineral/wood = 15,
@@ -329,6 +328,47 @@
category = CAT_WEAPON_RANGED
crafting_flags = CRAFT_CHECK_DENSITY
/datum/crafting_recipe/ratvarian_repeater
name = "Emplaced Ratvarian Repeater"
tool_behaviors = list(TOOL_SCREWDRIVER,TOOL_WRENCH)
result = /obj/structure/mounted_gun/ratvarian_repeater
reqs = list(
/obj/item/stack/cable_coil = 15,
/obj/item/stock_parts/micro_laser = 2,
/obj/item/stock_parts/capacitor = 2,
/obj/item/reagent_containers/cup/glass/drinkingglass = 2,
/obj/item/stack/sheet/bronze = 5,
/obj/item/stack/rods = 10,
)
time = 15 SECONDS
category = CAT_WEAPON_RANGED
crafting_flags = CRAFT_CHECK_DENSITY
/datum/crafting_recipe/detached_ratvarian_repeater
name = "Iconoclast's Repeater"
tool_behaviors = list(TOOL_WELDER)
result = /obj/item/gun/energy/laser/musket/repeater
structures = list(
/obj/structure/mounted_gun/ratvarian_repeater = CRAFTING_STRUCTURE_CONSUME,
)
time = 10 SECONDS
category = CAT_WEAPON_RANGED
crafting_flags = CRAFT_CHECK_DENSITY
/datum/crafting_recipe/large_ballista
name = "Improvised Ballista"
tool_behaviors = list(TOOL_WELDER,TOOL_SCREWDRIVER,TOOL_WRENCH,TOOL_WIRECUTTER)
result = /obj/structure/mounted_gun/ballista
reqs = list(
/obj/item/stack/cable_coil = 15,
/obj/item/stack/sheet/iron = 10,
/obj/item/stack/rods = 10,
)
time = 8 SECONDS
category = CAT_WEAPON_RANGED
crafting_flags = CRAFT_CHECK_DENSITY
/datum/crafting_recipe/trash_cannon
name = "Trash Cannon"
tool_behaviors = list(TOOL_WELDER, TOOL_SCREWDRIVER)
+37 -16
View File
@@ -117,6 +117,14 @@
time = 4 SECONDS
category = CAT_CLOTHING
/datum/crafting_recipe/fannypack
name = "Fannypack"
result = /obj/item/storage/belt/fannypack
reqs = list(/obj/item/stack/sheet/cloth = 2,
/obj/item/stack/sheet/leather = 1)
time = 2 SECONDS
category = CAT_CONTAINERS
/datum/crafting_recipe/grass_sheath
name = "Grass Sabre Sheath"
result = /obj/item/storage/belt/sheath/grass_sabre
@@ -125,13 +133,31 @@
time = 4 SECONDS
category = CAT_CONTAINERS
/datum/crafting_recipe/fannypack
name = "Fannypack"
result = /obj/item/storage/belt/fannypack
reqs = list(/obj/item/stack/sheet/cloth = 2,
/obj/item/stack/sheet/leather = 1)
time = 2 SECONDS
category = CAT_CONTAINERS
/datum/crafting_recipe/gunsheath
name = "Gun Powered Sabre Sheath"
result = /obj/item/storage/belt/sheath/sabre/gunpowered
tool_behaviors = list(TOOL_SCREWDRIVER, TOOL_WRENCH)
reqs = list(/datum/reagent/gunpowder = 15,
/obj/item/storage/belt/sheath/sabre = 1,
/obj/item/weaponcrafting/receiver = 1,
/obj/item/stack/cable_coil = 5,
)
time = 15 SECONDS
category = CAT_EQUIPMENT
crafting_flags = parent_type::crafting_flags | CRAFT_SKIP_MATERIALS_PARITY
/datum/crafting_recipe/grass_gunsheath
name = "Gun Powered Grass Sabre Sheath"
result = /obj/item/storage/belt/sheath/grass_sabre/gunpowered
tool_behaviors = list(TOOL_SCREWDRIVER, TOOL_WRENCH)
reqs = list(/datum/reagent/gunpowder = 15,
/obj/item/storage/belt/sheath/grass_sabre = 1,
/obj/item/weaponcrafting/receiver = 1,
/obj/item/stack/cable_coil = 5,
)
time = 15 SECONDS
category = CAT_EQUIPMENT
crafting_flags = parent_type::crafting_flags | CRAFT_SKIP_MATERIALS_PARITY
/datum/crafting_recipe/hudsunsec
name = "Security HUDsunglasses"
@@ -219,8 +245,8 @@
/datum/crafting_recipe/lizardboots
name = "Lizard Skin Boots"
result = /obj/effect/spawner/random/clothing/lizardboots
reqs = list(/obj/item/stack/sheet/animalhide/lizard = 1, /obj/item/stack/sheet/leather = 1)
result = /obj/item/clothing/shoes/cowboy/lizard
reqs = list(/obj/item/stack/sheet/animalhide/carbon/lizard = 1, /obj/item/stack/sheet/leather = 1)
time = 6 SECONDS
category = CAT_CLOTHING
@@ -264,7 +290,7 @@
name = "Lizard Cloche Hat"
result = /obj/item/clothing/head/costume/lizard
time = 1 SECONDS
reqs = list(/obj/item/stack/sheet/animalhide/lizard = 1)
reqs = list(/obj/item/stack/sheet/animalhide/carbon/lizard = 1)
category = CAT_CLOTHING
/datum/crafting_recipe/kittyears
@@ -421,12 +447,7 @@
)
reqs = list(/obj/item/stack/sheet/cloth = 4)
category = CAT_CLOTHING
/datum/crafting_recipe/chaplain_hood/New()
. = ..()
//the resulting hoodie can be used to craft other hoodies.
//recipe blacklists should be refactored to only affect components and not tools.
blacklist -= result
blacklist_result = NEVER_BLACKLIST_RESULT //the resulting hoodie can be used to craft other hoodies.
/datum/crafting_recipe/flower_garland
name = "Flower Garland"
+12
View File
@@ -131,3 +131,15 @@
)
category = CAT_TOOLS
/datum/crafting_recipe/lantern
name = "Lantern"
result = /obj/item/flashlight/lantern
reqs = list(
/obj/item/flashlight/flare/candle = 1,
/obj/item/stack/rods = 1,
/obj/item/stack/sheet/glass = 1,
)
crafting_flags = CRAFT_SKIP_MATERIALS_PARITY
tool_behaviors = list(TOOL_SCREWDRIVER)
category = CAT_TOOLS
@@ -0,0 +1,85 @@
/// Component to handle the behavior of a nullrod keeping track of cultists it has crit or killed, and converting the item into a cult weapon when sacrificed
/datum/component/cult_kill_tracker
/// Lazylist, tracks weakrefs()s to all cultists which have been crit or killed by this nullrod.
var/list/cultists_slain
/// Ref to the last mob hit with this rod.
var/last_ref
/// The stat of the target being hit with this rod, before actually performing damage calculations.
var/last_stat = DEAD
/datum/component/cult_kill_tracker/Initialize(...)
if(!istype(parent, /obj/item))
return COMPONENT_INCOMPATIBLE
/datum/component/cult_kill_tracker/RegisterWithParent()
. = ..()
RegisterSignal(parent, COMSIG_ITEM_ATTACK_ZONE, PROC_REF(on_attack_zone))
RegisterSignal(parent, COMSIG_ITEM_AFTERATTACK, PROC_REF(post_hit))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
RegisterSignal(parent, COMSIG_ITEM_CULT_SACRIFICE, PROC_REF(on_sacrificed))
if(isgun(parent))
RegisterSignal(parent, COMSIG_PROJECTILE_ON_HIT, PROC_REF(on_projectile_hit))
RegisterSignal(parent, COMSIG_PROJECTILE_POST_HIT_LIVING, PROC_REF(post_hit))
/datum/component/cult_kill_tracker/UnregisterFromParent()
. = ..()
UnregisterSignal(parent, list(COMSIG_ITEM_ATTACK_ZONE, COMSIG_ITEM_AFTERATTACK, COMSIG_ATOM_EXAMINE, COMSIG_ITEM_CULT_SACRIFICE, COMSIG_PROJECTILE_ON_HIT, COMSIG_PROJECTILE_POST_HIT_LIVING))
/datum/component/cult_kill_tracker/proc/on_attack_zone(obj/item/source, mob/living/target, mob/living/user)
SIGNAL_HANDLER
if(!user.mind?.holy_role)
return
if(!IS_CULTIST(target) || istype(target, /mob/living/carbon/human/cult_ghost))
return
last_ref = WEAKREF(target)
last_stat = target.stat
/datum/component/cult_kill_tracker/proc/post_hit(source, mob/living/target)
SIGNAL_HANDLER
if(!last_ref)
return
var/mob/living/resolved_mob = locate(last_ref)
//If they got deleted during the processing of the attack, they're probably fucking dead.
if(!istype(resolved_mob) || (resolved_mob == target && resolved_mob.stat > last_stat))
LAZYOR(cultists_slain, last_ref)
last_ref = null
last_stat = DEAD
/datum/component/cult_kill_tracker/proc/on_examine(obj/item/source, mob/viewer, list/examine_list)
SIGNAL_HANDLER
if(!IS_CULTIST(viewer) || !GET_ATOM_BLOOD_DNA_LENGTH(source))
return
var/num_slain = LAZYLEN(cultists_slain)
examine_list += span_cult_italic("It has the blood of [num_slain] fallen cultist[num_slain == 1 ? "" : "s"] on it. \
<b>Offering</b> it to Nar'sie will transform it into a [num_slain >= 3 ? "powerful" : "standard"] cult weapon.")
/datum/component/cult_kill_tracker/proc/on_sacrificed(obj/item/source, obj/effect/rune/convert/rune)
SIGNAL_HANDLER
var/num_slain = LAZYLEN(cultists_slain)
var/displayed_message = "[source] glows an unholy red and begins to transform..."
if(num_slain && GET_ATOM_BLOOD_DNA_LENGTH(source))
displayed_message += " The blood of [num_slain] fallen cultist[num_slain == 1 ? "":"s"] is absorbed into [source]!"
source.visible_message(span_cult_italic(displayed_message))
switch(num_slain)
if(0)
rune.animate_convert_item(source, /obj/item/melee/cultblade/dagger)
if(1)
rune.animate_convert_item(source, /obj/item/melee/cultblade)
else
rune.animate_convert_item(source, /obj/item/melee/cultblade/halberd)
return COMPONENT_SACRIFICE_SUCCESSFUL
/datum/component/cult_kill_tracker/proc/on_projectile_hit(obj/projectile/source, atom/movable/firer, atom/target)
SIGNAL_HANDLER
if(!isliving(firer) || !isliving(target))
return
var/mob/living/firer_mob = firer
var/mob/living/target_mob = target
if(!firer_mob.mind.holy_role)
return
if(!IS_CULTIST(target_mob) || istype(target_mob, /mob/living/carbon/human/cult_ghost))
return
last_ref = WEAKREF(target_mob)
last_stat = target_mob.stat
+1 -1
View File
@@ -90,7 +90,7 @@
/// The requirements for the mob to be effected by the damage aura.
/datum/component/damage_aura/proc/check_requirements(mob/living/target_mob)
if(target_mob.stat == DEAD || faction_check(target_mob.faction, immune_factions))
if(target_mob.stat == DEAD || target_mob.has_faction(immune_factions))
return TRUE
return FALSE
+4 -4
View File
@@ -137,12 +137,12 @@
no_rewinds_message = "\"Rewind complete. You have arrived at: 10 seconds ago.\""
/datum/component/dejavu/timeline/rewind()
playsound(get_turf(parent), 'sound/items/modsuit/rewinder.ogg')
. = ..()
playsound(get_turf(parent), 'sound/items/modsuit/rewinder.ogg', 50, TRUE)
return ..()
/datum/component/dejavu/wizard
rewind_message = "Your temporal ward activated, pulling you through spacetime!"
/datum/component/dejavu/wizard/rewind()
playsound(get_turf(parent), 'sound/items/modsuit/rewinder.ogg')
. = ..()
playsound(get_turf(parent), 'sound/items/modsuit/rewinder.ogg', 50, TRUE)
return ..()
+4 -5
View File
@@ -23,9 +23,9 @@
RegisterSignal(SSticker, COMSIG_TICKER_ROUND_STARTING, PROC_REF(comp_on_round_start))
return
//if the round has already taken long enough, just leap right away.
//if the round has already taken long enough, evolve in three seconds.
if((world.time - SSticker.round_start_time) > evolve_mark)
leap(silent = TRUE)
addtimer(CALLBACK(src, PROC_REF(leap)), 3 SECONDS, TIMER_DELETE_ME)
return
setup_timer()
@@ -49,12 +49,11 @@
var/mark = evolve_mark - sum
timer_id = addtimer(CALLBACK(src, PROC_REF(leap), FALSE), mark, TIMER_STOPPABLE)
/datum/component/evolutionary_leap/proc/leap(silent)
/datum/component/evolutionary_leap/proc/leap()
var/mob/living/old_mob = parent
if (old_mob.stat == DEAD)
return
var/mob/living/new_mob = evolve_path
var/new_mob_name = initial(new_mob.name)
if(!silent)
old_mob.visible_message(span_warning("[old_mob] evolves into \a [new_mob_name]!"))
old_mob.visible_message(span_warning("[old_mob] evolves into \a [new_mob_name]!"))
old_mob.change_mob_type(evolve_path, old_mob.loc, new_name = new_mob_name, delete_old_mob = TRUE)
+1 -1
View File
@@ -51,5 +51,5 @@
return
to_chat(user, grant_message)
user.faction |= faction_to_grant
user.add_faction(faction_to_grant)
used = TRUE
+16 -14
View File
@@ -148,20 +148,22 @@
/datum/fantasy_affix/shrapnel/apply(datum/component/fantasy/comp, newName)
. = ..()
// higher means more likely
var/list/weighted_projectile_types = list(/obj/projectile/meteor = 1,
/obj/projectile/energy/nuclear_particle = 1,
/obj/projectile/beam/pulse = 1,
/obj/projectile/bullet/honker = 15,
/obj/projectile/temp = 15,
/obj/projectile/ion = 15,
/obj/projectile/magic/door = 15,
/obj/projectile/magic/locker = 15,
/obj/projectile/magic/fetch = 15,
/obj/projectile/beam/emitter = 15,
/obj/projectile/magic/flying = 15,
/obj/projectile/bullet/incendiary/c9mm = 15,
/obj/projectile/temp/hot = 15,
/obj/projectile/beam/disabler = 15)
var/list/weighted_projectile_types = list(
/obj/projectile/beam/disabler = 15,
/obj/projectile/beam/emitter = 15,
/obj/projectile/bullet/honker = 15,
/obj/projectile/beam/pulse = 1,
/obj/projectile/bullet/incendiary/c9mm = 15,
/obj/projectile/energy/nuclear_particle = 1,
/obj/projectile/ion = 15,
/obj/projectile/magic/door = 15,
/obj/projectile/magic/fetch = 15,
/obj/projectile/magic/flying = 15,
/obj/projectile/magic/locker = 15,
/obj/projectile/meteor = 1,
/obj/projectile/temp = 15,
/obj/projectile/temp/hot = 15,
)
var/obj/projectile/picked_projectiletype = pick_weight(weighted_projectile_types)
+48 -1
View File
@@ -52,7 +52,7 @@
/datum/terror_handler/heart_problems
handler_type = TERROR_HANDLER_EFFECT
default = TRUE
COOLDOWN_DECLARE(effect_cd)
COOLDOWN_DECLARE(message_cd)
/datum/terror_handler/heart_problems/tick(seconds_per_tick, terror_buildup)
. = ..()
@@ -67,6 +67,9 @@
if (terror_buildup < TERROR_BUILDUP_HEART_ATTACK || !prob(15))
owner.adjust_oxy_loss(8)
if (!COOLDOWN_FINISHED(src, message_cd))
return
COOLDOWN_START(src, message_cd, TERROR_MESSAGE_CD)
if (terror_buildup < TERROR_BUILDUP_FEAR)
to_chat(owner, span_warning("Your heart skips a beat."))
else
@@ -167,4 +170,48 @@
deltimer(panic_end_timer)
panic_end_timer = null
/datum/terror_handler/startle
handler_type = TERROR_HANDLER_EFFECT
COOLDOWN_DECLARE(startle_cd)
/datum/terror_handler/startle/tick(seconds_per_tick, terror_buildup)
. = ..()
if (owner.stat >= UNCONSCIOUS || !COOLDOWN_FINISHED(src, startle_cd))
return
if (terror_buildup < TERROR_BUILDUP_FEAR || terror_buildup - component.last_tick_buildup < TERROR_STARTLE_MINIMUM_DIFFERENCE)
return
// The more scared we are, and the more fear we acquired at once last tick, the higher the probability of us being startled is
// Not SPT_PROB or FEAR_SCALING because this is only triggered during ticks when we actually acquire fear
if (!prob(15 * (terror_buildup - component.last_tick_buildup) / TERROR_STARTLE_MINIMUM_DIFFERENCE * terror_buildup / TERROR_BUILDUP_FEAR))
return
COOLDOWN_START(src, startle_cd, TERROR_STARTLE_COOLDOWN)
switch (rand(1, 3))
if (1)
to_chat(owner, span_warning("You are startled!"))
owner.emote("jump")
owner.Immobilize(0.1 SECONDS * (terror_buildup / TERROR_BUILDUP_FEAR))
if (2)
owner.emote("scream")
owner.say("AAAAH!!", forced = "phobia")
if (!prob(15 * (terror_buildup / TERROR_BUILDUP_FEAR)))
return
var/held_item = owner.get_active_held_item()
if (owner.dropItemToGround(held_item))
owner.visible_message(
span_danger("[owner.name] drops \the [held_item]!"),
span_warning("You drop \the [held_item]!"), null, COMBAT_MESSAGE_RANGE)
if (3)
to_chat(owner, span_warning("You lose your balance!"))
owner.adjust_staggered_up_to(2 SECONDS * (terror_buildup / TERROR_BUILDUP_FEAR), 20 SECONDS)
owner.add_movespeed_modifier(/datum/movespeed_modifier/status_effect/spooked)
addtimer(CALLBACK(src, PROC_REF(speed_up)), 3 SECONDS, TIMER_STOPPABLE | TIMER_DELETE_ME)
/datum/terror_handler/startle/proc/speed_up()
owner.remove_movespeed_modifier(/datum/movespeed_modifier/status_effect/spooked)
#undef FEAR_SCALING
+7 -5
View File
@@ -73,7 +73,7 @@
/datum/component/fearful/proc/add_handler(handler_type, source)
for (var/datum/terror_handler/existing as anything in terror_handlers)
if (existing.type == handler_type)
if (existing.type == handler_type && !existing.bespoke)
terror_handlers[existing] += source
return
@@ -83,6 +83,7 @@
if (!overriden_handlers[override_type])
overriden_handlers[override_type] = list()
overriden_handlers[override_type][handler_type] = TRUE
return handler
/datum/component/fearful/proc/get_fear_multiplier()
var/multiplier = 1
@@ -168,11 +169,12 @@
)
return COMPONENT_BLOCK_MISC_HELP
for (var/datum/brain_trauma/mild/phobia/phobia in source.get_traumas())
if (!phobia.is_scary_mob(hugger))
continue
var/hug_buildup = 0
for (var/datum/terror_handler/handler as anything in terror_handlers)
hug_buildup += handler.on_hug(hugger)
terror_buildup += HUG_TERROR_AMOUNT
if (hug_buildup > 0)
terror_buildup += hug_buildup
source.visible_message(
span_warning("[source] recoils in fear as [hugger] attempts to hug [source.p_them()]!"),
span_boldwarning("You recoil in terror as [hugger] attempts to hug you!"),
@@ -71,7 +71,7 @@
. = ..()
owner.clear_mood_event("nyctophobia")
// Nightmare spell version with quicker buildup that shadows the quirk one, also handles removing the status upon reaching 0 terror
/// Nightmare spell version with quicker buildup that shadows the quirk one, also handles removing the status upon reaching 0 terror
/datum/terror_handler/simple_source/nyctophobia/terrified
// Takes about 30 seconds to reach maximum if you include 100 from casting the spell
buildup_per_second = 15
@@ -87,6 +87,7 @@
/// Makes the owner afraid of being stuck in closets, crates, mechs, etc
/datum/terror_handler/simple_source/claustrophobia
buildup_per_second = 15
COOLDOWN_DECLARE(message_cd)
/datum/terror_handler/simple_source/claustrophobia/Destroy(force)
owner.clear_mood_event("claustrophobia")
@@ -100,8 +101,9 @@
if (isturf(owner.loc))
return FALSE
if (SPT_PROB(15, seconds_per_tick))
if (COOLDOWN_FINISHED(src, message_cd) && SPT_PROB(15, seconds_per_tick))
to_chat(owner, span_warning("You feel trapped! Must escape... can't breathe..."))
COOLDOWN_START(src, message_cd, TERROR_MESSAGE_CD)
return TRUE
@@ -116,6 +118,7 @@
/// Makes the owner afraid of certain jolly figures
/datum/terror_handler/simple_source/clausophobia
buildup_per_second = 20
COOLDOWN_DECLARE(message_cd)
/datum/terror_handler/simple_source/clausophobia/check_condition(seconds_per_tick, terror_buildup)
. = ..()
@@ -142,14 +145,16 @@
if (!certified_jolly)
return FALSE
if (SPT_PROB(15, seconds_per_tick))
if (COOLDOWN_FINISHED(src, message_cd) && SPT_PROB(15, seconds_per_tick))
to_chat(owner, span_warning("Santa Claus is here! I gotta get out of here!"))
COOLDOWN_START(src, message_cd, TERROR_MESSAGE_CD)
return TRUE
/// Makes the owner afraid of being alone
/datum/terror_handler/simple_source/monophobia
buildup_per_second = 2.5 // Pretty low, ~4 minutes to reach passive cap
COOLDOWN_DECLARE(message_cd)
/datum/terror_handler/simple_source/monophobia/check_condition(seconds_per_tick, terror_buildup)
. = ..()
@@ -167,4 +172,8 @@
if (istype(friend, /mob/living/basic/pet) || friend.ckey)
return FALSE
if (COOLDOWN_FINISHED(src, message_cd) && SPT_PROB(10, seconds_per_tick))
to_chat(owner, span_warning("You feel terribly lonely..."))
COOLDOWN_START(src, message_cd, TERROR_MESSAGE_CD)
return TRUE
@@ -0,0 +1,170 @@
/// Complex terror source that increases buildup whenever the owner hears, sees, or tries to say something they're afraid of
/datum/terror_handler/phobia_source
handler_type = TERROR_HANDLER_SOURCE
bespoke = TRUE
/// Last time we got scared shitless for passive increase in fear
/// Regex for words that set the phobia off
var/regex/trigger_regex
// Instead of cycling every atom, only cycle the relevant types
var/list/trigger_mobs
/// Includes mob equipment
var/list/trigger_objs
var/list/trigger_turfs
var/list/trigger_species
/// What mood event to apply when we see the thing & freak out.
var/mood_event_type = /datum/mood_event/phobia
/// Cooldown for proximity checks so we don't spam a range 7 view every two seconds.
COOLDOWN_DECLARE(check_cooldown)
/// Cooldown for the actual scare effect so chat spam won't instantly send us spiraling
COOLDOWN_DECLARE(scare_cooldown)
/datum/terror_handler/phobia_source/New(mob/living/new_owner, datum/component/fearful/new_component)
. = ..()
RegisterSignal(new_owner, COMSIG_MOB_SAY, PROC_REF(handle_speech))
RegisterSignal(new_owner, COMSIG_MOVABLE_HEAR, PROC_REF(handle_hearing))
/datum/terror_handler/phobia_source/Destroy(force)
UnregisterSignal(owner, list(COMSIG_MOB_SAY, COMSIG_MOVABLE_HEAR))
return ..()
/datum/terror_handler/phobia_source/proc/can_trigger()
return !HAS_TRAIT(owner, TRAIT_FEARLESS) && !HAS_TRAIT(owner, TRAIT_MIND_TEMPORARILY_GONE) && owner.stat < UNCONSCIOUS
/datum/terror_handler/phobia_source/tick(seconds_per_tick, terror_buildup)
. = ..()
if (!can_trigger())
return
// If we're still scared from the last trigger, keep adding just a tiiiiny buildup to prevent it from fading away
if (!COOLDOWN_FINISHED(src, scare_cooldown))
return 0.01
if(!COOLDOWN_FINISHED(src, check_cooldown) || owner.is_blind())
return
COOLDOWN_START(src, check_cooldown, PHOBIA_CHECK_DELAY)
if(LAZYLEN(trigger_objs))
for (var/obj/seen_thing in view(owner.client?.view || world.view, owner))
if(is_scary_item(seen_thing))
return freak_out(seen_thing)
if(LAZYLEN(trigger_turfs))
for(var/turf/checked in view(owner.client?.view || world.view, owner))
if(is_type_in_typecache(checked, trigger_turfs))
return freak_out(checked)
if(LAZYLEN(trigger_mobs) || LAZYLEN(trigger_species) || LAZYLEN(trigger_objs))
for(var/mob/living/checked in view(owner.client?.view || world.view, owner))
if (checked != owner && is_scary_mob(checked))
return freak_out(checked)
/// Returns true if this item should be scary to us
/datum/terror_handler/phobia_source/proc/is_scary_item(obj/checked)
if (QDELETED(checked) || !is_type_in_typecache(checked, trigger_objs) || checked.invisibility > owner.see_invisible)
return FALSE
if (!isitem(checked) || !ismob(checked.loc) || HAS_TRAIT(checked.loc, TRAIT_UNKNOWN_APPEARANCE))
return TRUE
var/obj/item/checked_item = checked
return !HAS_TRAIT(checked_item, TRAIT_EXAMINE_SKIP)
/datum/terror_handler/phobia_source/proc/is_scary_mob(mob/living/checked)
if (checked.invisibility > owner.see_invisible || checked.alpha == 0)
return FALSE
if (is_type_in_typecache(checked, trigger_mobs))
return TRUE
if (!ishuman(checked))
return FALSE
var/mob/living/carbon/human/as_human = checked
if (LAZYLEN(trigger_species))
// Can't be racist(?) if you can't see their face
if (is_type_in_typecache(as_human.dna?.species, trigger_species) && !as_human.is_face_obscured())
return TRUE
if (!LAZYLEN(trigger_objs))
return FALSE
for (var/obj/item/equipped as anything in as_human.get_visible_items())
if (is_scary_item(equipped))
return TRUE
return FALSE
/datum/terror_handler/phobia_source/proc/handle_hearing(datum/source, list/hearing_args)
SIGNAL_HANDLER
if (!can_trigger() || !COOLDOWN_FINISHED(src, scare_cooldown))
return
// Words can't trigger you if you can't hear them *taps head*
if(HAS_TRAIT(owner, TRAIT_DEAF) || owner == hearing_args[HEARING_SPEAKER] || !owner.has_language(hearing_args[HEARING_LANGUAGE]))
return
if(trigger_regex.Find(hearing_args[HEARING_RAW_MESSAGE]))
// To react AFTER the chat message
addtimer(CALLBACK(src, PROC_REF(freak_out), null, trigger_regex.group[2]), 1 SECONDS)
hearing_args[HEARING_RAW_MESSAGE] = trigger_regex.Replace(hearing_args[HEARING_RAW_MESSAGE], "[span_phobia("$2")]$3")
/datum/terror_handler/phobia_source/proc/handle_speech(datum/source, list/speech_args)
SIGNAL_HANDLER
if (!can_trigger())
return
if (trigger_regex.Find(speech_args[SPEECH_MESSAGE]) == 0)
return
var/stutter = prob(50)
var/whisper = prob(30)
if (!stutter && !whisper)
return
if (whisper)
speech_args[SPEECH_SPANS] |= SPAN_SMALL_VOICE
if (stutter)
owner.set_stutter_if_lower(4 SECONDS)
to_chat(owner, span_warning("You struggle to say the word \"[span_phobia("[trigger_regex.group[2]]")]\"!"))
/datum/terror_handler/phobia_source/proc/freak_out(reason)
COOLDOWN_START(src, scare_cooldown, 12 SECONDS)
var/message = pick("spooks you to the bone", "shakes you up", "terrifies you", "sends you into a panic", "sends chills down your spine")
if(istext(reason))
to_chat(owner, span_bolddanger("Hearing [span_phobia(reason)] [message]!"))
owner.add_mood_event("phobia_minor", /datum/mood_event/startled)
// Because this is called from a signal and not the main process, we need to add the buildup by hand
if (component.terror_buildup < TERROR_BUILDUP_PASSIVE_MAXIMUM)
component.terror_buildup = min(component.terror_buildup + PHOBIA_WORD_TERROR_BUILDUP, TERROR_BUILDUP_PASSIVE_MAXIMUM)
return
if(mood_event_type)
owner.add_mood_event("phobia", mood_event_type)
if(isatom(reason))
var/atom/as_atom = reason
to_chat(owner, span_bolddanger("Seeing [span_phobia("[as_atom.name]")] [message]!"))
else
to_chat(owner, span_bolddanger("Something [message]!"))
return PHOBIA_FREAKOUT_TERROR_BUILDUP
/datum/terror_handler/phobia_source/on_hug(mob/living/hugger)
if (is_scary_mob(hugger))
return HUG_TERROR_AMOUNT
return 0
/// Snowflake handler for hemophobia which triggers on bloodied items and mobs
/datum/terror_handler/phobia_source/blood
/datum/terror_handler/phobia_source/blood/is_scary_item(obj/checked)
if (GET_ATOM_BLOOD_DNA_LENGTH(checked))
return TRUE
return ..()
/datum/terror_handler/phobia_source/blood/is_scary_mob(mob/living/checked)
if (GET_ATOM_BLOOD_DNA_LENGTH(checked))
return TRUE
return ..()
@@ -11,6 +11,8 @@
var/default = FALSE
/// Other effects which should be disabled while this one is running
var/list/overrides
/// Is this handler bespoke, and always requires a new instance upon adding even when one is already present?
var/bespoke = FALSE
/datum/terror_handler/New(mob/living/new_owner, datum/component/fearful/new_component)
. = ..()
@@ -25,3 +27,7 @@
/// Single tick of terror handler, returns adjustment to terror buildup
/datum/terror_handler/proc/tick(seconds_per_tick, terror_buildup)
return 0
/// Additional effects when we're hugged by a mob, returns fear adjustment per hug. Should only be positive.
/datum/terror_handler/proc/on_hug(mob/living/hugger)
return 0
+1 -1
View File
@@ -1,5 +1,6 @@
// A thing you can fish in
/datum/component/fishing_spot
dupe_mode = COMPONENT_DUPE_UNIQUE
/// Defines the probabilities and fish availibilty
var/datum/fish_source/fish_source
@@ -26,7 +27,6 @@
REMOVE_TRAIT(parent, TRAIT_FISHING_SPOT, REF(src))
fish_source.on_fishing_spot_del(src)
fish_source = null
REMOVE_TRAIT(parent, TRAIT_FISHING_SPOT, REF(src))
return ..()
/datum/component/fishing_spot/proc/handle_cast(datum/source, obj/item/fishing_rod/rod, mob/user)
+4 -2
View File
@@ -568,8 +568,10 @@ Behavior that's still missing from this component that original food items had t
qdel(food)
return FALSE
if(SEND_SIGNAL(eater, COMSIG_CARBON_ATTEMPT_EAT, food) & COMSIG_CARBON_BLOCK_EAT)
return
if(SEND_SIGNAL(eater, COMSIG_CARBON_ATTEMPT_EAT, food) & BLOCK_EAT_ATTEMPT)
return FALSE
if(SEND_SIGNAL(food, COMSIG_FOOD_ATTEMPT_EAT, eater, feeder) & BLOCK_EAT_ATTEMPT)
return FALSE
return TRUE
///Applies food buffs according to the crafting complexity
+33
View File
@@ -0,0 +1,33 @@
/// Allows mobs with this component to have all of their limbs operated on without needing drapes
/datum/component/free_operation
dupe_mode = COMPONENT_DUPE_SOURCES
/datum/component/free_operation/Initialize(check)
if (!iscarbon(parent))
return COMPONENT_INCOMPATIBLE
ADD_TRAIT(parent, TRAIT_READY_TO_OPERATE, REF(src))
var/mob/living/carbon/owner = parent
for (var/obj/item/bodypart/limb as anything in owner.bodyparts)
ADD_TRAIT(limb, TRAIT_READY_TO_OPERATE, REF(src))
/datum/component/free_operation/Destroy(force)
REMOVE_TRAIT(parent, TRAIT_READY_TO_OPERATE, REF(src))
var/mob/living/carbon/owner = parent
for (var/obj/item/bodypart/limb as anything in owner.bodyparts)
REMOVE_TRAIT(limb, TRAIT_READY_TO_OPERATE, REF(src))
return ..()
/datum/component/free_operation/RegisterWithParent()
RegisterSignal(parent, COMSIG_CARBON_ATTACH_LIMB, PROC_REF(flag_limb))
RegisterSignal(parent, COMSIG_CARBON_REMOVE_LIMB, PROC_REF(unflag_limb))
/datum/component/free_operation/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_CARBON_ATTACH_LIMB, COMSIG_CARBON_REMOVE_LIMB))
/datum/component/free_operation/proc/flag_limb(mob/living/carbon/source, obj/item/bodypart/limb)
SIGNAL_HANDLER
ADD_TRAIT(limb, TRAIT_READY_TO_OPERATE, REF(src))
/datum/component/free_operation/proc/unflag_limb(mob/living/carbon/source, obj/item/bodypart/limb)
SIGNAL_HANDLER
REMOVE_TRAIT(limb, TRAIT_READY_TO_OPERATE, REF(src))
+1 -2
View File
@@ -209,8 +209,7 @@
stop_autofiring()
return COMPONENT_CLIENT_MOUSEUP_INTERCEPT
/datum/component/automatic_fire/proc/stop_autofiring(datum/source, atom/object, turf/location, control, params)
/datum/component/automatic_fire/proc/stop_autofiring(mob/living/source, obj/item/swapped_to, obj/item/swapped_from)
SIGNAL_HANDLER
if(autofire_stat != AUTOFIRE_STAT_FIRING)
return
+10
View File
@@ -23,11 +23,21 @@ GLOBAL_LIST_EMPTY(GPS_list)
/datum/component/gps/kheiral_cuffs/Initialize(_gpstag = "COM0")
. = ..()
RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(deactivate_kheiral_cuffs))
RegisterSignal(parent, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(deactivate_if_station))
/datum/component/gps/kheiral_cuffs/proc/deactivate_kheiral_cuffs(datum/source)
SIGNAL_HANDLER
qdel(src)
/datum/component/gps/kheiral_cuffs/proc/deactivate_if_station(atom/movable/moved_atom, turf/old_turf, turf/new_turf)
SIGNAL_HANDLER
if(!isturf(new_turf))
return
if(is_station_level(new_turf.z))
qdel(src)
return
///GPS component subtype. Only gps/item's can be used to open the UI.
/datum/component/gps/item
var/updating = TRUE //Automatic updating of GPS list. Can be set to manual by user.
+2 -2
View File
@@ -36,7 +36,7 @@
heal_when_sinked = TRUE,
health_per_second = 1,
outline_colour = COLOR_PALE_GREEN,
damage_res_sinked = list(BRUTE = 1, BURN = 1, TOX = 1, STAMINA = 0, OXY = 1))
damage_res_sinked = string_assoc_list(list(BRUTE = 1, BURN = 1, TOX = 1, STAMINA = 0, OXY = 1)))
if (!isbasicmob(parent))
return COMPONENT_INCOMPATIBLE
@@ -112,7 +112,7 @@
if(sinked && heal_when_sinked)
stop_regenerating()
living_target.icon_state = target_icon_state
living_target.damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, STAMINA = 0, OXY = 1)
living_target.damage_coeff = string_assoc_list(list(BRUTE = 1, BURN = 1, TOX = 1, STAMINA = 0, OXY = 1))
living_target.set_density(TRUE)
sinked = FALSE
@@ -19,6 +19,8 @@
/datum/component/holographic_nature/RegisterWithParent()
AddComponent(/datum/component/connect_loc_behalf, parent, loc_connections)
if(isliving(parent))
RegisterSignal(parent, COMSIG_LIVING_MOB_BUMP, PROC_REF(on_bumped))
RegisterSignal(parent, COMSIG_LIVING_MOB_BUMPED, PROC_REF(on_bumped))
RegisterSignal(parent, COMSIG_MOB_APPLY_DAMAGE, PROC_REF(on_mob_damaged))
return
@@ -26,6 +28,7 @@
if(isobj(atom_parent) && atom_parent.uses_integrity)
RegisterSignal(parent, COMSIG_ATOM_TAKE_DAMAGE, PROC_REF(on_object_damaged))
/datum/component/holographic_nature/proc/on_mob_damaged(mob/living/source, damage_amount, damagetype, def_zone, blocked, wound_bonus, exposed_wound_bonus, sharpness, attack_direction, attacking_item)
SIGNAL_HANDLER
if(damagetype == BURN || damagetype == BRUTE)
@@ -44,6 +47,10 @@
if(isprojectile(thing) || thing.density)
apply_effects()
/datum/component/holographic_nature/proc/on_bumped(atom/movable/source, mob/living/crossing_mob)
SIGNAL_HANDLER
apply_effects()
/datum/component/holographic_nature/proc/apply_effects()
if(!COOLDOWN_FINISHED(src, glitch_cooldown))
return
+1 -2
View File
@@ -94,9 +94,8 @@
/datum/component/jetpack/proc/setup_trail(mob/user)
if(trail)
QDEL_NULL(trail)
trail = new effect_type
trail = new effect_type(user)
trail.auto_process = FALSE
trail.set_up(user)
trail.start()
/datum/component/jetpack/proc/activate(datum/source, mob/new_user)
@@ -1,801 +0,0 @@
/*!
This datum should be used for handling mineral contents of machines and whatever else is supposed to hold minerals and make use of them.
Variables:
amount - raw amount of the mineral this container is holding, calculated by the defined value SHEET_MATERIAL_AMOUNT=SHEET_MATERIAL_AMOUNT.
max_amount - max raw amount of mineral this container can hold.
sheet_type - type of the mineral sheet the container handles, used for output.
parent - object that this container is being used by, used for output.
MAX_STACK_SIZE - size of a stack of mineral sheets. Constant.
*/
//The full item was consumed
#define MATERIAL_INSERT_ITEM_SUCCESS 1
/datum/component/material_container
/// The maximum amount of materials this material container can contain
var/max_amount
/// Map of material ref -> amount
var/list/materials //Map of key = material ref | Value = amount
/// The list of materials that this material container can accept
var/list/allowed_materials
/// The typecache of things that this material container can accept
var/list/allowed_item_typecache
/// Whether or not this material container allows specific amounts from sheets to be inserted
var/precise_insertion = FALSE
/// The material container flags. See __DEFINES/construction/materials.dm.
var/mat_container_flags
/// Signals that are registered with this contained
var/list/registered_signals
/// Sets up the proper signals and fills the list of materials with the appropriate references.
/datum/component/material_container/Initialize(
list/init_mats,
max_amt = 0,
_mat_container_flags = NONE,
list/allowed_mats = init_mats,
list/allowed_items,
list/container_signals
)
if(!isatom(parent))
return COMPONENT_INCOMPATIBLE
materials = list()
max_amount = max(0, max_amt)
mat_container_flags = _mat_container_flags
allowed_materials = allowed_mats || list()
if(allowed_items)
if(ispath(allowed_items) && allowed_items == /obj/item/stack)
allowed_item_typecache = GLOB.typecache_stack
else
allowed_item_typecache = typecacheof(allowed_items)
//Make the assoc list material reference -> amount
for(var/mat in init_mats)
var/mat_ref = GET_MATERIAL_REF(mat)
if(isnull(mat_ref))
continue
var/mat_amt = init_mats[mat]
if(isnull(mat_amt))
mat_amt = 0
materials[mat_ref] += mat_amt
//all user handled signals
if(length(container_signals))
for(var/signal in container_signals)
parent.RegisterSignal(src, signal, container_signals[signal])
/datum/component/material_container/Destroy(force)
materials = null
allowed_materials = null
return ..()
/datum/component/material_container/RegisterWithParent()
. = ..()
var/atom/atom_target = parent
//can we insert into this container
if(!(mat_container_flags & MATCONTAINER_NO_INSERT))
//to insert stuff into the container
RegisterSignal(atom_target, COMSIG_ATOM_ITEM_INTERACTION, PROC_REF(on_item_insert))
RegisterSignal(atom_target, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY, PROC_REF(on_secondary_insert))
//screen tips for inserting items
atom_target.flags_1 |= HAS_CONTEXTUAL_SCREENTIPS_1
RegisterSignal(atom_target, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM, PROC_REF(on_requesting_context_from_item))
//to see available materials
if(mat_container_flags & MATCONTAINER_EXAMINE)
RegisterSignal(atom_target, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
//drop sheets when the object is deconstructed but not deleted
RegisterSignal(parent, COMSIG_OBJ_DECONSTRUCT, PROC_REF(drop_sheets))
/datum/component/material_container/UnregisterFromParent()
var/list/signals = list()
if(!(mat_container_flags & MATCONTAINER_NO_INSERT))
signals += COMSIG_ATOM_ITEM_INTERACTION
signals += COMSIG_ATOM_ITEM_INTERACTION_SECONDARY
signals += COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM
if(mat_container_flags & MATCONTAINER_EXAMINE)
signals += COMSIG_ATOM_EXAMINE
signals += COMSIG_OBJ_DECONSTRUCT
UnregisterSignal(parent, signals)
/datum/component/material_container/proc/drop_sheets()
SIGNAL_HANDLER
retrieve_all()
/datum/component/material_container/proc/on_examine(datum/source, mob/user, list/examine_texts)
SIGNAL_HANDLER
for(var/I in materials)
var/datum/material/M = I
var/amt = materials[I] / SHEET_MATERIAL_AMOUNT
if(amt)
examine_texts += span_notice("It has [amt] sheets of [LOWER_TEXT(M.name)] stored.")
/datum/component/material_container/vv_edit_var(var_name, var_value)
var/old_flags = mat_container_flags
. = ..()
if(var_name == NAMEOF(src, mat_container_flags) && parent)
if(!(old_flags & MATCONTAINER_EXAMINE) && mat_container_flags & MATCONTAINER_EXAMINE)
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
else if(old_flags & MATCONTAINER_EXAMINE && !(mat_container_flags & MATCONTAINER_EXAMINE))
UnregisterSignal(parent, COMSIG_ATOM_EXAMINE)
if(old_flags & MATCONTAINER_NO_INSERT && !(mat_container_flags & MATCONTAINER_NO_INSERT))
RegisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION, PROC_REF(on_item_insert))
RegisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY, PROC_REF(on_secondary_insert))
else if(!(old_flags & MATCONTAINER_NO_INSERT) && mat_container_flags & MATCONTAINER_NO_INSERT)
UnregisterSignal(parent, list(COMSIG_ATOM_ITEM_INTERACTION, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY))
/**
* 3 Types of Procs
* Material Insertion : Insert materials into the container
* Material Validation : Checks how much materials are available, Extracts materials from items if the container can hold them
* Material Removal : Removes material from the container
*
* Each Proc further belongs to a specific category
* LOW LEVEL: Procs that are used internally & should not be used anywhere else unless you know what your doing
* MID LEVEL: Procs that can be used by machines(like recycler, stacking machines) to bypass majority of checks
* HIGH LEVEL: Procs that can be used by anyone publicly and guarantees safety checks & limits
*/
//================================Material Insertion procs==============================
//======================================LOW LEVEL=========================================
/**
* Inserts the relevant materials from an item into this material container.
* This low level proc should not be used directly by anyone
*
* Arguments:
* - [source][/obj/item]: The source of the materials we are inserting.
* - multiplier: The multiplier for the materials extract from this item being inserted.
* - context: the atom performing the operation, is used mostly for silo logging, the silo resends this signal on the context to give it a
* chance to process the item
* - user_data: in the form rendered by ID_DATA(user), for material logging (and if this component is connected to a silo, also for permission checking)
*/
/datum/component/material_container/proc/insert_item_materials(obj/item/source, multiplier = 1, atom/context = parent, alist/user_data)
var/primary_mat
var/max_mat_value = 0
var/material_amount = 0
var/list/item_materials = source.get_material_composition(mat_container_flags)
var/list/mats_consumed = list()
for(var/MAT in item_materials)
if(!can_hold_material(MAT))
continue
var/mat_amount = OPTIMAL_COST(item_materials[MAT] * multiplier)
materials[MAT] += mat_amount
if(item_materials[MAT] > max_mat_value)
max_mat_value = item_materials[MAT]
primary_mat = MAT
mats_consumed[MAT] = mat_amount
material_amount += mat_amount
if(length(mats_consumed))
SEND_SIGNAL(src, COMSIG_MATCONTAINER_ITEM_CONSUMED, source, primary_mat, mats_consumed, material_amount, context, user_data)
return primary_mat
//===================================================================================
//===============================MID LEVEL===================================================
/**
* For inserting an amount of material. Use this to add materials to the container directly
*
* Arguments:
* - amt: amount of said material to insert
* - mat: the material type to insert
*/
/datum/component/material_container/proc/insert_amount_mat(amt, datum/material/mat)
if(amt <= 0)
return 0
amt = OPTIMAL_COST(amt)
if(!has_space(amt))
return 0
var/total_amount_saved = total_amount()
if(mat)
if(!istype(mat))
mat = GET_MATERIAL_REF(mat)
materials[mat] += amt
else
var/num_materials = length(materials)
if(!num_materials)
return 0
amt /= num_materials
for(var/i in materials)
materials[i] += amt
return (total_amount() - total_amount_saved)
/**
* Proc specifically for inserting items, use this when you want to insert any item into the container
* this bypasses most of the material flag checks so much be used by machines like recycler, stacking machine etc that
* does not care for such checks
*
* Arguments:
* - [weapon][obj/item]: the item you are trying to insert
* - multiplier: The multiplier for the materials being inserted
* - context: the atom performing the operation, this is the last argument sent in COMSIG_MATCONTAINER_ITEM_CONSUMED and is used mostly for silo logging
* * - delete_item: should we delete the item after its materials are consumed. does not apply to stacks if they were split due to lack of space
* * - user_data - in the form rendered by ID_DATA(user), for material logging (and if this component is connected to a silo, also for permission checking)
*/
/datum/component/material_container/proc/insert_item(obj/item/weapon, multiplier = 1, atom/context = parent, delete_item = TRUE, alist/user_data)
if(QDELETED(weapon))
return MATERIAL_INSERT_ITEM_NO_MATS
multiplier = CEILING(multiplier, 0.01)
var/obj/item/target = weapon
var/material_amount = OPTIMAL_COST(get_item_material_amount(target) * multiplier)
if(!material_amount)
return MATERIAL_INSERT_ITEM_NO_MATS
var/obj/item/stack/item_stack
if(isstack(weapon) && !has_space(material_amount)) //not enough space split and feed as many sheets possible
item_stack = weapon
var/space_left = max_amount - total_amount()
if(!space_left)
return MATERIAL_INSERT_ITEM_NO_SPACE
var/material_per_sheet = material_amount / item_stack.amount
var/sheets_to_insert = floor(space_left / material_per_sheet)
if(!sheets_to_insert)
return MATERIAL_INSERT_ITEM_NO_SPACE
target = fast_split_stack(item_stack, sheets_to_insert)
material_amount = get_item_material_amount(target) * multiplier
material_amount = OPTIMAL_COST(material_amount)
//do the insert
var/last_inserted_id = insert_item_materials(target, multiplier, context, user_data = user_data)
if(!isnull(last_inserted_id))
if(delete_item || target != weapon) //we could have split the stack ourselves
qdel(target) //item gone
return material_amount
else if(!isnull(item_stack) && item_stack != target) //insertion failed, merge the split stack back into the original
var/obj/item/stack/inserting_stack = target
item_stack.add(inserting_stack.amount)
qdel(inserting_stack)
return MATERIAL_INSERT_ITEM_FAILURE
//============================================================================================
//===================================HIGH LEVEL===================================================
/**
* inserts an item from the players hand into the container. Loops through all the contents inside recursively
* Does all explicit checking for mat flags & callbacks to check if insertion is valid
* This proc is what you should be using for almost all cases
*
* Arguments:
* * held_item - the item to insert
* * user - the mob inserting this item
* * context - the atom performing the operation, this is the last argument sent in COMSIG_MATCONTAINER_ITEM_CONSUMED and is used mostly for silo logging
*/
/datum/component/material_container/proc/user_insert(obj/item/held_item, mob/living/user, atom/context = parent)
set waitfor = FALSE
. = 0
//All items that do not have any contents
var/list/obj/item/items = list(held_item)
//is this the first item we are ever processing
var/first_checks = TRUE
//list of items to delete
var/list/obj/item/to_delete = list()
//The status of the last insert attempt
var/inserted = 0
//All messages to be displayed to chat
var/list/chat_msgs = list()
//differs from held_item when using TK
var/obj/item/active_held = user.get_active_held_item()
//omni tools can act as any tool so get its real behaviour
active_held = active_held.get_proxy_attacker_for(held_item)
while(items.len)
//no point inserting more items
if(inserted == MATERIAL_INSERT_ITEM_NO_SPACE)
break
//Pop the 1st item out from the list
var/obj/item/target_item = items[1]
items -= target_item
//e.g. projectiles inside bullets are not objects
if(!istype(target_item))
continue
//can't allow abstract, hologram items
if((target_item.item_flags & ABSTRACT) || (target_item.flags_1 & HOLOGRAM_1))
continue
//user defined conditions
if(SEND_SIGNAL(src, COMSIG_MATCONTAINER_PRE_USER_INSERT, target_item, user) & MATCONTAINER_BLOCK_INSERT)
continue
//item is either indestructible, not allowed for redemption or not in the allowed types
if((target_item.resistance_flags & INDESTRUCTIBLE) || (target_item.item_flags & NO_MAT_REDEMPTION) || (allowed_item_typecache && !is_type_in_typecache(target_item, allowed_item_typecache)))
if(!(mat_container_flags & MATCONTAINER_SILENT))
var/list/status_data = chat_msgs["[MATERIAL_INSERT_ITEM_FAILURE]"] || list()
var/list/item_data = status_data[target_item.name] || list()
item_data["count"] += 1
status_data[target_item.name] = item_data
chat_msgs["[MATERIAL_INSERT_ITEM_FAILURE]"] = status_data
if(target_item.resistance_flags & INDESTRUCTIBLE)
if(target_item != active_held) //move it out of any storage medium its in so it doesn't get consumed with its parent, but only if that storage medium is not our hand
target_item.forceMove(get_turf(context))
continue
//storage items usually come here
//this is so players can insert items from their bags into machines for convinience
if(!target_item.atom_storage || !target_item.contents.len)
continue
//at this point we can check if we have enough for all items & other stuff
if(first_checks)
//duffle bags needs to be unzipped
if(target_item.atom_storage?.locked)
if(!(mat_container_flags & MATCONTAINER_SILENT))
to_chat(user, span_warning("[target_item] has its storage locked"))
return
//anything that isn't a stack cannot be split so find out if we have enough space, we don't want to consume half the contents of an object & leave it in a broken state
//for duffle bags and other storage items we can check for space 1 item at a time
if(!isstack(target_item) && !target_item.atom_storage)
var/total_amount = 0
for(var/obj/item/weapon as anything in target_item.get_all_contents_type(/obj/item))
total_amount += get_item_material_amount(weapon)
if(!has_space(total_amount))
if(!(mat_container_flags & MATCONTAINER_SILENT))
to_chat(user, span_warning("[parent] does not have enough space for [target_item]!"))
return
first_checks = FALSE
//if stack, check if we want to read precise amount of sheets to insert
var/obj/item/stack/item_stack = null
if(isstack(target_item) && precise_insertion)
var/atom/current_parent = parent
item_stack = target_item
var/requested_amount = tgui_input_number(user, "How much do you want to insert?", "Inserting [item_stack.singular_name]s", item_stack.amount, item_stack.amount)
if(!requested_amount || QDELETED(target_item) || QDELETED(user) || QDELETED(src))
continue
if(parent != current_parent || user.get_active_held_item() != active_held)
continue
if(requested_amount != item_stack.amount) //only split if its not the whole amount
target_item = fast_split_stack(item_stack, requested_amount) //split off the requested amount
requested_amount = 0
//is this item a stack and was it split by the player?
var/was_stack_split = !isnull(item_stack) && item_stack != target_item
//if it was split then item_stack has the reference to the original stack/item
var/obj/item/original_item = was_stack_split ? item_stack : target_item
//if this item is not the one the player is holding then don't remove it from their hand
if(original_item != active_held)
original_item = null
if(!isnull(original_item) && !user.temporarilyRemoveItemFromInventory(original_item)) //remove from hand(if split remove the original stack else the target)
return
//insert the item
var/item_name = target_item.name
var/item_count = 1
var/is_stack = FALSE
var/obj/item/stack/the_stack
if(isstack(target_item))
the_stack = target_item
item_name = the_stack.singular_name
item_count = the_stack.amount
is_stack = TRUE
//we typically don't want to consume bags, boxes but only their contents. so we skip processing
inserted = !target_item.atom_storage ? insert_item(target_item, 1, context, is_stack, user_data = ID_DATA(user)) : 0
if(inserted > 0)
. += inserted
inserted /= SHEET_MATERIAL_AMOUNT // display units inserted as sheets for improved readability
//collect all messages to print later
var/list/status_data = chat_msgs["[MATERIAL_INSERT_ITEM_SUCCESS]"] || list()
var/list/item_data = status_data[item_name] || list()
item_data["count"] += item_count
item_data["amount"] += inserted
item_data["stack"] = is_stack
status_data[item_name] = item_data
chat_msgs["[MATERIAL_INSERT_ITEM_SUCCESS]"] = status_data
//delete the item or merge stacks if any left over
if(is_stack)
//player split it & machine further split that due to lack of space? merge with remaining stack
if(!QDELETED(target_item) && was_stack_split)
var/obj/item/stack/inserting_stack = target_item
item_stack.add(inserting_stack.amount)
qdel(inserting_stack)
//was this the original item in the players hand? put what's left back in the player's hand
if(!QDELETED(original_item))
user.put_in_active_hand(original_item)
//skip processing children & other stuff. irrelevant for stacks
continue
//queue the object for deletion
to_delete += target_item
else
//collect all messages to print later
var/list/status_data = chat_msgs["[inserted]"] || list()
var/list/item_data = status_data[item_name] || list()
item_data["count"] += item_count
status_data[item_name] = item_data
chat_msgs["[inserted]"] = status_data
//player split the stack by the requested amount but even that split amount could not be salvaged. merge it back with the original
if(was_stack_split)
var/obj/item/stack/inserting_stack = target_item
item_stack.add(inserting_stack.amount)
qdel(inserting_stack)
//was this the original item in the players hand? put it back because we coudn't salvage it
if(!QDELETED(original_item))
user.put_in_active_hand(original_item)
//we can stop here as remaining items will fail to insert as well
if(inserted == MATERIAL_INSERT_ITEM_NO_SPACE)
break
//we failed to process the item so don't bother going into its contents
//but if we are dealing with storage items like bags, boxes etc then we make a exception
if(!target_item.atom_storage)
continue
//If any mats were consumed we can proceed to delete the parent
//If it has children then we will process them first in the 2nd round
//This is done so we don't delete the children when the parent is consumed
//We only do this on the 1st iteration so we don't re-iterate through its children again
if(target_item.contents.len)
if(target_item.atom_storage?.locked) //can't access contents of locked storage(like duffle bags)
continue
//process children
items += target_item.contents
//we now summarize the chat msgs collected
if(!(mat_container_flags & MATCONTAINER_SILENT))
for(var/status in chat_msgs)
var/list/status_data = chat_msgs[status]
for(var/item_name in status_data)
//read the params
var/list/chat_data = status_data[item_name]
var/count = chat_data["count"]
var/amount = chat_data["amount"]
//decode the message
switch(text2num(status))
if(MATERIAL_INSERT_ITEM_SUCCESS) //no problems full item was consumed
if(chat_data["stack"])
var/sheets = min(count, amount) //minimum between sheets inserted vs sheets consumed(values differ for alloys)
to_chat(user, span_notice("[sheets > 1 ? "[sheets] " : ""][item_name][sheets > 1 ? "s were" : " was"] added to [parent]."))
else
to_chat(user, span_notice("[count > 1 ? "[count] " : ""][item_name][count > 1 ? "s" : ""], worth [amount] sheets, [count > 1 ? "were" : "was"] added to [parent]."))
if(MATERIAL_INSERT_ITEM_NO_SPACE) //no space
to_chat(user, span_warning("[parent] has no space to accept [item_name]!"))
if(MATERIAL_INSERT_ITEM_NO_MATS) //no materials inside these items
to_chat(user, span_warning("[item_name][count > 1 ? "s have" : " has"] no materials that can be accepted by [parent]!"))
if(MATERIAL_INSERT_ITEM_FAILURE) //could be because the material type was not accepted or other stuff
to_chat(user, span_warning("[item_name][count > 1 ? "s were" : " was"] rejected by [parent]!"))
//finally delete the items
for(var/obj/item/deleting as anything in to_delete)
if(!QDELETED(deleting)) //deleting parents also delete their children so we check
qdel(deleting)
/datum/component/material_container/proc/on_item_insert(datum/source, mob/living/user, obj/item/weapon)
SIGNAL_HANDLER
// Don't insert material items with left click
if (isstack(weapon))
return attempt_insert(user, weapon)
/datum/component/material_container/proc/on_secondary_insert(datum/source, mob/living/user, obj/item/weapon)
SIGNAL_HANDLER
return attempt_insert(user, weapon)
/// Proc that allows players to fill the parent with mats
/datum/component/material_container/proc/attempt_insert(mob/living/user, obj/item/weapon)
//Allows you to attack the machine with iron sheets for e.g.
if(!(mat_container_flags & MATCONTAINER_ANY_INTENT) && user.combat_mode)
return
if(ismachinery(parent))
var/obj/machinery/machine = parent
if(machine.machine_stat || machine.panel_open)
return
user_insert(weapon, user)
return ITEM_INTERACT_SUCCESS
//===============================================================================================
//======================================Material Validation=======================================
//=========================================LOW LEVEL===================================
/**
* Proc that returns TRUE if the container has space
*
* Arguments:
* * amt - can this container hold this much amount of materials
*/
/datum/component/material_container/proc/has_space(amt = 0)
return (total_amount() + amt) <= max_amount
/**
* The default check for whether we can add materials to this material container.
*
* Arguments:
* - [mat][/atom/material]: The material we are checking for insertability.
*/
/datum/component/material_container/proc/can_hold_material(datum/material/mat)
if(mat in allowed_materials)
return TRUE
if(istype(mat) && ((mat.id in allowed_materials) || (mat.type in allowed_materials)))
allowed_materials += mat // This could get messy with passing lists by ref... but if you're doing that the list expansion is probably being taken care of elsewhere anyway...
return TRUE
if(SEND_SIGNAL(src, COMSIG_MATCONTAINER_MAT_CHECK, mat) & MATCONTAINER_ALLOW_MAT)
allowed_materials += mat
return TRUE
return FALSE
//========================================================================================
//===================================MID LEVEL=============================================
/**
* Returns the amount of a specific material in this container.
*
* Arguments:
* -[mat][datum/material] : the material type to check for 3 cases
* a) If it's an path its ref is retrieved
* b) If it's text then its an category material & there is no way to deal with it so return 0
* c) If normal material proceeds as usual
*/
/datum/component/material_container/proc/get_material_amount(datum/material/mat)
if(!istype(mat))
mat = GET_MATERIAL_REF(mat)
return materials[mat]
/**
* Returns the amount of material relevant to this container;
* if this container does not support glass, any glass in 'I' will not be taken into account
*
* Arguments:
* - [I][obj/item]: the item whos materials must be retrieved
*/
/datum/component/material_container/proc/get_item_material_amount(obj/item/item)
if(!istype(item) || !item.custom_materials)
return 0
var/material_amount = 0
var/list/item_materials = item.get_material_composition(mat_container_flags)
for(var/MAT in item_materials)
if(!can_hold_material(MAT))
continue
material_amount += item_materials[MAT]
return material_amount
//================================================================================================
//=========================================HIGH LEVEL==========================================
/// returns the total amount of material in the container
/datum/component/material_container/proc/total_amount()
. = 0
for(var/i in materials)
. += get_material_amount(i)
/**
* Returns TRUE if you have enough of the specified material.
*
* Arguments:
* - [req_mat][datum/material]: the material to check for
* - amount: how much material do we need
*/
/datum/component/material_container/proc/has_enough_of_material(datum/material/req_mat, amount = 1)
return get_material_amount(req_mat) >= OPTIMAL_COST(amount)
/**
* Checks if its possible to afford a certain amount of materials. Takes a dictionary of materials.
* coefficient can be thought of as the machines efficiency & multiplier as the print quantity
*
* Arguments:
* - mats: list of materials(key=material, value= 1 unit of material) to check for
* - coefficient: scaling applied to 1 unit of material in the mats list
* - multiplier: how many units(after scaling) do we require
*/
/datum/component/material_container/proc/has_materials(list/mats, coefficient = 1, multiplier = 1)
if(!length(mats))
return FALSE
for(var/x in mats) //Loop through all required materials
var/wanted = OPTIMAL_COST(mats[x] * coefficient) * multiplier
if(!has_enough_of_material(x, wanted))//Not a category, so just check the normal way
testing("didn't have: [x] wanted: [wanted]")
return FALSE
return TRUE
//==========================================================================================================
//================================================Material Usage============================================
//==================================================LOW LEVEL=======================================
/**
* Uses an amount of a specific material, effectively removing it.
*
* Arguments:
* - amt: amount of said material to use
* - [mat][datum/material]: type of mat to use
*/
/datum/component/material_container/proc/use_amount_mat(amt, datum/material/mat)
//round amount
amt = OPTIMAL_COST(amt)
//get ref if necessary
if(!istype(mat))
mat = GET_MATERIAL_REF(mat)
//check if sufficient is available
if(materials[mat] < amt)
return 0
//consume & return amount consumed
materials[mat] -= amt
return amt
//==============================================================================================
//=========================================MID LEVEL==========================================
/**
* For consuming a dictionary of materials.
*
* Arguments:
* - mats: map of materials to consume(key = material type, value = amount)
* - coefficient: how much fraction of unit material in the mats list must be consumed. This is usually your machines efficiency
* - multiplier: how many units of material in the mats list(after each unit is multiplied and rounded with coefficient) must be consumed, This is usually your print quantity
*/
/datum/component/material_container/proc/use_materials(list/mats, coefficient = 1, multiplier = 1)
if(!mats || !length(mats))
return FALSE
var/amount_removed = 0
for(var/i in mats)
amount_removed += use_amount_mat(OPTIMAL_COST(mats[i] * coefficient) * multiplier, i)
return amount_removed
//============================================================================================
//===========================================HIGH LEVEL=======================================
/**
* For spawning stacks (mineral sheets or ore) at a specific location. Used by machines to output sheets.
*
* Arguments:
* stack_amt: number of sheets to extract
* [material][datum/material]: type of sheets present in this container to extract
* [target][atom]: drop location
* [atom][context]: context - the atom performing the operation, this is the last argument sent in COMSIG_MATCONTAINER_STACK_RETRIEVED and is used mostly for silo logging
* user_data - in the form rendered by ID_DATA(user), for material logging (and if this component is connected to a silo, also for permission checking)
*/
/datum/component/material_container/proc/retrieve_stack(stack_amt, datum/material/material, atom/target = null, atom/context = parent, alist/user_data)
//do we support sheets of this material
if(!material.sheet_type)
return 0 //Add greyscale sheet handling here later
if(!can_hold_material(material))
return 0
//requested amount greater than available amount or just an invalid value
stack_amt = min(floor(materials[material] / SHEET_MATERIAL_AMOUNT), stack_amt)
if(stack_amt <= 0)
return 0
//auto drop location
if(!target)
var/atom/parent_atom = parent
target = parent_atom.drop_location()
if(!target)
return 0
//eject sheets based on available amount after each iteration
var/count = 0
while(stack_amt > 0)
var/type_to_retrieve = material.sheet_type || material.ore_type
//don't merge yet. we need to do stuff with it first
var/obj/item/stack/new_stack = new type_to_retrieve(target, min(stack_amt, MAX_STACK_SIZE), FALSE)
if(istype(new_stack, /obj/item/stack/sheet))
var/obj/item/stack/sheet/new_sheets = new_stack
new_sheets.manufactured = TRUE
count += new_stack.amount
//use material & deduct work needed
use_amount_mat(new_stack.amount * SHEET_MATERIAL_AMOUNT, material)
stack_amt -= new_stack.amount
//send signal
SEND_SIGNAL(src, COMSIG_MATCONTAINER_STACK_RETRIEVED, new_stack, context, user_data)
//no point merging anything into an already full stack
if(new_stack.amount == new_stack.max_amount)
continue
//now we can merge since we are done with it
for(var/obj/item/stack/item_stack in target)
if(item_stack == new_stack || item_stack.type != type_to_retrieve) //don't merge with self or different type
continue
//speed merge
var/merge_amount = min(item_stack.amount, new_stack.max_amount - new_stack.get_amount())
item_stack.use(merge_amount)
new_stack.add(merge_amount)
break
return count
/**
* Proc to get all the materials and dump them as sheets
*
* Arguments:
* - target: drop location of the sheets
* - context: the atom which is ejecting the sheets. Used mostly in silo logging
*/
/datum/component/material_container/proc/retrieve_all(target = null, atom/context = parent)
var/result = 0
for(var/MAT in materials)
result += retrieve_stack(amount2sheet(materials[MAT]), MAT, target, context, user_data = ID_DATA(null))
return result
//============================================================================================
/datum/component/material_container/ui_static_data(mob/user)
var/list/data = list()
data["SHEET_MATERIAL_AMOUNT"] = SHEET_MATERIAL_AMOUNT
return data
/// List format is list(material_name = list(amount = ..., ref = ..., etc.))
/datum/component/material_container/ui_data(mob/user)
var/list/data = list()
for(var/datum/material/material as anything in materials)
var/amount = materials[material]
data += list(list(
"name" = material.name,
"ref" = REF(material),
"amount" = amount,
"color" = material.greyscale_color || material.color
))
return data
/**
* Adds context sensitivy directly to the material container file for screentips
* Arguments:
* * source - refers to item that will display its screentip
* * context - refers to, in this case, an item in the users hand hovering over the material container, such as an autolathe
* * held_item - refers to the item that has materials accepted by the material container
* * user - refers to user who will see the screentip when the proper context and tool are there
*/
/datum/component/material_container/proc/on_requesting_context_from_item(datum/source, list/context, obj/item/held_item, mob/living/user)
SIGNAL_HANDLER
if(isnull(held_item))
return NONE
if(!(mat_container_flags & MATCONTAINER_ANY_INTENT) && user.combat_mode)
return NONE
if(held_item.item_flags & ABSTRACT)
return NONE
if((held_item.flags_1 & HOLOGRAM_1) || (held_item.item_flags & NO_MAT_REDEMPTION) || (allowed_item_typecache && !is_type_in_typecache(held_item, allowed_item_typecache)))
return NONE
var/list/item_materials = held_item.get_material_composition(mat_container_flags)
if(!length(item_materials))
return NONE
for(var/material in item_materials)
if(can_hold_material(material))
continue
return NONE
if (isstack(held_item))
context[SCREENTIP_CONTEXT_LMB] = "Insert stack"
context[SCREENTIP_CONTEXT_RMB] = "Insert"
return CONTEXTUAL_SCREENTIP_SET
#undef MATERIAL_INSERT_ITEM_SUCCESS
@@ -1,292 +0,0 @@
/*
This component allows machines to connect remotely to a material container
(namely an /obj/machinery/ore_silo) elsewhere. It offers optional graceful
fallback to a local material storage in case remote storage is unavailable, and
handles linking back and forth.
*/
/datum/component/remote_materials
// Three possible states:
// 1. silo exists, materials is parented to silo
// 2. silo is null, materials is parented to parent
// 3. silo is null, materials is null
///The silo machine this container is connected to
var/obj/machinery/ore_silo/silo
///Material container. the value is either the silo or local
var/datum/component/material_container/mat_container
///Should we create a local storage if we can't connect to silo
var/allow_standalone
///Local size of container when silo = null
var/local_size = INFINITY
///Flags used for the local material container(exceptions for item insert & intent flags)
var/mat_container_flags = NONE
///List of signals to hook onto the local container
var/list/mat_container_signals
///Callback for round start silo connections. Have to cancel this if the parent gets destroyed before round start
VAR_PRIVATE/datum/callback/connection
/datum/component/remote_materials/Initialize(
mapload,
allow_standalone = TRUE,
force_connect = FALSE,
mat_container_flags = NONE,
list/mat_container_signals = null,
)
if (!isatom(parent))
return COMPONENT_INCOMPATIBLE
src.allow_standalone = allow_standalone
src.mat_container_flags = mat_container_flags
src.mat_container_signals = mat_container_signals
RegisterSignal(parent, COMSIG_ATOM_TOOL_ACT(TOOL_MULTITOOL), PROC_REF(OnMultitool))
var/turf/T = get_turf(parent)
var/connect_to_silo = FALSE
if(force_connect || (mapload && is_station_level(T.z)))
connect_to_silo = TRUE
if(mapload) // wait for silo to initialize during mapload
connection = CALLBACK(src, PROC_REF(_PrepareStorage), connect_to_silo)
SSticker.OnRoundstart(connection)
else //directly register in round
_PrepareStorage(connect_to_silo)
/**
* Internal proc. prepares local storage if onnect_to_silo = FALSE
*
* Arguments
* connect_to_silo- if true connect to global silo. If not successfull then go to local storage
* only if allow_standalone = TRUE, else you a null mat_container
*/
/datum/component/remote_materials/proc/_PrepareStorage(connect_to_silo)
PRIVATE_PROC(TRUE)
connection = null
if (connect_to_silo)
silo = GLOB.ore_silo_default
if (silo)
silo.connect_receptacle(src, parent)
if(!(mat_container_flags & MATCONTAINER_NO_INSERT))
RegisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION, PROC_REF(on_item_insert))
RegisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY, PROC_REF(on_secondary_insert))
if (!mat_container && allow_standalone)
_MakeLocal()
/datum/component/remote_materials/Destroy()
if(connection)
LAZYREMOVE(SSticker.round_start_events, connection)
connection = null
if(silo)
allow_standalone = FALSE
disconnect()
if(mat_container)
mat_container = null
return ..()
/datum/component/remote_materials/proc/_MakeLocal()
PRIVATE_PROC(TRUE)
silo = null
mat_container = parent.AddComponent( \
/datum/component/material_container, \
SSmaterials.materials_by_category[MAT_CATEGORY_SILO], \
local_size, \
mat_container_flags, \
container_signals = mat_container_signals, \
allowed_items = /obj/item/stack \
)
/// Adds/Removes this connection from the silo
/datum/component/remote_materials/proc/toggle_holding()
if(isnull(silo))
return
if(!silo.holds[src])
silo.holds[src] = TRUE
else
silo.holds -= src
/**
* Sets the storage size for local materials when not linked with silo
* Arguments
*
* * size - the new size for local storage. measured in SHEET_MATERIAL_SIZE units
*/
/datum/component/remote_materials/proc/set_local_size(size)
local_size = size
if (!silo && mat_container)
mat_container.max_amount = size
///Disconnects this component from the silo
/datum/component/remote_materials/proc/disconnect()
if(isnull(silo))
return
UnregisterSignal(parent, list(COMSIG_ATOM_ITEM_INTERACTION, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY))
silo.disconnect_receptacle(src, parent)
if (allow_standalone)
_MakeLocal()
/datum/component/remote_materials/proc/OnMultitool(datum/source, mob/user, obj/item/multitool/M)
SIGNAL_HANDLER
. = NONE
if (!QDELETED(M.buffer) && istype(M.buffer, /obj/machinery/ore_silo))
if (silo == M.buffer)
to_chat(user, span_warning("[parent] is already connected to [silo]!"))
return ITEM_INTERACT_BLOCKING
if(!check_z_level(M.buffer))
to_chat(user, span_warning("[parent] is too far away to get a connection signal!"))
return ITEM_INTERACT_BLOCKING
var/obj/machinery/ore_silo/new_silo = M.buffer
var/datum/component/material_container/new_container = new_silo.GetComponent(/datum/component/material_container)
if (silo)
silo.disconnect_receptacle(src, parent)
else if (mat_container)
//transfer all mats to silo. whatever cannot be transfered is dumped out as sheets
if(mat_container.total_amount())
for(var/datum/material/mat as anything in mat_container.materials)
var/mat_amount = mat_container.materials[mat]
if(!mat_amount || !new_container.has_space(mat_amount) || !new_container.can_hold_material(mat))
continue
new_container.materials[mat] += mat_amount
mat_container.materials[mat] = 0
qdel(mat_container)
new_silo.connect_receptacle(src, parent)
if(!(mat_container_flags & MATCONTAINER_NO_INSERT))
RegisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION, PROC_REF(on_item_insert))
RegisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY, PROC_REF(on_secondary_insert))
to_chat(user, span_notice("You connect [parent] to [silo] from the multitool's buffer."))
return ITEM_INTERACT_SUCCESS
/datum/component/remote_materials/proc/on_item_insert(datum/source, mob/living/user, obj/item/target)
SIGNAL_HANDLER
// Only insert stacks with left click
if(isstack(target))
return attempt_insert(user, target)
/datum/component/remote_materials/proc/on_secondary_insert(datum/source, mob/living/user, obj/item/target)
SIGNAL_HANDLER
return attempt_insert(user, target)
/// Insert mats into silo
/datum/component/remote_materials/proc/attempt_insert(mob/living/user, obj/item/target)
if(!(mat_container_flags & MATCONTAINER_ANY_INTENT) && user.combat_mode)
return
if(silo)
mat_container.user_insert(target, user, parent)
return ITEM_INTERACT_SUCCESS
/**
* Checks if the param silo is in the same level as this components parent i.e. connected machine, rcd, etc
*
* Arguments
* silo_to_check- Is this components parent in the same Z level as this param silo. If null
* then check this components connected silo
*
* Returns true if both are on the station or same z level
*/
/datum/component/remote_materials/proc/check_z_level(obj/silo_to_check = silo)
if(isnull(silo_to_check))
return FALSE
return is_valid_z_level(get_turf(silo_to_check), get_turf(parent))
/// returns TRUE if this connection put on hold by the silo
/datum/component/remote_materials/proc/on_hold()
return check_z_level() ? silo.holds[src] : FALSE
/**
* Check if this connection can use any materials from the silo
* Returns true only if
* - The parent is of type movable atom
* - A mat container is actually present
* - The silo in not on hold
* Arguments
* * check_hold - should we check if the silo is on hold
* * user_data - in the form rendered by ID_DATA(user); used as a reference for silo bans and access checks
*/
/datum/component/remote_materials/proc/can_use_resource(check_hold = TRUE, alist/user_data)
var/atom/movable/movable_parent = parent
if (!istype(movable_parent))
return FALSE
if (!mat_container) //no silolink & local storage not supported
movable_parent.say("No access to material storage, please contact the quartermaster.")
return FALSE
if(check_hold && on_hold()) //silo on hold
movable_parent.say("Mineral access is on hold, please contact the quartermaster.")
return FALSE
if(SEND_SIGNAL(movable_parent, COMSIG_ORE_SILO_PERMISSION_CHECKED, user_data, movable_parent) & COMPONENT_ORE_SILO_DENY)
return FALSE
return TRUE
/**
* Use materials from either the silo(if connected) or from the local storage. If silo then this action
* is logged else not e.g. action="build" & name="matter bin" means you are trying to build a matter bin
*
* Arguments
* [mats][list]- list of materials to use
* coefficient- each mat unit is scaled by this value then rounded. This value if usually your machine efficiency e.g. upgraded protolathe has reduced costs
* multiplier- each mat unit is scaled by this value then rounded after it is scaled by coefficient. This value is your print quatity e.g. printing multiple items
* action- For logging only. e.g. build, create, i.e. the action you are trying to perform
* name- For logging only. the design you are trying to build e.g. matter bin, etc.
* user_data - in the form rendered by ID_DATA(user), for material logging and (if this component is connected to a silo), permission checking
*/
/datum/component/remote_materials/proc/use_materials(list/mats, coefficient = 1, multiplier = 1, action = "processed", name = "design", alist/user_data)
if(!can_use_resource(user_data = user_data))
return 0
var/amount_consumed = mat_container.use_materials(mats, coefficient, multiplier)
if (silo)//log only if silo is linked
var/list/scaled_mats = list()
for(var/i in mats)
scaled_mats[i] = OPTIMAL_COST(OPTIMAL_COST(mats[i] * coefficient) * multiplier)
silo.silo_log(parent, action, -multiplier, name, scaled_mats, user_data)
return amount_consumed
/**
* Ejects the given material ref and logs it
*
* Arguments
* [material_ref][datum/material]- The material type you are trying to eject
* eject_amount- how many sheets to eject
* [drop_target][atom]- optional where to drop the sheets. null means it is dropped at this components parent location
* user_data - in the form rendered by ID_DATA(user), for material logging and (if this component is connected to a silo), permission checking
*/
/datum/component/remote_materials/proc/eject_sheets(datum/material/material_ref, eject_amount, atom/drop_target = null, alist/user_data)
if(!can_use_resource(user_data = user_data))
return 0
var/atom/movable/movable_parent = parent
if(isnull(drop_target))
drop_target = movable_parent.drop_location()
return mat_container.retrieve_stack(eject_amount, material_ref, target = drop_target, context = parent, user_data = user_data)
/**
* Insert an item into the mat container, helper proc to insert items with the correct context
*
* Arguments
* * obj/item/weapon - the item you are trying to insert
* * multiplier - the multiplier applied on the materials consumed
* * user_data - an alist in the form rendered by ID_DATA(user), for logging who/where/when the item was inserted
*/
/datum/component/remote_materials/proc/insert_item(obj/item/weapon, multiplier = 1, alist/user_data)
// Inserting materials automatically shouldn't be permission-restricted
if(!islist(user_data))
user_data = ID_DATA(null)
user_data[SILICON_OVERRIDE] = SILICON_OVERRIDE
if(!can_use_resource(FALSE, user_data))
return MATERIAL_INSERT_ITEM_FAILURE
return mat_container.insert_item(weapon, multiplier, parent, user_data = user_data)
+29 -36
View File
@@ -5,9 +5,10 @@
* and call set_painting_tool_color() on the parent for more specific object behavior.
*/
/datum/component/palette
/// The maximum number of colors this palette can have.
var/max_colors
/*
* A list that stores a selection of colors.
* The number of available spaces is defined by the available_space arg of Initialize()
*/
var/list/colors = list()
/*
@@ -20,25 +21,20 @@
/// The radial menu choice datums are stored here as a microop to avoid generating new ones every time the menu is opened or updated.
var/list/datum/radial_menu_choice/menu_choices
/datum/component/palette/Initialize(available_space, selected_color)
/datum/component/palette/Initialize(max_colors, selected_color)
if(!isitem(parent))
return COMPONENT_INCOMPATIBLE
if(!isnum(available_space) || available_space < 1) /// This component means nothing if there's no space for colors
stack_trace("palette component initialized without a proper value for the available_space arg")
if(!isnum(max_colors) || max_colors < 1) /// This component means nothing if there's no space for colors
stack_trace("palette component initialized without a proper value for the max_colors arg")
return COMPONENT_INCOMPATIBLE
for(var/index in 1 to available_space)
colors += "#ffffff"
src.colors = colors
src.max_colors = max_colors
src.selected_color = selected_color || "#ffffff"
RegisterSignal(parent, COMSIG_ITEM_ATTACK_SELF_SECONDARY, PROC_REF(on_attack_self_secondary))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
RegisterSignal(parent, COMSIG_PAINTING_TOOL_SET_COLOR, PROC_REF(on_painting_tool_set_color))
RegisterSignal(parent, COMSIG_PAINTING_TOOL_GET_ADDITIONAL_DATA, PROC_REF(get_palette_data))
RegisterSignal(parent, COMSIG_PAINTING_TOOL_PALETTE_COLOR_CHANGED, PROC_REF(palette_color_changed))
/datum/component/palette/Destroy()
QDEL_NULL(color_picker_menu)
@@ -51,7 +47,7 @@
SIGNAL_HANDLER
examine_list += span_notice("<b>Right-Click</b> this item while it's in your active hand to open/close its color picker menu.")
examine_list += span_notice("In the color picker, <b>Left-Click</b> a color button to pick it or <b>Right-Click</b> to edit it.")
examine_list += span_notice("In the color picker, <b>Left-Click</b> a color button to pick it or <b>Right-Click</b> to remove it.")
/datum/component/palette/proc/on_attack_self_secondary(datum/source, mob/user)
SIGNAL_HANDLER
@@ -72,12 +68,27 @@
/datum/component/palette/proc/build_radial_list()
var/radial_list = list()
LAZYSETLEN(menu_choices, length(colors))
var/color_count = length(colors)
LAZYSETLEN(menu_choices, max(color_count+1))
if(color_count < max_colors && !(selected_color in colors))
var/datum/radial_menu_choice/add_option = peek(menu_choices)
if(!add_option)
add_option = new
menu_choices[color_count+1] = add_option
var/image/element = image(icon = 'icons/hud/radial.dmi', icon_state = "palette_element")
element.color = selected_color
var/image/plus = image(icon = 'icons/hud/radial.dmi', icon_state = "palette_add")
plus.appearance_flags = /image::appearance_flags | RESET_COLOR
element.add_overlay(plus)
add_option.image = element
add_option.name = "Add Color ([selected_color])"
radial_list["add"] = add_option
for(var/index in 1 to length(colors))
var/hexcolor = colors[index]
var/datum/radial_menu_choice/option = menu_choices[index]
if(!option)
option = new
menu_choices[index] = option
var/icon_state_to_use = hexcolor == selected_color ? "palette_selected" : "palette_element"
var/image/element = image(icon = 'icons/hud/radial.dmi', icon_state = icon_state_to_use)
element.color = hexcolor
@@ -104,11 +115,14 @@
close_radial_menu()
return
var/is_right_clicking = LAZYACCESS(params2list(params), RIGHT_CLICK)
if(choice == "add")
if(length(colors) < max_colors)
colors += selected_color
update_radial_list()
return
var/index = text2num(choice)
if(is_right_clicking)
var/chosen_color = tgui_color_picker(user, "Pick new color", "[parent]", colors[index]) // BUBBERSTATION EDIT: TGUI COLOR PICKER
if(chosen_color && !QDELETED(src) && !IS_DEAD_OR_INCAP(user) && user.is_holding(parent))
colors[index] = chosen_color
colors.Cut(index, index+1)
update_radial_list()
else
var/obj/item/parent_item = parent
@@ -119,24 +133,3 @@
selected_color = chosen_color
update_radial_list()
/datum/component/palette/proc/get_palette_data(datum/source, data)
SIGNAL_HANDLER
var/list/painting_data = list()
for(var/hexcolor in colors)
painting_data += list(list(
"color" = hexcolor,
"is_selected" = hexcolor == selected_color
))
data["paint_tool_palette"] = painting_data
/datum/component/palette/proc/palette_color_changed(datum/source, chosen_color, index)
SIGNAL_HANDLER
var/was_selected_color = selected_color == colors[index]
colors[index] = chosen_color
if(was_selected_color)
var/obj/item/parent_item = parent
parent_item.set_painting_tool_color(chosen_color)
else
update_radial_list()
+4 -2
View File
@@ -116,7 +116,8 @@
var/original_speed = shell.loaded_projectile.speed
var/original_wounds_bonus = shell.loaded_projectile.wound_bonus
var/original_bare_wounds_bonus = shell.loaded_projectile.exposed_wound_bonus
var/original_ignored_faction = shell.loaded_projectile.ignored_factions
var/original_ignored_faction = shell.loaded_projectile.get_faction()
var/original_ignored_allies = shell.loaded_projectile.allies
for(var/i in 1 to num_pellets)
shell.ready_proj(target, user, SUPPRESSED_VERY, zone_override, fired_from)
@@ -133,7 +134,8 @@
shell.loaded_projectile.speed = original_speed
shell.loaded_projectile.wound_bonus = original_wounds_bonus
shell.loaded_projectile.exposed_wound_bonus = original_bare_wounds_bonus
shell.loaded_projectile.ignored_factions = original_ignored_faction
shell.loaded_projectile.set_faction(original_ignored_faction)
shell.loaded_projectile.set_allies(original_ignored_allies)
pellets += shell.loaded_projectile
var/turf/current_loc = get_turf(fired_from)
if (!istype(target_loc) || !istype(current_loc) || !(shell.loaded_projectile))
+229 -267
View File
@@ -1,150 +1,178 @@
/datum/component/plumbing
dupe_mode = COMPONENT_DUPE_ALLOWED
///Index with "1" = /datum/ductnet/theductpointingnorth etc. "1" being the num2text from NORTH define
var/list/datum/ductnet/ducts = list()
///shortcut to our parents' reagent holder
var/list/datum/ductnet/ducts
///shortcut to our parents' reagent holder. The holder that sends reagents into the pipeline
var/datum/reagents/reagents
///TRUE if we wanna add proper pipe overlays under our parent object. this is pretty good if i may so so myself
var/use_overlays = TRUE
///Whether our tile is covered and we should hide our ducts
var/tile_covered = FALSE
///directions in wich we act as a supplier
var/supply_connects
var/supply_connects = NONE
///direction in wich we act as a demander
var/demand_connects
///FALSE to pretty much just not exist in the plumbing world so we can be moved, TRUE to go plumbo mode
var/active = FALSE
///if TRUE connects will spin with the parent object visually and codually, so you can have it work in any direction. FALSE if you want it to be static
var/turn_connects = TRUE
var/demand_connects = NONE
///The layer on which we connect. Don't add multiple. If you want multiple layer connects for some reason you can just add multiple components with different layers
var/ducting_layer = DUCT_LAYER_DEFAULT
///In-case we don't want the main machine to get the reagents, but perhaps whoever is buckled to it
var/recipient_reagents_holder
///What color is our demand connect?
var/demand_color = COLOR_RED
///What color is our supply connect?
var/supply_color = COLOR_BLUE
///Extend the pipe to the edge for wall-mounted plumbed devices, like sinks and showers
var/extend_pipe_to_edge = FALSE
/// How many distinct reagents can we accept at once
/// Ex - if this was set to "3", our component would only request the first 3 reagents found, even if more are available
var/distinct_reagent_cap = INFINITY
///turn_connects is for wheter or not we spin with the object to change our pipes
/datum/component/plumbing/Initialize(start=TRUE, ducting_layer, turn_connects=TRUE, datum/reagents/custom_receiver, extend_pipe_to_edge = FALSE)
/datum/component/plumbing/Initialize(ducting_layer)
if(!ismovable(parent))
return COMPONENT_INCOMPATIBLE
if(ducting_layer)
src.ducting_layer = ducting_layer
var/atom/movable/parent_movable = parent
if(!parent_movable.reagents && !custom_receiver)
if(!parent_movable.reagents)
return COMPONENT_INCOMPATIBLE
if(GLOB.plumbing_layer_names["[ducting_layer]"])
src.ducting_layer = ducting_layer
ducts = list()
reagents = parent_movable.reagents
src.turn_connects = turn_connects
src.extend_pipe_to_edge = extend_pipe_to_edge
set_recipient_reagents_holder(custom_receiver ? custom_receiver : parent_movable.reagents)
if(start)
on_parent_dir_change(parent_movable, NONE, parent_movable.dir)
if(parent_movable.anchored)
if(PERFORM_ALL_TESTS(maptest_log_mapping))
var/datum/overlap = ducting_layer_check(parent_movable, ducting_layer)
if(!isnull(overlap))
var/message = GLOB.plumbing_layer_names["[ducting_layer]"]
if(istype(overlap, /obj/machinery/duct))
message = "plumbing duct on [message]"
else
message = "plumbing machine on [message]"
log_mapping("Overlapping [message] detected at [AREACOORD(parent_movable)]")
parent_movable.set_anchored(FALSE)
return
enable()
/datum/component/plumbing/RegisterWithParent()
RegisterSignals(parent, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING), PROC_REF(disable))
RegisterSignals(parent, list(COMSIG_OBJ_DEFAULT_UNFASTEN_WRENCH), PROC_REF(toggle_active))
RegisterSignal(parent, COMSIG_ATOM_TOOL_ACT(TOOL_WRENCH), PROC_REF(check_wrench))
RegisterSignal(parent, COMSIG_MOVABLE_SET_ANCHORED, PROC_REF(toggle_active))
RegisterSignal(parent, COMSIG_OBJ_HIDE, PROC_REF(hide))
RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(create_overlays)) //called by lateinit on startup
RegisterSignal(parent, COMSIG_ATOM_DIR_CHANGE, PROC_REF(on_parent_dir_change)) //called when placed on a shuttle and it moves, and other edge cases
RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(create_overlays))
RegisterSignal(parent, COMSIG_ATOM_POST_DIR_CHANGE, PROC_REF(on_parent_dir_change))
RegisterSignal(parent, COMSIG_MOVABLE_CHANGE_DUCT_LAYER, PROC_REF(change_ducting_layer))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
/datum/component/plumbing/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING, COMSIG_OBJ_DEFAULT_UNFASTEN_WRENCH, COMSIG_OBJ_HIDE, \
COMSIG_ATOM_UPDATE_OVERLAYS, COMSIG_ATOM_DIR_CHANGE, COMSIG_MOVABLE_CHANGE_DUCT_LAYER))
REMOVE_TRAIT(parent, TRAIT_UNDERFLOOR, REF(src))
UnregisterSignal(parent, list(
COMSIG_ATOM_TOOL_ACT(TOOL_WRENCH),
COMSIG_MOVABLE_SET_ANCHORED,
COMSIG_OBJ_HIDE,
COMSIG_ATOM_UPDATE_OVERLAYS,
COMSIG_ATOM_POST_DIR_CHANGE,
COMSIG_MOVABLE_CHANGE_DUCT_LAYER,
COMSIG_ATOM_EXAMINE,
))
/datum/component/plumbing/Destroy()
ducts = null
disable()
ducts.Cut()
reagents = null
set_recipient_reagents_holder(null) //null is there so it's obvious we're setting this to nothing
return ..()
/datum/component/plumbing/process()
if(!demand_connects || !reagents)
return PROCESS_KILL
///Returns if the machine is active or not
/datum/component/plumbing/proc/active()
var/atom/movable/parent_movable = parent
return parent_movable.anchored
if(!reagents.holder_full())
for(var/D in GLOB.cardinals)
if(D & demand_connects)
send_request(D)
///Returns the reagent holder meant to receive the reagents. Can be different from the one that sends reagents to the network
/datum/component/plumbing/proc/recipient_reagents_holder()
return reagents
///Can we be added to the ductnet?
/datum/component/plumbing/proc/can_add(datum/ductnet/ductnet, dir)
if(!active)
return
if(!dir || !ductnet)
return FALSE
if(num2text(dir) in ducts)
return FALSE
///Give the direction of a pipe, and it'll return wich direction it originally was when its object pointed SOUTH
/datum/component/plumbing/proc/get_original_direction(dir)
if(!dir)
return 0
var/atom/movable/parent_movable = parent
return turn(dir, dir2angle(parent_movable.dir) - 180)
return TRUE
///settle wherever we are, and start behaving like a piece of plumbing
/datum/component/plumbing/proc/enable()
if(demand_connects)
START_PROCESSING(SSplumbing, src)
///called from in process(). only calls process_request(), but can be overwritten for children with special behaviour
/datum/component/plumbing/proc/send_request(dir)
process_request(dir = dir)
for(var/direction in GLOB.cardinals)
if(!(direction & (demand_connects | supply_connects)))
continue
///check who can give us what we want, and how many each of them will give us
/datum/component/plumbing/proc/process_request(amount = MACHINE_REAGENT_TRANSFER, reagent, dir, round_robin = TRUE)
//find the duct to take from
var/datum/ductnet/net
if(!ducts.Find(num2text(dir)))
return FALSE
net = ducts[num2text(dir)]
var/opposite_dir = REVERSE_DIR(direction)
for(var/atom/movable/found_atom in get_step(parent, direction))
var/obj/machinery/duct/duct = found_atom
if(istype(duct))
if(duct.neighbours && (duct.duct_layer & ducting_layer))
duct.neighbours[parent] = opposite_dir
duct.update_appearance(UPDATE_ICON_STATE)
duct.net.add_plumber(src, direction)
continue
//find all valid suppliers in the duct
var/list/valid_suppliers = list()
for(var/datum/component/plumbing/supplier as anything in net.suppliers)
if(supplier.can_give(amount, reagent, net))
valid_suppliers += supplier
var/suppliersLeft = valid_suppliers.len
if(!suppliersLeft)
return FALSE
for(var/datum/component/plumbing/plumber as anything in found_atom.GetComponents(/datum/component/plumbing))
if(plumber.active() && (plumber.ducting_layer & ducting_layer))
if((plumber.demand_connects & opposite_dir) && (supply_connects & direction) || (plumber.supply_connects & opposite_dir) && (demand_connects & direction)) //make sure we arent connecting two supplies or demands
var/datum/ductnet/net = new
net.add_plumber(src, direction)
net.add_plumber(plumber, opposite_dir)
//take an equal amount from each supplier
var/currentRequest
var/target_volume = reagents.total_volume + amount
for(var/datum/component/plumbing/give as anything in valid_suppliers)
currentRequest = (target_volume - reagents.total_volume) / suppliersLeft
give.transfer_to(src, currentRequest, reagent, net, round_robin)
suppliersLeft--
return TRUE
/datum/component/plumbing/proc/disable()
SIGNAL_HANDLER
///returns TRUE when they can give the specified amount and reagent. called by process request
/datum/component/plumbing/proc/can_give(amount, reagent, datum/ductnet/net)
SHOULD_BE_PURE(TRUE)
STOP_PROCESSING(SSplumbing, src)
if(amount <= 0)
return
while(ducts.len)
var/datum/ductnet/net = ducts[ducts[1]]
if(reagent) //only asked for one type of reagent
for(var/datum/reagent/contained_reagent as anything in reagents.reagent_list)
if(contained_reagent.type == reagent)
return TRUE
else if(reagents.total_volume) //take whatever
return TRUE
//disconnect ourself from any ducts connected to us
for(var/obj/machinery/duct/pipe as anything in net.ducts)
if(pipe.neighbours[parent])
pipe.neighbours -= parent
pipe.update_appearance(UPDATE_ICON_STATE)
return FALSE
//remove ourself from this network and delete it if emtpy
if(net.remove_plumber(src))
qdel(net)
///this is where the reagent is actually transferred and is thus the finish point of our process()
/datum/component/plumbing/proc/transfer_to(datum/component/plumbing/target, amount, reagent, datum/ductnet/net, round_robin = TRUE)
if(!reagents || !target || !target.reagents)
return FALSE
/datum/component/plumbing/proc/check_wrench(obj/parent_obj, mob/user, tool, processing_recipes)
SIGNAL_HANDLER
reagents.trans_to(target.recipient_reagents_holder, amount, target_id = reagent, methods = round_robin ? LINEAR : NONE)
if(!active())
var/datum/overlap = ducting_layer_check(parent_obj, -ducting_layer)
if(!isnull(overlap))
parent_obj.balloon_alert(user, "overlapping [istype(overlap, /obj/machinery/duct) ? "duct" : "machine"] detected!")
return ITEM_INTERACT_FAILURE
/datum/component/plumbing/proc/toggle_active(obj/parent_obj, new_state)
SIGNAL_HANDLER
// Follow atmos's rule of exposing the connection if you unwrench it and only hiding again if tile is placed back down.
if(tile_covered)
tile_covered = FALSE
parent_obj.update_appearance()
if(new_state)
enable()
else
disable()
/datum/component/plumbing/proc/hide(atom/movable/parent_obj, underfloor_accessibility)
SIGNAL_HANDLER
var/atom/movable/parent_movable = parent
// If machine is unanchored, keep connector visible.
// This doesn't necessary map to `active`, so check parent.
var/should_hide = !underfloor_accessibility
if(parent_movable.anchored || !should_hide)
tile_covered = should_hide
parent_obj.update_appearance()
///We create our luxurious piping overlays/underlays, to indicate where we do what. only called once if use_overlays = TRUE in Initialize()
/datum/component/plumbing/proc/create_overlays(atom/movable/parent_movable, list/overlays)
SIGNAL_HANDLER
if(tile_covered || !use_overlays)
if(tile_covered)
return
//Copied from ducts handle_layer()
@@ -165,7 +193,6 @@
var/duct_x = offset - parent_movable.pixel_x - parent_movable.pixel_w
var/duct_y = offset - parent_movable.pixel_y - parent_movable.pixel_z
var/duct_layer = PLUMBING_PIPE_VISIBILE_LAYER + ducting_layer * 0.0003
var/extension_handled = FALSE
for(var/direction in GLOB.cardinals)
var/color
@@ -176,203 +203,138 @@
else
continue
var/direction_text = dir2text(direction)
var/image/overlay
if(turn_connects)
overlay = image('icons/obj/pipes_n_cables/hydrochem/connects.dmi', "[direction_text]-[ducting_layer]", layer = duct_layer)
else
overlay = image('icons/obj/pipes_n_cables/hydrochem/connects.dmi', "[direction_text]-[ducting_layer]-s", layer = duct_layer)
overlay.dir = direction
var/image/overlay = image('icons/obj/pipes_n_cables/hydrochem/connects.dmi', "[dir2text(direction)]-[ducting_layer]", layer = duct_layer)
overlay.color = color
overlay.pixel_w = duct_x
overlay.pixel_z = duct_y
overlays += overlay
// This is a little wiggley extension to make wallmounts like sinks and showers visually link to the pipe
if(extend_pipe_to_edge && !extension_handled)
var/image/edge_overlay = image('icons/obj/pipes_n_cables/hydrochem/connects.dmi', "edge-extension", layer = duct_layer)
edge_overlay.dir = parent_movable.dir
edge_overlay.color = color
edge_overlay.pixel_w = -parent_movable.pixel_x - parent_movable.pixel_w
edge_overlay.pixel_z = -parent_movable.pixel_y - parent_movable.pixel_z
overlays += edge_overlay
// only show extension for the first pipe. This means we'll only reflect that color.
extension_handled = TRUE
///we stop acting like a plumbing thing and disconnect if we are, so we can safely be moved and stuff
/datum/component/plumbing/proc/disable()
/datum/component/plumbing/proc/on_parent_dir_change(atom/movable/parent_obj, old_dir, new_dir)
SIGNAL_HANDLER
if(!active)
return
STOP_PROCESSING(SSplumbing, src)
//remove_plumber() can remove all ducts at once if they all belong to the same pipenet
//for e.g. in case of circular connections
//so we check if we have ducts to remove after each iteration
while(ducts.len)
var/datum/ductnet/duct = ducts[ducts[1]] //for maps index 1 will return the 1st key
duct.remove_plumber(src)
active = FALSE
for(var/direction in GLOB.cardinals)
if(!(direction & (demand_connects | supply_connects)))
continue
for(var/obj/machinery/duct/duct in get_step(parent, direction))
if(!(duct.duct_layer & ducting_layer))
continue
duct.remove_connects(REVERSE_DIR(direction))
duct.neighbours.Remove(parent)
duct.update_appearance()
///settle wherever we are, and start behaving like a piece of plumbing
/datum/component/plumbing/proc/enable()
if(active)
return
update_dir()
active = TRUE
var/atom/movable/parent_movable = parent
// Destroy any ducts under us on the same layer.
// Ducts also self-destruct if placed under a plumbing machine.
// Machines disable when they get moved
for(var/obj/machinery/duct/duct in parent_movable.loc)
if(duct.anchored && (duct.duct_layer & ducting_layer))
log_mapping("[duct.name] was found at [AREACOORD(parent_movable)] spawning on top of [parent_movable.name], \
which should not occur as it takes ducts itself!")
duct.disconnect_duct()
if(demand_connects)
START_PROCESSING(SSplumbing, src)
for(var/direction in GLOB.cardinals)
if(!(direction & (demand_connects | supply_connects)))
continue
for(var/atom/movable/found_atom in get_step(parent, direction))
if(istype(found_atom, /obj/machinery/duct))
var/obj/machinery/duct/duct = found_atom
duct.attempt_connect()
continue
for(var/datum/component/plumbing/plumber as anything in found_atom.GetComponents(/datum/component/plumbing))
if(plumber.ducting_layer & ducting_layer)
direct_connect(plumber, direction)
/// Toggle our machinery on or off. This is called by a hook from default_unfasten_wrench with anchored as only param, so we dont have to copypaste this on every object that can move
/datum/component/plumbing/proc/toggle_active(obj/parent_obj, new_state)
SIGNAL_HANDLER
// Follow atmos's rule of exposing the connection if you unwrench it and only hiding again if tile is placed back down.
if(tile_covered)
tile_covered = FALSE
parent_obj.update_appearance()
if(new_state)
enable()
else
disable()
/** We update our connects only when we settle down by taking our current and original direction to find our new connects
* If someone wants it to fucking spin while connected to something go actually knock yourself out
*/
/datum/component/plumbing/proc/update_dir()
if(!turn_connects)
return
var/atom/movable/AM = parent
var/new_demand_connects
var/new_supply_connects
var/new_dir = AM.dir
var/angle = 180 - dir2angle(new_dir)
if(new_dir == SOUTH)
demand_connects = initial(demand_connects)
supply_connects = initial(supply_connects)
else
demand_connects = initial(demand_connects)
supply_connects = initial(supply_connects)
if(new_dir != SOUTH)
var/angle = 180 - dir2angle(new_dir)
var/new_demand_connects = NONE
var/new_supply_connects = NONE
for(var/direction in GLOB.cardinals)
if(direction & initial(demand_connects))
new_demand_connects += turn(direction, angle)
if(direction & initial(supply_connects))
new_supply_connects += turn(direction, angle)
if(direction & demand_connects)
new_demand_connects |= turn(direction, angle)
if(direction & supply_connects)
new_supply_connects |= turn(direction, angle)
demand_connects = new_demand_connects
supply_connects = new_supply_connects
///Give the direction of a pipe, and it'll return wich direction it originally was when its object pointed SOUTH
/datum/component/plumbing/proc/get_original_direction(dir)
var/atom/movable/parent_movable = parent
return turn(dir, dir2angle(parent_movable.dir) - 180)
//special case in-case we want to connect directly with another machine without a duct
/datum/component/plumbing/proc/direct_connect(datum/component/plumbing/plumbing, dir)
if(!plumbing.active)
return
var/opposite_dir = REVERSE_DIR(dir)
if(plumbing.demand_connects & opposite_dir && supply_connects & dir || plumbing.supply_connects & opposite_dir && demand_connects & dir) //make sure we arent connecting two supplies or demands
var/datum/ductnet/net = new()
net.add_plumber(src, dir)
net.add_plumber(plumbing, opposite_dir)
/datum/component/plumbing/proc/hide(atom/movable/parent_obj, underfloor_accessibility)
SIGNAL_HANDLER
// If machine is unanchored, keep connector visible.
// This doesn't necessary map to `active`, so check parent.
var/atom/movable/parent_movable = parent
var/should_hide = !underfloor_accessibility
if(should_hide)
ADD_TRAIT(parent_obj, TRAIT_UNDERFLOOR, REF(src))
else
REMOVE_TRAIT(parent_obj, TRAIT_UNDERFLOOR, REF(src))
if(parent_movable.anchored || !should_hide)
tile_covered = should_hide
parent_obj.update_appearance()
if(length(ducts))
disable()
enable()
/datum/component/plumbing/proc/change_ducting_layer(obj/source, obj/changer, new_layer = DUCT_LAYER_DEFAULT)
SIGNAL_HANDLER
ducting_layer = new_layer
var/atom/movable/parent_movable = parent
parent_movable.update_appearance()
ducting_layer = new_layer
source.update_appearance(UPDATE_OVERLAYS)
if(changer)
playsound(changer, 'sound/items/tools/ratchet.ogg', 10, TRUE) //sound
//quickly disconnect and reconnect the network.
if(active)
if(length(ducts))
disable()
enable()
/datum/component/plumbing/proc/set_recipient_reagents_holder(datum/reagents/receiver)
if(recipient_reagents_holder)
UnregisterSignal(recipient_reagents_holder, COMSIG_QDELETING) //stop tracking whoever we were tracking
if(receiver)
RegisterSignal(receiver, COMSIG_QDELETING, PROC_REF(handle_reagent_del)) //on deletion call a wrapper proc that clears us, and maybe reagents too
/datum/component/plumbing/process()
if(!demand_connects)
return PROCESS_KILL
recipient_reagents_holder = receiver
var/datum/reagents/receiver = recipient_reagents_holder()
if(QDELETED(receiver))
return PROCESS_KILL
/datum/component/plumbing/proc/handle_reagent_del(datum/source)
SIGNAL_HANDLER
if(source == reagents)
reagents = null
if(source == recipient_reagents_holder)
set_recipient_reagents_holder(null)
if(!receiver.holder_full())
for(var/dir in GLOB.cardinals)
if(dir & demand_connects)
send_request(dir)
/**
* Called when the dir changes. Need to adjust positioning of pipes.
*/
/datum/component/plumbing/proc/on_parent_dir_change(atom/movable/parent_obj, old_dir, new_dir)
/datum/component/plumbing/proc/on_examine(atom/movable/source, mob/user, list/examine_list)
SIGNAL_HANDLER
if(old_dir == new_dir)
if(distinct_reagent_cap != INFINITY)
examine_list += span_notice("This plumbing component will only accept up to [distinct_reagent_cap] distinct reagents at once.")
///called from in process(). only calls process_request(), but can be overwritten for children with special behaviour
/datum/component/plumbing/proc/send_request(dir)
var/amount_to_give = MACHINE_REAGENT_TRANSFER
// infinite cap means we need to special handling, process_request will just grab as much as it wants.
if(distinct_reagent_cap == INFINITY)
process_request(amount_to_give, null, dir) // null for no specific reagent, we're not picky.
return
// Defer to later frame because pixel_* is actually updated after all callbacks
addtimer(CALLBACK(parent_obj, TYPE_PROC_REF(/atom/, update_appearance)), 0.1 SECONDS)
// we have a cap, so we need to figure out what reagents we want
var/list/all_allowed_reagents = get_all_network_reagents(ducts["[dir]"])
if(length(all_allowed_reagents) > distinct_reagent_cap)
all_allowed_reagents.Cut(distinct_reagent_cap + 1)
else if(!length(all_allowed_reagents))
return
// request an even amount of each allowed reagent
var/amount_per_reagent = round(amount_to_give / length(all_allowed_reagents), CHEMICAL_VOLUME_ROUNDING)
for(var/allowed_reagent in all_allowed_reagents)
process_request(amount_per_reagent, allowed_reagent, dir)
/// Returns a list of all distinct reagent types available in the passed duct network.
/// The passed net can be null, it is handled.
/datum/component/plumbing/proc/get_all_network_reagents(datum/ductnet/net)
var/list/distinct_reagents = list()
for(var/datum/reagent/existing_regent as anything in reagents.reagent_list)
distinct_reagents |= existing_regent.type
for(var/datum/component/plumbing/supplier as anything in net?.suppliers)
for(var/datum/reagent/chemical as anything in supplier.reagents.reagent_list)
distinct_reagents |= chemical.type
return distinct_reagents
///check who can give us what we want, and how many each of them will give us
/datum/component/plumbing/proc/process_request(amount = MACHINE_REAGENT_TRANSFER, reagent, dir, round_robin = TRUE)
//find the duct to take from
var/dirtext = num2text(dir)
var/datum/ductnet/net = ducts[dirtext]
if(QDELETED(net))
if(net)
ducts -= dirtext
return FALSE
//find all valid suppliers in the duct
var/list/valid_suppliers = list()
for(var/datum/component/plumbing/supplier as anything in net?.suppliers)
if(supplier.can_give(amount, reagent, net))
valid_suppliers += supplier
var/suppliersLeft = length(valid_suppliers)
if(!suppliersLeft)
return FALSE
//take an equal amount from each supplier
var/currentRequest
var/target_volume = reagents.total_volume + amount
for(var/datum/component/plumbing/give as anything in valid_suppliers)
currentRequest = (target_volume - reagents.total_volume) / suppliersLeft
give.transfer_to(src, currentRequest, reagent, net, round_robin)
suppliersLeft--
return TRUE
///returns TRUE when they can give the specified amount and reagent. called by process request
/datum/component/plumbing/proc/can_give(amount, reagent, datum/ductnet/net)
SHOULD_BE_PURE(TRUE)
if(amount <= 0)
return FALSE
if(reagent) //only asked for one type of reagent
return reagents.has_reagent(reagent)
else if(reagents.total_volume) //take whatever
return TRUE
return FALSE
///this is where the reagent is actually transferred and is thus the finish point of our process()
/datum/component/plumbing/proc/transfer_to(datum/component/plumbing/target, amount, reagent, datum/ductnet/net, round_robin = TRUE)
reagents.trans_to(target.recipient_reagents_holder(), amount, target_id = reagent, methods = round_robin ? LINEAR : NONE)
@@ -1,13 +1,14 @@
/datum/component/plumbing/automated_iv
demand_connects = SOUTH
supply_connects = NORTH
///Temporary holder to store all the reagents from the iv drip before transferring it to the ducts
distinct_reagent_cap = 3
///Temporary holder to store all the reagents from the iv drip before transferring to the ducts
var/datum/reagents/plumbing/holder
/datum/component/plumbing/automated_iv/Initialize(start=TRUE, _ducting_layer, _turn_connects=TRUE, datum/reagents/custom_receiver)
. = ..()
/datum/component/plumbing/automated_iv/Initialize(ducting_layer)
if(!istype(parent, /obj/machinery/iv_drip/plumbing))
return COMPONENT_INCOMPATIBLE
. = ..()
var/obj/machinery/iv_drip/plumbing/drip = parent
holder = new(drip.reagents.maximum_volume, drip.reagents.flags)
holder.my_atom = drip
@@ -17,16 +18,13 @@
return ..()
/datum/component/plumbing/automated_iv/can_give(amount, reagent)
. = ..()
if(!.)
return
var/obj/machinery/iv_drip/plumbing/drip = parent
return drip.mode == 0
return ..() && drip.mode == IV_TAKING
/datum/component/plumbing/automated_iv/send_request(dir)
var/obj/machinery/iv_drip/plumbing/drip = parent
if(drip.mode == 1)
process_request(dir = dir)
if(drip.mode == IV_INJECTING)
return ..()
/datum/component/plumbing/automated_iv/transfer_to(datum/component/plumbing/target, amount, reagent, datum/ductnet/net, round_robin = TRUE)
reagents.trans_to(holder, reagents.total_volume)
+1 -2
View File
@@ -4,10 +4,9 @@
demand_connects = WEST
supply_connects = EAST
/datum/component/plumbing/buffer/Initialize(start = TRUE, _turn_connects = TRUE, _ducting_layer, datum/reagents/custom_receiver)
/datum/component/plumbing/buffer/Initialize(ducting_layer)
if(!istype(parent, /obj/machinery/plumbing/buffer))
return COMPONENT_INCOMPATIBLE
return ..()
/datum/component/plumbing/buffer/can_give(amount, reagent, datum/ductnet/net)
@@ -3,23 +3,18 @@
supply_connects = EAST
var/obj/machinery/plumbing/acclimator/myacclimator
/datum/component/plumbing/acclimator/Initialize(start=TRUE, _ducting_layer, _turn_connects=TRUE, datum/reagents/custom_receiver)
. = ..()
/datum/component/plumbing/acclimator/Initialize(ducting_layer)
if(!istype(parent, /obj/machinery/plumbing/acclimator))
return COMPONENT_INCOMPATIBLE
myacclimator = parent
/datum/component/plumbing/acclimator/Destroy(force)
myacclimator = null
return ..()
/datum/component/plumbing/acclimator/can_give(amount, reagent)
. = ..()
if(. && myacclimator.emptying)
return TRUE
return FALSE
var/obj/machinery/plumbing/acclimator/myacclimator = parent
return myacclimator.emptying && ..()
///We're overriding process and not send_request, because all process does is do the requests, so we might aswell cut out the middle man and save some code from running
/datum/component/plumbing/acclimator/process()
if(myacclimator.emptying)
return
return ..()
var/obj/machinery/plumbing/acclimator/myacclimator = parent
if(!myacclimator.emptying)
..()
@@ -0,0 +1,48 @@
///We handle the unity part of plumbing. We track who is connected to who.
/datum/ductnet
///All the ducts that make this network
var/list/obj/machinery/duct/ducts
///Stuff that can supply chems used by machines to retrive chems
var/list/datum/component/plumbing/suppliers
///Stuff that demands chems keep track of components that need their ducts updated as this net evolves
var/list/datum/component/plumbing/demanders
/datum/ductnet/New(obj/machinery/duct/parent)
ducts = parent ? list(parent) : list()
suppliers = list()
demanders = list()
return ..()
/datum/ductnet/Destroy(force)
ducts.Cut()
for(var/datum/component/plumbing/plumbing as anything in suppliers + demanders)
remove_plumber(plumbing)
suppliers.Cut()
demanders.Cut()
return ..()
///add a plumbing object to either demanders or suppliers
/datum/ductnet/proc/add_plumber(datum/component/plumbing/plumbing, dir)
var/dirtext = num2text(dir)
if(plumbing.ducts[dirtext] == src)
return FALSE
plumbing.ducts[dirtext] = src
if(dir & plumbing.supply_connects)
suppliers += plumbing
if(dir & plumbing.demand_connects)
demanders += plumbing
return TRUE
///remove a plumber. we don't delete ourselves because ductnets don't persist through plumbing objects.
/datum/ductnet/proc/remove_plumber(datum/component/plumbing/plumbing)
for(var/dir in plumbing.ducts)
if(plumbing.ducts[dir] == src)
plumbing.ducts -= dir
dir = text2num(dir)
if(dir & plumbing.supply_connects)
suppliers -= plumbing
if(dir & plumbing.demand_connects)
demanders -= plumbing
//return if this net has no ducts like when 2 machines are connected
return ducts.len == 0
+2 -2
View File
@@ -3,10 +3,10 @@
demand_connects = NORTH
supply_connects = SOUTH | EAST | WEST //SOUTH is straight, EAST is left and WEST is right. We look from the perspective of the insert
/datum/component/plumbing/filter/Initialize(start=TRUE, _ducting_layer, _turn_connects=TRUE, datum/reagents/custom_receiver)
. = ..()
/datum/component/plumbing/filter/Initialize(ducting_layer)
if(!istype(parent, /obj/machinery/plumbing/filter))
return COMPONENT_INCOMPATIBLE
return ..()
/datum/component/plumbing/filter/can_give(amount, reagent, datum/ductnet/net)
. = ..()
@@ -0,0 +1,60 @@
/// Special demand connector that consumes as normal, but redirects water into the magical water space.
/datum/component/plumbing/hydroponics
demand_connects = SOUTH
/// Alternate reagents container to buffer incoming water
var/datum/reagents/water_reagents
/// Decides if we receive either water or regular reagents
var/receive_water = FALSE
/datum/component/plumbing/hydroponics/Initialize(ducting_layer)
. = ..()
if(!istype(parent, /obj/machinery/hydroponics/constructable))
return COMPONENT_INCOMPATIBLE
var/obj/machinery/hydroponics/constructable/hydro_parent = parent
water_reagents = new(hydro_parent.maxwater)
water_reagents.my_atom = hydro_parent
/datum/component/plumbing/hydroponics/Destroy()
QDEL_NULL(water_reagents)
return ..()
/datum/component/plumbing/hydroponics/recipient_reagents_holder()
return receive_water ? water_reagents : reagents
/datum/component/plumbing/hydroponics/send_request(dir)
var/obj/machinery/hydroponics/constructable/hydro_parent = parent
var/initial_nutri_amount = reagents.total_volume
if(initial_nutri_amount < reagents.maximum_volume)
// Well boy howdy, we have no way to tell a supply to not mix the water with everything else,
// So we'll let it leak in, and move the water over.
receive_water = FALSE
process_request(dir = dir, round_robin = FALSE)
// Move the leaked water from nutrients to... water
var/leaking_water_amount = reagents.get_reagent_amount(/datum/reagent/water)
if(leaking_water_amount)
reagents.trans_to(water_reagents, leaking_water_amount, target_id = /datum/reagent/water)
// We should only take MACHINE_REAGENT_TRANSFER every tick; this is the remaining amount we can take
var/remaining_transfer_amount = max(MACHINE_REAGENT_TRANSFER - (reagents.total_volume - initial_nutri_amount), 0)
// How much extra water we should gather this tick to try to fill the water tray.
var/extra_water_to_gather = clamp(hydro_parent.maxwater - hydro_parent.waterlevel - water_reagents.total_volume, 0, remaining_transfer_amount)
if(extra_water_to_gather > 0)
receive_water = TRUE
process_request(
amount = extra_water_to_gather,
reagent = /datum/reagent/water,
dir = dir
)
// Now transfer all remaining water in that buffer and clear it out.
var/final_water_amount = water_reagents.total_volume
if(final_water_amount)
hydro_parent.adjust_waterlevel(round(final_water_amount))
// Using a pipe doesn't afford you extra water storage and the baseline behavior for trays is that excess water goes into the shadow realm.
water_reagents.clear_reagents()
@@ -2,10 +2,10 @@
demand_connects = NORTH
supply_connects = SOUTH
/datum/component/plumbing/reaction_chamber/Initialize(start=TRUE, _ducting_layer, _turn_connects=TRUE, datum/reagents/custom_receiver)
. = ..()
/datum/component/plumbing/reaction_chamber/Initialize(ducting_layer)
if(!istype(parent, /obj/machinery/plumbing/reaction_chamber))
return COMPONENT_INCOMPATIBLE
return ..()
/datum/component/plumbing/reaction_chamber/can_give(amount, reagent, datum/ductnet/net)
. = FALSE
@@ -66,22 +66,37 @@
if(chamber.emptying) //if we are still emptying then keep checking for reagents until we are emptied out
chamber.RegisterSignal(reagents, COMSIG_REAGENTS_HOLDER_UPDATED, TYPE_PROC_REF(/obj/machinery/plumbing/reaction_chamber, on_reagent_change))
/datum/component/plumbing/buffered
///The reagent to request into this buffer
VAR_PROTECTED/datum/reagent/request
///The buffer meant to store reagents
VAR_PRIVATE/datum/reagents/buffer
/datum/component/plumbing/buffered/Initialize(ducting_layer)
. = ..()
buffer = new (maximum = 50, new_flags = NO_REACT)
buffer.my_atom = parent
/datum/component/plumbing/buffered/Destroy()
QDEL_NULL(buffer)
return ..()
/datum/component/plumbing/buffered/recipient_reagents_holder()
return buffer
/datum/component/plumbing/buffered/send_request(dir)
process_request(reagent = request, dir = dir)
///Special connect that we currently use for reaction chambers. Being used so we can keep certain inputs separate, like into a special internal acid container
/datum/component/plumbing/acidic_input
/datum/component/plumbing/buffered/acidic_input
demand_connects = WEST
demand_color = COLOR_YELLOW
ducting_layer = SECOND_DUCT_LAYER
/datum/component/plumbing/acidic_input/send_request(dir)
process_request(reagent = /datum/reagent/reaction_agent/acidic_buffer, dir = dir)
request = /datum/reagent/reaction_agent/acidic_buffer
///Special connect that we currently use for reaction chambers. Being used so we can keep certain inputs separate, like into a special internal base container
/datum/component/plumbing/alkaline_input
/datum/component/plumbing/buffered/alkaline_input
demand_connects = EAST
demand_color = COLOR_VIBRANT_LIME
ducting_layer = FOURTH_DUCT_LAYER
/datum/component/plumbing/alkaline_input/send_request(dir)
process_request(reagent = /datum/reagent/reaction_agent/basic_buffer, dir = dir)
request = /datum/reagent/reaction_agent/basic_buffer
@@ -3,6 +3,36 @@
/datum/component/plumbing/simple_demand
demand_connects = SOUTH
/datum/component/plumbing/simple_demand/Initialize(ducting_layer, distinct_reagent_cap = INFINITY)
src.distinct_reagent_cap = distinct_reagent_cap
return ..()
///Component for adding an extended overlay on wall mounts
/datum/component/plumbing/simple_demand/extended/create_overlays(atom/movable/parent_movable, list/overlays)
. = ..()
// This is a little wiggley extension to make wallmounts like sinks and showers visually link to the pipe
if(overlays.len)
var/image/edge_overlay = image('icons/obj/pipes_n_cables/hydrochem/connects.dmi', "edge-extension", layer = PLUMBING_PIPE_VISIBILE_LAYER + ducting_layer * 0.0003)
edge_overlay.dir = parent_movable.dir
edge_overlay.color = demand_color
edge_overlay.pixel_w = -parent_movable.pixel_x - parent_movable.pixel_w
edge_overlay.pixel_z = -parent_movable.pixel_y - parent_movable.pixel_z
overlays += edge_overlay
///For disposing reagents
/datum/component/plumbing/simple_demand/disposer
/datum/component/plumbing/simple_demand/disposer/Initialize(ducting_layer, distinct_reagent_cap)
if(!istype(parent, /obj/machinery/plumbing/disposer))
return COMPONENT_INCOMPATIBLE
return ..()
/datum/component/plumbing/simple_demand/disposer/send_request(dir)
var/obj/machinery/plumbing/disposer/target = parent
if(target.on)
process_request(target.disposal_rate * SSFLUIDS_DT, dir = dir)
///has one pipe output that only supplies. example is liquid pump and manual input pipe
/datum/component/plumbing/simple_supply
supply_connects = SOUTH
@@ -12,10 +42,13 @@
demand_connects = WEST
supply_connects = EAST
///Lazily demand from any direction. Overlays won't look good, and the aquarium sprite occupies about the entire 32x32 area anyway.
///Lazily demand from any direction.
/datum/component/plumbing/aquarium
demand_connects = SOUTH|NORTH|EAST|WEST
use_overlays = FALSE
/datum/component/plumbing/aquarium/create_overlays(atom/movable/parent_movable, list/overlays)
//Overlays won't look good, and the aquarium sprite occupies about the entire 32x32 area anyway.
return
///Connects different layer of ducts
/datum/component/plumbing/manifold
+4 -4
View File
@@ -2,10 +2,10 @@
demand_connects = NORTH
supply_connects = SOUTH | EAST
/datum/component/plumbing/splitter/Initialize(start=TRUE, _ducting_layer, _turn_connects=TRUE, datum/reagents/custom_receiver)
. = ..()
if(. && !istype(parent, /obj/machinery/plumbing/splitter))
return FALSE
/datum/component/plumbing/splitter/Initialize(ducting_layer)
if(!istype(parent, /obj/machinery/plumbing/splitter))
return COMPONENT_INCOMPATIBLE
return ..()
/datum/component/plumbing/splitter/can_give(amount, reagent, datum/ductnet/net)
. = ..()
@@ -0,0 +1,50 @@
/// Similar to the projectile drop element but holds an instance of an atom instead
/datum/component/projectile_instance_drop
dupe_mode = COMPONENT_DUPE_ALLOWED
var/atom/movable/dropped_atom
/datum/component/projectile_instance_drop/Initialize(atom/movable/dropped_atom)
if(!isprojectile(parent))
return COMPONENT_INCOMPATIBLE
if (!dropped_atom)
stack_trace("[type] created with no atom to drop.")
return COMPONENT_INCOMPATIBLE
src.dropped_atom = dropped_atom
/datum/component/projectile_instance_drop/RegisterWithParent()
dropped_atom.forceMove(parent)
RegisterSignal(parent, COMSIG_PROJECTILE_RANGE_OUT, PROC_REF(drop_item))
RegisterSignal(parent, COMSIG_PROJECTILE_SELF_ON_HIT, PROC_REF(on_projectile_hit))
RegisterSignals(dropped_atom, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING), PROC_REF(on_instance_left))
/datum/component/projectile_instance_drop/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PROJECTILE_SELF_ON_HIT))
if (dropped_atom)
UnregisterSignal(dropped_atom, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING))
QDEL_NULL(dropped_atom) // If it left our atom's contents then this is a null reference, if it didn't then it's fucked I guess
/datum/component/projectile_instance_drop/Destroy(force)
dropped_atom = null
return ..()
/// When we hit something check if we should hit the floor
/datum/component/projectile_instance_drop/proc/on_projectile_hit(obj/projectile/source, atom/movable/firer, atom/target, angle, hit_limb_zone, blocked, pierce_hit)
SIGNAL_HANDLER
if (blocked < 100 && !pierce_hit)
drop_item(source)
/// Drop item to ground
/datum/component/projectile_instance_drop/proc/drop_item(obj/projectile/source)
SIGNAL_HANDLER
var/turf/drop_turf = source.drop_location()
var/atom/movable/dropping = dropped_atom
dropping.forceMove(drop_turf)
if (isitem(dropping))
var/obj/item/item_dropped = dropping
item_dropped.do_drop_animation(drop_turf)
/// If our projectile exits our contents then remove the component
/datum/component/projectile_instance_drop/proc/on_instance_left()
SIGNAL_HANDLER
dropped_atom = null
qdel(src)
+103 -25
View File
@@ -13,6 +13,8 @@
var/change_base_icon_state = FALSE
/// If true, changing the reskin also changes the inhand_icon_state of the atom
var/change_inhand_icon_state = FALSE
/// If true, changing the reskin also changes the worn_icon_state of the atom
var/change_worn_icon_state = TRUE
/// If true, unset vars are reset to their original values when applying this skin
var/reset_missing = TRUE
@@ -24,6 +26,44 @@
var/new_icon
/// Optional, icon_state to change the atom to when applied
var/new_icon_state
/// Specifies the icon state for the atom's appearance in hand. Should appear in both new_lefthand_file and new_righthand_file.
var/new_inhand_icon_state
/// Optional, specifies the left hand inhand icon file. Don't forget to set the right hand file as well.
var/new_lefthand_file
/// Optional, specifies the right hand inhand icon file. Don't forget to set the left hand file as well.
var/new_righthand_file
/// Optional, specifies the worn icon file.
var/new_worn_icon
/// When set true, will allow concrete subtypes of abstract subtypes (such as for organizational purposes) to be selectable as reskins in the loadout menu.
var/allow_all_subtypes_in_loadout
/// Mandatory for GAGs items. The path to the greyscale item this is to be applied to.
var/atom/greyscale_item_path
/// Auto populated. The greyscale_config from greyscale_item_path.
VAR_FINAL/greyscale_config
/// Auto populated. The greyscale_colors from greyscale_item_path.
VAR_FINAL/greyscale_colors
/datum/atom_skin/New()
. = ..()
if(isnull(greyscale_item_path))
return
// Populate the fields required for GAGS previews
greyscale_config = greyscale_item_path::greyscale_config
greyscale_colors = greyscale_item_path::greyscale_colors
// The icon isn't 'new'. So we don't make an extra icon in map_icons.
if(greyscale_item_path::post_init_icon_state == new_icon_state)
new_icon_state = null
/// Returns the correct preview icon state for this atom skin, whether it be a map_preview or a normal icon. This is assumed from our var population above.
/datum/atom_skin/proc/get_preview_icon_state()
if(isnull(greyscale_item_path)) // Not a GAGs icon
return new_icon_state
if(isnull(new_icon_state)) // The base GAGS item icon
return "[greyscale_item_path]"
return "[greyscale_item_path]--[new_icon_state]" // A GAGs reskin of the item icon
/**
* Applies all relevant skin changes to the given atom
@@ -31,17 +71,27 @@
*
* * apply_to: The atom to apply the skin to
*/
/datum/atom_skin/proc/apply(atom/apply_to)
/datum/atom_skin/proc/apply(atom/apply_to, mob/user)
SHOULD_CALL_PARENT(TRUE)
APPLY_VAR_OR_RESET_INITIAL(apply_to, name, new_name, reset_missing)
APPLY_VAR_OR_RESET_INITIAL(apply_to, desc, new_desc, reset_missing)
APPLY_VAR_OR_RESET_INITIAL(apply_to, icon, new_icon, reset_missing)
if(!HAS_TRAIT(apply_to, TRAIT_WAS_RENAMED))
APPLY_VAR_OR_RESET_INITIAL(apply_to, name, new_name, reset_missing)
APPLY_VAR_OR_RESET_INITIAL(apply_to, desc, new_desc, reset_missing)
if(!apply_to.greyscale_config) // Never want to be resetting a GAGS icon back to initial.
APPLY_VAR_OR_RESET_INITIAL(apply_to, icon, new_icon, reset_missing)
APPLY_VAR_OR_RESET_TO(apply_to, icon_state, new_icon_state, reset_missing, initial(apply_to.post_init_icon_state) || initial(apply_to.icon_state))
if(change_base_icon_state)
APPLY_VAR_OR_RESET_INITIAL(apply_to, base_icon_state, new_icon_state, reset_missing)
if(change_inhand_icon_state && isitem(apply_to))
if(isitem(apply_to))
var/obj/item/item_apply_to = apply_to
APPLY_VAR_OR_RESET_INITIAL(item_apply_to, inhand_icon_state, new_icon_state, reset_missing)
if(!item_apply_to.greyscale_config_worn)
APPLY_VAR_OR_RESET_INITIAL(item_apply_to, worn_icon, new_worn_icon, reset_missing)
if(!item_apply_to.greyscale_config_inhand_left)
APPLY_VAR_OR_RESET_INITIAL(item_apply_to, lefthand_file, new_lefthand_file, reset_missing)
APPLY_VAR_OR_RESET_INITIAL(item_apply_to, righthand_file, new_righthand_file, reset_missing)
if(change_worn_icon_state)
APPLY_VAR_OR_RESET_INITIAL(item_apply_to, worn_icon_state, new_icon_state, reset_missing)
if(change_inhand_icon_state || new_inhand_icon_state)
APPLY_VAR_OR_RESET_INITIAL(item_apply_to, inhand_icon_state, new_inhand_icon_state || new_icon_state, reset_missing)
/**
* Resets all changes this skin would have made to the given atom
@@ -54,13 +104,20 @@
SHOULD_CALL_PARENT(TRUE)
RESET_INITIAL_IF_SET(clear_from, name, new_name)
RESET_INITIAL_IF_SET(clear_from, desc, new_desc)
RESET_INITIAL_IF_SET(clear_from, icon, new_icon)
var/is_greyscale = clear_from.greyscale_config || clear_from.greyscale_colors
if(!is_greyscale)
RESET_INITIAL_IF_SET(clear_from, icon, new_icon)
RESET_TO_IF_SET(clear_from, icon_state, new_icon_state, initial(clear_from.post_init_icon_state) || initial(clear_from.icon_state))
if(change_base_icon_state)
RESET_INITIAL_IF_SET(clear_from, base_icon_state, new_icon_state)
if(change_inhand_icon_state && isitem(clear_from))
if(isitem(clear_from))
var/obj/item/item_clear_from = clear_from
RESET_INITIAL_IF_SET(item_clear_from, inhand_icon_state, new_icon_state)
RESET_INITIAL_IF_SET(item_clear_from, worn_icon, new_worn_icon)
RESET_INITIAL_IF_SET(item_clear_from, inhand_icon_state, change_inhand_icon_state ? new_inhand_icon_state : new_icon_state)
if(!is_greyscale)
RESET_INITIAL_IF_SET(item_clear_from, worn_icon_state, new_icon_state)
RESET_INITIAL_IF_SET(item_clear_from, lefthand_file, new_lefthand_file)
RESET_INITIAL_IF_SET(item_clear_from, righthand_file, new_righthand_file)
/// Gets a preview image for this skin based on the given atom's icon and icon_state
/datum/atom_skin/proc/get_preview_icon(atom/for_atom)
@@ -82,14 +139,24 @@
VAR_PRIVATE/infinite_reskin = FALSE
/// List of subtypes of /datum/atom_skin that are not allowed to be used for this item
VAR_PRIVATE/list/blacklisted_subtypes
/// Currently applied skin preview_name
VAR_PRIVATE/current_skin
/datum/component/reskinable_item/Initialize(base_reskin_type, infinite = FALSE, initial_skin, list/blacklisted_subtypes = list())
/datum/component/reskinable_item/Initialize(base_reskin_type, infinite = FALSE, initial_skin, list/blacklisted_subtypes)
if(!isatom(parent) || isarea(parent))
return COMPONENT_INCOMPATIBLE
var/atom/atom_parent = parent
#ifdef UNIT_TESTS
if(atom_parent.greyscale_config && (atom_parent.type in GLOB.all_loadout_datums)) // We only care about these when they're in the loadout
var/datum/atom_skin/reskin_type = base_reskin_type
if(reskin_type && isnull(reskin_type::greyscale_item_path)) // greyscale_item_path unset
stack_trace("[type] added to a greyscale item without setting the greyscale_item_path! In [reskin_type], add 'greyscale_item_path = [atom_parent::type]'.")
else if(atom_parent.type != reskin_type::greyscale_item_path) // greyscale_item_path set but doesn't match the item it's being added to
stack_trace("[type] added to an invalid item type, [atom_parent.type]. [reskin_type] is set up to only be added to: [reskin_type::greyscale_item_path]. \
Either fix its greyscale_item_path if this is incorrect, or apply a different skin.")
#endif
src.base_reskin_type = base_reskin_type
src.infinite_reskin = infinite
src.blacklisted_subtypes = blacklisted_subtypes
@@ -97,7 +164,10 @@
if(initial_skin)
set_skin_by_name(initial_skin)
var/atom/atom_parent = parent
var/list/reskin_options = get_skins_by_name()
if(length(reskin_options) <= 1) // Check that we actually have reskin options - if not there's no point to existing past this point.
return COMPONENT_REDUNDANT
atom_parent.flags_1 |= HAS_CONTEXTUAL_SCREENTIPS_1
/datum/component/reskinable_item/RegisterWithParent()
@@ -110,15 +180,20 @@
UnregisterSignal(parent, COMSIG_ATOM_EXAMINE_TAGS)
UnregisterSignal(parent, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM)
/datum/component/reskinable_item/CheckDupeComponent(datum/component/comp, base_reskin_type, infinite = FALSE, initial_skin, list/blacklisted_subtypes = list())
if(src.base_reskin_type != base_reskin_type)
return FALSE // new comp - though the alt-click behavior will collide
/datum/component/reskinable_item/CheckDupeComponent(datum/component/comp, base_reskin_type, infinite = FALSE, initial_skin, list/blacklisted_subtypes)
// Always absorb added components
src.base_reskin_type = base_reskin_type
src.infinite_reskin = infinite
src.blacklisted_subtypes = blacklisted_subtypes
set_skin_by_name(initial_skin)
return TRUE // same comp
if(initial_skin)
set_skin_by_name(initial_skin)
var/list/reskin_options = get_skins_by_name()
if(length(reskin_options) <= 1) // Check that we actually have reskin options - if not there's no point to existing past this point.
qdel(src)
return TRUE
/datum/component/reskinable_item/proc/get_skins_by_name()
var/list/reskin_options = list()
@@ -127,15 +202,16 @@
return reskin_options
/datum/component/reskinable_item/proc/set_skin_by_name(input_name)
/datum/component/reskinable_item/proc/set_skin_by_name(input_name, mob/user)
var/list/reskin_options = get_skins_by_name()
var/list/atom_skins = get_atom_skins()
if(current_skin)
var/datum/atom_skin/previous_skin = GLOB.atom_skins[reskin_options[current_skin]]
previous_skin.clear_skin(parent)
var/datum/atom_skin/previous_skin = atom_skins[reskin_options[current_skin]]
previous_skin.clear_skin(parent, user)
if(input_name)
var/datum/atom_skin/reskin_to_apply = GLOB.atom_skins[reskin_options[input_name]]
reskin_to_apply.apply(parent)
var/datum/atom_skin/reskin_to_apply = atom_skins[reskin_options[input_name]]
reskin_to_apply.apply(parent, user)
current_skin = input_name
@@ -179,8 +255,10 @@
var/atom/atom_parent = parent
var/list/items = list()
var/list/atom_skins = get_atom_skins()
for(var/reskin_name, reskin_typepath in get_skins_by_name())
items[reskin_name] = GLOB.atom_skins[reskin_typepath].get_preview_icon(atom_parent)
var/datum/atom_skin/reskin = atom_skins[reskin_typepath]
items[reskin_name] = image(icon = reskin.new_icon || atom_parent.icon, icon_state = reskin.new_icon_state || atom_parent.icon_state)
sort_list(items)
@@ -188,7 +266,7 @@
if(!pick || !items[pick])
return
set_skin_by_name(pick)
set_skin_by_name(pick, user)
to_chat(user, span_info("[parent] is now skinned as '[pick].'"))
if(!infinite_reskin)
+7 -3
View File
@@ -32,6 +32,8 @@
var/override_allow_spacemove = FALSE
/// can anyone other than the rider unbuckle the rider?
var/can_force_unbuckle = TRUE
/// Like last_bumped for mobs, but for vehicles. Exists to allow the door bump cooldown while also passing door openings through Bumped()
var/vehicle_last_bumped = 0
/**
* Ride check flags defined for the specific riding component types, so we know if we need arms, legs, or whatever.
@@ -270,10 +272,12 @@
/// So we can check all occupants when we bump a door to see if anyone has access
/datum/component/riding/proc/vehicle_bump(atom/movable/movable_parent, obj/machinery/door/possible_bumped_door)
SIGNAL_HANDLER
if(!istype(possible_bumped_door))
if(!istype(possible_bumped_door) || world.time - vehicle_last_bumped <= 0.3 SECONDS)
return
for(var/occupant in movable_parent.buckled_mobs)
INVOKE_ASYNC(possible_bumped_door, TYPE_PROC_REF(/obj/machinery/door/, bumpopen), occupant)
for(var/mob/living/occupant in movable_parent.buckled_mobs)
vehicle_last_bumped = world.time
occupant.last_bumped = 0
INVOKE_ASYNC(possible_bumped_door, TYPE_PROC_REF(/atom, Bumped), occupant)
/datum/component/riding/proc/Unbuckle(atom/movable/M)
addtimer(CALLBACK(parent, TYPE_PROC_REF(/atom/movable/, unbuckle_mob), M), 0, TIMER_UNIQUE)
+2 -2
View File
@@ -69,7 +69,7 @@
// for fireman carries, check if the ridden is stunned/restrained
else if((ride_check_flags & CARRIER_NEEDS_ARM) && (HAS_TRAIT(living_parent, TRAIT_RESTRAINED) || INCAPACITATED_IGNORING(living_parent, INCAPABLE_RESTRAINTS|INCAPABLE_GRAB)))
. = FALSE
else if((ride_check_flags & JUST_FRIEND_RIDERS) && !(living_parent.faction.Find(REF(rider))))
else if((ride_check_flags & JUST_FRIEND_RIDERS) && !(living_parent.has_ally(rider)))
. = FALSE
if(. || !consequences)
@@ -234,7 +234,7 @@
/datum/component/riding/creature/handle_buckle(mob/living/rider)
. = ..()
var/mob/living/ridden = parent
if(!require_minigame || ridden.faction.Find(REF(rider)))
if(!require_minigame || ridden.has_ally(rider))
return
ridden.Shake(pixelshiftx = 1, pixelshifty = 0, duration = 1 SECONDS)
ridden.spin(spintime = 1 SECONDS, speed = 1)
-146
View File
@@ -1,146 +0,0 @@
/datum/component/simple_rotation
/// Additional stuff to do after rotation
var/datum/callback/post_rotation
/// Rotation flags for special behavior
var/rotation_flags = NONE
/**
* Adds the ability to rotate an object by Alt-click or using Right-click verbs.
*
* args:
* * rotation_flags (optional) Bitflags that determine behavior for rotation (defined at the top of this file)
* * post_rotation (optional) Callback proc that is used after the object is rotated (sound effects, balloon alerts, etc.)
**/
/datum/component/simple_rotation/Initialize(rotation_flags = NONE, post_rotation)
if(!ismovable(parent))
return COMPONENT_INCOMPATIBLE
var/atom/movable/source = parent
source.flags_1 |= HAS_CONTEXTUAL_SCREENTIPS_1
src.rotation_flags = rotation_flags
src.post_rotation = post_rotation || CALLBACK(src, PROC_REF(default_post_rotation))
/datum/component/simple_rotation/RegisterWithParent()
RegisterSignal(parent, COMSIG_CLICK_ALT, PROC_REF(rotate_left))
RegisterSignal(parent, COMSIG_CLICK_ALT_SECONDARY, PROC_REF(rotate_right))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(ExamineMessage))
RegisterSignal(parent, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM, PROC_REF(on_requesting_context_from_item))
return ..()
/datum/component/simple_rotation/PostTransfer(datum/new_parent)
//Because of the callbacks which we don't track cleanly we can't transfer this
//item cleanly, better to let the new of the new item create a new rotation datum
//instead (there's no real state worth transferring)
return COMPONENT_NOTRANSFER
/datum/component/simple_rotation/UnregisterFromParent()
UnregisterSignal(parent, list(
COMSIG_CLICK_ALT,
COMSIG_CLICK_ALT_SECONDARY,
COMSIG_ATOM_EXAMINE,
COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM,
))
return ..()
/datum/component/simple_rotation/Destroy()
post_rotation = null
return ..()
/datum/component/simple_rotation/proc/ExamineMessage(datum/source, mob/user, list/examine_list)
SIGNAL_HANDLER
if(rotation_flags & ROTATION_REQUIRE_WRENCH)
examine_list += span_notice("This requires a wrench to be rotated.")
/datum/component/simple_rotation/proc/rotate_right(datum/source, mob/user)
SIGNAL_HANDLER
rotate(user, ROTATION_CLOCKWISE)
return CLICK_ACTION_SUCCESS
/datum/component/simple_rotation/proc/rotate_left(datum/source, mob/user)
SIGNAL_HANDLER
rotate(user, ROTATION_COUNTERCLOCKWISE)
return CLICK_ACTION_SUCCESS
/datum/component/simple_rotation/proc/rotate(mob/user, degrees)
if(QDELETED(user))
CRASH("[src] is being rotated [user ? "with a qdeleting" : "without a"] user")
if(!istype(user))
CRASH("[src] is being rotated without a user of the wrong type: [user.type]")
if(!isnum(degrees))
CRASH("[src] is being rotated without providing the amount of degrees needed")
if(!can_be_rotated(user, degrees) || !can_user_rotate(user, degrees))
return
var/obj/rotated_obj = parent
rotated_obj.setDir(turn(rotated_obj.dir, degrees))
if(rotation_flags & ROTATION_REQUIRE_WRENCH)
playsound(rotated_obj, 'sound/items/tools/ratchet.ogg', 50, TRUE)
post_rotation.Invoke(user, degrees)
/datum/component/simple_rotation/proc/can_user_rotate(mob/user, degrees)
if(isliving(user) && user.can_perform_action(parent, NEED_DEXTERITY))
return TRUE
if((rotation_flags & ROTATION_GHOSTS_ALLOWED) && isobserver(user) && CONFIG_GET(flag/ghost_interaction))
return TRUE
return FALSE
/datum/component/simple_rotation/proc/can_be_rotated(mob/user, degrees, silent=FALSE)
var/obj/rotated_obj = parent
if(!rotated_obj.Adjacent(user))
silent = TRUE
if(rotation_flags & ROTATION_REQUIRE_WRENCH)
if(!isliving(user))
return FALSE
var/obj/item/tool = user.get_active_held_item()
if(!tool || tool.tool_behaviour != TOOL_WRENCH)
if(!silent)
rotated_obj.balloon_alert(user, "need a wrench!")
return FALSE
if(!(rotation_flags & ROTATION_IGNORE_ANCHORED) && rotated_obj.anchored)
if(istype(rotated_obj, /obj/structure/window) && !silent)
rotated_obj.balloon_alert(user, "need to unscrew!")
else if(!silent)
rotated_obj.balloon_alert(user, "need to unwrench!")
return FALSE
if(rotation_flags & ROTATION_NEEDS_ROOM)
var/target_dir = turn(rotated_obj.dir, degrees)
var/obj/structure/window/rotated_window = rotated_obj
var/fulltile = istype(rotated_window) ? rotated_window.fulltile : FALSE
if(!valid_build_direction(rotated_obj.loc, target_dir, is_fulltile = fulltile))
if(!silent)
rotated_obj.balloon_alert(user, "can't rotate in that direction!")
return FALSE
if(rotation_flags & ROTATION_NEEDS_UNBLOCKED)
var/turf/rotate_turf = get_turf(rotated_obj)
if(rotate_turf.is_blocked_turf(source_atom = rotated_obj))
if(!silent)
rotated_obj.balloon_alert(user, "rotation is blocked!")
return FALSE
return TRUE
/datum/component/simple_rotation/proc/default_post_rotation(mob/user, degrees)
return
// maybe we don't need the item context proc but instead the hand one? since we don't need to check held_item
/datum/component/simple_rotation/proc/on_requesting_context_from_item(atom/source, list/context, obj/item/held_item, mob/user)
SIGNAL_HANDLER
var/rotation_screentip = FALSE
if(can_be_rotated(user, ROTATION_CLOCKWISE, silent=TRUE))
context[SCREENTIP_CONTEXT_ALT_LMB] = "Rotate left"
rotation_screentip = TRUE
if(can_be_rotated(user, ROTATION_COUNTERCLOCKWISE, silent=TRUE))
context[SCREENTIP_CONTEXT_ALT_RMB] = "Rotate right"
rotation_screentip = TRUE
if(rotation_screentip)
return CONTEXTUAL_SCREENTIP_SET
return NONE
+1 -12
View File
@@ -9,8 +9,7 @@
if(!ismob(parent))
return COMPONENT_INCOMPATIBLE
access = new_access
RegisterSignal(parent, COMSIG_MOB_TRIED_ACCESS, PROC_REF(on_tried_access))
RegisterSignal(parent, COMSIG_MOB_RETRIEVE_SIMPLE_ACCESS, PROC_REF(retrieve_access))
RegisterSignal(parent, COMSIG_MOB_RETRIEVE_ACCESS, PROC_REF(retrieve_access))
if(!donor_atom)
return
if(isorgan(donor_atom))
@@ -19,16 +18,6 @@
RegisterSignal(donor_atom, COMSIG_IMPLANT_REMOVED, PROC_REF(on_donor_removed))
RegisterSignal(donor_atom, COMSIG_QDELETING, PROC_REF(on_donor_removed))
/datum/component/simple_access/proc/on_tried_access(datum/source, atom/locked_thing)
SIGNAL_HANDLER
if(!isobj(locked_thing))
return LOCKED_ATOM_INCOMPATIBLE
var/obj/locked_object = locked_thing
if(locked_object.check_access_list(access))
return ACCESS_ALLOWED
else
return ACCESS_DISALLOWED
/datum/component/simple_access/proc/retrieve_access(datum/source, list/access_list)
SIGNAL_HANDLER
access_list += access
+188
View File
@@ -0,0 +1,188 @@
// A component for basic mobs that makes them able to slide under doors by right-clicking on them.
// Sliding under doors has a configurable initial delay, and allows the mob to remain under the door indefinitely.
// If the door is opened while the mob is under it, then the mob will be ejected from their hiding spot.
// Instantly sliding under doors should be handled by giving the mob the PASSDOORS pass flag.
/datum/component/slide_under_doors
can_transfer = TRUE
/// The delay for sliding under a door in deciseconds.
var/slide_in_delay = 0
/// The delay for sliding out from under a door in deciseconds.
var/slide_out_delay = 0
/// The user that is currently under a door, if any.
var/mob/living/current_user = null
/// The door the user is currently under, if any.
var/obj/machinery/door/current_door = null
/datum/component/slide_under_doors/Initialize(slide_in_delay = 5 SECONDS, slide_out_delay = 1 SECONDS)
if (!isbasicmob(parent))
return COMPONENT_INCOMPATIBLE
src.slide_in_delay = slide_in_delay
src.slide_out_delay = slide_out_delay
/datum/component/slide_under_doors/RegisterWithParent()
RegisterSignal(parent, COMSIG_MOB_CLIENT_LOGIN, PROC_REF(on_user_login))
RegisterSignal(parent, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_user_unarmed_attack))
notify_user()
/datum/component/slide_under_doors/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_MOB_CLIENT_LOGIN, COMSIG_LIVING_UNARMED_ATTACK))
if (!QDELETED(current_user) && !QDELETED(current_door))
slide_out_from_under_door()
else if (current_user || current_door)
unregister_user_and_door()
/datum/component/slide_under_doors/proc/on_user_login(mob/living/user, client/user_client)
SIGNAL_HANDLER
notify_user()
/datum/component/slide_under_doors/proc/notify_user()
to_chat(parent, span_notice("You can slide under doors! <b>Right-click on a door to slide under it.</b>"))
/datum/component/slide_under_doors/proc/on_user_unarmed_attack(mob/living/user, atom/target, is_adjacent, modifiers)
SIGNAL_HANDLER
if (!modifiers[RIGHT_CLICK])
return
if (!istype(target, /obj/machinery/door))
return
INVOKE_ASYNC(src, PROC_REF(try_slide_under_door), user, target)
return COMPONENT_CANCEL_ATTACK_CHAIN
/datum/component/slide_under_doors/proc/try_slide_under_door(mob/living/user, obj/machinery/door/door)
if (!can_slide_under_door(user, door))
return
user.visible_message(
message = span_danger("\The [user] start[user.p_s()] sliding under \the [door]!"),
self_message = span_notice("You start sliding under \the [door]."),
blind_message = span_hear("You hear squeezing."),
)
playsound(user, 'sound/effects/footstep/gib_step.ogg', vol = 50, vary = TRUE, ignore_walls = FALSE)
if (!do_after(user, slide_in_delay, door, extra_checks = CALLBACK(src, PROC_REF(can_slide_under_door), user, door)))
return
slide_under_door(user, door)
/datum/component/slide_under_doors/proc/can_slide_under_door(mob/living/user, obj/machinery/door/door)
if (!user.Adjacent(door))
return FALSE
if (user.loc == door)
return FALSE
if (!isturf(user.loc))
user.balloon_alert(user, "not on the ground!")
return FALSE
if (!door.IsReachableBy(user))
user.balloon_alert(user, "can't reach!")
return FALSE
if (HAS_TRAIT(user, TRAIT_INCAPACITATED))
user.balloon_alert(user, "incapacitated!")
return FALSE
if (!(door.pass_flags_self & PASSDOORS))
door.balloon_alert(user, "impassable!")
return FALSE
if (!door.density)
door.balloon_alert(user, "already open!")
return FALSE
return TRUE
/datum/component/slide_under_doors/proc/slide_under_door(mob/living/user, obj/machinery/door/door)
user.visible_message(
message = span_danger("\The [user] slide[user.p_s()] under \the [door] with a pop!"),
self_message = span_notice("You slide under \the [door] with a pop!"),
blind_message = span_hear("You hear a pop."),
)
playsound(user, 'sound/effects/meatslap.ogg', vol = 50, vary = TRUE, ignore_walls = FALSE)
user.forceMove(door)
register_user_and_door(user, door)
/datum/component/slide_under_doors/proc/try_slide_out_from_under_door(move_dir)
if (DOING_INTERACTION_WITH_TARGET(current_user, current_door))
return
var/turf/visible_turf = get_step(current_user, move_dir)
if (visible_turf)
visible_turf.visible_message(
message = span_danger("Something starts sliding out from under \the [current_door]!"),
blind_message = span_hear("You hear squeezing."),
ignored_mobs = current_user,
)
playsound(visible_turf, 'sound/effects/footstep/gib_step.ogg', vol = 50, vary = TRUE, ignore_walls = FALSE)
to_chat(current_user, span_notice("You start sliding out from under \the [current_door]."))
if (!do_after(current_user, slide_out_delay, current_door, timed_action_flags = IGNORE_INCAPACITATED))
return
slide_out_from_under_door(move_dir)
/datum/component/slide_under_doors/proc/slide_out_from_under_door(move_dir)
// We need to store local references to the user and the door before unregistering them.
var/mob/living/user = current_user
var/obj/machinery/door/door = current_door
unregister_user_and_door()
user.forceMove(door.drop_location())
if (move_dir)
user.Move(get_step(user, move_dir), move_dir)
user.visible_message(
message = span_danger("\The [user] slide[user.p_s()] out from under \the [door] with a pop!"),
self_message = span_notice("You slide out from under \the [door] with a pop!"),
blind_message = span_hear("You hear a pop."),
)
playsound(user, 'sound/effects/meatslap.ogg', vol = 50, vary = TRUE, ignore_walls = FALSE)
/datum/component/slide_under_doors/proc/register_user_and_door(mob/living/user, obj/machinery/door/door)
ADD_TRAIT(user, TRAIT_INCAPACITATED, UNDER_DOOR_TRAIT)
RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(on_user_moved))
current_user = user
RegisterSignal(door, COMSIG_QDELETING, PROC_REF(on_door_qdeleting))
RegisterSignal(door, COMSIG_ATOM_DENSITY_CHANGED, PROC_REF(on_door_density_changed))
RegisterSignal(door, COMSIG_ATOM_RELAYMOVE, PROC_REF(on_door_relaymove))
current_door = door
/datum/component/slide_under_doors/proc/unregister_user_and_door()
if (current_user)
REMOVE_TRAITS_IN(current_user, UNDER_DOOR_TRAIT)
UnregisterSignal(current_user, COMSIG_MOVABLE_MOVED)
current_user = null
if (current_door)
UnregisterSignal(current_door, list(COMSIG_QDELETING, COMSIG_ATOM_DENSITY_CHANGED, COMSIG_ATOM_RELAYMOVE))
current_door = null
/datum/component/slide_under_doors/proc/on_user_moved(mob/living/user, obj/machinery/door/door, dir, forced, list/old_locs)
SIGNAL_HANDLER
if (user.loc != door)
unregister_user_and_door()
/datum/component/slide_under_doors/proc/on_door_qdeleting(obj/machinery/door/door, force)
SIGNAL_HANDLER
unregister_user_and_door()
/datum/component/slide_under_doors/proc/on_door_density_changed(obj/machinery/door/door)
SIGNAL_HANDLER
if (!door.density)
slide_out_from_under_door()
/datum/component/slide_under_doors/proc/on_door_relaymove(obj/machinery/door/door, mob/living/user, move_dir)
SIGNAL_HANDLER
INVOKE_ASYNC(src, PROC_REF(try_slide_out_from_under_door), move_dir)
return COMSIG_BLOCK_RELAYMOVE
+3 -3
View File
@@ -32,13 +32,13 @@
if(!isliving(parent))
return COMPONENT_INCOMPATIBLE
var/mob/living/living_parent = parent
living_parent.faction |= FACTION_SLIME
living_parent.add_faction(FACTION_SLIME)
RegisterSignal(living_parent, COMSIG_ENTER_AREA, PROC_REF(start_slime_prodaction))
/datum/component/slime_friends/Destroy(force)
. = ..()
var/mob/living/living_parent = parent
living_parent.faction -= FACTION_SLIME
living_parent.remove_faction(FACTION_SLIME)
timer = null
/// Start slime prodaction when we leave wizden.
@@ -56,6 +56,6 @@
var/turf/where = get_turf(friend)
var/new_colour = pick(colours)
var/mob/living/basic/slime/new_friend = new(where, new_colour, SLIME_LIFE_STAGE_ADULT)
new_friend.faction = friend.faction.Copy()
SET_FACTION_AND_ALLIES_FROM(new_friend, friend)
new_friend.set_enraged_behaviour()
friend.nutrition -= 50
+4 -4
View File
@@ -1,6 +1,6 @@
/datum/component/soapbox
/// List of our current soapboxxer(s) who are gaining loud speech
var/list/soapboxers = list()
var/list/soapboxers
/// Gives atoms moving over us the soapbox speech and takes it away when they leave
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_loc_entered),
@@ -21,21 +21,21 @@
if(QDELETED(soapbox_arrive))
return
RegisterSignal(soapbox_arrive, COMSIG_MOB_SAY, PROC_REF(soapbox_speech))
soapboxers += soapbox_arrive
LAZYADD(soapboxers, soapbox_arrive)
///Takes away loud speech from our movable when it leaves the turf our parent is on
/datum/component/soapbox/proc/on_loc_exited(datum/source, mob/living/soapbox_leave)
SIGNAL_HANDLER
if(soapbox_leave in soapboxers)
UnregisterSignal(soapbox_leave, COMSIG_MOB_SAY)
soapboxers -= soapbox_leave
LAZYREMOVE(soapboxers, soapbox_leave)
///We don't want our soapboxxer to keep their loud say if the parent is moved out from under them
/datum/component/soapbox/proc/parent_moved(datum/source)
SIGNAL_HANDLER
for(var/atom/movable/loud as anything in soapboxers)
UnregisterSignal(loud, COMSIG_MOB_SAY)
soapboxers.Cut()
LAZYNULL(soapboxers)
///Gives a mob a unique say span
/datum/component/soapbox/proc/soapbox_speech(datum/source, list/speech_args)
+1 -4
View File
@@ -94,19 +94,16 @@
if (isliving(created))
var/mob/living/created_mob = created
created_mob.faction = src.faction
created_mob.set_faction(faction)
RegisterSignal(created, COMSIG_MOB_STATCHANGE, PROC_REF(mob_stat_changed))
SEND_SIGNAL(src, COMSIG_SPAWNER_SPAWNED, created)
RegisterSignal(created, COMSIG_QDELETING, PROC_REF(on_deleted))
spawn_callback?.Invoke(created)
if (spawn_text)
spawner.visible_message(span_danger("A creature [spawn_text] [spawner]."))
/// Remove weakrefs to atoms which have been killed or deleted without us picking it up somehow
/datum/component/spawner/proc/validate_references()
for (var/datum/weakref/weak_thing as anything in spawned_things)
+1
View File
@@ -84,6 +84,7 @@
var/atom/parent_atom = parent
sticker_overlay = mutable_appearance(icon = our_sticker.icon, icon_state = our_sticker.icon_state, layer = parent_atom.layer + 0.01, appearance_flags = RESET_COLOR)
sticker_overlay.color = our_sticker.color
sticker_overlay.pixel_w = px - ICON_SIZE_X / 2
sticker_overlay.pixel_z = py - ICON_SIZE_Y / 2
+4 -4
View File
@@ -326,7 +326,7 @@
return
var/mob/living/disrespected = attacked_atom
if(disrespected.stat || faction_check(punching_person.faction, disrespected.faction) || !(FACTION_MINING in disrespected.faction))
if(disrespected.stat || disrespected.faction_check_atom(punching_person) || !disrespected.has_faction(FACTION_MINING))
return
add_action(ACTION_DISRESPECT, 60 * (ismegafauna(disrespected) ? 2 : 1))
@@ -339,7 +339,7 @@
var/mob/living/attacked = attacked_mob
var/mob/mob_parent = parent
if(faction_check(attacking_person.faction, attacked.faction) || !(FACTION_MINING in attacked.faction) || (istype(mob_parent.get_active_held_item(), /obj/item/kinetic_crusher) && attacked.has_status_effect(/datum/status_effect/crusher_mark)))
if(attacking_person.faction_check_atom(attacked) || !attacked.has_faction(FACTION_MINING) || (istype(mob_parent.get_active_held_item(), /obj/item/kinetic_crusher) && attacked.has_status_effect(/datum/status_effect/crusher_mark)))
return
add_action(ACTION_MELEED, 50 * (ismegafauna(attacked) ? 1.5 : 1))
@@ -373,7 +373,7 @@
/datum/component/style/proc/on_resonator_burst(datum/source, mob/creator, mob/living/hit_living)
SIGNAL_HANDLER
if(faction_check(creator.faction, hit_living.faction) || (hit_living.stat != CONSCIOUS) || !(FACTION_MINING in hit_living.faction))
if(creator.faction_check_atom(hit_living) || (hit_living.stat != CONSCIOUS) || !hit_living.has_faction(FACTION_MINING))
return
add_action(ACTION_TRAPPER, 70)
@@ -429,7 +429,7 @@
if(died == parent)
change_points(-500, use_multiplier = FALSE)
return
else if(faction_check(mob_parent.faction, died.faction) || !(FACTION_MINING in died.faction) || (died.z != mob_parent.z) || !(died in view(mob_parent.client?.view, get_turf(mob_parent))))
else if(mob_parent.faction_check_atom(died) || !died.has_faction(FACTION_MINING) || (died.z != mob_parent.z) || !(died in view(mob_parent.client?.view, get_turf(mob_parent))))
return
if(ismegafauna(died))
add_action(ACTION_MAJOR_KILL, 350)
+2 -2
View File
@@ -76,8 +76,8 @@
var/mob/living/simple_animal/hostile/angry_boy = summoned
angry_boy.friends |= summoner // do not attack our summon boy
spawned_mobs |= summoned
if(faction != null)
summoned.faction = faction.Copy()
if(faction)
summoned.set_faction(faction)
RegisterSignals(summoned, list(COMSIG_LIVING_DEATH, COMSIG_QDELETING), PROC_REF(on_spawned_death))
spawn_location.visible_message(span_danger("[summoned] [spawn_text]!"))
-363
View File
@@ -1,363 +0,0 @@
/// Allows an item to be used to initiate surgeries.
/datum/component/surgery_initiator
/// The currently selected target that the user is proposing a surgery on
var/datum/weakref/surgery_target_ref
/// The last user, as a weakref
var/datum/weakref/last_user_ref
/datum/component/surgery_initiator/Initialize()
. = ..()
if(!isitem(parent))
return COMPONENT_INCOMPATIBLE
var/obj/item/surgery_tool = parent
surgery_tool.item_flags |= ITEM_HAS_CONTEXTUAL_SCREENTIPS
///Make sure we can use the surgery UI while laying down
surgery_tool.interaction_flags_atom |= INTERACT_ATOM_IGNORE_MOBILITY
/datum/component/surgery_initiator/Destroy(force)
last_user_ref = null
surgery_target_ref = null
return ..()
/datum/component/surgery_initiator/RegisterWithParent()
RegisterSignal(parent, COMSIG_ITEM_ATTACK, PROC_REF(initiate_surgery_moment))
RegisterSignal(parent, COMSIG_ITEM_REQUESTING_CONTEXT_FOR_TARGET, PROC_REF(add_item_context))
/datum/component/surgery_initiator/UnregisterFromParent()
UnregisterSignal(parent, COMSIG_ITEM_ATTACK, COMSIG_ITEM_REQUESTING_CONTEXT_FOR_TARGET)
unregister_signals()
/datum/component/surgery_initiator/proc/unregister_signals()
var/mob/living/last_user = last_user_ref?.resolve()
if (!isnull(last_user_ref))
UnregisterSignal(last_user, COMSIG_MOB_SELECTED_ZONE_SET)
var/mob/living/surgery_target = surgery_target_ref?.resolve()
if (!isnull(surgery_target_ref))
UnregisterSignal(surgery_target, COMSIG_MOB_SURGERY_STARTED)
/// Does the surgery initiation.
/datum/component/surgery_initiator/proc/initiate_surgery_moment(datum/source, atom/target, mob/user)
SIGNAL_HANDLER
if(!isliving(target))
return
INVOKE_ASYNC(src, PROC_REF(do_initiate_surgery_moment), target, user)
return COMPONENT_CANCEL_ATTACK_CHAIN
/datum/component/surgery_initiator/proc/do_initiate_surgery_moment(mob/living/target, mob/user)
var/datum/surgery/current_surgery
for(var/i_one in target.surgeries)
var/datum/surgery/surgeryloop = i_one
if(surgeryloop.location == user.zone_selected)
current_surgery = surgeryloop
break
if (!isnull(current_surgery) && !current_surgery.step_in_progress)
attempt_cancel_surgery(current_surgery, target, user)
return
var/list/available_surgeries = get_available_surgeries(user, target)
if(!length(available_surgeries))
if (target.body_position == LYING_DOWN || !(target.mobility_flags & MOBILITY_LIEDOWN))
target.balloon_alert(user, "no surgeries available!")
else
target.balloon_alert(user, "make them lie down!")
return
unregister_signals()
last_user_ref = WEAKREF(user)
surgery_target_ref = WEAKREF(target)
RegisterSignal(user, COMSIG_MOB_SELECTED_ZONE_SET, PROC_REF(on_set_selected_zone))
RegisterSignal(target, COMSIG_MOB_SURGERY_STARTED, PROC_REF(on_mob_surgery_started))
ui_interact(user)
/datum/component/surgery_initiator/proc/get_available_surgeries(mob/user, mob/living/target)
var/list/available_surgeries = list()
var/obj/item/bodypart/affecting = target.get_bodypart(check_zone(user.zone_selected))
for(var/datum/surgery/surgery as anything in GLOB.surgeries_list)
if(!surgery.possible_locs.Find(user.zone_selected))
continue
if(!is_type_in_list(target, surgery.target_mobtypes))
continue
if(user == target && !HAS_TRAIT(user, TRAIT_SELF_SURGERY) && !(surgery.surgery_flags & SURGERY_SELF_OPERABLE))
continue
if(isnull(affecting))
if(surgery.surgery_flags & SURGERY_REQUIRE_LIMB)
continue
else
if(surgery.requires_bodypart_type && !(affecting.bodytype & surgery.requires_bodypart_type))
continue
if(surgery.targetable_wound && !affecting.get_wound_type(surgery.targetable_wound))
continue
if((surgery.surgery_flags & SURGERY_REQUIRES_REAL_LIMB) && (affecting.bodypart_flags & BODYPART_PSEUDOPART))
continue
if(IS_IN_INVALID_SURGICAL_POSITION(target, surgery))
continue
if(!surgery.can_start(user, target))
continue
available_surgeries += surgery
return available_surgeries
/// Does the surgery de-initiation.
/datum/component/surgery_initiator/proc/attempt_cancel_surgery(datum/surgery/the_surgery, mob/living/patient, mob/user)
var/selected_zone = user.zone_selected
if(the_surgery.status == 1)
patient.surgeries -= the_surgery
user.visible_message(
span_notice("[user] removes [parent] from [patient]'s [patient.parse_zone_with_bodypart(selected_zone)]."),
span_notice("You remove [parent] from [patient]'s [patient.parse_zone_with_bodypart(selected_zone)]."),
)
patient.balloon_alert(user, "stopped work on [patient.parse_zone_with_bodypart(selected_zone)]")
qdel(the_surgery)
return
var/required_tool_type = TOOL_CAUTERY
var/obj/item/close_tool = user.get_inactive_held_item()
var/is_robotic = the_surgery.requires_bodypart_type == BODYTYPE_ROBOTIC
if(is_robotic)
required_tool_type = TOOL_SCREWDRIVER
if(!close_tool || close_tool.tool_behaviour != required_tool_type)
patient.balloon_alert(user, "need a [is_robotic ? "screwdriver": "cautery"] in your inactive hand to stop the surgery!")
return
if(the_surgery.operated_bodypart)
the_surgery.operated_bodypart.adjustBleedStacks(-5)
patient.surgeries -= the_surgery
user.visible_message(
span_notice("[user] closes [patient]'s [patient.parse_zone_with_bodypart(selected_zone)] with [close_tool] and removes [parent]."),
span_notice("You close [patient]'s [patient.parse_zone_with_bodypart(selected_zone)] with [close_tool] and remove [parent]."),
)
patient.balloon_alert(user, "closed up [patient.parse_zone_with_bodypart(selected_zone)]")
qdel(the_surgery)
/datum/component/surgery_initiator/proc/on_mob_surgery_started(mob/source, datum/surgery/surgery, surgery_location)
SIGNAL_HANDLER
var/mob/living/last_user = last_user_ref.resolve()
if (surgery_location != last_user.zone_selected)
return
if (!isnull(last_user))
source.balloon_alert(last_user, "someone else started a surgery!")
ui_close()
/datum/component/surgery_initiator/proc/on_set_selected_zone(mob/source, new_zone)
ui_interact(source)
/datum/component/surgery_initiator/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if (!ui)
ui = new(user, src, "SurgeryInitiator")
ui.open()
/datum/component/surgery_initiator/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if (.)
return .
var/mob/user = usr
var/mob/living/surgery_target = surgery_target_ref.resolve()
if (isnull(surgery_target))
return TRUE
switch (action)
if ("change_zone")
var/zone = params["new_zone"]
if (!(zone in list(
BODY_ZONE_HEAD,
BODY_ZONE_CHEST,
BODY_ZONE_L_ARM,
BODY_ZONE_R_ARM,
BODY_ZONE_L_LEG,
BODY_ZONE_R_LEG,
BODY_ZONE_PRECISE_EYES,
BODY_ZONE_PRECISE_MOUTH,
BODY_ZONE_PRECISE_GROIN,
)))
return TRUE
var/atom/movable/screen/zone_sel/zone_selector = user.hud_used?.zone_select
zone_selector?.set_selected_zone(zone, user, should_log = FALSE)
return TRUE
if ("start_surgery")
for (var/datum/surgery/surgery as anything in get_available_surgeries(user, surgery_target))
if (surgery.name == params["surgery_name"])
try_choose_surgery(user, surgery_target, surgery)
return TRUE
/datum/component/surgery_initiator/ui_assets(mob/user)
return list(
get_asset_datum(/datum/asset/simple/body_zones),
)
/datum/component/surgery_initiator/ui_data(mob/user)
var/mob/living/surgery_target = surgery_target_ref?.resolve()
var/list/surgeries = list()
if (!isnull(surgery_target))
for (var/datum/surgery/surgery as anything in get_available_surgeries(user, surgery_target))
var/list/surgery_info = list(
"name" = surgery.name,
)
if (surgery_needs_exposure(surgery, surgery_target))
surgery_info["blocked"] = TRUE
surgery_info["blocked_reason"] = "Their body is covered!" // SKYRAT EDIT ADDITION - Surgically unremovable bodyparts
// SKYRAT EDIT START - Surgically unremovable bodyparts
if (surgery.removes_target_bodypart)
if (iscarbon(surgery_target))
var/mob/living/carbon/carbon_target = surgery_target
var/obj/item/bodypart/affecting_limb = carbon_target.get_bodypart(check_zone(user.zone_selected))
if(!affecting_limb.can_be_surgically_removed)
surgery_info["blocked"] = TRUE
surgery_info["blocked_reason"] = "That limb cannot be surgically removed!"
// SKYRAT EDIT END
surgeries += list(surgery_info)
return list(
"selected_zone" = user.zone_selected,
"target_name" = surgery_target?.name,
"surgeries" = surgeries,
)
/datum/component/surgery_initiator/ui_close(mob/user)
unregister_signals()
surgery_target_ref = null
return ..()
/datum/component/surgery_initiator/ui_status(mob/user, datum/ui_state/state)
var/mob/living/surgery_target = surgery_target_ref?.resolve()
if (isnull(surgery_target))
return UI_CLOSE
if (!can_start_surgery(user, surgery_target))
return UI_CLOSE
return ..()
/datum/component/surgery_initiator/proc/can_start_surgery(mob/user, mob/living/target)
if (!user.Adjacent(target))
return FALSE
// The item was moved somewhere else
var/atom/movable/tool = parent
if (tool.loc != user)
return FALSE
// While we were choosing, another surgery was started at the same location
for (var/datum/surgery/surgery in target.surgeries)
if (surgery.location == user.zone_selected)
return FALSE
return TRUE
/datum/component/surgery_initiator/proc/try_choose_surgery(mob/user, mob/living/target, datum/surgery/surgery)
if (!can_start_surgery(user, target))
// This could have a more detailed message, but the UI closes when this is true anyway, so
// if it ever comes up, it'll be because of lag.
target.balloon_alert(user, "can't start the surgery!")
return
var/selected_zone = user.zone_selected
var/obj/item/bodypart/affecting_limb = target.get_bodypart(check_zone(selected_zone))
if ((surgery.surgery_flags & SURGERY_REQUIRE_LIMB) && isnull(affecting_limb))
target.balloon_alert(user, "patient has no [parse_zone(selected_zone)]!")
return
if (!isnull(affecting_limb))
if(surgery.requires_bodypart_type && !(affecting_limb.bodytype & surgery.requires_bodypart_type))
target.balloon_alert(user, "not the right type of limb!")
return
if(surgery.targetable_wound && !affecting_limb.get_wound_type(surgery.targetable_wound))
target.balloon_alert(user, "no wound to operate on!")
return
// SKYRAT EDIT START - Limbs that can't be surgically removed
if (surgery.removes_target_bodypart && !isnull(affecting_limb) && !affecting_limb.can_be_surgically_removed)
target.balloon_alert(user, "limb can't be surgically removed!")
return
// SKYRAT EDIT END
if (IS_IN_INVALID_SURGICAL_POSITION(target, surgery))
target.balloon_alert(user, "patient is not lying down!")
return
if (!surgery.can_start(user, target))
target.balloon_alert(user, "can't start the surgery!")
return
if (surgery_needs_exposure(surgery, target))
target.balloon_alert(user, "expose [target.p_their()] [target.parse_zone_with_bodypart(selected_zone)]!")
return
ui_close()
var/datum/surgery/procedure = new surgery.type(target, selected_zone, affecting_limb)
target.balloon_alert(user, "starting \"[LOWER_TEXT(procedure.name)]\"")
user.visible_message(
span_notice("[user] drapes [parent] over [target]'s [target.parse_zone_with_bodypart(selected_zone)] to prepare for surgery."),
span_notice("You drape [parent] over [target]'s [target.parse_zone_with_bodypart(selected_zone)] to prepare for \an [procedure.name]."),
)
log_combat(user, target, "operated on", null, "(OPERATION TYPE: [procedure.name]) (TARGET AREA: [selected_zone])")
return TRUE // BUBBER EDIT ADDITION - For modular_zubbers/code/datums/components/surgery_initiator.dm
/datum/component/surgery_initiator/proc/surgery_needs_exposure(datum/surgery/surgery, mob/living/target)
var/mob/living/user = last_user_ref?.resolve()
if (isnull(user))
return FALSE
if(surgery.surgery_flags & SURGERY_IGNORE_CLOTHES)
return FALSE
return !get_location_accessible(target, user.zone_selected)
/**
* Adds context sensitivy directly to the surgery initator file for screentips
* Arguments:
* * source - the surgery drapes, cloak, or bedsheet calling surgery initator
* * context - Preparing Surgery, the component has a lot of ballon alerts to deal with most contexts
* * target - the living target mob you are doing surgery on
* * user - refers to user who will see the screentip when the drapes are on a living target
*/
/datum/component/surgery_initiator/proc/add_item_context(obj/item/source, list/context, atom/target, mob/living/user,)
SIGNAL_HANDLER
if(!isliving(target))
return NONE
context[SCREENTIP_CONTEXT_LMB] = "Prepare Surgery"
return CONTEXTUAL_SCREENTIP_SET
+39
View File
@@ -0,0 +1,39 @@
/// Used for tracking mobs that have had synthetic blood injected into them.
/// The higher the synth content, the more nerfed the blood is for blood worms.
/// This decays over time while the mob is alive.
/datum/component/synth_blood
// This has a performance improvement over COMPONENT_DUPE_HIGHLANDER as it skips creating a new component instance.
dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
/// 0-1 of how synthetic this mobs blood is right now.
var/synth_content = 0
/datum/component/synth_blood/Initialize(new_synth_content = 0)
if (!isliving(parent))
return COMPONENT_INCOMPATIBLE
if (new_synth_content <= 0)
return COMPONENT_REDUNDANT
synth_content = min(new_synth_content, 1)
RegisterSignal(parent, COMSIG_LIVING_LIFE, PROC_REF(on_life))
/datum/component/synth_blood/InheritComponent(datum/component/synth_blood/new_component, i_am_original, new_synth_content)
if (new_component)
new_synth_content = new_component.synth_content
if (new_synth_content <= 0)
qdel(src)
synth_content = min(new_synth_content, 1)
/datum/component/synth_blood/Destroy(force)
UnregisterSignal(parent, COMSIG_LIVING_LIFE)
return ..()
/datum/component/synth_blood/proc/on_life(mob/living/living_parent, seconds_per_tick, times_fired)
SIGNAL_HANDLER
if (synth_content <= 0)
qdel(src)
else if (living_parent.stat != DEAD)
// Ten minutes (or 600 seconds) to fully clear synth content from one to zero.
synth_content = max(synth_content - (1/600) * seconds_per_tick, 0)
-197
View File
@@ -1,197 +0,0 @@
/// Component which allows mobs to be smashed onto this surface like a wrestling move
/datum/component/table_smash
/// If true, mobs will be placed gently on the table even if they're in an aggressive grab
var/gentle_push
/// Callback invoked after a hostile table action
var/datum/callback/after_smash
/datum/component/table_smash/Initialize(gentle_push = FALSE, after_smash = null)
. = ..()
if (!isobj(parent))
return COMPONENT_INCOMPATIBLE
src.gentle_push = gentle_push
src.after_smash = after_smash
/datum/component/table_smash/RegisterWithParent()
RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_interaction))
RegisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION, PROC_REF(on_item_interaction))
var/static/list/loc_connections = list(
COMSIG_LIVING_DISARM_COLLIDE = PROC_REF(on_pushed_into),
)
AddComponent(/datum/component/connect_loc_behalf, parent, loc_connections)
/datum/component/table_smash/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_ATOM_ATTACK_HAND, COMSIG_ATOM_ITEM_INTERACTION))
/// Called when someone clicks on our surface
/datum/component/table_smash/proc/on_interaction(obj/table, mob/user)
SIGNAL_HANDLER
if (!table.Adjacent(user) || !user.pulling)
return
if (!isliving(user.pulling))
if (!(user.pulling.pass_flags & PASSTABLE))
return
user.Move_Pulled(table)
if (user.pulling.loc == table.loc)
user.visible_message(span_notice("[user] places [user.pulling] onto [table]."),
span_notice("You place [user.pulling] onto [table]."))
user.stop_pulling()
return COMPONENT_CANCEL_ATTACK_CHAIN
var/mob/living/pushed_mob = user.pulling
if (pushed_mob.buckled)
if (pushed_mob.buckled == table)
//Already buckled to the table, you probably meant to unbuckle them
return
to_chat(user, span_warning("[pushed_mob] is buckled to [pushed_mob.buckled]!"))
return COMPONENT_CANCEL_ATTACK_CHAIN
INVOKE_ASYNC(src, PROC_REF(perform_table_smash), table, user)
return COMPONENT_CANCEL_ATTACK_CHAIN
/// We have a mob being pressed onto the table, but how strongly?
/datum/component/table_smash/proc/perform_table_smash(obj/table, mob/living/user)
var/mob/living/pushed_mob = user.pulling
if (user.combat_mode)
switch(user.grab_state)
if (GRAB_PASSIVE)
to_chat(user, span_warning("You need a better grip to do that!"))
return
if (GRAB_AGGRESSIVE)
if (gentle_push)
tableplace(user, pushed_mob)
else
tablepush(user, pushed_mob)
if (GRAB_NECK to GRAB_KILL)
tablelimbsmash(user, pushed_mob)
else
pushed_mob.visible_message(span_notice("[user] begins to place [pushed_mob] onto [table]..."), \
span_userdanger("[user] begins to place [pushed_mob] onto [table]..."))
if (do_after(user, 3.5 SECONDS, target = pushed_mob))
tableplace(user, pushed_mob)
else
return
user.stop_pulling()
/// Called when someone clicks on our surface with an item
/datum/component/table_smash/proc/on_item_interaction(obj/table, mob/living/user, obj/item/item, modifiers)
SIGNAL_HANDLER
if (!istype(item, /obj/item/riding_offhand))
return NONE
var/obj/item/riding_offhand/riding_item = item
var/mob/living/carried_mob = riding_item.rider
if (carried_mob == user) //Piggyback user.
return NONE
INVOKE_ASYNC(src, PROC_REF(riding_offhand_act), user, item)
return ITEM_INTERACT_BLOCKING
/// Called when someone clicks on our surface using a fireman's carry
/datum/component/table_smash/proc/riding_offhand_act(mob/living/user, obj/item/riding_offhand/riding_item)
var/mob/living/carried_mob = riding_item.rider
if (user.combat_mode)
user.unbuckle_mob(carried_mob)
tablelimbsmash(user, carried_mob)
return ITEM_INTERACT_SUCCESS
var/tableplace_delay = 3.5 SECONDS
var/skills_space = ""
if (HAS_TRAIT(user, TRAIT_QUICKER_CARRY))
tableplace_delay = 2 SECONDS
skills_space = " expertly"
else if (HAS_TRAIT(user, TRAIT_QUICK_CARRY))
tableplace_delay = 2.75 SECONDS
skills_space = " quickly"
var/obj/item/organ/cyberimp/chest/spine/potential_spine = user.get_organ_slot(ORGAN_SLOT_SPINE)
if (istype(potential_spine))
tableplace_delay *= potential_spine.athletics_boost_multiplier
carried_mob.visible_message(span_notice("[user] begins to[skills_space] place [carried_mob] onto [parent]..."),
span_userdanger("[user] begins to[skills_space] place [carried_mob] onto [parent]..."))
if (!do_after(user, tableplace_delay, target = carried_mob))
return ITEM_INTERACT_BLOCKING
user.unbuckle_mob(carried_mob)
tableplace(user, carried_mob)
return ITEM_INTERACT_SUCCESS
/// Gently place the mob onto the table
/datum/component/table_smash/proc/tableplace(mob/living/user, mob/living/pushed_mob)
var/obj/table = parent
pushed_mob.forceMove(table.loc)
pushed_mob.set_resting(TRUE, TRUE)
pushed_mob.visible_message(span_notice("[user] places [pushed_mob] onto [parent]."), \
span_notice("[user] places [pushed_mob] onto [parent]."))
log_combat(user, pushed_mob, "places", null, "onto [parent]")
/// Aggressively smash the mob onto the table
/datum/component/table_smash/proc/tablepush(mob/living/user, mob/living/pushed_mob)
if (HAS_TRAIT(user, TRAIT_PACIFISM))
to_chat(user, span_danger("Throwing [pushed_mob] onto the table might hurt them!"))
return
var/passtable_key = REF(user)
passtable_on(pushed_mob, passtable_key)
for (var/obj/obj in user.loc.contents)
if (!obj.CanAllowThrough(pushed_mob))
return
var/obj/table = parent
pushed_mob.Move(table.loc)
passtable_off(pushed_mob, passtable_key)
if (pushed_mob.loc != table.loc) //Something prevented the tabling
return
pushed_mob.Knockdown(3 SECONDS)
pushed_mob.apply_damage(10, BRUTE)
pushed_mob.apply_damage(40, STAMINA)
playsound(pushed_mob, 'sound/effects/tableslam.ogg', 90, TRUE)
pushed_mob.visible_message(span_danger("[user] slams [pushed_mob] onto \the [parent]!"), \
span_userdanger("[user] slams you onto \the [parent]!"))
log_combat(user, pushed_mob, "tabled", null, "onto [parent]")
pushed_mob.add_mood_event("table", /datum/mood_event/table)
SEND_SIGNAL(user, COMSIG_LIVING_TABLE_SLAMMING, pushed_mob, parent)
after_smash?.Invoke(pushed_mob)
/// Even more aggressively smash a single part of a mob onto the table
/datum/component/table_smash/proc/tablelimbsmash(mob/living/user, mob/living/pushed_mob)
var/obj/table = parent
pushed_mob.Knockdown(3 SECONDS)
var/obj/item/bodypart/banged_limb = pushed_mob.get_bodypart(user.zone_selected) || pushed_mob.get_bodypart(BODY_ZONE_HEAD)
var/extra_wound = 0
if (HAS_TRAIT(user, TRAIT_HULK))
extra_wound = 20
pushed_mob.apply_damage(30, BRUTE, banged_limb, wound_bonus = extra_wound)
pushed_mob.apply_damage(60, STAMINA)
playsound(pushed_mob, 'sound/effects/bang.ogg', 90, TRUE)
pushed_mob.visible_message(span_danger("[user] smashes [pushed_mob]'s [banged_limb.plaintext_zone] against \the [parent]!"),
span_userdanger("[user] smashes your [banged_limb.plaintext_zone] against \the [parent]"))
log_combat(user, pushed_mob, "head slammed", null, "against [parent]")
pushed_mob.add_mood_event("table", /datum/mood_event/table_limbsmash, banged_limb)
table.take_damage(50)
SEND_SIGNAL(user, COMSIG_LIVING_TABLE_LIMB_SLAMMING, pushed_mob, parent)
after_smash?.Invoke(pushed_mob)
/// Called when someone is shoved into our tile
/datum/component/table_smash/proc/on_pushed_into(turf/source, mob/living/shover, mob/living/target, shove_flags, obj/item/weapon)
SIGNAL_HANDLER
if((shove_flags & SHOVE_KNOCKDOWN_BLOCKED) || !(shove_flags & SHOVE_BLOCKED))
return
target.Knockdown(SHOVE_KNOCKDOWN_TABLE, daze_amount = 3 SECONDS)
target.visible_message(span_danger("[shover.name] shoves [target.name] onto \the [parent]!"),
span_userdanger("You're shoved onto \the [parent] by [shover.name]!"), span_hear("You hear aggressive shuffling followed by a loud thud!"), COMBAT_MESSAGE_RANGE, shover)
to_chat(shover, span_danger("You shove [target.name] onto \the [parent]!"))
target.throw_at(parent, 1, 1, null, FALSE) //1 speed throws with no spin are basically just forcemoves with a hard collision check
log_combat(shover, target, "shoved", "onto [parent] (table)[weapon ? " with [weapon]" : ""]")
after_smash?.Invoke(target)
return COMSIG_LIVING_SHOVE_HANDLED
+4
View File
@@ -48,6 +48,10 @@
ADD_TRAIT(user, TRAIT_UNKNOWN_APPEARANCE, REF(src))
current_slot = slot
source.pixel_x = source.base_pixel_x
source.pixel_y = source.base_pixel_y
source.pixel_w = source.base_pixel_w
source.pixel_z = source.base_pixel_z
on_icon_update(source)
+1 -1
View File
@@ -50,7 +50,7 @@
if(!isliving(parent))
return FALSE // Figure this out when we actually need it
var/mob/living/living_parent = parent
return living_parent.faction.Find(REF(potential_friend))
return living_parent.has_ally(potential_friend)
///Ran once taming succeeds
/datum/component/tameable/proc/on_tame(atom/source, mob/living/tamer, obj/item/food, inform_tamer = FALSE)
@@ -91,7 +91,7 @@
else
end_windup()
/datum/component/throwbonus_on_windup/proc/on_hands_swap(mob/living/source)
/datum/component/throwbonus_on_windup/proc/on_hands_swap(mob/living/source, obj/item/swapped_to, obj/item/swapped_from)
SIGNAL_HANDLER
if(source.get_active_held_item() != parent)
+3 -3
View File
@@ -37,11 +37,11 @@
var/udder_filled_percentage = PERCENT(udder.reagents.total_volume / udder.reagents.maximum_volume)
switch(udder_filled_percentage)
if(0 to 10)
examine_list += span_notice("[parent]'s [udder] is dry.")
examine_list += span_notice("[parent]'s [udder.name] is dry.")
if(11 to 99)
examine_list += span_notice("[parent]'s [udder] can be milked if you have something to contain it.")
examine_list += span_notice("[parent]'s [udder.name] can be milked if you have something to contain it.")
if(100)
examine_list += span_notice("[parent]'s [udder] is round and full, and can be milked if you have something to contain it.")
examine_list += span_notice("[parent]'s [udder.name] is round and full, and can be milked if you have something to contain it.")
///signal called on parent being attacked with an item
+34 -5
View File
@@ -17,10 +17,20 @@
/// Overlay added when you are in danger
var/state_danger
/// If null, does nothing
/// If set to a ztrait, like ZTRAIT_MINING, we check zlevels with that trait for weather towers
/// Having no active radar towers on those levels will reduce the accuracy of the warnings
var/radar_z_trait
/// Modifier to the accuracy of the weather warning times
/// Changes every unique storm event
VAR_FINAL/accuracy_mod = 0
/datum/component/weather_announcer/Initialize(
state_normal,
state_warning,
state_danger,
radar_z_trait,
)
. = ..()
if (!ismovable(parent))
@@ -34,6 +44,7 @@
src.state_normal = state_normal
src.state_warning = state_warning
src.state_danger = state_danger
src.radar_z_trait = radar_z_trait
var/atom/speaker = parent
speaker.update_appearance(UPDATE_ICON)
update_light_color()
@@ -70,6 +81,14 @@
var/atom/speaker = parent
speaker.update_appearance(UPDATE_ICON)
/datum/component/weather_announcer/proc/check_accuracy()
for(var/z_level in SSmapping.levels_by_trait(radar_z_trait))
for(var/obj/machinery/power/weather_tower/radar as anything in GLOB.weather_towers["[z_level]"])
if(radar.active)
return TRUE
return FALSE
/datum/component/weather_announcer/process(seconds_per_tick)
if (!enabled)
return
@@ -84,7 +103,7 @@
var/obj/machinery/announcement_system/aas = get_announcement_system(/datum/aas_config_entry/weather, speaker, list(RADIO_CHANNEL_SUPPLY))
// Active AAS will override default announcement lines
if (aas)
msg = aas.compile_config_message(/datum/aas_config_entry/weather, list(), !is_weather_dangerous ? 4 : warning_level + 1)
msg = aas.compile_config_message(/datum/aas_config_entry/weather, list(), is_weather_dangerous ? warning_level + 1 : 4)
// Stop toggling on radios for it, please!
aas.broadcast(msg, list(RADIO_CHANNEL_SUPPLY))
// Still say it, because you can be not on our level
@@ -117,17 +136,17 @@
return "Error in meteorological calculation. Please report this deviation to a trained programmer."
/datum/component/weather_announcer/proc/time_till_storm()
var/list/mining_z_levels = SSmapping.levels_by_trait(ZTRAIT_MINING)
var/list/mining_z_levels = SSmapping.levels_by_trait(radar_z_trait)
if(!length(mining_z_levels))
return // No problems if there are no mining z levels
for(var/datum/weather/check_weather as anything in SSweather.processing)
if(!(check_weather.weather_flags & WEATHER_BAROMETER) || check_weather.stage == WIND_DOWN_STAGE || check_weather.stage == END_STAGE)
continue
for (var/mining_level in mining_z_levels)
if(mining_level in check_weather.impacted_z_levels)
warning_level = WEATHER_ALERT_IMMINENT_OR_ACTIVE
accuracy_mod = 0 // reset for next storm
return 0
var/time_until_next = INFINITY
@@ -135,6 +154,13 @@
var/next_time = timeleft(SSweather.next_hit_by_zlevel["[mining_level ]"]) || INFINITY
if (next_time && next_time < time_until_next)
time_until_next = next_time
if(!check_accuracy())
if(!accuracy_mod && radar_z_trait)
accuracy_mod = rand(-9, 9) * 5 SECONDS
time_until_next = max(10 SECONDS, time_until_next + accuracy_mod)
return time_until_next
/// Polls existing weather for what kind of warnings we should be displaying.
@@ -156,7 +182,7 @@
for(var/datum/weather/check_weather as anything in SSweather.processing)
if(!(check_weather.weather_flags & WEATHER_BAROMETER) || check_weather.stage == WIND_DOWN_STAGE || check_weather.stage == END_STAGE)
continue
var/list/mining_z_levels = SSmapping.levels_by_trait(ZTRAIT_MINING)
var/list/mining_z_levels = SSmapping.levels_by_trait(radar_z_trait)
for(var/mining_level in mining_z_levels)
if(mining_level in check_weather.impacted_z_levels)
is_weather_dangerous = (check_weather.weather_flags & FUNCTIONAL_WEATHER)
@@ -167,10 +193,13 @@
if(isnull(time_until_next))
return
if (time_until_next == 0)
examine_texts += span_warning ("A storm is currently active, please seek shelter.")
examine_texts += span_warning("A storm is currently active, please seek shelter.")
else
examine_texts += span_notice("The next storm is inbound in [DisplayTimeText(time_until_next)].")
if(!check_accuracy())
examine_texts += span_smallnoticeital("Due to insufficient radar coverage, the timing of this forecast may be inaccurate.")
/datum/component/weather_announcer/RegisterWithParent()
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))