mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 11:39:50 +02:00
feat: start of monorepo
This commit is contained in:
1 parent
14f39d71ac
commit
73c7bdf0c0
260 files changed
+1236
-1235
No files matched your search
@@ -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]);
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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, []);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Discriminator for user caused errors to not report in logs
|
||||
*/
|
||||
export class UserError {
|
||||
constructor(public message: string) {}
|
||||
}
|
||||
Reference in new issue
Block a user