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>
This commit is contained in:
Luc
2023-03-04 09:22:07 +01:00
committed by GitHub
co-authored by S34N
parent a91dd0fffb
commit b33d6aa080
5 changed files with 457 additions and 1 deletions
+43
View File
@@ -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
+2
View File
@@ -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)
+291 -1
View File
@@ -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)
. += "<span class='warning'>It's been tipped over and won't be usable unless it's righted.</span>"
if(Adjacent(user))
. += "<span class='notice'>You can <b>Alt-Click</b> it to right it.</span>"
/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, "<span class='warning'>[src] is tipped over and non-functional! You'll need to right it first.</span>")
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("<span class='warning'>[src] seems to sway a bit!</span>")
to_chat(user, "<span class='danger'>You might want to think twice about doing that again, [src] looks like it could come crashing down!</span>")
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("<span class='notice'>[num_freebies] free goodie\s tumble[num_freebies > 1 ? "" : "s"] out of [src]!</span>")
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("<span class='warning'>You'll need to right it first!</span>")
return
default_deconstruction_crowbar(user, I)
/obj/machinery/economy/vending/multitool_act(mob/user, obj/item/I)
. = TRUE
if(tilted)
to_chat("<span class='warning'>You'll need to right it first!</span>")
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("<span class='warning'>You'll need to right it first!</span>")
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("<span class='warning'>You'll need to right it first!</span>")
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("<span class='warning'>The fastening bolts aren't on the ground, you'll need to right it first!</span>")
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, "<span class='warning'>[src] is tipped over and non-functional! You'll need to right it first.</span>")
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(
"<span class='danger'>[victim] is crushed by [src]!</span>",
"<span class='userdanger'>[src] crushes you!</span>",
"<span class='warning'>You hear a loud crunch!</span>"
)
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(
"<span class='danger'>[L] is crushed by [src]!</span>",
"<span class='userdanger'>[src] falls on top of you, crushing you!</span>"
)
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("<span class='danger'>[src] tips over!</span>", "<span class='danger'>You hear a loud crash!</span>")
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(
"<span class='notice'>[user] rights [src].</span>",
"<span class='notice'>You right [src].</span>",
"<span class='notice'>You hear a loud clang.</span>"
)
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
+119
View File
@@ -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(
"<span class='danger'>[victim]'s legs shatter with a sickening crunch!</span>",
"<span class='userdanger'>Your legs shatter with a sickening crunch!</span>",
"<span class='danger'>You hear a sickening crunch!</span>"
)
// 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(
"<span class='danger'>[victim] gets pinned underneath [machine]!</span>",
"<span class='userdanger'>You are pinned down by [machine]!</span>"
)
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(
"<span class='danger'>[machine]'s panel shatters against [victim]!</span>",
"<span class='userdanger>[machine] lands on you, its panel shattering!</span>"
)
for(var/i in 1 to machine.num_shards)
var/obj/item/shard/shard = new /obj/item/shard(get_turf(victim))
// do a little dance to force the embeds, but make sure the glass isn't gigapowered afterwards
shard.embed_chance = 100
shard.embedded_pain_chance = 5
shard.embedded_impact_pain_multiplier = 1
shard.embedded_ignore_throwspeed_threshold = TRUE
victim.hitby(shard, skipcatch = TRUE, hitpush = FALSE)
shard.embed_chance = initial(shard.embed_chance)
shard.embedded_pain_chance = initial(shard.embedded_pain_chance)
shard.embedded_impact_pain_multiplier = initial(shard.embedded_pain_multiplier)
shard.embedded_ignore_throwspeed_threshold = initial(shard.embedded_ignore_throwspeed_threshold)
playsound(machine, "shatter", 50)
return machine.squish_damage * (3/4)
/datum/vendor_crit/pop_head
/datum/vendor_crit/pop_head/tip_crit_effect(obj/machinery/economy/vending/machine, mob/living/carbon/victim)
// pop!
var/obj/item/organ/external/head/H = victim.get_organ("head")
var/obj/item/organ/internal/brain/B = victim.get_int_organ_tag("brain")
if(H)
victim.visible_message("<span class='danger'>[H] gets crushed under [machine], and explodes in a shower of gore!</span>", "<span class='userdanger'>Oh f-</span>")
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("<span class='danger'>[victim]'s head seems to be crushed under [machine]...but wait, they had none in the first place!</span>")
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(
"<span class='danger'>[src] crashes around [victim], but doesn't seem to crush them!</span>",
"<span class='userdanger'>[src] crashes around you, but only around you! You're fine!</span>"
)
return 1000
+2
View File
@@ -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"