graphics and settings

This commit is contained in:
tresabhi committed 2026-06-16 19:44:58 -05:00
1 parent 7e2fba35d0
commit b572ac5ea8
15 files changed
+367 -8

No files matched your search

+12
View File
@@ -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": ""
}
},
Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

@@ -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 (
<div>
<div
data-disabled={isMin}
onClick={() => {
if (!isMin) onChange(value - 1);
}}
>
<CaretLeftIcon />
</div>
<div>{options[value]}</div>
<div
data-disabled={isMax}
onClick={() => {
if (!isMax) onChange(value + 1);
}}
>
<CaretRightIcon />
</div>
</div>
);
}
@@ -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 (
<code
<Text
data-variant={variant}
className={classNames(styles.code, className)}
{...props}
@@ -0,0 +1,29 @@
.stats {
padding: var(--space-3);
gap: var(--space-3);
z-index: 1;
display: flex;
flex-direction: column;
position: absolute;
top: 0;
left: 0;
.graphs {
display: flex;
gap: var(--space-2);
.graph {
position: static !important;
}
}
.settings {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
}
@@ -0,0 +1,40 @@
import { Stats } from "@react-three/drei";
import { useRef } from "react";
import { settingsOrder } from "../../config/settings";
import { useSettingDeferred } from "../../hooks/useSetting";
import { Settings } from "../../stores/settings";
import { Code } from "../Code";
import styles from "./index.module.css";
const stats = [0, 1, 2];
export function RendererStats() {
const renderStatistics = useSettingDeferred("render_statistics");
if (!renderStatistics) return null;
return <Inner />;
}
function Inner() {
const parent = useRef<HTMLDivElement>(null!);
const settings = Settings.use();
return (
<div className={styles.stats}>
<div ref={parent} className={styles.graphs}>
{stats.map((stat) => (
<Stats parent={parent} showPanel={stat} className={styles.graph} />
))}
</div>
<div className={styles.settings}>
{settingsOrder[0].settings.map((setting) => (
<Code lowContrast size="minor">
{setting} = {settings[setting]}
</Code>
))}
</div>
</div>
);
}
@@ -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<SettingQuality3>("tank_quality");
const lightingQuality = useSetting<SettingQuality3>("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) {
@@ -10,7 +10,7 @@
height: 100%;
.canvas {
background-color: black;
background-color: black;
}
}
}
@@ -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 (
<div className={styles.wrapper}>
<div className={styles.sandbox}>
<div className={styles.sandbox} id="canvas-attach">
<RendererStats />
<Canvas
camera={{ fov }}
shadows
@@ -0,0 +1,66 @@
export enum SettingType {
Boolean,
Quality3,
}
interface SettingTypeBoolean {
type: SettingType.Boolean;
default: SettingBoolean;
}
interface SettingTypeQuality3 {
type: SettingType.Quality3;
default: SettingQuality3;
}
export enum SettingQuality3 {
Low,
Medium,
High,
}
export enum SettingBoolean {
False,
True,
}
export type Setting = SettingTypeBoolean | SettingTypeQuality3;
export const settings = {
tank_quality: {
type: SettingType.Quality3,
default: SettingQuality3.High,
},
lighting_quality: {
type: SettingType.Quality3,
default: SettingQuality3.High,
},
tank_slug_and_id: {
type: SettingType.Boolean,
default: SettingBoolean.False,
},
render_statistics: {
type: SettingType.Boolean,
default: SettingBoolean.False,
},
} as const satisfies Record<string, Setting>;
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"];
+7
View File
@@ -91,4 +91,11 @@ export const tools: Record<string, Tool> = {
accent: "gray",
banner_background_position: "bottom",
},
settings: {
id: "settings",
accent: "gray",
banner_background_position: "center",
},
};
@@ -0,0 +1,24 @@
import {
settings,
type SettingName,
type SettingType,
type SettingTypes,
} from "../config/settings";
import { Settings } from "../stores/settings";
export function useSetting<Type extends SettingTypes>(name: SettingName): Type {
const value = Settings.use((state) => state[name]);
return value as Type;
}
export function useSettingDeferred<Type extends SettingTypes>(
name: SettingName,
): Type {
const value = Settings.useDeferred(
(state) => state[name],
settings[name].default,
);
return value as Type;
}
@@ -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) => <Group group={group} />);
});
interface GroupProps {
group: SettingsGroup;
}
function Group({ group }: GroupProps) {
return (
<div>
<Heading>{group.name}</Heading>
<div>
{group.settings.map((setting) => (
<Setting name={setting} />
))}
</div>
</div>
);
}
interface SettingProps {
name: SettingName;
}
function Setting({ name }: SettingProps) {
const setting = settings[name];
return (
<div>
<Text>{name}</Text>
{setting.type === SettingType.Boolean && <TypeBoolean name={name} />}
{setting.type === SettingType.Quality3 && <TypeQuality3 name={name} />}
</div>
);
}
function TypeBoolean({ name }: SettingProps) {
const value = useSetting<SettingBoolean>(name);
return null;
}
function TypeQuality3({ name }: SettingProps) {
const value = useSetting<SettingQuality3>(name);
const strings = useStrings();
return (
<Carousel
value={value}
onChange={(value) => {
Settings.mutate((draft) => {
draft[name] = value;
});
}}
options={[
strings.settings.quality3.low,
strings.settings.quality3.medium,
strings.settings.quality3.high,
]}
/>
);
}
@@ -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;
---
<BaseLayout accent="amber" {locale}>
<ToolBanner {locale} tool={tools.settings} slot="pre-app" />
<Section>
<Page {locale} client:load />
</Section>
</BaseLayout>
@@ -0,0 +1,17 @@
import { Varuna } from "varuna";
import {
settings,
type SettingName,
type SettingTypes,
} from "../config/settings";
type Settings = Record<SettingName, SettingTypes>;
const defaults: Partial<Settings> = {};
for (const key in settings) {
const setting = settings[key as SettingName];
defaults[key as SettingName] = setting.default;
}
export const Settings = new Varuna<Settings>(defaults as Settings, "settings");