Compare commits
2 Commits
a0a2278421
...
f4b3fe79ae
| Author | SHA1 | Date | |
|---|---|---|---|
| f4b3fe79ae | |||
| 8b50c5f5b9 |
10
artifacts/balance_after_console.log
Normal file
10
artifacts/balance_after_console.log
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
[MCPRuntimeServer] port: scanning 6570-6585 (default)
|
||||||
|
[MCPRuntimeServer] listening on 127.0.0.1:6570
|
||||||
|
[MCPRegistry] runtime port 6570 registered for c:/users/tomek/documents/godot/randoloot
|
||||||
|
SIMULATION_PROFILE_DONE: rookie_energy
|
||||||
|
SIMULATION_PROFILE_DONE: energy
|
||||||
|
SIMULATION_PROFILE_DONE: machine
|
||||||
|
SIMULATION_PROFILE_DONE: gamma
|
||||||
|
BALANCE_SIMULATION_50: DONE 12000 rows
|
||||||
4
artifacts/balance_after_errors.log
Normal file
4
artifacts/balance_after_errors.log
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
WARNING: Camera2D overridden to physics process mode due to use of physics interpolation.
|
||||||
|
at: _update_process_callback (scene/2d/camera_2d.cpp:88)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] _ready (res://tests/levels/balance_simulation_50.gd:243)
|
||||||
1
artifacts/balance_after_summary.json
Normal file
1
artifacts/balance_after_summary.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
[{"profile":"rookie_energy","all_skills":[31,32,33],"first_boss_seconds":[50.9062027119016,54.0371225638878,54.6218465494118],"runs_with_ammo_shortage":0,"total_shortage_boards":0.0,"median_shortage_boards":0,"captain_level_50":42},{"profile":"energy","all_skills":[31,32,33],"first_boss_seconds":[9.93391123528054,12.3722786155386,12.5897881147346],"runs_with_ammo_shortage":0,"total_shortage_boards":0.0,"median_shortage_boards":0,"captain_level_50":42},{"profile":"machine","all_skills":[31,32,33],"first_boss_seconds":[9.93401598876286,12.3722786155386,12.601285739529],"runs_with_ammo_shortage":39,"total_shortage_boards":269.0,"median_shortage_boards":3,"captain_level_50":42},{"profile":"gamma","all_skills":[31,32,33],"first_boss_seconds":[9.93401598876286,12.4288612579985,12.6953388558485],"runs_with_ammo_shortage":8,"total_shortage_boards":28.0,"median_shortage_boards":0,"captain_level_50":42}]
|
||||||
47
artifacts/balance_changes_after.md
Normal file
47
artifacts/balance_changes_after.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Balans — zmiany po audycie, 17.10.2026
|
||||||
|
|
||||||
|
Wprowadzono zmiany zgodne z decyzjami użytkownika. Farmienie kredytów, przedmiotów i surowców pozostaje dostępne.
|
||||||
|
|
||||||
|
## XP
|
||||||
|
- Progi awansu podniesiono o 20%: 120 + 60 × (poziom kapitana − 1), wcześniej 100 + 50 × (...).
|
||||||
|
- Bazowe XP za przeciwnika nadal rośnie z poziomem układu: mnożnik 1 + 0,05 × (poziom układu − 1).
|
||||||
|
- Kapitan do dwóch poziomów ponad układem: pełne XP.
|
||||||
|
- Przewaga kapitana 3/4/5/6/7 poziomów: odpowiednio 50%, 25%, 12,5%, 6,25%, 3,125% bazowego XP, po przemnożeniu zaokrąglone do całości.
|
||||||
|
- Przewaga ośmiu poziomów lub więcej: 0 XP, również z ruin i niezależnie od bonusów XP wyposażenia.
|
||||||
|
- Gdy gracz przejdzie do odpowiednio trudniejszego układu, XP znów jest przyznawane. Kredyty i znaleziska pozostają bez tej redukcji.
|
||||||
|
- W teście 20 000 powtarzanych zabójstw na poziomie układu 1 kapitan zatrzymał się na poziomie 9. Nie da się tak zdobyć wszystkich 27 rang.
|
||||||
|
|
||||||
|
## Amunicja
|
||||||
|
- Nowa wyprawa: 200 zwykłych pocisków zamiast 50.
|
||||||
|
- Restart zachowuje bieżący stan, nie dokłada kolejnych 200 sztuk.
|
||||||
|
- Wrak, który wylosuje nagrodę amunicyjną: 40–60 zwykłych pocisków. Ta nagroda nie jest mnożona przez poziom układu, więc pozostaje bliska 50 także później.
|
||||||
|
- Częstość losowania tej kategorii nagrody nie została zwiększona.
|
||||||
|
|
||||||
|
## Boss
|
||||||
|
- Na poziomie 1: 240 kadłuba i 96 tarcz zamiast 400 i 160.
|
||||||
|
- Bazowe obrażenia: 32 zamiast 64.
|
||||||
|
- Cooldown strzału pomnożony przez 1,25; szybkość ruchu przez 0,8.
|
||||||
|
- Osłabienie liniowo wygasa przez poziomy 2–5; od poziomu 6 obowiązują standardowe statystyki bossa.
|
||||||
|
- Zwykli przeciwnicy i elity pozostają przy dotychczasowej sile.
|
||||||
|
|
||||||
|
## Weryfikacja
|
||||||
|
Przeszły testy:
|
||||||
|
- balance_adjustments_test: nowe progi, zanikanie XP i jego odblokowanie po przejściu dalej, 20 000 powtórnych zabójstw, normalny rozwój do układu 50, start i zachowanie amunicji, rzeczywiste nagrody wraków na poziomach 1 i 50, statystyki i płynna progresja bossa.
|
||||||
|
- credit_pickup_test: niezależne przyznawanie XP i fizyczne zbieranie kredytów.
|
||||||
|
- enemy_tiers_test: 1000 poziomów, generowanie spotkań i skład przeciwników.
|
||||||
|
- git diff --check: bez błędów białych znaków.
|
||||||
|
|
||||||
|
Ponownie uruchomiono 240 kampanii modelowych, 12 000 poziomów, z tymi samymi ziarnami i założeniami co w audycie. Plik wyjściowy: balance_simulation_50_after.json. Model nadal nie jest botem sterującym statkiem; przy niedoborze amunicji kontynuuje warunkowo, nie dowodzi wygrania poziomu.
|
||||||
|
|
||||||
|
| Wskaźnik | Przed | Po |
|
||||||
|
|---|---:|---:|
|
||||||
|
| Komplet obecnych 27 rang, mediana poziomu układu | 29 | 32 |
|
||||||
|
| Zakres poziomu zdobycia kompletu w próbach | 28–29 | 31–33 |
|
||||||
|
| Pierwszy boss, model plazmy z 75% trafień, mediana ostrzału | 53,8 s | 12,4 s |
|
||||||
|
| Pierwszy boss, model plazmy z 45% trafień, mediana ostrzału | 302,2 s | 54,0 s |
|
||||||
|
| Poziomy z brakami amunicji, wariant maszynowy, na 3000 | 332 | 269 |
|
||||||
|
| Poziomy z brakami amunicji, wariant gamma, na 3000 | 21 | 28 |
|
||||||
|
|
||||||
|
Liczba problematycznych poziomów maszynowego wariantu spadła o około 19%, ale przynajmniej jeden niedobór nadal wystąpił w 39 z 60 prób. Poprawa zaopatrzenia nie gwarantuje więc nieskończonego ostrzału; broń zachowuje przewagę braku kosztu energii i wymaga pilnowania zapasów. Niewielkie pogorszenie wariantu gamma jest wynikiem zmian całego przebiegu, w tym późniejszego zdobywania rang Oszczędnego magazynka; nie zmieniano jego amunicji ani obrażeń.
|
||||||
|
|
||||||
|
Progi podniesiono umiarkowanie, zamiast rozciągać istniejące dziewięć umiejętności aż do poziomu 50. Pozostawia to miejsce na planowane dodanie kolejnych umiejętności.
|
||||||
1
artifacts/balance_group_and_minerals.json
Normal file
1
artifacts/balance_group_and_minerals.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
[{"profile":"energy","level":1,"group_survival":[6.52173913043478,7.05036787356898,13.1939350848702]},{"profile":"energy","level":10,"group_survival":[4.01102377834588,5.52201939100195,10.1540811950208]},{"profile":"energy","level":30,"group_survival":[2.97006536092685,4.80908414167249,16.3574613644245]},{"profile":"energy","level":50,"group_survival":[3.05680111027414,4.60645732115947,16.2500810365801],"iron":[777,900,1041],"titanium":[783,932,1036],"uranium":[758,908,1044],"aluminium":[799,913,1029],"sulfur":[803,929,1094]},{"profile":"machine","level":1,"group_survival":[6.52173913043478,7.03571544900443,13.1939350848702]},{"profile":"machine","level":10,"group_survival":[4.01049301704368,5.45605886160167,10.2112745723193]},{"profile":"machine","level":30,"group_survival":[3.02356479780069,4.78255708322684,16.8260813742186]},{"profile":"machine","level":50,"group_survival":[3.04495300696683,4.58130397797198,16.2500810365801],"iron":[780,895,1041],"titanium":[9,111,346],"uranium":[746,898,1059],"aluminium":[799,912,1039],"sulfur":[15,124,356]},{"profile":"gamma","level":1,"group_survival":[6.52173913043478,7.03571544900443,13.5685294830832]},{"profile":"gamma","level":10,"group_survival":[4.01782181788556,5.51786921476537,10.1540811950208]},{"profile":"gamma","level":30,"group_survival":[3.09375760044777,4.7165180488108,16.6727537526076]},{"profile":"gamma","level":50,"group_survival":[3.04495300696683,4.53938320528597,17.3905413179933],"iron":[777,900,1041],"titanium":[511,663,801],"uranium":[486,644,791],"aluminium":[793,911,1021],"sulfur":[789,948,1087]},{"profile":"rookie_energy","level":1,"group_survival":[6.52173913043478,6.52173913043478,13.0434782608696]},{"profile":"rookie_energy","level":10,"group_survival":[3.69500331142399,4.64928713058482,8.99880894138201]},{"profile":"rookie_energy","level":30,"group_survival":[2.7622432099057,4.07652819426282,9.30532249568145]},{"profile":"rookie_energy","level":50,"group_survival":[2.38521823285148,3.62705909483219,13.5928772251247],"iron":[281,355,449],"titanium":[249,343,450],"uranium":[277,368,440],"aluminium":[256,366,471],"sulfur":[263,383,431]}]
|
||||||
127
artifacts/balance_report_50.md
Normal file
127
artifacts/balance_report_50.md
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
# Audyt balansu do 50. poziomu — 17.10.2026
|
||||||
|
|
||||||
|
## Najważniejszy wynik
|
||||||
|
|
||||||
|
Balans jest nierówny: trudny, zależny od łupu początek; duża przewaga gospodarcza plazmy; wygaszenie rozwoju umiejętności około 29. poziomu; nadmiar niektórych zasobów w dalszej grze. Najpierw warto poprawić te zależności, zamiast równomiernie zwiększać trudność wszystkich poziomów.
|
||||||
|
|
||||||
|
Nie zmieniono parametrów rozgrywki. Dodano wyłącznie narzędzia audytu i wyniki.
|
||||||
|
|
||||||
|
## Zakres i ograniczenia
|
||||||
|
|
||||||
|
- 240 kampanii modelowych: 60 deterministycznie zasianych prób dla każdego z czterech wariantów, po 50 poziomów. Razem 12 000 rekordów poziomów i 639 pokonanych wrogów w każdej warunkowej kampanii.
|
||||||
|
- Dodatkowo 600 ścieżek wyboru umiejętności: 300 z priorytetem odblokowania nowych typów i 300 z losowymi wyborami.
|
||||||
|
- Użyto prawdziwego generatora łupu, losowania addonów, statystyk wyposażenia, przeliczania bonusów statku, ofert i nauki umiejętności, nagród, receptur oraz cen sklepowych. Statystyki przeciwników i ich DPS pochodzą z rzeczywistego skryptu wroga.
|
||||||
|
- Górnictwo i wraki próbkowano według rozkładów liczby obiektów i nagród z kodu, bez generowania geometrii mapy. Pominięto utracone pozycje przy ewentualnym niepowodzeniu generatora; rzadkie ruiny uproszczono do średnich 15 XP i zwykłej generacji przedmiotu wyższej jakości, bez wymuszania górnych 10% addonów.
|
||||||
|
- Początkujący: 45% trafień, zebranie 30% dostępnych asteroid, wraków i łupu. Pozostałe warianty: 75% trafień i 75% zbierania. Przyjęto zebranie wszystkich kredytów z pokonanych wrogów.
|
||||||
|
- Priorytet umiejętności gospodarczych: Górnik, Eksplorator, Rekin biznesu, Oszczędny magazynek, potem pozostałe. Dlatego główny model nie reprezentuje najszybszego odblokowania dziewięciu różnych typów; temu służy dodatkowe 600 ścieżek.
|
||||||
|
- Start prawdziwą plazmą. Przejście na wybrany typ broni dopiero po jego znalezieniu. Model używa jednego głównego działa; pozostałe sloty broni pozostają puste. Dobiera moduły według jawnego wyniku punktowego w skrypcie, a nie wiedzy o przyszłym łupie.
|
||||||
|
- Boss jest liczony po pozostałych wrogach, jak w grze. Model szacuje czas zadawania obrażeń na podstawie DPS, celności, energii i regeneracji. Nie steruje statkiem, nie symuluje pocisków, odległości, taranowania ani grupowej walki.
|
||||||
|
- Aktywne rozszerzenia, aktywne skille, wirus, recykler i defensywne pasywy nie są rozgrywane w modelu walki. Ich użycie może znacząco ułatwić grę. Koszt części na naprawy, dobrowolne używanie rakiet, dronów i powerbanków również nie jest automatycznie odejmowany. Zapasy są więc górnymi bilansami dla oszczędnej gry, a nie prognozą każdego gracza.
|
||||||
|
- Przy niedoborze amunicji symulacja kontynuuje kampanię warunkowo, aby pokazać dalszą ekonomię. Nie oznacza to wygrania takiego poziomu. Zakupy policzono optymistycznie także z nagród bieżącego poziomu; rzeczywisty sklep jest dostępny dopiero po oczyszczeniu układu. Wykryte braki amunicji są zatem dolną granicą problemu.
|
||||||
|
- Walka nie przerywa kampanii po hipotetycznej śmierci. To audyt przepływu zasobów i relacji statystyk, a nie 240 udokumentowanych wygranych bota. Nie wylicza wiarygodnego procentu zwycięstw człowieka ani czasu całego przejścia.
|
||||||
|
|
||||||
|
## 1. Umiejętności kończą się za wcześnie
|
||||||
|
|
||||||
|
Obecnie jest dziewięć umiejętności po trzy rangi, czyli 27 punktów. Wszystkie rangi są osiągalne na 28. poziomie kapitana po zdobyciu 20 250 XP.
|
||||||
|
|
||||||
|
W głównych 240 próbach komplet wypadał na poziomach układu 28–29, z medianą 29. Nawet bez żadnych bonusów XP, samymi nagrodami za wrogów, komplet jest dostępny na 29. poziomie układu.
|
||||||
|
|
||||||
|
Dodatkowe ścieżki bez addonów XP:
|
||||||
|
- Wybieranie nowych umiejętności: dziewięć typów odblokowanych na poziomie 10 (mediana), przedział 10.–90. percentyla: 8–15; skrajne próby 8–21.
|
||||||
|
- Wybory losowe: dziewięć typów na poziomie 21 (mediana), przedział 13–26; skrajne próby 9–27.
|
||||||
|
- Wszystkie 27 rang: poziom 29 we wszystkich 600 próbach.
|
||||||
|
|
||||||
|
Od około 30. do 50. poziomu systemu brakuje więc dalszych decyzji rozwojowych kapitana. Kolejne awanse nadal przyznają punkt, ale funkcja przygotowywania ofert zeruje punkty, kiedy nie ma już dostępnych rang.
|
||||||
|
|
||||||
|
**Kierunek:** jeśli celem jest komplet około poziomu 50, próg XP 250 + 125 × (poziom kapitana − 1), zamiast 100 + 50 × (...), daje koszt 27 awansów 50 625 XP. Bazowe nagrody do 50. układu wynoszą 52 041 XP. To sensowny punkt startowy następnego testu, nie zatwierdzona poprawka balansu.
|
||||||
|
|
||||||
|
## 2. Kredyty i zasoby — duże rezerwy przy oszczędnej grze
|
||||||
|
|
||||||
|
Mediany po warunkowym ukończeniu 50. poziomu:
|
||||||
|
|
||||||
|
| Wariant | Kredyty | Surowce | Części | Zwykła amunicja | Rakiety | Powerbanki |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| Początkujący, plazma | 42 217 | 1827 | 1925 | 797 | 72 | 19 |
|
||||||
|
| Plazma, 75% trafień | 41 871 | 4589 | 5036 | 1795 | 183 | 49 |
|
||||||
|
| Broń maszynowa | 28 623 | 2985 | 5028 | 30 | 183 | 49 |
|
||||||
|
| Działko gamma | 41 703 | 4069 | 5004 | 1808 | 181 | 49 |
|
||||||
|
|
||||||
|
Dla gamma pozostawało około 26 sztuk amunicji radiacyjnej. Przedziały 10.–90. percentyla i checkpointy są w pliku JSON podsumowania.
|
||||||
|
|
||||||
|
Plazma nie zużywa zwykłej amunicji, a energię odzyskuje bez kosztu. Kredyty mają bardzo niewiele obowiązkowych odpływów: zakupy amunicji zwykłej, radiacyjnej i rakiet. Nie ma regularnego kosztu wymiany sprzętu ani napraw kupowanych za kredyty. Duży stan konta nie oznacza więc, że nagroda za zabójstwo jest sama w sobie za wysoka; problemem jest także brak powodów do wydawania.
|
||||||
|
|
||||||
|
Surowce nie skalują się bez końca z poziomem: bazowo asteroida daje 5–7 jednostek, Górnik dodaje do 10. Pełny Górnik podnosi średni uzysk z 6 do 16, czyli 2,67 raza. Zapas rośnie szczególnie wtedy, gdy gracz nie wytwarza potrzebnych sobie rzeczy, bo jego główną bronią jest plazma.
|
||||||
|
|
||||||
|
Stałe 10 części za każdy zbędny przedmiot, niezależnie od poziomu i rzadkości, daje dużą podaż napraw. Skrypt naprawczy zużywa jedną część na jeden punkt kadłuba. Zapas 5000 części odpowiada około 50 bazowym kadłubom Szerszenia. Naprawy i aktywne moduły zmniejszą pokazane rezerwy.
|
||||||
|
|
||||||
|
Prosta kontrola wrażliwości dla mediany plazmy, tylko w skali całej kampanii:
|
||||||
|
- Naprawianie średnio 25 punktów kadłuba na poziom zużywa 1250 części; z bilansu około 5036 zostałoby około 3786.
|
||||||
|
- Dwie rakiety na poziom to 100 rakiet; mediana z wraków daje 183, więc nadal pozostaje rezerwa.
|
||||||
|
- Jeden start dronów na poziom zużywa 2500 zwykłych pocisków; 1795 sztuk z bilansu plus 15 partii craftu wystarcza w ujęciu sumarycznym, kosztem 90 jednostek minerałów.
|
||||||
|
- Dwa powerbanki na poziom to 100 sztuk; przy 47 znalezionych brakujące 53 wymagają 318 jednostek minerałów.
|
||||||
|
Nie jest to pełna druga symulacja: nie uwzględnia momentu znalezienia modułu, chwilowych braków i podziału minerałów na typy. Pokazuje jednak, że rezerwy nie znikają od umiarkowanego używania wyposażenia.
|
||||||
|
|
||||||
|
**Kierunek:** dodać wartościowe zastosowania kredytów, ograniczyć stały odzysk części lub powiązać go z przedmiotem i ponownie zmierzyć Górnika. Nie zwiększać wszystkich receptur jednakowo — mogłoby to dodatkowo zaszkodzić broni maszynowej.
|
||||||
|
|
||||||
|
## 3. Amunicja: broń maszynowa ma wąskie gardło
|
||||||
|
|
||||||
|
Wariant maszynowy miał niedobór finansowalnej amunicji w 39 z 60 kampanii, łącznie na 332 z 3000 poziomów modelu. Mediana to trzy problematyczne poziomy na kampanię; skrajna próba miała 27. Na samym poziomie 10 braki wystąpiły w 19 z 60 prób. To wynik optymistycznej dostępności sklepu — realne braki w trakcie walki mogą być częstsze.
|
||||||
|
|
||||||
|
Do 50. poziomu typowe zapotrzebowanie wynosi około 381 zwykłych pocisków na poziom, z uwzględnieniem oszczędności kapitana i ostrzeliwania asteroid. Plazma w tym czasie może zachować około 1800 niewykorzystanych zwykłych pocisków.
|
||||||
|
|
||||||
|
Dla gamma odnotowano braki w 8 z 60 kampanii, na 21 z 3000 poziomów. Późniejsze zaopatrzenie zwykle wystarczało z craftu. Gamma miała jednak znacznie dłuższy modelowy czas walki niż plazma.
|
||||||
|
|
||||||
|
Dodatkowe sprawdzenie podziału surowców pokazuje ważny szczegół: mediana maszynowego wariantu na poziomie 50 to około 2985 jednostek minerałów łącznie, ale tylko 111 tytanu i 124 siarki, wobec około 895 żelaza, 898 uranu i 912 aluminium. W plazmie każdy rodzaj kończył zwykle w okolicach 900 jednostek. Łączna liczba surowców maskuje więc niedobór konkretnych składników amunicji.
|
||||||
|
|
||||||
|
**Kierunek:** poprawić startową dostępność amunicji i możliwość uzyskania jej przed oczyszczeniem układu. Przetestować wydajność craftu dla broni maszynowej osobno, zamiast dalej zwiększać jego koszty wraz z resztą gospodarki.
|
||||||
|
|
||||||
|
## 4. Trudność: mocny pierwszy boss, później osłabienie zwykłych przeciwników
|
||||||
|
|
||||||
|
Już na poziomie 1:
|
||||||
|
- Zwykły wróg ma 62,5 kadłuba + 25 tarcz = 87,5 łącznej osłony.
|
||||||
|
- Boss ma 400 kadłuba + 160 tarcz = 560 osłony, czyli 6,4 raza więcej.
|
||||||
|
- Boss zadaje 64 obrażenia bazowe na strzał, zwykły wróg 10. Przy broni bez dolnego ograniczenia cooldownu boss strzela dodatkowo dwa razy częściej: do 12,8 raza większy DPS.
|
||||||
|
- Boss regeneruje łącznie 5,6 punktu kadłuba i tarcz na sekundę. Na poziomie 50 jest to 19,32/s.
|
||||||
|
|
||||||
|
Dla plazmy z 75% trafień mediana czasu zadawania obrażeń bossowi wynosiła około 54 s na poziomie 1, 20 s na poziomie 10 i 16 s na poziomie 50. U początkującego z 45% trafień pierwszy boss wymagał około 302 s w modelu, głównie wskutek wyczerpywania energii i regeneracji przeciwnika. To nie jest czas całej misji i nie uwzględnia rozszerzeń ani manewrów.
|
||||||
|
|
||||||
|
W pełnej ekspozycji na ogień pierwszego bossa bazowy statek ma tylko około 2,3–4,7 s efektywnej wytrzymałości, zależnie od typu broni wroga. Nie jest to rzeczywisty czas przeżycia ruchomego gracza. Pokazuje jednak dużą różnicę między wymaganą długością ostrzału a tolerancją na błędy.
|
||||||
|
|
||||||
|
W 211 z 3000 poziomów wariantu początkującego model wykrył sytuację, w której długotrwały DPS plazmy po zużyciu energii nie przekraczał łącznej regeneracji bossa. To nie dowodzi nieodwracalnej blokady w grze: można się przeładować, użyć banków, słońca, drugiej broni lub rozszerzeń. Oznacza jednak ryzyko pozornie niekończącej się walki dla gracza, który tego jeszcze nie potrafi.
|
||||||
|
|
||||||
|
Na poziomie 50 w 58 z 60 prób zwykłej plazmy regeneracja tarcz statku przewyższała teoretyczny DPS przynajmniej jednego pojedynczego zwykłego przeciwnika. Regeneracja gracza rośnie z addonami, a zwykli przeciwnicy tracą znaczenie w pojedynkę. Nie dotyczy to automatycznie całych grup, taranowania ani bossa. Wyłączone w modelu pasywy i odzysk tarcz z trafień mogą jeszcze zwiększać przewagę gracza.
|
||||||
|
|
||||||
|
Osobno policzono ekspozycję na równoczesny ogień obu elit i do trzech zwykłych wrogów, po doborze wyposażenia na danym poziomie. To próbka wysokiej presji, a nie stwierdzenie, że taka grupa zawsze jest razem na mapie. Przy pełnej ekspozycji mediana wytrzymałości plazmowego statku wynosiła około 7,1 s na poziomie 1 (czterech wrogów), 5,5 s na poziomie 10 i 4,6 s na poziomie 50 (pięciu). Pokazuje to, że mimo przewagi nad pojedynczym normalnym wrogiem, grupy pozostają groźne. Ruch, rozdzielanie grup i likwidacja pierwszych celów zmieniają ten bilans.
|
||||||
|
|
||||||
|
**Kierunek:** osłabić pierwszy skok do bossa, ograniczyć regenerację wroga podczas otrzymywania trafień albo opóźnić jej rozpoczęcie oraz sprawdzić miękki limit/słabsze sumowanie regeneracji tarcz gracza. Następny etap to dynamiczne testy walki grupowej i realne próby sterowania.
|
||||||
|
|
||||||
|
Powrót z ucieczki ma obecnie ten sam próg 10% co jej rozpoczęcie. Wróg z 9% kadłuba wraca po około sekundzie regeneracji, a z 9,9% po około 0,1 s. Warto przetestować histerezę, np. ucieczka poniżej 10%, powrót dopiero po 20–25%, zamiast oscylowania wokół jednego progu.
|
||||||
|
|
||||||
|
## 5. Śmierć pozwala na nieograniczone farmienie
|
||||||
|
|
||||||
|
Restart zachowuje XP, kredyty, części, minerały i ekwipunek, a odtwarza przeciwników, wraki i asteroidy. Zmiana losowania asteroid nie ogranicza ponownego pobierania nagród.
|
||||||
|
|
||||||
|
Już poziom 1 zawiera przed bossem dwie elity i dwóch zwykłych wrogów. To bazowo 140 XP i 110 kredytów na próbę, przed bonusami wyposażenia. Zabicie tej grupy i celowa śmierć u bossa może powtarzać się bez limitu. 145 takich prób daje co najmniej 20 250 XP potrzebnych na wszystkie 27 rang, wciąż na pierwszym poziomie systemu. To nie jest prognoza typowego gracza ani szybki sposób na komplet, lecz dowód braku ograniczenia gospodarczego restartów.
|
||||||
|
|
||||||
|
**Kierunek:** zdecydować, które nagrody mogą się odradzać po śmierci. Można zachować postęp, ale nagrody przypisać do stałych ID w danym układzie; albo odnawiać część zasobów przy ograniczonej nagrodzie za powtórne zabójstwa. Sama kara kredytowa nie zatrzyma farmienia XP i minerałów.
|
||||||
|
|
||||||
|
## Zalecana kolejność prac
|
||||||
|
|
||||||
|
1. Zasady nagród po śmierci — usunąć możliwość nieograniczonego powtarzania pełnych nagród.
|
||||||
|
2. Wczesny boss i regeneracja wrogów — poprawić tolerancję na błędy początkującego.
|
||||||
|
3. Amunicja do broni maszynowej — zapewnić wykonalną grę przed odblokowaniem sklepu.
|
||||||
|
4. Rozciągnąć rozwój kapitana do docelowego horyzontu kampanii.
|
||||||
|
5. Dodać sensowne wydatki kredytowe; zmierzyć części i Górnika z aktywnym używaniem wyposażenia.
|
||||||
|
6. Dopiero potem regulować trudność późnych poziomów i sumowanie regeneracji gracza.
|
||||||
|
|
||||||
|
## Pliki i odtworzenie
|
||||||
|
|
||||||
|
- tests/levels/balance_simulation_50.gd oraz .tscn — główna symulacja; ziarno losowań łupu 920001 + numer próby × 7919, ofert 440001 + numer próby × 3571.
|
||||||
|
- tests/levels/balance_skill_paths.gd oraz .tscn — dodatkowe 600 ścieżek umiejętności.
|
||||||
|
- artifacts/balance_simulation_50.json — wszystkie 12 000 rekordów oraz wyniki końcowe.
|
||||||
|
- artifacts/balance_simulation_50_summary.json — checkpointy, 10./50./90. percentyl; wynik końcowy umiejętności dodatkowo minimum i maksimum.
|
||||||
|
- artifacts/balance_simulation_50_checkpoints.csv — tabela median do dalszej analizy.
|
||||||
|
- artifacts/balance_skill_paths.json — surowe ścieżki nauki umiejętności.
|
||||||
|
- artifacts/balance_group_and_minerals.json — kwantyle ekspozycji grupowej i końcowe zapasy każdego minerału.
|
||||||
|
|
||||||
|
Uruchom sceny headless w Godot 4.7.2 z projektem jako --path. Symulacja nie modyfikuje plików parametrów rozgrywki. Wyniki odnoszą się do obecnego stanu źródeł; po zmianie balansu należy uruchomić ją ponownie.
|
||||||
1
artifacts/balance_simulation_50.json
Normal file
1
artifacts/balance_simulation_50.json
Normal file
File diff suppressed because one or more lines are too long
1
artifacts/balance_simulation_50_after.json
Normal file
1
artifacts/balance_simulation_50_after.json
Normal file
File diff suppressed because one or more lines are too long
BIN
artifacts/balance_simulation_50_checkpoints.amunicja.translation
Normal file
BIN
artifacts/balance_simulation_50_checkpoints.amunicja.translation
Normal file
Binary file not shown.
BIN
artifacts/balance_simulation_50_checkpoints.boss.translation
Normal file
BIN
artifacts/balance_simulation_50_checkpoints.boss.translation
Normal file
Binary file not shown.
29
artifacts/balance_simulation_50_checkpoints.csv
Normal file
29
artifacts/balance_simulation_50_checkpoints.csv
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"profil","poziom","poziom_kapitana","rangi_umiejetnosci","kredyty","czesci","surowce","amunicja","gamma","rakiety","powerbanki","boss_czas_s","niedobor_amunicji_proby","proby_z_blokada_regeneracji","proby_z_regeneracja_tarczy_wyzsza_niz_DPS_zwyklego"
|
||||||
|
"rookie_energy","1","3","2","230","0","17","50","0","2","0","302,161","0","0","0"
|
||||||
|
"rookie_energy","5","7","6","1369","41","102","64","0","3","1","1356,668","0","3","0"
|
||||||
|
"rookie_energy","10","12","11","3349","151","265","100","0","6","2","84,518","0","18","0"
|
||||||
|
"rookie_energy","20","21","20","9115","496","645","219","0","19","5","72,538","0","4","6"
|
||||||
|
"rookie_energy","30","29","27","17264","929","1034","365","0","33","10","45,398","0","0","26"
|
||||||
|
"rookie_energy","40","37","27","28227","1384","1409","530","0","52","14","40,604","0","0","31"
|
||||||
|
"rookie_energy","50","46","27","42217","1925","1827","797","0","72","19","36,629","0","0","47"
|
||||||
|
"energy","1","3","2","230","10","37","50","0","2","0","53,796","0","0","0"
|
||||||
|
"energy","5","8","7","1384","157","274","88","0","5","2","21,654","0","0","0"
|
||||||
|
"energy","10","12","11","3373","493","727","164","0","11","6","20,134","0","0","4"
|
||||||
|
"energy","20","21","20","9157","1391","1696","444","0","44","15","18,779","0","0","26"
|
||||||
|
"energy","30","29","27","17257","2455","2646","807","0","74","25","17,332","0","0","47"
|
||||||
|
"energy","40","37","27","28162","3684","3594","1275","0","129","36","16,358","0","0","58"
|
||||||
|
"energy","50","46","27","41871","5036","4589","1795","0","183","49","15,744","0","0","58"
|
||||||
|
"machine","1","3","2","230","10","37","50","0","2","0","53,748","1","0","0"
|
||||||
|
"machine","5","8","7","1369","159","243","58","0","5","2","24,535","13","0","0"
|
||||||
|
"machine","10","12","11","1215","484","516","24","0","11","6","27,247","19","0","4"
|
||||||
|
"machine","20","21","20","3447","1399","1157","24","0","44","14","24,802","6","0","31"
|
||||||
|
"machine","30","29","27","9501","2459","1778","27","0","75","25","21,845","4","0","47"
|
||||||
|
"machine","40","37","27","17413","3680","2377","32","0","129","37","21,567","1","0","58"
|
||||||
|
"machine","50","46","27","28623","5028","2985","30","0","183","49","20,398","1","0","58"
|
||||||
|
"gamma","1","3","2","230","10","37","50","0","2","0","53,748","2","0","0"
|
||||||
|
"gamma","5","8","7","1373","159","251","88","0","5","2","46,158","5","0","0"
|
||||||
|
"gamma","10","12","11","3366","487","666","161","13","11","6","47,116","0","0","5"
|
||||||
|
"gamma","20","21","20","9104","1391","1485","442","23","44","15","50,441","0","0","25"
|
||||||
|
"gamma","30","29","27","17223","2449","2359","817","24","75","25","46,36","0","0","48"
|
||||||
|
"gamma","40","37","27","28006","3655","3211","1307","27","127","37","47,536","0","0","58"
|
||||||
|
"gamma","50","46","27","41703","5004","4069","1808","26","181","49","43,366","0","0","58"
|
||||||
|
19
artifacts/balance_simulation_50_checkpoints.csv.import
Normal file
19
artifacts/balance_simulation_50_checkpoints.csv.import
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="csv_translation"
|
||||||
|
type="Translation"
|
||||||
|
uid="uid://5d86u3fuhdxs"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
files=["res://artifacts/balance_simulation_50_checkpoints.poziom.translation", "res://artifacts/balance_simulation_50_checkpoints.poziom.translation", "res://artifacts/balance_simulation_50_checkpoints.rangi.translation", "res://artifacts/balance_simulation_50_checkpoints.kredyty.translation", "res://artifacts/balance_simulation_50_checkpoints.czesci.translation", "res://artifacts/balance_simulation_50_checkpoints.surowce.translation", "res://artifacts/balance_simulation_50_checkpoints.amunicja.translation", "res://artifacts/balance_simulation_50_checkpoints.gamma.translation", "res://artifacts/balance_simulation_50_checkpoints.rakiety.translation", "res://artifacts/balance_simulation_50_checkpoints.powerbanki.translation", "res://artifacts/balance_simulation_50_checkpoints.boss.translation", "res://artifacts/balance_simulation_50_checkpoints.niedobor.translation", "res://artifacts/balance_simulation_50_checkpoints.proby.translation", "res://artifacts/balance_simulation_50_checkpoints.proby.translation"]
|
||||||
|
|
||||||
|
source_file="res://artifacts/balance_simulation_50_checkpoints.csv"
|
||||||
|
dest_files=["res://artifacts/balance_simulation_50_checkpoints.poziom.translation", "res://artifacts/balance_simulation_50_checkpoints.poziom.translation", "res://artifacts/balance_simulation_50_checkpoints.rangi.translation", "res://artifacts/balance_simulation_50_checkpoints.kredyty.translation", "res://artifacts/balance_simulation_50_checkpoints.czesci.translation", "res://artifacts/balance_simulation_50_checkpoints.surowce.translation", "res://artifacts/balance_simulation_50_checkpoints.amunicja.translation", "res://artifacts/balance_simulation_50_checkpoints.gamma.translation", "res://artifacts/balance_simulation_50_checkpoints.rakiety.translation", "res://artifacts/balance_simulation_50_checkpoints.powerbanki.translation", "res://artifacts/balance_simulation_50_checkpoints.boss.translation", "res://artifacts/balance_simulation_50_checkpoints.niedobor.translation", "res://artifacts/balance_simulation_50_checkpoints.proby.translation", "res://artifacts/balance_simulation_50_checkpoints.proby.translation"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress=1
|
||||||
|
delimiter=0
|
||||||
|
unescape_keys=false
|
||||||
|
unescape_translations=true
|
||||||
BIN
artifacts/balance_simulation_50_checkpoints.czesci.translation
Normal file
BIN
artifacts/balance_simulation_50_checkpoints.czesci.translation
Normal file
Binary file not shown.
BIN
artifacts/balance_simulation_50_checkpoints.gamma.translation
Normal file
BIN
artifacts/balance_simulation_50_checkpoints.gamma.translation
Normal file
Binary file not shown.
BIN
artifacts/balance_simulation_50_checkpoints.kredyty.translation
Normal file
BIN
artifacts/balance_simulation_50_checkpoints.kredyty.translation
Normal file
Binary file not shown.
BIN
artifacts/balance_simulation_50_checkpoints.niedobor.translation
Normal file
BIN
artifacts/balance_simulation_50_checkpoints.niedobor.translation
Normal file
Binary file not shown.
Binary file not shown.
BIN
artifacts/balance_simulation_50_checkpoints.poziom.translation
Normal file
BIN
artifacts/balance_simulation_50_checkpoints.poziom.translation
Normal file
Binary file not shown.
BIN
artifacts/balance_simulation_50_checkpoints.proby.translation
Normal file
BIN
artifacts/balance_simulation_50_checkpoints.proby.translation
Normal file
Binary file not shown.
BIN
artifacts/balance_simulation_50_checkpoints.rakiety.translation
Normal file
BIN
artifacts/balance_simulation_50_checkpoints.rakiety.translation
Normal file
Binary file not shown.
BIN
artifacts/balance_simulation_50_checkpoints.rangi.translation
Normal file
BIN
artifacts/balance_simulation_50_checkpoints.rangi.translation
Normal file
Binary file not shown.
BIN
artifacts/balance_simulation_50_checkpoints.surowce.translation
Normal file
BIN
artifacts/balance_simulation_50_checkpoints.surowce.translation
Normal file
Binary file not shown.
1
artifacts/balance_simulation_50_summary.json
Normal file
1
artifacts/balance_simulation_50_summary.json
Normal file
File diff suppressed because one or more lines are too long
10
artifacts/balance_simulation_console.log
Normal file
10
artifacts/balance_simulation_console.log
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
[MCPRuntimeServer] port: scanning 6570-6585 (default)
|
||||||
|
[MCPRuntimeServer] listening on 127.0.0.1:6570
|
||||||
|
[MCPRegistry] runtime port 6570 registered for c:/users/tomek/documents/godot/randoloot
|
||||||
|
SIMULATION_PROFILE_DONE: rookie_energy
|
||||||
|
SIMULATION_PROFILE_DONE: energy
|
||||||
|
SIMULATION_PROFILE_DONE: machine
|
||||||
|
SIMULATION_PROFILE_DONE: gamma
|
||||||
|
BALANCE_SIMULATION_50: DONE 12000 rows
|
||||||
4
artifacts/balance_simulation_errors.log
Normal file
4
artifacts/balance_simulation_errors.log
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
WARNING: Camera2D overridden to physics process mode due to use of physics interpolation.
|
||||||
|
at: _update_process_callback (scene/2d/camera_2d.cpp:88)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] _ready (res://tests/levels/balance_simulation_50.gd:243)
|
||||||
1
artifacts/balance_skill_paths.json
Normal file
1
artifacts/balance_skill_paths.json
Normal file
File diff suppressed because one or more lines are too long
46
artifacts/radiation_import.log
Normal file
46
artifacts/radiation_import.log
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
[ 0% ] [90m[1mfirst_scan_filesystem[22m | Started Project initialization (5 steps)[39m[0m
|
||||||
|
[ 0% ] [90m[1mfirst_scan_filesystem[22m | Scanning file structure...[39m[0m
|
||||||
|
[ 16% ] [90m[1mfirst_scan_filesystem[22m | Loading global class names...[39m[0m
|
||||||
|
[ 33% ] [90m[1mfirst_scan_filesystem[22m | Verifying GDExtensions...[39m[0m
|
||||||
|
[ 50% ] [90m[1mfirst_scan_filesystem[22m | Creating autoload scripts...[39m[0m
|
||||||
|
[ 66% ] [90m[1mfirst_scan_filesystem[22m | Initializing plugins...[39m[0m
|
||||||
|
Godot MCP Toolkit v1.0.3 — by NPGameDev · npgamedev.com
|
||||||
|
[MCPExtensions] Hot-reload watcher active
|
||||||
|
[MCPServer] port: scanning 6550-6560 (default)
|
||||||
|
[MCPServer] session token written to user://addons/godot_mcp_toolkit/project_instance_32cc2271ac67/mcp_token
|
||||||
|
[MCPServer] listening on 127.0.0.1:6551
|
||||||
|
[MCPRegistry] registered c:/users/tomek/documents/godot/randoloot on port 6551
|
||||||
|
[ 83% ] [90m[1mfirst_scan_filesystem[22m | Starting file scan...[39m[0m
|
||||||
|
[92m[ DONE ][39m [1mfirst_scan_filesystem[22m
|
||||||
|
[0m
|
||||||
|
[ 0% ] [90m[1mupdate_scripts_classes[22m | Started Registering global classes... (4 steps)[39m[0m
|
||||||
|
[ 0% ] [90m[1mupdate_scripts_classes[22m | WeaponAudio[39m[0m
|
||||||
|
[ 20% ] [90m[1mupdate_scripts_classes[22m | [39m[0m
|
||||||
|
[ 40% ] [90m[1mupdate_scripts_classes[22m | [39m[0m
|
||||||
|
[ 60% ] [90m[1mupdate_scripts_classes[22m | [39m[0m
|
||||||
|
[92m[ DONE ][39m [1mupdate_scripts_classes[22m
|
||||||
|
[0m
|
||||||
|
[ 0% ] [90m[1mreimport[22m | Started (Re)Importing Assets (1 steps)[39m[0m
|
||||||
|
[ 0% ] [90m[1mreimport[22m | Preparing files to reimport...[39m[0m
|
||||||
|
[ 0% ] [90m[1mreimport[22m | Executing pre-reimport operations...[39m[0m
|
||||||
|
[ 0% ] [90m[1mreimport[22m | radiation.mp3[39m[0m
|
||||||
|
[ 50% ] [90m[1mreimport[22m | Finalizing Asset Import...[39m[0m
|
||||||
|
[92m[ DONE ][39m [1mreimport[22m
|
||||||
|
[0m
|
||||||
|
[ 0% ] [90m[1mreimport[22m | Started (Re)Importing Assets (1 steps)[39m[0m
|
||||||
|
[ 0% ] [90m[1mreimport[22m | Executing post-reimport operations...[39m[0m
|
||||||
|
[92m[ DONE ][39m [1mreimport[22m
|
||||||
|
[0m
|
||||||
|
[ 0% ] [90m[1mloading_editor_layout[22m | Started Loading editor (5 steps)[39m[0m
|
||||||
|
[ 0% ] [90m[1mloading_editor_layout[22m | Loading editor layout...[39m[0m
|
||||||
|
[ 16% ] [90m[1mloading_editor_layout[22m | Loading docks...[39m[0m
|
||||||
|
[ 33% ] [90m[1mloading_editor_layout[22m | Reopening scenes...[39m[0m
|
||||||
|
[ 50% ] [90m[1mloading_editor_layout[22m | Loading central editor layout...[39m[0m
|
||||||
|
[ 66% ] [90m[1mloading_editor_layout[22m | Loading plugin window layout...[39m[0m
|
||||||
|
[ 83% ] [90m[1mloading_editor_layout[22m | Editor layout ready.[39m[0m
|
||||||
|
[92m[ DONE ][39m [1mloading_editor_layout[22m
|
||||||
|
[0m
|
||||||
|
[MCPServer] stopped
|
||||||
|
[MCPRegistry] deregistered c:/users/tomek/documents/godot/randoloot
|
||||||
0
artifacts/radiation_import_errors.log
Normal file
0
artifacts/radiation_import_errors.log
Normal file
BIN
assets/sounds/effects/radiation.mp3
Normal file
BIN
assets/sounds/effects/radiation.mp3
Normal file
Binary file not shown.
19
assets/sounds/effects/radiation.mp3.import
Normal file
19
assets/sounds/effects/radiation.mp3.import
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="mp3"
|
||||||
|
type="AudioStreamMP3"
|
||||||
|
uid="uid://xvk4gbt3m8jk"
|
||||||
|
path="res://.godot/imported/radiation.mp3-702854fbc0298ea0549de8d1ea05ab90.mp3str"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sounds/effects/radiation.mp3"
|
||||||
|
dest_files=["res://.godot/imported/radiation.mp3-702854fbc0298ea0549de8d1ea05ab90.mp3str"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@ -14,7 +14,7 @@ stat_suffix = " DMG"
|
|||||||
weapon_kind = 4
|
weapon_kind = 4
|
||||||
fire_interval = 0.8
|
fire_interval = 0.8
|
||||||
projectile_speed = 430.0
|
projectile_speed = 430.0
|
||||||
weapon_range = 400.0
|
weapon_range = 300.0
|
||||||
damage_lower_roll_min = 10
|
damage_lower_roll_min = 10
|
||||||
damage_lower_roll_max = 14
|
damage_lower_roll_max = 14
|
||||||
damage_spread_roll_min = 4
|
damage_spread_roll_min = 4
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const XP_BASE := 100
|
const XP_BASE := 120
|
||||||
const XP_PER_LEVEL := 50
|
const XP_PER_LEVEL := 60
|
||||||
const LEARNING_POINTS_PER_LEVEL := 1
|
const LEARNING_POINTS_PER_LEVEL := 1
|
||||||
const MAX_SKILL_RANK := 20
|
const MAX_SKILL_RANK := 20
|
||||||
const SKILL_RESET_COST_PER_LEVEL := 100
|
const SKILL_RESET_COST_PER_LEVEL := 100
|
||||||
@ -34,3 +34,13 @@ static func skill_bonus(skill: String, rank: int) -> float:
|
|||||||
|
|
||||||
static func xp_required(level: int) -> int:
|
static func xp_required(level: int) -> int:
|
||||||
return XP_BASE + XP_PER_LEVEL * (maxi(1, level) - 1)
|
return XP_BASE + XP_PER_LEVEL * (maxi(1, level) - 1)
|
||||||
|
|
||||||
|
|
||||||
|
# Full XP within two levels; progressively exhausted rewards on easy systems.
|
||||||
|
static func encounter_xp_factor(captain_level: int, system_level: int) -> float:
|
||||||
|
var gap := maxi(1, captain_level) - clampi(system_level, 1, 1000)
|
||||||
|
if gap <= 2:
|
||||||
|
return 1.0
|
||||||
|
if gap >= 8:
|
||||||
|
return 0.0
|
||||||
|
return pow(0.5, gap - 2)
|
||||||
@ -191,10 +191,13 @@ func add_rewards(xp_amount: int, credit_amount: int) -> void:
|
|||||||
prepare_skill_offers()
|
prepare_skill_offers()
|
||||||
emit_changed()
|
emit_changed()
|
||||||
|
|
||||||
|
func encounter_xp(base_amount: float, system_level: int, xp_multiplier: float = 1.0) -> int:
|
||||||
|
return maxi(0, roundi(maxf(0.0, base_amount) * maxf(0.0, xp_multiplier) * BALANCE.encounter_xp_factor(level, system_level)))
|
||||||
|
|
||||||
func reward_enemy(system_level: int, tier: int, xp_multiplier: float, credit_multiplier: float, grant_credits: bool = true) -> Vector2i:
|
func reward_enemy(system_level: int, tier: int, xp_multiplier: float, credit_multiplier: float, grant_credits: bool = true) -> Vector2i:
|
||||||
var index := clampi(tier, 0, 2)
|
var index := clampi(tier, 0, 2)
|
||||||
var scale_factor := 1.0 + 0.05 * (clampi(system_level, 1, 1000) - 1)
|
var scale_factor := 1.0 + 0.05 * (clampi(system_level, 1, 1000) - 1)
|
||||||
var xp := roundi(BALANCE.KILL_XP[index] * scale_factor * maxf(0.0, xp_multiplier))
|
var xp := encounter_xp(BALANCE.KILL_XP[index] * scale_factor, system_level, xp_multiplier)
|
||||||
var money := roundi(BALANCE.KILL_CREDITS[index] * scale_factor * maxf(0.0, credit_multiplier))
|
var money := roundi(BALANCE.KILL_CREDITS[index] * scale_factor * maxf(0.0, credit_multiplier))
|
||||||
add_rewards(xp, money if grant_credits else 0)
|
add_rewards(xp, money if grant_credits else 0)
|
||||||
return Vector2i(xp, money)
|
return Vector2i(xp, money)
|
||||||
|
|||||||
@ -110,6 +110,16 @@ func apply_level_settings(level: int, tier: int) -> void:
|
|||||||
activation_range = 760.0 * boss_multiplier
|
activation_range = 760.0 * boss_multiplier
|
||||||
weapon_range = 680.0 * boss_multiplier
|
weapon_range = 680.0 * boss_multiplier
|
||||||
weapon_cooldown = maxf(0.55, 1.5 / (1.0 + 0.001 * (clampi(level, 1, 1000) - 1))) / boss_multiplier
|
weapon_cooldown = maxf(0.55, 1.5 / (1.0 + 0.001 * (clampi(level, 1, 1000) - 1))) / boss_multiplier
|
||||||
|
if tier == 2:
|
||||||
|
# Ease the first encounter, returning smoothly to standard bosses at level 6.
|
||||||
|
var intro := clampf(float(level - 1) / 5.0, 0.0, 1.0)
|
||||||
|
max_hull *= lerpf(0.6, 1.0, intro)
|
||||||
|
hull = max_hull
|
||||||
|
max_shields = max_hull * 0.4
|
||||||
|
shields = max_shields
|
||||||
|
weapon_damage *= lerpf(0.5, 1.0, intro)
|
||||||
|
weapon_cooldown *= lerpf(1.25, 1.0, intro)
|
||||||
|
approach_speed *= lerpf(0.8, 1.0, intro)
|
||||||
$CollisionShape2D.scale = Vector2.ONE * boss_multiplier
|
$CollisionShape2D.scale = Vector2.ONE * boss_multiplier
|
||||||
if is_instance_valid(_shield_visual):
|
if is_instance_valid(_shield_visual):
|
||||||
_shield_visual.scale = Vector2.ONE * get_visual_size() / 64.0
|
_shield_visual.scale = Vector2.ONE * get_visual_size() / 64.0
|
||||||
|
|||||||
@ -2,7 +2,7 @@ extends RefCounted
|
|||||||
class_name WeaponAudio
|
class_name WeaponAudio
|
||||||
|
|
||||||
const SOUND_PATHS := {
|
const SOUND_PATHS := {
|
||||||
ItemDefinition.WeaponKind.GAMMA: "res://assets/sounds/effects/plasma.mp3",
|
ItemDefinition.WeaponKind.GAMMA: "res://assets/sounds/effects/radiation.mp3",
|
||||||
ItemDefinition.WeaponKind.PLASMA: "res://assets/sounds/effects/plasma.mp3",
|
ItemDefinition.WeaponKind.PLASMA: "res://assets/sounds/effects/plasma.mp3",
|
||||||
ItemDefinition.WeaponKind.MACHINE_GUN: "res://assets/sounds/effects/machine_gun.mp3",
|
ItemDefinition.WeaponKind.MACHINE_GUN: "res://assets/sounds/effects/machine_gun.mp3",
|
||||||
ItemDefinition.WeaponKind.LASER: "res://assets/sounds/effects/laser.mp3",
|
ItemDefinition.WeaponKind.LASER: "res://assets/sounds/effects/laser.mp3",
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const MODULE_SLOT_COUNT := 5
|
|||||||
const EXTENSION_SLOT_COUNT := 5
|
const EXTENSION_SLOT_COUNT := 5
|
||||||
const MODULE_CATEGORIES := [ItemDefinition.Category.SHIELD, ItemDefinition.Category.ARMOR, ItemDefinition.Category.ENGINE, ItemDefinition.Category.REACTOR, ItemDefinition.Category.COMPUTER]
|
const MODULE_CATEGORIES := [ItemDefinition.Category.SHIELD, ItemDefinition.Category.ARMOR, ItemDefinition.Category.ENGINE, ItemDefinition.Category.REACTOR, ItemDefinition.Category.COMPUTER]
|
||||||
const MODULE_NAMES := ["Tarcze", "Pancerz", "Silnik", "Reaktor", "Komputer"]
|
const MODULE_NAMES := ["Tarcze", "Pancerz", "Silnik", "Reaktor", "Komputer"]
|
||||||
const STARTING_AMMUNITION := 50
|
const STARTING_AMMUNITION := 200
|
||||||
const AMMUNITION_CRAFT_AMOUNT := 10
|
const AMMUNITION_CRAFT_AMOUNT := 10
|
||||||
const AMMUNITION_BUY_AMOUNT := 100
|
const AMMUNITION_BUY_AMOUNT := 100
|
||||||
const AMMUNITION_BUY_BASE_COST := 60
|
const AMMUNITION_BUY_BASE_COST := 60
|
||||||
|
|||||||
@ -131,7 +131,7 @@ func claim() -> bool:
|
|||||||
queue_free()
|
queue_free()
|
||||||
return true
|
return true
|
||||||
elif kind == "ruin":
|
elif kind == "ruin":
|
||||||
var xp := roundi(_rng.randi_range(10, 20) * scale)
|
var xp := GameState.captain.encounter_xp(_rng.randi_range(10, 20) * scale, GameState.system_level)
|
||||||
GameState.captain.add_rewards(xp, 0)
|
GameState.captain.add_rewards(xp, 0)
|
||||||
_drop_module(true)
|
_drop_module(true)
|
||||||
summary = "+%d doświadczenia · +1 moduł" % xp
|
summary = "+%d doświadczenia · +1 moduł" % xp
|
||||||
@ -150,7 +150,7 @@ func claim() -> bool:
|
|||||||
inventory.missile_ammunition += amount
|
inventory.missile_ammunition += amount
|
||||||
summary = "+%d rakiet" % amount
|
summary = "+%d rakiet" % amount
|
||||||
3:
|
3:
|
||||||
var amount := roundi(_rng.randi_range(5, 15) * scale)
|
var amount := _rng.randi_range(40, 60)
|
||||||
inventory.ammunition += amount
|
inventory.ammunition += amount
|
||||||
summary = "+%d amunicji" % amount
|
summary = "+%d amunicji" % amount
|
||||||
inventory.ammunition_changed.emit()
|
inventory.ammunition_changed.emit()
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const MAX_LEVEL := 1000
|
const MAX_LEVEL := 1000
|
||||||
const COUNT_POINTS := [Vector2(1, 5), Vector2(10, 10), Vector2(50, 18), Vector2(100, 23), Vector2(250, 29), Vector2(500, 34), Vector2(1000, 40)]
|
const COUNT_POINTS := [Vector2(1, 11), Vector2(10, 11), Vector2(15, 11), Vector2(50, 18), Vector2(100, 23), Vector2(250, 29), Vector2(500, 34), Vector2(1000, 40)]
|
||||||
const ELITES_PER_LEVEL := 2
|
const ELITES_PER_LEVEL := 2
|
||||||
|
|
||||||
static func enemy_count(level: int) -> int:
|
static func enemy_count(level: int) -> int:
|
||||||
|
|||||||
@ -7,7 +7,7 @@ const PAGES := [
|
|||||||
["Sterowanie", "02", "[font_size=26][color=#f1bd79]Opanuj statek[/color][/font_size]\n\n[color=#91dbea][b]W[/b][/color] — ciąg do przodu.\n[color=#91dbea][b]S[/b][/color] — hamowanie; po zatrzymaniu lot wstecz.\n[color=#91dbea][b]A / D[/b][/color] — obracanie statku.\n[color=#91dbea][b]Q / E[/b][/color] — ruch boczny w lewo / prawo.\n[color=#91dbea][b]TAB[/b][/color] — dopalanie: większa prędkość kosztem energii.\n\n[color=#91dbea][b]Kursor myszy[/b][/color] — celowanie.\n[color=#91dbea][b]LPM / PPM / ŚPM[/b][/color] — strzelanie z trzech slotów broni.\n[color=#91dbea][b]1–5[/b][/color] — użycie zamontowanych rozszerzeń.\n[color=#91dbea][b]F1–F5[/b][/color] — aktywne umiejętności kapitana.\n[color=#91dbea][b]R[/b][/color] — użycie powerbanku, jeśli masz go w zapasach.\n\nStatek ma bezwładność. Obrót nie zatrzymuje ruchu — hamuj wcześniej i wykorzystuj ruch boczny, żeby unikać ostrzału."],
|
["Sterowanie", "02", "[font_size=26][color=#f1bd79]Opanuj statek[/color][/font_size]\n\n[color=#91dbea][b]W[/b][/color] — ciąg do przodu.\n[color=#91dbea][b]S[/b][/color] — hamowanie; po zatrzymaniu lot wstecz.\n[color=#91dbea][b]A / D[/b][/color] — obracanie statku.\n[color=#91dbea][b]Q / E[/b][/color] — ruch boczny w lewo / prawo.\n[color=#91dbea][b]TAB[/b][/color] — dopalanie: większa prędkość kosztem energii.\n\n[color=#91dbea][b]Kursor myszy[/b][/color] — celowanie.\n[color=#91dbea][b]LPM / PPM / ŚPM[/b][/color] — strzelanie z trzech slotów broni.\n[color=#91dbea][b]1–5[/b][/color] — użycie zamontowanych rozszerzeń.\n[color=#91dbea][b]F1–F5[/b][/color] — aktywne umiejętności kapitana.\n[color=#91dbea][b]R[/b][/color] — użycie powerbanku, jeśli masz go w zapasach.\n\nStatek ma bezwładność. Obrót nie zatrzymuje ruchu — hamuj wcześniej i wykorzystuj ruch boczny, żeby unikać ostrzału."],
|
||||||
["Walka i przetrwanie", "03", "[font_size=26][color=#f1bd79]Wróć z wyprawy[/color][/font_size]\n\n[color=#91dbea][b]Tarcze, kadłub i energia[/b][/color]\nTarcze pochłaniają obrażenia przed kadłubem. Utrata całego kadłuba oznacza zniszczenie statku. Energia zasila część broni oraz dopalanie; odnawia się z czasem, szybciej przy gwieździe.\n\n[color=#91dbea][b]Dobieraj broń do zapasów[/b][/color]\nBroń energetyczna zużywa energię. Działko maszynowe potrzebuje zwykłej amunicji, gamma — napromieniowanej, a wyrzutnie — rakiet. Puste zapasy oznaczają brak strzału. Absorpcja ogranicza obrażenia danego typu.\n\n[color=#91dbea][b]Korzystaj z rozszerzeń[/b][/color]\nMiny atakują obszarowo, flary odciągają uwagę, kamuflaż pomaga zerwać kontakt, teleport przenosi w bezpieczne miejsce, a drony wspierają ostrzał. Doładowanie tarcz działa przy gwieździe. Sprawdzaj koszt i cooldown w opisie.\n\n[color=#f1bd79][b]Uważaj na uciekających przeciwników[/b][/color]\nWróg z kadłubem poniżej 10% ucieka wolniej, ale może zostawić minę. Nie leć bezmyślnie prosto za nim."],
|
["Walka i przetrwanie", "03", "[font_size=26][color=#f1bd79]Wróć z wyprawy[/color][/font_size]\n\n[color=#91dbea][b]Tarcze, kadłub i energia[/b][/color]\nTarcze pochłaniają obrażenia przed kadłubem. Utrata całego kadłuba oznacza zniszczenie statku. Energia zasila część broni oraz dopalanie; odnawia się z czasem, szybciej przy gwieździe.\n\n[color=#91dbea][b]Dobieraj broń do zapasów[/b][/color]\nBroń energetyczna zużywa energię. Działko maszynowe potrzebuje zwykłej amunicji, gamma — napromieniowanej, a wyrzutnie — rakiet. Puste zapasy oznaczają brak strzału. Absorpcja ogranicza obrażenia danego typu.\n\n[color=#91dbea][b]Korzystaj z rozszerzeń[/b][/color]\nMiny atakują obszarowo, flary odciągają uwagę, kamuflaż pomaga zerwać kontakt, teleport przenosi w bezpieczne miejsce, a drony wspierają ostrzał. Doładowanie tarcz działa przy gwieździe. Sprawdzaj koszt i cooldown w opisie.\n\n[color=#f1bd79][b]Uważaj na uciekających przeciwników[/b][/color]\nWróg z kadłubem poniżej 10% ucieka wolniej, ale może zostawić minę. Nie leć bezmyślnie prosto za nim."],
|
||||||
["Łup i wytwarzanie", "04", "[font_size=26][color=#f1bd79]Zbieraj. Porównuj. Twórz.[/color][/font_size]\n\n[color=#91dbea][b]Łup zbierasz przez podlecenie[/b][/color]\nPrzedmioty w pobliżu są przyciągane do statku. Magazyn mieści 20 przedmiotów; przy pełnym magazynie zwolnij miejsce, żeby zebrać kolejny.\n\n[color=#91dbea][b]Asteroidy zostawiają fizyczne odłamki[/b][/color]\nStrzelaj do asteroid. Po zniszczeniu rozpadają się bazowo na 5–7 poszarpanych fragmentów. Każdy to jedna jednostka surowca — musisz go zebrać. Umiejętność górnika zwiększa uzysk.\n\n[color=#91dbea][b]Przeszukuj wraki[/b][/color]\nPodleć do wraku i przytrzymaj E, aby go przeszukać. Możesz znaleźć zapasy lub wyposażenie.\n\n[color=#91dbea][b]Warsztat — przycisk TWÓRZ[/b][/color]\nZ minerałów wytworzysz części, amunicję, rakiety i powerbanki. Receptura pokazuje wymagane składniki oraz ilość produktu.\n\n[color=#91dbea][b]Odzyskuj części[/b][/color]\nNajedź na zbędny przedmiot w magazynie i użyj przycisku rozłożenia. Części przydadzą się do napraw."],
|
["Łup i wytwarzanie", "04", "[font_size=26][color=#f1bd79]Zbieraj. Porównuj. Twórz.[/color][/font_size]\n\n[color=#91dbea][b]Łup zbierasz przez podlecenie[/b][/color]\nPrzedmioty w pobliżu są przyciągane do statku. Magazyn mieści 20 przedmiotów; przy pełnym magazynie zwolnij miejsce, żeby zebrać kolejny.\n\n[color=#91dbea][b]Asteroidy zostawiają fizyczne odłamki[/b][/color]\nStrzelaj do asteroid. Po zniszczeniu rozpadają się bazowo na 5–7 poszarpanych fragmentów. Każdy to jedna jednostka surowca — musisz go zebrać. Umiejętność górnika zwiększa uzysk.\n\n[color=#91dbea][b]Przeszukuj wraki[/b][/color]\nPodleć do wraku i przytrzymaj E, aby go przeszukać. Możesz znaleźć zapasy lub wyposażenie.\n\n[color=#91dbea][b]Warsztat — przycisk TWÓRZ[/b][/color]\nZ minerałów wytworzysz części, amunicję, rakiety i powerbanki. Receptura pokazuje wymagane składniki oraz ilość produktu.\n\n[color=#91dbea][b]Odzyskuj części[/b][/color]\nNajedź na zbędny przedmiot w magazynie i użyj przycisku rozłożenia. Części przydadzą się do napraw."],
|
||||||
["Statek i kapitan", "05", "[font_size=26][color=#f1bd79]Buduj własny styl gry[/color][/font_size]\n\n[color=#91dbea][b]Wyposażenie[/b][/color]\nKliknij panel statku w prawym górnym rogu i otwórz magazyn. Przeciągaj przedmioty do pasujących slotów: 3 bronie, 5 modułów i 5 rozszerzeń. Aktywne rozszerzenia uruchamiasz klawiszami 1–5; pasywne (oznaczone P) działają automatycznie w tych samych slotach. Najedź kursorem, żeby sprawdzić parametry i porównanie wyposażenia.\n\n[color=#91dbea][b]Nie każdy duplikat jest taki sam[/b][/color]\nBroń i moduły mogą mieć różne parametry oraz addony. Aktywne rozszerzenia losują cooldown od 30 do 5 pełnych sekund. Pasywne mają losowaną siłę efektu: Recykler odnawia tarcze i energię po zebraniu odłamka, Wirus przejmuje uciekających wrogów, a Pancerz z tytanu ogranicza obrażenia przy niskim kadłubie. Krótsze odnowienia są rzadkie — przed rozłożeniem porównaj egzemplarze. Rozszerzenia tego samego typu dzielą aktywne odnowienie.\n\n[color=#91dbea][b]Umiejętności kapitana[/b][/color]\nZa pokonanych wrogów zdobywasz doświadczenie i kredyty. Po awansie wybierasz umiejętność. Pasywne działają stale, aktywne przypisujesz do F1–F5.\n\n[color=#f1bd79][b]Okno umiejętności nie pauzuje gry.[/b][/color]\nOtwieraj je w bezpiecznym miejscu. Przed kolejnym układem uzupełnij zapasy i sprawdź wyposażenie."]
|
["Statek i kapitan", "05", "[font_size=26][color=#f1bd79]Buduj własny styl gry[/color][/font_size]\n\n[color=#91dbea][b]Wyposażenie[/b][/color]\nKliknij panel statku w prawym górnym rogu i otwórz magazyn. Przeciągaj przedmioty do pasujących slotów: 3 bronie, 5 modułów i 5 rozszerzeń. Aktywne rozszerzenia uruchamiasz klawiszami 1–5; pasywne (oznaczone P) działają automatycznie w tych samych slotach. Najedź kursorem, żeby sprawdzić parametry i porównanie wyposażenia.\n\n[color=#91dbea][b]Nie każdy duplikat jest taki sam[/b][/color]\nBroń i moduły mogą mieć różne parametry oraz addony. Aktywne rozszerzenia losują cooldown od 30 do 5 pełnych sekund. Pasywne mają losowaną siłę efektu: Recykler odnawia tarcze i energię po zebraniu odłamka, Wirus przejmuje uciekających wrogów, a Pancerz z tytanu ogranicza obrażenia przy niskim kadłubie. Krótsze odnowienia są rzadkie — przed rozłożeniem porównaj egzemplarze. Rozszerzenia tego samego typu dzielą aktywne odnowienie.\n\n[color=#91dbea][b]Umiejętności kapitana[/b][/color]\nZa pokonanych wrogów zdobywasz doświadczenie i kredyty. Trudniejsze układy dają więcej bazowego XP. Jeśli kapitan przewyższa poziom układu o ponad 2 poziomy, XP za przeciwników i ruiny maleje; przy przewadze 8 poziomów spada do zera. Kredyty i surowce nadal zbierasz. Po awansie wybierasz umiejętność. Pasywne działają stale, aktywne przypisujesz do F1–F5.\n\n[color=#f1bd79][b]Okno umiejętności nie pauzuje gry.[/b][/color]\nOtwieraj je w bezpiecznym miejscu. Przed kolejnym układem uzupełnij zapasy i sprawdź wyposażenie."]
|
||||||
]
|
]
|
||||||
var _panel: PanelContainer
|
var _panel: PanelContainer
|
||||||
var _reader: RichTextLabel
|
var _reader: RichTextLabel
|
||||||
|
|||||||
@ -14,7 +14,7 @@ func _ready() -> void:
|
|||||||
ship.get_node("WeaponController").set_physics_process(false)
|
ship.get_node("WeaponController").set_physics_process(false)
|
||||||
ship.set("credits_multiplier", 1.5)
|
ship.set("credits_multiplier", 1.5)
|
||||||
var inventory := ship.get_node("ShipInventory") as ShipInventory
|
var inventory := ship.get_node("ShipInventory") as ShipInventory
|
||||||
assert(inventory.ammunition == 50)
|
assert(inventory.ammunition == ShipInventory.STARTING_AMMUNITION)
|
||||||
var enemy := ENEMY.instantiate() as CharacterBody2D
|
var enemy := ENEMY.instantiate() as CharacterBody2D
|
||||||
enemy.position = Vector2(700, 0)
|
enemy.position = Vector2(700, 0)
|
||||||
add_child(enemy)
|
add_child(enemy)
|
||||||
|
|||||||
92
tests/levels/balance_adjustments_test.gd
Normal file
92
tests/levels/balance_adjustments_test.gd
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
extends Node
|
||||||
|
const BAL = preload("res://scripts/captain/captain_balance.gd")
|
||||||
|
const ENEMY = preload("res://scenes/game/enemy_ship.tscn")
|
||||||
|
const POI = preload("res://scripts/levels/exploration_poi.gd")
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
assert(BAL.xp_required(1) == 120 and BAL.xp_required(2) == 180)
|
||||||
|
var captain := CaptainProgress.new()
|
||||||
|
for level in range(1, 1001):
|
||||||
|
assert(BAL.xp_required(level) == 120 + 60 * (level - 1))
|
||||||
|
for gap in range(0, 12):
|
||||||
|
captain.level = 20 + gap
|
||||||
|
var xp := captain.reward_enemy(20, 0, 1.0, 1.0).x
|
||||||
|
var expected := roundi(39.0 * BAL.encounter_xp_factor(20 + gap, 20))
|
||||||
|
assert(xp == expected)
|
||||||
|
captain.level = 9
|
||||||
|
assert(captain.encounter_xp(1000000, 1, 1000) == 0)
|
||||||
|
var money := captain.credits
|
||||||
|
assert(captain.reward_enemy(1, 0, 1.0, 1.0) == Vector2i(0, 15))
|
||||||
|
assert(captain.credits == money + 15)
|
||||||
|
captain = CaptainProgress.new()
|
||||||
|
for index in 20000:
|
||||||
|
captain.reward_enemy(1, index % 3, 1.0, 1.0)
|
||||||
|
assert(captain.level == 9)
|
||||||
|
assert(captain.encounter_xp(20, 1) == 0)
|
||||||
|
assert(captain.reward_enemy(9, 0, 1.0, 1.0).x > 0)
|
||||||
|
# Normal progress still reaches later captain levels.
|
||||||
|
captain = CaptainProgress.new()
|
||||||
|
for level in range(1, 51):
|
||||||
|
for tier in preload("res://scripts/levels/level_balance.gd").enemy_tiers(level):
|
||||||
|
captain.reward_enemy(level, tier, 1.0, 1.0)
|
||||||
|
assert(captain.level >= 39 and captain.level <= 41)
|
||||||
|
print("NORMAL_PROGRESS_CAPTAIN_LEVEL_AT_50: ", captain.level)
|
||||||
|
GameState.start_new_run()
|
||||||
|
var ship := preload("res://scenes/game/player_ship.tscn").instantiate()
|
||||||
|
add_child(ship)
|
||||||
|
ship.set_physics_process(false)
|
||||||
|
ship.get_node("WeaponController").set_physics_process(false)
|
||||||
|
ship.get_node("ExtensionSystems").set_physics_process(false)
|
||||||
|
var inv: ShipInventory = ship.get_node("ShipInventory")
|
||||||
|
assert(inv.ammunition == 200)
|
||||||
|
inv.ammunition = 123
|
||||||
|
inv.preserve_for_restart()
|
||||||
|
ship.queue_free()
|
||||||
|
await get_tree().process_frame
|
||||||
|
ship = preload("res://scenes/game/player_ship.tscn").instantiate()
|
||||||
|
add_child(ship)
|
||||||
|
ship.set_physics_process(false)
|
||||||
|
ship.get_node("WeaponController").set_physics_process(false)
|
||||||
|
ship.get_node("ExtensionSystems").set_physics_process(false)
|
||||||
|
inv = ship.get_node("ShipInventory")
|
||||||
|
assert(inv.ammunition == 123, "Restart must preserve ammunition, not grant another starting stack")
|
||||||
|
var ammo_wrecks := 0
|
||||||
|
for level in [1, 50]:
|
||||||
|
GameState.system_level = level
|
||||||
|
for reward_seed in range(1, 50):
|
||||||
|
var poi := StaticBody2D.new()
|
||||||
|
poi.set_script(POI)
|
||||||
|
poi.kind = "wreck"
|
||||||
|
poi.player = ship
|
||||||
|
poi.reward_seed = reward_seed
|
||||||
|
poi.poi_id = "balance:%d:%d" % [level, reward_seed]
|
||||||
|
add_child(poi)
|
||||||
|
var state: int = poi._rng.state
|
||||||
|
var choice: int = poi._rng.randi_range(0, 3)
|
||||||
|
poi._rng.state = state
|
||||||
|
if choice == 3:
|
||||||
|
var before := inv.ammunition
|
||||||
|
assert(poi.claim())
|
||||||
|
assert(inv.ammunition - before >= 40 and inv.ammunition - before <= 60)
|
||||||
|
ammo_wrecks += 1
|
||||||
|
else:
|
||||||
|
poi.queue_free()
|
||||||
|
assert(ammo_wrecks > 10)
|
||||||
|
var enemy := ENEMY.instantiate()
|
||||||
|
add_child(enemy)
|
||||||
|
enemy.set_physics_process(false)
|
||||||
|
enemy.apply_level_settings(1, 2)
|
||||||
|
assert(is_equal_approx(enemy.max_hull, 240.0))
|
||||||
|
assert(is_equal_approx(enemy.max_shields, 96.0))
|
||||||
|
assert(is_equal_approx(enemy.weapon_damage, 32.0))
|
||||||
|
assert(is_equal_approx(enemy.approach_speed, 99.2))
|
||||||
|
var previous_hull: float = enemy.max_hull
|
||||||
|
for level in range(2, 7):
|
||||||
|
enemy.apply_level_settings(level, 2)
|
||||||
|
assert(enemy.max_hull > previous_hull)
|
||||||
|
previous_hull = enemy.max_hull
|
||||||
|
assert(is_equal_approx(enemy.max_hull, 500.0))
|
||||||
|
enemy.apply_level_settings(1, 0)
|
||||||
|
assert(is_equal_approx(enemy.max_hull, 62.5) and is_equal_approx(enemy.weapon_damage, 10.0))
|
||||||
|
print("BALANCE_ADJUSTMENTS_TEST: PASS XP thresholds, farm limit, normal progression, starting/restart ammo, actual wreck rewards and smooth boss introduction")
|
||||||
|
get_tree().quit()
|
||||||
1
tests/levels/balance_adjustments_test.gd.uid
Normal file
1
tests/levels/balance_adjustments_test.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://b21jkaebwdnla
|
||||||
4
tests/levels/balance_adjustments_test.tscn
Normal file
4
tests/levels/balance_adjustments_test.tscn
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://tests/levels/balance_adjustments_test.gd" id="1"]
|
||||||
|
[node name="BalanceAdjustmentsTest" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
262
tests/levels/balance_simulation_50.gd
Normal file
262
tests/levels/balance_simulation_50.gd
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
extends Node
|
||||||
|
# Reproducible Monte Carlo audit. Combat is an analytical estimate, not a movement bot.
|
||||||
|
const GEN = preload("res://scripts/items/item_generator.gd")
|
||||||
|
const BAL = preload("res://scripts/levels/level_balance.gd")
|
||||||
|
const SKILLS = preload("res://scripts/captain/captain_skills.gd")
|
||||||
|
const RECIPES = preload("res://scripts/items/crafting_recipes.gd")
|
||||||
|
const RUNS := 60
|
||||||
|
var ship: CharacterBody2D
|
||||||
|
var inv: ShipInventory
|
||||||
|
var enemy: CharacterBody2D
|
||||||
|
var rows: Array = []
|
||||||
|
var finishes: Array = []
|
||||||
|
var weapon_id := ""
|
||||||
|
var collect := 0.75
|
||||||
|
var accuracy := 0.75
|
||||||
|
var shot_demand := 0.0
|
||||||
|
var mined_this_level := 0
|
||||||
|
var cash_spent := 0
|
||||||
|
var ammo_shortages := 0
|
||||||
|
|
||||||
|
func item_score(item: ItemInstance) -> float:
|
||||||
|
if item == null:
|
||||||
|
return -1.0
|
||||||
|
var v := 0.0
|
||||||
|
for key in ["max_hull", "max_shields", "max_energy", "energy_regeneration", "shield_regeneration", "damage_percent", "critical_chance_percent", "critical_damage_percent", "kinetic_absorption", "energy_absorption", "radiation_absorption", "hull_percent", "shield_percent", "experience_percent", "credits_percent"]:
|
||||||
|
var weight: float = {"energy_regeneration": 10.0, "shield_regeneration": 10.0, "damage_percent": 2.0, "experience_percent": 2.0}.get(key, 1.0)
|
||||||
|
v += item.get_stat_total(key) * weight
|
||||||
|
return v
|
||||||
|
|
||||||
|
func train() -> void:
|
||||||
|
var priority := ["miner", "explorer", "business_shark", "efficient_magazine", "lucky", "electrician", "reactor_overload", "tactical_maneuver", "power_redirect"]
|
||||||
|
while GameState.captain.learning_points > 0:
|
||||||
|
GameState.captain.prepare_skill_offers()
|
||||||
|
var selected := ""
|
||||||
|
for id in priority:
|
||||||
|
if id in GameState.captain.skill_offers:
|
||||||
|
selected = id
|
||||||
|
break
|
||||||
|
if selected.is_empty() or not GameState.captain.train_skill(selected):
|
||||||
|
break
|
||||||
|
|
||||||
|
func loot(level: int, tier: int, module_only: bool = false) -> void:
|
||||||
|
var item: ItemInstance
|
||||||
|
if module_only:
|
||||||
|
var pool: Array = GEN.DEFINITIONS.filter(func(d): return d.category != ItemDefinition.Category.WEAPON)
|
||||||
|
item = GEN.generate_loot(level, tier, pool.pick_random(), ship.luck_percent)
|
||||||
|
else:
|
||||||
|
item = GEN.generate_loot(level, tier, null, ship.luck_percent)
|
||||||
|
var equipped := false
|
||||||
|
if item.definition.category == ItemDefinition.Category.WEAPON and item.definition.item_id == weapon_id:
|
||||||
|
if inv.weapon_slots[0].definition.item_id != weapon_id or inv.get_weapon_comparison(item, 0).dps_delta > 0.0:
|
||||||
|
inv.weapon_slots[0] = item
|
||||||
|
equipped = true
|
||||||
|
elif item.definition.category in ShipInventory.MODULE_CATEGORIES:
|
||||||
|
var index: int = ShipInventory.MODULE_CATEGORIES.find(item.definition.category)
|
||||||
|
if item_score(item) > item_score(inv.module_slots[index]):
|
||||||
|
equipped = true
|
||||||
|
if inv.module_slots[index] != null:
|
||||||
|
GameState.captain.parts += 10
|
||||||
|
inv.module_slots[index] = item
|
||||||
|
elif item.definition.category == ItemDefinition.Category.ACTIVE_EXTENSION:
|
||||||
|
var same := -1
|
||||||
|
for index in inv.extension_slots.size():
|
||||||
|
if inv.extension_slots[index] != null and inv.extension_slots[index].definition.item_id == item.definition.item_id:
|
||||||
|
same = index
|
||||||
|
if same >= 0:
|
||||||
|
var old: ItemInstance = inv.extension_slots[same]
|
||||||
|
if (item.is_passive_extension() and item.get_passive_value() > old.get_passive_value()) or (not item.is_passive_extension() and item.extension_cooldown < old.extension_cooldown):
|
||||||
|
inv.extension_slots[same] = item
|
||||||
|
equipped = true
|
||||||
|
GameState.captain.parts += 10
|
||||||
|
elif inv.extension_slots.has(null):
|
||||||
|
inv.extension_slots[inv.extension_slots.find(null)] = item
|
||||||
|
equipped = true
|
||||||
|
if not equipped:
|
||||||
|
GameState.captain.parts += 10
|
||||||
|
inv._recalculate_ship_stats()
|
||||||
|
|
||||||
|
func supplies(level: int) -> void:
|
||||||
|
mined_this_level = 0
|
||||||
|
var scale := BAL.power_scale(level)
|
||||||
|
for index in randi_range(6, 10):
|
||||||
|
if randf() > collect:
|
||||||
|
continue
|
||||||
|
mined_this_level += 1
|
||||||
|
var mineral: String = RECIPES.MINERALS.pick_random()
|
||||||
|
GameState.captain.minerals[mineral] += randi_range(5, 7) + roundi(GameState.captain.get_skill_bonus("miner"))
|
||||||
|
var wrecks: int = randi_range(2, 4) + roundi(GameState.captain.get_skill_bonus("explorer"))
|
||||||
|
for index in wrecks:
|
||||||
|
if randf() > collect:
|
||||||
|
continue
|
||||||
|
match randi_range(0, 3):
|
||||||
|
0: GameState.captain.parts += roundi(randi_range(3, 8) * scale)
|
||||||
|
1: loot(level, 0, true)
|
||||||
|
2: inv.missile_ammunition += maxi(1, roundi(scale))
|
||||||
|
3: inv.ammunition += roundi(randi_range(40, 60))
|
||||||
|
# Rare ruins: mean XP, module generation uses the real loot rules.
|
||||||
|
if randf() < 0.08 * collect:
|
||||||
|
GameState.captain.add_rewards(GameState.captain.encounter_xp(15 * scale, level), 0)
|
||||||
|
loot(level + 1, 2, true)
|
||||||
|
train()
|
||||||
|
|
||||||
|
func top_up(level: int) -> void:
|
||||||
|
var demand := ceili(shot_demand)
|
||||||
|
var gamma := weapon_id == "gamma_cannon"
|
||||||
|
var stock: int = inv.irradiated_ammunition if gamma else inv.ammunition
|
||||||
|
if weapon_id == "plasma_cannon":
|
||||||
|
return
|
||||||
|
var recipe := "irradiated_ammunition" if gamma else "ammunition"
|
||||||
|
while stock < demand and inv.can_craft(recipe):
|
||||||
|
inv.craft(recipe)
|
||||||
|
stock += 50
|
||||||
|
var cost: int = inv.get_irradiated_ammunition_buy_cost() if gamma else inv.get_ammunition_buy_cost()
|
||||||
|
var batch := 10 if gamma else 100
|
||||||
|
while stock < demand and GameState.captain.credits >= cost:
|
||||||
|
GameState.captain.credits -= cost
|
||||||
|
cash_spent += cost
|
||||||
|
stock += batch
|
||||||
|
if stock < demand:
|
||||||
|
ammo_shortages += 1
|
||||||
|
if gamma:
|
||||||
|
inv.irradiated_ammunition = maxi(0, stock - demand)
|
||||||
|
else:
|
||||||
|
inv.ammunition = maxi(0, stock - demand)
|
||||||
|
|
||||||
|
func combat(level: int, tier: int) -> Dictionary:
|
||||||
|
enemy.apply_level_settings(level, tier)
|
||||||
|
var item: ItemInstance = inv.weapon_slots[0]
|
||||||
|
var dps: float = inv.get_total_dps() * accuracy
|
||||||
|
var interval: float = item.definition.fire_interval
|
||||||
|
var energy_rate: float = item.get_energy_cost() / interval
|
||||||
|
var sustainable := minf(1.0, ship.energy_regeneration / maxf(0.001, energy_rate))
|
||||||
|
var profile := DamageTypes.enemy_profile(level, GameState.system_seed)
|
||||||
|
dps *= 1.0 - profile.absorption[item.definition.get_damage_type()] / 100.0
|
||||||
|
if item.definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA:
|
||||||
|
# Existing comparison includes 1 + .25 * interval; no stacking DoT assumed.
|
||||||
|
pass
|
||||||
|
var regen: float = (enemy.max_hull + enemy.max_shields) * 0.01
|
||||||
|
var burst_time: float = ship.max_energy / maxf(0.001, energy_rate - ship.energy_regeneration) if energy_rate > ship.energy_regeneration else 100000.0
|
||||||
|
var ehp: float = enemy.max_hull + enemy.max_shields
|
||||||
|
var seconds := ehp / maxf(0.01, dps - regen)
|
||||||
|
if seconds > burst_time:
|
||||||
|
var remaining := maxf(0.0, ehp - maxf(0.0, dps - regen) * burst_time)
|
||||||
|
seconds = burst_time + remaining / maxf(0.01, dps * sustainable - regen)
|
||||||
|
seconds = minf(3600.0, seconds)
|
||||||
|
var enemy_dps: float = enemy.get_weapon_dps()
|
||||||
|
var resistance: float = ship.absorption[DamageTypes.for_weapon(enemy.weapon_kind)] / 100.0
|
||||||
|
# Stationary exposure baseline. Movement success must be supplied, not inferred.
|
||||||
|
var incoming := enemy_dps * (1.0 - resistance)
|
||||||
|
var survival: float = (ship.max_hull + ship.max_shields) / maxf(0.01, incoming - ship.shield_regeneration)
|
||||||
|
var used := seconds / interval * (1.0 - GameState.captain.get_skill_bonus("efficient_magazine")) if item.definition.weapon_kind in [ItemDefinition.WeaponKind.MACHINE_GUN, ItemDefinition.WeaponKind.GAMMA] else 0.0
|
||||||
|
if item.definition.item_id != weapon_id:
|
||||||
|
used = 0.0
|
||||||
|
return {"seconds": seconds, "survival": survival, "used": used, "sustainable": sustainable, "regen_lock": dps * sustainable <= regen, "enemy_dps": incoming, "required_avoid": 1.0 - clampf((ship.shield_regeneration + (ship.max_hull + ship.max_shields) / maxf(0.01, seconds)) / maxf(0.01, incoming), 0.0, 1.0)}
|
||||||
|
|
||||||
|
func group_exposure(level: int) -> Dictionary:
|
||||||
|
# Upper-pressure sample: both elites plus up to three normal ships firing together.
|
||||||
|
var tiers: Array[int] = [1, 1]
|
||||||
|
for index in mini(3, BAL.enemy_count(level) - 3):
|
||||||
|
tiers.append(0)
|
||||||
|
var incoming := 0.0
|
||||||
|
for tier in tiers:
|
||||||
|
enemy.apply_level_settings(level, tier)
|
||||||
|
incoming += enemy.get_weapon_dps() * (1.0 - ship.absorption[DamageTypes.for_weapon(enemy.weapon_kind)] / 100.0)
|
||||||
|
var survival: float = (ship.max_hull + ship.max_shields) / maxf(0.01, incoming - ship.shield_regeneration)
|
||||||
|
return {"survival": survival, "size": tiers.size(), "dps": incoming}
|
||||||
|
|
||||||
|
func run_trial(profile: String, trial: int) -> void:
|
||||||
|
seed(920001 + trial * 7919)
|
||||||
|
GameState.start_new_run()
|
||||||
|
GameState.captain._skill_rng.seed = 440001 + trial * 3571
|
||||||
|
weapon_id = "machine_gun" if profile == "machine" else ("gamma_cannon" if profile == "gamma" else "plasma_cannon")
|
||||||
|
collect = 0.3 if profile == "rookie_energy" else 0.75
|
||||||
|
accuracy = 0.45 if profile == "rookie_energy" else 0.75
|
||||||
|
inv.weapon_slots.fill(null)
|
||||||
|
inv.module_slots.fill(null)
|
||||||
|
inv.extension_slots.fill(null)
|
||||||
|
inv._add_starter_items()
|
||||||
|
inv.ammunition = ShipInventory.STARTING_AMMUNITION
|
||||||
|
inv.irradiated_ammunition = 0
|
||||||
|
inv.missile_ammunition = 2
|
||||||
|
inv.energy_banks = 0
|
||||||
|
inv._recalculate_ship_stats()
|
||||||
|
cash_spent = 0
|
||||||
|
ammo_shortages = 0
|
||||||
|
var all_skills := 0
|
||||||
|
var all_types := 0
|
||||||
|
var kills := 0
|
||||||
|
var earned := 0
|
||||||
|
for level in range(1, 51):
|
||||||
|
GameState.system_level = level
|
||||||
|
GameState.system_seed = 100000 + trial * 53 + level * 997
|
||||||
|
# Explore before combat. No spending resources on optional missiles/banks.
|
||||||
|
supplies(level)
|
||||||
|
shot_demand = 0.0
|
||||||
|
var fight_seconds := 0.0
|
||||||
|
var boss_seconds := 0.0
|
||||||
|
var boss_survival := 0.0
|
||||||
|
var regen_locks := 0
|
||||||
|
var boss_avoid := 0.0
|
||||||
|
var normal_immune := 0
|
||||||
|
var tiers: Array[int] = BAL.enemy_tiers(level)
|
||||||
|
tiers.pop_front()
|
||||||
|
tiers.append(2)
|
||||||
|
for tier in tiers:
|
||||||
|
var c := combat(level, tier)
|
||||||
|
fight_seconds += c.seconds
|
||||||
|
shot_demand += c.used
|
||||||
|
if tier == 2:
|
||||||
|
boss_seconds = c.seconds
|
||||||
|
boss_survival = c.survival
|
||||||
|
boss_avoid = c.required_avoid
|
||||||
|
if tier == 0 and ship.shield_regeneration >= c.enemy_dps:
|
||||||
|
normal_immune += 1
|
||||||
|
regen_locks += int(c.regen_lock)
|
||||||
|
var reward := GameState.captain.reward_enemy(level, tier, ship.experience_multiplier, ship.credits_multiplier)
|
||||||
|
earned += reward.y
|
||||||
|
kills += 1
|
||||||
|
train()
|
||||||
|
if all_skills == 0 and GameState.captain.get_spent_learning_points() == 27:
|
||||||
|
all_skills = level
|
||||||
|
if all_types == 0 and GameState.captain.skills.size() == SKILLS.IDS.size():
|
||||||
|
all_types = level
|
||||||
|
if randf() < 0.1 * collect:
|
||||||
|
inv.energy_banks += 1
|
||||||
|
if randf() < (0.65 if tier == 0 else (0.9 if tier == 1 else 1.0)) * collect:
|
||||||
|
loot(level, tier)
|
||||||
|
# Add mining shots only for kinetic/radiation builds, same hit accuracy.
|
||||||
|
if weapon_id != "plasma_cannon" and inv.weapon_slots[0].definition.item_id == weapon_id:
|
||||||
|
shot_demand += float(mined_this_level) * 20.0 * BAL.power_scale(level) / maxf(1, inv.weapon_slots[0].get_stat_total("damage")) / accuracy * (1.0 - GameState.captain.get_skill_bonus("efficient_magazine"))
|
||||||
|
var group := group_exposure(level)
|
||||||
|
var before_shortages := ammo_shortages
|
||||||
|
top_up(level)
|
||||||
|
var minerals := 0
|
||||||
|
for count in GameState.captain.minerals.values():
|
||||||
|
minerals += int(count)
|
||||||
|
rows.append({"profile": profile, "trial": trial, "level": level, "captain_level": GameState.captain.level, "ranks": GameState.captain.get_spent_learning_points(), "credits": GameState.captain.credits, "earned": earned, "spent": cash_spent, "parts": GameState.captain.parts, "minerals": minerals, "ammunition": inv.ammunition, "gamma": inv.irradiated_ammunition, "missiles": inv.missile_ammunition, "banks": inv.energy_banks, "ammo_demand": shot_demand, "shortage": ammo_shortages > before_shortages, "boss_seconds": boss_seconds, "boss_survival": boss_survival, "boss_required_avoid": boss_avoid, "normal_immune": normal_immune, "fight_seconds": fight_seconds, "regen_locks": regen_locks, "dps": inv.get_total_dps(), "energy_regen": ship.energy_regeneration, "shield_regen": ship.shield_regeneration, "ehp": ship.max_hull + ship.max_shields, "mineral_stocks": GameState.captain.minerals.duplicate(), "group_survival": group.survival, "group_size": group.size, "group_dps": group.dps})
|
||||||
|
finishes.append({"profile": profile, "trial": trial, "all_skills": all_skills, "all_types": all_types, "shortage_boards": ammo_shortages, "kills": kills})
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
GameState.start_new_run()
|
||||||
|
ship = preload("res://scenes/game/player_ship.tscn").instantiate()
|
||||||
|
add_child(ship)
|
||||||
|
inv = ship.get_node("ShipInventory")
|
||||||
|
ship.set_physics_process(false)
|
||||||
|
ship.get_node("WeaponController").set_physics_process(false)
|
||||||
|
ship.get_node("ExtensionSystems").set_physics_process(false)
|
||||||
|
enemy = preload("res://scenes/game/enemy_ship.tscn").instantiate()
|
||||||
|
add_child(enemy)
|
||||||
|
enemy.set_physics_process(false)
|
||||||
|
for profile in ["rookie_energy", "energy", "machine", "gamma"]:
|
||||||
|
for trial in RUNS:
|
||||||
|
run_trial(profile, trial)
|
||||||
|
print("SIMULATION_PROFILE_DONE: ", profile)
|
||||||
|
await get_tree().process_frame
|
||||||
|
var output := {"runs_per_profile": RUNS, "profiles": 4, "levels": 50, "rows": rows, "finishes": finishes}
|
||||||
|
var output_path := "res://artifacts/balance_simulation_50_after.json" if "--after-balance" in OS.get_cmdline_user_args() else "res://artifacts/balance_simulation_50.json"
|
||||||
|
var file := FileAccess.open(output_path, FileAccess.WRITE)
|
||||||
|
file.store_string(JSON.stringify(output))
|
||||||
|
file.close()
|
||||||
|
print("BALANCE_SIMULATION_50: DONE ", rows.size(), " rows")
|
||||||
|
get_tree().quit()
|
||||||
1
tests/levels/balance_simulation_50.gd.uid
Normal file
1
tests/levels/balance_simulation_50.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://cda37388p0bsd
|
||||||
4
tests/levels/balance_simulation_50.tscn
Normal file
4
tests/levels/balance_simulation_50.tscn
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://tests/levels/balance_simulation_50.gd" id="1"]
|
||||||
|
[node name="BalanceSimulation50" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
35
tests/levels/balance_skill_paths.gd
Normal file
35
tests/levels/balance_skill_paths.gd
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
extends Node
|
||||||
|
const BAL = preload("res://scripts/levels/level_balance.gd")
|
||||||
|
func _ready() -> void:
|
||||||
|
var outcomes: Array = []
|
||||||
|
for style in ["diverse", "random"]:
|
||||||
|
for trial in 300:
|
||||||
|
var captain := CaptainProgress.new()
|
||||||
|
captain._skill_rng.seed = 81723 + trial
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = 55001 + trial
|
||||||
|
var all_types := 0
|
||||||
|
var all_ranks := 0
|
||||||
|
for level in range(1, 51):
|
||||||
|
for tier in BAL.enemy_tiers(level):
|
||||||
|
captain.reward_enemy(level, tier, 1.0, 1.0)
|
||||||
|
while captain.learning_points > 0:
|
||||||
|
captain.prepare_skill_offers()
|
||||||
|
if captain.skill_offers.is_empty():
|
||||||
|
break
|
||||||
|
var pick: String = captain.skill_offers[rng.randi_range(0, captain.skill_offers.size() - 1)]
|
||||||
|
if style == "diverse":
|
||||||
|
for skill in captain.skill_offers:
|
||||||
|
if captain.get_skill_rank(skill) < captain.get_skill_rank(pick):
|
||||||
|
pick = skill
|
||||||
|
assert(captain.train_skill(pick))
|
||||||
|
if captain.skills.size() == 9 and all_types == 0:
|
||||||
|
all_types = level
|
||||||
|
if captain.get_spent_learning_points() == 27 and all_ranks == 0:
|
||||||
|
all_ranks = level
|
||||||
|
outcomes.append({"style": style, "trial": trial, "all_types": all_types, "all_ranks": all_ranks})
|
||||||
|
var f := FileAccess.open("res://artifacts/balance_skill_paths.json", FileAccess.WRITE)
|
||||||
|
f.store_string(JSON.stringify(outcomes))
|
||||||
|
f.close()
|
||||||
|
print("BALANCE_SKILL_PATHS: PASS 600 seeded skill paths using actual offers, training and XP")
|
||||||
|
get_tree().quit()
|
||||||
1
tests/levels/balance_skill_paths.gd.uid
Normal file
1
tests/levels/balance_skill_paths.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://c8ppmpp1big6q
|
||||||
4
tests/levels/balance_skill_paths.tscn
Normal file
4
tests/levels/balance_skill_paths.tscn
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://tests/levels/balance_skill_paths.gd" id="1"]
|
||||||
|
[node name="BalanceSkillPaths" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
@ -5,6 +5,10 @@ const GENERATOR := preload("res://scripts/levels/system_generator.gd")
|
|||||||
const ENEMY := preload("res://scenes/game/enemy_ship.tscn")
|
const ENEMY := preload("res://scenes/game/enemy_ship.tscn")
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
assert(BALANCE.enemy_tiers(1).count(0) == 8)
|
||||||
|
assert(preload("res://resources/items/gamma_cannon.tres").weapon_range == 300.0)
|
||||||
|
for level in range(2, 1001):
|
||||||
|
assert(BALANCE.enemy_count(level) >= BALANCE.enemy_count(level - 1))
|
||||||
for level in range(1, 1001):
|
for level in range(1, 1001):
|
||||||
var tiers := BALANCE.enemy_tiers(level)
|
var tiers := BALANCE.enemy_tiers(level)
|
||||||
assert(tiers.count(1) == 2 and tiers.count(2) == 1)
|
assert(tiers.count(1) == 2 and tiers.count(2) == 1)
|
||||||
|
|||||||
@ -59,8 +59,7 @@ func _ready() -> void:
|
|||||||
assert(absf(position.x) <= 2380 and absf(position.y) <= 1580)
|
assert(absf(position.x) <= 2380 and absf(position.y) <= 1580)
|
||||||
for other_index in range(index + 1, enemies.size()):
|
for other_index in range(index + 1, enemies.size()):
|
||||||
assert(position.distance_to(enemies[other_index]["position"]) >= GENERATOR.ENEMY_MIN_SEPARATION)
|
assert(position.distance_to(enemies[other_index]["position"]) >= GENERATOR.ENEMY_MIN_SEPARATION)
|
||||||
if level <= 10:
|
assert(enemies[index]["tier"] == BALANCE.enemy_tiers(level)[index])
|
||||||
assert(enemies[index]["tier"] == (2 if index == 0 else 0))
|
|
||||||
assert(observed_counts.size() == 5)
|
assert(observed_counts.size() == 5)
|
||||||
assert(observed_ship_types.size() == GENERATOR.ALIEN_SHIPS.TEXTURES.size())
|
assert(observed_ship_types.size() == GENERATOR.ALIEN_SHIPS.TEXTURES.size())
|
||||||
print("SYSTEM_GENERATION_TEST: PASS (420 layouts, deterministic retries, planets, safe spawn and level balance 1–1000)")
|
print("SYSTEM_GENERATION_TEST: PASS (420 layouts, deterministic retries, planets, safe spawn and level balance 1–1000)")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user