Files
SyncIt21 f640a0e972 Enhances algorithm for finding an atom mount (#94076)
## About The Pull Request
Depends on #94064 for the unit test but offers a better method for
finding an atom to mount on
- Finding a mount now takes into consideration the objects pixel x & y
offsets meaning diagonal mounting is now supported. Gives great
flexibility for mappers
- If you don't want to use pixel offsets but default back to using the
objects direction that behaviour is still preserved. Useful if your
object uses directional icon states(lights & cameras for now) AND don't
use offsets
- If no direction could be specified then as the last resort it defaults
back to the objects local turf for mounting

## Changelog
🆑
fix: all mounted objects on tables, fences, windows & walls should fall
of correctly when the atom it is placed on is destroyed
fix: security telescreen now falls off when their mounted wall is
destroyed
fix: defib wall mount falls off when their mounted wall is destroyed
fix: floor lights are mounted to the ground/catwalk/tram floor they are
sitting on meaning destroying it will destroy the light
fix: wall mounted plaques now fall off when their mounted wall is
destroyed
/🆑
2025-12-12 12:23:22 -05:00

212 lines
7.9 KiB
Plaintext

/obj/item/wallframe/fish
name = "fish mount"
desc = "The frame of a mount for trophy fish, to show off your proudest catch."
icon = 'icons/obj/wallmounts.dmi'
icon_state = "fish_mount_item"
result_path = /obj/structure/fish_mount
custom_materials = list(/datum/material/wood = SHEET_MATERIAL_AMOUNT * 2)
pixel_shift = 31
///Reference to the persistent_id of the mount this was spawned from.
var/persistence_id
/obj/item/wallframe/fish/after_attach(obj/structure/fish_mount/mount)
. = ..()
mount.persistence_id = persistence_id
///A wallmounted structure on which a fish can be attached to be used as room decoration.
/obj/structure/fish_mount
name = "fish mount"
desc = "A mount for trophy fish, to show off your proudest catch."
icon = 'icons/obj/wallmounts.dmi'
icon_state = "fish_mount"
anchored = TRUE
opacity = FALSE
density = FALSE
layer = SIGN_LAYER
max_integrity = 100
custom_materials = list(/datum/material/wood = SHEET_MATERIAL_AMOUNT)
armor_type = /datum/armor/structure_sign
resistance_flags = FLAMMABLE
/// The instance of the fish mounted on this.
var/obj/item/fish/mounted_fish
/// The identifier for mounts that carry the trophy between rounds.
var/persistence_id
/// Trophies from persistence have a good chance to be dusted if removal is attempted, though rarely it pays off.
var/persistence_loaded_fish = FALSE
/obj/structure/fish_mount/Initialize(mapload)
. = ..()
//Mounted fish shouldn't flop. It should also show size and weight to everyone.
add_traits(list(TRAIT_STOP_FISH_FLOPPING, TRAIT_EXAMINE_FISH), INNATE_TRAIT)
if(mapload)
find_and_mount_on_atom()
if(!persistence_id)
return
if(SSfishing.initialized)
load_trophy_fish()
else
RegisterSignal(SSfishing, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(load_trophy_fish))
/obj/structure/fish_mount/Destroy(force)
QDEL_NULL(mounted_fish)
return ..()
/obj/structure/fish_mount/proc/load_trophy_fish(datum/source)
SIGNAL_HANDLER
SSpersistence.load_trophy_fish(src)
UnregisterSignal(SSfishing, COMSIG_SUBSYSTEM_POST_INITIALIZE)
if(!mounted_fish)
add_first_fish()
/obj/structure/fish_mount/screwdriver_act(mob/living/user, obj/item/item)
. = ..()
balloon_alert(user, "removing mount...")
if(!item.use_tool(src, user, 3 SECONDS, volume = 50))
return ITEM_INTERACT_BLOCKING
playsound(src, 'sound/items/deconstruct.ogg', 50, TRUE)
balloon_alert_to_viewers("fish mount removed")
deconstruct()
return ITEM_INTERACT_SUCCESS
/obj/structure/fish_mount/proc/add_first_fish()
var/list/valid_picks = subtypesof(/obj/item/fish) - typesof(/obj/item/fish/holo) - /obj/item/fish/starfish/chrystarfish // chrystarfish immediately shatters when placed
var/obj/item/fish/fish_path = pick(valid_picks)
if(fish_path.fish_id_redirect_path)
fish_path = fish_path.fish_id_redirect_path
var/fluff_name = pick("John Trasen III", "a nameless intern", "Pun Pun", AQUARIUM_COMPANY, "Unknown", "Central Command")
add_fish(new fish_path(loc), from_persistence = TRUE, catcher = fluff_name)
mounted_fish.randomize_size_and_weight()
mounted_fish.set_status(FISH_DEAD)
SSpersistence.save_trophy_fish(src)
/obj/structure/fish_mount/atom_deconstruct(disassembled = TRUE)
. = ..()
if(disassembled)
var/obj/item/wallframe/fish/frame = new (loc)
frame.persistence_id = persistence_id
mounted_fish?.forceMove(loc)
/obj/structure/fish_mount/item_interaction(mob/living/user, obj/item/item, list/modifiers)
if(!isfish(item) || user.combat_mode)
return ..()
if(mounted_fish)
balloon_alert(user, "remove other fish first!")
return ITEM_INTERACT_BLOCKING
if(item.flags_1 & HOLOGRAM_1)
balloon_alert(user, "fish not mountable!")
return ITEM_INTERACT_BLOCKING
balloon_alert(user, "mounting fish...")
if(!do_after(user, 3 SECONDS, src) || mounted_fish)
return ITEM_INTERACT_BLOCKING
add_fish(item, catcher = user.name)
balloon_alert_to_viewers("fish mounted")
playsound(loc, 'sound/machines/click.ogg', 30, TRUE)
return ITEM_INTERACT_SUCCESS
/obj/structure/fish_mount/IsContainedAtomAccessible(atom/contained, atom/movable/user)
return TRUE
/obj/structure/fish_mount/proc/add_fish(obj/item/fish/fish, from_persistence = FALSE, catcher)
if(QDELETED(src)) // don't ever try to add a fish to one of these that's already been deleted - and get rid of the one that was created
qdel(fish)
return
if(QDELETED(fish)) // no adding deleted fishies either
return
if(mounted_fish)
mounted_fish.forceMove(loc)
fish.forceMove(src)
vis_contents += fish
fish.vis_flags |= (VIS_INHERIT_PLANE|VIS_INHERIT_LAYER)
fish.interaction_flags_item &= ~INTERACT_ITEM_ATTACK_HAND_PICKUP
fish.obj_flags &= ~UNIQUE_RENAME
fish.remove_fillet_type()
fish.anchored = TRUE
mounted_fish = fish
if(!fish.catcher_name)
fish.catcher_name = catcher
if(!fish.catch_date)
fish.catch_date = "[time2text(world.realtime, "Day, Month DD", NO_TIMEZONE)], [CURRENT_STATION_YEAR]"
AddElement(/datum/element/beauty, get_fish_beauty())
RegisterSignals(fish, list(COMSIG_ATOM_ATTACK_HAND, COMSIG_ATOM_ATTACK_PAW), PROC_REF(on_fish_attack_hand))
if(from_persistence)
persistence_loaded_fish = TRUE
fish.add_traits(list(TRAIT_NO_FISHING_ACHIEVEMENT, TRAIT_FISH_LOW_PRICE), INNATE_TRAIT)
else if(persistence_id)
SSpersistence.save_trophy_fish(src)
/obj/structure/fish_mount/proc/get_fish_beauty()
var/beauty = 100 + mounted_fish.beauty * 1.2
var/datum/material/main_material = mounted_fish.get_master_material()
if(main_material)
beauty += main_material.beauty_modifier * mounted_fish.weight
return round(beauty)
/obj/structure/fish_mount/proc/on_fish_attack_hand(datum/source, mob/living/user)
SIGNAL_HANDLER
INVOKE_ASYNC(src, PROC_REF(remove_fish), user)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/obj/structure/fish_mount/attack_hand_secondary(mob/living/user, list/modifiers)
. = ..()
if(!mounted_fish)
balloon_alert(user, "no fish mounted!")
else
remove_fish(user)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/obj/structure/fish_mount/proc/remove_fish(mob/living/user)
balloon_alert(user, "removing fish...")
if(!do_after(user, (persistence_loaded_fish ? 6 : 3) SECONDS, src) || !mounted_fish)
return
var/obj/item/fish/fish_reference = mounted_fish
if(persistence_loaded_fish)
roll_for_safe_removal(user)
if(QDELETED(fish_reference))
var/ash_type = /obj/effect/decal/cleanable/ash
if(fish_reference.w_class >= WEIGHT_CLASS_BULKY)
ash_type = /obj/effect/decal/cleanable/ash/large
new ash_type(loc)
visible_message("[fish_reference] turns into dust as [fish_reference.p_theyre()] removed from [src].")
else
user.put_in_hands(mounted_fish)
balloon_alert_to_viewers("fish removed")
/obj/structure/fish_mount/Exited(atom/movable/gone)
if(gone != mounted_fish)
return ..()
RemoveElement(/datum/element/beauty, get_fish_beauty())
if(!QDELETED(mounted_fish) && (!persistence_loaded_fish || roll_for_safe_removal()))
UnregisterSignal(mounted_fish, list(COMSIG_ATOM_ATTACK_HAND, COMSIG_ATOM_ATTACK_PAW))
mounted_fish.vis_flags &= ~(VIS_INHERIT_PLANE|VIS_INHERIT_LAYER)
mounted_fish.interaction_flags_item |= INTERACT_ITEM_ATTACK_HAND_PICKUP
mounted_fish.obj_flags |= UNIQUE_RENAME
mounted_fish.add_fillet_type()
mounted_fish.anchored = FALSE
persistence_loaded_fish = FALSE
mounted_fish = null
return ..()
/obj/structure/fish_mount/proc/roll_for_safe_removal(mob/living/user)
if(isnull(mounted_fish))
return FALSE
///the base success rate is calculated considering the item inventory size and the heaviness of the fish.
var/success_prob = 100/(mounted_fish.w_class + GET_FISH_WEIGHT_RANK(mounted_fish.weight))
var/fishing_prowess = user?.mind?.get_skill_level(/datum/skill/fishing)
success_prob += fishing_prowess * 4 // up to 28% fixed bonus chance to safely retrieve the trophy depending on skill.
if(!prob(success_prob))
qdel(mounted_fish)
return FALSE
persistence_loaded_fish = FALSE //this way we don't roll again on Exited()
return TRUE
/obj/structure/fish_mount/bar
persistence_id = "Bar"
MAPPING_DIRECTIONAL_HELPERS(/obj/structure/fish_mount/bar, /obj/item/wallframe/fish::pixel_shift)