mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-12 12:16:11 +01:00
Compare commits
130
Commits
09f5d4e3f7
...
v1.27.17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1558b69931 | ||
|
|
a560a3d3ae | ||
|
|
b620110d0f | ||
|
|
23ea72bf40 | ||
|
|
aaee0b7965 | ||
|
|
347523f9e7 | ||
|
|
3205630a0f | ||
|
|
8446e3fbbd | ||
|
|
6d25a97fc8 | ||
|
|
bdbfce0e18 | ||
|
|
8033e8f7cc | ||
|
|
f98c801d6c | ||
|
|
339663adaf | ||
|
|
2b15357dee | ||
|
|
1e05717daf | ||
|
|
c20f8c5040 | ||
|
|
f22f4eeccd | ||
|
|
79c450798c | ||
|
|
d247e67690 | ||
|
|
69b2035cfc | ||
|
|
62fed63310 | ||
|
|
7c29046390 | ||
|
|
b65c3c1d93 | ||
|
|
87f5d8fe8f | ||
|
|
54c7b6b476 | ||
|
|
7bdaf9ca93 | ||
|
|
525b9377cc | ||
|
|
94cba0c9ba | ||
|
|
4ddf77fc7f | ||
|
|
ae6dc259dc | ||
|
|
c2e95f6ef6 | ||
|
|
868812e578 | ||
|
|
b5064f9e8b | ||
|
|
a0eb0d54f0 | ||
|
|
e47b1315a9 | ||
|
|
60025365c4 | ||
|
|
cde6904513 | ||
|
|
997d1df8f9 | ||
|
|
9f15ded0c9 | ||
|
|
b2ee97b3d4 | ||
|
|
03d819a6c1 | ||
|
|
8ff26d57f6 | ||
|
|
c243ebd19a | ||
|
|
40f5042896 | ||
|
|
21130b29f8 | ||
|
|
d50b054243 | ||
|
|
6b7f8049fe | ||
|
|
e6029157aa | ||
|
|
ba656eb603 | ||
|
|
e8dd26b2ba | ||
|
|
897da9479d | ||
|
|
291395718f | ||
|
|
42e860b171 | ||
|
|
3b6d339310 | ||
|
|
728fd5b824 | ||
|
|
34217b2754 | ||
|
|
1e7acc2775 | ||
|
|
0b5c88531f | ||
|
|
e145d7c6e8 | ||
|
|
af13e3f184 | ||
|
|
19a2492555 | ||
|
|
3c51f4f94a | ||
|
|
86f4712b87 | ||
|
|
ea90ca21b1 | ||
|
|
b3046d8655 | ||
|
|
8f9e537a5e | ||
|
|
9368d058f2 | ||
|
|
31b628049a | ||
|
|
072e913433 | ||
|
|
e3f131cdfd | ||
|
|
5cab394ced | ||
|
|
2033fab21a | ||
|
|
7471567147 | ||
|
|
43aee373ea | ||
|
|
993083665b | ||
|
|
f2c25424d9 | ||
|
|
bbe53deb98 | ||
|
|
1f07c2be74 | ||
|
|
ea744b673a | ||
|
|
d8a6cac52f | ||
|
|
1524b9a35d | ||
|
|
36738f57a9 | ||
|
|
fca6cb23d1 | ||
|
|
a704b291fd | ||
|
|
e1b5b21fae | ||
|
|
097f2792ae | ||
|
|
089d0e9682 | ||
|
|
6a92b3ae80 | ||
|
|
1e91065033 | ||
|
|
b20bb97ea3 | ||
|
|
ba97a9f28b | ||
|
|
81e480f983 | ||
|
|
40e4e0a274 | ||
|
|
83b7de77de | ||
|
|
20f7119854 | ||
|
|
275ddcc7a6 | ||
|
|
4858f31da4 | ||
|
|
4bc255a624 | ||
|
|
17755641a1 | ||
|
|
0e6b0087ce | ||
|
|
fcd084a86f | ||
|
|
af699ff5b7 | ||
|
|
93665df223 | ||
|
|
f8b4f9372a | ||
|
|
e84e15daf4 | ||
|
|
d7af7e19dd | ||
|
|
c5cf172e42 | ||
|
|
2e8cac885c | ||
|
|
9ed4a856bf | ||
|
|
eedd4b3f27 | ||
|
|
3b7fe0b114 | ||
|
|
a6d66b94f5 | ||
|
|
c7a7bbb809 | ||
|
|
02ce6ba8d9 | ||
|
|
9dd057d594 | ||
|
|
64ec981496 | ||
|
|
8afafdeece | ||
|
|
6b9fd1d392 | ||
|
|
c1efc50134 | ||
|
|
b4b1f4e916 | ||
|
|
3de0c40ce3 | ||
|
|
893bfa8393 | ||
|
|
cc25535c34 | ||
|
|
a4cdf16fcd | ||
|
|
36588b11e6 | ||
|
|
e927838c14 | ||
|
|
5197ebb6a7 | ||
|
|
51089a3e91 | ||
|
|
c6121fd4bd | ||
|
|
7dca9d3244 |
No files matched your search
@@ -53,6 +53,7 @@ env:
|
|||||||
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
|
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
|
||||||
PUBLIC_ASSET_FAST_USER: blitzkit
|
PUBLIC_ASSET_FAST_USER: blitzkit
|
||||||
PUBLIC_ASSET_FAST_REPO: assets
|
PUBLIC_ASSET_FAST_REPO: assets
|
||||||
|
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
download_client:
|
download_client:
|
||||||
|
|||||||
@@ -14,6 +14,17 @@ 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: 'true'
|
||||||
|
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 }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -27,4 +27,15 @@ jobs:
|
|||||||
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: 'true'
|
||||||
|
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 }}
|
||||||
run: bun build:ratingleaderboard --region=asia
|
run: bun build:ratingleaderboard --region=asia
|
||||||
@@ -27,4 +27,15 @@ jobs:
|
|||||||
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: 'true'
|
||||||
|
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 }}
|
||||||
run: bun build:ratingleaderboard --region=eu
|
run: bun build:ratingleaderboard --region=eu
|
||||||
@@ -27,4 +27,15 @@ jobs:
|
|||||||
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: 'true'
|
||||||
|
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 }}
|
||||||
run: bun build:ratingleaderboard --region=com
|
run: bun build:ratingleaderboard --region=com
|
||||||
@@ -19,7 +19,7 @@ concurrency:
|
|||||||
env:
|
env:
|
||||||
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
|
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
|
||||||
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
|
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
|
||||||
ALLOW_ROBOTS: 'true'
|
ALLOW_ROBOTS: ${{ secrets.ALLOW_ROBOTS }}
|
||||||
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
|
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
|
||||||
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
|
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
|
||||||
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
|
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
|
||||||
@@ -29,6 +29,8 @@ env:
|
|||||||
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
|
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
|
||||||
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
|
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
|
||||||
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
|
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
|
||||||
|
PUBLIC_GUIDE_WEBSITE: ${{ secrets.PUBLIC_GUIDE_WEBSITE }}
|
||||||
|
SERVICING: ${{ secrets.SERVICING }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
Vendored
+3
@@ -86,6 +86,7 @@
|
|||||||
"gameplay",
|
"gameplay",
|
||||||
"glsl",
|
"glsl",
|
||||||
"GLTF",
|
"GLTF",
|
||||||
|
"gtag",
|
||||||
"haha",
|
"haha",
|
||||||
"highp",
|
"highp",
|
||||||
"iife",
|
"iife",
|
||||||
@@ -108,6 +109,7 @@
|
|||||||
"Maddoxkkm",
|
"Maddoxkkm",
|
||||||
"malum",
|
"malum",
|
||||||
"mantlets",
|
"mantlets",
|
||||||
|
"mediump",
|
||||||
"metallicness",
|
"metallicness",
|
||||||
"metalness",
|
"metalness",
|
||||||
"minitelrose",
|
"minitelrose",
|
||||||
@@ -220,6 +222,7 @@
|
|||||||
"yolo",
|
"yolo",
|
||||||
"youtuber",
|
"youtuber",
|
||||||
"youtubers",
|
"youtubers",
|
||||||
|
"zoomable",
|
||||||
"zustand"
|
"zustand"
|
||||||
],
|
],
|
||||||
"deno.enable": false,
|
"deno.enable": false,
|
||||||
|
|||||||
@@ -1,661 +0,0 @@
|
|||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 19 November 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works, specifically designed to ensure
|
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
|
||||||
share and change all versions of a program--to make sure it remains free
|
|
||||||
software for all its users.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
|
||||||
you this License which gives you legal permission to copy, distribute
|
|
||||||
and/or modify the software.
|
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
|
||||||
improvements made in alternate versions of the program, if they
|
|
||||||
receive widespread use, become available for other developers to
|
|
||||||
incorporate. Many developers of free software are heartened and
|
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
|
||||||
ensure that, in such cases, the modified source code becomes available
|
|
||||||
to the community. It requires the operator of a network server to
|
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
|
||||||
this license.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the work with which it is combined will remain governed by version
|
|
||||||
3 of the GNU General Public License.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published
|
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
|
||||||
network, you should also make sure that it provides a way for users to
|
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
|
||||||
of the code. There are many ways you could offer source, and different
|
|
||||||
solutions will be better for different programs; see section 13 for the
|
|
||||||
specific requirements.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
# BlitzKit License
|
||||||
|
|
||||||
|
Version 1.1, August 5th, 2025. Copyright (c) TrèsAbhi.
|
||||||
|
|
||||||
|
Permission is hereby granted to anyone to:
|
||||||
|
|
||||||
|
- Fork, clone, and download the contents of this repository.
|
||||||
|
- Modify the code locally in private.
|
||||||
|
- Use and run the code **only for personal, non-public testing, or educational purposes**.
|
||||||
|
|
||||||
|
The following restrictions apply:
|
||||||
|
|
||||||
|
- You may **not** deploy, host, or make this project or any derivative publicly accessible.
|
||||||
|
- You may **not** share screenshots, recordings, or any form of media from this project unless you intend to merge your changes back into BlitzKit.
|
||||||
|
- You may **not** use this project or its contents in any public or commercial product.
|
||||||
|
- You may **not** redistribute modified or unmodified versions of this project.
|
||||||
|
|
||||||
|
Contributions:
|
||||||
|
|
||||||
|
- You are encouraged to propose changes or improvements via pull requests to the original BlitzKit repository.
|
||||||
|
- Any such contributions will be considered under the same license terms unless explicitly stated otherwise.
|
||||||
|
|
||||||
|
Violations of this license may result in revocation of usage rights and takedown requests.
|
||||||
|
|
||||||
|
For questions or licensing inquiries, contact support@blitzkit.app.
|
||||||
|
|
||||||
|
## Notes on Existing Forks
|
||||||
|
|
||||||
|
All forks fall under these categories:
|
||||||
|
|
||||||
|
- If your fork was made from commit `8462901` (Apr 17, 2023) or between commits `2958124` (Jul 3, 2023) and `85e483e` (Jul 13, 2024), your fork falls under the **MIT license**.
|
||||||
|
- If your fork was made from commits between `5646093` (Jul 13, 2024) and `e3f131c` (Aug 5, 2025), your fork falls under the **AGPL-3.0 license**.
|
||||||
|
- If your fork was made between commit `f7c356d` (Apr 17, 2023) and `2958124` (Jul 3, 2023), your fork falls under **a default exclusive copyright**.
|
||||||
|
- If your fork was made from commits between `e3f131c` (Aug 5, 2025) and today, your fork falls under **this license**.
|
||||||
|
|
||||||
|
MIT and AGPL-3.0 licensed fork owners:
|
||||||
|
|
||||||
|
- You may continue to use your fork of BlitzKit under the corresponding license.
|
||||||
|
- You may upgrade your fork up to commit `5cab394` (Aug 4, 2025) and keep the AGPL-3.0 license.
|
||||||
|
- You may upgrade to commit `e3f131c` (Aug 5, 2025) or beyond but have to **switch to this license** where all rules apply.
|
||||||
|
- If you upgrade to commit `e3f131c` (Aug 5, 2025) or beyond, you must retract all public deployments of BlitzKit.
|
||||||
|
- Existing screenshots, recordings, or any form of media can remain.
|
||||||
|
|
||||||
|
Default exclusive copyright fork owners:
|
||||||
|
|
||||||
|
- You may continue to use their forks under the AGPL-3.0 license as no license was applied to BlitzKit at the time of forking.
|
||||||
|
|
||||||
|
BlitzKit licensed fork owners:
|
||||||
|
|
||||||
|
- You may continue to upgrade to the latest commits only under the conditions of this license.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# BlitzKit 1.27.12
|
||||||
|
|
||||||
|
This update only affects the website.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added the ability to sort by HE shell penetration
|
||||||
|
- The Tankopedia compare bars now account for equipment and selected shells
|
||||||
|
- Made the hero section of the Tankopedia a bit shorter to show users that the page is scrollable
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- The "Clear filters" button no longer shows up after clearing the search bar
|
||||||
|
- Guns for some tanks no longer go transparent when far away from the camera
|
||||||
|
- The dynamic armor tab no longer lags and flickers when rotating the turret of a tank
|
||||||
|
- Reimplemented the missing tool backgrounds in the navigation bar hamburger menu
|
||||||
|
- The navigation bar now properly goes transparent for pages that request like the Tankopedia pages
|
||||||
|
- Removed the tank name text outline because it doesn't work on most devices
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# BlitzKit 1.27.13
|
||||||
|
|
||||||
|
This update adds a feature for a future version of WoTB 👁️👃👁️
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added compatibility for the new mechanic for the upcoming Polish Tank Destroyers
|
||||||
|
- This code will remain dormant until the update for WoTB is released
|
||||||
|
- Added a progress bar to the hero section of the Tankopedia to show that the model is loading
|
||||||
|
- Reordered some Tankopedia statistics to bring important statistics like Damage to the very top
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- The second column in the footer is now aligned on mobile devices
|
||||||
|
- Fixed an issue that stopped the armor highlighting from working on the Kiwi browser and on older devices
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- Removed permissions for robots to crawl non-blitzkit.app domains
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# BlitzKit 1.27.14
|
||||||
|
|
||||||
|
This is the 14th patch for `1.27`, making it the new longest lasting version beating the previous record of `1.15.13`!
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Optimized the rendering of the shells in the Tankopedia search
|
||||||
|
- The Assault Shells cursor now works on iOS devices
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# BlitzKit 1.27.15
|
||||||
|
|
||||||
|
I did not see this coming
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- The Assault Shell graph now supports damage factors of above 100% now
|
||||||
@@ -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!
|
||||||
+3
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkit",
|
"name": "blitzkit",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.27.11",
|
"version": "1.27.17",
|
||||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "./types.d.ts",
|
"types": "./types.d.ts",
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
"start:bot": "bun dist/bot/main.js",
|
"start:bot": "bun dist/bot/main.js",
|
||||||
"dev:docs": "vitepress dev docs",
|
"dev:docs": "vitepress dev docs",
|
||||||
"dev:website": "cd packages/website && bun run dev",
|
"dev:website": "cd packages/website && bun run dev",
|
||||||
|
"dev": "bun run dev:website",
|
||||||
"dev:api": "bun --filter=@blitzkit/api run dev",
|
"dev:api": "bun --filter=@blitzkit/api run dev",
|
||||||
"setup:depotdownloader": "bun --filter=@blitzkit/scripts run setup:depotdownloader",
|
"setup:depotdownloader": "bun --filter=@blitzkit/scripts run setup:depotdownloader",
|
||||||
"setup:patches": "bun --filter=@blitzkit/scripts run setup:patches",
|
"setup:patches": "bun --filter=@blitzkit/scripts run setup:patches",
|
||||||
@@ -40,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 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,4 +4,6 @@ export const youtubers = [
|
|||||||
{ id: 'UCxRpZqMElssTdPYDK3x7wyg', name: 'N90' },
|
{ id: 'UCxRpZqMElssTdPYDK3x7wyg', name: 'N90' },
|
||||||
{ id: 'UC28Hc6p2MGoB5s78eHCYkQg', name: 'GuidesBlitz' },
|
{ id: 'UC28Hc6p2MGoB5s78eHCYkQg', name: 'GuidesBlitz' },
|
||||||
{ id: 'UCLMEfqtlwLDN-UXX21RiEww', name: 'WoT Blitz Best Replays' },
|
{ id: 'UCLMEfqtlwLDN-UXX21RiEww', name: 'WoT Blitz Best Replays' },
|
||||||
|
{ id: 'UC3fZbetRUFoJL0IEN7IhG0Q', name: 'EconomicsMate' },
|
||||||
|
{ id: 'UCtsUVwBlhEnUunAKLn2dtsg', name: 'Megalion69' },
|
||||||
];
|
];
|
||||||
@@ -5,10 +5,10 @@
|
|||||||
// source: packages/core/src/protos/camouflage_definitions.proto
|
// source: packages/core/src/protos/camouflage_definitions.proto
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
import { BinaryReader, BinaryWriter } from '@bufbuild/protobuf/wire';
|
||||||
import { createBaseI18nString, I18nString } from "./i18n";
|
import { createBaseI18nString, I18nString } from './i18n';
|
||||||
|
|
||||||
export const protobufPackage = "blitzkit";
|
export const protobufPackage = 'blitzkit';
|
||||||
|
|
||||||
export interface CamouflageDefinitions {
|
export interface CamouflageDefinitions {
|
||||||
camouflages: { [key: number]: Camouflage };
|
camouflages: { [key: number]: Camouflage };
|
||||||
@@ -22,8 +22,8 @@ export interface CamouflageDefinitions_CamouflagesEntry {
|
|||||||
export interface Camouflage {
|
export interface Camouflage {
|
||||||
id: number;
|
id: number;
|
||||||
name: I18nString;
|
name: I18nString;
|
||||||
tank_name?: I18nString | undefined;
|
tank_name?: I18nString;
|
||||||
tank_name_full?: I18nString | undefined;
|
tank_name_full?: I18nString;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBaseCamouflageDefinitions(): CamouflageDefinitions {
|
export function createBaseCamouflageDefinitions(): CamouflageDefinitions {
|
||||||
@@ -31,15 +31,25 @@ export function createBaseCamouflageDefinitions(): CamouflageDefinitions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CamouflageDefinitions: MessageFns<CamouflageDefinitions> = {
|
export const CamouflageDefinitions: MessageFns<CamouflageDefinitions> = {
|
||||||
encode(message: CamouflageDefinitions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
encode(
|
||||||
|
message: CamouflageDefinitions,
|
||||||
|
writer: BinaryWriter = new BinaryWriter(),
|
||||||
|
): BinaryWriter {
|
||||||
Object.entries(message.camouflages).forEach(([key, value]) => {
|
Object.entries(message.camouflages).forEach(([key, value]) => {
|
||||||
CamouflageDefinitions_CamouflagesEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join();
|
CamouflageDefinitions_CamouflagesEntry.encode(
|
||||||
|
{ key: key as any, value },
|
||||||
|
writer.uint32(10).fork(),
|
||||||
|
).join();
|
||||||
});
|
});
|
||||||
return writer;
|
return writer;
|
||||||
},
|
},
|
||||||
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): CamouflageDefinitions {
|
decode(
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
input: BinaryReader | Uint8Array,
|
||||||
|
length?: number,
|
||||||
|
): CamouflageDefinitions {
|
||||||
|
const reader =
|
||||||
|
input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
const message = createBaseCamouflageDefinitions();
|
const message = createBaseCamouflageDefinitions();
|
||||||
while (reader.pos < end) {
|
while (reader.pos < end) {
|
||||||
@@ -50,7 +60,10 @@ export const CamouflageDefinitions: MessageFns<CamouflageDefinitions> = {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry1 = CamouflageDefinitions_CamouflagesEntry.decode(reader, reader.uint32());
|
const entry1 = CamouflageDefinitions_CamouflagesEntry.decode(
|
||||||
|
reader,
|
||||||
|
reader.uint32(),
|
||||||
|
);
|
||||||
if (entry1.value !== undefined) {
|
if (entry1.value !== undefined) {
|
||||||
message.camouflages[entry1.key] = entry1.value;
|
message.camouflages[entry1.key] = entry1.value;
|
||||||
}
|
}
|
||||||
@@ -68,10 +81,12 @@ export const CamouflageDefinitions: MessageFns<CamouflageDefinitions> = {
|
|||||||
fromJSON(object: any): CamouflageDefinitions {
|
fromJSON(object: any): CamouflageDefinitions {
|
||||||
return {
|
return {
|
||||||
camouflages: isObject(object.camouflages)
|
camouflages: isObject(object.camouflages)
|
||||||
? Object.entries(object.camouflages).reduce<{ [key: number]: Camouflage }>((acc, [key, value]) => {
|
? Object.entries(object.camouflages).reduce<{
|
||||||
acc[globalThis.Number(key)] = Camouflage.fromJSON(value);
|
[key: number]: Camouflage;
|
||||||
return acc;
|
}>((acc, [key, value]) => {
|
||||||
}, {})
|
acc[globalThis.Number(key)] = Camouflage.fromJSON(value);
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
: {},
|
: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -90,20 +105,23 @@ export const CamouflageDefinitions: MessageFns<CamouflageDefinitions> = {
|
|||||||
return obj;
|
return obj;
|
||||||
},
|
},
|
||||||
|
|
||||||
create<I extends Exact<DeepPartial<CamouflageDefinitions>, I>>(base?: I): CamouflageDefinitions {
|
create<I extends Exact<DeepPartial<CamouflageDefinitions>, I>>(
|
||||||
|
base?: I,
|
||||||
|
): CamouflageDefinitions {
|
||||||
return CamouflageDefinitions.fromPartial(base ?? ({} as any));
|
return CamouflageDefinitions.fromPartial(base ?? ({} as any));
|
||||||
},
|
},
|
||||||
fromPartial<I extends Exact<DeepPartial<CamouflageDefinitions>, I>>(object: I): CamouflageDefinitions {
|
fromPartial<I extends Exact<DeepPartial<CamouflageDefinitions>, I>>(
|
||||||
|
object: I,
|
||||||
|
): CamouflageDefinitions {
|
||||||
const message = createBaseCamouflageDefinitions();
|
const message = createBaseCamouflageDefinitions();
|
||||||
message.camouflages = Object.entries(object.camouflages ?? {}).reduce<{ [key: number]: Camouflage }>(
|
message.camouflages = Object.entries(object.camouflages ?? {}).reduce<{
|
||||||
(acc, [key, value]) => {
|
[key: number]: Camouflage;
|
||||||
if (value !== undefined) {
|
}>((acc, [key, value]) => {
|
||||||
acc[globalThis.Number(key)] = Camouflage.fromPartial(value);
|
if (value !== undefined) {
|
||||||
}
|
acc[globalThis.Number(key)] = Camouflage.fromPartial(value);
|
||||||
return acc;
|
}
|
||||||
},
|
return acc;
|
||||||
{},
|
}, {});
|
||||||
);
|
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -112,90 +130,116 @@ export function createBaseCamouflageDefinitions_CamouflagesEntry(): CamouflageDe
|
|||||||
return { key: 0, value: createBaseCamouflage() };
|
return { key: 0, value: createBaseCamouflage() };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CamouflageDefinitions_CamouflagesEntry: MessageFns<CamouflageDefinitions_CamouflagesEntry> = {
|
export const CamouflageDefinitions_CamouflagesEntry: MessageFns<CamouflageDefinitions_CamouflagesEntry> =
|
||||||
encode(message: CamouflageDefinitions_CamouflagesEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
{
|
||||||
if (message.key !== undefined) {
|
encode(
|
||||||
writer.uint32(8).uint32(message.key);
|
message: CamouflageDefinitions_CamouflagesEntry,
|
||||||
}
|
writer: BinaryWriter = new BinaryWriter(),
|
||||||
if (message.value !== undefined) {
|
): BinaryWriter {
|
||||||
Camouflage.encode(message.value, writer.uint32(18).fork()).join();
|
if (message.key !== undefined) {
|
||||||
}
|
writer.uint32(8).uint32(message.key);
|
||||||
return writer;
|
|
||||||
},
|
|
||||||
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): CamouflageDefinitions_CamouflagesEntry {
|
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
|
||||||
const message = createBaseCamouflageDefinitions_CamouflagesEntry();
|
|
||||||
while (reader.pos < end) {
|
|
||||||
const tag = reader.uint32();
|
|
||||||
switch (tag >>> 3) {
|
|
||||||
case 1: {
|
|
||||||
if (tag !== 8) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.key = reader.uint32();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
case 2: {
|
|
||||||
if (tag !== 18) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.value = Camouflage.decode(reader, reader.uint32());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
if (message.value !== undefined) {
|
||||||
break;
|
Camouflage.encode(message.value, writer.uint32(18).fork()).join();
|
||||||
}
|
}
|
||||||
reader.skip(tag & 7);
|
return writer;
|
||||||
}
|
},
|
||||||
return message;
|
|
||||||
},
|
|
||||||
|
|
||||||
fromJSON(object: any): CamouflageDefinitions_CamouflagesEntry {
|
decode(
|
||||||
return {
|
input: BinaryReader | Uint8Array,
|
||||||
key: globalThis.Number(assertSet("CamouflageDefinitions_CamouflagesEntry.key", object.key)),
|
length?: number,
|
||||||
value: Camouflage.fromJSON(assertSet("CamouflageDefinitions_CamouflagesEntry.value", object.value)),
|
): CamouflageDefinitions_CamouflagesEntry {
|
||||||
};
|
const reader =
|
||||||
},
|
input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseCamouflageDefinitions_CamouflagesEntry();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 8) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
toJSON(message: CamouflageDefinitions_CamouflagesEntry): unknown {
|
message.key = reader.uint32();
|
||||||
const obj: any = {};
|
continue;
|
||||||
if (message.key !== undefined) {
|
}
|
||||||
obj.key = Math.round(message.key);
|
case 2: {
|
||||||
}
|
if (tag !== 18) {
|
||||||
if (message.value !== undefined) {
|
break;
|
||||||
obj.value = Camouflage.toJSON(message.value);
|
}
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
|
|
||||||
create<I extends Exact<DeepPartial<CamouflageDefinitions_CamouflagesEntry>, I>>(
|
message.value = Camouflage.decode(reader, reader.uint32());
|
||||||
base?: I,
|
continue;
|
||||||
): CamouflageDefinitions_CamouflagesEntry {
|
}
|
||||||
return CamouflageDefinitions_CamouflagesEntry.fromPartial(base ?? ({} as any));
|
}
|
||||||
},
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
fromPartial<I extends Exact<DeepPartial<CamouflageDefinitions_CamouflagesEntry>, I>>(
|
break;
|
||||||
object: I,
|
}
|
||||||
): CamouflageDefinitions_CamouflagesEntry {
|
reader.skip(tag & 7);
|
||||||
const message = createBaseCamouflageDefinitions_CamouflagesEntry();
|
}
|
||||||
message.key = object.key ?? 0;
|
return message;
|
||||||
message.value = (object.value !== undefined && object.value !== null)
|
},
|
||||||
? Camouflage.fromPartial(object.value)
|
|
||||||
: createBaseCamouflage();
|
fromJSON(object: any): CamouflageDefinitions_CamouflagesEntry {
|
||||||
return message;
|
return {
|
||||||
},
|
key: globalThis.Number(
|
||||||
};
|
assertSet('CamouflageDefinitions_CamouflagesEntry.key', object.key),
|
||||||
|
),
|
||||||
|
value: Camouflage.fromJSON(
|
||||||
|
assertSet(
|
||||||
|
'CamouflageDefinitions_CamouflagesEntry.value',
|
||||||
|
object.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: CamouflageDefinitions_CamouflagesEntry): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.key !== undefined) {
|
||||||
|
obj.key = Math.round(message.key);
|
||||||
|
}
|
||||||
|
if (message.value !== undefined) {
|
||||||
|
obj.value = Camouflage.toJSON(message.value);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<
|
||||||
|
I extends Exact<DeepPartial<CamouflageDefinitions_CamouflagesEntry>, I>,
|
||||||
|
>(base?: I): CamouflageDefinitions_CamouflagesEntry {
|
||||||
|
return CamouflageDefinitions_CamouflagesEntry.fromPartial(
|
||||||
|
base ?? ({} as any),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
fromPartial<
|
||||||
|
I extends Exact<DeepPartial<CamouflageDefinitions_CamouflagesEntry>, I>,
|
||||||
|
>(object: I): CamouflageDefinitions_CamouflagesEntry {
|
||||||
|
const message = createBaseCamouflageDefinitions_CamouflagesEntry();
|
||||||
|
message.key = object.key ?? 0;
|
||||||
|
message.value =
|
||||||
|
object.value !== undefined && object.value !== null
|
||||||
|
? Camouflage.fromPartial(object.value)
|
||||||
|
: createBaseCamouflage();
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export function createBaseCamouflage(): Camouflage {
|
export function createBaseCamouflage(): Camouflage {
|
||||||
return { id: 0, name: createBaseI18nString(), tank_name: undefined, tank_name_full: undefined };
|
return {
|
||||||
|
id: 0,
|
||||||
|
name: createBaseI18nString(),
|
||||||
|
tank_name: undefined,
|
||||||
|
tank_name_full: undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Camouflage: MessageFns<Camouflage> = {
|
export const Camouflage: MessageFns<Camouflage> = {
|
||||||
encode(message: Camouflage, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
encode(
|
||||||
|
message: Camouflage,
|
||||||
|
writer: BinaryWriter = new BinaryWriter(),
|
||||||
|
): BinaryWriter {
|
||||||
if (message.id !== 0) {
|
if (message.id !== 0) {
|
||||||
writer.uint32(8).uint32(message.id);
|
writer.uint32(8).uint32(message.id);
|
||||||
}
|
}
|
||||||
@@ -206,13 +250,17 @@ export const Camouflage: MessageFns<Camouflage> = {
|
|||||||
I18nString.encode(message.tank_name, writer.uint32(26).fork()).join();
|
I18nString.encode(message.tank_name, writer.uint32(26).fork()).join();
|
||||||
}
|
}
|
||||||
if (message.tank_name_full !== undefined) {
|
if (message.tank_name_full !== undefined) {
|
||||||
I18nString.encode(message.tank_name_full, writer.uint32(34).fork()).join();
|
I18nString.encode(
|
||||||
|
message.tank_name_full,
|
||||||
|
writer.uint32(34).fork(),
|
||||||
|
).join();
|
||||||
}
|
}
|
||||||
return writer;
|
return writer;
|
||||||
},
|
},
|
||||||
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): Camouflage {
|
decode(input: BinaryReader | Uint8Array, length?: number): Camouflage {
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
const reader =
|
||||||
|
input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
const message = createBaseCamouflage();
|
const message = createBaseCamouflage();
|
||||||
while (reader.pos < end) {
|
while (reader.pos < end) {
|
||||||
@@ -261,10 +309,14 @@ export const Camouflage: MessageFns<Camouflage> = {
|
|||||||
|
|
||||||
fromJSON(object: any): Camouflage {
|
fromJSON(object: any): Camouflage {
|
||||||
return {
|
return {
|
||||||
id: globalThis.Number(assertSet("Camouflage.id", object.id)),
|
id: globalThis.Number(assertSet('Camouflage.id', object.id)),
|
||||||
name: I18nString.fromJSON(assertSet("Camouflage.name", object.name)),
|
name: I18nString.fromJSON(assertSet('Camouflage.name', object.name)),
|
||||||
tank_name: isSet(object.tank_name) ? I18nString.fromJSON(object.tank_name) : undefined,
|
tank_name: isSet(object.tank_name)
|
||||||
tank_name_full: isSet(object.tank_name_full) ? I18nString.fromJSON(object.tank_name_full) : undefined,
|
? I18nString.fromJSON(object.tank_name)
|
||||||
|
: undefined,
|
||||||
|
tank_name_full: isSet(object.tank_name_full)
|
||||||
|
? I18nString.fromJSON(object.tank_name_full)
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -288,37 +340,57 @@ export const Camouflage: MessageFns<Camouflage> = {
|
|||||||
create<I extends Exact<DeepPartial<Camouflage>, I>>(base?: I): Camouflage {
|
create<I extends Exact<DeepPartial<Camouflage>, I>>(base?: I): Camouflage {
|
||||||
return Camouflage.fromPartial(base ?? ({} as any));
|
return Camouflage.fromPartial(base ?? ({} as any));
|
||||||
},
|
},
|
||||||
fromPartial<I extends Exact<DeepPartial<Camouflage>, I>>(object: I): Camouflage {
|
fromPartial<I extends Exact<DeepPartial<Camouflage>, I>>(
|
||||||
|
object: I,
|
||||||
|
): Camouflage {
|
||||||
const message = createBaseCamouflage();
|
const message = createBaseCamouflage();
|
||||||
message.id = object.id ?? 0;
|
message.id = object.id ?? 0;
|
||||||
message.name = (object.name !== undefined && object.name !== null)
|
message.name =
|
||||||
? I18nString.fromPartial(object.name)
|
object.name !== undefined && object.name !== null
|
||||||
: createBaseI18nString();
|
? I18nString.fromPartial(object.name)
|
||||||
message.tank_name = (object.tank_name !== undefined && object.tank_name !== null)
|
: createBaseI18nString();
|
||||||
? I18nString.fromPartial(object.tank_name)
|
message.tank_name =
|
||||||
: undefined;
|
object.tank_name !== undefined && object.tank_name !== null
|
||||||
message.tank_name_full = (object.tank_name_full !== undefined && object.tank_name_full !== null)
|
? I18nString.fromPartial(object.tank_name)
|
||||||
? I18nString.fromPartial(object.tank_name_full)
|
: undefined;
|
||||||
: undefined;
|
message.tank_name_full =
|
||||||
|
object.tank_name_full !== undefined && object.tank_name_full !== null
|
||||||
|
? I18nString.fromPartial(object.tank_name_full)
|
||||||
|
: undefined;
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
type Builtin =
|
||||||
|
| Date
|
||||||
|
| Function
|
||||||
|
| Uint8Array
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| undefined;
|
||||||
|
|
||||||
export type DeepPartial<T> = T extends Builtin ? T
|
export type DeepPartial<T> = T extends Builtin
|
||||||
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
? T
|
||||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
: T extends globalThis.Array<infer U>
|
||||||
: T extends { $case: string; value: unknown } ? { $case: T["$case"]; value?: DeepPartial<T["value"]> }
|
? globalThis.Array<DeepPartial<U>>
|
||||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
: T extends ReadonlyArray<infer U>
|
||||||
: Partial<T>;
|
? ReadonlyArray<DeepPartial<U>>
|
||||||
|
: T extends { $case: string; value: unknown }
|
||||||
|
? { $case: T['$case']; value?: DeepPartial<T['value']> }
|
||||||
|
: T extends {}
|
||||||
|
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
|
: Partial<T>;
|
||||||
|
|
||||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||||
export type Exact<P, I extends P> = P extends Builtin ? P
|
export type Exact<P, I extends P> = P extends Builtin
|
||||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
? P
|
||||||
|
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
|
||||||
|
[K in Exclude<keyof I, KeysOfUnion<P>>]: never;
|
||||||
|
};
|
||||||
|
|
||||||
function isObject(value: any): boolean {
|
function isObject(value: any): boolean {
|
||||||
return typeof value === "object" && value !== null;
|
return typeof value === 'object' && value !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSet(value: any): boolean {
|
function isSet(value: any): boolean {
|
||||||
|
|||||||
@@ -108,55 +108,55 @@ 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 {
|
message AssaultRanges {
|
||||||
required uint32 id = 1;
|
repeated AssaultRange ranges = 1;
|
||||||
required float rotation_speed = 2;
|
repeated ShellType types = 2;
|
||||||
optional ResearchCost research_cost = 3;
|
}
|
||||||
required uint32 weight = 4;
|
|
||||||
required I18nString name = 5;
|
message AssaultRange {
|
||||||
required uint32 tier = 6;
|
required float distance = 1;
|
||||||
repeated ShellDefinition shells = 7;
|
required float factor = 2;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message ShellDefinition {
|
message ShellDefinition {
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -790,6 +790,7 @@
|
|||||||
"fire.caliber": "caliber",
|
"fire.caliber": "caliber",
|
||||||
"fire.standardPenetration": "standard penetration",
|
"fire.standardPenetration": "standard penetration",
|
||||||
"fire.premiumPenetration": "premium penetration",
|
"fire.premiumPenetration": "premium penetration",
|
||||||
|
"fire.tertiaryPenetration": "tertiary penetration",
|
||||||
"fire.damage": "damage",
|
"fire.damage": "damage",
|
||||||
"fire.moduleDamage": "module damage",
|
"fire.moduleDamage": "module damage",
|
||||||
"fire.shellVelocity": "shell velocity",
|
"fire.shellVelocity": "shell velocity",
|
||||||
@@ -886,6 +887,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": {
|
||||||
|
"assault": {
|
||||||
|
"title": "Assault shells"
|
||||||
|
},
|
||||||
|
|
||||||
"rotator": {
|
"rotator": {
|
||||||
"hull": "Hull",
|
"hull": "Hull",
|
||||||
"turret": "Turret",
|
"turret": "Turret",
|
||||||
@@ -908,6 +913,13 @@
|
|||||||
"percentage": "%s1%",
|
"percentage": "%s1%",
|
||||||
"ghost_factory": "Ghost Factory",
|
"ghost_factory": "Ghost Factory",
|
||||||
"dead_rail": "Dead Rail"
|
"dead_rail": "Dead Rail"
|
||||||
|
},
|
||||||
|
|
||||||
|
"flexibility": {
|
||||||
|
"yaw": "Yaw",
|
||||||
|
"pitch": "Pitch",
|
||||||
|
"min": "Min",
|
||||||
|
"max": "Max"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1021,7 +1033,6 @@
|
|||||||
|
|
||||||
"review": {
|
"review": {
|
||||||
"title": "Review videos",
|
"title": "Review videos",
|
||||||
"disclaimer": "BlitzKit does not endorse any review videos.",
|
|
||||||
"join": "Apply to %s1 YouTuber program"
|
"join": "Apply to %s1 YouTuber program"
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1078,7 +1089,6 @@
|
|||||||
"hull_traverse_speed": "Hull traverse speed",
|
"hull_traverse_speed": "Hull traverse speed",
|
||||||
"camouflage": "Camouflage",
|
"camouflage": "Camouflage",
|
||||||
"reload": "Reload",
|
"reload": "Reload",
|
||||||
|
|
||||||
"crewCount": "Crew count",
|
"crewCount": "Crew count",
|
||||||
"shellNormalization": "Normalization",
|
"shellNormalization": "Normalization",
|
||||||
"shellRicochet": "Ricochet",
|
"shellRicochet": "Ricochet",
|
||||||
@@ -1086,12 +1096,13 @@
|
|||||||
"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",
|
||||||
"damage": "Damage",
|
"damage": "Damage",
|
||||||
|
"damageWithoutAssault": "Damage without assault shells",
|
||||||
"clipDamage": "Clipping potential",
|
"clipDamage": "Clipping potential",
|
||||||
"moduleDamage": "Module damage",
|
"moduleDamage": "Module damage",
|
||||||
"explosionRadius": "Explosion radius",
|
"explosionRadius": "Explosion radius",
|
||||||
@@ -1144,7 +1155,9 @@
|
|||||||
"shellRange": "Shell range",
|
"shellRange": "Shell range",
|
||||||
"shellCapacity": "Shell capacity",
|
"shellCapacity": "Shell capacity",
|
||||||
"gunType": "Gun type",
|
"gunType": "Gun type",
|
||||||
"penetrationAt250m": "Penetration at 250m"
|
"penetrationAt250m": "Penetration at 250m",
|
||||||
|
"burstShells": "Burst shell count",
|
||||||
|
"burstInterShell": "Burst inter-shell"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -698,6 +698,7 @@
|
|||||||
"fire.caliber": "calibre",
|
"fire.caliber": "calibre",
|
||||||
"fire.standardPenetration": "penetración estándar",
|
"fire.standardPenetration": "penetración estándar",
|
||||||
"fire.premiumPenetration": "penetración prémium",
|
"fire.premiumPenetration": "penetración prémium",
|
||||||
|
"fire.tertiaryPenetration": "tertiary penetration",
|
||||||
"fire.damage": "daño",
|
"fire.damage": "daño",
|
||||||
"fire.moduleDamage": "daño al módulo",
|
"fire.moduleDamage": "daño al módulo",
|
||||||
"fire.shellVelocity": "velocidad del proyectil",
|
"fire.shellVelocity": "velocidad del proyectil",
|
||||||
@@ -786,6 +787,9 @@
|
|||||||
"tank_seo_title": "%s1 - Nivel %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
"tank_seo_title": "%s1 - Nivel %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
||||||
"tank_seo_description": "%s1 blindaje, estadisticas, equipamiento, proviciones y mas.\n%s2 DPM, %3hp alfa, %s4mm penetracion, %s5kph velocidad maxima, %s6 hp salud",
|
"tank_seo_description": "%s1 blindaje, estadisticas, equipamiento, proviciones y mas.\n%s2 DPM, %3hp alfa, %s4mm penetracion, %s5kph velocidad maxima, %s6 hp salud",
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
|
"assault": {
|
||||||
|
"title": "Assault shells"
|
||||||
|
},
|
||||||
"rotator": {
|
"rotator": {
|
||||||
"hull": "Hull",
|
"hull": "Hull",
|
||||||
"turret": "Turret",
|
"turret": "Turret",
|
||||||
@@ -806,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": {
|
||||||
@@ -908,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": {
|
||||||
@@ -969,12 +978,13 @@
|
|||||||
"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",
|
||||||
"damage": "Daño",
|
"damage": "Daño",
|
||||||
|
"damageWithoutAssault": "Damage without assault shells",
|
||||||
"clipDamage": "Clipping potential",
|
"clipDamage": "Clipping potential",
|
||||||
"moduleDamage": "Daño a módulos",
|
"moduleDamage": "Daño a módulos",
|
||||||
"explosionRadius": "Radio de Explosión",
|
"explosionRadius": "Radio de Explosión",
|
||||||
@@ -1027,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": {
|
||||||
|
|||||||
@@ -698,6 +698,7 @@
|
|||||||
"fire.caliber": "caliber",
|
"fire.caliber": "caliber",
|
||||||
"fire.standardPenetration": "standard penetration",
|
"fire.standardPenetration": "standard penetration",
|
||||||
"fire.premiumPenetration": "premium penetration",
|
"fire.premiumPenetration": "premium penetration",
|
||||||
|
"fire.tertiaryPenetration": "tertiary penetration",
|
||||||
"fire.damage": "damage",
|
"fire.damage": "damage",
|
||||||
"fire.moduleDamage": "module damage",
|
"fire.moduleDamage": "module damage",
|
||||||
"fire.shellVelocity": "shell velocity",
|
"fire.shellVelocity": "shell velocity",
|
||||||
@@ -786,6 +787,9 @@
|
|||||||
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
||||||
"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": {
|
||||||
|
"assault": {
|
||||||
|
"title": "Assault shells"
|
||||||
|
},
|
||||||
"rotator": {
|
"rotator": {
|
||||||
"hull": "Hull",
|
"hull": "Hull",
|
||||||
"turret": "Turret",
|
"turret": "Turret",
|
||||||
@@ -806,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": {
|
||||||
@@ -908,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": {
|
||||||
@@ -969,12 +978,13 @@
|
|||||||
"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",
|
||||||
"damage": "Dégâts",
|
"damage": "Dégâts",
|
||||||
|
"damageWithoutAssault": "Damage without assault shells",
|
||||||
"clipDamage": "Clipping potential",
|
"clipDamage": "Clipping potential",
|
||||||
"moduleDamage": "Module damage",
|
"moduleDamage": "Module damage",
|
||||||
"explosionRadius": "Explosion radius",
|
"explosionRadius": "Explosion radius",
|
||||||
@@ -1027,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": {
|
||||||
|
|||||||
@@ -698,6 +698,7 @@
|
|||||||
"fire.caliber": "砲口径",
|
"fire.caliber": "砲口径",
|
||||||
"fire.standardPenetration": "通常弾貫通力",
|
"fire.standardPenetration": "通常弾貫通力",
|
||||||
"fire.premiumPenetration": "課金弾貫通力",
|
"fire.premiumPenetration": "課金弾貫通力",
|
||||||
|
"fire.tertiaryPenetration": "tertiary penetration",
|
||||||
"fire.damage": "ダメージ",
|
"fire.damage": "ダメージ",
|
||||||
"fire.moduleDamage": "モジュールダメージ",
|
"fire.moduleDamage": "モジュールダメージ",
|
||||||
"fire.shellVelocity": "弾速",
|
"fire.shellVelocity": "弾速",
|
||||||
@@ -786,6 +787,9 @@
|
|||||||
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
||||||
"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": {
|
||||||
|
"assault": {
|
||||||
|
"title": "Assault shells"
|
||||||
|
},
|
||||||
"rotator": {
|
"rotator": {
|
||||||
"hull": "Hull",
|
"hull": "Hull",
|
||||||
"turret": "Turret",
|
"turret": "Turret",
|
||||||
@@ -806,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": {
|
||||||
@@ -908,7 +918,6 @@
|
|||||||
},
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"title": "Review videos",
|
"title": "Review videos",
|
||||||
"disclaimer": "BlitzKitはいかなるレビュー動画も後援していません。",
|
|
||||||
"join": "%s1 YouTuberプログラムに応募する"
|
"join": "%s1 YouTuberプログラムに応募する"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -969,12 +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",
|
||||||
"clipDamage": "弾倉火力",
|
"clipDamage": "弾倉火力",
|
||||||
"moduleDamage": "モジュールダメージ",
|
"moduleDamage": "モジュールダメージ",
|
||||||
"explosionRadius": "爆風半径",
|
"explosionRadius": "爆風半径",
|
||||||
@@ -1027,7 +1037,9 @@
|
|||||||
"shellRange": "射程",
|
"shellRange": "射程",
|
||||||
"shellCapacity": "総弾数",
|
"shellCapacity": "総弾数",
|
||||||
"gunType": "主砲タイプ",
|
"gunType": "主砲タイプ",
|
||||||
"penetrationAt250m": "250m での貫通力"
|
"penetrationAt250m": "250m での貫通力",
|
||||||
|
"burstShells": "Burst shell count",
|
||||||
|
"burstInterShell": "Burst inter-shell"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"firepower": {
|
"firepower": {
|
||||||
|
|||||||
@@ -380,7 +380,7 @@
|
|||||||
},
|
},
|
||||||
"embeds": {
|
"embeds": {
|
||||||
"name": "osadzenie",
|
"name": "osadzenie",
|
||||||
"description": "Jak działa osadzanie"
|
"description": "Jak działają osadzone"
|
||||||
},
|
},
|
||||||
"introduction": {
|
"introduction": {
|
||||||
"name": "wprowadzenie",
|
"name": "wprowadzenie",
|
||||||
@@ -698,6 +698,7 @@
|
|||||||
"fire.caliber": "caliber",
|
"fire.caliber": "caliber",
|
||||||
"fire.standardPenetration": "standard penetration",
|
"fire.standardPenetration": "standard penetration",
|
||||||
"fire.premiumPenetration": "premium penetration",
|
"fire.premiumPenetration": "premium penetration",
|
||||||
|
"fire.tertiaryPenetration": "tertiary penetration",
|
||||||
"fire.damage": "damage",
|
"fire.damage": "damage",
|
||||||
"fire.moduleDamage": "module damage",
|
"fire.moduleDamage": "module damage",
|
||||||
"fire.shellVelocity": "shell velocity",
|
"fire.shellVelocity": "shell velocity",
|
||||||
@@ -786,6 +787,9 @@
|
|||||||
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
||||||
"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": {
|
||||||
|
"assault": {
|
||||||
|
"title": "Assault shells"
|
||||||
|
},
|
||||||
"rotator": {
|
"rotator": {
|
||||||
"hull": "Hull",
|
"hull": "Hull",
|
||||||
"turret": "Turret",
|
"turret": "Turret",
|
||||||
@@ -806,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": {
|
||||||
@@ -908,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": {
|
||||||
@@ -969,12 +978,13 @@
|
|||||||
"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",
|
||||||
"damage": "Uszkodzenia",
|
"damage": "Uszkodzenia",
|
||||||
|
"damageWithoutAssault": "Damage without assault shells",
|
||||||
"clipDamage": "Clipping potential",
|
"clipDamage": "Clipping potential",
|
||||||
"moduleDamage": "Module damage",
|
"moduleDamage": "Module damage",
|
||||||
"explosionRadius": "Explosion radius",
|
"explosionRadius": "Explosion radius",
|
||||||
@@ -1027,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": {
|
||||||
|
|||||||
@@ -698,6 +698,7 @@
|
|||||||
"fire.caliber": "calibre",
|
"fire.caliber": "calibre",
|
||||||
"fire.standardPenetration": "penetração padrão",
|
"fire.standardPenetration": "penetração padrão",
|
||||||
"fire.premiumPenetration": "penetração premium",
|
"fire.premiumPenetration": "penetração premium",
|
||||||
|
"fire.tertiaryPenetration": "tertiary penetration",
|
||||||
"fire.damage": "dano",
|
"fire.damage": "dano",
|
||||||
"fire.moduleDamage": "dano a módulos",
|
"fire.moduleDamage": "dano a módulos",
|
||||||
"fire.shellVelocity": "velocidade do projétil",
|
"fire.shellVelocity": "velocidade do projétil",
|
||||||
@@ -786,6 +787,9 @@
|
|||||||
"tank_seo_title": "%s1 - Nível %s2 %s3 %s4 | %s5 Tanquepedia | World of Tanks Blitz (WoTB)",
|
"tank_seo_title": "%s1 - Nível %s2 %s3 %s4 | %s5 Tanquepedia | World of Tanks Blitz (WoTB)",
|
||||||
"tank_seo_description": "%s1 blindagem, estatísticas, equipamentos, provisões e mais. %s2 DPM, %s3hp alfa, %s4mm de penetração, %s5km/h velocidade máxima, %s6hp vida.",
|
"tank_seo_description": "%s1 blindagem, estatísticas, equipamentos, provisões e mais. %s2 DPM, %s3hp alfa, %s4mm de penetração, %s5km/h velocidade máxima, %s6hp vida.",
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
|
"assault": {
|
||||||
|
"title": "Assault shells"
|
||||||
|
},
|
||||||
"rotator": {
|
"rotator": {
|
||||||
"hull": "Hull",
|
"hull": "Hull",
|
||||||
"turret": "Turret",
|
"turret": "Turret",
|
||||||
@@ -806,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": {
|
||||||
@@ -908,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": {
|
||||||
@@ -969,12 +978,13 @@
|
|||||||
"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",
|
||||||
"damage": "Dano",
|
"damage": "Dano",
|
||||||
|
"damageWithoutAssault": "Damage without assault shells",
|
||||||
"clipDamage": "Potencial de clipe",
|
"clipDamage": "Potencial de clipe",
|
||||||
"moduleDamage": "Dano a módulos",
|
"moduleDamage": "Dano a módulos",
|
||||||
"explosionRadius": "Raio de Explosão",
|
"explosionRadius": "Raio de Explosão",
|
||||||
@@ -1027,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": {
|
||||||
|
|||||||
@@ -697,7 +697,8 @@
|
|||||||
"fire.reload": "перезарядка",
|
"fire.reload": "перезарядка",
|
||||||
"fire.caliber": "калибр",
|
"fire.caliber": "калибр",
|
||||||
"fire.standardPenetration": "базовое пробитие",
|
"fire.standardPenetration": "базовое пробитие",
|
||||||
"fire.premiumPenetration": "улучшенное пробитие ",
|
"fire.premiumPenetration": "премиумное пробитие ",
|
||||||
|
"fire.tertiaryPenetration": "пробитие третьего снаряда",
|
||||||
"fire.damage": "урон",
|
"fire.damage": "урон",
|
||||||
"fire.moduleDamage": "урон по модулям",
|
"fire.moduleDamage": "урон по модулям",
|
||||||
"fire.shellVelocity": "скорость снаряда",
|
"fire.shellVelocity": "скорость снаряда",
|
||||||
@@ -786,6 +787,9 @@
|
|||||||
"tank_seo_title": "%s1 - %s4 %s3 %s2 уровня | Танкопедия %s5 | World of Tanks Blitz (WoTB)",
|
"tank_seo_title": "%s1 - %s4 %s3 %s2 уровня | Танкопедия %s5 | World of Tanks Blitz (WoTB)",
|
||||||
"tank_seo_description": "%s1 броня, статистика, оборудование, снабжение и многое другое. %s2 УВМ, %s3hp альфа, %s4mm пробитие, %s5км/ч лучшая скорость, %s6hp здоровье.",
|
"tank_seo_description": "%s1 броня, статистика, оборудование, снабжение и многое другое. %s2 УВМ, %s3hp альфа, %s4mm пробитие, %s5км/ч лучшая скорость, %s6hp здоровье.",
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
|
"assault": {
|
||||||
|
"title": "Штурмовые снаряды"
|
||||||
|
},
|
||||||
"rotator": {
|
"rotator": {
|
||||||
"hull": "Корпус",
|
"hull": "Корпус",
|
||||||
"turret": "Башня",
|
"turret": "Башня",
|
||||||
@@ -806,6 +810,12 @@
|
|||||||
"percentage": "%s1%",
|
"percentage": "%s1%",
|
||||||
"ghost_factory": "Промзона",
|
"ghost_factory": "Промзона",
|
||||||
"dead_rail": "Эшелон"
|
"dead_rail": "Эшелон"
|
||||||
|
},
|
||||||
|
"flexibility": {
|
||||||
|
"yaw": "Отклонение",
|
||||||
|
"pitch": "Наклон",
|
||||||
|
"min": "Вниз",
|
||||||
|
"max": "Вверх"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
@@ -908,7 +918,6 @@
|
|||||||
},
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"title": "Видеообзоры",
|
"title": "Видеообзоры",
|
||||||
"disclaimer": "BlitzKit не несёт ответственности за видеообзоры.",
|
|
||||||
"join": "Предложить ютубера для программы %s1"
|
"join": "Предложить ютубера для программы %s1"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -969,12 +978,13 @@
|
|||||||
"dpmEffective": "Эффективность за 60с",
|
"dpmEffective": "Эффективность за 60с",
|
||||||
"shells": "Снарядов в барабане",
|
"shells": "Снарядов в барабане",
|
||||||
"mostOptimalShellIndex": "Самый эффективный снаряд",
|
"mostOptimalShellIndex": "Самый эффективный снаряд",
|
||||||
"shellReloads": "Перезарядка снарядов",
|
"shellReloads": "Reloads",
|
||||||
"shellReload": "Перезарядка",
|
"shellReload": "Перезарядка",
|
||||||
"intraClip": "Между снарядами",
|
"intraClip": "Между снарядами",
|
||||||
"caliber": "Калибр",
|
"caliber": "Калибр",
|
||||||
"penetration": "Пробитие",
|
"penetration": "Пробитие",
|
||||||
"damage": "Урон",
|
"damage": "Урон",
|
||||||
|
"damageWithoutAssault": "Урон без штурмовых снарядов",
|
||||||
"clipDamage": "Урон с барабана",
|
"clipDamage": "Урон с барабана",
|
||||||
"moduleDamage": "Урон по модулям",
|
"moduleDamage": "Урон по модулям",
|
||||||
"explosionRadius": "Разлет осколков",
|
"explosionRadius": "Разлет осколков",
|
||||||
@@ -1027,7 +1037,9 @@
|
|||||||
"shellRange": "Дальность снаряда",
|
"shellRange": "Дальность снаряда",
|
||||||
"shellCapacity": "Боезапас",
|
"shellCapacity": "Боезапас",
|
||||||
"gunType": "Тип орудия",
|
"gunType": "Тип орудия",
|
||||||
"penetrationAt250m": "Пробитие на 250м"
|
"penetrationAt250m": "Пробитие на 250м",
|
||||||
|
"burstShells": "Burst shell count",
|
||||||
|
"burstInterShell": "Burst inter-shell"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"firepower": {
|
"firepower": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"test": {
|
"test": {
|
||||||
"default": "тест",
|
"default": "тест",
|
||||||
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is %s1 because it has the color of %s2. I like it so much that I will say its name 3 times! %s1! %s1! %s1! Once again, it's %s2, not %s3.",
|
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is %s1 because it has the color of %s2. I like it so much that I will say its name 3 times! %s1! %s1! %s1! Once again, it's %s2, not %s3.",
|
||||||
"fox": "The quick brown fox jumps over the lazy dog."
|
"fox": "Швидка бура лисиця перестрибує через лінивого собаку."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"units": {
|
"units": {
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"hp": "%s1хп",
|
"hp": "%s1хп",
|
||||||
"x": "х%s1",
|
"x": "х%s1",
|
||||||
"percentage": "%s1%",
|
"percentage": "%s1%",
|
||||||
"deg_s": "%s1°/s"
|
"deg_s": "%s1°/с"
|
||||||
},
|
},
|
||||||
"gun_types": {
|
"gun_types": {
|
||||||
"auto_loader": "Магазинна система заряджання",
|
"auto_loader": "Магазинна система заряджання",
|
||||||
@@ -413,10 +413,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"name": "debug",
|
"name": "налагодження",
|
||||||
"description": "Debug information about the bot",
|
"description": "Інформація про налагодження бота",
|
||||||
"body": {
|
"body": {
|
||||||
"title": "# Debug\n\n",
|
"title": "# Налагодження\n\n",
|
||||||
"version": "Версія",
|
"version": "Версія",
|
||||||
"shard": {
|
"shard": {
|
||||||
"label": "Уламок",
|
"label": "Уламок",
|
||||||
@@ -499,7 +499,7 @@
|
|||||||
},
|
},
|
||||||
"embed": {
|
"embed": {
|
||||||
"title": "Невірний обліковий запис?",
|
"title": "Невірний обліковий запис?",
|
||||||
"description": "Use the command again and select a username from the search results."
|
"description": "Використайте команду ще раз і виберіть ім'я користувача з результатів пошуку."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search_clans": {
|
"search_clans": {
|
||||||
@@ -508,7 +508,7 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"limit": {
|
"limit": {
|
||||||
"name": "ліміт",
|
"name": "ліміт",
|
||||||
"description": "The max number of clan results (default: %s1)"
|
"description": "Максимальна кількість результатів клану (за замовчуванням: %s1)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
@@ -531,16 +531,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rating_leaderboard": {
|
"rating_leaderboard": {
|
||||||
"name": "rating-leaderboard",
|
"name": "таблиця рейтингу",
|
||||||
"description": "Rating battles statistics",
|
"description": "Статистика рейтингових боїв",
|
||||||
"subcommands": {
|
"subcommands": {
|
||||||
"neighbors": {
|
"neighbors": {
|
||||||
"name": "neighbors",
|
"name": "neighbors",
|
||||||
"description": "Neighboring players on the leaderboard"
|
"description": "Neighboring players on the leaderboard"
|
||||||
},
|
},
|
||||||
"league": {
|
"league": {
|
||||||
"name": "league",
|
"name": "ліга",
|
||||||
"description": "Top players of a league"
|
"description": "Найкращий гравець ліги"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -549,8 +549,8 @@
|
|||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
"season": {
|
"season": {
|
||||||
"name": "season",
|
"name": "сезон",
|
||||||
"description": "Season number (default: current)",
|
"description": "Базова інформація про гравця",
|
||||||
"choices": {
|
"choices": {
|
||||||
"current": "%s1 (current)"
|
"current": "%s1 (current)"
|
||||||
}
|
}
|
||||||
@@ -578,14 +578,14 @@
|
|||||||
"description": "Коротка статистика за період"
|
"description": "Коротка статистика за період"
|
||||||
},
|
},
|
||||||
"owned_tanks": {
|
"owned_tanks": {
|
||||||
"name": "owned-tanks",
|
"name": "отримані танки",
|
||||||
"description": "Shows all own tanks in a tier",
|
"description": "Показує всі ваші танки даного рівня",
|
||||||
"body": {
|
"body": {
|
||||||
"title": "# %s1's owned tier %s2 tanks\n\n"
|
"title": "# %s1's owned tier %s2 tanks\n\n"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"name": "permissions",
|
"name": "дозволи",
|
||||||
"description": "Checks wether the bot has the required permissions to function",
|
"description": "Checks wether the bot has the required permissions to function",
|
||||||
"body": {
|
"body": {
|
||||||
"title": "# Permissions\n\n%s1",
|
"title": "# Permissions\n\n%s1",
|
||||||
@@ -615,7 +615,7 @@
|
|||||||
},
|
},
|
||||||
"player_info": {
|
"player_info": {
|
||||||
"name": "player-info",
|
"name": "player-info",
|
||||||
"description": "Basic information about a player",
|
"description": "Базова інформація про гравця",
|
||||||
"body": {
|
"body": {
|
||||||
"title": "%s1's information",
|
"title": "%s1's information",
|
||||||
"nickname": "Нік",
|
"nickname": "Нік",
|
||||||
@@ -635,7 +635,7 @@
|
|||||||
"description": "Today's rating statistics"
|
"description": "Today's rating statistics"
|
||||||
},
|
},
|
||||||
"season": {
|
"season": {
|
||||||
"name": "season",
|
"name": "сезон",
|
||||||
"description": "This season's rating statistics"
|
"description": "This season's rating statistics"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -698,6 +698,7 @@
|
|||||||
"fire.caliber": "калібр",
|
"fire.caliber": "калібр",
|
||||||
"fire.standardPenetration": "базове пробиття",
|
"fire.standardPenetration": "базове пробиття",
|
||||||
"fire.premiumPenetration": "преміум пробиття",
|
"fire.premiumPenetration": "преміум пробиття",
|
||||||
|
"fire.tertiaryPenetration": "tertiary penetration",
|
||||||
"fire.damage": "шкода",
|
"fire.damage": "шкода",
|
||||||
"fire.moduleDamage": "шкода по модулях",
|
"fire.moduleDamage": "шкода по модулях",
|
||||||
"fire.shellVelocity": "швидкість польоту снаряда",
|
"fire.shellVelocity": "швидкість польоту снаряда",
|
||||||
@@ -786,6 +787,9 @@
|
|||||||
"tank_seo_title": "%s1 - Рівень %s2 %s3 %s4 | %s5 Танкопедія | WoT Blitz",
|
"tank_seo_title": "%s1 - Рівень %s2 %s3 %s4 | %s5 Танкопедія | WoT Blitz",
|
||||||
"tank_seo_description": "%s1 броня, характеристики, обладнання, амуніція та більше. %s2 шкода на хвилину, %s3 разова шкода, %s4 мм пробиття, %s5 км/год максимальна швидкість, %s6 міцність.",
|
"tank_seo_description": "%s1 броня, характеристики, обладнання, амуніція та більше. %s2 шкода на хвилину, %s3 разова шкода, %s4 мм пробиття, %s5 км/год максимальна швидкість, %s6 міцність.",
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
|
"assault": {
|
||||||
|
"title": "Assault shells"
|
||||||
|
},
|
||||||
"rotator": {
|
"rotator": {
|
||||||
"hull": "Hull",
|
"hull": "Hull",
|
||||||
"turret": "Turret",
|
"turret": "Turret",
|
||||||
@@ -806,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": {
|
||||||
@@ -908,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": {
|
||||||
@@ -969,12 +978,13 @@
|
|||||||
"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": "Бронепробиття",
|
||||||
"damage": "Шкода",
|
"damage": "Шкода",
|
||||||
|
"damageWithoutAssault": "Damage without assault shells",
|
||||||
"clipDamage": "Clipping potential",
|
"clipDamage": "Clipping potential",
|
||||||
"moduleDamage": "Шкода по модулях",
|
"moduleDamage": "Шкода по модулях",
|
||||||
"explosionRadius": "Радіус розльоту уламків",
|
"explosionRadius": "Радіус розльоту уламків",
|
||||||
@@ -1027,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": {
|
||||||
|
|||||||
@@ -41,15 +41,15 @@
|
|||||||
"preview": "预览"
|
"preview": "预览"
|
||||||
},
|
},
|
||||||
"locales": {
|
"locales": {
|
||||||
"en": "英文",
|
"en": "英语",
|
||||||
"es": "西班牙文",
|
"es": "西班牙语",
|
||||||
"pt": "葡萄牙文",
|
"pt": "葡萄牙语",
|
||||||
"ru": "俄文",
|
"ru": "俄语",
|
||||||
"uk": "乌克兰文",
|
"uk": "乌克兰语",
|
||||||
"zh": "中文",
|
"zh": "中文",
|
||||||
"ja": "日文",
|
"ja": "日语",
|
||||||
"fr": "法文",
|
"fr": "法语",
|
||||||
"pl": "波兰文"
|
"pl": "波兰语"
|
||||||
},
|
},
|
||||||
"wn8_percentile": {
|
"wn8_percentile": {
|
||||||
"0": "非常差",
|
"0": "非常差",
|
||||||
@@ -283,7 +283,7 @@
|
|||||||
"name": "阶级",
|
"name": "阶级",
|
||||||
"description": "阶级",
|
"description": "阶级",
|
||||||
"choices": {
|
"choices": {
|
||||||
"tier": "%s1- %s2 级"
|
"tier": "%s1阶 - %s2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -494,7 +494,7 @@
|
|||||||
"accounts_linked": "帐号已绑定",
|
"accounts_linked": "帐号已绑定",
|
||||||
"no_role_permissions": {
|
"no_role_permissions": {
|
||||||
"title": "%s1 验证失败",
|
"title": "%s1 验证失败",
|
||||||
"description": "我没有管理角色的权限。"
|
"description": "我没有管理身份的权限。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"embed": {
|
"embed": {
|
||||||
@@ -517,7 +517,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search_players": {
|
"search_players": {
|
||||||
"name": "搜索玩家",
|
"name": "搜索-玩家",
|
||||||
"description": "搜索玩家",
|
"description": "搜索玩家",
|
||||||
"options": {
|
"options": {
|
||||||
"limit": {
|
"limit": {
|
||||||
@@ -578,7 +578,7 @@
|
|||||||
"description": "一段时间内的快速统计"
|
"description": "一段时间内的快速统计"
|
||||||
},
|
},
|
||||||
"owned_tanks": {
|
"owned_tanks": {
|
||||||
"name": "已擁有的戰車",
|
"name": "已拥有的战车",
|
||||||
"description": "显示该阶级中所有您已拥有的战车",
|
"description": "显示该阶级中所有您已拥有的战车",
|
||||||
"body": {
|
"body": {
|
||||||
"title": "# %s1 所拥有的 %s2 阶战车\n\n"
|
"title": "# %s1 所拥有的 %s2 阶战车\n\n"
|
||||||
@@ -589,8 +589,8 @@
|
|||||||
"description": "检查机器人是否拥有运行所需的权限",
|
"description": "检查机器人是否拥有运行所需的权限",
|
||||||
"body": {
|
"body": {
|
||||||
"title": "# 权限\n\n%s1",
|
"title": "# 权限\n\n%s1",
|
||||||
"view_channels": "查看頻道:需要找到重新整理按鈕使用的訊息",
|
"view_channels": "查看频道:需要找到重新整理按钮使用的讯息",
|
||||||
"read_message_history": "讀取歷史訊息: 需要找到訊息以重新整理",
|
"read_message_history": "读取消息历史: 需要找到消息以刷新",
|
||||||
"attach_files": "附加文件:需要将图像添加到基于图像的命令",
|
"attach_files": "附加文件:需要将图像添加到基于图像的命令",
|
||||||
"external_emojis": "外部表情符号:需要添加国家旗帜和战车类别"
|
"external_emojis": "外部表情符号:需要添加国家旗帜和战车类别"
|
||||||
}
|
}
|
||||||
@@ -698,6 +698,7 @@
|
|||||||
"fire.caliber": "口径",
|
"fire.caliber": "口径",
|
||||||
"fire.standardPenetration": "标准炮弹穿透力",
|
"fire.standardPenetration": "标准炮弹穿透力",
|
||||||
"fire.premiumPenetration": "加值炮弹穿透力",
|
"fire.premiumPenetration": "加值炮弹穿透力",
|
||||||
|
"fire.tertiaryPenetration": "tertiary penetration",
|
||||||
"fire.damage": "炮弹伤害值",
|
"fire.damage": "炮弹伤害值",
|
||||||
"fire.moduleDamage": "模组伤害值",
|
"fire.moduleDamage": "模组伤害值",
|
||||||
"fire.shellVelocity": "炮弹飞行速度",
|
"fire.shellVelocity": "炮弹飞行速度",
|
||||||
@@ -786,6 +787,9 @@
|
|||||||
"tank_seo_title": "%s1 - %s2 阶 %s3 %s4 | %s5 战车百科 | 战车世界闪击战 (WoTB)",
|
"tank_seo_title": "%s1 - %s2 阶 %s3 %s4 | %s5 战车百科 | 战车世界闪击战 (WoTB)",
|
||||||
"tank_seo_description": "%s1 装甲,统计设备,装备,供给等。 %s2 每分钟伤害, %s3生命值, %s4毫米穿透, %s5kph 最高速度, %s6hp 生命值。",
|
"tank_seo_description": "%s1 装甲,统计设备,装备,供给等。 %s2 每分钟伤害, %s3生命值, %s4毫米穿透, %s5kph 最高速度, %s6hp 生命值。",
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
|
"assault": {
|
||||||
|
"title": "Assault shells"
|
||||||
|
},
|
||||||
"rotator": {
|
"rotator": {
|
||||||
"hull": "车体",
|
"hull": "车体",
|
||||||
"turret": "炮塔",
|
"turret": "炮塔",
|
||||||
@@ -796,9 +800,9 @@
|
|||||||
},
|
},
|
||||||
"ricochet": {
|
"ricochet": {
|
||||||
"angle": "角度",
|
"angle": "角度",
|
||||||
"normalization": "轉正",
|
"normalization": "转正",
|
||||||
"effective_angle": "有效角度",
|
"effective_angle": "有效角度",
|
||||||
"armor": "裝甲",
|
"armor": "装甲",
|
||||||
"no_ricochet": "高爆弹不会跳弹"
|
"no_ricochet": "高爆弹不会跳弹"
|
||||||
},
|
},
|
||||||
"view_range": {
|
"view_range": {
|
||||||
@@ -806,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": {
|
||||||
@@ -854,7 +864,7 @@
|
|||||||
"track": "履带"
|
"track": "履带"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"penetration": "%s1生命值 擊穿",
|
"penetration": "%s1生命值 击穿",
|
||||||
"blocked": "抵挡伤害",
|
"blocked": "抵挡伤害",
|
||||||
"ricochet": "跳弹",
|
"ricochet": "跳弹",
|
||||||
"splash": "%s1生命值 溅射"
|
"splash": "%s1生命值 溅射"
|
||||||
@@ -908,7 +918,6 @@
|
|||||||
},
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"title": "回顾影片",
|
"title": "回顾影片",
|
||||||
"disclaimer": "BlitzKit 不支援任何影片回顾。",
|
|
||||||
"join": "应用到 %s1 YouTuber程序"
|
"join": "应用到 %s1 YouTuber程序"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -969,12 +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",
|
||||||
"clipDamage": "弹匣伤害期望值",
|
"clipDamage": "弹匣伤害期望值",
|
||||||
"moduleDamage": "模组伤害",
|
"moduleDamage": "模组伤害",
|
||||||
"explosionRadius": "溅射半径",
|
"explosionRadius": "溅射半径",
|
||||||
@@ -1027,7 +1037,9 @@
|
|||||||
"shellRange": "射程",
|
"shellRange": "射程",
|
||||||
"shellCapacity": "携弹量",
|
"shellCapacity": "携弹量",
|
||||||
"gunType": "炮管类型",
|
"gunType": "炮管类型",
|
||||||
"penetrationAt250m": "两百五十米处的穿透力"
|
"penetrationAt250m": "两百五十米处的穿透力",
|
||||||
|
"burstShells": "Burst shell count",
|
||||||
|
"burstInterShell": "Burst inter-shell"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"firepower": {
|
"firepower": {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { FileChange } from '../core/github/commitMultipleFiles';
|
|||||||
export async function blitzkitTankIcons() {
|
export async function blitzkitTankIcons() {
|
||||||
console.log('Building blitzkit tank icons...');
|
console.log('Building blitzkit tank icons...');
|
||||||
|
|
||||||
console.log('Starting website dev server...');
|
|
||||||
const browser = await launch();
|
const browser = await launch();
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
const tanks = Object.values((await fetchTankDefinitions()).tanks);
|
const tanks = Object.values((await fetchTankDefinitions()).tanks);
|
||||||
@@ -19,12 +18,14 @@ export async function blitzkitTankIcons() {
|
|||||||
width: BLITZKIT_TANK_ICON_SIZE.width - 1,
|
width: BLITZKIT_TANK_ICON_SIZE.width - 1,
|
||||||
height: BLITZKIT_TANK_ICON_SIZE.height,
|
height: BLITZKIT_TANK_ICON_SIZE.height,
|
||||||
});
|
});
|
||||||
await page.goto(`http://localhost:4321/api/tankopedia/tank-icon/${id}/`, {
|
|
||||||
waitUntil: 'networkidle0',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (index === 0) {
|
try {
|
||||||
console.log('Dev server started!');
|
await page.goto(`http://localhost:4321/api/tankopedia/tank-icon/${id}/`, {
|
||||||
|
waitUntil: 'networkidle0',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${id} ${name} time out`, error);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const screenshot = await page.screenshot({
|
const screenshot = await page.screenshot({
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -21,7 +21,6 @@ import { gameModeBanners } from './gameModeBanners';
|
|||||||
import { glossary } from './glossary';
|
import { glossary } from './glossary';
|
||||||
import { migration } from './migration';
|
import { migration } from './migration';
|
||||||
import { moduleIcons } from './moduleIcons';
|
import { moduleIcons } from './moduleIcons';
|
||||||
import { popularTanks } from './popularTanks';
|
|
||||||
import { shellIcons } from './shellIcons';
|
import { shellIcons } from './shellIcons';
|
||||||
import { skillIcons } from './skillIcons';
|
import { skillIcons } from './skillIcons';
|
||||||
import { tankArmors } from './tankArmors';
|
import { tankArmors } from './tankArmors';
|
||||||
@@ -47,7 +46,7 @@ const methods = [
|
|||||||
boosterIcons,
|
boosterIcons,
|
||||||
tankIcons,
|
tankIcons,
|
||||||
blitzkitTankIcons,
|
blitzkitTankIcons,
|
||||||
popularTanks,
|
// popularTanks,
|
||||||
glossary,
|
glossary,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -33,8 +33,6 @@ export async function tankModels() {
|
|||||||
|
|
||||||
const id = toUniqueId(nation, tank.id);
|
const id = toUniqueId(nation, tank.id);
|
||||||
|
|
||||||
if (id !== 21777) continue;
|
|
||||||
|
|
||||||
const parameters = await readYAMLDVPL<TankParameters>(
|
const parameters = await readYAMLDVPL<TankParameters>(
|
||||||
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||||
);
|
);
|
||||||
@@ -43,8 +41,6 @@ export async function tankModels() {
|
|||||||
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ''),
|
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ''),
|
||||||
);
|
);
|
||||||
|
|
||||||
await writeFile('../../temp/model.glb', await nodeIO.writeBinary(model));
|
|
||||||
|
|
||||||
changes.push({
|
changes.push({
|
||||||
path: `3d/tanks/models/${id}.glb`,
|
path: `3d/tanks/models/${id}.glb`,
|
||||||
content: Buffer.from(await nodeIO.writeBinary(model)),
|
content: Buffer.from(await nodeIO.writeBinary(model)),
|
||||||
|
|||||||
@@ -73,12 +73,13 @@ export async function commitMultipleFiles(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (diff.status === DiffStatus.Unchanged) {
|
if (diff.status === DiffStatus.Unchanged) {
|
||||||
console.log(`🔵 (${diff.change.toLocaleString()}B) ${change.path}`);
|
// just don't bother
|
||||||
|
// console.log(`🔵 (${diff.change.toLocaleString()}B) ${change.path}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
`${
|
`${
|
||||||
diff.status === DiffStatus.New ? '🟢' : '🟡'
|
diff.status === DiffStatus.New ? '🟢' : '🟡'
|
||||||
} (+${diff.change.toLocaleString()}B) ${change.path}`,
|
} (${diff.change > 0 ? '+' : ''}${diff.change.toLocaleString()}B) ${change.path}`,
|
||||||
);
|
);
|
||||||
changes.push(change);
|
changes.push(change);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
import partytown from '@astrojs/partytown';
|
|
||||||
import react from '@astrojs/react';
|
import react from '@astrojs/react';
|
||||||
import sitemap from '@astrojs/sitemap';
|
import sitemap from '@astrojs/sitemap';
|
||||||
import { SUPPORTED_LOCALES } from '@blitzkit/i18n';
|
import { SUPPORTED_LOCALES } from '@blitzkit/i18n';
|
||||||
@@ -21,7 +20,7 @@ export default defineConfig({
|
|||||||
outDir: '../../dist/website',
|
outDir: '../../dist/website',
|
||||||
prefetch: true,
|
prefetch: true,
|
||||||
|
|
||||||
integrations: [react(), sitemap(), partytown()],
|
integrations: [react(), sitemap()],
|
||||||
vite: {
|
vite: {
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/partytown": "^2.1.4",
|
|
||||||
"@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:*",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -100,8 +100,7 @@ export function ArmorPlateDisplay() {
|
|||||||
const { protagonist } = duelStore.getState();
|
const { protagonist } = duelStore.getState();
|
||||||
const tank = draft.model;
|
const tank = draft.model;
|
||||||
const turret = tank.turrets[protagonist.turret.id];
|
const turret = tank.turrets[protagonist.turret.id];
|
||||||
const gun =
|
const gun = turret.guns[protagonist.gun.id];
|
||||||
turret.guns[protagonist.gun.gun_type!.value.base.id];
|
|
||||||
const track = tank.tracks[protagonist.track.id];
|
const track = tank.tracks[protagonist.track.id];
|
||||||
|
|
||||||
if (highlightArmor.name.startsWith('hull_')) {
|
if (highlightArmor.name.startsWith('hull_')) {
|
||||||
@@ -154,12 +153,8 @@ export function ArmorPlateDisplay() {
|
|||||||
const turret = tank.turrets[protagonist.turret.id];
|
const turret = tank.turrets[protagonist.turret.id];
|
||||||
const initialTurret =
|
const initialTurret =
|
||||||
initialTank.turrets[protagonist.turret.id];
|
initialTank.turrets[protagonist.turret.id];
|
||||||
const gun =
|
const gun = turret.guns[protagonist.gun.id];
|
||||||
turret.guns[protagonist.gun.gun_type!.value.base.id];
|
const initialGun = initialTurret.guns[protagonist.gun.id];
|
||||||
const initialGun =
|
|
||||||
initialTurret.guns[
|
|
||||||
protagonist.gun.gun_type!.value.base.id
|
|
||||||
];
|
|
||||||
const track = tank.tracks[protagonist.track.id];
|
const track = tank.tracks[protagonist.track.id];
|
||||||
const initialTrack =
|
const initialTrack =
|
||||||
initialTank.tracks[protagonist.track.id];
|
initialTank.tracks[protagonist.track.id];
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ export const PrimaryArmorScene = memo(() => {
|
|||||||
const tankModelDefinition = useTankModelDefinition();
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
turretModelDefinition.guns[gun.gun_type!.value.base.id];
|
|
||||||
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
||||||
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
||||||
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import { invalidate, useFrame } from '@react-three/fiber';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
|
NormalBlending,
|
||||||
Object3D,
|
Object3D,
|
||||||
ShaderMaterial,
|
ShaderMaterial,
|
||||||
|
SubtractiveBlending,
|
||||||
UniformsLib,
|
UniformsLib,
|
||||||
UniformsUtils,
|
UniformsUtils,
|
||||||
Vector2,
|
Vector2,
|
||||||
@@ -32,6 +34,9 @@ const excludeMaterial = new MeshBasicMaterial({
|
|||||||
colorWrite: false,
|
colorWrite: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
const isHalloween = date.getMonth() === 9 && date.getDate() === 31;
|
||||||
|
|
||||||
export function PrimaryArmorSceneComponent({
|
export function PrimaryArmorSceneComponent({
|
||||||
node,
|
node,
|
||||||
thickness,
|
thickness,
|
||||||
@@ -46,6 +51,8 @@ export function PrimaryArmorSceneComponent({
|
|||||||
fog: true,
|
fog: true,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
|
|
||||||
|
blending: isHalloween ? SubtractiveBlending : NormalBlending,
|
||||||
|
|
||||||
uniforms: UniformsUtils.merge([
|
uniforms: UniformsUtils.merge([
|
||||||
UniformsLib.common,
|
UniformsLib.common,
|
||||||
UniformsLib.fog,
|
UniformsLib.fog,
|
||||||
|
|||||||
+8
-7
@@ -1,3 +1,5 @@
|
|||||||
|
precision mediump float;
|
||||||
|
|
||||||
#define USE_FOG
|
#define USE_FOG
|
||||||
|
|
||||||
#include <fog_pars_fragment>
|
#include <fog_pars_fragment>
|
||||||
@@ -23,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;
|
||||||
@@ -36,7 +37,7 @@ float getDistance(vec2 coord, float depth) {
|
|||||||
|
|
||||||
vec3 getPenetrationColor(bool isThreeCalibersRule, bool couldHaveRicochet) {
|
vec3 getPenetrationColor(bool isThreeCalibersRule, bool couldHaveRicochet) {
|
||||||
if (advancedHighlighting && couldHaveRicochet) {
|
if (advancedHighlighting && couldHaveRicochet) {
|
||||||
return vec3(0.0, 1.0, isThreeCalibersRule ? 1.0 : 0.0);
|
return vec3(0.0, 1.0, isThreeCalibersRule ? 0.5 : 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return vec3(0.0, 1.0, 0.0);
|
return vec3(0.0, 1.0, 0.0);
|
||||||
@@ -79,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,17 +97,17 @@ void main() {
|
|||||||
penetrationChance = clamp(1.0 - (delta + randomization) / (2.0 * randomization), 0.0, 1.0);
|
penetrationChance = clamp(1.0 - (delta + randomization) / (2.0 * randomization), 0.0, 1.0);
|
||||||
|
|
||||||
if (canSplash) {
|
if (canSplash) {
|
||||||
float splashDamage = HALF * damage - EXPLOSION_DAMAGE_FACTOR * finalThickness;
|
float splashDamage = 0.5 * damage - EXPLOSION_DAMAGE_FACTOR * finalThickness;
|
||||||
splashChance = step(0.0, splashDamage);
|
splashChance = step(0.0, splashDamage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float alpha = opaque ? 1.0 : HALF;
|
float alpha = opaque ? 1.0 : 0.5;
|
||||||
vec3 baseColor = vec3(1.0, splashChance * GREEN_VALUE, 0.0);
|
vec3 baseColor = vec3(1.0, splashChance * GREEN_VALUE, 0.0);
|
||||||
|
|
||||||
if (advancedHighlighting && didRicochet) {
|
if (advancedHighlighting && didRicochet) {
|
||||||
baseColor = vec3(1.0, baseColor.g, 1.0);
|
baseColor = vec3(1.0, baseColor.g, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (greenPenetration || advancedHighlighting) {
|
if (greenPenetration || advancedHighlighting) {
|
||||||
|
|||||||
+2
@@ -1,3 +1,5 @@
|
|||||||
|
precision mediump float;
|
||||||
|
|
||||||
#define USE_FOG
|
#define USE_FOG
|
||||||
|
|
||||||
#include <fog_pars_vertex>
|
#include <fog_pars_vertex>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
+2
@@ -1,3 +1,5 @@
|
|||||||
|
precision mediump float;
|
||||||
|
|
||||||
uniform float thickness;
|
uniform float thickness;
|
||||||
uniform float penetration;
|
uniform float penetration;
|
||||||
|
|
||||||
|
|||||||
+2
@@ -1,3 +1,5 @@
|
|||||||
|
precision mediump float;
|
||||||
|
|
||||||
#include <clipping_planes_pars_vertex>
|
#include <clipping_planes_pars_vertex>
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|||||||
+2
@@ -1,3 +1,5 @@
|
|||||||
|
precision mediump float;
|
||||||
|
|
||||||
varying vec3 vNormal;
|
varying vec3 vNormal;
|
||||||
varying vec3 vViewPosition;
|
varying vec3 vViewPosition;
|
||||||
|
|
||||||
|
|||||||
+2
@@ -1,3 +1,5 @@
|
|||||||
|
precision mediump float;
|
||||||
|
|
||||||
varying vec3 vNormal;
|
varying vec3 vNormal;
|
||||||
varying vec3 vViewPosition;
|
varying vec3 vViewPosition;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { normalizeAngleRad } from '@blitzkit/core';
|
|
||||||
import { useThree, type ThreeEvent } from '@react-three/fiber';
|
import { useThree, type ThreeEvent } from '@react-three/fiber';
|
||||||
import { memo, useRef } from 'react';
|
import { memo, useRef } from 'react';
|
||||||
import { Group, Plane, Vector2, Vector3 } from 'three';
|
import { Group, Plane, Vector2, Vector3 } from 'three';
|
||||||
@@ -43,8 +42,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
const tankModelDefinition = useTankModelDefinition();
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
turretModelDefinition.guns[gun.gun_type!.value.base.id];
|
|
||||||
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
||||||
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
||||||
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
||||||
@@ -153,15 +151,11 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
|
|
||||||
const position = new Vector2();
|
const position = new Vector2();
|
||||||
const delta = new Vector2();
|
const delta = new Vector2();
|
||||||
let pitch = 0;
|
|
||||||
let yaw = 0;
|
|
||||||
|
|
||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
yaw = protagonist.yaw;
|
|
||||||
pitch = protagonist.pitch;
|
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
mutateTankopediaEphemeral((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
@@ -190,21 +184,18 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
delta.set(event.clientX, event.clientY).sub(position);
|
delta.set(event.clientX, event.clientY).sub(position);
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
|
|
||||||
[pitch, yaw] = applyPitchYawLimits(
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
pitch,
|
modelTransformEvent.last!.pitch,
|
||||||
yaw + delta.x * (Math.PI / boundingRect.width),
|
modelTransformEvent.last!.yaw +
|
||||||
|
delta.x * (Math.PI / boundingRect.width),
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateDuel((draft) => {
|
|
||||||
draft.protagonist.pitch = normalizeAngleRad(pitch);
|
|
||||||
draft.protagonist.yaw = normalizeAngleRad(yaw);
|
|
||||||
});
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
mutateTankopediaEphemeral((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
@@ -253,8 +244,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
|
|
||||||
const position = new Vector2();
|
const position = new Vector2();
|
||||||
const delta = new Vector2();
|
const delta = new Vector2();
|
||||||
let pitch = 0;
|
|
||||||
let yaw = 0;
|
|
||||||
|
|
||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -268,8 +257,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
pitch = protagonist.pitch;
|
|
||||||
yaw = protagonist.yaw;
|
|
||||||
|
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener('pointermove', handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener('pointerup', handlePointerUp);
|
||||||
@@ -290,24 +277,22 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
delta.set(event.clientX, event.clientY).sub(position);
|
delta.set(event.clientX, event.clientY).sub(position);
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
|
|
||||||
[pitch, yaw] = applyPitchYawLimits(
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
pitch - delta.y * (Math.PI / boundingRect.height),
|
modelTransformEvent.last!.pitch -
|
||||||
yaw + delta.x * (Math.PI / boundingRect.width),
|
delta.y * (Math.PI / boundingRect.height),
|
||||||
|
modelTransformEvent.last!.yaw +
|
||||||
|
delta.x * (Math.PI / boundingRect.width),
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
mutateTankopediaEphemeral((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
mutateDuel((draft) => {
|
|
||||||
draft.protagonist.pitch = normalizeAngleRad(pitch);
|
|
||||||
draft.protagonist.yaw = normalizeAngleRad(yaw);
|
|
||||||
});
|
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener('pointerup', handlePointerUp);
|
||||||
}
|
}
|
||||||
@@ -341,8 +326,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
|
|
||||||
const position = new Vector2();
|
const position = new Vector2();
|
||||||
const delta = new Vector2();
|
const delta = new Vector2();
|
||||||
let pitch = 0;
|
|
||||||
let yaw = 0;
|
|
||||||
|
|
||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -356,9 +339,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
pitch = protagonist.pitch;
|
|
||||||
yaw = protagonist.yaw;
|
|
||||||
|
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener('pointermove', handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener('pointerup', handlePointerUp);
|
||||||
}
|
}
|
||||||
@@ -378,24 +358,23 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
delta.set(event.clientX, event.clientY).sub(position);
|
delta.set(event.clientX, event.clientY).sub(position);
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
|
|
||||||
[pitch, yaw] = applyPitchYawLimits(
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
pitch - delta.y * (Math.PI / boundingRect.height),
|
modelTransformEvent.last!.pitch -
|
||||||
yaw + delta.x * (Math.PI / boundingRect.width),
|
delta.y * (Math.PI / boundingRect.height),
|
||||||
|
modelTransformEvent.last!.yaw +
|
||||||
|
delta.x * (Math.PI / boundingRect.width),
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
mutateTankopediaEphemeral((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
mutateDuel((draft) => {
|
|
||||||
draft.protagonist.pitch = normalizeAngleRad(pitch);
|
|
||||||
draft.protagonist.yaw = normalizeAngleRad(yaw);
|
|
||||||
});
|
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener('pointerup', handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { I_HAT, J_HAT } from '@blitzkit/core';
|
import { I_HAT, J_HAT } from '@blitzkit/core';
|
||||||
import { type MeshProps, useThree } from '@react-three/fiber';
|
import { type MeshProps, useThree } from '@react-three/fiber';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
|
import type { QuicklimeEvent } from 'quicklime';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Box3,
|
Box3,
|
||||||
@@ -186,16 +187,18 @@ export function StaticArmorSceneComponent({
|
|||||||
const distanceToBarrel = clip.distanceToPoint(barrelOrigin);
|
const distanceToBarrel = clip.distanceToPoint(barrelOrigin);
|
||||||
const point = new Vector3();
|
const point = new Vector3();
|
||||||
|
|
||||||
function handleModelTransform(data: ModelTransformEventData) {
|
function handleModelTransform(
|
||||||
|
event: QuicklimeEvent<ModelTransformEventData>,
|
||||||
|
) {
|
||||||
clip.normal
|
clip.normal
|
||||||
.set(0, 0, -1)
|
.set(0, 0, -1)
|
||||||
.applyAxisAngle(I_HAT, data.pitch)
|
.applyAxisAngle(I_HAT, event.data.pitch)
|
||||||
.applyAxisAngle(J_HAT, data.yaw ?? 0);
|
.applyAxisAngle(J_HAT, event.data.yaw);
|
||||||
clip.constant = 0;
|
clip.constant = 0;
|
||||||
|
|
||||||
point
|
point
|
||||||
.copy(gunOrigin)
|
.copy(gunOrigin)
|
||||||
.applyAxisAngle(J_HAT, data.yaw ?? 0)
|
.applyAxisAngle(J_HAT, event.data.yaw)
|
||||||
.add(clip.normal.multiplyScalar(-distanceToBarrel))
|
.add(clip.normal.multiplyScalar(-distanceToBarrel))
|
||||||
.add(neckOrigin);
|
.add(neckOrigin);
|
||||||
clip.normal.normalize();
|
clip.normal.normalize();
|
||||||
|
|||||||
@@ -33,43 +33,35 @@ export function Body({ stats }: BodyProps) {
|
|||||||
<StickyColumnHeaderCell key={key} top={137}>
|
<StickyColumnHeaderCell key={key} top={137}>
|
||||||
<Flex justify="center">
|
<Flex justify="center">
|
||||||
<Flex>
|
<Flex>
|
||||||
{gun.gun_type!.value.base.shells.map(
|
{gun.shells.map((thisShell, shellIndex) => (
|
||||||
(thisShell, shellIndex) => (
|
<IconButton
|
||||||
<IconButton
|
color={thisShell.id === shell.id ? undefined : 'gray'}
|
||||||
color={thisShell.id === shell.id ? undefined : 'gray'}
|
variant="soft"
|
||||||
variant="soft"
|
key={thisShell.id}
|
||||||
key={thisShell.id}
|
style={{
|
||||||
style={{
|
borderTopLeftRadius: shellIndex === 0 ? undefined : 0,
|
||||||
borderTopLeftRadius: shellIndex === 0 ? undefined : 0,
|
borderBottomLeftRadius:
|
||||||
borderBottomLeftRadius:
|
shellIndex === 0 ? undefined : 0,
|
||||||
shellIndex === 0 ? undefined : 0,
|
borderTopRightRadius:
|
||||||
borderTopRightRadius:
|
shellIndex === gun.shells.length - 1 ? undefined : 0,
|
||||||
shellIndex ===
|
borderBottomRightRadius:
|
||||||
gun.gun_type!.value.base.shells.length - 1
|
shellIndex === gun.shells.length - 1 ? undefined : 0,
|
||||||
? undefined
|
marginLeft: shellIndex === 0 ? 0 : -1,
|
||||||
: 0,
|
}}
|
||||||
borderBottomRightRadius:
|
onClick={() => {
|
||||||
shellIndex ===
|
mutateCompareEphemeral((draft) => {
|
||||||
gun.gun_type!.value.base.shells.length - 1
|
draft.members[index].shell = thisShell;
|
||||||
? undefined
|
});
|
||||||
: 0,
|
}}
|
||||||
marginLeft: shellIndex === 0 ? 0 : -1,
|
>
|
||||||
}}
|
<img
|
||||||
onClick={() => {
|
alt={unwrap(thisShell.name)}
|
||||||
mutateCompareEphemeral((draft) => {
|
width={16}
|
||||||
draft.members[index].shell = thisShell;
|
height={16}
|
||||||
});
|
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
||||||
}}
|
/>
|
||||||
>
|
</IconButton>
|
||||||
<img
|
))}
|
||||||
alt={unwrap(thisShell.name)}
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</StickyColumnHeaderCell>
|
</StickyColumnHeaderCell>
|
||||||
@@ -79,6 +71,12 @@ export function Body({ stats }: BodyProps) {
|
|||||||
|
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
<CompareRow stats={stats} value="dpm" decimals={0} />
|
<CompareRow stats={stats} value="dpm" decimals={0} />
|
||||||
|
<CompareRow stats={stats} value="damage" decimals={0} />
|
||||||
|
{hasNonRegularGun && (
|
||||||
|
<CompareRow stats={stats} indent value="clipDamage" decimals={0} />
|
||||||
|
)}
|
||||||
|
<CompareRow stats={stats} value="moduleDamage" decimals={0} />
|
||||||
|
<CompareRow stats={stats} value="penetration" decimals={0} />
|
||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
name={strings.website.tools.tankopedia.characteristics.values.reload}
|
name={strings.website.tools.tankopedia.characteristics.values.reload}
|
||||||
@@ -105,12 +103,6 @@ export function Body({ stats }: BodyProps) {
|
|||||||
<CompareRow stats={stats} value="shells" decimals={0} />
|
<CompareRow stats={stats} value="shells" decimals={0} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<CompareRow stats={stats} value="penetration" decimals={0} />
|
|
||||||
<CompareRow stats={stats} value="damage" decimals={0} />
|
|
||||||
{hasNonRegularGun && (
|
|
||||||
<CompareRow stats={stats} indent value="clipDamage" decimals={0} />
|
|
||||||
)}
|
|
||||||
<CompareRow stats={stats} value="moduleDamage" decimals={0} />
|
|
||||||
<CompareRow stats={stats} value="caliber" decimals={0} />
|
<CompareRow stats={stats} value="caliber" decimals={0} />
|
||||||
<CompareRow stats={stats} value="shellNormalization" decimals={0} />
|
<CompareRow stats={stats} value="shellNormalization" decimals={0} />
|
||||||
<CompareRow
|
<CompareRow
|
||||||
|
|||||||
@@ -250,8 +250,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
|
|
||||||
member.turret = member.tank.turrets[0];
|
member.turret = member.tank.turrets[0];
|
||||||
member.gun = member.turret.guns[0];
|
member.gun = member.turret.guns[0];
|
||||||
member.shell =
|
member.shell = member.gun.shells[0];
|
||||||
member.gun.gun_type!.value.base.shells[0];
|
|
||||||
member.engine = member.tank.engines[0];
|
member.engine = member.tank.engines[0];
|
||||||
member.track = member.tank.tracks[0];
|
member.track = member.tank.tracks[0];
|
||||||
});
|
});
|
||||||
@@ -267,8 +266,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
|
|
||||||
member.turret = member.tank.turrets.at(-1)!;
|
member.turret = member.tank.turrets.at(-1)!;
|
||||||
member.gun = member.turret.guns.at(-1)!;
|
member.gun = member.turret.guns.at(-1)!;
|
||||||
member.shell =
|
member.shell = member.gun.shells[0];
|
||||||
member.gun.gun_type!.value.base.shells[0];
|
|
||||||
member.engine = member.tank.engines.at(-1)!;
|
member.engine = member.tank.engines.at(-1)!;
|
||||||
member.track = member.tank.tracks.at(-1)!;
|
member.track = member.tank.tracks.at(-1)!;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -127,8 +127,8 @@ const strings = getStrings(locale);
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex gap="5" display={{ initial: 'flex', sm: 'none' }}>
|
<Flex gap="5" display={{ initial: 'flex', sm: 'none' }} width="100%">
|
||||||
<Flex direction="column" justify="between" align="end">
|
<Flex direction="column" gap="1" flexGrow="1" flexBasis="0" align="end">
|
||||||
<Text size="2" color="gray">BlitzKit</Text>
|
<Text size="2" color="gray">BlitzKit</Text>
|
||||||
{
|
{
|
||||||
PUBLIC_ASSET_BRANCH !== 'preview' && (
|
PUBLIC_ASSET_BRANCH !== 'preview' && (
|
||||||
@@ -140,7 +140,7 @@ const strings = getStrings(locale);
|
|||||||
<Text size="2" color="gray">CDN</Text>
|
<Text size="2" color="gray">CDN</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex direction="column" justify="between" align="start">
|
<Flex direction="column" gap="1" flexGrow="1" flexBasis="0" align="start">
|
||||||
<Code size="2" color="gray">
|
<Code size="2" color="gray">
|
||||||
{packageJson.version}
|
{packageJson.version}
|
||||||
</Code>
|
</Code>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Canvas } from '@react-three/fiber';
|
|
||||||
import { GuessEphemeral } from '../stores/guessEphemeral';
|
import { GuessEphemeral } from '../stores/guessEphemeral';
|
||||||
import { TankopediaDisplay } from '../stores/tankopediaPersistent/constants';
|
import { TankopediaDisplay } from '../stores/tankopediaPersistent/constants';
|
||||||
|
import { SmartCanvas } from './SmartCanvas';
|
||||||
import { Controls } from './Tankopedia/HeroSection/components/TankSandbox/components/Control';
|
import { Controls } from './Tankopedia/HeroSection/components/TankSandbox/components/Control';
|
||||||
import { Lighting } from './Tankopedia/HeroSection/components/TankSandbox/components/Lighting';
|
import { Lighting } from './Tankopedia/HeroSection/components/TankSandbox/components/Lighting';
|
||||||
import { TankModel } from './Tankopedia/HeroSection/components/TankSandbox/components/TankModel';
|
import { TankModel } from './Tankopedia/HeroSection/components/TankSandbox/components/TankModel';
|
||||||
@@ -13,7 +13,7 @@ export function GuessRenderer() {
|
|||||||
const isRevealed = guessState !== null;
|
const isRevealed = guessState !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Canvas
|
<SmartCanvas
|
||||||
style={{
|
style={{
|
||||||
filter: `grayscale(${isRevealed ? 0 : 1}) contrast(${isRevealed ? 1 : CONTRAST}) brightness(${isRevealed ? 1 : BRIGHTNESS})`,
|
filter: `grayscale(${isRevealed ? 0 : 1}) contrast(${isRevealed ? 1 : CONTRAST}) brightness(${isRevealed ? 1 : BRIGHTNESS})`,
|
||||||
// filter: `drop-shadow(0 0 1rem ${Var('black-a11')})`,
|
// filter: `drop-shadow(0 0 1rem ${Var('black-a11')})`,
|
||||||
@@ -25,6 +25,6 @@ export function GuessRenderer() {
|
|||||||
<Controls distanceScale={2} zoomable={false} />
|
<Controls distanceScale={2} zoomable={false} />
|
||||||
<Lighting display={TankopediaDisplay.Model} />
|
<Lighting display={TankopediaDisplay.Model} />
|
||||||
<TankModel />
|
<TankModel />
|
||||||
</Canvas>
|
</SmartCanvas>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
import { Link, Table } from '@radix-ui/themes';
|
import { Link, Table } from '@radix-ui/themes';
|
||||||
import MarkdownToJSX from 'markdown-to-jsx';
|
import MarkdownToJSX from 'markdown-to-jsx';
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
interface MarkdownProps {
|
interface MarkdownProps {
|
||||||
children: string;
|
children: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Image(props: ComponentProps<'img'>) {
|
||||||
|
return <img style={{ maxWidth: '100%' }} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
export function Markdown({ children }: MarkdownProps) {
|
export function Markdown({ children }: MarkdownProps) {
|
||||||
return (
|
return (
|
||||||
<MarkdownToJSX
|
<MarkdownToJSX
|
||||||
@@ -17,6 +22,7 @@ export function Markdown({ children }: MarkdownProps) {
|
|||||||
tr: { component: Table.Row },
|
tr: { component: Table.Row },
|
||||||
th: { component: Table.ColumnHeaderCell },
|
th: { component: Table.ColumnHeaderCell },
|
||||||
td: { component: Table.Cell },
|
td: { component: Table.Cell },
|
||||||
|
img: { component: Image },
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function ModuleManager({
|
|||||||
const draft = { ...modules };
|
const draft = { ...modules };
|
||||||
draft.turret = turret;
|
draft.turret = turret;
|
||||||
draft.gun = draft.turret.guns.at(-1)!;
|
draft.gun = draft.turret.guns.at(-1)!;
|
||||||
draft.shell = draft.gun.gun_type!.value.base.shells[0];
|
draft.shell = draft.gun.shells[0];
|
||||||
|
|
||||||
onChange?.(draft);
|
onChange?.(draft);
|
||||||
setTurretMenuOpen(false);
|
setTurretMenuOpen(false);
|
||||||
@@ -83,9 +83,7 @@ export function ModuleManager({
|
|||||||
|
|
||||||
<ModuleButton
|
<ModuleButton
|
||||||
module="gun"
|
module="gun"
|
||||||
discriminator={
|
discriminator={TIER_ROMAN_NUMERALS[modules.gun.tier]}
|
||||||
TIER_ROMAN_NUMERALS[modules.gun.gun_type!.value.base.tier]
|
|
||||||
}
|
|
||||||
onClick={() => setGunMenuOpen(true)}
|
onClick={() => setGunMenuOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -95,18 +93,16 @@ export function ModuleManager({
|
|||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
<DropdownMenu.RadioGroup
|
<DropdownMenu.RadioGroup value={`${modules.gun.id}`}>
|
||||||
value={`${modules.gun.gun_type!.value.base.id}`}
|
|
||||||
>
|
|
||||||
{[...modules.turret.guns].reverse().map((gun) => {
|
{[...modules.turret.guns].reverse().map((gun) => {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.RadioItem
|
<DropdownMenu.RadioItem
|
||||||
key={gun.gun_type!.value.base.id}
|
key={gun.id}
|
||||||
value={`${gun.gun_type!.value.base.id}`}
|
value={`${gun.id}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const draft = { ...modules };
|
const draft = { ...modules };
|
||||||
draft.gun = gun;
|
draft.gun = gun;
|
||||||
draft.shell = draft.gun.gun_type!.value.base.shells[0];
|
draft.shell = draft.gun.shells[0];
|
||||||
|
|
||||||
onChange?.(draft);
|
onChange?.(draft);
|
||||||
setGunMenuOpen(false);
|
setGunMenuOpen(false);
|
||||||
@@ -115,11 +111,9 @@ export function ModuleManager({
|
|||||||
<ModuleButton
|
<ModuleButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
module="gun"
|
module="gun"
|
||||||
discriminator={
|
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
|
||||||
TIER_ROMAN_NUMERALS[gun.gun_type!.value.base.tier]
|
|
||||||
}
|
|
||||||
/>{' '}
|
/>{' '}
|
||||||
{unwrap(gun.gun_type!.value.base.name)}
|
{unwrap(gun.name)}
|
||||||
</DropdownMenu.RadioItem>
|
</DropdownMenu.RadioItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export interface Props {
|
|||||||
integrated?: boolean;
|
integrated?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { integrated } = Astro.props;
|
const { integrated = false } = Astro.props;
|
||||||
const toolsFiltered = TOOLS.filter((tool) => !('href' in tool));
|
const toolsFiltered = TOOLS.filter((tool) => !('href' in tool));
|
||||||
const { locale } = Astro.params;
|
const { locale } = Astro.params;
|
||||||
const branchName = resolveBranchName(locale);
|
const branchName = resolveBranchName(locale);
|
||||||
@@ -47,7 +47,7 @@ const strings = getStrings(locale);
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar[data-expanded='false'] .content {
|
.navbar[data-expanded='false'][data-integrated='false'] .content {
|
||||||
background-color: var(--black-a8);
|
background-color: var(--black-a8);
|
||||||
backdrop-filter: blur(4rem);
|
backdrop-filter: blur(4rem);
|
||||||
box-shadow: var(--shadow-3);
|
box-shadow: var(--shadow-3);
|
||||||
@@ -87,6 +87,7 @@ const strings = getStrings(locale);
|
|||||||
direction="column"
|
direction="column"
|
||||||
align="center"
|
align="center"
|
||||||
data-expanded="false"
|
data-expanded="false"
|
||||||
|
data-integrated={`${integrated}`}
|
||||||
className="navbar"
|
className="navbar"
|
||||||
width="100%"
|
width="100%"
|
||||||
top="0"
|
top="0"
|
||||||
@@ -232,7 +233,7 @@ const strings = getStrings(locale);
|
|||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
borderRadius: Var('radius-2'),
|
borderRadius: Var('radius-2'),
|
||||||
backgroundImage: `url(/assets/images/${tool.id}.webp)`,
|
backgroundImage: `url(/assets/images/tools/${tool.id}.webp)`,
|
||||||
backgroundSize: '200%',
|
backgroundSize: '200%',
|
||||||
backgroundPosition: 'right center',
|
backgroundPosition: 'right center',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -6,42 +6,20 @@ const PUBLIC_GOOGLE_TAG_MANAGER_ID = assertSecret(
|
|||||||
);
|
);
|
||||||
---
|
---
|
||||||
|
|
||||||
{/* google analytics */}
|
|
||||||
<script
|
<script
|
||||||
is:inline
|
is:inline
|
||||||
async
|
async
|
||||||
type="text/partytown"
|
|
||||||
src={`https://www.googletagmanager.com/gtag/js?id=${PUBLIC_GOOGLE_TAG_MANAGER_ID}`}
|
src={`https://www.googletagmanager.com/gtag/js?id=${PUBLIC_GOOGLE_TAG_MANAGER_ID}`}
|
||||||
></script>
|
></script>
|
||||||
<script
|
|
||||||
is:inline
|
<script is:inline define:vars={{ PUBLIC_GOOGLE_TAG_MANAGER_ID }}>
|
||||||
define:vars={{ PUBLIC_GOOGLE_TAG_MANAGER_ID }}
|
|
||||||
type="text/partytown"
|
|
||||||
>
|
|
||||||
window.dataLayer = window.dataLayer || [];
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
|
||||||
function gtag() {
|
function gtag() {
|
||||||
dataLayer.push(arguments);
|
dataLayer.push(arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
gtag('js', new Date());
|
gtag('js', new Date());
|
||||||
|
|
||||||
gtag('config', PUBLIC_GOOGLE_TAG_MANAGER_ID);
|
gtag('config', PUBLIC_GOOGLE_TAG_MANAGER_ID);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{/* nitro ads */}
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
<script is:inline data-cfasync="false">
|
|
||||||
window.nitroAds = window.nitroAds || {
|
|
||||||
createAd: function () {
|
|
||||||
return new Promise((e) => {
|
|
||||||
window.nitroAds.queue.push(['createAd', arguments, e]);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
addUserToken: function () {
|
|
||||||
window.nitroAds.queue.push(['addUserToken', arguments]);
|
|
||||||
},
|
|
||||||
queue: [],
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -80,7 +80,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt={unwrap(tank.name)}
|
alt={unwrap(tank.name)}
|
||||||
src={asset(`icons/tanks/blitzkit/${tank.id}.webp`)}
|
src={asset(`icons/tanks/big/${tank.id}.webp`)}
|
||||||
className="image"
|
className="image"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -25,18 +25,18 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
|||||||
|
|
||||||
const turret = tank.turrets.at(-1)!;
|
const turret = tank.turrets.at(-1)!;
|
||||||
const gun = turret.guns.at(-1)!;
|
const gun = turret.guns.at(-1)!;
|
||||||
const shell0 = gun.gun_type!.value.base.shells[0];
|
const shell0 = gun.shells[0];
|
||||||
const shell1 = gun.gun_type!.value.base.shells[1];
|
const shell1 = gun.shells[1];
|
||||||
|
const shell2 = gun.shells[2];
|
||||||
const tracks = tank.tracks.at(-1)!;
|
const tracks = tank.tracks.at(-1)!;
|
||||||
const engine = tank.engines.at(-1)!;
|
const engine = tank.engines.at(-1)!;
|
||||||
const tankModelDefinition = modelDefinitions.models[tank.id];
|
const tankModelDefinition = modelDefinitions.models[tank.id];
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
turretModelDefinition.guns[gun.gun_type!.value.base.id];
|
|
||||||
|
|
||||||
switch (tankopediaSort.by) {
|
switch (tankopediaSort.by) {
|
||||||
case 'fire.aimTime':
|
case 'fire.aimTime':
|
||||||
return gun.gun_type!.value.base.aim_time.toFixed(2);
|
return gun.aim_time.toFixed(2);
|
||||||
case 'fire.caliber':
|
case 'fire.caliber':
|
||||||
return shell0.caliber.toFixed(0);
|
return shell0.caliber.toFixed(0);
|
||||||
case 'fire.damage':
|
case 'fire.damage':
|
||||||
@@ -46,7 +46,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
|||||||
case 'fire.dispersionMoving':
|
case 'fire.dispersionMoving':
|
||||||
return tracks.dispersion_move.toFixed(3);
|
return tracks.dispersion_move.toFixed(3);
|
||||||
case 'fire.dispersionStill':
|
case 'fire.dispersionStill':
|
||||||
return gun.gun_type!.value.base.dispersion_base.toFixed(3);
|
return gun.dispersion_base.toFixed(3);
|
||||||
case 'fire.dpm':
|
case 'fire.dpm':
|
||||||
return resolveDpm(gun, shell0).toFixed(0);
|
return resolveDpm(gun, shell0).toFixed(0);
|
||||||
case 'fire.dpmPremium':
|
case 'fire.dpmPremium':
|
||||||
@@ -57,10 +57,12 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
|||||||
return shell0.penetration.near.toFixed(0);
|
return shell0.penetration.near.toFixed(0);
|
||||||
case 'fire.premiumPenetration':
|
case 'fire.premiumPenetration':
|
||||||
return shell1 ? shell1.penetration.near.toFixed(0) : '--';
|
return shell1 ? shell1.penetration.near.toFixed(0) : '--';
|
||||||
|
case 'fire.tertiaryPenetration':
|
||||||
|
return shell1 ? shell2.penetration.near.toFixed(0) : '--';
|
||||||
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':
|
||||||
@@ -86,7 +88,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
|||||||
engine.weight +
|
engine.weight +
|
||||||
tracks.weight +
|
tracks.weight +
|
||||||
turret.weight +
|
turret.weight +
|
||||||
gun.gun_type!.value.base.weight)
|
gun.weight)
|
||||||
).toFixed(1);
|
).toFixed(1);
|
||||||
case 'maneuverability.weight':
|
case 'maneuverability.weight':
|
||||||
return (
|
return (
|
||||||
@@ -94,7 +96,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
|||||||
engine.weight +
|
engine.weight +
|
||||||
tracks.weight +
|
tracks.weight +
|
||||||
turret.weight +
|
turret.weight +
|
||||||
gun.gun_type!.value.base.weight) /
|
gun.weight) /
|
||||||
1000
|
1000
|
||||||
).toFixed(1);
|
).toFixed(1);
|
||||||
case 'maneuverability.traverseSpeed':
|
case 'maneuverability.traverseSpeed':
|
||||||
@@ -108,11 +110,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
|||||||
case 'survivability.camouflageMoving':
|
case 'survivability.camouflageMoving':
|
||||||
return (tank.camouflage_moving * 100).toFixed(0);
|
return (tank.camouflage_moving * 100).toFixed(0);
|
||||||
case 'survivability.camouflageShooting':
|
case 'survivability.camouflageShooting':
|
||||||
return (
|
return (tank.camouflage_still * gun.camouflage_loss * 100).toFixed(0);
|
||||||
tank.camouflage_still *
|
|
||||||
gun.gun_type!.value.base.camouflage_loss *
|
|
||||||
100
|
|
||||||
).toFixed(0);
|
|
||||||
case 'survivability.volume': {
|
case 'survivability.volume': {
|
||||||
const dimensions = normalizeBoundingBox(
|
const dimensions = normalizeBoundingBox(
|
||||||
unionBoundingBox(
|
unionBoundingBox(
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
key={shellType}
|
||||||
value={`${shellType}`}
|
value={`${shellType}`}
|
||||||
radius="none"
|
radius="none"
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? 'solid' : 'soft'}
|
||||||
|
|||||||
@@ -21,27 +21,17 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
|||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const performSearch = useCallback(
|
const performSearch = useCallback(
|
||||||
debounce(() => {
|
debounce(() => {
|
||||||
// tankFiltersStore.setState({ searching: false });
|
|
||||||
$tankFilters.setKey('searching', false);
|
$tankFilters.setKey('searching', false);
|
||||||
|
|
||||||
if (!input.current) return;
|
if (!input.current) return;
|
||||||
|
|
||||||
const sanitized = input.current.value.trim();
|
const sanitized = input.current.value.trim();
|
||||||
|
|
||||||
// tankFiltersStore.setState({
|
$tankFilters.setKey('search', sanitized.length === 0 ? null : sanitized);
|
||||||
// search: sanitized.length === 0 ? undefined : sanitized,
|
|
||||||
// });
|
|
||||||
$tankFilters.setKey(
|
|
||||||
'search',
|
|
||||||
sanitized.length === 0 ? undefined : sanitized,
|
|
||||||
);
|
|
||||||
}, 500),
|
}, 500),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const handleChange = useCallback(() => {
|
const handleChange = useCallback(() => {
|
||||||
// if (!tankFiltersStore.getState().searching) {
|
|
||||||
// tankFiltersStore.setState({ searching: true });
|
|
||||||
// }
|
|
||||||
if (!tankFilters.searching) {
|
if (!tankFilters.searching) {
|
||||||
$tankFilters.setKey('searching', true);
|
$tankFilters.setKey('searching', true);
|
||||||
}
|
}
|
||||||
@@ -69,7 +59,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
|||||||
<TextField.Root
|
<TextField.Root
|
||||||
variant="classic"
|
variant="classic"
|
||||||
disabled={skeleton}
|
disabled={skeleton}
|
||||||
defaultValue={tankFilters.search}
|
defaultValue={tankFilters.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}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export function Sort() {
|
|||||||
<Item by="fire.caliber" />
|
<Item by="fire.caliber" />
|
||||||
<Item by="fire.standardPenetration" />
|
<Item by="fire.standardPenetration" />
|
||||||
<Item by="fire.premiumPenetration" />
|
<Item by="fire.premiumPenetration" />
|
||||||
|
<Item by="fire.tertiaryPenetration" />
|
||||||
<Item by="fire.damage" />
|
<Item by="fire.damage" />
|
||||||
<Item by="fire.moduleDamage" />
|
<Item by="fire.moduleDamage" />
|
||||||
<Item by="fire.shellVelocity" />
|
<Item by="fire.shellVelocity" />
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
const tankFilters = useStore($tankFilters);
|
const tankFilters = useStore($tankFilters);
|
||||||
const tankopediaSort = useStore($tankopediaSort);
|
const tankopediaSort = useStore($tankopediaSort);
|
||||||
const tanksFiltered = useMemo(() => {
|
const tanksFiltered = useMemo(() => {
|
||||||
if (tankFilters.search === undefined) {
|
if (tankFilters.search === null) {
|
||||||
const filtered = awaitedTanksDefinitionsArray.filter((tank) =>
|
const filtered = awaitedTanksDefinitionsArray.filter((tank) =>
|
||||||
filterTank(tankFilters, tank),
|
filterTank(tankFilters, tank),
|
||||||
);
|
);
|
||||||
@@ -104,11 +104,9 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.camouflage_still *
|
a.camouflage_still *
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base
|
a.turrets.at(-1)!.guns.at(-1)!.camouflage_loss -
|
||||||
.camouflage_loss -
|
|
||||||
b.camouflage_still *
|
b.camouflage_still *
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base
|
b.turrets.at(-1)!.guns.at(-1)!.camouflage_loss,
|
||||||
.camouflage_loss,
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -171,11 +169,11 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
(a, b) =>
|
(a, b) =>
|
||||||
resolveDpm(
|
resolveDpm(
|
||||||
a.turrets.at(-1)!.guns.at(-1)!,
|
a.turrets.at(-1)!.guns.at(-1)!,
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0],
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0],
|
||||||
) -
|
) -
|
||||||
resolveDpm(
|
resolveDpm(
|
||||||
b.turrets.at(-1)!.guns.at(-1)!,
|
b.turrets.at(-1)!.guns.at(-1)!,
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0],
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -183,18 +181,16 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
case 'fire.dpmPremium':
|
case 'fire.dpmPremium':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
(a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[1]
|
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
||||||
? resolveDpm(
|
? resolveDpm(
|
||||||
a.turrets.at(-1)!.guns.at(-1)!,
|
a.turrets.at(-1)!.guns.at(-1)!,
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base
|
a.turrets.at(-1)!.guns.at(-1)!.shells[1],
|
||||||
.shells[1],
|
|
||||||
)
|
)
|
||||||
: 0) -
|
: 0) -
|
||||||
(b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[1]
|
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
||||||
? resolveDpm(
|
? resolveDpm(
|
||||||
b.turrets.at(-1)!.guns.at(-1)!,
|
b.turrets.at(-1)!.guns.at(-1)!,
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base
|
b.turrets.at(-1)!.guns.at(-1)!.shells[1],
|
||||||
.shells[1],
|
|
||||||
)
|
)
|
||||||
: 0),
|
: 0),
|
||||||
);
|
);
|
||||||
@@ -211,98 +207,92 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
case 'fire.caliber':
|
case 'fire.caliber':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber -
|
||||||
.caliber -
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber,
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
|
||||||
.caliber,
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.standardPenetration':
|
case 'fire.standardPenetration':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near -
|
||||||
.penetration.near -
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near,
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[0]
|
|
||||||
.penetration.near,
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.premiumPenetration':
|
case 'fire.premiumPenetration':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
(a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[1]
|
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration.near ??
|
||||||
?.penetration.near ?? 0) -
|
0) -
|
||||||
(b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.shells[1]
|
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration.near ??
|
||||||
?.penetration.near ?? 0),
|
0),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'fire.tertiaryPenetration':
|
||||||
|
sorted = filtered.sort(
|
||||||
|
(a, b) =>
|
||||||
|
(a.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration.near ??
|
||||||
|
0) -
|
||||||
|
(b.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration.near ??
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -318,13 +308,11 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
|
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
|
||||||
.guns[a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.id]
|
.guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch.max +
|
||||||
.pitch.max +
|
|
||||||
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
|
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
|
||||||
0) -
|
0) -
|
||||||
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
|
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
|
||||||
.guns[b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.id]
|
.guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch.max -
|
||||||
.pitch.max -
|
|
||||||
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
|
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
|
||||||
0),
|
0),
|
||||||
);
|
);
|
||||||
@@ -334,13 +322,11 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
|
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
|
||||||
.guns[b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.id]
|
.guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch.min +
|
||||||
.pitch.min +
|
|
||||||
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
|
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
|
||||||
0) -
|
0) -
|
||||||
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
|
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
|
||||||
.guns[a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.id]
|
.guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch.min -
|
||||||
.pitch.min -
|
|
||||||
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
|
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
|
||||||
0),
|
0),
|
||||||
);
|
);
|
||||||
@@ -372,14 +358,13 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
a.engines.at(-1)!.weight +
|
a.engines.at(-1)!.weight +
|
||||||
a.tracks.at(-1)!.weight +
|
a.tracks.at(-1)!.weight +
|
||||||
a.turrets.at(-1)!.weight +
|
a.turrets.at(-1)!.weight +
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base
|
a.turrets.at(-1)!.guns.at(-1)!.weight) -
|
||||||
.weight) -
|
|
||||||
b.engines.at(-1)!.power /
|
b.engines.at(-1)!.power /
|
||||||
(b.weight +
|
(b.weight +
|
||||||
b.engines.at(-1)!.weight +
|
b.engines.at(-1)!.weight +
|
||||||
b.tracks.at(-1)!.weight +
|
b.tracks.at(-1)!.weight +
|
||||||
b.turrets.at(-1)!.weight +
|
b.turrets.at(-1)!.weight +
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.weight),
|
b.turrets.at(-1)!.guns.at(-1)!.weight),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -390,12 +375,12 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
a.engines.at(-1)!.weight +
|
a.engines.at(-1)!.weight +
|
||||||
a.tracks.at(-1)!.weight +
|
a.tracks.at(-1)!.weight +
|
||||||
a.turrets.at(-1)!.weight +
|
a.turrets.at(-1)!.weight +
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.weight -
|
a.turrets.at(-1)!.guns.at(-1)!.weight -
|
||||||
(b.weight +
|
(b.weight +
|
||||||
b.engines.at(-1)!.weight +
|
b.engines.at(-1)!.weight +
|
||||||
b.tracks.at(-1)!.weight +
|
b.tracks.at(-1)!.weight +
|
||||||
b.turrets.at(-1)!.weight +
|
b.turrets.at(-1)!.weight +
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.gun_type!.value.base.weight),
|
b.turrets.at(-1)!.guns.at(-1)!.weight),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#assault-cursor {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
#assault-cursor:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
import { DragHandleDots2Icon } from '@radix-ui/react-icons';
|
||||||
|
import { Box, Flex, Text } from '@radix-ui/themes';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { clamp } from 'three/src/math/MathUtils.js';
|
||||||
|
import {
|
||||||
|
fakeLogSquishyHigh,
|
||||||
|
fakeLogSquishyHighInverse,
|
||||||
|
} from '../../../../../../../../../core/blitzkit/fakeLog';
|
||||||
|
import { Var } from '../../../../../../../../../core/radix/var';
|
||||||
|
import { Duel } from '../../../../../../../../../stores/duel';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
maxDistance: number;
|
||||||
|
maxFakeDistance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AssaultCursor({ maxDistance, maxFakeDistance }: Props) {
|
||||||
|
const assaultDistance = Duel.use(
|
||||||
|
(state) => state.protagonist.assaultDistance,
|
||||||
|
);
|
||||||
|
const [x, setX] = useState(fakeLogSquishyHigh(assaultDistance / maxDistance));
|
||||||
|
const cursor = useRef<HTMLDivElement>(null);
|
||||||
|
const mutateDuel = Duel.useMutation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let localX = x;
|
||||||
|
let lastX = 0;
|
||||||
|
|
||||||
|
function handlePointerDown(event: PointerEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
lastX = event.clientX;
|
||||||
|
|
||||||
|
window.addEventListener('pointermove', handlePointerMove);
|
||||||
|
window.addEventListener('pointerup', handlePointerUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerMove(event: PointerEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const parent = cursor.current?.parentElement;
|
||||||
|
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
const width = parent.clientWidth;
|
||||||
|
const movementX = event.clientX - lastX;
|
||||||
|
lastX = event.clientX;
|
||||||
|
|
||||||
|
setX((state) => {
|
||||||
|
localX = clamp(state + movementX / width, 0, 1);
|
||||||
|
return localX;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerUp() {
|
||||||
|
mutateDuel((draft) => {
|
||||||
|
draft.protagonist.assaultDistance =
|
||||||
|
fakeLogSquishyHighInverse(localX * maxFakeDistance) * maxDistance;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
|
window.removeEventListener('pointerup', handlePointerUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.current?.addEventListener('pointerdown', handlePointerDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cursor.current?.removeEventListener('pointerdown', handlePointerDown);
|
||||||
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
|
window.removeEventListener('pointerup', handlePointerUp);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={cursor}
|
||||||
|
position="absolute"
|
||||||
|
top="0"
|
||||||
|
left={`${x * 100}%`}
|
||||||
|
height="100%"
|
||||||
|
width="2pt"
|
||||||
|
id="assault-cursor"
|
||||||
|
style={{
|
||||||
|
touchAction: 'none',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
backgroundColor: Var('accent-11'),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
direction="column"
|
||||||
|
position="absolute"
|
||||||
|
left="50%"
|
||||||
|
bottom="100%"
|
||||||
|
style={{ transform: 'translateX(-50%)' }}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
style={{
|
||||||
|
width: '1.5rem',
|
||||||
|
height: '1.5rem',
|
||||||
|
backgroundColor: Var('accent-9'),
|
||||||
|
borderRadius: `50% 50% ${Var('radius-1')} ${Var('radius-1')}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ display: 'flex' }} highContrast color="purple">
|
||||||
|
<DragHandleDots2Icon />
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
height="0.5rem"
|
||||||
|
width="2pt"
|
||||||
|
style={{
|
||||||
|
backgroundColor: Var('accent-11'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
+158
@@ -0,0 +1,158 @@
|
|||||||
|
import type { AssaultRanges } from '@blitzkit/core';
|
||||||
|
import { literals } from '@blitzkit/i18n';
|
||||||
|
import { Box, Flex, Text } from '@radix-ui/themes';
|
||||||
|
import { clamp } from 'three/src/math/MathUtils.js';
|
||||||
|
import {
|
||||||
|
fakeLogSquishyHigh,
|
||||||
|
fakeLogSquishyLow,
|
||||||
|
} from '../../../../../../../core/blitzkit/fakeLog';
|
||||||
|
import type { TankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
|
||||||
|
import { Var } from '../../../../../../../core/radix/var';
|
||||||
|
import { useLocale } from '../../../../../../../hooks/useLocale';
|
||||||
|
import { VisualizerCard } from '../VisualizerCard';
|
||||||
|
import { AssaultCursor } from './components/Cursor';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
ranges: AssaultRanges;
|
||||||
|
stats: TankCharacteristics;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AssaultRangesVisualizer({ ranges: _ranges, stats }: Props) {
|
||||||
|
const maxDistance = _ranges.ranges.at(-1)!.distance;
|
||||||
|
const ranges = _ranges.ranges.map((range) => ({
|
||||||
|
factor: range.factor,
|
||||||
|
distance: range.distance,
|
||||||
|
fakeDistance: fakeLogSquishyHigh(range.distance / maxDistance),
|
||||||
|
}));
|
||||||
|
const maxFakeDistance = ranges.at(-1)!.fakeDistance;
|
||||||
|
const maxFactor = ranges[0].factor;
|
||||||
|
|
||||||
|
const { strings } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VisualizerCard>
|
||||||
|
<Flex
|
||||||
|
position="absolute"
|
||||||
|
top="0"
|
||||||
|
left="0"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
direction="column"
|
||||||
|
>
|
||||||
|
<Text m="3" size="2" color="gray">
|
||||||
|
{strings.website.tools.tankopedia.visualizers.assault.title}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
p="3"
|
||||||
|
pt="8"
|
||||||
|
pr="5"
|
||||||
|
style={{ backgroundColor: Var('gray-1') }}
|
||||||
|
flexGrow="1"
|
||||||
|
>
|
||||||
|
<Flex flexGrow="1">
|
||||||
|
<Box position="relative" width={Var('space-8')}>
|
||||||
|
{ranges.map((range, index) => (
|
||||||
|
<Text
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
bottom: `${fakeLogSquishyLow(range.factor / maxFactor) * 100}%`,
|
||||||
|
transform: 'translateY(50%)',
|
||||||
|
}}
|
||||||
|
color="gray"
|
||||||
|
size="1"
|
||||||
|
wrap="nowrap"
|
||||||
|
>
|
||||||
|
{literals(strings.common.units.hp, [
|
||||||
|
`${Math.round(stats.damageWithoutAssault * range.factor)}`,
|
||||||
|
])}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Flex flexGrow="1" align="end" position="relative">
|
||||||
|
{ranges.map((range, index) => {
|
||||||
|
const mix =
|
||||||
|
clamp(2 * (range.factor / maxFactor - 1) + 1, 0, 1) * 100;
|
||||||
|
const color0 = `color-mix(in hsl, ${Var('tomato-9')}, ${Var('jade-9')} ${mix}%)`;
|
||||||
|
const color1 = `color-mix(in hsl, ${Var('tomato-7')}, ${Var('jade-7')} ${mix}%)`;
|
||||||
|
const last = ranges[index - 1];
|
||||||
|
const lastFakeDistance = last?.fakeDistance ?? 0;
|
||||||
|
const fakeWidth = range.fakeDistance - lastFakeDistance;
|
||||||
|
const fraction = fakeWidth / ranges.at(-1)!.fakeDistance;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
width={`${fraction * 100}%`}
|
||||||
|
height={`${fakeLogSquishyLow(range.factor / maxFactor) * 100}%`}
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(45deg, ${color1}, ${color0})`,
|
||||||
|
}}
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{ranges.map((range, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
position="absolute"
|
||||||
|
left="0"
|
||||||
|
bottom={`${fakeLogSquishyLow(range.factor / maxFactor) * 100}%`}
|
||||||
|
width="100%"
|
||||||
|
height="1pt"
|
||||||
|
style={{
|
||||||
|
transform: 'translateY(50%)',
|
||||||
|
backgroundColor: Var('gray-11'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<AssaultCursor
|
||||||
|
maxDistance={maxDistance}
|
||||||
|
maxFakeDistance={maxFakeDistance}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex ml="8" height={Var('space-7')}>
|
||||||
|
{ranges.map((range, index) => {
|
||||||
|
const last = ranges[index - 1];
|
||||||
|
const lastFakeDistance = last?.fakeDistance ?? 0;
|
||||||
|
const fakeWidth = range.fakeDistance - lastFakeDistance;
|
||||||
|
const fraction = fakeWidth / ranges.at(-1)!.fakeDistance;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
position="relative"
|
||||||
|
key={index}
|
||||||
|
width={`${fraction * 100}%`}
|
||||||
|
height="100%"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
size="1"
|
||||||
|
color="gray"
|
||||||
|
mt="2"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
transform: 'translateX(50%)',
|
||||||
|
writingMode: 'vertical-rl',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{literals(strings.common.units.m, [
|
||||||
|
`${Math.round(range.distance)}`,
|
||||||
|
])}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</VisualizerCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
+30
-96
@@ -5,36 +5,36 @@ import {
|
|||||||
resolvePenetrationCoefficient,
|
resolvePenetrationCoefficient,
|
||||||
} from '@blitzkit/core';
|
} from '@blitzkit/core';
|
||||||
import { literals } from '@blitzkit/i18n';
|
import { literals } from '@blitzkit/i18n';
|
||||||
import {
|
import { Flex, Heading, IconButton, Tooltip } from '@radix-ui/themes';
|
||||||
Flex,
|
|
||||||
Heading,
|
|
||||||
IconButton,
|
|
||||||
Slider,
|
|
||||||
Text,
|
|
||||||
TextField,
|
|
||||||
Tooltip,
|
|
||||||
} from '@radix-ui/themes';
|
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { lerp } from 'three/src/math/MathUtils.js';
|
|
||||||
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||||
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
|
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
|
||||||
import { Duel } from '../../../../../../stores/duel';
|
import { Duel } from '../../../../../../stores/duel';
|
||||||
|
import type { MaybeSkeletonComponentProps } from '../../../../../../types/maybeSkeletonComponentProps';
|
||||||
|
import { AssaultRangesVisualizer } from './AssaultRangesVisualizer';
|
||||||
|
import { GunFlexibilityVisualizer } from './GunFlexibilityVisualizer';
|
||||||
import { Info } from './Info';
|
import { Info } from './Info';
|
||||||
import { InfoWithDelta } from './InfoWithDelta';
|
import { InfoWithDelta } from './InfoWithDelta';
|
||||||
|
import { ReloadVisualizer } from './ReloadVisualizer';
|
||||||
import { RicochetVisualizer } from './RicochetVisualizer';
|
import { RicochetVisualizer } from './RicochetVisualizer';
|
||||||
import { StatsTableWrapper } from './StatsTableWrapper';
|
import { StatsTableWrapper } from './StatsTableWrapper';
|
||||||
import type { StatsAcceptorProps } from './TraverseVisualizer';
|
import type { StatsAcceptorProps } from './TraverseVisualizer';
|
||||||
|
|
||||||
export function Firepower({ stats }: StatsAcceptorProps) {
|
export function Firepower({
|
||||||
|
stats,
|
||||||
|
skeleton,
|
||||||
|
}: StatsAcceptorProps & MaybeSkeletonComponentProps) {
|
||||||
const mutateDuel = Duel.useMutation();
|
const mutateDuel = Duel.useMutation();
|
||||||
const { strings, unwrap } = useLocale();
|
const { strings, unwrap } = useLocale();
|
||||||
const [penetrationDistance, setPenetrationDistance] = useState(250);
|
const [penetrationDistance, setPenetrationDistance] = useState(250);
|
||||||
const setPenetrationDistanceDebounced = debounce((value: number) => {
|
const setPenetrationDistanceDebounced = debounce((value: number) => {
|
||||||
setPenetrationDistance(value);
|
setPenetrationDistance(value);
|
||||||
}, 500);
|
}, 500);
|
||||||
const { gun, shell, turret } = Duel.use((state) => state.protagonist);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
|
const shell = Duel.use((state) => state.protagonist.shell);
|
||||||
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
|
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
|
||||||
const hasSupercharger = useEquipment(107);
|
const hasSupercharger = useEquipment(107);
|
||||||
const hasCalibratedShells = useEquipment(103);
|
const hasCalibratedShells = useEquipment(103);
|
||||||
@@ -48,8 +48,7 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
|||||||
]);
|
]);
|
||||||
const tankModelDefinition = useTankModelDefinition();
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
turretModelDefinition.guns[gun.gun_type!.value.base.id];
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (penetrationDistanceInput.current) {
|
if (penetrationDistanceInput.current) {
|
||||||
@@ -65,7 +64,7 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
|||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Flex>
|
<Flex>
|
||||||
{gun.gun_type!.value.base.shells.map((thisShell, shellIndex) => {
|
{gun.shells.map((thisShell, shellIndex) => {
|
||||||
const selected = thisShell.id === shell.id;
|
const selected = thisShell.id === shell.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -78,13 +77,9 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
|||||||
borderTopLeftRadius: shellIndex === 0 ? undefined : 0,
|
borderTopLeftRadius: shellIndex === 0 ? undefined : 0,
|
||||||
borderBottomLeftRadius: shellIndex === 0 ? undefined : 0,
|
borderBottomLeftRadius: shellIndex === 0 ? undefined : 0,
|
||||||
borderTopRightRadius:
|
borderTopRightRadius:
|
||||||
shellIndex === gun.gun_type!.value.base.shells.length - 1
|
shellIndex === gun.shells.length - 1 ? undefined : 0,
|
||||||
? undefined
|
|
||||||
: 0,
|
|
||||||
borderBottomRightRadius:
|
borderBottomRightRadius:
|
||||||
shellIndex === gun.gun_type!.value.base.shells.length - 1
|
shellIndex === gun.shells.length - 1 ? undefined : 0,
|
||||||
? undefined
|
|
||||||
: 0,
|
|
||||||
marginLeft: shellIndex === 0 ? 0 : -1,
|
marginLeft: shellIndex === 0 ? 0 : -1,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -112,6 +107,17 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
|||||||
{strings.common.gun_types[gun.gun_type!.$case]}
|
{strings.common.gun_types[gun.gun_type!.$case]}
|
||||||
</Info>
|
</Info>
|
||||||
<InfoWithDelta stats={stats} decimals={0} value="dpm" />
|
<InfoWithDelta stats={stats} decimals={0} value="dpm" />
|
||||||
|
<InfoWithDelta stats={stats} decimals={0} value="damage" />
|
||||||
|
{gun.gun_type!.$case !== 'regular' && (
|
||||||
|
<InfoWithDelta stats={stats} indent decimals={0} value="clipDamage" />
|
||||||
|
)}
|
||||||
|
<InfoWithDelta stats={stats} decimals={0} value="moduleDamage" />
|
||||||
|
<InfoWithDelta stats={stats} decimals={0} value="penetration" />
|
||||||
|
|
||||||
|
{gun.assault_ranges?.types.includes(shell.type) && (
|
||||||
|
<AssaultRangesVisualizer stats={stats} ranges={gun.assault_ranges} />
|
||||||
|
)}
|
||||||
|
|
||||||
{gun.gun_type!.$case === 'auto_reloader' && (
|
{gun.gun_type!.$case === 'auto_reloader' && (
|
||||||
<InfoWithDelta stats={stats} decimals={0} indent value="dpmEffective" />
|
<InfoWithDelta stats={stats} decimals={0} indent value="dpmEffective" />
|
||||||
)}
|
)}
|
||||||
@@ -170,81 +176,8 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
|||||||
value="intraClip"
|
value="intraClip"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* <ReloadVisualizer stats={stats} /> */}
|
<ReloadVisualizer stats={stats} />
|
||||||
|
|
||||||
<InfoWithDelta stats={stats} decimals={0} value="penetration" />
|
|
||||||
{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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<InfoWithDelta stats={stats} decimals={0} value="damage" />
|
|
||||||
{gun.gun_type!.$case !== 'regular' && (
|
|
||||||
<InfoWithDelta stats={stats} indent decimals={0} value="clipDamage" />
|
|
||||||
)}
|
|
||||||
<InfoWithDelta stats={stats} decimals={0} value="moduleDamage" />
|
|
||||||
{isExplosive(shell.type) && (
|
{isExplosive(shell.type) && (
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
stats={stats}
|
stats={stats}
|
||||||
@@ -333,7 +266,7 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
|||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
stats={stats}
|
stats={stats}
|
||||||
decimals={1}
|
decimals={1}
|
||||||
prefix="x "
|
prefix="x"
|
||||||
indent
|
indent
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
value="dispersionGunDamaged"
|
value="dispersionGunDamaged"
|
||||||
@@ -394,6 +327,7 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<GunFlexibilityVisualizer skeleton={skeleton} />
|
||||||
</StatsTableWrapper>
|
</StatsTableWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
|
||||||
|
export const zoom = 60;
|
||||||
|
|
||||||
|
export function CameraPositioner() {
|
||||||
|
useFrame(({ camera, gl: { domElement } }) => {
|
||||||
|
const { width } = domElement.getBoundingClientRect();
|
||||||
|
const physicalWidth = width / zoom;
|
||||||
|
|
||||||
|
camera.position.set(10, 0, -physicalWidth / 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
import type { QuicklimeEvent } from 'quicklime';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import type { Group } from 'three';
|
||||||
|
import { awaitableModelDefinitions } from '../../../../../../../../core/awaitables/modelDefinitions';
|
||||||
|
import {
|
||||||
|
modelTransformEvent,
|
||||||
|
type ModelTransformEventData,
|
||||||
|
} from '../../../../../../../../core/blitzkit/modelTransform';
|
||||||
|
import { Duel } from '../../../../../../../../stores/duel';
|
||||||
|
import { ModelChunk } from './ModelChunk';
|
||||||
|
|
||||||
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
|
|
||||||
|
export function CanvasContent() {
|
||||||
|
const hullWrapper = useRef<Group>(null);
|
||||||
|
const gunWrapper = useRef<Group>(null);
|
||||||
|
|
||||||
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
|
|
||||||
|
const tankModel = modelDefinitions.models[tank.id];
|
||||||
|
const turretModel = tankModel.turrets[turret.id];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleModelTransformEvent(
|
||||||
|
event: QuicklimeEvent<ModelTransformEventData>,
|
||||||
|
) {
|
||||||
|
if (!gunWrapper.current || !hullWrapper.current) return;
|
||||||
|
|
||||||
|
gunWrapper.current.rotation.x = event.data.pitch;
|
||||||
|
if (event.data.yaw) hullWrapper.current.rotation.y = event.data.yaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
modelTransformEvent.on(handleModelTransformEvent);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
modelTransformEvent.off(handleModelTransformEvent);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<group ref={hullWrapper} position={[0, 0, turretModel.gun_origin.z]}>
|
||||||
|
<group position={[0, 0, -turretModel.gun_origin.z]}>
|
||||||
|
<ModelChunk only="body" />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<ModelChunk only="turret" />
|
||||||
|
|
||||||
|
<group ref={gunWrapper}>
|
||||||
|
<ModelChunk only="gun" />
|
||||||
|
</group>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
import { SmartCanvas } from '../../../../../../../SmartCanvas';
|
||||||
|
import { CameraPositioner, zoom } from './CameraPositioner';
|
||||||
|
import { CanvasContent } from './CanvasContent';
|
||||||
|
|
||||||
|
export function FlexibilityCanvas() {
|
||||||
|
return (
|
||||||
|
<SmartCanvas orthographic camera={{ zoom, rotation: [0, Math.PI / 2, 0] }}>
|
||||||
|
<CanvasContent />
|
||||||
|
<CameraPositioner />
|
||||||
|
</SmartCanvas>
|
||||||
|
);
|
||||||
|
}
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
import { mauveDark } from '@radix-ui/colors';
|
||||||
|
import { LineBasicMaterial, MeshBasicMaterial } from 'three';
|
||||||
|
import { awaitableModelDefinitions } from '../../../../../../../../core/awaitables/modelDefinitions';
|
||||||
|
import { jsxTree } from '../../../../../../../../core/blitzkit/jsxTree';
|
||||||
|
import { useModel } from '../../../../../../../../hooks/useModel';
|
||||||
|
import { Duel } from '../../../../../../../../stores/duel';
|
||||||
|
|
||||||
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
|
|
||||||
|
const surfaceMaterial: Record<Props['only'], MeshBasicMaterial> = {
|
||||||
|
gun: new MeshBasicMaterial({
|
||||||
|
color: mauveDark.mauve8,
|
||||||
|
toneMapped: false,
|
||||||
|
}),
|
||||||
|
turret: new MeshBasicMaterial({
|
||||||
|
color: mauveDark.mauve6,
|
||||||
|
toneMapped: false,
|
||||||
|
}),
|
||||||
|
body: new MeshBasicMaterial({
|
||||||
|
color: mauveDark.mauve6,
|
||||||
|
toneMapped: false,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.5,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const outlineMaterial: Record<Props['only'], LineBasicMaterial> = {
|
||||||
|
gun: new LineBasicMaterial({
|
||||||
|
color: mauveDark.mauve11,
|
||||||
|
toneMapped: false,
|
||||||
|
}),
|
||||||
|
turret: new LineBasicMaterial({
|
||||||
|
color: mauveDark.mauve8,
|
||||||
|
toneMapped: false,
|
||||||
|
}),
|
||||||
|
body: new LineBasicMaterial({
|
||||||
|
color: mauveDark.mauve8,
|
||||||
|
toneMapped: false,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.5,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
only: 'gun' | 'turret' | 'body';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModelChunk({ only }: Props) {
|
||||||
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
|
|
||||||
|
const tankModel = modelDefinitions.models[tank.id];
|
||||||
|
const turretModel = tankModel.turrets[turret.id];
|
||||||
|
const gunModel = turretModel.guns[gun.id];
|
||||||
|
|
||||||
|
const originZ = tankModel.turret_origin.z + turretModel.gun_origin.z;
|
||||||
|
const originY = tankModel.turret_origin.y + turretModel.gun_origin.y;
|
||||||
|
|
||||||
|
const { gltf } = useModel(tank.id);
|
||||||
|
const nodes = Object.values(gltf.nodes);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group position={[0, -originY, originZ]} rotation={[-Math.PI / 2, 0, 0]}>
|
||||||
|
{nodes.map((node) => {
|
||||||
|
const isCurrentMantlet =
|
||||||
|
node.name ===
|
||||||
|
`gun_${gunModel.model_id.toString().padStart(2, '0')}_mask`;
|
||||||
|
const isCurrentGun =
|
||||||
|
node.name === `gun_${gunModel.model_id.toString().padStart(2, '0')}`;
|
||||||
|
const isGun = isCurrentGun || isCurrentMantlet;
|
||||||
|
|
||||||
|
const isCurrentTurret =
|
||||||
|
node.name ===
|
||||||
|
`turret_${turretModel.model_id.toString().padStart(2, '0')}`;
|
||||||
|
const isTurret = isCurrentTurret;
|
||||||
|
|
||||||
|
const isHull = node.name === 'hull';
|
||||||
|
const isWheel = node.name.startsWith('chassis_wheel_');
|
||||||
|
const isTrack = node.name.startsWith('chassis_track_');
|
||||||
|
const isBody = isHull || isWheel || isTrack;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(only === 'gun' && !isGun) ||
|
||||||
|
(only === 'turret' && !isTurret) ||
|
||||||
|
(only === 'body' && !isBody)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsxTree(node, {
|
||||||
|
mesh(mesh, props, key) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<mesh key={key} {...props} material={surfaceMaterial[only]} />
|
||||||
|
<lineSegments material={outlineMaterial[only]}>
|
||||||
|
<edgesGeometry args={[mesh.geometry, 29]} />
|
||||||
|
</lineSegments>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
+397
@@ -0,0 +1,397 @@
|
|||||||
|
import { literals } from '@blitzkit/i18n';
|
||||||
|
import { Box, Card, Flex } from '@radix-ui/themes';
|
||||||
|
import { clamp } from 'lodash-es';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { degToRad, radToDeg } from 'three/src/math/MathUtils.js';
|
||||||
|
import { awaitableModelDefinitions } from '../../../../../../../core/awaitables/modelDefinitions';
|
||||||
|
import {
|
||||||
|
DEFAULT_PITCH_TRANSITION,
|
||||||
|
applyPitchYawLimits,
|
||||||
|
} from '../../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
|
import { hasEquipment } from '../../../../../../../core/blitzkit/hasEquipment';
|
||||||
|
import { modelTransformEvent } from '../../../../../../../core/blitzkit/modelTransform';
|
||||||
|
import { Var } from '../../../../../../../core/radix/var';
|
||||||
|
import { useLocale } from '../../../../../../../hooks/useLocale';
|
||||||
|
import { Duel } from '../../../../../../../stores/duel';
|
||||||
|
import type { MaybeSkeletonComponentProps } from '../../../../../../../types/maybeSkeletonComponentProps';
|
||||||
|
import { VisualizerCard } from '../VisualizerCard';
|
||||||
|
import { VisualizerCornerStat } from '../VisualizerCornerStat';
|
||||||
|
import { FlexibilityCanvas } from './components/FlexibilityCanvas';
|
||||||
|
|
||||||
|
const ANGLE_COEFFICIENT = 1 / 10;
|
||||||
|
|
||||||
|
const modelDefinition = await awaitableModelDefinitions;
|
||||||
|
|
||||||
|
function mag(x: number) {
|
||||||
|
return (1 / Math.PI) * Math.atan(ANGLE_COEFFICIENT * x) + 1 / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function magInverse(y: number): number {
|
||||||
|
return (1 / ANGLE_COEFFICIENT) * Math.tan(Math.PI * y - Math.PI / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function c(thetaDeg: number, m = 1) {
|
||||||
|
const theta = thetaDeg * (Math.PI / 180);
|
||||||
|
return `${m * Math.cos(theta)} ${-m * Math.sin(theta)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GunFlexibilityVisualizer({
|
||||||
|
skeleton,
|
||||||
|
}: MaybeSkeletonComponentProps) {
|
||||||
|
const { strings } = useLocale();
|
||||||
|
|
||||||
|
const duelStore = Duel.useStore();
|
||||||
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
|
|
||||||
|
const tankModel = modelDefinition.models[tank.id];
|
||||||
|
const turretModel = tankModel.turrets[turret.id];
|
||||||
|
const gunModel = turretModel.guns[gun.id];
|
||||||
|
|
||||||
|
const [yaw, setYaw] = useState(0);
|
||||||
|
const [pitch, setPitch] = useState(0);
|
||||||
|
const [minPitch, setMinPitch] = useState(
|
||||||
|
-degToRad(gunModel.pitch.front?.max ?? gunModel.pitch.max),
|
||||||
|
);
|
||||||
|
const [maxPitch, setMaxPitch] = useState(
|
||||||
|
-degToRad(gunModel.pitch.front?.min ?? gunModel.pitch.min),
|
||||||
|
);
|
||||||
|
|
||||||
|
const container = useRef<HTMLDivElement>(null);
|
||||||
|
const highlighter = useRef<HTMLDivElement>(null);
|
||||||
|
const pointer = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const p = gunModel.pitch;
|
||||||
|
const y = turretModel.yaw;
|
||||||
|
const f = p.front;
|
||||||
|
const b = p.back;
|
||||||
|
const t = gunModel.pitch.transition ?? DEFAULT_PITCH_TRANSITION;
|
||||||
|
|
||||||
|
let d = '';
|
||||||
|
|
||||||
|
// right
|
||||||
|
const mo = mag(p.max);
|
||||||
|
if (y) {
|
||||||
|
d += `M 0 0`;
|
||||||
|
d += `L ${c(90 - y.max, mo)}`;
|
||||||
|
} else if (b) {
|
||||||
|
d += `M ${c(-90 + b.range / 2 + t, mo)}`;
|
||||||
|
} else {
|
||||||
|
d += `M ${c(-90, mo)}`;
|
||||||
|
}
|
||||||
|
if (f) {
|
||||||
|
d += `A ${mo} ${mo} 0 0 0 ${c(90 - f.range / 2 - t, mo)}`;
|
||||||
|
} else {
|
||||||
|
d += `A ${mo} ${mo} 0 0 0 ${c(90, mo)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// front
|
||||||
|
if (f) {
|
||||||
|
const m = mag(f.max);
|
||||||
|
d += `L ${c(90 - f.range / 2, m)}`;
|
||||||
|
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// left
|
||||||
|
if (f) {
|
||||||
|
d += `L ${c(90 + f.range / 2 + t, mo)}`;
|
||||||
|
} else {
|
||||||
|
d += `L ${c(90, mo)}`;
|
||||||
|
}
|
||||||
|
if (y) {
|
||||||
|
d += `A ${mo} ${mo} 0 0 0 ${c(90 - y.min, mo)}`;
|
||||||
|
} else if (b) {
|
||||||
|
d += `A ${mo} ${mo} 0 0 0 ${c(-90 - b.range / 2 - t, mo)}`;
|
||||||
|
} else {
|
||||||
|
d += `A ${mo} ${mo} 0 0 0 ${c(-90, mo)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// back
|
||||||
|
if (!y && b) {
|
||||||
|
const m = mag(b.max);
|
||||||
|
d += `L ${c(-90 - b.range / 2, m)}`;
|
||||||
|
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
d += `Z`;
|
||||||
|
|
||||||
|
// right
|
||||||
|
const mi = mag(p.min);
|
||||||
|
if (y) {
|
||||||
|
d += `M 0 0`;
|
||||||
|
d += `L ${c(90 - y.max, mi)}`;
|
||||||
|
} else if (b) {
|
||||||
|
d += `M ${c(-90 + b.range / 2 + t, mi)}`;
|
||||||
|
} else {
|
||||||
|
d += `M ${c(-90, mi)}`;
|
||||||
|
}
|
||||||
|
if (f) {
|
||||||
|
d += `A ${mi} ${mi} 0 0 0 ${c(90 - f.range / 2 - t, mi)}`;
|
||||||
|
} else {
|
||||||
|
d += `A ${mi} ${mi} 0 0 0 ${c(90, mi)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// front
|
||||||
|
if (f) {
|
||||||
|
const m = mag(f.min);
|
||||||
|
d += `L ${c(90 - f.range / 2, m)}`;
|
||||||
|
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// left
|
||||||
|
if (f) {
|
||||||
|
d += `L ${c(90 + f.range / 2 + t, mi)}`;
|
||||||
|
} else {
|
||||||
|
d += `L ${c(90, mi)}`;
|
||||||
|
}
|
||||||
|
if (y) {
|
||||||
|
d += `A ${mi} ${mi} 0 0 0 ${c(90 - y.min, mi)}`;
|
||||||
|
} else if (b) {
|
||||||
|
d += `A ${mi} ${mi} 0 0 0 ${c(-90 - b.range / 2 - t, mi)}`;
|
||||||
|
} else {
|
||||||
|
d += `A ${mi} ${mi} 0 0 0 ${c(-90, mi)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// back
|
||||||
|
if (!y && b) {
|
||||||
|
const m = mag(b.min);
|
||||||
|
d += `L ${c(-90 - b.range / 2, m)}`;
|
||||||
|
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
d += `Z`;
|
||||||
|
|
||||||
|
const r = mag(radToDeg(-pitch));
|
||||||
|
const dotX = r * Math.sin(yaw);
|
||||||
|
const dotY = r * Math.cos(yaw);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column-reverse" gap="0">
|
||||||
|
<Box mt="-3" px="2">
|
||||||
|
<Card variant="classic" style={{ height: '10rem' }}>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(var(--gray-1), var(--gray-3))',
|
||||||
|
}}
|
||||||
|
position="absolute"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
top="0"
|
||||||
|
left="0"
|
||||||
|
>
|
||||||
|
{!skeleton && <FlexibilityCanvas />}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
top="0"
|
||||||
|
left="33%"
|
||||||
|
style={{
|
||||||
|
background: `conic-gradient(
|
||||||
|
at 0 50%,
|
||||||
|
|
||||||
|
transparent,
|
||||||
|
transparent ${Math.PI / 2 - maxPitch}rad,
|
||||||
|
|
||||||
|
${Var('cyan-a4')} ${Math.PI / 2 - maxPitch}rad,
|
||||||
|
${Var('jade-a4')} ${Math.PI / 2 - minPitch}rad,
|
||||||
|
|
||||||
|
transparent ${Math.PI / 2 - minPitch}rad,
|
||||||
|
transparent 180deg
|
||||||
|
)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<VisualizerCard
|
||||||
|
ref={container}
|
||||||
|
style={{ touchAction: 'none' }}
|
||||||
|
onPointerMove={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!container.current || !highlighter.current || !pointer.current)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const rect = container.current.getBoundingClientRect();
|
||||||
|
const u = event.clientX - rect.left - rect.width / 2;
|
||||||
|
const v = event.clientY - rect.top - rect.height / 2;
|
||||||
|
let yaw = Math.atan2(u, -v);
|
||||||
|
|
||||||
|
const {
|
||||||
|
equipmentMatrix,
|
||||||
|
tank: { equipment_preset },
|
||||||
|
} = duelStore.getState().protagonist;
|
||||||
|
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||||
|
122,
|
||||||
|
equipment_preset,
|
||||||
|
equipmentMatrix,
|
||||||
|
);
|
||||||
|
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||||
|
124,
|
||||||
|
equipment_preset,
|
||||||
|
equipmentMatrix,
|
||||||
|
);
|
||||||
|
|
||||||
|
const min = applyPitchYawLimits(
|
||||||
|
-Math.PI,
|
||||||
|
yaw,
|
||||||
|
p,
|
||||||
|
y,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
|
hasDownImprovedVerticalStabilizer,
|
||||||
|
);
|
||||||
|
const max = applyPitchYawLimits(
|
||||||
|
Math.PI,
|
||||||
|
yaw,
|
||||||
|
p,
|
||||||
|
y,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
|
hasDownImprovedVerticalStabilizer,
|
||||||
|
);
|
||||||
|
|
||||||
|
yaw = min[1];
|
||||||
|
let t = 2 * Math.sqrt((u / rect.width) ** 2 + (v / rect.height) ** 2);
|
||||||
|
t = clamp(t, 0, 1);
|
||||||
|
let pitch = degToRad(magInverse(-t));
|
||||||
|
pitch = clamp(pitch, min[0], max[0]);
|
||||||
|
|
||||||
|
setPitch(pitch);
|
||||||
|
setYaw(yaw);
|
||||||
|
setMinPitch(min[0]);
|
||||||
|
setMaxPitch(max[0]);
|
||||||
|
|
||||||
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
|
}}
|
||||||
|
mb="0"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
ref={highlighter}
|
||||||
|
position="absolute"
|
||||||
|
top="0"
|
||||||
|
left="0"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
style={{
|
||||||
|
borderRadius: Var('radius-2'),
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
style={{
|
||||||
|
opacity: 2 ** -3,
|
||||||
|
backgroundImage: `url(/assets/images/tankopedia/visualizers/ricochet/armor-hash.png)`,
|
||||||
|
boxShadow: 'inset 0 0 4rem 1rem black',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: 'calc(100%)',
|
||||||
|
height: 'calc(100%)',
|
||||||
|
}}
|
||||||
|
viewBox="-1 -1 2 2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
fill="var(--gray-5)"
|
||||||
|
stroke="var(--gray-8)"
|
||||||
|
strokeWidth="1px"
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
d={d}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<path
|
||||||
|
stroke="var(--gray-10)"
|
||||||
|
fill="transparent"
|
||||||
|
strokeWidth="1px"
|
||||||
|
strokeDasharray="0.5rem 0.5rem"
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
d={`
|
||||||
|
M 0.5 0
|
||||||
|
A 0.5 0.5 0 1 1 -0.5 0
|
||||||
|
A 0.5 0.5 0 1 1 0.5 0
|
||||||
|
Z
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
ref={pointer}
|
||||||
|
position="absolute"
|
||||||
|
top="50%"
|
||||||
|
left="50%"
|
||||||
|
width="0"
|
||||||
|
height="0"
|
||||||
|
style={{
|
||||||
|
transform: `translate(-50%, -50%) rotate(${yaw + Math.PI}rad)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
width="1pt"
|
||||||
|
height="14rem"
|
||||||
|
style={{
|
||||||
|
borderRadius: Var('radius-1'),
|
||||||
|
background: `linear-gradient(${Var('gray-a11')}, ${Var('gray-a6')})`,
|
||||||
|
boxShadow:
|
||||||
|
'var(--white-a12) -0.25rem 0 1rem, var(--black-a12) 0.25rem 0 1rem',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
left={`${50 * (dotX + 1)}%`}
|
||||||
|
bottom={`${50 * (dotY + 1)}%`}
|
||||||
|
width="0.25rem"
|
||||||
|
height="0.25rem"
|
||||||
|
style={{
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: Var('gray-12'),
|
||||||
|
transform: 'translate(-50%, 50%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VisualizerCornerStat
|
||||||
|
label={strings.website.tools.tankopedia.visualizers.flexibility.max}
|
||||||
|
value={literals(strings.common.units.deg, [
|
||||||
|
radToDeg(maxPitch).toFixed(0),
|
||||||
|
])}
|
||||||
|
side="bottom-left"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VisualizerCornerStat
|
||||||
|
label={strings.website.tools.tankopedia.visualizers.flexibility.min}
|
||||||
|
value={literals(strings.common.units.deg, [
|
||||||
|
radToDeg(minPitch).toFixed(0),
|
||||||
|
])}
|
||||||
|
side="bottom-right"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VisualizerCornerStat
|
||||||
|
label={strings.website.tools.tankopedia.visualizers.flexibility.pitch}
|
||||||
|
value={literals(strings.common.units.deg, [
|
||||||
|
radToDeg(pitch).toFixed(0),
|
||||||
|
])}
|
||||||
|
side="top-left"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VisualizerCornerStat
|
||||||
|
label={strings.website.tools.tankopedia.visualizers.flexibility.yaw}
|
||||||
|
value={literals(strings.common.units.deg, [radToDeg(yaw).toFixed(0)])}
|
||||||
|
side="top-right"
|
||||||
|
/>
|
||||||
|
</VisualizerCard>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
+139
-125
@@ -1,7 +1,7 @@
|
|||||||
import { createDefaultSkills } from '@blitzkit/core';
|
import { createDefaultSkills } from '@blitzkit/core';
|
||||||
import { Flex, Progress, Text } from '@radix-ui/themes';
|
import { Flex, Progress, Text } from '@radix-ui/themes';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
import { useMemo, type ComponentProps, type ReactNode } from 'react';
|
import { memo, useMemo, type ComponentProps, type ReactNode } from 'react';
|
||||||
import { awaitableEquipmentDefinitions } from '../../../../../../core/awaitables/equipmentDefinitions';
|
import { awaitableEquipmentDefinitions } from '../../../../../../core/awaitables/equipmentDefinitions';
|
||||||
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
|
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
|
||||||
import { awaitableProvisionDefinitions } from '../../../../../../core/awaitables/provisionDefinitions';
|
import { awaitableProvisionDefinitions } from '../../../../../../core/awaitables/provisionDefinitions';
|
||||||
@@ -48,133 +48,147 @@ const [
|
|||||||
awaitableSkillDefinitions,
|
awaitableSkillDefinitions,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function InfoWithDelta({
|
export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
||||||
stats,
|
({ stats, indent, noRanking, deltaType, ...props }) => {
|
||||||
indent,
|
const { strings } = useLocale();
|
||||||
noRanking,
|
const relativeAgainst = TankopediaEphemeral.use(
|
||||||
deltaType,
|
(state) => state.relativeAgainst,
|
||||||
...props
|
);
|
||||||
}: InfoWithDeltaProps) {
|
const uhWhatDoICallThisVariable =
|
||||||
const { strings } = useLocale();
|
|
||||||
const relativeAgainst = TankopediaEphemeral.use(
|
|
||||||
(state) => state.relativeAgainst,
|
|
||||||
);
|
|
||||||
const uhWhatDoICallThisVariable =
|
|
||||||
typeof props.value === 'function'
|
|
||||||
? props.value(stats)!
|
|
||||||
: (stats[props.value] as number);
|
|
||||||
const delta = useDelta(uhWhatDoICallThisVariable);
|
|
||||||
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
|
||||||
const others = useMemo(() => {
|
|
||||||
const defaultSkills = createDefaultSkills(skillDefinitions);
|
|
||||||
|
|
||||||
return Object.values(tankDefinitions.tanks)
|
|
||||||
.filter(
|
|
||||||
(tank) =>
|
|
||||||
(relativeAgainst === TankopediaRelativeAgainst.Class &&
|
|
||||||
tank.tier === protagonistTank.tier &&
|
|
||||||
tank.class === protagonistTank.class) ||
|
|
||||||
(relativeAgainst === TankopediaRelativeAgainst.Tier &&
|
|
||||||
tank.tier === protagonistTank.tier) ||
|
|
||||||
relativeAgainst === TankopediaRelativeAgainst.All,
|
|
||||||
)
|
|
||||||
.map((tank) => {
|
|
||||||
const thisTankModel = modelDefinitions.models[tank.id];
|
|
||||||
const member = tankToDuelMember(
|
|
||||||
tank,
|
|
||||||
thisTankModel,
|
|
||||||
provisionDefinitions,
|
|
||||||
);
|
|
||||||
|
|
||||||
return tankCharacteristics(
|
|
||||||
{
|
|
||||||
applyDynamicArmor: false,
|
|
||||||
applyReactiveArmor: false,
|
|
||||||
applySpallLiner: false,
|
|
||||||
camouflage: member.camouflage,
|
|
||||||
consumables: member.consumables,
|
|
||||||
crewSkills: defaultSkills,
|
|
||||||
provisions: member.provisions,
|
|
||||||
engine: member.engine,
|
|
||||||
gun: member.gun,
|
|
||||||
equipmentMatrix: member.equipmentMatrix,
|
|
||||||
shell: member.shell,
|
|
||||||
stockEngine: tank.engines[0],
|
|
||||||
stockGun: tank.turrets[0].guns[0],
|
|
||||||
stockTrack: tank.tracks[0],
|
|
||||||
stockTurret: tank.turrets[0],
|
|
||||||
tank,
|
|
||||||
track: member.track,
|
|
||||||
turret: member.turret,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
equipmentDefinitions,
|
|
||||||
provisionDefinitions,
|
|
||||||
tankModelDefinition: modelDefinitions.models[tank.id],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.filter((tank) => {
|
|
||||||
const othersValue =
|
|
||||||
typeof props.value === 'function'
|
|
||||||
? props.value(tank)!
|
|
||||||
: (tank[props.value] as number);
|
|
||||||
|
|
||||||
return othersValue !== undefined;
|
|
||||||
});
|
|
||||||
}, [relativeAgainst]);
|
|
||||||
const betterTanks = others.filter((tank) => {
|
|
||||||
const othersValue =
|
|
||||||
typeof props.value === 'function'
|
typeof props.value === 'function'
|
||||||
? props.value(tank)!
|
? props.value(stats)!
|
||||||
: (tank[props.value] as number);
|
: (stats[props.value] as number);
|
||||||
|
const delta = useDelta(uhWhatDoICallThisVariable);
|
||||||
|
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
||||||
|
const shellIndex = Duel.use((state) =>
|
||||||
|
state.protagonist.gun.shells.indexOf(state.protagonist.shell),
|
||||||
|
);
|
||||||
|
const equipmentMatrix = Duel.use(
|
||||||
|
(state) => state.protagonist.equipmentMatrix,
|
||||||
|
);
|
||||||
|
const others = useMemo(() => {
|
||||||
|
const defaultSkills = createDefaultSkills(skillDefinitions);
|
||||||
|
|
||||||
if (othersValue === undefined) return false;
|
return Object.values(tankDefinitions.tanks)
|
||||||
return deltaType === 'lowerIsBetter'
|
.filter(
|
||||||
? othersValue < uhWhatDoICallThisVariable
|
(tank) =>
|
||||||
: othersValue > uhWhatDoICallThisVariable;
|
(relativeAgainst === TankopediaRelativeAgainst.Class &&
|
||||||
});
|
tank.tier === protagonistTank.tier &&
|
||||||
const goodness = (others.length - betterTanks.length) / others.length;
|
tank.class === protagonistTank.class) ||
|
||||||
let color: ComponentProps<typeof Progress>['color'];
|
(relativeAgainst === TankopediaRelativeAgainst.Tier &&
|
||||||
|
tank.tier === protagonistTank.tier) ||
|
||||||
|
relativeAgainst === TankopediaRelativeAgainst.All,
|
||||||
|
)
|
||||||
|
.map((tank) => {
|
||||||
|
const thisTankModel = modelDefinitions.models[tank.id];
|
||||||
|
const member = tankToDuelMember(
|
||||||
|
tank,
|
||||||
|
thisTankModel,
|
||||||
|
provisionDefinitions,
|
||||||
|
);
|
||||||
|
|
||||||
if (goodness <= 0.25) color = 'red';
|
if (member.gun.shells[shellIndex] === undefined) {
|
||||||
else if (goodness <= 0.5) color = 'orange';
|
return undefined;
|
||||||
else if (goodness <= 0.75) color = 'yellow';
|
}
|
||||||
else color = 'green';
|
|
||||||
|
|
||||||
return (
|
return tankCharacteristics(
|
||||||
<Flex direction="column">
|
{
|
||||||
<Info
|
applyDynamicArmor: false,
|
||||||
deltaType={deltaType}
|
applyReactiveArmor: false,
|
||||||
indent={indent}
|
applySpallLiner: false,
|
||||||
{...props}
|
camouflage: member.camouflage,
|
||||||
name={
|
consumables: member.consumables,
|
||||||
'name' in props
|
crewSkills: defaultSkills,
|
||||||
? props.name
|
provisions: member.provisions,
|
||||||
: strings.website.tools.tankopedia.characteristics.values[
|
engine: member.engine,
|
||||||
props.value
|
gun: member.gun,
|
||||||
]
|
equipmentMatrix: equipmentMatrix,
|
||||||
}
|
shell: member.gun.shells[shellIndex],
|
||||||
delta={delta}
|
stockEngine: tank.engines[0],
|
||||||
>
|
stockGun: tank.turrets[0].guns[0],
|
||||||
{uhWhatDoICallThisVariable}
|
stockTrack: tank.tracks[0],
|
||||||
</Info>
|
stockTurret: tank.turrets[0],
|
||||||
|
tank,
|
||||||
|
track: member.track,
|
||||||
|
turret: member.turret,
|
||||||
|
assaultDistance: member.assaultDistance,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
equipmentDefinitions,
|
||||||
|
provisionDefinitions,
|
||||||
|
tankModelDefinition: modelDefinitions.models[tank.id],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.filter((tank) => {
|
||||||
|
if (tank === undefined) return false;
|
||||||
|
|
||||||
{!noRanking && (
|
const othersValue =
|
||||||
<Flex pl={indent ? '2' : '0'} align="center" gap="2" mb="2">
|
typeof props.value === 'function'
|
||||||
<Progress
|
? props.value(tank)!
|
||||||
variant="soft"
|
: (tank[props.value] as number);
|
||||||
size="1"
|
|
||||||
value={goodness * 100}
|
|
||||||
color={color}
|
|
||||||
style={{ height: '0.125rem', opacity: 0.5 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text color="gray" size="1">
|
return othersValue !== undefined;
|
||||||
{clamp(betterTanks.length + 1, 1, others.length)} / {others.length}
|
}) as TankCharacteristics[];
|
||||||
</Text>
|
}, [relativeAgainst, shellIndex, equipmentMatrix]);
|
||||||
</Flex>
|
const betterTanks = others.filter((tank) => {
|
||||||
)}
|
const othersValue =
|
||||||
</Flex>
|
typeof props.value === 'function'
|
||||||
);
|
? props.value(tank)!
|
||||||
}
|
: (tank[props.value] as number);
|
||||||
|
|
||||||
|
if (othersValue === undefined) return false;
|
||||||
|
return deltaType === 'lowerIsBetter'
|
||||||
|
? othersValue < uhWhatDoICallThisVariable
|
||||||
|
: othersValue > uhWhatDoICallThisVariable;
|
||||||
|
});
|
||||||
|
const goodness = (others.length - betterTanks.length) / others.length;
|
||||||
|
let color: ComponentProps<typeof Progress>['color'];
|
||||||
|
|
||||||
|
if (goodness <= 0.25) color = 'red';
|
||||||
|
else if (goodness <= 0.5) color = 'orange';
|
||||||
|
else if (goodness <= 0.75) color = 'yellow';
|
||||||
|
else color = 'green';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column">
|
||||||
|
<Info
|
||||||
|
deltaType={deltaType}
|
||||||
|
indent={indent}
|
||||||
|
name={
|
||||||
|
'name' in props
|
||||||
|
? props.name
|
||||||
|
: strings.website.tools.tankopedia.characteristics.values[
|
||||||
|
props.value
|
||||||
|
]
|
||||||
|
}
|
||||||
|
delta={delta}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{uhWhatDoICallThisVariable}
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
{!noRanking && (
|
||||||
|
<Flex pl={indent ? '2' : '0'} align="center" gap="2">
|
||||||
|
<Progress
|
||||||
|
variant="soft"
|
||||||
|
size="1"
|
||||||
|
value={goodness * 100}
|
||||||
|
color={color}
|
||||||
|
style={{ height: '0.125rem', opacity: 0.5 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text color="gray" size="1">
|
||||||
|
{clamp(betterTanks.length + 1, 1, others.length)} /{' '}
|
||||||
|
{others.length}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
(a, b) =>
|
||||||
|
(typeof a.value === 'function' ? a.value(a.stats) : a.stats[a.value]) ===
|
||||||
|
(typeof b.value === 'function' ? b.value(b.stats) : b.stats[b.value]),
|
||||||
|
);
|
||||||
+1
-3
@@ -40,7 +40,7 @@ export function Shell({ index, stats }: ShellProps & StatsAcceptorProps) {
|
|||||||
<Box
|
<Box
|
||||||
left="50%"
|
left="50%"
|
||||||
position="absolute"
|
position="absolute"
|
||||||
ml="calc(25% + 2.5rem)"
|
ml="40%"
|
||||||
width="1.25rem"
|
width="1.25rem"
|
||||||
height="1.25rem"
|
height="1.25rem"
|
||||||
style={{
|
style={{
|
||||||
@@ -70,11 +70,9 @@ export function Shell({ index, stats }: ShellProps & StatsAcceptorProps) {
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
maskImage:
|
maskImage:
|
||||||
'url(/assets/images/tankopedia/visualizers/reload/shell.png)',
|
'url(/assets/images/tankopedia/visualizers/reload/shell.png)',
|
||||||
// maskComposite: 'exclude',
|
|
||||||
maskSize: 'contain',
|
maskSize: 'contain',
|
||||||
maskRepeat: 'no-repeat',
|
maskRepeat: 'no-repeat',
|
||||||
maskPosition: 'center',
|
maskPosition: 'center',
|
||||||
// background: 'red',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
+2
-2
@@ -13,8 +13,8 @@ export function ShellBorder(props: ComponentProps<'svg'>) {
|
|||||||
<path
|
<path
|
||||||
d="M149.917 7.5L151.5 9.91406V50.085L149.917 52.5H134.674C133.504 51.6657 132.116 51.1813 130.673 51.1133L130.319 51.1045H93.085C91.8919 51.1046 90.7173 51.3895 89.6582 51.9336L89.4482 52.0459L88.6494 52.4883C48.806 52.0806 22.9344 39.046 11.1416 30C22.9344 20.954 48.806 7.91846 88.6494 7.51074L89.4482 7.9541C90.5611 8.57122 91.8125 8.89543 93.085 8.89551H130.319C131.888 8.89551 133.409 8.40216 134.674 7.5H149.917Z"
|
d="M149.917 7.5L151.5 9.91406V50.085L149.917 52.5H134.674C133.504 51.6657 132.116 51.1813 130.673 51.1133L130.319 51.1045H93.085C91.8919 51.1046 90.7173 51.3895 89.6582 51.9336L89.4482 52.0459L88.6494 52.4883C48.806 52.0806 22.9344 39.046 11.1416 30C22.9344 20.954 48.806 7.91846 88.6494 7.51074L89.4482 7.9541C90.5611 8.57122 91.8125 8.89543 93.085 8.89551H130.319C131.888 8.89551 133.409 8.40216 134.674 7.5H149.917Z"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="15"
|
strokeWidth="15"
|
||||||
stroke-linejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
#reload-visualizer-flash {
|
||||||
|
background: transparent;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reload-visualizer-flash.go {
|
||||||
|
background: var(--white-a3);
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reload {
|
||||||
|
width: 65%;
|
||||||
|
height: 65%;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reload.go {
|
||||||
|
width: 70%;
|
||||||
|
height: 70%;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
+122
-59
@@ -1,12 +1,15 @@
|
|||||||
import { type Datum, type Serie } from '@nivo/line';
|
import { Box, Code } from '@radix-ui/themes';
|
||||||
import { Box, Card, Code } from '@radix-ui/themes';
|
|
||||||
import { times } from 'lodash-es';
|
import { times } from 'lodash-es';
|
||||||
import { Quicklime, type QuicklimeCallback } from 'quicklime';
|
import { Quicklime, type QuicklimeCallback } from 'quicklime';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { Var } from '../../../../../../../core/radix/var';
|
import { Var } from '../../../../../../../core/radix/var';
|
||||||
import type { StatsAcceptorProps } from '../TraverseVisualizer';
|
import type { StatsAcceptorProps } from '../TraverseVisualizer';
|
||||||
|
import { VisualizerCard } from '../VisualizerCard';
|
||||||
import { Shell } from './components/Shell';
|
import { Shell } from './components/Shell';
|
||||||
import { Target } from './components/Target';
|
import { Target } from './components/Target';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const PRECISION = 2;
|
||||||
|
|
||||||
export interface ReloadUpdateData {
|
export interface ReloadUpdateData {
|
||||||
reload: number;
|
reload: number;
|
||||||
@@ -15,30 +18,29 @@ export interface ReloadUpdateData {
|
|||||||
|
|
||||||
export const reloadUpdate = new Quicklime<ReloadUpdateData>();
|
export const reloadUpdate = new Quicklime<ReloadUpdateData>();
|
||||||
|
|
||||||
const T = 60;
|
|
||||||
|
|
||||||
export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||||
const reloadCircle = useRef<HTMLDivElement>(null);
|
const reloadCircle = useRef<HTMLDivElement>(null);
|
||||||
const reloadCore = useRef<HTMLDivElement>(null);
|
const reloadCore = useRef<HTMLDivElement>(null);
|
||||||
const reloadGlow = useRef<HTMLDivElement>(null);
|
const reloadGlow = useRef<HTMLDivElement>(null);
|
||||||
const dpmDisplay = useRef<HTMLSpanElement>(null);
|
const totalTime = useRef<HTMLSpanElement>(null);
|
||||||
|
const progressTime = useRef<HTMLSpanElement>(null);
|
||||||
|
const flash = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
/**
|
|
||||||
* 3-shell gun example:
|
|
||||||
*
|
|
||||||
* 0-1: clip reload
|
|
||||||
* ~~~: shell shot
|
|
||||||
* 1-2: intra-clip
|
|
||||||
* ~~~: shell shot
|
|
||||||
* 2-3: intra-clip
|
|
||||||
* ~~~: shell shot & reset to 0
|
|
||||||
*
|
|
||||||
* intra-clip time = intraClip * (n - 1)
|
|
||||||
*/
|
|
||||||
const state = useRef(0);
|
const state = useRef(0);
|
||||||
const dpm = useRef(0);
|
|
||||||
const shotPool = useRef(new Set<number>());
|
const shoot = useCallback(() => {
|
||||||
const data = useRef<Serie[]>([{ id: 'value', data: [] }]);
|
flash.current?.classList.add('go');
|
||||||
|
reloadCircle.current?.classList.add('go');
|
||||||
|
reloadGlow.current?.classList.add('go');
|
||||||
|
|
||||||
|
void flash.current?.offsetWidth;
|
||||||
|
void reloadCircle.current?.offsetWidth;
|
||||||
|
void reloadGlow.current?.offsetWidth;
|
||||||
|
|
||||||
|
flash.current?.classList.remove('go');
|
||||||
|
reloadCircle.current?.classList.remove('go');
|
||||||
|
reloadGlow.current?.classList.remove('go');
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancel = false;
|
let cancel = false;
|
||||||
@@ -46,40 +48,76 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
let lastState = state.current;
|
let lastState = state.current;
|
||||||
const shellArray = new Array(stats.shells);
|
const shellArray = new Array(stats.shells);
|
||||||
|
|
||||||
function shoot() {
|
|
||||||
shotPool.current.add(Date.now() / 1000);
|
|
||||||
dpm.current += stats.damage;
|
|
||||||
}
|
|
||||||
|
|
||||||
function frame() {
|
function frame() {
|
||||||
if (!dpmDisplay.current) return;
|
if (!totalTime.current || !progressTime.current) return;
|
||||||
|
|
||||||
|
const reloadThreshold = stats.shellReloads
|
||||||
|
? stats.shellReloads.length
|
||||||
|
: 1;
|
||||||
const now = Date.now() / 1000;
|
const now = Date.now() / 1000;
|
||||||
const dt = now - lastT;
|
const dt = now - lastT;
|
||||||
let reload;
|
let reload;
|
||||||
let shells;
|
let shells;
|
||||||
|
|
||||||
shotPool.current.forEach((t) => {
|
if (state.current < reloadThreshold) {
|
||||||
if (now - t > 60) shotPool.current.delete(t);
|
const shellReload =
|
||||||
});
|
reloadThreshold === 1
|
||||||
|
? stats.shellReload!
|
||||||
|
: stats.shellReloads![Math.floor(state.current)];
|
||||||
|
|
||||||
dpm.current = shotPool.current.size * stats.damage;
|
state.current = Math.min(
|
||||||
dpmDisplay.current.innerText = `${Math.round(dpm.current)}`;
|
reloadThreshold,
|
||||||
|
state.current + dt / shellReload,
|
||||||
if (state.current < 1) {
|
);
|
||||||
state.current = Math.min(1, state.current + dt / stats.shellReload!);
|
reload = state.current % 1;
|
||||||
reload = state.current;
|
|
||||||
shells = shellArray.fill(reload);
|
shells = shellArray.fill(reload);
|
||||||
|
|
||||||
if (state.current >= 1) shoot();
|
if (reloadThreshold > 1) {
|
||||||
} else if (state.current < stats.shells) {
|
shells = shells.map((_, index) =>
|
||||||
|
Math.max(0, state.current - shellArray.length + index + 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalTime.current.innerHTML = shellReload.toFixed(PRECISION);
|
||||||
|
progressTime.current.innerHTML = ((1 - reload) * shellReload).toFixed(
|
||||||
|
PRECISION,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (state.current >= reloadThreshold) shoot();
|
||||||
|
} else if (state.current < stats.shells + reloadThreshold - 1) {
|
||||||
|
let interval;
|
||||||
|
|
||||||
|
if (
|
||||||
|
stats.burstShells === 1 ||
|
||||||
|
Math.floor(state.current) % stats.burstShells === 0
|
||||||
|
) {
|
||||||
|
interval = stats.intraClip!;
|
||||||
|
} else {
|
||||||
|
interval = stats.burstInterShell!;
|
||||||
|
}
|
||||||
|
|
||||||
state.current = Math.min(
|
state.current = Math.min(
|
||||||
stats.shells,
|
stats.shells + reloadThreshold,
|
||||||
state.current + dt / stats.intraClip!,
|
state.current + dt / interval,
|
||||||
);
|
);
|
||||||
shells = times(stats.shells, (index) => index > state.current - 1);
|
|
||||||
reload = state.current % 1;
|
reload = state.current % 1;
|
||||||
|
|
||||||
|
if (reloadThreshold > 1) {
|
||||||
|
shells = shellArray.map((_, index) =>
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
Math.floor(index + 1 + reloadThreshold - state.current),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
shells = times(stats.shells, (index) => index > state.current - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalTime.current.innerHTML = interval.toFixed(PRECISION);
|
||||||
|
progressTime.current.innerHTML = ((1 - reload) * interval).toFixed(
|
||||||
|
PRECISION,
|
||||||
|
);
|
||||||
|
|
||||||
if (lastState % 1 > state.current % 1) shoot();
|
if (lastState % 1 > state.current % 1) shoot();
|
||||||
} else {
|
} else {
|
||||||
state.current = 0;
|
state.current = 0;
|
||||||
@@ -93,15 +131,6 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
|
|
||||||
lastT = now;
|
lastT = now;
|
||||||
lastState = state.current;
|
lastState = state.current;
|
||||||
|
|
||||||
(data.current[0].data as Datum[]).push({
|
|
||||||
x: now,
|
|
||||||
y: dpm.current / (stats.dpm + stats.damage),
|
|
||||||
});
|
|
||||||
(data.current[0].data as Datum[]).splice(
|
|
||||||
0,
|
|
||||||
data.current[0].data.findIndex((d) => now - (d.x as number) < T),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
frame();
|
frame();
|
||||||
@@ -133,7 +162,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="classic" mb="6" style={{ aspectRatio: '1 / 1' }}>
|
<VisualizerCard>
|
||||||
<Box position="absolute" top="0" left="0" width="100%" height="100%">
|
<Box position="absolute" top="0" left="0" width="100%" height="100%">
|
||||||
<Target />
|
<Target />
|
||||||
|
|
||||||
@@ -154,8 +183,6 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
position="absolute"
|
position="absolute"
|
||||||
top="50%"
|
top="50%"
|
||||||
left="50%"
|
left="50%"
|
||||||
width="12rem"
|
|
||||||
height="12rem"
|
|
||||||
className="reload"
|
className="reload"
|
||||||
ref={reloadGlow}
|
ref={reloadGlow}
|
||||||
style={{
|
style={{
|
||||||
@@ -170,8 +197,6 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
position="absolute"
|
position="absolute"
|
||||||
top="50%"
|
top="50%"
|
||||||
left="50%"
|
left="50%"
|
||||||
width="12rem"
|
|
||||||
height="12rem"
|
|
||||||
className="reload"
|
className="reload"
|
||||||
ref={reloadCircle}
|
ref={reloadCircle}
|
||||||
style={{
|
style={{
|
||||||
@@ -219,14 +244,52 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Code
|
||||||
|
ref={progressTime}
|
||||||
|
size="1"
|
||||||
|
variant="ghost"
|
||||||
|
weight="bold"
|
||||||
|
color="gray"
|
||||||
|
highContrast
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
right: 'calc(50% + var(--space-8))',
|
||||||
|
transform: 'translateX(50%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
0.00
|
||||||
|
</Code>
|
||||||
|
<Code
|
||||||
|
ref={totalTime}
|
||||||
|
size="1"
|
||||||
|
variant="ghost"
|
||||||
|
weight="bold"
|
||||||
|
color="gray"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: 'calc(50% + var(--space-8))',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
0.00
|
||||||
|
</Code>
|
||||||
|
|
||||||
{stats.shells > 1 &&
|
{stats.shells > 1 &&
|
||||||
times(stats.shells, (index) => (
|
times(stats.shells, (index) => (
|
||||||
<Shell stats={stats} index={index} key={index} />
|
<Shell stats={stats} index={index} key={index} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Box position="absolute" right="3" bottom="3">
|
<Box
|
||||||
<Code ref={dpmDisplay}>0 dpm</Code>
|
id="reload-visualizer-flash"
|
||||||
</Box>
|
ref={flash}
|
||||||
</Card>
|
position="absolute"
|
||||||
|
top="0"
|
||||||
|
left="0"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
/>
|
||||||
|
</VisualizerCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+2
-4
@@ -11,7 +11,7 @@ export function ShellSwitcher() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex>
|
<Flex>
|
||||||
{gun.gun_type!.value.base.shells.map((thisShell, shellIndex) => {
|
{gun.shells.map((thisShell, shellIndex) => {
|
||||||
const selected = thisShell.id === shell.id;
|
const selected = thisShell.id === shell.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -26,9 +26,7 @@ export function ShellSwitcher() {
|
|||||||
borderBottomLeftRadius: shellIndex === 0 ? '50%' : 0,
|
borderBottomLeftRadius: shellIndex === 0 ? '50%' : 0,
|
||||||
borderTopRightRadius: 0,
|
borderTopRightRadius: 0,
|
||||||
borderBottomRightRadius:
|
borderBottomRightRadius:
|
||||||
shellIndex === gun.gun_type!.value.base.shells.length - 1
|
shellIndex === gun.shells.length - 1 ? '50%' : 0,
|
||||||
? '50%'
|
|
||||||
: 0,
|
|
||||||
marginLeft: shellIndex === 0 ? 0 : -1,
|
marginLeft: shellIndex === 0 ? 0 : -1,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
+64
-100
@@ -1,12 +1,14 @@
|
|||||||
import { asset, isExplosive } from '@blitzkit/core';
|
import { asset, isExplosive } from '@blitzkit/core';
|
||||||
import { literals } from '@blitzkit/i18n';
|
import { literals } from '@blitzkit/i18n';
|
||||||
import { Box, Card, Code, Flex, Text } from '@radix-ui/themes';
|
import { Box, Flex, Text } from '@radix-ui/themes';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { clamp, degToRad, radToDeg } from 'three/src/math/MathUtils.js';
|
import { clamp, degToRad, radToDeg } from 'three/src/math/MathUtils.js';
|
||||||
import { Var } from '../../../../../../../core/radix/var';
|
import { Var } from '../../../../../../../core/radix/var';
|
||||||
import { useLocale } from '../../../../../../../hooks/useLocale';
|
import { useLocale } from '../../../../../../../hooks/useLocale';
|
||||||
import { Duel } from '../../../../../../../stores/duel';
|
import { Duel } from '../../../../../../../stores/duel';
|
||||||
import type { StatsAcceptorProps } from '../TraverseVisualizer';
|
import type { StatsAcceptorProps } from '../TraverseVisualizer';
|
||||||
|
import { VisualizerCard } from '../VisualizerCard';
|
||||||
|
import { VisualizerCornerStat } from '../VisualizerCornerStat';
|
||||||
import { ShellSwitcher } from './components/ShellSwitcher';
|
import { ShellSwitcher } from './components/ShellSwitcher';
|
||||||
|
|
||||||
export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
||||||
@@ -26,13 +28,9 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" mb="6" align="center">
|
<Flex direction="column" mb="6" align="center">
|
||||||
<Card
|
<VisualizerCard
|
||||||
variant="classic"
|
mb="0"
|
||||||
style={{
|
style={{ width: '100%' }}
|
||||||
width: '100%',
|
|
||||||
aspectRatio: '1 / 1',
|
|
||||||
touchAction: 'none',
|
|
||||||
}}
|
|
||||||
ref={container}
|
ref={container}
|
||||||
onPointerMove={(event) => {
|
onPointerMove={(event) => {
|
||||||
if (!container.current || explodes) return;
|
if (!container.current || explodes) return;
|
||||||
@@ -73,23 +71,23 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
position="absolute"
|
position="absolute"
|
||||||
style={{
|
style={{
|
||||||
background: `conic-gradient(
|
background: `conic-gradient(
|
||||||
at 0 50%,
|
at 0 50%,
|
||||||
|
|
||||||
${Var('ruby-a2')},
|
${Var('ruby-a2')},
|
||||||
${Var('ruby-a1')} ${Math.PI / 2 - ricochet}rad,
|
${Var('ruby-a1')} ${Math.PI / 2 - ricochet}rad,
|
||||||
|
|
||||||
${Var('jade-a2')} ${Math.PI / 2 - ricochet}rad,
|
${Var('jade-a2')} ${Math.PI / 2 - ricochet}rad,
|
||||||
${Var('jade-a4')} ${Math.PI / 2 - normalization}rad,
|
${Var('jade-a4')} ${Math.PI / 2 - normalization}rad,
|
||||||
|
|
||||||
${Var('cyan-a5')} ${Math.PI / 2 - normalization}rad,
|
${Var('cyan-a5')} ${Math.PI / 2 - normalization}rad,
|
||||||
${Var('cyan-a3')} ${Math.PI / 2 + normalization}rad,
|
${Var('cyan-a3')} ${Math.PI / 2 + normalization}rad,
|
||||||
|
|
||||||
${Var('jade-a2')} ${Math.PI / 2 + normalization}rad,
|
${Var('jade-a2')} ${Math.PI / 2 + normalization}rad,
|
||||||
${Var('jade-a4')} ${Math.PI / 2 + ricochet}rad,
|
${Var('jade-a4')} ${Math.PI / 2 + ricochet}rad,
|
||||||
|
|
||||||
${Var('ruby-a1')} ${Math.PI / 2 + ricochet}rad,
|
${Var('ruby-a1')} ${Math.PI / 2 + ricochet}rad,
|
||||||
${Var('ruby-a2')} 180deg
|
${Var('ruby-a2')} 180deg
|
||||||
`,
|
`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -160,85 +158,51 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Flex
|
<VisualizerCornerStat
|
||||||
style={{ userSelect: 'none' }}
|
label={strings.website.tools.tankopedia.visualizers.ricochet.angle}
|
||||||
gap="4"
|
value={literals(strings.common.units.deg, [
|
||||||
position="absolute"
|
radToDeg(Math.abs(angle)).toFixed(0),
|
||||||
top="0"
|
])}
|
||||||
left="0"
|
side="top-left"
|
||||||
p="3"
|
/>
|
||||||
width="100%"
|
<VisualizerCornerStat
|
||||||
justify="between"
|
label={
|
||||||
>
|
strings.website.tools.tankopedia.visualizers.ricochet.normalization
|
||||||
<Flex gap="2">
|
}
|
||||||
<Text color="gray" size="1">
|
value={
|
||||||
{strings.website.tools.tankopedia.visualizers.ricochet.angle}
|
doesRicochet
|
||||||
</Text>
|
? '-'
|
||||||
<Code size="1" variant="ghost">
|
: literals(strings.common.units.deg, [
|
||||||
{literals(strings.common.units.deg, [
|
radToDeg(Math.min(normalization, Math.abs(angle))).toFixed(0),
|
||||||
radToDeg(Math.abs(angle)).toFixed(0),
|
])
|
||||||
])}
|
}
|
||||||
</Code>
|
side="top-right"
|
||||||
</Flex>
|
/>
|
||||||
|
<VisualizerCornerStat
|
||||||
<Flex gap="2">
|
label={
|
||||||
<Text color="gray" size="1">
|
strings.website.tools.tankopedia.visualizers.ricochet
|
||||||
{
|
.effective_angle
|
||||||
strings.website.tools.tankopedia.visualizers.ricochet
|
}
|
||||||
.normalization
|
value={
|
||||||
}
|
doesRicochet
|
||||||
</Text>
|
? '-'
|
||||||
<Code size="1" variant="ghost">
|
: literals(strings.common.units.deg, [
|
||||||
{doesRicochet
|
radToDeg(Math.abs(effectiveAngle)).toFixed(0),
|
||||||
? '-'
|
])
|
||||||
: literals(strings.common.units.deg, [
|
}
|
||||||
radToDeg(Math.min(normalization, Math.abs(angle))).toFixed(
|
side="bottom-left"
|
||||||
0,
|
/>
|
||||||
),
|
<VisualizerCornerStat
|
||||||
])}
|
label={strings.website.tools.tankopedia.visualizers.ricochet.armor}
|
||||||
</Code>
|
value={
|
||||||
</Flex>
|
doesRicochet
|
||||||
</Flex>
|
? '-'
|
||||||
|
: literals(strings.common.units.percentage, [
|
||||||
<Flex
|
armorPercentage.toFixed(0),
|
||||||
style={{ userSelect: 'none' }}
|
])
|
||||||
gap="4"
|
}
|
||||||
position="absolute"
|
side="bottom-right"
|
||||||
bottom="0"
|
/>
|
||||||
left="0"
|
|
||||||
p="3"
|
|
||||||
width="100%"
|
|
||||||
justify="between"
|
|
||||||
>
|
|
||||||
<Flex gap="2">
|
|
||||||
<Text color="gray" size="1">
|
|
||||||
{
|
|
||||||
strings.website.tools.tankopedia.visualizers.ricochet
|
|
||||||
.effective_angle
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
<Code size="1" variant="ghost">
|
|
||||||
{doesRicochet
|
|
||||||
? '-'
|
|
||||||
: literals(strings.common.units.deg, [
|
|
||||||
radToDeg(Math.abs(effectiveAngle)).toFixed(0),
|
|
||||||
])}
|
|
||||||
</Code>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex gap="2">
|
|
||||||
<Text color="gray" size="1">
|
|
||||||
{strings.website.tools.tankopedia.visualizers.ricochet.armor}
|
|
||||||
</Text>
|
|
||||||
<Code size="1" variant="ghost">
|
|
||||||
{doesRicochet
|
|
||||||
? '-'
|
|
||||||
: literals(strings.common.units.percentage, [
|
|
||||||
armorPercentage.toFixed(0),
|
|
||||||
])}
|
|
||||||
</Code>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{explodes && (
|
{explodes && (
|
||||||
<Flex
|
<Flex
|
||||||
@@ -262,7 +226,7 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</VisualizerCard>
|
||||||
|
|
||||||
<ShellSwitcher />
|
<ShellSwitcher />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
+4
-3
@@ -1,8 +1,9 @@
|
|||||||
import { literals } from '@blitzkit/i18n';
|
import { literals } from '@blitzkit/i18n';
|
||||||
import { Card, Checkbox, Code, Flex, Tabs, Text } from '@radix-ui/themes';
|
import { Checkbox, Code, Flex, Tabs, Text } from '@radix-ui/themes';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import type { TankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
|
import type { TankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
|
||||||
import { useLocale } from '../../../../../../../hooks/useLocale';
|
import { useLocale } from '../../../../../../../hooks/useLocale';
|
||||||
|
import { VisualizerCard } from '../VisualizerCard';
|
||||||
|
|
||||||
export interface StatsAcceptorProps {
|
export interface StatsAcceptorProps {
|
||||||
stats: TankCharacteristics;
|
stats: TankCharacteristics;
|
||||||
@@ -58,7 +59,7 @@ export function TraverseVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
}, [terrain, turretSpeedDeg, hullSpeedDeg]);
|
}, [terrain, turretSpeedDeg, hullSpeedDeg]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="classic" style={{ aspectRatio: '1 / 1' }}>
|
<VisualizerCard>
|
||||||
<div
|
<div
|
||||||
ref={hull}
|
ref={hull}
|
||||||
style={{
|
style={{
|
||||||
@@ -188,6 +189,6 @@ export function TraverseVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</VisualizerCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import { Card, type CardProps } from '@radix-ui/themes';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
export const VisualizerCard = forwardRef<HTMLDivElement, CardProps>(
|
||||||
|
({ style, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
ref={ref}
|
||||||
|
variant="classic"
|
||||||
|
mt="4"
|
||||||
|
mb="6"
|
||||||
|
style={{ aspectRatio: '1 / 1', ...style }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
import { Code, Flex, Text } from '@radix-ui/themes';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
side: `${'top' | 'bottom'}-${'left' | 'right'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VisualizerCornerStat({ label, value, side }: Props) {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
gap="2"
|
||||||
|
position="absolute"
|
||||||
|
top={side === 'top-left' || side === 'top-right' ? '3' : undefined}
|
||||||
|
bottom={
|
||||||
|
side === 'bottom-left' || side === 'bottom-right' ? '3' : undefined
|
||||||
|
}
|
||||||
|
left={side === 'top-left' || side === 'bottom-left' ? '3' : undefined}
|
||||||
|
right={side === 'top-right' || side === 'bottom-right' ? '3' : undefined}
|
||||||
|
>
|
||||||
|
<Text color="gray" size="1">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Code size="1" variant="ghost">
|
||||||
|
{value}
|
||||||
|
</Code>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
+19
-14
@@ -3,11 +3,13 @@ import { useEffect } from 'react';
|
|||||||
import { awaitableEquipmentDefinitions } from '../../../../../core/awaitables/equipmentDefinitions';
|
import { awaitableEquipmentDefinitions } from '../../../../../core/awaitables/equipmentDefinitions';
|
||||||
import { awaitableProvisionDefinitions } from '../../../../../core/awaitables/provisionDefinitions';
|
import { awaitableProvisionDefinitions } from '../../../../../core/awaitables/provisionDefinitions';
|
||||||
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
|
||||||
|
import { modelTransformEvent } from '../../../../../core/blitzkit/modelTransform';
|
||||||
import { tankCharacteristics } from '../../../../../core/blitzkit/tankCharacteristics';
|
import { tankCharacteristics } from '../../../../../core/blitzkit/tankCharacteristics';
|
||||||
import { useEquipment } from '../../../../../hooks/useEquipment';
|
import { useEquipment } from '../../../../../hooks/useEquipment';
|
||||||
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
|
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
|
||||||
import { Duel } from '../../../../../stores/duel';
|
import { Duel } from '../../../../../stores/duel';
|
||||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||||
|
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps';
|
||||||
import { Crew } from './components/Crew';
|
import { Crew } from './components/Crew';
|
||||||
import { Firepower } from './components/Firepower';
|
import { Firepower } from './components/Firepower';
|
||||||
import { Maneuverability } from './components/Maneuverability';
|
import { Maneuverability } from './components/Maneuverability';
|
||||||
@@ -18,12 +20,15 @@ const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
|||||||
awaitableProvisionDefinitions,
|
awaitableProvisionDefinitions,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function Characteristics() {
|
export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const mutateDuel = Duel.useMutation();
|
const mutateDuel = Duel.useMutation();
|
||||||
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
|
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
|
||||||
const provisions = Duel.use((state) => state.protagonist.provisions);
|
const provisions = Duel.use((state) => state.protagonist.provisions);
|
||||||
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(
|
||||||
|
(state) => state.protagonist.assaultDistance,
|
||||||
|
);
|
||||||
const equipmentMatrix = Duel.use(
|
const equipmentMatrix = Duel.use(
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
(state) => state.protagonist.equipmentMatrix,
|
||||||
);
|
);
|
||||||
@@ -36,8 +41,7 @@ export function Characteristics() {
|
|||||||
const stockGun = stockTurret.guns[0];
|
const stockGun = stockTurret.guns[0];
|
||||||
const tankModelDefinition = useTankModelDefinition();
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
turretModelDefinition.guns[gun.gun_type!.value.base.id];
|
|
||||||
const stats = tankCharacteristics(
|
const stats = tankCharacteristics(
|
||||||
{
|
{
|
||||||
tank,
|
tank,
|
||||||
@@ -58,6 +62,7 @@ export function Characteristics() {
|
|||||||
applyReactiveArmor: false,
|
applyReactiveArmor: false,
|
||||||
applyDynamicArmor: false,
|
applyDynamicArmor: false,
|
||||||
applySpallLiner: false,
|
applySpallLiner: false,
|
||||||
|
assaultDistance,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tankModelDefinition,
|
tankModelDefinition,
|
||||||
@@ -70,16 +75,16 @@ export function Characteristics() {
|
|||||||
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mutateDuel((draft) => {
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
[draft.protagonist.pitch, draft.protagonist.yaw] = applyPitchYawLimits(
|
modelTransformEvent.last!.pitch,
|
||||||
draft.protagonist.pitch,
|
modelTransformEvent.last!.yaw,
|
||||||
draft.protagonist.yaw,
|
gunModelDefinition.pitch,
|
||||||
gunModelDefinition.pitch,
|
turretModelDefinition.yaw,
|
||||||
turretModelDefinition.yaw,
|
hasImprovedVerticalStabilizer,
|
||||||
hasImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
);
|
||||||
);
|
|
||||||
});
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}, [hasImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer]);
|
}, [hasImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,7 +94,7 @@ export function Characteristics() {
|
|||||||
align={{ initial: 'center', sm: 'start' }}
|
align={{ initial: 'center', sm: 'start' }}
|
||||||
width={{ initial: '100%', sm: 'auto' }}
|
width={{ initial: '100%', sm: 'auto' }}
|
||||||
>
|
>
|
||||||
<Firepower stats={stats} />
|
<Firepower skeleton={skeleton} stats={stats} />
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
gap="8"
|
gap="8"
|
||||||
|
|||||||
+15
-26
@@ -33,7 +33,7 @@ type ModuleDefinition =
|
|||||||
|
|
||||||
function extractModule(raw: ModuleDefinition | TankDefinition) {
|
function extractModule(raw: ModuleDefinition | TankDefinition) {
|
||||||
if ('gun_type' in raw) {
|
if ('gun_type' in raw) {
|
||||||
return raw.gun_type!.value.base;
|
return raw;
|
||||||
} else
|
} else
|
||||||
return raw as Exclude<ModuleDefinition | TankDefinition, GunDefinition>;
|
return raw as Exclude<ModuleDefinition | TankDefinition, GunDefinition>;
|
||||||
}
|
}
|
||||||
@@ -179,37 +179,31 @@ export function Modules() {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!draft.protagonist.turret.guns.some(
|
!draft.protagonist.turret.guns.some(
|
||||||
(gun) =>
|
(gun) => gun.id === draft.protagonist.gun.id,
|
||||||
gun.gun_type!.value.base.id ===
|
|
||||||
draft.protagonist.gun.gun_type!.value.base.id,
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
|
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
|
||||||
draft.protagonist.shell =
|
draft.protagonist.shell = draft.protagonist.gun.shells[0];
|
||||||
draft.protagonist.gun.gun_type!.value.base.shells[0];
|
|
||||||
}
|
}
|
||||||
} else if (unlock.type === ModuleType.GUN) {
|
} else if (unlock.type === ModuleType.GUN) {
|
||||||
const gunInTurret = draft.protagonist.turret.guns.find(
|
const gunInTurret = draft.protagonist.turret.guns.find(
|
||||||
(gun) => gun.gun_type!.value.base.id === unlock.id,
|
(gun) => gun.id === unlock.id,
|
||||||
);
|
);
|
||||||
if (gunInTurret) {
|
if (gunInTurret) {
|
||||||
draft.protagonist.gun = gunInTurret;
|
draft.protagonist.gun = gunInTurret;
|
||||||
draft.protagonist.shell = gunInTurret.gun_type!.value.base.shells[0];
|
draft.protagonist.shell = gunInTurret.shells[0];
|
||||||
} else {
|
} else {
|
||||||
// TODO: warn somehow?
|
// TODO: warn somehow?
|
||||||
const suitableTurret = draft.protagonist.tank.turrets.find((turret) =>
|
const suitableTurret = draft.protagonist.tank.turrets.find((turret) =>
|
||||||
turret.guns.some(
|
turret.guns.some((gun) => gun.id === unlock.id),
|
||||||
(gun) => gun.gun_type!.value.base.id === unlock.id,
|
|
||||||
),
|
|
||||||
)!;
|
)!;
|
||||||
const gunInSuitableTurret = suitableTurret.guns.find(
|
const gunInSuitableTurret = suitableTurret.guns.find(
|
||||||
(gun) => gun.gun_type!.value.base.id === unlock.id,
|
(gun) => gun.id === unlock.id,
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
draft.protagonist.turret = suitableTurret;
|
draft.protagonist.turret = suitableTurret;
|
||||||
draft.protagonist.gun = gunInSuitableTurret;
|
draft.protagonist.gun = gunInSuitableTurret;
|
||||||
draft.protagonist.shell =
|
draft.protagonist.shell = gunInSuitableTurret.shells[0];
|
||||||
gunInSuitableTurret.gun_type!.value.base.shells[0];
|
|
||||||
}
|
}
|
||||||
} else if (unlock.type === ModuleType.ENGINE) {
|
} else if (unlock.type === ModuleType.ENGINE) {
|
||||||
draft.protagonist.engine = draft.protagonist.tank.engines.find(
|
draft.protagonist.engine = draft.protagonist.tank.engines.find(
|
||||||
@@ -241,11 +235,9 @@ export function Modules() {
|
|||||||
} else if (unlock.type === ModuleType.GUN) {
|
} else if (unlock.type === ModuleType.GUN) {
|
||||||
module = tank.turrets
|
module = tank.turrets
|
||||||
.find((turret) =>
|
.find((turret) =>
|
||||||
turret.guns.some(
|
turret.guns.some((gun) => gun.id === unlock.id),
|
||||||
(gun) => gun.gun_type!.value.base.id === unlock.id,
|
|
||||||
),
|
|
||||||
)!
|
)!
|
||||||
.guns.find((gun) => gun.gun_type!.value.base.id === unlock.id)!;
|
.guns.find((gun) => gun.id === unlock.id)!;
|
||||||
} else if (unlock.type === ModuleType.VEHICLE) {
|
} else if (unlock.type === ModuleType.VEHICLE) {
|
||||||
module = tankDefinitions.tanks[unlock.id];
|
module = tankDefinitions.tanks[unlock.id];
|
||||||
}
|
}
|
||||||
@@ -308,7 +300,7 @@ export function Modules() {
|
|||||||
(unlock.type === ModuleType.TURRET
|
(unlock.type === ModuleType.TURRET
|
||||||
? turret.id
|
? turret.id
|
||||||
: unlock.type === ModuleType.GUN
|
: unlock.type === ModuleType.GUN
|
||||||
? gun.gun_type!.value.base.id
|
? gun.id
|
||||||
: unlock.type === ModuleType.ENGINE
|
: unlock.type === ModuleType.ENGINE
|
||||||
? engine.id
|
? engine.id
|
||||||
: unlock.type === ModuleType.TRACKS
|
: unlock.type === ModuleType.TRACKS
|
||||||
@@ -319,8 +311,7 @@ export function Modules() {
|
|||||||
unlock.type === ModuleType.TURRET
|
unlock.type === ModuleType.TURRET
|
||||||
? extractModule(module).id == topTurret.id
|
? extractModule(module).id == topTurret.id
|
||||||
: unlock.type === ModuleType.GUN
|
: unlock.type === ModuleType.GUN
|
||||||
? extractModule(module).id ===
|
? extractModule(module).id === topGun.id
|
||||||
topGun.gun_type!.value.base.id
|
|
||||||
: unlock.type === ModuleType.ENGINE
|
: unlock.type === ModuleType.ENGINE
|
||||||
? extractModule(module).id === topEngine.id
|
? extractModule(module).id === topEngine.id
|
||||||
: unlock.type === ModuleType.TRACKS
|
: unlock.type === ModuleType.TRACKS
|
||||||
@@ -371,8 +362,7 @@ export function Modules() {
|
|||||||
mutateDuel((draft) => {
|
mutateDuel((draft) => {
|
||||||
draft.protagonist.turret = draft.protagonist.tank.turrets[0];
|
draft.protagonist.turret = draft.protagonist.tank.turrets[0];
|
||||||
draft.protagonist.gun = draft.protagonist.turret.guns[0];
|
draft.protagonist.gun = draft.protagonist.turret.guns[0];
|
||||||
draft.protagonist.shell =
|
draft.protagonist.shell = draft.protagonist.gun.shells[0];
|
||||||
draft.protagonist.gun.gun_type!.value.base.shells[0];
|
|
||||||
draft.protagonist.engine = draft.protagonist.tank.engines[0];
|
draft.protagonist.engine = draft.protagonist.tank.engines[0];
|
||||||
draft.protagonist.track = draft.protagonist.tank.tracks[0];
|
draft.protagonist.track = draft.protagonist.tank.tracks[0];
|
||||||
});
|
});
|
||||||
@@ -387,8 +377,7 @@ export function Modules() {
|
|||||||
draft.protagonist.turret =
|
draft.protagonist.turret =
|
||||||
draft.protagonist.tank.turrets.at(-1)!;
|
draft.protagonist.tank.turrets.at(-1)!;
|
||||||
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
|
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
|
||||||
draft.protagonist.shell =
|
draft.protagonist.shell = draft.protagonist.gun.shells[0];
|
||||||
draft.protagonist.gun.gun_type!.value.base.shells[0];
|
|
||||||
draft.protagonist.engine =
|
draft.protagonist.engine =
|
||||||
draft.protagonist.tank.engines.at(-1)!;
|
draft.protagonist.tank.engines.at(-1)!;
|
||||||
draft.protagonist.track =
|
draft.protagonist.track =
|
||||||
@@ -427,7 +416,7 @@ export function Modules() {
|
|||||||
{tree(ModuleType.GUN, [
|
{tree(ModuleType.GUN, [
|
||||||
{
|
{
|
||||||
cost: { type: 'xp', value: 0 },
|
cost: { type: 'xp', value: 0 },
|
||||||
id: gun0.gun_type!.value.base.id,
|
id: gun0.id,
|
||||||
type: ModuleType.GUN,
|
type: ModuleType.GUN,
|
||||||
},
|
},
|
||||||
])}
|
])}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Flex } from '@radix-ui/themes';
|
import { Flex } from '@radix-ui/themes';
|
||||||
|
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
||||||
import { Characteristics } from './components/Characteristics';
|
import { Characteristics } from './components/Characteristics';
|
||||||
import { CompareOptions } from './components/Characteristics/components/CompareOptions';
|
import { CompareOptions } from './components/Characteristics/components/CompareOptions';
|
||||||
import { StatSearch } from './components/Characteristics/components/StatSearch';
|
import { StatSearch } from './components/Characteristics/components/StatSearch';
|
||||||
@@ -9,7 +10,9 @@ import { Modules } from './components/Modules';
|
|||||||
import { Provisions } from './components/Provisions';
|
import { Provisions } from './components/Provisions';
|
||||||
import { Skills } from './components/Skills';
|
import { Skills } from './components/Skills';
|
||||||
|
|
||||||
export function CharacteristicsSection() {
|
export function CharacteristicsSection({
|
||||||
|
skeleton,
|
||||||
|
}: MaybeSkeletonComponentProps) {
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="3">
|
<Flex direction="column" gap="3">
|
||||||
<Flex
|
<Flex
|
||||||
@@ -64,7 +67,7 @@ export function CharacteristicsSection() {
|
|||||||
<CompareOptions />
|
<CompareOptions />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Characteristics />
|
<Characteristics skeleton={skeleton} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { CaretRightIcon } from '@radix-ui/react-icons';
|
||||||
|
import { Box, Flex, Link, Text } from '@radix-ui/themes';
|
||||||
|
import { Var } from '../../../core/radix/var';
|
||||||
|
import type { TankGuide } from '../../../pages/[...locale]/tanks/[slug]/index.astro';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
guide: TankGuide;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GuideSection({ guide }: Props) {
|
||||||
|
return (
|
||||||
|
<Flex justify="center">
|
||||||
|
<Link
|
||||||
|
color="gray"
|
||||||
|
href={guide.url}
|
||||||
|
underline="none"
|
||||||
|
highContrast
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
maxWidth: '35rem',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: Var('space-4'),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex maxWidth="35rem" direction="column" gap="4">
|
||||||
|
<Flex direction="column" gap="4" position="relative">
|
||||||
|
<Text size="4" weight="medium">
|
||||||
|
<Flex width="100%" justify="start" align="center" gap="2">
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
width: '1.5em',
|
||||||
|
height: '1.5em',
|
||||||
|
objectFit: 'contain',
|
||||||
|
}}
|
||||||
|
src="https://guidesblitz.com/wp-content/uploads/2023/11/cropped-logo-1.png"
|
||||||
|
/>
|
||||||
|
GuidesBlitz
|
||||||
|
</Flex>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text size="2">{guide.description}</Text>
|
||||||
|
<img style={{ width: '100%' }} src={guide.image} />
|
||||||
|
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
bottom="0"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
left="0"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(${Var('gray-a1')}, ${Var('gray-1')})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Text color="purple">
|
||||||
|
<Flex width="100%" justify="center" align="center" gap="1">
|
||||||
|
Read the full guide
|
||||||
|
<CaretRightIcon width="1em" height="1em" />
|
||||||
|
</Flex>
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Link>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-1
@@ -90,7 +90,7 @@ export function CustomShellButton() {
|
|||||||
modified.normalization = undefined;
|
modified.normalization = undefined;
|
||||||
modified.ricochet = undefined;
|
modified.ricochet = undefined;
|
||||||
modified.explosion_radius =
|
modified.explosion_radius =
|
||||||
gun.gun_type!.value.base.shells.find(
|
gun.shells.find(
|
||||||
(shell) => shell.type === ShellType.HE,
|
(shell) => shell.type === ShellType.HE,
|
||||||
)?.explosion_radius ?? 1;
|
)?.explosion_radius ?? 1;
|
||||||
break;
|
break;
|
||||||
|
|||||||
+23
-36
@@ -1,5 +1,6 @@
|
|||||||
import { HeightIcon, WidthIcon } from '@radix-ui/react-icons';
|
import { HeightIcon, WidthIcon } from '@radix-ui/react-icons';
|
||||||
import { Flex, TextField } from '@radix-ui/themes';
|
import { Flex, TextField } from '@radix-ui/themes';
|
||||||
|
import type { QuicklimeEvent } from 'quicklime';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { degToRad, radToDeg } from 'three/src/math/MathUtils.js';
|
import { degToRad, radToDeg } from 'three/src/math/MathUtils.js';
|
||||||
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
@@ -20,39 +21,26 @@ export function QuickInputs() {
|
|||||||
const tankModelDefinition = useTankModelDefinition();
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
const turretModelDefinition =
|
const turretModelDefinition =
|
||||||
tankModelDefinition.turrets[protagonist.turret.id];
|
tankModelDefinition.turrets[protagonist.turret.id];
|
||||||
const gunModelDefinition =
|
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
||||||
turretModelDefinition.guns[protagonist.gun.gun_type!.value.base.id];
|
|
||||||
const initialGunPitch =
|
const initialGunPitch =
|
||||||
tankModelDefinition.initial_turret_rotation?.pitch ?? 0;
|
tankModelDefinition.initial_turret_rotation?.pitch ?? 0;
|
||||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||||
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!yawInput.current) return;
|
function handleTransformEvent(
|
||||||
|
event: QuicklimeEvent<ModelTransformEventData>,
|
||||||
yawInput.current.value = radToDeg(protagonist.yaw).toFixed(1);
|
) {
|
||||||
}, [protagonist.yaw]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!pitchInput.current) return;
|
|
||||||
|
|
||||||
pitchInput.current.value = (
|
|
||||||
-radToDeg(protagonist.pitch) + initialGunPitch
|
|
||||||
).toFixed(1);
|
|
||||||
}, [protagonist.pitch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function handleTransformEvent({ pitch, yaw }: ModelTransformEventData) {
|
|
||||||
if (!pitchInput.current || !yawInput.current) return;
|
if (!pitchInput.current || !yawInput.current) return;
|
||||||
|
|
||||||
pitchInput.current.value = (
|
pitchInput.current.value = (
|
||||||
-radToDeg(pitch) +
|
-radToDeg(event.data.pitch) +
|
||||||
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
|
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
|
||||||
).toFixed(1);
|
).toFixed(1);
|
||||||
|
|
||||||
if (yaw === undefined) return;
|
if (event.data.yaw === undefined) return;
|
||||||
|
|
||||||
yawInput.current.value = radToDeg(yaw).toFixed(1);
|
yawInput.current.value = radToDeg(event.data.yaw).toFixed(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
modelTransformEvent.on(handleTransformEvent);
|
modelTransformEvent.on(handleTransformEvent);
|
||||||
@@ -85,24 +73,27 @@ export function QuickInputs() {
|
|||||||
defaultValue="0.0"
|
defaultValue="0.0"
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
const value = Number(yawInput.current!.value);
|
const value = Number(yawInput.current!.value);
|
||||||
|
|
||||||
if (isNaN(value)) {
|
if (isNaN(value)) {
|
||||||
yawInput.current!.value = radToDeg(protagonist.yaw).toFixed(1);
|
yawInput.current!.value = radToDeg(
|
||||||
|
modelTransformEvent.last!.yaw,
|
||||||
|
).toFixed(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [pitch, yaw] = applyPitchYawLimits(
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
protagonist.pitch,
|
modelTransformEvent.last!.pitch,
|
||||||
degToRad(value),
|
degToRad(value),
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
|
||||||
mutateDuel((state) => {
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
state.protagonist.pitch = pitch;
|
yawInput.current!.value = radToDeg(
|
||||||
state.protagonist.yaw = yaw;
|
modelTransformEvent.last!.yaw,
|
||||||
});
|
).toFixed(1);
|
||||||
yawInput.current!.value = radToDeg(protagonist.yaw).toFixed(1);
|
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
@@ -128,25 +119,21 @@ export function QuickInputs() {
|
|||||||
const value = Number(pitchInput.current!.value);
|
const value = Number(pitchInput.current!.value);
|
||||||
if (isNaN(value)) {
|
if (isNaN(value)) {
|
||||||
pitchInput.current!.value = (
|
pitchInput.current!.value = (
|
||||||
-radToDeg(protagonist.pitch) + initialGunPitch
|
-radToDeg(modelTransformEvent.last!.pitch) + initialGunPitch
|
||||||
).toFixed(1);
|
).toFixed(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const [pitch, yaw] = applyPitchYawLimits(
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
degToRad(-value + initialGunPitch),
|
degToRad(-value + initialGunPitch),
|
||||||
protagonist.yaw,
|
modelTransformEvent.last!.yaw,
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
mutateDuel((state) => {
|
|
||||||
state.protagonist.pitch = pitch;
|
|
||||||
state.protagonist.yaw = yaw;
|
|
||||||
});
|
|
||||||
pitchInput.current!.value = (
|
pitchInput.current!.value = (
|
||||||
-radToDeg(protagonist.pitch) + initialGunPitch
|
-radToDeg(modelTransformEvent.last!.pitch) + initialGunPitch
|
||||||
).toFixed(1);
|
).toFixed(1);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
|
|||||||
+9
-45
@@ -15,7 +15,6 @@ import {
|
|||||||
GearIcon,
|
GearIcon,
|
||||||
} from '@radix-ui/react-icons';
|
} from '@radix-ui/react-icons';
|
||||||
import {
|
import {
|
||||||
Box,
|
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -36,10 +35,7 @@ import { useFullscreenAvailability } from '../../../../../hooks/useFullscreenAva
|
|||||||
import { useLocale } from '../../../../../hooks/useLocale';
|
import { useLocale } from '../../../../../hooks/useLocale';
|
||||||
import { App } from '../../../../../stores/app';
|
import { App } from '../../../../../stores/app';
|
||||||
import { Duel } from '../../../../../stores/duel';
|
import { Duel } from '../../../../../stores/duel';
|
||||||
import {
|
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||||
ShootingRangeZoom,
|
|
||||||
TankopediaEphemeral,
|
|
||||||
} from '../../../../../stores/tankopediaEphemeral';
|
|
||||||
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
|
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
|
||||||
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
|
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
|
||||||
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps';
|
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps';
|
||||||
@@ -97,23 +93,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
<>
|
<>
|
||||||
{disturbed && <QuickInputs />}
|
{disturbed && <QuickInputs />}
|
||||||
|
|
||||||
{display === TankopediaDisplay.ShootingRange && (
|
|
||||||
<Box
|
|
||||||
width="1rem"
|
|
||||||
height="1rem"
|
|
||||||
position="absolute"
|
|
||||||
top="50%"
|
|
||||||
left="50%"
|
|
||||||
style={{
|
|
||||||
zIndex: 2,
|
|
||||||
backgroundImage: 'url(/assets/images/icons/aim-caret.png)',
|
|
||||||
backgroundSize: 'contain',
|
|
||||||
transform: 'translateX(-50%)',
|
|
||||||
borderRadius: '50%',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Thicknesses skeleton={skeleton} thicknessRange={thicknessRange} />
|
<Thicknesses skeleton={skeleton} thicknessRange={thicknessRange} />
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
@@ -162,7 +141,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
mr="2"
|
mr="2"
|
||||||
mb="1"
|
mb="1"
|
||||||
>
|
>
|
||||||
{TIER_ROMAN_NUMERALS[antagonistGun.gun_type!.value.base.tier]}
|
{TIER_ROMAN_NUMERALS[antagonistGun.tier]}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
@@ -182,26 +161,20 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
{[...antagonistUniqueGuns].reverse().map(({ gun, turret }) => (
|
{[...antagonistUniqueGuns].reverse().map(({ gun, turret }) => (
|
||||||
<ModuleButton
|
<ModuleButton
|
||||||
module="gun"
|
module="gun"
|
||||||
key={gun.gun_type!.value.base.id}
|
key={gun.id}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
mutateDuel((draft) => {
|
mutateDuel((draft) => {
|
||||||
draft.antagonist.turret = turret;
|
draft.antagonist.turret = turret;
|
||||||
draft.antagonist.gun = gun;
|
draft.antagonist.gun = gun;
|
||||||
draft.antagonist.shell =
|
draft.antagonist.shell = gun.shells[0];
|
||||||
gun.gun_type!.value.base.shells[0];
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
selected={
|
selected={gun.id === antagonistGun.id}
|
||||||
gun.gun_type!.value.base.id ===
|
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
|
||||||
antagonistGun.gun_type!.value.base.id
|
|
||||||
}
|
|
||||||
discriminator={
|
|
||||||
TIER_ROMAN_NUMERALS[gun.gun_type!.value.base.tier]
|
|
||||||
}
|
|
||||||
secondaryDiscriminator={
|
secondaryDiscriminator={
|
||||||
<Text style={{ fontSize: '0.75em' }}>
|
<Text style={{ fontSize: '0.75em' }}>
|
||||||
{literals(strings.common.units.mm, [
|
{literals(strings.common.units.mm, [
|
||||||
gun.gun_type!.value.base.shells[0].caliber.toFixed(0),
|
gun.shells[0].caliber.toFixed(0),
|
||||||
])}
|
])}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
@@ -219,7 +192,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
}}
|
}}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
>
|
>
|
||||||
{antagonistGun.gun_type!.value.base.shells.map((thisShell) => (
|
{antagonistGun.shells.map((thisShell) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
color={
|
color={
|
||||||
thisShell.id === antagonistShell.id && !hasCustomShell
|
thisShell.id === antagonistShell.id && !hasCustomShell
|
||||||
@@ -388,8 +361,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
draft.antagonist.turret = tank.turrets.at(-1)!;
|
draft.antagonist.turret = tank.turrets.at(-1)!;
|
||||||
draft.antagonist.gun =
|
draft.antagonist.gun =
|
||||||
draft.antagonist.turret.guns.at(-1)!;
|
draft.antagonist.turret.guns.at(-1)!;
|
||||||
draft.antagonist.shell =
|
draft.antagonist.shell = draft.antagonist.gun.shells[0];
|
||||||
draft.antagonist.gun.gun_type!.value.base.shells[0];
|
|
||||||
});
|
});
|
||||||
setAntagonistSelectorOpen(false);
|
setAntagonistSelectorOpen(false);
|
||||||
}}
|
}}
|
||||||
@@ -408,7 +380,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
mutateTankopediaEphemeral((draft) => {
|
mutateTankopediaEphemeral((draft) => {
|
||||||
draft.disturbed = true;
|
draft.disturbed = true;
|
||||||
draft.display = Number(value);
|
draft.display = Number(value);
|
||||||
draft.shootingRangeZoom = ShootingRangeZoom.Arcade;
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -448,13 +419,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
{/* <SegmentedControl.Item value={`${TankopediaDisplay.ShootingRange}`}>
|
|
||||||
<Tooltip content="Shooting range">
|
|
||||||
<Flex height="100%" align="center">
|
|
||||||
<img src="/assets/images/icons/tankopedia-shooting-range.png" style={{ height: '1.25em' }} />
|
|
||||||
</Flex>
|
|
||||||
</Tooltip>
|
|
||||||
</SegmentedControl.Item> */}
|
|
||||||
</SegmentedControl.Root>
|
</SegmentedControl.Root>
|
||||||
|
|
||||||
{disturbed && (
|
{disturbed && (
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
import { Box, Progress } from '@radix-ui/themes';
|
||||||
|
import { useProgress } from '@react-three/drei';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
fontSize?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tracker({ fontSize }: Props) {
|
||||||
|
const { loaded, total } = useProgress();
|
||||||
|
const progress = total === 0 ? 0 : (loaded / total) * 100;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
left="50%"
|
||||||
|
bottom="0"
|
||||||
|
width="min(100%, 16rem)"
|
||||||
|
style={{
|
||||||
|
transform: `translate(-50%, calc(${fontSize} / 2))`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Progress color="gray" value={progress} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
+36
-21
@@ -1,13 +1,14 @@
|
|||||||
import { TankType } from '@blitzkit/core';
|
import { TankType } from '@blitzkit/core';
|
||||||
import { Box, Heading } from '@radix-ui/themes';
|
import { Flex, Heading } from '@radix-ui/themes';
|
||||||
import { Var } from '../../../../../core/radix/var';
|
import { Var } from '../../../../../../core/radix/var';
|
||||||
import { useLocale } from '../../../../../hooks/useLocale';
|
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||||
import { Duel } from '../../../../../stores/duel';
|
import { Duel } from '../../../../../../stores/duel';
|
||||||
import type { RadixColor } from '../../../../../stores/embedState';
|
import type { RadixColor } from '../../../../../../stores/embedState';
|
||||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||||
|
import { Tracker } from './components/Tracker';
|
||||||
|
|
||||||
interface TitleProps {
|
interface TitleProps {
|
||||||
outline?: boolean;
|
foreground?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NATION_COLORS: Record<
|
export const NATION_COLORS: Record<
|
||||||
@@ -65,7 +66,7 @@ export const NATION_COLORS: Record<
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Title({ outline }: TitleProps) {
|
export function Title({ foreground }: TitleProps) {
|
||||||
const { unwrap } = useLocale();
|
const { unwrap } = useLocale();
|
||||||
const protagonist = Duel.use((state) => state.protagonist.tank);
|
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||||
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
||||||
@@ -81,13 +82,32 @@ export function Title({ outline }: TitleProps) {
|
|||||||
: `${nationColors.text}-12`,
|
: `${nationColors.text}-12`,
|
||||||
);
|
);
|
||||||
const name = unwrap(protagonist.name);
|
const name = unwrap(protagonist.name);
|
||||||
|
const fontSize = revealed
|
||||||
|
? disturbed
|
||||||
|
? '2rem'
|
||||||
|
: `min(65vh, ${125 / name.length}vw)`
|
||||||
|
: `min(48vh, ${75 / name.length}vw)`;
|
||||||
|
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Flex
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
mutateTankopediaEphemeral((draft) => {
|
||||||
|
draft.disturbed = true;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
direction="column"
|
||||||
position="absolute"
|
position="absolute"
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
top={disturbed ? '6rem' : '50%'}
|
top={disturbed ? '6rem' : '50%'}
|
||||||
left="50%"
|
left="50%"
|
||||||
|
width="100%"
|
||||||
|
height={disturbed ? fontSize : '100%'}
|
||||||
style={{
|
style={{
|
||||||
|
pointerEvents: disturbed ? 'none' : undefined,
|
||||||
transitionDuration: '1s',
|
transitionDuration: '1s',
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
}}
|
}}
|
||||||
@@ -97,20 +117,13 @@ export function Title({ outline }: TitleProps) {
|
|||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
fontSize: revealed
|
fontSize,
|
||||||
? disturbed
|
|
||||||
? '2rem'
|
|
||||||
: `min(65vh, ${125 / name.length}vw)`
|
|
||||||
: `min(48vh, ${75 / name.length}vw)`,
|
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
color: outline ? Var(`${nationColors.tint}-a2`) : color,
|
color: foreground ? Var('gray-a10') : color,
|
||||||
opacity: outline && disturbed ? 0 : 1,
|
opacity: foreground && disturbed ? 0 : 1,
|
||||||
WebkitTextStroke: outline
|
|
||||||
? `${revealed ? (disturbed ? '1px' : 'min(2px, 0.2vw)') : 0} ${color}`
|
|
||||||
: undefined,
|
|
||||||
letterSpacing: disturbed || !revealed ? 0 : '-0.03em',
|
letterSpacing: disturbed || !revealed ? 0 : '-0.03em',
|
||||||
transition: `
|
transition: `
|
||||||
letter-spacing 1.5s ${disturbed ? '' : 'cubic-bezier(.81,-2,.68,1)'},
|
letter-spacing 1.5s ${disturbed ? '' : 'cubic-bezier(0.81, -2, 0.68, 1)'},
|
||||||
font-size 1s,
|
font-size 1s,
|
||||||
-webkit-text-stroke 2s,
|
-webkit-text-stroke 2s,
|
||||||
opacity 1s
|
opacity 1s
|
||||||
@@ -120,6 +133,8 @@ export function Title({ outline }: TitleProps) {
|
|||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</Heading>
|
</Heading>
|
||||||
</Box>
|
|
||||||
|
{!revealed && <Tracker fontSize={fontSize} />}
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+20
-141
@@ -1,23 +1,15 @@
|
|||||||
import { I_HAT, J_HAT } from '@blitzkit/core';
|
import { I_HAT, J_HAT } from '@blitzkit/core';
|
||||||
import { OrbitControls } from '@react-three/drei';
|
import { OrbitControls } from '@react-three/drei';
|
||||||
import { invalidate, useThree } from '@react-three/fiber';
|
import { invalidate, useFrame, useThree } from '@react-three/fiber';
|
||||||
import { clamp } from 'lodash-es';
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { PerspectiveCamera, Vector3 } from 'three';
|
import { PerspectiveCamera, Vector3 } from 'three';
|
||||||
import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
|
import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
|
||||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
|
||||||
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
|
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
|
||||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
||||||
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose';
|
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose';
|
||||||
import { Duel } from '../../../../../../stores/duel';
|
import { Duel } from '../../../../../../stores/duel';
|
||||||
import {
|
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||||
SHOOTING_RANGE_ZOOM_COEFFICIENTS,
|
|
||||||
ShootingRangeZoom,
|
|
||||||
TankopediaEphemeral,
|
|
||||||
} from '../../../../../../stores/tankopediaEphemeral';
|
|
||||||
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
|
|
||||||
import { aimTarget } from './SceneProps/constants';
|
|
||||||
|
|
||||||
const poseDistances: Record<Pose, number> = {
|
const poseDistances: Record<Pose, number> = {
|
||||||
[Pose.HullDown]: 15,
|
[Pose.HullDown]: 15,
|
||||||
@@ -27,11 +19,6 @@ const poseDistances: Record<Pose, number> = {
|
|||||||
|
|
||||||
const modelDefinitions = await awaitableModelDefinitions;
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
|
|
||||||
const ARCADE_MODE_DISTANCE = 19;
|
|
||||||
// const ARCADE_MODE_ANGLE = Math.PI / 8;
|
|
||||||
const ARCADE_MODE_ANGLE = degToRad(10);
|
|
||||||
const ARCADE_MODE_FOV = 54;
|
|
||||||
|
|
||||||
interface ControlsProps {
|
interface ControlsProps {
|
||||||
naked?: boolean;
|
naked?: boolean;
|
||||||
autoRotate?: boolean;
|
autoRotate?: boolean;
|
||||||
@@ -48,7 +35,6 @@ export function Controls({
|
|||||||
distanceScale = 1,
|
distanceScale = 1,
|
||||||
}: ControlsProps) {
|
}: ControlsProps) {
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||||
const display = TankopediaEphemeral.use((state) => state.display);
|
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||||
const camera = useThree((state) => state.camera as PerspectiveCamera);
|
const camera = useThree((state) => state.camera as PerspectiveCamera);
|
||||||
@@ -70,9 +56,7 @@ export function Controls({
|
|||||||
const antagonistTurretModelDefinition =
|
const antagonistTurretModelDefinition =
|
||||||
antagonistModelDefinition.turrets[antagonistTurret.id];
|
antagonistModelDefinition.turrets[antagonistTurret.id];
|
||||||
const protagonistGunModelDefinition =
|
const protagonistGunModelDefinition =
|
||||||
protagonistTurretModelDefinition.guns[
|
protagonistTurretModelDefinition.guns[protagonistGun.id];
|
||||||
protagonistGun.gun_type!.value.base.id
|
|
||||||
];
|
|
||||||
const protagonistHullOrigin = new Vector3(
|
const protagonistHullOrigin = new Vector3(
|
||||||
protagonistTrackModelDefinition.origin.x,
|
protagonistTrackModelDefinition.origin.x,
|
||||||
protagonistTrackModelDefinition.origin.y,
|
protagonistTrackModelDefinition.origin.y,
|
||||||
@@ -103,42 +87,21 @@ export function Controls({
|
|||||||
gunHeight + (naked ? 10 : 2),
|
gunHeight + (naked ? 10 : 2),
|
||||||
-13,
|
-13,
|
||||||
).multiplyScalar(distanceScale);
|
).multiplyScalar(distanceScale);
|
||||||
const protagonistGunOriginOnlyY = new Vector3(
|
|
||||||
0,
|
|
||||||
protagonistTurretModelDefinition.gun_origin.y,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
const shellOrigin = protagonistHullOrigin
|
|
||||||
.clone()
|
|
||||||
.add(protagonistTurretOrigin)
|
|
||||||
.add(protagonistGunOriginOnlyY);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const t0 = useRef(Date.now() / 1000);
|
||||||
if (!orbitControls.current || !doAutoRotate) return;
|
useFrame(() => {
|
||||||
|
if (!doAutoRotate) return;
|
||||||
|
|
||||||
const t0 = Date.now() / 1000;
|
const now = Date.now() / 1000;
|
||||||
let cancel = false;
|
const t = now - t0.current;
|
||||||
|
|
||||||
function frame() {
|
camera.position
|
||||||
if (!cancel) requestAnimationFrame(frame);
|
.copy(inspectModeInitialPosition)
|
||||||
|
.applyAxisAngle(I_HAT, (Math.PI / 32) * Math.sin(t / 9))
|
||||||
|
.applyAxisAngle(J_HAT, (-Math.PI / 16) * Math.sin(t / 7));
|
||||||
|
|
||||||
const now = Date.now() / 1000;
|
invalidate();
|
||||||
const t = now - t0;
|
});
|
||||||
|
|
||||||
camera.position
|
|
||||||
.copy(inspectModeInitialPosition)
|
|
||||||
.applyAxisAngle(I_HAT, (Math.PI / 32) * Math.sin(t / 9))
|
|
||||||
.applyAxisAngle(J_HAT, (-Math.PI / 16) * Math.sin(t / 7));
|
|
||||||
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
frame();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancel = true;
|
|
||||||
};
|
|
||||||
}, [doAutoRotate, camera]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe(
|
const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe(
|
||||||
@@ -252,38 +215,11 @@ export function Controls({
|
|||||||
}, [camera, protagonistTank.id, antagonistTank.id]);
|
}, [camera, protagonistTank.id, antagonistTank.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isInitialIteration = true;
|
|
||||||
|
|
||||||
function handleKeyDown(event: KeyboardEvent) {
|
|
||||||
if (display !== TankopediaDisplay.ShootingRange || event.repeat) return;
|
|
||||||
|
|
||||||
if (event.key === 'Shift') {
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
|
||||||
draft.shootingRangeZoom =
|
|
||||||
draft.shootingRangeZoom === ShootingRangeZoom.Arcade
|
|
||||||
? ShootingRangeZoom.Zoom0
|
|
||||||
: ShootingRangeZoom.Arcade;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleWheel(event: WheelEvent) {
|
function handleWheel(event: WheelEvent) {
|
||||||
if (display !== TankopediaDisplay.ShootingRange) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
|
||||||
draft.shootingRangeZoom = clamp(
|
|
||||||
draft.shootingRangeZoom - Math.sign(event.deltaY),
|
|
||||||
ShootingRangeZoom.Arcade,
|
|
||||||
ShootingRangeZoom.Zoom2,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleScroll(event: Event) {
|
function handleScroll(event: Event) {
|
||||||
if (display !== TankopediaDisplay.ShootingRange) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +231,6 @@ export function Controls({
|
|||||||
|
|
||||||
poseEvent.on(disturb);
|
poseEvent.on(disturb);
|
||||||
canvas.addEventListener('pointerdown', disturb);
|
canvas.addEventListener('pointerdown', disturb);
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
|
||||||
canvas.addEventListener('wheel', handleWheel);
|
canvas.addEventListener('wheel', handleWheel);
|
||||||
document.body.addEventListener('scroll', handleScroll);
|
document.body.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
@@ -304,80 +239,24 @@ export function Controls({
|
|||||||
|
|
||||||
orbitControls.current.rotateSpeed = 0.25;
|
orbitControls.current.rotateSpeed = 0.25;
|
||||||
|
|
||||||
if (display === TankopediaDisplay.ShootingRange) {
|
camera.position.copy(inspectModeInitialPosition);
|
||||||
const { shootingRangeZoom } = tankopediaEphemeralStore.getState();
|
orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0);
|
||||||
|
orbitControls.current.enablePan = true;
|
||||||
orbitControls.current.enablePan = false;
|
orbitControls.current.enableZoom = true;
|
||||||
orbitControls.current.enableZoom = false;
|
camera.fov = naked ? 20 : 25;
|
||||||
orbitControls.current.rotateSpeed *=
|
|
||||||
SHOOTING_RANGE_ZOOM_COEFFICIENTS[shootingRangeZoom];
|
|
||||||
camera.fov =
|
|
||||||
ARCADE_MODE_FOV * SHOOTING_RANGE_ZOOM_COEFFICIENTS[shootingRangeZoom];
|
|
||||||
|
|
||||||
if (shootingRangeZoom === ShootingRangeZoom.Arcade) {
|
|
||||||
orbitControls.current.target.set(0, gunHeight + 3, 0);
|
|
||||||
|
|
||||||
if (isInitialIteration) {
|
|
||||||
isInitialIteration = false;
|
|
||||||
camera.position.set(
|
|
||||||
0,
|
|
||||||
gunHeight + ARCADE_MODE_DISTANCE * Math.sin(ARCADE_MODE_ANGLE),
|
|
||||||
ARCADE_MODE_DISTANCE * Math.cos(ARCADE_MODE_ANGLE),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
camera.position
|
|
||||||
.copy(aimTarget)
|
|
||||||
.sub(orbitControls.current.target)
|
|
||||||
.normalize()
|
|
||||||
.multiplyScalar(-ARCADE_MODE_DISTANCE)
|
|
||||||
.add(orbitControls.current.target);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
orbitControls.current.target.copy(shellOrigin);
|
|
||||||
|
|
||||||
camera.position
|
|
||||||
.copy(aimTarget)
|
|
||||||
.sub(shellOrigin)
|
|
||||||
.normalize()
|
|
||||||
.multiplyScalar(-Number.EPSILON)
|
|
||||||
.add(shellOrigin);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
camera.position.copy(inspectModeInitialPosition);
|
|
||||||
orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0);
|
|
||||||
orbitControls.current.enablePan = true;
|
|
||||||
orbitControls.current.enableZoom = true;
|
|
||||||
camera.fov = naked ? 20 : 25;
|
|
||||||
}
|
|
||||||
|
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCamera();
|
updateCamera();
|
||||||
|
|
||||||
const unsubscribeDisplay = tankopediaEphemeralStore.subscribe(
|
|
||||||
(state) => state.display === TankopediaDisplay.ShootingRange,
|
|
||||||
() => {
|
|
||||||
disturb();
|
|
||||||
updateCamera();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const unsubscribeShootingRangeZoom = tankopediaEphemeralStore.subscribe(
|
|
||||||
(state) => state.shootingRangeZoom,
|
|
||||||
updateCamera,
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
canvas.removeEventListener('pointerdown', disturb);
|
canvas.removeEventListener('pointerdown', disturb);
|
||||||
poseEvent.off(disturb);
|
poseEvent.off(disturb);
|
||||||
unsubscribeDisplay();
|
|
||||||
unsubscribeShootingRangeZoom();
|
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
canvas.removeEventListener('wheel', handleWheel);
|
canvas.removeEventListener('wheel', handleWheel);
|
||||||
document.body.removeEventListener('scroll', handleScroll);
|
document.body.removeEventListener('scroll', handleScroll);
|
||||||
};
|
};
|
||||||
}, [display === TankopediaDisplay.ShootingRange]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OrbitControls
|
<OrbitControls
|
||||||
|
|||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||||
|
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
|
||||||
|
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
|
import { modelTransformEvent } from '../../../../../../core/blitzkit/modelTransform';
|
||||||
|
import { Duel } from '../../../../../../stores/duel';
|
||||||
|
|
||||||
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
|
|
||||||
|
export function InitialAligner() {
|
||||||
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
|
|
||||||
|
const tankModel = modelDefinitions.models[tank.id];
|
||||||
|
const turretModel = tankModel.turrets[turret.id];
|
||||||
|
const gunModel = turretModel.guns[gun.id];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
|
degToRad(8),
|
||||||
|
degToRad(25),
|
||||||
|
gunModel.pitch,
|
||||||
|
turretModel.yaw,
|
||||||
|
);
|
||||||
|
|
||||||
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
Loaded 100 of 129 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user