mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-11 11:46:10 +01:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b82e905757 | ||
|
|
c42e5b7dc3 | ||
|
|
b716218ecd | ||
|
|
511346e4bb | ||
|
|
5c2e49453e | ||
|
|
5d397de766 | ||
|
|
aa763f65d1 | ||
|
|
94cbd95509 | ||
|
|
f3762e356f | ||
|
|
5747d7549f | ||
|
|
aea4905d20 |
No files matched your search
@@ -22,6 +22,19 @@ env:
|
||||
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
|
||||
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
|
||||
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
|
||||
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
|
||||
ALLOW_ROBOTS: ${{ secrets.ALLOW_ROBOTS }}
|
||||
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
|
||||
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
|
||||
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
|
||||
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
|
||||
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
|
||||
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
|
||||
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
|
||||
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
|
||||
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
|
||||
PUBLIC_GUIDE_WEBSITE: ${{ secrets.PUBLIC_GUIDE_WEBSITE }}
|
||||
SERVICING: ${{ secrets.SERVICING }}
|
||||
|
||||
jobs:
|
||||
host:
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# BlitzKit 1.27.18
|
||||
|
||||
## Features
|
||||
|
||||
- Added a mini-game to BlitzKit similar to "Guess Who's That Pokemon" but with tanks: http://blitzkit.vercel.app/guess
|
||||
- Switched to a new renderer for better lighting for the Tankopedia
|
||||
|
||||
## Fixes
|
||||
|
||||
- The loading bar for the Tankopedia sandbox now shows up again
|
||||
- Removed a lot of dead code, improving performance
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "blitzkit",
|
||||
"private": true,
|
||||
"version": "1.27.17",
|
||||
"version": "1.27.18",
|
||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||
"type": "module",
|
||||
"types": "./types.d.ts",
|
||||
|
||||
@@ -861,6 +861,13 @@
|
||||
},
|
||||
|
||||
"tools": {
|
||||
"mixer": {
|
||||
"name": "Mixer",
|
||||
"button": "Mix and match",
|
||||
"description": "Mix tank modules",
|
||||
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz"
|
||||
},
|
||||
|
||||
"guess": {
|
||||
"name": "Guess the tank",
|
||||
"button": "Guess away",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 490 KiB After Width: | Height: | Size: 669 KiB |
+7
-13
@@ -80,7 +80,7 @@ export function PrimaryArmorSceneComponent({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function handleShellChange() {
|
||||
function handleShellChange() {
|
||||
const duel = duelStore.getState();
|
||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
@@ -97,14 +97,8 @@ export function PrimaryArmorSceneComponent({
|
||||
material.uniforms.damage.value = shell.armor_damage;
|
||||
material.uniforms.explosionRadius.value = shell.explosion_radius;
|
||||
|
||||
await handleProtagonistEquipmentChange(
|
||||
duel.protagonist.equipmentMatrix,
|
||||
true,
|
||||
);
|
||||
await handleAntagonistEquipmentChange(
|
||||
duel.antagonist.equipmentMatrix,
|
||||
true,
|
||||
);
|
||||
handleProtagonistEquipmentChange(duel.protagonist.equipmentMatrix, true);
|
||||
handleAntagonistEquipmentChange(duel.antagonist.equipmentMatrix, true);
|
||||
|
||||
invalidate();
|
||||
}
|
||||
@@ -121,12 +115,12 @@ export function PrimaryArmorSceneComponent({
|
||||
function handleWireframeChange(wireframe: boolean) {
|
||||
material.wireframe = wireframe;
|
||||
}
|
||||
async function handleProtagonistEquipmentChange(
|
||||
function handleProtagonistEquipmentChange(
|
||||
equipment: EquipmentMatrix,
|
||||
noInvalidate = false,
|
||||
) {
|
||||
const duel = duelStore.getState();
|
||||
const hasEnhancedArmor = await hasEquipment(
|
||||
const hasEnhancedArmor = hasEquipment(
|
||||
110,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
@@ -137,7 +131,7 @@ export function PrimaryArmorSceneComponent({
|
||||
|
||||
if (!noInvalidate) invalidate();
|
||||
}
|
||||
async function handleAntagonistEquipmentChange(
|
||||
function handleAntagonistEquipmentChange(
|
||||
equipment: EquipmentMatrix,
|
||||
noInvalidate = false,
|
||||
) {
|
||||
@@ -145,7 +139,7 @@ export function PrimaryArmorSceneComponent({
|
||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
const penetration = shell.penetration.near;
|
||||
const hasCalibratedShells = await hasEquipment(
|
||||
const hasCalibratedShells = hasEquipment(
|
||||
103,
|
||||
duel.antagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Path,
|
||||
Quaternion,
|
||||
} from 'three';
|
||||
import { isHalloween } from '../../../core/blitzkit/isHalloween';
|
||||
import { LocaleProvider, useLocale } from '../../../hooks/useLocale';
|
||||
import {
|
||||
TankopediaEphemeral,
|
||||
@@ -72,11 +73,8 @@ export function ShotDisplay() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const data = new Date();
|
||||
const isHalloween = data.getMonth() === 9 && data.getDate() === 31;
|
||||
|
||||
if (
|
||||
isHalloween &&
|
||||
isHalloween() &&
|
||||
shot?.splashRadius !== undefined &&
|
||||
shot.in.status === 'penetration'
|
||||
) {
|
||||
|
||||
+4
-6
@@ -54,11 +54,9 @@ export function SpacedArmorSubExternal({
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
material.uniforms.penetration.value = shell.penetration.near;
|
||||
}
|
||||
async function handleProtagonistEquipmentChange(
|
||||
equipment: EquipmentMatrix,
|
||||
) {
|
||||
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||
const duel = duelStore.getState();
|
||||
const hasEnhancedArmor = await hasEquipment(
|
||||
const hasEnhancedArmor = hasEquipment(
|
||||
110,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
@@ -67,12 +65,12 @@ export function SpacedArmorSubExternal({
|
||||
? thickness * 1.03
|
||||
: thickness;
|
||||
}
|
||||
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||
const duel = duelStore.getState();
|
||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
const penetration = shell.penetration.near;
|
||||
const hasCalibratedShells = await hasEquipment(
|
||||
const hasCalibratedShells = hasEquipment(
|
||||
103,
|
||||
duel.antagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
|
||||
+4
-6
@@ -64,11 +64,9 @@ export function SpacedArmorSubSpaced({
|
||||
shell.normalization ?? 0,
|
||||
);
|
||||
}
|
||||
async function handleProtagonistEquipmentChange(
|
||||
equipment: EquipmentMatrix,
|
||||
) {
|
||||
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||
const duel = duelStore.getState();
|
||||
const hasEnhancedArmor = await hasEquipment(
|
||||
const hasEnhancedArmor = hasEquipment(
|
||||
110,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
@@ -77,12 +75,12 @@ export function SpacedArmorSubSpaced({
|
||||
? thickness * 1.03
|
||||
: thickness;
|
||||
}
|
||||
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||
const duel = duelStore.getState();
|
||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
const penetration = shell.penetration.near;
|
||||
const hasCalibratedShells = await hasEquipment(
|
||||
const hasCalibratedShells = hasEquipment(
|
||||
103,
|
||||
duel.antagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
|
||||
+8
-10
@@ -81,7 +81,7 @@ export function SpacedArmorSceneComponent({
|
||||
const camera = useThree((state) => state.camera);
|
||||
|
||||
const shoot = useCallback(
|
||||
async (
|
||||
(
|
||||
point: Vector3,
|
||||
intersections: Intersection[],
|
||||
allowRicochet: boolean,
|
||||
@@ -103,12 +103,12 @@ export function SpacedArmorSceneComponent({
|
||||
},
|
||||
};
|
||||
|
||||
const hasCalibratedShells = await hasEquipment(
|
||||
const hasCalibratedShells = hasEquipment(
|
||||
103,
|
||||
antagonist.tank.equipment_preset,
|
||||
antagonist.equipmentMatrix,
|
||||
);
|
||||
const hasEnhancedArmor = await hasEquipment(
|
||||
const hasEnhancedArmor = hasEquipment(
|
||||
110,
|
||||
protagonist.tank.equipment_preset,
|
||||
protagonist.equipmentMatrix,
|
||||
@@ -340,7 +340,7 @@ export function SpacedArmorSceneComponent({
|
||||
props.scene.children,
|
||||
true,
|
||||
);
|
||||
const outShot = await shoot(
|
||||
const outShot = shoot(
|
||||
lastLayer.point,
|
||||
ricochetIntersections,
|
||||
false,
|
||||
@@ -386,13 +386,11 @@ export function SpacedArmorSceneComponent({
|
||||
thickness,
|
||||
} satisfies ArmorUserData
|
||||
}
|
||||
onClick={async (event) => {
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const shot = (await shoot(
|
||||
event.point,
|
||||
event.intersections,
|
||||
true,
|
||||
))!;
|
||||
|
||||
const shot = shoot(event.point, event.intersections, true)!;
|
||||
|
||||
tankopediaEphemeralStore.setState({ shot });
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -30,7 +30,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
const wrapper = useRef<Group>(null);
|
||||
const turretContainer = useRef<Group>(null);
|
||||
const gunContainer = useRef<Group>(null);
|
||||
const protagonist = Duel.use((draft) => draft.protagonist);
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const track = Duel.use((state) => state.protagonist.track);
|
||||
const turret = Duel.use((state) => state.protagonist.turret);
|
||||
@@ -46,7 +45,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
||||
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
||||
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const canvas = useThree((state) => state.gl.domElement);
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const duelStore = Duel.useStore();
|
||||
@@ -167,14 +165,14 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
async function handlePointerMove(event: PointerEvent) {
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
@@ -261,14 +259,14 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
async function handlePointerMove(event: PointerEvent) {
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
@@ -342,14 +340,14 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
async function handlePointerMove(event: PointerEvent) {
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
FrontSide,
|
||||
LineBasicMaterial,
|
||||
Mesh,
|
||||
MeshStandardMaterial,
|
||||
MeshBasicMaterial,
|
||||
Object3D,
|
||||
Plane,
|
||||
Quaternion,
|
||||
@@ -108,7 +108,7 @@ export function StaticArmorSceneComponent({
|
||||
|
||||
const surfaceMaterial = useMemo(
|
||||
() =>
|
||||
new MeshStandardMaterial({
|
||||
new MeshBasicMaterial({
|
||||
color,
|
||||
opacity,
|
||||
transparent: opacity < 1,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { PlusIcon, TrashIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Dialog, Flex, SegmentedControl } from '@radix-ui/themes';
|
||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
@@ -16,10 +15,7 @@ interface ControlsProps {
|
||||
onAddTankDialogOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const [provisionDefinitions, modelDefinitions] = await Promise.all([
|
||||
awaitableProvisionDefinitions,
|
||||
awaitableModelDefinitions,
|
||||
]);
|
||||
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||
|
||||
export function Controls({
|
||||
addTankDialogOpen,
|
||||
@@ -60,11 +56,9 @@ export function Controls({
|
||||
<TankSearch
|
||||
compact
|
||||
onSelect={(tank) => {
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
|
||||
mutateCompareEphemeral((draft) => {
|
||||
draft.members.push(
|
||||
tankToCompareMember(tank, model, provisionDefinitions),
|
||||
tankToCompareMember(tank, provisionDefinitions),
|
||||
);
|
||||
draft.sorting = undefined;
|
||||
});
|
||||
@@ -74,13 +68,7 @@ export function Controls({
|
||||
mutateCompareEphemeral((draft) => {
|
||||
draft.members.push(
|
||||
...tanks.map((tank) => {
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
|
||||
return tankToCompareMember(
|
||||
tank,
|
||||
model,
|
||||
provisionDefinitions,
|
||||
);
|
||||
return tankToCompareMember(tank, provisionDefinitions);
|
||||
}),
|
||||
);
|
||||
draft.sorting = undefined;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
|
||||
import { Dialog, Flex, IconButton } from '@radix-ui/themes';
|
||||
import { useState } from 'react';
|
||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
@@ -14,10 +13,7 @@ interface TankControlProps {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const [provisionDefinitions, modelDefinitions] = await Promise.all([
|
||||
awaitableProvisionDefinitions,
|
||||
awaitableModelDefinitions,
|
||||
]);
|
||||
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||
|
||||
export function TankControl({ index, slug }: TankControlProps) {
|
||||
const { locale } = useLocale();
|
||||
@@ -61,12 +57,9 @@ export function TankControl({ index, slug }: TankControlProps) {
|
||||
<TankSearch
|
||||
compact
|
||||
onSelect={(tank) => {
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
|
||||
mutateCompareEphemeral((draft) => {
|
||||
draft.members[index] = tankToCompareMember(
|
||||
tank,
|
||||
model,
|
||||
provisionDefinitions,
|
||||
);
|
||||
draft.sorting = undefined;
|
||||
|
||||
@@ -22,8 +22,7 @@ export function GuessBackground() {
|
||||
|
||||
useEffect(() => {
|
||||
mutateDuel((state) => {
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
state.protagonist = tankToDuelMember(tank, model, provisionDefinitions);
|
||||
state.protagonist = tankToDuelMember(tank, provisionDefinitions);
|
||||
});
|
||||
mutateTankopediaEphemeral((state) => {
|
||||
state.model = modelDefinitions.models[tank.id];
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { GuessEphemeral } from '../stores/guessEphemeral';
|
||||
import { TankopediaDisplay } from '../stores/tankopediaPersistent/constants';
|
||||
import { SmartCanvas } from './SmartCanvas';
|
||||
import { Controls } from './Tankopedia/HeroSection/components/TankSandbox/components/Control';
|
||||
import { Lighting } from './Tankopedia/HeroSection/components/TankSandbox/components/Lighting';
|
||||
import { TankModel } from './Tankopedia/HeroSection/components/TankSandbox/components/TankModel';
|
||||
|
||||
const BRIGHTNESS = 2 ** -5;
|
||||
const BRIGHTNESS = 2 ** -5.5;
|
||||
const CONTRAST = 2 ** 2;
|
||||
|
||||
export function GuessRenderer() {
|
||||
@@ -16,14 +15,13 @@ export function GuessRenderer() {
|
||||
<SmartCanvas
|
||||
style={{
|
||||
filter: `grayscale(${isRevealed ? 0 : 1}) contrast(${isRevealed ? 1 : CONTRAST}) brightness(${isRevealed ? 1 : BRIGHTNESS})`,
|
||||
// filter: `drop-shadow(0 0 1rem ${Var('black-a11')})`,
|
||||
transitionDuration: isRevealed ? '2s' : undefined,
|
||||
transitionProperty: 'filter',
|
||||
}}
|
||||
camera={{ position: [-5, 5, -5] }}
|
||||
>
|
||||
<Controls distanceScale={2} zoomable={false} />
|
||||
<Lighting display={TankopediaDisplay.Model} />
|
||||
<Controls zoomable={false} />
|
||||
<Lighting />
|
||||
<TankModel />
|
||||
</SmartCanvas>
|
||||
);
|
||||
|
||||
@@ -138,7 +138,7 @@ export function Guesser() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Flex justify="center">
|
||||
<Flex justify="center" style={{ userSelect: 'none' }}>
|
||||
<Text>
|
||||
{literals(strings.website.tools.guess.stats, [
|
||||
`${correctGuesses}`,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { SpotLightProps } from '@react-three/fiber';
|
||||
import { memo } from 'react';
|
||||
import { Color } from 'three';
|
||||
import { isHalloween } from '../core/blitzkit/isHalloween';
|
||||
|
||||
interface Props extends SpotLightProps {
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
export const HelpingSpotLight = memo<Props>(({ debug, ...props }) => {
|
||||
const color = new Color();
|
||||
|
||||
if (isHalloween()) color.set(1, 0.25, 0.25);
|
||||
|
||||
return (
|
||||
<>
|
||||
{debug && (
|
||||
<mesh position={props.position}>
|
||||
<icosahedronGeometry args={[0.1, 2]} />
|
||||
<meshBasicMaterial color={props.color} />
|
||||
</mesh>
|
||||
)}
|
||||
|
||||
<spotLight color={color} {...props} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import { awaitableModelDefinitions } from '../../../core/awaitables/modelDefinitions';
|
||||
import { jsxTree } from '../../../core/blitzkit/jsxTree';
|
||||
import { useModel } from '../../../hooks/useModel';
|
||||
import { MixerEphemeral } from '../../../stores/mixerEphemeral';
|
||||
|
||||
const modelDefinitions = await awaitableModelDefinitions;
|
||||
|
||||
export function Model() {
|
||||
const hull = MixerEphemeral.use((state) => state.hull);
|
||||
const turret = MixerEphemeral.use((state) => state.turret);
|
||||
const gun = MixerEphemeral.use((state) => state.gun);
|
||||
|
||||
const hullGltf = useModel(hull.id);
|
||||
const turretGltf = useModel(turret.tank.id);
|
||||
const gunGltf = useModel(gun.tank.id);
|
||||
|
||||
const hullNodes = Object.values(hullGltf.gltf.nodes);
|
||||
const turretNodes = Object.values(turretGltf.gltf.nodes);
|
||||
const gunNodes = Object.values(gunGltf.gltf.nodes);
|
||||
|
||||
const tankModel = modelDefinitions.models[hull.id];
|
||||
const trackModel = tankModel.tracks[hull.tracks.at(-1)!.id];
|
||||
|
||||
const turretTankModel = modelDefinitions.models[turret.tank.id];
|
||||
const turretTrackModel =
|
||||
turretTankModel.tracks[turret.tank.tracks.at(-1)!.id];
|
||||
const turretModel = turretTankModel.turrets[turret.turret.id];
|
||||
|
||||
const gunTankModel = modelDefinitions.models[gun.tank.id];
|
||||
const gunTrackModel = gunTankModel.tracks[gun.tank.tracks.at(-1)!.id];
|
||||
const gunTurretModel = gunTankModel.turrets[gun.turret.id];
|
||||
const gunModel = gunTurretModel.guns[gun.gun.id];
|
||||
|
||||
return (
|
||||
<group rotation={[-Math.PI / 2, 0, 0]}>
|
||||
{hullNodes.map((node) => {
|
||||
const isHull = node.name === 'hull';
|
||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
||||
const isTrack = node.name.startsWith('chassis_track_');
|
||||
const isVisible = isHull || isWheel || isTrack;
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return jsxTree(node, {
|
||||
mesh(_, props, key) {
|
||||
return <mesh {...props} key={key} castShadow receiveShadow />;
|
||||
},
|
||||
});
|
||||
})}
|
||||
|
||||
<group
|
||||
position={[
|
||||
tankModel.turret_origin.x +
|
||||
trackModel.origin.x -
|
||||
turretTankModel.turret_origin.x -
|
||||
turretTrackModel.origin.x,
|
||||
tankModel.turret_origin.z +
|
||||
trackModel.origin.z -
|
||||
turretTankModel.turret_origin.z -
|
||||
turretTrackModel.origin.z,
|
||||
tankModel.turret_origin.y +
|
||||
trackModel.origin.y -
|
||||
turretTankModel.turret_origin.y -
|
||||
turretTrackModel.origin.y,
|
||||
]}
|
||||
>
|
||||
{turretNodes.map((node) => {
|
||||
const isCurrentTurret =
|
||||
node.name ===
|
||||
`turret_${turretModel.model_id.toString().padStart(2, '0')}`;
|
||||
const isVisible = isCurrentTurret;
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return jsxTree(node, {
|
||||
mesh(_, props, key) {
|
||||
return <mesh {...props} key={key} castShadow receiveShadow />;
|
||||
},
|
||||
});
|
||||
})}
|
||||
|
||||
<group
|
||||
position={[
|
||||
turretTankModel.turret_origin.x +
|
||||
turretTrackModel.origin.x +
|
||||
turretModel.gun_origin.x -
|
||||
gunTankModel.turret_origin.x -
|
||||
gunTrackModel.origin.x -
|
||||
gunTurretModel.gun_origin.x,
|
||||
turretTankModel.turret_origin.z +
|
||||
turretTrackModel.origin.z +
|
||||
turretModel.gun_origin.z -
|
||||
gunTankModel.turret_origin.z -
|
||||
gunTrackModel.origin.z -
|
||||
gunTurretModel.gun_origin.z,
|
||||
turretTankModel.turret_origin.y +
|
||||
turretTrackModel.origin.y +
|
||||
turretModel.gun_origin.y -
|
||||
gunTankModel.turret_origin.y -
|
||||
gunTrackModel.origin.y -
|
||||
gunTurretModel.gun_origin.y,
|
||||
]}
|
||||
>
|
||||
{gunNodes.map((node) => {
|
||||
const isCurrentMantlet =
|
||||
node.name ===
|
||||
`gun_${gunModel.model_id.toString().padStart(2, '0')}_mask`;
|
||||
const isCurrentGun =
|
||||
node.name ===
|
||||
`gun_${gunModel.model_id.toString().padStart(2, '0')}`;
|
||||
const isVisible = isCurrentGun || isCurrentMantlet;
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return jsxTree(node, {
|
||||
mesh(_, props, key) {
|
||||
return <mesh {...props} key={key} castShadow receiveShadow />;
|
||||
},
|
||||
});
|
||||
})}
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { OrbitControls } from '@react-three/drei';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { Suspense } from 'react';
|
||||
import { Model } from './components/Model';
|
||||
|
||||
export function MixerScene() {
|
||||
return (
|
||||
<Canvas
|
||||
shadows
|
||||
camera={{
|
||||
fov: 45,
|
||||
position: [5, 5, -5],
|
||||
}}
|
||||
>
|
||||
<OrbitControls />
|
||||
|
||||
<directionalLight position={[10, 10, 10]} intensity={5} castShadow />
|
||||
<directionalLight position={[-10, -10, -10]} intensity={3} castShadow />
|
||||
|
||||
<Suspense fallback={null}>
|
||||
<Model />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
+1
-30
@@ -1,14 +1,6 @@
|
||||
import {
|
||||
asset,
|
||||
coefficient,
|
||||
isExplosive,
|
||||
resolvePenetrationCoefficient,
|
||||
} from '@blitzkit/core';
|
||||
import { asset, isExplosive } from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n';
|
||||
import { Flex, Heading, IconButton, Tooltip } from '@radix-ui/themes';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
@@ -28,34 +20,13 @@ export function Firepower({
|
||||
}: StatsAcceptorProps & MaybeSkeletonComponentProps) {
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const { strings, unwrap } = useLocale();
|
||||
const [penetrationDistance, setPenetrationDistance] = useState(250);
|
||||
const setPenetrationDistanceDebounced = debounce((value: number) => {
|
||||
setPenetrationDistance(value);
|
||||
}, 500);
|
||||
const turret = Duel.use((state) => state.protagonist.turret);
|
||||
const gun = Duel.use((state) => state.protagonist.gun);
|
||||
const shell = Duel.use((state) => state.protagonist.shell);
|
||||
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
|
||||
const hasSupercharger = useEquipment(107);
|
||||
const hasCalibratedShells = useEquipment(103);
|
||||
const penetrationCoefficient = coefficient([
|
||||
hasCalibratedShells,
|
||||
resolvePenetrationCoefficient(true, shell.type) - 1,
|
||||
]);
|
||||
const penetrationLossOverDistanceCoefficient = coefficient([
|
||||
hasSupercharger,
|
||||
-0.6,
|
||||
]);
|
||||
const tankModelDefinition = useTankModelDefinition();
|
||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||
|
||||
useEffect(() => {
|
||||
if (penetrationDistanceInput.current) {
|
||||
penetrationDistanceInput.current.value = `${penetrationDistance}`;
|
||||
}
|
||||
}, [penetrationDistance]);
|
||||
|
||||
return (
|
||||
<StatsTableWrapper>
|
||||
<Flex align="center" gap="4">
|
||||
|
||||
+1
-6
@@ -80,12 +80,7 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
||||
relativeAgainst === TankopediaRelativeAgainst.All,
|
||||
)
|
||||
.map((tank) => {
|
||||
const thisTankModel = modelDefinitions.models[tank.id];
|
||||
const member = tankToDuelMember(
|
||||
tank,
|
||||
thisTankModel,
|
||||
provisionDefinitions,
|
||||
);
|
||||
const member = tankToDuelMember(tank, provisionDefinitions);
|
||||
|
||||
if (member.gun.shells[shellIndex] === undefined) {
|
||||
return undefined;
|
||||
|
||||
-1
@@ -21,7 +21,6 @@ const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
||||
]);
|
||||
|
||||
export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
|
||||
const provisions = Duel.use((state) => state.protagonist.provisions);
|
||||
const consumables = Duel.use((state) => state.protagonist.consumables);
|
||||
|
||||
-1
@@ -14,7 +14,6 @@ import { Duel } from '../../../../../../../stores/duel';
|
||||
import './index.css';
|
||||
|
||||
export function QuickInputs() {
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const protagonist = Duel.use((state) => state.protagonist);
|
||||
const yawInput = useRef<HTMLInputElement>(null);
|
||||
const pitchInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
+2
-2
@@ -13,10 +13,10 @@ export function Tracker({ fontSize }: Props) {
|
||||
<Box
|
||||
position="absolute"
|
||||
left="50%"
|
||||
bottom="0"
|
||||
top="50%"
|
||||
width="min(100%, 16rem)"
|
||||
style={{
|
||||
transform: `translate(-50%, calc(${fontSize} / 2))`,
|
||||
transform: `translate(-50%, calc(${fontSize} / 2 + var(--space-4)))`,
|
||||
}}
|
||||
>
|
||||
<Progress color="gray" value={progress} />
|
||||
|
||||
+11
-85
@@ -1,92 +1,19 @@
|
||||
import { TankType } from '@blitzkit/core';
|
||||
import { Flex, Heading } from '@radix-ui/themes';
|
||||
import { Var } from '../../../../../../core/radix/var';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import type { RadixColor } from '../../../../../../stores/embedState';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { Tracker } from './components/Tracker';
|
||||
|
||||
interface TitleProps {
|
||||
foreground?: boolean;
|
||||
}
|
||||
|
||||
export const NATION_COLORS: Record<
|
||||
string,
|
||||
{
|
||||
text: RadixColor;
|
||||
background: RadixColor[];
|
||||
tint: RadixColor;
|
||||
}
|
||||
> = {
|
||||
france: {
|
||||
text: 'mint',
|
||||
tint: 'purple',
|
||||
background: ['mint', 'cyan', 'jade'],
|
||||
},
|
||||
germany: {
|
||||
text: 'sage',
|
||||
tint: 'blue',
|
||||
background: ['slate', 'sage', 'mauve'],
|
||||
},
|
||||
usa: {
|
||||
text: 'gold',
|
||||
tint: 'red',
|
||||
background: ['brown', 'gold', 'gray'],
|
||||
},
|
||||
european: {
|
||||
text: 'sky',
|
||||
tint: 'violet',
|
||||
background: ['cyan', 'blue', 'indigo'],
|
||||
},
|
||||
ussr: {
|
||||
text: 'bronze',
|
||||
tint: 'red',
|
||||
background: ['bronze', 'tomato', 'bronze'],
|
||||
},
|
||||
uk: {
|
||||
text: 'gold',
|
||||
tint: 'red',
|
||||
background: ['bronze', 'gold', 'brown'],
|
||||
},
|
||||
japan: {
|
||||
text: 'mint',
|
||||
tint: 'violet',
|
||||
background: ['green', 'teal', 'jade'],
|
||||
},
|
||||
china: {
|
||||
text: 'mint',
|
||||
tint: 'pink',
|
||||
background: ['green', 'teal', 'jade'],
|
||||
},
|
||||
other: {
|
||||
text: 'amber',
|
||||
tint: 'sky',
|
||||
background: ['amber', 'yellow', 'gray'],
|
||||
},
|
||||
};
|
||||
|
||||
export function Title({ foreground }: TitleProps) {
|
||||
export function Title() {
|
||||
const { unwrap } = useLocale();
|
||||
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
||||
const revealed = TankopediaEphemeral.use((state) => state.revealed);
|
||||
const nationColors = NATION_COLORS[protagonist.nation];
|
||||
const color = Var(
|
||||
disturbed
|
||||
? protagonist.type === TankType.COLLECTOR
|
||||
? 'blue-11'
|
||||
: protagonist.type === TankType.PREMIUM
|
||||
? 'amber-11'
|
||||
: 'gray-12'
|
||||
: `${nationColors.text}-12`,
|
||||
);
|
||||
const name = unwrap(protagonist.name);
|
||||
const fontSize = revealed
|
||||
? disturbed
|
||||
? revealed
|
||||
? '2rem'
|
||||
: `min(65vh, ${125 / name.length}vw)`
|
||||
: `min(48vh, ${75 / name.length}vw)`;
|
||||
: `min(16vh, ${125 / name.length}vw)`
|
||||
: `min(12vh, ${75 / name.length}vw)`;
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
|
||||
return (
|
||||
@@ -95,19 +22,19 @@ export function Title({ foreground }: TitleProps) {
|
||||
event.preventDefault();
|
||||
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.disturbed = true;
|
||||
draft.revealed = true;
|
||||
});
|
||||
}}
|
||||
direction="column"
|
||||
position="absolute"
|
||||
align="center"
|
||||
justify="center"
|
||||
top={disturbed ? '6rem' : '50%'}
|
||||
top={revealed ? '6rem' : '50%'}
|
||||
left="50%"
|
||||
width="100%"
|
||||
height={disturbed ? fontSize : '100%'}
|
||||
height={revealed ? fontSize : '100%'}
|
||||
style={{
|
||||
pointerEvents: disturbed ? 'none' : undefined,
|
||||
pointerEvents: revealed ? 'none' : undefined,
|
||||
transitionDuration: '1s',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
@@ -119,11 +46,10 @@ export function Title({ foreground }: TitleProps) {
|
||||
pointerEvents: 'none',
|
||||
fontSize,
|
||||
whiteSpace: 'nowrap',
|
||||
color: foreground ? Var('gray-a10') : color,
|
||||
opacity: foreground && disturbed ? 0 : 1,
|
||||
letterSpacing: disturbed || !revealed ? 0 : '-0.03em',
|
||||
opacity: revealed ? 1 : 0.5,
|
||||
letterSpacing: revealed || !revealed ? 0 : '-0.03em',
|
||||
transition: `
|
||||
letter-spacing 1.5s ${disturbed ? '' : 'cubic-bezier(0.81, -2, 0.68, 1)'},
|
||||
letter-spacing 1.5s ${revealed ? '' : 'cubic-bezier(0.81, -2, 0.68, 1)'},
|
||||
font-size 1s,
|
||||
-webkit-text-stroke 2s,
|
||||
opacity 1s
|
||||
|
||||
+4
-6
@@ -24,7 +24,6 @@ interface ControlsProps {
|
||||
autoRotate?: boolean;
|
||||
zoomable?: boolean;
|
||||
enableRotate?: boolean;
|
||||
distanceScale?: number;
|
||||
}
|
||||
|
||||
export function Controls({
|
||||
@@ -32,7 +31,6 @@ export function Controls({
|
||||
autoRotate = true,
|
||||
zoomable = true,
|
||||
enableRotate = true,
|
||||
distanceScale = 1,
|
||||
}: ControlsProps) {
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const duelStore = Duel.useStore();
|
||||
@@ -86,7 +84,7 @@ export function Controls({
|
||||
-8,
|
||||
gunHeight + (naked ? 10 : 2),
|
||||
-13,
|
||||
).multiplyScalar(distanceScale);
|
||||
);
|
||||
|
||||
const t0 = useRef(Date.now() / 1000);
|
||||
useFrame(() => {
|
||||
@@ -111,14 +109,14 @@ export function Controls({
|
||||
},
|
||||
);
|
||||
|
||||
async function handlePoseEvent(event: Pose) {
|
||||
function handlePoseEvent(event: Pose) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
import { Environment } from '@react-three/drei';
|
||||
import { Euler } from 'three';
|
||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||
import { useModel } from '../../../../../../hooks/useModel';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
|
||||
|
||||
const LIGHTS_COUNT = 5;
|
||||
|
||||
interface LightingProps {
|
||||
display: TankopediaDisplay;
|
||||
}
|
||||
|
||||
export function Lighting({ display }: LightingProps) {
|
||||
const protagonist = Duel.use((state) => state.protagonist);
|
||||
const { hasPbr } = useModel(protagonist.tank.id);
|
||||
const isBrighterLighting =
|
||||
!hasPbr && display !== TankopediaDisplay.StaticArmor;
|
||||
|
||||
return (
|
||||
<>
|
||||
{display !== TankopediaDisplay.StaticArmor && (
|
||||
<Environment
|
||||
files="https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/winter_river_1k.hdr"
|
||||
environmentIntensity={isBrighterLighting ? 1.5 : 1.25}
|
||||
environmentRotation={new Euler(0, degToRad(180), 0)}
|
||||
/>
|
||||
)}
|
||||
{display === TankopediaDisplay.StaticArmor && (
|
||||
<Environment preset="lobby" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { useHelper } from '@react-three/drei';
|
||||
import { times } from 'lodash-es';
|
||||
import { useRef } from 'react';
|
||||
import { SpotLight, SpotLightHelper } from 'three';
|
||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||
import { HelpingSpotLight } from '../../../../../../HelpingSpotLight';
|
||||
|
||||
const LIGHTS_COUNT = 5;
|
||||
const LIGHT_DISTANCE = 13;
|
||||
const LIGHT_HEIGHT_0 = 4;
|
||||
const LIGHT_HEIGHT_1 = 2;
|
||||
const INTENSITY = 30;
|
||||
|
||||
export function Lighting() {
|
||||
const topLight = useRef<SpotLight>(null!);
|
||||
const frontLight = useRef<SpotLight>(null!);
|
||||
|
||||
useHelper(topLight, SpotLightHelper, 'red');
|
||||
useHelper(frontLight, SpotLightHelper, 'green');
|
||||
|
||||
return (
|
||||
<>
|
||||
{times(LIGHTS_COUNT, (index) => {
|
||||
const x = index / (LIGHTS_COUNT - 1);
|
||||
const theta = 2 * Math.PI * (index / LIGHTS_COUNT) - degToRad(152);
|
||||
const position = [
|
||||
LIGHT_DISTANCE * Math.sin(theta),
|
||||
LIGHT_HEIGHT_0 * (1 - x) + LIGHT_HEIGHT_1 * x,
|
||||
LIGHT_DISTANCE * Math.cos(theta),
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<HelpingSpotLight
|
||||
position={position}
|
||||
intensity={INTENSITY}
|
||||
penumbra={1}
|
||||
castShadow
|
||||
decay={1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
+12
-224
@@ -1,240 +1,28 @@
|
||||
import { asset, I_HAT, J_HAT } from '@blitzkit/core';
|
||||
import { useFrame, useLoader } from '@react-three/fiber';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { useRef } from 'react';
|
||||
import {
|
||||
Group,
|
||||
MeshStandardMaterial,
|
||||
PerspectiveCamera,
|
||||
TextureLoader,
|
||||
Vector2,
|
||||
Vector3,
|
||||
} from 'three';
|
||||
import { GLTFLoader } from 'three/examples/jsm/Addons.js';
|
||||
import { degToRad, radToDeg } from 'three/src/math/MathUtils.js';
|
||||
import { awaitableEquipmentDefinitions } from '../../../../../../../core/awaitables/equipmentDefinitions';
|
||||
import { awaitableModelDefinitions } from '../../../../../../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../../../../../../core/awaitables/provisionDefinitions';
|
||||
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
|
||||
import { modelTransformEvent } from '../../../../../../../core/blitzkit/modelTransform';
|
||||
import { tankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
|
||||
import { Duel } from '../../../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../../../stores/tankopediaEphemeral';
|
||||
import { aimTarget } from './constants';
|
||||
import { MeshStandardMaterial } from 'three';
|
||||
|
||||
const [modelDefinitions, equipmentDefinitions, provisionDefinitions] =
|
||||
await Promise.all([
|
||||
awaitableModelDefinitions,
|
||||
awaitableEquipmentDefinitions,
|
||||
awaitableProvisionDefinitions,
|
||||
]);
|
||||
|
||||
const emptyVector = new Vector2();
|
||||
const SIZE = 2 ** 5;
|
||||
|
||||
export function SceneProps() {
|
||||
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
||||
const targetCircleWrapper = useRef<Group>(null);
|
||||
const clientTargetCircle = useRef<HTMLDivElement>(null);
|
||||
const serverTargetCircle = useRef<HTMLDivElement>(null);
|
||||
const material = useRef<MeshStandardMaterial>(null);
|
||||
const playground = useRef<Group>(null);
|
||||
|
||||
const turret = Duel.use((state) => state.protagonist.turret);
|
||||
const track = Duel.use((state) => state.protagonist.track);
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const gun = Duel.use((state) => state.protagonist.gun);
|
||||
const shell = Duel.use((state) => state.protagonist.shell);
|
||||
const engine = Duel.use((state) => state.protagonist.engine);
|
||||
const tankModelDefinition = modelDefinitions.models[tank.id];
|
||||
const trackModelDefinition =
|
||||
modelDefinitions.models[tank.id].tracks[track.id];
|
||||
const gunModelDefinition =
|
||||
tankModelDefinition.turrets[turret.id].guns[gun.id];
|
||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||
const hullOrigin = new Vector3(
|
||||
trackModelDefinition.origin.x,
|
||||
trackModelDefinition.origin.y,
|
||||
-trackModelDefinition.origin.z,
|
||||
);
|
||||
const turretOrigin = new Vector3(
|
||||
tankModelDefinition.turret_origin.x,
|
||||
tankModelDefinition.turret_origin.y,
|
||||
-tankModelDefinition.turret_origin.z,
|
||||
);
|
||||
const gunOriginOnlyY = new Vector3(0, turretModelDefinition.gun_origin.y, 0);
|
||||
const shellOrigin = hullOrigin.clone().add(turretOrigin).add(gunOriginOnlyY);
|
||||
const camouflage = Duel.use((state) => state.protagonist.camouflage);
|
||||
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
|
||||
const consumables = Duel.use((state) => state.protagonist.consumables);
|
||||
const provisions = Duel.use((state) => state.protagonist.provisions);
|
||||
const assaultDistance = Duel.use(
|
||||
(state) => state.protagonist.assaultDistance,
|
||||
);
|
||||
const equipmentMatrix = Duel.use(
|
||||
(state) => state.protagonist.equipmentMatrix,
|
||||
);
|
||||
const characteristics = tankCharacteristics(
|
||||
{
|
||||
tank,
|
||||
turret,
|
||||
gun,
|
||||
shell,
|
||||
consumables,
|
||||
applyDynamicArmor: false,
|
||||
applyReactiveArmor: false,
|
||||
applySpallLiner: false,
|
||||
camouflage,
|
||||
crewSkills,
|
||||
engine,
|
||||
equipmentMatrix,
|
||||
provisions,
|
||||
stockEngine: tank.engines[0],
|
||||
stockGun: tank.turrets[0].guns[0],
|
||||
stockTurret: tank.turrets[0],
|
||||
stockTrack: tank.tracks[0],
|
||||
track,
|
||||
assaultDistance,
|
||||
},
|
||||
{ equipmentDefinitions, provisionDefinitions, tankModelDefinition },
|
||||
);
|
||||
let { dispersion } = characteristics;
|
||||
const gunRotationSpeed = degToRad(characteristics.gunTraverseSpeed);
|
||||
const mockTank = useLoader(GLTFLoader, asset(`3d/tanks/models/6929.glb`));
|
||||
const texture = useLoader(TextureLoader, '/assets/images/3d/grid.png');
|
||||
const path = new Vector3();
|
||||
const direction = new Vector3();
|
||||
texture.anisotropy = 2;
|
||||
|
||||
useFrame(({ camera }) => {
|
||||
if (!material.current) return;
|
||||
material.current.opacity = clamp(0.5 * camera.position.y, 0, 1);
|
||||
});
|
||||
|
||||
let lastTime = 0;
|
||||
let DEBUG_disable = false;
|
||||
|
||||
useFrame(({ raycaster, camera, clock, gl }) => {
|
||||
if (
|
||||
!playground.current ||
|
||||
!targetCircleWrapper.current ||
|
||||
DEBUG_disable ||
|
||||
!clientTargetCircle.current ||
|
||||
!serverTargetCircle.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
raycaster.setFromCamera(emptyVector, camera);
|
||||
|
||||
const cameraIntersections = raycaster.intersectObjects(
|
||||
playground.current.children,
|
||||
true,
|
||||
);
|
||||
|
||||
let length: number;
|
||||
|
||||
if (cameraIntersections.length === 0) {
|
||||
length = characteristics.shellRange;
|
||||
direction.set(0, 0, -1).applyQuaternion(camera.quaternion);
|
||||
aimTarget.copy(shellOrigin).addScaledVector(direction, length);
|
||||
} else {
|
||||
aimTarget.copy(cameraIntersections[0].point);
|
||||
path.copy(aimTarget).sub(shellOrigin);
|
||||
length = path.length();
|
||||
direction.copy(path).normalize();
|
||||
}
|
||||
|
||||
const deltaT = clock.elapsedTime - lastTime;
|
||||
lastTime = clock.elapsedTime;
|
||||
|
||||
const desiredPitch = Math.asin(direction.y);
|
||||
const desiredYaw = Math.atan2(-direction.x, -direction.z);
|
||||
const deltaPitch = desiredPitch - modelTransformEvent.last!.pitch;
|
||||
const deltaYaw = desiredYaw - modelTransformEvent.last!.yaw;
|
||||
const deltaAngle = Math.sqrt(deltaPitch ** 2 + deltaYaw ** 2);
|
||||
const maxPossibleRotationInTime = gunRotationSpeed * deltaT;
|
||||
const clampedDeltaAngle = Math.min(maxPossibleRotationInTime, deltaAngle);
|
||||
const clampedDeltaPitch =
|
||||
deltaAngle === 0 ? 0 : deltaPitch * (clampedDeltaAngle / deltaAngle);
|
||||
const clampedDeltaYaw =
|
||||
deltaAngle === 0 ? 0 : deltaYaw * (clampedDeltaAngle / deltaAngle);
|
||||
|
||||
const [pitch, yaw] = applyPitchYawLimits(
|
||||
modelTransformEvent.last!.pitch + clampedDeltaPitch,
|
||||
modelTransformEvent.last!.yaw + clampedDeltaYaw,
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
);
|
||||
|
||||
const actualDeltaPitch = pitch - modelTransformEvent.last!.pitch;
|
||||
const actualDeltaYaw = yaw - modelTransformEvent.last!.yaw;
|
||||
const deltaRotation = Math.abs(actualDeltaPitch) + Math.abs(actualDeltaYaw);
|
||||
const rotationSpeed = deltaRotation / deltaT;
|
||||
const turretTraversePenalty =
|
||||
characteristics.dispersionTurretTraversing * radToDeg(rotationSpeed);
|
||||
|
||||
const penalty = Math.sqrt(1 + turretTraversePenalty ** 2);
|
||||
|
||||
if (penalty === 1) {
|
||||
const dispersionMod = dispersion - characteristics.dispersion;
|
||||
const dispersionModDischarged =
|
||||
dispersionMod * Math.exp(-deltaT / characteristics.aimTime) -
|
||||
dispersionMod;
|
||||
|
||||
dispersion += dispersionModDischarged;
|
||||
} else {
|
||||
dispersion = Math.max(characteristics.dispersion * penalty, dispersion);
|
||||
}
|
||||
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
|
||||
const gunDirection = new Vector3(0, 0, -1)
|
||||
.applyAxisAngle(I_HAT, pitch)
|
||||
.applyAxisAngle(J_HAT, yaw);
|
||||
|
||||
raycaster.set(shellOrigin, gunDirection);
|
||||
|
||||
const gunIntersections = raycaster.intersectObjects(
|
||||
playground.current.children,
|
||||
true,
|
||||
);
|
||||
|
||||
let gunTarget: Vector3;
|
||||
|
||||
if (gunIntersections.length === 0) {
|
||||
gunTarget = shellOrigin
|
||||
.clone()
|
||||
.add(gunDirection.multiplyScalar(characteristics.shellRange));
|
||||
} else {
|
||||
gunTarget = gunIntersections[0].point;
|
||||
}
|
||||
|
||||
targetCircleWrapper.current.position.copy(gunTarget);
|
||||
|
||||
const distanceToTarget = gunTarget.distanceTo(shellOrigin);
|
||||
const dispersionRadius = dispersion * (distanceToTarget / 100);
|
||||
const dispersionSize = 2 * dispersionRadius;
|
||||
const targetDistanceFromCamera = gunTarget.distanceTo(camera.position);
|
||||
const angularSize =
|
||||
2 * Math.atan(dispersionSize / (2 * targetDistanceFromCamera));
|
||||
const verticalScreenSize =
|
||||
2 * Math.tan(degToRad((camera as PerspectiveCamera).fov) / 2);
|
||||
const screenSpaceSize =
|
||||
gl.domElement.clientHeight * (angularSize / verticalScreenSize);
|
||||
const targetCircleDotsSize = `${screenSpaceSize}px`;
|
||||
|
||||
clientTargetCircle.current.style.width = targetCircleDotsSize;
|
||||
clientTargetCircle.current.style.height = targetCircleDotsSize;
|
||||
serverTargetCircle.current.style.width = targetCircleDotsSize;
|
||||
serverTargetCircle.current.style.height = targetCircleDotsSize;
|
||||
});
|
||||
|
||||
if (!disturbed) return null;
|
||||
|
||||
return (
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<planeGeometry args={[10, 10]} />
|
||||
<meshStandardMaterial ref={material} map={texture} transparent />
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
|
||||
<planeGeometry args={[SIZE, SIZE]} />
|
||||
<meshStandardMaterial
|
||||
transparent
|
||||
ref={material}
|
||||
color="black"
|
||||
roughness={1}
|
||||
metalness={1}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
+9
-7
@@ -14,7 +14,6 @@ import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent
|
||||
import { ModelTankWrapper } from '../../../../../Armor/components/ModelTankWrapper';
|
||||
|
||||
export function TankModel() {
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const duelStore = Duel.useStore();
|
||||
const protagonist = Duel.use((draft) => draft.protagonist);
|
||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
@@ -105,6 +104,7 @@ export function TankModel() {
|
||||
key={key}
|
||||
onPointerDown={onPointerDown}
|
||||
castShadow
|
||||
receiveShadow
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -140,14 +140,14 @@ export function TankModel() {
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
async function handlePointerMove(event: PointerEvent) {
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
@@ -184,6 +184,7 @@ export function TankModel() {
|
||||
key={key}
|
||||
onPointerDown={onPointerDown}
|
||||
castShadow
|
||||
receiveShadow
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -227,14 +228,14 @@ export function TankModel() {
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
async function handlePointerMove(event: PointerEvent) {
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
@@ -269,6 +270,7 @@ export function TankModel() {
|
||||
key={key}
|
||||
onPointerDown={onPointerDown}
|
||||
castShadow
|
||||
receiveShadow
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
+2
-3
@@ -55,7 +55,6 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
||||
const protagonistGun = Duel.use((state) => state.protagonist.gun);
|
||||
const protagonistTurret = Duel.use((state) => state.protagonist.turret);
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const tankModelDefinition = useTankModelDefinition();
|
||||
const turretModelDefinition =
|
||||
tankModelDefinition.turrets[protagonistTurret.id];
|
||||
@@ -199,7 +198,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
localClippingEnabled: true,
|
||||
preserveDrawingBuffer: true,
|
||||
}}
|
||||
shadows
|
||||
shadows="soft"
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMissed={() => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
@@ -232,7 +231,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
{display === TankopediaDisplay.StaticArmor && (
|
||||
<StaticArmor thicknessRange={thicknessRange} />
|
||||
)}
|
||||
<Lighting display={display} />
|
||||
<Lighting />
|
||||
</Suspense>
|
||||
|
||||
<InitialAligner />
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Box, Flex } from '@radix-ui/themes';
|
||||
import { times } from 'lodash-es';
|
||||
import { Suspense, useEffect, useMemo, useRef } from 'react';
|
||||
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
|
||||
import { Var } from '../../../core/radix/var';
|
||||
import { useFullScreen } from '../../../hooks/useFullScreen';
|
||||
import { Duel } from '../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||
@@ -10,7 +9,7 @@ import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonCo
|
||||
import type { ThicknessRange } from '../../Armor/components/StaticArmor';
|
||||
import { Options } from './components/Options';
|
||||
import { TankSandbox } from './components/TankSandbox';
|
||||
import { NATION_COLORS, Title } from './components/TankSandbox/Title';
|
||||
import { Title } from './components/TankSandbox/Title';
|
||||
|
||||
const tankDefinitions = await awaitableTankDefinitions;
|
||||
|
||||
@@ -40,7 +39,6 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const duelStore = Duel.useStore();
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const nationColors = NATION_COLORS[protagonist.nation];
|
||||
|
||||
useEffect(() => {
|
||||
if (disturbed) {
|
||||
@@ -85,9 +83,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
left="0"
|
||||
style={{
|
||||
opacity: disturbed ? 0 : 1,
|
||||
background: `linear-gradient(${nationColors.background
|
||||
.map((color) => Var(`${color}-2`))
|
||||
.join(',')})`,
|
||||
background: 'black',
|
||||
transitionDuration: '1s',
|
||||
}}
|
||||
/>
|
||||
@@ -118,8 +114,6 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
position="relative"
|
||||
>
|
||||
<Box position="absolute" width="100%" height="100%" overflow="hidden">
|
||||
<Title />
|
||||
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
@@ -145,7 +139,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Title foreground />
|
||||
<Title />
|
||||
|
||||
<Options
|
||||
skeleton={skeleton}
|
||||
|
||||
@@ -127,11 +127,18 @@ export const guessTool: Tool = {
|
||||
branches: ['main', 'dev', 'opentest', 'preview'],
|
||||
};
|
||||
|
||||
export const mixerTool: Tool = {
|
||||
id: 'mixer',
|
||||
button: { color: 'red' },
|
||||
branches: ['main', 'dev', 'opentest', 'preview'],
|
||||
};
|
||||
|
||||
export const TOOLS: Tool[] = [
|
||||
tankopediaTool,
|
||||
compareTool,
|
||||
performanceTool,
|
||||
// guessTool,
|
||||
guessTool,
|
||||
// mixerTool,
|
||||
// chartsTool,
|
||||
galleryTool,
|
||||
sessionTool,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export function isHalloween() {
|
||||
const data = new Date();
|
||||
return data.getMonth() === 9 && data.getDate() === 31;
|
||||
}
|
||||
@@ -1,19 +1,14 @@
|
||||
import type {
|
||||
ModelDefinition,
|
||||
ProvisionDefinitions,
|
||||
TankDefinition,
|
||||
} from '@blitzkit/core';
|
||||
import type { ProvisionDefinitions, TankDefinition } from '@blitzkit/core';
|
||||
import { uniqueId } from 'lodash-es';
|
||||
import type { CompareMember } from '../../stores/compareEphemeral';
|
||||
import { tankToDuelMember } from './tankToDuelMember';
|
||||
|
||||
export function tankToCompareMember(
|
||||
tank: TankDefinition,
|
||||
model: ModelDefinition,
|
||||
provisionDefinitions: ProvisionDefinitions,
|
||||
) {
|
||||
return {
|
||||
...tankToDuelMember(tank, model, provisionDefinitions),
|
||||
...tankToDuelMember(tank, provisionDefinitions),
|
||||
key: uniqueId(),
|
||||
} satisfies CompareMember;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
createDefaultProvisions,
|
||||
ModelDefinition,
|
||||
type ProvisionDefinitions,
|
||||
type TankDefinition,
|
||||
} from '@blitzkit/core';
|
||||
@@ -9,7 +8,6 @@ import { genericDefaultEquipmentMatrix } from '../../stores/duel/constants';
|
||||
|
||||
export function tankToDuelMember(
|
||||
tank: TankDefinition,
|
||||
model: ModelDefinition,
|
||||
provisionDefinitions: ProvisionDefinitions,
|
||||
) {
|
||||
const turret = tank.turrets.at(-1)!;
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
modelTransformEvent,
|
||||
type ModelTransformEventData,
|
||||
} from '../core/blitzkit/modelTransform';
|
||||
import { Duel } from '../stores/duel';
|
||||
import { useTankModelDefinition } from './useTankModelDefinition';
|
||||
|
||||
export function useTankTransform(
|
||||
@@ -25,7 +24,6 @@ export function useTankTransform(
|
||||
gunContainer: RefObject<Group>,
|
||||
) {
|
||||
const tankModelDefinition = useTankModelDefinition();
|
||||
const duelStore = Duel.useStore();
|
||||
|
||||
useEffect(() => {
|
||||
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
||||
|
||||
@@ -114,11 +114,7 @@ function Content() {
|
||||
|
||||
if (tank === undefined) return;
|
||||
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
|
||||
draft.members.push(
|
||||
tankToCompareMember(tank, model, provisionDefinitions),
|
||||
);
|
||||
draft.members.push(tankToCompareMember(tank, provisionDefinitions));
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -7,7 +7,6 @@ import { GuessRendererLoader } from '../../../components/GuessRendererLoader';
|
||||
import { awaitableModelDefinitions } from '../../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../../core/awaitables/provisionDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
|
||||
import { Var } from '../../../core/radix/var';
|
||||
import {
|
||||
type LocaleAcceptorProps,
|
||||
LocaleProvider,
|
||||
@@ -33,13 +32,13 @@ export function Page({ locale }: LocaleAcceptorProps) {
|
||||
return (
|
||||
<GuessEphemeral.Provider data={initialTank}>
|
||||
<LocaleProvider locale={locale}>
|
||||
<Page2 />
|
||||
<Container />
|
||||
</LocaleProvider>
|
||||
</GuessEphemeral.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Page2() {
|
||||
function Container() {
|
||||
const tank = GuessEphemeral.use((state) => state.tank);
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
|
||||
@@ -57,65 +56,62 @@ function Content() {
|
||||
const tank = GuessEphemeral.use((state) => state.tank);
|
||||
const guessState = GuessEphemeral.use((state) => state.guessState);
|
||||
const isRevealed = guessState !== null;
|
||||
const name = unwrap(tank.name);
|
||||
const fontSize = `min(48vh, ${55 / name.length}vw)`;
|
||||
const transitionDuration = isRevealed ? '2s' : undefined;
|
||||
|
||||
return (
|
||||
<Flex flexGrow="1" position="relative" overflow="hidden">
|
||||
<GuessBackground />
|
||||
|
||||
<Flex
|
||||
position="absolute"
|
||||
left="50%"
|
||||
top="50%"
|
||||
ml="9"
|
||||
<Heading
|
||||
style={{
|
||||
transitionDuration,
|
||||
opacity: isRevealed ? 1 : 0,
|
||||
transitionDuration: isRevealed ? '2s' : undefined,
|
||||
transform: 'translateY(-50%)',
|
||||
fontWeight: 900,
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
fontSize,
|
||||
userSelect: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<Heading weight="medium" size="9">
|
||||
{unwrap(tank.name)}
|
||||
</Heading>
|
||||
</Flex>
|
||||
{name}
|
||||
</Heading>
|
||||
|
||||
<Box
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
top="0"
|
||||
right={isRevealed ? '16rem' : '0'}
|
||||
style={{
|
||||
transitionDuration: isRevealed ? '2s' : undefined,
|
||||
}}
|
||||
style={{ transitionDuration }}
|
||||
>
|
||||
<Suspense fallback={<GuessRendererLoader />}>
|
||||
<GuessRenderer />
|
||||
</Suspense>
|
||||
</Box>
|
||||
|
||||
<Flex
|
||||
position="absolute"
|
||||
left="50%"
|
||||
top="50%"
|
||||
ml="9"
|
||||
<Heading
|
||||
style={{
|
||||
opacity: isRevealed ? 1 : 0,
|
||||
transitionDuration: isRevealed ? '2s' : undefined,
|
||||
transform: 'translateY(-50%)',
|
||||
color: 'transparent',
|
||||
WebkitTextStroke: `1px ${Var('gray-12')}`,
|
||||
transitionDuration,
|
||||
opacity: isRevealed ? 0.5 : 0,
|
||||
fontWeight: 900,
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
fontSize,
|
||||
userSelect: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
WebkitTextStroke: `1px inherit`,
|
||||
}}
|
||||
weight="medium"
|
||||
size="9"
|
||||
>
|
||||
{unwrap(tank.name)}
|
||||
</Heading>
|
||||
</Flex>
|
||||
{name}
|
||||
</Heading>
|
||||
|
||||
<Guesser />
|
||||
</Flex>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Box, Flex, Heading, Link } from '@radix-ui/themes';
|
||||
import type { GetStaticPaths } from 'astro';
|
||||
import { AesonPlug } from '../../components/AesonPlug';
|
||||
import HomeHero from '../../components/HomeHero.astro';
|
||||
import { HomePageHotTanks } from '../../components/HomePageHotTanks';
|
||||
import { Plugs } from '../../components/Plugs';
|
||||
import ToolCard from '../../components/ToolCard.astro';
|
||||
import {
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
discordTool,
|
||||
embedTool,
|
||||
galleryTool,
|
||||
guessTool,
|
||||
moreTool,
|
||||
performanceTool,
|
||||
sessionTool,
|
||||
@@ -25,7 +25,6 @@ import { getStrings } from '../../core/i18n/getStrings';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
|
||||
const PUBLIC_ASSET_BRANCH = assertSecret(import.meta.env.PUBLIC_BRANCH);
|
||||
const MODE = assertSecret(import.meta.env.MODE);
|
||||
const locale = Astro.params.locale ?? DEFAULT_LOCALE;
|
||||
const strings = getStrings(Astro.params.locale);
|
||||
|
||||
@@ -110,22 +109,22 @@ export const getStaticPaths = (() => {
|
||||
|
||||
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
||||
<!-- <ToolCard tool={chartsTool} /> -->
|
||||
<!-- <ToolCard tool={guessTool} /> -->
|
||||
<ToolCard tool={guessTool} />
|
||||
<ToolCard tool={galleryTool} />
|
||||
</Flex>
|
||||
|
||||
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
||||
<ToolCard tool={sessionTool} />
|
||||
</Flex>
|
||||
|
||||
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
||||
<ToolCard tool={discordTool} />
|
||||
</Flex>
|
||||
|
||||
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
||||
<ToolCard tool={tierListTool} />
|
||||
</Flex>
|
||||
|
||||
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
||||
<ToolCard tool={embedTool} />
|
||||
<ToolCard tool={moreTool} />
|
||||
</Flex>
|
||||
|
||||
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
||||
<ToolCard tool={moreTool} />
|
||||
<Box flexGrow="1" />
|
||||
</Flex>
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Box } from '@radix-ui/themes';
|
||||
import { useRef } from 'react';
|
||||
import { MixerScene } from '../../../components/MixerScene';
|
||||
import { PageWrapper } from '../../../components/PageWrapper';
|
||||
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
|
||||
import {
|
||||
LocaleProvider,
|
||||
type LocaleAcceptorProps,
|
||||
} from '../../../hooks/useLocale';
|
||||
import { MixerEphemeral } from '../../../stores/mixerEphemeral';
|
||||
|
||||
const tankDefinitions = await awaitableTankDefinitions;
|
||||
const tanks = Object.values(tankDefinitions.tanks);
|
||||
|
||||
export function Page({ locale }: LocaleAcceptorProps) {
|
||||
const hull = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
|
||||
const turretTank = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
|
||||
const turret = useRef(
|
||||
turretTank.current.turrets[
|
||||
Math.floor(Math.random() * turretTank.current.turrets.length)
|
||||
],
|
||||
);
|
||||
const gunTank = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
|
||||
const gunTurret = useRef(
|
||||
gunTank.current.turrets[
|
||||
Math.floor(Math.random() * gunTank.current.turrets.length)
|
||||
],
|
||||
);
|
||||
const gun = useRef(
|
||||
gunTurret.current.guns[
|
||||
Math.floor(Math.random() * gunTurret.current.guns.length)
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<MixerEphemeral.Provider
|
||||
data={{
|
||||
hull: hull.current,
|
||||
turret: { tank: turretTank.current, turret: turret.current },
|
||||
gun: {
|
||||
tank: gunTank.current,
|
||||
turret: gunTurret.current,
|
||||
gun: gun.current,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Content />
|
||||
</MixerEphemeral.Provider>
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Content() {
|
||||
return (
|
||||
<PageWrapper p="0">
|
||||
<Box flexGrow="1" position="relative">
|
||||
<Box position="absolute" width="100%" height="100%" top="0" left="0">
|
||||
<MixerScene />
|
||||
</Box>
|
||||
</Box>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
import { PageWrapper } from '../../../components/PageWrapper';
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import { Page } from './_index';
|
||||
|
||||
export { getStaticPaths } from '../index.astro';
|
||||
|
||||
const { locale } = Astro.params;
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<PageWrapper p="0" maxWidth="unset">
|
||||
<Page {locale} client:load />
|
||||
</PageWrapper>
|
||||
</BaseLayout>
|
||||
@@ -55,7 +55,7 @@ export const Duel = createContextualStore(
|
||||
model: ModelDefinition;
|
||||
provisionDefinitions: ProvisionDefinitions;
|
||||
}) => {
|
||||
const protagonist = tankToDuelMember(tank, model, provisionDefinitions);
|
||||
const protagonist = tankToDuelMember(tank, provisionDefinitions);
|
||||
|
||||
return create<DuelStore>()(
|
||||
subscribeWithSelector<DuelStore>(() => ({
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type {
|
||||
GunDefinition,
|
||||
TankDefinition,
|
||||
TurretDefinition,
|
||||
} from '@blitzkit/core';
|
||||
import { create } from 'zustand';
|
||||
import { createContextualStore } from '../core/zustand/createContextualStore';
|
||||
|
||||
export interface MixerEphemeral {
|
||||
hull: TankDefinition;
|
||||
turret: {
|
||||
tank: TankDefinition;
|
||||
turret: TurretDefinition;
|
||||
};
|
||||
gun: {
|
||||
tank: TankDefinition;
|
||||
turret: TurretDefinition;
|
||||
gun: GunDefinition;
|
||||
};
|
||||
}
|
||||
|
||||
export const MixerEphemeral = createContextualStore((data: MixerEphemeral) =>
|
||||
create<MixerEphemeral>()(() => data),
|
||||
);
|
||||
Reference in new issue
Block a user