Merge branch 'main' into keys-wrongly-displayed

This commit is contained in:
VariableVince
2026-05-06 20:21:33 +02:00
committed by GitHub
157 changed files with 15162 additions and 6846 deletions
-1
View File
@@ -138,7 +138,6 @@ jobs:
CDN_BASE: ${{ vars.CDN_BASE }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
API_KEY: ${{ secrets.API_KEY }}
SERVER_HOST_MASTERS: ${{ secrets.SERVER_HOST_MASTERS }}
SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }}
+1 -1
View File
@@ -13,4 +13,4 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: toshimaru/auto-author-assign@4d585cc37690897bd9015942ed6e766aa7cdb97f
- uses: toshimaru/auto-author-assign@bdd7688cbf9e6d5683f02f8c7d8ae4062a254b6d
-4
View File
@@ -71,7 +71,6 @@ jobs:
IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
API_KEY: ${{ secrets.API_KEY }}
SERVER_HOST_STAGING: ${{ secrets.SERVER_HOST_STAGING }}
SSH_KEY: ~/.ssh/id_rsa
@@ -122,7 +121,6 @@ jobs:
IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
API_KEY: ${{ secrets.API_KEY }}
SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }}
SSH_KEY: ~/.ssh/id_rsa
@@ -173,7 +171,6 @@ jobs:
IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
API_KEY: ${{ secrets.API_KEY }}
SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }}
SSH_KEY: ~/.ssh/id_rsa
@@ -224,7 +221,6 @@ jobs:
IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
API_KEY: ${{ secrets.API_KEY }}
SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }}
SSH_KEY: ~/.ssh/id_rsa
-1
View File
@@ -101,7 +101,6 @@ All new features and bug fixes should include relevant tests. We use **Vitest**.
## Submitting a Pull Request
1. **Commit your changes**:
- Write clear, concise commit messages.
- Use the present tense ("Add feature" not "Added feature").
-4
View File
@@ -181,7 +181,6 @@ Feel free to ask questions in the translation Discord server!
To ensure code quality and project stability, we use a progressive contribution system:
1. **New Contributors**: Limited to UI improvements and small bug fixes only
- This helps you become familiar with the codebase
- UI changes are easier to review and less likely to break core functionality
- Small, focused PRs have a higher chance of being accepted
@@ -193,20 +192,17 @@ To ensure code quality and project stability, we use a progressive contribution
### How to Contribute Successfully
1. **Before Starting Work**:
- Open an issue describing what you want to contribute
- Wait for maintainer feedback before investing significant time
- Small improvements can proceed directly to PR stage
2. **Code Quality Requirements**:
- All code must be well-commented and follow existing style patterns
- New features should not break existing functionality
- Code should be thoroughly tested before submission
- All code changes in src/core _MUST_ be tested.
3. **Pull Request Process**:
- Keep PRs focused on a single feature or bug fix
- Include screenshots for UI changes
- Describe what testing you've performed
-1
View File
@@ -134,7 +134,6 @@ ENV=$ENV
HOST=$HOST
GHCR_IMAGE=$GHCR_IMAGE
GHCR_TOKEN=$GHCR_TOKEN
TURNSTILE_SECRET_KEY=$TURNSTILE_SECRET_KEY
API_KEY=$API_KEY
DOMAIN=$DOMAIN
SUBDOMAIN=$SUBDOMAIN
+5
View File
@@ -238,6 +238,11 @@
class="hidden w-full h-full page-content relative z-50"
></troubleshooting-modal>
<clan-modal
id="page-clan"
inline
class="hidden w-full h-full page-content relative z-50"
></clan-modal>
<account-modal
id="page-account"
inline
Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 221 KiB

