mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-01 06:59:51 +01:00
Compare commits
5
Commits
5cae5d1583
...
1f767af0b5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f767af0b5 | ||
|
|
f73ffc3c93 | ||
|
|
5d4e0541b2 | ||
|
|
cbf320454f | ||
|
|
74fbd15c5f |
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;
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 =
|
||||
|
||||
@@ -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,
|
||||
|
||||
+4
-4
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)[];
|
||||
};
|
||||
Reference in new issue
Block a user