mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-18 09:14:09 +00:00
Merge branch 'main' into sounds
This commit is contained in:
@@ -13,10 +13,14 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@8f717f0dfca33b78d3c933452e42558e4456c8e7
|
- uses: actions/stale@8f717f0dfca33b78d3c933452e42558e4456c8e7
|
||||||
with:
|
with:
|
||||||
days-before-close: 14
|
days-before-pr-close: 14
|
||||||
days-before-stale: 14
|
days-before-pr-stale: 14
|
||||||
|
days-before-issue-close: -1
|
||||||
|
days-before-issue-stale: -1
|
||||||
exempt-draft-pr: true
|
exempt-draft-pr: true
|
||||||
exempt-pr-assignees: evanpelle
|
exempt-pr-assignees: evanpelle
|
||||||
stale-pr-label: "stale"
|
exempt-pr-labels: "will not stale"
|
||||||
|
stale-pr-label: "Stale"
|
||||||
stale-pr-message: "This pull request is stale because it has been open for 14 days with no activity. If you want to keep this pull request open, add a comment or update the branch."
|
stale-pr-message: "This pull request is stale because it has been open for 14 days with no activity. If you want to keep this pull request open, add a comment or update the branch."
|
||||||
start-date: 2025-09-03T00:00:00+00:00
|
close-pr-message: "This pull request has been closed because twenty-eight days have passed without activity. If someone wants to keep working on it, feel free to take the code."
|
||||||
|
close-pr-label: "Orphaned"
|
||||||
|
|||||||
+2
-2
@@ -17,8 +17,8 @@ export default {
|
|||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
statements: 21.5,
|
statements: 21.5,
|
||||||
branches: 16.5,
|
branches: 16,
|
||||||
lines: 22.0,
|
lines: 21.0,
|
||||||
functions: 20.5,
|
functions: 20.5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+1011
File diff suppressed because it is too large
Load Diff
+31
-7
@@ -6,7 +6,18 @@
|
|||||||
"lang_code": "en"
|
"lang_code": "en"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"close": "Close"
|
"close": "Close",
|
||||||
|
"available": "Available",
|
||||||
|
"preset_max": "Max",
|
||||||
|
"summary_send": "Send",
|
||||||
|
"summary_keep": "Keep",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"send": "Send",
|
||||||
|
"cap_label": "Cap",
|
||||||
|
"cap_tooltip": "Recipient’s remaining capacity",
|
||||||
|
"target_dead": "Target eliminated",
|
||||||
|
"target_dead_note": "You can't send resources to an eliminated player.",
|
||||||
|
"none": "None"
|
||||||
},
|
},
|
||||||
"main": {
|
"main": {
|
||||||
"title": "OpenFront (ALPHA)",
|
"title": "OpenFront (ALPHA)",
|
||||||
@@ -590,8 +601,6 @@
|
|||||||
"troops": "Troops",
|
"troops": "Troops",
|
||||||
"betrayals": "Betrayals",
|
"betrayals": "Betrayals",
|
||||||
"traitor": "Traitor",
|
"traitor": "Traitor",
|
||||||
"stable": "Stable",
|
|
||||||
"trust": "Trust",
|
|
||||||
"trading": "Trading",
|
"trading": "Trading",
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
"stopped": "Stopped",
|
"stopped": "Stopped",
|
||||||
@@ -600,9 +609,6 @@
|
|||||||
"nuke": "Nukes sent by them to you",
|
"nuke": "Nukes sent by them to you",
|
||||||
"start_trade": "Start Trading",
|
"start_trade": "Start Trading",
|
||||||
"stop_trade": "Stop Trading",
|
"stop_trade": "Stop Trading",
|
||||||
"yes": "Yes",
|
|
||||||
"no": "No",
|
|
||||||
"none": "None",
|
|
||||||
"alliances": "Alliances",
|
"alliances": "Alliances",
|
||||||
"flag": "Flag",
|
"flag": "Flag",
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
@@ -615,6 +621,23 @@
|
|||||||
"send_gold": "Send Gold",
|
"send_gold": "Send Gold",
|
||||||
"emotes": "Emojis"
|
"emotes": "Emojis"
|
||||||
},
|
},
|
||||||
|
"send_troops_modal": {
|
||||||
|
"title_with_name": "Send Troops to {name}",
|
||||||
|
"available_tooltip": "Your current available troops",
|
||||||
|
"min_keep": "Min keep",
|
||||||
|
"min_keep_pct": "(30%)",
|
||||||
|
"slider_tooltip": "{{percent}}% • {{amount}}",
|
||||||
|
"toggle_attack_bar_mode": "Use attack bar to send troops",
|
||||||
|
"warning_attackbar": "Once enabled, you can't open this modal directly. You'll only send troops via the attack bar.",
|
||||||
|
"aria_slider": "Troops slider",
|
||||||
|
"capacity_note": "Receiver can accept only {{amount}} right now."
|
||||||
|
},
|
||||||
|
"send_gold_modal": {
|
||||||
|
"title_with_name": "Send Gold to {name}",
|
||||||
|
"available_tooltip": "Your current available gold",
|
||||||
|
"aria_slider": "Amount slider",
|
||||||
|
"slider_tooltip": "{{percent}}% • {{amount}}"
|
||||||
|
},
|
||||||
"replay_panel": {
|
"replay_panel": {
|
||||||
"replay_speed": "Replay speed",
|
"replay_speed": "Replay speed",
|
||||||
"game_speed": "Game speed",
|
"game_speed": "Game speed",
|
||||||
@@ -633,7 +656,8 @@
|
|||||||
"choose_spawn": "Choose a starting location"
|
"choose_spawn": "Choose a starting location"
|
||||||
},
|
},
|
||||||
"territory_patterns": {
|
"territory_patterns": {
|
||||||
"title": "Select Territory Skin",
|
"title": "Skins",
|
||||||
|
"colors": "Colors",
|
||||||
"purchase": "Purchase",
|
"purchase": "Purchase",
|
||||||
"blocked": {
|
"blocked": {
|
||||||
"login": "You must be logged in to access this pattern.",
|
"login": "You must be logged in to access this pattern.",
|
||||||
|
|||||||
+71
-7
@@ -130,9 +130,8 @@
|
|||||||
"disable_nations": "Malŝalti naciojn",
|
"disable_nations": "Malŝalti naciojn",
|
||||||
"instant_build": "Tujkonstruaĵo",
|
"instant_build": "Tujkonstruaĵo",
|
||||||
"infinite_gold": "Senfina oro",
|
"infinite_gold": "Senfina oro",
|
||||||
"donate_gold": "Donaci oron",
|
|
||||||
"infinite_troops": "Senfinaj trupoj",
|
"infinite_troops": "Senfinaj trupoj",
|
||||||
"donate_troops": "Donaci trupojn",
|
"compact_map": "Karteto",
|
||||||
"disable_nukes": "Malŝalti nukleajn armilojn",
|
"disable_nukes": "Malŝalti nukleajn armilojn",
|
||||||
"enables_title": "Ebligi Agordojn",
|
"enables_title": "Ebligi Agordojn",
|
||||||
"start": "Komenci la ludon"
|
"start": "Komenci la ludon"
|
||||||
@@ -177,8 +176,10 @@
|
|||||||
"halkidiki": "Ĥalkidiko",
|
"halkidiki": "Ĥalkidiko",
|
||||||
"straitofgibraltar": "Ĝibraltara Markolo",
|
"straitofgibraltar": "Ĝibraltara Markolo",
|
||||||
"italia": "Italio",
|
"italia": "Italio",
|
||||||
|
"japan": "Japanio",
|
||||||
"yenisei": "Jenisejo",
|
"yenisei": "Jenisejo",
|
||||||
"pluto": "Plutono"
|
"pluto": "Plutono",
|
||||||
|
"montreal": "Montrealo"
|
||||||
},
|
},
|
||||||
"map_categories": {
|
"map_categories": {
|
||||||
"continental": "Kontinenta",
|
"continental": "Kontinenta",
|
||||||
@@ -196,8 +197,9 @@
|
|||||||
"join_lobby": "Aliĝi al la ludejo",
|
"join_lobby": "Aliĝi al la ludejo",
|
||||||
"checking": "Kontrolado de la ludejo...",
|
"checking": "Kontrolado de la ludejo...",
|
||||||
"not_found": "Ludejo ne trovita. Bonvolu kontroli la ID kaj reprovi.",
|
"not_found": "Ludejo ne trovita. Bonvolu kontroli la ID kaj reprovi.",
|
||||||
"error": "Eraro okazis. Bonvolu provi denove.",
|
"error": "Eraro okazis. Bonvolu reprovi aŭ kontaktu helpon.",
|
||||||
"joined_waiting": "Sukcese aliĝis! Atendante la komencon de la ludo..."
|
"joined_waiting": "Sukcese aliĝis! Atendante la komencon de la ludo...",
|
||||||
|
"version_mismatch": "Ĉi tiu ludo estis kreita kun malsama versio. Ne eblas aliĝi."
|
||||||
},
|
},
|
||||||
"public_lobby": {
|
"public_lobby": {
|
||||||
"join": "Kunigi la baldaŭan ludon",
|
"join": "Kunigi la baldaŭan ludon",
|
||||||
@@ -227,6 +229,7 @@
|
|||||||
"donate_gold": "Donacu oron",
|
"donate_gold": "Donacu oron",
|
||||||
"infinite_troops": "Senfinaj trupoj",
|
"infinite_troops": "Senfinaj trupoj",
|
||||||
"donate_troops": "Donacu trupojn",
|
"donate_troops": "Donacu trupojn",
|
||||||
|
"compact_map": "Karteto",
|
||||||
"enables_title": "Ebligi Agordojn",
|
"enables_title": "Ebligi Agordojn",
|
||||||
"player": "Ludanto",
|
"player": "Ludanto",
|
||||||
"players": "Ludantoj",
|
"players": "Ludantoj",
|
||||||
@@ -314,6 +317,8 @@
|
|||||||
"territory_patterns_desc": "Elektu ĉu montri teritoriajn ŝablonajn dezajnojn en la ludo",
|
"territory_patterns_desc": "Elektu ĉu montri teritoriajn ŝablonajn dezajnojn en la ludo",
|
||||||
"performance_overlay_label": "Efikeco Supermetaĵo",
|
"performance_overlay_label": "Efikeco Supermetaĵo",
|
||||||
"performance_overlay_desc": "Ŝalti la efikecon supermetaĵon. Kiam ebligita, la efikeco supermetaĵo estos montrata. Premu la Majuskligan klavon\n+ D dum la ludo por ŝalti.",
|
"performance_overlay_desc": "Ŝalti la efikecon supermetaĵon. Kiam ebligita, la efikeco supermetaĵo estos montrata. Premu la Majuskligan klavon\n+ D dum la ludo por ŝalti.",
|
||||||
|
"performance_overlay_enabled": "Funkciada supermetaĵo ŝaltita",
|
||||||
|
"performance_overlay_disabled": "Funkciada supermetaĵo malŝaltita",
|
||||||
"easter_writing_speed_label": "Rapidskriba multiganto",
|
"easter_writing_speed_label": "Rapidskriba multiganto",
|
||||||
"easter_writing_speed_desc": "Alĝustigu kiom rapide vi ŝajnigas kodi (x1–x100)",
|
"easter_writing_speed_desc": "Alĝustigu kiom rapide vi ŝajnigas kodi (x1–x100)",
|
||||||
"easter_bug_count_label": "Nombro da cimoj",
|
"easter_bug_count_label": "Nombro da cimoj",
|
||||||
@@ -494,7 +499,8 @@
|
|||||||
"nation": "Nacio",
|
"nation": "Nacio",
|
||||||
"player": "Ludanto",
|
"player": "Ludanto",
|
||||||
"team": "Teamo",
|
"team": "Teamo",
|
||||||
"d_troops": "Defendante trupoj",
|
"alliance_timeout": "Alianco finiĝas en",
|
||||||
|
"troops": "Trupoj",
|
||||||
"a_troops": "Atakante trupoj",
|
"a_troops": "Atakante trupoj",
|
||||||
"gold": "Oro",
|
"gold": "Oro",
|
||||||
"ports": "Havenoj",
|
"ports": "Havenoj",
|
||||||
@@ -585,7 +591,7 @@
|
|||||||
"choose_spawn": "Elektu komencan lokon"
|
"choose_spawn": "Elektu komencan lokon"
|
||||||
},
|
},
|
||||||
"territory_patterns": {
|
"territory_patterns": {
|
||||||
"title": "Elekti Teritoriajn Ŝablonojn",
|
"title": "Elekti Teritoria Ŝablono",
|
||||||
"purchase": "Aĉeti",
|
"purchase": "Aĉeti",
|
||||||
"blocked": {
|
"blocked": {
|
||||||
"login": "Vi devas esti ensalutinta por aliri ĉi tiun ŝablonon.",
|
"login": "Vi devas esti ensalutinta por aliri ĉi tiun ŝablonon.",
|
||||||
@@ -644,5 +650,63 @@
|
|||||||
"radial_menu": {
|
"radial_menu": {
|
||||||
"delete_unit_title": "Forigi trupunon",
|
"delete_unit_title": "Forigi trupunon",
|
||||||
"delete_unit_description": "Alklaku por forigi la plej proksiman trupunon"
|
"delete_unit_description": "Alklaku por forigi la plej proksiman trupunon"
|
||||||
|
},
|
||||||
|
"discord_user_header": {
|
||||||
|
"avatar_alt": "Profilbildo"
|
||||||
|
},
|
||||||
|
"player_stats_table": {
|
||||||
|
"building_stats": "Konstruaĵaj statistikoj",
|
||||||
|
"ship_arrivals": "Ŝipoj alvenoj",
|
||||||
|
"nuke_stats": "Bomboj statistikoj",
|
||||||
|
"player_metrics": "Ludanta statistikoj",
|
||||||
|
"building": "Konstruaĵo",
|
||||||
|
"ship_type": "Ŝipa tipo",
|
||||||
|
"weapon": "Armilo",
|
||||||
|
"built": "Konstruita",
|
||||||
|
"destroyed": "Detruita",
|
||||||
|
"captured": "Ekkaptita",
|
||||||
|
"lost": "Perdita",
|
||||||
|
"hits": "Frapoj",
|
||||||
|
"launched": "Lanĉita",
|
||||||
|
"landed": "Surterigita",
|
||||||
|
"sent": "Sendita",
|
||||||
|
"arrived": "Alveninta",
|
||||||
|
"attack": "Atako",
|
||||||
|
"received": "Ricevita",
|
||||||
|
"cancelled": "Nuligita",
|
||||||
|
"count": "Nombro",
|
||||||
|
"gold": "Oro",
|
||||||
|
"workers": "Laboristoj",
|
||||||
|
"war": "Milito",
|
||||||
|
"trade": "Komerci",
|
||||||
|
"steal": "Ŝteli",
|
||||||
|
"unit": {
|
||||||
|
"city": "Urbo",
|
||||||
|
"port": "Haveno",
|
||||||
|
"defp": "Defenda Posteno",
|
||||||
|
"saml": "SAM-lanĉilo",
|
||||||
|
"silo": "Misila Silo",
|
||||||
|
"wshp": "Militŝipo",
|
||||||
|
"fact": "Fabriko",
|
||||||
|
"trade": "Komerca ŝipo",
|
||||||
|
"trans": "Transporta ŝipo",
|
||||||
|
"abomb": "Atombombo",
|
||||||
|
"hbomb": "Hidrogenbombo",
|
||||||
|
"mirv": "MIRV",
|
||||||
|
"mirvw": "MIRV-kapo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"game_list": {
|
||||||
|
"recent_games": "Lastaj ludoj",
|
||||||
|
"game_id": "Ludo ID",
|
||||||
|
"mode": "Reĝimo",
|
||||||
|
"mode_ffa": "Ĉiu por si",
|
||||||
|
"mode_team": "Teamo",
|
||||||
|
"view": "Vido",
|
||||||
|
"details": "Detaloj",
|
||||||
|
"started": "Komencita",
|
||||||
|
"map": "Karto",
|
||||||
|
"difficulty": "Malfacileco",
|
||||||
|
"type": "Tipo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-7
@@ -130,9 +130,8 @@
|
|||||||
"disable_nations": "Désactiver les nations",
|
"disable_nations": "Désactiver les nations",
|
||||||
"instant_build": "Construction instantanée",
|
"instant_build": "Construction instantanée",
|
||||||
"infinite_gold": "Or infini",
|
"infinite_gold": "Or infini",
|
||||||
"donate_gold": "Donner de l'or",
|
|
||||||
"infinite_troops": "Troupes infinies",
|
"infinite_troops": "Troupes infinies",
|
||||||
"donate_troops": "Donner des troupes",
|
"compact_map": "Mini-carte",
|
||||||
"disable_nukes": "Désactiver les armes nucléaires",
|
"disable_nukes": "Désactiver les armes nucléaires",
|
||||||
"enables_title": "Activer les paramètres",
|
"enables_title": "Activer les paramètres",
|
||||||
"start": "Commencer la partie"
|
"start": "Commencer la partie"
|
||||||
@@ -177,8 +176,10 @@
|
|||||||
"halkidiki": "Chalcidique",
|
"halkidiki": "Chalcidique",
|
||||||
"straitofgibraltar": "Détroit de Gibraltar",
|
"straitofgibraltar": "Détroit de Gibraltar",
|
||||||
"italia": "Italie",
|
"italia": "Italie",
|
||||||
|
"japan": "Japon",
|
||||||
"yenisei": "Ienisseï",
|
"yenisei": "Ienisseï",
|
||||||
"pluto": "Pluton"
|
"pluto": "Pluton",
|
||||||
|
"montreal": "Montréal"
|
||||||
},
|
},
|
||||||
"map_categories": {
|
"map_categories": {
|
||||||
"continental": "Continental",
|
"continental": "Continental",
|
||||||
@@ -196,8 +197,9 @@
|
|||||||
"join_lobby": "Rejoindre le salon",
|
"join_lobby": "Rejoindre le salon",
|
||||||
"checking": "Vérification du salon...",
|
"checking": "Vérification du salon...",
|
||||||
"not_found": "Salon introuvable. Veuillez vérifier l'ID et réessayer.",
|
"not_found": "Salon introuvable. Veuillez vérifier l'ID et réessayer.",
|
||||||
"error": "Une erreur est survenue. Veuillez réessayer.",
|
"error": "Une erreur s'est produite. Veuillez réessayer ou contacter le support.",
|
||||||
"joined_waiting": "Rejoint avec succès ! En attente du début de la partie..."
|
"joined_waiting": "Rejoint avec succès ! En attente du début de la partie...",
|
||||||
|
"version_mismatch": "Cette partie a été créée avec une version différente. Impossible de rejoindre."
|
||||||
},
|
},
|
||||||
"public_lobby": {
|
"public_lobby": {
|
||||||
"join": "Rejoindre la prochaine partie",
|
"join": "Rejoindre la prochaine partie",
|
||||||
@@ -227,6 +229,7 @@
|
|||||||
"donate_gold": "Donner de l'or",
|
"donate_gold": "Donner de l'or",
|
||||||
"infinite_troops": "Troupes infinies",
|
"infinite_troops": "Troupes infinies",
|
||||||
"donate_troops": "Donner des troupes",
|
"donate_troops": "Donner des troupes",
|
||||||
|
"compact_map": "Mini-carte",
|
||||||
"enables_title": "Activer les paramètres",
|
"enables_title": "Activer les paramètres",
|
||||||
"player": "Joueur",
|
"player": "Joueur",
|
||||||
"players": "Joueurs",
|
"players": "Joueurs",
|
||||||
@@ -314,6 +317,8 @@
|
|||||||
"territory_patterns_desc": "Choisissez si vous voulez afficher les motifs de territoire en jeu",
|
"territory_patterns_desc": "Choisissez si vous voulez afficher les motifs de territoire en jeu",
|
||||||
"performance_overlay_label": "Surcouche de performances",
|
"performance_overlay_label": "Surcouche de performances",
|
||||||
"performance_overlay_desc": "Activer/désactiver la surcouche de performances. Lorsqu'elle est activée, la surcouche de performances sera affichée. Appuyez sur Maj+D pendant le jeu pour l\"activer.",
|
"performance_overlay_desc": "Activer/désactiver la surcouche de performances. Lorsqu'elle est activée, la surcouche de performances sera affichée. Appuyez sur Maj+D pendant le jeu pour l\"activer.",
|
||||||
|
"performance_overlay_enabled": "Affichage des performances activé",
|
||||||
|
"performance_overlay_disabled": "Affichage des performances désactivé",
|
||||||
"easter_writing_speed_label": "Multiplicateur de vitesse d'écriture",
|
"easter_writing_speed_label": "Multiplicateur de vitesse d'écriture",
|
||||||
"easter_writing_speed_desc": "Ajuster la vitesse à laquelle vous prétendez coder (x1–x100)",
|
"easter_writing_speed_desc": "Ajuster la vitesse à laquelle vous prétendez coder (x1–x100)",
|
||||||
"easter_bug_count_label": "Nombre de bugs",
|
"easter_bug_count_label": "Nombre de bugs",
|
||||||
@@ -494,7 +499,8 @@
|
|||||||
"nation": "Nation",
|
"nation": "Nation",
|
||||||
"player": "Joueur",
|
"player": "Joueur",
|
||||||
"team": "Équipe",
|
"team": "Équipe",
|
||||||
"d_troops": "Troupes en défense",
|
"alliance_timeout": "L'alliance se termine dans",
|
||||||
|
"troops": "Troupes",
|
||||||
"a_troops": "Troupes en attaque",
|
"a_troops": "Troupes en attaque",
|
||||||
"gold": "Or",
|
"gold": "Or",
|
||||||
"ports": "Ports",
|
"ports": "Ports",
|
||||||
@@ -585,7 +591,7 @@
|
|||||||
"choose_spawn": "Choisissez un emplacement de départ"
|
"choose_spawn": "Choisissez un emplacement de départ"
|
||||||
},
|
},
|
||||||
"territory_patterns": {
|
"territory_patterns": {
|
||||||
"title": "Sélectionnez un motif de territoire",
|
"title": "Sélectionner un motif de territoire",
|
||||||
"purchase": "Acheter",
|
"purchase": "Acheter",
|
||||||
"blocked": {
|
"blocked": {
|
||||||
"login": "Vous devez être connecté pour accéder à ce motif.",
|
"login": "Vous devez être connecté pour accéder à ce motif.",
|
||||||
@@ -644,5 +650,63 @@
|
|||||||
"radial_menu": {
|
"radial_menu": {
|
||||||
"delete_unit_title": "Supprimer une unité",
|
"delete_unit_title": "Supprimer une unité",
|
||||||
"delete_unit_description": "Cliquez pour supprimer l'unité la plus proche"
|
"delete_unit_description": "Cliquez pour supprimer l'unité la plus proche"
|
||||||
|
},
|
||||||
|
"discord_user_header": {
|
||||||
|
"avatar_alt": "Avatar"
|
||||||
|
},
|
||||||
|
"player_stats_table": {
|
||||||
|
"building_stats": "Statistiques de construction",
|
||||||
|
"ship_arrivals": "Arrivées de navire",
|
||||||
|
"nuke_stats": "Statistiques de bombes",
|
||||||
|
"player_metrics": "Statistiques du joueur",
|
||||||
|
"building": "Batîment",
|
||||||
|
"ship_type": "Type de navire",
|
||||||
|
"weapon": "Arme",
|
||||||
|
"built": "Construit",
|
||||||
|
"destroyed": "Détruit",
|
||||||
|
"captured": "Capturé",
|
||||||
|
"lost": "Perdu",
|
||||||
|
"hits": "Coups",
|
||||||
|
"launched": "Lancés",
|
||||||
|
"landed": "Atterris",
|
||||||
|
"sent": "Envoyés",
|
||||||
|
"arrived": "Arrivés",
|
||||||
|
"attack": "Attaque",
|
||||||
|
"received": "Reçu",
|
||||||
|
"cancelled": "Annulé",
|
||||||
|
"count": "Compte",
|
||||||
|
"gold": "Or",
|
||||||
|
"workers": "Ouvriers",
|
||||||
|
"war": "Guerre",
|
||||||
|
"trade": "Commercer",
|
||||||
|
"steal": "Voler",
|
||||||
|
"unit": {
|
||||||
|
"city": "Ville",
|
||||||
|
"port": "Port",
|
||||||
|
"defp": "Poste de défense",
|
||||||
|
"saml": "Lanceur de SAM",
|
||||||
|
"silo": "Silo à missiles",
|
||||||
|
"wshp": "Navire de guerre",
|
||||||
|
"fact": "Usine",
|
||||||
|
"trade": "Bateau de commerce",
|
||||||
|
"trans": "Bateau de transport",
|
||||||
|
"abomb": "Bombe atomique",
|
||||||
|
"hbomb": "Bombe à hydrogène",
|
||||||
|
"mirv": "MIRV",
|
||||||
|
"mirvw": "Ogive de MIRV"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"game_list": {
|
||||||
|
"recent_games": "Parties récentes",
|
||||||
|
"game_id": "ID de la partie",
|
||||||
|
"mode": "Mode",
|
||||||
|
"mode_ffa": "Chacun pour soi",
|
||||||
|
"mode_team": "Équipe",
|
||||||
|
"view": "Vue",
|
||||||
|
"details": "Détails",
|
||||||
|
"started": "Débuté",
|
||||||
|
"map": "Carte",
|
||||||
|
"difficulty": "Difficulté",
|
||||||
|
"type": "Type"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-8
@@ -130,9 +130,8 @@
|
|||||||
"disable_nations": "Letiltott nemzetek",
|
"disable_nations": "Letiltott nemzetek",
|
||||||
"instant_build": "Instant építés",
|
"instant_build": "Instant építés",
|
||||||
"infinite_gold": "Végtelen arany",
|
"infinite_gold": "Végtelen arany",
|
||||||
"donate_gold": "Arany adományomzása",
|
|
||||||
"infinite_troops": "Végtelen katonák",
|
"infinite_troops": "Végtelen katonák",
|
||||||
"donate_troops": "Katonák adományozása",
|
"compact_map": "Mini térkép",
|
||||||
"disable_nukes": "Nukleáris fegyverek letiltása",
|
"disable_nukes": "Nukleáris fegyverek letiltása",
|
||||||
"enables_title": "Beállítások engedélyezése",
|
"enables_title": "Beállítások engedélyezése",
|
||||||
"start": "Játék indítása"
|
"start": "Játék indítása"
|
||||||
@@ -177,8 +176,10 @@
|
|||||||
"halkidiki": "Halkidiki",
|
"halkidiki": "Halkidiki",
|
||||||
"straitofgibraltar": "Gibraltári-szoros",
|
"straitofgibraltar": "Gibraltári-szoros",
|
||||||
"italia": "Olaszország",
|
"italia": "Olaszország",
|
||||||
|
"japan": "Japán",
|
||||||
"yenisei": "Jenisej",
|
"yenisei": "Jenisej",
|
||||||
"pluto": "Plútó"
|
"pluto": "Plútó",
|
||||||
|
"montreal": "Montreal"
|
||||||
},
|
},
|
||||||
"map_categories": {
|
"map_categories": {
|
||||||
"continental": "Kontinentális",
|
"continental": "Kontinentális",
|
||||||
@@ -196,8 +197,9 @@
|
|||||||
"join_lobby": "Csatlakozás lobbyhoz",
|
"join_lobby": "Csatlakozás lobbyhoz",
|
||||||
"checking": "Lobby ellenőrzése...",
|
"checking": "Lobby ellenőrzése...",
|
||||||
"not_found": "Lobby nem található. Kérlek, ellenőrizd az azonosítót, és próbáld újra.",
|
"not_found": "Lobby nem található. Kérlek, ellenőrizd az azonosítót, és próbáld újra.",
|
||||||
"error": "Hiba történt. Kérlek, próbáld újra.",
|
"error": "Hiba történt. \nKérjük próbáld újra, vagy lépj kapcsolatba velünk.",
|
||||||
"joined_waiting": "Sikeresen csatlakoztál! Várakozás a játék kezdésére..."
|
"joined_waiting": "Sikeresen csatlakoztál! Várakozás a játék kezdésére...",
|
||||||
|
"version_mismatch": "Ez a játék egy másik verzióval lett létrehozva. Nem lehet csatlakozni."
|
||||||
},
|
},
|
||||||
"public_lobby": {
|
"public_lobby": {
|
||||||
"join": "Csatlakozás a következő játékhoz",
|
"join": "Csatlakozás a következő játékhoz",
|
||||||
@@ -227,6 +229,7 @@
|
|||||||
"donate_gold": "Arany adományozása",
|
"donate_gold": "Arany adományozása",
|
||||||
"infinite_troops": "Végtelen katonák",
|
"infinite_troops": "Végtelen katonák",
|
||||||
"donate_troops": "Katonák adományozása",
|
"donate_troops": "Katonák adományozása",
|
||||||
|
"compact_map": "Mini térkép",
|
||||||
"enables_title": "Beállítások engedélyezése",
|
"enables_title": "Beállítások engedélyezése",
|
||||||
"player": "Játékos",
|
"player": "Játékos",
|
||||||
"players": "Játékosok",
|
"players": "Játékosok",
|
||||||
@@ -264,10 +267,10 @@
|
|||||||
},
|
},
|
||||||
"unit_type": {
|
"unit_type": {
|
||||||
"city": "Város",
|
"city": "Város",
|
||||||
"defense_post": "Védelmi poszt",
|
"defense_post": "Védelmi állás",
|
||||||
"port": "Kikötő",
|
"port": "Kikötő",
|
||||||
"warship": "Hadihajó",
|
"warship": "Hadihajó",
|
||||||
"missile_silo": "Rakéta siló",
|
"missile_silo": "Rakétasiló",
|
||||||
"sam_launcher": "Rakétaelhárító rendszer",
|
"sam_launcher": "Rakétaelhárító rendszer",
|
||||||
"atom_bomb": "Atombomba",
|
"atom_bomb": "Atombomba",
|
||||||
"hydrogen_bomb": "Hidrogénbomba",
|
"hydrogen_bomb": "Hidrogénbomba",
|
||||||
@@ -314,6 +317,8 @@
|
|||||||
"territory_patterns_desc": "Válaszd ki, hogy megjelenjenek-e a területmintázatok a játékban",
|
"territory_patterns_desc": "Válaszd ki, hogy megjelenjenek-e a területmintázatok a játékban",
|
||||||
"performance_overlay_label": "Teljesítmény kijelző",
|
"performance_overlay_label": "Teljesítmény kijelző",
|
||||||
"performance_overlay_desc": "Kapcsoló a teljesítmény kijelzőhöz. Ha engedélyezve van, a teljesítmény kijelző megjelenik. A játék közben nyomd meg a Shift-D-t a váltáshoz.",
|
"performance_overlay_desc": "Kapcsoló a teljesítmény kijelzőhöz. Ha engedélyezve van, a teljesítmény kijelző megjelenik. A játék közben nyomd meg a Shift-D-t a váltáshoz.",
|
||||||
|
"performance_overlay_enabled": "Teljesítmény kijelző engedélyezve",
|
||||||
|
"performance_overlay_disabled": "Teljesítmény kijelző letiltva",
|
||||||
"easter_writing_speed_label": "Írási sebesség szorzó",
|
"easter_writing_speed_label": "Írási sebesség szorzó",
|
||||||
"easter_writing_speed_desc": "Állítsd be, milyen gyorsan szimulálod a kódolást (x1–x100)",
|
"easter_writing_speed_desc": "Állítsd be, milyen gyorsan szimulálod a kódolást (x1–x100)",
|
||||||
"easter_bug_count_label": "Hiba számláló",
|
"easter_bug_count_label": "Hiba számláló",
|
||||||
@@ -494,7 +499,8 @@
|
|||||||
"nation": "Nemzet",
|
"nation": "Nemzet",
|
||||||
"player": "Játékos",
|
"player": "Játékos",
|
||||||
"team": "Csapat",
|
"team": "Csapat",
|
||||||
"d_troops": "Védekező katonák",
|
"alliance_timeout": "Szövetség felbomlik",
|
||||||
|
"troops": "Katonák",
|
||||||
"a_troops": "Támadó katonák",
|
"a_troops": "Támadó katonák",
|
||||||
"gold": "Arany",
|
"gold": "Arany",
|
||||||
"ports": "Kikötők",
|
"ports": "Kikötők",
|
||||||
@@ -644,5 +650,63 @@
|
|||||||
"radial_menu": {
|
"radial_menu": {
|
||||||
"delete_unit_title": "Egység törlése",
|
"delete_unit_title": "Egység törlése",
|
||||||
"delete_unit_description": "Kattints a legközelebbi egység törléséhez"
|
"delete_unit_description": "Kattints a legközelebbi egység törléséhez"
|
||||||
|
},
|
||||||
|
"discord_user_header": {
|
||||||
|
"avatar_alt": "Profilkép"
|
||||||
|
},
|
||||||
|
"player_stats_table": {
|
||||||
|
"building_stats": "Épületstatisztikák",
|
||||||
|
"ship_arrivals": "Hajóérkezések",
|
||||||
|
"nuke_stats": "Nukleáris statisztikák",
|
||||||
|
"player_metrics": "Játékos mutatók",
|
||||||
|
"building": "Épület",
|
||||||
|
"ship_type": "Hajó típus",
|
||||||
|
"weapon": "Fegyver",
|
||||||
|
"built": "Épített",
|
||||||
|
"destroyed": "Megsemmisült",
|
||||||
|
"captured": "Elfoglalva",
|
||||||
|
"lost": "Elveszett",
|
||||||
|
"hits": "Találatok",
|
||||||
|
"launched": "Elindított",
|
||||||
|
"landed": "Célba ért",
|
||||||
|
"sent": "Elküldött",
|
||||||
|
"arrived": "Megérkezett",
|
||||||
|
"attack": "Támadás",
|
||||||
|
"received": "Fogadott",
|
||||||
|
"cancelled": "Megszakítva",
|
||||||
|
"count": "Mennyiség",
|
||||||
|
"gold": "Arany",
|
||||||
|
"workers": "Munkások",
|
||||||
|
"war": "Háború",
|
||||||
|
"trade": "Kereskedelem",
|
||||||
|
"steal": "Lopás",
|
||||||
|
"unit": {
|
||||||
|
"city": "Város",
|
||||||
|
"port": "Kikötő",
|
||||||
|
"defp": "Védelmi állás",
|
||||||
|
"saml": "Rakétaelhárító rendszer",
|
||||||
|
"silo": "Rakétasiló",
|
||||||
|
"wshp": "Hadihajó",
|
||||||
|
"fact": "Gyár",
|
||||||
|
"trade": "Kereskedelmi hajó",
|
||||||
|
"trans": "Szállítóhajó",
|
||||||
|
"abomb": "Atombomba",
|
||||||
|
"hbomb": "Hidrogénbomba",
|
||||||
|
"mirv": "MIRV",
|
||||||
|
"mirvw": "MIRV robbanófej"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"game_list": {
|
||||||
|
"recent_games": "Legutóbbi Játékok",
|
||||||
|
"game_id": "Játék azonosító",
|
||||||
|
"mode": "Mód",
|
||||||
|
"mode_ffa": "Mindenki mindenki ellen",
|
||||||
|
"mode_team": "Csapat",
|
||||||
|
"view": "Nézet",
|
||||||
|
"details": "Részletek",
|
||||||
|
"started": "Elkezdődött",
|
||||||
|
"map": "Térkép",
|
||||||
|
"difficulty": "Nehézség",
|
||||||
|
"type": "Típus"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-7
@@ -130,9 +130,8 @@
|
|||||||
"disable_nations": "国家を無効化",
|
"disable_nations": "国家を無効化",
|
||||||
"instant_build": "即時建設",
|
"instant_build": "即時建設",
|
||||||
"infinite_gold": "資金無限",
|
"infinite_gold": "資金無限",
|
||||||
"donate_gold": "資金援助",
|
|
||||||
"infinite_troops": "兵士無限",
|
"infinite_troops": "兵士無限",
|
||||||
"donate_troops": "軍事支援",
|
"compact_map": "ミニマップ",
|
||||||
"disable_nukes": "核兵器使用禁止",
|
"disable_nukes": "核兵器使用禁止",
|
||||||
"enables_title": "機能の有効化",
|
"enables_title": "機能の有効化",
|
||||||
"start": "ゲーム開始"
|
"start": "ゲーム開始"
|
||||||
@@ -177,8 +176,10 @@
|
|||||||
"halkidiki": "ハルキディキ半島",
|
"halkidiki": "ハルキディキ半島",
|
||||||
"straitofgibraltar": "ジブラルタル海峡",
|
"straitofgibraltar": "ジブラルタル海峡",
|
||||||
"italia": "イタリア",
|
"italia": "イタリア",
|
||||||
|
"japan": "日本",
|
||||||
"yenisei": "エニセイ川",
|
"yenisei": "エニセイ川",
|
||||||
"pluto": "冥王星"
|
"pluto": "冥王星",
|
||||||
|
"montreal": "モントリオール"
|
||||||
},
|
},
|
||||||
"map_categories": {
|
"map_categories": {
|
||||||
"continental": "大陸",
|
"continental": "大陸",
|
||||||
@@ -196,8 +197,9 @@
|
|||||||
"join_lobby": "ロビーに参加",
|
"join_lobby": "ロビーに参加",
|
||||||
"checking": "ロビーを確認中...",
|
"checking": "ロビーを確認中...",
|
||||||
"not_found": "ロビーが見つかりません。IDを確認してもう一度お試しください。",
|
"not_found": "ロビーが見つかりません。IDを確認してもう一度お試しください。",
|
||||||
"error": "エラーが発生しました。もう一度お試しください。",
|
"error": "エラーが発生しました。もう一度試すか、サポートにお問い合わせください。",
|
||||||
"joined_waiting": "参加に成功しました!ゲーム開始をお待ちください..."
|
"joined_waiting": "参加に成功しました!ゲーム開始をお待ちください...",
|
||||||
|
"version_mismatch": "このゲームは別のバージョンで作成されました。参加できません。"
|
||||||
},
|
},
|
||||||
"public_lobby": {
|
"public_lobby": {
|
||||||
"join": "次のゲームに参加",
|
"join": "次のゲームに参加",
|
||||||
@@ -227,6 +229,7 @@
|
|||||||
"donate_gold": "資金援助",
|
"donate_gold": "資金援助",
|
||||||
"infinite_troops": "兵士無限",
|
"infinite_troops": "兵士無限",
|
||||||
"donate_troops": "軍事支援",
|
"donate_troops": "軍事支援",
|
||||||
|
"compact_map": "ミニマップ",
|
||||||
"enables_title": "機能設定",
|
"enables_title": "機能設定",
|
||||||
"player": "プレイヤー",
|
"player": "プレイヤー",
|
||||||
"players": "プレイヤー",
|
"players": "プレイヤー",
|
||||||
@@ -314,6 +317,8 @@
|
|||||||
"territory_patterns_desc": "ゲーム内で領土の模様を表示するかどうか",
|
"territory_patterns_desc": "ゲーム内で領土の模様を表示するかどうか",
|
||||||
"performance_overlay_label": "パフォーマンスオーバーレイ",
|
"performance_overlay_label": "パフォーマンスオーバーレイ",
|
||||||
"performance_overlay_desc": "パフォーマンス表示(オーバーレイ)を切り替えます。有効にすると画面上にパフォーマンスオーバーレイが表示されます。ゲーム中は Shift+D を押して切り替えられます。",
|
"performance_overlay_desc": "パフォーマンス表示(オーバーレイ)を切り替えます。有効にすると画面上にパフォーマンスオーバーレイが表示されます。ゲーム中は Shift+D を押して切り替えられます。",
|
||||||
|
"performance_overlay_enabled": "パフォーマンスオーバーレイ有効",
|
||||||
|
"performance_overlay_disabled": "パフォーマンスオーバーレイ無効",
|
||||||
"easter_writing_speed_label": "書き込み速度の倍率",
|
"easter_writing_speed_label": "書き込み速度の倍率",
|
||||||
"easter_writing_speed_desc": "コードを書く速さを調節する(x1-x100)",
|
"easter_writing_speed_desc": "コードを書く速さを調節する(x1-x100)",
|
||||||
"easter_bug_count_label": "バグの個数",
|
"easter_bug_count_label": "バグの個数",
|
||||||
@@ -494,7 +499,8 @@
|
|||||||
"nation": "国家",
|
"nation": "国家",
|
||||||
"player": "プレイヤー",
|
"player": "プレイヤー",
|
||||||
"team": "チーム",
|
"team": "チーム",
|
||||||
"d_troops": "防衛兵士数",
|
"alliance_timeout": "同盟終了まで",
|
||||||
|
"troops": "軍隊",
|
||||||
"a_troops": "攻撃兵士数",
|
"a_troops": "攻撃兵士数",
|
||||||
"gold": "資金",
|
"gold": "資金",
|
||||||
"ports": "港",
|
"ports": "港",
|
||||||
@@ -585,7 +591,7 @@
|
|||||||
"choose_spawn": "スタート地点を選んで下さい"
|
"choose_spawn": "スタート地点を選んで下さい"
|
||||||
},
|
},
|
||||||
"territory_patterns": {
|
"territory_patterns": {
|
||||||
"title": "領土の模様を選択",
|
"title": "領土スキンを選択",
|
||||||
"purchase": "購入",
|
"purchase": "購入",
|
||||||
"blocked": {
|
"blocked": {
|
||||||
"login": "このページにアクセスするにはログインしてください。",
|
"login": "このページにアクセスするにはログインしてください。",
|
||||||
@@ -644,5 +650,63 @@
|
|||||||
"radial_menu": {
|
"radial_menu": {
|
||||||
"delete_unit_title": "ユニットを削除する",
|
"delete_unit_title": "ユニットを削除する",
|
||||||
"delete_unit_description": "クリックで最も近いユニットを削除します"
|
"delete_unit_description": "クリックで最も近いユニットを削除します"
|
||||||
|
},
|
||||||
|
"discord_user_header": {
|
||||||
|
"avatar_alt": "アバター"
|
||||||
|
},
|
||||||
|
"player_stats_table": {
|
||||||
|
"building_stats": "建築物統計情報",
|
||||||
|
"ship_arrivals": "船舶到着数",
|
||||||
|
"nuke_stats": "核兵器統計情報",
|
||||||
|
"player_metrics": "プレイヤー統計情報",
|
||||||
|
"building": "建築物",
|
||||||
|
"ship_type": "船舶の種類",
|
||||||
|
"weapon": "兵器",
|
||||||
|
"built": "建設",
|
||||||
|
"destroyed": "破壊",
|
||||||
|
"captured": "鹵獲",
|
||||||
|
"lost": "損失",
|
||||||
|
"hits": "着弾",
|
||||||
|
"launched": "発射",
|
||||||
|
"landed": "着陸",
|
||||||
|
"sent": "送信",
|
||||||
|
"arrived": "到着",
|
||||||
|
"attack": "攻撃",
|
||||||
|
"received": "受信",
|
||||||
|
"cancelled": "取消",
|
||||||
|
"count": "数",
|
||||||
|
"gold": "資金",
|
||||||
|
"workers": "労働者",
|
||||||
|
"war": "戦争",
|
||||||
|
"trade": "取引",
|
||||||
|
"steal": "略奪",
|
||||||
|
"unit": {
|
||||||
|
"city": "都市",
|
||||||
|
"port": "港",
|
||||||
|
"defp": "防衛ポスト",
|
||||||
|
"saml": "SAMランチャー",
|
||||||
|
"silo": "ミサイル格納庫",
|
||||||
|
"wshp": "戦艦",
|
||||||
|
"fact": "工場",
|
||||||
|
"trade": "交易船",
|
||||||
|
"trans": "輸送船",
|
||||||
|
"abomb": "原子爆弾",
|
||||||
|
"hbomb": "水素爆弾",
|
||||||
|
"mirv": "MIRV",
|
||||||
|
"mirvw": "MIRV 弾頭"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"game_list": {
|
||||||
|
"recent_games": "最近のゲーム",
|
||||||
|
"game_id": "ゲームID",
|
||||||
|
"mode": "モード",
|
||||||
|
"mode_ffa": "バトルロワイヤル",
|
||||||
|
"mode_team": "チーム",
|
||||||
|
"view": "見る",
|
||||||
|
"details": "詳細",
|
||||||
|
"started": "既に開始",
|
||||||
|
"map": "地図",
|
||||||
|
"difficulty": "難易度",
|
||||||
|
"type": "タイプ"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-6
@@ -130,9 +130,8 @@
|
|||||||
"disable_nations": "Отключить нации",
|
"disable_nations": "Отключить нации",
|
||||||
"instant_build": "Мгновенная стройка",
|
"instant_build": "Мгновенная стройка",
|
||||||
"infinite_gold": "Неограниченное золото",
|
"infinite_gold": "Неограниченное золото",
|
||||||
"donate_gold": "Пожертвовать золото",
|
|
||||||
"infinite_troops": "Неограниченные войска",
|
"infinite_troops": "Неограниченные войска",
|
||||||
"donate_troops": "Пожертвовать войска",
|
"compact_map": "Мини-карта",
|
||||||
"disable_nukes": "Отключить бомбы",
|
"disable_nukes": "Отключить бомбы",
|
||||||
"enables_title": "Разрешения",
|
"enables_title": "Разрешения",
|
||||||
"start": "Начать игру"
|
"start": "Начать игру"
|
||||||
@@ -177,8 +176,10 @@
|
|||||||
"halkidiki": "Халкидики",
|
"halkidiki": "Халкидики",
|
||||||
"straitofgibraltar": "Гибралтарский пролив",
|
"straitofgibraltar": "Гибралтарский пролив",
|
||||||
"italia": "Италия",
|
"italia": "Италия",
|
||||||
|
"japan": "Япония",
|
||||||
"yenisei": "Енисей",
|
"yenisei": "Енисей",
|
||||||
"pluto": "Плутон"
|
"pluto": "Плутон",
|
||||||
|
"montreal": "Монреаль"
|
||||||
},
|
},
|
||||||
"map_categories": {
|
"map_categories": {
|
||||||
"continental": "Континентальные",
|
"continental": "Континентальные",
|
||||||
@@ -196,8 +197,9 @@
|
|||||||
"join_lobby": "Присоединиться к лобби",
|
"join_lobby": "Присоединиться к лобби",
|
||||||
"checking": "Проверка лобби...",
|
"checking": "Проверка лобби...",
|
||||||
"not_found": "Лобби не найдено. Пожалуйста, проверьте правильность ID и попробуйте ещё раз.",
|
"not_found": "Лобби не найдено. Пожалуйста, проверьте правильность ID и попробуйте ещё раз.",
|
||||||
"error": "Произошла ошибка. Пожалуйста, попробуйте ещё раз.",
|
"error": "Произошла ошибка. Пожалуйста, попробуйте ещё раз или обратитесь в службу поддержки.",
|
||||||
"joined_waiting": "Вы успешно присоединились! Ожидание начала игры..."
|
"joined_waiting": "Вы успешно присоединились! Ожидание начала игры...",
|
||||||
|
"version_mismatch": "Эта игра была создана в другой версии. Невозможно присоединиться."
|
||||||
},
|
},
|
||||||
"public_lobby": {
|
"public_lobby": {
|
||||||
"join": "Присоединиться к следующей игре",
|
"join": "Присоединиться к следующей игре",
|
||||||
@@ -227,6 +229,7 @@
|
|||||||
"donate_gold": "Пожертвование золота",
|
"donate_gold": "Пожертвование золота",
|
||||||
"infinite_troops": "Неограниченные войска",
|
"infinite_troops": "Неограниченные войска",
|
||||||
"donate_troops": "Пожертвование войск",
|
"donate_troops": "Пожертвование войск",
|
||||||
|
"compact_map": "Мини-карта",
|
||||||
"enables_title": "Разрешения",
|
"enables_title": "Разрешения",
|
||||||
"player": "Игрок",
|
"player": "Игрок",
|
||||||
"players": "Игрока(-ов)",
|
"players": "Игрока(-ов)",
|
||||||
@@ -314,6 +317,8 @@
|
|||||||
"territory_patterns_desc": "Выберите, показывать ли узоры территорий в игре",
|
"territory_patterns_desc": "Выберите, показывать ли узоры территорий в игре",
|
||||||
"performance_overlay_label": "Оверлей производительности",
|
"performance_overlay_label": "Оверлей производительности",
|
||||||
"performance_overlay_desc": "Включить/выключить оверлей производительности. Если включено, будет отображаться оверлей производительности. Нажмите Shift+D во время игры для включения/выключения.",
|
"performance_overlay_desc": "Включить/выключить оверлей производительности. Если включено, будет отображаться оверлей производительности. Нажмите Shift+D во время игры для включения/выключения.",
|
||||||
|
"performance_overlay_enabled": "Оверлей производительности включён",
|
||||||
|
"performance_overlay_disabled": "Оверлей производительности выключен",
|
||||||
"easter_writing_speed_label": "Множитель скорости печати",
|
"easter_writing_speed_label": "Множитель скорости печати",
|
||||||
"easter_writing_speed_desc": "Настройте скорость, с которой вы делаете вид, что программируете (x1–x100)",
|
"easter_writing_speed_desc": "Настройте скорость, с которой вы делаете вид, что программируете (x1–x100)",
|
||||||
"easter_bug_count_label": "Количество багов",
|
"easter_bug_count_label": "Количество багов",
|
||||||
@@ -494,7 +499,8 @@
|
|||||||
"nation": "Нация",
|
"nation": "Нация",
|
||||||
"player": "Игрок",
|
"player": "Игрок",
|
||||||
"team": "Команда",
|
"team": "Команда",
|
||||||
"d_troops": "Войска защиты",
|
"alliance_timeout": "Конец союза через",
|
||||||
|
"troops": "Войска",
|
||||||
"a_troops": "Войска атаки",
|
"a_troops": "Войска атаки",
|
||||||
"gold": "Золото",
|
"gold": "Золото",
|
||||||
"ports": "Порты",
|
"ports": "Порты",
|
||||||
@@ -644,5 +650,63 @@
|
|||||||
"radial_menu": {
|
"radial_menu": {
|
||||||
"delete_unit_title": "Удалить объект",
|
"delete_unit_title": "Удалить объект",
|
||||||
"delete_unit_description": "Нажмите, чтобы удалить ближайший объект"
|
"delete_unit_description": "Нажмите, чтобы удалить ближайший объект"
|
||||||
|
},
|
||||||
|
"discord_user_header": {
|
||||||
|
"avatar_alt": "Аватар"
|
||||||
|
},
|
||||||
|
"player_stats_table": {
|
||||||
|
"building_stats": "Статистика строительства",
|
||||||
|
"ship_arrivals": "Прибытия кораблей",
|
||||||
|
"nuke_stats": "Статистика бомбардирования",
|
||||||
|
"player_metrics": "Статистика игрока",
|
||||||
|
"building": "Строительство",
|
||||||
|
"ship_type": "Тип корабля",
|
||||||
|
"weapon": "Оружие",
|
||||||
|
"built": "Построено",
|
||||||
|
"destroyed": "Уничтожено",
|
||||||
|
"captured": "Захвачено",
|
||||||
|
"lost": "Утеряно",
|
||||||
|
"hits": "Удары",
|
||||||
|
"launched": "Запущено",
|
||||||
|
"landed": "Приземлено",
|
||||||
|
"sent": "Отправлено",
|
||||||
|
"arrived": "Прибыло",
|
||||||
|
"attack": "Атака",
|
||||||
|
"received": "Получено",
|
||||||
|
"cancelled": "Отменено",
|
||||||
|
"count": "Количество",
|
||||||
|
"gold": "Золото",
|
||||||
|
"workers": "Рабочие",
|
||||||
|
"war": "Войны",
|
||||||
|
"trade": "Обмен",
|
||||||
|
"steal": "Украдено",
|
||||||
|
"unit": {
|
||||||
|
"city": "Город",
|
||||||
|
"port": "Порт",
|
||||||
|
"defp": "Укрепление",
|
||||||
|
"saml": "Пусковая установка ЗРК",
|
||||||
|
"silo": "Ракетная шахта",
|
||||||
|
"wshp": "Военный корабль",
|
||||||
|
"fact": "Фабрика",
|
||||||
|
"trade": "Торговый корабль",
|
||||||
|
"trans": "Транспортный корабль",
|
||||||
|
"abomb": "Атомная бомба",
|
||||||
|
"hbomb": "Водородная бомба",
|
||||||
|
"mirv": "РГЧ ИН",
|
||||||
|
"mirvw": "Боеголовка РГЧ ИН"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"game_list": {
|
||||||
|
"recent_games": "Недавние игры",
|
||||||
|
"game_id": "ID игры",
|
||||||
|
"mode": "Режим",
|
||||||
|
"mode_ffa": "Каждый против каждого",
|
||||||
|
"mode_team": "Команда",
|
||||||
|
"view": "Осмотреть",
|
||||||
|
"details": "Подробности",
|
||||||
|
"started": "Начато",
|
||||||
|
"map": "Карта",
|
||||||
|
"difficulty": "Сложность",
|
||||||
|
"type": "Тип"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+78
-14
@@ -130,9 +130,8 @@
|
|||||||
"disable_nations": "Вимкнути нації",
|
"disable_nations": "Вимкнути нації",
|
||||||
"instant_build": "Миттєве будівництво",
|
"instant_build": "Миттєве будівництво",
|
||||||
"infinite_gold": "Необмежене золото",
|
"infinite_gold": "Необмежене золото",
|
||||||
"donate_gold": "Пожертвувати золото",
|
|
||||||
"infinite_troops": "Необмежені війська",
|
"infinite_troops": "Необмежені війська",
|
||||||
"donate_troops": "Пожертвувати війська",
|
"compact_map": "Мінімапа",
|
||||||
"disable_nukes": "Вимкнути бомби",
|
"disable_nukes": "Вимкнути бомби",
|
||||||
"enables_title": "Дозволи",
|
"enables_title": "Дозволи",
|
||||||
"start": "Розпочати гру"
|
"start": "Розпочати гру"
|
||||||
@@ -177,8 +176,10 @@
|
|||||||
"halkidiki": "Халкідіки",
|
"halkidiki": "Халкідіки",
|
||||||
"straitofgibraltar": "Гібралтарська протока",
|
"straitofgibraltar": "Гібралтарська протока",
|
||||||
"italia": "Італія",
|
"italia": "Італія",
|
||||||
|
"japan": "Японія",
|
||||||
"yenisei": "Єнісей",
|
"yenisei": "Єнісей",
|
||||||
"pluto": "Плутон"
|
"pluto": "Плутон",
|
||||||
|
"montreal": "Монреаль"
|
||||||
},
|
},
|
||||||
"map_categories": {
|
"map_categories": {
|
||||||
"continental": "Континентальні",
|
"continental": "Континентальні",
|
||||||
@@ -196,8 +197,9 @@
|
|||||||
"join_lobby": "Приєднатися до лобі",
|
"join_lobby": "Приєднатися до лобі",
|
||||||
"checking": "Перевірка лобі...",
|
"checking": "Перевірка лобі...",
|
||||||
"not_found": "Лобі не знайдено. Будь ласка, перевірте дійсність ID і спробуйте знову.",
|
"not_found": "Лобі не знайдено. Будь ласка, перевірте дійсність ID і спробуйте знову.",
|
||||||
"error": "Сталася помилка. Будь ласка, спробуйте знову.",
|
"error": "Сталася помилка. Спробуйте ще раз або зверніться до служби підтримки.",
|
||||||
"joined_waiting": "Ви успішно приєдналися! Очікування початку гри..."
|
"joined_waiting": "Ви успішно приєдналися! Очікування початку гри...",
|
||||||
|
"version_mismatch": "Цю гру створено в іншій версії. Неможливо приєднатися."
|
||||||
},
|
},
|
||||||
"public_lobby": {
|
"public_lobby": {
|
||||||
"join": "Приєднатися до наступної гри",
|
"join": "Приєднатися до наступної гри",
|
||||||
@@ -211,7 +213,7 @@
|
|||||||
"enter_username": "Введіть своє ім'я гравця",
|
"enter_username": "Введіть своє ім'я гравця",
|
||||||
"not_string": "Ім'я гравця має бути рядком.",
|
"not_string": "Ім'я гравця має бути рядком.",
|
||||||
"too_short": "Ім'я гравця повинно містити щонайменше {min} символів.",
|
"too_short": "Ім'я гравця повинно містити щонайменше {min} символів.",
|
||||||
"too_long": "Ім'я гравця не повинно перевищувати {max} символів.",
|
"too_long": "Довжина ім'я гравця не повинна перевищувати {max} символів.",
|
||||||
"invalid_chars": "Ім'я гравця може містити лише латинські літери, цифри, пробіли, знаки підкреслення та [квадратні дужки]."
|
"invalid_chars": "Ім'я гравця може містити лише латинські літери, цифри, пробіли, знаки підкреслення та [квадратні дужки]."
|
||||||
},
|
},
|
||||||
"host_modal": {
|
"host_modal": {
|
||||||
@@ -227,6 +229,7 @@
|
|||||||
"donate_gold": "Пожертвування золота",
|
"donate_gold": "Пожертвування золота",
|
||||||
"infinite_troops": "Безмежні війська",
|
"infinite_troops": "Безмежні війська",
|
||||||
"donate_troops": "Пожертвування військ",
|
"donate_troops": "Пожертвування військ",
|
||||||
|
"compact_map": "Мінімапа",
|
||||||
"enables_title": "Дозволи",
|
"enables_title": "Дозволи",
|
||||||
"player": "Гравець",
|
"player": "Гравець",
|
||||||
"players": "Гравці(в)",
|
"players": "Гравці(в)",
|
||||||
@@ -314,6 +317,8 @@
|
|||||||
"territory_patterns_desc": "Виберіть, чи показувати візерунки територій у грі",
|
"territory_patterns_desc": "Виберіть, чи показувати візерунки територій у грі",
|
||||||
"performance_overlay_label": "Оверлей продуктивности",
|
"performance_overlay_label": "Оверлей продуктивности",
|
||||||
"performance_overlay_desc": "Увімкнення/вимкнення оверлея продуктивности. Якщо ввімкнено, буде показано оверлей продуктивности. Натисніть Shift+D під час гри, щоб увімкнути/вимкнути його.",
|
"performance_overlay_desc": "Увімкнення/вимкнення оверлея продуктивности. Якщо ввімкнено, буде показано оверлей продуктивности. Натисніть Shift+D під час гри, щоб увімкнути/вимкнути його.",
|
||||||
|
"performance_overlay_enabled": "Оверлей продуктивності увімкнено",
|
||||||
|
"performance_overlay_disabled": "Оверлей продуктивності вимкнено",
|
||||||
"easter_writing_speed_label": "Множник швидкості друку",
|
"easter_writing_speed_label": "Множник швидкості друку",
|
||||||
"easter_writing_speed_desc": "Налаштуйте швидкість, з якою ви удаєте, що програмуєте (x1–x100)",
|
"easter_writing_speed_desc": "Налаштуйте швидкість, з якою ви удаєте, що програмуєте (x1–x100)",
|
||||||
"easter_bug_count_label": "Кількість багів",
|
"easter_bug_count_label": "Кількість багів",
|
||||||
@@ -494,7 +499,8 @@
|
|||||||
"nation": "Нація",
|
"nation": "Нація",
|
||||||
"player": "Гравець",
|
"player": "Гравець",
|
||||||
"team": "Команда",
|
"team": "Команда",
|
||||||
"d_troops": "Оборонні війська",
|
"alliance_timeout": "Кінець союзу через",
|
||||||
|
"troops": "Війська",
|
||||||
"a_troops": "Наступальні війська",
|
"a_troops": "Наступальні війська",
|
||||||
"gold": "Золото",
|
"gold": "Золото",
|
||||||
"ports": "Порти",
|
"ports": "Порти",
|
||||||
@@ -511,14 +517,14 @@
|
|||||||
"retreating": "відступає",
|
"retreating": "відступає",
|
||||||
"boat": "Човен",
|
"boat": "Човен",
|
||||||
"alliance_request_status": "{name} {status} запрошення до союзу",
|
"alliance_request_status": "{name} {status} запрошення до союзу",
|
||||||
"alliance_accepted": "прийняв",
|
"alliance_accepted": "приймає",
|
||||||
"alliance_rejected": "відхилив",
|
"alliance_rejected": "відхиляє",
|
||||||
"duration_second": "1 сек",
|
"duration_second": "1 сек",
|
||||||
"betrayal_description": "Ви розірвали союз із {name}, ставши ЗРАДНИКОМ (оборону знижено на {malusPercent}% протягом {durationText})",
|
"betrayal_description": "Ви розірвали союз із {name}, ставши ЗРАДНИКОМ (оборону знижено на {malusPercent}% протягом {durationText})",
|
||||||
"duration_seconds_plural": "{seconds} сек",
|
"duration_seconds_plural": "{seconds} сек",
|
||||||
"betrayed_you": "{name} розірвав союз із вами",
|
"betrayed_you": "{name} розриває союз із вами",
|
||||||
"about_to_expire": "Ваш союз із {name} скоро закінчиться!",
|
"about_to_expire": "Союз із {name} скоро закінчиться!",
|
||||||
"alliance_expired": "Ваш союз із {name} закінчився",
|
"alliance_expired": "Союз із {name} закінчився",
|
||||||
"attack_request": "{name} просить вас атакувати {target}",
|
"attack_request": "{name} просить вас атакувати {target}",
|
||||||
"sent_emoji": "Надіслано {name}: {emoji}",
|
"sent_emoji": "Надіслано {name}: {emoji}",
|
||||||
"renew_alliance": "Запит на поновлення",
|
"renew_alliance": "Запит на поновлення",
|
||||||
@@ -526,7 +532,7 @@
|
|||||||
"focus": "Оглянути",
|
"focus": "Оглянути",
|
||||||
"accept_alliance": "Прийняти",
|
"accept_alliance": "Прийняти",
|
||||||
"reject_alliance": "Відхилити",
|
"reject_alliance": "Відхилити",
|
||||||
"alliance_renewed": "Ваш союз із {name} було поновлено",
|
"alliance_renewed": "Союз із {name} було поновлено",
|
||||||
"ignore": "Ігнорувати",
|
"ignore": "Ігнорувати",
|
||||||
"unit_voluntarily_deleted": "Об'єкт добровільно видалено"
|
"unit_voluntarily_deleted": "Об'єкт добровільно видалено"
|
||||||
},
|
},
|
||||||
@@ -585,7 +591,7 @@
|
|||||||
"choose_spawn": "Виберіть початкове розташування"
|
"choose_spawn": "Виберіть початкове розташування"
|
||||||
},
|
},
|
||||||
"territory_patterns": {
|
"territory_patterns": {
|
||||||
"title": "Виберіть візерунок території",
|
"title": "Вибір візерунка території",
|
||||||
"purchase": "Придбати",
|
"purchase": "Придбати",
|
||||||
"blocked": {
|
"blocked": {
|
||||||
"login": "Ви повинні ввійти, щоб отримати доступ до цього візерунку.",
|
"login": "Ви повинні ввійти, щоб отримати доступ до цього візерунку.",
|
||||||
@@ -644,5 +650,63 @@
|
|||||||
"radial_menu": {
|
"radial_menu": {
|
||||||
"delete_unit_title": "Видалити об'єкт",
|
"delete_unit_title": "Видалити об'єкт",
|
||||||
"delete_unit_description": "Клацніть, щоб видалити найближчий об'єкт"
|
"delete_unit_description": "Клацніть, щоб видалити найближчий об'єкт"
|
||||||
|
},
|
||||||
|
"discord_user_header": {
|
||||||
|
"avatar_alt": "Аватар"
|
||||||
|
},
|
||||||
|
"player_stats_table": {
|
||||||
|
"building_stats": "Статистика будівництва",
|
||||||
|
"ship_arrivals": "Прибуття кораблів",
|
||||||
|
"nuke_stats": "Статистика бомбардувань",
|
||||||
|
"player_metrics": "Статистика гравця",
|
||||||
|
"building": "Будівництво",
|
||||||
|
"ship_type": "Тип корабля",
|
||||||
|
"weapon": "Зброя",
|
||||||
|
"built": "Побудовано",
|
||||||
|
"destroyed": "Знищено",
|
||||||
|
"captured": "Захоплено",
|
||||||
|
"lost": "Втрачено",
|
||||||
|
"hits": "Удари",
|
||||||
|
"launched": "Запущено",
|
||||||
|
"landed": "Приземлено",
|
||||||
|
"sent": "Відправлено",
|
||||||
|
"arrived": "Прибуло",
|
||||||
|
"attack": "Атаки",
|
||||||
|
"received": "Отримано",
|
||||||
|
"cancelled": "Скасовано",
|
||||||
|
"count": "Кількість",
|
||||||
|
"gold": "Золото",
|
||||||
|
"workers": "Робітники",
|
||||||
|
"war": "Війни",
|
||||||
|
"trade": "Обмін",
|
||||||
|
"steal": "Украдено",
|
||||||
|
"unit": {
|
||||||
|
"city": "Місто",
|
||||||
|
"port": "Порт",
|
||||||
|
"defp": "Пункт оборони",
|
||||||
|
"saml": "Пускова установка ЗРК",
|
||||||
|
"silo": "Ракетна шахта",
|
||||||
|
"wshp": "Військовий корабель",
|
||||||
|
"fact": "Фабрика",
|
||||||
|
"trade": "Торговий корабель",
|
||||||
|
"trans": "Транспортний корабель",
|
||||||
|
"abomb": "Атомна бомба",
|
||||||
|
"hbomb": "Воднева бомба",
|
||||||
|
"mirv": "РГЧ ІН",
|
||||||
|
"mirvw": "Боєголовка РГЧ ІН"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"game_list": {
|
||||||
|
"recent_games": "Нещодавні ігри",
|
||||||
|
"game_id": "ID гри",
|
||||||
|
"mode": "Режим",
|
||||||
|
"mode_ffa": "Всі проти всіх",
|
||||||
|
"mode_team": "Команда",
|
||||||
|
"view": "Оглянути",
|
||||||
|
"details": "Подробиці",
|
||||||
|
"started": "Почато",
|
||||||
|
"map": "Мапа",
|
||||||
|
"difficulty": "Складність",
|
||||||
|
"type": "Тип"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { UserMeResponse } from "../core/ApiSchemas";
|
|||||||
import "./components/Difficulties";
|
import "./components/Difficulties";
|
||||||
import "./components/PatternButton";
|
import "./components/PatternButton";
|
||||||
import { discordLogin, getApiBase, getUserMe, logOut } from "./jwt";
|
import { discordLogin, getApiBase, getUserMe, logOut } from "./jwt";
|
||||||
import { translateText } from "./Utils";
|
import { isInIframe, translateText } from "./Utils";
|
||||||
|
|
||||||
@customElement("account-modal")
|
@customElement("account-modal")
|
||||||
export class AccountModal extends LitElement {
|
export class AccountModal extends LitElement {
|
||||||
@@ -268,6 +268,10 @@ export class AccountButton extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (isInIframe()) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.isVisible) {
|
if (!this.isVisible) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
GameID,
|
GameID,
|
||||||
GameRecord,
|
GameRecord,
|
||||||
GameStartInfo,
|
GameStartInfo,
|
||||||
PlayerPattern,
|
PlayerCosmeticRefs,
|
||||||
PlayerRecord,
|
PlayerRecord,
|
||||||
ServerMessage,
|
ServerMessage,
|
||||||
} from "../core/Schemas";
|
} from "../core/Schemas";
|
||||||
@@ -52,8 +52,7 @@ import { SoundManager } from "./sound/SoundManager";
|
|||||||
|
|
||||||
export interface LobbyConfig {
|
export interface LobbyConfig {
|
||||||
serverConfig: ServerConfig;
|
serverConfig: ServerConfig;
|
||||||
pattern: PlayerPattern | undefined;
|
cosmetics: PlayerCosmeticRefs;
|
||||||
flag: string;
|
|
||||||
playerName: string;
|
playerName: string;
|
||||||
clientID: ClientID;
|
clientID: ClientID;
|
||||||
gameID: GameID;
|
gameID: GameID;
|
||||||
|
|||||||
@@ -188,9 +188,10 @@ export class InputHandler {
|
|||||||
buildDefensePost: "Digit4",
|
buildDefensePost: "Digit4",
|
||||||
buildMissileSilo: "Digit5",
|
buildMissileSilo: "Digit5",
|
||||||
buildSamLauncher: "Digit6",
|
buildSamLauncher: "Digit6",
|
||||||
buildAtomBomb: "Digit7",
|
buildWarship: "Digit7",
|
||||||
buildHydrogenBomb: "Digit8",
|
buildAtomBomb: "Digit8",
|
||||||
buildWarship: "Digit9",
|
buildHydrogenBomb: "Digit9",
|
||||||
|
buildMIRV: "Digit0",
|
||||||
...saved,
|
...saved,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -409,6 +410,11 @@ export class InputHandler {
|
|||||||
this.uiState.ghostStructure = UnitType.Warship;
|
this.uiState.ghostStructure = UnitType.Warship;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.code === this.keybinds.buildMIRV) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.uiState.ghostStructure = UnitType.MIRV;
|
||||||
|
}
|
||||||
|
|
||||||
// Shift-D to toggle performance overlay
|
// Shift-D to toggle performance overlay
|
||||||
console.log(e.code, e.shiftKey, e.ctrlKey, e.altKey, e.metaKey);
|
console.log(e.code, e.shiftKey, e.ctrlKey, e.altKey, e.metaKey);
|
||||||
if (e.code === "KeyD" && e.shiftKey) {
|
if (e.code === "KeyD" && e.shiftKey) {
|
||||||
|
|||||||
+18
-7
@@ -34,6 +34,7 @@ import { UsernameInput } from "./UsernameInput";
|
|||||||
import {
|
import {
|
||||||
generateCryptoRandomUUID,
|
generateCryptoRandomUUID,
|
||||||
incrementGamesPlayed,
|
incrementGamesPlayed,
|
||||||
|
isInIframe,
|
||||||
translateText,
|
translateText,
|
||||||
} from "./Utils";
|
} from "./Utils";
|
||||||
import "./components/NewsButton";
|
import "./components/NewsButton";
|
||||||
@@ -207,6 +208,10 @@ class Client {
|
|||||||
const patternButton = document.getElementById(
|
const patternButton = document.getElementById(
|
||||||
"territory-patterns-input-preview-button",
|
"territory-patterns-input-preview-button",
|
||||||
);
|
);
|
||||||
|
if (isInIframe() && patternButton) {
|
||||||
|
patternButton.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
this.patternsModal instanceof TerritoryPatternsModal;
|
this.patternsModal instanceof TerritoryPatternsModal;
|
||||||
if (patternButton === null)
|
if (patternButton === null)
|
||||||
throw new Error("territory-patterns-input-preview-button");
|
throw new Error("territory-patterns-input-preview-button");
|
||||||
@@ -504,18 +509,24 @@ class Client {
|
|||||||
}
|
}
|
||||||
const config = await getServerConfigFromClient();
|
const config = await getServerConfigFromClient();
|
||||||
|
|
||||||
|
const pattern = this.userSettings.getSelectedPatternName(
|
||||||
|
await fetchCosmetics(),
|
||||||
|
);
|
||||||
|
|
||||||
this.gameStop = joinLobby(
|
this.gameStop = joinLobby(
|
||||||
this.eventBus,
|
this.eventBus,
|
||||||
{
|
{
|
||||||
gameID: lobby.gameID,
|
gameID: lobby.gameID,
|
||||||
serverConfig: config,
|
serverConfig: config,
|
||||||
pattern:
|
cosmetics: {
|
||||||
this.userSettings.getSelectedPatternName(await fetchCosmetics()) ??
|
color: this.userSettings.getSelectedColor() ?? undefined,
|
||||||
undefined,
|
patternName: pattern?.name ?? undefined,
|
||||||
flag:
|
patternColorPaletteName: pattern?.colorPalette?.name ?? undefined,
|
||||||
this.flagInput === null || this.flagInput.getCurrentFlag() === "xx"
|
flag:
|
||||||
? ""
|
this.flagInput === null || this.flagInput.getCurrentFlag() === "xx"
|
||||||
: this.flagInput.getCurrentFlag(),
|
? ""
|
||||||
|
: this.flagInput.getCurrentFlag(),
|
||||||
|
},
|
||||||
playerName: this.usernameInput?.getCurrentUsername() ?? "",
|
playerName: this.usernameInput?.getCurrentUsername() ?? "",
|
||||||
token: getPlayToken(),
|
token: getPlayToken(),
|
||||||
clientID: lobby.clientID,
|
clientID: lobby.clientID,
|
||||||
|
|||||||
@@ -449,6 +449,8 @@ export class SinglePlayerModal extends LitElement {
|
|||||||
? (this.userSettings.getDevOnlyPattern() ?? null)
|
? (this.userSettings.getDevOnlyPattern() ?? null)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const selectedColor = this.userSettings.getSelectedColor();
|
||||||
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("join-lobby", {
|
new CustomEvent("join-lobby", {
|
||||||
detail: {
|
detail: {
|
||||||
@@ -466,6 +468,7 @@ export class SinglePlayerModal extends LitElement {
|
|||||||
? ""
|
? ""
|
||||||
: flagInput.getCurrentFlag(),
|
: flagInput.getCurrentFlag(),
|
||||||
pattern: selectedPattern ?? undefined,
|
pattern: selectedPattern ?? undefined,
|
||||||
|
color: selectedColor ? { color: selectedColor } : undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export class TerritoryPatternsModal extends LitElement {
|
|||||||
public previewButton: HTMLElement | null = null;
|
public previewButton: HTMLElement | null = null;
|
||||||
|
|
||||||
@state() private selectedPattern: PlayerPattern | null;
|
@state() private selectedPattern: PlayerPattern | null;
|
||||||
|
@state() private selectedColor: string | null = null;
|
||||||
|
|
||||||
|
@state() private activeTab: "patterns" | "colors" = "patterns";
|
||||||
|
|
||||||
private cosmetics: Cosmetics | null = null;
|
private cosmetics: Cosmetics | null = null;
|
||||||
|
|
||||||
@@ -44,6 +47,7 @@ export class TerritoryPatternsModal extends LitElement {
|
|||||||
if (userMeResponse === null) {
|
if (userMeResponse === null) {
|
||||||
this.userSettings.setSelectedPatternName(undefined);
|
this.userSettings.setSelectedPatternName(undefined);
|
||||||
this.selectedPattern = null;
|
this.selectedPattern = null;
|
||||||
|
this.selectedColor = null;
|
||||||
}
|
}
|
||||||
this.userMeResponse = userMeResponse;
|
this.userMeResponse = userMeResponse;
|
||||||
this.cosmetics = await fetchCosmetics();
|
this.cosmetics = await fetchCosmetics();
|
||||||
@@ -51,6 +55,7 @@ export class TerritoryPatternsModal extends LitElement {
|
|||||||
this.cosmetics !== null
|
this.cosmetics !== null
|
||||||
? this.userSettings.getSelectedPatternName(this.cosmetics)
|
? this.userSettings.getSelectedPatternName(this.cosmetics)
|
||||||
: null;
|
: null;
|
||||||
|
this.selectedColor = this.userSettings.getSelectedColor() ?? null;
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +63,31 @@ export class TerritoryPatternsModal extends LitElement {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderTabNavigation(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="flex border-b border-gray-600 mb-4 justify-center">
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 text-sm font-medium transition-colors duration-200 ${this
|
||||||
|
.activeTab === "patterns"
|
||||||
|
? "text-blue-400 border-b-2 border-blue-400 bg-blue-400/10"
|
||||||
|
: "text-gray-400 hover:text-white"}"
|
||||||
|
@click=${() => (this.activeTab = "patterns")}
|
||||||
|
>
|
||||||
|
${translateText("territory_patterns.title")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 text-sm font-medium transition-colors duration-200 ${this
|
||||||
|
.activeTab === "colors"
|
||||||
|
? "text-blue-400 border-b-2 border-blue-400 bg-blue-400/10"
|
||||||
|
: "text-gray-400 hover:text-white"}"
|
||||||
|
@click=${() => (this.activeTab = "colors")}
|
||||||
|
>
|
||||||
|
${translateText("territory_patterns.colors")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
private renderPatternGrid(): TemplateResult {
|
private renderPatternGrid(): TemplateResult {
|
||||||
const buttons: TemplateResult[] = [];
|
const buttons: TemplateResult[] = [];
|
||||||
for (const pattern of Object.values(this.cosmetics?.patterns ?? {})) {
|
for (const pattern of Object.values(this.cosmetics?.patterns ?? {})) {
|
||||||
@@ -105,14 +135,39 @@ export class TerritoryPatternsModal extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderColorSwatchGrid(): TemplateResult {
|
||||||
|
const hexCodes = (this.userMeResponse?.player.flares ?? [])
|
||||||
|
.filter((flare) => flare.startsWith("color:"))
|
||||||
|
.map((flare) => "#" + flare.split(":")[1]);
|
||||||
|
return html`
|
||||||
|
<div class="flex flex-wrap gap-3 p-2 justify-center items-center">
|
||||||
|
${hexCodes.map(
|
||||||
|
(hexCode) => html`
|
||||||
|
<div
|
||||||
|
class="w-12 h-12 rounded-lg border-2 border-white/30 cursor-pointer transition-all duration-200 hover:scale-110 hover:shadow-lg"
|
||||||
|
style="background-color: ${hexCode};"
|
||||||
|
title="${hexCode}"
|
||||||
|
@click=${() => this.selectColor(hexCode)}
|
||||||
|
></div>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.isActive) return html``;
|
if (!this.isActive) return html``;
|
||||||
return html`
|
return html`
|
||||||
<o-modal
|
<o-modal
|
||||||
id="territoryPatternsModal"
|
id="territoryPatternsModal"
|
||||||
title="${translateText("territory_patterns.title")}"
|
title="${this.activeTab === "patterns"
|
||||||
|
? translateText("territory_patterns.title")
|
||||||
|
: translateText("territory_patterns.colors")}"
|
||||||
>
|
>
|
||||||
${this.renderPatternGrid()}
|
${this.renderTabNavigation()}
|
||||||
|
${this.activeTab === "patterns"
|
||||||
|
? this.renderPatternGrid()
|
||||||
|
: this.renderColorSwatchGrid()}
|
||||||
</o-modal>
|
</o-modal>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -130,6 +185,8 @@ export class TerritoryPatternsModal extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private selectPattern(pattern: PlayerPattern | null) {
|
private selectPattern(pattern: PlayerPattern | null) {
|
||||||
|
this.selectedColor = null;
|
||||||
|
this.userSettings.setSelectedColor(undefined);
|
||||||
if (pattern === null) {
|
if (pattern === null) {
|
||||||
this.userSettings.setSelectedPatternName(undefined);
|
this.userSettings.setSelectedPatternName(undefined);
|
||||||
} else {
|
} else {
|
||||||
@@ -145,8 +202,32 @@ export class TerritoryPatternsModal extends LitElement {
|
|||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private selectColor(hexCode: string) {
|
||||||
|
this.selectedPattern = null;
|
||||||
|
this.userSettings.setSelectedPatternName(undefined);
|
||||||
|
this.selectedColor = hexCode;
|
||||||
|
this.userSettings.setSelectedColor(hexCode);
|
||||||
|
this.refresh();
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderColorPreview(
|
||||||
|
hexCode: string,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="rounded"
|
||||||
|
style="width: ${width}px; height: ${height}px; background-color: ${hexCode};"
|
||||||
|
></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
public async refresh() {
|
public async refresh() {
|
||||||
const preview = renderPatternPreview(this.selectedPattern ?? null, 48, 48);
|
const preview = this.selectedColor
|
||||||
|
? this.renderColorPreview(this.selectedColor, 48, 48)
|
||||||
|
: renderPatternPreview(this.selectedPattern ?? null, 48, 48);
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
|
|
||||||
// Wait for the DOM to be updated and the o-modal element to be available
|
// Wait for the DOM to be updated and the o-modal element to be available
|
||||||
|
|||||||
@@ -377,11 +377,7 @@ export class Transport {
|
|||||||
lastTurn: numTurns,
|
lastTurn: numTurns,
|
||||||
token: this.lobbyConfig.token,
|
token: this.lobbyConfig.token,
|
||||||
username: this.lobbyConfig.playerName,
|
username: this.lobbyConfig.playerName,
|
||||||
cosmetics: {
|
cosmetics: this.lobbyConfig.cosmetics,
|
||||||
flag: this.lobbyConfig.flag,
|
|
||||||
patternName: this.lobbyConfig.pattern?.name,
|
|
||||||
patternColorPaletteName: this.lobbyConfig.pattern?.colorPalette?.name,
|
|
||||||
},
|
|
||||||
} satisfies ClientJoinMessage);
|
} satisfies ClientJoinMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -494,11 +494,20 @@ export class UserSettingModal extends LitElement {
|
|||||||
@change=${this.handleKeybindChange}
|
@change=${this.handleKeybindChange}
|
||||||
></setting-keybind>
|
></setting-keybind>
|
||||||
|
|
||||||
|
<setting-keybind
|
||||||
|
action="buildWarship"
|
||||||
|
label=${translateText("user_setting.build_warship")}
|
||||||
|
description=${translateText("user_setting.build_warship_desc")}
|
||||||
|
defaultKey="Digit7"
|
||||||
|
.value=${this.keybinds["buildWarship"]?.key ?? ""}
|
||||||
|
@change=${this.handleKeybindChange}
|
||||||
|
></setting-keybind>
|
||||||
|
|
||||||
<setting-keybind
|
<setting-keybind
|
||||||
action="buildAtomBomb"
|
action="buildAtomBomb"
|
||||||
label=${translateText("user_setting.build_atom_bomb")}
|
label=${translateText("user_setting.build_atom_bomb")}
|
||||||
description=${translateText("user_setting.build_atom_bomb_desc")}
|
description=${translateText("user_setting.build_atom_bomb_desc")}
|
||||||
defaultKey="Digit7"
|
defaultKey="Digit8"
|
||||||
.value=${this.keybinds["buildAtomBomb"]?.key ?? ""}
|
.value=${this.keybinds["buildAtomBomb"]?.key ?? ""}
|
||||||
@change=${this.handleKeybindChange}
|
@change=${this.handleKeybindChange}
|
||||||
></setting-keybind>
|
></setting-keybind>
|
||||||
@@ -507,17 +516,17 @@ export class UserSettingModal extends LitElement {
|
|||||||
action="buildHydrogenBomb"
|
action="buildHydrogenBomb"
|
||||||
label=${translateText("user_setting.build_hydrogen_bomb")}
|
label=${translateText("user_setting.build_hydrogen_bomb")}
|
||||||
description=${translateText("user_setting.build_hydrogen_bomb_desc")}
|
description=${translateText("user_setting.build_hydrogen_bomb_desc")}
|
||||||
defaultKey="Digit8"
|
defaultKey="Digit9"
|
||||||
.value=${this.keybinds["buildHydrogenBomb"]?.key ?? ""}
|
.value=${this.keybinds["buildHydrogenBomb"]?.key ?? ""}
|
||||||
@change=${this.handleKeybindChange}
|
@change=${this.handleKeybindChange}
|
||||||
></setting-keybind>
|
></setting-keybind>
|
||||||
|
|
||||||
<setting-keybind
|
<setting-keybind
|
||||||
action="buildWarship"
|
action="buildMIRV"
|
||||||
label=${translateText("user_setting.build_warship")}
|
label=${translateText("user_setting.build_MIRV")}
|
||||||
description=${translateText("user_setting.build_warship_desc")}
|
description=${translateText("user_setting.build_MIRV_desc")}
|
||||||
defaultKey="Digit9"
|
defaultKey="Digit0"
|
||||||
.value=${this.keybinds["buildWarship"]?.key ?? ""}
|
.value=${this.keybinds["buildMIRV"]?.key ?? ""}
|
||||||
@change=${this.handleKeybindChange}
|
@change=${this.handleKeybindChange}
|
||||||
></setting-keybind>
|
></setting-keybind>
|
||||||
|
|
||||||
|
|||||||
@@ -235,3 +235,13 @@ export function incrementGamesPlayed(): void {
|
|||||||
console.warn("Failed to increment games played in localStorage:", error);
|
console.warn("Failed to increment games played in localStorage:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isInIframe(): boolean {
|
||||||
|
try {
|
||||||
|
return window.self !== window.top;
|
||||||
|
} catch (e) {
|
||||||
|
// If we can't access window.top due to cross-origin restrictions,
|
||||||
|
// we're definitely in an iframe
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export class FxLayer implements Layer {
|
|||||||
const my = this.game.myPlayer();
|
const my = this.game.myPlayer();
|
||||||
if (!my) return;
|
if (!my) return;
|
||||||
if (unit.owner() !== my) return;
|
if (unit.owner() !== my) return;
|
||||||
if (!unit.isActive()) return;
|
if (!unit.isActive() || unit.retreating()) return;
|
||||||
if (this.boatTargetFxByUnitId.has(unit.id())) return;
|
if (this.boatTargetFxByUnitId.has(unit.id())) return;
|
||||||
const t = unit.targetTile();
|
const t = unit.targetTile();
|
||||||
if (t !== undefined) {
|
if (t !== undefined) {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { LitElement, TemplateResult, html } from "lit";
|
|||||||
import { ref } from "lit-html/directives/ref.js";
|
import { ref } from "lit-html/directives/ref.js";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
import allianceIcon from "../../../../resources/images/AllianceIcon.svg";
|
import allianceIcon from "../../../../resources/images/AllianceIcon.svg";
|
||||||
import portIcon from "../../../../resources/images/AnchorIcon.png";
|
|
||||||
import warshipIcon from "../../../../resources/images/BattleshipIconWhite.svg";
|
import warshipIcon from "../../../../resources/images/BattleshipIconWhite.svg";
|
||||||
import cityIcon from "../../../../resources/images/CityIconWhite.svg";
|
import cityIcon from "../../../../resources/images/CityIconWhite.svg";
|
||||||
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
|
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
|
||||||
import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg";
|
import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg";
|
||||||
import missileSiloIcon from "../../../../resources/images/MissileSiloUnit.png";
|
import missileSiloIcon from "../../../../resources/images/MissileSiloIconWhite.svg";
|
||||||
|
import portIcon from "../../../../resources/images/PortIcon.svg";
|
||||||
import samLauncherIcon from "../../../../resources/images/SamLauncherIconWhite.svg";
|
import samLauncherIcon from "../../../../resources/images/SamLauncherIconWhite.svg";
|
||||||
import { renderPlayerFlag } from "../../../core/CustomFlag";
|
import { renderPlayerFlag } from "../../../core/CustomFlag";
|
||||||
import { EventBus } from "../../../core/EventBus";
|
import { EventBus } from "../../../core/EventBus";
|
||||||
@@ -364,18 +364,18 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
|||||||
cityIcon,
|
cityIcon,
|
||||||
"player_info_overlay.cities",
|
"player_info_overlay.cities",
|
||||||
)}
|
)}
|
||||||
${this.displayUnitCount(
|
|
||||||
player,
|
|
||||||
UnitType.Port,
|
|
||||||
portIcon,
|
|
||||||
"player_info_overlay.ports",
|
|
||||||
)}
|
|
||||||
${this.displayUnitCount(
|
${this.displayUnitCount(
|
||||||
player,
|
player,
|
||||||
UnitType.Factory,
|
UnitType.Factory,
|
||||||
factoryIcon,
|
factoryIcon,
|
||||||
"player_info_overlay.factories",
|
"player_info_overlay.factories",
|
||||||
)}
|
)}
|
||||||
|
${this.displayUnitCount(
|
||||||
|
player,
|
||||||
|
UnitType.Port,
|
||||||
|
portIcon,
|
||||||
|
"player_info_overlay.ports",
|
||||||
|
)}
|
||||||
${this.displayUnitCount(
|
${this.displayUnitCount(
|
||||||
player,
|
player,
|
||||||
UnitType.MissileSilo,
|
UnitType.MissileSilo,
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ import { CloseViewEvent, MouseUpEvent } from "../../InputHandler";
|
|||||||
import {
|
import {
|
||||||
SendAllianceRequestIntentEvent,
|
SendAllianceRequestIntentEvent,
|
||||||
SendBreakAllianceIntentEvent,
|
SendBreakAllianceIntentEvent,
|
||||||
SendDonateGoldIntentEvent,
|
|
||||||
SendDonateTroopsIntentEvent,
|
|
||||||
SendEmbargoIntentEvent,
|
SendEmbargoIntentEvent,
|
||||||
SendEmojiIntentEvent,
|
SendEmojiIntentEvent,
|
||||||
SendTargetPlayerIntentEvent,
|
SendTargetPlayerIntentEvent,
|
||||||
@@ -44,6 +42,7 @@ import { UIState } from "../UIState";
|
|||||||
import { ChatModal } from "./ChatModal";
|
import { ChatModal } from "./ChatModal";
|
||||||
import { EmojiTable } from "./EmojiTable";
|
import { EmojiTable } from "./EmojiTable";
|
||||||
import { Layer } from "./Layer";
|
import { Layer } from "./Layer";
|
||||||
|
import "./SendResourceModal";
|
||||||
|
|
||||||
@customElement("player-panel")
|
@customElement("player-panel")
|
||||||
export class PlayerPanel extends LitElement implements Layer {
|
export class PlayerPanel extends LitElement implements Layer {
|
||||||
@@ -51,21 +50,17 @@ export class PlayerPanel extends LitElement implements Layer {
|
|||||||
public eventBus: EventBus;
|
public eventBus: EventBus;
|
||||||
public emojiTable: EmojiTable;
|
public emojiTable: EmojiTable;
|
||||||
public uiState: UIState;
|
public uiState: UIState;
|
||||||
|
|
||||||
private actions: PlayerActions | null = null;
|
private actions: PlayerActions | null = null;
|
||||||
private tile: TileRef | null = null;
|
private tile: TileRef | null = null;
|
||||||
private _profileForPlayerId: number | null = null;
|
private _profileForPlayerId: number | null = null;
|
||||||
|
|
||||||
@state()
|
@state() private sendTarget: PlayerView | null = null;
|
||||||
public isVisible: boolean = false;
|
@state() private sendMode: "troops" | "gold" | "none" = "none";
|
||||||
|
@state() public isVisible: boolean = false;
|
||||||
@state()
|
@state() private allianceExpiryText: string | null = null;
|
||||||
private allianceExpiryText: string | null = null;
|
@state() private allianceExpirySeconds: number | null = null;
|
||||||
|
@state() private otherProfile: PlayerProfile | null = null;
|
||||||
@state()
|
|
||||||
private allianceExpirySeconds: number | null = null;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private otherProfile: PlayerProfile | null = null;
|
|
||||||
|
|
||||||
private ctModal: ChatModal;
|
private ctModal: ChatModal;
|
||||||
|
|
||||||
@@ -138,6 +133,8 @@ export class PlayerPanel extends LitElement implements Layer {
|
|||||||
|
|
||||||
public hide() {
|
public hide() {
|
||||||
this.isVisible = false;
|
this.isVisible = false;
|
||||||
|
this.sendMode = "none";
|
||||||
|
this.sendTarget = null;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,19 +163,23 @@ export class PlayerPanel extends LitElement implements Layer {
|
|||||||
this.hide();
|
this.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openSendTroops(target: PlayerView) {
|
||||||
|
this.sendTarget = target;
|
||||||
|
this.sendMode = "troops";
|
||||||
|
}
|
||||||
|
|
||||||
|
private openSendGold(target: PlayerView) {
|
||||||
|
this.sendTarget = target;
|
||||||
|
this.sendMode = "gold";
|
||||||
|
}
|
||||||
|
|
||||||
private handleDonateTroopClick(
|
private handleDonateTroopClick(
|
||||||
e: Event,
|
e: Event,
|
||||||
myPlayer: PlayerView,
|
myPlayer: PlayerView,
|
||||||
other: PlayerView,
|
other: PlayerView,
|
||||||
) {
|
) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.eventBus.emit(
|
this.openSendTroops(other);
|
||||||
new SendDonateTroopsIntentEvent(
|
|
||||||
other,
|
|
||||||
myPlayer.troops() * this.uiState.attackRatio,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
this.hide();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleDonateGoldClick(
|
private handleDonateGoldClick(
|
||||||
@@ -187,10 +188,20 @@ export class PlayerPanel extends LitElement implements Layer {
|
|||||||
other: PlayerView,
|
other: PlayerView,
|
||||||
) {
|
) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.eventBus.emit(new SendDonateGoldIntentEvent(other, null));
|
this.openSendGold(other);
|
||||||
this.hide();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private closeSend = () => {
|
||||||
|
this.sendTarget = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
private confirmSend = (
|
||||||
|
e: CustomEvent<{ amount: number; closePanel?: boolean }>,
|
||||||
|
) => {
|
||||||
|
this.closeSend();
|
||||||
|
if (e.detail?.closePanel) this.hide();
|
||||||
|
};
|
||||||
|
|
||||||
private handleEmbargoClick(
|
private handleEmbargoClick(
|
||||||
e: Event,
|
e: Event,
|
||||||
myPlayer: PlayerView,
|
myPlayer: PlayerView,
|
||||||
@@ -312,10 +323,11 @@ export class PlayerPanel extends LitElement implements Layer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getExpiryColorClass(seconds: number | null): string {
|
private getExpiryColorClass(seconds: number | null): string {
|
||||||
if (seconds === null) return "text-white";
|
if (seconds === null) return "text-white"; // Default color
|
||||||
if (seconds <= 30) return "text-red-400";
|
|
||||||
if (seconds <= 60) return "text-yellow-400";
|
if (seconds <= 30) return "text-red-400"; // Last 30 seconds: Red
|
||||||
return "text-emerald-400";
|
if (seconds <= 60) return "text-yellow-400"; // Last 60 seconds: Yellow
|
||||||
|
return "text-emerald-400"; // More than 60 seconds: Green
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTraitorRemainingSeconds(player: PlayerView): number | null {
|
private getTraitorRemainingSeconds(player: PlayerView): number | null {
|
||||||
@@ -433,31 +445,27 @@ export class PlayerPanel extends LitElement implements Layer {
|
|||||||
return html`
|
return html`
|
||||||
<div class="mb-1 flex justify-between gap-2">
|
<div class="mb-1 flex justify-between gap-2">
|
||||||
<div
|
<div
|
||||||
class="inline-flex items-center gap-1.5 rounded-full bg-zinc-800 px-2.5 py-1
|
class="inline-flex items-center gap-1.5 rounded-full bg-white/[0.04] px-2.5 py-1
|
||||||
text-lg font-semibold text-zinc-100"
|
text-base font-semibold text-zinc-200"
|
||||||
>
|
>
|
||||||
<span class="mr-0.5">💰</span>
|
<span class="mr-0.5">💰</span>
|
||||||
<span
|
<span translate="no" class="inline-block w-[45px] text-right">
|
||||||
translate="no"
|
|
||||||
class="inline-block w-[45px] text-right text-zinc-50"
|
|
||||||
>
|
|
||||||
${renderNumber(other.gold() || 0)}
|
${renderNumber(other.gold() || 0)}
|
||||||
</span>
|
</span>
|
||||||
<span class="opacity-95">${translateText("player_panel.gold")}</span>
|
<span class="opacity-95 whitespace-nowrap"
|
||||||
|
>${translateText("player_panel.gold")}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="inline-flex items-center gap-1.5 rounded-full bg-zinc-800 px-2.5 py-1
|
class="inline-flex items-center gap-1.5 rounded-full bg-white/[0.04] px-2.5 py-1
|
||||||
text-lg font-semibold text-zinc-100"
|
text-base font-semibold text-zinc-200"
|
||||||
>
|
>
|
||||||
<span class="mr-0.5">🛡️</span>
|
<span class="mr-0.5">🛡️</span>
|
||||||
<span
|
<span translate="no" class="inline-block w-[45px] text-right">
|
||||||
translate="no"
|
|
||||||
class="inline-block w-[45px] text-right text-zinc-50"
|
|
||||||
>
|
|
||||||
${renderTroops(other.troops() || 0)}
|
${renderTroops(other.troops() || 0)}
|
||||||
</span>
|
</span>
|
||||||
<span class="opacity-95"
|
<span class="opacity-95 whitespace-nowrap"
|
||||||
>${translateText("player_panel.troops")}</span
|
>${translateText("player_panel.troops")}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -554,7 +562,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
|||||||
})
|
})
|
||||||
: html`
|
: html`
|
||||||
<div class="py-2 text-zinc-300">
|
<div class="py-2 text-zinc-300">
|
||||||
${translateText("player_panel.none")}
|
${translateText("common.none")}
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
@@ -567,7 +575,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
|||||||
if (this.allianceExpiryText === null) return html``;
|
if (this.allianceExpiryText === null) return html``;
|
||||||
return html`
|
return html`
|
||||||
<div class="grid grid-cols-[auto,1fr] gap-x-6 gap-y-2 text-base">
|
<div class="grid grid-cols-[auto,1fr] gap-x-6 gap-y-2 text-base">
|
||||||
<div class="font-semibold text-zinc-400">
|
<div class="font-semibold text-zinc-300">
|
||||||
${translateText("player_panel.alliance_time_remaining")}
|
${translateText("player_panel.alliance_time_remaining")}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right font-semibold">
|
<div class="text-right font-semibold">
|
||||||
@@ -713,6 +721,8 @@ export class PlayerPanel extends LitElement implements Layer {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
const other = owner as PlayerView;
|
const other = owner as PlayerView;
|
||||||
|
const myGoldNum = my.gold();
|
||||||
|
const myTroopsNum = Number(my.troops());
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
@@ -744,12 +754,11 @@ export class PlayerPanel extends LitElement implements Layer {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 z-[1001] flex items-center justify-center overflow-auto
|
class="fixed inset-0 z-[1001] flex items-center justify-center overflow-auto
|
||||||
bg-black/40 backdrop-blur-sm backdrop-brightness-110 pointer-events-auto"
|
bg-black/15 backdrop-blur-sm backdrop-brightness-110 pointer-events-auto"
|
||||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||||
@wheel=${(e: MouseEvent) => e.stopPropagation()}
|
@wheel=${(e: MouseEvent) => e.stopPropagation()}
|
||||||
@click=${() => this.hide()}
|
@click=${() => this.hide()}
|
||||||
>
|
>
|
||||||
<!-- Stop clicks inside the panel from closing it -->
|
|
||||||
<div
|
<div
|
||||||
class="pointer-events-auto max-h-[90vh] overflow-y-auto min-w-[240px] w-auto px-4 py-2"
|
class="pointer-events-auto max-h-[90vh] overflow-y-auto min-w-[240px] w-auto px-4 py-2"
|
||||||
@click=${(e: MouseEvent) => e.stopPropagation()}
|
@click=${(e: MouseEvent) => e.stopPropagation()}
|
||||||
@@ -763,8 +772,8 @@ export class PlayerPanel extends LitElement implements Layer {
|
|||||||
@click=${this.handleClose}
|
@click=${this.handleClose}
|
||||||
class="absolute -top-3 -right-3 flex h-7 w-7 items-center justify-center
|
class="absolute -top-3 -right-3 flex h-7 w-7 items-center justify-center
|
||||||
rounded-full bg-zinc-700 text-white shadow hover:bg-red-500 transition-colors"
|
rounded-full bg-zinc-700 text-white shadow hover:bg-red-500 transition-colors"
|
||||||
aria-label=${translateText("player_panel.close") || "Close"}
|
aria-label=${translateText("common.close") || "Close"}
|
||||||
title=${translateText("player_panel.close") || "Close"}
|
title=${translateText("common.close") || "Close"}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
@@ -775,6 +784,28 @@ export class PlayerPanel extends LitElement implements Layer {
|
|||||||
<!-- Identity (flag, name, type, traitor, relation) -->
|
<!-- Identity (flag, name, type, traitor, relation) -->
|
||||||
<div class="mb-1">${this.renderIdentityRow(other, my)}</div>
|
<div class="mb-1">${this.renderIdentityRow(other, my)}</div>
|
||||||
|
|
||||||
|
${this.sendTarget
|
||||||
|
? html`
|
||||||
|
<send-resource-modal
|
||||||
|
.open=${this.sendMode !== "none"}
|
||||||
|
.mode=${this.sendMode}
|
||||||
|
.total=${this.sendMode === "troops"
|
||||||
|
? myTroopsNum
|
||||||
|
: myGoldNum}
|
||||||
|
.uiState=${this.uiState}
|
||||||
|
.myPlayer=${my}
|
||||||
|
.target=${this.sendTarget}
|
||||||
|
.gameView=${this.g}
|
||||||
|
.eventBus=${this.eventBus}
|
||||||
|
.format=${this.sendMode === "troops"
|
||||||
|
? renderTroops
|
||||||
|
: renderNumber}
|
||||||
|
@confirm=${this.confirmSend}
|
||||||
|
@close=${this.closeSend}
|
||||||
|
></send-resource-modal>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
|
||||||
<ui-divider></ui-divider>
|
<ui-divider></ui-divider>
|
||||||
|
|
||||||
<!-- Resources -->
|
<!-- Resources -->
|
||||||
|
|||||||
@@ -96,11 +96,15 @@ export class RailroadLayer implements Layer {
|
|||||||
|
|
||||||
renderLayer(context: CanvasRenderingContext2D) {
|
renderLayer(context: CanvasRenderingContext2D) {
|
||||||
this.updateRailColors();
|
this.updateRailColors();
|
||||||
if (this.transformHandler.scale <= 2) {
|
const scale = this.transformHandler.scale;
|
||||||
// When zoomed out, don't show the railroads
|
if (scale <= 1) {
|
||||||
// to prevent map clutter.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const rawAlpha = (scale - 1) / (2 - 1); // maps 1->0, 2->1
|
||||||
|
const alpha = Math.max(0, Math.min(1, rawAlpha));
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.globalAlpha = alpha;
|
||||||
context.drawImage(
|
context.drawImage(
|
||||||
this.canvas,
|
this.canvas,
|
||||||
-this.game.width() / 2,
|
-this.game.width() / 2,
|
||||||
@@ -108,14 +112,11 @@ export class RailroadLayer implements Layer {
|
|||||||
this.game.width(),
|
this.game.width(),
|
||||||
this.game.height(),
|
this.game.height(),
|
||||||
);
|
);
|
||||||
|
context.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleRailroadRendering(railUpdate: RailroadUpdate) {
|
private handleRailroadRendering(railUpdate: RailroadUpdate) {
|
||||||
for (const railRoad of railUpdate.railTiles) {
|
for (const railRoad of railUpdate.railTiles) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const x = this.game.x(railRoad.tile);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const y = this.game.y(railRoad.tile);
|
|
||||||
if (railUpdate.isActive) {
|
if (railUpdate.isActive) {
|
||||||
this.paintRailroad(railRoad);
|
this.paintRailroad(railRoad);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,588 @@
|
|||||||
|
import { html, LitElement } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
|
import { EventBus } from "../../../core/EventBus";
|
||||||
|
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||||
|
import { within } from "../../../core/Util";
|
||||||
|
import {
|
||||||
|
SendDonateGoldIntentEvent,
|
||||||
|
SendDonateTroopsIntentEvent,
|
||||||
|
} from "../../Transport";
|
||||||
|
import { renderTroops, translateText } from "../../Utils";
|
||||||
|
import { UIState } from "../UIState";
|
||||||
|
|
||||||
|
@customElement("send-resource-modal")
|
||||||
|
export class SendResourceModal extends LitElement {
|
||||||
|
@property({ attribute: false }) eventBus: EventBus | null = null;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) open: boolean = false;
|
||||||
|
@property({ type: String }) mode: "troops" | "gold" = "troops";
|
||||||
|
|
||||||
|
@property({ type: Object }) total: number | bigint = 0;
|
||||||
|
@property({ type: Object }) uiState: UIState | null = null; // to seed initial %
|
||||||
|
@property({ attribute: false }) format: (n: number) => string = renderTroops;
|
||||||
|
|
||||||
|
@property({ attribute: false }) myPlayer: PlayerView | null = null;
|
||||||
|
@property({ attribute: false }) target: PlayerView | null = null;
|
||||||
|
@property({ attribute: false }) gameView: GameView | null = null;
|
||||||
|
|
||||||
|
@property({ type: String }) heading: string | null = null;
|
||||||
|
|
||||||
|
@state() private sendAmount: number = 0;
|
||||||
|
@state() private selectedPercent: number | null = null;
|
||||||
|
|
||||||
|
private PRESETS = [10, 25, 50, 75, 100] as const;
|
||||||
|
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
const initPct =
|
||||||
|
this.uiState && typeof this.uiState.attackRatio === "number"
|
||||||
|
? Math.round(this.uiState.attackRatio * 100)
|
||||||
|
: 100;
|
||||||
|
this.selectedPercent = this.sanitizePercent(initPct);
|
||||||
|
|
||||||
|
const basis = this.getPercentBasis();
|
||||||
|
this.sendAmount = this.clampSend(
|
||||||
|
Math.floor((basis * this.selectedPercent) / 100),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updated(changed: Map<string, unknown>) {
|
||||||
|
if (changed.has("open") && this.open) {
|
||||||
|
// If either side is dead, just close and do nothing
|
||||||
|
if (!this.isSenderAlive() || !this.isTargetAlive()) {
|
||||||
|
this.closeModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queueMicrotask(() =>
|
||||||
|
(this.querySelector('[role="dialog"]') as HTMLElement | null)?.focus(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
changed.has("total") ||
|
||||||
|
changed.has("mode") ||
|
||||||
|
changed.has("target") ||
|
||||||
|
changed.has("gameView")
|
||||||
|
) {
|
||||||
|
const basis = this.getPercentBasis();
|
||||||
|
if (this.selectedPercent !== null) {
|
||||||
|
const pct = this.sanitizePercent(this.selectedPercent);
|
||||||
|
const raw = Math.floor((basis * pct) / 100);
|
||||||
|
this.sendAmount = this.clampSend(raw);
|
||||||
|
} else {
|
||||||
|
this.sendAmount = this.clampSend(this.sendAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeModal() {
|
||||||
|
this.dispatchEvent(new CustomEvent("close"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private confirm() {
|
||||||
|
if (!this.isSenderAlive() || !this.isTargetAlive() || !this.eventBus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const myPlayer = this.myPlayer;
|
||||||
|
const target = this.target;
|
||||||
|
const amount = this.limitAmount(this.sendAmount);
|
||||||
|
|
||||||
|
if (!myPlayer || !target || amount <= 0) return;
|
||||||
|
|
||||||
|
if (this.mode === "troops") {
|
||||||
|
const myTroops = Number(myPlayer.troops());
|
||||||
|
if (amount > myTroops) return;
|
||||||
|
this.eventBus.emit(new SendDonateTroopsIntentEvent(target, amount));
|
||||||
|
} else {
|
||||||
|
const myGold = Number(myPlayer.gold());
|
||||||
|
if (amount > myGold) return;
|
||||||
|
this.eventBus.emit(new SendDonateGoldIntentEvent(target, BigInt(amount)));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("confirm", {
|
||||||
|
detail: { amount, closePanel: true, success: true },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleKeydown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.closeModal();
|
||||||
|
}
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.confirm();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private toNum(x: unknown): number {
|
||||||
|
if (typeof x === "bigint") return Number(x);
|
||||||
|
return Number(x ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTotalNumber(): number {
|
||||||
|
const base = this.toNum(this.total);
|
||||||
|
return this.isSenderAlive() ? base : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sanitizePercent(p: number) {
|
||||||
|
return within(p, 0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Internal capacity only for troops; gold is unlimited. */
|
||||||
|
private getCapacityLeft(): number | null {
|
||||||
|
if (!this.isTargetAlive()) return 0;
|
||||||
|
if (this.mode !== "troops") return null;
|
||||||
|
if (!this.gameView || !this.target) return null;
|
||||||
|
const current = this.toNum(this.target.troops());
|
||||||
|
const max = this.toNum(this.gameView.config().maxTroops(this.target));
|
||||||
|
return Math.max(0, max - current);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPercentBasis(): number {
|
||||||
|
return this.getTotalNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
private limitAmount(proposed: number): number {
|
||||||
|
const cap = this.getCapacityLeft();
|
||||||
|
const total = this.getTotalNumber();
|
||||||
|
const hardMax = cap === null ? total : Math.min(total, cap);
|
||||||
|
return within(proposed, 0, hardMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clampSend(n: number) {
|
||||||
|
const total = this.getTotalNumber();
|
||||||
|
const byTotal = within(n, 0, total);
|
||||||
|
return this.limitAmount(byTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private percentOfBasis(n: number): number {
|
||||||
|
const basis = this.getPercentBasis();
|
||||||
|
return basis ? Math.round((n / basis) * 100) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private keepAfter(allowed: number): number {
|
||||||
|
const total = this.getTotalNumber();
|
||||||
|
return Math.max(0, total - allowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFillColor(): string {
|
||||||
|
return this.mode === "troops"
|
||||||
|
? "rgb(168 85 247)" /* purple */
|
||||||
|
: "rgb(234 179 8)" /* amber */;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMinKeepRatio(): number {
|
||||||
|
return this.mode === "troops" ? 0.3 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isTargetAlive(): boolean {
|
||||||
|
return this.target?.isAlive() ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isSenderAlive(): boolean {
|
||||||
|
return this.myPlayer?.isAlive() ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private i18n = {
|
||||||
|
title: (name: string) =>
|
||||||
|
this.mode === "troops"
|
||||||
|
? translateText("send_troops_modal.title_with_name", { name })
|
||||||
|
: translateText("send_gold_modal.title_with_name", { name }),
|
||||||
|
|
||||||
|
availableChip: () => translateText("common.available"),
|
||||||
|
|
||||||
|
availableTooltip: () =>
|
||||||
|
this.mode === "troops"
|
||||||
|
? translateText("send_troops_modal.available_tooltip")
|
||||||
|
: translateText("send_gold_modal.available_tooltip"),
|
||||||
|
|
||||||
|
max: () => translateText("common.preset_max"),
|
||||||
|
|
||||||
|
ariaSlider: () =>
|
||||||
|
this.mode === "troops"
|
||||||
|
? translateText("send_troops_modal.aria_slider")
|
||||||
|
: translateText("send_gold_modal.aria_slider"),
|
||||||
|
|
||||||
|
summarySend: () => translateText("common.summary_send"),
|
||||||
|
summaryKeep: () => translateText("common.summary_keep"),
|
||||||
|
|
||||||
|
closeLabel: () => translateText("common.close"),
|
||||||
|
cancel: () => translateText("common.cancel"),
|
||||||
|
send: () => translateText("common.send"),
|
||||||
|
|
||||||
|
cap: () => translateText("common.cap_label"),
|
||||||
|
capTooltip: () => translateText("common.cap_tooltip"),
|
||||||
|
|
||||||
|
sliderTooltip: (percent: number, amountStr: string) =>
|
||||||
|
this.mode === "troops"
|
||||||
|
? translateText("send_troops_modal.slider_tooltip", {
|
||||||
|
percent,
|
||||||
|
amount: amountStr,
|
||||||
|
})
|
||||||
|
: translateText("send_gold_modal.slider_tooltip", {
|
||||||
|
percent,
|
||||||
|
amount: amountStr,
|
||||||
|
}),
|
||||||
|
|
||||||
|
capacityNote: (amountStr: string) =>
|
||||||
|
translateText("send_troops_modal.capacity_note", { amount: amountStr }),
|
||||||
|
|
||||||
|
targetDeadTitle: () => translateText("common.target_dead"),
|
||||||
|
targetDeadNote: () => translateText("common.target_dead_note"),
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderHeader() {
|
||||||
|
const name = this.target?.name?.() ?? "";
|
||||||
|
return html`
|
||||||
|
<div class="mb-3 flex items-center justify-between">
|
||||||
|
<h2
|
||||||
|
id="send-title"
|
||||||
|
class="text-lg font-semibold tracking-tight text-zinc-100"
|
||||||
|
>
|
||||||
|
${this.heading ?? this.i18n.title(name)}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
class="rounded-md px-2 text-2xl leading-none text-zinc-300 hover:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-white/30"
|
||||||
|
@click=${() => this.closeModal()}
|
||||||
|
aria-label=${this.i18n.closeLabel()}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderAvailable() {
|
||||||
|
const total = this.getTotalNumber();
|
||||||
|
const cap = this.getCapacityLeft();
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="mb-4 pb-3 border-b border-zinc-800">
|
||||||
|
<div class="flex items-center gap-2 text-[13px]">
|
||||||
|
<!-- Available -->
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-1 rounded-full bg-indigo-600/15 px-2 py-0.5 ring-1 ring-indigo-400/40 text-indigo-100"
|
||||||
|
title=${this.i18n.availableTooltip()}
|
||||||
|
>
|
||||||
|
<span class="opacity-90">${this.i18n.availableChip()}</span>
|
||||||
|
<span class="font-mono tabular-nums">${this.format(total)}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
${cap !== null
|
||||||
|
? html`
|
||||||
|
<!-- Cap -->
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-1 rounded-full bg-amber-500/10 px-2 py-0.5 ring-1 ring-amber-400/40 text-amber-200"
|
||||||
|
title=${this.i18n.capTooltip()}
|
||||||
|
>
|
||||||
|
<span class="opacity-90">${this.i18n.cap()}</span>
|
||||||
|
<span class="font-mono tabular-nums"
|
||||||
|
>${this.format(cap)}</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
: html``}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderPresets(percentNow: number) {
|
||||||
|
const basis = this.getTotalNumber();
|
||||||
|
const dead = !this.isSenderAlive() || !this.isTargetAlive();
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="mb-8 grid grid-cols-5 gap-2">
|
||||||
|
${this.PRESETS.map((p) => {
|
||||||
|
const pct = this.sanitizePercent(p);
|
||||||
|
const active = (this.selectedPercent ?? percentNow) === pct;
|
||||||
|
const label = pct === 100 ? this.i18n.max() : `${pct}%`;
|
||||||
|
return html`
|
||||||
|
<button
|
||||||
|
?disabled=${dead}
|
||||||
|
class="rounded-lg px-3 py-2 text-sm ring-1 transition
|
||||||
|
${dead
|
||||||
|
? "bg-zinc-800/70 text-zinc-400 ring-zinc-700 cursor-not-allowed"
|
||||||
|
: active
|
||||||
|
? "bg-indigo-600 text-white ring-indigo-300/60"
|
||||||
|
: "bg-zinc-800 text-zinc-200 ring-zinc-700 hover:bg-zinc-700 hover:text-zinc-50"}"
|
||||||
|
@click=${() => {
|
||||||
|
if (dead) return;
|
||||||
|
this.selectedPercent = pct;
|
||||||
|
const raw = Math.floor((basis * pct) / 100);
|
||||||
|
this.sendAmount = this.clampSend(raw);
|
||||||
|
}}
|
||||||
|
?aria-pressed=${active}
|
||||||
|
title="${pct}%"
|
||||||
|
>
|
||||||
|
${label}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderSlider(percentNow: number) {
|
||||||
|
const basis = this.getTotalNumber();
|
||||||
|
const cap = this.getCapacityLeft();
|
||||||
|
const hardMax = cap === null ? basis : Math.min(basis, cap);
|
||||||
|
const dead = !this.isSenderAlive() || !this.isTargetAlive();
|
||||||
|
|
||||||
|
// Where to draw the cap marker (as % of Available)
|
||||||
|
const capPercent =
|
||||||
|
cap === null
|
||||||
|
? null
|
||||||
|
: Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
100,
|
||||||
|
Math.round((Math.min(cap, basis) / (basis || 1)) * 100),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fill = this.getFillColor();
|
||||||
|
const disabled = basis <= 0 || dead;
|
||||||
|
const sliderOuterMb = capPercent !== null ? "mb-8" : "mb-2";
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="${sliderOuterMb}">
|
||||||
|
<div
|
||||||
|
class="relative px-1 rounded-lg overflow-visible focus-within:ring-2 focus-within:ring-indigo-500/30"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
.max=${basis}
|
||||||
|
.value=${this.sendAmount}
|
||||||
|
?disabled=${disabled}
|
||||||
|
@input=${(e: Event) => {
|
||||||
|
if (dead) return;
|
||||||
|
const raw = Number((e.target as HTMLInputElement).value);
|
||||||
|
const pctRaw = basis ? Math.round((raw / basis) * 100) : 0;
|
||||||
|
this.selectedPercent = this.sanitizePercent(pctRaw);
|
||||||
|
const clamped = Math.min(raw, hardMax);
|
||||||
|
this.sendAmount = this.clampSend(clamped);
|
||||||
|
}}
|
||||||
|
class="w-full appearance-none bg-transparent range-x focus:outline-none"
|
||||||
|
aria-label=${this.i18n.ariaSlider()}
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax=${hardMax}
|
||||||
|
aria-valuetext=${this.i18n.sliderTooltip(
|
||||||
|
percentNow,
|
||||||
|
this.format(this.sendAmount),
|
||||||
|
)}
|
||||||
|
style="--percent:${percentNow}%; --fill:${fill}; --track: rgba(255,255,255,.28); --thumb-ring: rgb(24 24 27);"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Tooltip -->
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute -top-6 -translate-x-1/2 select-none"
|
||||||
|
style="left:${percentNow}%"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="rounded bg-[#0f1116] ring-1 ring-zinc-700 text-zinc-100 px-1.5 py-0.5 text-[12px] shadow whitespace-nowrap w-max z-50"
|
||||||
|
>
|
||||||
|
${percentNow}% • ${this.format(this.sendAmount)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cap marker -->
|
||||||
|
${capPercent !== null
|
||||||
|
? html`
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute top-1/2 -translate-y-1/2 h-3 w-[2px] bg-amber-400/80 shadow"
|
||||||
|
style="left:${capPercent}%;"
|
||||||
|
title=${this.i18n.capTooltip()}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute top-full mt-1.5 -translate-x-1/2 select-none"
|
||||||
|
style="left:${capPercent}%"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="rounded bg-[#0f1116] ring-1 ring-amber-400/40 text-amber-200 px-1 py-0.5 text-[11px] shadow whitespace-nowrap"
|
||||||
|
>
|
||||||
|
${this.i18n.cap()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html``}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderCapacityNote(allowed: number) {
|
||||||
|
const capped = allowed !== this.sendAmount;
|
||||||
|
if (!capped) return html``;
|
||||||
|
return html`<p class="mt-1 text-xs text-amber-300">
|
||||||
|
${this.i18n.capacityNote(this.format(allowed))}
|
||||||
|
</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderSummary(allowed: number) {
|
||||||
|
const total = this.getTotalNumber();
|
||||||
|
const keep = this.keepAfter(allowed);
|
||||||
|
const belowMinKeep =
|
||||||
|
this.getMinKeepRatio() > 0 &&
|
||||||
|
keep < Math.floor(total * this.getMinKeepRatio());
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="mt-3 text-center text-sm text-zinc-200">
|
||||||
|
${this.i18n.summarySend()}
|
||||||
|
<span class="font-semibold text-indigo-400 font-mono"
|
||||||
|
>${this.format(allowed)}</span
|
||||||
|
>
|
||||||
|
· ${this.i18n.summaryKeep()}
|
||||||
|
<span
|
||||||
|
class="font-semibold font-mono ${belowMinKeep
|
||||||
|
? "text-amber-400"
|
||||||
|
: "text-emerald-400"}"
|
||||||
|
>
|
||||||
|
${this.format(keep)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderActions() {
|
||||||
|
const total = this.getTotalNumber();
|
||||||
|
const dead = !this.isSenderAlive() || !this.isTargetAlive();
|
||||||
|
const disabled = total <= 0 || this.clampSend(this.sendAmount) <= 0 || dead;
|
||||||
|
return html`
|
||||||
|
<div class="mt-5 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
class="h-10 min-w-24 rounded-lg px-3 text-sm font-semibold
|
||||||
|
text-zinc-100 bg-zinc-800 ring-1 ring-zinc-700
|
||||||
|
hover:bg-zinc-700 focus:outline-none
|
||||||
|
focus-visible:ring-2 focus-visible:ring-white/20"
|
||||||
|
@click=${() => this.closeModal()}
|
||||||
|
>
|
||||||
|
${this.i18n.cancel()}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="h-10 min-w-24 rounded-lg px-3 text-sm font-semibold text-white
|
||||||
|
bg-indigo-600 enabled:hover:bg-indigo-500
|
||||||
|
focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400/50
|
||||||
|
disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
?disabled=${disabled}
|
||||||
|
@click=${() => this.confirm()}
|
||||||
|
>
|
||||||
|
${this.i18n.send()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderDeadNote() {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="mb-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-amber-200 text-sm"
|
||||||
|
>
|
||||||
|
<div class="font-semibold">${this.i18n.targetDeadTitle()}</div>
|
||||||
|
<div>${this.i18n.targetDeadNote()}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderSliderStyles() {
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
.range-x {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: 8px;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.range-x::-webkit-slider-runnable-track {
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--fill) 0,
|
||||||
|
var(--fill) var(--percent),
|
||||||
|
/* allowed (clamped) fill */ rgba(255, 255, 255, 0.22)
|
||||||
|
var(--percent),
|
||||||
|
rgba(255, 255, 255, 0.22) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.range-x::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--fill);
|
||||||
|
border: 3px solid var(--thumb-ring);
|
||||||
|
margin-top: -5px;
|
||||||
|
}
|
||||||
|
.range-x::-moz-range-track {
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: rgba(255, 255, 255, 0.22);
|
||||||
|
}
|
||||||
|
.range-x::-moz-range-progress {
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--fill);
|
||||||
|
}
|
||||||
|
.range-x::-moz-range-thumb {
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--fill);
|
||||||
|
border: 3px solid var(--thumb-ring);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.open) return html``;
|
||||||
|
|
||||||
|
const percent = this.percentOfBasis(this.sendAmount);
|
||||||
|
const allowed = this.limitAmount(this.sendAmount);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="fixed inset-0 z-[1100] flex items-center justify-center p-4">
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-black/60 backdrop-blur-sm rounded-2xl"
|
||||||
|
@click=${() => this.closeModal()}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="send-title"
|
||||||
|
class="relative z-10 w-full max-w-[540px] focus:outline-none"
|
||||||
|
tabindex="0"
|
||||||
|
@keydown=${this.handleKeydown}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="rounded-2xl bg-zinc-900 p-5 shadow-2xl ring-1 ring-zinc-800 max-h-[90vh] text-zinc-200"
|
||||||
|
@click=${(e: MouseEvent) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
${this.renderHeader()} ${this.renderAvailable()}
|
||||||
|
${!this.isTargetAlive() ? this.renderDeadNote() : html``}
|
||||||
|
${this.renderPresets(percent)} ${this.renderSlider(percent)}
|
||||||
|
${this.mode === "troops"
|
||||||
|
? this.renderCapacityNote(allowed)
|
||||||
|
: html``}
|
||||||
|
${this.renderSummary(allowed)} ${this.renderActions()}
|
||||||
|
${this.renderSliderStyles()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,9 +18,10 @@ export const STRUCTURE_SHAPES: Partial<Record<UnitType, ShapeType>> = {
|
|||||||
[UnitType.DefensePost]: "octagon",
|
[UnitType.DefensePost]: "octagon",
|
||||||
[UnitType.SAMLauncher]: "square",
|
[UnitType.SAMLauncher]: "square",
|
||||||
[UnitType.MissileSilo]: "triangle",
|
[UnitType.MissileSilo]: "triangle",
|
||||||
|
[UnitType.Warship]: "cross",
|
||||||
[UnitType.AtomBomb]: "cross",
|
[UnitType.AtomBomb]: "cross",
|
||||||
[UnitType.HydrogenBomb]: "cross",
|
[UnitType.HydrogenBomb]: "cross",
|
||||||
[UnitType.Warship]: "cross",
|
[UnitType.MIRV]: "cross",
|
||||||
};
|
};
|
||||||
export const LEVEL_SCALE_FACTOR = 3;
|
export const LEVEL_SCALE_FACTOR = 3;
|
||||||
export const ICON_SCALE_FACTOR_ZOOMED_IN = 3.5;
|
export const ICON_SCALE_FACTOR_ZOOMED_IN = 3.5;
|
||||||
|
|||||||
@@ -149,6 +149,11 @@ export class TerritoryLayer implements Layer {
|
|||||||
if (!this.game.inSpawnPhase()) {
|
if (!this.game.inSpawnPhase()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.spawnHighlight();
|
||||||
|
}
|
||||||
|
|
||||||
|
private spawnHighlight() {
|
||||||
if (this.game.ticks() % 5 === 0) {
|
if (this.game.ticks() % 5 === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -159,11 +164,18 @@ export class TerritoryLayer implements Layer {
|
|||||||
this.game.width(),
|
this.game.width(),
|
||||||
this.game.height(),
|
this.game.height(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.drawFocusedPlayerHighlight();
|
||||||
|
|
||||||
const humans = this.game
|
const humans = this.game
|
||||||
.playerViews()
|
.playerViews()
|
||||||
.filter((p) => p.type() === PlayerType.Human);
|
.filter((p) => p.type() === PlayerType.Human);
|
||||||
|
|
||||||
|
const focusedPlayer = this.game.focusedPlayer();
|
||||||
for (const human of humans) {
|
for (const human of humans) {
|
||||||
|
if (human === focusedPlayer) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const center = human.nameLocation();
|
const center = human.nameLocation();
|
||||||
if (!center) {
|
if (!center) {
|
||||||
continue;
|
continue;
|
||||||
@@ -190,37 +202,34 @@ export class TerritoryLayer implements Layer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawFocusedPlayerHighlight() {
|
||||||
|
const focusedPlayer = this.game.focusedPlayer();
|
||||||
|
|
||||||
|
if (!focusedPlayer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const center = focusedPlayer.nameLocation();
|
||||||
|
if (!center) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Breathing border animation
|
// Breathing border animation
|
||||||
this.borderAnimTime += 1;
|
this.borderAnimTime += 3;
|
||||||
const minPadding = 3;
|
const minPadding = 6;
|
||||||
const maxPadding = 8;
|
const maxPadding = 12;
|
||||||
// Range: [minPadding..maxPadding]
|
// Range: [minPadding..maxPadding]
|
||||||
const breathingPadding =
|
const breathingPadding =
|
||||||
minPadding +
|
minPadding +
|
||||||
(maxPadding - minPadding) *
|
(maxPadding - minPadding) *
|
||||||
(0.5 + 0.5 * Math.sin(this.borderAnimTime * 0.3));
|
(0.5 + 0.5 * Math.sin(this.borderAnimTime * 0.3));
|
||||||
|
|
||||||
if (focusedPlayer) {
|
this.drawBreathingRing(
|
||||||
// Clear previous animated border
|
center.x,
|
||||||
if (this.highlightContext) {
|
center.y,
|
||||||
this.highlightContext.clearRect(
|
breathingPadding,
|
||||||
0,
|
this.theme.spawnHighlightColor(),
|
||||||
0,
|
);
|
||||||
this.game.width(),
|
|
||||||
this.game.height(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const center = focusedPlayer.nameLocation();
|
|
||||||
if (center) {
|
|
||||||
this.drawBreathingRing(
|
|
||||||
center.x,
|
|
||||||
center.y,
|
|
||||||
breathingPadding,
|
|
||||||
this.theme.spawnHighlightColor(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@@ -560,7 +569,7 @@ export class TerritoryLayer implements Layer {
|
|||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
||||||
ctx.strokeStyle = color.toRgbString();
|
ctx.strokeStyle = color.toRgbString();
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 4;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { customElement } from "lit/decorators.js";
|
|||||||
import warshipIcon from "../../../../resources/images/BattleshipIconWhite.svg";
|
import warshipIcon from "../../../../resources/images/BattleshipIconWhite.svg";
|
||||||
import cityIcon from "../../../../resources/images/CityIconWhite.svg";
|
import cityIcon from "../../../../resources/images/CityIconWhite.svg";
|
||||||
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
|
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
|
||||||
import missileSiloIcon from "../../../../resources/images/MissileSiloUnit.png";
|
import mirvIcon from "../../../../resources/images/MIRVIcon.svg";
|
||||||
|
import missileSiloIcon from "../../../../resources/images/MissileSiloIconWhite.svg";
|
||||||
import hydrogenBombIcon from "../../../../resources/images/MushroomCloudIconWhite.svg";
|
import hydrogenBombIcon from "../../../../resources/images/MushroomCloudIconWhite.svg";
|
||||||
import atomBombIcon from "../../../../resources/images/NukeIconWhite.svg";
|
import atomBombIcon from "../../../../resources/images/NukeIconWhite.svg";
|
||||||
import portIcon from "../../../../resources/images/PortIcon.svg";
|
import portIcon from "../../../../resources/images/PortIcon.svg";
|
||||||
@@ -59,7 +60,8 @@ export class UnitDisplay extends LitElement implements Layer {
|
|||||||
config.isUnitDisabled(UnitType.SAMLauncher) &&
|
config.isUnitDisabled(UnitType.SAMLauncher) &&
|
||||||
config.isUnitDisabled(UnitType.Warship) &&
|
config.isUnitDisabled(UnitType.Warship) &&
|
||||||
config.isUnitDisabled(UnitType.AtomBomb) &&
|
config.isUnitDisabled(UnitType.AtomBomb) &&
|
||||||
config.isUnitDisabled(UnitType.HydrogenBomb);
|
config.isUnitDisabled(UnitType.HydrogenBomb) &&
|
||||||
|
config.isUnitDisabled(UnitType.MIRV);
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +80,7 @@ export class UnitDisplay extends LitElement implements Layer {
|
|||||||
switch (item) {
|
switch (item) {
|
||||||
case UnitType.AtomBomb:
|
case UnitType.AtomBomb:
|
||||||
case UnitType.HydrogenBomb:
|
case UnitType.HydrogenBomb:
|
||||||
|
case UnitType.MIRV:
|
||||||
return (
|
return (
|
||||||
this.cost(item) <= (player?.gold() ?? 0n) &&
|
this.cost(item) <= (player?.gold() ?? 0n) &&
|
||||||
(player?.units(UnitType.MissileSilo).length ?? 0) > 0
|
(player?.units(UnitType.MissileSilo).length ?? 0) > 0
|
||||||
@@ -124,7 +127,7 @@ export class UnitDisplay extends LitElement implements Layer {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="hidden xl:flex md:flex fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] xl:flex-row lg:flex-col md:flex-col xl:gap-5 lg:gap-2 md:gap-2 justify-center items-center"
|
class="hidden 2xl:flex lg:flex fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] 2xl:flex-row xl:flex-col lg:flex-col 2xl:gap-5 xl:gap-2 lg:gap-2 justify-center items-center"
|
||||||
>
|
>
|
||||||
<div class="bg-gray-800/70 backdrop-blur-sm rounded-lg p-0.5">
|
<div class="bg-gray-800/70 backdrop-blur-sm rounded-lg p-0.5">
|
||||||
<div class="grid grid-rows-1 auto-cols-max grid-flow-col gap-1 w-fit">
|
<div class="grid grid-rows-1 auto-cols-max grid-flow-col gap-1 w-fit">
|
||||||
@@ -174,26 +177,33 @@ export class UnitDisplay extends LitElement implements Layer {
|
|||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800/70 backdrop-blur-sm rounded-lg p-0.5 w-fit">
|
<div class="bg-gray-800/70 backdrop-blur-sm rounded-lg p-0.5 w-fit">
|
||||||
<div class="grid grid-rows-1 auto-cols-max grid-flow-col gap-1">
|
<div class="grid grid-rows-1 auto-cols-max grid-flow-col gap-1">
|
||||||
|
${this.renderUnitItem(
|
||||||
|
warshipIcon,
|
||||||
|
this._warships,
|
||||||
|
UnitType.Warship,
|
||||||
|
"warship",
|
||||||
|
this.keybinds["buildWarship"]?.key ?? "7",
|
||||||
|
)}
|
||||||
${this.renderUnitItem(
|
${this.renderUnitItem(
|
||||||
atomBombIcon,
|
atomBombIcon,
|
||||||
null,
|
null,
|
||||||
UnitType.AtomBomb,
|
UnitType.AtomBomb,
|
||||||
"atom_bomb",
|
"atom_bomb",
|
||||||
this.keybinds["buildAtomBomb"]?.key ?? "7",
|
this.keybinds["buildAtomBomb"]?.key ?? "8",
|
||||||
)}
|
)}
|
||||||
${this.renderUnitItem(
|
${this.renderUnitItem(
|
||||||
hydrogenBombIcon,
|
hydrogenBombIcon,
|
||||||
null,
|
null,
|
||||||
UnitType.HydrogenBomb,
|
UnitType.HydrogenBomb,
|
||||||
"hydrogen_bomb",
|
"hydrogen_bomb",
|
||||||
this.keybinds["buildHydrogenBomb"]?.key ?? "8",
|
this.keybinds["buildHydrogenBomb"]?.key ?? "9",
|
||||||
)}
|
)}
|
||||||
${this.renderUnitItem(
|
${this.renderUnitItem(
|
||||||
warshipIcon,
|
mirvIcon,
|
||||||
this._warships,
|
null,
|
||||||
UnitType.Warship,
|
UnitType.MIRV,
|
||||||
"warship",
|
"mirv",
|
||||||
this.keybinds["buildWarship"]?.key ?? "9",
|
this.keybinds["buildMIRV"]?.key ?? "0",
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LitElement, TemplateResult, html } from "lit";
|
import { LitElement, TemplateResult, html } from "lit";
|
||||||
import { customElement, state } from "lit/decorators.js";
|
import { customElement, state } from "lit/decorators.js";
|
||||||
import { translateText } from "../../../client/Utils";
|
import { isInIframe, translateText } from "../../../client/Utils";
|
||||||
import { ColorPalette, Pattern } from "../../../core/CosmeticSchemas";
|
import { ColorPalette, Pattern } from "../../../core/CosmeticSchemas";
|
||||||
import { EventBus } from "../../../core/EventBus";
|
import { EventBus } from "../../../core/EventBus";
|
||||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||||
@@ -36,6 +36,8 @@ export class WinModal extends LitElement implements Layer {
|
|||||||
|
|
||||||
private _title: string;
|
private _title: string;
|
||||||
|
|
||||||
|
private rand = Math.random();
|
||||||
|
|
||||||
// Override to prevent shadow DOM creation
|
// Override to prevent shadow DOM creation
|
||||||
createRenderRoot() {
|
createRenderRoot() {
|
||||||
return this;
|
return this;
|
||||||
@@ -98,6 +100,9 @@ export class WinModal extends LitElement implements Layer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
innerHtml() {
|
innerHtml() {
|
||||||
|
if (isInIframe() || this.rand < 0.25) {
|
||||||
|
return this.steamWishlist();
|
||||||
|
}
|
||||||
return this.renderPatternButton();
|
return this.renderPatternButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export type Player = z.infer<typeof PlayerSchema>;
|
|||||||
export type PlayerCosmetics = z.infer<typeof PlayerCosmeticsSchema>;
|
export type PlayerCosmetics = z.infer<typeof PlayerCosmeticsSchema>;
|
||||||
export type PlayerCosmeticRefs = z.infer<typeof PlayerCosmeticRefsSchema>;
|
export type PlayerCosmeticRefs = z.infer<typeof PlayerCosmeticRefsSchema>;
|
||||||
export type PlayerPattern = z.infer<typeof PlayerPatternSchema>;
|
export type PlayerPattern = z.infer<typeof PlayerPatternSchema>;
|
||||||
|
export type PlayerColor = z.infer<typeof PlayerColorSchema>;
|
||||||
export type Flag = z.infer<typeof FlagSchema>;
|
export type Flag = z.infer<typeof FlagSchema>;
|
||||||
export type GameStartInfo = z.infer<typeof GameStartInfoSchema>;
|
export type GameStartInfo = z.infer<typeof GameStartInfoSchema>;
|
||||||
|
|
||||||
@@ -386,6 +387,7 @@ export const FlagSchema = z
|
|||||||
|
|
||||||
export const PlayerCosmeticRefsSchema = z.object({
|
export const PlayerCosmeticRefsSchema = z.object({
|
||||||
flag: FlagSchema.optional(),
|
flag: FlagSchema.optional(),
|
||||||
|
color: z.string().optional(),
|
||||||
patternName: PatternNameSchema.optional(),
|
patternName: PatternNameSchema.optional(),
|
||||||
patternColorPaletteName: z.string().optional(),
|
patternColorPaletteName: z.string().optional(),
|
||||||
});
|
});
|
||||||
@@ -395,10 +397,17 @@ export const PlayerPatternSchema = z.object({
|
|||||||
patternData: PatternDataSchema,
|
patternData: PatternDataSchema,
|
||||||
colorPalette: ColorPaletteSchema.optional(),
|
colorPalette: ColorPaletteSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const PlayerColorSchema = z.object({
|
||||||
|
color: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
export const PlayerCosmeticsSchema = z.object({
|
export const PlayerCosmeticsSchema = z.object({
|
||||||
flag: FlagSchema.optional(),
|
flag: FlagSchema.optional(),
|
||||||
pattern: PlayerPatternSchema.optional(),
|
pattern: PlayerPatternSchema.optional(),
|
||||||
|
color: PlayerColorSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PlayerSchema = z.object({
|
export const PlayerSchema = z.object({
|
||||||
clientID: ID,
|
clientID: ID,
|
||||||
username: UsernameSchema,
|
username: UsernameSchema,
|
||||||
|
|||||||
@@ -114,27 +114,6 @@ export function inscribed(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMode(list: Set<number>): number {
|
|
||||||
// Count occurrences
|
|
||||||
const counts = new Map<number, number>();
|
|
||||||
for (const item of list) {
|
|
||||||
counts.set(item, (counts.get(item) ?? 0) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the item with the highest count
|
|
||||||
let mode = 0;
|
|
||||||
let maxCount = 0;
|
|
||||||
|
|
||||||
for (const [item, count] of counts) {
|
|
||||||
if (count > maxCount) {
|
|
||||||
maxCount = count;
|
|
||||||
mode = item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sanitize(name: string): string {
|
export function sanitize(name: string): string {
|
||||||
return Array.from(name)
|
return Array.from(name)
|
||||||
.join("")
|
.join("")
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ export interface Theme {
|
|||||||
// Don't call directly, use PlayerView
|
// Don't call directly, use PlayerView
|
||||||
territoryColor(playerInfo: PlayerView): Colord;
|
territoryColor(playerInfo: PlayerView): Colord;
|
||||||
// Don't call directly, use PlayerView
|
// Don't call directly, use PlayerView
|
||||||
borderColor(playerInfo: PlayerView): Colord;
|
borderColor(territoryColor: Colord): Colord;
|
||||||
// Don't call directly, use PlayerView
|
// Don't call directly, use PlayerView
|
||||||
defendedBorderColors(territoryColor: Colord): { light: Colord; dark: Colord };
|
defendedBorderColors(territoryColor: Colord): { light: Colord; dark: Colord };
|
||||||
focusedBorderColor(): Colord;
|
focusedBorderColor(): Colord;
|
||||||
|
|||||||
@@ -55,19 +55,8 @@ export class PastelTheme implements Theme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't call directly, use PlayerView
|
// Don't call directly, use PlayerView
|
||||||
borderColor(player: PlayerView): Colord {
|
borderColor(territoryColor: Colord): Colord {
|
||||||
if (this.borderColorCache.has(player.id())) {
|
return territoryColor.darken(0.125);
|
||||||
return this.borderColorCache.get(player.id())!;
|
|
||||||
}
|
|
||||||
const tc = this.territoryColor(player).rgba;
|
|
||||||
const color = colord({
|
|
||||||
r: Math.max(tc.r - 40, 0),
|
|
||||||
g: Math.max(tc.g - 40, 0),
|
|
||||||
b: Math.max(tc.b - 40, 0),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.borderColorCache.set(player.id(), color);
|
|
||||||
return color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defendedBorderColors(territoryColor: Colord): {
|
defendedBorderColors(territoryColor: Colord): {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Config } from "../configuration/Config";
|
|||||||
import { Execution, Game, Player, UnitType } from "../game/Game";
|
import { Execution, Game, Player, UnitType } from "../game/Game";
|
||||||
import { GameImpl } from "../game/GameImpl";
|
import { GameImpl } from "../game/GameImpl";
|
||||||
import { GameMap, TileRef } from "../game/GameMap";
|
import { GameMap, TileRef } from "../game/GameMap";
|
||||||
import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util";
|
import { calculateBoundingBox, inscribed, simpleHash } from "../Util";
|
||||||
|
|
||||||
export class PlayerExecution implements Execution {
|
export class PlayerExecution implements Execution {
|
||||||
private readonly ticksPerClusterCalc = 20;
|
private readonly ticksPerClusterCalc = 20;
|
||||||
@@ -27,18 +27,30 @@ export class PlayerExecution implements Execution {
|
|||||||
|
|
||||||
tick(ticks: number) {
|
tick(ticks: number) {
|
||||||
this.player.decayRelations();
|
this.player.decayRelations();
|
||||||
this.player.units().forEach((u) => {
|
for (const u of this.player.units()) {
|
||||||
const tileOwner = this.mg!.owner(u.tile());
|
if (!u.info().territoryBound) {
|
||||||
if (u.info().territoryBound) {
|
continue;
|
||||||
if (tileOwner.isPlayer()) {
|
|
||||||
if (tileOwner !== this.player) {
|
|
||||||
this.mg!.player(tileOwner.id()).captureUnit(u);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
u.delete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
const owner = this.mg!.owner(u.tile());
|
||||||
|
if (!owner?.isPlayer()) {
|
||||||
|
u.delete();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (owner === this.player) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const captor = this.mg!.player(owner.id());
|
||||||
|
if (u.type() === UnitType.DefensePost) {
|
||||||
|
u.decreaseLevel(captor);
|
||||||
|
if (u.isActive()) {
|
||||||
|
captor.captureUnit(u);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
captor.captureUnit(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.player.isAlive()) {
|
if (!this.player.isAlive()) {
|
||||||
// Player has no tiles, delete any remaining units and gold
|
// Player has no tiles, delete any remaining units and gold
|
||||||
@@ -209,22 +221,37 @@ export class PlayerExecution implements Execution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getCapturingPlayer(cluster: Set<TileRef>): Player | null {
|
private getCapturingPlayer(cluster: Set<TileRef>): Player | null {
|
||||||
const neighborsIDs = new Set<number>();
|
// Collect unique neighbor IDs (excluding self) as candidates
|
||||||
|
const candidatesIDs = new Set<number>();
|
||||||
|
const selfID = this.player.smallID();
|
||||||
|
|
||||||
for (const t of cluster) {
|
for (const t of cluster) {
|
||||||
for (const neighbor of this.mg.neighbors(t)) {
|
for (const neighbor of this.mg.neighbors(t)) {
|
||||||
if (this.mg.ownerID(neighbor) !== this.player.smallID()) {
|
if (this.mg.ownerID(neighbor) !== selfID) {
|
||||||
neighborsIDs.add(this.mg.ownerID(neighbor));
|
candidatesIDs.add(this.mg.ownerID(neighbor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let largestNeighborAttack: Player | null = null;
|
// Filter out friendly and non-player candidates
|
||||||
let largestTroopCount: number = 0;
|
const neighbors = new Set<Player>();
|
||||||
for (const id of neighborsIDs) {
|
for (const id of candidatesIDs) {
|
||||||
const neighbor = this.mg.playerBySmallID(id);
|
const neighbor = this.mg.playerBySmallID(id);
|
||||||
if (!neighbor.isPlayer() || this.player.isFriendly(neighbor)) {
|
if (!neighbor.isPlayer() || neighbor.isFriendly(this.player)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
neighbors.add(neighbor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no enemies, return null
|
||||||
|
if (neighbors.size === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the largest attack from the neighbors
|
||||||
|
let largestNeighborAttack: Player | null = null;
|
||||||
|
let largestTroopCount = 0;
|
||||||
|
for (const neighbor of neighbors) {
|
||||||
for (const attack of neighbor.outgoingAttacks()) {
|
for (const attack of neighbor.outgoingAttacks()) {
|
||||||
if (attack.target() === this.player) {
|
if (attack.target() === this.player) {
|
||||||
if (attack.troops() > largestTroopCount) {
|
if (attack.troops() > largestTroopCount) {
|
||||||
@@ -234,20 +261,10 @@ export class PlayerExecution implements Execution {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (largestNeighborAttack !== null) {
|
|
||||||
return largestNeighborAttack;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fall back to getting mode if no attacks
|
// Return the largest neighbor attack
|
||||||
const mode = getMode(neighborsIDs);
|
// If there is no largest neighbor attack, this will return null
|
||||||
if (!this.mg.playerBySmallID(mode).isPlayer()) {
|
return largestNeighborAttack;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const capturing = this.mg.playerBySmallID(mode);
|
|
||||||
if (!capturing.isPlayer()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return capturing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateClusters(): Set<TileRef>[] {
|
private calculateClusters(): Set<TileRef>[] {
|
||||||
|
|||||||
@@ -494,6 +494,7 @@ export interface Unit {
|
|||||||
// Upgradable Structures
|
// Upgradable Structures
|
||||||
level(): number;
|
level(): number;
|
||||||
increaseLevel(): void;
|
increaseLevel(): void;
|
||||||
|
decreaseLevel(destroyer?: Player): void;
|
||||||
|
|
||||||
// Warships
|
// Warships
|
||||||
setPatrolTile(tile: TileRef): void;
|
setPatrolTile(tile: TileRef): void;
|
||||||
|
|||||||
+26
-18
@@ -197,36 +197,44 @@ export class PlayerView {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultTerritoryColor = this.game
|
||||||
|
.config()
|
||||||
|
.theme()
|
||||||
|
.territoryColor(this);
|
||||||
|
const defaultBorderColor = this.game
|
||||||
|
.config()
|
||||||
|
.theme()
|
||||||
|
.borderColor(defaultTerritoryColor);
|
||||||
|
|
||||||
const pattern = this.cosmetics.pattern;
|
const pattern = this.cosmetics.pattern;
|
||||||
if (pattern) {
|
if (pattern) {
|
||||||
const territoryColor = this.game.config().theme().territoryColor(this);
|
|
||||||
pattern.colorPalette ??= {
|
pattern.colorPalette ??= {
|
||||||
name: "",
|
name: "",
|
||||||
primaryColor: territoryColor.toHex(),
|
primaryColor: defaultTerritoryColor.toHex(),
|
||||||
secondaryColor: territoryColor.darken(0.125).toHex(),
|
secondaryColor: defaultBorderColor.toHex(),
|
||||||
} satisfies ColorPalette;
|
} satisfies ColorPalette;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (this.team() === null) {
|
||||||
this.team() === null &&
|
|
||||||
this.cosmetics.pattern?.colorPalette?.primaryColor !== undefined
|
|
||||||
) {
|
|
||||||
this._territoryColor = colord(
|
this._territoryColor = colord(
|
||||||
this.cosmetics.pattern.colorPalette.primaryColor,
|
this.cosmetics.color?.color ??
|
||||||
|
this.cosmetics.pattern?.colorPalette?.primaryColor ??
|
||||||
|
defaultTerritoryColor.toHex(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this._territoryColor = this.game.config().theme().territoryColor(this);
|
this._territoryColor = defaultTerritoryColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.cosmetics.pattern?.colorPalette?.secondaryColor !== undefined) {
|
const maybeFocusedBorderColor =
|
||||||
this._borderColor = colord(
|
this.game.myClientID() === this.data.clientID
|
||||||
this.cosmetics.pattern.colorPalette.secondaryColor,
|
? this.game.config().theme().focusedBorderColor()
|
||||||
);
|
: defaultBorderColor;
|
||||||
} else if (this.game.myClientID() === this.data.clientID) {
|
|
||||||
this._borderColor = this.game.config().theme().focusedBorderColor();
|
this._borderColor = new Colord(
|
||||||
} else {
|
pattern?.colorPalette?.secondaryColor ??
|
||||||
this._borderColor = this.game.config().theme().borderColor(this);
|
this.cosmetics.color?.color ??
|
||||||
}
|
maybeFocusedBorderColor.toHex(),
|
||||||
|
);
|
||||||
|
|
||||||
this._defendedBorderColors = this.game
|
this._defendedBorderColors = this.game
|
||||||
.config()
|
.config()
|
||||||
|
|||||||
@@ -576,7 +576,11 @@ export class PlayerImpl implements Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canDonateGold(recipient: Player): boolean {
|
canDonateGold(recipient: Player): boolean {
|
||||||
if (!this.isFriendly(recipient)) {
|
if (
|
||||||
|
!this.isAlive() ||
|
||||||
|
!recipient.isAlive() ||
|
||||||
|
!this.isFriendly(recipient)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -599,7 +603,11 @@ export class PlayerImpl implements Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canDonateTroops(recipient: Player): boolean {
|
canDonateTroops(recipient: Player): boolean {
|
||||||
if (!this.isFriendly(recipient)) {
|
if (
|
||||||
|
!this.isAlive() ||
|
||||||
|
!recipient.isAlive() ||
|
||||||
|
!this.isFriendly(recipient)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -397,6 +397,18 @@ export class UnitImpl implements Unit {
|
|||||||
this.mg.addUpdate(this.toUpdate());
|
this.mg.addUpdate(this.toUpdate());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
decreaseLevel(destroyer?: Player): void {
|
||||||
|
this._level--;
|
||||||
|
if ([UnitType.MissileSilo, UnitType.SAMLauncher].includes(this.type())) {
|
||||||
|
this._missileTimerQueue.pop();
|
||||||
|
}
|
||||||
|
if (this._level <= 0) {
|
||||||
|
this.delete(true, destroyer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.mg.addUpdate(this.toUpdate());
|
||||||
|
}
|
||||||
|
|
||||||
trainType(): TrainType | undefined {
|
trainType(): TrainType | undefined {
|
||||||
return this._trainType;
|
return this._trainType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,6 +169,20 @@ export class UserSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSelectedColor(): string | undefined {
|
||||||
|
const data = localStorage.getItem("settings.territoryColor") ?? undefined;
|
||||||
|
if (data === undefined) return undefined;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedColor(color: string | undefined): void {
|
||||||
|
if (color === undefined) {
|
||||||
|
localStorage.removeItem("settings.territoryColor");
|
||||||
|
} else {
|
||||||
|
localStorage.setItem("settings.territoryColor", color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
backgroundMusicVolume(): number {
|
backgroundMusicVolume(): number {
|
||||||
return this.getFloat("settings.backgroundMusicVolume", 0);
|
return this.getFloat("settings.backgroundMusicVolume", 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,16 +126,12 @@ export class MapPlaylist {
|
|||||||
|
|
||||||
const rand = new PseudoRandom(Date.now());
|
const rand = new PseudoRandom(Date.now());
|
||||||
|
|
||||||
const ffa1: GameMapType[] = rand.shuffleArray([...maps]);
|
const ffa: GameMapType[] = rand.shuffleArray([...maps]);
|
||||||
const ffa2: GameMapType[] = rand.shuffleArray([...maps]);
|
|
||||||
const team: GameMapType[] = rand.shuffleArray([...maps]);
|
const team: GameMapType[] = rand.shuffleArray([...maps]);
|
||||||
|
|
||||||
this.mapsPlaylist = [];
|
this.mapsPlaylist = [];
|
||||||
for (let i = 0; i < maps.length; i++) {
|
for (let i = 0; i < maps.length; i++) {
|
||||||
if (!this.addNextMap(this.mapsPlaylist, ffa1, GameMode.FFA)) {
|
if (!this.addNextMap(this.mapsPlaylist, ffa, GameMode.FFA)) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!this.addNextMap(this.mapsPlaylist, ffa2, GameMode.FFA)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!this.addNextMap(this.mapsPlaylist, team, GameMode.Team)) {
|
if (!this.addNextMap(this.mapsPlaylist, team, GameMode.Team)) {
|
||||||
|
|||||||
+54
-106
@@ -1,22 +1,18 @@
|
|||||||
import { Cosmetics } from "../core/CosmeticSchemas";
|
import { Cosmetics } from "../core/CosmeticSchemas";
|
||||||
import { decodePatternData } from "../core/PatternDecoder";
|
import { decodePatternData } from "../core/PatternDecoder";
|
||||||
import { PlayerPattern } from "../core/Schemas";
|
import {
|
||||||
|
PlayerColor,
|
||||||
|
PlayerCosmeticRefs,
|
||||||
|
PlayerCosmetics,
|
||||||
|
PlayerPattern,
|
||||||
|
} from "../core/Schemas";
|
||||||
|
|
||||||
type PatternResult =
|
type CosmeticResult =
|
||||||
| { type: "allowed"; pattern: PlayerPattern }
|
| { type: "allowed"; cosmetics: PlayerCosmetics }
|
||||||
| { type: "unknown" }
|
|
||||||
| { type: "forbidden"; reason: string };
|
| { type: "forbidden"; reason: string };
|
||||||
|
|
||||||
export interface PrivilegeChecker {
|
export interface PrivilegeChecker {
|
||||||
isPatternAllowed(
|
isAllowed(flares: string[], refs: PlayerCosmeticRefs): CosmeticResult;
|
||||||
flares: readonly string[],
|
|
||||||
name: string,
|
|
||||||
colorPaletteName: string | null,
|
|
||||||
): PatternResult;
|
|
||||||
isCustomFlagAllowed(
|
|
||||||
flag: string,
|
|
||||||
flares: readonly string[] | undefined,
|
|
||||||
): true | "restricted" | "invalid";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PrivilegeCheckerImpl implements PrivilegeChecker {
|
export class PrivilegeCheckerImpl implements PrivilegeChecker {
|
||||||
@@ -25,28 +21,53 @@ export class PrivilegeCheckerImpl implements PrivilegeChecker {
|
|||||||
private b64urlDecode: (base64: string) => Uint8Array,
|
private b64urlDecode: (base64: string) => Uint8Array,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
isAllowed(flares: string[], refs: PlayerCosmeticRefs): CosmeticResult {
|
||||||
|
const cosmetics: PlayerCosmetics = {};
|
||||||
|
if (refs.patternName) {
|
||||||
|
try {
|
||||||
|
cosmetics.pattern = this.isPatternAllowed(
|
||||||
|
flares,
|
||||||
|
refs.patternName,
|
||||||
|
refs.patternColorPaletteName ?? null,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return { type: "forbidden", reason: "invalid pattern: " + e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (refs.color) {
|
||||||
|
try {
|
||||||
|
cosmetics.color = this.isColorAllowed(flares, refs.color);
|
||||||
|
} catch (e) {
|
||||||
|
return { type: "forbidden", reason: "invalid color: " + e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: "allowed", cosmetics };
|
||||||
|
}
|
||||||
|
|
||||||
isPatternAllowed(
|
isPatternAllowed(
|
||||||
flares: readonly string[],
|
flares: readonly string[],
|
||||||
name: string,
|
name: string,
|
||||||
colorPaletteName: string | null,
|
colorPaletteName: string | null,
|
||||||
): PatternResult {
|
): PlayerPattern {
|
||||||
// Look for the pattern in the cosmetics.json config
|
// Look for the pattern in the cosmetics.json config
|
||||||
const found = this.cosmetics.patterns[name];
|
const found = this.cosmetics.patterns[name];
|
||||||
if (!found) return { type: "forbidden", reason: "pattern not found" };
|
if (!found) throw new Error(`Pattern ${name} not found`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
decodePatternData(found.pattern, this.b64urlDecode);
|
decodePatternData(found.pattern, this.b64urlDecode);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { type: "forbidden", reason: "invalid pattern" };
|
throw new Error(`Invalid pattern ${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorPalette = this.cosmetics.colorPalettes?.[colorPaletteName ?? ""];
|
const colorPalette = this.cosmetics.colorPalettes?.[colorPaletteName ?? ""];
|
||||||
|
|
||||||
if (flares.includes("pattern:*")) {
|
if (flares.includes("pattern:*")) {
|
||||||
return {
|
return {
|
||||||
type: "allowed",
|
name: found.name,
|
||||||
pattern: { name: found.name, patternData: found.pattern, colorPalette },
|
patternData: found.pattern,
|
||||||
};
|
colorPalette,
|
||||||
|
} satisfies PlayerPattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
const flareName =
|
const flareName =
|
||||||
@@ -56,101 +77,28 @@ export class PrivilegeCheckerImpl implements PrivilegeChecker {
|
|||||||
if (flares.includes(flareName)) {
|
if (flares.includes(flareName)) {
|
||||||
// Player has a flare for this pattern
|
// Player has a flare for this pattern
|
||||||
return {
|
return {
|
||||||
type: "allowed",
|
name: found.name,
|
||||||
pattern: { name: found.name, patternData: found.pattern, colorPalette },
|
patternData: found.pattern,
|
||||||
};
|
colorPalette,
|
||||||
|
} satisfies PlayerPattern;
|
||||||
} else {
|
} else {
|
||||||
return { type: "forbidden", reason: "no flares for pattern" };
|
throw new Error(`No flares for pattern ${name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isCustomFlagAllowed(
|
isColorAllowed(flares: string[], color: string): PlayerColor {
|
||||||
flag: string,
|
const allowedColors = flares
|
||||||
flares: readonly string[] | undefined,
|
.filter((flare) => flare.startsWith("color:"))
|
||||||
): true | "restricted" | "invalid" {
|
.map((flare) => "#" + flare.split(":")[1]);
|
||||||
if (!flag.startsWith("!")) return "invalid";
|
if (!allowedColors.includes(color)) {
|
||||||
const code = flag.slice(1);
|
throw new Error(`Color ${color} not allowed`);
|
||||||
if (!code) return "invalid";
|
|
||||||
const segments = code.split("_");
|
|
||||||
if (segments.length === 0) return "invalid";
|
|
||||||
|
|
||||||
const MAX_LAYERS = 6; // Maximum number of layers allowed
|
|
||||||
if (segments.length > MAX_LAYERS) return "invalid";
|
|
||||||
|
|
||||||
const superFlare = flares?.includes("flag:*") ?? false;
|
|
||||||
|
|
||||||
for (const segment of segments) {
|
|
||||||
const [layerKey, colorKey] = segment.split("-");
|
|
||||||
if (!layerKey || !colorKey) return "invalid";
|
|
||||||
const layer = this.cosmetics.flag?.layers[layerKey];
|
|
||||||
const color = this.cosmetics.flag?.color[colorKey];
|
|
||||||
if (!layer || !color) return "invalid";
|
|
||||||
|
|
||||||
// Super-flare bypasses all restrictions
|
|
||||||
if (superFlare) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check layer restrictions
|
|
||||||
const layerSpec = layer;
|
|
||||||
let layerAllowed = false;
|
|
||||||
if (!layerSpec.flares) {
|
|
||||||
layerAllowed = true;
|
|
||||||
} else {
|
|
||||||
// By flare
|
|
||||||
if (
|
|
||||||
layerSpec.flares &&
|
|
||||||
flares?.some((f) => layerSpec.flares?.includes(f))
|
|
||||||
) {
|
|
||||||
layerAllowed = true;
|
|
||||||
}
|
|
||||||
// By named flag:layer:{name}
|
|
||||||
if (flares?.includes(`flag:layer:${layerSpec.name}`)) {
|
|
||||||
layerAllowed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check color restrictions
|
|
||||||
const colorSpec = color;
|
|
||||||
let colorAllowed = false;
|
|
||||||
if (!colorSpec.flares) {
|
|
||||||
colorAllowed = true;
|
|
||||||
} else {
|
|
||||||
// By flare
|
|
||||||
if (
|
|
||||||
colorSpec.flares &&
|
|
||||||
flares?.some((f) => colorSpec.flares?.includes(f))
|
|
||||||
) {
|
|
||||||
colorAllowed = true;
|
|
||||||
}
|
|
||||||
// By named flag:color:{name}
|
|
||||||
if (flares?.includes(`flag:color:${colorSpec.name}`)) {
|
|
||||||
colorAllowed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If either part is restricted, block
|
|
||||||
if (!(layerAllowed && colorAllowed)) {
|
|
||||||
return "restricted";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return { color };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FailOpenPrivilegeChecker implements PrivilegeChecker {
|
export class FailOpenPrivilegeChecker implements PrivilegeChecker {
|
||||||
isPatternAllowed(
|
isAllowed(flares: string[], refs: PlayerCosmeticRefs): CosmeticResult {
|
||||||
flares: readonly string[],
|
return { type: "allowed", cosmetics: {} };
|
||||||
name: string,
|
|
||||||
colorPaletteName: string | null,
|
|
||||||
): PatternResult {
|
|
||||||
return { type: "unknown" };
|
|
||||||
}
|
|
||||||
|
|
||||||
isCustomFlagAllowed(
|
|
||||||
flag: string,
|
|
||||||
flares: readonly string[] | undefined,
|
|
||||||
): true | "restricted" | "invalid" {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-82
@@ -13,9 +13,6 @@ import {
|
|||||||
ClientMessageSchema,
|
ClientMessageSchema,
|
||||||
ID,
|
ID,
|
||||||
PartialGameRecordSchema,
|
PartialGameRecordSchema,
|
||||||
PlayerCosmeticRefs,
|
|
||||||
PlayerCosmetics,
|
|
||||||
PlayerPattern,
|
|
||||||
ServerErrorMessage,
|
ServerErrorMessage,
|
||||||
} from "../core/Schemas";
|
} from "../core/Schemas";
|
||||||
import { replacer } from "../core/Util";
|
import { replacer } from "../core/Util";
|
||||||
@@ -26,7 +23,6 @@ import { GameManager } from "./GameManager";
|
|||||||
import { getUserMe, verifyClientToken } from "./jwt";
|
import { getUserMe, verifyClientToken } from "./jwt";
|
||||||
import { logger } from "./Logger";
|
import { logger } from "./Logger";
|
||||||
|
|
||||||
import { assertNever } from "../core/Util";
|
|
||||||
import { PrivilegeRefresher } from "./PrivilegeRefresher";
|
import { PrivilegeRefresher } from "./PrivilegeRefresher";
|
||||||
import { initWorkerMetrics } from "./WorkerMetrics";
|
import { initWorkerMetrics } from "./WorkerMetrics";
|
||||||
|
|
||||||
@@ -366,15 +362,15 @@ export async function startWorker() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { perm, cosmetics, error } = checkCosmetics(
|
const cosmeticResult = privilegeRefresher
|
||||||
clientMsg.cosmetics,
|
.get()
|
||||||
flares ?? [],
|
.isAllowed(flares ?? [], clientMsg.cosmetics ?? {});
|
||||||
);
|
|
||||||
if (perm === "forbidden") {
|
if (cosmeticResult.type === "forbidden") {
|
||||||
log.warn(`Forbidden: ${error}`, {
|
log.warn(`Forbidden: ${cosmeticResult.reason}`, {
|
||||||
clientID: clientMsg.clientID,
|
clientID: clientMsg.clientID,
|
||||||
});
|
});
|
||||||
ws.close(1002, error);
|
ws.close(1002, cosmeticResult.reason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,7 +384,7 @@ export async function startWorker() {
|
|||||||
ip,
|
ip,
|
||||||
clientMsg.username,
|
clientMsg.username,
|
||||||
ws,
|
ws,
|
||||||
cosmetics,
|
cosmeticResult.cosmetics,
|
||||||
);
|
);
|
||||||
|
|
||||||
const wasFound = gm.addClient(
|
const wasFound = gm.addClient(
|
||||||
@@ -424,76 +420,6 @@ export async function startWorker() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function checkCosmetics(
|
|
||||||
cosmetics: PlayerCosmeticRefs | undefined,
|
|
||||||
flares: readonly string[],
|
|
||||||
): {
|
|
||||||
perm: "forbidden" | "allowed";
|
|
||||||
cosmetics?: PlayerCosmetics | undefined;
|
|
||||||
error?: string;
|
|
||||||
} {
|
|
||||||
if (cosmetics === undefined) {
|
|
||||||
return {
|
|
||||||
perm: "allowed",
|
|
||||||
cosmetics: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Check if the flag is allowed
|
|
||||||
if (cosmetics.flag !== undefined) {
|
|
||||||
if (cosmetics.flag.startsWith("!")) {
|
|
||||||
const allowed = privilegeRefresher
|
|
||||||
.get()
|
|
||||||
.isCustomFlagAllowed(cosmetics.flag, flares);
|
|
||||||
if (allowed !== true) {
|
|
||||||
log.warn(`Custom flag ${allowed}: ${cosmetics.flag}`);
|
|
||||||
return {
|
|
||||||
perm: "forbidden",
|
|
||||||
error: `Custom flag ${allowed}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let pattern: PlayerPattern | undefined;
|
|
||||||
// Check if the pattern is allowed
|
|
||||||
if (cosmetics.patternName !== undefined) {
|
|
||||||
const result = privilegeRefresher
|
|
||||||
.get()
|
|
||||||
.isPatternAllowed(
|
|
||||||
flares,
|
|
||||||
cosmetics.patternName,
|
|
||||||
cosmetics.patternColorPaletteName ?? null,
|
|
||||||
);
|
|
||||||
switch (result.type) {
|
|
||||||
case "allowed":
|
|
||||||
pattern = result.pattern;
|
|
||||||
break;
|
|
||||||
case "unknown":
|
|
||||||
log.warn(`Pattern ${cosmetics.patternName} unknown`);
|
|
||||||
return {
|
|
||||||
perm: "forbidden",
|
|
||||||
error: "Could not look up pattern, backend may be offline",
|
|
||||||
};
|
|
||||||
case "forbidden":
|
|
||||||
log.warn(`Pattern ${cosmetics.patternName}: ${result.reason}`);
|
|
||||||
return {
|
|
||||||
perm: "forbidden",
|
|
||||||
error: `Pattern ${cosmetics.patternName}: ${result.reason}`,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
assertNever(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
perm: "allowed",
|
|
||||||
cosmetics: {
|
|
||||||
flag: cosmetics.flag,
|
|
||||||
pattern: pattern,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// The load balancer will handle routing to this server based on path
|
// The load balancer will handle routing to this server based on path
|
||||||
const PORT = config.workerPortByIndex(workerId);
|
const PORT = config.workerPortByIndex(workerId);
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { PlayerExecution } from "../../../src/core/execution/PlayerExecution";
|
||||||
|
import {
|
||||||
|
Game,
|
||||||
|
Player,
|
||||||
|
PlayerInfo,
|
||||||
|
PlayerType,
|
||||||
|
UnitType,
|
||||||
|
} from "../../../src/core/game/Game";
|
||||||
|
import { setup } from "../../util/Setup";
|
||||||
|
import { executeTicks } from "../../util/utils";
|
||||||
|
|
||||||
|
let game: Game;
|
||||||
|
let player: Player;
|
||||||
|
let otherPlayer: Player;
|
||||||
|
|
||||||
|
describe("PlayerExecution", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
game = await setup(
|
||||||
|
"big_plains",
|
||||||
|
{
|
||||||
|
infiniteGold: true,
|
||||||
|
instantBuild: true,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
new PlayerInfo("player", PlayerType.Human, "client_id1", "player_id"),
|
||||||
|
new PlayerInfo("other", PlayerType.Human, "client_id2", "other_id"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
while (game.inSpawnPhase()) {
|
||||||
|
game.executeNextTick();
|
||||||
|
}
|
||||||
|
|
||||||
|
player = game.player("player_id");
|
||||||
|
otherPlayer = game.player("other_id");
|
||||||
|
|
||||||
|
game.addExecution(new PlayerExecution(player));
|
||||||
|
game.addExecution(new PlayerExecution(otherPlayer));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("DefensePost lv. 1 is destroyed when tile owner changes", () => {
|
||||||
|
const tile = game.ref(50, 50);
|
||||||
|
player.conquer(tile);
|
||||||
|
const defensePost = player.buildUnit(UnitType.DefensePost, tile, {});
|
||||||
|
|
||||||
|
game.executeNextTick();
|
||||||
|
expect(game.unitCount(UnitType.DefensePost)).toBe(1);
|
||||||
|
expect(defensePost.level()).toBe(1);
|
||||||
|
|
||||||
|
otherPlayer.conquer(tile);
|
||||||
|
executeTicks(game, 2);
|
||||||
|
|
||||||
|
expect(game.unitCount(UnitType.DefensePost)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("DefensePost lv. 2+ is downgraded when tile owner changes", () => {
|
||||||
|
const tile = game.ref(50, 50);
|
||||||
|
player.conquer(tile);
|
||||||
|
const defensePost = player.buildUnit(UnitType.DefensePost, tile, {});
|
||||||
|
defensePost.increaseLevel();
|
||||||
|
|
||||||
|
expect(defensePost.level()).toBe(2);
|
||||||
|
expect(game.unitCount(UnitType.DefensePost)).toBe(2); // unitCount sums levels
|
||||||
|
expect(player.units(UnitType.DefensePost)).toHaveLength(1);
|
||||||
|
expect(defensePost.isActive()).toBe(true);
|
||||||
|
|
||||||
|
otherPlayer.conquer(tile);
|
||||||
|
executeTicks(game, 2);
|
||||||
|
|
||||||
|
expect(defensePost.level()).toBe(1);
|
||||||
|
expect(game.unitCount(UnitType.DefensePost)).toBe(1);
|
||||||
|
expect(otherPlayer.units(UnitType.DefensePost)).toHaveLength(1);
|
||||||
|
expect(defensePost.owner()).toBe(otherPlayer);
|
||||||
|
expect(defensePost.isActive()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Non-DefensePost structures are transferred (not downgraded) when tile owner changes", () => {
|
||||||
|
const tile = game.ref(50, 50);
|
||||||
|
player.conquer(tile);
|
||||||
|
const city = player.buildUnit(UnitType.City, tile, {});
|
||||||
|
|
||||||
|
expect(game.unitCount(UnitType.City)).toBe(1);
|
||||||
|
expect(city.level()).toBe(1);
|
||||||
|
expect(city.owner()).toBe(player);
|
||||||
|
expect(city.isActive()).toBe(true);
|
||||||
|
|
||||||
|
otherPlayer.conquer(tile);
|
||||||
|
executeTicks(game, 2);
|
||||||
|
|
||||||
|
expect(game.unitCount(UnitType.City)).toBe(1);
|
||||||
|
expect(city.level()).toBe(1);
|
||||||
|
expect(city.owner()).toBe(otherPlayer);
|
||||||
|
expect(city.isActive()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import type { Cosmetics } from "../../src/core/CosmeticSchemas";
|
|
||||||
import { PrivilegeCheckerImpl } from "../../src/server/Privilege";
|
|
||||||
|
|
||||||
describe("PrivilegeChecker.isCustomFlagAllowed (with mock cosmetics)", () => {
|
|
||||||
const dummyPatternDecoder = (_base64: string) => {
|
|
||||||
throw new Error("Method not implemented");
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockCosmetics: Cosmetics = {
|
|
||||||
patterns: {},
|
|
||||||
flag: {
|
|
||||||
layers: {
|
|
||||||
a: {
|
|
||||||
name: "chocolate",
|
|
||||||
flares: ["cosmetic:flags"],
|
|
||||||
},
|
|
||||||
b: { name: "center_hline" },
|
|
||||||
c: { name: "admin_layer" },
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
a: { color: "#ff0000", name: "red", flares: ["cosmetic:red"] },
|
|
||||||
b: { color: "#00ff00", name: "green" },
|
|
||||||
c: { color: "#0000ff", name: "blue", flares: ["cosmetic:blue"] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const checker = new PrivilegeCheckerImpl(mockCosmetics, dummyPatternDecoder);
|
|
||||||
|
|
||||||
it("allowed: unrestricted layer/color", () => {
|
|
||||||
expect(checker.isCustomFlagAllowed("!b-b", [])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allowed: donor layer with correct flare", () => {
|
|
||||||
expect(checker.isCustomFlagAllowed("!a-b", ["cosmetic:flags"])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allowed: color with correct flare", () => {
|
|
||||||
expect(checker.isCustomFlagAllowed("!b-c", ["cosmetic:blue"])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("invalid: non-existent layer", () => {
|
|
||||||
expect(checker.isCustomFlagAllowed("!zzz-a", [])).toBe("invalid");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("invalid: non-existent color", () => {
|
|
||||||
expect(checker.isCustomFlagAllowed("!a-zzz", [])).toBe("invalid");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allowed: superFlare allows all listed", () => {
|
|
||||||
expect(checker.isCustomFlagAllowed("!a-a", ["flag:*"])).toBe(true);
|
|
||||||
expect(checker.isCustomFlagAllowed("!b-b", ["flag:*"])).toBe(true);
|
|
||||||
expect(checker.isCustomFlagAllowed("!c-a", ["flag:*"])).toBe(true);
|
|
||||||
expect(checker.isCustomFlagAllowed("!a-c", ["flag:*"])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("invalid: superFlare does not allow non-existent", () => {
|
|
||||||
expect(checker.isCustomFlagAllowed("!zzz-zzz", ["flag:*"])).toBe("invalid");
|
|
||||||
});
|
|
||||||
it("allowed: flare flag:layer:chocolate allows chocolate layer", () => {
|
|
||||||
expect(checker.isCustomFlagAllowed("!a-b", ["flag:layer:chocolate"])).toBe(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it("allowed: flare flag:color:blue allows blue color", () => {
|
|
||||||
expect(checker.isCustomFlagAllowed("!b-c", ["flag:color:blue"])).toBe(true);
|
|
||||||
});
|
|
||||||
it("restricted: only color flare, layer still restricted", () => {
|
|
||||||
expect(checker.isCustomFlagAllowed("!a-c", ["cosmetic:blue"])).toBe(
|
|
||||||
"restricted",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it("restricted: only layer flare, color still restricted", () => {
|
|
||||||
expect(checker.isCustomFlagAllowed("!c-a", ["cosmetic:flags"])).toBe(
|
|
||||||
"restricted",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allowed: two segments, both unrestricted", () => {
|
|
||||||
expect(checker.isCustomFlagAllowed("!b-b_b-b", [])).toBe(true);
|
|
||||||
});
|
|
||||||
it("allowed: two segments, both by flare", () => {
|
|
||||||
expect(
|
|
||||||
checker.isCustomFlagAllowed("!a-c_a-c", [
|
|
||||||
"cosmetic:flags",
|
|
||||||
"cosmetic:blue",
|
|
||||||
]),
|
|
||||||
).toBe(true);
|
|
||||||
expect(checker.isCustomFlagAllowed("!a-c_a-c", ["cosmetic:flags"])).toBe(
|
|
||||||
"restricted",
|
|
||||||
);
|
|
||||||
expect(checker.isCustomFlagAllowed("!a-c_a-c", ["cosmetic:blue"])).toBe(
|
|
||||||
"restricted",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user