From d143bbcde25ae259da5075e0b47b1f78e1e921e6 Mon Sep 17 00:00:00 2001 From: Ryan Huellen Date: Tue, 6 Jan 2026 18:36:58 -0600 Subject: [PATCH 01/12] feat: land mines --- resources/lang/ar.json | 3 +- resources/lang/bg.json | 6 +- resources/lang/cs.json | 6 +- resources/lang/da.json | 6 +- resources/lang/de.json | 8 +- resources/lang/el.json | 6 +- resources/lang/en.json | 8 +- resources/lang/eo.json | 6 +- resources/lang/fa.json | 6 +- resources/lang/fi.json | 6 +- resources/lang/fr.json | 8 +- resources/lang/gl.json | 6 +- resources/lang/he.json | 6 +- resources/lang/hu.json | 6 +- resources/lang/ja.json | 8 +- resources/lang/ko.json | 8 +- resources/lang/mk.json | 6 +- resources/lang/nl.json | 8 +- resources/lang/pl.json | 8 +- resources/lang/pt-PT.json | 6 +- resources/lang/ru.json | 8 +- resources/lang/sk.json | 6 +- resources/lang/sl.json | 6 +- resources/lang/sv-SE.json | 6 +- resources/lang/tp.json | 3 +- resources/lang/tr.json | 8 +- resources/lang/uk.json | 8 +- resources/lang/zh-CN.json | 8 +- src/client/graphics/layers/BuildMenu.ts | 52 +- .../graphics/layers/StructureDrawingUtils.ts | 17 +- .../graphics/layers/StructureIconsLayer.ts | 9 +- src/client/graphics/layers/StructureLayer.ts | 6 + src/core/GameRunner.ts | 51 +- src/core/configuration/DefaultConfig.ts | 26 +- src/core/execution/ConstructionExecution.ts | 5 + src/core/execution/LandMineExecution.ts | 184 +++++++ src/core/execution/PlayerExecution.ts | 7 +- .../nation/structureSpawnTileValue.ts | 41 ++ src/core/game/Game.ts | 7 +- src/core/game/PlayerImpl.ts | 1 + tests/LandMine.test.ts | 458 ++++++++++++++++++ 41 files changed, 939 insertions(+), 109 deletions(-) create mode 100644 src/core/execution/LandMineExecution.ts create mode 100644 tests/LandMine.test.ts diff --git a/resources/lang/ar.json b/resources/lang/ar.json index 1c608031ce..75f716fdd0 100644 --- a/resources/lang/ar.json +++ b/resources/lang/ar.json @@ -202,7 +202,8 @@ "sam_launcher": "قاذف صواريخ مضادة", "atom_bomb": "قنبلة نووية", "hydrogen_bomb": "قنبلة هيدروجينية", - "mirv": "MIRV" + "mirv": "MIRV", + "land_mine": "لغم أرضي" }, "user_setting": { "title": "إعدادات المستخدم", diff --git a/resources/lang/bg.json b/resources/lang/bg.json index 2fef4934e3..d8963e4d58 100644 --- a/resources/lang/bg.json +++ b/resources/lang/bg.json @@ -324,7 +324,8 @@ "atom_bomb": "Атомна бомба", "hydrogen_bomb": "Водородна бомба", "mirv": "МИРВ", - "factory": "Фабрика" + "factory": "Фабрика", + "land_mine": "Мина" }, "user_setting": { "title": "Потребителски настройки", @@ -517,7 +518,8 @@ "port": "Изпраща търговски кораби, за да се получава злато", "defense_post": "Увеличава отбраната на близки граници", "city": "Увеличава максималната популация", - "factory": "Създава железопътни линии и пуска влакове" + "factory": "Създава железопътни линии и пуска влакове", + "land_mine": "Избухва, когато враг завземе тази клетка" }, "not_enough_money": "Няма достатъчно пари" }, diff --git a/resources/lang/cs.json b/resources/lang/cs.json index 2f7410f20f..f5816a4c4e 100644 --- a/resources/lang/cs.json +++ b/resources/lang/cs.json @@ -216,7 +216,8 @@ "sam_launcher": "Odpalovač SAM", "atom_bomb": "Atomová bomba", "hydrogen_bomb": "Vodíková bomba", - "mirv": "MIRV" + "mirv": "MIRV", + "land_mine": "Pozemní mina" }, "user_setting": { "title": "Uživatelská nastavení", @@ -345,7 +346,8 @@ "warship": "Zachytává obchodní lodě, ničí lodě a čluny", "port": "Odešle obchodní lodě pro generování zlata", "defense_post": "Zvýšit ochranu blízkých hranic", - "city": "Zvýšit maximální počet obyvatel" + "city": "Zvýšit maximální počet obyvatel", + "land_mine": "Vybuchne, když nepřítel obsadí toto pole" }, "not_enough_money": "Nedostatek peněz" }, diff --git a/resources/lang/da.json b/resources/lang/da.json index c5efe53070..4a76eee361 100644 --- a/resources/lang/da.json +++ b/resources/lang/da.json @@ -258,7 +258,8 @@ "atom_bomb": "Atombombe", "hydrogen_bomb": "Brintbombe", "mirv": "MIRV", - "factory": "Fabrik" + "factory": "Fabrik", + "land_mine": "Landmine" }, "user_setting": { "title": "Brugerindstillinger", @@ -427,7 +428,8 @@ "port": "Sender handelsskibe for at generere guld", "defense_post": "Forstærker forsvaret af nærliggende grænser", "city": "Øger maksimal befolkning", - "factory": "Opretter jernbaner og sender tog afsted" + "factory": "Opretter jernbaner og sender tog afsted", + "land_mine": "Eksploderer når en fjende indtager denne felt" }, "not_enough_money": "Ikke nok penge" }, diff --git a/resources/lang/de.json b/resources/lang/de.json index cdedccd6ab..2185af7a6f 100644 --- a/resources/lang/de.json +++ b/resources/lang/de.json @@ -247,7 +247,8 @@ "atom_bomb": "Atombombe", "hydrogen_bomb": "Wasserstoffbombe", "mirv": "MIRV-Rakete", - "factory": "Fabrik" + "factory": "Fabrik", + "land_mine": "Landmine" }, "user_setting": { "title": "Benutzer Einstellungen", @@ -390,7 +391,8 @@ "warship": "Erobert Handelsschiffe, zerstört Schiffe und Boote", "port": "Sendet Handelsschiffe, um Gold zu generieren", "defense_post": "Erhöht Verteidigung der Grenzen in der Nähe", - "city": "Erhöht maximale Bevölkerung" + "city": "Erhöht maximale Bevölkerung", + "land_mine": "Explodiert, wenn ein Feind dieses Feld erobert" }, "not_enough_money": "Nicht genug Geld" }, @@ -539,4 +541,4 @@ "grogu": "Grogu" } } -} +} \ No newline at end of file diff --git a/resources/lang/el.json b/resources/lang/el.json index cde595ed1c..fa7754a2b1 100644 --- a/resources/lang/el.json +++ b/resources/lang/el.json @@ -304,7 +304,8 @@ "atom_bomb": "Ατομική Βόμβα", "hydrogen_bomb": "Βόμβα Υδρογόνου", "mirv": "MIRV", - "factory": "Εργοστάσιο" + "factory": "Εργοστάσιο", + "land_mine": "Νάρκη" }, "user_setting": { "title": "Ρυθμίσεις Χρήστη", @@ -497,7 +498,8 @@ "port": "Στέλνει εμπορικά πλοία για την παραγωγή χρυσού", "defense_post": "Αυξάνει την άμυνα των κοντινών συνόρων", "city": "Αυξάνει τον μέγιστο πληθυσμό", - "factory": "Δημιουργεί σιδηρόδρομους και στέλνει τρένα" + "factory": "Δημιουργεί σιδηρόδρομους και στέλνει τρένα", + "land_mine": "Εκρήγνυται όταν ένας εχθρός καταλάβει αυτό το πλακίδιο" }, "not_enough_money": "Όχι αρκετά χρήματα" }, diff --git a/resources/lang/en.json b/resources/lang/en.json index da20af0646..da028e94f0 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -380,7 +380,8 @@ "atom_bomb": "Atom Bomb", "hydrogen_bomb": "Hydrogen Bomb", "mirv": "MIRV", - "factory": "Factory" + "factory": "Factory", + "land_mine": "Land Mine" }, "user_setting": { "title": "User Settings", @@ -574,7 +575,8 @@ "port": "Sends trade ships to generate gold", "defense_post": "Increases defenses of nearby borders", "city": "Increases max population", - "factory": "Creates railroads and spawns trains" + "factory": "Creates railroads and spawns trains", + "land_mine": "Explodes upon enemy capture" }, "not_enough_money": "Not enough money" }, @@ -870,4 +872,4 @@ "mode_ffa": "Free-for-All", "mode_team": "Team" } -} +} \ No newline at end of file diff --git a/resources/lang/eo.json b/resources/lang/eo.json index 4703c919d9..b2c34c9d46 100644 --- a/resources/lang/eo.json +++ b/resources/lang/eo.json @@ -275,7 +275,8 @@ "atom_bomb": "Atombombo", "hydrogen_bomb": "Hidrogenbombo", "mirv": "MIRV", - "factory": "Fabriko" + "factory": "Fabriko", + "land_mine": "Termino" }, "user_setting": { "title": "Uzantparametroj", @@ -461,7 +462,8 @@ "port": "Sendas komercŝipojn por produkti oron", "defense_post": "Pligrandigas defendojn de proksimumaj landlimoj", "city": "Pligrandigas maksiman loĝantaron", - "factory": "Kreas fervojojn kaj generas trajnojn" + "factory": "Kreas fervojojn kaj generas trajnojn", + "land_mine": "Eksplodas kiam malamiko kaptas ĉi tiun kahelon" }, "not_enough_money": "Ne sufiĉe da mono" }, diff --git a/resources/lang/fa.json b/resources/lang/fa.json index 34cc78ce2d..ef740ad085 100644 --- a/resources/lang/fa.json +++ b/resources/lang/fa.json @@ -324,7 +324,8 @@ "atom_bomb": "بمب اتم", "hydrogen_bomb": "بمب هیدروژنی", "mirv": "میرووی", - "factory": "کارخانه" + "factory": "کارخانه", + "land_mine": "مین زمینی" }, "user_setting": { "title": "تنظیمات کاربر", @@ -517,7 +518,8 @@ "port": "کشتی‌های تجاری را برای تولید طلا ارسال می‌کند", "defense_post": "دفاعات مرزهای اطراف را افزایش می‌دهد", "city": "حداکثر جمعیت را افزایش می‌دهد", - "factory": "راه‌آهن می‌سازد و قطارها را ظاهر می‌کند" + "factory": "راه‌آهن می‌سازد و قطارها را ظاهر می‌کند", + "land_mine": "زمانی که دشمن این خانه را تصرف کند منفجر می‌شود" }, "not_enough_money": "پول کافی نیست" }, diff --git a/resources/lang/fi.json b/resources/lang/fi.json index 52ca74680c..4d3adad713 100644 --- a/resources/lang/fi.json +++ b/resources/lang/fi.json @@ -272,7 +272,8 @@ "atom_bomb": "Atomipommi", "hydrogen_bomb": "Vetypommi", "mirv": "MIRV", - "factory": "Tehdas" + "factory": "Tehdas", + "land_mine": "Maamiina" }, "user_setting": { "title": "Käyttäjäasetukset", @@ -456,7 +457,8 @@ "port": "Lähettää kauppa-aluksia tuottaakseen kultaa", "defense_post": "Parantaa läheisten rajojen puolustusta", "city": "Lisää enimmäisväkilukuasi", - "factory": "Luo rautateitä ja lähettää junia" + "factory": "Luo rautateitä ja lähettää junia", + "land_mine": "Räjähtää kun vihollinen valtaa tämän ruudun" }, "not_enough_money": "Ei tarpeeksi rahaa" }, diff --git a/resources/lang/fr.json b/resources/lang/fr.json index 31328191e4..3c54e4b347 100644 --- a/resources/lang/fr.json +++ b/resources/lang/fr.json @@ -324,7 +324,8 @@ "atom_bomb": "Bombe atomique", "hydrogen_bomb": "Bombe à hydrogène", "mirv": "MIRV", - "factory": "Usine" + "factory": "Usine", + "land_mine": "Mine terrestre" }, "user_setting": { "title": "Paramètres utilisateur", @@ -517,7 +518,8 @@ "port": "Envoie des navires commerciaux pour produire de l'or", "defense_post": "Augmente les défenses des frontières proches", "city": "Augmente la population maximale", - "factory": "Crée des chemins de fer et fait apparaître des trains" + "factory": "Crée des chemins de fer et fait apparaître des trains", + "land_mine": "Explose quand un ennemi capture cette case" }, "not_enough_money": "Pas assez d'argent" }, @@ -805,4 +807,4 @@ "mode_ffa": "Chacun pour soi", "mode_team": "En équipe" } -} +} \ No newline at end of file diff --git a/resources/lang/gl.json b/resources/lang/gl.json index 768b7d07b9..d1032a29ab 100644 --- a/resources/lang/gl.json +++ b/resources/lang/gl.json @@ -258,7 +258,8 @@ "atom_bomb": "Bomba atómica", "hydrogen_bomb": "Bomba de hidróxeno", "mirv": "MIRV", - "factory": "Fábrica" + "factory": "Fábrica", + "land_mine": "Mina terrestre" }, "user_setting": { "title": "Axustes do usuario", @@ -427,7 +428,8 @@ "port": "Envía barcos comerciais para xerar ouro", "defense_post": "Reforza as defensas das fronteiras próximas", "city": "Aumenta a poboación máxima", - "factory": "Constrúe vías férreas e xera trens" + "factory": "Constrúe vías férreas e xera trens", + "land_mine": "Explota cando un inimigo captura esta cela" }, "not_enough_money": "Non tes ouro dabondo" }, diff --git a/resources/lang/he.json b/resources/lang/he.json index 895b58560c..859dd2d66d 100644 --- a/resources/lang/he.json +++ b/resources/lang/he.json @@ -247,7 +247,8 @@ "atom_bomb": "פצצת אטום", "hydrogen_bomb": "פצצת מימן", "mirv": "MIRV", - "factory": "מפעל" + "factory": "מפעל", + "land_mine": "מוקש" }, "user_setting": { "title": "הגדרות משתמש", @@ -390,7 +391,8 @@ "warship": "לוכד ספינות סחר, הורס ספינות סחר וכיבוש.", "port": "שולח ספינות סחר כדי לייצר זהב", "defense_post": "מעלה הגנות של גבולות קרובים", - "city": "מעלה כמות אוכלוסיה מירבית" + "city": "מעלה כמות אוכלוסיה מירבית", + "land_mine": "מתפוצץ כאשר אויב כובש את המשבצת הזו" }, "not_enough_money": "אין מספיק זהב" }, diff --git a/resources/lang/hu.json b/resources/lang/hu.json index 343380f019..33b9fc3ca1 100644 --- a/resources/lang/hu.json +++ b/resources/lang/hu.json @@ -275,7 +275,8 @@ "atom_bomb": "Atombomba", "hydrogen_bomb": "Hidrogénbomba", "mirv": "MIRV", - "factory": "Gyár" + "factory": "Gyár", + "land_mine": "Akna" }, "user_setting": { "title": "Játékos beállítások", @@ -461,7 +462,8 @@ "port": "Kereskedőhajókat küld, hogy arany termeljen", "defense_post": "Növeli a közeli határok védelmét", "city": "Növeli a maximális népességet", - "factory": "Vasútvonalakat hoz létre és vonatokat indít" + "factory": "Vasútvonalakat hoz létre és vonatokat indít", + "land_mine": "Felrobban, amikor egy ellenség elfoglalja ezt a mezőt" }, "not_enough_money": "Nincs elég pénz" }, diff --git a/resources/lang/ja.json b/resources/lang/ja.json index 33bcbc2bf0..84180822e1 100644 --- a/resources/lang/ja.json +++ b/resources/lang/ja.json @@ -324,7 +324,8 @@ "atom_bomb": "原子爆弾", "hydrogen_bomb": "水素爆弾", "mirv": "MIRV", - "factory": "工場" + "factory": "工場", + "land_mine": "地雷" }, "user_setting": { "title": "ユーザー設定", @@ -517,7 +518,8 @@ "port": "貿易船を送って資金を獲得する", "defense_post": "近くの国境の防御を強化します", "city": "最大人口が増加します", - "factory": "列車が行き来できる線路を作成します" + "factory": "列車が行き来できる線路を作成します", + "land_mine": "敵がこのタイルを占領すると爆発します" }, "not_enough_money": "資金不足" }, @@ -805,4 +807,4 @@ "mode_ffa": "デスマッチ", "mode_team": "チーム" } -} +} \ No newline at end of file diff --git a/resources/lang/ko.json b/resources/lang/ko.json index 0534d27f55..0fabdf49de 100644 --- a/resources/lang/ko.json +++ b/resources/lang/ko.json @@ -251,7 +251,8 @@ "atom_bomb": "원자 폭탄", "hydrogen_bomb": "수소 폭탄", "mirv": "다탄두 미사일", - "factory": "공장" + "factory": "공장", + "land_mine": "지뢰" }, "user_setting": { "title": "사용자 설정", @@ -413,7 +414,8 @@ "port": "무역선을 보내 금을 생산합니다", "defense_post": "주변 경계의 방어력을 강화합니다", "city": "최대 인구 수를 증가시킵니다", - "factory": "철도를 만들고 기차를 생성합니다" + "factory": "철도를 만들고 기차를 생성합니다", + "land_mine": "적이 이 타일을 점령하면 폭발합니다" }, "not_enough_money": "돈이 부족합니다" }, @@ -580,4 +582,4 @@ "not_authorized": "이 웹사이트에 접근할 권한이 없습니다.", "contact_admin": "이 메시지가 잘못 표시되었다고 생각되면 웹사이트 관리자에게 문의하십시오." } -} +} \ No newline at end of file diff --git a/resources/lang/mk.json b/resources/lang/mk.json index 62b17904d2..4ed229f515 100644 --- a/resources/lang/mk.json +++ b/resources/lang/mk.json @@ -272,7 +272,8 @@ "atom_bomb": "Атомска бомба", "hydrogen_bomb": "Водородна бомба", "mirv": "MIRV", - "factory": "Фабрика" + "factory": "Фабрика", + "land_mine": "Мина" }, "user_setting": { "title": "Кориснички поставки", @@ -456,7 +457,8 @@ "port": "Испраќа трговски бродови за да генерира злато", "defense_post": "Ја зголемува одбраната на блиските граници", "city": "Го зголемува максимумот на популација", - "factory": "Создава железници и пушта возови" + "factory": "Создава железници и пушта возови", + "land_mine": "Експлодира кога непријател ќе го освои ова поле" }, "not_enough_money": "Нема доволно пари" }, diff --git a/resources/lang/nl.json b/resources/lang/nl.json index 7461504080..bb27b716ac 100644 --- a/resources/lang/nl.json +++ b/resources/lang/nl.json @@ -324,7 +324,8 @@ "atom_bomb": "Atoombom", "hydrogen_bomb": "Waterstofbom", "mirv": "MIRV", - "factory": "Fabriek" + "factory": "Fabriek", + "land_mine": "Landmijn" }, "user_setting": { "title": "Gebruikersinstellingen", @@ -517,7 +518,8 @@ "port": "Stuurt handelsschepen om goud te genereren", "defense_post": "Versterkt verdediging eromheen", "city": "Verhoogt maximale bevolking", - "factory": "Maakt spoorwegen en laat treinen rijden" + "factory": "Maakt spoorwegen en laat treinen rijden", + "land_mine": "Ontploft wanneer een vijand dit veld inneemt" }, "not_enough_money": "Niet genoeg goud" }, @@ -805,4 +807,4 @@ "mode_ffa": "Iedereen tegen elkaar", "mode_team": "Team" } -} +} \ No newline at end of file diff --git a/resources/lang/pl.json b/resources/lang/pl.json index 9ab3595939..8a3504a0aa 100644 --- a/resources/lang/pl.json +++ b/resources/lang/pl.json @@ -304,7 +304,8 @@ "atom_bomb": "Bomba atomowa", "hydrogen_bomb": "Bomba wodorowa", "mirv": "MIRV", - "factory": "Fabryka" + "factory": "Fabryka", + "land_mine": "Mina lądowa" }, "user_setting": { "title": "Ustawienia użytkownika", @@ -497,7 +498,8 @@ "port": "Wysyła statki handlowe, aby wygenerować złoto", "defense_post": "Wzmacnia obronę pobliskich granic", "city": "Zwiększa maksymalną populację", - "factory": "Tworzy linie kolejowe i przywołuje pociągi" + "factory": "Tworzy linie kolejowe i przywołuje pociągi", + "land_mine": "Wybucha gdy wróg zajmie to pole" }, "not_enough_money": "Za mało złota" }, @@ -760,4 +762,4 @@ "mode_ffa": "Każdy na Każdego", "mode_team": "Drużyna" } -} +} \ No newline at end of file diff --git a/resources/lang/pt-PT.json b/resources/lang/pt-PT.json index 7ba8a7f86c..80a8b3ded8 100644 --- a/resources/lang/pt-PT.json +++ b/resources/lang/pt-PT.json @@ -258,7 +258,8 @@ "atom_bomb": "Bomba Atómica", "hydrogen_bomb": "Bomba de Hidrogénio", "mirv": "MIRV", - "factory": "Fábrica" + "factory": "Fábrica", + "land_mine": "Mina Terrestre" }, "user_setting": { "title": "Configurações de Utilizador", @@ -427,7 +428,8 @@ "port": "Envia navios de comércio para gerar ouro", "defense_post": "Aumenta as defesas de fronteiras próximas", "city": "Aumenta a população máxima", - "factory": "Cria ferrovias e comboios" + "factory": "Cria ferrovias e comboios", + "land_mine": "Explode quando um inimigo captura esta casa" }, "not_enough_money": "Dinheiro insuficiente" }, diff --git a/resources/lang/ru.json b/resources/lang/ru.json index ce7d1cfe6f..10395607aa 100644 --- a/resources/lang/ru.json +++ b/resources/lang/ru.json @@ -324,7 +324,8 @@ "atom_bomb": "Атомная бомба", "hydrogen_bomb": "Водородная бомба", "mirv": "РГЧ ИН", - "factory": "Фабрика" + "factory": "Фабрика", + "land_mine": "Мина" }, "user_setting": { "title": "Пользовательские настройки", @@ -517,7 +518,8 @@ "port": "Отправляет торговые корабли для генерации золота", "defense_post": "Укрепляет защиту ближайших границ", "city": "Увеличивает максимальное население", - "factory": "Прокладывает железнодорожные пути и создаёт поезда" + "factory": "Прокладывает железнодорожные пути и создаёт поезда", + "land_mine": "Взрывается, когда враг захватывает эту клетку" }, "not_enough_money": "Недостаточно средств" }, @@ -805,4 +807,4 @@ "mode_ffa": "Все против всех", "mode_team": "Команда" } -} +} \ No newline at end of file diff --git a/resources/lang/sk.json b/resources/lang/sk.json index cf2bac42aa..34950528c0 100644 --- a/resources/lang/sk.json +++ b/resources/lang/sk.json @@ -258,7 +258,8 @@ "atom_bomb": "Atómová bomba", "hydrogen_bomb": "Vodíková bomba", "mirv": "MIRV", - "factory": "Továreň" + "factory": "Továreň", + "land_mine": "Pozemná mína" }, "user_setting": { "title": "Užívateľské nastavenia", @@ -427,7 +428,8 @@ "port": "Posiela obchodné lode na tvorbu zlata", "defense_post": "Zvyšuje obranu okolitých hraníc", "city": "Zvyšuje maximálnu populáciu", - "factory": "Tvorí železnice a vlaky" + "factory": "Tvorí železnice a vlaky", + "land_mine": "Vybuchne, keď nepriateľ obsadí toto pole" }, "not_enough_money": "Nedostatok zlata" }, diff --git a/resources/lang/sl.json b/resources/lang/sl.json index b786336aee..8dafb6f43f 100644 --- a/resources/lang/sl.json +++ b/resources/lang/sl.json @@ -275,7 +275,8 @@ "atom_bomb": "Atomska bomba", "hydrogen_bomb": "Vodikova bomba", "mirv": "MIRV", - "factory": "Tovarna" + "factory": "Tovarna", + "land_mine": "Mina" }, "user_setting": { "title": "Uporabniške nastavitve", @@ -461,7 +462,8 @@ "port": "Pošilja trgovske ladje za pridobivanje zlata", "defense_post": "Poveča obrambo bližnjih meja", "city": "Poveča največjo populacijo", - "factory": "Ustvarja železnice in ustvarja vlake" + "factory": "Ustvarja železnice in ustvarja vlake", + "land_mine": "Eksplodira, ko sovražnik zavzame to polje" }, "not_enough_money": "Ni dovolj denarja" }, diff --git a/resources/lang/sv-SE.json b/resources/lang/sv-SE.json index de6923f55c..f6a2091df1 100644 --- a/resources/lang/sv-SE.json +++ b/resources/lang/sv-SE.json @@ -253,7 +253,8 @@ "atom_bomb": "Liten atombomb", "hydrogen_bomb": "Vätebomb", "mirv": "MIRV", - "factory": "Fabrik" + "factory": "Fabrik", + "land_mine": "Landmina" }, "user_setting": { "title": "Användarinställningar", @@ -415,7 +416,8 @@ "port": "Skickar handelsfartyg för att generera guld", "defense_post": "Ökar försvaret mot närliggande gränser", "city": "Ökar max befolkning", - "factory": "Skapar järnvägar och producerar tåg" + "factory": "Skapar järnvägar och producerar tåg", + "land_mine": "Exploderar när en fiende intar denna ruta" }, "not_enough_money": "Ej tillräckligt med guld" }, diff --git a/resources/lang/tp.json b/resources/lang/tp.json index 5cfeb1bbe0..5502773a57 100644 --- a/resources/lang/tp.json +++ b/resources/lang/tp.json @@ -202,7 +202,8 @@ "sam_launcher": "ilo tawa pi ilo sewi pi pakala wawa", "atom_bomb": "ilo pi pakala wawa pi kipisi ijo", "hydrogen_bomb": "ilo pi pakala wawa pi wan ijo", - "mirv": "ilo pi pakala wawa mute" + "mirv": "ilo pi pakala wawa mute", + "land_mine": "ilo pakala ma" }, "user_setting": { "title": "ken pi jan musi", diff --git a/resources/lang/tr.json b/resources/lang/tr.json index 0afa36091e..57ec0f76ea 100644 --- a/resources/lang/tr.json +++ b/resources/lang/tr.json @@ -272,7 +272,8 @@ "atom_bomb": "Atom Bombası", "hydrogen_bomb": "Hidrojen Bombası", "mirv": "MIRV", - "factory": "Fabrika" + "factory": "Fabrika", + "land_mine": "Kara Mayını" }, "user_setting": { "title": "Kullanıcı Ayarları", @@ -456,7 +457,8 @@ "port": "Altın üretmek için ticaret gemileri gönderir", "defense_post": "Yakındaki sınırların savunmasını artırır", "city": "Maksimum nüfusu artırır", - "factory": "Demiryolları yapar ve trenler oluşturur" + "factory": "Demiryolları yapar ve trenler oluşturur", + "land_mine": "Düşman bu kareyi ele geçirdiğinde patlar" }, "not_enough_money": "Yeterli para yok" }, @@ -645,4 +647,4 @@ "delete_unit_title": "Birimi Sil", "delete_unit_description": "En yakın birimi silmek için tıklayın" } -} +} \ No newline at end of file diff --git a/resources/lang/uk.json b/resources/lang/uk.json index 2c11971d33..5cde589665 100644 --- a/resources/lang/uk.json +++ b/resources/lang/uk.json @@ -324,7 +324,8 @@ "atom_bomb": "Атомна бомба", "hydrogen_bomb": "Воднева бомба", "mirv": "РГЧ ІН", - "factory": "Фабрика" + "factory": "Фабрика", + "land_mine": "Міна" }, "user_setting": { "title": "Користувацькі налаштування", @@ -517,7 +518,8 @@ "port": "Відправляє торгові кораблі для генерації золота", "defense_post": "Підсилює оборону найближчих кордонів", "city": "Збільшує максимальне населення", - "factory": "Прокладає залізничні колії та створює поїзди" + "factory": "Прокладає залізничні колії та створює поїзди", + "land_mine": "Вибухає, коли ворог захоплює цю клітинку" }, "not_enough_money": "Недостатньо коштів" }, @@ -805,4 +807,4 @@ "mode_ffa": "Усі проти всіх", "mode_team": "Команда" } -} +} \ No newline at end of file diff --git a/resources/lang/zh-CN.json b/resources/lang/zh-CN.json index 1952eacd30..bff2cae129 100644 --- a/resources/lang/zh-CN.json +++ b/resources/lang/zh-CN.json @@ -324,7 +324,8 @@ "atom_bomb": "原子弹", "hydrogen_bomb": "氢弹", "mirv": "MIRV", - "factory": "工厂" + "factory": "工厂", + "land_mine": "地雷" }, "user_setting": { "title": "用户设置", @@ -517,7 +518,8 @@ "port": "发送商船来获得黄金", "defense_post": "增加附近边界的防御力", "city": "增加最大人口", - "factory": "创建铁轨并生成火车" + "factory": "创建铁轨并生成火车", + "land_mine": "当敌人占领此格时爆炸" }, "not_enough_money": "金钱不足" }, @@ -805,4 +807,4 @@ "mode_ffa": "混战", "mode_team": "团队" } -} +} \ No newline at end of file diff --git a/src/client/graphics/layers/BuildMenu.ts b/src/client/graphics/layers/BuildMenu.ts index e2e46f9b1e..f58c8533d8 100644 --- a/src/client/graphics/layers/BuildMenu.ts +++ b/src/client/graphics/layers/BuildMenu.ts @@ -28,6 +28,7 @@ import warshipIcon from "/images/BattleshipIconWhite.svg?url"; import cityIcon from "/images/CityIconWhite.svg?url"; import factoryIcon from "/images/FactoryIconWhite.svg?url"; import goldCoinIcon from "/images/GoldCoinIcon.svg?url"; +import landMineIcon from "/images/ExplosionIconWhite.svg?url"; import mirvIcon from "/images/MIRVIcon.svg?url"; import missileSiloIcon from "/images/MissileSiloIconWhite.svg?url"; import hydrogenBombIcon from "/images/MushroomCloudIconWhite.svg?url"; @@ -103,6 +104,13 @@ export const buildTable: BuildItemDisplay[][] = [ key: "unit_type.defense_post", countable: true, }, + { + unitType: UnitType.LandMine, + icon: landMineIcon, + description: "build_menu.desc.land_mine", + key: "unit_type.land_mine", + countable: true, + }, { unitType: UnitType.City, icon: cityIcon, @@ -399,7 +407,7 @@ export class BuildMenu extends LitElement implements Layer { } else if (buildableUnit.canBuild) { const rocketDirectionUp = buildableUnit.type === UnitType.AtomBomb || - buildableUnit.type === UnitType.HydrogenBomb + buildableUnit.type === UnitType.HydrogenBomb ? this.uiState.rocketDirectionUp : undefined; this.eventBus.emit( @@ -416,27 +424,27 @@ export class BuildMenu extends LitElement implements Layer { @contextmenu=${(e: MouseEvent) => e.preventDefault()} > ${this.filteredBuildTable.map( - (row) => html` + (row) => html`
${row.map((item) => { - const buildableUnit = this.playerActions?.buildableUnits.find( - (bu) => bu.type === item.unitType, - ); - if (buildableUnit === undefined) { - return html``; - } - const enabled = - buildableUnit.canBuild !== false || - buildableUnit.canUpgrade !== false; - return html` + const buildableUnit = this.playerActions?.buildableUnits.find( + (bu) => bu.type === item.unitType, + ); + if (buildableUnit === undefined) { + return html``; + } + const enabled = + buildableUnit.canBuild !== false || + buildableUnit.canUpgrade !== false; + return html` `; - })} + })}
`, - )} + )} `; } diff --git a/src/client/graphics/layers/StructureDrawingUtils.ts b/src/client/graphics/layers/StructureDrawingUtils.ts index 9fe6a940b1..d3345067ac 100644 --- a/src/client/graphics/layers/StructureDrawingUtils.ts +++ b/src/client/graphics/layers/StructureDrawingUtils.ts @@ -6,6 +6,7 @@ import { TransformHandler } from "../TransformHandler"; import anchorIcon from "/images/AnchorIcon.png?url"; import cityIcon from "/images/CityIcon.png?url"; import factoryIcon from "/images/FactoryUnit.png?url"; +import landMineIcon from "/images/buildings/fortAlt2.png?url"; import missileSiloIcon from "/images/MissileSiloUnit.png?url"; import SAMMissileIcon from "/images/SamLauncherUnit.png?url"; import shieldIcon from "/images/ShieldIcon.png?url"; @@ -17,6 +18,7 @@ export const STRUCTURE_SHAPES: Partial> = { [UnitType.DefensePost]: "octagon", [UnitType.SAMLauncher]: "square", [UnitType.MissileSilo]: "triangle", + [UnitType.LandMine]: "circle", [UnitType.Warship]: "cross", [UnitType.AtomBomb]: "cross", [UnitType.HydrogenBomb]: "cross", @@ -62,6 +64,7 @@ export class SpriteFactory { [UnitType.Port, { iconPath: anchorIcon, image: null }], [UnitType.MissileSilo, { iconPath: missileSiloIcon, image: null }], [UnitType.SAMLauncher, { iconPath: SAMMissileIcon, image: null }], + [UnitType.LandMine, { iconPath: landMineIcon, image: null }], ]); constructor( theme: Theme, @@ -259,13 +262,13 @@ export class SpriteFactory { const shape = STRUCTURE_SHAPES[type]; const texture = shape ? this.createIcon( - owner, - type, - isConstruction, - isMarkedForDeletion, - shape, - renderIcon, - ) + owner, + type, + isConstruction, + isMarkedForDeletion, + shape, + renderIcon, + ) : PIXI.Texture.EMPTY; this.textureCache.set(cacheKey, texture); return texture; diff --git a/src/client/graphics/layers/StructureIconsLayer.ts b/src/client/graphics/layers/StructureIconsLayer.ts index 0c896f7176..c6b52180d1 100644 --- a/src/client/graphics/layers/StructureIconsLayer.ts +++ b/src/client/graphics/layers/StructureIconsLayer.ts @@ -54,7 +54,7 @@ class StructureRenderInfo { public dotContainer: PIXI.Container, public level: number = 0, public underConstruction: boolean = true, - ) {} + ) { } } export class StructureIconsLayer implements Layer { @@ -91,6 +91,7 @@ export class StructureIconsLayer implements Layer { [UnitType.Port, { visible: true }], [UnitType.MissileSilo, { visible: true }], [UnitType.SAMLauncher, { visible: true }], + [UnitType.LandMine, { visible: true }], ]); private lastGhostQueryAt: number; potentialUpgrade: StructureRenderInfo | undefined; @@ -676,9 +677,9 @@ export class StructureIconsLayer implements Layer { Math.max( 1, scale / - (target === render.levelContainer - ? LEVEL_SCALE_FACTOR - : ICON_SCALE_FACTOR_ZOOMED_IN), + (target === render.levelContainer + ? LEVEL_SCALE_FACTOR + : ICON_SCALE_FACTOR_ZOOMED_IN), ), ); } else if (scale > DOTS_ZOOM_THRESHOLD) { diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index 819c4e3f74..ac0100320d 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -10,6 +10,7 @@ import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameView, UnitView } from "../../../core/game/GameView"; import cityIcon from "/images/buildings/cityAlt1.png?url"; import factoryIcon from "/images/buildings/factoryAlt1.png?url"; +import landMineIcon from "/images/buildings/fortAlt2.png?url"; import shieldIcon from "/images/buildings/fortAlt3.png?url"; import anchorIcon from "/images/buildings/port1.png?url"; import missileSiloIcon from "/images/buildings/silo1.png?url"; @@ -69,6 +70,11 @@ export class StructureLayer implements Layer { borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR, territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR, }, + [UnitType.LandMine]: { + icon: landMineIcon, + borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR, + territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR, + }, }; constructor( diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 5e45612ea4..a4fd31cf6b 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -75,6 +75,7 @@ export async function createGameRunner( game, new Executor(game, gameStart.gameID, clientID), callBack, + clientID, ); gr.init(); return gr; @@ -84,6 +85,7 @@ export class GameRunner { private turns: Turn[] = []; private currTurn = 0; private isExecuting = false; + private myPlayer: Player | null = null; private playerViewData: Record = {}; @@ -91,7 +93,8 @@ export class GameRunner { public game: Game, private execManager: Executor, private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void, - ) {} + private clientID: ClientID, + ) { } init() { if (this.game.config().isRandomSpawn()) { @@ -169,6 +172,9 @@ export class GameRunner { const packedTileUpdates = updates[GameUpdateType.Tile].map((u) => u.update); updates[GameUpdateType.Tile] = []; + // Filter out units that should be hidden from this player + this.filterHiddenUnits(updates); + this.callBack({ tick: this.game.ticks(), packedTileUpdates: new BigUint64Array(packedTileUpdates), @@ -179,6 +185,49 @@ export class GameRunner { this.isExecuting = false; } + /** + * Filters out unit updates for units that should be hidden from this client. + * Units with visibleToEnemies: false are only visible to their owner and allies. + */ + private filterHiddenUnits(updates: GameUpdates): void { + // Lazy-load our player reference + if (this.myPlayer === null) { + this.myPlayer = this.game.playerByClientID(this.clientID) ?? null; + } + + updates[GameUpdateType.Unit] = updates[GameUpdateType.Unit].filter( + (unitUpdate) => { + const unitInfo = this.game.config().unitInfo(unitUpdate.unitType); + + // If unit is visible to enemies (default), keep it + if (unitInfo.visibleToEnemies !== false) { + return true; + } + + // Unit is hidden from enemies - check if we should see it + if (this.myPlayer === null) { + // Spectator - can't see hidden enemy units + return false; + } + + // Check if we own or are allied with the owner + const ownerSmallID = unitUpdate.ownerID; + if (this.myPlayer.smallID() === ownerSmallID) { + return true; // We own it + } + + // Check if owner is our ally + const owner = this.game.playerBySmallID(ownerSmallID); + if (owner.isPlayer() && this.myPlayer.isAlliedWith(owner)) { + return true; // Allied with owner + } + + // Enemy unit that should be hidden + return false; + }, + ); + } + public playerActions( playerID: PlayerID, x?: number, diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index e3cc0da2cd..de8836f46b 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -258,7 +258,7 @@ export class DefaultConfig implements Config { private _gameConfig: GameConfig, private _userSettings: UserSettings | null, private _isReplay: boolean, - ) {} + ) { } stripePublishableKey(): string { return Env.STRIPE_PUBLISHABLE_KEY ?? ""; @@ -405,7 +405,7 @@ export class DefaultConfig implements Config { // Geometric mean of base spawn rate and port multiplier const combined = Math.sqrt( this.tradeShipBaseSpawn(numTradeShips, numPlayerTradeShips) * - this.tradeShipPortMultiplier(numPlayerPorts), + this.tradeShipPortMultiplier(numPlayerPorts), ); return Math.floor(25 / combined); @@ -561,6 +561,16 @@ export class DefaultConfig implements Config { territoryBound: false, experimental: true, }; + case UnitType.LandMine: + return { + cost: this.costWrapper( + (numUnits: number) => Math.min(250_000, (numUnits + 1) * 50_000), + UnitType.LandMine, + ), + territoryBound: true, + constructionDuration: this.instantBuild() ? 0 : 5 * 10, + visibleToEnemies: false, + }; default: assertNever(type); } @@ -840,11 +850,11 @@ export class DefaultConfig implements Config { player.type() === PlayerType.Human && this.infiniteTroops() ? 1_000_000_000 : 2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000) + - player - .units(UnitType.City) - .map((city) => city.level()) - .reduce((a, b) => a + b, 0) * - this.cityTroopIncrease(); + player + .units(UnitType.City) + .map((city) => city.level()) + .reduce((a, b) => a + b, 0) * + this.cityTroopIncrease(); if (player.type() === PlayerType.Bot) { return maxTroops / 3; @@ -917,6 +927,8 @@ export class DefaultConfig implements Config { return { inner: 12, outer: 30 }; case UnitType.HydrogenBomb: return { inner: 80, outer: 100 }; + case UnitType.LandMine: + return { inner: 6, outer: 15 }; } throw new Error(`Unknown nuke type: ${unitType}`); } diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts index 2fdd92da1c..041987f337 100644 --- a/src/core/execution/ConstructionExecution.ts +++ b/src/core/execution/ConstructionExecution.ts @@ -3,6 +3,7 @@ import { TileRef } from "../game/GameMap"; import { CityExecution } from "./CityExecution"; import { DefensePostExecution } from "./DefensePostExecution"; import { FactoryExecution } from "./FactoryExecution"; +import { LandMineExecution } from "./LandMineExecution"; import { MirvExecution } from "./MIRVExecution"; import { MissileSiloExecution } from "./MissileSiloExecution"; import { NukeExecution } from "./NukeExecution"; @@ -144,6 +145,9 @@ export class ConstructionExecution implements Execution { case UnitType.Factory: this.mg.addExecution(new FactoryExecution(this.structure!)); break; + case UnitType.LandMine: + this.mg.addExecution(new LandMineExecution(this.structure!)); + break; default: console.warn( `unit type ${this.constructionType} cannot be constructed`, @@ -160,6 +164,7 @@ export class ConstructionExecution implements Execution { case UnitType.SAMLauncher: case UnitType.City: case UnitType.Factory: + case UnitType.LandMine: return true; default: return false; diff --git a/src/core/execution/LandMineExecution.ts b/src/core/execution/LandMineExecution.ts new file mode 100644 index 0000000000..10d706c149 --- /dev/null +++ b/src/core/execution/LandMineExecution.ts @@ -0,0 +1,184 @@ +import { + Execution, + Game, + isStructureType, + MessageType, + Player, + Unit, + UnitType, +} from "../game/Game"; +import { TileRef } from "../game/GameMap"; +import { PseudoRandom } from "../PseudoRandom"; + +const SPRITE_RADIUS = 16; + +export class LandMineExecution implements Execution { + private mg: Game; + private active: boolean = true; + private originalOwner: Player; + + constructor(private mine: Unit) { + this.originalOwner = mine.owner(); + } + + init(mg: Game, ticks: number): void { + this.mg = mg; + } + + activeDuringSpawnPhase(): boolean { + return false; + } + + tick(ticks: number): void { + if (!this.mine.isActive()) { + this.active = false; + return; + } + + // Do nothing while the structure is under construction + if (this.mine.isUnderConstruction()) { + return; + } + + // Check if the mine's tile has been captured by an enemy + const currentOwner = this.mg.owner(this.mine.tile()); + if (!currentOwner.isPlayer()) { + // Tile is now terra nullius, delete the mine + this.mine.delete(); + this.active = false; + return; + } + + // If the tile is still owned by the original owner, do nothing + if (currentOwner === this.originalOwner) { + return; + } + + // If captured by an ally of the original owner, transfer ownership + if (currentOwner.isFriendly(this.originalOwner)) { + // Update owner without detonating + this.originalOwner = currentOwner; + return; + } + + // An enemy has captured the tile - DETONATE! + this.detonate(currentOwner); + } + + private tilesToDestroy(attacker: Player): Set { + const magnitude = this.mg.config().nukeMagnitudes(UnitType.LandMine); + const rand = new PseudoRandom(this.mg.ticks()); + const inner2 = magnitude.inner * magnitude.inner; + const outer2 = magnitude.outer * magnitude.outer; + const tile = this.mine.tile(); + + // Only include tiles owned by the attacker + return this.mg.bfs(tile, (_, n: TileRef) => { + const owner = this.mg.owner(n); + if (!owner.isPlayer() || owner !== attacker) { + return false; + } + const d2 = this.mg.euclideanDistSquared(tile, n); + return d2 <= outer2 && (d2 <= inner2 || rand.chance(2)); + }); + } + + private detonate(attacker: Player) { + const magnitude = this.mg.config().nukeMagnitudes(UnitType.LandMine); + const tile = this.mine.tile(); + const toDestroy = this.tilesToDestroy(attacker); + + // Calculate max troops for death factor + const maxTroops = this.mg.config().maxTroops(attacker); + + // Only damage the attacker's territory and troops + for (const t of toDestroy) { + attacker.relinquish(t); + attacker.removeTroops( + this.mg + .config() + .nukeDeathFactor( + UnitType.AtomBomb, // Use atom bomb death factor calculation + attacker.troops(), + attacker.numTilesOwned(), + maxTroops, + ), + ); + + if (this.mg.isLand(t)) { + this.mg.setFallout(t, true); + } + } + + // Also damage attacker's outgoing attacks + attacker.outgoingAttacks().forEach((attack) => { + const deaths = this.mg + .config() + .nukeDeathFactor( + UnitType.AtomBomb, + attack.troops(), + attacker.numTilesOwned(), + maxTroops, + ); + attack.setTroops(attack.troops() - deaths); + }); + + // Destroy attacker's units in blast radius (excluding nukes in flight) + const outer2 = magnitude.outer * magnitude.outer; + for (const unit of this.mg.units()) { + // Skip units not owned by the attacker + if (unit.owner() !== attacker) { + continue; + } + + if ( + unit.type() !== UnitType.AtomBomb && + unit.type() !== UnitType.HydrogenBomb && + unit.type() !== UnitType.MIRVWarhead && + unit.type() !== UnitType.MIRV + ) { + if (this.mg.euclideanDistSquared(tile, unit.tile()) < outer2) { + unit.delete(true, this.originalOwner); + } + } + } + + // Notify the attacker + this.mg.displayMessage( + `You triggered a land mine placed by ${this.originalOwner.displayName()}!`, + MessageType.NUKE_INBOUND, + attacker.id(), + ); + + // Notify the mine owner + this.mg.displayMessage( + `Your land mine was triggered by ${attacker.displayName()}!`, + MessageType.CAPTURED_ENEMY_UNIT, + this.originalOwner.id(), + ); + + // Redraw buildings in the area + this.redrawBuildings(magnitude.outer + SPRITE_RADIUS); + + // Delete the mine + this.mine.delete(false); + this.active = false; + } + + private redrawBuildings(range: number) { + const tile = this.mine.tile(); + const rangeSquared = range * range; + for (const unit of this.mg.units()) { + if (isStructureType(unit.type())) { + if (this.mg.euclideanDistSquared(tile, unit.tile()) < rangeSquared) { + unit.touch(); + } + } + } + } + + isActive(): boolean { + return this.active; + } +} + diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index c7e452e5d3..df063af2c8 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -19,7 +19,7 @@ export class PlayerExecution implements Execution { private mg: Game; private active = true; - constructor(private player: Player) {} + constructor(private player: Player) { } activeDuringSpawnPhase(): boolean { return false; @@ -54,6 +54,9 @@ export class PlayerExecution implements Execution { if (u.isActive()) { captor.captureUnit(u); } + } else if (u.type() === UnitType.LandMine) { + // Land mines are handled by LandMineExecution - don't capture them + // The LandMineExecution will detect the ownership change and detonate } else { captor.captureUnit(u); } @@ -98,7 +101,7 @@ export class PlayerExecution implements Execution { if ( embargo.isTemporary && this.mg.ticks() - embargo.createdAt > - this.mg.config().temporaryEmbargoDuration() + this.mg.config().temporaryEmbargoDuration() ) { this.player.stopEmbargo(embargo.target); } diff --git a/src/core/execution/nation/structureSpawnTileValue.ts b/src/core/execution/nation/structureSpawnTileValue.ts index b01df682a2..70e0f9d8be 100644 --- a/src/core/execution/nation/structureSpawnTileValue.ts +++ b/src/core/execution/nation/structureSpawnTileValue.ts @@ -148,6 +148,47 @@ export function structureSpawnTileValue( return w; }; } + case UnitType.LandMine: { + // Land mines should be placed near borders where enemies are likely to attack + return (tile) => { + let w = 0; + + // Prefer to be close to the border (where enemies will attack) + const [closest, closestBorderDist] = closestTile(mg, borderTiles, tile); + if (closest !== null) { + // Prefer tiles close to the border but not right on it + w += Math.max(0, borderSpacing - closestBorderDist); + + // Prefer adjacent players who are hostile + const neighbors: Set = new Set(); + for (const neighborTile of mg.neighbors(closest)) { + if (!mg.isLand(neighborTile)) continue; + const id = mg.ownerID(neighborTile); + if (id === player.smallID()) continue; + const neighbor = mg.playerBySmallID(id); + if (!neighbor.isPlayer()) continue; + neighbors.add(neighbor); + } + for (const neighbor of neighbors) { + w += + borderSpacing * (Relation.Friendly - player.relation(neighbor)); + } + } + + // Prefer to be away from other land mines + const otherTiles: Set = new Set( + otherUnits.map((u) => u.tile()), + ); + otherTiles.delete(tile); + const closestOther = closestTwoTiles(mg, otherTiles, [tile]); + if (closestOther !== null) { + const d = mg.manhattanDist(closestOther.x, tile); + w += Math.min(d, structureSpacing); + } + + return w; + }; + } default: throw new Error(`Value function not implemented for ${type}`); } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index cefcc90e87..66dd519f86 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -208,6 +208,7 @@ export interface UnitInfo { upgradable?: boolean; canBuildTrainStation?: boolean; experimental?: boolean; + visibleToEnemies?: boolean; } export enum UnitType { @@ -227,6 +228,7 @@ export enum UnitType { MIRVWarhead = "MIRV Warhead", Train = "Train", Factory = "Factory", + LandMine = "Land Mine", } export enum TrainType { @@ -242,6 +244,7 @@ const _structureTypes: ReadonlySet = new Set([ UnitType.MissileSilo, UnitType.Port, UnitType.Factory, + UnitType.LandMine, ]); export function isStructureType(type: UnitType): boolean { @@ -310,6 +313,8 @@ export interface UnitParamsMap { [UnitType.MIRVWarhead]: { targetTile?: number; }; + + [UnitType.LandMine]: Record; } // Type helper to get params type for a specific unit type @@ -335,7 +340,7 @@ export class Nation { constructor( public readonly spawnCell: Cell | undefined, public readonly playerInfo: PlayerInfo, - ) {} + ) { } } export class Cell { diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 09c02c5a72..faa802ae04 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -1003,6 +1003,7 @@ export class PlayerImpl implements Player { case UnitType.SAMLauncher: case UnitType.City: case UnitType.Factory: + case UnitType.LandMine: return this.landBasedStructureSpawn(targetTile, validTiles); default: assertNever(unitType); diff --git a/tests/LandMine.test.ts b/tests/LandMine.test.ts new file mode 100644 index 0000000000..ca4865e49f --- /dev/null +++ b/tests/LandMine.test.ts @@ -0,0 +1,458 @@ +import { AttackExecution } from "../src/core/execution/AttackExecution"; +import { ConstructionExecution } from "../src/core/execution/ConstructionExecution"; +import { SpawnExecution } from "../src/core/execution/SpawnExecution"; +import { + Game, + GameUpdates, + Player, + PlayerInfo, + PlayerType, + UnitType, +} from "../src/core/game/Game"; +import { TileRef } from "../src/core/game/GameMap"; +import { GameUpdateType, UnitUpdate } from "../src/core/game/GameUpdates"; +import { GameID } from "../src/core/Schemas"; +import { setup } from "./util/Setup"; +import { constructionExecution, executeTicks } from "./util/utils"; + +const gameID: GameID = "game_id"; +let game: Game; +let defender: Player; +let attacker: Player; +let defenderSpawn: TileRef; +let attackerSpawn: TileRef; + +describe("LandMine", () => { + beforeEach(async () => { + game = await setup("plains", { + infiniteGold: true, + instantBuild: true, + infiniteTroops: true, + }); + + const defenderInfo = new PlayerInfo( + "defender", + PlayerType.Human, + null, + "defender_id", + ); + game.addPlayer(defenderInfo); + + const attackerInfo = new PlayerInfo( + "attacker", + PlayerType.Human, + null, + "attacker_id", + ); + game.addPlayer(attackerInfo); + + defenderSpawn = game.ref(10, 10); + attackerSpawn = game.ref(20, 10); + + game.addExecution( + new SpawnExecution( + gameID, + game.player(defenderInfo.id).info(), + defenderSpawn, + ), + new SpawnExecution( + gameID, + game.player(attackerInfo.id).info(), + attackerSpawn, + ), + ); + + while (game.inSpawnPhase()) { + game.executeNextTick(); + } + + defender = game.player(defenderInfo.id); + attacker = game.player(attackerInfo.id); + + game.addExecution( + new AttackExecution(50, defender, game.terraNullius().id()), + ); + game.addExecution( + new AttackExecution(50, attacker, game.terraNullius().id()), + ); + + for (let i = 0; i < 50; i++) { + game.executeNextTick(); + } + }); + + test("land mine should be buildable", async () => { + constructionExecution(game, defender, 10, 10, UnitType.LandMine); + expect(defender.units(UnitType.LandMine)).toHaveLength(1); + }); + + test("land mine should explode when enemy captures the tile", async () => { + constructionExecution(game, defender, 10, 10, UnitType.LandMine); + const mine = defender.units(UnitType.LandMine)[0]; + expect(mine).toBeDefined(); + expect(mine.isActive()).toBe(true); + + const mineTile = mine.tile(); + + game.addExecution(new AttackExecution(100, attacker, defender.id())); + + let ticks = 0; + const maxTicks = 500; + while (game.owner(mineTile) !== attacker && ticks < maxTicks) { + game.executeNextTick(); + ticks++; + } + + executeTicks(game, 5); + + expect(mine.isActive()).toBe(false); + }); + + test("land mine explosion should only hurt the attacker, not the original owner", async () => { + constructionExecution(game, defender, 10, 10, UnitType.LandMine); + const mine = defender.units(UnitType.LandMine)[0]; + const mineTile = mine.tile(); + + const defenderInitialTiles = defender.numTilesOwned(); + + game.addExecution(new AttackExecution(100, attacker, defender.id())); + + let ticks = 0; + const maxTicks = 500; + while (game.owner(mineTile) !== attacker && ticks < maxTicks) { + game.executeNextTick(); + ticks++; + } + + const defenderTilesAfterCapture = defender.numTilesOwned(); + + executeTicks(game, 5); + + expect(defender.numTilesOwned()).toBeGreaterThanOrEqual( + defenderTilesAfterCapture - 5, + ); + }); + + test("land mine should NOT explode while under construction", async () => { + const slowBuildGame = await setup("plains", { + infiniteGold: true, + instantBuild: false, + infiniteTroops: true, + }); + + const defenderInfo2 = new PlayerInfo( + "defender2", + PlayerType.Human, + null, + "defender2_id", + ); + slowBuildGame.addPlayer(defenderInfo2); + + const attackerInfo2 = new PlayerInfo( + "attacker2", + PlayerType.Human, + null, + "attacker2_id", + ); + slowBuildGame.addPlayer(attackerInfo2); + + slowBuildGame.addExecution( + new SpawnExecution( + gameID, + slowBuildGame.player(defenderInfo2.id).info(), + slowBuildGame.ref(10, 10), + ), + new SpawnExecution( + gameID, + slowBuildGame.player(attackerInfo2.id).info(), + slowBuildGame.ref(20, 10), + ), + ); + + while (slowBuildGame.inSpawnPhase()) { + slowBuildGame.executeNextTick(); + } + + const defender2 = slowBuildGame.player(defenderInfo2.id); + const attacker2 = slowBuildGame.player(attackerInfo2.id); + + slowBuildGame.addExecution( + new AttackExecution(50, defender2, slowBuildGame.terraNullius().id()), + ); + slowBuildGame.addExecution( + new AttackExecution(50, attacker2, slowBuildGame.terraNullius().id()), + ); + + for (let i = 0; i < 30; i++) { + slowBuildGame.executeNextTick(); + } + + slowBuildGame.addExecution( + new ConstructionExecution( + defender2, + UnitType.LandMine, + slowBuildGame.ref(10, 10), + ), + ); + + slowBuildGame.executeNextTick(); + slowBuildGame.executeNextTick(); + + const mines = defender2.units(UnitType.LandMine); + expect(mines).toHaveLength(1); + const mine = mines[0]; + expect(mine.isUnderConstruction()).toBe(true); + + const mineTile = mine.tile(); + + const attackerTilesBefore = attacker2.numTilesOwned(); + + slowBuildGame.addExecution( + new AttackExecution(100, attacker2, defender2.id()), + ); + + let ticks = 0; + const maxTicks = 500; + while (slowBuildGame.owner(mineTile) !== attacker2 && ticks < maxTicks) { + slowBuildGame.executeNextTick(); + ticks++; + } + + executeTicks(slowBuildGame, 5); + + expect(attacker2.numTilesOwned()).toBeGreaterThan(attackerTilesBefore - 20); + }); + + test("land mine should NOT explode when captured by ally", async () => { + const allyInfo = new PlayerInfo("ally", PlayerType.Human, null, "ally_id"); + game.addPlayer(allyInfo); + + game.addExecution( + new SpawnExecution(gameID, allyInfo, game.ref(10, 20)), + ); + + for (let i = 0; i < 10; i++) { + game.executeNextTick(); + } + + const ally = game.player(allyInfo.id); + + const allianceRequest = defender.createAllianceRequest(ally); + if (allianceRequest) { + allianceRequest.accept(); + } + + expect(defender.isAlliedWith(ally)).toBe(true); + + constructionExecution(game, defender, 10, 10, UnitType.LandMine); + const mine = defender.units(UnitType.LandMine)[0]; + expect(mine).toBeDefined(); + + const mineTile = mine.tile(); + + ally.conquer(mineTile); + + executeTicks(game, 5); + + const allyTiles = ally.numTilesOwned(); + expect(allyTiles).toBeGreaterThan(0); + }); + + test("land mine detonation destroys the mine unit", async () => { + constructionExecution(game, defender, 10, 10, UnitType.LandMine); + const mine = defender.units(UnitType.LandMine)[0]; + const mineTile = mine.tile(); + + game.addExecution(new AttackExecution(100, attacker, defender.id())); + + let ticks = 0; + const maxTicks = 500; + while (game.owner(mineTile) !== attacker && ticks < maxTicks) { + game.executeNextTick(); + ticks++; + } + + executeTicks(game, 5); + + expect(mine.isActive()).toBe(false); + expect(defender.units(UnitType.LandMine)).toHaveLength(0); + }); + + test("land mine has same cost as defense post", async () => { + const config = game.config(); + const landMineCost = config.unitInfo(UnitType.LandMine).cost(game, defender); + const defensePostCost = config + .unitInfo(UnitType.DefensePost) + .cost(game, defender); + + expect(landMineCost).toEqual(defensePostCost); + }); + + test("land mine is a territory-bound structure", async () => { + constructionExecution(game, defender, 10, 10, UnitType.LandMine); + const mine = defender.units(UnitType.LandMine)[0]; + + expect(mine.info().territoryBound).toBe(true); + }); + + test("land mine should not be visible to enemies", async () => { + constructionExecution(game, defender, 10, 10, UnitType.LandMine); + const mine = defender.units(UnitType.LandMine)[0]; + + expect(mine.info().visibleToEnemies).toBe(false); + }); + + test("land mine visibility property is configured correctly in config", async () => { + const config = game.config(); + const landMineInfo = config.unitInfo(UnitType.LandMine); + + expect(landMineInfo.visibleToEnemies).toBe(false); + + const defensePostInfo = config.unitInfo(UnitType.DefensePost); + expect(defensePostInfo.visibleToEnemies).toBeUndefined(); + }); + + test("server should not send land mine unit updates to enemies", async () => { + game.addExecution( + new ConstructionExecution(defender, UnitType.LandMine, game.ref(10, 10)), + ); + + let allUnitUpdates: UnitUpdate[] = []; + for (let i = 0; i < 10; i++) { + const updates: GameUpdates = game.executeNextTick(); + allUnitUpdates = allUnitUpdates.concat(updates[GameUpdateType.Unit]); + } + + const mine = defender.units(UnitType.LandMine)[0]; + expect(mine).toBeDefined(); + + const landMineUpdates = allUnitUpdates.filter( + (u: UnitUpdate) => u.unitType === UnitType.LandMine, + ); + expect(landMineUpdates.length).toBeGreaterThan(0); + + const attackerSmallID = attacker.smallID(); + + const filteredForAttacker = allUnitUpdates.filter( + (unitUpdate: UnitUpdate) => { + const unitInfo = game.config().unitInfo(unitUpdate.unitType); + + if (unitInfo.visibleToEnemies !== false) { + return true; + } + + if (attackerSmallID === unitUpdate.ownerID) { + return true; + } + + const owner = game.playerBySmallID(unitUpdate.ownerID); + if (owner.isPlayer() && attacker.isAlliedWith(owner)) { + return true; + } + + return false; + }, + ); + + const attackerLandMineUpdates = filteredForAttacker.filter( + (u: UnitUpdate) => u.unitType === UnitType.LandMine, + ); + expect(attackerLandMineUpdates).toHaveLength(0); + + const defenderSmallID = defender.smallID(); + + const filteredForDefender = allUnitUpdates.filter( + (unitUpdate: UnitUpdate) => { + const unitInfo = game.config().unitInfo(unitUpdate.unitType); + + if (unitInfo.visibleToEnemies !== false) { + return true; + } + + if (defenderSmallID === unitUpdate.ownerID) { + return true; + } + + const owner = game.playerBySmallID(unitUpdate.ownerID); + if (owner.isPlayer() && defender.isAlliedWith(owner)) { + return true; + } + + return false; + }, + ); + + const defenderLandMineUpdates = filteredForDefender.filter( + (u: UnitUpdate) => u.unitType === UnitType.LandMine, + ); + expect(defenderLandMineUpdates.length).toBeGreaterThan(0); + }); + + test("allied players should receive land mine updates from allies", async () => { + const allyInfo = new PlayerInfo( + "ally", + PlayerType.Human, + "ally_client", + "ally_id", + ); + game.addPlayer(allyInfo); + + game.addExecution(new SpawnExecution(gameID, allyInfo, game.ref(10, 20))); + + for (let i = 0; i < 10; i++) { + game.executeNextTick(); + } + + const ally = game.player(allyInfo.id); + + const allianceRequest = defender.createAllianceRequest(ally); + if (allianceRequest) { + allianceRequest.accept(); + } + expect(defender.isAlliedWith(ally)).toBe(true); + + game.addExecution( + new ConstructionExecution(defender, UnitType.LandMine, game.ref(10, 10)), + ); + + let allUnitUpdates: UnitUpdate[] = []; + for (let i = 0; i < 10; i++) { + const updates: GameUpdates = game.executeNextTick(); + allUnitUpdates = allUnitUpdates.concat(updates[GameUpdateType.Unit]); + } + + const mine = defender.units(UnitType.LandMine)[0]; + expect(mine).toBeDefined(); + + const landMineUpdates = allUnitUpdates.filter( + (u: UnitUpdate) => u.unitType === UnitType.LandMine, + ); + expect(landMineUpdates.length).toBeGreaterThan(0); + + const allySmallID = ally.smallID(); + + const filteredForAlly = allUnitUpdates.filter((unitUpdate: UnitUpdate) => { + const unitInfo = game.config().unitInfo(unitUpdate.unitType); + + if (unitInfo.visibleToEnemies !== false) { + return true; + } + + if (allySmallID === unitUpdate.ownerID) { + return true; + } + + const owner = game.playerBySmallID(unitUpdate.ownerID); + if (owner.isPlayer() && ally.isAlliedWith(owner)) { + return true; + } + + return false; + }); + + const allyLandMineUpdates = filteredForAlly.filter( + (u: UnitUpdate) => u.unitType === UnitType.LandMine, + ); + expect(allyLandMineUpdates.length).toBeGreaterThan(0); + }); +}); From bf731a1d7af6769e006d3352bf818f7fa6106ebf Mon Sep 17 00:00:00 2001 From: Ryan Huellen Date: Tue, 6 Jan 2026 20:05:23 -0600 Subject: [PATCH 02/12] chore: added build --- resources/lang/bg.json | 2 ++ resources/lang/el.json | 2 ++ resources/lang/en.json | 2 ++ resources/lang/fa.json | 2 ++ resources/lang/fr.json | 2 ++ resources/lang/ja.json | 2 ++ resources/lang/nl.json | 2 ++ resources/lang/pl.json | 2 ++ resources/lang/ru.json | 2 ++ resources/lang/uk.json | 2 ++ resources/lang/zh-CN.json | 2 ++ src/client/InputHandler.ts | 6 ++++++ src/client/UserSettingModal.ts | 9 +++++++++ src/client/graphics/layers/UnitDisplay.ts | 11 +++++++++++ 14 files changed, 48 insertions(+) diff --git a/resources/lang/bg.json b/resources/lang/bg.json index d8963e4d58..d2d36c45c5 100644 --- a/resources/lang/bg.json +++ b/resources/lang/bg.json @@ -369,6 +369,8 @@ "build_factory_desc": "Изграждане на фабрика под курсора Ви.", "build_defense_post": "Изграждане на отбранителен пост", "build_defense_post_desc": "Изграждане на отбранителен пост под курсора Ви.", + "build_land_mine": "Изграждане на мина", + "build_land_mine_desc": "Изграждане на мина под курсора Ви.", "build_port": "Изграждане на пристанище", "build_port_desc": "Изграждане на пристанище под курсора Ви.", "build_warship": "Изграждане на боен кораб", diff --git a/resources/lang/el.json b/resources/lang/el.json index fa7754a2b1..565fb4ef65 100644 --- a/resources/lang/el.json +++ b/resources/lang/el.json @@ -349,6 +349,8 @@ "build_factory_desc": "Κτίσε ένα Εργοστάσιο κάτω από τον δείκτη σου.", "build_defense_post": "Κατασκευή Φρουρίου", "build_defense_post_desc": "Κτίσε ένα Φρούριο κάτω από τον δείκτη σου.", + "build_land_mine": "Κατασκευή Νάρκης", + "build_land_mine_desc": "Κτίσε μια Νάρκη κάτω από τον δείκτη σου.", "build_port": "Κατασκευή Λιμανιού", "build_port_desc": "Κτίσε ένα Λιμάνι κάτω από τον δείκτη σου.", "build_warship": "Κατασκευή Πολεμικού Πλοίου", diff --git a/resources/lang/en.json b/resources/lang/en.json index da028e94f0..a2a02fe125 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -426,6 +426,8 @@ "build_factory_desc": "Build a Factory under your cursor.", "build_defense_post": "Build Defense Post", "build_defense_post_desc": "Build a Defense Post under your cursor.", + "build_land_mine": "Build Land Mine", + "build_land_mine_desc": "Build a Land Mine under your cursor.", "build_port": "Build Port", "build_port_desc": "Build a Port under your cursor.", "build_warship": "Build Warship", diff --git a/resources/lang/fa.json b/resources/lang/fa.json index ef740ad085..65905fd5a2 100644 --- a/resources/lang/fa.json +++ b/resources/lang/fa.json @@ -369,6 +369,8 @@ "build_factory_desc": "کارخانه ای در زیر مکان‌نما بسازید.", "build_defense_post": "ساخت پست دفاعی", "build_defense_post_desc": "یک پست دفاعی در زیر مکان‌نما بسازید.", + "build_land_mine": "ساخت مین زمینی", + "build_land_mine_desc": "یک مین زمینی در زیر مکان‌نما بسازید.", "build_port": "ساخت بندر", "build_port_desc": "یک بندر در زیر مکان‌نما بسازید.", "build_warship": "کشتی‌های جنگی بساز", diff --git a/resources/lang/fr.json b/resources/lang/fr.json index 3c54e4b347..69e42f9382 100644 --- a/resources/lang/fr.json +++ b/resources/lang/fr.json @@ -369,6 +369,8 @@ "build_factory_desc": "Construire une ville sous votre curseur.", "build_defense_post": "Construire un poste de défense", "build_defense_post_desc": "Construire un poste de défense sous votre curseur.", + "build_land_mine": "Construire une mine terrestre", + "build_land_mine_desc": "Construire une mine terrestre sous votre curseur.", "build_port": "Construire un port", "build_port_desc": "Construire un port sous votre curseur.", "build_warship": "Construire un navire de guerre", diff --git a/resources/lang/ja.json b/resources/lang/ja.json index 84180822e1..0d738ff6a9 100644 --- a/resources/lang/ja.json +++ b/resources/lang/ja.json @@ -369,6 +369,8 @@ "build_factory_desc": "選択した位置に工場を建設します。", "build_defense_post": "防衛ポストを建設", "build_defense_post_desc": "選択した位置に防衛ポストを建設します。", + "build_land_mine": "地雷を建設", + "build_land_mine_desc": "選択した位置に地雷を建設します。", "build_port": "港を建設", "build_port_desc": "選択した位置に港を建設します。", "build_warship": "戦艦を建造", diff --git a/resources/lang/nl.json b/resources/lang/nl.json index bb27b716ac..4ebb3f9455 100644 --- a/resources/lang/nl.json +++ b/resources/lang/nl.json @@ -369,6 +369,8 @@ "build_factory_desc": "Bouw een Fabriek onder je cursor.", "build_defense_post": "Bouw Verdedigingspost", "build_defense_post_desc": "Bouw een Verdedigingspost onder je cursor.", + "build_land_mine": "Bouw Landmijn", + "build_land_mine_desc": "Bouw een Landmijn onder je cursor.", "build_port": "Bouw Haven", "build_port_desc": "Bouw een Haven onder je cursor.", "build_warship": "Bouw Oorlogsschip", diff --git a/resources/lang/pl.json b/resources/lang/pl.json index 8a3504a0aa..ac969f62f5 100644 --- a/resources/lang/pl.json +++ b/resources/lang/pl.json @@ -349,6 +349,8 @@ "build_factory_desc": "Zbuduj fabrykę pod twoim kursorem.", "build_defense_post": "Buduj posterunek obronny", "build_defense_post_desc": "Zbuduj posterunek obronny pod twoim kursorem.", + "build_land_mine": "Zbuduj minę lądową", + "build_land_mine_desc": "Zbuduj minę lądową pod twoim kursorem.", "build_port": "Zbuduj port", "build_port_desc": "Zbuduj port pod twoim kursorem.", "build_warship": "Zbuduj okręt wojenny", diff --git a/resources/lang/ru.json b/resources/lang/ru.json index 10395607aa..8484c84b41 100644 --- a/resources/lang/ru.json +++ b/resources/lang/ru.json @@ -369,6 +369,8 @@ "build_factory_desc": "Разместить фабрику под указателем.", "build_defense_post": "Разместить укрепление", "build_defense_post_desc": "Разместить укрепление под указателем.", + "build_land_mine": "Разместить мину", + "build_land_mine_desc": "Разместить мину под указателем.", "build_port": "Разместить порт", "build_port_desc": "Разместить порт под указателем.", "build_warship": "Разместить военный корабль", diff --git a/resources/lang/uk.json b/resources/lang/uk.json index 5cde589665..ce5b62b84b 100644 --- a/resources/lang/uk.json +++ b/resources/lang/uk.json @@ -369,6 +369,8 @@ "build_factory_desc": "Будувати фабрику під указівником.", "build_defense_post": "Розмістити пункт оборони", "build_defense_post_desc": "Розмістити пункт оборони під указівником.", + "build_land_mine": "Розмістити міну", + "build_land_mine_desc": "Розмістити міну під указівником.", "build_port": "Розмістити порт", "build_port_desc": "Розмістити порт під указівником.", "build_warship": "Розмістити військовий корабель", diff --git a/resources/lang/zh-CN.json b/resources/lang/zh-CN.json index bff2cae129..13a2f4059c 100644 --- a/resources/lang/zh-CN.json +++ b/resources/lang/zh-CN.json @@ -369,6 +369,8 @@ "build_factory_desc": "在鼠标位置建造工厂。", "build_defense_post": "建造要塞", "build_defense_post_desc": "在鼠标位置建造要塞。", + "build_land_mine": "建造地雷", + "build_land_mine_desc": "在鼠标位置建造地雷。", "build_port": "建造港口", "build_port_desc": "在鼠标位置建造港口。", "build_warship": "部署军舰", diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index d01798445b..f7bd5dc507 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -211,6 +211,7 @@ export class InputHandler { buildFactory: "Digit2", buildPort: "Digit3", buildDefensePost: "Digit4", + buildLandMine: "Minus", buildMissileSilo: "Digit5", buildSamLauncher: "Digit6", buildWarship: "Digit7", @@ -402,6 +403,11 @@ export class InputHandler { this.setGhostStructure(UnitType.DefensePost); } + if (e.code === this.keybinds.buildLandMine) { + e.preventDefault(); + this.setGhostStructure(UnitType.LandMine); + } + if (e.code === this.keybinds.buildMissileSilo) { e.preventDefault(); this.setGhostStructure(UnitType.MissileSilo); diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index 39f2967c77..a65b3ab9a4 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -484,6 +484,15 @@ export class UserSettingModal extends LitElement { @change=${this.handleKeybindChange} > + + Date: Tue, 6 Jan 2026 20:19:56 -0600 Subject: [PATCH 03/12] fix: land mines should be destroyed when captured if they are still being built --- src/core/execution/PlayerExecution.ts | 5 +- tests/LandMine.test.ts | 94 +++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index df063af2c8..d09f68e7e6 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -55,8 +55,9 @@ export class PlayerExecution implements Execution { captor.captureUnit(u); } } else if (u.type() === UnitType.LandMine) { - // Land mines are handled by LandMineExecution - don't capture them - // The LandMineExecution will detect the ownership change and detonate + if (u.isUnderConstruction()) { + u.delete(); + } } else { captor.captureUnit(u); } diff --git a/tests/LandMine.test.ts b/tests/LandMine.test.ts index ca4865e49f..dc1ceabaca 100644 --- a/tests/LandMine.test.ts +++ b/tests/LandMine.test.ts @@ -223,6 +223,100 @@ describe("LandMine", () => { expect(attacker2.numTilesOwned()).toBeGreaterThan(attackerTilesBefore - 20); }); + test("land mine under construction should be destroyed when captured", async () => { + const slowBuildGame = await setup("plains", { + infiniteGold: true, + instantBuild: false, + infiniteTroops: true, + }); + + const defenderInfo2 = new PlayerInfo( + "defender2", + PlayerType.Human, + null, + "defender2_id", + ); + slowBuildGame.addPlayer(defenderInfo2); + + const attackerInfo2 = new PlayerInfo( + "attacker2", + PlayerType.Human, + null, + "attacker2_id", + ); + slowBuildGame.addPlayer(attackerInfo2); + + slowBuildGame.addExecution( + new SpawnExecution( + gameID, + slowBuildGame.player(defenderInfo2.id).info(), + slowBuildGame.ref(10, 10), + ), + new SpawnExecution( + gameID, + slowBuildGame.player(attackerInfo2.id).info(), + slowBuildGame.ref(20, 10), + ), + ); + + while (slowBuildGame.inSpawnPhase()) { + slowBuildGame.executeNextTick(); + } + + const defender2 = slowBuildGame.player(defenderInfo2.id); + const attacker2 = slowBuildGame.player(attackerInfo2.id); + + slowBuildGame.addExecution( + new AttackExecution(50, defender2, slowBuildGame.terraNullius().id()), + ); + slowBuildGame.addExecution( + new AttackExecution(50, attacker2, slowBuildGame.terraNullius().id()), + ); + + for (let i = 0; i < 30; i++) { + slowBuildGame.executeNextTick(); + } + + slowBuildGame.addExecution( + new ConstructionExecution( + defender2, + UnitType.LandMine, + slowBuildGame.ref(10, 10), + ), + ); + + slowBuildGame.executeNextTick(); + slowBuildGame.executeNextTick(); + + const mines = defender2.units(UnitType.LandMine); + expect(mines).toHaveLength(1); + const mine = mines[0]; + expect(mine.isUnderConstruction()).toBe(true); + + const mineTile = mine.tile(); + + slowBuildGame.addExecution( + new AttackExecution(100, attacker2, defender2.id()), + ); + + let ticks = 0; + const maxTicks = 500; + while (slowBuildGame.owner(mineTile) !== attacker2 && ticks < maxTicks) { + slowBuildGame.executeNextTick(); + ticks++; + } + + expect(slowBuildGame.owner(mineTile)).toBe(attacker2); + + executeTicks(slowBuildGame, 5); + + expect(mine.isActive()).toBe(false); + + expect(attacker2.units(UnitType.LandMine)).toHaveLength(0); + + expect(defender2.units(UnitType.LandMine)).toHaveLength(0); + }); + test("land mine should NOT explode when captured by ally", async () => { const allyInfo = new PlayerInfo("ally", PlayerType.Human, null, "ally_id"); game.addPlayer(allyInfo); From 6e246a8c262ade4d785a7930e5497a00d150d53e Mon Sep 17 00:00:00 2001 From: Ryan Huellen Date: Tue, 6 Jan 2026 20:46:33 -0600 Subject: [PATCH 04/12] feat: better structure icon --- resources/images/buildings/landMine1.png | Bin 0 -> 364 bytes .../graphics/layers/StructureDrawingUtils.ts | 2 +- src/client/graphics/layers/StructureLayer.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 resources/images/buildings/landMine1.png diff --git a/resources/images/buildings/landMine1.png b/resources/images/buildings/landMine1.png new file mode 100644 index 0000000000000000000000000000000000000000..dc433d6bbdaa0d1b4094c41336b1d792f061d171 GIT binary patch literal 364 zcmV-y0h9iTP)`ZkW#YQ- zZb>vX%QEfY&&TniC<0@wj3*bgt;5MgR-JDG?I4b0;d!38)Vl%HL1~1wwhO=j0000< KMNUMnLSTZku9ols literal 0 HcmV?d00001 diff --git a/src/client/graphics/layers/StructureDrawingUtils.ts b/src/client/graphics/layers/StructureDrawingUtils.ts index d3345067ac..76fae9ef2b 100644 --- a/src/client/graphics/layers/StructureDrawingUtils.ts +++ b/src/client/graphics/layers/StructureDrawingUtils.ts @@ -6,7 +6,7 @@ import { TransformHandler } from "../TransformHandler"; import anchorIcon from "/images/AnchorIcon.png?url"; import cityIcon from "/images/CityIcon.png?url"; import factoryIcon from "/images/FactoryUnit.png?url"; -import landMineIcon from "/images/buildings/fortAlt2.png?url"; +import landMineIcon from "/images/buildings/landMine1.png?url"; import missileSiloIcon from "/images/MissileSiloUnit.png?url"; import SAMMissileIcon from "/images/SamLauncherUnit.png?url"; import shieldIcon from "/images/ShieldIcon.png?url"; diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index ac0100320d..9842c83b07 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -10,7 +10,7 @@ import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameView, UnitView } from "../../../core/game/GameView"; import cityIcon from "/images/buildings/cityAlt1.png?url"; import factoryIcon from "/images/buildings/factoryAlt1.png?url"; -import landMineIcon from "/images/buildings/fortAlt2.png?url"; +import landMineIcon from "/images/buildings/landMine1.png?url"; import shieldIcon from "/images/buildings/fortAlt3.png?url"; import anchorIcon from "/images/buildings/port1.png?url"; import missileSiloIcon from "/images/buildings/silo1.png?url"; From 86097af0eebfa690b96853725bddd16bbc35cf7a Mon Sep 17 00:00:00 2001 From: Ryan Huellen Date: Tue, 6 Jan 2026 20:46:39 -0600 Subject: [PATCH 05/12] fix: updated cost to 75k base --- src/core/configuration/DefaultConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index de8836f46b..9647273482 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -564,7 +564,7 @@ export class DefaultConfig implements Config { case UnitType.LandMine: return { cost: this.costWrapper( - (numUnits: number) => Math.min(250_000, (numUnits + 1) * 50_000), + (numUnits: number) => Math.min(250_000, 75_000 + numUnits * 50_000), UnitType.LandMine, ), territoryBound: true, From 36e20a68e9181522c15ac9d3e29cf17a39b42658 Mon Sep 17 00:00:00 2001 From: Ryan Huellen Date: Tue, 6 Jan 2026 20:47:15 -0600 Subject: [PATCH 06/12] style: prettier --- resources/lang/de.json | 2 +- resources/lang/en.json | 2 +- resources/lang/fr.json | 2 +- resources/lang/ja.json | 2 +- resources/lang/ko.json | 2 +- resources/lang/nl.json | 2 +- resources/lang/pl.json | 2 +- resources/lang/ru.json | 2 +- resources/lang/tr.json | 2 +- resources/lang/uk.json | 2 +- resources/lang/zh-CN.json | 2 +- src/client/graphics/layers/BuildMenu.ts | 46 +++++++++---------- .../graphics/layers/StructureDrawingUtils.ts | 16 +++---- .../graphics/layers/StructureIconsLayer.ts | 8 ++-- src/client/graphics/layers/StructureLayer.ts | 2 +- src/core/GameRunner.ts | 2 +- src/core/configuration/DefaultConfig.ts | 14 +++--- src/core/execution/LandMineExecution.ts | 15 +++--- src/core/execution/PlayerExecution.ts | 4 +- src/core/game/Game.ts | 2 +- tests/LandMine.test.ts | 8 ++-- 21 files changed, 68 insertions(+), 71 deletions(-) diff --git a/resources/lang/de.json b/resources/lang/de.json index 2185af7a6f..512c1dbcaf 100644 --- a/resources/lang/de.json +++ b/resources/lang/de.json @@ -541,4 +541,4 @@ "grogu": "Grogu" } } -} \ No newline at end of file +} diff --git a/resources/lang/en.json b/resources/lang/en.json index a2a02fe125..7049a0c796 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -874,4 +874,4 @@ "mode_ffa": "Free-for-All", "mode_team": "Team" } -} \ No newline at end of file +} diff --git a/resources/lang/fr.json b/resources/lang/fr.json index 69e42f9382..9996beb82c 100644 --- a/resources/lang/fr.json +++ b/resources/lang/fr.json @@ -809,4 +809,4 @@ "mode_ffa": "Chacun pour soi", "mode_team": "En équipe" } -} \ No newline at end of file +} diff --git a/resources/lang/ja.json b/resources/lang/ja.json index 0d738ff6a9..d990dc9504 100644 --- a/resources/lang/ja.json +++ b/resources/lang/ja.json @@ -809,4 +809,4 @@ "mode_ffa": "デスマッチ", "mode_team": "チーム" } -} \ No newline at end of file +} diff --git a/resources/lang/ko.json b/resources/lang/ko.json index 0fabdf49de..60b965cbef 100644 --- a/resources/lang/ko.json +++ b/resources/lang/ko.json @@ -582,4 +582,4 @@ "not_authorized": "이 웹사이트에 접근할 권한이 없습니다.", "contact_admin": "이 메시지가 잘못 표시되었다고 생각되면 웹사이트 관리자에게 문의하십시오." } -} \ No newline at end of file +} diff --git a/resources/lang/nl.json b/resources/lang/nl.json index 4ebb3f9455..2c0dd3eea5 100644 --- a/resources/lang/nl.json +++ b/resources/lang/nl.json @@ -809,4 +809,4 @@ "mode_ffa": "Iedereen tegen elkaar", "mode_team": "Team" } -} \ No newline at end of file +} diff --git a/resources/lang/pl.json b/resources/lang/pl.json index ac969f62f5..274ee96a3d 100644 --- a/resources/lang/pl.json +++ b/resources/lang/pl.json @@ -764,4 +764,4 @@ "mode_ffa": "Każdy na Każdego", "mode_team": "Drużyna" } -} \ No newline at end of file +} diff --git a/resources/lang/ru.json b/resources/lang/ru.json index 8484c84b41..9aa50b51fa 100644 --- a/resources/lang/ru.json +++ b/resources/lang/ru.json @@ -809,4 +809,4 @@ "mode_ffa": "Все против всех", "mode_team": "Команда" } -} \ No newline at end of file +} diff --git a/resources/lang/tr.json b/resources/lang/tr.json index 57ec0f76ea..98b1b190be 100644 --- a/resources/lang/tr.json +++ b/resources/lang/tr.json @@ -647,4 +647,4 @@ "delete_unit_title": "Birimi Sil", "delete_unit_description": "En yakın birimi silmek için tıklayın" } -} \ No newline at end of file +} diff --git a/resources/lang/uk.json b/resources/lang/uk.json index ce5b62b84b..1d0e6b1949 100644 --- a/resources/lang/uk.json +++ b/resources/lang/uk.json @@ -809,4 +809,4 @@ "mode_ffa": "Усі проти всіх", "mode_team": "Команда" } -} \ No newline at end of file +} diff --git a/resources/lang/zh-CN.json b/resources/lang/zh-CN.json index 13a2f4059c..cd8beda1ff 100644 --- a/resources/lang/zh-CN.json +++ b/resources/lang/zh-CN.json @@ -809,4 +809,4 @@ "mode_ffa": "混战", "mode_team": "团队" } -} \ No newline at end of file +} diff --git a/src/client/graphics/layers/BuildMenu.ts b/src/client/graphics/layers/BuildMenu.ts index f58c8533d8..263e8e788b 100644 --- a/src/client/graphics/layers/BuildMenu.ts +++ b/src/client/graphics/layers/BuildMenu.ts @@ -26,9 +26,9 @@ import { UIState } from "../UIState"; import { Layer } from "./Layer"; import warshipIcon from "/images/BattleshipIconWhite.svg?url"; import cityIcon from "/images/CityIconWhite.svg?url"; +import landMineIcon from "/images/ExplosionIconWhite.svg?url"; import factoryIcon from "/images/FactoryIconWhite.svg?url"; import goldCoinIcon from "/images/GoldCoinIcon.svg?url"; -import landMineIcon from "/images/ExplosionIconWhite.svg?url"; import mirvIcon from "/images/MIRVIcon.svg?url"; import missileSiloIcon from "/images/MissileSiloIconWhite.svg?url"; import hydrogenBombIcon from "/images/MushroomCloudIconWhite.svg?url"; @@ -407,7 +407,7 @@ export class BuildMenu extends LitElement implements Layer { } else if (buildableUnit.canBuild) { const rocketDirectionUp = buildableUnit.type === UnitType.AtomBomb || - buildableUnit.type === UnitType.HydrogenBomb + buildableUnit.type === UnitType.HydrogenBomb ? this.uiState.rocketDirectionUp : undefined; this.eventBus.emit( @@ -424,27 +424,27 @@ export class BuildMenu extends LitElement implements Layer { @contextmenu=${(e: MouseEvent) => e.preventDefault()} > ${this.filteredBuildTable.map( - (row) => html` + (row) => html`
${row.map((item) => { - const buildableUnit = this.playerActions?.buildableUnits.find( - (bu) => bu.type === item.unitType, - ); - if (buildableUnit === undefined) { - return html``; - } - const enabled = - buildableUnit.canBuild !== false || - buildableUnit.canUpgrade !== false; - return html` + const buildableUnit = this.playerActions?.buildableUnits.find( + (bu) => bu.type === item.unitType, + ); + if (buildableUnit === undefined) { + return html``; + } + const enabled = + buildableUnit.canBuild !== false || + buildableUnit.canUpgrade !== false; + return html` `; - })} + })}
`, - )} + )} `; } diff --git a/src/client/graphics/layers/StructureDrawingUtils.ts b/src/client/graphics/layers/StructureDrawingUtils.ts index 76fae9ef2b..0cc139a0b1 100644 --- a/src/client/graphics/layers/StructureDrawingUtils.ts +++ b/src/client/graphics/layers/StructureDrawingUtils.ts @@ -4,9 +4,9 @@ import { Cell, UnitType } from "../../../core/game/Game"; import { GameView, PlayerView, UnitView } from "../../../core/game/GameView"; import { TransformHandler } from "../TransformHandler"; import anchorIcon from "/images/AnchorIcon.png?url"; +import landMineIcon from "/images/buildings/landMine1.png?url"; import cityIcon from "/images/CityIcon.png?url"; import factoryIcon from "/images/FactoryUnit.png?url"; -import landMineIcon from "/images/buildings/landMine1.png?url"; import missileSiloIcon from "/images/MissileSiloUnit.png?url"; import SAMMissileIcon from "/images/SamLauncherUnit.png?url"; import shieldIcon from "/images/ShieldIcon.png?url"; @@ -262,13 +262,13 @@ export class SpriteFactory { const shape = STRUCTURE_SHAPES[type]; const texture = shape ? this.createIcon( - owner, - type, - isConstruction, - isMarkedForDeletion, - shape, - renderIcon, - ) + owner, + type, + isConstruction, + isMarkedForDeletion, + shape, + renderIcon, + ) : PIXI.Texture.EMPTY; this.textureCache.set(cacheKey, texture); return texture; diff --git a/src/client/graphics/layers/StructureIconsLayer.ts b/src/client/graphics/layers/StructureIconsLayer.ts index c6b52180d1..35e3c08729 100644 --- a/src/client/graphics/layers/StructureIconsLayer.ts +++ b/src/client/graphics/layers/StructureIconsLayer.ts @@ -54,7 +54,7 @@ class StructureRenderInfo { public dotContainer: PIXI.Container, public level: number = 0, public underConstruction: boolean = true, - ) { } + ) {} } export class StructureIconsLayer implements Layer { @@ -677,9 +677,9 @@ export class StructureIconsLayer implements Layer { Math.max( 1, scale / - (target === render.levelContainer - ? LEVEL_SCALE_FACTOR - : ICON_SCALE_FACTOR_ZOOMED_IN), + (target === render.levelContainer + ? LEVEL_SCALE_FACTOR + : ICON_SCALE_FACTOR_ZOOMED_IN), ), ); } else if (scale > DOTS_ZOOM_THRESHOLD) { diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index 9842c83b07..0f0d27dbba 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -10,8 +10,8 @@ import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameView, UnitView } from "../../../core/game/GameView"; import cityIcon from "/images/buildings/cityAlt1.png?url"; import factoryIcon from "/images/buildings/factoryAlt1.png?url"; -import landMineIcon from "/images/buildings/landMine1.png?url"; import shieldIcon from "/images/buildings/fortAlt3.png?url"; +import landMineIcon from "/images/buildings/landMine1.png?url"; import anchorIcon from "/images/buildings/port1.png?url"; import missileSiloIcon from "/images/buildings/silo1.png?url"; import SAMMissileIcon from "/images/buildings/silo4.png?url"; diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index a4fd31cf6b..1266d12be5 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -94,7 +94,7 @@ export class GameRunner { private execManager: Executor, private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void, private clientID: ClientID, - ) { } + ) {} init() { if (this.game.config().isRandomSpawn()) { diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 9647273482..06b44b06f8 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -258,7 +258,7 @@ export class DefaultConfig implements Config { private _gameConfig: GameConfig, private _userSettings: UserSettings | null, private _isReplay: boolean, - ) { } + ) {} stripePublishableKey(): string { return Env.STRIPE_PUBLISHABLE_KEY ?? ""; @@ -405,7 +405,7 @@ export class DefaultConfig implements Config { // Geometric mean of base spawn rate and port multiplier const combined = Math.sqrt( this.tradeShipBaseSpawn(numTradeShips, numPlayerTradeShips) * - this.tradeShipPortMultiplier(numPlayerPorts), + this.tradeShipPortMultiplier(numPlayerPorts), ); return Math.floor(25 / combined); @@ -850,11 +850,11 @@ export class DefaultConfig implements Config { player.type() === PlayerType.Human && this.infiniteTroops() ? 1_000_000_000 : 2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000) + - player - .units(UnitType.City) - .map((city) => city.level()) - .reduce((a, b) => a + b, 0) * - this.cityTroopIncrease(); + player + .units(UnitType.City) + .map((city) => city.level()) + .reduce((a, b) => a + b, 0) * + this.cityTroopIncrease(); if (player.type() === PlayerType.Bot) { return maxTroops / 3; diff --git a/src/core/execution/LandMineExecution.ts b/src/core/execution/LandMineExecution.ts index 10d706c149..32f675ad98 100644 --- a/src/core/execution/LandMineExecution.ts +++ b/src/core/execution/LandMineExecution.ts @@ -95,14 +95,12 @@ export class LandMineExecution implements Execution { for (const t of toDestroy) { attacker.relinquish(t); attacker.removeTroops( - this.mg - .config() - .nukeDeathFactor( - UnitType.AtomBomb, // Use atom bomb death factor calculation - attacker.troops(), - attacker.numTilesOwned(), - maxTroops, - ), + this.mg.config().nukeDeathFactor( + UnitType.AtomBomb, // Use atom bomb death factor calculation + attacker.troops(), + attacker.numTilesOwned(), + maxTroops, + ), ); if (this.mg.isLand(t)) { @@ -181,4 +179,3 @@ export class LandMineExecution implements Execution { return this.active; } } - diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index d09f68e7e6..4f7752819b 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -19,7 +19,7 @@ export class PlayerExecution implements Execution { private mg: Game; private active = true; - constructor(private player: Player) { } + constructor(private player: Player) {} activeDuringSpawnPhase(): boolean { return false; @@ -102,7 +102,7 @@ export class PlayerExecution implements Execution { if ( embargo.isTemporary && this.mg.ticks() - embargo.createdAt > - this.mg.config().temporaryEmbargoDuration() + this.mg.config().temporaryEmbargoDuration() ) { this.player.stopEmbargo(embargo.target); } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 66dd519f86..c94a4b445f 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -340,7 +340,7 @@ export class Nation { constructor( public readonly spawnCell: Cell | undefined, public readonly playerInfo: PlayerInfo, - ) { } + ) {} } export class Cell { diff --git a/tests/LandMine.test.ts b/tests/LandMine.test.ts index dc1ceabaca..7723d813f5 100644 --- a/tests/LandMine.test.ts +++ b/tests/LandMine.test.ts @@ -321,9 +321,7 @@ describe("LandMine", () => { const allyInfo = new PlayerInfo("ally", PlayerType.Human, null, "ally_id"); game.addPlayer(allyInfo); - game.addExecution( - new SpawnExecution(gameID, allyInfo, game.ref(10, 20)), - ); + game.addExecution(new SpawnExecution(gameID, allyInfo, game.ref(10, 20))); for (let i = 0; i < 10; i++) { game.executeNextTick(); @@ -374,7 +372,9 @@ describe("LandMine", () => { test("land mine has same cost as defense post", async () => { const config = game.config(); - const landMineCost = config.unitInfo(UnitType.LandMine).cost(game, defender); + const landMineCost = config + .unitInfo(UnitType.LandMine) + .cost(game, defender); const defensePostCost = config .unitInfo(UnitType.DefensePost) .cost(game, defender); From 70da3ce451928922aa205980ec9f68d05df26ce8 Mon Sep 17 00:00:00 2001 From: Ryan Huellen Date: Tue, 6 Jan 2026 20:53:41 -0600 Subject: [PATCH 07/12] fix: removed erroneous test --- tests/LandMine.test.ts | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/tests/LandMine.test.ts b/tests/LandMine.test.ts index 7723d813f5..1537f93709 100644 --- a/tests/LandMine.test.ts +++ b/tests/LandMine.test.ts @@ -311,7 +311,7 @@ describe("LandMine", () => { executeTicks(slowBuildGame, 5); expect(mine.isActive()).toBe(false); - + ``; expect(attacker2.units(UnitType.LandMine)).toHaveLength(0); expect(defender2.units(UnitType.LandMine)).toHaveLength(0); @@ -370,18 +370,6 @@ describe("LandMine", () => { expect(defender.units(UnitType.LandMine)).toHaveLength(0); }); - test("land mine has same cost as defense post", async () => { - const config = game.config(); - const landMineCost = config - .unitInfo(UnitType.LandMine) - .cost(game, defender); - const defensePostCost = config - .unitInfo(UnitType.DefensePost) - .cost(game, defender); - - expect(landMineCost).toEqual(defensePostCost); - }); - test("land mine is a territory-bound structure", async () => { constructionExecution(game, defender, 10, 10, UnitType.LandMine); const mine = defender.units(UnitType.LandMine)[0]; @@ -396,16 +384,6 @@ describe("LandMine", () => { expect(mine.info().visibleToEnemies).toBe(false); }); - test("land mine visibility property is configured correctly in config", async () => { - const config = game.config(); - const landMineInfo = config.unitInfo(UnitType.LandMine); - - expect(landMineInfo.visibleToEnemies).toBe(false); - - const defensePostInfo = config.unitInfo(UnitType.DefensePost); - expect(defensePostInfo.visibleToEnemies).toBeUndefined(); - }); - test("server should not send land mine unit updates to enemies", async () => { game.addExecution( new ConstructionExecution(defender, UnitType.LandMine, game.ref(10, 10)), From 73fc5703ed5ce939d47fdc82620972a77e5c4912 Mon Sep 17 00:00:00 2001 From: Ryan Huellen Date: Tue, 6 Jan 2026 21:06:00 -0600 Subject: [PATCH 08/12] fix: lint issues --- src/core/GameRunner.ts | 4 +--- tests/LandMine.test.ts | 3 --- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 1266d12be5..fba2dfafc5 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -191,9 +191,7 @@ export class GameRunner { */ private filterHiddenUnits(updates: GameUpdates): void { // Lazy-load our player reference - if (this.myPlayer === null) { - this.myPlayer = this.game.playerByClientID(this.clientID) ?? null; - } + this.myPlayer ??= this.game.playerByClientID(this.clientID) ?? null; updates[GameUpdateType.Unit] = updates[GameUpdateType.Unit].filter( (unitUpdate) => { diff --git a/tests/LandMine.test.ts b/tests/LandMine.test.ts index 1537f93709..97b8e0c41a 100644 --- a/tests/LandMine.test.ts +++ b/tests/LandMine.test.ts @@ -113,8 +113,6 @@ describe("LandMine", () => { const mine = defender.units(UnitType.LandMine)[0]; const mineTile = mine.tile(); - const defenderInitialTiles = defender.numTilesOwned(); - game.addExecution(new AttackExecution(100, attacker, defender.id())); let ticks = 0; @@ -311,7 +309,6 @@ describe("LandMine", () => { executeTicks(slowBuildGame, 5); expect(mine.isActive()).toBe(false); - ``; expect(attacker2.units(UnitType.LandMine)).toHaveLength(0); expect(defender2.units(UnitType.LandMine)).toHaveLength(0); From 2ccf8069f9e8823b3eafbc2fca345b31d7852b5f Mon Sep 17 00:00:00 2001 From: Ryan Huellen Date: Tue, 6 Jan 2026 21:12:43 -0600 Subject: [PATCH 09/12] fix: removed non-english translations per code rabbit --- resources/lang/ar.json | 3 +-- resources/lang/bg.json | 8 ++------ resources/lang/cs.json | 6 ++---- resources/lang/da.json | 6 ++---- resources/lang/de.json | 6 ++---- resources/lang/el.json | 8 ++------ resources/lang/eo.json | 6 ++---- resources/lang/fa.json | 8 ++------ resources/lang/fi.json | 6 ++---- resources/lang/fr.json | 8 ++------ resources/lang/gl.json | 6 ++---- resources/lang/he.json | 6 ++---- resources/lang/hu.json | 6 ++---- resources/lang/ja.json | 8 ++------ resources/lang/ko.json | 6 ++---- resources/lang/mk.json | 6 ++---- resources/lang/nl.json | 8 ++------ resources/lang/pl.json | 8 ++------ resources/lang/pt-PT.json | 6 ++---- resources/lang/ru.json | 8 ++------ resources/lang/sk.json | 6 ++---- resources/lang/sl.json | 6 ++---- resources/lang/sv-SE.json | 6 ++---- resources/lang/tp.json | 3 +-- resources/lang/tr.json | 6 ++---- resources/lang/uk.json | 8 ++------ resources/lang/zh-CN.json | 8 ++------ 27 files changed, 52 insertions(+), 124 deletions(-) diff --git a/resources/lang/ar.json b/resources/lang/ar.json index 75f716fdd0..1c608031ce 100644 --- a/resources/lang/ar.json +++ b/resources/lang/ar.json @@ -202,8 +202,7 @@ "sam_launcher": "قاذف صواريخ مضادة", "atom_bomb": "قنبلة نووية", "hydrogen_bomb": "قنبلة هيدروجينية", - "mirv": "MIRV", - "land_mine": "لغم أرضي" + "mirv": "MIRV" }, "user_setting": { "title": "إعدادات المستخدم", diff --git a/resources/lang/bg.json b/resources/lang/bg.json index d2d36c45c5..2fef4934e3 100644 --- a/resources/lang/bg.json +++ b/resources/lang/bg.json @@ -324,8 +324,7 @@ "atom_bomb": "Атомна бомба", "hydrogen_bomb": "Водородна бомба", "mirv": "МИРВ", - "factory": "Фабрика", - "land_mine": "Мина" + "factory": "Фабрика" }, "user_setting": { "title": "Потребителски настройки", @@ -369,8 +368,6 @@ "build_factory_desc": "Изграждане на фабрика под курсора Ви.", "build_defense_post": "Изграждане на отбранителен пост", "build_defense_post_desc": "Изграждане на отбранителен пост под курсора Ви.", - "build_land_mine": "Изграждане на мина", - "build_land_mine_desc": "Изграждане на мина под курсора Ви.", "build_port": "Изграждане на пристанище", "build_port_desc": "Изграждане на пристанище под курсора Ви.", "build_warship": "Изграждане на боен кораб", @@ -520,8 +517,7 @@ "port": "Изпраща търговски кораби, за да се получава злато", "defense_post": "Увеличава отбраната на близки граници", "city": "Увеличава максималната популация", - "factory": "Създава железопътни линии и пуска влакове", - "land_mine": "Избухва, когато враг завземе тази клетка" + "factory": "Създава железопътни линии и пуска влакове" }, "not_enough_money": "Няма достатъчно пари" }, diff --git a/resources/lang/cs.json b/resources/lang/cs.json index f5816a4c4e..2f7410f20f 100644 --- a/resources/lang/cs.json +++ b/resources/lang/cs.json @@ -216,8 +216,7 @@ "sam_launcher": "Odpalovač SAM", "atom_bomb": "Atomová bomba", "hydrogen_bomb": "Vodíková bomba", - "mirv": "MIRV", - "land_mine": "Pozemní mina" + "mirv": "MIRV" }, "user_setting": { "title": "Uživatelská nastavení", @@ -346,8 +345,7 @@ "warship": "Zachytává obchodní lodě, ničí lodě a čluny", "port": "Odešle obchodní lodě pro generování zlata", "defense_post": "Zvýšit ochranu blízkých hranic", - "city": "Zvýšit maximální počet obyvatel", - "land_mine": "Vybuchne, když nepřítel obsadí toto pole" + "city": "Zvýšit maximální počet obyvatel" }, "not_enough_money": "Nedostatek peněz" }, diff --git a/resources/lang/da.json b/resources/lang/da.json index 4a76eee361..c5efe53070 100644 --- a/resources/lang/da.json +++ b/resources/lang/da.json @@ -258,8 +258,7 @@ "atom_bomb": "Atombombe", "hydrogen_bomb": "Brintbombe", "mirv": "MIRV", - "factory": "Fabrik", - "land_mine": "Landmine" + "factory": "Fabrik" }, "user_setting": { "title": "Brugerindstillinger", @@ -428,8 +427,7 @@ "port": "Sender handelsskibe for at generere guld", "defense_post": "Forstærker forsvaret af nærliggende grænser", "city": "Øger maksimal befolkning", - "factory": "Opretter jernbaner og sender tog afsted", - "land_mine": "Eksploderer når en fjende indtager denne felt" + "factory": "Opretter jernbaner og sender tog afsted" }, "not_enough_money": "Ikke nok penge" }, diff --git a/resources/lang/de.json b/resources/lang/de.json index 512c1dbcaf..cdedccd6ab 100644 --- a/resources/lang/de.json +++ b/resources/lang/de.json @@ -247,8 +247,7 @@ "atom_bomb": "Atombombe", "hydrogen_bomb": "Wasserstoffbombe", "mirv": "MIRV-Rakete", - "factory": "Fabrik", - "land_mine": "Landmine" + "factory": "Fabrik" }, "user_setting": { "title": "Benutzer Einstellungen", @@ -391,8 +390,7 @@ "warship": "Erobert Handelsschiffe, zerstört Schiffe und Boote", "port": "Sendet Handelsschiffe, um Gold zu generieren", "defense_post": "Erhöht Verteidigung der Grenzen in der Nähe", - "city": "Erhöht maximale Bevölkerung", - "land_mine": "Explodiert, wenn ein Feind dieses Feld erobert" + "city": "Erhöht maximale Bevölkerung" }, "not_enough_money": "Nicht genug Geld" }, diff --git a/resources/lang/el.json b/resources/lang/el.json index 565fb4ef65..cde595ed1c 100644 --- a/resources/lang/el.json +++ b/resources/lang/el.json @@ -304,8 +304,7 @@ "atom_bomb": "Ατομική Βόμβα", "hydrogen_bomb": "Βόμβα Υδρογόνου", "mirv": "MIRV", - "factory": "Εργοστάσιο", - "land_mine": "Νάρκη" + "factory": "Εργοστάσιο" }, "user_setting": { "title": "Ρυθμίσεις Χρήστη", @@ -349,8 +348,6 @@ "build_factory_desc": "Κτίσε ένα Εργοστάσιο κάτω από τον δείκτη σου.", "build_defense_post": "Κατασκευή Φρουρίου", "build_defense_post_desc": "Κτίσε ένα Φρούριο κάτω από τον δείκτη σου.", - "build_land_mine": "Κατασκευή Νάρκης", - "build_land_mine_desc": "Κτίσε μια Νάρκη κάτω από τον δείκτη σου.", "build_port": "Κατασκευή Λιμανιού", "build_port_desc": "Κτίσε ένα Λιμάνι κάτω από τον δείκτη σου.", "build_warship": "Κατασκευή Πολεμικού Πλοίου", @@ -500,8 +497,7 @@ "port": "Στέλνει εμπορικά πλοία για την παραγωγή χρυσού", "defense_post": "Αυξάνει την άμυνα των κοντινών συνόρων", "city": "Αυξάνει τον μέγιστο πληθυσμό", - "factory": "Δημιουργεί σιδηρόδρομους και στέλνει τρένα", - "land_mine": "Εκρήγνυται όταν ένας εχθρός καταλάβει αυτό το πλακίδιο" + "factory": "Δημιουργεί σιδηρόδρομους και στέλνει τρένα" }, "not_enough_money": "Όχι αρκετά χρήματα" }, diff --git a/resources/lang/eo.json b/resources/lang/eo.json index b2c34c9d46..4703c919d9 100644 --- a/resources/lang/eo.json +++ b/resources/lang/eo.json @@ -275,8 +275,7 @@ "atom_bomb": "Atombombo", "hydrogen_bomb": "Hidrogenbombo", "mirv": "MIRV", - "factory": "Fabriko", - "land_mine": "Termino" + "factory": "Fabriko" }, "user_setting": { "title": "Uzantparametroj", @@ -462,8 +461,7 @@ "port": "Sendas komercŝipojn por produkti oron", "defense_post": "Pligrandigas defendojn de proksimumaj landlimoj", "city": "Pligrandigas maksiman loĝantaron", - "factory": "Kreas fervojojn kaj generas trajnojn", - "land_mine": "Eksplodas kiam malamiko kaptas ĉi tiun kahelon" + "factory": "Kreas fervojojn kaj generas trajnojn" }, "not_enough_money": "Ne sufiĉe da mono" }, diff --git a/resources/lang/fa.json b/resources/lang/fa.json index 65905fd5a2..34cc78ce2d 100644 --- a/resources/lang/fa.json +++ b/resources/lang/fa.json @@ -324,8 +324,7 @@ "atom_bomb": "بمب اتم", "hydrogen_bomb": "بمب هیدروژنی", "mirv": "میرووی", - "factory": "کارخانه", - "land_mine": "مین زمینی" + "factory": "کارخانه" }, "user_setting": { "title": "تنظیمات کاربر", @@ -369,8 +368,6 @@ "build_factory_desc": "کارخانه ای در زیر مکان‌نما بسازید.", "build_defense_post": "ساخت پست دفاعی", "build_defense_post_desc": "یک پست دفاعی در زیر مکان‌نما بسازید.", - "build_land_mine": "ساخت مین زمینی", - "build_land_mine_desc": "یک مین زمینی در زیر مکان‌نما بسازید.", "build_port": "ساخت بندر", "build_port_desc": "یک بندر در زیر مکان‌نما بسازید.", "build_warship": "کشتی‌های جنگی بساز", @@ -520,8 +517,7 @@ "port": "کشتی‌های تجاری را برای تولید طلا ارسال می‌کند", "defense_post": "دفاعات مرزهای اطراف را افزایش می‌دهد", "city": "حداکثر جمعیت را افزایش می‌دهد", - "factory": "راه‌آهن می‌سازد و قطارها را ظاهر می‌کند", - "land_mine": "زمانی که دشمن این خانه را تصرف کند منفجر می‌شود" + "factory": "راه‌آهن می‌سازد و قطارها را ظاهر می‌کند" }, "not_enough_money": "پول کافی نیست" }, diff --git a/resources/lang/fi.json b/resources/lang/fi.json index 4d3adad713..52ca74680c 100644 --- a/resources/lang/fi.json +++ b/resources/lang/fi.json @@ -272,8 +272,7 @@ "atom_bomb": "Atomipommi", "hydrogen_bomb": "Vetypommi", "mirv": "MIRV", - "factory": "Tehdas", - "land_mine": "Maamiina" + "factory": "Tehdas" }, "user_setting": { "title": "Käyttäjäasetukset", @@ -457,8 +456,7 @@ "port": "Lähettää kauppa-aluksia tuottaakseen kultaa", "defense_post": "Parantaa läheisten rajojen puolustusta", "city": "Lisää enimmäisväkilukuasi", - "factory": "Luo rautateitä ja lähettää junia", - "land_mine": "Räjähtää kun vihollinen valtaa tämän ruudun" + "factory": "Luo rautateitä ja lähettää junia" }, "not_enough_money": "Ei tarpeeksi rahaa" }, diff --git a/resources/lang/fr.json b/resources/lang/fr.json index 9996beb82c..31328191e4 100644 --- a/resources/lang/fr.json +++ b/resources/lang/fr.json @@ -324,8 +324,7 @@ "atom_bomb": "Bombe atomique", "hydrogen_bomb": "Bombe à hydrogène", "mirv": "MIRV", - "factory": "Usine", - "land_mine": "Mine terrestre" + "factory": "Usine" }, "user_setting": { "title": "Paramètres utilisateur", @@ -369,8 +368,6 @@ "build_factory_desc": "Construire une ville sous votre curseur.", "build_defense_post": "Construire un poste de défense", "build_defense_post_desc": "Construire un poste de défense sous votre curseur.", - "build_land_mine": "Construire une mine terrestre", - "build_land_mine_desc": "Construire une mine terrestre sous votre curseur.", "build_port": "Construire un port", "build_port_desc": "Construire un port sous votre curseur.", "build_warship": "Construire un navire de guerre", @@ -520,8 +517,7 @@ "port": "Envoie des navires commerciaux pour produire de l'or", "defense_post": "Augmente les défenses des frontières proches", "city": "Augmente la population maximale", - "factory": "Crée des chemins de fer et fait apparaître des trains", - "land_mine": "Explose quand un ennemi capture cette case" + "factory": "Crée des chemins de fer et fait apparaître des trains" }, "not_enough_money": "Pas assez d'argent" }, diff --git a/resources/lang/gl.json b/resources/lang/gl.json index d1032a29ab..768b7d07b9 100644 --- a/resources/lang/gl.json +++ b/resources/lang/gl.json @@ -258,8 +258,7 @@ "atom_bomb": "Bomba atómica", "hydrogen_bomb": "Bomba de hidróxeno", "mirv": "MIRV", - "factory": "Fábrica", - "land_mine": "Mina terrestre" + "factory": "Fábrica" }, "user_setting": { "title": "Axustes do usuario", @@ -428,8 +427,7 @@ "port": "Envía barcos comerciais para xerar ouro", "defense_post": "Reforza as defensas das fronteiras próximas", "city": "Aumenta a poboación máxima", - "factory": "Constrúe vías férreas e xera trens", - "land_mine": "Explota cando un inimigo captura esta cela" + "factory": "Constrúe vías férreas e xera trens" }, "not_enough_money": "Non tes ouro dabondo" }, diff --git a/resources/lang/he.json b/resources/lang/he.json index 859dd2d66d..895b58560c 100644 --- a/resources/lang/he.json +++ b/resources/lang/he.json @@ -247,8 +247,7 @@ "atom_bomb": "פצצת אטום", "hydrogen_bomb": "פצצת מימן", "mirv": "MIRV", - "factory": "מפעל", - "land_mine": "מוקש" + "factory": "מפעל" }, "user_setting": { "title": "הגדרות משתמש", @@ -391,8 +390,7 @@ "warship": "לוכד ספינות סחר, הורס ספינות סחר וכיבוש.", "port": "שולח ספינות סחר כדי לייצר זהב", "defense_post": "מעלה הגנות של גבולות קרובים", - "city": "מעלה כמות אוכלוסיה מירבית", - "land_mine": "מתפוצץ כאשר אויב כובש את המשבצת הזו" + "city": "מעלה כמות אוכלוסיה מירבית" }, "not_enough_money": "אין מספיק זהב" }, diff --git a/resources/lang/hu.json b/resources/lang/hu.json index 33b9fc3ca1..343380f019 100644 --- a/resources/lang/hu.json +++ b/resources/lang/hu.json @@ -275,8 +275,7 @@ "atom_bomb": "Atombomba", "hydrogen_bomb": "Hidrogénbomba", "mirv": "MIRV", - "factory": "Gyár", - "land_mine": "Akna" + "factory": "Gyár" }, "user_setting": { "title": "Játékos beállítások", @@ -462,8 +461,7 @@ "port": "Kereskedőhajókat küld, hogy arany termeljen", "defense_post": "Növeli a közeli határok védelmét", "city": "Növeli a maximális népességet", - "factory": "Vasútvonalakat hoz létre és vonatokat indít", - "land_mine": "Felrobban, amikor egy ellenség elfoglalja ezt a mezőt" + "factory": "Vasútvonalakat hoz létre és vonatokat indít" }, "not_enough_money": "Nincs elég pénz" }, diff --git a/resources/lang/ja.json b/resources/lang/ja.json index d990dc9504..33bcbc2bf0 100644 --- a/resources/lang/ja.json +++ b/resources/lang/ja.json @@ -324,8 +324,7 @@ "atom_bomb": "原子爆弾", "hydrogen_bomb": "水素爆弾", "mirv": "MIRV", - "factory": "工場", - "land_mine": "地雷" + "factory": "工場" }, "user_setting": { "title": "ユーザー設定", @@ -369,8 +368,6 @@ "build_factory_desc": "選択した位置に工場を建設します。", "build_defense_post": "防衛ポストを建設", "build_defense_post_desc": "選択した位置に防衛ポストを建設します。", - "build_land_mine": "地雷を建設", - "build_land_mine_desc": "選択した位置に地雷を建設します。", "build_port": "港を建設", "build_port_desc": "選択した位置に港を建設します。", "build_warship": "戦艦を建造", @@ -520,8 +517,7 @@ "port": "貿易船を送って資金を獲得する", "defense_post": "近くの国境の防御を強化します", "city": "最大人口が増加します", - "factory": "列車が行き来できる線路を作成します", - "land_mine": "敵がこのタイルを占領すると爆発します" + "factory": "列車が行き来できる線路を作成します" }, "not_enough_money": "資金不足" }, diff --git a/resources/lang/ko.json b/resources/lang/ko.json index 60b965cbef..0534d27f55 100644 --- a/resources/lang/ko.json +++ b/resources/lang/ko.json @@ -251,8 +251,7 @@ "atom_bomb": "원자 폭탄", "hydrogen_bomb": "수소 폭탄", "mirv": "다탄두 미사일", - "factory": "공장", - "land_mine": "지뢰" + "factory": "공장" }, "user_setting": { "title": "사용자 설정", @@ -414,8 +413,7 @@ "port": "무역선을 보내 금을 생산합니다", "defense_post": "주변 경계의 방어력을 강화합니다", "city": "최대 인구 수를 증가시킵니다", - "factory": "철도를 만들고 기차를 생성합니다", - "land_mine": "적이 이 타일을 점령하면 폭발합니다" + "factory": "철도를 만들고 기차를 생성합니다" }, "not_enough_money": "돈이 부족합니다" }, diff --git a/resources/lang/mk.json b/resources/lang/mk.json index 4ed229f515..62b17904d2 100644 --- a/resources/lang/mk.json +++ b/resources/lang/mk.json @@ -272,8 +272,7 @@ "atom_bomb": "Атомска бомба", "hydrogen_bomb": "Водородна бомба", "mirv": "MIRV", - "factory": "Фабрика", - "land_mine": "Мина" + "factory": "Фабрика" }, "user_setting": { "title": "Кориснички поставки", @@ -457,8 +456,7 @@ "port": "Испраќа трговски бродови за да генерира злато", "defense_post": "Ја зголемува одбраната на блиските граници", "city": "Го зголемува максимумот на популација", - "factory": "Создава железници и пушта возови", - "land_mine": "Експлодира кога непријател ќе го освои ова поле" + "factory": "Создава железници и пушта возови" }, "not_enough_money": "Нема доволно пари" }, diff --git a/resources/lang/nl.json b/resources/lang/nl.json index 2c0dd3eea5..7461504080 100644 --- a/resources/lang/nl.json +++ b/resources/lang/nl.json @@ -324,8 +324,7 @@ "atom_bomb": "Atoombom", "hydrogen_bomb": "Waterstofbom", "mirv": "MIRV", - "factory": "Fabriek", - "land_mine": "Landmijn" + "factory": "Fabriek" }, "user_setting": { "title": "Gebruikersinstellingen", @@ -369,8 +368,6 @@ "build_factory_desc": "Bouw een Fabriek onder je cursor.", "build_defense_post": "Bouw Verdedigingspost", "build_defense_post_desc": "Bouw een Verdedigingspost onder je cursor.", - "build_land_mine": "Bouw Landmijn", - "build_land_mine_desc": "Bouw een Landmijn onder je cursor.", "build_port": "Bouw Haven", "build_port_desc": "Bouw een Haven onder je cursor.", "build_warship": "Bouw Oorlogsschip", @@ -520,8 +517,7 @@ "port": "Stuurt handelsschepen om goud te genereren", "defense_post": "Versterkt verdediging eromheen", "city": "Verhoogt maximale bevolking", - "factory": "Maakt spoorwegen en laat treinen rijden", - "land_mine": "Ontploft wanneer een vijand dit veld inneemt" + "factory": "Maakt spoorwegen en laat treinen rijden" }, "not_enough_money": "Niet genoeg goud" }, diff --git a/resources/lang/pl.json b/resources/lang/pl.json index 274ee96a3d..9ab3595939 100644 --- a/resources/lang/pl.json +++ b/resources/lang/pl.json @@ -304,8 +304,7 @@ "atom_bomb": "Bomba atomowa", "hydrogen_bomb": "Bomba wodorowa", "mirv": "MIRV", - "factory": "Fabryka", - "land_mine": "Mina lądowa" + "factory": "Fabryka" }, "user_setting": { "title": "Ustawienia użytkownika", @@ -349,8 +348,6 @@ "build_factory_desc": "Zbuduj fabrykę pod twoim kursorem.", "build_defense_post": "Buduj posterunek obronny", "build_defense_post_desc": "Zbuduj posterunek obronny pod twoim kursorem.", - "build_land_mine": "Zbuduj minę lądową", - "build_land_mine_desc": "Zbuduj minę lądową pod twoim kursorem.", "build_port": "Zbuduj port", "build_port_desc": "Zbuduj port pod twoim kursorem.", "build_warship": "Zbuduj okręt wojenny", @@ -500,8 +497,7 @@ "port": "Wysyła statki handlowe, aby wygenerować złoto", "defense_post": "Wzmacnia obronę pobliskich granic", "city": "Zwiększa maksymalną populację", - "factory": "Tworzy linie kolejowe i przywołuje pociągi", - "land_mine": "Wybucha gdy wróg zajmie to pole" + "factory": "Tworzy linie kolejowe i przywołuje pociągi" }, "not_enough_money": "Za mało złota" }, diff --git a/resources/lang/pt-PT.json b/resources/lang/pt-PT.json index 80a8b3ded8..7ba8a7f86c 100644 --- a/resources/lang/pt-PT.json +++ b/resources/lang/pt-PT.json @@ -258,8 +258,7 @@ "atom_bomb": "Bomba Atómica", "hydrogen_bomb": "Bomba de Hidrogénio", "mirv": "MIRV", - "factory": "Fábrica", - "land_mine": "Mina Terrestre" + "factory": "Fábrica" }, "user_setting": { "title": "Configurações de Utilizador", @@ -428,8 +427,7 @@ "port": "Envia navios de comércio para gerar ouro", "defense_post": "Aumenta as defesas de fronteiras próximas", "city": "Aumenta a população máxima", - "factory": "Cria ferrovias e comboios", - "land_mine": "Explode quando um inimigo captura esta casa" + "factory": "Cria ferrovias e comboios" }, "not_enough_money": "Dinheiro insuficiente" }, diff --git a/resources/lang/ru.json b/resources/lang/ru.json index 9aa50b51fa..ce7d1cfe6f 100644 --- a/resources/lang/ru.json +++ b/resources/lang/ru.json @@ -324,8 +324,7 @@ "atom_bomb": "Атомная бомба", "hydrogen_bomb": "Водородная бомба", "mirv": "РГЧ ИН", - "factory": "Фабрика", - "land_mine": "Мина" + "factory": "Фабрика" }, "user_setting": { "title": "Пользовательские настройки", @@ -369,8 +368,6 @@ "build_factory_desc": "Разместить фабрику под указателем.", "build_defense_post": "Разместить укрепление", "build_defense_post_desc": "Разместить укрепление под указателем.", - "build_land_mine": "Разместить мину", - "build_land_mine_desc": "Разместить мину под указателем.", "build_port": "Разместить порт", "build_port_desc": "Разместить порт под указателем.", "build_warship": "Разместить военный корабль", @@ -520,8 +517,7 @@ "port": "Отправляет торговые корабли для генерации золота", "defense_post": "Укрепляет защиту ближайших границ", "city": "Увеличивает максимальное население", - "factory": "Прокладывает железнодорожные пути и создаёт поезда", - "land_mine": "Взрывается, когда враг захватывает эту клетку" + "factory": "Прокладывает железнодорожные пути и создаёт поезда" }, "not_enough_money": "Недостаточно средств" }, diff --git a/resources/lang/sk.json b/resources/lang/sk.json index 34950528c0..cf2bac42aa 100644 --- a/resources/lang/sk.json +++ b/resources/lang/sk.json @@ -258,8 +258,7 @@ "atom_bomb": "Atómová bomba", "hydrogen_bomb": "Vodíková bomba", "mirv": "MIRV", - "factory": "Továreň", - "land_mine": "Pozemná mína" + "factory": "Továreň" }, "user_setting": { "title": "Užívateľské nastavenia", @@ -428,8 +427,7 @@ "port": "Posiela obchodné lode na tvorbu zlata", "defense_post": "Zvyšuje obranu okolitých hraníc", "city": "Zvyšuje maximálnu populáciu", - "factory": "Tvorí železnice a vlaky", - "land_mine": "Vybuchne, keď nepriateľ obsadí toto pole" + "factory": "Tvorí železnice a vlaky" }, "not_enough_money": "Nedostatok zlata" }, diff --git a/resources/lang/sl.json b/resources/lang/sl.json index 8dafb6f43f..b786336aee 100644 --- a/resources/lang/sl.json +++ b/resources/lang/sl.json @@ -275,8 +275,7 @@ "atom_bomb": "Atomska bomba", "hydrogen_bomb": "Vodikova bomba", "mirv": "MIRV", - "factory": "Tovarna", - "land_mine": "Mina" + "factory": "Tovarna" }, "user_setting": { "title": "Uporabniške nastavitve", @@ -462,8 +461,7 @@ "port": "Pošilja trgovske ladje za pridobivanje zlata", "defense_post": "Poveča obrambo bližnjih meja", "city": "Poveča največjo populacijo", - "factory": "Ustvarja železnice in ustvarja vlake", - "land_mine": "Eksplodira, ko sovražnik zavzame to polje" + "factory": "Ustvarja železnice in ustvarja vlake" }, "not_enough_money": "Ni dovolj denarja" }, diff --git a/resources/lang/sv-SE.json b/resources/lang/sv-SE.json index f6a2091df1..de6923f55c 100644 --- a/resources/lang/sv-SE.json +++ b/resources/lang/sv-SE.json @@ -253,8 +253,7 @@ "atom_bomb": "Liten atombomb", "hydrogen_bomb": "Vätebomb", "mirv": "MIRV", - "factory": "Fabrik", - "land_mine": "Landmina" + "factory": "Fabrik" }, "user_setting": { "title": "Användarinställningar", @@ -416,8 +415,7 @@ "port": "Skickar handelsfartyg för att generera guld", "defense_post": "Ökar försvaret mot närliggande gränser", "city": "Ökar max befolkning", - "factory": "Skapar järnvägar och producerar tåg", - "land_mine": "Exploderar när en fiende intar denna ruta" + "factory": "Skapar järnvägar och producerar tåg" }, "not_enough_money": "Ej tillräckligt med guld" }, diff --git a/resources/lang/tp.json b/resources/lang/tp.json index 5502773a57..5cfeb1bbe0 100644 --- a/resources/lang/tp.json +++ b/resources/lang/tp.json @@ -202,8 +202,7 @@ "sam_launcher": "ilo tawa pi ilo sewi pi pakala wawa", "atom_bomb": "ilo pi pakala wawa pi kipisi ijo", "hydrogen_bomb": "ilo pi pakala wawa pi wan ijo", - "mirv": "ilo pi pakala wawa mute", - "land_mine": "ilo pakala ma" + "mirv": "ilo pi pakala wawa mute" }, "user_setting": { "title": "ken pi jan musi", diff --git a/resources/lang/tr.json b/resources/lang/tr.json index 98b1b190be..0afa36091e 100644 --- a/resources/lang/tr.json +++ b/resources/lang/tr.json @@ -272,8 +272,7 @@ "atom_bomb": "Atom Bombası", "hydrogen_bomb": "Hidrojen Bombası", "mirv": "MIRV", - "factory": "Fabrika", - "land_mine": "Kara Mayını" + "factory": "Fabrika" }, "user_setting": { "title": "Kullanıcı Ayarları", @@ -457,8 +456,7 @@ "port": "Altın üretmek için ticaret gemileri gönderir", "defense_post": "Yakındaki sınırların savunmasını artırır", "city": "Maksimum nüfusu artırır", - "factory": "Demiryolları yapar ve trenler oluşturur", - "land_mine": "Düşman bu kareyi ele geçirdiğinde patlar" + "factory": "Demiryolları yapar ve trenler oluşturur" }, "not_enough_money": "Yeterli para yok" }, diff --git a/resources/lang/uk.json b/resources/lang/uk.json index 1d0e6b1949..2c11971d33 100644 --- a/resources/lang/uk.json +++ b/resources/lang/uk.json @@ -324,8 +324,7 @@ "atom_bomb": "Атомна бомба", "hydrogen_bomb": "Воднева бомба", "mirv": "РГЧ ІН", - "factory": "Фабрика", - "land_mine": "Міна" + "factory": "Фабрика" }, "user_setting": { "title": "Користувацькі налаштування", @@ -369,8 +368,6 @@ "build_factory_desc": "Будувати фабрику під указівником.", "build_defense_post": "Розмістити пункт оборони", "build_defense_post_desc": "Розмістити пункт оборони під указівником.", - "build_land_mine": "Розмістити міну", - "build_land_mine_desc": "Розмістити міну під указівником.", "build_port": "Розмістити порт", "build_port_desc": "Розмістити порт під указівником.", "build_warship": "Розмістити військовий корабель", @@ -520,8 +517,7 @@ "port": "Відправляє торгові кораблі для генерації золота", "defense_post": "Підсилює оборону найближчих кордонів", "city": "Збільшує максимальне населення", - "factory": "Прокладає залізничні колії та створює поїзди", - "land_mine": "Вибухає, коли ворог захоплює цю клітинку" + "factory": "Прокладає залізничні колії та створює поїзди" }, "not_enough_money": "Недостатньо коштів" }, diff --git a/resources/lang/zh-CN.json b/resources/lang/zh-CN.json index cd8beda1ff..1952eacd30 100644 --- a/resources/lang/zh-CN.json +++ b/resources/lang/zh-CN.json @@ -324,8 +324,7 @@ "atom_bomb": "原子弹", "hydrogen_bomb": "氢弹", "mirv": "MIRV", - "factory": "工厂", - "land_mine": "地雷" + "factory": "工厂" }, "user_setting": { "title": "用户设置", @@ -369,8 +368,6 @@ "build_factory_desc": "在鼠标位置建造工厂。", "build_defense_post": "建造要塞", "build_defense_post_desc": "在鼠标位置建造要塞。", - "build_land_mine": "建造地雷", - "build_land_mine_desc": "在鼠标位置建造地雷。", "build_port": "建造港口", "build_port_desc": "在鼠标位置建造港口。", "build_warship": "部署军舰", @@ -520,8 +517,7 @@ "port": "发送商船来获得黄金", "defense_post": "增加附近边界的防御力", "city": "增加最大人口", - "factory": "创建铁轨并生成火车", - "land_mine": "当敌人占领此格时爆炸" + "factory": "创建铁轨并生成火车" }, "not_enough_money": "金钱不足" }, From a5314d362ec25f979dd185a3c75f71d79fe30135 Mon Sep 17 00:00:00 2001 From: Ryan Huellen Date: Tue, 6 Jan 2026 21:14:51 -0600 Subject: [PATCH 10/12] fix: using unused key for building land mines --- src/client/InputHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index f7bd5dc507..5455397ecb 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -211,7 +211,7 @@ export class InputHandler { buildFactory: "Digit2", buildPort: "Digit3", buildDefensePost: "Digit4", - buildLandMine: "Minus", + buildLandMine: "KeyM", buildMissileSilo: "Digit5", buildSamLauncher: "Digit6", buildWarship: "Digit7", From bbb7de9dab9f56ff0fada0d40ebdb824f0556237 Mon Sep 17 00:00:00 2001 From: Ryan Huellen Date: Tue, 6 Jan 2026 21:16:10 -0600 Subject: [PATCH 11/12] fix: land mine capture by ally --- src/core/execution/LandMineExecution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/execution/LandMineExecution.ts b/src/core/execution/LandMineExecution.ts index 32f675ad98..3129e5b0f3 100644 --- a/src/core/execution/LandMineExecution.ts +++ b/src/core/execution/LandMineExecution.ts @@ -56,7 +56,7 @@ export class LandMineExecution implements Execution { // If captured by an ally of the original owner, transfer ownership if (currentOwner.isFriendly(this.originalOwner)) { - // Update owner without detonating + currentOwner.captureUnit(this.mine); this.originalOwner = currentOwner; return; } From e2b5e4f8e7d727d244c068946834d7503dddc1f7 Mon Sep 17 00:00:00 2001 From: Ryan Huellen Date: Tue, 6 Jan 2026 21:23:10 -0600 Subject: [PATCH 12/12] fix: displayed build landmine key --- src/client/UserSettingModal.ts | 2 +- src/client/graphics/layers/UnitDisplay.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index a65b3ab9a4..0d007465db 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -488,7 +488,7 @@ export class UserSettingModal extends LitElement { action="buildLandMine" label=${translateText("user_setting.build_land_mine")} description=${translateText("user_setting.build_land_mine_desc")} - defaultKey="Minus" + defaultKey="KeyM" .value=${this.keybinds["buildLandMine"]?.key ?? ""} @change=${this.handleKeybindChange} >
diff --git a/src/client/graphics/layers/UnitDisplay.ts b/src/client/graphics/layers/UnitDisplay.ts index 9503a04528..f1f5a6edee 100644 --- a/src/client/graphics/layers/UnitDisplay.ts +++ b/src/client/graphics/layers/UnitDisplay.ts @@ -171,7 +171,7 @@ export class UnitDisplay extends LitElement implements Layer { this._landMine, UnitType.LandMine, "land_mine", - this.keybinds["buildLandMine"]?.key ?? "-", + this.keybinds["buildLandMine"]?.key ?? "M", )} ${this.renderUnitItem( missileSiloIcon,