Compare commits

...
2 Commits
Author SHA1 Message Date
tresabhi 885c260605 add consumable buttons 2026-05-28 12:55:11 -05:00
tresabhi f8a71feb46 use tag system for equipment 2026-05-28 12:34:14 -05:00
11 changed files with 130 additions and 75 deletions

No files matched your search

+3 -1
View File
@@ -4,6 +4,8 @@
"amogos",
"Tankopedia",
"uasset",
"oodle"
"oodle",
"blitzkit",
"webp"
]
}
+2 -35
View File
@@ -130,43 +130,10 @@ public partial class GameInterface
return provider.Image(icon);
}
private static readonly string equipmentPDAPrefix =
"Blitz/Content/TanksStuff/Equipment/PDA_Equipment";
private static readonly string equipmentPDASuffix = ".uasset";
public byte[] EquipmentIcon(string[] names)
public byte[] EquipmentIcon(string tag)
{
List<string> candidates = [];
foreach (var name in names)
{
candidates.AddRange([
$"{equipmentPDAPrefix}{name}{equipmentPDASuffix}",
$"{equipmentPDAPrefix}_{name}{equipmentPDASuffix}",
]);
}
string? candidate = null;
foreach (var c in candidates)
{
if (!files.Contains(c))
{
continue;
}
candidate = c;
break;
}
if (candidate is null)
{
Console.WriteLine($"No equipment pda found for {string.Join(", ", names)}");
return [];
}
var pda = provider.PDA(candidate);
var pda = provider.Discovered<UPrimaryDataAsset>(tag);
var icon = pda.Get<FSoftObjectPath>("Icon");
return provider.Image(icon);
}
}
+1 -5
View File
@@ -198,12 +198,8 @@
},
"tanks": {
"characteristics": {
"title": "Characteristics"
},
"loadout": {
"title": "Loadout",
"consumables": "Consumables",
"modules": "Modules",
"equipment": "Equipment",
"alternative": "Alt"
@@ -13,6 +13,36 @@
}
}
.consumables {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--space-3);
.consumable {
font-size: var(--space-5);
img {
width: 1em;
height: 1em;
}
.info {
display: flex;
flex-direction: column;
.entry {
display: flex;
gap: var(--space-2);
svg {
width: 1em;
height: 1em;
}
}
}
}
}
.equipment {
display: flex;
flex-direction: column;
@@ -5,9 +5,11 @@ import { StandardPrice } from "@protos/blitz_static_standard_price";
import type { UpgradeLine } from "@protos/blitz_static_tank_upgrade_line";
import type { StageParameters } from "@protos/blitz_static_tank_upgrade_single_stage";
import {
ClockIcon,
CornerBottomLeftIcon,
DoubleArrowLeftIcon,
DoubleArrowRightIcon,
ResetIcon,
TrashIcon,
} from "@radix-ui/react-icons";
import { chunk } from "lodash-es";
@@ -26,7 +28,7 @@ import { useProtagonist } from "../../hooks/useProtagonist";
import { useStrings } from "../../hooks/useStrings";
import { useTierPrices } from "../../hooks/useTierPrices";
import { useUpgradePreset } from "../../hooks/useUpgradePreset";
import { Tankopedia } from "../../stores/tankopedia";
import { Tankopedia, TankopediaCompare } from "../../stores/tankopedia";
import { Button } from "../Button";
import { Heading } from "../Heading";
import { IconButton } from "../IconButton";
@@ -38,13 +40,15 @@ import type { ComputedCharacteristics } from "../../tankopedia/computeCharacteri
import { Tooltip } from "../Tooltip";
import { useGameStrings } from "../../hooks/useGameStrings";
import { TankopediaCollapsible } from "../TankopediaCollapsible";
import type { ConsumableComponent } from "@protos/blitz_static_consumable_component";
import { Tank } from "@protos/tank";
import { select } from "three/src/nodes/math/ConditionalNode.js";
interface TankopediaLoadoutProps {
characteristics: ComputedCharacteristics;
}
export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) {
const strings = useStrings();
const tank = useProtagonist();
const showModules = tank.tank!.upgrade_lines.some(
@@ -53,15 +57,9 @@ export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) {
return (
<>
<TankopediaCollapsible title={strings.tanks.loadout.title}>
{showModules && <Modules />}
<Equipment />
<Consumables characteristics={characteristics} />
</TankopediaCollapsible>
<TankopediaCollapsible title={strings.tanks.characteristics.title}>
lmao
</TankopediaCollapsible>
{showModules && <Modules />}
<Equipment />
<Consumables characteristics={characteristics} />
</>
);
}
@@ -71,21 +69,88 @@ interface ConsumablesProps {
}
function Consumables({ characteristics }: ConsumablesProps) {
const strings = useStrings();
const consumables = useConsumables();
const tierPrices = useTierPrices();
const tank = useProtagonist();
const isCompatible = useCompatibility(tank, characteristics);
return Object.entries(consumables.consumables).map(
([id, { compatibility, consumable, purchase }]) => {
if (!isCompatible(compatibility!)) return null;
return (
<div className={styles.section}>
<div className={styles.header}>
<Heading size="4">{strings.tanks.loadout.consumables}</Heading>
return (
<span>
{id} {JSON.stringify(consumable, null, 2)}
</span>
);
},
<IconButton
color="red"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.protagonist.consumables = [];
});
}}
>
<TrashIcon />
</IconButton>
</div>
<div className={styles.consumables}>
{Object.entries(consumables.consumables).map(
([id, { compatibility, consumable, purchase }]) => {
if (!isCompatible(compatibility!)) return null;
return <Consumable id={id} consumable={consumable!} />;
},
)}
</div>
</div>
);
}
interface ConsumableProps {
id: string;
consumable: ConsumableComponent;
}
function Consumable({ id, consumable }: ConsumableProps) {
const gameStrings = useGameStrings("ConsumableEntity");
const isSelected = Tankopedia.use((state) =>
state.protagonist.consumables.includes(id),
);
return (
<Tooltip tooltip={gameStrings[consumable!.name_key]}>
<Button
radius="1"
variant={isSelected ? "surface" : "soft"}
color={isSelected ? undefined : "gray"}
className={styles.consumable}
onClick={() => {
Tankopedia.mutate((draft) => {
const index = draft.protagonist.consumables.indexOf(id);
if (index === -1) {
draft.protagonist.consumables.push(id);
} else {
draft.protagonist.consumables.splice(index, 1);
}
});
}}
>
<img src={`/media/consumables/${id}.webp`} />
<div className={styles.info}>
<Text lowContrast size="minor">
<div className={styles.entry}>
<ClockIcon />
10s
</div>
</Text>
<Text lowContrast size="minor">
<div className={styles.entry}>
<ResetIcon />
10s
</div>
</Text>
</div>
</Button>
</Tooltip>
);
}
@@ -20,9 +20,7 @@
white-space: nowrap;
}
&:hover,
&:active,
&:focus-within {
&:hover {
.text {
display: block;
}
@@ -12,7 +12,7 @@
display: flex;
flex-direction: column;
gap: var(--space-6);
gap: var(--space-5);
padding: var(--space-5);
overflow-y: auto;
@@ -4,7 +4,7 @@ import { notFoundResponse } from "../../../api/responses";
import { mixStaticPaths } from "../../../astro/mixStaticPaths";
import { game } from "../../../game/game";
import { getStaticPaths as _getStaticPaths } from "../_index";
import { assetsDiscovery } from "../../../game/assetsDiscovery";
import { assertDiscoveryTag } from "../../../game/assertDiscoveryTag";
export const getStaticPaths = mixStaticPaths(_getStaticPaths, () => {
return api.metadata
@@ -18,13 +18,9 @@ const HACK_nameMap: Record<string, string> = {
export function GET({ params }: APIContext<never, { id: string }>) {
const item = api.metadata.item(params.id);
const names = assetsDiscovery(item, HACK_nameMap);
const icon = game!.equipmentIcon(names);
if (icon.length === 0) {
return notFoundResponse;
}
const assetsDiscovery = item.BlitzStaticAssetsDiscovery();
const tag = assertDiscoveryTag(assetsDiscovery);
const icon = game!.equipmentIcon(tag);
const array = new Uint8Array(icon);
return new Response(array);
@@ -16,6 +16,5 @@ export enum TankopediaCompare {
export const Tankopedia = new Varuna<Tankopedia, Tank>((tank) => ({
compare: TankopediaCompare.TierAndClass,
protagonist: createTankState(tank),
}));
@@ -23,6 +23,7 @@ export function createTankState({ tank, id }: Tank) {
id,
equipment: {},
consumables: [],
upgrades,
alternates,
@@ -8,6 +8,7 @@ export interface TankState {
id: string;
equipment: Record<number, number>;
consumables: string[];
upgrades: Record<string, number>;
alternates: Record<string, boolean>;