Use b for building and r for recruitment
Nix package / build (push) Successful in 8m22s
Linux package builds (test) / linux (push) Successful in 2m17s
Rust checks / check (push) Successful in 3m20s
Linux package builds (test) / aur (push) Successful in 7m34s

- Replace Ctrl+Space with `b` and Alt+Space with `r` in README and
  documentation.
- Remove the old Alt+Space recruitment keybinding and adjust key
  handling code.
- Revamp skirmish view to display colored lines and updated status
  messages.
- Increase test suite to 83 tests and update test expectations for new
  shortcuts.
- Update flake.lock with the latest revision and hash.
This commit is contained in:
GarandPLG committed 2026-10-23 11:29:25 +02:00
1 parent 73f544d5d6
commit 6c1141a711
12 files changed
+166 -92

No files matched your search

+5 -3
View File
@@ -105,9 +105,9 @@ Możesz zaznaczyć pracującego górnika ponownie: rysowanie kolejnego zadania z
Wood i Metal mają rzeczywisty stan w magazynie każdego gracza, inicjalizowany z `--starting-wood` i `--starting-metal`. Nagłówek pokazuje bieżące zasoby. Gracz otrzymuje 5 Wood co około 5 sekund symulacji; metal nie przyrasta bez kopalni. Złoża Ore są początkowo neutralne. Wood i Metal mają rzeczywisty stan w magazynie każdego gracza, inicjalizowany z `--starting-wood` i `--starting-metal`. Nagłówek pokazuje bieżące zasoby. Gracz otrzymuje 5 Wood co około 5 sekund symulacji; metal nie przyrasta bez kopalni. Złoża Ore są początkowo neutralne.
Ustaw kursor na neutralnym Ore i otwórz widok budowania przez `Ctrl+Space`. Jeśli na jednej z czterech sąsiednich kratek stoi własny Miner z uprawnieniem budowania i masz 20 Wood oraz 10 Metal, Enter rozpoczyna budowę Mine. Koszt jest pobierany od razu, a budowa trwa około 10 sekund. Panel pozostaje otwarty i pokazuje postęp; procent widać też na kratce. Po odejściu budowniczego budowa zatrzymuje się do jego powrotu. Nie można budować zdalnie na rudzie bez sąsiedniego własnego budowniczego. Ustaw kursor na neutralnym Ore i otwórz widok budowania przez `b`. Jeśli na jednej z czterech sąsiednich kratek stoi własny Miner z uprawnieniem budowania i masz 20 Wood oraz 10 Metal, Enter rozpoczyna budowę Mine. Koszt jest pobierany od razu, a budowa trwa około 10 sekund. Panel pozostaje otwarty i pokazuje postęp; procent widać też na kratce. Po odejściu budowniczego budowa zatrzymuje się do jego powrotu. Nie można budować zdalnie na rudzie bez sąsiedniego własnego budowniczego.
Delete w panelu budowy anuluje trwającą budowę, przywraca pierwotne neutralne Ore i zwraca cały koszt. Ctrl+Space lub Esc jedynie zamyka panel — budowa nadal postępuje, jeżeli budowniczy jest obecny. Niedokończona kopalnia nie produkuje metalu i nie przyjmuje jednostek. Na początkowej mapie złoże obok bazy spełnia warunek sąsiedztwa; kopalnie nie pojawiają się automatycznie. Delete w panelu budowy anuluje trwającą budowę, przywraca pierwotne neutralne Ore i zwraca cały koszt. b lub Esc jedynie zamyka panel — budowa nadal postępuje, jeżeli budowniczy jest obecny. Niedokończona kopalnia nie produkuje metalu i nie przyjmuje jednostek. Na początkowej mapie złoże obok bazy spełnia warunek sąsiedztwa; kopalnie nie pojawiają się automatycznie.
Mine wydobywa 1 Metal co około sekundę, aż do wyczerpania złoża. Możesz wysłać do własnej kopalni górnika zwykłą trasą zatwierdzaną spacją. Wejście trwa około 3 sekund jako Moving; górnik pozostaje na kratce kopalni i zwiększa wydobycie do 2 Metal/s. Po jego wyjściu bonus znika. W kopalni mieści się jedna jednostka; obcy górnicy nie mogą do niej wejść. Panel Tab pokazuje aktualne tempo wydobycia. Mine wydobywa 1 Metal co około sekundę, aż do wyczerpania złoża. Możesz wysłać do własnej kopalni górnika zwykłą trasą zatwierdzaną spacją. Wejście trwa około 3 sekund jako Moving; górnik pozostaje na kratce kopalni i zwiększa wydobycie do 2 Metal/s. Po jego wyjściu bonus znika. W kopalni mieści się jedna jednostka; obcy górnicy nie mogą do niej wejść. Panel Tab pokazuje aktualne tempo wydobycia.
@@ -117,7 +117,7 @@ Legenda klawiszy zależy od trybu: na mapie pokazuje nawigację i dostępne zazn
## Rekrutacja ## Rekrutacja
Ustaw kursor na własnym budynku Base i naciśnij `Alt+Space` lub `r`. Jeśli terminal albo menedżer okien przechwytuje Alt+Space, użyj `r`. Enter dodaje jednego Minera do kolejki za **10 Wood i 15 Metal**, pobierane od razu. Każda rekrutacja trwa około **10 sekund**. Panel pokazuje liczbę zamówionych górników (włącznie z aktualnie rekrutowanym), procent bieżącej rekrutacji i powód oczekiwania. Esc, Alt+Space lub `r` zamyka panel bez kasowania kolejki. Liczbę i postęp widać też pod Tab. Ustaw kursor na własnym budynku Base i naciśnij `r`. Enter dodaje jednego Minera do kolejki za **10 Wood i 15 Metal**, pobierane od razu. Każda rekrutacja trwa około **10 sekund**. Panel pokazuje liczbę zamówionych górników (włącznie z aktualnie rekrutowanym), procent bieżącej rekrutacji i powód oczekiwania. Esc lub `r` zamyka panel bez kasowania kolejki. Liczbę i postęp widać też pod Tab.
Pierwszy górnik może zostać wyszkolony do 100%, nawet jeśli baza jest zajęta. Gotowy czeka, aż kratka będzie wolna, i pojawia się w bazie bez nadpisywania stojącej tam jednostki. Następna rekrutacja pozostaje na 0%, dopóki poprzedni zrekrutowany górnik nie opuści bazy. Wyprowadź go zwykłą trasą, aby wznowić kolejkę. Każda baza ma niezależną kolejkę; nowe jednostki otrzymują właściciela bazy i pełne uprawnienia Minera. Pierwszy górnik może zostać wyszkolony do 100%, nawet jeśli baza jest zajęta. Gotowy czeka, aż kratka będzie wolna, i pojawia się w bazie bez nadpisywania stojącej tam jednostki. Następna rekrutacja pozostaje na 0%, dopóki poprzedni zrekrutowany górnik nie opuści bazy. Wyprowadź go zwykłą trasą, aby wznowić kolejkę. Każda baza ma niezależną kolejkę; nowe jednostki otrzymują właściciela bazy i pełne uprawnienia Minera.
@@ -139,3 +139,5 @@ Polecenia lokalne i szczegóły środowiska opisano w [docs/TESTING.md](docs/TES
Projekt jest udostępniany na licencji [MIT](LICENSE). Projekt jest udostępniany na licencji [MIT](LICENSE).
Copyright (c) 2026 GarandPLG. Copyright (c) 2026 GarandPLG.
Budowanie otwiera `b`, rekrutację `r`; zwiększanie głośności: `+`, zmniejszanie: `-`, wyciszenie: `m`.
+3 -3
View File
@@ -25,7 +25,7 @@ nix develop -c cargo run -- --disable-audio --view skirmish
Ostatnie polecenie wymaga interaktywnego terminala. `shellHook` drukuje dostępne polecenia, bez automatycznej kompilacji. Przy kontrolach używaj `--locked`, aby nie aktualizować `Cargo.lock`. Formatowanie stosuj wyłącznie do uzgodnionych zmian; nie używaj automatycznego `clippy --fix` do porządkowania całego repozytorium. Ostatnie polecenie wymaga interaktywnego terminala. `shellHook` drukuje dostępne polecenia, bez automatycznej kompilacji. Przy kontrolach używaj `--locked`, aby nie aktualizować `Cargo.lock`. Formatowanie stosuj wyłącznie do uzgodnionych zmian; nie używaj automatycznego `clippy --fix` do porządkowania całego repozytorium.
Aktualny zestaw obejmuje 82 testów. Clippy ma istniejące ostrzeżenia dla biblioteki. `clippy::unused_unit` jest dozwolony ze względu na wymagane jawne `-> ()`. Zakres i polecenia opisano w [TESTING.md](TESTING.md). Nie traktuj tego jako potwierdzenia działania pełnej rozgrywki. Nowa nietrywialna logika wymaga małego testu regresji. Nie dodawaj zależności ani warstw abstrakcji bez potrzeby. Aktualny zestaw obejmuje 83 testy. Clippy ma istniejące ostrzeżenia dla biblioteki. `clippy::unused_unit` jest dozwolony ze względu na wymagane jawne `-> ()`. Zakres i polecenia opisano w [TESTING.md](TESTING.md). Nie traktuj tego jako potwierdzenia działania pełnej rozgrywki. Nowa nietrywialna logika wymaga małego testu regresji. Nie dodawaj zależności ani warstw abstrakcji bez potrzeby.
`Cargo.toml` wymaga Rust 1.88 lub nowszego, zabrania `unsafe` we własnym kodzie i ignorowania wyników `must_use`, a w release włącza kontrolę przepełnień. CI Gitea w `.gitea/workflows/rust.yml` sprawdza formatowanie, kompilację, testy debug/release i Clippy bez Nix, przez Rust 1.99.1 instalowany rustup; lokalnie nadal używaj Nix. Oddzielny `.gitea/workflows/nix.yml` buduje `.#default` z `default.nix`, z testami pakietu i kontrolą instalacji. Nie dodawaj `-D warnings` przed uporządkowaniem istniejących ostrzeżeń. `Cargo.toml` wymaga Rust 1.88 lub nowszego, zabrania `unsafe` we własnym kodzie i ignorowania wyników `must_use`, a w release włącza kontrolę przepełnień. CI Gitea w `.gitea/workflows/rust.yml` sprawdza formatowanie, kompilację, testy debug/release i Clippy bez Nix, przez Rust 1.99.1 instalowany rustup; lokalnie nadal używaj Nix. Oddzielny `.gitea/workflows/nix.yml` buduje `.#default` z `default.nix`, z testami pakietu i kontrolą instalacji. Nie dodawaj `-D warnings` przed uporządkowaniem istniejących ostrzeżeń.
@@ -42,7 +42,7 @@ Audio wymaga ALSA i urządzenia odtwarzania; do pracy nad TUI używaj `--disable
- Współrzędne planszy to `(row, col)`, indeksowanie `cells[row][col]`; rozmiar mapy i rozmiar widocznego obszaru to różne wartości. - Współrzędne planszy to `(row, col)`, indeksowanie `cells[row][col]`; rozmiar mapy i rozmiar widocznego obszaru to różne wartości.
- `CellWidget` przechowuje także dane gry. Zmiana modelu komórki dotyka planszy, jednostek, struktur i panelu bocznego. - `CellWidget` przechowuje także dane gry. Zmiana modelu komórki dotyka planszy, jednostek, struktur i panelu bocznego.
- Lista skrótów w widokach oraz tabela `KEYBINDINGS` muszą odpowiadać obsłudze akcji. - Lista skrótów w widokach oraz tabela `KEYBINDINGS` muszą odpowiadać obsłudze akcji.
- Konteksty mapy, trasy i budowania określa `available_skirmish_actions`. Globalne wyjście i muzyka działają niezależnie od legendy. Ctrl+Space otwiera budowanie dla wskazanego pola; Enter wymaga poprawnego miejsca, sąsiedniego własnego budowniczego i środków. Delete anuluje własną budowę i zwraca koszt, Esc tylko zamyka panel. - Konteksty mapy, trasy i budowania określa `available_skirmish_actions`. Globalne wyjście i muzyka działają niezależnie od legendy. b otwiera budowanie dla wskazanego pola; Enter wymaga poprawnego miejsca, sąsiedniego własnego budowniczego i środków. Delete anuluje własną budowę i zwraca koszt, Esc tylko zamyka panel.
- Bieżące zasoby są w SkirmishState, nie w ustawieniach CLI. Gameplay rozpoczyna budowę przez SkirmishState::start_mine_construction, aby objąć ją opłatą; BoardState::can_build_at/start_mine_construction obsługują samą walidację terenu i zmianę struktury. MineConstruction zachowuje oryginalne Ore i postępuje tylko przy sąsiednim własnym budowniczym. Mine zachowuje rezerwę Ore i kredytuje wyłącznie właściciela. Unit::get_can_build deleguje uprawnienia przez Units i Option. Ruch do gotowej własnej Mine korzysta ze Structures::can_enter i pozostaje Moving. Górnik na kratce kopalni podwaja wydobycie; nie twórz odrębnej kopii obsadzonej jednostki. Zmiany sprawdzaj przez tests/resources.rs, tests/context_keybindings.rs i tests/construction.rs. - Bieżące zasoby są w SkirmishState, nie w ustawieniach CLI. Gameplay rozpoczyna budowę przez SkirmishState::start_mine_construction, aby objąć ją opłatą; BoardState::can_build_at/start_mine_construction obsługują samą walidację terenu i zmianę struktury. MineConstruction zachowuje oryginalne Ore i postępuje tylko przy sąsiednim własnym budowniczym. Mine zachowuje rezerwę Ore i kredytuje wyłącznie właściciela. Unit::get_can_build deleguje uprawnienia przez Units i Option. Ruch do gotowej własnej Mine korzysta ze Structures::can_enter i pozostaje Moving. Górnik na kratce kopalni podwaja wydobycie; nie twórz odrębnej kopii obsadzonej jednostki. Zmiany sprawdzaj przez tests/resources.rs, tests/context_keybindings.rs i tests/construction.rs.
- Rekrutację opłacaj przez SkirmishState::recruit_miner, nie bezpośrednie BaseBuilding::queue_miner. Alt+Space otwiera panel tylko przy własnej bazie i poza innym trybem modalnym. Pierwsza rekrutacja może dojść do 100% na zajętej bazie, ale nie może nadpisać jednostki; kolejne stoją na 0% do wyjścia poprzedniego rekruta. BaseBuilding zachowuje licznik, postęp i waiting_for_departure; BoardState uruchamia spawn po ruchu istniejących jednostek. Sprawdzaj tests/recruitment.rs. - Rekrutację opłacaj przez SkirmishState::recruit_miner, nie bezpośrednie BaseBuilding::queue_miner. r otwiera panel tylko przy własnej bazie i poza innym trybem modalnym. Pierwsza rekrutacja może dojść do 100% na zajętej bazie, ale nie może nadpisać jednostki; kolejne stoją na 0% do wyjścia poprzedniego rekruta. BaseBuilding zachowuje licznik, postęp i waiting_for_departure; BoardState uruchamia spawn po ruchu istniejących jednostek. Sprawdzaj tests/recruitment.rs.
- `advance_turn` jest wywoływane przez tick potyczki. Zachowaj jedną aktualizację na jednostkę na tick, aktualność jej współrzędnych, kolejność zadań i ochronę zajętych pól. Regresje sprawdzaj przez `tests/digging.rs`. - `advance_turn` jest wywoływane przez tick potyczki. Zachowaj jedną aktualizację na jednostkę na tick, aktualność jej współrzędnych, kolejność zadań i ochronę zajętych pól. Regresje sprawdzaj przez `tests/digging.rs`.
+3 -3
View File
@@ -87,13 +87,13 @@ Mapa powstaje deterministycznie: kamień, baza gracza w `(map_height / 2, 2)`, b
MineConstruction zachowuje gotowy model Mine z oryginalnym Ore oraz licznik budowy. Po ruchu jednostek BoardState::advance_construction nalicza tick tylko przy sąsiednim własnym budowniczym; odejście pauzuje budowę, powrót wznawia. Po 304 tickach (10032 ms, około 10 sekund) struktura staje się Mine. Do tego czasu nie można do niej wejść ani wydobywać. Procenty są na kratce, w panelu Tab i widoku budowania. SkirmishState::cancel_mine_construction dopuszcza tylko własny aktywny plac, przywraca oryginalne neutralne Ore i jednokrotnie zwraca koszt; zakończonej kopalni nie da się w ten sposób sprzedać. Resources::spend sprawdza oba zasoby przed odjęciem. MineConstruction zachowuje gotowy model Mine z oryginalnym Ore oraz licznik budowy. Po ruchu jednostek BoardState::advance_construction nalicza tick tylko przy sąsiednim własnym budowniczym; odejście pauzuje budowę, powrót wznawia. Po 304 tickach (10032 ms, około 10 sekund) struktura staje się Mine. Do tego czasu nie można do niej wejść ani wydobywać. Procenty są na kratce, w panelu Tab i widoku budowania. SkirmishState::cancel_mine_construction dopuszcza tylko własny aktywny plac, przywraca oryginalne neutralne Ore i jednokrotnie zwraca koszt; zakończonej kopalni nie da się w ten sposób sprzedać. Resources::spend sprawdza oba zasoby przed odjęciem.
`available_skirmish_actions` jest wspólne dla obsługi wejścia i legendy. Na mapie spacja jest dostępna przy własnym górniku, a zoom zależy od poziomu. Rysowanie trasy ukrywa zoom, przewijanie i budowanie; Esc anuluje trasę. Ctrl+Space otwiera SkirmishState::building_view dla wskazanego pola. Poprawne pole z funduszami udostępnia Enter/Build Mine; panel pozostaje otwarty z procentami i Delete/Cancel construction dla własnej trwającej budowy. Esc lub Ctrl+Space tylko zamyka panel, nie anuluje budowy. Obsługiwany jest również NUL starszych terminali. Muzyka i wyjście są globalne. Widget legendy stosuje opisy zależne od kontekstu i szerokości kolumn proporcjonalne do tekstu, zachowując stałą wysokość stopki i geometrię mapy. `available_skirmish_actions` jest wspólne dla obsługi wejścia i legendy. Na mapie spacja jest dostępna przy własnym górniku, a zoom zależy od poziomu. Rysowanie trasy ukrywa zoom, przewijanie i budowanie; Esc anuluje trasę. b otwiera SkirmishState::building_view dla wskazanego pola. Poprawne pole z funduszami udostępnia Enter/Build Mine; panel pozostaje otwarty z procentami i Delete/Cancel construction dla własnej trwającej budowy. Esc lub b tylko zamyka panel, nie anuluje budowy. Muzyka i wyjście są globalne. Widget legendy stosuje opisy zależne od kontekstu i szerokości kolumn proporcjonalne do tekstu, zachowując stałą wysokość stopki i geometrię mapy.
`Structures::can_enter` jest wspólną regułą dla planowania trasy i jej wykonywania: baza i tunel pozostają przechodnie; Mine przyjmuje własnego Minera, jeżeli kratka nie jest zajęta. Wejście do kopalni jest Moving (około 3 sekund), również gdy zadanie wcześniej miało typ Digging. Obsadzony górnik pozostaje jedyną jednostką na kratce, można go zaznaczyć i wyprowadzić zwykłą trasą. Bonus wylicza się z bieżącej obecności jednostki po ruchu; nie ma osobnej kopii pracownika ani osobnego zadania pracy. `Structures::can_enter` jest wspólną regułą dla planowania trasy i jej wykonywania: baza i tunel pozostają przechodnie; Mine przyjmuje własnego Minera, jeżeli kratka nie jest zajęta. Wejście do kopalni jest Moving (około 3 sekund), również gdy zadanie wcześniej miało typ Digging. Obsadzony górnik pozostaje jedyną jednostką na kratce, można go zaznaczyć i wyprowadzić zwykłą trasą. Bonus wylicza się z bieżącej obecności jednostki po ruchu; nie ma osobnej kopii pracownika ani osobnego zadania pracy.
BaseBuilding przechowuje queued_miners, recruitment_ticks i waiting_for_departure. Ponieważ kolejka obsługuje wyłącznie identycznych Minerów, wystarcza licznik pozycji zamiast listy obiektów. SkirmishState::recruit_miner sprawdza typ i właściciela bazy, granice, pojemność licznika i MinerUnit::RECRUIT_COST (10 Wood, 15 Metal), po czym opłaca dodanie pozycji. BoardState::advance_recruitment jest wywoływane po ruchu jednostek, przed budowaniem i wydobyciem. Pierwszy Miner rekrutuje się przez 304 ticki (10032 ms), nawet na zajętej bazie; przy 100% czeka na wolne pole. Spawn tworzy jednostkę z właścicielem i współrzędnymi bazy oraz pustą kolejką zadań. Po spawnie waiting_for_departure blokuje odliczanie kolejnych pozycji, dopóki baza jest zajęta. Nigdy nie nadpisujemy obecnej jednostki. Każda baza ma osobną kolejkę; przeciwnik korzysta z własnych zasobów. BaseBuilding przechowuje queued_miners, recruitment_ticks i waiting_for_departure. Ponieważ kolejka obsługuje wyłącznie identycznych Minerów, wystarcza licznik pozycji zamiast listy obiektów. SkirmishState::recruit_miner sprawdza typ i właściciela bazy, granice, pojemność licznika i MinerUnit::RECRUIT_COST (10 Wood, 15 Metal), po czym opłaca dodanie pozycji. BoardState::advance_recruitment jest wywoływane po ruchu jednostek, przed budowaniem i wydobyciem. Pierwszy Miner rekrutuje się przez 304 ticki (10032 ms), nawet na zajętej bazie; przy 100% czeka na wolne pole. Spawn tworzy jednostkę z właścicielem i współrzędnymi bazy oraz pustą kolejką zadań. Po spawnie waiting_for_departure blokuje odliczanie kolejnych pozycji, dopóki baza jest zajęta. Nigdy nie nadpisujemy obecnej jednostki. Każda baza ma osobną kolejkę; przeciwnik korzysta z własnych zasobów.
Alt+Space lub r jest dostępne na własnej Base poza rysowaniem trasy i widokiem budowania. SkirmishState::recruitment_view otwiera modal z kosztem, liczbą pozycji, procentem i statusem Recruiting/Ready/Paused. Enter dodaje Minera tylko przy wystarczających zasobach. Esc, r lub Alt+Space zamyka panel, zachowując kolejkę; ruch mapy i otwarcie innego modalu są wtedy zablokowane. Opisy w legendzie zależą od trybu, a postęp i liczba pozycji są też pod Tab. r jest dostępne na własnej Base poza rysowaniem trasy i widokiem budowania. SkirmishState::recruitment_view otwiera modal z kosztem, liczbą pozycji, procentem i statusem Recruiting/Ready/Paused. Enter dodaje Minera tylko przy wystarczających zasobach. Esc lub r zamyka panel, zachowując kolejkę; ruch mapy i otwarcie innego modalu są wtedy zablokowane. Opisy w legendzie zależą od trybu, a postęp i liczba pozycji są też pod Tab.
## Klawisze i zlecanie kopania ## Klawisze i zlecanie kopania
@@ -133,7 +133,7 @@ To obserwacje statyczne, nie lista napraw wykonanych w tej sesji:
- **Zaznaczenie:** wybrany górnik czeka podczas rysowania trasy; inne jednostki wykonują zadania normalnie. - **Zaznaczenie:** wybrany górnik czeka podczas rysowania trasy; inne jednostki wykonują zadania normalnie.
- **Audio:** usunięto aktywne odpytywanie; pomiar CPU na docelowym urządzeniu i odsłuch pozostają do wykonania. Test dekodera nie weryfikuje urządzenia audio. - **Audio:** usunięto aktywne odpytywanie; pomiar CPU na docelowym urządzeniu i odsłuch pozostają do wykonania. Test dekodera nie weryfikuje urządzenia audio.
- **Granice UI:** małe terminale, resize i trzy poziomy zoomu wymagają interaktywnego sprawdzenia. Indeksowanie planszy zakłada poprawne współrzędne. - **Granice UI:** małe terminale, resize i trzy poziomy zoomu wymagają interaktywnego sprawdzenia. Indeksowanie planszy zakłada poprawne współrzędne.
- **Pokrycie:** 82 testów CLI, sterowania i kontekstowej legendy, renderowania, planszy, zasobów, budowania/obsadzania kopalni, rekrutacji i blokowania kolejki bazy, zadań i dekodowania audio; szczegóły w [TESTING.md](TESTING.md). Workflow Gitea sprawdza też profil release; nie obejmuje pełnej rozgrywki. Clippy ma istniejące ostrzeżenia, unused_unit jest jawnie dozwolony. Nie uruchamiać automatycznych poprawek poza zakresem zadania. - **Pokrycie:** 83 testy CLI, sterowania i kontekstowej legendy, renderowania, planszy, zasobów, budowania/obsadzania kopalni, rekrutacji i blokowania kolejki bazy, zadań i dekodowania audio; szczegóły w [TESTING.md](TESTING.md). Workflow Gitea sprawdza też profil release; nie obejmuje pełnej rozgrywki. Clippy ma istniejące ostrzeżenia, unused_unit jest jawnie dozwolony. Nie uruchamiać automatycznych poprawek poza zakresem zadania.
## Gdzie zacząć konkretną zmianę ## Gdzie zacząć konkretną zmianę
+12
View File
@@ -1,5 +1,17 @@
# Dziennik prac # Dziennik prac
## 2026-10-21 — spójne skróty głośności
Przyciszanie muzyki przeniesiono z `n` na `-`, zgodnie z parą `+`/`-`. Zaktualizowano wspólną tabelę skrótów (także legendę), istniejący test komend audio i README. Kompilacja wszystkich targetów, testy `tests/app.rs` i kontrola formatowania przez Nix przeszły. Nie sprawdzano odsłuchu.
## 2026-10-21 — skróty b/r i kolory okien budowy oraz rekrutacji
Zastąpiono Ctrl+Space przez `b`, a Alt+Space przez `r`; usunięto także obsługę NUL dla budowania. Oba klawisze otwierają i zamykają właściwy panel. Ponieważ `b` sterowało głośnością, zwiększanie głośności przeniesiono na `+`. Legenda korzysta ze wspólnej tabeli skrótów.
Okna używają kolorowanych linii Ratatui: żółte nazwy i koszty, cyjanowe postępy i produkcja, zielone dostępne działania, czerwone błędy oraz żółte komunikaty oczekiwania. Zachowano tekstowe opisy statusów. Zaktualizowano testy i bieżącą dokumentację; dodano regresję kolorów obu okien i sprawdzenie odrzucania starych skrótów.
Weryfikacja: kompilacja wszystkich targetów i 83 testy debug przez `nix develop -c` przeszły. Nie sprawdzano interaktywnego terminala ani odsłuchu. Zastanej zmiany `flake.lock` nie modyfikowano.
## 2026-10-19 — alternatywny skrót rekrutacji ## 2026-10-19 — alternatywny skrót rekrutacji
Zgłoszono brak reakcji na Alt+Space. Testy przekazywanego zdarzenia Alt+Space działają; nie potwierdzono, jakie zdarzenie wysyła terminal użytkownika ani czy skrót przechwytuje menedżer okien. Dodano alternatywne `r` do tej samej akcji Recruitment i pokazano oba skróty w legendzie. Obowiązują te same ograniczenia własnej bazy i trybu; `r` także zamyka panel bez kasowania kolejki. README opisuje alternatywę. Zgłoszono brak reakcji na Alt+Space. Testy przekazywanego zdarzenia Alt+Space działają; nie potwierdzono, jakie zdarzenie wysyła terminal użytkownika ani czy skrót przechwytuje menedżer okien. Dodano alternatywne `r` do tej samej akcji Recruitment i pokazano oba skróty w legendzie. Obowiązują te same ograniczenia własnej bazy i trybu; `r` także zamyka panel bez kasowania kolejki. README opisuje alternatywę.
+4 -4
View File
@@ -18,12 +18,12 @@ Testy korzystają z wbudowanego mechanizmu Rust, Clap i `ratatui::backend::TestB
| `tests/tasks.rs` | 7 | Przyrost i limit kopania, reset etapu, pusta ścieżka, kolejność zadań, timer MovingTask, całkowity postęp, zachowanie liczby etapów przy konwersji, formatowanie postępu aktywnych i oczekujących zadań | | `tests/tasks.rs` | 7 | Przyrost i limit kopania, reset etapu, pusta ścieżka, kolejność zadań, timer MovingTask, całkowity postęp, zachowanie liczby etapów przy konwersji, formatowanie postępu aktywnych i oczekujących zadań |
| `tests/audio.rs` | 2 | Zapętlanie rzeczywistego pliku OGG po jego końcu i ładowanie soundtracków obok binarki poza katalogiem repozytorium | | `tests/audio.rs` | 2 | Zapętlanie rzeczywistego pliku OGG po jego końcu i ładowanie soundtracków obok binarki poza katalogiem repozytorium |
| `tests/resources.rs` | 7 | Stan z CLI, alias starting-iron, pasywne Wood, neutralne złoża, brak kopalń na starcie, budowa wyłącznie na Ore, wyczerpanie złoża i przychód właściciela, ochrona przepełnień i bieżące zasoby w TUI | | `tests/resources.rs` | 7 | Stan z CLI, alias starting-iron, pasywne Wood, neutralne złoża, brak kopalń na starcie, budowa wyłącznie na Ore, wyczerpanie złoża i przychód właściciela, ochrona przepełnień i bieżące zasoby w TUI |
| `tests/context_keybindings.rs` | 4 | Kontekst mapy i trasy, anulowanie przez Esc, blokada budowania na nieprawidłowym polu, Ctrl+Space i NUL ze starszych terminali, globalne wyjście i czytelność legendy przy 120 kolumnach | | `tests/context_keybindings.rs` | 5 | Kontekst mapy i trasy, anulowanie przez Esc, blokada budowania na nieprawidłowym polu, b i odrzucenie starych skrótów, globalne wyjście i czytelność legendy przy 120 kolumnach i kolory tekstu obu okien |
| `tests/construction.rs` | 16 | Uprawnienia budowniczego i delegacja, sąsiedztwo i brak zdalnej budowy, właściciel, reguła kamienia i wyjątek Ore, ochrona zajętych/wyczerpanych/wrogich złóż, atomowa opłata, 10 sekund budowy, pauza i wznowienie, brak przedwczesnego wydobycia/wejścia, procenty i anulowanie Delete w TUI, jednokrotny zwrot i zachowanie Ore, ruch do/z Mine i bonus wydobycia, ochrona rezerwy | | `tests/construction.rs` | 16 | Uprawnienia budowniczego i delegacja, sąsiedztwo i brak zdalnej budowy, właściciel, reguła kamienia i wyjątek Ore, ochrona zajętych/wyczerpanych/wrogich złóż, atomowa opłata, 10 sekund budowy, pauza i wznowienie, brak przedwczesnego wydobycia/wejścia, procenty i anulowanie Delete w TUI, jednokrotny zwrot i zachowanie Ore, ruch do/z Mine i bonus wydobycia, ochrona rezerwy |
| `tests/recruitment.rs` | 9 | Wyłącznie własna Base, atomowy koszt każdej pozycji i brak obciążenia przy błędzie, 10 sekund rekrutacji, oczekiwanie przy 100%, brak nadpisania jednostki, blokada kolejnych pozycji do wyjścia poprzedniego rekruta, wznowienie po ruchu, niezależne bazy i właściciele, Alt+Space/r/Enter/Esc i kontekstowa legenda | | `tests/recruitment.rs` | 9 | Wyłącznie własna Base, atomowy koszt każdej pozycji i brak obciążenia przy błędzie, 10 sekund rekrutacji, oczekiwanie przy 100%, brak nadpisania jednostki, blokada kolejnych pozycji do wyjścia poprzedniego rekruta, wznowienie po ruchu, niezależne bazy i właściciele, r/Enter/Esc i kontekstowa legenda |
| `tests/digging.rs` | 18 | Wykonywanie kopania i ruchu, kolejki, 3 sekundy na krok, wejście po wykopaniu, blokady i walidacja, kolory i procenty w TUI, podział trasy Moving/Digging, ochrona struktur, zmiana rodzaju zadania, panel Tab z postępem etapu i całego zadania | | `tests/digging.rs` | 18 | Wykonywanie kopania i ruchu, kolejki, 3 sekundy na krok, wejście po wykopaniu, blokady i walidacja, kolory i procenty w TUI, podział trasy Moving/Digging, ochrona struktur, zmiana rodzaju zadania, panel Tab z postępem etapu i całego zadania |
Łącznie: 82 testów. Wszystkie testy znajdują się w `tests/` i sprawdzają publiczne interfejsy biblioteki lub binarkę. Funkcja `load_audio` jest eksportowana przez `app::threads`, dzięki czemu test korzysta z tego samego dekodera co odtwarzanie. Łącznie: 83 testy. Wszystkie testy znajdują się w `tests/` i sprawdzają publiczne interfejsy biblioteki lub binarkę. Funkcja `load_audio` jest eksportowana przez `app::threads`, dzięki czemu test korzysta z tego samego dekodera co odtwarzanie.
Wybrane zestawy i pojedynczy przypadek: Wybrane zestawy i pojedynczy przypadek:
@@ -59,7 +59,7 @@ Workflow określa obraz zadania jawnie jako `node:22-bookworm`. Kontener działa
Wersję Rust w CI określa `RUST_VERSION` w workflow, niezależnie od lokalnego `flake.lock`. Formatowanie, kompilacja, testy i Clippy są oddzielnymi krokami Cargo. Polecenia kompilujące projekt mają `--locked`; Clippy nie używa `-D warnings` z powodu istniejących ostrzeżeń. `actions/cache@v4` zachowuje osobno toolchain oraz rejestr Cargo, repozytoria git i `target`. Klucze uwzględniają wersję Rust, a dla Cargo także manifest, lockfile i kod; klucze częściowe pozwalają ponownie wykorzystać wcześniejsze kompilacje. Działanie cache wymaga dostępnego serwera cache runnera. Pierwszy przebieg wypełnia cache; błędy pobierania cache mogą spowodować ponowną instalację i kompilację. Wersję Rust w CI określa `RUST_VERSION` w workflow, niezależnie od lokalnego `flake.lock`. Formatowanie, kompilacja, testy i Clippy są oddzielnymi krokami Cargo. Polecenia kompilujące projekt mają `--locked`; Clippy nie używa `-D warnings` z powodu istniejących ostrzeżeń. `actions/cache@v4` zachowuje osobno toolchain oraz rejestr Cargo, repozytoria git i `target`. Klucze uwzględniają wersję Rust, a dla Cargo także manifest, lockfile i kod; klucze częściowe pozwalają ponownie wykorzystać wcześniejsze kompilacje. Działanie cache wymaga dostępnego serwera cache runnera. Pierwszy przebieg wypełnia cache; błędy pobierania cache mogą spowodować ponowną instalację i kompilację.
Release ma `overflow-checks = true`. Ten sam zestaw 82 testów można wykonać z kontrolą przepełnień w profilu produkcyjnym: Release ma `overflow-checks = true`. Ten sam zestaw 83 testów można wykonać z kontrolą przepełnień w profilu produkcyjnym:
```sh ```sh
nix develop -c cargo test --all-targets --release --locked nix develop -c cargo test --all-targets --release --locked
Generated
+9 -9
View File
@@ -8,11 +8,11 @@
"rust-analyzer-src": "rust-analyzer-src" "rust-analyzer-src": "rust-analyzer-src"
}, },
"locked": { "locked": {
"lastModified": 1792320163, "lastModified": 1792494797,
"narHash": "sha256-3YqCuAdykExGnEFw44w+8qsjdOEZMBCViDpvue5ENoc=", "narHash": "sha256-W0hLL8YLjWT/QlHcnNbON3EeDostg+GD9fw6rv4jbyY=",
"owner": "nix-community", "owner": "nix-community",
"repo": "fenix", "repo": "fenix",
"rev": "53d7844fe302be84e5e63b2ca92a2f84c6490b4e", "rev": "e77f56ee0521b21e5a987dd3e88b6bb367d777a7",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -46,11 +46,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1792094642, "lastModified": 1792502884,
"narHash": "sha256-zp8ToDMnqZcu4o4IH9pdwXFXGp7yLm/M0dEnNWnyd7I=", "narHash": "sha256-IoNisKcGbUjdd45qCqywGMCYmZ5EnkxezvLII0FWwNA=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "fe13242281a67641c0102cdb1087f0ebe586c7df", "rev": "02c087f3fe673c9e317f206a627cb6b910336dbe",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -70,11 +70,11 @@
"rust-analyzer-src": { "rust-analyzer-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1792141191, "lastModified": 1792431056,
"narHash": "sha256-H161gJK9JlpkP3bGnsbTdzzRcP51oiFYrHbCdsQH4kc=", "narHash": "sha256-FiKlJK+NoB9J9+xIQPfJaRfK42msh55Pt6FsJ++NcxY=",
"owner": "rust-lang", "owner": "rust-lang",
"repo": "rust-analyzer", "repo": "rust-analyzer",
"rev": "70e0c46be740a471bcc95f2be290f4bbfdcffd87", "rev": "7afa765037d97a62b53c59907e9f503409f2a673",
"type": "github" "type": "github"
}, },
"original": { "original": {
+7 -23
View File
@@ -112,15 +112,6 @@ pub struct KeyBinding {
/// The slice is ordered primarily for readability; the runtime lookup scans /// The slice is ordered primarily for readability; the runtime lookup scans
/// this slice linearly, which is acceptable given the modest size of the list. /// this slice linearly, which is acceptable given the modest size of the list.
static KEYBINDINGS: &[KeyBinding] = &[ static KEYBINDINGS: &[KeyBinding] = &[
KeyBinding {
action: Action::Recruitment,
code: KeyCode::Char(' '),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::ALT,
group: Group::Opener,
symbol: "Alt + Space / r",
description: "Recruit",
},
KeyBinding { KeyBinding {
action: Action::Recruitment, action: Action::Recruitment,
code: KeyCode::Char('r'), code: KeyCode::Char('r'),
@@ -132,11 +123,11 @@ static KEYBINDINGS: &[KeyBinding] = &[
}, },
KeyBinding { KeyBinding {
action: Action::Building, action: Action::Building,
code: KeyCode::Char(' '), code: KeyCode::Char('b'),
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::NONE,
group: Group::Opener, group: Group::Opener,
symbol: "Ctrl + Space", symbol: "b",
description: "Buildings", description: "Buildings",
}, },
KeyBinding { KeyBinding {
@@ -303,20 +294,20 @@ static KEYBINDINGS: &[KeyBinding] = &[
}, },
KeyBinding { KeyBinding {
action: Action::VolumeUp, action: Action::VolumeUp,
code: KeyCode::Char('b'), code: KeyCode::Char('+'),
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
group: Group::Music, group: Group::Music,
symbol: "b", symbol: "+",
description: "🔊", description: "🔊",
}, },
KeyBinding { KeyBinding {
action: Action::VolumeDown, action: Action::VolumeDown,
code: KeyCode::Char('n'), code: KeyCode::Char('-'),
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
group: Group::Music, group: Group::Music,
symbol: "n", symbol: "-",
description: "🔉", description: "🔉",
}, },
KeyBinding { KeyBinding {
@@ -361,13 +352,6 @@ pub fn binding_for(action: Action) -> Option<&'static KeyBinding> {
/// was pressed. Returns `None` when the event does not correspond to any known /// was pressed. Returns `None` when the event does not correspond to any known
/// action. /// action.
pub fn event_to_action(event: &KeyEvent) -> Option<Action> { pub fn event_to_action(event: &KeyEvent) -> Option<Action> {
// Legacy terminals encode Ctrl+Space as NUL instead of a modified space.
if event.code == KeyCode::Null
&& event.kind == KeyEventKind::Press
&& (event.modifiers == KeyModifiers::NONE || event.modifiers == KeyModifiers::CONTROL)
{
return Some(Action::Building);
}
if let Some(b) = KEYBINDINGS if let Some(b) = KEYBINDINGS
.iter() .iter()
.find(|b| b.code == event.code && b.kind == event.kind && b.modifiers == event.modifiers) .find(|b| b.code == event.code && b.kind == event.kind && b.modifiers == event.modifiers)
+55 -26
View File
@@ -203,65 +203,94 @@ pub fn skirmish_view(app: &App, area: Rect, buf: &mut Buffer) {
let popup: Rect = main_area.centered(Constraint::Length(64), Constraint::Length(14)); let popup: Rect = main_area.centered(Constraint::Length(64), Constraint::Length(14));
let coords: (usize, usize) = board.get_focused_cell().get_coords(); let coords: (usize, usize) = board.get_focused_cell().get_coords();
let structure: Structures = board.get_ref_cell(coords.0, coords.1).get_structure(); let structure: Structures = board.get_ref_cell(coords.0, coords.1).get_structure();
let status: String = if let Structures::MineConstruction(site) = structure let status: Vec<Line<'_>> = if let Structures::MineConstruction(site) = structure
&& site.get_owner() == Players::Player && site.get_owner() == Players::Player
{ {
format!( vec![
"Construction: {}%\n{}\nDelete: cancel and refund full cost", Line::from(format!("Construction: {}%", site.progress_percent())).cyan(),
site.progress_percent(),
if board.has_adjacent_builder(coords, Players::Player) { if board.has_adjacent_builder(coords, Players::Player) {
"Builder working" Line::from("Builder working").green()
} else { } else {
"Paused: adjacent friendly builder required" Line::from("Paused: adjacent friendly builder required").yellow()
} },
) Line::from("Delete: cancel and refund full cost").yellow(),
]
} else if matches!(structure, Structures::Mine(_)) } else if matches!(structure, Structures::Mine(_))
&& structure.get_owner() == Players::Player && structure.get_owner() == Players::Player
{ {
"Mine completed".to_string() vec![Line::from("Mine completed").green()]
} else if states } else if states
.skirmish .skirmish
.can_start_mine_construction(coords, Players::Player) .can_start_mine_construction(coords, Players::Player)
{ {
"Ready to build: Enter".to_string() vec![Line::from("Ready to build: Enter").green()]
} else { } else {
"Construction unavailable: check site, builder and resources".to_string() vec![Line::from("Construction unavailable: check site, builder and resources").red()]
}; };
let mut lines: Vec<Line<'_>> = vec![
Line::from("Mine").yellow().bold(),
Line::from(format!(
"Cost: {} Wood + {} Metal | Build time: 10s",
Mine::COST.wood,
Mine::COST.metal
))
.yellow(),
Line::from("Requires: neutral Ore + adjacent friendly builder").gray(),
Line::from("Produces: 1 Metal/s (2 with a Miner inside)").cyan(),
];
lines.extend(status);
Clear.render(popup, buf); Clear.render(popup, buf);
Paragraph::new(format!("Mine\nCost: {} Wood + {} Metal | Build time: 10s\nRequires: neutral Ore + adjacent friendly builder\nProduces: 1 Metal/s (2 with a Miner inside)\n{status}", Mine::COST.wood, Mine::COST.metal)) Paragraph::new(lines)
.block(Block::bordered().title("Buildings").padding(Padding::uniform(1))) .block(
Block::bordered()
.title(Line::from("Buildings").magenta().bold())
.padding(Padding::uniform(1)),
)
.render(popup, buf); .render(popup, buf);
} }
if states.skirmish.recruitment_view { if states.skirmish.recruitment_view {
let popup: Rect = main_area.centered(Constraint::Length(64), Constraint::Length(14)); let popup: Rect = main_area.centered(Constraint::Length(64), Constraint::Length(14));
let coords: (usize, usize) = board.get_focused_cell().get_coords(); let coords: (usize, usize) = board.get_focused_cell().get_coords();
let cell: &CellWidget = board.get_ref_cell(coords.0, coords.1); let cell: &CellWidget = board.get_ref_cell(coords.0, coords.1);
let text: String = if let Structures::Base(base) = cell.get_structure() let lines: Vec<Line<'_>> = if let Structures::Base(base) = cell.get_structure()
&& base.get_owner() == Players::Player && base.get_owner() == Players::Player
{ {
let status: &'static str = let status: &'static str =
base.recruitment_status(cell.get_ref_option_unit().is_some()); base.recruitment_status(cell.get_ref_option_unit().is_some());
let action: &'static str = if states.skirmish.can_recruit_miner(coords, Players::Player) let action: Line<'_> = if states.skirmish.can_recruit_miner(coords, Players::Player) {
{ Line::from("Enter: queue Miner").green()
"Enter: queue Miner"
} else { } else {
"Insufficient resources or queue full" Line::from("Insufficient resources or queue full").red()
}; };
format!( vec![
"Miner\nCost: {} Wood + {} Metal | Recruit time: 10s\nQueued Miners: {} (includes active)\nCurrent progress: {}%\n{status}\n{action}", Line::from("Miner").yellow().bold(),
Line::from(format!(
"Cost: {} Wood + {} Metal | Recruit time: 10s",
MinerUnit::RECRUIT_COST.wood, MinerUnit::RECRUIT_COST.wood,
MinerUnit::RECRUIT_COST.metal, MinerUnit::RECRUIT_COST.metal
base.queued_miners(), ))
.yellow(),
Line::from(format!(
"Queued Miners: {} (includes active)",
base.queued_miners()
))
.cyan(),
Line::from(format!(
"Current progress: {}%",
base.recruitment_progress() base.recruitment_progress()
) ))
.cyan(),
Line::from(status).yellow(),
action,
]
} else { } else {
"Select your Base to recruit Miners".to_string() vec![Line::from("Select your Base to recruit Miners").red()]
}; };
Clear.render(popup, buf); Clear.render(popup, buf);
Paragraph::new(text) Paragraph::new(lines)
.block( .block(
Block::bordered() Block::bordered()
.title("Recruitment") .title(Line::from("Recruitment").magenta().bold())
.padding(Padding::uniform(1)), .padding(Padding::uniform(1)),
) )
.render(popup, buf); .render(popup, buf);
+2 -2
View File
@@ -82,8 +82,8 @@ fn key_release_does_not_trigger_quit() {
fn music_keys_send_commands_without_opening_audio_device() { fn music_keys_send_commands_without_opening_audio_device() {
let (mut app, rx) = app(); let (mut app, rx) = app();
press(&mut app, KeyCode::Char('m')); press(&mut app, KeyCode::Char('m'));
press(&mut app, KeyCode::Char('b')); press(&mut app, KeyCode::Char('+'));
press(&mut app, KeyCode::Char('n')); press(&mut app, KeyCode::Char('-'));
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::Mute)); assert!(matches!(rx.try_recv().unwrap(), AudioCmd::Mute));
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeUp)); assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeUp));
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeDown)); assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeDown));
+3 -3
View File
@@ -145,7 +145,7 @@ fn construction_view_starts_paid_build_on_enter_and_stays_open() -> () {
} }
handle_keybindings( handle_keybindings(
&mut app, &mut app,
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL), KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE),
); );
assert!( assert!(
available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter) available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter)
@@ -361,7 +361,7 @@ fn panel_shows_progress_and_delete_cancels_while_escape_only_closes_panel() -> (
} }
handle_keybindings( handle_keybindings(
&mut app, &mut app,
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL), KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE),
); );
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert!( assert!(
@@ -395,7 +395,7 @@ fn panel_shows_progress_and_delete_cancels_while_escape_only_closes_panel() -> (
)); ));
handle_keybindings( handle_keybindings(
&mut app, &mut app,
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL), KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE),
); );
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE)); handle_keybindings(&mut app, KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE));
assert!(matches!( assert!(matches!(
+53 -8
View File
@@ -5,6 +5,7 @@ use ratatui::{
buffer::Cell, buffer::Cell,
crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
layout::Rect, layout::Rect,
style::Color,
}; };
use std::sync::mpsc::{Receiver, Sender, channel}; use std::sync::mpsc::{Receiver, Sender, channel};
use war_in_tunnels::{ use war_in_tunnels::{
@@ -45,7 +46,7 @@ fn selecting_miner_switches_help_and_escape_cancels_only_the_route() -> () {
assert!(actions.contains(&Action::Delete)); assert!(actions.contains(&Action::Delete));
assert!(!actions.contains(&Action::Building)); assert!(!actions.contains(&Action::Building));
assert!(!actions.contains(&Action::ZoomIn)); assert!(!actions.contains(&Action::ZoomIn));
press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL); press(&mut app, KeyCode::Char('b'), KeyModifiers::NONE);
assert!(!app.states_ref().unwrap().skirmish.building_view); assert!(!app.states_ref().unwrap().skirmish.building_view);
press(&mut app, KeyCode::Esc, KeyModifiers::NONE); press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
assert_eq!(app.view, View::Skirmish); assert_eq!(app.view, View::Skirmish);
@@ -72,7 +73,7 @@ fn building_view_has_its_own_help_and_does_not_build_on_invalid_tile() -> () {
let mut app: App = app(); let mut app: App = app();
let before: war_in_tunnels::app::states::skirmish_states::BoardState = let before: war_in_tunnels::app::states::skirmish_states::BoardState =
app.states_ref().unwrap().skirmish.board.clone(); app.states_ref().unwrap().skirmish.board.clone();
press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL); press(&mut app, KeyCode::Char('b'), KeyModifiers::NONE);
assert!(app.states_ref().unwrap().skirmish.building_view); assert!(app.states_ref().unwrap().skirmish.building_view);
assert_eq!( assert_eq!(
available_skirmish_actions(&app.states_ref().unwrap().skirmish), available_skirmish_actions(&app.states_ref().unwrap().skirmish),
@@ -109,13 +110,17 @@ fn building_view_has_its_own_help_and_does_not_build_on_invalid_tile() -> () {
} }
#[test] #[test]
fn legacy_control_space_and_quit_work_in_building_view() -> () { fn building_key_toggles_panel_and_old_shortcuts_do_not_open_it() -> () {
let mut app: App = app(); let mut app: App = app();
press(&mut app, KeyCode::Null, KeyModifiers::NONE); press(&mut app, KeyCode::Null, KeyModifiers::NONE);
assert!(app.states_ref().unwrap().skirmish.building_view); press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL);
press(&mut app, KeyCode::Null, KeyModifiers::CONTROL); press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
assert!(!app.states_ref().unwrap().skirmish.building_view); assert!(!app.states_ref().unwrap().skirmish.building_view);
press(&mut app, KeyCode::Null, KeyModifiers::NONE); press(&mut app, KeyCode::Char('b'), KeyModifiers::NONE);
assert!(app.states_ref().unwrap().skirmish.building_view);
press(&mut app, KeyCode::Char('b'), KeyModifiers::NONE);
assert!(!app.states_ref().unwrap().skirmish.building_view);
press(&mut app, KeyCode::Char('b'), KeyModifiers::NONE);
press(&mut app, KeyCode::Char('c'), KeyModifiers::CONTROL); press(&mut app, KeyCode::Char('c'), KeyModifiers::CONTROL);
assert!(app.exit); assert!(app.exit);
} }
@@ -138,7 +143,7 @@ fn contextual_help_fits_normal_terminal_and_changes_when_route_is_selected() ->
.iter() .iter()
.map(|cell: &Cell| -> &str { cell.symbol() }) .map(|cell: &Cell| -> &str { cell.symbol() })
.collect(); .collect();
assert!(text.contains("Ctrl + Space - Build")); assert!(text.contains("b - Build"));
assert!(text.contains("Space - Select miner")); assert!(text.contains("Space - Select miner"));
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE); press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
terminal terminal
@@ -155,6 +160,46 @@ fn contextual_help_fits_normal_terminal_and_changes_when_route_is_selected() ->
.collect(); .collect();
assert!(text.contains("Space - Confirm route")); assert!(text.contains("Space - Confirm route"));
assert!(text.contains("Esc - Cancel route")); assert!(text.contains("Esc - Cancel route"));
assert!(!text.contains("Ctrl + Space")); assert!(!text.contains("b - Build"));
assert!(!text.contains("Zoom in")); assert!(!text.contains("Zoom in"));
} }
#[test]
fn construction_and_recruitment_popups_render_semantic_text_colors() -> () {
for (key, name, status, status_color) in [
('b', "Mine", "Construction unavailable", Color::Red),
('r', "Miner", "Enter: queue Miner", Color::Green),
] {
let mut app: App = app();
if key == 'r' {
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
}
press(&mut app, KeyCode::Char(key), KeyModifiers::NONE);
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(160, 40)).unwrap();
terminal
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
frame.render_widget(&mut app, frame.area());
})
.unwrap();
let cells: &[Cell] = &terminal.backend().buffer().content;
for (text, color) in [
(name, Color::Yellow),
("Cost:", Color::Yellow),
(status, status_color),
] {
assert!(
cells.windows(text.len()).any(|window: &[Cell]| -> bool {
window
.iter()
.map(|cell: &Cell| -> &str { cell.symbol() })
.collect::<String>()
== text
&& window
.iter()
.all(|cell: &Cell| -> bool { cell.fg == color })
}),
"missing colored text: {text}"
);
}
}
}
+10 -8
View File
@@ -234,7 +234,7 @@ fn recruitment_panel_queues_with_enter_shows_ready_state_and_closes_without_losi
available_skirmish_actions(&app.states_ref().unwrap().skirmish) available_skirmish_actions(&app.states_ref().unwrap().skirmish)
.contains(&Action::Recruitment) .contains(&Action::Recruitment)
); );
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
assert!(app.states_ref().unwrap().skirmish.recruitment_view); assert!(app.states_ref().unwrap().skirmish.recruitment_view);
press(&mut app, KeyCode::Enter, KeyModifiers::NONE); press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
press(&mut app, KeyCode::Enter, KeyModifiers::NONE); press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
@@ -246,7 +246,7 @@ fn recruitment_panel_queues_with_enter_shows_ready_state_and_closes_without_losi
!available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter) !available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter)
); );
press(&mut app, KeyCode::Right, KeyModifiers::NONE); press(&mut app, KeyCode::Right, KeyModifiers::NONE);
press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL); press(&mut app, KeyCode::Char('b'), KeyModifiers::NONE);
assert_eq!( assert_eq!(
app.states_ref() app.states_ref()
.unwrap() .unwrap()
@@ -281,8 +281,8 @@ fn recruitment_panel_queues_with_enter_shows_ready_state_and_closes_without_losi
press(&mut app, KeyCode::Esc, KeyModifiers::NONE); press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
assert!(!app.states_ref().unwrap().skirmish.recruitment_view); assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
assert_eq!(app.view, View::Skirmish); assert_eq!(app.view, View::Skirmish);
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
assert!(!app.states_ref().unwrap().skirmish.recruitment_view); assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
assert_eq!( assert_eq!(
base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(), base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(),
@@ -295,13 +295,13 @@ fn recruitment_panel_is_unavailable_off_base_and_during_route_selection() -> ()
let mut app: App = app(); let mut app: App = app();
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE); press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
assert!(!app.states_ref().unwrap().skirmish.recruitment_view); assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
assert!(!app.states_ref().unwrap().skirmish.recruitment_view); assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
press(&mut app, KeyCode::Left, KeyModifiers::NONE); press(&mut app, KeyCode::Left, KeyModifiers::NONE);
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE); press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE); press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
assert!(!app.states_ref().unwrap().skirmish.recruitment_view); assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
assert!(!app.states_ref().unwrap().skirmish.recruitment_view); assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
assert!( assert!(
app.states_ref() app.states_ref()
@@ -312,15 +312,17 @@ fn recruitment_panel_is_unavailable_off_base_and_during_route_selection() -> ()
.marking_cells .marking_cells
); );
press(&mut app, KeyCode::Esc, KeyModifiers::NONE); press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
press(&mut app, KeyCode::Char('c'), KeyModifiers::CONTROL); press(&mut app, KeyCode::Char('c'), KeyModifiers::CONTROL);
assert!(app.exit); assert!(app.exit);
} }
#[test] #[test]
fn recruitment_fallback_opens_queues_and_closes_on_own_base() -> () { fn recruitment_key_opens_queues_and_closes_on_own_base() -> () {
let mut app: App = app(); let mut app: App = app();
press(&mut app, KeyCode::Left, KeyModifiers::NONE); press(&mut app, KeyCode::Left, KeyModifiers::NONE);
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE); press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
assert!(app.states_ref().unwrap().skirmish.recruitment_view); assert!(app.states_ref().unwrap().skirmish.recruitment_view);
press(&mut app, KeyCode::Enter, KeyModifiers::NONE); press(&mut app, KeyCode::Enter, KeyModifiers::NONE);