Merge pull request #399 from Sandstorm-Station/automated-sex

Makes the interactions subsystem tick
This commit is contained in:
SandPoot
2025-04-21 04:31:28 -03:00
committed by GitHub
8 changed files with 184 additions and 61 deletions

View File

@@ -34,6 +34,17 @@ GLOBAL_LIST_INIT(lewd_kiss_sounds, list(
'modular_sand/sound/interactions/kiss4.ogg',
'modular_sand/sound/interactions/kiss5.ogg'
))
GLOBAL_LIST_INIT(interaction_speeds, list(
4 SECONDS,
2 SECONDS,
1 SECONDS,
0.8 SECONDS,
0.5 SECONDS, // lowest value must always be over or equal to the subsystem wait/cooldown for interaction
))
#define INTERACTION_NORMAL 0
#define INTERACTION_LEWD 1
#define INTERACTION_EXTREME 2
#define CUM_TARGET_MOUTH "mouth"
#define CUM_TARGET_THROAT "throat"

View File

@@ -1,8 +1,9 @@
SUBSYSTEM_DEF(interactions)
PROCESSING_SUBSYSTEM_DEF(interactions)
name = "Interactions"
flags = SS_NO_FIRE
wait = 0.5 SECONDS
stat_tag = "ACT"
init_order = INIT_ORDER_INTERACTIONS
var/list/interactions
flags = SS_BACKGROUND|SS_POST_FIRE_TIMING
VAR_PROTECTED/list/blacklisted_mobs = list(
/mob/dead,
/mob/dview,
@@ -22,8 +23,9 @@ SUBSYSTEM_DEF(interactions)
/mob/living/simple_animal/hostile/retaliate/goat
)
VAR_PROTECTED/initialized_blacklist
var/list/interactions = list()
/datum/controller/subsystem/interactions/Initialize(timeofday)
/datum/controller/subsystem/processing/interactions/Initialize(timeofday)
prepare_interactions()
prepare_blacklisted_mobs()
. = ..()
@@ -31,13 +33,13 @@ SUBSYSTEM_DEF(interactions)
to_chat(world, span_boldannounce(extra_info))
log_subsystem(src, extra_info)
/datum/controller/subsystem/interactions/stat_entry(msg)
/datum/controller/subsystem/processing/interactions/stat_entry(msg)
msg += "|🖐:[LAZYLEN(interactions)]|"
msg += "🚫👨:[LAZYLEN(blacklisted_mobs)]"
return ..()
/// Makes the interactions, they're also a global list because having it as a list and just hanging around there is stupid
/datum/controller/subsystem/interactions/proc/prepare_interactions()
/datum/controller/subsystem/processing/interactions/proc/prepare_interactions()
QDEL_NULL_LIST(interactions)
interactions = list()
for(var/datum/interaction/interaction as anything in subtypesof(/datum/interaction))
@@ -48,7 +50,7 @@ SUBSYSTEM_DEF(interactions)
interactions["[interaction.type]"] = interaction
/// Blacklisting!
/datum/controller/subsystem/interactions/proc/prepare_blacklisted_mobs()
/datum/controller/subsystem/processing/interactions/proc/prepare_blacklisted_mobs()
blacklisted_mobs = typecacheof(blacklisted_mobs)
initialized_blacklist = TRUE
@@ -56,7 +58,7 @@ SUBSYSTEM_DEF(interactions)
* Lewd interactions have a blacklist for certain mobs. When we evalute the user and target, both of
* their requirements must be satisfied, and the mob must not be of a blacklisted type.
*/
/datum/controller/subsystem/interactions/proc/is_blacklisted(mob/living/creature)
/datum/controller/subsystem/processing/interactions/proc/is_blacklisted(mob/living/creature)
if(!creature || !initialized_blacklist)
return TRUE
if(is_type_in_typecache(creature, blacklisted_mobs))

View File

