diff --git a/packages/website/src/components/Armor/components/PrimaryArmorSceneComponent/index.tsx b/packages/website/src/components/Armor/components/PrimaryArmorSceneComponent/index.tsx index d54a2f70e..a2419b4af 100644 --- a/packages/website/src/components/Armor/components/PrimaryArmorSceneComponent/index.tsx +++ b/packages/website/src/components/Armor/components/PrimaryArmorSceneComponent/index.tsx @@ -22,7 +22,7 @@ import { jsxTree } from "../../../../core/blitzkit/jsxTree"; import { Duel } from "../../../../stores/duel"; import { Tankopedia } from "../../../../stores/tankopedia"; import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent"; -import { transitionEvent } from "../../../Tankopedia/HeroSection/components/TankSandbox/components/Lighting"; +import { transitionEvent } from "../../../Lighting"; import fragmentShader from "./shaders/fragment.glsl?raw"; import vertexShader from "./shaders/vertex.glsl?raw"; import { spacedArmorRenderTarget } from "./target"; diff --git a/packages/website/src/components/Armor/components/StaticArmorSceneComponent.tsx b/packages/website/src/components/Armor/components/StaticArmorSceneComponent.tsx index 37173d197..4046ae014 100644 --- a/packages/website/src/components/Armor/components/StaticArmorSceneComponent.tsx +++ b/packages/website/src/components/Armor/components/StaticArmorSceneComponent.tsx @@ -27,7 +27,7 @@ import { discardClippingPlane } from "../../../core/three/discardClippingPlane"; import { useEquipment } from "../../../hooks/useEquipment"; import { Duel } from "../../../stores/duel"; import { Tankopedia } from "../../../stores/tankopedia"; -import { transitionEvent } from "../../Tankopedia/HeroSection/components/TankSandbox/components/Lighting"; +import { transitionEvent } from "../../Lighting"; import { ArmorType } from "./SpacedArmorScene"; import type { ArmorUserData, diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/AutoClear.tsx b/packages/website/src/components/CanvasAutoClear.tsx similarity index 76% rename from packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/AutoClear.tsx rename to packages/website/src/components/CanvasAutoClear.tsx index e792d90bd..a259ae0d2 100644 --- a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/AutoClear.tsx +++ b/packages/website/src/components/CanvasAutoClear.tsx @@ -1,6 +1,6 @@ import { useFrame } from "@react-three/fiber"; -export function AutoClear() { +export function CanvasAutoClear() { useFrame(({ gl }) => { gl.clear(); }, 0); diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Control.tsx b/packages/website/src/components/CanvasControl.tsx similarity index 94% rename from packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Control.tsx rename to packages/website/src/components/CanvasControl.tsx index c9a5d56f1..afae3ba24 100644 --- a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Control.tsx +++ b/packages/website/src/components/CanvasControl.tsx @@ -5,13 +5,13 @@ import type { QuicklimeEvent } from "quicklime"; import { useEffect, useRef } from "react"; import { PerspectiveCamera, Vector3 } from "three"; import { OrbitControls as OrbitControlsClass } from "three-stdlib"; -import { api } from "../../../../../../api/dynamic"; -import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits"; -import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment"; -import { Pose, poseEvent } from "../../../../../../core/blitzkit/pose"; -import { controlsEnabledEvent } from "../../../../../../core/controlsEnabled"; import { Duel } from "../../../../../../stores/duel"; -import { Tankopedia } from "../../../../../../stores/tankopedia"; +import { api } from "../api/dynamic"; +import { applyPitchYawLimits } from "../core/blitz/applyPitchYawLimits"; +import { hasEquipment } from "../core/blitzkit/hasEquipment"; +import { Pose, poseEvent } from "../core/blitzkit/pose"; +import { controlsEnabledEvent } from "../core/controlsEnabled"; +import { Tankopedia } from "../stores/tankopedia"; const poseDistances: Record = { [Pose.HullDown]: 15, @@ -29,7 +29,7 @@ interface ControlsProps { enableRotate?: boolean; } -export function Controls({ +export function CanvasControl({ autoRotate = true, zoomable = true, enableRotate = true, diff --git a/packages/website/src/components/GuessRenderer.tsx b/packages/website/src/components/GuessRenderer.tsx index 8bb46acad..18573f8ec 100644 --- a/packages/website/src/components/GuessRenderer.tsx +++ b/packages/website/src/components/GuessRenderer.tsx @@ -1,9 +1,9 @@ import { useModel } from "../hooks/useModel"; import { Guess, GuessState } from "../stores/guess"; +import { Controls } from "./CanvasControl"; +import { Lighting } from "./Lighting"; 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"; +import { TankModel } from "./TankModel"; const BRIGHTNESS = 2 ** -5.5; const CONTRAST = 2 ** 2; diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/InitialAligner.tsx b/packages/website/src/components/InitialCameraAligner.tsx similarity index 74% rename from packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/InitialAligner.tsx rename to packages/website/src/components/InitialCameraAligner.tsx index 9bd6999ae..ca7da1b59 100644 --- a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/InitialAligner.tsx +++ b/packages/website/src/components/InitialCameraAligner.tsx @@ -1,13 +1,13 @@ import { useEffect } from "react"; import { degToRad } from "three/src/math/MathUtils.js"; -import { api } from "../../../../../../api/dynamic"; -import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits"; -import { modelTransformEvent } from "../../../../../../core/blitzkit/modelTransform"; import { Duel } from "../../../../../../stores/duel"; +import { api } from "../api/dynamic"; +import { applyPitchYawLimits } from "../core/blitz/applyPitchYawLimits"; +import { modelTransformEvent } from "../core/blitzkit/modelTransform"; const modelDefinitions = await api.models(); -export function InitialAligner() { +export function InitialCameraAligner() { const tank = Duel.use((state) => state.protagonist.tank); const turret = Duel.use((state) => state.protagonist.turret); const gun = Duel.use((state) => state.protagonist.gun); diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Lighting.tsx b/packages/website/src/components/Lighting.tsx similarity index 95% rename from packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Lighting.tsx rename to packages/website/src/components/Lighting.tsx index 26abe418b..a4d90f60f 100644 --- a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Lighting.tsx +++ b/packages/website/src/components/Lighting.tsx @@ -12,9 +12,9 @@ import { } from "react"; import { HemisphereLight, SpotLight, type Group } from "three"; import { degToRad, lerp } from "three/src/math/MathUtils.js"; -import { Tankopedia } from "../../../../../../stores/tankopedia"; -import { TankopediaPersistent } from "../../../../../../stores/tankopediaPersistent"; -import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants"; +import { Tankopedia } from "../stores/tankopedia"; +import { TankopediaPersistent } from "../stores/tankopediaPersistent"; +import { TankopediaDisplay } from "../stores/tankopediaPersistent/constants"; const ANGLE = degToRad(15); const REVEAL_ANIMATION_TIME = 3; diff --git a/packages/website/src/components/MixerScene/index.tsx b/packages/website/src/components/MixerScene/index.tsx index 3259d73fb..54b8c2f65 100644 --- a/packages/website/src/components/MixerScene/index.tsx +++ b/packages/website/src/components/MixerScene/index.tsx @@ -4,8 +4,8 @@ import type { QuicklimeEvent } from "quicklime"; import { forwardRef, Suspense, useEffect, useRef } from "react"; import type { OrbitControls as OrbitControlsClass } from "three-stdlib"; import { controlsEnabledEvent } from "../../core/controlsEnabled"; -import { Lighting } from "../Tankopedia/HeroSection/components/TankSandbox/components/Lighting"; -import { SceneProps } from "../Tankopedia/HeroSection/components/TankSandbox/components/SceneProps"; +import { Lighting } from "../Lighting"; +import { SceneProps } from "../SceneProps"; import { MixerFallback } from "./components/MixerFallback"; import { Model } from "./components/Model"; diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/SceneProps/index.tsx b/packages/website/src/components/SceneProps/index.tsx similarity index 100% rename from packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/SceneProps/index.tsx rename to packages/website/src/components/SceneProps/index.tsx diff --git a/packages/website/src/components/ScreenshotButton.tsx b/packages/website/src/components/ScreenshotButton.tsx index b86be58b9..d6675364f 100644 --- a/packages/website/src/components/ScreenshotButton.tsx +++ b/packages/website/src/components/ScreenshotButton.tsx @@ -9,7 +9,7 @@ import { import { Jimp } from "jimp"; import { useCallback, type RefObject } from "react"; import { useLocale } from "../hooks/useLocale"; -import { screenshotReadyEvent } from "./Tankopedia/HeroSection/components/TankSandbox/components/SceneProps"; +import { screenshotReadyEvent } from "./SceneProps"; interface Props extends ButtonProps { canvas: RefObject; diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/TankModel.tsx b/packages/website/src/components/TankModel.tsx similarity index 92% rename from packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/TankModel.tsx rename to packages/website/src/components/TankModel.tsx index ded24bba1..72a34b833 100644 --- a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/TankModel.tsx +++ b/packages/website/src/components/TankModel.tsx @@ -1,18 +1,18 @@ import { invalidate, useThree, type ThreeEvent } from "@react-three/fiber"; import { useRef } from "react"; import { Group, Mesh, MeshStandardMaterial, Vector2 } from "three"; -import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits"; -import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment"; -import { jsxTree } from "../../../../../../core/blitzkit/jsxTree"; -import { modelTransformEvent } from "../../../../../../core/blitzkit/modelTransform"; -import { controlsEnabledEvent } from "../../../../../../core/controlsEnabled"; -import { useModel } from "../../../../../../hooks/useModel"; import { useTankModelDefinition } from "../../../../../../hooks/useTankModelDefinition"; -import { useTankTransform } from "../../../../../../hooks/useTankTransform"; import { Duel } from "../../../../../../stores/duel"; -import { Tankopedia } from "../../../../../../stores/tankopedia"; -import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants"; -import { ModelTankWrapper } from "../../../../../Armor/components/ModelTankWrapper"; +import { applyPitchYawLimits } from "../core/blitz/applyPitchYawLimits"; +import { hasEquipment } from "../core/blitzkit/hasEquipment"; +import { jsxTree } from "../core/blitzkit/jsxTree"; +import { modelTransformEvent } from "../core/blitzkit/modelTransform"; +import { controlsEnabledEvent } from "../core/controlsEnabled"; +import { useModel } from "../hooks/useModel"; +import { useTankTransform } from "../hooks/useTankTransform"; +import { Tankopedia } from "../stores/tankopedia"; +import { TankopediaDisplay } from "../stores/tankopediaPersistent/constants"; +import { ModelTankWrapper } from "./Armor/components/ModelTankWrapper"; export function TankModel() { const protagonist = Duel.use((draft) => draft.protagonist); diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/index.tsx b/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/index.tsx deleted file mode 100644 index d9640dc9e..000000000 --- a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/index.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { - forwardRef, - Suspense, - useEffect, - useImperativeHandle, - useRef, -} from "react"; -import { applyPitchYawLimits } from "../../../../../core/blitz/applyPitchYawLimits"; -import { modelTransformEvent } from "../../../../../core/blitzkit/modelTransform"; -import { Pose, poseEvent } from "../../../../../core/blitzkit/pose"; -import { useEquipment } from "../../../../../hooks/useEquipment"; -import { useModel } from "../../../../../hooks/useModel"; -import { useTankModelDefinition } from "../../../../../hooks/useTankModelDefinition"; -import { Duel } from "../../../../../stores/duel"; -import { Tankopedia } from "../../../../../stores/tankopedia"; -import { TankopediaPersistent } from "../../../../../stores/tankopediaPersistent"; -import { TankopediaDisplay } from "../../../../../stores/tankopediaPersistent/constants"; -import { Armor } from "../../../../Armor"; -import { ArmorPlateDisplay } from "../../../../Armor/components/ArmorPlateDisplay"; -import { ShotDisplay } from "../../../../Armor/components/ShotDisplay"; -import { - StaticArmor, - type ThicknessRange, -} from "../../../../Armor/components/StaticArmor"; -import { SmartCanvas } from "../../../../SmartCanvas"; -import { AutoClear } from "./components/AutoClear"; -import { Controls } from "./components/Control"; -import { InitialAligner } from "./components/InitialAligner"; -import { Lighting } from "./components/Lighting"; -import { SceneProps } from "./components/SceneProps"; -import { TankModel } from "./components/TankModel"; - -interface TankSandboxProps { - thicknessRange: ThicknessRange; -} - -export const TankSandbox = forwardRef( - ({ thicknessRange }, ref) => { - const canvas = useRef(null); - const hasImprovedVerticalStabilizer = useEquipment(122); - const hasDownImprovedVerticalStabilizer = useEquipment(124); - const protagonistGun = Duel.use((state) => state.protagonist.gun); - const protagonistTurret = Duel.use((state) => state.protagonist.turret); - const tankModelDefinition = useTankModelDefinition(); - const turretModelDefinition = - tankModelDefinition.turrets[protagonistTurret.id]; - const gunModelDefinition = turretModelDefinition.guns[protagonistGun.id]; - const display = Tankopedia.use((state) => state.display); - const hideTankModelUnderArmor = TankopediaPersistent.use( - (state) => state.hideTankModelUnderArmor, - ); - - const id = Duel.use((state) => state.protagonist.tank.id); - const { hasPbr } = useModel(id); - - useImperativeHandle(ref, () => canvas.current!, []); - - function handlePointerDown() { - window.addEventListener("pointermove", handlePointerMove); - window.addEventListener("pointerup", handlePointerUp); - } - function handlePointerMove(event: PointerEvent) { - event.preventDefault(); - } - function handlePointerUp(event: PointerEvent) { - event.preventDefault(); - - window.removeEventListener("pointermove", handlePointerMove); - window.removeEventListener("pointerup", handlePointerUp); - } - - useEffect(() => { - function handlePoseEvent(pose: Pose) { - switch (pose) { - case Pose.HullDown: { - const [pitch, yaw] = applyPitchYawLimits( - -Infinity, - 0, - gunModelDefinition.pitch!, - turretModelDefinition.yaw, - hasImprovedVerticalStabilizer, - hasDownImprovedVerticalStabilizer, - ); - - modelTransformEvent.dispatch({ pitch, yaw }); - - break; - } - - case Pose.FaceHug: { - const [pitch, yaw] = applyPitchYawLimits( - Infinity, - 0, - gunModelDefinition.pitch!, - turretModelDefinition.yaw, - hasImprovedVerticalStabilizer, - ); - - modelTransformEvent.dispatch({ pitch, yaw }); - - break; - } - - case Pose.Default: - const [pitch, yaw] = applyPitchYawLimits( - 0, - 0, - gunModelDefinition.pitch!, - turretModelDefinition.yaw, - hasImprovedVerticalStabilizer, - ); - - modelTransformEvent.dispatch({ pitch, yaw }); - - break; - } - } - - poseEvent.on(handlePoseEvent); - - return () => { - poseEvent.off(handlePoseEvent); - }; - }); - - useEffect(() => { - const [pitch, yaw] = applyPitchYawLimits( - modelTransformEvent.last!.pitch, - modelTransformEvent.last!.yaw, - gunModelDefinition.pitch!, - turretModelDefinition.yaw, - hasImprovedVerticalStabilizer, - ); - - modelTransformEvent.dispatch({ pitch, yaw }); - }, [protagonistGun, protagonistTurret]); - - useEffect(() => { - if (display !== TankopediaDisplay.DynamicArmor) { - Tankopedia.mutate((draft) => { - draft.shot = undefined; - }); - } - - if (display !== TankopediaDisplay.StaticArmor) { - Tankopedia.mutate((draft) => { - draft.highlightArmor = undefined; - }); - } - }, [display]); - - return ( - { - Tankopedia.mutate((draft) => { - draft.shot = undefined; - draft.highlightArmor = undefined; - }); - }} - style={{ - userSelect: "none", - width: "100%", - height: "100%", - outline: undefined, - }} - > - - - - {(display === TankopediaDisplay.Model || - (display === TankopediaDisplay.DynamicArmor && - !hideTankModelUnderArmor)) && } - - - - - - {/* Controls within Suspense to allow for frame-perfect start of camera auto-rotate */} - - - {display === TankopediaDisplay.DynamicArmor && } - {display === TankopediaDisplay.StaticArmor && ( - - )} - - - - - ); - }, -); diff --git a/packages/website/src/components/TankopediaLoadout/index.tsx b/packages/website/src/components/TankopediaLoadout/index.tsx index 268bbda26..2c0ef6e9f 100644 --- a/packages/website/src/components/TankopediaLoadout/index.tsx +++ b/packages/website/src/components/TankopediaLoadout/index.tsx @@ -1,10 +1,10 @@ import type { Consumable } from "@blitzkit/core"; import { TrashIcon } from "@radix-ui/react-icons"; -import { useTankCompatibility } from "../../hooks/useTankCompatibility"; import { useConsumables } from "../../hooks/useConsumables"; -import { useProtagonist } from "../../hooks/useProtagonist"; import { useProtagonistGun } from "../../hooks/useProtagonistGun"; +import { useProtagonistTank } from "../../hooks/useProtagonistTank"; import { useStrings } from "../../hooks/useStrings"; +import { useTankCompatibility } from "../../hooks/useTankCompatibility"; import { Tankopedia } from "../../stores/tankopedia"; import { hasUpgrades } from "../../tankopedia/hasUpgrades"; import { vehicleStatusKeys } from "../../tankopedia/tankState"; @@ -19,7 +19,7 @@ interface TankopediaLoadoutProps { } export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) { - const tank = useProtagonist(); + const tank = useProtagonistTank(); const showModules = hasUpgrades(tank); return ( @@ -120,7 +120,7 @@ interface ConsumablesProps { function Consumables({ characteristics }: ConsumablesProps) { const strings = useStrings(); const consumables = useConsumables(); - const tank = useProtagonist(); + const tank = useProtagonistTank(); const gun = useProtagonistGun(); const isCompatible = useTankCompatibility(tank, gun); @@ -165,7 +165,7 @@ function Consumable({ id, consumable }: ConsumableProps) { const isSelected = Tankopedia.use((state) => state.protagonist.consumables.includes(id), ); - const tank = useProtagonist(); + const tank = useProtagonistTank(); const unwrap = useUnwrap(); return ( @@ -214,7 +214,7 @@ function Consumable({ id, consumable }: ConsumableProps) { function Equipment() { const strings = useStrings(); - const tank = useProtagonist(); + const tank = useProtagonistTank(); const equipment = useEquipment(tank.tank!); return ( @@ -273,7 +273,7 @@ interface EquipmentSlotProps { function EquipmentSlot({ slot, rowIndex, columnIndex }: EquipmentSlotProps) { const equipmentIndex = rowIndex * equipmentColumns + columnIndex; - const tank = useProtagonist(); + const tank = useProtagonistTank(); const equipment = useEquipment(tank.tank!); const { price } = equipment.price.unlock_slot_prices[equipmentIndex]; @@ -307,7 +307,7 @@ function EquipmentOption({ columnIndex, }: EquipmentOptionProps) { const equipmentIndex = rowIndex * equipmentColumns + columnIndex; - const tank = useProtagonist(); + const tank = useProtagonistTank(); const equipment = useEquipment(tank.tank!); const slot = equipment.preset.slots[equipmentIndex]; const id = slot.options_catalog_i_ds[optionIndex]; @@ -346,7 +346,7 @@ function EquipmentOption({ function Modules() { const strings = useStrings(); - const tank = useProtagonist(); + const tank = useProtagonistTank(); const upgrades = Tankopedia.use((state) => state.protagonist.upgrades); const alternates = Tankopedia.use((state) => state.protagonist.alternates); const isStock = useMemo(() => { @@ -496,7 +496,7 @@ interface LineElementProps { function LineElement({ index, lineName, stage }: LineElementProps) { const upgrades = Tankopedia.use((state) => state.protagonist.upgrades); const alternates = Tankopedia.use((state) => state.protagonist.alternates); - const tank = useProtagonist(); + const tank = useProtagonistTank(); const strings = useStrings(); const gameStrings = useGameStrings("TankEntity"); const upgradePreset = useUpgradePreset(tank.tank!.tank_upgrade_preset); diff --git a/packages/website/src/components/TankopediaSandbox/index.module.css b/packages/website/src/components/TankopediaSandbox/index.module.css new file mode 100644 index 000000000..e545b2a26 --- /dev/null +++ b/packages/website/src/components/TankopediaSandbox/index.module.css @@ -0,0 +1,16 @@ +.wrapper { + height: calc(100dvh - 16rem); + position: relative; + + .sandbox { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + .canvas { + background-color: black; + } + } +} diff --git a/packages/website/src/components/TankopediaSandbox/index.tsx b/packages/website/src/components/TankopediaSandbox/index.tsx new file mode 100644 index 000000000..b866debe5 --- /dev/null +++ b/packages/website/src/components/TankopediaSandbox/index.tsx @@ -0,0 +1,202 @@ +import { + forwardRef, + Suspense, + useEffect, + useImperativeHandle, + useRef, +} from "react"; +import { applyPitchYawLimits } from "../../core/blitz/applyPitchYawLimits"; +import { modelTransformEvent } from "../../core/blitzkit/modelTransform"; +import { Pose, poseEvent } from "../../core/blitzkit/pose"; +import { useEquipment } from "../../hooks/useEquipment"; +import { useModel } from "../../hooks/useModel"; +import { useProtagonistGun } from "../../hooks/useProtagonistGun"; +import { useProtagonistTank } from "../../hooks/useProtagonistTank"; +import { useProtagonistTurret } from "../../hooks/useProtagonistTurret"; +import { Tankopedia } from "../../stores/tankopedia"; +import { TankopediaPersistent } from "../../stores/tankopediaPersistent"; +import { TankopediaDisplay } from "../../stores/tankopediaPersistent/constants"; +import { Armor } from "../Armor"; +import { ArmorPlateDisplay } from "../Armor/components/ArmorPlateDisplay"; +import { ShotDisplay } from "../Armor/components/ShotDisplay"; +import { + StaticArmor, + type ThicknessRange, +} from "../Armor/components/StaticArmor"; +import { CanvasAutoClear } from "../CanvasAutoClear"; +import { CanvasControl } from "../CanvasControl"; +import { InitialCameraAligner } from "../InitialCameraAligner"; +import { Lighting } from "../Lighting"; +import { SceneProps } from "../SceneProps"; +import { SmartCanvas } from "../SmartCanvas"; +import { TankModel } from "../TankModel"; +import styles from "./index.module.css"; + +interface TankSandboxProps { + thicknessRange: ThicknessRange; +} + +export const TankopediaSandbox = forwardRef< + HTMLCanvasElement, + TankSandboxProps +>(({ thicknessRange }, ref) => { + const canvas = useRef(null); + const hasImprovedVerticalStabilizer = useEquipment(122); + const hasDownImprovedVerticalStabilizer = useEquipment(124); + + const tank = useProtagonistTank(); + const gun = useProtagonistGun(); + const turret = useProtagonistTurret(); + + const model = Tankopedia.use((state) => state.protagonist.model); + const turretModelDefinition = model.turrets[turret.id]; + const gunModelDefinition = turretModelDefinition.guns[gun.id]; + const display = Tankopedia.use((state) => state.display); + const hideTankModelUnderArmor = TankopediaPersistent.use( + (state) => state.hideTankModelUnderArmor, + ); + + const { hasPbr } = useModel(tank.id); + + useImperativeHandle(ref, () => canvas.current!, []); + + function handlePointerDown() { + window.addEventListener("pointermove", handlePointerMove); + window.addEventListener("pointerup", handlePointerUp); + } + function handlePointerMove(event: PointerEvent) { + event.preventDefault(); + } + function handlePointerUp(event: PointerEvent) { + event.preventDefault(); + + window.removeEventListener("pointermove", handlePointerMove); + window.removeEventListener("pointerup", handlePointerUp); + } + + useEffect(() => { + function handlePoseEvent(pose: Pose) { + switch (pose) { + case Pose.HullDown: { + const [pitch, yaw] = applyPitchYawLimits( + -Infinity, + 0, + gunModelDefinition.pitch!, + turretModelDefinition.yaw, + hasImprovedVerticalStabilizer, + hasDownImprovedVerticalStabilizer, + ); + + modelTransformEvent.dispatch({ pitch, yaw }); + + break; + } + + case Pose.FaceHug: { + const [pitch, yaw] = applyPitchYawLimits( + Infinity, + 0, + gunModelDefinition.pitch!, + turretModelDefinition.yaw, + hasImprovedVerticalStabilizer, + ); + + modelTransformEvent.dispatch({ pitch, yaw }); + + break; + } + + case Pose.Default: + const [pitch, yaw] = applyPitchYawLimits( + 0, + 0, + gunModelDefinition.pitch!, + turretModelDefinition.yaw, + hasImprovedVerticalStabilizer, + ); + + modelTransformEvent.dispatch({ pitch, yaw }); + + break; + } + } + + poseEvent.on(handlePoseEvent); + + return () => { + poseEvent.off(handlePoseEvent); + }; + }); + + useEffect(() => { + const [pitch, yaw] = applyPitchYawLimits( + modelTransformEvent.last!.pitch, + modelTransformEvent.last!.yaw, + gunModelDefinition.pitch!, + turretModelDefinition.yaw, + hasImprovedVerticalStabilizer, + ); + + modelTransformEvent.dispatch({ pitch, yaw }); + }, [gun, turret]); + + useEffect(() => { + if (display !== TankopediaDisplay.DynamicArmor) { + Tankopedia.mutate((draft) => { + draft.shot = undefined; + }); + } + + if (display !== TankopediaDisplay.StaticArmor) { + Tankopedia.mutate((draft) => { + draft.highlightArmor = undefined; + }); + } + }, [display]); + + return ( +
+ { + Tankopedia.mutate((draft) => { + draft.shot = undefined; + draft.highlightArmor = undefined; + }); + }} + > + + + + {(display === TankopediaDisplay.Model || + (display === TankopediaDisplay.DynamicArmor && + !hideTankModelUnderArmor)) && } + + + + + + + {/* Controls within Suspense to allow for frame-perfect start of camera auto-rotate */} + + + {display === TankopediaDisplay.DynamicArmor && } + {display === TankopediaDisplay.StaticArmor && ( + + )} + + + + +
+ ); +}); diff --git a/packages/website/src/config/camera.ts b/packages/website/src/config/camera.ts new file mode 100644 index 000000000..8df5e237a --- /dev/null +++ b/packages/website/src/config/camera.ts @@ -0,0 +1,14 @@ +import { degToRad } from "three/src/math/MathUtils.js"; + +export const fov = 20; + +export const initialCameraR = 15; +export const initialCameraPhi = degToRad(90 - 10); +export const initialCameraTheta = degToRad(90 + 15); + +export const cameraSwingVerticalAmplitude = degToRad(5); +export const cameraSwingVerticalFrequency = Math.E ** -6; +export const cameraSwingHorizontalAmplitude = degToRad(30); +export const cameraSwingHorizontalFrequency = Math.E ** -5; + +export const cameraCenterCorrectionAmplitude = 0.8; diff --git a/packages/website/src/hooks/useProtagonist.ts b/packages/website/src/hooks/useProtagonistTank.ts similarity index 87% rename from packages/website/src/hooks/useProtagonist.ts rename to packages/website/src/hooks/useProtagonistTank.ts index 1838119c1..7c5a6d89b 100644 --- a/packages/website/src/hooks/useProtagonist.ts +++ b/packages/website/src/hooks/useProtagonistTank.ts @@ -2,7 +2,7 @@ import { api } from "../api/dynamic"; import { Tankopedia } from "../stores/tankopedia"; import { useAwait } from "./useAwait"; -export function useProtagonist() { +export function useProtagonistTank() { const id = Tankopedia.use((state) => state.protagonist.tank); const tank = useAwait(() => api.tank(id), `tank-${id}`); return tank; diff --git a/packages/website/src/hooks/useProtagonistTurret.ts b/packages/website/src/hooks/useProtagonistTurret.ts index d5c2b31fe..bd6f58c35 100644 --- a/packages/website/src/hooks/useProtagonistTurret.ts +++ b/packages/website/src/hooks/useProtagonistTurret.ts @@ -1,8 +1,8 @@ import { Tankopedia } from "../stores/tankopedia"; -import { useProtagonist } from "./useProtagonist"; +import { useProtagonistTank } from "./useProtagonistTank"; export function useProtagonistTurret() { const id = Tankopedia.use((state) => state.protagonist.gun); - const tank = useProtagonist(); + const tank = useProtagonistTank(); return tank.turrets[id]; } diff --git a/packages/website/src/hooks/useTankModelDefinition.ts b/packages/website/src/hooks/useTankModelDefinition.ts deleted file mode 100644 index 242713e4b..000000000 --- a/packages/website/src/hooks/useTankModelDefinition.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Tankopedia } from "../stores/tankopedia"; - -export function useTankModelDefinition() { - return Tankopedia.use((state) => state.model); -} diff --git a/packages/website/src/pages/[...chunk_default]/[...locale]/tanks/[slug]/_index.tsx b/packages/website/src/pages/[...chunk_default]/[...locale]/tanks/[slug]/_index.tsx index b0e3cc32a..66a74a2e5 100644 --- a/packages/website/src/pages/[...chunk_default]/[...locale]/tanks/[slug]/_index.tsx +++ b/packages/website/src/pages/[...chunk_default]/[...locale]/tanks/[slug]/_index.tsx @@ -1,6 +1,7 @@ import { useMemo } from "react"; import { api } from "../../../../../api/dynamic"; import { TankopediaLoadout } from "../../../../../components/TankopediaLoadout"; +import { TankopediaSandbox } from "../../../../../components/TankopediaSandbox"; import { withErrorWrapper } from "../../../../../hocs/withErrorWrapper"; import { withLocale } from "../../../../../hocs/withLocale"; import { useAwait } from "../../../../../hooks/useAwait";