mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-13 03:33:21 +00:00
[MIRROR] Add recipe macro recording to reagent dispenser (#8769)
Co-authored-by: ShadowLarkens <shadowlarkens@gmail.com> Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
@@ -21,6 +21,11 @@
|
|||||||
anchored = TRUE
|
anchored = TRUE
|
||||||
unacidable = TRUE
|
unacidable = TRUE
|
||||||
|
|
||||||
|
/// Records the reagents dispensed by the user if this list is not null
|
||||||
|
var/list/recording_recipe
|
||||||
|
/// Saves all the recipes recorded by the machine
|
||||||
|
var/list/saved_recipes = list()
|
||||||
|
|
||||||
/obj/machinery/chemical_dispenser/Initialize()
|
/obj/machinery/chemical_dispenser/Initialize()
|
||||||
. = ..()
|
. = ..()
|
||||||
if(spawn_cartridges)
|
if(spawn_cartridges)
|
||||||
@@ -154,25 +159,38 @@
|
|||||||
var/obj/item/weapon/reagent_containers/chem_disp_cartridge/C = cartridges[label]
|
var/obj/item/weapon/reagent_containers/chem_disp_cartridge/C = cartridges[label]
|
||||||
chemicals.Add(list(list("name" = label, "id" = label, "volume" = C.reagents.total_volume))) // list in a list because Byond merges the first list...
|
chemicals.Add(list(list("name" = label, "id" = label, "volume" = C.reagents.total_volume))) // list in a list because Byond merges the first list...
|
||||||
data["chemicals"] = chemicals
|
data["chemicals"] = chemicals
|
||||||
|
|
||||||
|
data["recipes"] = saved_recipes
|
||||||
|
data["recordingRecipe"] = recording_recipe
|
||||||
return data
|
return data
|
||||||
|
|
||||||
/obj/machinery/chemical_dispenser/tgui_act(action, params)
|
/obj/machinery/chemical_dispenser/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
|
||||||
if(..())
|
. = ..()
|
||||||
return TRUE
|
if(.)
|
||||||
|
return
|
||||||
|
if(stat & BROKEN)
|
||||||
|
return FALSE
|
||||||
|
|
||||||
|
add_fingerprint(ui.user)
|
||||||
|
|
||||||
. = TRUE
|
|
||||||
switch(action)
|
switch(action)
|
||||||
if("amount")
|
if("amount")
|
||||||
amount = clamp(round(text2num(params["amount"]), 1), 0, 120) // round to nearest 1 and clamp 0 - 120
|
amount = clamp(round(text2num(params["amount"]), 1), 0, 120) // round to nearest 1 and clamp 0 - 120
|
||||||
|
. = TRUE
|
||||||
|
|
||||||
if("dispense")
|
if("dispense")
|
||||||
var/label = params["reagent"]
|
var/label = params["reagent"]
|
||||||
if(cartridges[label] && container && container.is_open_container())
|
if(recording_recipe)
|
||||||
|
recording_recipe += list(list("id" = label, "amount" = amount))
|
||||||
|
else if(cartridges[label] && container && container.is_open_container())
|
||||||
var/obj/item/weapon/reagent_containers/chem_disp_cartridge/C = cartridges[label]
|
var/obj/item/weapon/reagent_containers/chem_disp_cartridge/C = cartridges[label]
|
||||||
playsound(src, 'sound/machines/reagent_dispense.ogg', 25, 1)
|
playsound(src, 'sound/machines/reagent_dispense.ogg', 25, 1)
|
||||||
C.reagents.trans_to(container, amount)
|
C.reagents.trans_to(container, amount)
|
||||||
|
. = TRUE
|
||||||
|
|
||||||
if("remove")
|
if("remove")
|
||||||
var/amount = text2num(params["amount"])
|
var/amount = text2num(params["amount"])
|
||||||
if(!container || !amount)
|
if(!container || !amount || recording_recipe)
|
||||||
return
|
return
|
||||||
var/datum/reagents/R = container.reagents
|
var/datum/reagents/R = container.reagents
|
||||||
var/id = params["reagent"]
|
var/id = params["reagent"]
|
||||||
@@ -180,18 +198,82 @@
|
|||||||
R.remove_reagent(id, amount)
|
R.remove_reagent(id, amount)
|
||||||
else if(amount == -1) // Isolate
|
else if(amount == -1) // Isolate
|
||||||
R.isolate_reagent(id)
|
R.isolate_reagent(id)
|
||||||
|
. = TRUE
|
||||||
|
|
||||||
if("ejectBeaker")
|
if("ejectBeaker")
|
||||||
if(container)
|
if(container)
|
||||||
container.forceMove(get_turf(src))
|
container.forceMove(get_turf(src))
|
||||||
|
if(Adjacent(ui.user)) // So the AI doesn't get a beaker somehow.
|
||||||
if(Adjacent(usr)) // So the AI doesn't get a beaker somehow.
|
ui.user.put_in_hands(container)
|
||||||
usr.put_in_hands(container)
|
|
||||||
|
|
||||||
container = null
|
container = null
|
||||||
else
|
. = TRUE
|
||||||
return FALSE
|
|
||||||
|
if("record_recipe")
|
||||||
|
recording_recipe = list()
|
||||||
|
. = TRUE
|
||||||
|
|
||||||
|
if("cancel_recording")
|
||||||
|
recording_recipe = null
|
||||||
|
. = TRUE
|
||||||
|
|
||||||
|
if("clear_recipes")
|
||||||
|
if(tgui_alert(ui.user, "Clear all recipes?", "Clear?", list("No", "Yes")) == "Yes")
|
||||||
|
saved_recipes = list()
|
||||||
|
. = TRUE
|
||||||
|
|
||||||
|
if("save_recording")
|
||||||
|
var/name = tgui_input_text(ui.user, "What do you want to name this recipe?", "Recipe Name?", "Recipe Name", MAX_NAME_LEN)
|
||||||
|
if(tgui_status(ui.user, state) != STATUS_INTERACTIVE)
|
||||||
|
return
|
||||||
|
if(saved_recipes[name] && tgui_alert(ui.user, "\"[name]\" already exists, do you want to overwrite it?",, list("No", "Yes")) != "Yes")
|
||||||
|
return
|
||||||
|
if(name && recording_recipe)
|
||||||
|
for(var/list/L in recording_recipe)
|
||||||
|
var/label = L["id"]
|
||||||
|
// Verify this dispenser can dispense every chemical
|
||||||
|
if(!cartridges[label])
|
||||||
|
visible_message(span_warning("[src] buzzes."), span_warning("You hear a faint buzz."))
|
||||||
|
to_chat(ui.user, span_warning("[src] cannot find <b>[label]</b>!"))
|
||||||
|
playsound(src, 'sound/machines/buzz-two.ogg', 50, TRUE)
|
||||||
|
return
|
||||||
|
saved_recipes[name] = recording_recipe
|
||||||
|
recording_recipe = null
|
||||||
|
. = TRUE
|
||||||
|
|
||||||
|
if("dispense_recipe")
|
||||||
|
var/list/chemicals_to_dispense = saved_recipes[params["recipe"]]
|
||||||
|
if(!LAZYLEN(chemicals_to_dispense))
|
||||||
|
return
|
||||||
|
|
||||||
|
if(!recording_recipe)
|
||||||
|
if(!container)
|
||||||
|
to_chat(ui.user, span_warning("There is no beaker in [src]."))
|
||||||
|
return
|
||||||
|
|
||||||
|
for(var/list/L in chemicals_to_dispense)
|
||||||
|
var/label = L["id"]
|
||||||
|
var/dispense_amount = L["amount"]
|
||||||
|
|
||||||
|
var/obj/item/weapon/reagent_containers/chem_disp_cartridge/C = cartridges[label]
|
||||||
|
if(!C)
|
||||||
|
visible_message(span_warning("[src] buzzes."), span_warning("You hear a faint buzz."))
|
||||||
|
to_chat(ui.user, span_warning("[src] cannot find <b>[label]</b>!"))
|
||||||
|
playsound(src, 'sound/machines/buzz-two.ogg', 50, TRUE)
|
||||||
|
break
|
||||||
|
|
||||||
|
// Allows copying recipes
|
||||||
|
playsound(src, 'sound/machines/reagent_dispense.ogg', 25, 1)
|
||||||
|
var/amount_actually_dispensed = C.reagents.trans_to(container, dispense_amount)
|
||||||
|
if(dispense_amount != amount_actually_dispensed)
|
||||||
|
visible_message(span_warning("[src] buzzes."), span_warning("You hear a faint buzz."))
|
||||||
|
to_chat(ui.user, span_warning("[src] was only able to dispense [amount_actually_dispensed]u out of [dispense_amount]u requested of <b>[label]</b>!"))
|
||||||
|
playsound(src, 'sound/machines/buzz-two.ogg', 50, TRUE)
|
||||||
|
break
|
||||||
|
else
|
||||||
|
recording_recipe += chemicals_to_dispense
|
||||||
|
|
||||||
|
. = TRUE
|
||||||
|
|
||||||
add_fingerprint(usr)
|
|
||||||
|
|
||||||
/obj/machinery/chemical_dispenser/attack_ghost(mob/user)
|
/obj/machinery/chemical_dispenser/attack_ghost(mob/user)
|
||||||
if(stat & BROKEN)
|
if(stat & BROKEN)
|
||||||
|
|||||||
@@ -11,12 +11,24 @@ export const ChemDispenserBeaker = (props) => {
|
|||||||
beakerCurrentVolume,
|
beakerCurrentVolume,
|
||||||
beakerMaxVolume,
|
beakerMaxVolume,
|
||||||
beakerContents = [],
|
beakerContents = [],
|
||||||
|
recipes,
|
||||||
|
recordingRecipe,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
|
const recording = !!recordingRecipe;
|
||||||
|
const recordedContents =
|
||||||
|
recording &&
|
||||||
|
recordingRecipe.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
name: r.id.replace(/_/, ' '),
|
||||||
|
volume: r.amount,
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section
|
<Section
|
||||||
title="Beaker"
|
title={recording ? 'Virtual Beaker' : 'Beaker'}
|
||||||
flex="content"
|
fill
|
||||||
minHeight="25%"
|
scrollable
|
||||||
buttons={
|
buttons={
|
||||||
<Box>
|
<Box>
|
||||||
{!!isBeakerLoaded && (
|
{!!isBeakerLoaded && (
|
||||||
@@ -35,12 +47,13 @@ export const ChemDispenserBeaker = (props) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<BeakerContents
|
<BeakerContents
|
||||||
beakerLoaded={isBeakerLoaded}
|
beakerLoaded={recordedContents || isBeakerLoaded}
|
||||||
beakerContents={beakerContents}
|
beakerContents={recordedContents || beakerContents}
|
||||||
buttons={(chemical) => (
|
buttons={(chemical) => (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
icon="compress-arrows-alt"
|
icon="compress-arrows-alt"
|
||||||
|
disabled={recording}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
act('remove', {
|
act('remove', {
|
||||||
reagent: chemical.id,
|
reagent: chemical.id,
|
||||||
@@ -53,6 +66,7 @@ export const ChemDispenserBeaker = (props) => {
|
|||||||
{removeAmounts.map((a, i) => (
|
{removeAmounts.map((a, i) => (
|
||||||
<Button
|
<Button
|
||||||
key={i}
|
key={i}
|
||||||
|
disabled={recording}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
act('remove', {
|
act('remove', {
|
||||||
reagent: chemical.id,
|
reagent: chemical.id,
|
||||||
@@ -64,6 +78,7 @@ export const ChemDispenserBeaker = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
<Button
|
<Button
|
||||||
|
disabled={recording}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
act('remove', {
|
act('remove', {
|
||||||
reagent: chemical.id,
|
reagent: chemical.id,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useBackend } from '../../backend';
|
import { useBackend } from '../../backend';
|
||||||
import { Button, Flex, Section } from '../../components';
|
import { Button, Flex, Icon, Section, Tooltip } from '../../components';
|
||||||
import { Data } from './types';
|
import { Data } from './types';
|
||||||
|
|
||||||
export const ChemDispenserChemicals = (props) => {
|
export const ChemDispenserChemicals = (props) => {
|
||||||
@@ -13,6 +15,7 @@ export const ChemDispenserChemicals = (props) => {
|
|||||||
<Section
|
<Section
|
||||||
title={data.glass ? 'Drink Dispenser' : 'Chemical Dispenser'}
|
title={data.glass ? 'Drink Dispenser' : 'Chemical Dispenser'}
|
||||||
flexGrow
|
flexGrow
|
||||||
|
buttons={<RecordingBlinker />}
|
||||||
>
|
>
|
||||||
<Flex direction="row" wrap="wrap" height="100%" align="flex-start">
|
<Flex direction="row" wrap="wrap" height="100%" align="flex-start">
|
||||||
{chemicals.map((c, i) => (
|
{chemicals.map((c, i) => (
|
||||||
@@ -39,3 +42,29 @@ export const ChemDispenserChemicals = (props) => {
|
|||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RecordingBlinker = (props) => {
|
||||||
|
const { data } = useBackend<Data>();
|
||||||
|
const recording = !!data.recordingRecipe;
|
||||||
|
|
||||||
|
const [blink, setBlink] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (recording) {
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
setBlink((v) => !v);
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
}, [recording]);
|
||||||
|
|
||||||
|
if (!recording) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="Recording in progress">
|
||||||
|
<Icon mt={0.7} color="bad" name={blink ? 'circle-o' : 'circle'} />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { useBackend } from '../../backend';
|
||||||
|
import { Box, Button, Section } from '../../components';
|
||||||
|
import { Data } from './types';
|
||||||
|
|
||||||
|
export const ChemDispenserRecipes = (props) => {
|
||||||
|
const { act, data } = useBackend<Data>();
|
||||||
|
const { recipes, recordingRecipe } = data;
|
||||||
|
|
||||||
|
const recording: boolean = !!recordingRecipe;
|
||||||
|
const recipeData = Object.keys(recipes).sort();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
title="Recipes"
|
||||||
|
fill
|
||||||
|
scrollable
|
||||||
|
buttons={
|
||||||
|
<>
|
||||||
|
{!recording && (
|
||||||
|
<Button icon="circle" onClick={() => act('record_recipe')}>
|
||||||
|
Record
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{recording && (
|
||||||
|
<Button
|
||||||
|
icon="ban"
|
||||||
|
color="bad"
|
||||||
|
onClick={() => act('cancel_recording')}
|
||||||
|
>
|
||||||
|
Discard
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{recording && (
|
||||||
|
<Button
|
||||||
|
icon="save"
|
||||||
|
color="green"
|
||||||
|
onClick={() => act('save_recording')}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!recording && (
|
||||||
|
<Button.Confirm
|
||||||
|
icon="trash"
|
||||||
|
confirmIcon="trash"
|
||||||
|
color="bad"
|
||||||
|
onClick={() => act('clear_recipes')}
|
||||||
|
>
|
||||||
|
Clear All
|
||||||
|
</Button.Confirm>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{recording && (
|
||||||
|
<>
|
||||||
|
<Box color="green" fontSize={1.2} bold>
|
||||||
|
Recording In Progress...
|
||||||
|
</Box>
|
||||||
|
<Box color="label">
|
||||||
|
Press dispenser buttons in the order you wish for them to be
|
||||||
|
repeated, then click{' '}
|
||||||
|
<Box color="good" inline>
|
||||||
|
Save
|
||||||
|
</Box>
|
||||||
|
.
|
||||||
|
</Box>
|
||||||
|
<Box color="average" mb={1}>
|
||||||
|
Alternatively, if you mess up the recipe and want to discard this
|
||||||
|
recording, click{' '}
|
||||||
|
<Box color="bad" inline>
|
||||||
|
Discard
|
||||||
|
</Box>
|
||||||
|
.
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{recipeData.length
|
||||||
|
? recipeData.map((recipe) => (
|
||||||
|
<Button
|
||||||
|
key={recipe}
|
||||||
|
fluid
|
||||||
|
icon="flask"
|
||||||
|
onClick={() => act('dispense_recipe', { recipe })}
|
||||||
|
>
|
||||||
|
{recipe}
|
||||||
|
</Button>
|
||||||
|
))
|
||||||
|
: 'No Recipes.'}
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,7 +7,7 @@ export const ChemDispenserSettings = (props) => {
|
|||||||
const { act, data } = useBackend<Data>();
|
const { act, data } = useBackend<Data>();
|
||||||
const { amount } = data;
|
const { amount } = data;
|
||||||
return (
|
return (
|
||||||
<Section title="Settings" flex="content">
|
<Section title="Settings" fill>
|
||||||
<LabeledList>
|
<LabeledList>
|
||||||
<LabeledList.Item label="Dispense" verticalAlign="middle">
|
<LabeledList.Item label="Dispense" verticalAlign="middle">
|
||||||
{dispenseAmounts.map((a, i) => (
|
{dispenseAmounts.map((a, i) => (
|
||||||
|
|||||||
@@ -1,15 +1,40 @@
|
|||||||
|
import { useBackend } from '../../backend';
|
||||||
|
import { Stack } from '../../components';
|
||||||
import { Window } from '../../layouts';
|
import { Window } from '../../layouts';
|
||||||
import { ChemDispenserBeaker } from './ChemDispenserBeaker';
|
import { ChemDispenserBeaker } from './ChemDispenserBeaker';
|
||||||
import { ChemDispenserChemicals } from './ChemDispenserChemicals';
|
import { ChemDispenserChemicals } from './ChemDispenserChemicals';
|
||||||
|
import { ChemDispenserRecipes } from './ChemDispenserRecipes';
|
||||||
import { ChemDispenserSettings } from './ChemDispenserSettings';
|
import { ChemDispenserSettings } from './ChemDispenserSettings';
|
||||||
|
import { Data } from './types';
|
||||||
|
|
||||||
export const ChemDispenser = (props) => {
|
export const ChemDispenser = (props) => {
|
||||||
|
const { data } = useBackend<Data>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Window width={390} height={655}>
|
<Window width={680} height={540}>
|
||||||
<Window.Content className="Layout__content--flexColumn">
|
<Window.Content>
|
||||||
|
<Stack vertical fill>
|
||||||
|
<Stack.Item>
|
||||||
|
<Stack>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<Stack vertical fill>
|
||||||
|
<Stack.Item>
|
||||||
<ChemDispenserSettings />
|
<ChemDispenserSettings />
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<ChemDispenserRecipes />
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item grow>
|
||||||
<ChemDispenserChemicals />
|
<ChemDispenserChemicals />
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item grow>
|
||||||
<ChemDispenserBeaker />
|
<ChemDispenserBeaker />
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
</Window.Content>
|
</Window.Content>
|
||||||
</Window>
|
</Window>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { BooleanLike } from 'common/react';
|
import { BooleanLike } from 'common/react';
|
||||||
|
|
||||||
|
export type Recipe = {
|
||||||
|
id: string;
|
||||||
|
amount: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type Data = {
|
export type Data = {
|
||||||
amount: number;
|
amount: number;
|
||||||
isBeakerLoaded: BooleanLike;
|
isBeakerLoaded: BooleanLike;
|
||||||
@@ -8,6 +13,8 @@ export type Data = {
|
|||||||
beakerCurrentVolume: number | null;
|
beakerCurrentVolume: number | null;
|
||||||
beakerMaxVolume: number | null;
|
beakerMaxVolume: number | null;
|
||||||
chemicals: reagent[];
|
chemicals: reagent[];
|
||||||
|
recipes: Record<string, Recipe[]>;
|
||||||
|
recordingRecipe: Recipe[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type reagent = { name: string; id: string; volume: number };
|
type reagent = { name: string; id: string; volume: number };
|
||||||
|
|||||||
Reference in New Issue
Block a user