Files
Bubberstation/code/game/objects/structures/mirror.dm
SyncIt21 6dc40ca522 Standardizes object deconstruction throughout the codebase. (#82280)
## About The Pull Request
When it comes to deconstructing an object we have `proc/deconstruct()` &
`NO_DECONSTRUCT`

Lets talk about the flag first. 

**Problems with `NO_DECONSTRUCTION`**
I know what the comment says on what it should do

b5593bc693/code/__DEFINES/obj_flags.dm (L18)

But everywhere people have decided to give their own meaning/definition
to this flag. Here are some examples on how this flag is used

**1. Make the object just disappear(not drop anything) when
deconstructed**
This is by far the largest use case everywhere. If an object is
deconstructed(either via tools or smashed apart) then if it has this
flag it should not drop any of its contents but just disappear. You have
seen this code pattern used everywhere

b5593bc693/code/game/machinery/constructable_frame.dm (L26-L31)

This behaviour is then leveraged by 2 important components.

When an object is frozen, if it is deconstructed it should just
disappear without leaving any traces behind

b5593bc693/code/datums/elements/frozen.dm (L66-L67)

By hologram objects. Obviously if you destroy an hologram nothing real
should drop out

b5593bc693/code/modules/holodeck/computer.dm (L301-L304)

And there are other use cases as well but we won't go into them as they
aren't as significant as these.

**2. To stop an object from being wrenched ??**
Yeah this one is weird. Like why? I understand in some instances (chair,
table, rack etc) a wrench can be used to deconstruct a object so using
the flag there to stop it from happening makes sense but why can't we
even anchor an object just because of this flag?

b5593bc693/code/game/objects/objs.dm (L368-L369)
This is one of those instances where somebody just decided this
behaviour for their own convenience just like the above example with no
explanation as to why

**3. To stop using tools to deconstruct the object** 
This was the original intent of the flag but it is enforced in few
places far & between. One example is when deconstructing the a machine
via crowbar.

b5593bc693/code/game/machinery/_machinery.dm (L811)

But machines are a special dual use case for this flag. Because if you
look at its deconstruct proc the flag also prevents the machine from
spawning a frame.

b5593bc693/code/game/machinery/_machinery.dm (L820-L822)

How can 1 flag serve 2 purposes within the same type?

**4. Simply forget to check for this flag altogether**
Yup if you find this flag not doing its job for some objects don't be
surprised. People & sometimes even maintainers just forget that it even
exists

b5593bc693/code/game/objects/items/piggy_bank.dm (L66-L67)

**Solution**
These are the main examples i found. As you can see the same flag can
perform 2 different functions within the same type and do something else
in a different object & in some instances don't even work cause people
just forget, etc.

In order to bring consistency to this flag we need to move it to the
atom level where it means the same thing everywhere. Where in the atom
you may ask? .Well, I'll just post what MrMelbert said in
https://github.com/tgstation/tgstation/pull/81656#discussion_r1503086862

> ...Ideally the .deconstruct call would handle NO_DECONSTRUCTION
handling as it wants,

Yup that's the ideal case now. This flag is checked directly in
`deconstruct()`. Now like i said we want to give a universal definition
to this flag and as you have seen from my examples it is used in 3 cases
1) Make an object disappear(doesn't dropping anything) when
deconstructed
2) Stop it from being wrenched
3) Stop it from being deconstructed via tools

We can't enforce points 2 & 3 inside `deconstruct()` which leaves us
with only case 1) i.e. make the object disappear. And that's what i have
done. Therefore after more than a decade or since this flag got
introduced `NO_DECONSTRUCT` now has a new definition as of 2024

_"Make an object disappear(don't dropping anything) when deconstructed
either via tools or forcefully smashed apart"_

Now i very well understand this will open up bugs in places where cases
2 & 3 are required but its worth it. In fact they could even be qol
changes for all we know so who knows it might even benefit us but for
now we need to give a universal definition to this flag to bring some
consistency & that's what this PR does.

