mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: moved to astro
This commit is contained in:
1 parent
292df5f5d9
commit
a1721eb05e
150 files changed
+386
-363
No files matched your search
@@ -0,0 +1,80 @@
|
||||
import { normalizeAngleRad, PitchLimits, YawLimits } from '@blitzkit/core';
|
||||
import { clamp } from 'lodash';
|
||||
import { degToRad } from 'three/src/math/MathUtils';
|
||||
|
||||
export function applyPitchYawLimits(
|
||||
pitch: number, // rad
|
||||
yaw: number, // rad
|
||||
pitchLimits: PitchLimits, // deg
|
||||
yawLimits?: YawLimits, // deg
|
||||
verticalStabilizer = false,
|
||||
) {
|
||||
let newPitch = pitch;
|
||||
let newYaw = yaw;
|
||||
|
||||
if (yawLimits) {
|
||||
newYaw = normalizeAngleRad(
|
||||
clamp(newYaw, -degToRad(yawLimits.max), -degToRad(yawLimits.min)),
|
||||
);
|
||||
}
|
||||
|
||||
const depressionBuff = verticalStabilizer ? degToRad(3) : 0;
|
||||
const elevationBuff = verticalStabilizer ? degToRad(4) : 0;
|
||||
let lowerPitch = -degToRad(pitchLimits.max) - depressionBuff;
|
||||
let upperPitch = -degToRad(pitchLimits.min) + elevationBuff;
|
||||
const transition = degToRad(pitchLimits.transition ?? 10);
|
||||
|
||||
if (pitchLimits.back) {
|
||||
const range = degToRad(pitchLimits.back.range);
|
||||
const yawRotatedAbs = Math.abs(normalizeAngleRad(newYaw - Math.PI));
|
||||
|
||||
if (yawRotatedAbs <= range + transition) {
|
||||
if (yawRotatedAbs <= range) {
|
||||
// inside range
|
||||
lowerPitch = -degToRad(pitchLimits.back.max);
|
||||
upperPitch = -degToRad(pitchLimits.back.min);
|
||||
} else {
|
||||
// inside transition
|
||||
const transitionProgress = (yawRotatedAbs - range) / transition;
|
||||
const maxInterpolatedPitch =
|
||||
(1 - transitionProgress) * pitchLimits.back.max +
|
||||
transitionProgress * pitchLimits.max;
|
||||
const minInterpolatedPitch =
|
||||
(1 - transitionProgress) * pitchLimits.back.min +
|
||||
transitionProgress * pitchLimits.min;
|
||||
|
||||
lowerPitch = -degToRad(maxInterpolatedPitch);
|
||||
upperPitch = -degToRad(minInterpolatedPitch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pitchLimits.front) {
|
||||
const range = degToRad(pitchLimits.front.range);
|
||||
const yawAbs = Math.abs(normalizeAngleRad(newYaw));
|
||||
|
||||
if (yawAbs <= range + transition) {
|
||||
if (yawAbs <= range) {
|
||||
// inside range
|
||||
lowerPitch = -degToRad(pitchLimits.front.max);
|
||||
upperPitch = -degToRad(pitchLimits.front.min);
|
||||
} else {
|
||||
// inside transition
|
||||
const transitionProgress = (yawAbs - range) / transition;
|
||||
const maxInterpolatedPitch =
|
||||
(1 - transitionProgress) * pitchLimits.front.max +
|
||||
transitionProgress * pitchLimits.max;
|
||||
const minInterpolatedPitch =
|
||||
(1 - transitionProgress) * pitchLimits.front.min +
|
||||
transitionProgress * pitchLimits.min;
|
||||
|
||||
lowerPitch = -degToRad(maxInterpolatedPitch);
|
||||
upperPitch = -degToRad(minInterpolatedPitch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newPitch = clamp(newPitch, lowerPitch, upperPitch);
|
||||
|
||||
return [newPitch, newYaw];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Vector3, Vector3Tuple } from 'three';
|
||||
import { rotateDavaVector } from './rotateDavaVector';
|
||||
|
||||
export function correctZYTuple(tuple: Vector3Tuple) {
|
||||
return rotateDavaVector(new Vector3(tuple[0], tuple[1], -tuple[2]));
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { I_HAT } from '@blitzkit/core';
|
||||
import { Vector3 } from 'three';
|
||||
|
||||
export function rotateDavaVector(vector: Vector3) {
|
||||
return vector.applyAxisAngle(I_HAT, Math.PI / 2);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { I_HAT } from '@blitzkit/core';
|
||||
import { Vector3 } from 'three';
|
||||
|
||||
export function unrotateDavaVector(vector: Vector3) {
|
||||
return vector.applyAxisAngle(I_HAT, -Math.PI / 2);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { TankDefinition } from '@blitzkit/core';
|
||||
import { TankFilters } from '../../stores/tankFilters';
|
||||
|
||||
export function filterTank(filters: TankFilters, tank: TankDefinition) {
|
||||
return (
|
||||
(filters.tiers.length === 0 || filters.tiers.includes(tank.tier)) &&
|
||||
(filters.nations.length === 0 || filters.nations.includes(tank.nation)) &&
|
||||
(filters.classes.length === 0 || filters.classes.includes(tank.class)) &&
|
||||
(filters.types.length === 0 || filters.types.includes(tank.treeType)) &&
|
||||
(filters.testing === 'include' ||
|
||||
(filters.testing === 'only' && tank.testing) ||
|
||||
(filters.testing === 'exclude' && !tank.testing))
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { equipmentDefinitions } from '@blitzkit/core';
|
||||
import { EquipmentMatrix } from '../../stores/duel';
|
||||
|
||||
export async function hasEquipment(
|
||||
id: number,
|
||||
equipmentPreset: string,
|
||||
equipmentMatrix: EquipmentMatrix,
|
||||
) {
|
||||
const awaitedEquipmentDefinitions = await equipmentDefinitions;
|
||||
const presetRows = awaitedEquipmentDefinitions.presets[equipmentPreset];
|
||||
return presetRows.some((equipmentRow, rowIndex) => {
|
||||
return equipmentRow.some((equipment, columnIndex) => {
|
||||
const choice = equipmentMatrix[rowIndex][columnIndex];
|
||||
if (choice === 0) return false;
|
||||
const equipped = equipment[choice === -1 ? 0 : 1];
|
||||
return equipped === id;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { GroupProps, MeshProps } from '@react-three/fiber';
|
||||
import { Mesh, Object3D } from 'three';
|
||||
|
||||
export function jsxTree(
|
||||
node: Object3D,
|
||||
mergers?: {
|
||||
mesh?: MeshProps | ((mesh: Mesh) => MeshProps);
|
||||
group?: GroupProps | ((object3d: Object3D) => GroupProps);
|
||||
},
|
||||
key?: string,
|
||||
) {
|
||||
if (node instanceof Mesh) {
|
||||
return (
|
||||
<mesh
|
||||
key={key}
|
||||
geometry={node.geometry}
|
||||
material={node.material}
|
||||
position={node.position}
|
||||
rotation={node.rotation}
|
||||
scale={node.scale}
|
||||
{...(mergers?.mesh &&
|
||||
(typeof mergers.mesh === 'function'
|
||||
? mergers.mesh(node)
|
||||
: mergers.mesh))}
|
||||
/>
|
||||
);
|
||||
} else if (node instanceof Object3D) {
|
||||
return (
|
||||
<group
|
||||
key={key}
|
||||
position={node.position}
|
||||
rotation={node.rotation}
|
||||
scale={node.scale}
|
||||
{...(mergers?.group &&
|
||||
(typeof mergers.group === 'function'
|
||||
? mergers.group(node)
|
||||
: mergers.group))}
|
||||
>
|
||||
{node.children.map((child) => jsxTree(child, mergers, child.uuid))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { EventManager } from '@blitzkit/core';
|
||||
import { invalidate } from '@react-three/fiber';
|
||||
|
||||
export interface ModelTransformEventData {
|
||||
yaw?: number;
|
||||
pitch: number;
|
||||
}
|
||||
|
||||
export const modelTransformEvent = new EventManager<ModelTransformEventData>();
|
||||
modelTransformEvent.on(() => invalidate());
|
||||
@@ -0,0 +1,6 @@
|
||||
export function nameToArmorId(name: string) {
|
||||
const match = name.match(/.+_armor_(-?\d+)/);
|
||||
const group1 = match![1];
|
||||
|
||||
return parseInt(group1);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { EventManager } from '@blitzkit/core';
|
||||
|
||||
export enum Pose {
|
||||
HullDown,
|
||||
FaceHug,
|
||||
Default,
|
||||
}
|
||||
|
||||
export const poseEvent = new EventManager<Pose>();
|
||||
@@ -0,0 +1,3 @@
|
||||
export function pushTankopediaPath(id: number) {
|
||||
window.history.pushState(null, '', `/tools/tankopedia/${id}`);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
const pattern = /\d+$/;
|
||||
|
||||
export function resolveArmorIndex(name: string) {
|
||||
const match = name.match(pattern)?.[0];
|
||||
return match ? parseInt(match) : undefined;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { GunDefinition } from '@blitzkit/core';
|
||||
|
||||
export function resolveReload(gun: GunDefinition) {
|
||||
if (gun.type === 'regular') {
|
||||
return gun.reload;
|
||||
} else if (gun.type === 'autoLoader') {
|
||||
return gun.reload + (gun.count - 1) * gun.intraClip;
|
||||
} else {
|
||||
return (
|
||||
gun.reload.reduce((a, b) => a + b, 0) + (gun.count - 1) * gun.intraClip
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ModelArmor } from '@blitzkit/core';
|
||||
|
||||
export function resolveArmor(armor: ModelArmor, index: number) {
|
||||
const spaced = armor.spaced?.includes(index) ?? false;
|
||||
const thickness = index === -1 ? 0 : (armor.thickness[index] ?? 0);
|
||||
|
||||
return { spaced, thickness };
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
import {
|
||||
coefficient,
|
||||
degressiveStat,
|
||||
EngineDefinition,
|
||||
EquipmentDefinitions,
|
||||
GunDefinition,
|
||||
isExplosive,
|
||||
ModelDefinition,
|
||||
normalizeBoundingBox,
|
||||
progressiveStat,
|
||||
ProvisionDefinitions,
|
||||
resolveDpm,
|
||||
resolveNearPenetration,
|
||||
resolvePenetrationCoefficient,
|
||||
ShellDefinition,
|
||||
sum,
|
||||
TankDefinition,
|
||||
TrackDefinition,
|
||||
TurretDefinition,
|
||||
unionBoundingBox,
|
||||
} from '@blitzkit/core';
|
||||
import { EquipmentMatrix } from '../../stores/duel';
|
||||
|
||||
export type TankCharacteristics = ReturnType<typeof tankCharacteristics>;
|
||||
|
||||
export function tankCharacteristics(
|
||||
{
|
||||
tank,
|
||||
turret,
|
||||
gun,
|
||||
shell,
|
||||
consumables,
|
||||
equipmentMatrix,
|
||||
crewMastery,
|
||||
provisions,
|
||||
engine,
|
||||
crewSkills,
|
||||
camouflage,
|
||||
track,
|
||||
stockEngine,
|
||||
stockTrack,
|
||||
stockTurret,
|
||||
stockGun,
|
||||
applyReactiveArmor,
|
||||
applyDynamicArmor,
|
||||
applySpallLiner,
|
||||
}: {
|
||||
tank: TankDefinition;
|
||||
turret: TurretDefinition;
|
||||
gun: GunDefinition;
|
||||
shell: ShellDefinition;
|
||||
track: TrackDefinition;
|
||||
engine: EngineDefinition;
|
||||
stockEngine: EngineDefinition;
|
||||
stockTrack: TrackDefinition;
|
||||
stockTurret: TurretDefinition;
|
||||
stockGun: GunDefinition;
|
||||
consumables: number[];
|
||||
equipmentMatrix: EquipmentMatrix;
|
||||
crewMastery: number;
|
||||
provisions: number[];
|
||||
crewSkills: Record<string, number>;
|
||||
camouflage: boolean;
|
||||
applyReactiveArmor: boolean;
|
||||
applyDynamicArmor: boolean;
|
||||
applySpallLiner: boolean;
|
||||
},
|
||||
{
|
||||
tankModelDefinition,
|
||||
equipmentDefinitions,
|
||||
provisionDefinitions,
|
||||
}: {
|
||||
tankModelDefinition: ModelDefinition;
|
||||
equipmentDefinitions: EquipmentDefinitions;
|
||||
provisionDefinitions: ProvisionDefinitions;
|
||||
},
|
||||
) {
|
||||
const presetRows = equipmentDefinitions.presets[tank.equipment];
|
||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||
|
||||
function equipment(id: number) {
|
||||
return presetRows.some((equipmentRow, rowIndex) => {
|
||||
return equipmentRow.some((equipment, columnIndex) => {
|
||||
const choice = equipmentMatrix[rowIndex][columnIndex];
|
||||
if (choice === 0) return false;
|
||||
const equipped = equipment[choice === -1 ? 0 : 1];
|
||||
return equipped === id;
|
||||
});
|
||||
});
|
||||
}
|
||||
function consumable(id: number) {
|
||||
return consumables.includes(id);
|
||||
}
|
||||
function provision(id: number) {
|
||||
return provisions.includes(id);
|
||||
}
|
||||
|
||||
const hasGunRammer = equipment(100);
|
||||
const hasImprovedVentilation = equipment(102);
|
||||
const hasCalibratedShells = equipment(103);
|
||||
const hasEnhancedGunLayingDrive = equipment(104);
|
||||
const hasSupercharger = equipment(107);
|
||||
const hasVerticalStabilizer = equipment(105);
|
||||
const hasRefinedGun = equipment(106);
|
||||
const hasImprovedAssembly = equipment(111);
|
||||
const hasImprovedOptics = equipment(114);
|
||||
const hasImprovedControl = equipment(116);
|
||||
const hasEngineAccelerator = equipment(117);
|
||||
const hasCamouflageNet = equipment(115);
|
||||
const hasImprovedVerticalStabilizer = equipment(122);
|
||||
const hasImprovedSuspension = equipment(123);
|
||||
|
||||
const hasTungsten = consumable(45);
|
||||
const hasAdrenaline = consumable(18);
|
||||
const hasShellReloadBoost = consumable(29);
|
||||
const hasEnginePowerBoost = consumable(14);
|
||||
const hasImprovedEnginePowerBoost = consumable(27);
|
||||
const hasReticleCalibration = consumable(28);
|
||||
const hasReducedEnginePowerBoost = consumable(80);
|
||||
|
||||
const hasStandardFuel = provision(18);
|
||||
const hasImprovedFuel = provision(19);
|
||||
const hasProtectiveKit = provision(22);
|
||||
const hasSandbagArmor = provision(28);
|
||||
const hasEnhancedSandbagArmor = provision(29);
|
||||
const hasImprovedGunPowder = provision(46);
|
||||
const hasGearOil = provision(44);
|
||||
const hasImprovedGearOil = provision(45);
|
||||
|
||||
const gunDefaultPitch = tankModelDefinition.turretRotation?.pitch ?? 0;
|
||||
const provisionCrewBonus =
|
||||
provisions.reduce(
|
||||
(total, provision) =>
|
||||
provisionDefinitions[provision].crew
|
||||
? total + provisionDefinitions[provision].crew! / 100
|
||||
: total,
|
||||
0,
|
||||
) + (hasImprovedVentilation ? 0.08 : 0);
|
||||
const commanderMastery = crewMastery + provisionCrewBonus;
|
||||
const loaderMastery =
|
||||
commanderMastery *
|
||||
(tank.crew.some(({ type }) => type === 'loader') ? 1.1 : 1.05);
|
||||
const gunnerMastery =
|
||||
commanderMastery *
|
||||
(tank.crew.some(({ type }) => type === 'gunner') ? 1.1 : 1.05);
|
||||
const driverMastery =
|
||||
commanderMastery *
|
||||
(tank.crew.some(({ type }) => type === 'driver') ? 1.1 : 1.05);
|
||||
const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]);
|
||||
const damageCoefficient =
|
||||
coefficient([hasTungsten, 0.15]) *
|
||||
coefficient(
|
||||
[applyReactiveArmor, -0.27],
|
||||
[applyDynamicArmor, -0.1],
|
||||
[applySpallLiner && shell.type === 'he', -0.2],
|
||||
);
|
||||
const reloadCoefficient =
|
||||
(coefficient([hasGunRammer, -0.05]) *
|
||||
coefficient([true, degressiveStat(loaderMastery)])) /
|
||||
coefficient([hasAdrenaline && gun.type === 'regular', 0.17]);
|
||||
const penetrationCoefficient = coefficient([
|
||||
hasCalibratedShells,
|
||||
resolvePenetrationCoefficient(true, shell.type) - 1,
|
||||
]);
|
||||
const healthCoefficient = coefficient(
|
||||
[hasSandbagArmor, 0.03],
|
||||
[hasEnhancedSandbagArmor, 0.06],
|
||||
[hasImprovedAssembly, 0.04],
|
||||
);
|
||||
const shellVelocityCoefficient = coefficient(
|
||||
[hasSupercharger, 0.35],
|
||||
[hasImprovedGunPowder, 0.3],
|
||||
);
|
||||
const penetrationLossOverDistanceCoefficient = coefficient([
|
||||
hasSupercharger,
|
||||
-0.6,
|
||||
]);
|
||||
const aimTimeCoefficient =
|
||||
coefficient(
|
||||
[hasEnhancedGunLayingDrive, -0.15],
|
||||
[hasReticleCalibration, -0.3],
|
||||
) * coefficient([true, degressiveStat(gunnerMastery)]);
|
||||
const dispersionStillCoefficient =
|
||||
coefficient([hasRefinedGun, -0.15], [hasReticleCalibration, -0.3]) *
|
||||
coefficient([true, degressiveStat(gunnerMastery)]);
|
||||
const dispersionDamagedCoefficient = coefficient([
|
||||
hasVerticalStabilizer,
|
||||
-0.12,
|
||||
]);
|
||||
const dispersionHullTraverseCoefficient = coefficient(
|
||||
[hasVerticalStabilizer, -0.15],
|
||||
[true, -crewSkills.smooth_turn / 100],
|
||||
);
|
||||
const dispersionAfterShotCoefficient = coefficient(
|
||||
[hasVerticalStabilizer, -0.15],
|
||||
[true, -crewSkills.soft_recoil / 100],
|
||||
);
|
||||
const dispersionMovingCoefficient = coefficient(
|
||||
[hasVerticalStabilizer, -0.15],
|
||||
[true, -crewSkills.smooth_driving / 100],
|
||||
);
|
||||
const dispersionTurretTraverseCoefficient = coefficient(
|
||||
[hasVerticalStabilizer, -0.15],
|
||||
[true, -crewSkills.smooth_turret * (2 / 100)],
|
||||
);
|
||||
const enginePowerCoefficient = coefficient(
|
||||
[
|
||||
hasEngineAccelerator,
|
||||
tank.class === 'heavyTank' || tank.class === 'AT-SPG' ? 0.05 : 0.04,
|
||||
],
|
||||
[hasReducedEnginePowerBoost, 0.1],
|
||||
[hasEnginePowerBoost, 0.2],
|
||||
[hasImprovedEnginePowerBoost, 0.35],
|
||||
[hasStandardFuel, 0.03],
|
||||
[hasImprovedFuel, 0.1],
|
||||
[hasGearOil, 0.03],
|
||||
[hasImprovedGearOil, 0.06],
|
||||
);
|
||||
const turretTraverseCoefficient =
|
||||
coefficient([hasStandardFuel, 0.03], [hasImprovedFuel, 0.1]) *
|
||||
coefficient([true, progressiveStat(gunnerMastery)]);
|
||||
const hullTraverseCoefficient =
|
||||
coefficient(
|
||||
[hasImprovedControl, 0.11],
|
||||
[hasImprovedEnginePowerBoost, 0.01],
|
||||
[true, crewSkills.virtuoso / 100],
|
||||
) * coefficient([true, progressiveStat(driverMastery)]);
|
||||
const resistanceHardCoefficient = coefficient([hasImprovedSuspension, -0.2]);
|
||||
const resistanceMediumCoefficient = coefficient([
|
||||
hasImprovedSuspension,
|
||||
-0.15,
|
||||
]);
|
||||
const resistanceSoftCoefficient = coefficient([hasImprovedSuspension, -0.3]);
|
||||
const viewRangeCoefficient =
|
||||
coefficient([
|
||||
hasImprovedOptics,
|
||||
tank.class === 'heavyTank'
|
||||
? 0.06
|
||||
: tank.class === 'mediumTank'
|
||||
? 0.08
|
||||
: tank.class === 'lightTank'
|
||||
? 0.11
|
||||
: 0.04,
|
||||
]) * coefficient([true, progressiveStat(commanderMastery)]);
|
||||
const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]);
|
||||
|
||||
const speedForwardsSum = sum(
|
||||
[hasImprovedEnginePowerBoost, 4],
|
||||
[hasGearOil, 2],
|
||||
[hasImprovedGearOil, 4],
|
||||
);
|
||||
const speedBackwardsSum = sum(
|
||||
[hasImprovedEnginePowerBoost, 8],
|
||||
[hasGearOil, 2],
|
||||
[hasImprovedGearOil, 4],
|
||||
);
|
||||
const camouflageCoefficientMoving = sum(
|
||||
[
|
||||
true,
|
||||
coefficient([
|
||||
camouflage,
|
||||
tank.class === 'AT-SPG'
|
||||
? 0.04
|
||||
: tank.class === 'heavyTank'
|
||||
? 0.03
|
||||
: 0.02,
|
||||
]),
|
||||
],
|
||||
[
|
||||
hasCamouflageNet,
|
||||
tank.class === 'heavyTank'
|
||||
? 0.02
|
||||
: tank.class === 'mediumTank'
|
||||
? 0.04
|
||||
: tank.class === 'lightTank'
|
||||
? 0.07
|
||||
: 0.1,
|
||||
],
|
||||
);
|
||||
const camouflageCoefficientStill = sum(
|
||||
[
|
||||
true,
|
||||
coefficient(
|
||||
[
|
||||
camouflage,
|
||||
tank.class === 'AT-SPG'
|
||||
? 0.04
|
||||
: tank.class === 'heavyTank'
|
||||
? 0.03
|
||||
: 0.02,
|
||||
],
|
||||
[true, crewSkills.camouflage * (3 / 100)],
|
||||
),
|
||||
],
|
||||
[
|
||||
hasCamouflageNet,
|
||||
tank.class === 'heavyTank'
|
||||
? 0.04
|
||||
: tank.class === 'mediumTank'
|
||||
? 0.08
|
||||
: tank.class === 'lightTank'
|
||||
? 0.14
|
||||
: 0.2,
|
||||
],
|
||||
);
|
||||
|
||||
const size = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
tankModelDefinition.boundingBox,
|
||||
turretModelDefinition.boundingBox,
|
||||
),
|
||||
);
|
||||
const weightKg =
|
||||
tank.weight + engine.weight + track.weight + turret.weight + gun.weight;
|
||||
const weightTons = weightKg / 1000;
|
||||
const stockWeight =
|
||||
tank.weight +
|
||||
stockEngine.weight +
|
||||
stockTrack.weight +
|
||||
stockTurret.weight +
|
||||
stockGun.weight;
|
||||
const resolvedEnginePower = engine.power * enginePowerCoefficient;
|
||||
const dpm = resolveDpm(
|
||||
gun,
|
||||
shell,
|
||||
damageCoefficient,
|
||||
reloadCoefficient,
|
||||
intraClipCoefficient,
|
||||
);
|
||||
const intraClip =
|
||||
gun.type === 'regular' ? undefined : gun.intraClip * intraClipCoefficient;
|
||||
const mostOptimalShellIndex =
|
||||
gun.type === 'autoReloader'
|
||||
? gun.reload.reduce<null | { index: number; reload: number }>(
|
||||
(current, reloadRaw, index) => {
|
||||
const reload =
|
||||
reloadRaw * reloadCoefficient + (index > 0 ? intraClip! : 0);
|
||||
|
||||
if (current === null || reload < current.reload) {
|
||||
return { index, reload };
|
||||
}
|
||||
return current;
|
||||
},
|
||||
null,
|
||||
)!.index
|
||||
: undefined;
|
||||
const damage = shell.damage.armor * damageCoefficient;
|
||||
const dpmEffective =
|
||||
gun.type === 'autoReloader'
|
||||
? gun.reload[0] > gun.reload[1] + gun.intraClip
|
||||
? ((damageCoefficient * shell.damage.armor) /
|
||||
(gun.reload.at(-1)! * reloadCoefficient +
|
||||
gun.intraClip * intraClipCoefficient)) *
|
||||
(60 -
|
||||
(gun.reload.slice(0, -1).length - 1) *
|
||||
gun.intraClip *
|
||||
intraClipCoefficient) +
|
||||
damageCoefficient *
|
||||
shell.damage.armor *
|
||||
gun.reload.slice(0, -1).length
|
||||
: ((damageCoefficient * shell.damage.armor) /
|
||||
(gun.reload[0] * reloadCoefficient +
|
||||
gun.intraClip * intraClipCoefficient)) *
|
||||
(60 -
|
||||
(gun.reload.slice(1).length - 1) *
|
||||
gun.intraClip *
|
||||
intraClipCoefficient) +
|
||||
damageCoefficient * shell.damage.armor * gun.reload.slice(1).length
|
||||
: undefined;
|
||||
const shells = gun.type === 'regular' ? 1 : gun.count;
|
||||
const shellReloads =
|
||||
gun.type === 'autoReloader'
|
||||
? gun.reload.map((reload) => reload * reloadCoefficient)
|
||||
: undefined;
|
||||
const shellReload =
|
||||
gun.type === 'autoReloader' ? undefined : gun.reload * reloadCoefficient;
|
||||
const caliber = shell.caliber;
|
||||
const penetration =
|
||||
resolveNearPenetration(shell.penetration) * penetrationCoefficient;
|
||||
const clipDamage = gun.type === 'regular' ? undefined : gun.count * damage;
|
||||
const moduleDamage = shell.damage.module * damageCoefficient;
|
||||
const explosionRadius = shell.explosionRadius;
|
||||
const shellVelocity = shell.speed * shellVelocityCoefficient;
|
||||
const aimTime = gun.aimTime * aimTimeCoefficient;
|
||||
const dispersion = gun.dispersion.base * dispersionStillCoefficient;
|
||||
const maxDispersion =
|
||||
gun.dispersion.base *
|
||||
Math.sqrt(
|
||||
((gun.dispersion.traverse * turret.traverseSpeed) ** 2 +
|
||||
(track.dispersion.traverse * track.traverseSpeed) ** 2 +
|
||||
track.dispersion.move * tank.speed.forwards) **
|
||||
2,
|
||||
);
|
||||
const dispersionMoving = track.dispersion.move * dispersionMovingCoefficient;
|
||||
const dispersionHullTraversing =
|
||||
track.dispersion.traverse * dispersionHullTraverseCoefficient;
|
||||
const dispersionTurretTraversing =
|
||||
gun.dispersion.traverse * dispersionTurretTraverseCoefficient;
|
||||
const dispersionShooting =
|
||||
gun.dispersion.shot * dispersionAfterShotCoefficient;
|
||||
const dispersionGunDamaged =
|
||||
gun.dispersion.damaged * dispersionDamagedCoefficient;
|
||||
const gunDepression =
|
||||
gunModelDefinition.pitch.max +
|
||||
gunDefaultPitch +
|
||||
(hasImprovedVerticalStabilizer ? 3 : 0);
|
||||
const gunElevation =
|
||||
-gunModelDefinition.pitch.min -
|
||||
gunDefaultPitch +
|
||||
(hasImprovedVerticalStabilizer ? 4 : 0);
|
||||
const gunFrontalDepression = gunModelDefinition.pitch.front?.max;
|
||||
const gunFrontalElevation = gunModelDefinition.pitch.front
|
||||
? -gunModelDefinition.pitch.front.min
|
||||
: undefined;
|
||||
const gunRearDepression = gunModelDefinition.pitch.back?.max;
|
||||
const gunRearElevation = gunModelDefinition.pitch.back
|
||||
? -gunModelDefinition.pitch.back.min
|
||||
: undefined;
|
||||
const azimuthLeft = turretModelDefinition.yaw
|
||||
? -turretModelDefinition.yaw.min
|
||||
: undefined;
|
||||
const azimuthRight = turretModelDefinition.yaw?.max;
|
||||
const speedForwards = tank.speed.forwards + speedForwardsSum;
|
||||
const speedBackwards = tank.speed.backwards + speedBackwardsSum;
|
||||
const enginePower = resolvedEnginePower;
|
||||
const hardTerrainCoefficientRaw =
|
||||
track.resistance.hard * resistanceHardCoefficient;
|
||||
const hardTerrainCoefficient = 100 / hardTerrainCoefficientRaw;
|
||||
const mediumTerrainCoefficientRaw =
|
||||
track.resistance.medium * resistanceMediumCoefficient;
|
||||
const mediumTerrainCoefficient = 100 / mediumTerrainCoefficientRaw;
|
||||
const softTerrainCoefficientRaw =
|
||||
track.resistance.soft * resistanceSoftCoefficient;
|
||||
const softTerrainCoefficient = 100 / softTerrainCoefficientRaw;
|
||||
const powerToWeightRatioHardTerrain =
|
||||
resolvedEnginePower / weightTons / hardTerrainCoefficientRaw;
|
||||
const powerToWeightRatioMediumTerrain =
|
||||
resolvedEnginePower / weightTons / mediumTerrainCoefficientRaw;
|
||||
const powerToWeightRatioSoftTerrain =
|
||||
resolvedEnginePower / weightTons / softTerrainCoefficientRaw;
|
||||
const weight = weightTons;
|
||||
const turretTraverseSpeed = turret.traverseSpeed * turretTraverseCoefficient;
|
||||
const hullTraverseHardTerrain =
|
||||
(resolvedEnginePower / stockEngine.power) *
|
||||
track.traverseSpeed *
|
||||
hullTraverseCoefficient *
|
||||
(track.resistance.hard / hardTerrainCoefficientRaw) *
|
||||
(stockWeight / weightKg);
|
||||
const hullTraverseMediumTerrain =
|
||||
(resolvedEnginePower / stockEngine.power) *
|
||||
track.traverseSpeed *
|
||||
hullTraverseCoefficient *
|
||||
(track.resistance.hard / mediumTerrainCoefficientRaw) *
|
||||
(stockWeight / weightKg);
|
||||
const hullTraverseSoftTerrain =
|
||||
(resolvedEnginePower / stockEngine.power) *
|
||||
track.traverseSpeed *
|
||||
hullTraverseCoefficient *
|
||||
(track.resistance.hard / softTerrainCoefficientRaw) *
|
||||
(stockWeight / weightKg);
|
||||
const health = (tank.health + turret.health) * healthCoefficient;
|
||||
const fireChance = engine.fireChance * fireChanceCoefficient;
|
||||
const viewRange = turret.viewRange * viewRangeCoefficient;
|
||||
const camouflageStill = tank.camouflage.still * camouflageCoefficientStill;
|
||||
const camouflageMoving =
|
||||
tank.class === 'lightTank'
|
||||
? tank.camouflage.still * camouflageCoefficientStill
|
||||
: tank.camouflage.moving * camouflageCoefficientMoving;
|
||||
const camouflageShootingStill =
|
||||
tank.camouflage.still * gun.camouflageLoss * camouflageCoefficientStill;
|
||||
const camouflageShootingMoving =
|
||||
(tank.class === 'lightTank'
|
||||
? tank.camouflage.still * camouflageCoefficientStill
|
||||
: tank.camouflage.moving * camouflageCoefficientMoving) *
|
||||
gun.camouflageLoss;
|
||||
const camouflageCaughtOnFire =
|
||||
tank.camouflage.onFire * tank.camouflage.still * camouflageCoefficientStill;
|
||||
const width = size[2];
|
||||
const height = size[0];
|
||||
const length = size[1];
|
||||
const volume = width * height * length;
|
||||
const shellNormalization = shell.normalization ?? 0;
|
||||
const shellRicochet = isExplosive(shell.type) ? undefined : shell.ricochet;
|
||||
|
||||
return {
|
||||
shellNormalization,
|
||||
shellRicochet,
|
||||
dpm,
|
||||
dpmEffective,
|
||||
shells,
|
||||
mostOptimalShellIndex,
|
||||
shellReloads,
|
||||
shellReload,
|
||||
intraClip,
|
||||
caliber,
|
||||
penetration,
|
||||
damage,
|
||||
clipDamage,
|
||||
moduleDamage,
|
||||
explosionRadius,
|
||||
shellVelocity,
|
||||
aimTime,
|
||||
dispersion,
|
||||
maxDispersion,
|
||||
dispersionMoving,
|
||||
dispersionHullTraversing,
|
||||
dispersionTurretTraversing,
|
||||
dispersionShooting,
|
||||
dispersionGunDamaged,
|
||||
gunDepression,
|
||||
gunElevation,
|
||||
gunFrontalDepression,
|
||||
gunFrontalElevation,
|
||||
gunRearDepression,
|
||||
gunRearElevation,
|
||||
azimuthLeft,
|
||||
azimuthRight,
|
||||
speedForwards,
|
||||
speedBackwards,
|
||||
hardTerrainCoefficient,
|
||||
hardTerrainCoefficientRaw,
|
||||
mediumTerrainCoefficient,
|
||||
mediumTerrainCoefficientRaw,
|
||||
softTerrainCoefficient,
|
||||
softTerrainCoefficientRaw,
|
||||
enginePower,
|
||||
powerToWeightRatioHardTerrain,
|
||||
powerToWeightRatioMediumTerrain,
|
||||
powerToWeightRatioSoftTerrain,
|
||||
weight,
|
||||
turretTraverseSpeed,
|
||||
hullTraverseHardTerrain,
|
||||
hullTraverseMediumTerrain,
|
||||
hullTraverseSoftTerrain,
|
||||
health,
|
||||
fireChance,
|
||||
viewRange,
|
||||
camouflageStill,
|
||||
camouflageMoving,
|
||||
camouflageShootingStill,
|
||||
camouflageShootingMoving,
|
||||
camouflageCaughtOnFire,
|
||||
width,
|
||||
height,
|
||||
length,
|
||||
volume,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ProvisionDefinitions, TankDefinition } from '@blitzkit/core';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { CompareMember } from '../../stores/compareEphemeral';
|
||||
import { tankToDuelMember } from './tankToDuelMember';
|
||||
|
||||
export function tankToCompareMember(
|
||||
tank: TankDefinition,
|
||||
provisionDefinitions: ProvisionDefinitions,
|
||||
) {
|
||||
return {
|
||||
...tankToDuelMember(tank, provisionDefinitions),
|
||||
key: uniqueId(),
|
||||
} satisfies CompareMember;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
availableProvisions,
|
||||
ProvisionDefinitions,
|
||||
TankDefinition,
|
||||
} from '@blitzkit/core';
|
||||
import { DuelMember } from '../../stores/duel';
|
||||
import { genericDefaultEquipmentMatrix } from '../../stores/duel/constants';
|
||||
|
||||
const PROVISION_PREFERENCES = [
|
||||
19, // improved fuel
|
||||
18, // standard fuel
|
||||
22, // protective kit
|
||||
];
|
||||
|
||||
function infinityFallback(value: number) {
|
||||
return value === -1 ? Infinity : value;
|
||||
}
|
||||
|
||||
export function tankToDuelMember(
|
||||
tank: TankDefinition,
|
||||
provisionDefinitions: ProvisionDefinitions,
|
||||
) {
|
||||
const turret = tank.turrets.at(-1)!;
|
||||
const gun = turret.guns.at(-1)!;
|
||||
const provisionsList = availableProvisions(tank, gun, provisionDefinitions);
|
||||
|
||||
return {
|
||||
camouflage: true,
|
||||
consumables: [],
|
||||
equipmentMatrix: genericDefaultEquipmentMatrix,
|
||||
cooldownBooster: 0,
|
||||
crewMastery: 1,
|
||||
provisions: provisionsList
|
||||
.sort(
|
||||
(a, b) =>
|
||||
infinityFallback(PROVISION_PREFERENCES.indexOf(a.id)) -
|
||||
infinityFallback(PROVISION_PREFERENCES.indexOf(b.id)),
|
||||
)
|
||||
.sort((a, b) => (b.crew ?? 0) - (a.crew ?? 0))
|
||||
.slice(0, tank.provisions)
|
||||
.map(({ id }) => id),
|
||||
engine: tank.engines.at(-1)!,
|
||||
gun,
|
||||
shell: tank.turrets.at(-1)!.guns.at(-1)!.shells[0],
|
||||
tank,
|
||||
track: tank.tracks.at(-1)!,
|
||||
yaw: 0,
|
||||
pitch: 0,
|
||||
turret,
|
||||
} satisfies DuelMember;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Theme } from '@nivo/core';
|
||||
|
||||
export const nivoTheme = {
|
||||
theme: {
|
||||
text: { fill: 'var(--gray-11)' },
|
||||
grid: { line: { stroke: 'var(--gray-3)' } },
|
||||
} satisfies Theme,
|
||||
borderRadius: 2,
|
||||
innerPadding: 1,
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { RadixColorCompound, RadixRadius } from '../../stores/embedState';
|
||||
|
||||
export function toWidthVar(state: number) {
|
||||
return `${state}px`;
|
||||
}
|
||||
export function toRadiusVar(state: RadixRadius) {
|
||||
return `var(--radius-${state})`;
|
||||
}
|
||||
export function toColorVar(state: RadixColorCompound) {
|
||||
return `var(--${state.base}-${state.variant})`;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
type Name =
|
||||
| `color-${'background' | `panel-${'solid' | 'translucent'}` | 'surface' | 'overlay'}`
|
||||
| `radius-${1 | 2 | 3 | 4 | 5 | 6 | 'full'}`
|
||||
| `space-${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`
|
||||
| string;
|
||||
|
||||
export function Var(name: Name) {
|
||||
return `var(--${name})`;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Material, Mesh, Object3D, Vector3 } from 'three';
|
||||
|
||||
export function discardClippingPlane(object: Object3D, point: Vector3) {
|
||||
if (
|
||||
object instanceof Mesh &&
|
||||
object.material instanceof Material &&
|
||||
object.material.clippingPlanes !== null
|
||||
) {
|
||||
return object.material.clippingPlanes.every(
|
||||
(plane) => plane.distanceToPoint(point) < 0,
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
'use client';
|
||||
|
||||
import { type Draft, produce } from 'immer';
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { type StoreApi, useStore as useStoreZustand } from 'zustand';
|
||||
|
||||
type StoreProviderProps<InitData> = {
|
||||
children: ReactNode;
|
||||
} & (InitData extends unknown ? { data?: InitData } : { data: InitData });
|
||||
|
||||
// idk why zustand doesn't export this
|
||||
type ExtractState<S> = S extends {
|
||||
getState: () => infer T;
|
||||
}
|
||||
? T
|
||||
: never;
|
||||
|
||||
export function createContextualSafeStore<
|
||||
Store extends StoreApi<unknown>,
|
||||
InitData,
|
||||
>(initialize: (data: InitData) => Store) {
|
||||
type Type = Store extends StoreApi<infer T> ? T : never;
|
||||
|
||||
const Context = createContext<Store | undefined>(undefined);
|
||||
|
||||
function Provider(props: StoreProviderProps<InitData>) {
|
||||
const storeRef = useRef<Store>();
|
||||
if (!storeRef.current) {
|
||||
storeRef.current = initialize((props as { data: InitData }).data);
|
||||
}
|
||||
|
||||
return (
|
||||
<Context.Provider value={storeRef.current}>
|
||||
{props.children}
|
||||
</Context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useStore() {
|
||||
const store = useContext(Context);
|
||||
if (!store) throw new Error('useStore must be used within Provider');
|
||||
return store as Store;
|
||||
}
|
||||
|
||||
function use<Slice = Type>(
|
||||
selector: (store: Type) => Slice = (state) => state as Slice,
|
||||
) {
|
||||
const store = useStore();
|
||||
return useStoreZustand(
|
||||
store,
|
||||
selector as (state: ExtractState<Store>) => Slice,
|
||||
);
|
||||
}
|
||||
|
||||
function useDeferred<Slice = Type>(
|
||||
initial: Slice,
|
||||
selector: (store: Type) => Slice = (state) => state as Slice,
|
||||
) {
|
||||
const [state, setState] = useState(initial);
|
||||
const trueState = use(selector);
|
||||
|
||||
useEffect(() => {
|
||||
setState(trueState);
|
||||
}, [trueState]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function useMutation() {
|
||||
const counterStoreContext = useStore();
|
||||
|
||||
return function (recipe: (draft: Draft<Type>) => void) {
|
||||
counterStoreContext.setState(
|
||||
produce<Type>((draft) => {
|
||||
recipe(draft);
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
return { Provider, use, useMutation, useStore, useDeferred };
|
||||
}
|
||||
Reference in new issue
Block a user