mirror of
https://github.com/SPLURT-Station/S.P.L.U.R.T-Station-13.git
synced 2025-12-09 07:48:55 +00:00
Merge pull request #399 from Sandstorm-Station/automated-sex
Makes the interactions subsystem tick
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user