new aquarium ui and few new additions (#85627)

## About The Pull Request
gives the aquarium a new ui:

![image](https://github.com/user-attachments/assets/e00b16dd-e457-4742-95c3-c68dfeac9bc5)
u can now also pet and nickname ur fish through this interface. petting
them will have them do a small dance and increase their happiness

![image](https://github.com/user-attachments/assets/150528f0-befc-47ea-8dbc-01052bfb702d)

the hearts indicate how happy the fish is with the tank's living
conditions and if theyve been petted recently.

## Why It's Good For The Game
gives aquariums a better UI making it easier to use

## Changelog
🆑
qol: gives aquariums a new easier to use UI
/🆑
This commit is contained in:
Ben10Omintrix
2024-09-04 15:44:36 +02:00
committed by GitHub
parent 5db0b3fdc9
commit 9f686f5cc6
5 changed files with 405 additions and 99 deletions
@@ -17,6 +17,8 @@
#define COMSIG_FISH_EATEN_BY_OTHER_FISH "fish_eaten_by_other_fish"
///From /obj/item/fish/feed: (fed_reagents, fed_reagent_type)
#define COMSIG_FISH_FED "fish_on_fed"
///from /obj/item/fish/pet_fish
#define COMSIG_FISH_PETTED "fish_petted"
///From /obj/item/fish/update_size_and_weight: (new_size, new_weight)
#define COMSIG_FISH_UPDATE_SIZE_AND_WEIGHT "fish_update_size_and_weight"
///From /obj/item/fish/update_fish_force: (weight_rank, bonus_malus)
+6 -2
View File
@@ -24,7 +24,6 @@
//Current layer for the visual object
var/base_layer
/**
* Fish sprite how to:
* Need to be centered on 16,16 in the dmi and facing left by default.
@@ -97,6 +96,7 @@
ADD_TRAIT(parent, TRAIT_FISH_CASE_COMPATIBILE, REF(src))
RegisterSignal(parent, COMSIG_TRY_INSERTING_IN_AQUARIUM, PROC_REF(is_ready_to_insert))
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(enter_aquarium))
RegisterSignal(parent, COMSIG_FISH_PETTED, PROC_REF(on_fish_petted))
//If component is added to something already in aquarium at the time initialize it properly.
var/atom/movable/movable_parent = parent
@@ -274,7 +274,6 @@
dead_animation()
return
/// Create looping random path animation, pixel offsets parameters include offsets already
/datum/component/aquarium_content/proc/swim_animation()
var/avg_width = round(sprite_width / 2)
@@ -325,6 +324,11 @@
base_layer = current_aquarium.request_layer(layer_mode)
vc_obj.layer = base_layer
/datum/component/aquarium_content/proc/on_fish_petted()
SIGNAL_HANDLER
new /obj/effect/temp_visual/heart(get_turf(parent))
/datum/component/aquarium_content/proc/randomize_base_position()
var/list/aq_properties = current_aquarium.get_surface_properties()
var/avg_width = round(sprite_width / 2)
+37 -15
View File
@@ -307,14 +307,30 @@
/obj/structure/aquarium/ui_data(mob/user)
. = ..()
.["fluid_type"] = fluid_type
.["fluidType"] = fluid_type
.["temperature"] = fluid_temp
.["allow_breeding"] = allow_breeding
.["feeding_interval"] = feeding_interval / (1 MINUTES)
var/list/content_data = list()
for(var/atom/movable/fish in contents)
content_data += list(list("name"=fish.name,"ref"=ref(fish)))
.["contents"] = content_data
.["allowBreeding"] = allow_breeding
.["FishData"] = list()
.["feedingInterval"] = feeding_interval / (1 MINUTES)
.["PropData"] = list()
for(var/atom/movable/item in contents)
if(isfish(item))
var/obj/item/fish/fish = item
.["fishData"] += list(list(
"fish_ref" = REF(fish),
"fish_name" = fish.name,
"fish_happiness" = fish.get_happiness_value(),
"fish_icon" = fish::icon,
"fish_icon_state" = fish::icon_state,
"fish_health" = fish.health,
))
continue
.["propData"] += list(list(
"prop_ref" = REF(item),
"prop_name" = item.name,
"prop_icon" = item::icon,
"prop_icon_state" = item::icon_state,
))
/obj/structure/aquarium/ui_static_data(mob/user)
. = ..()
@@ -322,6 +338,7 @@
.["minTemperature"] = min_fluid_temp
.["maxTemperature"] = max_fluid_temp
.["fluidTypes"] = fluid_types
.["heart_icon"] = 'icons/effects/effects.dmi'
/obj/structure/aquarium/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
@@ -345,14 +362,19 @@
if("feeding_interval")
feeding_interval = params["feeding_interval"] MINUTES
. = TRUE
if("remove")
var/atom/movable/inside = locate(params["ref"]) in contents
if(inside)
if(isitem(inside))
user.put_in_hands(inside)
else
inside.forceMove(get_turf(src))
to_chat(user,span_notice("You take out [inside] from [src]."))
if("pet_fish")
var/obj/item/fish/fish = locate(params["fish_reference"]) in contents
fish?.pet_fish(user)
if("remove_item")
var/atom/movable/item = locate(params["item_reference"]) in contents
item?.forceMove(drop_location())
to_chat(user, span_notice("You take out [item] from [src]."))
if("rename_fish")
var/new_name = sanitize_name(params["chosen_name"])
if(!new_name)
return
var/atom/movable/fish = locate(params["fish_reference"]) in contents
fish.name = new_name
/obj/structure/aquarium/ui_interact(mob/user, datum/tgui/ui)
. = ..()
+33
View File
@@ -1,3 +1,9 @@
#define FISH_SAD 0
#define FISH_NEUTRAL 1
#define FISH_SATISFIED 2
#define FISH_HAPPY 3
#define FISH_VERY_HAPPY 4
// Fish path used for autogenerated fish
/obj/item/fish
name = "generic looking aquarium fish"
@@ -147,6 +153,8 @@
/// The beauty this fish provides to the aquarium it's inserted in.
var/beauty = FISH_BEAUTY_GENERIC
///have we recently pet this fish
var/recently_petted = FALSE
/obj/item/fish/Initialize(mapload, apply_qualities = TRUE)
. = ..()
@@ -755,6 +763,31 @@
if(HAS_TRAIT(src, TRAIT_FISH_FROM_CASE)) //Avoid printing money by simply ordering fish and sending it back.
calculated_price *= 0.05
return round(calculated_price)
/obj/item/fish/proc/get_happiness_value()
var/happiness_value = 0
if(recently_petted)
happiness_value++
if(HAS_TRAIT(src, TRAIT_FISH_NO_HUNGER) || min((world.time - last_feeding) / feeding_frequency, 1) < 0.5)
happiness_value++
var/obj/structure/aquarium/aquarium = loc
if(!istype(aquarium))
return happiness_value
if(compatible_fluid_type(required_fluid_type, aquarium.fluid_type))
happiness_value++
if(ISINRANGE(aquarium.fluid_temp, required_temperature_min, required_temperature_max))
happiness_value++
return happiness_value
/obj/item/fish/proc/pet_fish(mob/living/user)
if(recently_petted)
to_chat(user, span_warning("[src] runs away from your finger as you dip it into the water!"))
return
if(electrogenesis_power > 15 MEGA JOULES)
user.electrocute_act(5, src) //was it all worth it?
recently_petted = TRUE
SEND_SIGNAL(src, COMSIG_FISH_PETTED)
to_chat(user, span_notice("[src] dances around!"))
addtimer(VARSET_CALLBACK(src, recently_petted, FALSE), 30 SECONDS)
/// Returns random fish, using random_case_rarity probabilities.
/proc/random_fish_type(required_fluid)
+327 -82
View File
@@ -1,10 +1,15 @@
import { capitalizeFirst } from 'common/string';
import {
Box,
Button,
DmIcon,
Flex,
Icon,
Knob,
LabeledControls,
LabeledList,
NumberInput,
Section,
Stack,
} from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react';
@@ -13,98 +18,338 @@ import { Window } from '../layouts';
type Data = {
temperature: number;
fluid_type: string;
fluidType: string;
minTemperature: number;
maxTemperature: number;
fluidTypes: string[];
contents: { ref: string; name: string }[];
allow_breeding: BooleanLike;
feeding_interval: number;
fishData: FishData[];
propData: PropData[];
allowBreeding: BooleanLike;
feedingInterval: number;
heartIcon: string;
heartIconState: string;
heartEmptyIconState: string;
};
type FishData = {
fish_ref: string;
fish_name: string;
fish_happiness: number;
fish_icon: string;
fish_icon_state: string;
fish_health: number;
};
type PropData = {
prop_ref: string;
prop_name: string;
prop_icon: string;
prop_icon_state: string;
};
export const Aquarium = (props) => {
const { act, data } = useBackend<Data>();
const {
temperature,
fluid_type,
minTemperature,
maxTemperature,
fluidTypes,
contents,
allow_breeding,
feeding_interval,
} = data;
const { fishData } = data;
return (
<Window width={520} height={400}>
<Window width={765} height={500} theme="ntos">
<Window.Content>
<Section title="Aquarium Controls">
<LabeledControls>
<LabeledControls.Item label="Temperature">
<Knob
size={1.25}
mb={1}
value={temperature}
unit="K"
minValue={minTemperature}
maxValue={maxTemperature}
step={1}
stepPixelSize={1}
onDrag={(_, value) =>
act('temperature', {
temperature: value,
})
}
/>
</LabeledControls.Item>
<LabeledControls.Item label="Fluid">
<Flex direction="column" mb={1}>
{fluidTypes.map((f) => (
<Flex.Item key={f}>
<Button
fluid
content={f}
selected={fluid_type === f}
onClick={() => act('fluid', { fluid: f })}
/>
</Flex.Item>
))}
</Flex>
</LabeledControls.Item>
<LabeledControls.Item label="Reproduction and Growth">
<Button
content={allow_breeding ? 'Online' : 'Offline'}
selected={allow_breeding}
onClick={() => act('allow_breeding')}
/>
</LabeledControls.Item>
<LabeledControls.Item label="Feeding Interval">
<NumberInput
fluid
value={feeding_interval}
minValue={1}
maxValue={7}
step={1}
unit="minutes"
onChange={(value) =>
act('feeding_interval', {
feeding_interval: value,
})
}
/>
</LabeledControls.Item>
</LabeledControls>
</Section>
<Section title="Contents">
{contents.map((movable) => (
<Button
key={movable.ref}
content={movable.name}
onClick={() => act('remove', { ref: movable.ref })}
/>
))}
</Section>
<Stack vertical fill>
<Stack.Item>
<Settings />
</Stack.Item>
<Stack.Item grow>
<Flex>
<Flex.Item height="300px" width="75%">
<Section fill title="Fish" scrollable>
<Stack wrap>
{fishData.map((fish) => (
<Stack.Item
width="44%"
height="100px"
ml={1}
key={fish.fish_ref}
style={{
background: 'rgba(36, 50, 67, 0.5)',
padding: '5px 5px',
borderRadius: '1em',
border: '3px solid #574e82',
}}
>
<FishInfo fish={fish} />
</Stack.Item>
))}
</Stack>
</Section>
</Flex.Item>
<Flex.Item ml={1} grow>
<PropTypes />
</Flex.Item>
</Flex>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
const FishInfo = (props) => {
const { act, data } = useBackend<Data>();
const { fish } = props;
return (
<Stack vertical>
<Stack.Item>
<Flex>
<Flex.Item width="40px">
<DmIcon
style={{ borderRadius: '1em', background: '#151326' }}
icon={fish.fish_icon}
icon_state={fish.fish_icon_state}
height="40px"
width="40px"
/>
</Flex.Item>
<Flex.Item grow>
<Stack vertical>
<Stack.Item
ml={1}
style={{ fontSize: '13px', fontWeight: 'bold' }}
>
{fish.fish_name.toUpperCase()}
</Stack.Item>
<Stack.Item mt={fish.fish_health > 0 ? -4 : 1}>
{(fish.fish_health > 0 && (
<CalculateHappiness happiness={fish.fish_happiness} />
)) || <Icon ml={2} name="skull-crossbones" textColor="white" />}
</Stack.Item>
</Stack>
</Flex.Item>
<Flex.Item>
<Flex>
<Button
fluid
icon="arrow-up"
color="transparent"
onClick={() =>
act('remove_item', {
item_reference: fish.fish_ref,
})
}
/>
</Flex>
</Flex.Item>
</Flex>
</Stack.Item>
<Stack.Item grow>
<Flex>
<Flex.Item width="50%">
<Button
textAlign="center"
mt={1}
fluid
color="transparent"
icon="paw"
style={{
padding: '3px',
borderRadius: '1em',
background: '#151326',
}}
onClick={() =>
act('pet_fish', {
fish_reference: fish.fish_ref,
})
}
>
Pet
</Button>
</Flex.Item>
<Flex.Item width="50%">
<Button.Input
textAlign="center"
mt={1}
ml={1}
fluid
placeholder="Rename"
color="transparent"
onCommit={(e, value) => {
act('rename_fish', {
fish_reference: fish.fish_ref,
chosen_name: value,
});
}}
style={{
padding: '3px',
borderRadius: '1em',
background: '#151326',
}}
>
<Flex>
<Flex.Item ml={3}>
<Icon name="keyboard" />
</Flex.Item>
<Flex.Item ml={1}>Rename</Flex.Item>
</Flex>
</Button.Input>
</Flex.Item>
</Flex>
</Stack.Item>
</Stack>
);
};
const PropTypes = (props) => {
const { act, data } = useBackend<Data>();
const { propData } = data;
return (
<Section scrollable fill title="Props">
<Stack vertical>
{propData.map((prop) => (
<Stack.Item className="candystripe" key={prop.prop_ref}>
<Button
fluid
color="transparent"
onClick={() =>
act('remove_item', {
item_reference: prop.prop_ref,
})
}
>
<Flex>
<Flex.Item>
<DmIcon
icon={prop.prop_icon}
icon_state={prop.prop_icon_state}
height="40px"
width="40px"
/>
</Flex.Item>
<Flex.Item
ml={1}
mt={1}
style={{ fontSize: '11px', fontWeight: 'bold' }}
>
{capitalizeFirst(prop.prop_name)}
</Flex.Item>
</Flex>
</Button>
</Stack.Item>
))}
</Stack>
</Section>
);
};
const CalculateHappiness = (props) => {
const { data } = useBackend<Data>();
const { heartIcon } = data;
const { happiness } = props;
return (
<Box>
{Array.from({ length: 5 }, (_, index) => (
<DmIcon
key={index}
ml={index === 0 ? 0 : -6}
icon={heartIcon}
icon_state={happiness >= index ? 'full_heart' : 'empty_heart'}
height="48px"
width="48px"
/>
))}
</Box>
);
};
const Settings = (props) => {
const { act, data } = useBackend<Data>();
const {
temperature,
minTemperature,
maxTemperature,
fluidTypes,
fluidType,
allowBreeding,
feedingInterval,
} = data;
return (
<Flex fill>
<Flex.Item grow>
<Section fill title="Temperature">
<Knob
mt={3}
size={1.5}
mb={1}
value={temperature}
unit="K"
minValue={minTemperature}
maxValue={maxTemperature}
step={1}
stepPixelSize={1}
onDrag={(_, value) =>
act('temperature', {
temperature: value,
})
}
/>
</Section>
</Flex.Item>
<Flex.Item ml={1} grow>
<Section fill title="Fluid">
<Flex direction="column" mb={1}>
{fluidTypes.map((f) => (
<Flex.Item className="candystripe" key={f}>
<Button
textAlign="center"
fluid
color="transparent"
selected={fluidType === f}
onClick={() => act('fluid', { fluid: f })}
>
{f}
</Button>
</Flex.Item>
))}
</Flex>
</Section>
</Flex.Item>
<Flex.Item ml={1} grow>
<Section fill title="Settings">
<Box mt={2}>
<LabeledList>
<LabeledList.Item label="Reproduction">
<Button
textAlign="center"
width="75px"
content={allowBreeding ? 'Online' : 'Offline'}
selected={allowBreeding}
onClick={() => act('allow_breeding')}
/>
</LabeledList.Item>
<LabeledList.Item label="Feeding Interval">
<NumberInput
width="15px"
value={feedingInterval}
minValue={1}
maxValue={7}
step={1}
unit="minutes"
onChange={(value) =>
act('feeding_interval', {
feeding_interval: value,
})
}
/>
</LabeledList.Item>
</LabeledList>
</Box>
</Section>
</Flex.Item>
</Flex>
);
};
function dissectName(input: string): string {
return input.split(' ')[0].slice(0, 18);
}