feat: start of monorepo

This commit is contained in:
TrèsAbhi committed 2024-09-06 21:45:58 -05:00
1 parent 14f39d71ac
commit 73c7bdf0c0
260 files changed
+1236 -1235

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
import { useLayoutEffect, useState } from 'react';
import * as App from '../stores/app';
let cache: Record<string, boolean | Promise<boolean>> = {};
export function useAdExempt() {
const patreon = App.use((state) => state.logins.patreon);
const [exempt, setExempt] = useState(false);
useLayoutEffect(() => {
(async () => {
if (!patreon) return setExempt(false);
if (!(patreon.token in cache)) {
cache[patreon.token] = new Promise<boolean>(async (resolve) => {
const response = await fetch(
`/api/patreon/membership/${patreon.token}`,
{ cache: 'force-cache' },
);
if (!response.ok) return resolve(false);
const json = await response.json();
const exempt = typeof json === 'boolean' && json;
cache[patreon.token] = exempt;
resolve(exempt);
});
}
setExempt(await cache[patreon.token]);
})();
}, []);
return exempt;
}
@@ -0,0 +1,12 @@
import { EquipmentMatrix } from '../../packages/website/src/stores/duel';
import { EquipmentPreset } from '../core/blitzkit/equipmentDefinitions';
export function useAppliedEquipments(
matrix: EquipmentMatrix,
preset: EquipmentPreset,
) {
const flatMatrix = matrix.flat();
return preset
.flat()
.map((options, index) => options[flatMatrix[index] == 1 ? 1 : 0]);
}
+22
View File
@@ -0,0 +1,22 @@
import { ObjectMap, useLoader } from '@react-three/fiber';
import { GLTF, GLTFLoader } from 'three-stdlib';
import { asset } from '../core/blitzkit/asset';
const cache: Record<
number,
{ gltf: GLTF & ObjectMap; hasDynamicArmor: boolean }
> = {};
export function useArmor(id: number) {
const gltf = useLoader(GLTFLoader, asset(`3d/tanks/armor/${id}.glb`));
if (!cache[id]) {
cache[id] = {
gltf,
hasDynamicArmor: Object.values(gltf.nodes).some(
(node) =>
node.name.includes('state_00') || node.name.includes('state_01'),
),
};
}
return cache[id];
}
@@ -0,0 +1,13 @@
import { use } from 'react';
import { averageDefinitions } from '../core/blitzkit/averageDefinitions';
import { discoveredIdsDefinitions } from '../core/blitzkit/discoveredIdDefinitions';
export function useAveragesExclusionRatio() {
const awaitedDiscoveredIdsDefinitions = use(discoveredIdsDefinitions);
const awaitedAverageDefinitions = use(averageDefinitions);
return (
awaitedDiscoveredIdsDefinitions.count /
awaitedAverageDefinitions.samples.total
);
}
+8
View File
@@ -0,0 +1,8 @@
import usePromise from 'react-promise-suspense';
/**
* A more stable alternative to `use` using `usePromise`.
*/
export function useAwait<Type>(promise: Promise<Type>) {
return usePromise(async () => await promise, []);
}
+11
View File
@@ -0,0 +1,11 @@
import { useEffect, useRef } from 'react';
export function useDelta(value: number) {
const lastValue = useRef(value);
useEffect(() => {
lastValue.current = value;
});
return value - lastValue.current;
}
@@ -0,0 +1,35 @@
import { sumBy } from 'lodash';
import { use, useMemo } from 'react';
import * as Duel from '../../packages/website/src/stores/duel';
import {
TankDefinition,
tankDefinitions,
} from '../core/blitzkit/tankDefinitions';
export enum UseEccentricityMode {
Tier,
Class,
}
export function useEccentricity(mode: UseEccentricityMode) {
const awaitedTankDefinitions = use(tankDefinitions);
const tank = Duel.use((state) => state.protagonist.tank);
const similarTanks = useMemo(
() =>
Object.values(awaitedTankDefinitions).filter(
(tankB) =>
tank.tier === tankB.tier &&
(mode === UseEccentricityMode.Class
? tank.class === tankB.class
: true),
),
[tank.id],
);
function eccentricity(property: (tank: TankDefinition) => number) {
const average = sumBy(similarTanks, property) / similarTanks.length;
return property(tank) / average;
}
return eccentricity;
}
@@ -0,0 +1,30 @@
import { use, useMemo } from 'react';
import * as Duel from '../../packages/website/src/stores/duel';
import { equipmentDefinitions } from '../core/blitzkit/equipmentDefinitions';
export function useEquipment(id: number, antagonist = false) {
const awaitedEquipmentDefinitions = use(equipmentDefinitions);
const member = Duel.use(
(state) => state[antagonist ? 'antagonist' : 'protagonist']!,
);
const equipmentRows =
awaitedEquipmentDefinitions.presets[member.tank.equipment];
const equipmentMatrix = Duel.use(
(state) =>
state[antagonist ? 'antagonist' : 'protagonist']!.equipmentMatrix,
);
const value = useMemo(
() =>
equipmentRows.some((equipmentRow, rowIndex) => {
return equipmentRow.some((equipment, columnIndex) => {
const choice = equipmentMatrix[rowIndex][columnIndex];
if (choice === 0) return false;
const equipped = equipment[choice === -1 ? 0 : 1];
return equipped === id;
});
}),
[equipmentMatrix, member.tank],
);
return value;
}
@@ -0,0 +1,7 @@
import { equipmentDefinitions } from '../core/blitzkit/equipmentDefinitions';
import { useAwait } from './useAwait';
export function useEquipmentPreset(preset: string) {
const awaitedEquipmentDefinitions = useAwait(equipmentDefinitions);
return awaitedEquipmentDefinitions.presets[preset];
}
@@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';
export function useFullScreen() {
const [isFullScreen, setIsFullScreen] = useState(
typeof document !== 'undefined' && document.fullscreenElement !== null,
);
useEffect(() => {
function handleFullScreenChange() {
setIsFullScreen(document.fullscreenElement !== null);
}
document.addEventListener('fullscreenchange', handleFullScreenChange);
return () => {
document.removeEventListener('fullscreenchange', handleFullScreenChange);
};
}, []);
return isFullScreen;
}
@@ -0,0 +1,11 @@
import { useLayoutEffect, useState } from 'react';
export function useFullscreenAvailability() {
const [available, setAvailable] = useState(false);
useLayoutEffect(() => {
setAvailable(document.fullscreenEnabled);
}, []);
return available;
}
@@ -0,0 +1,29 @@
import { RefObject, useLayoutEffect, useRef } from 'react';
interface UseIntersectionOptions extends IntersectionObserverInit {
disabled: boolean;
}
export function useIntersection(
callback: () => void,
ref: RefObject<HTMLElement>,
options?: UseIntersectionOptions,
) {
const observer = useRef<IntersectionObserver>();
useLayoutEffect(() => {
if (options?.disabled || !ref.current) return;
const rect = ref.current.getBoundingClientRect();
const visible = !(rect.bottom < 0 || rect.top - window.innerHeight >= 0);
if (visible) callback();
observer.current = new IntersectionObserver(callback, options);
observer.current.observe(ref.current);
return () => observer.current!.disconnect();
}, []);
return observer;
}
+28
View File
@@ -0,0 +1,28 @@
import { ObjectMap, useLoader } from '@react-three/fiber';
import { Mesh, MeshStandardMaterial } from 'three';
import { GLTF, GLTFLoader } from 'three-stdlib';
import { asset } from '../core/blitzkit/asset';
const cache: Record<
number,
{
gltf: GLTF & ObjectMap;
hasPbr: boolean;
}
> = {};
export function useModel(id: number) {
const gltf = useLoader(GLTFLoader, asset(`3d/tanks/models/${id}.glb`));
if (!cache[id]) {
cache[id] = {
gltf,
hasPbr: Object.values(gltf.nodes).some(
(node) =>
node instanceof Mesh &&
node.material instanceof MeshStandardMaterial &&
node.material.roughnessMap !== null,
),
};
}
return cache[id];
}
+26
View File
@@ -0,0 +1,26 @@
import { RefObject, useEffect, useRef, useState } from 'react';
/**
* Thanks GuCier!
*
* https://stackoverflow.com/a/65008608/12294756
*/
export default function useOnScreen(ref: RefObject<HTMLElement>) {
const [isIntersecting, setIntersecting] = useState(false);
const observer = useRef<IntersectionObserver>();
useEffect(() => {
observer.current = new IntersectionObserver(([entry]) =>
setIntersecting(entry.isIntersecting),
);
}, [ref]);
useEffect(() => {
if (!ref.current) return;
observer.current?.observe(ref.current);
return () => observer.current?.disconnect();
}, [ref]);
return isIntersecting;
}
@@ -0,0 +1,6 @@
import * as Duel from '../../packages/website/src/stores/duel';
export function useProvision(id: number) {
const provisions = Duel.use((state) => state.protagonist.provisions);
return provisions.includes(id);
}
@@ -0,0 +1,7 @@
'use client';
import * as TankopediaEphemeral from '../../packages/website/src/stores/tankopediaEphemeral';
export function useTankModelDefinition() {
return TankopediaEphemeral.use((state) => state.model);
}
@@ -0,0 +1,83 @@
import { RefObject, useEffect } from 'react';
import { Euler, Group, Vector3 } from 'three';
import { degToRad } from 'three/src/math/MathUtils';
import { DuelMember } from '../../packages/website/src/stores/duel';
import { I_HAT, J_HAT, K_HAT } from '../constants/axis';
import { correctZYTuple } from '../core/blitz/correctZYTuple';
import {
ModelTransformEventData,
modelTransformEvent,
} from '../core/blitzkit/modelTransform';
import { useTankModelDefinition } from './useTankModelDefinition';
export function useTankTransform(
member: DuelMember,
turretContainer: RefObject<Group>,
gunContainer: RefObject<Group>,
) {
const tankModelDefinition = useTankModelDefinition();
useEffect(() => {
const trackModelDefinition = tankModelDefinition.tracks[member.track.id];
const turretModelDefinition = tankModelDefinition.turrets[member.turret.id];
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
const turretOrigin = correctZYTuple(tankModelDefinition.turretOrigin);
const gunOrigin = correctZYTuple(turretModelDefinition.gunOrigin);
const turretPosition = new Vector3();
const turretRotation = new Euler();
const gunPosition = new Vector3();
const gunRotation = new Euler();
function handleModelTransform({ yaw, pitch }: ModelTransformEventData) {
gunPosition
.set(0, 0, 0)
.sub(hullOrigin)
.sub(turretOrigin)
.sub(gunOrigin)
.applyAxisAngle(I_HAT, pitch)
.add(gunOrigin)
.add(turretOrigin)
.add(hullOrigin);
gunRotation.set(pitch, 0, 0);
gunContainer.current?.position.copy(gunPosition);
gunContainer.current?.rotation.copy(gunRotation);
if (yaw === undefined) return;
turretPosition
.set(0, 0, 0)
.sub(hullOrigin)
.sub(turretOrigin)
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
turretRotation.set(0, 0, yaw);
if (tankModelDefinition.turretRotation) {
const initialPitch = -degToRad(
tankModelDefinition.turretRotation.pitch,
);
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
const initialRoll = -degToRad(tankModelDefinition.turretRotation.roll);
turretPosition
.applyAxisAngle(I_HAT, initialPitch)
.applyAxisAngle(J_HAT, initialRoll)
.applyAxisAngle(K_HAT, initialYaw);
turretRotation.x += initialPitch;
turretRotation.y += initialRoll;
turretRotation.z += initialYaw;
}
turretPosition.add(turretOrigin).add(hullOrigin);
turretContainer.current?.position.copy(turretPosition);
turretContainer.current?.rotation.copy(turretRotation);
}
handleModelTransform(member);
modelTransformEvent.on(handleModelTransform);
return () => {
modelTransformEvent.off(handleModelTransform);
};
}, [member]);
}
@@ -0,0 +1,44 @@
import { useEffect, useState } from 'react';
import * as App from '../../packages/website/src/stores/app';
import { TankVotes } from '../app/api/tank-voting/[id]/average/route';
import { unwrapBlitzkitResponse } from '../core/blitzkit/unwrapBlitzkitResponse';
export interface BlitzkitResponseError {
status: 'error';
error: string;
message?: unknown;
}
export type BlitzkitResponse<Data = undefined> =
| BlitzkitResponseError
| {
status: 'ok';
data: Data;
};
const cache: Record<string, Promise<TankVotes> | TankVotes> = {};
export function useTankVotes(id: number) {
const wargaming = App.use((state) => state.logins.wargaming);
const [votes, setVotes] = useState<TankVotes | null>(null);
useEffect(() => {
if (id in cache) {
Promise.all([cache[id]]).then(([votes]) => setVotes(votes));
} else {
const cacheable = fetch(
`/api/tank-voting/${id}/average${
wargaming ? `?player=${wargaming.id}&token=${wargaming.token}` : ''
}`,
)
.then(
(response) => response.json() as Promise<BlitzkitResponse<TankVotes>>,
)
.then(unwrapBlitzkitResponse);
cacheable.then(setVotes);
cache[id] = cacheable;
}
}, [id, wargaming]);
return votes;
}
+6
View File
@@ -0,0 +1,6 @@
/**
* Discriminator for user caused errors to not report in logs
*/
export class UserError {
constructor(public message: string) {}
}