Merge branch 'main' into beg
No files matched your search
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,135 @@
|
||||
---
|
||||
import { Grid, Text } from "@radix-ui/themes";
|
||||
import { credits } from "../config/credits";
|
||||
import { getStrings } from "../core/i18n/getStrings";
|
||||
|
||||
interface Props {
|
||||
locale: string;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
|
||||
const strings = getStrings(locale);
|
||||
---
|
||||
|
||||
<style>
|
||||
.credits {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
padding: var(--space-4);
|
||||
|
||||
.inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
|
||||
width: 100%;
|
||||
max-width: 80rem;
|
||||
|
||||
.list {
|
||||
gap: var(--space-5);
|
||||
|
||||
.link {
|
||||
display: contents;
|
||||
|
||||
.person {
|
||||
--profile-size: 5rem;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding: var(--space-4);
|
||||
padding-top: 0;
|
||||
margin-top: calc(var(--profile-size) / 2);
|
||||
gap: var(--space-1);
|
||||
|
||||
box-shadow: var(--shadow-3);
|
||||
border-radius: var(--radius-3);
|
||||
|
||||
.contain {
|
||||
border-radius: inherit;
|
||||
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
.hint {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
filter: saturate(20) brightness(0.125) blur(4rem);
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
|
||||
position: relative;
|
||||
|
||||
margin-top: calc(var(--profile-size) / -2);
|
||||
|
||||
object-fit: cover;
|
||||
width: var(--profile-size);
|
||||
height: var(--profile-size);
|
||||
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="credits">
|
||||
<div class="inner">
|
||||
<Grid
|
||||
className="list"
|
||||
py="4"
|
||||
columns="repeat(auto-fill, minmax(16rem, 1fr))"
|
||||
flow="dense"
|
||||
gap="4"
|
||||
gapY="8"
|
||||
>
|
||||
{
|
||||
credits.map((person) => (
|
||||
<a target="_blank" href={person.url} class="link">
|
||||
<div class="person">
|
||||
<div class="contain">
|
||||
<div
|
||||
class="hint"
|
||||
style={`background-image: url(${person.image})`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<img src={person.image} />
|
||||
|
||||
<Text weight="medium" className="text" color="gray" highContrast>
|
||||
{person.name}
|
||||
</Text>
|
||||
<Text className="text" color="gray" wrap="pretty">
|
||||
{person.description}
|
||||
</Text>
|
||||
</div>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,9 +84,6 @@ const strings = getStrings(locale);
|
||||
sm: "start",
|
||||
}}
|
||||
>
|
||||
<LinkI18n {locale} href="/docs/about/credits" color="gray">
|
||||
{strings.website.footer.about.credits}
|
||||
</LinkI18n>
|
||||
<Link
|
||||
href="https://discord.gg/nDt7AjGJQH"
|
||||
target="_blank"
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
interface CreditPeople {
|
||||
name: string;
|
||||
description: string;
|
||||
url?: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export const credits: CreditPeople[] = [
|
||||
{
|
||||
name: "TrèsAbhi",
|
||||
description: "Founder and lead developer",
|
||||
url: "https://abhi-deep.com/",
|
||||
image: "https://github.com/tresabhi.png",
|
||||
},
|
||||
{
|
||||
name: "HitJack",
|
||||
description: "Developer on website",
|
||||
url: "https://marksstuff.com",
|
||||
image: "https://github.com/Hitjack007.png",
|
||||
},
|
||||
{
|
||||
name: "aeson000",
|
||||
description: "Advisor on game and website",
|
||||
url: "https://discord.gg/WHdER7ZPAD",
|
||||
image: "/media/users/aeson000.png",
|
||||
},
|
||||
{
|
||||
name: "GonnaHetzMe",
|
||||
description: "Developer and advisor on back end",
|
||||
url: "https://github.com/karelpak1",
|
||||
image: "https://github.com/karelpak1.png",
|
||||
},
|
||||
{
|
||||
name: "ChickenMan7777",
|
||||
description: "Moderator and former advisor on game mechanics",
|
||||
url: "https://discord.gg/rEsUbXCqD9",
|
||||
image: "/media/users/chickenman7777.webp",
|
||||
},
|
||||
{
|
||||
name: "Avenir",
|
||||
description: "Moderator and elite Advanced Recon Commando",
|
||||
image: "/media/users/avenir.webp",
|
||||
},
|
||||
{
|
||||
name: "Pyogenics",
|
||||
description: "Former advisor on game 3D format",
|
||||
url: "https://github.com/Pyogenics",
|
||||
image: "https://github.com/Pyogenics.png",
|
||||
},
|
||||
{
|
||||
name: "Prince_NA",
|
||||
description: "Former advisor on bot and website",
|
||||
image: "/media/users/prince-na.webp",
|
||||
},
|
||||
{
|
||||
name: "Vovko",
|
||||
description: "Former advisor on bot",
|
||||
url: "https://amth.one/",
|
||||
image: "https://github.com/cufee.png",
|
||||
},
|
||||
{
|
||||
name: "S0AP",
|
||||
description: "Former advisor on and advertiser for bot",
|
||||
url: "https://discord.gg/3KfH93um3Y",
|
||||
image: "/media/users/s0ap.webp",
|
||||
},
|
||||
{
|
||||
name: "Synx",
|
||||
description: "Former advisor on bot and website",
|
||||
url: "https://www.youtube.com/@Synx_Blitz",
|
||||
image: "/media/users/synx.webp",
|
||||
},
|
||||
{
|
||||
name: "34wotb",
|
||||
description: "Former advisor on bot",
|
||||
url: "https://www.twitch.tv/34wotb/",
|
||||
image: "/media/users/34wotb.jpg",
|
||||
},
|
||||
{
|
||||
name: "HisRoyalFatness",
|
||||
description: "Former advisor on website",
|
||||
url: "https://www.youtube.com/@hisroyalfatness",
|
||||
image: "/media/users/hisroyalfatness.jpg",
|
||||
},
|
||||
{
|
||||
name: "Minitelrose",
|
||||
description: "Former advisor on game mechanics",
|
||||
url: "https://wotinspector.com/",
|
||||
image: "/media/users/minitelrose.webp",
|
||||
},
|
||||
{
|
||||
name: "Maddox",
|
||||
description: "Game asset decompression",
|
||||
url: "https://github.com/Maddoxkkm",
|
||||
image: "https://github.com/Maddoxkkm.png",
|
||||
},
|
||||
{
|
||||
name: "TJ Duff",
|
||||
description: "Chief executive cheer leader",
|
||||
image: "/media/users/tj-duff.jpg",
|
||||
},
|
||||
];
|
||||
@@ -74,4 +74,8 @@ export const tools: Record<string, Tool> = {
|
||||
href: "https://discord.gg/nDt7AjGJQH",
|
||||
button: "plum",
|
||||
},
|
||||
credits: {
|
||||
id: "credits",
|
||||
button: "bronze",
|
||||
},
|
||||
};
|
||||
@@ -1,237 +0,0 @@
|
||||
import {
|
||||
parseHierarchies,
|
||||
Sc2ReadStream,
|
||||
type CamouflagesXml,
|
||||
type CamouflagesYaml,
|
||||
type TankParameters,
|
||||
} from "@blitzkit/core";
|
||||
import {
|
||||
Document,
|
||||
Node,
|
||||
NodeIO,
|
||||
PropertyType,
|
||||
Scene,
|
||||
} from "@gltf-transform/core";
|
||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||
import { dedup, mergeDocuments, unpartition } from "@gltf-transform/functions";
|
||||
import type { APIContext } from "astro";
|
||||
import type { Vector3Tuple, Vector4Tuple } from "three";
|
||||
import { SKIN_NUMBER } from "../../../../../constants/skinNumber";
|
||||
import { api } from "../../../../../core/blitzkit/api";
|
||||
import { mixStaticPaths } from "../../../../../core/blitzkit/mixStaticPaths";
|
||||
import { vfs } from "../../../../../core/blitzkit/vfs";
|
||||
import {
|
||||
extractMaterials,
|
||||
extractModel,
|
||||
} from "../../../../[...chunk_models_tanks]/[id]/model.glb";
|
||||
import { getStaticPaths as _getStaticPaths } from "../_index";
|
||||
|
||||
const SKIN_NUMBER_SLOTS = 5;
|
||||
const DIGIT_COLUMNS = 5;
|
||||
const DIGIT_ROWS = 2;
|
||||
|
||||
export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
|
||||
const camouflages = await api.camouflageDefinitions();
|
||||
|
||||
return Object.values(camouflages.camouflages)
|
||||
.filter((camouflage) => camouflage.tank_id)
|
||||
.map((camouflage) => ({
|
||||
params: { id: camouflage.tank_id!, skin: camouflage.id },
|
||||
props: { skin: camouflage.id },
|
||||
}));
|
||||
});
|
||||
|
||||
const camouflagesXml = await vfs.xml<{ root: CamouflagesXml }>(
|
||||
"Data/XML/item_defs/vehicles/common/camouflages.xml",
|
||||
);
|
||||
const camouflagesYaml = await vfs.yaml<CamouflagesYaml>(
|
||||
"Data/camouflages.yaml",
|
||||
);
|
||||
const customization = await vfs.yaml<
|
||||
Record<string, { Name: string; Path: string }>
|
||||
>("Data/3d/Customization.yaml");
|
||||
|
||||
export async function GET({ props }: APIContext<{ skin: number }>) {
|
||||
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
||||
|
||||
const items = new Map(
|
||||
Object.values(customization).map(({ Name, Path }) => [
|
||||
Name,
|
||||
Path.replace("~res:/3d/", "").replace(/\.sc2$/, ""),
|
||||
]),
|
||||
);
|
||||
const key = Object.keys(camouflagesXml.root.camouflages).find(
|
||||
(key) => camouflagesXml.root.camouflages[key].id === props.skin,
|
||||
)!;
|
||||
const include = camouflagesXml.root.camouflages[key].vehicleFilter.include;
|
||||
const [nation, tankKey] = (
|
||||
include as { vehicle: { name: string } }
|
||||
).vehicle.name.split(":");
|
||||
const { preset, customEntities } = camouflagesYaml[key];
|
||||
const skinNumber = Number(preset?.match(/(\d+)$/)?.[1] ?? 1);
|
||||
const parameters = await vfs.yaml<TankParameters>(
|
||||
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||
);
|
||||
const basePath = parameters.resourcesPath.blitzModelPath.replace(
|
||||
/\.sc2$/,
|
||||
"",
|
||||
);
|
||||
const sc2 = new Sc2ReadStream(
|
||||
(await vfs.file(`Data/3d/${basePath}.sc2`)).buffer as ArrayBuffer,
|
||||
).sc2();
|
||||
const slots: Record<
|
||||
string,
|
||||
{
|
||||
target: string;
|
||||
position: Vector3Tuple;
|
||||
rotation: Vector4Tuple;
|
||||
scale: Vector3Tuple;
|
||||
}
|
||||
> = {};
|
||||
const states: Record<string, string | null> = {};
|
||||
|
||||
parseHierarchies(sc2["#hierarchy"], null, (hierarchy, components) => {
|
||||
for (const component of components) {
|
||||
if (component["comp.typename"] === "NewSlotComponent") {
|
||||
for (const slot of Object.values(component.slots)) {
|
||||
slots[slot.name] = {
|
||||
target: hierarchy.name,
|
||||
position: slot.transform.position,
|
||||
rotation: slot.transform.rotation,
|
||||
scale: slot.transform.scale,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (component["comp.typename"] === "SlotComponent") {
|
||||
slots[component["sc.slotName"]] = {
|
||||
target: hierarchy.name,
|
||||
position: component["sc.attachmentTranslation"] as Vector3Tuple,
|
||||
rotation: component["sc.attachmentRotation"] as Vector4Tuple,
|
||||
scale: component["sc.attachmentScale"] as Vector3Tuple,
|
||||
};
|
||||
}
|
||||
|
||||
if (component["comp.typename"] !== "StateSwitcherComponent") continue;
|
||||
|
||||
const active = component[`ssc.state${component["ssc.activeState"]}`];
|
||||
// camos without their own variant keep the default piece, but groups
|
||||
// of elements to hide stay hidden
|
||||
let state: string | null =
|
||||
active && !/skin\d*/.test(active) && !active.includes("hide")
|
||||
? active
|
||||
: null;
|
||||
|
||||
for (let index = 0; index < component["ssc.statesCount"]; index++) {
|
||||
const name = component[`ssc.state${index}`];
|
||||
const match = name.match(/skin(\d*)/);
|
||||
|
||||
if (
|
||||
match &&
|
||||
!name.includes("hide") &&
|
||||
Number(match[1] || 1) === skinNumber
|
||||
) {
|
||||
state = name;
|
||||
}
|
||||
}
|
||||
|
||||
states[hierarchy.name] = state;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const document = new Document();
|
||||
const scene = document.createScene().setExtras({ states });
|
||||
const targets = new Map<string, Node>();
|
||||
|
||||
document.createBuffer();
|
||||
|
||||
if (preset) await extractMaterials(document, sc2, basePath, preset);
|
||||
|
||||
for (const name in customEntities) {
|
||||
const slot = slots[name];
|
||||
const path = items.get(customEntities[name].item);
|
||||
|
||||
if (
|
||||
!slot ||
|
||||
!path ||
|
||||
!(await vfs.resolve(`Data/3d/${path}.sc2`)) ||
|
||||
!(await vfs.resolve(`Data/3d/${path}.scg`))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const part = await extractModel(vfs, path, true);
|
||||
const map = mergeDocuments(document, part);
|
||||
const partScene = map.get(part.getRoot().listScenes()[0]) as Scene;
|
||||
const node = document
|
||||
.createNode(name)
|
||||
.setTranslation(slot.position)
|
||||
.setRotation(slot.rotation)
|
||||
.setScale(slot.scale);
|
||||
let target = targets.get(slot.target);
|
||||
|
||||
if (!target) {
|
||||
target = document.createNode(slot.target);
|
||||
targets.set(slot.target, target);
|
||||
scene.addChild(target);
|
||||
}
|
||||
|
||||
for (const child of partScene.listChildren()) {
|
||||
node.addChild(child as Node);
|
||||
}
|
||||
|
||||
partScene.dispose();
|
||||
target.addChild(node);
|
||||
}
|
||||
|
||||
applySkinNumber(document, SKIN_NUMBER);
|
||||
|
||||
await document.transform(
|
||||
dedup({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.TEXTURE] }),
|
||||
unpartition(),
|
||||
);
|
||||
|
||||
return new Response(await nodeIO.writeBinary(document));
|
||||
}
|
||||
|
||||
function applySkinNumber(document: Document, number: string) {
|
||||
const start = Math.ceil((SKIN_NUMBER_SLOTS - number.length) / 2) + 1;
|
||||
|
||||
for (const node of document.getRoot().listNodes()) {
|
||||
const match = node.getName().match(/_number_\d+_(\d+)$/);
|
||||
|
||||
if (!match) continue;
|
||||
|
||||
const digit = number[Number(match[1]) - start];
|
||||
|
||||
if (digit === undefined) {
|
||||
node.dispose();
|
||||
continue;
|
||||
}
|
||||
|
||||
const cell = (Number(digit) + 9) % 10;
|
||||
const column = cell % DIGIT_COLUMNS;
|
||||
const row = Math.floor(cell / DIGIT_COLUMNS);
|
||||
|
||||
node.traverse((child) => {
|
||||
for (const primitive of child.getMesh()?.listPrimitives() ?? []) {
|
||||
const uvs = primitive.getAttribute("TEXCOORD_0")?.clone();
|
||||
|
||||
if (!uvs) continue;
|
||||
|
||||
for (let index = 0; index < uvs.getCount(); index++) {
|
||||
const [u, v] = uvs.getElement(index, [0, 0]) as [number, number];
|
||||
|
||||
uvs.setElement(index, [
|
||||
(column + u) / DIGIT_COLUMNS,
|
||||
(row + v) / DIGIT_ROWS,
|
||||
]);
|
||||
}
|
||||
|
||||
primitive.setAttribute("TEXCOORD_0", uvs);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
import CreditsPromo from "../../../../components/CreditsPromo.astro";
|
||||
import { tools } from "../../../../constants/tools";
|
||||
import ToolLayout from "../../../../layouts/ToolLayout.astro";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<ToolLayout tool={tools.credits} {locale}>
|
||||
<CreditsPromo {locale} />
|
||||
</ToolLayout>
|
||||