@@ -2,64 +2,114 @@
"name": "Bosphorus Straits",
"nations": [
{
"coordinates": [520, 300],
"name": "Istanbul",
"coordinates": [564, 245],
"name": "Beykoz",
"flag": "tr"
},
{
"coordinates": [360, 280],
"name": "Thrace",
"coordinates": [820, 209],
"name": "Sile",
"flag": "tr"
},
{
"coordinates": [220, 260],
"name": "Edirne",
"coordinates": [700, 316],
"name": "Çekmeköy",
"flag": "tr"
},
{
"coordinates": [650, 360],
"name": "Bursa",
"coordinates": [800, 438],
"name": "Pendik",
"flag": "tr"
},
{
"coordinates": [690, 290],
"name": "Izmit",
"coordinates": [797, 566],
"name": "Tuzla",
"flag": "tr"
},
{
"coordinates": [430, 430],
"name": "Canakkale",
"coordinates": [486, 381],
"name": "Üsküdar",
"flag": "tr"
},
{
"coordinates": [320, 330],
"name": "Tekirdag",
"coordinates": [534, 425],
"name": "Kadıköy",
"flag": "tr"
},
{
"coordinates": [610, 320],
"name": "Yalova",
"coordinates": [559, 568],
"name": "Adalar",
"flag": "tr"
},
{
"coordinates": [720, 260],
"coordinates": [635, 500],
"name": "Maltepe",
"flag": "tr"
},
{
"coordinates": [701, 423],
"name": "Sancaktepe",
"flag": "tr"
},
{
"coordinates": [424, 394],
"name": "Fatih",
"flag": "tr"
},
{
"coordinates": [128, 46],
"name": "Arnavutköy",
"flag": "tr"
},
{
"coordinates": [965, 544],
"name": "Kocaeli",
"flag": "tr"
},
{
"coordinates": [160, 120],
"name": "Varna",
"flag": "bg"
"coordinates": [42, 325],
"name": "Büyükçekmece",
"flag": "tr"
},
{
"coordinates": [220, 150],
"name": "Burgas",
"flag": "bg"
"coordinates": [336, 175],
"name": "Eyüpsultan",
"flag": "tr"
},
{
"coordinates": [820, 470],
"name": "Aegean Isles",
"flag": "gr"
"coordinates": [459, 157],
"name": "Sarıyer",
"flag": "tr"
},
{
"coordinates": [477, 297],
"name": "Beşiktaş",
"flag": "tr"
},
{
"coordinates": [171, 379],
"name": "Avcılar",
"flag": "tr"
},
{
"coordinates": [308, 412],
"name": "Bakırköy",
"flag": "tr"
},
{
"coordinates": [263, 283],
"name": "Başakşehir",
"flag": "tr"
},
{
"coordinates": [402, 272],
"name": "Sultangazi",
"flag": "tr"
},
{
"coordinates": [130, 270],
"name": "Esenyurt",
"flag": "tr"
}
]
}
@@ -3,7 +3,7 @@
"nations": [
{
"coordinates": [2260, 400],
"flag": "west_germany",
"flag": "de",
"name": "Jermaine"
},
{
@@ -13,7 +13,7 @@
},
{
"coordinates": [2050, 460],
"flag": "Fascist Spain",
"flag": "es",
"name": "Splain"
},
{
@@ -33,12 +33,12 @@
},
{
"coordinates": [2342, 436],
"flag": "Communist Romania",
"flag": "ro",
"name": "Rollandia"
},
{
"coordinates": [1222, 310],
"flag": "Ukrainian SSR",
"flag": "ua",
"name": "Ucryin"
},
{
@@ -63,7 +63,7 @@
},
{
"coordinates": [2555, 475],
"flag": "Georgian SSR",
"flag": "ge",
"name": "Georgia"
},
{
@@ -93,7 +93,7 @@
},
{
"coordinates": [1260, 400],
"flag": "Kazakh SSR",
"flag": "kz",
"name": "Azakah"
},
{
@@ -233,7 +233,7 @@
},
{
"coordinates": [2950, 1150],
"flag": "Apartheid South Africa",
"flag": "za",
"name": "Southern African State"
},
{
@@ -243,7 +243,7 @@
},
{
"coordinates": [2860, 1050],
"flag": "Apartheid South Africa",
"flag": "za",
"name": "Southern South West Africa"
},
{
+80 -70
View File
@@ -3,7 +3,7 @@
"nations": [
{
"coordinates": [634, 781],
"name": "Republic of Ireland",
"name": "Ireland",
"flag": "ie"
},
{
@@ -12,78 +12,78 @@
"flag": "gb-eng"
},
{
"coordinates": [935, 1289],
"name": "Kingdom of Spain",
"coordinates": [874, 1286],
"name": "Spain",
"flag": "es"
},
{
"coordinates": [1087, 931],
"name": "French Republic",
"coordinates": [1087, 983],
"name": "France",
"flag": "fr"
},
{
"coordinates": [1541, 1180],
"name": "Italian Republic",
"name": "Italy",
"flag": "it"
},
{
"coordinates": [1339, 983],
"name": "Swiss Confederation",
"coordinates": [1339, 997],
"name": "Switzerland",
"flag": "ch"
},
{
"coordinates": [1360, 428],
"name": "Kingdom of Norway",
"name": "Norway",
"flag": "no"
},
{
"coordinates": [1605, 573],
"name": "Kingdom of Sweden",
"coordinates": [1609, 477],
"name": "Sweden",
"flag": "se"
},
{
"coordinates": [2007, 309],
"name": "Republic of Finland",
"coordinates": [2032, 346],
"name": "Finland",
"flag": "fi"
},
{
"coordinates": [1200, 830],
"name": "Kingdom of Belgium",
"coordinates": [1182, 836],
"name": "Belgium",
"flag": "be"
},
{
"coordinates": [1264, 752],
"name": "Kingdom of the Netherlands",
"coordinates": [1268, 764],
"name": "Netherlands",
"flag": "nl"
},
{
"coordinates": [1443, 798],
"name": "Federal Republic of Germany",
"coordinates": [1445, 821],
"name": "Germany",
"flag": "de"
},
{
"coordinates": [1444, 969],
"name": "Republic of Austria",
"coordinates": [1507, 959],
"name": "Austria",
"flag": "at"
},
{
"coordinates": [1850, 810],
"name": "Republic of Poland",
"coordinates": [1751, 772],
"name": "Poland",
"flag": "pl"
},
{
"coordinates": [1630, 909],
"name": "Czech Republic",
"coordinates": [1618, 874],
"name": "Czechia",
"flag": "cz"
},
{
"coordinates": [2342, 936],
"coordinates": [2231, 885],
"name": "Ukraine",
"flag": "ua"
},
{
"coordinates": [2167, 708],
"name": "Republic of Belarus",
"coordinates": [2206, 676],
"name": "Belarus",
"flag": "by"
},
{
@@ -92,28 +92,28 @@
"flag": "ro"
},
{
"coordinates": [2432, 1265],
"name": "Republic of Turkiye",
"coordinates": [2334, 1313],
"name": "rkiye",
"flag": "tr"
},
{
"coordinates": [769, 1535],
"name": "Kingdom of Morocco",
"name": "Morocco",
"flag": "ma"
},
{
"coordinates": [2535, 720],
"name": "Russian Federation",
"coordinates": [2591, 610],
"name": "Russia",
"flag": "ru"
},
{
"coordinates": [2539, 1455],
"name": "Syrian Arab Republic",
"coordinates": [2538, 1418],
"name": "Syria",
"flag": "sy"
},
{
"coordinates": [2689, 1441],
"name": "Republic of Iraq",
"name": "Iraq",
"flag": "iq"
},
{
@@ -122,8 +122,8 @@
"flag": "ge"
},
{
"coordinates": [1389, 1473],
"name": "Republic of Tunisia",
"coordinates": [1359, 1473],
"name": "Tunisia",
"flag": "tn"
},
{
@@ -133,62 +133,62 @@
},
{
"coordinates": [680, 1254],
"name": "Portuguese Republic",
"name": "Portugal",
"flag": "pt"
},
{
"coordinates": [1891, 1299],
"name": "Hellenic Republic",
"name": "Greece",
"flag": "gr"
},
{
"coordinates": [1906, 1113],
"name": "Republic of Serbia",
"name": "Serbia",
"flag": "rs"
},
{
"coordinates": [1751, 983],
"name": "Republic of Hungary",
"name": "Hungary",
"flag": "hu"
},
{
"coordinates": [1784, 908],
"name": "Slovak Republic",
"coordinates": [1826, 902],
"name": "Slovakia",
"flag": "sk"
},
{
"coordinates": [1624, 1038],
"name": "Republic of Croatia",
"name": "Croatia",
"flag": "hr"
},
{
"coordinates": [1734, 1094],
"coordinates": [1738, 1107],
"name": "Bosnia and Herzegovina",
"flag": "ba"
},
{
"coordinates": [1817, 1213],
"name": "Republic of Albania",
"name": "Albania",
"flag": "al"
},
{
"coordinates": [2092, 1158],
"name": "Republic of Bulgaria",
"name": "Bulgaria",
"flag": "bg"
},
{
"coordinates": [1939, 702],
"name": "Republic of Lithuania",
"coordinates": [1958, 704],
"name": "Lithuania",
"flag": "lt"
},
{
"coordinates": [2014, 618],
"name": "Republic of Latvia",
"coordinates": [2030, 617],
"name": "Latvia",
"flag": "lv"
},
{
"coordinates": [2033, 504],
"name": "Republic of Estonia",
"coordinates": [2052, 516],
"name": "Estonia",
"flag": "ee"
},
{
@@ -197,14 +197,14 @@
"flag": "gb-wls"
},
{
"coordinates": [863, 573],
"coordinates": [841, 603],
"name": "Scotland",
"flag": "gb-sct"
},
{
"coordinates": [2688, 427],
"name": "USSR",
"flag": "ussr"
"coordinates": [2890, 857],
"name": "Kazakhstan",
"flag": "kz"
},
{
"coordinates": [719, 685],
@@ -212,44 +212,54 @@
"flag": "northern_ireland"
},
{
"coordinates": [2011, 103],
"name": "Polar Bears",
"flag": "polar_bears"
"coordinates": [1900, 132],
"name": "Sápmi",
"flag": "Sami flag"
},
{
"coordinates": [1369, 628],
"name": "Kingdom of Denmark",
"coordinates": [1377, 625],
"name": "Denmark",
"flag": "dk"
},
{
"coordinates": [2406, 1638],
"name": "State of Israel",
"name": "Israel",
"flag": "il"
},
{
"coordinates": [2226, 1661],
"name": "Arab Republic of Egypt",
"name": "Egypt",
"flag": "eg"
},
{
"coordinates": [1847, 1652],
"name": "State of Libya",
"name": "Libya",
"flag": "ly"
},
{
"coordinates": [2571, 1601],
"name": "Hashemite Kingdom of Jordan",
"coordinates": [2535, 1609],
"name": "Jordan",
"flag": "jo"
},
{
"coordinates": [2473, 1528],
"name": "Lebanese Republic",
"name": "Lebanon",
"flag": "lb"
},
{
"coordinates": [254, 274],
"coordinates": [266, 265],
"name": "Iceland",
"flag": "is"
},
{
"coordinates": [1045, 1188],
"name": "Andorra",
"flag": "ad"
},
{
"coordinates": [1290, 1121],
"name": "Monaco",
"flag": "mc"
}
]
}
@@ -12,17 +12,17 @@
"flag": "ie"
},
{
"coordinates": [650, 500],
"coordinates": [650, 477],
"name": "United Kingdom",
"flag": "gb"
},
{
"coordinates": [560, 800],
"coordinates": [612, 809],
"name": "Spain",
"flag": "es"
},
{
"coordinates": [726, 616],
"coordinates": [729, 648],
"name": "France",
"flag": "fr"
},
@@ -32,32 +32,32 @@
"flag": "it"
},
{
"coordinates": [872, 634],
"coordinates": [895, 641],
"name": "Switzerland",
"flag": "ch"
},
{
"coordinates": [960, 271],
"coordinates": [935, 259],
"name": "Norway",
"flag": "no"
},
{
"coordinates": [1095, 336],
"coordinates": [1105, 286],
"name": "Sweden",
"flag": "se"
},
{
"coordinates": [1403, 235],
"coordinates": [1438, 209],
"name": "Finland",
"flag": "fi"
},
{
"coordinates": [775, 541],
"coordinates": [819, 534],
"name": "Belgium",
"flag": "be"
},
{
"coordinates": [868, 487],
"coordinates": [868, 485],
"name": "Netherlands",
"flag": "nl"
},
@@ -72,13 +72,13 @@
"flag": "at"
},
{
"coordinates": [1120, 477],
"coordinates": [1220, 491],
"name": "Poland",
"flag": "pl"
},
{
"coordinates": [1060, 530],
"name": "Czech Republic",
"coordinates": [1078, 564],
"name": "Czechia",
"flag": "cz"
},
{
@@ -87,18 +87,18 @@
"flag": "ua"
},
{
"coordinates": [1500, 440],
"coordinates": [1517, 424],
"name": "Belarus",
"flag": "by"
},
{
"coordinates": [1400, 670],
"coordinates": [1414, 667],
"name": "Romania",
"flag": "ro"
},
{
"coordinates": [1580, 834],
"name": "Turkey",
"coordinates": [1614, 834],
"name": "Türkiye",
"flag": "tr"
},
{
@@ -107,13 +107,13 @@
"flag": "ma"
},
{
"coordinates": [1674, 449],
"coordinates": [1771, 413],
"name": "Russia",
"flag": "ru"
},
{
"coordinates": [1750, 950],
"name": "Syrian Arab Republic",
"name": "Syria",
"flag": "sy"
},
{
@@ -152,9 +152,39 @@
"flag": "rs"
},
{
"coordinates": [1200, 630],
"coordinates": [1218, 602],
"name": "Hungary",
"flag": "hu"
},
{
"coordinates": [1277, 90],
"name": "Sápmi",
"flag": "Sami flag"
},
{
"coordinates": [1406, 324],
"name": "Estonia",
"flag": "ee"
},
{
"coordinates": [1380, 384],
"name": "Latvia",
"flag": "lv"
},
{
"coordinates": [1355, 444],
"name": "Lithuania",
"flag": "lt"
},
{
"coordinates": [1121, 668],
"name": "Croatia",
"flag": "hr"
},
{
"coordinates": [959, 394],
"name": "Denmark",
"flag": "dk"
}
]
}
@@ -2,154 +2,139 @@
"name": "GatewayToTheAtlantic",
"nations": [
{
"coordinates": [2144, 344],
"coordinates": [2161, 420],
"name": "Swiss Confederation",
"flag": "ch"
},
{
"coordinates": [1964, 371],
"name": "Kingdom of Burgundy",
"coordinates": [1938, 263],
"name": "Duchy of Burgundy",
"flag": "burgundy"
},
{
"coordinates": [1334, 537],
"coordinates": [1388, 485],
"name": "Duchy of Aquitaine",
"flag": "aquitaine"
},
{
"coordinates": [2115, 684],
"coordinates": [2137, 636],
"name": "County of Provence",
"flag": "provence"
},
{
"coordinates": [1207, 763],
"name": "The Basque",
"flag": "es-pv"
"coordinates": [1266, 748],
"name": "Kingdom of Navarre",
"flag": "navarre"
},
{
"coordinates": [1281, 1142],
"coordinates": [1375, 1190],
"name": "Kingdom of Valencia",
"flag": "valencia"
},
{
"coordinates": [1660, 891],
"coordinates": [1696, 858],
"name": "Catalonia",
"flag": "catalonia"
},
{
"coordinates": [561, 764],
"coordinates": [543, 807],
"name": "Kingdom of Galicia",
"flag": "es-ga"
},
{
"coordinates": [1004, 1436],
"coordinates": [1128, 1388],
"name": "Emirate of Granada",
"flag": "granada"
},
{
"coordinates": [431, 1197],
"name": "Portuguese Republic",
"flag": "pt"
"coordinates": [523, 1156],
"name": "Kingdom of Portugal",
"flag": "kingdom_of_portugal"
},
{
"coordinates": [560, 1894],
"name": "Kingdom of Morocco",
"flag": "ma"
"coordinates": [851, 1805],
"name": "Marinid Sultanate",
"flag": "marinid"
},
{
"coordinates": [1609, 1837],
"name": "Algeria",
"flag": "dz"
"coordinates": [1424, 1725],
"name": "Zayyanid Sultanate",
"flag": "zayyanid"
},
{
"coordinates": [1733, 622],
"coordinates": [1604, 641],
"name": "County of Armagnac",
"flag": "armagnac"
},
{
"coordinates": [896, 1240],
"coordinates": [946, 1300],
"name": "City of Cordoba",
"flag": "cordoba"
},
{
"coordinates": [636, 1781],
"name": "City of Seville",
"flag": "seville"
},
{
"coordinates": [750, 873],
"coordinates": [740, 1021],
"name": "Kingdom of Leon",
"flag": "leon"
},
{
"coordinates": [1001, 882],
"name": "Kingdom of Castille",
"coordinates": [1040, 1036],
"name": "Crown of Castile",
"flag": "castille"
},
{
"coordinates": [775, 724],
"coordinates": [847, 767],
"name": "Principality of Asturias",
"flag": "asturias"
},
{
"coordinates": [1755, 1130],
"name": "The Old Ones",
"flag": "neuragic_empire"
"name": "Kingdom of Majorca",
"flag": "majorca"
},
{
"coordinates": [2097, 1670],
"name": "Tamazgha",
"flag": "Amazigh flag"
"coordinates": [2004, 1630],
"name": "Hafsid Sultanate",
"flag": "hafsid"
},
{
"coordinates": [979, 1013],
"name": "Kingdom of Spain",
"flag": "es"
"coordinates": [1374, 926],
"name": "Crown of Aragon",
"flag": "catalonia"
},
{
"coordinates": [468, 930],
"name": "Sardines",
"flag": "sardines"
},
{
"coordinates": [1667, 96],
"coordinates": [1695, 119],
"name": "City of Paris",
"flag": "paris"
},
{
"coordinates": [1716, 296],
"name": "Baguettes",
"flag": "baguette"
},
{
"coordinates": [1017, 180],
"coordinates": [1121, 221],
"name": "Kingdom of Brittany",
"flag": "brittany"
},
{
"coordinates": [2072, 567],
"name": "An Anti-Pope",
"flag": "antipope"
"coordinates": [1933, 614],
"name": "City of Avignon",
"flag": "avignon"
},
{
"coordinates": [1355, 76],
"coordinates": [1434, 129],
"name": "Duchy of Normandy",
"flag": "normandy"
},
{
"coordinates": [1402, 529],
"name": "Wine",
"flag": ""
"coordinates": [1644, 383],
"name": "Kingdom of France",
"flag": "Franks"
},
{
"coordinates": [1475, 1657],
"name": "French Foreign Legion",
"flag": "French foreign legion"
"coordinates": [772, 1399],
"name": "City of Seville",
"flag": "seville"
},
{
"coordinates": [1685, 417],
"name": "French Republic",
"flag": "fr"
"coordinates": [2147, 90],
"name": "Holy Roman Empire",
"flag": "Holy Roman Empire"
}
]
}
+291 -241
View File
@@ -3,488 +3,538 @@
"nations": [
{
"coordinates": [2309, 535],
"flag": "tr",
"name": "Türkiye"
"name": "Türkiye",
"flag": "tr"
},
{
"coordinates": [2030, 409],
"flag": "west_germany",
"name": "West Germany"
},
{
"coordinates": [2074, 382],
"flag": "east_germany",
"name": "East Germany"
"coordinates": [2050, 395],
"name": "Germany",
"flag": "de"
},
{
"coordinates": [1966, 442],
"flag": "fr",
"name": "France"
"name": "France",
"flag": "fr"
},
{
"coordinates": [1872, 528],
"flag": "Fascist Spain",
"name": "Spain"
"name": "Spain",
"flag": "es"
},
{
"coordinates": [2074, 498],
"flag": "it",
"name": "Italy"
"name": "Italy",
"flag": "it"
},
{
"coordinates": [1912, 379],
"flag": "gb",
"name": "United Kingdom"
"name": "United Kingdom",
"flag": "gb"
},
{
"coordinates": [1841, 373],
"flag": "ie",
"name": "Ireland"
"name": "Ireland",
"flag": "ie"
},
{
"coordinates": [2153, 378],
"flag": "pl",
"name": "Poland"
"name": "Poland",
"flag": "pl"
},
{
"coordinates": [2178, 539],
"flag": "gr",
"name": "Greece"
"name": "Greece",
"flag": "gr"
},
{
"coordinates": [2222, 493],
"flag": "bg",
"name": "Bulgaria"
"name": "Bulgaria",
"flag": "bg"
},
{
"coordinates": [2135, 481],
"flag": "yugoslavia",
"name": "Yugoslavia"
"name": "Serbia",
"flag": "rs"
},
{
"coordinates": [2242, 461],
"flag": "Communist Romania",
"name": "Romania"
"name": "Romania",
"flag": "ro"
},
{
"coordinates": [2163, 441],
"flag": "hu",
"name": "Hungary"
"name": "Hungary",
"flag": "hu"
},
{
"coordinates": [2272, 418],
"flag": "Ukrainian SSR",
"name": "Ukrainian SSR"
"name": "Ukraine",
"flag": "ua"
},
{
"coordinates": [2093, 297],
"flag": "se",
"name": "Sweden"
"name": "Sweden",
"flag": "se"
},
{
"coordinates": [2027, 282],
"flag": "no",
"name": "Norway"
"name": "Norway",
"flag": "no"
},
{
"coordinates": [2191, 194],
"flag": "Sami flag",
"name": "Sapmi"
"name": "Sápmi",
"flag": "Sami flag"
},
{
"coordinates": [2206, 262],
"flag": "fi",
"name": "Finland"
"name": "Finland",
"flag": "fi"
},
{
"coordinates": [2376, 363],
"flag": "Russian SSR",
"name": "Russian SSR"
"name": "Russia",
"flag": "ru"
},
{
"coordinates": [2222, 371],
"flag": "Byelorussian SSR",
"name": "Byelorussian SSR"
"name": "Belarus",
"flag": "by"
},
{
"coordinates": [2441, 507],
"flag": "Georgian SSR",
"name": "Georgian SSR"
"name": "Georgia",
"flag": "ge"
},
{
"coordinates": [2402, 580],
"flag": "Second Republic of Iraq",
"name": "Iraq"
"name": "Iraq",
"flag": "iq"
},
{
"coordinates": [2353, 595],
"flag": "sy",
"name": "Syria"
"name": "Syria",
"flag": "sy"
},
{
"coordinates": [2414, 679],
"flag": "sa",
"name": "Saudi Arabia"
"name": "Saudi Arabia",
"flag": "sa"
},
{
"coordinates": [2434, 815],
"flag": "North yemen",
"name": "North Yemen"
},
{
"coordinates": [2479, 824],
"flag": "south yemen",
"name": "South Yemen"
"coordinates": [2456, 820],
"name": "Yemen",
"flag": "ye"
},
{
"coordinates": [2554, 724],
"flag": "ae",
"name": "United Arab Emirates"
"name": "United Arab Emirates",
"flag": "ae"
},
{
"coordinates": [2532, 609],
"flag": "Pahlavi Iran",
"name": "Iran"
"name": "Iran",
"flag": "ir"
},
{
"coordinates": [2683, 650],
"flag": "pk",
"name": "Pakistan"
"name": "Pakistan",
"flag": "pk"
},
{
"coordinates": [2654, 580],
"flag": "af",
"name": "Afghanistan"
"name": "Afghanistan",
"flag": "af"
},
{
"coordinates": [2727, 416],
"flag": "Kazakh SSR",
"name": "Kazakh SSR"
"name": "Kazakhstan",
"flag": "kz"
},
{
"coordinates": [2556, 544],
"flag": "Turkmen SSR",
"name": "Turkmen SSR"
"name": "Turkmenistan",
"flag": "tm"
},
{
"coordinates": [2947, 362],
"flag": "Zheleznogorsk",
"name": "Zheleznogorsk"
},
{
"coordinates": [3252, 229],
"flag": "Siberia",
"name": "Siberia"
"coordinates": [3255, 278],
"name": "Siberia",
"flag": "Siberia"
},
{
"coordinates": [2810, 744],
"flag": "in",
"name": "India"
"name": "India",
"flag": "in"
},
{
"coordinates": [1717, 237],
"flag": "is",
"name": "Iceland"
"name": "Iceland",
"flag": "is"
},
{
"coordinates": [2944, 709],
"flag": "bd",
"name": "Bangladesh"
"name": "Bangladesh",
"flag": "bd"
},
{
"coordinates": [2868, 635],
"flag": "np",
"name": "Nepal"
"coordinates": [2870, 656],
"name": "Nepal",
"flag": "np"
},
{
"coordinates": [3254, 672],
"flag": "cn",
"name": "China"
"coordinates": [3172, 624],
"name": "China",
"flag": "cn"
},
{
"coordinates": [3373, 521],
"flag": "kp",
"name": "North Korea"
"name": "North Korea",
"flag": "kp"
},
{
"coordinates": [3389, 573],
"flag": "kr",
"name": "South Korea"
"name": "South Korea",
"flag": "kr"
},
{
"coordinates": [3515, 571],
"flag": "jp",
"name": "Japan"
"name": "Japan",
"flag": "jp"
},
{
"coordinates": [3026, 457],
"flag": "mn",
"name": "Mongolia"
"name": "Mongolia",
"flag": "mn"
},
{
"coordinates": [3229, 995],
"flag": "id",
"name": "Indonesia"
"name": "Indonesia",
"flag": "id"
},
{
"coordinates": [3121, 755],
"flag": "vn",
"name": "North Vietnam"
"coordinates": [3145, 802],
"name": "Vietnam",
"flag": "vn"
},
{
"coordinates": [3153, 833],
"flag": "South Vietnam",
"name": "South Vietnam"
"coordinates": [3020, 750],
"name": "Myanmar",
"flag": "mm"
},
{
"coordinates": [3013, 722],
"flag": "Burma2",
"name": "Burma"
},
{
"coordinates": [3095, 822],
"flag": "kh",
"name": "Cambodia"
"coordinates": [3082, 822],
"name": "Thailand",
"flag": "th"
},
{
"coordinates": [3538, 1067],
"flag": "pg",
"name": "Papua New Guinea"
"name": "Papua New Guinea",
"flag": "pg"
},
{
"coordinates": [3542, 1356],
"flag": "au",
"name": "Australia"
"coordinates": [3570, 1392],
"name": "Australia",
"flag": "au"
},
{
"coordinates": [3422, 1203],
"flag": "Australian Aboriginal Flag",
"name": "Nawan-mirri"
"coordinates": [3416, 1213],
"name": "Nawan-mirri",
"flag": "Australian Aboriginal Flag"
},
{
"coordinates": [3880, 1521],
"flag": "nz",
"name": "New Zealand"
"name": "New Zealand",
"flag": "nz"
},
{
"coordinates": [2632, 1893],
"flag": "aq",
"name": "Antarctica"
"name": "Antarctica",
"flag": "aq"
},
{
"coordinates": [2038, 590],
"flag": "tn",
"name": "Tunisia"
"name": "Tunisia",
"flag": "tn"
},
{
"coordinates": [2116, 653],
"flag": "ly",
"name": "Libya"
"coordinates": [2138, 675],
"name": "Libya",
"flag": "ly"
},
{
"coordinates": [2281, 653],
"flag": "United Arab Republic",
"name": "United Arab Republic"
"name": "Egypt",
"flag": "eg"
},
{
"coordinates": [1859, 613],
"flag": "ma",
"name": "Morocco"
"name": "Morocco",
"flag": "ma"
},
{
"coordinates": [1943, 615],
"flag": "dz",
"name": "Algeria"
"coordinates": [1952, 671],
"name": "Algeria",
"flag": "dz"
},
{
"coordinates": [2317, 754],
"flag": "sd",
"name": "Sudan"
"coordinates": [2271, 788],
"name": "Sudan",
"flag": "sd"
},
{
"coordinates": [2466, 918],
"flag": "so",
"name": "Somalia"
"name": "Somalia",
"flag": "so"
},
{
"coordinates": [2352, 895],
"flag": "Imperial Ethiopia",
"name": "Ethiopia"
"name": "Ethiopia",
"flag": "et"
},
{
"coordinates": [1790, 729],
"flag": "Mauritania",
"name": "Mauritania"
"name": "Mauritania",
"flag": "mr"
},
{
"coordinates": [2154, 764],
"flag": "td",
"name": "Chad"
"coordinates": [2118, 768],
"name": "Chad",
"flag": "td"
},
{
"coordinates": [2051, 745],
"flag": "ne",
"name": "Niger"
"coordinates": [2009, 810],
"name": "Niger",
"flag": "ne"
},
{
"coordinates": [2040, 930],
"flag": "ng",
"name": "Nigeria"
"coordinates": [1996, 909],
"name": "Nigeria",
"flag": "ng"
},
{
"coordinates": [1805, 907],
"flag": "lr",
"name": "Liberia"
"name": "Liberia",
"flag": "lr"
},
{
"coordinates": [2195, 918],
"flag": "cf",
"name": "Central African Republic"
"name": "Central African Republic",
"flag": "cf"
},
{
"coordinates": [2197, 1070],
"flag": "Zaire",
"name": "Zaire"
"name": "DR Congo",
"flag": "cd"
},
{
"coordinates": [2189, 1372],
"flag": "Apartheid South Africa",
"name": "South Africa"
"coordinates": [2211, 1389],
"name": "South Africa",
"flag": "za"
},
{
"coordinates": [2452, 1247],
"flag": "mg",
"name": "Madagascar"
"name": "Madagascar",
"flag": "mg"
},
{
"coordinates": [2356, 1165],
"flag": "mz",
"name": "Mozambique"
"name": "Mozambique",
"flag": "mz"
},
{
"coordinates": [2368, 1032],
"flag": "tz",
"name": "Tanzania"
"name": "Tanzania",
"flag": "tz"
},
{
"coordinates": [1934, 762],
"flag": "ml",
"name": "Mali"
"coordinates": [1894, 789],
"name": "Mali",
"flag": "ml"
},
{
"coordinates": [2128, 1292],
"flag": "Apartheid South Africa",
"name": "South West Africa"
"name": "Namibia",
"flag": "na"
},
{
"coordinates": [2099, 1178],
"flag": "ao",
"name": "Angola"
"name": "Angola",
"flag": "ao"
},
{
"coordinates": [1375, 1121],
"flag": "br",
"name": "Brazil"
"coordinates": [1418, 1167],
"name": "Brazil",
"flag": "br"
},
{
"coordinates": [1203, 1059],
"flag": "amazonas",
"name": "Amazonas"
"coordinates": [1225, 1065],
"name": "Amazonas",
"flag": "amazonas"
},
{
"coordinates": [1210, 1395],
"flag": "ar",
"name": "Argentina"
"coordinates": [1243, 1428],
"name": "Argentina",
"flag": "ar"
},
{
"coordinates": [1107, 1419],
"flag": "cl",
"name": "Chile"
"coordinates": [1125, 1356],
"name": "Chile",
"flag": "cl"
},
{
"coordinates": [1064, 1114],
"flag": "pe",
"name": "Peru"
"coordinates": [1064, 1111],
"name": "Peru",
"flag": "pe"
},
{
"coordinates": [1065, 938],
"flag": "co",
"name": "Colombia"
"coordinates": [1083, 938],
"name": "Colombia",
"flag": "co"
},
{
"coordinates": [1192, 938],
"flag": "ve",
"name": "Venezuela"
"coordinates": [1183, 921],
"name": "Venezuela",
"flag": "ve"
},
{
"coordinates": [913, 833],
"flag": "ni",
"name": "Nicaragua"
"name": "Nicaragua",
"flag": "ni"
},
{
"coordinates": [788, 744],
"flag": "mx",
"name": "Mexico"
"coordinates": [764, 743],
"name": "Mexico",
"flag": "mx"
},
{
"coordinates": [1011, 555],
"flag": "us",
"name": "USA"
"coordinates": [1034, 556],
"name": "United States",
"flag": "us"
},
{
"coordinates": [800, 624],
"flag": "Texas",
"name": "Texas"
"coordinates": [766, 623],
"name": "Texas",
"flag": "Texas"
},
{
"coordinates": [551, 564],
"flag": "California",
"name": "California"
"name": "California",
"flag": "California"
},
{
"coordinates": [703, 483],
"flag": "Utah",
"name": "Utah"
"coordinates": [654, 530],
"name": "Utah",
"flag": "Utah"
},
{
"coordinates": [1077, 444],
"flag": "Quebec",
"name": "Quebec"
"coordinates": [1079, 385],
"name": "Quebec",
"flag": "Quebec"
},
{
"coordinates": [1231, 395],
"flag": "Newfoundland",
"name": "Newfoundland"
"coordinates": [1211, 364],
"name": "Newfoundland and Labrador",
"flag": "newfoundlandandlabrador"
},
{
"coordinates": [967, 418],
"flag": "ca",
"name": "Canada"
"coordinates": [957, 406],
"name": "Canada",
"flag": "ca"
},
{
"coordinates": [170, 244],
"flag": "Alaska",
"name": "Alaska"
"name": "Alaska",
"flag": "Alaska"
},
{
"coordinates": [741, 234],
"flag": "Nunavut",
"name": "Nunavut"
"coordinates": [857, 232],
"name": "Nunavut",
"flag": "Nunavut"
},
{
"coordinates": [484, 256],
"flag": "Yukon",
"name": "Yukon"
"coordinates": [475, 254],
"name": "Yukon",
"flag": "Yukon"
},
{
"coordinates": [1434, 223],
"flag": "gl",
"name": "Greenland"
"coordinates": [1448, 137],
"name": "Greenland",
"flag": "gl"
},
{
"coordinates": [2247, 1229],
"flag": "Rhodesia",
"name": "Rhodesia"
"name": "Zimbabwe",
"flag": "zw"
},
{
"coordinates": [550, 438],
"name": "Washington",
"flag": "Washington"
},
{
"coordinates": [778, 518],
"name": "Kansas",
"flag": "Kansas"
},
{
"coordinates": [912, 613],
"name": "Mississippi",
"flag": "Mississippi"
},
{
"coordinates": [1208, 1219],
"name": "Bolivia",
"flag": "bo"
},
{
"coordinates": [2059, 1015],
"name": "Gabon",
"flag": "ga"
},
{
"coordinates": [689, 437],
"name": "Montana",
"flag": "Montana"
},
{
"coordinates": [517, 357],
"name": "British Columbia",
"flag": "britishcolumbia"
},
{
"coordinates": [618, 358],
"name": "Alberta",
"flag": "alberta"
},
{
"coordinates": [732, 360],
"name": "Saskatchewan",
"flag": "saskatchewan"
},
{
"coordinates": [834, 367],
"name": "Manitoba",
"flag": "manitoba"
},
{
"coordinates": [1108, 500],
"name": "Massachusetts",
"flag": "Massachusetts"
},
{
"coordinates": [845, 465],
"name": "Minnesota",
"flag": "Minnesota"
},
{
"coordinates": [960, 500],
"name": "Michigan",
"flag": "Michigan"
},
{
"coordinates": [655, 247],
"name": "Northwest Territories",
"flag": "northwestterritories"
}
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 MiB

After

Width:  |  Height:  |  Size: 5.0 MiB

@@ -82,10 +82,15 @@
"name": "Skid Row"
},
{
"coordinates": [925, 935],
"coordinates": [965, 865],
"flag": "California",
"name": "Inglewood"
},
{
"coordinates": [815, 945],
"flag": "California",
"name": "L.A.X."
},
{
"coordinates": [1180, 1010],
"flag": "California",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

+11 -11
View File
@@ -12,12 +12,12 @@
"name": "Apollo 14"
},
{
"coordinates": [780, 345],
"coordinates": [780, 340],
"flag": "us",
"name": "Apollo 15"
},
{
"coordinates": [825, 735],
"coordinates": [825, 705],
"flag": "us",
"name": "Apollo 11"
},
@@ -37,7 +37,7 @@
"name": "Surveyor 3"
},
{
"coordinates": [256, 148],
"coordinates": [250, 148],
"flag": "us",
"name": "Apollo 13"
},
@@ -48,7 +48,7 @@
},
{
"coordinates": [510, 170],
"coordinates": [515, 170],
"flag": "Russian SSR",
"name": "Luna 17"
},
@@ -95,7 +95,7 @@
"name": "Chang'e 4"
},
{
"coordinates": [270, 2690],
"coordinates": [260, 268],
"flag": "cn",
"name": "Chang'e 5"
},
@@ -106,7 +106,7 @@
},
{
"coordinates": [830, 735],
"coordinates": [830, 745],
"flag": "jp",
"name": "S.L.I.M."
},
@@ -117,7 +117,7 @@
"name": "Chandrayaan 3"
},
{
"coordinates": [732, 3490],
"coordinates": [732, 3493],
"flag": "in",
"name": "Chandrayaan 1"
},
@@ -125,18 +125,18 @@
{
"coordinates": [755, 3035],
"flag": "",
"name": "T▅▚░S▅cr▅░M▅l▅t▅r░B▅s▅"
"name": "T▆p░S▅cr▅t░M▊l▊t▅r░B▅s▅"
},
{
"coordinates": [628, 921],
"flag": "",
"name": "[]"
"name": ""
}
],
"teamGameSpawnAreas": {
"2": [
{ "x": 0, "y": 0, "width": 1308, "height": 1750 },
{ "x": 0, "y": 1750, "width": 1308, "height": 1750 }
{ "x": 0, "y": 0, "width": 1308, "height": 1754 },
{ "x": 0, "y": 1754, "width": 1308, "height": 1754 }
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

@@ -0,0 +1,140 @@
{
"name": "Middle East",
"nations": [
{
"coordinates": [300, 65],
"name": "Ottoman Empire",
"flag": "tr"
},
{
"coordinates": [1639, 558],
"name": "Qajar Dynasty",
"flag": "Persia"
},
{
"coordinates": [1141, 797],
"name": "Emirate of Kuwait",
"flag": "Socialist_flag"
},
{
"coordinates": [1880, 1353],
"name": "Sultanate of Muscat",
"flag": "Socialist_flag"
},
{
"coordinates": [1703, 1402],
"name": "Imamate of Oman",
"flag": "White Flag"
},
{
"coordinates": [1592, 1239],
"name": "Trucial States",
"flag": "Socialist_flag"
},
{
"coordinates": [1129, 1875],
"name": "Aden Protectorate",
"flag": "gb"
},
{
"coordinates": [964, 1744],
"name": "Kingdom of Yemen",
"flag": "Kingdom of Yemen"
},
{
"coordinates": [844, 1655],
"name": "Emirate of Asir",
"flag": "Emirate of Asir"
},
{
"coordinates": [579, 1173],
"name": "Kingdom of Hejaz",
"flag": "Arabia"
},
{
"coordinates": [800, 1052],
"name": "Rashidi Emirate",
"flag": "Rashidi Emirate"
},
{
"coordinates": [1092, 1336],
"name": "Sultanate of Nejd",
"flag": "Sultanate of Nejd"
},
{
"coordinates": [1397, 1128],
"name": "Qatar",
"flag": "qa"
},
{
"coordinates": [973, 296],
"name": "Kingdom of Iraq",
"flag": "Kingdom of Iraq"
},
{
"coordinates": [554, 364],
"name": "Kingdom of Syria",
"flag": "Kingdom of Syria"
},
{
"coordinates": [423, 647],
"name": "Palestine Mandate",
"flag": "gb"
},
{
"coordinates": [100, 781],
"name": "Kingdom of Egypt",
"flag": "Kingdom of Egypt"
},
{
"coordinates": [159, 1530],
"name": "Anglo-Egyptian Sudan",
"flag": "gb"
},
{
"coordinates": [578, 1766],
"name": "Italian Eritrea",
"flag": "italy"
},
{
"coordinates": [401, 2005],
"name": "Ethiopian Empire",
"flag": "Ethiopian Empire"
},
{
"coordinates": [826, 2044],
"name": "French Somaliland",
"flag": "fr"
},
{
"coordinates": [1455, 902],
"name": "British Bushehr",
"flag": "gb"
},
{
"coordinates": [185, 375],
"name": "British Cyprus",
"flag": "gb"
},
{
"coordinates": [2127, 373],
"name": "Emirate of Afghanistan",
"flag": "Emirate of Afghanistan"
},
{
"coordinates": [2087, 925],
"name": "Baluchistan Agency",
"flag": "gb"
},
{
"coordinates": [932, 15],
"name": "Republic of Armenia",
"flag": "am"
},
{
"coordinates": [1671, 71],
"name": "Russian State",
"flag": "ru"
}
]
}
+154 -54
View File
@@ -6,11 +6,6 @@
"name": "Florida",
"flag": "Florida"
},
{
"coordinates": [1010, 435],
"name": "Canada",
"flag": "ca"
},
{
"coordinates": [1250, 1130],
"name": "Mexico",
@@ -32,47 +27,47 @@
"flag": "ni"
},
{
"coordinates": [1734, 1403],
"coordinates": [1651, 1399],
"name": "Panama",
"flag": "pa"
},
{
"coordinates": [1821, 1395],
"coordinates": [1760, 1410],
"name": "Colombia",
"flag": "co"
},
{
"coordinates": [1896, 1379],
"coordinates": [1883, 1395],
"name": "Venezuela",
"flag": "ve"
},
{
"coordinates": [1725, 1180],
"coordinates": [1692, 1161],
"name": "Cuba",
"flag": "cu"
},
{
"coordinates": [467, 333],
"coordinates": [331, 341],
"name": "Alaska",
"flag": "Alaska"
},
{
"coordinates": [1154, 914],
"coordinates": [1062, 932],
"name": "Arizona",
"flag": "Arizona"
},
{
"coordinates": [1010, 865],
"coordinates": [940, 905],
"name": "California",
"flag": "California"
},
{
"coordinates": [1307, 863],
"coordinates": [1147, 843],
"name": "Colorado",
"flag": "Colorado"
},
{
"coordinates": [1673, 965],
"coordinates": [1593, 974],
"name": "Georgia",
"flag": "Georgia_US"
},
@@ -82,169 +77,274 @@
"flag": "Hawaii"
},
{
"coordinates": [1120, 760],
"coordinates": [1000, 710],
"name": "Idaho",
"flag": "Idaho"
},
{
"coordinates": [1551, 813],
"coordinates": [1490, 789],
"name": "Illinois",
"flag": "Illinois"
},
{
"coordinates": [1412, 873],
"coordinates": [1335, 835],
"name": "Kansas",
"flag": "Kansas"
},
{
"coordinates": [1651, 880],
"coordinates": [1525, 872],
"name": "Kentucky",
"flag": "Kentucky"
},
{
"coordinates": [1514, 1007],
"coordinates": [1410, 984],
"name": "Louisiana",
"flag": "Louisiana"
},
{
"coordinates": [1884, 735],
"coordinates": [1854, 720],
"name": "Maine",
"flag": "Maine"
},
{
"coordinates": [1634, 764],
"coordinates": [1572, 762],
"name": "Michigan",
"flag": "Michigan"
},
{
"coordinates": [1556, 965],
"coordinates": [1472, 940],
"name": "Mississippi",
"flag": "Mississippi"
},
{
"coordinates": [1527, 728],
"coordinates": [1390, 688],
"name": "Minnesota",
"flag": "Minnesota"
},
{
"coordinates": [1529, 880],
"coordinates": [1427, 829],
"name": "Missouri",
"flag": "Missouri"
},
{
"coordinates": [1225, 754],
"coordinates": [1100, 677],
"name": "Montana",
"flag": "Montana"
},
{
"coordinates": [1413, 789],
"coordinates": [1263, 787],
"name": "Nebraska",
"flag": "Nebraska"
},
{
"coordinates": [1090, 852],
"coordinates": [946, 818],
"name": "Nevada",
"flag": "Nevada"
},
{
"coordinates": [1253, 933],
"coordinates": [1169, 932],
"name": "New Mexico",
"flag": "New_Mexico"
},
{
"coordinates": [1833, 792],
"coordinates": [1737, 768],
"name": "New York",
"flag": "New_York"
},
{
"coordinates": [1444, 716],
"coordinates": [1239, 670],
"name": "North Dakota",
"flag": "North_Dakota"
},
{
"coordinates": [1704, 812],
"coordinates": [1612, 816],
"name": "Ohio",
"flag": "Ohio"
},
{
"coordinates": [1397, 921],
"coordinates": [1296, 902],
"name": "Oklahoma",
"flag": "Oklahoma"
},
{
"coordinates": [976, 754],
"coordinates": [873, 757],
"name": "Oregon",
"flag": "Oregon"
},
{
"coordinates": [1752, 716],
"coordinates": [1686, 796],
"name": "Pennsylvania",
"flag": "Pennsylvania"
},
{
"coordinates": [1716, 937],
"coordinates": [1636, 934],
"name": "South Carolina",
"flag": "South_Carolina"
},
{
"coordinates": [1419, 753],
"coordinates": [1302, 721],
"name": "South Dakota",
"flag": "South_Dakota"
},
{
"coordinates": [1648, 981],
"coordinates": [1579, 900],
"name": "Tennessee",
"flag": "Tennessee"
},
{
"coordinates": [1407, 1005],
"coordinates": [1279, 1013],
"name": "Texas",
"flag": "Texas"
},
{
"coordinates": [1827, 742],
"coordinates": [1785, 727],
"name": "Vermont",
"flag": "Vermont"
},
{
"coordinates": [1767, 857],
"coordinates": [1652, 864],
"name": "Virginia",
"flag": "Virginia"
},
{
"coordinates": [994, 700],
"coordinates": [897, 673],
"name": "Washington",
"flag": "Washington"
},
{
"coordinates": [1261, 759],
"coordinates": [1118, 764],
"name": "Wyoming",
"flag": "Wyoming"
},
{
"coordinates": [1867, 561],
"coordinates": [1798, 592],
"name": "Quebec",
"flag": "Quebec"
},
{
"coordinates": [1738, 80],
"name": "Santa Claus",
"flag": "santa_claus"
"coordinates": [1080, 367],
"name": "Northwest Territories",
"flag": "northwestterritories"
},
{
"coordinates": [1189, 240],
"name": "Polar Bears",
"flag": "polar_bears"
},
{
"coordinates": [1480, 350],
"name": "Frost Giants",
"flag": "frost_giant"
"coordinates": [1440, 319],
"name": "Nunavut",
"flag": "Nunavut"
},
{
"coordinates": [2399, 171],
"name": "Greenland",
"flag": "gl"
},
{
"coordinates": [777, 382],
"name": "Yukon",
"flag": "Yukon"
},
{
"coordinates": [787, 561],
"name": "British Columbia",
"flag": "britishcolumbia"
},
{
"coordinates": [966, 521],
"name": "Alberta",
"flag": "alberta"
},
{
"coordinates": [1172, 569],
"name": "Saskatchewan",
"flag": "saskatchewan"
},
{
"coordinates": [1343, 507],
"name": "Manitoba",
"flag": "manitoba"
},
{
"coordinates": [1575, 621],
"name": "Ontario",
"flag": "ontario"
},
{
"coordinates": [1903, 671],
"name": "New Brunswick",
"flag": "newbrunswick"
},
{
"coordinates": [1924, 748],
"name": "Nova Scotia",
"flag": "novascotia"
},
{
"coordinates": [1983, 547],
"name": "Newfoundland and Labrador",
"flag": "newfoundlandandlabrador"
},
{
"coordinates": [1793, 1209],
"name": "Haiti",
"flag": "ht"
},
{
"coordinates": [1851, 1215],
"name": "Dominican Republic",
"flag": "do"
},
{
"coordinates": [1912, 1224],
"name": "Puerto Rico",
"flag": "pr"
},
{
"coordinates": [2020, 1422],
"name": "Guyana",
"flag": "gy"
},
{
"coordinates": [2785, 358],
"name": "Iceland",
"flag": "is"
},
{
"coordinates": [1975, 702],
"name": "Prince Edward Island",
"flag": "princeedwardisland"
},
{
"coordinates": [1037, 838],
"name": "Utah",
"flag": "Utah"
},
{
"coordinates": [1402, 772],
"name": "Iowa",
"flag": "Iowa"
},
{
"coordinates": [1685, 902],
"name": "North Carolina",
"flag": "North_Carolina"
},
{
"coordinates": [1417, 904],
"name": "Arkansas",
"flag": "Arkansas"
},
{
"coordinates": [1547, 819],
"name": "Indiana",
"flag": "Indiana"
},
{
"coordinates": [1528, 963],
"name": "Alabama",
"flag": "Alabama"
},
{
"coordinates": [1811, 782],
"name": "Massachusetts",
"flag": "Massachusetts"
}
]
}
+22 -72
View File
@@ -2,28 +2,28 @@
"name": "Oceania",
"nations": [
{
"coordinates": [718, 738],
"coordinates": [515, 690],
"name": "Australia",
"flag": "au"
},
{
"coordinates": [1050, 809],
"coordinates": [1010, 836],
"name": "New Zealand",
"flag": "nz"
},
{
"coordinates": [436, 407],
"name": "Timor-Leste",
"name": "Timor Leste",
"flag": "tl"
},
{
"coordinates": [182, 378],
"coordinates": [236, 336],
"name": "Indonesia",
"flag": "id"
},
{
"coordinates": [292, 243],
"name": "Brunei Darussalam",
"name": "Brunei",
"flag": "bn"
},
{
@@ -32,33 +32,33 @@
"flag": "sg"
},
{
"coordinates": [120, 261],
"coordinates": [115, 228],
"name": "Malaysia",
"flag": "my"
},
{
"coordinates": [106, 129],
"coordinates": [112, 113],
"name": "Thailand",
"flag": "th"
},
{
"coordinates": [51, 42],
"coordinates": [34, 23],
"name": "Myanmar",
"flag": "mm"
},
{
"coordinates": [158, 162],
"coordinates": [171, 153],
"name": "Cambodia",
"flag": "kh"
},
{
"coordinates": [182, 43],
"coordinates": [203, 105],
"name": "Vietnam",
"flag": "vn"
},
{
"coordinates": [143, 37],
"name": "Lao PDR",
"coordinates": [127, 42],
"name": "Laos",
"flag": "la"
},
{
@@ -67,70 +67,35 @@
"flag": "hk"
},
{
"coordinates": [359, 1],
"name": "Taiwan, Province of China",
"coordinates": [362, 9],
"name": "Taiwan",
"flag": "tw"
},
{
"coordinates": [366, 119],
"coordinates": [425, 194],
"name": "Philippines",
"flag": "ph"
},
{
"coordinates": [536, 207],
"name": "Palau",
"flag": "pw"
},
{
"coordinates": [834, 215],
"name": "Micronesia",
"flag": "fm"
},
{
"coordinates": [664, 113],
"name": "Guam",
"flag": "gu"
},
{
"coordinates": [1042, 317],
"name": "Marshall Islands",
"flag": "mh"
},
{
"coordinates": [799, 385],
"coordinates": [630, 384],
"name": "Papua New Guinea",
"flag": "pg"
},
{
"coordinates": [862, 442],
"coordinates": [855, 424],
"name": "Solomon Islands",
"flag": "sb"
},
{
"coordinates": [945, 497],
"name": "Vanuatu",
"flag": "vu"
},
{
"coordinates": [930, 574],
"coordinates": [925, 574],
"name": "New Caledonia",
"flag": "nc"
},
{
"coordinates": [1085, 526],
"coordinates": [1080, 528],
"name": "Fiji",
"flag": "fj"
},
{
"coordinates": [1169, 568],
"name": "Tonga",
"flag": "to"
},
{
"coordinates": [1236, 541],
"name": "Niue",
"flag": "nu"
},
{
"coordinates": [1204, 473],
"name": "Samoa",
@@ -142,24 +107,9 @@
"flag": "ck"
},
{
"coordinates": [1623, 424],
"name": "French Polynesia",
"flag": "pf"
},
{
"coordinates": [1393, 278],
"name": "Kiribati",
"flag": "ki"
},
{
"coordinates": [1420, 56],
"name": "United States",
"flag": "us"
},
{
"coordinates": [1996, 644],
"name": "Chile",
"flag": "cl"
"coordinates": [1413, 56],
"name": "Hawaii",
"flag": "Hawaii"
}
]
}
@@ -17,12 +17,12 @@
"flag": "ht"
},
{
"coordinates": [112, 209],
"coordinates": [122, 143],
"name": "Belize",
"flag": "bz"
},
{
"coordinates": [71, 282],
"coordinates": [68, 257],
"name": "Guatemala",
"flag": "gt"
},
@@ -42,84 +42,89 @@
"flag": "pa"
},
{
"coordinates": [740, 1180],
"coordinates": [847, 1209],
"name": "Bolivia",
"flag": "bo"
},
{
"coordinates": [849, 1770],
"coordinates": [861, 1851],
"name": "Argentina",
"flag": "ar"
},
{
"coordinates": [1394, 1309],
"coordinates": [1153, 1095],
"name": "Brazil",
"flag": "br"
},
{
"coordinates": [691, 1371],
"coordinates": [687, 1605],
"name": "Chile",
"flag": "cl"
},
{
"coordinates": [527, 503],
"coordinates": [582, 543],
"name": "Colombia",
"flag": "co"
},
{
"coordinates": [384, 746],
"coordinates": [444, 750],
"name": "Ecuador",
"flag": "ec"
},
{
"coordinates": [933, 423],
"name": "French Guyana",
"coordinates": [1204, 615],
"name": "French Guiana",
"flag": "gf"
},
{
"coordinates": [800, 410],
"coordinates": [981, 546],
"name": "Guyana",
"flag": "gy"
},
{
"coordinates": [541, 1092],
"coordinates": [537, 1017],
"name": "Peru",
"flag": "pe"
},
{
"coordinates": [960, 1496],
"coordinates": [1028, 1404],
"name": "Paraguay",
"flag": "py"
},
{
"coordinates": [890, 610],
"coordinates": [1097, 586],
"name": "Suriname",
"flag": "sr"
},
{
"coordinates": [1091, 1635],
"coordinates": [1118, 1695],
"name": "Uruguay",
"flag": "uy"
},
{
"coordinates": [678, 904],
"coordinates": [770, 465],
"name": "Venezuela",
"flag": "ve"
},
{
"coordinates": [1270, 1035],
"name": "The Biggest Snakes",
"flag": "Aztec Empire"
"coordinates": [795, 808],
"name": "Amazonas",
"flag": "amazonas"
},
{
"coordinates": [894, 693],
"name": "Normal Capybaras",
"flag": ""
"coordinates": [1267, 794],
"name": "Pará",
"flag": "Para"
},
{
"coordinates": [884, 832],
"name": "Just Otters",
"flag": ""
"coordinates": [1550, 1066],
"name": "Bahia",
"flag": "bahia"
},
{
"coordinates": [1374, 1404],
"name": "São Paulo",
"flag": "Sao Paulo"
}
]
}
+215 -160
View File
@@ -2,309 +2,364 @@
"name": "World",
"nations": [
{
"coordinates": [375, 272],
"flag": "us",
"name": "United States"
"coordinates": [484, 284],
"name": "United States",
"flag": "us"
},
{
"coordinates": [372, 136],
"flag": "ca",
"name": "Canada"
"coordinates": [374, 186],
"name": "Canada",
"flag": "ca"
},
{
"coordinates": [375, 374],
"flag": "mx",
"name": "Mexico"
"name": "Mexico",
"flag": "mx"
},
{
"coordinates": [500, 378],
"flag": "cu",
"name": "Cuba"
"name": "Cuba",
"flag": "cu"
},
{
"coordinates": [524, 474],
"flag": "co",
"name": "Colombia"
"coordinates": [527, 487],
"name": "Colombia",
"flag": "co"
},
{
"coordinates": [593, 473],
"flag": "ve",
"name": "Venezuela"
"coordinates": [576, 466],
"name": "Venezuela",
"flag": "ve"
},
{
"coordinates": [596, 705],
"flag": "ar",
"name": "Argentina"
"coordinates": [565, 737],
"name": "Argentina",
"flag": "ar"
},
{
"coordinates": [637, 567],
"flag": "br",
"name": "Brazil"
"coordinates": [684, 574],
"name": "Brazil",
"flag": "br"
},
{
"coordinates": [1280, 975],
"flag": "aq",
"name": "Antarctica"
"name": "Antarctica",
"flag": "aq"
},
{
"coordinates": [709, 57],
"flag": "gl",
"name": "Greenland"
"name": "Greenland",
"flag": "gl"
},
{
"coordinates": [831, 112],
"flag": "is",
"name": "Iceland"
"name": "Iceland",
"flag": "is"
},
{
"coordinates": [925, 186],
"flag": "gb",
"name": "United Kingdom"
"name": "United Kingdom",
"flag": "gb"
},
{
"coordinates": [887, 183],
"flag": "ie",
"name": "Ireland"
"name": "Ireland",
"flag": "ie"
},
{
"coordinates": [908, 264],
"flag": "es",
"name": "Spain"
"name": "Spain",
"flag": "es"
},
{
"coordinates": [1004, 250],
"flag": "it",
"name": "Italy"
"name": "Italy",
"flag": "it"
},
{
"coordinates": [958, 220],
"flag": "fr",
"name": "France"
"coordinates": [948, 221],
"name": "France",
"flag": "fr"
},
{
"coordinates": [997, 205],
"flag": "de",
"name": "Germany"
"coordinates": [990, 195],
"name": "Germany",
"flag": "de"
},
{
"coordinates": [1064, 101],
"flag": "se",
"name": "Sweden"
"coordinates": [1014, 137],
"name": "Sweden",
"flag": "se"
},
{
"coordinates": [1046, 193],
"flag": "pl",
"name": "Poland"
"coordinates": [1031, 193],
"name": "Poland",
"flag": "pl"
},
{
"coordinates": [1061, 188],
"flag": "by",
"name": "Belarus"
"coordinates": [1102, 183],
"name": "Belarus",
"flag": "by"
},
{
"coordinates": [1073, 243],
"flag": "ro",
"name": "Romania"
"coordinates": [1073, 230],
"name": "Romania",
"flag": "ro"
},
{
"coordinates": [1161, 274],
"flag": "tr",
"name": "Turkey"
"coordinates": [1123, 272],
"name": "Türkiye",
"flag": "tr"
},
{
"coordinates": [969, 133],
"flag": "no",
"name": "Norway"
"name": "Norway",
"flag": "no"
},
{
"coordinates": [1062, 133],
"flag": "fi",
"name": "Finland"
"coordinates": [1082, 126],
"name": "Finland",
"flag": "fi"
},
{
"coordinates": [1099, 211],
"flag": "ua",
"name": "Ukraine"
"coordinates": [1135, 210],
"name": "Ukraine",
"flag": "ua"
},
{
"coordinates": [1344, 136],
"flag": "ru",
"name": "Russia"
"coordinates": [1351, 134],
"name": "Russia",
"flag": "ru"
},
{
"coordinates": [1537, 186],
"flag": "mn",
"name": "Mongolia"
"coordinates": [1500, 203],
"name": "Mongolia",
"flag": "mn"
},
{
"coordinates": [1524, 328],
"flag": "cn",
"name": "China"
"coordinates": [1527, 303],
"name": "China",
"flag": "cn"
},
{
"coordinates": [1368, 373],
"flag": "in",
"name": "India"
"coordinates": [1331, 353],
"name": "India",
"flag": "in"
},
{
"coordinates": [1276, 239],
"flag": "kz",
"name": "Kazakhstan"
"coordinates": [1279, 207],
"name": "Kazakhstan",
"flag": "kz"
},
{
"coordinates": [1238, 309],
"flag": "ir",
"name": "Islamic Republic Of Iran"
"name": "Iran",
"flag": "ir"
},
{
"coordinates": [1178, 351],
"flag": "sa",
"name": "Saudi Arabia"
"name": "Saudi Arabia",
"flag": "sa"
},
{
"coordinates": [1679, 657],
"flag": "au",
"name": "Australia"
"name": "Australia",
"flag": "au"
},
{
"coordinates": [1890, 775],
"flag": "nz",
"name": "New Zealand"
"name": "New Zealand",
"flag": "nz"
},
{
"coordinates": [918, 342],
"flag": "dz",
"name": "Algeria"
"coordinates": [957, 295],
"name": "Algeria",
"flag": "dz"
},
{
"coordinates": [1030, 332],
"flag": "ly",
"name": "Libyan Arab Jamahiriya"
"coordinates": [1024, 340],
"name": "Libya",
"flag": "ly"
},
{
"coordinates": [1092, 335],
"flag": "eg",
"name": "Egypt"
"coordinates": [1094, 340],
"name": "Egypt",
"flag": "eg"
},
{
"coordinates": [963, 410],
"flag": "ne",
"name": "Niger"
"coordinates": [956, 389],
"name": "Niger",
"flag": "ne"
},
{
"coordinates": [1112, 406],
"flag": "sd",
"name": "Sudan"
"coordinates": [1101, 400],
"name": "Sudan",
"flag": "sd"
},
{
"coordinates": [1074, 508],
"flag": "cd",
"name": "DR Congo"
"coordinates": [1051, 513],
"name": "DR Congo",
"flag": "cd"
},
{
"coordinates": [1154, 443],
"flag": "et",
"name": "Ethiopia"
"coordinates": [1153, 451],
"name": "Ethiopia",
"flag": "et"
},
{
"coordinates": [1075, 707],
"flag": "za",
"name": "South Africa"
"name": "South Africa",
"flag": "za"
},
{
"coordinates": [1194, 627],
"flag": "mg",
"name": "Madagascar"
"name": "Madagascar",
"flag": "mg"
},
{
"coordinates": [1052, 420],
"flag": "td",
"name": "Chad"
"coordinates": [1036, 420],
"name": "Chad",
"flag": "td"
},
{
"coordinates": [1030, 665],
"flag": "na",
"name": "Namibia"
"coordinates": [1039, 646],
"name": "Namibia",
"flag": "na"
},
{
"coordinates": [1632, 465],
"flag": "ph",
"name": "Philippines"
"name": "Philippines",
"flag": "ph"
},
{
"coordinates": [1537, 426],
"flag": "th",
"name": "Thailand"
"coordinates": [1502, 415],
"name": "Thailand",
"flag": "th"
},
{
"coordinates": [1610, 364],
"flag": "tw",
"name": "Taiwan"
"name": "Taiwan",
"flag": "tw"
},
{
"coordinates": [1710, 290],
"flag": "jp",
"name": "Japan"
"name": "Japan",
"flag": "jp"
},
{
"coordinates": [1869, 119],
"flag": "ru",
"name": "Siberia"
"coordinates": [1626, 118],
"name": "Siberia",
"flag": "Siberia"
},
{
"coordinates": [74, 117],
"flag": "polar_bears",
"name": "Polar Bears"
"coordinates": [422, 95],
"name": "Nunavut",
"flag": "Nunavut"
},
{
"coordinates": [419, 975],
"flag": "aq",
"name": "West Antarctica"
"name": "West Antarctica",
"flag": "aq"
},
{
"coordinates": [542, 603],
"flag": "pe",
"name": "Peru"
"coordinates": [516, 567],
"name": "Peru",
"flag": "pe"
},
{
"coordinates": [1075, 615],
"flag": "zm",
"name": "Zambia"
"coordinates": [1097, 598],
"name": "Zambia",
"flag": "zm"
},
{
"coordinates": [1099, 165],
"flag": "lv",
"name": "Latvia"
"coordinates": [1067, 168],
"name": "Latvia",
"flag": "lv"
},
{
"coordinates": [1427, 336],
"flag": "bt",
"name": "Bhutan"
"coordinates": [1419, 338],
"name": "Bhutan",
"flag": "bt"
},
{
"coordinates": [1511, 524],
"flag": "id",
"name": "Indonesia"
"name": "Indonesia",
"flag": "id"
},
{
"coordinates": [1809, 977],
"flag": "aq",
"name": "East Antarctica"
"name": "East Antarctica",
"flag": "aq"
},
{
"coordinates": [1255, 382],
"flag": "om",
"name": "Oman"
"name": "Oman",
"flag": "om"
},
{
"coordinates": [853, 373],
"flag": "ma",
"name": "Morocco"
"coordinates": [886, 327],
"name": "Morocco",
"flag": "ma"
},
{
"coordinates": [656, 678],
"flag": "uy",
"name": "Uruguay"
"coordinates": [626, 704],
"name": "Uruguay",
"flag": "uy"
},
{
"coordinates": [581, 620],
"name": "Bolivia",
"flag": "bo"
},
{
"coordinates": [95, 115],
"name": "Alaska",
"flag": "Alaska"
},
{
"coordinates": [243, 131],
"name": "Yukon",
"flag": "Yukon"
},
{
"coordinates": [264, 276],
"name": "California",
"flag": "California"
},
{
"coordinates": [371, 299],
"name": "Texas",
"flag": "Texas"
},
{
"coordinates": [532, 191],
"name": "Quebec",
"flag": "Quebec"
},
{
"coordinates": [932, 476],
"name": "Benin",
"flag": "bj"
},
{
"coordinates": [845, 425],
"name": "Senegal",
"flag": "sn"
},
{
"coordinates": [1152, 522],
"name": "Kenya",
"flag": "ke"
},
{
"coordinates": [1320, 282],
"name": "Pakistan",
"flag": "pk"
},
{
"coordinates": [1384, 466],
"name": "Sri Lanka",
"flag": "lk"
}
]
}
+1
View File
@@ -53,6 +53,7 @@ var maps = []struct {
{Name: "straitofmalacca"},
{Name: "mars"},
{Name: "mena"},
{Name: "middleeast"},
{Name: "montreal"},
{Name: "newyorkcity"},
{Name: "northamerica"},
+1804 -4907
View File
File diff suppressed because it is too large Load Diff
+53 -66
View File
@@ -29,104 +29,91 @@
]
},
"devDependencies": {
"@datastructures-js/priority-queue": "^6.3.3",
"@eslint/compat": "^1.2.7",
"@eslint/js": "^9.21.0",
"@tailwindcss/vite": "^4.1.18",
"@datastructures-js/priority-queue": "^6.3.5",
"@eslint/compat": "^2.0.5",
"@eslint/js": "^10.0.1",
"@tailwindcss/vite": "^4.2.4",
"@types/benchmark": "^2.1.5",
"@types/chai": "^4.3.17",
"@types/d3": "^7.4.3",
"@types/ejs": "^3.1.5",
"@types/express": "^5.0.6",
"@types/google-protobuf": "^3.15.12",
"@types/hammerjs": "^2.0.46",
"@types/howler": "^2.2.12",
"@types/jquery": "^3.5.31",
"@types/js-yaml": "^4.0.9",
"@types/msgpack5": "^3.4.6",
"@types/node": "^22.10.2",
"@types/pg": "^8.11.11",
"@types/node": "^24.12.0",
"@types/pg": "^8.20.0",
"@types/seedrandom": "^3.0.8",
"@types/sinon": "^17.0.3",
"@types/systeminformation": "^3.23.1",
"@types/ws": "^8.5.11",
"@vitest/coverage-v8": "^4.0.16",
"@vitest/ui": "^4.0.16",
"autoprefixer": "^10.4.20",
"@types/ws": "^8.18.1",
"@vitest/coverage-v8": "^4.1.5",
"@vitest/ui": "^4.1.5",
"autoprefixer": "^10.5.0",
"benchmark": "^2.1.4",
"canvas": "^3.2.1",
"chai": "^5.1.1",
"canvas": "^3.2.3",
"concurrently": "^9.2.1",
"cross-env": "^7.0.3",
"cross-env": "^10.1.0",
"d3": "^7.9.0",
"eslint": "^9.21.0",
"eslint-config-prettier": "^10.1.1",
"eslint-formatter-gha": "^1.5.2",
"glob": "^13.0.0",
"globals": "^16.0.0",
"eslint": "^10.3.0",
"eslint-config-prettier": "^10.1.8",
"eslint-formatter-gha": "^2.0.1",
"glob": "^13.0.6",
"globals": "^17.6.0",
"husky": "^9.1.7",
"jsdom": "^27.4.0",
"lint-staged": "^16.1.2",
"lit": "^3.3.1",
"jsdom": "^29.1.1",
"lint-staged": "^16.4.0",
"lit": "^3.3.2",
"lit-markdown": "^1.3.2",
"mrmime": "^2.0.0",
"mrmime": "^2.0.1",
"pixi-filters": "^6.1.5",
"pixi.js": "^8.17.1",
"prettier": "^3.5.3",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-sh": "^0.17.4",
"protobufjs": "^7.5.5",
"sinon": "^21.0.1",
"sinon-chai": "^4.0.0",
"tailwindcss": "^4.1.18",
"pixi.js": "^8.18.1",
"prettier": "^3.8.3",
"prettier-plugin-organize-imports": "^4.3.0",
"prettier-plugin-sh": "^0.18.1",
"tailwindcss": "^4.2.4",
"tsconfig-paths": "^4.2.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.1",
"vite": "^7.3.2",
"vite": "^8.0.10",
"vite-plugin-html": "^3.2.2",
"vite-plugin-static-copy": "^3.1.4",
"vite-tsconfig-paths": "^6.0.3",
"vitest": "^4.0.16",
"vitest-canvas-mock": "^1.1.3"
"vitest": "^4.1.5",
"vitest-canvas-mock": "^1.1.4"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.758.0",
"@lit-labs/virtualizer": "^2.1.1",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.200.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.200.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.200.0",
"@opentelemetry/resources": "^2.0.0",
"@opentelemetry/sdk-logs": "^0.200.0",
"@opentelemetry/sdk-metrics": "^2.0.0",
"@opentelemetry/semantic-conventions": "^1.32.0",
"@opentelemetry/winston-transport": "^0.11.0",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api-logs": "^0.216.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.216.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.216.0",
"@opentelemetry/resources": "^2.7.1",
"@opentelemetry/sdk-logs": "^0.216.0",
"@opentelemetry/sdk-metrics": "^2.7.1",
"@opentelemetry/semantic-conventions": "^1.40.0",
"@opentelemetry/winston-transport": "^0.26.0",
"@types/compression": "^1.8.1",
"colord": "^2.9.3",
"colorjs.io": "^0.5.2",
"colorjs.io": "^0.6.1",
"compression": "^1.8.1",
"dompurify": "^3.4.0",
"dotenv": "^16.5.0",
"ejs": "^3.1.10",
"dompurify": "^3.4.2",
"dotenv": "^17.4.2",
"ejs": "^5.0.2",
"express": "^5.2.1",
"express-rate-limit": "^8.3.2",
"fastpriorityqueue": "^0.7.5",
"express-rate-limit": "^8.4.1",
"fastpriorityqueue": "^0.8.0",
"howler": "^2.2.4",
"intl-messageformat": "^10.7.16",
"intl-messageformat": "^11.2.3",
"ip-anonymize": "^0.1.0",
"jose": "^6.0.10",
"jose": "^6.2.3",
"js-yaml": "^4.1.1",
"limiter": "^3.0.0",
"nanoid": "^3.3.6",
"node-html-parser": "^7.0.2",
"obscenity": "^0.4.3",
"nanoid": "^5.1.11",
"node-html-parser": "^7.1.0",
"obscenity": "^0.4.6",
"seedrandom": "^3.0.5",
"ts-node": "^10.9.2",
"tsx": "^4.17.0",
"uuid": "^14.0.0",
"winston": "^3.17.0",
"ws": "^8.18.0",
"zod": "^4.0.5"
"tsx": "^4.21.0",
"winston": "^3.19.0",
"ws": "^8.20.0",
"zod": "^4.4.2"
},
"type": "module"
}
+40 -6
View File
@@ -665,6 +665,16 @@
"continent": "North America",
"name": "El Salvador"
},
{
"code": "Emirate of Afghanistan",
"continent": "Asia",
"name": "Emirate of Afghanistan"
},
{
"code": "Emirate of Asir",
"continent": "Asia",
"name": "Emirate of Asir"
},
{
"code": "granada",
"continent": "Europe",
@@ -721,6 +731,11 @@
"continent": "Africa",
"name": "Ethiopia"
},
{
"code": "Ethiopian Empire",
"continent": "Africa",
"name": "Ethiopian Empire"
},
{
"code": "eu",
"continent": "Europe",
@@ -1050,11 +1065,6 @@
"continent": "Europe",
"name": "Italy"
},
{
"code": "italy",
"continent": "Europe",
"name": "Kingdom of Italy"
},
{
"code": "jm",
"continent": "North America",
@@ -1130,6 +1140,11 @@
"continent": "Asia",
"name": "Kingdom of Iraq"
},
{
"code": "italy",
"continent": "Europe",
"name": "Kingdom of Italy"
},
{
"code": "Kingdom of Jerusalem",
"continent": "Asia",
@@ -1140,6 +1155,16 @@
"continent": "Asia",
"name": "Kingdom of Judah"
},
{
"code": "Kingdom of Syria",
"continent": "Asia",
"name": "Kingdom of Syria"
},
{
"code": "Kingdom of Yemen",
"continent": "Asia",
"name": "Kingdom of Yemen"
},
{
"code": "Kirghiz SSR",
"continent": "Asia",
@@ -1808,6 +1833,11 @@
"continent": "North America",
"name": "Quebec"
},
{
"code": "Rashidi Emirate",
"continent": "Asia",
"name": "Rashidi Emirate"
},
{
"code": "Republic of China",
"continent": "Asia",
@@ -2057,7 +2087,7 @@
},
{
"code": "Socialist_flag",
"name": "Socialist Flag"
"name": "Red Flag"
},
{
"code": "sb",
@@ -2463,6 +2493,10 @@
"continent": "Africa",
"name": "Western Sahara"
},
{
"code": "White Flag",
"name": "White Flag"
},
{
"code": "Wisconsin",
"continent": "North America",
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" viewBox="0 0 150 150">
<image href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAADICAYAAACZBDirAAAKlklEQVR4AezZsW5VRx7A4QGEREcBEn0a6KAECwjd7ivQUKy0GB4ATINoMZQ0mIUt8hS7HQEKJArcw6ZHgo4CISH2jCMnFnbsc+6dM2dmzhf5Ark+d+Y/3zg/JcrhsPuvn7u3nnWv/3Wv716BQWDgn4OqfwZiy2LTYtu6q/zz68cAPum+9bx7/aN7/dS9fBEgQKB2gdiy2LTYtti4P86zM4D/6d79Z/fyRYAAgVYFYuNi67bOtx3AWMW/bb0zo18clQCBWQrE1sXmhRjA+N/FsYqzlHBoAgRmKRCb93MM4LVZHt+hCRCYu8C1GMArc1eY5fkdmgCBKzGA8f+QoCBAgMDcBH6KAZzboZ2XAAECWwICuMXgl3kJOC2B3wUWDuD58+fDxsZGePHiRfj48WP4/v27FwM/A34GsvwMxObE9jx+/DjEFv2es+G/Dg7gsWPHwvr6enj16lW4fv16uHTpUjhx4sTwnX2CAAECCwrE5sT2rK6ubrXo/v374ejRo4NXGxTAs2fPhs3NzXD79u1w5MiRwZv5AAECkws0N0Bs0draWnjz5k2IjRpywEEBvHr1ajh9+vSQ9T1LgACBLAIxfrFRQzbrHcCVlZVw69at4C8CBAiUKhAbFUPYd77eAbx27Vo4fLj343339xwBAgSSCcRG3bx58y/X+/EbvYt27ty5Hz/r7wkQIFCcwJkzZ3rP1DuAQxbtvbsHCRAgkFhgSKt6B/D48eOJx7QcAQIE0gucOnWq96K9A9h7xYIfNBoBAgR2CgjgTg1/JkBgVgICOKvrdlgCBHYKCOBOjZb/7GwECOwSEMBdJN4gQGAuAgI4l5t2TgIEdgkI4C4Sb7Qn4EQE9hYQwL1dvEuAwAwEBHAGl+yIBAjsLSCAe7t4l0ArAs6xj4AA7oPjWwQItC0ggG3fr9MRILCPgADug+NbBAjULXDQ9AJ4kJDvEyDQrIAANnu1DkaAwEECAniQkO8TINCsQNMBbPbWHIwAgSQCApiE0SIECNQoIIA13pqZCRBIIiCASRgLXMRIBAgcKCCABxJ5gACBVgUEsNWbdS4CBA4UEMADiTxQn4CJCfQTEMB+Tp4iQKBBAQFs8FIdiQCBfgIC2M/JUwRqETDnAAEBHIDlUQIE2hIQwLbu02kIEBggIIADsDxKgEDZAkOnE8ChYp4nQKAZAQFs5iodhACBoQICOFTM8wQINCPQVACbuRUHIUAgi4AAZmG2CQECJQoIYIm3YiYCBLIICGAW5gyb2IIAgcECAjiYzAcIEGhFQABbuUnnIEBgsIAADibzgfIETERgMQEBXMzNpwgQaEBAABu4REcgQGAxAQFczM2nCJQiYI4lBARwCTwfJUCgbgEBrPv+TE+AwBICArgEno8SIDCtwLK7C+Cygj5PgEC1AgJY7dUZnACBZQUEcFlBnydAoFqBqgNYrbrBCRAoQkAAi7gGQxAgMIWAAE6hbk8CBIoQEMAirmGBIXyEAIGlBQRwaUILECBQq4AA1npz5iZAYGkBAVya0AL5BexIII2AAKZxtAoBAhUKCGCFl2ZkAgTSCAhgGkerEMglYJ+EAgKYENNSBAjUJSCAdd2XaQkQSCgggAkxLUWAwLgCqVcXwNSi1iNAoBoBAazmqgxKgEBqAQFMLWo9AgSqEagqgNWoGpQAgSoEBLCKazIkAQJjCAjgGKrWJECgCgEBrOKaQgjmJEAguYAAJie1IAECtQgIYC03ZU4CBJILCGByUgumF7AigXEEBHAcV6sSIFCBgABWcElGJEBgHAEBHMfVqgRSCVhnRAEBHBHX0gQIlC0ggGXfj+kIEBhRQABHxLU0AQLLCYz9aQEcW9j6BAgUKyCAxV6NwQgQGFtAAMcWtj4BAsUKFB3AYtUMRoBAEwIC2MQ1OgQBAosICOAiaj5DgEATAgJY6jWaiwCB0QUEcHRiGxAgUKqAAJZ6M+YiQGB0AQEcndgGwwV8gkAeAQHM42wXAgQKFBDAAi/FSAQI5BEQwDzOdiHQV8BzGQUEMCO2rQgQKEtAAMu6D9MQIJBRQAAzYtuKAIH9BXJ/VwBzi9uPAIFiBASwmKswCAECuQUEMLe4/QgQKEagqAAWo2IQAgRmISCAs7hmhyRAYC8BAdxLxXsECMxCQABLuWZzECCQXUAAs5PbkACBUgQEsJSbMAcBAtkFBDA7uQ13C3iHwDQCAjiNu10JEChAQAALuAQjECAwjYAATuNuVwLbAn6fUEAAJ8S3NQEC0woI4LT+didAYEIBAZwQ39YE5i4w9fkFcOobsD8BApMJCOBk9DYmQGBqAQGc+gbsT4DAZAKTBnCyU9uYAAECnYAAdgi+CBCYp4AAzvPenZoAgU5AADuESb5sSoDA5AICOPkVGIAAgakEBHAqefsSIDC5gABOfgVzHMCZCZQhIIBl3IMpCBCYQEAAJ0C3JQECZQgIYBn3YIr5CDhpQQICWNBlGIUAgbwCApjX224ECBQkIIAFXYZRCLQuUNr5BLC0GzEPAQLZBAQwG7WNCBAoTUAAS7sR8xAgkE0gawCzncpGBAgQ6CEggD2QPEKAQJsCAtjmvToVAQI9BASwB1KSRyxCgEBxAgJY3JUYiACBXAICmEvaPgQIFCcggMVdSYsDOROBMgUEsMx7MRUBAhkEBDADsi0IEChTQADLvBdTtSPgJAULCGDBl2M0AgTGFRDAcX2tToBAwQICWPDlGI1A7QKlzy+Apd+Q+QgQGE1AAEejtTABAqULCGDpN2Q+AgRGExg1gKNNbWECBAgkEBDABIiWIECgTgEBrPPeTE2AQAIBAUyAuOcS3iRAoHgBASz+igxIgMBYAgI4lqx1CRAoXkAAi7+iGgc0M4E6BASwjnsyJQECIwgI4AioliRAoA4BAazjnkxZj4BJKxIQwIouy6gECKQVEMC0nlYjQKAiAQGs6LKMSqB0gdrmE8Dabsy8BAgkExDAZJQWIkCgNgEBrO3GzEuAQDKBpAFMNpWFCBAgkEFAADMg24IAgTIFBLDMezEVAQIZBHoH8MOHDxnGqXgLoxMgUITAkFb1DuDm5mYRhzMEAQIE9hMY0ioB3E/S9wgQqE5glAA+e/YsfP36tToMA+cQsAeBMgQ+f/4cnj592nuY3v8G+O7du3Dv3r3eC3uQAAECuQXu3r0b3r9/33vb3gGMKz58+DC8ffs2/tGLAAECRQnE//R99OjRoJkGBfDbt29hZWUlrK+vh/jnQTt5mECbAk41sUBs0YMHD8KFCxcGd2lQAOM5v3z5Eu7cuRMuXrwYNjY2wsuXL8OnT5/it7wIECCQRSA2J7bnyZMnWy1aW1sLsU1DNx8cwO0NXr9+HW7cuBEuX74cTp48GQ4dOuTFwM+An4EsPwOxObE9q6urIbZou0tDf184gEM38jwBAu0J1H4iAaz9Bs1PgMDCAjGAvy38aR8kQIBAvQK/xQA+r3d+kxMgQGBhgecxgL8s+nGfI0CAQMUCv8QA/tod4F/dyxcBAgTmIhCb92sMYDzw9e6X/3YvXwQIEGhdILYuNi9sBzAe+O/dL7GK3W++DhTwAAECNQrExsXWbc2+M4DxjVjFK90f/t29/N/hDsEXAQLVC8SWxabFtsXG/XGg/wMAAP//p4HOyAAAAAZJREFUAwA3Oy4c7Kb0ZAAAAABJRU5ErkJggg==" x="7.500" y="32.813" width="135.000" height="84.375" />
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 69 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 90 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 84 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 99 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 127 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 74 KiB

Before

Width:  |  Height:  |  Size: 207 B

After

Width:  |  Height:  |  Size: 207 B

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 175 B

Before

Width:  |  Height:  |  Size: 248 B

After

Width:  |  Height:  |  Size: 248 B

+3 -86
View File
@@ -1,87 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 100 100"
fill="none"
x="0px"
y="0px"
version="1.1"
id="svg67"
sodipodi:docname="noun-medal-4567887.svg"
width="100"
height="100"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs71">
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Invert"
id="filter203"
x="0"
y="0"
width="1"
height="1">
<feColorMatrix
type="hueRotate"
values="180"
result="color1"
id="feColorMatrix199" />
<feColorMatrix
values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 -0.21 -0.72 -0.07 2 0 "
result="color2"
id="feColorMatrix201" />
</filter>
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Invert"
id="filter209"
x="0"
y="0"
width="1"
height="1">
<feColorMatrix
type="hueRotate"
values="180"
result="color1"
id="feColorMatrix205" />
<feColorMatrix
values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 -0.21 -0.72 -0.07 2 0 "
result="color2"
id="feColorMatrix207" />
</filter>
</defs>
<sodipodi:namedview
id="namedview69"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
showgrid="false"
inkscape:zoom="6.456"
inkscape:cx="49.953532"
inkscape:cy="37.716853"
inkscape:window-width="1920"
inkscape:window-height="1010"
inkscape:window-x="1913"
inkscape:window-y="-6"
inkscape:window-maximized="1"
inkscape:current-layer="svg67" />
<path
d="m 59.903346,13.687732 v 6.602231 h 6.60223 v 19.806691 h -6.60223 v 6.602231 H 40.096654 v -6.602231 h -6.60223 V 20.289963 h 6.60223 v -6.602231 z"
fill="#000000"
id="path59"
style="stroke-width:1.65056;filter:url(#filter209)" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 33.494424,0.48327138 V 7.0855019 h -6.602231 v 6.6022301 h -6.60223 v 33.011153 h 6.60223 v 6.60223 h 6.602231 v 6.602231 h -6.602231 v 39.613383 h 6.602231 v -6.602231 h 6.60223 v -6.60223 h 4.951673 v -6.602231 h 6.602231 v 6.602231 h 6.60223 v 6.60223 h 6.602231 v 6.602231 h 6.60223 V 59.903346 h -4.951673 v -6.602231 h 6.602231 v -6.60223 h 6.60223 V 13.687732 h -6.60223 V 7.0855019 H 66.505576 V 0.48327138 Z M 58.252788,86.312268 v -6.602231 h -6.60223 v -6.60223 h -6.602231 v 6.60223 h -6.60223 v 6.602231 H 33.494424 V 59.903346 H 64.855019 V 86.312268 Z M 59.903346,7.0855019 H 40.096654 v 6.6022301 h -6.60223 v 6.602231 h -6.602231 v 19.806691 h 6.602231 v 6.602231 h 6.60223 v 6.60223 h 19.806692 v -6.60223 h 6.60223 v -6.602231 h 6.602231 V 20.289963 h -6.602231 v -6.602231 h -6.60223 z"
fill="#000000"
id="path61"
style="stroke-width:1.65056;filter:url(#filter203)" />
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path d="m59.903346 13.687732v6.602231h6.60223v19.806691h-6.60223v6.602231H40.096654v-6.602231h-6.60223V20.289963h6.60223v-6.602231z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.494424.48327138V7.0855019h-6.602231v6.6022301h-6.60223v33.011153h6.60223v6.60223h6.602231v6.602231h-6.602231v39.613383h6.602231v-6.602231h6.60223v-6.60223h4.951673v-6.602231h6.602231v6.602231h6.60223v6.60223h6.602231v6.602231h6.60223V59.903346h-4.951673v-6.602231h6.602231v-6.60223h6.60223V13.687732h-6.60223V7.0855019H66.505576V.48327138ZM58.252788 86.312268v-6.602231h-6.60223v-6.60223h-6.602231v6.60223h-6.60223v6.602231H33.494424V59.903346H64.855019V86.312268ZM59.903346 7.0855019H40.096654v6.6022301h-6.60223v6.602231h-6.602231v19.806691h6.602231v6.602231h6.60223v6.60223h19.806692v-6.60223h6.60223v-6.602231h6.602231V20.289963h-6.602231v-6.602231h-6.60223z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 955 B

Before

Width:  |  Height:  |  Size: 243 B

After

Width:  |  Height:  |  Size: 243 B

Before

Width:  |  Height:  |  Size: 284 B

After

Width:  |  Height:  |  Size: 284 B

Before

Width:  |  Height:  |  Size: 210 B

After

Width:  |  Height:  |  Size: 210 B

+116 -2
View File
@@ -16,6 +16,7 @@
"summary_send": "Send",
"summary_keep": "Keep",
"cancel": "Cancel",
"confirm": "Confirm",
"send": "Send",
"cap_label": "Cap",
"cap_tooltip": "Recipients remaining capacity",
@@ -49,6 +50,7 @@
"leaderboard": "Leaderboard",
"account": "Account",
"help": "Help",
"clans": "Clans",
"menu": "Menu",
"troubleshooting": "Troubleshooting",
"go_to_troubleshooting": "Go to our troubleshooting page"
@@ -222,6 +224,115 @@
"logging_in": "Logging in...",
"success": "Successfully logged in as {email}!"
},
"clan_modal": {
"title": "Clans",
"my_clans": "My Clans",
"browse": "Browse",
"no_clans": "You're not in any clans yet.",
"sign_in_for_clans": "Sign in to join and manage clans",
"request_pending": "Request Pending",
"search_placeholder": "Search by clan tag...",
"no_results": "No clans found.",
"invite_only": "Invite Only",
"members": "Members",
"status": "Status",
"open": "Open",
"join_clan": "Join Clan",
"request_invite": "Request Invite",
"leave_clan": "Leave Clan",
"manage_clan": "Manage",
"transfer_leadership": "Transfer Leadership",
"clan_name": "Clan Name",
"description": "Description",
"open_clan": "Open Clan",
"open_clan_desc": "Anyone can join without an invite",
"clan_settings": "Clan Settings",
"save_changes": "Save Changes",
"promote": "Promote",
"demote": "Demote",
"kick": "Kick",
"danger_zone": "Danger Zone",
"disband_clan": "Disband Clan",
"transfer_warning": "This will make the selected member the new leader. You will become a regular member. This action cannot be undone.",
"confirm_transfer": "Transfer leadership to {name}",
"select_new_leader": "Select a new leader",
"search_members_placeholder": "Filter current page by ID or role...",
"search_requests_placeholder": "Search by player public ID...",
"per_page": "Per page",
"sort_by": "Sort by",
"sort_default": "Role",
"sort_total_wins": "Total Wins",
"sort_total_losses": "Total Losses",
"sort_ffa_wins": "FFA Wins",
"sort_ffa_losses": "FFA Losses",
"sort_team_wins": "Team Wins",
"sort_team_losses": "Team Losses",
"sort_hvn_wins": "HvN Wins",
"sort_hvn_losses": "HvN Losses",
"sort_ranked_wins": "Ranked Wins",
"sort_ranked_losses": "Ranked Losses",
"sort_1v1_wins": "1v1 Wins",
"sort_1v1_losses": "1v1 Losses",
"sort_order_asc": "Ascending",
"sort_order_desc": "Descending",
"join_requests": "Join Requests",
"no_requests": "No pending join requests.",
"pending_requests_count": "{count, plural, one {# pending request} other {# pending requests}}",
"approve": "Approve",
"deny": "Deny",
"requested_on": "Requested to join [{tag}] on {date}.",
"pending_applications": "Pending Applications",
"no_pending_applications": "No pending applications.",
"applied": "Applied",
"cancel_request": "Cancel",
"statistics": "Statistics",
"stats_total": "Total",
"stats_ffa": "FFA",
"stats_team": "Teams",
"stats_hvn": "HvN",
"stats_ranked": "Ranked",
"stats_1v1": "1v1",
"no_description": "No description",
"saving": "Saving...",
"join_request_cancelled": "Join request cancelled.",
"failed_to_load_clan": "Failed to load clan",
"join_request_sent": "Join request sent! Waiting for approval.",
"left_clan": "You left the clan.",
"settings_saved": "Clan settings saved!",
"clan_disbanded": "Clan disbanded.",
"member_promoted": "Member promoted!",
"member_demoted": "Member demoted.",
"member_kicked": "Member kicked.",
"leadership_transferred": "Leadership transferred!",
"failed_to_load_requests": "Failed to load requests",
"request_approved": "Request approved!",
"request_denied": "Request denied.",
"ban": "Ban",
"unban": "Unban",
"banned_players": "Banned Players",
"no_bans": "No banned players.",
"ban_reason_prompt": "Ban reason (optional, max 200 characters):",
"confirm_ban": "Are you sure you want to ban this player? They will be removed from the clan and unable to rejoin.",
"member_banned": "Player banned.",
"member_unbanned": "Player unbanned.",
"banned_by_label": "by",
"ban_reason": "Reason: {reason}",
"error_banned": "You are banned from this clan.",
"error_banned_reason": "You are banned from this clan. Reason: {reason}",
"confirm_kick": "Are you sure you want to kick this member?",
"confirm_disband": "Are you sure you want to disband [{tag}] {name}? This cannot be undone.",
"joined_date": "Member since {date}.",
"member_count": "{count, plural, one {# member} other {# members}}",
"role_leader": "Leader",
"role_officer": "Officer",
"role_member": "Member",
"error_already_member": "Already a member",
"error_request_pending": "Join request already pending",
"error_rate_limited_generic": "Please wait before joining another clan",
"error_network": "Network error",
"error_failed": "Action failed",
"error_loading": "Failed to load"
},
"account_modal": {
"title": "Account",
"connected_as": "Connected as",
@@ -298,6 +409,7 @@
"giantworldmap": "Giant World Map",
"europe": "Europe",
"mena": "MENA",
"middleeast": "Middle East",
"northamerica": "North America",
"oceania": "Oceania",
"blacksea": "Black Sea",
@@ -575,9 +687,9 @@
"left_click_label": "Left Click to Open Menu",
"left_click_desc": "When ON, left-click opens menu and sword button attacks. When OFF, left-click attacks directly.",
"left_click_menu": "Left Click Menu",
"attack_ratio_label": "⚔️ Attack Ratio",
"attack_ratio_label": "Attack Ratio",
"attack_ratio_desc": "What percentage of your troops to send in an attack (1100%)",
"territory_patterns_label": "🏳️ Territory Skins",
"territory_patterns_label": "Territory Skins",
"territory_patterns_desc": "Choose whether to display territory skin designs in game",
"coordinate_grid_label": "Coordinate Grid",
"coordinate_grid_desc": "Toggle the alphanumeric grid overlay",
@@ -637,6 +749,8 @@
"boat_attack_desc": "Send a boat attack to the tile under your cursor.",
"ground_attack": "Ground Attack",
"ground_attack_desc": "Send a ground attack to the tile under your cursor.",
"retaliate_attack": "Retaliate",
"retaliate_attack_desc": "Send a retaliation attack to blunt/negate the force of the most recent active attacker. Only available when you are being attacked.",
"ally_keybinds": "Ally Keybinds",
"request_alliance": "Request Alliance",
"request_alliance_desc": "Send an alliance request to the player whose tile is under your cursor.",
+79 -29
View File
@@ -1,80 +1,130 @@
{
"map": {
"height": 612,
"num_land_tiles": 387974,
"num_land_tiles": 382332,
"width": 1000
},
"map16x": {
"height": 153,
"num_land_tiles": 22991,
"num_land_tiles": 22523,
"width": 250
},
"map4x": {
"height": 306,
"num_land_tiles": 95304,
"num_land_tiles": 93711,
"width": 500
},
"name": "Bosphorus Straits",
"nations": [
{
"coordinates": [520, 300],
"coordinates": [564, 245],
"flag": "tr",
"name": "Istanbul"
"name": "Beykoz"
},
{
"coordinates": [360, 280],
"coordinates": [820, 209],
"flag": "tr",
"name": "Thrace"
"name": "Sile"
},
{
"coordinates": [220, 260],
"coordinates": [700, 316],
"flag": "tr",
"name": "Edirne"
"name": "Çekmeköy"
},
{
"coordinates": [650, 360],
"coordinates": [800, 438],
"flag": "tr",
"name": "Bursa"
"name": "Pendik"
},
{
"coordinates": [690, 290],
"coordinates": [797, 566],
"flag": "tr",
"name": "Izmit"
"name": "Tuzla"
},
{
"coordinates": [430, 430],
"coordinates": [486, 381],
"flag": "tr",
"name": "Canakkale"
"name": "Üsküdar"
},
{
"coordinates": [320, 330],
"coordinates": [534, 425],
"flag": "tr",
"name": "Tekirdag"
"name": "Kadıköy"
},
{
"coordinates": [610, 320],
"coordinates": [559, 568],
"flag": "tr",
"name": "Yalova"
"name": "Adalar"
},
{
"coordinates": [720, 260],
"coordinates": [635, 500],
"flag": "tr",
"name": "Maltepe"
},
{
"coordinates": [701, 423],
"flag": "tr",
"name": "Sancaktepe"
},
{
"coordinates": [424, 394],
"flag": "tr",
"name": "Fatih"
},
{
"coordinates": [128, 46],
"flag": "tr",
"name": "Arnavutköy"
},
{
"coordinates": [965, 544],
"flag": "tr",
"name": "Kocaeli"
},
{
"coordinates": [160, 120],
"flag": "bg",
"name": "Varna"
"coordinates": [42, 325],
"flag": "tr",
"name": "Büyükçekmece"
},
{
"coordinates": [220, 150],
"flag": "bg",
"name": "Burgas"
"coordinates": [336, 175],
"flag": "tr",
"name": "Eyüpsultan"
},
{
"coordinates": [820, 470],
"flag": "gr",
"name": "Aegean Isles"
"coordinates": [459, 157],
"flag": "tr",
"name": "Sarıyer"
},
{
"coordinates": [477, 297],
"flag": "tr",
"name": "Beşiktaş"
},
{
"coordinates": [171, 379],
"flag": "tr",
"name": "Avcılar"
},
{
"coordinates": [308, 412],
"flag": "tr",
"name": "Bakırköy"
},
{
"coordinates": [263, 283],
"flag": "tr",
"name": "Başakşehir"
},
{
"coordinates": [402, 272],
"flag": "tr",
"name": "Sultangazi"
},
{
"coordinates": [130, 270],
"flag": "tr",
"name": "Esenyurt"
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

+10 -10
View File
@@ -6,19 +6,19 @@
},
"map16x": {
"height": 336,
"num_land_tiles": 108584,
"num_land_tiles": 108613,
"width": 770
},
"map4x": {
"height": 672,
"num_land_tiles": 458859,
"num_land_tiles": 459895,
"width": 1540
},
"name": "Dyslexdria",
"nations": [
{
"coordinates": [2260, 400],
"flag": "west_germany",
"flag": "de",
"name": "Jermaine"
},
{
@@ -28,7 +28,7 @@
},
{
"coordinates": [2050, 460],
"flag": "Fascist Spain",
"flag": "es",
"name": "Splain"
},
{
@@ -48,12 +48,12 @@
},
{
"coordinates": [2342, 436],
"flag": "Communist Romania",
"flag": "ro",
"name": "Rollandia"
},
{
"coordinates": [1222, 310],
"flag": "Ukrainian SSR",
"flag": "ua",
"name": "Ucryin"
},
{
@@ -78,7 +78,7 @@
},
{
"coordinates": [2555, 475],
"flag": "Georgian SSR",
"flag": "ge",
"name": "Georgia"
},
{
@@ -108,7 +108,7 @@
},
{
"coordinates": [1260, 400],
"flag": "Kazakh SSR",
"flag": "kz",
"name": "Azakah"
},
{
@@ -248,7 +248,7 @@
},
{
"coordinates": [2950, 1150],
"flag": "Apartheid South Africa",
"flag": "za",
"name": "Southern African State"
},
{
@@ -258,7 +258,7 @@
},
{
"coordinates": [2860, 1050],
"flag": "Apartheid South Africa",
"flag": "za",
"name": "Southern South West Africa"
},
{
+80 -70
View File
@@ -19,7 +19,7 @@
{
"coordinates": [634, 781],
"flag": "ie",
"name": "Republic of Ireland"
"name": "Ireland"
},
{
"coordinates": [962, 780],
@@ -27,79 +27,79 @@
"name": "England"
},
{
"coordinates": [935, 1289],
"coordinates": [874, 1286],
"flag": "es",
"name": "Kingdom of Spain"
"name": "Spain"
},
{
"coordinates": [1087, 931],
"coordinates": [1087, 983],
"flag": "fr",
"name": "French Republic"
"name": "France"
},
{
"coordinates": [1541, 1180],
"flag": "it",
"name": "Italian Republic"
"name": "Italy"
},
{
"coordinates": [1339, 983],
"coordinates": [1339, 997],
"flag": "ch",
"name": "Swiss Confederation"
"name": "Switzerland"
},
{
"coordinates": [1360, 428],
"flag": "no",
"name": "Kingdom of Norway"
"name": "Norway"
},
{
"coordinates": [1605, 573],
"coordinates": [1609, 477],
"flag": "se",
"name": "Kingdom of Sweden"
"name": "Sweden"
},
{
"coordinates": [2007, 309],
"coordinates": [2032, 346],
"flag": "fi",
"name": "Republic of Finland"
"name": "Finland"
},
{
"coordinates": [1200, 830],
"coordinates": [1182, 836],
"flag": "be",
"name": "Kingdom of Belgium"
"name": "Belgium"
},
{
"coordinates": [1264, 752],
"coordinates": [1268, 764],
"flag": "nl",
"name": "Kingdom of the Netherlands"
"name": "Netherlands"
},
{
"coordinates": [1443, 798],
"coordinates": [1445, 821],
"flag": "de",
"name": "Federal Republic of Germany"
"name": "Germany"
},
{
"coordinates": [1444, 969],
"coordinates": [1507, 959],
"flag": "at",
"name": "Republic of Austria"
"name": "Austria"
},
{
"coordinates": [1850, 810],
"coordinates": [1751, 772],
"flag": "pl",
"name": "Republic of Poland"
"name": "Poland"
},
{
"coordinates": [1630, 909],
"coordinates": [1618, 874],
"flag": "cz",
"name": "Czech Republic"
"name": "Czechia"
},
{
"coordinates": [2342, 936],
"coordinates": [2231, 885],
"flag": "ua",
"name": "Ukraine"
},
{
"coordinates": [2167, 708],
"coordinates": [2206, 676],
"flag": "by",
"name": "Republic of Belarus"
"name": "Belarus"
},
{
"coordinates": [2046, 990],
@@ -107,29 +107,29 @@
"name": "Romania"
},
{
"coordinates": [2432, 1265],
"coordinates": [2334, 1313],
"flag": "tr",
"name": "Republic of Turkiye"
"name": "rkiye"
},
{
"coordinates": [769, 1535],
"flag": "ma",
"name": "Kingdom of Morocco"
"name": "Morocco"
},
{
"coordinates": [2535, 720],
"coordinates": [2591, 610],
"flag": "ru",
"name": "Russian Federation"
"name": "Russia"
},
{
"coordinates": [2539, 1455],
"coordinates": [2538, 1418],
"flag": "sy",
"name": "Syrian Arab Republic"
"name": "Syria"
},
{
"coordinates": [2689, 1441],
"flag": "iq",
"name": "Republic of Iraq"
"name": "Iraq"
},
{
"coordinates": [2748, 1138],
@@ -137,9 +137,9 @@
"name": "Georgia"
},
{
"coordinates": [1389, 1473],
"coordinates": [1359, 1473],
"flag": "tn",
"name": "Republic of Tunisia"
"name": "Tunisia"
},
{
"coordinates": [1066, 1506],
@@ -149,62 +149,62 @@
{
"coordinates": [680, 1254],
"flag": "pt",
"name": "Portuguese Republic"
"name": "Portugal"
},
{
"coordinates": [1891, 1299],
"flag": "gr",
"name": "Hellenic Republic"
"name": "Greece"
},
{
"coordinates": [1906, 1113],
"flag": "rs",
"name": "Republic of Serbia"
"name": "Serbia"
},
{
"coordinates": [1751, 983],
"flag": "hu",
"name": "Republic of Hungary"
"name": "Hungary"
},
{
"coordinates": [1784, 908],
"coordinates": [1826, 902],
"flag": "sk",
"name": "Slovak Republic"
"name": "Slovakia"
},
{
"coordinates": [1624, 1038],
"flag": "hr",
"name": "Republic of Croatia"
"name": "Croatia"
},
{
"coordinates": [1734, 1094],
"coordinates": [1738, 1107],
"flag": "ba",
"name": "Bosnia and Herzegovina"
},
{
"coordinates": [1817, 1213],
"flag": "al",
"name": "Republic of Albania"
"name": "Albania"
},
{
"coordinates": [2092, 1158],
"flag": "bg",
"name": "Republic of Bulgaria"
"name": "Bulgaria"
},
{
"coordinates": [1939, 702],
"coordinates": [1958, 704],
"flag": "lt",
"name": "Republic of Lithuania"
"name": "Lithuania"
},
{
"coordinates": [2014, 618],
"coordinates": [2030, 617],
"flag": "lv",
"name": "Republic of Latvia"
"name": "Latvia"
},
{
"coordinates": [2033, 504],
"coordinates": [2052, 516],
"flag": "ee",
"name": "Republic of Estonia"
"name": "Estonia"
},
{
"coordinates": [863, 775],
@@ -212,14 +212,14 @@
"name": "Wales"
},
{
"coordinates": [863, 573],
"coordinates": [841, 603],
"flag": "gb-sct",
"name": "Scotland"
},
{
"coordinates": [2688, 427],
"flag": "ussr",
"name": "USSR"
"coordinates": [2890, 857],
"flag": "kz",
"name": "Kazakhstan"
},
{
"coordinates": [719, 685],
@@ -227,44 +227,54 @@
"name": "Northern Ireland"
},
{
"coordinates": [2011, 103],
"flag": "polar_bears",
"name": "Polar Bears"
"coordinates": [1900, 132],
"flag": "Sami flag",
"name": "Sápmi"
},
{
"coordinates": [1369, 628],
"coordinates": [1377, 625],
"flag": "dk",
"name": "Kingdom of Denmark"
"name": "Denmark"
},
{
"coordinates": [2406, 1638],
"flag": "il",
"name": "State of Israel"
"name": "Israel"
},
{
"coordinates": [2226, 1661],
"flag": "eg",
"name": "Arab Republic of Egypt"
"name": "Egypt"
},
{
"coordinates": [1847, 1652],
"flag": "ly",
"name": "State of Libya"
"name": "Libya"
},
{
"coordinates": [2571, 1601],
"coordinates": [2535, 1609],
"flag": "jo",
"name": "Hashemite Kingdom of Jordan"
"name": "Jordan"
},
{
"coordinates": [2473, 1528],
"flag": "lb",
"name": "Lebanese Republic"
"name": "Lebanon"
},
{
"coordinates": [254, 274],
"coordinates": [266, 265],
"flag": "is",
"name": "Iceland"
},
{
"coordinates": [1045, 1188],
"flag": "ad",
"name": "Andorra"
},
{
"coordinates": [1290, 1121],
"flag": "mc",
"name": "Monaco"
}
]
}
+49 -19
View File
@@ -27,17 +27,17 @@
"name": "Ireland"
},
{
"coordinates": [650, 500],
"coordinates": [650, 477],
"flag": "gb",
"name": "United Kingdom"
},
{
"coordinates": [560, 800],
"coordinates": [612, 809],
"flag": "es",
"name": "Spain"
},
{
"coordinates": [726, 616],
"coordinates": [729, 648],
"flag": "fr",
"name": "France"
},
@@ -47,32 +47,32 @@
"name": "Italy"
},
{
"coordinates": [872, 634],
"coordinates": [895, 641],
"flag": "ch",
"name": "Switzerland"
},
{
"coordinates": [960, 271],
"coordinates": [935, 259],
"flag": "no",
"name": "Norway"
},
{
"coordinates": [1095, 336],
"coordinates": [1105, 286],
"flag": "se",
"name": "Sweden"
},
{
"coordinates": [1403, 235],
"coordinates": [1438, 209],
"flag": "fi",
"name": "Finland"
},
{
"coordinates": [775, 541],
"coordinates": [819, 534],
"flag": "be",
"name": "Belgium"
},
{
"coordinates": [868, 487],
"coordinates": [868, 485],
"flag": "nl",
"name": "Netherlands"
},
@@ -87,14 +87,14 @@
"name": "Austria"
},
{
"coordinates": [1120, 477],
"coordinates": [1220, 491],
"flag": "pl",
"name": "Poland"
},
{
"coordinates": [1060, 530],
"coordinates": [1078, 564],
"flag": "cz",
"name": "Czech Republic"
"name": "Czechia"
},
{
"coordinates": [1540, 602],
@@ -102,19 +102,19 @@
"name": "Ukraine"
},
{
"coordinates": [1500, 440],
"coordinates": [1517, 424],
"flag": "by",
"name": "Belarus"
},
{
"coordinates": [1400, 670],
"coordinates": [1414, 667],
"flag": "ro",
"name": "Romania"
},
{
"coordinates": [1580, 834],
"coordinates": [1614, 834],
"flag": "tr",
"name": "Turkey"
"name": "Türkiye"
},
{
"coordinates": [525, 955],
@@ -122,14 +122,14 @@
"name": "Morocco"
},
{
"coordinates": [1674, 449],
"coordinates": [1771, 413],
"flag": "ru",
"name": "Russia"
},
{
"coordinates": [1750, 950],
"flag": "sy",
"name": "Syrian Arab Republic"
"name": "Syria"
},
{
"coordinates": [1930, 950],
@@ -167,9 +167,39 @@
"name": "Serbia"
},
{
"coordinates": [1200, 630],
"coordinates": [1218, 602],
"flag": "hu",
"name": "Hungary"
},
{
"coordinates": [1277, 90],
"flag": "Sami flag",
"name": "Sápmi"
},
{
"coordinates": [1406, 324],
"flag": "ee",
"name": "Estonia"
},
{
"coordinates": [1380, 384],
"flag": "lv",
"name": "Latvia"
},
{
"coordinates": [1355, 444],
"flag": "lt",
"name": "Lithuania"
},
{
"coordinates": [1121, 668],
"flag": "hr",
"name": "Croatia"
},
{
"coordinates": [959, 394],
"flag": "dk",
"name": "Denmark"
}
]
}
@@ -17,154 +17,139 @@
"name": "GatewayToTheAtlantic",
"nations": [
{
"coordinates": [2144, 344],
"coordinates": [2161, 420],
"flag": "ch",
"name": "Swiss Confederation"
},
{
"coordinates": [1964, 371],
"coordinates": [1938, 263],
"flag": "burgundy",
"name": "Kingdom of Burgundy"
"name": "Duchy of Burgundy"
},
{
"coordinates": [1334, 537],
"coordinates": [1388, 485],
"flag": "aquitaine",
"name": "Duchy of Aquitaine"
},
{
"coordinates": [2115, 684],
"coordinates": [2137, 636],
"flag": "provence",
"name": "County of Provence"
},
{
"coordinates": [1207, 763],
"flag": "es-pv",
"name": "The Basque"
"coordinates": [1266, 748],
"flag": "navarre",
"name": "Kingdom of Navarre"
},
{
"coordinates": [1281, 1142],
"coordinates": [1375, 1190],
"flag": "valencia",
"name": "Kingdom of Valencia"
},
{
"coordinates": [1660, 891],
"coordinates": [1696, 858],
"flag": "catalonia",
"name": "Catalonia"
},
{
"coordinates": [561, 764],
"coordinates": [543, 807],
"flag": "es-ga",
"name": "Kingdom of Galicia"
},
{
"coordinates": [1004, 1436],
"coordinates": [1128, 1388],
"flag": "granada",
"name": "Emirate of Granada"
},
{
"coordinates": [431, 1197],
"flag": "pt",
"name": "Portuguese Republic"
"coordinates": [523, 1156],
"flag": "kingdom_of_portugal",
"name": "Kingdom of Portugal"
},
{
"coordinates": [560, 1894],
"flag": "ma",
"name": "Kingdom of Morocco"
"coordinates": [851, 1805],
"flag": "marinid",
"name": "Marinid Sultanate"
},
{
"coordinates": [1609, 1837],
"flag": "dz",
"name": "Algeria"
"coordinates": [1424, 1725],
"flag": "zayyanid",
"name": "Zayyanid Sultanate"
},
{
"coordinates": [1733, 622],
"coordinates": [1604, 641],
"flag": "armagnac",
"name": "County of Armagnac"
},
{
"coordinates": [896, 1240],
"coordinates": [946, 1300],
"flag": "cordoba",
"name": "City of Cordoba"
},
{
"coordinates": [636, 1781],
"flag": "seville",
"name": "City of Seville"
},
{
"coordinates": [750, 873],
"coordinates": [740, 1021],
"flag": "leon",
"name": "Kingdom of Leon"
},
{
"coordinates": [1001, 882],
"coordinates": [1040, 1036],
"flag": "castille",
"name": "Kingdom of Castille"
"name": "Crown of Castile"
},
{
"coordinates": [775, 724],
"coordinates": [847, 767],
"flag": "asturias",
"name": "Principality of Asturias"
},
{
"coordinates": [1755, 1130],
"flag": "neuragic_empire",
"name": "The Old Ones"
"flag": "majorca",
"name": "Kingdom of Majorca"
},
{
"coordinates": [2097, 1670],
"flag": "Amazigh flag",
"name": "Tamazgha"
"coordinates": [2004, 1630],
"flag": "hafsid",
"name": "Hafsid Sultanate"
},
{
"coordinates": [979, 1013],
"flag": "es",
"name": "Kingdom of Spain"
"coordinates": [1374, 926],
"flag": "catalonia",
"name": "Crown of Aragon"
},
{
"coordinates": [468, 930],
"flag": "sardines",
"name": "Sardines"
},
{
"coordinates": [1667, 96],
"coordinates": [1695, 119],
"flag": "paris",
"name": "City of Paris"
},
{
"coordinates": [1716, 296],
"flag": "baguette",
"name": "Baguettes"
},
{
"coordinates": [1017, 180],
"coordinates": [1121, 221],
"flag": "brittany",
"name": "Kingdom of Brittany"
},
{
"coordinates": [2072, 567],
"flag": "antipope",
"name": "An Anti-Pope"
"coordinates": [1933, 614],
"flag": "avignon",
"name": "City of Avignon"
},
{
"coordinates": [1355, 76],
"coordinates": [1434, 129],
"flag": "normandy",
"name": "Duchy of Normandy"
},
{
"coordinates": [1402, 529],
"flag": "",
"name": "Wine"
"coordinates": [1644, 383],
"flag": "Franks",
"name": "Kingdom of France"
},
{
"coordinates": [1475, 1657],
"flag": "French foreign legion",
"name": "French Foreign Legion"
"coordinates": [772, 1399],
"flag": "seville",
"name": "City of Seville"
},
{
"coordinates": [1685, 417],
"flag": "fr",
"name": "French Republic"
"coordinates": [2147, 90],
"flag": "Holy Roman Empire",
"name": "Holy Roman Empire"
}
]
}
+147 -97
View File
@@ -22,14 +22,9 @@
"name": "Türkiye"
},
{
"coordinates": [2030, 409],
"flag": "west_germany",
"name": "West Germany"
},
{
"coordinates": [2074, 382],
"flag": "east_germany",
"name": "East Germany"
"coordinates": [2050, 395],
"flag": "de",
"name": "Germany"
},
{
"coordinates": [1966, 442],
@@ -38,7 +33,7 @@
},
{
"coordinates": [1872, 528],
"flag": "Fascist Spain",
"flag": "es",
"name": "Spain"
},
{
@@ -73,12 +68,12 @@
},
{
"coordinates": [2135, 481],
"flag": "yugoslavia",
"name": "Yugoslavia"
"flag": "rs",
"name": "Serbia"
},
{
"coordinates": [2242, 461],
"flag": "Communist Romania",
"flag": "ro",
"name": "Romania"
},
{
@@ -88,8 +83,8 @@
},
{
"coordinates": [2272, 418],
"flag": "Ukrainian SSR",
"name": "Ukrainian SSR"
"flag": "ua",
"name": "Ukraine"
},
{
"coordinates": [2093, 297],
@@ -104,7 +99,7 @@
{
"coordinates": [2191, 194],
"flag": "Sami flag",
"name": "Sapmi"
"name": "Sápmi"
},
{
"coordinates": [2206, 262],
@@ -113,22 +108,22 @@
},
{
"coordinates": [2376, 363],
"flag": "Russian SSR",
"name": "Russian SSR"
"flag": "ru",
"name": "Russia"
},
{
"coordinates": [2222, 371],
"flag": "Byelorussian SSR",
"name": "Byelorussian SSR"
"flag": "by",
"name": "Belarus"
},
{
"coordinates": [2441, 507],
"flag": "Georgian SSR",
"name": "Georgian SSR"
"flag": "ge",
"name": "Georgia"
},
{
"coordinates": [2402, 580],
"flag": "Second Republic of Iraq",
"flag": "iq",
"name": "Iraq"
},
{
@@ -142,14 +137,9 @@
"name": "Saudi Arabia"
},
{
"coordinates": [2434, 815],
"flag": "North yemen",
"name": "North Yemen"
},
{
"coordinates": [2479, 824],
"flag": "south yemen",
"name": "South Yemen"
"coordinates": [2456, 820],
"flag": "ye",
"name": "Yemen"
},
{
"coordinates": [2554, 724],
@@ -158,7 +148,7 @@
},
{
"coordinates": [2532, 609],
"flag": "Pahlavi Iran",
"flag": "ir",
"name": "Iran"
},
{
@@ -173,21 +163,16 @@
},
{
"coordinates": [2727, 416],
"flag": "Kazakh SSR",
"name": "Kazakh SSR"
"flag": "kz",
"name": "Kazakhstan"
},
{
"coordinates": [2556, 544],
"flag": "Turkmen SSR",
"name": "Turkmen SSR"
"flag": "tm",
"name": "Turkmenistan"
},
{
"coordinates": [2947, 362],
"flag": "Zheleznogorsk",
"name": "Zheleznogorsk"
},
{
"coordinates": [3252, 229],
"coordinates": [3255, 278],
"flag": "Siberia",
"name": "Siberia"
},
@@ -207,12 +192,12 @@
"name": "Bangladesh"
},
{
"coordinates": [2868, 635],
"coordinates": [2870, 656],
"flag": "np",
"name": "Nepal"
},
{
"coordinates": [3254, 672],
"coordinates": [3172, 624],
"flag": "cn",
"name": "China"
},
@@ -242,24 +227,19 @@
"name": "Indonesia"
},
{
"coordinates": [3121, 755],
"coordinates": [3145, 802],
"flag": "vn",
"name": "North Vietnam"
"name": "Vietnam"
},
{
"coordinates": [3153, 833],
"flag": "South Vietnam",
"name": "South Vietnam"
"coordinates": [3020, 750],
"flag": "mm",
"name": "Myanmar"
},
{
"coordinates": [3013, 722],
"flag": "Burma2",
"name": "Burma"
},
{
"coordinates": [3095, 822],
"flag": "kh",
"name": "Cambodia"
"coordinates": [3082, 822],
"flag": "th",
"name": "Thailand"
},
{
"coordinates": [3538, 1067],
@@ -267,12 +247,12 @@
"name": "Papua New Guinea"
},
{
"coordinates": [3542, 1356],
"coordinates": [3570, 1392],
"flag": "au",
"name": "Australia"
},
{
"coordinates": [3422, 1203],
"coordinates": [3416, 1213],
"flag": "Australian Aboriginal Flag",
"name": "Nawan-mirri"
},
@@ -292,14 +272,14 @@
"name": "Tunisia"
},
{
"coordinates": [2116, 653],
"coordinates": [2138, 675],
"flag": "ly",
"name": "Libya"
},
{
"coordinates": [2281, 653],
"flag": "United Arab Republic",
"name": "United Arab Republic"
"flag": "eg",
"name": "Egypt"
},
{
"coordinates": [1859, 613],
@@ -307,12 +287,12 @@
"name": "Morocco"
},
{
"coordinates": [1943, 615],
"coordinates": [1952, 671],
"flag": "dz",
"name": "Algeria"
},
{
"coordinates": [2317, 754],
"coordinates": [2271, 788],
"flag": "sd",
"name": "Sudan"
},
@@ -323,26 +303,26 @@
},
{
"coordinates": [2352, 895],
"flag": "Imperial Ethiopia",
"flag": "et",
"name": "Ethiopia"
},
{
"coordinates": [1790, 729],
"flag": "Mauritania",
"flag": "mr",
"name": "Mauritania"
},
{
"coordinates": [2154, 764],
"coordinates": [2118, 768],
"flag": "td",
"name": "Chad"
},
{
"coordinates": [2051, 745],
"coordinates": [2009, 810],
"flag": "ne",
"name": "Niger"
},
{
"coordinates": [2040, 930],
"coordinates": [1996, 909],
"flag": "ng",
"name": "Nigeria"
},
@@ -358,12 +338,12 @@
},
{
"coordinates": [2197, 1070],
"flag": "Zaire",
"name": "Zaire"
"flag": "cd",
"name": "DR Congo"
},
{
"coordinates": [2189, 1372],
"flag": "Apartheid South Africa",
"coordinates": [2211, 1389],
"flag": "za",
"name": "South Africa"
},
{
@@ -382,14 +362,14 @@
"name": "Tanzania"
},
{
"coordinates": [1934, 762],
"coordinates": [1894, 789],
"flag": "ml",
"name": "Mali"
},
{
"coordinates": [2128, 1292],
"flag": "Apartheid South Africa",
"name": "South West Africa"
"flag": "na",
"name": "Namibia"
},
{
"coordinates": [2099, 1178],
@@ -397,37 +377,37 @@
"name": "Angola"
},
{
"coordinates": [1375, 1121],
"coordinates": [1418, 1167],
"flag": "br",
"name": "Brazil"
},
{
"coordinates": [1203, 1059],
"coordinates": [1225, 1065],
"flag": "amazonas",
"name": "Amazonas"
},
{
"coordinates": [1210, 1395],
"coordinates": [1243, 1428],
"flag": "ar",
"name": "Argentina"
},
{
"coordinates": [1107, 1419],
"coordinates": [1125, 1356],
"flag": "cl",
"name": "Chile"
},
{
"coordinates": [1064, 1114],
"coordinates": [1064, 1111],
"flag": "pe",
"name": "Peru"
},
{
"coordinates": [1065, 938],
"coordinates": [1083, 938],
"flag": "co",
"name": "Colombia"
},
{
"coordinates": [1192, 938],
"coordinates": [1183, 921],
"flag": "ve",
"name": "Venezuela"
},
@@ -437,17 +417,17 @@
"name": "Nicaragua"
},
{
"coordinates": [788, 744],
"coordinates": [764, 743],
"flag": "mx",
"name": "Mexico"
},
{
"coordinates": [1011, 555],
"coordinates": [1034, 556],
"flag": "us",
"name": "USA"
"name": "United States"
},
{
"coordinates": [800, 624],
"coordinates": [766, 623],
"flag": "Texas",
"name": "Texas"
},
@@ -457,22 +437,22 @@
"name": "California"
},
{
"coordinates": [703, 483],
"coordinates": [654, 530],
"flag": "Utah",
"name": "Utah"
},
{
"coordinates": [1077, 444],
"coordinates": [1079, 385],
"flag": "Quebec",
"name": "Quebec"
},
{
"coordinates": [1231, 395],
"flag": "Newfoundland",
"name": "Newfoundland"
"coordinates": [1211, 364],
"flag": "newfoundlandandlabrador",
"name": "Newfoundland and Labrador"
},
{
"coordinates": [967, 418],
"coordinates": [957, 406],
"flag": "ca",
"name": "Canada"
},
@@ -482,24 +462,94 @@
"name": "Alaska"
},
{
"coordinates": [741, 234],
"coordinates": [857, 232],
"flag": "Nunavut",
"name": "Nunavut"
},
{
"coordinates": [484, 256],
"coordinates": [475, 254],
"flag": "Yukon",
"name": "Yukon"
},
{
"coordinates": [1434, 223],
"coordinates": [1448, 137],
"flag": "gl",
"name": "Greenland"
},
{
"coordinates": [2247, 1229],
"flag": "Rhodesia",
"name": "Rhodesia"
"flag": "zw",
"name": "Zimbabwe"
},
{
"coordinates": [550, 438],
"flag": "Washington",
"name": "Washington"
},
{
"coordinates": [778, 518],
"flag": "Kansas",
"name": "Kansas"
},
{
"coordinates": [912, 613],
"flag": "Mississippi",
"name": "Mississippi"
},
{
"coordinates": [1208, 1219],
"flag": "bo",
"name": "Bolivia"
},
{
"coordinates": [2059, 1015],
"flag": "ga",
"name": "Gabon"
},
{
"coordinates": [689, 437],
"flag": "Montana",
"name": "Montana"
},
{
"coordinates": [517, 357],
"flag": "britishcolumbia",
"name": "British Columbia"
},
{
"coordinates": [618, 358],
"flag": "alberta",
"name": "Alberta"
},
{
"coordinates": [732, 360],
"flag": "saskatchewan",
"name": "Saskatchewan"
},
{
"coordinates": [834, 367],
"flag": "manitoba",
"name": "Manitoba"
},
{
"coordinates": [1108, 500],
"flag": "Massachusetts",
"name": "Massachusetts"
},
{
"coordinates": [845, 465],
"flag": "Minnesota",
"name": "Minnesota"
},
{
"coordinates": [960, 500],
"flag": "Michigan",
"name": "Michigan"
},
{
"coordinates": [655, 247],
"flag": "northwestterritories",
"name": "Northwest Territories"
}
]
}
+11 -6
View File
@@ -1,17 +1,17 @@
{
"map": {
"height": 2320,
"num_land_tiles": 2069756,
"height": 2276,
"num_land_tiles": 2066156,
"width": 1800
},
"map16x": {
"height": 580,
"height": 569,
"num_land_tiles": 123168,
"width": 450
},
"map4x": {
"height": 1160,
"num_land_tiles": 509370,
"height": 1138,
"num_land_tiles": 508470,
"width": 900
},
"name": "Los Angeles",
@@ -97,10 +97,15 @@
"name": "Skid Row"
},
{
"coordinates": [925, 935],
"coordinates": [965, 865],
"flag": "California",
"name": "Inglewood"
},
{
"coordinates": [815, 945],
"flag": "California",
"name": "L.A.X."
},
{
"coordinates": [1180, 1010],
"flag": "California",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

+18 -18
View File
@@ -1,17 +1,17 @@
{
"map": {
"height": 3500,
"num_land_tiles": 1646031,
"height": 3508,
"num_land_tiles": 1517614,
"width": 1308
},
"map16x": {
"height": 875,
"num_land_tiles": 93490,
"height": 877,
"num_land_tiles": 84692,
"width": 327
},
"map4x": {
"height": 1750,
"num_land_tiles": 396787,
"height": 1754,
"num_land_tiles": 364566,
"width": 654
},
"name": "Luna",
@@ -27,12 +27,12 @@
"name": "Apollo 14"
},
{
"coordinates": [780, 345],
"coordinates": [780, 340],
"flag": "us",
"name": "Apollo 15"
},
{
"coordinates": [825, 735],
"coordinates": [825, 705],
"flag": "us",
"name": "Apollo 11"
},
@@ -52,7 +52,7 @@
"name": "Surveyor 3"
},
{
"coordinates": [256, 148],
"coordinates": [250, 148],
"flag": "us",
"name": "Apollo 13"
},
@@ -62,7 +62,7 @@
"name": "Artemis II"
},
{
"coordinates": [510, 170],
"coordinates": [515, 170],
"flag": "Russian SSR",
"name": "Luna 17"
},
@@ -107,7 +107,7 @@
"name": "Chang'e 4"
},
{
"coordinates": [270, 2690],
"coordinates": [260, 268],
"flag": "cn",
"name": "Chang'e 5"
},
@@ -117,7 +117,7 @@
"name": "Chang'e 6"
},
{
"coordinates": [830, 735],
"coordinates": [830, 745],
"flag": "jp",
"name": "S.L.I.M."
},
@@ -127,34 +127,34 @@
"name": "Chandrayaan 3"
},
{
"coordinates": [732, 3490],
"coordinates": [732, 3493],
"flag": "in",
"name": "Chandrayaan 1"
},
{
"coordinates": [755, 3035],
"flag": "",
"name": "T▅▚░S▅cr▅░M▅l▅t▅r░B▅s▅"
"name": "T▆p░S▅cr▅t░M▊l▊t▅r░B▅s▅"
},
{
"coordinates": [628, 921],
"flag": "",
"name": "[]"
"name": ""
}
],
"teamGameSpawnAreas": {
"2": [
{
"height": 1750,
"height": 1754,
"width": 1308,
"x": 0,
"y": 0
},
{
"height": 1750,
"height": 1754,
"width": 1308,
"x": 0,
"y": 1750
"y": 1754
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

+155
View File
@@ -0,0 +1,155 @@
{
"map": {
"height": 2060,
"num_land_tiles": 3449078,
"width": 2200
},
"map16x": {
"height": 515,
"num_land_tiles": 211600,
"width": 550
},
"map4x": {
"height": 1030,
"num_land_tiles": 856603,
"width": 1100
},
"name": "Middle East",
"nations": [
{
"coordinates": [300, 65],
"flag": "tr",
"name": "Ottoman Empire"
},
{
"coordinates": [1639, 558],
"flag": "Persia",
"name": "Qajar Dynasty"
},
{
"coordinates": [1141, 797],
"flag": "Socialist_flag",
"name": "Emirate of Kuwait"
},
{
"coordinates": [1880, 1353],
"flag": "Socialist_flag",
"name": "Sultanate of Muscat"
},
{
"coordinates": [1703, 1402],
"flag": "White Flag",
"name": "Imamate of Oman"
},
{
"coordinates": [1592, 1239],
"flag": "Socialist_flag",
"name": "Trucial States"
},
{
"coordinates": [1129, 1875],
"flag": "gb",
"name": "Aden Protectorate"
},
{
"coordinates": [964, 1744],
"flag": "Kingdom of Yemen",
"name": "Kingdom of Yemen"
},
{
"coordinates": [844, 1655],
"flag": "Emirate of Asir",
"name": "Emirate of Asir"
},
{
"coordinates": [579, 1173],
"flag": "Arabia",
"name": "Kingdom of Hejaz"
},
{
"coordinates": [800, 1052],
"flag": "Rashidi Emirate",
"name": "Rashidi Emirate"
},
{
"coordinates": [1092, 1336],
"flag": "Sultanate of Nejd",
"name": "Sultanate of Nejd"
},
{
"coordinates": [1397, 1128],
"flag": "qa",
"name": "Qatar"
},
{
"coordinates": [973, 296],
"flag": "Kingdom of Iraq",
"name": "Kingdom of Iraq"
},
{
"coordinates": [554, 364],
"flag": "Kingdom of Syria",
"name": "Kingdom of Syria"
},
{
"coordinates": [423, 647],
"flag": "gb",
"name": "Palestine Mandate"
},
{
"coordinates": [100, 781],
"flag": "Kingdom of Egypt",
"name": "Kingdom of Egypt"
},
{
"coordinates": [159, 1530],
"flag": "gb",
"name": "Anglo-Egyptian Sudan"
},
{
"coordinates": [578, 1766],
"flag": "italy",
"name": "Italian Eritrea"
},
{
"coordinates": [401, 2005],
"flag": "Ethiopian Empire",
"name": "Ethiopian Empire"
},
{
"coordinates": [826, 2044],
"flag": "fr",
"name": "French Somaliland"
},
{
"coordinates": [1455, 902],
"flag": "gb",
"name": "British Bushehr"
},
{
"coordinates": [185, 375],
"flag": "gb",
"name": "British Cyprus"
},
{
"coordinates": [2127, 373],
"flag": "Emirate of Afghanistan",
"name": "Emirate of Afghanistan"
},
{
"coordinates": [2087, 925],
"flag": "gb",
"name": "Baluchistan Agency"
},
{
"coordinates": [932, 15],
"flag": "am",
"name": "Republic of Armenia"
},
{
"coordinates": [1671, 71],
"flag": "ru",
"name": "Russian State"
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+154 -54
View File
@@ -21,11 +21,6 @@
"flag": "Florida",
"name": "Florida"
},
{
"coordinates": [1010, 435],
"flag": "ca",
"name": "Canada"
},
{
"coordinates": [1250, 1130],
"flag": "mx",
@@ -47,47 +42,47 @@
"name": "Nicaragua"
},
{
"coordinates": [1734, 1403],
"coordinates": [1651, 1399],
"flag": "pa",
"name": "Panama"
},
{
"coordinates": [1821, 1395],
"coordinates": [1760, 1410],
"flag": "co",
"name": "Colombia"
},
{
"coordinates": [1896, 1379],
"coordinates": [1883, 1395],
"flag": "ve",
"name": "Venezuela"
},
{
"coordinates": [1725, 1180],
"coordinates": [1692, 1161],
"flag": "cu",
"name": "Cuba"
},
{
"coordinates": [467, 333],
"coordinates": [331, 341],
"flag": "Alaska",
"name": "Alaska"
},
{
"coordinates": [1154, 914],
"coordinates": [1062, 932],
"flag": "Arizona",
"name": "Arizona"
},
{
"coordinates": [1010, 865],
"coordinates": [940, 905],
"flag": "California",
"name": "California"
},
{
"coordinates": [1307, 863],
"coordinates": [1147, 843],
"flag": "Colorado",
"name": "Colorado"
},
{
"coordinates": [1673, 965],
"coordinates": [1593, 974],
"flag": "Georgia_US",
"name": "Georgia"
},
@@ -97,169 +92,274 @@
"name": "Hawaii"
},
{
"coordinates": [1120, 760],
"coordinates": [1000, 710],
"flag": "Idaho",
"name": "Idaho"
},
{
"coordinates": [1551, 813],
"coordinates": [1490, 789],
"flag": "Illinois",
"name": "Illinois"
},
{
"coordinates": [1412, 873],
"coordinates": [1335, 835],
"flag": "Kansas",
"name": "Kansas"
},
{
"coordinates": [1651, 880],
"coordinates": [1525, 872],
"flag": "Kentucky",
"name": "Kentucky"
},
{
"coordinates": [1514, 1007],
"coordinates": [1410, 984],
"flag": "Louisiana",
"name": "Louisiana"
},
{
"coordinates": [1884, 735],
"coordinates": [1854, 720],
"flag": "Maine",
"name": "Maine"
},
{
"coordinates": [1634, 764],
"coordinates": [1572, 762],
"flag": "Michigan",
"name": "Michigan"
},
{
"coordinates": [1556, 965],
"coordinates": [1472, 940],
"flag": "Mississippi",
"name": "Mississippi"
},
{
"coordinates": [1527, 728],
"coordinates": [1390, 688],
"flag": "Minnesota",
"name": "Minnesota"
},
{
"coordinates": [1529, 880],
"coordinates": [1427, 829],
"flag": "Missouri",
"name": "Missouri"
},
{
"coordinates": [1225, 754],
"coordinates": [1100, 677],
"flag": "Montana",
"name": "Montana"
},
{
"coordinates": [1413, 789],
"coordinates": [1263, 787],
"flag": "Nebraska",
"name": "Nebraska"
},
{
"coordinates": [1090, 852],
"coordinates": [946, 818],
"flag": "Nevada",
"name": "Nevada"
},
{
"coordinates": [1253, 933],
"coordinates": [1169, 932],
"flag": "New_Mexico",
"name": "New Mexico"
},
{
"coordinates": [1833, 792],
"coordinates": [1737, 768],
"flag": "New_York",
"name": "New York"
},
{
"coordinates": [1444, 716],
"coordinates": [1239, 670],
"flag": "North_Dakota",
"name": "North Dakota"
},
{
"coordinates": [1704, 812],
"coordinates": [1612, 816],
"flag": "Ohio",
"name": "Ohio"
},
{
"coordinates": [1397, 921],
"coordinates": [1296, 902],
"flag": "Oklahoma",
"name": "Oklahoma"
},
{
"coordinates": [976, 754],
"coordinates": [873, 757],
"flag": "Oregon",
"name": "Oregon"
},
{
"coordinates": [1752, 716],
"coordinates": [1686, 796],
"flag": "Pennsylvania",
"name": "Pennsylvania"
},
{
"coordinates": [1716, 937],
"coordinates": [1636, 934],
"flag": "South_Carolina",
"name": "South Carolina"
},
{
"coordinates": [1419, 753],
"coordinates": [1302, 721],
"flag": "South_Dakota",
"name": "South Dakota"
},
{
"coordinates": [1648, 981],
"coordinates": [1579, 900],
"flag": "Tennessee",
"name": "Tennessee"
},
{
"coordinates": [1407, 1005],
"coordinates": [1279, 1013],
"flag": "Texas",
"name": "Texas"
},
{
"coordinates": [1827, 742],
"coordinates": [1785, 727],
"flag": "Vermont",
"name": "Vermont"
},
{
"coordinates": [1767, 857],
"coordinates": [1652, 864],
"flag": "Virginia",
"name": "Virginia"
},
{
"coordinates": [994, 700],
"coordinates": [897, 673],
"flag": "Washington",
"name": "Washington"
},
{
"coordinates": [1261, 759],
"coordinates": [1118, 764],
"flag": "Wyoming",
"name": "Wyoming"
},
{
"coordinates": [1867, 561],
"coordinates": [1798, 592],
"flag": "Quebec",
"name": "Quebec"
},
{
"coordinates": [1738, 80],
"flag": "santa_claus",
"name": "Santa Claus"
"coordinates": [1080, 367],
"flag": "northwestterritories",
"name": "Northwest Territories"
},
{
"coordinates": [1189, 240],
"flag": "polar_bears",
"name": "Polar Bears"
},
{
"coordinates": [1480, 350],
"flag": "frost_giant",
"name": "Frost Giants"
"coordinates": [1440, 319],
"flag": "Nunavut",
"name": "Nunavut"
},
{
"coordinates": [2399, 171],
"flag": "gl",
"name": "Greenland"
},
{
"coordinates": [777, 382],
"flag": "Yukon",
"name": "Yukon"
},
{
"coordinates": [787, 561],
"flag": "britishcolumbia",
"name": "British Columbia"
},
{
"coordinates": [966, 521],
"flag": "alberta",
"name": "Alberta"
},
{
"coordinates": [1172, 569],
"flag": "saskatchewan",
"name": "Saskatchewan"
},
{
"coordinates": [1343, 507],
"flag": "manitoba",
"name": "Manitoba"
},
{
"coordinates": [1575, 621],
"flag": "ontario",
"name": "Ontario"
},
{
"coordinates": [1903, 671],
"flag": "newbrunswick",
"name": "New Brunswick"
},
{
"coordinates": [1924, 748],
"flag": "novascotia",
"name": "Nova Scotia"
},
{
"coordinates": [1983, 547],
"flag": "newfoundlandandlabrador",
"name": "Newfoundland and Labrador"
},
{
"coordinates": [1793, 1209],
"flag": "ht",
"name": "Haiti"
},
{
"coordinates": [1851, 1215],
"flag": "do",
"name": "Dominican Republic"
},
{
"coordinates": [1912, 1224],
"flag": "pr",
"name": "Puerto Rico"
},
{
"coordinates": [2020, 1422],
"flag": "gy",
"name": "Guyana"
},
{
"coordinates": [2785, 358],
"flag": "is",
"name": "Iceland"
},
{
"coordinates": [1975, 702],
"flag": "princeedwardisland",
"name": "Prince Edward Island"
},
{
"coordinates": [1037, 838],
"flag": "Utah",
"name": "Utah"
},
{
"coordinates": [1402, 772],
"flag": "Iowa",
"name": "Iowa"
},
{
"coordinates": [1685, 902],
"flag": "North_Carolina",
"name": "North Carolina"
},
{
"coordinates": [1417, 904],
"flag": "Arkansas",
"name": "Arkansas"
},
{
"coordinates": [1547, 819],
"flag": "Indiana",
"name": "Indiana"
},
{
"coordinates": [1528, 963],
"flag": "Alabama",
"name": "Alabama"
},
{
"coordinates": [1811, 782],
"flag": "Massachusetts",
"name": "Massachusetts"
}
]
}
+22 -72
View File
@@ -17,29 +17,29 @@
"name": "Oceania",
"nations": [
{
"coordinates": [718, 738],
"coordinates": [515, 690],
"flag": "au",
"name": "Australia"
},
{
"coordinates": [1050, 809],
"coordinates": [1010, 836],
"flag": "nz",
"name": "New Zealand"
},
{
"coordinates": [436, 407],
"flag": "tl",
"name": "Timor-Leste"
"name": "Timor Leste"
},
{
"coordinates": [182, 378],
"coordinates": [236, 336],
"flag": "id",
"name": "Indonesia"
},
{
"coordinates": [292, 243],
"flag": "bn",
"name": "Brunei Darussalam"
"name": "Brunei"
},
{
"coordinates": [152, 282],
@@ -47,34 +47,34 @@
"name": "Singapore"
},
{
"coordinates": [120, 261],
"coordinates": [115, 228],
"flag": "my",
"name": "Malaysia"
},
{
"coordinates": [106, 129],
"coordinates": [112, 113],
"flag": "th",
"name": "Thailand"
},
{
"coordinates": [51, 42],
"coordinates": [34, 23],
"flag": "mm",
"name": "Myanmar"
},
{
"coordinates": [158, 162],
"coordinates": [171, 153],
"flag": "kh",
"name": "Cambodia"
},
{
"coordinates": [182, 43],
"coordinates": [203, 105],
"flag": "vn",
"name": "Vietnam"
},
{
"coordinates": [143, 37],
"coordinates": [127, 42],
"flag": "la",
"name": "Lao PDR"
"name": "Laos"
},
{
"coordinates": [278, 18],
@@ -82,70 +82,35 @@
"name": "Hong Kong"
},
{
"coordinates": [359, 1],
"coordinates": [362, 9],
"flag": "tw",
"name": "Taiwan, Province of China"
"name": "Taiwan"
},
{
"coordinates": [366, 119],
"coordinates": [425, 194],
"flag": "ph",
"name": "Philippines"
},
{
"coordinates": [536, 207],
"flag": "pw",
"name": "Palau"
},
{
"coordinates": [834, 215],
"flag": "fm",
"name": "Micronesia"
},
{
"coordinates": [664, 113],
"flag": "gu",
"name": "Guam"
},
{
"coordinates": [1042, 317],
"flag": "mh",
"name": "Marshall Islands"
},
{
"coordinates": [799, 385],
"coordinates": [630, 384],
"flag": "pg",
"name": "Papua New Guinea"
},
{
"coordinates": [862, 442],
"coordinates": [855, 424],
"flag": "sb",
"name": "Solomon Islands"
},
{
"coordinates": [945, 497],
"flag": "vu",
"name": "Vanuatu"
},
{
"coordinates": [930, 574],
"coordinates": [925, 574],
"flag": "nc",
"name": "New Caledonia"
},
{
"coordinates": [1085, 526],
"coordinates": [1080, 528],
"flag": "fj",
"name": "Fiji"
},
{
"coordinates": [1169, 568],
"flag": "to",
"name": "Tonga"
},
{
"coordinates": [1236, 541],
"flag": "nu",
"name": "Niue"
},
{
"coordinates": [1204, 473],
"flag": "ws",
@@ -157,24 +122,9 @@
"name": "Cook Islands"
},
{
"coordinates": [1623, 424],
"flag": "pf",
"name": "French Polynesia"
},
{
"coordinates": [1393, 278],
"flag": "ki",
"name": "Kiribati"
},
{
"coordinates": [1420, 56],
"flag": "us",
"name": "United States"
},
{
"coordinates": [1996, 644],
"flag": "cl",
"name": "Chile"
"coordinates": [1413, 56],
"flag": "Hawaii",
"name": "Hawaii"
}
]
}
+30 -25
View File
@@ -32,12 +32,12 @@
"name": "Haiti"
},
{
"coordinates": [112, 209],
"coordinates": [122, 143],
"flag": "bz",
"name": "Belize"
},
{
"coordinates": [71, 282],
"coordinates": [68, 257],
"flag": "gt",
"name": "Guatemala"
},
@@ -57,84 +57,89 @@
"name": "Panama"
},
{
"coordinates": [740, 1180],
"coordinates": [847, 1209],
"flag": "bo",
"name": "Bolivia"
},
{
"coordinates": [849, 1770],
"coordinates": [861, 1851],
"flag": "ar",
"name": "Argentina"
},
{
"coordinates": [1394, 1309],
"coordinates": [1153, 1095],
"flag": "br",
"name": "Brazil"
},
{
"coordinates": [691, 1371],
"coordinates": [687, 1605],
"flag": "cl",
"name": "Chile"
},
{
"coordinates": [527, 503],
"coordinates": [582, 543],
"flag": "co",
"name": "Colombia"
},
{
"coordinates": [384, 746],
"coordinates": [444, 750],
"flag": "ec",
"name": "Ecuador"
},
{
"coordinates": [933, 423],
"coordinates": [1204, 615],
"flag": "gf",
"name": "French Guyana"
"name": "French Guiana"
},
{
"coordinates": [800, 410],
"coordinates": [981, 546],
"flag": "gy",
"name": "Guyana"
},
{
"coordinates": [541, 1092],
"coordinates": [537, 1017],
"flag": "pe",
"name": "Peru"
},
{
"coordinates": [960, 1496],
"coordinates": [1028, 1404],
"flag": "py",
"name": "Paraguay"
},
{
"coordinates": [890, 610],
"coordinates": [1097, 586],
"flag": "sr",
"name": "Suriname"
},
{
"coordinates": [1091, 1635],
"coordinates": [1118, 1695],
"flag": "uy",
"name": "Uruguay"
},
{
"coordinates": [678, 904],
"coordinates": [770, 465],
"flag": "ve",
"name": "Venezuela"
},
{
"coordinates": [1270, 1035],
"flag": "Aztec Empire",
"name": "The Biggest Snakes"
"coordinates": [795, 808],
"flag": "amazonas",
"name": "Amazonas"
},
{
"coordinates": [894, 693],
"flag": "",
"name": "Normal Capybaras"
"coordinates": [1267, 794],
"flag": "Para",
"name": "Pará"
},
{
"coordinates": [884, 832],
"flag": "",
"name": "Just Otters"
"coordinates": [1550, 1066],
"flag": "bahia",
"name": "Bahia"
},
{
"coordinates": [1374, 1404],
"flag": "Sao Paulo",
"name": "São Paulo"
}
]
}
+99 -44
View File
@@ -17,12 +17,12 @@
"name": "World",
"nations": [
{
"coordinates": [375, 272],
"coordinates": [484, 284],
"flag": "us",
"name": "United States"
},
{
"coordinates": [372, 136],
"coordinates": [374, 186],
"flag": "ca",
"name": "Canada"
},
@@ -37,22 +37,22 @@
"name": "Cuba"
},
{
"coordinates": [524, 474],
"coordinates": [527, 487],
"flag": "co",
"name": "Colombia"
},
{
"coordinates": [593, 473],
"coordinates": [576, 466],
"flag": "ve",
"name": "Venezuela"
},
{
"coordinates": [596, 705],
"coordinates": [565, 737],
"flag": "ar",
"name": "Argentina"
},
{
"coordinates": [637, 567],
"coordinates": [684, 574],
"flag": "br",
"name": "Brazil"
},
@@ -92,39 +92,39 @@
"name": "Italy"
},
{
"coordinates": [958, 220],
"coordinates": [948, 221],
"flag": "fr",
"name": "France"
},
{
"coordinates": [997, 205],
"coordinates": [990, 195],
"flag": "de",
"name": "Germany"
},
{
"coordinates": [1064, 101],
"coordinates": [1014, 137],
"flag": "se",
"name": "Sweden"
},
{
"coordinates": [1046, 193],
"coordinates": [1031, 193],
"flag": "pl",
"name": "Poland"
},
{
"coordinates": [1061, 188],
"coordinates": [1102, 183],
"flag": "by",
"name": "Belarus"
},
{
"coordinates": [1073, 243],
"coordinates": [1073, 230],
"flag": "ro",
"name": "Romania"
},
{
"coordinates": [1161, 274],
"coordinates": [1123, 272],
"flag": "tr",
"name": "Turkey"
"name": "Türkiye"
},
{
"coordinates": [969, 133],
@@ -132,44 +132,44 @@
"name": "Norway"
},
{
"coordinates": [1062, 133],
"coordinates": [1082, 126],
"flag": "fi",
"name": "Finland"
},
{
"coordinates": [1099, 211],
"coordinates": [1135, 210],
"flag": "ua",
"name": "Ukraine"
},
{
"coordinates": [1344, 136],
"coordinates": [1351, 134],
"flag": "ru",
"name": "Russia"
},
{
"coordinates": [1537, 186],
"coordinates": [1500, 203],
"flag": "mn",
"name": "Mongolia"
},
{
"coordinates": [1524, 328],
"coordinates": [1527, 303],
"flag": "cn",
"name": "China"
},
{
"coordinates": [1368, 373],
"coordinates": [1331, 353],
"flag": "in",
"name": "India"
},
{
"coordinates": [1276, 239],
"coordinates": [1279, 207],
"flag": "kz",
"name": "Kazakhstan"
},
{
"coordinates": [1238, 309],
"flag": "ir",
"name": "Islamic Republic Of Iran"
"name": "Iran"
},
{
"coordinates": [1178, 351],
@@ -187,37 +187,37 @@
"name": "New Zealand"
},
{
"coordinates": [918, 342],
"coordinates": [957, 295],
"flag": "dz",
"name": "Algeria"
},
{
"coordinates": [1030, 332],
"coordinates": [1024, 340],
"flag": "ly",
"name": "Libyan Arab Jamahiriya"
"name": "Libya"
},
{
"coordinates": [1092, 335],
"coordinates": [1094, 340],
"flag": "eg",
"name": "Egypt"
},
{
"coordinates": [963, 410],
"coordinates": [956, 389],
"flag": "ne",
"name": "Niger"
},
{
"coordinates": [1112, 406],
"coordinates": [1101, 400],
"flag": "sd",
"name": "Sudan"
},
{
"coordinates": [1074, 508],
"coordinates": [1051, 513],
"flag": "cd",
"name": "DR Congo"
},
{
"coordinates": [1154, 443],
"coordinates": [1153, 451],
"flag": "et",
"name": "Ethiopia"
},
@@ -232,12 +232,12 @@
"name": "Madagascar"
},
{
"coordinates": [1052, 420],
"coordinates": [1036, 420],
"flag": "td",
"name": "Chad"
},
{
"coordinates": [1030, 665],
"coordinates": [1039, 646],
"flag": "na",
"name": "Namibia"
},
@@ -247,7 +247,7 @@
"name": "Philippines"
},
{
"coordinates": [1537, 426],
"coordinates": [1502, 415],
"flag": "th",
"name": "Thailand"
},
@@ -262,14 +262,14 @@
"name": "Japan"
},
{
"coordinates": [1869, 119],
"flag": "ru",
"coordinates": [1626, 118],
"flag": "Siberia",
"name": "Siberia"
},
{
"coordinates": [74, 117],
"flag": "polar_bears",
"name": "Polar Bears"
"coordinates": [422, 95],
"flag": "Nunavut",
"name": "Nunavut"
},
{
"coordinates": [419, 975],
@@ -277,22 +277,22 @@
"name": "West Antarctica"
},
{
"coordinates": [542, 603],
"coordinates": [516, 567],
"flag": "pe",
"name": "Peru"
},
{
"coordinates": [1075, 615],
"coordinates": [1097, 598],
"flag": "zm",
"name": "Zambia"
},
{
"coordinates": [1099, 165],
"coordinates": [1067, 168],
"flag": "lv",
"name": "Latvia"
},
{
"coordinates": [1427, 336],
"coordinates": [1419, 338],
"flag": "bt",
"name": "Bhutan"
},
@@ -312,14 +312,69 @@
"name": "Oman"
},
{
"coordinates": [853, 373],
"coordinates": [886, 327],
"flag": "ma",
"name": "Morocco"
},
{
"coordinates": [656, 678],
"coordinates": [626, 704],
"flag": "uy",
"name": "Uruguay"
},
{
"coordinates": [581, 620],
"flag": "bo",
"name": "Bolivia"
},
{
"coordinates": [95, 115],
"flag": "Alaska",
"name": "Alaska"
},
{
"coordinates": [243, 131],
"flag": "Yukon",
"name": "Yukon"
},
{
"coordinates": [264, 276],
"flag": "California",
"name": "California"
},
{
"coordinates": [371, 299],
"flag": "Texas",
"name": "Texas"
},
{
"coordinates": [532, 191],
"flag": "Quebec",
"name": "Quebec"
},
{
"coordinates": [932, 476],
"flag": "bj",
"name": "Benin"
},
{
"coordinates": [845, 425],
"flag": "sn",
"name": "Senegal"
},
{
"coordinates": [1152, 522],
"flag": "ke",
"name": "Kenya"
},
{
"coordinates": [1320, 282],
"flag": "pk",
"name": "Pakistan"
},
{
"coordinates": [1384, 466],
"flag": "lk",
"name": "Sri Lanka"
}
]
}
-36
View File
@@ -2,8 +2,6 @@ import newsItemsFallback from "resources/news.json";
import { z } from "zod";
import type { NewsItem } from "../core/ApiSchemas";
import {
ClanLeaderboardResponse,
ClanLeaderboardResponseSchema,
NewsItemSchema,
PlayerProfile,
PlayerProfileSchema,
@@ -236,40 +234,6 @@ export async function fetchGameById(
}
}
export async function fetchClanLeaderboard(): Promise<
ClanLeaderboardResponse | false
> {
try {
const res = await fetch(`${getApiBase()}/public/clans/leaderboard`, {
headers: { Accept: "application/json" },
});
if (!res.ok) {
console.warn(
"fetchClanLeaderboard: unexpected status",
res.status,
res.statusText,
);
return false;
}
const json = await res.json();
const parsed = ClanLeaderboardResponseSchema.safeParse(json);
if (!parsed.success) {
console.warn(
"fetchClanLeaderboard: Zod validation failed",
parsed.error.toString(),
);
return false;
}
return parsed.data;
} catch (err) {
console.warn("fetchClanLeaderboard: request failed", err);
return false;
}
}
export async function fetchPlayerLeaderboard(
page: number,
): Promise<RankedLeaderboardResponse | "reached_limit" | false> {
+494
View File
@@ -0,0 +1,494 @@
import {
type ClanBansResponse,
ClanBansResponseSchema,
type ClanBrowseResponse,
ClanBrowseResponseSchema,
type ClanInfo,
ClanInfoSchema,
type ClanLeaderboardResponse,
ClanLeaderboardResponseSchema,
type ClanMembersResponse,
ClanMembersResponseSchema,
type ClanRequestsResponse,
ClanRequestsResponseSchema,
type ClanStats,
ClanStatsSchema,
JoinClanResponseSchema,
} from "../core/ClanApiSchemas";
import { getApiBase } from "./Api";
import { getAuthHeader } from "./Auth";
export type {
ClanBan,
ClanBansResponse,
ClanBrowseResponse,
ClanInfo,
ClanJoinRequest,
ClanMember,
ClanMembersResponse,
ClanMemberStats,
ClanMemberWL,
ClanRequestsResponse,
ClanStats,
} from "../core/ClanApiSchemas";
async function clanFetch(
path: string,
options?: RequestInit,
): Promise<Response> {
const url = `${getApiBase()}${path}`;
return fetch(url, {
...options,
headers: {
Accept: "application/json",
...options?.headers,
Authorization: await getAuthHeader(),
},
});
}
export async function fetchClanLeaderboard(): Promise<
ClanLeaderboardResponse | false
> {
try {
const res = await fetch(`${getApiBase()}/public/clans/leaderboard`, {
headers: { Accept: "application/json" },
});
if (!res.ok) {
console.warn(
"fetchClanLeaderboard: unexpected status",
res.status,
res.statusText,
);
return false;
}
const json = await res.json();
const parsed = ClanLeaderboardResponseSchema.safeParse(json);
if (!parsed.success) {
console.warn(
"fetchClanLeaderboard: Zod validation failed",
parsed.error.toString(),
);
return false;
}
return parsed.data;
} catch (err) {
console.warn("fetchClanLeaderboard: request failed", err);
return false;
}
}
export async function fetchClanStats(tag: string): Promise<ClanStats | false> {
try {
const res = await fetch(
`${getApiBase()}/public/clan/${encodeURIComponent(tag)}`,
{ headers: { Accept: "application/json" } },
);
if (!res.ok) return false;
const json = await res.json();
const parsed = ClanStatsSchema.safeParse(json?.clan);
if (!parsed.success) {
console.warn("fetchClanStats: Zod validation failed", parsed.error);
return false;
}
return parsed.data;
} catch (err) {
console.warn("fetchClanStats: request failed", err);
return false;
}
}
export async function fetchClans(
search?: string,
page = 1,
limit = 20,
): Promise<ClanBrowseResponse | false> {
try {
const params = new URLSearchParams();
params.set("page", String(page));
params.set("limit", String(limit));
if (search && search.length >= 2) params.set("search", search);
const res = await clanFetch(`/clans?${params}`);
if (!res.ok) return false;
const json = await res.json();
const parsed = ClanBrowseResponseSchema.safeParse(json);
if (!parsed.success) {
console.warn("fetchClans: Zod validation failed", parsed.error);
return false;
}
return parsed.data;
} catch {
return false;
}
}
export async function fetchClanDetail(tag: string): Promise<ClanInfo | false> {
try {
const res = await clanFetch(`/clans/${encodeURIComponent(tag)}`);
if (!res.ok) return false;
const json = await res.json();
const parsed = ClanInfoSchema.safeParse(json);
if (!parsed.success) {
console.warn("fetchClanDetail: Zod validation failed", parsed.error);
return false;
}
return parsed.data;
} catch {
return false;
}
}
export type ClanMemberSort =
| "default"
| "winsTotal"
| "lossesTotal"
| "winsFfa"
| "lossesFfa"
| "winsTeam"
| "lossesTeam"
| "winsHvn"
| "lossesHvn"
| "winsRanked"
| "lossesRanked"
| "wins1v1"
| "losses1v1";
export type ClanMemberOrder = "asc" | "desc";
export async function fetchClanMembers(
tag: string,
page = 1,
limit = 20,
sort: ClanMemberSort = "default",
order?: ClanMemberOrder,
): Promise<ClanMembersResponse | false> {
try {
const params = new URLSearchParams();
params.set("page", String(page));
params.set("limit", String(limit));
if (sort !== "default") params.set("sort", sort);
if (order) params.set("order", order);
const res = await clanFetch(
`/clans/${encodeURIComponent(tag)}/members?${params}`,
);
if (!res.ok) return false;
const json = await res.json();
const parsed = ClanMembersResponseSchema.safeParse(json);
if (!parsed.success) {
console.warn("fetchClanMembers: Zod validation failed", parsed.error);
return false;
}
return parsed.data;
} catch {
return false;
}
}
export async function joinClan(
tag: string,
): Promise<
{ status: "joined" | "requested" } | { error: string; reason?: string }
> {
try {
const res = await clanFetch(`/clans/${encodeURIComponent(tag)}/join`, {
method: "POST",
});
if (res.status === 409) {
const body = await res.json().catch(() => ({}));
const msg = (body as { message?: string }).message ?? "";
return {
error: msg.toLowerCase().includes("request")
? "clan_modal.error_request_pending"
: "clan_modal.error_already_member",
};
}
if (res.status === 429) {
return { error: "clan_modal.error_rate_limited_generic" };
}
if (res.status === 403) {
const body = await res.json().catch(() => ({}));
const b = body as { code?: string; reason?: string | null };
if (b.code === "BANNED") {
return {
error: b.reason
? "clan_modal.error_banned_reason"
: "clan_modal.error_banned",
...(b.reason ? { reason: b.reason } : {}),
};
}
return {
error: "clan_modal.error_failed",
};
}
if (!res.ok) {
return {
error: "clan_modal.error_failed",
};
}
const json = await res.json();
const parsed = JoinClanResponseSchema.safeParse(json);
if (!parsed.success) {
console.warn("joinClan: Zod validation failed", parsed.error);
return { error: "clan_modal.error_failed" };
}
return parsed.data;
} catch {
return { error: "clan_modal.error_network" };
}
}
export async function leaveClan(
tag: string,
): Promise<true | { error: string }> {
try {
const res = await clanFetch(`/clans/${encodeURIComponent(tag)}/leave`, {
method: "POST",
});
if (!res.ok) {
return {
error: "clan_modal.error_failed",
};
}
return true;
} catch {
return { error: "clan_modal.error_network" };
}
}
export async function updateClan(
tag: string,
patch: { name?: string; description?: string; isOpen?: boolean },
): Promise<ClanInfo | { error: string }> {
try {
const res = await clanFetch(`/clans/${encodeURIComponent(tag)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(patch),
});
if (!res.ok) {
return {
error: "clan_modal.error_failed",
};
}
const json = await res.json();
const parsed = ClanInfoSchema.safeParse(json);
if (!parsed.success) {
console.warn("updateClan: Zod validation failed", parsed.error);
return { error: "clan_modal.error_failed" };
}
return parsed.data;
} catch {
return { error: "clan_modal.error_network" };
}
}
export async function disbandClan(
tag: string,
): Promise<true | { error: string }> {
try {
const res = await clanFetch(`/clans/${encodeURIComponent(tag)}`, {
method: "DELETE",
});
if (!res.ok) {
return {
error: "clan_modal.error_failed",
};
}
return true;
} catch {
return { error: "clan_modal.error_network" };
}
}
async function memberAction(
tag: string,
targetPublicId: string,
action: string,
): Promise<true | { error: string }> {
try {
const res = await clanFetch(`/clans/${encodeURIComponent(tag)}/${action}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ targetPublicId }),
});
if (!res.ok) {
return { error: "clan_modal.error_failed" };
}
return true;
} catch {
return { error: "clan_modal.error_network" };
}
}
export const kickMember = (tag: string, targetPublicId: string) =>
memberAction(tag, targetPublicId, "kick");
export const promoteMember = (tag: string, targetPublicId: string) =>
memberAction(tag, targetPublicId, "promote");
export const demoteMember = (tag: string, targetPublicId: string) =>
memberAction(tag, targetPublicId, "demote");
export const transferLeadership = (tag: string, targetPublicId: string) =>
memberAction(tag, targetPublicId, "transfer");
export async function fetchClanRequests(
tag: string,
page = 1,
limit = 20,
): Promise<ClanRequestsResponse | false> {
try {
const params = new URLSearchParams();
params.set("page", String(page));
params.set("limit", String(limit));
const res = await clanFetch(
`/clans/${encodeURIComponent(tag)}/requests?${params}`,
);
if (!res.ok) return false;
const json = await res.json();
const parsed = ClanRequestsResponseSchema.safeParse(json);
if (!parsed.success) {
console.warn("fetchClanRequests: Zod validation failed", parsed.error);
return false;
}
return parsed.data;
} catch {
return false;
}
}
export async function approveClanRequest(
tag: string,
targetPublicId: string,
): Promise<true | { error: string }> {
try {
const res = await clanFetch(
`/clans/${encodeURIComponent(tag)}/requests/approve`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ targetPublicId }),
},
);
if (!res.ok) {
return {
error: "clan_modal.error_failed",
};
}
return true;
} catch {
return { error: "clan_modal.error_network" };
}
}
export async function denyClanRequest(
tag: string,
targetPublicId: string,
): Promise<true | { error: string }> {
try {
const res = await clanFetch(
`/clans/${encodeURIComponent(tag)}/requests/deny`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ targetPublicId }),
},
);
if (!res.ok) {
return {
error: "clan_modal.error_failed",
};
}
return true;
} catch {
return { error: "clan_modal.error_network" };
}
}
export async function withdrawClanRequest(
tag: string,
): Promise<true | { error: string }> {
try {
const res = await clanFetch(
`/clans/${encodeURIComponent(tag)}/requests/withdraw`,
{ method: "POST" },
);
if (!res.ok) {
return {
error: "clan_modal.error_failed",
};
}
return true;
} catch {
return { error: "clan_modal.error_network" };
}
}
export async function banClanMember(
tag: string,
targetPublicId: string,
reason?: string,
): Promise<true | { error: string }> {
try {
const body: { targetPublicId: string; reason?: string } = {
targetPublicId,
};
if (reason) body.reason = reason;
const res = await clanFetch(`/clans/${encodeURIComponent(tag)}/ban`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
return { error: "clan_modal.error_failed" };
}
return true;
} catch {
return { error: "clan_modal.error_network" };
}
}
export async function unbanClanMember(
tag: string,
targetPublicId: string,
): Promise<true | { error: string }> {
try {
const res = await clanFetch(`/clans/${encodeURIComponent(tag)}/unban`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ targetPublicId }),
});
if (!res.ok) {
return { error: "clan_modal.error_failed" };
}
return true;
} catch {
return { error: "clan_modal.error_network" };
}
}
export async function fetchClanBans(
tag: string,
page = 1,
limit = 20,
): Promise<ClanBansResponse | false> {
try {
const params = new URLSearchParams();
params.set("page", String(page));
params.set("limit", String(limit));
const res = await clanFetch(
`/clans/${encodeURIComponent(tag)}/bans?${params}`,
);
if (!res.ok) return false;
const json = await res.json();
const parsed = ClanBansResponseSchema.safeParse(json);
if (!parsed.success) {
console.warn("fetchClanBans: Zod validation failed", parsed.error);
return false;
}
return parsed.data;
} catch {
return false;
}
}
+460
View File
@@ -0,0 +1,460 @@
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { getUserMe, invalidateUserMe } from "./Api";
import { type ClanInfo, type ClanMember, type ClanStats } from "./ClanApi";
import { BaseModal } from "./components/BaseModal";
import "./components/clan/ClanBansView";
import "./components/clan/ClanBrowseView";
import type { BrowseState } from "./components/clan/ClanBrowseView";
import "./components/clan/ClanCard";
import "./components/clan/ClanDetailView";
import "./components/clan/ClanManageView";
import "./components/clan/ClanMyRequestsView";
import "./components/clan/ClanRequestsView";
import type { ClanRole } from "./components/clan/ClanShared";
import "./components/clan/ClanTransferView";
import "./components/ConfirmDialog";
import "./components/CopyButton";
import { modalHeader } from "./components/ui/ModalHeader";
import { translateText } from "./Utils";
type Tab = "my-clans" | "browse";
type View =
| "list"
| "detail"
| "manage"
| "transfer"
| "requests"
| "bans"
| "my-requests";
@customElement("clan-modal")
export class ClanModal extends BaseModal {
@state() private activeTab: Tab = "my-clans";
@state() private view: View = "list";
@state() private loading = false;
@state() private myClans: ClanInfo[] = [];
@state() private myPendingRequests: {
tag: string;
name: string;
createdAt: string;
}[] = [];
@state() private selectedClanTag = "";
@state() private selectedClan: ClanInfo | null = null;
@state() private myRole: ClanRole | null = null;
private myPublicId: string | null = null;
@state() private myClanRoles = new Map<string, ClanRole>();
// Lifted browse state — survives tab switches
private browseCache: BrowseState | null = null;
// Lifted detail cache — survives sub-view navigation
private detailCache: {
tag: string;
members: ClanMember[];
membersTotal: number;
pendingRequestCount: number;
stats: ClanStats | null;
} | null = null;
render() {
const content = this.renderInner();
if (this.inline) return content;
return html`
<o-modal
id="clan-modal"
title=""
?hideCloseButton=${true}
?inline=${this.inline}
hideHeader
>
${content}
</o-modal>
`;
}
protected onOpen(): void {
this.loadMyClans();
}
protected onClose(): void {
this.activeTab = "my-clans";
this.view = "list";
this.selectedClan = null;
this.selectedClanTag = "";
this.myRole = null;
this.browseCache = null;
this.detailCache = null;
}
private async loadMyClans() {
this.loading = true;
try {
const me = await getUserMe();
if (!this.isModalOpen) return;
if (!me || Object.keys(me.user).length === 0) {
window.dispatchEvent(
new CustomEvent("show-message", {
detail: {
message: translateText("clan_modal.sign_in_for_clans"),
color: "red",
duration: 3000,
},
}),
);
this.close();
window.showPage?.("page-account");
return;
}
this.myPublicId = me.player.publicId;
this.myPendingRequests = me.player.clanRequests ?? [];
const roles = new Map<string, ClanRole>();
const clans: ClanInfo[] = [];
for (const c of me.player.clans ?? []) {
roles.set(c.tag, c.role);
clans.push({
tag: c.tag,
name: c.name,
description: "",
isOpen: false,
memberCount: c.memberCount,
});
}
this.myClanRoles = roles;
this.myClans = clans;
} finally {
this.loading = false;
}
}
private renderInner() {
if (this.loading) {
return html`
<div class="${this.modalContainerClass}">
${modalHeader({
title: translateText("clan_modal.title"),
onBack: () => this.close(),
ariaLabel: translateText("common.back"),
})}
${this.renderLoadingSpinner()}
</div>
`;
}
if (this.view === "my-requests") {
return html`<clan-my-requests-view
.myPendingRequests=${this.myPendingRequests}
@navigate-back=${() => (this.view = "list")}
@request-withdrawn=${(e: CustomEvent<{ tag: string }>) => {
this.myPendingRequests = this.myPendingRequests.filter(
(r) => r.tag !== e.detail.tag,
);
if (this.myPendingRequests.length === 0) this.view = "list";
}}
></clan-my-requests-view>`;
}
if (this.selectedClanTag) {
if (this.view === "manage") {
return html`<clan-manage-view
.clanTag=${this.selectedClanTag}
.selectedClan=${this.selectedClan}
.myPublicId=${this.myPublicId}
.myRole=${this.myRole}
@navigate-detail=${() => (this.view = "detail")}
@navigate-bans=${() => (this.view = "bans")}
@navigate-transfer=${() => (this.view = "transfer")}
@clan-updated=${(e: CustomEvent<Partial<ClanInfo>>) => {
if (this.selectedClan) {
this.selectedClan = { ...this.selectedClan, ...e.detail };
}
this.detailCache = null;
invalidateUserMe();
}}
@clan-disbanded=${(e: CustomEvent<{ tag: string }>) => {
const roles = new Map(this.myClanRoles);
roles.delete(e.detail.tag);
this.myClanRoles = roles;
this.myClans = this.myClans.filter((c) => c.tag !== e.detail.tag);
this.selectedClan = null;
this.selectedClanTag = "";
this.myRole = null;
this.view = "list";
this.loadMyClans();
}}
></clan-manage-view>`;
}
if (this.view === "transfer") {
return html`<clan-transfer-view
.clanTag=${this.selectedClanTag}
.selectedClan=${this.selectedClan}
@navigate-back=${() => (this.view = "manage")}
@leadership-transferred=${() => {
this.loadMyClans().then(() =>
this.openDetail(this.selectedClanTag),
);
}}
></clan-transfer-view>`;
}
if (this.view === "requests") {
return html`<clan-requests-view
.clanTag=${this.selectedClanTag}
.selectedClan=${this.selectedClan}
@navigate-back=${() => (this.view = "detail")}
@request-approved=${() => {
if (this.selectedClan) {
this.selectedClan = {
...this.selectedClan,
memberCount: (this.selectedClan.memberCount ?? 0) + 1,
};
}
this.detailCache = null;
}}
></clan-requests-view>`;
}
if (this.view === "bans") {
return html`<clan-bans-view
.clanTag=${this.selectedClanTag}
@navigate-back=${() => (this.view = "manage")}
></clan-bans-view>`;
}
// Default: detail view
return html`<clan-detail-view
.clanTag=${this.selectedClanTag}
.cachedClan=${this.selectedClan}
.myPublicId=${this.myPublicId}
.myClanRoles=${this.myClanRoles}
.myPendingRequests=${this.myPendingRequests}
.cachedDetail=${this.detailCache?.tag === this.selectedClanTag
? this.detailCache
: null}
@navigate-back=${() => {
this.view = "list";
this.selectedClan = null;
this.selectedClanTag = "";
this.myRole = null;
this.detailCache = null;
}}
@detail-loaded=${(
e: CustomEvent<{
clan: ClanInfo;
myRole: ClanRole | null;
members: ClanMember[];
membersTotal: number;
pendingRequestCount: number;
stats: ClanStats | null;
}>,
) => {
this.selectedClan = e.detail.clan;
this.myRole = e.detail.myRole;
this.detailCache = {
tag: e.detail.clan.tag,
members: e.detail.members,
membersTotal: e.detail.membersTotal,
pendingRequestCount: e.detail.pendingRequestCount,
stats: e.detail.stats,
};
}}
@navigate-manage=${() => (this.view = "manage")}
@navigate-requests=${() => (this.view = "requests")}
@clan-joined=${(e: CustomEvent<{ tag: string }>) => {
this.myClanRoles = new Map([
...this.myClanRoles,
[e.detail.tag, "member" as ClanRole],
]);
this.openDetail(e.detail.tag);
}}
@clan-left=${(e: CustomEvent<{ tag: string }>) => {
const roles = new Map(this.myClanRoles);
roles.delete(e.detail.tag);
this.myClanRoles = roles;
this.selectedClan = null;
this.selectedClanTag = "";
this.myRole = null;
this.view = "list";
this.loadMyClans();
}}
@request-sent=${(e: CustomEvent<{ tag: string; name: string }>) => {
this.myPendingRequests = [
...this.myPendingRequests,
{
tag: e.detail.tag,
name: e.detail.name,
createdAt: new Date().toISOString(),
},
];
}}
></clan-detail-view>`;
}
// List view (tabs + my clans / browse)
return html`
<div class="${this.modalContainerClass}">
${modalHeader({
title: translateText("clan_modal.title"),
onBack: () => this.close(),
ariaLabel: translateText("common.back"),
})}
${this.renderTabs()}
<div class="flex-1 overflow-y-auto custom-scrollbar mr-1">
${this.activeTab === "my-clans"
? this.renderMyClans()
: html`<clan-browse-view
.myClanRoles=${this.myClanRoles}
.myPendingRequests=${this.myPendingRequests}
.cachedState=${this.browseCache}
@browse-updated=${(e: CustomEvent<BrowseState>) => {
this.browseCache = e.detail;
}}
@clan-select=${(e: CustomEvent<{ tag: string }>) =>
this.openDetail(e.detail.tag)}
></clan-browse-view>`}
</div>
</div>
`;
}
private openDetail(tag: string) {
this.selectedClanTag = tag;
this.view = "detail";
}
private renderTabs() {
const tabs: { key: Tab; label: string }[] = [
{ key: "my-clans", label: translateText("clan_modal.my_clans") },
{ key: "browse", label: translateText("clan_modal.browse") },
];
return html`
<div class="flex border-b border-white/10 px-4 lg:px-6 gap-1">
${tabs.map(
(tab) => html`
<button
@click=${() => {
this.activeTab = tab.key;
this.view = "list";
this.selectedClan = null;
this.selectedClanTag = "";
if (tab.key === "my-clans") {
this.loadMyClans();
}
}}
class="px-4 py-3 text-sm font-bold uppercase tracking-wider transition-all relative
${this.activeTab === tab.key
? "text-aquarius"
: "text-white/40 hover:text-white/70"}"
>
${tab.label}
${this.activeTab === tab.key
? html`<div
class="absolute bottom-0 left-0 right-0 h-0.5 bg-malibu-blue"
></div>`
: ""}
</button>
`,
)}
</div>
`;
}
private renderMyClans() {
const hasClans = this.myClans.length > 0;
const hasRequests = this.myPendingRequests.length > 0;
if (!hasClans && !hasRequests) {
return html`
<div class="flex flex-col items-center justify-center p-12 text-center">
<p class="text-white/40 text-sm mb-4">
${translateText("clan_modal.no_clans")}
</p>
<button
@click=${() => (this.activeTab = "browse")}
class="px-6 py-2 text-sm font-bold text-white uppercase tracking-wider bg-malibu-blue hover:bg-aquarius active:bg-malibu-blue/80 rounded-lg transition-all"
>
${translateText("clan_modal.browse")}
</button>
</div>
`;
}
return html`
<div class="p-4 lg:p-6 space-y-3">
${hasRequests ? this.renderPendingRequestsButton() : ""}
${this.myClans.map(
(clan) => html`
<clan-card
.clan=${clan}
.clanRole=${this.myClanRoles.get(clan.tag)}
@clan-select=${(e: CustomEvent<{ tag: string }>) =>
this.openDetail(e.detail.tag)}
></clan-card>
`,
)}
</div>
`;
}
private renderPendingRequestsButton() {
const count = this.myPendingRequests.length;
return html`
<button
@click=${() => (this.view = "my-requests")}
class="w-full flex items-center justify-between bg-amber-500/10 hover:bg-amber-500/15 rounded-xl border border-amber-500/20 p-4 transition-all cursor-pointer group"
>
<div class="flex items-center gap-3">
<div
class="w-10 h-10 rounded-xl bg-amber-500/20 flex items-center justify-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5 text-amber-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div class="text-left">
<span class="text-amber-400 text-sm font-bold">
${translateText("clan_modal.pending_applications")}
</span>
<span class="text-amber-400/60 text-xs block">
${translateText("clan_modal.pending_requests_count", {
count,
})}
</span>
</div>
</div>
<div class="flex items-center gap-2">
<span
class="px-2.5 py-1 text-xs font-bold rounded-full bg-amber-500/20 text-amber-400 border border-amber-500/30"
>
${count}
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5 text-amber-400/40 group-hover:text-amber-400/70 transition-colors"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</button>
`;
}
}
+47 -2
View File
@@ -13,7 +13,12 @@ import {
import { createPartialGameRecord, findClosestBy, replacer } from "../core/Util";
import { ServerConfig } from "../core/configuration/Config";
import { getGameLogicConfig } from "../core/configuration/ConfigLoader";
import { BuildableUnit, Structures, UnitType } from "../core/game/Game";
import {
BuildableUnit,
PlayerType,
Structures,
UnitType,
} from "../core/game/Game";
import { TileRef } from "../core/game/GameMap";
import { GameMapLoader } from "../core/game/GameMapLoader";
import {
@@ -34,6 +39,7 @@ import {
DoBreakAllianceEvent,
DoGroundAttackEvent,
DoRequestAllianceEvent,
DoRetaliateAttackEvent,
InputHandler,
MouseMoveEvent,
MouseUpEvent,
@@ -237,7 +243,7 @@ async function createClientGame(
userSettings,
lobbyConfig.gameRecord !== undefined,
);
let gameMap: TerrainMapData | null = null;
let gameMap: TerrainMapData;
if (terrainLoad) {
gameMap = await terrainLoad;
@@ -391,6 +397,10 @@ export class ClientGameRunner {
DoGroundAttackEvent,
this.doGroundAttackUnderCursor.bind(this),
);
this.eventBus.on(
DoRetaliateAttackEvent,
this.doRetaliateAttackMostRecent.bind(this),
);
this.eventBus.on(
DoRequestAllianceEvent,
this.doRequestAllianceUnderCursor.bind(this),
@@ -783,6 +793,41 @@ export class ClientGameRunner {
});
}
private doRetaliateAttackMostRecent(): void {
if (!this.isActive || this.gameView.inSpawnPhase()) {
return;
}
if (this.myPlayer === null) {
if (!this.clientID) return;
const myPlayer = this.gameView.playerByClientID(this.clientID);
if (myPlayer === null) return;
this.myPlayer = myPlayer;
}
const incomingAttacks = this.myPlayer.incomingAttacks().filter((a) => {
const t = (
this.gameView.playerBySmallID(a.attackerID) as PlayerView
).type();
return t !== PlayerType.Bot;
});
if (incomingAttacks.length === 0) return;
const mostRecentAttack = incomingAttacks[incomingAttacks.length - 1];
const attacker = this.gameView.playerBySmallID(
mostRecentAttack.attackerID,
) as PlayerView;
if (!attacker) return;
const counterTroops = Math.min(
mostRecentAttack.troops,
this.renderer.uiState.attackRatio * this.myPlayer.troops(),
);
this.eventBus.emit(new SendAttackIntentEvent(attacker.id(), counterTroops));
}
private doRequestAllianceUnderCursor(): void {
const tile = this.getTileUnderCursor();
if (tile === null) return;
+7 -7
View File
@@ -128,19 +128,19 @@ export class GameModeSelector extends LitElement {
${this.renderSmallActionCard(
translateText("main.create"),
this.openHostLobby,
"bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105",
"bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105 hover:shadow-[var(--shadow-lobby-card-hover)]",
)}
${!crazyGamesSDK.isOnCrazyGames()
? this.renderSmallActionCard(
translateText("mode_selector.ranked_title"),
this.openRankedMenu,
"bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105",
"bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105 hover:shadow-[var(--shadow-lobby-card-hover)]",
)
: html`<div class="invisible"></div>`}
${this.renderSmallActionCard(
translateText("main.join"),
this.openJoinLobby,
"bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105",
"bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105 hover:shadow-[var(--shadow-lobby-card-hover)]",
)}
</div>
<!-- iOS Add to Home Screen banner -->
@@ -200,19 +200,19 @@ export class GameModeSelector extends LitElement {
${this.renderSmallActionCard(
translateText("main.create"),
this.openHostLobby,
"bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105",
"bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105 hover:shadow-[var(--shadow-lobby-card-hover)]",
)}
${!crazyGamesSDK.isOnCrazyGames()
? this.renderSmallActionCard(
translateText("mode_selector.ranked_title"),
this.openRankedMenu,
"bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105",
"bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105 hover:shadow-[var(--shadow-lobby-card-hover)]",
)
: html`<div class="invisible"></div>`}
${this.renderSmallActionCard(
translateText("main.join"),
this.openJoinLobby,
"bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105",
"bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105 hover:shadow-[var(--shadow-lobby-card-hover)]",
)}
</div>
</div>
@@ -275,7 +275,7 @@ export class GameModeSelector extends LitElement {
? getSecondsUntilServerTimestamp(lobby.startsAt, this.serverTimeOffset)
: undefined;
let timeDisplay: string = "";
let timeDisplay: string;
let timeDisplayUppercase = false;
if (timeRemaining === undefined) {
timeDisplay = renderDuration(this.defaultLobbyTime);
+18 -4
View File
@@ -153,6 +153,8 @@ export class DoBoatAttackEvent implements GameEvent {}
export class DoGroundAttackEvent implements GameEvent {}
export class DoRetaliateAttackEvent implements GameEvent {}
export class DoRequestAllianceEvent implements GameEvent {}
export class DoBreakAllianceEvent implements GameEvent {}
@@ -497,6 +499,11 @@ export class InputHandler {
this.eventBus.emit(new DoGroundAttackEvent());
}
if (this.keybindMatchesEvent(e, this.keybinds.retaliateAttack)) {
e.preventDefault();
this.eventBus.emit(new DoRetaliateAttackEvent());
}
if (this.keybindMatchesEvent(e, this.keybinds.attackRatioDown)) {
e.preventDefault();
const increment = this.userSettings.attackRatioIncrement();
@@ -520,10 +527,12 @@ export class InputHandler {
}
// Two-phase build keybind matching: exact code match first, then digit/Numpad alias.
const matchedBuild = this.resolveBuildKeybind(e.code, e.shiftKey);
if (matchedBuild !== null) {
e.preventDefault();
this.setGhostStructure(matchedBuild);
if (this.canUseBuildKeybinds()) {
const matchedBuild = this.resolveBuildKeybind(e.code, e.shiftKey);
if (matchedBuild !== null) {
e.preventDefault();
this.setGhostStructure(matchedBuild);
}
}
if (this.keybindMatchesEvent(e, this.keybinds.requestAlliance)) {
@@ -944,6 +953,11 @@ export class InputHandler {
return null;
}
private canUseBuildKeybinds(): boolean {
const myPlayer = this.gameView.myPlayer?.();
return !this.gameView.inSpawnPhase() && myPlayer?.isAlive() === true;
}
private getPinchDistance(): number {
const pointerEvents = Array.from(this.pointers.values());
const dx = pointerEvents[0].clientX - pointerEvents[1].clientX;
+5 -5
View File
@@ -20,6 +20,7 @@ import {
import "./AccountModal";
import { getUserMe } from "./Api";
import { userAuth } from "./Auth";
import "./ClanModal";
import { joinLobby, type JoinLobbyResult } from "./ClientGameRunner";
import { getPlayerCosmeticsRefs } from "./Cosmetics";
import { crazyGamesSDK } from "./CrazyGamesSDK";
@@ -440,11 +441,9 @@ class Client {
const onUserMe = async (userMeResponse: UserMeResponse | false) => {
updateAccountNavButton(userMeResponse);
const hasLinkedAccount =
!crazyGamesSDK.isOnCrazyGames() &&
((userMeResponse || null)?.player?.flares?.length ?? 0) > 0;
console.log("ads enabled: ", hasLinkedAccount);
window.adsEnabled = !hasLinkedAccount && !crazyGamesSDK.isOnCrazyGames();
const isAdFree =
userMeResponse !== false && userMeResponse.player?.adfree === true;
window.adsEnabled = !isAdFree && !crazyGamesSDK.isOnCrazyGames();
document.dispatchEvent(
new CustomEvent("userMeResponse", {
detail: userMeResponse,
@@ -829,6 +828,7 @@ class Client {
"leaderboard-button",
"token-login",
"matchmaking-modal",
"clan-modal",
"lang-selector",
"homepage-promos",
].forEach((tag) => {
+11 -1
View File
@@ -664,7 +664,17 @@ export class UserSettingModal extends BaseModal {
></setting-keybind>
<setting-keybind
action=${KeybindAction.swapDirection}
action==${KeybindAction.retaliateAttack}
label=${translateText("user_setting.retaliate_attack")}
description=${translateText("user_setting.retaliate_attack_desc")}
defaultKey=${this.defaultKeybinds.retaliateAttack}
.value=${this.getKeyValue("retaliateAttack")}
.display=${this.getKeyChar("retaliateAttack")}
@change=${this.handleKeybindChange}
></setting-keybind>
<setting-keybind
action="swapDirection"
label=${translateText("user_setting.swap_direction")}
description=${translateText("user_setting.swap_direction_desc")}
.defaultKey=${this.defaultKeybinds.swapDirection}
+2 -3
View File
@@ -1,7 +1,6 @@
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { v4 as uuidv4 } from "uuid";
import { translateText } from "../client/Utils";
import { generateCryptoRandomUUID, translateText } from "../client/Utils";
import { sanitizeClanTag } from "../core/Util";
import {
MAX_CLAN_TAG_LENGTH,
@@ -224,7 +223,7 @@ export class UsernameInput extends LitElement {
}
export function genAnonUsername(): string {
const uuid = uuidv4();
const uuid = generateCryptoRandomUUID();
const cleanUuid = uuid.replace(/-/g, "").toLowerCase();
const decimal = BigInt(`0x${cleanUuid}`);
const threeDigits = decimal % 1000n;
+12
View File
@@ -755,6 +755,18 @@ export function getServerNow(
return localNowMs + serverTimeOffsetMs;
}
export function showToast(
message: string,
color: "red" | "green",
duration = 3500,
): void {
window.dispatchEvent(
new CustomEvent("show-message", {
detail: { message, color, duration },
}),
);
}
export function getSecondsUntilServerTimestamp(
targetServerTimestampMs: number,
serverTimeOffsetMs: number,
+34 -33
View File
@@ -154,42 +154,43 @@ export abstract class BaseModal extends LitElement {
}
}
/**
* Renders a standardized loading spinner with optional custom message.
* Use this for consistent loading states across all modals.
*
* @param message - Optional loading message text. Defaults to no message.
* @param spinnerColor - Optional spinner color. Defaults to 'blue'.
* @returns TemplateResult of the loading UI
*/
protected renderLoadingSpinner(
message?: string,
spinnerColor: "blue" | "green" | "yellow" | "white" = "blue",
): TemplateResult {
const colorClasses = {
blue: "border-blue-500/30 border-t-blue-500",
green: "border-green-500/30 border-t-green-500",
yellow: "border-yellow-500/30 border-t-yellow-500",
white: "border-white/20 border-t-white",
};
return html`
<div
class="flex flex-col items-center justify-center p-12 text-white h-full min-h-[400px]"
>
<div
class="w-12 h-12 border-4 ${colorClasses[
spinnerColor
]} rounded-full animate-spin mb-4"
></div>
${message
? html`<p
class="text-white/60 font-medium tracking-wide animate-pulse"
>
${message}
</p>`
: ""}
</div>
`;
return renderLoadingSpinner(message, spinnerColor);
}
}
const spinnerColorClasses: Record<string, string> = {
blue: "border-blue-500/30 border-t-blue-500",
green: "border-green-500/30 border-t-green-500",
yellow: "border-yellow-500/30 border-t-yellow-500",
white: "border-white/20 border-t-white",
};
/**
* Renders a standardized loading spinner with optional custom message.
* Use this for consistent loading states across all modals.
*/
export function renderLoadingSpinner(
message?: string,
spinnerColor: "blue" | "green" | "yellow" | "white" = "blue",
): TemplateResult {
return html`
<div
class="flex flex-col items-center justify-center p-12 text-white h-full min-h-[400px]"
>
<div
class="w-12 h-12 border-4 ${spinnerColorClasses[
spinnerColor
]} rounded-full animate-spin mb-4"
></div>
${message
? html`<p class="text-white/60 font-medium tracking-wide animate-pulse">
${message}
</p>`
: ""}
</div>
`;
}
+129
View File
@@ -0,0 +1,129 @@
import { html, LitElement, render as litRender } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { translateText } from "../Utils";
/**
* A reusable inline confirmation dialog.
*
* Usage:
* ```html
* <confirm-dialog
* .message=${"Are you sure?"}
* variant="danger"
* @confirm=${() => doThing()}
* @cancel=${() => {}}
* ></confirm-dialog>
* ```
*
* For ban-style flows, add a textarea:
* ```html
* <confirm-dialog
* .message=${"Ban this player?"}
* variant="warning"
* textareaPlaceholder="Reason (optional)"
* @confirm=${(e) => ban(e.detail.text)}
* @cancel=${() => {}}
* ></confirm-dialog>
* ```
*/
@customElement("confirm-dialog")
export class ConfirmDialog extends LitElement {
@property() message = "";
@property() variant: "danger" | "warning" = "danger";
@property() textareaPlaceholder = "";
@property({ type: Boolean }) disabled = false;
@state() private text = "";
private portal: HTMLDivElement | null = null;
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.portal = document.createElement("div");
document.body.appendChild(this.portal);
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.portal) {
litRender(html``, this.portal);
this.portal.remove();
this.portal = null;
}
}
render() {
if (this.portal) {
litRender(this.renderOverlay(), this.portal);
}
return html``;
}
private renderOverlay() {
const isDanger = this.variant === "danger";
const borderColor = isDanger ? "border-red-500/50" : "border-amber-500/50";
const cardBg = "bg-surface";
const textColor = isDanger ? "text-red-300" : "text-amber-300";
const btnClass = isDanger
? "bg-red-600 text-white hover:bg-red-700"
: "bg-amber-600 text-white hover:bg-amber-700";
return html`
<div
class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/80"
@click=${(e: Event) => {
if (e.target === e.currentTarget) this.handleCancel();
}}
>
<div
class="mx-4 w-full max-w-sm p-6 rounded-2xl border ${borderColor} ${cardBg} shadow-2xl"
>
<p class="text-sm font-medium ${textColor} mb-5">${this.message}</p>
${this.textareaPlaceholder
? html`<textarea
.value=${this.text}
@input=${(e: Event) =>
(this.text = (e.target as HTMLTextAreaElement).value)}
maxlength="200"
rows="2"
placeholder="${this.textareaPlaceholder}"
class="w-full px-3 py-2 mb-4 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/30 focus:outline-none focus:ring-2 focus:ring-amber-500/50 text-sm resize-none"
></textarea>`
: ""}
<div class="flex gap-3">
<button
@click=${() => this.handleCancel()}
?disabled=${this.disabled}
class="flex-1 px-4 py-2.5 text-xs font-bold uppercase tracking-wider rounded-xl bg-white/5 text-white/60 border border-white/10 hover:bg-white/10 hover:text-white/80 transition-all disabled:opacity-50 disabled:pointer-events-none"
>
${translateText("common.cancel")}
</button>
<button
@click=${() => this.handleConfirm()}
?disabled=${this.disabled}
class="flex-1 px-4 py-2.5 text-xs font-bold uppercase tracking-wider rounded-xl ${btnClass} transition-all disabled:opacity-50 disabled:pointer-events-none border-0"
>
${translateText("common.confirm")}
</button>
</div>
</div>
</div>
`;
}
private handleConfirm() {
this.dispatchEvent(
new CustomEvent("confirm", { detail: { text: this.text } }),
);
this.text = "";
}
private handleCancel() {
this.dispatchEvent(new CustomEvent("cancel"));
this.text = "";
}
}
+1 -1
View File
@@ -404,7 +404,7 @@ export class CosmeticContainer extends LitElement {
if (this.name) {
this._nameEl ??= document.createElement("div");
const cfg = rarityConfig[this.rarity] ?? fallback;
this._nameEl.className = `text-xs font-bold uppercase tracking-wider text-center truncate w-full`;
this._nameEl.className = `text-xs font-bold uppercase tracking-wider text-center whitespace-normal break-words w-full`;
this._nameEl.style.color = cfg.nameColor;
this._nameEl.title = this.name;
this._nameEl.textContent = this.name;
+5
View File
@@ -123,6 +123,11 @@ export class DesktopNavBar extends LitElement {
data-page="page-leaderboard"
data-i18n="main.leaderboard"
></button>
<button
class="nav-menu-item text-white/70 hover:text-blue-500 font-medium tracking-wider uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
data-page="page-clan"
data-i18n="main.clans"
></button>
<div class="relative">
<button
class="nav-menu-item text-white/70 hover:text-malibu-blue font-medium tracking-wider uppercase cursor-pointer transition-colors [&.active]:text-malibu-blue "
+9 -4
View File
@@ -69,17 +69,17 @@ export class MobileNavBar extends LitElement {
></div>
<div
class="flex-1 w-full flex flex-col justify-start overflow-y-auto lg:pt-[clamp(1rem,3vh,4rem)] lg:pb-[clamp(0.5rem,2vh,2rem)] lg:px-[clamp(1rem,1.5vw,2rem)] p-5 gap-[clamp(1rem,3vh,3rem)]"
class="flex-1 w-full flex flex-col justify-start overflow-y-auto lg:pt-[clamp(1rem,3vh,4rem)] lg:pb-[clamp(0.5rem,2vh,2rem)] lg:px-[clamp(1rem,1.5vw,2rem)] pt-4 pb-4 px-5 gap-4 lg:gap-[clamp(1rem,3vh,3rem)]"
>
<!-- Logo + Menu -->
<div
class="flex flex-col text-malibu-blue mb-[clamp(1rem,2vh,2rem)] ml-[clamp(0.2rem,0.4vw,0.4vh)]"
class="flex flex-col text-malibu-blue mb-4 ml-[clamp(0.2rem,0.4vw,0.4vh)]"
>
<div class="flex flex-col items-center gap-2">
<div class="flex flex-col items-center gap-1">
<img
src=${assetUrl("images/OpenFrontLogo.svg")}
alt="OpenFront"
class="h-full w-auto"
class="w-auto h-auto max-w-[220px] max-h-[4.5rem]"
/>
<div
id="game-version"
@@ -114,6 +114,11 @@ export class MobileNavBar extends LitElement {
data-page="page-leaderboard"
data-i18n="main.leaderboard"
></button>
<button
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
data-page="page-clan"
data-i18n="main.clans"
></button>
<div
class="no-crazygames nav-menu-item flex items-center w-full cursor-pointer"
data-page="page-item-store"
+225
View File
@@ -0,0 +1,225 @@
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { type ClanBan, fetchClanBans, unbanClanMember } from "../../ClanApi";
import { translateText } from "../../Utils";
import "../CopyButton";
import { modalHeader } from "../ui/ModalHeader";
import {
formatClanDate,
modalContainerClass,
renderLoadingSpinner,
renderMemberSearchInput,
renderServerPagination,
showToast,
} from "./ClanShared";
@customElement("clan-bans-view")
export class ClanBansView extends LitElement {
createRenderRoot() {
return this;
}
@property() clanTag = "";
@state() private bans: ClanBan[] = [];
@state() private bansTotal = 0;
@state() private bansPage = 1;
@state() private bansLimit = 20;
@state() private memberActionPending = false;
@state() private loading = false;
private memberSearch = "";
private memberSearchDebounce: ReturnType<typeof setTimeout> | null = null;
connectedCallback() {
super.connectedCallback();
this.loadBans(1);
}
disconnectedCallback() {
if (this.memberSearchDebounce) clearTimeout(this.memberSearchDebounce);
super.disconnectedCallback();
}
private async loadBans(page: number, showLoading = true) {
if (showLoading) this.loading = true;
else this.memberActionPending = true;
try {
const data = await fetchClanBans(this.clanTag, page);
if (!data) {
showToast(translateText("clan_modal.error_failed"), "red");
return;
}
if (data.results.length === 0 && page > 1) {
await this.loadBans(1, false);
return;
}
this.bans = data.results;
this.bansTotal = data.total;
this.bansLimit = data.limit;
this.bansPage = data.page;
} finally {
if (showLoading) this.loading = false;
else this.memberActionPending = false;
}
}
private async handleUnban(publicId: string) {
if (this.memberActionPending) return;
this.memberActionPending = true;
try {
const result = await unbanClanMember(this.clanTag, publicId);
if (result !== true) {
showToast(translateText(result.error), "red");
return;
}
this.bans = this.bans.filter((b) => b.publicId !== publicId);
this.bansTotal--;
showToast(translateText("clan_modal.member_unbanned"), "green");
if (this.bans.length === 0 && this.bansPage > 1) {
await this.loadBans(this.bansPage - 1, false);
}
} finally {
this.memberActionPending = false;
}
}
private onSearchInput(e: Event) {
if (this.memberSearchDebounce) clearTimeout(this.memberSearchDebounce);
this.memberSearchDebounce = setTimeout(() => {
this.memberSearch = (e.target as HTMLInputElement).value;
this.requestUpdate();
}, 200);
}
render() {
if (this.loading)
return html`<div class="${modalContainerClass}">
${modalHeader({
title: translateText("clan_modal.banned_players"),
onBack: () =>
this.dispatchEvent(
new CustomEvent("navigate-back", {
bubbles: true,
composed: true,
}),
),
ariaLabel: translateText("common.back"),
})}${renderLoadingSpinner()}
</div>`;
const totalPages = Math.ceil(this.bansTotal / this.bansLimit);
const filtered = this.memberSearch
? this.bans.filter((b) =>
b.publicId.toLowerCase().includes(this.memberSearch.toLowerCase()),
)
: this.bans;
return html`
<div class="${modalContainerClass}">
${modalHeader({
title: translateText("clan_modal.banned_players"),
onBack: () =>
this.dispatchEvent(
new CustomEvent("navigate-back", {
bubbles: true,
composed: true,
}),
),
ariaLabel: translateText("common.back"),
rightContent: html`<span
class="text-xs font-bold uppercase tracking-wider px-3 py-1 rounded-full bg-white/10 text-white/50 border border-white/10"
>${this.bansTotal}</span
>`,
})}
<div class="flex-1 overflow-y-auto custom-scrollbar mr-1 p-4 lg:p-6">
${renderMemberSearchInput(
(e) => this.onSearchInput(e),
"clan_modal.search_members_placeholder",
)}
${filtered.length === 0
? html`<div
class="flex flex-col items-center justify-center p-12 text-center"
>
<p class="text-white/40 text-sm">
${translateText("clan_modal.no_bans")}
</p>
</div>`
: html`
<div class="space-y-3">
${filtered.map(
(ban) => html`
<div
class="bg-white/5 rounded-xl border border-white/10 p-4 space-y-2"
>
<div class="flex items-center gap-2">
<div
class="w-8 h-8 rounded-full bg-red-500/20 flex items-center justify-center shrink-0"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4 text-red-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
/>
</svg>
</div>
<copy-button
compact
.copyText=${ban.publicId}
.displayText=${ban.publicId}
.showVisibilityToggle=${false}
.showCopyIcon=${false}
></copy-button>
<span class="text-white/30 text-xs shrink-0"
>${translateText(
"clan_modal.banned_by_label",
)}</span
>
<copy-button
compact
.copyText=${ban.bannedBy}
.displayText=${ban.bannedBy}
.showVisibilityToggle=${false}
.showCopyIcon=${false}
></copy-button>
<span class="text-white/30 text-xs shrink-0"
>${formatClanDate(ban.createdAt)}</span
>
<div class="flex-1"></div>
<button
@click=${() => this.handleUnban(ban.publicId)}
?disabled=${this.memberActionPending}
class="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded-lg bg-green-500/20 text-green-400 border border-green-500/30 hover:bg-green-500/30 transition-all disabled:opacity-50 disabled:pointer-events-none shrink-0"
>
${translateText("clan_modal.unban")}
</button>
</div>
${ban.reason
? html`<div class="text-white/50 text-xs pl-10">
${translateText("clan_modal.ban_reason", {
reason: ban.reason,
})}
</div>`
: ""}
</div>
`,
)}
</div>
${totalPages > 1
? renderServerPagination(this.bansPage, totalPages, (p) =>
this.loadBans(p, false),
)
: ""}
`}
</div>
</div>
`;
}
}
@@ -0,0 +1,185 @@
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { type ClanBrowseResponse, fetchClans } from "../../ClanApi";
import { translateText } from "../../Utils";
import "./ClanCard";
import { type ClanRole, renderLoadingSpinner } from "./ClanShared";
export interface BrowseState {
data: ClanBrowseResponse | null;
page: number;
query: string;
}
@customElement("clan-browse-view")
export class ClanBrowseView extends LitElement {
createRenderRoot() {
return this;
}
@property({ type: Object }) myClanRoles: Map<string, ClanRole> = new Map();
@property({ type: Array }) myPendingRequests: { tag: string }[] = [];
@property({ type: Object }) cachedState: BrowseState | null = null;
@state() private searchQuery = "";
@state() private browseData: ClanBrowseResponse | null = null;
@state() private browsePage = 1;
@state() private loading = false;
@state() private errorMsg = "";
private searchDebounce: ReturnType<typeof setTimeout> | null = null;
private asyncGeneration = 0;
private emitState() {
this.dispatchEvent(
new CustomEvent("browse-updated", {
detail: {
data: this.browseData,
page: this.browsePage,
query: this.searchQuery,
} satisfies BrowseState,
bubbles: true,
composed: true,
}),
);
}
async loadBrowse() {
const gen = ++this.asyncGeneration;
this.loading = true;
this.errorMsg = "";
try {
const data = await fetchClans(
this.searchQuery || undefined,
this.browsePage,
);
if (gen !== this.asyncGeneration) return;
if (data === false) throw new Error("fetch failed");
this.browseData = data;
this.emitState();
} catch {
if (gen !== this.asyncGeneration) return;
this.errorMsg = translateText("clan_modal.error_loading");
} finally {
if (gen === this.asyncGeneration) this.loading = false;
}
}
private onSearchInput(e: Event) {
this.searchQuery = (e.target as HTMLInputElement).value;
if (this.searchDebounce) clearTimeout(this.searchDebounce);
this.searchDebounce = setTimeout(() => {
this.browsePage = 1;
this.loadBrowse();
}, 400);
}
connectedCallback() {
super.connectedCallback();
if (this.cachedState?.data) {
this.browseData = this.cachedState.data;
this.browsePage = this.cachedState.page;
this.searchQuery = this.cachedState.query;
} else {
this.loadBrowse();
}
}
disconnectedCallback() {
if (this.searchDebounce) clearTimeout(this.searchDebounce);
super.disconnectedCallback();
}
render() {
if (this.loading && !this.browseData)
return html`<div class="p-4 lg:p-6">${renderLoadingSpinner()}</div>`;
const totalPages = this.browseData
? Math.ceil(this.browseData.total / this.browseData.limit)
: 0;
const pendingTags = new Set(this.myPendingRequests.map((r) => r.tag));
const filtered = (this.browseData?.results ?? []).filter(
(clan) => !this.myClanRoles.has(clan.tag),
);
return html`
<div class="p-4 lg:p-6 space-y-4">
<div class="relative">
<input
type="text"
.value=${this.searchQuery}
@input=${(e: Event) => this.onSearchInput(e)}
class="w-full pl-10 pr-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/20 focus:outline-none focus:ring-2 focus:ring-malibu-blue/50 focus:border-malibu-blue/50 transition-all font-medium hover:bg-white/10 text-sm"
placeholder="${translateText("clan_modal.search_placeholder")}"
/>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4 text-white/30 absolute left-3 top-1/2 -translate-y-1/2"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
</div>
${this.errorMsg
? html`<p class="text-red-400 text-sm text-center py-4">
${this.errorMsg}
</p>`
: ""}
<div class="space-y-3">
${filtered.length === 0 && this.browseData
? html`<p class="text-white/40 text-sm text-center py-8">
${translateText("clan_modal.no_results")}
</p>`
: filtered.map(
(clan) =>
html`<clan-card
.clan=${clan}
?pending=${pendingTags.has(clan.tag)}
></clan-card>`,
)}
</div>
${totalPages > 1
? html`
<div class="flex items-center justify-center gap-2 pt-2">
<button
@click=${() => {
this.browsePage = Math.max(1, this.browsePage - 1);
this.loadBrowse();
}}
?disabled=${this.browsePage <= 1}
class="px-2 py-1 text-xs font-bold rounded-lg transition-all ${this
.browsePage <= 1
? "text-white/20 cursor-not-allowed"
: "text-white/60 hover:text-white hover:bg-white/10"}"
>
&lt;
</button>
<span class="text-xs text-white/50 font-medium">
${this.browsePage} / ${totalPages}
</span>
<button
@click=${() => {
this.browsePage = Math.min(totalPages, this.browsePage + 1);
this.loadBrowse();
}}
?disabled=${this.browsePage >= totalPages}
class="px-2 py-1 text-xs font-bold rounded-lg transition-all ${this
.browsePage >= totalPages
? "text-white/20 cursor-not-allowed"
: "text-white/60 hover:text-white hover:bg-white/10"}"
>
&gt;
</button>
</div>
`
: ""}
</div>
`;
}
}
+108
View File
@@ -0,0 +1,108 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import type { ClanInfo } from "../../ClanApi";
import { translateText } from "../../Utils";
import { translateClanRole } from "./ClanShared";
@customElement("clan-card")
export class ClanCard extends LitElement {
@property({ type: Object }) clan!: ClanInfo;
@property() clanRole?: string;
@property({ type: Boolean }) pending = false;
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
private onClick() {
this.dispatchEvent(
new CustomEvent("clan-select", {
detail: { tag: this.clan.tag },
bubbles: true,
composed: true,
}),
);
}
private renderBadge() {
const base =
"text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-full shrink-0";
if (this.clanRole) {
const colors =
this.clanRole === "leader"
? "bg-amber-500/20 text-amber-400 border border-amber-500/30"
: "bg-malibu-blue/15 text-aquarius border border-malibu-blue/30";
return html`<span class="${base} ${colors}"
>${translateClanRole(this.clanRole)}</span
>`;
}
if (this.pending) {
return html`<span
class="${base} bg-amber-500/20 text-amber-400 border border-amber-500/30"
>${translateText("clan_modal.request_pending")}</span
>`;
}
if (this.clan.isOpen) {
return html`<span
class="${base} bg-green-500/20 text-green-400 border border-green-500/30"
>${translateText("clan_modal.open")}</span
>`;
}
return html`<span
class="${base} bg-red-500/20 text-red-400 border border-red-500/30"
>${translateText("clan_modal.invite_only")}</span
>`;
}
render() {
const clan = this.clan;
return html`
<button
@click=${() => this.onClick()}
class="w-full text-left bg-white/5 hover:bg-white/10 rounded-xl border border-white/10 hover:border-white/20 p-4 transition-all cursor-pointer group"
>
<div class="flex items-center gap-4">
<div
class="w-12 h-12 rounded-xl bg-gradient-to-br ${clan.isOpen
? "from-malibu-blue/20 to-aquarius/20"
: "from-amber-500/20 to-orange-500/20"} flex items-center justify-center border border-white/10 shrink-0"
>
<span class="text-white font-bold text-sm">${clan.tag}</span>
</div>
<div class="flex-1 min-w-0">
<span class="text-white font-bold truncate block"
>${clan.name}</span
>
<div class="flex items-center gap-4 mt-1 text-xs text-white/40">
<span
>${translateText("clan_modal.member_count", {
count: clan.memberCount ?? 0,
})}</span
>
</div>
</div>
${this.renderBadge()}
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5 text-white/20 group-hover:text-white/50 transition-colors shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</button>
`;
}
}
@@ -0,0 +1,540 @@
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { invalidateUserMe } from "../../Api";
import {
type ClanInfo,
type ClanMember,
type ClanMemberOrder,
type ClanMemberSort,
type ClanStats,
fetchClanDetail,
fetchClanMembers,
fetchClanStats,
joinClan,
leaveClan,
} from "../../ClanApi";
import { translateText } from "../../Utils";
import "../ConfirmDialog";
import "../CopyButton";
import { modalHeader } from "../ui/ModalHeader";
import {
type ClanRole,
defaultOrderForSort,
filterMembersBySearch,
modalContainerClass,
renderClanWL,
renderLoadingSpinner,
renderMemberPagination,
renderMemberRow,
renderMemberSearchInput,
renderMemberSortControl,
renderStat,
showToast,
} from "./ClanShared";
@customElement("clan-detail-view")
export class ClanDetailView extends LitElement {
createRenderRoot() {
return this;
}
@property() clanTag = "";
@property() myPublicId: string | null = null;
@property({ type: Object }) myClanRoles: Map<string, ClanRole> = new Map();
@property({ type: Array }) myPendingRequests: {
tag: string;
name: string;
createdAt: string;
}[] = [];
@property({ type: Object }) cachedDetail: {
tag: string;
members: ClanMember[];
membersTotal: number;
pendingRequestCount: number;
stats: ClanStats | null;
} | null = null;
@property({ type: Object }) cachedClan: ClanInfo | null = null;
@state() private selectedClan: ClanInfo | null = null;
@state() private myRole: ClanRole | null = null;
@state() private members: ClanMember[] = [];
@state() private membersTotal = 0;
@state() private memberPage = 1;
@state() private membersPerPage = 10;
@state() private memberSort: ClanMemberSort = "default";
@state() private memberOrder: ClanMemberOrder = "asc";
@state() private pendingRequestCount = 0;
@state() private clanStats: ClanStats | null = null;
@state() private loading = false;
@state() private actionPending = false;
private memberSearch = "";
private memberSearchDebounce: ReturnType<typeof setTimeout> | null = null;
private asyncGeneration = 0;
connectedCallback() {
super.connectedCallback();
if (this.cachedDetail && this.cachedDetail.tag === this.clanTag) {
this.restoreFromCache(this.cachedDetail);
} else if (this.clanTag) {
this.loadDetail();
}
}
private restoreFromCache(cache: NonNullable<typeof this.cachedDetail>) {
this.selectedClan = this.cachedClan;
this.members = cache.members;
this.membersTotal = cache.membersTotal;
this.pendingRequestCount = cache.pendingRequestCount;
this.clanStats = cache.stats;
this.memberPage = 1;
const knownRole = this.myClanRoles.get(this.clanTag);
this.myRole = knownRole ?? null;
}
disconnectedCallback() {
if (this.memberSearchDebounce) clearTimeout(this.memberSearchDebounce);
super.disconnectedCallback();
}
private async loadDetail() {
const gen = ++this.asyncGeneration;
this.loading = true;
this.myRole = null;
this.pendingRequestCount = 0;
this.memberSearch = "";
const isMember = this.myClanRoles.has(this.clanTag);
const [detail, membersRes, stats] = await Promise.all([
fetchClanDetail(this.clanTag),
isMember
? fetchClanMembers(
this.clanTag,
1,
this.membersPerPage,
this.memberSort,
this.memberOrder,
)
: Promise.resolve(false as const),
fetchClanStats(this.clanTag),
]);
if (gen !== this.asyncGeneration) return;
this.clanStats = stats || null;
this.loading = false;
if (!detail) {
showToast(translateText("clan_modal.failed_to_load_clan"), "red");
this.dispatchEvent(
new CustomEvent("navigate-back", { bubbles: true, composed: true }),
);
return;
}
this.selectedClan = detail;
this.memberPage = 1;
if (membersRes) {
this.members = membersRes.results;
this.membersTotal = membersRes.total;
this.pendingRequestCount = membersRes.pendingRequests ?? 0;
const knownRole = this.myClanRoles.get(this.clanTag);
if (knownRole) {
this.myRole = knownRole;
} else {
const me = this.myPublicId
? membersRes.results.find((m) => m.publicId === this.myPublicId)
: null;
this.myRole = me ? me.role : null;
}
} else {
this.members = [];
this.membersTotal = 0;
this.myRole = null;
}
this.dispatchEvent(
new CustomEvent("detail-loaded", {
detail: {
clan: detail,
myRole: this.myRole,
members: this.members,
membersTotal: this.membersTotal,
pendingRequestCount: this.pendingRequestCount,
stats: this.clanStats,
},
bubbles: true,
composed: true,
}),
);
}
private async loadMemberPage(page: number) {
if (!this.selectedClan) return;
const res = await fetchClanMembers(
this.selectedClan.tag,
page,
this.membersPerPage,
this.memberSort,
this.memberOrder,
);
if (!res) return;
if (res.results.length === 0 && page > 1) {
await this.loadMemberPage(1);
return;
}
this.members = res.results;
this.membersTotal = res.total;
this.memberPage = page;
this.pendingRequestCount = res.pendingRequests ?? 0;
if (this.selectedClan.memberCount !== res.total) {
this.selectedClan = { ...this.selectedClan, memberCount: res.total };
}
}
private onSortChange(sort: ClanMemberSort) {
if (sort === this.memberSort) return;
this.memberSort = sort;
this.memberOrder = defaultOrderForSort(sort);
this.loadMemberPage(1);
}
private onOrderToggle() {
this.memberOrder = this.memberOrder === "asc" ? "desc" : "asc";
this.loadMemberPage(1);
}
private async handleJoin() {
if (!this.selectedClan || this.actionPending) return;
this.actionPending = true;
try {
const result = await joinClan(this.selectedClan.tag);
if ("error" in result) {
showToast(
result.reason
? translateText(result.error, { reason: result.reason })
: translateText(result.error),
"red",
);
return;
}
invalidateUserMe();
if (result.status === "joined") {
// Joining an open clan should immediately switch this detail page into
// member mode and refresh member-only data without requiring remount.
this.myRole = "member";
await this.loadMemberPage(1);
this.dispatchEvent(
new CustomEvent("clan-joined", {
detail: { tag: this.selectedClan.tag },
bubbles: true,
composed: true,
}),
);
} else {
this.dispatchEvent(
new CustomEvent("request-sent", {
detail: {
tag: this.selectedClan.tag,
name: this.selectedClan.name,
},
bubbles: true,
composed: true,
}),
);
showToast(translateText("clan_modal.join_request_sent"), "green");
}
} finally {
this.actionPending = false;
}
}
private async handleLeave() {
if (!this.selectedClan || this.actionPending) return;
this.actionPending = true;
try {
const result = await leaveClan(this.selectedClan.tag);
if (result !== true) {
showToast(translateText(result.error), "red");
return;
}
invalidateUserMe();
this.dispatchEvent(
new CustomEvent("clan-left", {
detail: { tag: this.selectedClan.tag },
bubbles: true,
composed: true,
}),
);
showToast(translateText("clan_modal.left_clan"), "green");
} finally {
this.actionPending = false;
}
}
private onSearchInput(e: Event) {
const val = (e.target as HTMLInputElement).value;
if (this.memberSearchDebounce) clearTimeout(this.memberSearchDebounce);
this.memberSearchDebounce = setTimeout(() => {
this.memberSearch = val;
this.requestUpdate();
}, 200);
}
render() {
if (this.loading) {
return html`
<div class="${modalContainerClass}">
${modalHeader({
title: translateText("clan_modal.title"),
onBack: () => this.back(),
ariaLabel: translateText("common.back"),
})}
${renderLoadingSpinner()}
</div>
`;
}
const clan = this.selectedClan;
if (!clan) return "";
const isMember = this.myRole !== null;
const isLeader = this.myRole === "leader";
const isOfficer = this.myRole === "officer";
const canManageRequests = isLeader || isOfficer;
const hasPendingRequest = this.myPendingRequests.some(
(r) => r.tag === clan.tag,
);
return html`
<div class="${modalContainerClass}">
${modalHeader({
title: clan.name,
onBack: () => this.back(),
ariaLabel: translateText("common.back"),
rightContent: html`
<span
class="text-xs font-bold uppercase tracking-wider px-3 py-1 rounded-full bg-white/10 text-white/50 border border-white/10"
>
[${clan.tag}]
</span>
`,
})}
<div class="flex-1 overflow-y-auto custom-scrollbar mr-1 p-4 lg:p-6">
<div class="space-y-6">
<div class="bg-white/5 rounded-xl border border-white/10 p-5">
<p class="text-white/70 text-sm">
${clan.description ||
translateText("clan_modal.no_description")}
</p>
</div>
<div class="grid grid-cols-2 gap-3">
${renderStat(
translateText("clan_modal.members"),
`${clan.memberCount ?? 0}`,
)}
${renderStat(
translateText("clan_modal.status"),
clan.isOpen
? translateText("clan_modal.open")
: translateText("clan_modal.invite_only"),
)}
</div>
${this.clanStats ? renderClanWL(this.clanStats) : ""}
${canManageRequests && this.pendingRequestCount > 0
? this.renderRequestsButton()
: ""}
${isMember ? this.renderMembersList() : ""}
<div class="flex flex-wrap gap-3">
${this.renderActionButtons(
isMember,
isLeader,
isOfficer,
hasPendingRequest,
clan,
)}
</div>
</div>
</div>
</div>
`;
}
private renderRequestsButton() {
return html`
<button
@click=${() =>
this.dispatchEvent(
new CustomEvent("navigate-requests", {
bubbles: true,
composed: true,
}),
)}
class="w-full flex items-center justify-between bg-amber-500/10 hover:bg-amber-500/15 rounded-xl border border-amber-500/20 p-4 transition-all cursor-pointer group"
>
<div class="flex items-center gap-3">
<div
class="w-10 h-10 rounded-xl bg-amber-500/20 flex items-center justify-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5 text-amber-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
/>
</svg>
</div>
<div class="text-left">
<span class="text-amber-400 text-sm font-bold">
${translateText("clan_modal.join_requests")}
</span>
<span class="text-amber-400/60 text-xs block">
${translateText("clan_modal.pending_requests_count", {
count: this.pendingRequestCount,
})}
</span>
</div>
</div>
<div class="flex items-center gap-2">
<span
class="px-2.5 py-1 text-xs font-bold rounded-full bg-amber-500/20 text-amber-400 border border-amber-500/30"
>
${this.pendingRequestCount}
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5 text-amber-400/40 group-hover:text-amber-400/70 transition-colors"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</button>
`;
}
private renderMembersList() {
const filtered = filterMembersBySearch(this.members, this.memberSearch);
return html`
<div class="bg-white/5 rounded-xl border border-white/10 p-5 space-y-3">
<h3 class="text-sm font-bold text-white/60 uppercase tracking-wider">
${translateText("clan_modal.members")}
</h3>
${renderMemberSearchInput(
(e: Event) => this.onSearchInput(e),
undefined,
renderMemberSortControl(
this.memberSort,
this.memberOrder,
(s) => this.onSortChange(s),
() => this.onOrderToggle(),
),
)}
<div class="space-y-2">
${filtered.map((m) => renderMemberRow(m, this.myPublicId))}
</div>
${renderMemberPagination(
this.memberPage,
this.membersTotal,
this.membersPerPage,
(p) => this.loadMemberPage(p),
(pp) => {
this.membersPerPage = pp;
this.loadMemberPage(1);
},
)}
</div>
`;
}
private renderActionButtons(
isMember: boolean,
isLeader: boolean,
isOfficer: boolean,
hasPendingRequest: boolean,
clan: ClanInfo,
) {
const buttons: ReturnType<typeof html>[] = [];
if (!isMember && hasPendingRequest) {
buttons.push(html`
<button
disabled
class="flex-1 px-6 py-3 text-sm font-bold text-white/40 uppercase tracking-wider bg-white/5 rounded-xl border border-white/10 cursor-not-allowed"
>
${translateText("clan_modal.request_pending")}
</button>
`);
} else if (!isMember && clan.isOpen) {
buttons.push(html`
<button
@click=${() => this.handleJoin()}
?disabled=${this.actionPending}
class="flex-1 px-6 py-3 text-sm font-bold text-white uppercase tracking-wider bg-malibu-blue hover:bg-aquarius active:bg-malibu-blue/80 rounded-xl transition-all disabled:opacity-50 disabled:pointer-events-none"
>
${translateText("clan_modal.join_clan")}
</button>
`);
} else if (!isMember && !clan.isOpen) {
buttons.push(html`
<button
@click=${() => this.handleJoin()}
?disabled=${this.actionPending}
class="flex-1 px-6 py-3 text-sm font-bold text-white uppercase tracking-wider bg-gradient-to-r from-amber-600 to-amber-700 hover:from-amber-500 hover:to-amber-600 rounded-xl transition-all shadow-lg hover:shadow-amber-900/40 border border-white/5 disabled:opacity-50 disabled:pointer-events-none"
>
${translateText("clan_modal.request_invite")}
</button>
`);
}
if (isMember && !isLeader) {
buttons.push(html`
<button
@click=${() => this.handleLeave()}
?disabled=${this.actionPending}
class="flex-1 px-6 py-3 text-sm font-bold text-white/70 uppercase tracking-wider bg-red-600/30 hover:bg-red-600/50 rounded-xl transition-all border border-red-500/30 disabled:opacity-50 disabled:pointer-events-none"
>
${translateText("clan_modal.leave_clan")}
</button>
`);
}
if (isLeader || isOfficer) {
buttons.push(html`
<button
@click=${() =>
this.dispatchEvent(
new CustomEvent("navigate-manage", {
bubbles: true,
composed: true,
}),
)}
class="flex-1 px-6 py-3 text-sm font-bold text-white uppercase tracking-wider bg-white/10 hover:bg-white/15 rounded-xl transition-all border border-white/10"
>
${translateText("clan_modal.manage_clan")}
</button>
`);
}
return buttons;
}
private back() {
this.dispatchEvent(
new CustomEvent("navigate-back", { bubbles: true, composed: true }),
);
}
}
@@ -0,0 +1,615 @@
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { invalidateUserMe } from "../../Api";
import {
banClanMember,
type ClanInfo,
type ClanMember,
type ClanMemberOrder,
type ClanMemberSort,
demoteMember,
disbandClan,
fetchClanMembers,
kickMember,
promoteMember,
updateClan,
} from "../../ClanApi";
import { translateText } from "../../Utils";
import "../ConfirmDialog";
import "../CopyButton";
import { modalHeader } from "../ui/ModalHeader";
import {
type ClanRole,
defaultOrderForSort,
filterMembersBySearch,
formatClanDate,
modalContainerClass,
renderLoadingSpinner,
renderMemberPagination,
renderMemberSearchInput,
renderMemberSortControl,
renderRoleIcon,
showToast,
} from "./ClanShared";
@customElement("clan-manage-view")
export class ClanManageView extends LitElement {
createRenderRoot() {
return this;
}
@property() clanTag = "";
@property({ type: Object }) selectedClan: ClanInfo | null = null;
@property() myPublicId: string | null = null;
@property() myRole: ClanRole | null = null;
@state() private manageName = "";
@state() private manageDescription = "";
@state() private manageIsOpen = true;
@state() private saving = false;
@state() private members: ClanMember[] = [];
@state() private membersTotal = 0;
@state() private memberPage = 1;
@state() private membersPerPage = 10;
@state() private memberSort: ClanMemberSort = "default";
@state() private memberOrder: ClanMemberOrder = "asc";
@state() private memberActionPending = false;
@state() private loading = false;
@state() private confirmAction: "disband" | "kick" | "ban" | null = null;
@state() private confirmTargetId: string | null = null;
@state() private pendingRequestCount = 0;
@state() private actionPending = false;
private memberSearch = "";
private memberSearchDebounce: ReturnType<typeof setTimeout> | null = null;
connectedCallback() {
super.connectedCallback();
if (this.selectedClan) {
this.manageName = this.selectedClan.name;
this.manageDescription = this.selectedClan.description ?? "";
this.manageIsOpen = this.selectedClan.isOpen ?? true;
}
this.loadMembers(1);
}
disconnectedCallback() {
if (this.memberSearchDebounce) clearTimeout(this.memberSearchDebounce);
super.disconnectedCallback();
}
private async loadMembers(page: number) {
if (this.members.length === 0) this.loading = true;
const res = await fetchClanMembers(
this.clanTag,
page,
this.membersPerPage,
this.memberSort,
this.memberOrder,
);
if (!res) {
this.loading = false;
return;
}
if (res.results.length === 0 && page > 1) {
await this.loadMembers(1);
return;
}
this.members = res.results;
this.membersTotal = res.total;
this.memberPage = page;
this.pendingRequestCount = res.pendingRequests ?? 0;
if (this.selectedClan && this.selectedClan.memberCount !== res.total) {
this.dispatchEvent(
new CustomEvent("clan-updated", {
detail: { memberCount: res.total },
bubbles: true,
composed: true,
}),
);
}
this.loading = false;
}
private async handleSaveSettings() {
const clan = this.selectedClan;
if (!clan) return;
const patch: { name?: string; description?: string; isOpen?: boolean } = {};
if (this.manageName !== clan.name) patch.name = this.manageName;
if ((this.manageDescription ?? "") !== (clan.description ?? ""))
patch.description = this.manageDescription;
if (this.manageIsOpen !== (clan.isOpen ?? true))
patch.isOpen = this.manageIsOpen;
if (Object.keys(patch).length === 0) return;
this.saving = true;
const result = await updateClan(this.clanTag, patch);
if ("error" in result) {
showToast(translateText(result.error), "red");
this.saving = false;
return;
}
this.dispatchEvent(
new CustomEvent("clan-updated", {
detail: {
name: result.name,
description: result.description,
isOpen: result.isOpen,
},
bubbles: true,
composed: true,
}),
);
this.saving = false;
showToast(translateText("clan_modal.settings_saved"), "green");
this.dispatchEvent(
new CustomEvent("navigate-detail", { bubbles: true, composed: true }),
);
}
private async handlePromote(publicId: string) {
if (this.memberActionPending) return;
this.memberActionPending = true;
try {
const result = await promoteMember(this.clanTag, publicId);
if (result !== true) {
showToast(translateText(result.error), "red");
return;
}
await this.loadMembers(this.memberPage);
showToast(translateText("clan_modal.member_promoted"), "green");
} finally {
this.memberActionPending = false;
}
}
private async handleDemote(publicId: string) {
if (this.memberActionPending) return;
this.memberActionPending = true;
try {
const result = await demoteMember(this.clanTag, publicId);
if (result !== true) {
showToast(translateText(result.error), "red");
return;
}
await this.loadMembers(this.memberPage);
showToast(translateText("clan_modal.member_demoted"), "green");
} finally {
this.memberActionPending = false;
}
}
private async handleKick(publicId: string) {
if (this.memberActionPending) return;
this.memberActionPending = true;
try {
const result = await kickMember(this.clanTag, publicId);
if (result !== true) {
showToast(translateText(result.error), "red");
return;
}
await this.loadMembers(this.memberPage);
showToast(translateText("clan_modal.member_kicked"), "green");
} finally {
this.memberActionPending = false;
}
}
private async handleBan(publicId: string, reason: string) {
if (this.memberActionPending) return;
this.memberActionPending = true;
try {
const result = await banClanMember(
this.clanTag,
publicId,
reason.trim().slice(0, 200) || undefined,
);
if (result !== true) {
showToast(translateText(result.error), "red");
return;
}
await this.loadMembers(this.memberPage);
showToast(translateText("clan_modal.member_banned"), "green");
} finally {
this.memberActionPending = false;
}
}
private async handleDisband() {
if (this.actionPending) return;
this.actionPending = true;
try {
const result = await disbandClan(this.clanTag);
if (result !== true) {
showToast(translateText(result.error), "red");
return;
}
invalidateUserMe();
this.dispatchEvent(
new CustomEvent("clan-disbanded", {
detail: { tag: this.clanTag },
bubbles: true,
composed: true,
}),
);
showToast(translateText("clan_modal.clan_disbanded"), "green");
} finally {
this.actionPending = false;
}
}
private clearConfirm() {
this.confirmAction = null;
this.confirmTargetId = null;
}
private onSearchInput(e: Event) {
if (this.memberSearchDebounce) clearTimeout(this.memberSearchDebounce);
this.memberSearchDebounce = setTimeout(() => {
this.memberSearch = (e.target as HTMLInputElement).value;
this.requestUpdate();
}, 200);
}
private onSortChange(sort: ClanMemberSort) {
if (sort === this.memberSort) return;
this.memberSort = sort;
this.memberOrder = defaultOrderForSort(sort);
this.loadMembers(1);
}
private onOrderToggle() {
this.memberOrder = this.memberOrder === "asc" ? "desc" : "asc";
this.loadMembers(1);
}
private navigateDetail = () =>
this.dispatchEvent(
new CustomEvent("navigate-detail", { bubbles: true, composed: true }),
);
render() {
if (this.loading)
return html`<div class="${modalContainerClass}">
${modalHeader({
title: translateText("clan_modal.manage_clan"),
onBack: this.navigateDetail,
ariaLabel: translateText("common.back"),
})}
${renderLoadingSpinner()}
</div>`;
const clan = this.selectedClan;
if (!clan) return "";
return html`${this.renderManageContent(clan)}${this.renderConfirmOverlay()}`;
}
private renderConfirmOverlay() {
if (!this.confirmAction) return "";
if (this.confirmAction === "disband") {
return html`<confirm-dialog
.message=${translateText("clan_modal.confirm_disband", {
tag: this.selectedClan?.tag ?? "",
name: this.selectedClan?.name ?? "",
})}
variant="danger"
?disabled=${this.actionPending}
@confirm=${() => {
this.clearConfirm();
this.handleDisband();
}}
@cancel=${() => this.clearConfirm()}
></confirm-dialog>`;
}
if (this.confirmAction === "kick" && this.confirmTargetId) {
return html`<confirm-dialog
.message=${translateText("clan_modal.confirm_kick")}
variant="warning"
?disabled=${this.memberActionPending}
@confirm=${() => {
const id = this.confirmTargetId!;
this.clearConfirm();
this.handleKick(id);
}}
@cancel=${() => this.clearConfirm()}
></confirm-dialog>`;
}
if (this.confirmAction === "ban" && this.confirmTargetId) {
return html`<confirm-dialog
.message=${translateText("clan_modal.confirm_ban")}
variant="warning"
textareaPlaceholder=${translateText("clan_modal.ban_reason_prompt")}
?disabled=${this.memberActionPending}
@confirm=${(e: CustomEvent<{ text: string }>) => {
const id = this.confirmTargetId!;
const reason = e.detail.text;
this.clearConfirm();
this.handleBan(id, reason);
}}
@cancel=${() => this.clearConfirm()}
></confirm-dialog>`;
}
return "";
}
private renderManageContent(clan: ClanInfo) {
return html`
<div class="${modalContainerClass}">
${modalHeader({
title: translateText("clan_modal.manage_clan"),
onBack: this.navigateDetail,
ariaLabel: translateText("common.back"),
rightContent: html`<span
class="text-xs font-bold uppercase tracking-wider px-3 py-1 rounded-full bg-white/10 text-white/50 border border-white/10"
>[${clan.tag}]</span
>`,
})}
<div class="flex-1 overflow-y-auto custom-scrollbar mr-1 p-4 lg:p-6">
<div class="space-y-6">
<!-- Edit Settings -->
<div
class="bg-white/5 rounded-2xl border border-white/10 p-6 space-y-5"
>
<h3
class="text-sm font-bold text-white/60 uppercase tracking-wider"
>
${translateText("clan_modal.clan_settings")}
</h3>
<div>
<label
class="block text-[10px] font-bold text-white/40 uppercase tracking-wider mb-2"
>${translateText("clan_modal.clan_name")}</label
>
<input
type="text"
.value=${this.manageName}
@input=${(e: Event) =>
(this.manageName = (e.target as HTMLInputElement).value)}
maxlength="35"
class="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/20 focus:outline-none focus:ring-2 focus:ring-malibu-blue/50 focus:border-malibu-blue/50 transition-all font-medium hover:bg-white/10 text-sm"
/>
</div>
<div>
<label
class="block text-[10px] font-bold text-white/40 uppercase tracking-wider mb-2"
>${translateText("clan_modal.description")}</label
>
<textarea
.value=${this.manageDescription}
@input=${(e: Event) =>
(this.manageDescription = (
e.target as HTMLTextAreaElement
).value)}
maxlength="200"
rows="3"
class="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/20 focus:outline-none focus:ring-2 focus:ring-malibu-blue/50 focus:border-malibu-blue/50 transition-all font-medium hover:bg-white/10 text-sm resize-none"
></textarea>
</div>
<div class="flex items-center justify-between">
<div>
<div class="text-white text-sm font-bold">
${translateText("clan_modal.open_clan")}
</div>
<div class="text-white/40 text-xs">
${translateText("clan_modal.open_clan_desc")}
</div>
</div>
<button
role="switch"
aria-checked="${this.manageIsOpen}"
aria-label="${translateText("clan_modal.open_clan")}"
@click=${() => (this.manageIsOpen = !this.manageIsOpen)}
class="relative w-12 h-7 rounded-full transition-all ${this
.manageIsOpen
? "bg-malibu-blue"
: "bg-white/20"}"
>
<div
class="absolute top-1 w-5 h-5 rounded-full bg-white shadow transition-all ${this
.manageIsOpen
? "left-6"
: "left-1"}"
></div>
</button>
</div>
<button
@click=${() => this.handleSaveSettings()}
?disabled=${this.saving}
class="w-full px-6 py-3 text-sm font-bold text-white uppercase tracking-wider bg-malibu-blue hover:bg-aquarius active:bg-malibu-blue/80 rounded-xl transition-all disabled:opacity-50"
>
${this.saving
? translateText("clan_modal.saving")
: translateText("clan_modal.save_changes")}
</button>
</div>
<!-- Member Management -->
<div
class="bg-white/5 rounded-2xl border border-white/10 p-6 space-y-4"
>
<h3
class="text-sm font-bold text-white/60 uppercase tracking-wider"
>
${translateText("clan_modal.members")}
(${clan.memberCount ?? 0})
</h3>
${renderMemberSearchInput(
(e) => this.onSearchInput(e),
undefined,
renderMemberSortControl(
this.memberSort,
this.memberOrder,
(s) => this.onSortChange(s),
() => this.onOrderToggle(),
),
)}
${(() => {
const filtered = filterMembersBySearch(
this.members,
this.memberSearch,
);
return html`
<div class="space-y-2">
${filtered.map((m) => this.renderManageMemberRow(m))}
</div>
${renderMemberPagination(
this.memberPage,
this.membersTotal,
this.membersPerPage,
(p) => this.loadMembers(p),
(pp) => {
this.membersPerPage = pp;
this.loadMembers(1);
},
)}
`;
})()}
</div>
<!-- Danger Zone -->
<div
class="bg-red-500/5 rounded-2xl border border-red-500/20 p-6 space-y-4"
>
<h3
class="text-sm font-bold text-red-400/80 uppercase tracking-wider"
>
${translateText("clan_modal.danger_zone")}
</h3>
<button
@click=${() =>
this.dispatchEvent(
new CustomEvent("navigate-bans", {
bubbles: true,
composed: true,
}),
)}
class="w-full px-6 py-3 text-sm font-bold text-red-400 uppercase tracking-wider bg-red-600/20 hover:bg-red-600/30 rounded-xl transition-all border border-red-500/30"
>
${translateText("clan_modal.banned_players")}
</button>
${this.myRole === "leader"
? html`
<button
@click=${() =>
this.dispatchEvent(
new CustomEvent("navigate-transfer", {
bubbles: true,
composed: true,
}),
)}
class="w-full px-6 py-3 text-sm font-bold text-amber-400 uppercase tracking-wider bg-amber-600/20 hover:bg-amber-600/30 rounded-xl transition-all border border-amber-500/30"
>
${translateText("clan_modal.transfer_leadership")}
</button>
<button
@click=${() => {
this.confirmAction = "disband";
this.confirmTargetId = null;
}}
?disabled=${this.confirmAction === "disband"}
class="w-full px-6 py-3 text-sm font-bold text-red-400 uppercase tracking-wider bg-red-600/20 hover:bg-red-600/30 rounded-xl transition-all border border-red-500/30 disabled:opacity-50 disabled:pointer-events-none"
>
${translateText("clan_modal.disband_clan")}
</button>
`
: ""}
</div>
</div>
</div>
</div>
`;
}
private renderManageMemberRow(member: ClanMember) {
const isLeader = member.role === "leader";
const isMe = member.publicId === this.myPublicId;
const canModerate =
!isMe &&
!isLeader &&
(this.myRole === "leader" ||
(this.myRole === "officer" && member.role === "member"));
const canPromote =
!isMe && this.myRole === "leader" && member.role === "member";
const canDemote =
!isMe && this.myRole === "leader" && member.role === "officer";
return html`
<div
class="flex flex-col py-2.5 px-3 rounded-xl border
${isMe
? "bg-malibu-blue/10 border-malibu-blue/20"
: "bg-white/5 border-white/10"}"
>
<div class="flex items-center flex-wrap gap-1.5">
<div
class="w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold shrink-0
${isMe
? "bg-malibu-blue/20 text-aquarius"
: "bg-white/10 text-white/50"}"
>
${renderRoleIcon(member.role)}
</div>
<copy-button
compact
.copyText=${member.publicId}
.displayText=${member.publicId}
.showVisibilityToggle=${false}
.showCopyIcon=${false}
></copy-button>
<span class="text-white/30 text-[10px] whitespace-nowrap">
${translateText("clan_modal.joined_date", {
date: formatClanDate(member.joinedAt),
})}
</span>
<div class="flex items-center gap-1.5 ml-auto flex-wrap justify-end">
${canPromote
? html`<button
@click=${() => this.handlePromote(member.publicId)}
?disabled=${this.memberActionPending}
class="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-full bg-purple-500/10 text-purple-400/70 border border-purple-500/20 hover:bg-purple-500/20 hover:text-purple-400 transition-all disabled:opacity-50 disabled:pointer-events-none"
>
${translateText("clan_modal.promote")}
</button>`
: ""}
${canDemote
? html`<button
@click=${() => this.handleDemote(member.publicId)}
?disabled=${this.memberActionPending}
class="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-full bg-white/5 text-white/40 border border-white/10 hover:bg-white/10 hover:text-white/60 transition-all disabled:opacity-50 disabled:pointer-events-none"
>
${translateText("clan_modal.demote")}
</button>`
: ""}
${canModerate
? html`
<button
@click=${() => {
this.confirmAction = "kick";
this.confirmTargetId = member.publicId;
}}
?disabled=${this.memberActionPending ||
this.confirmAction !== null}
class="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-full bg-red-500/10 text-red-400/70 border border-red-500/20 hover:bg-red-500/20 hover:text-red-400 transition-all disabled:opacity-50 disabled:pointer-events-none"
>
${translateText("clan_modal.kick")}
</button>
<button
@click=${() => {
this.confirmAction = "ban";
this.confirmTargetId = member.publicId;
}}
?disabled=${this.memberActionPending ||
this.confirmAction !== null}
class="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-full bg-red-500/10 text-red-400/70 border border-red-500/20 hover:bg-red-500/20 hover:text-red-400 transition-all disabled:opacity-50 disabled:pointer-events-none"
>
${translateText("clan_modal.ban")}
</button>
`
: ""}
</div>
</div>
</div>
`;
}
}
@@ -0,0 +1,105 @@
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { invalidateUserMe } from "../../Api";
import { withdrawClanRequest } from "../../ClanApi";
import { translateText } from "../../Utils";
import { modalHeader } from "../ui/ModalHeader";
import { formatClanDate, modalContainerClass, showToast } from "./ClanShared";
@customElement("clan-my-requests-view")
export class ClanMyRequestsView extends LitElement {
createRenderRoot() {
return this;
}
@property({ type: Array }) myPendingRequests: {
tag: string;
name: string;
createdAt: string;
}[] = [];
@state() private actionPending = false;
async handleWithdrawRequest(tag: string) {
if (this.actionPending) return;
this.actionPending = true;
try {
const result = await withdrawClanRequest(tag);
if (result !== true) {
showToast(translateText(result.error), "red");
return;
}
invalidateUserMe();
showToast(translateText("clan_modal.join_request_cancelled"), "green");
this.dispatchEvent(
new CustomEvent("request-withdrawn", {
detail: { tag },
bubbles: true,
composed: true,
}),
);
} finally {
this.actionPending = false;
}
}
render() {
return html`
<div class="${modalContainerClass}">
${modalHeader({
title: translateText("clan_modal.pending_applications"),
onBack: () =>
this.dispatchEvent(
new CustomEvent("navigate-back", {
bubbles: true,
composed: true,
}),
),
ariaLabel: translateText("common.back"),
})}
<div class="flex-1 overflow-y-auto custom-scrollbar mr-1 p-4 lg:p-6">
${this.myPendingRequests.length === 0
? html`<p class="text-white/40 text-sm text-center py-8">
${translateText("clan_modal.no_pending_applications")}
</p>`
: html`
<div class="space-y-3">
${this.myPendingRequests.map(
(req) => html`
<div
class="flex items-center gap-3 bg-white/5 rounded-xl border border-white/10 p-4"
>
<div
class="w-10 h-10 rounded-xl bg-amber-500/10 flex items-center justify-center border border-amber-500/20 shrink-0"
>
<span class="text-amber-400 font-bold text-xs"
>${req.tag}</span
>
</div>
<div class="flex-1 min-w-0">
<span
class="text-white font-bold text-sm truncate block"
>${req.name}</span
>
<span class="text-white/30 text-xs">
${translateText("clan_modal.applied")}
${formatClanDate(req.createdAt)}
</span>
</div>
<button
@click=${() => this.handleWithdrawRequest(req.tag)}
?disabled=${this.actionPending}
class="text-[10px] font-bold uppercase tracking-wider px-3 py-1 rounded-full bg-red-500/15 text-red-400 border border-red-500/20 hover:bg-red-500/25 transition-all cursor-pointer disabled:opacity-50 disabled:pointer-events-none"
>
${translateText("clan_modal.cancel_request")}
</button>
</div>
`,
)}
</div>
`}
</div>
</div>
`;
}
}

Some files were not shown because too many files have changed in this diff Show More