From b09d40013d4a386a798e4b61dcbfdc875c2a8561 Mon Sep 17 00:00:00 2001 From: CHOMPStation2StaffMirrorBot <94713762+CHOMPStation2StaffMirrorBot@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:53:56 -0700 Subject: [PATCH] [MIRROR] Refactors vending machine tilting (#12833) Co-authored-by: Guti <32563288+TheCaramelion@users.noreply.github.com> --- code/__defines/crushing.dm | 15 ++ code/_helpers/unsorted.dm | 27 ++- code/datums/components/traits/unlucky.dm | 2 +- code/modules/economy/vending.dm | 41 +--- code/modules/economy/vending_tilting.dm | 273 +++++++++++++++++++++++ vorestation.dme | 2 + 6 files changed, 321 insertions(+), 39 deletions(-) create mode 100644 code/__defines/crushing.dm create mode 100644 code/modules/economy/vending_tilting.dm diff --git a/code/__defines/crushing.dm b/code/__defines/crushing.dm new file mode 100644 index 0000000000..62705e0e3b --- /dev/null +++ b/code/__defines/crushing.dm @@ -0,0 +1,15 @@ +// from _vending.dm + +/// Set if the tipped object successfully crushed a subtype of /mob/living. +#define SUCCESSFULLY_CRUSHED_MOB (1<<0) +/// Set if the tipped object successfully crushed a subtype of /atom. +#define SUCCESSFULLY_CRUSHED_ATOM (1<<1) +/// Set if the tipped object successfully actually fell over, which can fail if it couldn't enter the turf it tried to fall into. +#define SUCCESSFULLY_FELL_OVER (1<<2) + +#define CRUSH_CRIT_SHATTER_LEGS "crush_crit_shatter_legs" +#define CRUSH_CRIT_PARAPLEGIC "crush_crit_paraplegic" +#define CRUSH_CRIT_SQUISH_LIMB "crush_crit_pin" +#define CRUSH_CRIT_HEADGIB "crush_crit_headgib" +#define VENDOR_CRUSH_CRIT_PIN "vendor_crush_crit_pin" +#define VENDOR_CRUSH_CRIT_GLASSCANDY "vendor_crush_crit_glasscandy" diff --git a/code/_helpers/unsorted.dm b/code/_helpers/unsorted.dm index fcf4b2dc7f..981534736d 100644 --- a/code/_helpers/unsorted.dm +++ b/code/_helpers/unsorted.dm @@ -599,13 +599,26 @@ Turf and target are seperate in case you want to teleport some distance from a t return 1 -/proc/is_blocked_turf(turf/T) - var/cant_pass = 0 - if(T.density) cant_pass = 1 - for(var/atom/A in T) - if(A.density)//&&A.anchored - cant_pass = 1 - return cant_pass +/proc/is_blocked_turf(turf/T, exclude_mobs = FALSE, source_atom = null, list/ignore_atoms, type_list = FALSE) + if(T.density) + return TRUE + + for(var/atom/movable/movable_content as anything in T.contents) + if((movable_content == source_atom)) + continue + if(length(ignore_atoms)) + if(!type_list && (movable_content in ignore_atoms)) + continue + else if(type_list && is_type_in_list(movable_content, ignore_atoms)) + continue + + // If the thing is dense AND we're including mobs or the thing isn't a mob AND if there's a source atom and + // it cannot pass through the thing on the turf, we consider the turf blocked. + if(movable_content.density && (!exclude_mobs || !ismob(movable_content))) + if(source_atom && movable_content.CanPass(source_atom, get_dir(T, source_atom))) + continue + return TRUE + return FALSE /* //If you uncomment and use this I will assimilate you. -C.L. //Takes: Anything that could possibly have variables and a varname to check. diff --git a/code/datums/components/traits/unlucky.dm b/code/datums/components/traits/unlucky.dm index a057670d21..23e227b6bd 100644 --- a/code/datums/components/traits/unlucky.dm +++ b/code/datums/components/traits/unlucky.dm @@ -230,7 +230,7 @@ else if(darth_vendor.tilted || !darth_vendor.density) continue - darth_vendor.crush_under(living_guy) + darth_vendor.tilt(living_guy) consume_omen() return diff --git a/code/modules/economy/vending.dm b/code/modules/economy/vending.dm index 63b2a6f159..bd7f8fa5d1 100644 --- a/code/modules/economy/vending.dm +++ b/code/modules/economy/vending.dm @@ -78,8 +78,12 @@ var/req_log_access = ACCESS_CARGO //default access for checking logs is cargo var/has_logs = 0 //defaults to 0, set to anything else for vendor to have logs var/can_rotate = 1 //Defaults to yes, can be set to 0 for vendors without or with unwanted directionals. - var/tilted = FALSE + var/tilted = FALSE + var/tilted_rotation = 0 + var/tiltable = TRUE + var/squish_damage = 25 + var/crit_chance = 15 /obj/machinery/vending/Initialize(mapload) . = ..() @@ -359,10 +363,10 @@ GLOBAL_LIST_EMPTY(vending_products) if(shock(user, 100)) return - if(tilted) - balloon_alert(user, "setting upright") - if(do_after(user, 2 SECONDS, src)) - tilt(TRUE) + if(tilted && !user.buckled) + to_chat(user, span_notice("You begin righting [src].")) + if(do_after(user, 5 SECONDS, target = src)) + untilt(user) return if(user.a_intent == I_HURT && density) @@ -755,38 +759,13 @@ GLOBAL_LIST_EMPTY(vending_products) visible_message(span_warning("\The [src] launches \a [throw_item] at \the [target]!")) return 1 -/obj/machinery/vending/proc/crush_under(mob/living/unlucky) - if(!unlucky) - return - visible_message(span_warning("\The [src] tilts over, falling on [unlucky]!")) - forceMove(get_turf(unlucky)) - for(var/mob/living/mob in get_turf(unlucky)) - mob.take_overall_damage(30) - mob.Stun(2) - mob.Weaken(5) - tilt() - -/obj/machinery/vending/proc/tilt(upright = FALSE) - if(!upright) - var/matrix/M = matrix() - M.Turn(90) - animate(src, transform = M, time = 0.25 SECOND) - playsound(src, 'sound/effects/meteorimpact.ogg', 50, 1) - tilted = TRUE - anchored = FALSE - plane = ABOVE_MOB_PLANE - else - animate(src, transform = matrix(), time = 0.4 SECOND) - tilted = FALSE - plane = initial(plane) - /obj/machinery/vending/proc/punch_machine(mob/living/stupid_person) stupid_person.visible_message(span_danger("[stupid_person] kicks \the [src]!"), span_danger("You kick \the [src]")) playsound(src, 'sound/effects/clang2.ogg', 25, TRUE) animate_shake() if(prob(10)) - crush_under(stupid_person) + tilt(stupid_person) return if(prob(5)) diff --git a/code/modules/economy/vending_tilting.dm b/code/modules/economy/vending_tilting.dm new file mode 100644 index 0000000000..7e6b278d26 --- /dev/null +++ b/code/modules/economy/vending_tilting.dm @@ -0,0 +1,273 @@ +/** + * Can this atom be curshed by the vending machine + * Arguments + * + * * atom/atom_target - the atom we are checking for +*/ +/proc/check_atom_crushable(atom/atom_target) + var/static/list/vendor_uncrushable_objects = list( + /obj/structure/bed/chair, + /obj/machinery/conveyor, + /obj/structure/table + ) + + if(is_type_in_list(atom_target, vendor_uncrushable_objects)) + return FALSE + + if(atom_target.uses_integrity && !(atom_target.invisibility > SEE_INVISIBLE_LIVING)) + return TRUE + + return FALSE + +/** + * Causes src to fall onto [target], crushing everything on it (including itself) with [damage] + * and a small chance to do a spectacular effect per entity (if a chance above 0 is provided). + * + * Args: + * * turf/target: The turf to fall onto. Cannot be null. + * * damage: The raw numerical damage to do by default. + * * chance_to_crit: The percent chance of a critical hit occurring. Default: 0 + * * forced_crit_case: If given a value from crushing.dm, [target] and its contents will always be hit with that specific critical hit. Default: null + * * paralyze_time: The time, in deciseconds, a given mob/living will be paralyzed for if crushed. + * * crush_dir: The direction the crush is coming from. Default: dir of src to [target]. + * * damage_type: The type of damage to do. Default: BRUTE + * * damage_flag: The attack flag for armor purposes. Default: MELEE + * * rotation: The angle of which to rotate src's transform by on a successful tilt. Default: 90. + * + * Returns: A collection of bitflags defined in crushing.dm. Read that file's documentation for info. + */ +/atom/movable/proc/fall_and_crush(turf/target, damage, chance_to_crit = 0, forced_crit_case = null, paralyze_time, crush_dir = get_dir(get_turf(src), target), damage_type = BRUTE, damage_flag = "melee", rotation = 90) + + ASSERT(!isnull(target)) + + var/flags_to_return = NONE + + if(!is_blocked_turf(target, TRUE, src, list(src))) + for(var/atom/atom_target in (target.contents) + target) + if(isarea(atom_target)) + continue + + if(SEND_SIGNAL(atom_target, COMSIG_PRE_TILT_AND_CRUSH, src) & COMPONENT_IMMUNE_TO_TILT_AND_CRUSH) + continue + + var/crit_case = forced_crit_case + if(isnull(crit_case) && chance_to_crit > 0) + if(prob(chance_to_crit)) + crit_case = pick_weight(get_crit_crush_chances()) + var/crit_rebate_mult = 1 + + if(!isnull(crit_case)) + crit_rebate_mult = fall_and_crush_crit_rebate_table(crit_case) + apply_crit_crush(crit_case, atom_target) + + var/adjusted_damage = damage * crit_rebate_mult + var/crushed + if(isliving(atom_target)) + crushed = TRUE + var/mob/living/carbon/human/living_target = atom_target + var/was_alive = living_target.stat != DEAD + var/blocked = living_target.run_armor_check(attack_flag = damage_flag) + if(iscarbon(living_target)) + var/mob/living/carbon/carbon_target = living_target + if(prob(30)) + carbon_target.take_overall_damage(adjusted_damage) + else + var/brute = (damage_type == BRUTE ? damage : 0) * 0.5 + var/burn = (damage_type == BURN ? damage : 0) * 0.5 + carbon_target.apply_damage(brute, BRUTE, pick(BP_ALL)) + carbon_target.apply_damage(burn, BURN, pick(BP_ALL)) + else + living_target.apply_damage(adjusted_damage, damage_type, blocked = blocked) + + living_target.Stun(paralyze_time) + living_target.Weaken(paralyze_time*2) + living_target.emote("scream") + playsound(src, 'sound/effects/meteorimpact.ogg', 50, 1) + post_crush_living(living_target, was_alive) + flags_to_return |= (SUCCESSFULLY_CRUSHED_MOB|SUCCESSFULLY_CRUSHED_ATOM) + + else if(check_atom_crushable(atom_target)) + atom_target.take_damage(adjusted_damage, damage_type, damage_flag) + crushed = TRUE + flags_to_return |= SUCCESSFULLY_CRUSHED_ATOM + + if(crushed) + atom_target.visible_message(span_danger("[atom_target] is crushed by [src]!"), span_userdanger("You are crushed by [src]!")) + SEND_SIGNAL(atom_target, COMSIG_POST_TILT_AND_CRUSH, src) + + var/matrix/to_turn = turn(transform, rotation) + animate(src, transform = to_turn, 0.2 SECONDS) + playsound(src, 'sound/effects/bang.ogg', 40) + + visible_message(span_danger("[src] tips over, slamming hard onto [target]!")) + flags_to_return |= SUCCESSFULLY_FELL_OVER + post_tilt() + else + visible_message(span_danger("[src] rebounts comically as it fails to slam onto [target]!")) + + forceMove(target, crush_dir) + return flags_to_return + +/** + * Returns a assoc list of (critcase -> num), where critcase is a critical define in crushing.dm and num is a weight. + * Use with pickweight to acquire a random critcase. + */ +/atom/movable/proc/get_crit_crush_chances() + RETURN_TYPE(/list) + + return list( + CRUSH_CRIT_SHATTER_LEGS = 100, + /* + CRUSH_CRIT_PARAPLEGIC = 80, + CRUSH_CRIT_HEADGIB = 20, + */ + CRUSH_CRIT_SQUISH_LIMB = 100 + ) + +/** + * Exists for the purposes of custom behavior. + * Called directly after [crushed] is crushed. + * + * Args: + * * mob/living/crushed: The mob that was crushed. + * * was_alive: Boolean. True if the mob was alive before the crushing. + */ +/atom/movable/proc/post_crush_living(mob/living/crushed, was_alive) + return + +/** + * Exists for the purposes of custom behavior. + * Called directly after src actually rotates and falls over. + */ +/atom/movable/proc/post_tilt() + return + +/** + * Should be where critcase effects are actually implemented. Use this to apply critcases. + * Args: + * * crit_case: The chosen critcase, defined in crushing.dm. + * * atom/atom_target: The target to apply the critical hit to. Cannot be null. Can be anything except /area. + * + * Returns: + * TRUE if a crit case is successfully applied, FALSE otherwise. + */ +/atom/movable/proc/apply_crit_crush(crit_case, atom/atom_target) + switch(crit_case) + if(CRUSH_CRIT_SHATTER_LEGS) + if(!ishuman(atom_target)) + return FALSE + var/mob/living/carbon/human/human_target = atom_target + var/obj/item/organ/external/left_leg = human_target.get_organ(BP_L_LEG) + if(left_leg) + left_leg.fracture() + var/obj/item/organ/external/right_leg = human_target.get_organ(BP_R_LEG) + if(right_leg) + right_leg.fracture() + if(left_leg || right_leg) + human_target.visible_message(span_danger("[human_target]'s legs shatter with a sickening crunch!"), span_userdanger("Your legs shatter with a sickening crunch!")) + return TRUE + if(CRUSH_CRIT_SQUISH_LIMB) + if(!ishuman(atom_target)) + return FALSE + var/mob/living/carbon/human/human_target = atom_target + for(var/obj/item/organ/external/squish_part in human_target.organs) + squish_part.take_damage(15) + human_target.visible_message(span_danger("[human_target]'s body is maimed underneath the mass of [src]!"), span_userdanger("Your body is maimed underneath the mass of [src]!")) + return TRUE + + return FALSE + +/** + * Tilts ontop of the atom supplied, if crit is true some extra shit can happen. See [fall_and_crush] for return values. + * Arguments: + * fatty - atom to tilt the vendor onto + * local_crit_chance - percent chance of a critical hit + * forced_crit - specific critical hit case to use, if any + * range - the range of the machine when thrown if not adjacent +*/ +/obj/machinery/vending/proc/tilt(atom/fatty, local_crit_chance = crit_chance, forced_crit, range = 1) + if(QDELETED(src)) + return + + . = NONE + + var/picked_rotation = pick(90, 270) + if(Adjacent(fatty)) + . = fall_and_crush(get_turf(fatty), squish_damage, local_crit_chance, forced_crit, 4, rotation = picked_rotation) + + if(. & SUCCESSFULLY_FELL_OVER) + visible_message(span_danger("[src] tips over!")) + tilted = TRUE + tilted_rotation = picked_rotation + anchored = FALSE + plane = ABOVE_MOB_PLANE + + if(get_turf(fatty) != get_turf(src)) + throw_at(get_turf(fatty), range, 1, FALSE) + +/** + * Allows damage to be reduced on certain crit cases. + * Args: + * * crit_case: The critical case chosen. + */ +/atom/movable/proc/fall_and_crush_crit_rebate_table(crit_case) + ASSERT(!isnull(crit_case)) + + switch(crit_case) + if (CRUSH_CRIT_SHATTER_LEGS) + return 0.2 + else + return 1 + +/obj/machinery/vending/fall_and_crush_crit_rebate_table(crit_case) + return crit_case == VENDOR_CRUSH_CRIT_GLASSCANDY ? 0.33 : ..() + +/obj/machinery/vending/get_crit_crush_chances() + return list( + VENDOR_CRUSH_CRIT_GLASSCANDY = 100, + VENDOR_CRUSH_CRIT_PIN = 100 + ) + +/obj/machinery/vending/apply_crit_crush(crit_case, atom_target) + . = ..() + if (.) + return TRUE + + switch (crit_case) + if(VENDOR_CRUSH_CRIT_GLASSCANDY) + if (!ishuman(atom_target)) + return FALSE + var/mob/living/carbon/human/human_target = atom_target + for(var/i in 1 to 7) + var/obj/item/material/shard/shard = new /obj/item/material/shard(get_turf(human_target)) + human_target.embed(shard, def_zone = pick(BP_ALL)) + if(VENDOR_CRUSH_CRIT_PIN) + if(!isliving(atom_target)) + return FALSE + var/mob/living/living_target = atom_target + forceMove(get_turf(living_target)) + buckle_mob(living_target, TRUE) + living_target.visible_message(span_danger("[living_target] is pinned underneath [src]!"), span_userdanger("You are pinned down by [src]!")) + living_target.Weaken(20) + return TRUE + + return FALSE + +/** + * Rights the vendor up, unpinning mobs under it, if any. + * Arguments: + * user - mob that has untilted the vendor + */ +/obj/machinery/vending/proc/untilt(mob/user) + if(user) + user.visible_message(span_notice("[user] rights [src]."), \ + span_notice("You right [src].")) + + unbuckle_all_mobs(TRUE) + + tilted = FALSE + plane = initial(layer) + + var/matrix/to_turn = turn(transform, -tilted_rotation) + animate(src, transform = to_turn, 0.2 SECONDS) + tilted_rotation = 0 diff --git a/vorestation.dme b/vorestation.dme index e4529b69e8..07d080483b 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -86,6 +86,7 @@ #include "code\__defines\construction_datum.dm" #include "code\__defines\cooldowns.dm" #include "code\__defines\crafting.dm" +#include "code\__defines\crushing.dm" #include "code\__defines\damage_organs.dm" #include "code\__defines\database.dm" #include "code\__defines\departmentgoals.dm" @@ -2915,6 +2916,7 @@ #include "code\modules\economy\vending_machines.dm" #include "code\modules\economy\vending_machines_vr.dm" #include "code\modules\economy\vending_refills.dm" +#include "code\modules\economy\vending_tilting.dm" #include "code\modules\emotes\custom_emote.dm" #include "code\modules\emotes\emote_define.dm" #include "code\modules\emotes\emote_mob.dm"