Compare commits

...
5 Commits
Author SHA1 Message Date
TrèsAbhi ff02f06cbc feat: tanksDirectoryNations 2026-02-01 19:34:36 -06:00
TrèsAbhi 7a817cbe8c feat: smooth curtains 2026-02-01 12:42:52 -06:00
TrèsAbhi 58e0876aa0 feat: curtains 2026-02-01 12:41:29 -06:00
TrèsAbhi f84f846e76 feat: scroll functionality 2026-02-01 12:31:51 -06:00
TrèsAbhi ddb869e767 feat: start of scroller 2026-02-01 12:24:55 -06:00
9 changed files with 261 additions and 22 deletions

No files matched your search

+1
View File
@@ -221,6 +221,7 @@
"tungstentip",
"Turretless",
"TYPELESS",
"uasset",
"ubjson",
"Unicum",
"UNLOCALIZED",
+1
View File
@@ -4,6 +4,7 @@
"license": "BlitzKit License",
"private": true,
"scripts": {
"build:game": "cd packages/game && dotnet build",
"build:assets": "cd packages/scripts && bun run build:assets",
"build:averages": "bun --filter=@blitzkit/scripts run build:averages",
"build:bot": "cd packages/scripts && bun run build:bot",
+3
View File
@@ -0,0 +1,3 @@
[*.cs]
indent_style = space
indent_size = 2
-1
View File
@@ -4,7 +4,6 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NodeApiAssemblyJSModuleType>esm</NodeApiAssemblyJSModuleType>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
+35 -16
View File
@@ -7,22 +7,41 @@ namespace game.src.classes;
[JSExport]
public class BlitzProvider
{
private readonly DefaultFileProvider provider;
private readonly DefaultFileProvider provider;
public BlitzProvider(string directory)
{
provider = new(
directory: directory,
searchOption: SearchOption.AllDirectories,
versions: new(EGame.GAME_UE5_5),
pathComparer: StringComparer.OrdinalIgnoreCase
);
provider.Initialize();
provider.Mount();
}
public string[] files;
public string[] tanksDirectoryNations;
public string[] Files()
{
return provider.Files.Keys.ToArray();
}
public BlitzProvider(string directory)
{
provider = new(
directory: directory,
searchOption: SearchOption.AllDirectories,
versions: new(EGame.GAME_UE5_5),
pathComparer: StringComparer.OrdinalIgnoreCase
);
provider.Initialize();
provider.Mount();
files = [.. provider.Files.Keys];
var tanksBase = "Blitz/Content/Tanks/";
tanksDirectoryNations =
[
.. files
.Where(path => path.StartsWith(tanksBase))
.Select(path => path[tanksBase.Length..])
.Select(path => path.Split('/')[0])
.Where(nation => !nation.EndsWith(".uasset") && nation != "TankStub")
.Distinct(),
];
}
public string[] Files => files;
public string[] TanksDirectoryNations => tanksDirectoryNations;
public byte[] TankBigIcon(string tankId, string pdaName)
{
return [];
}
}
@@ -2,6 +2,7 @@
import { api } from "../core/api/dynamic";
import { tools } from "../core/tools/tools";
import { Heading } from "./Heading";
import { Scroller } from "./Scroller";
import { Section } from "./Section";
import { TankCard } from "./TankCard";
import ToolCard from "./ToolCard.astro";
@@ -35,8 +36,6 @@ const popular = [
}
.tanks {
/* overflow-x: scroll; */
overflow-x: hidden;
z-index: 1;
.content {
@@ -50,11 +49,11 @@ const popular = [
<Section>
<Heading size="2" className="title">{strings.home.tanks}</Heading>
<div class="tanks">
<Scroller client:load className="tanks">
<div class="content">
{popular.map((tank) => <TankCard {tank} />)}
</div>
</div>
</Scroller>
<ToolCard gradient {locale} tool={tools.tanks} />
@@ -1 +0,0 @@
export function Scroller() {}
@@ -0,0 +1,78 @@
.scroller {
display: flex;
flex-direction: column;
gap: var(--space-3);
position: relative;
.content {
overflow: auto;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
}
.bar-wrapper {
opacity: 0;
padding: var(--space-1);
background-color: var(--gray-3);
transition: opacity var(--duration-brief);
border-radius: var(--max-size);
.bar-container {
height: var(--space-3);
position: relative;
.bar {
height: 100%;
min-width: min(100%, var(--space-6));
position: absolute;
border-radius: var(--max-size);
background-color: var(--gray-7);
cursor: grab;
&:active {
cursor: grabbing;
}
}
}
}
&:hover,
&:focus-within {
.bar-wrapper {
opacity: 1;
}
}
.curtain {
width: min(25%, var(--space-7));
height: calc(100% - var(--space-3) - 2 * var(--space-1));
position: absolute;
top: 0;
opacity: 0;
pointer-events: none;
&[data-side="left"] {
left: 0;
background: linear-gradient(90deg, var(--gray-1), transparent);
}
&[data-side="right"] {
right: 0;
background: linear-gradient(90deg, transparent, var(--gray-1));
}
}
}
@@ -0,0 +1,140 @@
import { useEffect, useRef, type ComponentProps } from "react";
import { clamp } from "three/src/math/MathUtils.js";
import { classNames } from "../../core/ui/classNames";
import styles from "./index.module.css";
interface ScrollerProps extends ComponentProps<"div"> {}
export function Scroller({ children, className, ...props }: ScrollerProps) {
const bar = useRef<HTMLDivElement>(null!);
const container = useRef<HTMLDivElement>(null!);
const content = useRef<HTMLDivElement>(null!);
const curtainLeft = useRef<HTMLDivElement>(null!);
const curtainRight = useRef<HTMLDivElement>(null!);
useEffect(() => {
function updateBarSize() {
const visible = content.current.clientWidth;
const total = content.current.scrollWidth;
if (total <= 0) return;
bar.current.style.width = `${(visible / total) * 100}%`;
}
function syncBar() {
const maxTravel = container.current.clientWidth - bar.current.clientWidth;
const maxScroll =
content.current.scrollWidth - content.current.clientWidth;
if (maxScroll <= 0 || maxTravel <= 0) {
bar.current.style.left = "0";
return;
}
const ratio = content.current.scrollLeft / maxScroll;
bar.current.style.left = `${ratio * maxTravel}px`;
}
function syncCurtains() {
const maxScroll =
content.current.scrollWidth - content.current.clientWidth;
if (maxScroll <= 0) {
curtainLeft.current.style.opacity = "0";
curtainRight.current.style.opacity = "0";
return;
}
const fadeDistance = Math.min(60, content.current.clientWidth * 0.15);
const leftOpacity = clamp(
content.current.scrollLeft / fadeDistance,
0,
1,
);
const rightOpacity = clamp(
(maxScroll - content.current.scrollLeft) / fadeDistance,
0,
1,
);
curtainLeft.current.style.opacity = `${leftOpacity}`;
curtainRight.current.style.opacity = `${rightOpacity}`;
}
let lastX = 0;
function handlePointerDown(event: PointerEvent) {
lastX = event.clientX;
bar.current.setPointerCapture(event.pointerId);
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
}
function handlePointerMove(event: PointerEvent) {
const dx = event.clientX - lastX;
lastX = event.clientX;
const maxTravel = container.current.clientWidth - bar.current.clientWidth;
if (maxTravel <= 0) return;
const maxScroll =
content.current.scrollWidth - content.current.clientWidth;
const delta = (dx / maxTravel) * maxScroll;
content.current.scrollLeft += delta;
syncBar();
}
function handlePointerUp(event: PointerEvent) {
bar.current.releasePointerCapture(event.pointerId);
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
}
function handleScroll() {
syncBar();
syncCurtains();
}
function handleResize() {
updateBarSize();
syncBar();
syncCurtains();
}
updateBarSize();
syncBar();
syncCurtains();
bar.current.addEventListener("pointerdown", handlePointerDown);
content.current.addEventListener("scroll", syncBar);
window.addEventListener("resize", handleResize);
content.current.addEventListener("scroll", handleScroll);
return () => {
bar.current.removeEventListener("pointerdown", handlePointerDown);
content.current.removeEventListener("scroll", syncBar);
window.removeEventListener("resize", handleResize);
content.current.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<div className={classNames(styles.scroller, className)} {...props}>
<div className={styles.content} ref={content}>
{children}
</div>
<div className={styles["bar-wrapper"]}>
<div className={styles["bar-container"]} ref={container}>
<div className={styles.bar} ref={bar} />
</div>
</div>
<div ref={curtainLeft} data-side="left" className={styles.curtain} />
<div ref={curtainRight} data-side="right" className={styles.curtain} />
</div>
);
}