diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 5603bae60f2..5ed83d99f36 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -956,4 +956,13 @@
/// Called when the MODsuit wearer is unset.
#define COMSIG_MOD_WEARER_UNSET "mod_wearer_unset"
+/// from /obj/structure/cursed_slot_machine/handle_status_effect() when someone pulls the handle on the slot machine
+#define COMSIG_CURSED_SLOT_MACHINE_USE "cursed_slot_machine_use"
+ #define SLOT_MACHINE_USE_CANCEL (1<<0) //! we've used up the number of times we may use this slot machine. womp womp.
+ #define SLOT_MACHINE_USE_POSTPONE (1<<1) //! we haven't used up all our attempts to gamble away our life but we should chill for a few seconds
+/// from /obj/structure/cursed_slot_machine/determine_victor() when someone loses.
+#define COMSIG_CURSED_SLOT_MACHINE_LOST "cursed_slot_machine_lost"
+
+/// from /obj/structure/cursed_slot_machine/determine_victor() when someone finally wins.
+#define COMSIG_GLOB_CURSED_SLOT_MACHINE_WON "cursed_slot_machine_won"
diff --git a/code/datums/status_effects/debuffs.dm b/code/datums/status_effects/debuffs.dm
index e128becc582..ef5c7b0f43a 100644
--- a/code/datums/status_effects/debuffs.dm
+++ b/code/datums/status_effects/debuffs.dm
@@ -1119,3 +1119,172 @@
if(new_filter)
animate(get_filter("ray"), offset = 10, time = 10 SECONDS, loop = -1)
animate(offset = 0, time = 10 SECONDS)
+
+#define DEFAULT_MAX_CURSE_COUNT 5
+
+/// Status effect that gives the target miscellanous debuffs while throwing a status alert and causing them to smoke from the damage they're incurring.
+/// Purposebuilt for cursed slot machines.
+/datum/status_effect/cursed
+ id = "cursed"
+ alert_type = /obj/screen/alert/status_effect/cursed
+ /// The max number of curses a target can incur with this status effect.
+ var/max_curse_count = DEFAULT_MAX_CURSE_COUNT
+ /// The amount of times we have been "applied" to the target.
+ var/curse_count = 0
+ /// Raw probability we have to deal damage this tick.
+ var/damage_chance = 10
+ /// Are we currently in the process of sending a monologue?
+ var/monologuing = FALSE
+ /// The hand we are branded to.
+ var/obj/item/organ/external/branded_hand = null
+
+/datum/status_effect/cursed/on_apply()
+ RegisterSignal(owner, COMSIG_MOB_STATCHANGE, PROC_REF(on_stat_changed))
+ RegisterSignal(owner, COMSIG_MOB_DEATH, PROC_REF(on_death))
+ RegisterSignal(owner, COMSIG_CURSED_SLOT_MACHINE_USE, PROC_REF(check_curses))
+ RegisterSignal(owner, COMSIG_CURSED_SLOT_MACHINE_LOST, PROC_REF(update_curse_count))
+ RegisterSignal(SSdcs, COMSIG_GLOB_CURSED_SLOT_MACHINE_WON, PROC_REF(clear_curses))
+ return ..()
+
+/datum/status_effect/cursed/Destroy()
+ UnregisterSignal(SSdcs, COMSIG_GLOB_CURSED_SLOT_MACHINE_WON)
+ branded_hand = null
+ return ..()
+
+/// Checks the number of curses we have and returns information back to the slot machine. `max_curse_amount` is set by the slot machine itself.
+/datum/status_effect/cursed/proc/check_curses(mob/user, max_curse_amount)
+ SIGNAL_HANDLER
+ if(curse_count >= max_curse_amount)
+ return SLOT_MACHINE_USE_CANCEL
+
+ if(monologuing)
+ to_chat(owner, "Your arm is resisting your attempts to pull the lever!") // listening to kitschy monologues to postpone your powergaming is the true curse here.
+ return SLOT_MACHINE_USE_POSTPONE
+
+/// Handles the debuffs of this status effect and incrementing the number of curses we have.
+/datum/status_effect/cursed/proc/update_curse_count()
+ SIGNAL_HANDLER
+ curse_count++
+
+ linked_alert?.update_appearance() // we may have not initialized it yet
+
+ addtimer(CALLBACK(src, PROC_REF(handle_after_effects), 1 SECONDS)) // give it a second to let the failure sink in before we exact our toll
+
+/// Makes a nice lorey message about the curse level we're at. I think it's nice
+/datum/status_effect/cursed/proc/handle_after_effects()
+ if(QDELETED(src))
+ return
+
+ monologuing = TRUE
+ var/list/messages = list()
+ switch(curse_count)
+ if(1) // basically your first is a "freebie" that will still require urgent medical attention and will leave you smoking forever but could be worse tbh
+ if(ishuman(owner))
+ var/mob/living/carbon/human/human_owner = owner
+ playsound(human_owner, 'sound/weapons/sear.ogg', 50, TRUE)
+ var/obj/item/organ/external/affecting = human_owner.get_active_hand()
+ branded_hand = affecting
+
+ messages += "Your hand burns, and you quickly let go of the lever! You feel a little sick as the nerves deaden in your hand..."
+ messages += "Some smoke appears to be coming out of your hand now, but it's not too bad..."
+ messages += "Fucking stupid machine."
+
+ if(2)
+ messages += "The machine didn't burn you this time, it must be some arcane work of the brand recognizing a source..."
+ messages += "Blisters and boils start to appear over your skin. Each one hissing searing hot steam out of its own pocket..."
+ messages += "You understand that the machine tortures you with its simplistic allure. It can kill you at any moment, but it derives a sick satisfaction at forcing you to keep going."
+ messages += "If you could get away from here, you might be able to live with some medical supplies. Is it too late to stop now?"
+ messages += "As you shut your eyes to dwell on this conundrum, the brand surges in pain. You shudder to think what might happen if you go unconscious."
+
+ if(3)
+ owner.emote("cough")
+ messages += "Your throat becomes to feel like it's slowly caking up with sand and dust. You eject the contents of the back of your throat onto your sleeve."
+ messages += "It is sand. Crimson red. You've never felt so thirsty in your life, yet you don't trust your own hand to carry the glass to your lips."
+ messages += "You get the sneaking feeling that if someone else were to win, that it might clear your curse too. Saving your life is a noble cause."
+ messages += "Of course, you might have to not speak on the nature of this machine, in case they scamper off to leave you to die."
+ messages += "Is it truly worth it to condemn someone to this fate to cure the manifestation of your own hedonistic urges? You'll have to decide quickly."
+
+ if(4)
+ messages += "A migraine swells over your head as your thoughts become hazy. Your hand desperately inches closer towards the slot machine for one final pull..."
+ messages += "The ultimate test of mind over matter. You can jerk your own muscle back in order to prevent a terrible fate, but your life already is worth so little now."
+ messages += "This is what they want, is it not? To witness your failure against itself? The compulsion carries you forward as a sinking feeling of dread fills your stomach."
+ messages += "Paradoxically, where there is hopelessness, there is elation. Elation at the fact that there's still enough power in you for one more pull."
+ messages += "Your legs desperately wish to jolt away on the thought of running away from this wretched machination, but your own arm remains complacent in the thought of seeing spinning wheels."
+ messages += "The toll has already been exacted. There is no longer death on 'your' terms. Is your dignity worth more than your life?"
+
+ if(5 to INFINITY)
+ if(max_curse_count != DEFAULT_MAX_CURSE_COUNT) // this probably will only happen through admin schenanigans letting people stack up infinite curses or something
+ to_chat(owner, "Do you still think you're in control?")
+ return
+
+ to_chat(owner, "Why couldn't I get one more try?!")
+ owner.gib()
+ qdel(src)
+ return
+ for(var/message in messages)
+ to_chat(owner, message)
+ sleep(3 SECONDS) // yes yes a bit fast but it can be a lot of text and i want the whole thing to send before the cooldown on the slot machine might expire
+ monologuing = FALSE
+
+/// Cleans ourselves up and removes our curses. Meant to be done in a "positive" way, when the curse is broken. Directly use qdel otherwise.
+/datum/status_effect/cursed/proc/clear_curses()
+ SIGNAL_HANDLER
+
+ owner.visible_message(
+ "The smoke slowly clears from [owner.name]...",
+ "Your skin finally settles down and your throat no longer feels as dry... The brand disappearing confirms that the curse has been lifted.",)
+ qdel(src)
+
+/// If our owner's stat changes, rapidly surge the damage chance.
+/datum/status_effect/cursed/proc/on_stat_changed()
+ SIGNAL_HANDLER
+ if(owner.stat == CONSCIOUS || owner.stat == DEAD) // reset on these two states
+ damage_chance = initial(damage_chance)
+ return
+
+ to_chat(owner, "As your body crumbles, you feel the curse of the slot machine surge through your body!")
+ damage_chance += 75 //ruh roh raggy
+
+/// If our owner dies without getting gibbed, we gib them, because fuck you baltamore
+/datum/status_effect/cursed/proc/on_death(mob/living/source, gibbed)
+ SIGNAL_HANDLER
+ if(!ishuman(owner))
+ return
+ if(gibbed)
+ return
+ var/mob/living/carbon/human/H = owner
+ INVOKE_ASYNC(H, TYPE_PROC_REF(/mob, gib))
+
+/datum/status_effect/cursed/tick()
+ if(curse_count <= 1)
+ return // you get one "freebie" (single damage) to nudge you into thinking this is a bad idea before the house begins to win.
+
+ // the house won.
+ var/ticked_coefficient = rand(15, 40) * 2 / 100
+ var/effective_percentile_chance = ((curse_count == 2 ? 1 : curse_count) * damage_chance * ticked_coefficient)
+
+ if(prob(effective_percentile_chance))
+ owner.apply_damages(
+ brute = (curse_count * ticked_coefficient),
+ burn = (curse_count * ticked_coefficient),
+ oxy = (curse_count * ticked_coefficient),
+ )
+
+/obj/screen/alert/status_effect/cursed
+ name = "Cursed!"
+ desc = "The brand on your hand reminds you of your greed, yet you seem to be okay otherwise."
+ icon_state = "cursed_by_slots"
+
+/obj/screen/alert/status_effect/cursed/update_desc()
+ . = ..()
+ var/datum/status_effect/cursed/linked_effect = attached_effect
+ var/curses = linked_effect.curse_count
+ switch(curses)
+ if(2)
+ desc = "Your greed is catching up to you..."
+ if(3)
+ desc = "You really don't feel good right now... But why stop now?"
+ if(4 to INFINITY)
+ desc = "Real winners quit before they reach the ultimate prize."
+
+#undef DEFAULT_MAX_CURSE_COUNT
diff --git a/code/modules/ruins/lavalandruin_code/sin_ruins.dm b/code/modules/ruins/lavalandruin_code/sin_ruins.dm
index d9ecb7dc5c2..59350857e21 100644
--- a/code/modules/ruins/lavalandruin_code/sin_ruins.dm
+++ b/code/modules/ruins/lavalandruin_code/sin_ruins.dm
@@ -1,56 +1,101 @@
//These objects are used in the cardinal sin-themed ruins (i.e. Gluttony, Pride...)
// Greed
-#define WIN_PROB 5
-
-/obj/structure/cursed_slot_machine //Greed's slot machine: Used in the Greed ruin. Deals clone damage on each use, with a successful use giving a d20 of fate.
+/// Greed's slot machine: Used in the Greed ruin. Deals damage on each use, with a successful use giving a d20 of fate.
+/obj/structure/cursed_slot_machine
name = "greed's slot machine"
desc = "High stakes, high rewards."
- icon = 'icons/obj/economy.dmi'
- icon_state = "slots-off"
+ icon = 'icons/obj/computer.dmi'
+ icon_state = "slots"
anchored = TRUE
density = TRUE
+ /// Variable that tracks the screen we display.
+ var/icon_screen = "slots_screen"
+ /// Should we be emitting light?
+ var/brightness_on = TRUE
+ /// The probability the player has to win.
+ var/win_prob = 5
+ /// The maximum amount of curses we will allow a player to have before disallowing them to use the machine.
+ var/max_curse_amount = 5
+ /// machine's reward when you hit jackpot
+ var/prize = /obj/structure/cursed_money
+ /// should we be applying the cursed status effect?
+ var/status_effect_on_roll = TRUE
+ /// Length of the cooldown between the machine being used and being able to spin the machine again.
+ var/cooldown_length = 30 SECONDS
+ /// Are we currently in use? Anti-spam prevention measure.
+ var/in_active_use = FALSE
+ /// Cooldown between pulls of the cursed slot machine.
+ COOLDOWN_DECLARE(spin_cooldown)
+
+/obj/structure/cursed_slot_machine/Initialize(mapload)
+ . = ..()
+ update_appearance()
+ set_light(brightness_on)
/obj/structure/cursed_slot_machine/attack_hand(mob/user)
- interact(user)
-
-/obj/structure/cursed_slot_machine/interact(mob/living/carbon/human/user)
- if(!istype(user))
+ if(!ishuman(user))
return
- if(in_use)
+ if(!check_and_set_usage(user))
return
- in_use = TRUE
- if(!ismachineperson(user))
- user.adjustCloneLoss(20)
- else
- to_chat(user, "Your brain is sparking!")
- user.adjustBrainLoss(15)
- if(user.stat)
- to_chat(user, "No... just one more try...")
- user.gib()
- else
- user.visible_message("[user] pulls [src]'s lever with a glint in [user.p_their()] eyes!", "You feel a draining as you pull the lever, but you \
- know it'll be worth it.")
- icon_state = "slots-on"
- playsound(src, 'sound/lavaland/cursed_slot_machine.ogg', 50)
- addtimer(CALLBACK(src, PROC_REF(determine_victor), user), 50)
+ user.visible_message(
+ "[user] pulls [src]'s lever with a glint in [user.p_their()] eyes!",
+ "You feel a draining as you pull the lever, but you know it'll be worth it.")
-/obj/structure/cursed_slot_machine/proc/determine_victor(mob/living/user)
- icon_state = "slots-off"
- in_use = FALSE
- if(prob(WIN_PROB))
- playsound(src, 'sound/lavaland/cursed_slot_machine_jackpot.ogg', 50)
- new/obj/structure/cursed_money(get_turf(src))
- if(user)
- to_chat(user, "You've hit jackpot. Laughter echoes around you as your reward appears in the machine's place.")
- qdel(src)
- else
- if(user)
- to_chat(user, "Fucking machine! Must be rigged. Still... one more try couldn't hurt, right?")
+ icon_screen = "slots_screen_working"
+ update_appearance()
+ playsound(src, 'sound/lavaland/cursed_slot_machine.ogg', 50, FALSE)
+ addtimer(CALLBACK(src, PROC_REF(determine_victor), user), 5 SECONDS)
-#undef WIN_PROB
+/obj/structure/cursed_slot_machine/update_overlays()
+ . = ..()
+ var/overlay_state = icon_screen
+ . += mutable_appearance(icon, overlay_state)
+ . += emissive_appearance(icon, overlay_state, src)
+
+/// Validates that the user can use the cursed slot machine. User is the person using the slot machine. Returns TRUE if we can, FALSE otherwise.
+/obj/structure/cursed_slot_machine/proc/check_and_set_usage(mob/living/carbon/human/user)
+ if(in_active_use)
+ to_chat(user, "The machine is already spinning!")
+ return FALSE
+
+ var/signal_value = SEND_SIGNAL(user, COMSIG_CURSED_SLOT_MACHINE_USE, max_curse_amount)
+
+ if(!COOLDOWN_FINISHED(src, spin_cooldown) || (signal_value & SLOT_MACHINE_USE_POSTPONE))
+ to_chat(user, "The machine doesn't engage. You get the compulsion to try again in a few seconds.")
+ return FALSE
+
+ if(signal_value & SLOT_MACHINE_USE_CANCEL) // failsafe in case we don't want to let the machine be used for some reason (like if we're maxed out on curses but not getting gibbed)
+ atom_say("We're sorry, but we can no longer serve you at this establishment.")
+ return FALSE
+
+ in_active_use = TRUE
+ return TRUE
+
+/obj/structure/cursed_slot_machine/proc/determine_victor(mob/living/carbon/human/user)
+ icon_screen = initial(icon_screen)
+ update_appearance()
+
+ in_active_use = FALSE
+ COOLDOWN_START(src, spin_cooldown, cooldown_length)
+
+ if(!prob(win_prob))
+ if(status_effect_on_roll && isnull(user.has_status_effect(/datum/status_effect/cursed)))
+ user.apply_status_effect(/datum/status_effect/cursed)
+
+ SEND_SIGNAL(user, COMSIG_CURSED_SLOT_MACHINE_LOST)
+ playsound(src, 'sound/machines/buzz-sigh.ogg', 30, TRUE)
+ return
+
+ playsound(src, 'sound/lavaland/cursed_slot_machine_jackpot.ogg', 50, FALSE)
+ new prize(get_turf(src))
+ if(user)
+ to_chat(user, "You've hit the jackpot!!! Laughter echoes around you as your reward appears in the machine's place.")
+
+ SEND_GLOBAL_SIGNAL(COMSIG_GLOB_CURSED_SLOT_MACHINE_WON)
+ qdel(src)
/obj/structure/cursed_money
name = "bag of money"
@@ -82,7 +127,7 @@
var/turf/T = get_turf(user)
var/obj/item/dice/d20/fate/one_use/critical_fail = new(T)
user.put_in_hands(critical_fail)
- qdel(src)
+ collapse()
// Gluttony
/obj/effect/gluttony //Gluttony's wall: Used in the Gluttony ruin. Only lets the overweight through.
diff --git a/icons/mob/screen_alert.dmi b/icons/mob/screen_alert.dmi
index b3a990bc54c..d09c2406376 100644
Binary files a/icons/mob/screen_alert.dmi and b/icons/mob/screen_alert.dmi differ
diff --git a/icons/obj/computer.dmi b/icons/obj/computer.dmi
index 8dd128724f1..dddd4fff81a 100644
Binary files a/icons/obj/computer.dmi and b/icons/obj/computer.dmi differ