From 50d609242dab94b9da95b002c41ce802da397cd8 Mon Sep 17 00:00:00 2001 From: Jeremiah <42397676+jlsnow301@users.noreply.github.com> Date: Wed, 3 Sep 2025 01:01:44 -0700 Subject: [PATCH] [tgui] uselocalstate maintenance 2 (#92716) ## About The Pull Request Refactors much of spellbook to rid it of some bad conventions and improves style Replaces use of uselocalstate -> useAtom Moves spellbook to a folder to improve readability ## Why It's Good For The Game useLocalState is deprecated ## Changelog --- tgui/packages/tgui/interfaces/Spellbook.tsx | 792 ------------------ .../interfaces/Spellbook/CategoryDisplay.tsx | 51 ++ .../interfaces/Spellbook/EnscribedName.tsx | 24 + .../tgui/interfaces/Spellbook/Loadouts.tsx | 113 +++ .../tgui/interfaces/Spellbook/Locked.tsx | 27 + .../tgui/interfaces/Spellbook/Randomize.tsx | 51 ++ .../interfaces/Spellbook/SearchSpells.tsx | 60 ++ .../interfaces/Spellbook/SpellResults.tsx | 71 ++ .../interfaces/Spellbook/SpellTabDisplay.tsx | 122 +++ .../interfaces/Spellbook/TableOfContents.tsx | 101 +++ .../tgui/interfaces/Spellbook/constants.ts | 82 ++ .../tgui/interfaces/Spellbook/index.tsx | 102 +++ .../tgui/interfaces/Spellbook/types.ts | 71 ++ 13 files changed, 875 insertions(+), 792 deletions(-) delete mode 100644 tgui/packages/tgui/interfaces/Spellbook.tsx create mode 100644 tgui/packages/tgui/interfaces/Spellbook/CategoryDisplay.tsx create mode 100644 tgui/packages/tgui/interfaces/Spellbook/EnscribedName.tsx create mode 100644 tgui/packages/tgui/interfaces/Spellbook/Loadouts.tsx create mode 100644 tgui/packages/tgui/interfaces/Spellbook/Locked.tsx create mode 100644 tgui/packages/tgui/interfaces/Spellbook/Randomize.tsx create mode 100644 tgui/packages/tgui/interfaces/Spellbook/SearchSpells.tsx create mode 100644 tgui/packages/tgui/interfaces/Spellbook/SpellResults.tsx create mode 100644 tgui/packages/tgui/interfaces/Spellbook/SpellTabDisplay.tsx create mode 100644 tgui/packages/tgui/interfaces/Spellbook/TableOfContents.tsx create mode 100644 tgui/packages/tgui/interfaces/Spellbook/constants.ts create mode 100644 tgui/packages/tgui/interfaces/Spellbook/index.tsx create mode 100644 tgui/packages/tgui/interfaces/Spellbook/types.ts diff --git a/tgui/packages/tgui/interfaces/Spellbook.tsx b/tgui/packages/tgui/interfaces/Spellbook.tsx deleted file mode 100644 index b349d36d1c4..00000000000 --- a/tgui/packages/tgui/interfaces/Spellbook.tsx +++ /dev/null @@ -1,792 +0,0 @@ -import type { ReactNode } from 'react'; -import { - Box, - Button, - Dimmer, - Divider, - Icon, - Input, - NoticeBox, - ProgressBar, - Section, - Stack, -} from 'tgui-core/components'; -import type { BooleanLike } from 'tgui-core/react'; - -import { useBackend, useLocalState } from '../backend'; -import { Window } from '../layouts'; - -enum SpellCategory { - Offensive = 'Offensive', - Defensive = 'Defensive', - Mobility = 'Mobility', - Assistance = 'Assistance', - Rituals = 'Rituals', - Perks = 'Perks', -} - -type byondRef = string; - -type SpellEntry = { - // Name of the spell - name: string; - // Description of what the spell does - desc: string; - // Byond REF of the spell entry datum - ref: byondRef; - // Whether the spell requires wizard clothing to cast - requires_wizard_garb: BooleanLike; - // Spell points required to buy the spell - cost: number; - // How many times the spell has been bought - times: number; - // Cooldown length of the spell once cast once - cooldown: number; - // Category of the spell - cat: SpellCategory; - // Whether the spell is refundable - refundable: BooleanLike; - // The verb displayed when buying - buyword: Buywords; -}; - -type Data = { - owner: string; - points: number; - semi_random_bonus: number; - full_random_bonus: number; - entries: SpellEntry[]; -}; - -type TabType = { - title: string; - blurb?: string; - component?: () => ReactNode; - locked?: boolean; - scrollable?: boolean; -}; - -const TAB2NAME: TabType[] = [ - { - title: 'Enscribed Name', - blurb: - "This book answers only to its owner, and of course, must have one. The permanence of the pact between a spellbook and its owner ensures such a powerful artifact cannot fall into enemy hands, or be used in ways that break the Federation's rules such as bartering spells.", - component: () => , - }, - { - title: 'Table of Contents', - component: () => , - }, - { - title: 'Offensive', - blurb: 'Spells and items geared towards debilitating and destroying.', - scrollable: true, - }, - { - title: 'Defensive', - blurb: - "Spells and items geared towards improving your survivability or reducing foes' ability to attack.", - scrollable: true, - }, - { - title: 'Mobility', - blurb: - 'Spells and items geared towards improving your ability to move. It is a good idea to take at least one.', - scrollable: true, - }, - { - title: 'Assistance', - blurb: - 'Spells and items geared towards bringing in outside forces to aid you or improving upon your other items and abilities.', - scrollable: true, - }, - { - title: 'Challenges', - blurb: - 'The Wizard Federation is looking for shows of power. Arming the station against you will increase the danger, but will grant you more charges for your spellbook.', - locked: true, - scrollable: true, - }, - { - title: 'Rituals', - blurb: - 'These powerful spells change the very fabric of reality. Not always in your favour.', - scrollable: true, - }, - { - title: 'Loadouts', - blurb: - 'The Wizard Federation accepts that sometimes, choosing is hard. You can choose from some approved wizard loadouts here.', - component: () => , - }, - { - title: 'Randomize', - blurb: - "If you didn't like the loadouts offered, you can embrace chaos. Not recommended for newer wizards.", - component: () => , - }, - { - title: 'Perks', - blurb: - 'Perks are useful (and not so useful) improvements to the soul and body collected from all corners of the universe.', - scrollable: true, - }, - { - title: 'Table of Contents', - component: () => , - }, -]; - -enum Buywords { - Learn = 'Learn', - Summon = 'Summon', - Cast = 'Cast', -} - -const BUYWORD2ICON = { - Learn: 'plus', - Summon: 'hat-wizard', - Cast: 'meteor', -}; - -const EnscribedName = (props) => { - const { data } = useBackend(); - const { owner } = data; - return ( - <> - - {owner} - - - - ); -}; - -const lineHeightToc = '30.6px'; - -const TableOfContents = (props) => { - const [tabIndex, setTabIndex] = useLocalState('tab-index', 1); - return ( - - + + {tabIndex} + + + } + > + + + + +
+ + + {tabIndex + 1} + + + } + > + +
+
+ + ); +} diff --git a/tgui/packages/tgui/interfaces/Spellbook/SpellTabDisplay.tsx b/tgui/packages/tgui/interfaces/Spellbook/SpellTabDisplay.tsx new file mode 100644 index 00000000000..c3ccd0b046a --- /dev/null +++ b/tgui/packages/tgui/interfaces/Spellbook/SpellTabDisplay.tsx @@ -0,0 +1,122 @@ +import { + Button, + Divider, + NoticeBox, + Section, + Stack, +} from 'tgui-core/components'; +import { useBackend } from '../../backend'; +import { BUYWORD2ICON } from './constants'; +import { + Buywords, + type SpellbookData, + SpellCategory, + type SpellEntry, +} from './types'; + +type Props = { + tabSpells: SpellEntry[]; + cooldownOffset?: number; + pointOffset?: number; +}; + +function getTimeOrCat(entry: SpellEntry) { + if (entry.cat === SpellCategory.Rituals) { + if (entry.times) { + return `Cast ${entry.times} times.`; + } else { + return 'Not cast yet.'; + } + } else { + if (entry.cooldown) { + return `${entry.cooldown}s Cooldown`; + } else { + return ''; + } + } +} + +export function SpellTabDisplay(props: Props) { + const { act, data } = useBackend(); + const { points } = data; + const { tabSpells, cooldownOffset, pointOffset } = props; + + return ( + + {tabSpells + .sort((a, b) => { + return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; + }) + .map((entry) => ( + + + + + {getTimeOrCat(entry)} + + + {entry.cost} points + + {entry.buyword === Buywords.Learn && ( + + +
+ {!entry.refundable ? ( + No refunds. + ) : ( + + )} +
+
+ +
+ ))} +
+ ); +} diff --git a/tgui/packages/tgui/interfaces/Spellbook/TableOfContents.tsx b/tgui/packages/tgui/interfaces/Spellbook/TableOfContents.tsx new file mode 100644 index 00000000000..8ab6fb902fe --- /dev/null +++ b/tgui/packages/tgui/interfaces/Spellbook/TableOfContents.tsx @@ -0,0 +1,101 @@ +import { useAtom } from 'jotai'; +import { Box, Button, Divider } from 'tgui-core/components'; +import { tabAtom } from '.'; +import { Tab } from './types'; + +export const lineHeightToc = '30.6px'; + +export function TableOfContents(props) { + const [_tabIndex, setTabIndex] = useAtom(tabAtom); + + return ( + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/tgui/packages/tgui/interfaces/Spellbook/constants.ts b/tgui/packages/tgui/interfaces/Spellbook/constants.ts new file mode 100644 index 00000000000..7c3760d8e69 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Spellbook/constants.ts @@ -0,0 +1,82 @@ +import { EnscribedName } from './EnscribedName'; +import { Loadouts } from './Loadouts'; +import { Randomize } from './Randomize'; +import { TableOfContents } from './TableOfContents'; +import type { TabType } from './types'; + +export const TAB2NAME: TabType[] = [ + { + title: 'Enscribed Name', + blurb: + "This book answers only to its owner, and of course, must have one. The permanence of the pact between a spellbook and its owner ensures such a powerful artifact cannot fall into enemy hands, or be used in ways that break the Federation's rules such as bartering spells.", + component: EnscribedName, + }, + { + title: 'Table of Contents', + component: TableOfContents, + }, + { + title: 'Offensive', + blurb: 'Spells and items geared towards debilitating and destroying.', + scrollable: true, + }, + { + title: 'Defensive', + blurb: + "Spells and items geared towards improving your survivability or reducing foes' ability to attack.", + scrollable: true, + }, + { + title: 'Mobility', + blurb: + 'Spells and items geared towards improving your ability to move. It is a good idea to take at least one.', + scrollable: true, + }, + { + title: 'Assistance', + blurb: + 'Spells and items geared towards bringing in outside forces to aid you or improving upon your other items and abilities.', + scrollable: true, + }, + { + title: 'Challenges', + blurb: + 'The Wizard Federation is looking for shows of power. Arming the station against you will increase the danger, but will grant you more charges for your spellbook.', + locked: true, + scrollable: true, + }, + { + title: 'Rituals', + blurb: + 'These powerful spells change the very fabric of reality. Not always in your favour.', + scrollable: true, + }, + { + title: 'Loadouts', + blurb: + 'The Wizard Federation accepts that sometimes, choosing is hard. You can choose from some approved wizard loadouts here.', + component: Loadouts, + }, + { + title: 'Randomize', + blurb: + "If you didn't like the loadouts offered, you can embrace chaos. Not recommended for newer wizards.", + component: Randomize, + }, + { + title: 'Perks', + blurb: + 'Perks are useful (and not so useful) improvements to the soul and body collected from all corners of the universe.', + scrollable: true, + }, + { + title: 'Table of Contents', + component: TableOfContents, + }, +]; + +export const BUYWORD2ICON = { + Learn: 'plus', + Summon: 'hat-wizard', + Cast: 'meteor', +}; diff --git a/tgui/packages/tgui/interfaces/Spellbook/index.tsx b/tgui/packages/tgui/interfaces/Spellbook/index.tsx new file mode 100644 index 00000000000..7df95693488 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Spellbook/index.tsx @@ -0,0 +1,102 @@ +import { sample } from 'es-toolkit'; +import { atom, useAtom } from 'jotai'; +import { useEffect, useState } from 'react'; +import { + Button, + Input, + ProgressBar, + Section, + Stack, +} from 'tgui-core/components'; +import { useBackend } from '../../backend'; +import { Window } from '../../layouts'; +import { SearchSpells } from './SearchSpells'; +import { SpellResults } from './SpellResults'; +import { type SpellbookData, Tab } from './types'; + +export const tabAtom = atom(Tab.TableOfContents); +export const spellSearchAtom = atom(''); + +export const widthSection = '466px'; +export const heightSection = '456px'; + +const searchVerbs = [ + 'Searching', + 'Seeking', + 'Contemplating', + 'Divining', + 'Scrying', + 'Peeking', + 'Pondering', + 'Gazing', + 'Studying', + 'Reviewing', +]; + +export function Spellbook(props) { + const { data } = useBackend(); + const { points } = data; + + const [selectedVerb, setSelectedVerb] = useState(searchVerbs[0]); + const [spellSearch, setSpellSearch] = useAtom(spellSearchAtom); + + useEffect(() => { + // Ensures it only changes on reset + if (spellSearch === '') { + setSelectedVerb(sample(searchVerbs)); + } + }, [spellSearch]); + + return ( + + + + + + {spellSearch.length > 1 ? ( + +
setSpellSearch('')} + > + Stop {selectedVerb} + + } + > + +
+
+ ) : ( + + )} +
+
+ +
+ + + + {`${points} points left to spend.`} + + + + + + +
+
+
+
+
+ ); +} diff --git a/tgui/packages/tgui/interfaces/Spellbook/types.ts b/tgui/packages/tgui/interfaces/Spellbook/types.ts new file mode 100644 index 00000000000..ca480ee2701 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Spellbook/types.ts @@ -0,0 +1,71 @@ +import type { BooleanLike } from 'tgui-core/react'; + +export enum SpellCategory { + Offensive = 'Offensive', + Defensive = 'Defensive', + Mobility = 'Mobility', + Assistance = 'Assistance', + Rituals = 'Rituals', + Perks = 'Perks', +} + +export type SpellEntry = { + // Name of the spell + name: string; + // Description of what the spell does + desc: string; + // Byond REF of the spell entry datum + ref: string; + // Whether the spell requires wizard clothing to cast + requires_wizard_garb: BooleanLike; + // Spell points required to buy the spell + cost: number; + // How many times the spell has been bought + times: number; + // Cooldown length of the spell once cast once + cooldown: number; + // Category of the spell + cat: SpellCategory; + // Whether the spell is refundable + refundable: BooleanLike; + // The verb displayed when buying + buyword: Buywords; +}; + +export type SpellbookData = { + owner: string; + points: number; + semi_random_bonus: number; + full_random_bonus: number; + entries: SpellEntry[]; +}; + +export type TabType = { + title: string; +} & Partial<{ + blurb: string; + component: (props?: any) => React.JSX.Element; + locked: boolean; + scrollable: boolean; +}>; + +export enum Buywords { + Learn = 'Learn', + Summon = 'Summon', + Cast = 'Cast', +} + +export enum Tab { + EnscribedName = 0, + TableOfContents = 1, + Offensive = 2, + Defensive = 3, + Mobility = 4, + Assistance = 5, + Challenges = 6, + Rituals = 7, + Loadouts = 8, + Randomize = 9, + Perks = 10, + TableOfContents2 = 11, +}