Merge branch 'main' into compare-switch-all-shells

This commit is contained in:
TrèsAbhi authored and GitHub committed 2026-10-09 17:39:15 -05:00
commit b45f281cb9
14 files changed
+134 -25

No files matched your search

@@ -434,6 +434,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
resistance_hard: terrainResistances[0], resistance_hard: terrainResistances[0],
resistance_medium: terrainResistances[1], resistance_medium: terrainResistances[1],
resistance_soft: terrainResistances[2], resistance_soft: terrainResistances[2],
repair_time: track.hysteresisHealth / track.healthRegenPerSec,
tier: track.level, tier: track.level,
unlocks: resolveUnlocks( unlocks: resolveUnlocks(
this.blitzModuleTypeToBlitzkit, this.blitzModuleTypeToBlitzkit,
@@ -87,6 +87,7 @@ message TrackDefinition {
required float resistance_medium = 10; required float resistance_medium = 10;
required float resistance_soft = 11; required float resistance_soft = 11;
repeated Unlock unlocks = 12; repeated Unlock unlocks = 12;
required float repair_time = 13;
} }
message EngineDefinition { message EngineDefinition {
@@ -338,6 +338,7 @@ export interface TrackDefinition {
resistance_medium: number; resistance_medium: number;
resistance_soft: number; resistance_soft: number;
unlocks: Unlock[]; unlocks: Unlock[];
repair_time: number;
} }
export interface EngineDefinition { export interface EngineDefinition {
@@ -1759,6 +1760,7 @@ function createBaseTrackDefinition(): TrackDefinition {
resistance_medium: 0, resistance_medium: 0,
resistance_soft: 0, resistance_soft: 0,
unlocks: [], unlocks: [],
repair_time: 0,
}; };
} }
@@ -1800,6 +1802,9 @@ export const TrackDefinition: MessageFns<TrackDefinition> = {
for (const v of message.unlocks) { for (const v of message.unlocks) {
Unlock.encode(v!, writer.uint32(98).fork()).join(); Unlock.encode(v!, writer.uint32(98).fork()).join();
} }
if (message.repair_time !== 0) {
writer.uint32(109).float(message.repair_time);
}
return writer; return writer;
}, },
@@ -1906,6 +1911,14 @@ export const TrackDefinition: MessageFns<TrackDefinition> = {
message.unlocks.push(Unlock.decode(reader, reader.uint32())); message.unlocks.push(Unlock.decode(reader, reader.uint32()));
continue; continue;
} }
case 13: {
if (tag !== 109) {
break;
}
message.repair_time = reader.float();
continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
@@ -1929,6 +1942,7 @@ export const TrackDefinition: MessageFns<TrackDefinition> = {
resistance_medium: isSet(object.resistance_medium) ? globalThis.Number(object.resistance_medium) : 0, resistance_medium: isSet(object.resistance_medium) ? globalThis.Number(object.resistance_medium) : 0,
resistance_soft: isSet(object.resistance_soft) ? globalThis.Number(object.resistance_soft) : 0, resistance_soft: isSet(object.resistance_soft) ? globalThis.Number(object.resistance_soft) : 0,
unlocks: globalThis.Array.isArray(object?.unlocks) ? object.unlocks.map((e: any) => Unlock.fromJSON(e)) : [], unlocks: globalThis.Array.isArray(object?.unlocks) ? object.unlocks.map((e: any) => Unlock.fromJSON(e)) : [],
repair_time: isSet(object.repair_time) ? globalThis.Number(object.repair_time) : 0,
}; };
}, },
@@ -1970,6 +1984,9 @@ export const TrackDefinition: MessageFns<TrackDefinition> = {
if (message.unlocks?.length) { if (message.unlocks?.length) {
obj.unlocks = message.unlocks.map((e) => Unlock.toJSON(e)); obj.unlocks = message.unlocks.map((e) => Unlock.toJSON(e));
} }
if (message.repair_time !== 0) {
obj.repair_time = message.repair_time;
}
return obj; return obj;
}, },
@@ -1994,6 +2011,7 @@ export const TrackDefinition: MessageFns<TrackDefinition> = {
message.resistance_medium = object.resistance_medium ?? 0; message.resistance_medium = object.resistance_medium ?? 0;
message.resistance_soft = object.resistance_soft ?? 0; message.resistance_soft = object.resistance_soft ?? 0;
message.unlocks = object.unlocks?.map((e) => Unlock.fromPartial(e)) || []; message.unlocks = object.unlocks?.map((e) => Unlock.fromPartial(e)) || [];
message.repair_time = object.repair_time ?? 0;
return message; return message;
}, },
}; };
@@ -34,6 +34,8 @@ export interface VehicleDefinitions {
}; };
level: number; level: number;
hullPosition: string; hullPosition: string;
hysteresisHealth: number;
healthRegenPerSec: number;
armor: { armor: {
leftTrack: leftTrack:
| number | number
+1
View File
@@ -683,6 +683,7 @@
"hullTraverseSoftTerrain": "Soft", "hullTraverseSoftTerrain": "Soft",
"health": "Health", "health": "Health",
"fireChance": "Fire chance", "fireChance": "Fire chance",
"trackRepairTime": "Track repair time",
"viewRange": "View range", "viewRange": "View range",
"camouflageStill": "Camouflage still", "camouflageStill": "Camouflage still",
"camouflageMoving": "Moving", "camouflageMoving": "Moving",
@@ -128,19 +128,30 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
? shotStatusColors[shot.out.status] ? shotStatusColors[shot.out.status]
: undefined; : undefined;
const inTitleColor = shotStatusColors[shot.in.status]; const inTitleColor = shotStatusColors[shot.in.status];
const hasOutLayers = shot.out !== undefined && shot.out.layers.length > 0;
const penetrationChance = shot.penetrationChance !== undefined && (
<Text color="gray" weight="bold">
{literals(strings.common.units.percentage, {
value: (shot.penetrationChance * 100).toFixed(0),
})}
</Text>
);
return ( return (
<Card variant="classic" {...props}> <Card variant="classic" {...props}>
<Flex direction="column" gap="2"> <Flex direction="column" gap="2">
<Flex direction="column" gap="1"> <Flex direction="column" gap="1">
<Text color={inTitleColor} weight="bold"> <Flex justify="between" align="center" gap="4">
{literals( <Text color={inTitleColor} weight="bold">
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[ {literals(
shot.in.status strings.website.tools.tankopedia.sandbox.dynamic.shot_card
], .status[shot.in.status],
{ damage: Math.round(shot.damage).toLocaleString(locale) } { damage: Math.round(shot.damage).toLocaleString(locale) }
)} )}
</Text> </Text>
{!hasOutLayers && penetrationChance}
</Flex>
<table <table
style={{ style={{
@@ -204,13 +215,17 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
</Flex> </Flex>
</Inset> </Inset>
<Text color={outTitleColor} weight="bold"> <Flex justify="between" align="center" gap="4">
{literals( <Text color={outTitleColor} weight="bold">
strings.website.tools.tankopedia.sandbox.dynamic.shot_card {literals(
.status[shot.out.status], strings.website.tools.tankopedia.sandbox.dynamic.shot_card
{ damage: Math.round(shot.damage).toLocaleString(locale) } .status[shot.out.status],
)} { damage: Math.round(shot.damage).toLocaleString(locale) }
</Text> )}
</Text>
{penetrationChance}
</Flex>
<Flex direction="column"> <Flex direction="column">
{shot.out.layers.map((layer, index) => { {shot.out.layers.map((layer, index) => {
@@ -63,6 +63,36 @@ export type ArmorUserData = {
} }
); );
const PENETRATION_RANDOMIZATION = 0.05;
function resolvePenetrationChance(shoot: (scale: number) => Shot | null) {
function penetrates(scale: number) {
const shot = shoot(scale);
return (shot?.out ?? shot?.in)?.status === "penetration";
}
let low = 1 - PENETRATION_RANDOMIZATION;
let high = 1 + PENETRATION_RANDOMIZATION;
if (penetrates(low)) return 1;
if (!penetrates(high)) return 0;
for (let iteration = 0; iteration < 16; iteration++) {
const middle = (low + high) / 2;
if (penetrates(middle)) {
high = middle;
} else {
low = middle;
}
}
return (
(1 + PENETRATION_RANDOMIZATION - high) / (2 * PENETRATION_RANDOMIZATION)
);
}
const omitMaterial = new MeshBasicMaterial({ const omitMaterial = new MeshBasicMaterial({
colorWrite: false, colorWrite: false,
depthWrite: true, depthWrite: true,
@@ -89,6 +119,7 @@ export function SpacedArmorSceneComponent({
intersections: Intersection[], intersections: Intersection[],
allowRicochet: boolean, allowRicochet: boolean,
remainingPenetrationInput?: number, remainingPenetrationInput?: number,
penetrationScale = 1,
) => { ) => {
const { customShell } = Tankopedia.state; const { customShell } = Tankopedia.state;
const shell = customShell ?? Duel.state.antagonist.shell; const shell = customShell ?? Duel.state.antagonist.shell;
@@ -125,6 +156,7 @@ export function SpacedArmorSceneComponent({
Duel.state.protagonist.equipmentMatrix, Duel.state.protagonist.equipmentMatrix,
); );
const penetration = const penetration =
penetrationScale *
shell.penetration!.near * shell.penetration!.near *
resolvePenetrationCoefficient( resolvePenetrationCoefficient(
hasCalibratedShells, hasCalibratedShells,
@@ -367,6 +399,7 @@ export function SpacedArmorSceneComponent({
ricochetIntersections, ricochetIntersections,
false, false,
remainingPenetration, remainingPenetration,
penetrationScale,
); );
shot.in.status = "ricochet"; shot.in.status = "ricochet";
@@ -422,6 +455,16 @@ export function SpacedArmorSceneComponent({
const shot = shoot(event.point, event.intersections, true)!; const shot = shoot(event.point, event.intersections, true)!;
shot.penetrationChance = resolvePenetrationChance((scale) =>
shoot(
event.point,
event.intersections,
true,
undefined,
scale,
),
);
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.shot = shot; draft.shot = shot;
}); });
@@ -25,6 +25,7 @@ import { FilterControl } from "./TankSearch/components/FilterControl";
const tankDefinitions = await api.tankDefinitions(); const tankDefinitions = await api.tankDefinitions();
const POOL_HEIGHT = "16rem"; const POOL_HEIGHT = "16rem";
const FILTERS_WIDTH = "32rem";
export function PlaylistGenerator({ skeleton }: MaybeSkeletonComponentProps) { export function PlaylistGenerator({ skeleton }: MaybeSkeletonComponentProps) {
const [largeWarningOpen, setLargeWarningOpen] = useState(false); const [largeWarningOpen, setLargeWarningOpen] = useState(false);
@@ -80,13 +81,19 @@ export function PlaylistGenerator({ skeleton }: MaybeSkeletonComponentProps) {
<Flex justify="center" width="100%" align="center" px="4"> <Flex justify="center" width="100%" align="center" px="4">
<Flex <Flex
flexGrow="1" flexGrow="1"
gap="9" gap="6"
align="center" align="center"
justify="center" justify="center"
maxWidth="80rem" maxWidth="80rem"
direction={{ initial: "column-reverse", md: "row" }} direction={{ initial: "column-reverse", md: "row" }}
> >
<Flex direction="column" gap="4" align="center"> <Flex
direction="column"
gap="4"
align="center"
flexShrink="0"
width={{ initial: "100%", md: FILTERS_WIDTH }}
>
<FilterControl /> <FilterControl />
<Button <Button
@@ -24,6 +24,14 @@ export function Survivability({ stats }: StatsAcceptorProps) {
decimals={0} decimals={0}
value={(stats) => stats.fireChance * 100} value={(stats) => stats.fireChance * 100}
/> />
{stats.trackRepairTime !== undefined && (
<InfoWithDelta
value="trackRepairTime"
stats={stats}
deltaType="lowerIsBetter"
decimals={2}
/>
)}
<InfoWithDelta value="viewRange" stats={stats} decimals={0} /> <InfoWithDelta value="viewRange" stats={stats} decimals={0} />
<ViewRangeVisualizer stats={stats} /> <ViewRangeVisualizer stats={stats} />
<Info <Info
@@ -31,11 +31,11 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
return ( return (
<Flex <Flex
position="absolute" position="absolute"
right={display === TankopediaDisplay.StaticArmor ? "0" : "-6rem"} right="0"
top="50%" top="50%"
mr="4" mr="4"
style={{ style={{
transform: "translateY(-50%)", transform: `translate(${display === TankopediaDisplay.StaticArmor ? "0" : "calc(100% + var(--space-4))"}, -50%)`,
userSelect: "none", userSelect: "none",
transitionDuration: "200ms", transitionDuration: "200ms",
}} }}
@@ -119,6 +119,7 @@ export function tankCharacteristics(
const hasImprovedVerticalStabilizer = equipment(122); const hasImprovedVerticalStabilizer = equipment(122);
const hasDownImprovedVerticalStabilizer = equipment(124); const hasDownImprovedVerticalStabilizer = equipment(124);
const hasImprovedSuspension = equipment(123); const hasImprovedSuspension = equipment(123);
const hasToolbox = equipment(113);
const hasTungsten = consumable(45); const hasTungsten = consumable(45);
const hasAdrenaline = consumable(18); const hasAdrenaline = consumable(18);
@@ -518,6 +519,12 @@ export function tankCharacteristics(
} }
const fireChance = engine.fire_chance * fireChanceCoefficient; const fireChance = engine.fire_chance * fireChanceCoefficient;
const trackRepairTime = track.repair_time
? track.repair_time /
((hasToolbox ? 1.2 : 1) *
(1 + (crewSkills.repair ?? 0) * 0.04) *
(hasProtectiveKit ? 1.1 : 1))
: undefined;
const viewRange = turret.view_range * viewRangeCoefficient; const viewRange = turret.view_range * viewRangeCoefficient;
const camouflageStill = tank.camouflage_still * camouflageCoefficientStill; const camouflageStill = tank.camouflage_still * camouflageCoefficientStill;
const camouflageMoving = const camouflageMoving =
@@ -602,6 +609,7 @@ export function tankCharacteristics(
hullTraverseSoftTerrain, hullTraverseSoftTerrain,
health, health,
fireChance, fireChance,
trackRepairTime,
viewRange, viewRange,
camouflageStill, camouflageStill,
camouflageMoving, camouflageMoving,
@@ -1,3 +1,4 @@
import sharp from "sharp";
import type { APIContext, GetStaticPathsItem } from "astro"; import type { APIContext, GetStaticPathsItem } from "astro";
import { mixStaticPaths } from "../../../../core/blitzkit/mixStaticPaths"; import { mixStaticPaths } from "../../../../core/blitzkit/mixStaticPaths";
import { vfs } from "../../../../core/blitzkit/vfs"; import { vfs } from "../../../../core/blitzkit/vfs";
@@ -29,7 +30,10 @@ export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
}); });
export async function GET({ props }: APIContext<{ path: string }>) { export async function GET({ props }: APIContext<{ path: string }>) {
const bytes = new Uint8Array(await vfs.file(props.path)); const bytes = await sharp(new Uint8Array(await vfs.file(props.path)))
.trim({ background: { r: 0, g: 0, b: 0, alpha: 0 }, threshold: 1 })
.webp()
.toBuffer();
return new Response(bytes); return new Response(new Uint8Array(bytes));
} }
@@ -94,7 +94,7 @@ function ModuleChooser({
<Dialog.Trigger> <Dialog.Trigger>
<Button <Button
variant="surface" variant="surface"
size={{ initial: "3", md: "4" }} size={{ initial: "2", xs: "3", md: "4" }}
style={{ style={{
overflow: "clip", overflow: "clip",
borderTopRightRadius: subSelector ? 0 : undefined, borderTopRightRadius: subSelector ? 0 : undefined,
@@ -137,7 +137,7 @@ function ModuleChooser({
<Popover.Trigger> <Popover.Trigger>
<IconButton <IconButton
variant="surface" variant="surface"
size={{ initial: "3", md: "4" }} size={{ initial: "2", xs: "3", md: "4" }}
style={{ style={{
overflow: "clip", overflow: "clip",
borderTopLeftRadius: 0, borderTopLeftRadius: 0,
@@ -204,7 +204,7 @@ function ModuleChooser({
function ModuleSeparator() { function ModuleSeparator() {
return ( return (
<Text size="5" color="gray" trim="end"> <Text size={{ initial: "3", xs: "5" }} color="gray" trim="end">
<CaretRightIcon width="1em" height="1em" /> <CaretRightIcon width="1em" height="1em" />
</Text> </Text>
); );
@@ -260,7 +260,7 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
</Button> </Button>
</Flex> </Flex>
<Flex gap="2" align="center"> <Flex gap={{ initial: "1", xs: "2" }} align="center">
<ModuleChooser <ModuleChooser
skeleton={skeleton} skeleton={skeleton}
tank={hullTankId.id} tank={hullTankId.id}
@@ -43,6 +43,7 @@ export type Shot = {
containsGaps: boolean; containsGaps: boolean;
damage: number; damage: number;
splashRadius?: number; splashRadius?: number;
penetrationChance?: number;
in: { in: {
surfaceNormal: Vector3; surfaceNormal: Vector3;