diff --git a/packages/core/src/blitzkit/api/server/1_tankDefinitions.ts b/packages/core/src/blitzkit/api/server/1_tankDefinitions.ts index e7192e140..9e211001b 100644 --- a/packages/core/src/blitzkit/api/server/1_tankDefinitions.ts +++ b/packages/core/src/blitzkit/api/server/1_tankDefinitions.ts @@ -434,6 +434,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 { resistance_hard: terrainResistances[0], resistance_medium: terrainResistances[1], resistance_soft: terrainResistances[2], + repair_time: track.hysteresisHealth / track.healthRegenPerSec, tier: track.level, unlocks: resolveUnlocks( this.blitzModuleTypeToBlitzkit, diff --git a/packages/core/src/protos/tank_definitions.proto b/packages/core/src/protos/tank_definitions.proto index 6c9cb1078..9a2713bff 100644 --- a/packages/core/src/protos/tank_definitions.proto +++ b/packages/core/src/protos/tank_definitions.proto @@ -87,6 +87,7 @@ message TrackDefinition { required float resistance_medium = 10; required float resistance_soft = 11; repeated Unlock unlocks = 12; + required float repair_time = 13; } message EngineDefinition { diff --git a/packages/core/src/protos/tank_definitions.ts b/packages/core/src/protos/tank_definitions.ts index be6e2a1f0..52117f6d9 100644 --- a/packages/core/src/protos/tank_definitions.ts +++ b/packages/core/src/protos/tank_definitions.ts @@ -338,6 +338,7 @@ export interface TrackDefinition { resistance_medium: number; resistance_soft: number; unlocks: Unlock[]; + repair_time: number; } export interface EngineDefinition { @@ -1759,6 +1760,7 @@ function createBaseTrackDefinition(): TrackDefinition { resistance_medium: 0, resistance_soft: 0, unlocks: [], + repair_time: 0, }; } @@ -1800,6 +1802,9 @@ export const TrackDefinition: MessageFns = { for (const v of message.unlocks) { Unlock.encode(v!, writer.uint32(98).fork()).join(); } + if (message.repair_time !== 0) { + writer.uint32(109).float(message.repair_time); + } return writer; }, @@ -1906,6 +1911,14 @@ export const TrackDefinition: MessageFns = { message.unlocks.push(Unlock.decode(reader, reader.uint32())); continue; } + case 13: { + if (tag !== 109) { + break; + } + + message.repair_time = reader.float(); + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; @@ -1929,6 +1942,7 @@ export const TrackDefinition: MessageFns = { resistance_medium: isSet(object.resistance_medium) ? globalThis.Number(object.resistance_medium) : 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)) : [], + repair_time: isSet(object.repair_time) ? globalThis.Number(object.repair_time) : 0, }; }, @@ -1970,6 +1984,9 @@ export const TrackDefinition: MessageFns = { if (message.unlocks?.length) { obj.unlocks = message.unlocks.map((e) => Unlock.toJSON(e)); } + if (message.repair_time !== 0) { + obj.repair_time = message.repair_time; + } return obj; }, @@ -1994,6 +2011,7 @@ export const TrackDefinition: MessageFns = { message.resistance_medium = object.resistance_medium ?? 0; message.resistance_soft = object.resistance_soft ?? 0; message.unlocks = object.unlocks?.map((e) => Unlock.fromPartial(e)) || []; + message.repair_time = object.repair_time ?? 0; return message; }, }; diff --git a/packages/core/src/types/vehicleDefinitions.ts b/packages/core/src/types/vehicleDefinitions.ts index e0ff86532..e497c6979 100644 --- a/packages/core/src/types/vehicleDefinitions.ts +++ b/packages/core/src/types/vehicleDefinitions.ts @@ -34,6 +34,8 @@ export interface VehicleDefinitions { }; level: number; hullPosition: string; + hysteresisHealth: number; + healthRegenPerSec: number; armor: { leftTrack: | number diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index 0377c9e62..9a5c5dea1 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -683,6 +683,7 @@ "hullTraverseSoftTerrain": "Soft", "health": "Health", "fireChance": "Fire chance", + "trackRepairTime": "Track repair time", "viewRange": "View range", "camouflageStill": "Camouflage still", "camouflageMoving": "Moving", diff --git a/packages/website/src/components/Armor/components/ShotDisplayCard.tsx b/packages/website/src/components/Armor/components/ShotDisplayCard.tsx index e27c1de5c..6720458f3 100644 --- a/packages/website/src/components/Armor/components/ShotDisplayCard.tsx +++ b/packages/website/src/components/Armor/components/ShotDisplayCard.tsx @@ -128,19 +128,30 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) { ? shotStatusColors[shot.out.status] : undefined; const inTitleColor = shotStatusColors[shot.in.status]; + const hasOutLayers = shot.out !== undefined && shot.out.layers.length > 0; + const penetrationChance = shot.penetrationChance !== undefined && ( + + {literals(strings.common.units.percentage, { + value: (shot.penetrationChance * 100).toFixed(0), + })} + + ); return ( - - {literals( - strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[ - shot.in.status - ], - { damage: Math.round(shot.damage).toLocaleString(locale) } - )} - + + + {literals( + strings.website.tools.tankopedia.sandbox.dynamic.shot_card + .status[shot.in.status], + { damage: Math.round(shot.damage).toLocaleString(locale) } + )} + + + {!hasOutLayers && penetrationChance} + - - {literals( - strings.website.tools.tankopedia.sandbox.dynamic.shot_card - .status[shot.out.status], - { damage: Math.round(shot.damage).toLocaleString(locale) } - )} - + + + {literals( + strings.website.tools.tankopedia.sandbox.dynamic.shot_card + .status[shot.out.status], + { damage: Math.round(shot.damage).toLocaleString(locale) } + )} + + + {penetrationChance} + {shot.out.layers.map((layer, index) => { diff --git a/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx b/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx index 784836464..57149cbe4 100644 --- a/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx +++ b/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx @@ -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({ colorWrite: false, depthWrite: true, @@ -89,6 +119,7 @@ export function SpacedArmorSceneComponent({ intersections: Intersection[], allowRicochet: boolean, remainingPenetrationInput?: number, + penetrationScale = 1, ) => { const { customShell } = Tankopedia.state; const shell = customShell ?? Duel.state.antagonist.shell; @@ -125,6 +156,7 @@ export function SpacedArmorSceneComponent({ Duel.state.protagonist.equipmentMatrix, ); const penetration = + penetrationScale * shell.penetration!.near * resolvePenetrationCoefficient( hasCalibratedShells, @@ -367,6 +399,7 @@ export function SpacedArmorSceneComponent({ ricochetIntersections, false, remainingPenetration, + penetrationScale, ); shot.in.status = "ricochet"; @@ -422,6 +455,16 @@ export function SpacedArmorSceneComponent({ const shot = shoot(event.point, event.intersections, true)!; + shot.penetrationChance = resolvePenetrationChance((scale) => + shoot( + event.point, + event.intersections, + true, + undefined, + scale, + ), + ); + Tankopedia.mutate((draft) => { draft.shot = shot; }); diff --git a/packages/website/src/components/PlaylistGenerator.tsx b/packages/website/src/components/PlaylistGenerator.tsx index c533495ca..a64999bc4 100644 --- a/packages/website/src/components/PlaylistGenerator.tsx +++ b/packages/website/src/components/PlaylistGenerator.tsx @@ -25,6 +25,7 @@ import { FilterControl } from "./TankSearch/components/FilterControl"; const tankDefinitions = await api.tankDefinitions(); const POOL_HEIGHT = "16rem"; +const FILTERS_WIDTH = "32rem"; export function PlaylistGenerator({ skeleton }: MaybeSkeletonComponentProps) { const [largeWarningOpen, setLargeWarningOpen] = useState(false); @@ -80,13 +81,19 @@ export function PlaylistGenerator({ skeleton }: MaybeSkeletonComponentProps) { - + - +