Compare commits

...
5 Commits
Author SHA1 Message Date
tresabhi 1f767af0b5 angles 2026-09-30 20:59:18 -05:00
tresabhi f73ffc3c93 correct axes 2026-09-30 19:00:25 -05:00
tresabhi 5d4e0541b2 correct filtering 2026-09-30 18:41:22 -05:00
tresabhi cbf320454f various bug fixes 2026-09-30 18:38:15 -05:00
tresabhi 74fbd15c5f TankopediaCharacteristics 2026-09-30 18:22:18 -05:00
23 changed files with 608 additions and 134 deletions

No files matched your search

@@ -1,7 +1,6 @@
import {
BlitzCrewType,
ChassisDefinitionsList,
EngineDefinitionsList,
GunDefinitionsList,
TankParameters,
toUniqueId,
@@ -17,7 +16,7 @@ import { ServerBlitzKitAPI2 } from "./2_camouflages";
export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
private vector3TupleToBlitzkit(tuple: Vector3Tuple) {
return { x: tuple[0], y: tuple[1], z: tuple[2] } satisfies Vector3;
return { x: tuple[0], y: tuple[1], z: -tuple[2] } satisfies Vector3;
}
private assignArmor(
@@ -50,9 +49,6 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
const gunList = await this.vfs.xml<{
root: GunDefinitionsList;
}>(`Data/XML/item_defs/vehicles/${nation}/components/guns.xml`);
const enginesList = await this.vfs.xml<{
root: EngineDefinitionsList;
}>(`Data/XML/item_defs/vehicles/${nation}/components/engines.xml`);
const chassisList = await this.vfs.xml<{
root: ChassisDefinitionsList;
}>(`Data/XML/item_defs/vehicles/${nation}/components/chassis.xml`);
@@ -27,8 +27,10 @@ export abstract class ServerBlitzKitAPI5 extends ServerBlitzKitAPI4 {
.map((level) => {
return Object.values(level).map((options) => {
return {
left: this.optionalDevices!.root[options.device0].id,
right: this.optionalDevices!.root[options.device1].id,
options: [
this.optionalDevices!.root[options.device0].id,
this.optionalDevices!.root[options.device1].id,
],
} satisfies EquipmentSlot;
});
})
+6 -9
View File
@@ -1,8 +1,8 @@
import { ScpgReadStream, VertexAttribute } from './scpg';
import { ScpgReadStream, VertexAttribute } from "./scpg";
interface PolygonGroupRaw {
'##name': 'PolygonGroup';
'#id': ArrayBuffer;
"##name": "PolygonGroup";
"#id": ArrayBuffer;
cubeTextureCoordCount: number;
indexCount: number;
indexFormat: 0 | 1;
@@ -24,7 +24,7 @@ interface BlitzkitPolygonGroup {
}
const vertexAttributesArray = Object.values(VertexAttribute).filter(
(value) => typeof value === 'number',
(value) => typeof value === "number",
);
export const vertexAttributeVectorSizes = {
@@ -105,11 +105,8 @@ export class ScgReadStream extends ScpgReadStream {
}
polygonGroups.set(
new DataView(polygonGroupRaw['#id']).getBigUint64(0, true),
{
vertices,
indices,
},
new DataView(polygonGroupRaw["#id"]).getBigUint64(0, true),
{ vertices, indices },
);
});
+37 -8
View File
@@ -1,16 +1,45 @@
export function literals(
string: string,
literals?: Record<string, string | number | undefined>
literals?: Record<string, string | number | undefined>,
) {
for (const key in literals) {
const mutated = string.replaceAll(`{${key}}`, `${literals[key]}`);
if (import.meta.env.DEV && mutated === string) {
console.warn(`Could not find literal {${key}} in string "${string}"`);
}
string = mutated;
string = string.replaceAll(`{${key}}`, `${literals[key]}`);
}
return string;
}
export function literalsArray<Type, Literal = Type | string>(
string: string,
literals?: Record<string, Literal>,
) {
if (!literals) return [string];
let array: (string | Literal)[] = [string];
for (const key in literals) {
const value = literals[key];
const next: (string | Literal)[] = [];
for (const chunk of array) {
if (typeof chunk !== "string") {
next.push(chunk);
continue;
}
const parts = chunk.split(`{${key}}`);
for (let index = 0; index < parts.length; index++) {
if (parts[index]) next.push(parts[index]);
if (index < parts.length - 1) {
next.push(value);
}
}
}
array = next;
}
return array;
}
+11
View File
@@ -1,4 +1,15 @@
{
"units": {
"m": "{value}m",
"mm": "{value}mm",
"deg": "{value}°",
"hp": "{value}hp",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/s",
"plus": "+{value}"
},
"tanks": {
"characteristics": {
"titles": {
@@ -1,6 +1,5 @@
import { memo, useRef } from "react";
import { Group } from "three";
import { correctZYTuple } from "../core/blitz/correctZYTuple";
import { nameToArmorId } from "../core/blitzkit/nameToArmorId";
import { resolveArmor } from "../core/blitzkit/resolveThickness";
import { useArmor } from "../hooks/useArmor";
@@ -8,6 +7,7 @@ import { useConsumable } from "../hooks/useConsumable";
import { DuelModule, useDuel } from "../hooks/useDuel";
import { useDuelModel } from "../hooks/useDuelModel";
import { useTankTransform } from "../hooks/useTankTransform";
import { toThreeVector } from "../three/toThreeVector";
import { PrimaryArmorSceneComponent } from "./PrimaryArmorSceneComponent";
export const PrimaryArmorScene = memo(() => {
@@ -23,9 +23,9 @@ export const PrimaryArmorScene = memo(() => {
const trackModelDefinition = tankModelDefinition.tracks[track.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const hullOrigin = correctZYTuple(trackModelDefinition.origin!);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin!);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin!);
const hullOrigin = toThreeVector(trackModelDefinition.origin);
const turretOrigin = toThreeVector(tankModelDefinition.turret_origin);
const gunOrigin = toThreeVector(turretModelDefinition.gun_origin);
const isDynamicArmorActive = useConsumable("protagonist", 73);
useTankTransform(track, turret, turretContainer, gunContainer);
@@ -1,6 +1,5 @@
import { memo, useRef } from "react";
import { Group, Plane, Scene, Vector3 } from "three";
import { correctZYTuple } from "../core/blitz/correctZYTuple";
import { nameToArmorId } from "../core/blitzkit/nameToArmorId";
import { resolveArmor } from "../core/blitzkit/resolveThickness";
import { useArmor } from "../hooks/useArmor";
@@ -9,6 +8,7 @@ import { DuelModule, useDuel } from "../hooks/useDuel";
import { useDuelModel } from "../hooks/useDuelModel";
import { useModel } from "../hooks/useModel";
import { useTankTransform } from "../hooks/useTankTransform";
import { toThreeVector } from "../three/toThreeVector";
import { SpacedArmorSceneComponent } from "./SpacedArmorSceneComponent";
export enum ArmorType {
@@ -36,9 +36,9 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
const trackModelDefinition = tankModelDefinition.tracks[track.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const hullOrigin = correctZYTuple(trackModelDefinition.origin!);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin!);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin!);
const hullOrigin = toThreeVector(trackModelDefinition.origin);
const turretOrigin = toThreeVector(tankModelDefinition.turret_origin);
const gunOrigin = toThreeVector(turretModelDefinition.gun_origin);
const maskOrigin =
gunModelDefinition.mask === undefined
? undefined
@@ -2,7 +2,6 @@ import { useThree, type ThreeEvent } from "@react-three/fiber";
import { memo, useRef } from "react";
import { Group, Plane, Vector2, Vector3 } from "three";
import { applyPitchYawLimits } from "../core/blitz/applyPitchYawLimits";
import { correctZYTuple } from "../core/blitz/correctZYTuple";
import { modelTransformEvent } from "../core/blitzkit/modelTransform";
import { nameToArmorId } from "../core/blitzkit/nameToArmorId";
import { resolveArmor } from "../core/blitzkit/resolveThickness";
@@ -16,6 +15,7 @@ import { useModel } from "../hooks/useModel";
import { useTankTransform } from "../hooks/useTankTransform";
import { Tankopedia } from "../stores/tankopedia";
import { TankopediaPersistent } from "../stores/tankopediaPersistent";
import { toThreeVector } from "../three/toThreeVector";
import { ArmorType } from "./SpacedArmorScene";
import { StaticArmorSceneComponent } from "./StaticArmorSceneComponent";
@@ -44,9 +44,9 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const trackModelDefinition = tankModelDefinition.tracks[track.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const hullOrigin = correctZYTuple(trackModelDefinition.origin!);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin!);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin!);
const hullOrigin = toThreeVector(trackModelDefinition.origin);
const turretOrigin = toThreeVector(tankModelDefinition.turret_origin);
const gunOrigin = toThreeVector(turretModelDefinition.gun_origin);
const canvas = useThree((state) => state.gl.domElement);
const maskOrigin =
+16 -3
View File
@@ -5,7 +5,7 @@ import { applyPitchYawLimits } from "../core/blitz/applyPitchYawLimits";
import { jsxTree } from "../core/blitzkit/jsxTree";
import { modelTransformEvent } from "../core/blitzkit/modelTransform";
import { controlsEnabledEvent } from "../core/controlsEnabled";
import { DuelModule, useDuel } from "../hooks/useDuel";
import { DuelModule, getDuel, useDuel } from "../hooks/useDuel";
import { useDuelModel } from "../hooks/useDuelModel";
import { hasEquipment } from "../hooks/useEquipment";
import { useModel } from "../hooks/useModel";
@@ -22,6 +22,7 @@ export function TankModel() {
const turretContainer = useRef<Group>(null!);
const gunContainer = useRef<Group>(null!);
const tankModelDefinition = useDuelModel("protagonist");
const trackModelDefinition = tankModelDefinition.tracks[track.id];
const turretModelDefinition = tankModelDefinition.turrets[protagonist.turret];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun];
const { gltf } = useModel(protagonist.tank);
@@ -31,6 +32,18 @@ export function TankModel() {
return (
<>
{/* <mesh
scale={0.1}
position={[
trackModelDefinition.origin!.x + tankModelDefinition.turret_origin!.x,
trackModelDefinition.origin!.y + tankModelDefinition.turret_origin!.y,
trackModelDefinition.origin!.z + tankModelDefinition.turret_origin!.z,
]}
>
<sphereGeometry />
<meshBasicMaterial color="red" depthTest={false} />
</mesh> */}
{nodes.map((node) => {
const isHull = node.name === "hull";
const isWheel = node.name.startsWith("chassis_wheel_");
@@ -129,7 +142,7 @@ export function TankModel() {
window.addEventListener("pointerup", handlePointerUp);
}
function handlePointerMove(event: PointerEvent) {
const protagonist = useDuel("protagonist", DuelModule.Tank);
const protagonist = getDuel("protagonist", DuelModule.Tank);
const hasImprovedVerticalStabilizer = hasEquipment(
122,
@@ -214,7 +227,7 @@ export function TankModel() {
window.addEventListener("pointerup", handlePointerUp);
}
function handlePointerMove(event: PointerEvent) {
const protagonist = useDuel("protagonist", DuelModule.Tank);
const protagonist = getDuel("protagonist", DuelModule.Tank);
const hasImprovedVerticalStabilizer = hasEquipment(
122,
@@ -1,12 +1,12 @@
import { useRef } from "react";
import { Group, ShaderMaterial, UniformsLib, UniformsUtils } from "three";
import { correctZYTuple } from "../../../../../../../core/blitz/correctZYTuple";
import { jsxTree } from "../../../../../../../core/blitzkit/jsxTree";
import { useArmor } from "../../../../../../../hooks/useArmor";
import { useModel } from "../../../../../../../hooks/useModel";
import { useTankModelDefinition } from "../../../../../../../hooks/useTankModelDefinition";
import { useTankTransform } from "../../../../../../../hooks/useTankTransform";
import { Duel } from "../../../../../../../stores/duel";
import { toThreeVector } from "../../../../../../../three/toThreeVector";
import { ModelTankWrapper } from "../../../../../../ModelTankWrapper";
import fragmentShader from "./shaders/fragment.glsl?raw";
import vertexShader from "./shaders/vertex.glsl?raw";
@@ -37,9 +37,9 @@ export function TransitionSkeleton() {
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const trackModelDefinition = tankModelDefinition.tracks[track.id];
const wrapper = useRef<Group>(null);
const hullOrigin = correctZYTuple(trackModelDefinition.origin!);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin!);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin!);
const hullOrigin = toThreeVector(trackModelDefinition.origin);
const turretOrigin = toThreeVector(tankModelDefinition.turret_origin);
const gunOrigin = toThreeVector(turretModelDefinition.gun_origin);
useTankTransform(track, turret, turretContainer, gunContainer);
@@ -0,0 +1,138 @@
.characteristics-wrapper {
margin: var(--space-4);
--base-cell-width: 20rem;
--group-span: 7;
container-type: inline-size;
.characteristics {
--N: round(down, 100cqw / var(--base-cell-width));
--cell-width: calc(100cqw / var(--N));
--cell-height: calc(var(--cell-width) / var(--group-span));
display: grid;
grid-template-columns: repeat(
auto-fill,
minmax(var(--base-cell-width), 1fr)
);
grid-auto-rows: var(--cell-height);
grid-auto-flow: dense;
box-sizing: border-box;
.group {
--gradient-size: min(100%, var(--space-8));
grid-column: span 1;
display: grid;
grid-template-columns: inherit;
grid-auto-rows: inherit;
.title {
grid-row: span 1;
display: flex;
align-items: center;
margin: 0 calc(var(--space-4));
.center {
flex: 1;
padding: var(--space-3);
.text {
.align {
display: flex;
align-items: center;
gap: var(--space-3);
img {
width: 1em;
height: 1em;
}
}
}
}
}
.content {
margin: 0 var(--space-4);
gap: inherit;
height: 100%;
display: grid;
grid-template-columns: inherit;
grid-auto-rows: inherit;
.item {
padding: 0 var(--space-3);
grid-row: span 1;
display: flex;
flex-direction: column;
justify-content: center;
gap: var(--space-2);
.value {
display: flex;
justify-content: space-between;
.units {
display: flex;
align-items: end;
gap: var(--space-2);
}
}
.ranking {
display: flex;
align-items: center;
gap: var(--space-3);
.bar-wrapper {
flex: 1;
height: var(--space-1);
background-color: var(--gray-3);
border-radius: var(--radius-max);
overflow: hidden;
.bar {
height: 100%;
}
}
}
}
.toy {
grid-row: span var(--group-span);
padding: var(--space-4) 0;
.sandbox {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--gray-2);
outline: var(--gray-5) solid var(--space-1);
border-radius: var(--space-3);
}
}
.dead {
grid-row: span 1;
}
}
.dead {
grid-row: span 1;
}
}
}
}
@@ -0,0 +1,183 @@
import { characteristicsGroups } from "../../config/characteristicsGroups";
import { useCharacteristicRenderer } from "../../hooks/useCharacteristicRenderer";
import { useStrings } from "../../hooks/useStrings";
import type {
CharacteristicRenderConfig,
CharacteristicReturnType,
CharacteristicsGroup,
ComputedCharacteristics,
} from "../../types/characteristics";
import { Heading } from "../Heading";
import { Text } from "../Text";
import styles from "./index.module.css";
interface TankopediaCharacteristicsProps {
characteristics: ComputedCharacteristics;
}
export function TankopediaCharacteristics({
characteristics,
}: TankopediaCharacteristicsProps) {
return (
<div className={styles["characteristics-wrapper"]}>
<div className={styles.characteristics}>
{characteristicsGroups.map((group) => {
const items = group.order.filter(
(item) => "name" in item && item.name in characteristics,
).length;
const shouldRender = items > 0;
if (!shouldRender) return null;
return (
<Group
key={group.name}
group={group}
characteristics={characteristics}
/>
);
})}
</div>
</div>
);
}
interface GroupProps {
group: CharacteristicsGroup;
characteristics: ComputedCharacteristics;
}
function Group({ group, characteristics }: GroupProps) {
const strings = useStrings();
const toys = group.order.filter((item) => "toy" in item).length;
const items = group.order.filter(
(item) => "name" in item && characteristics[item.name] !== null,
).length;
const groupSpan = `calc(${toys} * var(--group-span) + ${items} + 2)`;
const contentSpan = `calc(${toys} * var(--group-span) + ${items})`;
return (
<div
className={styles.group}
style={{
gridRow: `span ${groupSpan}`,
gridTemplateRows: `repeat(${groupSpan}, 1fr)`,
}}
>
<div className={styles.title}>
<div className={styles.center}>
<Heading size="3" className={styles.text}>
<div className={styles.align}>
{
(
strings.tanks.characteristics.titles as Record<string, string>
)[group.name]
}
<img src={`/media/groups/${group.name}.png`} />
</div>
</Heading>
</div>
</div>
<div
className={styles.content}
style={{
gridRow: `span ${contentSpan}`,
}}
>
{group.order.map((item) => (
<>
{"toy" in item && <Toy {...item} />}
{"name" in item && characteristics[item.name] !== null && (
<Item config={item} value={characteristics[item.name]!} />
)}
</>
))}
<div className={styles.dead} />
</div>
</div>
);
}
interface ItemProps {
config: CharacteristicRenderConfig;
value: CharacteristicReturnType;
}
function Item({ config, value }: ItemProps) {
const strings = useStrings();
const renderCharacteristic = useCharacteristicRenderer();
const hasUnits = config.units !== undefined;
return (
<span className={styles.item}>
<div className={styles.value}>
<Text lowContrast>
{
(strings.tanks.characteristics.items as Record<string, string>)[
config.name
]
}
</Text>
<Text lowContrast={hasUnits} size={hasUnits ? "minor" : undefined}>
<div className={styles.units}>
{renderCharacteristic(value, config)}
</div>
</Text>
</div>
<div className={styles.ranking}>
<Bar value={0.5} />
<Text lowContrast size="minor" className={styles.number}>
1 / 132
</Text>
</div>
</span>
);
}
interface BarProps {
value: number;
}
const barColors = ["red", "orange", "amber", "jade"];
function Bar({ value }: BarProps) {
const index = (barColors.length - 1) * value;
const iA = Math.floor(index);
const iB = Math.ceil(index);
const A = barColors[iA];
const B = barColors[iB];
const x = index % 1;
const p = (1 - x) * 100;
const v = 7;
return (
<div className={styles["bar-wrapper"]}>
<div
className={styles.bar}
style={{
backgroundColor: `color-mix(in srgb, var(--${A}-${v}) ${p}%, var(--${B}-${v}))`,
width: `${value * 100}%`,
}}
/>
</div>
);
}
interface ToyProps {
toy: string;
}
function Toy({ toy }: ToyProps) {
return (
<div className={styles.toy}>
<div className={styles.sandbox}>
<Text lowContrast>{toy} toy</Text>
</div>
</div>
);
}
@@ -0,0 +1,18 @@
import type { CharacteristicsGroup } from "../types/characteristics";
export const characteristicsGroups: CharacteristicsGroup[] = [
{
name: "firepower",
order: [
{
name: "gun_type",
},
{
name: "damage",
},
{
name: "clip_size",
},
],
},
];
@@ -1,7 +0,0 @@
import type { Vector3 as BlitzkitVector3 } from "@blitzkit/protos";
import { Vector3 as THREEVector3 } from "three";
import { rotateDavaVector } from "./rotateDavaVector";
export function correctZYTuple(tuple: BlitzkitVector3) {
return rotateDavaVector(new THREEVector3(tuple.x, tuple.y, -tuple.z));
}
@@ -613,56 +613,3 @@ export function tankCharacteristics(
burstInterShell,
};
}
export enum TankCharacteristicsCondition {
Regular,
AutoLoader,
AutoReloader,
}
export const characteristicsOrder: {
name: string;
items: TankCharacteristicsKey[];
}[] = [
{
name: "Firepower",
items: [
"gunType",
"dpm",
"shells",
"shellReload",
"shellReloads",
"intraClip",
"penetration",
"penetrationAt250m",
"damage",
"clipDamage",
"moduleDamage",
"caliber",
"shellNormalization",
"shellRicochet",
"shellVelocity",
"shellRange",
"shellCapacity",
"aimTime",
"dispersion",
"dispersionMoving",
"dispersionHullTraversing",
"dispersionTurretTraversing",
"dispersionShooting",
"dispersionGunDamaged",
"gunDepression",
"gunElevation",
"gunFrontalDepression",
"gunFrontalElevation",
"gunRearDepression",
"gunRearElevation",
"azimuthLeft",
"azimuthRight",
],
},
{
name: "Crew",
items: ["crewCount"],
},
];
@@ -0,0 +1,75 @@
import { literalsArray } from "@blitzkit/i18n/src/literals";
import { useMemo, type ReactNode } from "react";
import { Text } from "../components/Text";
import { characteristicsGroups } from "../config/characteristicsGroups";
import type {
CharacteristicRenderConfig,
CharacteristicReturnType,
} from "../types/characteristics";
import { useLocale } from "./useLocale";
import { useStrings } from "./useStrings";
export function useCharacteristicRenderer() {
const groups = useMemo(() => {
const groups: string[] = [];
for (const group of characteristicsGroups) {
for (const item of group.order) {
if ("strings" in item && item.strings) {
groups.push(item.strings);
}
}
}
return groups;
}, []);
const locale = useLocale();
const strings = useStrings();
function renderCharacteristic(
characteristic: CharacteristicReturnType,
config: CharacteristicRenderConfig,
): ReactNode {
if (characteristic === null) return null;
if (config.render)
return config.render({ output: characteristic, strings });
if (typeof characteristic === "number") {
if (Number.isFinite(characteristic)) {
let value: ReactNode = characteristic;
if (config.decimals !== undefined) {
value *= 10 ** config.decimals;
value = Math.round(value);
value /= 10 ** config.decimals;
}
if (config.localize) {
value = value.toLocaleString(locale);
} else {
value = value.toFixed(config.decimals);
}
if (config.units !== undefined) {
value = literalsArray(strings.units[config.units], {
value: <Text>{value}</Text>,
});
}
return value;
}
return `${characteristic < 0 ? "-" : ""}∞`;
}
if (typeof characteristic === "string") {
return characteristic;
}
throw new Error("Unknown characteristic type");
}
return renderCharacteristic;
}
+10 -12
View File
@@ -5,11 +5,11 @@ import type { QuicklimeEvent } from "quicklime";
import { type RefObject, useEffect } from "react";
import { Euler, Group, Vector3 } from "three";
import { degToRad } from "three/src/math/MathUtils.js";
import { correctZYTuple } from "../core/blitz/correctZYTuple";
import {
modelTransformEvent,
type ModelTransformEventData,
} from "../core/blitzkit/modelTransform";
import { toThreeVector } from "../three/toThreeVector";
import { useDuelModel } from "./useDuelModel";
export function useTankTransform(
@@ -23,9 +23,9 @@ export function useTankTransform(
useEffect(() => {
const trackModelDefinition = tankModelDefinition.tracks[track.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const hullOrigin = correctZYTuple(trackModelDefinition.origin!);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin!);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin!);
const hullOrigin = toThreeVector(trackModelDefinition.origin);
const turretOrigin = toThreeVector(tankModelDefinition.turret_origin);
const gunOrigin = toThreeVector(turretModelDefinition.gun_origin);
const turretPosition = new Vector3();
const turretRotation = new Euler();
const gunPosition = new Vector3();
@@ -54,14 +54,12 @@ export function useTankTransform(
gunContainer.current?.position.copy(gunPosition);
gunContainer.current?.rotation.copy(gunRotation);
if (yaw === undefined) return;
turretPosition
.set(0, 0, 0)
.sub(hullOrigin)
.sub(turretOrigin)
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
turretRotation.set(0, 0, yaw);
.applyAxisAngle(J_HAT, yaw);
turretRotation.set(0, yaw, 0);
if (tankModelDefinition.initial_turret_rotation) {
const initialPitch = -degToRad(
@@ -76,11 +74,11 @@ export function useTankTransform(
turretPosition
.applyAxisAngle(I_HAT, initialPitch)
.applyAxisAngle(J_HAT, initialRoll)
.applyAxisAngle(K_HAT, initialYaw);
.applyAxisAngle(J_HAT, initialYaw)
.applyAxisAngle(K_HAT, initialRoll);
turretRotation.x += initialPitch;
turretRotation.y += initialRoll;
turretRotation.z += initialYaw;
turretRotation.y += initialYaw;
turretRotation.z += initialRoll;
}
turretPosition.add(turretOrigin).add(hullOrigin);
@@ -262,13 +262,11 @@ async function extractModel(vfs: AbstractVFS, path: string) {
break;
case "TransformComponent": {
const localTranslation = component["tc.localTranslation"];
// The game resets top-level node translation to [0, 0, 0] on load.
// Child nodes must keep their authored local offsets.
node.setTranslation(
parent instanceof Scene ? [0, 0, 0] : localTranslation,
);
node.setRotation(component["tc.localRotation"]);
const t = component["tc.localTranslation"];
const r = component["tc.localRotation"];
node.setTranslation([t[0], t[2], -t[1]]);
node.setRotation([r[0], r[2], -r[1], r[3]]);
node.setScale(component["tc.localScale"]);
break;
@@ -332,10 +330,27 @@ async function extractModel(vfs: AbstractVFS, path: string) {
}
const vertexSize = vertexAttributeGltfVectorSizes[attribute];
const processed = new Float32Array(value.flat());
if (
attribute === VertexAttribute.VERTEX ||
attribute === VertexAttribute.NORMAL ||
attribute === VertexAttribute.TANGENT ||
attribute === VertexAttribute.BINORMAL
) {
for (let i = 0; i < processed.length; i += 3) {
const y = processed[i + 1];
const z = processed[i + 2];
processed[i + 1] = z;
processed[i + 2] = -y;
}
}
const attributeAccessor = document
.createAccessor(name)
.setType(vertexSize === 1 ? "SCALAR" : `VEC${vertexSize}`)
.setArray(new Float32Array(value.flat()))
.setArray(processed)
.setBuffer(buffer);
primitive.setAttribute(name, attributeAccessor);
@@ -1,20 +1,26 @@
import { useMemo } from "react";
import { api } from "../../../../../api/dynamic";
import type { ThicknessRange } from "../../../../../components/StaticArmor";
import { TankopediaCharacteristics } from "../../../../../components/TankopediaCharacteristics";
import { TankopediaLoadout } from "../../../../../components/TankopediaLoadout";
import { TankopediaSandbox } from "../../../../../components/TankopediaSandbox";
import { defaultEqualizer } from "../../../../../config/equalizer";
import { withErrorWrapper } from "../../../../../hocs/withErrorWrapper";
import { withLocale } from "../../../../../hocs/withLocale";
import { useAwait } from "../../../../../hooks/useAwait";
import { Tankopedia } from "../../../../../stores/tankopedia";
import { computeCharacteristics } from "../../../../../tankopedia/computeCharacteristics";
import type { MaybeSkeletonComponentProps } from "../../../../../types/maybeSkeletonComponentProps";
import styles from "./_index.module.css";
interface PageProps {
type PageProps = MaybeSkeletonComponentProps & {
id: number;
}
};
const tanks = await api.tanks();
export const Page = withErrorWrapper(
withLocale<PageProps>(({ id }) => {
withLocale<PageProps>(({ skeleton, id }) => {
const protagonistTank = useAwait(() => api.tank(id), `tank-${id}`);
Tankopedia.useInitialization(protagonistTank);
@@ -27,6 +33,27 @@ export const Page = withErrorWrapper(
[protagonist],
);
const equalize = Tankopedia.use((state) => state.equalize);
const thicknessRange = useMemo(() => {
const entries = Object.values(tanks.tanks);
const filtered = entries.filter(
(thisTank) => thisTank.tier === protagonistTank.tier,
);
const value =
(filtered.reduce((accumulator, thisTank) => {
return (
accumulator +
thisTank.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration!.near *
((equalize ? thisTank.equalizer : undefined) ?? defaultEqualizer)
.penetration
);
}, 0) /
filtered.length) *
(3 / 4);
return { value } satisfies ThicknessRange;
}, [protagonist, equalize]);
return (
<div className={styles.page}>
<div className={styles.loadout}>
@@ -34,7 +61,7 @@ export const Page = withErrorWrapper(
</div>
<div className={styles.sandbox}>
<TankopediaSandbox parameters={parameters} />
{!skeleton && <TankopediaSandbox thicknessRange={thicknessRange} />}
<TankopediaCharacteristics characteristics={characteristics} />
</div>
</div>
@@ -27,5 +27,7 @@ const { id, locale } = Astro.props;
<ColorScale color="amber" />
<ColorScale color="jade" />
<Page {locale} {id} client:load />
<Page {id} {locale} client:only="react">
<Page slot="fallback" {id} {locale} skeleton />
</Page>
</BaseLayout>
@@ -33,7 +33,7 @@ export function createTankState(tank: TankDefinition) {
equipment: {},
status: createTankStatus(),
model: models.models[turret.id],
model: models.models[tank.id],
consumables: [],
provisions: createDefaultProvisions(tank, gun, provisions, scripts),
@@ -0,0 +1,6 @@
import type { Vector3 as BlitzKitVector } from "@blitzkit/protos";
import { Vector3 } from "three";
export function toThreeVector(vector?: BlitzKitVector) {
return new Vector3(vector!.x, vector!.y, vector!.z);
}
+29 -5
View File
@@ -1,8 +1,10 @@
import type { Strings } from "@blitzkit/i18n";
import type {
Equalizer,
GunDefinition,
ShellDefinition,
} from "@blitzkit/protos";
import type { ReactNode } from "react";
import { characteristics } from "../config/characteristics";
export type Characteristic = {
@@ -36,10 +38,32 @@ export enum CharacteristicType {
Number,
}
type CharacteristicName = keyof typeof characteristics;
type CharacteristicReturnType = ReturnType<Characteristic["compute"]>;
export type CharacteristicName = keyof typeof characteristics;
export type CharacteristicReturnType = ReturnType<Characteristic["compute"]>;
export type ComputedCharacteristics = Record<
CharacteristicName,
CharacteristicReturnType
export type ComputedCharacteristics = Partial<
Record<CharacteristicName, CharacteristicReturnType>
>;
export interface CharacteristicRenderConfig {
name: CharacteristicName;
decimals?: number;
units?: keyof Strings["units"];
localize?: boolean;
strings?: string;
render?: (data: {
output: CharacteristicReturnType;
strings: Strings;
}) => ReactNode;
}
export interface ToyRenderConfig {
toy: string;
}
export type CharacteristicsGroup = {
name: string;
order: (CharacteristicRenderConfig | ToyRenderConfig)[];
};