mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-04 00:10:38 +01:00
Merge branch 'main' into compare-switch-all-shells
This commit is contained in:
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+8
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user