@@ -15,13 +15,30 @@
return
menu.open_menu(usr, src)
#define INTERACTION_NORMAL 0
#define INTERACTION_LEWD 1
#define INTERACTION_EXTREME 2
/// The menu itself, only var is target which is the mob you are interacting with
/datum/component/interaction_menu_granter
var/mob/living/target
var/mob/living/auto_interaction_target
var/datum/interaction/currently_active_interaction
var/next_interaction_time
var/auto_interaction_pace = 1 SECONDS
/datum/component/interaction_menu_granter/process(delta_time)
if(!currently_active_interaction)
auto_interaction_target = null
currently_active_interaction = null
return PROCESS_KILL
if(QDELETED(auto_interaction_target))
auto_interaction_target = null
currently_active_interaction = null
return PROCESS_KILL
if(world.time <= next_interaction_time)
return
next_interaction_time = world.time + auto_interaction_pace
if(!currently_active_interaction.do_action(parent, auto_interaction_target, apply_cooldown = FALSE))
auto_interaction_target = null
currently_active_interaction = null
return PROCESS_KILL
/datum/component/interaction_menu_granter/Initialize(...)
if(!ismob(parent))
@@ -29,21 +46,24 @@
var/mob/parent_mob = parent
if(!parent_mob.client)
return COMPONENT_INCOMPATIBLE
. = ..()
return ..()
/datum/component/interaction_menu_granter/RegisterWithParent()
. = ..()
RegisterSignal(parent, COMSIG_MOB_CTRLSHIFTCLICKON, PROC_REF(open_menu))
/datum/component/interaction_menu_granter/Destroy(force, ...)
target = null
. = ..()
STOP_PROCESSING(SSinteractions, src)
if(target)
UnregisterSignal(target, COMSIG_PARENT_QDELETING)
target = null
auto_interaction_target = null
currently_active_interaction = null
return ..()
/datum/component/interaction_menu_granter/UnregisterFromParent()
UnregisterSignal(parent, COMSIG_MOB_CTRLSHIFTCLICKON)
if(target)
UnregisterSignal(target, COMSIG_PARENT_QDELETING)
. = ..()
return ..()
/// The one interacting is clicker, the interacted is clicked.
/datum/component/interaction_menu_granter/proc/open_menu(mob/clicker, mob/clicked)
@@ -95,8 +115,9 @@
//Getting player
var/mob/living/self = parent
//Getting info
.["isTargetSelf"] = target == self
.["interactingWith"] = target != self ? "Interacting with \the [target]..." : "Interacting with yourself..."
.["isTargetSelf"] = target == self // Why all of these?
.["user"] = self // Because people may have the same name
.["target"] = target // target == self can distinguish
.["selfAttributes"] = self.list_interaction_attributes(self)
.["lust"] = self.get_lust()
.["maxLust"] = self.get_lust_tolerance() * 3
@@ -340,6 +361,10 @@
if(HAS_TRAIT(user, TRAIT_ESTROUS_DETECT))
.["theirLust"] = target.get_lust()
.["theirMaxLust"] = target.get_lust_tolerance() * 3
.["auto_interaction_pace"] = auto_interaction_pace
.["is_auto_target_self"] = auto_interaction_target == self
.["auto_interaction_target"] = auto_interaction_target
.["currently_active_interaction"] = currently_active_interaction?.type
//Get their genitals
var/list/genitals = list()
@@ -463,6 +488,7 @@
interaction["additionalDetails"] = I.additional_details
sent_interactions += list(interaction)
.["interactions"] = sent_interactions
.["interaction_speeds"] = GLOB.interaction_speeds
/proc/num_to_pref(num)
switch(num)
@@ -480,21 +506,41 @@
switch(action)
if("interact")
var/datum/interaction/o = SSinteractions.interactions[params["interaction"]]
if(o)
o.do_action(parent_mob, target)
if(!o)
return FALSE
if(o == currently_active_interaction)
to_chat(parent_mob, span_notice("This interaction is being automated, sit back, relax or do a different one during it."))
return TRUE
return FALSE
o.do_action(parent_mob, target)
return TRUE
if("interaction_pace")
var/speed = params["speed"]
if(!(speed in GLOB.interaction_speeds))
return FALSE
src.auto_interaction_pace = speed
return TRUE
if("toggle_auto_interaction")
var/datum/interaction/o = SSinteractions.interactions[params["interaction"]]
if(!o || (currently_active_interaction == o) && (auto_interaction_target == target))
auto_interaction_target = null
currently_active_interaction = null
STOP_PROCESSING(SSinteractions, src)
else
auto_interaction_target = target
currently_active_interaction = o
START_PROCESSING(SSinteractions, src)
return TRUE
if("favorite")
var/datum/interaction/interaction = SSinteractions.interactions[params["interaction"]]
if(interaction)
var/datum/preferences/prefs = parent_mob.client.prefs
if(interaction.type in prefs.favorite_interactions)
LAZYREMOVE(prefs.favorite_interactions, interaction.type)
else
LAZYADD(prefs.favorite_interactions, interaction.type)
prefs.save_preferences()
return TRUE
return FALSE
if(!interaction)
return FALSE
var/datum/preferences/prefs = parent_mob.client.prefs
if(interaction.type in prefs.favorite_interactions)
LAZYREMOVE(prefs.favorite_interactions, interaction.type)
else
LAZYADD(prefs.favorite_interactions, interaction.type)
prefs.save_preferences()
return TRUE
if("genital")
var/mob/living/carbon/self = parent_mob
if("visibility" in params)
@@ -627,7 +673,3 @@
return FALSE
prefs.save_preferences()
return TRUE
#undef INTERACTION_NORMAL
#undef INTERACTION_LEWD
#undef INTERACTION_EXTREME