**Problem with deconstruct()**
This proc actually sends out a signal which is currently used by the
material container but could be used by other objects later on.

3e84c3e6da/code/game/objects/obj_defense.dm (L160)

So objects that override this proc should call its parent. Sadly that
isn't the case in many instances like such

3e84c3e6da/code/game/machinery/deployable.dm (L20-L23)

Instead of `return ..()` which would delete the object & send the signal
it deletes the object directly thus the signal never gets sent.

**Solution**
Make this proc non overridable. For objects to add their own custom
deconstruction behaviour a new proc has been introduced
`atom_deconstruct()` Subtypes should now override this proc to handle
object deconstruction.

If objects have certain important stuff inside them (like mobs in
machines for example) they want to drop by handling `NO_DECONSTRUCT`
flag in a more carefully customized way they can do this by overriding
`handle_deconstruct()` which by default delegates to
`atom_deconstruct()` if the `NO_DECONSTRUCT` flag is absent. This proc
will allow you to handle the flag in a more customized way if you ever
need to.

## Why It's Good For The Game
1) I'm goanna post the full comment from MrMelbert
https://github.com/tgstation/tgstation/pull/81656#discussion_r1503086862

> ...Ideally the .deconstruct call would handle NO_DECONSTRUCTION
handling as it wants, but there's a shocking lack of consistency around
NO_DECONSTRUCTION, where some objects treat it as "allow deconstruction,
but make it drop no parts" and others simply "disallow deconstruction at
all"

This PR now makes `NO_DECONSTRUCTION` handled by `deconstruct()` & gives
this flag the consistency it deserves. Not to mention as shown in case 4
there are objects that simply forgot to check for this flag. Now it
applies for those missing instances as well.

2) No more copying pasting the most overused code pattern in this code
base history `if(obj_flags & NO_DECONSTRUCTION)`. Just makes code
cleaner everywhere

3) All objects now send the `COMSIG_OBJ_DECONSTRUCT` signal on object
deconstruction which is now available for use should you need it

## Changelog
🆑
refactor: refactors how objects are deconstructed in relation to the
`NO_DECONSTRUCTION` flag. Certain objects & machinery may display
different tool interactions & behaviours when destroyed/deconstructed.
Report these changes if you feel like they are bugs
/🆑

---------

Co-authored-by: san7890 <the@san7890.com>
2024-04-04 18:55:51 -06:00

418 lines
14 KiB
Plaintext

