mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-15 05:36:11 +01:00
Compare commits
97
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe4afdcbf0 | ||
|
|
ef2a47e756 | ||
|
|
c2d625e1ed | ||
|
|
d33990bdb4 | ||
|
|
96482d4268 | ||
|
|
8b527f2333 | ||
|
|
f56c31fb48 | ||
|
|
7d164e415a | ||
|
|
b3a9f0889e | ||
|
|
6d24ed92d0 | ||
|
|
d287e66265 | ||
|
|
ac29d1ead0 | ||
|
|
b6c3fdc4e9 | ||
|
|
4efdaaf7c9 | ||
|
|
87b6a36520 | ||
|
|
cf38cb3a6e | ||
|
|
9cb5113f6e | ||
|
|
7dd1c0c32a | ||
|
|
03233f3fdc | ||
|
|
377159c298 | ||
|
|
d8431fb620 | ||
|
|
0546ddefbd | ||
|
|
26f6889012 | ||
|
|
3ad6088565 | ||
|
|
b6fd568cc6 | ||
|
|
3861b07aef | ||
|
|
2bf57e2c72 | ||
|
|
744c6654cf | ||
|
|
b82e905757 | ||
|
|
c42e5b7dc3 | ||
|
|
b716218ecd | ||
|
|
511346e4bb | ||
|
|
5c2e49453e | ||
|
|
5d397de766 | ||
|
|
aa763f65d1 | ||
|
|
94cbd95509 | ||
|
|
f3762e356f | ||
|
|
5747d7549f | ||
|
|
aea4905d20 | ||
|
|
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
@@ -22,6 +22,19 @@ env:
|
|||||||
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
|
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
|
||||||
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
|
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
|
||||||
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
|
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
|
||||||
|
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
|
||||||
|
ALLOW_ROBOTS: ${{ secrets.ALLOW_ROBOTS }}
|
||||||
|
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
|
||||||
|
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
|
||||||
|
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
|
||||||
|
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
|
||||||
|
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
|
||||||
|
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
|
||||||
|
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
|
||||||
|
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
|
||||||
|
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
|
||||||
|
PUBLIC_GUIDE_WEBSITE: ${{ secrets.PUBLIC_GUIDE_WEBSITE }}
|
||||||
|
SERVICING: ${{ secrets.SERVICING }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
host:
|
host:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"trailingComma": "all",
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 80
|
|
||||||
}
|
|
||||||
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!
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# BlitzKit 1.27.18
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added a mini-game to BlitzKit similar to "Guess Who's That Pokemon" but with tanks: http://blitzkit.vercel.app/guess
|
||||||
|
- Switched to a new renderer for better lighting for the Tankopedia
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- The loading bar for the Tankopedia sandbox now shows up again
|
||||||
|
- Removed a lot of dead code, improving performance
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# BlitzKit 1.27.19
|
||||||
|
|
||||||
|
Two updates in a day? I just got a little excited by the new Tankopedia lighting.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- The camera is now lower town in the Tankopedia, giving the tank a more heroic feel
|
||||||
|
- Tanks are better framed across all devices, minimizing whitespace
|
||||||
|
- Added a skip button for the reload visualizer to skip to the next event
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Removed the useless "show grid" setting
|
||||||
|
- Taking screenshots now no longer includes the scene props like the floor
|
||||||
|
- The subtler color contrast for the advanced highlighting has now been reverted back to the higher contrast
|
||||||
|
- The shot display card is now properly aligned as a proper table again
|
||||||
|
- Tank modules are once again considered in the penetration tester
|
||||||
|
- Tank filters in the antagonist selector in the Tankopedia sandbox are now properly aligned
|
||||||
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkit",
|
"name": "blitzkit",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.27.15",
|
"version": "1.27.19",
|
||||||
"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
@@ -861,6 +861,13 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tools": {
|
"tools": {
|
||||||
|
"mixer": {
|
||||||
|
"name": "Mixer",
|
||||||
|
"button": "Mix and match",
|
||||||
|
"description": "Mix tank modules",
|
||||||
|
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz"
|
||||||
|
},
|
||||||
|
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess the tank",
|
"name": "Guess the tank",
|
||||||
"button": "Guess away",
|
"button": "Guess away",
|
||||||
@@ -887,6 +894,10 @@
|
|||||||
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
|
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
|
||||||
|
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
|
"reload": {
|
||||||
|
"skip": "Skip"
|
||||||
|
},
|
||||||
|
|
||||||
"assault": {
|
"assault": {
|
||||||
"title": "Assault shells"
|
"title": "Assault shells"
|
||||||
},
|
},
|
||||||
@@ -913,6 +924,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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -928,9 +946,7 @@
|
|||||||
"green_penetration": "Green penetration",
|
"green_penetration": "Green penetration",
|
||||||
"hide_model_under_armor": "Hide model under armor",
|
"hide_model_under_armor": "Hide model under armor",
|
||||||
"opaque": "Opaque",
|
"opaque": "Opaque",
|
||||||
"dev_wireframe": "DEV: wireframe",
|
"dev_wireframe": "DEV: wireframe"
|
||||||
"environment": "Environment",
|
|
||||||
"show_grid": "Show grid"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"screenshot": {
|
"screenshot": {
|
||||||
@@ -1026,7 +1042,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 +1105,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 +1164,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`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "varuna",
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { produce } from "immer";
|
||||||
|
import { merge } from "lodash-es";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export class Varuna<Type, Arguments = void> {
|
||||||
|
private listeners = new Set<(state: Type) => void>();
|
||||||
|
|
||||||
|
private initialized = false;
|
||||||
|
private _initial?: Type;
|
||||||
|
private _state?: Type;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private creator: Type | ((...args: Arguments[]) => Type),
|
||||||
|
private persistence?: string
|
||||||
|
) {
|
||||||
|
if (typeof creator !== "function") this.initialize(creator);
|
||||||
|
}
|
||||||
|
|
||||||
|
private initialize(initial: Type) {
|
||||||
|
let data = initial;
|
||||||
|
|
||||||
|
if (this.persistence && typeof localStorage !== "undefined") {
|
||||||
|
const dehydrated = localStorage.getItem(this.persistence);
|
||||||
|
|
||||||
|
if (dehydrated) {
|
||||||
|
const rehydrated = JSON.parse(dehydrated) as unknown;
|
||||||
|
data = merge(data, rehydrated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
this._initial = data;
|
||||||
|
this._state = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
useInitialization(...args: Arguments[]) {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
if (typeof this.creator !== "function") {
|
||||||
|
throw new Error("Provider must be used with a function creator");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initialize((this.creator as (...args: Arguments[]) => Type)(...args));
|
||||||
|
}
|
||||||
|
|
||||||
|
private dispatch() {
|
||||||
|
const state = this.state;
|
||||||
|
this.listeners.forEach((callback) => callback(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
on<Slice>(slicer: (state: Type) => Slice, callback: (state: Slice) => void) {
|
||||||
|
let last = slicer(this.state);
|
||||||
|
|
||||||
|
function listener(state: Type) {
|
||||||
|
const next = slicer(state);
|
||||||
|
|
||||||
|
if (next !== last) {
|
||||||
|
callback(next);
|
||||||
|
last = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners.add(listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.listeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mutate(mutator: (state: Type) => void) {
|
||||||
|
this.set(produce(this.state, mutator));
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertInitialized() {
|
||||||
|
if (!this.initialized) throw new Error("State used before initialization");
|
||||||
|
}
|
||||||
|
|
||||||
|
get state() {
|
||||||
|
this.assertInitialized();
|
||||||
|
return this._state as Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(state: Type) {
|
||||||
|
this.assertInitialized();
|
||||||
|
this._state = state;
|
||||||
|
this.dispatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
get initial() {
|
||||||
|
this.assertInitialized();
|
||||||
|
return this._initial as Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
use<Slice = Type>(
|
||||||
|
slicer: (state: Type) => Slice = (state) => state as unknown as Slice
|
||||||
|
) {
|
||||||
|
const instance = this;
|
||||||
|
const [slice, setSlice] = useState(slicer(this.state));
|
||||||
|
|
||||||
|
useEffect(() => instance.on(slicer, setSlice), []);
|
||||||
|
|
||||||
|
return slice;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json"
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"@astrolib/seo": "^1.0.0-beta.8",
|
"@astrolib/seo": "^1.0.0-beta.8",
|
||||||
"@blitzkit/core": "workspace:*",
|
"@blitzkit/core": "workspace:*",
|
||||||
"@blitzkit/i18n": "workspace:*",
|
"@blitzkit/i18n": "workspace:*",
|
||||||
|
"varuna": "workspace:*",
|
||||||
"@fontsource/inter": "^5.2.5",
|
"@fontsource/inter": "^5.2.5",
|
||||||
"@nanostores/persistent": "^0.10.2",
|
"@nanostores/persistent": "^0.10.2",
|
||||||
"@nanostores/react": "^0.7.3",
|
"@nanostores/react": "^0.7.3",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 490 KiB After Width: | Height: | Size: 669 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -1,4 +1,4 @@
|
|||||||
import { ReloadIcon } from '@radix-ui/react-icons';
|
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Code,
|
Code,
|
||||||
@@ -6,26 +6,22 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import { Html } from '@react-three/drei';
|
import { Html } from "@react-three/drei";
|
||||||
import { useRef } from 'react';
|
import { useRef } from "react";
|
||||||
import { radToDeg } from 'three/src/math/MathUtils.js';
|
import { radToDeg } from "three/src/math/MathUtils.js";
|
||||||
import { resolveArmorIndex } from '../../../core/blitzkit/resolveArmorIndex';
|
import { resolveArmorIndex } from "../../../core/blitzkit/resolveArmorIndex";
|
||||||
import { App } from '../../../stores/app';
|
import { App } from "../../../stores/app";
|
||||||
import { Duel } from '../../../stores/duel';
|
import { Duel } from "../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../stores/tankopedia";
|
||||||
import { layerTypeNames } from './ShotDisplayCard';
|
import { layerTypeNames } from "./ShotDisplayCard";
|
||||||
import { ArmorType } from './SpacedArmorScene';
|
import { ArmorType } from "./SpacedArmorScene";
|
||||||
|
|
||||||
export function ArmorPlateDisplay() {
|
export function ArmorPlateDisplay() {
|
||||||
const highlightArmor = TankopediaEphemeral.use(
|
const highlightArmor = Tankopedia.use((state) => state.highlightArmor);
|
||||||
(state) => state.highlightArmor,
|
|
||||||
);
|
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const developerMode = App.useDeferred((state) => state.developerMode, false);
|
const developerMode = App.useDeferred((state) => state.developerMode, false);
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
|
|
||||||
if (highlightArmor === undefined) return null;
|
if (highlightArmor === undefined) return null;
|
||||||
|
|
||||||
@@ -33,20 +29,20 @@ export function ArmorPlateDisplay() {
|
|||||||
<group position={highlightArmor.point}>
|
<group position={highlightArmor.point}>
|
||||||
<Html
|
<Html
|
||||||
center
|
center
|
||||||
style={{ pointerEvents: highlightArmor.editingPlate ? 'auto' : 'none' }}
|
style={{ pointerEvents: highlightArmor.editingPlate ? "auto" : "none" }}
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
variant="classic"
|
variant="classic"
|
||||||
mb="4"
|
mb="4"
|
||||||
style={{
|
style={{
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: "nowrap",
|
||||||
color: highlightArmor.color,
|
color: highlightArmor.color,
|
||||||
transform: 'translateY(-50%)',
|
transform: "translateY(-50%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex direction="column">
|
<Flex direction="column">
|
||||||
<Text weight="bold">
|
<Text weight="bold">
|
||||||
{layerTypeNames[highlightArmor.type]}{' '}
|
{layerTypeNames[highlightArmor.type]}{" "}
|
||||||
{highlightArmor.thickness.toFixed(0)}
|
{highlightArmor.thickness.toFixed(0)}
|
||||||
<Text size="1" weight="regular">
|
<Text size="1" weight="regular">
|
||||||
mm
|
mm
|
||||||
@@ -92,35 +88,34 @@ export function ArmorPlateDisplay() {
|
|||||||
|
|
||||||
if (isNaN(thickness) || thickness < 0) return;
|
if (isNaN(thickness) || thickness < 0) return;
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
if (event.key === "Enter") {
|
||||||
input.current?.blur();
|
input.current?.blur();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
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_")) {
|
||||||
tank.armor.thickness[index] = thickness;
|
tank.armor.thickness[index] = thickness;
|
||||||
} else if (highlightArmor.name.startsWith('turret_')) {
|
} else if (highlightArmor.name.startsWith("turret_")) {
|
||||||
turret.armor.thickness[index] = thickness;
|
turret.armor.thickness[index] = thickness;
|
||||||
} else if (highlightArmor.name.startsWith('gun_')) {
|
} else if (highlightArmor.name.startsWith("gun_")) {
|
||||||
if (highlightArmor.name.includes('_armor_')) {
|
if (highlightArmor.name.includes("_armor_")) {
|
||||||
gun.armor.thickness[index] = thickness;
|
gun.armor.thickness[index] = thickness;
|
||||||
} else {
|
} else {
|
||||||
gun.thickness = thickness;
|
gun.thickness = thickness;
|
||||||
}
|
}
|
||||||
} else if (highlightArmor.name.startsWith('chassis_')) {
|
} else if (highlightArmor.name.startsWith("chassis_")) {
|
||||||
track.thickness = thickness;
|
track.thickness = thickness;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === "Escape") {
|
||||||
input.current?.blur();
|
input.current?.blur();
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -138,7 +133,7 @@ export function ArmorPlateDisplay() {
|
|||||||
mt="2"
|
mt="2"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
if (!highlightArmor.editingPlate) return;
|
if (!highlightArmor.editingPlate) return;
|
||||||
|
|
||||||
const index = resolveArmorIndex(highlightArmor.name);
|
const index = resolveArmorIndex(highlightArmor.name);
|
||||||
@@ -146,38 +141,33 @@ export function ArmorPlateDisplay() {
|
|||||||
if (index === undefined) return;
|
if (index === undefined) return;
|
||||||
|
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
const initialState =
|
const initialState = Tankopedia.initial;
|
||||||
tankopediaEphemeralStore.getInitialState();
|
|
||||||
const { protagonist } = duelStore.getState();
|
const { protagonist } = duelStore.getState();
|
||||||
const tank = draft.model;
|
const tank = draft.model;
|
||||||
const initialTank = initialState.model;
|
const initialTank = initialState.model;
|
||||||
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];
|
||||||
|
|
||||||
if (highlightArmor.name.startsWith('hull_')) {
|
if (highlightArmor.name.startsWith("hull_")) {
|
||||||
tank.armor.thickness[index] =
|
tank.armor.thickness[index] =
|
||||||
initialTank.armor.thickness[index];
|
initialTank.armor.thickness[index];
|
||||||
} else if (highlightArmor.name.startsWith('turret_')) {
|
} else if (highlightArmor.name.startsWith("turret_")) {
|
||||||
turret.armor.thickness[index] =
|
turret.armor.thickness[index] =
|
||||||
initialTurret.armor.thickness[index];
|
initialTurret.armor.thickness[index];
|
||||||
} else if (highlightArmor.name.startsWith('gun_')) {
|
} else if (highlightArmor.name.startsWith("gun_")) {
|
||||||
if (highlightArmor.name.includes('_armor_')) {
|
if (highlightArmor.name.includes("_armor_")) {
|
||||||
gun.armor.thickness[index] =
|
gun.armor.thickness[index] =
|
||||||
initialGun.armor.thickness[index];
|
initialGun.armor.thickness[index];
|
||||||
} else {
|
} else {
|
||||||
gun.thickness = initialGun.thickness;
|
gun.thickness = initialGun.thickness;
|
||||||
}
|
}
|
||||||
} else if (highlightArmor.name.startsWith('chassis_')) {
|
} else if (highlightArmor.name.startsWith("chassis_")) {
|
||||||
track.thickness = initialTrack.thickness;
|
track.thickness = initialTrack.thickness;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export const PrimaryArmorScene = memo(() => {
|
|||||||
const wrapper = useRef<Group>(null);
|
const wrapper = useRef<Group>(null);
|
||||||
const turretContainer = useRef<Group>(null);
|
const turretContainer = useRef<Group>(null);
|
||||||
const gunContainer = useRef<Group>(null);
|
const gunContainer = useRef<Group>(null);
|
||||||
// const protagonist = Duel.use((draft) => draft.protagonist);
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const track = Duel.use((state) => state.protagonist.track);
|
const track = Duel.use((state) => state.protagonist.track);
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
@@ -24,8 +23,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);
|
||||||
|
|||||||
+44
-47
@@ -2,26 +2,28 @@ import {
|
|||||||
canSplash,
|
canSplash,
|
||||||
isExplosive,
|
isExplosive,
|
||||||
resolvePenetrationCoefficient,
|
resolvePenetrationCoefficient,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { invalidate, useFrame } from '@react-three/fiber';
|
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,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
import { degToRad } from "three/src/math/MathUtils.js";
|
||||||
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
|
||||||
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
|
import { jsxTree } from "../../../../core/blitzkit/jsxTree";
|
||||||
import { Duel, type EquipmentMatrix } from '../../../../stores/duel';
|
import { Duel, type EquipmentMatrix } from "../../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../stores/tankopedia";
|
||||||
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
|
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
|
||||||
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";
|
||||||
import { spacedArmorRenderTarget } from './target';
|
import { spacedArmorRenderTarget } from "./target";
|
||||||
|
|
||||||
interface PrimaryArmorSceneComponentProps {
|
interface PrimaryArmorSceneComponentProps {
|
||||||
thickness: number;
|
thickness: number;
|
||||||
@@ -32,13 +34,15 @@ 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,
|
||||||
}: PrimaryArmorSceneComponentProps) {
|
}: PrimaryArmorSceneComponentProps) {
|
||||||
const tankopediaPersistentStore = TankopediaPersistent.useStore();
|
const tankopediaPersistentStore = TankopediaPersistent.useStore();
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const material = new ShaderMaterial({
|
const material = new ShaderMaterial({
|
||||||
fragmentShader,
|
fragmentShader,
|
||||||
vertexShader,
|
vertexShader,
|
||||||
@@ -46,6 +50,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,
|
||||||
@@ -73,31 +79,25 @@ export function PrimaryArmorSceneComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function handleShellChange() {
|
function handleShellChange() {
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||||
|
|
||||||
material.uniforms.caliber.value = shell.caliber;
|
material.uniforms.caliber.value = shell.caliber;
|
||||||
material.uniforms.ricochet.value = degToRad(
|
material.uniforms.ricochet.value = degToRad(
|
||||||
isExplosive(shell.type) ? 90 : shell.ricochet!,
|
isExplosive(shell.type) ? 90 : shell.ricochet!
|
||||||
);
|
);
|
||||||
material.uniforms.normalization.value = degToRad(
|
material.uniforms.normalization.value = degToRad(
|
||||||
shell.normalization ?? 0,
|
shell.normalization ?? 0
|
||||||
);
|
);
|
||||||
material.uniforms.isExplosive.value = isExplosive(shell.type);
|
material.uniforms.isExplosive.value = isExplosive(shell.type);
|
||||||
material.uniforms.canSplash.value = canSplash(shell.type);
|
material.uniforms.canSplash.value = canSplash(shell.type);
|
||||||
material.uniforms.damage.value = shell.armor_damage;
|
material.uniforms.damage.value = shell.armor_damage;
|
||||||
material.uniforms.explosionRadius.value = shell.explosion_radius;
|
material.uniforms.explosionRadius.value = shell.explosion_radius;
|
||||||
|
|
||||||
await handleProtagonistEquipmentChange(
|
handleProtagonistEquipmentChange(duel.protagonist.equipmentMatrix, true);
|
||||||
duel.protagonist.equipmentMatrix,
|
handleAntagonistEquipmentChange(duel.antagonist.equipmentMatrix, true);
|
||||||
true,
|
|
||||||
);
|
|
||||||
await handleAntagonistEquipmentChange(
|
|
||||||
duel.antagonist.equipmentMatrix,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
@@ -114,15 +114,15 @@ export function PrimaryArmorSceneComponent({
|
|||||||
function handleWireframeChange(wireframe: boolean) {
|
function handleWireframeChange(wireframe: boolean) {
|
||||||
material.wireframe = wireframe;
|
material.wireframe = wireframe;
|
||||||
}
|
}
|
||||||
async function handleProtagonistEquipmentChange(
|
function handleProtagonistEquipmentChange(
|
||||||
equipment: EquipmentMatrix,
|
equipment: EquipmentMatrix,
|
||||||
noInvalidate = false,
|
noInvalidate = false
|
||||||
) {
|
) {
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const hasEnhancedArmor = await hasEquipment(
|
const hasEnhancedArmor = hasEquipment(
|
||||||
110,
|
110,
|
||||||
duel.protagonist.tank.equipment_preset,
|
duel.protagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
material.uniforms.thickness.value = hasEnhancedArmor
|
material.uniforms.thickness.value = hasEnhancedArmor
|
||||||
? thickness * 1.03
|
? thickness * 1.03
|
||||||
@@ -130,18 +130,18 @@ export function PrimaryArmorSceneComponent({
|
|||||||
|
|
||||||
if (!noInvalidate) invalidate();
|
if (!noInvalidate) invalidate();
|
||||||
}
|
}
|
||||||
async function handleAntagonistEquipmentChange(
|
function handleAntagonistEquipmentChange(
|
||||||
equipment: EquipmentMatrix,
|
equipment: EquipmentMatrix,
|
||||||
noInvalidate = false,
|
noInvalidate = false
|
||||||
) {
|
) {
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||||
const penetration = shell.penetration.near;
|
const penetration = shell.penetration.near;
|
||||||
const hasCalibratedShells = await hasEquipment(
|
const hasCalibratedShells = hasEquipment(
|
||||||
103,
|
103,
|
||||||
duel.antagonist.tank.equipment_preset,
|
duel.antagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
|
|
||||||
material.uniforms.penetration.value =
|
material.uniforms.penetration.value =
|
||||||
@@ -159,37 +159,34 @@ export function PrimaryArmorSceneComponent({
|
|||||||
handleOpaqueChange(initialState.opaque);
|
handleOpaqueChange(initialState.opaque);
|
||||||
handleWireframeChange(initialState.wireframe);
|
handleWireframeChange(initialState.wireframe);
|
||||||
handleProtagonistEquipmentChange(
|
handleProtagonistEquipmentChange(
|
||||||
duelStore.getState().protagonist.equipmentMatrix,
|
duelStore.getState().protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
handleAntagonistEquipmentChange(
|
handleAntagonistEquipmentChange(
|
||||||
duelStore.getState().antagonist.equipmentMatrix,
|
duelStore.getState().antagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
||||||
tankopediaEphemeralStore.subscribe(
|
Tankopedia.on((state) => state.customShell, handleShellChange),
|
||||||
(state) => state.customShell,
|
|
||||||
handleShellChange,
|
|
||||||
),
|
|
||||||
tankopediaPersistentStore.subscribe(
|
tankopediaPersistentStore.subscribe(
|
||||||
(state) => state.greenPenetration,
|
(state) => state.greenPenetration,
|
||||||
handleGreenPenetrationChange,
|
handleGreenPenetrationChange
|
||||||
),
|
),
|
||||||
tankopediaPersistentStore.subscribe(
|
tankopediaPersistentStore.subscribe(
|
||||||
(state) => state.advancedHighlighting,
|
(state) => state.advancedHighlighting,
|
||||||
handleAdvancedHighlightingChange,
|
handleAdvancedHighlightingChange
|
||||||
),
|
),
|
||||||
tankopediaPersistentStore.subscribe(
|
tankopediaPersistentStore.subscribe(
|
||||||
(state) => state.opaque,
|
(state) => state.opaque,
|
||||||
handleOpaqueChange,
|
handleOpaqueChange
|
||||||
),
|
),
|
||||||
duelStore.subscribe(
|
duelStore.subscribe(
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
(state) => state.protagonist.equipmentMatrix,
|
||||||
(equipment) => handleProtagonistEquipmentChange(equipment),
|
(equipment) => handleProtagonistEquipmentChange(equipment)
|
||||||
),
|
),
|
||||||
duelStore.subscribe(
|
duelStore.subscribe(
|
||||||
(state) => state.antagonist.equipmentMatrix,
|
(state) => state.antagonist.equipmentMatrix,
|
||||||
(equipment) => handleAntagonistEquipmentChange(equipment),
|
(equipment) => handleAntagonistEquipmentChange(equipment)
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -200,7 +197,7 @@ export function PrimaryArmorSceneComponent({
|
|||||||
|
|
||||||
useFrame(({ gl, camera }) => {
|
useFrame(({ gl, camera }) => {
|
||||||
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
|
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
|
||||||
gl.getPixelRatio(),
|
gl.getPixelRatio()
|
||||||
);
|
);
|
||||||
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
|
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
|
||||||
material.uniforms.spacedArmorDepth.value =
|
material.uniforms.spacedArmorDepth.value =
|
||||||
|
|||||||
+4
-5
@@ -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;
|
||||||
@@ -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,13 +97,13 @@ 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) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { J_HAT, K_HAT } from '@blitzkit/core';
|
import { J_HAT, K_HAT } from "@blitzkit/core";
|
||||||
import { Box, Flex, Text } from '@radix-ui/themes';
|
import { Box, Flex, Text } from "@radix-ui/themes";
|
||||||
import { Html } from '@react-three/drei';
|
import { Html } from "@react-three/drei";
|
||||||
import { invalidate, useFrame } from '@react-three/fiber';
|
import { invalidate, useFrame } from "@react-three/fiber";
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from "lodash-es";
|
||||||
import { useEffect, useRef, type ComponentProps } from 'react';
|
import { useEffect, useRef, type ComponentProps } from "react";
|
||||||
import {
|
import {
|
||||||
BufferGeometry,
|
BufferGeometry,
|
||||||
DoubleSide,
|
DoubleSide,
|
||||||
@@ -15,23 +15,24 @@ import {
|
|||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
Path,
|
Path,
|
||||||
Quaternion,
|
Quaternion,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import { LocaleProvider, useLocale } from '../../../hooks/useLocale';
|
import { isHalloween } from "../../../core/blitzkit/isHalloween";
|
||||||
|
import { LocaleProvider, useLocale } from "../../../hooks/useLocale";
|
||||||
import {
|
import {
|
||||||
TankopediaEphemeral,
|
Tankopedia,
|
||||||
type ShotLayerNonExternal,
|
type ShotLayerNonExternal,
|
||||||
type ShotStatus,
|
type ShotStatus,
|
||||||
} from '../../../stores/tankopediaEphemeral';
|
} from "../../../stores/tankopedia";
|
||||||
import { ShotDisplayCard } from './ShotDisplayCard';
|
import { ShotDisplayCard } from "./ShotDisplayCard";
|
||||||
|
|
||||||
export const shotStatusColors: Record<
|
export const shotStatusColors: Record<
|
||||||
ShotStatus,
|
ShotStatus,
|
||||||
ComponentProps<typeof Text>['color']
|
ComponentProps<typeof Text>["color"]
|
||||||
> = {
|
> = {
|
||||||
blocked: 'red',
|
blocked: "red",
|
||||||
penetration: 'green',
|
penetration: "green",
|
||||||
ricochet: 'yellow',
|
ricochet: "yellow",
|
||||||
splash: 'orange',
|
splash: "orange",
|
||||||
};
|
};
|
||||||
|
|
||||||
const SURFACE_POINT_SIZE = 12;
|
const SURFACE_POINT_SIZE = 12;
|
||||||
@@ -40,7 +41,7 @@ const TRACER_THICK = 1 / 32;
|
|||||||
const TRACER_THIN = TRACER_THICK / 2;
|
const TRACER_THIN = TRACER_THICK / 2;
|
||||||
|
|
||||||
export function ShotDisplay() {
|
export function ShotDisplay() {
|
||||||
const shot = TankopediaEphemeral.use((state) => state.shot);
|
const shot = Tankopedia.use((state) => state.shot);
|
||||||
const inTracer = useRef<Mesh>(null);
|
const inTracer = useRef<Mesh>(null);
|
||||||
const outTracer = useRef<Mesh>(null);
|
const outTracer = useRef<Mesh>(null);
|
||||||
const splashRadiusWrapper = useRef<Group>(null);
|
const splashRadiusWrapper = useRef<Group>(null);
|
||||||
@@ -51,16 +52,16 @@ export function ShotDisplay() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shot?.splashRadius !== undefined) {
|
if (shot?.splashRadius !== undefined) {
|
||||||
const outlineGeometry = new BufferGeometry().setFromPoints(
|
const outlineGeometry = new BufferGeometry().setFromPoints(
|
||||||
new Path().absarc(0, 0, 1, 0, Math.PI * 2).getSpacedPoints(50),
|
new Path().absarc(0, 0, 1, 0, Math.PI * 2).getSpacedPoints(50)
|
||||||
);
|
);
|
||||||
const outlineMaterial = new LineDashedMaterial({
|
const outlineMaterial = new LineDashedMaterial({
|
||||||
color: 'orange',
|
color: "orange",
|
||||||
dashSize: 0.05,
|
dashSize: 0.05,
|
||||||
gapSize: 0.025,
|
gapSize: 0.025,
|
||||||
});
|
});
|
||||||
const outline = new Line(
|
const outline = new Line(
|
||||||
outlineGeometry,
|
outlineGeometry,
|
||||||
outlineMaterial,
|
outlineMaterial
|
||||||
).computeLineDistances();
|
).computeLineDistances();
|
||||||
|
|
||||||
splashRadiusWrapper.current?.add(outline);
|
splashRadiusWrapper.current?.add(outline);
|
||||||
@@ -72,15 +73,12 @@ export function ShotDisplay() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const data = new Date();
|
|
||||||
const isHalloween = data.getMonth() === 9 && data.getDate() === 31;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isHalloween &&
|
isHalloween() &&
|
||||||
shot?.splashRadius !== undefined &&
|
shot?.splashRadius !== undefined &&
|
||||||
shot.in.status === 'penetration'
|
shot.in.status === "penetration"
|
||||||
) {
|
) {
|
||||||
const audio = new Audio('/assets/audio/lotta-damage.mp3');
|
const audio = new Audio("/assets/audio/lotta-damage.mp3");
|
||||||
|
|
||||||
audio.currentTime = 0.7;
|
audio.currentTime = 0.7;
|
||||||
audio.volume = 0.25;
|
audio.volume = 0.25;
|
||||||
@@ -136,11 +134,11 @@ export function ShotDisplay() {
|
|||||||
const outLength =
|
const outLength =
|
||||||
shot.out && shot.out.layers.length > 0
|
shot.out && shot.out.layers.length > 0
|
||||||
? (shot.in.layers.at(-1) as ShotLayerNonExternal).point.distanceTo(
|
? (shot.in.layers.at(-1) as ShotLayerNonExternal).point.distanceTo(
|
||||||
(shot.out.layers.at(-1) as ShotLayerNonExternal).point,
|
(shot.out.layers.at(-1) as ShotLayerNonExternal).point
|
||||||
)
|
)
|
||||||
: SHOT_DISPLAY_LENGTH_INFINITY;
|
: SHOT_DISPLAY_LENGTH_INFINITY;
|
||||||
const inLast = shot.in.layers.findLast(
|
const inLast = shot.in.layers.findLast(
|
||||||
(layer) => layer.type !== null,
|
(layer) => layer.type !== null
|
||||||
) as ShotLayerNonExternal;
|
) as ShotLayerNonExternal;
|
||||||
const outTitleColor = shot.out
|
const outTitleColor = shot.out
|
||||||
? shotStatusColors[shot.out.status]
|
? shotStatusColors[shot.out.status]
|
||||||
@@ -156,7 +154,7 @@ export function ShotDisplay() {
|
|||||||
position={inLast.point}
|
position={inLast.point}
|
||||||
ref={splashRadiusWrapper}
|
ref={splashRadiusWrapper}
|
||||||
rotation={new Euler().setFromQuaternion(
|
rotation={new Euler().setFromQuaternion(
|
||||||
new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal),
|
new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<mesh>
|
<mesh>
|
||||||
@@ -174,13 +172,13 @@ export function ShotDisplay() {
|
|||||||
<Html position={inLast.point} center>
|
<Html position={inLast.point} center>
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: 'none',
|
pointerEvents: "none",
|
||||||
transform: `translateY(${tracerGoingUp ? 50 : -50}%)`,
|
transform: `translateY(${tracerGoingUp ? 50 : -50}%)`,
|
||||||
}}
|
}}
|
||||||
mb={tracerGoingUp ? '0' : '9'}
|
mb={tracerGoingUp ? "0" : "9"}
|
||||||
pb={tracerGoingUp ? '0' : '4'}
|
pb={tracerGoingUp ? "0" : "4"}
|
||||||
mt={tracerGoingUp ? '9' : '0'}
|
mt={tracerGoingUp ? "9" : "0"}
|
||||||
pt={tracerGoingUp ? '4' : '0'}
|
pt={tracerGoingUp ? "4" : "0"}
|
||||||
>
|
>
|
||||||
<LocaleProvider locale={locale}>
|
<LocaleProvider locale={locale}>
|
||||||
<ShotDisplayCard shot={shot} />
|
<ShotDisplayCard shot={shot} />
|
||||||
@@ -192,7 +190,7 @@ export function ShotDisplay() {
|
|||||||
if (layer.type === null) return null;
|
if (layer.type === null) return null;
|
||||||
|
|
||||||
const shotStatusColor =
|
const shotStatusColor =
|
||||||
shot.in.status === 'splash'
|
shot.in.status === "splash"
|
||||||
? shotStatusColors.splash
|
? shotStatusColors.splash
|
||||||
: shotStatusColors[layer.status];
|
: shotStatusColors[layer.status];
|
||||||
|
|
||||||
@@ -201,8 +199,8 @@ export function ShotDisplay() {
|
|||||||
position={layer.point}
|
position={layer.point}
|
||||||
key={index}
|
key={index}
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: 'none',
|
pointerEvents: "none",
|
||||||
transform: 'translateY(-50%)',
|
transform: "translateY(-50%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
@@ -227,7 +225,7 @@ export function ShotDisplay() {
|
|||||||
<group
|
<group
|
||||||
position={inLast.point}
|
position={inLast.point}
|
||||||
rotation={new Euler().setFromQuaternion(
|
rotation={new Euler().setFromQuaternion(
|
||||||
new Quaternion().setFromUnitVectors(J_HAT, shot.in.surfaceNormal),
|
new Quaternion().setFromUnitVectors(J_HAT, shot.in.surfaceNormal)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<mesh position={[0, inLength / 2, 0]} renderOrder={2}>
|
<mesh position={[0, inLength / 2, 0]} renderOrder={2}>
|
||||||
@@ -255,8 +253,8 @@ export function ShotDisplay() {
|
|||||||
rotation={new Euler().setFromQuaternion(
|
rotation={new Euler().setFromQuaternion(
|
||||||
new Quaternion().setFromUnitVectors(
|
new Quaternion().setFromUnitVectors(
|
||||||
J_HAT,
|
J_HAT,
|
||||||
shot.out.surfaceNormal.clone(),
|
shot.out.surfaceNormal.clone()
|
||||||
),
|
)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<mesh position={[0, outLength / 2, 0]} renderOrder={2}>
|
<mesh position={[0, outLength / 2, 0]} renderOrder={2}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { literals } from '@blitzkit/i18n/src/literals';
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -6,19 +6,19 @@ import {
|
|||||||
Separator,
|
Separator,
|
||||||
Text,
|
Text,
|
||||||
type CardProps,
|
type CardProps,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from "react";
|
||||||
import { radToDeg } from 'three/src/math/MathUtils.js';
|
import { radToDeg } from "three/src/math/MathUtils.js";
|
||||||
import { useLocale } from '../../../hooks/useLocale';
|
import { useLocale } from "../../../hooks/useLocale";
|
||||||
import type { Shot, ShotLayer } from '../../../stores/tankopediaEphemeral';
|
import type { Shot, ShotLayer } from "../../../stores/tankopedia";
|
||||||
import { shotStatusColors } from './ShotDisplay';
|
import { shotStatusColors } from "./ShotDisplay";
|
||||||
import { ArmorType } from './SpacedArmorScene';
|
import { ArmorType } from "./SpacedArmorScene";
|
||||||
|
|
||||||
export const layerTypeNames = {
|
export const layerTypeNames = {
|
||||||
[ArmorType.Primary]: 'primary',
|
[ArmorType.Primary]: "primary",
|
||||||
[ArmorType.Spaced]: 'spaced',
|
[ArmorType.Spaced]: "spaced",
|
||||||
[ArmorType.External]: 'external',
|
[ArmorType.External]: "external",
|
||||||
null: 'gap',
|
null: "gap",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function LayerEntry({
|
function LayerEntry({
|
||||||
@@ -29,7 +29,7 @@ function LayerEntry({
|
|||||||
angle,
|
angle,
|
||||||
}: {
|
}: {
|
||||||
layerIndex?: string;
|
layerIndex?: string;
|
||||||
shotStatusColor: ComponentProps<typeof Text>['color'];
|
shotStatusColor: ComponentProps<typeof Text>["color"];
|
||||||
layerName: string;
|
layerName: string;
|
||||||
layer: ShotLayer;
|
layer: ShotLayer;
|
||||||
angle?: number;
|
angle?: number;
|
||||||
@@ -37,44 +37,38 @@ function LayerEntry({
|
|||||||
const { locale, strings } = useLocale();
|
const { locale, strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex align="center" gap="2">
|
<tr>
|
||||||
<Text size="1" color="gray" style={{ width: 12 }}>
|
<td>
|
||||||
{layerIndex}
|
<Text size="1" color="gray">
|
||||||
</Text>
|
{layerIndex}
|
||||||
<Text size="2" color={shotStatusColor} style={{ width: 48 }}>
|
</Text>
|
||||||
{layerName}
|
</td>
|
||||||
</Text>
|
|
||||||
|
|
||||||
{layer.type === null && (
|
<td>
|
||||||
<>
|
<Text size="2" color={shotStatusColor}>
|
||||||
<Text size="2" color={shotStatusColor} style={{ width: 64 }}>
|
{layerName}
|
||||||
|
</Text>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{layer.type === null && (
|
||||||
|
<Text size="2" color={shotStatusColor}>
|
||||||
{literals(strings.common.units.mm, [
|
{literals(strings.common.units.mm, [
|
||||||
Math.round(layer.distance * 1000).toLocaleString(locale),
|
Math.round(layer.distance * 1000).toLocaleString(locale),
|
||||||
])}
|
])}
|
||||||
</Text>
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
<Text size="2" color="gray">
|
{layer.type === ArmorType.External && (
|
||||||
{literals(
|
<Text size="2" color={shotStatusColor}>
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
{literals(strings.common.units.mm, [
|
||||||
.ricochet_loss,
|
Math.round(layer.thickness).toLocaleString(locale),
|
||||||
[Math.max(-100, -50 * layer.distance).toFixed(0)],
|
])}
|
||||||
)}
|
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
)}
|
||||||
)}
|
{(layer.type === ArmorType.Primary ||
|
||||||
|
layer.type === ArmorType.Spaced) && (
|
||||||
{layer.type === ArmorType.External && (
|
<Text size="2" color={shotStatusColor}>
|
||||||
<Text size="2" color={shotStatusColor} style={{ width: 64 }}>
|
|
||||||
{literals(strings.common.units.mm, [
|
|
||||||
Math.round(layer.thickness).toLocaleString(locale),
|
|
||||||
])}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(layer.type === ArmorType.Primary ||
|
|
||||||
layer.type === ArmorType.Spaced) && (
|
|
||||||
<>
|
|
||||||
<Text size="2" color={shotStatusColor} style={{ width: 80 }}>
|
|
||||||
{literals(
|
{literals(
|
||||||
angle === undefined
|
angle === undefined
|
||||||
? strings.common.units.mm
|
? strings.common.units.mm
|
||||||
@@ -83,20 +77,35 @@ function LayerEntry({
|
|||||||
[
|
[
|
||||||
Math.round(layer.thicknessAngled).toLocaleString(locale),
|
Math.round(layer.thicknessAngled).toLocaleString(locale),
|
||||||
radToDeg(angle ?? 0).toFixed(0),
|
radToDeg(angle ?? 0).toFixed(0),
|
||||||
],
|
]
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{layer.type === null && (
|
||||||
|
<Text size="2" color="gray">
|
||||||
|
{literals(
|
||||||
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
||||||
|
.ricochet_loss,
|
||||||
|
[Math.max(-100, -50 * layer.distance).toFixed(0)]
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(layer.type === ArmorType.Primary ||
|
||||||
|
layer.type === ArmorType.Spaced) && (
|
||||||
<Text size="2" color="gray">
|
<Text size="2" color="gray">
|
||||||
{literals(
|
{literals(
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
||||||
.nominal,
|
.nominal,
|
||||||
[Math.round(layer.thickness).toLocaleString(locale)],
|
[Math.round(layer.thickness).toLocaleString(locale)]
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
)}
|
||||||
)}
|
</td>
|
||||||
</Flex>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +124,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
const inTitleColor = shotStatusColors[shot.in.status];
|
const inTitleColor = shotStatusColors[shot.in.status];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="classic" style={{ width: 300 }} {...props}>
|
<Card variant="classic" {...props}>
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
<Flex direction="column" gap="1">
|
<Flex direction="column" gap="1">
|
||||||
<Text color={inTitleColor} weight="bold">
|
<Text color={inTitleColor} weight="bold">
|
||||||
@@ -123,11 +132,16 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[
|
||||||
shot.in.status
|
shot.in.status
|
||||||
],
|
],
|
||||||
[Math.round(shot.damage).toLocaleString(locale)],
|
[Math.round(shot.damage).toLocaleString(locale)]
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Flex direction="column">
|
<table
|
||||||
|
style={{
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
borderSpacing: "var(--space-1) 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{shot.in.layers.map((layer, index) => {
|
{shot.in.layers.map((layer, index) => {
|
||||||
const layerName =
|
const layerName =
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||||
@@ -141,7 +155,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
? undefined
|
? undefined
|
||||||
: `${(shot.containsGaps ? index / 2 : index) + 1}.`;
|
: `${(shot.containsGaps ? index / 2 : index) + 1}.`;
|
||||||
const shotStatusColor =
|
const shotStatusColor =
|
||||||
shot.in.status === 'splash'
|
shot.in.status === "splash"
|
||||||
? shotStatusColors.splash
|
? shotStatusColors.splash
|
||||||
: shotStatusColors[layer.status];
|
: shotStatusColors[layer.status];
|
||||||
|
|
||||||
@@ -161,7 +175,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Flex>
|
</table>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{shot.out && shot.out.layers.length > 0 && (
|
{shot.out && shot.out.layers.length > 0 && (
|
||||||
@@ -188,7 +202,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
{literals(
|
{literals(
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||||
.status[shot.out.status],
|
.status[shot.out.status],
|
||||||
[Math.round(shot.damage).toLocaleString(locale)],
|
[Math.round(shot.damage).toLocaleString(locale)]
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@@ -210,7 +224,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
1
|
1
|
||||||
}.`;
|
}.`;
|
||||||
const shotStatusColor =
|
const shotStatusColor =
|
||||||
shot.out!.status === 'splash'
|
shot.out!.status === "splash"
|
||||||
? shotStatusColors.splash
|
? shotStatusColors.splash
|
||||||
: shotStatusColors[layer.status];
|
: shotStatusColors[layer.status];
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
+25
-30
@@ -1,20 +1,20 @@
|
|||||||
import { resolvePenetrationCoefficient } from '@blitzkit/core';
|
import { resolvePenetrationCoefficient } from "@blitzkit/core";
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
AdditiveBlending,
|
AdditiveBlending,
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
Object3D,
|
Object3D,
|
||||||
Plane,
|
Plane,
|
||||||
ShaderMaterial,
|
ShaderMaterial,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import type { ArmorUserData, ExternalModuleVariant } from '../..';
|
import type { ArmorUserData, ExternalModuleVariant } from "../..";
|
||||||
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
|
||||||
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
|
import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
|
||||||
import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel';
|
import { Duel, type EquipmentMatrix } from "../../../../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
import { ArmorType } from '../../../SpacedArmorScene';
|
import { ArmorType } from "../../../SpacedArmorScene";
|
||||||
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";
|
||||||
|
|
||||||
interface SpacedArmorSubExternalProps {
|
interface SpacedArmorSubExternalProps {
|
||||||
node: Object3D;
|
node: Object3D;
|
||||||
@@ -29,7 +29,6 @@ export function SpacedArmorSubExternal({
|
|||||||
variant,
|
variant,
|
||||||
clip,
|
clip,
|
||||||
}: SpacedArmorSubExternalProps) {
|
}: SpacedArmorSubExternalProps) {
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const material = new ShaderMaterial({
|
const material = new ShaderMaterial({
|
||||||
fragmentShader,
|
fragmentShader,
|
||||||
@@ -50,32 +49,30 @@ export function SpacedArmorSubExternal({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleShellChange() {
|
function handleShellChange() {
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||||
material.uniforms.penetration.value = shell.penetration.near;
|
material.uniforms.penetration.value = shell.penetration.near;
|
||||||
}
|
}
|
||||||
async function handleProtagonistEquipmentChange(
|
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
equipment: EquipmentMatrix,
|
|
||||||
) {
|
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const hasEnhancedArmor = await hasEquipment(
|
const hasEnhancedArmor = hasEquipment(
|
||||||
110,
|
110,
|
||||||
duel.protagonist.tank.equipment_preset,
|
duel.protagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
material.uniforms.thickness.value = hasEnhancedArmor
|
material.uniforms.thickness.value = hasEnhancedArmor
|
||||||
? thickness * 1.03
|
? thickness * 1.03
|
||||||
: thickness;
|
: thickness;
|
||||||
}
|
}
|
||||||
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||||
const penetration = shell.penetration.near;
|
const penetration = shell.penetration.near;
|
||||||
const hasCalibratedShells = await hasEquipment(
|
const hasCalibratedShells = hasEquipment(
|
||||||
103,
|
103,
|
||||||
duel.antagonist.tank.equipment_preset,
|
duel.antagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
|
|
||||||
material.uniforms.penetration.value =
|
material.uniforms.penetration.value =
|
||||||
@@ -85,25 +82,22 @@ export function SpacedArmorSubExternal({
|
|||||||
|
|
||||||
handleShellChange();
|
handleShellChange();
|
||||||
handleProtagonistEquipmentChange(
|
handleProtagonistEquipmentChange(
|
||||||
duelStore.getState().protagonist.equipmentMatrix,
|
duelStore.getState().protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
handleAntagonistEquipmentChange(
|
handleAntagonistEquipmentChange(
|
||||||
duelStore.getState().antagonist.equipmentMatrix,
|
duelStore.getState().antagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
||||||
tankopediaEphemeralStore.subscribe(
|
Tankopedia.on((state) => state.customShell, handleShellChange),
|
||||||
(state) => state.customShell,
|
|
||||||
handleShellChange,
|
|
||||||
),
|
|
||||||
duelStore.subscribe(
|
duelStore.subscribe(
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
(state) => state.protagonist.equipmentMatrix,
|
||||||
handleProtagonistEquipmentChange,
|
handleProtagonistEquipmentChange
|
||||||
),
|
),
|
||||||
duelStore.subscribe(
|
duelStore.subscribe(
|
||||||
(state) => state.antagonist.equipmentMatrix,
|
(state) => state.antagonist.equipmentMatrix,
|
||||||
handleAntagonistEquipmentChange,
|
handleAntagonistEquipmentChange
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -125,6 +119,7 @@ export function SpacedArmorSubExternal({
|
|||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
{...props}
|
{...props}
|
||||||
|
onClick={() => {}}
|
||||||
key={`${key}-spaced-sub-external-exclude`}
|
key={`${key}-spaced-sub-external-exclude`}
|
||||||
renderOrder={3}
|
renderOrder={3}
|
||||||
material={
|
material={
|
||||||
|
|||||||
+27
-33
@@ -1,20 +1,20 @@
|
|||||||
import { isExplosive, resolvePenetrationCoefficient } from '@blitzkit/core';
|
import { isExplosive, resolvePenetrationCoefficient } from "@blitzkit/core";
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
AdditiveBlending,
|
AdditiveBlending,
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
ShaderMaterial,
|
ShaderMaterial,
|
||||||
type Object3D,
|
type Object3D,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
import { degToRad } from "three/src/math/MathUtils.js";
|
||||||
import type { ArmorUserData } from '../..';
|
import type { ArmorUserData } from "../..";
|
||||||
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
|
||||||
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
|
import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
|
||||||
import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel';
|
import { Duel, type EquipmentMatrix } from "../../../../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
import { ArmorType } from '../../../SpacedArmorScene';
|
import { ArmorType } from "../../../SpacedArmorScene";
|
||||||
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";
|
||||||
|
|
||||||
interface SpacedArmorSubSpacedProps {
|
interface SpacedArmorSubSpacedProps {
|
||||||
node: Object3D;
|
node: Object3D;
|
||||||
@@ -30,7 +30,6 @@ export function SpacedArmorSubSpaced({
|
|||||||
node,
|
node,
|
||||||
thickness,
|
thickness,
|
||||||
}: SpacedArmorSubSpacedProps) {
|
}: SpacedArmorSubSpacedProps) {
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const material = new ShaderMaterial({
|
const material = new ShaderMaterial({
|
||||||
fragmentShader,
|
fragmentShader,
|
||||||
@@ -52,40 +51,38 @@ export function SpacedArmorSubSpaced({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleShellChange() {
|
function handleShellChange() {
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||||
|
|
||||||
material.uniforms.penetration.value = shell.penetration.near;
|
material.uniforms.penetration.value = shell.penetration.near;
|
||||||
material.uniforms.caliber.value = shell.caliber;
|
material.uniforms.caliber.value = shell.caliber;
|
||||||
material.uniforms.ricochet.value = degToRad(
|
material.uniforms.ricochet.value = degToRad(
|
||||||
isExplosive(shell.type) ? 90 : shell.ricochet!,
|
isExplosive(shell.type) ? 90 : shell.ricochet!
|
||||||
);
|
);
|
||||||
material.uniforms.normalization.value = degToRad(
|
material.uniforms.normalization.value = degToRad(
|
||||||
shell.normalization ?? 0,
|
shell.normalization ?? 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
async function handleProtagonistEquipmentChange(
|
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
equipment: EquipmentMatrix,
|
|
||||||
) {
|
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const hasEnhancedArmor = await hasEquipment(
|
const hasEnhancedArmor = hasEquipment(
|
||||||
110,
|
110,
|
||||||
duel.protagonist.tank.equipment_preset,
|
duel.protagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
material.uniforms.thickness.value = hasEnhancedArmor
|
material.uniforms.thickness.value = hasEnhancedArmor
|
||||||
? thickness * 1.03
|
? thickness * 1.03
|
||||||
: thickness;
|
: thickness;
|
||||||
}
|
}
|
||||||
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||||
const penetration = shell.penetration.near;
|
const penetration = shell.penetration.near;
|
||||||
const hasCalibratedShells = await hasEquipment(
|
const hasCalibratedShells = hasEquipment(
|
||||||
103,
|
103,
|
||||||
duel.antagonist.tank.equipment_preset,
|
duel.antagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
|
|
||||||
material.uniforms.penetration.value =
|
material.uniforms.penetration.value =
|
||||||
@@ -95,25 +92,22 @@ export function SpacedArmorSubSpaced({
|
|||||||
|
|
||||||
handleShellChange();
|
handleShellChange();
|
||||||
handleProtagonistEquipmentChange(
|
handleProtagonistEquipmentChange(
|
||||||
duelStore.getState().protagonist.equipmentMatrix,
|
duelStore.getState().protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
handleAntagonistEquipmentChange(
|
handleAntagonistEquipmentChange(
|
||||||
duelStore.getState().antagonist.equipmentMatrix,
|
duelStore.getState().antagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
||||||
tankopediaEphemeralStore.subscribe(
|
Tankopedia.on((state) => state.customShell, handleShellChange),
|
||||||
(state) => state.customShell,
|
|
||||||
handleShellChange,
|
|
||||||
),
|
|
||||||
duelStore.subscribe(
|
duelStore.subscribe(
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
(state) => state.protagonist.equipmentMatrix,
|
||||||
handleProtagonistEquipmentChange,
|
handleProtagonistEquipmentChange
|
||||||
),
|
),
|
||||||
duelStore.subscribe(
|
duelStore.subscribe(
|
||||||
(state) => state.antagonist.equipmentMatrix,
|
(state) => state.antagonist.equipmentMatrix,
|
||||||
handleAntagonistEquipmentChange,
|
handleAntagonistEquipmentChange
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
+61
-62
@@ -2,9 +2,9 @@ import {
|
|||||||
ShellType,
|
ShellType,
|
||||||
isExplosive,
|
isExplosive,
|
||||||
resolvePenetrationCoefficient,
|
resolvePenetrationCoefficient,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { useThree } from '@react-three/fiber';
|
import { useThree } from "@react-three/fiber";
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from "react";
|
||||||
import {
|
import {
|
||||||
type Intersection,
|
type Intersection,
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
@@ -14,21 +14,21 @@ import {
|
|||||||
Raycaster,
|
Raycaster,
|
||||||
Scene,
|
Scene,
|
||||||
Vector3,
|
Vector3,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
import { degToRad } from "three/src/math/MathUtils.js";
|
||||||
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
|
||||||
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
|
import { jsxTree } from "../../../../core/blitzkit/jsxTree";
|
||||||
import { discardClippingPlane } from '../../../../core/three/discardClippingPlane';
|
import { discardClippingPlane } from "../../../../core/three/discardClippingPlane";
|
||||||
import { Duel } from '../../../../stores/duel';
|
import { Duel } from "../../../../stores/duel";
|
||||||
import {
|
import {
|
||||||
type Shot,
|
type Shot,
|
||||||
type ShotLayerBase,
|
type ShotLayerBase,
|
||||||
type ShotLayerNonExternal,
|
type ShotLayerNonExternal,
|
||||||
TankopediaEphemeral,
|
Tankopedia,
|
||||||
} from '../../../../stores/tankopediaEphemeral';
|
} from "../../../../stores/tankopedia";
|
||||||
import { ArmorType } from '../SpacedArmorScene';
|
import { ArmorType } from "../SpacedArmorScene";
|
||||||
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
|
import { SpacedArmorSubExternal } from "./components/SpacedArmorSubExternal";
|
||||||
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
|
import { SpacedArmorSubSpaced } from "./components/SpacedArmorSubSpaced";
|
||||||
|
|
||||||
type SpacedArmorSceneComponentProps = {
|
type SpacedArmorSceneComponentProps = {
|
||||||
node: Object3D;
|
node: Object3D;
|
||||||
@@ -45,7 +45,7 @@ type SpacedArmorSceneComponentProps = {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ExternalModuleVariant = 'gun' | 'track';
|
export type ExternalModuleVariant = "gun" | "track";
|
||||||
export type ArmorUserData = {
|
export type ArmorUserData = {
|
||||||
thickness: number;
|
thickness: number;
|
||||||
} & (
|
} & (
|
||||||
@@ -76,19 +76,18 @@ export function SpacedArmorSceneComponent({
|
|||||||
clip,
|
clip,
|
||||||
...props
|
...props
|
||||||
}: SpacedArmorSceneComponentProps) {
|
}: SpacedArmorSceneComponentProps) {
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
|
|
||||||
const shoot = useCallback(
|
const shoot = useCallback(
|
||||||
async (
|
(
|
||||||
point: Vector3,
|
point: Vector3,
|
||||||
intersections: Intersection[],
|
intersections: Intersection[],
|
||||||
allowRicochet: boolean,
|
allowRicochet: boolean,
|
||||||
remainingPenetrationInput?: number,
|
remainingPenetrationInput?: number
|
||||||
) => {
|
) => {
|
||||||
const { antagonist, protagonist } = duelStore.getState();
|
const { antagonist, protagonist } = duelStore.getState();
|
||||||
const { customShell } = tankopediaEphemeralStore.getState();
|
const { customShell } = Tankopedia.state;
|
||||||
const shell = customShell ?? antagonist.shell;
|
const shell = customShell ?? antagonist.shell;
|
||||||
const cameraNormal = camera.position.clone().sub(point).normalize();
|
const cameraNormal = camera.position.clone().sub(point).normalize();
|
||||||
const shot: Shot = {
|
const shot: Shot = {
|
||||||
@@ -99,19 +98,19 @@ export function SpacedArmorSceneComponent({
|
|||||||
in: {
|
in: {
|
||||||
surfaceNormal: cameraNormal,
|
surfaceNormal: cameraNormal,
|
||||||
layers: [],
|
layers: [],
|
||||||
status: 'blocked',
|
status: "blocked",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasCalibratedShells = await hasEquipment(
|
const hasCalibratedShells = hasEquipment(
|
||||||
103,
|
103,
|
||||||
antagonist.tank.equipment_preset,
|
antagonist.tank.equipment_preset,
|
||||||
antagonist.equipmentMatrix,
|
antagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const hasEnhancedArmor = await hasEquipment(
|
const hasEnhancedArmor = hasEquipment(
|
||||||
110,
|
110,
|
||||||
protagonist.tank.equipment_preset,
|
protagonist.tank.equipment_preset,
|
||||||
protagonist.equipmentMatrix,
|
protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const penetration =
|
const penetration =
|
||||||
shell.penetration.near *
|
shell.penetration.near *
|
||||||
@@ -119,8 +118,8 @@ export function SpacedArmorSceneComponent({
|
|||||||
const thicknessCoefficient = hasEnhancedArmor ? 1.03 : 1;
|
const thicknessCoefficient = hasEnhancedArmor ? 1.03 : 1;
|
||||||
const filteredIntersections = intersections.filter(
|
const filteredIntersections = intersections.filter(
|
||||||
(intersection) =>
|
(intersection) =>
|
||||||
'type' in intersection.object.userData &&
|
"type" in intersection.object.userData &&
|
||||||
!discardClippingPlane(intersection.object, intersection.point),
|
!discardClippingPlane(intersection.object, intersection.point)
|
||||||
) as unknown as Intersection<Object3D & { userData: ArmorUserData }>[];
|
) as unknown as Intersection<Object3D & { userData: ArmorUserData }>[];
|
||||||
const encounteredExternalModuleVariants: ExternalModuleVariant[] = [];
|
const encounteredExternalModuleVariants: ExternalModuleVariant[] = [];
|
||||||
const noDuplicateIntersections: typeof filteredIntersections = [];
|
const noDuplicateIntersections: typeof filteredIntersections = [];
|
||||||
@@ -129,12 +128,12 @@ export function SpacedArmorSceneComponent({
|
|||||||
if (intersection.object.userData.type === ArmorType.External) {
|
if (intersection.object.userData.type === ArmorType.External) {
|
||||||
if (
|
if (
|
||||||
!encounteredExternalModuleVariants.includes(
|
!encounteredExternalModuleVariants.includes(
|
||||||
intersection.object.userData.variant,
|
intersection.object.userData.variant
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
noDuplicateIntersections.push(intersection);
|
noDuplicateIntersections.push(intersection);
|
||||||
encounteredExternalModuleVariants.push(
|
encounteredExternalModuleVariants.push(
|
||||||
intersection.object.userData.variant,
|
intersection.object.userData.variant
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -162,7 +161,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
const surfaceNormal = intersection
|
const surfaceNormal = intersection
|
||||||
.normal!.clone()
|
.normal!.clone()
|
||||||
.applyQuaternion(
|
.applyQuaternion(
|
||||||
intersection.object.getWorldQuaternion(new Quaternion()),
|
intersection.object.getWorldQuaternion(new Quaternion())
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shell.type === ShellType.HEAT && index > 0) {
|
if (shell.type === ShellType.HEAT && index > 0) {
|
||||||
@@ -174,7 +173,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
shot.in.layers.push({
|
shot.in.layers.push({
|
||||||
type: null,
|
type: null,
|
||||||
distance,
|
distance,
|
||||||
status: blocked ? 'blocked' : 'penetration',
|
status: blocked ? "blocked" : "penetration",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (blocked) break;
|
if (blocked) break;
|
||||||
@@ -194,16 +193,16 @@ export function SpacedArmorSceneComponent({
|
|||||||
surfaceNormal,
|
surfaceNormal,
|
||||||
status:
|
status:
|
||||||
shell.type === ShellType.HE
|
shell.type === ShellType.HE
|
||||||
? 'blocked'
|
? "blocked"
|
||||||
: blocked
|
: blocked
|
||||||
? 'blocked'
|
? "blocked"
|
||||||
: 'penetration',
|
: "penetration",
|
||||||
variant: layer.variant,
|
variant: layer.variant,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const thickness = layer.thickness * thicknessCoefficient;
|
const thickness = layer.thickness * thicknessCoefficient;
|
||||||
const ricochet = degToRad(
|
const ricochet = degToRad(
|
||||||
isExplosive(shell.type) ? 90 : shell.ricochet!,
|
isExplosive(shell.type) ? 90 : shell.ricochet!
|
||||||
);
|
);
|
||||||
const angle = surfaceNormal.angleTo(cameraNormal);
|
const angle = surfaceNormal.angleTo(cameraNormal);
|
||||||
|
|
||||||
@@ -227,7 +226,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
thicknessAngled: finalThickness,
|
thicknessAngled: finalThickness,
|
||||||
point: intersection.point,
|
point: intersection.point,
|
||||||
surfaceNormal,
|
surfaceNormal,
|
||||||
status: 'ricochet',
|
status: "ricochet",
|
||||||
angle,
|
angle,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -245,10 +244,10 @@ export function SpacedArmorSceneComponent({
|
|||||||
surfaceNormal: intersection.face!.normal,
|
surfaceNormal: intersection.face!.normal,
|
||||||
status:
|
status:
|
||||||
shell.type === ShellType.HE && layer.type !== ArmorType.Primary
|
shell.type === ShellType.HE && layer.type !== ArmorType.Primary
|
||||||
? 'blocked'
|
? "blocked"
|
||||||
: blocked
|
: blocked
|
||||||
? 'blocked'
|
? "blocked"
|
||||||
: 'penetration',
|
: "penetration",
|
||||||
angle,
|
angle,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -282,10 +281,10 @@ export function SpacedArmorSceneComponent({
|
|||||||
|
|
||||||
return acc + layer.thicknessAngled;
|
return acc + layer.thicknessAngled;
|
||||||
},
|
},
|
||||||
0,
|
0
|
||||||
);
|
);
|
||||||
const distanceFromSpacedArmor = firstLayer.point.distanceTo(
|
const distanceFromSpacedArmor = firstLayer.point.distanceTo(
|
||||||
lastLayer.point,
|
lastLayer.point
|
||||||
);
|
);
|
||||||
const finalDamage = Math.max(
|
const finalDamage = Math.max(
|
||||||
0,
|
0,
|
||||||
@@ -294,7 +293,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
(1 - distanceFromSpacedArmor / shell.explosion_radius!) -
|
(1 - distanceFromSpacedArmor / shell.explosion_radius!) -
|
||||||
1.1 *
|
1.1 *
|
||||||
(lastLayer.thicknessAngled +
|
(lastLayer.thicknessAngled +
|
||||||
Math.min(penetration, totalSpacedArmorThickness)),
|
Math.min(penetration, totalSpacedArmorThickness))
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -308,19 +307,19 @@ export function SpacedArmorSceneComponent({
|
|||||||
* for parity's sake, I am capping the value here.
|
* for parity's sake, I am capping the value here.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (shot.in.layers.length > 1 || lastLayer.status === 'blocked') {
|
if (shot.in.layers.length > 1 || lastLayer.status === "blocked") {
|
||||||
shot.in.status = finalDamage > 0 ? 'splash' : 'blocked';
|
shot.in.status = finalDamage > 0 ? "splash" : "blocked";
|
||||||
shot.damage = finalDamage;
|
shot.damage = finalDamage;
|
||||||
} else {
|
} else {
|
||||||
shot.in.status = 'penetration';
|
shot.in.status = "penetration";
|
||||||
shot.damage = shell.armor_damage;
|
shot.damage = shell.armor_damage;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (lastLayer.status === 'blocked') {
|
if (lastLayer.status === "blocked") {
|
||||||
shot.in.status = 'blocked';
|
shot.in.status = "blocked";
|
||||||
shot.damage = 0;
|
shot.damage = 0;
|
||||||
} else if (lastLayer.status === 'penetration') {
|
} else if (lastLayer.status === "penetration") {
|
||||||
shot.in.status = 'penetration';
|
shot.in.status = "penetration";
|
||||||
shot.damage = shell.armor_damage;
|
shot.damage = shell.armor_damage;
|
||||||
} else {
|
} else {
|
||||||
const caster = new Raycaster();
|
const caster = new Raycaster();
|
||||||
@@ -331,29 +330,29 @@ export function SpacedArmorSceneComponent({
|
|||||||
.sub(
|
.sub(
|
||||||
lastLayer.surfaceNormal
|
lastLayer.surfaceNormal
|
||||||
.clone()
|
.clone()
|
||||||
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal)),
|
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal))
|
||||||
);
|
);
|
||||||
|
|
||||||
caster.set(lastLayer.point, ricochetNormal);
|
caster.set(lastLayer.point, ricochetNormal);
|
||||||
|
|
||||||
const ricochetIntersections = caster.intersectObjects(
|
const ricochetIntersections = caster.intersectObjects(
|
||||||
props.scene.children,
|
props.scene.children,
|
||||||
true,
|
true
|
||||||
);
|
);
|
||||||
const outShot = await shoot(
|
const outShot = shoot(
|
||||||
lastLayer.point,
|
lastLayer.point,
|
||||||
ricochetIntersections,
|
ricochetIntersections,
|
||||||
false,
|
false,
|
||||||
remainingPenetration,
|
remainingPenetration
|
||||||
);
|
);
|
||||||
|
|
||||||
shot.in.status = 'ricochet';
|
shot.in.status = "ricochet";
|
||||||
if (outShot === null) {
|
if (outShot === null) {
|
||||||
shot.damage = 0;
|
shot.damage = 0;
|
||||||
shot.out = {
|
shot.out = {
|
||||||
surfaceNormal: ricochetNormal,
|
surfaceNormal: ricochetNormal,
|
||||||
layers: [],
|
layers: [],
|
||||||
status: 'ricochet',
|
status: "ricochet",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
shot.damage = outShot.damage;
|
shot.damage = outShot.damage;
|
||||||
@@ -366,7 +365,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
|
|
||||||
return shot;
|
return shot;
|
||||||
},
|
},
|
||||||
[camera],
|
[camera]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -386,14 +385,14 @@ export function SpacedArmorSceneComponent({
|
|||||||
thickness,
|
thickness,
|
||||||
} satisfies ArmorUserData
|
} satisfies ArmorUserData
|
||||||
}
|
}
|
||||||
onClick={async (event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const shot = (await shoot(
|
|
||||||
event.point,
|
const shot = shoot(event.point, event.intersections, true)!;
|
||||||
event.intersections,
|
|
||||||
true,
|
Tankopedia.mutate((draft) => {
|
||||||
))!;
|
draft.shot = shot;
|
||||||
tankopediaEphemeralStore.setState({ shot });
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
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';
|
import { applyPitchYawLimits } from "../../../core/blitz/applyPitchYawLimits";
|
||||||
import { applyPitchYawLimits } from '../../../core/blitz/applyPitchYawLimits';
|
import { correctZYTuple } from "../../../core/blitz/correctZYTuple";
|
||||||
import { correctZYTuple } from '../../../core/blitz/correctZYTuple';
|
import { hasEquipment } from "../../../core/blitzkit/hasEquipment";
|
||||||
import { hasEquipment } from '../../../core/blitzkit/hasEquipment';
|
import { modelTransformEvent } from "../../../core/blitzkit/modelTransform";
|
||||||
import { modelTransformEvent } from '../../../core/blitzkit/modelTransform';
|
import { nameToArmorId } from "../../../core/blitzkit/nameToArmorId";
|
||||||
import { nameToArmorId } from '../../../core/blitzkit/nameToArmorId';
|
import { resolveArmor } from "../../../core/blitzkit/resolveThickness";
|
||||||
import { resolveArmor } from '../../../core/blitzkit/resolveThickness';
|
import { useArmor } from "../../../hooks/useArmor";
|
||||||
import { useArmor } from '../../../hooks/useArmor';
|
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 { Tankopedia } from "../../../stores/tankopedia";
|
||||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
import { TankopediaPersistent } from "../../../stores/tankopediaPersistent";
|
||||||
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
import { ModelTankWrapper } from "./ModelTankWrapper";
|
||||||
import { ModelTankWrapper } from './ModelTankWrapper';
|
import { ArmorType } from "./SpacedArmorScene";
|
||||||
import { ArmorType } from './SpacedArmorScene';
|
import { StaticArmorSceneComponent } from "./StaticArmorSceneComponent";
|
||||||
import { StaticArmorSceneComponent } from './StaticArmorSceneComponent';
|
|
||||||
|
|
||||||
interface ArmorSceneProps {
|
interface ArmorSceneProps {
|
||||||
thicknessRange: ThicknessRange;
|
thicknessRange: ThicknessRange;
|
||||||
@@ -31,7 +30,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
const wrapper = useRef<Group>(null);
|
const wrapper = useRef<Group>(null);
|
||||||
const turretContainer = useRef<Group>(null);
|
const turretContainer = useRef<Group>(null);
|
||||||
const gunContainer = useRef<Group>(null);
|
const gunContainer = useRef<Group>(null);
|
||||||
const protagonist = Duel.use((draft) => draft.protagonist);
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const track = Duel.use((state) => state.protagonist.track);
|
const track = Duel.use((state) => state.protagonist.track);
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
@@ -43,30 +41,27 @@ 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);
|
||||||
const mutateDuel = Duel.useMutation();
|
|
||||||
const canvas = useThree((state) => state.gl.domElement);
|
const canvas = useThree((state) => state.gl.domElement);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const maskOrigin =
|
const maskOrigin =
|
||||||
gunModelDefinition.mask === undefined
|
gunModelDefinition.mask === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
|
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
|
||||||
const showPrimaryArmor = TankopediaPersistent.use(
|
const showPrimaryArmor = TankopediaPersistent.use(
|
||||||
(state) => state.showPrimaryArmor,
|
(state) => state.showPrimaryArmor
|
||||||
);
|
);
|
||||||
const showSpacedArmor = TankopediaPersistent.use(
|
const showSpacedArmor = TankopediaPersistent.use(
|
||||||
(state) => state.showSpacedArmor,
|
(state) => state.showSpacedArmor
|
||||||
);
|
);
|
||||||
const showExternalModules = TankopediaPersistent.use(
|
const showExternalModules = TankopediaPersistent.use(
|
||||||
(state) => state.showExternalModules,
|
(state) => state.showExternalModules
|
||||||
);
|
);
|
||||||
const isDynamicArmorActive = Duel.use((state) =>
|
const isDynamicArmorActive = Duel.use((state) =>
|
||||||
state.protagonist.consumables.includes(73),
|
state.protagonist.consumables.includes(73)
|
||||||
);
|
);
|
||||||
|
|
||||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||||
@@ -75,12 +70,12 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
<ModelTankWrapper ref={wrapper}>
|
<ModelTankWrapper ref={wrapper}>
|
||||||
<group position={hullOrigin}>
|
<group position={hullOrigin}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isHull = node.name.startsWith('hull_');
|
const isHull = node.name.startsWith("hull_");
|
||||||
const isVisible = isHull;
|
const isVisible = isHull;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
tankModelDefinition.armor,
|
tankModelDefinition.armor,
|
||||||
armorId,
|
armorId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -88,8 +83,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
thickness === undefined ||
|
thickness === undefined ||
|
||||||
(!showPrimaryArmor && !spaced) ||
|
(!showPrimaryArmor && !spaced) ||
|
||||||
(!showSpacedArmor && spaced) ||
|
(!showSpacedArmor && spaced) ||
|
||||||
(isDynamicArmorActive && node.name.includes('state_00')) ||
|
(isDynamicArmorActive && node.name.includes("state_00")) ||
|
||||||
(!isDynamicArmorActive && node.name.includes('state_01'))
|
(!isDynamicArmorActive && node.name.includes("state_01"))
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -108,8 +103,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
</group>
|
</group>
|
||||||
|
|
||||||
{modelNodes.map((node) => {
|
{modelNodes.map((node) => {
|
||||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
const isWheel = node.name.startsWith("chassis_wheel_");
|
||||||
const isTrack = node.name.startsWith('chassis_track_');
|
const isTrack = node.name.startsWith("chassis_track_");
|
||||||
const isVisible = isWheel || isTrack;
|
const isVisible = isWheel || isTrack;
|
||||||
|
|
||||||
if (!isVisible || !showExternalModules) return null;
|
if (!isVisible || !showExternalModules) return null;
|
||||||
@@ -131,13 +126,13 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
<group ref={turretContainer}>
|
<group ref={turretContainer}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isCurrentTurret = node.name.startsWith(
|
const isCurrentTurret = node.name.startsWith(
|
||||||
`turret_${turretModelDefinition.model_id.toString().padStart(2, '0')}`,
|
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`
|
||||||
);
|
);
|
||||||
const isVisible = isCurrentTurret;
|
const isVisible = isCurrentTurret;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
turretModelDefinition.armor,
|
turretModelDefinition.armor,
|
||||||
armorId,
|
armorId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -145,71 +140,64 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
thickness === undefined ||
|
thickness === undefined ||
|
||||||
(!showPrimaryArmor && !spaced) ||
|
(!showPrimaryArmor && !spaced) ||
|
||||||
(!showSpacedArmor && spaced) ||
|
(!showSpacedArmor && spaced) ||
|
||||||
(isDynamicArmorActive && node.name.includes('state_00')) ||
|
(isDynamicArmorActive && node.name.includes("state_00")) ||
|
||||||
(!isDynamicArmorActive && node.name.includes('state_01'))
|
(!isDynamicArmorActive && node.name.includes("state_01"))
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
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) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener("pointermove", handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
async function handlePointerMove(event: PointerEvent) {
|
function handlePointerMove(event: PointerEvent) {
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||||
122,
|
122,
|
||||||
duel.protagonist.tank.equipment_preset,
|
duel.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
duel.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||||
124,
|
124,
|
||||||
duel.protagonist.tank.equipment_preset,
|
duel.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
duel.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const boundingRect = canvas.getBoundingClientRect();
|
const boundingRect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
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) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.protagonist.pitch = normalizeAngleRad(pitch);
|
|
||||||
draft.protagonist.yaw = normalizeAngleRad(yaw);
|
|
||||||
});
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener("pointermove", handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -218,7 +206,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
<StaticArmorSceneComponent
|
<StaticArmorSceneComponent
|
||||||
name={node.name}
|
name={node.name}
|
||||||
thicknessRange={thicknessRange}
|
thicknessRange={thicknessRange}
|
||||||
key={node.uuid}
|
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
node={node}
|
node={node}
|
||||||
@@ -232,13 +219,13 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
<group ref={gunContainer}>
|
<group ref={gunContainer}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isCurrentGun = node.name.startsWith(
|
const isCurrentGun = node.name.startsWith(
|
||||||
`gun_${gunModelDefinition.model_id.toString().padStart(2, '0')}`,
|
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`
|
||||||
);
|
);
|
||||||
const isVisible = isCurrentGun;
|
const isVisible = isCurrentGun;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
gunModelDefinition.armor,
|
gunModelDefinition.armor,
|
||||||
armorId,
|
armorId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -246,70 +233,64 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
thickness === undefined ||
|
thickness === undefined ||
|
||||||
(!showPrimaryArmor && !spaced) ||
|
(!showPrimaryArmor && !spaced) ||
|
||||||
(!showSpacedArmor && spaced) ||
|
(!showSpacedArmor && spaced) ||
|
||||||
(isDynamicArmorActive && node.name.includes('state_00')) ||
|
(isDynamicArmorActive && node.name.includes("state_00")) ||
|
||||||
(!isDynamicArmorActive && node.name.includes('state_01'))
|
(!isDynamicArmorActive && node.name.includes("state_01"))
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
async function handlePointerMove(event: PointerEvent) {
|
function handlePointerMove(event: PointerEvent) {
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||||
122,
|
122,
|
||||||
duel.protagonist.tank.equipment_preset,
|
duel.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
duel.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||||
124,
|
124,
|
||||||
duel.protagonist.tank.equipment_preset,
|
duel.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
duel.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const boundingRect = canvas.getBoundingClientRect();
|
const boundingRect = canvas.getBoundingClientRect();
|
||||||
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) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
mutateDuel((draft) => {
|
window.removeEventListener("pointermove", handlePointerMove);
|
||||||
draft.protagonist.pitch = normalizeAngleRad(pitch);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
draft.protagonist.yaw = normalizeAngleRad(yaw);
|
|
||||||
});
|
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -331,7 +312,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
{modelNodes.map((node) => {
|
{modelNodes.map((node) => {
|
||||||
const gunString = `gun_${gunModelDefinition.model_id
|
const gunString = `gun_${gunModelDefinition.model_id
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(2, '0')}`;
|
.padStart(2, "0")}`;
|
||||||
const isCurrentGun = gunModelDefinition.mask
|
const isCurrentGun = gunModelDefinition.mask
|
||||||
? node.name.startsWith(gunString)
|
? node.name.startsWith(gunString)
|
||||||
: node.name === gunString;
|
: node.name === gunString;
|
||||||
@@ -341,63 +322,57 @@ 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();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
pitch = protagonist.pitch;
|
window.addEventListener("pointermove", handlePointerMove);
|
||||||
yaw = protagonist.yaw;
|
window.addEventListener("pointerup", handlePointerUp);
|
||||||
|
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
|
||||||
}
|
}
|
||||||
async function handlePointerMove(event: PointerEvent) {
|
function handlePointerMove(event: PointerEvent) {
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||||
122,
|
122,
|
||||||
duel.protagonist.tank.equipment_preset,
|
duel.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
duel.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||||
124,
|
124,
|
||||||
duel.protagonist.tank.equipment_preset,
|
duel.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
duel.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const boundingRect = canvas.getBoundingClientRect();
|
const boundingRect = canvas.getBoundingClientRect();
|
||||||
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) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
mutateDuel((draft) => {
|
|
||||||
draft.protagonist.pitch = normalizeAngleRad(pitch);
|
window.removeEventListener("pointermove", handlePointerMove);
|
||||||
draft.protagonist.yaw = normalizeAngleRad(yaw);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
});
|
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
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 { useEffect, useMemo } from 'react';
|
import type { QuicklimeEvent } from "quicklime";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Box3,
|
Box3,
|
||||||
Color,
|
Color,
|
||||||
@@ -9,33 +10,33 @@ import {
|
|||||||
FrontSide,
|
FrontSide,
|
||||||
LineBasicMaterial,
|
LineBasicMaterial,
|
||||||
Mesh,
|
Mesh,
|
||||||
MeshStandardMaterial,
|
MeshBasicMaterial,
|
||||||
Object3D,
|
Object3D,
|
||||||
Plane,
|
Plane,
|
||||||
Quaternion,
|
Quaternion,
|
||||||
Vector3,
|
Vector3,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import { unrotateDavaVector } from '../../../core/blitz/unrotateDavaVector';
|
import { unrotateDavaVector } from "../../../core/blitz/unrotateDavaVector";
|
||||||
import { jsxTree } from '../../../core/blitzkit/jsxTree';
|
import { jsxTree } from "../../../core/blitzkit/jsxTree";
|
||||||
import {
|
import {
|
||||||
type ModelTransformEventData,
|
type ModelTransformEventData,
|
||||||
modelTransformEvent,
|
modelTransformEvent,
|
||||||
} from '../../../core/blitzkit/modelTransform';
|
} from "../../../core/blitzkit/modelTransform";
|
||||||
import { discardClippingPlane } from '../../../core/three/discardClippingPlane';
|
import { discardClippingPlane } from "../../../core/three/discardClippingPlane";
|
||||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../stores/tankopedia";
|
||||||
import { ArmorType } from './SpacedArmorScene';
|
import { ArmorType } from "./SpacedArmorScene";
|
||||||
import type {
|
import type {
|
||||||
ArmorUserData,
|
ArmorUserData,
|
||||||
ExternalModuleVariant,
|
ExternalModuleVariant,
|
||||||
} from './SpacedArmorSceneComponent';
|
} from "./SpacedArmorSceneComponent";
|
||||||
import type { ThicknessRange } from './StaticArmor';
|
import type { ThicknessRange } from "./StaticArmor";
|
||||||
|
|
||||||
type StaticArmorSceneComponentProps = {
|
type StaticArmorSceneComponentProps = {
|
||||||
name: string;
|
name: string;
|
||||||
thickness: number;
|
thickness: number;
|
||||||
thicknessRange: ThicknessRange;
|
thicknessRange: ThicknessRange;
|
||||||
node: Object3D;
|
node: Object3D;
|
||||||
onPointerDown?: MeshProps['onPointerDown'];
|
onPointerDown?: MeshProps["onPointerDown"];
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
type: Exclude<ArmorType, ArmorType.External>;
|
type: Exclude<ArmorType, ArmorType.External>;
|
||||||
@@ -68,8 +69,6 @@ export function StaticArmorSceneComponent({
|
|||||||
onPointerDown,
|
onPointerDown,
|
||||||
...props
|
...props
|
||||||
}: StaticArmorSceneComponentProps) {
|
}: StaticArmorSceneComponentProps) {
|
||||||
const mutateTankopediaEphemeralStore = TankopediaEphemeral.useMutation();
|
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
const x = thickness / thicknessRange.value;
|
const x = thickness / thicknessRange.value;
|
||||||
const xClamped = clamp(x, 0, 1);
|
const xClamped = clamp(x, 0, 1);
|
||||||
@@ -91,7 +90,7 @@ export function StaticArmorSceneComponent({
|
|||||||
color = new Color(
|
color = new Color(
|
||||||
clamp(1 - (7 / 8) * xClamped, 0, 1),
|
clamp(1 - (7 / 8) * xClamped, 0, 1),
|
||||||
0,
|
0,
|
||||||
clamp(1 - (1 / 8) * xClamped, 0, 1),
|
clamp(1 - (1 / 8) * xClamped, 0, 1)
|
||||||
);
|
);
|
||||||
opacity = clamp(x + 1 / 2, 0, 1);
|
opacity = clamp(x + 1 / 2, 0, 1);
|
||||||
break;
|
break;
|
||||||
@@ -107,14 +106,14 @@ export function StaticArmorSceneComponent({
|
|||||||
|
|
||||||
const surfaceMaterial = useMemo(
|
const surfaceMaterial = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new MeshStandardMaterial({
|
new MeshBasicMaterial({
|
||||||
color,
|
color,
|
||||||
opacity,
|
opacity,
|
||||||
transparent: opacity < 1,
|
transparent: opacity < 1,
|
||||||
depthWrite,
|
depthWrite,
|
||||||
...(props.clip ? { clippingPlanes: [props.clip] } : {}),
|
...(props.clip ? { clippingPlanes: [props.clip] } : {}),
|
||||||
}),
|
}),
|
||||||
[thickness],
|
[thickness]
|
||||||
);
|
);
|
||||||
const outlineMaterial = useMemo(
|
const outlineMaterial = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -123,7 +122,7 @@ export function StaticArmorSceneComponent({
|
|||||||
.clone()
|
.clone()
|
||||||
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
|
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
|
||||||
}),
|
}),
|
||||||
[thickness],
|
[thickness]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -138,11 +137,11 @@ export function StaticArmorSceneComponent({
|
|||||||
outlineMaterial.visible = true;
|
outlineMaterial.visible = true;
|
||||||
} else if (
|
} else if (
|
||||||
selectedName === name ||
|
selectedName === name ||
|
||||||
(name.startsWith('chassis_') && selectedName.startsWith('chassis_')) ||
|
(name.startsWith("chassis_") && selectedName.startsWith("chassis_")) ||
|
||||||
(name.startsWith('gun_') &&
|
(name.startsWith("gun_") &&
|
||||||
selectedName.startsWith('gun_') &&
|
selectedName.startsWith("gun_") &&
|
||||||
!name.includes('_armor_') &&
|
!name.includes("_armor_") &&
|
||||||
!selectedName.includes('_armor_'))
|
!selectedName.includes("_armor_"))
|
||||||
) {
|
) {
|
||||||
// this selected, stand out!
|
// this selected, stand out!
|
||||||
surfaceMaterial.opacity = 1;
|
surfaceMaterial.opacity = 1;
|
||||||
@@ -165,9 +164,9 @@ export function StaticArmorSceneComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
tankopediaEphemeralStore.subscribe(
|
Tankopedia.on(
|
||||||
(state) => state.highlightArmor?.name,
|
(state) => state.highlightArmor?.name,
|
||||||
handleHighlightArmor,
|
handleHighlightArmor
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -180,22 +179,24 @@ export function StaticArmorSceneComponent({
|
|||||||
const { clip } = props;
|
const { clip } = props;
|
||||||
const gunOrigin = unrotateDavaVector(props.gunOrigin.clone());
|
const gunOrigin = unrotateDavaVector(props.gunOrigin.clone());
|
||||||
const neckOrigin = unrotateDavaVector(props.hullOrigin.clone()).add(
|
const neckOrigin = unrotateDavaVector(props.hullOrigin.clone()).add(
|
||||||
unrotateDavaVector(props.turretOrigin.clone()),
|
unrotateDavaVector(props.turretOrigin.clone())
|
||||||
);
|
);
|
||||||
const barrelOrigin = neckOrigin.clone().add(gunOrigin);
|
const barrelOrigin = neckOrigin.clone().add(gunOrigin);
|
||||||
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();
|
||||||
@@ -226,7 +227,7 @@ export function StaticArmorSceneComponent({
|
|||||||
{
|
{
|
||||||
type: props.type,
|
type: props.type,
|
||||||
variant:
|
variant:
|
||||||
props.type === ArmorType.External ? props.variant : 'gun',
|
props.type === ArmorType.External ? props.variant : "gun",
|
||||||
thickness,
|
thickness,
|
||||||
} satisfies ArmorUserData
|
} satisfies ArmorUserData
|
||||||
}
|
}
|
||||||
@@ -235,7 +236,7 @@ export function StaticArmorSceneComponent({
|
|||||||
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const { editStatic } = tankopediaEphemeralStore.getState();
|
const { editStatic } = Tankopedia.state;
|
||||||
|
|
||||||
const bounds = new Box3().setFromObject(event.object);
|
const bounds = new Box3().setFromObject(event.object);
|
||||||
const point = bounds.min
|
const point = bounds.min
|
||||||
@@ -250,13 +251,13 @@ export function StaticArmorSceneComponent({
|
|||||||
const surfaceNormal = event
|
const surfaceNormal = event
|
||||||
.normal!.clone()
|
.normal!.clone()
|
||||||
.applyQuaternion(
|
.applyQuaternion(
|
||||||
event.object.getWorldQuaternion(new Quaternion()),
|
event.object.getWorldQuaternion(new Quaternion())
|
||||||
);
|
);
|
||||||
const angle = surfaceNormal.angleTo(cameraNormal);
|
const angle = surfaceNormal.angleTo(cameraNormal);
|
||||||
const thicknessAngled =
|
const thicknessAngled =
|
||||||
thickness / Math.sin(Math.PI / 2 - angle);
|
thickness / Math.sin(Math.PI / 2 - angle);
|
||||||
|
|
||||||
mutateTankopediaEphemeralStore((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.highlightArmor = {
|
draft.highlightArmor = {
|
||||||
type: props.type,
|
type: props.type,
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { asset } from '@blitzkit/core';
|
import { asset } from "@blitzkit/core";
|
||||||
import { Flex, Heading, IconButton, Table } from '@radix-ui/themes';
|
import { Flex, Heading, IconButton, Table } from "@radix-ui/themes";
|
||||||
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 { CompareEphemeral } from '../../stores/compareEphemeral';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
||||||
import { CompareRow } from './CompareRow';
|
import { CompareRow } from "./CompareRow";
|
||||||
import { CompareSectionTitle } from './CompareSectionTitle';
|
import { CompareSectionTitle } from "./CompareSectionTitle";
|
||||||
|
|
||||||
interface BodyProps {
|
interface BodyProps {
|
||||||
stats: TankCharacteristics[];
|
stats: TankCharacteristics[];
|
||||||
@@ -14,10 +14,9 @@ interface BodyProps {
|
|||||||
export function Body({ stats }: BodyProps) {
|
export function Body({ stats }: BodyProps) {
|
||||||
const members = CompareEphemeral.use((state) => state.members);
|
const members = CompareEphemeral.use((state) => state.members);
|
||||||
const hasNonRegularGun = members.some(
|
const hasNonRegularGun = members.some(
|
||||||
({ gun }) => gun.gun_type!.$case !== 'regular',
|
({ gun }) => gun.gun_type!.$case !== "regular"
|
||||||
);
|
);
|
||||||
const { unwrap, strings } = useLocale();
|
const { unwrap, strings } = useLocale();
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -33,43 +32,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 ===
|
CompareEphemeral.mutate((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>
|
||||||
@@ -96,7 +87,7 @@ export function Body({ stats }: BodyProps) {
|
|||||||
}
|
}
|
||||||
display={(stats) =>
|
display={(stats) =>
|
||||||
stats.shellReload?.toFixed(2) ??
|
stats.shellReload?.toFixed(2) ??
|
||||||
stats.shellReloads!.map((reload) => reload.toFixed(2)).join(', ')
|
stats.shellReloads!.map((reload) => reload.toFixed(2)).join(", ")
|
||||||
}
|
}
|
||||||
deltaNominalDisplay={(delta) => delta.toFixed(2)}
|
deltaNominalDisplay={(delta) => delta.toFixed(2)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,21 +2,21 @@ import {
|
|||||||
CaretLeftIcon,
|
CaretLeftIcon,
|
||||||
CaretRightIcon,
|
CaretRightIcon,
|
||||||
CaretSortIcon,
|
CaretSortIcon,
|
||||||
} from '@radix-ui/react-icons';
|
} from "@radix-ui/react-icons";
|
||||||
import { Flex, IconButton, Table, Text } from '@radix-ui/themes';
|
import { Flex, IconButton, Table, Text } from "@radix-ui/themes";
|
||||||
import { useRef } from 'react';
|
import { useRef } from "react";
|
||||||
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 { CompareEphemeral } from '../../stores/compareEphemeral';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import { ComparePersistent } from '../../stores/comparePersistent';
|
import { ComparePersistent } from "../../stores/comparePersistent";
|
||||||
import { StickyRowHeaderCell } from '../StickyRowHeaderCell';
|
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
|
||||||
|
|
||||||
type CompareRowProps = {
|
type CompareRowProps = {
|
||||||
display?: (
|
display?: (
|
||||||
member: Awaited<TankCharacteristics>,
|
member: Awaited<TankCharacteristics>
|
||||||
) => number | string | undefined;
|
) => number | string | undefined;
|
||||||
deltaType?: 'higherIsBetter' | 'lowerIsBetter';
|
deltaType?: "higherIsBetter" | "lowerIsBetter";
|
||||||
decimals?: number;
|
decimals?: number;
|
||||||
deltaNominalDisplay?: (delta: number) => string | number;
|
deltaNominalDisplay?: (delta: number) => string | number;
|
||||||
indent?: boolean;
|
indent?: boolean;
|
||||||
@@ -33,7 +33,7 @@ type CompareRowProps = {
|
|||||||
|
|
||||||
export function CompareRow({
|
export function CompareRow({
|
||||||
display,
|
display,
|
||||||
deltaType = 'higherIsBetter',
|
deltaType = "higherIsBetter",
|
||||||
decimals,
|
decimals,
|
||||||
deltaNominalDisplay,
|
deltaNominalDisplay,
|
||||||
indent,
|
indent,
|
||||||
@@ -41,17 +41,16 @@ export function CompareRow({
|
|||||||
...props
|
...props
|
||||||
}: CompareRowProps) {
|
}: CompareRowProps) {
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
const sorting = CompareEphemeral.use((state) => state.sorting);
|
const sorting = CompareEphemeral.use((state) => state.sorting);
|
||||||
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
||||||
const values = stats.map((stat) =>
|
const values = stats.map((stat) =>
|
||||||
typeof props.value === 'function'
|
typeof props.value === "function"
|
||||||
? props.value(stat)!
|
? props.value(stat)!
|
||||||
: (stat[props.value] as number),
|
: (stat[props.value] as number)
|
||||||
);
|
);
|
||||||
const id = useRef(Math.random());
|
const id = useRef(Math.random());
|
||||||
const name =
|
const name =
|
||||||
typeof props.value === 'string'
|
typeof props.value === "string"
|
||||||
? strings.website.tools.tankopedia.characteristics.values[props.value]
|
? strings.website.tools.tankopedia.characteristics.values[props.value]
|
||||||
: props.name;
|
: props.name;
|
||||||
|
|
||||||
@@ -60,21 +59,21 @@ export function CompareRow({
|
|||||||
<StickyRowHeaderCell>
|
<StickyRowHeaderCell>
|
||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
style={{ whiteSpace: 'nowrap' }}
|
style={{ whiteSpace: "nowrap" }}
|
||||||
gap={indent ? { initial: '4', sm: '6' } : { initial: '1', sm: '2' }}
|
gap={indent ? { initial: "4", sm: "6" } : { initial: "1", sm: "2" }}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
color={sorting?.by === id.current ? undefined : 'gray'}
|
color={sorting?.by === id.current ? undefined : "gray"}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members.sort((memberA, memberB) => {
|
draft.members.sort((memberA, memberB) => {
|
||||||
const indexA = draft.members.indexOf(memberA);
|
const indexA = draft.members.indexOf(memberA);
|
||||||
const indexB = draft.members.indexOf(memberB);
|
const indexB = draft.members.indexOf(memberB);
|
||||||
const valueA = values[indexA];
|
const valueA = values[indexA];
|
||||||
const valueB = values[indexB];
|
const valueB = values[indexB];
|
||||||
|
|
||||||
return draft.sorting?.direction === 'ascending' &&
|
return draft.sorting?.direction === "ascending" &&
|
||||||
draft.sorting.by === id.current
|
draft.sorting.by === id.current
|
||||||
? valueA - valueB
|
? valueA - valueB
|
||||||
: valueB - valueA;
|
: valueB - valueA;
|
||||||
@@ -83,26 +82,26 @@ export function CompareRow({
|
|||||||
draft.sorting = {
|
draft.sorting = {
|
||||||
by: id.current,
|
by: id.current,
|
||||||
direction:
|
direction:
|
||||||
draft.sorting?.direction === 'ascending' &&
|
draft.sorting?.direction === "ascending" &&
|
||||||
draft.sorting.by === id.current
|
draft.sorting.by === id.current
|
||||||
? 'descending'
|
? "descending"
|
||||||
: 'ascending',
|
: "ascending",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(sorting === undefined || sorting.by !== id.current) && (
|
{(sorting === undefined || sorting.by !== id.current) && (
|
||||||
<CaretSortIcon style={{ transform: 'rotate(90deg)' }} />
|
<CaretSortIcon style={{ transform: "rotate(90deg)" }} />
|
||||||
)}
|
)}
|
||||||
{sorting?.by === id.current && (
|
{sorting?.by === id.current && (
|
||||||
<>
|
<>
|
||||||
{sorting.direction === 'ascending' && <CaretRightIcon />}
|
{sorting.direction === "ascending" && <CaretRightIcon />}
|
||||||
{sorting.direction === 'descending' && <CaretLeftIcon />}
|
{sorting.direction === "descending" && <CaretLeftIcon />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Text size={{ initial: '1', sm: '2' }}>{name}</Text>
|
<Text size={{ initial: "1", sm: "2" }}>{name}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</StickyRowHeaderCell>
|
</StickyRowHeaderCell>
|
||||||
|
|
||||||
@@ -110,7 +109,7 @@ export function CompareRow({
|
|||||||
const delta = value - values[0];
|
const delta = value - values[0];
|
||||||
const deltaPercentage = value / values[0] - 1;
|
const deltaPercentage = value / values[0] - 1;
|
||||||
const normalizedDeltaPercentage = Math.round(
|
const normalizedDeltaPercentage = Math.round(
|
||||||
Math.min(100, Math.abs(deltaPercentage) * 2 * 100 + 25),
|
Math.min(100, Math.abs(deltaPercentage) * 2 * 100 + 25)
|
||||||
);
|
);
|
||||||
const resolvedDisplayValue = display
|
const resolvedDisplayValue = display
|
||||||
? display(stats[index])
|
? display(stats[index])
|
||||||
@@ -129,12 +128,12 @@ export function CompareRow({
|
|||||||
value === values[0]
|
value === values[0]
|
||||||
? undefined
|
? undefined
|
||||||
: (
|
: (
|
||||||
deltaType === 'higherIsBetter'
|
deltaType === "higherIsBetter"
|
||||||
? value > values[0]
|
? value > values[0]
|
||||||
: value < values[0]
|
: value < values[0]
|
||||||
)
|
)
|
||||||
? `color-mix(in srgb, ${Var('green-7')} ${normalizedDeltaPercentage}%, ${Var('green-3')})`
|
? `color-mix(in srgb, ${Var("green-7")} ${normalizedDeltaPercentage}%, ${Var("green-3")})`
|
||||||
: `color-mix(in srgb, ${Var('red-7')} ${normalizedDeltaPercentage}%, ${Var('red-3')})`,
|
: `color-mix(in srgb, ${Var("red-7")} ${normalizedDeltaPercentage}%, ${Var("red-3")})`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
@@ -142,11 +141,11 @@ export function CompareRow({
|
|||||||
justify="center"
|
justify="center"
|
||||||
gap="2"
|
gap="2"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ textAlign: 'center' }} wrap="nowrap">
|
<Text style={{ textAlign: "center" }} wrap="nowrap">
|
||||||
{resolvedDisplayValue}
|
{resolvedDisplayValue}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@@ -154,10 +153,10 @@ export function CompareRow({
|
|||||||
resolvedDisplayValue !== undefined &&
|
resolvedDisplayValue !== undefined &&
|
||||||
values[0] !== undefined && (
|
values[0] !== undefined && (
|
||||||
<>
|
<>
|
||||||
{deltaMode === 'absolute' && (
|
{deltaMode === "absolute" && (
|
||||||
<Text color="gray">
|
<Text color="gray">
|
||||||
(
|
(
|
||||||
{`${delta > 0 ? '+' : ''}${
|
{`${delta > 0 ? "+" : ""}${
|
||||||
deltaNominalDisplay
|
deltaNominalDisplay
|
||||||
? deltaNominalDisplay(delta)
|
? deltaNominalDisplay(delta)
|
||||||
: decimals === undefined
|
: decimals === undefined
|
||||||
@@ -168,11 +167,11 @@ export function CompareRow({
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{deltaMode === 'percentage' && (
|
{deltaMode === "percentage" && (
|
||||||
<Text color="gray">
|
<Text color="gray">
|
||||||
(
|
(
|
||||||
{`${deltaPercentage > 0 ? '+' : ''}${Math.round(
|
{`${deltaPercentage > 0 ? "+" : ""}${Math.round(
|
||||||
deltaPercentage * 100,
|
deltaPercentage * 100
|
||||||
)}%`}
|
)}%`}
|
||||||
)
|
)
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import { asset, availableProvisions, permanentSkills } from '@blitzkit/core';
|
import { asset, availableProvisions, permanentSkills } from "@blitzkit/core";
|
||||||
import { checkConsumableProvisionInclusivity } from '@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity';
|
import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
|
||||||
import { ComponentPlaceholderIcon } from '@radix-ui/react-icons';
|
import { ComponentPlaceholderIcon } from "@radix-ui/react-icons";
|
||||||
import { Button, Flex, IconButton, Popover, Table } from '@radix-ui/themes';
|
import { Button, Flex, IconButton, Popover, Table } from "@radix-ui/themes";
|
||||||
import { times } from 'lodash-es';
|
import { times } from "lodash-es";
|
||||||
import { awaitableConsumableDefinitions } from '../../core/awaitables/consumableDefinitions';
|
import { awaitableConsumableDefinitions } from "../../core/awaitables/consumableDefinitions";
|
||||||
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 { awaitableSkillDefinitions } from '../../core/awaitables/skillDefinitions';
|
import { awaitableSkillDefinitions } from "../../core/awaitables/skillDefinitions";
|
||||||
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 { CompareEphemeral } from '../../stores/compareEphemeral';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import type { EquipmentMatrix } from '../../stores/duel';
|
import type { EquipmentMatrix } from "../../stores/duel";
|
||||||
import { BlitzkitButtonGrayIcon } from '../BlitzkitButtonGrayIcon';
|
import { BlitzkitButtonGrayIcon } from "../BlitzkitButtonGrayIcon";
|
||||||
import { ConsumablesManager } from '../ConsumablesManager';
|
import { ConsumablesManager } from "../ConsumablesManager";
|
||||||
import { CrewSkillManager } from '../CrewSkillManager';
|
import { CrewSkillManager } from "../CrewSkillManager";
|
||||||
import { EquipmentManager } from '../EquipmentManager';
|
import { EquipmentManager } from "../EquipmentManager";
|
||||||
import { ModuleManager } from '../ModuleManager';
|
import { ModuleManager } from "../ModuleManager";
|
||||||
import { ProvisionsManager } from '../ProvisionsManager';
|
import { ProvisionsManager } from "../ProvisionsManager";
|
||||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
||||||
import { StickyTableRoot } from '../StickyTableRoot';
|
import { StickyTableRoot } from "../StickyTableRoot";
|
||||||
import { Body } from './Body';
|
import { Body } from "./Body";
|
||||||
import { InsertionMarker } from './IntersectionMarker';
|
import { InsertionMarker } from "./IntersectionMarker";
|
||||||
import { TankCard } from './TankCard';
|
import { TankCard } from "./TankCard";
|
||||||
|
|
||||||
interface CompareTableProps {
|
interface CompareTableProps {
|
||||||
stats: TankCharacteristics[];
|
stats: TankCharacteristics[];
|
||||||
@@ -43,7 +43,6 @@ const [
|
|||||||
export function CompareTable({ stats }: CompareTableProps) {
|
export function CompareTable({ stats }: CompareTableProps) {
|
||||||
const crewSkills = CompareEphemeral.use((state) => state.crewSkills);
|
const crewSkills = CompareEphemeral.use((state) => state.crewSkills);
|
||||||
const members = CompareEphemeral.use((state) => state.members);
|
const members = CompareEphemeral.use((state) => state.members);
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
const { unwrap, strings } = useLocale();
|
const { unwrap, strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,20 +50,20 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
size="1"
|
size="1"
|
||||||
variant="surface"
|
variant="surface"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
maxWidth: '100%',
|
maxWidth: "100%",
|
||||||
maxHeight: '100%',
|
maxHeight: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
// height: 'calc(100vh - 128px)'
|
// height: 'calc(100vh - 128px)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<StickyColumnHeaderCell width="0" style={{ height: '100%' }}>
|
<StickyColumnHeaderCell width="0" style={{ height: "100%" }}>
|
||||||
<Flex
|
<Flex
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
align="center"
|
align="center"
|
||||||
justify="center"
|
justify="center"
|
||||||
@@ -82,11 +81,11 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
|
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Cell style={{ height: '100%' }}>
|
<Table.Cell style={{ height: "100%" }}>
|
||||||
<Flex
|
<Flex
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
align="center"
|
align="center"
|
||||||
justify="center"
|
justify="center"
|
||||||
@@ -107,15 +106,15 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
crewSkills[skill] === 0
|
crewSkills[skill] === 0
|
||||||
? Var('gray-11')
|
? Var("gray-11")
|
||||||
: permanentSkills.includes(skill)
|
: permanentSkills.includes(skill)
|
||||||
? Var('crimson-11')
|
? Var("crimson-11")
|
||||||
: Var('amber-11'),
|
: Var("amber-11"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
),
|
)
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -125,7 +124,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<CrewSkillManager
|
<CrewSkillManager
|
||||||
skillLevels={crewSkills}
|
skillLevels={crewSkills}
|
||||||
onChange={(skills) => {
|
onChange={(skills) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.crewSkills = skills;
|
draft.crewSkills = skills;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -135,7 +134,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
Object.keys(draft.crewSkills).forEach((skill) => {
|
Object.keys(draft.crewSkills).forEach((skill) => {
|
||||||
draft.crewSkills[skill] = 0;
|
draft.crewSkills[skill] = 0;
|
||||||
});
|
});
|
||||||
@@ -147,7 +146,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
Object.keys(draft.crewSkills).forEach((skill) => {
|
Object.keys(draft.crewSkills).forEach((skill) => {
|
||||||
draft.crewSkills[skill] = 7;
|
draft.crewSkills[skill] = 7;
|
||||||
});
|
});
|
||||||
@@ -177,19 +176,19 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
consumables,
|
consumables,
|
||||||
camouflage,
|
camouflage,
|
||||||
},
|
},
|
||||||
index,
|
index
|
||||||
) => {
|
) => {
|
||||||
const equipmentPreset =
|
const equipmentPreset =
|
||||||
equipmentDefinitions.presets[tank.equipment_preset];
|
equipmentDefinitions.presets[tank.equipment_preset];
|
||||||
const provisionsList = availableProvisions(
|
const provisionsList = availableProvisions(
|
||||||
tank,
|
tank,
|
||||||
gun,
|
gun,
|
||||||
provisionDefinitions,
|
provisionDefinitions
|
||||||
);
|
);
|
||||||
const consumablesList = Object.values(
|
const consumablesList = Object.values(
|
||||||
consumableDefinitions.consumables,
|
consumableDefinitions.consumables
|
||||||
).filter((consumable) =>
|
).filter((consumable) =>
|
||||||
checkConsumableProvisionInclusivity(consumable, tank, gun),
|
checkConsumableProvisionInclusivity(consumable, tank, gun)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -205,7 +204,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<Flex align="center">
|
<Flex align="center">
|
||||||
{(
|
{(
|
||||||
['turret', 'gun', 'engine', 'chassis'] as const
|
["turret", "gun", "engine", "chassis"] as const
|
||||||
).map((module, index) => (
|
).map((module, index) => (
|
||||||
<img
|
<img
|
||||||
key={module}
|
key={module}
|
||||||
@@ -215,7 +214,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
src={asset(`icons/modules/${module}.webp`)}
|
src={asset(`icons/modules/${module}.webp`)}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: index > 0 ? -8 : 0,
|
marginLeft: index > 0 ? -8 : 0,
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -232,7 +231,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
engine={engine}
|
engine={engine}
|
||||||
track={track}
|
track={track}
|
||||||
onChange={(modules) => {
|
onChange={(modules) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index] = {
|
draft.members[index] = {
|
||||||
...draft.members[index],
|
...draft.members[index],
|
||||||
...modules,
|
...modules,
|
||||||
@@ -245,13 +244,12 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
const member = draft.members[index];
|
const member = draft.members[index];
|
||||||
|
|
||||||
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];
|
||||||
});
|
});
|
||||||
@@ -262,13 +260,12 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
const member = draft.members[index];
|
const member = draft.members[index];
|
||||||
|
|
||||||
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)!;
|
||||||
});
|
});
|
||||||
@@ -290,9 +287,9 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
radius="large"
|
radius="large"
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: "100%",
|
||||||
width: 12,
|
width: 12,
|
||||||
position: 'relative',
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex direction="column" justify="center">
|
<Flex direction="column" justify="center">
|
||||||
@@ -300,14 +297,14 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<img
|
<img
|
||||||
alt={unwrap(
|
alt={unwrap(
|
||||||
provisionDefinitions.provisions[provision]
|
provisionDefinitions.provisions[provision]
|
||||||
.name,
|
.name
|
||||||
)}
|
)}
|
||||||
key={provision}
|
key={provision}
|
||||||
src={asset(
|
src={asset(
|
||||||
`/icons/provisions/${provision}.webp`,
|
`/icons/provisions/${provision}.webp`
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
left: '50%',
|
left: "50%",
|
||||||
/**
|
/**
|
||||||
* max = 100% - 16px - 4px
|
* max = 100% - 16px - 4px
|
||||||
* min = 4px
|
* min = 4px
|
||||||
@@ -322,11 +319,11 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
? 0.5
|
? 0.5
|
||||||
: index / (provisions.length - 1)
|
: index / (provisions.length - 1)
|
||||||
} * (100% - 24px))`,
|
} * (100% - 24px))`,
|
||||||
transform: 'translateX(-50%)',
|
transform: "translateX(-50%)",
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -341,12 +338,12 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Popover.Content>
|
<Popover.Content>
|
||||||
<ProvisionsManager
|
<ProvisionsManager
|
||||||
provisions={provisionsList.map(
|
provisions={provisionsList.map(
|
||||||
(provision) => provision.id,
|
(provision) => provision.id
|
||||||
)}
|
)}
|
||||||
selected={provisions}
|
selected={provisions}
|
||||||
disabled={tank.max_provisions === provisions.length}
|
disabled={tank.max_provisions === provisions.length}
|
||||||
onChange={(provisions) => {
|
onChange={(provisions) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].provisions = provisions;
|
draft.members[index].provisions = provisions;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -355,7 +352,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].provisions = [];
|
draft.members[index].provisions = [];
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -396,8 +393,8 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
height: 8,
|
height: 8,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
equipment === -1
|
equipment === -1
|
||||||
? Var('crimson-11')
|
? Var("crimson-11")
|
||||||
: Var('gray-11'),
|
: Var("gray-11"),
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -407,8 +404,8 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
height: 8,
|
height: 8,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
equipment === 1
|
equipment === 1
|
||||||
? Var('crimson-11')
|
? Var("crimson-11")
|
||||||
: Var('gray-11'),
|
: Var("gray-11"),
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -426,7 +423,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
matrix={equipmentMatrix}
|
matrix={equipmentMatrix}
|
||||||
preset={equipmentPreset}
|
preset={equipmentPreset}
|
||||||
onChange={(matrix) => {
|
onChange={(matrix) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].equipmentMatrix = matrix;
|
draft.members[index].equipmentMatrix = matrix;
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -438,10 +435,10 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].equipmentMatrix = times(
|
draft.members[index].equipmentMatrix = times(
|
||||||
3,
|
3,
|
||||||
() => times(3, () => 0),
|
() => times(3, () => 0)
|
||||||
) as EquipmentMatrix;
|
) as EquipmentMatrix;
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -455,7 +452,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members.forEach((member) => {
|
draft.members.forEach((member) => {
|
||||||
member.equipmentMatrix =
|
member.equipmentMatrix =
|
||||||
draft.members[index].equipmentMatrix;
|
draft.members[index].equipmentMatrix;
|
||||||
@@ -479,9 +476,9 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
radius="large"
|
radius="large"
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: "100%",
|
||||||
width: 12,
|
width: 12,
|
||||||
position: 'relative',
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex direction="column" justify="center">
|
<Flex direction="column" justify="center">
|
||||||
@@ -490,14 +487,14 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
alt={unwrap(
|
alt={unwrap(
|
||||||
consumableDefinitions.consumables[
|
consumableDefinitions.consumables[
|
||||||
consumable
|
consumable
|
||||||
].name,
|
].name
|
||||||
)}
|
)}
|
||||||
key={consumable}
|
key={consumable}
|
||||||
src={asset(
|
src={asset(
|
||||||
`/icons/consumables/${consumable}.webp`,
|
`/icons/consumables/${consumable}.webp`
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
left: '50%',
|
left: "50%",
|
||||||
/**
|
/**
|
||||||
* max = 100% - 16px - 4px
|
* max = 100% - 16px - 4px
|
||||||
* min = 4px
|
* min = 4px
|
||||||
@@ -512,11 +509,11 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
? 0.5
|
? 0.5
|
||||||
: index / (consumables.length - 1)
|
: index / (consumables.length - 1)
|
||||||
} * (100% - 24px))`,
|
} * (100% - 24px))`,
|
||||||
transform: 'translateX(-50%)',
|
transform: "translateX(-50%)",
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -533,7 +530,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
consumables={consumablesList}
|
consumables={consumablesList}
|
||||||
selected={consumables}
|
selected={consumables}
|
||||||
onConsumablesChange={(consumables) => {
|
onConsumablesChange={(consumables) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].consumables = consumables;
|
draft.members[index].consumables = consumables;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -546,7 +543,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].consumables = [];
|
draft.members[index].consumables = [];
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -564,18 +561,18 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
{!tank.fixed_camouflage && (
|
{!tank.fixed_camouflage && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].camouflage = !camouflage;
|
draft.members[index].camouflage = !camouflage;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
variant={camouflage ? 'solid' : 'outline'}
|
variant={camouflage ? "solid" : "outline"}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={asset('icons/camo.webp')}
|
src={asset("icons/camo.webp")}
|
||||||
style={{
|
style={{
|
||||||
width: '1em',
|
width: "1em",
|
||||||
height: '1em',
|
height: "1em",
|
||||||
transform: 'scale(2) translate(15%, 15%)',
|
transform: "scale(2) translate(15%, 15%)",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -583,7 +580,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
)}
|
)}
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
|
|||||||
@@ -1,33 +1,27 @@
|
|||||||
import { PlusIcon, TrashIcon } from '@radix-ui/react-icons';
|
import { PlusIcon, TrashIcon } from "@radix-ui/react-icons";
|
||||||
import { Button, Dialog, Flex, SegmentedControl } from '@radix-ui/themes';
|
import { Button, Dialog, Flex, SegmentedControl } from "@radix-ui/themes";
|
||||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
|
||||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
|
||||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
|
||||||
import {
|
import {
|
||||||
ComparePersistent,
|
ComparePersistent,
|
||||||
type DeltaMode,
|
type DeltaMode,
|
||||||
} from '../../stores/comparePersistent';
|
} from "../../stores/comparePersistent";
|
||||||
import { TankSearch } from '../TankSearch';
|
import { TankSearch } from "../TankSearch";
|
||||||
|
|
||||||
interface ControlsProps {
|
interface ControlsProps {
|
||||||
addTankDialogOpen: boolean;
|
addTankDialogOpen: boolean;
|
||||||
onAddTankDialogOpenChange: (open: boolean) => void;
|
onAddTankDialogOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [provisionDefinitions, modelDefinitions] = await Promise.all([
|
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||||
awaitableProvisionDefinitions,
|
|
||||||
awaitableModelDefinitions,
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function Controls({
|
export function Controls({
|
||||||
addTankDialogOpen,
|
addTankDialogOpen,
|
||||||
onAddTankDialogOpenChange,
|
onAddTankDialogOpenChange,
|
||||||
}: ControlsProps) {
|
}: ControlsProps) {
|
||||||
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
const mutateComparePersistent = ComparePersistent.useMutation();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -60,28 +54,20 @@ export function Controls({
|
|||||||
<TankSearch
|
<TankSearch
|
||||||
compact
|
compact
|
||||||
onSelect={(tank) => {
|
onSelect={(tank) => {
|
||||||
const model = modelDefinitions.models[tank.id];
|
CompareEphemeral.mutate((draft) => {
|
||||||
|
|
||||||
mutateCompareEphemeral((draft) => {
|
|
||||||
draft.members.push(
|
draft.members.push(
|
||||||
tankToCompareMember(tank, model, provisionDefinitions),
|
tankToCompareMember(tank, provisionDefinitions)
|
||||||
);
|
);
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
onAddTankDialogOpenChange(false);
|
onAddTankDialogOpenChange(false);
|
||||||
}}
|
}}
|
||||||
onSelectAll={(tanks) => {
|
onSelectAll={(tanks) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members.push(
|
draft.members.push(
|
||||||
...tanks.map((tank) => {
|
...tanks.map((tank) => {
|
||||||
const model = modelDefinitions.models[tank.id];
|
return tankToCompareMember(tank, provisionDefinitions);
|
||||||
|
})
|
||||||
return tankToCompareMember(
|
|
||||||
tank,
|
|
||||||
model,
|
|
||||||
provisionDefinitions,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -97,7 +83,7 @@ export function Controls({
|
|||||||
variant="soft"
|
variant="soft"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members = [];
|
draft.members = [];
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -110,18 +96,18 @@ export function Controls({
|
|||||||
variant="classic"
|
variant="classic"
|
||||||
value={deltaMode}
|
value={deltaMode}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
mutateComparePersistent((draft) => {
|
ComparePersistent.mutate((draft) => {
|
||||||
draft.deltaMode = value as DeltaMode;
|
draft.deltaMode = value as DeltaMode;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SegmentedControl.Item value={'none' satisfies DeltaMode}>
|
<SegmentedControl.Item value={"none" satisfies DeltaMode}>
|
||||||
{strings.website.tools.compare.actions.deltas.none}
|
{strings.website.tools.compare.actions.deltas.none}
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
<SegmentedControl.Item value={'percentage' satisfies DeltaMode}>
|
<SegmentedControl.Item value={"percentage" satisfies DeltaMode}>
|
||||||
{strings.website.tools.compare.actions.deltas.percentage}
|
{strings.website.tools.compare.actions.deltas.percentage}
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
<SegmentedControl.Item value={'absolute' satisfies DeltaMode}>
|
<SegmentedControl.Item value={"absolute" satisfies DeltaMode}>
|
||||||
{strings.website.tools.compare.actions.deltas.absolute}
|
{strings.website.tools.compare.actions.deltas.absolute}
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
</SegmentedControl.Root>
|
</SegmentedControl.Root>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { type TankDefinition, tankIcon } from '@blitzkit/core';
|
import { type TankDefinition, tankIcon } from "@blitzkit/core";
|
||||||
import { Flex, Text } from '@radix-ui/themes';
|
import { Flex, Text } from "@radix-ui/themes";
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from "react";
|
||||||
import { Vector2 } from 'three';
|
import { Vector2 } from "three";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
||||||
import { InsertionMarker } from './IntersectionMarker';
|
import { InsertionMarker } from "./IntersectionMarker";
|
||||||
import { insertionMarkers } from './IntersectionMarker/constants';
|
import { insertionMarkers } from "./IntersectionMarker/constants";
|
||||||
import { TankControl } from './TankControl';
|
import { TankControl } from "./TankControl";
|
||||||
|
|
||||||
interface TankCardProps {
|
interface TankCardProps {
|
||||||
index: number;
|
index: number;
|
||||||
@@ -16,7 +16,6 @@ interface TankCardProps {
|
|||||||
|
|
||||||
export function TankCard({ index, tank }: TankCardProps) {
|
export function TankCard({ index, tank }: TankCardProps) {
|
||||||
const draggable = useRef<HTMLDivElement>(null);
|
const draggable = useRef<HTMLDivElement>(null);
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
const { unwrap } = useLocale();
|
const { unwrap } = useLocale();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -34,8 +33,8 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
|
|
||||||
delta.set(0, 0);
|
delta.set(0, 0);
|
||||||
current.copy(initial.set(event.clientX, event.clientY));
|
current.copy(initial.set(event.clientX, event.clientY));
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener("pointermove", handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
function handlePointerMove(event: PointerEvent) {
|
function handlePointerMove(event: PointerEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -43,7 +42,7 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
current.set(event.clientX, event.clientY);
|
current.set(event.clientX, event.clientY);
|
||||||
delta.copy(current).sub(initial);
|
delta.copy(current).sub(initial);
|
||||||
insertionMarkers.forEach(({ element }) => {
|
insertionMarkers.forEach(({ element }) => {
|
||||||
element.style.opacity = '0';
|
element.style.opacity = "0";
|
||||||
});
|
});
|
||||||
|
|
||||||
const distances = insertionMarkers.map((insertionMarker) => {
|
const distances = insertionMarkers.map((insertionMarker) => {
|
||||||
@@ -56,34 +55,34 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
const closest = distances.reduce((a, b) =>
|
const closest = distances.reduce((a, b) =>
|
||||||
a.distance < b.distance ? a : b,
|
a.distance < b.distance ? a : b
|
||||||
);
|
);
|
||||||
|
|
||||||
dropIndex = closest.index;
|
dropIndex = closest.index;
|
||||||
closest.element.style.opacity = '1';
|
closest.element.style.opacity = "1";
|
||||||
draggable.current!.style.position = 'fixed';
|
draggable.current!.style.position = "fixed";
|
||||||
draggable.current!.style.cursor = 'grabbing';
|
draggable.current!.style.cursor = "grabbing";
|
||||||
draggable.current!.style.zIndex = '1';
|
draggable.current!.style.zIndex = "1";
|
||||||
draggable.current!.style.left = `${initialRect.left + delta.x}px`;
|
draggable.current!.style.left = `${initialRect.left + delta.x}px`;
|
||||||
draggable.current!.style.top = `${initialRect.top + delta.y}px`;
|
draggable.current!.style.top = `${initialRect.top + delta.y}px`;
|
||||||
}
|
}
|
||||||
function handlePointerUp(event: PointerEvent) {
|
function handlePointerUp(event: PointerEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
draggable.current!.style.position = 'static';
|
draggable.current!.style.position = "static";
|
||||||
draggable.current!.style.zIndex = 'unset';
|
draggable.current!.style.zIndex = "unset";
|
||||||
draggable.current!.style.cursor = 'grab';
|
draggable.current!.style.cursor = "grab";
|
||||||
|
|
||||||
insertionMarkers.forEach(({ element }) => {
|
insertionMarkers.forEach(({ element }) => {
|
||||||
element.style.opacity = '0';
|
element.style.opacity = "0";
|
||||||
});
|
});
|
||||||
|
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener("pointermove", handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
|
|
||||||
if (dropIndex === -1) return;
|
if (dropIndex === -1) return;
|
||||||
|
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
const fromIndex = index;
|
const fromIndex = index;
|
||||||
const toIndex = dropIndex > index ? dropIndex - 1 : dropIndex;
|
const toIndex = dropIndex > index ? dropIndex - 1 : dropIndex;
|
||||||
|
|
||||||
@@ -95,10 +94,10 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
dropIndex = -1;
|
dropIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
draggable.current?.addEventListener('pointerdown', handlePointerDown);
|
draggable.current?.addEventListener("pointerdown", handlePointerDown);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
draggable.current?.removeEventListener('pointerdown', handlePointerDown);
|
draggable.current?.removeEventListener("pointerdown", handlePointerDown);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -114,9 +113,9 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
justify="between"
|
justify="between"
|
||||||
gap="2"
|
gap="2"
|
||||||
style={{
|
style={{
|
||||||
userSelect: 'none',
|
userSelect: "none",
|
||||||
touchAction: 'none',
|
touchAction: "none",
|
||||||
cursor: 'grab',
|
cursor: "grab",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -126,16 +125,16 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
height={64}
|
height={64}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
style={{
|
style={{
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: "nowrap",
|
||||||
maxWidth: 128,
|
maxWidth: 128,
|
||||||
overflow: 'hidden',
|
overflow: "hidden",
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: "ellipsis",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{unwrap(tank.name)}
|
{unwrap(tank.name)}
|
||||||
|
|||||||
@@ -1,35 +1,30 @@
|
|||||||
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
|
import { ExternalLinkIcon, LoopIcon, TrashIcon } from "@radix-ui/react-icons";
|
||||||
import { Dialog, Flex, IconButton } from '@radix-ui/themes';
|
import { Dialog, Flex, IconButton } from "@radix-ui/themes";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
|
||||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
|
||||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
import { LinkI18n } from "../LinkI18n";
|
||||||
import { LinkI18n } from '../LinkI18n';
|
import { TankSearch } from "../TankSearch";
|
||||||
import { TankSearch } from '../TankSearch';
|
|
||||||
|
|
||||||
interface TankControlProps {
|
interface TankControlProps {
|
||||||
index: number;
|
index: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [provisionDefinitions, modelDefinitions] = await Promise.all([
|
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||||
awaitableProvisionDefinitions,
|
|
||||||
awaitableModelDefinitions,
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function TankControl({ index, slug }: TankControlProps) {
|
export function TankControl({ index, slug }: TankControlProps) {
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
|
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap="3" justify="center" style={{ width: '100%' }}>
|
<Flex gap="3" justify="center" style={{ width: "100%" }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members.splice(index, 1);
|
draft.members.splice(index, 1);
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -61,13 +56,10 @@ export function TankControl({ index, slug }: TankControlProps) {
|
|||||||
<TankSearch
|
<TankSearch
|
||||||
compact
|
compact
|
||||||
onSelect={(tank) => {
|
onSelect={(tank) => {
|
||||||
const model = modelDefinitions.models[tank.id];
|
CompareEphemeral.mutate((draft) => {
|
||||||
|
|
||||||
mutateCompareEphemeral((draft) => {
|
|
||||||
draft.members[index] = tankToCompareMember(
|
draft.members[index] = tankToCompareMember(
|
||||||
tank,
|
tank,
|
||||||
model,
|
provisionDefinitions
|
||||||
provisionDefinitions,
|
|
||||||
);
|
);
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ import {
|
|||||||
idToRegion,
|
idToRegion,
|
||||||
sumCompositeStats,
|
sumCompositeStats,
|
||||||
type IndividualTankStats,
|
type IndividualTankStats,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { STRINGS } from '@blitzkit/i18n';
|
import { STRINGS } from "@blitzkit/i18n";
|
||||||
import { ReloadIcon } from '@radix-ui/react-icons';
|
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||||
import { ContextMenu } from '@radix-ui/themes';
|
import { ContextMenu } from "@radix-ui/themes";
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { breakdownConfig } from '../../constants/embeds';
|
import { breakdownConfig } from "../../constants/embeds";
|
||||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
|
||||||
import { awaitableTankDefinitions } from '../../core/awaitables/tankDefinitions';
|
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
||||||
import { EmbedBreakdownPersistent } from '../../stores/embedBreakdownPersistent';
|
import { EmbedBreakdown } from "../../stores/embedBreakdown";
|
||||||
import { useEmbedStateCurry } from '../../stores/embedState/utilities';
|
import { useEmbedStateCurry } from "../../stores/embedState/utilities";
|
||||||
import { BreakdownEmbedCard, BreakdownEmbedWrapper } from '../TanksEmbed';
|
import { BreakdownEmbedCard, BreakdownEmbedWrapper } from "../TanksEmbed";
|
||||||
|
|
||||||
export const compositeStatsKeysOptions = compositeStatsKeys.map((value) => ({
|
export const compositeStatsKeysOptions = compositeStatsKeys.map((value) => ({
|
||||||
value,
|
value,
|
||||||
@@ -55,7 +55,7 @@ export const fakeCompositeStats = compositeStats(
|
|||||||
wins: 30,
|
wins: 30,
|
||||||
xp: 1230 * 50,
|
xp: 1230 * 50,
|
||||||
},
|
},
|
||||||
averageDefinitions.averages[7297].mu,
|
averageDefinitions.averages[7297].mu
|
||||||
);
|
);
|
||||||
|
|
||||||
export function BreakdownPreview() {
|
export function BreakdownPreview() {
|
||||||
@@ -64,11 +64,11 @@ export function BreakdownPreview() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BreakdownEmbedWrapper>
|
<BreakdownEmbedWrapper>
|
||||||
{useEmbedState('showTotal') && (
|
{useEmbedState("showTotal") && (
|
||||||
<BreakdownEmbedCard composite={fakeCompositeStats} tank={null} />
|
<BreakdownEmbedCard composite={fakeCompositeStats} tank={null} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tanks.slice(0, useEmbedState('listMaxTanks')).map((tank) => (
|
{tanks.slice(0, useEmbedState("listMaxTanks")).map((tank) => (
|
||||||
<BreakdownEmbedCard
|
<BreakdownEmbedCard
|
||||||
composite={fakeCompositeStats}
|
composite={fakeCompositeStats}
|
||||||
key={tank.id}
|
key={tank.id}
|
||||||
@@ -80,26 +80,17 @@ export function BreakdownPreview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function BreakdownRenderer() {
|
export function BreakdownRenderer() {
|
||||||
return (
|
|
||||||
<EmbedBreakdownPersistent.Provider>
|
|
||||||
<BreakdownRendererContent />
|
|
||||||
</EmbedBreakdownPersistent.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreakdownRendererContent() {
|
|
||||||
const { useEmbedState } = useEmbedStateCurry<typeof breakdownConfig>();
|
const { useEmbedState } = useEmbedStateCurry<typeof breakdownConfig>();
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const mutateEmbedBreakdownPersistent = EmbedBreakdownPersistent.useMutation();
|
const id = Number(params.get("id"));
|
||||||
const id = Number(params.get('id'));
|
const tankStatsA = EmbedBreakdown.use((state) => state[id] ?? []);
|
||||||
const tankStatsA = EmbedBreakdownPersistent.use((state) => state[id] ?? []);
|
|
||||||
const [tankStatsB, setTankStatsB] = useState<IndividualTankStats[]>([]);
|
const [tankStatsB, setTankStatsB] = useState<IndividualTankStats[]>([]);
|
||||||
const diff = useMemo(
|
const diff = useMemo(
|
||||||
() =>
|
() =>
|
||||||
deltaTankStats(tankStatsA, tankStatsB).sort(
|
deltaTankStats(tankStatsA, tankStatsB).sort(
|
||||||
(a, b) => b.last_battle_time - a.last_battle_time,
|
(a, b) => b.last_battle_time - a.last_battle_time
|
||||||
),
|
),
|
||||||
[tankStatsB],
|
[tankStatsB]
|
||||||
);
|
);
|
||||||
const compositeStats = diff
|
const compositeStats = diff
|
||||||
.map((diff) => {
|
.map((diff) => {
|
||||||
@@ -109,14 +100,14 @@ function BreakdownRendererContent() {
|
|||||||
|
|
||||||
const compositeStats = calculateCompositeStats(
|
const compositeStats = calculateCompositeStats(
|
||||||
{ battle_life_time: diff.battle_life_time, ...diff.all },
|
{ battle_life_time: diff.battle_life_time, ...diff.all },
|
||||||
average.mu,
|
average.mu
|
||||||
);
|
);
|
||||||
|
|
||||||
return { id: diff.tank_id, compositeStats };
|
return { id: diff.tank_id, compositeStats };
|
||||||
})
|
})
|
||||||
.filter((stats) => stats !== null);
|
.filter((stats) => stats !== null);
|
||||||
const sum = sumCompositeStats(
|
const sum = sumCompositeStats(
|
||||||
compositeStats.map(({ compositeStats }) => compositeStats),
|
compositeStats.map(({ compositeStats }) => compositeStats)
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -135,12 +126,12 @@ function BreakdownRendererContent() {
|
|||||||
<ContextMenu.Root>
|
<ContextMenu.Root>
|
||||||
<ContextMenu.Trigger>
|
<ContextMenu.Trigger>
|
||||||
<BreakdownEmbedWrapper>
|
<BreakdownEmbedWrapper>
|
||||||
{useEmbedState('showTotal') && (
|
{useEmbedState("showTotal") && (
|
||||||
<BreakdownEmbedCard composite={sum} tank={null} />
|
<BreakdownEmbedCard composite={sum} tank={null} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{compositeStats
|
{compositeStats
|
||||||
.slice(0, useEmbedState('listMaxTanks'))
|
.slice(0, useEmbedState("listMaxTanks"))
|
||||||
.map(({ id, compositeStats }) => {
|
.map(({ id, compositeStats }) => {
|
||||||
const tank = tankDefinitions.tanks[id];
|
const tank = tankDefinitions.tanks[id];
|
||||||
|
|
||||||
@@ -163,7 +154,7 @@ function BreakdownRendererContent() {
|
|||||||
|
|
||||||
if (newA === null) return;
|
if (newA === null) return;
|
||||||
|
|
||||||
mutateEmbedBreakdownPersistent((draft) => {
|
EmbedBreakdown.mutate((draft) => {
|
||||||
draft[id] = newA;
|
draft[id] = newA;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import locales from '@blitzkit/i18n/locales.json';
|
import locales from "@blitzkit/i18n/locales.json";
|
||||||
import { literals } from '@blitzkit/i18n/src/literals';
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import { Flex, Text } from '@radix-ui/themes';
|
import { Flex, Text } from "@radix-ui/themes";
|
||||||
import fuzzysort from 'fuzzysort';
|
import fuzzysort from "fuzzysort";
|
||||||
import { times } from 'lodash-es';
|
import { times } from "lodash-es";
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from "react";
|
||||||
import { awaitableGallery } from '../../core/awaitables/gallery';
|
import { awaitableGallery } from "../../core/awaitables/gallery";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { GalleryEphemeral } from '../../stores/galleryEphemeral';
|
import { Gallery } from "../../stores/gallery";
|
||||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||||
import { InlineSkeleton } from '../InlineSkeleton';
|
import { InlineSkeleton } from "../InlineSkeleton";
|
||||||
import { GalleryCard } from './Card';
|
import { GalleryCard } from "./Card";
|
||||||
|
|
||||||
export interface Avatar {
|
export interface Avatar {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -22,7 +22,7 @@ const DEFAULT_LOADED = 42;
|
|||||||
const PREVIEW_COUNT = 28;
|
const PREVIEW_COUNT = 28;
|
||||||
|
|
||||||
export function GalleryList({ skeleton }: MaybeSkeletonComponentProps) {
|
export function GalleryList({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const search = GalleryEphemeral.use((state) => state.search);
|
const search = Gallery.use((state) => state.search);
|
||||||
const [loadedCards, setLoadedCards] = useState(DEFAULT_LOADED);
|
const [loadedCards, setLoadedCards] = useState(DEFAULT_LOADED);
|
||||||
const { locale, strings } = useLocale();
|
const { locale, strings } = useLocale();
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
@@ -35,9 +35,9 @@ export function GalleryList({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
.go(search, gallery.avatars, {
|
.go(search, gallery.avatars, {
|
||||||
keys: [
|
keys: [
|
||||||
...locales.supported.map(
|
...locales.supported.map(
|
||||||
(supported) => `name.locales.${supported.locale}`,
|
(supported) => `name.locales.${supported.locale}`
|
||||||
),
|
),
|
||||||
'id',
|
"id",
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.map((result) => result.obj);
|
.map((result) => result.obj);
|
||||||
@@ -72,7 +72,7 @@ export function GalleryList({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
skeleton
|
skeleton
|
||||||
onIntersection={() => setLoadedCards((state) => state + 2)}
|
onIntersection={() => setLoadedCards((state) => state + 2)}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||||
import { TextField } from '@radix-ui/themes';
|
import { TextField } from "@radix-ui/themes";
|
||||||
import { useRef } from 'react';
|
import { useRef } from "react";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { GalleryEphemeral } from '../../stores/galleryEphemeral';
|
import { Gallery } from "../../stores/gallery";
|
||||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||||
|
|
||||||
export function GallerySearch({ skeleton }: MaybeSkeletonComponentProps) {
|
export function GallerySearch({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const mutateGalleryEphemeral = GalleryEphemeral.useMutation();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -17,7 +16,7 @@ export function GallerySearch({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
placeholder={strings.website.tools.gallery.search.hint}
|
placeholder={strings.website.tools.gallery.search.hint}
|
||||||
ref={input}
|
ref={input}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
mutateGalleryEphemeral((draft) => {
|
Gallery.mutate((draft) => {
|
||||||
const trimmed = event.target.value.trim();
|
const trimmed = event.target.value.trim();
|
||||||
draft.search = trimmed.length === 0 ? undefined : trimmed;
|
draft.search = trimmed.length === 0 ? undefined : trimmed;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Box } from '@radix-ui/themes';
|
import { Box } from "@radix-ui/themes";
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
import { awaitableModelDefinitions } from "../../core/awaitables/modelDefinitions";
|
||||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
|
||||||
import { tankToDuelMember } from '../../core/blitzkit/tankToDuelMember';
|
import { tankToDuelMember } from "../../core/blitzkit/tankToDuelMember";
|
||||||
import { Var } from '../../core/radix/var';
|
import { Var } from "../../core/radix/var";
|
||||||
import { Duel } from '../../stores/duel';
|
import { Duel } from "../../stores/duel";
|
||||||
import { GuessEphemeral } from '../../stores/guessEphemeral';
|
import { Guess, GuessState } from "../../stores/guess";
|
||||||
import { TankopediaEphemeral } from '../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../stores/tankopedia";
|
||||||
import './index.css';
|
import "./index.css";
|
||||||
|
|
||||||
const [modelDefinitions, provisionDefinitions] = await Promise.all([
|
const [modelDefinitions, provisionDefinitions] = await Promise.all([
|
||||||
awaitableModelDefinitions,
|
awaitableModelDefinitions,
|
||||||
@@ -15,17 +15,15 @@ const [modelDefinitions, provisionDefinitions] = await Promise.all([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export function GuessBackground() {
|
export function GuessBackground() {
|
||||||
const guessState = GuessEphemeral.use((state) => state.guessState);
|
const guessState = Guess.use((state) => state.guessState);
|
||||||
const tank = GuessEphemeral.use((state) => state.tank);
|
const tank = Guess.use((state) => state.tank);
|
||||||
const mutateDuel = Duel.useMutation();
|
const mutateDuel = Duel.useMutation();
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mutateDuel((state) => {
|
mutateDuel((state) => {
|
||||||
const model = modelDefinitions.models[tank.id];
|
state.protagonist = tankToDuelMember(tank, provisionDefinitions);
|
||||||
state.protagonist = tankToDuelMember(tank, model, provisionDefinitions);
|
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((state) => {
|
Tankopedia.mutate((state) => {
|
||||||
state.model = modelDefinitions.models[tank.id];
|
state.model = modelDefinitions.models[tank.id];
|
||||||
});
|
});
|
||||||
}, [tank]);
|
}, [tank]);
|
||||||
@@ -39,13 +37,13 @@ export function GuessBackground() {
|
|||||||
top="0"
|
top="0"
|
||||||
left="0"
|
left="0"
|
||||||
style={{
|
style={{
|
||||||
transitionDuration: '2s',
|
transitionDuration: "2s",
|
||||||
backgroundColor: Var(
|
backgroundColor: Var(
|
||||||
guessState === null
|
guessState === GuessState.NotGuessed
|
||||||
? 'accent-2'
|
? "accent-2"
|
||||||
: guessState
|
: guessState === GuessState.Correct
|
||||||
? 'jade-3'
|
? "jade-3"
|
||||||
: 'tomato-3',
|
: "tomato-3"
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -53,7 +51,7 @@ export function GuessBackground() {
|
|||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
style={{
|
style={{
|
||||||
background: `radial-gradient(circle, ${Var('color-panel-translucent')}, ${Var('color-panel-solid')})`,
|
background: `radial-gradient(circle, ${Var("color-panel-translucent")}, ${Var("color-panel-solid")})`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -67,13 +65,13 @@ export function GuessBackground() {
|
|||||||
left="0"
|
left="0"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `
|
backgroundImage: `
|
||||||
radial-gradient(circle, ${Var('white-a1')} 20%, transparent 20%),
|
radial-gradient(circle, ${Var("white-a1")} 20%, transparent 20%),
|
||||||
radial-gradient(circle at 0 0, ${Var('white-a1')} 10%, transparent 10%),
|
radial-gradient(circle at 0 0, ${Var("white-a1")} 10%, transparent 10%),
|
||||||
radial-gradient(circle at 100% 0, ${Var('white-a1')} 10%, transparent 10%),
|
radial-gradient(circle at 100% 0, ${Var("white-a1")} 10%, transparent 10%),
|
||||||
radial-gradient(circle at 0 100%, ${Var('white-a1')} 10%, transparent 10%),
|
radial-gradient(circle at 0 100%, ${Var("white-a1")} 10%, transparent 10%),
|
||||||
radial-gradient(circle at 100% 100%, ${Var('white-a1')} 10%, transparent 10%)
|
radial-gradient(circle at 100% 100%, ${Var("white-a1")} 10%, transparent 10%)
|
||||||
`,
|
`,
|
||||||
backgroundSize: '4rem 4rem',
|
backgroundSize: "4rem 4rem",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,30 +1,28 @@
|
|||||||
import { Canvas } from '@react-three/fiber';
|
import { Guess, GuessState } from "../stores/guess";
|
||||||
import { GuessEphemeral } from '../stores/guessEphemeral';
|
import { SmartCanvas } from "./SmartCanvas";
|
||||||
import { TankopediaDisplay } from '../stores/tankopediaPersistent/constants';
|
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';
|
|
||||||
|
|
||||||
const BRIGHTNESS = 2 ** -5;
|
const BRIGHTNESS = 2 ** -5.5;
|
||||||
const CONTRAST = 2 ** 2;
|
const CONTRAST = 2 ** 2;
|
||||||
|
|
||||||
export function GuessRenderer() {
|
export function GuessRenderer() {
|
||||||
const guessState = GuessEphemeral.use((state) => state.guessState);
|
const guessState = Guess.use((state) => state.guessState);
|
||||||
const isRevealed = guessState !== null;
|
const isRevealed = guessState !== GuessState.NotGuessed;
|
||||||
|
|
||||||
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')})`,
|
transitionDuration: isRevealed ? "2s" : undefined,
|
||||||
transitionDuration: isRevealed ? '2s' : undefined,
|
transitionProperty: "filter",
|
||||||
transitionProperty: 'filter',
|
|
||||||
}}
|
}}
|
||||||
camera={{ position: [-5, 5, -5] }}
|
camera={{ position: [-5, 5, -5] }}
|
||||||
>
|
>
|
||||||
<Controls distanceScale={2} zoomable={false} />
|
<Controls zoomable={false} />
|
||||||
<Lighting display={TankopediaDisplay.Model} />
|
<Lighting />
|
||||||
<TankModel />
|
<TankModel />
|
||||||
</Canvas>
|
</SmartCanvas>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2,13 +2,13 @@ import {
|
|||||||
SEARCH_KEYS,
|
SEARCH_KEYS,
|
||||||
TankDefinition,
|
TankDefinition,
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { literals } from '@blitzkit/i18n';
|
import { literals } from "@blitzkit/i18n";
|
||||||
import {
|
import {
|
||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
PaperPlaneIcon,
|
PaperPlaneIcon,
|
||||||
} from '@radix-ui/react-icons';
|
} from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -17,16 +17,16 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import fuzzysort from 'fuzzysort';
|
import fuzzysort from "fuzzysort";
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from "lodash-es";
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { awaitableTankDefinitions } from '../core/awaitables/tankDefinitions';
|
import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions";
|
||||||
import { awaitableTankNames } from '../core/awaitables/tankNames';
|
import { awaitableTankNames } from "../core/awaitables/tankNames";
|
||||||
import { useLocale } from '../hooks/useLocale';
|
import { useLocale } from "../hooks/useLocale";
|
||||||
import { GuessEphemeral } from '../stores/guessEphemeral';
|
import { Guess, GuessState } from "../stores/guess";
|
||||||
import { classIcons } from './ClassIcon';
|
import { classIcons } from "./ClassIcon";
|
||||||
import { SearchResults } from './SearchResults';
|
import { SearchResults } from "./SearchResults";
|
||||||
|
|
||||||
const { go } = fuzzysort;
|
const { go } = fuzzysort;
|
||||||
|
|
||||||
@@ -38,16 +38,15 @@ const [tankNames, tankDefinitions] = await Promise.all([
|
|||||||
const ids = Object.keys(tankDefinitions.tanks);
|
const ids = Object.keys(tankDefinitions.tanks);
|
||||||
|
|
||||||
export function Guesser() {
|
export function Guesser() {
|
||||||
const tank = GuessEphemeral.use((state) => state.tank);
|
const tank = Guess.use((state) => state.tank);
|
||||||
const guessState = GuessEphemeral.use((state) => state.guessState);
|
const guessState = Guess.use((state) => state.guessState);
|
||||||
const correctGuesses = GuessEphemeral.use((state) => state.correctGuesses);
|
const correctGuesses = Guess.use((state) => state.correctGuesses);
|
||||||
const totalGuesses = GuessEphemeral.use((state) => state.totalGuesses);
|
const totalGuesses = Guess.use((state) => state.totalGuesses);
|
||||||
const { strings, unwrap } = useLocale();
|
const { strings, unwrap } = useLocale();
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const [results, setResults] = useState<TankDefinition[] | null>(null);
|
const [results, setResults] = useState<TankDefinition[] | null>(null);
|
||||||
const [selected, setSelected] = useState<TankDefinition | null>(null);
|
const [selected, setSelected] = useState<TankDefinition | null>(null);
|
||||||
const mutateGuessEphemeral = GuessEphemeral.useMutation();
|
|
||||||
|
|
||||||
const requestSearch = useCallback(() => {
|
const requestSearch = useCallback(() => {
|
||||||
setSelected(null);
|
setSelected(null);
|
||||||
@@ -72,14 +71,14 @@ export function Guesser() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setResults(
|
setResults(
|
||||||
searchResults.map((result) => tankDefinitions.tanks[result.obj.id]),
|
searchResults.map((result) => tankDefinitions.tanks[result.obj.id])
|
||||||
);
|
);
|
||||||
}, 500),
|
}, 500),
|
||||||
[],
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (guessState !== null) setSelected(null);
|
if (guessState !== GuessState.NotGuessed) setSelected(null);
|
||||||
}, [guessState]);
|
}, [guessState]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -88,7 +87,7 @@ export function Guesser() {
|
|||||||
position="absolute"
|
position="absolute"
|
||||||
bottom="0"
|
bottom="0"
|
||||||
left="50%"
|
left="50%"
|
||||||
style={{ transform: 'translateX(-50%)' }}
|
style={{ transform: "translateX(-50%)" }}
|
||||||
width="100%"
|
width="100%"
|
||||||
p="4"
|
p="4"
|
||||||
maxWidth="25rem"
|
maxWidth="25rem"
|
||||||
@@ -138,7 +137,7 @@ export function Guesser() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Flex justify="center">
|
<Flex justify="center" style={{ userSelect: "none" }}>
|
||||||
<Text>
|
<Text>
|
||||||
{literals(strings.website.tools.guess.stats, [
|
{literals(strings.website.tools.guess.stats, [
|
||||||
`${correctGuesses}`,
|
`${correctGuesses}`,
|
||||||
@@ -149,7 +148,7 @@ export function Guesser() {
|
|||||||
|
|
||||||
<Flex gap="3">
|
<Flex gap="3">
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
disabled={guessState !== null}
|
disabled={guessState !== GuessState.NotGuessed}
|
||||||
ref={input}
|
ref={input}
|
||||||
onChange={requestSearch}
|
onChange={requestSearch}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
@@ -164,13 +163,19 @@ export function Guesser() {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="3"
|
size="3"
|
||||||
color={guessState === null && selected === null ? 'red' : undefined}
|
color={
|
||||||
|
guessState === GuessState.NotGuessed && selected === null
|
||||||
|
? "red"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (guessState === null) {
|
if (guessState === GuessState.NotGuessed) {
|
||||||
const correct = selected !== null && selected.id === tank.id;
|
const correct = selected !== null && selected.id === tank.id;
|
||||||
|
|
||||||
mutateGuessEphemeral((draft) => {
|
Guess.mutate((draft) => {
|
||||||
draft.guessState = correct;
|
draft.guessState = correct
|
||||||
|
? GuessState.Correct
|
||||||
|
: GuessState.Incorrect;
|
||||||
draft.totalGuesses++;
|
draft.totalGuesses++;
|
||||||
draft.correctGuesses += correct ? 1 : 0;
|
draft.correctGuesses += correct ? 1 : 0;
|
||||||
});
|
});
|
||||||
@@ -178,21 +183,22 @@ export function Guesser() {
|
|||||||
const id = Number(ids[Math.floor(Math.random() * ids.length)]);
|
const id = Number(ids[Math.floor(Math.random() * ids.length)]);
|
||||||
const tank = tankDefinitions.tanks[id];
|
const tank = tankDefinitions.tanks[id];
|
||||||
|
|
||||||
mutateGuessEphemeral((draft) => {
|
Guess.mutate((draft) => {
|
||||||
if (!input.current) return;
|
if (!input.current) return;
|
||||||
|
|
||||||
draft.tank = tank;
|
draft.tank = tank;
|
||||||
draft.guessState = null;
|
draft.guessState = GuessState.NotGuessed;
|
||||||
input.current.value = '';
|
|
||||||
|
input.current.value = "";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{guessState === null ? (
|
{guessState === GuessState.NotGuessed ? (
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
strings.website.tools.guess.search[
|
strings.website.tools.guess.search[
|
||||||
selected === null ? 'skip' : 'guess'
|
selected === null ? "skip" : "guess"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
{selected === null ? <ArrowRightIcon /> : <PaperPlaneIcon />}
|
{selected === null ? <ArrowRightIcon /> : <PaperPlaneIcon />}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { SpotLightProps } from '@react-three/fiber';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
interface Props extends SpotLightProps {
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HelpingSpotLight = memo<Props>(({ debug, ...props }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{debug && (
|
||||||
|
<mesh position={props.position}>
|
||||||
|
<icosahedronGeometry args={[0.1, 2]} />
|
||||||
|
<meshBasicMaterial color={props.color} />
|
||||||
|
</mesh>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<spotLight {...props} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -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 },
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
|
||||||
|
import { jsxTree } from "../../../core/blitzkit/jsxTree";
|
||||||
|
import { useModel } from "../../../hooks/useModel";
|
||||||
|
import { Mixer } from "../../../stores/mixer";
|
||||||
|
|
||||||
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
|
|
||||||
|
export function Model() {
|
||||||
|
const hull = Mixer.use((state) => state.hull);
|
||||||
|
const turret = Mixer.use((state) => state.turret);
|
||||||
|
const gun = Mixer.use((state) => state.gun);
|
||||||
|
|
||||||
|
const hullGltf = useModel(hull.id);
|
||||||
|
const turretGltf = useModel(turret.tank.id);
|
||||||
|
const gunGltf = useModel(gun.tank.id);
|
||||||
|
|
||||||
|
const hullNodes = Object.values(hullGltf.gltf.nodes);
|
||||||
|
const turretNodes = Object.values(turretGltf.gltf.nodes);
|
||||||
|
const gunNodes = Object.values(gunGltf.gltf.nodes);
|
||||||
|
|
||||||
|
const tankModel = modelDefinitions.models[hull.id];
|
||||||
|
const trackModel = tankModel.tracks[hull.tracks.at(-1)!.id];
|
||||||
|
|
||||||
|
const turretTankModel = modelDefinitions.models[turret.tank.id];
|
||||||
|
const turretTrackModel =
|
||||||
|
turretTankModel.tracks[turret.tank.tracks.at(-1)!.id];
|
||||||
|
const turretModel = turretTankModel.turrets[turret.turret.id];
|
||||||
|
|
||||||
|
const gunTankModel = modelDefinitions.models[gun.tank.id];
|
||||||
|
const gunTrackModel = gunTankModel.tracks[gun.tank.tracks.at(-1)!.id];
|
||||||
|
const gunTurretModel = gunTankModel.turrets[gun.turret.id];
|
||||||
|
const gunModel = gunTurretModel.guns[gun.gun.id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group rotation={[-Math.PI / 2, 0, 0]}>
|
||||||
|
{hullNodes.map((node) => {
|
||||||
|
const isHull = node.name === "hull";
|
||||||
|
const isWheel = node.name.startsWith("chassis_wheel_");
|
||||||
|
const isTrack = node.name.startsWith("chassis_track_");
|
||||||
|
const isVisible = isHull || isWheel || isTrack;
|
||||||
|
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
return jsxTree(node, {
|
||||||
|
mesh(_, props, key) {
|
||||||
|
return <mesh {...props} key={key} castShadow receiveShadow />;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
|
||||||
|
<group
|
||||||
|
position={[
|
||||||
|
tankModel.turret_origin.x +
|
||||||
|
trackModel.origin.x -
|
||||||
|
turretTankModel.turret_origin.x -
|
||||||
|
turretTrackModel.origin.x,
|
||||||
|
tankModel.turret_origin.z +
|
||||||
|
trackModel.origin.z -
|
||||||
|
turretTankModel.turret_origin.z -
|
||||||
|
turretTrackModel.origin.z,
|
||||||
|
tankModel.turret_origin.y +
|
||||||
|
trackModel.origin.y -
|
||||||
|
turretTankModel.turret_origin.y -
|
||||||
|
turretTrackModel.origin.y,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{turretNodes.map((node) => {
|
||||||
|
const isCurrentTurret =
|
||||||
|
node.name ===
|
||||||
|
`turret_${turretModel.model_id.toString().padStart(2, "0")}`;
|
||||||
|
const isVisible = isCurrentTurret;
|
||||||
|
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
return jsxTree(node, {
|
||||||
|
mesh(_, props, key) {
|
||||||
|
return <mesh {...props} key={key} castShadow receiveShadow />;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
|
||||||
|
<group
|
||||||
|
position={[
|
||||||
|
turretTankModel.turret_origin.x +
|
||||||
|
turretTrackModel.origin.x +
|
||||||
|
turretModel.gun_origin.x -
|
||||||
|
gunTankModel.turret_origin.x -
|
||||||
|
gunTrackModel.origin.x -
|
||||||
|
gunTurretModel.gun_origin.x,
|
||||||
|
turretTankModel.turret_origin.z +
|
||||||
|
turretTrackModel.origin.z +
|
||||||
|
turretModel.gun_origin.z -
|
||||||
|
gunTankModel.turret_origin.z -
|
||||||
|
gunTrackModel.origin.z -
|
||||||
|
gunTurretModel.gun_origin.z,
|
||||||
|
turretTankModel.turret_origin.y +
|
||||||
|
turretTrackModel.origin.y +
|
||||||
|
turretModel.gun_origin.y -
|
||||||
|
gunTankModel.turret_origin.y -
|
||||||
|
gunTrackModel.origin.y -
|
||||||
|
gunTurretModel.gun_origin.y,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{gunNodes.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 isVisible = isCurrentGun || isCurrentMantlet;
|
||||||
|
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
return jsxTree(node, {
|
||||||
|
mesh(_, props, key) {
|
||||||
|
return <mesh {...props} key={key} castShadow receiveShadow />;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { OrbitControls } from "@react-three/drei";
|
||||||
|
import { Canvas } from "@react-three/fiber";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import { Lighting } from "../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
||||||
|
import { Model } from "./components/Model";
|
||||||
|
|
||||||
|
export function MixerScene() {
|
||||||
|
return (
|
||||||
|
<Canvas
|
||||||
|
shadows="soft"
|
||||||
|
camera={{
|
||||||
|
fov: 45,
|
||||||
|
position: [5, 5, -5],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<OrbitControls />
|
||||||
|
|
||||||
|
<Lighting />
|
||||||
|
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<Model />
|
||||||
|
</Suspense>
|
||||||
|
</Canvas>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
import { Flex, Select, Text } from '@radix-ui/themes';
|
import { Flex, Select, Text } from '@radix-ui/themes';
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from '../../hooks/useLocale';
|
||||||
import {
|
import { Performance, type PlayerCountPeriod } from '../../stores/performance';
|
||||||
TankPerformanceEphemeral,
|
|
||||||
type PlayerCountPeriod,
|
|
||||||
} from '../../stores/tankPerformanceEphemeral';
|
|
||||||
|
|
||||||
export function PlayerCountControl() {
|
export function PlayerCountControl() {
|
||||||
const playerCountPeriod = TankPerformanceEphemeral.use(
|
const playerCountPeriod = Performance.use((state) => state.playerCountPeriod);
|
||||||
(state) => state.playerCountPeriod,
|
|
||||||
);
|
|
||||||
const mutateTankPerformanceSort = TankPerformanceEphemeral.useMutation();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -18,7 +12,7 @@ export function PlayerCountControl() {
|
|||||||
<Select.Root
|
<Select.Root
|
||||||
value={playerCountPeriod}
|
value={playerCountPeriod}
|
||||||
onValueChange={(period) => {
|
onValueChange={(period) => {
|
||||||
mutateTankPerformanceSort((draft) => {
|
Performance.mutate((draft) => {
|
||||||
draft.playerCountPeriod = period as PlayerCountPeriod;
|
draft.playerCountPeriod = period as PlayerCountPeriod;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { memo } from 'react';
|
|||||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
||||||
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from '../../hooks/useLocale';
|
||||||
import { TankPerformanceEphemeral } from '../../stores/tankPerformanceEphemeral';
|
import { Performance } from '../../stores/performance';
|
||||||
import { TankRowHeaderCell } from '../TankRowHeaderCell';
|
import { TankRowHeaderCell } from '../TankRowHeaderCell';
|
||||||
|
|
||||||
interface TankRowProps {
|
interface TankRowProps {
|
||||||
@@ -17,7 +17,7 @@ export const TankRow = memo<TankRowProps>(
|
|||||||
({ tank }) => {
|
({ tank }) => {
|
||||||
const averages = averageDefinitions.averages[tank.id];
|
const averages = averageDefinitions.averages[tank.id];
|
||||||
const ratio = useAveragesExclusionRatio();
|
const ratio = useAveragesExclusionRatio();
|
||||||
const playerCountPeriod = TankPerformanceEphemeral.use(
|
const playerCountPeriod = Performance.use(
|
||||||
(state) => state.playerCountPeriod,
|
(state) => state.playerCountPeriod,
|
||||||
);
|
);
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
import { Table } from "@radix-ui/themes";
|
||||||
import { Table } from '@radix-ui/themes';
|
import { times } from "lodash-es";
|
||||||
import { times } from 'lodash-es';
|
import { Fragment, Suspense, useEffect, useMemo, useState } from "react";
|
||||||
import { Fragment, Suspense, useEffect, useMemo, useState } from 'react';
|
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
|
||||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
||||||
import { awaitableTankDefinitions } from '../../core/awaitables/tankDefinitions';
|
import { filterTank } from "../../core/blitzkit/filterTank";
|
||||||
import { filterTank } from '../../core/blitzkit/filterTank';
|
import { Performance } from "../../stores/performance";
|
||||||
import { $tankFilters } from '../../stores/tankFilters';
|
import { TankFilters } from "../../stores/tankFilters";
|
||||||
import { TankPerformanceEphemeral } from '../../stores/tankPerformanceEphemeral';
|
import { TankPerformanceSort } from "../../stores/tankPerformanceSort";
|
||||||
import { TankPerformanceSort } from '../../stores/tankPerformanceSort';
|
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
import { RowLoader } from "./RowLoader";
|
||||||
import { RowLoader } from './RowLoader';
|
import { TankRow } from "./TankRow";
|
||||||
import { TankRow } from './TankRow';
|
import { Total } from "./Total";
|
||||||
import { Total } from './Total';
|
|
||||||
|
|
||||||
const PREVIEW_COUNT = 10;
|
const PREVIEW_COUNT = 10;
|
||||||
const DEFAULT_LOADED_ROWS = 25;
|
const DEFAULT_LOADED_ROWS = 25;
|
||||||
@@ -25,7 +24,7 @@ const averageDefinitionsArray = Object.entries(averageDefinitions.averages).map(
|
|||||||
([id, average]) => ({
|
([id, average]) => ({
|
||||||
id: Number(id),
|
id: Number(id),
|
||||||
...average,
|
...average,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
@@ -40,98 +39,97 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sort = TankPerformanceSort.use();
|
const sort = TankPerformanceSort.use();
|
||||||
const tankPerformancePersistentStore = TankPerformanceEphemeral.useStore();
|
|
||||||
const filters = useStore($tankFilters);
|
|
||||||
const tanksSorted = useMemo(() => {
|
const tanksSorted = useMemo(() => {
|
||||||
const { playerCountPeriod } = tankPerformancePersistentStore.getState();
|
const { playerCountPeriod } = Performance.state;
|
||||||
|
|
||||||
switch (sort.type) {
|
switch (sort.type) {
|
||||||
case 'accuracy':
|
case "accuracy":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction * (a.mu.hits / a.mu.shots - b.mu.hits / b.mu.shots),
|
sort.direction * (a.mu.hits / a.mu.shots - b.mu.hits / b.mu.shots)
|
||||||
);
|
);
|
||||||
case 'capture_points':
|
case "capture_points":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.capture_points / a.mu.battles -
|
(a.mu.capture_points / a.mu.battles -
|
||||||
b.mu.capture_points / b.mu.battles),
|
b.mu.capture_points / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'damage':
|
case "damage":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.damage_dealt / a.mu.battles -
|
(a.mu.damage_dealt / a.mu.battles -
|
||||||
b.mu.damage_dealt / b.mu.battles),
|
b.mu.damage_dealt / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'damage_ratio':
|
case "damage_ratio":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.damage_dealt / a.mu.damage_received -
|
(a.mu.damage_dealt / a.mu.damage_received -
|
||||||
b.mu.damage_dealt / b.mu.damage_received),
|
b.mu.damage_dealt / b.mu.damage_received)
|
||||||
);
|
);
|
||||||
case 'damage_taken':
|
case "damage_taken":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.damage_received / a.mu.battles -
|
(a.mu.damage_received / a.mu.battles -
|
||||||
b.mu.damage_received / b.mu.battles),
|
b.mu.damage_received / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'hits':
|
case "hits":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.hits / a.mu.battles - b.mu.hits / b.mu.battles),
|
(a.mu.hits / a.mu.battles - b.mu.hits / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'kills':
|
case "kills":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.frags / a.mu.battles - b.mu.frags / b.mu.battles),
|
(a.mu.frags / a.mu.battles - b.mu.frags / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'shots':
|
case "shots":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.shots / a.mu.battles - b.mu.shots / b.mu.battles),
|
(a.mu.shots / a.mu.battles - b.mu.shots / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'players':
|
case "players":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.samples[playerCountPeriod] - b.samples[playerCountPeriod]),
|
(a.samples[playerCountPeriod] - b.samples[playerCountPeriod])
|
||||||
);
|
);
|
||||||
case 'spots':
|
case "spots":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.spotted / a.mu.battles - b.mu.spotted / b.mu.battles),
|
(a.mu.spotted / a.mu.battles - b.mu.spotted / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'survival':
|
case "survival":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.survived_battles / a.mu.battles -
|
(a.mu.survived_battles / a.mu.battles -
|
||||||
b.mu.survived_battles / b.mu.battles),
|
b.mu.survived_battles / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'winrate':
|
case "winrate":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.wins / a.mu.battles - b.mu.wins / b.mu.battles),
|
(a.mu.wins / a.mu.battles - b.mu.wins / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'xp':
|
case "xp":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction * (a.mu.xp / a.mu.battles - b.mu.xp / b.mu.battles),
|
sort.direction * (a.mu.xp / a.mu.battles - b.mu.xp / b.mu.battles)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [sort]);
|
}, [sort]);
|
||||||
|
const filters = TankFilters.use();
|
||||||
const tanks = useMemo(() => {
|
const tanks = useMemo(() => {
|
||||||
return tanksSorted.filter((tank) =>
|
return tanksSorted.filter((tank) =>
|
||||||
filterTank(filters, tankDefinitions.tanks[tank.id]),
|
filterTank(filters, tankDefinitions.tanks[tank.id])
|
||||||
);
|
);
|
||||||
}, [filters, sort]);
|
}, [filters, sort]);
|
||||||
const [loadedRows, setLoadedRows] = useState(DEFAULT_LOADED_ROWS);
|
const [loadedRows, setLoadedRows] = useState(DEFAULT_LOADED_ROWS);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { memo, useCallback } from 'react';
|
|||||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
||||||
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from '../../hooks/useLocale';
|
||||||
import { TankPerformanceEphemeral } from '../../stores/tankPerformanceEphemeral';
|
import { Performance } from '../../stores/performance';
|
||||||
import { StickyRowHeaderCell } from '../StickyRowHeaderCell';
|
import { StickyRowHeaderCell } from '../StickyRowHeaderCell';
|
||||||
|
|
||||||
interface AverageDefinitionsEntryWithId extends AverageDefinitionsEntry {
|
interface AverageDefinitionsEntryWithId extends AverageDefinitionsEntry {
|
||||||
@@ -23,7 +23,7 @@ const averageDefinitions = await awaitableAverageDefinitions;
|
|||||||
|
|
||||||
export const Total = memo<TotalProps>(
|
export const Total = memo<TotalProps>(
|
||||||
({ tanks }) => {
|
({ tanks }) => {
|
||||||
const playerCountPeriod = TankPerformanceEphemeral.use(
|
const playerCountPeriod = Performance.use(
|
||||||
(state) => state.playerCountPeriod,
|
(state) => state.playerCountPeriod,
|
||||||
);
|
);
|
||||||
const { strings, locale } = useLocale();
|
const { strings, locale } = useLocale();
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -4,11 +4,10 @@ import {
|
|||||||
unionBoundingBox,
|
unionBoundingBox,
|
||||||
type TankDefinition,
|
type TankDefinition,
|
||||||
} from '@blitzkit/core';
|
} from '@blitzkit/core';
|
||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { awaitableModelDefinitions } from '../../../core/awaitables/modelDefinitions';
|
import { awaitableModelDefinitions } from '../../../core/awaitables/modelDefinitions';
|
||||||
import { resolveReload } from '../../../core/blitzkit/resolveReload';
|
import { resolveReload } from '../../../core/blitzkit/resolveReload';
|
||||||
import { $tankopediaSort } from '../../../stores/tankopediaSort';
|
import { TankSort } from '../../../stores/tankopediaSort';
|
||||||
import { TankCard } from '../../TankCard';
|
import { TankCard } from '../../TankCard';
|
||||||
|
|
||||||
interface TankSearchCardProps {
|
interface TankSearchCardProps {
|
||||||
@@ -19,25 +18,24 @@ interface TankSearchCardProps {
|
|||||||
const modelDefinitions = await awaitableModelDefinitions;
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
|
|
||||||
export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
||||||
const tankopediaSort = useStore($tankopediaSort);
|
const by = TankSort.use((state) => state.by);
|
||||||
const discriminator = useMemo(() => {
|
const discriminator = useMemo(() => {
|
||||||
if (tankopediaSort.by.startsWith('meta')) return undefined;
|
if (by.startsWith('meta')) return undefined;
|
||||||
|
|
||||||
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 (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 +45,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 +61,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 +87,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 +95,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 +109,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(
|
||||||
@@ -138,7 +132,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
|||||||
return Math.max(bounds.x, bounds.y, bounds.z).toFixed(0);
|
return Math.max(bounds.x, bounds.y, bounds.z).toFixed(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [tankopediaSort.by]);
|
}, [by]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TankCard
|
<TankCard
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import {
|
|||||||
TANK_CLASSES,
|
TANK_CLASSES,
|
||||||
TankType,
|
TankType,
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -14,29 +13,22 @@ import {
|
|||||||
Link,
|
Link,
|
||||||
Popover,
|
Popover,
|
||||||
Text,
|
Text,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import { isEqual, times } from 'lodash-es';
|
import { isEqual, times } from "lodash-es";
|
||||||
import { awaitableGameDefinitions } from '../../../core/awaitables/gameDefinitions';
|
import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions";
|
||||||
import { Var } from '../../../core/radix/var';
|
import { Var } from "../../../core/radix/var";
|
||||||
import { useLocale } from '../../../hooks/useLocale';
|
import { useLocale } from "../../../hooks/useLocale";
|
||||||
import {
|
import { TankFilters } from "../../../stores/tankFilters";
|
||||||
$tankFilters,
|
import { TankSort } from "../../../stores/tankopediaSort";
|
||||||
initialTankFilters,
|
import { classIcons } from "../../ClassIcon";
|
||||||
type TankFilters,
|
import { ExperimentIcon } from "../../ExperimentIcon";
|
||||||
} from '../../../stores/tankFilters';
|
import { GunAutoloaderIcon } from "../../GunAutoloaderIcon";
|
||||||
import {
|
import { GunAutoreloaderIcon } from "../../GunAutoreloaderIcon";
|
||||||
$tankopediaSort,
|
import { GunRegularIcon } from "../../GunRegularIcon";
|
||||||
initialTankopediaSort,
|
import { MissingShellIcon } from "../../MissingShellIcon";
|
||||||
} from '../../../stores/tankopediaSort';
|
import { ResearchedIcon } from "../../ResearchedIcon";
|
||||||
import { classIcons } from '../../ClassIcon';
|
import { ScienceIcon } from "../../ScienceIcon";
|
||||||
import { ExperimentIcon } from '../../ExperimentIcon';
|
import { ScienceOffIcon } from "../../ScienceOffIcon";
|
||||||
import { GunAutoloaderIcon } from '../../GunAutoloaderIcon';
|
|
||||||
import { GunAutoreloaderIcon } from '../../GunAutoreloaderIcon';
|
|
||||||
import { GunRegularIcon } from '../../GunRegularIcon';
|
|
||||||
import { MissingShellIcon } from '../../MissingShellIcon';
|
|
||||||
import { ResearchedIcon } from '../../ResearchedIcon';
|
|
||||||
import { ScienceIcon } from '../../ScienceIcon';
|
|
||||||
import { ScienceOffIcon } from '../../ScienceOffIcon';
|
|
||||||
|
|
||||||
interface FilterControlProps {
|
interface FilterControlProps {
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
@@ -45,29 +37,29 @@ interface FilterControlProps {
|
|||||||
const gameDefinitions = await awaitableGameDefinitions;
|
const gameDefinitions = await awaitableGameDefinitions;
|
||||||
|
|
||||||
const shellTypeIcons: Record<ShellType, string> = {
|
const shellTypeIcons: Record<ShellType, string> = {
|
||||||
[ShellType.AP]: 'ap',
|
[ShellType.AP]: "ap",
|
||||||
[ShellType.APCR]: 'ap_cr',
|
[ShellType.APCR]: "ap_cr",
|
||||||
[ShellType.HE]: 'he',
|
[ShellType.HE]: "he",
|
||||||
[ShellType.HEAT]: 'hc',
|
[ShellType.HEAT]: "hc",
|
||||||
};
|
};
|
||||||
|
|
||||||
function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
||||||
const tankFilters = useStore($tankFilters);
|
const shells = TankFilters.use((state) => state.shells);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Root>
|
<Popover.Root>
|
||||||
<Popover.Trigger>
|
<Popover.Trigger>
|
||||||
<IconButton variant="soft" radius="none" color="gray" highContrast>
|
<IconButton variant="soft" radius="none" color="gray" highContrast>
|
||||||
{tankFilters.shells[index] === null && (
|
{shells[index] === null && (
|
||||||
<Text color="gray" style={{ display: 'contents' }}>
|
<Text color="gray" style={{ display: "contents" }}>
|
||||||
<MissingShellIcon width="1em" height="1em" />
|
<MissingShellIcon width="1em" height="1em" />
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{tankFilters.shells[index] !== null && (
|
{shells[index] !== null && (
|
||||||
<img
|
<img
|
||||||
style={{ width: '1em', height: '1em' }}
|
style={{ width: "1em", height: "1em" }}
|
||||||
src={asset(
|
src={asset(
|
||||||
`icons/shells/${shellTypeIcons[tankFilters.shells[index]]}${premium ? '_premium' : ''}.webp`,
|
`icons/shells/${shellTypeIcons[shells[index]]}${premium ? "_premium" : ""}.webp`
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -78,35 +70,33 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
|||||||
<Inset>
|
<Inset>
|
||||||
<Flex>
|
<Flex>
|
||||||
{Object.values(ShellType).map((shellType) => {
|
{Object.values(ShellType).map((shellType) => {
|
||||||
if (typeof shellType === 'string') return null;
|
if (typeof shellType === "string") return null;
|
||||||
|
|
||||||
const selected = tankFilters.shells[index] === shellType;
|
const selected = shells[index] === shellType;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
key={shellType}
|
key={shellType}
|
||||||
value={`${shellType}`}
|
value={`${shellType}`}
|
||||||
radius="none"
|
radius="none"
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? "solid" : "soft"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const mutated = [
|
const mutated = [...shells] as TankFilters["shells"];
|
||||||
tankFilters.shells[0],
|
|
||||||
tankFilters.shells[1],
|
|
||||||
tankFilters.shells[2],
|
|
||||||
] as TankFilters['shells'];
|
|
||||||
|
|
||||||
mutated[index] = selected ? null : shellType;
|
mutated[index] = selected ? null : shellType;
|
||||||
|
|
||||||
$tankFilters.setKey('shells', mutated);
|
TankFilters.mutate((draft) => {
|
||||||
|
draft.shells = mutated;
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
highContrast={selected}
|
highContrast={selected}
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? undefined : "gray"}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={asset(
|
src={asset(
|
||||||
`icons/shells/${shellTypeIcons[shellType]}${premium ? '_premium' : ''}.webp`,
|
`icons/shells/${shellTypeIcons[shellType]}${premium ? "_premium" : ""}.webp`
|
||||||
)}
|
)}
|
||||||
style={{ width: '1em', height: '1em' }}
|
style={{ width: "1em", height: "1em" }}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
@@ -119,26 +109,26 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FilterControl({ compact }: FilterControlProps) {
|
export function FilterControl({ compact }: FilterControlProps) {
|
||||||
const tankFilters = useStore($tankFilters);
|
const tankFilters = TankFilters.use();
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
const clearable = !isEqual(tankFilters, initialTankFilters);
|
const clearable = !isEqual(tankFilters, TankFilters.initial);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" align="center">
|
<Flex direction="column" align="center">
|
||||||
<Flex
|
<Flex
|
||||||
height="fit-content"
|
height="fit-content"
|
||||||
gap="2"
|
gap="2"
|
||||||
align={{ initial: 'start', md: 'center' }}
|
align={{ initial: "start", md: "center" }}
|
||||||
justify="center"
|
justify="center"
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
flexShrink="0"
|
flexShrink="0"
|
||||||
direction={compact ? 'row' : { initial: 'row', md: 'column' }}
|
direction={compact ? "row" : { initial: "row", md: "column" }}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: 'var(--radius-5)' }}
|
style={{ borderRadius: "var(--radius-5)" }}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
{times(5, (index) => {
|
{times(5, (index) => {
|
||||||
const tier = index + 1;
|
const tier = index + 1;
|
||||||
@@ -147,21 +137,19 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
key={tier}
|
key={tier}
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.tiers.includes(tier)) {
|
if (tankFilters.tiers.includes(tier)) {
|
||||||
$tankFilters.setKey(
|
TankFilters.mutate((draft) => {
|
||||||
'tiers',
|
draft.tiers = draft.tiers.filter((t) => t !== tier);
|
||||||
tankFilters.tiers.filter((t) => t !== tier),
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('tiers', [
|
TankFilters.mutate((draft) => {
|
||||||
...tankFilters.tiers,
|
draft.tiers = [...draft.tiers, tier];
|
||||||
tier,
|
});
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -172,15 +160,15 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
display={{ initial: 'none', md: 'block' }}
|
display={{ initial: "none", md: "block" }}
|
||||||
style={{
|
style={{
|
||||||
height: Var('space-1'),
|
height: Var("space-1"),
|
||||||
backgroundColor: Var('gray-a3'),
|
backgroundColor: Var("gray-a3"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
{times(5, (index) => {
|
{times(5, (index) => {
|
||||||
const tier = index + 6;
|
const tier = index + 6;
|
||||||
@@ -189,21 +177,19 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
key={tier}
|
key={tier}
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.tiers.includes(tier)) {
|
if (tankFilters.tiers.includes(tier)) {
|
||||||
$tankFilters.setKey(
|
TankFilters.mutate((draft) => {
|
||||||
'tiers',
|
draft.tiers = draft.tiers.filter((t) => t !== tier);
|
||||||
tankFilters.tiers.filter((t) => t !== tier),
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('tiers', [
|
TankFilters.mutate((draft) => {
|
||||||
...tankFilters.tiers,
|
draft.tiers = [...draft.tiers, tier];
|
||||||
tier,
|
});
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -216,12 +202,12 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
flexShrink="0"
|
flexShrink="0"
|
||||||
direction={compact ? 'row' : { initial: 'row', md: 'column' }}
|
direction={compact ? "row" : { initial: "row", md: "column" }}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: 'var(--radius-5)' }}
|
style={{ borderRadius: "var(--radius-5)" }}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
{gameDefinitions.nations
|
{gameDefinitions.nations
|
||||||
.slice(0, Math.ceil(gameDefinitions.nations.length / 2))
|
.slice(0, Math.ceil(gameDefinitions.nations.length / 2))
|
||||||
@@ -231,26 +217,24 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
key={nation}
|
key={nation}
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? "solid" : "soft"}
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
radius="none"
|
radius="none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.nations.includes(nation)) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.nations.includes(nation)) {
|
||||||
'nations',
|
draft.nations = draft.nations.filter(
|
||||||
tankFilters.nations.filter((n) => n !== nation),
|
(n) => n !== nation
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('nations', [
|
draft.nations = [...draft.nations, nation];
|
||||||
...tankFilters.nations,
|
}
|
||||||
nation,
|
});
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
style={{ width: '1em', height: '1em' }}
|
style={{ width: "1em", height: "1em" }}
|
||||||
src={asset(`flags/circle/${nation}.webp`)}
|
src={asset(`flags/circle/${nation}.webp`)}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -259,15 +243,15 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
display={{ initial: 'none', md: 'block' }}
|
display={{ initial: "none", md: "block" }}
|
||||||
style={{
|
style={{
|
||||||
height: Var('space-1'),
|
height: Var("space-1"),
|
||||||
backgroundColor: Var('gray-a3'),
|
backgroundColor: Var("gray-a3"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
{gameDefinitions.nations
|
{gameDefinitions.nations
|
||||||
.slice(Math.ceil(gameDefinitions.nations.length / 2))
|
.slice(Math.ceil(gameDefinitions.nations.length / 2))
|
||||||
@@ -278,26 +262,24 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
<IconButton
|
<IconButton
|
||||||
key={nation}
|
key={nation}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? "solid" : "soft"}
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
radius="none"
|
radius="none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.nations.includes(nation)) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.nations.includes(nation)) {
|
||||||
'nations',
|
draft.nations = draft.nations.filter(
|
||||||
tankFilters.nations.filter((n) => n !== nation),
|
(n) => n !== nation
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('nations', [
|
draft.nations = [...draft.nations, nation];
|
||||||
...tankFilters.nations,
|
}
|
||||||
nation,
|
});
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
style={{ width: '1em', height: '1em' }}
|
style={{ width: "1em", height: "1em" }}
|
||||||
src={asset(`flags/circle/${nation}.webp`)}
|
src={asset(`flags/circle/${nation}.webp`)}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -307,15 +289,15 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
direction={{ initial: 'row', md: 'column' }}
|
direction={compact ? "row" : { initial: "row", md: "column" }}
|
||||||
align="start"
|
align="start"
|
||||||
gap={{ initial: '2', md: '1' }}
|
gap={{ initial: "2", md: "1" }}
|
||||||
>
|
>
|
||||||
<Flex gap="2" align="start">
|
<Flex gap="2" align="start">
|
||||||
<Flex
|
<Flex
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: 'var(--radius-full)' }}
|
style={{ borderRadius: "var(--radius-full)" }}
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
{TANK_CLASSES.map((tankClass) => {
|
{TANK_CLASSES.map((tankClass) => {
|
||||||
const Icon = classIcons[tankClass];
|
const Icon = classIcons[tankClass];
|
||||||
@@ -324,25 +306,23 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
key={tankClass}
|
key={tankClass}
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.classes.includes(tankClass)) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.classes.includes(tankClass)) {
|
||||||
'classes',
|
draft.classes = draft.classes.filter(
|
||||||
tankFilters.classes.filter((c) => c !== tankClass),
|
(c) => c !== tankClass
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('classes', [
|
draft.classes = [...draft.classes, tankClass];
|
||||||
...tankFilters.classes,
|
}
|
||||||
tankClass,
|
});
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon style={{ width: '1em', height: '1em' }} />
|
<Icon style={{ width: "1em", height: "1em" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -350,96 +330,90 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: 'var(--radius-full)' }}
|
style={{ borderRadius: "var(--radius-full)" }}
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={
|
variant={
|
||||||
tankFilters.gunType.includes('regular') ? 'solid' : 'soft'
|
tankFilters.gunType.includes("regular") ? "solid" : "soft"
|
||||||
}
|
}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={
|
color={
|
||||||
tankFilters.gunType.includes('regular') ? undefined : 'gray'
|
tankFilters.gunType.includes("regular") ? undefined : "gray"
|
||||||
}
|
}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.gunType.includes('regular')) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.gunType.includes("regular")) {
|
||||||
'gunType',
|
draft.gunType = draft.gunType.filter(
|
||||||
tankFilters.gunType.filter((t) => t !== 'regular'),
|
(t) => t !== "regular"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('gunType', [
|
draft.gunType = [...draft.gunType, "regular"];
|
||||||
...tankFilters.gunType,
|
}
|
||||||
'regular',
|
});
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GunRegularIcon style={{ width: '1em', height: '1em' }} />
|
<GunRegularIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={
|
variant={
|
||||||
tankFilters.gunType.includes('auto_loader') ? 'solid' : 'soft'
|
tankFilters.gunType.includes("auto_loader") ? "solid" : "soft"
|
||||||
}
|
}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={
|
color={
|
||||||
tankFilters.gunType.includes('auto_loader')
|
tankFilters.gunType.includes("auto_loader")
|
||||||
? undefined
|
? undefined
|
||||||
: 'gray'
|
: "gray"
|
||||||
}
|
}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.gunType.includes('auto_loader')) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.gunType.includes("auto_loader")) {
|
||||||
'gunType',
|
draft.gunType = draft.gunType.filter(
|
||||||
tankFilters.gunType.filter((t) => t !== 'auto_loader'),
|
(t) => t !== "auto_loader"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('gunType', [
|
draft.gunType = [...draft.gunType, "auto_loader"];
|
||||||
...tankFilters.gunType,
|
}
|
||||||
'auto_loader',
|
});
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GunAutoloaderIcon style={{ width: '1em', height: '1em' }} />
|
<GunAutoloaderIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={
|
variant={
|
||||||
tankFilters.gunType.includes('auto_reloader')
|
tankFilters.gunType.includes("auto_reloader")
|
||||||
? 'solid'
|
? "solid"
|
||||||
: 'soft'
|
: "soft"
|
||||||
}
|
}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={
|
color={
|
||||||
tankFilters.gunType.includes('auto_reloader')
|
tankFilters.gunType.includes("auto_reloader")
|
||||||
? undefined
|
? undefined
|
||||||
: 'gray'
|
: "gray"
|
||||||
}
|
}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.gunType.includes('auto_reloader')) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.gunType.includes("auto_reloader")) {
|
||||||
'gunType',
|
draft.gunType = draft.gunType.filter(
|
||||||
tankFilters.gunType.filter((t) => t !== 'auto_reloader'),
|
(t) => t !== "auto_reloader"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('gunType', [
|
draft.gunType = [...draft.gunType, "auto_reloader"];
|
||||||
...tankFilters.gunType,
|
}
|
||||||
'auto_reloader',
|
});
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GunAutoreloaderIcon style={{ width: '1em', height: '1em' }} />
|
<GunAutoreloaderIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: 'var(--radius-full)' }}
|
style={{ borderRadius: "var(--radius-full)" }}
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
<ShellFilter index={0} />
|
<ShellFilter index={0} />
|
||||||
<ShellFilter index={1} premium />
|
<ShellFilter index={1} premium />
|
||||||
@@ -450,160 +424,158 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
<Flex gap="2" align="start">
|
<Flex gap="2" align="start">
|
||||||
<Flex
|
<Flex
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: 'var(--radius-full)' }}
|
style={{ borderRadius: "var(--radius-full)" }}
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={
|
variant={
|
||||||
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
||||||
? 'solid'
|
? "solid"
|
||||||
: 'soft'
|
: "soft"
|
||||||
}
|
}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={
|
color={
|
||||||
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
||||||
? undefined
|
? undefined
|
||||||
: 'gray'
|
: "gray"
|
||||||
}
|
}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.types.includes(TankType.RESEARCHABLE)) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.types.includes(TankType.RESEARCHABLE)) {
|
||||||
'types',
|
draft.types = draft.types.filter(
|
||||||
tankFilters.types.filter(
|
(t) => t !== TankType.RESEARCHABLE
|
||||||
(t) => t !== TankType.RESEARCHABLE,
|
);
|
||||||
),
|
} else {
|
||||||
);
|
draft.types = [...draft.types, TankType.RESEARCHABLE];
|
||||||
} else {
|
}
|
||||||
$tankFilters.setKey('types', [
|
});
|
||||||
...tankFilters.types,
|
|
||||||
TankType.RESEARCHABLE,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
|
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={
|
variant={
|
||||||
tankFilters.types?.includes(TankType.PREMIUM)
|
tankFilters.types?.includes(TankType.PREMIUM)
|
||||||
? 'solid'
|
? "solid"
|
||||||
: 'soft'
|
: "soft"
|
||||||
}
|
}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={
|
color={
|
||||||
tankFilters.types?.includes(TankType.PREMIUM)
|
tankFilters.types?.includes(TankType.PREMIUM)
|
||||||
? 'amber'
|
? "amber"
|
||||||
: 'gray'
|
: "gray"
|
||||||
}
|
}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.types.includes(TankType.PREMIUM)) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.types.includes(TankType.PREMIUM)) {
|
||||||
'types',
|
draft.types = draft.types.filter(
|
||||||
tankFilters.types.filter((t) => t !== TankType.PREMIUM),
|
(t) => t !== TankType.PREMIUM
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('types', [
|
draft.types = [...draft.types, TankType.PREMIUM];
|
||||||
...tankFilters.types,
|
}
|
||||||
TankType.PREMIUM,
|
});
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
color={
|
color={
|
||||||
tankFilters.types?.includes(TankType.PREMIUM)
|
tankFilters.types?.includes(TankType.PREMIUM)
|
||||||
? undefined
|
? undefined
|
||||||
: 'amber'
|
: "amber"
|
||||||
}
|
}
|
||||||
style={{ display: 'flex', justifyContent: 'center' }}
|
style={{ display: "flex", justifyContent: "center" }}
|
||||||
>
|
>
|
||||||
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
|
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</Text>
|
</Text>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={
|
variant={
|
||||||
tankFilters.types?.includes(TankType.COLLECTOR)
|
tankFilters.types?.includes(TankType.COLLECTOR)
|
||||||
? 'solid'
|
? "solid"
|
||||||
: 'soft'
|
: "soft"
|
||||||
}
|
}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={
|
color={
|
||||||
tankFilters.types?.includes(TankType.COLLECTOR)
|
tankFilters.types?.includes(TankType.COLLECTOR)
|
||||||
? 'blue'
|
? "blue"
|
||||||
: 'gray'
|
: "gray"
|
||||||
}
|
}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.types.includes(TankType.COLLECTOR)) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.types.includes(TankType.COLLECTOR)) {
|
||||||
'types',
|
draft.types = draft.types.filter(
|
||||||
tankFilters.types.filter((t) => t !== TankType.COLLECTOR),
|
(t) => t !== TankType.COLLECTOR
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('types', [
|
draft.types = [...draft.types, TankType.COLLECTOR];
|
||||||
...tankFilters.types,
|
}
|
||||||
TankType.COLLECTOR,
|
});
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
color={
|
color={
|
||||||
tankFilters.types?.includes(TankType.COLLECTOR)
|
tankFilters.types?.includes(TankType.COLLECTOR)
|
||||||
? undefined
|
? undefined
|
||||||
: 'blue'
|
: "blue"
|
||||||
}
|
}
|
||||||
style={{ display: 'flex', justifyContent: 'center' }}
|
style={{ display: "flex", justifyContent: "center" }}
|
||||||
>
|
>
|
||||||
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
|
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</Text>
|
</Text>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: 'var(--radius-full)' }}
|
style={{ borderRadius: "var(--radius-full)" }}
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={tankFilters.testing === 'include' ? 'solid' : 'soft'}
|
variant={tankFilters.testing === "include" ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={tankFilters.testing === 'include' ? undefined : 'gray'}
|
color={tankFilters.testing === "include" ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
$tankFilters.setKey('testing', 'include');
|
TankFilters.mutate((draft) => {
|
||||||
|
draft.testing = "include";
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ExperimentIcon
|
<ExperimentIcon
|
||||||
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={tankFilters.testing === 'exclude' ? 'solid' : 'soft'}
|
variant={tankFilters.testing === "exclude" ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={tankFilters.testing === 'exclude' ? undefined : 'gray'}
|
color={tankFilters.testing === "exclude" ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
$tankFilters.setKey('testing', 'exclude');
|
TankFilters.mutate((draft) => {
|
||||||
|
draft.testing = "exclude";
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ScienceOffIcon
|
<ScienceOffIcon
|
||||||
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={tankFilters.testing === 'only' ? 'solid' : 'soft'}
|
variant={tankFilters.testing === "only" ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={tankFilters.testing === 'only' ? undefined : 'gray'}
|
color={tankFilters.testing === "only" ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
$tankFilters.setKey('testing', 'only');
|
TankFilters.mutate((draft) => {
|
||||||
|
draft.testing = "only";
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ScienceIcon
|
<ScienceIcon
|
||||||
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -619,8 +591,8 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
href="#"
|
href="#"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
$tankFilters.set(initialTankFilters);
|
TankFilters.set(TankFilters.initial);
|
||||||
$tankopediaSort.set(initialTankopediaSort);
|
TankSort.set(TankSort.initial);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{strings.website.common.tank_search.clear_filters}
|
{strings.website.common.tank_search.clear_filters}
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
import { Flex, Link, Text } from '@radix-ui/themes';
|
import { Flex, Link, Text } from "@radix-ui/themes";
|
||||||
import { useLocale } from '../../../hooks/useLocale';
|
import { useLocale } from "../../../hooks/useLocale";
|
||||||
import { $tankFilters, initialTankFilters } from '../../../stores/tankFilters';
|
import { TankFilters } from "../../../stores/tankFilters";
|
||||||
|
|
||||||
interface NoResultsProps {
|
interface NoResultsProps {
|
||||||
type?: 'filters' | 'search';
|
type?: "filters" | "search";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NoResults({ type = 'filters' }: NoResultsProps) {
|
export function NoResults({ type = "filters" }: NoResultsProps) {
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flexGrow="1" align="center" justify="center">
|
<Flex flexGrow="1" align="center" justify="center">
|
||||||
<Text color="gray">
|
<Text color="gray">
|
||||||
{strings.website.common.tank_search.no_tanks_found.body}{' '}
|
{strings.website.common.tank_search.no_tanks_found.body}{" "}
|
||||||
<Link
|
<Link
|
||||||
href="#"
|
href="#"
|
||||||
underline="always"
|
underline="always"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
$tankFilters.set(initialTankFilters);
|
TankFilters.set(TankFilters.initial);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{type === 'filters'
|
{type === "filters"
|
||||||
? strings.website.common.tank_search.no_tanks_found.clear_filters
|
? strings.website.common.tank_search.no_tanks_found.clear_filters
|
||||||
: strings.website.common.tank_search.no_tanks_found.search_again}
|
: strings.website.common.tank_search.no_tanks_found.search_again}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { type TankDefinition } from '@blitzkit/core';
|
import { type TankDefinition } from "@blitzkit/core";
|
||||||
import { useStore } from '@nanostores/react';
|
import { CaretRightIcon } from "@radix-ui/react-icons";
|
||||||
import { CaretRightIcon } from '@radix-ui/react-icons';
|
import { Button, TextField } from "@radix-ui/themes";
|
||||||
import { Button, TextField } from '@radix-ui/themes';
|
import { useLocale } from "../../../hooks/useLocale";
|
||||||
import { useLocale } from '../../../hooks/useLocale';
|
import { TankFilters } from "../../../stores/tankFilters";
|
||||||
import { $tankFilters } from '../../../stores/tankFilters';
|
import { LinkI18n } from "../../LinkI18n";
|
||||||
import { LinkI18n } from '../../LinkI18n';
|
|
||||||
|
|
||||||
interface QuickLinkProps {
|
interface QuickLinkProps {
|
||||||
topResult?: TankDefinition;
|
topResult?: TankDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QuickLink({ topResult }: QuickLinkProps) {
|
export function QuickLink({ topResult }: QuickLinkProps) {
|
||||||
const tankFilters = useStore($tankFilters);
|
|
||||||
const { locale, unwrap } = useLocale();
|
const { locale, unwrap } = useLocale();
|
||||||
|
const search = TankFilters.use((state) => state.search);
|
||||||
|
const searching = TankFilters.use((state) => state.searching);
|
||||||
|
|
||||||
if (!tankFilters.search || !topResult || tankFilters.searching) return null;
|
if (!search || !topResult || searching) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField.Slot>
|
<TextField.Slot>
|
||||||
|
|||||||
@@ -1,38 +1,37 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
import { Flex, Separator, Text } from "@radix-ui/themes";
|
||||||
import { Flex, Separator, Text } from '@radix-ui/themes';
|
import { isEqual } from "lodash-es";
|
||||||
import { isEqual } from 'lodash-es';
|
import { useMemo } from "react";
|
||||||
import { useMemo } from 'react';
|
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
||||||
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
|
import { useLocale } from "../../../hooks/useLocale";
|
||||||
import { useLocale } from '../../../hooks/useLocale';
|
import { TankFilters } from "../../../stores/tankFilters";
|
||||||
import { $tankFilters, initialTankFilters } from '../../../stores/tankFilters';
|
import { TankopediaPersistent } from "../../../stores/tankopediaPersistent";
|
||||||
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
import { TankSort } from "../../../stores/tankopediaSort";
|
||||||
import { $tankopediaSort } from '../../../stores/tankopediaSort';
|
import { TankCard } from "../../TankCard";
|
||||||
import { TankCard } from '../../TankCard';
|
import { TankCardWrapper } from "./TankCardWrapper";
|
||||||
import { TankCardWrapper } from './TankCardWrapper';
|
|
||||||
|
|
||||||
const tankDefinitions = await awaitableTankDefinitions;
|
const tankDefinitions = await awaitableTankDefinitions;
|
||||||
|
|
||||||
export function RecentlyViewed() {
|
export function RecentlyViewed() {
|
||||||
const tankopediaPersistentStore = TankopediaPersistent.useStore();
|
const tankopediaPersistentStore = TankopediaPersistent.useStore();
|
||||||
const filters = useStore($tankFilters);
|
const filters = TankFilters.use();
|
||||||
// non-reactive because it is a little weird that it updates instantly even before the page loads
|
// non-reactive because it is a little weird that it updates instantly even before the page loads
|
||||||
const recentlyViewed = tankopediaPersistentStore
|
const recentlyViewed = tankopediaPersistentStore
|
||||||
.getState()
|
.getState()
|
||||||
.recentlyViewed.filter((id) => id in tankDefinitions.tanks);
|
.recentlyViewed.filter((id) => id in tankDefinitions.tanks);
|
||||||
const sort = useStore($tankopediaSort);
|
|
||||||
const hasFilters = useMemo(
|
const hasFilters = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Object.entries(filters).some(([key, value]) => {
|
Object.entries(filters).some(([key, value]) => {
|
||||||
return !isEqual(
|
return !isEqual(
|
||||||
value,
|
value,
|
||||||
initialTankFilters[key as keyof typeof initialTankFilters],
|
TankFilters.initial[key as keyof typeof TankFilters.initial]
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
[filters],
|
[filters]
|
||||||
);
|
);
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
const by = TankSort.use((state) => state.by);
|
||||||
|
|
||||||
if (recentlyViewed.length === 0 || sort.by !== 'meta.none' || hasFilters) {
|
if (recentlyViewed.length === 0 || by !== "meta.none" || hasFilters) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import type { TankDefinition } from '@blitzkit/core';
|
import type { TankDefinition } from "@blitzkit/core";
|
||||||
import { useStore } from '@nanostores/react';
|
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||||
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
import { Flex, Spinner, TextField } from "@radix-ui/themes";
|
||||||
import { Flex, Spinner, TextField } from '@radix-ui/themes';
|
import { debounce } from "lodash-es";
|
||||||
import { debounce } from 'lodash-es';
|
import { type KeyboardEventHandler, useCallback, useRef } from "react";
|
||||||
import { type KeyboardEventHandler, useCallback, useRef } from 'react';
|
import { useLocale } from "../../../hooks/useLocale";
|
||||||
import { useLocale } from '../../../hooks/useLocale';
|
import { TankFilters } from "../../../stores/tankFilters";
|
||||||
import { $tankFilters } from '../../../stores/tankFilters';
|
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||||
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
import { QuickLink } from "./QuickLink";
|
||||||
import { QuickLink } from './QuickLink';
|
import { Sort } from "./Sort";
|
||||||
import { Sort } from './Sort';
|
|
||||||
|
|
||||||
type SearchBarProps = MaybeSkeletonComponentProps & {
|
type SearchBarProps = MaybeSkeletonComponentProps & {
|
||||||
topResult?: TankDefinition;
|
topResult?: TankDefinition;
|
||||||
@@ -17,30 +16,37 @@ type SearchBarProps = MaybeSkeletonComponentProps & {
|
|||||||
|
|
||||||
export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
const tankFilters = useStore($tankFilters);
|
const search = TankFilters.use((state) => state.search);
|
||||||
|
const searching = TankFilters.use((state) => state.searching);
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const performSearch = useCallback(
|
const performSearch = useCallback(
|
||||||
debounce(() => {
|
debounce(() => {
|
||||||
$tankFilters.setKey('searching', false);
|
TankFilters.mutate((draft) => {
|
||||||
|
draft.searching = false;
|
||||||
|
});
|
||||||
|
|
||||||
if (!input.current) return;
|
if (!input.current) return;
|
||||||
|
|
||||||
const sanitized = input.current.value.trim();
|
const sanitized = input.current.value.trim();
|
||||||
|
|
||||||
$tankFilters.setKey('search', sanitized.length === 0 ? null : sanitized);
|
TankFilters.mutate((draft) => {
|
||||||
|
draft.search = sanitized.length === 0 ? null : sanitized;
|
||||||
|
});
|
||||||
}, 500),
|
}, 500),
|
||||||
[],
|
[]
|
||||||
);
|
);
|
||||||
const handleChange = useCallback(() => {
|
const handleChange = useCallback(() => {
|
||||||
if (!tankFilters.searching) {
|
if (!searching) {
|
||||||
$tankFilters.setKey('searching', true);
|
TankFilters.mutate((draft) => {
|
||||||
|
draft.searching = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
performSearch();
|
performSearch();
|
||||||
}, [tankFilters]);
|
}, [searching]);
|
||||||
const handleKeyDown = useCallback<KeyboardEventHandler>(
|
const handleKeyDown = useCallback<KeyboardEventHandler>(
|
||||||
(event) => {
|
(event) => {
|
||||||
if (event.key !== 'Enter' || !topResult || tankFilters.searching) return;
|
if (event.key !== "Enter" || !topResult || searching) return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -50,7 +56,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
|||||||
window.location.href = `/tanks/${topResult.slug}`;
|
window.location.href = `/tanks/${topResult.slug}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[topResult],
|
[topResult]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -59,7 +65,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
|||||||
<TextField.Root
|
<TextField.Root
|
||||||
variant="classic"
|
variant="classic"
|
||||||
disabled={skeleton}
|
disabled={skeleton}
|
||||||
defaultValue={tankFilters.search ?? undefined}
|
defaultValue={search ?? undefined}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
ref={input}
|
ref={input}
|
||||||
placeholder={strings.website.common.tank_search.search_bar_hint}
|
placeholder={strings.website.common.tank_search.search_bar_hint}
|
||||||
@@ -67,7 +73,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
<TextField.Slot>
|
<TextField.Slot>
|
||||||
{tankFilters.searching ? <Spinner /> : <MagnifyingGlassIcon />}
|
{searching ? <Spinner /> : <MagnifyingGlassIcon />}
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
|
|
||||||
<QuickLink topResult={topResult} />
|
<QuickLink topResult={topResult} />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { CaretDownIcon } from '@radix-ui/react-icons';
|
import { CaretDownIcon } from '@radix-ui/react-icons';
|
||||||
import { Button, DropdownMenu } from '@radix-ui/themes';
|
import { Button, DropdownMenu } from '@radix-ui/themes';
|
||||||
import { useLocale } from '../../../hooks/useLocale';
|
import { useLocale } from '../../../hooks/useLocale';
|
||||||
@@ -6,7 +5,7 @@ import type {
|
|||||||
TankopediaSortBy,
|
TankopediaSortBy,
|
||||||
TankopediaSortDirection,
|
TankopediaSortDirection,
|
||||||
} from '../../../stores/tankopediaPersistent';
|
} from '../../../stores/tankopediaPersistent';
|
||||||
import { $tankopediaSort } from '../../../stores/tankopediaSort';
|
import { TankSort } from '../../../stores/tankopediaSort';
|
||||||
|
|
||||||
interface ItemProps {
|
interface ItemProps {
|
||||||
by: TankopediaSortBy;
|
by: TankopediaSortBy;
|
||||||
@@ -18,13 +17,15 @@ interface ItemProps {
|
|||||||
|
|
||||||
function Item({ by }: ItemProps) {
|
function Item({ by }: ItemProps) {
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
const tankopediaSort = useStore($tankopediaSort);
|
const _by = TankSort.use((state) => state.by);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
checked={tankopediaSort.by === by}
|
checked={_by === by}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
$tankopediaSort.setKey('by', by);
|
TankSort.mutate((draft) => {
|
||||||
|
draft.by = by;
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{strings.website.common.tank_search.sort[by]}
|
{strings.website.common.tank_search.sort[by]}
|
||||||
@@ -33,7 +34,7 @@ function Item({ by }: ItemProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Sort() {
|
export function Sort() {
|
||||||
const tankopediaSort = useStore($tankopediaSort);
|
const direction = TankSort.use((state) => state.direction);
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -119,12 +120,11 @@ export function Sort() {
|
|||||||
}
|
}
|
||||||
</DropdownMenu.Label>
|
</DropdownMenu.Label>
|
||||||
<DropdownMenu.RadioGroup
|
<DropdownMenu.RadioGroup
|
||||||
value={tankopediaSort.direction}
|
value={direction}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
$tankopediaSort.setKey(
|
TankSort.mutate((draft) => {
|
||||||
'direction',
|
draft.direction = value as TankopediaSortDirection;
|
||||||
value as TankopediaSortDirection,
|
});
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.RadioItem value="ascending">
|
<DropdownMenu.RadioItem value="ascending">
|
||||||
|
|||||||
@@ -1,44 +1,43 @@
|
|||||||
import { TIER_ROMAN_NUMERALS } from '@blitzkit/core';
|
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
||||||
import { useStore } from '@nanostores/react';
|
import { Flex, IconButton, Text } from "@radix-ui/themes";
|
||||||
import { Flex, IconButton, Text } from '@radix-ui/themes';
|
import { times } from "lodash-es";
|
||||||
import { times } from 'lodash-es';
|
import { memo } from "react";
|
||||||
import { memo } from 'react';
|
import { TankFilters } from "../../../stores/tankFilters";
|
||||||
import { $tankFilters } from '../../../stores/tankFilters';
|
import { TankSort } from "../../../stores/tankopediaSort";
|
||||||
import { $tankopediaSort } from '../../../stores/tankopediaSort';
|
|
||||||
|
|
||||||
export const TierFilter = memo(() => {
|
export const TierFilter = memo(() => {
|
||||||
const tankFilters = useStore($tankFilters);
|
const by = TankSort.use((state) => state.by);
|
||||||
const tankopediaSort = useStore($tankopediaSort);
|
const search = TankFilters.use((state) => state.search);
|
||||||
|
const tiers = TankFilters.use((state) => state.tiers);
|
||||||
|
|
||||||
if (tankopediaSort.by !== 'meta.none' || tankFilters.search) return null;
|
if (by !== "meta.none" || search) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex justify="center">
|
<Flex justify="center">
|
||||||
<Flex
|
<Flex
|
||||||
direction="row"
|
direction="row"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: 'var(--radius-full)' }}
|
style={{ borderRadius: "var(--radius-full)" }}
|
||||||
>
|
>
|
||||||
{times(10, (index) => {
|
{times(10, (index) => {
|
||||||
const tier = 10 - index;
|
const tier = 10 - index;
|
||||||
const selected = tankFilters.tiers.includes(tier);
|
const selected = tiers.includes(tier);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
key={tier}
|
key={tier}
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.tiers.includes(tier)) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tiers.includes(tier)) {
|
||||||
'tiers',
|
draft.tiers = tiers.filter((t) => t !== tier);
|
||||||
tankFilters.tiers.filter((t) => t !== tier),
|
} else {
|
||||||
);
|
draft.tiers = [...tiers, tier];
|
||||||
} else {
|
}
|
||||||
$tankFilters.setKey('tiers', [...tankFilters.tiers, tier]);
|
});
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="2">{TIER_ROMAN_NUMERALS[tier]}</Text>
|
<Text size="2">{TIER_ROMAN_NUMERALS[tier]}</Text>
|
||||||
|
|||||||
@@ -5,37 +5,36 @@ import {
|
|||||||
SEARCH_KEYS,
|
SEARCH_KEYS,
|
||||||
unionBoundingBox,
|
unionBoundingBox,
|
||||||
type TankDefinition,
|
type TankDefinition,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { literals } from '@blitzkit/i18n/src/literals';
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import { useStore } from '@nanostores/react';
|
import { Callout, Flex, Link, Text, type FlexProps } from "@radix-ui/themes";
|
||||||
import { Callout, Flex, Link, Text, type FlexProps } from '@radix-ui/themes';
|
import fuzzysort from "fuzzysort";
|
||||||
import fuzzysort from 'fuzzysort';
|
import { times, uniq } from "lodash-es";
|
||||||
import { times, uniq } from 'lodash-es';
|
import { memo, useEffect, useMemo, useState } from "react";
|
||||||
import { memo, useEffect, useMemo, useState } from 'react';
|
import { awaitableGameDefinitions } from "../../core/awaitables/gameDefinitions";
|
||||||
import { awaitableGameDefinitions } from '../../core/awaitables/gameDefinitions';
|
import { awaitableModelDefinitions } from "../../core/awaitables/modelDefinitions";
|
||||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
||||||
import { awaitableTankDefinitions } from '../../core/awaitables/tankDefinitions';
|
import { awaitableTankNames } from "../../core/awaitables/tankNames";
|
||||||
import { awaitableTankNames } from '../../core/awaitables/tankNames';
|
import { filterTank } from "../../core/blitzkit/filterTank";
|
||||||
import { filterTank } from '../../core/blitzkit/filterTank';
|
import { resolveReload } from "../../core/blitzkit/resolveReload";
|
||||||
import { resolveReload } from '../../core/blitzkit/resolveReload';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { TankFilters } from "../../stores/tankFilters";
|
||||||
import { $tankFilters } from '../../stores/tankFilters';
|
import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
|
||||||
import { TankopediaPersistent } from '../../stores/tankopediaPersistent';
|
import { SORT_UNITS } from "../../stores/tankopediaPersistent/constants";
|
||||||
import { SORT_UNITS } from '../../stores/tankopediaPersistent/constants';
|
import { TankSort } from "../../stores/tankopediaSort";
|
||||||
import { $tankopediaSort } from '../../stores/tankopediaSort';
|
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
import { ExperimentIcon } from "../ExperimentIcon";
|
||||||
import { ExperimentIcon } from '../ExperimentIcon';
|
import { TankSearchCard } from "./components/Card";
|
||||||
import { TankSearchCard } from './components/Card';
|
import { FilterControl } from "./components/FilterControl";
|
||||||
import { FilterControl } from './components/FilterControl';
|
import { NoResults } from "./components/NoResults";
|
||||||
import { NoResults } from './components/NoResults';
|
import { RecentlyViewed } from "./components/RecentlyViewed";
|
||||||
import { RecentlyViewed } from './components/RecentlyViewed';
|
import { SearchBar } from "./components/SearchBar";
|
||||||
import { SearchBar } from './components/SearchBar';
|
import { SkeletonTankCard } from "./components/SkeletonTankCard";
|
||||||
import { SkeletonTankCard } from './components/SkeletonTankCard';
|
import { TankCardWrapper } from "./components/TankCardWrapper";
|
||||||
import { TankCardWrapper } from './components/TankCardWrapper';
|
import { MAX_RECENTLY_VIEWED } from "./constants";
|
||||||
import { MAX_RECENTLY_VIEWED } from './constants';
|
|
||||||
|
|
||||||
type TankSearchProps = MaybeSkeletonComponentProps &
|
type TankSearchProps = MaybeSkeletonComponentProps &
|
||||||
Omit<FlexProps, 'onSelect'> & {
|
Omit<FlexProps, "onSelect"> & {
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
onSelect?: (tank: TankDefinition) => void;
|
onSelect?: (tank: TankDefinition) => void;
|
||||||
onSelectAll?: (tanks: TankDefinition[]) => void;
|
onSelectAll?: (tanks: TankDefinition[]) => void;
|
||||||
@@ -57,21 +56,21 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
const { strings, locale } = useLocale();
|
const { strings, locale } = useLocale();
|
||||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||||
const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks);
|
const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks);
|
||||||
const tankFilters = useStore($tankFilters);
|
const tankopediaSort = TankSort.use();
|
||||||
const tankopediaSort = useStore($tankopediaSort);
|
const tankFilters = TankFilters.use();
|
||||||
const tanksFiltered = useMemo(() => {
|
const tanksFiltered = useMemo(() => {
|
||||||
if (tankFilters.search === null) {
|
if (tankFilters.search === null) {
|
||||||
const filtered = awaitedTanksDefinitionsArray.filter((tank) =>
|
const filtered = awaitedTanksDefinitionsArray.filter((tank) =>
|
||||||
filterTank(tankFilters, tank),
|
filterTank(tankFilters, tank)
|
||||||
);
|
);
|
||||||
let sorted: TankDefinition[];
|
let sorted: TankDefinition[];
|
||||||
|
|
||||||
switch (tankopediaSort.by) {
|
switch (tankopediaSort.by) {
|
||||||
case 'meta.none':
|
case "meta.none":
|
||||||
sorted = metaSortTank(filtered, gameDefinitions);
|
sorted = metaSortTank(filtered, gameDefinitions);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'survivability.health': {
|
case "survivability.health": {
|
||||||
sorted = filtered.sort((a, b) => {
|
sorted = filtered.sort((a, b) => {
|
||||||
const aHealth = a.health + a.turrets.at(-1)!.health;
|
const aHealth = a.health + a.turrets.at(-1)!.health;
|
||||||
const bHealth = b.health + b.turrets.at(-1)!.health;
|
const bHealth = b.health + b.turrets.at(-1)!.health;
|
||||||
@@ -81,38 +80,36 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'survivability.viewRange':
|
case "survivability.viewRange":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.view_range - b.turrets.at(-1)!.view_range,
|
a.turrets.at(-1)!.view_range - b.turrets.at(-1)!.view_range
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'survivability.camouflageStill':
|
case "survivability.camouflageStill":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) => a.camouflage_still - b.camouflage_still,
|
(a, b) => a.camouflage_still - b.camouflage_still
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'survivability.camouflageMoving':
|
case "survivability.camouflageMoving":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) => a.camouflage_moving - b.camouflage_moving,
|
(a, b) => a.camouflage_moving - b.camouflage_moving
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'survivability.camouflageShooting':
|
case "survivability.camouflageShooting":
|
||||||
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;
|
||||||
|
|
||||||
case 'survivability.volume':
|
case "survivability.volume":
|
||||||
sorted = filtered.sort((a, b) => {
|
sorted = filtered.sort((a, b) => {
|
||||||
const aTankModelDefinition = modelDefinitions.models[a.id];
|
const aTankModelDefinition = modelDefinitions.models[a.id];
|
||||||
const bTankModelDefinition = modelDefinitions.models[b.id];
|
const bTankModelDefinition = modelDefinitions.models[b.id];
|
||||||
@@ -123,14 +120,14 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
const aSize = normalizeBoundingBox(
|
const aSize = normalizeBoundingBox(
|
||||||
unionBoundingBox(
|
unionBoundingBox(
|
||||||
aTankModelDefinition.bounding_box,
|
aTankModelDefinition.bounding_box,
|
||||||
aTurretModelDefinition.bounding_box,
|
aTurretModelDefinition.bounding_box
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
const bSize = normalizeBoundingBox(
|
const bSize = normalizeBoundingBox(
|
||||||
unionBoundingBox(
|
unionBoundingBox(
|
||||||
bTankModelDefinition.bounding_box,
|
bTankModelDefinition.bounding_box,
|
||||||
bTurretModelDefinition.bounding_box,
|
bTurretModelDefinition.bounding_box
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
const aVolume = aSize.x * aSize.y * aSize.z;
|
const aVolume = aSize.x * aSize.y * aSize.z;
|
||||||
const bVolume = bSize.x * bSize.y * bSize.z;
|
const bVolume = bSize.x * bSize.y * bSize.z;
|
||||||
@@ -139,7 +136,7 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'survivability.length':
|
case "survivability.length":
|
||||||
sorted = filtered.sort((a, b) => {
|
sorted = filtered.sort((a, b) => {
|
||||||
const aTankModelDefinition = modelDefinitions.models[a.id];
|
const aTankModelDefinition = modelDefinitions.models[a.id];
|
||||||
const bTankModelDefinition = modelDefinitions.models[b.id];
|
const bTankModelDefinition = modelDefinitions.models[b.id];
|
||||||
@@ -150,14 +147,14 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
const aSize = normalizeBoundingBox(
|
const aSize = normalizeBoundingBox(
|
||||||
unionBoundingBox(
|
unionBoundingBox(
|
||||||
aTankModelDefinition.bounding_box,
|
aTankModelDefinition.bounding_box,
|
||||||
aTurretModelDefinition.bounding_box,
|
aTurretModelDefinition.bounding_box
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
const bSize = normalizeBoundingBox(
|
const bSize = normalizeBoundingBox(
|
||||||
unionBoundingBox(
|
unionBoundingBox(
|
||||||
bTankModelDefinition.bounding_box,
|
bTankModelDefinition.bounding_box,
|
||||||
bTurretModelDefinition.bounding_box,
|
bTurretModelDefinition.bounding_box
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
const aLength = Math.max(aSize.x, aSize.y, aSize.z);
|
const aLength = Math.max(aSize.x, aSize.y, aSize.z);
|
||||||
const bLength = Math.max(bSize.x, bSize.y, bSize.z);
|
const bLength = Math.max(bSize.x, bSize.y, bSize.z);
|
||||||
@@ -166,215 +163,193 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.dpm':
|
case "fire.dpm":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(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;
|
||||||
|
|
||||||
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)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.reload':
|
case "fire.reload":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
resolveReload(a.turrets.at(-1)!.guns.at(-1)!) -
|
resolveReload(a.turrets.at(-1)!.guns.at(-1)!) -
|
||||||
resolveReload(b.turrets.at(-1)!.guns.at(-1)!),
|
resolveReload(b.turrets.at(-1)!.guns.at(-1)!)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
case 'fire.dispersionMoving':
|
case "fire.dispersionMoving":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.tracks.at(-1)!.dispersion_move -
|
a.tracks.at(-1)!.dispersion_move -
|
||||||
b.tracks.at(-1)!.dispersion_move,
|
b.tracks.at(-1)!.dispersion_move
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.gunDepression':
|
case "fire.gunDepression":
|
||||||
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)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.gunElevation':
|
case "fire.gunElevation":
|
||||||
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)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'maneuverability.forwardsSpeed':
|
case "maneuverability.forwardsSpeed":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) => a.speed_forwards - b.speed_forwards,
|
(a, b) => a.speed_forwards - b.speed_forwards
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'maneuverability.backwardsSpeed':
|
case "maneuverability.backwardsSpeed":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) => a.speed_backwards - b.speed_backwards,
|
(a, b) => a.speed_backwards - b.speed_backwards
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'maneuverability.power':
|
case "maneuverability.power":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) => a.engines.at(-1)!.power - b.engines.at(-1)!.power,
|
(a, b) => a.engines.at(-1)!.power - b.engines.at(-1)!.power
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'maneuverability.powerToWeight':
|
case "maneuverability.powerToWeight":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.engines.at(-1)!.power /
|
a.engines.at(-1)!.power /
|
||||||
@@ -382,43 +357,42 @@ 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;
|
||||||
|
|
||||||
case 'maneuverability.weight':
|
case "maneuverability.weight":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.weight +
|
a.weight +
|
||||||
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;
|
||||||
|
|
||||||
case 'maneuverability.traverseSpeed':
|
case "maneuverability.traverseSpeed":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.tracks.at(-1)!.traverse_speed -
|
a.tracks.at(-1)!.traverse_speed -
|
||||||
b.tracks.at(-1)!.traverse_speed,
|
b.tracks.at(-1)!.traverse_speed
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tankopediaSort.direction === 'ascending'
|
return tankopediaSort.direction === "ascending"
|
||||||
? sorted
|
? sorted
|
||||||
: sorted.reverse();
|
: sorted.reverse();
|
||||||
} else {
|
} else {
|
||||||
@@ -426,7 +400,7 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
keys: SEARCH_KEYS,
|
keys: SEARCH_KEYS,
|
||||||
});
|
});
|
||||||
const searchedTanks = searchedRaw.map(
|
const searchedTanks = searchedRaw.map(
|
||||||
(result) => tankDefinitions.tanks[result.obj.id],
|
(result) => tankDefinitions.tanks[result.obj.id]
|
||||||
);
|
);
|
||||||
return searchedTanks;
|
return searchedTanks;
|
||||||
}
|
}
|
||||||
@@ -484,24 +458,24 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{tankopediaSort.by !== 'meta.none' && (
|
{tankopediaSort.by !== "meta.none" && (
|
||||||
<Text color="gray">
|
<Text color="gray">
|
||||||
{literals(strings.website.common.tank_search.sorting_by, [
|
{literals(strings.website.common.tank_search.sorting_by, [
|
||||||
strings.website.common.tank_search.sort[tankopediaSort.by],
|
strings.website.common.tank_search.sort[tankopediaSort.by],
|
||||||
])}
|
])}
|
||||||
{SORT_UNITS[tankopediaSort.by] === undefined
|
{SORT_UNITS[tankopediaSort.by] === undefined
|
||||||
? ''
|
? ""
|
||||||
: ` (${SORT_UNITS[tankopediaSort.by]})`}
|
: ` (${SORT_UNITS[tankopediaSort.by]})`}
|
||||||
, {tankopediaSort.direction}
|
, {tankopediaSort.direction}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{tankFilters.testing === 'only' && (
|
{tankFilters.testing === "only" && (
|
||||||
<Flex justify="center" mt="4">
|
<Flex justify="center" mt="4">
|
||||||
<Callout.Root color="amber">
|
<Callout.Root color="amber">
|
||||||
<Callout.Icon>
|
<Callout.Icon>
|
||||||
<ExperimentIcon style={{ width: '1em', height: '1em' }} />
|
<ExperimentIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</Callout.Icon>
|
</Callout.Icon>
|
||||||
<Callout.Text>
|
<Callout.Text>
|
||||||
{strings.website.common.warnings.test}
|
{strings.website.common.warnings.test}
|
||||||
@@ -530,12 +504,12 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
key={index}
|
key={index}
|
||||||
onIntersection={() => {
|
onIntersection={() => {
|
||||||
setLoadedCards((state) =>
|
setLoadedCards((state) =>
|
||||||
Math.min(state + 2, tanksFiltered.length),
|
Math.min(state + 2, tanksFiltered.length)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
)}
|
)}
|
||||||
</TankCardWrapper>
|
</TankCardWrapper>
|
||||||
)}
|
)}
|
||||||
@@ -548,15 +522,15 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
<TankCardWrapper>
|
<TankCardWrapper>
|
||||||
{times(
|
{times(
|
||||||
Math.round(
|
Math.round(
|
||||||
skeleton ? DEFAULT_LOADED_CARDS : 10 + 10 * Math.random(),
|
skeleton ? DEFAULT_LOADED_CARDS : 10 + 10 * Math.random()
|
||||||
),
|
),
|
||||||
(index) => (
|
(index) => (
|
||||||
<SkeletonTankCard key={index} />
|
<SkeletonTankCard key={index} />
|
||||||
),
|
)
|
||||||
)}
|
)}
|
||||||
</TankCardWrapper>
|
</TankCardWrapper>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
+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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+12
-15
@@ -1,19 +1,16 @@
|
|||||||
import { TIER_ROMAN_NUMERALS } from '@blitzkit/core';
|
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
||||||
import { literals } from '@blitzkit/i18n/src/literals';
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import { Flex, SegmentedControl, Text, type FlexProps } from '@radix-ui/themes';
|
import { Flex, SegmentedControl, Text, type FlexProps } from "@radix-ui/themes";
|
||||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||||
import { Duel } from '../../../../../../stores/duel';
|
import { Duel } from "../../../../../../stores/duel";
|
||||||
import {
|
import {
|
||||||
TankopediaEphemeral,
|
Tankopedia,
|
||||||
TankopediaRelativeAgainst,
|
TankopediaRelativeAgainst,
|
||||||
} from '../../../../../../stores/tankopediaEphemeral';
|
} from "../../../../../../stores/tankopedia";
|
||||||
|
|
||||||
export function CompareOptions(props: FlexProps) {
|
export function CompareOptions(props: FlexProps) {
|
||||||
const relativeAgainst = TankopediaEphemeral.use(
|
const relativeAgainst = Tankopedia.use((state) => state.relativeAgainst);
|
||||||
(state) => state.relativeAgainst,
|
|
||||||
);
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -21,7 +18,7 @@ export function CompareOptions(props: FlexProps) {
|
|||||||
align="center"
|
align="center"
|
||||||
gap="2"
|
gap="2"
|
||||||
justify="center"
|
justify="center"
|
||||||
direction={{ initial: 'column', sm: 'row' }}
|
direction={{ initial: "column", sm: "row" }}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Text color="gray">
|
<Text color="gray">
|
||||||
@@ -33,7 +30,7 @@ export function CompareOptions(props: FlexProps) {
|
|||||||
size="1"
|
size="1"
|
||||||
value={`${relativeAgainst}`}
|
value={`${relativeAgainst}`}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.relativeAgainst = Number(value) as TankopediaRelativeAgainst;
|
draft.relativeAgainst = Number(value) as TankopediaRelativeAgainst;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -44,13 +41,13 @@ export function CompareOptions(props: FlexProps) {
|
|||||||
[
|
[
|
||||||
TIER_ROMAN_NUMERALS[tank.tier],
|
TIER_ROMAN_NUMERALS[tank.tier],
|
||||||
strings.common.tank_class_short_plural_lowercase[tank.class],
|
strings.common.tank_class_short_plural_lowercase[tank.class],
|
||||||
],
|
]
|
||||||
)}
|
)}
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.Tier}`}>
|
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.Tier}`}>
|
||||||
{literals(
|
{literals(
|
||||||
strings.website.tools.tankopedia.characteristics.compare.tier,
|
strings.website.tools.tankopedia.characteristics.compare.tier,
|
||||||
[TIER_ROMAN_NUMERALS[tank.tier]],
|
[TIER_ROMAN_NUMERALS[tank.tier]]
|
||||||
)}
|
)}
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.All}`}>
|
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.All}`}>
|
||||||
|
|||||||
+21
-125
@@ -1,62 +1,31 @@
|
|||||||
import {
|
import { asset, isExplosive } from '@blitzkit/core';
|
||||||
asset,
|
|
||||||
coefficient,
|
|
||||||
isExplosive,
|
|
||||||
resolvePenetrationCoefficient,
|
|
||||||
} 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 { useEffect, useRef, useState } from 'react';
|
|
||||||
import { lerp } from 'three/src/math/MathUtils.js';
|
|
||||||
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 turret = Duel.use((state) => state.protagonist.turret);
|
||||||
const setPenetrationDistanceDebounced = debounce((value: number) => {
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
setPenetrationDistance(value);
|
const shell = Duel.use((state) => state.protagonist.shell);
|
||||||
}, 500);
|
|
||||||
const { gun, shell, turret } = Duel.use((state) => state.protagonist);
|
|
||||||
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
|
|
||||||
const hasSupercharger = useEquipment(107);
|
|
||||||
const hasCalibratedShells = useEquipment(103);
|
|
||||||
const penetrationCoefficient = coefficient([
|
|
||||||
hasCalibratedShells,
|
|
||||||
resolvePenetrationCoefficient(true, shell.type) - 1,
|
|
||||||
]);
|
|
||||||
const penetrationLossOverDistanceCoefficient = coefficient([
|
|
||||||
hasSupercharger,
|
|
||||||
-0.6,
|
|
||||||
]);
|
|
||||||
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(() => {
|
|
||||||
if (penetrationDistanceInput.current) {
|
|
||||||
penetrationDistanceInput.current.value = `${penetrationDistance}`;
|
|
||||||
}
|
|
||||||
}, [penetrationDistance]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatsTableWrapper>
|
<StatsTableWrapper>
|
||||||
@@ -66,7 +35,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 +48,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 +85,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 +147,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 +237,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 +298,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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+19
-19
@@ -1,10 +1,10 @@
|
|||||||
import { CaretDownIcon, CaretUpIcon } from '@radix-ui/react-icons';
|
import { CaretDownIcon, CaretUpIcon } from "@radix-ui/react-icons";
|
||||||
import { Box, Flex, Text } from '@radix-ui/themes';
|
import { Box, Flex, Text } from "@radix-ui/themes";
|
||||||
import fuzzysort from 'fuzzysort';
|
import fuzzysort from "fuzzysort";
|
||||||
import { Quicklime } from 'quicklime';
|
import { Quicklime } from "quicklime";
|
||||||
import { useEffect, useRef, type ReactNode } from 'react';
|
import { useEffect, useRef, type ReactNode } from "react";
|
||||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
import { Mark } from '../../../../../Mark';
|
import { Mark } from "../../../../../Mark";
|
||||||
|
|
||||||
export const highlightedRows = new Set<HTMLDivElement>();
|
export const highlightedRows = new Set<HTMLDivElement>();
|
||||||
export const highlightedRowsUpdate = new Quicklime();
|
export const highlightedRowsUpdate = new Quicklime();
|
||||||
@@ -16,7 +16,7 @@ export interface InfoProps {
|
|||||||
delta?: number;
|
delta?: number;
|
||||||
decimals?: number;
|
decimals?: number;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
deltaType?: 'higherIsBetter' | 'lowerIsBetter';
|
deltaType?: "higherIsBetter" | "lowerIsBetter";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Info({
|
export function Info({
|
||||||
@@ -26,15 +26,15 @@ export function Info({
|
|||||||
delta,
|
delta,
|
||||||
decimals,
|
decimals,
|
||||||
prefix,
|
prefix,
|
||||||
deltaType = 'higherIsBetter',
|
deltaType = "higherIsBetter",
|
||||||
}: InfoProps) {
|
}: InfoProps) {
|
||||||
const statSearch = TankopediaEphemeral.use((state) => state.statSearch);
|
const statSearch = Tankopedia.use((state) => state.statSearch);
|
||||||
const container = useRef<HTMLDivElement>(null);
|
const container = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
let label = name;
|
let label = name;
|
||||||
let highlighted = false;
|
let highlighted = false;
|
||||||
|
|
||||||
if (statSearch !== undefined && typeof name === 'string') {
|
if (statSearch !== undefined && typeof name === "string") {
|
||||||
const result = fuzzysort.single(statSearch, name);
|
const result = fuzzysort.single(statSearch, name);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
@@ -62,7 +62,7 @@ export function Info({
|
|||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
pl={indent ? '2' : '0'}
|
pl={indent ? "2" : "0"}
|
||||||
width="100%"
|
width="100%"
|
||||||
gap="4"
|
gap="4"
|
||||||
ref={container}
|
ref={container}
|
||||||
@@ -77,9 +77,9 @@ export function Info({
|
|||||||
<>
|
<>
|
||||||
<Text
|
<Text
|
||||||
color={
|
color={
|
||||||
delta * (deltaType === 'lowerIsBetter' ? -1 : 1) > 0
|
delta * (deltaType === "lowerIsBetter" ? -1 : 1) > 0
|
||||||
? 'green'
|
? "green"
|
||||||
: 'tomato'
|
: "tomato"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{decimals !== undefined
|
{decimals !== undefined
|
||||||
@@ -88,9 +88,9 @@ export function Info({
|
|||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
color={
|
color={
|
||||||
delta * (deltaType === 'lowerIsBetter' ? -1 : 1) > 0
|
delta * (deltaType === "lowerIsBetter" ? -1 : 1) > 0
|
||||||
? 'green'
|
? "green"
|
||||||
: 'tomato'
|
: "tomato"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{delta > 0 ? <CaretUpIcon /> : <CaretDownIcon />}
|
{delta > 0 ? <CaretUpIcon /> : <CaretDownIcon />}
|
||||||
@@ -105,7 +105,7 @@ export function Info({
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{decimals !== undefined && typeof children === 'number'
|
{decimals !== undefined && typeof children === "number"
|
||||||
? children.toFixed(decimals)
|
? children.toFixed(decimals)
|
||||||
: children}
|
: children}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
+145
-153
@@ -1,27 +1,27 @@
|
|||||||
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";
|
||||||
import { awaitableSkillDefinitions } from '../../../../../../core/awaitables/skillDefinitions';
|
import { awaitableSkillDefinitions } from "../../../../../../core/awaitables/skillDefinitions";
|
||||||
import { awaitableTankDefinitions } from '../../../../../../core/awaitables/tankDefinitions';
|
import { awaitableTankDefinitions } from "../../../../../../core/awaitables/tankDefinitions";
|
||||||
import {
|
import {
|
||||||
tankCharacteristics,
|
tankCharacteristics,
|
||||||
type TankCharacteristics,
|
type TankCharacteristics,
|
||||||
} from '../../../../../../core/blitzkit/tankCharacteristics';
|
} from "../../../../../../core/blitzkit/tankCharacteristics";
|
||||||
import { tankToDuelMember } from '../../../../../../core/blitzkit/tankToDuelMember';
|
import { tankToDuelMember } from "../../../../../../core/blitzkit/tankToDuelMember";
|
||||||
import { useDelta } from '../../../../../../hooks/useDelta';
|
import { useDelta } from "../../../../../../hooks/useDelta";
|
||||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||||
import { Duel } from '../../../../../../stores/duel';
|
import { Duel } from "../../../../../../stores/duel";
|
||||||
import {
|
import {
|
||||||
TankopediaEphemeral,
|
Tankopedia,
|
||||||
TankopediaRelativeAgainst,
|
TankopediaRelativeAgainst,
|
||||||
} from '../../../../../../stores/tankopediaEphemeral';
|
} from "../../../../../../stores/tankopedia";
|
||||||
import { Info, type InfoProps } from './Info';
|
import { Info, type InfoProps } from "./Info";
|
||||||
|
|
||||||
type InfoWithDeltaProps = Omit<InfoProps, 'name'> & {
|
type InfoWithDeltaProps = Omit<InfoProps, "name"> & {
|
||||||
stats: TankCharacteristics;
|
stats: TankCharacteristics;
|
||||||
noRanking?: boolean;
|
noRanking?: boolean;
|
||||||
} & (
|
} & (
|
||||||
@@ -48,148 +48,140 @@ const [
|
|||||||
awaitableSkillDefinitions,
|
awaitableSkillDefinitions,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function InfoWithDelta({
|
export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
||||||
stats,
|
({ stats, indent, noRanking, deltaType, ...props }) => {
|
||||||
indent,
|
const { strings } = useLocale();
|
||||||
noRanking,
|
const relativeAgainst = Tankopedia.use((state) => state.relativeAgainst);
|
||||||
deltaType,
|
const uhWhatDoICallThisVariable =
|
||||||
...props
|
typeof props.value === "function"
|
||||||
}: InfoWithDeltaProps) {
|
? props.value(stats)!
|
||||||
const { strings } = useLocale();
|
: (stats[props.value] as number);
|
||||||
const relativeAgainst = TankopediaEphemeral.use(
|
const delta = useDelta(uhWhatDoICallThisVariable);
|
||||||
(state) => state.relativeAgainst,
|
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
||||||
);
|
const shellIndex = Duel.use((state) =>
|
||||||
const uhWhatDoICallThisVariable =
|
state.protagonist.gun.shells.indexOf(state.protagonist.shell)
|
||||||
typeof props.value === 'function'
|
);
|
||||||
? props.value(stats)!
|
const equipmentMatrix = Duel.use(
|
||||||
: (stats[props.value] as number);
|
(state) => state.protagonist.equipmentMatrix
|
||||||
const delta = useDelta(uhWhatDoICallThisVariable);
|
);
|
||||||
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
const others = useMemo(() => {
|
||||||
const shellIndex = Duel.use((state) =>
|
const defaultSkills = createDefaultSkills(skillDefinitions);
|
||||||
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)
|
return Object.values(tankDefinitions.tanks)
|
||||||
.filter(
|
.filter(
|
||||||
(tank) =>
|
(tank) =>
|
||||||
(relativeAgainst === TankopediaRelativeAgainst.Class &&
|
(relativeAgainst === TankopediaRelativeAgainst.Class &&
|
||||||
tank.tier === protagonistTank.tier &&
|
tank.tier === protagonistTank.tier &&
|
||||||
tank.class === protagonistTank.class) ||
|
tank.class === protagonistTank.class) ||
|
||||||
(relativeAgainst === TankopediaRelativeAgainst.Tier &&
|
(relativeAgainst === TankopediaRelativeAgainst.Tier &&
|
||||||
tank.tier === protagonistTank.tier) ||
|
tank.tier === protagonistTank.tier) ||
|
||||||
relativeAgainst === TankopediaRelativeAgainst.All,
|
relativeAgainst === TankopediaRelativeAgainst.All
|
||||||
)
|
)
|
||||||
.map((tank) => {
|
.map((tank) => {
|
||||||
const thisTankModel = modelDefinitions.models[tank.id];
|
const member = tankToDuelMember(tank, provisionDefinitions);
|
||||||
const member = tankToDuelMember(
|
|
||||||
tank,
|
|
||||||
thisTankModel,
|
|
||||||
provisionDefinitions,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (member.gun.gun_type!.value.base.shells[shellIndex] === undefined) {
|
if (member.gun.shells[shellIndex] === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tankCharacteristics(
|
return tankCharacteristics(
|
||||||
{
|
{
|
||||||
applyDynamicArmor: false,
|
applyDynamicArmor: false,
|
||||||
applyReactiveArmor: false,
|
applyReactiveArmor: false,
|
||||||
applySpallLiner: false,
|
applySpallLiner: false,
|
||||||
camouflage: member.camouflage,
|
camouflage: member.camouflage,
|
||||||
consumables: member.consumables,
|
consumables: member.consumables,
|
||||||
crewSkills: defaultSkills,
|
crewSkills: defaultSkills,
|
||||||
provisions: member.provisions,
|
provisions: member.provisions,
|
||||||
engine: member.engine,
|
engine: member.engine,
|
||||||
gun: member.gun,
|
gun: member.gun,
|
||||||
equipmentMatrix: equipmentMatrix,
|
equipmentMatrix: equipmentMatrix,
|
||||||
shell: member.gun.gun_type!.value.base.shells[shellIndex],
|
shell: member.gun.shells[shellIndex],
|
||||||
stockEngine: tank.engines[0],
|
stockEngine: tank.engines[0],
|
||||||
stockGun: tank.turrets[0].guns[0],
|
stockGun: tank.turrets[0].guns[0],
|
||||||
stockTrack: tank.tracks[0],
|
stockTrack: tank.tracks[0],
|
||||||
stockTurret: tank.turrets[0],
|
stockTurret: tank.turrets[0],
|
||||||
tank,
|
tank,
|
||||||
track: member.track,
|
track: member.track,
|
||||||
turret: member.turret,
|
turret: member.turret,
|
||||||
assaultDistance: member.assaultDistance,
|
assaultDistance: member.assaultDistance,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
equipmentDefinitions,
|
equipmentDefinitions,
|
||||||
provisionDefinitions,
|
provisionDefinitions,
|
||||||
tankModelDefinition: modelDefinitions.models[tank.id],
|
tankModelDefinition: modelDefinitions.models[tank.id],
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.filter((tank) => {
|
.filter((tank) => {
|
||||||
if (tank === undefined) return false;
|
if (tank === undefined) return false;
|
||||||
|
|
||||||
const othersValue =
|
const othersValue =
|
||||||
typeof props.value === 'function'
|
typeof props.value === "function"
|
||||||
? props.value(tank)!
|
? props.value(tank)!
|
||||||
: (tank[props.value] as number);
|
: (tank[props.value] as number);
|
||||||
|
|
||||||
return othersValue !== undefined;
|
return othersValue !== undefined;
|
||||||
}) as TankCharacteristics[];
|
}) as TankCharacteristics[];
|
||||||
}, [relativeAgainst, shellIndex, equipmentMatrix]);
|
}, [relativeAgainst, shellIndex, equipmentMatrix]);
|
||||||
const betterTanks = others.filter((tank) => {
|
const betterTanks = others.filter((tank) => {
|
||||||
const othersValue =
|
const othersValue =
|
||||||
typeof props.value === 'function'
|
typeof props.value === "function"
|
||||||
? props.value(tank)!
|
? props.value(tank)!
|
||||||
: (tank[props.value] as number);
|
: (tank[props.value] as number);
|
||||||
|
|
||||||
if (othersValue === undefined) return false;
|
if (othersValue === undefined) return false;
|
||||||
return deltaType === 'lowerIsBetter'
|
return deltaType === "lowerIsBetter"
|
||||||
? othersValue < uhWhatDoICallThisVariable
|
? othersValue < uhWhatDoICallThisVariable
|
||||||
: othersValue > uhWhatDoICallThisVariable;
|
: othersValue > uhWhatDoICallThisVariable;
|
||||||
});
|
});
|
||||||
const goodness = (others.length - betterTanks.length) / others.length;
|
const goodness = (others.length - betterTanks.length) / others.length;
|
||||||
let color: ComponentProps<typeof Progress>['color'];
|
let color: ComponentProps<typeof Progress>["color"];
|
||||||
|
|
||||||
if (goodness <= 0.25) color = 'red';
|
if (goodness <= 0.25) color = "red";
|
||||||
else if (goodness <= 0.5) color = 'orange';
|
else if (goodness <= 0.5) color = "orange";
|
||||||
else if (goodness <= 0.75) color = 'yellow';
|
else if (goodness <= 0.75) color = "yellow";
|
||||||
else color = 'green';
|
else color = "green";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column">
|
<Flex direction="column">
|
||||||
<Info
|
<Info
|
||||||
deltaType={deltaType}
|
deltaType={deltaType}
|
||||||
indent={indent}
|
indent={indent}
|
||||||
{...props}
|
name={
|
||||||
name={
|
"name" in props
|
||||||
'name' in props
|
? props.name
|
||||||
? props.name
|
: strings.website.tools.tankopedia.characteristics.values[
|
||||||
: strings.website.tools.tankopedia.characteristics.values[
|
props.value
|
||||||
props.value
|
]
|
||||||
]
|
}
|
||||||
}
|
delta={delta}
|
||||||
delta={delta}
|
{...props}
|
||||||
>
|
>
|
||||||
{uhWhatDoICallThisVariable}
|
{uhWhatDoICallThisVariable}
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
{!noRanking && (
|
{!noRanking && (
|
||||||
<Flex pl={indent ? '2' : '0'} align="center" gap="2" mb="2">
|
<Flex pl={indent ? "2" : "0"} align="center" gap="2">
|
||||||
<Progress
|
<Progress
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size="1"
|
size="1"
|
||||||
value={goodness * 100}
|
value={goodness * 100}
|
||||||
color={color}
|
color={color}
|
||||||
style={{ height: '0.125rem', opacity: 0.5 }}
|
style={{ height: "0.125rem", opacity: 0.5 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text color="gray" size="1">
|
<Text color="gray" size="1">
|
||||||
{clamp(betterTanks.length + 1, 1, others.length)} / {others.length}
|
{clamp(betterTanks.length + 1, 1, others.length)} /{" "}
|
||||||
</Text>
|
{others.length}
|
||||||
</Flex>
|
</Text>
|
||||||
)}
|
</Flex>
|
||||||
</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;
|
||||||
|
}
|
||||||
+145
-59
@@ -1,12 +1,17 @@
|
|||||||
import { type Datum, type Serie } from '@nivo/line';
|
import { DoubleArrowRightIcon } from '@radix-ui/react-icons';
|
||||||
import { Box, Card, Code } from '@radix-ui/themes';
|
import { Box, Button, 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 { useLocale } from '../../../../../../../hooks/useLocale';
|
||||||
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 +20,31 @@ 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 { strings } = useLocale();
|
||||||
|
|
||||||
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 +52,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 +135,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 +166,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 +187,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 +201,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 +248,71 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Code
|
||||||
|
ref={progressTime}
|
||||||
|
size="2"
|
||||||
|
variant="ghost"
|
||||||
|
weight="bold"
|
||||||
|
color="gray"
|
||||||
|
highContrast
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
right: 'calc(50% + var(--space-8))',
|
||||||
|
transform: 'translate(50%, -50%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
0.00
|
||||||
|
</Code>
|
||||||
|
<Code
|
||||||
|
ref={totalTime}
|
||||||
|
size="2"
|
||||||
|
variant="ghost"
|
||||||
|
weight="bold"
|
||||||
|
color="gray"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: 'calc(50% + var(--space-8))',
|
||||||
|
transform: 'translate(-50%, -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%"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="1"
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
highContrast
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '68%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
state.current = Math.floor(state.current + 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{strings.website.tools.tankopedia.visualizers.reload.skip}
|
||||||
|
<DoubleArrowRightIcon />
|
||||||
|
</Button>
|
||||||
|
</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>
|
||||||
|
|||||||
+19
-20
@@ -1,16 +1,15 @@
|
|||||||
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||||
import { Flex, Spinner, TextField, type FlexProps } from '@radix-ui/themes';
|
import { Flex, Spinner, TextField, type FlexProps } from "@radix-ui/themes";
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from "lodash-es";
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
import { highlightedRows, highlightedRowsUpdate } from './Info';
|
import { highlightedRows, highlightedRowsUpdate } from "./Info";
|
||||||
|
|
||||||
export function StatSearch(props: FlexProps) {
|
export function StatSearch(props: FlexProps) {
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const count = useRef<HTMLDivElement>(null);
|
const count = useRef<HTMLDivElement>(null);
|
||||||
const [index, setIndex] = useState(0);
|
const [index, setIndex] = useState(0);
|
||||||
const search = useCallback(() => {
|
const search = useCallback(() => {
|
||||||
@@ -20,7 +19,7 @@ export function StatSearch(props: FlexProps) {
|
|||||||
setSearching(true);
|
setSearching(true);
|
||||||
} else {
|
} else {
|
||||||
setSearching(false);
|
setSearching(false);
|
||||||
count.current.textContent = '';
|
count.current.textContent = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
@@ -31,21 +30,21 @@ export function StatSearch(props: FlexProps) {
|
|||||||
if (!input.current || !count.current) return;
|
if (!input.current || !count.current) return;
|
||||||
|
|
||||||
if (input.current.value.length === 0) {
|
if (input.current.value.length === 0) {
|
||||||
count.current.textContent = '';
|
count.current.textContent = "";
|
||||||
|
|
||||||
setSearching(false);
|
setSearching(false);
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.statSearch = undefined;
|
draft.statSearch = undefined;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSearching(false);
|
setSearching(false);
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.statSearch = input.current!.value;
|
draft.statSearch = input.current!.value;
|
||||||
});
|
});
|
||||||
}, 500),
|
}, 500),
|
||||||
[],
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -53,7 +52,7 @@ export function StatSearch(props: FlexProps) {
|
|||||||
if (!count.current || !input.current) return;
|
if (!count.current || !input.current) return;
|
||||||
|
|
||||||
count.current.textContent =
|
count.current.textContent =
|
||||||
input.current.value.length === 0 ? '' : `0 / ${highlightedRows.size}`;
|
input.current.value.length === 0 ? "" : `0 / ${highlightedRows.size}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
highlightedRowsUpdate.on(handleHighlightedRowsUpdate);
|
highlightedRowsUpdate.on(handleHighlightedRowsUpdate);
|
||||||
@@ -69,23 +68,23 @@ export function StatSearch(props: FlexProps) {
|
|||||||
ref={input}
|
ref={input}
|
||||||
placeholder={strings.website.tools.tankopedia.search}
|
placeholder={strings.website.tools.tankopedia.search}
|
||||||
variant="classic"
|
variant="classic"
|
||||||
style={{ width: '100%' }}
|
style={{ width: "100%" }}
|
||||||
onChange={search}
|
onChange={search}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (!count.current) return;
|
if (!count.current) return;
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
if (event.key === "Enter") {
|
||||||
const newIndex = (index + 1) % highlightedRows.size;
|
const newIndex = (index + 1) % highlightedRows.size;
|
||||||
count.current.textContent = `${newIndex + 1} / ${highlightedRows.size}`;
|
count.current.textContent = `${newIndex + 1} / ${highlightedRows.size}`;
|
||||||
const array = [...highlightedRows];
|
const array = [...highlightedRows];
|
||||||
|
|
||||||
array.forEach((element) => element.classList.remove('focused'));
|
array.forEach((element) => element.classList.remove("focused"));
|
||||||
|
|
||||||
if (array[newIndex]) {
|
if (array[newIndex]) {
|
||||||
array[newIndex].classList.add('focused');
|
array[newIndex].classList.add("focused");
|
||||||
array[newIndex].scrollIntoView({
|
array[newIndex].scrollIntoView({
|
||||||
block: 'center',
|
block: "center",
|
||||||
inline: 'center',
|
inline: "center",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+42
-42
@@ -1,37 +1,38 @@
|
|||||||
import { Flex } from '@radix-ui/themes';
|
import { Flex } from "@radix-ui/themes";
|
||||||
import { useEffect } from 'react';
|
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 { tankCharacteristics } from '../../../../../core/blitzkit/tankCharacteristics';
|
import { modelTransformEvent } from "../../../../../core/blitzkit/modelTransform";
|
||||||
import { useEquipment } from '../../../../../hooks/useEquipment';
|
import { tankCharacteristics } from "../../../../../core/blitzkit/tankCharacteristics";
|
||||||
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
|
import { useEquipment } from "../../../../../hooks/useEquipment";
|
||||||
import { Duel } from '../../../../../stores/duel';
|
import { useTankModelDefinition } from "../../../../../hooks/useTankModelDefinition";
|
||||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
import { Duel } from "../../../../../stores/duel";
|
||||||
import { Crew } from './components/Crew';
|
import { Tankopedia } from "../../../../../stores/tankopedia";
|
||||||
import { Firepower } from './components/Firepower';
|
import type { MaybeSkeletonComponentProps } from "../../../../../types/maybeSkeletonComponentProps";
|
||||||
import { Maneuverability } from './components/Maneuverability';
|
import { Crew } from "./components/Crew";
|
||||||
import { Survivability } from './components/Survivability';
|
import { Firepower } from "./components/Firepower";
|
||||||
|
import { Maneuverability } from "./components/Maneuverability";
|
||||||
|
import { Survivability } from "./components/Survivability";
|
||||||
|
|
||||||
const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
||||||
awaitableEquipmentDefinitions,
|
awaitableEquipmentDefinitions,
|
||||||
awaitableProvisionDefinitions,
|
awaitableProvisionDefinitions,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function Characteristics() {
|
export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const mutateDuel = Duel.useMutation();
|
const crewSkills = Tankopedia.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);
|
||||||
const consumables = Duel.use((state) => state.protagonist.consumables);
|
const consumables = Duel.use((state) => state.protagonist.consumables);
|
||||||
const camouflage = Duel.use((state) => state.protagonist.camouflage);
|
const camouflage = Duel.use((state) => state.protagonist.camouflage);
|
||||||
const assaultDistance = Duel.use(
|
const assaultDistance = Duel.use(
|
||||||
(state) => state.protagonist.assaultDistance,
|
(state) => state.protagonist.assaultDistance
|
||||||
);
|
);
|
||||||
const equipmentMatrix = Duel.use(
|
const equipmentMatrix = Duel.use(
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
(state) => state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const { tank, turret, gun, engine, track, shell } = Duel.use(
|
const { tank, turret, gun, engine, track, shell } = Duel.use(
|
||||||
(state) => state.protagonist,
|
(state) => state.protagonist
|
||||||
);
|
);
|
||||||
const stockEngine = tank.engines[0];
|
const stockEngine = tank.engines[0];
|
||||||
const stockTrack = tank.tracks[0];
|
const stockTrack = tank.tracks[0];
|
||||||
@@ -39,8 +40,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,
|
||||||
@@ -67,47 +67,47 @@ export function Characteristics() {
|
|||||||
tankModelDefinition,
|
tankModelDefinition,
|
||||||
equipmentDefinitions: equipmentDefinitions,
|
equipmentDefinitions: equipmentDefinitions,
|
||||||
provisionDefinitions: provisionDefinitions,
|
provisionDefinitions: provisionDefinitions,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||||
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 (
|
||||||
<Flex
|
<Flex
|
||||||
gap="8"
|
gap="8"
|
||||||
direction={{ initial: 'column', md: 'row' }}
|
direction={{ initial: "column", md: "row" }}
|
||||||
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"
|
||||||
direction={{ initial: 'column', lg: 'row' }}
|
direction={{ initial: "column", lg: "row" }}
|
||||||
align={{ initial: 'center', sm: 'start' }}
|
align={{ initial: "center", sm: "start" }}
|
||||||
width={{ initial: '100%', sm: 'auto' }}
|
width={{ initial: "100%", sm: "auto" }}
|
||||||
>
|
>
|
||||||
<Maneuverability stats={stats} />
|
<Maneuverability stats={stats} />
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
gap="8"
|
gap="8"
|
||||||
direction="column"
|
direction="column"
|
||||||
align={{ initial: 'center', sm: 'start' }}
|
align={{ initial: "center", sm: "start" }}
|
||||||
width={{ initial: '100%', sm: 'auto' }}
|
width={{ initial: "100%", sm: "auto" }}
|
||||||
>
|
>
|
||||||
<Survivability stats={stats} />
|
<Survivability stats={stats} />
|
||||||
<Crew stats={stats} />
|
<Crew stats={stats} />
|
||||||
|
|||||||
+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,
|
||||||
},
|
},
|
||||||
])}
|
])}
|
||||||
|
|||||||
+10
-11
@@ -1,4 +1,4 @@
|
|||||||
import { InfoCircledIcon } from '@radix-ui/react-icons';
|
import { InfoCircledIcon } from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -6,15 +6,14 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Popover,
|
Popover,
|
||||||
Text,
|
Text,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import { useLocale } from '../../../../hooks/useLocale';
|
import { useLocale } from "../../../../hooks/useLocale";
|
||||||
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../stores/tankopedia";
|
||||||
import { CrewSkillManager } from '../../../CrewSkillManager';
|
import { CrewSkillManager } from "../../../CrewSkillManager";
|
||||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
|
||||||
|
|
||||||
export function Skills() {
|
export function Skills() {
|
||||||
const skillLevels = TankopediaEphemeral.use((state) => state.skills);
|
const skillLevels = Tankopedia.use((state) => state.skills);
|
||||||
const mutateTankopediaTemporary = TankopediaEphemeral.useMutation();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,7 +45,7 @@ export function Skills() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaTemporary((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
Object.keys(draft.skills).forEach((skill) => {
|
Object.keys(draft.skills).forEach((skill) => {
|
||||||
draft.skills[skill] = 0;
|
draft.skills[skill] = 0;
|
||||||
});
|
});
|
||||||
@@ -58,7 +57,7 @@ export function Skills() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaTemporary((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
Object.keys(draft.skills).forEach((skill) => {
|
Object.keys(draft.skills).forEach((skill) => {
|
||||||
draft.skills[skill] = 7;
|
draft.skills[skill] = 7;
|
||||||
});
|
});
|
||||||
@@ -72,7 +71,7 @@ export function Skills() {
|
|||||||
<CrewSkillManager
|
<CrewSkillManager
|
||||||
skillLevels={skillLevels}
|
skillLevels={skillLevels}
|
||||||
onChange={(skills) => {
|
onChange={(skills) => {
|
||||||
mutateTankopediaTemporary((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.skills = skills;
|
draft.skills = skills;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
Loaded 100 of 174 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user