From 40a9e54ee7f4b971b2e36f4f839174159512c876 Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Fri, 16 Jan 2026 04:21:49 +0900 Subject: [PATCH 1/7] mls (v4.13) (#2907) ## Description: mls for v29 Version identifier within MLS: 4.13 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: aotumuri --- resources/lang/bg.json | 223 ++++++++++++++++++++++++++++++--------- resources/lang/ja.json | 231 +++++++++++++++++++++++++++++++---------- resources/lang/nl.json | 223 ++++++++++++++++++++++++++++++--------- 3 files changed, 529 insertions(+), 148 deletions(-) diff --git a/resources/lang/bg.json b/resources/lang/bg.json index 2fef4934e..ce630b90f 100644 --- a/resources/lang/bg.json +++ b/resources/lang/bg.json @@ -7,6 +7,7 @@ }, "common": { "close": "Затвори", + "back": "Назад", "available": "Наличен", "preset_max": "Макс", "summary_send": "Изпрати", @@ -17,7 +18,9 @@ "cap_tooltip": "Оставащ капацитет на получателя", "target_dead": "Целта бе елиминирана", "target_dead_note": "Не можеш да изпращаш ресурси на елиминиран играч.", - "none": "Няма" + "none": "Няма", + "copied": "Копирано!", + "click_to_copy": "Кликни, за да копираш" }, "main": { "title": "OpenFront (АЛФА)", @@ -26,17 +29,28 @@ "checking_login": "Проверяване на входа...", "logged_in": "Влезли сте!", "log_out": "Излез от профила си", - "create_lobby": "Създай частна игра", - "join_lobby": "Присъедини се към частна игра", - "single_player": "Самостоятелна игра", + "create": "Създай частна игра", + "join": "Присъедини се към частна игра", + "solo": "Самостоятелна игра", "instructions": "Инструкции", + "game_info": "Информация за играта", "wiki": "Wiki", "privacy_policy": "Поверителност", "terms_of_service": "Условия за ползване", - "reddit": "Reddit" + "copyright": "© OpenFront™ and Contributors", + "reddit": "Reddit", + "play": "Играй", + "news": "Новини", + "store": "Магазин", + "options": "Опции", + "keys": "Клавиши", + "stats": "Статистики", + "account": "Акаунт", + "help": "Помощ", + "menu": "Меню", + "pick_pattern": "Избери шаблон!" }, "news": { - "see_all_releases": "Виж всички издания", "github_link": "в GitHub", "title": "Бележки по изданието" }, @@ -64,7 +78,7 @@ "ui_gold": "Злато - Количеството злато, което притежаваш и скоростта, с която го получаваш.", "ui_attack_ratio": "Съотношение на атака - Количеството войници, които ще се използват при атака. Можеш да коригираш съотношението на атака, използвайки плъзгача. Притежаването на повече атакуващи войници от тези в защита ще доведе до по-малка загуба на войници при атака, докато разполагането с по-малко ще увеличи щетите, нанесени на атакуващите ти войници. Ефектът не надхвърля съотношения 2:1.", "ui_events": "Панел за събития", - "ui_events_desc": "Панелът за събития показва най-новите събития, заявки и съобщения от бърз чат. Някои примери са:", + "ui_events_desc": "Панелът за събития показва най-актуалните събития, заявки и съобщения от бърз чат. Някои примери са:", "ui_events_alliance": "Съюз - Заявките за съюз могат да бъдат приети или отхвърлени. Съюзниците могат да споделят ресурси и войници, но не могат да се атакуват взаимно. Кликането върху \"Фокусиране\" премества изгледа върху играча, изпратил заявката.", "ui_events_attack": "Атаки - Показани са атаките срещу теб, както и твоите собствени атаки. Кликни върху съобщението, за да центрираш изгледа върху атаката, ракетата или лодката (транспортен кораб). Можеш да оттеглиш войниците си, като кликнеш върху червения бутон X. Това ще струва живота на 25% от атакуващите ти войници. Ако оттеглиш атака с лодка, лодката се връща в началната си точка и ще атакува там, ако тази земя е била превзета от друг. Ракетите не могат да бъдат оттеглени, след като бъдат изстреляни.", "ui_events_quickchat": "Бърз чат - Тук можеш да видиш изпратените и получените съобщения в чата. Изпрати съобщение до играч, като кликнеш върху иконката за бърз чат в менюто му с информация.", @@ -83,6 +97,8 @@ "radial_attack": "Отвориш менюто за атака.", "radial_info": "Отвориш информационното меню.", "radial_boat": "Изпратиш лодка (транспортен кораб) за атака на избраното място. Възможно е само, ако имаш достъп до вода.", + "radial_donate_troops": "Дариш войници на съюзника, на когото си отворил радиалното меню, еквивалентни на процента на плъзгача за съотношение на атака.", + "radial_donate_gold": "Отваря плъзгащото меню за даряване на злато, за да можеш бързо да изпратиш злато на съюзниците.", "radial_close": "Затвориш менюто.", "info_title": "Информационно меню", "info_enemy_desc": "Съдържа информация като име на избрания играч, злато, войници, дали е спряна търговията с теб, изпратени ракети към теб и дали играчът е предател. Спряната търговия означава, че няма да получаваш злато от него и той няма да ти изпраща злато чрез търговски кораби. Ръчно (ако играчът кликне върху „Прекратяване на търговия“, което продължава, докато и двамата не кликнат върху „Започване на търговия“) или автоматично (ако си предал съюзника си, което продължава, докато не станете съюзници отново или след 5 минути). Показва се \"Да\" на \"Предател\" за 30 секунди, когато играчът е предал и нападнал играч, който е бил в съюз с него. Иконките по-долу представляват следните взаимодействия:", @@ -114,7 +130,7 @@ "build_silo": "Ракетен силоз", "build_silo_desc": "Позволява изстрелване на ракети.", "build_sam": "Противоракетна установка земя-въздух SAM", - "build_sam_desc": "Може да прихваща вражески ракети в своя обхват от 100 пиксела. Със 100% шанс да свали атомна бомба, 80% за водородна бомба и 50% за отделни бойни ракети на МИРВ. Противоракетната установка земя-въздух SAM има 7,5 секунди охлаждане.", + "build_sam_desc": "Може да прехваща вражески ракети в обхват от 100 пиксела. Противоракетната установка земя-въздух SAM има време за охлаждане от 7,5 секунди.", "build_atom": "Атомна бомба", "build_atom_desc": "Малка експлозивна бомба, която унищожава територия, сгради, кораби и лодки. Поражда се от най-близкия ракетен силоз и се приземява в областта, в която първо си щракнал, за да я построиш.", "build_hydrogen": "Водородна бомба", @@ -129,12 +145,15 @@ "icon_embargo": "Стоп знак за долар - Ембарго. Този играч е спрял да търгува с теб автоматично или ръчно.", "icon_request": "Пликче за писмо - молба за съюз. Този играч ти е изпратил молба за съюз.", "info_enemy_panel": "Информационно меню за врагове", - "exit_confirmation": "Сигурен ли си, че искаш да излезеш от играта?" + "exit_confirmation": "Сигурен ли си, че искаш да излезеш от играта?", + "bomb_direction": "Посока на дъгата на атомна/водородна бомба" }, "single_modal": { - "title": "Самостоятелна Игра", + "title": "Самостоятелно", "random_spawn": "Случайно появяване", "allow_alliances": "Позволяване на съюзничества", + "toggle_achievements": "Превключване на постижения", + "sign_in_for_achievements": "Впиши се, за да получаваш постижения", "options_title": "Опции", "bots": "Ботове: ", "bots_disabled": "Изключени", @@ -145,6 +164,8 @@ "infinite_troops": "Безкрайна популация", "compact_map": "Компактна карта", "max_timer": "Продължителност на играта (в минути)", + "max_timer_placeholder": "Минути", + "max_timer_invalid": "Моля, въведи валидна максимална стойност на таймера (1-120 минути)", "disable_nukes": "Изключване на ядрени оръжия", "enables_title": "Активиране на настройки", "start": "Започване на игра" @@ -156,10 +177,26 @@ }, "account_modal": { "title": "Акаунт", - "logged_in_as": "Вписан като {email}", + "connected_as": "Вписан като", + "stats_overview": "Преглед на статистики", + "save_progress_title": "Запази си напредъка", + "save_progress_desc": "Свържи си акаунта, за да запазиш статистиките, ранка и козметиките си в безопасност.", + "link_discord": "Свържи Discord акаунт", + "link_via_email_placeholder": "Свържи чрез имейл", + "link_button": "Свържи", + "log_out": "Изход от профила", + "welcome_back": "Добре дошъл отново!", + "sign_in_desc": "Впиши се, за да запазиш статистиките и напредъка си", + "or": "ИЛИ", + "email_placeholder": "Въведи имейл адреса си", + "get_magic_link": "Използвай магически линк", + "linked_account": "Вписан като {account_name}", "fetching_account": "Взима се информацията за профила...", - "logged_in_with_discord": "Вписал си се с Discord", - "recovery_email_sent": "Имейл за възстановяване бе изпратен на {email}" + "recovery_email_sent": "Имейл за възстановяване бе изпратен на {email}", + "not_found": "Не е намерен", + "clear_session": "Изчисти сесията", + "failed_to_send_recovery_email": "Грешка при изпращането на имейл за възстановяване", + "enter_email_address": "Моля, въведи имейл адрес" }, "stats_modal": { "title": "Статистики", @@ -167,11 +204,40 @@ "loading": "Зареждане...", "error": "Грешка при зареждането на клановите статистики", "no_stats": "Няма налични кланови статистики", + "no_data_yet": "Все още няма данни", "clan": "Клан", "games": "Игри", "win_score": "Резултат на победи", + "win_score_tooltip": "Претеглени победи въз основа на участието на клана и трудността на мача", "loss_score": "Резултат на загуби", - "win_loss_ratio": "П/З" + "loss_score_tooltip": "Претеглени загуби въз основа на участието на клана и трудността на мача", + "win_loss_ratio": "П/З", + "ratio": "Съотношение", + "rank": "Ранк", + "try_again": "Опитай отново" + }, + "game_info_modal": { + "title": "Информация за играта", + "players": "Играчи", + "atoms": "Атомни бомби", + "hydros": "Водородни бомби", + "mirv": "МИРВ", + "bombs": "Бомби", + "total_gold": "Общо", + "all_gold": "Всичко злато", + "trade": "Търговия", + "conquest_gold": "Завладяно злато от играч", + "stolen_gold": "Откраднато с военни кораби", + "num_of_conquests": "Брой завладяни играчи", + "duration": "Продължителност", + "survival_time": "Време на оцеляване", + "war": "Война", + "economy": "Икономика", + "conquests": "Завоевания", + "pirate": "Пират", + "conquered": "Завладяно", + "loading_game_info": "Зареждат се статистиките на играта", + "no_winner": "Играта е свършила без победител" }, "map": { "map": "Карта", @@ -186,6 +252,7 @@ "asia": "Азия", "mars": "Марс", "southamerica": "Южна Америка", + "britanniaclassic": "Британия (Класическа)", "britannia": "Британия", "gatewaytotheatlantic": "Порта към Атлантика", "australia": "Австралия", @@ -196,7 +263,7 @@ "betweentwoseas": "Между Две Морета", "faroeislands": "Фарьорски острови", "deglaciatedantarctica": "Обезледена Антарктида", - "europeclassic": "Европа (класическа)", + "europeclassic": "Европа (Класическа)", "falklandislands": "Фолкландски острови", "baikal": "Байкал", "halkidiki": "Халкидики", @@ -206,19 +273,33 @@ "yenisei": "Енисей", "pluto": "Плутон", "montreal": "Монтреал", + "newyorkcity": "Ню Йорк", "achiran": "Ахиран", "baikalnukewars": "Байкал (Ядрени войни)", "fourislands": "Четири острова", "gulfofstlawrence": "Залив Сейнт Лорънс", - "lisbon": "Лисабон" + "lisbon": "Лисабон", + "svalmel": "Свалмел", + "manicouagan": "Маникуаган", + "lemnos": "Лемнос", + "sierpinski": "Серпински", + "twolakes": "Две езера", + "straitofhormuz": "Ормузки проток", + "surrounded": "Обкражен", + "didier": "Дидиер", + "didierfrance": "Дидиер (Франция)", + "amazonriver": "Река Амазонка" }, "map_categories": { "continental": "Континентално", "regional": "Регионално", - "fantasy": "Друго" + "fantasy": "Друго", + "special": "Специално", + "arcade": "Аркада" }, "map_component": { - "loading": "Зареждане..." + "loading": "Зареждане...", + "error": "Грешка" }, "private_lobby": { "title": "Присъединяване към частна игра", @@ -229,42 +310,55 @@ "checking": "Проверяване на частна игра...", "not_found": "Не е намерена частната игра. Моля, провери ID-то и опитай отново.", "error": "Възникна грешка. Моля, опитай отново или се свържи с екипа за поддръжка.", - "joined_waiting": "Присъединяването е успешно! Чакане за започване на играта...", - "version_mismatch": "Тази игра е създадена на различна версия. Не можеш да бъдеш присъединен." + "joined_waiting": "Присъедини се към лобито! Чакаме хостът да започне...", + "version_mismatch": "Тази игра е създадена на различна версия. Не можеш да бъдеш присъединен.", + "disabled_units": "Изключване на военни единици" }, "public_lobby": { "join": "Присъединяване към следващата игра", "waiting": "чакащи играчи", - "teams_Duos": "по 2-ма (Дуос)", - "teams_Trios": "по 3-ма (Триос)", - "teams_Quads": "по 4-ма (Куадс)", + "teams_Duos": "{team_count} отбора по 2-ма (Дуос)", + "teams_Trios": "{team_count} отбора по 3-ма (Триос)", + "teams_Quads": "{team_count} отбора по 4-ма (Куадс)", + "waiting_for_players": "Изчакване на играчи", + "starting_game": "Играта се стартира…", "teams_hvn": "Хора срещу Нации", + "teams_hvn_detailed": "{num} Души vs {num} Нации", "teams": "{num} отбора", - "players_per_team": "по {num}" + "players_per_team": "по {num}", + "started": "Стартирана" }, "matchmaking_modal": { - "title": "Мачмейкинг", + "title": "1v1 Ранков мачмейкинг (АЛФА)", "connecting": "Свързване със сървъра за мачмейкинг...", "searching": "Търси се игра...", - "waiting_for_game": "Изчаква се да започне играта..." + "waiting_for_game": "Изчаква се да започне играта...", + "elo": "Твоето ЕЛО: {elo}" }, "username": { "enter_username": "Въведи потребителско име", "not_string": "Потребителското име трябва да е символен низ.", "too_short": "Потребителското име трябва да е дълго поне {min} символа.", "too_long": "Потребителското име не трябва да надвишава {max} символа.", - "invalid_chars": "Потребителското име може да съдържа само букви, цифри, интервали, долни черти и [квадратни скоби]." + "invalid_chars": "Потребителското име може да съдържа само букви, цифри, интервали и долни черти.", + "tag": "ТАГ", + "tag_too_short": "Клановият таг трябва да съдържа от 2 до 5 буквено-цифрови знака.", + "tag_invalid_chars": "Клановият таг може да съдържа само букви и цифри." }, "host_modal": { - "title": "Частна игра", + "title": "Създай частна игра", + "label": "Частна", "mode": "Начин на игра", "team_count": "Брой отбори", + "team_type": "Вид на отбора", "options_title": "Опции", "bots": "Ботове: ", "bots_disabled": "Изключени", + "player_immunity_duration": "Продължителност на военния имунитет (минути)", "nations": "Нации: ", "disable_nations": "Изключване на нации", "max_timer": "Продължителност на играта (в минути)", + "mins_placeholder": "Минути", "instant_build": "Незабавно построяване", "infinite_gold": "Безкрайно злато", "donate_gold": "Даряване на злато", @@ -283,7 +377,11 @@ "assigned_teams": "Назначени отбори", "empty_teams": "Празни отбори", "empty_team": "Празен", - "remove_player": "Премахване на {username}" + "remove_player": "Премахване на {username}", + "teams_Duos": "Дуос (отбори по 2-ма)", + "teams_Trios": "Триос (отбори по 3-ма)", + "teams_Quads": "Куадс (отбори по 4-ма)", + "teams_Humans Vs Nations": "Хора срещу Нации" }, "team_colors": { "red": "Червен", @@ -301,16 +399,20 @@ "code_license": "Кодът е лицензиран съгласно AGPL-3.0 (без гаранция)" }, "difficulty": { - "difficulty": "Трудност", - "Easy": "Релаксирано", - "Medium": "Балансирано", - "Hard": "Интензивно", - "Impossible": "Невъзможно" + "difficulty": "Трудност на нациите", + "easy": "Лесно", + "medium": "Средно", + "hard": "Трудно", + "impossible": "Невъзможно" }, "game_mode": { "ffa": "Всеки срещу всеки (FFA)", "teams": "Отбори" }, + "public_game_modifier": { + "random_spawn": "Случайно появяване", + "compact_map": "Компактна карта" + }, "select_lang": { "title": "Изберете език" }, @@ -340,16 +442,18 @@ "special_effects_desc": "Превключване на специалните ефекти. Деактивиране, за да се увеличи производителността", "structure_sprites_label": "Структурни спрайтове", "structure_sprites_desc": "Превключване на структурните спрайтове", + "cursor_cost_label_label": "Цена на изграждане под курсора", + "cursor_cost_label_desc": "Показване на цената за изграждане под иконката на курсора", "anonymous_names_label": "Скрити имена", "anonymous_names_desc": "Скриване на истинските имена на играчите с произволни такива на екрана ти.", "lobby_id_visibility_label": "Скрити ID-та на частните игри", "lobby_id_visibility_desc": "Скриване на ID-то на частната игра при нейното създаване", + "toggle_visibility": "Превключване на видимостта", "left_click_label": "Щтракване на ляв бутон, за да се отвори менюто", "left_click_desc": "Когато е ВКЛЮЧЕНО, щракването с ляв бутон отваря менюто и атаките се извършват чрез бутона на меч. Когато е ИЗКЛЮЧЕНО, щракването с ляв бутон атакува директно.", "left_click_menu": "Меню ляв клик", "attack_ratio_label": "⚔️ Съотношение на атака", "attack_ratio_desc": "Какъв процент от Вашите войници да се изпратят в атака (1–100%)", - "troop_ratio_desc": "Коригиране на баланса между войници (за битка) и работници (за производство на злато) (1–100%)", "territory_patterns_label": "🏳️ Териториални шаблони", "territory_patterns_desc": "Избиране дали да се показват дизайни на шаблони на територия в играта", "performance_overlay_label": "Горен слой за производителност", @@ -358,6 +462,7 @@ "easter_writing_speed_desc": "Регулиране на това колко бързо се преструвате, че кодирате(x1–x100)", "easter_bug_count_label": "Брой грешки", "easter_bug_count_desc": "С колко грешки сте ок (0–1000, емоционално)", + "press_a_key": "Натисни клавиш", "view_options": "Вижте настройките", "toggle_view": "Превключване на изгледа", "toggle_view_desc": "Алтернативен изглед (терен/държави)", @@ -416,7 +521,8 @@ "exit_game_label": "Напускане на играта", "exit_game_info": "Връщане в главното меню", "background_music_volume": "Сила на фоновата музика", - "sound_effects_volume": "Сила на звука на звуковите ефекти" + "sound_effects_volume": "Сила на звука на звуковите ефекти", + "keybind_conflict_error": "Клавишът {key} вече е вързан за друго действие." }, "chat": { "title": "Бърз чат", @@ -529,6 +635,7 @@ "other_team": "{team} отбор спечели!", "you_won": "Ти спечели!", "other_won": "{player} спечели!", + "nation_won": "Нацията {nation} спечели!", "exit": "Напускане на играта", "keep": "Продължаване на играта", "spectate": "Наблюдаване", @@ -537,7 +644,7 @@ "ofm_winter_description": "Присъедини се към състезателния турнир и се състезавай срещу най-добрите играчи", "join_tournament": "Присъедини се към турнира", "join_discord": "Присъедини се към общността ни в Discord!", - "discord_description": "Свържи се с други играчи, получавай актуална информация и споделяй стратегии", + "discord_description": "Свържи се с играчи, открий нови функции и спечели награди!", "join_server": "Влез в сървъра", "youtube_tutorial": "Нужда от помощ?" }, @@ -549,7 +656,7 @@ "team": "Отбор", "owned": "Притежавано", "gold": "Злато", - "troops": "Войници", + "maxtroops": "Максимални войници", "launchers": "Установки", "sams": "Противоракетни установки земя-въздух SAM", "warships": "Бойни кораби", @@ -565,6 +672,7 @@ "team": "Отбор", "alliance_timeout": "Съюзът изтича след", "troops": "Войници", + "maxtroops": "Максимални войници", "a_troops": "Атакуващи войници", "gold": "Злато", "ports": "Пристанища", @@ -575,7 +683,9 @@ "warships": "Бойни кораби", "health": "Живот", "attitude": "Становище", - "levels": "Нива" + "levels": "Нива", + "wilderness_title": "Пустош", + "irradiated_wilderness_title": "Облъчена пустош" }, "events_display": { "retreating": "отстъпване", @@ -653,7 +763,10 @@ "send_alliance": "Изпрати съюз", "send_troops": "Изпрати войници", "send_gold": "Изпрати злато", - "emotes": "Емоджита" + "emotes": "Емоджита", + "arc_up": "Възходяща дъга", + "arc_down": "Низходяща дъга", + "flip_rocket_trajectory": "Обърни траекторията на ракетата" }, "send_troops_modal": { "title_with_name": "Изпрати войници на {name}", @@ -702,25 +815,31 @@ }, "heads_up_message": { "choose_spawn": "Изберете начална локация", - "random_spawn": "Случайното появяване е активирано. Избиране на начална локация за теб..." + "random_spawn": "Случайното появяване е активирано. Избиране на начална локация за теб...", + "singleplayer_game_paused": "Играта е на пауза", + "multiplayer_game_paused": "Играта е паузирана от създателя на лобито" }, "territory_patterns": { "title": "Териториални шаблони", "colors": "Цветове", "purchase": "Купуване", "show_only_owned": "Моите шаблони", + "all_owned": "Имаш всички шаблони! Провери отново по-късно за нови артикули.", + "not_logged_in": "Не си се вписал в профил", "blocked": { "login": "Трябва да сте влезли в профила си, за да получите достъп до този шаблон.", "purchase": "Закупете този шаблон, за да го отключите." }, "pattern": { "default": "Стандартен" - } + }, + "select_skin": "Избери шаблон", + "selected": "е избран" }, "flag_input": { - "title": "Изберете знаме", - "button_title": "Изберете знаме!", - "search_flag": "Търсене..." + "title": "Избери знаме", + "button_title": "Избери знаме!", + "search_flag": "Търси..." }, "spawn_ad": { "loading": "Зарежда се реклама..." @@ -786,8 +905,9 @@ "mode": "Вид", "mode_ffa": "Всеки срещу всеки (FFA)", "mode_team": "Отбор", - "view": "Виж", + "replay": "Повторение", "details": "Детайли", + "ranking": "Класиране", "started": "Стартирана", "map": "Карта", "difficulty": "Трудност", @@ -796,13 +916,20 @@ "player_stats_tree": { "public": "Публична", "private": "Частна", - "singleplayer": "Самостоятелна Игра", + "singleplayer": "Самостоятелно", "mode": "Вид", "stats_wins": "Победи", "stats_losses": "Загуби", "stats_wlr": "Съотношение победи:загуби", "stats_games_played": "Изиграни игри", "mode_ffa": "Всеки срещу всеки (FFA)", - "mode_team": "Отбор" + "mode_team": "Отбор", + "no_stats": "Няма записани статистики за тази селекция." + }, + "matchmaking_button": { + "play_ranked": "1v1 Ранков мачмейкинг", + "description": "(АЛФА)", + "login_required": "Впиши се, за да играеш ранково!", + "must_login": "Трябва да си вписан в профила си, за да играеш ранков мачмейкинг." } } diff --git a/resources/lang/ja.json b/resources/lang/ja.json index 33bcbc2bf..381fd0390 100644 --- a/resources/lang/ja.json +++ b/resources/lang/ja.json @@ -7,6 +7,7 @@ }, "common": { "close": "閉じる", + "back": "戻る", "available": "利用可能", "preset_max": "最大", "summary_send": "送る", @@ -17,7 +18,9 @@ "cap_tooltip": "受取主が受け取れる量", "target_dead": "ターゲットは排除されました", "target_dead_note": "排除されたプレイヤーにはリソースを送ることができません。", - "none": "なし" + "none": "なし", + "copied": "コピーに成功しました!", + "click_to_copy": "クリックしてコピー" }, "main": { "title": "OpenFront (ALPHA)", @@ -26,17 +29,28 @@ "checking_login": "ログイン中...", "logged_in": "ログイン中!", "log_out": "ログアウト", - "create_lobby": "ロビーを作成", - "join_lobby": "ロビーに参加", - "single_player": "シングルプレイヤー", + "create": "ロビーを作成", + "join": "ロビーに参加", + "solo": "1人のロビー", "instructions": "説明書", + "game_info": "ゲームの情報", "wiki": "ウィキ", "privacy_policy": "プライバシーポリシー", "terms_of_service": "利用規約", - "reddit": "Reddit" + "copyright": "©️ OpenFront™ と貢献者", + "reddit": "Reddit", + "play": "プレイ", + "news": "お知らせ", + "store": "ストア", + "options": "設定", + "keys": "キー設定", + "stats": "統計", + "account": "アカウント", + "help": "ヘルプ", + "menu": "メニュー", + "pick_pattern": "模様を選択してください!" }, "news": { - "see_all_releases": "すべてのリリースを見る", "github_link": "GitHub上で", "title": "更新情報" }, @@ -67,7 +81,7 @@ "ui_events_desc": "イベントパネルには、最新のイベント、リクエスト、クイックチャットメッセージが表示されます。以下がその一例です:", "ui_events_alliance": "同盟 — 同盟リクエストは承認または拒否できます。同盟関係にあるプレイヤーは資源や軍隊を共有できますが、互いに攻撃することはできません。「Focus(注視)」をクリックすると、リクエストを送ったプレイヤーの位置に画面が移動します。", "ui_events_attack": "攻撃 — 敵からの攻撃や自分の攻撃が表示されます。メッセージをクリックすると、その攻撃・核・ボート(輸送船)に画面が移動します。赤い「X」ボタンをクリックすると軍隊を撤退させることができますが、その場合攻撃部隊の25%が犠牲になります。ボート攻撃を撤退させた場合、ボートは出発地点に戻り、その地点が占領されていれば再び攻撃します。核攻撃は発射後に撤退することはできません。", - "ui_events_quickchat": "クイックチャット – ここでは送信・受信したチャットメッセージを確認できます。プレイヤーにメッセージを送るには、そのプレイヤーの情報メニューにあるクイックチャットアイコンをクリックしてください。", + "ui_events_quickchat": "クイックチャット:ここでは、送信・受信されたメッセージを確認できます。プレイヤーにメッセージを送信するには、そのプレイヤーの情報メニューにあるクイックチャットアイコンをクリックしてください。", "ui_options": "オプション", "ui_options_desc": "以下の項目が含まれます:", "ui_playeroverlay": "プレイヤー情報オーバーレイ", @@ -77,12 +91,14 @@ "option_timer": "タイマー - ゲーム開始からの経過時間", "option_exit": "終了ボタン", "option_settings": "設定メニュー - 設定メニューを開きます。左クリックでオルタネート表示、ダークモード、絵文字、アクション、匿名モードを切り替えることができます。", - "radial_title": "ラジアルメニュー", - "radial_desc": "右クリック(またはモバイルでタッチ)するとラジアルメニューが開きます。右クリックすると、ラジアルメニューを閉じます。メニューから、次のようにできます:", + "radial_title": "円形メニュー", + "radial_desc": "右クリック(またはモバイルでタッチ)すると円形メニューが開きます。右クリックすると、円形メニューを閉じます。メニューから、次のようにできます:", "radial_build": "ビルドメニューを開く。", "radial_attack": "攻撃メニューを開く。", "radial_info": "情報メニューを開く。", "radial_boat": "ボート(輸送船)を派遣して、指定した場所を攻撃します。領地が水辺に接している場合にのみ使用可能です。", + "radial_donate_troops": "円形メニューを開いた味方に、攻撃比率スライダーのパーセンテージに相当する軍隊を寄付します。", + "radial_donate_gold": "資金寄付スライダー:メニューが開き、味方に資金を素早く送信できるようになります。", "radial_close": "メニューを閉じる。", "info_title": "情報メニュー", "info_enemy_desc": "選択されたプレイヤーの名前、所持金、軍隊数、「あなたとの貿易停止」状態、あなたへの核攻撃の有無、裏切り者かどうかなどの情報を含みます。「貿易停止」とは、相手からのゴールドが受け取れず、相手も貿易船を通じてあなたにゴールドを送らなくなることを意味します。これは、手動(プレイヤーが「貿易を停止」をクリックした場合。両者が「貿易を再開」をクリックするまで継続)または、自動(同盟を裏切った場合。再度同盟になるか、5分経過するまで継続)で発生します。「裏切り者」は、そのプレイヤーが同盟中のプレイヤーを攻撃して裏切った場合に、30秒間「Yes」と表示されます。\n下のアイコンは、以下のプレイヤー間のやりとりを表しています:", @@ -110,11 +126,11 @@ "build_port": "港", "build_port_desc": "水辺にのみ建設でき、このアイコンから戦艦を建築することが可能です。自国と他国の間に貿易制限が為されていない限り、自動的に交易船を送り出し、交易が完了すると両国に資金がもたらされます。貿易は手動で「貿易停止」または「貿易開始」を切り替えることができます。また、あなたが相手を攻撃したり、攻撃された場合には交易は自動的に停止し、5分経過するか同盟を結ぶと再開されます。", "build_warship": "戦艦", - "build_warship_desc": "このユニットは、指定したエリアを巡回し、貿易船を拿捕したり、敵の軍艦やボートを撃破したりします。最寄りの港から出現し、最初にクリックした場所を巡回し始めます。軍艦は攻撃クリックで選択し、移動先を攻撃クリックすることで操作できます。", + "build_warship_desc": "このユニットは、指定したエリアを巡回し、貿易船を拿捕したり、敵の戦艦やボートを撃破したりします。最寄りの港から出現し、最初にクリックした場所を巡回し始めます。軍艦は攻撃クリックで選択し、移動先を攻撃クリックすることで操作できます。", "build_silo": "ミサイル格納庫", "build_silo_desc": "ミサイルの発射を可能にします。", "build_sam": "SAMランチャー", - "build_sam_desc": "100ピクセル以内に入った敵ミサイルを、クールダウン7.5秒で迎撃できます。命中率は、原子爆弾に対して100%、水素爆弾に対して80%、MIRVに対して50%です。", + "build_sam_desc": "半径100ピクセル内の敵ミサイルを迎撃できます。SAMのクールダウン時間は7.5秒です。", "build_atom": "原子爆弾", "build_atom_desc": "小型の爆弾で、領土・建物・船舶・ボートを破壊します。最寄りのミサイル格納庫から発射され、最初にクリックした場所に着弾します。", "build_hydrogen": "水素爆弾", @@ -129,12 +145,15 @@ "icon_embargo": "取引停止 - このプレイヤーがあなたを自動または手動で貿易制限をかけているときに表示されます。", "icon_request": "メール - このプレイヤーがあなたへ同盟の申込みをしているときに表示されます。", "info_enemy_panel": "敵の情報パネル", - "exit_confirmation": "本当にゲームを終了しますか?" + "exit_confirmation": "本当にゲームを終了しますか?", + "bomb_direction": "原子爆弾 / 水素爆弾の軌道の向き" }, "single_modal": { - "title": "シングルプレイヤー", + "title": "ソロ", "random_spawn": "ランダムスポーン", "allow_alliances": "同盟を許可", + "toggle_achievements": "実績の表示の切り替え", + "sign_in_for_achievements": "実績を確認するにはサインインしてください", "options_title": "オプション", "bots": "ボット数: ", "bots_disabled": "無効", @@ -145,6 +164,8 @@ "infinite_troops": "兵士無限", "compact_map": "小型マップ", "max_timer": "ゲーム時間 (分)", + "max_timer_placeholder": "分", + "max_timer_invalid": "適切な最大プレイ時間(1~120分)を入力してください", "disable_nukes": "核兵器使用禁止", "enables_title": "機能の有効化", "start": "ゲーム開始" @@ -156,10 +177,26 @@ }, "account_modal": { "title": "アカウント", - "logged_in_as": "{email} としてログインしました", + "connected_as": "接続されたアカウント", + "stats_overview": "統計の概要", + "save_progress_title": "進捗状況を保存する", + "save_progress_desc": "アカウントをリンクして、統計、ランク、コスメティックを安全に保ちます。", + "link_discord": "Discordアカウントを連携する", + "link_via_email_placeholder": "メールで連携する", + "link_button": "連携", + "log_out": "ログアウト", + "welcome_back": "おかえりなさい", + "sign_in_desc": "統計と進捗状況を保存するにはサインインしてください", + "or": "または", + "email_placeholder": "メールアドレスを入力してください", + "get_magic_link": "マジックリンクを入手", + "linked_account": "{account_name} としてログインしました", "fetching_account": "アカウント情報を取得中...", - "logged_in_with_discord": "Discordでログインしました", - "recovery_email_sent": "{email} に回復用のメールを送信しました" + "recovery_email_sent": "{email} に回復用のメールを送信しました", + "not_found": "見つかりません", + "clear_session": "セッションをクリア", + "failed_to_send_recovery_email": "再設定メールを送信できませんでした", + "enter_email_address": "メールアドレスを入力してください" }, "stats_modal": { "title": "ステータス", @@ -167,11 +204,40 @@ "loading": "ロード中…", "error": "クランステータスの読み込みに失敗しました", "no_stats": "クランステータスがありません", + "no_data_yet": "まだデータはありません", "clan": "クラン", "games": "ゲーム", "win_score": "勝利スコア", + "win_score_tooltip": "クランの参加と試合の難易度に基づいて重み付けされた勝利", "loss_score": "敗北スコア", - "win_loss_ratio": "勝利/敗北" + "loss_score_tooltip": "クランの参加と試合の難易度に基づいて重み付けされた敗北", + "win_loss_ratio": "勝利/敗北", + "ratio": "比率", + "rank": "ランク", + "try_again": "もう一度やり直してください" + }, + "game_info_modal": { + "title": "ゲームの詳細", + "players": "プレイヤー", + "atoms": "原子爆弾", + "hydros": "水素爆弾", + "mirv": "MIRV", + "bombs": "爆弾", + "total_gold": "合計", + "all_gold": "合計資金", + "trade": "貿易", + "conquest_gold": "征服したプレイヤーの資金数", + "stolen_gold": "戦艦で盗んだ資金", + "num_of_conquests": "征服したプレイヤーの数", + "duration": "間隔", + "survival_time": "生存時間", + "war": "戦争", + "economy": "経済", + "conquests": "征服", + "pirate": "海賊", + "conquered": "征服された", + "loading_game_info": "ゲームの統計を読み込んでいます", + "no_winner": "この試合の勝者はいなかった" }, "map": { "map": "地図", @@ -186,6 +252,7 @@ "asia": "アジア", "mars": "火星", "southamerica": "南アメリカ", + "britanniaclassic": "ブリタニア(クラシック)", "britannia": "ブリタニア", "gatewaytotheatlantic": "西ヨーロッパ", "australia": "オーストラリア", @@ -196,7 +263,7 @@ "betweentwoseas": "2つの海の間", "faroeislands": "フェロー諸島", "deglaciatedantarctica": "退氷した南極大陸", - "europeclassic": "ヨーロッパ (クラシック)", + "europeclassic": "ヨーロッパ(クラシック)", "falklandislands": "フォークランド諸島", "baikal": "バイカル湖付近", "halkidiki": "ハルキディキ半島", @@ -206,19 +273,33 @@ "yenisei": "エニセイ川", "pluto": "冥王星", "montreal": "モントリオール", + "newyorkcity": "ニューヨーク市", "achiran": "アチラン", "baikalnukewars": "バイカル(核戦争)", "fourislands": "4つの島", "gulfofstlawrence": "セントローレンス湾", - "lisbon": "リスボンの都市圏" + "lisbon": "リスボンの都市圏", + "svalmel": "スヴァルメル", + "manicouagan": "マニクアガン湖", + "lemnos": "レムノス島", + "sierpinski": "シェルピンスキー", + "twolakes": "二つの湖", + "straitofhormuz": "ホルムズ海峡", + "surrounded": "囲まれた島", + "didier": "ディディエ", + "didierfrance": "ディディエ(フランス)", + "amazonriver": "アマゾン川" }, "map_categories": { "continental": "大陸", "regional": "地域", - "fantasy": "その他" + "fantasy": "その他", + "special": "特殊マップ", + "arcade": "お楽しみマップ" }, "map_component": { - "loading": "読み込み中…" + "loading": "読み込み中…", + "error": "エラー" }, "private_lobby": { "title": "ランダム", @@ -229,42 +310,55 @@ "checking": "ロビーを確認中...", "not_found": "ロビーが見つかりません。IDを確認してもう一度お試しください。", "error": "エラーが発生しました。もう一度試すか、サポートにお問い合わせください。", - "joined_waiting": "参加に成功しました!ゲーム開始をお待ちください...", - "version_mismatch": "このゲームは別のバージョンで作成されました。参加できません。" + "joined_waiting": "ロビーに参加しました!ホストの開始を待っています…", + "version_mismatch": "このゲームは別のバージョンで作成されました。参加できません。", + "disabled_units": "無効ユニット" }, "public_lobby": { "join": "次のゲームに参加", "waiting": "人が参加しています...", - "teams_Duos": "2 人プレイヤー(ドゥオ)", - "teams_Trios": "3 人プレイヤー(トリオ)", - "teams_Quads": "4 人プレイヤー(クワッド)", - "teams_hvn": "プレイヤー対国家", + "teams_Duos": "{team_count}個の2人1組のチーム(デュオ)", + "teams_Trios": "{team_count}個の3人1組のチーム(トリオ)", + "teams_Quads": "{team_count}個の4人1組のチーム(クワッド)", + "waiting_for_players": "プレイヤーを待っています", + "starting_game": "ゲームを開始します...", + "teams_hvn": "人類 vs 国家", + "teams_hvn_detailed": "{num} 人類 vs {num} 国家", "teams": "{num}チーム", - "players_per_team": "{num}人プレイヤー" + "players_per_team": "{num}人プレイヤー", + "started": "開始しています" }, "matchmaking_modal": { - "title": "マッチングする", + "title": "1v1ランクマッチを作成 (アルファ版) ", "connecting": "サーバーに接続中…", "searching": "対戦相手を検索中…", - "waiting_for_game": "ゲーム開始を待っています…" + "waiting_for_game": "ゲーム開始を待っています…", + "elo": "あなたのELO: {elo}" }, "username": { "enter_username": "ユーザー名を入力", "not_string": "ユーザー名は文字列で入力してください。", "too_short": "ユーザー名は{min}文字より長い必要があります。", "too_long": "ユーザー名は{max}文字より短い必要があります。", - "invalid_chars": "ユーザー名には英字、数字、スペース、アンダースコア、および [角括弧] のみ使用できます。" + "invalid_chars": "ユーザー名には、文字、数字、スペース、アンダーバーのみを含めることができます。", + "tag": "タグ", + "tag_too_short": "クランタグは2〜5文字の英数字でなければなりません。", + "tag_invalid_chars": "クランタグには文字と数字のみを含めることができます。" }, "host_modal": { - "title": "プライベートロビー", + "title": "プライベートロビーを作成", + "label": "プライベート", "mode": "モード", "team_count": "チームの数", + "team_type": "チームタイプ", "options_title": "オプション", "bots": "ボット数: ", "bots_disabled": "無効", + "player_immunity_duration": "PVPの無敵時間(分)", "nations": "諸国: ", "disable_nations": "国家を無効化", "max_timer": "ゲーム時間 (分)", + "mins_placeholder": "分", "instant_build": "即時建築", "infinite_gold": "資金無限", "donate_gold": "資金援助", @@ -283,7 +377,11 @@ "assigned_teams": "チーム編成", "empty_teams": "空きのチーム", "empty_team": "空き", - "remove_player": "{username}を削除" + "remove_player": "{username}を削除", + "teams_Duos": "デュオ(2人1組)", + "teams_Trios": "トリオ(3人1組)", + "teams_Quads": "クワッド(4人1組)", + "teams_Humans Vs Nations": "人類 vs 国家" }, "team_colors": { "red": "赤", @@ -301,16 +399,20 @@ "code_license": "本ゲームのコードは AGPL-3.0 ライセンスに基づき公開されています(無保証)" }, "difficulty": { - "difficulty": "難易度", - "Easy": "簡単", - "Medium": "普通", - "Hard": "難しい", - "Impossible": "不可能" + "difficulty": "国家の難易度", + "easy": "簡単", + "medium": "普通", + "hard": "難しい", + "impossible": "不可能" }, "game_mode": { "ffa": "バトルロワイヤル", "teams": "チーム" }, + "public_game_modifier": { + "random_spawn": "ランダムスポーン", + "compact_map": "コンパクトマップ" + }, "select_lang": { "title": "言語を選択" }, @@ -335,21 +437,23 @@ "emojis_label": "絵文字を表示", "emojis_desc": "ゲーム中で絵文字を表示します", "alert_frame_label": "アラートフレーム", - "alert_frame_desc": "警告フレームの表示をを切り替えます。有効時、裏切られたときや陸上から攻撃を受けたときにフレームが表示されます。", + "alert_frame_desc": "警告フレームの表示を切り替えます。有効時、裏切られたときや陸上から攻撃を受けたときにフレームが表示されます。", "special_effects_label": "特殊効果", "special_effects_desc": "特殊効果を切り替えます。無効にするとパフォーマンスが向上します。", "structure_sprites_label": "建物アイコン", "structure_sprites_desc": "建物アイコンの表示切替", + "cursor_cost_label_label": "カーソルの下に表示される建設コスト", + "cursor_cost_label_desc": "建物を建てる際にカーソルの下に必要資金を表示する", "anonymous_names_label": "ユーザー名を匿名にする", "anonymous_names_desc": "自分の画面では他のプレイヤーのユーザー名を非表示にし、代わりに別の名前で表示します。", "lobby_id_visibility_label": "ロビーIDを非表示", "lobby_id_visibility_desc": "プライベートロビー作成時にロビーIDを隠す", + "toggle_visibility": "表示を切り替え", "left_click_label": "左クリックでメニューを開く", "left_click_desc": "オンにすると左クリックでメニューを開くことができ、剣ボタンで攻撃します。オフにすると左クリックでそのまま攻撃します。", "left_click_menu": "左クリックでメニューを開く", "attack_ratio_label": "⚔️ 出撃兵力の比率", "attack_ratio_desc": "初期時点で出撃する兵力の割合を設定します(1–100%)", - "troop_ratio_desc": "初期時点で兵士と金を生産する労働者の割合を設定します(1–100%)", "territory_patterns_label": "🏳️ 領土の模様", "territory_patterns_desc": "ゲーム内で領土の模様を表示するかどうか", "performance_overlay_label": "パフォーマンスオーバーレイ", @@ -358,6 +462,7 @@ "easter_writing_speed_desc": "コードを書く速さを調節する(x1-x100)", "easter_bug_count_label": "バグの個数", "easter_bug_count_desc": "どのぐらいの個数のバグを許容できるか(0–1000個)", + "press_a_key": "キーを押す", "view_options": "表示オプション", "toggle_view": "表示切り替え", "toggle_view_desc": "国境を非表示にし、地形だけが見れます", @@ -416,7 +521,8 @@ "exit_game_label": "ゲームから退出する", "exit_game_info": "メインメニューに戻ります", "background_music_volume": "BGM音量", - "sound_effects_volume": "効果音音量" + "sound_effects_volume": "効果音音量", + "keybind_conflict_error": "{key} キーはすでに他のアクションに使われています。" }, "chat": { "title": "クイックチャット", @@ -451,7 +557,7 @@ "mirv": "MIRVを[P1]に発射!", "focus": "[P1]に集中砲火だ!", "finish": "[P1]にとどめだ!", - "build_warships": "軍艦を建造せよ!" + "build_warships": "戦艦を建造せよ!" }, "defend": { "defend": "[P1]を守る!", @@ -513,7 +619,7 @@ "mirv": "指定したプレイヤーのみを狙う超大規模な爆発", "missile_silo": "核ミサイルの発射に使用される", "sam_launcher": "飛来する核ミサイルを迎撃する", - "warship": "貿易船を捕獲し、敵の船やボートを破壊する", + "warship": "貿易船を捕獲し、戦艦やボートを破壊する", "port": "貿易船を送って資金を獲得する", "defense_post": "近くの国境の防御を強化します", "city": "最大人口が増加します", @@ -529,6 +635,7 @@ "other_team": "{team}チームが勝利しました。", "you_won": "勝利!", "other_won": "{player}の勝利!", + "nation_won": "国家 {nation} が勝利しました!", "exit": "ゲームから退出", "keep": "観戦する", "spectate": "観戦する", @@ -537,7 +644,7 @@ "ofm_winter_description": "競技トーナメントにして、最強のプレイヤーたちに挑もう", "join_tournament": "トーナメントに参加", "join_discord": "Discordコミュニティに参加しよう!", - "discord_description": "他のプレイヤーと交流して、アップデート情報や戦略を共有しよう", + "discord_description": "プレイヤーとつながり、新しい機能を発見し、賞品を獲得しましょう!", "join_server": "サーバに入る", "youtube_tutorial": "ヘルプが必要ですか?" }, @@ -549,7 +656,7 @@ "team": "チーム", "owned": "領土", "gold": "ゴールド", - "troops": "兵士", + "maxtroops": "最大兵力", "launchers": "ランチャー", "sams": "SAM", "warships": "戦艦", @@ -565,6 +672,7 @@ "team": "チーム", "alliance_timeout": "同盟終了まで", "troops": "軍隊", + "maxtroops": "最大兵力", "a_troops": "攻撃兵士数", "gold": "資金", "ports": "港", @@ -575,7 +683,9 @@ "warships": "戦艦", "health": "体力", "attitude": "態度", - "levels": "レベル" + "levels": "レベル", + "wilderness_title": "荒野", + "irradiated_wilderness_title": "放射線に汚染された荒野" }, "events_display": { "retreating": "撤退中", @@ -653,7 +763,10 @@ "send_alliance": "同盟を要請", "send_troops": "軍隊を送信", "send_gold": "資金を送信", - "emotes": "絵文字" + "emotes": "絵文字", + "arc_up": "上向きの弧", + "arc_down": "下向きの弧", + "flip_rocket_trajectory": "ロケットの軌道を反転" }, "send_troops_modal": { "title_with_name": "{name}へ軍隊を送信", @@ -702,20 +815,26 @@ }, "heads_up_message": { "choose_spawn": "スタート地点を選んで下さい", - "random_spawn": "ランダムスポーンが有効です。開始地点を設定しています…" + "random_spawn": "ランダムスポーンが有効です。開始地点を設定しています…", + "singleplayer_game_paused": "ゲームを一時停止", + "multiplayer_game_paused": "ロビー作成者によってゲームが一時停止されました" }, "territory_patterns": { "title": "領土スキン", "colors": "色", "purchase": "購入", "show_only_owned": "自分の領地", + "all_owned": "すべてのスキンを手に入れました!新しいアイテムについては後ほどご確認ください。", + "not_logged_in": "ログインされていません", "blocked": { "login": "スキンを解放するにはログインしてください", "purchase": "スキンを解放するには購入してください" }, "pattern": { "default": "デフォルト" - } + }, + "select_skin": "スキンを選択", + "selected": "選択済" }, "flag_input": { "title": "旗を選択", @@ -786,8 +905,9 @@ "mode": "モード", "mode_ffa": "バトルロワイヤル", "mode_team": "チーム", - "view": "見る", + "replay": "リプレイ", "details": "詳細", + "ranking": "ランキング", "started": "既に開始", "map": "地図", "difficulty": "難易度", @@ -796,13 +916,20 @@ "player_stats_tree": { "public": "公開", "private": "非公開", - "singleplayer": "シングルプレイヤー", + "singleplayer": "ソロ", "mode": "モード", "stats_wins": "勝利数", "stats_losses": "敗北数", "stats_wlr": "勝敗比", "stats_games_played": "プレイ数", "mode_ffa": "デスマッチ", - "mode_team": "チーム" + "mode_team": "チーム", + "no_stats": "この選択に対する統計は記録されていません。" + }, + "matchmaking_button": { + "play_ranked": "1v1のランクマッチを作成", + "description": "(アルファ版)", + "login_required": "ランクマッチをプレイするにはログインしてください!", + "must_login": "ランクマッチをプレイするにはログインする必要があります。" } } diff --git a/resources/lang/nl.json b/resources/lang/nl.json index 746150408..d103626c0 100644 --- a/resources/lang/nl.json +++ b/resources/lang/nl.json @@ -7,6 +7,7 @@ }, "common": { "close": "Sluiten", + "back": "Terug", "available": "Beschikbaar", "preset_max": "Max", "summary_send": "Verstuur", @@ -17,7 +18,9 @@ "cap_tooltip": "Resterende capaciteit ontvanger", "target_dead": "Doelwit uitgeschakeld", "target_dead_note": "Je kunt geen middelen sturen naar een dode speler.", - "none": "Geen" + "none": "Geen", + "copied": "Gekopieerd!", + "click_to_copy": "Klik om te kopiëren" }, "main": { "title": "OpenFront (ALFA)", @@ -26,17 +29,28 @@ "checking_login": "Inlog controleren...", "logged_in": "Ingelogd!", "log_out": "Uitloggen", - "create_lobby": "Lobby aanmaken", - "join_lobby": "Lobby toetreden", - "single_player": "Eén Speler", + "create": "Lobby aanmaken", + "join": "Lobby toetreden", + "solo": "Solo-lobby", "instructions": "Instructies", + "game_info": "Spelinformatie", "wiki": "Wiki", "privacy_policy": "Privacybeleid", "terms_of_service": "Servicevoorwaarden", - "reddit": "Reddit" + "copyright": "© OpenFront™ en Bijdragers", + "reddit": "Reddit", + "play": "Spelen", + "news": "Nieuws", + "store": "Winkel", + "options": "Opties", + "keys": "Sneltoetsen", + "stats": "Statistieken", + "account": "Account", + "help": "Help", + "menu": "Menu", + "pick_pattern": "Kies een skin!" }, "news": { - "see_all_releases": "Bekijk alle releases", "github_link": "op GitHub", "title": "Release-opmerkingen" }, @@ -83,6 +97,8 @@ "radial_attack": "Het aanvalsmenu openen.", "radial_info": "Infomenu openen.", "radial_boat": "Stuur een Boot (transportschip) voor een aanval op de geselecteerde locatie. Alleen beschikbaar als je toegang hebt tot water.", + "radial_donate_troops": "Doneer troepen, gelijk aan het percentage van de ingestelde aanvalsverhouding, aan de bondgenoot waarop je het radiale menu hebt geopend.", + "radial_donate_gold": "Opent het gouddonatiemenu zodat je bondgenoten snel goud kan sturen.", "radial_close": "Het menu sluiten.", "info_title": "Infomenu", "info_enemy_desc": "Bevat informatie zoals de naam van de geselecteerde speler, goud, troepen, of ze de handel hebben stopgezet, hoeveel kernwapens ze op je hebben afgevuurd, en of de speler een verrader is. Een verrader is een speler die een bondgenoot heeft aangevallen. Handel gestopt betekent dat jullie geen goud meer van elkaar ontvangen via handelsschepen. Handmatig (als de speler op \"Stop handel\" heeft geklikt, wat duurt totdat jullie beide op \"Start handel\" hebben geklikt) of automatisch (als jij jullie bondgenootschap hebt verraden, wat 5 minuten duurt of korter als jullie weer bondgenoten worden). Verrader toont 30 seconden lang Ja als de speler een bondgenoot heeft aangevallen. De iconen hieronder staan voor de volgende interacties:", @@ -94,8 +110,8 @@ "info_ally_panel": "Infopaneel bondgenoot", "info_ally_desc": "Wanneer je een bondgenootschap sluit met een speler, worden de volgende nieuwe iconen beschikbaar:", "ally_betray": "Verraad je bondgenoot, beëindig het bondgenootschap, stop de handel, verzwak je verdediging. De handel tussen jullie wordt 5 minuten gepauzeerd (of totdat jullie weer bondgenoten worden) en anderen stoppen de handel mogelijk ook. En tenzij de andere speler zelf een verrader was, wordt je 30 seconden als verrader gemarkeerd. Gedurende deze tijd staat er een icoon boven je naam en is je verdediging 50% zwakker. Bots zullen minder snel een bondgenoten willen worden en spelers zullen zich wel tweemaal bedenken voor ze dat doen.", - "ally_donate": "Geef een deel van je troepen aan je bondgenoot. Gebruikt wanneer ze weinig troepen hebben en worden aangevallen, of wanneer ze die extra kracht nodig hebben om een ​​vijand te verpletteren.", - "ally_donate_gold": "Geef een deel van je goud aan je bondgenoot. Wanneer zij weinig goud hebben en het voor gebouwen nodig hebben, of wanneer je teamgenoot aan het sparen is voor die MIRV.", + "ally_donate": "Doneer een deel van je troepen aan je bondgenoot. Gebruikt wanneer ze weinig troepen hebben en worden aangevallen, of wanneer ze die extra kracht nodig hebben om een ​​vijand te verpletteren.", + "ally_donate_gold": "Doneer een deel van je goud aan je bondgenoot. Wanneer zij weinig goud hebben en het voor gebouwen nodig hebben, of wanneer je teamgenoot aan het sparen is voor die MIRV.", "build_menu_title": "Bouwmenu", "build_menu_desc": "Maak hier een van of bekijk hoeveel van elke je al hebt gemaakt:", "build_name": "Naam", @@ -114,7 +130,7 @@ "build_silo": "Raketsilo", "build_silo_desc": "Maakt het lanceren van raketten mogelijk.", "build_sam": "Luchtdoelraket (SAM)-lanceerder", - "build_sam_desc": "Kan vijandelijke raketten onderscheppen binnen een straal van 80 pixels of, voor MIRV-kernkoppen, 50 pixels. Raakt 100% van de atoombommen, 80% van de waterstofbommen en 50% van de individuele MIRV-kernkoppen. De SAM heeft een afkoeltijd van 7,5 seconden.", + "build_sam_desc": "Kan vijandelijke raketten onderscheppen binnen een bereik van 100 pixels. De SAM-lanceerder heeft een herstelperiode van 7,5 seconden.", "build_atom": "Atoombom", "build_atom_desc": "Kleine explosieve bom die gebied, gebouwen, schepen en boten vernietigt. Komt vanuit de dichtstbijzijnde Raketsilo en landt op de plek waar je hebt geklikt om het te bouwen.", "build_hydrogen": "Waterstofbom", @@ -129,12 +145,15 @@ "icon_embargo": "Dollar stopbord - Embargo. Deze speler heeft de handel met jou gestopt, automatisch of handmatig.", "icon_request": "Envelop - Alliantieverzoek. Deze speler stuurde je een verzoek om bondgenoten te worden.", "info_enemy_panel": "Infopaneel vijand", - "exit_confirmation": "Weet je zeker dat je dit spel wilt verlaten?" + "exit_confirmation": "Weet je zeker dat je dit spel wilt verlaten?", + "bomb_direction": "Atoom- / waterstofbom boogrichting" }, "single_modal": { - "title": "Eén speler", + "title": "Solo", "random_spawn": "Willekeurige startpositie", "allow_alliances": "Bondgenootschappen toestaan", + "toggle_achievements": "Prestaties in- of uitschakelen", + "sign_in_for_achievements": "Meld je aan voor prestaties", "options_title": "Opties", "bots": "Bots:", "bots_disabled": "Uitgeschakeld", @@ -145,6 +164,8 @@ "infinite_troops": "Oneindige troepen", "compact_map": "Compacte kaart", "max_timer": "Spellengte (minuten)", + "max_timer_placeholder": "Min.", + "max_timer_invalid": "Voer een geldige max. timertijd in (1-120 minuten)", "disable_nukes": "Kernwapens uitschakelen", "enables_title": "Onderdelen inschakelen", "start": "Start Spel" @@ -156,10 +177,26 @@ }, "account_modal": { "title": "Account", - "logged_in_as": "Ingelogd als {email}", + "connected_as": "Gekoppeld als", + "stats_overview": "Overzicht van statistieken", + "save_progress_title": "Sla je voortgang op", + "save_progress_desc": "Koppel je account om je statistieken, rang en cosmetica veilig te houden.", + "link_discord": "Discord-account koppelen", + "link_via_email_placeholder": "Koppel via e-mail", + "link_button": "Koppelen", + "log_out": "Uitloggen", + "welcome_back": "Welkom terug", + "sign_in_desc": "Log in om je statistieken en voortgang op te slaan", + "or": "OF", + "email_placeholder": "Voer je e-mailadres in", + "get_magic_link": "Krijg Magische Link", + "linked_account": "Ingelogd als {account_name}", "fetching_account": "Accountgegevens ophalen...", - "logged_in_with_discord": "Ingelogd met Discord", - "recovery_email_sent": "Herstelmail verzonden naar {email}" + "recovery_email_sent": "Herstelmail verzonden naar {email}", + "not_found": "Niet Gevonden", + "clear_session": "Sessie Wissen", + "failed_to_send_recovery_email": "Verzenden herstel e-mail mislukt", + "enter_email_address": "Voer een e-mailadres in alsjeblieft" }, "stats_modal": { "title": "Statistieken", @@ -167,11 +204,40 @@ "loading": "Laden...", "error": "Fout bij het laden van clan statistieken", "no_stats": "Er zijn geen clan statistieken beschikbaar", + "no_data_yet": "Nog geen gegevens", "clan": "Clan", "games": "Spellen", "win_score": "Win Score", + "win_score_tooltip": "Gewogen aantal overwinningen op basis van clandeelname en matchmoeilijkheidsgraad", "loss_score": "Verlies Score", - "win_loss_ratio": "Gewonnen/Verloren" + "loss_score_tooltip": "Gewogen aantal verliezen op basis van clandeelname en matchmoeilijkheidsgraad", + "win_loss_ratio": "Gewonnen/Verloren", + "ratio": "Verhouding", + "rank": "Rang", + "try_again": "Opnieuw Proberen" + }, + "game_info_modal": { + "title": "Spelinformatie", + "players": "Spelers", + "atoms": "Atoombom", + "hydros": "Waterstofbom", + "mirv": "MIRV", + "bombs": "Bommen", + "total_gold": "Totaal", + "all_gold": "Alle goud", + "trade": "Handel", + "conquest_gold": "Veroverd spelersgoud", + "stolen_gold": "Gestolen met oorlogsschepen", + "num_of_conquests": "Aantal veroverde spelers", + "duration": "Tijdsduur", + "survival_time": "Overlevingstijd", + "war": "Oorlog", + "economy": "Economie", + "conquests": "Veroveringen", + "pirate": "Kapen", + "conquered": "Veroverd", + "loading_game_info": "Spelstatistieken worden geladen", + "no_winner": "Dit spel eindigde zonder winnaar" }, "map": { "map": "Kaart", @@ -186,6 +252,7 @@ "asia": "Azië", "mars": "Mars", "southamerica": "Zuid-Amerika", + "britanniaclassic": "Britannia (Klassiek)", "britannia": "Groot-Brittanië", "gatewaytotheatlantic": "Poort van de Atlantische Oceaan", "australia": "Australië", @@ -196,7 +263,7 @@ "betweentwoseas": "Tussen twee zeeën", "faroeislands": "Faeröer eilanden", "deglaciatedantarctica": "Ontdooid Antarctica", - "europeclassic": "Europa (klassiek)", + "europeclassic": "Europa (Klassiek)", "falklandislands": "Falklandeilanden", "baikal": "Baikalmeer", "halkidiki": "Chalkidiki", @@ -206,19 +273,33 @@ "yenisei": "Jenisej", "pluto": "Pluto", "montreal": "Montreal", + "newyorkcity": "New York City", "achiran": "Achiran", "baikalnukewars": "Baikal (Kernoorlog)", "fourislands": "Vier Eilanden", "gulfofstlawrence": "Saint Lawrencebaai", - "lisbon": "Lissabon" + "lisbon": "Lissabon", + "svalmel": "Svalmel", + "manicouagan": "Manicouagan", + "lemnos": "Limnos", + "sierpinski": "Sierpinski", + "twolakes": "Twee Meren", + "straitofhormuz": "Straat van Hormuz", + "surrounded": "Omringd", + "didier": "Didier", + "didierfrance": "Didier (Frankrijk)", + "amazonriver": "Amazonerivier" }, "map_categories": { "continental": "Continent", "regional": "Regio", - "fantasy": "Overig" + "fantasy": "Overig", + "special": "Speciaal", + "arcade": "Arcade" }, "map_component": { - "loading": "Laden..." + "loading": "Laden...", + "error": "Fout" }, "private_lobby": { "title": "Privélobby toetreden", @@ -229,47 +310,60 @@ "checking": "Lobby controleren...", "not_found": "Lobby niet gevonden. Controleer het ID en probeer het opnieuw.", "error": "Er is een fout opgetreden. Probeer het opnieuw of neem contact op met support.", - "joined_waiting": "Succesvol toegetreden! Wachten tot het spel begint...", - "version_mismatch": "Dit spel is aangemaakt met een andere versie. Kan niet afspelen of deelnemen." + "joined_waiting": "Toegetreden tot lobby! Wachten op host om te starten...", + "version_mismatch": "Dit spel is aangemaakt met een andere versie. Kan niet afspelen of deelnemen.", + "disabled_units": "Uitgeschakelde Eenheden" }, "public_lobby": { "join": "Deelnemen aan volgende Spel", "waiting": "spelers wachten", - "teams_Duos": "van 2 (Duo's)", - "teams_Trios": "van 3 (Trio's)", - "teams_Quads": "van 4 (Viertallen)", + "teams_Duos": "{team_count} Teams van 2 (Duo's)", + "teams_Trios": "{team_count} Teams van 3 (Trio's)", + "teams_Quads": "{team_count} Teams van 4 (Viertallen)", + "waiting_for_players": "Wachten op spelers", + "starting_game": "Spel starten…", "teams_hvn": "Mensen vs Naties", + "teams_hvn_detailed": "{num} Mensen vs {num} Naties", "teams": "{num} Teams", - "players_per_team": "van {num}" + "players_per_team": "van {num}", + "started": "Begonnen" }, "matchmaking_modal": { - "title": "Matchmaking", + "title": "1v1 Competitieve Matchmaking (ALFA)", "connecting": "Verbinden met matchmakingserver...", "searching": "Zoeken naar een spel...", - "waiting_for_game": "Wachten tot het spel begint..." + "waiting_for_game": "Wachten tot het spel begint...", + "elo": "Jouw ELO: {elo}" }, "username": { "enter_username": "Voer je gebruikersnaam in", "not_string": "Gebruikersnaam moet een tekenreeks zijn.", "too_short": "Gebruikersnaam moet minstens {min} tekens lang zijn.", "too_long": "Gebruikersnaam mag niet langer zijn dan {max} tekens.", - "invalid_chars": "Gebruikersnaam mag alleen letters, cijfers, spaties, underscores en [vierkante haakjes] bevatten." + "invalid_chars": "Gebruikersnaam kan alleen letters, cijfers, spaties en underscores bevatten.", + "tag": "TAG", + "tag_too_short": "Clantag moet 2-5 alfanumerieke tekens zijn.", + "tag_invalid_chars": "Clantag kan alleen letters en cijfers bevatten." }, "host_modal": { - "title": "Privélobby", + "title": "Privélobby Aanmaken", + "label": "Privé", "mode": "Modus", "team_count": "Aantal teams", + "team_type": "Teamtype", "options_title": "Opties", "bots": "Bots:", "bots_disabled": "Uitgeschakeld", + "player_immunity_duration": "PVP-immuniteitsduur (minuten)", "nations": "Naties: ", "disable_nations": "Naties uitschakelen", "max_timer": "Spellengte (minuten)", + "mins_placeholder": "Min.", "instant_build": "Bouwwachttijd uitschakelen", "infinite_gold": "Oneindig goud", - "donate_gold": "Goud geven", + "donate_gold": "Goud doneren", "infinite_troops": "Oneindige troepen", - "donate_troops": "Troepen geven", + "donate_troops": "Troepen doneren", "compact_map": "Compacte kaart", "enables_title": "Onderdelen inschakelen", "player": "Speler", @@ -283,7 +377,11 @@ "assigned_teams": "Toegewezen Teams", "empty_teams": "Lege Teams", "empty_team": "Leeg", - "remove_player": "Verwijder {username}" + "remove_player": "Verwijder {username}", + "teams_Duos": "Duo's (teams van 2)", + "teams_Trios": "Trio's (teams van 3)", + "teams_Quads": "Viertallen (teams van 4)", + "teams_Humans Vs Nations": "Mensen vs Naties" }, "team_colors": { "red": "Rood", @@ -301,16 +399,20 @@ "code_license": "Code gelicenseerd onder AGPL-3.0 (geen garantie)" }, "difficulty": { - "difficulty": "Moeilijkheidsgraad", - "Easy": "Ontspannen", - "Medium": "Gebalanceerd", - "Hard": "Intens", - "Impossible": "Onmogelijk" + "difficulty": "Natie moeilijkheidsgraad", + "easy": "Makkelijk", + "medium": "Gemiddeld", + "hard": "Moeilijk", + "impossible": "Onmogelijk" }, "game_mode": { "ffa": "Iedereen tegen elkaar (FFA)", "teams": "Teams" }, + "public_game_modifier": { + "random_spawn": "Willekeurige Startpositie", + "compact_map": "Compacte Kaart" + }, "select_lang": { "title": "Kies taal" }, @@ -340,16 +442,18 @@ "special_effects_desc": "Visuele effecten aanzetten. Zet uit om de prestaties van het spel te verbeteren", "structure_sprites_label": "Gebouw afbeeldingen", "structure_sprites_desc": "3D-afbeeldingen gebouwen in-/uitschakelen", + "cursor_cost_label_label": "Cursor Bouwkosten", + "cursor_cost_label_desc": "Toon kosten onder de bouwcursor", "anonymous_names_label": "Verborgen Namen", "anonymous_names_desc": "Vervang echte spelersnamen door willekeurige namen op je scherm.", "lobby_id_visibility_label": "Verborgen Lobby-ID's", "lobby_id_visibility_desc": "Verberg Lobby-ID tijdens het maken van een privélobby", + "toggle_visibility": "Zichtbaar/onzichtbaar", "left_click_label": "Linkermuisknop voor openen menu", "left_click_desc": "Als AAN: linkermuisknop opent het Radiale menu met zwaard-aanvalsknop. Als UIT: linkermuisknop opent direct de aanval.", "left_click_menu": "Linkermuisknop Radiale Menu", "attack_ratio_label": "⚔️ Aanvalsverhouding", "attack_ratio_desc": "Welk percentage van je troepen je bij een aanval stuurt (1-100%)", - "troop_ratio_desc": "De balans tussen troepen (voor gevechten) en werkers (voor goudproductie) aanpassen (1-100%)", "territory_patterns_label": "🏳️ Skins voor gebieden", "territory_patterns_desc": "Kies of je skins op gebieden wilt weergeven in het spel", "performance_overlay_label": "Prestatie-overlay", @@ -358,6 +462,7 @@ "easter_writing_speed_desc": "Pas aan hoe snel je pretendeert te programmeren (x1-x100)", "easter_bug_count_label": "Aantal bugs", "easter_bug_count_desc": "Hoeveel bugs je oké vindt (0-1000, gevoelsmatig)", + "press_a_key": "Druk op een toets", "view_options": "Weergave-opties", "toggle_view": "Weergave wisselen", "toggle_view_desc": "Weergave wisselen (terrein/landen)", @@ -416,7 +521,8 @@ "exit_game_label": "Spel Verlaten", "exit_game_info": "Terug naar hoofdmenu", "background_music_volume": "Volume achtergrondmuziek", - "sound_effects_volume": "Volume geluidseffecten" + "sound_effects_volume": "Volume geluidseffecten", + "keybind_conflict_error": "De toets {key} is al verbonden aan een andere actie." }, "chat": { "title": "Snelchat", @@ -529,6 +635,7 @@ "other_team": "{team} team heeft gewonnen!", "you_won": "Je hebt gewonnen!", "other_won": "{player} heeft gewonnen!", + "nation_won": "Natie {nation} heeft gewonnen!", "exit": "Verlaat spel", "keep": "Blijf spelen", "spectate": "Toekijken", @@ -537,7 +644,7 @@ "ofm_winter_description": "Doe mee met het competitieve toernooi en concurreer met de beste spelers", "join_tournament": "Toernooi toetreden", "join_discord": "Word lid van onze Discord-gemeenschap!", - "discord_description": "Leg contact met andere spelers, krijg updates en deel strategieën", + "discord_description": "Maak contact met spelers, ontdek nieuwe functies en win prijzen!", "join_server": "Word lid van server", "youtube_tutorial": "Wat hulp nodig?" }, @@ -549,7 +656,7 @@ "team": "Team", "owned": "Bezit", "gold": "Goud", - "troops": "Troepen", + "maxtroops": "Max. troepen", "launchers": "Raketsilo's", "sams": "SAM-lanceerders", "warships": "Oorlogsschepen", @@ -565,6 +672,7 @@ "team": "Team", "alliance_timeout": "Alliantie eindigt over", "troops": "Troepen", + "maxtroops": "Max. troepen", "a_troops": "Aanvallende troepen", "gold": "Goud", "ports": "Havens", @@ -575,7 +683,9 @@ "warships": "Oorlogsschepen", "health": "Gezondheid", "attitude": "Houding", - "levels": "Levels" + "levels": "Levels", + "wilderness_title": "Wildernis", + "irradiated_wilderness_title": "Bestraalde Wildernis" }, "events_display": { "retreating": "trekken zich terug", @@ -653,7 +763,10 @@ "send_alliance": "Stuur Alliantieverzoek", "send_troops": "Geef Troepen", "send_gold": "Geef Goud", - "emotes": "Emoji's" + "emotes": "Emoji's", + "arc_up": "Opwaartse boog", + "arc_down": "Neerwaartse boog", + "flip_rocket_trajectory": "Rakettraject spiegelen" }, "send_troops_modal": { "title_with_name": "Stuur Troepen naar {name}", @@ -702,20 +815,26 @@ }, "heads_up_message": { "choose_spawn": "Kies een startlocatie", - "random_spawn": "Willekeurige startpositie is ingeschakeld. Positie wordt voor je gekozen..." + "random_spawn": "Willekeurige startpositie is ingeschakeld. Positie wordt voor je gekozen...", + "singleplayer_game_paused": "Spel gepauzeerd", + "multiplayer_game_paused": "Spel gepauzeerd door Lobby-maker" }, "territory_patterns": { "title": "Skins ", "colors": "Kleuren", "purchase": "Kopen", "show_only_owned": "Mijn Skins", + "all_owned": "Je bezit alle skins! Kom later terug voor nieuwe items.", + "not_logged_in": "Niet ingelogd", "blocked": { "login": "Je moet ingelogd zijn voor toegang tot deze skin.", "purchase": "Koop deze skin om te ontgrendelen." }, "pattern": { "default": "Standaard" - } + }, + "select_skin": "Kies Skin", + "selected": "geselecteerd" }, "flag_input": { "title": "Selecteer Vlag", @@ -786,8 +905,9 @@ "mode": "Modus", "mode_ffa": "Iedereen tegen elkaar", "mode_team": "Team", - "view": "Weergeven", + "replay": "Herhaling", "details": "Details", + "ranking": "Rang", "started": "Begonnen", "map": "Kaart", "difficulty": "Moeilijkheidsgraad", @@ -796,13 +916,20 @@ "player_stats_tree": { "public": "Openbaar", "private": "Privé", - "singleplayer": "Eén Speler", + "singleplayer": "Solo", "mode": "Modus", "stats_wins": "Overwinningen", "stats_losses": "Nederlagen", "stats_wlr": "Winst:verliesverhouding", "stats_games_played": "Gespeelde spellen", "mode_ffa": "Iedereen tegen elkaar", - "mode_team": "Team" + "mode_team": "Team", + "no_stats": "Geen statistieken vastgelegd voor deze selectie." + }, + "matchmaking_button": { + "play_ranked": "1v1 Competitieve Matchmaking", + "description": "(ALFA)", + "login_required": "Log in om competitief te spelen!", + "must_login": "Je moet ingelogd zijn om competitieve matchmaking te spelen." } } From 0466eeac134c70a3dc4ef0ef28b4c991f7682d0b Mon Sep 17 00:00:00 2001 From: DevelopingTom Date: Thu, 15 Jan 2026 21:24:35 +0100 Subject: [PATCH 2/7] Add train gold to game info ranking (#2901) ## Description: The game info panel was missing the gold generated with trains, which was recently added into the recorded stats. This PR adds the gold train ranking, grouped with the naval trade. Visually the game info panel is not matching the new visual identity, but this PR only focuses on the missing data. image ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: IngloriousTom --- .../baseComponents/ranking/GameInfoRanking.ts | 12 +++- .../baseComponents/ranking/PlayerRow.ts | 68 +++++++++++++++---- .../baseComponents/ranking/RankingControls.ts | 11 ++- .../baseComponents/ranking/RankingHeader.ts | 23 ++++--- tests/GameInfoRanking.test.ts | 15 ++-- 5 files changed, 100 insertions(+), 29 deletions(-) diff --git a/src/client/components/baseComponents/ranking/GameInfoRanking.ts b/src/client/components/baseComponents/ranking/GameInfoRanking.ts index 015d6ae40..fa78d36f0 100644 --- a/src/client/components/baseComponents/ranking/GameInfoRanking.ts +++ b/src/client/components/baseComponents/ranking/GameInfoRanking.ts @@ -2,6 +2,8 @@ import { AnalyticsRecord, PlayerRecord } from "../../../../core/Schemas"; import { GOLD_INDEX_STEAL, GOLD_INDEX_TRADE, + GOLD_INDEX_TRAIN_OTHER, + GOLD_INDEX_TRAIN_SELF, GOLD_INDEX_WAR, } from "../../../../core/StatsSchemas"; @@ -12,7 +14,8 @@ export enum RankType { MIRV = "MIRV", TotalGold = "TotalGold", StolenGold = "StolenGold", - TradedGold = "TradedGold", + NavalTrade = "NavalTrade", + TrainTrade = "TrainTrade", ConqueredGold = "ConqueredGold", Lifetime = "Lifetime", } @@ -134,10 +137,15 @@ export class Ranking { return Number(player.gold.reduce((sum, gold) => sum + gold, 0n)); case RankType.StolenGold: return Number(player.gold[GOLD_INDEX_STEAL] ?? 0n); - case RankType.TradedGold: + case RankType.NavalTrade: return Number(player.gold[GOLD_INDEX_TRADE] ?? 0n); case RankType.ConqueredGold: return Number(player.gold[GOLD_INDEX_WAR] ?? 0n); + case RankType.TrainTrade: { + const ownTrains = player.gold[GOLD_INDEX_TRAIN_SELF] ?? 0n; + const otherTrains = player.gold[GOLD_INDEX_TRAIN_OTHER] ?? 0n; + return Number(ownTrains + otherTrains); + } } } diff --git a/src/client/components/baseComponents/ranking/PlayerRow.ts b/src/client/components/baseComponents/ranking/PlayerRow.ts index 2ebe635ed..9989962a9 100644 --- a/src/client/components/baseComponents/ranking/PlayerRow.ts +++ b/src/client/components/baseComponents/ranking/PlayerRow.ts @@ -1,5 +1,10 @@ import { LitElement, html } from "lit"; import { customElement, property } from "lit/decorators.js"; +import { + GOLD_INDEX_TRADE, + GOLD_INDEX_TRAIN_OTHER, + GOLD_INDEX_TRAIN_SELF, +} from "src/core/StatsSchemas"; import { renderNumber } from "../../../Utils"; import { PlayerInfo, RankType } from "./GameInfoRanking"; @@ -67,10 +72,12 @@ export class PlayerRow extends LitElement { case RankType.MIRV: return this.renderBombScore(); case RankType.TotalGold: - case RankType.TradedGold: case RankType.ConqueredGold: case RankType.StolenGold: return this.renderGoldScore(); + case RankType.NavalTrade: + case RankType.TrainTrade: + return this.renderTradeScore(); default: return html``; } @@ -109,14 +116,15 @@ export class PlayerRow extends LitElement { `; } - private renderBombType(value: number, highlight: boolean) { + + private renderMultiScoreType(value: number, highlight: boolean) { return html`
- ${value} + ${renderNumber(value)}
`; } @@ -124,17 +132,17 @@ export class PlayerRow extends LitElement { private renderAllBombs() { return html`
- ${this.renderBombType( + ${this.renderMultiScoreType( this.player.atoms, this.rankType === RankType.Atoms, )} / - ${this.renderBombType( + ${this.renderMultiScoreType( this.player.hydros, this.rankType === RankType.Hydros, )} / - ${this.renderBombType( + ${this.renderMultiScoreType( this.player.mirv, this.rankType === RankType.MIRV, )} @@ -142,9 +150,28 @@ export class PlayerRow extends LitElement { `; } + private renderAllTrades() { + const navalTrade = this.player.gold[GOLD_INDEX_TRADE] ?? 0n; + const ownTrainTrade = this.player.gold[GOLD_INDEX_TRAIN_SELF] ?? 0n; + const otherTrainTrade = this.player.gold[GOLD_INDEX_TRAIN_OTHER] ?? 0n; + return html` +
+ ${this.renderMultiScoreType( + Number(navalTrade), + this.rankType === RankType.NavalTrade, + )} + / + ${this.renderMultiScoreType( + Number(ownTrainTrade + otherTrainTrade), + this.rankType === RankType.TrainTrade, + )} +
+ `; + } + private renderBombScore() { return html` -
+
${this.renderPlayerIcon()}
${this.renderPlayerName()} ${this.renderAllBombs()} @@ -157,13 +184,12 @@ export class PlayerRow extends LitElement { return html`
${this.renderPlayerIcon()} -
- ${this.renderPlayerName()} -
+
${this.renderPlayerName()}
+
${renderNumber(this.score)}
@@ -172,6 +198,24 @@ export class PlayerRow extends LitElement { `; } + private renderTradeScore() { + return html` +
+ ${this.renderPlayerIcon()} +
${this.renderPlayerName()}
+
+ +
+
+ ${this.renderAllTrades()} +
+ +
+ `; + } + private renderPlayerName() { return html`
diff --git a/src/client/components/baseComponents/ranking/RankingControls.ts b/src/client/components/baseComponents/ranking/RankingControls.ts index 59e3ea76c..25321d8aa 100644 --- a/src/client/components/baseComponents/ranking/RankingControls.ts +++ b/src/client/components/baseComponents/ranking/RankingControls.ts @@ -7,8 +7,10 @@ const economyRankings = new Set([ RankType.TotalGold, RankType.StolenGold, RankType.ConqueredGold, - RankType.TradedGold, + RankType.NavalTrade, + RankType.TrainTrade, ]); +const tradeRankings = new Set([RankType.NavalTrade, RankType.TrainTrade]); const bombRankings = new Set([RankType.Atoms, RankType.Hydros, RankType.MIRV]); const warRankings = new Set([ RankType.Conquests, @@ -18,6 +20,7 @@ const warRankings = new Set([ ]); const isEconomyRanking = (t: RankType) => economyRankings.has(t); +const isTradeRanking = (t: RankType) => tradeRankings.has(t); const isBombRanking = (t: RankType) => bombRankings.has(t); const isWarRanking = (t: RankType) => warRankings.has(t); @@ -87,7 +90,6 @@ export class RankingControls extends LitElement { if (!isEconomyRanking(this.rankType)) return ""; const econButtons = [ - [RankType.TradedGold, "game_info_modal.trade"], [RankType.StolenGold, "game_info_modal.pirate"], [RankType.ConqueredGold, "game_info_modal.conquered"], [RankType.TotalGold, "game_info_modal.total_gold"], @@ -95,6 +97,11 @@ export class RankingControls extends LitElement { return html`
+ ${this.renderSubButton( + RankType.NavalTrade, + isTradeRanking(this.rankType), + "game_info_modal.trade", + )} ${econButtons.map(([type, label]) => this.renderSubButton(type as RankType, this.rankType === type, label), )} diff --git a/src/client/components/baseComponents/ranking/RankingHeader.ts b/src/client/components/baseComponents/ranking/RankingHeader.ts index 881de1101..6869b9526 100644 --- a/src/client/components/baseComponents/ranking/RankingHeader.ts +++ b/src/client/components/baseComponents/ranking/RankingHeader.ts @@ -36,17 +36,17 @@ export class RankingHeader extends LitElement { case RankType.MIRV: return html`
- ${this.renderBombHeaderButton( + ${this.renderMultipleChoiceHeaderButton( translateText("game_info_modal.atoms"), RankType.Atoms, )} / - ${this.renderBombHeaderButton( + ${this.renderMultipleChoiceHeaderButton( translateText("game_info_modal.hydros"), RankType.Hydros, )} / - ${this.renderBombHeaderButton( + ${this.renderMultipleChoiceHeaderButton( translateText("game_info_modal.mirv"), RankType.MIRV, )} @@ -56,10 +56,15 @@ export class RankingHeader extends LitElement { return html`
${translateText("game_info_modal.all_gold")}
`; - case RankType.TradedGold: - return html`
- ${translateText("game_info_modal.trade")} -
`; + case RankType.NavalTrade: + case RankType.TrainTrade: + return html` +
+ ${this.renderMultipleChoiceHeaderButton("🚂", RankType.TrainTrade)} + / + ${this.renderMultipleChoiceHeaderButton("🚢", RankType.NavalTrade)} +
+ `; case RankType.ConqueredGold: return html`
${translateText("game_info_modal.conquest_gold")} @@ -74,13 +79,13 @@ export class RankingHeader extends LitElement { } } - private renderBombHeaderButton(label: string, type: RankType) { + private renderMultipleChoiceHeaderButton(label: string, type: RankType) { return html` diff --git a/tests/GameInfoRanking.test.ts b/tests/GameInfoRanking.test.ts index 7955fb807..1e523ae93 100644 --- a/tests/GameInfoRanking.test.ts +++ b/tests/GameInfoRanking.test.ts @@ -13,6 +13,8 @@ import { AnalyticsRecord } from "../src/core/Schemas"; import { GOLD_INDEX_STEAL, GOLD_INDEX_TRADE, + GOLD_INDEX_TRAIN_OTHER, + GOLD_INDEX_TRAIN_SELF, GOLD_INDEX_WAR, } from "../src/core/StatsSchemas"; @@ -55,7 +57,7 @@ describe("Ranking class", () => { stats: { units: { port: [2n, 0n, 0n, 2n] }, conquests: 5n, - gold: [0n, 100n, 20n, 0n], // total 120 + gold: [0n, 100n, 20n, 0n, 15n, 5n], // total 140 bombs: { abomb: [1n], hbomb: [1n], @@ -70,7 +72,7 @@ describe("Ranking class", () => { stats: { units: { city: [2n, 0n, 0n, 2n] }, conquests: 8n, - gold: [0n, 50n, 10n, 5n], // total 65 + gold: [0n, 50n, 10n, 5n], // total 65, no train trade bombs: { abomb: [0n], hbomb: [2n], @@ -86,7 +88,7 @@ describe("Ranking class", () => { // no units, but has conquests/killedAt to count as played conquests: 8n, killedAt: BigInt(600), - gold: [0n, 10n, 2n, 10n], // total 22 + gold: [0n, 10n, 2n, 10n, 0n, 5n], // total 27 bombs: {}, }, persistentID: null, @@ -178,9 +180,14 @@ describe("Ranking class", () => { expect(r.score(p1, RankType.StolenGold)).toBe( Number(p1.gold[GOLD_INDEX_STEAL] ?? 0n), ); - expect(r.score(p1, RankType.TradedGold)).toBe( + expect(r.score(p1, RankType.NavalTrade)).toBe( Number(p1.gold[GOLD_INDEX_TRADE] ?? 0n), ); + const ownTrain = p1.gold[GOLD_INDEX_TRAIN_SELF] ?? 0n; + const otherTrain = p1.gold[GOLD_INDEX_TRAIN_OTHER] ?? 0n; + expect(r.score(p1, RankType.TrainTrade)).toBe( + Number(ownTrain + otherTrain), + ); expect(r.score(p1, RankType.ConqueredGold)).toBe( Number(p1.gold[GOLD_INDEX_WAR] ?? 0n), ); From dbb5eb5993b31f18139e6574262fde87947b7005 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Thu, 15 Jan 2026 16:05:00 -0800 Subject: [PATCH 3/7] use GIT_COMMIT instead of version for manifest.json cache busting to prevent users from pulling stale manifest if the version is not updated --- src/client/TerrainMapFileLoader.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/TerrainMapFileLoader.ts b/src/client/TerrainMapFileLoader.ts index 771698902..c3b185ac9 100644 --- a/src/client/TerrainMapFileLoader.ts +++ b/src/client/TerrainMapFileLoader.ts @@ -1,4 +1,6 @@ -import version from "resources/version.txt?raw"; import { FetchGameMapLoader } from "../core/game/FetchGameMapLoader"; -export const terrainMapFileLoader = new FetchGameMapLoader(`/maps`, version); +export const terrainMapFileLoader = new FetchGameMapLoader( + `/maps`, + window.GIT_COMMIT, +); From e1d4b9a00e0ef09315b3138463de3771b5e4c377 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Thu, 15 Jan 2026 16:16:33 -0800 Subject: [PATCH 4/7] allow name to be placed on shore tiles, this prevents rivers from bisecting player names causing them to be too small --- src/client/graphics/NameBoxCalculator.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/graphics/NameBoxCalculator.ts b/src/client/graphics/NameBoxCalculator.ts index 67f21916c..fa2165512 100644 --- a/src/client/graphics/NameBoxCalculator.ts +++ b/src/client/graphics/NameBoxCalculator.ts @@ -92,6 +92,7 @@ export function createGrid( const tile = game.ref(cell.x, cell.y); grid[x - scaledBoundingBox.min.x][y - scaledBoundingBox.min.y] = game.isLake(tile) || + game.isShore(tile) || game.owner(tile) === player || game.hasFallout(tile); } From d758e213513990b5c9a1470ed195533c6ed89adf Mon Sep 17 00:00:00 2001 From: DevelopingTom Date: Fri, 16 Jan 2026 19:14:38 +0100 Subject: [PATCH 5/7] Restyle game rank modal (#2918) ## Description: The game rank modal was still using the old style, which clashes strongly with the new one. This PR changes changes the modal style to be consistent with the new one: ### Old image ### New ![redesign](https://github.com/user-attachments/assets/ecf4f0ae-88f0-433c-90be-f41447e17afe) Tagged as `v29` to have a consistent style in the same version. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: IngloriousTom --- resources/lang/en.json | 2 + src/client/GameInfoModal.ts | 8 ++- src/client/components/baseComponents/Modal.ts | 4 +- .../baseComponents/ranking/PlayerRow.ts | 60 +++++++++---------- .../baseComponents/ranking/RankingControls.ts | 10 ++-- .../baseComponents/ranking/RankingHeader.ts | 14 +++-- 6 files changed, 54 insertions(+), 44 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index c18d7be1a..d3c3d4e62 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -224,6 +224,8 @@ "total_gold": "Total", "all_gold": "All gold", "trade": "Trade", + "train_trade": "Train", + "naval_trade": "Tradeship", "conquest_gold": "Conquered player gold", "stolen_gold": "Stolen with warships", "num_of_conquests": "Number of conquered players", diff --git a/src/client/GameInfoModal.ts b/src/client/GameInfoModal.ts index a1f50e4b7..917665a93 100644 --- a/src/client/GameInfoModal.ts +++ b/src/client/GameInfoModal.ts @@ -49,7 +49,9 @@ export class GameInfoModal extends LitElement { title="${translateText("game_info_modal.title")}" translationKey="main.game_info" > -
+
${this.isLoadingGame ? this.renderLoadingAnimation() @@ -108,7 +110,7 @@ export class GameInfoModal extends LitElement { const isUnusualThumbnailSize = hasUnusualThumbnailSize(info.config.gameMap); return html`
${this.mapImage ? html` 0 ? this.score(this.rankedPlayers[0]) : 0; return html` -
    +
      `} ${!this.hideHeader && this.title ? html`
      ${this.title}
      ` diff --git a/src/client/components/baseComponents/ranking/PlayerRow.ts b/src/client/components/baseComponents/ranking/PlayerRow.ts index 9989962a9..773188c8b 100644 --- a/src/client/components/baseComponents/ranking/PlayerRow.ts +++ b/src/client/components/baseComponents/ranking/PlayerRow.ts @@ -27,15 +27,13 @@ export class PlayerRow extends LitElement { const visibleBorder = player.winner || this.currentPlayer; return html`
    • ${Number(this.score).toFixed(0)}
      @@ -106,10 +104,10 @@ export class PlayerRow extends LitElement { const width = Math.min(Math.max((this.score / bestScore) * 100, 0), 100); return html`
      -
      +
      @@ -121,8 +119,8 @@ export class PlayerRow extends LitElement { return html`
      ${renderNumber(value)}
      @@ -157,13 +155,13 @@ export class PlayerRow extends LitElement { return html`
      ${this.renderMultiScoreType( - Number(navalTrade), - this.rankType === RankType.NavalTrade, + Number(ownTrainTrade + otherTrainTrade), + this.rankType === RankType.TrainTrade, )} / ${this.renderMultiScoreType( - Number(ownTrainTrade + otherTrainTrade), - this.rankType === RankType.TrainTrade, + Number(navalTrade), + this.rankType === RankType.NavalTrade, )}
      `; @@ -189,7 +187,7 @@ export class PlayerRow extends LitElement {
      ${renderNumber(this.score)}
      @@ -200,18 +198,20 @@ export class PlayerRow extends LitElement { private renderTradeScore() { return html` -
      - ${this.renderPlayerIcon()} -
      ${this.renderPlayerName()}
      -
      - -
      -
      - ${this.renderAllTrades()} +
      +
      + ${this.renderPlayerIcon()} +
      + ${this.renderPlayerName()} +
      +
      + +
      +
      + ${this.renderAllTrades()} +
      +
      -
      `; } @@ -221,7 +221,7 @@ export class PlayerRow extends LitElement {
      ${this.player.tag ? this.renderTag(this.player.tag) : ""}
      ${this.player.username}
      @@ -232,7 +232,7 @@ export class PlayerRow extends LitElement { private renderTag(tag: string) { return html`
      ${tag}
      diff --git a/src/client/components/baseComponents/ranking/RankingControls.ts b/src/client/components/baseComponents/ranking/RankingControls.ts index 25321d8aa..e32933efe 100644 --- a/src/client/components/baseComponents/ranking/RankingControls.ts +++ b/src/client/components/baseComponents/ranking/RankingControls.ts @@ -57,9 +57,9 @@ export class RankingControls extends LitElement { private renderButton(type: RankType, active: boolean, label: string) { return html` From d0fda1d5358397a53fb95dc733a15299a01c5023 Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 15 Jan 2026 20:57:46 +0000 Subject: [PATCH 6/7] mergestats (#2904) If this PR fixes an issue, link it below. If not, delete these two lines. Resolves #2704 ## Description: Merges together easy + medium difficulties. Before: image After: (dont have one to show oop) (btw that win ratio in the first screenshot is not mine.. :skull:) ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n --- .../baseComponents/stats/PlayerStatsTree.ts | 210 +++++++++++++----- 1 file changed, 158 insertions(+), 52 deletions(-) diff --git a/src/client/components/baseComponents/stats/PlayerStatsTree.ts b/src/client/components/baseComponents/stats/PlayerStatsTree.ts index 4ec48f70a..e703ee2a2 100644 --- a/src/client/components/baseComponents/stats/PlayerStatsTree.ts +++ b/src/client/components/baseComponents/stats/PlayerStatsTree.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from "lit"; +import { LitElement, PropertyValues, html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { PlayerStatsLeaf, PlayerStatsTree } from "../../../../core/ApiSchemas"; import { @@ -21,22 +21,31 @@ export class PlayerStatsTreeView extends LitElement { @state() selectedMode: GameMode = GameMode.FFA; @state() selectedDifficulty: Difficulty = Difficulty.Medium; + private get typeNode() { + return this.statsTree?.[this.selectedType]; + } + + private get modeNode() { + return this.typeNode?.[this.selectedMode]; + } + + private get shouldMergeDifficulties() { + return this.selectedType === GameType.Public; + } + private get availableTypes(): GameType[] { if (!this.statsTree) return []; return Object.keys(this.statsTree).filter(isGameType); } private get availableModes(): GameMode[] { - const typeNode = this.statsTree?.[this.selectedType]; - if (!typeNode) return []; - return Object.keys(typeNode).filter(isGameMode); + if (!this.typeNode) return []; + return Object.keys(this.typeNode).filter(isGameMode); } private get availableDifficulties(): Difficulty[] { - const typeNode = this.statsTree?.[this.selectedType]; - const modeNode = typeNode?.[this.selectedMode]; - if (!modeNode) return []; - return Object.keys(modeNode).filter(isDifficulty); + if (!this.modeNode) return []; + return Object.keys(this.modeNode).filter(isDifficulty); } private labelForMode(m: GameMode) { @@ -50,52 +59,37 @@ export class PlayerStatsTreeView extends LitElement { } private getSelectedLeaf(): PlayerStatsLeaf | null { - const typeNode = this.statsTree?.[this.selectedType]; - if (!typeNode) return null; - const modeNode = typeNode[this.selectedMode]; + const modeNode = this.modeNode; if (!modeNode) return null; - const diffNode = modeNode[this.selectedDifficulty]; - if (!diffNode) return null; - return diffNode; - } - private getDisplayedStats(): PlayerStats | null { - const leaf = this.getSelectedLeaf(); - if (!leaf || !leaf.stats) return null; - return leaf.stats; - } - - private setGameType(t: GameType) { - if (this.selectedType === t) return; - this.selectedType = t; - const modes = this.availableModes; - if (!modes.includes(this.selectedMode)) { - this.selectedMode = modes[0] ?? this.selectedMode; + if (!this.shouldMergeDifficulties) { + return modeNode[this.selectedDifficulty] ?? null; } - const diffs = this.availableDifficulties; - if (!diffs.includes(this.selectedDifficulty)) { - this.selectedDifficulty = diffs[0] ?? this.selectedDifficulty; - } - this.requestUpdate(); + + const diffKeys = Object.keys(modeNode).filter(isDifficulty); + if (!diffKeys.length) return null; + + return diffKeys.reduce((merged, diffKey) => { + const leaf = modeNode[diffKey]; + if (!leaf) return merged; + if (!merged) { + return { + wins: leaf.wins, + losses: leaf.losses, + total: leaf.total, + stats: this.cloneStats(leaf.stats), + }; + } + return { + wins: merged.wins + leaf.wins, + losses: merged.losses + leaf.losses, + total: merged.total + leaf.total, + stats: this.mergeStats(merged.stats, leaf.stats), + }; + }, null); } - private setMode(m: GameMode) { - if (this.selectedMode === m) return; - this.selectedMode = m; - const diffs = this.availableDifficulties; - if (!diffs.includes(this.selectedDifficulty)) { - this.selectedDifficulty = diffs[0] ?? this.selectedDifficulty; - } - this.requestUpdate(); - } - - private setDifficulty(d: Difficulty) { - if (this.selectedDifficulty === d) return; - this.selectedDifficulty = d; - this.requestUpdate(); - } - - render() { + private syncSelection(): void { const types = this.availableTypes; if (types.length && !types.includes(this.selectedType)) { this.selectedType = types[0]; @@ -105,10 +99,122 @@ export class PlayerStatsTreeView extends LitElement { this.selectedMode = modes[0]; } const diffs = this.availableDifficulties; - if (diffs.length && !diffs.includes(this.selectedDifficulty)) { + if ( + !this.shouldMergeDifficulties && + diffs.length && + !diffs.includes(this.selectedDifficulty) + ) { this.selectedDifficulty = diffs[0]; } + } + protected willUpdate(changedProperties: PropertyValues) { + if ( + changedProperties.has("statsTree") || + changedProperties.has("selectedType") || + changedProperties.has("selectedMode") || + changedProperties.has("selectedDifficulty") + ) { + this.syncSelection(); + } + } + + private setGameType(t: GameType) { + if (this.selectedType === t) return; + this.selectedType = t; + this.requestUpdate(); + } + + private setMode(m: GameMode) { + if (this.selectedMode === m) return; + this.selectedMode = m; + this.requestUpdate(); + } + + private setDifficulty(d: Difficulty) { + if (this.selectedDifficulty === d) return; + this.selectedDifficulty = d; + this.requestUpdate(); + } + + private mergeStats( + base: PlayerStats | undefined, + next: PlayerStats | undefined, + ): PlayerStats | undefined { + if (!base && !next) return undefined; + if (!base) return this.cloneStats(next); + if (!next) return this.cloneStats(base); + + return { + attacks: this.mergeStatArrays(base.attacks, next.attacks), + betrayals: this.mergeStatValue(base.betrayals, next.betrayals), + killedAt: this.mergeStatValue(base.killedAt, next.killedAt), + conquests: this.mergeStatValue(base.conquests, next.conquests), + boats: this.mergeStatRecord(base.boats, next.boats), + bombs: this.mergeStatRecord(base.bombs, next.bombs), + gold: this.mergeStatArrays(base.gold, next.gold), + units: this.mergeStatRecord(base.units, next.units), + }; + } + + private mergeStatValue( + base: bigint | undefined, + next: bigint | undefined, + ): bigint | undefined { + if (base === undefined && next === undefined) return undefined; + return (base ?? 0n) + (next ?? 0n); + } + + private mergeStatArrays( + base: bigint[] | undefined, + next: bigint[] | undefined, + ): bigint[] | undefined { + if (!base && !next) return undefined; + const maxLen = Math.max(base?.length ?? 0, next?.length ?? 0); + const merged: bigint[] = []; + for (let i = 0; i < maxLen; i += 1) { + merged[i] = (base?.[i] ?? 0n) + (next?.[i] ?? 0n); + } + return merged; + } + + private mergeStatRecord( + base: Partial> | undefined, + next: Partial> | undefined, + ): Partial> | undefined { + if (!base && !next) return undefined; + const merged: Partial> = {}; + const keys = new Set([ + ...Object.keys(base ?? {}), + ...Object.keys(next ?? {}), + ]) as Set; + keys.forEach((key) => { + const mergedArray = this.mergeStatArrays(base?.[key], next?.[key]); + if (mergedArray) { + merged[key] = mergedArray; + } + }); + return Object.keys(merged).length ? merged : undefined; + } + + private cloneStats(stats: PlayerStats | undefined): PlayerStats | undefined { + if (!stats) return undefined; + return { + attacks: stats.attacks ? [...stats.attacks] : undefined, + betrayals: stats.betrayals, + killedAt: stats.killedAt, + conquests: stats.conquests, + boats: stats.boats ? { ...stats.boats } : undefined, + bombs: stats.bombs ? { ...stats.bombs } : undefined, + gold: stats.gold ? [...stats.gold] : undefined, + units: stats.units ? { ...stats.units } : undefined, + }; + } + + render() { + const types = this.availableTypes; + const modes = this.availableModes; + const diffs = this.availableDifficulties; const leaf = this.getSelectedLeaf(); const wlr = leaf ? leaf.losses === 0n @@ -167,7 +273,7 @@ export class PlayerStatsTreeView extends LitElement { : html``} - ${diffs.length + ${!this.shouldMergeDifficulties && diffs.length ? html`
      @@ -209,7 +315,7 @@ export class PlayerStatsTreeView extends LitElement {
      From 4e8454f3ccdc49d70e0bb300127c6b305780916d Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:19:41 +0100 Subject: [PATCH 7/7] =?UTF-8?q?Lobby=20Gold=20Options=20(Starting=20Gold,?= =?UTF-8?q?=20Gold=20Multiplier)=20=F0=9F=92=B0=20(#2915)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: We might want to add this to v29 to have a third possible public game modifier from the beginning on 😄 Would be fun - Add starting gold option (0 to 1_000_000_000 allowed, also applies to nations) - Add gold multiplier option (0.1 to 1000 allowed, also applies to nations and bots) - Add third public game modifier (3% chance of starting with 5M gold) - Why 5M? It's enough gold to massively change the game start but not enough to insta-hydro someone (launcher + hydro is 6M) image ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin --- resources/lang/en.json | 15 +- src/client/HostLobbyModal.ts | 204 +++++++++++++++++++++ src/client/PublicLobby.ts | 3 + src/client/SinglePlayerModal.ts | 224 ++++++++++++++++++++++++ src/core/Schemas.ts | 3 + src/core/configuration/Config.ts | 2 + src/core/configuration/DefaultConfig.ts | 32 +++- src/core/game/Game.ts | 1 + src/core/game/PlayerImpl.ts | 2 +- src/server/GameServer.ts | 8 +- src/server/MapPlaylist.ts | 8 +- 11 files changed, 488 insertions(+), 14 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index d3c3d4e62..6027fc282 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -171,7 +171,11 @@ "max_timer_invalid": "Please enter a valid max timer value (1-120 minutes)", "disable_nukes": "Disable Nukes", "enables_title": "Enable Settings", - "start": "Start Game" + "start": "Start Game", + "gold_multiplier": "Gold multiplier", + "gold_multiplier_placeholder": "2.0x", + "starting_gold": "Starting gold", + "starting_gold_placeholder": "5000000" }, "token_login_modal": { "title": "Logging in...", @@ -381,7 +385,11 @@ "teams_Duos": "Duos (teams of 2)", "teams_Trios": "Trios (teams of 3)", "teams_Quads": "Quads (teams of 4)", - "teams_Humans Vs Nations": "Humans vs Nations" + "teams_Humans Vs Nations": "Humans vs Nations", + "gold_multiplier": "Gold multiplier", + "gold_multiplier_placeholder": "2.0x", + "starting_gold": "Starting gold", + "starting_gold_placeholder": "5000000" }, "team_colors": { "red": "Red", @@ -411,7 +419,8 @@ }, "public_game_modifier": { "random_spawn": "Random Spawn", - "compact_map": "Compact Map" + "compact_map": "Compact Map", + "starting_gold": "5M Starting Gold" }, "select_lang": { "title": "Select Language" diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 5abdb60a9..e7d709c12 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -60,6 +60,10 @@ export class HostLobbyModal extends BaseModal { @state() private instantBuild: boolean = false; @state() private randomSpawn: boolean = false; @state() private compactMap: boolean = false; + @state() private goldMultiplier: boolean = false; + @state() private goldMultiplierValue: number | undefined = undefined; + @state() private startingGold: boolean = false; + @state() private startingGoldValue: number | undefined = undefined; @state() private lobbyId = ""; @state() private copySuccess = false; @state() private lobbyUrlSuffix = ""; @@ -739,6 +743,158 @@ export class HostLobbyModal extends BaseModal { ${translateText("host_modal.player_immunity_duration")}
      + + +
      this.goldMultiplier, + (val) => (this.goldMultiplier = val), + () => this.goldMultiplierValue, + (val) => (this.goldMultiplierValue = val), + 2, + ).click} + @keydown=${this.createToggleHandlers( + () => this.goldMultiplier, + (val) => (this.goldMultiplier = val), + () => this.goldMultiplierValue, + (val) => (this.goldMultiplierValue = val), + 2, + ).keydown} + class="relative p-3 rounded-xl border transition-all duration-200 flex flex-col items-center justify-between gap-2 h-full cursor-pointer min-h-[100px] ${this + .goldMultiplier + ? "bg-blue-500/20 border-blue-500/50" + : "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20"}" + > +
      +
      + ${this.goldMultiplier + ? html` + + ` + : ""} +
      +
      + + ${this.goldMultiplier + ? html`` + : html`
      `} + +
      + ${translateText("single_modal.gold_multiplier")} +
      +
      + + +
      this.startingGold, + (val) => (this.startingGold = val), + () => this.startingGoldValue, + (val) => (this.startingGoldValue = val), + 5000000, + ).click} + @keydown=${this.createToggleHandlers( + () => this.startingGold, + (val) => (this.startingGold = val), + () => this.startingGoldValue, + (val) => (this.startingGoldValue = val), + 5000000, + ).keydown} + class="relative p-3 rounded-xl border transition-all duration-200 flex flex-col items-center justify-between gap-2 h-full cursor-pointer min-h-[100px] ${this + .startingGold + ? "bg-blue-500/20 border-blue-500/50" + : "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20"}" + > +
      +
      + ${this.startingGold + ? html` + + ` + : ""} +
      +
      + + ${this.startingGold + ? html`` + : html`
      `} + +
      + ${translateText("single_modal.starting_gold")} +
      +
      @@ -968,6 +1124,10 @@ export class HostLobbyModal extends BaseModal { this.lobbyCreatorClientID = ""; this.lobbyIdVisible = true; this.nationCount = 0; + this.goldMultiplier = false; + this.goldMultiplierValue = undefined; + this.startingGold = false; + this.startingGoldValue = undefined; this.leaveLobbyOnClose = true; } @@ -1036,6 +1196,44 @@ export class HostLobbyModal extends BaseModal { this.putGameConfig(); } + private handleGoldMultiplierValueKeyDown(e: KeyboardEvent) { + if (["+", "-", "e", "E"].includes(e.key)) { + e.preventDefault(); + } + } + + private handleGoldMultiplierValueChanges(e: Event) { + const input = e.target as HTMLInputElement; + const value = parseFloat(input.value); + + if (isNaN(value) || value < 0.1 || value > 1000) { + this.goldMultiplierValue = undefined; + input.value = ""; + } else { + this.goldMultiplierValue = value; + } + this.putGameConfig(); + } + + private handleStartingGoldValueKeyDown(e: KeyboardEvent) { + if (["-", "+", "e", "E"].includes(e.key)) { + e.preventDefault(); + } + } + + private handleStartingGoldValueChanges(e: Event) { + const input = e.target as HTMLInputElement; + input.value = input.value.replace(/[eE+-]/g, ""); + const value = parseInt(input.value); + + if (isNaN(value) || value < 0 || value > 1000000000) { + this.startingGoldValue = undefined; + } else { + this.startingGoldValue = value; + } + this.putGameConfig(); + } + private handleRandomSpawnChange = (val: boolean) => { this.randomSpawn = val; this.putGameConfig(); @@ -1151,6 +1349,12 @@ export class HostLobbyModal extends BaseModal { }), maxTimerValue: this.maxTimer === true ? this.maxTimerValue : undefined, + goldMultiplier: + this.goldMultiplier === true + ? this.goldMultiplierValue + : undefined, + startingGold: + this.startingGold === true ? this.startingGoldValue : undefined, } satisfies Partial, }, bubbles: true, diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts index c7516804d..4c895ab8f 100644 --- a/src/client/PublicLobby.ts +++ b/src/client/PublicLobby.ts @@ -374,6 +374,9 @@ export class PublicLobby extends LitElement { if (publicGameModifiers.isCompact) { labels.push(translateText("public_game_modifier.compact_map")); } + if (publicGameModifiers.startingGold) { + labels.push(translateText("public_game_modifier.starting_gold")); + } return labels; } diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index 12c805751..c34dfe268 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -52,6 +52,10 @@ export class SinglePlayerModal extends BaseModal { @state() private showAchievements: boolean = false; @state() private mapWins: Map> = new Map(); @state() private userMeResponse: UserMeResponse | false = false; + @state() private goldMultiplier: boolean = false; + @state() private goldMultiplierValue: number | undefined = undefined; + @state() private startingGold: boolean = false; + @state() private startingGoldValue: number | undefined = undefined; @state() private disabledUnits: UnitType[] = []; @@ -601,6 +605,180 @@ export class SinglePlayerModal extends BaseModal { ${translateText("single_modal.max_timer")}
      + + +
      { + if ( + (e.target as HTMLElement).tagName.toLowerCase() === + "input" + ) + return; + this.goldMultiplier = !this.goldMultiplier; + if (!this.goldMultiplier) { + this.goldMultiplierValue = undefined; + } else { + if ( + !this.goldMultiplierValue || + this.goldMultiplierValue <= 0 + ) { + this.goldMultiplierValue = 2; + } + setTimeout(() => { + const input = this.renderRoot.querySelector( + "#gold-multiplier-value", + ) as HTMLInputElement; + if (input) { + input.focus(); + input.select(); + } + }, 0); + } + }} + > +
      +
      + ${this.goldMultiplier + ? html` + + ` + : ""} +
      +
      + + ${this.goldMultiplier + ? html`` + : html`
      `} + +
      + ${translateText("single_modal.gold_multiplier")} +
      +
      + + +
      { + if ( + (e.target as HTMLElement).tagName.toLowerCase() === + "input" + ) + return; + this.startingGold = !this.startingGold; + if (!this.startingGold) { + this.startingGoldValue = undefined; + } else { + if ( + !this.startingGoldValue || + this.startingGoldValue < 0 + ) { + this.startingGoldValue = 5000000; + } + setTimeout(() => { + const input = this.renderRoot.querySelector( + "#starting-gold-value", + ) as HTMLInputElement; + if (input) { + input.focus(); + input.select(); + } + }, 0); + } + }} + > +
      +
      + ${this.startingGold + ? html` + + ` + : ""} +
      +
      + + ${this.startingGold + ? html`` + : html`
      `} + +
      + ${translateText("single_modal.starting_gold")} +
      +
      @@ -714,6 +892,10 @@ export class SinglePlayerModal extends BaseModal { this.randomSpawn = false; this.teamCount = 2; this.disabledUnits = []; + this.goldMultiplier = false; + this.goldMultiplierValue = undefined; + this.startingGold = false; + this.startingGoldValue = undefined; } private handleSelectRandomMap() { @@ -767,6 +949,42 @@ export class SinglePlayerModal extends BaseModal { } } + private handleGoldMultiplierValueKeyDown(e: KeyboardEvent) { + if (["+", "-", "e", "E"].includes(e.key)) { + e.preventDefault(); + } + } + + private handleGoldMultiplierValueChanges(e: Event) { + const input = e.target as HTMLInputElement; + const value = parseFloat(input.value); + + if (isNaN(value) || value < 0.1 || value > 1000) { + this.goldMultiplierValue = undefined; + input.value = ""; + } else { + this.goldMultiplierValue = value; + } + } + + private handleStartingGoldValueKeyDown(e: KeyboardEvent) { + if (["-", "+", "e", "E"].includes(e.key)) { + e.preventDefault(); + } + } + + private handleStartingGoldValueChanges(e: Event) { + const input = e.target as HTMLInputElement; + input.value = input.value.replace(/[eE+-]/g, ""); + const value = parseInt(input.value); + + if (isNaN(value) || value < 0 || value > 1000000000) { + this.startingGoldValue = undefined; + } else { + this.startingGoldValue = value; + } + } + private handleGameModeSelection(value: GameMode) { this.gameMode = value; } @@ -888,6 +1106,12 @@ export class SinglePlayerModal extends BaseModal { : { disableNations: this.disableNations, }), + ...(this.goldMultiplier && this.goldMultiplierValue + ? { goldMultiplier: this.goldMultiplierValue } + : {}), + ...(this.startingGold && this.startingGoldValue !== undefined + ? { startingGold: this.startingGoldValue } + : {}), }, lobbyCreatedAt: Date.now(), // ms; server should be authoritative in MP }, diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 10a1a84b2..28362063f 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -190,6 +190,7 @@ export const GameConfigSchema = z.object({ .object({ isCompact: z.boolean(), isRandomSpawn: z.boolean(), + startingGold: z.number().int().min(0).optional(), }) .optional(), disableNations: z.boolean(), @@ -204,6 +205,8 @@ export const GameConfigSchema = z.object({ spawnImmunityDuration: z.number().int().min(0).optional(), // In ticks disabledUnits: z.enum(UnitType).array().optional(), playerTeams: TeamCountConfigSchema.optional(), + goldMultiplier: z.number().min(0.1).max(1000).optional(), + startingGold: z.number().int().min(0).max(1000000000).optional(), }); export const TeamSchema = z.string(); diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 4c9fce54d..ac1d9ee4a 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -76,6 +76,8 @@ export interface Config { numSpawnPhaseTurns(): number; userSettings(): UserSettings; playerTeams(): TeamCountConfig; + goldMultiplier(): number; + startingGold(playerInfo: PlayerInfo): Gold; startManpower(playerInfo: PlayerInfo): number; troopIncreaseRate(player: Player | PlayerView): number; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 7311cb60c..36057bdad 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -245,6 +245,15 @@ export class DefaultConfig implements Config { donateTroops(): boolean { return this._gameConfig.donateTroops; } + goldMultiplier(): number { + return this._gameConfig.goldMultiplier ?? 1; + } + startingGold(playerInfo: PlayerInfo): Gold { + if (playerInfo.playerType === PlayerType.Bot) { + return 0n; + } + return BigInt(this._gameConfig.startingGold ?? 0); + } trainSpawnRate(numPlayerFactories: number): number { // hyperbolic decay, midpoint at 10 factories @@ -252,15 +261,21 @@ export class DefaultConfig implements Config { return (numPlayerFactories + 10) * 18; } trainGold(rel: "self" | "team" | "ally" | "other"): Gold { + const multiplier = this.goldMultiplier(); + let baseGold: bigint; switch (rel) { case "ally": - return 35_000n; + baseGold = 35_000n; + break; case "team": case "other": - return 25_000n; + baseGold = 25_000n; + break; case "self": - return 10_000n; + baseGold = 10_000n; + break; } + return BigInt(Math.floor(Number(baseGold) * multiplier)); } trainStationMinRange(): number { @@ -281,7 +296,8 @@ export class DefaultConfig implements Config { const numPortBonus = numPorts - 1; // Hyperbolic decay, midpoint at 5 ports, 3x bonus max. const bonus = 1 + 2 * (numPortBonus / (numPortBonus + 5)); - return BigInt(Math.floor(baseGold * bonus)); + const multiplier = this.goldMultiplier(); + return BigInt(Math.floor(baseGold * bonus * multiplier)); } // Probability of trade ship spawn = 1 / tradeShipSpawnRate @@ -791,10 +807,14 @@ export class DefaultConfig implements Config { } goldAdditionRate(player: Player): Gold { + const multiplier = this.goldMultiplier(); + let baseRate: bigint; if (player.type() === PlayerType.Bot) { - return 50n; + baseRate = 50n; + } else { + baseRate = 100n; } - return 100n; + return BigInt(Math.floor(Number(baseRate) * multiplier)); } nukeMagnitudes(unitType: UnitType): NukeMagnitude { diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 4ead6efe5..fdfff12d8 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -211,6 +211,7 @@ export enum GameMapSize { export interface PublicGameModifiers { isCompact: boolean; isRandomSpawn: boolean; + startingGold?: number; } export interface UnitInfo { diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 3b773576e..e09360acc 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -112,7 +112,7 @@ export class PlayerImpl implements Player { ) { this._name = playerInfo.name; this._troops = toInt(startTroops); - this._gold = 0n; + this._gold = mg.config().startingGold(playerInfo); this._displayName = this._name; this._pseudo_random = new PseudoRandom(simpleHash(this.playerInfo.id)); } diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 068253920..1f685a72e 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -127,14 +127,18 @@ export class GameServer { if (gameConfig.gameMode !== undefined) { this.gameConfig.gameMode = gameConfig.gameMode; } - if (gameConfig.disabledUnits !== undefined) { this.gameConfig.disabledUnits = gameConfig.disabledUnits; } - if (gameConfig.playerTeams !== undefined) { this.gameConfig.playerTeams = gameConfig.playerTeams; } + if (gameConfig.goldMultiplier !== undefined) { + this.gameConfig.goldMultiplier = gameConfig.goldMultiplier; + } + if (gameConfig.startingGold !== undefined) { + this.gameConfig.startingGold = gameConfig.startingGold; + } } public joinClient(client: Client) { diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index a9ba0e78d..beadd0bec 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -94,7 +94,9 @@ export class MapPlaylist { const playerTeams = mode === GameMode.Team ? this.getTeamCount() : undefined; - let { isCompact, isRandomSpawn } = this.getRandomPublicGameModifiers(); + const modifiers = this.getRandomPublicGameModifiers(); + const { startingGold } = modifiers; + let { isCompact, isRandomSpawn } = modifiers; // Duos, Trios, and Quads should not get random spawn (as it defeats the purpose) if ( @@ -122,7 +124,8 @@ export class MapPlaylist { maxPlayers: await this.lobbyMaxPlayers(map, mode, playerTeams, isCompact), gameType: GameType.Public, gameMapSize: isCompact ? GameMapSize.Compact : GameMapSize.Normal, - publicGameModifiers: { isCompact, isRandomSpawn }, + publicGameModifiers: { isCompact, isRandomSpawn, startingGold }, + startingGold, difficulty: playerTeams === HumansVsNations ? Difficulty.Impossible @@ -198,6 +201,7 @@ export class MapPlaylist { return { isRandomSpawn: Math.random() < 0.1, // 10% chance isCompact: Math.random() < 0.05, // 5% chance + startingGold: Math.random() < 0.03 ? 5_000_000 : undefined, // 3% chance }; }