mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 12:09:51 +02:00
Compare commits
28
Commits
a9a870c3e6
...
5d2c63fed9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d2c63fed9 | ||
|
|
cead140855 | ||
|
|
63a0143d4f | ||
|
|
19f22993f1 | ||
|
|
b2eb74914f | ||
|
|
05572918b1 | ||
|
|
1cc8b26b16 | ||
|
|
a8e8d20cf1 | ||
|
|
55deb30607 | ||
|
|
03b8b89a17 | ||
|
|
3e421c7eb7 | ||
|
|
fc27caabdc | ||
|
|
3d35fae5ce | ||
|
|
f6dfc4f176 | ||
|
|
fdbbe2f356 | ||
|
|
cdf2aee43f | ||
|
|
56933ddfac | ||
|
|
1df2e818be | ||
|
|
1e4d00ea34 | ||
|
|
1bde50e0a3 | ||
|
|
88290ec1ab | ||
|
|
e8325df832 | ||
|
|
394f28e6ed | ||
|
|
f5f170bcca | ||
|
|
2ffe3fc006 | ||
|
|
406d70b5b7 | ||
|
|
f801e81c8b | ||
|
|
a0e1d8cee0 |
No files matched your search
@@ -73,10 +73,14 @@ jobs:
|
||||
with:
|
||||
repo-token: ${{ secrets.GH_TOKEN }}
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
|
||||
- run: bun run setup
|
||||
|
||||
- name: AppArmor hack
|
||||
run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
|
||||
|
||||
- name: Install patches
|
||||
run: bun run setup:patches --source=${{ github.event.inputs.asset_source || 'steam:444200/444202' }}
|
||||
|
||||
- name: Build assets
|
||||
run: bun run build:assets --all-targets --source=${{ github.event.inputs.asset_source || 'steam:444200/444202' }}
|
||||
@@ -6,6 +6,7 @@
|
||||
"name": "blitzkit",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.18.0",
|
||||
"lz4js": "^0.2.0",
|
||||
"prisma": "^6.18.0",
|
||||
"sharp": "^0.34.5",
|
||||
"typescript": "^5.9.3",
|
||||
@@ -78,7 +79,6 @@
|
||||
"dotenv": "^17.2.2",
|
||||
"fast-xml-parser": "^5.3.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lz4js": "^0.2.0",
|
||||
"pickleparser": "^0.2.1",
|
||||
"protobufjs": "^7.5.4",
|
||||
"three": "^0.180.0",
|
||||
@@ -118,7 +118,6 @@
|
||||
"fast-xml-parser": "^5.2.5",
|
||||
"googleapis": "^162.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lz4js": "^0.2.0",
|
||||
"progress": "^2.0.3",
|
||||
"puppeteer": "^24.22.3",
|
||||
"sharp": "^0.34.4",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# BlitzKit 1.30.3
|
||||
|
||||
## Fixes
|
||||
|
||||
- BlitzKit now correctly implements minor WoTB updates
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "blitzkit",
|
||||
"version": "1.30.3",
|
||||
"version": "1.30.4",
|
||||
"license": "BlitzKit License",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -21,7 +21,7 @@
|
||||
"dev": "bun run dev:website",
|
||||
"dev:api": "bun --filter=@blitzkit/api run dev",
|
||||
"setup:depotdownloader": "bun --filter=@blitzkit/scripts run setup:depotdownloader",
|
||||
"setup:patches": "bun --filter=@blitzkit/scripts run setup:patches",
|
||||
"setup:patches": "mkdir -p packages/closed/node_modules && ln -sf ../../../node_modules/lz4js packages/closed/node_modules/lz4js && cd packages/scripts && bun run setup:patches",
|
||||
"start:website": "bun --filter=@blitzkit/website run start",
|
||||
"host:docs": "vitepress preview docs",
|
||||
"lint": "tsc",
|
||||
@@ -35,7 +35,7 @@
|
||||
"build:reviews": "cd packages/scripts && bun build:reviews",
|
||||
"build:ids": "cd packages/scripts && bun build:ids",
|
||||
"prunestrings": "cd packages/scripts && bun prunestrings",
|
||||
"setup": "bun install && bun run build:prisma && bun run build:proxyclient",
|
||||
"setup": "bun install && bun run build:prisma",
|
||||
"patchnotes": "cd packages/scripts && bun run patchnotes"
|
||||
},
|
||||
"type": "module",
|
||||
@@ -44,6 +44,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.18.0",
|
||||
"lz4js": "^0.2.0",
|
||||
"prisma": "^6.18.0",
|
||||
"sharp": "^0.34.5",
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
+1
-1
Submodule packages/closed updated: 5b4a85d503...f89f0e0772.
@@ -19,7 +19,6 @@
|
||||
"dotenv": "^17.2.2",
|
||||
"fast-xml-parser": "^5.3.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lz4js": "^0.2.0",
|
||||
"pickleparser": "^0.2.1",
|
||||
"protobufjs": "^7.5.4",
|
||||
"three": "^0.180.0",
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
}
|
||||
},
|
||||
"leagues": {
|
||||
"diamond": "Диамантовая",
|
||||
"diamond": "Алмазная",
|
||||
"platinum": "Платиновая",
|
||||
"gold": "Золотая",
|
||||
"silver": "Серебряная",
|
||||
@@ -196,7 +196,7 @@
|
||||
"get_help": "Получить помощь",
|
||||
"uncaught_error": "# BlitzKit столкнулся с ошибкой!\nНам очень жаль. Не стесняйтесь и [присоединяйтесь к официальному серверу Discord](https://discord.gg/nDt7AjGJQH), чтобы получить помощь.",
|
||||
"preview": "# Эта команда находится в тестируется\n\nВы можете получить доступ к этой команде только на [официальном сервере Discord] (https://discord.gg/nDt7AjGJQH). Команда скоро станет общедоступной.",
|
||||
"query_too_small": "# Ваш поисковой запрос слишком короткий\nДлина вашего запроса должна составлять не менее 3 символов.",
|
||||
"query_too_small": "# Ваш поисковой слишком короткий\nДлина вашего запроса не менее 3 символов.",
|
||||
"query_too_long": "# Ваш поисковой запрос слишком длинный\nДлина вашего запроса должна составлять не более 100 символов."
|
||||
},
|
||||
"wn8": {
|
||||
@@ -320,13 +320,13 @@
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"dead": ".",
|
||||
"dead": "# Команды статистики мертвы\n\nБот BlitzKit использует BlitzStars для получения периодической статистики. BlitzStars включил агрессивную защиту Cloudflare; следовательно, отключив BlitzKit от использования API BlitzStar. Это повлияло на следующие команды:\n\n- `/stats`\n- `/today`\n- `/breakdown`\n- `/evolution`\n- `/full-stats`\n\nПожалуйста, используйте [Aftermath](<https://amth.one/invite>) в качестве альтернативы, которая совершенно не зависит от BlitzStars. Команда статистики BlitzKit-hosted вернется в ближайшее время.",
|
||||
"replay": {
|
||||
"name": "повтор",
|
||||
"description": "Показать результаты повтора",
|
||||
"options": {
|
||||
"file": {
|
||||
"name": "Файл",
|
||||
"name": "файл",
|
||||
"description": "Запись повтора"
|
||||
}
|
||||
}
|
||||
@@ -351,7 +351,7 @@
|
||||
"upgrades": "Улучшения: {cost}",
|
||||
"purchase": "Покупка: {cost}",
|
||||
"equipment": "Снаряжение: {cost}",
|
||||
"estimation": "# Это приблизительная оценка!\n\nВ этот список могут входить уже исследованные танки из-за неточной информации, предоставляемой Wargaming. Попробуйте указать «стартовый-танк» в строке при использовании команды, если сможете."
|
||||
"estimation": "# Это приблизительная оценка!\n\nВ этот список могут входить уже исследованные танки из-за неточной информации, предоставляемой Wargaming. Попробуйте указать «стартовый танк» в строке при использовании команды, если сможете."
|
||||
},
|
||||
"errors": {
|
||||
"non_tech_tree": "# {tank} отсутствует в дереве танков\n\nВы не можете исследовать премиумный / коллекционный танк.",
|
||||
@@ -363,7 +363,7 @@
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"name": "о-боте",
|
||||
"name": "about",
|
||||
"description": "Вся необходимая информация о боте",
|
||||
"subcommands": {
|
||||
"commands": {
|
||||
@@ -372,7 +372,7 @@
|
||||
"body": "# Команды\n\nBlitzKit предлагает следующие команды:\n\n"
|
||||
},
|
||||
"embeds": {
|
||||
"name": "встраиваемые-элементы",
|
||||
"name": "embeds",
|
||||
"description": "Как работают встраиваемые элементы"
|
||||
},
|
||||
"introduction": {
|
||||
@@ -384,7 +384,7 @@
|
||||
"description": "Как пригласить бота"
|
||||
},
|
||||
"timezones": {
|
||||
"name": "часовой-пояс",
|
||||
"name": "timezones",
|
||||
"description": "Как BlitzKit определяет и использует ваш часовой пояс"
|
||||
},
|
||||
"credits": {
|
||||
@@ -409,11 +409,11 @@
|
||||
"name": "отладка",
|
||||
"description": "Сведения для отладки",
|
||||
"body": {
|
||||
"title": "# Отладка",
|
||||
"title": "# Debug\n\n",
|
||||
"version": "Версия",
|
||||
"shard": {
|
||||
"label": "Фрагмент",
|
||||
"default": "по умолчанию"
|
||||
"default": "default"
|
||||
},
|
||||
"uptime": "Время работы"
|
||||
}
|
||||
@@ -443,11 +443,11 @@
|
||||
},
|
||||
"body": {
|
||||
"subtitle": "Неактивен {days}+ дней",
|
||||
"listing": "{days} дней"
|
||||
"listing": "{days{days}"
|
||||
}
|
||||
},
|
||||
"full_stats": {
|
||||
"name": "полная-статистика",
|
||||
"name": "full-stats",
|
||||
"description": "Полная игровая статистика",
|
||||
"body": {
|
||||
"cards": {
|
||||
@@ -496,7 +496,7 @@
|
||||
}
|
||||
},
|
||||
"search_clans": {
|
||||
"name": "поиск-кланов",
|
||||
"name": "search-clans",
|
||||
"description": "Поиск кланов",
|
||||
"options": {
|
||||
"limit": {
|
||||
@@ -510,7 +510,7 @@
|
||||
}
|
||||
},
|
||||
"search_players": {
|
||||
"name": "поиск-игроков",
|
||||
"name": "search-players",
|
||||
"description": "Поиск игроков",
|
||||
"options": {
|
||||
"limit": {
|
||||
@@ -524,7 +524,7 @@
|
||||
}
|
||||
},
|
||||
"rating_leaderboard": {
|
||||
"name": "таблица-лидеров",
|
||||
"name": "rating-leaderboard",
|
||||
"description": "Статистика рейтингов боёв",
|
||||
"subcommands": {
|
||||
"neighbors": {
|
||||
@@ -563,7 +563,7 @@
|
||||
}
|
||||
},
|
||||
"today": {
|
||||
"name": "cегодня",
|
||||
"name": "today",
|
||||
"description": "Танки, на которых сыграли сегодня и статистика"
|
||||
},
|
||||
"stats": {
|
||||
@@ -607,8 +607,8 @@
|
||||
}
|
||||
},
|
||||
"player_info": {
|
||||
"name": "информация-об-игроке",
|
||||
"description": "Основная информация о игроке",
|
||||
"name": "player-ifor",
|
||||
"description": "Basic information about a player",
|
||||
"body": {
|
||||
"title": "Информация о {name}",
|
||||
"nickname": "Никнейм",
|
||||
@@ -824,7 +824,7 @@
|
||||
"skip": "Пропустить",
|
||||
"guess": "Угадать",
|
||||
"next": "Следующий",
|
||||
"no_results": "Ничего не найденo"
|
||||
"no_results": "Ничего не найдено"
|
||||
},
|
||||
"stats": "{correct} / {total} правильно, серия: {streak}",
|
||||
"help": {
|
||||
|
||||
@@ -876,7 +876,7 @@
|
||||
"yaw": "Yaw",
|
||||
"pitch": "Pitch",
|
||||
"min": "Min",
|
||||
"max": "Max"
|
||||
"max": "Макс"
|
||||
}
|
||||
},
|
||||
"sandbox": {
|
||||
@@ -908,7 +908,7 @@
|
||||
"normalization": "Нормалізація",
|
||||
"ricochet": "Рикошет",
|
||||
"explosion_radius": "Радіус розльоту уламків",
|
||||
"exit": "Exit",
|
||||
"exit": "Вийти",
|
||||
"reset": "Reset"
|
||||
},
|
||||
"advanced": "Advanced highlighting",
|
||||
@@ -923,7 +923,7 @@
|
||||
},
|
||||
"status": {
|
||||
"penetration": "{damage}hp penetration",
|
||||
"blocked": "Blocked",
|
||||
"blocked": "Заблоковано",
|
||||
"ricochet": "Рикошет",
|
||||
"splash": "{damage}hp splash"
|
||||
},
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
"fast-xml-parser": "^5.2.5",
|
||||
"googleapis": "^162.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lz4js": "^0.2.0",
|
||||
"progress": "^2.0.3",
|
||||
"puppeteer": "^24.22.3",
|
||||
"sharp": "^0.34.4",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { mkdir, writeFile } from "fs/promises";
|
||||
import { parse as parsePath } from "path";
|
||||
import ProgressBar from "progress";
|
||||
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
||||
import { vfs } from "./buildAssets/constants";
|
||||
import { PATCHES_ROOT, vfs } from "./buildAssets/constants";
|
||||
import { writeDVPL } from "./core/blitz/writeDVPL";
|
||||
|
||||
const versionTextFile = await vfs.text(`Data/version.txt`);
|
||||
@@ -20,27 +20,30 @@ let patchIndex = 1;
|
||||
while (true) {
|
||||
const response = await fetch(
|
||||
`${assertSecret(
|
||||
import.meta.env.WOTB_DLC_CDN
|
||||
)}/dlc/s${currentVersion}_${patchIndex}.yaml`
|
||||
import.meta.env.WOTB_DLC_CDN,
|
||||
)}/dlc/s${currentVersion}_${patchIndex}.yaml`,
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
console.log(patchIndex, response.status)
|
||||
|
||||
if (!response.ok) break;
|
||||
|
||||
console.log(`Applying patch ${patchIndex}...`);
|
||||
|
||||
const data = parseYaml(await response.text());
|
||||
const dvpm = await fetch(
|
||||
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${data.dx11}`
|
||||
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${data.dx11}`,
|
||||
).then((response) => response.arrayBuffer());
|
||||
const dvpd = await fetch(
|
||||
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${data.dx11.replace(
|
||||
".dvpm",
|
||||
".dvpd"
|
||||
)}`
|
||||
".dvpd",
|
||||
)}`,
|
||||
).then((response) => response.arrayBuffer());
|
||||
const files = await dvp(dvpm, dvpd);
|
||||
const bar = new ProgressBar(
|
||||
`Patching ${files.length} files :bar`,
|
||||
files.length
|
||||
files.length,
|
||||
);
|
||||
|
||||
for (const { path, data } of files) {
|
||||
@@ -49,43 +52,55 @@ while (true) {
|
||||
const { dir } = parsePath(path);
|
||||
|
||||
try {
|
||||
await mkdir(`Data/${dir}`, { recursive: true });
|
||||
await mkdir(`${PATCHES_ROOT}/Data/${dir}`, { recursive: true });
|
||||
} catch (error) {
|
||||
console.warn(`Failed to make directory "${dir}"`);
|
||||
}
|
||||
|
||||
const isDvpl = path.endsWith(".dvpl");
|
||||
const buffer = Buffer.from(data);
|
||||
|
||||
await writeFile(
|
||||
`Data/${path}${isDvpl ? "" : ".dvpl"}`,
|
||||
new Uint8Array(isDvpl ? buffer : writeDVPL(buffer))
|
||||
`${PATCHES_ROOT}/Data/${path}`,
|
||||
new Uint8Array(buffer),
|
||||
);
|
||||
|
||||
bar.tick();
|
||||
}
|
||||
|
||||
if ("dynamicContentLocalizationsDir" in data) {
|
||||
console.log("Found dynamic content localizations; patching...");
|
||||
console.log('patches done for patch')
|
||||
|
||||
if ("dynamicContentLocalizationsDir" in data) {
|
||||
console.log(
|
||||
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${
|
||||
data.dynamicContentLocalizationsDir
|
||||
}/en.yaml`,
|
||||
);
|
||||
const localizationsResponse = await fetch(
|
||||
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${
|
||||
data.dynamicContentLocalizationsDir
|
||||
}/en.yaml`
|
||||
}/en.yaml`,
|
||||
);
|
||||
console.log('fetched')
|
||||
const newStrings = parseYaml(await localizationsResponse.text());
|
||||
const oldStrings = await vfs.yaml<Record<string, string>>(
|
||||
`Data/Strings/en.yaml`
|
||||
);
|
||||
console.log('got new strings')
|
||||
const oldStrings =
|
||||
await vfs.yaml<Record<string, string>>(`Data/Strings/en.yaml`);
|
||||
console.log('got old strings')
|
||||
const patchedStrings = { ...oldStrings, ...newStrings };
|
||||
const patchedContent = stringifyYaml(patchedStrings);
|
||||
console.log('stringified')
|
||||
|
||||
await mkdir(`${PATCHES_ROOT}/Data/Strings`, { recursive: true });
|
||||
console.log('mkdir')
|
||||
await writeFile(
|
||||
`Data/Strings/en.yaml`,
|
||||
new Uint8Array(writeDVPL(Buffer.from(patchedContent)))
|
||||
`${PATCHES_ROOT}/Data/Strings/en.yaml`,
|
||||
new Uint8Array((Buffer.from(patchedContent))),
|
||||
);
|
||||
console.log('file')
|
||||
}
|
||||
|
||||
patchIndex++;
|
||||
} else break;
|
||||
}
|
||||
|
||||
vfs.dispose()
|
||||
process.exit(0);
|
||||
@@ -3,6 +3,9 @@ import { AbstractVFS } from "../core/vfs/abstract";
|
||||
import { LocalVFS } from "../core/vfs/local";
|
||||
import { SteamVFS } from "../core/vfs/steam";
|
||||
import { ZipVFS } from "../core/vfs/zip";
|
||||
import { MixedVFS } from "../core/vfs/mixed";
|
||||
|
||||
export const PATCHES_ROOT = "../../temp/patches";
|
||||
|
||||
const steamPattern = /steam:(\d+)\/(\d+)/;
|
||||
const zipPattern = /zip:(.+)/;
|
||||
@@ -35,4 +38,6 @@ if (!source) {
|
||||
throw new Error("No valid source specified");
|
||||
}
|
||||
|
||||
resolved = new MixedVFS([new LocalVFS(PATCHES_ROOT), resolved]);
|
||||
|
||||
export const vfs = resolved;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { AbstractVFS } from "./abstract";
|
||||
|
||||
export class MixedVFS extends AbstractVFS {
|
||||
constructor(private vfses: AbstractVFS[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
async init() {
|
||||
for (const vfs of this.vfses) {
|
||||
await vfs.init();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
for (const vfs of this.vfses) {
|
||||
vfs.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async has(path: string) {
|
||||
for (const vfs of this.vfses) {
|
||||
if (await vfs.has(path)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async raw(path: string) {
|
||||
for (const vfs of this.vfses) {
|
||||
if (await vfs.has(path)) return await vfs.raw(path);
|
||||
}
|
||||
|
||||
throw new Error(`File not found: ${path}`);
|
||||
}
|
||||
|
||||
async dir(path: string) {
|
||||
const files = new Set<string>();
|
||||
|
||||
for (const vfs of this.vfses) {
|
||||
for (const file of await vfs.dir(path)) {
|
||||
files.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(files);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { J_HAT, K_HAT } from "@blitzkit/core";
|
||||
import { Box, Flex, Text } from "@radix-ui/themes";
|
||||
import { Html } from "@react-three/drei";
|
||||
import { invalidate, useFrame } from "@react-three/fiber";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { clamp } from "lodash-es";
|
||||
import { useEffect, useRef, type ComponentProps } from "react";
|
||||
import { useEffect, useRef, type ComponentProps, type RefObject } from "react";
|
||||
import {
|
||||
BufferGeometry,
|
||||
DoubleSide,
|
||||
@@ -42,11 +42,10 @@ const TRACER_THIN = TRACER_THICK / 2;
|
||||
|
||||
export function ShotDisplay() {
|
||||
const shot = Tankopedia.use((state) => state.shot);
|
||||
const inTracer = useRef<Mesh>(null);
|
||||
const outTracer = useRef<Mesh>(null);
|
||||
const splashRadiusWrapper = useRef<Group>(null);
|
||||
const splashRadiusMaterial = useRef<MeshBasicMaterial>(null);
|
||||
let animationStartTime: number | null = null;
|
||||
const inTracer = useRef<Mesh>(null!);
|
||||
const outTracer = useRef<Mesh>(null!);
|
||||
const splashRadiusWrapper = useRef<Group>(null!);
|
||||
const splashRadiusMaterial = useRef<MeshBasicMaterial>(null!);
|
||||
const { locale } = useLocale();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -64,10 +63,10 @@ export function ShotDisplay() {
|
||||
outlineMaterial,
|
||||
).computeLineDistances();
|
||||
|
||||
splashRadiusWrapper.current?.add(outline);
|
||||
splashRadiusWrapper.current.add(outline);
|
||||
|
||||
return () => {
|
||||
splashRadiusWrapper.current?.remove(outline);
|
||||
splashRadiusWrapper.current.remove(outline);
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -86,44 +85,6 @@ export function ShotDisplay() {
|
||||
}
|
||||
}, [shot]);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!shot) return;
|
||||
|
||||
const t = clock.elapsedTime;
|
||||
|
||||
if (inTracer.current) {
|
||||
const tracerT1 = t % 1;
|
||||
const tracerT2 = (t + 0.5) % 1;
|
||||
|
||||
inTracer.current.scale.set(1, 1 - 2 * Math.abs(tracerT1 - 0.5), 1);
|
||||
inTracer.current.position.set(0, (1 - tracerT1) * inLength, 0);
|
||||
|
||||
if (outTracer.current) {
|
||||
outTracer.current.scale.set(1, 1 - 2 * Math.abs(tracerT2 - 0.5), 1);
|
||||
outTracer.current.position.set(0, tracerT2 * outLength, 0);
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
if (
|
||||
shot.splashRadius !== undefined &&
|
||||
splashRadiusWrapper.current &&
|
||||
splashRadiusMaterial.current
|
||||
) {
|
||||
if (animationStartTime === null) animationStartTime = t;
|
||||
|
||||
const scale =
|
||||
clamp(4 * (t - animationStartTime), 0, 1) * shot.splashRadius;
|
||||
const opacity = 1 - clamp(2 * (t - animationStartTime), 0, 1);
|
||||
|
||||
splashRadiusWrapper.current.scale.set(scale, scale, scale);
|
||||
splashRadiusMaterial.current.opacity = opacity;
|
||||
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
|
||||
if (!shot) return null;
|
||||
|
||||
const inLength =
|
||||
@@ -149,6 +110,15 @@ export function ShotDisplay() {
|
||||
|
||||
return (
|
||||
<group>
|
||||
<Animator
|
||||
inTracer={inTracer}
|
||||
outTracer={outTracer}
|
||||
inLength={inLength}
|
||||
outLength={outLength}
|
||||
splashRadiusWrapper={splashRadiusWrapper}
|
||||
splashRadiusMaterial={splashRadiusMaterial}
|
||||
/>
|
||||
|
||||
{shot.splashRadius && (
|
||||
<group
|
||||
position={inLast.point}
|
||||
@@ -283,3 +253,59 @@ export function ShotDisplay() {
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
interface AnimatorProps {
|
||||
inTracer: RefObject<Mesh>;
|
||||
outTracer: RefObject<Mesh>;
|
||||
|
||||
splashRadiusWrapper: RefObject<Group>;
|
||||
splashRadiusMaterial: RefObject<MeshBasicMaterial>;
|
||||
|
||||
inLength: number;
|
||||
outLength: number;
|
||||
}
|
||||
|
||||
function Animator({
|
||||
inTracer,
|
||||
outTracer,
|
||||
splashRadiusWrapper,
|
||||
splashRadiusMaterial,
|
||||
inLength,
|
||||
outLength,
|
||||
}: AnimatorProps) {
|
||||
const shot = Tankopedia.use((state) => state.shot!);
|
||||
|
||||
let animationStartTime: number | null = null;
|
||||
|
||||
useFrame(({ clock, invalidate }) => {
|
||||
const t = clock.elapsedTime;
|
||||
|
||||
const tracerT1 = t % 1;
|
||||
const tracerT2 = (t + 0.5) % 1;
|
||||
|
||||
inTracer.current.scale.set(1, 1 - 2 * Math.abs(tracerT1 - 0.5), 1);
|
||||
inTracer.current.position.set(0, (1 - tracerT1) * inLength, 0);
|
||||
|
||||
if (outTracer.current) {
|
||||
outTracer.current.scale.set(1, 1 - 2 * Math.abs(tracerT2 - 0.5), 1);
|
||||
outTracer.current.position.set(0, tracerT2 * outLength, 0);
|
||||
}
|
||||
|
||||
invalidate();
|
||||
|
||||
if (shot.splashRadius !== undefined) {
|
||||
if (animationStartTime === null) animationStartTime = t;
|
||||
|
||||
const scale =
|
||||
clamp(4 * (t - animationStartTime), 0, 1) * shot.splashRadius;
|
||||
const opacity = 1 - clamp(2 * (t - animationStartTime), 0, 1);
|
||||
|
||||
splashRadiusWrapper.current.scale.set(scale, scale, scale);
|
||||
splashRadiusMaterial.current.opacity = opacity;
|
||||
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createPortal, useFrame, useThree } from '@react-three/fiber';
|
||||
import { memo, useState } from 'react';
|
||||
import { DepthTexture, Scene, Vector2 } from 'three';
|
||||
import { PrimaryArmorScene } from './components/PrimaryArmorScene';
|
||||
import { spacedArmorRenderTarget } from './components/PrimaryArmorSceneComponent/target';
|
||||
import { SpacedArmorScene } from './components/SpacedArmorScene';
|
||||
import { createPortal, useFrame, useThree } from "@react-three/fiber";
|
||||
import { memo, useState } from "react";
|
||||
import { DepthTexture, Scene, Vector2 } from "three";
|
||||
import { PrimaryArmorScene } from "./components/PrimaryArmorScene";
|
||||
import { spacedArmorRenderTarget } from "./components/PrimaryArmorSceneComponent/target";
|
||||
import { SpacedArmorScene } from "./components/SpacedArmorScene";
|
||||
|
||||
export const Armor = memo(() => {
|
||||
const rootScene = useThree((state) => state.scene);
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
|
||||
export const zoom = 60;
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
|
||||
export function AutoClear() {
|
||||
useFrame(({ gl }) => {
|
||||
|
||||
+34
-26
@@ -19,6 +19,8 @@ const poseDistances: Record<Pose, number> = {
|
||||
[Pose.Default]: -1,
|
||||
};
|
||||
|
||||
const inspectModeInitialPosition = new Vector3(-8, 2, -13);
|
||||
|
||||
const modelDefinitions = await awaitableModelDefinitions;
|
||||
|
||||
interface ControlsProps {
|
||||
@@ -77,22 +79,6 @@ export function Controls({
|
||||
protagonistHullOrigin.y +
|
||||
protagonistTurretOrigin.y +
|
||||
protagonistGunOrigin.y;
|
||||
const inspectModeInitialPosition = new Vector3(-8, 2, -13);
|
||||
|
||||
const t0 = useRef(Date.now() / 1000);
|
||||
useFrame(() => {
|
||||
if (!doAutoRotate) return;
|
||||
|
||||
const now = Date.now() / 1000;
|
||||
const t = now - t0.current;
|
||||
|
||||
camera.position
|
||||
.copy(inspectModeInitialPosition)
|
||||
.applyAxisAngle(I_HAT, (Math.PI / 32) * Math.sin(t / 9))
|
||||
.applyAxisAngle(J_HAT, (-Math.PI / 16) * Math.sin(t / 7));
|
||||
|
||||
invalidate();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function handleControlsEnabled(event: QuicklimeEvent<boolean>) {
|
||||
@@ -249,15 +235,37 @@ export function Controls({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<OrbitControls
|
||||
enableRotate={enableRotate}
|
||||
maxDistance={40}
|
||||
minDistance={5}
|
||||
enableZoom={zoomable}
|
||||
zoomSpeed={zoomable ? undefined : 0}
|
||||
ref={orbitControls}
|
||||
enabled={controlsEnabledEvent.last!}
|
||||
enableDamping={false}
|
||||
/>
|
||||
<>
|
||||
{doAutoRotate && <Animator />}
|
||||
|
||||
<OrbitControls
|
||||
enableRotate={enableRotate}
|
||||
maxDistance={40}
|
||||
minDistance={5}
|
||||
enableZoom={zoomable}
|
||||
zoomSpeed={zoomable ? undefined : 0}
|
||||
ref={orbitControls}
|
||||
enabled={controlsEnabledEvent.last!}
|
||||
enableDamping={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Animator() {
|
||||
const clock = useThree((state) => state.clock);
|
||||
const t0 = useRef(clock.elapsedTime);
|
||||
|
||||
useFrame(({ camera }) => {
|
||||
const t = clock.elapsedTime - t0.current;
|
||||
|
||||
camera.position
|
||||
.copy(inspectModeInitialPosition)
|
||||
.applyAxisAngle(I_HAT, (Math.PI / 32) * Math.sin(t / 9))
|
||||
.applyAxisAngle(J_HAT, (-Math.PI / 16) * Math.sin(t / 7));
|
||||
|
||||
invalidate();
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
+99
-73
@@ -1,63 +1,127 @@
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { times } from "lodash-es";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { clamp, times } from "lodash-es";
|
||||
import { Quicklime } from "quicklime";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState, type RefObject } from "react";
|
||||
import { HemisphereLight, SpotLight, type Group } from "three";
|
||||
import { clamp, degToRad, lerp } from "three/src/math/MathUtils.js";
|
||||
import { degToRad, lerp } from "three/src/math/MathUtils.js";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
import { TankopediaPersistent } from "../../../../../../stores/tankopediaPersistent";
|
||||
import { HelpingSpotLight } from "../../../../../HelpingSpotLight";
|
||||
|
||||
const ANGLE = Math.PI * 2 ** -2;
|
||||
const ANGLE = degToRad(10);
|
||||
const REVEAL_ANIMATION_TIME = 3;
|
||||
const TRANSITION_ANIMATION_TIME = 0.5;
|
||||
|
||||
const LIGHTS_COUNT = 5;
|
||||
const THETA_OFFSET = degToRad(-152);
|
||||
const LIGHT_DISTANCE = 13;
|
||||
const LIGHTS_COUNT = 4;
|
||||
const THETA_OFFSET = degToRad(180 - 45);
|
||||
const LIGHT_DISTANCE = 20;
|
||||
const LIGHT_HEIGHT_0 = 4;
|
||||
const LIGHT_HEIGHT_1 = 2;
|
||||
const INTENSITY_0 = 3.5;
|
||||
const INTENSITY_1 = 1.5;
|
||||
const LIGHT_HEIGHT_1 = 6;
|
||||
const INTENSITY_0 = 6;
|
||||
const INTENSITY_1 = 3;
|
||||
const HEMISPHERE_INTENSITY = 2;
|
||||
|
||||
export const transitionEvent = new Quicklime<number>(0);
|
||||
|
||||
export function Lighting() {
|
||||
const wrapper = useRef<Group>(null!);
|
||||
const t0 = useRef(0);
|
||||
const transition = useRef(false);
|
||||
const clock = useThree((state) => state.clock);
|
||||
const display = Tankopedia.use((state) => state.display);
|
||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||
const animationTime = useRef(REVEAL_ANIMATION_TIME);
|
||||
|
||||
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
|
||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||
|
||||
const animationTime = useRef(REVEAL_ANIMATION_TIME);
|
||||
const t0 = useRef(performance.now() / 1e3 - animationTime.current);
|
||||
|
||||
const isRevealing = useRef(true);
|
||||
|
||||
const [animate, setAnimate] = useState(true);
|
||||
|
||||
console.log(animate);
|
||||
|
||||
useEffect(() => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.revealed = true;
|
||||
});
|
||||
|
||||
t0.current = performance.now() / 1000 - REVEAL_ANIMATION_TIME;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isRevealing.current) {
|
||||
isRevealing.current = false;
|
||||
} else {
|
||||
animationTime.current = TRANSITION_ANIMATION_TIME;
|
||||
t0.current = performance.now() / 1e3;
|
||||
}
|
||||
|
||||
setAnimate(true);
|
||||
}, [requestedDisplay]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{animate && (
|
||||
<Animator
|
||||
t0={t0}
|
||||
animationTime={animationTime}
|
||||
wrapper={wrapper}
|
||||
stop={() => {
|
||||
setAnimate(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<group ref={wrapper}>
|
||||
<hemisphereLight intensity={0} color="#ffffff" groundColor="#afafaf" />
|
||||
|
||||
{times(LIGHTS_COUNT, (index) => {
|
||||
const x = index / (LIGHTS_COUNT - 1);
|
||||
const theta = 2 * Math.PI * (index / LIGHTS_COUNT) + THETA_OFFSET;
|
||||
const position = [
|
||||
LIGHT_DISTANCE * Math.sin(theta),
|
||||
lerp(LIGHT_HEIGHT_0, LIGHT_HEIGHT_1, x),
|
||||
LIGHT_DISTANCE * Math.cos(theta),
|
||||
] as const;
|
||||
const intensity = lerp(INTENSITY_0, INTENSITY_1, x);
|
||||
|
||||
return (
|
||||
<HelpingSpotLight
|
||||
userData={{ index }}
|
||||
key={index}
|
||||
position={position}
|
||||
intensity={intensity}
|
||||
penumbra={1}
|
||||
castShadow={highGraphics}
|
||||
decay={0}
|
||||
color="#ffffff"
|
||||
angle={0}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface AnimatorProps {
|
||||
stop: () => void;
|
||||
|
||||
t0: RefObject<number>;
|
||||
animationTime: RefObject<number>;
|
||||
wrapper: RefObject<Group>;
|
||||
}
|
||||
|
||||
function Animator({ stop, t0, animationTime, wrapper }: AnimatorProps) {
|
||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||
|
||||
useFrame(({ invalidate }) => {
|
||||
if (transition.current) {
|
||||
transition.current = false;
|
||||
t0.current = performance.now() / 1000;
|
||||
const dt = performance.now() / 1e3 - t0.current;
|
||||
const x = clamp(dt / animationTime.current, 0, 2);
|
||||
|
||||
if (x === 2) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const x = clamp(
|
||||
(performance.now() / 1000 - t0.current) / animationTime.current,
|
||||
0,
|
||||
2,
|
||||
);
|
||||
const t = (0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5) ** 2;
|
||||
|
||||
if (t !== transitionEvent.last) {
|
||||
transitionEvent.dispatch(t);
|
||||
}
|
||||
|
||||
for (const child of wrapper.current.children) {
|
||||
if (child instanceof SpotLight) {
|
||||
child.angle = ANGLE * t;
|
||||
@@ -66,53 +130,15 @@ export function Lighting() {
|
||||
}
|
||||
}
|
||||
|
||||
if (x < 2) invalidate();
|
||||
transitionEvent.dispatch(t);
|
||||
invalidate();
|
||||
|
||||
if (
|
||||
x > 1 &&
|
||||
Tankopedia.state.display !== Tankopedia.state.requestedDisplay
|
||||
) {
|
||||
if (x >= 1 && Tankopedia.state.display !== requestedDisplay) {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.display = requestedDisplay;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (requestedDisplay === display) return;
|
||||
|
||||
transition.current = true;
|
||||
animationTime.current = TRANSITION_ANIMATION_TIME;
|
||||
}, [requestedDisplay]);
|
||||
|
||||
return (
|
||||
<group ref={wrapper}>
|
||||
<hemisphereLight intensity={0} color="#ffffff" groundColor="#afafaf" />
|
||||
|
||||
{times(LIGHTS_COUNT, (index) => {
|
||||
const x = index / (LIGHTS_COUNT - 1);
|
||||
const theta = 2 * Math.PI * (index / LIGHTS_COUNT) + THETA_OFFSET;
|
||||
const position = [
|
||||
LIGHT_DISTANCE * Math.sin(theta),
|
||||
lerp(LIGHT_HEIGHT_0, LIGHT_HEIGHT_1, x),
|
||||
LIGHT_DISTANCE * Math.cos(theta),
|
||||
] as const;
|
||||
const intensity = lerp(INTENSITY_0, INTENSITY_1, x);
|
||||
|
||||
return (
|
||||
<HelpingSpotLight
|
||||
userData={{ index }}
|
||||
key={index}
|
||||
position={position}
|
||||
intensity={intensity}
|
||||
penumbra={1}
|
||||
castShadow={highGraphics}
|
||||
decay={0}
|
||||
color="#ffffff"
|
||||
angle={0}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
+2
-10
@@ -1,10 +1,9 @@
|
||||
import { invalidate, useFrame } from "@react-three/fiber";
|
||||
import { clamp } from "lodash-es";
|
||||
import { invalidate } from "@react-three/fiber";
|
||||
import { Quicklime, type QuicklimeEvent } from "quicklime";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Mesh, MeshStandardMaterial } from "three";
|
||||
|
||||
const SIZE = 2 ** 6;
|
||||
const SIZE = 14;
|
||||
|
||||
export const screenshotReadyEvent = new Quicklime(false);
|
||||
|
||||
@@ -12,11 +11,6 @@ export function SceneProps() {
|
||||
const mesh = useRef<Mesh>(null!);
|
||||
const material = useRef<MeshStandardMaterial>(null);
|
||||
|
||||
useFrame(({ camera }) => {
|
||||
if (!material.current) return;
|
||||
material.current.opacity = clamp(0.5 * camera.position.y, 0, 0.5);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function handleScreenshotReady(event: QuicklimeEvent<boolean>) {
|
||||
mesh.current.visible = !event.data;
|
||||
@@ -36,11 +30,9 @@ export function SceneProps() {
|
||||
receiveShadow
|
||||
visible={!screenshotReadyEvent.last!}
|
||||
ref={mesh}
|
||||
renderOrder={-1}
|
||||
>
|
||||
<planeGeometry args={[SIZE, SIZE]} />
|
||||
<meshStandardMaterial
|
||||
transparent
|
||||
ref={material}
|
||||
color="black"
|
||||
roughness={1}
|
||||
|
||||
+1
-1
@@ -156,7 +156,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
localClippingEnabled: true,
|
||||
preserveDrawingBuffer: true,
|
||||
}}
|
||||
shadows="soft"
|
||||
shadows="basic"
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMissed={() => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
|
||||
@@ -40,5 +40,5 @@ export const TankopediaPersistent = new Varuna<TankopediaPersistent>(
|
||||
direction: "ascending",
|
||||
},
|
||||
},
|
||||
"tankopedia-2"
|
||||
"tankopedia-3",
|
||||
);
|
||||
Reference in new issue
Block a user