feat: upgrade rest of them

This commit is contained in:
TrèsAbhi committed 2025-09-02 16:24:13 -05:00
1 parent fe4afdcbf0
commit 30e87743fc
63 files changed
+824 -1123

No files matched your search

+5 -1
View File
@@ -94,8 +94,12 @@ export class Varuna<Type, Arguments = void> {
use<Slice = Type>( use<Slice = Type>(
slicer: (state: Type) => Slice = (state) => state as unknown as Slice slicer: (state: Type) => Slice = (state) => state as unknown as Slice
) { ) {
return this.useDeferred(slicer, slicer(this.state));
}
useDeferred<Slice = Type>(slicer: (state: Type) => Slice, initial: Slice) {
const instance = this; const instance = this;
const [slice, setSlice] = useState(slicer(this.state)); const [slice, setSlice] = useState(initial);
useEffect(() => instance.on(slicer, setSlice), []); useEffect(() => instance.on(slicer, setSlice), []);
@@ -20,7 +20,6 @@ import { ArmorType } from "./SpacedArmorScene";
export function ArmorPlateDisplay() { export function ArmorPlateDisplay() {
const highlightArmor = Tankopedia.use((state) => state.highlightArmor); const highlightArmor = Tankopedia.use((state) => state.highlightArmor);
const developerMode = App.useDeferred((state) => state.developerMode, false); const developerMode = App.useDeferred((state) => state.developerMode, false);
const duelStore = Duel.useStore();
const input = useRef<HTMLInputElement>(null); const input = useRef<HTMLInputElement>(null);
if (highlightArmor === undefined) return null; if (highlightArmor === undefined) return null;
@@ -93,7 +92,7 @@ export function ArmorPlateDisplay() {
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
const { protagonist } = duelStore.getState(); const { protagonist } = Duel.state;
const tank = draft.model; const tank = draft.model;
const turret = tank.turrets[protagonist.turret.id]; const turret = tank.turrets[protagonist.turret.id];
const gun = turret.guns[protagonist.gun.id]; const gun = turret.guns[protagonist.gun.id];
@@ -142,7 +141,7 @@ export function ArmorPlateDisplay() {
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
const initialState = Tankopedia.initial; const initialState = Tankopedia.initial;
const { protagonist } = duelStore.getState(); const { protagonist } = Duel.state;
const tank = draft.model; const tank = draft.model;
const initialTank = initialState.model; const initialTank = initialState.model;
const turret = tank.turrets[protagonist.turret.id]; const turret = tank.turrets[protagonist.turret.id];
@@ -41,8 +41,6 @@ export function PrimaryArmorSceneComponent({
node, node,
thickness, thickness,
}: PrimaryArmorSceneComponentProps) { }: PrimaryArmorSceneComponentProps) {
const tankopediaPersistentStore = TankopediaPersistent.useStore();
const duelStore = Duel.useStore();
const material = new ShaderMaterial({ const material = new ShaderMaterial({
fragmentShader, fragmentShader,
vertexShader, vertexShader,
@@ -80,9 +78,9 @@ export function PrimaryArmorSceneComponent({
useEffect(() => { useEffect(() => {
function handleShellChange() { function handleShellChange() {
const duel = duelStore.getState();
const tankopediaEphemeral = Tankopedia.state; const tankopediaEphemeral = Tankopedia.state;
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
material.uniforms.caliber.value = shell.caliber; material.uniforms.caliber.value = shell.caliber;
material.uniforms.ricochet.value = degToRad( material.uniforms.ricochet.value = degToRad(
@@ -96,8 +94,14 @@ export function PrimaryArmorSceneComponent({
material.uniforms.damage.value = shell.armor_damage; material.uniforms.damage.value = shell.armor_damage;
material.uniforms.explosionRadius.value = shell.explosion_radius; material.uniforms.explosionRadius.value = shell.explosion_radius;
handleProtagonistEquipmentChange(duel.protagonist.equipmentMatrix, true); handleProtagonistEquipmentChange(
handleAntagonistEquipmentChange(duel.antagonist.equipmentMatrix, true); Duel.state.protagonist.equipmentMatrix,
true
);
handleAntagonistEquipmentChange(
Duel.state.antagonist.equipmentMatrix,
true
);
invalidate(); invalidate();
} }
@@ -118,10 +122,9 @@ export function PrimaryArmorSceneComponent({
equipment: EquipmentMatrix, equipment: EquipmentMatrix,
noInvalidate = false noInvalidate = false
) { ) {
const duel = duelStore.getState();
const hasEnhancedArmor = hasEquipment( const hasEnhancedArmor = hasEquipment(
110, 110,
duel.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
equipment equipment
); );
material.uniforms.thickness.value = hasEnhancedArmor material.uniforms.thickness.value = hasEnhancedArmor
@@ -134,13 +137,13 @@ export function PrimaryArmorSceneComponent({
equipment: EquipmentMatrix, equipment: EquipmentMatrix,
noInvalidate = false noInvalidate = false
) { ) {
const duel = duelStore.getState();
const tankopediaEphemeral = Tankopedia.state; const tankopediaEphemeral = Tankopedia.state;
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
const penetration = shell.penetration.near; const penetration = shell.penetration.near;
const hasCalibratedShells = hasEquipment( const hasCalibratedShells = hasEquipment(
103, 103,
duel.antagonist.tank.equipment_preset, Duel.state.antagonist.tank.equipment_preset,
equipment equipment
); );
@@ -151,40 +154,33 @@ export function PrimaryArmorSceneComponent({
if (!noInvalidate) invalidate(); if (!noInvalidate) invalidate();
} }
const initialState = tankopediaPersistentStore.getState();
handleShellChange(); handleShellChange();
handleGreenPenetrationChange(initialState.greenPenetration); handleGreenPenetrationChange(TankopediaPersistent.state.greenPenetration);
handleAdvancedHighlightingChange(initialState.advancedHighlighting); handleAdvancedHighlightingChange(
handleOpaqueChange(initialState.opaque); TankopediaPersistent.state.advancedHighlighting
handleWireframeChange(initialState.wireframe);
handleProtagonistEquipmentChange(
duelStore.getState().protagonist.equipmentMatrix
);
handleAntagonistEquipmentChange(
duelStore.getState().antagonist.equipmentMatrix
); );
handleOpaqueChange(TankopediaPersistent.state.opaque);
handleWireframeChange(TankopediaPersistent.state.wireframe);
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
const unsubscribes = [ const unsubscribes = [
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange), Duel.on((state) => state.antagonist.shell, handleShellChange),
Tankopedia.on((state) => state.customShell, handleShellChange), Tankopedia.on((state) => state.customShell, handleShellChange),
tankopediaPersistentStore.subscribe( TankopediaPersistent.on(
(state) => state.greenPenetration, (state) => state.greenPenetration,
handleGreenPenetrationChange handleGreenPenetrationChange
), ),
tankopediaPersistentStore.subscribe( TankopediaPersistent.on(
(state) => state.advancedHighlighting, (state) => state.advancedHighlighting,
handleAdvancedHighlightingChange handleAdvancedHighlightingChange
), ),
tankopediaPersistentStore.subscribe( TankopediaPersistent.on((state) => state.opaque, handleOpaqueChange),
(state) => state.opaque, Duel.on(
handleOpaqueChange
),
duelStore.subscribe(
(state) => state.protagonist.equipmentMatrix, (state) => state.protagonist.equipmentMatrix,
(equipment) => handleProtagonistEquipmentChange(equipment) (equipment) => handleProtagonistEquipmentChange(equipment)
), ),
duelStore.subscribe( Duel.on(
(state) => state.antagonist.equipmentMatrix, (state) => state.antagonist.equipmentMatrix,
(equipment) => handleAntagonistEquipmentChange(equipment) (equipment) => handleAntagonistEquipmentChange(equipment)
), ),
@@ -29,7 +29,6 @@ export function SpacedArmorSubExternal({
variant, variant,
clip, clip,
}: SpacedArmorSubExternalProps) { }: SpacedArmorSubExternalProps) {
const duelStore = Duel.useStore();
const material = new ShaderMaterial({ const material = new ShaderMaterial({
fragmentShader, fragmentShader,
vertexShader, vertexShader,
@@ -48,16 +47,15 @@ export function SpacedArmorSubExternal({
useEffect(() => { useEffect(() => {
function handleShellChange() { function handleShellChange() {
const duel = duelStore.getState();
const tankopediaEphemeral = Tankopedia.state; const tankopediaEphemeral = Tankopedia.state;
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
material.uniforms.penetration.value = shell.penetration.near; material.uniforms.penetration.value = shell.penetration.near;
} }
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) { function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
const duel = duelStore.getState();
const hasEnhancedArmor = hasEquipment( const hasEnhancedArmor = hasEquipment(
110, 110,
duel.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
equipment equipment
); );
material.uniforms.thickness.value = hasEnhancedArmor material.uniforms.thickness.value = hasEnhancedArmor
@@ -65,13 +63,13 @@ export function SpacedArmorSubExternal({
: thickness; : thickness;
} }
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const duel = duelStore.getState();
const tankopediaEphemeral = Tankopedia.state; const tankopediaEphemeral = Tankopedia.state;
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
const penetration = shell.penetration.near; const penetration = shell.penetration.near;
const hasCalibratedShells = hasEquipment( const hasCalibratedShells = hasEquipment(
103, 103,
duel.antagonist.tank.equipment_preset, Duel.state.antagonist.tank.equipment_preset,
equipment equipment
); );
@@ -81,21 +79,17 @@ export function SpacedArmorSubExternal({
} }
handleShellChange(); handleShellChange();
handleProtagonistEquipmentChange( handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
duelStore.getState().protagonist.equipmentMatrix handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
);
handleAntagonistEquipmentChange(
duelStore.getState().antagonist.equipmentMatrix
);
const unsubscribes = [ const unsubscribes = [
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange), Duel.on((state) => state.antagonist.shell, handleShellChange),
Tankopedia.on((state) => state.customShell, handleShellChange), Tankopedia.on((state) => state.customShell, handleShellChange),
duelStore.subscribe( Duel.on(
(state) => state.protagonist.equipmentMatrix, (state) => state.protagonist.equipmentMatrix,
handleProtagonistEquipmentChange handleProtagonistEquipmentChange
), ),
duelStore.subscribe( Duel.on(
(state) => state.antagonist.equipmentMatrix, (state) => state.antagonist.equipmentMatrix,
handleAntagonistEquipmentChange handleAntagonistEquipmentChange
), ),
@@ -30,7 +30,6 @@ export function SpacedArmorSubSpaced({
node, node,
thickness, thickness,
}: SpacedArmorSubSpacedProps) { }: SpacedArmorSubSpacedProps) {
const duelStore = Duel.useStore();
const material = new ShaderMaterial({ const material = new ShaderMaterial({
fragmentShader, fragmentShader,
vertexShader, vertexShader,
@@ -50,9 +49,9 @@ export function SpacedArmorSubSpaced({
useEffect(() => { useEffect(() => {
function handleShellChange() { function handleShellChange() {
const duel = duelStore.getState();
const tankopediaEphemeral = Tankopedia.state; const tankopediaEphemeral = Tankopedia.state;
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
material.uniforms.penetration.value = shell.penetration.near; material.uniforms.penetration.value = shell.penetration.near;
material.uniforms.caliber.value = shell.caliber; material.uniforms.caliber.value = shell.caliber;
@@ -64,10 +63,9 @@ export function SpacedArmorSubSpaced({
); );
} }
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) { function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
const duel = duelStore.getState();
const hasEnhancedArmor = hasEquipment( const hasEnhancedArmor = hasEquipment(
110, 110,
duel.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
equipment equipment
); );
material.uniforms.thickness.value = hasEnhancedArmor material.uniforms.thickness.value = hasEnhancedArmor
@@ -75,13 +73,13 @@ export function SpacedArmorSubSpaced({
: thickness; : thickness;
} }
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const duel = duelStore.getState();
const tankopediaEphemeral = Tankopedia.state; const tankopediaEphemeral = Tankopedia.state;
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
const penetration = shell.penetration.near; const penetration = shell.penetration.near;
const hasCalibratedShells = hasEquipment( const hasCalibratedShells = hasEquipment(
103, 103,
duel.antagonist.tank.equipment_preset, Duel.state.antagonist.tank.equipment_preset,
equipment equipment
); );
@@ -91,21 +89,17 @@ export function SpacedArmorSubSpaced({
} }
handleShellChange(); handleShellChange();
handleProtagonistEquipmentChange( handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
duelStore.getState().protagonist.equipmentMatrix handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
);
handleAntagonistEquipmentChange(
duelStore.getState().antagonist.equipmentMatrix
);
const unsubscribes = [ const unsubscribes = [
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange), Duel.on((state) => state.antagonist.shell, handleShellChange),
Tankopedia.on((state) => state.customShell, handleShellChange), Tankopedia.on((state) => state.customShell, handleShellChange),
duelStore.subscribe( Duel.on(
(state) => state.protagonist.equipmentMatrix, (state) => state.protagonist.equipmentMatrix,
handleProtagonistEquipmentChange handleProtagonistEquipmentChange
), ),
duelStore.subscribe( Duel.on(
(state) => state.antagonist.equipmentMatrix, (state) => state.antagonist.equipmentMatrix,
handleAntagonistEquipmentChange handleAntagonistEquipmentChange
), ),
@@ -76,7 +76,6 @@ export function SpacedArmorSceneComponent({
clip, clip,
...props ...props
}: SpacedArmorSceneComponentProps) { }: SpacedArmorSceneComponentProps) {
const duelStore = Duel.useStore();
const camera = useThree((state) => state.camera); const camera = useThree((state) => state.camera);
const shoot = useCallback( const shoot = useCallback(
@@ -86,9 +85,8 @@ export function SpacedArmorSceneComponent({
allowRicochet: boolean, allowRicochet: boolean,
remainingPenetrationInput?: number remainingPenetrationInput?: number
) => { ) => {
const { antagonist, protagonist } = duelStore.getState();
const { customShell } = Tankopedia.state; const { customShell } = Tankopedia.state;
const shell = customShell ?? antagonist.shell; const shell = customShell ?? Duel.state.antagonist.shell;
const cameraNormal = camera.position.clone().sub(point).normalize(); const cameraNormal = camera.position.clone().sub(point).normalize();
const shot: Shot = { const shot: Shot = {
splashRadius: splashRadius:
@@ -104,13 +102,13 @@ export function SpacedArmorSceneComponent({
const hasCalibratedShells = hasEquipment( const hasCalibratedShells = hasEquipment(
103, 103,
antagonist.tank.equipment_preset, Duel.state.antagonist.tank.equipment_preset,
antagonist.equipmentMatrix Duel.state.antagonist.equipmentMatrix
); );
const hasEnhancedArmor = hasEquipment( const hasEnhancedArmor = hasEquipment(
110, 110,
protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix
); );
const penetration = const penetration =
shell.penetration.near * shell.penetration.near *
@@ -46,7 +46,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin); const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin); const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
const canvas = useThree((state) => state.gl.domElement); const canvas = useThree((state) => state.gl.domElement);
const duelStore = Duel.useStore();
const maskOrigin = const maskOrigin =
gunModelDefinition.mask === undefined gunModelDefinition.mask === undefined
? undefined ? undefined
@@ -165,16 +165,15 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
window.addEventListener("pointerup", handlePointerUp); window.addEventListener("pointerup", handlePointerUp);
} }
function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix
); );
const boundingRect = canvas.getBoundingClientRect(); const boundingRect = canvas.getBoundingClientRect();
@@ -258,16 +257,15 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
window.addEventListener("pointerup", handlePointerUp); window.addEventListener("pointerup", handlePointerUp);
} }
function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix
); );
const boundingRect = canvas.getBoundingClientRect(); const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position); delta.set(event.clientX, event.clientY).sub(position);
@@ -339,16 +337,15 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
window.addEventListener("pointerup", handlePointerUp); window.addEventListener("pointerup", handlePointerUp);
} }
function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix
); );
const boundingRect = canvas.getBoundingClientRect(); const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position); delta.set(event.clientX, event.clientY).sub(position);
@@ -1,19 +1,19 @@
import { assertSecret, idToRegion } from '@blitzkit/core'; import { assertSecret, idToRegion } from "@blitzkit/core";
import { Button, Flex, Heading, Text } from '@radix-ui/themes'; import { Button, Flex, Heading, Text } from "@radix-ui/themes";
import { useEffect, useState } from 'react'; import { useEffect, useState } from "react";
import { import {
LocaleProvider, LocaleProvider,
useLocale, useLocale,
type LocaleAcceptorProps, type LocaleAcceptorProps,
} from '../../hooks/useLocale'; } from "../../hooks/useLocale";
import { App } from '../../stores/app'; import { App } from "../../stores/app";
import { CURRENT_POLICIES_AGREEMENT_INDEX } from '../../stores/app/constants'; import { CURRENT_POLICIES_AGREEMENT_INDEX } from "../../stores/app/constants";
import { LinkI18n } from '../LinkI18n'; import { LinkI18n } from "../LinkI18n";
import './index.css'; import "./index.css";
type Extension = type Extension =
| { | {
status: 'ok'; status: "ok";
data: { data: {
access_token: string; access_token: string;
account_id: number; account_id: number;
@@ -21,15 +21,13 @@ type Extension =
}; };
} }
| { | {
status: 'error'; status: "error";
}; };
export function Checks({ locale }: LocaleAcceptorProps) { export function Checks({ locale }: LocaleAcceptorProps) {
return ( return (
<LocaleProvider locale={locale}> <LocaleProvider locale={locale}>
<App.Provider> <Content />
<Content />
</App.Provider>
</LocaleProvider> </LocaleProvider>
); );
} }
@@ -40,19 +38,17 @@ export function Checks({ locale }: LocaleAcceptorProps) {
function Content() { function Content() {
const { locale } = useLocale(); const { locale } = useLocale();
const logins = App.use((state) => state.logins); const logins = App.use((state) => state.logins);
const mutateApp = App.useMutation();
const appStore = App.useStore();
const policiesAgreementIndex = App.use( const policiesAgreementIndex = App.use(
(state) => state.policiesAgreementIndex, (state) => state.policiesAgreementIndex
); );
const isLegal = window.location.pathname.startsWith('/legal'); const isLegal = window.location.pathname.startsWith("/legal");
const [showPoliciesAgreement, setShowPoliciesAgreement] = useState(false); const [showPoliciesAgreement, setShowPoliciesAgreement] = useState(false);
useEffect(() => { useEffect(() => {
function unsubscribe() { function unsubscribe() {
window.removeEventListener('scroll', show); window.removeEventListener("scroll", show);
window.removeEventListener('pointermove', show); window.removeEventListener("pointermove", show);
window.removeEventListener('pointerdown', show); window.removeEventListener("pointerdown", show);
} }
function show() { function show() {
@@ -64,9 +60,9 @@ function Content() {
policiesAgreementIndex !== CURRENT_POLICIES_AGREEMENT_INDEX && policiesAgreementIndex !== CURRENT_POLICIES_AGREEMENT_INDEX &&
!isLegal !isLegal
) { ) {
window.addEventListener('scroll', show); window.addEventListener("scroll", show);
window.addEventListener('pointermove', show); window.addEventListener("pointermove", show);
window.addEventListener('pointerdown', show); window.addEventListener("pointerdown", show);
} }
return unsubscribe; return unsubscribe;
@@ -78,25 +74,25 @@ function Content() {
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24; const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
if (expiresInDays < 0) { if (expiresInDays < 0) {
mutateApp((draft) => { App.mutate((draft) => {
draft.logins.wargaming = undefined; draft.logins.wargaming = undefined;
}); });
} else if (expiresInDays < 7) { } else if (expiresInDays < 7) {
const { wargaming } = appStore.getState().logins; const { wargaming } = App.state.logins;
if (!wargaming) return; if (!wargaming) return;
fetch( fetch(
`https://api.worldoftanks.${idToRegion( `https://api.worldoftanks.${idToRegion(
wargaming.id, wargaming.id
)}/wot/auth/prolongate/?application_id=${assertSecret( )}/wot/auth/prolongate/?application_id=${assertSecret(
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID, import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
)}&access_token=${wargaming.token}`, )}&access_token=${wargaming.token}`
) )
.then((response) => response.json() as Promise<Extension>) .then((response) => response.json() as Promise<Extension>)
.then((json) => { .then((json) => {
mutateApp((draft) => { App.mutate((draft) => {
if (json.status === 'error') { if (json.status === "error") {
draft.logins.wargaming = undefined; draft.logins.wargaming = undefined;
} else { } else {
draft.logins.wargaming = { draft.logins.wargaming = {
@@ -126,11 +122,11 @@ function Content() {
<Text> <Text>
This website utilizes cookies to perform analytics and personalize This website utilizes cookies to perform analytics and personalize
your experience. You can learn more through{' '} your experience. You can learn more through{" "}
<LinkI18n locale={locale} href="/docs/legal/privacy-policy"> <LinkI18n locale={locale} href="/docs/legal/privacy-policy">
our privacy policy our privacy policy
</LinkI18n> </LinkI18n>
. By using BlitzKit, you also agree to our{' '} . By using BlitzKit, you also agree to our{" "}
<LinkI18n locale={locale} href="/docs/legal/terms-of-service"> <LinkI18n locale={locale} href="/docs/legal/terms-of-service">
terms of service terms of service
</LinkI18n> </LinkI18n>
@@ -141,8 +137,8 @@ function Content() {
<Button <Button
mt="2" mt="2"
onClick={() => { onClick={() => {
appStore.setState({ App.mutate((draft) => {
policiesAgreementIndex: CURRENT_POLICIES_AGREEMENT_INDEX, draft.policiesAgreementIndex = CURRENT_POLICIES_AGREEMENT_INDEX;
}); });
setShowPoliciesAgreement(false); setShowPoliciesAgreement(false);
}} }}
@@ -1,16 +1,14 @@
import { Switch } from '@radix-ui/themes'; import { Switch } from "@radix-ui/themes";
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index'; import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
import { EmbedState } from '../../stores/embedState'; import { EmbedState } from "../../stores/embedState";
export function Boolean({ configKey }: EmbedPreviewControllerProps) { export function Boolean({ configKey }: EmbedPreviewControllerProps) {
const mutateEmbedState = EmbedState.useMutation();
return ( return (
<Switch <Switch
variant="classic" variant="classic"
checked={EmbedState.use((state) => state[configKey] as boolean)} checked={EmbedState.use((state) => state[configKey] as boolean)}
onCheckedChange={() => { onCheckedChange={() => {
mutateEmbedState((draft) => { EmbedState.mutate((draft) => {
draft[configKey] = !draft[configKey]; draft[configKey] = !draft[configKey];
}); });
}} }}
@@ -1,16 +1,15 @@
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index'; import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
import { EmbedState } from '../../stores/embedState'; import { EmbedState } from "../../stores/embedState";
import { ColorControllerRaw } from './ColorControllerRaw'; import { ColorControllerRaw } from "./ColorControllerRaw";
export function Color({ configKey }: EmbedPreviewControllerProps) { export function Color({ configKey }: EmbedPreviewControllerProps) {
const mutateEmbedState = EmbedState.useMutation();
const color = EmbedState.use((state) => state[configKey] as string); const color = EmbedState.use((state) => state[configKey] as string);
return ( return (
<ColorControllerRaw <ColorControllerRaw
value={color} value={color}
onValueChange={(value) => { onValueChange={(value) => {
mutateEmbedState((draft) => { EmbedState.mutate((draft) => {
draft[configKey] = value; draft[configKey] = value;
}); });
}} }}
@@ -1,11 +1,11 @@
import { Select } from '@radix-ui/themes'; import { Select } from "@radix-ui/themes";
import { useLocale } from '../../hooks/useLocale'; import { useLocale } from "../../hooks/useLocale";
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index'; import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
import { EmbedState } from '../../stores/embedState'; import { EmbedState } from "../../stores/embedState";
import type { import type {
EmbedConfigItemType, EmbedConfigItemType,
EmbedItemType, EmbedItemType,
} from '../../stores/embedState/constants'; } from "../../stores/embedState/constants";
export function Enum({ export function Enum({
configKey, configKey,
@@ -13,14 +13,13 @@ export function Enum({
}: EmbedPreviewControllerProps & { }: EmbedPreviewControllerProps & {
config: EmbedConfigItemType<EmbedItemType.Enum>; config: EmbedConfigItemType<EmbedItemType.Enum>;
}) { }) {
const mutateEmbedState = EmbedState.useMutation();
const { locale } = useLocale(); const { locale } = useLocale();
return ( return (
<Select.Root <Select.Root
value={EmbedState.use((state) => state[configKey] as string)} value={EmbedState.use((state) => state[configKey] as string)}
onValueChange={(value) => { onValueChange={(value) => {
mutateEmbedState((draft) => { EmbedState.mutate((draft) => {
draft[configKey] = value; draft[configKey] = value;
}); });
}} }}
@@ -2,9 +2,9 @@ import {
searchPlayersAcrossRegions, searchPlayersAcrossRegions,
type AccountListItem, type AccountListItem,
type AccountListWithServer, type AccountListWithServer,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n/src/literals'; import { literals } from "@blitzkit/i18n/src/literals";
import { CaretLeftIcon, Link2Icon, PersonIcon } from '@radix-ui/react-icons'; import { CaretLeftIcon, Link2Icon, PersonIcon } from "@radix-ui/react-icons";
import { import {
Button, Button,
Dialog, Dialog,
@@ -13,15 +13,15 @@ import {
Spinner, Spinner,
Text, Text,
TextField, TextField,
} from '@radix-ui/themes'; } from "@radix-ui/themes";
import { debounce } from 'lodash-es'; import { debounce } from "lodash-es";
import { useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState } from "react";
import { stringify } from 'urlon'; import { stringify } from "urlon";
import type { embedConfigurations } from '../../constants/embeds'; import type { embedConfigurations } from "../../constants/embeds";
import { useLocale } from '../../hooks/useLocale'; import { useLocale } from "../../hooks/useLocale";
import { EmbedState, type EmbedStateStore } from '../../stores/embedState'; import { EmbedState, type EmbedStateStore } from "../../stores/embedState";
import { CopyButton } from '../CopyButton'; import { CopyButton } from "../CopyButton";
import { SearchResults } from '../SearchResults'; import { SearchResults } from "../SearchResults";
interface GenerateURLProps { interface GenerateURLProps {
embed: keyof typeof embedConfigurations; embed: keyof typeof embedConfigurations;
@@ -31,7 +31,6 @@ export function GenerateURL({ embed }: GenerateURLProps) {
const [searchDialogOpen, setSearchDialogOpen] = useState(false); const [searchDialogOpen, setSearchDialogOpen] = useState(false);
const [copyDialogOpen, setCopyDialogOpen] = useState(false); const [copyDialogOpen, setCopyDialogOpen] = useState(false);
const [copyUser, setCopyUser] = useState<AccountListItem | null>(null); const [copyUser, setCopyUser] = useState<AccountListItem | null>(null);
const embedStateStore = EmbedState.useStore();
const input = useRef<HTMLInputElement>(null); const input = useRef<HTMLInputElement>(null);
const [searching, setSearching] = useState(false); const [searching, setSearching] = useState(false);
const [results, setResults] = useState<AccountListWithServer>([]); const [results, setResults] = useState<AccountListWithServer>([]);
@@ -52,7 +51,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
setSearching(false); setSearching(false);
setResults(results); setResults(results);
}, 500), }, 500),
[], []
); );
const handleChange = useCallback(() => { const handleChange = useCallback(() => {
if (!input.current) return; if (!input.current) return;
@@ -94,7 +93,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
<TextField.Root <TextField.Root
variant="classic" variant="classic"
mt="4" mt="4"
style={{ width: '20rem' }} style={{ width: "20rem" }}
placeholder={strings.website.common.player_search.hint} placeholder={strings.website.common.player_search.hint}
ref={input} ref={input}
onChange={handleChange} onChange={handleChange}
@@ -136,7 +135,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
</Flex> </Flex>
<Dialog.Close> <Dialog.Close>
<Button color="red" variant="outline" style={{ width: '100%' }}> <Button color="red" variant="outline" style={{ width: "100%" }}>
{strings.website.tools.embed.configuration.export.generate.cancel} {strings.website.tools.embed.configuration.export.generate.cancel}
</Button> </Button>
</Dialog.Close> </Dialog.Close>
@@ -144,7 +143,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
</Dialog.Root> </Dialog.Root>
<Dialog.Root open={copyDialogOpen} onOpenChange={setCopyDialogOpen}> <Dialog.Root open={copyDialogOpen} onOpenChange={setCopyDialogOpen}>
<Dialog.Content style={{ width: 'fit-content' }}> <Dialog.Content style={{ width: "fit-content" }}>
<Flex direction="column" gap="1"> <Flex direction="column" gap="1">
<Heading size="5"> <Heading size="5">
{ {
@@ -156,7 +155,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
{literals( {literals(
strings.website.tools.embed.configuration.export.generate strings.website.tools.embed.configuration.export.generate
.success.info, .success.info,
[`${copyUser?.nickname}`], [`${copyUser?.nickname}`]
)} )}
</Text> </Text>
</Flex> </Flex>
@@ -170,7 +169,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
setSearchDialogOpen(true); setSearchDialogOpen(true);
}} }}
> >
<CaretLeftIcon />{' '} <CaretLeftIcon />{" "}
{ {
strings.website.tools.embed.configuration.export.generate strings.website.tools.embed.configuration.export.generate
.success.back .success.back
@@ -181,12 +180,10 @@ export function GenerateURL({ embed }: GenerateURLProps) {
copy={() => { copy={() => {
if (!copyUser) return; if (!copyUser) return;
const state = embedStateStore.getState();
const initial = embedStateStore.getInitialState();
const shallowState: EmbedStateStore = {}; const shallowState: EmbedStateStore = {};
Object.entries(state).forEach(([key, value]) => { Object.entries(EmbedState.state).forEach(([key, value]) => {
if (value !== initial[key]) { if (value !== EmbedState.initial[key]) {
shallowState[key] = value; shallowState[key] = value;
} }
}); });
@@ -202,7 +199,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
return `${location.origin}/embed/${embed}/host?${searchParams.toString()}`; return `${location.origin}/embed/${embed}/host?${searchParams.toString()}`;
}} }}
> >
<Link2Icon />{' '} <Link2Icon />{" "}
{ {
strings.website.tools.embed.configuration.export.generate strings.website.tools.embed.configuration.export.generate
.success.copy .success.copy
@@ -6,17 +6,16 @@ import {
Text, Text,
TextField, TextField,
Tooltip, Tooltip,
} from '@radix-ui/themes'; } from "@radix-ui/themes";
import { useRef, useState } from 'react'; import { useRef, useState } from "react";
import { parse } from 'urlon'; import { parse } from "urlon";
import { useLocale } from '../../hooks/useLocale'; import { useLocale } from "../../hooks/useLocale";
import { EmbedState } from '../../stores/embedState'; import { EmbedState } from "../../stores/embedState";
export function Import() { export function Import() {
const importInput = useRef<HTMLInputElement>(null); const importInput = useRef<HTMLInputElement>(null);
const [showInvalidURLPrompt, setShowInvalidURLPrompt] = useState(false); const [showInvalidURLPrompt, setShowInvalidURLPrompt] = useState(false);
const [showConfirmation, setConfirmation] = useState(false); const [showConfirmation, setConfirmation] = useState(false);
const mutateEmbedState = EmbedState.useMutation();
const { strings } = useLocale(); const { strings } = useLocale();
return ( return (
@@ -64,15 +63,15 @@ export function Import() {
try { try {
const url = new URL(importInput.current.value); const url = new URL(importInput.current.value);
const state = url.searchParams.get('state'); const state = url.searchParams.get("state");
if (state === null) throw undefined; if (state === null) throw undefined;
const json = parse(state); const json = parse(state);
mutateEmbedState((draft) => { EmbedState.mutate((draft) => {
Object.assign(draft, json); Object.assign(draft, json);
}); });
importInput.current.value = ''; importInput.current.value = "";
setConfirmation(true); setConfirmation(true);
setTimeout(() => setConfirmation(false), 2000); setTimeout(() => setConfirmation(false), 2000);
} catch { } catch {
@@ -1,10 +1,8 @@
import { Slider } from '@radix-ui/themes'; import { Slider } from "@radix-ui/themes";
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index'; import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
import { EmbedState, type RadixRadius } from '../../stores/embedState'; import { EmbedState, type RadixRadius } from "../../stores/embedState";
export function Radius({ configKey }: EmbedPreviewControllerProps) { export function Radius({ configKey }: EmbedPreviewControllerProps) {
const mutateEmbedState = EmbedState.useMutation();
return ( return (
<Slider <Slider
variant="classic" variant="classic"
@@ -13,12 +11,12 @@ export function Radius({ configKey }: EmbedPreviewControllerProps) {
value={[ value={[
EmbedState.use((state) => { EmbedState.use((state) => {
const radius = state[configKey] as RadixRadius; const radius = state[configKey] as RadixRadius;
return radius === 'full' ? 5 : parseInt(radius); return radius === "full" ? 5 : parseInt(radius);
}), }),
]} ]}
onValueChange={([value]) => { onValueChange={([value]) => {
mutateEmbedState((draft) => { EmbedState.mutate((draft) => {
draft[configKey] = value === 5 ? 'full' : `${value}`; draft[configKey] = value === 5 ? "full" : `${value}`;
}); });
}} }}
/> />
@@ -1,21 +1,18 @@
import { FontBoldIcon, FontSizeIcon } from '@radix-ui/react-icons'; import { FontBoldIcon, FontSizeIcon } from "@radix-ui/react-icons";
import { Flex, Slider, Text } from '@radix-ui/themes'; import { Flex, Slider, Text } from "@radix-ui/themes";
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index'; import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
import { EmbedState, type RadixSizeWithout0 } from '../../stores/embedState'; import { EmbedState, type RadixSizeWithout0 } from "../../stores/embedState";
import { import {
radixTextWeights, radixTextWeights,
type EmbedConfigItemType, type EmbedConfigItemType,
type EmbedItemType, type EmbedItemType,
} from '../../stores/embedState/constants'; } from "../../stores/embedState/constants";
import { ColorControllerRaw } from './ColorControllerRaw'; import { ColorControllerRaw } from "./ColorControllerRaw";
export function RichText({ configKey }: EmbedPreviewControllerProps) { export function RichText({ configKey }: EmbedPreviewControllerProps) {
const mutateEmbedState = EmbedState.useMutation();
const state = EmbedState.use( const state = EmbedState.use(
(state) => (state) =>
state[ state[configKey] as EmbedConfigItemType<EmbedItemType.RichText>["default"]
configKey
] as EmbedConfigItemType<EmbedItemType.RichText>['default'],
); );
return ( return (
@@ -24,11 +21,11 @@ export function RichText({ configKey }: EmbedPreviewControllerProps) {
<ColorControllerRaw <ColorControllerRaw
value={state.color} value={state.color}
onValueChange={(value) => { onValueChange={(value) => {
mutateEmbedState((draft) => { EmbedState.mutate((draft) => {
( (
draft[ draft[
configKey configKey
] as EmbedConfigItemType<EmbedItemType.RichText>['default'] ] as EmbedConfigItemType<EmbedItemType.RichText>["default"]
).color = value; ).color = value;
}); });
}} }}
@@ -45,11 +42,11 @@ export function RichText({ configKey }: EmbedPreviewControllerProps) {
max={9} max={9}
value={[Number(state.size)]} value={[Number(state.size)]}
onValueChange={([value]) => { onValueChange={([value]) => {
mutateEmbedState((draft) => { EmbedState.mutate((draft) => {
( (
draft[ draft[
configKey configKey
] as EmbedConfigItemType<EmbedItemType.RichText>['default'] ] as EmbedConfigItemType<EmbedItemType.RichText>["default"]
).size = `${value}` as RadixSizeWithout0; ).size = `${value}` as RadixSizeWithout0;
}); });
}} }}
@@ -66,11 +63,11 @@ export function RichText({ configKey }: EmbedPreviewControllerProps) {
max={radixTextWeights.length - 1} max={radixTextWeights.length - 1}
value={[radixTextWeights.indexOf(state.weight)]} value={[radixTextWeights.indexOf(state.weight)]}
onValueChange={([value]) => { onValueChange={([value]) => {
mutateEmbedState((draft) => { EmbedState.mutate((draft) => {
( (
draft[ draft[
configKey configKey
] as EmbedConfigItemType<EmbedItemType.RichText>['default'] ] as EmbedConfigItemType<EmbedItemType.RichText>["default"]
).weight = radixTextWeights[value]; ).weight = radixTextWeights[value];
}); });
}} }}
@@ -1,10 +1,8 @@
import { Slider } from '@radix-ui/themes'; import { Slider } from "@radix-ui/themes";
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index'; import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
import { EmbedState, type RadixSize } from '../../stores/embedState'; import { EmbedState, type RadixSize } from "../../stores/embedState";
export function Size({ configKey }: EmbedPreviewControllerProps) { export function Size({ configKey }: EmbedPreviewControllerProps) {
const mutateEmbedState = EmbedState.useMutation();
return ( return (
<Slider <Slider
variant="classic" variant="classic"
@@ -14,7 +12,7 @@ export function Size({ configKey }: EmbedPreviewControllerProps) {
EmbedState.use((state) => parseInt(state[configKey] as RadixSize)), EmbedState.use((state) => parseInt(state[configKey] as RadixSize)),
]} ]}
onValueChange={([value]) => { onValueChange={([value]) => {
mutateEmbedState((draft) => { EmbedState.mutate((draft) => {
draft[configKey] = `${value}`; draft[configKey] = `${value}`;
}); });
}} }}
@@ -1,10 +1,8 @@
import { Slider } from '@radix-ui/themes'; import { Slider } from "@radix-ui/themes";
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index'; import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
import { EmbedState, type RadixSizeWithout0 } from '../../stores/embedState'; import { EmbedState, type RadixSizeWithout0 } from "../../stores/embedState";
export function SizeWithout0({ configKey }: EmbedPreviewControllerProps) { export function SizeWithout0({ configKey }: EmbedPreviewControllerProps) {
const mutateEmbedState = EmbedState.useMutation();
return ( return (
<Slider <Slider
variant="classic" variant="classic"
@@ -12,11 +10,11 @@ export function SizeWithout0({ configKey }: EmbedPreviewControllerProps) {
max={9} max={9}
value={[ value={[
EmbedState.use((state) => EmbedState.use((state) =>
parseInt(state[configKey] as RadixSizeWithout0), parseInt(state[configKey] as RadixSizeWithout0)
), ),
]} ]}
onValueChange={([value]) => { onValueChange={([value]) => {
mutateEmbedState((draft) => { EmbedState.mutate((draft) => {
draft[configKey] = `${value}`; draft[configKey] = `${value}`;
}); });
}} }}
@@ -1,10 +1,10 @@
import { Slider as RadixSlider } from '@radix-ui/themes'; import { Slider as RadixSlider } from "@radix-ui/themes";
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index'; import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
import { EmbedState, type RadixSize } from '../../stores/embedState'; import { EmbedState, type RadixSize } from "../../stores/embedState";
import type { import type {
EmbedConfigItemType, EmbedConfigItemType,
EmbedItemType, EmbedItemType,
} from '../../stores/embedState/constants'; } from "../../stores/embedState/constants";
export function Slider({ export function Slider({
configKey, configKey,
@@ -12,8 +12,6 @@ export function Slider({
}: EmbedPreviewControllerProps & { }: EmbedPreviewControllerProps & {
config: EmbedConfigItemType<EmbedItemType.Slider>; config: EmbedConfigItemType<EmbedItemType.Slider>;
}) { }) {
const mutateEmbedState = EmbedState.useMutation();
return ( return (
<RadixSlider <RadixSlider
variant="classic" variant="classic"
@@ -23,7 +21,7 @@ export function Slider({
EmbedState.use((state) => parseInt(state[configKey] as RadixSize)), EmbedState.use((state) => parseInt(state[configKey] as RadixSize)),
]} ]}
onValueChange={([value]) => { onValueChange={([value]) => {
mutateEmbedState((draft) => { EmbedState.mutate((draft) => {
draft[configKey] = value; draft[configKey] = value;
}); });
}} }}
@@ -1,17 +1,15 @@
import { TextField } from '@radix-ui/themes'; import { TextField } from "@radix-ui/themes";
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index'; import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
import { EmbedState } from '../../stores/embedState'; import { EmbedState } from "../../stores/embedState";
export function TextController({ configKey }: EmbedPreviewControllerProps) { export function TextController({ configKey }: EmbedPreviewControllerProps) {
const mutateEmbedState = EmbedState.useMutation();
return ( return (
<TextField.Root <TextField.Root
variant="classic" variant="classic"
placeholder="Empty" placeholder="Empty"
value={EmbedState.use((state) => state[configKey] as string)} value={EmbedState.use((state) => state[configKey] as string)}
onChange={(event) => { onChange={(event) => {
mutateEmbedState((draft) => { EmbedState.mutate((draft) => {
draft[configKey] = event.target.value; draft[configKey] = event.target.value;
}); });
}} }}
@@ -17,10 +17,9 @@ const [modelDefinitions, provisionDefinitions] = await Promise.all([
export function GuessBackground() { export function GuessBackground() {
const guessState = Guess.use((state) => state.guessState); const guessState = Guess.use((state) => state.guessState);
const tank = Guess.use((state) => state.tank); const tank = Guess.use((state) => state.tank);
const mutateDuel = Duel.useMutation();
useEffect(() => { useEffect(() => {
mutateDuel((state) => { Duel.mutate((state) => {
state.protagonist = tankToDuelMember(tank, provisionDefinitions); state.protagonist = tankToDuelMember(tank, provisionDefinitions);
}); });
Tankopedia.mutate((state) => { Tankopedia.mutate((state) => {
@@ -1,15 +1,14 @@
import { literals } from '@blitzkit/i18n/src/literals'; import { literals } from "@blitzkit/i18n/src/literals";
import { EyeOpenIcon } from '@radix-ui/react-icons'; import { EyeOpenIcon } from "@radix-ui/react-icons";
import { Box, Flex, Heading, Text } from '@radix-ui/themes'; import { Box, Flex, Heading, Text } from "@radix-ui/themes";
import { awaitablePopularTanks } from '../core/awaitables/popularTanks'; import { awaitablePopularTanks } from "../core/awaitables/popularTanks";
import { awaitableTankDefinitions } from '../core/awaitables/tankDefinitions'; import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions";
import { import {
LocaleProvider, LocaleProvider,
useLocale, useLocale,
type LocaleAcceptorProps, type LocaleAcceptorProps,
} from '../hooks/useLocale'; } from "../hooks/useLocale";
import { TankopediaPersistent } from '../stores/tankopediaPersistent'; import { TankCard } from "./TankCard";
import { TankCard } from './TankCard';
const [tankDefinitions, popularTanks] = await Promise.all([ const [tankDefinitions, popularTanks] = await Promise.all([
awaitableTankDefinitions, awaitableTankDefinitions,
@@ -19,9 +18,7 @@ const [tankDefinitions, popularTanks] = await Promise.all([
export function HomePageHotTanks({ locale }: LocaleAcceptorProps) { export function HomePageHotTanks({ locale }: LocaleAcceptorProps) {
return ( return (
<LocaleProvider locale={locale}> <LocaleProvider locale={locale}>
<TankopediaPersistent.Provider> <Content />
<Content />
</TankopediaPersistent.Provider>
</LocaleProvider> </LocaleProvider>
); );
} }
@@ -35,7 +32,7 @@ function Content() {
{strings.website.home.hot_tanks.title} {strings.website.home.hot_tanks.title}
</Heading> </Heading>
<Text color="gray" align="center" mb="5" size="2"> <Text color="gray" align="center" mb="5" size="2">
{literals(strings.website.home.hot_tanks.subtitle, ['7'])} {literals(strings.website.home.hot_tanks.subtitle, ["7"])}
</Text> </Text>
<Flex justify="center" gap="4" wrap="wrap"> <Flex justify="center" gap="4" wrap="wrap">
@@ -1,4 +1,4 @@
import { PersonIcon } from '@radix-ui/react-icons'; import { PersonIcon } from "@radix-ui/react-icons";
import { import {
Button, Button,
Flex, Flex,
@@ -6,91 +6,78 @@ import {
Popover, Popover,
Skeleton, Skeleton,
Text, Text,
} from '@radix-ui/themes'; } from "@radix-ui/themes";
import { Suspense } from 'react'; import { Suspense } from "react";
import { App } from '../../stores/app'; import { App } from "../../stores/app";
import { BlitzKitTheme } from '../BlitzKitTheme'; import { BlitzKitTheme } from "../BlitzKitTheme";
import { WargamingAccountName } from '../WargamingAccountName'; import { WargamingAccountName } from "../WargamingAccountName";
import { WargamingIcon } from '../WargamingIcon'; import { WargamingIcon } from "../WargamingIcon";
import { WargamingLoginButton } from '../WargamingLoginButton'; import { WargamingLoginButton } from "../WargamingLoginButton";
export function NavbarAccount() { export function NavbarAccount() {
return (
<App.Provider>
<Content />
</App.Provider>
);
}
function Content() {
const logins = App.use((state) => state.logins); const logins = App.use((state) => state.logins);
const mutateApp = App.useMutation();
return ( return (
<App.Provider> <Popover.Root>
<Popover.Root> <Popover.Trigger>
<Popover.Trigger> <IconButton variant="ghost" color="gray">
<IconButton variant="ghost" color="gray"> <PersonIcon />
<PersonIcon /> </IconButton>
</IconButton> </Popover.Trigger>
</Popover.Trigger>
<BlitzKitTheme style={{ display: 'contents' }}> <BlitzKitTheme style={{ display: "contents" }}>
<Popover.Content <Popover.Content
align="end" align="end"
width="320px" width="320px"
onOpenAutoFocus={(event) => event.preventDefault()} onOpenAutoFocus={(event) => event.preventDefault()}
> >
{logins.wargaming && ( {logins.wargaming && (
<Flex direction="column" gap="2"> <Flex direction="column" gap="2">
<Text align="center" color="gray"> <Text align="center" color="gray">
Logged in accounts Logged in accounts
</Text> </Text>
{logins.wargaming && ( {logins.wargaming && (
<Flex align="center" gap="2" justify="center"> <Flex align="center" gap="2" justify="center">
<WargamingIcon width={15} height={15} /> <WargamingIcon width={15} height={15} />
<Text> <Text>
<Suspense <Suspense fallback={<Skeleton height="1em" width="5rem" />}>
fallback={<Skeleton height="1em" width="5rem" />} <WargamingAccountName />
> </Suspense>
<WargamingAccountName /> </Text>
</Suspense> <Button
</Text> color="red"
<Button variant="ghost"
color="red" onClick={() => {
variant="ghost" App.mutate((draft) => {
onClick={() => { draft.logins.wargaming = undefined;
mutateApp((draft) => { });
draft.logins.wargaming = undefined; }}
}); >
}} Logout
> </Button>
Logout </Flex>
</Button> )}
</Flex> </Flex>
)} )}
</Flex>
)}
{/* {!logins. !== !logins.wargaming && ( {/* {!logins. !== !logins.wargaming && (
<Inset side="x"> <Inset side="x">
<Separator my="4" size="4" /> <Separator my="4" size="4" />
</Inset> </Inset>
)} */} )} */}
{!logins.wargaming && ( {!logins.wargaming && (
<Flex direction="column" gap="2"> <Flex direction="column" gap="2">
<Text align="center" color="gray"> <Text align="center" color="gray">
Log in with... Log in with...
</Text> </Text>
{!logins.wargaming && <WargamingLoginButton />} {!logins.wargaming && <WargamingLoginButton />}
</Flex> </Flex>
)} )}
</Popover.Content> </Popover.Content>
</BlitzKitTheme> </BlitzKitTheme>
</Popover.Root> </Popover.Root>
</App.Provider>
); );
} }
@@ -2,16 +2,15 @@ import {
CaretDownIcon, CaretDownIcon,
CaretSortIcon, CaretSortIcon,
CaretUpIcon, CaretUpIcon,
} from '@radix-ui/react-icons'; } from "@radix-ui/react-icons";
import { Button, Flex, Table } from '@radix-ui/themes'; import { Button, Flex, Table } from "@radix-ui/themes";
import { useLocale } from '../../hooks/useLocale'; import { useLocale } from "../../hooks/useLocale";
import { TankPerformanceSort } from '../../stores/tankPerformanceSort'; import { TankPerformanceSort } from "../../stores/tankPerformanceSort";
import { TankPerformanceSortTypeNamesArray } from '../../stores/tankPerformanceSort/constants'; import { TankPerformanceSortTypeNamesArray } from "../../stores/tankPerformanceSort/constants";
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell'; import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
export function Header() { export function Header() {
const sort = TankPerformanceSort.use(); const sort = TankPerformanceSort.use();
const store = TankPerformanceSort.useStore();
const { strings } = useLocale(); const { strings } = useLocale();
return ( return (
@@ -34,13 +33,18 @@ export function Header() {
<Flex align="center" gap="1"> <Flex align="center" gap="1">
<Button <Button
variant="ghost" variant="ghost"
color={isSelected ? undefined : 'gray'} color={isSelected ? undefined : "gray"}
highContrast={!isSelected} highContrast={!isSelected}
onClick={() => { onClick={() => {
if (isSelected) { if (isSelected) {
store.setState({ direction: -sort.direction as 1 | -1 }); TankPerformanceSort.mutate((draft) => {
draft.direction *= -1;
});
} else { } else {
store.setState({ type, direction: -1 }); TankPerformanceSort.mutate((draft) => {
draft.type = type;
draft.direction = -1;
});
} }
}} }}
> >
+8 -17
View File
@@ -1,20 +1,11 @@
import { assertSecret } from '@blitzkit/core'; import { assertSecret } from "@blitzkit/core";
import { ArrowRightIcon } from '@radix-ui/react-icons'; import { ArrowRightIcon } from "@radix-ui/react-icons";
import { Button, Code, Flex, Link, Text } from '@radix-ui/themes'; import { Button, Code, Flex, Link, Text } from "@radix-ui/themes";
import { App } from '../stores/app';
export function Plugs() { export function Plugs() {
return (
<App.Provider>
<Content />
</App.Provider>
);
}
function Content() {
const promoteOpentest = const promoteOpentest =
assertSecret(import.meta.env.PUBLIC_PROMOTE_OPENTEST) === 'true' && assertSecret(import.meta.env.PUBLIC_PROMOTE_OPENTEST) === "true" &&
assertSecret(import.meta.env.PUBLIC_BRANCH) !== 'opentest'; assertSecret(import.meta.env.PUBLIC_BRANCH) !== "opentest";
const promotions = [promoteOpentest]; const promotions = [promoteOpentest];
if (promotions.every((promotion) => !promotion)) return null; if (promotions.every((promotion) => !promotion)) return null;
@@ -31,11 +22,11 @@ function Content() {
{promoteOpentest && ( {promoteOpentest && (
<Flex direction="column" gap="3" align="center"> <Flex direction="column" gap="3" align="center">
<Text size="5"> <Text size="5">
BlitzKit{' '} BlitzKit{" "}
<Code variant="outline" size="4" color="gray"> <Code variant="outline" size="4" color="gray">
opentest opentest
</Code>{' '} </Code>{" "}
is available now{' '} is available now{" "}
</Text> </Text>
<Link href="https://opentest.blitzkit.app/" target="_blank"> <Link href="https://opentest.blitzkit.app/" target="_blank">
@@ -3,16 +3,16 @@ import {
fetchTankDefinitions, fetchTankDefinitions,
TankType, TankType,
type TankDefinition, type TankDefinition,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { Flex, Text, type TextProps } from '@radix-ui/themes'; import { Flex, Text, type TextProps } from "@radix-ui/themes";
import { uniq } from 'lodash-es'; import { uniq } from "lodash-es";
import { forwardRef, type ReactNode } from 'react'; import { forwardRef, type ReactNode } from "react";
import { useLocale } from '../../hooks/useLocale'; import { useLocale } from "../../hooks/useLocale";
import { TankopediaPersistent } from '../../stores/tankopediaPersistent'; import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
import { classIcons } from '../ClassIcon'; import { classIcons } from "../ClassIcon";
import { LinkI18n } from '../LinkI18n'; import { LinkI18n } from "../LinkI18n";
import { MAX_RECENTLY_VIEWED } from '../TankSearch/constants'; import { MAX_RECENTLY_VIEWED } from "../TankSearch/constants";
import './index.css'; import "./index.css";
type TankCardProps = TextProps & { type TankCardProps = TextProps & {
tank: TankDefinition; tank: TankDefinition;
@@ -33,11 +33,11 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
style, style,
...props ...props
}: TankCardProps, }: TankCardProps,
ref, ref
) => { ) => {
const { unwrap, locale } = useLocale(); const { unwrap, locale } = useLocale();
const provideLink = !noLink && onSelect === undefined; const provideLink = !noLink && onSelect === undefined;
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
const Icon = classIcons[tank.class]; const Icon = classIcons[tank.class];
return ( return (
@@ -47,15 +47,15 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
size="2" size="2"
color={ color={
tank.type === TankType.COLLECTOR tank.type === TankType.COLLECTOR
? 'blue' ? "blue"
: tank.type === TankType.PREMIUM : tank.type === TankType.PREMIUM
? 'amber' ? "amber"
: 'gray' : "gray"
} }
highContrast={tank.type === TankType.RESEARCHABLE} highContrast={tank.type === TankType.RESEARCHABLE}
onClick={() => { onClick={() => {
onSelect?.(tank); onSelect?.(tank);
mutateTankopediaPersistent((draft) => { TankopediaPersistent.mutate((draft) => {
draft.recentlyViewed = uniq([tank.id, ...draft.recentlyViewed]) draft.recentlyViewed = uniq([tank.id, ...draft.recentlyViewed])
.filter((id) => id in tankDefinitions.tanks) .filter((id) => id in tankDefinitions.tanks)
.slice(0, MAX_RECENTLY_VIEWED); .slice(0, MAX_RECENTLY_VIEWED);
@@ -73,7 +73,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
locale={locale} locale={locale}
className="link" className="link"
underline="hover" underline="hover"
href={provideLink ? `/tanks/${tank.slug}` : '#'} href={provideLink ? `/tanks/${tank.slug}` : "#"}
onClick={(event) => { onClick={(event) => {
if (!provideLink) event.preventDefault(); if (!provideLink) event.preventDefault();
}} }}
@@ -104,9 +104,9 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
color="gray" color="gray"
align="center" align="center"
style={{ style={{
whiteSpace: 'nowrap', whiteSpace: "nowrap",
overflow: 'hidden', overflow: "hidden",
textOverflow: 'ellipsis', textOverflow: "ellipsis",
}} }}
> >
{discriminator} {discriminator}
@@ -115,5 +115,5 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
</LinkI18n> </LinkI18n>
</Text> </Text>
); );
}, }
); );
@@ -12,12 +12,11 @@ import { TankCardWrapper } from "./TankCardWrapper";
const tankDefinitions = await awaitableTankDefinitions; const tankDefinitions = await awaitableTankDefinitions;
export function RecentlyViewed() { export function RecentlyViewed() {
const tankopediaPersistentStore = TankopediaPersistent.useStore();
const filters = TankFilters.use(); const filters = TankFilters.use();
// non-reactive because it is a little weird that it updates instantly even before the page loads // non-reactive because it is a little weird that it updates instantly even before the page loads
const recentlyViewed = tankopediaPersistentStore const recentlyViewed = TankopediaPersistent.state.recentlyViewed.filter(
.getState() (id) => id in tankDefinitions.tanks
.recentlyViewed.filter((id) => id in tankDefinitions.tanks); );
const hasFilters = useMemo( const hasFilters = useMemo(
() => () =>
Object.entries(filters).some(([key, value]) => { Object.entries(filters).some(([key, value]) => {
@@ -54,7 +54,7 @@ const [gameDefinitions, modelDefinitions, tankDefinitions, tankNames] =
export const TankSearch = memo<TankSearchProps>( export const TankSearch = memo<TankSearchProps>(
({ compact, onSelect, onSelectAll, skeleton, ...props }) => { ({ compact, onSelect, onSelectAll, skeleton, ...props }) => {
const { strings, locale } = useLocale(); const { strings, locale } = useLocale();
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks); const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks);
const tankopediaSort = TankSort.use(); const tankopediaSort = TankSort.use();
const tankFilters = TankFilters.use(); const tankFilters = TankFilters.use();
@@ -443,7 +443,7 @@ export const TankSearch = memo<TankSearchProps>(
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
onSelectAll(tanksFiltered); onSelectAll(tanksFiltered);
mutateTankopediaPersistent((draft) => { TankopediaPersistent.mutate((draft) => {
draft.recentlyViewed = uniq([ draft.recentlyViewed = uniq([
...tanksFiltered.map(({ id }) => id), ...tanksFiltered.map(({ id }) => id),
...draft.recentlyViewed, ...draft.recentlyViewed,
@@ -1,24 +1,23 @@
import { assertSecret, fetchTankDefinitions } from '@blitzkit/core'; import { assertSecret, fetchTankDefinitions } from "@blitzkit/core";
import { MinusCircledIcon } from '@radix-ui/react-icons'; import { MinusCircledIcon } from "@radix-ui/react-icons";
import { Callout, Flex } from '@radix-ui/themes'; import { Callout, Flex } from "@radix-ui/themes";
import { uniq } from 'lodash-es'; import { uniq } from "lodash-es";
import { useEffect } from 'react'; import { useEffect } from "react";
import { Duel } from '../../stores/duel'; import { Duel } from "../../stores/duel";
import { TankopediaPersistent } from '../../stores/tankopediaPersistent'; import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
import { AesonPlug } from '../AesonPlug'; import { AesonPlug } from "../AesonPlug";
import { ExperimentIcon } from '../ExperimentIcon'; import { ExperimentIcon } from "../ExperimentIcon";
import { MAX_RECENTLY_VIEWED } from '../TankSearch/constants'; import { MAX_RECENTLY_VIEWED } from "../TankSearch/constants";
const tankDefinitions = await fetchTankDefinitions(); const tankDefinitions = await fetchTankDefinitions();
export function CalloutsSection() { export function CalloutsSection() {
const tank = Duel.use((state) => state.protagonist.tank); const tank = Duel.use((state) => state.protagonist.tank);
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
const promoteAeson = const promoteAeson =
assertSecret(import.meta.env.PUBLIC_BRANCH) === 'preview'; assertSecret(import.meta.env.PUBLIC_BRANCH) === "preview";
useEffect(() => { useEffect(() => {
mutateTankopediaPersistent((draft) => { TankopediaPersistent.mutate((draft) => {
draft.recentlyViewed = uniq([tank.id, ...draft.recentlyViewed]) draft.recentlyViewed = uniq([tank.id, ...draft.recentlyViewed])
.filter((id) => id in tankDefinitions.tanks) .filter((id) => id in tankDefinitions.tanks)
.slice(0, MAX_RECENTLY_VIEWED); .slice(0, MAX_RECENTLY_VIEWED);
@@ -45,7 +44,7 @@ export function CalloutsSection() {
{tank.testing && ( {tank.testing && (
<Callout.Root color="red"> <Callout.Root color="red">
<Callout.Icon> <Callout.Icon>
<ExperimentIcon style={{ width: '1em', height: '1em' }} /> <ExperimentIcon style={{ width: "1em", height: "1em" }} />
</Callout.Icon> </Callout.Icon>
<Callout.Text> <Callout.Text>
Tanks in testing are subject to change and may not represent the Tanks in testing are subject to change and may not represent the
@@ -1,14 +1,14 @@
import { DragHandleDots2Icon } from '@radix-ui/react-icons'; import { DragHandleDots2Icon } from "@radix-ui/react-icons";
import { Box, Flex, Text } from '@radix-ui/themes'; import { Box, Flex, Text } from "@radix-ui/themes";
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from "react";
import { clamp } from 'three/src/math/MathUtils.js'; import { clamp } from "three/src/math/MathUtils.js";
import { import {
fakeLogSquishyHigh, fakeLogSquishyHigh,
fakeLogSquishyHighInverse, fakeLogSquishyHighInverse,
} from '../../../../../../../../../core/blitzkit/fakeLog'; } from "../../../../../../../../../core/blitzkit/fakeLog";
import { Var } from '../../../../../../../../../core/radix/var'; import { Var } from "../../../../../../../../../core/radix/var";
import { Duel } from '../../../../../../../../../stores/duel'; import { Duel } from "../../../../../../../../../stores/duel";
import './index.css'; import "./index.css";
interface Props { interface Props {
maxDistance: number; maxDistance: number;
@@ -17,11 +17,10 @@ interface Props {
export function AssaultCursor({ maxDistance, maxFakeDistance }: Props) { export function AssaultCursor({ maxDistance, maxFakeDistance }: Props) {
const assaultDistance = Duel.use( const assaultDistance = Duel.use(
(state) => state.protagonist.assaultDistance, (state) => state.protagonist.assaultDistance
); );
const [x, setX] = useState(fakeLogSquishyHigh(assaultDistance / maxDistance)); const [x, setX] = useState(fakeLogSquishyHigh(assaultDistance / maxDistance));
const cursor = useRef<HTMLDivElement>(null); const cursor = useRef<HTMLDivElement>(null);
const mutateDuel = Duel.useMutation();
useEffect(() => { useEffect(() => {
let localX = x; let localX = x;
@@ -32,8 +31,8 @@ export function AssaultCursor({ maxDistance, maxFakeDistance }: Props) {
lastX = event.clientX; lastX = event.clientX;
window.addEventListener('pointermove', handlePointerMove); window.addEventListener("pointermove", handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener("pointerup", handlePointerUp);
} }
function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
@@ -54,21 +53,21 @@ export function AssaultCursor({ maxDistance, maxFakeDistance }: Props) {
} }
function handlePointerUp() { function handlePointerUp() {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.protagonist.assaultDistance = draft.protagonist.assaultDistance =
fakeLogSquishyHighInverse(localX * maxFakeDistance) * maxDistance; fakeLogSquishyHighInverse(localX * maxFakeDistance) * maxDistance;
}); });
window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener("pointerup", handlePointerUp);
} }
cursor.current?.addEventListener('pointerdown', handlePointerDown); cursor.current?.addEventListener("pointerdown", handlePointerDown);
return () => { return () => {
cursor.current?.removeEventListener('pointerdown', handlePointerDown); cursor.current?.removeEventListener("pointerdown", handlePointerDown);
window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener("pointerup", handlePointerUp);
}; };
}, []); }, []);
@@ -82,9 +81,9 @@ export function AssaultCursor({ maxDistance, maxFakeDistance }: Props) {
width="2pt" width="2pt"
id="assault-cursor" id="assault-cursor"
style={{ style={{
touchAction: 'none', touchAction: "none",
transform: 'translateX(-50%)', transform: "translateX(-50%)",
backgroundColor: Var('accent-11'), backgroundColor: Var("accent-11"),
}} }}
> >
<Flex <Flex
@@ -93,19 +92,19 @@ export function AssaultCursor({ maxDistance, maxFakeDistance }: Props) {
position="absolute" position="absolute"
left="50%" left="50%"
bottom="100%" bottom="100%"
style={{ transform: 'translateX(-50%)' }} style={{ transform: "translateX(-50%)" }}
> >
<Flex <Flex
align="center" align="center"
justify="center" justify="center"
style={{ style={{
width: '1.5rem', width: "1.5rem",
height: '1.5rem', height: "1.5rem",
backgroundColor: Var('accent-9'), backgroundColor: Var("accent-9"),
borderRadius: `50% 50% ${Var('radius-1')} ${Var('radius-1')}`, borderRadius: `50% 50% ${Var("radius-1")} ${Var("radius-1")}`,
}} }}
> >
<Text style={{ display: 'flex' }} highContrast color="purple"> <Text style={{ display: "flex" }} highContrast color="purple">
<DragHandleDots2Icon /> <DragHandleDots2Icon />
</Text> </Text>
</Flex> </Flex>
@@ -114,7 +113,7 @@ export function AssaultCursor({ maxDistance, maxFakeDistance }: Props) {
height="0.5rem" height="0.5rem"
width="2pt" width="2pt"
style={{ style={{
backgroundColor: Var('accent-11'), backgroundColor: Var("accent-11"),
}} }}
/> />
</Flex> </Flex>
@@ -1,24 +1,23 @@
import { asset, isExplosive } from '@blitzkit/core'; import { asset, isExplosive } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { Flex, Heading, IconButton, Tooltip } from '@radix-ui/themes'; import { Flex, Heading, IconButton, Tooltip } from "@radix-ui/themes";
import { useLocale } from '../../../../../../hooks/useLocale'; import { useLocale } from "../../../../../../hooks/useLocale";
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition'; import { useTankModelDefinition } from "../../../../../../hooks/useTankModelDefinition";
import { Duel } from '../../../../../../stores/duel'; import { Duel } from "../../../../../../stores/duel";
import type { MaybeSkeletonComponentProps } from '../../../../../../types/maybeSkeletonComponentProps'; import type { MaybeSkeletonComponentProps } from "../../../../../../types/maybeSkeletonComponentProps";
import { AssaultRangesVisualizer } from './AssaultRangesVisualizer'; import { AssaultRangesVisualizer } from "./AssaultRangesVisualizer";
import { GunFlexibilityVisualizer } from './GunFlexibilityVisualizer'; import { GunFlexibilityVisualizer } from "./GunFlexibilityVisualizer";
import { Info } from './Info'; import { Info } from "./Info";
import { InfoWithDelta } from './InfoWithDelta'; import { InfoWithDelta } from "./InfoWithDelta";
import { ReloadVisualizer } from './ReloadVisualizer'; import { ReloadVisualizer } from "./ReloadVisualizer";
import { RicochetVisualizer } from './RicochetVisualizer'; import { RicochetVisualizer } from "./RicochetVisualizer";
import { StatsTableWrapper } from './StatsTableWrapper'; import { StatsTableWrapper } from "./StatsTableWrapper";
import type { StatsAcceptorProps } from './TraverseVisualizer'; import type { StatsAcceptorProps } from "./TraverseVisualizer";
export function Firepower({ export function Firepower({
stats, stats,
skeleton, skeleton,
}: StatsAcceptorProps & MaybeSkeletonComponentProps) { }: StatsAcceptorProps & MaybeSkeletonComponentProps) {
const mutateDuel = Duel.useMutation();
const { strings, unwrap } = useLocale(); const { strings, unwrap } = useLocale();
const turret = Duel.use((state) => state.protagonist.turret); const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun); const gun = Duel.use((state) => state.protagonist.gun);
@@ -41,8 +40,8 @@ export function Firepower({
return ( return (
<Tooltip content={unwrap(thisShell.name)} key={thisShell.id}> <Tooltip content={unwrap(thisShell.name)} key={thisShell.id}>
<IconButton <IconButton
color={selected ? undefined : 'gray'} color={selected ? undefined : "gray"}
variant={selected ? 'solid' : 'soft'} variant={selected ? "solid" : "soft"}
highContrast={selected} highContrast={selected}
style={{ style={{
borderTopLeftRadius: shellIndex === 0 ? undefined : 0, borderTopLeftRadius: shellIndex === 0 ? undefined : 0,
@@ -54,7 +53,7 @@ export function Firepower({
marginLeft: shellIndex === 0 ? 0 : -1, marginLeft: shellIndex === 0 ? 0 : -1,
}} }}
onClick={() => { onClick={() => {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.protagonist.shell = thisShell; draft.protagonist.shell = thisShell;
}); });
}} }}
@@ -79,7 +78,7 @@ export function Firepower({
</Info> </Info>
<InfoWithDelta stats={stats} decimals={0} value="dpm" /> <InfoWithDelta stats={stats} decimals={0} value="dpm" />
<InfoWithDelta stats={stats} decimals={0} value="damage" /> <InfoWithDelta stats={stats} decimals={0} value="damage" />
{gun.gun_type!.$case !== 'regular' && ( {gun.gun_type!.$case !== "regular" && (
<InfoWithDelta stats={stats} indent decimals={0} value="clipDamage" /> <InfoWithDelta stats={stats} indent decimals={0} value="clipDamage" />
)} )}
<InfoWithDelta stats={stats} decimals={0} value="moduleDamage" /> <InfoWithDelta stats={stats} decimals={0} value="moduleDamage" />
@@ -89,13 +88,13 @@ export function Firepower({
<AssaultRangesVisualizer stats={stats} ranges={gun.assault_ranges} /> <AssaultRangesVisualizer stats={stats} ranges={gun.assault_ranges} />
)} )}
{gun.gun_type!.$case === 'auto_reloader' && ( {gun.gun_type!.$case === "auto_reloader" && (
<InfoWithDelta stats={stats} decimals={0} indent value="dpmEffective" /> <InfoWithDelta stats={stats} decimals={0} indent value="dpmEffective" />
)} )}
{gun.gun_type!.$case !== 'regular' && ( {gun.gun_type!.$case !== "regular" && (
<InfoWithDelta stats={stats} value="shells" /> <InfoWithDelta stats={stats} value="shells" />
)} )}
{gun.gun_type!.$case === 'auto_reloader' ? ( {gun.gun_type!.$case === "auto_reloader" ? (
<> <>
<Info <Info
indent indent
@@ -120,7 +119,7 @@ export function Firepower({
name={literals( name={literals(
strings.website.tools.tankopedia.characteristics.values strings.website.tools.tankopedia.characteristics.values
.shell_index, .shell_index,
[`${index + 1}`], [`${index + 1}`]
)} )}
decimals={2} decimals={2}
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
@@ -137,8 +136,8 @@ export function Firepower({
value="shellReload" value="shellReload"
/> />
)} )}
{(gun.gun_type!.$case === 'auto_loader' || {(gun.gun_type!.$case === "auto_loader" ||
gun.gun_type!.$case === 'auto_reloader') && ( gun.gun_type!.$case === "auto_reloader") && (
<InfoWithDelta <InfoWithDelta
stats={stats} stats={stats}
indent indent
@@ -192,7 +191,7 @@ export function Firepower({
name={literals( name={literals(
strings.website.tools.tankopedia.characteristics.values strings.website.tools.tankopedia.characteristics.values
.dispersion_at_distance, .dispersion_at_distance,
['100'], ["100"]
)} )}
/> />
<InfoWithDelta <InfoWithDelta
@@ -1,22 +1,22 @@
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { Box, Card, Flex } from '@radix-ui/themes'; import { Box, Card, Flex } from "@radix-ui/themes";
import { clamp } from 'lodash-es'; import { clamp } from "lodash-es";
import { useRef, useState } from 'react'; import { useRef, useState } from "react";
import { degToRad, radToDeg } from 'three/src/math/MathUtils.js'; import { degToRad, radToDeg } from "three/src/math/MathUtils.js";
import { awaitableModelDefinitions } from '../../../../../../../core/awaitables/modelDefinitions'; import { awaitableModelDefinitions } from "../../../../../../../core/awaitables/modelDefinitions";
import { import {
DEFAULT_PITCH_TRANSITION, DEFAULT_PITCH_TRANSITION,
applyPitchYawLimits, applyPitchYawLimits,
} from '../../../../../../../core/blitz/applyPitchYawLimits'; } from "../../../../../../../core/blitz/applyPitchYawLimits";
import { hasEquipment } from '../../../../../../../core/blitzkit/hasEquipment'; import { hasEquipment } from "../../../../../../../core/blitzkit/hasEquipment";
import { modelTransformEvent } from '../../../../../../../core/blitzkit/modelTransform'; import { modelTransformEvent } from "../../../../../../../core/blitzkit/modelTransform";
import { Var } from '../../../../../../../core/radix/var'; import { Var } from "../../../../../../../core/radix/var";
import { useLocale } from '../../../../../../../hooks/useLocale'; import { useLocale } from "../../../../../../../hooks/useLocale";
import { Duel } from '../../../../../../../stores/duel'; import { Duel } from "../../../../../../../stores/duel";
import type { MaybeSkeletonComponentProps } from '../../../../../../../types/maybeSkeletonComponentProps'; import type { MaybeSkeletonComponentProps } from "../../../../../../../types/maybeSkeletonComponentProps";
import { VisualizerCard } from '../VisualizerCard'; import { VisualizerCard } from "../VisualizerCard";
import { VisualizerCornerStat } from '../VisualizerCornerStat'; import { VisualizerCornerStat } from "../VisualizerCornerStat";
import { FlexibilityCanvas } from './components/FlexibilityCanvas'; import { FlexibilityCanvas } from "./components/FlexibilityCanvas";
const ANGLE_COEFFICIENT = 1 / 10; const ANGLE_COEFFICIENT = 1 / 10;
@@ -40,7 +40,6 @@ export function GunFlexibilityVisualizer({
}: MaybeSkeletonComponentProps) { }: MaybeSkeletonComponentProps) {
const { strings } = useLocale(); const { strings } = useLocale();
const duelStore = Duel.useStore();
const tank = Duel.use((state) => state.protagonist.tank); const tank = Duel.use((state) => state.protagonist.tank);
const turret = Duel.use((state) => state.protagonist.turret); const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun); const gun = Duel.use((state) => state.protagonist.gun);
@@ -52,10 +51,10 @@ export function GunFlexibilityVisualizer({
const [yaw, setYaw] = useState(0); const [yaw, setYaw] = useState(0);
const [pitch, setPitch] = useState(0); const [pitch, setPitch] = useState(0);
const [minPitch, setMinPitch] = useState( const [minPitch, setMinPitch] = useState(
-degToRad(gunModel.pitch.front?.max ?? gunModel.pitch.max), -degToRad(gunModel.pitch.front?.max ?? gunModel.pitch.max)
); );
const [maxPitch, setMaxPitch] = useState( const [maxPitch, setMaxPitch] = useState(
-degToRad(gunModel.pitch.front?.min ?? gunModel.pitch.min), -degToRad(gunModel.pitch.front?.min ?? gunModel.pitch.min)
); );
const container = useRef<HTMLDivElement>(null); const container = useRef<HTMLDivElement>(null);
@@ -68,7 +67,7 @@ export function GunFlexibilityVisualizer({
const b = p.back; const b = p.back;
const t = gunModel.pitch.transition ?? DEFAULT_PITCH_TRANSITION; const t = gunModel.pitch.transition ?? DEFAULT_PITCH_TRANSITION;
let d = ''; let d = "";
// right // right
const mo = mag(p.max); const mo = mag(p.max);
@@ -169,10 +168,10 @@ export function GunFlexibilityVisualizer({
return ( return (
<Flex direction="column-reverse" gap="0"> <Flex direction="column-reverse" gap="0">
<Box mt="-3" px="2"> <Box mt="-3" px="2">
<Card variant="classic" style={{ height: '10rem' }}> <Card variant="classic" style={{ height: "10rem" }}>
<Box <Box
style={{ style={{
background: 'linear-gradient(var(--gray-1), var(--gray-3))', background: "linear-gradient(var(--gray-1), var(--gray-3))",
}} }}
position="absolute" position="absolute"
width="100%" width="100%"
@@ -195,8 +194,8 @@ export function GunFlexibilityVisualizer({
transparent, transparent,
transparent ${Math.PI / 2 - maxPitch}rad, transparent ${Math.PI / 2 - maxPitch}rad,
${Var('cyan-a4')} ${Math.PI / 2 - maxPitch}rad, ${Var("cyan-a4")} ${Math.PI / 2 - maxPitch}rad,
${Var('jade-a4')} ${Math.PI / 2 - minPitch}rad, ${Var("jade-a4")} ${Math.PI / 2 - minPitch}rad,
transparent ${Math.PI / 2 - minPitch}rad, transparent ${Math.PI / 2 - minPitch}rad,
transparent 180deg transparent 180deg
@@ -209,7 +208,7 @@ export function GunFlexibilityVisualizer({
<VisualizerCard <VisualizerCard
ref={container} ref={container}
style={{ touchAction: 'none' }} style={{ touchAction: "none" }}
onPointerMove={(event) => { onPointerMove={(event) => {
event.preventDefault(); event.preventDefault();
@@ -221,19 +220,15 @@ export function GunFlexibilityVisualizer({
const v = event.clientY - rect.top - rect.height / 2; const v = event.clientY - rect.top - rect.height / 2;
let yaw = Math.atan2(u, -v); let yaw = Math.atan2(u, -v);
const {
equipmentMatrix,
tank: { equipment_preset },
} = duelStore.getState().protagonist;
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
equipment_preset, Duel.state.protagonist.tank.equipment_preset,
equipmentMatrix, Duel.state.protagonist.equipmentMatrix
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
equipment_preset, Duel.state.protagonist.tank.equipment_preset,
equipmentMatrix, Duel.state.protagonist.equipmentMatrix
); );
const min = applyPitchYawLimits( const min = applyPitchYawLimits(
@@ -242,7 +237,7 @@ export function GunFlexibilityVisualizer({
p, p,
y, y,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer
); );
const max = applyPitchYawLimits( const max = applyPitchYawLimits(
Math.PI, Math.PI,
@@ -250,7 +245,7 @@ export function GunFlexibilityVisualizer({
p, p,
y, y,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer
); );
yaw = min[1]; yaw = min[1];
@@ -276,8 +271,8 @@ export function GunFlexibilityVisualizer({
width="100%" width="100%"
height="100%" height="100%"
style={{ style={{
borderRadius: Var('radius-2'), borderRadius: Var("radius-2"),
overflow: 'hidden', overflow: "hidden",
}} }}
> >
<Box <Box
@@ -286,18 +281,18 @@ export function GunFlexibilityVisualizer({
style={{ style={{
opacity: 2 ** -3, opacity: 2 ** -3,
backgroundImage: `url(/assets/images/tankopedia/visualizers/ricochet/armor-hash.png)`, backgroundImage: `url(/assets/images/tankopedia/visualizers/ricochet/armor-hash.png)`,
boxShadow: 'inset 0 0 4rem 1rem black', boxShadow: "inset 0 0 4rem 1rem black",
}} }}
/> />
</Box> </Box>
<svg <svg
style={{ style={{
position: 'absolute', position: "absolute",
top: 0, top: 0,
left: 0, left: 0,
width: 'calc(100%)', width: "calc(100%)",
height: 'calc(100%)', height: "calc(100%)",
}} }}
viewBox="-1 -1 2 2" viewBox="-1 -1 2 2"
> >
@@ -340,11 +335,11 @@ export function GunFlexibilityVisualizer({
width="1pt" width="1pt"
height="14rem" height="14rem"
style={{ style={{
borderRadius: Var('radius-1'), borderRadius: Var("radius-1"),
background: `linear-gradient(${Var('gray-a11')}, ${Var('gray-a6')})`, background: `linear-gradient(${Var("gray-a11")}, ${Var("gray-a6")})`,
boxShadow: boxShadow:
'var(--white-a12) -0.25rem 0 1rem, var(--black-a12) 0.25rem 0 1rem', "var(--white-a12) -0.25rem 0 1rem, var(--black-a12) 0.25rem 0 1rem",
transform: 'translateX(-50%)', transform: "translateX(-50%)",
}} }}
/> />
</Box> </Box>
@@ -356,9 +351,9 @@ export function GunFlexibilityVisualizer({
width="0.25rem" width="0.25rem"
height="0.25rem" height="0.25rem"
style={{ style={{
borderRadius: '50%', borderRadius: "50%",
backgroundColor: Var('gray-12'), backgroundColor: Var("gray-12"),
transform: 'translate(-50%, 50%)', transform: "translate(-50%, 50%)",
}} }}
/> />
@@ -1,12 +1,12 @@
import { asset } from '@blitzkit/core'; import { asset } from "@blitzkit/core";
import { Flex, IconButton, Tooltip } from '@radix-ui/themes'; import { Flex, IconButton, Tooltip } from "@radix-ui/themes";
import { useLocale } from '../../../../../../../../hooks/useLocale'; import { useLocale } from "../../../../../../../../hooks/useLocale";
import { Duel } from '../../../../../../../../stores/duel'; import { Duel } from "../../../../../../../../stores/duel";
export function ShellSwitcher() { export function ShellSwitcher() {
const gun = Duel.use((state) => state.protagonist.gun); const gun = Duel.use((state) => state.protagonist.gun);
const shell = Duel.use((state) => state.protagonist.shell); const shell = Duel.use((state) => state.protagonist.shell);
const mutateDuel = Duel.useMutation();
const { unwrap } = useLocale(); const { unwrap } = useLocale();
return ( return (
@@ -18,26 +18,26 @@ export function ShellSwitcher() {
<Tooltip content={unwrap(thisShell.name)} key={thisShell.id}> <Tooltip content={unwrap(thisShell.name)} key={thisShell.id}>
<IconButton <IconButton
size="3" size="3"
color={selected ? undefined : 'gray'} color={selected ? undefined : "gray"}
variant={selected ? 'solid' : 'soft'} variant={selected ? "solid" : "soft"}
highContrast={selected} highContrast={selected}
style={{ style={{
borderTopLeftRadius: 0, borderTopLeftRadius: 0,
borderBottomLeftRadius: shellIndex === 0 ? '50%' : 0, borderBottomLeftRadius: shellIndex === 0 ? "50%" : 0,
borderTopRightRadius: 0, borderTopRightRadius: 0,
borderBottomRightRadius: borderBottomRightRadius:
shellIndex === gun.shells.length - 1 ? '50%' : 0, shellIndex === gun.shells.length - 1 ? "50%" : 0,
marginLeft: shellIndex === 0 ? 0 : -1, marginLeft: shellIndex === 0 ? 0 : -1,
}} }}
onClick={() => { onClick={() => {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.protagonist.shell = thisShell; draft.protagonist.shell = thisShell;
}); });
}} }}
> >
<img <img
alt={unwrap(thisShell.name)} alt={unwrap(thisShell.name)}
style={{ width: '1.125rem', height: '1.125rem' }} style={{ width: "1.125rem", height: "1.125rem" }}
src={asset(`icons/shells/${thisShell.icon}.webp`)} src={asset(`icons/shells/${thisShell.icon}.webp`)}
/> />
</IconButton> </IconButton>
@@ -1,5 +1,5 @@
import { checkConsumableProvisionInclusivity } from '@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity'; import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
import { InfoCircledIcon } from '@radix-ui/react-icons'; import { InfoCircledIcon } from "@radix-ui/react-icons";
import { import {
Button, Button,
Flex, Flex,
@@ -7,31 +7,30 @@ import {
IconButton, IconButton,
Popover, Popover,
Text, Text,
} from '@radix-ui/themes'; } from "@radix-ui/themes";
import { awaitableConsumableDefinitions } from '../../../../core/awaitables/consumableDefinitions'; import { awaitableConsumableDefinitions } from "../../../../core/awaitables/consumableDefinitions";
import { useEquipment } from '../../../../hooks/useEquipment'; import { useEquipment } from "../../../../hooks/useEquipment";
import { useLocale } from '../../../../hooks/useLocale'; import { useLocale } from "../../../../hooks/useLocale";
import { Duel } from '../../../../stores/duel'; import { Duel } from "../../../../stores/duel";
import { ConsumablesManager } from '../../../ConsumablesManager'; import { ConsumablesManager } from "../../../ConsumablesManager";
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper'; import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
const consumableDefinitions = await awaitableConsumableDefinitions; const consumableDefinitions = await awaitableConsumableDefinitions;
export function Consumables() { export function Consumables() {
const mutateDuel = Duel.useMutation();
const protagonist = Duel.use((state) => state.protagonist); const protagonist = Duel.use((state) => state.protagonist);
const consumables = Duel.use((state) => state.protagonist.consumables); const consumables = Duel.use((state) => state.protagonist.consumables);
const consumablesList = Object.values( const consumablesList = Object.values(
consumableDefinitions.consumables, consumableDefinitions.consumables
).filter((consumable) => ).filter((consumable) =>
checkConsumableProvisionInclusivity( checkConsumableProvisionInclusivity(
consumable, consumable,
protagonist.tank, protagonist.tank,
protagonist.gun, protagonist.gun
), )
); );
const cooldownBooster = Duel.use( const cooldownBooster = Duel.use(
(state) => state.protagonist.cooldownBooster, (state) => state.protagonist.cooldownBooster
); );
const hasConsumableDeliverySystem = useEquipment(118); const hasConsumableDeliverySystem = useEquipment(118);
const hasHighEndConsumables = useEquipment(101); const hasHighEndConsumables = useEquipment(101);
@@ -67,7 +66,7 @@ export function Consumables() {
variant="ghost" variant="ghost"
color="red" color="red"
onClick={() => { onClick={() => {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.protagonist.consumables = []; draft.protagonist.consumables = [];
}); });
}} }}
@@ -85,11 +84,11 @@ export function Consumables() {
hasHighEndConsumables={hasHighEndConsumables} hasHighEndConsumables={hasHighEndConsumables}
timers timers
onConsumablesChange={(consumables) => { onConsumablesChange={(consumables) => {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.protagonist.consumables = consumables; draft.protagonist.consumables = consumables;
}); });
}} }}
justify={{ initial: 'center', sm: 'start' }} justify={{ initial: "center", sm: "start" }}
/> />
</ConfigurationChildWrapper> </ConfigurationChildWrapper>
); );
@@ -1,19 +1,18 @@
import { Button, Flex, Heading } from '@radix-ui/themes'; import { Button, Flex, Heading } from "@radix-ui/themes";
import { awaitableEquipmentDefinitions } from '../../../../core/awaitables/equipmentDefinitions'; import { awaitableEquipmentDefinitions } from "../../../../core/awaitables/equipmentDefinitions";
import { useLocale } from '../../../../hooks/useLocale'; import { useLocale } from "../../../../hooks/useLocale";
import { Duel } from '../../../../stores/duel'; import { Duel } from "../../../../stores/duel";
import { EquipmentManager } from '../../../EquipmentManager'; import { EquipmentManager } from "../../../EquipmentManager";
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper'; import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
const equipmentDefinitions = await awaitableEquipmentDefinitions; const equipmentDefinitions = await awaitableEquipmentDefinitions;
export function Equipment() { export function Equipment() {
const mutateDuel = Duel.useMutation();
const protagonist = Duel.use((state) => state.protagonist); const protagonist = Duel.use((state) => state.protagonist);
const equipmentPreset = const equipmentPreset =
equipmentDefinitions.presets[protagonist.tank.equipment_preset]; equipmentDefinitions.presets[protagonist.tank.equipment_preset];
const equipmentMatrix = Duel.use( const equipmentMatrix = Duel.use(
(state) => state.protagonist.equipmentMatrix, (state) => state.protagonist.equipmentMatrix
); );
const { strings } = useLocale(); const { strings } = useLocale();
@@ -27,7 +26,7 @@ export function Equipment() {
variant="ghost" variant="ghost"
color="red" color="red"
onClick={() => { onClick={() => {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.protagonist.equipmentMatrix.forEach((row) => { draft.protagonist.equipmentMatrix.forEach((row) => {
row.forEach((_, index) => { row.forEach((_, index) => {
row[index] = 0; row[index] = 0;
@@ -44,7 +43,7 @@ export function Equipment() {
matrix={equipmentMatrix} matrix={equipmentMatrix}
preset={equipmentPreset} preset={equipmentPreset}
onChange={(matrix) => { onChange={(matrix) => {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.protagonist.equipmentMatrix = matrix; draft.protagonist.equipmentMatrix = matrix;
}); });
}} }}
@@ -1,15 +1,14 @@
import { asset } from '@blitzkit/core'; import { asset } from "@blitzkit/core";
import { Flex, Heading } from '@radix-ui/themes'; import { Flex, Heading } from "@radix-ui/themes";
import { useLocale } from '../../../../hooks/useLocale'; import { useLocale } from "../../../../hooks/useLocale";
import { Duel } from '../../../../stores/duel'; import { Duel } from "../../../../stores/duel";
import { GenericTankComponentButton } from '../../../ModuleButtons/GenericTankComponentButton'; import { GenericTankComponentButton } from "../../../ModuleButtons/GenericTankComponentButton";
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper'; import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
export function Miscellaneous() { export function Miscellaneous() {
const mutateDuel = Duel.useMutation();
const camouflage = Duel.use((state) => state.protagonist.camouflage); const camouflage = Duel.use((state) => state.protagonist.camouflage);
const cooldownBooster = Duel.use( const cooldownBooster = Duel.use(
(state) => state.protagonist.cooldownBooster, (state) => state.protagonist.cooldownBooster
); );
const { tank } = Duel.use((state) => state.protagonist); const { tank } = Duel.use((state) => state.protagonist);
const { strings } = useLocale(); const { strings } = useLocale();
@@ -23,37 +22,37 @@ export function Miscellaneous() {
<Flex gap="2" align="center"> <Flex gap="2" align="center">
{!tank.fixed_camouflage && ( {!tank.fixed_camouflage && (
<GenericTankComponentButton <GenericTankComponentButton
icon={asset('icons/camo.webp')} icon={asset("icons/camo.webp")}
selected={camouflage} selected={camouflage}
iconStyles={{ iconStyles={{
top: '50%', top: "50%",
left: '50%', left: "50%",
transform: 'translate(calc(-50% + 4px), calc(-50% + 4px))', transform: "translate(calc(-50% + 4px), calc(-50% + 4px))",
}} }}
onClick={() => { onClick={() => {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.protagonist.camouflage = !camouflage; draft.protagonist.camouflage = !camouflage;
}); });
}} }}
/> />
)} )}
<GenericTankComponentButton <GenericTankComponentButton
icon={asset('icons/boosters/equipment.webp')} icon={asset("icons/boosters/equipment.webp")}
selected={cooldownBooster > 0} selected={cooldownBooster > 0}
banner={ banner={
cooldownBooster === 0 cooldownBooster === 0
? undefined ? undefined
: cooldownBooster === 1 : cooldownBooster === 1
? '#afb0abc0' ? "#afb0abc0"
: cooldownBooster === 2 : cooldownBooster === 2
? '#5f72cbc0' ? "#5f72cbc0"
: '#9b3cc0c0' : "#9b3cc0c0"
} }
iconStyles={{ iconStyles={{
transform: 'translate(-50%, -50%) scale(0.8)', transform: "translate(-50%, -50%) scale(0.8)",
}} }}
onClick={() => { onClick={() => {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.protagonist.cooldownBooster++; draft.protagonist.cooldownBooster++;
if (draft.protagonist.cooldownBooster === 4) if (draft.protagonist.cooldownBooster === 4)
draft.protagonist.cooldownBooster = 0; draft.protagonist.cooldownBooster = 0;
@@ -10,7 +10,7 @@ import {
TurretDefinition, TurretDefinition,
type TankDefinition, type TankDefinition,
type Unlock, type Unlock,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { import {
Button, Button,
ChevronDownIcon, ChevronDownIcon,
@@ -18,12 +18,12 @@ import {
Heading, Heading,
IconButton, IconButton,
Text, Text,
} from '@radix-ui/themes'; } from "@radix-ui/themes";
import { awaitableTankDefinitions } from '../../../../core/awaitables/tankDefinitions'; import { awaitableTankDefinitions } from "../../../../core/awaitables/tankDefinitions";
import { useLocale } from '../../../../hooks/useLocale'; import { useLocale } from "../../../../hooks/useLocale";
import { Duel } from '../../../../stores/duel'; import { Duel } from "../../../../stores/duel";
import { LinkI18n } from '../../../LinkI18n'; import { LinkI18n } from "../../../LinkI18n";
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper'; import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
type ModuleDefinition = type ModuleDefinition =
| TurretDefinition | TurretDefinition
@@ -32,7 +32,7 @@ type ModuleDefinition =
| EngineDefinition; | EngineDefinition;
function extractModule(raw: ModuleDefinition | TankDefinition) { function extractModule(raw: ModuleDefinition | TankDefinition) {
if ('gun_type' in raw) { if ("gun_type" in raw) {
return raw; return raw;
} else } else
return raw as Exclude<ModuleDefinition | TankDefinition, GunDefinition>; return raw as Exclude<ModuleDefinition | TankDefinition, GunDefinition>;
@@ -43,11 +43,11 @@ interface ModuleWithUnlocks {
} }
const moduleTypeTypeIconName: Record<ModuleType, string> = { const moduleTypeTypeIconName: Record<ModuleType, string> = {
[ModuleType.VEHICLE]: '', [ModuleType.VEHICLE]: "",
[ModuleType.ENGINE]: 'engine', [ModuleType.ENGINE]: "engine",
[ModuleType.TRACKS]: 'chassis', [ModuleType.TRACKS]: "chassis",
[ModuleType.TURRET]: 'turret', [ModuleType.TURRET]: "turret",
[ModuleType.GUN]: 'gun', [ModuleType.GUN]: "gun",
}; };
function ModuleButton({ function ModuleButton({
@@ -70,11 +70,11 @@ function ModuleButton({
<IconButton <IconButton
size="4" size="4"
radius="small" radius="small"
variant={selected ? 'surface' : 'soft'} variant={selected ? "surface" : "soft"}
color={top ? 'amber' : selected ? undefined : 'gray'} color={top ? "amber" : selected ? undefined : "gray"}
onClick={onClick} onClick={onClick}
style={{ style={{
position: 'relative', position: "relative",
width: isTank ? 72 : undefined, width: isTank ? 72 : undefined,
height: isTank ? 72 : undefined, height: isTank ? 72 : undefined,
}} }}
@@ -83,7 +83,7 @@ function ModuleButton({
size="1" size="1"
color="gray" color="gray"
style={{ style={{
position: 'absolute', position: "absolute",
left: isTank ? 8 : 4, left: isTank ? 8 : 4,
top: isTank ? 8 : 4, top: isTank ? 8 : 4,
}} }}
@@ -94,13 +94,13 @@ function ModuleButton({
{unlock.cost.value > 0 && ( {unlock.cost.value > 0 && (
<Flex <Flex
style={{ style={{
position: 'absolute', position: "absolute",
left: '50%', left: "50%",
bottom: 4, bottom: 4,
transform: 'translateX(-50%)', transform: "translateX(-50%)",
}} }}
align="center" align="center"
gap={isTank ? '1' : undefined} gap={isTank ? "1" : undefined}
> >
<Text size="1" color="gray"> <Text size="1" color="gray">
{formatCompact(locale, unlock.cost.value)} {formatCompact(locale, unlock.cost.value)}
@@ -112,7 +112,7 @@ function ModuleButton({
style={{ style={{
width: 12, width: 12,
height: 12, height: 12,
objectFit: 'contain', objectFit: "contain",
}} }}
/> />
</Flex> </Flex>
@@ -128,7 +128,7 @@ function ModuleButton({
style={{ style={{
width: isTank ? 64 : 32, width: isTank ? 64 : 32,
height: isTank ? 64 : 32, height: isTank ? 64 : 32,
objectFit: 'contain', objectFit: "contain",
}} }}
/> />
</IconButton> </IconButton>
@@ -149,7 +149,6 @@ function ModuleButton({
const tankDefinitions = await awaitableTankDefinitions; const tankDefinitions = await awaitableTankDefinitions;
export function Modules() { export function Modules() {
const mutateDuel = Duel.useMutation();
const tank = Duel.use((state) => state.protagonist.tank); const tank = Duel.use((state) => state.protagonist.tank);
const hasUpgrades = const hasUpgrades =
tank.turrets.length > 1 || tank.turrets.length > 1 ||
@@ -171,15 +170,15 @@ export function Modules() {
const { strings } = useLocale(); const { strings } = useLocale();
function setByUnlock(unlock: Unlock) { function setByUnlock(unlock: Unlock) {
mutateDuel((draft) => { Duel.mutate((draft) => {
if (unlock.type === ModuleType.TURRET) { if (unlock.type === ModuleType.TURRET) {
draft.protagonist.turret = draft.protagonist.tank.turrets.find( draft.protagonist.turret = draft.protagonist.tank.turrets.find(
(turret) => turret.id === unlock.id, (turret) => turret.id === unlock.id
)!; )!;
if ( if (
!draft.protagonist.turret.guns.some( !draft.protagonist.turret.guns.some(
(gun) => gun.id === draft.protagonist.gun.id, (gun) => gun.id === draft.protagonist.gun.id
) )
) { ) {
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!; draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
@@ -187,7 +186,7 @@ export function Modules() {
} }
} else if (unlock.type === ModuleType.GUN) { } else if (unlock.type === ModuleType.GUN) {
const gunInTurret = draft.protagonist.turret.guns.find( const gunInTurret = draft.protagonist.turret.guns.find(
(gun) => gun.id === unlock.id, (gun) => gun.id === unlock.id
); );
if (gunInTurret) { if (gunInTurret) {
draft.protagonist.gun = gunInTurret; draft.protagonist.gun = gunInTurret;
@@ -195,10 +194,10 @@ export function Modules() {
} else { } else {
// TODO: warn somehow? // TODO: warn somehow?
const suitableTurret = draft.protagonist.tank.turrets.find((turret) => const suitableTurret = draft.protagonist.tank.turrets.find((turret) =>
turret.guns.some((gun) => gun.id === unlock.id), turret.guns.some((gun) => gun.id === unlock.id)
)!; )!;
const gunInSuitableTurret = suitableTurret.guns.find( const gunInSuitableTurret = suitableTurret.guns.find(
(gun) => gun.id === unlock.id, (gun) => gun.id === unlock.id
)!; )!;
draft.protagonist.turret = suitableTurret; draft.protagonist.turret = suitableTurret;
@@ -207,11 +206,11 @@ export function Modules() {
} }
} else if (unlock.type === ModuleType.ENGINE) { } else if (unlock.type === ModuleType.ENGINE) {
draft.protagonist.engine = draft.protagonist.tank.engines.find( draft.protagonist.engine = draft.protagonist.tank.engines.find(
(engine) => engine.id === unlock.id, (engine) => engine.id === unlock.id
)!; )!;
} else if (unlock.type === ModuleType.TRACKS) { } else if (unlock.type === ModuleType.TRACKS) {
draft.protagonist.track = draft.protagonist.tank.tracks.find( draft.protagonist.track = draft.protagonist.tank.tracks.find(
(track) => track.id === unlock.id, (track) => track.id === unlock.id
)!; )!;
} }
}); });
@@ -235,7 +234,7 @@ export function Modules() {
} else if (unlock.type === ModuleType.GUN) { } else if (unlock.type === ModuleType.GUN) {
module = tank.turrets module = tank.turrets
.find((turret) => .find((turret) =>
turret.guns.some((gun) => gun.id === unlock.id), turret.guns.some((gun) => gun.id === unlock.id)
)! )!
.guns.find((gun) => gun.id === unlock.id)!; .guns.find((gun) => gun.id === unlock.id)!;
} else if (unlock.type === ModuleType.VEHICLE) { } else if (unlock.type === ModuleType.VEHICLE) {
@@ -257,12 +256,12 @@ export function Modules() {
{unlocks.length > 1 && ( {unlocks.length > 1 && (
<div <div
style={{ style={{
backgroundColor: 'currentcolor', backgroundColor: "currentcolor",
width: `calc(${central ? 100 : 50}% + ${ width: `calc(${central ? 100 : 50}% + ${
central ? 8 : 4 central ? 8 : 4
}px)`, }px)`,
height: 1, height: 1,
position: 'absolute', position: "absolute",
right: first ? 0 : undefined, right: first ? 0 : undefined,
left: last ? 0 : undefined, left: last ? 0 : undefined,
transform: `translateX(${first ? 4 : last ? -4 : 0}px)`, transform: `translateX(${first ? 4 : last ? -4 : 0}px)`,
@@ -279,14 +278,14 @@ export function Modules() {
> >
<div <div
style={{ style={{
backgroundColor: 'currentcolor', backgroundColor: "currentcolor",
width: 1, width: 1,
height: 8, height: 8,
}} }}
/> />
<ChevronDownIcon <ChevronDownIcon
style={{ style={{
transform: 'translateY(-7px)', transform: "translateY(-7px)",
}} }}
/> />
</Flex> </Flex>
@@ -326,7 +325,7 @@ export function Modules() {
1 && ( 1 && (
<div <div
style={{ style={{
backgroundColor: 'currentcolor', backgroundColor: "currentcolor",
width: 1, width: 1,
height: 8, height: 8,
marginBottom: -8, marginBottom: -8,
@@ -337,7 +336,7 @@ export function Modules() {
{(extractModule(module) as ModuleWithUnlocks).unlocks && {(extractModule(module) as ModuleWithUnlocks).unlocks &&
tree( tree(
type, type,
(extractModule(module) as ModuleWithUnlocks).unlocks!, (extractModule(module) as ModuleWithUnlocks).unlocks!
)} )}
</Flex> </Flex>
); );
@@ -359,7 +358,7 @@ export function Modules() {
variant="ghost" variant="ghost"
color="red" color="red"
onClick={() => { onClick={() => {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.protagonist.turret = draft.protagonist.tank.turrets[0]; draft.protagonist.turret = draft.protagonist.tank.turrets[0];
draft.protagonist.gun = draft.protagonist.turret.guns[0]; draft.protagonist.gun = draft.protagonist.turret.guns[0];
draft.protagonist.shell = draft.protagonist.gun.shells[0]; draft.protagonist.shell = draft.protagonist.gun.shells[0];
@@ -373,7 +372,7 @@ export function Modules() {
<Button <Button
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.protagonist.turret = draft.protagonist.turret =
draft.protagonist.tank.turrets.at(-1)!; draft.protagonist.tank.turrets.at(-1)!;
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!; draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
@@ -394,28 +393,28 @@ export function Modules() {
<Flex gap="2" wrap="wrap" gapY="6"> <Flex gap="2" wrap="wrap" gapY="6">
{tree(ModuleType.ENGINE, [ {tree(ModuleType.ENGINE, [
{ {
cost: { type: 'xp', value: 0 }, cost: { type: "xp", value: 0 },
id: engine0.id, id: engine0.id,
type: ModuleType.ENGINE, type: ModuleType.ENGINE,
}, },
])} ])}
{tree(ModuleType.TRACKS, [ {tree(ModuleType.TRACKS, [
{ {
cost: { type: 'xp', value: 0 }, cost: { type: "xp", value: 0 },
id: track0.id, id: track0.id,
type: ModuleType.TRACKS, type: ModuleType.TRACKS,
}, },
])} ])}
{tree(ModuleType.TURRET, [ {tree(ModuleType.TURRET, [
{ {
cost: { type: 'xp', value: 0 }, cost: { type: "xp", value: 0 },
id: turret0.id, id: turret0.id,
type: ModuleType.TURRET, type: ModuleType.TURRET,
}, },
])} ])}
{tree(ModuleType.GUN, [ {tree(ModuleType.GUN, [
{ {
cost: { type: 'xp', value: 0 }, cost: { type: "xp", value: 0 },
id: gun0.id, id: gun0.id,
type: ModuleType.GUN, type: ModuleType.GUN,
}, },
@@ -1,15 +1,14 @@
import { availableProvisions } from '@blitzkit/core'; import { availableProvisions } from "@blitzkit/core";
import { Button, Flex, Heading } from '@radix-ui/themes'; import { Button, Flex, Heading } from "@radix-ui/themes";
import { awaitableProvisionDefinitions } from '../../../../core/awaitables/provisionDefinitions'; import { awaitableProvisionDefinitions } from "../../../../core/awaitables/provisionDefinitions";
import { useLocale } from '../../../../hooks/useLocale'; import { useLocale } from "../../../../hooks/useLocale";
import { Duel } from '../../../../stores/duel'; import { Duel } from "../../../../stores/duel";
import { ProvisionsManager } from '../../../ProvisionsManager'; import { ProvisionsManager } from "../../../ProvisionsManager";
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper'; import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
const provisionDefinitions = await awaitableProvisionDefinitions; const provisionDefinitions = await awaitableProvisionDefinitions;
export function Provisions() { export function Provisions() {
const mutateDuel = Duel.useMutation();
const { tank, gun } = Duel.use((state) => state.protagonist); const { tank, gun } = Duel.use((state) => state.protagonist);
const provisions = Duel.use((state) => state.protagonist.provisions); const provisions = Duel.use((state) => state.protagonist.provisions);
const provisionsList = availableProvisions(tank, gun, provisionDefinitions); const provisionsList = availableProvisions(tank, gun, provisionDefinitions);
@@ -25,7 +24,7 @@ export function Provisions() {
variant="ghost" variant="ghost"
color="red" color="red"
onClick={() => { onClick={() => {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.protagonist.provisions = []; draft.protagonist.provisions = [];
}); });
}} }}
@@ -39,7 +38,7 @@ export function Provisions() {
selected={provisions} selected={provisions}
disabled={tank.max_provisions === provisions.length} disabled={tank.max_provisions === provisions.length}
onChange={(provisions) => { onChange={(provisions) => {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.protagonist.provisions = provisions; draft.protagonist.provisions = provisions;
}); });
}} }}
@@ -6,7 +6,6 @@ import { Tankopedia } from "../../../../../../stores/tankopedia";
export function DynamicArmorSwitcher() { export function DynamicArmorSwitcher() {
const tank = Duel.use((state) => state.protagonist.tank); const tank = Duel.use((state) => state.protagonist.tank);
const mutateDuel = Duel.useMutation();
const isDynamicArmorActive = Duel.use((state) => const isDynamicArmorActive = Duel.use((state) =>
state.protagonist.consumables.includes(73) state.protagonist.consumables.includes(73)
); );
@@ -20,7 +19,7 @@ export function DynamicArmorSwitcher() {
variant="soft" variant="soft"
size={{ initial: "2", sm: "3" }} size={{ initial: "2", sm: "3" }}
onClick={() => { onClick={() => {
mutateDuel((draft) => { Duel.mutate((draft) => {
if (draft.protagonist.consumables.includes(73)) { if (draft.protagonist.consumables.includes(73)) {
draft.protagonist.consumables = draft.protagonist.consumables =
draft.protagonist.consumables.filter((id) => id !== 73); draft.protagonist.consumables.filter((id) => id !== 73);
@@ -25,7 +25,6 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
const showPrimaryArmor = TankopediaPersistent.use( const showPrimaryArmor = TankopediaPersistent.use(
(state) => state.showPrimaryArmor (state) => state.showPrimaryArmor
); );
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
const editStatic = Tankopedia.use((state) => state.editStatic); const editStatic = Tankopedia.use((state) => state.editStatic);
const { strings } = useLocale(); const { strings } = useLocale();
@@ -50,7 +49,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
gap="2" gap="2"
style={{ opacity: showPrimaryArmor ? 1 : 0.5, cursor: "pointer" }} style={{ opacity: showPrimaryArmor ? 1 : 0.5, cursor: "pointer" }}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { TankopediaPersistent.mutate((draft) => {
draft.showPrimaryArmor = !draft.showPrimaryArmor; draft.showPrimaryArmor = !draft.showPrimaryArmor;
}); });
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
@@ -91,7 +90,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
gap="2" gap="2"
style={{ opacity: showSpacedArmor ? 1 : 0.5, cursor: "pointer" }} style={{ opacity: showSpacedArmor ? 1 : 0.5, cursor: "pointer" }}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { TankopediaPersistent.mutate((draft) => {
draft.showSpacedArmor = !draft.showSpacedArmor; draft.showSpacedArmor = !draft.showSpacedArmor;
}); });
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
@@ -130,7 +129,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
gap="2" gap="2"
style={{ opacity: showExternalModules ? 1 : 0.5, cursor: "pointer" }} style={{ opacity: showExternalModules ? 1 : 0.5, cursor: "pointer" }}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { TankopediaPersistent.mutate((draft) => {
draft.showExternalModules = !draft.showExternalModules; draft.showExternalModules = !draft.showExternalModules;
}); });
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
@@ -69,10 +69,8 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const antagonistTank = Duel.use((state) => state.antagonist.tank); const antagonistTank = Duel.use((state) => state.antagonist.tank);
const hasCalibratedShells = useEquipment(103, true); const hasCalibratedShells = useEquipment(103, true);
const [tab, setTab] = useState("search"); const [tab, setTab] = useState("search");
const mutateDuel = Duel.useMutation();
const hasEnhancedArmor = useEquipment(110); const hasEnhancedArmor = useEquipment(110);
const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets); const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets);
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
const { strings, unwrap } = useLocale(); const { strings, unwrap } = useLocale();
const revealed = Tankopedia.use((state) => state.revealed); const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = Tankopedia.use((state) => state.disturbed); const disturbed = Tankopedia.use((state) => state.disturbed);
@@ -151,7 +149,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
module="gun" module="gun"
key={gun.id} key={gun.id}
onClick={() => onClick={() =>
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.antagonist.turret = turret; draft.antagonist.turret = turret;
draft.antagonist.gun = gun; draft.antagonist.gun = gun;
draft.antagonist.shell = gun.shells[0]; draft.antagonist.shell = gun.shells[0];
@@ -192,7 +190,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
size={{ initial: "2", sm: "3" }} size={{ initial: "2", sm: "3" }}
radius="none" radius="none"
onClick={() => { onClick={() => {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.antagonist.shell = thisShell; draft.antagonist.shell = thisShell;
}); });
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
@@ -229,7 +227,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
size={{ initial: "2", sm: "3" }} size={{ initial: "2", sm: "3" }}
radius="none" radius="none"
onClick={() => { onClick={() => {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.antagonist.equipmentMatrix[0][0] = hasCalibratedShells draft.antagonist.equipmentMatrix[0][0] = hasCalibratedShells
? 0 ? 0
: 1; : 1;
@@ -254,7 +252,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
size={{ initial: "2", sm: "3" }} size={{ initial: "2", sm: "3" }}
radius="none" radius="none"
onClick={() => { onClick={() => {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.protagonist.equipmentMatrix[1][1] = hasEnhancedArmor draft.protagonist.equipmentMatrix[1][1] = hasEnhancedArmor
? 0 ? 0
: -1; : -1;
@@ -303,7 +301,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
mb="1" mb="1"
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { TankopediaPersistent.mutate((draft) => {
draft.advancedHighlighting = !draft.advancedHighlighting; draft.advancedHighlighting = !draft.advancedHighlighting;
}); });
}} }}
@@ -343,7 +341,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<TankSearch <TankSearch
compact compact
onSelect={(tank) => { onSelect={(tank) => {
mutateDuel((draft) => { Duel.mutate((draft) => {
draft.antagonist.tank = tank; draft.antagonist.tank = tank;
draft.antagonist.engine = tank.engines.at(-1)!; draft.antagonist.engine = tank.engines.at(-1)!;
draft.antagonist.track = tank.tracks.at(-1)!; draft.antagonist.track = tank.tracks.at(-1)!;
@@ -32,7 +32,6 @@ export function Controls({
zoomable = true, zoomable = true,
enableRotate = true, enableRotate = true,
}: ControlsProps) { }: ControlsProps) {
const duelStore = Duel.useStore();
const camera = useThree((state) => state.camera as PerspectiveCamera); const camera = useThree((state) => state.camera as PerspectiveCamera);
const canvas = useThree((state) => state.gl.domElement); const canvas = useThree((state) => state.gl.domElement);
const orbitControls = useRef<OrbitControlsClass>(null); const orbitControls = useRef<OrbitControlsClass>(null);
@@ -108,16 +107,15 @@ export function Controls({
); );
function handlePoseEvent(event: Pose) { function handlePoseEvent(event: Pose) {
const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix
); );
switch (event) { switch (event) {
@@ -14,7 +14,6 @@ import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent
import { ModelTankWrapper } from "../../../../../Armor/components/ModelTankWrapper"; import { ModelTankWrapper } from "../../../../../Armor/components/ModelTankWrapper";
export function TankModel() { export function TankModel() {
const duelStore = Duel.useStore();
const protagonist = Duel.use((draft) => draft.protagonist); const protagonist = Duel.use((draft) => draft.protagonist);
const track = Duel.use((state) => state.protagonist.track); const track = Duel.use((state) => state.protagonist.track);
const turret = Duel.use((state) => state.protagonist.turret); const turret = Duel.use((state) => state.protagonist.turret);
@@ -135,16 +134,15 @@ export function TankModel() {
window.addEventListener("pointerup", handlePointerUp); window.addEventListener("pointerup", handlePointerUp);
} }
function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix
); );
const boundingRect = canvas.getBoundingClientRect(); const boundingRect = canvas.getBoundingClientRect();
@@ -223,16 +221,15 @@ export function TankModel() {
window.addEventListener("pointerup", handlePointerUp); window.addEventListener("pointerup", handlePointerUp);
} }
function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix
); );
const boundingRect = canvas.getBoundingClientRect(); const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position); delta.set(event.clientX, event.clientY).sub(position);
@@ -36,8 +36,6 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
return { value } satisfies ThicknessRange; return { value } satisfies ThicknessRange;
}, [protagonist]); }, [protagonist]);
const duelStore = Duel.useStore();
const mutateDuel = Duel.useMutation();
useEffect(() => { useEffect(() => {
if (disturbed) { if (disturbed) {
@@ -47,12 +45,13 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
useEffect(() => { useEffect(() => {
function handleKeyDown(event: KeyboardEvent) { function handleKeyDown(event: KeyboardEvent) {
const { shells } = duelStore.getState().antagonist.gun;
times(3, (index) => { times(3, (index) => {
if (event.key === `${index + 1}` && shells.length > index) { if (
mutateDuel((draft) => { event.key === `${index + 1}` &&
draft.antagonist.shell = shells[index]; Duel.state.antagonist.gun.shells.length > index
) {
Duel.mutate((draft) => {
draft.antagonist.shell = Duel.state.antagonist.gun.shells[index];
}); });
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.customShell = undefined; draft.customShell = undefined;
@@ -1,5 +1,5 @@
import type { BlitzkitResponse, StarsInt } from '@blitzkit/core'; import type { BlitzkitResponse, StarsInt } from "@blitzkit/core";
import { PaperPlaneIcon } from '@radix-ui/react-icons'; import { PaperPlaneIcon } from "@radix-ui/react-icons";
import { import {
AlertDialog, AlertDialog,
Button, Button,
@@ -7,15 +7,14 @@ import {
Flex, Flex,
Spinner, Spinner,
Text, Text,
} from '@radix-ui/themes'; } from "@radix-ui/themes";
import { useState } from 'react'; import { useState } from "react";
import { useLocale } from '../../../../../../hooks/useLocale'; import { useLocale } from "../../../../../../hooks/useLocale";
import { App } from '../../../../../../stores/app'; import { App } from "../../../../../../stores/app";
import { Duel } from '../../../../../../stores/duel'; import { Duel } from "../../../../../../stores/duel";
import { Stars } from '../../Stars'; import { Stars } from "../../Stars";
export function VoteCaster() { export function VoteCaster() {
const appStore = App.useStore();
const tank = Duel.use((state) => state.protagonist.tank); const tank = Duel.use((state) => state.protagonist.tank);
const [easiness, setEasiness] = useState<StarsInt | null>(null); const [easiness, setEasiness] = useState<StarsInt | null>(null);
const [firepower, setFirepower] = useState<StarsInt | null>(null); const [firepower, setFirepower] = useState<StarsInt | null>(null);
@@ -42,28 +41,28 @@ export function VoteCaster() {
<Flex direction="column" mt="4"> <Flex direction="column" mt="4">
<Flex align="center" justify="between"> <Flex align="center" justify="between">
<Text> <Text>
Easiness{' '} Easiness{" "}
<Text color="gray" size="2"> <Text color="gray" size="2">
{easiness === null ? '(empty)' : `(${easiness})`} {easiness === null ? "(empty)" : `(${easiness})`}
</Text> </Text>
</Text> </Text>
<Stars stars={easiness} size="5" onCast={setEasiness} /> <Stars stars={easiness} size="5" onCast={setEasiness} />
</Flex> </Flex>
<Flex align="center" justify="between"> <Flex align="center" justify="between">
<Text> <Text>
Firepower{' '} Firepower{" "}
<Text color="gray" size="2"> <Text color="gray" size="2">
{firepower === null ? '(empty)' : `(${firepower})`} {firepower === null ? "(empty)" : `(${firepower})`}
</Text> </Text>
</Text> </Text>
<Stars stars={firepower} size="5" onCast={setFirepower} /> <Stars stars={firepower} size="5" onCast={setFirepower} />
</Flex> </Flex>
<Flex align="center" justify="between"> <Flex align="center" justify="between">
<Text> <Text>
Maneuverability{' '} Maneuverability{" "}
<Text color="gray" size="2"> <Text color="gray" size="2">
{maneuverability === null {maneuverability === null
? '(empty)' ? "(empty)"
: `(${maneuverability})`} : `(${maneuverability})`}
</Text> </Text>
</Text> </Text>
@@ -75,9 +74,9 @@ export function VoteCaster() {
</Flex> </Flex>
<Flex align="center" justify="between"> <Flex align="center" justify="between">
<Text> <Text>
Survivability{' '} Survivability{" "}
<Text color="gray" size="2"> <Text color="gray" size="2">
{survivability === null ? '(empty)' : `(${survivability})`} {survivability === null ? "(empty)" : `(${survivability})`}
</Text> </Text>
</Text> </Text>
<Stars stars={survivability} size="5" onCast={setSurvivability} /> <Stars stars={survivability} size="5" onCast={setSurvivability} />
@@ -100,7 +99,7 @@ export function VoteCaster() {
survivability === null) survivability === null)
} }
onClick={() => { onClick={() => {
const { wargaming } = appStore.getState().logins; const { wargaming } = App.state.logins;
if (!wargaming) { if (!wargaming) {
return setShowDialogue(false); return setShowDialogue(false);
@@ -110,26 +109,26 @@ export function VoteCaster() {
fetch( fetch(
`/api/tank-voting/${tank.id}/cast?${ `/api/tank-voting/${tank.id}/cast?${
easiness === null ? '' : `easiness=${easiness}&` easiness === null ? "" : `easiness=${easiness}&`
}${firepower === null ? '' : `firepower=${firepower}&`}${ }${firepower === null ? "" : `firepower=${firepower}&`}${
maneuverability === null maneuverability === null
? '' ? ""
: `maneuverability=${maneuverability}&` : `maneuverability=${maneuverability}&`
}${ }${
survivability === null survivability === null
? '' ? ""
: `survivability=${survivability}&` : `survivability=${survivability}&`
}player=${wargaming.id}&token=${wargaming.token}`, }player=${wargaming.id}&token=${wargaming.token}`
) )
.then( .then(
(response) => response.json() as Promise<BlitzkitResponse>, (response) => response.json() as Promise<BlitzkitResponse>
) )
.then((json) => { .then((json) => {
setShowDialogue(false); setShowDialogue(false);
setPostVote(json.status === 'ok'); setPostVote(json.status === "ok");
if (json.status === 'error') { if (json.status === "error") {
console.error('vote failed', json); console.error("vote failed", json);
} }
}); });
}} }}
@@ -1,87 +0,0 @@
import { type Draft, produce } from 'immer';
import {
createContext,
type ReactNode,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { type StoreApi, useStore as useStoreZustand } from 'zustand';
type StoreProviderProps<InitData> = {
children: ReactNode;
} & (InitData extends unknown ? { data?: InitData } : { data: InitData });
// idk why zustand doesn't export this
type ExtractState<S> = S extends {
getState: () => infer T;
}
? T
: never;
export function createContextualStore<
Store extends StoreApi<unknown>,
InitData,
>(initialize: (data: InitData) => Store) {
type Type = Store extends StoreApi<infer T> ? T : never;
const Context = createContext<Store | undefined>(undefined);
function Provider(props: StoreProviderProps<InitData>) {
const storeRef = useRef<Store>();
if (!storeRef.current) {
storeRef.current = initialize((props as { data: InitData }).data);
}
return (
<Context.Provider value={storeRef.current}>
{props.children}
</Context.Provider>
);
}
function useStore() {
const store = useContext(Context);
if (!store) throw new Error('useStore must be used within Provider');
return store as Store;
}
function use<Slice = Type>(
selector: (store: Type) => Slice = (state) => state as Slice,
) {
const store = useStore();
return useStoreZustand(
store,
selector as (state: ExtractState<Store>) => Slice,
);
}
function useDeferred<Slice = Type>(
selector: (store: Type) => Slice = (state) => state as Slice,
initial: Slice,
) {
const [state, setState] = useState(initial);
const trueState = use(selector);
useEffect(() => {
setState(trueState);
}, [trueState]);
return state;
}
function useMutation() {
const counterStoreContext = useStore();
return function (recipe: (draft: Draft<Type>) => void) {
counterStoreContext.setState(
produce<Type>((draft) => {
recipe(draft);
}),
);
};
}
return { Provider, use, useMutation, useStore, useDeferred };
}
@@ -1,48 +1,38 @@
import { useEffect } from 'react'; import { useEffect } from "react";
import { App } from '../../../../stores/app'; import { App } from "../../../../stores/app";
export const AUTH_PROVIDERS = ['wargaming'] as const; export const AUTH_PROVIDERS = ["wargaming"] as const;
export type AuthProvider = (typeof AUTH_PROVIDERS)[number]; export type AuthProvider = (typeof AUTH_PROVIDERS)[number];
export const AUTH_PROVIDER_NAMES: Record<AuthProvider, string> = { export const AUTH_PROVIDER_NAMES: Record<AuthProvider, string> = {
wargaming: 'Wargaming', wargaming: "Wargaming",
}; };
interface AuthorizeProps { interface AuthorizeProps {
provider: AuthProvider; provider: AuthProvider;
} }
export function Authorize(props: AuthorizeProps) { export function Authorize({ provider }: AuthorizeProps) {
return (
<App.Provider>
<Content {...props} />
</App.Provider>
);
}
function Content({ provider }: AuthorizeProps) {
const mutateApp = App.useMutation();
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
switch (provider) { switch (provider) {
case 'wargaming': { case "wargaming": {
if ( if (
!searchParams.has('expires_at') || !searchParams.has("expires_at") ||
!searchParams.has('access_token') || !searchParams.has("access_token") ||
!searchParams.has('account_id') !searchParams.has("account_id")
) { ) {
break; break;
} }
mutateApp((draft) => { App.mutate((draft) => {
draft.logins.wargaming = { draft.logins.wargaming = {
expires: Number(searchParams.get('expires_at')) * 1000, expires: Number(searchParams.get("expires_at")) * 1000,
token: searchParams.get('access_token')!, token: searchParams.get("access_token")!,
id: Number(searchParams.get('account_id'))!, id: Number(searchParams.get("account_id"))!,
}; };
}); });
@@ -53,7 +43,7 @@ function Content({ provider }: AuthorizeProps) {
throw new Error(`Unknown provider: ${provider}`); throw new Error(`Unknown provider: ${provider}`);
} }
window.location.href = searchParams.get('return') ?? '/'; window.location.href = searchParams.get("return") ?? "/";
})(); })();
}, []); }, []);
@@ -17,9 +17,7 @@ import {
LocaleProvider, LocaleProvider,
useLocale, useLocale,
} from "../../../hooks/useLocale"; } from "../../../hooks/useLocale";
import { App } from "../../../stores/app";
import { CompareEphemeral } from "../../../stores/compareEphemeral"; import { CompareEphemeral } from "../../../stores/compareEphemeral";
import { TankopediaPersistent } from "../../../stores/tankopediaPersistent";
const [ const [
modelDefinitions, modelDefinitions,
@@ -40,11 +38,7 @@ export function Page({ locale }: LocaleAcceptorProps) {
return ( return (
<LocaleProvider locale={locale}> <LocaleProvider locale={locale}>
<App.Provider> <Content />
<TankopediaPersistent.Provider>
<Content />
</TankopediaPersistent.Provider>
</App.Provider>
</LocaleProvider> </LocaleProvider>
); );
} }
@@ -5,36 +5,35 @@ import {
ResetIcon, ResetIcon,
TimerIcon, TimerIcon,
WidthIcon, WidthIcon,
} from '@radix-ui/react-icons'; } from "@radix-ui/react-icons";
import { Box, Button, Flex, Heading, ScrollArea, Text } from '@radix-ui/themes'; import { Box, Button, Flex, Heading, ScrollArea, Text } from "@radix-ui/themes";
import { useEffect, useRef, useState, type ReactNode } from 'react'; import { useEffect, useRef, useState, type ReactNode } from "react";
import { CopyButton } from '../../../../components/CopyButton'; import { CopyButton } from "../../../../components/CopyButton";
import { Boolean } from '../../../../components/Embeds/Boolean'; import { Boolean } from "../../../../components/Embeds/Boolean";
import { Color } from '../../../../components/Embeds/Color'; import { Color } from "../../../../components/Embeds/Color";
import { Enum } from '../../../../components/Embeds/Enum'; import { Enum } from "../../../../components/Embeds/Enum";
import { GenerateURL } from '../../../../components/Embeds/GenerateURL'; import { GenerateURL } from "../../../../components/Embeds/GenerateURL";
import { Import } from '../../../../components/Embeds/Import'; import { Import } from "../../../../components/Embeds/Import";
import { PreviewWrapper } from '../../../../components/Embeds/PreviewWrapper'; import { PreviewWrapper } from "../../../../components/Embeds/PreviewWrapper";
import { Radius } from '../../../../components/Embeds/Radius'; import { Radius } from "../../../../components/Embeds/Radius";
import { RichText } from '../../../../components/Embeds/RichText'; import { RichText } from "../../../../components/Embeds/RichText";
import { Size } from '../../../../components/Embeds/Size'; import { Size } from "../../../../components/Embeds/Size";
import { SizeWithout0 } from '../../../../components/Embeds/SizeWithout0'; import { SizeWithout0 } from "../../../../components/Embeds/SizeWithout0";
import { Slider } from '../../../../components/Embeds/Slider'; import { Slider } from "../../../../components/Embeds/Slider";
import { TextController } from '../../../../components/Embeds/TextController'; import { TextController } from "../../../../components/Embeds/TextController";
import { PageWrapper } from '../../../../components/PageWrapper'; import { PageWrapper } from "../../../../components/PageWrapper";
import { import {
embedConfigurations, embedConfigurations,
extractEmbedConfigDefaults, extractEmbedConfigDefaults,
} from '../../../../constants/embeds'; } from "../../../../constants/embeds";
import { NAVBAR_HEIGHT } from '../../../../constants/navbar'; import { NAVBAR_HEIGHT } from "../../../../constants/navbar";
import { import {
LocaleProvider, LocaleProvider,
useLocale, useLocale,
type LocaleAcceptorProps, type LocaleAcceptorProps,
} from '../../../../hooks/useLocale'; } from "../../../../hooks/useLocale";
import { App } from '../../../../stores/app'; import { EmbedState, type EmbedConfig } from "../../../../stores/embedState";
import { EmbedState, type EmbedConfig } from '../../../../stores/embedState'; import { EmbedItemType } from "../../../../stores/embedState/constants";
import { EmbedItemType } from '../../../../stores/embedState/constants';
export interface EmbedPreviewControllerProps { export interface EmbedPreviewControllerProps {
configKey: string; configKey: string;
@@ -47,31 +46,28 @@ interface PageProps {
export function Page({ embed, locale }: PageProps & LocaleAcceptorProps) { export function Page({ embed, locale }: PageProps & LocaleAcceptorProps) {
const config = embedConfigurations[embed]; const config = embedConfigurations[embed];
EmbedState.useInitialization(extractEmbedConfigDefaults(config));
return ( return (
<LocaleProvider locale={locale}> <LocaleProvider locale={locale}>
<App.Provider> <Content embed={embed} />
<EmbedState.Provider data={extractEmbedConfigDefaults(config)}>
<Content embed={embed} />
</EmbedState.Provider>
</App.Provider>
</LocaleProvider> </LocaleProvider>
); );
} }
function Content({ embed }: PageProps) { function Content({ embed }: PageProps) {
const embedStateStore = EmbedState.useStore();
const config = embedConfigurations[embed] as EmbedConfig; const config = embedConfigurations[embed] as EmbedConfig;
const [backgroundImage, setBackgroundImage] = useState( const [backgroundImage, setBackgroundImage] = useState(
'/assets/images/backgrounds/embed-default.webp', "/assets/images/backgrounds/embed-default.webp"
); );
const fileInput = useRef<HTMLInputElement>(); const fileInput = useRef<HTMLInputElement>();
const { strings } = useLocale(); const { strings } = useLocale();
useEffect(() => { useEffect(() => {
fileInput.current = document.createElement('input'); fileInput.current = document.createElement("input");
fileInput.current.type = 'file'; fileInput.current.type = "file";
fileInput.current.style.display = 'none'; fileInput.current.style.display = "none";
fileInput.current.accept = 'image/*'; fileInput.current.accept = "image/*";
fileInput.current.onchange = (event) => { fileInput.current.onchange = (event) => {
const file = (event.target as HTMLInputElement).files?.[0]; const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return; if (!file) return;
@@ -84,7 +80,7 @@ function Content({ embed }: PageProps) {
<Flex> <Flex>
<ScrollArea <ScrollArea
scrollbars="vertical" scrollbars="vertical"
style={{ height: '100%', maxWidth: 320 }} style={{ height: "100%", maxWidth: 320 }}
> >
<Flex direction="column" gap="2" p="4"> <Flex direction="column" gap="2" p="4">
<Import /> <Import />
@@ -102,7 +98,7 @@ function Content({ embed }: PageProps) {
<CopyButton <CopyButton
variant="outline" variant="outline"
copy={() => copy={() =>
'body { margin: 0; background-color: transparent; overflow: hidden; }' "body { margin: 0; background-color: transparent; overflow: hidden; }"
} }
> >
<CodeIcon /> <CodeIcon />
@@ -110,19 +106,19 @@ function Content({ embed }: PageProps) {
</CopyButton> </CopyButton>
<CopyButton <CopyButton
variant="outline" variant="outline"
copy={() => `${embedStateStore.getState().width}`} copy={() => `${EmbedState.state.width}`}
> >
<WidthIcon /> <WidthIcon />
{strings.website.tools.embed.configuration.export.width} {strings.website.tools.embed.configuration.export.width}
</CopyButton> </CopyButton>
<CopyButton <CopyButton
variant="outline" variant="outline"
copy={() => `${embedStateStore.getState().height}`} copy={() => `${EmbedState.state.height}`}
> >
<HeightIcon /> <HeightIcon />
{strings.website.tools.embed.configuration.export.height} {strings.website.tools.embed.configuration.export.height}
</CopyButton> </CopyButton>
<CopyButton variant="outline" copy={() => '1'}> <CopyButton variant="outline" copy={() => "1"}>
<TimerIcon /> <TimerIcon />
{strings.website.tools.embed.configuration.export.framerate} {strings.website.tools.embed.configuration.export.framerate}
</CopyButton> </CopyButton>
@@ -136,13 +132,10 @@ function Content({ embed }: PageProps) {
color="red" color="red"
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
embedStateStore.setState( EmbedState.set(EmbedState.initial);
embedStateStore.getInitialState(),
true,
);
}} }}
> >
<ResetIcon />{' '} <ResetIcon />{" "}
{strings.website.tools.embed.configuration.customize.reset} {strings.website.tools.embed.configuration.customize.reset}
</Button> </Button>
</Flex> </Flex>
@@ -211,11 +204,11 @@ function Content({ embed }: PageProps) {
return ( return (
<Flex <Flex
key={configKey} key={configKey}
direction={oneLiner ? undefined : 'column'} direction={oneLiner ? undefined : "column"}
gap={oneLiner ? '2' : '1'} gap={oneLiner ? "2" : "1"}
justify={oneLiner ? 'between' : undefined} justify={oneLiner ? "between" : undefined}
mb={oneLiner ? '2' : '4'} mb={oneLiner ? "2" : "4"}
pb={item.pad ? '6' : undefined} pb={item.pad ? "6" : undefined}
> >
<Text> <Text>
{ {
@@ -229,7 +222,7 @@ function Content({ embed }: PageProps) {
gap="2" gap="2"
align="center" align="center"
flexGrow="1" flexGrow="1"
justify={oneLiner ? 'end' : undefined} justify={oneLiner ? "end" : undefined}
> >
{control} {control}
</Flex> </Flex>
@@ -250,9 +243,9 @@ function Content({ embed }: PageProps) {
direction="column" direction="column"
height="100%" height="100%"
style={{ style={{
borderRadius: 'var(--radius-3)', borderRadius: "var(--radius-3)",
overflow: 'hidden', overflow: "hidden",
boxShadow: 'var(--shadow-3)', boxShadow: "var(--shadow-3)",
}} }}
> >
<Flex <Flex
@@ -261,7 +254,7 @@ function Content({ embed }: PageProps) {
p="2" p="2"
pl="3" pl="3"
style={{ style={{
backgroundColor: 'var(--color-panel-solid)', backgroundColor: "var(--color-panel-solid)",
}} }}
> >
<Text color="gray"> <Text color="gray">
@@ -1,22 +1,22 @@
import { Box } from '@radix-ui/themes'; import { Box } from "@radix-ui/themes";
import { useMemo } from 'react'; import { useMemo } from "react";
import { parse } from 'urlon'; import { parse } from "urlon";
import { BlitzKitTheme } from '../../../../../components/BlitzKitTheme'; import { BlitzKitTheme } from "../../../../../components/BlitzKitTheme";
import { import {
embedConfigurations, embedConfigurations,
embedRenderers, embedRenderers,
extractEmbedConfigDefaults, extractEmbedConfigDefaults,
} from '../../../../../constants/embeds'; } from "../../../../../constants/embeds";
import { import {
LocaleProvider, LocaleProvider,
type LocaleAcceptorProps, type LocaleAcceptorProps,
} from '../../../../../hooks/useLocale'; } from "../../../../../hooks/useLocale";
import { import {
EmbedState, EmbedState,
type EmbedConfig, type EmbedConfig,
type ExtractEmbedConfigTypes, type ExtractEmbedConfigTypes,
} from '../../../../../stores/embedState'; } from "../../../../../stores/embedState";
import { useEmbedStateCurry } from '../../../../../stores/embedState/utilities'; import { useEmbedStateCurry } from "../../../../../stores/embedState/utilities";
interface PageProps { interface PageProps {
embed: keyof typeof embedConfigurations; embed: keyof typeof embedConfigurations;
@@ -25,7 +25,7 @@ interface PageProps {
export function Page({ embed, locale }: PageProps & LocaleAcceptorProps) { export function Page({ embed, locale }: PageProps & LocaleAcceptorProps) {
const state = useMemo(() => { const state = useMemo(() => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const stateRaw = params.get('state'); const stateRaw = params.get("state");
const partialState = stateRaw === null ? {} : parse(stateRaw); const partialState = stateRaw === null ? {} : parse(stateRaw);
const state = { const state = {
...extractEmbedConfigDefaults(embedConfigurations[embed]), ...extractEmbedConfigDefaults(embedConfigurations[embed]),
@@ -35,11 +35,11 @@ export function Page({ embed, locale }: PageProps & LocaleAcceptorProps) {
return state; return state;
}, [embed]); }, [embed]);
EmbedState.useInitialization(state);
return ( return (
<LocaleProvider locale={locale}> <LocaleProvider locale={locale}>
<EmbedState.Provider data={state}> <Content embed={embed} />
<Content embed={embed} />
</EmbedState.Provider>
</LocaleProvider> </LocaleProvider>
); );
} }
@@ -47,11 +47,11 @@ export function Page({ embed, locale }: PageProps & LocaleAcceptorProps) {
function Content({ embed }: PageProps) { function Content({ embed }: PageProps) {
const Renderer = embedRenderers[embed]; const Renderer = embedRenderers[embed];
const { useEmbedState } = useEmbedStateCurry(); const { useEmbedState } = useEmbedStateCurry();
const width = useEmbedState('width'); const width = useEmbedState("width");
const height = useEmbedState('height'); const height = useEmbedState("height");
return ( return (
<BlitzKitTheme style={{ background: 'transparent' }}> <BlitzKitTheme style={{ background: "transparent" }}>
<Box width={`${width}px`} height={`${height}px`} overflow="hidden"> <Box width={`${width}px`} height={`${height}px`} overflow="hidden">
<Renderer /> <Renderer />
</Box> </Box>
@@ -1,26 +1,19 @@
import { Box, Flex, Link, Text } from '@radix-ui/themes'; import { Box, Flex, Link, Text } from "@radix-ui/themes";
import { PreviewWrapper } from '../../../components/Embeds/PreviewWrapper'; import { PreviewWrapper } from "../../../components/Embeds/PreviewWrapper";
import { LinkI18n } from '../../../components/LinkI18n'; import { LinkI18n } from "../../../components/LinkI18n";
import { PageWrapper } from '../../../components/PageWrapper'; import { PageWrapper } from "../../../components/PageWrapper";
import { import { embedConfigurations } from "../../../constants/embeds";
embedConfigurations, import { Var } from "../../../core/radix/var";
extractEmbedConfigDefaults,
} from '../../../constants/embeds';
import { Var } from '../../../core/radix/var';
import { import {
type LocaleAcceptorProps, type LocaleAcceptorProps,
LocaleProvider, LocaleProvider,
useLocale, useLocale,
} from '../../../hooks/useLocale'; } from "../../../hooks/useLocale";
import { App } from '../../../stores/app';
import { EmbedState } from '../../../stores/embedState';
export function Page({ locale }: LocaleAcceptorProps) { export function Page({ locale }: LocaleAcceptorProps) {
return ( return (
<LocaleProvider locale={locale}> <LocaleProvider locale={locale}>
<App.Provider> <Content />
<Content />
</App.Provider>
</LocaleProvider> </LocaleProvider>
); );
} }
@@ -31,7 +24,7 @@ function Content() {
return ( return (
<PageWrapper color="red"> <PageWrapper color="red">
<Text align="center" mt="3"> <Text align="center" mt="3">
{strings.website.tools.embed.coming_soon}{' '} {strings.website.tools.embed.coming_soon}{" "}
<Link <Link
underline="always" underline="always"
href="https://discord.gg/nDt7AjGJQH" href="https://discord.gg/nDt7AjGJQH"
@@ -42,61 +35,56 @@ function Content() {
</Text> </Text>
<Flex justify="center"> <Flex justify="center">
{Object.entries(embedConfigurations).map(([embed, config]) => ( {Object.keys(embedConfigurations).map((embed) => (
<EmbedState.Provider <LinkI18n
key={embed} locale={locale}
data={extractEmbedConfigDefaults(config)} href={`/embed/${embed}`}
color="gray"
highContrast
style={{
width: "fit-content",
height: "fit-content",
}}
underline="hover"
> >
<LinkI18n <Flex
locale={locale} direction="column"
href={`/embed/${embed}`} overflow="hidden"
color="gray"
highContrast
style={{ style={{
width: 'fit-content', borderRadius: Var("radius-4"),
height: 'fit-content', backgroundColor: Var("color-panel-solid"),
boxShadow: Var("shadow-3"),
}} }}
underline="hover"
> >
<Flex <Flex justify="center" p="2">
direction="column" <Text>
{
strings.website.tools.embed.types[
embed as keyof typeof strings.website.tools.embed.types
].name
}
</Text>
</Flex>
<Box
width="20rem"
height="20rem"
overflow="hidden" overflow="hidden"
position="relative"
style={{ style={{
borderRadius: Var('radius-4'), backgroundImage:
backgroundColor: Var('color-panel-solid'), "url(/assets/images/backgrounds/embed-default.webp)",
boxShadow: Var('shadow-3'), backgroundSize: "cover",
backgroundPosition: "center",
}} }}
> >
<Flex justify="center" p="2"> <PreviewWrapper
<Text> naked
{ name={embed as keyof typeof embedConfigurations}
strings.website.tools.embed.types[ />
embed as keyof typeof strings.website.tools.embed.types </Box>
].name </Flex>
} </LinkI18n>
</Text>
</Flex>
<Box
width="20rem"
height="20rem"
overflow="hidden"
position="relative"
style={{
backgroundImage:
'url(/assets/images/backgrounds/embed-default.webp)',
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
<PreviewWrapper
naked
name={embed as keyof typeof embedConfigurations}
/>
</Box>
</Flex>
</LinkI18n>
</EmbedState.Provider>
))} ))}
</Flex> </Flex>
</PageWrapper> </PageWrapper>
@@ -5,7 +5,6 @@ import { Guesser } from "../../../components/Guesser";
import { GuessRenderer } from "../../../components/GuessRenderer"; import { GuessRenderer } from "../../../components/GuessRenderer";
import { GuessRendererLoader } from "../../../components/GuessRendererLoader"; import { GuessRendererLoader } from "../../../components/GuessRendererLoader";
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions"; import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
import { awaitableProvisionDefinitions } from "../../../core/awaitables/provisionDefinitions";
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions"; import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
import { import {
type LocaleAcceptorProps, type LocaleAcceptorProps,
@@ -16,12 +15,10 @@ import { Duel } from "../../../stores/duel";
import { Guess, GuessState } from "../../../stores/guess"; import { Guess, GuessState } from "../../../stores/guess";
import { Tankopedia } from "../../../stores/tankopedia"; import { Tankopedia } from "../../../stores/tankopedia";
const [tankDefinitions, modelDefinitions, provisionDefinitions] = const [tankDefinitions, modelDefinitions] = await Promise.all([
await Promise.all([ awaitableTankDefinitions,
awaitableTankDefinitions, awaitableModelDefinitions,
awaitableModelDefinitions, ]);
awaitableProvisionDefinitions,
]);
const ids = Object.keys(tankDefinitions.tanks); const ids = Object.keys(tankDefinitions.tanks);
@@ -43,12 +40,9 @@ function Container() {
const model = modelDefinitions.models[tank.id]; const model = modelDefinitions.models[tank.id];
Tankopedia.useInitialization(model); Tankopedia.useInitialization(model);
Duel.useInitialization({ tank, model });
return ( return <Content />;
<Duel.Provider data={{ tank, model, provisionDefinitions }}>
<Content />
</Duel.Provider>
);
} }
function Content() { function Content() {
@@ -1,23 +1,20 @@
import { PageWrapper } from '../../../components/PageWrapper'; import { PageWrapper } from "../../../components/PageWrapper";
import { PerformanceInfo } from '../../../components/Performance/PerformanceInfo'; import { PerformanceInfo } from "../../../components/Performance/PerformanceInfo";
import { PlayerCountControl } from '../../../components/Performance/PlayerCountControl'; import { PlayerCountControl } from "../../../components/Performance/PlayerCountControl";
import { TankTable } from '../../../components/Performance/Table'; import { TankTable } from "../../../components/Performance/Table";
import { FilterControl } from '../../../components/TankSearch/components/FilterControl'; import { FilterControl } from "../../../components/TankSearch/components/FilterControl";
import { import {
LocaleProvider, LocaleProvider,
type LocaleAcceptorProps, type LocaleAcceptorProps,
} from '../../../hooks/useLocale'; } from "../../../hooks/useLocale";
import { TankPerformanceSort } from '../../../stores/tankPerformanceSort'; import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
type PageProps = MaybeSkeletonComponentProps & LocaleAcceptorProps; type PageProps = MaybeSkeletonComponentProps & LocaleAcceptorProps;
export function Page({ skeleton, locale }: PageProps) { export function Page({ skeleton, locale }: PageProps) {
return ( return (
<LocaleProvider locale={locale}> <LocaleProvider locale={locale}>
<TankPerformanceSort.Provider> <Content skeleton={skeleton} />
<Content skeleton={skeleton} />
</TankPerformanceSort.Provider>
</LocaleProvider> </LocaleProvider>
); );
} }
@@ -1,27 +1,24 @@
import { DEFAULT_LOCALE } from '@blitzkit/i18n'; import { DEFAULT_LOCALE } from "@blitzkit/i18n";
import { Flex, Switch, Text } from '@radix-ui/themes'; import { Flex, Switch, Text } from "@radix-ui/themes";
import { LocaleSwitcher } from '../../../components/LocaleSwitcher'; import { LocaleSwitcher } from "../../../components/LocaleSwitcher";
import { PageWrapper } from '../../../components/PageWrapper'; import { PageWrapper } from "../../../components/PageWrapper";
import { import {
LocaleProvider, LocaleProvider,
useLocale, useLocale,
type LocaleAcceptorProps, type LocaleAcceptorProps,
} from '../../../hooks/useLocale'; } from "../../../hooks/useLocale";
import { App } from '../../../stores/app'; import { App } from "../../../stores/app";
export function Page({ locale = DEFAULT_LOCALE }: LocaleAcceptorProps) { export function Page({ locale = DEFAULT_LOCALE }: LocaleAcceptorProps) {
return ( return (
<LocaleProvider locale={locale}> <LocaleProvider locale={locale}>
<App.Provider> <Content locale={locale} />
<Content locale={locale} />
</App.Provider>
</LocaleProvider> </LocaleProvider>
); );
} }
function Content({ locale }: LocaleAcceptorProps) { function Content({ locale }: LocaleAcceptorProps) {
const developerMode = App.useDeferred((state) => state.developerMode, false); const developerMode = App.useDeferred((state) => state.developerMode, false);
const appStore = App.useStore();
const { strings } = useLocale(); const { strings } = useLocale();
return ( return (
@@ -33,7 +30,9 @@ function Content({ locale }: LocaleAcceptorProps) {
variant="classic" variant="classic"
checked={developerMode} checked={developerMode}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
appStore.setState({ developerMode: checked }) App.mutate((draft) => {
draft.developerMode = checked;
})
} }
/> />
</Flex> </Flex>
@@ -9,16 +9,13 @@ import { MetaSection } from "../../../../components/Tankopedia/MetaSection";
import { TechTreeSection } from "../../../../components/Tankopedia/TechTreeSection"; import { TechTreeSection } from "../../../../components/Tankopedia/TechTreeSection";
import { VideoSection } from "../../../../components/Tankopedia/VideoSection"; import { VideoSection } from "../../../../components/Tankopedia/VideoSection";
import { awaitableModelDefinitions } from "../../../../core/awaitables/modelDefinitions"; import { awaitableModelDefinitions } from "../../../../core/awaitables/modelDefinitions";
import { awaitableProvisionDefinitions } from "../../../../core/awaitables/provisionDefinitions";
import { awaitableTankDefinitions } from "../../../../core/awaitables/tankDefinitions"; import { awaitableTankDefinitions } from "../../../../core/awaitables/tankDefinitions";
import { import {
LocaleProvider, LocaleProvider,
type LocaleAcceptorProps, type LocaleAcceptorProps,
} from "../../../../hooks/useLocale"; } from "../../../../hooks/useLocale";
import { App } from "../../../../stores/app";
import { Duel } from "../../../../stores/duel"; import { Duel } from "../../../../stores/duel";
import { Tankopedia } from "../../../../stores/tankopedia"; import { Tankopedia } from "../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
import type { MaybeSkeletonComponentProps } from "../../../../types/maybeSkeletonComponentProps"; import type { MaybeSkeletonComponentProps } from "../../../../types/maybeSkeletonComponentProps";
import type { TankGuide } from "./index.astro"; import type { TankGuide } from "./index.astro";
@@ -28,41 +25,32 @@ type PageProps = MaybeSkeletonComponentProps &
guide?: TankGuide; guide?: TankGuide;
}; };
const [tankDefinitions, provisionDefinitions, modelDefinitions] = const [tankDefinitions, modelDefinitions] = await Promise.all([
await Promise.all([ awaitableTankDefinitions,
awaitableTankDefinitions, awaitableModelDefinitions,
awaitableProvisionDefinitions, ]);
awaitableModelDefinitions,
]);
export function Page({ id, skeleton, locale, guide }: PageProps) { export function Page({ id, skeleton, locale, guide }: PageProps) {
const tank = tankDefinitions.tanks[id]; const tank = tankDefinitions.tanks[id];
const model = modelDefinitions.models[id]; const model = modelDefinitions.models[id];
Tankopedia.useInitialization(model); Tankopedia.useInitialization(model);
Duel.useInitialization({ tank, model });
return ( return (
<LocaleProvider locale={locale}> <LocaleProvider locale={locale}>
<App.Provider> <PageWrapper p="0" maxWidth="unset" color="purple" gap="9" pb="9">
<TankopediaPersistent.Provider> <HeroSection skeleton={skeleton} />
<Duel.Provider <MetaSection />
data={{ tank, model, provisionDefinitions: provisionDefinitions }} <CalloutsSection />
> {tank.type === TankType.RESEARCHABLE && !tank.deprecated && (
<PageWrapper p="0" maxWidth="unset" color="purple" gap="9" pb="9"> <TechTreeSection skeleton={skeleton} />
<HeroSection skeleton={skeleton} /> )}
<MetaSection /> <CharacteristicsSection skeleton={skeleton} />
<CalloutsSection /> <GameModeSection />
{tank.type === TankType.RESEARCHABLE && !tank.deprecated && ( {guide && <GuideSection guide={guide} />}
<TechTreeSection skeleton={skeleton} /> <VideoSection skeleton={skeleton} />
)} </PageWrapper>
<CharacteristicsSection skeleton={skeleton} />
<GameModeSection />
{guide && <GuideSection guide={guide} />}
<VideoSection skeleton={skeleton} />
</PageWrapper>
</Duel.Provider>
</TankopediaPersistent.Provider>
</App.Provider>
</LocaleProvider> </LocaleProvider>
); );
} }
@@ -1,12 +1,10 @@
import { PageWrapper } from '../../../components/PageWrapper'; import { PageWrapper } from "../../../components/PageWrapper";
import { TankSearch } from '../../../components/TankSearch'; import { TankSearch } from "../../../components/TankSearch";
import { import {
LocaleProvider, LocaleProvider,
type LocaleAcceptorProps, type LocaleAcceptorProps,
} from '../../../hooks/useLocale'; } from "../../../hooks/useLocale";
import { App } from '../../../stores/app'; import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
export function Page({ export function Page({
locale, locale,
@@ -15,11 +13,7 @@ export function Page({
return ( return (
<LocaleProvider locale={locale}> <LocaleProvider locale={locale}>
<PageWrapper color="purple" maxWidth="80rem"> <PageWrapper color="purple" maxWidth="80rem">
<TankopediaPersistent.Provider> <TankSearch skeleton={skeleton} />
<App.Provider>
<TankSearch skeleton={skeleton} />
</App.Provider>
</TankopediaPersistent.Provider>
</PageWrapper> </PageWrapper>
</LocaleProvider> </LocaleProvider>
); );
@@ -1,21 +1,18 @@
import { PageWrapper } from '../../../components/PageWrapper'; import { PageWrapper } from "../../../components/PageWrapper";
import { FilterControl } from '../../../components/TankSearch/components/FilterControl'; import { FilterControl } from "../../../components/TankSearch/components/FilterControl";
import { TierListControls } from '../../../components/TierList/Controls'; import { TierListControls } from "../../../components/TierList/Controls";
import { TierListTable } from '../../../components/TierList/Table'; import { TierListTable } from "../../../components/TierList/Table";
import { TierListTiles } from '../../../components/TierList/Tiles'; import { TierListTiles } from "../../../components/TierList/Tiles";
import { URLManager } from '../../../components/TierList/URLManager'; import { URLManager } from "../../../components/TierList/URLManager";
import { import {
LocaleProvider, LocaleProvider,
type LocaleAcceptorProps, type LocaleAcceptorProps,
} from '../../../hooks/useLocale'; } from "../../../hooks/useLocale";
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
export function Page({ locale }: LocaleAcceptorProps) { export function Page({ locale }: LocaleAcceptorProps) {
return ( return (
<LocaleProvider locale={locale}> <LocaleProvider locale={locale}>
<TankopediaPersistent.Provider> <Content />
<Content />
</TankopediaPersistent.Provider>
</LocaleProvider> </LocaleProvider>
); );
} }
@@ -1,19 +1,14 @@
import { TankSandbox } from "../../../../../components/Tankopedia/HeroSection/components/TankSandbox"; import { TankSandbox } from "../../../../../components/Tankopedia/HeroSection/components/TankSandbox";
import { awaitableModelDefinitions } from "../../../../../core/awaitables/modelDefinitions"; import { awaitableModelDefinitions } from "../../../../../core/awaitables/modelDefinitions";
import { awaitableProvisionDefinitions } from "../../../../../core/awaitables/provisionDefinitions";
import { awaitableTankDefinitions } from "../../../../../core/awaitables/tankDefinitions"; import { awaitableTankDefinitions } from "../../../../../core/awaitables/tankDefinitions";
import { LocaleProvider } from "../../../../../hooks/useLocale"; import { LocaleProvider } from "../../../../../hooks/useLocale";
import { App } from "../../../../../stores/app";
import { Duel } from "../../../../../stores/duel"; import { Duel } from "../../../../../stores/duel";
import { Tankopedia } from "../../../../../stores/tankopedia"; import { Tankopedia } from "../../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../../stores/tankopediaPersistent";
const [provisionDefinitions, tankDefinitions, modelDefinitions] = const [tankDefinitions, modelDefinitions] = await Promise.all([
await Promise.all([ awaitableTankDefinitions,
awaitableProvisionDefinitions, awaitableModelDefinitions,
awaitableTankDefinitions, ]);
awaitableModelDefinitions,
]);
interface PageProps { interface PageProps {
id: number; id: number;
@@ -24,16 +19,11 @@ export function Page({ id }: PageProps) {
const model = modelDefinitions.models[id]; const model = modelDefinitions.models[id];
Tankopedia.useInitialization(model); Tankopedia.useInitialization(model);
Duel.useInitialization({ tank, model });
return ( return (
<LocaleProvider locale="en"> <LocaleProvider locale="en">
<App.Provider> <Content />
<TankopediaPersistent.Provider>
<Duel.Provider data={{ provisionDefinitions, model, tank }}>
<Content />
</Duel.Provider>
</TankopediaPersistent.Provider>
</App.Provider>
</LocaleProvider> </LocaleProvider>
); );
} }
+9 -16
View File
@@ -1,7 +1,4 @@
import { merge } from 'lodash-es'; import { Varuna } from "varuna";
import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware';
import { createContextualStore } from '../../core/zustand/createContextualStore';
export interface WargamingLogin { export interface WargamingLogin {
token: string; token: string;
@@ -9,7 +6,7 @@ export interface WargamingLogin {
expires: number; expires: number;
} }
interface AppStore { interface App {
developerMode: boolean; developerMode: boolean;
policiesAgreementIndex: number; policiesAgreementIndex: number;
logins: { logins: {
@@ -17,15 +14,11 @@ interface AppStore {
}; };
} }
export const App = createContextualStore(() => export const App = new Varuna<App>(
create<AppStore>()( {
persist( developerMode: false,
subscribeWithSelector<AppStore>(() => ({ policiesAgreementIndex: -1,
developerMode: false, logins: {},
policiesAgreementIndex: -1, },
logins: {}, "app-2"
})),
{ name: 'app', merge: (a, b) => merge(b, a) },
),
),
); );
-15
View File
@@ -1,15 +0,0 @@
import type { Samples } from '@blitzkit/core';
import { create } from 'zustand';
import { createContextualStore } from '../core/zustand/createContextualStore';
export interface Charts {
period: ChartsPeriod;
}
export type ChartsPeriod = keyof Samples;
export const Charts = createContextualStore(() =>
create<Charts>()(() => ({
period: 'd_30',
})),
);
+13 -26
View File
@@ -2,16 +2,14 @@ import {
ModelDefinition, ModelDefinition,
type EngineDefinition, type EngineDefinition,
type GunDefinition, type GunDefinition,
type ProvisionDefinitions,
type ShellDefinition, type ShellDefinition,
type TankDefinition, type TankDefinition,
type TrackDefinition, type TrackDefinition,
type TurretDefinition, type TurretDefinition,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { create } from 'zustand'; import { Varuna } from "varuna";
import { subscribeWithSelector } from 'zustand/middleware'; import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
import { tankToDuelMember } from '../../core/blitzkit/tankToDuelMember'; import { tankToDuelMember } from "../../core/blitzkit/tankToDuelMember";
import { createContextualStore } from '../../core/zustand/createContextualStore';
type EquipmentMatrixItem = -1 | 0 | 1; type EquipmentMatrixItem = -1 | 0 | 1;
type EquipmentMatrixRow = [ type EquipmentMatrixRow = [
@@ -45,24 +43,13 @@ export interface DuelStore {
antagonist: DuelMember; antagonist: DuelMember;
} }
export const Duel = createContextualStore( const provisionDefinitions = await awaitableProvisionDefinitions;
({
tank,
model,
provisionDefinitions,
}: {
tank: TankDefinition;
model: ModelDefinition;
provisionDefinitions: ProvisionDefinitions;
}) => {
const protagonist = tankToDuelMember(tank, provisionDefinitions);
return create<DuelStore>()( export const Duel = new Varuna<
subscribeWithSelector<DuelStore>(() => ({ DuelStore,
protagonist, { tank: TankDefinition; model: ModelDefinition }
model, >(({ tank, model }) => {
antagonist: protagonist, const protagonist = tankToDuelMember(tank, provisionDefinitions);
})),
); return { protagonist, antagonist: protagonist, model };
}, });
);
+13 -14
View File
@@ -1,16 +1,15 @@
import * as radixColors from '@radix-ui/colors'; import * as radixColors from "@radix-ui/colors";
import { create } from 'zustand'; import { Varuna } from "varuna";
import { createContextualStore } from '../../core/zustand/createContextualStore';
import { import {
type EmbedConfigItemType, type EmbedConfigItemType,
EmbedItemType, EmbedItemType,
radixGrays, radixGrays,
radixTextWeights, radixTextWeights,
} from './constants'; } from "./constants";
export type RadixSize = `${0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`; export type RadixSize = `${0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`;
export type RadixSizeWithout0 = Exclude<RadixSize, '0'>; export type RadixSizeWithout0 = Exclude<RadixSize, "0">;
export type RadixRadius = `${1 | 2 | 3 | 4}` | 'full'; export type RadixRadius = `${1 | 2 | 3 | 4}` | "full";
export type RadixTextWeight = (typeof radixTextWeights)[number]; export type RadixTextWeight = (typeof radixTextWeights)[number];
export type RadixColorCompound = { export type RadixColorCompound = {
base: RadixColor; base: RadixColor;
@@ -18,7 +17,7 @@ export type RadixColorCompound = {
}; };
export type RadixTextColor = export type RadixTextColor =
| Exclude<RadixColor, RadixColorGrays> | Exclude<RadixColor, RadixColorGrays>
| 'gray' | "gray"
| undefined; | undefined;
export type RadixColorVariantRaw = `${ export type RadixColorVariantRaw = `${
| 1 | 1
@@ -33,17 +32,17 @@ export type RadixColorVariantRaw = `${
| 10 | 10
| 11 | 11
| 12}`; | 12}`;
export type RadixColorVariant = `${'' | 'a'}${RadixColorVariantRaw}`; export type RadixColorVariant = `${"" | "a"}${RadixColorVariantRaw}`;
export type RadixColor = Exclude< export type RadixColor = Exclude<
keyof typeof radixColors, keyof typeof radixColors,
`${string}${'A' | 'P3' | 'Dark'}` `${string}${"A" | "P3" | "Dark"}`
>; >;
export type RadixColorGrays = (typeof radixGrays)[number]; export type RadixColorGrays = (typeof radixGrays)[number];
export type ExtractEmbedConfigTypes<Config extends EmbedConfig> = { export type ExtractEmbedConfigTypes<Config extends EmbedConfig> = {
[Key in keyof Config]: ({ [Key in keyof Config]: ({
type: Config[Key]['type']; type: Config[Key]["type"];
} & EmbedConfigItem)['default']; } & EmbedConfigItem)["default"];
}; };
export type EmbedConfig = { export type EmbedConfig = {
@@ -102,8 +101,8 @@ export type EmbedConfigItem = (
pad?: boolean; pad?: boolean;
}; };
export type EmbedStateStore = Record<string, EmbedConfigItem['default']>; export type EmbedStateStore = Record<string, EmbedConfigItem["default"]>;
export const EmbedState = createContextualStore((init: EmbedStateStore) => export const EmbedState = new Varuna<EmbedStateStore, EmbedStateStore>(
create<EmbedStateStore>()(() => init), (data) => data
); );
@@ -1,6 +1,5 @@
import type en from '@blitzkit/i18n/strings/en.json'; import type en from "@blitzkit/i18n/strings/en.json";
import { create } from 'zustand'; import { Varuna } from "varuna";
import { createContextualStore } from '../../core/zustand/createContextualStore';
export type TankPerformanceSortType = export type TankPerformanceSortType =
keyof typeof en.website.tools.performance.table.stats; keyof typeof en.website.tools.performance.table.stats;
@@ -10,9 +9,7 @@ export interface TankPerformanceSort {
direction: -1 | 1; direction: -1 | 1;
} }
export const TankPerformanceSort = createContextualStore(() => export const TankPerformanceSort = new Varuna<TankPerformanceSort>({
create<TankPerformanceSort>()(() => ({ type: "winrate",
type: 'winrate', direction: -1,
direction: -1, });
})),
);
@@ -1,14 +1,11 @@
import type en from '@blitzkit/i18n/strings/en.json'; import type en from "@blitzkit/i18n/strings/en.json";
import * as lodash from 'lodash-es'; import { Varuna } from "varuna";
import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware';
import { createContextualStore } from '../../core/zustand/createContextualStore';
export type TankopediaSortBy = keyof typeof en.website.common.tank_search.sort; export type TankopediaSortBy = keyof typeof en.website.common.tank_search.sort;
export type TankopediaSortDirection = 'ascending' | 'descending'; export type TankopediaSortDirection = "ascending" | "descending";
export type TankopediaTestTankDisplay = 'include' | 'exclude' | 'only'; export type TankopediaTestTankDisplay = "include" | "exclude" | "only";
export interface TankopediaPersistentStore { export interface TankopediaPersistent {
wireframe: boolean; wireframe: boolean;
opaque: boolean; opaque: boolean;
advancedHighlighting: boolean; advancedHighlighting: boolean;
@@ -25,25 +22,21 @@ export interface TankopediaPersistentStore {
}; };
} }
export const TankopediaPersistent = createContextualStore(() => export const TankopediaPersistent = new Varuna<TankopediaPersistent>(
create<TankopediaPersistentStore>()( {
persist( hideTankModelUnderArmor: false,
subscribeWithSelector<TankopediaPersistentStore>(() => ({ wireframe: false,
hideTankModelUnderArmor: false, opaque: false,
wireframe: false, advancedHighlighting: false,
opaque: false, greenPenetration: false,
advancedHighlighting: false, showSpacedArmor: true,
greenPenetration: false, showExternalModules: true,
showSpacedArmor: true, showPrimaryArmor: true,
showExternalModules: true, recentlyViewed: [],
showPrimaryArmor: true, sort: {
recentlyViewed: [], by: "meta.none",
sort: { direction: "ascending",
by: 'meta.none', },
direction: 'ascending', },
}, "tankopedia-2"
})),
{ name: 'tankopedia', merge: (a, b) => lodash.merge(b, a) },
),
),
); );