{!!value &&
}
@@ -58,36 +77,38 @@ const BarLabel = (props) => {
};
export const HypertorusTemperatures = (props, context) => {
- const { data } = useBackend(context);
+ const { data } = useBackend
(context);
const {
- power_level,
base_max_temperature,
+ internal_coolant_temperature_archived,
+ internal_coolant_temperature,
+ internal_fusion_temperature_archived,
internal_fusion_temperature,
- moderator_internal_temperature,
+ internal_output_temperature_archived,
internal_output_temperature,
- internal_coolant_temperature,
+ moderator_internal_temperature_archived,
+ moderator_internal_temperature,
+ power_level,
+ selectable_fuel = [],
+ selected,
temperature_period,
} = data;
const internal_fusion_temperature_delta =
- (internal_fusion_temperature - data.internal_fusion_temperature_archived) /
+ (internal_fusion_temperature - internal_fusion_temperature_archived) /
temperature_period;
const internal_output_temperature_delta =
- (internal_output_temperature - data.internal_output_temperature_archived) /
+ (internal_output_temperature - internal_output_temperature_archived) /
temperature_period;
const internal_coolant_temperature_delta =
- (internal_coolant_temperature -
- data.internal_coolant_temperature_archived) /
+ (internal_coolant_temperature - internal_coolant_temperature_archived) /
temperature_period;
const moderator_internal_temperature_delta =
- (moderator_internal_temperature -
- data.moderator_internal_temperature_archived) /
+ (moderator_internal_temperature - moderator_internal_temperature_archived) /
temperature_period;
- const selected_fuel = (data.selectable_fuel || []).filter(
- (d) => d.id === data.selected
- )[0];
+ const selected_fuel = selectable_fuel.filter((d) => d.id === selected)[0];
let prev_power_level_temperature = 10 ** (1 + power_level);
let next_power_level_temperature = 10 ** (2 + power_level);
@@ -99,8 +120,7 @@ export const HypertorusTemperatures = (props, context) => {
prev_power_level_temperature = 500;
} else if (power_level === 6) {
next_power_level_temperature =
- base_max_temperature *
- (selected_fuel ? selected_fuel.temperature_multiplier : 1);
+ base_max_temperature * (selected_fuel?.temperature_multiplier ?? 1);
}
const temperatures = [
@@ -112,7 +132,7 @@ export const HypertorusTemperatures = (props, context) => {
internal_output_temperature,
internal_coolant_temperature,
].filter((d) => d),
- ].map((d) => parseFloat(d));
+ ].map((d) => d);
const maxTemperature = Math.max(...temperatures);
const minTemperature = Math.max(
diff --git a/tgui/packages/tgui/interfaces/Hypertorus/helpers.js b/tgui/packages/tgui/interfaces/Hypertorus/helpers.js
deleted file mode 100644
index ceef63107952..000000000000
--- a/tgui/packages/tgui/interfaces/Hypertorus/helpers.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Icon, Tooltip } from '../../components';
-
-// Exponential rendering specifically for HFR values.
-// Note that we don't want to use unicode exponents as anything over ^3
-// is more or less unreadable.
-export const to_exponential_if_big = (value) => {
- if (Math.abs(value) > 5000) {
- return value.toExponential(1);
- }
- return Math.round(value);
-};
-
-// Simple question mark icon with a hover tooltip
-export const HoverHelp = (props) => (
-
-
-
-);
-
-// When no hover help is available, but we want a placeholder for spacing
-export const HelpDummy = (props) => ;
diff --git a/tgui/packages/tgui/interfaces/Hypertorus/helpers.tsx b/tgui/packages/tgui/interfaces/Hypertorus/helpers.tsx
new file mode 100644
index 000000000000..8e4746b20f98
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/Hypertorus/helpers.tsx
@@ -0,0 +1,25 @@
+import { Icon, Tooltip } from 'tgui/components';
+
+// Exponential rendering specifically for HFR values.
+// Note that we don't want to use unicode exponents as anything over ^3
+// is more or less unreadable.
+export const to_exponential_if_big = (value: number) => {
+ if (Math.abs(value) > 5000) {
+ return value.toExponential(1);
+ }
+ return Math.round(value);
+};
+
+// Simple question mark icon with a hover tooltip
+export const HoverHelp = ({ content }: { content: string }) => {
+ return (
+
+
+
+ );
+};
+
+// When no hover help is available, but we want a placeholder for spacing
+export const HelpDummy = (props) => {
+ return ;
+};
diff --git a/tgui/packages/tgui/interfaces/Hypertorus/index.js b/tgui/packages/tgui/interfaces/Hypertorus/index.tsx
similarity index 61%
rename from tgui/packages/tgui/interfaces/Hypertorus/index.js
rename to tgui/packages/tgui/interfaces/Hypertorus/index.tsx
index b15f9841419a..12e770b6ea9b 100644
--- a/tgui/packages/tgui/interfaces/Hypertorus/index.js
+++ b/tgui/packages/tgui/interfaces/Hypertorus/index.tsx
@@ -1,25 +1,70 @@
-import { useBackend } from 'tgui/backend';
import { Button, Collapsible, Flex, Section, Stack } from 'tgui/components';
-import { Window } from 'tgui/layouts';
import { HypertorusSecondaryControls, HypertorusWasteRemove } from './Controls';
+
+import { BooleanLike } from 'common/react';
import { HypertorusGases } from './Gases';
import { HypertorusParameters } from './Parameters';
import { HypertorusRecipes } from './Recipes';
import { HypertorusTemperatures } from './Temperatures';
+import { Window } from 'tgui/layouts';
+import { useBackend } from 'tgui/backend';
+
+export type HypertorusData = {
+ start_power: number;
+ start_cooling: number;
+ start_fuel: number;
+ start_moderator: number;
+ power_level: number;
+ selected: string;
+ selectable_fuel: HypertorusFuel[];
+ base_max_temperature: number;
+};
+
+export type HypertorusGas = {
+ id: string;
+ amount: number;
+};
+
+export type HypertorusFilter = {
+ gas_id: string;
+ gas_name: string;
+ enabled: BooleanLike;
+};
+
+export type HypertorusFuel = {
+ id: string | null;
+ requirements: string[];
+ fusion_byproducts: string[];
+ product_gases: string[];
+ recipe_cooling_multiplier: number;
+ recipe_heating_multiplier: number;
+ energy_loss_multiplier: number;
+ fuel_consumption_multiplier: number;
+ gas_production_multiplier: number;
+ temperature_multiplier: number;
+};
const HypertorusMainControls = (props, context) => {
- const { act, data } = useBackend(context);
+ const { act, data } = useBackend(context);
+ const {
+ start_power,
+ start_cooling,
+ start_fuel,
+ start_moderator,
+ power_level,
+ base_max_temperature,
+ } = data;
return (
-
+
-
+
{'Start power: '}
@@ -27,25 +72,23 @@ const HypertorusMainControls = (props, context) => {
{'Start cooling: '}
act('fuel', { mode: id })}
- selectableFuels={data.selectable_fuel}
- selectedFuelID={data.selected}
/>
diff --git a/tgui/packages/tgui/interfaces/IVDrip.tsx b/tgui/packages/tgui/interfaces/IVDrip.tsx
index 2d0f204c1ac9..b9342e4016dc 100644
--- a/tgui/packages/tgui/interfaces/IVDrip.tsx
+++ b/tgui/packages/tgui/interfaces/IVDrip.tsx
@@ -52,6 +52,78 @@ export const IVDrip = (props, context) => {
+ {mode === MODE.injecting && injectFromPlumbing ? ( // Plumbing drip injects with the rate from network
+
+ Controlled by the plumbing network
+
+ ) : (
+ !!canAdjustTransfer && (
+
+
+ )
+ )}
+ act('changeMode')}
+ />
+ }>
+ {mode
+ ? hasInternalStorage
+ ? 'Reagents from network'
+ : 'Reagents from container'
+ : 'Blood into container'}
+
{hasContainer || hasInternalStorage ? (
{
)
}>
{
)}
- act('changeMode')}
- />
- }>
- {mode
- ? hasInternalStorage
- ? 'Reagents from network'
- : 'Reagents from container'
- : 'Blood into container'}
-
{hasObjectAttached ? (
{
) : (
- No object hasObjectAttached.
+ No object attached.
)}
- {!!hasObjectAttached &&
- (mode === MODE.injecting && injectFromPlumbing ? ( // Plumbing drip injects with the rate from network
-
- Controlled by the plumbing network
-
- ) : (
- (!!hasContainer || !!hasInternalStorage) &&
- !!canAdjustTransfer && (
-
-
- act('changeRate', {
- rate: value,
- })
- }
- />
-
- )
- ))}
diff --git a/tgui/packages/tgui/interfaces/InfuserBook.tsx b/tgui/packages/tgui/interfaces/InfuserBook.tsx
index 728f757450ce..ae3d81b96afa 100644
--- a/tgui/packages/tgui/interfaces/InfuserBook.tsx
+++ b/tgui/packages/tgui/interfaces/InfuserBook.tsx
@@ -143,7 +143,9 @@ export const InfuserBook = (props, context) => {
icon={tabIcon}
key={tabIndex}
selected={chapter === tabIndex}
- onClick={() => switchChapter(tabIndex)}>
+ onClick={
+ tabIndex === 4 ? null : () => switchChapter(tabIndex)
+ }>
{tab}
);
@@ -246,7 +248,7 @@ const InfuserEntry = (props: InfuserEntryProps, context) => {
{entry.desc}{' '}
- {!entry.threshold_desc && (
+ {entry.threshold_desc && (
<>If a subject infuses with enough DNA, {entry.threshold_desc}>
)}
diff --git a/tgui/packages/tgui/interfaces/MafiaPanel.js b/tgui/packages/tgui/interfaces/MafiaPanel.js
index fe8d845f41d6..589db274c701 100644
--- a/tgui/packages/tgui/interfaces/MafiaPanel.js
+++ b/tgui/packages/tgui/interfaces/MafiaPanel.js
@@ -6,7 +6,7 @@ import { Window } from '../layouts';
export const MafiaPanel = (props, context) => {
const { act, data } = useBackend(context);
- const { actions, phase, roleinfo, role_theme, admin_controls } = data;
+ const { phase, roleinfo, role_theme, admin_controls } = data;
return (
@@ -21,18 +21,6 @@ export const MafiaPanel = (props, context) => {
)}
- {actions?.map((action) => (
-
-
- act('mf_action', {
- atype: action,
- })
- }>
- {action}
-
-
- ))}
{!!roleinfo && (
@@ -120,7 +108,7 @@ const MafiaLobby = (props, context) => {
tooltip={multiline`
Submit a vote to start the game early.
Starts when half of the current signup list have voted to start.
- Requires a bare minimum of three players.
+ Requires a bare minimum of six players.
`}
content="Start Now!"
onClick={() => act('vote_to_start')}
@@ -235,7 +223,7 @@ const MafiaListOfRoles = (props, context) => {
icon="question"
onClick={() =>
act('mf_lookup', {
- atype: r.slice(0, -3),
+ role_name: r.slice(0, -3),
})
}
/>
@@ -323,17 +311,16 @@ const MafiaPlayers = (props, context) => {
`Votes: ${player.votes}`}
- {player.actions?.map((action) => (
+ {player.possible_actions?.map((action) => (
- act('mf_targ_action', {
- atype: action,
+ act('perform_action', {
+ action_ref: action.ref,
target: player.ref,
})
}>
- {action}
+ {action.name}
))}
diff --git a/tgui/packages/tgui/interfaces/PortableScrubber.tsx b/tgui/packages/tgui/interfaces/PortableScrubber.tsx
index 0c747afe9f7d..4ce813255c1f 100644
--- a/tgui/packages/tgui/interfaces/PortableScrubber.tsx
+++ b/tgui/packages/tgui/interfaces/PortableScrubber.tsx
@@ -1,9 +1,10 @@
-import { BooleanLike } from 'common/react';
-import { useBackend } from '../backend';
import { Button, Section } from '../components';
-import { getGasLabel } from '../constants';
-import { Window } from '../layouts';
+
+import { BooleanLike } from 'common/react';
import { PortableBasicInfo } from './common/PortableAtmos';
+import { Window } from '../layouts';
+import { getGasLabel } from '../constants';
+import { useBackend } from '../backend';
type Data = {
filterTypes: Filter[];
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/multiz_performance.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/multiz_performance.tsx
index 1d3a70f144c9..f844b9f41f2a 100644
--- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/multiz_performance.tsx
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/multiz_performance.tsx
@@ -6,6 +6,7 @@ export const multiz_performance: Feature = {
description: 'How detailed multi-z is. Lower this to improve performance',
component: createDropdownInput({
[-1]: 'Standard',
+ 2: 'High',
1: 'Medium',
0: 'Low',
}),
diff --git a/tgui/packages/tgui/interfaces/ProduceConsole.js b/tgui/packages/tgui/interfaces/ProduceConsole.tsx
similarity index 80%
rename from tgui/packages/tgui/interfaces/ProduceConsole.js
rename to tgui/packages/tgui/interfaces/ProduceConsole.tsx
index 99d718e68e98..a467beceb553 100644
--- a/tgui/packages/tgui/interfaces/ProduceConsole.js
+++ b/tgui/packages/tgui/interfaces/ProduceConsole.tsx
@@ -1,3 +1,4 @@
+import { BooleanLike } from 'common/react';
import { capitalize, createSearch } from 'common/string';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Dimmer, Divider, Icon, Input, NumberInput, Section, Stack, Tabs } from '../components';
@@ -5,6 +6,36 @@ import { Window } from '../layouts';
const buttonWidth = 2;
+type OrderDatum = {
+ name: string;
+ desc: number;
+ cat: string;
+ ref: string;
+ cost: number;
+ product_icon: string;
+};
+
+type Item = {
+ name: string;
+ amt: number;
+};
+
+type Data = {
+ credit_type: string;
+ off_cooldown: BooleanLike;
+ points: number;
+ express_tooltip: string;
+ purchase_tooltip: string;
+ forced_express: string;
+ cargo_value: number;
+ cargo_cost_multiplier: number;
+ express_cost_multiplier: number;
+ order_categories: string[];
+ order_datums: OrderDatum[];
+ item_amts: Item[];
+ total_cost: number;
+};
+
const TAB2NAME = [
{
component: () => ShoppingTab,
@@ -20,34 +51,41 @@ const findAmount = (item_amts, name) => {
};
const ShoppingTab = (props, context) => {
- const { data, act } = useBackend(context);
+ const { data, act } = useBackend(context);
const { credit_type, order_categories, order_datums, item_amts } = data;
- const [shopIndex, setShopIndex] = useLocalState(context, 'shop-index', 1);
- const [condensed, setCondensed] = useLocalState(context, 'condensed', false);
+ const [shopCategory, setShopCategory] = useLocalState(
+ context,
+ 'shopCategory',
+ order_categories[0]
+ );
+ const [condensed] = useLocalState(context, 'condensed', false);
const [searchItem, setSearchItem] = useLocalState(context, 'searchItem', '');
- const search = createSearch(searchItem, (order_datums) => order_datums.name);
+ const search = createSearch(
+ searchItem,
+ (order_datums) => order_datums.name
+ );
let goods =
searchItem.length > 0
- ? data.order_datums.filter(search)
- : order_datums.filter((item) => item && item.cat === shopIndex);
+ ? order_datums.filter((item) => search(item) && item.cat === shopCategory)
+ : order_datums.filter((item) => item && item.cat === shopCategory);
return (
- {order_categories.map((item, key) => (
+ {order_categories.map((category) => (
{
- setShopIndex(item);
+ setShopCategory(category);
if (searchItem.length > 0) {
setSearchItem('');
}
}}>
- {item}
+ {category}
))}
@@ -60,10 +98,6 @@ const ShoppingTab = (props, context) => {
value={searchItem}
onInput={(e, value) => {
setSearchItem(value);
-
- if (value.length > 0) {
- setShopIndex(1);
- }
}}
fluid
/>
@@ -75,8 +109,8 @@ const ShoppingTab = (props, context) => {
- {goods.map((item) => (
-
+ {goods.map((item, key) => (
+
{
};
const CheckoutTab = (props, context) => {
- const { data, act } = useBackend(context);
+ const { data, act } = useBackend(context);
const {
credit_type,
purchase_tooltip,
express_tooltip,
forced_express,
+ cargo_value,
order_datums,
total_cost,
+ cargo_cost_multiplier,
+ express_cost_multiplier,
item_amts,
} = data;
-
+ const total_cargo_cost = Math.floor(total_cost * cargo_cost_multiplier);
const checkout_list = order_datums.filter(
(food) => food && (findAmount(item_amts, food.name) || 0)
);
@@ -187,8 +224,8 @@ const CheckoutTab = (props, context) => {
>
)}
- {checkout_list.map((item) => (
-
+ {checkout_list.map((item, key) => (
+
{capitalize(item.name)}
@@ -228,7 +265,8 @@ const CheckoutTab = (props, context) => {
- Total Cost: {total_cost}
+ Total Cost:{total_cargo_cost}(Express:
+ {total_cost * express_cost_multiplier})
{!forced_express && (
@@ -236,7 +274,12 @@ const CheckoutTab = (props, context) => {
fluid
icon="plane-departure"
content="Purchase"
- tooltip={purchase_tooltip}
+ disabled={total_cargo_cost < cargo_value}
+ tooltip={
+ total_cargo_cost < cargo_value
+ ? `Total Cost must be above or equal to ${cargo_value}`
+ : purchase_tooltip
+ }
tooltipPosition="top"
onClick={() => act('purchase')}
/>
@@ -248,7 +291,10 @@ const CheckoutTab = (props, context) => {
icon="parachute-box"
color="yellow"
content="Express"
- tooltip={express_tooltip}
+ disabled={total_cost <= 0}
+ tooltip={
+ total_cost <= 0 ? 'Order atleast 1 item' : express_tooltip
+ }
tooltipPosition="top-start"
onClick={() => act('express')}
/>
@@ -261,7 +307,6 @@ const CheckoutTab = (props, context) => {
};
const OrderSent = (props, context) => {
- const { act, data } = useBackend(context);
return (
@@ -277,13 +322,13 @@ const OrderSent = (props, context) => {
};
export const ProduceConsole = (props, context) => {
- const { act, data } = useBackend(context);
- const { points, off_cooldown } = data;
+ const { data } = useBackend(context);
+ const { points, off_cooldown, order_categories } = data;
const [tabIndex, setTabIndex] = useLocalState(context, 'tab-index', 1);
const [condensed, setCondensed] = useLocalState(context, 'condensed', false);
const TabComponent = TAB2NAME[tabIndex - 1].component();
return (
-
+
{!off_cooldown && }
diff --git a/tgui/packages/tgui/interfaces/Reflector.tsx b/tgui/packages/tgui/interfaces/Reflector.tsx
new file mode 100644
index 000000000000..a5f3e24292fa
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/Reflector.tsx
@@ -0,0 +1,242 @@
+import { useBackend } from '../backend';
+import { Box, Button, Stack, Icon, LabeledControls, Section, NumberInput, Table } from '../components';
+import { Window } from '../layouts';
+
+type Data = {
+ reflector_name: string;
+ rotation_angle: number;
+};
+export const Reflector = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { reflector_name, rotation_angle } = data;
+ return (
+
+
+
+
+
+
+
+
+
+ act('rotate', {
+ rotation_angle: 315,
+ })
+ }
+ />
+
+
+
+ act('rotate', {
+ rotation_angle: 270,
+ })
+ }
+ />
+
+
+
+ act('rotate', {
+ rotation_angle: 225,
+ })
+ }
+ />
+
+
+
+
+
+ act('rotate', {
+ rotation_angle: 0,
+ })
+ }
+ />
+
+
+
+
+
+
+
+
+ act('rotate', {
+ rotation_angle: 180,
+ })
+ }
+ />
+
+
+
+
+
+ act('rotate', {
+ rotation_angle: 45,
+ })
+ }
+ />
+
+
+
+ act('rotate', {
+ rotation_angle: 90,
+ })
+ }
+ />
+
+
+
+ act('rotate', {
+ rotation_angle: 135,
+ })
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+ act('rotate', {
+ rotation_angle: value,
+ })
+ }
+ />
+
+
+
+
+
+
+ act('calculate', {
+ rotation_angle: -5,
+ })
+ }
+ />
+
+
+
+ act('calculate', {
+ rotation_angle: -10,
+ })
+ }
+ />
+
+
+
+ act('calculate', {
+ rotation_angle: -15,
+ })
+ }
+ />
+
+
+
+
+
+ act('calculate', {
+ rotation_angle: 5,
+ })
+ }
+ />
+
+
+
+ act('calculate', {
+ rotation_angle: 10,
+ })
+ }
+ />
+
+
+
+ act('calculate', {
+ rotation_angle: 15,
+ })
+ }
+ />
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/common/InputButtons.tsx b/tgui/packages/tgui/interfaces/common/InputButtons.tsx
index c5f79add6e9b..d7cbc8cd7aa4 100644
--- a/tgui/packages/tgui/interfaces/common/InputButtons.tsx
+++ b/tgui/packages/tgui/interfaces/common/InputButtons.tsx
@@ -1,13 +1,14 @@
-import { useBackend } from '../../backend';
import { Box, Button, Flex } from '../../components';
+import { useBackend } from '../../backend';
+
type InputButtonsData = {
large_buttons: boolean;
swapped_buttons: boolean;
};
type InputButtonsProps = {
- input: string | number;
+ input: string | number | string[];
message?: string;
};
diff --git a/tgui/packages/tgui/links.test.ts b/tgui/packages/tgui/links.test.ts
new file mode 100644
index 000000000000..3e2b5b8fda96
--- /dev/null
+++ b/tgui/packages/tgui/links.test.ts
@@ -0,0 +1,79 @@
+import { captureExternalLinks } from './links';
+
+describe('captureExternalLinks', () => {
+ let addEventListenerSpy;
+ let clickHandler;
+
+ beforeEach(() => {
+ addEventListenerSpy = jest.spyOn(document, 'addEventListener');
+ captureExternalLinks();
+ clickHandler = addEventListenerSpy.mock.calls[0][1];
+ });
+
+ afterEach(() => {
+ addEventListenerSpy.mockRestore();
+ });
+
+ it('should subscribe to document clicks', () => {
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
+ 'click',
+ expect.any(Function)
+ );
+ });
+
+ it('should preventDefault and send a message when a non-BYOND external link is clicked', () => {
+ const externalLink = {
+ tagName: 'A',
+ getAttribute: () => 'https://example.com',
+ parentElement: document.body,
+ };
+ const byond = { sendMessage: jest.fn() };
+ // @ts-ignore
+ global.Byond = byond;
+
+ const evt = { target: externalLink, preventDefault: jest.fn() };
+ clickHandler(evt);
+
+ expect(evt.preventDefault).toHaveBeenCalled();
+ expect(byond.sendMessage).toHaveBeenCalledWith({
+ type: 'openLink',
+ url: 'https://example.com',
+ });
+ });
+
+ it('should not preventDefault or send a message when a BYOND link is clicked', () => {
+ const byondLink = {
+ tagName: 'A',
+ getAttribute: () => 'byond://server-address',
+ parentElement: document.body,
+ };
+ const byond = { sendMessage: jest.fn() };
+ // @ts-ignore
+ global.Byond = byond;
+
+ const evt = { target: byondLink, preventDefault: jest.fn() };
+ clickHandler(evt);
+
+ expect(evt.preventDefault).not.toHaveBeenCalled();
+ expect(byond.sendMessage).not.toHaveBeenCalled();
+ });
+
+ it('should add https:// to www links', () => {
+ const wwwLink = {
+ tagName: 'A',
+ getAttribute: () => 'www.example.com',
+ parentElement: document.body,
+ };
+ const byond = { sendMessage: jest.fn() };
+ // @ts-ignore
+ global.Byond = byond;
+
+ const evt = { target: wwwLink, preventDefault: jest.fn() };
+ clickHandler(evt);
+
+ expect(byond.sendMessage).toHaveBeenCalledWith({
+ type: 'openLink',
+ url: 'https://www.example.com',
+ });
+ });
+});
diff --git a/tgui/packages/tgui/links.js b/tgui/packages/tgui/links.ts
similarity index 76%
rename from tgui/packages/tgui/links.js
rename to tgui/packages/tgui/links.ts
index 5775500b7431..6c2355331fcd 100644
--- a/tgui/packages/tgui/links.js
+++ b/tgui/packages/tgui/links.ts
@@ -9,9 +9,8 @@
*/
export const captureExternalLinks = () => {
// Subscribe to all document clicks
- document.addEventListener('click', (e) => {
- /** @type {HTMLElement} */
- let target = e.target;
+ document.addEventListener('click', (evt: MouseEvent) => {
+ let target = evt.target as HTMLElement;
// Recurse down the tree to find a valid link
while (true) {
// Reached the end, bail.
@@ -22,18 +21,17 @@ export const captureExternalLinks = () => {
if (tagName === 'a') {
break;
}
- target = target.parentElement;
+ target = target.parentElement as HTMLElement;
}
const hrefAttr = target.getAttribute('href') || '';
// Leave BYOND links alone
- // prettier-ignore
- const isByondLink = hrefAttr.charAt(0) === '?'
- || hrefAttr.startsWith('byond://');
+ const isByondLink =
+ hrefAttr.charAt(0) === '?' || hrefAttr.startsWith('byond://');
if (isByondLink) {
return;
}
// Prevent default action
- e.preventDefault();
+ evt.preventDefault();
// Normalize the URL
let url = hrefAttr;
if (url.toLowerCase().startsWith('www')) {
diff --git a/tgui/packages/tgui/logging.js b/tgui/packages/tgui/logging.js
deleted file mode 100644
index 11241611eed8..000000000000
--- a/tgui/packages/tgui/logging.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-import { sendLogEntry } from 'tgui-dev-server/link/client.cjs';
-
-const LEVEL_DEBUG = 0;
-const LEVEL_LOG = 1;
-const LEVEL_INFO = 2;
-const LEVEL_WARN = 3;
-const LEVEL_ERROR = 4;
-
-const log = (level, ns, ...args) => {
- // Send logs to a remote log collector
- if (process.env.NODE_ENV !== 'production') {
- sendLogEntry(level, ns, ...args);
- }
- // Send important logs to the backend
- if (level >= LEVEL_INFO) {
- // prettier-ignore
- const logEntry = [ns, ...args]
- .map(value => {
- if (typeof value === 'string') {
- return value;
- }
- if (value instanceof Error) {
- return value.stack || String(value);
- }
- return JSON.stringify(value);
- })
- .filter(value => value)
- .join(' ')
- + '\nUser Agent: ' + navigator.userAgent;
- Byond.sendMessage({
- type: 'log',
- ns,
- message: logEntry,
- });
- }
-};
-
-export const createLogger = (ns) => {
- return {
- debug: (...args) => log(LEVEL_DEBUG, ns, ...args),
- log: (...args) => log(LEVEL_LOG, ns, ...args),
- info: (...args) => log(LEVEL_INFO, ns, ...args),
- warn: (...args) => log(LEVEL_WARN, ns, ...args),
- error: (...args) => log(LEVEL_ERROR, ns, ...args),
- };
-};
-
-/**
- * A generic instance of the logger.
- *
- * Does not have a namespace associated with it.
- */
-export const logger = createLogger();
diff --git a/tgui/packages/tgui/logging.ts b/tgui/packages/tgui/logging.ts
new file mode 100644
index 000000000000..cc2e9d2f0187
--- /dev/null
+++ b/tgui/packages/tgui/logging.ts
@@ -0,0 +1,68 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+import { sendLogEntry } from 'tgui-dev-server/link/client.cjs';
+
+const LEVEL_DEBUG = 0;
+const LEVEL_LOG = 1;
+const LEVEL_INFO = 2;
+const LEVEL_WARN = 3;
+const LEVEL_ERROR = 4;
+
+interface Logger {
+ debug: (...args: any[]) => void;
+ log: (...args: any[]) => void;
+ info: (...args: any[]) => void;
+ warn: (...args: any[]) => void;
+ error: (...args: any[]) => void;
+}
+
+const log = (level: number, namespace = 'Generic', ...args: any[]): void => {
+ // Send logs to a remote log collector
+ if (process.env.NODE_ENV !== 'production') {
+ sendLogEntry(level, namespace, ...args);
+ }
+ // Send important logs to the backend
+ if (level >= LEVEL_INFO) {
+ const logEntry =
+ [namespace, ...args]
+ .map((value) => {
+ if (typeof value === 'string') {
+ return value;
+ }
+ if (value instanceof Error) {
+ return value.stack || String(value);
+ }
+ return JSON.stringify(value);
+ })
+ .filter((value) => value)
+ .join(' ') +
+ '\nUser Agent: ' +
+ navigator.userAgent;
+ Byond.sendMessage({
+ type: 'log',
+ ns: namespace,
+ message: logEntry,
+ });
+ }
+};
+
+export const createLogger = (namespace?: string): Logger => {
+ return {
+ debug: (...args) => log(LEVEL_DEBUG, namespace, ...args),
+ log: (...args) => log(LEVEL_LOG, namespace, ...args),
+ info: (...args) => log(LEVEL_INFO, namespace, ...args),
+ warn: (...args) => log(LEVEL_WARN, namespace, ...args),
+ error: (...args) => log(LEVEL_ERROR, namespace, ...args),
+ };
+};
+
+/**
+ * A generic instance of the logger.
+ *
+ * Does not have a namespace associated with it.
+ */
+export const logger: Logger = createLogger();
diff --git a/tgui/packages/tgui/routes.js b/tgui/packages/tgui/routes.tsx
similarity index 65%
rename from tgui/packages/tgui/routes.js
rename to tgui/packages/tgui/routes.tsx
index eb4ddff15393..b841c84b8875 100644
--- a/tgui/packages/tgui/routes.js
+++ b/tgui/packages/tgui/routes.tsx
@@ -4,32 +4,36 @@
* @license MIT
*/
-import { selectBackend } from './backend';
import { Icon, Section, Stack } from './components';
-import { selectDebug } from './debug/selectors';
+
+import { Store } from 'common/redux';
import { Window } from './layouts';
+import { selectBackend } from './backend';
+import { selectDebug } from './debug/selectors';
const requireInterface = require.context('./interfaces');
-const routingError = (type, name) => () => {
- return (
-
-
- {type === 'notFound' && (
-
- Interface {name} was not found.
-
- )}
- {type === 'missingExport' && (
-
- Interface {name} is missing an export.
-
- )}
-
-
- );
-};
+const routingError =
+ (type: 'notFound' | 'missingExport', name: string) => () => {
+ return (
+
+
+ {type === 'notFound' && (
+
+ Interface {name} was not found.
+
+ )}
+ {type === 'missingExport' && (
+
+ Interface {name} is missing an export.
+
+ )}
+
+
+ );
+ };
+// Displays an empty Window with scrollable content
const SuspendedWindow = () => {
return (
@@ -38,6 +42,7 @@ const SuspendedWindow = () => {
);
};
+// Displays a loading screen with a spinning icon
const RefreshingWindow = () => {
return (
@@ -55,7 +60,8 @@ const RefreshingWindow = () => {
);
};
-export const getRoutedComponent = (store) => {
+// Get the component for the current route
+export const getRoutedComponent = (store: Store) => {
const state = store.getState();
const { suspended, config } = selectBackend(state);
if (suspended) {
@@ -73,14 +79,14 @@ export const getRoutedComponent = (store) => {
}
const name = config?.interface;
const interfacePathBuilders = [
- (name) => `./${name}.tsx`,
- (name) => `./${name}.js`,
- (name) => `./${name}/index.tsx`,
- (name) => `./${name}/index.js`,
+ (name: string) => `./${name}.tsx`,
+ (name: string) => `./${name}.js`,
+ (name: string) => `./${name}/index.tsx`,
+ (name: string) => `./${name}/index.js`,
];
let esModule;
while (!esModule && interfacePathBuilders.length > 0) {
- const interfacePathBuilder = interfacePathBuilders.shift();
+ const interfacePathBuilder = interfacePathBuilders.shift()!;
const interfacePath = interfacePathBuilder(name);
try {
esModule = requireInterface(interfacePath);
diff --git a/tgui/packages/tgui/sanitize.test.ts b/tgui/packages/tgui/sanitize.test.ts
new file mode 100644
index 000000000000..b1adb94ca074
--- /dev/null
+++ b/tgui/packages/tgui/sanitize.test.ts
@@ -0,0 +1,36 @@
+import { sanitizeText } from './sanitize';
+
+describe('sanitizeText', () => {
+ it('should sanitize basic HTML input', () => {
+ const input = 'Hello, world!';
+ const expected = 'Hello, world!';
+ const result = sanitizeText(input);
+ expect(result).toBe(expected);
+ });
+
+ it('should sanitize advanced HTML input when advHtml flag is true', () => {
+ const input =
+ 'Hello, world!';
+ const expected = 'Hello, world!';
+ const result = sanitizeText(input, true);
+ expect(result).toBe(expected);
+ });
+
+ it('should allow specific HTML tags when tags array is provided', () => {
+ const input = 'Hello, world!Goodbye, world!';
+ const tags = ['b'];
+ const expected = 'Hello, world!Goodbye, world!';
+ const result = sanitizeText(input, false, tags);
+ expect(result).toBe(expected);
+ });
+
+ it('should allow advanced HTML tags when advTags array is provided and advHtml flag is true', () => {
+ const input =
+ 'Hello, world!';
+ const advTags = ['iframe'];
+ const expected =
+ 'Hello, world!';
+ const result = sanitizeText(input, true, undefined, undefined, advTags);
+ expect(result).toBe(expected);
+ });
+});
diff --git a/tgui/packages/tgui/sanitize.js b/tgui/packages/tgui/sanitize.ts
similarity index 74%
rename from tgui/packages/tgui/sanitize.js
rename to tgui/packages/tgui/sanitize.ts
index 008bba049af1..a40d23a320d3 100644
--- a/tgui/packages/tgui/sanitize.js
+++ b/tgui/packages/tgui/sanitize.ts
@@ -53,15 +53,15 @@ const defAttr = ['class', 'style'];
/**
* Feed it a string and it should spit out a sanitized version.
*
- * @param {string} input
- * @param {boolean} advHtml
- * @param {array} tags
- * @param {array} forbidAttr
- * @param {array} advTags
+ * @param input - Input HTML string to sanitize
+ * @param advHtml - Flag to enable/disable advanced HTML
+ * @param tags - List of allowed HTML tags
+ * @param forbidAttr - List of forbidden HTML attributes
+ * @param advTags - List of advanced HTML tags allowed for trusted sources
*/
export const sanitizeText = (
- input,
- advHtml,
+ input: string,
+ advHtml = false,
tags = defTag,
forbidAttr = defAttr,
advTags = advTag
@@ -69,7 +69,7 @@ export const sanitizeText = (
// This is VERY important to think first if you NEED
// the tag you put in here. We are pushing all this
// though dangerouslySetInnerHTML and even though
- // the default DOMPurify kills javascript, it dosn't
+ // the default DOMPurify kills javascript, it doesn't
// kill href links or such
if (advHtml) {
tags = tags.concat(advTags);
diff --git a/tools/UpdatePaths/Scripts/74651_apc_to_apc_helpers.txt b/tools/UpdatePaths/Scripts/74651_apc_to_apc_helpers.txt
new file mode 100644
index 000000000000..34c4c581f526
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/74651_apc_to_apc_helpers.txt
@@ -0,0 +1,38 @@
+#comment Repaths subtypes and some commonly used properties of apc to apc helpers. You really should change it if you have other apc's that this or else it might break everything.
+
+/obj/machinery/power/apc/unlocked : /obj/machinery/power/apc/auto_name{@OLD}, /obj/effect/mapping_helpers/apc/unlocked
+/obj/machinery/power/apc/syndicate : /obj/machinery/power/apc/auto_name{@OLD}, /obj/effect/mapping_helpers/apc/syndicate_access
+/obj/machinery/power/apc/away : /obj/machinery/power/apc/auto_name{@OLD}, /obj/effect/mapping_helpers/apc/away_general_access
+/obj/machinery/power/apc/highcap/five_k : /obj/machinery/power/apc/auto_name{@OLD}, /obj/effect/mapping_helpers/apc/cell_5k
+/obj/machinery/power/apc/highcap/five_k/directional/north : /obj/machinery/power/apc/auto_name/directional/north{@OLD}, /obj/effect/mapping_helpers/apc/cell_5k
+/obj/machinery/power/apc/highcap/five_k/directional/south : /obj/machinery/power/apc/auto_name/directional/south{@OLD}, /obj/effect/mapping_helpers/apc/cell_5k
+/obj/machinery/power/apc/highcap/five_k/directional/east : /obj/machinery/power/apc/auto_name/directional/east{@OLD}, /obj/effect/mapping_helpers/apc/cell_5k
+/obj/machinery/power/apc/highcap/five_k/directional/west : /obj/machinery/power/apc/auto_name/directional/west{@OLD}, /obj/effect/mapping_helpers/apc/cell_5k
+/obj/machinery/power/apc/highcap/ten_k : /obj/machinery/power/apc/auto_name{@OLD}, /obj/effect/mapping_helpers/apc/cell_10k
+/obj/machinery/power/apc/highcap/ten_k/directional/north : /obj/machinery/power/apc/auto_name/directional/north{@OLD}, /obj/effect/mapping_helpers/apc/cell_10k
+/obj/machinery/power/apc/highcap/ten_k/directional/south : /obj/machinery/power/apc/auto_name/directional/south{@OLD}, /obj/effect/mapping_helpers/apc/cell_10k
+/obj/machinery/power/apc/highcap/ten_k/directional/east : /obj/machinery/power/apc/auto_name/directional/east{@OLD}, /obj/effect/mapping_helpers/apc/cell_10k
+/obj/machinery/power/apc/highcap/ten_k/directional/west : /obj/machinery/power/apc/auto_name/directional/west{@OLD}, /obj/effect/mapping_helpers/apc/cell_10k
+/obj/machinery/power/apc/sm_apc : /obj/machinery/power/apc/auto_name{@OLD}, /obj/effect/mapping_helpers/apc/cell_10k
+/obj/machinery/power/apc/sm_apc/directional/north : /obj/machinery/power/apc/auto_name/directional/north{@OLD}, /obj/effect/mapping_helpers/apc/cell_10k
+/obj/machinery/power/apc/sm_apc/directional/south : /obj/machinery/power/apc/auto_name/directional/south{@OLD}, /obj/effect/mapping_helpers/apc/cell_10k
+/obj/machinery/power/apc/sm_apc/directional/east : /obj/machinery/power/apc/auto_name/directional/east{@OLD}, /obj/effect/mapping_helpers/apc/cell_10k
+/obj/machinery/power/apc/sm_apc/directional/west : /obj/machinery/power/apc/auto_name/directional/west{@OLD}, /obj/effect/mapping_helpers/apc/cell_10k
+/obj/machinery/power/apc/@SUBTYPES{pixel_y = 25} : /obj/machinery/power/apc/auto_name/directional/north{@OLD;pixel_y=@SKIP;dir=@SKIP}
+/obj/machinery/power/apc/@SUBTYPES{pixel_y = -25} : /obj/machinery/power/apc/auto_name/directional/south{@OLD;pixel_y=@SKIP;dir=@SKIP}
+/obj/machinery/power/apc/@SUBTYPES{pixel_x = 25} : /obj/machinery/power/apc/auto_name/directional/east{@OLD;pixel_x=@SKIP;dir=@SKIP}
+/obj/machinery/power/apc/@SUBTYPES{pixel_x = -25} : /obj/machinery/power/apc/auto_name/directional/west{@OLD;pixel_x=@SKIP;dir=@SKIP}
+/obj/machinery/power/apc/@SUBTYPES{name = "Worn Out APC"} : /obj/machinery/power/apc/worn_out{@OLD;name=@SKIP}
+/obj/machinery/power/apc/auto_name/directional/north : /obj/machinery/power/apc/auto_name/directional/north{@OLD;name=@SKIP}
+/obj/machinery/power/apc/auto_name/directional/south : /obj/machinery/power/apc/auto_name/directional/south{@OLD;name=@SKIP}
+/obj/machinery/power/apc/auto_name/directional/east : /obj/machinery/power/apc/auto_name/directional/east{@OLD;name=@SKIP}
+/obj/machinery/power/apc/auto_name/directional/west : /obj/machinery/power/apc/auto_name/directional/west{@OLD;name=@SKIP}
+/obj/machinery/power/apc/@SUBTYPES{aidisabled = 1} : @OLD{@OLD;aidisabled=@SKIP}, /obj/effect/mapping_helpers/apc/cut_AI_wire
+/obj/machinery/power/apc/@SUBTYPES{locked = 0} : @OLD{@OLD;locked=@SKIP}, /obj/effect/mapping_helpers/apc/unlocked
+/obj/machinery/power/apc/@SUBTYPES{req_access = list(ACCESS_AWAY_GENERAL)} : @OLD{@OLD;req_access=@SKIP}, /obj/effect/mapping_helpers/apc/away_general_access
+/obj/machinery/power/apc/@SUBTYPES{req_access = list(ACCESS_SYNDICATE)} : @OLD{@OLD;req_access=@SKIP}, /obj/effect/mapping_helpers/apc/syndicate_access
+/obj/machinery/power/apc/@SUBTYPES{req_access = list("syndicate")} : @OLD{@OLD;req_access=@SKIP}, /obj/effect/mapping_helpers/apc/syndicate_access
+/obj/machinery/power/apc/@SUBTYPES{cell_type = /obj/item/stock_parts/cell/upgraded/plus} : @OLD{@OLD;cell_type=@SKIP}, /obj/effect/mapping_helpers/apc/cell_5k
+/obj/machinery/power/apc/@SUBTYPES{cell_type = /obj/item/stock_parts/cell/high} : @OLD{@OLD;cell_type=@SKIP}, /obj/effect/mapping_helpers/apc/cell_10k
+/obj/machinery/power/apc/@SUBTYPES{start_charge = 100} : @OLD{@OLD;start_charge=@SKIP}, /obj/effect/mapping_helpers/apc/full_charge
+/obj/machinery/power/apc/@SUBTYPES{start_charge = 0} : @OLD{@OLD;start_charge=@SKIP}, /obj/effect/mapping_helpers/apc/no_charge
\ No newline at end of file
diff --git a/tools/UpdatePaths/Scripts/74747_space_bat.txt b/tools/UpdatePaths/Scripts/74747_space_bat.txt
new file mode 100644
index 000000000000..744eefc296a7
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/74747_space_bat.txt
@@ -0,0 +1,3 @@
+# turning space bat simple animals into basic mobs
+
+/mob/living/simple_animal/hostile/retaliate/bat : /mob/living/basic/bat{@OLD}
diff --git a/tools/UpdatePaths/Scripts/74812_tree_repath.txt b/tools/UpdatePaths/Scripts/74812_tree_repath.txt
new file mode 100644
index 000000000000..ca3d98a73b0e
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/74812_tree_repath.txt
@@ -0,0 +1,2 @@
+/mob/living/simple_animal/hostile/tree : /mob/living/basic/tree{@OLD;wander=@SKIP}
+/mob/living/simple_animal/hostile/tree/festivus : /mob/living/basic/festivus{@OLD;wander=@SKIP}
\ No newline at end of file
diff --git a/tools/WebhookProcessor/github_webhook_processor.php b/tools/WebhookProcessor/github_webhook_processor.php
index c6c6e0f3061b..3f3802c4625c 100644
--- a/tools/WebhookProcessor/github_webhook_processor.php
+++ b/tools/WebhookProcessor/github_webhook_processor.php
@@ -705,17 +705,11 @@ function checkchangelog($payload) {
$tags[] = 'Quality of Life';
}
break;
- case 'soundadd':
- if($item != 'added a new sound thingy') {
+ case 'sound':
+ if($item != 'added/modified/removed audio or sound effects') {
$tags[] = 'Sound';
}
break;
- case 'sounddel':
- if($item != 'removed an old sound thingy') {
- $tags[] = 'Sound';
- $tags[] = 'Removal';
- }
- break;
case 'add':
case 'adds':
case 'rscadd':
@@ -730,17 +724,11 @@ function checkchangelog($payload) {
$tags[] = 'Removal';
}
break;
- case 'imageadd':
- if($item != 'added some icons and images') {
+ case 'image':
+ if($item != 'added/modified/removed some icons or images') {
$tags[] = 'Sprites';
}
break;
- case 'imagedel':
- if($item != 'deleted some icons and images') {
- $tags[] = 'Sprites';
- $tags[] = 'Removal';
- }
- break;
case 'typo':
case 'spellcheck':
if($item != 'fixed a few typos') {
diff --git a/tools/midi2piano/MidiDependencies/midi.py b/tools/midi2piano/MidiDependencies/midi.py
index c1c6df640784..9a40784cc53e 100644
--- a/tools/midi2piano/MidiDependencies/midi.py
+++ b/tools/midi2piano/MidiDependencies/midi.py
@@ -247,9 +247,9 @@ def score2opus(score=None):
abs_time = 0
for event in sorted_events: # convert abs times => delta times
- delta_time = event[1] - abs_time
+ seconds_per_tick = event[1] - abs_time
abs_time = event[1]
- event[1] = delta_time
+ event[1] = seconds_per_tick
opus_tracks.append(sorted_events)
opus_tracks.insert(0,ticks)
_clean_up_warnings()
diff --git a/tools/midi2piano/midi2piano.py b/tools/midi2piano/midi2piano.py
index ab57bcc3f183..591cd5d2a65f 100644
--- a/tools/midi2piano/midi2piano.py
+++ b/tools/midi2piano/midi2piano.py
@@ -170,9 +170,9 @@ def sort_score_by_event_times(score):
key=lambda indx: score[indx][0])
))
-def convert_into_delta_times(score):
+def convert_into_seconds_per_ticks(score):
"""
- Transform start_time into delta_time and returns new score
+ Transform start_time into seconds_per_tick and returns new score
"""
return list(map(
lambda super_event: (
@@ -300,7 +300,7 @@ def main_cycle():
score = filter_empty_tracks(score)
score = merge_events(score)
score = sort_score_by_event_times(score)
- score = convert_into_delta_times(score)
+ score = convert_into_seconds_per_ticks(score)
score = perform_roundation(score)
most_frequent_dur = obtain_common_duration(score)
score = reduce_score_to_chords(score)
diff --git a/tools/pull_request_hooks/changelogConfig.js b/tools/pull_request_hooks/changelogConfig.js
index a607fd9a4572..c4672c879079 100644
--- a/tools/pull_request_hooks/changelogConfig.js
+++ b/tools/pull_request_hooks/changelogConfig.js
@@ -40,30 +40,16 @@ export const CHANGELOG_ENTRIES = [
],
[
- ["soundadd"],
+ ["sound"],
{
- placeholders: ["added a new sound thingy"],
+ placeholders: ["added/modified/removed audio or sound effects"],
},
],
[
- ["sounddel"],
+ ["image"],
{
- placeholders: ["removed an old sound thingy"],
- },
- ],
-
- [
- ["imageadd"],
- {
- placeholders: ["added some icons and images"],
- },
- ],
-
- [
- ["imagedel"],
- {
- placeholders: ["deleted some icons and images"],
+ placeholders: ["added/modified/removed some icons or images"],
},
],