diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index 1158b0849..319f77ba7 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -1,4 +1,11 @@ { + "settings": { + "quality3": { + "low": "Low", + "medium": "Medium", + "high": "High" + } + }, "price": { "free": "Free" }, @@ -159,6 +166,11 @@ "name": "Changelogs", "slogan": "BlitzKit patch notes", "action": "Read up" + }, + "settings": { + "name": "Settings", + "slogan": "", + "action": "" } }, diff --git a/packages/website-ue/public/media/tools/banners/settings.webp b/packages/website-ue/public/media/tools/banners/settings.webp new file mode 100644 index 000000000..ff4f75757 Binary files /dev/null and b/packages/website-ue/public/media/tools/banners/settings.webp differ diff --git a/packages/website-ue/src/components/Carousel.tsx b/packages/website-ue/src/components/Carousel.tsx new file mode 100644 index 000000000..08271cb2e --- /dev/null +++ b/packages/website-ue/src/components/Carousel.tsx @@ -0,0 +1,37 @@ +import { CaretLeftIcon, CaretRightIcon } from "@radix-ui/react-icons"; +import { type ReactNode } from "react"; + +interface Props { + value: number; + onChange: (value: number) => void; + options: ReactNode[]; +} + +export function Carousel({ value, onChange, options }: Props) { + const isMin = value === 0; + const isMax = value === options.length - 1; + + return ( +
+
{ + if (!isMin) onChange(value - 1); + }} + > + +
+ +
{options[value]}
+ +
{ + if (!isMax) onChange(value + 1); + }} + > + +
+
+ ); +} diff --git a/packages/website-ue/src/components/Code/index.tsx b/packages/website-ue/src/components/Code/index.tsx index 8811704e3..ef200ef39 100644 --- a/packages/website-ue/src/components/Code/index.tsx +++ b/packages/website-ue/src/components/Code/index.tsx @@ -1,14 +1,15 @@ -import type { ComponentProps } from "react"; import { classNames } from "../../ui/classNames"; +import { Text, type TextProps } from "../Text"; import styles from "./index.module.css"; -export interface CodeProps extends ComponentProps<"code"> { +export interface CodeProps extends TextProps { + lowContrast?: boolean; variant?: "ghost" | "solid"; } export function Code({ className, variant = "ghost", ...props }: CodeProps) { return ( - ; +} + +function Inner() { + const parent = useRef(null!); + const settings = Settings.use(); + + return ( +
+
+ {stats.map((stat) => ( + + ))} +
+ +
+ {settingsOrder[0].settings.map((setting) => ( + + {setting} = {settings[setting]} + + ))} +
+
+ ); +} diff --git a/packages/website-ue/src/components/TankModelPart.tsx b/packages/website-ue/src/components/TankModelPart.tsx index 8feff02bb..e8a3cee36 100644 --- a/packages/website-ue/src/components/TankModelPart.tsx +++ b/packages/website-ue/src/components/TankModelPart.tsx @@ -1,6 +1,8 @@ import { useGLTF } from "@react-three/drei"; import { useEffect } from "react"; import { Mesh, MeshStandardMaterial } from "three"; +import { SettingQuality3 } from "../config/settings"; +import { useSetting } from "../hooks/useSetting"; interface Props { url: string; @@ -8,6 +10,9 @@ interface Props { } export function TankModelPart({ url, map }: Props) { + const tankQuality = useSetting("tank_quality"); + const lightingQuality = useSetting("lighting_quality"); + const gltf = useGLTF(url); const [root] = gltf.scene.children; @@ -17,16 +22,32 @@ export function TankModelPart({ url, map }: Props) { // TODO: remove this hack and fix the gltf material.vertexColors = false; material.needsUpdate = true; + + if (tankQuality <= SettingQuality3.Medium) { + material.normalMap = null; + } + + if (tankQuality <= SettingQuality3.Low) { + material.metalness = 0; + material.roughness = 1; + + material.metalnessMap = null; + material.roughnessMap = null; + } + + if (tankQuality <= SettingQuality3.Medium) { + material.aoMap = null; + } } } for (const node of Object.values(gltf.nodes)) { if (node instanceof Mesh) { - node.castShadow = true; - node.receiveShadow = true; + node.castShadow = lightingQuality >= SettingQuality3.Medium; + node.receiveShadow = lightingQuality >= SettingQuality3.High; } } - }, [url]); + }, [url, tankQuality]); useEffect(() => { for (const child of root.children) { diff --git a/packages/website-ue/src/components/TankopediaSandbox/index.module.css b/packages/website-ue/src/components/TankopediaSandbox/index.module.css index b4372736a..e545b2a26 100644 --- a/packages/website-ue/src/components/TankopediaSandbox/index.module.css +++ b/packages/website-ue/src/components/TankopediaSandbox/index.module.css @@ -10,7 +10,7 @@ height: 100%; .canvas { - background-color: black; + background-color: black; } } } diff --git a/packages/website-ue/src/components/TankopediaSandbox/index.tsx b/packages/website-ue/src/components/TankopediaSandbox/index.tsx index 695793afb..018cfe79f 100644 --- a/packages/website-ue/src/components/TankopediaSandbox/index.tsx +++ b/packages/website-ue/src/components/TankopediaSandbox/index.tsx @@ -1,6 +1,7 @@ import { type StageParameters } from "@protos/blitz_static_tank_upgrade_single_stage"; import { Canvas } from "@react-three/fiber"; import { fov } from "../../config/camera"; +import { RendererStats } from "../RendererStats"; import { TankModelChassis } from "../TankModelChassis"; import { TankModelGun } from "../TankModelGun"; import { TankModelHull } from "../TankModelHull"; @@ -17,7 +18,9 @@ interface TankopediaSandboxProps { export function TankopediaSandbox({ parameters }: TankopediaSandboxProps) { return (
-
+
+ + ; + +export type SettingName = keyof typeof settings; + +export interface SettingsGroup { + name: string; + settings: SettingName[]; +} + +export const settingsOrder: SettingsGroup[] = [ + { + name: "general", + settings: ["tank_quality", "lighting_quality"], + }, + { + name: "developer", + settings: ["tank_slug_and_id", "render_statistics"], + }, +]; + +export type SettingTypes = Setting["default"]; diff --git a/packages/website-ue/src/config/tools.ts b/packages/website-ue/src/config/tools.ts index b63b6bb11..99376e7ac 100644 --- a/packages/website-ue/src/config/tools.ts +++ b/packages/website-ue/src/config/tools.ts @@ -91,4 +91,11 @@ export const tools: Record = { accent: "gray", banner_background_position: "bottom", }, + + settings: { + id: "settings", + + accent: "gray", + banner_background_position: "center", + }, }; diff --git a/packages/website-ue/src/hooks/useSetting.ts b/packages/website-ue/src/hooks/useSetting.ts new file mode 100644 index 000000000..46a256d00 --- /dev/null +++ b/packages/website-ue/src/hooks/useSetting.ts @@ -0,0 +1,24 @@ +import { + settings, + type SettingName, + type SettingType, + type SettingTypes, +} from "../config/settings"; +import { Settings } from "../stores/settings"; + +export function useSetting(name: SettingName): Type { + const value = Settings.use((state) => state[name]); + + return value as Type; +} + +export function useSettingDeferred( + name: SettingName, +): Type { + const value = Settings.useDeferred( + (state) => state[name], + settings[name].default, + ); + + return value as Type; +} diff --git a/packages/website-ue/src/pages/[...locale]/settings/_index.tsx b/packages/website-ue/src/pages/[...locale]/settings/_index.tsx new file mode 100644 index 000000000..de63844b3 --- /dev/null +++ b/packages/website-ue/src/pages/[...locale]/settings/_index.tsx @@ -0,0 +1,83 @@ +import { Carousel } from "../../../components/Carousel"; +import { Heading } from "../../../components/Heading"; +import { Text } from "../../../components/Text"; +import { + SettingBoolean, + SettingQuality3, + settings, + settingsOrder, + SettingType, + type Setting, + type SettingName, + type SettingsGroup, +} from "../../../config/settings"; +import { withLocale } from "../../../hocs/withLocale"; +import { useSetting } from "../../../hooks/useSetting"; +import { useStrings } from "../../../hooks/useStrings"; +import { Settings } from "../../../stores/settings"; + +export const Page = withLocale(() => { + return settingsOrder.map((group) => ); +}); + +interface GroupProps { + group: SettingsGroup; +} + +function Group({ group }: GroupProps) { + return ( +
+ {group.name} + +
+ {group.settings.map((setting) => ( + + ))} +
+
+ ); +} + +interface SettingProps { + name: SettingName; +} + +function Setting({ name }: SettingProps) { + const setting = settings[name]; + + return ( +
+ {name} + + {setting.type === SettingType.Boolean && } + {setting.type === SettingType.Quality3 && } +
+ ); +} + +function TypeBoolean({ name }: SettingProps) { + const value = useSetting(name); + + return null; +} + +function TypeQuality3({ name }: SettingProps) { + const value = useSetting(name); + const strings = useStrings(); + + return ( + { + Settings.mutate((draft) => { + draft[name] = value; + }); + }} + options={[ + strings.settings.quality3.low, + strings.settings.quality3.medium, + strings.settings.quality3.high, + ]} + /> + ); +} diff --git a/packages/website-ue/src/pages/[...locale]/settings/index.astro b/packages/website-ue/src/pages/[...locale]/settings/index.astro new file mode 100644 index 000000000..29272eb65 --- /dev/null +++ b/packages/website-ue/src/pages/[...locale]/settings/index.astro @@ -0,0 +1,19 @@ +--- +import { Section } from "../../../components/Section"; +import ToolBanner from "../../../components/ToolBanner.astro"; +import { tools } from "../../../config/tools"; +import BaseLayout from "../../../layouts/BaseLayout.astro"; +import { Page } from "./_index"; + +export { getStaticPaths } from "../index.astro"; + +const { locale } = Astro.props; +--- + + + + +
+ +
+
diff --git a/packages/website-ue/src/stores/settings.ts b/packages/website-ue/src/stores/settings.ts new file mode 100644 index 000000000..8efa9ecaf --- /dev/null +++ b/packages/website-ue/src/stores/settings.ts @@ -0,0 +1,17 @@ +import { Varuna } from "varuna"; +import { + settings, + type SettingName, + type SettingTypes, +} from "../config/settings"; + +type Settings = Record; + +const defaults: Partial = {}; + +for (const key in settings) { + const setting = settings[key as SettingName]; + defaults[key as SettingName] = setting.default; +} + +export const Settings = new Varuna(defaults as Settings, "settings");