mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:50:45 +00:00
Render name layer with Pixi
This commit is contained in:
@@ -26,6 +26,7 @@ export default [
|
||||
allowDefaultProject: [
|
||||
"__mocks__/fileMock.js",
|
||||
"eslint.config.js",
|
||||
"scripts/build-namelayer-assets.mjs",
|
||||
"scripts/sync-assets.mjs",
|
||||
],
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"docs:map-generator": "cd map-generator && go doc -cmd -u -all",
|
||||
"tunnel": "npm run build-prod && npm run start:server",
|
||||
"test": "vitest run && vitest run tests/server",
|
||||
"build:namelayer-assets": "node scripts/build-namelayer-assets.mjs",
|
||||
"perf": "npx tsx tests/perf/run-all.ts",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"format": "prettier --ignore-unknown --write .",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,96 @@
|
||||
<?xml version="1.0"?>
|
||||
<font>
|
||||
<info face="namelayer_overpass" size="48" bold="0" italic="0"/>
|
||||
<common lineHeight="56" base="48" scaleW="1024" scaleH="384" pages="1" packed="0"/>
|
||||
<pages>
|
||||
<page id="0" file="namelayer_overpass.png"/>
|
||||
</pages>
|
||||
<chars count="86">
|
||||
<char id="65" x="0" y="0" width="64" height="64" page="0" xadvance="33" xoffset="0" yoffset="0"/>
|
||||
<char id="66" x="64" y="0" width="64" height="64" page="0" xadvance="33" xoffset="0" yoffset="0"/>
|
||||
<char id="67" x="128" y="0" width="64" height="64" page="0" xadvance="35" xoffset="0" yoffset="0"/>
|
||||
<char id="68" x="192" y="0" width="64" height="64" page="0" xadvance="35" xoffset="0" yoffset="0"/>
|
||||
<char id="69" x="256" y="0" width="64" height="64" page="0" xadvance="33" xoffset="0" yoffset="0"/>
|
||||
<char id="70" x="320" y="0" width="64" height="64" page="0" xadvance="30" xoffset="0" yoffset="0"/>
|
||||
<char id="71" x="384" y="0" width="64" height="64" page="0" xadvance="38" xoffset="0" yoffset="0"/>
|
||||
<char id="72" x="448" y="0" width="64" height="64" page="0" xadvance="35" xoffset="0" yoffset="0"/>
|
||||
<char id="73" x="512" y="0" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="74" x="576" y="0" width="64" height="64" page="0" xadvance="24" xoffset="0" yoffset="0"/>
|
||||
<char id="75" x="640" y="0" width="64" height="64" page="0" xadvance="33" xoffset="0" yoffset="0"/>
|
||||
<char id="76" x="704" y="0" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="77" x="768" y="0" width="64" height="64" page="0" xadvance="40" xoffset="0" yoffset="0"/>
|
||||
<char id="78" x="832" y="0" width="64" height="64" page="0" xadvance="35" xoffset="0" yoffset="0"/>
|
||||
<char id="79" x="896" y="0" width="64" height="64" page="0" xadvance="38" xoffset="0" yoffset="0"/>
|
||||
<char id="80" x="960" y="0" width="64" height="64" page="0" xadvance="33" xoffset="0" yoffset="0"/>
|
||||
<char id="81" x="0" y="64" width="64" height="64" page="0" xadvance="38" xoffset="0" yoffset="0"/>
|
||||
<char id="82" x="64" y="64" width="64" height="64" page="0" xadvance="35" xoffset="0" yoffset="0"/>
|
||||
<char id="83" x="128" y="64" width="64" height="64" page="0" xadvance="33" xoffset="0" yoffset="0"/>
|
||||
<char id="84" x="192" y="64" width="64" height="64" page="0" xadvance="30" xoffset="0" yoffset="0"/>
|
||||
<char id="85" x="256" y="64" width="64" height="64" page="0" xadvance="35" xoffset="0" yoffset="0"/>
|
||||
<char id="86" x="320" y="64" width="64" height="64" page="0" xadvance="33" xoffset="0" yoffset="0"/>
|
||||
<char id="87" x="384" y="64" width="64" height="64" page="0" xadvance="46" xoffset="0" yoffset="0"/>
|
||||
<char id="88" x="448" y="64" width="64" height="64" page="0" xadvance="33" xoffset="0" yoffset="0"/>
|
||||
<char id="89" x="512" y="64" width="64" height="64" page="0" xadvance="33" xoffset="0" yoffset="0"/>
|
||||
<char id="90" x="576" y="64" width="64" height="64" page="0" xadvance="30" xoffset="0" yoffset="0"/>
|
||||
<char id="97" x="640" y="64" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="98" x="704" y="64" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="99" x="768" y="64" width="64" height="64" page="0" xadvance="24" xoffset="0" yoffset="0"/>
|
||||
<char id="100" x="832" y="64" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="101" x="896" y="64" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="102" x="960" y="64" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="103" x="0" y="128" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="104" x="64" y="128" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="105" x="128" y="128" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="106" x="192" y="128" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="107" x="256" y="128" width="64" height="64" page="0" xadvance="24" xoffset="0" yoffset="0"/>
|
||||
<char id="108" x="320" y="128" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="109" x="384" y="128" width="64" height="64" page="0" xadvance="40" xoffset="0" yoffset="0"/>
|
||||
<char id="110" x="448" y="128" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="111" x="512" y="128" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="112" x="576" y="128" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="113" x="640" y="128" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="114" x="704" y="128" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="115" x="768" y="128" width="64" height="64" page="0" xadvance="24" xoffset="0" yoffset="0"/>
|
||||
<char id="116" x="832" y="128" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="117" x="896" y="128" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="118" x="960" y="128" width="64" height="64" page="0" xadvance="24" xoffset="0" yoffset="0"/>
|
||||
<char id="119" x="0" y="192" width="64" height="64" page="0" xadvance="35" xoffset="0" yoffset="0"/>
|
||||
<char id="120" x="64" y="192" width="64" height="64" page="0" xadvance="24" xoffset="0" yoffset="0"/>
|
||||
<char id="121" x="128" y="192" width="64" height="64" page="0" xadvance="24" xoffset="0" yoffset="0"/>
|
||||
<char id="122" x="192" y="192" width="64" height="64" page="0" xadvance="24" xoffset="0" yoffset="0"/>
|
||||
<char id="48" x="256" y="192" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="49" x="320" y="192" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="50" x="384" y="192" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="51" x="448" y="192" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="52" x="512" y="192" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="53" x="576" y="192" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="54" x="640" y="192" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="55" x="704" y="192" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="56" x="768" y="192" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="57" x="832" y="192" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="95" x="896" y="192" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="32" x="960" y="192" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="252" x="0" y="256" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="220" x="64" y="256" width="64" height="64" page="0" xadvance="35" xoffset="0" yoffset="0"/>
|
||||
<char id="46" x="128" y="256" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="91" x="192" y="256" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="93" x="256" y="256" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="43" x="320" y="256" width="64" height="64" page="0" xadvance="29" xoffset="0" yoffset="0"/>
|
||||
<char id="45" x="384" y="256" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="61" x="448" y="256" width="64" height="64" page="0" xadvance="29" xoffset="0" yoffset="0"/>
|
||||
<char id="40" x="512" y="256" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="41" x="576" y="256" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="44" x="640" y="256" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="39" x="704" y="256" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="58" x="768" y="256" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="33" x="832" y="256" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="63" x="896" y="256" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="47" x="960" y="256" width="64" height="64" page="0" xadvance="16" xoffset="0" yoffset="0"/>
|
||||
<char id="64" x="0" y="320" width="64" height="64" page="0" xadvance="49" xoffset="0" yoffset="0"/>
|
||||
<char id="35" x="64" y="320" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="36" x="128" y="320" width="64" height="64" page="0" xadvance="27" xoffset="0" yoffset="0"/>
|
||||
<char id="37" x="192" y="320" width="64" height="64" page="0" xadvance="43" xoffset="0" yoffset="0"/>
|
||||
<char id="38" x="256" y="320" width="64" height="64" page="0" xadvance="33" xoffset="0" yoffset="0"/>
|
||||
<char id="34" x="320" y="320" width="64" height="64" page="0" xadvance="18" xoffset="0" yoffset="0"/>
|
||||
</chars>
|
||||
</font>
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2021 The Overpass Project Authors (https://github.com/RedHatOfficial/Overpass)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,274 @@
|
||||
{
|
||||
"frames": {
|
||||
"AllianceIcon.svg": {
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 64,
|
||||
"h": 64
|
||||
}
|
||||
},
|
||||
"AllianceIconFaded.svg": {
|
||||
"frame": {
|
||||
"x": 64,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 64,
|
||||
"h": 64
|
||||
}
|
||||
},
|
||||
"AllianceRequestBlackIcon.svg": {
|
||||
"frame": {
|
||||
"x": 128,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 64,
|
||||
"h": 64
|
||||
}
|
||||
},
|
||||
"AllianceRequestWhiteIcon.svg": {
|
||||
"frame": {
|
||||
"x": 192,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 64,
|
||||
"h": 64
|
||||
}
|
||||
},
|
||||
"CrownIcon.svg": {
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 64,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 64,
|
||||
"h": 64
|
||||
}
|
||||
},
|
||||
"DisconnectedIcon.svg": {
|
||||
"frame": {
|
||||
"x": 64,
|
||||
"y": 64,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 64,
|
||||
"h": 64
|
||||
}
|
||||
},
|
||||
"EmbargoBlackIcon.svg": {
|
||||
"frame": {
|
||||
"x": 128,
|
||||
"y": 64,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 64,
|
||||
"h": 64
|
||||
}
|
||||
},
|
||||
"EmbargoWhiteIcon.svg": {
|
||||
"frame": {
|
||||
"x": 192,
|
||||
"y": 64,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 64,
|
||||
"h": 64
|
||||
}
|
||||
},
|
||||
"NukeIconRed.svg": {
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 128,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 64,
|
||||
"h": 64
|
||||
}
|
||||
},
|
||||
"NukeIconWhite.svg": {
|
||||
"frame": {
|
||||
"x": 64,
|
||||
"y": 128,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 64,
|
||||
"h": 64
|
||||
}
|
||||
},
|
||||
"QuestionMarkIcon.svg": {
|
||||
"frame": {
|
||||
"x": 128,
|
||||
"y": 128,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 64,
|
||||
"h": 64
|
||||
}
|
||||
},
|
||||
"TargetIcon.svg": {
|
||||
"frame": {
|
||||
"x": 192,
|
||||
"y": 128,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 64,
|
||||
"h": 64
|
||||
}
|
||||
},
|
||||
"TraitorIcon.svg": {
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 192,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 64
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 64,
|
||||
"h": 64
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"app": "scripts/build-namelayer-assets.mjs",
|
||||
"image": "namelayer-icons.png",
|
||||
"format": "RGBA8888",
|
||||
"size": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"scale": "1"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,351 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const fontsDir = path.join(root, "resources", "fonts");
|
||||
const imagesDir = path.join(root, "resources", "images");
|
||||
|
||||
const fontPng = "namelayer_overpass.png";
|
||||
const fontXml = "namelayer_overpass.xml";
|
||||
const fontFace = "namelayer_overpass";
|
||||
const fontSourceCandidates = [
|
||||
"overpass-regular.otf",
|
||||
"overpass-regular.ttf",
|
||||
"overpass.otf",
|
||||
"overpass.ttf",
|
||||
"overpass.woff",
|
||||
];
|
||||
const glyphs = Array.from(
|
||||
new Set(
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_ üÜ.[]+-=(),':!?/@#$%&\"".split(
|
||||
"",
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const iconSources = [
|
||||
"AllianceIcon.svg",
|
||||
"AllianceIconFaded.svg",
|
||||
"AllianceRequestBlackIcon.svg",
|
||||
"AllianceRequestWhiteIcon.svg",
|
||||
"CrownIcon.svg",
|
||||
"DisconnectedIcon.svg",
|
||||
"EmbargoBlackIcon.svg",
|
||||
"EmbargoWhiteIcon.svg",
|
||||
"NukeIconRed.svg",
|
||||
"NukeIconWhite.svg",
|
||||
"QuestionMarkIcon.svg",
|
||||
"TargetIcon.svg",
|
||||
"TraitorIcon.svg",
|
||||
];
|
||||
|
||||
fs.mkdirSync(fontsDir, { recursive: true });
|
||||
fs.mkdirSync(imagesDir, { recursive: true });
|
||||
|
||||
const canvasApi = await loadCanvasApi();
|
||||
|
||||
await buildBitmapFont();
|
||||
await buildIconAtlas();
|
||||
await buildEmojiAtlas();
|
||||
|
||||
async function loadCanvasApi() {
|
||||
try {
|
||||
const api = await import("canvas");
|
||||
const fontPath = fontSourceCandidates
|
||||
.map((fileName) => path.join(fontsDir, fileName))
|
||||
.find((candidate) => fs.existsSync(candidate));
|
||||
try {
|
||||
if (!fontPath) {
|
||||
throw new Error(
|
||||
`No Overpass font source found. Tried: ${fontSourceCandidates.join(
|
||||
", ",
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
api.registerFont(fontPath, {
|
||||
family: "OverpassNameLayer",
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"Could not register Overpass; using canvas fallback font",
|
||||
error,
|
||||
);
|
||||
}
|
||||
return api;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"canvas native bindings are unavailable; writing deterministic fallback NameLayer assets",
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function buildBitmapFont() {
|
||||
if (!canvasApi) {
|
||||
const fallbackXml = fs
|
||||
.readFileSync(path.join(fontsDir, "round_6x6_modified.xml"), "utf8")
|
||||
.replace(/face="round_6x6_modified"/g, `face="${fontFace}"`)
|
||||
.replace(/file="round_6x6_modified\.png"/g, `file="${fontPng}"`);
|
||||
fs.writeFileSync(
|
||||
path.join(fontsDir, fontPng),
|
||||
fs.readFileSync(path.join(fontsDir, "round_6x6_modified.png")),
|
||||
);
|
||||
fs.writeFileSync(path.join(fontsDir, fontXml), fallbackXml);
|
||||
return;
|
||||
}
|
||||
|
||||
const { createCanvas } = canvasApi;
|
||||
const cell = 64;
|
||||
const cols = 16;
|
||||
const rows = Math.ceil(glyphs.length / cols);
|
||||
const canvas = createCanvas(cols * cell, rows * cell);
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.textBaseline = "alphabetic";
|
||||
ctx.textAlign = "left";
|
||||
ctx.font = '48px "OverpassNameLayer", Arial, sans-serif';
|
||||
|
||||
const chars = [];
|
||||
glyphs.forEach((glyph, index) => {
|
||||
const col = index % cols;
|
||||
const row = Math.floor(index / cols);
|
||||
const x = col * cell;
|
||||
const y = row * cell;
|
||||
const metrics = ctx.measureText(glyph);
|
||||
const advance = glyph === " " ? 16 : Math.max(16, Math.ceil(metrics.width));
|
||||
const drawX = x + 4;
|
||||
const drawY = y + 48;
|
||||
if (glyph !== " ") {
|
||||
ctx.fillText(glyph, drawX, drawY);
|
||||
}
|
||||
chars.push({
|
||||
id: glyph.codePointAt(0),
|
||||
x,
|
||||
y,
|
||||
width: cell,
|
||||
height: cell,
|
||||
xadvance: advance,
|
||||
xoffset: 0,
|
||||
yoffset: 0,
|
||||
label: glyph,
|
||||
});
|
||||
});
|
||||
|
||||
const xml = `<?xml version="1.0"?>
|
||||
<font>
|
||||
<info face="${fontFace}" size="48" bold="0" italic="0"/>
|
||||
<common lineHeight="56" base="48" scaleW="${canvas.width}" scaleH="${canvas.height}" pages="1" packed="0"/>
|
||||
<pages>
|
||||
<page id="0" file="${fontPng}"/>
|
||||
</pages>
|
||||
<chars count="${chars.length}">
|
||||
${chars
|
||||
.map(
|
||||
(char) =>
|
||||
` <char id="${char.id}" x="${char.x}" y="${char.y}" width="${char.width}" height="${char.height}" page="0" xadvance="${char.xadvance}" xoffset="${char.xoffset}" yoffset="${char.yoffset}"/>`,
|
||||
)
|
||||
.join("\n")}
|
||||
</chars>
|
||||
</font>
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(fontsDir, fontPng), canvas.toBuffer("image/png"));
|
||||
fs.writeFileSync(path.join(fontsDir, fontXml), xml);
|
||||
}
|
||||
|
||||
async function buildIconAtlas() {
|
||||
if (!canvasApi) {
|
||||
writeFallbackAtlas("namelayer-icons", iconSources);
|
||||
return;
|
||||
}
|
||||
|
||||
const { createCanvas, loadImage } = canvasApi;
|
||||
const cell = 64;
|
||||
const cols = 4;
|
||||
const rows = Math.ceil(iconSources.length / cols);
|
||||
const canvas = createCanvas(cols * cell, rows * cell);
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
const frames = {};
|
||||
|
||||
for (let i = 0; i < iconSources.length; i++) {
|
||||
const source = iconSources[i];
|
||||
const col = i % cols;
|
||||
const row = Math.floor(i / cols);
|
||||
const x = col * cell;
|
||||
const y = row * cell;
|
||||
try {
|
||||
const img = await loadIconImage(path.join(imagesDir, source), loadImage);
|
||||
ctx.drawImage(img, x, y, cell, cell);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Could not pack ${source}; leaving empty atlas frame`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
frames[source] = {
|
||||
frame: { x, y, w: cell, h: cell },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: cell, h: cell },
|
||||
sourceSize: { w: cell, h: cell },
|
||||
};
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(imagesDir, "namelayer-icons.png"),
|
||||
canvas.toBuffer("image/png"),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(imagesDir, "namelayer-icons.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
frames,
|
||||
meta: {
|
||||
app: "scripts/build-namelayer-assets.mjs",
|
||||
image: "namelayer-icons.png",
|
||||
format: "RGBA8888",
|
||||
size: { w: canvas.width, h: canvas.height },
|
||||
scale: "1",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function loadIconImage(sourcePath, loadImage) {
|
||||
if (path.extname(sourcePath).toLowerCase() !== ".svg") {
|
||||
return loadImage(sourcePath);
|
||||
}
|
||||
|
||||
let svg = fs.readFileSync(sourcePath, "utf8");
|
||||
if (!/<svg[^>]*\swidth=/i.test(svg) || !/<svg[^>]*\sheight=/i.test(svg)) {
|
||||
const [, , , width, height] =
|
||||
svg.match(
|
||||
/viewBox=["']\s*([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s*["']/i,
|
||||
) ?? [];
|
||||
svg = svg.replace(
|
||||
/<svg\b/i,
|
||||
`<svg width="${width ?? 64}" height="${height ?? 64}"`,
|
||||
);
|
||||
}
|
||||
|
||||
return loadImage(
|
||||
`data:image/svg+xml;base64,${Buffer.from(svg, "utf8").toString("base64")}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function buildEmojiAtlas() {
|
||||
if (!canvasApi) {
|
||||
const emojis = readEmojiTable();
|
||||
writeFallbackAtlas("namelayer-emojis", emojis);
|
||||
return;
|
||||
}
|
||||
|
||||
const { createCanvas } = canvasApi;
|
||||
const emojis = readEmojiTable();
|
||||
const cell = 64;
|
||||
const cols = 8;
|
||||
const rows = Math.max(1, Math.ceil(emojis.length / cols));
|
||||
const canvas = createCanvas(cols * cell, rows * cell);
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.font =
|
||||
'48px "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", sans-serif';
|
||||
const frames = {};
|
||||
|
||||
emojis.forEach((emoji, index) => {
|
||||
const col = index % cols;
|
||||
const row = Math.floor(index / cols);
|
||||
const x = col * cell;
|
||||
const y = row * cell;
|
||||
ctx.fillText(emoji, x + cell / 2, y + cell / 2);
|
||||
frames[emoji] = {
|
||||
frame: { x, y, w: cell, h: cell },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: cell, h: cell },
|
||||
sourceSize: { w: cell, h: cell },
|
||||
};
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(imagesDir, "namelayer-emojis.png"),
|
||||
canvas.toBuffer("image/png"),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(imagesDir, "namelayer-emojis.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
frames,
|
||||
meta: {
|
||||
app: "scripts/build-namelayer-assets.mjs",
|
||||
image: "namelayer-emojis.png",
|
||||
format: "RGBA8888",
|
||||
size: { w: canvas.width, h: canvas.height },
|
||||
scale: "1",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function readEmojiTable() {
|
||||
const utilSource = fs.readFileSync(
|
||||
path.join(root, "src", "core", "Util.ts"),
|
||||
"utf8",
|
||||
);
|
||||
const tableSource = utilSource.match(
|
||||
/export const emojiTable = \[([\s\S]*?)\] as const;/,
|
||||
)?.[1];
|
||||
return tableSource
|
||||
? Array.from(tableSource.matchAll(/"([^"]+)"/g), (match) => match[1])
|
||||
: [];
|
||||
}
|
||||
|
||||
function writeFallbackAtlas(name, keys) {
|
||||
const transparentPng = Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAFgwJ/lD6N7wAAAABJRU5ErkJggg==",
|
||||
"base64",
|
||||
);
|
||||
const frames = {};
|
||||
|
||||
for (const key of keys) {
|
||||
frames[key] = {
|
||||
frame: { x: 0, y: 0, w: 1, h: 1 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 1, h: 1 },
|
||||
sourceSize: { w: 1, h: 1 },
|
||||
};
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(imagesDir, `${name}.png`), transparentPng);
|
||||
fs.writeFileSync(
|
||||
path.join(imagesDir, `${name}.json`),
|
||||
JSON.stringify(
|
||||
{
|
||||
frames,
|
||||
meta: {
|
||||
app: "scripts/build-namelayer-assets.mjs",
|
||||
image: `${name}.png`,
|
||||
format: "RGBA8888",
|
||||
size: { w: 1, h: 1 },
|
||||
scale: "1",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -341,6 +341,10 @@ export function updateAllianceProgressIconRefs(
|
||||
}
|
||||
|
||||
export function computeAllianceClipPath(fraction: number): string {
|
||||
const topCut = 20 + (1 - fraction) * 80 * 0.78; // min 20%, max 82.40%
|
||||
const topCut = computeAllianceTopCutPercent(fraction);
|
||||
return `inset(${topCut.toFixed(2)}% -2px 0 -2px)`;
|
||||
}
|
||||
|
||||
export function computeAllianceTopCutPercent(fraction: number): number {
|
||||
return 20 + (1 - fraction) * 80 * 0.78; // min 20%, max 82.40%
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,104 @@
|
||||
import * as PIXI from "pixi.js";
|
||||
import { assetUrl } from "../../../core/AssetUrls";
|
||||
|
||||
const nameLayerFont = assetUrl("fonts/namelayer_overpass.xml");
|
||||
const fallbackFont = assetUrl("fonts/round_6x6_modified.xml");
|
||||
const iconAtlas = assetUrl("images/namelayer-icons.json");
|
||||
const emojiAtlas = assetUrl("images/namelayer-emojis.json");
|
||||
|
||||
export const NAME_LAYER_FONT_FAMILY = "namelayer_overpass";
|
||||
export const NAME_LAYER_FALLBACK_FONT_FAMILY = "round_6x6_modified";
|
||||
|
||||
export class NameLayerAssets {
|
||||
public fontFamily = NAME_LAYER_FONT_FAMILY;
|
||||
|
||||
private readonly textures = new Map<string, PIXI.Texture | null>();
|
||||
private readonly pendingTextures = new Map<string, Promise<void>>();
|
||||
private readonly warnedTextureFailures = new Set<string>();
|
||||
private preloadPromise: Promise<void> | null = null;
|
||||
|
||||
preload(): Promise<void> {
|
||||
this.preloadPromise ??= this.loadBaseAssets();
|
||||
return this.preloadPromise;
|
||||
}
|
||||
|
||||
getTexture(src: string): PIXI.Texture | null {
|
||||
const cached = this.textures.get(src);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (!this.pendingTextures.has(src)) {
|
||||
this.pendingTextures.set(
|
||||
src,
|
||||
PIXI.Assets.load(src)
|
||||
.then((texture: PIXI.Texture) => {
|
||||
this.textures.set(src, texture);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.textures.set(src, null);
|
||||
this.warnTextureFailure(src, error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.pendingTextures.delete(src);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
preloadTextures(srcs: Iterable<string>): void {
|
||||
for (const src of srcs) {
|
||||
this.getTexture(src);
|
||||
}
|
||||
}
|
||||
|
||||
resetWarningsForTests(): void {
|
||||
this.warnedTextureFailures.clear();
|
||||
}
|
||||
|
||||
private async loadBaseAssets(): Promise<void> {
|
||||
await this.loadFont();
|
||||
await Promise.all([
|
||||
this.loadOptionalAtlas(iconAtlas, "static icon atlas"),
|
||||
this.loadOptionalAtlas(emojiAtlas, "emoji atlas"),
|
||||
]);
|
||||
}
|
||||
|
||||
private async loadFont(): Promise<void> {
|
||||
try {
|
||||
await PIXI.Assets.load(nameLayerFont);
|
||||
this.fontFamily = NAME_LAYER_FONT_FAMILY;
|
||||
return;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"NameLayer generated bitmap font unavailable; using fallback font",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await PIXI.Assets.load(fallbackFont);
|
||||
this.fontFamily = NAME_LAYER_FALLBACK_FONT_FAMILY;
|
||||
} catch (error) {
|
||||
console.error("NameLayer failed to load bitmap font", error);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadOptionalAtlas(src: string, label: string): Promise<void> {
|
||||
try {
|
||||
await PIXI.Assets.load(src);
|
||||
} catch (error) {
|
||||
console.warn(`NameLayer ${label} unavailable`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private warnTextureFailure(src: string, error: unknown): void {
|
||||
if (this.warnedTextureFailures.has(src)) {
|
||||
return;
|
||||
}
|
||||
this.warnedTextureFailures.add(src);
|
||||
console.warn(`NameLayer texture omitted after load failure: ${src}`, error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
export const NAME_LAYER_ICON_GAP = 4;
|
||||
export const NAME_LAYER_MAX_ZOOM_SCALE = 17;
|
||||
export const NAME_LAYER_TROOP_MARGIN_RATIO = -0.05;
|
||||
|
||||
export interface NameLayerVisibilityInput {
|
||||
isLayerVisible: boolean;
|
||||
transformScale: number;
|
||||
baseSize: number;
|
||||
isOnScreen: boolean;
|
||||
}
|
||||
|
||||
export interface NameLayerLayoutInput {
|
||||
fontSize: number;
|
||||
iconSize: number;
|
||||
iconCount: number;
|
||||
centeredIconCount: number;
|
||||
hasFlag: boolean;
|
||||
flagAspectRatio: number;
|
||||
nameWidth: number;
|
||||
troopWidth: number;
|
||||
}
|
||||
|
||||
export interface NameLayerLayout {
|
||||
flag: { x: number; y: number; width: number; height: number } | null;
|
||||
nameText: { x: number; y: number };
|
||||
troopText: { x: number; y: number };
|
||||
iconPositions: { x: number; y: number }[];
|
||||
centeredIconPositions: { x: number; y: number }[];
|
||||
height: number;
|
||||
width: number;
|
||||
rows: { iconsY: number | null; nameY: number; troopsY: number };
|
||||
}
|
||||
|
||||
const SUPPORTED_TEXT_CHARS = new Set(
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_ üÜ.[]+-=(),':!?/@#$%&\"".split(
|
||||
"",
|
||||
),
|
||||
);
|
||||
|
||||
const warnedUnsupportedGlyphs = new Set<string>();
|
||||
|
||||
export function computeNameLayerVisible({
|
||||
isLayerVisible,
|
||||
transformScale,
|
||||
baseSize,
|
||||
isOnScreen,
|
||||
}: NameLayerVisibilityInput): boolean {
|
||||
const size = transformScale * baseSize;
|
||||
return (
|
||||
isLayerVisible &&
|
||||
size >= 7 &&
|
||||
!(transformScale > NAME_LAYER_MAX_ZOOM_SCALE && size > 100) &&
|
||||
isOnScreen
|
||||
);
|
||||
}
|
||||
|
||||
export function computeNameLayerScale(baseSize: number): number {
|
||||
return Math.min(baseSize * 0.25, 3);
|
||||
}
|
||||
|
||||
export function computeNameLayerFontSize(baseSize: number): number {
|
||||
return Math.max(4, Math.floor(baseSize * 0.4));
|
||||
}
|
||||
|
||||
export function computeNameLayerLayout({
|
||||
fontSize,
|
||||
iconSize,
|
||||
iconCount,
|
||||
centeredIconCount,
|
||||
hasFlag,
|
||||
flagAspectRatio,
|
||||
nameWidth,
|
||||
troopWidth,
|
||||
}: NameLayerLayoutInput): NameLayerLayout {
|
||||
const visibleIconCount = Math.max(0, iconCount);
|
||||
const iconRowHeight = visibleIconCount > 0 ? iconSize : 0;
|
||||
const iconRowWidth =
|
||||
visibleIconCount > 0
|
||||
? visibleIconCount * iconSize +
|
||||
(visibleIconCount - 1) * NAME_LAYER_ICON_GAP
|
||||
: 0;
|
||||
const flagHeight = hasFlag ? fontSize : 0;
|
||||
const flagWidth = hasFlag ? Math.max(0, flagHeight * flagAspectRatio) : 0;
|
||||
const nameRowHeight = fontSize;
|
||||
const troopMargin = fontSize * NAME_LAYER_TROOP_MARGIN_RATIO;
|
||||
const troopHeight = fontSize;
|
||||
const nameRowWidth = flagWidth + nameWidth;
|
||||
const totalHeight = iconRowHeight + nameRowHeight + troopMargin + troopHeight;
|
||||
const width = Math.max(iconRowWidth, nameRowWidth, troopWidth);
|
||||
|
||||
let cursorY = -totalHeight / 2;
|
||||
const iconsY = visibleIconCount > 0 ? cursorY + iconRowHeight / 2 : null;
|
||||
cursorY += iconRowHeight;
|
||||
const nameY = cursorY + nameRowHeight / 2;
|
||||
cursorY += nameRowHeight + troopMargin;
|
||||
const troopsY = cursorY + troopHeight / 2;
|
||||
|
||||
const iconPositions: { x: number; y: number }[] = [];
|
||||
if (visibleIconCount > 0 && iconsY !== null) {
|
||||
const startX = -iconRowWidth / 2 + iconSize / 2;
|
||||
for (let i = 0; i < visibleIconCount; i++) {
|
||||
iconPositions.push({
|
||||
x: startX + i * (iconSize + NAME_LAYER_ICON_GAP),
|
||||
y: iconsY,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const nameStartX = -nameRowWidth / 2;
|
||||
const flag = hasFlag
|
||||
? {
|
||||
x: nameStartX + flagWidth / 2,
|
||||
y: nameY,
|
||||
width: flagWidth,
|
||||
height: flagHeight,
|
||||
}
|
||||
: null;
|
||||
const nameTextX = nameStartX + flagWidth + nameWidth / 2;
|
||||
const centeredIconPositions = Array.from(
|
||||
{ length: centeredIconCount },
|
||||
() => ({
|
||||
x: 0,
|
||||
y: nameY,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
flag,
|
||||
nameText: { x: nameTextX, y: nameY },
|
||||
troopText: { x: 0, y: troopsY },
|
||||
iconPositions,
|
||||
centeredIconPositions,
|
||||
height: totalHeight,
|
||||
width,
|
||||
rows: { iconsY, nameY, troopsY },
|
||||
};
|
||||
}
|
||||
|
||||
export function computeTraitorFlashDurationSeconds(
|
||||
remainingTicks: number,
|
||||
): number | null {
|
||||
const remainingSeconds = Math.round((remainingTicks / 10) * 2) / 2;
|
||||
if (remainingSeconds > 15) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const clampedSeconds = Math.max(0, Math.min(15, remainingSeconds));
|
||||
const normalizedTime = clampedSeconds / 15;
|
||||
const easedProgress = 1 - Math.pow(1 - normalizedTime, 3);
|
||||
return 0.2 + (1.0 - 0.2) * easedProgress;
|
||||
}
|
||||
|
||||
export function computeTraitorFlashAlpha(
|
||||
remainingTicks: number,
|
||||
nowMs: number,
|
||||
): number {
|
||||
const duration = computeTraitorFlashDurationSeconds(remainingTicks);
|
||||
if (duration === null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const durationMs = Math.max(1, duration * 1000);
|
||||
const phase = (nowMs % durationMs) / durationMs;
|
||||
const wave = phase < 0.5 ? phase / 0.5 : (1 - phase) / 0.5;
|
||||
const eased = 0.5 - Math.cos(wave * Math.PI) / 2;
|
||||
return 1 - eased * 0.7;
|
||||
}
|
||||
|
||||
export function replaceUnsupportedNameGlyphs(
|
||||
value: string,
|
||||
warn: (message: string) => void = console.warn,
|
||||
): string {
|
||||
let changed = false;
|
||||
let result = "";
|
||||
|
||||
for (const char of value) {
|
||||
if (SUPPORTED_TEXT_CHARS.has(char)) {
|
||||
result += char;
|
||||
continue;
|
||||
}
|
||||
|
||||
changed = true;
|
||||
result += "?";
|
||||
if (!warnedUnsupportedGlyphs.has(char)) {
|
||||
warnedUnsupportedGlyphs.add(char);
|
||||
warn(`NameLayer unsupported glyph replaced with ?: ${char}`);
|
||||
}
|
||||
}
|
||||
|
||||
return changed ? result : value;
|
||||
}
|
||||
|
||||
export function resetNameLayerGlyphWarningsForTests(): void {
|
||||
warnedUnsupportedGlyphs.clear();
|
||||
}
|
||||
+75
-1
@@ -1,4 +1,14 @@
|
||||
import { computeAllianceClipPath } from "../src/client/graphics/PlayerIcons";
|
||||
import {
|
||||
computeAllianceClipPath,
|
||||
computeAllianceTopCutPercent,
|
||||
} from "../src/client/graphics/PlayerIcons";
|
||||
import {
|
||||
computeNameLayerLayout,
|
||||
computeTraitorFlashAlpha,
|
||||
computeTraitorFlashDurationSeconds,
|
||||
replaceUnsupportedNameGlyphs,
|
||||
resetNameLayerGlyphWarningsForTests,
|
||||
} from "../src/client/graphics/layers/NameLayerLayout";
|
||||
|
||||
describe("PlayerIcons", () => {
|
||||
describe("computeAllianceClipPath", () => {
|
||||
@@ -37,5 +47,69 @@ describe("PlayerIcons", () => {
|
||||
expect(result).toContain("-2px");
|
||||
expect(result.match(/-2px/g)).toHaveLength(2); // Should appear twice (left and right)
|
||||
});
|
||||
|
||||
test("shares numeric top-cut helper with Pixi masks", () => {
|
||||
expect(computeAllianceTopCutPercent(1.0)).toBeCloseTo(20);
|
||||
expect(computeAllianceTopCutPercent(0.5)).toBeCloseTo(51.2);
|
||||
expect(computeAllianceTopCutPercent(0.0)).toBeCloseTo(82.4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("NameLayerLayout", () => {
|
||||
test("computes DOM-compatible local row positions with flag and icon gaps", () => {
|
||||
const layout = computeNameLayerLayout({
|
||||
fontSize: 10,
|
||||
iconSize: 15,
|
||||
iconCount: 2,
|
||||
centeredIconCount: 1,
|
||||
hasFlag: true,
|
||||
flagAspectRatio: 2,
|
||||
nameWidth: 40,
|
||||
troopWidth: 30,
|
||||
});
|
||||
|
||||
expect(layout.iconPositions).toEqual([
|
||||
{ x: -9.5, y: -9.75 },
|
||||
{ x: 9.5, y: -9.75 },
|
||||
]);
|
||||
expect(layout.flag).toEqual({ x: -20, y: 2.75, width: 20, height: 10 });
|
||||
expect(layout.nameText).toEqual({ x: 10, y: 2.75 });
|
||||
expect(layout.troopText).toEqual({ x: 0, y: 12.25 });
|
||||
expect(layout.centeredIconPositions).toEqual([{ x: 0, y: 2.75 }]);
|
||||
});
|
||||
|
||||
test("keeps no-flag names centered on the text width", () => {
|
||||
const layout = computeNameLayerLayout({
|
||||
fontSize: 12,
|
||||
iconSize: 18,
|
||||
iconCount: 0,
|
||||
centeredIconCount: 0,
|
||||
hasFlag: false,
|
||||
flagAspectRatio: 1,
|
||||
nameWidth: 60,
|
||||
troopWidth: 24,
|
||||
});
|
||||
|
||||
expect(layout.flag).toBeNull();
|
||||
expect(layout.nameText.x).toBe(0);
|
||||
expect(layout.width).toBe(60);
|
||||
});
|
||||
|
||||
test("matches traitor flash duration thresholds and alpha extrema", () => {
|
||||
expect(computeTraitorFlashDurationSeconds(156)).toBeNull();
|
||||
expect(computeTraitorFlashDurationSeconds(150)).toBeCloseTo(1);
|
||||
expect(computeTraitorFlashDurationSeconds(0)).toBeCloseTo(0.2);
|
||||
expect(computeTraitorFlashAlpha(150, 0)).toBeCloseTo(1);
|
||||
expect(computeTraitorFlashAlpha(150, 500)).toBeCloseTo(0.3);
|
||||
});
|
||||
|
||||
test("replaces unsupported glyphs once per glyph", () => {
|
||||
resetNameLayerGlyphWarningsForTests();
|
||||
const warn = vi.fn();
|
||||
|
||||
expect(replaceUnsupportedNameGlyphs("A🙂🙂B", warn)).toBe("A??B");
|
||||
expect(replaceUnsupportedNameGlyphs("🙂", warn)).toBe("?");
|
||||
expect(warn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user