// Normal Mirrors
#define CHANGE_HAIR "Change Hair"
#define CHANGE_BEARD "Change Beard"
// Magic Mirrors!
#define CHANGE_RACE "Change Race"
#define CHANGE_SEX "Change Sex"
#define CHANGE_NAME "Change Name"
#define CHANGE_EYES "Change Eyes"
#define INERT_MIRROR_OPTIONS list(CHANGE_HAIR, CHANGE_BEARD)
#define PRIDE_MIRROR_OPTIONS list(CHANGE_HAIR, CHANGE_BEARD, CHANGE_RACE, CHANGE_SEX, CHANGE_EYES)
#define MAGIC_MIRROR_OPTIONS list(CHANGE_HAIR, CHANGE_BEARD, CHANGE_RACE, CHANGE_SEX, CHANGE_EYES, CHANGE_NAME)
/obj/structure/mirror
name = "mirror"
desc = "Mirror mirror on the wall, who's the most robust of them all?"
icon = 'icons/obj/watercloset.dmi'
icon_state = "mirror"
movement_type = FLOATING
density = FALSE
anchored = TRUE
integrity_failure = 0.5
max_integrity = 200
var/list/mirror_options = INERT_MIRROR_OPTIONS
///Flags this race must have to be selectable with this type of mirror.
var/race_flags = MIRROR_MAGIC
///List of all Races that can be chosen, decided by its Initialize.
var/list/selectable_races = list()
/obj/structure/mirror/Initialize(mapload)
. = ..()
update_choices()
/obj/structure/mirror/Destroy()
mirror_options = null
selectable_races = null
return ..()
/obj/structure/mirror/proc/update_choices()
for(var/i in mirror_options)
mirror_options[i] = icon('icons/hud/radial.dmi', i)
/obj/structure/mirror/Initialize(mapload)
. = ..()
var/static/list/reflection_filter = alpha_mask_filter(icon = icon('icons/obj/watercloset.dmi', "mirror_mask"))
var/static/matrix/reflection_matrix = matrix(0.75, 0, 0, 0, 0.75, 0)
var/datum/callback/can_reflect = CALLBACK(src, PROC_REF(can_reflect))
var/list/update_signals = list(COMSIG_ATOM_BREAK)
AddComponent(/datum/component/reflection, reflection_filter = reflection_filter, reflection_matrix = reflection_matrix, can_reflect = can_reflect, update_signals = update_signals)
/obj/structure/mirror/proc/can_reflect(atom/movable/target)
///I'm doing it this way too, because the signal is sent before the broken variable is set to TRUE.
if(atom_integrity <= integrity_failure * max_integrity)
return FALSE
if(broken || !isliving(target) || HAS_TRAIT(target, TRAIT_NO_MIRROR_REFLECTION))
return FALSE
return TRUE
MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror, 28)
/obj/structure/mirror/Initialize(mapload)
. = ..()
find_and_hang_on_wall()
/obj/structure/mirror/broken
icon_state = "mirror_broke"
/obj/structure/mirror/broken/Initialize(mapload)
. = ..()
atom_break(null, mapload)
MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
/obj/structure/mirror/attack_hand(mob/living/carbon/human/user)
. = ..()
if(. || !ishuman(user) || broken)
return TRUE
if(!istype(src, /obj/structure/mirror/magic) && !user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
return TRUE //no tele-grooming (if nonmagical)
return display_radial_menu(user)
/obj/structure/mirror/proc/display_radial_menu(mob/living/carbon/human/user)
var/pick = show_radial_menu(user, src, mirror_options, user, radius = 36, require_near = TRUE)
if(!pick)
return TRUE //get out
switch(pick)
if(CHANGE_HAIR)
change_hair(user)
if(CHANGE_BEARD)
change_beard(user)
if(CHANGE_RACE)
change_race(user)
if(CHANGE_SEX) // sex: yes
change_sex(user)
if(CHANGE_NAME)
change_name(user)
if(CHANGE_EYES)
change_eyes(user)
return display_radial_menu(user)
/obj/structure/mirror/proc/change_beard(mob/living/carbon/human/beard_dresser)
if(beard_dresser.physique == FEMALE)
if(beard_dresser.facial_hairstyle == "Shaved")
balloon_alert(beard_dresser, "nothing to shave!")
return TRUE
var/shave_beard = tgui_alert(beard_dresser, "Shave your beard?", "Grooming", list("Yes", "No"))
if(shave_beard == "Yes")
beard_dresser.set_facial_hairstyle("Shaved", update = TRUE)
return TRUE
var/new_style = tgui_input_list(beard_dresser, "Select a facial hairstyle", "Grooming", GLOB.facial_hairstyles_list)
if(isnull(new_style))
return TRUE
if(HAS_TRAIT(beard_dresser, TRAIT_SHAVED))
to_chat(beard_dresser, span_notice("If only growing back facial hair were that easy for you... The reminder makes you feel terrible."))
beard_dresser.add_mood_event("bald_hair_day", /datum/mood_event/bald_reminder)
return TRUE
beard_dresser.set_facial_hairstyle(new_style, update = TRUE)
/obj/structure/mirror/proc/change_hair(mob/living/carbon/human/hairdresser)
var/new_style = tgui_input_list(hairdresser, "Select a hairstyle", "Grooming", GLOB.hairstyles_list)
if(isnull(new_style))
return TRUE
if(HAS_TRAIT(hairdresser, TRAIT_BALD))
to_chat(hairdresser, span_notice("If only growing back hair were that easy for you... The reminder makes you feel terrible."))
hairdresser.add_mood_event("bald_hair_day", /datum/mood_event/bald_reminder)
return TRUE
hairdresser.set_hairstyle(new_style, update = TRUE)
/obj/structure/mirror/proc/change_name(mob/living/carbon/human/user)
var/newname = sanitize_name(tgui_input_text(user, "Who are we again?", "Name change", user.name, MAX_NAME_LEN), allow_numbers = TRUE) //It's magic so whatever.
if(!newname)
return TRUE
user.real_name = newname
user.name = newname
if(user.dna)
user.dna.real_name = newname
if(user.mind)
user.mind.name = newname
// Erm ackshually the proper term is species. Get it right??
/obj/structure/mirror/proc/change_race(mob/living/carbon/human/race_changer)
var/racechoice = tgui_input_list(race_changer, "What are we again?", "Race change", selectable_races)
if(isnull(racechoice))
return TRUE
var/new_race_path = selectable_races[racechoice]
if(!ispath(new_race_path, /datum/species))
return TRUE
var/datum/species/newrace = new new_race_path()
var/attributes_desc = newrace.get_physical_attributes()
var/answer = tgui_alert(race_changer, attributes_desc, "Become a [newrace]?", list("Yes", "No"))
if(answer != "Yes")
qdel(newrace)
change_race(race_changer) // try again
return
race_changer.set_species(newrace, icon_update = FALSE)
if(HAS_TRAIT(race_changer, TRAIT_USES_SKINTONES))
var/new_s_tone = tgui_input_list(race_changer, "Choose your skin tone", "Race change", GLOB.skin_tones)
if(new_s_tone)
race_changer.skin_tone = new_s_tone
race_changer.dna.update_ui_block(DNA_SKIN_TONE_BLOCK)
else if(HAS_TRAIT(race_changer, TRAIT_MUTANT_COLORS) && !HAS_TRAIT(race_changer, TRAIT_FIXED_MUTANT_COLORS))
var/new_mutantcolor = input(race_changer, "Choose your skin color:", "Race change", race_changer.dna.features["mcolor"]) as color|null
if(new_mutantcolor)
var/list/mutant_hsv = rgb2hsv(new_mutantcolor)
if(mutant_hsv[3] >= 50) // mutantcolors must be bright
race_changer.dna.features["mcolor"] = sanitize_hexcolor(new_mutantcolor)
race_changer.dna.update_uf_block(DNA_MUTANT_COLOR_BLOCK)
else
to_chat(race_changer, span_notice("Invalid color. Your color is not bright enough."))
return TRUE
race_changer.update_body(is_creating = TRUE)
race_changer.update_mutations_overlay() // no hulk lizard
// possible Genders: MALE, FEMALE, PLURAL, NEUTER
// possible Physique: MALE, FEMALE
// saved you a click (many)
/obj/structure/mirror/proc/change_sex(mob/living/carbon/human/sexy)
var/chosen_sex = tgui_input_list(sexy, "Become a..", "Confirmation", list("Warlock", "Witch", "Wizard", "Itzard")) // YOU try coming up with the 'it' version of wizard
switch(chosen_sex)
if("Warlock")
sexy.gender = MALE
to_chat(sexy, span_notice("Man, you feel like a man!"))
if("Witch")
sexy.gender = FEMALE
to_chat(sexy, span_notice("Man, you feel like a woman!"))
if("Wizard")
sexy.gender = PLURAL
to_chat(sexy, span_notice("Woah dude, you feel like a dude!"))
if("Itzard")
sexy.gender = NEUTER
to_chat(sexy, span_notice("Woah dude, you feel like something else!"))
var/chosen_physique = tgui_input_list(sexy, "Alter your physique as well?", "Confirmation", list("Warlock Physique", "Witch Physique", "Wizards Don't Need Gender"))
if(chosen_physique && chosen_physique != "Wizards Don't Need Gender")
sexy.physique = (chosen_physique == "Warlock Physique") ? MALE : FEMALE
sexy.dna.update_ui_block(DNA_GENDER_BLOCK)
sexy.update_body(is_creating = TRUE) // or else physique won't change properly
sexy.update_mutations_overlay() //(hulk male/female)
sexy.update_clothing(ITEM_SLOT_ICLOTHING) // update gender shaped clothing
/obj/structure/mirror/proc/change_eyes(mob/living/carbon/human/user)
var/new_eye_color = input(user, "Choose your eye color", "Eye Color", user.eye_color_left) as color|null
if(isnull(new_eye_color))
return TRUE
user.eye_color_left = sanitize_hexcolor(new_eye_color)
user.eye_color_right = sanitize_hexcolor(new_eye_color)
user.dna.update_ui_block(DNA_EYE_COLOR_LEFT_BLOCK)
user.dna.update_ui_block(DNA_EYE_COLOR_RIGHT_BLOCK)
user.update_body()
to_chat(user, span_notice("You gaze at your new eyes with your new eyes. Perfect!"))
/obj/structure/mirror/examine_status(mob/living/carbon/human/user)
if(broken)
return list()// no message spam
return ..()
/obj/structure/mirror/attacked_by(obj/item/I, mob/living/user)
if(broken || !istype(user) || !I.force)
return ..()
. = ..()
if(broken) // breaking a mirror truly gets you bad luck!
to_chat(user, span_warning("A chill runs down your spine as [src] shatters..."))
user.AddComponent(/datum/component/omen, incidents_left = 7)
/obj/structure/mirror/bullet_act(obj/projectile/P)
if(broken || !isliving(P.firer) || !P.damage)
return ..()
. = ..()
if(broken) // breaking a mirror truly gets you bad luck!
var/mob/living/unlucky_dude = P.firer
to_chat(unlucky_dude, span_warning("A chill runs down your spine as [src] shatters..."))
unlucky_dude.AddComponent(/datum/component/omen, incidents_left = 7)
/obj/structure/mirror/atom_break(damage_flag, mapload)
. = ..()
if(broken)
return
icon_state = "mirror_broke"
if(!mapload)
playsound(src, SFX_SHATTER, 70, TRUE)
if(desc == initial(desc))
desc = "Oh no, seven years of bad luck!"
broken = TRUE
/obj/structure/mirror/atom_deconstruct(disassembled = TRUE)
if(!disassembled)
new /obj/item/shard(loc)
else
new /obj/item/wallframe/mirror(loc)
/obj/structure/mirror/welder_act(mob/living/user, obj/item/I)
..()
if(user.combat_mode)
return FALSE
if(!broken)
return TRUE
if(!I.tool_start_check(user, amount=1))
return TRUE
balloon_alert(user, "repairing...")
if(I.use_tool(src, user, 10, volume = 50))
balloon_alert(user, "repaired")
broken = FALSE
icon_state = initial(icon_state)
desc = initial(desc)
return TRUE
/obj/structure/mirror/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0)
switch(damage_type)
if(BRUTE)
playsound(src, 'sound/effects/hit_on_shattered_glass.ogg', 70, TRUE)
if(BURN)
playsound(src, 'sound/effects/hit_on_shattered_glass.ogg', 70, TRUE)
/obj/item/wallframe/mirror
name = "mirror"
desc = "An unmounted mirror. Attach it to a wall to use."
icon = 'icons/obj/watercloset.dmi'
icon_state = "mirror"
custom_materials = list(
/datum/material/glass = SHEET_MATERIAL_AMOUNT,
/datum/material/silver = SHEET_MATERIAL_AMOUNT,
)
result_path = /obj/structure/mirror
pixel_shift = 28
/obj/structure/mirror/magic
name = "magic mirror"
desc = "Turn and face the strange... face."
icon_state = "magic_mirror"
mirror_options = MAGIC_MIRROR_OPTIONS
/obj/structure/mirror/magic/Initialize(mapload)
. = ..()
if(length(selectable_races))
return
for(var/datum/species/species_type as anything in subtypesof(/datum/species))
if(initial(species_type.changesource_flags) & race_flags)
selectable_races[initial(species_type.name)] = species_type
selectable_races = sort_list(selectable_races)
/obj/structure/mirror/magic/change_beard(mob/living/carbon/human/beard_dresser) // magical mirrors do nothing but give you the damn beard
var/new_style = tgui_input_list(beard_dresser, "Select a facial hairstyle", "Grooming", GLOB.facial_hairstyles_list)
if(isnull(new_style))
return TRUE
beard_dresser.set_facial_hairstyle(new_style, update = TRUE)
return TRUE
//Magic mirrors can change hair color as well
/obj/structure/mirror/magic/change_hair(mob/living/carbon/human/user)
var/hairchoice = tgui_alert(user, "Hairstyle or hair color?", "Change Hair", list("Style", "Color"))
if(hairchoice == "Style") //So you just want to use a mirror then?
return ..()
var/new_hair_color = input(user, "Choose your hair color", "Hair Color", user.hair_color) as color|null
if(new_hair_color)
user.set_haircolor(sanitize_hexcolor(new_hair_color), update = FALSE)
user.dna.update_ui_block(DNA_HAIR_COLOR_BLOCK)
if(user.physique == MALE)
var/new_face_color = input(user, "Choose your facial hair color", "Hair Color", user.facial_hair_color) as color|null
if(new_face_color)
user.set_facial_haircolor(sanitize_hexcolor(new_face_color), update = FALSE)
user.dna.update_ui_block(DNA_FACIAL_HAIR_COLOR_BLOCK)
user.update_body_parts()
/obj/structure/mirror/magic/attack_hand(mob/living/carbon/human/user)
. = ..()
if(.)
return TRUE
if(HAS_TRAIT(user, TRAIT_ADVANCEDTOOLUSER) && HAS_TRAIT(user, TRAIT_LITERATE))
return TRUE
to_chat(user, span_alert("You feel quite intelligent."))
// Prevents wizards from being soft locked out of everything
// If this stays after the species was changed once more, well, the magic mirror did it. It's magic i aint gotta explain shit
user.add_traits(list(TRAIT_LITERATE, TRAIT_ADVANCEDTOOLUSER), SPECIES_TRAIT)
return TRUE
/obj/structure/mirror/magic/lesser/Initialize(mapload)
// Roundstart species don't have a flag, so it has to be set on Initialize.
selectable_races = get_selectable_species().Copy()
return ..()
/obj/structure/mirror/magic/badmin
race_flags = MIRROR_BADMIN
/obj/structure/mirror/magic/pride
name = "pride's mirror"
desc = "Pride cometh before the..."
race_flags = MIRROR_PRIDE
mirror_options = PRIDE_MIRROR_OPTIONS
/obj/structure/mirror/magic/pride/attack_hand(mob/living/carbon/human/user)
. = ..()
if(.)
return TRUE
user.visible_message(
span_bolddanger("The ground splits beneath [user] as [user.p_their()] hand leaves the mirror!"),
span_notice("Perfect. Much better! Now <i>nobody</i> will be able to resist yo-"),
)
var/turf/user_turf = get_turf(user)
var/list/levels = SSmapping.levels_by_trait(ZTRAIT_SPACE_RUINS)
var/turf/dest
if(length(levels))
dest = locate(user_turf.x, user_turf.y, pick(levels))
user_turf.ChangeTurf(/turf/open/chasm, flags = CHANGETURF_INHERIT_AIR)
var/turf/open/chasm/new_chasm = user_turf
new_chasm.set_target(dest)
new_chasm.drop(user)
#undef CHANGE_HAIR
#undef CHANGE_BEARD
#undef CHANGE_RACE
#undef CHANGE_SEX
#undef CHANGE_NAME
#undef CHANGE_EYES
#undef INERT_MIRROR_OPTIONS
#undef PRIDE_MIRROR_OPTIONS
#undef MAGIC_MIRROR_OPTIONS