mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-24 13:38:41 +01:00
## 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. /🆑
293 lines
12 KiB
Plaintext
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
|