main commit

This commit is contained in:
2026-06-21 11:27:44 +02:00
commit f45e5197a4
44 changed files with 4511 additions and 0 deletions
View File
+95
View File
@@ -0,0 +1,95 @@
import settingsFile from "@common/shared/settings";
import { SettingDefinition, SettingRealType, SettingType } from "@common/shared/typings/settings";
import { Controller, OnStart } from "@flamework/core";
import { Atom, atom } from "@rbxts/charm";
import Object, { keys } from "@rbxts/object-utils";
import { Events, Functions } from "../network";
/**
* Setting Management Controller
*/
@Controller()
export class SettingsController implements OnStart {
private atomsCache: Map<string, Map<string, Atom<SettingRealType<SettingType>>>> = new Map();
private defCache: Map<string, Map<string, SettingDefinition>> = new Map();
/**
* Fetch player settings from the server.
*/
fetchSettings() {
Functions.settings.GetSettings().then((settings) => {
settings.forEach((section, sectionKey) => {
const sectionAtoms = this.atomsCache.get(sectionKey)!;
section.forEach((val, key) => {
sectionAtoms.get(key)?.(val);
});
});
});
}
/**
* Get all sections names.
* @returns Sections names in the settings file.
*/
getSections() {
return keys(this.atomsCache);
}
/**
* Get All atoms and their definition in the given section.
* @param section The name of the section.
* @returns An array with setting definition and an the atom linked to the type definition.
*/
getSectionWithDef(section: string) {
const ret: [SettingDefinition, Atom<unknown>][] = [];
this.atomsCache.get(section)?.forEach((value, key) => {
ret.push([this.defCache.get(section)!.get(key)!, value]);
});
return ret;
}
/**
* Get an atom with an section name and a setting name.
* @param section Section name.
* @param setting Setting name.
* @returns The atom linked to the setting.
*/
getSetting(section: string, setting: string) {
return this.atomsCache.get(section)?.get(setting);
}
/**
* @inheritdoc
*/
onStart(): void {
this.setupAtoms();
this.fetchSettings();
}
/**
* Send a section of settings to the server to save.
* @param section The section to save.
*/
saveSettings(section: string) {
const atoms = this.atomsCache.get(section);
if (!atoms) return;
const map = new Map<string, boolean | number | string>();
atoms.forEach((value, key) => {
map.set(key, value() as boolean | number | string);
});
Events.settings.SetSettings(section, map);
}
/**
* Setup atoms and definition at service start.
*/
private setupAtoms() {
const sections = Object.keys<Record<string, SettingDefinition[]>>(settingsFile).filter((key) =>
typeIs(settingsFile[key], "table"),
);
sections.forEach((key) => {
const map = new Map<string, Charm.Atom<unknown>>();
const defMap = new Map<string, SettingDefinition>();
this.atomsCache.set(key, map);
this.defCache.set(key, defMap);
settingsFile[key].forEach((def) => {
map.set(def.id, atom(def.default));
defMap.set(def.id, def);
});
});
}
}
+4
View File
@@ -0,0 +1,4 @@
import { GlobalEvents, GlobalFunctions } from "@common/shared/network";
export const Events = GlobalEvents.createClient({});
export const Functions = GlobalFunctions.createClient({});
@@ -0,0 +1,59 @@
import { NotDerivate, useBox } from "@common/shared/utils/ui/vide";
import { Atom } from "@rbxts/charm";
import Vide, { Derivable, source } from "@rbxts/vide";
/**
* Props of a toggle.
*/
interface ToggleProps {
/**
* A charm atom linked to the toggle.
*/
Atom?: Atom<boolean>;
/**
* The position of the toggle in roblox ui.
*/
Position?: Derivable<UDim2>;
/**
* The size of the toggle
*/
Size?: Derivable<UDim2>;
}
/**
* A toggle is a UI component linked to an boolean.
* @param props Props of the toggle.
* @returns A Vide node with the toggle.
*/
export function Toggle(props: ToggleProps = {}) {
const [enable, setEnable] = useBox(props.Atom, false);
const sphereSize = source(0);
return (
<imagebutton
BackgroundColor3={new Color3(1, 1, 1)}
MouseButton1Click={() => setEnable(!enable())}
Position={props?.Position}
Size={props?.Size ?? UDim2.fromOffset(90, 30)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
<uiaspectratioconstraint AspectRatio={1.8} />
<frame
BackgroundColor3={new Color3(0.5, 0.5, 0.5)}
Position={new UDim2(0, 2, 0, 2)}
Size={new UDim2(1, -4, 1, -4)}
Visible={NotDerivate(enable)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
</frame>
<frame
AbsoluteSizeChanged={(value) => sphereSize(value.X)}
BackgroundColor3={new Color3(0, 0, 0)}
Position={() => (enable() ? new UDim2(1, -sphereSize() - 2, 0, 2) : new UDim2(0, 2, 0, 2))}
Size={new UDim2(1, -4, 1, -4)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
<uiaspectratioconstraint AspectRatio={1} />
</frame>
</imagebutton>
);
}
@@ -0,0 +1,127 @@
import { SettingsController } from "@common/client/controllers/settings";
import { SettingDefinition, SettingRealType, SettingType } from "@common/shared/typings/settings";
import { Atom } from "@rbxts/charm";
import Vide, { Case, For, Source, source, Switch } from "@rbxts/vide";
import { Toggle } from "../global/toggle";
/**
* The settings menu as vide components.
* @param settingsController The settings Controller for fetching settings.
* @returns A Vide node with the settings menu.
*/
export function Settings(settingsController: SettingsController) {
const keys = source(settingsController.getSections());
const panel = source(keys()[0]);
return (
<frame
BackgroundColor3={new Color3(0, 0, 0)}
Position={UDim2.fromScale(0.05, 0.05)}
Size={UDim2.fromScale(0.9, 0.9)}
>
<uicorner />
<uistroke Color={new Color3(1, 1, 1)} />
<scrollingframe BackgroundTransparency={1} Size={UDim2.fromScale(0.3, 1)}>
<uigridlayout CellPadding={UDim2.fromOffset(0, 0)} CellSize={new UDim2(1, 0, 0, 40)} />
<For each={keys}>{(group) => <SettingsGroup name={group} panelSource={panel} />}</For>
</scrollingframe>
<frame Position={UDim2.fromScale(0.3, 0)} Size={new UDim2(0, 1, 1, 0)} />
<frame BackgroundTransparency={1} Position={new UDim2(0.3, 10, 0, 0)} Size={new UDim2(0.7, -10, 1, 0)}>
<uilistlayout Padding={new UDim(0, 10)} />
<For each={() => settingsController.getSectionWithDef(panel())}>
{([def, atom]) => <SettingsEntry atom={atom} definition={def} />}
</For>
</frame>
</frame>
);
}
/**
* An entry in the settings menu.
* @param props Properties of the entry.
* @param props.definition Setting definition.
* @param props.atom Charm atom with the value of the setting.
* @returns A setting entry.
*/
export function SettingsEntry<T extends SettingType>(props: {
/**
* The atom with the value of the setting.
*/
atom: Atom<SettingRealType<T>>;
/**
* The definition of the setting.
*/
definition: SettingDefinition<T>;
}) {
return (
<frame AutomaticSize={Enum.AutomaticSize.Y} BackgroundTransparency={1}>
<uilistlayout />
<textlabel
AutomaticSize={Enum.AutomaticSize.Y}
BackgroundTransparency={1}
Text={props.definition.name}
TextColor3={new Color3(1, 1, 1)}
TextSize={30}
TextXAlignment={Enum.TextXAlignment.Left}
/>
<textlabel
AutomaticSize={Enum.AutomaticSize.Y}
BackgroundTransparency={1}
Text={props.definition.description}
TextColor3={new Color3(1, 1, 1)}
TextSize={25}
TextTransparency={0.5}
TextXAlignment={Enum.TextXAlignment.Left}
/>
<Switch condition={() => props.definition.type}>
<Case match={SettingType.Boolean}>
{() => (
<>
<frame Size={UDim2.fromOffset(0, 10)} />
<Toggle Atom={props.atom as unknown as Atom<boolean>} />
</>
)}
</Case>
</Switch>
</frame>
);
}
/**
* A button for each setting section the settings menu.
* @param props Properties of the button.
* @param props.name The name of the entry.
* @param props.panelSource A vide source for the current panel.
* @returns The Settings group button.
*/
export function SettingsGroup(props: {
/**
* The name of the settings group.
*/
name: string;
/**
* A vide source with the name of the current panel.
*/
panelSource: Source<string>;
}) {
return (
<frame BackgroundTransparency={1}>
<textbutton
BackgroundTransparency={1}
FontFace={() =>
props.name === props.panelSource()
? Font.fromEnum(Enum.Font.SourceSansBold)
: Font.fromEnum(Enum.Font.SourceSans)
}
MouseButton1Click={() => {
props.panelSource(props.name);
}}
Size={UDim2.fromScale(1, 1)}
Text={props.name}
TextColor3={new Color3(1, 1, 1)}
TextScaled={true}
/>
</frame>
);
}
@@ -0,0 +1,5 @@
import { Toggle } from "@common/client/ui/components/global/toggle";
import { Hoarcekat } from "@common/shared/utils/ui/hoarcekat";
import { Node } from "@rbxts/vide";
export = Hoarcekat(Toggle as () => Node);