From b33d6aa08069e801688ae0693d35f069844486b3 Mon Sep 17 00:00:00 2001 From: Luc <89928798+lewcc@users.noreply.github.com> Date: Sat, 4 Mar 2023 03:22:07 -0500 Subject: [PATCH] Tipping vending machines for freebies and/or crushes (#20060) * Initial commit * Add RNG tip behavior, including free items. * Add some additional checks * Small cleanups * Add squishing, drop normal damage to 40 * Haha, unless? (this is how it is on tg) * Datumizes vendor tips, part one * Datumizing, part 2 * Remove unused * Remove old "key" var * Fix .proc call * double dwarf damage * Update code/game/machinery/vendors/vending.dm Thanks, sean Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com> * Add a warning hit * squish component review * update visible message * remove extra defines * charlie review * Add explosion tilts, some slight cleanups * Review changes - Tweaks self knockover damage slightly to not be the same as crit damage - farie's reviews * Unindent, clean up crit logic * Refactor some code out of main vendor proc also re-adds the head-popping crit lol * making linter happy :) * Removes global list in favor of static one --------- Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com> --- code/datums/components/squish.dm | 43 +++ .../game/machinery/vendors/generic_vendors.dm | 2 + code/game/machinery/vendors/vending.dm | 292 +++++++++++++++++- code/game/machinery/vendors/vendor_crits.dm | 119 +++++++ paradise.dme | 2 + 5 files changed, 457 insertions(+), 1 deletion(-) create mode 100644 code/datums/components/squish.dm create mode 100644 code/game/machinery/vendors/vendor_crits.dm diff --git a/code/datums/components/squish.dm b/code/datums/components/squish.dm new file mode 100644 index 00000000000..232c584dae0 --- /dev/null +++ b/code/datums/components/squish.dm @@ -0,0 +1,43 @@ +#define SHORT_SCALE (5/7) +#define TALL_SCALE (7/5) + +/** + * squish.dm + * + * It's an element that squishes things. After the duration passes, it reverses the transformation it squished with, taking into account if they are a different orientation than they started (read: rotationally-fluid) + * + * Normal squishes apply vertically, as if the target is being squished from above, but you can set reverse to TRUE if you want to squish them from the sides, like if they pancake into a wall from the East or West +*/ +/datum/element/squish + element_flags = ELEMENT_DETACH + +/datum/element/squish/Attach(datum/target, duration = 20 SECONDS, reverse = FALSE) + . = ..() + if(!iscarbon(target)) + return ELEMENT_INCOMPATIBLE + + var/mob/living/carbon/C = target + var/was_lying = C.body_position == LYING_DOWN + addtimer(CALLBACK(src, PROC_REF(Detach), C, was_lying, reverse), duration) + + if(reverse) + C.transform = C.transform.Scale(SHORT_SCALE, TALL_SCALE) + else + C.transform = C.transform.Scale(TALL_SCALE, SHORT_SCALE) + +/datum/element/squish/Detach(mob/living/carbon/C, was_lying, reverse) + . = ..() + if(!istype(C)) + return + var/is_lying = C.body_position == LYING_DOWN + + if(reverse) + is_lying = !is_lying + + if(was_lying == is_lying) + C.transform = C.transform.Scale(SHORT_SCALE, TALL_SCALE) + else + C.transform = C.transform.Scale(TALL_SCALE, SHORT_SCALE) + +#undef SHORT_SCALE +#undef TALL_SCALE diff --git a/code/game/machinery/vendors/generic_vendors.dm b/code/game/machinery/vendors/generic_vendors.dm index 834a7f40c64..66c1e9bb3d5 100644 --- a/code/game/machinery/vendors/generic_vendors.dm +++ b/code/game/machinery/vendors/generic_vendors.dm @@ -373,6 +373,7 @@ contraband = list(/obj/item/reagent_containers/glass/bottle/wizarditis = 1) armor = list(melee = 100, bullet = 100, laser = 100, energy = 100, bomb = 0, bio = 0, rad = 0, fire = 100, acid = 50) resistance_flags = FIRE_PROOF + tiltable = FALSE // don't let a poor wizard screw themselves /obj/machinery/economy/vending/autodrobe name = "\improper AutoDrobe" @@ -859,6 +860,7 @@ icon_panel = "wallmed" icon_broken = "wallmed" density = FALSE //It is wall-mounted, and thus, not dense. --Superxpdude + tiltable = FALSE products = list(/obj/item/stack/medical/bruise_pack = 2, /obj/item/stack/medical/ointment = 2, /obj/item/reagent_containers/hypospray/autoinjector = 4, /obj/item/healthanalyzer = 1) contraband = list(/obj/item/reagent_containers/syringe/charcoal = 4, /obj/item/reagent_containers/syringe/antiviral = 4, /obj/item/reagent_containers/food/pill/tox = 1) armor = list(melee = 50, bullet = 20, laser = 20, energy = 20, bomb = 0, bio = 0, rad = 0, fire = 100, acid = 70) diff --git a/code/game/machinery/vendors/vending.dm b/code/game/machinery/vendors/vending.dm index 39c2c4f3124..c913af6400b 100644 --- a/code/game/machinery/vendors/vending.dm +++ b/code/game/machinery/vendors/vending.dm @@ -1,3 +1,11 @@ +// Using these to decide how a vendor crush should be handled after crushing a carbon. +/// Just jump ship, the crit handled everything it needs to. +#define VENDOR_CRUSH_HANDLED 0 +/// Throw the vendor at the target's tile. +#define VENDOR_THROW_AT_TARGET 1 +/// Don't actually throw at the target, just tip it in place. +#define VENDOR_TIP_IN_PLACE 2 + /** * Datum used to hold information about a product in a vending machine */ @@ -114,6 +122,33 @@ ///the money account that is tethered to this vendor var/datum/money_account/vendor_account + /// If this vending machine can be tipped or not + var/tiltable = TRUE + /// If this vendor is currently tipped + var/tilted = FALSE + /// Amount of damage to deal when tipped + var/squish_damage = 40 // yowch + /// Factor of extra damage to deal when triggering a crit + var/crit_damage_factor = 2 + /// Factor of extra damage to deal when you knock it over onto yourself + var/self_knockover_factor = 1.5 + /// All possible crits that could be applied. We only need to build this up once + var/static/list/all_possible_crits = list() + /// Possible crit effects from this vending machine tipping. + var/list/possible_crits = list( + /datum/vendor_crit/pop_head, + /datum/vendor_crit/embed, + /datum/vendor_crit/pin, + /datum/vendor_crit/shatter, + /datum/vendor_crit/lucky + ) + /// number of shards to apply when a crit embeds + var/num_shards = 7 + /// Last time the machine was punched + var/last_hit_time = 0 + /// How long to wait before resetting the warning cooldown + var/hit_warning_cooldown_length = 10 SECONDS + /obj/machinery/economy/vending/Initialize(mapload) . = ..() var/build_inv = FALSE @@ -145,6 +180,11 @@ if(account_database) vendor_account = account_database.vendor_account + + if(!length(all_possible_crits)) + for(var/typepath in subtypesof(/datum/vendor_crit)) + all_possible_crits[typepath] = new typepath() + reconnect_database() power_change() @@ -154,6 +194,13 @@ QDEL_NULL(inserted_item) return ..() +/obj/machinery/economy/vending/examine(mob/user) + . = ..() + if(tilted) + . += "It's been tipped over and won't be usable unless it's righted." + if(Adjacent(user)) + . += "You can Alt-Click it to right it." + /obj/machinery/economy/vending/RefreshParts() //Better would be to make constructable child if(!component_parts) return @@ -300,7 +347,19 @@ else ..() +/obj/machinery/economy/vending/AltClick(mob/user) + if(!tilted || !Adjacent(user) || HAS_TRAIT(user, TRAIT_HANDS_BLOCKED)) + return + + untilt(user) + /obj/machinery/economy/vending/attackby(obj/item/I, mob/user, params) + if(tilted) + if(user.a_intent == INTENT_HELP) + to_chat(user, "[src] is tipped over and non-functional! You'll need to right it first.") + return + return ..() + if(isspacecash(I)) insert_cash(I, user) return @@ -326,24 +385,72 @@ if(item_slot_check(user, I)) insert_item(user, I) return - return ..() + . = ..() + if(tiltable && !tilted && I.force) + if(resistance_flags & INDESTRUCTIBLE) + // no goodies, but also no tilts + return + var/should_warn = world.time > last_hit_time + hit_warning_cooldown_length + last_hit_time = world.time + if(should_warn) + visible_message("[src] seems to sway a bit!") + to_chat(user, "You might want to think twice about doing that again, [src] looks like it could come crashing down!") + return + + switch(rand(1, 100)) + if(1 to 5) + freebie(user, 3) + if(6 to 15) + freebie(user, 2) + if(16 to 25) + freebie(user, 1) + if(26 to 75) + return + if(76 to 90) + tilt(user) + if(91 to 100) + tilt(user, crit = TRUE) +/obj/machinery/economy/vending/proc/freebie(mob/user, num_freebies) + visible_message("[num_freebies] free goodie\s tumble[num_freebies > 1 ? "" : "s"] out of [src]!") + + for(var/i in 1 to num_freebies) + for(var/datum/data/vending_product/R in shuffle(product_records)) + + if(R.amount <= 0) + continue + + var/dump_path = R.product_path + if(!dump_path) + continue + new dump_path(get_turf(src)) + R.amount-- + break /obj/machinery/economy/vending/crowbar_act(mob/user, obj/item/I) if(!component_parts) return . = TRUE + if(tilted) + to_chat("You'll need to right it first!") + return default_deconstruction_crowbar(user, I) /obj/machinery/economy/vending/multitool_act(mob/user, obj/item/I) . = TRUE + if(tilted) + to_chat("You'll need to right it first!") + return if(!I.use_tool(src, user, 0, volume = I.tool_volume)) return wires.Interact(user) /obj/machinery/economy/vending/screwdriver_act(mob/user, obj/item/I) . = TRUE + if(tilted) + to_chat("You'll need to right it first!") + return if(!I.use_tool(src, user, 0, volume = I.tool_volume)) return if(!anchored) @@ -360,11 +467,17 @@ /obj/machinery/economy/vending/wirecutter_act(mob/user, obj/item/I) . = TRUE + if(tilted) + to_chat("You'll need to right it first!") + return if(I.use_tool(src, user, 0, volume = 0)) wires.Interact(user) /obj/machinery/economy/vending/wrench_act(mob/user, obj/item/I) . = TRUE + if(tilted) + to_chat("The fastening bolts aren't on the ground, you'll need to right it first!") + return if(!I.use_tool(src, user, 0, volume = 0)) return default_unfasten_wrench(user, I, time = 6 SECONDS) @@ -442,6 +555,22 @@ emagged = TRUE to_chat(user, "You short out the product lock on [src]") +/obj/machinery/economy/vending/ex_act(severity) + . = ..() + if(QDELETED(src) || (resistance_flags & INDESTRUCTIBLE) || tilted || !tiltable) + return + var/tilt_prob = 0 + switch(severity) + if(EXPLODE_LIGHT) + tilt_prob = 10 + if(EXPLODE_HEAVY) + tilt_prob = 50 + if(EXPLODE_DEVASTATE) + tilt_prob = 80 + + if(prob(tilt_prob)) + tilt() + /obj/machinery/economy/vending/attack_ai(mob/user) return attack_hand(user) @@ -452,6 +581,10 @@ if(stat & (BROKEN|NOPOWER)) return + if(tilted) + to_chat(user, "[src] is tipped over and non-functional! You'll need to right it first.") + return + if(seconds_electrified != 0 && shock(user, 100)) return @@ -799,6 +932,160 @@ /obj/machinery/economy/vending/onTransitZ() return + +/** + * Select a random valid crit. + */ +/obj/machinery/economy/vending/proc/choose_crit(mob/living/carbon/victim) + if(!length(possible_crits)) + return + for(var/crit_path in shuffle(possible_crits)) + var/datum/vendor_crit/C = all_possible_crits[crit_path] + if(C.is_valid(src, victim)) + return C + +/obj/machinery/economy/vending/proc/handle_squish_carbon(mob/living/carbon/victim, damage_to_deal, crit, from_combat) + + // Damage points to "refund", if a crit already beats the shit out of you we can shelve some of the extra damage. + var/crit_rebate = 0 + + var/should_throw_at_target = TRUE + + if(HAS_TRAIT(victim, TRAIT_DWARF)) + // also double damage if you're short + damage_to_deal *= 2 + + var/datum/vendor_crit/critical_attack = choose_crit(victim) + if(!from_combat && crit && critical_attack) + crit_rebate = critical_attack.tip_crit_effect(src, victim) + if(critical_attack.harmless) + tilt_over(critical_attack.fall_towards_mob ? victim : null) + return VENDOR_CRUSH_HANDLED + + should_throw_at_target = critical_attack.fall_towards_mob + add_attack_logs(null, victim, "critically crushed by [src] causing [critical_attack]") + else + victim.visible_message( + "[victim] is crushed by [src]!", + "[src] crushes you!", + "You hear a loud crunch!" + ) + add_attack_logs(null, victim, "crushed by [src]") + + // 30% chance to spread damage across the entire body, 70% chance to target two limbs in particular + damage_to_deal = max(damage_to_deal - crit_rebate, 0) + if(prob(30)) + victim.apply_damage(damage_to_deal, BRUTE, BODY_ZONE_CHEST, spread_damage = TRUE) + else + var/picked_zone + var/num_parts_to_pick = 2 + for(var/i = 1 to num_parts_to_pick) + picked_zone = pick(BODY_ZONE_CHEST, BODY_ZONE_HEAD, BODY_ZONE_L_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_ARM, BODY_ZONE_R_LEG) + victim.apply_damage((damage_to_deal) * (1 / num_parts_to_pick), BRUTE, picked_zone) + + victim.AddElement(/datum/element/squish, 80 SECONDS) + victim.emote("scream") + + return should_throw_at_target ? VENDOR_THROW_AT_TARGET : VENDOR_TIP_IN_PLACE + +/** + * Tilts the machine onto the atom passed in. + * + * Arguments: + * * victim - The thing the machine is falling on top of + * * crit - if true, some special damage effects might happen. + * * from_combat - If true, hold off on some of the additional damage and extra effects. + */ +/obj/machinery/economy/vending/proc/tilt(atom/victim, crit = FALSE, from_combat = FALSE) + if(QDELETED(src) || !has_gravity(src) || !tiltable || tilted) + return + + tilted = TRUE + layer = ABOVE_MOB_LAYER + + var/should_throw_at_target = TRUE + + . = FALSE + + if(!victim || !in_range(victim, src)) + tilt_over() + return + for(var/mob/living/L in get_turf(victim)) + // Damage to deal outright + var/damage_to_deal = squish_damage + if(!from_combat) + if(crit) + // increase damage if you knock it over onto yourself + damage_to_deal *= crit_damage_factor + else + damage_to_deal *= self_knockover_factor + + if(iscarbon(L)) + var/throw_spec = handle_squish_carbon(victim, damage_to_deal, crit, from_combat) + switch(throw_spec) + if(VENDOR_CRUSH_HANDLED) + return TRUE + if(VENDOR_THROW_AT_TARGET) + should_throw_at_target = TRUE + if(VENDOR_TIP_IN_PLACE) + should_throw_at_target = FALSE + else + L.visible_message( + "[L] is crushed by [src]!", + "[src] falls on top of you, crushing you!" + ) + L.apply_damage(damage_to_deal, BRUTE) + + add_attack_logs(null, L, "crushed by [src]") + + . = TRUE + L.Weaken(6 SECONDS) + L.KnockDown(12 SECONDS) + + playsound(L, "sound/effects/blobattack.ogg", 40, TRUE) + playsound(L, "sound/effects/splat.ogg", 50, TRUE) + + tilt_over(should_throw_at_target ? victim : null) + +/obj/machinery/economy/vending/proc/tilt_over(mob/victim) + visible_message("[src] tips over!", "You hear a loud crash!") + playsound(src, "sound/effects/bang.ogg", 100, TRUE) + var/matrix/M = matrix() + M.Turn(pick(90, 270)) + transform = M + if(victim && get_turf(victim) != get_turf(src)) + throw_at(get_turf(victim), 1, 1, spin = FALSE) + +/obj/machinery/economy/vending/proc/untilt(mob/user) + if(!tilted) + return + + if(user) + user.visible_message( + "[user] begins to right [src].", + "You begin to right [src]." + ) + if(!do_after(user, 7 SECONDS, TRUE, src)) + return + user.visible_message( + "[user] rights [src].", + "You right [src].", + "You hear a loud clang." + ) + + unbuckle_all_mobs(TRUE) + + tilted = FALSE + layer = initial(layer) + + var/matrix/M = matrix() + M.Turn(0) + transform = M + +/obj/machinery/economy/vending/shove_impact(mob/living/target, mob/living/attacker) + tilt(target, from_combat = TRUE) + return TRUE + /* * Vending machine types */ @@ -818,3 +1105,6 @@ */ +#undef VENDOR_CRUSH_HANDLED +#undef VENDOR_THROW_AT_TARGET +#undef VENDOR_TIP_IN_PLACE diff --git a/code/game/machinery/vendors/vendor_crits.dm b/code/game/machinery/vendors/vendor_crits.dm new file mode 100644 index 00000000000..ff86e2d8863 --- /dev/null +++ b/code/game/machinery/vendors/vendor_crits.dm @@ -0,0 +1,119 @@ +/** + * Framework for custom vendor crits. + */ + +/datum/vendor_crit + /// If it'll deal damage or not + var/harmless = FALSE + /// If we should be thrown against the mob or not. + var/fall_towards_mob = TRUE + +/** + * Return whether or not the crit selected is valid. + */ +/datum/vendor_crit/proc/is_valid(obj/machinery/economy/vending/machine, mob/living/carbon/victim) + return TRUE + +/*** + * Perform the tip crit effect on a victim. + * Arguments: + * * machine - The machine that was tipped over + * * user - The unfortunate victim upon whom it was tipped over + * Returns: The "crit rebate", or the amount of damage to subtract from the original amount of damage dealt, to soften the blow. + */ +/datum/vendor_crit/proc/tip_crit_effect(obj/machinery/economy/vending/machine, mob/living/carbon/victim) + return 0 + +/datum/vendor_crit/shatter + +/datum/vendor_crit/shatter/tip_crit_effect(obj/machinery/economy/vending/machine, mob/living/carbon/victim) + victim.bleed(150) + var/obj/item/organ/external/leg/right = victim.get_organ(BODY_ZONE_R_LEG) + var/obj/item/organ/external/leg/left = victim.get_organ(BODY_ZONE_L_LEG) + if(istype(left)) + left.receive_damage(200) + if(istype(right)) + right.receive_damage(200) + + if(left || right) + victim.visible_message( + "[victim]'s legs shatter with a sickening crunch!", + "Your legs shatter with a sickening crunch!", + "You hear a sickening crunch!" + ) + + // that's a LOT of damage, let's rebate most of it. + return machine.squish_damage * (5/6) + +/datum/vendor_crit/pin + +/datum/vendor_crit/pin/tip_crit_effect(obj/machinery/economy/vending/machine, mob/living/carbon/victim) + machine.forceMove(get_turf(victim)) + machine.buckle_mob(victim, force=TRUE) + victim.visible_message( + "[victim] gets pinned underneath [machine]!", + "You are pinned down by [machine]!" + ) + + return 0 + +/datum/vendor_crit/embed + +/datum/vendor_crit/embed/is_valid(obj/machinery/economy/vending/machine, mob/living/carbon/victim) + . = ..() + if(machine.num_shards <= 0) + return FALSE + +/datum/vendor_crit/embed/tip_crit_effect(obj/machinery/economy/vending/machine, mob/living/carbon/victim) + victim.visible_message( + "[machine]'s panel shatters against [victim]!", + "[H] gets crushed under [machine], and explodes in a shower of gore!", "Oh f-") + new /obj/effect/gibspawner/human(get_turf(victim)) + H.drop_organs() + H.droplimb(TRUE) + H.disfigure() + victim.apply_damage(50, BRUTE, BODY_ZONE_HEAD) + else + H.visible_message("[victim]'s head seems to be crushed under [machine]...but wait, they had none in the first place!") + if(B in H) + victim.adjustBrainLoss(80) + + return 0 + +/datum/vendor_crit/lucky + harmless = TRUE + +/datum/vendor_crit/lucky/tip_crit_effect(obj/machinery/economy/vending/machine, mob/living/carbon/victim) + victim.visible_message( + "[src] crashes around [victim], but doesn't seem to crush them!", + "[src] crashes around you, but only around you! You're fine!" + ) + + return 1000 diff --git a/paradise.dme b/paradise.dme index ada4e4235c3..cd7131c035c 100644 --- a/paradise.dme +++ b/paradise.dme @@ -372,6 +372,7 @@ #include "code\datums\components\spawner.dm" #include "code\datums\components\spooky.dm" #include "code\datums\components\squeak.dm" +#include "code\datums\components\squish.dm" #include "code\datums\components\surgery_initiator.dm" #include "code\datums\components\swarming.dm" #include "code\datums\discord\discord_manager.dm" @@ -806,6 +807,7 @@ #include "code\game\machinery\vendors\departmental_vendors.dm" #include "code\game\machinery\vendors\generic_vendors.dm" #include "code\game\machinery\vendors\vending.dm" +#include "code\game\machinery\vendors\vendor_crits.dm" #include "code\game\machinery\vendors\wardrobe_vendors.dm" #include "code\game\magic\Uristrunes.dm" #include "code\game\mecha\mech_bay.dm"