View File

@@ -46,7 +46,7 @@
var/list/additional_details
/// Checks if user can do an interaction, action_check is for whether you're actually doing it or not (useful for the menu and not removing the buttons)
/datum/interaction/proc/evaluate_user(mob/living/user, silent = TRUE, action_check = TRUE)
/datum/interaction/proc/evaluate_user(mob/living/user, silent = TRUE, apply_cooldown = TRUE)
if(SSinteractions.is_blacklisted(user))
return FALSE
@@ -70,7 +70,7 @@
if(COOLDOWN_FINISHED(user, last_interaction_time))
return TRUE
if(action_check)
if(apply_cooldown)
return FALSE
else
return TRUE
@@ -106,21 +106,21 @@
return TRUE
/// Actually doing the action, has a few checks to see if it's valid, usually overwritten to be make things actually happen and what-not
/datum/interaction/proc/do_action(mob/living/user, mob/living/target)
/datum/interaction/proc/do_action(mob/living/user, mob/living/target, apply_cooldown = TRUE)
if(!(interaction_flags & INTERACTION_FLAG_USER_IS_TARGET))
if(user == target) //tactical href fix
to_chat(user, span_warning("You cannot target yourself."))
return
return FALSE
if(get_dist(user, target) > max_distance)
to_chat(user, span_warning("They are too far away."))
return
return FALSE
if(interaction_flags & INTERACTION_FLAG_ADJACENT && !(user.Adjacent(target) && target.Adjacent(user)))
to_chat(user, span_warning("You cannot get to them."))
return
if(!evaluate_user(user, silent = FALSE))
return
return FALSE
if(!evaluate_user(user, silent = FALSE, apply_cooldown = apply_cooldown))
return FALSE
if(!evaluate_target(user, target, silent = FALSE))
return
return FALSE
if(write_log_user)
user.log_message("[write_log_user] [target]", LOG_ATTACK)
@@ -128,7 +128,8 @@
target.log_message("[write_log_target] [user]", LOG_VICTIM, log_globally = FALSE)
display_interaction(user, target)
post_interaction(user, target)
post_interaction(user, target, apply_cooldown)
return TRUE
/// Display the message
/datum/interaction/proc/display_interaction(mob/living/user, mob/living/target)
@@ -138,8 +139,9 @@
user.visible_message("<span class='[simple_style]'>[capitalize(use_message)]</span>")
/// After the interaction, the base only plays the sound and only if it has one
/datum/interaction/proc/post_interaction(mob/living/user, mob/living/target)
COOLDOWN_START(user, last_interaction_time, 0.6 SECONDS)
/datum/interaction/proc/post_interaction(mob/living/user, mob/living/target, apply_cooldown = TRUE)
if(apply_cooldown)
COOLDOWN_START(user, last_interaction_time, 0.5 SECONDS)
if(interaction_sound)
var/soundfile_to_play

View File

@@ -12,7 +12,7 @@
var/user_refractory_cost
var/target_refractory_cost
/datum/interaction/lewd/evaluate_user(mob/living/user, silent = TRUE, action_check = TRUE)
/datum/interaction/lewd/evaluate_user(mob/living/user, silent = TRUE, apply_cooldown = TRUE)
. = ..()
if(!.)
return FALSE
@@ -45,11 +45,11 @@
if(!(has_penis == TRUE))
if((user_require_penis_exposed) && has_penis == HAS_UNEXPOSED_GENITAL)
if(!silent)
to_chat(user, span_warning("Your penis need to be exposed."))
to_chat(user, span_warning("Your penis needs to be exposed."))
return FALSE
if((user_require_penis_unexposed) && has_penis == HAS_EXPOSED_GENITAL)
if(!silent)
to_chat(user, span_warning("Your penis need to be unexposed."))
to_chat(user, span_warning("Your penis needs to be unexposed."))
return FALSE
var/user_require_balls_exposed = !!(required_from_user_exposed & INTERACTION_REQUIRE_BALLS)
@@ -243,8 +243,6 @@
if(interaction_flags & INTERACTION_FLAG_OOC_CONSENT)
if((!user.ckey) || (user.client && user.client.prefs.toggles & VERB_CONSENT))
return TRUE
if(action_check)
return FALSE
return FALSE
/datum/interaction/lewd/evaluate_target(mob/living/user, mob/living/target, silent = TRUE)

View File

