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