Compare commits

...
2 Commits
Author SHA1 Message Date
tresabhi af85fb74cb haha characteristics render! 2026-10-01 15:52:14 -05:00
tresabhi bd42cb44e4 equipment looks 2026-10-01 11:59:19 -05:00
14 changed files with 148 additions and 56 deletions

No files matched your search

@@ -21,20 +21,20 @@
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
} }
&[data-list-child="true"] { &[data-list-child="true"]:not(:first-child),
&:not(:first-child) { *:not(:first-child) > &[data-parent-list-child="true"] {
border-top-left-radius: 0; border-top-left-radius: 0;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-left-width: var(--space-0); border-left-width: var(--space-0);
} }
&:not(:last-child) { &[data-list-child="true"]:not(:last-child),
border-top-right-radius: 0; *:not(:last-child) > &[data-parent-list-child="true"] {
border-bottom-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right-width: var(--space-0); border-right-width: var(--space-0);
}
} }
svg, svg,
@@ -74,6 +74,6 @@
&[data-variant="surface"], &[data-variant="surface"],
&[data-variant="outline"] { &[data-variant="outline"] {
border: var(--space-1) solid var(--accent-6); outline: var(--space-1) solid var(--accent-6);
} }
} }
@@ -8,8 +8,10 @@ export interface ButtonProps extends ComponentProps<"button"> {
radius?: `${1 | 2 | 3}` | "max"; radius?: `${1 | 2 | 3}` | "max";
variant?: "solid" | "surface" | "soft" | "inverted" | "ghost" | "outline"; variant?: "solid" | "surface" | "soft" | "inverted" | "ghost" | "outline";
size?: "regular" | "minor"; size?: "regular" | "minor";
listChild?: boolean;
highContrast?: boolean; // TODO: add high contrast highContrast?: boolean; // TODO: add high contrast
listChild?: boolean;
parentListChild?: boolean;
} }
export const LIGHT_TEXT_COLORS = new Set<Color>([ export const LIGHT_TEXT_COLORS = new Set<Color>([
@@ -34,7 +36,9 @@ export function Button({
style, style,
radius = "max", radius = "max",
size = "regular", size = "regular",
listChild, listChild,
parentListChild,
...props ...props
}: ButtonProps) { }: ButtonProps) {
return ( return (
@@ -46,6 +50,7 @@ export function Button({
data-radius={radius} data-radius={radius}
data-size={size} data-size={size}
data-list-child={listChild} data-list-child={listChild}
data-parent-list-child={parentListChild}
{...props} {...props}
/> />
); );
@@ -2,4 +2,10 @@
padding: var(--space-3); padding: var(--space-3);
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
img,
svg {
width: 0.75em;
height: 0.75em;
}
} }
@@ -23,7 +23,7 @@ export function TankopediaCharacteristics({
<div className={styles.characteristics}> <div className={styles.characteristics}>
{characteristicsGroups.map((group) => { {characteristicsGroups.map((group) => {
const items = group.order.filter( const items = group.order.filter(
(item) => "name" in item && item.name in characteristics, (item) => "name" in item && characteristics.has(item.name),
).length; ).length;
const shouldRender = items > 0; const shouldRender = items > 0;
@@ -51,7 +51,7 @@ function Group({ group, characteristics }: GroupProps) {
const strings = useStrings(); const strings = useStrings();
const toys = group.order.filter((item) => "toy" in item).length; const toys = group.order.filter((item) => "toy" in item).length;
const items = group.order.filter( const items = group.order.filter(
(item) => "name" in item && characteristics[item.name] !== null, (item) => "name" in item && characteristics.has(item.name),
).length; ).length;
const groupSpan = `calc(${toys} * var(--group-span) + ${items} + 2)`; const groupSpan = `calc(${toys} * var(--group-span) + ${items} + 2)`;
const contentSpan = `calc(${toys} * var(--group-span) + ${items})`; const contentSpan = `calc(${toys} * var(--group-span) + ${items})`;
@@ -88,8 +88,8 @@ function Group({ group, characteristics }: GroupProps) {
{group.order.map((item) => ( {group.order.map((item) => (
<> <>
{"toy" in item && <Toy {...item} />} {"toy" in item && <Toy {...item} />}
{"name" in item && characteristics[item.name] !== null && ( {"name" in item && characteristics.get(item.name) && (
<Item config={item} value={characteristics[item.name]!} /> <Item config={item} value={characteristics.get(item.name)!} />
)} )}
</> </>
))} ))}
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import type { Consumable, EquipmentSlot } from "@blitzkit/protos"; import type { Consumable, EquipmentSlot } from "@blitzkit/protos";
import { ClockIcon, ResetIcon, TrashIcon } from "@radix-ui/react-icons"; import { ClockIcon, ResetIcon, TrashIcon } from "@radix-ui/react-icons";
import { chunk } from "lodash-es"; import { chunk } from "lodash-es";
@@ -196,7 +197,7 @@ function ConsumableEntry({ id, consumable }: ConsumableProps) {
}); });
}} }}
> >
<img src={`/media/consumables/${id}.webp`} /> <img src={alias("api", `/icons/consumables/${consumable.id}.webp`)} />
<div className={styles.info}> <div className={styles.info}>
<Text lowContrast size="minor"> <Text lowContrast size="minor">
<div className={styles.entry}> <div className={styles.entry}>
@@ -336,12 +337,12 @@ function EquipmentOption({
return ( return (
<Tooltip tooltip={unwrap(specificEquipment.name)}> <Tooltip tooltip={unwrap(specificEquipment.name)}>
<Button <IconButton
color={isSelected ? undefined : "gray"} color={isSelected ? undefined : "gray"}
variant={isSelected ? "surface" : "soft"} variant={isSelected ? "surface" : "soft"}
data-selected={isSelected} data-selected={isSelected}
radius="1" radius="1"
listChild parentListChild
className={styles.slot} className={styles.slot}
onClick={() => { onClick={() => {
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
@@ -353,8 +354,12 @@ function EquipmentOption({
}); });
}} }}
> >
<img src={`/media/equipment/${id}.webp`} /> <img
</Button> className={styles.icon}
alt={unwrap(specificEquipment.name)}
src={alias("api", `/icons/equipment/${id}.webp`)}
/>
</IconButton>
</Tooltip> </Tooltip>
); );
} }
@@ -26,12 +26,12 @@ export const characteristics = {
damage: { damage: {
type: CharacteristicType.Number, type: CharacteristicType.Number,
compute({ characteristic, gun, shell, equalizer, assault_distance }) { compute({ characteristic, gun, shell, environment }) {
let coefficient = 1; let coefficient = 1;
if (gun.assault_ranges && gun.assault_ranges.types.includes(shell.type)) { if (gun.assault_ranges && gun.assault_ranges.types.includes(shell.type)) {
const match = gun.assault_ranges.ranges.find( const match = gun.assault_ranges.ranges.find(
({ distance }) => distance >= assault_distance, ({ distance }) => distance >= environment.assault_distance,
); );
if (match !== undefined) { if (match !== undefined) {
@@ -39,7 +39,7 @@ export const characteristics = {
} }
} }
coefficient *= equalizer.damage; // coefficient *= equalizer.damage;
return coefficient * shell.armor_damage; return coefficient * shell.armor_damage;
}, },
@@ -0,0 +1,72 @@
import { api } from "../api/dynamic";
import { characteristics } from "../config/characteristics";
import { Tankopedia, type TankEnvironment } from "../stores/tankopedia";
import type { TankState } from "../tankopedia/tankState";
import type {
CharacteristicContext,
CharacteristicName,
ComputedCharacteristics,
} from "../types/characteristics";
import type { DuelSide } from "./useEquipment";
const tanks = await api.tanks();
export function useCharacteristics(side: DuelSide) {
const state = Tankopedia.use((state) => state[side]);
const environment = Tankopedia.use((state) => state.environment);
return computeCharacteristics(state, environment);
}
export function computeCharacteristics(
state: TankState,
environment: TankEnvironment,
) {
const computed: ComputedCharacteristics = new Map();
const tank = tanks.tanks[state.tank];
const engine = tank.engines.find((engine) => engine.id === state.engine)!;
const track = tank.tracks.find((track) => track.id === state.track)!;
const turret = tank.turrets.find((turret) => turret.id === state.turret)!;
const gun = turret.guns.find((gun) => gun.id === state.gun)!;
const shell = gun.shells.find((shell) => shell.id === state.shell)!;
function characteristic(name: CharacteristicName) {
if (!computed.has(name)) {
throw new Error(`Characteristic ${name} not computed yet`);
}
return computed.get(name)!;
}
const context = {
state,
environment,
tank,
engine,
track,
turret,
gun,
shell,
characteristic,
} satisfies CharacteristicContext;
for (const name in characteristics) {
const characteristic = characteristics[name as CharacteristicName];
let shouldRender = true;
if ("should_render" in characteristic) {
shouldRender = characteristic.should_render(context);
}
if (!shouldRender) continue;
const value = characteristic.compute(context);
computed.set(name as CharacteristicName, value);
}
return computed;
}
@@ -43,14 +43,14 @@ const { accent, locale } = Astro.props;
color: var(--gray-12); color: var(--gray-12);
font-family: var(--font-family-regular); font-family: var(--font-family-regular);
.app { main {
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
background-color: var(--gray-1); background-color: var(--gray-1);
padding: var(--space-4); /* padding: var(--space-4); */
gap: var(--space-6); gap: var(--space-6);
} }
} }
@@ -75,9 +75,9 @@ const { accent, locale } = Astro.props;
<body> <body>
<slot name="pre-app" /> <slot name="pre-app" />
<div class="app"> <main>
<slot /> <slot />
</div> </main>
<!-- <Footer {locale} /> --> <!-- <Footer {locale} /> -->
</body> </body>
@@ -8,8 +8,8 @@ import { defaultEqualizer } from "../../../../../config/equalizer";
import { withErrorWrapper } from "../../../../../hocs/withErrorWrapper"; import { withErrorWrapper } from "../../../../../hocs/withErrorWrapper";
import { withLocale } from "../../../../../hocs/withLocale"; import { withLocale } from "../../../../../hocs/withLocale";
import { useAwait } from "../../../../../hooks/useAwait"; import { useAwait } from "../../../../../hooks/useAwait";
import { useCharacteristics } from "../../../../../hooks/useCharacteristics";
import { Tankopedia } from "../../../../../stores/tankopedia"; import { Tankopedia } from "../../../../../stores/tankopedia";
import { computeCharacteristics } from "../../../../../tankopedia/computeCharacteristics";
import type { MaybeSkeletonComponentProps } from "../../../../../types/maybeSkeletonComponentProps"; import type { MaybeSkeletonComponentProps } from "../../../../../types/maybeSkeletonComponentProps";
import styles from "./_index.module.css"; import styles from "./_index.module.css";
@@ -26,14 +26,10 @@ export const Page = withErrorWrapper(
Tankopedia.useInitialization(protagonistTank); Tankopedia.useInitialization(protagonistTank);
const protagonist = Tankopedia.use((state) => state.protagonist); const protagonist = Tankopedia.use((state) => state.protagonist);
// const protagonistEquipment = useEquipment("protagonist", protagonistTank.tank!); const equalize = Tankopedia.use((state) => state.environment.equalize);
const characteristics = useMemo( const characteristics = useCharacteristics("protagonist");
() => computeCharacteristics(),
[protagonist],
);
const equalize = Tankopedia.use((state) => state.equalize);
const thicknessRange = useMemo(() => { const thicknessRange = useMemo(() => {
const entries = Object.values(tanks.tanks); const entries = Object.values(tanks.tanks);
const filtered = entries.filter( const filtered = entries.filter(
+7 -3
View File
@@ -75,14 +75,18 @@ export enum TankopediaRelativeAgainst {
All, All,
} }
export interface TankEnvironment {
equalize: boolean;
assault_distance: number;
}
interface Tankopedia { interface Tankopedia {
disturbed: boolean; disturbed: boolean;
revealed: boolean; revealed: boolean;
protagonist: TankState; protagonist: TankState;
antagonist: TankState; antagonist: TankState;
environment: TankEnvironment;
equalize: boolean;
shot?: Shot; shot?: Shot;
skills: Record<string, number>; skills: Record<string, number>;
@@ -121,7 +125,7 @@ export const Tankopedia = new Soapstone<Tankopedia, [TankDefinition]>(
antagonist: createTankState(tank), antagonist: createTankState(tank),
model: models.models[tank.id], model: models.models[tank.id],
equalize: false, environment: { equalize: false, assault_distance: 0 },
relativeAgainst: TankopediaRelativeAgainst.Class, relativeAgainst: TankopediaRelativeAgainst.Class,
editStatic: false, editStatic: false,
@@ -1,7 +0,0 @@
import type { ComputedCharacteristics } from "../types/characteristics";
export function computeCharacteristics() {
const characteristics: Partial<ComputedCharacteristics> = {};
return characteristics as ComputedCharacteristics;
}
@@ -27,7 +27,10 @@ export function createTankState(tank: TankDefinition) {
shell: gun.shells[0].id, shell: gun.shells[0].id,
track: tank.tracks.at(-1)!.id, track: tank.tracks.at(-1)!.id,
assault_distance: (gun.assault_ranges?.ranges[0].distance ?? 0) / 2, environment: {
assault_distance: (gun.assault_ranges?.ranges[0].distance ?? 0) / 2,
},
speed: 0, speed: 0,
equipment: {}, equipment: {},
@@ -9,7 +9,6 @@ export interface TankState {
shell: number; shell: number;
track: number; track: number;
assault_distance: number;
speed: number; speed: number;
equipment: Record<number, number>; equipment: Record<number, number>;
+17 -8
View File
@@ -1,11 +1,16 @@
import type { Strings } from "@blitzkit/i18n"; import type { Strings } from "@blitzkit/i18n";
import type { import type {
Equalizer, EngineDefinition,
GunDefinition, GunDefinition,
ShellDefinition, ShellDefinition,
TankDefinition,
TrackDefinition,
TurretDefinition,
} from "@blitzkit/protos"; } from "@blitzkit/protos";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { characteristics } from "../config/characteristics"; import { characteristics } from "../config/characteristics";
import type { TankEnvironment } from "../stores/tankopedia";
import type { TankState } from "../tankopedia/tankState";
export type Characteristic = { export type Characteristic = {
should_render?(context: CharacteristicContext): boolean; should_render?(context: CharacteristicContext): boolean;
@@ -20,14 +25,17 @@ export type Characteristic = {
} }
); );
interface CharacteristicContext { export interface CharacteristicContext {
state: TankState;
environment: TankEnvironment;
tank: TankDefinition;
engine: EngineDefinition;
track: TrackDefinition;
turret: TurretDefinition;
gun: GunDefinition; gun: GunDefinition;
shell: ShellDefinition; shell: ShellDefinition;
equalizer: Equalizer;
assault_distance: number;
characteristic( characteristic(
name: CharacteristicName, name: CharacteristicName,
): ReturnType<Characteristic["compute"]>; ): ReturnType<Characteristic["compute"]>;
@@ -41,8 +49,9 @@ export enum CharacteristicType {
export type CharacteristicName = keyof typeof characteristics; export type CharacteristicName = keyof typeof characteristics;
export type CharacteristicReturnType = ReturnType<Characteristic["compute"]>; export type CharacteristicReturnType = ReturnType<Characteristic["compute"]>;
export type ComputedCharacteristics = Partial< export type ComputedCharacteristics = Map<
Record<CharacteristicName, CharacteristicReturnType> CharacteristicName,
CharacteristicReturnType
>; >;
export interface CharacteristicRenderConfig { export interface CharacteristicRenderConfig {