@@ -3,7 +3,7 @@ import { BlockQuote, Button, Icon, ProgressBar, Section, Stack, Slider, Tooltip
type HeaderInfo = {
isTargetSelf: boolean;
interactingWith: string;
target: string;
lust: number;
maxLust: number;
selfAttributes: string[];
@@ -22,7 +22,7 @@ export const InfoSection = (props, context) => {
const { act, data } = useBackend<HeaderInfo>(context);
const {
isTargetSelf,
interactingWith,
target,
lust,
maxLust,
selfAttributes,
@@ -37,7 +37,7 @@ export const InfoSection = (props, context) => {
moaning_multiplier,
} = data;
return (
<Section title={interactingWith} fill>
<Section title={`Interacting with ${isTargetSelf ? "yourself" : target}...`} fill>
<Stack vertical fill>
<Stack.Item grow basis={0}>
<Section fill overflow="auto">

View File

@@ -1,5 +1,5 @@
import { useLocalState } from '../../backend';
import { Button, Icon, Input, Section, Tabs, Stack } from '../../components';
import { useBackend, useLocalState } from '../../backend';
import { Button, Icon, Input, Section, Tabs, Stack, Slider } from '../../components';
import {
InteractionsTab,
@@ -8,13 +8,27 @@ import {
ContentPreferencesTab,
} from './tabs';
type MainTypes = {
interaction_speeds: number[];
currently_active_interaction: string;
auto_interaction_pace: number;
auto_interaction_target: string;
is_auto_target_self: boolean;
}
export const MainContent = (props, context) => {
const { act, data } = useBackend<MainTypes>(context);
const [
searchText,
setSearchText,
] = useLocalState(context, 'searchText', '');
const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0);
const [inFavorites, setInFavorites] = useLocalState(context, 'inFavorites', false);
const interaction_speeds = (data.interaction_speeds || []) as number[];
const { auto_interaction_pace, auto_interaction_target, currently_active_interaction, is_auto_target_self } = data;
return (
<Section fill>
<Stack vertical fill>
@@ -59,7 +73,7 @@ export const MainContent = (props, context) => {
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item grow basis={0} mb={-2.3}>
<Stack.Item grow basis={0} mb={tabIndex === 0 ? -1 : -2.3}>
<Section overflow="auto" fill>
{(() => {
switch (tabIndex) {
@@ -75,6 +89,35 @@ export const MainContent = (props, context) => {
})()}
</Section>
</Stack.Item>
{tabIndex === 0 && (
<Stack.Item>
<Stack fill>
{!!currently_active_interaction && (
<Stack.Item>
<Button
icon="stop"
selected
tooltip={`Stop interacting with ${is_auto_target_self ? "yourself" : auto_interaction_target}`}
onClick={() => act("toggle_auto_interaction")}
/>
</Stack.Item>
)}
<Stack.Item grow>
<Slider
fluid
minValue={1}
maxValue={interaction_speeds.length}
value={interaction_speeds.indexOf(auto_interaction_pace) + 1}
format={value => interaction_speeds[value - 1] / 10}
unit="seconds"
stepPixelSize={50}
onChange={(e, value) => act("interaction_pace",
{ speed: interaction_speeds[value - 1] })}
/>
</Stack.Item>
</Stack>
</Stack.Item>
)}
</Stack>
</Section>
);

View File

@@ -9,14 +9,24 @@ type ContentInfo = {
interactions: InteractionData[];
favorite_interactions: string[];
user_is_blacklisted: boolean;
target: string;
target_is_blacklisted: boolean;
currently_active_interaction: string;
is_auto_target_self: boolean;
auto_interaction_target: string;
}
type InteractionData = {
key: string;
desc: string;
type: number;
additionalDetails: string[];
additionalDetails: additionalDetailsContent[];
}
type additionalDetailsContent = {
info: string;
icon: string;
color: string;
}
const INTERACTION_NORMAL = 0;
@@ -49,7 +59,7 @@ export const InteractionsTab = (props, context) => {
? valid_favorites
: interactions;
const { user_is_blacklisted, target_is_blacklisted } = data;
const { auto_interaction_target, currently_active_interaction, is_auto_target_self, user_is_blacklisted, target, target_is_blacklisted } = data;
return (
<Stack vertical>
@@ -58,6 +68,21 @@ export const InteractionsTab = (props, context) => {
interactions_to_display.map((interaction) => (
<Stack.Item key={interaction.key}>
<Stack fill>
{interaction.type !== INTERACTION_NORMAL && (
<Stack.Item>
<Button
key={interaction.key}
icon={(currently_active_interaction === interaction.key) && (auto_interaction_target === target)
? "stop" : "play"}
selected={(currently_active_interaction === interaction.key) && (auto_interaction_target === target)}
tooltip={(currently_active_interaction === interaction.key) && (auto_interaction_target === target)
? `Stop interacting with ${is_auto_target_self ? "yourself" : auto_interaction_target}` : "Automatically repeat this interaction"}
onClick={() => act('toggle_auto_interaction', {
interaction: interaction.key,
})}
/>
</Stack.Item>
)}
<Stack.Item grow>
<Button
key={interaction.key}