mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 11:39:50 +02:00
Compare commits
58
Commits
ue-archive-4
...
v1.27.17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1558b69931 | ||
|
|
a560a3d3ae | ||
|
|
b620110d0f | ||
|
|
23ea72bf40 | ||
|
|
aaee0b7965 | ||
|
|
347523f9e7 | ||
|
|
3205630a0f | ||
|
|
8446e3fbbd | ||
|
|
6d25a97fc8 | ||
|
|
bdbfce0e18 | ||
|
|
8033e8f7cc | ||
|
|
f98c801d6c | ||
|
|
339663adaf | ||
|
|
2b15357dee | ||
|
|
1e05717daf | ||
|
|
c20f8c5040 | ||
|
|
f22f4eeccd | ||
|
|
79c450798c | ||
|
|
d247e67690 | ||
|
|
69b2035cfc | ||
|
|
62fed63310 | ||
|
|
7c29046390 | ||
|
|
b65c3c1d93 | ||
|
|
87f5d8fe8f | ||
|
|
525b9377cc | ||
|
|
94cba0c9ba | ||
|
|
4ddf77fc7f | ||
|
|
ae6dc259dc | ||
|
|
c2e95f6ef6 | ||
|
|
868812e578 | ||
|
|
b5064f9e8b | ||
|
|
a0eb0d54f0 | ||
|
|
e47b1315a9 | ||
|
|
cde6904513 | ||
|
|
997d1df8f9 | ||
|
|
9f15ded0c9 | ||
|
|
b2ee97b3d4 | ||
|
|
03d819a6c1 | ||
|
|
8ff26d57f6 | ||
|
|
c243ebd19a | ||
|
|
40f5042896 | ||
|
|
21130b29f8 | ||
|
|
d50b054243 | ||
|
|
6b7f8049fe | ||
|
|
e6029157aa | ||
|
|
ba656eb603 | ||
|
|
e8dd26b2ba | ||
|
|
897da9479d | ||
|
|
291395718f | ||
|
|
42e860b171 | ||
|
|
3b6d339310 | ||
|
|
728fd5b824 | ||
|
|
34217b2754 | ||
|
|
0b5c88531f | ||
|
|
e145d7c6e8 | ||
|
|
86f4712b87 | ||
|
|
ea90ca21b1 | ||
|
|
b3046d8655 |
No files matched your search
@@ -29,6 +29,8 @@ env:
|
|||||||
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
|
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
|
||||||
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
|
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
|
||||||
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
|
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
|
||||||
|
PUBLIC_GUIDE_WEBSITE: ${{ secrets.PUBLIC_GUIDE_WEBSITE }}
|
||||||
|
SERVICING: ${{ secrets.SERVICING }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
Vendored
+1
@@ -222,6 +222,7 @@
|
|||||||
"yolo",
|
"yolo",
|
||||||
"youtuber",
|
"youtuber",
|
||||||
"youtubers",
|
"youtubers",
|
||||||
|
"zoomable",
|
||||||
"zustand"
|
"zustand"
|
||||||
],
|
],
|
||||||
"deno.enable": false,
|
"deno.enable": false,
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# BlitzKit 1.27.16
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added links to GuidesBlitz to Tankopedia to aid users understand tanks beyond just numbers and stats
|
||||||
|
- Added a gun depression and yaw visualizer
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Made the colors of the advanced highlighting mode more subtle
|
||||||
|
- Made the Tankopedia stat lines more condensed
|
||||||
|
- You can now scroll the page even with your cursor on the Tankopedia sandbox
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fixed a bug where the range of increased gun depression was incorrectly doubled
|
||||||
|
- Removed an unintended space before the "x" in the "Gun damaged" stat
|
||||||
|
- Fixed an issue causing the 3D renderer to render frames even when off screen
|
||||||
|
- Fixed a differential issue causing excessive rerendering of the stat lines
|
||||||
|
- Images no longer exceed the width of the device in the docs
|
||||||
|
- Assault shells no longer scale the module damage with distance
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- Removed all code relating to the canceled "Shooting Range" mode
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# BlitzKit 1.27.17
|
||||||
|
|
||||||
|
This update was released early for the website. Today, it'll be officially announced and released for the bot
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added the shell reload visualizer to aid people in getting a feel for how long shell reloads really are
|
||||||
|
- Added lines of stats for the burst shells
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Removed the redundant word "Shell" from "Shell reload" and "Shell reloads"
|
||||||
|
- Removed the penetration over distance slider
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fixed the DPM of tanks with burst shells like the ATAC
|
||||||
|
- The DPM will still be different from the game due to a bug in World of Tanks Blitz; I am trying to get Wargaming to fix it; trust BlitzKit!
|
||||||
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkit",
|
"name": "blitzkit",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.27.15",
|
"version": "1.27.17",
|
||||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "./types.d.ts",
|
"types": "./types.d.ts",
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cheerio": "^1.1.2",
|
||||||
"googleapis": "^144.0.0",
|
"googleapis": "^144.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"protobufjs": "^7.5.2",
|
"protobufjs": "^7.5.2",
|
||||||
|
|||||||
@@ -197,15 +197,10 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
|
|||||||
|
|
||||||
turret.guns.forEach((gun) => {
|
turret.guns.forEach((gun) => {
|
||||||
if (
|
if (
|
||||||
gun.gun_type!.value.base.research_cost !== undefined &&
|
gun.research_cost !== undefined &&
|
||||||
typeof gun.gun_type!.value.base.research_cost
|
typeof gun.research_cost.research_cost_type!.value === 'number'
|
||||||
.research_cost_type!.value === 'number'
|
|
||||||
) {
|
) {
|
||||||
gunXps.set(
|
gunXps.set(gun.id, gun.research_cost.research_cost_type!.value);
|
||||||
gun.gun_type!.value.base.id,
|
|
||||||
gun.gun_type!.value.base.research_cost.research_cost_type!
|
|
||||||
.value,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
GunDefinition,
|
GunDefinition,
|
||||||
GunDefinitionAutoReloaderProperties,
|
GunDefinitionAutoReloader,
|
||||||
ShellDefinition,
|
ShellDefinition,
|
||||||
} from '../protos';
|
} from '../protos';
|
||||||
|
|
||||||
@@ -15,34 +15,46 @@ export function resolveDpm(
|
|||||||
let dps: number;
|
let dps: number;
|
||||||
|
|
||||||
if (gun.gun_type!.$case === 'regular') {
|
if (gun.gun_type!.$case === 'regular') {
|
||||||
dps = alpha / (reloadCoefficient * gun.gun_type!.value.extension.reload);
|
dps = alpha / (reloadCoefficient * gun.gun_type!.value.reload);
|
||||||
} else if (gun.gun_type!.$case === 'auto_loader') {
|
} else if (gun.gun_type!.$case === 'auto_loader') {
|
||||||
dps =
|
const damage = alpha * gun.gun_type!.value.shell_count;
|
||||||
(alpha * gun.gun_type!.value.extension.shell_count) /
|
let time = gun.gun_type!.value.clip_reload * reloadCoefficient;
|
||||||
(gun.gun_type!.value.extension.clip_reload * reloadCoefficient +
|
|
||||||
(gun.gun_type!.value.extension.shell_count - 1) *
|
|
||||||
gun.gun_type!.value.extension.intra_clip *
|
|
||||||
intraClipCoefficient);
|
|
||||||
} else {
|
|
||||||
const mostOptimalShell =
|
|
||||||
gun.gun_type!.value.extension.shell_reloads.reduce<null | {
|
|
||||||
index: number;
|
|
||||||
reload: number;
|
|
||||||
}>((current, reloadRaw, index) => {
|
|
||||||
const reload =
|
|
||||||
reloadRaw * reloadCoefficient +
|
|
||||||
(index > 0
|
|
||||||
? (
|
|
||||||
gun.gun_type!.value
|
|
||||||
.extension as GunDefinitionAutoReloaderProperties
|
|
||||||
).intra_clip * intraClipCoefficient
|
|
||||||
: 0);
|
|
||||||
|
|
||||||
if (current === null || reload < current.reload) {
|
if (gun.burst) {
|
||||||
return { index, reload };
|
time +=
|
||||||
}
|
(gun.gun_type!.value.shell_count / gun.burst.count - 1) *
|
||||||
return current;
|
gun.gun_type!.value.intra_clip *
|
||||||
}, null)!;
|
intraClipCoefficient;
|
||||||
|
|
||||||
|
time +=
|
||||||
|
(gun.gun_type!.value.shell_count / gun.burst.count) *
|
||||||
|
(gun.burst.count - 1) *
|
||||||
|
gun.burst.interval;
|
||||||
|
} else {
|
||||||
|
time +=
|
||||||
|
(gun.gun_type!.value.shell_count - 1) *
|
||||||
|
gun.gun_type!.value.intra_clip *
|
||||||
|
intraClipCoefficient;
|
||||||
|
}
|
||||||
|
|
||||||
|
dps = damage / time;
|
||||||
|
} else {
|
||||||
|
const mostOptimalShell = gun.gun_type!.value.shell_reloads.reduce<null | {
|
||||||
|
index: number;
|
||||||
|
reload: number;
|
||||||
|
}>((current, reloadRaw, index) => {
|
||||||
|
const reload =
|
||||||
|
reloadRaw * reloadCoefficient +
|
||||||
|
(index > 0
|
||||||
|
? (gun.gun_type!.value as GunDefinitionAutoReloader).intra_clip *
|
||||||
|
intraClipCoefficient
|
||||||
|
: 0);
|
||||||
|
|
||||||
|
if (current === null || reload < current.reload) {
|
||||||
|
return { index, reload };
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}, null)!;
|
||||||
|
|
||||||
dps = alpha / mostOptimalShell?.reload;
|
dps = alpha / mostOptimalShell?.reload;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ export function uniqueGuns(turrets: TurretDefinition[]) {
|
|||||||
|
|
||||||
forEachRight(turrets, (turret) => {
|
forEachRight(turrets, (turret) => {
|
||||||
forEachRight(turret.guns, (gun) => {
|
forEachRight(turret.guns, (gun) => {
|
||||||
if (!gunIds.has(gun.gun_type!.value.base.id)) {
|
if (!gunIds.has(gun.id)) {
|
||||||
gunIds.add(gun.gun_type!.value.base.id);
|
gunIds.add(gun.id);
|
||||||
guns.push({ turret, gun });
|
guns.push({ turret, gun });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -108,58 +108,47 @@ message GunDefinition {
|
|||||||
GunDefinitionAutoLoader auto_loader = 2;
|
GunDefinitionAutoLoader auto_loader = 2;
|
||||||
GunDefinitionAutoReloader auto_reloader = 3;
|
GunDefinitionAutoReloader auto_reloader = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
required uint32 id = 4;
|
||||||
|
required float rotation_speed = 5;
|
||||||
|
optional ResearchCost research_cost = 6;
|
||||||
|
required uint32 weight = 7;
|
||||||
|
required I18nString name = 8;
|
||||||
|
required uint32 tier = 9;
|
||||||
|
repeated ShellDefinition shells = 10;
|
||||||
|
required float camouflage_loss = 11;
|
||||||
|
required float aim_time = 12;
|
||||||
|
required float dispersion_base = 13;
|
||||||
|
required float dispersion_traverse = 14;
|
||||||
|
required float dispersion_shot = 15;
|
||||||
|
required float dispersion_damaged = 16;
|
||||||
|
repeated Unlock unlocks = 17;
|
||||||
|
required uint32 shell_capacity = 18;
|
||||||
|
optional AssaultRanges assault_ranges = 19;
|
||||||
|
optional Burst burst = 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GunDefinitionRegular {
|
message GunDefinitionRegular {
|
||||||
required GunDefinitionBase base = 1;
|
|
||||||
required GunDefinitionRegularProperties extension = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GunDefinitionRegularProperties {
|
|
||||||
required float reload = 1;
|
required float reload = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GunDefinitionAutoLoader {
|
message GunDefinitionAutoLoader {
|
||||||
required GunDefinitionBase base = 1;
|
|
||||||
required GunDefinitionAutoLoaderProperties extension = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GunDefinitionAutoLoaderProperties {
|
|
||||||
required float clip_reload = 1;
|
required float clip_reload = 1;
|
||||||
required float intra_clip = 2;
|
required float intra_clip = 2;
|
||||||
required float shell_count = 3;
|
required float shell_count = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GunDefinitionAutoReloader {
|
message Burst {
|
||||||
required GunDefinitionBase base = 1;
|
required uint32 count = 1;
|
||||||
required GunDefinitionAutoReloaderProperties extension = 2;
|
required float interval = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GunDefinitionAutoReloaderProperties {
|
message GunDefinitionAutoReloader {
|
||||||
repeated float shell_reloads = 1;
|
repeated float shell_reloads = 1;
|
||||||
required float intra_clip = 2;
|
required float intra_clip = 2;
|
||||||
required float shell_count = 3;
|
required float shell_count = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GunDefinitionBase {
|
|
||||||
required uint32 id = 1;
|
|
||||||
required float rotation_speed = 2;
|
|
||||||
optional ResearchCost research_cost = 3;
|
|
||||||
required uint32 weight = 4;
|
|
||||||
required I18nString name = 5;
|
|
||||||
required uint32 tier = 6;
|
|
||||||
repeated ShellDefinition shells = 7;
|
|
||||||
required float camouflage_loss = 8;
|
|
||||||
required float aim_time = 9;
|
|
||||||
required float dispersion_base = 10;
|
|
||||||
required float dispersion_traverse = 11;
|
|
||||||
required float dispersion_shot = 12;
|
|
||||||
required float dispersion_damaged = 13;
|
|
||||||
repeated Unlock unlocks = 14;
|
|
||||||
required uint32 shell_capacity = 15;
|
|
||||||
optional AssaultRanges assault_ranges = 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
message AssaultRanges {
|
message AssaultRanges {
|
||||||
repeated AssaultRange ranges = 1;
|
repeated AssaultRange ranges = 1;
|
||||||
repeated ShellType types = 2;
|
repeated ShellType types = 2;
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -913,6 +913,13 @@
|
|||||||
"percentage": "%s1%",
|
"percentage": "%s1%",
|
||||||
"ghost_factory": "Ghost Factory",
|
"ghost_factory": "Ghost Factory",
|
||||||
"dead_rail": "Dead Rail"
|
"dead_rail": "Dead Rail"
|
||||||
|
},
|
||||||
|
|
||||||
|
"flexibility": {
|
||||||
|
"yaw": "Yaw",
|
||||||
|
"pitch": "Pitch",
|
||||||
|
"min": "Min",
|
||||||
|
"max": "Max"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1026,7 +1033,6 @@
|
|||||||
|
|
||||||
"review": {
|
"review": {
|
||||||
"title": "Review videos",
|
"title": "Review videos",
|
||||||
"disclaimer": "BlitzKit does not endorse any review videos.",
|
|
||||||
"join": "Apply to %s1 YouTuber program"
|
"join": "Apply to %s1 YouTuber program"
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1090,8 +1096,8 @@
|
|||||||
"dpmEffective": "Effective at 60s",
|
"dpmEffective": "Effective at 60s",
|
||||||
"shells": "Shells",
|
"shells": "Shells",
|
||||||
"mostOptimalShellIndex": "Most optimal shell",
|
"mostOptimalShellIndex": "Most optimal shell",
|
||||||
"shellReloads": "Shell reloads",
|
"shellReloads": "Reloads",
|
||||||
"shellReload": "Shell reload",
|
"shellReload": "Reload",
|
||||||
"intraClip": "Intra-clip",
|
"intraClip": "Intra-clip",
|
||||||
"caliber": "Caliber",
|
"caliber": "Caliber",
|
||||||
"penetration": "Penetration",
|
"penetration": "Penetration",
|
||||||
@@ -1149,7 +1155,9 @@
|
|||||||
"shellRange": "Shell range",
|
"shellRange": "Shell range",
|
||||||
"shellCapacity": "Shell capacity",
|
"shellCapacity": "Shell capacity",
|
||||||
"gunType": "Gun type",
|
"gunType": "Gun type",
|
||||||
"penetrationAt250m": "Penetration at 250m"
|
"penetrationAt250m": "Penetration at 250m",
|
||||||
|
"burstShells": "Burst shell count",
|
||||||
|
"burstInterShell": "Burst inter-shell"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -810,6 +810,12 @@
|
|||||||
"percentage": "%s1%",
|
"percentage": "%s1%",
|
||||||
"ghost_factory": "Ghost Factory",
|
"ghost_factory": "Ghost Factory",
|
||||||
"dead_rail": "Dead Rail"
|
"dead_rail": "Dead Rail"
|
||||||
|
},
|
||||||
|
"flexibility": {
|
||||||
|
"yaw": "Yaw",
|
||||||
|
"pitch": "Pitch",
|
||||||
|
"min": "Min",
|
||||||
|
"max": "Max"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
@@ -912,7 +918,6 @@
|
|||||||
},
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"title": "Vídeos de reseña",
|
"title": "Vídeos de reseña",
|
||||||
"disclaimer": "BlitzKit no respalda ningún vídeo de reseñas.",
|
|
||||||
"join": "Aplica para el programa de YouTube de %s1"
|
"join": "Aplica para el programa de YouTube de %s1"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -973,8 +978,8 @@
|
|||||||
"dpmEffective": "Effective at 60s",
|
"dpmEffective": "Effective at 60s",
|
||||||
"shells": "Proyectiles",
|
"shells": "Proyectiles",
|
||||||
"mostOptimalShellIndex": "Proyectil más óptimo",
|
"mostOptimalShellIndex": "Proyectil más óptimo",
|
||||||
"shellReloads": "Tiempos de recarga",
|
"shellReloads": "Reloads",
|
||||||
"shellReload": "Tiempo de recarga",
|
"shellReload": "Recarga",
|
||||||
"intraClip": "Intra-clip",
|
"intraClip": "Intra-clip",
|
||||||
"caliber": "Calibre",
|
"caliber": "Calibre",
|
||||||
"penetration": "Penetración",
|
"penetration": "Penetración",
|
||||||
@@ -1032,7 +1037,9 @@
|
|||||||
"shellRange": "Rango del proyectil",
|
"shellRange": "Rango del proyectil",
|
||||||
"shellCapacity": "Capacidad de proyectiles",
|
"shellCapacity": "Capacidad de proyectiles",
|
||||||
"gunType": "Tipo de cañón",
|
"gunType": "Tipo de cañón",
|
||||||
"penetrationAt250m": "Penetración a 250 m"
|
"penetrationAt250m": "Penetración a 250 m",
|
||||||
|
"burstShells": "Burst shell count",
|
||||||
|
"burstInterShell": "Burst inter-shell"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"firepower": {
|
"firepower": {
|
||||||
|
|||||||
@@ -810,6 +810,12 @@
|
|||||||
"percentage": "%s1%",
|
"percentage": "%s1%",
|
||||||
"ghost_factory": "Ghost Factory",
|
"ghost_factory": "Ghost Factory",
|
||||||
"dead_rail": "Dead Rail"
|
"dead_rail": "Dead Rail"
|
||||||
|
},
|
||||||
|
"flexibility": {
|
||||||
|
"yaw": "Yaw",
|
||||||
|
"pitch": "Pitch",
|
||||||
|
"min": "Min",
|
||||||
|
"max": "Max"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
@@ -912,7 +918,6 @@
|
|||||||
},
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"title": "Review videos",
|
"title": "Review videos",
|
||||||
"disclaimer": "BlitzKit does not endorse any review videos.",
|
|
||||||
"join": "Apply to %s1 YouTuber program"
|
"join": "Apply to %s1 YouTuber program"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -973,8 +978,8 @@
|
|||||||
"dpmEffective": "Effective at 60s",
|
"dpmEffective": "Effective at 60s",
|
||||||
"shells": "Shells",
|
"shells": "Shells",
|
||||||
"mostOptimalShellIndex": "Most optimal shell",
|
"mostOptimalShellIndex": "Most optimal shell",
|
||||||
"shellReloads": "Shell reloads",
|
"shellReloads": "Reloads",
|
||||||
"shellReload": "Shell reload",
|
"shellReload": "Reload",
|
||||||
"intraClip": "Intra-clip",
|
"intraClip": "Intra-clip",
|
||||||
"caliber": "Caliber",
|
"caliber": "Caliber",
|
||||||
"penetration": "Penetration",
|
"penetration": "Penetration",
|
||||||
@@ -1032,7 +1037,9 @@
|
|||||||
"shellRange": "Shell range",
|
"shellRange": "Shell range",
|
||||||
"shellCapacity": "Shell capacity",
|
"shellCapacity": "Shell capacity",
|
||||||
"gunType": "Gun type",
|
"gunType": "Gun type",
|
||||||
"penetrationAt250m": "Penetration at 250m"
|
"penetrationAt250m": "Penetration at 250m",
|
||||||
|
"burstShells": "Burst shell count",
|
||||||
|
"burstInterShell": "Burst inter-shell"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"firepower": {
|
"firepower": {
|
||||||
|
|||||||
@@ -810,6 +810,12 @@
|
|||||||
"percentage": "%s1%",
|
"percentage": "%s1%",
|
||||||
"ghost_factory": "Ghost Factory",
|
"ghost_factory": "Ghost Factory",
|
||||||
"dead_rail": "Dead Rail"
|
"dead_rail": "Dead Rail"
|
||||||
|
},
|
||||||
|
"flexibility": {
|
||||||
|
"yaw": "Yaw",
|
||||||
|
"pitch": "Pitch",
|
||||||
|
"min": "Min",
|
||||||
|
"max": "Max"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
@@ -912,7 +918,6 @@
|
|||||||
},
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"title": "Review videos",
|
"title": "Review videos",
|
||||||
"disclaimer": "BlitzKitはいかなるレビュー動画も後援していません。",
|
|
||||||
"join": "%s1 YouTuberプログラムに応募する"
|
"join": "%s1 YouTuberプログラムに応募する"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -973,7 +978,7 @@
|
|||||||
"dpmEffective": "60秒での有効値",
|
"dpmEffective": "60秒での有効値",
|
||||||
"shells": "砲弾",
|
"shells": "砲弾",
|
||||||
"mostOptimalShellIndex": "最適な弾数",
|
"mostOptimalShellIndex": "最適な弾数",
|
||||||
"shellReloads": "自動装填時間",
|
"shellReloads": "Reloads",
|
||||||
"shellReload": "装填時間",
|
"shellReload": "装填時間",
|
||||||
"intraClip": "砲弾再装填時間",
|
"intraClip": "砲弾再装填時間",
|
||||||
"caliber": "口径",
|
"caliber": "口径",
|
||||||
@@ -1032,7 +1037,9 @@
|
|||||||
"shellRange": "射程",
|
"shellRange": "射程",
|
||||||
"shellCapacity": "総弾数",
|
"shellCapacity": "総弾数",
|
||||||
"gunType": "主砲タイプ",
|
"gunType": "主砲タイプ",
|
||||||
"penetrationAt250m": "250m での貫通力"
|
"penetrationAt250m": "250m での貫通力",
|
||||||
|
"burstShells": "Burst shell count",
|
||||||
|
"burstInterShell": "Burst inter-shell"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"firepower": {
|
"firepower": {
|
||||||
|
|||||||
@@ -810,6 +810,12 @@
|
|||||||
"percentage": "%s1%",
|
"percentage": "%s1%",
|
||||||
"ghost_factory": "Ghost Factory",
|
"ghost_factory": "Ghost Factory",
|
||||||
"dead_rail": "Dead Rail"
|
"dead_rail": "Dead Rail"
|
||||||
|
},
|
||||||
|
"flexibility": {
|
||||||
|
"yaw": "Yaw",
|
||||||
|
"pitch": "Pitch",
|
||||||
|
"min": "Min",
|
||||||
|
"max": "Max"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
@@ -912,7 +918,6 @@
|
|||||||
},
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"title": "Review videos",
|
"title": "Review videos",
|
||||||
"disclaimer": "BlitzKit does not endorse any review videos.",
|
|
||||||
"join": "Apply to %s1 YouTuber program"
|
"join": "Apply to %s1 YouTuber program"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -973,8 +978,8 @@
|
|||||||
"dpmEffective": "Effective at 60s",
|
"dpmEffective": "Effective at 60s",
|
||||||
"shells": "Shells",
|
"shells": "Shells",
|
||||||
"mostOptimalShellIndex": "Most optimal shell",
|
"mostOptimalShellIndex": "Most optimal shell",
|
||||||
"shellReloads": "Shell reloads",
|
"shellReloads": "Reloads",
|
||||||
"shellReload": "Shell reload",
|
"shellReload": "Reload",
|
||||||
"intraClip": "Intra-clip",
|
"intraClip": "Intra-clip",
|
||||||
"caliber": "Caliber",
|
"caliber": "Caliber",
|
||||||
"penetration": "Penetration",
|
"penetration": "Penetration",
|
||||||
@@ -1032,7 +1037,9 @@
|
|||||||
"shellRange": "Shell range",
|
"shellRange": "Shell range",
|
||||||
"shellCapacity": "Shell capacity",
|
"shellCapacity": "Shell capacity",
|
||||||
"gunType": "Gun type",
|
"gunType": "Gun type",
|
||||||
"penetrationAt250m": "Penetration at 250m"
|
"penetrationAt250m": "Penetration at 250m",
|
||||||
|
"burstShells": "Burst shell count",
|
||||||
|
"burstInterShell": "Burst inter-shell"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"firepower": {
|
"firepower": {
|
||||||
|
|||||||
@@ -810,6 +810,12 @@
|
|||||||
"percentage": "%s1%",
|
"percentage": "%s1%",
|
||||||
"ghost_factory": "Ghost Factory",
|
"ghost_factory": "Ghost Factory",
|
||||||
"dead_rail": "Dead Rail"
|
"dead_rail": "Dead Rail"
|
||||||
|
},
|
||||||
|
"flexibility": {
|
||||||
|
"yaw": "Yaw",
|
||||||
|
"pitch": "Pitch",
|
||||||
|
"min": "Min",
|
||||||
|
"max": "Max"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
@@ -912,7 +918,6 @@
|
|||||||
},
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"title": "Vídeos de análise",
|
"title": "Vídeos de análise",
|
||||||
"disclaimer": "BlitzKit não aprova nenhum vídeo de análise.",
|
|
||||||
"join": "Candidate-se para %s1 um programa YouTuber"
|
"join": "Candidate-se para %s1 um programa YouTuber"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -973,8 +978,8 @@
|
|||||||
"dpmEffective": "Efetivo aos 60s",
|
"dpmEffective": "Efetivo aos 60s",
|
||||||
"shells": "Projéteis",
|
"shells": "Projéteis",
|
||||||
"mostOptimalShellIndex": "Projétil mais ideal",
|
"mostOptimalShellIndex": "Projétil mais ideal",
|
||||||
"shellReloads": "Recarga de projétil",
|
"shellReloads": "Reloads",
|
||||||
"shellReload": "Recarregamento de projétil",
|
"shellReload": "Recarga",
|
||||||
"intraClip": "Intra-clipe",
|
"intraClip": "Intra-clipe",
|
||||||
"caliber": "Calibre",
|
"caliber": "Calibre",
|
||||||
"penetration": "Penetração",
|
"penetration": "Penetração",
|
||||||
@@ -1032,7 +1037,9 @@
|
|||||||
"shellRange": "Alcance de projétil",
|
"shellRange": "Alcance de projétil",
|
||||||
"shellCapacity": "Capacidade de projéteis",
|
"shellCapacity": "Capacidade de projéteis",
|
||||||
"gunType": "Tipo de arma",
|
"gunType": "Tipo de arma",
|
||||||
"penetrationAt250m": "Penetração à 250m"
|
"penetrationAt250m": "Penetração à 250m",
|
||||||
|
"burstShells": "Burst shell count",
|
||||||
|
"burstInterShell": "Burst inter-shell"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"firepower": {
|
"firepower": {
|
||||||
|
|||||||
@@ -788,7 +788,7 @@
|
|||||||
"tank_seo_description": "%s1 броня, статистика, оборудование, снабжение и многое другое. %s2 УВМ, %s3hp альфа, %s4mm пробитие, %s5км/ч лучшая скорость, %s6hp здоровье.",
|
"tank_seo_description": "%s1 броня, статистика, оборудование, снабжение и многое другое. %s2 УВМ, %s3hp альфа, %s4mm пробитие, %s5км/ч лучшая скорость, %s6hp здоровье.",
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
"assault": {
|
"assault": {
|
||||||
"title": "Assault shells"
|
"title": "Штурмовые снаряды"
|
||||||
},
|
},
|
||||||
"rotator": {
|
"rotator": {
|
||||||
"hull": "Корпус",
|
"hull": "Корпус",
|
||||||
@@ -810,6 +810,12 @@
|
|||||||
"percentage": "%s1%",
|
"percentage": "%s1%",
|
||||||
"ghost_factory": "Промзона",
|
"ghost_factory": "Промзона",
|
||||||
"dead_rail": "Эшелон"
|
"dead_rail": "Эшелон"
|
||||||
|
},
|
||||||
|
"flexibility": {
|
||||||
|
"yaw": "Отклонение",
|
||||||
|
"pitch": "Наклон",
|
||||||
|
"min": "Вниз",
|
||||||
|
"max": "Вверх"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
@@ -912,7 +918,6 @@
|
|||||||
},
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"title": "Видеообзоры",
|
"title": "Видеообзоры",
|
||||||
"disclaimer": "BlitzKit не несёт ответственности за видеообзоры.",
|
|
||||||
"join": "Предложить ютубера для программы %s1"
|
"join": "Предложить ютубера для программы %s1"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -973,13 +978,13 @@
|
|||||||
"dpmEffective": "Эффективность за 60с",
|
"dpmEffective": "Эффективность за 60с",
|
||||||
"shells": "Снарядов в барабане",
|
"shells": "Снарядов в барабане",
|
||||||
"mostOptimalShellIndex": "Самый эффективный снаряд",
|
"mostOptimalShellIndex": "Самый эффективный снаряд",
|
||||||
"shellReloads": "Перезарядка снарядов",
|
"shellReloads": "Reloads",
|
||||||
"shellReload": "Перезарядка",
|
"shellReload": "Перезарядка",
|
||||||
"intraClip": "Между снарядами",
|
"intraClip": "Между снарядами",
|
||||||
"caliber": "Калибр",
|
"caliber": "Калибр",
|
||||||
"penetration": "Пробитие",
|
"penetration": "Пробитие",
|
||||||
"damage": "Урон",
|
"damage": "Урон",
|
||||||
"damageWithoutAssault": "Damage without assault shells",
|
"damageWithoutAssault": "Урон без штурмовых снарядов",
|
||||||
"clipDamage": "Урон с барабана",
|
"clipDamage": "Урон с барабана",
|
||||||
"moduleDamage": "Урон по модулям",
|
"moduleDamage": "Урон по модулям",
|
||||||
"explosionRadius": "Разлет осколков",
|
"explosionRadius": "Разлет осколков",
|
||||||
@@ -1032,7 +1037,9 @@
|
|||||||
"shellRange": "Дальность снаряда",
|
"shellRange": "Дальность снаряда",
|
||||||
"shellCapacity": "Боезапас",
|
"shellCapacity": "Боезапас",
|
||||||
"gunType": "Тип орудия",
|
"gunType": "Тип орудия",
|
||||||
"penetrationAt250m": "Пробитие на 250м"
|
"penetrationAt250m": "Пробитие на 250м",
|
||||||
|
"burstShells": "Burst shell count",
|
||||||
|
"burstInterShell": "Burst inter-shell"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"firepower": {
|
"firepower": {
|
||||||
|
|||||||
@@ -810,6 +810,12 @@
|
|||||||
"percentage": "%s1%",
|
"percentage": "%s1%",
|
||||||
"ghost_factory": "Ghost Factory",
|
"ghost_factory": "Ghost Factory",
|
||||||
"dead_rail": "Dead Rail"
|
"dead_rail": "Dead Rail"
|
||||||
|
},
|
||||||
|
"flexibility": {
|
||||||
|
"yaw": "Yaw",
|
||||||
|
"pitch": "Pitch",
|
||||||
|
"min": "Min",
|
||||||
|
"max": "Max"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
@@ -912,7 +918,6 @@
|
|||||||
},
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"title": "Review videos",
|
"title": "Review videos",
|
||||||
"disclaimer": "BlitzKit does not endorse any review videos.",
|
|
||||||
"join": "Apply to %s1 YouTuber program"
|
"join": "Apply to %s1 YouTuber program"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -973,8 +978,8 @@
|
|||||||
"dpmEffective": "Effective at 60s",
|
"dpmEffective": "Effective at 60s",
|
||||||
"shells": "Снаряди",
|
"shells": "Снаряди",
|
||||||
"mostOptimalShellIndex": "Most optimal shell",
|
"mostOptimalShellIndex": "Most optimal shell",
|
||||||
"shellReloads": "Shell reloads",
|
"shellReloads": "Reloads",
|
||||||
"shellReload": "Shell reload",
|
"shellReload": "Час перезаряджання",
|
||||||
"intraClip": "Intra-clip",
|
"intraClip": "Intra-clip",
|
||||||
"caliber": "Калібр",
|
"caliber": "Калібр",
|
||||||
"penetration": "Бронепробиття",
|
"penetration": "Бронепробиття",
|
||||||
@@ -1032,7 +1037,9 @@
|
|||||||
"shellRange": "Shell range",
|
"shellRange": "Shell range",
|
||||||
"shellCapacity": "Shell capacity",
|
"shellCapacity": "Shell capacity",
|
||||||
"gunType": "Тип гармати",
|
"gunType": "Тип гармати",
|
||||||
"penetrationAt250m": "Penetration at 250m"
|
"penetrationAt250m": "Penetration at 250m",
|
||||||
|
"burstShells": "Burst shell count",
|
||||||
|
"burstInterShell": "Burst inter-shell"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"firepower": {
|
"firepower": {
|
||||||
|
|||||||
@@ -810,6 +810,12 @@
|
|||||||
"percentage": "%s1%",
|
"percentage": "%s1%",
|
||||||
"ghost_factory": "幽灵工厂",
|
"ghost_factory": "幽灵工厂",
|
||||||
"dead_rail": "死亡铁道"
|
"dead_rail": "死亡铁道"
|
||||||
|
},
|
||||||
|
"flexibility": {
|
||||||
|
"yaw": "Yaw",
|
||||||
|
"pitch": "Pitch",
|
||||||
|
"min": "Min",
|
||||||
|
"max": "Max"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
@@ -912,7 +918,6 @@
|
|||||||
},
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"title": "回顾影片",
|
"title": "回顾影片",
|
||||||
"disclaimer": "BlitzKit 不支援任何影片回顾。",
|
|
||||||
"join": "应用到 %s1 YouTuber程序"
|
"join": "应用到 %s1 YouTuber程序"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -973,8 +978,8 @@
|
|||||||
"dpmEffective": "60秒内生效",
|
"dpmEffective": "60秒内生效",
|
||||||
"shells": "炮弹 ",
|
"shells": "炮弹 ",
|
||||||
"mostOptimalShellIndex": "最优炮弹",
|
"mostOptimalShellIndex": "最优炮弹",
|
||||||
"shellReloads": "弹匣装填",
|
"shellReloads": "Reloads",
|
||||||
"shellReload": "炮弹装填",
|
"shellReload": "装填时间",
|
||||||
"intraClip": "炮弹装填",
|
"intraClip": "炮弹装填",
|
||||||
"caliber": "口径",
|
"caliber": "口径",
|
||||||
"penetration": "穿深",
|
"penetration": "穿深",
|
||||||
@@ -1032,7 +1037,9 @@
|
|||||||
"shellRange": "射程",
|
"shellRange": "射程",
|
||||||
"shellCapacity": "携弹量",
|
"shellCapacity": "携弹量",
|
||||||
"gunType": "炮管类型",
|
"gunType": "炮管类型",
|
||||||
"penetrationAt250m": "两百五十米处的穿透力"
|
"penetrationAt250m": "两百五十米处的穿透力",
|
||||||
|
"burstShells": "Burst shell count",
|
||||||
|
"burstInterShell": "Burst inter-shell"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"firepower": {
|
"firepower": {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
EquipmentSlot,
|
EquipmentSlot,
|
||||||
GameDefinitions,
|
GameDefinitions,
|
||||||
GunDefinition,
|
GunDefinition,
|
||||||
GunDefinitionBase,
|
|
||||||
I18nString,
|
I18nString,
|
||||||
MapDefinitions,
|
MapDefinitions,
|
||||||
ModelDefinitions,
|
ModelDefinitions,
|
||||||
@@ -274,6 +273,7 @@ interface VehicleDefinitions {
|
|||||||
pumpGunMode?: boolean;
|
pumpGunMode?: boolean;
|
||||||
pumpGunReloadTimes?: string;
|
pumpGunReloadTimes?: string;
|
||||||
clip?: { count: number; rate: number };
|
clip?: { count: number; rate: number };
|
||||||
|
burst?: { count: number; rate: number };
|
||||||
models: { undamaged: string };
|
models: { undamaged: string };
|
||||||
extras?: {
|
extras?: {
|
||||||
trayShell?: {
|
trayShell?: {
|
||||||
@@ -1304,7 +1304,7 @@ export async function definitions() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const base = {
|
tankDefinitions.tanks[tankId].turrets[turretIndex].guns.push({
|
||||||
id: gunId,
|
id: gunId,
|
||||||
weight: gunListEntry.weight,
|
weight: gunListEntry.weight,
|
||||||
rotation_speed: gunListEntry.rotationSpeed,
|
rotation_speed: gunListEntry.rotationSpeed,
|
||||||
@@ -1324,41 +1324,38 @@ export async function definitions() {
|
|||||||
unlocks: resolveUnlocks(gun.unlocks),
|
unlocks: resolveUnlocks(gun.unlocks),
|
||||||
shell_capacity: gun.maxAmmo ?? gunListEntry.maxAmmo,
|
shell_capacity: gun.maxAmmo ?? gunListEntry.maxAmmo,
|
||||||
assault_ranges,
|
assault_ranges,
|
||||||
} satisfies GunDefinitionBase;
|
burst:
|
||||||
|
gun.burst && gun.burst.count > 1
|
||||||
tankDefinitions.tanks[tankId].turrets[turretIndex].guns.push({
|
? {
|
||||||
|
count: gun.burst.count,
|
||||||
|
interval: 60 / gun.burst.rate,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
gun_type:
|
gun_type:
|
||||||
gunType === 'regular'
|
gunType === 'regular'
|
||||||
? {
|
? {
|
||||||
$case: 'regular',
|
$case: 'regular',
|
||||||
value: {
|
value: {
|
||||||
base,
|
reload: gun.reloadTime,
|
||||||
extension: { reload: gun.reloadTime },
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: gunType === 'autoReloader'
|
: gunType === 'autoReloader'
|
||||||
? {
|
? {
|
||||||
$case: 'auto_reloader',
|
$case: 'auto_reloader',
|
||||||
value: {
|
value: {
|
||||||
base,
|
intra_clip: 60 / gun.clip!.rate,
|
||||||
extension: {
|
shell_count: gunClipCount,
|
||||||
intra_clip: 60 / gun.clip!.rate,
|
shell_reloads: gun
|
||||||
shell_count: gunClipCount,
|
.pumpGunReloadTimes!.split(' ')
|
||||||
shell_reloads: gun
|
.map(Number),
|
||||||
.pumpGunReloadTimes!.split(' ')
|
|
||||||
.map(Number),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
$case: 'auto_loader',
|
$case: 'auto_loader',
|
||||||
value: {
|
value: {
|
||||||
base,
|
intra_clip: 60 / gun.clip!.rate,
|
||||||
extension: {
|
clip_reload: gun.reloadTime,
|
||||||
intra_clip: 60 / gun.clip!.rate,
|
shell_count: gunClipCount,
|
||||||
clip_reload: gun.reloadTime,
|
|
||||||
shell_count: gunClipCount,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} satisfies GunDefinition);
|
} satisfies GunDefinition);
|
||||||
@@ -1407,7 +1404,7 @@ export async function definitions() {
|
|||||||
|
|
||||||
tankDefinitions.tanks[tankId].turrets[turretIndex].guns[
|
tankDefinitions.tanks[tankId].turrets[turretIndex].guns[
|
||||||
gunIndex
|
gunIndex
|
||||||
].gun_type!.value.base!.shells.push({
|
].shells.push({
|
||||||
id: shellId,
|
id: shellId,
|
||||||
name: shellName,
|
name: shellName,
|
||||||
velocity: gunShellEntry.speed,
|
velocity: gunShellEntry.speed,
|
||||||
@@ -1495,9 +1492,7 @@ export async function definitions() {
|
|||||||
turret.research_cost = turretXps.get(turret.id);
|
turret.research_cost = turretXps.get(turret.id);
|
||||||
|
|
||||||
Object.values(turret.guns).forEach((gunRaw) => {
|
Object.values(turret.guns).forEach((gunRaw) => {
|
||||||
gunRaw.gun_type!.value.base!.research_cost = gunXps.get(
|
gunRaw.research_cost = gunXps.get(gunRaw.id);
|
||||||
gunRaw.gun_type!.value.base!.id,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export async function tankIcons() {
|
|||||||
|
|
||||||
const nationVehicleId = tank.id;
|
const nationVehicleId = tank.id;
|
||||||
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
||||||
|
|
||||||
const parameters = await readYAMLDVPL<TankParameters>(
|
const parameters = await readYAMLDVPL<TankParameters>(
|
||||||
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { toUniqueId } from '@blitzkit/core';
|
import { toUniqueId } from '@blitzkit/core';
|
||||||
import { NodeIO } from '@gltf-transform/core';
|
import { NodeIO } from '@gltf-transform/core';
|
||||||
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
|
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
|
||||||
import { readdir, writeFile } from 'fs/promises';
|
import { readdir } from 'fs/promises';
|
||||||
import { extractModel } from '../core/blitz/extractModel';
|
import { extractModel } from '../core/blitz/extractModel';
|
||||||
import { readXMLDVPL } from '../core/blitz/readXMLDVPL';
|
import { readXMLDVPL } from '../core/blitz/readXMLDVPL';
|
||||||
import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL';
|
import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL';
|
||||||
@@ -32,6 +32,7 @@ export async function tankModels() {
|
|||||||
if (tankKey.includes('tutorial_bot')) continue;
|
if (tankKey.includes('tutorial_bot')) continue;
|
||||||
|
|
||||||
const id = toUniqueId(nation, tank.id);
|
const id = toUniqueId(nation, tank.id);
|
||||||
|
|
||||||
const parameters = await readYAMLDVPL<TankParameters>(
|
const parameters = await readYAMLDVPL<TankParameters>(
|
||||||
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||||
);
|
);
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -100,8 +100,7 @@ export function ArmorPlateDisplay() {
|
|||||||
const { protagonist } = duelStore.getState();
|
const { protagonist } = duelStore.getState();
|
||||||
const tank = draft.model;
|
const tank = draft.model;
|
||||||
const turret = tank.turrets[protagonist.turret.id];
|
const turret = tank.turrets[protagonist.turret.id];
|
||||||
const gun =
|
const gun = turret.guns[protagonist.gun.id];
|
||||||
turret.guns[protagonist.gun.gun_type!.value.base.id];
|
|
||||||
const track = tank.tracks[protagonist.track.id];
|
const track = tank.tracks[protagonist.track.id];
|
||||||
|
|
||||||
if (highlightArmor.name.startsWith('hull_')) {
|
if (highlightArmor.name.startsWith('hull_')) {
|
||||||
@@ -154,12 +153,8 @@ export function ArmorPlateDisplay() {
|
|||||||
const turret = tank.turrets[protagonist.turret.id];
|
const turret = tank.turrets[protagonist.turret.id];
|
||||||
const initialTurret =
|
const initialTurret =
|
||||||
initialTank.turrets[protagonist.turret.id];
|
initialTank.turrets[protagonist.turret.id];
|
||||||
const gun =
|
const gun = turret.guns[protagonist.gun.id];
|
||||||
turret.guns[protagonist.gun.gun_type!.value.base.id];
|
const initialGun = initialTurret.guns[protagonist.gun.id];
|
||||||
const initialGun =
|
|
||||||
initialTurret.guns[
|
|
||||||
protagonist.gun.gun_type!.value.base.id
|
|
||||||
];
|
|
||||||
const track = tank.tracks[protagonist.track.id];
|
const track = tank.tracks[protagonist.track.id];
|
||||||
const initialTrack =
|
const initialTrack =
|
||||||
initialTank.tracks[protagonist.track.id];
|
initialTank.tracks[protagonist.track.id];
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ export const PrimaryArmorScene = memo(() => {
|
|||||||
const tankModelDefinition = useTankModelDefinition();
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
turretModelDefinition.guns[gun.gun_type!.value.base.id];
|
|
||||||
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
||||||
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
||||||
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import { invalidate, useFrame } from '@react-three/fiber';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
|
NormalBlending,
|
||||||
Object3D,
|
Object3D,
|
||||||
ShaderMaterial,
|
ShaderMaterial,
|
||||||
|
SubtractiveBlending,
|
||||||
UniformsLib,
|
UniformsLib,
|
||||||
UniformsUtils,
|
UniformsUtils,
|
||||||
Vector2,
|
Vector2,
|
||||||
@@ -32,6 +34,9 @@ const excludeMaterial = new MeshBasicMaterial({
|
|||||||
colorWrite: false,
|
colorWrite: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
const isHalloween = date.getMonth() === 9 && date.getDate() === 31;
|
||||||
|
|
||||||
export function PrimaryArmorSceneComponent({
|
export function PrimaryArmorSceneComponent({
|
||||||
node,
|
node,
|
||||||
thickness,
|
thickness,
|
||||||
@@ -46,6 +51,8 @@ export function PrimaryArmorSceneComponent({
|
|||||||
fog: true,
|
fog: true,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
|
|
||||||
|
blending: isHalloween ? SubtractiveBlending : NormalBlending,
|
||||||
|
|
||||||
uniforms: UniformsUtils.merge([
|
uniforms: UniformsUtils.merge([
|
||||||
UniformsLib.common,
|
UniformsLib.common,
|
||||||
UniformsLib.fog,
|
UniformsLib.fog,
|
||||||
|
|||||||
+6
-7
@@ -25,7 +25,6 @@ uniform mat4 inverseProjectionMatrix;
|
|||||||
uniform highp sampler2D spacedArmorBuffer;
|
uniform highp sampler2D spacedArmorBuffer;
|
||||||
uniform highp sampler2D spacedArmorDepth;
|
uniform highp sampler2D spacedArmorDepth;
|
||||||
|
|
||||||
const float HALF = 0.5;
|
|
||||||
const float EXPLOSION_DAMAGE_FACTOR = 1.1;
|
const float EXPLOSION_DAMAGE_FACTOR = 1.1;
|
||||||
const float RANDOMIZATION_FACTOR = 0.05;
|
const float RANDOMIZATION_FACTOR = 0.05;
|
||||||
const float GREEN_VALUE = 0.392;
|
const float GREEN_VALUE = 0.392;
|
||||||
@@ -38,7 +37,7 @@ float getDistance(vec2 coord, float depth) {
|
|||||||
|
|
||||||
vec3 getPenetrationColor(bool isThreeCalibersRule, bool couldHaveRicochet) {
|
vec3 getPenetrationColor(bool isThreeCalibersRule, bool couldHaveRicochet) {
|
||||||
if (advancedHighlighting && couldHaveRicochet) {
|
if (advancedHighlighting && couldHaveRicochet) {
|
||||||
return vec3(0.0, 1.0, isThreeCalibersRule ? 1.0 : 0.0);
|
return vec3(0.0, 1.0, isThreeCalibersRule ? 0.5 : 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return vec3(0.0, 1.0, 0.0);
|
return vec3(0.0, 1.0, 0.0);
|
||||||
@@ -81,12 +80,12 @@ void main() {
|
|||||||
float distFromArmor = primaryDist - spacedDist;
|
float distFromArmor = primaryDist - spacedDist;
|
||||||
|
|
||||||
if (canSplash) {
|
if (canSplash) {
|
||||||
float finalDamage = HALF * damage * (1.0 - distFromArmor / explosionRadius) - EXPLOSION_DAMAGE_FACTOR * (finalThickness + spacedArmorThickness);
|
float finalDamage = 0.5 * damage * (1.0 - distFromArmor / explosionRadius) - EXPLOSION_DAMAGE_FACTOR * (finalThickness + spacedArmorThickness);
|
||||||
splashChance = step(0.0, finalDamage);
|
splashChance = step(0.0, finalDamage);
|
||||||
penetrationChance = 0.0;
|
penetrationChance = 0.0;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
remainingPen -= HALF * remainingPen * distFromArmor;
|
remainingPen -= 0.5 * remainingPen * distFromArmor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,17 +97,17 @@ void main() {
|
|||||||
penetrationChance = clamp(1.0 - (delta + randomization) / (2.0 * randomization), 0.0, 1.0);
|
penetrationChance = clamp(1.0 - (delta + randomization) / (2.0 * randomization), 0.0, 1.0);
|
||||||
|
|
||||||
if (canSplash) {
|
if (canSplash) {
|
||||||
float splashDamage = HALF * damage - EXPLOSION_DAMAGE_FACTOR * finalThickness;
|
float splashDamage = 0.5 * damage - EXPLOSION_DAMAGE_FACTOR * finalThickness;
|
||||||
splashChance = step(0.0, splashDamage);
|
splashChance = step(0.0, splashDamage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float alpha = opaque ? 1.0 : HALF;
|
float alpha = opaque ? 1.0 : 0.5;
|
||||||
vec3 baseColor = vec3(1.0, splashChance * GREEN_VALUE, 0.0);
|
vec3 baseColor = vec3(1.0, splashChance * GREEN_VALUE, 0.0);
|
||||||
|
|
||||||
if (advancedHighlighting && didRicochet) {
|
if (advancedHighlighting && didRicochet) {
|
||||||
baseColor = vec3(1.0, baseColor.g, 1.0);
|
baseColor = vec3(1.0, baseColor.g, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (greenPenetration || advancedHighlighting) {
|
if (greenPenetration || advancedHighlighting) {
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
|||||||
const tankModelDefinition = useTankModelDefinition();
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
turretModelDefinition.guns[gun.gun_type!.value.base.id];
|
|
||||||
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
||||||
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
||||||
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { normalizeAngleRad } from '@blitzkit/core';
|
|
||||||
import { useThree, type ThreeEvent } from '@react-three/fiber';
|
import { useThree, type ThreeEvent } from '@react-three/fiber';
|
||||||
import { memo, useRef } from 'react';
|
import { memo, useRef } from 'react';
|
||||||
import { Group, Plane, Vector2, Vector3 } from 'three';
|
import { Group, Plane, Vector2, Vector3 } from 'three';
|
||||||
@@ -43,8 +42,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
const tankModelDefinition = useTankModelDefinition();
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
turretModelDefinition.guns[gun.gun_type!.value.base.id];
|
|
||||||
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
||||||
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
||||||
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
||||||
@@ -153,15 +151,11 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
|
|
||||||
const position = new Vector2();
|
const position = new Vector2();
|
||||||
const delta = new Vector2();
|
const delta = new Vector2();
|
||||||
let pitch = 0;
|
|
||||||
let yaw = 0;
|
|
||||||
|
|
||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
yaw = protagonist.yaw;
|
|
||||||
pitch = protagonist.pitch;
|
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
mutateTankopediaEphemeral((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
@@ -190,21 +184,18 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
delta.set(event.clientX, event.clientY).sub(position);
|
delta.set(event.clientX, event.clientY).sub(position);
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
|
|
||||||
[pitch, yaw] = applyPitchYawLimits(
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
pitch,
|
modelTransformEvent.last!.pitch,
|
||||||
yaw + delta.x * (Math.PI / boundingRect.width),
|
modelTransformEvent.last!.yaw +
|
||||||
|
delta.x * (Math.PI / boundingRect.width),
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateDuel((draft) => {
|
|
||||||
draft.protagonist.pitch = normalizeAngleRad(pitch);
|
|
||||||
draft.protagonist.yaw = normalizeAngleRad(yaw);
|
|
||||||
});
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
mutateTankopediaEphemeral((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
@@ -253,8 +244,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
|
|
||||||
const position = new Vector2();
|
const position = new Vector2();
|
||||||
const delta = new Vector2();
|
const delta = new Vector2();
|
||||||
let pitch = 0;
|
|
||||||
let yaw = 0;
|
|
||||||
|
|
||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -268,8 +257,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
pitch = protagonist.pitch;
|
|
||||||
yaw = protagonist.yaw;
|
|
||||||
|
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener('pointermove', handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener('pointerup', handlePointerUp);
|
||||||
@@ -290,24 +277,22 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
delta.set(event.clientX, event.clientY).sub(position);
|
delta.set(event.clientX, event.clientY).sub(position);
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
|
|
||||||
[pitch, yaw] = applyPitchYawLimits(
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
pitch - delta.y * (Math.PI / boundingRect.height),
|
modelTransformEvent.last!.pitch -
|
||||||
yaw + delta.x * (Math.PI / boundingRect.width),
|
delta.y * (Math.PI / boundingRect.height),
|
||||||
|
modelTransformEvent.last!.yaw +
|
||||||
|
delta.x * (Math.PI / boundingRect.width),
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
mutateTankopediaEphemeral((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
mutateDuel((draft) => {
|
|
||||||
draft.protagonist.pitch = normalizeAngleRad(pitch);
|
|
||||||
draft.protagonist.yaw = normalizeAngleRad(yaw);
|
|
||||||
});
|
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener('pointerup', handlePointerUp);
|
||||||
}
|
}
|
||||||
@@ -341,8 +326,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
|
|
||||||
const position = new Vector2();
|
const position = new Vector2();
|
||||||
const delta = new Vector2();
|
const delta = new Vector2();
|
||||||
let pitch = 0;
|
|
||||||
let yaw = 0;
|
|
||||||
|
|
||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -356,9 +339,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
pitch = protagonist.pitch;
|
|
||||||
yaw = protagonist.yaw;
|
|
||||||
|
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener('pointermove', handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener('pointerup', handlePointerUp);
|
||||||
}
|
}
|
||||||
@@ -378,24 +358,23 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
delta.set(event.clientX, event.clientY).sub(position);
|
delta.set(event.clientX, event.clientY).sub(position);
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
|
|
||||||
[pitch, yaw] = applyPitchYawLimits(
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
pitch - delta.y * (Math.PI / boundingRect.height),
|
modelTransformEvent.last!.pitch -
|
||||||
yaw + delta.x * (Math.PI / boundingRect.width),
|
delta.y * (Math.PI / boundingRect.height),
|
||||||
|
modelTransformEvent.last!.yaw +
|
||||||
|
delta.x * (Math.PI / boundingRect.width),
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
mutateTankopediaEphemeral((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
mutateDuel((draft) => {
|
|
||||||
draft.protagonist.pitch = normalizeAngleRad(pitch);
|
|
||||||
draft.protagonist.yaw = normalizeAngleRad(yaw);
|
|
||||||
});
|
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener('pointerup', handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { I_HAT, J_HAT } from '@blitzkit/core';
|
import { I_HAT, J_HAT } from '@blitzkit/core';
|
||||||
import { type MeshProps, useThree } from '@react-three/fiber';
|
import { type MeshProps, useThree } from '@react-three/fiber';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
|
import type { QuicklimeEvent } from 'quicklime';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Box3,
|
Box3,
|
||||||
@@ -186,16 +187,18 @@ export function StaticArmorSceneComponent({
|
|||||||
const distanceToBarrel = clip.distanceToPoint(barrelOrigin);
|
const distanceToBarrel = clip.distanceToPoint(barrelOrigin);
|
||||||
const point = new Vector3();
|
const point = new Vector3();
|
||||||
|
|
||||||
function handleModelTransform(data: ModelTransformEventData) {
|
function handleModelTransform(
|
||||||
|
event: QuicklimeEvent<ModelTransformEventData>,
|
||||||
|
) {
|
||||||
clip.normal
|
clip.normal
|
||||||
.set(0, 0, -1)
|
.set(0, 0, -1)
|
||||||
.applyAxisAngle(I_HAT, data.pitch)
|
.applyAxisAngle(I_HAT, event.data.pitch)
|
||||||
.applyAxisAngle(J_HAT, data.yaw ?? 0);
|
.applyAxisAngle(J_HAT, event.data.yaw);
|
||||||
clip.constant = 0;
|
clip.constant = 0;
|
||||||
|
|
||||||
point
|
point
|
||||||
.copy(gunOrigin)
|
.copy(gunOrigin)
|
||||||
.applyAxisAngle(J_HAT, data.yaw ?? 0)
|
.applyAxisAngle(J_HAT, event.data.yaw)
|
||||||
.add(clip.normal.multiplyScalar(-distanceToBarrel))
|
.add(clip.normal.multiplyScalar(-distanceToBarrel))
|
||||||
.add(neckOrigin);
|
.add(neckOrigin);
|
||||||
clip.normal.normalize();
|
clip.normal.normalize();
|
||||||
|
|||||||
@@ -33,43 +33,35 @@ export function Body({ stats }: BodyProps) {
|
|||||||
<StickyColumnHeaderCell key={key} top={137}>
|
<StickyColumnHeaderCell key={key} top={137}>
|
||||||
<Flex justify="center">
|
<Flex justify="center">
|
||||||
<Flex>
|
<Flex>
|
||||||
{gun.gun_type!.value.base.shells.map(
|
{gun.shells.map((thisShell, shellIndex) => (
|
||||||
(thisShell, shellIndex) => (
|
<IconButton
|
||||||
<IconButton
|
color={thisShell.id === shell.id ? undefined : 'gray'}
|
||||||
color={thisShell.id === shell.id ? undefined : 'gray'}
|
variant="soft"
|
||||||
variant="soft"
|
key={thisShell.id}
|
||||||
key={thisShell.id}
|
style={{
|
||||||
style={{
|
borderTopLeftRadius: shellIndex === 0 ? undefined : 0,
|
||||||
borderTopLeftRadius: shellIndex === 0 ? undefined : 0,
|
borderBottomLeftRadius:
|
||||||
borderBottomLeftRadius:
|
shellIndex === 0 ? undefined : 0,
|
||||||
shellIndex === 0 ? undefined : 0,
|
borderTopRightRadius:
|
||||||
borderTopRightRadius:
|
shellIndex === gun.shells.length - 1 ? undefined : 0,
|
||||||
shellIndex ===
|
borderBottomRightRadius:
|
||||||
gun.gun_type!.value.base.shells.length - 1
|
shellIndex === gun.shells.length - 1 ? undefined : 0,
|
||||||
? undefined
|
marginLeft: shellIndex === 0 ? 0 : -1,
|
||||||
: 0,
|
}}
|
||||||
borderBottomRightRadius:
|
onClick={() => {
|
||||||
shellIndex ===
|
mutateCompareEphemeral((draft) => {
|
||||||
gun.gun_type!.value.base.shells.length - 1
|
draft.members[index].shell = thisShell;
|
||||||
? undefined
|
});
|
||||||
: 0,
|
}}
|
||||||
marginLeft: shellIndex === 0 ? 0 : -1,
|
>
|
||||||
}}
|
<img
|
||||||
onClick={() => {
|
alt={unwrap(thisShell.name)}
|
||||||
mutateCompareEphemeral((draft) => {
|
width={16}
|
||||||
draft.members[index].shell = thisShell;
|
height={16}
|
||||||
});
|
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
||||||
}}
|
/>
|
||||||
>
|
</IconButton>
|
||||||
<img
|
))}
|
||||||
alt={unwrap(thisShell.name)}
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</StickyColumnHeaderCell>
|
</StickyColumnHeaderCell>
|
||||||
|
|||||||
@@ -250,8 +250,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
|
|
||||||
member.turret = member.tank.turrets[0];
|
member.turret = member.tank.turrets[0];
|
||||||
member.gun = member.turret.guns[0];
|
member.gun = member.turret.guns[0];
|
||||||
member.shell =
|
member.shell = member.gun.shells[0];
|
||||||
member.gun.gun_type!.value.base.shells[0];
|
|
||||||
member.engine = member.tank.engines[0];
|
member.engine = member.tank.engines[0];
|
||||||
member.track = member.tank.tracks[0];
|
member.track = member.tank.tracks[0];
|
||||||
});
|
});
|
||||||
@@ -267,8 +266,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
|
|
||||||
member.turret = member.tank.turrets.at(-1)!;
|
member.turret = member.tank.turrets.at(-1)!;
|
||||||
member.gun = member.turret.guns.at(-1)!;
|
member.gun = member.turret.guns.at(-1)!;
|
||||||
member.shell =
|
member.shell = member.gun.shells[0];
|
||||||
member.gun.gun_type!.value.base.shells[0];
|
|
||||||
member.engine = member.tank.engines.at(-1)!;
|
member.engine = member.tank.engines.at(-1)!;
|
||||||
member.track = member.tank.tracks.at(-1)!;
|
member.track = member.tank.tracks.at(-1)!;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Canvas } from '@react-three/fiber';
|
|
||||||
import { GuessEphemeral } from '../stores/guessEphemeral';
|
import { GuessEphemeral } from '../stores/guessEphemeral';
|
||||||
import { TankopediaDisplay } from '../stores/tankopediaPersistent/constants';
|
import { TankopediaDisplay } from '../stores/tankopediaPersistent/constants';
|
||||||
|
import { SmartCanvas } from './SmartCanvas';
|
||||||
import { Controls } from './Tankopedia/HeroSection/components/TankSandbox/components/Control';
|
import { Controls } from './Tankopedia/HeroSection/components/TankSandbox/components/Control';
|
||||||
import { Lighting } from './Tankopedia/HeroSection/components/TankSandbox/components/Lighting';
|
import { Lighting } from './Tankopedia/HeroSection/components/TankSandbox/components/Lighting';
|
||||||
import { TankModel } from './Tankopedia/HeroSection/components/TankSandbox/components/TankModel';
|
import { TankModel } from './Tankopedia/HeroSection/components/TankSandbox/components/TankModel';
|
||||||
@@ -13,7 +13,7 @@ export function GuessRenderer() {
|
|||||||
const isRevealed = guessState !== null;
|
const isRevealed = guessState !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Canvas
|
<SmartCanvas
|
||||||
style={{
|
style={{
|
||||||
filter: `grayscale(${isRevealed ? 0 : 1}) contrast(${isRevealed ? 1 : CONTRAST}) brightness(${isRevealed ? 1 : BRIGHTNESS})`,
|
filter: `grayscale(${isRevealed ? 0 : 1}) contrast(${isRevealed ? 1 : CONTRAST}) brightness(${isRevealed ? 1 : BRIGHTNESS})`,
|
||||||
// filter: `drop-shadow(0 0 1rem ${Var('black-a11')})`,
|
// filter: `drop-shadow(0 0 1rem ${Var('black-a11')})`,
|
||||||
@@ -25,6 +25,6 @@ export function GuessRenderer() {
|
|||||||
<Controls distanceScale={2} zoomable={false} />
|
<Controls distanceScale={2} zoomable={false} />
|
||||||
<Lighting display={TankopediaDisplay.Model} />
|
<Lighting display={TankopediaDisplay.Model} />
|
||||||
<TankModel />
|
<TankModel />
|
||||||
</Canvas>
|
</SmartCanvas>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
import { Link, Table } from '@radix-ui/themes';
|
import { Link, Table } from '@radix-ui/themes';
|
||||||
import MarkdownToJSX from 'markdown-to-jsx';
|
import MarkdownToJSX from 'markdown-to-jsx';
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
interface MarkdownProps {
|
interface MarkdownProps {
|
||||||
children: string;
|
children: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Image(props: ComponentProps<'img'>) {
|
||||||
|
return <img style={{ maxWidth: '100%' }} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
export function Markdown({ children }: MarkdownProps) {
|
export function Markdown({ children }: MarkdownProps) {
|
||||||
return (
|
return (
|
||||||
<MarkdownToJSX
|
<MarkdownToJSX
|
||||||
@@ -17,6 +22,7 @@ export function Markdown({ children }: MarkdownProps) {
|
|||||||
tr: { component: Table.Row },
|
tr: { component: Table.Row },
|
||||||
th: { component: Table.ColumnHeaderCell },
|
th: { component: Table.ColumnHeaderCell },
|
||||||
td: { component: Table.Cell },
|
td: { component: Table.Cell },
|
||||||
|
img: { component: Image },
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function ModuleManager({
|
|||||||
const draft = { ...modules };
|
const draft = { ...modules };
|
||||||
draft.turret = turret;
|
draft.turret = turret;
|
||||||
draft.gun = draft.turret.guns.at(-1)!;
|
draft.gun = draft.turret.guns.at(-1)!;
|
||||||
draft.shell = draft.gun.gun_type!.value.base.shells[0];
|
draft.shell = draft.gun.shells[0];
|
||||||
|
|
||||||
onChange?.(draft);
|
onChange?.(draft);
|
||||||
setTurretMenuOpen(false);
|
setTurretMenuOpen(false);
|
||||||
@@ -83,9 +83,7 @@ export function ModuleManager({
|
|||||||
|
|
||||||
<ModuleButton
|
<ModuleButton
|
||||||
module="gun"
|
module="gun"
|
||||||
discriminator={
|
discriminator={TIER_ROMAN_NUMERALS[modules.gun.tier]}
|
||||||
TIER_ROMAN_NUMERALS[modules.gun.gun_type!.value.base.tier]
|
|
||||||
}
|
|
||||||
onClick={() => setGunMenuOpen(true)}
|
onClick={() => setGunMenuOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -95,18 +93,16 @@ export function ModuleManager({
|
|||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
<DropdownMenu.RadioGroup
|
<DropdownMenu.RadioGroup value={`${modules.gun.id}`}>
|
||||||
value={`${modules.gun.gun_type!.value.base.id}`}
|
|
||||||
>
|
|
||||||
{[...modules.turret.guns].reverse().map((gun) => {
|
{[...modules.turret.guns].reverse().map((gun) => {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.RadioItem
|
<DropdownMenu.RadioItem
|
||||||
key={gun.gun_type!.value.base.id}
|
key={gun.id}
|
||||||
value={`${gun.gun_type!.value.base.id}`}
|
value={`${gun.id}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const draft = { ...modules };
|
const draft = { ...modules };
|
||||||
draft.gun = gun;
|
draft.gun = gun;
|
||||||
draft.shell = draft.gun.gun_type!.value.base.shells[0];
|
draft.shell = draft.gun.shells[0];
|
||||||
|
|
||||||
onChange?.(draft);
|
onChange?.(draft);
|
||||||
setGunMenuOpen(false);
|
setGunMenuOpen(false);
|
||||||
@@ -115,11 +111,9 @@ export function ModuleManager({
|
|||||||
<ModuleButton
|
<ModuleButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
module="gun"
|
module="gun"
|
||||||
discriminator={
|
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
|
||||||
TIER_ROMAN_NUMERALS[gun.gun_type!.value.base.tier]
|
|
||||||
}
|
|
||||||
/>{' '}
|
/>{' '}
|
||||||
{unwrap(gun.gun_type!.value.base.name)}
|
{unwrap(gun.name)}
|
||||||
</DropdownMenu.RadioItem>
|
</DropdownMenu.RadioItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
import { Heading, Link, Text } from '@radix-ui/themes';
|
||||||
|
import { PageWrapper } from './PageWrapper';
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageWrapper align="center" justify="center" gap="0">
|
||||||
|
<Heading>BlitzKit down for servicing</Heading>
|
||||||
|
<Text color="gray">
|
||||||
|
<Link color="blue" href="https://discord.gg/nDt7AjGJQH">
|
||||||
|
Join the official Discord server
|
||||||
|
</Link>
|
||||||
|
for updates.
|
||||||
|
</Text>
|
||||||
|
</PageWrapper>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Canvas, type CanvasProps } from '@react-three/fiber';
|
||||||
|
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||||
|
import { useOnScreen } from '../hooks/useOnScreen';
|
||||||
|
|
||||||
|
export const SmartCanvas = forwardRef<HTMLCanvasElement, CanvasProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
const canvas = useRef<HTMLCanvasElement>(null);
|
||||||
|
const onScreen = useOnScreen(canvas);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => canvas.current!, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Canvas
|
||||||
|
frameloop={onScreen ? 'demand' : 'never'}
|
||||||
|
ref={canvas}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -25,19 +25,18 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
|||||||
|
|
||||||
const turret = tank.turrets.at(-1)!;
|
const turret = tank.turrets.at(-1)!;
|
||||||
const gun = turret.guns.at(-1)!;
|
const gun = turret.guns.at(-1)!;
|
||||||
const shell0 = gun.gun_type!.value.base.shells[0];
|
const shell0 = gun.shells[0];
|
||||||
const shell1 = gun.gun_type!.value.base.shells[1];
|
const shell1 = gun.shells[1];
|
||||||
const shell2 = gun.gun_type!.value.base.shells[2];
|
const shell2 = gun.shells[2];
|
||||||
const tracks = tank.tracks.at(-1)!;
|
const tracks = tank.tracks.at(-1)!;
|
||||||
const engine = tank.engines.at(-1)!;
|
const engine = tank.engines.at(-1)!;
|
||||||
const tankModelDefinition = modelDefinitions.models[tank.id];
|
const tankModelDefinition = modelDefinitions.models[tank.id];
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
turretModelDefinition.guns[gun.gun_type!.value.base.id];
|
|
||||||
|
|
||||||
switch (tankopediaSort.by) {
|
switch (tankopediaSort.by) {
|
||||||
case 'fire.aimTime':
|
case 'fire.aimTime':
|
||||||
return gun.gun_type!.value.base.aim_time.toFixed(2);
|
return gun.aim_time.toFixed(2);
|
||||||
case 'fire.caliber':
|
case 'fire.caliber':
|
||||||
return shell0.caliber.toFixed(0);
|
return shell0.caliber.toFixed(0);
|
||||||
case 'fire.damage':
|
case 'fire.damage':
|
||||||
@@ -47,7 +46,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
|||||||
case 'fire.dispersionMoving':
|
case 'fire.dispersionMoving':
|
||||||
return tracks.dispersion_move.toFixed(3);
|
return tracks.dispersion_move.toFixed(3);
|
||||||
case 'fire.dispersionStill':
|
case 'fire.dispersionStill':
|
||||||
return gun.gun_type!.value.base.dispersion_base.toFixed(3);
|
return gun.dispersion_base.toFixed(3);
|
||||||
case 'fire.dpm':
|
case 'fire.dpm':
|
||||||
return resolveDpm(gun, shell0).toFixed(0);
|
return resolveDpm(gun, shell0).toFixed(0);
|
||||||
case 'fire.dpmPremium':
|
case 'fire.dpmPremium':
|
||||||
@@ -63,7 +62,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
|||||||
case 'fire.shellVelocity':
|
case 'fire.shellVelocity':
|
||||||
return shell0.velocity.toFixed(0);
|
return shell0.velocity.toFixed(0);
|
||||||
case 'fire.shellCapacity':
|
case 'fire.shellCapacity':
|
||||||
return gun.gun_type!.value.base.shell_capacity.toFixed(0);
|
return gun.shell_capacity.toFixed(0);
|
||||||
case 'fire.shellRange':
|
case 'fire.shellRange':
|
||||||
return shell0.range.toFixed(0);
|
return shell0.range.toFixed(0);
|
||||||
case 'fire.gunDepression':
|
case 'fire.gunDepression':
|
||||||
@@ -89,7 +88,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
|||||||
engine.weight +
|
engine.weight +
|
||||||
tracks.weight +
|
tracks.weight +
|
||||||
turret.weight +
|
turret.weight +
|
||||||
gun.gun_type!.value.base.weight)
|
gun.weight)
|
||||||
).toFixed(1);
|
).toFixed(1);
|
||||||
case 'maneuverability.weight':
|
case 'maneuverability.weight':
|
||||||
return (
|
return (
|
||||||
@@ -97,7 +96,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
|||||||
engine.weight +
|
engine.weight +
|
||||||
tracks.weight +
|
tracks.weight +
|
||||||
turret.weight +
|
turret.weight +
|
||||||
gun.gun_type!.value.base.weight) /
|
gun.weight) /
|
||||||
1000
|
1000
|
||||||
).toFixed(1);
|
).toFixed(1);
|
||||||
case 'maneuverability.traverseSpeed':
|
case 'maneuverability.traverseSpeed':
|
||||||
@@ -111,11 +110,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
|||||||
case 'survivability.camouflageMoving':
|
case 'survivability.camouflageMoving':
|
||||||
return (tank.camouflage_moving * 100).toFixed(0);
|
return (tank.camouflage_moving * 100).toFixed(0);
|
||||||
case 'survivability.camouflageShooting':
|
case 'survivability.camouflageShooting':
|
||||||
return (
|
return (tank.camouflage_still * gun.camouflage_loss * 100).toFixed(0);
|
||||||
tank.camouflage_still *
|
|
||||||
gun.gun_type!.value.base.camouflage_loss *
|
|
||||||
100
|
|
||||||
).toFixed(0);
|
|
||||||
case 'survivability.volume': {
|
case 'survivability.volume': {
|
||||||
const dimensions = normalizeBoundingBox(
|
const dimensions = normalizeBoundingBox(
|
||||||
unionBoundingBox(
|
unionBoundingBox(
|
||||||
|
|||||||
@@ -104,11 +104,9 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.camouflage_still *
|
a.camouflage_still *
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base
|
a.turrets.at(-1)!.guns.at(-1)!.camouflage_loss -
|
||||||
.camouflage_loss -
|
|
||||||
b.camouflage_still *
|
b.camouflage_still *
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base
|
b.turrets.at(-1)!.guns.at(-1)!.camouflage_loss,
|
||||||
.camouflage_loss,
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -171,11 +169,11 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
(a, b) =>
|
(a, b) =>
|
||||||
resolveDpm(
|
resolveDpm(
|
||||||
a.turrets.at(-1)!.guns.at(-1)!,
|
a.turrets.at(-1)!.guns.at(-1)!,
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0],
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0],
|
||||||
) -
|
) -
|
||||||
resolveDpm(
|
resolveDpm(
|
||||||
b.turrets.at(-1)!.guns.at(-1)!,
|
b.turrets.at(-1)!.guns.at(-1)!,
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0],
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -183,18 +181,16 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
case 'fire.dpmPremium':
|
case 'fire.dpmPremium':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
(a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[1]
|
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
||||||
? resolveDpm(
|
? resolveDpm(
|
||||||
a.turrets.at(-1)!.guns.at(-1)!,
|
a.turrets.at(-1)!.guns.at(-1)!,
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base
|
a.turrets.at(-1)!.guns.at(-1)!.shells[1],
|
||||||
.shells[1],
|
|
||||||
)
|
)
|
||||||
: 0) -
|
: 0) -
|
||||||
(b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[1]
|
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
||||||
? resolveDpm(
|
? resolveDpm(
|
||||||
b.turrets.at(-1)!.guns.at(-1)!,
|
b.turrets.at(-1)!.guns.at(-1)!,
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base
|
b.turrets.at(-1)!.guns.at(-1)!.shells[1],
|
||||||
.shells[1],
|
|
||||||
)
|
)
|
||||||
: 0),
|
: 0),
|
||||||
);
|
);
|
||||||
@@ -211,108 +207,92 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
case 'fire.caliber':
|
case 'fire.caliber':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber -
|
||||||
.caliber -
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber,
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
|
||||||
.caliber,
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.standardPenetration':
|
case 'fire.standardPenetration':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near -
|
||||||
.penetration.near -
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near,
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
|
||||||
.penetration.near,
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.premiumPenetration':
|
case 'fire.premiumPenetration':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
(a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[1]
|
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration.near ??
|
||||||
?.penetration.near ?? 0) -
|
0) -
|
||||||
(b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[1]
|
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration.near ??
|
||||||
?.penetration.near ?? 0),
|
0),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.tertiaryPenetration':
|
case 'fire.tertiaryPenetration':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
(a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[2]
|
(a.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration.near ??
|
||||||
?.penetration.near ?? 0) -
|
0) -
|
||||||
(b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[2]
|
(b.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration.near ??
|
||||||
?.penetration.near ?? 0),
|
0),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.damage':
|
case 'fire.damage':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage -
|
||||||
.armor_damage -
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage,
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
|
||||||
.armor_damage,
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.moduleDamage':
|
case 'fire.moduleDamage':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage -
|
||||||
.module_damage -
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage,
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
|
||||||
.module_damage,
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.shellVelocity':
|
case 'fire.shellVelocity':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity -
|
||||||
.velocity -
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity,
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
|
||||||
.velocity,
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.shellRange':
|
case 'fire.shellRange':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0].range -
|
||||||
.range -
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0].range,
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
|
||||||
.range,
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.shellCapacity':
|
case 'fire.shellCapacity':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base
|
a.turrets.at(-1)!.guns.at(-1)!.shell_capacity -
|
||||||
.shell_capacity -
|
b.turrets.at(-1)!.guns.at(-1)!.shell_capacity,
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base
|
|
||||||
.shell_capacity,
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.aimTime':
|
case 'fire.aimTime':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.aim_time -
|
a.turrets.at(-1)!.guns.at(-1)!.aim_time -
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.aim_time,
|
b.turrets.at(-1)!.guns.at(-1)!.aim_time,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.dispersionStill':
|
case 'fire.dispersionStill':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base
|
a.turrets.at(-1)!.guns.at(-1)!.dispersion_base -
|
||||||
.dispersion_base -
|
b.turrets.at(-1)!.guns.at(-1)!.dispersion_base,
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base
|
|
||||||
.dispersion_base,
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -328,13 +308,11 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
|
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
|
||||||
.guns[a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.id]
|
.guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch.max +
|
||||||
.pitch.max +
|
|
||||||
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
|
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
|
||||||
0) -
|
0) -
|
||||||
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
|
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
|
||||||
.guns[b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.id]
|
.guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch.max -
|
||||||
.pitch.max -
|
|
||||||
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
|
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
|
||||||
0),
|
0),
|
||||||
);
|
);
|
||||||
@@ -344,13 +322,11 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
|
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
|
||||||
.guns[b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.id]
|
.guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch.min +
|
||||||
.pitch.min +
|
|
||||||
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
|
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
|
||||||
0) -
|
0) -
|
||||||
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
|
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
|
||||||
.guns[a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.id]
|
.guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch.min -
|
||||||
.pitch.min -
|
|
||||||
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
|
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
|
||||||
0),
|
0),
|
||||||
);
|
);
|
||||||
@@ -382,14 +358,13 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
a.engines.at(-1)!.weight +
|
a.engines.at(-1)!.weight +
|
||||||
a.tracks.at(-1)!.weight +
|
a.tracks.at(-1)!.weight +
|
||||||
a.turrets.at(-1)!.weight +
|
a.turrets.at(-1)!.weight +
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base
|
a.turrets.at(-1)!.guns.at(-1)!.weight) -
|
||||||
.weight) -
|
|
||||||
b.engines.at(-1)!.power /
|
b.engines.at(-1)!.power /
|
||||||
(b.weight +
|
(b.weight +
|
||||||
b.engines.at(-1)!.weight +
|
b.engines.at(-1)!.weight +
|
||||||
b.tracks.at(-1)!.weight +
|
b.tracks.at(-1)!.weight +
|
||||||
b.turrets.at(-1)!.weight +
|
b.turrets.at(-1)!.weight +
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.weight),
|
b.turrets.at(-1)!.guns.at(-1)!.weight),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -400,12 +375,12 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
a.engines.at(-1)!.weight +
|
a.engines.at(-1)!.weight +
|
||||||
a.tracks.at(-1)!.weight +
|
a.tracks.at(-1)!.weight +
|
||||||
a.turrets.at(-1)!.weight +
|
a.turrets.at(-1)!.weight +
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.weight -
|
a.turrets.at(-1)!.guns.at(-1)!.weight -
|
||||||
(b.weight +
|
(b.weight +
|
||||||
b.engines.at(-1)!.weight +
|
b.engines.at(-1)!.weight +
|
||||||
b.tracks.at(-1)!.weight +
|
b.tracks.at(-1)!.weight +
|
||||||
b.turrets.at(-1)!.weight +
|
b.turrets.at(-1)!.weight +
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.weight),
|
b.turrets.at(-1)!.guns.at(-1)!.weight),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -1,6 +1,6 @@
|
|||||||
import type { AssaultRanges } from '@blitzkit/core';
|
import type { AssaultRanges } from '@blitzkit/core';
|
||||||
import { literals } from '@blitzkit/i18n';
|
import { literals } from '@blitzkit/i18n';
|
||||||
import { Box, Card, Flex, Text } from '@radix-ui/themes';
|
import { Box, Flex, Text } from '@radix-ui/themes';
|
||||||
import { clamp } from 'three/src/math/MathUtils.js';
|
import { clamp } from 'three/src/math/MathUtils.js';
|
||||||
import {
|
import {
|
||||||
fakeLogSquishyHigh,
|
fakeLogSquishyHigh,
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import type { TankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
|
import type { TankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
|
||||||
import { Var } from '../../../../../../../core/radix/var';
|
import { Var } from '../../../../../../../core/radix/var';
|
||||||
import { useLocale } from '../../../../../../../hooks/useLocale';
|
import { useLocale } from '../../../../../../../hooks/useLocale';
|
||||||
|
import { VisualizerCard } from '../VisualizerCard';
|
||||||
import { AssaultCursor } from './components/Cursor';
|
import { AssaultCursor } from './components/Cursor';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -29,7 +30,7 @@ export function AssaultRangesVisualizer({ ranges: _ranges, stats }: Props) {
|
|||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card mb="6" variant="classic" style={{ aspectRatio: '1 / 1' }}>
|
<VisualizerCard>
|
||||||
<Flex
|
<Flex
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top="0"
|
top="0"
|
||||||
@@ -152,6 +153,6 @@ export function AssaultRangesVisualizer({ ranges: _ranges, stats }: Props) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</VisualizerCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+20
-95
@@ -5,37 +5,36 @@ import {
|
|||||||
resolvePenetrationCoefficient,
|
resolvePenetrationCoefficient,
|
||||||
} from '@blitzkit/core';
|
} from '@blitzkit/core';
|
||||||
import { literals } from '@blitzkit/i18n';
|
import { literals } from '@blitzkit/i18n';
|
||||||
import {
|
import { Flex, Heading, IconButton, Tooltip } from '@radix-ui/themes';
|
||||||
Flex,
|
|
||||||
Heading,
|
|
||||||
IconButton,
|
|
||||||
Slider,
|
|
||||||
Text,
|
|
||||||
TextField,
|
|
||||||
Tooltip,
|
|
||||||
} from '@radix-ui/themes';
|
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { lerp } from 'three/src/math/MathUtils.js';
|
|
||||||
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||||
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
|
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
|
||||||
import { Duel } from '../../../../../../stores/duel';
|
import { Duel } from '../../../../../../stores/duel';
|
||||||
|
import type { MaybeSkeletonComponentProps } from '../../../../../../types/maybeSkeletonComponentProps';
|
||||||
import { AssaultRangesVisualizer } from './AssaultRangesVisualizer';
|
import { AssaultRangesVisualizer } from './AssaultRangesVisualizer';
|
||||||
|
import { GunFlexibilityVisualizer } from './GunFlexibilityVisualizer';
|
||||||
import { Info } from './Info';
|
import { Info } from './Info';
|
||||||
import { InfoWithDelta } from './InfoWithDelta';
|
import { InfoWithDelta } from './InfoWithDelta';
|
||||||
|
import { ReloadVisualizer } from './ReloadVisualizer';
|
||||||
import { RicochetVisualizer } from './RicochetVisualizer';
|
import { RicochetVisualizer } from './RicochetVisualizer';
|
||||||
import { StatsTableWrapper } from './StatsTableWrapper';
|
import { StatsTableWrapper } from './StatsTableWrapper';
|
||||||
import type { StatsAcceptorProps } from './TraverseVisualizer';
|
import type { StatsAcceptorProps } from './TraverseVisualizer';
|
||||||
|
|
||||||
export function Firepower({ stats }: StatsAcceptorProps) {
|
export function Firepower({
|
||||||
|
stats,
|
||||||
|
skeleton,
|
||||||
|
}: StatsAcceptorProps & MaybeSkeletonComponentProps) {
|
||||||
const mutateDuel = Duel.useMutation();
|
const mutateDuel = Duel.useMutation();
|
||||||
const { strings, unwrap } = useLocale();
|
const { strings, unwrap } = useLocale();
|
||||||
const [penetrationDistance, setPenetrationDistance] = useState(250);
|
const [penetrationDistance, setPenetrationDistance] = useState(250);
|
||||||
const setPenetrationDistanceDebounced = debounce((value: number) => {
|
const setPenetrationDistanceDebounced = debounce((value: number) => {
|
||||||
setPenetrationDistance(value);
|
setPenetrationDistance(value);
|
||||||
}, 500);
|
}, 500);
|
||||||
const { gun, shell, turret } = Duel.use((state) => state.protagonist);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
|
const shell = Duel.use((state) => state.protagonist.shell);
|
||||||
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
|
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
|
||||||
const hasSupercharger = useEquipment(107);
|
const hasSupercharger = useEquipment(107);
|
||||||
const hasCalibratedShells = useEquipment(103);
|
const hasCalibratedShells = useEquipment(103);
|
||||||
@@ -49,8 +48,7 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
|||||||
]);
|
]);
|
||||||
const tankModelDefinition = useTankModelDefinition();
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
turretModelDefinition.guns[gun.gun_type!.value.base.id];
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (penetrationDistanceInput.current) {
|
if (penetrationDistanceInput.current) {
|
||||||
@@ -66,7 +64,7 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
|||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Flex>
|
<Flex>
|
||||||
{gun.gun_type!.value.base.shells.map((thisShell, shellIndex) => {
|
{gun.shells.map((thisShell, shellIndex) => {
|
||||||
const selected = thisShell.id === shell.id;
|
const selected = thisShell.id === shell.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -79,13 +77,9 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
|||||||
borderTopLeftRadius: shellIndex === 0 ? undefined : 0,
|
borderTopLeftRadius: shellIndex === 0 ? undefined : 0,
|
||||||
borderBottomLeftRadius: shellIndex === 0 ? undefined : 0,
|
borderBottomLeftRadius: shellIndex === 0 ? undefined : 0,
|
||||||
borderTopRightRadius:
|
borderTopRightRadius:
|
||||||
shellIndex === gun.gun_type!.value.base.shells.length - 1
|
shellIndex === gun.shells.length - 1 ? undefined : 0,
|
||||||
? undefined
|
|
||||||
: 0,
|
|
||||||
borderBottomRightRadius:
|
borderBottomRightRadius:
|
||||||
shellIndex === gun.gun_type!.value.base.shells.length - 1
|
shellIndex === gun.shells.length - 1 ? undefined : 0,
|
||||||
? undefined
|
|
||||||
: 0,
|
|
||||||
marginLeft: shellIndex === 0 ? 0 : -1,
|
marginLeft: shellIndex === 0 ? 0 : -1,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -120,11 +114,8 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
|||||||
<InfoWithDelta stats={stats} decimals={0} value="moduleDamage" />
|
<InfoWithDelta stats={stats} decimals={0} value="moduleDamage" />
|
||||||
<InfoWithDelta stats={stats} decimals={0} value="penetration" />
|
<InfoWithDelta stats={stats} decimals={0} value="penetration" />
|
||||||
|
|
||||||
{gun.gun_type!.value.base.assault_ranges?.types.includes(shell.type) && (
|
{gun.assault_ranges?.types.includes(shell.type) && (
|
||||||
<AssaultRangesVisualizer
|
<AssaultRangesVisualizer stats={stats} ranges={gun.assault_ranges} />
|
||||||
stats={stats}
|
|
||||||
ranges={gun.gun_type!.value.base.assault_ranges}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{gun.gun_type!.$case === 'auto_reloader' && (
|
{gun.gun_type!.$case === 'auto_reloader' && (
|
||||||
@@ -185,75 +176,8 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
|||||||
value="intraClip"
|
value="intraClip"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* <ReloadVisualizer stats={stats} /> */}
|
<ReloadVisualizer stats={stats} />
|
||||||
|
|
||||||
{typeof shell.penetration !== 'number' && (
|
|
||||||
<>
|
|
||||||
<InfoWithDelta
|
|
||||||
stats={stats}
|
|
||||||
indent
|
|
||||||
decimals={0}
|
|
||||||
name={literals(
|
|
||||||
strings.website.tools.tankopedia.characteristics.values
|
|
||||||
.at_distance,
|
|
||||||
[`${penetrationDistance}`],
|
|
||||||
)}
|
|
||||||
noRanking
|
|
||||||
value={() =>
|
|
||||||
lerp(
|
|
||||||
shell.penetration.near,
|
|
||||||
shell.penetration.far,
|
|
||||||
(penetrationDistance * penetrationLossOverDistanceCoefficient) /
|
|
||||||
500,
|
|
||||||
) * penetrationCoefficient
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Flex align="center" gap="2" style={{ paddingLeft: 24 }}>
|
|
||||||
<Text color="gray">
|
|
||||||
{
|
|
||||||
strings.website.tools.tankopedia.characteristics.values
|
|
||||||
.at_distance_slider_label
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
<Slider
|
|
||||||
variant="classic"
|
|
||||||
key={penetrationDistance}
|
|
||||||
min={0}
|
|
||||||
max={500}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
defaultValue={[penetrationDistance]}
|
|
||||||
onValueChange={([value]) => {
|
|
||||||
setPenetrationDistanceDebounced(value);
|
|
||||||
|
|
||||||
if (!penetrationDistanceInput.current) return;
|
|
||||||
|
|
||||||
penetrationDistanceInput.current.value = value.toString();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextField.Root
|
|
||||||
variant="classic"
|
|
||||||
style={{ width: 64 }}
|
|
||||||
ref={penetrationDistanceInput}
|
|
||||||
defaultValue={penetrationDistance}
|
|
||||||
onBlur={() => {
|
|
||||||
setPenetrationDistance(
|
|
||||||
Math.min(
|
|
||||||
parseInt(penetrationDistanceInput.current!.value),
|
|
||||||
500,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.currentTarget.blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextField.Slot side="right">m</TextField.Slot>
|
|
||||||
</TextField.Root>
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{isExplosive(shell.type) && (
|
{isExplosive(shell.type) && (
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
stats={stats}
|
stats={stats}
|
||||||
@@ -342,7 +266,7 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
|||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
stats={stats}
|
stats={stats}
|
||||||
decimals={1}
|
decimals={1}
|
||||||
prefix="x "
|
prefix="x"
|
||||||
indent
|
indent
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
value="dispersionGunDamaged"
|
value="dispersionGunDamaged"
|
||||||
@@ -403,6 +327,7 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<GunFlexibilityVisualizer skeleton={skeleton} />
|
||||||
</StatsTableWrapper>
|
</StatsTableWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
|
||||||
|
export const zoom = 60;
|
||||||
|
|
||||||
|
export function CameraPositioner() {
|
||||||
|
useFrame(({ camera, gl: { domElement } }) => {
|
||||||
|
const { width } = domElement.getBoundingClientRect();
|
||||||
|
const physicalWidth = width / zoom;
|
||||||
|
|
||||||
|
camera.position.set(10, 0, -physicalWidth / 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
import type { QuicklimeEvent } from 'quicklime';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import type { Group } from 'three';
|
||||||
|
import { awaitableModelDefinitions } from '../../../../../../../../core/awaitables/modelDefinitions';
|
||||||
|
import {
|
||||||
|
modelTransformEvent,
|
||||||
|
type ModelTransformEventData,
|
||||||
|
} from '../../../../../../../../core/blitzkit/modelTransform';
|
||||||
|
import { Duel } from '../../../../../../../../stores/duel';
|
||||||
|
import { ModelChunk } from './ModelChunk';
|
||||||
|
|
||||||
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
|
|
||||||
|
export function CanvasContent() {
|
||||||
|
const hullWrapper = useRef<Group>(null);
|
||||||
|
const gunWrapper = useRef<Group>(null);
|
||||||
|
|
||||||
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
|
|
||||||
|
const tankModel = modelDefinitions.models[tank.id];
|
||||||
|
const turretModel = tankModel.turrets[turret.id];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleModelTransformEvent(
|
||||||
|
event: QuicklimeEvent<ModelTransformEventData>,
|
||||||
|
) {
|
||||||
|
if (!gunWrapper.current || !hullWrapper.current) return;
|
||||||
|
|
||||||
|
gunWrapper.current.rotation.x = event.data.pitch;
|
||||||
|
if (event.data.yaw) hullWrapper.current.rotation.y = event.data.yaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
modelTransformEvent.on(handleModelTransformEvent);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
modelTransformEvent.off(handleModelTransformEvent);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<group ref={hullWrapper} position={[0, 0, turretModel.gun_origin.z]}>
|
||||||
|
<group position={[0, 0, -turretModel.gun_origin.z]}>
|
||||||
|
<ModelChunk only="body" />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<ModelChunk only="turret" />
|
||||||
|
|
||||||
|
<group ref={gunWrapper}>
|
||||||
|
<ModelChunk only="gun" />
|
||||||
|
</group>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
import { SmartCanvas } from '../../../../../../../SmartCanvas';
|
||||||
|
import { CameraPositioner, zoom } from './CameraPositioner';
|
||||||
|
import { CanvasContent } from './CanvasContent';
|
||||||
|
|
||||||
|
export function FlexibilityCanvas() {
|
||||||
|
return (
|
||||||
|
<SmartCanvas orthographic camera={{ zoom, rotation: [0, Math.PI / 2, 0] }}>
|
||||||
|
<CanvasContent />
|
||||||
|
<CameraPositioner />
|
||||||
|
</SmartCanvas>
|
||||||
|
);
|
||||||
|
}
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
import { mauveDark } from '@radix-ui/colors';
|
||||||
|
import { LineBasicMaterial, MeshBasicMaterial } from 'three';
|
||||||
|
import { awaitableModelDefinitions } from '../../../../../../../../core/awaitables/modelDefinitions';
|
||||||
|
import { jsxTree } from '../../../../../../../../core/blitzkit/jsxTree';
|
||||||
|
import { useModel } from '../../../../../../../../hooks/useModel';
|
||||||
|
import { Duel } from '../../../../../../../../stores/duel';
|
||||||
|
|
||||||
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
|
|
||||||
|
const surfaceMaterial: Record<Props['only'], MeshBasicMaterial> = {
|
||||||
|
gun: new MeshBasicMaterial({
|
||||||
|
color: mauveDark.mauve8,
|
||||||
|
toneMapped: false,
|
||||||
|
}),
|
||||||
|
turret: new MeshBasicMaterial({
|
||||||
|
color: mauveDark.mauve6,
|
||||||
|
toneMapped: false,
|
||||||
|
}),
|
||||||
|
body: new MeshBasicMaterial({
|
||||||
|
color: mauveDark.mauve6,
|
||||||
|
toneMapped: false,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.5,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const outlineMaterial: Record<Props['only'], LineBasicMaterial> = {
|
||||||
|
gun: new LineBasicMaterial({
|
||||||
|
color: mauveDark.mauve11,
|
||||||
|
toneMapped: false,
|
||||||
|
}),
|
||||||
|
turret: new LineBasicMaterial({
|
||||||
|
color: mauveDark.mauve8,
|
||||||
|
toneMapped: false,
|
||||||
|
}),
|
||||||
|
body: new LineBasicMaterial({
|
||||||
|
color: mauveDark.mauve8,
|
||||||
|
toneMapped: false,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.5,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
only: 'gun' | 'turret' | 'body';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModelChunk({ only }: Props) {
|
||||||
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
|
|
||||||
|
const tankModel = modelDefinitions.models[tank.id];
|
||||||
|
const turretModel = tankModel.turrets[turret.id];
|
||||||
|
const gunModel = turretModel.guns[gun.id];
|
||||||
|
|
||||||
|
const originZ = tankModel.turret_origin.z + turretModel.gun_origin.z;
|
||||||
|
const originY = tankModel.turret_origin.y + turretModel.gun_origin.y;
|
||||||
|
|
||||||
|
const { gltf } = useModel(tank.id);
|
||||||
|
const nodes = Object.values(gltf.nodes);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group position={[0, -originY, originZ]} rotation={[-Math.PI / 2, 0, 0]}>
|
||||||
|
{nodes.map((node) => {
|
||||||
|
const isCurrentMantlet =
|
||||||
|
node.name ===
|
||||||
|
`gun_${gunModel.model_id.toString().padStart(2, '0')}_mask`;
|
||||||
|
const isCurrentGun =
|
||||||
|
node.name === `gun_${gunModel.model_id.toString().padStart(2, '0')}`;
|
||||||
|
const isGun = isCurrentGun || isCurrentMantlet;
|
||||||
|
|
||||||
|
const isCurrentTurret =
|
||||||
|
node.name ===
|
||||||
|
`turret_${turretModel.model_id.toString().padStart(2, '0')}`;
|
||||||
|
const isTurret = isCurrentTurret;
|
||||||
|
|
||||||
|
const isHull = node.name === 'hull';
|
||||||
|
const isWheel = node.name.startsWith('chassis_wheel_');
|
||||||
|
const isTrack = node.name.startsWith('chassis_track_');
|
||||||
|
const isBody = isHull || isWheel || isTrack;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(only === 'gun' && !isGun) ||
|
||||||
|
(only === 'turret' && !isTurret) ||
|
||||||
|
(only === 'body' && !isBody)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsxTree(node, {
|
||||||
|
mesh(mesh, props, key) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<mesh key={key} {...props} material={surfaceMaterial[only]} />
|
||||||
|
<lineSegments material={outlineMaterial[only]}>
|
||||||
|
<edgesGeometry args={[mesh.geometry, 29]} />
|
||||||
|
</lineSegments>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
+397
@@ -0,0 +1,397 @@
|
|||||||
|
import { literals } from '@blitzkit/i18n';
|
||||||
|
import { Box, Card, Flex } from '@radix-ui/themes';
|
||||||
|
import { clamp } from 'lodash-es';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { degToRad, radToDeg } from 'three/src/math/MathUtils.js';
|
||||||
|
import { awaitableModelDefinitions } from '../../../../../../../core/awaitables/modelDefinitions';
|
||||||
|
import {
|
||||||
|
DEFAULT_PITCH_TRANSITION,
|
||||||
|
applyPitchYawLimits,
|
||||||
|
} from '../../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
|
import { hasEquipment } from '../../../../../../../core/blitzkit/hasEquipment';
|
||||||
|
import { modelTransformEvent } from '../../../../../../../core/blitzkit/modelTransform';
|
||||||
|
import { Var } from '../../../../../../../core/radix/var';
|
||||||
|
import { useLocale } from '../../../../../../../hooks/useLocale';
|
||||||
|
import { Duel } from '../../../../../../../stores/duel';
|
||||||
|
import type { MaybeSkeletonComponentProps } from '../../../../../../../types/maybeSkeletonComponentProps';
|
||||||
|
import { VisualizerCard } from '../VisualizerCard';
|
||||||
|
import { VisualizerCornerStat } from '../VisualizerCornerStat';
|
||||||
|
import { FlexibilityCanvas } from './components/FlexibilityCanvas';
|
||||||
|
|
||||||
|
const ANGLE_COEFFICIENT = 1 / 10;
|
||||||
|
|
||||||
|
const modelDefinition = await awaitableModelDefinitions;
|
||||||
|
|
||||||
|
function mag(x: number) {
|
||||||
|
return (1 / Math.PI) * Math.atan(ANGLE_COEFFICIENT * x) + 1 / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function magInverse(y: number): number {
|
||||||
|
return (1 / ANGLE_COEFFICIENT) * Math.tan(Math.PI * y - Math.PI / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function c(thetaDeg: number, m = 1) {
|
||||||
|
const theta = thetaDeg * (Math.PI / 180);
|
||||||
|
return `${m * Math.cos(theta)} ${-m * Math.sin(theta)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GunFlexibilityVisualizer({
|
||||||
|
skeleton,
|
||||||
|
}: MaybeSkeletonComponentProps) {
|
||||||
|
const { strings } = useLocale();
|
||||||
|
|
||||||
|
const duelStore = Duel.useStore();
|
||||||
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
|
|
||||||
|
const tankModel = modelDefinition.models[tank.id];
|
||||||
|
const turretModel = tankModel.turrets[turret.id];
|
||||||
|
const gunModel = turretModel.guns[gun.id];
|
||||||
|
|
||||||
|
const [yaw, setYaw] = useState(0);
|
||||||
|
const [pitch, setPitch] = useState(0);
|
||||||
|
const [minPitch, setMinPitch] = useState(
|
||||||
|
-degToRad(gunModel.pitch.front?.max ?? gunModel.pitch.max),
|
||||||
|
);
|
||||||
|
const [maxPitch, setMaxPitch] = useState(
|
||||||
|
-degToRad(gunModel.pitch.front?.min ?? gunModel.pitch.min),
|
||||||
|
);
|
||||||
|
|
||||||
|
const container = useRef<HTMLDivElement>(null);
|
||||||
|
const highlighter = useRef<HTMLDivElement>(null);
|
||||||
|
const pointer = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const p = gunModel.pitch;
|
||||||
|
const y = turretModel.yaw;
|
||||||
|
const f = p.front;
|
||||||
|
const b = p.back;
|
||||||
|
const t = gunModel.pitch.transition ?? DEFAULT_PITCH_TRANSITION;
|
||||||
|
|
||||||
|
let d = '';
|
||||||
|
|
||||||
|
// right
|
||||||
|
const mo = mag(p.max);
|
||||||
|
if (y) {
|
||||||
|
d += `M 0 0`;
|
||||||
|
d += `L ${c(90 - y.max, mo)}`;
|
||||||
|
} else if (b) {
|
||||||
|
d += `M ${c(-90 + b.range / 2 + t, mo)}`;
|
||||||
|
} else {
|
||||||
|
d += `M ${c(-90, mo)}`;
|
||||||
|
}
|
||||||
|
if (f) {
|
||||||
|
d += `A ${mo} ${mo} 0 0 0 ${c(90 - f.range / 2 - t, mo)}`;
|
||||||
|
} else {
|
||||||
|
d += `A ${mo} ${mo} 0 0 0 ${c(90, mo)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// front
|
||||||
|
if (f) {
|
||||||
|
const m = mag(f.max);
|
||||||
|
d += `L ${c(90 - f.range / 2, m)}`;
|
||||||
|
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// left
|
||||||
|
if (f) {
|
||||||
|
d += `L ${c(90 + f.range / 2 + t, mo)}`;
|
||||||
|
} else {
|
||||||
|
d += `L ${c(90, mo)}`;
|
||||||
|
}
|
||||||
|
if (y) {
|
||||||
|
d += `A ${mo} ${mo} 0 0 0 ${c(90 - y.min, mo)}`;
|
||||||
|
} else if (b) {
|
||||||
|
d += `A ${mo} ${mo} 0 0 0 ${c(-90 - b.range / 2 - t, mo)}`;
|
||||||
|
} else {
|
||||||
|
d += `A ${mo} ${mo} 0 0 0 ${c(-90, mo)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// back
|
||||||
|
if (!y && b) {
|
||||||
|
const m = mag(b.max);
|
||||||
|
d += `L ${c(-90 - b.range / 2, m)}`;
|
||||||
|
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
d += `Z`;
|
||||||
|
|
||||||
|
// right
|
||||||
|
const mi = mag(p.min);
|
||||||
|
if (y) {
|
||||||
|
d += `M 0 0`;
|
||||||
|
d += `L ${c(90 - y.max, mi)}`;
|
||||||
|
} else if (b) {
|
||||||
|
d += `M ${c(-90 + b.range / 2 + t, mi)}`;
|
||||||
|
} else {
|
||||||
|
d += `M ${c(-90, mi)}`;
|
||||||
|
}
|
||||||
|
if (f) {
|
||||||
|
d += `A ${mi} ${mi} 0 0 0 ${c(90 - f.range / 2 - t, mi)}`;
|
||||||
|
} else {
|
||||||
|
d += `A ${mi} ${mi} 0 0 0 ${c(90, mi)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// front
|
||||||
|
if (f) {
|
||||||
|
const m = mag(f.min);
|
||||||
|
d += `L ${c(90 - f.range / 2, m)}`;
|
||||||
|
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// left
|
||||||
|
if (f) {
|
||||||
|
d += `L ${c(90 + f.range / 2 + t, mi)}`;
|
||||||
|
} else {
|
||||||
|
d += `L ${c(90, mi)}`;
|
||||||
|
}
|
||||||
|
if (y) {
|
||||||
|
d += `A ${mi} ${mi} 0 0 0 ${c(90 - y.min, mi)}`;
|
||||||
|
} else if (b) {
|
||||||
|
d += `A ${mi} ${mi} 0 0 0 ${c(-90 - b.range / 2 - t, mi)}`;
|
||||||
|
} else {
|
||||||
|
d += `A ${mi} ${mi} 0 0 0 ${c(-90, mi)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// back
|
||||||
|
if (!y && b) {
|
||||||
|
const m = mag(b.min);
|
||||||
|
d += `L ${c(-90 - b.range / 2, m)}`;
|
||||||
|
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
d += `Z`;
|
||||||
|
|
||||||
|
const r = mag(radToDeg(-pitch));
|
||||||
|
const dotX = r * Math.sin(yaw);
|
||||||
|
const dotY = r * Math.cos(yaw);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column-reverse" gap="0">
|
||||||
|
<Box mt="-3" px="2">
|
||||||
|
<Card variant="classic" style={{ height: '10rem' }}>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(var(--gray-1), var(--gray-3))',
|
||||||
|
}}
|
||||||
|
position="absolute"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
top="0"
|
||||||
|
left="0"
|
||||||
|
>
|
||||||
|
{!skeleton && <FlexibilityCanvas />}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
top="0"
|
||||||
|
left="33%"
|
||||||
|
style={{
|
||||||
|
background: `conic-gradient(
|
||||||
|
at 0 50%,
|
||||||
|
|
||||||
|
transparent,
|
||||||
|
transparent ${Math.PI / 2 - maxPitch}rad,
|
||||||
|
|
||||||
|
${Var('cyan-a4')} ${Math.PI / 2 - maxPitch}rad,
|
||||||
|
${Var('jade-a4')} ${Math.PI / 2 - minPitch}rad,
|
||||||
|
|
||||||
|
transparent ${Math.PI / 2 - minPitch}rad,
|
||||||
|
transparent 180deg
|
||||||
|
)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<VisualizerCard
|
||||||
|
ref={container}
|
||||||
|
style={{ touchAction: 'none' }}
|
||||||
|
onPointerMove={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!container.current || !highlighter.current || !pointer.current)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const rect = container.current.getBoundingClientRect();
|
||||||
|
const u = event.clientX - rect.left - rect.width / 2;
|
||||||
|
const v = event.clientY - rect.top - rect.height / 2;
|
||||||
|
let yaw = Math.atan2(u, -v);
|
||||||
|
|
||||||
|
const {
|
||||||
|
equipmentMatrix,
|
||||||
|
tank: { equipment_preset },
|
||||||
|
} = duelStore.getState().protagonist;
|
||||||
|
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||||
|
122,
|
||||||
|
equipment_preset,
|
||||||
|
equipmentMatrix,
|
||||||
|
);
|
||||||
|
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||||
|
124,
|
||||||
|
equipment_preset,
|
||||||
|
equipmentMatrix,
|
||||||
|
);
|
||||||
|
|
||||||
|
const min = applyPitchYawLimits(
|
||||||
|
-Math.PI,
|
||||||
|
yaw,
|
||||||
|
p,
|
||||||
|
y,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
|
hasDownImprovedVerticalStabilizer,
|
||||||
|
);
|
||||||
|
const max = applyPitchYawLimits(
|
||||||
|
Math.PI,
|
||||||
|
yaw,
|
||||||
|
p,
|
||||||
|
y,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
|
hasDownImprovedVerticalStabilizer,
|
||||||
|
);
|
||||||
|
|
||||||
|
yaw = min[1];
|
||||||
|
let t = 2 * Math.sqrt((u / rect.width) ** 2 + (v / rect.height) ** 2);
|
||||||
|
t = clamp(t, 0, 1);
|
||||||
|
let pitch = degToRad(magInverse(-t));
|
||||||
|
pitch = clamp(pitch, min[0], max[0]);
|
||||||
|
|
||||||
|
setPitch(pitch);
|
||||||
|
setYaw(yaw);
|
||||||
|
setMinPitch(min[0]);
|
||||||
|
setMaxPitch(max[0]);
|
||||||
|
|
||||||
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
|
}}
|
||||||
|
mb="0"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
ref={highlighter}
|
||||||
|
position="absolute"
|
||||||
|
top="0"
|
||||||
|
left="0"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
style={{
|
||||||
|
borderRadius: Var('radius-2'),
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
style={{
|
||||||
|
opacity: 2 ** -3,
|
||||||
|
backgroundImage: `url(/assets/images/tankopedia/visualizers/ricochet/armor-hash.png)`,
|
||||||
|
boxShadow: 'inset 0 0 4rem 1rem black',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: 'calc(100%)',
|
||||||
|
height: 'calc(100%)',
|
||||||
|
}}
|
||||||
|
viewBox="-1 -1 2 2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
fill="var(--gray-5)"
|
||||||
|
stroke="var(--gray-8)"
|
||||||
|
strokeWidth="1px"
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
d={d}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<path
|
||||||
|
stroke="var(--gray-10)"
|
||||||
|
fill="transparent"
|
||||||
|
strokeWidth="1px"
|
||||||
|
strokeDasharray="0.5rem 0.5rem"
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
d={`
|
||||||
|
M 0.5 0
|
||||||
|
A 0.5 0.5 0 1 1 -0.5 0
|
||||||
|
A 0.5 0.5 0 1 1 0.5 0
|
||||||
|
Z
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
ref={pointer}
|
||||||
|
position="absolute"
|
||||||
|
top="50%"
|
||||||
|
left="50%"
|
||||||
|
width="0"
|
||||||
|
height="0"
|
||||||
|
style={{
|
||||||
|
transform: `translate(-50%, -50%) rotate(${yaw + Math.PI}rad)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
width="1pt"
|
||||||
|
height="14rem"
|
||||||
|
style={{
|
||||||
|
borderRadius: Var('radius-1'),
|
||||||
|
background: `linear-gradient(${Var('gray-a11')}, ${Var('gray-a6')})`,
|
||||||
|
boxShadow:
|
||||||
|
'var(--white-a12) -0.25rem 0 1rem, var(--black-a12) 0.25rem 0 1rem',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
left={`${50 * (dotX + 1)}%`}
|
||||||
|
bottom={`${50 * (dotY + 1)}%`}
|
||||||
|
width="0.25rem"
|
||||||
|
height="0.25rem"
|
||||||
|
style={{
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: Var('gray-12'),
|
||||||
|
transform: 'translate(-50%, 50%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VisualizerCornerStat
|
||||||
|
label={strings.website.tools.tankopedia.visualizers.flexibility.max}
|
||||||
|
value={literals(strings.common.units.deg, [
|
||||||
|
radToDeg(maxPitch).toFixed(0),
|
||||||
|
])}
|
||||||
|
side="bottom-left"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VisualizerCornerStat
|
||||||
|
label={strings.website.tools.tankopedia.visualizers.flexibility.min}
|
||||||
|
value={literals(strings.common.units.deg, [
|
||||||
|
radToDeg(minPitch).toFixed(0),
|
||||||
|
])}
|
||||||
|
side="bottom-right"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VisualizerCornerStat
|
||||||
|
label={strings.website.tools.tankopedia.visualizers.flexibility.pitch}
|
||||||
|
value={literals(strings.common.units.deg, [
|
||||||
|
radToDeg(pitch).toFixed(0),
|
||||||
|
])}
|
||||||
|
side="top-left"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VisualizerCornerStat
|
||||||
|
label={strings.website.tools.tankopedia.visualizers.flexibility.yaw}
|
||||||
|
value={literals(strings.common.units.deg, [radToDeg(yaw).toFixed(0)])}
|
||||||
|
side="top-right"
|
||||||
|
/>
|
||||||
|
</VisualizerCard>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
+139
-140
@@ -1,7 +1,7 @@
|
|||||||
import { createDefaultSkills } from '@blitzkit/core';
|
import { createDefaultSkills } from '@blitzkit/core';
|
||||||
import { Flex, Progress, Text } from '@radix-ui/themes';
|
import { Flex, Progress, Text } from '@radix-ui/themes';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
import { useMemo, type ComponentProps, type ReactNode } from 'react';
|
import { memo, useMemo, type ComponentProps, type ReactNode } from 'react';
|
||||||
import { awaitableEquipmentDefinitions } from '../../../../../../core/awaitables/equipmentDefinitions';
|
import { awaitableEquipmentDefinitions } from '../../../../../../core/awaitables/equipmentDefinitions';
|
||||||
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
|
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
|
||||||
import { awaitableProvisionDefinitions } from '../../../../../../core/awaitables/provisionDefinitions';
|
import { awaitableProvisionDefinitions } from '../../../../../../core/awaitables/provisionDefinitions';
|
||||||
@@ -48,148 +48,147 @@ const [
|
|||||||
awaitableSkillDefinitions,
|
awaitableSkillDefinitions,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function InfoWithDelta({
|
export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
||||||
stats,
|
({ stats, indent, noRanking, deltaType, ...props }) => {
|
||||||
indent,
|
const { strings } = useLocale();
|
||||||
noRanking,
|
const relativeAgainst = TankopediaEphemeral.use(
|
||||||
deltaType,
|
(state) => state.relativeAgainst,
|
||||||
...props
|
);
|
||||||
}: InfoWithDeltaProps) {
|
const uhWhatDoICallThisVariable =
|
||||||
const { strings } = useLocale();
|
|
||||||
const relativeAgainst = TankopediaEphemeral.use(
|
|
||||||
(state) => state.relativeAgainst,
|
|
||||||
);
|
|
||||||
const uhWhatDoICallThisVariable =
|
|
||||||
typeof props.value === 'function'
|
|
||||||
? props.value(stats)!
|
|
||||||
: (stats[props.value] as number);
|
|
||||||
const delta = useDelta(uhWhatDoICallThisVariable);
|
|
||||||
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
|
||||||
const shellIndex = Duel.use((state) =>
|
|
||||||
state.protagonist.gun.gun_type!.value.base.shells.indexOf(
|
|
||||||
state.protagonist.shell,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const equipmentMatrix = Duel.use(
|
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
|
||||||
);
|
|
||||||
const others = useMemo(() => {
|
|
||||||
const defaultSkills = createDefaultSkills(skillDefinitions);
|
|
||||||
|
|
||||||
return Object.values(tankDefinitions.tanks)
|
|
||||||
.filter(
|
|
||||||
(tank) =>
|
|
||||||
(relativeAgainst === TankopediaRelativeAgainst.Class &&
|
|
||||||
tank.tier === protagonistTank.tier &&
|
|
||||||
tank.class === protagonistTank.class) ||
|
|
||||||
(relativeAgainst === TankopediaRelativeAgainst.Tier &&
|
|
||||||
tank.tier === protagonistTank.tier) ||
|
|
||||||
relativeAgainst === TankopediaRelativeAgainst.All,
|
|
||||||
)
|
|
||||||
.map((tank) => {
|
|
||||||
const thisTankModel = modelDefinitions.models[tank.id];
|
|
||||||
const member = tankToDuelMember(
|
|
||||||
tank,
|
|
||||||
thisTankModel,
|
|
||||||
provisionDefinitions,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (member.gun.gun_type!.value.base.shells[shellIndex] === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tankCharacteristics(
|
|
||||||
{
|
|
||||||
applyDynamicArmor: false,
|
|
||||||
applyReactiveArmor: false,
|
|
||||||
applySpallLiner: false,
|
|
||||||
camouflage: member.camouflage,
|
|
||||||
consumables: member.consumables,
|
|
||||||
crewSkills: defaultSkills,
|
|
||||||
provisions: member.provisions,
|
|
||||||
engine: member.engine,
|
|
||||||
gun: member.gun,
|
|
||||||
equipmentMatrix: equipmentMatrix,
|
|
||||||
shell: member.gun.gun_type!.value.base.shells[shellIndex],
|
|
||||||
stockEngine: tank.engines[0],
|
|
||||||
stockGun: tank.turrets[0].guns[0],
|
|
||||||
stockTrack: tank.tracks[0],
|
|
||||||
stockTurret: tank.turrets[0],
|
|
||||||
tank,
|
|
||||||
track: member.track,
|
|
||||||
turret: member.turret,
|
|
||||||
assaultDistance: member.assaultDistance,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
equipmentDefinitions,
|
|
||||||
provisionDefinitions,
|
|
||||||
tankModelDefinition: modelDefinitions.models[tank.id],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.filter((tank) => {
|
|
||||||
if (tank === undefined) return false;
|
|
||||||
|
|
||||||
const othersValue =
|
|
||||||
typeof props.value === 'function'
|
|
||||||
? props.value(tank)!
|
|
||||||
: (tank[props.value] as number);
|
|
||||||
|
|
||||||
return othersValue !== undefined;
|
|
||||||
}) as TankCharacteristics[];
|
|
||||||
}, [relativeAgainst, shellIndex, equipmentMatrix]);
|
|
||||||
const betterTanks = others.filter((tank) => {
|
|
||||||
const othersValue =
|
|
||||||
typeof props.value === 'function'
|
typeof props.value === 'function'
|
||||||
? props.value(tank)!
|
? props.value(stats)!
|
||||||
: (tank[props.value] as number);
|
: (stats[props.value] as number);
|
||||||
|
const delta = useDelta(uhWhatDoICallThisVariable);
|
||||||
|
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
||||||
|
const shellIndex = Duel.use((state) =>
|
||||||
|
state.protagonist.gun.shells.indexOf(state.protagonist.shell),
|
||||||
|
);
|
||||||
|
const equipmentMatrix = Duel.use(
|
||||||
|
(state) => state.protagonist.equipmentMatrix,
|
||||||
|
);
|
||||||
|
const others = useMemo(() => {
|
||||||
|
const defaultSkills = createDefaultSkills(skillDefinitions);
|
||||||
|
|
||||||
if (othersValue === undefined) return false;
|
return Object.values(tankDefinitions.tanks)
|
||||||
return deltaType === 'lowerIsBetter'
|
.filter(
|
||||||
? othersValue < uhWhatDoICallThisVariable
|
(tank) =>
|
||||||
: othersValue > uhWhatDoICallThisVariable;
|
(relativeAgainst === TankopediaRelativeAgainst.Class &&
|
||||||
});
|
tank.tier === protagonistTank.tier &&
|
||||||
const goodness = (others.length - betterTanks.length) / others.length;
|
tank.class === protagonistTank.class) ||
|
||||||
let color: ComponentProps<typeof Progress>['color'];
|
(relativeAgainst === TankopediaRelativeAgainst.Tier &&
|
||||||
|
tank.tier === protagonistTank.tier) ||
|
||||||
|
relativeAgainst === TankopediaRelativeAgainst.All,
|
||||||
|
)
|
||||||
|
.map((tank) => {
|
||||||
|
const thisTankModel = modelDefinitions.models[tank.id];
|
||||||
|
const member = tankToDuelMember(
|
||||||
|
tank,
|
||||||
|
thisTankModel,
|
||||||
|
provisionDefinitions,
|
||||||
|
);
|
||||||
|
|
||||||
if (goodness <= 0.25) color = 'red';
|
if (member.gun.shells[shellIndex] === undefined) {
|
||||||
else if (goodness <= 0.5) color = 'orange';
|
return undefined;
|
||||||
else if (goodness <= 0.75) color = 'yellow';
|
}
|
||||||
else color = 'green';
|
|
||||||
|
|
||||||
return (
|
return tankCharacteristics(
|
||||||
<Flex direction="column">
|
{
|
||||||
<Info
|
applyDynamicArmor: false,
|
||||||
deltaType={deltaType}
|
applyReactiveArmor: false,
|
||||||
indent={indent}
|
applySpallLiner: false,
|
||||||
{...props}
|
camouflage: member.camouflage,
|
||||||
name={
|
consumables: member.consumables,
|
||||||
'name' in props
|
crewSkills: defaultSkills,
|
||||||
? props.name
|
provisions: member.provisions,
|
||||||
: strings.website.tools.tankopedia.characteristics.values[
|
engine: member.engine,
|
||||||
props.value
|
gun: member.gun,
|
||||||
]
|
equipmentMatrix: equipmentMatrix,
|
||||||
}
|
shell: member.gun.shells[shellIndex],
|
||||||
delta={delta}
|
stockEngine: tank.engines[0],
|
||||||
>
|
stockGun: tank.turrets[0].guns[0],
|
||||||
{uhWhatDoICallThisVariable}
|
stockTrack: tank.tracks[0],
|
||||||
</Info>
|
stockTurret: tank.turrets[0],
|
||||||
|
tank,
|
||||||
|
track: member.track,
|
||||||
|
turret: member.turret,
|
||||||
|
assaultDistance: member.assaultDistance,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
equipmentDefinitions,
|
||||||
|
provisionDefinitions,
|
||||||
|
tankModelDefinition: modelDefinitions.models[tank.id],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.filter((tank) => {
|
||||||
|
if (tank === undefined) return false;
|
||||||
|
|
||||||
{!noRanking && (
|
const othersValue =
|
||||||
<Flex pl={indent ? '2' : '0'} align="center" gap="2" mb="2">
|
typeof props.value === 'function'
|
||||||
<Progress
|
? props.value(tank)!
|
||||||
variant="soft"
|
: (tank[props.value] as number);
|
||||||
size="1"
|
|
||||||
value={goodness * 100}
|
|
||||||
color={color}
|
|
||||||
style={{ height: '0.125rem', opacity: 0.5 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text color="gray" size="1">
|
return othersValue !== undefined;
|
||||||
{clamp(betterTanks.length + 1, 1, others.length)} / {others.length}
|
}) as TankCharacteristics[];
|
||||||
</Text>
|
}, [relativeAgainst, shellIndex, equipmentMatrix]);
|
||||||
</Flex>
|
const betterTanks = others.filter((tank) => {
|
||||||
)}
|
const othersValue =
|
||||||
</Flex>
|
typeof props.value === 'function'
|
||||||
);
|
? props.value(tank)!
|
||||||
}
|
: (tank[props.value] as number);
|
||||||
|
|
||||||
|
if (othersValue === undefined) return false;
|
||||||
|
return deltaType === 'lowerIsBetter'
|
||||||
|
? othersValue < uhWhatDoICallThisVariable
|
||||||
|
: othersValue > uhWhatDoICallThisVariable;
|
||||||
|
});
|
||||||
|
const goodness = (others.length - betterTanks.length) / others.length;
|
||||||
|
let color: ComponentProps<typeof Progress>['color'];
|
||||||
|
|
||||||
|
if (goodness <= 0.25) color = 'red';
|
||||||
|
else if (goodness <= 0.5) color = 'orange';
|
||||||
|
else if (goodness <= 0.75) color = 'yellow';
|
||||||
|
else color = 'green';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column">
|
||||||
|
<Info
|
||||||
|
deltaType={deltaType}
|
||||||
|
indent={indent}
|
||||||
|
name={
|
||||||
|
'name' in props
|
||||||
|
? props.name
|
||||||
|
: strings.website.tools.tankopedia.characteristics.values[
|
||||||
|
props.value
|
||||||
|
]
|
||||||
|
}
|
||||||
|
delta={delta}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{uhWhatDoICallThisVariable}
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
{!noRanking && (
|
||||||
|
<Flex pl={indent ? '2' : '0'} align="center" gap="2">
|
||||||
|
<Progress
|
||||||
|
variant="soft"
|
||||||
|
size="1"
|
||||||
|
value={goodness * 100}
|
||||||
|
color={color}
|
||||||
|
style={{ height: '0.125rem', opacity: 0.5 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text color="gray" size="1">
|
||||||
|
{clamp(betterTanks.length + 1, 1, others.length)} /{' '}
|
||||||
|
{others.length}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
(a, b) =>
|
||||||
|
(typeof a.value === 'function' ? a.value(a.stats) : a.stats[a.value]) ===
|
||||||
|
(typeof b.value === 'function' ? b.value(b.stats) : b.stats[b.value]),
|
||||||
|
);
|
||||||
+1
-3
@@ -40,7 +40,7 @@ export function Shell({ index, stats }: ShellProps & StatsAcceptorProps) {
|
|||||||
<Box
|
<Box
|
||||||
left="50%"
|
left="50%"
|
||||||
position="absolute"
|
position="absolute"
|
||||||
ml="calc(25% + 2.5rem)"
|
ml="40%"
|
||||||
width="1.25rem"
|
width="1.25rem"
|
||||||
height="1.25rem"
|
height="1.25rem"
|
||||||
style={{
|
style={{
|
||||||
@@ -70,11 +70,9 @@ export function Shell({ index, stats }: ShellProps & StatsAcceptorProps) {
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
maskImage:
|
maskImage:
|
||||||
'url(/assets/images/tankopedia/visualizers/reload/shell.png)',
|
'url(/assets/images/tankopedia/visualizers/reload/shell.png)',
|
||||||
// maskComposite: 'exclude',
|
|
||||||
maskSize: 'contain',
|
maskSize: 'contain',
|
||||||
maskRepeat: 'no-repeat',
|
maskRepeat: 'no-repeat',
|
||||||
maskPosition: 'center',
|
maskPosition: 'center',
|
||||||
// background: 'red',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
+2
-2
@@ -13,8 +13,8 @@ export function ShellBorder(props: ComponentProps<'svg'>) {
|
|||||||
<path
|
<path
|
||||||
d="M149.917 7.5L151.5 9.91406V50.085L149.917 52.5H134.674C133.504 51.6657 132.116 51.1813 130.673 51.1133L130.319 51.1045H93.085C91.8919 51.1046 90.7173 51.3895 89.6582 51.9336L89.4482 52.0459L88.6494 52.4883C48.806 52.0806 22.9344 39.046 11.1416 30C22.9344 20.954 48.806 7.91846 88.6494 7.51074L89.4482 7.9541C90.5611 8.57122 91.8125 8.89543 93.085 8.89551H130.319C131.888 8.89551 133.409 8.40216 134.674 7.5H149.917Z"
|
d="M149.917 7.5L151.5 9.91406V50.085L149.917 52.5H134.674C133.504 51.6657 132.116 51.1813 130.673 51.1133L130.319 51.1045H93.085C91.8919 51.1046 90.7173 51.3895 89.6582 51.9336L89.4482 52.0459L88.6494 52.4883C48.806 52.0806 22.9344 39.046 11.1416 30C22.9344 20.954 48.806 7.91846 88.6494 7.51074L89.4482 7.9541C90.5611 8.57122 91.8125 8.89543 93.085 8.89551H130.319C131.888 8.89551 133.409 8.40216 134.674 7.5H149.917Z"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="15"
|
strokeWidth="15"
|
||||||
stroke-linejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
#reload-visualizer-flash {
|
||||||
|
background: transparent;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reload-visualizer-flash.go {
|
||||||
|
background: var(--white-a3);
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reload {
|
||||||
|
width: 65%;
|
||||||
|
height: 65%;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reload.go {
|
||||||
|
width: 70%;
|
||||||
|
height: 70%;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
+122
-59
@@ -1,12 +1,15 @@
|
|||||||
import { type Datum, type Serie } from '@nivo/line';
|
import { Box, Code } from '@radix-ui/themes';
|
||||||
import { Box, Card, Code } from '@radix-ui/themes';
|
|
||||||
import { times } from 'lodash-es';
|
import { times } from 'lodash-es';
|
||||||
import { Quicklime, type QuicklimeCallback } from 'quicklime';
|
import { Quicklime, type QuicklimeCallback } from 'quicklime';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { Var } from '../../../../../../../core/radix/var';
|
import { Var } from '../../../../../../../core/radix/var';
|
||||||
import type { StatsAcceptorProps } from '../TraverseVisualizer';
|
import type { StatsAcceptorProps } from '../TraverseVisualizer';
|
||||||
|
import { VisualizerCard } from '../VisualizerCard';
|
||||||
import { Shell } from './components/Shell';
|
import { Shell } from './components/Shell';
|
||||||
import { Target } from './components/Target';
|
import { Target } from './components/Target';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const PRECISION = 2;
|
||||||
|
|
||||||
export interface ReloadUpdateData {
|
export interface ReloadUpdateData {
|
||||||
reload: number;
|
reload: number;
|
||||||
@@ -15,30 +18,29 @@ export interface ReloadUpdateData {
|
|||||||
|
|
||||||
export const reloadUpdate = new Quicklime<ReloadUpdateData>();
|
export const reloadUpdate = new Quicklime<ReloadUpdateData>();
|
||||||
|
|
||||||
const T = 60;
|
|
||||||
|
|
||||||
export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||||
const reloadCircle = useRef<HTMLDivElement>(null);
|
const reloadCircle = useRef<HTMLDivElement>(null);
|
||||||
const reloadCore = useRef<HTMLDivElement>(null);
|
const reloadCore = useRef<HTMLDivElement>(null);
|
||||||
const reloadGlow = useRef<HTMLDivElement>(null);
|
const reloadGlow = useRef<HTMLDivElement>(null);
|
||||||
const dpmDisplay = useRef<HTMLSpanElement>(null);
|
const totalTime = useRef<HTMLSpanElement>(null);
|
||||||
|
const progressTime = useRef<HTMLSpanElement>(null);
|
||||||
|
const flash = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
/**
|
|
||||||
* 3-shell gun example:
|
|
||||||
*
|
|
||||||
* 0-1: clip reload
|
|
||||||
* ~~~: shell shot
|
|
||||||
* 1-2: intra-clip
|
|
||||||
* ~~~: shell shot
|
|
||||||
* 2-3: intra-clip
|
|
||||||
* ~~~: shell shot & reset to 0
|
|
||||||
*
|
|
||||||
* intra-clip time = intraClip * (n - 1)
|
|
||||||
*/
|
|
||||||
const state = useRef(0);
|
const state = useRef(0);
|
||||||
const dpm = useRef(0);
|
|
||||||
const shotPool = useRef(new Set<number>());
|
const shoot = useCallback(() => {
|
||||||
const data = useRef<Serie[]>([{ id: 'value', data: [] }]);
|
flash.current?.classList.add('go');
|
||||||
|
reloadCircle.current?.classList.add('go');
|
||||||
|
reloadGlow.current?.classList.add('go');
|
||||||
|
|
||||||
|
void flash.current?.offsetWidth;
|
||||||
|
void reloadCircle.current?.offsetWidth;
|
||||||
|
void reloadGlow.current?.offsetWidth;
|
||||||
|
|
||||||
|
flash.current?.classList.remove('go');
|
||||||
|
reloadCircle.current?.classList.remove('go');
|
||||||
|
reloadGlow.current?.classList.remove('go');
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancel = false;
|
let cancel = false;
|
||||||
@@ -46,40 +48,76 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
let lastState = state.current;
|
let lastState = state.current;
|
||||||
const shellArray = new Array(stats.shells);
|
const shellArray = new Array(stats.shells);
|
||||||
|
|
||||||
function shoot() {
|
|
||||||
shotPool.current.add(Date.now() / 1000);
|
|
||||||
dpm.current += stats.damage;
|
|
||||||
}
|
|
||||||
|
|
||||||
function frame() {
|
function frame() {
|
||||||
if (!dpmDisplay.current) return;
|
if (!totalTime.current || !progressTime.current) return;
|
||||||
|
|
||||||
|
const reloadThreshold = stats.shellReloads
|
||||||
|
? stats.shellReloads.length
|
||||||
|
: 1;
|
||||||
const now = Date.now() / 1000;
|
const now = Date.now() / 1000;
|
||||||
const dt = now - lastT;
|
const dt = now - lastT;
|
||||||
let reload;
|
let reload;
|
||||||
let shells;
|
let shells;
|
||||||
|
|
||||||
shotPool.current.forEach((t) => {
|
if (state.current < reloadThreshold) {
|
||||||
if (now - t > 60) shotPool.current.delete(t);
|
const shellReload =
|
||||||
});
|
reloadThreshold === 1
|
||||||
|
? stats.shellReload!
|
||||||
|
: stats.shellReloads![Math.floor(state.current)];
|
||||||
|
|
||||||
dpm.current = shotPool.current.size * stats.damage;
|
state.current = Math.min(
|
||||||
dpmDisplay.current.innerText = `${Math.round(dpm.current)}`;
|
reloadThreshold,
|
||||||
|
state.current + dt / shellReload,
|
||||||
if (state.current < 1) {
|
);
|
||||||
state.current = Math.min(1, state.current + dt / stats.shellReload!);
|
reload = state.current % 1;
|
||||||
reload = state.current;
|
|
||||||
shells = shellArray.fill(reload);
|
shells = shellArray.fill(reload);
|
||||||
|
|
||||||
if (state.current >= 1) shoot();
|
if (reloadThreshold > 1) {
|
||||||
} else if (state.current < stats.shells) {
|
shells = shells.map((_, index) =>
|
||||||
|
Math.max(0, state.current - shellArray.length + index + 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalTime.current.innerHTML = shellReload.toFixed(PRECISION);
|
||||||
|
progressTime.current.innerHTML = ((1 - reload) * shellReload).toFixed(
|
||||||
|
PRECISION,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (state.current >= reloadThreshold) shoot();
|
||||||
|
} else if (state.current < stats.shells + reloadThreshold - 1) {
|
||||||
|
let interval;
|
||||||
|
|
||||||
|
if (
|
||||||
|
stats.burstShells === 1 ||
|
||||||
|
Math.floor(state.current) % stats.burstShells === 0
|
||||||
|
) {
|
||||||
|
interval = stats.intraClip!;
|
||||||
|
} else {
|
||||||
|
interval = stats.burstInterShell!;
|
||||||
|
}
|
||||||
|
|
||||||
state.current = Math.min(
|
state.current = Math.min(
|
||||||
stats.shells,
|
stats.shells + reloadThreshold,
|
||||||
state.current + dt / stats.intraClip!,
|
state.current + dt / interval,
|
||||||
);
|
);
|
||||||
shells = times(stats.shells, (index) => index > state.current - 1);
|
|
||||||
reload = state.current % 1;
|
reload = state.current % 1;
|
||||||
|
|
||||||
|
if (reloadThreshold > 1) {
|
||||||
|
shells = shellArray.map((_, index) =>
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
Math.floor(index + 1 + reloadThreshold - state.current),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
shells = times(stats.shells, (index) => index > state.current - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalTime.current.innerHTML = interval.toFixed(PRECISION);
|
||||||
|
progressTime.current.innerHTML = ((1 - reload) * interval).toFixed(
|
||||||
|
PRECISION,
|
||||||
|
);
|
||||||
|
|
||||||
if (lastState % 1 > state.current % 1) shoot();
|
if (lastState % 1 > state.current % 1) shoot();
|
||||||
} else {
|
} else {
|
||||||
state.current = 0;
|
state.current = 0;
|
||||||
@@ -93,15 +131,6 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
|
|
||||||
lastT = now;
|
lastT = now;
|
||||||
lastState = state.current;
|
lastState = state.current;
|
||||||
|
|
||||||
(data.current[0].data as Datum[]).push({
|
|
||||||
x: now,
|
|
||||||
y: dpm.current / (stats.dpm + stats.damage),
|
|
||||||
});
|
|
||||||
(data.current[0].data as Datum[]).splice(
|
|
||||||
0,
|
|
||||||
data.current[0].data.findIndex((d) => now - (d.x as number) < T),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
frame();
|
frame();
|
||||||
@@ -133,7 +162,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="classic" mb="6" style={{ aspectRatio: '1 / 1' }}>
|
<VisualizerCard>
|
||||||
<Box position="absolute" top="0" left="0" width="100%" height="100%">
|
<Box position="absolute" top="0" left="0" width="100%" height="100%">
|
||||||
<Target />
|
<Target />
|
||||||
|
|
||||||
@@ -154,8 +183,6 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
position="absolute"
|
position="absolute"
|
||||||
top="50%"
|
top="50%"
|
||||||
left="50%"
|
left="50%"
|
||||||
width="12rem"
|
|
||||||
height="12rem"
|
|
||||||
className="reload"
|
className="reload"
|
||||||
ref={reloadGlow}
|
ref={reloadGlow}
|
||||||
style={{
|
style={{
|
||||||
@@ -170,8 +197,6 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
position="absolute"
|
position="absolute"
|
||||||
top="50%"
|
top="50%"
|
||||||
left="50%"
|
left="50%"
|
||||||
width="12rem"
|
|
||||||
height="12rem"
|
|
||||||
className="reload"
|
className="reload"
|
||||||
ref={reloadCircle}
|
ref={reloadCircle}
|
||||||
style={{
|
style={{
|
||||||
@@ -219,14 +244,52 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Code
|
||||||
|
ref={progressTime}
|
||||||
|
size="1"
|
||||||
|
variant="ghost"
|
||||||
|
weight="bold"
|
||||||
|
color="gray"
|
||||||
|
highContrast
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
right: 'calc(50% + var(--space-8))',
|
||||||
|
transform: 'translateX(50%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
0.00
|
||||||
|
</Code>
|
||||||
|
<Code
|
||||||
|
ref={totalTime}
|
||||||
|
size="1"
|
||||||
|
variant="ghost"
|
||||||
|
weight="bold"
|
||||||
|
color="gray"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: 'calc(50% + var(--space-8))',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
0.00
|
||||||
|
</Code>
|
||||||
|
|
||||||
{stats.shells > 1 &&
|
{stats.shells > 1 &&
|
||||||
times(stats.shells, (index) => (
|
times(stats.shells, (index) => (
|
||||||
<Shell stats={stats} index={index} key={index} />
|
<Shell stats={stats} index={index} key={index} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Box position="absolute" right="3" bottom="3">
|
<Box
|
||||||
<Code ref={dpmDisplay}>0 dpm</Code>
|
id="reload-visualizer-flash"
|
||||||
</Box>
|
ref={flash}
|
||||||
</Card>
|
position="absolute"
|
||||||
|
top="0"
|
||||||
|
left="0"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
/>
|
||||||
|
</VisualizerCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+2
-4
@@ -11,7 +11,7 @@ export function ShellSwitcher() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex>
|
<Flex>
|
||||||
{gun.gun_type!.value.base.shells.map((thisShell, shellIndex) => {
|
{gun.shells.map((thisShell, shellIndex) => {
|
||||||
const selected = thisShell.id === shell.id;
|
const selected = thisShell.id === shell.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -26,9 +26,7 @@ export function ShellSwitcher() {
|
|||||||
borderBottomLeftRadius: shellIndex === 0 ? '50%' : 0,
|
borderBottomLeftRadius: shellIndex === 0 ? '50%' : 0,
|
||||||
borderTopRightRadius: 0,
|
borderTopRightRadius: 0,
|
||||||
borderBottomRightRadius:
|
borderBottomRightRadius:
|
||||||
shellIndex === gun.gun_type!.value.base.shells.length - 1
|
shellIndex === gun.shells.length - 1 ? '50%' : 0,
|
||||||
? '50%'
|
|
||||||
: 0,
|
|
||||||
marginLeft: shellIndex === 0 ? 0 : -1,
|
marginLeft: shellIndex === 0 ? 0 : -1,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
+64
-100
@@ -1,12 +1,14 @@
|
|||||||
import { asset, isExplosive } from '@blitzkit/core';
|
import { asset, isExplosive } from '@blitzkit/core';
|
||||||
import { literals } from '@blitzkit/i18n';
|
import { literals } from '@blitzkit/i18n';
|
||||||
import { Box, Card, Code, Flex, Text } from '@radix-ui/themes';
|
import { Box, Flex, Text } from '@radix-ui/themes';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { clamp, degToRad, radToDeg } from 'three/src/math/MathUtils.js';
|
import { clamp, degToRad, radToDeg } from 'three/src/math/MathUtils.js';
|
||||||
import { Var } from '../../../../../../../core/radix/var';
|
import { Var } from '../../../../../../../core/radix/var';
|
||||||
import { useLocale } from '../../../../../../../hooks/useLocale';
|
import { useLocale } from '../../../../../../../hooks/useLocale';
|
||||||
import { Duel } from '../../../../../../../stores/duel';
|
import { Duel } from '../../../../../../../stores/duel';
|
||||||
import type { StatsAcceptorProps } from '../TraverseVisualizer';
|
import type { StatsAcceptorProps } from '../TraverseVisualizer';
|
||||||
|
import { VisualizerCard } from '../VisualizerCard';
|
||||||
|
import { VisualizerCornerStat } from '../VisualizerCornerStat';
|
||||||
import { ShellSwitcher } from './components/ShellSwitcher';
|
import { ShellSwitcher } from './components/ShellSwitcher';
|
||||||
|
|
||||||
export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
||||||
@@ -26,13 +28,9 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" mb="6" align="center">
|
<Flex direction="column" mb="6" align="center">
|
||||||
<Card
|
<VisualizerCard
|
||||||
variant="classic"
|
mb="0"
|
||||||
style={{
|
style={{ width: '100%' }}
|
||||||
width: '100%',
|
|
||||||
aspectRatio: '1 / 1',
|
|
||||||
touchAction: 'none',
|
|
||||||
}}
|
|
||||||
ref={container}
|
ref={container}
|
||||||
onPointerMove={(event) => {
|
onPointerMove={(event) => {
|
||||||
if (!container.current || explodes) return;
|
if (!container.current || explodes) return;
|
||||||
@@ -73,23 +71,23 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
position="absolute"
|
position="absolute"
|
||||||
style={{
|
style={{
|
||||||
background: `conic-gradient(
|
background: `conic-gradient(
|
||||||
at 0 50%,
|
at 0 50%,
|
||||||
|
|
||||||
${Var('ruby-a2')},
|
${Var('ruby-a2')},
|
||||||
${Var('ruby-a1')} ${Math.PI / 2 - ricochet}rad,
|
${Var('ruby-a1')} ${Math.PI / 2 - ricochet}rad,
|
||||||
|
|
||||||
${Var('jade-a2')} ${Math.PI / 2 - ricochet}rad,
|
${Var('jade-a2')} ${Math.PI / 2 - ricochet}rad,
|
||||||
${Var('jade-a4')} ${Math.PI / 2 - normalization}rad,
|
${Var('jade-a4')} ${Math.PI / 2 - normalization}rad,
|
||||||
|
|
||||||
${Var('cyan-a5')} ${Math.PI / 2 - normalization}rad,
|
${Var('cyan-a5')} ${Math.PI / 2 - normalization}rad,
|
||||||
${Var('cyan-a3')} ${Math.PI / 2 + normalization}rad,
|
${Var('cyan-a3')} ${Math.PI / 2 + normalization}rad,
|
||||||
|
|
||||||
${Var('jade-a2')} ${Math.PI / 2 + normalization}rad,
|
${Var('jade-a2')} ${Math.PI / 2 + normalization}rad,
|
||||||
${Var('jade-a4')} ${Math.PI / 2 + ricochet}rad,
|
${Var('jade-a4')} ${Math.PI / 2 + ricochet}rad,
|
||||||
|
|
||||||
${Var('ruby-a1')} ${Math.PI / 2 + ricochet}rad,
|
${Var('ruby-a1')} ${Math.PI / 2 + ricochet}rad,
|
||||||
${Var('ruby-a2')} 180deg
|
${Var('ruby-a2')} 180deg
|
||||||
`,
|
`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -160,85 +158,51 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Flex
|
<VisualizerCornerStat
|
||||||
style={{ userSelect: 'none' }}
|
label={strings.website.tools.tankopedia.visualizers.ricochet.angle}
|
||||||
gap="4"
|
value={literals(strings.common.units.deg, [
|
||||||
position="absolute"
|
radToDeg(Math.abs(angle)).toFixed(0),
|
||||||
top="0"
|
])}
|
||||||
left="0"
|
side="top-left"
|
||||||
p="3"
|
/>
|
||||||
width="100%"
|
<VisualizerCornerStat
|
||||||
justify="between"
|
label={
|
||||||
>
|
strings.website.tools.tankopedia.visualizers.ricochet.normalization
|
||||||
<Flex gap="2">
|
}
|
||||||
<Text color="gray" size="1">
|
value={
|
||||||
{strings.website.tools.tankopedia.visualizers.ricochet.angle}
|
doesRicochet
|
||||||
</Text>
|
? '-'
|
||||||
<Code size="1" variant="ghost">
|
: literals(strings.common.units.deg, [
|
||||||
{literals(strings.common.units.deg, [
|
radToDeg(Math.min(normalization, Math.abs(angle))).toFixed(0),
|
||||||
radToDeg(Math.abs(angle)).toFixed(0),
|
])
|
||||||
])}
|
}
|
||||||
</Code>
|
side="top-right"
|
||||||
</Flex>
|
/>
|
||||||
|
<VisualizerCornerStat
|
||||||
<Flex gap="2">
|
label={
|
||||||
<Text color="gray" size="1">
|
strings.website.tools.tankopedia.visualizers.ricochet
|
||||||
{
|
.effective_angle
|
||||||
strings.website.tools.tankopedia.visualizers.ricochet
|
}
|
||||||
.normalization
|
value={
|
||||||
}
|
doesRicochet
|
||||||
</Text>
|
? '-'
|
||||||
<Code size="1" variant="ghost">
|
: literals(strings.common.units.deg, [
|
||||||
{doesRicochet
|
radToDeg(Math.abs(effectiveAngle)).toFixed(0),
|
||||||
? '-'
|
])
|
||||||
: literals(strings.common.units.deg, [
|
}
|
||||||
radToDeg(Math.min(normalization, Math.abs(angle))).toFixed(
|
side="bottom-left"
|
||||||
0,
|
/>
|
||||||
),
|
<VisualizerCornerStat
|
||||||
])}
|
label={strings.website.tools.tankopedia.visualizers.ricochet.armor}
|
||||||
</Code>
|
value={
|
||||||
</Flex>
|
doesRicochet
|
||||||
</Flex>
|
? '-'
|
||||||
|
: literals(strings.common.units.percentage, [
|
||||||
<Flex
|
armorPercentage.toFixed(0),
|
||||||
style={{ userSelect: 'none' }}
|
])
|
||||||
gap="4"
|
}
|
||||||
position="absolute"
|
side="bottom-right"
|
||||||
bottom="0"
|
/>
|
||||||
left="0"
|
|
||||||
p="3"
|
|
||||||
width="100%"
|
|
||||||
justify="between"
|
|
||||||
>
|
|
||||||
<Flex gap="2">
|
|
||||||
<Text color="gray" size="1">
|
|
||||||
{
|
|
||||||
strings.website.tools.tankopedia.visualizers.ricochet
|
|
||||||
.effective_angle
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
<Code size="1" variant="ghost">
|
|
||||||
{doesRicochet
|
|
||||||
? '-'
|
|
||||||
: literals(strings.common.units.deg, [
|
|
||||||
radToDeg(Math.abs(effectiveAngle)).toFixed(0),
|
|
||||||
])}
|
|
||||||
</Code>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex gap="2">
|
|
||||||
<Text color="gray" size="1">
|
|
||||||
{strings.website.tools.tankopedia.visualizers.ricochet.armor}
|
|
||||||
</Text>
|
|
||||||
<Code size="1" variant="ghost">
|
|
||||||
{doesRicochet
|
|
||||||
? '-'
|
|
||||||
: literals(strings.common.units.percentage, [
|
|
||||||
armorPercentage.toFixed(0),
|
|
||||||
])}
|
|
||||||
</Code>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{explodes && (
|
{explodes && (
|
||||||
<Flex
|
<Flex
|
||||||
@@ -262,7 +226,7 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</VisualizerCard>
|
||||||
|
|
||||||
<ShellSwitcher />
|
<ShellSwitcher />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
+4
-3
@@ -1,8 +1,9 @@
|
|||||||
import { literals } from '@blitzkit/i18n';
|
import { literals } from '@blitzkit/i18n';
|
||||||
import { Card, Checkbox, Code, Flex, Tabs, Text } from '@radix-ui/themes';
|
import { Checkbox, Code, Flex, Tabs, Text } from '@radix-ui/themes';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import type { TankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
|
import type { TankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
|
||||||
import { useLocale } from '../../../../../../../hooks/useLocale';
|
import { useLocale } from '../../../../../../../hooks/useLocale';
|
||||||
|
import { VisualizerCard } from '../VisualizerCard';
|
||||||
|
|
||||||
export interface StatsAcceptorProps {
|
export interface StatsAcceptorProps {
|
||||||
stats: TankCharacteristics;
|
stats: TankCharacteristics;
|
||||||
@@ -58,7 +59,7 @@ export function TraverseVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
}, [terrain, turretSpeedDeg, hullSpeedDeg]);
|
}, [terrain, turretSpeedDeg, hullSpeedDeg]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="classic" style={{ aspectRatio: '1 / 1' }}>
|
<VisualizerCard>
|
||||||
<div
|
<div
|
||||||
ref={hull}
|
ref={hull}
|
||||||
style={{
|
style={{
|
||||||
@@ -188,6 +189,6 @@ export function TraverseVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</VisualizerCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import { Card, type CardProps } from '@radix-ui/themes';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
export const VisualizerCard = forwardRef<HTMLDivElement, CardProps>(
|
||||||
|
({ style, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
ref={ref}
|
||||||
|
variant="classic"
|
||||||
|
mt="4"
|
||||||
|
mb="6"
|
||||||
|
style={{ aspectRatio: '1 / 1', ...style }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
import { Code, Flex, Text } from '@radix-ui/themes';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
side: `${'top' | 'bottom'}-${'left' | 'right'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VisualizerCornerStat({ label, value, side }: Props) {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
gap="2"
|
||||||
|
position="absolute"
|
||||||
|
top={side === 'top-left' || side === 'top-right' ? '3' : undefined}
|
||||||
|
bottom={
|
||||||
|
side === 'bottom-left' || side === 'bottom-right' ? '3' : undefined
|
||||||
|
}
|
||||||
|
left={side === 'top-left' || side === 'bottom-left' ? '3' : undefined}
|
||||||
|
right={side === 'top-right' || side === 'bottom-right' ? '3' : undefined}
|
||||||
|
>
|
||||||
|
<Text color="gray" size="1">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Code size="1" variant="ghost">
|
||||||
|
{value}
|
||||||
|
</Code>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
+15
-14
@@ -3,11 +3,13 @@ import { useEffect } from 'react';
|
|||||||
import { awaitableEquipmentDefinitions } from '../../../../../core/awaitables/equipmentDefinitions';
|
import { awaitableEquipmentDefinitions } from '../../../../../core/awaitables/equipmentDefinitions';
|
||||||
import { awaitableProvisionDefinitions } from '../../../../../core/awaitables/provisionDefinitions';
|
import { awaitableProvisionDefinitions } from '../../../../../core/awaitables/provisionDefinitions';
|
||||||
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
|
||||||
|
import { modelTransformEvent } from '../../../../../core/blitzkit/modelTransform';
|
||||||
import { tankCharacteristics } from '../../../../../core/blitzkit/tankCharacteristics';
|
import { tankCharacteristics } from '../../../../../core/blitzkit/tankCharacteristics';
|
||||||
import { useEquipment } from '../../../../../hooks/useEquipment';
|
import { useEquipment } from '../../../../../hooks/useEquipment';
|
||||||
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
|
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
|
||||||
import { Duel } from '../../../../../stores/duel';
|
import { Duel } from '../../../../../stores/duel';
|
||||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||||
|
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps';
|
||||||
import { Crew } from './components/Crew';
|
import { Crew } from './components/Crew';
|
||||||
import { Firepower } from './components/Firepower';
|
import { Firepower } from './components/Firepower';
|
||||||
import { Maneuverability } from './components/Maneuverability';
|
import { Maneuverability } from './components/Maneuverability';
|
||||||
@@ -18,7 +20,7 @@ const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
|||||||
awaitableProvisionDefinitions,
|
awaitableProvisionDefinitions,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function Characteristics() {
|
export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const mutateDuel = Duel.useMutation();
|
const mutateDuel = Duel.useMutation();
|
||||||
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
|
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
|
||||||
const provisions = Duel.use((state) => state.protagonist.provisions);
|
const provisions = Duel.use((state) => state.protagonist.provisions);
|
||||||
@@ -39,8 +41,7 @@ export function Characteristics() {
|
|||||||
const stockGun = stockTurret.guns[0];
|
const stockGun = stockTurret.guns[0];
|
||||||
const tankModelDefinition = useTankModelDefinition();
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
turretModelDefinition.guns[gun.gun_type!.value.base.id];
|
|
||||||
const stats = tankCharacteristics(
|
const stats = tankCharacteristics(
|
||||||
{
|
{
|
||||||
tank,
|
tank,
|
||||||
@@ -74,16 +75,16 @@ export function Characteristics() {
|
|||||||
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mutateDuel((draft) => {
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
[draft.protagonist.pitch, draft.protagonist.yaw] = applyPitchYawLimits(
|
modelTransformEvent.last!.pitch,
|
||||||
draft.protagonist.pitch,
|
modelTransformEvent.last!.yaw,
|
||||||
draft.protagonist.yaw,
|
gunModelDefinition.pitch,
|
||||||
gunModelDefinition.pitch,
|
turretModelDefinition.yaw,
|
||||||
turretModelDefinition.yaw,
|
hasImprovedVerticalStabilizer,
|
||||||
hasImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
);
|
||||||
);
|
|
||||||
});
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}, [hasImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer]);
|
}, [hasImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -93,7 +94,7 @@ export function Characteristics() {
|
|||||||
align={{ initial: 'center', sm: 'start' }}
|
align={{ initial: 'center', sm: 'start' }}
|
||||||
width={{ initial: '100%', sm: 'auto' }}
|
width={{ initial: '100%', sm: 'auto' }}
|
||||||
>
|
>
|
||||||
<Firepower stats={stats} />
|
<Firepower skeleton={skeleton} stats={stats} />
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
gap="8"
|
gap="8"
|
||||||
|
|||||||
+15
-26
@@ -33,7 +33,7 @@ type ModuleDefinition =
|
|||||||
|
|
||||||
function extractModule(raw: ModuleDefinition | TankDefinition) {
|
function extractModule(raw: ModuleDefinition | TankDefinition) {
|
||||||
if ('gun_type' in raw) {
|
if ('gun_type' in raw) {
|
||||||
return raw.gun_type!.value.base;
|
return raw;
|
||||||
} else
|
} else
|
||||||
return raw as Exclude<ModuleDefinition | TankDefinition, GunDefinition>;
|
return raw as Exclude<ModuleDefinition | TankDefinition, GunDefinition>;
|
||||||
}
|
}
|
||||||
@@ -179,37 +179,31 @@ export function Modules() {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!draft.protagonist.turret.guns.some(
|
!draft.protagonist.turret.guns.some(
|
||||||
(gun) =>
|
(gun) => gun.id === draft.protagonist.gun.id,
|
||||||
gun.gun_type!.value.base.id ===
|
|
||||||
draft.protagonist.gun.gun_type!.value.base.id,
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
|
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
|
||||||
draft.protagonist.shell =
|
draft.protagonist.shell = draft.protagonist.gun.shells[0];
|
||||||
draft.protagonist.gun.gun_type!.value.base.shells[0];
|
|
||||||
}
|
}
|
||||||
} else if (unlock.type === ModuleType.GUN) {
|
} else if (unlock.type === ModuleType.GUN) {
|
||||||
const gunInTurret = draft.protagonist.turret.guns.find(
|
const gunInTurret = draft.protagonist.turret.guns.find(
|
||||||
(gun) => gun.gun_type!.value.base.id === unlock.id,
|
(gun) => gun.id === unlock.id,
|
||||||
);
|
);
|
||||||
if (gunInTurret) {
|
if (gunInTurret) {
|
||||||
draft.protagonist.gun = gunInTurret;
|
draft.protagonist.gun = gunInTurret;
|
||||||
draft.protagonist.shell = gunInTurret.gun_type!.value.base.shells[0];
|
draft.protagonist.shell = gunInTurret.shells[0];
|
||||||
} else {
|
} else {
|
||||||
// TODO: warn somehow?
|
// TODO: warn somehow?
|
||||||
const suitableTurret = draft.protagonist.tank.turrets.find((turret) =>
|
const suitableTurret = draft.protagonist.tank.turrets.find((turret) =>
|
||||||
turret.guns.some(
|
turret.guns.some((gun) => gun.id === unlock.id),
|
||||||
(gun) => gun.gun_type!.value.base.id === unlock.id,
|
|
||||||
),
|
|
||||||
)!;
|
)!;
|
||||||
const gunInSuitableTurret = suitableTurret.guns.find(
|
const gunInSuitableTurret = suitableTurret.guns.find(
|
||||||
(gun) => gun.gun_type!.value.base.id === unlock.id,
|
(gun) => gun.id === unlock.id,
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
draft.protagonist.turret = suitableTurret;
|
draft.protagonist.turret = suitableTurret;
|
||||||
draft.protagonist.gun = gunInSuitableTurret;
|
draft.protagonist.gun = gunInSuitableTurret;
|
||||||
draft.protagonist.shell =
|
draft.protagonist.shell = gunInSuitableTurret.shells[0];
|
||||||
gunInSuitableTurret.gun_type!.value.base.shells[0];
|
|
||||||
}
|
}
|
||||||
} else if (unlock.type === ModuleType.ENGINE) {
|
} else if (unlock.type === ModuleType.ENGINE) {
|
||||||
draft.protagonist.engine = draft.protagonist.tank.engines.find(
|
draft.protagonist.engine = draft.protagonist.tank.engines.find(
|
||||||
@@ -241,11 +235,9 @@ export function Modules() {
|
|||||||
} else if (unlock.type === ModuleType.GUN) {
|
} else if (unlock.type === ModuleType.GUN) {
|
||||||
module = tank.turrets
|
module = tank.turrets
|
||||||
.find((turret) =>
|
.find((turret) =>
|
||||||
turret.guns.some(
|
turret.guns.some((gun) => gun.id === unlock.id),
|
||||||
(gun) => gun.gun_type!.value.base.id === unlock.id,
|
|
||||||
),
|
|
||||||
)!
|
)!
|
||||||
.guns.find((gun) => gun.gun_type!.value.base.id === unlock.id)!;
|
.guns.find((gun) => gun.id === unlock.id)!;
|
||||||
} else if (unlock.type === ModuleType.VEHICLE) {
|
} else if (unlock.type === ModuleType.VEHICLE) {
|
||||||
module = tankDefinitions.tanks[unlock.id];
|
module = tankDefinitions.tanks[unlock.id];
|
||||||
}
|
}
|
||||||
@@ -308,7 +300,7 @@ export function Modules() {
|
|||||||
(unlock.type === ModuleType.TURRET
|
(unlock.type === ModuleType.TURRET
|
||||||
? turret.id
|
? turret.id
|
||||||
: unlock.type === ModuleType.GUN
|
: unlock.type === ModuleType.GUN
|
||||||
? gun.gun_type!.value.base.id
|
? gun.id
|
||||||
: unlock.type === ModuleType.ENGINE
|
: unlock.type === ModuleType.ENGINE
|
||||||
? engine.id
|
? engine.id
|
||||||
: unlock.type === ModuleType.TRACKS
|
: unlock.type === ModuleType.TRACKS
|
||||||
@@ -319,8 +311,7 @@ export function Modules() {
|
|||||||
unlock.type === ModuleType.TURRET
|
unlock.type === ModuleType.TURRET
|
||||||
? extractModule(module).id == topTurret.id
|
? extractModule(module).id == topTurret.id
|
||||||
: unlock.type === ModuleType.GUN
|
: unlock.type === ModuleType.GUN
|
||||||
? extractModule(module).id ===
|
? extractModule(module).id === topGun.id
|
||||||
topGun.gun_type!.value.base.id
|
|
||||||
: unlock.type === ModuleType.ENGINE
|
: unlock.type === ModuleType.ENGINE
|
||||||
? extractModule(module).id === topEngine.id
|
? extractModule(module).id === topEngine.id
|
||||||
: unlock.type === ModuleType.TRACKS
|
: unlock.type === ModuleType.TRACKS
|
||||||
@@ -371,8 +362,7 @@ export function Modules() {
|
|||||||
mutateDuel((draft) => {
|
mutateDuel((draft) => {
|
||||||
draft.protagonist.turret = draft.protagonist.tank.turrets[0];
|
draft.protagonist.turret = draft.protagonist.tank.turrets[0];
|
||||||
draft.protagonist.gun = draft.protagonist.turret.guns[0];
|
draft.protagonist.gun = draft.protagonist.turret.guns[0];
|
||||||
draft.protagonist.shell =
|
draft.protagonist.shell = draft.protagonist.gun.shells[0];
|
||||||
draft.protagonist.gun.gun_type!.value.base.shells[0];
|
|
||||||
draft.protagonist.engine = draft.protagonist.tank.engines[0];
|
draft.protagonist.engine = draft.protagonist.tank.engines[0];
|
||||||
draft.protagonist.track = draft.protagonist.tank.tracks[0];
|
draft.protagonist.track = draft.protagonist.tank.tracks[0];
|
||||||
});
|
});
|
||||||
@@ -387,8 +377,7 @@ export function Modules() {
|
|||||||
draft.protagonist.turret =
|
draft.protagonist.turret =
|
||||||
draft.protagonist.tank.turrets.at(-1)!;
|
draft.protagonist.tank.turrets.at(-1)!;
|
||||||
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
|
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
|
||||||
draft.protagonist.shell =
|
draft.protagonist.shell = draft.protagonist.gun.shells[0];
|
||||||
draft.protagonist.gun.gun_type!.value.base.shells[0];
|
|
||||||
draft.protagonist.engine =
|
draft.protagonist.engine =
|
||||||
draft.protagonist.tank.engines.at(-1)!;
|
draft.protagonist.tank.engines.at(-1)!;
|
||||||
draft.protagonist.track =
|
draft.protagonist.track =
|
||||||
@@ -427,7 +416,7 @@ export function Modules() {
|
|||||||
{tree(ModuleType.GUN, [
|
{tree(ModuleType.GUN, [
|
||||||
{
|
{
|
||||||
cost: { type: 'xp', value: 0 },
|
cost: { type: 'xp', value: 0 },
|
||||||
id: gun0.gun_type!.value.base.id,
|
id: gun0.id,
|
||||||
type: ModuleType.GUN,
|
type: ModuleType.GUN,
|
||||||
},
|
},
|
||||||
])}
|
])}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Flex } from '@radix-ui/themes';
|
import { Flex } from '@radix-ui/themes';
|
||||||
|
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
||||||
import { Characteristics } from './components/Characteristics';
|
import { Characteristics } from './components/Characteristics';
|
||||||
import { CompareOptions } from './components/Characteristics/components/CompareOptions';
|
import { CompareOptions } from './components/Characteristics/components/CompareOptions';
|
||||||
import { StatSearch } from './components/Characteristics/components/StatSearch';
|
import { StatSearch } from './components/Characteristics/components/StatSearch';
|
||||||
@@ -9,7 +10,9 @@ import { Modules } from './components/Modules';
|
|||||||
import { Provisions } from './components/Provisions';
|
import { Provisions } from './components/Provisions';
|
||||||
import { Skills } from './components/Skills';
|
import { Skills } from './components/Skills';
|
||||||
|
|
||||||
export function CharacteristicsSection() {
|
export function CharacteristicsSection({
|
||||||
|
skeleton,
|
||||||
|
}: MaybeSkeletonComponentProps) {
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="3">
|
<Flex direction="column" gap="3">
|
||||||
<Flex
|
<Flex
|
||||||
@@ -64,7 +67,7 @@ export function CharacteristicsSection() {
|
|||||||
<CompareOptions />
|
<CompareOptions />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Characteristics />
|
<Characteristics skeleton={skeleton} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { CaretRightIcon } from '@radix-ui/react-icons';
|
||||||
|
import { Box, Flex, Link, Text } from '@radix-ui/themes';
|
||||||
|
import { Var } from '../../../core/radix/var';
|
||||||
|
import type { TankGuide } from '../../../pages/[...locale]/tanks/[slug]/index.astro';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
guide: TankGuide;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GuideSection({ guide }: Props) {
|
||||||
|
return (
|
||||||
|
<Flex justify="center">
|
||||||
|
<Link
|
||||||
|
color="gray"
|
||||||
|
href={guide.url}
|
||||||
|
underline="none"
|
||||||
|
highContrast
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
maxWidth: '35rem',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: Var('space-4'),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex maxWidth="35rem" direction="column" gap="4">
|
||||||
|
<Flex direction="column" gap="4" position="relative">
|
||||||
|
<Text size="4" weight="medium">
|
||||||
|
<Flex width="100%" justify="start" align="center" gap="2">
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
width: '1.5em',
|
||||||
|
height: '1.5em',
|
||||||
|
objectFit: 'contain',
|
||||||
|
}}
|
||||||
|
src="https://guidesblitz.com/wp-content/uploads/2023/11/cropped-logo-1.png"
|
||||||
|
/>
|
||||||
|
GuidesBlitz
|
||||||
|
</Flex>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text size="2">{guide.description}</Text>
|
||||||
|
<img style={{ width: '100%' }} src={guide.image} />
|
||||||
|
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
bottom="0"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
left="0"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(${Var('gray-a1')}, ${Var('gray-1')})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Text color="purple">
|
||||||
|
<Flex width="100%" justify="center" align="center" gap="1">
|
||||||
|
Read the full guide
|
||||||
|
<CaretRightIcon width="1em" height="1em" />
|
||||||
|
</Flex>
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Link>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-1
@@ -90,7 +90,7 @@ export function CustomShellButton() {
|
|||||||
modified.normalization = undefined;
|
modified.normalization = undefined;
|
||||||
modified.ricochet = undefined;
|
modified.ricochet = undefined;
|
||||||
modified.explosion_radius =
|
modified.explosion_radius =
|
||||||
gun.gun_type!.value.base.shells.find(
|
gun.shells.find(
|
||||||
(shell) => shell.type === ShellType.HE,
|
(shell) => shell.type === ShellType.HE,
|
||||||
)?.explosion_radius ?? 1;
|
)?.explosion_radius ?? 1;
|
||||||
break;
|
break;
|
||||||
|
|||||||
+23
-36
@@ -1,5 +1,6 @@
|
|||||||
import { HeightIcon, WidthIcon } from '@radix-ui/react-icons';
|
import { HeightIcon, WidthIcon } from '@radix-ui/react-icons';
|
||||||
import { Flex, TextField } from '@radix-ui/themes';
|
import { Flex, TextField } from '@radix-ui/themes';
|
||||||
|
import type { QuicklimeEvent } from 'quicklime';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { degToRad, radToDeg } from 'three/src/math/MathUtils.js';
|
import { degToRad, radToDeg } from 'three/src/math/MathUtils.js';
|
||||||
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
@@ -20,39 +21,26 @@ export function QuickInputs() {
|
|||||||
const tankModelDefinition = useTankModelDefinition();
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
const turretModelDefinition =
|
const turretModelDefinition =
|
||||||
tankModelDefinition.turrets[protagonist.turret.id];
|
tankModelDefinition.turrets[protagonist.turret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
||||||
turretModelDefinition.guns[protagonist.gun.gun_type!.value.base.id];
|
|
||||||
const initialGunPitch =
|
const initialGunPitch =
|
||||||
tankModelDefinition.initial_turret_rotation?.pitch ?? 0;
|
tankModelDefinition.initial_turret_rotation?.pitch ?? 0;
|
||||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||||
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!yawInput.current) return;
|
function handleTransformEvent(
|
||||||
|
event: QuicklimeEvent<ModelTransformEventData>,
|
||||||
yawInput.current.value = radToDeg(protagonist.yaw).toFixed(1);
|
) {
|
||||||
}, [protagonist.yaw]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!pitchInput.current) return;
|
|
||||||
|
|
||||||
pitchInput.current.value = (
|
|
||||||
-radToDeg(protagonist.pitch) + initialGunPitch
|
|
||||||
).toFixed(1);
|
|
||||||
}, [protagonist.pitch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function handleTransformEvent({ pitch, yaw }: ModelTransformEventData) {
|
|
||||||
if (!pitchInput.current || !yawInput.current) return;
|
if (!pitchInput.current || !yawInput.current) return;
|
||||||
|
|
||||||
pitchInput.current.value = (
|
pitchInput.current.value = (
|
||||||
-radToDeg(pitch) +
|
-radToDeg(event.data.pitch) +
|
||||||
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
|
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
|
||||||
).toFixed(1);
|
).toFixed(1);
|
||||||
|
|
||||||
if (yaw === undefined) return;
|
if (event.data.yaw === undefined) return;
|
||||||
|
|
||||||
yawInput.current.value = radToDeg(yaw).toFixed(1);
|
yawInput.current.value = radToDeg(event.data.yaw).toFixed(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
modelTransformEvent.on(handleTransformEvent);
|
modelTransformEvent.on(handleTransformEvent);
|
||||||
@@ -85,24 +73,27 @@ export function QuickInputs() {
|
|||||||
defaultValue="0.0"
|
defaultValue="0.0"
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
const value = Number(yawInput.current!.value);
|
const value = Number(yawInput.current!.value);
|
||||||
|
|
||||||
if (isNaN(value)) {
|
if (isNaN(value)) {
|
||||||
yawInput.current!.value = radToDeg(protagonist.yaw).toFixed(1);
|
yawInput.current!.value = radToDeg(
|
||||||
|
modelTransformEvent.last!.yaw,
|
||||||
|
).toFixed(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [pitch, yaw] = applyPitchYawLimits(
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
protagonist.pitch,
|
modelTransformEvent.last!.pitch,
|
||||||
degToRad(value),
|
degToRad(value),
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
|
||||||
mutateDuel((state) => {
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
state.protagonist.pitch = pitch;
|
yawInput.current!.value = radToDeg(
|
||||||
state.protagonist.yaw = yaw;
|
modelTransformEvent.last!.yaw,
|
||||||
});
|
).toFixed(1);
|
||||||
yawInput.current!.value = radToDeg(protagonist.yaw).toFixed(1);
|
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
@@ -128,25 +119,21 @@ export function QuickInputs() {
|
|||||||
const value = Number(pitchInput.current!.value);
|
const value = Number(pitchInput.current!.value);
|
||||||
if (isNaN(value)) {
|
if (isNaN(value)) {
|
||||||
pitchInput.current!.value = (
|
pitchInput.current!.value = (
|
||||||
-radToDeg(protagonist.pitch) + initialGunPitch
|
-radToDeg(modelTransformEvent.last!.pitch) + initialGunPitch
|
||||||
).toFixed(1);
|
).toFixed(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const [pitch, yaw] = applyPitchYawLimits(
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
degToRad(-value + initialGunPitch),
|
degToRad(-value + initialGunPitch),
|
||||||
protagonist.yaw,
|
modelTransformEvent.last!.yaw,
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
mutateDuel((state) => {
|
|
||||||
state.protagonist.pitch = pitch;
|
|
||||||
state.protagonist.yaw = yaw;
|
|
||||||
});
|
|
||||||
pitchInput.current!.value = (
|
pitchInput.current!.value = (
|
||||||
-radToDeg(protagonist.pitch) + initialGunPitch
|
-radToDeg(modelTransformEvent.last!.pitch) + initialGunPitch
|
||||||
).toFixed(1);
|
).toFixed(1);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
|
|||||||
+9
-45
@@ -15,7 +15,6 @@ import {
|
|||||||
GearIcon,
|
GearIcon,
|
||||||
} from '@radix-ui/react-icons';
|
} from '@radix-ui/react-icons';
|
||||||
import {
|
import {
|
||||||
Box,
|
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -36,10 +35,7 @@ import { useFullscreenAvailability } from '../../../../../hooks/useFullscreenAva
|
|||||||
import { useLocale } from '../../../../../hooks/useLocale';
|
import { useLocale } from '../../../../../hooks/useLocale';
|
||||||
import { App } from '../../../../../stores/app';
|
import { App } from '../../../../../stores/app';
|
||||||
import { Duel } from '../../../../../stores/duel';
|
import { Duel } from '../../../../../stores/duel';
|
||||||
import {
|
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||||
ShootingRangeZoom,
|
|
||||||
TankopediaEphemeral,
|
|
||||||
} from '../../../../../stores/tankopediaEphemeral';
|
|
||||||
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
|
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
|
||||||
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
|
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
|
||||||
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps';
|
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps';
|
||||||
@@ -97,23 +93,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
<>
|
<>
|
||||||
{disturbed && <QuickInputs />}
|
{disturbed && <QuickInputs />}
|
||||||
|
|
||||||
{display === TankopediaDisplay.ShootingRange && (
|
|
||||||
<Box
|
|
||||||
width="1rem"
|
|
||||||
height="1rem"
|
|
||||||
position="absolute"
|
|
||||||
top="50%"
|
|
||||||
left="50%"
|
|
||||||
style={{
|
|
||||||
zIndex: 2,
|
|
||||||
backgroundImage: 'url(/assets/images/icons/aim-caret.png)',
|
|
||||||
backgroundSize: 'contain',
|
|
||||||
transform: 'translateX(-50%)',
|
|
||||||
borderRadius: '50%',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Thicknesses skeleton={skeleton} thicknessRange={thicknessRange} />
|
<Thicknesses skeleton={skeleton} thicknessRange={thicknessRange} />
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
@@ -162,7 +141,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
mr="2"
|
mr="2"
|
||||||
mb="1"
|
mb="1"
|
||||||
>
|
>
|
||||||
{TIER_ROMAN_NUMERALS[antagonistGun.gun_type!.value.base.tier]}
|
{TIER_ROMAN_NUMERALS[antagonistGun.tier]}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
@@ -182,26 +161,20 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
{[...antagonistUniqueGuns].reverse().map(({ gun, turret }) => (
|
{[...antagonistUniqueGuns].reverse().map(({ gun, turret }) => (
|
||||||
<ModuleButton
|
<ModuleButton
|
||||||
module="gun"
|
module="gun"
|
||||||
key={gun.gun_type!.value.base.id}
|
key={gun.id}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
mutateDuel((draft) => {
|
mutateDuel((draft) => {
|
||||||
draft.antagonist.turret = turret;
|
draft.antagonist.turret = turret;
|
||||||
draft.antagonist.gun = gun;
|
draft.antagonist.gun = gun;
|
||||||
draft.antagonist.shell =
|
draft.antagonist.shell = gun.shells[0];
|
||||||
gun.gun_type!.value.base.shells[0];
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
selected={
|
selected={gun.id === antagonistGun.id}
|
||||||
gun.gun_type!.value.base.id ===
|
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
|
||||||
antagonistGun.gun_type!.value.base.id
|
|
||||||
}
|
|
||||||
discriminator={
|
|
||||||
TIER_ROMAN_NUMERALS[gun.gun_type!.value.base.tier]
|
|
||||||
}
|
|
||||||
secondaryDiscriminator={
|
secondaryDiscriminator={
|
||||||
<Text style={{ fontSize: '0.75em' }}>
|
<Text style={{ fontSize: '0.75em' }}>
|
||||||
{literals(strings.common.units.mm, [
|
{literals(strings.common.units.mm, [
|
||||||
gun.gun_type!.value.base.shells[0].caliber.toFixed(0),
|
gun.shells[0].caliber.toFixed(0),
|
||||||
])}
|
])}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
@@ -219,7 +192,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
}}
|
}}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
>
|
>
|
||||||
{antagonistGun.gun_type!.value.base.shells.map((thisShell) => (
|
{antagonistGun.shells.map((thisShell) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
color={
|
color={
|
||||||
thisShell.id === antagonistShell.id && !hasCustomShell
|
thisShell.id === antagonistShell.id && !hasCustomShell
|
||||||
@@ -388,8 +361,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
draft.antagonist.turret = tank.turrets.at(-1)!;
|
draft.antagonist.turret = tank.turrets.at(-1)!;
|
||||||
draft.antagonist.gun =
|
draft.antagonist.gun =
|
||||||
draft.antagonist.turret.guns.at(-1)!;
|
draft.antagonist.turret.guns.at(-1)!;
|
||||||
draft.antagonist.shell =
|
draft.antagonist.shell = draft.antagonist.gun.shells[0];
|
||||||
draft.antagonist.gun.gun_type!.value.base.shells[0];
|
|
||||||
});
|
});
|
||||||
setAntagonistSelectorOpen(false);
|
setAntagonistSelectorOpen(false);
|
||||||
}}
|
}}
|
||||||
@@ -408,7 +380,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
mutateTankopediaEphemeral((draft) => {
|
mutateTankopediaEphemeral((draft) => {
|
||||||
draft.disturbed = true;
|
draft.disturbed = true;
|
||||||
draft.display = Number(value);
|
draft.display = Number(value);
|
||||||
draft.shootingRangeZoom = ShootingRangeZoom.Arcade;
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -448,13 +419,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
{/* <SegmentedControl.Item value={`${TankopediaDisplay.ShootingRange}`}>
|
|
||||||
<Tooltip content="Shooting range">
|
|
||||||
<Flex height="100%" align="center">
|
|
||||||
<img src="/assets/images/icons/tankopedia-shooting-range.png" style={{ height: '1.25em' }} />
|
|
||||||
</Flex>
|
|
||||||
</Tooltip>
|
|
||||||
</SegmentedControl.Item> */}
|
|
||||||
</SegmentedControl.Root>
|
</SegmentedControl.Root>
|
||||||
|
|
||||||
{disturbed && (
|
{disturbed && (
|
||||||
|
|||||||
+18
-4
@@ -1,5 +1,5 @@
|
|||||||
import { TankType } from '@blitzkit/core';
|
import { TankType } from '@blitzkit/core';
|
||||||
import { Box, Heading } from '@radix-ui/themes';
|
import { Flex, Heading } from '@radix-ui/themes';
|
||||||
import { Var } from '../../../../../../core/radix/var';
|
import { Var } from '../../../../../../core/radix/var';
|
||||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||||
import { Duel } from '../../../../../../stores/duel';
|
import { Duel } from '../../../../../../stores/duel';
|
||||||
@@ -87,13 +87,27 @@ export function Title({ foreground }: TitleProps) {
|
|||||||
? '2rem'
|
? '2rem'
|
||||||
: `min(65vh, ${125 / name.length}vw)`
|
: `min(65vh, ${125 / name.length}vw)`
|
||||||
: `min(48vh, ${75 / name.length}vw)`;
|
: `min(48vh, ${75 / name.length}vw)`;
|
||||||
|
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Flex
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
mutateTankopediaEphemeral((draft) => {
|
||||||
|
draft.disturbed = true;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
direction="column"
|
||||||
position="absolute"
|
position="absolute"
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
top={disturbed ? '6rem' : '50%'}
|
top={disturbed ? '6rem' : '50%'}
|
||||||
left="50%"
|
left="50%"
|
||||||
|
width="100%"
|
||||||
|
height={disturbed ? fontSize : '100%'}
|
||||||
style={{
|
style={{
|
||||||
|
pointerEvents: disturbed ? 'none' : undefined,
|
||||||
transitionDuration: '1s',
|
transitionDuration: '1s',
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
}}
|
}}
|
||||||
@@ -109,7 +123,7 @@ export function Title({ foreground }: TitleProps) {
|
|||||||
opacity: foreground && disturbed ? 0 : 1,
|
opacity: foreground && disturbed ? 0 : 1,
|
||||||
letterSpacing: disturbed || !revealed ? 0 : '-0.03em',
|
letterSpacing: disturbed || !revealed ? 0 : '-0.03em',
|
||||||
transition: `
|
transition: `
|
||||||
letter-spacing 1.5s ${disturbed ? '' : 'cubic-bezier(.81,-2,.68,1)'},
|
letter-spacing 1.5s ${disturbed ? '' : 'cubic-bezier(0.81, -2, 0.68, 1)'},
|
||||||
font-size 1s,
|
font-size 1s,
|
||||||
-webkit-text-stroke 2s,
|
-webkit-text-stroke 2s,
|
||||||
opacity 1s
|
opacity 1s
|
||||||
@@ -121,6 +135,6 @@ export function Title({ foreground }: TitleProps) {
|
|||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
{!revealed && <Tracker fontSize={fontSize} />}
|
{!revealed && <Tracker fontSize={fontSize} />}
|
||||||
</Box>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+20
-141
@@ -1,23 +1,15 @@
|
|||||||
import { I_HAT, J_HAT } from '@blitzkit/core';
|
import { I_HAT, J_HAT } from '@blitzkit/core';
|
||||||
import { OrbitControls } from '@react-three/drei';
|
import { OrbitControls } from '@react-three/drei';
|
||||||
import { invalidate, useThree } from '@react-three/fiber';
|
import { invalidate, useFrame, useThree } from '@react-three/fiber';
|
||||||
import { clamp } from 'lodash-es';
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { PerspectiveCamera, Vector3 } from 'three';
|
import { PerspectiveCamera, Vector3 } from 'three';
|
||||||
import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
|
import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
|
||||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
|
||||||
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
|
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
|
||||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
||||||
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose';
|
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose';
|
||||||
import { Duel } from '../../../../../../stores/duel';
|
import { Duel } from '../../../../../../stores/duel';
|
||||||
import {
|
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||||
SHOOTING_RANGE_ZOOM_COEFFICIENTS,
|
|
||||||
ShootingRangeZoom,
|
|
||||||
TankopediaEphemeral,
|
|
||||||
} from '../../../../../../stores/tankopediaEphemeral';
|
|
||||||
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
|
|
||||||
import { aimTarget } from './SceneProps/constants';
|
|
||||||
|
|
||||||
const poseDistances: Record<Pose, number> = {
|
const poseDistances: Record<Pose, number> = {
|
||||||
[Pose.HullDown]: 15,
|
[Pose.HullDown]: 15,
|
||||||
@@ -27,11 +19,6 @@ const poseDistances: Record<Pose, number> = {
|
|||||||
|
|
||||||
const modelDefinitions = await awaitableModelDefinitions;
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
|
|
||||||
const ARCADE_MODE_DISTANCE = 19;
|
|
||||||
// const ARCADE_MODE_ANGLE = Math.PI / 8;
|
|
||||||
const ARCADE_MODE_ANGLE = degToRad(10);
|
|
||||||
const ARCADE_MODE_FOV = 54;
|
|
||||||
|
|
||||||
interface ControlsProps {
|
interface ControlsProps {
|
||||||
naked?: boolean;
|
naked?: boolean;
|
||||||
autoRotate?: boolean;
|
autoRotate?: boolean;
|
||||||
@@ -48,7 +35,6 @@ export function Controls({
|
|||||||
distanceScale = 1,
|
distanceScale = 1,
|
||||||
}: ControlsProps) {
|
}: ControlsProps) {
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||||
const display = TankopediaEphemeral.use((state) => state.display);
|
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||||
const camera = useThree((state) => state.camera as PerspectiveCamera);
|
const camera = useThree((state) => state.camera as PerspectiveCamera);
|
||||||
@@ -70,9 +56,7 @@ export function Controls({
|
|||||||
const antagonistTurretModelDefinition =
|
const antagonistTurretModelDefinition =
|
||||||
antagonistModelDefinition.turrets[antagonistTurret.id];
|
antagonistModelDefinition.turrets[antagonistTurret.id];
|
||||||
const protagonistGunModelDefinition =
|
const protagonistGunModelDefinition =
|
||||||
protagonistTurretModelDefinition.guns[
|
protagonistTurretModelDefinition.guns[protagonistGun.id];
|
||||||
protagonistGun.gun_type!.value.base.id
|
|
||||||
];
|
|
||||||
const protagonistHullOrigin = new Vector3(
|
const protagonistHullOrigin = new Vector3(
|
||||||
protagonistTrackModelDefinition.origin.x,
|
protagonistTrackModelDefinition.origin.x,
|
||||||
protagonistTrackModelDefinition.origin.y,
|
protagonistTrackModelDefinition.origin.y,
|
||||||
@@ -103,42 +87,21 @@ export function Controls({
|
|||||||
gunHeight + (naked ? 10 : 2),
|
gunHeight + (naked ? 10 : 2),
|
||||||
-13,
|
-13,
|
||||||
).multiplyScalar(distanceScale);
|
).multiplyScalar(distanceScale);
|
||||||
const protagonistGunOriginOnlyY = new Vector3(
|
|
||||||
0,
|
|
||||||
protagonistTurretModelDefinition.gun_origin.y,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
const shellOrigin = protagonistHullOrigin
|
|
||||||
.clone()
|
|
||||||
.add(protagonistTurretOrigin)
|
|
||||||
.add(protagonistGunOriginOnlyY);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const t0 = useRef(Date.now() / 1000);
|
||||||
if (!orbitControls.current || !doAutoRotate) return;
|
useFrame(() => {
|
||||||
|
if (!doAutoRotate) return;
|
||||||
|
|
||||||
const t0 = Date.now() / 1000;
|
const now = Date.now() / 1000;
|
||||||
let cancel = false;
|
const t = now - t0.current;
|
||||||
|
|
||||||
function frame() {
|
camera.position
|
||||||
if (!cancel) requestAnimationFrame(frame);
|
.copy(inspectModeInitialPosition)
|
||||||
|
.applyAxisAngle(I_HAT, (Math.PI / 32) * Math.sin(t / 9))
|
||||||
|
.applyAxisAngle(J_HAT, (-Math.PI / 16) * Math.sin(t / 7));
|
||||||
|
|
||||||
const now = Date.now() / 1000;
|
invalidate();
|
||||||
const t = now - t0;
|
});
|
||||||
|
|
||||||
camera.position
|
|
||||||
.copy(inspectModeInitialPosition)
|
|
||||||
.applyAxisAngle(I_HAT, (Math.PI / 32) * Math.sin(t / 9))
|
|
||||||
.applyAxisAngle(J_HAT, (-Math.PI / 16) * Math.sin(t / 7));
|
|
||||||
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
frame();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancel = true;
|
|
||||||
};
|
|
||||||
}, [doAutoRotate, camera]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe(
|
const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe(
|
||||||
@@ -252,38 +215,11 @@ export function Controls({
|
|||||||
}, [camera, protagonistTank.id, antagonistTank.id]);
|
}, [camera, protagonistTank.id, antagonistTank.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isInitialIteration = true;
|
|
||||||
|
|
||||||
function handleKeyDown(event: KeyboardEvent) {
|
|
||||||
if (display !== TankopediaDisplay.ShootingRange || event.repeat) return;
|
|
||||||
|
|
||||||
if (event.key === 'Shift') {
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
|
||||||
draft.shootingRangeZoom =
|
|
||||||
draft.shootingRangeZoom === ShootingRangeZoom.Arcade
|
|
||||||
? ShootingRangeZoom.Zoom0
|
|
||||||
: ShootingRangeZoom.Arcade;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleWheel(event: WheelEvent) {
|
function handleWheel(event: WheelEvent) {
|
||||||
if (display !== TankopediaDisplay.ShootingRange) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
|
||||||
draft.shootingRangeZoom = clamp(
|
|
||||||
draft.shootingRangeZoom - Math.sign(event.deltaY),
|
|
||||||
ShootingRangeZoom.Arcade,
|
|
||||||
ShootingRangeZoom.Zoom2,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleScroll(event: Event) {
|
function handleScroll(event: Event) {
|
||||||
if (display !== TankopediaDisplay.ShootingRange) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +231,6 @@ export function Controls({
|
|||||||
|
|
||||||
poseEvent.on(disturb);
|
poseEvent.on(disturb);
|
||||||
canvas.addEventListener('pointerdown', disturb);
|
canvas.addEventListener('pointerdown', disturb);
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
|
||||||
canvas.addEventListener('wheel', handleWheel);
|
canvas.addEventListener('wheel', handleWheel);
|
||||||
document.body.addEventListener('scroll', handleScroll);
|
document.body.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
@@ -304,80 +239,24 @@ export function Controls({
|
|||||||
|
|
||||||
orbitControls.current.rotateSpeed = 0.25;
|
orbitControls.current.rotateSpeed = 0.25;
|
||||||
|
|
||||||
if (display === TankopediaDisplay.ShootingRange) {
|
camera.position.copy(inspectModeInitialPosition);
|
||||||
const { shootingRangeZoom } = tankopediaEphemeralStore.getState();
|
orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0);
|
||||||
|
orbitControls.current.enablePan = true;
|
||||||
orbitControls.current.enablePan = false;
|
orbitControls.current.enableZoom = true;
|
||||||
orbitControls.current.enableZoom = false;
|
camera.fov = naked ? 20 : 25;
|
||||||
orbitControls.current.rotateSpeed *=
|
|
||||||
SHOOTING_RANGE_ZOOM_COEFFICIENTS[shootingRangeZoom];
|
|
||||||
camera.fov =
|
|
||||||
ARCADE_MODE_FOV * SHOOTING_RANGE_ZOOM_COEFFICIENTS[shootingRangeZoom];
|
|
||||||
|
|
||||||
if (shootingRangeZoom === ShootingRangeZoom.Arcade) {
|
|
||||||
orbitControls.current.target.set(0, gunHeight + 3, 0);
|
|
||||||
|
|
||||||
if (isInitialIteration) {
|
|
||||||
isInitialIteration = false;
|
|
||||||
camera.position.set(
|
|
||||||
0,
|
|
||||||
gunHeight + ARCADE_MODE_DISTANCE * Math.sin(ARCADE_MODE_ANGLE),
|
|
||||||
ARCADE_MODE_DISTANCE * Math.cos(ARCADE_MODE_ANGLE),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
camera.position
|
|
||||||
.copy(aimTarget)
|
|
||||||
.sub(orbitControls.current.target)
|
|
||||||
.normalize()
|
|
||||||
.multiplyScalar(-ARCADE_MODE_DISTANCE)
|
|
||||||
.add(orbitControls.current.target);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
orbitControls.current.target.copy(shellOrigin);
|
|
||||||
|
|
||||||
camera.position
|
|
||||||
.copy(aimTarget)
|
|
||||||
.sub(shellOrigin)
|
|
||||||
.normalize()
|
|
||||||
.multiplyScalar(-Number.EPSILON)
|
|
||||||
.add(shellOrigin);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
camera.position.copy(inspectModeInitialPosition);
|
|
||||||
orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0);
|
|
||||||
orbitControls.current.enablePan = true;
|
|
||||||
orbitControls.current.enableZoom = true;
|
|
||||||
camera.fov = naked ? 20 : 25;
|
|
||||||
}
|
|
||||||
|
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCamera();
|
updateCamera();
|
||||||
|
|
||||||
const unsubscribeDisplay = tankopediaEphemeralStore.subscribe(
|
|
||||||
(state) => state.display === TankopediaDisplay.ShootingRange,
|
|
||||||
() => {
|
|
||||||
disturb();
|
|
||||||
updateCamera();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const unsubscribeShootingRangeZoom = tankopediaEphemeralStore.subscribe(
|
|
||||||
(state) => state.shootingRangeZoom,
|
|
||||||
updateCamera,
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
canvas.removeEventListener('pointerdown', disturb);
|
canvas.removeEventListener('pointerdown', disturb);
|
||||||
poseEvent.off(disturb);
|
poseEvent.off(disturb);
|
||||||
unsubscribeDisplay();
|
|
||||||
unsubscribeShootingRangeZoom();
|
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
canvas.removeEventListener('wheel', handleWheel);
|
canvas.removeEventListener('wheel', handleWheel);
|
||||||
document.body.removeEventListener('scroll', handleScroll);
|
document.body.removeEventListener('scroll', handleScroll);
|
||||||
};
|
};
|
||||||
}, [display === TankopediaDisplay.ShootingRange]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OrbitControls
|
<OrbitControls
|
||||||
|
|||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||||
|
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
|
||||||
|
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
|
import { modelTransformEvent } from '../../../../../../core/blitzkit/modelTransform';
|
||||||
|
import { Duel } from '../../../../../../stores/duel';
|
||||||
|
|
||||||
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
|
|
||||||
|
export function InitialAligner() {
|
||||||
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
|
|
||||||
|
const tankModel = modelDefinitions.models[tank.id];
|
||||||
|
const turretModel = tankModel.turrets[turret.id];
|
||||||
|
const gunModel = turretModel.guns[gun.id];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
|
degToRad(8),
|
||||||
|
degToRad(25),
|
||||||
|
gunModel.pitch,
|
||||||
|
turretModel.yaw,
|
||||||
|
);
|
||||||
|
|
||||||
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
-19
@@ -1,5 +1,4 @@
|
|||||||
import { Environment } from '@react-three/drei';
|
import { Environment } from '@react-three/drei';
|
||||||
import { times } from 'lodash-es';
|
|
||||||
import { Euler } from 'three';
|
import { Euler } from 'three';
|
||||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||||
import { useModel } from '../../../../../../hooks/useModel';
|
import { useModel } from '../../../../../../hooks/useModel';
|
||||||
@@ -30,24 +29,6 @@ export function Lighting({ display }: LightingProps) {
|
|||||||
{display === TankopediaDisplay.StaticArmor && (
|
{display === TankopediaDisplay.StaticArmor && (
|
||||||
<Environment preset="lobby" />
|
<Environment preset="lobby" />
|
||||||
)}
|
)}
|
||||||
{display === TankopediaDisplay.ShootingRange && (
|
|
||||||
<>
|
|
||||||
<directionalLight position={[0, -1, 0]} />
|
|
||||||
|
|
||||||
{times(LIGHTS_COUNT, (index) => (
|
|
||||||
<directionalLight
|
|
||||||
key={index}
|
|
||||||
castShadow
|
|
||||||
intensity={3 / LIGHTS_COUNT}
|
|
||||||
position={[
|
|
||||||
Math.sin(2 * Math.PI * (index / LIGHTS_COUNT)),
|
|
||||||
1,
|
|
||||||
Math.cos(2 * Math.PI * (index / LIGHTS_COUNT)),
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+12
-57
@@ -1,5 +1,4 @@
|
|||||||
import { asset, I_HAT, J_HAT } from '@blitzkit/core';
|
import { asset, I_HAT, J_HAT } from '@blitzkit/core';
|
||||||
import { Html } from '@react-three/drei';
|
|
||||||
import { useFrame, useLoader } from '@react-three/fiber';
|
import { useFrame, useLoader } from '@react-three/fiber';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
@@ -21,8 +20,6 @@ import { modelTransformEvent } from '../../../../../../../core/blitzkit/modelTra
|
|||||||
import { tankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
|
import { tankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
|
||||||
import { Duel } from '../../../../../../../stores/duel';
|
import { Duel } from '../../../../../../../stores/duel';
|
||||||
import { TankopediaEphemeral } from '../../../../../../../stores/tankopediaEphemeral';
|
import { TankopediaEphemeral } from '../../../../../../../stores/tankopediaEphemeral';
|
||||||
import { TankopediaDisplay } from '../../../../../../../stores/tankopediaPersistent/constants';
|
|
||||||
import { TargetCircle } from './components/TargetCircle';
|
|
||||||
import { aimTarget } from './constants';
|
import { aimTarget } from './constants';
|
||||||
|
|
||||||
const [modelDefinitions, equipmentDefinitions, provisionDefinitions] =
|
const [modelDefinitions, equipmentDefinitions, provisionDefinitions] =
|
||||||
@@ -42,9 +39,6 @@ export function SceneProps() {
|
|||||||
const material = useRef<MeshStandardMaterial>(null);
|
const material = useRef<MeshStandardMaterial>(null);
|
||||||
const playground = useRef<Group>(null);
|
const playground = useRef<Group>(null);
|
||||||
|
|
||||||
let lastPitch = Duel.use((state) => state.protagonist.pitch);
|
|
||||||
let lastYaw = Duel.use((state) => state.protagonist.yaw);
|
|
||||||
const display = TankopediaEphemeral.use((state) => state.display);
|
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
const track = Duel.use((state) => state.protagonist.track);
|
const track = Duel.use((state) => state.protagonist.track);
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
@@ -55,7 +49,7 @@ export function SceneProps() {
|
|||||||
const trackModelDefinition =
|
const trackModelDefinition =
|
||||||
modelDefinitions.models[tank.id].tracks[track.id];
|
modelDefinitions.models[tank.id].tracks[track.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition =
|
||||||
tankModelDefinition.turrets[turret.id].guns[gun.gun_type!.value.base.id];
|
tankModelDefinition.turrets[turret.id].guns[gun.id];
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
const hullOrigin = new Vector3(
|
const hullOrigin = new Vector3(
|
||||||
trackModelDefinition.origin.x,
|
trackModelDefinition.origin.x,
|
||||||
@@ -121,7 +115,6 @@ export function SceneProps() {
|
|||||||
|
|
||||||
useFrame(({ raycaster, camera, clock, gl }) => {
|
useFrame(({ raycaster, camera, clock, gl }) => {
|
||||||
if (
|
if (
|
||||||
display !== TankopediaDisplay.ShootingRange ||
|
|
||||||
!playground.current ||
|
!playground.current ||
|
||||||
!targetCircleWrapper.current ||
|
!targetCircleWrapper.current ||
|
||||||
DEBUG_disable ||
|
DEBUG_disable ||
|
||||||
@@ -156,8 +149,8 @@ export function SceneProps() {
|
|||||||
|
|
||||||
const desiredPitch = Math.asin(direction.y);
|
const desiredPitch = Math.asin(direction.y);
|
||||||
const desiredYaw = Math.atan2(-direction.x, -direction.z);
|
const desiredYaw = Math.atan2(-direction.x, -direction.z);
|
||||||
const deltaPitch = desiredPitch - lastPitch;
|
const deltaPitch = desiredPitch - modelTransformEvent.last!.pitch;
|
||||||
const deltaYaw = desiredYaw - lastYaw;
|
const deltaYaw = desiredYaw - modelTransformEvent.last!.yaw;
|
||||||
const deltaAngle = Math.sqrt(deltaPitch ** 2 + deltaYaw ** 2);
|
const deltaAngle = Math.sqrt(deltaPitch ** 2 + deltaYaw ** 2);
|
||||||
const maxPossibleRotationInTime = gunRotationSpeed * deltaT;
|
const maxPossibleRotationInTime = gunRotationSpeed * deltaT;
|
||||||
const clampedDeltaAngle = Math.min(maxPossibleRotationInTime, deltaAngle);
|
const clampedDeltaAngle = Math.min(maxPossibleRotationInTime, deltaAngle);
|
||||||
@@ -167,14 +160,14 @@ export function SceneProps() {
|
|||||||
deltaAngle === 0 ? 0 : deltaYaw * (clampedDeltaAngle / deltaAngle);
|
deltaAngle === 0 ? 0 : deltaYaw * (clampedDeltaAngle / deltaAngle);
|
||||||
|
|
||||||
const [pitch, yaw] = applyPitchYawLimits(
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
lastPitch + clampedDeltaPitch,
|
modelTransformEvent.last!.pitch + clampedDeltaPitch,
|
||||||
lastYaw + clampedDeltaYaw,
|
modelTransformEvent.last!.yaw + clampedDeltaYaw,
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
);
|
);
|
||||||
|
|
||||||
const actualDeltaPitch = pitch - lastPitch;
|
const actualDeltaPitch = pitch - modelTransformEvent.last!.pitch;
|
||||||
const actualDeltaYaw = yaw - lastYaw;
|
const actualDeltaYaw = yaw - modelTransformEvent.last!.yaw;
|
||||||
const deltaRotation = Math.abs(actualDeltaPitch) + Math.abs(actualDeltaYaw);
|
const deltaRotation = Math.abs(actualDeltaPitch) + Math.abs(actualDeltaYaw);
|
||||||
const rotationSpeed = deltaRotation / deltaT;
|
const rotationSpeed = deltaRotation / deltaT;
|
||||||
const turretTraversePenalty =
|
const turretTraversePenalty =
|
||||||
@@ -193,9 +186,7 @@ export function SceneProps() {
|
|||||||
dispersion = Math.max(characteristics.dispersion * penalty, dispersion);
|
dispersion = Math.max(characteristics.dispersion * penalty, dispersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
lastPitch = pitch;
|
|
||||||
lastYaw = yaw;
|
|
||||||
|
|
||||||
const gunDirection = new Vector3(0, 0, -1)
|
const gunDirection = new Vector3(0, 0, -1)
|
||||||
.applyAxisAngle(I_HAT, pitch)
|
.applyAxisAngle(I_HAT, pitch)
|
||||||
@@ -241,45 +232,9 @@ export function SceneProps() {
|
|||||||
if (!disturbed) return null;
|
if (!disturbed) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||||
{display !== TankopediaDisplay.ShootingRange && (
|
<planeGeometry args={[10, 10]} />
|
||||||
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
<meshStandardMaterial ref={material} map={texture} transparent />
|
||||||
<planeGeometry args={[10, 10]} />
|
</mesh>
|
||||||
<meshStandardMaterial ref={material} map={texture} transparent />
|
|
||||||
</mesh>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{display === TankopediaDisplay.ShootingRange && (
|
|
||||||
<>
|
|
||||||
<group ref={targetCircleWrapper}>
|
|
||||||
<Html center occlude={false} zIndexRange={[0, 0]}>
|
|
||||||
<TargetCircle variant="client" ref={clientTargetCircle} />
|
|
||||||
<TargetCircle variant="server" ref={serverTargetCircle} />
|
|
||||||
</Html>
|
|
||||||
</group>
|
|
||||||
|
|
||||||
<group ref={playground}>
|
|
||||||
<mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
|
|
||||||
<planeGeometry args={[100, 100]} />
|
|
||||||
<meshStandardMaterial color={0x404040} />
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
<mesh
|
|
||||||
receiveShadow
|
|
||||||
castShadow
|
|
||||||
position={[10, 1.5, -10]}
|
|
||||||
rotation={[0, Math.PI / 4, 0]}
|
|
||||||
>
|
|
||||||
<boxGeometry args={[1, 3, 1]} />
|
|
||||||
<meshStandardMaterial color={0xff8040} />
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
<group position={[0, 0, -25]} rotation={[-Math.PI / 2, 0, Math.PI]}>
|
|
||||||
<primitive object={mockTank.scene} />
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+12
-105
@@ -1,6 +1,5 @@
|
|||||||
import { normalizeAngleRad } from '@blitzkit/core';
|
|
||||||
import { invalidate, useThree, type ThreeEvent } from '@react-three/fiber';
|
import { invalidate, useThree, type ThreeEvent } from '@react-three/fiber';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Group, Mesh, MeshStandardMaterial, Vector2 } from 'three';
|
import { Group, Mesh, MeshStandardMaterial, Vector2 } from 'three';
|
||||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
||||||
@@ -10,10 +9,7 @@ import { useModel } from '../../../../../../hooks/useModel';
|
|||||||
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
|
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
|
||||||
import { useTankTransform } from '../../../../../../hooks/useTankTransform';
|
import { useTankTransform } from '../../../../../../hooks/useTankTransform';
|
||||||
import { Duel } from '../../../../../../stores/duel';
|
import { Duel } from '../../../../../../stores/duel';
|
||||||
import {
|
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||||
ShootingRangeZoom,
|
|
||||||
TankopediaEphemeral,
|
|
||||||
} from '../../../../../../stores/tankopediaEphemeral';
|
|
||||||
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
|
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
|
||||||
import { ModelTankWrapper } from '../../../../../Armor/components/ModelTankWrapper';
|
import { ModelTankWrapper } from '../../../../../Armor/components/ModelTankWrapper';
|
||||||
|
|
||||||
@@ -32,23 +28,10 @@ export function TankModel() {
|
|||||||
const tankModelDefinition = useTankModelDefinition();
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
const turretModelDefinition =
|
const turretModelDefinition =
|
||||||
tankModelDefinition.turrets[protagonist.turret.id];
|
tankModelDefinition.turrets[protagonist.turret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
||||||
turretModelDefinition.guns[protagonist.gun.gun_type!.value.base.id];
|
|
||||||
const { gltf } = useModel(protagonist.tank.id);
|
const { gltf } = useModel(protagonist.tank.id);
|
||||||
const nodes = Object.values(gltf.nodes);
|
const nodes = Object.values(gltf.nodes);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribeShootingRangeZoom = tankopediaEphemeralStore.subscribe(
|
|
||||||
(state) => state.shootingRangeZoom,
|
|
||||||
(zoom) => {
|
|
||||||
if (!hullContainer.current) return;
|
|
||||||
hullContainer.current.visible = zoom === ShootingRangeZoom.Arcade;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return unsubscribeShootingRangeZoom;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -125,23 +108,6 @@ export function TankModel() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
// group(group) {
|
|
||||||
// return {
|
|
||||||
// visible:
|
|
||||||
// group.name !== 'ST_base' &&
|
|
||||||
// group.name !== 'MR_Base2' &&
|
|
||||||
// (!group.name.includes('state_entity') ||
|
|
||||||
// (isDynamicArmorActive &&
|
|
||||||
// group.name.includes('state_entity_01')) ||
|
|
||||||
// (!isDynamicArmorActive &&
|
|
||||||
// group.name.includes('state_entity_00'))) &&
|
|
||||||
// (!group.name.includes('shields') ||
|
|
||||||
// (isDynamicArmorActive &&
|
|
||||||
// group.name.includes('shields_rev')) ||
|
|
||||||
// (!isDynamicArmorActive && group.name.endsWith('shields'))),
|
|
||||||
// };
|
|
||||||
// },
|
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
|
|
||||||
@@ -157,22 +123,12 @@ export function TankModel() {
|
|||||||
|
|
||||||
if (!isVisible) return null;
|
if (!isVisible) return null;
|
||||||
|
|
||||||
let pitch = 0;
|
|
||||||
let yaw = 0;
|
|
||||||
|
|
||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (
|
if (!isTurret) return;
|
||||||
!isTurret ||
|
|
||||||
tankopediaEphemeralStore.getState().display ===
|
|
||||||
TankopediaDisplay.ShootingRange
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
yaw = protagonist.yaw;
|
|
||||||
pitch = protagonist.pitch;
|
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
mutateTankopediaEphemeral((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
@@ -201,21 +157,18 @@ export function TankModel() {
|
|||||||
delta.set(event.clientX, event.clientY).sub(position);
|
delta.set(event.clientX, event.clientY).sub(position);
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
|
|
||||||
[pitch, yaw] = applyPitchYawLimits(
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
pitch,
|
modelTransformEvent.last!.pitch,
|
||||||
yaw + delta.x * (Math.PI / boundingRect.width),
|
modelTransformEvent.last!.yaw +
|
||||||
|
delta.x * (Math.PI / boundingRect.width),
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateDuel((draft) => {
|
|
||||||
draft.protagonist.pitch = normalizeAngleRad(pitch);
|
|
||||||
draft.protagonist.yaw = normalizeAngleRad(yaw);
|
|
||||||
});
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
mutateTankopediaEphemeral((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
@@ -234,24 +187,6 @@ export function TankModel() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
// group(group) {
|
|
||||||
// return {
|
|
||||||
// visible:
|
|
||||||
// group.name !== 'ST_base' &&
|
|
||||||
// group.name !== 'MR_Base2' &&
|
|
||||||
// (!group.name.includes('state_entity') ||
|
|
||||||
// (isDynamicArmorActive &&
|
|
||||||
// group.name.includes('state_entity_01')) ||
|
|
||||||
// (!isDynamicArmorActive &&
|
|
||||||
// group.name.includes('state_entity_00'))) &&
|
|
||||||
// (!group.name.includes('shields') ||
|
|
||||||
// (isDynamicArmorActive &&
|
|
||||||
// group.name.includes('shields_rev')) ||
|
|
||||||
// (!isDynamicArmorActive &&
|
|
||||||
// group.name.endsWith('shields'))),
|
|
||||||
// };
|
|
||||||
// },
|
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
|
|
||||||
@@ -277,12 +212,6 @@ export function TankModel() {
|
|||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (
|
|
||||||
tankopediaEphemeralStore.getState().display ===
|
|
||||||
TankopediaDisplay.ShootingRange
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
mutateTankopediaEphemeral((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
@@ -292,8 +221,8 @@ export function TankModel() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
pitch = protagonist.pitch;
|
pitch = modelTransformEvent.last!.pitch;
|
||||||
yaw = protagonist.yaw;
|
yaw = modelTransformEvent.last!.yaw;
|
||||||
|
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener('pointermove', handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener('pointerup', handlePointerUp);
|
||||||
@@ -322,16 +251,12 @@ export function TankModel() {
|
|||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
mutateTankopediaEphemeral((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
mutateDuel((draft) => {
|
|
||||||
draft.protagonist.pitch = normalizeAngleRad(pitch);
|
|
||||||
draft.protagonist.yaw = normalizeAngleRad(yaw);
|
|
||||||
});
|
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener('pointerup', handlePointerUp);
|
||||||
}
|
}
|
||||||
@@ -347,24 +272,6 @@ export function TankModel() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
// group(group) {
|
|
||||||
// return {
|
|
||||||
// visible:
|
|
||||||
// group.name !== 'ST_base' &&
|
|
||||||
// group.name !== 'MR_Base2' &&
|
|
||||||
// (!group.name.includes('state_entity') ||
|
|
||||||
// (isDynamicArmorActive &&
|
|
||||||
// group.name.includes('state_entity_01')) ||
|
|
||||||
// (!isDynamicArmorActive &&
|
|
||||||
// group.name.includes('state_entity_00'))) &&
|
|
||||||
// (!group.name.includes('shields') ||
|
|
||||||
// (isDynamicArmorActive &&
|
|
||||||
// group.name.includes('shields_rev')) ||
|
|
||||||
// (!isDynamicArmorActive &&
|
|
||||||
// group.name.endsWith('shields'))),
|
|
||||||
// };
|
|
||||||
// },
|
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
+3
-19
@@ -1,14 +1,11 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Group, ShaderMaterial, UniformsLib, UniformsUtils } from 'three';
|
import { Group, ShaderMaterial, UniformsLib, UniformsUtils } from 'three';
|
||||||
import { jsxTree } from '../../../../../../../core/blitzkit/jsxTree';
|
import { jsxTree } from '../../../../../../../core/blitzkit/jsxTree';
|
||||||
import { useModel } from '../../../../../../../hooks/useModel';
|
import { useModel } from '../../../../../../../hooks/useModel';
|
||||||
import { useTankModelDefinition } from '../../../../../../../hooks/useTankModelDefinition';
|
import { useTankModelDefinition } from '../../../../../../../hooks/useTankModelDefinition';
|
||||||
import { useTankTransform } from '../../../../../../../hooks/useTankTransform';
|
import { useTankTransform } from '../../../../../../../hooks/useTankTransform';
|
||||||
import { Duel } from '../../../../../../../stores/duel';
|
import { Duel } from '../../../../../../../stores/duel';
|
||||||
import {
|
import { TankopediaEphemeral } from '../../../../../../../stores/tankopediaEphemeral';
|
||||||
ShootingRangeZoom,
|
|
||||||
TankopediaEphemeral,
|
|
||||||
} from '../../../../../../../stores/tankopediaEphemeral';
|
|
||||||
import { ModelTankWrapper } from '../../../../../../Armor/components/ModelTankWrapper';
|
import { ModelTankWrapper } from '../../../../../../Armor/components/ModelTankWrapper';
|
||||||
import fragmentShader from './shaders/fragment.glsl?raw';
|
import fragmentShader from './shaders/fragment.glsl?raw';
|
||||||
import vertexShader from './shaders/vertex.glsl?raw';
|
import vertexShader from './shaders/vertex.glsl?raw';
|
||||||
@@ -34,24 +31,11 @@ export function TransitionSkeleton() {
|
|||||||
const tankModelDefinition = useTankModelDefinition();
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
const turretModelDefinition =
|
const turretModelDefinition =
|
||||||
tankModelDefinition.turrets[protagonist.turret.id];
|
tankModelDefinition.turrets[protagonist.turret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
||||||
turretModelDefinition.guns[protagonist.gun.gun_type!.value.base.id];
|
|
||||||
const { gltf } = useModel(protagonist.tank.id);
|
const { gltf } = useModel(protagonist.tank.id);
|
||||||
const nodes = Object.values(gltf.nodes);
|
const nodes = Object.values(gltf.nodes);
|
||||||
const revealed = tankopediaEphemeralStore((state) => state.revealed);
|
const revealed = tankopediaEphemeralStore((state) => state.revealed);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribeShootingRangeZoom = tankopediaEphemeralStore.subscribe(
|
|
||||||
(state) => state.shootingRangeZoom,
|
|
||||||
(zoom) => {
|
|
||||||
if (!hullContainer.current) return;
|
|
||||||
hullContainer.current.visible = zoom === ShootingRangeZoom.Arcade;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return unsubscribeShootingRangeZoom;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+42
-60
@@ -1,5 +1,5 @@
|
|||||||
import { BLITZKIT_TANK_ICON_SIZE } from '@blitzkit/core';
|
import { BLITZKIT_TANK_ICON_SIZE } from '@blitzkit/core';
|
||||||
import { Canvas, invalidate } from '@react-three/fiber';
|
import { invalidate } from '@react-three/fiber';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
Suspense,
|
Suspense,
|
||||||
@@ -13,7 +13,6 @@ import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimi
|
|||||||
import { modelTransformEvent } from '../../../../../core/blitzkit/modelTransform';
|
import { modelTransformEvent } from '../../../../../core/blitzkit/modelTransform';
|
||||||
import { Pose, poseEvent } from '../../../../../core/blitzkit/pose';
|
import { Pose, poseEvent } from '../../../../../core/blitzkit/pose';
|
||||||
import { useEquipment } from '../../../../../hooks/useEquipment';
|
import { useEquipment } from '../../../../../hooks/useEquipment';
|
||||||
import { useOnScreen } from '../../../../../hooks/useOnScreen';
|
|
||||||
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
|
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
|
||||||
import { Duel } from '../../../../../stores/duel';
|
import { Duel } from '../../../../../stores/duel';
|
||||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||||
@@ -26,8 +25,10 @@ import {
|
|||||||
StaticArmor,
|
StaticArmor,
|
||||||
type ThicknessRange,
|
type ThicknessRange,
|
||||||
} from '../../../../Armor/components/StaticArmor';
|
} from '../../../../Armor/components/StaticArmor';
|
||||||
|
import { SmartCanvas } from '../../../../SmartCanvas';
|
||||||
import { AutoClear } from './components/AutoClear';
|
import { AutoClear } from './components/AutoClear';
|
||||||
import { Controls } from './components/Control';
|
import { Controls } from './components/Control';
|
||||||
|
import { InitialAligner } from './components/InitialAligner';
|
||||||
import { InitialFogReveal } from './components/InitialFogReveal';
|
import { InitialFogReveal } from './components/InitialFogReveal';
|
||||||
import { Lighting } from './components/Lighting';
|
import { Lighting } from './components/Lighting';
|
||||||
import { SceneProps } from './components/SceneProps';
|
import { SceneProps } from './components/SceneProps';
|
||||||
@@ -58,14 +59,12 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
|||||||
const tankModelDefinition = useTankModelDefinition();
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
const turretModelDefinition =
|
const turretModelDefinition =
|
||||||
tankModelDefinition.turrets[protagonistTurret.id];
|
tankModelDefinition.turrets[protagonistTurret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[protagonistGun.id];
|
||||||
turretModelDefinition.guns[protagonistGun.gun_type!.value.base.id];
|
|
||||||
const rawDisplay = TankopediaEphemeral.use((state) => state.display);
|
const rawDisplay = TankopediaEphemeral.use((state) => state.display);
|
||||||
const [display, setDisplay] = useState(rawDisplay);
|
const [display, setDisplay] = useState(rawDisplay);
|
||||||
const hideTankModelUnderArmor = TankopediaPersistent.use(
|
const hideTankModelUnderArmor = TankopediaPersistent.use(
|
||||||
(state) => state.hideTankModelUnderArmor,
|
(state) => state.hideTankModelUnderArmor,
|
||||||
);
|
);
|
||||||
const onScreen = useOnScreen(canvas);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rawDisplay === display) return;
|
if (rawDisplay === display) return;
|
||||||
@@ -115,56 +114,44 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
|||||||
function handlePoseEvent(pose: Pose) {
|
function handlePoseEvent(pose: Pose) {
|
||||||
switch (pose) {
|
switch (pose) {
|
||||||
case Pose.HullDown: {
|
case Pose.HullDown: {
|
||||||
mutateDuel((draft) => {
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
const [pitch, yaw] = applyPitchYawLimits(
|
-Infinity,
|
||||||
-Infinity,
|
0,
|
||||||
0,
|
gunModelDefinition.pitch,
|
||||||
gunModelDefinition.pitch,
|
turretModelDefinition.yaw,
|
||||||
turretModelDefinition.yaw,
|
hasImprovedVerticalStabilizer,
|
||||||
hasImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
draft.protagonist.pitch = pitch;
|
|
||||||
draft.protagonist.yaw = yaw;
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case Pose.FaceHug: {
|
case Pose.FaceHug: {
|
||||||
mutateDuel((draft) => {
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
const [pitch, yaw] = applyPitchYawLimits(
|
Infinity,
|
||||||
Infinity,
|
0,
|
||||||
0,
|
gunModelDefinition.pitch,
|
||||||
gunModelDefinition.pitch,
|
turretModelDefinition.yaw,
|
||||||
turretModelDefinition.yaw,
|
hasImprovedVerticalStabilizer,
|
||||||
hasImprovedVerticalStabilizer,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
draft.protagonist.pitch = pitch;
|
|
||||||
draft.protagonist.yaw = yaw;
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case Pose.Default:
|
case Pose.Default:
|
||||||
mutateDuel((draft) => {
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
const [pitch, yaw] = applyPitchYawLimits(
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
gunModelDefinition.pitch,
|
||||||
gunModelDefinition.pitch,
|
turretModelDefinition.yaw,
|
||||||
turretModelDefinition.yaw,
|
hasImprovedVerticalStabilizer,
|
||||||
hasImprovedVerticalStabilizer,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
draft.protagonist.pitch = pitch;
|
|
||||||
draft.protagonist.yaw = yaw;
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -178,15 +165,15 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mutateDuel((draft) => {
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
[draft.protagonist.pitch, draft.protagonist.yaw] = applyPitchYawLimits(
|
modelTransformEvent.last!.pitch,
|
||||||
draft.protagonist.pitch,
|
modelTransformEvent.last!.yaw,
|
||||||
draft.protagonist.yaw,
|
gunModelDefinition.pitch,
|
||||||
gunModelDefinition.pitch,
|
turretModelDefinition.yaw,
|
||||||
turretModelDefinition.yaw,
|
hasImprovedVerticalStabilizer,
|
||||||
hasImprovedVerticalStabilizer,
|
);
|
||||||
);
|
|
||||||
});
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}, [protagonistGun, protagonistTurret]);
|
}, [protagonistGun, protagonistTurret]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -204,16 +191,9 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
|||||||
}, [display]);
|
}, [display]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Canvas
|
<SmartCanvas
|
||||||
ref={canvas}
|
ref={canvas}
|
||||||
scene={{ fog: naked ? undefined : fog.current }}
|
scene={{ fog: naked ? undefined : fog.current }}
|
||||||
frameloop={
|
|
||||||
onScreen
|
|
||||||
? display === TankopediaDisplay.ShootingRange
|
|
||||||
? 'always'
|
|
||||||
: 'demand'
|
|
||||||
: 'never'
|
|
||||||
}
|
|
||||||
gl={{
|
gl={{
|
||||||
clippingPlanes: [],
|
clippingPlanes: [],
|
||||||
localClippingEnabled: true,
|
localClippingEnabled: true,
|
||||||
@@ -254,7 +234,9 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
|||||||
)}
|
)}
|
||||||
<Lighting display={display} />
|
<Lighting display={display} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Canvas>
|
|
||||||
|
<InitialAligner />
|
||||||
|
</SmartCanvas>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -29,8 +29,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
(filtered.reduce((accumulator, thisTank) => {
|
(filtered.reduce((accumulator, thisTank) => {
|
||||||
return (
|
return (
|
||||||
accumulator +
|
accumulator +
|
||||||
thisTank.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
thisTank.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near
|
||||||
.penetration.near
|
|
||||||
);
|
);
|
||||||
}, 0) /
|
}, 0) /
|
||||||
filtered.length) *
|
filtered.length) *
|
||||||
@@ -51,8 +50,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleKeyDown(event: KeyboardEvent) {
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
const { shells } =
|
const { shells } = duelStore.getState().antagonist.gun;
|
||||||
duelStore.getState().antagonist.gun.gun_type!.value.base;
|
|
||||||
|
|
||||||
times(3, (index) => {
|
times(3, (index) => {
|
||||||
if (event.key === `${index + 1}` && shells.length > index) {
|
if (event.key === `${index + 1}` && shells.length > index) {
|
||||||
@@ -112,8 +110,6 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
top={isFullScreen ? '0' : undefined}
|
top={isFullScreen ? '0' : undefined}
|
||||||
left={isFullScreen ? '0' : undefined}
|
left={isFullScreen ? '0' : undefined}
|
||||||
>
|
>
|
||||||
<Title />
|
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
className="tank-sandbox-container"
|
className="tank-sandbox-container"
|
||||||
flexGrow="1"
|
flexGrow="1"
|
||||||
@@ -122,6 +118,8 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
position="relative"
|
position="relative"
|
||||||
>
|
>
|
||||||
<Box position="absolute" width="100%" height="100%" overflow="hidden">
|
<Box position="absolute" width="100%" height="100%" overflow="hidden">
|
||||||
|
<Title />
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
@@ -136,7 +134,9 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
transitionDuration: '2s',
|
transitionDuration: '2s',
|
||||||
opacity: revealed ? 1 : 0,
|
opacity: revealed ? 1 : 0,
|
||||||
filter: disturbed ? undefined : `drop-shadow(0 0 1rem black)`,
|
filter: disturbed
|
||||||
|
? undefined
|
||||||
|
: `drop-shadow(0 1rem 1rem black)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
@@ -145,6 +145,8 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Title foreground />
|
||||||
|
|
||||||
<Options
|
<Options
|
||||||
skeleton={skeleton}
|
skeleton={skeleton}
|
||||||
canvas={canvas}
|
canvas={canvas}
|
||||||
@@ -152,8 +154,6 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Title foreground />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,10 +24,6 @@ export function VideoSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
<Heading size="6">
|
<Heading size="6">
|
||||||
{strings.website.tools.tankopedia.review.title}
|
{strings.website.tools.tankopedia.review.title}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Text color="gray">
|
|
||||||
{strings.website.tools.tankopedia.review.disclaimer}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex gap="3" wrap="wrap" justify="center">
|
<Flex gap="3" wrap="wrap" justify="center">
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||||
|
|
||||||
|
export const DEFAULT_PITCH_TRANSITION = 15;
|
||||||
|
|
||||||
export function applyPitchYawLimits(
|
export function applyPitchYawLimits(
|
||||||
pitch: number, // rad
|
pitch: number, // rad
|
||||||
yaw: number, // rad
|
yaw: number, // rad
|
||||||
@@ -28,20 +30,23 @@ export function applyPitchYawLimits(
|
|||||||
const elevationBuff = verticalStabilizer ? degToRad(4) : 0;
|
const elevationBuff = verticalStabilizer ? degToRad(4) : 0;
|
||||||
let lowerPitch = -degToRad(pitchLimits.max) - depressionBuff;
|
let lowerPitch = -degToRad(pitchLimits.max) - depressionBuff;
|
||||||
let upperPitch = -degToRad(pitchLimits.min) + elevationBuff;
|
let upperPitch = -degToRad(pitchLimits.min) + elevationBuff;
|
||||||
const transition = degToRad(pitchLimits.transition ?? 15);
|
const transition = degToRad(
|
||||||
|
pitchLimits.transition ?? DEFAULT_PITCH_TRANSITION,
|
||||||
|
);
|
||||||
|
|
||||||
if (pitchLimits.back) {
|
if (pitchLimits.back) {
|
||||||
const range = degToRad(pitchLimits.back.range);
|
const range = degToRad(pitchLimits.back.range);
|
||||||
const yawRotatedAbs = Math.abs(normalizeAngleRad(newYaw - Math.PI));
|
const yawRotatedAbs = Math.abs(normalizeAngleRad(newYaw - Math.PI));
|
||||||
|
|
||||||
if (yawRotatedAbs <= range + transition) {
|
if (yawRotatedAbs <= range / 2 + transition) {
|
||||||
if (yawRotatedAbs <= range) {
|
if (yawRotatedAbs <= range / 2) {
|
||||||
// inside range
|
// inside range
|
||||||
lowerPitch = -degToRad(pitchLimits.back.max);
|
lowerPitch = -degToRad(pitchLimits.back.max);
|
||||||
upperPitch = -degToRad(pitchLimits.back.min);
|
upperPitch = -degToRad(pitchLimits.back.min);
|
||||||
} else {
|
} else {
|
||||||
// inside transition
|
// inside transition
|
||||||
const transitionProgress = (yawRotatedAbs - range) / transition;
|
const transitionProgress = (yawRotatedAbs - range / 2) / transition;
|
||||||
|
// console.log(transitionProgress);
|
||||||
const maxInterpolatedPitch =
|
const maxInterpolatedPitch =
|
||||||
(1 - transitionProgress) * pitchLimits.back.max +
|
(1 - transitionProgress) * pitchLimits.back.max +
|
||||||
transitionProgress * pitchLimits.max;
|
transitionProgress * pitchLimits.max;
|
||||||
@@ -59,14 +64,14 @@ export function applyPitchYawLimits(
|
|||||||
const range = degToRad(pitchLimits.front.range);
|
const range = degToRad(pitchLimits.front.range);
|
||||||
const yawAbs = Math.abs(normalizeAngleRad(newYaw));
|
const yawAbs = Math.abs(normalizeAngleRad(newYaw));
|
||||||
|
|
||||||
if (yawAbs <= range + transition) {
|
if (yawAbs <= range / 2 + transition) {
|
||||||
if (yawAbs <= range) {
|
if (yawAbs <= range / 2) {
|
||||||
// inside range
|
// inside range
|
||||||
lowerPitch = -degToRad(pitchLimits.front.max);
|
lowerPitch = -degToRad(pitchLimits.front.max);
|
||||||
upperPitch = -degToRad(pitchLimits.front.min);
|
upperPitch = -degToRad(pitchLimits.front.min);
|
||||||
} else {
|
} else {
|
||||||
// inside transition
|
// inside transition
|
||||||
const transitionProgress = (yawAbs - range) / transition;
|
const transitionProgress = (yawAbs - range / 2) / transition;
|
||||||
const maxInterpolatedPitch =
|
const maxInterpolatedPitch =
|
||||||
(1 - transitionProgress) * pitchLimits.front.max +
|
(1 - transitionProgress) * pitchLimits.front.max +
|
||||||
transitionProgress * pitchLimits.max;
|
transitionProgress * pitchLimits.max;
|
||||||
@@ -82,5 +87,5 @@ export function applyPitchYawLimits(
|
|||||||
|
|
||||||
newPitch = clamp(newPitch, lowerPitch, upperPitch);
|
newPitch = clamp(newPitch, lowerPitch, upperPitch);
|
||||||
|
|
||||||
return [newPitch, newYaw];
|
return [newPitch, newYaw] as const;
|
||||||
}
|
}
|
||||||
@@ -31,9 +31,8 @@ export function filterTank(filters: TankFilters, tank: TankDefinition) {
|
|||||||
SHELLS.every(
|
SHELLS.every(
|
||||||
(index) =>
|
(index) =>
|
||||||
filters.shells[index] === null ||
|
filters.shells[index] === null ||
|
||||||
gun.gun_type!.value.base.shells[index] === undefined ||
|
gun.shells[index] === undefined ||
|
||||||
gun.gun_type!.value.base.shells[index].type ===
|
gun.shells[index].type === filters.shells[index],
|
||||||
filters.shells[index],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import type { EquipmentMatrix } from '../../stores/duel';
|
import type { EquipmentMatrix } from '../../stores/duel';
|
||||||
import { awaitableEquipmentDefinitions } from '../awaitables/equipmentDefinitions';
|
import { awaitableEquipmentDefinitions } from '../awaitables/equipmentDefinitions';
|
||||||
|
|
||||||
export async function hasEquipment(
|
const equipmentDefinitions = await awaitableEquipmentDefinitions;
|
||||||
|
|
||||||
|
export function hasEquipment(
|
||||||
id: number,
|
id: number,
|
||||||
equipmentPreset: string,
|
equipmentPreset: string,
|
||||||
equipmentMatrix: EquipmentMatrix,
|
equipmentMatrix: EquipmentMatrix,
|
||||||
) {
|
) {
|
||||||
const equipmentDefinitions = await awaitableEquipmentDefinitions;
|
|
||||||
const preset = equipmentDefinitions.presets[equipmentPreset];
|
const preset = equipmentDefinitions.presets[equipmentPreset];
|
||||||
return preset.slots.some((slot, index) => {
|
return preset.slots.some((slot, index) => {
|
||||||
const row = Math.floor(index / 3);
|
const row = Math.floor(index / 3);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { EventManager } from '@blitzkit/core';
|
import { Quicklime } from 'quicklime';
|
||||||
|
|
||||||
export interface ModelTransformEventData {
|
export interface ModelTransformEventData {
|
||||||
yaw?: number;
|
yaw: number;
|
||||||
pitch: number;
|
pitch: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const modelTransformEvent = new EventManager<ModelTransformEventData>();
|
export const modelTransformEvent = new Quicklime<ModelTransformEventData>({
|
||||||
|
pitch: 0,
|
||||||
|
yaw: 0,
|
||||||
|
});
|
||||||
@@ -2,18 +2,16 @@ import type { GunDefinition } from '@blitzkit/core';
|
|||||||
|
|
||||||
export function resolveReload(gun: GunDefinition) {
|
export function resolveReload(gun: GunDefinition) {
|
||||||
if (gun.gun_type!.$case === 'regular') {
|
if (gun.gun_type!.$case === 'regular') {
|
||||||
return gun.gun_type!.value.extension.reload;
|
return gun.gun_type!.value.reload;
|
||||||
} else if (gun.gun_type!.$case === 'auto_loader') {
|
} else if (gun.gun_type!.$case === 'auto_loader') {
|
||||||
return (
|
return (
|
||||||
gun.gun_type!.value.extension.clip_reload +
|
gun.gun_type!.value.clip_reload +
|
||||||
(gun.gun_type!.value.extension.shell_count - 1) *
|
(gun.gun_type!.value.shell_count - 1) * gun.gun_type!.value.intra_clip
|
||||||
gun.gun_type!.value.extension.intra_clip
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
gun.gun_type!.value.extension.shell_reloads.reduce((a, b) => a + b, 0) +
|
gun.gun_type!.value.shell_reloads.reduce((a, b) => a + b, 0) +
|
||||||
(gun.gun_type!.value.extension.shell_count - 1) *
|
(gun.gun_type!.value.shell_count - 1) * gun.gun_type!.value.intra_clip
|
||||||
gun.gun_type!.value.extension.intra_clip
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,8 +80,7 @@ export function tankCharacteristics(
|
|||||||
) {
|
) {
|
||||||
const preset = equipmentDefinitions.presets[tank.equipment_preset];
|
const preset = equipmentDefinitions.presets[tank.equipment_preset];
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
turretModelDefinition.guns[gun.gun_type!.value.base.id];
|
|
||||||
|
|
||||||
function equipment(id: number) {
|
function equipment(id: number) {
|
||||||
return preset.slots.some((slot, index) => {
|
return preset.slots.some((slot, index) => {
|
||||||
@@ -152,6 +151,8 @@ export function tankCharacteristics(
|
|||||||
commanderMastery *
|
commanderMastery *
|
||||||
(tank.crew.some(({ type }) => type === CrewType.DRIVER) ? 1.1 : 1.05);
|
(tank.crew.some(({ type }) => type === CrewType.DRIVER) ? 1.1 : 1.05);
|
||||||
const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]);
|
const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]);
|
||||||
|
const burstShells = gun.burst?.count ?? 1;
|
||||||
|
const burstInterShell = gun.burst?.interval;
|
||||||
const armorDamageCoefficient =
|
const armorDamageCoefficient =
|
||||||
coefficient([hasTungsten, 0.15]) *
|
coefficient([hasTungsten, 0.15]) *
|
||||||
coefficient(
|
coefficient(
|
||||||
@@ -160,14 +161,12 @@ export function tankCharacteristics(
|
|||||||
[applySpallLiner && shell.type === ShellType.HE, -0.2],
|
[applySpallLiner && shell.type === ShellType.HE, -0.2],
|
||||||
);
|
);
|
||||||
const assaultDamageCoefficient =
|
const assaultDamageCoefficient =
|
||||||
gun.gun_type!.value.base.assault_ranges &&
|
gun.assault_ranges && gun.assault_ranges.types.includes(shell.type)
|
||||||
gun.gun_type!.value.base.assault_ranges.types.includes(shell.type)
|
? (gun.assault_ranges.ranges.find(
|
||||||
? (gun.gun_type!.value.base.assault_ranges.ranges.find(
|
|
||||||
({ distance }) => distance >= assaultDistance,
|
({ distance }) => distance >= assaultDistance,
|
||||||
)?.factor ?? 0)
|
)?.factor ?? 0)
|
||||||
: 1;
|
: 1;
|
||||||
const moduleDamageCoefficient =
|
const moduleDamageCoefficient = coefficient([hasTungsten, 0.15]);
|
||||||
coefficient([hasTungsten, 0.15]) * assaultDamageCoefficient;
|
|
||||||
const reloadCoefficient =
|
const reloadCoefficient =
|
||||||
(coefficient([hasGunRammer, -0.05]) *
|
(coefficient([hasGunRammer, -0.05]) *
|
||||||
coefficient([true, degressiveStat(loaderMastery)])) /
|
coefficient([true, degressiveStat(loaderMastery)])) /
|
||||||
@@ -323,18 +322,14 @@ export function tankCharacteristics(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
const weightKg =
|
const weightKg =
|
||||||
tank.weight +
|
tank.weight + engine.weight + track.weight + turret.weight + gun.weight;
|
||||||
engine.weight +
|
|
||||||
track.weight +
|
|
||||||
turret.weight +
|
|
||||||
gun.gun_type!.value.base.weight;
|
|
||||||
const weightTons = weightKg / 1000;
|
const weightTons = weightKg / 1000;
|
||||||
const stockWeight =
|
const stockWeight =
|
||||||
tank.weight +
|
tank.weight +
|
||||||
stockEngine.weight +
|
stockEngine.weight +
|
||||||
stockTrack.weight +
|
stockTrack.weight +
|
||||||
stockTurret.weight +
|
stockTurret.weight +
|
||||||
stockGun.gun_type!.value.base.weight;
|
stockGun.weight;
|
||||||
const resolvedEnginePower = engine.power * enginePowerCoefficient;
|
const resolvedEnginePower = engine.power * enginePowerCoefficient;
|
||||||
const damageCoefficientWithoutAssault = armorDamageCoefficient;
|
const damageCoefficientWithoutAssault = armorDamageCoefficient;
|
||||||
const damageCoefficient =
|
const damageCoefficient =
|
||||||
@@ -349,10 +344,10 @@ export function tankCharacteristics(
|
|||||||
const intraClip =
|
const intraClip =
|
||||||
gun.gun_type?.$case === 'regular'
|
gun.gun_type?.$case === 'regular'
|
||||||
? undefined
|
? undefined
|
||||||
: gun.gun_type!.value.extension.intra_clip * intraClipCoefficient;
|
: gun.gun_type!.value.intra_clip * intraClipCoefficient;
|
||||||
const mostOptimalShellIndex =
|
const mostOptimalShellIndex =
|
||||||
gun.gun_type!.$case === 'auto_reloader'
|
gun.gun_type!.$case === 'auto_reloader'
|
||||||
? gun.gun_type!.value.extension.shell_reloads.reduce<null | {
|
? gun.gun_type!.value.shell_reloads.reduce<null | {
|
||||||
index: number;
|
index: number;
|
||||||
reload: number;
|
reload: number;
|
||||||
}>((current, reloadRaw, index) => {
|
}>((current, reloadRaw, index) => {
|
||||||
@@ -370,43 +365,34 @@ export function tankCharacteristics(
|
|||||||
const damage = shell.armor_damage * damageCoefficient;
|
const damage = shell.armor_damage * damageCoefficient;
|
||||||
const dpmEffective =
|
const dpmEffective =
|
||||||
gun.gun_type!.$case === 'auto_reloader'
|
gun.gun_type!.$case === 'auto_reloader'
|
||||||
? gun.gun_type!.value.extension.shell_reloads[0] >
|
? gun.gun_type!.value.shell_reloads[0] >
|
||||||
gun.gun_type!.value.extension.shell_reloads[1] +
|
gun.gun_type!.value.shell_reloads[1] + gun.gun_type!.value.intra_clip
|
||||||
gun.gun_type!.value.extension.intra_clip
|
|
||||||
? ((armorDamageCoefficient * shell.armor_damage) /
|
? ((armorDamageCoefficient * shell.armor_damage) /
|
||||||
(gun.gun_type!.value.extension.shell_reloads.at(-1)! *
|
(gun.gun_type!.value.shell_reloads.at(-1)! * reloadCoefficient +
|
||||||
reloadCoefficient +
|
gun.gun_type!.value.intra_clip * intraClipCoefficient)) *
|
||||||
gun.gun_type!.value.extension.intra_clip *
|
|
||||||
intraClipCoefficient)) *
|
|
||||||
(60 -
|
(60 -
|
||||||
(gun.gun_type!.value.extension.shell_reloads.slice(0, -1).length -
|
(gun.gun_type!.value.shell_reloads.slice(0, -1).length - 1) *
|
||||||
1) *
|
gun.gun_type!.value.intra_clip *
|
||||||
gun.gun_type!.value.extension.intra_clip *
|
|
||||||
intraClipCoefficient) +
|
intraClipCoefficient) +
|
||||||
armorDamageCoefficient *
|
armorDamageCoefficient *
|
||||||
shell.armor_damage *
|
shell.armor_damage *
|
||||||
gun.gun_type!.value.extension.shell_reloads.slice(0, -1).length
|
gun.gun_type!.value.shell_reloads.slice(0, -1).length
|
||||||
: ((armorDamageCoefficient * shell.armor_damage) /
|
: ((armorDamageCoefficient * shell.armor_damage) /
|
||||||
(gun.gun_type!.value.extension.shell_reloads[0] *
|
(gun.gun_type!.value.shell_reloads[0] * reloadCoefficient +
|
||||||
reloadCoefficient +
|
gun.gun_type!.value.intra_clip * intraClipCoefficient)) *
|
||||||
gun.gun_type!.value.extension.intra_clip *
|
|
||||||
intraClipCoefficient)) *
|
|
||||||
(60 -
|
(60 -
|
||||||
(gun.gun_type!.value.extension.shell_reloads.slice(1).length -
|
(gun.gun_type!.value.shell_reloads.slice(1).length - 1) *
|
||||||
1) *
|
gun.gun_type!.value.intra_clip *
|
||||||
gun.gun_type!.value.extension.intra_clip *
|
|
||||||
intraClipCoefficient) +
|
intraClipCoefficient) +
|
||||||
armorDamageCoefficient *
|
armorDamageCoefficient *
|
||||||
shell.armor_damage *
|
shell.armor_damage *
|
||||||
gun.gun_type!.value.extension.shell_reloads.slice(1).length
|
gun.gun_type!.value.shell_reloads.slice(1).length
|
||||||
: undefined;
|
: undefined;
|
||||||
const shells =
|
const shells =
|
||||||
gun.gun_type!.$case === 'regular'
|
gun.gun_type!.$case === 'regular' ? 1 : gun.gun_type!.value.shell_count;
|
||||||
? 1
|
|
||||||
: gun.gun_type!.value.extension.shell_count;
|
|
||||||
const shellReloads =
|
const shellReloads =
|
||||||
gun.gun_type!.$case === 'auto_reloader'
|
gun.gun_type!.$case === 'auto_reloader'
|
||||||
? gun.gun_type!.value.extension.shell_reloads.map(
|
? gun.gun_type!.value.shell_reloads.map(
|
||||||
(reload) => reload * reloadCoefficient,
|
(reload) => reload * reloadCoefficient,
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -414,30 +400,28 @@ export function tankCharacteristics(
|
|||||||
gun.gun_type!.$case === 'auto_reloader'
|
gun.gun_type!.$case === 'auto_reloader'
|
||||||
? undefined
|
? undefined
|
||||||
: (gun.gun_type!.$case === 'regular'
|
: (gun.gun_type!.$case === 'regular'
|
||||||
? gun.gun_type!.value.extension.reload
|
? gun.gun_type!.value.reload
|
||||||
: gun.gun_type!.value.extension.clip_reload) * reloadCoefficient;
|
: gun.gun_type!.value.clip_reload) * reloadCoefficient;
|
||||||
const caliber = shell.caliber;
|
const caliber = shell.caliber;
|
||||||
const penetration = shell.penetration.near * penetrationCoefficient;
|
const penetration = shell.penetration.near * penetrationCoefficient;
|
||||||
const clipDamage =
|
const clipDamage =
|
||||||
gun.gun_type!.$case === 'regular'
|
gun.gun_type!.$case === 'regular'
|
||||||
? undefined
|
? undefined
|
||||||
: gun.gun_type!.value.extension.shell_count * damage;
|
: gun.gun_type!.value.shell_count * damage;
|
||||||
const moduleDamage = shell.module_damage * moduleDamageCoefficient;
|
const moduleDamage = shell.module_damage * moduleDamageCoefficient;
|
||||||
const explosionRadius = shell.explosion_radius;
|
const explosionRadius = shell.explosion_radius;
|
||||||
const shellVelocity = shell.velocity * shellVelocityCoefficient;
|
const shellVelocity = shell.velocity * shellVelocityCoefficient;
|
||||||
const aimTime = gun.gun_type!.value.base.aim_time * aimTimeCoefficient;
|
const aimTime = gun.aim_time * aimTimeCoefficient;
|
||||||
const dispersion =
|
const dispersion = gun.dispersion_base * dispersionStillCoefficient;
|
||||||
gun.gun_type!.value.base.dispersion_base * dispersionStillCoefficient;
|
|
||||||
const dispersionMoving = track.dispersion_move * dispersionMovingCoefficient;
|
const dispersionMoving = track.dispersion_move * dispersionMovingCoefficient;
|
||||||
const dispersionHullTraversing =
|
const dispersionHullTraversing =
|
||||||
track.dispersion_traverse * dispersionHullTraverseCoefficient;
|
track.dispersion_traverse * dispersionHullTraverseCoefficient;
|
||||||
const dispersionTurretTraversing =
|
const dispersionTurretTraversing =
|
||||||
gun.gun_type!.value.base.dispersion_traverse *
|
gun.dispersion_traverse * dispersionTurretTraverseCoefficient;
|
||||||
dispersionTurretTraverseCoefficient;
|
|
||||||
const dispersionShooting =
|
const dispersionShooting =
|
||||||
gun.gun_type!.value.base.dispersion_shot * dispersionAfterShotCoefficient;
|
gun.dispersion_shot * dispersionAfterShotCoefficient;
|
||||||
const dispersionGunDamaged =
|
const dispersionGunDamaged =
|
||||||
gun.gun_type!.value.base.dispersion_damaged * dispersionDamagedCoefficient;
|
gun.dispersion_damaged * dispersionDamagedCoefficient;
|
||||||
const gunDepression =
|
const gunDepression =
|
||||||
gunModelDefinition.pitch.max +
|
gunModelDefinition.pitch.max +
|
||||||
gunDefaultPitch +
|
gunDefaultPitch +
|
||||||
@@ -479,8 +463,7 @@ export function tankCharacteristics(
|
|||||||
resolvedEnginePower / weightTons / softTerrainCoefficientRaw;
|
resolvedEnginePower / weightTons / softTerrainCoefficientRaw;
|
||||||
const weight = weightTons;
|
const weight = weightTons;
|
||||||
const turretTraverseSpeed = turret.traverse_speed * turretTraverseCoefficient;
|
const turretTraverseSpeed = turret.traverse_speed * turretTraverseCoefficient;
|
||||||
const gunTraverseSpeed =
|
const gunTraverseSpeed = gun.rotation_speed * turretTraverseCoefficient;
|
||||||
gun.gun_type!.value.base.rotation_speed * turretTraverseCoefficient;
|
|
||||||
/**
|
/**
|
||||||
* resistance_hard is used in all cases as this is the coefficient between
|
* resistance_hard is used in all cases as this is the coefficient between
|
||||||
* the absolute best coefficient and the current
|
* the absolute best coefficient and the current
|
||||||
@@ -512,14 +495,12 @@ export function tankCharacteristics(
|
|||||||
? tank.camouflage_still * camouflageCoefficientStill
|
? tank.camouflage_still * camouflageCoefficientStill
|
||||||
: tank.camouflage_moving * camouflageCoefficientMoving;
|
: tank.camouflage_moving * camouflageCoefficientMoving;
|
||||||
const camouflageShootingStill =
|
const camouflageShootingStill =
|
||||||
tank.camouflage_still *
|
tank.camouflage_still * gun.camouflage_loss * camouflageCoefficientStill;
|
||||||
gun.gun_type!.value.base.camouflage_loss *
|
|
||||||
camouflageCoefficientStill;
|
|
||||||
const camouflageShootingMoving =
|
const camouflageShootingMoving =
|
||||||
(tank.class === TankClass.LIGHT
|
(tank.class === TankClass.LIGHT
|
||||||
? tank.camouflage_still * camouflageCoefficientStill
|
? tank.camouflage_still * camouflageCoefficientStill
|
||||||
: tank.camouflage_moving * camouflageCoefficientMoving) *
|
: tank.camouflage_moving * camouflageCoefficientMoving) *
|
||||||
gun.gun_type!.value.base.camouflage_loss;
|
gun.camouflage_loss;
|
||||||
const camouflageCaughtOnFire =
|
const camouflageCaughtOnFire =
|
||||||
tank.camouflage_onFire * tank.camouflage_still * camouflageCoefficientStill;
|
tank.camouflage_onFire * tank.camouflage_still * camouflageCoefficientStill;
|
||||||
const width = size.z;
|
const width = size.z;
|
||||||
@@ -530,7 +511,7 @@ export function tankCharacteristics(
|
|||||||
const shellRicochet = isExplosive(shell.type) ? undefined : shell.ricochet;
|
const shellRicochet = isExplosive(shell.type) ? undefined : shell.ricochet;
|
||||||
const crewCount = tank.crew.length;
|
const crewCount = tank.crew.length;
|
||||||
const shellRange = shell.range;
|
const shellRange = shell.range;
|
||||||
const shellCapacity = gun.gun_type!.value.base.shell_capacity;
|
const shellCapacity = gun.shell_capacity;
|
||||||
const gunType = gun.gun_type!.$case;
|
const gunType = gun.gun_type!.$case;
|
||||||
const penetrationAt250m = shell.penetration.far;
|
const penetrationAt250m = shell.penetration.far;
|
||||||
|
|
||||||
@@ -602,6 +583,8 @@ export function tankCharacteristics(
|
|||||||
shellCapacity,
|
shellCapacity,
|
||||||
gunType,
|
gunType,
|
||||||
penetrationAt250m,
|
penetrationAt250m,
|
||||||
|
burstShells,
|
||||||
|
burstInterShell,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import {
|
|||||||
type ProvisionDefinitions,
|
type ProvisionDefinitions,
|
||||||
type TankDefinition,
|
type TankDefinition,
|
||||||
} from '@blitzkit/core';
|
} from '@blitzkit/core';
|
||||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
|
||||||
import type { DuelMember } from '../../stores/duel';
|
import type { DuelMember } from '../../stores/duel';
|
||||||
import { genericDefaultEquipmentMatrix } from '../../stores/duel/constants';
|
import { genericDefaultEquipmentMatrix } from '../../stores/duel/constants';
|
||||||
import { applyPitchYawLimits } from '../blitz/applyPitchYawLimits';
|
|
||||||
|
|
||||||
export function tankToDuelMember(
|
export function tankToDuelMember(
|
||||||
tank: TankDefinition,
|
tank: TankDefinition,
|
||||||
@@ -15,16 +13,7 @@ export function tankToDuelMember(
|
|||||||
provisionDefinitions: ProvisionDefinitions,
|
provisionDefinitions: ProvisionDefinitions,
|
||||||
) {
|
) {
|
||||||
const turret = tank.turrets.at(-1)!;
|
const turret = tank.turrets.at(-1)!;
|
||||||
const turretModel = model.turrets[turret.id];
|
|
||||||
const gun = turret.guns.at(-1)!;
|
const gun = turret.guns.at(-1)!;
|
||||||
const gunModel = turretModel.guns[gun.gun_type!.value.base.id];
|
|
||||||
|
|
||||||
const [pitch, yaw] = applyPitchYawLimits(
|
|
||||||
degToRad(8),
|
|
||||||
degToRad(25),
|
|
||||||
gunModel.pitch,
|
|
||||||
turretModel.yaw,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
camouflage: true,
|
camouflage: true,
|
||||||
@@ -34,13 +23,10 @@ export function tankToDuelMember(
|
|||||||
provisions: createDefaultProvisions(tank, gun, provisionDefinitions),
|
provisions: createDefaultProvisions(tank, gun, provisionDefinitions),
|
||||||
engine: tank.engines.at(-1)!,
|
engine: tank.engines.at(-1)!,
|
||||||
gun,
|
gun,
|
||||||
shell: tank.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0],
|
shell: tank.turrets.at(-1)!.guns.at(-1)!.shells[0],
|
||||||
tank,
|
tank,
|
||||||
track: tank.tracks.at(-1)!,
|
track: tank.tracks.at(-1)!,
|
||||||
yaw,
|
|
||||||
pitch,
|
|
||||||
turret,
|
turret,
|
||||||
assaultDistance:
|
assaultDistance: (gun.assault_ranges?.ranges[0].distance ?? 0) / 2,
|
||||||
(gun.gun_type!.value.base.assault_ranges?.ranges[0].distance ?? 0) / 2,
|
|
||||||
} satisfies DuelMember;
|
} satisfies DuelMember;
|
||||||
}
|
}
|
||||||
@@ -25,5 +25,6 @@ export function useModel(id: number) {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return cache[id];
|
return cache[id];
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
TurretDefinition,
|
TurretDefinition,
|
||||||
} from '@blitzkit/core';
|
} from '@blitzkit/core';
|
||||||
import { invalidate } from '@react-three/fiber';
|
import { invalidate } from '@react-three/fiber';
|
||||||
|
import type { QuicklimeEvent } from 'quicklime';
|
||||||
import { type RefObject, useEffect } from 'react';
|
import { type RefObject, useEffect } from 'react';
|
||||||
import { Euler, Group, Vector3 } from 'three';
|
import { Euler, Group, Vector3 } from 'three';
|
||||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||||
@@ -37,10 +38,15 @@ export function useTankTransform(
|
|||||||
const gunPosition = new Vector3();
|
const gunPosition = new Vector3();
|
||||||
const gunRotation = new Euler();
|
const gunRotation = new Euler();
|
||||||
|
|
||||||
function handleModelTransform(modelTransform?: ModelTransformEventData) {
|
function handleModelTransform(
|
||||||
const duel = duelStore.getState();
|
event: QuicklimeEvent<ModelTransformEventData>,
|
||||||
const yaw = modelTransform?.yaw ?? duel.protagonist.yaw;
|
) {
|
||||||
const pitch = modelTransform?.pitch ?? duel.protagonist.pitch;
|
handleModelTransformInner(event.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModelTransformInner(data?: ModelTransformEventData) {
|
||||||
|
const yaw = data?.yaw ?? modelTransformEvent.last!.yaw;
|
||||||
|
const pitch = data?.pitch ?? modelTransformEvent.last!.pitch;
|
||||||
|
|
||||||
gunPosition
|
gunPosition
|
||||||
.set(0, 0, 0)
|
.set(0, 0, 0)
|
||||||
@@ -91,24 +97,12 @@ export function useTankTransform(
|
|||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModelTransform();
|
handleModelTransformInner();
|
||||||
|
|
||||||
modelTransformEvent.on(handleModelTransform);
|
modelTransformEvent.on(handleModelTransform);
|
||||||
|
|
||||||
const unsubscribes = [
|
|
||||||
duelStore.subscribe(
|
|
||||||
(state) => state.protagonist.pitch,
|
|
||||||
() => handleModelTransform(),
|
|
||||||
),
|
|
||||||
duelStore.subscribe(
|
|
||||||
(state) => state.protagonist.yaw,
|
|
||||||
() => handleModelTransform(),
|
|
||||||
),
|
|
||||||
() => modelTransformEvent.off(handleModelTransform),
|
|
||||||
];
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
modelTransformEvent.off(handleModelTransform);
|
||||||
};
|
};
|
||||||
}, [track, turret]);
|
}, [track, turret]);
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import Navbar from '../components/Navbar.astro';
|
|||||||
import Party3 from '../components/Party3.astro';
|
import Party3 from '../components/Party3.astro';
|
||||||
import './base.css';
|
import './base.css';
|
||||||
import { DEFAULT_LOCALE } from '@blitzkit/i18n';
|
import { DEFAULT_LOCALE } from '@blitzkit/i18n';
|
||||||
|
import Servicing from '../components/Servicing.astro';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
noFooter?: boolean;
|
noFooter?: boolean;
|
||||||
@@ -15,6 +16,7 @@ export interface Props {
|
|||||||
integrated?: boolean;
|
integrated?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const servicing = import.meta.env.SERVICING;
|
||||||
const { noFooter, noNavbar, integrated } = Astro.props;
|
const { noFooter, noNavbar, integrated } = Astro.props;
|
||||||
const locale = Astro.params.locale ?? DEFAULT_LOCALE;
|
const locale = Astro.params.locale ?? DEFAULT_LOCALE;
|
||||||
---
|
---
|
||||||
@@ -65,7 +67,7 @@ const locale = Astro.params.locale ?? DEFAULT_LOCALE;
|
|||||||
|
|
||||||
<Checks {locale} client:only="react" />
|
<Checks {locale} client:only="react" />
|
||||||
|
|
||||||
<slot />
|
{servicing ? <Servicing /> : <slot />}
|
||||||
|
|
||||||
{!noFooter && <Footer />}
|
{!noFooter && <Footer />}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { PageWrapper } from '../../../../components/PageWrapper';
|
|||||||
import { CalloutsSection } from '../../../../components/Tankopedia/CalloutsSection';
|
import { CalloutsSection } from '../../../../components/Tankopedia/CalloutsSection';
|
||||||
import { CharacteristicsSection } from '../../../../components/Tankopedia/CharacteristicsSection';
|
import { CharacteristicsSection } from '../../../../components/Tankopedia/CharacteristicsSection';
|
||||||
import { GameModeSection } from '../../../../components/Tankopedia/GameModeSection';
|
import { GameModeSection } from '../../../../components/Tankopedia/GameModeSection';
|
||||||
|
import { GuideSection } from '../../../../components/Tankopedia/GuideSection';
|
||||||
import { HeroSection } from '../../../../components/Tankopedia/HeroSection';
|
import { HeroSection } from '../../../../components/Tankopedia/HeroSection';
|
||||||
import { MetaSection } from '../../../../components/Tankopedia/MetaSection';
|
import { MetaSection } from '../../../../components/Tankopedia/MetaSection';
|
||||||
import { TechTreeSection } from '../../../../components/Tankopedia/TechTreeSection';
|
import { TechTreeSection } from '../../../../components/Tankopedia/TechTreeSection';
|
||||||
@@ -19,10 +20,12 @@ import { Duel } from '../../../../stores/duel';
|
|||||||
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
||||||
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
|
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
|
||||||
import type { MaybeSkeletonComponentProps } from '../../../../types/maybeSkeletonComponentProps';
|
import type { MaybeSkeletonComponentProps } from '../../../../types/maybeSkeletonComponentProps';
|
||||||
|
import type { TankGuide } from './index.astro';
|
||||||
|
|
||||||
type PageProps = MaybeSkeletonComponentProps &
|
type PageProps = MaybeSkeletonComponentProps &
|
||||||
LocaleAcceptorProps & {
|
LocaleAcceptorProps & {
|
||||||
id: number;
|
id: number;
|
||||||
|
guide?: TankGuide;
|
||||||
};
|
};
|
||||||
|
|
||||||
const [tankDefinitions, provisionDefinitions, modelDefinitions] =
|
const [tankDefinitions, provisionDefinitions, modelDefinitions] =
|
||||||
@@ -32,7 +35,7 @@ const [tankDefinitions, provisionDefinitions, modelDefinitions] =
|
|||||||
awaitableModelDefinitions,
|
awaitableModelDefinitions,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function Page({ id, skeleton, locale }: PageProps) {
|
export function Page({ id, skeleton, locale, guide }: PageProps) {
|
||||||
const tank = tankDefinitions.tanks[id];
|
const tank = tankDefinitions.tanks[id];
|
||||||
const model = modelDefinitions.models[id];
|
const model = modelDefinitions.models[id];
|
||||||
|
|
||||||
@@ -51,8 +54,9 @@ export function Page({ id, skeleton, locale }: PageProps) {
|
|||||||
{tank.type === TankType.RESEARCHABLE && !tank.deprecated && (
|
{tank.type === TankType.RESEARCHABLE && !tank.deprecated && (
|
||||||
<TechTreeSection skeleton={skeleton} />
|
<TechTreeSection skeleton={skeleton} />
|
||||||
)}
|
)}
|
||||||
<CharacteristicsSection />
|
<CharacteristicsSection skeleton={skeleton} />
|
||||||
<GameModeSection />
|
<GameModeSection />
|
||||||
|
{guide && <GuideSection guide={guide} />}
|
||||||
<VideoSection skeleton={skeleton} />
|
<VideoSection skeleton={skeleton} />
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
</Duel.Provider>
|
</Duel.Provider>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import { AstroSeo } from '@astrolib/seo';
|
import { AstroSeo } from '@astrolib/seo';
|
||||||
import {
|
import {
|
||||||
|
assertSecret,
|
||||||
asset,
|
asset,
|
||||||
createDefaultProvisions,
|
createDefaultProvisions,
|
||||||
createDefaultSkills,
|
createDefaultSkills,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
import { DEFAULT_LOCALE, unwrapper } from '@blitzkit/i18n';
|
import { DEFAULT_LOCALE, unwrapper } from '@blitzkit/i18n';
|
||||||
import { literals } from '@blitzkit/i18n/src/literals';
|
import { literals } from '@blitzkit/i18n/src/literals';
|
||||||
import type { GetStaticPaths } from 'astro';
|
import type { GetStaticPaths } from 'astro';
|
||||||
|
import { load } from 'cheerio';
|
||||||
import { tankCharacteristics } from '../../../../core/blitzkit/tankCharacteristics';
|
import { tankCharacteristics } from '../../../../core/blitzkit/tankCharacteristics';
|
||||||
import { getStrings } from '../../../../core/i18n/getStrings';
|
import { getStrings } from '../../../../core/i18n/getStrings';
|
||||||
import BaseLayout from '../../../../layouts/BaseLayout.astro';
|
import BaseLayout from '../../../../layouts/BaseLayout.astro';
|
||||||
@@ -43,6 +45,7 @@ const skillDefinitions = await fetchSkillDefinitions();
|
|||||||
const provisionDefinitions = await fetchProvisionDefinitions();
|
const provisionDefinitions = await fetchProvisionDefinitions();
|
||||||
const equipmentDefinitions = await fetchEquipmentDefinitions();
|
const equipmentDefinitions = await fetchEquipmentDefinitions();
|
||||||
const tankModelDefinitions = await fetchModelDefinitions();
|
const tankModelDefinitions = await fetchModelDefinitions();
|
||||||
|
|
||||||
const tank = tankDefinitions.tanks[id];
|
const tank = tankDefinitions.tanks[id];
|
||||||
const tankModelDefinition = tankModelDefinitions.models[tank.id];
|
const tankModelDefinition = tankModelDefinitions.models[tank.id];
|
||||||
const unwrap = unwrapper(locale);
|
const unwrap = unwrapper(locale);
|
||||||
@@ -57,10 +60,11 @@ const title = literals(strings.website.tools.tankopedia.tank_seo_title, [
|
|||||||
strings.common.nations[tank.nation as keyof typeof strings.common.nations],
|
strings.common.nations[tank.nation as keyof typeof strings.common.nations],
|
||||||
]);
|
]);
|
||||||
const url = import.meta.env.SITE + Astro.url.pathname;
|
const url = import.meta.env.SITE + Astro.url.pathname;
|
||||||
|
|
||||||
const icon = asset(`icons/tanks/big/${id}.webp`);
|
const icon = asset(`icons/tanks/big/${id}.webp`);
|
||||||
const turret = tank.turrets.at(-1)!;
|
const turret = tank.turrets.at(-1)!;
|
||||||
const gun = turret.guns.at(-1)!;
|
const gun = turret.guns.at(-1)!;
|
||||||
const shell = gun.gun_type!.value.base.shells[0];
|
const shell = gun.shells[0];
|
||||||
const characteristics = tankCharacteristics(
|
const characteristics = tankCharacteristics(
|
||||||
{
|
{
|
||||||
tank,
|
tank,
|
||||||
@@ -96,6 +100,30 @@ const description = literals(
|
|||||||
Math.round(characteristics.health).toLocaleString(locale),
|
Math.round(characteristics.health).toLocaleString(locale),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export interface TankGuide {
|
||||||
|
url: string;
|
||||||
|
description?: string;
|
||||||
|
image?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const guideUrl = `${assertSecret(import.meta.env.PUBLIC_GUIDE_WEBSITE)}/${tank.slug}`;
|
||||||
|
let guide: TankGuide | undefined = undefined;
|
||||||
|
const guideResponse = await fetch(guideUrl);
|
||||||
|
|
||||||
|
if (guideResponse.ok) {
|
||||||
|
const $ = await guideResponse.text().then(load);
|
||||||
|
const description = $('.entry-content p').first().text();
|
||||||
|
const image = $('.entry-content img').first().attr('src')?.toString();
|
||||||
|
|
||||||
|
if (description !== undefined || image !== undefined) {
|
||||||
|
guide = {
|
||||||
|
url: guideUrl,
|
||||||
|
description,
|
||||||
|
image,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<AstroSeo
|
<AstroSeo
|
||||||
@@ -110,7 +138,7 @@ const description = literals(
|
|||||||
<meta name="twitter:image:src" content={icon} />
|
<meta name="twitter:image:src" content={icon} />
|
||||||
|
|
||||||
<BaseLayout integrated>
|
<BaseLayout integrated>
|
||||||
<Page {locale} id={id} client:only="react">
|
<Page {locale} id={id} {guide} client:only="react">
|
||||||
<Page {locale} id={id} skeleton slot="fallback" />
|
<Page {locale} id={id} {guide} skeleton slot="fallback" />
|
||||||
</Page>
|
</Page>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
@@ -33,8 +33,6 @@ export interface DuelMember {
|
|||||||
shell: ShellDefinition;
|
shell: ShellDefinition;
|
||||||
track: TrackDefinition;
|
track: TrackDefinition;
|
||||||
equipmentMatrix: EquipmentMatrix;
|
equipmentMatrix: EquipmentMatrix;
|
||||||
pitch: number;
|
|
||||||
yaw: number;
|
|
||||||
consumables: number[];
|
consumables: number[];
|
||||||
provisions: number[];
|
provisions: number[];
|
||||||
camouflage: boolean;
|
camouflage: boolean;
|
||||||
|
|||||||
@@ -78,28 +78,10 @@ export enum TankopediaRelativeAgainst {
|
|||||||
All,
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ShootingRangeZoom {
|
|
||||||
Arcade,
|
|
||||||
Zoom0,
|
|
||||||
Zoom1,
|
|
||||||
Zoom2,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SHOOTING_RANGE_ZOOM_COEFFICIENTS: Record<
|
|
||||||
ShootingRangeZoom,
|
|
||||||
number
|
|
||||||
> = {
|
|
||||||
[ShootingRangeZoom.Arcade]: 1,
|
|
||||||
[ShootingRangeZoom.Zoom0]: 1 / 8,
|
|
||||||
[ShootingRangeZoom.Zoom1]: 1 / 16,
|
|
||||||
[ShootingRangeZoom.Zoom2]: 1 / 25,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TankopediaEphemeral {
|
interface TankopediaEphemeral {
|
||||||
disturbed: boolean;
|
disturbed: boolean;
|
||||||
revealed: boolean;
|
revealed: boolean;
|
||||||
shot?: Shot;
|
shot?: Shot;
|
||||||
shootingRangeZoom: ShootingRangeZoom;
|
|
||||||
skills: Record<string, number>;
|
skills: Record<string, number>;
|
||||||
relativeAgainst: TankopediaRelativeAgainst;
|
relativeAgainst: TankopediaRelativeAgainst;
|
||||||
controlsEnabled: boolean;
|
controlsEnabled: boolean;
|
||||||
@@ -133,7 +115,6 @@ export const TankopediaEphemeral = createContextualStore(
|
|||||||
subscribeWithSelector<TankopediaEphemeral>(() => ({
|
subscribeWithSelector<TankopediaEphemeral>(() => ({
|
||||||
disturbed: false,
|
disturbed: false,
|
||||||
revealed: false,
|
revealed: false,
|
||||||
shootingRangeZoom: ShootingRangeZoom.Arcade,
|
|
||||||
relativeAgainst: TankopediaRelativeAgainst.Class,
|
relativeAgainst: TankopediaRelativeAgainst.Class,
|
||||||
editStatic: false,
|
editStatic: false,
|
||||||
skills: createDefaultSkills(skillDefinitions),
|
skills: createDefaultSkills(skillDefinitions),
|
||||||
|
|||||||
@@ -38,5 +38,4 @@ export enum TankopediaDisplay {
|
|||||||
Model,
|
Model,
|
||||||
DynamicArmor,
|
DynamicArmor,
|
||||||
StaticArmor,
|
StaticArmor,
|
||||||
ShootingRange,
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user