mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-26 14:39:58 +01:00
[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. /🆑
This commit is contained in:
@@ -67,7 +67,7 @@
|
||||
/datum/component/interactable/ui_interact(mob/user, datum/tgui/ui)
|
||||
ui = SStgui.try_update_ui(user, src, ui)
|
||||
if(!ui)
|
||||
ui = new(user, src, "InteractionMenu")
|
||||
ui = new(user, src, "InteractionPanel")
|
||||
ui.open()
|
||||
|
||||
/datum/component/interactable/ui_status(mob/user, datum/ui_state/state)
|
||||
@@ -76,6 +76,11 @@
|
||||
|
||||
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()
|
||||
@@ -106,6 +111,34 @@
|
||||
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()
|
||||
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
// THIS IS A SKYRAT UI FILE
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Collapsible,
|
||||
Icon,
|
||||
NoticeBox,
|
||||
Section,
|
||||
Stack,
|
||||
} from 'tgui-core/components';
|
||||
|
||||
import { useBackend } from '../backend';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
class Interaction {
|
||||
categories;
|
||||
interactions;
|
||||
descriptions;
|
||||
colors;
|
||||
lewd_slots: LewdSlot[];
|
||||
self;
|
||||
ref_self;
|
||||
ref_user;
|
||||
block_interact;
|
||||
}
|
||||
|
||||
class LewdSlot {
|
||||
img;
|
||||
name;
|
||||
}
|
||||
|
||||
export const InteractionMenu = (props) => {
|
||||
const { act, data } = useBackend<Interaction>();
|
||||
const {
|
||||
categories = [],
|
||||
interactions,
|
||||
descriptions,
|
||||
colors,
|
||||
lewd_slots,
|
||||
self,
|
||||
ref_self,
|
||||
ref_user,
|
||||
block_interact,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<Window width={500} height={600} title={`Interact - ${self}`}>
|
||||
<Window.Content scrollable>
|
||||
{(block_interact && <NoticeBox>Unable to Interact</NoticeBox>) || (
|
||||
<NoticeBox>Able to Interact</NoticeBox>
|
||||
)}
|
||||
<Stack vertical>
|
||||
<Section key="interactions">
|
||||
{categories.map((category) => (
|
||||
<Collapsible key={category} title={category}>
|
||||
<Section fill>
|
||||
<Box mt={0.2}>
|
||||
{interactions[category].map((interaction) => (
|
||||
<Button
|
||||
key={interaction}
|
||||
width="150.5px"
|
||||
lineHeight={1.75}
|
||||
disabled={block_interact}
|
||||
color={block_interact ? 'grey' : colors[interaction]}
|
||||
content={interaction}
|
||||
tooltip={descriptions[interaction]}
|
||||
icon="exclamation-circle"
|
||||
onClick={() =>
|
||||
act('interact', {
|
||||
interaction: interaction,
|
||||
selfref: ref_self,
|
||||
userref: ref_user,
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Section>
|
||||
</Collapsible>
|
||||
))}
|
||||
</Section>
|
||||
{lewd_slots.length > 0 ? (
|
||||
<Section key="item_slots" title={'Lewd Slots'}>
|
||||
<Stack fill>
|
||||
{lewd_slots.map((element: LewdSlot) => {
|
||||
return (
|
||||
<Stack.Item key={element.name}>
|
||||
<Button
|
||||
onClick={() =>
|
||||
act('remove_lewd_item', {
|
||||
item_slot: element.name,
|
||||
selfref: ref_self,
|
||||
userref: ref_user,
|
||||
})
|
||||
}
|
||||
color="pink"
|
||||
tooltip={element.name}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
margin: '0.5em 0',
|
||||
}}
|
||||
>
|
||||
{element.img ? (
|
||||
<img
|
||||
src={`data:image/png;base64,${element.img}`}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
name="eye-slash"
|
||||
size={2}
|
||||
ml={0}
|
||||
mt={0.75}
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Button>
|
||||
</Stack.Item>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Section>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Stack>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
// THIS IS A BUBBER UI FILE
|
||||
import { Icon, ProgressBar, Section, Stack } from 'tgui-core/components';
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
|
||||
import { useBackend } from '../../backend';
|
||||
|
||||
type HeaderInfo = {
|
||||
isTargetSelf: BooleanLike;
|
||||
pleasure: number;
|
||||
arousal: number;
|
||||
pain: number;
|
||||
theirPleasure: number;
|
||||
theirArousal: number;
|
||||
theirPain: number;
|
||||
arousalLimit: number;
|
||||
theirName: string;
|
||||
yourName: string;
|
||||
};
|
||||
|
||||
export const InfoSection = () => {
|
||||
const { data } = useBackend<HeaderInfo>();
|
||||
const {
|
||||
isTargetSelf,
|
||||
pleasure,
|
||||
arousal,
|
||||
pain,
|
||||
theirPleasure,
|
||||
theirArousal,
|
||||
theirPain,
|
||||
yourName,
|
||||
theirName,
|
||||
arousalLimit,
|
||||
} = data;
|
||||
return (
|
||||
<Section fill>
|
||||
<Stack vertical fill>
|
||||
<Stack.Item grow>
|
||||
<Stack>
|
||||
<Stack.Item grow>{yourName}</Stack.Item>
|
||||
{!isTargetSelf ? <Stack.Item grow>{theirName}</Stack.Item> : null}
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Stack fill>
|
||||
<Stack.Item grow>
|
||||
<Stack vertical>
|
||||
<Stack.Item>
|
||||
<ProgressBar
|
||||
value={pleasure}
|
||||
maxValue={arousalLimit}
|
||||
color="purple"
|
||||
>
|
||||
<Icon name="heart" /> Pleasure
|
||||
</ProgressBar>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<ProgressBar
|
||||
value={arousal}
|
||||
maxValue={arousalLimit}
|
||||
color="pink"
|
||||
>
|
||||
<Icon name="tint" /> Arousal
|
||||
</ProgressBar>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<ProgressBar value={pain} maxValue={arousalLimit} color="red">
|
||||
<Icon name="bolt" /> Pain
|
||||
</ProgressBar>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
{!isTargetSelf ? (
|
||||
<Stack.Item grow>
|
||||
<Stack vertical>
|
||||
<Stack.Item>
|
||||
<ProgressBar
|
||||
value={theirPleasure}
|
||||
maxValue={arousalLimit}
|
||||
color="purple"
|
||||
>
|
||||
<Icon name="heart" /> Pleasure
|
||||
</ProgressBar>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<ProgressBar
|
||||
value={theirArousal}
|
||||
maxValue={arousalLimit}
|
||||
color="pink"
|
||||
>
|
||||
<Icon name="tint" /> Arousal
|
||||
</ProgressBar>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<ProgressBar
|
||||
value={theirPain}
|
||||
maxValue={arousalLimit}
|
||||
color="red"
|
||||
>
|
||||
<Icon name="bolt" /> Pain
|
||||
</ProgressBar>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
// THIS IS A BUBBER UI FILE
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
Input,
|
||||
Section,
|
||||
Stack,
|
||||
Tabs,
|
||||
} from 'tgui-core/components';
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
import { useBackend } from '../../backend';
|
||||
|
||||
type Interaction = {
|
||||
erp_interaction: BooleanLike;
|
||||
};
|
||||
|
||||
import { InteractionsTab, LewdItemsTab } from './tabs';
|
||||
|
||||
export const MainContent = () => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
const [showCategories, setShowCategories] = useState(true);
|
||||
const { data } = useBackend<Interaction>();
|
||||
const { erp_interaction } = data;
|
||||
const placeholder =
|
||||
tabIndex === 0
|
||||
? 'Search for an interaction'
|
||||
: tabIndex === 1
|
||||
? 'Search for an item'
|
||||
: 'Searching is unavailable for this tab';
|
||||
|
||||
return (
|
||||
<Section fill>
|
||||
<Stack vertical fill>
|
||||
<Stack.Item>
|
||||
<Tabs fluid textAlign="center">
|
||||
<Tabs.Tab selected={tabIndex === 0} onClick={() => setTabIndex(0)}>
|
||||
Interactions
|
||||
</Tabs.Tab>
|
||||
{erp_interaction && (
|
||||
<Tabs.Tab
|
||||
selected={tabIndex === 1}
|
||||
onClick={() => setTabIndex(1)}
|
||||
>
|
||||
Lewd Items
|
||||
</Tabs.Tab>
|
||||
)}
|
||||
</Tabs>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Stack align="baseline" fill>
|
||||
<Stack.Item>
|
||||
<Icon name="search" />
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Input
|
||||
fluid
|
||||
value={searchText}
|
||||
placeholder={placeholder}
|
||||
onChange={(value) => setSearchText(value)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
{tabIndex === 0 && (
|
||||
<Stack.Item>
|
||||
<Button
|
||||
icon={showCategories ? 'folder' : 'list'}
|
||||
color="green"
|
||||
tooltip={
|
||||
showCategories ? 'Hide Categories' : 'Show Categories'
|
||||
}
|
||||
onClick={() => setShowCategories(!showCategories)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow mb={-1.6}>
|
||||
<Section fill>
|
||||
{tabIndex === 1 ? (
|
||||
<LewdItemsTab searchText={searchText} />
|
||||
) : (
|
||||
<InteractionsTab
|
||||
searchText={searchText}
|
||||
showCategories={showCategories}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
// THIS IS A NOVA SECTOR UI FILE
|
||||
import { Section, Stack } from 'tgui-core/components';
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
import { useBackend } from '../../backend';
|
||||
import { Window } from '../../layouts';
|
||||
import { InfoSection } from './InfoSection';
|
||||
import { MainContent } from './MainContent';
|
||||
|
||||
type Interaction = {
|
||||
self;
|
||||
erp_interaction: BooleanLike;
|
||||
};
|
||||
|
||||
export function InteractionPanel() {
|
||||
const { act, data } = useBackend<Interaction>();
|
||||
const { self, erp_interaction } = data;
|
||||
|
||||
return (
|
||||
<Window width={500} height={600} title={`Interact - ${self}`}>
|
||||
<Window.Content scrollable>
|
||||
{erp_interaction && (
|
||||
<Section>
|
||||
<Stack vertical fill>
|
||||
<Stack.Item grow>
|
||||
<InfoSection />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Stack>
|
||||
<Stack.Item grow>
|
||||
<MainContent />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// THIS IS A BUBBER UI FILE
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Collapsible,
|
||||
NoticeBox,
|
||||
Section,
|
||||
Stack,
|
||||
} from 'tgui-core/components';
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
|
||||
import { useBackend } from '../../../backend';
|
||||
|
||||
type Interaction = {
|
||||
categories: string[];
|
||||
interactions: Record<string, string[]>;
|
||||
descriptions: Record<string, string>;
|
||||
colors: Record<string, string>;
|
||||
self: string;
|
||||
ref_self: string;
|
||||
ref_user: string;
|
||||
block_interact: BooleanLike;
|
||||
};
|
||||
|
||||
type InteractionsTabProps = {
|
||||
searchText: string;
|
||||
showCategories: boolean;
|
||||
};
|
||||
|
||||
export const InteractionsTab = (props: InteractionsTabProps) => {
|
||||
const { act, data } = useBackend<Interaction>();
|
||||
const {
|
||||
categories = [],
|
||||
interactions = {},
|
||||
descriptions = {},
|
||||
colors = {},
|
||||
ref_self,
|
||||
ref_user,
|
||||
block_interact,
|
||||
} = data;
|
||||
const { searchText, showCategories } = props;
|
||||
|
||||
const searchLower = searchText.toLowerCase();
|
||||
|
||||
const renderInteractionButton = (interaction: string) => {
|
||||
return (
|
||||
<Button
|
||||
key={interaction}
|
||||
width="150px"
|
||||
lineHeight={1.75}
|
||||
disabled={block_interact}
|
||||
color={block_interact ? 'grey' : colors[interaction]}
|
||||
tooltip={
|
||||
block_interact
|
||||
? 'You cannot interact right now'
|
||||
: descriptions[interaction]
|
||||
}
|
||||
icon="exclamation-circle"
|
||||
onClick={() =>
|
||||
act('interact', {
|
||||
interaction: interaction,
|
||||
selfref: ref_self,
|
||||
userref: ref_user,
|
||||
})
|
||||
}
|
||||
>
|
||||
{interaction}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const filterInteractions = (category: string) => {
|
||||
let categoryInteractions = interactions[category] || [];
|
||||
if (searchText) {
|
||||
categoryInteractions = categoryInteractions.filter((interaction) =>
|
||||
interaction.toLowerCase().includes(searchLower),
|
||||
);
|
||||
}
|
||||
return categoryInteractions;
|
||||
};
|
||||
|
||||
const allInteractions = useMemo(() => {
|
||||
return categories.flatMap((category) =>
|
||||
filterInteractions(category).map((interaction) => ({
|
||||
name: interaction,
|
||||
category,
|
||||
})),
|
||||
);
|
||||
}, [categories, searchLower]);
|
||||
|
||||
return (
|
||||
<Stack fill vertical>
|
||||
<NoticeBox>
|
||||
{block_interact ? 'Unable to Interact' : 'Able to Interact'}
|
||||
</NoticeBox>
|
||||
<Stack.Item grow>
|
||||
{showCategories ? (
|
||||
categories.map((category) => {
|
||||
const filteredInteractions = filterInteractions(category);
|
||||
if (filteredInteractions.length === 0) return null;
|
||||
return (
|
||||
<Collapsible
|
||||
key={category}
|
||||
title={category}
|
||||
buttons={
|
||||
<Box inline color="grey" fontSize={0.9}>
|
||||
{filteredInteractions.length}
|
||||
{' interactions'}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Section fill>
|
||||
<Box mt={0.2}>
|
||||
{filteredInteractions.map((interaction) =>
|
||||
renderInteractionButton(interaction),
|
||||
)}
|
||||
</Box>
|
||||
</Section>
|
||||
</Collapsible>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Section fill>
|
||||
<Box mt={0.2}>
|
||||
{allInteractions.map(({ name, category }) =>
|
||||
renderInteractionButton(name),
|
||||
)}
|
||||
</Box>
|
||||
</Section>
|
||||
)}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
// THIS IS A BUBBER UI FILE
|
||||
import { Box, Button, Icon, Stack } from 'tgui-core/components';
|
||||
|
||||
import { useBackend } from '../../../backend';
|
||||
|
||||
type LewdSlot = {
|
||||
img: string;
|
||||
name: string;
|
||||
item_name: string;
|
||||
lewd_slots: LewdSlot[];
|
||||
ref_self: string;
|
||||
ref_user: string;
|
||||
};
|
||||
|
||||
type LewdItemsTabPropsData = {
|
||||
searchText: string;
|
||||
};
|
||||
|
||||
export const LewdItemsTab = (props: LewdItemsTabPropsData) => {
|
||||
const { act, data } = useBackend<LewdSlot>();
|
||||
const { lewd_slots = [], ref_self, ref_user } = data;
|
||||
const { searchText } = props;
|
||||
const searchLower = searchText.toLowerCase();
|
||||
|
||||
const filteredSlots = lewd_slots.filter((slot) => {
|
||||
return (
|
||||
slot.name.toLowerCase().includes(searchLower) ||
|
||||
slot.item_name?.toLowerCase().includes(searchLower)
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack fill>
|
||||
{filteredSlots.length > 0 && (
|
||||
<Stack fill>
|
||||
{filteredSlots.map((slot) => {
|
||||
return (
|
||||
<Stack.Item key={slot.name}>
|
||||
<Button
|
||||
onClick={() =>
|
||||
act('remove_lewd_item', {
|
||||
item_slot: slot.name,
|
||||
selfref: ref_self,
|
||||
userref: ref_user,
|
||||
})
|
||||
}
|
||||
color="pink"
|
||||
tooltip={`${slot.name}${slot.item_name ? ` - ${slot.item_name}` : ''}`}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
margin: '0.5em 0',
|
||||
}}
|
||||
>
|
||||
{slot.img ? (
|
||||
<img
|
||||
src={`data:image/png;base64,${slot.img}`}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
name="eye-slash"
|
||||
size={2}
|
||||
ml={0}
|
||||
mt={0.75}
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Button>
|
||||
</Stack.Item>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
// THIS IS A BUBBER UI FILE
|
||||
export { InteractionsTab } from './InteractionsTab';
|
||||
export { LewdItemsTab } from './LewdItemsTab';
|
||||
Reference in New Issue
Block a user