mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
Merge branch 'main' into ue
This commit is contained in:
commit
5412c479c8
42 files changed
+1044
-934
No files matched your search
@@ -32,12 +32,13 @@
|
||||
"compare-versions": "^6.1.1",
|
||||
"fuzzysort": "^3.1.0",
|
||||
"immer": "^10.1.3",
|
||||
"jimp": "^1.6.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"markdown-to-jsx": "^7.7.13",
|
||||
"pwa-install-handler": "^2.6.3",
|
||||
"quicklime": "^1.2.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-promise-suspense": "^0.3.4",
|
||||
"three": "^0.180.0",
|
||||
"three-stdlib": "^2.36.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { I_HAT, J_HAT } from "@blitzkit/core";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { invalidate, useThree } from "@react-three/fiber";
|
||||
import { clamp } from "lodash-es";
|
||||
import type { QuicklimeEvent } from "quicklime";
|
||||
import { useEffect, useMemo, type ComponentProps } from "react";
|
||||
@@ -91,7 +91,7 @@ export function StaticArmorSceneComponent({
|
||||
color = new Color(
|
||||
clamp(1 - (7 / 8) * xClamped, 0, 1),
|
||||
0,
|
||||
clamp(1 - (1 / 8) * xClamped, 0, 1)
|
||||
clamp(1 - (1 / 8) * xClamped, 0, 1),
|
||||
);
|
||||
opacity = clamp(x + 1 / 2, 0, 1);
|
||||
break;
|
||||
@@ -115,7 +115,7 @@ export function StaticArmorSceneComponent({
|
||||
opacity: 0,
|
||||
userData: { opacity0: opacity },
|
||||
}),
|
||||
[thickness]
|
||||
[thickness],
|
||||
);
|
||||
const outlineMaterial = useMemo(
|
||||
() =>
|
||||
@@ -126,7 +126,7 @@ export function StaticArmorSceneComponent({
|
||||
opacity: 0,
|
||||
transparent: true,
|
||||
}),
|
||||
[thickness]
|
||||
[thickness],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -165,8 +165,11 @@ export function StaticArmorSceneComponent({
|
||||
}
|
||||
|
||||
surfaceMaterial.needsUpdate = true;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
handleHighlightArmor();
|
||||
|
||||
function handleTransitionEvent(event: QuicklimeEvent<number>) {
|
||||
surfaceMaterial.opacity = surfaceMaterial.userData.opacity0 * event.data;
|
||||
outlineMaterial.opacity = event.data;
|
||||
@@ -180,7 +183,7 @@ export function StaticArmorSceneComponent({
|
||||
const unsubscribes = [
|
||||
Tankopedia.on(
|
||||
(state) => state.highlightArmor?.name,
|
||||
handleHighlightArmor
|
||||
handleHighlightArmor,
|
||||
),
|
||||
() => transitionEvent.off(handleTransitionEvent),
|
||||
];
|
||||
@@ -194,14 +197,14 @@ export function StaticArmorSceneComponent({
|
||||
const { clip } = props;
|
||||
const gunOrigin = unrotateDavaVector(props.gunOrigin.clone());
|
||||
const neckOrigin = unrotateDavaVector(props.hullOrigin.clone()).add(
|
||||
unrotateDavaVector(props.turretOrigin.clone())
|
||||
unrotateDavaVector(props.turretOrigin.clone()),
|
||||
);
|
||||
const barrelOrigin = neckOrigin.clone().add(gunOrigin);
|
||||
const distanceToBarrel = clip.distanceToPoint(barrelOrigin);
|
||||
const point = new Vector3();
|
||||
|
||||
function handleModelTransform(
|
||||
event: QuicklimeEvent<ModelTransformEventData>
|
||||
event: QuicklimeEvent<ModelTransformEventData>,
|
||||
) {
|
||||
clip.normal
|
||||
.set(0, 0, -1)
|
||||
@@ -266,7 +269,7 @@ export function StaticArmorSceneComponent({
|
||||
const surfaceNormal = event
|
||||
.normal!.clone()
|
||||
.applyQuaternion(
|
||||
event.object.getWorldQuaternion(new Quaternion())
|
||||
event.object.getWorldQuaternion(new Quaternion()),
|
||||
);
|
||||
const angle = surfaceNormal.angleTo(cameraNormal);
|
||||
const thicknessAngled =
|
||||
|
||||
@@ -38,7 +38,7 @@ export function ConsumablesManager({
|
||||
timers
|
||||
? consumable.duration === undefined
|
||||
? undefined
|
||||
: consumable.duration * (hasHighEndConsumables ? 1.7 : 1)
|
||||
: consumable.duration * (hasHighEndConsumables ? 1.33 : 1)
|
||||
: undefined
|
||||
}
|
||||
cooldown={
|
||||
|
||||
@@ -1,26 +1,3 @@
|
||||
<script>
|
||||
import locales from "@blitzkit/i18n/locales.json";
|
||||
import { getBestLocale } from "../core/i18n/getBestLocale";
|
||||
|
||||
const isAPI = window.location.pathname.startsWith("/api");
|
||||
const isI18nPath = locales.supported.some(({ locale }) =>
|
||||
window.location.pathname.startsWith(`/${locale}/`)
|
||||
);
|
||||
const bestLocale = getBestLocale();
|
||||
const currentPath = locales.supported.find(({ locale }) =>
|
||||
window.location.pathname.startsWith(`/${locale}/`)
|
||||
);
|
||||
|
||||
if (
|
||||
!isAPI && // not an api, duh!
|
||||
localStorage.getItem("preferred-locale") !== null && // no preference, so probably google's crawler
|
||||
bestLocale !== currentPath?.locale // we need to redirect
|
||||
) {
|
||||
window.location.pathname = isI18nPath
|
||||
? window.location.pathname.replace(
|
||||
`/${currentPath?.locale}`,
|
||||
bestLocale === undefined ? "" : `/${bestLocale}`
|
||||
)
|
||||
: `/${bestLocale}${window.location.pathname}`;
|
||||
}
|
||||
</script>
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
Popover,
|
||||
type ButtonProps,
|
||||
} from "@radix-ui/themes";
|
||||
import type { RefObject } from "react";
|
||||
import { Jimp } from "jimp";
|
||||
import { useCallback, type RefObject } from "react";
|
||||
import { useLocale } from "../hooks/useLocale";
|
||||
import { screenshotReadyEvent } from "./Tankopedia/HeroSection/components/TankSandbox/components/SceneProps";
|
||||
|
||||
@@ -18,6 +19,35 @@ interface Props extends ButtonProps {
|
||||
export function ScreenshotButton({ canvas, name, children, ...props }: Props) {
|
||||
const { strings } = useLocale();
|
||||
|
||||
const screenshot = useCallback(async () => {
|
||||
if (!canvas.current) {
|
||||
throw new Error("Canvas not found");
|
||||
}
|
||||
|
||||
screenshotReadyEvent.dispatch(true);
|
||||
|
||||
const blob = await new Promise<Blob>((resolve, reject) => {
|
||||
requestAnimationFrame(() => {
|
||||
canvas.current.toBlob((blob) => {
|
||||
screenshotReadyEvent.dispatch(false);
|
||||
|
||||
if (!blob) {
|
||||
reject(new Error("Failed to capture screenshot"));
|
||||
} else {
|
||||
resolve(blob);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const buffer = await blob.arrayBuffer();
|
||||
const image = await Jimp.fromBuffer(buffer);
|
||||
|
||||
image.autocrop();
|
||||
|
||||
return image;
|
||||
}, [canvas]);
|
||||
|
||||
return (
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
@@ -37,23 +67,13 @@ export function ScreenshotButton({ canvas, name, children, ...props }: Props) {
|
||||
<Flex direction="column" gap="2">
|
||||
<Popover.Close>
|
||||
<Button
|
||||
onClick={() => {
|
||||
screenshotReadyEvent.dispatch(true);
|
||||
onClick={async () => {
|
||||
const image = await screenshot();
|
||||
const anchor = document.createElement("a");
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (!canvas.current) return;
|
||||
|
||||
const anchor = document.createElement("a");
|
||||
|
||||
anchor.setAttribute("download", `${name}.png`);
|
||||
anchor.setAttribute(
|
||||
"href",
|
||||
canvas.current.toDataURL("image/png")
|
||||
);
|
||||
anchor.click();
|
||||
|
||||
screenshotReadyEvent.dispatch(false);
|
||||
});
|
||||
anchor.setAttribute("download", `${name}.png`);
|
||||
anchor.setAttribute("href", await image.getBase64("image/png"));
|
||||
anchor.click();
|
||||
}}
|
||||
>
|
||||
<DownloadIcon />
|
||||
@@ -63,22 +83,16 @@ export function ScreenshotButton({ canvas, name, children, ...props }: Props) {
|
||||
<Popover.Close>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
screenshotReadyEvent.dispatch(true);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (!canvas.current) return;
|
||||
|
||||
canvas.current.toBlob((blob) => {
|
||||
if (!blob) return;
|
||||
|
||||
navigator.clipboard.write([
|
||||
new ClipboardItem({ "image/png": blob }),
|
||||
]);
|
||||
});
|
||||
|
||||
screenshotReadyEvent.dispatch(false);
|
||||
onClick={async () => {
|
||||
const image = await screenshot();
|
||||
const buffer = await image.getBuffer("image/png");
|
||||
const blob = new Blob([buffer as unknown as ArrayBuffer], {
|
||||
type: "image/png",
|
||||
});
|
||||
|
||||
navigator.clipboard.write([
|
||||
new ClipboardItem({ "image/png": blob }),
|
||||
]);
|
||||
}}
|
||||
>
|
||||
<CopyIcon />
|
||||
|
||||
@@ -3,12 +3,12 @@ import {
|
||||
resolveDpm,
|
||||
unionBoundingBox,
|
||||
type TankDefinition,
|
||||
} from '@blitzkit/core';
|
||||
import { useMemo } from 'react';
|
||||
import { awaitableModelDefinitions } from '../../../core/awaitables/modelDefinitions';
|
||||
import { resolveReload } from '../../../core/blitzkit/resolveReload';
|
||||
import { TankSort } from '../../../stores/tankopediaSort';
|
||||
import { TankCard } from '../../TankCard';
|
||||
} from "@blitzkit/core";
|
||||
import { useMemo } from "react";
|
||||
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
|
||||
import { resolveReload } from "../../../core/blitzkit/resolveReload";
|
||||
import { TankSort } from "../../../stores/tankopediaSort";
|
||||
import { TankCard } from "../../TankCard";
|
||||
|
||||
interface TankSearchCardProps {
|
||||
tank: TankDefinition;
|
||||
@@ -20,7 +20,7 @@ const modelDefinitions = await awaitableModelDefinitions;
|
||||
export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
||||
const by = TankSort.use((state) => state.by);
|
||||
const discriminator = useMemo(() => {
|
||||
if (by.startsWith('meta')) return undefined;
|
||||
if (by.startsWith("meta")) return undefined;
|
||||
|
||||
const turret = tank.turrets.at(-1)!;
|
||||
const gun = turret.guns.at(-1)!;
|
||||
@@ -34,53 +34,53 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
||||
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||
|
||||
switch (by) {
|
||||
case 'fire.aimTime':
|
||||
case "fire.aimTime":
|
||||
return gun.aim_time.toFixed(2);
|
||||
case 'fire.caliber':
|
||||
case "fire.caliber":
|
||||
return shell0.caliber.toFixed(0);
|
||||
case 'fire.damage':
|
||||
case "fire.damage":
|
||||
return shell0.armor_damage.toFixed(0);
|
||||
case 'fire.moduleDamage':
|
||||
case "fire.moduleDamage":
|
||||
return shell0.module_damage.toFixed(0);
|
||||
case 'fire.dispersionMoving':
|
||||
case "fire.dispersionMoving":
|
||||
return tracks.dispersion_move.toFixed(3);
|
||||
case 'fire.dispersionStill':
|
||||
case "fire.dispersionStill":
|
||||
return gun.dispersion_base.toFixed(3);
|
||||
case 'fire.dpm':
|
||||
case "fire.dpm":
|
||||
return resolveDpm(gun, shell0).toFixed(0);
|
||||
case 'fire.dpmPremium':
|
||||
return shell1 ? resolveDpm(gun, shell1).toFixed(0) : '--';
|
||||
case 'fire.reload':
|
||||
case "fire.dpmPremium":
|
||||
return shell1 ? resolveDpm(gun, shell1).toFixed(0) : "--";
|
||||
case "fire.reload":
|
||||
return resolveReload(gun).toFixed(2);
|
||||
case 'fire.standardPenetration':
|
||||
return shell0.penetration.near.toFixed(0);
|
||||
case 'fire.premiumPenetration':
|
||||
return shell1 ? shell1.penetration.near.toFixed(0) : '--';
|
||||
case 'fire.tertiaryPenetration':
|
||||
return shell1 ? shell2.penetration.near.toFixed(0) : '--';
|
||||
case 'fire.shellVelocity':
|
||||
case "fire.standardPenetration":
|
||||
return shell0?.penetration.near.toFixed(0);
|
||||
case "fire.premiumPenetration":
|
||||
return shell1 ? shell1?.penetration.near.toFixed(0) : "--";
|
||||
case "fire.tertiaryPenetration":
|
||||
return shell1 ? shell2?.penetration.near.toFixed(0) : "--";
|
||||
case "fire.shellVelocity":
|
||||
return shell0.velocity.toFixed(0);
|
||||
case 'fire.shellCapacity':
|
||||
case "fire.shellCapacity":
|
||||
return gun.shell_capacity.toFixed(0);
|
||||
case 'fire.shellRange':
|
||||
case "fire.shellRange":
|
||||
return shell0.range.toFixed(0);
|
||||
case 'fire.gunDepression':
|
||||
case "fire.gunDepression":
|
||||
return (
|
||||
gunModelDefinition.pitch.max +
|
||||
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
|
||||
).toFixed(1);
|
||||
case 'fire.gunElevation':
|
||||
case "fire.gunElevation":
|
||||
return (
|
||||
-gunModelDefinition.pitch.min -
|
||||
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
|
||||
).toFixed(1);
|
||||
case 'maneuverability.forwardsSpeed':
|
||||
case "maneuverability.forwardsSpeed":
|
||||
return tank.speed_forwards.toFixed(0);
|
||||
case 'maneuverability.backwardsSpeed':
|
||||
case "maneuverability.backwardsSpeed":
|
||||
return tank.speed_backwards.toFixed(0);
|
||||
case 'maneuverability.power':
|
||||
case "maneuverability.power":
|
||||
return engine.power.toFixed(0);
|
||||
case 'maneuverability.powerToWeight':
|
||||
case "maneuverability.powerToWeight":
|
||||
return (
|
||||
(1000 * engine.power) /
|
||||
(tank.weight +
|
||||
@@ -89,7 +89,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
||||
turret.weight +
|
||||
gun.weight)
|
||||
).toFixed(1);
|
||||
case 'maneuverability.weight':
|
||||
case "maneuverability.weight":
|
||||
return (
|
||||
(tank.weight +
|
||||
engine.weight +
|
||||
@@ -98,19 +98,19 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
||||
gun.weight) /
|
||||
1000
|
||||
).toFixed(1);
|
||||
case 'maneuverability.traverseSpeed':
|
||||
case "maneuverability.traverseSpeed":
|
||||
return tracks.traverse_speed.toFixed(1);
|
||||
case 'survivability.health':
|
||||
case "survivability.health":
|
||||
return (tank.health + turret.health).toFixed(0);
|
||||
case 'survivability.viewRange':
|
||||
case "survivability.viewRange":
|
||||
return turret.view_range.toFixed(0);
|
||||
case 'survivability.camouflageStill':
|
||||
case "survivability.camouflageStill":
|
||||
return (tank.camouflage_still * 100).toFixed(0);
|
||||
case 'survivability.camouflageMoving':
|
||||
case "survivability.camouflageMoving":
|
||||
return (tank.camouflage_moving * 100).toFixed(0);
|
||||
case 'survivability.camouflageShooting':
|
||||
case "survivability.camouflageShooting":
|
||||
return (tank.camouflage_still * gun.camouflage_loss * 100).toFixed(0);
|
||||
case 'survivability.volume': {
|
||||
case "survivability.volume": {
|
||||
const dimensions = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
tankModelDefinition.bounding_box,
|
||||
@@ -121,7 +121,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
||||
return (dimensions.x * dimensions.y * dimensions.z).toFixed(0);
|
||||
}
|
||||
|
||||
case 'survivability.length': {
|
||||
case "survivability.length": {
|
||||
const bounds = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
tankModelDefinition.bounding_box,
|
||||
|
||||
@@ -129,7 +129,7 @@ const consumablesArray = Object.values(consumableDefinitions.consumables)
|
||||
.filter(
|
||||
(consumable) =>
|
||||
!consumable.game_mode_exclusive &&
|
||||
consumable.name.locales[locales.default]
|
||||
consumable.name.locales[locales.default],
|
||||
)
|
||||
.map((consumable) => consumable.id);
|
||||
const provisionsArray = Array.from(
|
||||
@@ -137,7 +137,7 @@ const provisionsArray = Array.from(
|
||||
.filter(
|
||||
(provision) =>
|
||||
!provision.game_mode_exclusive &&
|
||||
provision.name.locales[locales.default]
|
||||
provision.name.locales[locales.default],
|
||||
)
|
||||
.reduce((uniqueMap, consumable) => {
|
||||
if (!uniqueMap.has(consumable.name.locales[locales.default])) {
|
||||
@@ -146,7 +146,7 @@ const provisionsArray = Array.from(
|
||||
|
||||
return uniqueMap;
|
||||
}, new Map<string, number>())
|
||||
.values()
|
||||
.values(),
|
||||
);
|
||||
const shellTypeIcons: Record<ShellType, string> = {
|
||||
[ShellType.AP]: "ap",
|
||||
@@ -171,7 +171,7 @@ const TANK_TYPE_COLORS: Record<TankType, string> = {
|
||||
};
|
||||
|
||||
const GUN_TYPES = Object.keys(
|
||||
GUN_TYPE_ICONS
|
||||
GUN_TYPE_ICONS,
|
||||
) as (keyof typeof GUN_TYPE_ICONS)[];
|
||||
|
||||
const MAX_ICONS = 4;
|
||||
@@ -180,7 +180,12 @@ const TIERS = times(10, (i) => 10 - i);
|
||||
|
||||
export function FilterControl() {
|
||||
return (
|
||||
<Flex align="center" justify="center" gap="2" wrap="wrap">
|
||||
<Flex
|
||||
align="center"
|
||||
justify={{ initial: "start", md: "center" }}
|
||||
gap="2"
|
||||
wrap="wrap"
|
||||
>
|
||||
<TiersFilter />
|
||||
<ClassFilter />
|
||||
<TypeFilter />
|
||||
@@ -346,7 +351,7 @@ function NationsFilter() {
|
||||
mr="-2"
|
||||
style={{
|
||||
backgroundImage: `url(${asset(
|
||||
`flags/scratched/${nation}.webp`
|
||||
`flags/scratched/${nation}.webp`,
|
||||
)})`,
|
||||
backgroundSize: "100%",
|
||||
backgroundRepeat: "no-repeat",
|
||||
@@ -437,7 +442,7 @@ function ClassFilter() {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (selected) {
|
||||
draft.classes = draft.classes.filter(
|
||||
(c) => c !== tankClass
|
||||
(c) => c !== tankClass,
|
||||
);
|
||||
} else {
|
||||
draft.classes = [...draft.classes, tankClass];
|
||||
@@ -683,7 +688,7 @@ function IndividualShellFilter({
|
||||
src={asset(
|
||||
`icons/shells/${shellTypeIcons[shells[index]]}${
|
||||
premium ? "_premium" : ""
|
||||
}.webp`
|
||||
}.webp`,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -718,7 +723,7 @@ function IndividualShellFilter({
|
||||
src={asset(
|
||||
`icons/shells/${shellTypeIcons[shellType]}${
|
||||
premium ? "_premium" : ""
|
||||
}.webp`
|
||||
}.webp`,
|
||||
)}
|
||||
style={{ width: "1.25em", height: "1.25em" }}
|
||||
/>
|
||||
@@ -917,7 +922,7 @@ function ConsumablesFilter() {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (selected) {
|
||||
draft.consumables = draft.consumables.filter(
|
||||
(n) => n !== consumable
|
||||
(n) => n !== consumable,
|
||||
);
|
||||
} else {
|
||||
draft.consumables = [...draft.consumables, consumable];
|
||||
@@ -1011,7 +1016,7 @@ function ProvisionsFilter() {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (selected) {
|
||||
draft.provisions = draft.provisions.filter(
|
||||
(n) => n !== provision
|
||||
(n) => n !== provision,
|
||||
);
|
||||
} else {
|
||||
draft.provisions = [...draft.provisions, provision];
|
||||
@@ -1140,7 +1145,7 @@ function GameModeAbilitiesFilter() {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (selected) {
|
||||
draft.abilities = draft.abilities.filter(
|
||||
(n) => n !== consumable
|
||||
(n) => n !== consumable,
|
||||
);
|
||||
} else {
|
||||
draft.abilities = [...draft.abilities, consumable];
|
||||
@@ -1189,7 +1194,7 @@ function GameModeAbilitiesFilter() {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (selected) {
|
||||
draft.powers = draft.powers.filter(
|
||||
(n) => n !== provision
|
||||
(n) => n !== provision,
|
||||
);
|
||||
} else {
|
||||
draft.powers = [...draft.powers, provision];
|
||||
|
||||
+4
-2
@@ -30,9 +30,11 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
||||
<Flex direction="column" mb="6" align="center">
|
||||
<VisualizerCard
|
||||
mb="0"
|
||||
style={{ width: "100%" }}
|
||||
style={{ width: "100%", touchAction: "none" }}
|
||||
ref={container}
|
||||
onPointerMove={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!container.current || explodes) return;
|
||||
|
||||
const rect = container.current.getBoundingClientRect();
|
||||
@@ -174,7 +176,7 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
||||
? "-"
|
||||
: literals(strings.common.units.deg, {
|
||||
value: radToDeg(
|
||||
Math.min(normalization, Math.abs(angle))
|
||||
Math.min(normalization, Math.abs(angle)),
|
||||
).toFixed(0),
|
||||
})
|
||||
}
|
||||
|
||||
+15
-15
@@ -55,17 +55,17 @@ export function Controls({
|
||||
const protagonistHullOrigin = new Vector3(
|
||||
protagonistTrackModelDefinition.origin.x,
|
||||
protagonistTrackModelDefinition.origin.y,
|
||||
-protagonistTrackModelDefinition.origin.z
|
||||
-protagonistTrackModelDefinition.origin.z,
|
||||
);
|
||||
const protagonistTurretOrigin = new Vector3(
|
||||
protagonistModelDefinition.turret_origin.x,
|
||||
protagonistModelDefinition.turret_origin.y,
|
||||
-protagonistModelDefinition.turret_origin.z
|
||||
-protagonistModelDefinition.turret_origin.z,
|
||||
);
|
||||
const protagonistGunOrigin = new Vector3(
|
||||
protagonistTurretModelDefinition.gun_origin.x,
|
||||
protagonistTurretModelDefinition.gun_origin.y,
|
||||
-protagonistTurretModelDefinition.gun_origin.z
|
||||
-protagonistTurretModelDefinition.gun_origin.z,
|
||||
);
|
||||
const antagonistGunHeight =
|
||||
protagonistTrackModelDefinition.origin.y +
|
||||
@@ -106,12 +106,12 @@ export function Controls({
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
Duel.state.protagonist.tank.equipment_preset,
|
||||
Duel.state.protagonist.equipmentMatrix
|
||||
Duel.state.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
Duel.state.protagonist.tank.equipment_preset,
|
||||
Duel.state.protagonist.equipmentMatrix
|
||||
Duel.state.protagonist.equipmentMatrix,
|
||||
);
|
||||
|
||||
switch (event) {
|
||||
@@ -122,7 +122,7 @@ export function Controls({
|
||||
protagonistGunModelDefinition.pitch,
|
||||
protagonistTurretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
|
||||
camera.position
|
||||
@@ -134,14 +134,14 @@ export function Controls({
|
||||
new Vector3(
|
||||
0,
|
||||
poseDistances[event] * Math.sin(pitch),
|
||||
poseDistances[event] * -Math.cos(pitch)
|
||||
)
|
||||
poseDistances[event] * -Math.cos(pitch),
|
||||
),
|
||||
);
|
||||
camera.lookAt(
|
||||
protagonistHullOrigin
|
||||
.clone()
|
||||
.add(protagonistTurretOrigin)
|
||||
.add(protagonistGunOrigin)
|
||||
.add(protagonistGunOrigin),
|
||||
);
|
||||
orbitControls.current?.target.set(0, antagonistGunHeight, 0);
|
||||
|
||||
@@ -155,7 +155,7 @@ export function Controls({
|
||||
protagonistGunModelDefinition.pitch,
|
||||
protagonistTurretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
|
||||
camera.position
|
||||
@@ -167,8 +167,8 @@ export function Controls({
|
||||
new Vector3(
|
||||
0,
|
||||
poseDistances[event] * Math.sin(pitch),
|
||||
poseDistances[event] * -Math.cos(pitch)
|
||||
)
|
||||
poseDistances[event] * -Math.cos(pitch),
|
||||
),
|
||||
);
|
||||
orbitControls.current?.target
|
||||
.set(0, 0, 0)
|
||||
@@ -179,8 +179,8 @@ export function Controls({
|
||||
new Vector3(
|
||||
0,
|
||||
0.5 * poseDistances[event] * Math.sin(pitch),
|
||||
0.5 * poseDistances[event] * -Math.cos(pitch)
|
||||
)
|
||||
0.5 * poseDistances[event] * -Math.cos(pitch),
|
||||
),
|
||||
);
|
||||
|
||||
break;
|
||||
@@ -251,7 +251,7 @@ export function Controls({
|
||||
return (
|
||||
<OrbitControls
|
||||
enableRotate={enableRotate}
|
||||
maxDistance={20}
|
||||
maxDistance={40}
|
||||
minDistance={5}
|
||||
enableZoom={zoomable}
|
||||
zoomSpeed={zoomable ? undefined : 0}
|
||||
|
||||
+6
-8
@@ -47,7 +47,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
const gunModelDefinition = turretModelDefinition.guns[protagonistGun.id];
|
||||
const display = Tankopedia.use((state) => state.display);
|
||||
const hideTankModelUnderArmor = TankopediaPersistent.use(
|
||||
(state) => state.hideTankModelUnderArmor
|
||||
(state) => state.hideTankModelUnderArmor,
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => canvas.current!, []);
|
||||
@@ -76,7 +76,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
@@ -90,7 +90,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
0,
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer
|
||||
hasImprovedVerticalStabilizer,
|
||||
);
|
||||
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
@@ -104,7 +104,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
0,
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer
|
||||
hasImprovedVerticalStabilizer,
|
||||
);
|
||||
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
@@ -126,7 +126,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
modelTransformEvent.last!.yaw,
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer
|
||||
hasImprovedVerticalStabilizer,
|
||||
);
|
||||
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
@@ -146,8 +146,6 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
}
|
||||
}, [display]);
|
||||
|
||||
console.log(TankopediaDisplay[display]);
|
||||
|
||||
return (
|
||||
<SmartCanvas
|
||||
resize={{ debounce: 0 }}
|
||||
@@ -196,5 +194,5 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
<InitialAligner />
|
||||
</SmartCanvas>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -2,9 +2,9 @@ import {
|
||||
normalizeAngleRad,
|
||||
type PitchLimits,
|
||||
type YawLimits,
|
||||
} from '@blitzkit/core';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||
} from "@blitzkit/core";
|
||||
import { clamp } from "lodash-es";
|
||||
import { degToRad } from "three/src/math/MathUtils.js";
|
||||
|
||||
export const DEFAULT_PITCH_TRANSITION = 15;
|
||||
|
||||
@@ -87,5 +87,5 @@ export function applyPitchYawLimits(
|
||||
|
||||
newPitch = clamp(newPitch, lowerPitch, upperPitch);
|
||||
|
||||
return [newPitch, newYaw] as const;
|
||||
return [newPitch, normalizeAngleRad(newYaw)] as const;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import { Mesh, Object3D } from "three";
|
||||
import { Mesh, MeshStandardMaterial, Object3D } from "three";
|
||||
|
||||
export function jsxTree(
|
||||
node: Object3D,
|
||||
@@ -7,14 +7,14 @@ export function jsxTree(
|
||||
mesh?: (
|
||||
mesh: Mesh,
|
||||
props: ComponentProps<"mesh">,
|
||||
key: string
|
||||
key: string,
|
||||
) => ReactNode;
|
||||
group?: (
|
||||
object3d: Object3D,
|
||||
props: ComponentProps<"group">,
|
||||
key: string
|
||||
key: string,
|
||||
) => ReactNode;
|
||||
}
|
||||
},
|
||||
): ReactNode {
|
||||
if (node instanceof Mesh) {
|
||||
const props = {
|
||||
@@ -25,6 +25,10 @@ export function jsxTree(
|
||||
scale: node.scale,
|
||||
};
|
||||
|
||||
if (props.material instanceof MeshStandardMaterial) {
|
||||
props.material.depthWrite = true;
|
||||
}
|
||||
|
||||
return mergers?.mesh ? (
|
||||
mergers.mesh(node, props, node.uuid)
|
||||
) : (
|
||||
|
||||
@@ -5,11 +5,11 @@ export function getBestLocale() {
|
||||
localStorage.getItem("preferred-locale") ??
|
||||
navigator.language.split("-")[0];
|
||||
const isSupported = locales.supported.some(
|
||||
({ locale }) => locale === desiredLocale
|
||||
({ locale }) => locale === desiredLocale,
|
||||
);
|
||||
const isDefault = desiredLocale === locales.default;
|
||||
|
||||
if (!isSupported || isDefault) return undefined;
|
||||
if (!isSupported || isDefault) return locales.default;
|
||||
|
||||
return desiredLocale;
|
||||
}
|
||||
@@ -12,7 +12,8 @@ export const getStaticPaths = (async () => {
|
||||
.map((tank) =>
|
||||
parentPaths.map((parent) => ({
|
||||
params: { ...parent.params, id: tank.id },
|
||||
}))
|
||||
props: parent.props,
|
||||
})),
|
||||
)
|
||||
.flat();
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
Reference in new issue
Block a user