Files
AlexisandGitHub 19f97ffa33 [Nova Port] Updated Interact Panel UI (#5233)
## About The Pull Request
Original PR: https://github.com/NovaSector/NovaSector/pull/6736

<img width="499" height="601" alt="image"
src="https://github.com/user-attachments/assets/05503d48-b021-4d35-9360-8f0c4b0180e3"
/>

## Why It's Good For The Game
The old UI was super outdated.
## Proof Of Testing
See the image above.
## Changelog
🆑 Adeheuer, xPokee
qol: Updated the Interaction Panel UI.
/🆑
2026-02-08 17:20:24 -05:00

293 lines
12 KiB
Plaintext

/datum/component/interactable
/// A hard reference to the parent
var/mob/living/carbon/human/self = null
/// A list of interactions that the user can engage in.
var/list/datum/interaction/interactions
var/interact_last = 0
var/interact_next = 0
///Holds a reference to a relayed body if one exists
var/obj/body_relay = null
/datum/component/interactable/Initialize(...)
if(QDELETED(parent))
qdel(src)
return
if(!ishuman(parent))
return COMPONENT_INCOMPATIBLE
self = parent
build_interactions_list()
/datum/component/interactable/proc/build_interactions_list()
interactions = list()
for(var/iterating_interaction_id in GLOB.interaction_instances)
var/datum/interaction/interaction = GLOB.interaction_instances[iterating_interaction_id]
if(interaction.lewd)
if(!self.client?.prefs?.read_preference(/datum/preference/toggle/erp))
continue
if(interaction.sexuality != "" && interaction.sexuality != self.client?.prefs?.read_preference(/datum/preference/choiced/erp_sexuality))
continue
interactions.Add(interaction)
/datum/component/interactable/RegisterWithParent()
RegisterSignal(parent, COMSIG_CLICK_CTRL_SHIFT, PROC_REF(open_interaction_menu))
/datum/component/interactable/UnregisterFromParent()
UnregisterSignal(parent, COMSIG_CLICK_CTRL_SHIFT)
/datum/component/interactable/Destroy(force, silent)
self = null
interactions = null
return ..()
/datum/component/interactable/proc/open_interaction_menu(datum/source, mob/user)
if(!ishuman(user))
return
build_interactions_list()
ui_interact(user)
/datum/component/interactable/proc/can_interact(datum/interaction/interaction, mob/living/carbon/human/target)
if(!interaction.allow_act(target, self))
return FALSE
if(interaction.lewd && !target.client?.prefs?.read_preference(/datum/preference/toggle/erp))
return FALSE
if(!interaction.distance_allowed && !target.Adjacent(self))
if(!body_relay || !target.Adjacent(body_relay))
return FALSE
if(interaction.category == INTERACTION_CAT_HIDE)
return FALSE
if(self == target && interaction.usage == INTERACTION_OTHER)
return FALSE
return TRUE
/// UI Control
/datum/component/interactable/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "InteractionPanel")
ui.open()
/datum/component/interactable/ui_status(mob/user, datum/ui_state/state)
if(!ishuman(user))
return UI_CLOSE
return UI_INTERACTIVE // This UI is always interactive as we handle distance flags via can_interact
/datum/component/interactable/ui_static_data(mob/user)
var/list/data = list()
data["arousalLimit"] = AROUSAL_LIMIT
return data
/datum/component/interactable/ui_data(mob/user)
var/list/data = list()
var/list/descriptions = list()
var/list/categories = list()
var/list/display_categories = list()
var/list/colors = list()
for(var/datum/interaction/interaction in interactions)
if(!can_interact(interaction, user))
continue
if(!categories[interaction.category])
categories[interaction.category] = list(interaction.name)
else
categories[interaction.category] += interaction.name
var/list/sorted_category = sort_list(categories[interaction.category])
categories[interaction.category] = sorted_category
descriptions[interaction.name] = interaction.description
colors[interaction.name] = interaction.color
data["descriptions"] = descriptions
data["colors"] = colors
for(var/category in categories)
display_categories += category
data["categories"] = sort_list(display_categories)
data["ref_user"] = REF(user)
data["ref_self"] = REF(self)
data["self"] = self.name
if(body_relay)
if(!can_see(user, self))
data["self"] = body_relay.name
data["block_interact"] = interact_next >= world.time
data["interactions"] = categories
data["erp_interaction"] = self.client?.prefs?.read_preference(/datum/preference/toggle/erp)
var/mob/living/carbon/human/human_user = user
data["isTargetSelf"] = (user == self)
// user (the one who opened the ui)
var/user_pleasure = 0
var/user_arousal = 0
var/user_pain = 0
if(user)
user_pleasure = human_user.pleasure
user_arousal = human_user.arousal
user_pain = human_user.pain
data["pleasure"] = user_pleasure
data["arousal"] = user_arousal
data["pain"] = user_pain
data["yourName"] = human_user.real_name
// self - the one who the interaction component belongs to, aka who it's opened on (confusing var name yep)
if(user != self)
data["theirPleasure"] = self.pleasure
data["theirArousal"] = self.arousal
data["theirPain"] = self.pain
data["theirName"] = self.real_name
var/list/parts = list()
if(ishuman(user) && can_lewd_strip(user, self))
if(self.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
if(self.has_vagina())
parts += list(generate_strip_entry(ORGAN_SLOT_VAGINA, self, user, self.vagina))
if(self.has_penis())
parts += list(generate_strip_entry(ORGAN_SLOT_PENIS, self, user, self.penis))
if(self.has_anus())
parts += list(generate_strip_entry(ORGAN_SLOT_ANUS, self, user, self.anus))
parts += list(generate_strip_entry(ORGAN_SLOT_NIPPLES, self, user, self.nipples))
data["lewd_slots"] = parts
return data
/**
* Takes the organ slot name, along with a target and source, along with the item on the target that the source can potentially interact with.
* If the source can't interact with said slot, or there is no item in the first place, it'll set the icon to null to indicate that TGUI should put a placeholder sprite.
*
* Arguments:
* * name - The name of slot to check and return inside the generated list.
* * target - The mob that's being interacted with.
* * source - The mob that's interacting.
* * item - The item that's currently inside said slot. Can be null.
*/
/datum/component/interactable/proc/generate_strip_entry(name, mob/living/carbon/human/target, mob/living/carbon/human/source, obj/item/clothing/sextoy/item)
return list(
"name" = name,
"img" = (item && can_lewd_strip(source, target, name)) ? icon2base64(icon(item.icon, item.icon_state, SOUTH, 1)) : null
)
/datum/component/interactable/ui_act(action, list/params)
. = ..()
if(.)
return
if(!ishuman(usr))
return
if(params["interaction"])
var/interaction_id = params["interaction"]
if(GLOB.interaction_instances[interaction_id])
var/mob/living/carbon/human/user = locate(params["userref"])
if(!can_interact(GLOB.interaction_instances[interaction_id], user))
return FALSE
if(body_relay && !can_see(user, self))
GLOB.interaction_instances[interaction_id].act(user, locate(params["selfref"]), body_relay)
else
GLOB.interaction_instances[interaction_id].act(user, locate(params["selfref"]))
var/datum/component/interactable/interaction_component = user.GetComponent(/datum/component/interactable)
interaction_component.interact_last = world.time
interact_next = interaction_component.interact_last + INTERACTION_COOLDOWN
interaction_component.interact_next = interact_next
return TRUE
if(params["item_slot"])
// This code should be easy enough to follow... I hope.
var/item_index = params["item_slot"]
var/mob/living/carbon/human/source = locate(params["userref"])
var/mob/living/carbon/human/target = locate(params["selfref"])
var/obj/item/clothing/sextoy/new_item = source.get_active_held_item()
var/obj/item/clothing/sextoy/existing_item = target.vars[item_index]
if(!existing_item && !new_item)
source.show_message(span_warning("No item to insert or remove!"))
return
if(!existing_item && !istype(new_item))
source.show_message(span_warning("The item you're holding is not a toy!"))
return
if(can_lewd_strip(source, target, item_index) && is_toy_compatible(new_item, item_index))
var/internal = (item_index in list(ORGAN_SLOT_VAGINA, ORGAN_SLOT_ANUS))
var/insert_or_attach = internal ? "insert" : "attach"
var/into_or_onto = internal ? "into" : "onto"
if(existing_item)
source.visible_message(span_purple("[source.name] starts trying to remove something from [target.name]'s [item_index]."), span_purple("You start to remove [existing_item.name] from [target.name]'s [item_index]."), span_purple("You hear someone trying to remove something from someone nearby."), vision_distance = 1, ignored_mobs = list(target))
else if (new_item)
source.visible_message(span_purple("[source.name] starts trying to [insert_or_attach] the [new_item.name] [into_or_onto] [target.name]'s [item_index]."), span_purple("You start to [insert_or_attach] the [new_item.name] [into_or_onto] [target.name]'s [item_index]."), span_purple("You hear someone trying to [insert_or_attach] something [into_or_onto] someone nearby."), vision_distance = 1, ignored_mobs = list(target))
if (source != target)
target.show_message(span_warning("[source.name] is trying to [existing_item ? "remove the [existing_item.name] [internal ? "in" : "on"]" : new_item ? "is trying to [insert_or_attach] the [new_item.name] [into_or_onto]" : span_alert("What the fuck, impossible condition? interaction_component.dm!")] your [item_index]!"))
if(do_after(
source,
5 SECONDS,
target,
interaction_key = "interaction_[item_index]"
) && can_lewd_strip(source, target, item_index))
if(existing_item)
source.visible_message(span_purple("[source.name] removes [existing_item.name] from [target.name]'s [item_index]."), span_purple("You remove [existing_item.name] from [target.name]'s [item_index]."), span_purple("You hear someone remove something from someone nearby."), vision_distance = 1)
target.dropItemToGround(existing_item, force = TRUE) // Force is true, cause nodrop shouldn't affect lewd items.
target.vars[item_index] = null
else if (new_item)
source.visible_message(span_purple("[source.name] [internal ? "inserts" : "attaches"] the [new_item.name] [into_or_onto] [target.name]'s [item_index]."), span_purple("You [insert_or_attach] the [new_item.name] [into_or_onto] [target.name]'s [item_index]."), span_purple("You hear someone [insert_or_attach] something [into_or_onto] someone nearby."), vision_distance = 1)
target.vars[item_index] = new_item
new_item.forceMove(target)
new_item.lewd_equipped(target, item_index)
target.update_inv_lewd()
else
source.show_message(span_warning("Failed to adjust [target.name]'s toys!"))
return TRUE
message_admins("Unhandled interaction '[params["interaction"]]'. Inform coders.")
/// Checks if the target has ERP toys enabled, and can be logially reached by the user.
/datum/component/interactable/proc/can_lewd_strip(mob/living/carbon/human/source, mob/living/carbon/human/target, slot_index)
if(!target.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
return FALSE
if(!(source.loc == target.loc || source.Adjacent(target)))
return FALSE
if(!source.has_arms())
return FALSE
if(!slot_index) // This condition is for the UI to decide if the button is shown at all. Slot index should never be null otherwise.
return TRUE
switch(slot_index)
if(ORGAN_SLOT_NIPPLES)
var/chest_exposed = target.has_breasts(required_state = REQUIRE_GENITAL_EXPOSED)
if(!chest_exposed)
chest_exposed = target.is_topless() // for when we don't have breasts
return chest_exposed
if(ORGAN_SLOT_PENIS)
return target.has_penis(required_state = REQUIRE_GENITAL_EXPOSED)
if(ORGAN_SLOT_VAGINA)
return target.has_vagina(required_state = REQUIRE_GENITAL_EXPOSED)
if(ORGAN_SLOT_ANUS)
return target.has_anus(required_state = REQUIRE_GENITAL_EXPOSED)
/// Decides if a player should be able to insert or remove an item from a provided lewd slot_index.
/datum/component/interactable/proc/is_toy_compatible(obj/item/clothing/sextoy/item, slot_index)
if(!item) // Used for UI code, should never be actually null during actual logic code.
return TRUE
switch(slot_index)
if(ORGAN_SLOT_VAGINA)
return item.lewd_slot_flags & LEWD_SLOT_VAGINA
if(ORGAN_SLOT_PENIS)
return item.lewd_slot_flags & LEWD_SLOT_PENIS
if(ORGAN_SLOT_ANUS)
return item.lewd_slot_flags & LEWD_SLOT_ANUS
if(ORGAN_SLOT_NIPPLES)
return item.lewd_slot_flags & LEWD_SLOT_NIPPLES
else
return FALSE