diff --git a/artifacts/alien_research.png b/artifacts/alien_research.png index 1f58eca..a2f4d6a 100644 Binary files a/artifacts/alien_research.png and b/artifacts/alien_research.png differ diff --git a/artifacts/alien_research_hover.png b/artifacts/alien_research_hover.png index 76e037b..ec2c814 100644 Binary files a/artifacts/alien_research_hover.png and b/artifacts/alien_research_hover.png differ diff --git a/artifacts/alien_weapon_icons_preview.png b/artifacts/alien_weapon_icons_preview.png new file mode 100644 index 0000000..9936cbb Binary files /dev/null and b/artifacts/alien_weapon_icons_preview.png differ diff --git a/artifacts/alien_weapon_icons_preview.png.import b/artifacts/alien_weapon_icons_preview.png.import new file mode 100644 index 0000000..ddfd7ba --- /dev/null +++ b/artifacts/alien_weapon_icons_preview.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cviddy840ck45" +path="res://.godot/imported/alien_weapon_icons_preview.png-00f065cd4dbfd932766d0e5aac769bc5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/alien_weapon_icons_preview.png" +dest_files=["res://.godot/imported/alien_weapon_icons_preview.png-00f065cd4dbfd932766d0e5aac769bc5.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/artifacts/ship_level_120_audit.json b/artifacts/ship_level_120_audit.json new file mode 100644 index 0000000..dd7243e --- /dev/null +++ b/artifacts/ship_level_120_audit.json @@ -0,0 +1,354 @@ +{ + "atomic_engine_cost_s": 27.4375902704401, + "booster_cost_s": 0.0, + "enemies": [ + { + "dps": 96.234, + "hull": 806.25, + "shields": 322.5, + "speed": 65.689, + "tier": 0 + }, + { + "dps": 125.1042, + "hull": 1612.5, + "shields": 645.0, + "speed": 65.689, + "tier": 1 + }, + { + "dps": 615.8976, + "hull": 25800.0, + "shields": 6192.0, + "speed": 131.378, + "tier": 2 + } + ], + "energy_max": { + "afterburner_speed": 463.95, + "max_energy": 323.92, + "raw_dps": 3098.5, + "regen": 46.0519998474121, + "scanner_range": 4358.8, + "shield_leech_percent": 0.0, + "shield_leech_s_at_50pct_accuracy_20pct_absorption": 0.0, + "shield_refill_s": 33.3333333333333, + "shield_regen": 42.357074640564, + "shields": 1411.9024880188, + "single_gun_cost_s": 83.5, + "speed": 154.65, + "stop_from_tab_s": 10.0, + "tab_speed": 309.3, + "two_guns_and_tab_cost_s": 238.2624, + "two_guns_cost_s": 167.0 + }, + "energy_runtime": { + "duration": 30.0, + "energy_end": 18.0124620915739, + "energy_start": 323.92, + "failed_shots": 199, + "shots": 101 + }, + "lasers": { + "median_two_lasers_cost_s": 84.5815827446423, + "samples": 200, + "two_lasers_sustainable_fraction": 0.0 + }, + "movement_runtime": { + "coast_to_stop_seconds": 9.98333333333341, + "space_drag": 48.0, + "tab_speed_after_20s": 480.0, + "thrust_acceleration": 90.0 + }, + "polarization": { + "energy_per_s_at_5_hits": 91.5558898519632, + "generator_capacity": 249.432638822183, + "per_hit": 18.3111779703926 + }, + "polarization_actual_5_tiny_hits": { + "energy_gained": 5.0, + "shield_damage": 5.0 + }, + "random_boss": { + "afterburner_speed": { + "max": 600.0, + "median": 507.018091035123, + "min": 353.137627549784, + "p90": 600.0 + }, + "max_energy": { + "max": 323.589558083697, + "median": 285.500100144797, + "min": 249.290733871268, + "p90": 315.970795614247 + }, + "raw_dps": { + "max": 6347.0, + "median": 2680.688722916, + "min": 1038.77122326506, + "p90": 3938.72175291923 + }, + "regen": { + "max": 35.8429503926344, + "median": 26.876527508128, + "min": 20.1948989159178, + "p90": 30.9219924728325 + }, + "scanner_range": { + "max": 4356.22367972439, + "median": 3811.52696089991, + "min": 3240.64599322552, + "p90": 4233.98804543628 + }, + "shield_leech_percent": { + "max": 10.0, + "median": 10.0, + "min": 0.0, + "p90": 10.0 + }, + "shield_leech_s_at_50pct_accuracy_20pct_absorption": { + "max": 44.6631993163473, + "median": 25.043783971798, + "min": 0.0, + "p90": 32.3302940672334 + }, + "shield_refill_s": { + "max": 751391.899957019, + "median": 38.4844442719922, + "min": 33.3333333333333, + "p90": 127.604214286799 + }, + "shield_regen": { + "max": 22.902648193883, + "median": 12.3089268813893, + "min": 0.0, + "p90": 16.8659462695747 + }, + "shields": { + "max": 893.263986326946, + "median": 517.16306950776, + "min": 324.0066162357, + "p90": 659.869313505764 + }, + "single_gun_cost_s": { + "max": 79.7287914190814, + "median": 60.0, + "min": 52.4, + "p90": 69.9263464576862 + }, + "single_gun_sustainable_fraction": 0.0, + "speed": { + "max": 240.0, + "median": 169.006030345041, + "min": 117.712542516595, + "p90": 205.609633461282 + }, + "stop_from_tab_s": { + "max": 10.0, + "median": 10.0, + "min": 10.0, + "p90": 10.0 + }, + "tab_speed": { + "max": 480.0, + "median": 338.012060690082, + "min": 235.42508503319, + "p90": 411.219266922564 + }, + "two_guns_and_tab_cost_s": { + "max": 219.373632560223, + "median": 184.260575282892, + "min": 162.597386184591, + "p90": 199.328372912332 + }, + "two_guns_cost_s": { + "max": 151.948700340403, + "median": 120.8, + "min": 104.8, + "p90": 135.126194351132 + }, + "two_guns_sustainable_fraction": 0.0 + }, + "random_normal": { + "afterburner_speed": { + "max": 600.0, + "median": 452.447683133625, + "min": 352.000006972615, + "p90": 551.133275529222 + }, + "max_energy": { + "max": 323.907385644101, + "median": 286.494693367482, + "min": 249.292095061386, + "p90": 316.928152668998 + }, + "raw_dps": { + "max": 4101.02153201906, + "median": 1564.75616821901, + "min": 896.4, + "p90": 2304.76932912164 + }, + "regen": { + "max": 32.1884921461455, + "median": 25.2923000261937, + "min": 19.9307386902277, + "p90": 28.4830161031905 + }, + "scanner_range": { + "max": 4355.11419896663, + "median": 3793.7352641508, + "min": 3239.4014536892, + "p90": 4226.36066470803 + }, + "shield_leech_percent": { + "max": 10.0, + "median": 5.0, + "min": 0.0, + "p90": 10.0 + }, + "shield_leech_s_at_50pct_accuracy_20pct_absorption": { + "max": 36.3476545005851, + "median": 18.536993061517, + "min": 0.0, + "p90": 25.7666683023867 + }, + "shield_refill_s": { + "max": 726953.090011701, + "median": 72.1297170840986, + "min": 33.3333333333333, + "p90": 491588.742516387 + }, + "shield_regen": { + "max": 19.7616361313606, + "median": 6.1617020184605, + "min": 0.0, + "p90": 12.8778917074988 + }, + "shields": { + "max": 726.953090011701, + "median": 433.00099690197, + "min": 324.415418261372, + "p90": 561.527853994357 + }, + "single_gun_cost_s": { + "max": 78.9490941963897, + "median": 54.4, + "min": 46.0, + "p90": 62.4083553618865 + }, + "single_gun_sustainable_fraction": 0.0, + "speed": { + "max": 237.892011493927, + "median": 150.815894377875, + "min": 117.333335657538, + "p90": 183.711091843074 + }, + "stop_from_tab_s": { + "max": 10.0, + "median": 10.0, + "min": 10.0, + "p90": 10.0 + }, + "tab_speed": { + "max": 475.784022987853, + "median": 301.63178875575, + "min": 234.666671315076, + "p90": 367.422183686148 + }, + "two_guns_and_tab_cost_s": { + "max": 214.370179879324, + "median": 172.033770480785, + "min": 148.42563852579, + "p90": 185.679488102582 + }, + "two_guns_cost_s": { + "max": 144.923398364779, + "median": 108.0, + "min": 93.2, + "p90": 121.727379755121 + }, + "two_guns_sustainable_fraction": 0.0 + }, + "representative": { + "afterburner_speed": 521.607400551023, + "max_energy": 249.432638822183, + "raw_dps": 1844.24679253242, + "regen": 26.5001715211498, + "scanner_range": 3444.02684605805, + "shield_leech_percent": 0.0, + "shield_leech_s_at_50pct_accuracy_20pct_absorption": 0.0, + "shield_refill_s": 366223.559407853, + "shield_regen": 0.0, + "shields": 366.223559407853, + "single_gun_cost_s": 50.4, + "speed": 173.869133517008, + "stop_from_tab_s": 10.0, + "tab_speed": 347.738267034016, + "two_guns_and_tab_cost_s": 160.07518054088, + "two_guns_cost_s": 105.2 + }, + "representative_energy_runtime": { + "duration": 30.0, + "energy_end": 10.1961149313249, + "energy_start": 249.432638822183, + "failed_shots": 159, + "shots": 81 + }, + "representative_single_gun_runtime": { + "duration": 30.0, + "energy_end": 10.7961149313243, + "energy_start": 249.432638822183, + "failed_shots": 38, + "shots": 82 + }, + "shield_leech_actual_from_1000_damage": 70.5951244009399, + "shield_max": { + "afterburner_speed": 463.95, + "max_energy": 323.92, + "raw_dps": 3470.32, + "regen": 27.392, + "scanner_range": 4358.8, + "shield_leech_percent": 10.0, + "shield_leech_s_at_50pct_accuracy_20pct_absorption": 70.5951244009399, + "shield_refill_s": 33.3333333333333, + "shield_regen": 42.357074640564, + "shields": 1411.9024880188, + "single_gun_cost_s": 66.8, + "speed": 154.65, + "stop_from_tab_s": 10.0, + "tab_speed": 309.3, + "two_guns_and_tab_cost_s": 204.8624, + "two_guns_cost_s": 133.6 + }, + "shield_runtime": { + "empty_to_full_seconds": 33.3499999999994, + "leech_on_1000_actual_damage": 100.0, + "regen_works_during_combat": true + }, + "solar_regen": 46.4547826269245, + "solar_two_guns_runtime": { + "duration": 30.0, + "energy_end": 11.9018712528032, + "energy_start": 249.432638822183, + "failed_shots": 112, + "shots": 128 + }, + "speed_max": { + "afterburner_speed": 600.0, + "max_energy": 323.92, + "raw_dps": 6816.7, + "regen": 27.392, + "scanner_range": 4358.8, + "shield_leech_percent": 0.0, + "shield_leech_s_at_50pct_accuracy_20pct_absorption": 0.0, + "shield_refill_s": 435880.0, + "shield_regen": 0.0, + "shields": 435.88, + "single_gun_cost_s": 83.5, + "speed": 240.0, + "stop_from_tab_s": 10.0, + "tab_speed": 480.0, + "two_guns_and_tab_cost_s": 238.2624, + "two_guns_cost_s": 167.0 + } +} \ No newline at end of file diff --git a/artifacts/ship_level_120_audit.md b/artifacts/ship_level_120_audit.md new file mode 100644 index 0000000..72d1b12 --- /dev/null +++ b/artifacts/ship_level_120_audit.md @@ -0,0 +1,58 @@ +# Audyt statku na poziomie 120 — przed poprawkami + +Poniższe wyniki opisują stan przed poprawkami. Dane pierwotne: `ship_level_120_audit_before.json`. Wyniki po poprawkach: `ship_level_120_balance_changes.md` i `ship_level_120_audit.json`. + +## Metoda i ograniczenia + +- Szerszeń, najszybsza bazowa jednostka: 108 px/s, bez umiejętności kapitana. +- Pełne wyposażenie poziomu 120: dwa działka plazmowe, wyrzutnia rakiet, pięć modułów. Wszystkie sloty odblokowane. +- 500 losowych zestawów ze zwykłego dropu, 500 z dropu bossów, 200 zestawów z laserami. +- Trzy legalne zestawy z maksymalnymi rollami nastawione na prędkość, energię lub tarcze. To granice osiągalnych parametrów, nie opis typowego łupu. +- Testy zasobów: symulacja 30 sekund na krokach 1/60 s, z rzeczywistą funkcją kosztu strzału. Nie jest to pełny test celności, unikania pocisków ani całej kampanii. Model ponawia nieudane strzały w następnym terminie strzału. +- Ruch sprawdzony wspólną funkcją sterowania. Odzysk tarcz oraz polaryzację sprawdzono rzeczywistymi funkcjami trafienia i leczenia. +- Pomiar DPS oznacza teoretyczny ogień wszystkich slotów, bez absorpcji celu, ograniczeń energii i wyczerpania amunicji. +- `shield_refill_s` w surowych danych przy braku regeneracji ma bardzo dużą wartość zastępczą; oznacza brak pasywnego ładowania. + +## Wyniki + +| Wskaźnik | Mediana zwykłego łupu | Mediana łupu bossów | +|---|---:|---:| +| Prędkość podstawowa | 151 px/s | 169 px/s | +| Prędkość TAB | 302 px/s | 338 px/s | +| Regeneracja energii | 62,7/s | 64,2/s | +| Pojemność generatora | 286 | 286 | +| Koszt jednego działka | 54,4/s | 60,0/s | +| Koszt dwóch działek | 108,0/s | 120,8/s | +| Pojemność tarcz | 433 | 517 | +| Pasywna regeneracja tarcz | 6,2/s | 14,1/s | +| Odzysk tarcz z obrażeń | 5% | 10% | + +### Energia + +82,8% zwykłych losowych zestawów oraz 64% zestawów bossowych podtrzymuje jedno działko plazmowe bez spadku zapasu energii. Żaden nie podtrzymuje dwóch plazm jednocześnie. Dwa lasery podtrzymuje 4% spośród 200 zestawów. + +Konkretny zestaw: regeneracja 61,7/s, koszt jednego działka 50,4/s, dwóch 105,2/s, pojemność 249,4. Po 30 sekundach z jedną bronią nadal pełny generator. Z dwoma działkami 25,4 energii i 79 pominiętych strzałów. Blisko gwiazdy regeneracja wzrosła do 124,0/s i podtrzymała oba działka z pełnym generatorem. + +Zwykły TAB kosztuje tylko 20/s, atomowy 10/s, orb dopalacza 0/s. Każdy z 1000 badanych reaktorów przewyższa koszt zwykłego TAB. Można zatem dopalać bez końca, jeżeli inne działania nie zużywają nadwyżki. Przyczyna strukturalna: regeneracja rośnie liniowo z poziomem, koszt energii broni tylko z pierwiastkiem obrażeń, a koszt TAB jest stały. + +### Prędkość + +Wyspecjalizowany zestaw osiąga 385,5 px/s, TAB 771,0 px/s, aktywny dopalacz teoretycznie 1156,5 px/s. Test sterowania osiągnął 771,0 px/s. Sam dryf po puszczeniu sterowania trwa około 48 s; aktywne hamowanie jest znacznie szybsze. Szerokość sektora wynosi 5200 px. Zwykły przeciwnik poziomu 120 porusza się około 65,7 px/s, boss 131,4 px/s. + +### Tarcze + +Pasywne ładowanie samo w sobie nie jest natychmiastowe. Zestaw tarczowy: 1411,9 pojemności, 74,6/s regeneracji, od zera do pełna 18,9 s. Regeneracja działa także w walce. + +Odzysk tarcz sumuje się bez globalnego limitu: osiem addonów daje 40% faktycznie zadanych obrażeń. Wywołanie leczenia dla 1000 obrażeń odnowiło 400 tarcz. Przy mocnym DPS ten efekt wielokrotnie przewyższa pasywną regenerację i może zdominować przeżywalność. Szacowanie leczenia na podstawie samego teoretycznego DPS jest górną ilustracją; rzeczywisty wynik ograniczają zasoby, trafienia i dostępne cele. + +### Orb polaryzacyjny + +Pięć trafień po 1 punkcie zabrało łącznie 5 tarcz i oddało 91,6 energii. Każdy drobny kontakt aktywuje pełne 5% maksymalnych tarcz. Brak limitu częstotliwości lub powiązania z wielkością trafienia tworzy bardzo korzystną zamianę drobnych obrażeń na energię. + +### Mnożniki ofensywne + +Przy maksymalnych rollach sumowanie bonusów obrażeń, szansy krytycznej i obrażeń krytycznych daje około 85 tys. teoretycznego DPS dla jednego z legalnych zestawów. Sam bonus +7% bazowych parametrów nie ogranicza iloczynu tych mnożników. Zestaw nie utrzymuje pełnego ognia bez dodatkowego źródła energii. + +## Wniosek + +Brak błędów runtime w audycie. Są jednak anomalie balansu: nieograniczony TAB z reaktorem 120, częsty nieograniczony ogień jednej broni, nadmierne sumowanie odzysku tarcz, zamiana drobnych trafień na dużo energii przez polaryzację oraz skrajne zestawy prędkości i krytyków. Nie zmieniano parametrów rozgrywki podczas audytu. diff --git a/artifacts/ship_level_120_audit_before.json b/artifacts/ship_level_120_audit_before.json new file mode 100644 index 0000000..094751b --- /dev/null +++ b/artifacts/ship_level_120_audit_before.json @@ -0,0 +1,354 @@ +{ + "atomic_engine_cost_s": 10.0, + "booster_cost_s": 0.0, + "enemies": [ + { + "dps": 38.4936, + "hull": 806.25, + "shields": 322.5, + "speed": 65.689, + "tier": 0 + }, + { + "dps": 50.04168, + "hull": 1612.5, + "shields": 645.0, + "speed": 65.689, + "tier": 1 + }, + { + "dps": 235.885714285714, + "hull": 25800.0, + "shields": 6192.0, + "speed": 131.378, + "tier": 2 + } + ], + "energy_max": { + "afterburner_speed": 463.95, + "max_energy": 323.92, + "raw_dps": 5018.33057163238, + "regen": 90.8359998474121, + "scanner_range": 4358.8, + "shield_leech_percent": 0.0, + "shield_leech_s_at_50pct_accuracy_20pct_absorption": 0.0, + "shield_refill_s": 18.9161642492539, + "shield_regen": 74.6399993896484, + "shields": 1411.9024880188, + "single_gun_cost_s": 83.5, + "speed": 154.65, + "stop_from_tab_s": 19.33125, + "tab_speed": 309.3, + "two_guns_and_tab_cost_s": 187.0, + "two_guns_cost_s": 167.0 + }, + "energy_runtime": { + "duration": 30.0, + "energy_end": 24.7860620915736, + "energy_start": 323.92, + "failed_shots": 119, + "shots": 181 + }, + "lasers": { + "median_two_lasers_cost_s": 84.5815827446423, + "samples": 200, + "two_lasers_sustainable_fraction": 0.04 + }, + "movement_runtime": { + "coast_to_stop_seconds": 48.1333333333319, + "space_drag": 16.0, + "tab_speed_after_20s": 771.023010253906, + "thrust_acceleration": 90.0 + }, + "polarization": { + "energy_per_s_at_5_hits": 91.5558898519632, + "generator_capacity": 249.432638822183, + "per_hit": 18.3111779703926 + }, + "polarization_actual_5_tiny_hits": { + "energy_gained": 91.5558898519632, + "shield_damage": 5.0 + }, + "random_boss": { + "afterburner_speed": { + "max": 818.649817801595, + "median": 507.018091035123, + "min": 353.137627549784, + "p90": 618.814400415383 + }, + "max_energy": { + "max": 323.589558083697, + "median": 285.500100144797, + "min": 249.290733871268, + "p90": 315.970795614247 + }, + "raw_dps": { + "max": 12607.4883806826, + "median": 3042.71482066662, + "min": 1038.77122326506, + "p90": 5568.48584820499 + }, + "regen": { + "max": 79.6612432948376, + "median": 64.233671778534, + "min": 50.5846967477534, + "p90": 73.5136057273531 + }, + "scanner_range": { + "max": 4356.22367972439, + "median": 3811.52696089991, + "min": 3240.64599322552, + "p90": 4233.98804543628 + }, + "shield_leech_percent": { + "max": 30.0, + "median": 10.0, + "min": 0.0, + "p90": 15.0 + }, + "shield_leech_s_at_50pct_accuracy_20pct_absorption": { + "max": 756.449302840955, + "median": 109.828278207903, + "min": 0.0, + "p90": 261.073693922348 + }, + "shield_refill_s": { + "max": 751391.899957019, + "median": 38.4844442719922, + "min": 10.1880518766895, + "p90": 127.604214286799 + }, + "shield_regen": { + "max": 47.0856902545033, + "median": 14.0510470081203, + "min": 0.0, + "p90": 27.9299953889516 + }, + "shields": { + "max": 893.263986326946, + "median": 517.16306950776, + "min": 324.0066162357, + "p90": 659.869313505764 + }, + "single_gun_cost_s": { + "max": 79.7287914190814, + "median": 60.0, + "min": 52.4, + "p90": 69.9263464576862 + }, + "single_gun_sustainable_fraction": 0.64, + "speed": { + "max": 272.883272600532, + "median": 169.006030345041, + "min": 117.712542516595, + "p90": 206.271466805128 + }, + "stop_from_tab_s": { + "max": 34.1104090750664, + "median": 21.1257537931301, + "min": 14.7140678145743, + "p90": 25.7839333506409 + }, + "tab_speed": { + "max": 545.766545201063, + "median": 338.012060690082, + "min": 235.42508503319, + "p90": 412.542933610255 + }, + "two_guns_and_tab_cost_s": { + "max": 171.948700340403, + "median": 140.8, + "min": 124.8, + "p90": 155.126194351132 + }, + "two_guns_cost_s": { + "max": 151.948700340403, + "median": 120.8, + "min": 104.8, + "p90": 135.126194351132 + }, + "two_guns_sustainable_fraction": 0.0 + }, + "random_normal": { + "afterburner_speed": { + "max": 713.67603448178, + "median": 452.447683133625, + "min": 352.000006972615, + "p90": 551.133275529222 + }, + "max_energy": { + "max": 323.907385644101, + "median": 286.494693367482, + "min": 249.292095061386, + "p90": 316.928152668998 + }, + "raw_dps": { + "max": 8919.67306740631, + "median": 1588.30306956395, + "min": 896.4, + "p90": 2393.21450762625 + }, + "regen": { + "max": 75.793565046211, + "median": 62.6697740547914, + "min": 49.7922160706832, + "p90": 71.1795886787146 + }, + "scanner_range": { + "max": 4355.11419896663, + "median": 3793.7352641508, + "min": 3239.4014536892, + "p90": 4226.36066470803 + }, + "shield_leech_percent": { + "max": 20.0, + "median": 5.0, + "min": 0.0, + "p90": 10.0 + }, + "shield_leech_s_at_50pct_accuracy_20pct_absorption": { + "max": 178.393461348126, + "median": 26.5760132536783, + "min": 0.0, + "p90": 77.6438581247026 + }, + "shield_refill_s": { + "max": 726953.090011701, + "median": 72.1297170840986, + "min": 16.2584431117418, + "p90": 491588.742516387 + }, + "shield_regen": { + "max": 25.7070644403072, + "median": 6.1617020184605, + "min": 0.0, + "p90": 14.5198328869572 + }, + "shields": { + "max": 726.953090011701, + "median": 433.00099690197, + "min": 324.415418261372, + "p90": 561.527853994357 + }, + "single_gun_cost_s": { + "max": 78.9490941963897, + "median": 54.4, + "min": 46.0, + "p90": 62.4083553618865 + }, + "single_gun_sustainable_fraction": 0.828, + "speed": { + "max": 237.892011493927, + "median": 150.815894377875, + "min": 117.333335657538, + "p90": 183.711091843074 + }, + "stop_from_tab_s": { + "max": 29.7365014367408, + "median": 18.8519867972344, + "min": 14.6666669571923, + "p90": 22.9638864803843 + }, + "tab_speed": { + "max": 475.784022987853, + "median": 301.63178875575, + "min": 234.666671315076, + "p90": 367.422183686148 + }, + "two_guns_and_tab_cost_s": { + "max": 164.923398364779, + "median": 128.0, + "min": 113.2, + "p90": 141.727379755121 + }, + "two_guns_cost_s": { + "max": 144.923398364779, + "median": 108.0, + "min": 93.2, + "p90": 121.727379755121 + }, + "two_guns_sustainable_fraction": 0.0 + }, + "representative": { + "afterburner_speed": 521.607400551023, + "max_energy": 249.432638822183, + "raw_dps": 1844.24679253242, + "regen": 61.6884683811878, + "scanner_range": 3444.02684605805, + "shield_leech_percent": 0.0, + "shield_leech_s_at_50pct_accuracy_20pct_absorption": 0.0, + "shield_refill_s": 366223.559407853, + "shield_regen": 0.0, + "shields": 366.223559407853, + "single_gun_cost_s": 50.4, + "speed": 173.869133517008, + "stop_from_tab_s": 21.733641689626, + "tab_speed": 347.738267034016, + "two_guns_and_tab_cost_s": 125.2, + "two_guns_cost_s": 105.2 + }, + "representative_energy_runtime": { + "duration": 30.0, + "energy_end": 25.358549118128, + "energy_start": 249.432638822183, + "failed_shots": 79, + "shots": 161 + }, + "representative_single_gun_runtime": { + "duration": 30.0, + "energy_end": 249.432638822183, + "energy_start": 249.432638822183, + "failed_shots": 0, + "shots": 120 + }, + "shield_leech_actual_from_1000_damage": 400.0, + "shield_max": { + "afterburner_speed": 463.95, + "max_energy": 323.92, + "raw_dps": 8029.32891461182, + "regen": 72.176, + "scanner_range": 4358.8, + "shield_leech_percent": 40.0, + "shield_leech_s_at_50pct_accuracy_20pct_absorption": 1284.69262633789, + "shield_refill_s": 18.9161642492539, + "shield_regen": 74.6399993896484, + "shields": 1411.9024880188, + "single_gun_cost_s": 66.8, + "speed": 154.65, + "stop_from_tab_s": 19.33125, + "tab_speed": 309.3, + "two_guns_and_tab_cost_s": 153.6, + "two_guns_cost_s": 133.6 + }, + "shield_runtime": { + "empty_to_full_seconds": 18.9166666666669, + "leech_on_1000_actual_damage": 400.0, + "regen_works_during_combat": true + }, + "solar_regen": 124.046628086734, + "solar_two_guns_runtime": { + "duration": 30.0, + "energy_end": 249.432638822183, + "energy_start": 249.432638822183, + "failed_shots": 0, + "shots": 240 + }, + "speed_max": { + "afterburner_speed": 1156.53455433655, + "max_energy": 323.92, + "raw_dps": 84950.3023669497, + "regen": 72.176, + "scanner_range": 4358.8, + "shield_leech_percent": 0.0, + "shield_leech_s_at_50pct_accuracy_20pct_absorption": 0.0, + "shield_refill_s": 435880.0, + "shield_regen": 0.0, + "shields": 435.88, + "single_gun_cost_s": 83.5, + "speed": 385.511518112183, + "stop_from_tab_s": 48.1889397640228, + "tab_speed": 771.023036224365, + "two_guns_and_tab_cost_s": 187.0, + "two_guns_cost_s": 167.0 + } +} \ No newline at end of file diff --git a/artifacts/ship_level_120_balance_changes.md b/artifacts/ship_level_120_balance_changes.md new file mode 100644 index 0000000..dd45665 --- /dev/null +++ b/artifacts/ship_level_120_balance_changes.md @@ -0,0 +1,51 @@ +# Balans statku — poprawki po audycie poziomu 120 + +Zmiany nie zmieniają wzrostu bazowych parametrów przedmiotów +7% ani wzrostu HP, tarcz i ataku przeciwników +10% / +20% po poziomie 120. Ograniczają łączne efekty wyposażenia. Istniejące przedmioty również podlegają limitom po przeliczeniu statystyk. + +## Energia + +- Nowe reaktory mają bazowy bonus regeneracji 4 zamiast 12 przed dotychczasowym mnożnikiem 0,5. Pozostaje skalowanie +7% na poziom. +- Łączna regeneracja bazowa i ze sprzętu: do 15% pojemności generatora/s, z minimum limitu 5/s. Dodatkowe addony regeneracji pozostają użyteczne. +- Pełne ładowanie słoneczne: dodatkowo do 8% generatora/s zamiast 25%. Maleje z odległością od gwiazdy. +- TAB: większa z wartości 20 energii/s i 22% pojemności generatora/s. Podstawowa regeneracja nie podtrzyma go sama. +- Orb silników atomowych nadal zmniejsza koszt TAB o połowę. Orb dopalacza zachowuje darmowy TAB. +- Czasowe umiejętności kapitana nadal mogą podnieść regenerację ponad limit sprzętu. To ograniczona czasowo korzyść. + +## Ruch + +- Łączna premia prędkości ze sprzętu: do +60%. +- Zwykły lot: do 240 px/s; TAB: do 480 px/s; aktywny dopalacz: do 600 px/s. +- Opór ruchu rośnie wraz z prędkością maksymalną: `max(16, max_speed * 0.2)`. Dryf z pełnego TAB zanika w około 10 s, zamiast wydłużać się do kilkudziesięciu sekund. Aktywne hamowanie jest szybsze. + +## Tarcze i polaryzacja + +- Pasywna regeneracja: do 3% maksymalnych tarcz/s. +- Odzysk z obrażeń: do 10% faktycznie zadanych obrażeń ze wszystkich addonów łącznie. +- Odzysk tarcz ma odnawianą rezerwę wielkości 5% maksymalnych tarcz. Rezerwa odnawia się w tempie 5% maksymalnych tarcz/s. Seria trafień i obrażenia obszarowe nie obchodzą tego limitu; dostępna na początku pełna rezerwa pozwala na jeden większy skok leczenia. +- Polaryzacja: energia z trafienia nie przekracza mniejszej wartości z 5% maksymalnych tarcz i faktycznej straty tarcz. Ma osobną rezerwę 5% pojemności generatora, odnawianą w tym samym tempie na sekundę. +- Uniki, trafienia w sam kadłub, dokowanie i zniszczenie nie uruchamiają polaryzacji. Leczenie nie działa po zniszczeniu ani podczas przekierowania zasilania. +- Pełny generator lub pełne tarcze nie zużywają rezerw. Przekładanie modułów nie odnawia rezerw. + +## Obrażenia i krytyki + +- Premia obrażeń ze sprzętu: do +100%. +- Szansa krytyczna ze sprzętu: do 40%; łącznie z kapitanem do 60%. +- Premia mnożnika krytycznego ze sprzętu: do +100%, czyli maksymalnie ×4 przy bazie ×2. +- Statystyki, porównywanie wyposażenia i obliczenia DPS uwzględniają te same limity. Podpowiedzi w statystykach wyjaśniają ograniczenia. + +## Ponowny audyt + +Ta sama próbka: Szerszeń, poziom 120, 500 zwykłych zestawów, 500 zestawów z bossów, 200 zestawów laserowych; brak umiejętności kapitana. Dodatkowo zestawy z maksymalnymi rollami. Ogień badano na krokach 1/60 s przez 30 s, a ruch wspólną funkcją sterowania. + +| Pomiar | Przed | Po | +|---|---:|---:| +| Mediana regeneracji energii, zwykłe zestawy | 62,7/s | 25,3/s | +| Zwykłe zestawy podtrzymujące jedną plazmę | 82,8% | 0% | +| Maksymalna prędkość badanego zestawu na TAB | 771 px/s | 480 px/s | +| Dryf po puszczeniu TAB w skrajnym zestawie | 48 s | 10 s | +| Pięć trafień po 1 pkt: odzysk energii | 91,6 | 5,0 | +| Leczenie z 1000 obrażeń, zestaw tarczowy | 400 | 70,6 | +| Ładowanie pustych tarcz, zestaw tarczowy | 18,9 s | 33,3 s | +| Teoretyczny DPS skrajnego zestawu ofensywnego | 84 950 | 6 817 | + +Wyniki dotyczą mierzonej próbki, nie wszystkich możliwych buildów. Specjalizacja energetyczna, umiejętności, orby i ładowanie słoneczne nadal mogą świadomie poprawiać bilans energii. Teoretyczny DPS nie uwzględnia ograniczeń energii, amunicji, celności i absorpcji celu; nie jest to symulacja całej kampanii. diff --git a/assets/freighter/research/weapon_technology_prompts.md b/assets/freighter/research/weapon_technology_prompts.md new file mode 100644 index 0000000..17ea479 --- /dev/null +++ b/assets/freighter/research/weapon_technology_prompts.md @@ -0,0 +1,36 @@ +# Ikony technologii broni + +Wygenerowane wbudowanym narzędziem ImageGen. Osiem osobnych grafik, bez tekstu i ramek; ramki dodaje interfejs. + +## weapons_a.png + +Use case: stylized-concept. Asset type: single square research technology icon for a gritty top-down science-fiction space game. Weak point: a single dark metallic alien starship armor plate with one glowing orange exposed vulnerable core, precision crosshair aimed at that core. Hand-painted polished game inventory icon, readable strong centered silhouette at 26 pixels, restrained metallic detail, dark navy-black background, subtle glow, high contrast. One compact composition occupies 85% of square. No text, no letters, no numbers, no outer frame, no border, no interface or multi-icon grid. Offensive technology, amber and orange accent palette. + +## weapons_a_next.png + +Use case: stylized-concept. Asset type: single square research technology icon for a gritty top-down science-fiction space game. Precision shot disabling a system: a bright orange precision bolt hitting an alien engine mechanism; a compact engine turbine with orange electric sparks and one severed circuit. Hand-painted polished game inventory icon, readable strong centered silhouette at 26 pixels, restrained metallic detail, dark navy-black background, subtle glow, high contrast. One compact composition occupies 85% of square. No text, no letters, no numbers, no outer frame, no border, no interface or multi-icon grid. Offensive technology, amber and orange accent palette. + +## weapons_a_next_next.png + +Use case: stylized-concept. Asset type: single square research technology icon for a gritty top-down science-fiction space game. Ricochet area weapon: an orange projectile striking a central alien hull and a tight circular orange shockwave reaching two nearby small alien silhouettes. Hand-painted polished game inventory icon, readable strong centered silhouette at 26 pixels, restrained metallic detail, dark navy-black background, subtle glow, high contrast. One compact composition occupies 85% of square. No text, no letters, no numbers, no outer frame, no border, no interface or multi-icon grid. Offensive technology, amber and orange accent palette. + +## weapons_a_next_next_next.png + +Use case: stylized-concept. Asset type: single square research technology icon for a gritty top-down science-fiction space game. Perfect shot: an orange lance passing through a cyan shield bubble and striking the armored alien hull inside, both shield and hull visibly flash at once. Hand-painted polished game inventory icon, readable strong centered silhouette at 26 pixels, restrained metallic detail, dark navy-black background, subtle glow, high contrast. One compact composition occupies 85% of square. No text, no letters, no numbers, no outer frame, no border, no interface or multi-icon grid. Offensive technology, amber and orange accent palette. + +## weapons_b.png + +Use case: stylized-concept. Asset type: single square research technology icon for a gritty top-down science-fiction space game. Predictive maneuver: a small emerald green futuristic fighter banking away from one incoming red projectile; two short green motion arcs communicate evasion. Hand-painted polished game inventory icon, readable strong centered silhouette at 26 pixels, restrained metallic detail, dark navy-black background, subtle glow, high contrast. One compact composition occupies 85% of square. No text, no letters, no numbers, no outer frame, no border, no interface or multi-icon grid. Defensive technology, emerald green accent palette. + +## weapons_b_next.png + +Use case: stylized-concept. Asset type: single square research technology icon for a gritty top-down science-fiction space game. Alien missile defense: a green point defense turret intercepting a red incoming missile, the missile breaks apart into harmless orange fragments outside a small green shield arc. Hand-painted polished game inventory icon, readable strong centered silhouette at 26 pixels, restrained metallic detail, dark navy-black background, subtle glow, high contrast. One compact composition occupies 85% of square. No text, no letters, no numbers, no outer frame, no border, no interface or multi-icon grid. Defensive technology, emerald green accent palette. + +## weapons_b_next_next.png + +Use case: stylized-concept. Asset type: single square research technology icon for a gritty top-down science-fiction space game. Alien targeting disruption: a green electronic targeting reticle with a red alien gun firing three diverging red trajectories that miss a centered green ship symbol. Hand-painted polished game inventory icon, readable strong centered silhouette at 26 pixels, restrained metallic detail, dark navy-black background, subtle glow, high contrast. One compact composition occupies 85% of square. No text, no letters, no numbers, no outer frame, no border, no interface or multi-icon grid. Defensive technology, emerald green accent palette. + +## weapons_b_next_next_next.png + +Use case: stylized-concept. Asset type: single square research technology icon for a gritty top-down science-fiction space game. Alien friendly fire jamming: two angular dark alien fighters and a red projectile from one striking the other, surrounded by a small emerald green electronic interference halo. Hand-painted polished game inventory icon, readable strong centered silhouette at 26 pixels, restrained metallic detail, dark navy-black background, subtle glow, high contrast. One compact composition occupies 85% of square. No text, no letters, no numbers, no outer frame, no border, no interface or multi-icon grid. Defensive technology, emerald green accent palette. + diff --git a/assets/freighter/research/weapons_a.png b/assets/freighter/research/weapons_a.png new file mode 100644 index 0000000..1fe6c50 Binary files /dev/null and b/assets/freighter/research/weapons_a.png differ diff --git a/assets/freighter/research/weapons_a.png.import b/assets/freighter/research/weapons_a.png.import new file mode 100644 index 0000000..5f2a7e6 --- /dev/null +++ b/assets/freighter/research/weapons_a.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bax7w2mtykgor" +path="res://.godot/imported/weapons_a.png-9c43553085a1a9139f5c7d1f7d0bef38.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/freighter/research/weapons_a.png" +dest_files=["res://.godot/imported/weapons_a.png-9c43553085a1a9139f5c7d1f7d0bef38.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/freighter/research/weapons_a_next.png b/assets/freighter/research/weapons_a_next.png new file mode 100644 index 0000000..cccda8a Binary files /dev/null and b/assets/freighter/research/weapons_a_next.png differ diff --git a/assets/freighter/research/weapons_a_next.png.import b/assets/freighter/research/weapons_a_next.png.import new file mode 100644 index 0000000..f906c56 --- /dev/null +++ b/assets/freighter/research/weapons_a_next.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c85khktupflwa" +path="res://.godot/imported/weapons_a_next.png-1c0b66600bfe7f38773b0fc786a50c2f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/freighter/research/weapons_a_next.png" +dest_files=["res://.godot/imported/weapons_a_next.png-1c0b66600bfe7f38773b0fc786a50c2f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/freighter/research/weapons_a_next_next.png b/assets/freighter/research/weapons_a_next_next.png new file mode 100644 index 0000000..87df509 Binary files /dev/null and b/assets/freighter/research/weapons_a_next_next.png differ diff --git a/assets/freighter/research/weapons_a_next_next.png.import b/assets/freighter/research/weapons_a_next_next.png.import new file mode 100644 index 0000000..01b238c --- /dev/null +++ b/assets/freighter/research/weapons_a_next_next.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b718pubb2lt04" +path="res://.godot/imported/weapons_a_next_next.png-bc17497d133cebc24c72e43d138e4de5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/freighter/research/weapons_a_next_next.png" +dest_files=["res://.godot/imported/weapons_a_next_next.png-bc17497d133cebc24c72e43d138e4de5.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/freighter/research/weapons_a_next_next_next.png b/assets/freighter/research/weapons_a_next_next_next.png new file mode 100644 index 0000000..5246cae Binary files /dev/null and b/assets/freighter/research/weapons_a_next_next_next.png differ diff --git a/assets/freighter/research/weapons_a_next_next_next.png.import b/assets/freighter/research/weapons_a_next_next_next.png.import new file mode 100644 index 0000000..a58da5d --- /dev/null +++ b/assets/freighter/research/weapons_a_next_next_next.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://efkejqdlvi51" +path="res://.godot/imported/weapons_a_next_next_next.png-131ca07347d68b9ce7a46fc260e67260.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/freighter/research/weapons_a_next_next_next.png" +dest_files=["res://.godot/imported/weapons_a_next_next_next.png-131ca07347d68b9ce7a46fc260e67260.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/freighter/research/weapons_b.png b/assets/freighter/research/weapons_b.png new file mode 100644 index 0000000..c5875e5 Binary files /dev/null and b/assets/freighter/research/weapons_b.png differ diff --git a/assets/freighter/research/weapons_b.png.import b/assets/freighter/research/weapons_b.png.import new file mode 100644 index 0000000..7898e57 --- /dev/null +++ b/assets/freighter/research/weapons_b.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://nhyqavg1336a" +path="res://.godot/imported/weapons_b.png-4e29879260d1fae488aa0493ee6d4995.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/freighter/research/weapons_b.png" +dest_files=["res://.godot/imported/weapons_b.png-4e29879260d1fae488aa0493ee6d4995.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/freighter/research/weapons_b_next.png b/assets/freighter/research/weapons_b_next.png new file mode 100644 index 0000000..b3ad362 Binary files /dev/null and b/assets/freighter/research/weapons_b_next.png differ diff --git a/assets/freighter/research/weapons_b_next.png.import b/assets/freighter/research/weapons_b_next.png.import new file mode 100644 index 0000000..438bb5f --- /dev/null +++ b/assets/freighter/research/weapons_b_next.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://co72crk8cu3bc" +path="res://.godot/imported/weapons_b_next.png-6d1626c8af414faf3da334bfa7036e27.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/freighter/research/weapons_b_next.png" +dest_files=["res://.godot/imported/weapons_b_next.png-6d1626c8af414faf3da334bfa7036e27.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/freighter/research/weapons_b_next_next.png b/assets/freighter/research/weapons_b_next_next.png new file mode 100644 index 0000000..d840a44 Binary files /dev/null and b/assets/freighter/research/weapons_b_next_next.png differ diff --git a/assets/freighter/research/weapons_b_next_next.png.import b/assets/freighter/research/weapons_b_next_next.png.import new file mode 100644 index 0000000..21298cc --- /dev/null +++ b/assets/freighter/research/weapons_b_next_next.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bjllc0ta5hxp2" +path="res://.godot/imported/weapons_b_next_next.png-9450fc181f456eca2b704c7549250e7e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/freighter/research/weapons_b_next_next.png" +dest_files=["res://.godot/imported/weapons_b_next_next.png-9450fc181f456eca2b704c7549250e7e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/freighter/research/weapons_b_next_next_next.png b/assets/freighter/research/weapons_b_next_next_next.png new file mode 100644 index 0000000..9ec8656 Binary files /dev/null and b/assets/freighter/research/weapons_b_next_next_next.png differ diff --git a/assets/freighter/research/weapons_b_next_next_next.png.import b/assets/freighter/research/weapons_b_next_next_next.png.import new file mode 100644 index 0000000..319e4aa --- /dev/null +++ b/assets/freighter/research/weapons_b_next_next_next.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://clw685uw2quu2" +path="res://.godot/imported/weapons_b_next_next_next.png-fc7b7e382143f62d5fd23260d45a93db.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/freighter/research/weapons_b_next_next_next.png" +dest_files=["res://.godot/imported/weapons_b_next_next_next.png-fc7b7e382143f62d5fd23260d45a93db.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/icons/orbs/atomic_engine.svg b/assets/icons/orbs/atomic_engine.svg new file mode 100644 index 0000000..7325da3 --- /dev/null +++ b/assets/icons/orbs/atomic_engine.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/orbs/atomic_engine.svg.import b/assets/icons/orbs/atomic_engine.svg.import new file mode 100644 index 0000000..2679648 --- /dev/null +++ b/assets/icons/orbs/atomic_engine.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://tibgmtl72krv" +path="res://.godot/imported/atomic_engine.svg-ea8c1ddea3c501df275722077f68d270.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/orbs/atomic_engine.svg" +dest_files=["res://.godot/imported/atomic_engine.svg-ea8c1ddea3c501df275722077f68d270.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/assets/icons/orbs/electricity.svg b/assets/icons/orbs/electricity.svg new file mode 100644 index 0000000..35b9676 --- /dev/null +++ b/assets/icons/orbs/electricity.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/orbs/electricity.svg.import b/assets/icons/orbs/electricity.svg.import new file mode 100644 index 0000000..bf955b5 --- /dev/null +++ b/assets/icons/orbs/electricity.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://pkyo5pilyr4c" +path="res://.godot/imported/electricity.svg-d427d134c8b8d820e86b5b3762ee5b15.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/orbs/electricity.svg" +dest_files=["res://.godot/imported/electricity.svg-d427d134c8b8d820e86b5b3762ee5b15.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/assets/icons/orbs/kinetic.svg b/assets/icons/orbs/kinetic.svg new file mode 100644 index 0000000..c008ebd --- /dev/null +++ b/assets/icons/orbs/kinetic.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/orbs/kinetic.svg.import b/assets/icons/orbs/kinetic.svg.import new file mode 100644 index 0000000..9476813 --- /dev/null +++ b/assets/icons/orbs/kinetic.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bkges2n325bqb" +path="res://.godot/imported/kinetic.svg-409d1b4b5c4a384e9b5ab9b80af7b4f7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/orbs/kinetic.svg" +dest_files=["res://.godot/imported/kinetic.svg-409d1b4b5c4a384e9b5ab9b80af7b4f7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/assets/icons/orbs/polarization.svg b/assets/icons/orbs/polarization.svg new file mode 100644 index 0000000..f986171 --- /dev/null +++ b/assets/icons/orbs/polarization.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/orbs/polarization.svg.import b/assets/icons/orbs/polarization.svg.import new file mode 100644 index 0000000..61a4a6e --- /dev/null +++ b/assets/icons/orbs/polarization.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dph88visw4jc" +path="res://.godot/imported/polarization.svg-f5403fd4cf6ca590b6c7ce5aaf6e6329.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/orbs/polarization.svg" +dest_files=["res://.godot/imported/polarization.svg-f5403fd4cf6ca590b6c7ce5aaf6e6329.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/assets/icons/orbs/radiation.svg b/assets/icons/orbs/radiation.svg new file mode 100644 index 0000000..e889d2c --- /dev/null +++ b/assets/icons/orbs/radiation.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/orbs/radiation.svg.import b/assets/icons/orbs/radiation.svg.import new file mode 100644 index 0000000..6b6c572 --- /dev/null +++ b/assets/icons/orbs/radiation.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dtmcg8nyvbxqx" +path="res://.godot/imported/radiation.svg-5d7a413a86edffcb25ca8f6d2859d05e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/orbs/radiation.svg" +dest_files=["res://.godot/imported/radiation.svg-5d7a413a86edffcb25ca8f6d2859d05e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/assets/icons/orbs/replication.svg b/assets/icons/orbs/replication.svg new file mode 100644 index 0000000..77e0ecb --- /dev/null +++ b/assets/icons/orbs/replication.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/orbs/replication.svg.import b/assets/icons/orbs/replication.svg.import new file mode 100644 index 0000000..5784c73 --- /dev/null +++ b/assets/icons/orbs/replication.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bkr5f7o0h5s03" +path="res://.godot/imported/replication.svg-f343de83ad94ff3d19a8ce7e870152e2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/orbs/replication.svg" +dest_files=["res://.godot/imported/replication.svg-f343de83ad94ff3d19a8ce7e870152e2.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/assets/planets/deuterium_deposit.png b/assets/planets/deuterium_deposit.png new file mode 100644 index 0000000..47b1184 Binary files /dev/null and b/assets/planets/deuterium_deposit.png differ diff --git a/assets/planets/deuterium_deposit.png.import b/assets/planets/deuterium_deposit.png.import new file mode 100644 index 0000000..31a3f74 --- /dev/null +++ b/assets/planets/deuterium_deposit.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://vouqi0b4nliq" +path="res://.godot/imported/deuterium_deposit.png-535b593bd24823844108c2999bc0fbac.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/planets/deuterium_deposit.png" +dest_files=["res://.godot/imported/deuterium_deposit.png-535b593bd24823844108c2999bc0fbac.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/planets/deuterium_deposit.prompt.md b/assets/planets/deuterium_deposit.prompt.md new file mode 100644 index 0000000..d3941d8 --- /dev/null +++ b/assets/planets/deuterium_deposit.prompt.md @@ -0,0 +1,5 @@ +# Deuterium deposit + +Generated with built-in image_gen, transparent PNG. + +undefined diff --git a/resources/items/affix_database.tres b/resources/items/affix_database.tres index 8fcadd9..d72d806 100644 --- a/resources/items/affix_database.tres +++ b/resources/items/affix_database.tres @@ -1,4 +1,4 @@ -[gd_resource type="Resource" script_class="ItemAffixDatabase" load_steps=19 format=3] +[gd_resource type="Resource" script_class="ItemAffixDatabase" load_steps=20 format=3] [ext_resource type="Script" path="res://scripts/items/item_affix_database.gd" id="1_database"] [ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="2_type"] @@ -23,6 +23,8 @@ [ext_resource type="Resource" path="res://resources/items/affixes/luck_percent.tres" id="luck"] +[ext_resource type="Resource" path="res://resources/items/affixes/attack_speed_percent.tres" id="attack_speed"] + [resource] script = ExtResource("1_database") -affixes = Array[ExtResource("2_type")]([ExtResource("3_affix"), ExtResource("4_affix"), ExtResource("5_affix"), ExtResource("6_affix"), ExtResource("7_affix"), ExtResource("8_affix"), ExtResource("9_affix"), ExtResource("10_affix"), ExtResource("11_affix"), ExtResource("12_affix"), ExtResource("13_affix"), ExtResource("14_affix"), ExtResource("kinetic"), ExtResource("energy"), ExtResource("radiation"), ExtResource("luck")]) +affixes = Array[ExtResource("2_type")]([ExtResource("3_affix"), ExtResource("4_affix"), ExtResource("5_affix"), ExtResource("6_affix"), ExtResource("7_affix"), ExtResource("8_affix"), ExtResource("9_affix"), ExtResource("10_affix"), ExtResource("11_affix"), ExtResource("12_affix"), ExtResource("13_affix"), ExtResource("14_affix"), ExtResource("kinetic"), ExtResource("energy"), ExtResource("radiation"), ExtResource("luck"), ExtResource("attack_speed")]) diff --git a/resources/items/affixes/attack_speed_percent.tres b/resources/items/affixes/attack_speed_percent.tres new file mode 100644 index 0000000..8c3872a --- /dev/null +++ b/resources/items/affixes/attack_speed_percent.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="ItemAffixDefinition" load_steps=2 format=3] +[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="1"] +[resource] +script = ExtResource("1") +affix_id = "attack_speed_percent" +display_name = "Szybkostrzelność" +description = "Zwiększa tempo strzałów tej broni. Koszt pojedynczego strzału pozostaje bez zmian. Limit bonusu: 25%." +stat_key = "attack_speed_percent" +category_mask = 1 +minimum_value_level_1 = 2.0 +maximum_value_level_1 = 3.0 +minimum_value_cap = 15.0 +maximum_value_cap = 25.0 diff --git a/resources/items/affixes/credits_percent.tres b/resources/items/affixes/credits_percent.tres index 66f54df..eac0402 100644 --- a/resources/items/affixes/credits_percent.tres +++ b/resources/items/affixes/credits_percent.tres @@ -12,6 +12,6 @@ selection_weight = 1.0 category_mask = 95 minimum_value_level_1 = 2 maximum_value_level_1 = 5 -minimum_value_level_1000 = 40 -maximum_value_level_1000 = 75 +minimum_value_cap = 40 +maximum_value_cap = 75 diff --git a/resources/items/affixes/critical_chance_percent.tres b/resources/items/affixes/critical_chance_percent.tres index 5fc050b..408bf93 100644 --- a/resources/items/affixes/critical_chance_percent.tres +++ b/resources/items/affixes/critical_chance_percent.tres @@ -11,5 +11,5 @@ unit = "%" category_mask = 95 minimum_value_level_1 = 1.0 maximum_value_level_1 = 2.0 -minimum_value_level_1000 = 10.0 -maximum_value_level_1000 = 20.0 +minimum_value_cap = 10.0 +maximum_value_cap = 20.0 diff --git a/resources/items/affixes/critical_damage_percent.tres b/resources/items/affixes/critical_damage_percent.tres index c94da61..e2ea5ae 100644 --- a/resources/items/affixes/critical_damage_percent.tres +++ b/resources/items/affixes/critical_damage_percent.tres @@ -11,5 +11,5 @@ unit = "%" category_mask = 95 minimum_value_level_1 = 5.0 maximum_value_level_1 = 10.0 -minimum_value_level_1000 = 50.0 -maximum_value_level_1000 = 100.0 +minimum_value_cap = 50.0 +maximum_value_cap = 100.0 diff --git a/resources/items/affixes/energy_absorption.tres b/resources/items/affixes/energy_absorption.tres index 7fc3ec3..c5af9b1 100644 --- a/resources/items/affixes/energy_absorption.tres +++ b/resources/items/affixes/energy_absorption.tres @@ -9,5 +9,5 @@ description = "Niweluje obrażenia tego typu przed tarczą i pancerzem. Łączny category_mask = 95 minimum_value_level_1 = 1.0 maximum_value_level_1 = 1.0 -minimum_value_level_1000 = 10.0 -maximum_value_level_1000 = 15.0 +minimum_value_cap = 10.0 +maximum_value_cap = 15.0 diff --git a/resources/items/affixes/energy_regeneration.tres b/resources/items/affixes/energy_regeneration.tres index 33321d1..6e09da0 100644 --- a/resources/items/affixes/energy_regeneration.tres +++ b/resources/items/affixes/energy_regeneration.tres @@ -12,6 +12,6 @@ selection_weight = 1.0 category_mask = 95 minimum_value_level_1 = 0.2 maximum_value_level_1 = 0.5 -minimum_value_level_1000 = 100 -maximum_value_level_1000 = 180 +minimum_value_cap = 100 +maximum_value_cap = 180 diff --git a/resources/items/affixes/experience_percent.tres b/resources/items/affixes/experience_percent.tres index 5dba5af..f3d936b 100644 --- a/resources/items/affixes/experience_percent.tres +++ b/resources/items/affixes/experience_percent.tres @@ -12,6 +12,6 @@ selection_weight = 1.0 category_mask = 95 minimum_value_level_1 = 2 maximum_value_level_1 = 5 -minimum_value_level_1000 = 40 -maximum_value_level_1000 = 75 +minimum_value_cap = 40 +maximum_value_cap = 75 diff --git a/resources/items/affixes/hull_percent.tres b/resources/items/affixes/hull_percent.tres index 0d52156..5c725c6 100644 --- a/resources/items/affixes/hull_percent.tres +++ b/resources/items/affixes/hull_percent.tres @@ -12,6 +12,6 @@ selection_weight = 1.0 category_mask = 95 minimum_value_level_1 = 1 maximum_value_level_1 = 3 -minimum_value_level_1000 = 25 -maximum_value_level_1000 = 50 +minimum_value_cap = 25 +maximum_value_cap = 50 diff --git a/resources/items/affixes/kinetic_absorption.tres b/resources/items/affixes/kinetic_absorption.tres index e588785..631ee21 100644 --- a/resources/items/affixes/kinetic_absorption.tres +++ b/resources/items/affixes/kinetic_absorption.tres @@ -9,5 +9,5 @@ description = "Niweluje obrażenia tego typu przed tarczą i pancerzem. Łączny category_mask = 95 minimum_value_level_1 = 1.0 maximum_value_level_1 = 1.0 -minimum_value_level_1000 = 10.0 -maximum_value_level_1000 = 15.0 +minimum_value_cap = 10.0 +maximum_value_cap = 15.0 diff --git a/resources/items/affixes/luck_percent.tres b/resources/items/affixes/luck_percent.tres index 8c4f8c1..10d53c3 100644 --- a/resources/items/affixes/luck_percent.tres +++ b/resources/items/affixes/luck_percent.tres @@ -11,5 +11,5 @@ selection_weight = 0.7 category_mask = 95 minimum_value_level_1 = 0.5 maximum_value_level_1 = 1.0 -minimum_value_level_1000 = 3.0 -maximum_value_level_1000 = 6.0 +minimum_value_cap = 3.0 +maximum_value_cap = 6.0 diff --git a/resources/items/affixes/missile_cooldown.tres b/resources/items/affixes/missile_cooldown.tres index fbef4ac..e9649e5 100644 --- a/resources/items/affixes/missile_cooldown.tres +++ b/resources/items/affixes/missile_cooldown.tres @@ -9,5 +9,5 @@ unit = "s" category_mask = 95 minimum_value_level_1 = 0.1 maximum_value_level_1 = 0.15 -minimum_value_level_1000 = 1.0 -maximum_value_level_1000 = 2.0 +minimum_value_cap = 1.0 +maximum_value_cap = 2.0 diff --git a/resources/items/affixes/radiation_absorption.tres b/resources/items/affixes/radiation_absorption.tres index 392fc3e..326f41c 100644 --- a/resources/items/affixes/radiation_absorption.tres +++ b/resources/items/affixes/radiation_absorption.tres @@ -9,5 +9,5 @@ description = "Niweluje obrażenia tego typu przed tarczą i pancerzem. Łączny category_mask = 95 minimum_value_level_1 = 1.0 maximum_value_level_1 = 1.0 -minimum_value_level_1000 = 10.0 -maximum_value_level_1000 = 15.0 +minimum_value_cap = 10.0 +maximum_value_cap = 15.0 diff --git a/resources/items/affixes/shield_on_damage.tres b/resources/items/affixes/shield_on_damage.tres index adce365..9bb5c6e 100644 --- a/resources/items/affixes/shield_on_damage.tres +++ b/resources/items/affixes/shield_on_damage.tres @@ -4,11 +4,11 @@ script = ExtResource("1_affix") affix_id = "shield_on_damage" display_name = "Odzysk tarcz z obrażeń" -description = "Globalnie odnawia tarcze o procent faktycznie zadanych obrażeń każdą bronią." +description = "Globalnie odnawia tarcze z faktycznie zadanych obrażeń. Łącznie do 10% obrażeń; tempo odzysku do 5% maksymalnych tarcz/s, z rezerwą na jedno takie odnowienie." stat_key = "shield_on_damage_percent" unit = "%" category_mask = 95 minimum_value_level_1 = 1.0 maximum_value_level_1 = 1.0 -minimum_value_level_1000 = 5.0 -maximum_value_level_1000 = 5.0 +minimum_value_cap = 5.0 +maximum_value_cap = 5.0 diff --git a/resources/items/affixes/shield_percent.tres b/resources/items/affixes/shield_percent.tres index 2040dc2..0b25c5d 100644 --- a/resources/items/affixes/shield_percent.tres +++ b/resources/items/affixes/shield_percent.tres @@ -12,6 +12,6 @@ selection_weight = 1.0 category_mask = 95 minimum_value_level_1 = 1 maximum_value_level_1 = 3 -minimum_value_level_1000 = 25 -maximum_value_level_1000 = 50 +minimum_value_cap = 25 +maximum_value_cap = 50 diff --git a/resources/items/affixes/shield_regeneration.tres b/resources/items/affixes/shield_regeneration.tres index 1c03fc1..9bf759c 100644 --- a/resources/items/affixes/shield_regeneration.tres +++ b/resources/items/affixes/shield_regeneration.tres @@ -12,6 +12,6 @@ selection_weight = 1.0 category_mask = 95 minimum_value_level_1 = 0.5 maximum_value_level_1 = 1 -minimum_value_level_1000 = 200 -maximum_value_level_1000 = 350 +minimum_value_cap = 200 +maximum_value_cap = 350 diff --git a/resources/items/affixes/speed_percent.tres b/resources/items/affixes/speed_percent.tres index 0d87bfe..7353180 100644 --- a/resources/items/affixes/speed_percent.tres +++ b/resources/items/affixes/speed_percent.tres @@ -12,6 +12,6 @@ selection_weight = 1.0 category_mask = 95 minimum_value_level_1 = 1 maximum_value_level_1 = 2 -minimum_value_level_1000 = 15 -maximum_value_level_1000 = 25 +minimum_value_cap = 15 +maximum_value_cap = 25 diff --git a/resources/items/compact_generator.tres b/resources/items/compact_generator.tres index b7c5786..9e870e5 100644 --- a/resources/items/compact_generator.tres +++ b/resources/items/compact_generator.tres @@ -14,4 +14,4 @@ description = "Zwiększa maksymalną pojemność energii." primary_stat = "max_energy" primary_value = 20.0 stat_suffix = " energii" -energy_regeneration_bonus = 12.0 +energy_regeneration_bonus = 4.0 diff --git a/scripts/captain/captain_progress.gd b/scripts/captain/captain_progress.gd index 6fc240a..5c81cac 100644 --- a/scripts/captain/captain_progress.gd +++ b/scripts/captain/captain_progress.gd @@ -201,7 +201,7 @@ func encounter_xp(base_amount: float, system_level: int, xp_multiplier: float = 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 scale_factor := 1.0 + 0.05 * (clampi(system_level, 1, 1000) - 1) + var scale_factor := preload("res://scripts/levels/level_balance.gd").item_stat_scale(clampi(system_level, 1, 1000)) 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)) add_rewards(xp, money if grant_credits else 0) diff --git a/scripts/game/alien_weapon_effects.gd b/scripts/game/alien_weapon_effects.gd new file mode 100644 index 0000000..165d72c --- /dev/null +++ b/scripts/game/alien_weapon_effects.gd @@ -0,0 +1,26 @@ +extends RefCounted +const DATA := preload("res://scripts/planets/alien_research.gd") + +static func roll_disabled_system(roll := -1.0, system := -1) -> int: + if not DATA.learned("weapons_a_next") or (randf() if roll < 0 else roll) >= 0.01: return -1 + return randi_range(0,2) if system < 0 else clampi(system,0,2) + +static func apply_hit(target: Variant, damage: float, critical: bool, kind: int, piercing: float, visited: Dictionary, splash := true, disabled_system := -2) -> void: + if not target is Node or target.is_queued_for_deletion(): return + var id: int = target.get_instance_id() + if visited.has(id): return + visited[id] = true + if target.has_method("take_research_weapon_hit"): + target.take_research_weapon_hit(damage,critical,kind,piercing,disabled_system) + elif target.has_method("take_piercing_weapon_hit"): + target.take_piercing_weapon_hit(damage,critical,kind,piercing) + elif target.has_method("take_weapon_hit"): + target.take_weapon_hit(damage,critical,kind) + elif target.has_method("take_damage"): + target.take_damage(damage) + if not splash or not DATA.learned("weapons_a_next_next") or not target is Node2D or not target.is_in_group("enemies"): return + var radius := float(target.get_visual_size())*0.5+10.0 + for other in target.get_tree().get_nodes_in_group("enemies"): + if other == target or other.is_queued_for_deletion() or float(other.get("hull")) <= 0: continue + if target.global_position.distance_to(other.global_position) <= radius+float(other.get_visual_size())*0.5: + apply_hit(other,damage,critical,kind,piercing,visited,false,-1) diff --git a/scripts/game/alien_weapon_effects.gd.uid b/scripts/game/alien_weapon_effects.gd.uid new file mode 100644 index 0000000..c2e5cff --- /dev/null +++ b/scripts/game/alien_weapon_effects.gd.uid @@ -0,0 +1 @@ +uid://bphkxpc45ig5e diff --git a/scripts/game/distant_loot.gd b/scripts/game/distant_loot.gd index 671ae21..f72261e 100644 --- a/scripts/game/distant_loot.gd +++ b/scripts/game/distant_loot.gd @@ -1,13 +1,16 @@ extends RefCounted const MAP_CHANCE := 0.15 const BANK_CHANCE := 0.15 -static func roll(level: int, boss: bool, seed_value: int, map_roll: float, _orb_roll: float) -> Array[Dictionary]: +const REPLICATION_CHANCE := 0.05 +static func roll(level: int, boss: bool, seed_value: int, map_roll: float, orb_roll: float) -> Array[Dictionary]: var result: Array[Dictionary] = [] if boss or map_roll < MAP_CHANCE: var profile := {"id":"distant:%d" % seed_value,"seed":seed_value,"name":"Daleka wyprawa %d" % level} result.append({"map":preload("res://scripts/planets/distant_expeditions.gd").offer(profile,level)}) if boss: - result.append({"orb":preload("res://scripts/items/special_orbs.gd").IDS[posmod(seed_value,preload("res://scripts/items/special_orbs.gd").IDS.size())]}) + var pool := preload("res://scripts/items/special_orbs.gd").COMMON_IDS + result.append({"orb":pool[posmod(seed_value,pool.size())]}) + if orb_roll < REPLICATION_CHANCE: result.append({"orb":"replication"}) return result static func crystal_amount(level: int, tier: int, roll_value: int) -> int: return clampi(roll_value,1,3) * maxi(1,ceili(maxi(1,level)/10.0)) * [1,2,5][clampi(tier,0,2)] diff --git a/scripts/game/distant_sector.gd b/scripts/game/distant_sector.gd index a5118a9..f4c1a5e 100644 --- a/scripts/game/distant_sector.gd +++ b/scripts/game/distant_sector.gd @@ -2,7 +2,6 @@ extends Node const GENERATOR := preload("res://scripts/levels/distant_sector_generator.gd") var game: Node2D var _status: Label -var _return: Button var _refresh := 0.0 func _ready() -> void: @@ -19,21 +18,24 @@ func _ready() -> void: if int(data.tier) == 2: game._pending_boss = data.duplicate() else: game._spawn_enemy(data,data.position) _build_structures() + var expeditions := Node.new() + expeditions.set_script(preload("res://scripts/planets/expedition_controller.gd")) + expeditions.game = game + expeditions.player = game.player + game.add_child(expeditions) var panel := PanelContainer.new() - panel.name = "DistantReturnPanel" + panel.name = "DistantStatusPanel" game.get_node("HUD/HUDRoot").add_child(panel) panel.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP) panel.offset_left = -195 panel.offset_right = 195 - panel.offset_top = 136 - panel.offset_bottom = 206 + panel.offset_top = 120 + panel.offset_bottom = 190 var ui := preload("res://scripts/planets/freighter_menu.gd") ui.style(panel,8,0.5) var column := VBoxContainer.new() panel.add_child(column) _status = ui.label(column,"",12,Color("e9b4fc")) - _return = ui.button(column,"POWRÓT DO FRACHTOWCA",_return_home,true) - _return.name = "ReturnFromDistant" _update_status() func _build_structures() -> void: @@ -75,13 +77,15 @@ func return_blocked() -> bool: func _update_status() -> void: _status.text = "WYPRAWA %d · %s" % [GameState.system_level,"SEKTOR OCZYSZCZONY" if game.system_cleared else ("BOSS" if game._boss_spawned else "%d wrogów" % game.get_remaining_enemies())] - _return.disabled = bool(game.player.get("_is_destroyed")) or return_blocked() - _return.tooltip_text = "Oddal się od przeciwników i zakończ walkę." if _return.disabled else "Wróć do układu startowego. Powrót nie zużywa deuteru. Zbierz łup przed odlotem." -func _return_home() -> void: - if game._transitioning or bool(game.player.get("_is_destroyed")) or return_blocked(): return +func _return_home() -> bool: + var controller := game.get_node_or_null("PlanetExpeditions") + if controller == null or not is_instance_valid(controller.screen) or not game.player.is_docked: return false + if not preload("res://scripts/planets/orbital_contact_range.gd").at_freighter(game.player, controller.freighter): return false + if game._transitioning or bool(game.player.get("_is_destroyed")) or return_blocked(): return false game._transitioning = true _load_home.call_deferred() + return true func _load_home() -> void: var active: Dictionary = GameState.active_distant_map.duplicate(true) diff --git a/scripts/game/enemy_ship.gd b/scripts/game/enemy_ship.gd index 4466f0f..616a84e 100644 --- a/scripts/game/enemy_ship.gd +++ b/scripts/game/enemy_ship.gd @@ -44,9 +44,12 @@ var _combat_target: Node2D var virus_controlled := false var _virus_infection_attempted := false var _research_player_hit := false +var _research_direct_hit := false var _weapon_piercing := 0.0 var combat_level := 1 var _reward_claimed := false +var _disabled_systems := [0.0,0.0,0.0] +var _offline_shields := 0.0 func roll_virus_infection() -> bool: if loot_tier == 2 or _virus_infection_attempted: @@ -169,11 +172,12 @@ func regenerate_defenses(delta: float) -> void: if loot_tier == 2 or hull <= 0.0 or is_queued_for_deletion(): return hull = minf(max_hull, hull + max_hull * 0.01 * delta) - shields = minf(max_shields, shields + max_shields * 0.01 * delta) + if not system_disabled(2): shields = minf(max_shields, shields + max_shields * 0.01 * delta) if is_instance_valid(_shield_visual): _shield_visual.set_shield_values(shields, max_shields) func _physics_process(delta: float) -> void: + tick_disabled_systems(delta) regenerate_defenses(delta) _slow_remaining = maxf(0.0, _slow_remaining - delta) _weapon_timer = maxf(0.0, _weapon_timer - delta) @@ -202,7 +206,8 @@ func _physics_process(delta: float) -> void: var movement := combat_movement(to_player) var desired_rotation := (movement if fleeing else direction).angle() + PI * 0.5 rotation = rotate_toward(rotation, desired_rotation, turn_speed * delta) - velocity = velocity.move_toward(movement * approach_speed * (0.5 if fleeing else 1.0) * get_slow_multiplier(), approach_acceleration * get_slow_multiplier() * delta) + var desired_velocity := Vector2.ZERO if system_disabled(0) else movement * approach_speed * (0.5 if fleeing else 1.0) * get_slow_multiplier() + velocity = velocity.move_toward(desired_velocity, approach_acceleration * delta) if not fleeing and to_player.length() <= weapon_range and _weapon_timer <= 0.0: _fire_plasma(direction) _weapon_timer = get_fire_interval() @@ -301,7 +306,10 @@ func _check_player_collision() -> void: _player.call("register_enemy_collision", global_position) func _fire_plasma(direction: Vector2) -> void: + if system_disabled(1): return + direction = research_aim_direction(direction) var shot_mask := 37 if virus_controlled else 115 + if not virus_controlled and preload("res://scripts/planets/alien_research.gd").learned("weapons_b_next_next_next"): shot_mask |= 4 var muzzle := global_position + direction * (get_visual_size() * 0.5 + 8.0) if weapon_kind == ItemDefinition.WeaponKind.LASER: var endpoint := muzzle + _laser_aim_direction(direction) * weapon_range @@ -323,6 +331,7 @@ func _fire_plasma(direction: Vector2) -> void: else: var projectile := preload("res://scenes/game/weapon_projectile.tscn").instantiate() projectile.enemy_shot = true + projectile.source_ship = self projectile.collision_mask = shot_mask projectile.missile_homing = false var speed := 430.0 if weapon_kind == ItemDefinition.WeaponKind.GAMMA else (950.0 if weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN else 600.0) @@ -336,15 +345,65 @@ func take_piercing_weapon_hit(amount: float, critical: bool, damage_type: int, p _weapon_piercing = 0.0 func take_weapon_hit(amount: float, critical: bool, damage_type: int = DamageTypes.Type.KINETIC) -> void: - if is_queued_for_deletion() or hull <= 0.0: + _take_player_hit(amount,critical,damage_type,true,-2) + +func take_research_weapon_hit(amount: float, critical: bool, damage_type: int, piercing: float, disabled_system: int) -> void: + _weapon_piercing = clampf(piercing,0.0,0.5) + _take_player_hit(amount,critical,damage_type,true,disabled_system) + _weapon_piercing = 0.0 + +func take_research_dot(amount: float, damage_type: int, piercing: float) -> void: + _weapon_piercing = clampf(piercing,0.0,0.5) + _take_player_hit(amount,false,damage_type,false,-1) + _weapon_piercing = 0.0 + +func _take_player_hit(amount: float, critical: bool, damage_type: int, direct: bool, disabled_system: int) -> void: + if is_queued_for_deletion() or hull <= 0.0 or amount <= 0.0: return var before := hull + shields _research_player_hit = true + var research := preload("res://scripts/planets/alien_research.gd") + if direct and amount > 0 and research.learned("weapons_a"): amount += 5.0 + _research_direct_hit = direct take_damage(amount, critical, damage_type) + _research_direct_hit = false _research_player_hit = false + if direct and hull > 0 and not is_queued_for_deletion(): + var chosen := preload("res://scripts/game/alien_weapon_effects.gd").roll_disabled_system() if disabled_system == -2 else disabled_system + if chosen >= 0: disable_system(chosen) if is_instance_valid(_player): _player.call("restore_shields_from_damage", maxf(0.0, before - hull - shields)) +func system_disabled(system: int) -> bool: + return system >= 0 and system < 3 and _disabled_systems[system] > 0.0 + +func disable_system(system: int) -> void: + if system < 0 or system >= 3 or hull <= 0 or is_queued_for_deletion(): return + if system == 2 and not system_disabled(2): + _offline_shields = shields + shields = 0.0 + _disabled_systems[system] = 3.0 + if is_instance_valid(_shield_visual): _shield_visual.set_shield_values(shields,max_shields) + var popup := DAMAGE_POPUP_SCENE.instantiate() as Label + popup.text = "%s OFF · 3 s" % ["SILNIK","BROŃ","TARCZE"][system] + popup.set("text_color",Color("efa653")) + popup.position = position+Vector2(-30,-50) + get_parent().add_child(popup) + +func tick_disabled_systems(delta: float) -> void: + var shields_offline := system_disabled(2) + for system in 3: _disabled_systems[system] = maxf(0.0,_disabled_systems[system]-delta) + if shields_offline and not system_disabled(2) and hull > 0: + shields = minf(max_shields,_offline_shields) + _offline_shields = 0.0 + if is_instance_valid(_shield_visual): _shield_visual.set_shield_values(shields,max_shields) + +func research_aim_direction(direction: Vector2, roll := -1.0, side := 0.0) -> Vector2: + if virus_controlled or not preload("res://scripts/planets/alien_research.gd").learned("weapons_b_next_next"): return direction + if (randf() if roll < 0 else roll) >= 0.25: return direction + var angle := randf_range(0.20,0.45) + return direction.rotated(angle*(signf(side) if side != 0.0 else (-1.0 if randf()<0.5 else 1.0))) + func take_damage(amount: float, critical: bool = false, damage_type: int = DamageTypes.Type.KINETIC, bypass_absorption: bool = false) -> void: if is_queued_for_deletion() or hull <= 0.0: return @@ -355,7 +414,9 @@ func take_damage(amount: float, critical: bool = false, damage_type: int = Damag var hull_multiplier := 1.0 + (research.bonus(progress, "hull") if _research_player_hit else 0.0) var piercing_damage := actual * _weapon_piercing if _research_player_hit else 0.0 var shield_damage := minf(shields, (actual - piercing_damage) * shield_multiplier) - var remaining := maxf(0.0, actual - shield_damage / shield_multiplier) + var double_hit := _research_direct_hit and research.learned("weapons_a_next_next_next") + if double_hit: shield_damage = minf(shields,actual*shield_multiplier) + var remaining := actual if double_hit else maxf(0.0, actual - shield_damage / shield_multiplier) shields -= shield_damage var hull_damage := minf(hull, remaining * hull_multiplier) hull = maxf(0.0, hull - hull_damage) diff --git a/scripts/game/flight_hud.gd b/scripts/game/flight_hud.gd index 28073b5..37ba7e1 100644 --- a/scripts/game/flight_hud.gd +++ b/scripts/game/flight_hud.gd @@ -45,8 +45,8 @@ func _create_navigation() -> void: navigation.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP) navigation.offset_left = -32 navigation.offset_right = 32 - navigation.offset_top = 44 - navigation.offset_bottom = 124 + navigation.offset_top = 28 + navigation.offset_bottom = 108 navigation.add_theme_constant_override("separation", 10) _captain_skills_ui = CanvasLayer.new() _captain_skills_ui.set_script(preload("res://scripts/ui/captain_skills_ui.gd")) @@ -127,7 +127,7 @@ func _ready() -> void: func _create_sidebar() -> void: _captain_panel = $HUDRoot/CaptainPanel _captain_panel.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT) - _captain_panel.position = Vector2(12, 44) + _captain_panel.position = Vector2(12, 28) _captain_panel.size = _captain_panel.get_combined_minimum_size() _captain_panel.resized.connect(_layout_debug) _create_system_badge() @@ -221,8 +221,8 @@ func _create_sidebar() -> void: _sidebar_toggle.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT) _sidebar_toggle.offset_left = -233 _sidebar_toggle.offset_right = -12 - _sidebar_toggle.offset_top = 44 - _sidebar_toggle.offset_bottom = 115.5 + _sidebar_toggle.offset_top = 28 + _sidebar_toggle.offset_bottom = 99.5 _sidebar_toggle.pressed.connect(_toggle_sidebar) _cockpit = Control.new() _cockpit.name = "Cockpit" @@ -298,7 +298,7 @@ func _layout_sidebar() -> void: var scroll := _sidebar.get_node("Scroll") as ScrollContainer scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED var content_height: float = scroll.get_node("Contents").get_combined_minimum_size().y + 16.0 - var available_height := maxf(100.0, $HUDRoot.size.y - 180.0 - _ship_passive_reserved_height) + var available_height := maxf(100.0, $HUDRoot.size.y - 164.0 - _ship_passive_reserved_height) var panel_scale := minf(EQUIPMENT_UI_SCALE, available_height / content_height) _sidebar.scale = Vector2.ONE * panel_scale _sidebar.size = Vector2(maxf(SIDEBAR_WIDTH, _sidebar.get_combined_minimum_size().x), content_height) @@ -315,7 +315,7 @@ func _layout_sidebar() -> void: _captain_skills_ui.call_deferred("_layout") _layout_debug() if not _sidebar.has_meta("floating_window_dragged"): - _sidebar.position = Vector2($HUDRoot.size.x - _sidebar.size.x * _sidebar.scale.x - 8, 168) + _sidebar.position = Vector2($HUDRoot.size.x - _sidebar.size.x * _sidebar.scale.x - 8, 152) else: _window_manager.call("clamp_window", _sidebar) _sidebar.visible = _sidebar_open @@ -633,8 +633,8 @@ func _update_system_badge() -> void: if not is_instance_valid(_system_badge): return _system_badge.position = Vector2(_captain_panel.position.x + _captain_panel.size.x + 10, _captain_panel.position.y) _system_title.text = ("Wyprawa %d" if not GameState.active_distant_map.is_empty() else "System %d") % GameState.system_level - _system_mode.text = "HARD" if GameState.system_level > 60 else "NORMALNY" - _system_mode.add_theme_color_override("font_color", Color("e88972") if GameState.system_level > 60 else Color("88a7ae")) + _system_mode.text = "HARD" if preload("res://scripts/levels/level_balance.gd").is_hard(GameState.system_level) else "NORMALNY" + _system_mode.add_theme_color_override("font_color", Color("e88972") if preload("res://scripts/levels/level_balance.gd").is_hard(GameState.system_level) else Color("88a7ae")) func _create_ship_statistics() -> void: _ship_statistics = preload("res://scripts/ui/ship_statistics.gd").new() diff --git a/scripts/game/game.gd b/scripts/game/game.gd index 9ffa55b..84e9769 100644 --- a/scripts/game/game.gd +++ b/scripts/game/game.gd @@ -262,10 +262,12 @@ func _load_distant(map_id: String) -> void: _transitioning = false return var cost: int = preload("res://scripts/planets/distant_expeditions.gd").fuel_cost(GameState.active_distant_map) + var map_snapshot: Dictionary = GameState.active_distant_map.duplicate(true) get_tree().paused = false var error := get_tree().reload_current_scene() if error != OK: GameState.restore_distant_home() + GameState.captain.expedition.distant_maps[map_id] = map_snapshot GameState.captain.expedition.deuter += cost GameState.captain.expedition.emit_changed() GameState.restart_inventory.clear() diff --git a/scripts/game/game_state.gd b/scripts/game/game_state.gd index 2fe1c5c..3b3eb48 100644 --- a/scripts/game/game_state.gd +++ b/scripts/game/game_state.gd @@ -26,6 +26,7 @@ func depart_distant(map_id: String) -> bool: if captain.expedition.deuter < cost: return false distant_home = {"level":system_level,"seed":system_seed} active_distant_map = map.duplicate(true) + captain.expedition.distant_maps.erase(map_id) captain.expedition.deuter -= cost system_level = int(map.level) system_seed = int(map.seed) diff --git a/scripts/game/radiation_status.gd b/scripts/game/radiation_status.gd index dbd4fe4..f107fd7 100644 --- a/scripts/game/radiation_status.gd +++ b/scripts/game/radiation_status.gd @@ -40,7 +40,9 @@ func _physics_process(delta: float) -> void: if _tick_elapsed >= 0.5 or (remaining <= 0.0 and _tick_elapsed > 0.0): step = _tick_elapsed _tick_elapsed = 0.0 - if credited_to_player and target.has_method("take_piercing_weapon_hit"): + if credited_to_player and target.has_method("take_research_dot"): + target.take_research_dot(tick_damage*step,DamageTypes.Type.RADIATION,shield_piercing) + elif credited_to_player and target.has_method("take_piercing_weapon_hit"): target.take_piercing_weapon_hit(tick_damage * step, false, DamageTypes.Type.RADIATION, shield_piercing) elif credited_to_player and target.has_method("take_weapon_hit"): target.take_weapon_hit(tick_damage * step, false, DamageTypes.Type.RADIATION) diff --git a/scripts/game/ship_controller.gd b/scripts/game/ship_controller.gd index 21d123a..832cc26 100644 --- a/scripts/game/ship_controller.gd +++ b/scripts/game/ship_controller.gd @@ -8,6 +8,7 @@ const CAPTAIN_BALANCE := preload("res://scripts/captain/captain_balance.gd") const SHIP_CATALOG := preload("res://scripts/game/ship_catalog.gd") const COMBAT_FLASH := preload("res://scripts/game/combat_flash.gd") const MINI_BOOST_ENERGY_PER_SECOND := 20.0 +const EQUIPMENT_BALANCE := preload("res://scripts/items/equipment_balance.gd") const DESTRUCTION_EFFECT := preload("res://scripts/game/player_destruction_effect.gd") @export var max_speed := 76.0 @@ -46,7 +47,7 @@ var critical_chance_bonus := 0.0 var critical_damage_bonus := 0.0 @export var solar_charge_radius := 550.0 @export var solar_charge_full_radius := 300.0 -@export var solar_charge_fraction_per_second := 0.25 +@export var solar_charge_fraction_per_second := 0.08 var charging_star: Node2D var angular_velocity := 0.0 @@ -62,6 +63,8 @@ var _boost_velocity_acceleration := 0.0 var _mini_boost_active := false var _mini_boost_exhausted := false var _mini_boost_coast_speed := 0.0 +var _shield_leech_budget := -1.0 +var _polarization_budget := -1.0 @onready var _shield_visual: Node2D = $ShieldVisual func _enter_tree() -> void: @@ -74,12 +77,14 @@ func _enter_tree() -> void: base_max_speed = float(chassis["speed"]) func apply_equipment_bonuses(bonuses: Dictionary) -> void: + bonuses = EQUIPMENT_BALANCE.bonuses(bonuses) absorption = DamageTypes.capped(Vector3(float(bonuses.get("kinetic_absorption", 0.0)), float(bonuses.get("energy_absorption", 0.0)), float(bonuses.get("radiation_absorption", 0.0)))) max_hull = (base_max_hull + float(bonuses.get("max_hull", 0.0))) * (1.0 + float(bonuses.get("hull_percent", 0.0)) / 100.0) max_shields = (base_max_shields + float(bonuses.get("max_shields", 0.0))) * (1.0 + float(bonuses.get("shield_percent", 0.0)) / 100.0) scanner_range = base_scanner_range + float(bonuses.get("scanner_range", 0.0)) max_energy = base_max_energy + float(bonuses.get("max_energy", 0.0)) - max_speed = (base_max_speed + float(bonuses.get("max_speed", 0.0))) * (1.0 + float(bonuses.get("speed_percent", 0.0)) / 100.0) + max_speed = EQUIPMENT_BALANCE.speed(base_max_speed,float(bonuses.get("max_speed",0.0)),float(bonuses.get("speed_percent",0.0))) + space_drag = maxf(16.0,max_speed*0.2) damage_multiplier = 1.0 + float(bonuses.get("damage_percent", 0.0)) / 100.0 experience_multiplier = 1.0 + float(bonuses.get("experience_percent", 0.0)) / 100.0 credits_multiplier = 1.0 + float(bonuses.get("credits_percent", 0.0)) / 100.0 @@ -89,6 +94,12 @@ func apply_equipment_bonuses(bonuses: Dictionary) -> void: critical_damage_bonus = float(bonuses.get("critical_damage_percent", 0.0)) / 100.0 energy_regeneration = base_energy_regeneration + float(bonuses.get("energy_regeneration", 0.0)) shield_regeneration = base_shield_regeneration + float(bonuses.get("shield_regeneration", 0.0)) + energy_regeneration = EQUIPMENT_BALANCE.energy_regen(energy_regeneration,max_energy) + shield_regeneration = EQUIPMENT_BALANCE.shield_regen(shield_regeneration,max_shields) + var leech_capacity := max_shields*EQUIPMENT_BALANCE.SHIELD_LEECH_FRACTION + var polarization_capacity := max_energy*EQUIPMENT_BALANCE.POLARIZATION_FRACTION + _shield_leech_budget = leech_capacity if _shield_leech_budget < 0 else minf(_shield_leech_budget,leech_capacity) + _polarization_budget = polarization_capacity if _polarization_budget < 0 else minf(_polarization_budget,polarization_capacity) reverse_max_speed = max_speed * 0.5 max_strafe_speed = max_speed * 0.5 hull = minf(hull, max_hull) @@ -169,6 +180,13 @@ func take_damage(amount: float, _critical: bool = false, damage_type: int = Dama var remaining_damage: float = hit["damage"] var shield_damage := minf(shields, remaining_damage) shields -= shield_damage + if shield_damage > 0.0: + var shield: ItemInstance = $ShipInventory.get_item("module",0) + if shield != null and "polarization" in shield.special_orbs: + var recovered := minf(max_shields*0.05,minf(shield_damage,maxf(0.0,_polarization_budget))) + recovered = minf(recovered,maxf(0.0,max_energy-energy)) + energy += recovered + _polarization_budget -= recovered remaining_damage -= shield_damage hull = maxf(0.0, hull - remaining_damage) if shield_damage > 0.0 and shields > 0.0: @@ -180,7 +198,10 @@ func take_damage(amount: float, _critical: bool = false, damage_type: int = Dama func restore_shields_from_damage(actual_damage: float) -> void: if _is_destroyed or actual_damage <= 0.0 or shield_on_damage_percent <= 0.0 or GameState.captain.is_skill_active("power_redirect"): return - shields = minf(max_shields, shields + actual_damage * shield_on_damage_percent / 100.0) + var recovered := minf(actual_damage*minf(10.0,shield_on_damage_percent)/100.0,maxf(0.0,_shield_leech_budget)) + recovered = minf(recovered,maxf(0.0,max_shields-shields)) + shields += recovered + _shield_leech_budget -= recovered _update_shield_visual() func get_passive_extension_value(effect: String) -> float: @@ -229,7 +250,8 @@ func _sync_virus_allies() -> void: ally.call("set_virus_controlled", false) func get_evasion_chance() -> float: - return GameState.captain.get_skill_bonus("tactical_maneuver") if GameState.captain.is_skill_active("tactical_maneuver") else 0.0 + var base := GameState.captain.get_skill_bonus("tactical_maneuver") if GameState.captain.is_skill_active("tactical_maneuver") else 0.0 + return 1.0-(1.0-base)*0.95 if preload("res://scripts/planets/alien_research.gd").learned("weapons_b") else base func get_energy_regeneration() -> float: return (energy_regeneration + get_solar_regeneration()) * GameState.captain.energy_regeneration_multiplier() @@ -245,7 +267,7 @@ func get_weapon_damage_multiplier() -> float: return damage_multiplier * (1.0 + GameState.captain.get_skill_bonus("gunner")) func get_critical_chance() -> float: - return clampf(critical_chance_bonus + GameState.captain.get_skill_bonus("sharp_eye"), 0.0, 1.0) + return clampf(critical_chance_bonus + GameState.captain.get_skill_bonus("sharp_eye"), 0.0, 0.60) func get_critical_multiplier() -> float: return CAPTAIN_BALANCE.BASE_CRITICAL_MULTIPLIER * (1.0 + maxf(0.0, critical_damage_bonus)) @@ -330,6 +352,7 @@ func _physics_process(delta: float) -> void: if _is_destroyed or is_docked: _mini_boost_active = false return + _refill_recovery_budgets(delta) _contact_damage_timer = maxf(0.0, _contact_damage_timer - delta) energy = minf(max_energy, energy + get_energy_regeneration() * delta) var was_mini_boosting := _mini_boost_active @@ -380,7 +403,7 @@ func is_mini_boosting() -> bool: return _mini_boost_active and not _is_destroyed and not is_docked func _get_allowed_speed() -> float: - return maxf(max_speed + _boost_speed, max_speed * 2.0 if _mini_boost_active else max_speed + _mini_boost_coast_speed) + return minf(EQUIPMENT_BALANCE.MAX_BOOST_SPEED,maxf(max_speed + _boost_speed, max_speed * 2.0 if _mini_boost_active else max_speed + _mini_boost_coast_speed)) func has_free_mini_boost() -> bool: var engine: ItemInstance = $ShipInventory.get_item("module",2) @@ -397,13 +420,25 @@ func _update_mini_boost(delta: float) -> void: return if _mini_boost_exhausted or Input.is_action_pressed("ship_brake"): return - var cost := MINI_BOOST_ENERGY_PER_SECOND * delta + var cost := get_mini_boost_energy_cost() * delta if energy < cost or energy <= 0.0: _mini_boost_exhausted = true return energy = maxf(0.0, energy - cost) _mini_boost_active = true +func get_mini_boost_energy_cost() -> float: + if has_free_mini_boost(): return 0.0 + var engine: ItemInstance = $ShipInventory.get_item("module",2) + var base_cost := maxf(MINI_BOOST_ENERGY_PER_SECOND,max_energy*0.22) + return base_cost * (0.5 if engine != null and "atomic_engine" in engine.special_orbs else 1.0) + +func _refill_recovery_budgets(delta: float) -> void: + var leech := max_shields*EQUIPMENT_BALANCE.SHIELD_LEECH_FRACTION + var polarization := max_energy*EQUIPMENT_BALANCE.POLARIZATION_FRACTION + _shield_leech_budget = minf(leech,maxf(0.0,_shield_leech_budget)+leech*delta) + _polarization_budget = minf(polarization,maxf(0.0,_polarization_budget)+polarization*delta) + func _input(event: InputEvent) -> void: if event is InputEventKey and event.is_action("ship_mini_boost"): # Tab controls the engine rather than moving focus between HUD buttons. @@ -414,7 +449,7 @@ func get_solar_regeneration() -> float: return 0.0 var distance := global_position.distance_to(charging_star.global_position) var strength := 1.0 - smoothstep(solar_charge_full_radius, solar_charge_radius, distance) - return max_energy * solar_charge_fraction_per_second * strength + return max_energy * minf(solar_charge_fraction_per_second,0.08) * strength func start_boost(speed_bonus: float, acceleration_time: float) -> void: _boost_target = max_speed * maxf(0.0, speed_bonus) diff --git a/scripts/game/weapon_controller.gd b/scripts/game/weapon_controller.gd index 9087ff6..cd0c680 100644 --- a/scripts/game/weapon_controller.gd +++ b/scripts/game/weapon_controller.gd @@ -116,7 +116,7 @@ func _physics_process(delta: float) -> void: _: if _shot_timers[slot_index] <= 0.0 and _consume_shot_resource(item): _fire_projectile(item) - _shot_timers[slot_index] = definition.fire_interval + _shot_timers[slot_index] = item.get_fire_interval() for slot_index in ShipInventory.WEAPON_SLOT_COUNT: _fire_just_pressed[slot_index] = false @@ -155,6 +155,7 @@ func _fire_projectile(item: ItemInstance) -> void: ship.get_parent().add_child(projectile) projectile.reset_physics_interpolation() projectile.set("shield_piercing", item.shield_piercing()) + projectile.set("damage_type_override", item.get_damage_type()) projectile.set("missile_homing", definition.missile_homing) WEAPON_AUDIO_SCRIPT.play(ship.get_parent(), muzzle_position, definition.weapon_kind) if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: @@ -199,10 +200,10 @@ func _fire_laser(slot_index: int, item: ItemInstance) -> void: _break_cloak_on_fire() if not result.is_empty(): var hit: Dictionary = ship.call("roll_weapon_hit", item) - _apply_damage(result.get("collider"), float(hit["damage"]), bool(hit["critical"]), item.shield_piercing()) + _apply_damage(result.get("collider"), float(hit["damage"]), bool(hit["critical"]), item.shield_piercing(), item.get_damage_type()) _spawn_laser_impact(endpoint) _laser_pulse_timers[slot_index] = definition.laser_pulse_duration - _shot_timers[slot_index] = definition.fire_interval + _shot_timers[slot_index] = item.get_fire_interval() func _break_cloak_on_fire() -> void: ship.landing_combat_lock = 6.0 @@ -232,13 +233,8 @@ func _consume_energy(amount: float) -> bool: ship.set("energy", current_energy - amount) return true -func _apply_damage(target: Variant, damage: float, critical: bool = false, piercing: float = 0.0) -> void: - if target is Node and target.has_method("take_piercing_weapon_hit"): - target.call("take_piercing_weapon_hit", damage, critical, DamageTypes.Type.ENERGY, piercing) - elif target is Node and target.has_method("take_weapon_hit"): - target.call("take_weapon_hit", damage, critical, DamageTypes.Type.ENERGY) - elif target is Node and target.has_method("take_damage"): - target.call("take_damage", damage) +func _apply_damage(target: Variant, damage: float, critical: bool = false, piercing: float = 0.0, damage_type: int = DamageTypes.Type.ENERGY) -> void: + preload("res://scripts/game/alien_weapon_effects.gd").apply_hit(target,damage,critical,damage_type,piercing,{}) func _spawn_laser_impact(at_position: Vector2) -> void: var effect := IMPACT_EFFECT_SCENE.instantiate() as Node2D diff --git a/scripts/game/weapon_projectile.gd b/scripts/game/weapon_projectile.gd index 0cd5c97..b1e5e43 100644 --- a/scripts/game/weapon_projectile.gd +++ b/scripts/game/weapon_projectile.gd @@ -29,8 +29,16 @@ var _already_hit: Dictionary = {} var _impact_handled := false var _engine_time := 0.0 var shield_piercing := 0.0 +var damage_type_override := -1 var _smoke_emission_timer := 0.0 var _smoke_puffs: Array[Dictionary] = [] +var source_ship: Node2D +var research_disabled_system := -2 +var _interception_checked := false +var _harmless_intercepted := false + +func get_damage_type() -> int: + return damage_type_override if damage_type_override >= 0 else DamageTypes.for_weapon(weapon_kind) func _ready() -> void: body_entered.connect(_on_body_entered) @@ -60,6 +68,12 @@ func configure( homing_target = target func _physics_process(delta: float) -> void: + if enemy_shot and (not is_instance_valid(source_ship) or not bool(source_ship.get("virus_controlled"))): + if preload("res://scripts/planets/alien_research.gd").learned("weapons_b_next_next_next"): collision_mask |= 4 + if weapon_kind == ItemDefinition.WeaponKind.MISSILE: + var player := get_parent().get_node_or_null("PlayerShip") as Node2D + if is_instance_valid(player) and global_position.distance_to(player.global_position) <= 240.0: + if try_intercept(player): return var travel_velocity := direction * projectile_speed if weapon_kind == ItemDefinition.WeaponKind.MISSILE and missile_homing: _flight_time += delta @@ -109,11 +123,13 @@ func _physics_process(delta: float) -> void: var step := displacement.length() var previous_position := global_position var next_position := previous_position + displacement + var exclusions: Array[RID] = [get_rid()] + if is_instance_valid(source_ship) and source_ship is CollisionObject2D: exclusions.append(source_ship.get_rid()) var query := PhysicsRayQueryParameters2D.create( previous_position, next_position, collision_mask, - [get_rid()] + exclusions ) var result := get_world_2d().direct_space_state.intersect_ray(query) if not result.is_empty(): @@ -249,6 +265,9 @@ func _on_area_entered(area: Area2D) -> void: _hit_target(area) func _hit_target(target: Node) -> void: + if target == source_ship: return + if enemy_shot and weapon_kind == ItemDefinition.WeaponKind.MISSILE and target is Node2D and target.name == "PlayerShip": + if try_intercept(target): return if _impact_handled: return _impact_handled = true @@ -274,37 +293,61 @@ func _spawn_impact_effect(is_explosion: bool) -> void: effect.call("configure", is_explosion, effect_color) func _explode_at_target(impact_target: Node) -> void: + if _harmless_intercepted: return _apply_damage_once(impact_target) var explosion_shape := CircleShape2D.new() explosion_shape.radius = 120.0 + if not enemy_shot and preload("res://scripts/planets/alien_research.gd").learned("weapons_a_next_next"): explosion_shape.radius += 10.0 var query := PhysicsShapeQueryParameters2D.new() query.shape = explosion_shape query.transform = Transform2D(0.0, global_position) # Player blasts can damage infected ships; converted fire still targets hostiles. var converted_shot := enemy_shot and (collision_mask & 4) != 0 query.collision_mask = 4 if converted_shot else (66 if enemy_shot else 68) + if enemy_shot and preload("res://scripts/planets/alien_research.gd").learned("weapons_b_next_next_next"): query.collision_mask |= 4 for result in get_world_2d().direct_space_state.intersect_shape(query, 32): _apply_damage_once(result.get("collider")) func _apply_damage_once(target: Variant) -> void: + if _harmless_intercepted: return if not (target is Node): return + if target == source_ship: return if enemy_shot and target.is_in_group("asteroids"): return var instance_id: int = target.get_instance_id() if _already_hit.has(instance_id): return + if not enemy_shot: + if research_disabled_system == -2: research_disabled_system = preload("res://scripts/game/alien_weapon_effects.gd").roll_disabled_system() + preload("res://scripts/game/alien_weapon_effects.gd").apply_hit(target,damage,is_critical,get_damage_type(),shield_piercing,_already_hit,weapon_kind != ItemDefinition.WeaponKind.MISSILE,research_disabled_system) + if weapon_kind == ItemDefinition.WeaponKind.GAMMA and get_damage_type() == DamageTypes.Type.RADIATION and not target.is_queued_for_deletion(): + preload("res://scripts/game/radiation_status.gd").apply(target,damage*0.25,true,shield_piercing) + return _already_hit[instance_id] = true if enemy_shot and target is CharacterBody2D and target.has_method("take_damage"): - target.call("take_damage", damage, is_critical, DamageTypes.for_weapon(weapon_kind)) + target.call("take_damage", damage, is_critical, get_damage_type()) elif target.has_method("take_piercing_weapon_hit"): - target.call("take_piercing_weapon_hit", damage, is_critical, DamageTypes.for_weapon(weapon_kind), shield_piercing) + target.call("take_piercing_weapon_hit", damage, is_critical, get_damage_type(), shield_piercing) elif target.has_method("take_weapon_hit"): - target.call("take_weapon_hit", damage, is_critical, DamageTypes.for_weapon(weapon_kind)) + target.call("take_weapon_hit", damage, is_critical, get_damage_type()) elif target.has_method("take_damage"): if target is CharacterBody2D: - target.call("take_damage", damage, is_critical, DamageTypes.for_weapon(weapon_kind)) + target.call("take_damage", damage, is_critical, get_damage_type()) else: target.call("take_damage", damage) - if weapon_kind == ItemDefinition.WeaponKind.GAMMA: + if weapon_kind == ItemDefinition.WeaponKind.GAMMA and get_damage_type() == DamageTypes.Type.RADIATION: preload("res://scripts/game/radiation_status.gd").apply(target, damage * 0.25, not enemy_shot, shield_piercing if not enemy_shot else 0.0) + +func try_intercept(player: Node2D, roll := -1.0) -> bool: + if _interception_checked or _impact_handled or not enemy_shot or weapon_kind != ItemDefinition.WeaponKind.MISSILE: return false + if not preload("res://scripts/planets/alien_research.gd").learned("weapons_b_next"): return false + if bool(player.get("_is_destroyed")) or bool(player.get("is_docked")): return false + if global_position.distance_to(player.global_position) > 240.0: return false + _interception_checked = true + if (randf() if roll < 0 else roll) >= 0.25: return false + _impact_handled = true + _harmless_intercepted = true + _spawn_impact_effect(true) + queue_free() + return true diff --git a/scripts/items/crafting_recipes.gd b/scripts/items/crafting_recipes.gd index 8adae3e..990c835 100644 --- a/scripts/items/crafting_recipes.gd +++ b/scripts/items/crafting_recipes.gd @@ -5,10 +5,10 @@ const NAMES := {"iron": "Żelazo", "titanium": "Tytan", "uranium": "Uran", "alum const COLORS := {"iron": Color("c5b5a0"), "titanium": Color("9ecbe9"), "uranium": Color("a9ef83"), "aluminium": Color("dce8ef"), "sulfur": Color("f4d466")} const IDS := ["parts", "ammunition", "missile", "energy_bank", "irradiated_ammunition"] # Base asteroid yield increased from mean 2 (1-3) to mean 6 (5-7). -# Triple mineral costs to preserve base crafts per asteroid; miner remains a bonus. +# Most recipes retain triple mineral costs; ordinary ammunition uses its own balance. const DATA := { "parts": {"name": "Części", "amount": 1, "cost": {"titanium": 3, "iron": 3, "aluminium": 3}}, - "ammunition": {"name": "Amunicja", "amount": 50, "cost": {"titanium": 3, "sulfur": 3}}, + "ammunition": {"name": "Amunicja", "amount": 10, "cost": {"iron": 1, "sulfur": 1}}, "missile": {"name": "Rakieta", "amount": 1, "cost": {"iron": 3, "sulfur": 3, "uranium": 3}}, "energy_bank": {"name": "Powerbank", "amount": 1, "cost": {"iron": 3, "aluminium": 3}}, "irradiated_ammunition": {"name": "Amunicja radiacyjna", "amount": 50, "cost": {"titanium": 3, "uranium": 3}}, diff --git a/scripts/items/equipment_balance.gd b/scripts/items/equipment_balance.gd new file mode 100644 index 0000000..533fd74 --- /dev/null +++ b/scripts/items/equipment_balance.gd @@ -0,0 +1,23 @@ +extends RefCounted +const MAX_SPEED := 240.0 +const MAX_BOOST_SPEED := 600.0 +const ENERGY_REGEN_FRACTION := 0.15 +const SHIELD_REGEN_FRACTION := 0.03 +const SHIELD_LEECH_FRACTION := 0.05 +const POLARIZATION_FRACTION := 0.05 +const BONUS_CAPS := {"speed_percent":60.0,"damage_percent":100.0,"critical_chance_percent":40.0,"critical_damage_percent":100.0,"shield_on_damage_percent":10.0} + +static func bonuses(raw: Dictionary) -> Dictionary: + var result := raw.duplicate() + for key in BONUS_CAPS: + result[key] = clampf(float(result.get(key,0.0)),0.0,BONUS_CAPS[key]) + return result + +static func speed(base: float, flat: float, percent: float) -> float: + return minf(MAX_SPEED,(base+flat)*(1.0+clampf(percent,0.0,60.0)/100.0)) + +static func energy_regen(value: float, maximum: float) -> float: + return minf(value,maxf(5.0,maximum*ENERGY_REGEN_FRACTION)) + +static func shield_regen(value: float, maximum: float) -> float: + return minf(value,maximum*SHIELD_REGEN_FRACTION) diff --git a/scripts/items/equipment_balance.gd.uid b/scripts/items/equipment_balance.gd.uid new file mode 100644 index 0000000..4290ea2 --- /dev/null +++ b/scripts/items/equipment_balance.gd.uid @@ -0,0 +1 @@ +uid://d0hrl5rjlb3ga diff --git a/scripts/items/expedition_pickup.gd b/scripts/items/expedition_pickup.gd index 327b5c0..2d299b2 100644 --- a/scripts/items/expedition_pickup.gd +++ b/scripts/items/expedition_pickup.gd @@ -14,7 +14,7 @@ func _ready() -> void: _label.size.x = 240 _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _label.add_theme_font_size_override("font_size",12) - _label.text = preload("res://scripts/items/special_orbs.gd").NAMES[str(payload.orb)] if payload.has("orb") else "Mapa wyprawy · poz. %d" % int(payload.map.level) + _label.text = preload("res://scripts/items/special_orbs.gd").NAMES[str(payload.orb)] if payload.has("orb") else "Mapa odległych systemów · poz. %d" % int(payload.map.level) add_child(_label) func _process(delta: float) -> void: _time += delta diff --git a/scripts/items/item_affix_definition.gd b/scripts/items/item_affix_definition.gd index 5d4bdaa..5ba40f7 100644 --- a/scripts/items/item_affix_definition.gd +++ b/scripts/items/item_affix_definition.gd @@ -20,8 +20,9 @@ class_name ItemAffixDefinition @export_range(1, 1000) var maximum_level: int = 1000 @export var minimum_value_level_1: float = 1.0 @export var maximum_value_level_1: float = 3.0 -@export var minimum_value_level_1000: float = 25.0 -@export var maximum_value_level_1000: float = 50.0 +# Caps apply to percentages and cooldown reductions; flat stats keep growing. +@export var minimum_value_cap: float = 25.0 +@export var maximum_value_cap: float = 50.0 @export_group("Zasady łączenia") # At most one affix from a nonempty group can appear on the same item. @@ -52,7 +53,10 @@ func roll_value(level: int) -> float: return randf_range(bounds.x, bounds.y) func get_value_range(level: int) -> Vector2: - var progression := float(clampi(level, 1, 1000) - 1) / 999.0 - var lower := lerpf(minimum_value_level_1, minimum_value_level_1000, progression) - var upper := lerpf(maximum_value_level_1, maximum_value_level_1000, progression) + var scale_factor := preload("res://scripts/levels/level_balance.gd").item_stat_scale(clampi(level, 1, 1000)) + var lower := minimum_value_level_1 * scale_factor + var upper := maximum_value_level_1 * scale_factor + if unit == "%" or stat_key == "missile_cooldown_reduction": + lower = minf(lower, minimum_value_cap) + upper = minf(upper, maximum_value_cap) return Vector2(minf(lower, upper), maxf(lower, upper)) diff --git a/scripts/items/item_generator.gd b/scripts/items/item_generator.gd index 8d5a3d0..86cb954 100644 --- a/scripts/items/item_generator.gd +++ b/scripts/items/item_generator.gd @@ -59,7 +59,7 @@ static func generate_for_definition(definition: ItemDefinition, level: int, enem static func roll_base_values(item: ItemInstance, level: int) -> void: var definition := item.definition item.item_level = clampi(level, 1, 1000) - var scale_factor := 1.0 + 0.05 * (item.item_level - 1) + var scale_factor := preload("res://scripts/levels/level_balance.gd").item_stat_scale(item.item_level) item.primary_value_override = definition.primary_value * scale_factor * randf_range(0.8, 1.2) if definition.primary_stat == "max_speed": item.primary_value_override = randf_range(1.0, 5.0) * scale_factor diff --git a/scripts/items/item_instance.gd b/scripts/items/item_instance.gd index f07ea0f..3979b6d 100644 --- a/scripts/items/item_instance.gd +++ b/scripts/items/item_instance.gd @@ -12,6 +12,20 @@ var _instance_ability: ActiveAbility func shield_piercing() -> float: return 0.5 if definition != null and definition.category == ItemDefinition.Category.WEAPON and "piercing" in special_orbs else 0.0 +func get_damage_type() -> int: + if "electricity" in special_orbs: return DamageTypes.Type.ENERGY + if "radiation" in special_orbs: return DamageTypes.Type.RADIATION + if "kinetic" in special_orbs: return DamageTypes.Type.KINETIC + return definition.get_damage_type() if definition != null else DamageTypes.Type.KINETIC + +func attack_speed_multiplier() -> float: + return 1.0 + clampf(get_stat_total("attack_speed_percent"), 0.0, 25.0) / 100.0 + +func get_fire_interval() -> float: + if definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE: + return maxf(1.0, maxf(1.0, 5.0 - get_stat_total("missile_cooldown_reduction")) / attack_speed_multiplier()) + return maxf(definition.fire_interval, definition.weapon_cooldown) / attack_speed_multiplier() + func is_passive_extension() -> bool: return definition != null and definition.category == ItemDefinition.Category.ACTIVE_EXTENSION and not definition.passive_effect.is_empty() @@ -169,7 +183,7 @@ func get_tooltip_text() -> String: if definition.energy_regeneration_bonus > 0.0: lines.append("Regeneracja generatora: +%.1f /s" % ((energy_regeneration_override if energy_regeneration_override >= 0.0 else definition.energy_regeneration_bonus) * ENERGY_REGENERATION_BONUS_MULTIPLIER)) if definition.category == ItemDefinition.Category.WEAPON: - lines.append("Typ obrażeń: %s" % DamageTypes.NAMES[definition.get_damage_type()]) + lines.append("Typ obrażeń: %s" % DamageTypes.NAMES[get_damage_type()]) lines.append("Każdy strzał / impuls losuje obrażenia z tego zakresu.") if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: lines.append("Zasięg: %.0f px • Amunicja: 1 / strzał" % definition.weapon_range) @@ -179,7 +193,7 @@ func get_tooltip_text() -> String: lines.append("Amunicja: 1 rakieta / strzał • Cooldown bazowy: 5 s • Bez kosztu energii") else: lines.append("Zasięg: %.0f px • Energia: %.1f / strzał" % [definition.weapon_range, get_energy_cost()]) - lines.append("Odstęp strzałów: %.2f s" % maxf(definition.fire_interval, definition.weapon_cooldown)) + lines.append("Odstęp strzałów: %.2f s" % get_fire_interval()) if not additional_modifiers.is_empty(): lines.append("\nADDONY (aktywne po zamontowaniu)") for modifier in additional_modifiers: @@ -189,7 +203,9 @@ func get_tooltip_text() -> String: for orb in special_orbs: if orb == "piercing": lines.append("\nORB PRZEBICIA · 50% obrażeń omija tarcze. Tylko ta broń.") elif orb == "booster": lines.append("\nORB DOPALACZA · TAB bez kosztu energii. Działa po zamontowaniu silnika.") + elif orb in ["electricity", "radiation", "kinetic", "atomic_engine", "polarization"]: + lines.append("\n%s · %s" % [preload("res://scripts/items/special_orbs.gd").NAMES[orb], preload("res://scripts/items/special_orbs.gd").DESCRIPTIONS[orb]]) return "\n".join(lines) static func stat_name(key: String) -> String: - return str({"luck_percent": "Szczęście", "kinetic_absorption": "Absorpcja kinetyczna", "energy_absorption": "Absorpcja energetyczna", "radiation_absorption": "Absorpcja radiacyjna", "missile_cooldown_reduction": "Skrócenie cooldownu rakiet", "scanner_range": "Zasięg skanera", "shield_on_damage_percent": "Odzysk tarcz z obrażeń", "damage": "Obrażenia", "max_hull": "Kadłub", "max_shields": "Tarcze", "max_energy": "Energia", "max_speed": "Prędkość", "energy_regeneration": "Regeneracja energii", "shield_regeneration": "Regeneracja tarcz", "hull_percent": "Kadłub", "shield_percent": "Tarcze", "damage_percent": "Obrażenia broni", "speed_percent": "Prędkość statku", "experience_percent": "Doświadczenie (EXP)", "credits_percent": "Kredyty", "critical_chance_percent": "Szansa na trafienie krytyczne", "critical_damage_percent": "Obrażenia krytyczne"}.get(key, key)) + return str({"attack_speed_percent":"Szybkostrzelność", "luck_percent": "Szczęście", "kinetic_absorption": "Absorpcja kinetyczna", "energy_absorption": "Absorpcja energetyczna", "radiation_absorption": "Absorpcja radiacyjna", "missile_cooldown_reduction": "Skrócenie cooldownu rakiet", "scanner_range": "Zasięg skanera", "shield_on_damage_percent": "Odzysk tarcz z obrażeń", "damage": "Obrażenia", "max_hull": "Kadłub", "max_shields": "Tarcze", "max_energy": "Energia", "max_speed": "Prędkość", "energy_regeneration": "Regeneracja energii", "shield_regeneration": "Regeneracja tarcz", "hull_percent": "Kadłub", "shield_percent": "Tarcze", "damage_percent": "Obrażenia broni", "speed_percent": "Prędkość statku", "experience_percent": "Doświadczenie (EXP)", "credits_percent": "Kredyty", "critical_chance_percent": "Szansa na trafienie krytyczne", "critical_damage_percent": "Obrażenia krytyczne"}.get(key, key)) diff --git a/scripts/items/ship_inventory.gd b/scripts/items/ship_inventory.gd index ba82071..cbbadcb 100644 --- a/scripts/items/ship_inventory.gd +++ b/scripts/items/ship_inventory.gd @@ -15,7 +15,7 @@ const MISSILE_DEFINITIONS := [preload("res://resources/items/missile_launcher.tr const MODULE_SLOT_COUNT := 5 const EXTENSION_SLOT_COUNT := 5 const CARGO_SLOT_COST := {"iron": 100, "aluminium": 50} -const WEAPON_UNLOCK_COSTS := [100, 500] +const WEAPON_UNLOCK_COSTS := [30, 200] const EXTENSION_UNLOCK_BASE_COST := 40 var unlocked_weapon_slots := 1 var unlocked_extension_slots := 1 @@ -338,7 +338,7 @@ func get_missile_cooldown(item: ItemInstance) -> float: for equipped in weapon_slots.slice(0,unlocked_weapon_slots) + module_slots: if equipped != null and equipped != item: reduction += equipped.get_stat_total("missile_cooldown_reduction") - return maxf(1.0, 5.0 - reduction) + return maxf(1.0, maxf(1.0, 5.0 - reduction) / item.attack_speed_multiplier()) func consume_missile() -> bool: if missile_ammunition <= 0: @@ -452,19 +452,24 @@ func _comparison_bonuses(incoming: ItemInstance = null, outgoing: ItemInstance = _add_comparison_bonuses(bonuses, item) if incoming != null: _add_comparison_bonuses(bonuses, incoming) - return bonuses + return preload("res://scripts/items/equipment_balance.gd").bonuses(bonuses) func _add_comparison_bonuses(bonuses: Dictionary, item: ItemInstance) -> void: for key in ["kinetic_absorption", "energy_absorption", "radiation_absorption", "max_hull", "max_shields", "max_energy", "max_speed", "scanner_range", "hull_percent", "shield_percent", "speed_percent", "damage_percent", "critical_damage_percent", "critical_chance_percent", "shield_on_damage_percent", "energy_regeneration", "shield_regeneration", "missile_cooldown_reduction", "experience_percent", "credits_percent", "luck_percent"]: bonuses[key] = float(bonuses.get(key, 0.0)) + item.get_stat_total(key) func _comparison_stats(bonuses: Dictionary) -> Dictionary: + bonuses = preload("res://scripts/items/equipment_balance.gd").bonuses(bonuses) var stats := {} for data in [["max_hull", "base_max_hull", "hull_percent", "Pancerz", "pkt"], ["max_shields", "base_max_shields", "shield_percent", "Tarcze", "pkt"], ["max_speed", "base_max_speed", "speed_percent", "Prędkość", "px/s"]]: var value := (float(ship.get(data[1])) + float(bonuses.get(data[0], 0.0))) * (1.0 + float(bonuses.get(data[2], 0.0)) / 100.0) stats[data[0]] = {"value": value, "name": data[3], "unit": data[4]} + stats.max_speed.value = minf(240.0,float(stats.max_speed.value)) for data in [["max_energy", "base_max_energy", "Energia", "pkt"], ["scanner_range", "base_scanner_range", "Zasięg skanera", "px"], ["energy_regeneration", "base_energy_regeneration", "Regeneracja energii", "pkt/s"], ["shield_regeneration", "base_shield_regeneration", "Regeneracja tarcz", "pkt/s"]]: stats[data[0]] = {"value": float(ship.get(data[1])) + float(bonuses.get(data[0], 0.0)), "name": data[2], "unit": data[3]} + var balance := preload("res://scripts/items/equipment_balance.gd") + stats.energy_regeneration.value = balance.energy_regen(float(stats.energy_regeneration.value),float(stats.max_energy.value)) + stats.shield_regeneration.value = balance.shield_regen(float(stats.shield_regeneration.value),float(stats.max_shields.value)) # These stats are bonuses in their own right, not rewards or hit scenarios. for data in [["kinetic_absorption", "Absorpcja kinetyczna"], ["energy_absorption", "Absorpcja energetyczna"], ["radiation_absorption", "Absorpcja radiacyjna"], ["damage_percent", "Obrażenia broni"], ["critical_damage_percent", "Obrażenia krytyczne"], ["critical_chance_percent", "Szansa na krytyk"], ["shield_on_damage_percent", "Odzysk tarcz z obrażeń"], ["experience_percent", "Doświadczenie"], ["credits_percent", "Kredyty"]]: stats[data[0]] = {"value": float(bonuses.get(data[0], 0.0)), "name": data[1], "unit": "%"} @@ -498,12 +503,13 @@ func _weapon_flat_damage_addons(item: ItemInstance) -> float: func _weapon_dps(item: ItemInstance, bonuses: Dictionary) -> float: if item == null or item.definition == null: return 0.0 + bonuses = preload("res://scripts/items/equipment_balance.gd").bonuses(bonuses) var damage_factor := (1.0 + float(bonuses.get("damage_percent", 0.0)) / 100.0) * (1.0 + GameState.captain.get_skill_bonus("gunner")) - var chance := clampf(GameState.captain.get_skill_bonus("sharp_eye") + float(bonuses.get("critical_chance_percent", 0.0)) / 100.0, 0.0, 1.0) + var chance := clampf(GameState.captain.get_skill_bonus("sharp_eye") + float(bonuses.get("critical_chance_percent", 0.0)) / 100.0, 0.0, 0.60) var critical_factor := preload("res://scripts/captain/captain_balance.gd").BASE_CRITICAL_MULTIPLIER * (1.0 + maxf(0.0, float(bonuses.get("critical_damage_percent", 0.0)) / 100.0)) - var interval := item.definition.fire_interval + var interval := item.get_fire_interval() if item.definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE: - interval = maxf(1.0, 5.0 - float(bonuses.get("missile_cooldown_reduction", 0.0))) + interval = maxf(1.0, maxf(1.0, 5.0 - float(bonuses.get("missile_cooldown_reduction", 0.0))) / item.attack_speed_multiplier()) return item.get_stat_total("damage") * damage_factor * (1.0 + chance * (critical_factor - 1.0)) / maxf(0.001, interval) func consume_irradiated_ammunition() -> bool: @@ -539,8 +545,8 @@ func get_weapon_dps(slot_index: int) -> float: if not is_slot_unlocked("weapon", slot_index): return 0.0 var item := get_item("weapon", slot_index) var value := _weapon_dps(item, _comparison_bonuses()) - if item != null and item.definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA: - value *= 1.0 + 0.25 * minf(preload("res://scripts/game/radiation_status.gd").DURATION, item.definition.fire_interval) + if item != null and item.definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA and item.get_damage_type() == DamageTypes.Type.RADIATION: + value *= 1.0 + 0.25 * minf(preload("res://scripts/game/radiation_status.gd").DURATION, item.get_fire_interval()) return value func get_total_dps() -> float: diff --git a/scripts/items/special_orbs.gd b/scripts/items/special_orbs.gd index 37cbc05..5f8498d 100644 --- a/scripts/items/special_orbs.gd +++ b/scripts/items/special_orbs.gd @@ -1,23 +1,38 @@ extends RefCounted # Every orb is consumed once. Further advancement requires another orb. -const IDS := ["piercing", "booster", "advancement"] -const NAMES := {"piercing":"Orb przebicia", "booster":"Orb dopalacza", "advancement":"Orb awansu"} -const DESCRIPTIONS := {"piercing":"50% obrażeń broni omija tarcze i trafia bezpośrednio w kadłub. Tylko broń.", "booster":"Dopalacz pod TAB nie zużywa energii i działa również przy pustym generatorze. Tylko silnik.", "advancement":"Awansuje przedmiot o 1 poziom i ponownie losuje jego parametry oraz wartości obecnych addonów. Zachowuje ich typy, liczbę i efekty orbów. Wyniki losowania mogą być niższe. Jedna sztuka daje jeden awans. Kolejny awans wymaga nowego orba (limit poziomu 1000). Nie działa na materiały zużywalne."} +const IDS := ["piercing", "booster", "advancement", "replication", "electricity", "radiation", "kinetic", "atomic_engine", "polarization"] +const COMMON_IDS := ["piercing", "booster", "advancement", "electricity", "radiation", "kinetic", "atomic_engine", "polarization"] +const DAMAGE_ORBS := ["electricity", "radiation", "kinetic"] +const NAMES := {"piercing":"Orb przebicia", "booster":"Orb dopalacza", "advancement":"Orb awansu", "replication":"Orb replikacji", "electricity":"Orb elektryczności", "radiation":"Orb radiacji", "kinetic":"Orb kinetyczny", "atomic_engine":"Orb silników atomowych", "polarization":"Orb polaryzacyjny"} +const DESCRIPTIONS := {"replication":"Tworzy dokładną, niezależną kopię wyposażenia w magazynie frachtowca. Oryginał zostaje. Kopiuje poziom, addony i efekty orbów. Drop: dodatkowe 5% z bossa dalekiej wyprawy.", "electricity":"Trwale zmienia typ obrażeń broni na energetyczny (jak laser). Nie zwiększa obrażeń ani kosztu strzału. Zastępuje poprzedni orb typu obrażeń.", "radiation":"Trwale zmienia typ obrażeń broni na radiacyjny. Nie zwiększa obrażeń ani kosztu strzału. Zastępuje poprzedni orb typu obrażeń.", "kinetic":"Trwale zmienia typ obrażeń broni na kinetyczny. Nie zwiększa obrażeń ani kosztu strzału. Zastępuje poprzedni orb typu obrażeń.", "atomic_engine":"Tylko silnik. Trwale zmniejsza zużycie energii dopalacza TAB o 50%. Nie sumuje się. Orb darmowego dopalacza ma pierwszeństwo.", "polarization":"Tylko tarcze. Trafienie w tarcze odnawia do 5% maksymalnych tarcz jako energię, nie więcej niż faktycznie stracone tarcze. Tempo odzysku: do 5% pojemności generatora/s, z rezerwą na jedno takie odnowienie. Uniki i trafienia w sam kadłub nie aktywują efektu.", "piercing":"50% obrażeń broni omija tarcze i trafia bezpośrednio w kadłub. Tylko broń.", "booster":"Dopalacz pod TAB nie zużywa energii i działa również przy pustym generatorze. Tylko silnik.", "advancement":"Awansuje przedmiot o 1 poziom i ponownie losuje jego parametry oraz wartości obecnych addonów. Zachowuje ich typy, liczbę i efekty orbów. Wyniki losowania mogą być niższe. Jedna sztuka daje jeden awans. Kolejny awans wymaga nowego orba (główna progresja do 120; 1000 to limit techniczny). Nie działa na materiały zużywalne."} static func icon(id: String) -> Texture2D: - return load("res://assets/icons/orbs/%s.png" % id) as Texture2D + return load("res://assets/icons/orbs/%s.%s" % [id, "png" if id in ["piercing", "booster", "advancement"] else "svg"]) as Texture2D static func accepts(item: ItemInstance, id: String) -> bool: if id not in IDS or item == null or item.definition == null: return false if id == "advancement": return preload("res://scripts/items/item_generator.gd").can_advance(item) + if id == "replication": return item.definition.category != ItemDefinition.Category.CONSUMABLE if id in item.special_orbs: return false + if id in DAMAGE_ORBS: + return item.definition.category == ItemDefinition.Category.WEAPON + if id == "polarization": return item.definition.category == ItemDefinition.Category.SHIELD return item.definition.category == (ItemDefinition.Category.WEAPON if id == "piercing" else ItemDefinition.Category.ENGINE) static func apply(progress: Resource, inventory: Node, item: ItemInstance, id: String) -> bool: if not accepts(item,id) or int(progress.orbs.get(id,0)) <= 0: return false if inventory == null or not bool(inventory.get_parent().get("is_docked")): return false - var owned: Array = inventory.cargo + inventory.weapon_slots + inventory.module_slots + progress.freighter_storage + var owned: Array = inventory.cargo.slice(0,inventory.unlocked_cargo_slots) + inventory.weapon_slots.slice(0,inventory.unlocked_weapon_slots) + inventory.module_slots + inventory.extension_slots.slice(0,inventory.unlocked_extension_slots) + progress.freighter_storage if not owned.has(item): return false if id == "advancement": if not preload("res://scripts/items/item_generator.gd").advance(item): return false + elif id == "replication": + var copy := item.duplicate() as ItemInstance + copy.special_orbs = item.special_orbs.duplicate() + copy.additional_modifiers = [] + for modifier in item.additional_modifiers: + copy.additional_modifiers.append(modifier.duplicate() as ItemStatModifier if modifier != null else null) + progress.freighter_storage.append(copy) else: + if id in DAMAGE_ORBS: + for previous in DAMAGE_ORBS: item.special_orbs.erase(previous) item.special_orbs.append(id) progress.orbs[id] = int(progress.orbs[id])-1 item.emit_changed() diff --git a/scripts/levels/exploration_poi.gd b/scripts/levels/exploration_poi.gd index 2be0acb..9e7bd36 100644 --- a/scripts/levels/exploration_poi.gd +++ b/scripts/levels/exploration_poi.gd @@ -92,7 +92,7 @@ static func wreck_reward_from_roll(roll: int) -> String: return "module" func reward_scale() -> float: - return 1.0 + 0.05 * (clampi(GameState.system_level, 1, 1000) - 1) + return preload("res://scripts/levels/level_balance.gd").item_stat_scale(clampi(GameState.system_level, 1, 1000)) func _update_label(near: bool) -> void: if kind == "ruin": diff --git a/scripts/levels/level_balance.gd b/scripts/levels/level_balance.gd index 66ab55e..3529abe 100644 --- a/scripts/levels/level_balance.gd +++ b/scripts/levels/level_balance.gd @@ -1,8 +1,11 @@ extends RefCounted -const NORMAL_MAX_LEVEL := 60 +const NORMAL_MAX_LEVEL := 120 +const ABSORPTION_MID_LEVEL := 60 const MAX_LEVEL := 120 -const HARD_STAT_MULTIPLIER := 2.0 +const ENEMY_GROWTH_PER_LEVEL := 0.10 +const ENDGAME_ENEMY_GROWTH_PER_LEVEL := 0.20 +const ITEM_GROWTH_PER_LEVEL := 0.07 const BOSS_HULL_MULTIPLIER := 5.0 const BOSS_SHIELD_MULTIPLIER := 3.0 const BOSS_DAMAGE_MULTIPLIER := 0.5 @@ -33,16 +36,22 @@ static func enemy_tiers(level: int) -> Array[int]: return tiers static func power_scale(level: int) -> float: - return 1.0 + 0.05 * (maxi(level, 1) - 1) + return item_stat_scale(level) + +static func item_stat_scale(level: int) -> float: + # Additive growth from level-one values, never compound interest. + return 1.0 + ITEM_GROWTH_PER_LEVEL * (maxi(level, 1) - 1) static func is_hard(level: int) -> bool: return level > NORMAL_MAX_LEVEL static func enemy_stat_scale(level: int) -> float: - return power_scale(level) * (HARD_STAT_MULTIPLIER if is_hard(level) else 1.0) + var normal_steps := mini(maxi(level, 1), MAX_LEVEL) - 1 + var endgame_steps := maxi(0, level - MAX_LEVEL) + return 1.0 + ENEMY_GROWTH_PER_LEVEL * normal_steps + ENDGAME_ENEMY_GROWTH_PER_LEVEL * endgame_steps static func enemy_absorption_total(level: int) -> float: var clamped_level := clampi(level, 1, MAX_LEVEL) - if clamped_level <= NORMAL_MAX_LEVEL: - return lerpf(2.0, 30.0, pow(float(clamped_level - 1) / float(NORMAL_MAX_LEVEL - 1), 1.25)) - return lerpf(30.0, 60.0, float(clamped_level - NORMAL_MAX_LEVEL) / float(MAX_LEVEL - NORMAL_MAX_LEVEL)) + if clamped_level <= ABSORPTION_MID_LEVEL: + return lerpf(2.0, 30.0, pow(float(clamped_level - 1) / float(ABSORPTION_MID_LEVEL - 1), 1.25)) + return lerpf(30.0, 60.0, float(clamped_level - ABSORPTION_MID_LEVEL) / float(MAX_LEVEL - ABSORPTION_MID_LEVEL)) diff --git a/scripts/planets/alien_research.gd b/scripts/planets/alien_research.gd index 125c1a7..374f952 100644 --- a/scripts/planets/alien_research.gd +++ b/scripts/planets/alien_research.gd @@ -1,15 +1,21 @@ extends RefCounted -const ACTIVE_IDS := ["general", "weapons", "hull", "shields"] +const ACTIVE_IDS := ["general", "weapons", "hull", "shields", "weapons_a", "weapons_a_next", "weapons_a_next_next", "weapons_a_next_next_next", "weapons_b", "weapons_b_next", "weapons_b_next_next", "weapons_b_next_next_next"] const BRANCHES := ["weapons", "hull", "shields"] -const TITLES := {"general":"Ogólna wiedza", "weapons":"Wiedza o broni", "hull":"Wiedza o kadłubie", "shields":"Wiedza o tarczach"} +const TITLES := {"general":"Ogólna wiedza", "weapons":"Wiedza o broni", "hull":"Wiedza o kadłubie", "shields":"Wiedza o tarczach", "weapons_a":"Słaby punkt", "weapons_a_next":"Celny strzał", "weapons_a_next_next":"Rykoszet", "weapons_a_next_next_next":"Strzał w 10", "weapons_b":"Manewr wyprzedzający", "weapons_b_next":"System obrony przed obcymi", "weapons_b_next_next":"Dezorientacja celowania", "weapons_b_next_next_next":"Zakłócenia systemów obcych"} + +static func learned(id: String) -> bool: + return id in ACTIVE_IDS and bool(GameState.captain.expedition.alien_skills.get(id,false)) + +static func path_color(id: String) -> Color: + return Color("efa653") if "_a" in id else Color("79cba8") static func parent(id: String) -> String: if id == "general": return "" - return id.split("_")[0] if "_" in id else "general" + return id.substr(0,id.rfind("_")) if "_" in id else "general" static func cost(id: String) -> int: - var depth := 2 if "_" in id else (0 if id == "general" else 1) - return 10 + depth * (depth + 3) + var depth := 0 if id == "general" else id.split("_").size() + return 10 + depth * 5 static func title(id: String) -> String: return str(TITLES.get(id, "Pusty slot")) @@ -20,6 +26,14 @@ static func description(id: String) -> String: "weapons": return "Broń obcych zadaje o 10% mniej obrażeń." "hull": return "Twoja broń zadaje o 10% więcej obrażeń kadłubom obcych." "shields": return "Twoja broń zadaje o 10% więcej obrażeń tarczom obcych." + "weapons_a": return "Każde bezpośrednie trafienie twojej broni zadaje obcym dodatkowe 5 obrażeń przed absorpcją." + "weapons_a_next": return "Każdy strzał ma 1% szansy wyłączyć na 3 sekundy losowy system obcego: silnik, broń lub tarcze. Obrażenia okresowe nie losują ponownie." + "weapons_a_next_next": return "Trafienie dowolną bronią razi też obcych do 10 px od obrysu trafionego statku. Promień eksplozji twoich rakiet rośnie o 10 px. Bez łańcuchowych rykoszetów." + "weapons_a_next_next_next": return "Każde bezpośrednie trafienie zadaje pełne obrażenia jednocześnie tarczom i kadłubowi obcego. Nadal działa absorpcja." + "weapons_b": return "Dodatkowe 5% szansy uniknięcia obrażeń od wrogich obcych. Łączy się niezależnie z pozostałymi unikami." + "weapons_b_next": return "Każda nadlatująca wroga rakieta ma 25% szansy zostać przechwycona w zasięgu 240 px od statku. Przechwycona rakieta wybucha bez zadawania obrażeń. Jeden rzut na rakietę." + "weapons_b_next_next": return "Wrogie pociski i lasery mają 25% szansy na dodatkowy szeroki rozrzut celowania." + "weapons_b_next_next_next": return "Pociski, rakiety i lasery obcych mogą trafiać także innych wrogich obcych. Trafienie własnego statku strzelca jest wykluczone." return "Miejsce na przyszłe badanie." static func learned_count(progress: Resource) -> int: diff --git a/scripts/planets/alien_research_tree.gd b/scripts/planets/alien_research_tree.gd index 88e3778..178f80e 100644 --- a/scripts/planets/alien_research_tree.gd +++ b/scripts/planets/alien_research_tree.gd @@ -2,12 +2,14 @@ extends Control const DATA := preload("res://scripts/planets/alien_research.gd") const MENU := preload("res://scripts/planets/freighter_menu.gd") const COLORS := [Color("dfb775"), Color("e68c77"), Color("79cba8"), Color("7bc9e6")] +const TILE_SIZE := Vector2(36,40) +const PLACEHOLDER_LEVELS := 4 var progress: Resource var refreshed: Callable var points: Dictionary = {} func _ready() -> void: - custom_minimum_size = Vector2(290, 294) + custom_minimum_size = Vector2(290, 300) resized.connect(_build) _build() @@ -17,17 +19,22 @@ func _build() -> void: child.queue_free() points.clear() var width := maxf(290, size.x) - _add_skill("general", Vector2(width * 0.5, 38), COLORS[0]) + _add_skill("general", Vector2(width * 0.5, 24), COLORS[0]) for i in range(3): var branch: String = DATA.BRANCHES[i] var x := width * (float(i) + 0.5) / 3.0 - _add_skill(branch, Vector2(x, 137), COLORS[i + 1]) - _add_placeholder(branch + "_a", Vector2(x - 31, 252)) - _add_placeholder(branch + "_b", Vector2(x + 31, 252)) + _add_skill(branch, Vector2(x, 80), COLORS[i + 1]) + for path in ["a","b"]: + var id: String = branch+"_"+path + for level in PLACEHOLDER_LEVELS: + var center := Vector2(x+(-22 if path=="a" else 22),142+level*46) + if id in DATA.ACTIVE_IDS: _add_skill(id,center,DATA.path_color(id)) + else: _add_placeholder(id,center) + id += "_next" var caption := MENU.label(self, ["BROŃ", "KADŁUB", "TARCZE"][i], 12, MENU.MUTED) caption.mouse_filter = Control.MOUSE_FILTER_IGNORE - caption.position = Vector2(x - 55, 177) - caption.size = Vector2(110, 18) + caption.position = Vector2(x - 45, 104) + caption.size = Vector2(90, 16) caption.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER queue_redraw() @@ -39,27 +46,28 @@ func _add_skill(id: String, center: Vector2, color: Color) -> void: var button := MENU.button(self, "", _purchase.bind(id), learned) button.set_script(preload("res://scripts/planets/research_skill_button.gd")) button.name = id - button.position = center - Vector2(28, 34) - button.size = Vector2(56, 68) + button.position = center - TILE_SIZE*0.5 + button.size = TILE_SIZE button.disabled = learned or not unlocked or progress.analysis_samples < DATA.cost(id) + if "_" in id: _style_path(button,DATA.path_color(id)) var status := "Zbadano" if learned else "Koszt: %d próbek" % DATA.cost(id) if not unlocked: status += "\nWymaga: " + DATA.title(prerequisite) elif not learned and progress.analysis_samples < DATA.cost(id): status += "\nZa mało próbek" button.tooltip_text = DATA.title(id) + "\n" + DATA.description(id) + "\n" + status var icon := TextureRect.new() - icon.texture = load("res://assets/freighter/research/" + id.split("_")[0] + ".png") + icon.texture = load("res://assets/freighter/research/" + id + ".png") icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS icon.mouse_filter = Control.MOUSE_FILTER_IGNORE - icon.position = Vector2(4, 2) - icon.size = Vector2(48, 48) + icon.position = Vector2(5, 1) + icon.size = Vector2(26, 26) if not unlocked: icon.modulate = Color(0.5, 0.55, 0.55, 0.65) button.add_child(icon) var price := MENU.label(button, "✓" if learned else str(DATA.cost(id)), 12, color if unlocked else MENU.MUTED) price.mouse_filter = Control.MOUSE_FILTER_IGNORE - price.position = Vector2(0, 48) - price.size = Vector2(56, 16) + price.position = Vector2(0, 27) + price.size = Vector2(36, 12) price.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER func _add_placeholder(id: String, center: Vector2) -> void: @@ -67,18 +75,26 @@ func _add_placeholder(id: String, center: Vector2) -> void: var slot := MENU.button(self, "", func(): pass) slot.name = id slot.disabled = true + _style_path(slot,DATA.path_color(id)) slot.focus_mode = Control.FOCUS_NONE slot.mouse_default_cursor_shape = Control.CURSOR_ARROW slot.set_script(preload("res://scripts/planets/research_skill_button.gd")) - slot.position = center - Vector2(28, 34) - slot.size = Vector2(56, 68) + slot.position = center - TILE_SIZE*0.5 + slot.size = TILE_SIZE slot.tooltip_text = "Pusty slot\nMiejsce na przyszłe badanie." var mark := MENU.label(slot, "·", 24, MENU.MUTED) mark.mouse_filter = Control.MOUSE_FILTER_IGNORE - mark.position = Vector2(0, 16) - mark.size = Vector2(56, 28) + mark.position = Vector2(0, 8) + mark.size = Vector2(36, 28) mark.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER +func _style_path(button: Button, color: Color) -> void: + for state in ["normal","hover","pressed","disabled","focus"]: + var style := button.get_theme_stylebox(state).duplicate() as StyleBoxFlat + style.border_color = color + style.set_border_width_all(1) + button.add_theme_stylebox_override(state,style) + func _purchase(id: String) -> void: if DATA.purchase(progress, id): refreshed.call() @@ -86,11 +102,10 @@ func _draw() -> void: for id in points: var prerequisite := DATA.parent(id) if prerequisite.is_empty(): continue - var start: Vector2 = points[prerequisite] + Vector2(0, 62 if "_" in id else 34) - var end: Vector2 = points[id] - Vector2(0, 34) + var start: Vector2 = points[prerequisite] + Vector2(0,TILE_SIZE.y*0.5) + var end: Vector2 = points[id] - Vector2(0,TILE_SIZE.y*0.5) var active: bool = id in DATA.ACTIVE_IDS and progress.alien_skills.get(prerequisite, false) - var color := Color("79b9a4") if active else Color("3c5158") + var color := DATA.path_color(id) if "_" in id else (Color("79b9a4") if active else Color("3c5158")) var middle := (start.y + end.y) * 0.5 - if "_" in id: middle = end.y - 12 draw_polyline(PackedVector2Array([start, Vector2(start.x, middle), Vector2(end.x, middle), end]), color, 1.5, true) diff --git a/scripts/planets/distant_expeditions.gd b/scripts/planets/distant_expeditions.gd index 39ab0b3..be437d1 100644 --- a/scripts/planets/distant_expeditions.gd +++ b/scripts/planets/distant_expeditions.gd @@ -16,6 +16,13 @@ static func offer(profile: Dictionary, source_level: int) -> Dictionary: var level := maxi(1, source_level) + roll(profile, 0x4d415053).randi_range(1, 5) return {"id":str(profile.id)+":distant_map", "level":level, "seed":roll(profile, 0x44495354).randi(), "source_level":maxi(1,source_level), "source_planet":str(profile.get("name","Nieznana planeta")), "name":"Odległy sektor %03d" % level} +static func visit_offer(progress: Resource, profile: Dictionary, source_level: int) -> Dictionary: + var visit := maxi(1, int(progress.visits.get(str(profile.id), 1))) + var visit_profile := profile.duplicate(true) + visit_profile.id = str(profile.id) + ":visit:%d" % visit + visit_profile.seed = int(profile.get("seed", 0)) ^ (visit * 7919) + return offer(visit_profile, source_level) + static func map_cost(map: Dictionary) -> int: return maxi(1, int(map.level)) * CREDITS_PER_LEVEL @@ -24,9 +31,10 @@ static func fuel_cost(map: Dictionary) -> int: static func buy(progress: Resource, captain: Resource, profile: Dictionary, source_level: int) -> bool: if not has_hermit(profile): return false - var map := offer(profile, source_level) - if progress.distant_maps.has(str(map.id)) or captain.credits < map_cost(map): return false + var map := visit_offer(progress, profile, source_level) + if progress.hermit_purchases.has(str(map.id)) or captain.credits < map_cost(map): return false captain.credits -= map_cost(map) + progress.hermit_purchases[str(map.id)] = true progress.distant_maps[str(map.id)] = map.duplicate(true) captain.emit_changed() progress.emit_changed() diff --git a/scripts/planets/distant_expeditions_panel.gd b/scripts/planets/distant_expeditions_panel.gd index dfe635d..25397bb 100644 --- a/scripts/planets/distant_expeditions_panel.gd +++ b/scripts/planets/distant_expeditions_panel.gd @@ -12,14 +12,18 @@ static func build(screen: Control) -> void: art.custom_minimum_size.y = 125 art.clip_contents = true section.add_child(art) - MENU.label(section, "DALEKIE WYPRAWY / ARCHIWUM MAP", 16, Color("e9b4fc")) - MENU.label(section, "Sektory poza znanymi szlakami. Podwójna liczba wrogów, jeden boss. Kryształy czasu z każdego wroga. Zwykli wrogowie: mapy i powerbanki po 15%. Boss zawsze zostawia dokładnie 1 mapę i 1 losowy orb.", 12) + MENU.label(section, "DALEKIE WYPRAWY / SEKTORY DO ZBADANIA", 16, Color("e9b4fc")) + MENU.label(section, "Główna progresja: poziomy 1–120. HP, tarcze i atak wrogów rosną o 10% wartości bazowej na poziom, po 120 o 20%. Bazowe parametry łupu rosną o 7% na poziom przedmiotu. Bonusy procentowe mają limity. Poziom 1000 to limit techniczny wyposażenia.", 12, MENU.MUTED) + if not GameState.active_distant_map.is_empty(): + MENU.label(section, "Trwa wyprawa do sektora %d. Użyj przycisku POWRÓT Z WYPRAWY na pokładzie, aby wrócić do układu startowego przed kolejnym lotem." % GameState.system_level, 12, MENU.GOLD) + return + MENU.label(section, "Sektory poza znanymi szlakami. Podwójna liczba wrogów, jeden boss. Kryształy czasu z każdego wroga. Zwykli wrogowie: mapy i powerbanki po 15%. Boss zawsze zostawia 1 mapę i 1 losowy orb, a dodatkowo ma 5% szansy na Orb replikacji.", 12) var fuel := MENU.label(section, "", 13, Color("95dce8")) fuel.name = "ExpeditionFuel" screen._economy_labels["expedition_fuel"] = fuel - MENU.label(section, "Lot: poziom × 1000 deuteru. Powrót bezpłatny. Mapy pozostają w archiwum i pozwalają wracać do sektora.", 12, MENU.GOLD) + MENU.label(section, "Lot: poziom × 1000 deuteru. Powrót bezpłatny. Mapa zużywa się przy wylocie. Każdy sektor można odwiedzić tylko raz.", 12, MENU.GOLD) if progress.distant_maps.is_empty(): - MENU.label(section, "Archiwum jest puste. Szukaj pustelnika na planecie lub map w łupach z wypraw.", 12, MENU.MUTED) + MENU.label(section, "Brak sektorów do zbadania. Szukaj pustelnika na planecie lub map w łupach z wypraw.", 12, MENU.MUTED) var maps: Array = progress.distant_maps.values() maps.sort_custom(func(a,b): return int(a.level) void: var map_id := str(map.id) var fly := MENU.button(row,"LEĆ",func(): screen._travel_to_distant(map_id),true) fly.name = "FlyDistant_"+map_id.md5_text().substr(0,8) - fly.tooltip_text = "Rozpocznij wyprawę. Pobiera %d deuteru." % cost + fly.tooltip_text = "Jednorazowa wyprawa. Zużywa mapę i pobiera %d deuteru." % cost var refresh_status := func(): var missing := maxf(0.0,cost-progress.deuter) status.text = "Paliwo gotowe" if missing <= 0 else "Brakuje %.1f deuteru" % missing diff --git a/scripts/planets/expedition_controller.gd b/scripts/planets/expedition_controller.gd index 67df396..c562eff 100644 --- a/scripts/planets/expedition_controller.gd +++ b/scripts/planets/expedition_controller.gd @@ -14,7 +14,6 @@ var _text: Label var _contact_label: Label var _prompt_target: Node2D var _freighter_half_size := Vector2.ZERO -var _scan_button: Button var _land_button: Button var _near_planet: Node2D var _message := "" @@ -23,6 +22,8 @@ var _previous_pause := false var _previous_docked := false var _hud_was_visible := true var _prompt_refresh_time := 0.0 +var _last_combat_blocked := false +var _needs_prompt_refresh := true func _ready() -> void: process_mode = Node.PROCESS_MODE_ALWAYS @@ -54,9 +55,8 @@ func _ready() -> void: var row := HBoxContainer.new() row.add_theme_constant_override("separation",7) column.add_child(row) - _scan_button = UI.button(row,"SKANUJ [F]",scan_nearby) _land_button = UI.button(row,"LĄDUJ [L]",land_nearby) - for button in [_scan_button,_land_button]: + for button in [_land_button]: CONTACT.compact_button(button,UI.CYAN) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL _prompt.hide() @@ -66,7 +66,7 @@ func combat_blocked() -> bool: if float(player.get("landing_combat_lock")) > 0: return true for enemy in get_tree().get_nodes_in_group("enemies"): - if enemy is Node2D and player.global_position.distance_to(enemy.global_position) < 650: + if enemy is Node2D and not enemy.is_queued_for_deletion() and float(enemy.get("hull")) > 0.0 and player.global_position.distance_to(enemy.global_position) < 650: return true return false @@ -74,7 +74,13 @@ func _process(delta: float) -> void: _message_time = maxf(0, _message_time - delta) if is_instance_valid(screen) or get_tree().paused or bool(player.get("_is_destroyed")): _prompt.hide() + _needs_prompt_refresh = true return + var blocked := combat_blocked() + if blocked != _last_combat_blocked or _needs_prompt_refresh: + _prompt_refresh_time = 0.1 + _last_combat_blocked = blocked + _needs_prompt_refresh = false if _prompt.visible: _layout_prompt() _prompt_refresh_time += delta # Proximity hints do not need to rebuild at the rendering frame rate. @@ -108,6 +114,10 @@ func _refresh_prompt() -> void: if blocked: _prompt.hide() return + if is_instance_valid(_near_planet): + var profile: Dictionary = _near_planet.planet_profile + if not GameState.captain.expedition.scans.has(profile.id): + GameState.captain.expedition.scan(profile) var text := "" if _message_time > 0: text = _message @@ -119,32 +129,38 @@ func _refresh_prompt() -> void: _prompt.tooltip_text = "Zadokuj, aby naprawić lądownik, rozbudować statek i przenieść wyposażenie." else: var profile: Dictionary = _near_planet.planet_profile - _contact_label.text = "● TELEMETRIA / "+("SKAN GOTOWY" if GameState.captain.expedition.scans.has(profile.id) else "NIEZBADANA PLANETA") - if GameState.captain.expedition.scans.has(profile.id): - text = _compact_scan(profile) - _prompt.tooltip_text = PROFILES.scan_description(profile,GameState.captain) - else: - text = "%s / EARTHLIKE\nZeskanuj warunki przed lądowaniem." % profile.name - _prompt.tooltip_text = "Skan ujawni warunki, zagrożenie i szanse powodzenia POI." - if blocked and _message_time <= 0: - text += "\nDokowanie i lądowanie zablokowane podczas walki." - var changed := _text.text != text or _scan_button.visible != (is_instance_valid(_near_planet) and not nearby_freighter) or _land_button.visible != has_destination + _contact_label.text = "● TELEMETRIA / DANE PLANETY" + text = _compact_scan(profile) + _prompt.tooltip_text = PROFILES.scan_description(profile,GameState.captain) + # A hidden card must be laid out again even if the target and text stayed the same. + var changed := not _prompt.visible or _text.text != text or _land_button.visible != has_destination if _text.text != text: _text.text = text - _scan_button.visible = is_instance_valid(_near_planet) and not nearby_freighter _land_button.visible = has_destination _land_button.text = "DOKUJ [L]" if nearby_freighter else "LĄDUJ [L]" _land_button.disabled = blocked _prompt.show() if changed: + _layout_prompt() # A standalone PanelContainer otherwise keeps its previous large height. _layout_prompt.call_deferred() func _layout_prompt() -> void: if not is_instance_valid(_prompt): return - _prompt.size = Vector2(300, _prompt.get_combined_minimum_size().y) + # Do not invalidate the wrapped report's container layout every frame. + var desired_size := Vector2(300, _prompt.get_combined_minimum_size().y) + if not _prompt.size.is_equal_approx(desired_size): + _prompt.size = desired_size var viewport_size := get_viewport().get_visible_rect().size + # Return reports belong to the screen, not to a moving planet or ship. + # Object avoidance can switch sides abruptly as the camera moves. + if _message_time > 0: + _prompt.position = Vector2( + maxf(10, (viewport_size.x - _prompt.size.x) * 0.5), + maxf(preload("res://scripts/ui/resource_bar.gd").HEIGHT + 12, viewport_size.y - _prompt.size.y - 160) + ) + return var target: Node2D = _prompt_target if is_instance_valid(_prompt_target) else player var canvas := target.get_global_transform_with_canvas() var radius := _freighter_half_size.x if target == freighter else float(target.get("radius")) if target != player else 30.0 @@ -163,28 +179,17 @@ func _compact_scan(profile: Dictionary) -> String: func _unhandled_key_input(event: InputEvent) -> void: if is_instance_valid(screen) or get_tree().paused or not event.pressed or event.echo: return - if event.keycode == KEY_F: - scan_nearby() - elif event.keycode == KEY_L: + if event.keycode == KEY_L: land_nearby() else: return get_viewport().set_input_as_handled() -func scan_nearby() -> void: - _refresh_prompt() - if not is_instance_valid(_near_planet): - return - GameState.captain.expedition.scan(_near_planet.planet_profile) - _message_time = 0 - _refresh_prompt() - func land_nearby() -> void: _refresh_prompt() if is_instance_valid(screen) or bool(player.get("_is_destroyed")): return if combat_blocked(): - _notice("Odsuń się od przeciwników i zakończ walkę.") return if CONTACT_RANGE.at_freighter(player,freighter): open_freighter() @@ -192,9 +197,6 @@ func land_nearby() -> void: if not is_instance_valid(_near_planet): return var progress: Resource = GameState.captain.expedition - if not progress.scans.has(_near_planet.planet_profile.id): - scan_nearby() - return if not progress.begin_visit(_near_planet.planet_profile): _notice("Lądownik wymaga naprawy do 100% we frachtowcu.") return @@ -243,6 +245,7 @@ func finish_surface(success: bool) -> void: _message += "\nBrak miejsca: %d modułów czeka we frachtowcu." % progress.delivered_modules.size() func _leave() -> void: + _needs_prompt_refresh = true if is_instance_valid(screen): _layer.remove_child(screen) screen.queue_free() @@ -276,7 +279,15 @@ func request_distant_travel(map_id: String) -> bool: return true func request_system_travel(level: int) -> bool: + if not GameState.active_distant_map.is_empty(): return false if not is_instance_valid(screen) or GameState.captain.expedition.active or level == GameState.system_level or not GameState.can_visit_system(level): return false if not game.travel_to_system(level): return false _leave() return true + +func request_distant_return() -> bool: + if not is_instance_valid(screen) or GameState.active_distant_map.is_empty(): return false + var sector := game.get_node_or_null("DistantSector") + if sector == null or not sector._return_home(): return false + _leave() + return true diff --git a/scripts/planets/freighter_command.gd b/scripts/planets/freighter_command.gd index f3eeb7c..208102c 100644 --- a/scripts/planets/freighter_command.gd +++ b/scripts/planets/freighter_command.gd @@ -14,13 +14,14 @@ static func build(screen: Control) -> void: destination.add_theme_font_size_override("font_size", 11) destination.add_theme_font_override("font", ThemeDB.fallback_font) for level in range(1, GameState.highest_system + 1): - destination.add_item("System %d%s" % [level, " · HARD" if level > 60 else ""], level) - destination.select(GameState.system_level - 1) + destination.add_item("System %d%s" % [level, " · HARD" if preload("res://scripts/levels/level_balance.gd").is_hard(level) else ""], level) + if GameState.active_distant_map.is_empty(): destination.select(GameState.system_level - 1) + destination.disabled = not GameState.active_distant_map.is_empty() row.add_child(destination) var travel := MENU.button(row, "LEĆ DO UKŁADU", func(): screen._travel_to_system(destination.get_selected_id()), true) travel.name = "TravelToSystem" travel.disabled = true - destination.item_selected.connect(func(_index): travel.disabled = destination.get_selected_id() == GameState.system_level) + destination.item_selected.connect(func(_index): travel.disabled = not GameState.active_distant_map.is_empty() or destination.get_selected_id() == GameState.system_level) MENU.label(navigation, "Nowe układy odblokujesz, przechodząc dalej przez tunel po pokonaniu przeciwników.", 11, MENU.MUTED) preload("res://scripts/planets/distant_expeditions_panel.gd").build(screen) var summary := MENU.card(screen._content) diff --git a/scripts/planets/freighter_screen.gd b/scripts/planets/freighter_screen.gd index ca41452..0d06a5c 100644 --- a/scripts/planets/freighter_screen.gd +++ b/scripts/planets/freighter_screen.gd @@ -67,6 +67,11 @@ func _build_header() -> void: var storage_button := MENU.button(header,"MAGAZYN",func(): _select_section("storage")) storage_button.custom_minimum_size.x = 100 storage_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER + if not GameState.active_distant_map.is_empty(): + var return_button := MENU.button(header,"POWRÓT Z WYPRAWY",_return_from_distant,true) + return_button.name = "ReturnFromDistant" + return_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER + return_button.tooltip_text = "Zakończ wyprawę i wróć do układu startowego. Powrót jest bezpłatny." var leave := MENU.button(header,"ODLOT [L]",func(): closed.emit()) leave.custom_minimum_size.x = 110 leave.size_flags_vertical = Control.SIZE_SHRINK_CENTER @@ -581,6 +586,10 @@ func _travel_to_distant(map_id: String) -> void: var controller := get_parent().get_parent() if controller.has_method("request_distant_travel"): controller.request_distant_travel(map_id) +func _return_from_distant() -> void: + var controller := get_parent().get_parent() + if controller.has_method("request_distant_return"): controller.request_distant_return() + func _assign_station(station_id: String, option: OptionButton) -> void: preload("res://scripts/planets/mining_network.gd").assign(captain.expedition, station_id, str(option.get_selected_metadata())) _refresh() @@ -615,7 +624,7 @@ func _build_alien_research() -> void: var row := MENU.row(_content) var balance := MENU.label(row, "PRÓBKI / %d" % progress.analysis_samples, 15, MENU.GOLD) balance.size_flags_horizontal = Control.SIZE_EXPAND_FILL - MENU.label(row, "%d / 4 ZBADANO" % preload("res://scripts/planets/alien_research.gd").learned_count(progress), 12, MENU.MUTED).custom_minimum_size.x = 110 + MENU.label(row, "%d / %d ZBADANO" % [preload("res://scripts/planets/alien_research.gd").learned_count(progress),preload("res://scripts/planets/alien_research.gd").ACTIVE_IDS.size()], 12, MENU.MUTED).custom_minimum_size.x = 110 var tree := preload("res://scripts/planets/alien_research_tree.gd").new() tree.progress = progress tree.refreshed = _research_completed diff --git a/scripts/planets/orb_workshop.gd b/scripts/planets/orb_workshop.gd index 6b57875..62f306d 100644 --- a/scripts/planets/orb_workshop.gd +++ b/scripts/planets/orb_workshop.gd @@ -4,7 +4,7 @@ const ORBS := preload("res://scripts/items/special_orbs.gd") static func build(screen: Control) -> void: var progress: Resource = screen.captain.expedition MENU.label(screen._content,"ORBY / SPECJALNE ULEPSZENIA",15,Color("d9b5ff")) - MENU.label(screen._content,"Każdy orb jest jednorazowy i znika po użyciu. Kolejny awans wymaga kolejnej sztuki. Efekty przebicia i dopalacza nie sumują się.",12,MENU.MUTED) + MENU.label(screen._content,"Każdy orb jest jednorazowy i znika po użyciu. Kolejny awans wymaga kolejnej sztuki. Efekty tego samego orba nie sumują się. Orby typu obrażeń zastępują się. Replika trafia do magazynu.",12,MENU.MUTED) for id in ORBS.IDS: var card := MENU.card(screen._content) var header := MENU.row(card) @@ -20,14 +20,14 @@ static func build(screen: Control) -> void: choices.add_theme_font_size_override("font_size",12) card.add_child(choices) if is_instance_valid(screen.inventory): - for kind in ["weapon","module","cargo"]: + for kind in ["weapon","module","extension","cargo"]: var items: Array = screen.inventory._get_slots(kind) for index in items.size(): if not screen.inventory.is_slot_unlocked(kind,index): continue - _add_item(choices,items[index],id,{"weapon":"Broń","module":"Moduł","cargo":"Ładownia"}[kind]) + _add_item(choices,items[index],id,{"weapon":"Broń","module":"Moduł","cargo":"Ładownia","extension":"Rozszerzenie"}[kind]) for item in progress.freighter_storage: _add_item(choices,item,id,"Magazyn fregaty") if choices.item_count == 0: choices.add_item("Brak odpowiedniego przedmiotu") - var apply := MENU.button(card,"AWANSUJ +1 POZIOM" if id == "advancement" else "NAŁÓŻ ORB",func(): + var apply := MENU.button(card,"AWANSUJ +1 POZIOM" if id == "advancement" else ("REPLIKUJ" if id == "replication" else "NAŁÓŻ ORB"),func(): var target: ItemInstance = choices.get_selected_metadata() if ORBS.apply(progress,screen.inventory,target,id): screen._audio.service_completed(-0.48) diff --git a/scripts/planets/planet_progress.gd b/scripts/planets/planet_progress.gd index d6c043c..aa42b51 100644 --- a/scripts/planets/planet_progress.gd +++ b/scripts/planets/planet_progress.gd @@ -11,6 +11,7 @@ const PROFILES := preload("res://scripts/planets/planet_profiles.gd") @export var freighter_rooms: Dictionary = {"expansion_3":"module_storage"} @export var mining_stations: Dictionary = {} @export var distant_maps: Dictionary = {} +@export var hermit_purchases: Dictionary = {} @export var orbs: Dictionary = {} @export var time_crystals: int = 0 @export var crew: Dictionary = {} diff --git a/scripts/planets/surface_deposit.gd b/scripts/planets/surface_deposit.gd index a5ba31f..45af5f4 100644 --- a/scripts/planets/surface_deposit.gd +++ b/scripts/planets/surface_deposit.gd @@ -1,18 +1,19 @@ extends Sprite2D ## Static world-space ore: no animated shimmer, screen-space noise or UV snapping. const ORE := preload("res://assets/planets/mineral_deposit.png") +const DEUTERIUM := preload("res://assets/planets/deuterium_deposit.png") const WIDTH := 128.0 func configure(map: Dictionary, poi: Dictionary) -> void: name = "MineralDeposit%d" % int(poi.id) - texture = ORE + texture = DEUTERIUM if poi.kind == "deep_deposit" else ORE texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS position = poi.position z_index = 10 var rng := RandomNumberGenerator.new() rng.seed = int(map.seed) ^ (int(poi.id)*7919) ^ 0x4f524553 rotation = rng.randf_range(-0.35,0.35) - scale = Vector2.ONE*WIDTH*rng.randf_range(0.95,1.08)/maxf(ORE.get_width(),ORE.get_height()) + scale = Vector2.ONE*WIDTH*rng.randf_range(0.95,1.08)/maxf(texture.get_width(),texture.get_height()) var blend := ShaderMaterial.new() blend.shader = preload("res://resources/effects/mineral_deposit.gdshader") var tint: Color = (map.landscape.low as Color).lerp(map.landscape.high,0.4)*0.8 diff --git a/scripts/planets/surface_screen.gd b/scripts/planets/surface_screen.gd index 9d9c7eb..cc8a21c 100644 --- a/scripts/planets/surface_screen.gd +++ b/scripts/planets/surface_screen.gd @@ -419,7 +419,10 @@ func _show_colony_poi(message: String) -> void: var network := preload("res://scripts/planets/mining_network.gd") var is_deposit: bool = _poi.kind == "deep_deposit" var art := TextureRect.new() - art.texture = load("res://assets/planets/props/deuterium_outpost.png" if is_deposit else "res://assets/planets/props/engineer_camp.png") + var art_path := "res://assets/planets/props/engineer_camp.png" + if is_deposit: + art_path = "res://assets/planets/props/deuterium_outpost.png" if progress.mining_stations.has(str(progress.active_profile.id)) else "res://assets/planets/deuterium_deposit.png" + art.texture = load(art_path) art.expand_mode = TextureRect.EXPAND_IGNORE_SIZE art.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED art.custom_minimum_size.y = 170 @@ -481,7 +484,7 @@ func _maximize_addon() -> void: func _show_hermit(message: String) -> void: var expeditions := preload("res://scripts/planets/distant_expeditions.gd") var profile: Dictionary = progress.active_profile - var map: Dictionary = expeditions.offer(profile, int(profile.get("system_level", GameState.system_level))) + var map: Dictionary = expeditions.visit_offer(progress, profile, int(profile.get("system_level", GameState.system_level))) var art := TextureRect.new() art.texture = load("res://assets/planets/props/hermit_cartographer.png") art.expand_mode = TextureRect.EXPAND_IGNORE_SIZE @@ -493,8 +496,8 @@ func _show_hermit(message: String) -> void: if not message.is_empty(): POI_CARD.text(_choices, message, 13, POI_CARD.GOLD) POI_CARD.text(_choices, "%s · poziom wyprawy %d" % [map.name, map.level], 15, POI_CARD.GOLD) POI_CARD.text(_choices, "Mapa: %d kredytów\nPaliwo na lot: %d deuteru\nPosiadasz: %d kredytów" % [expeditions.map_cost(map),expeditions.fuel_cost(map),captain.credits], 13) - if progress.distant_maps.has(str(map.id)): - POI_CARD.text(_choices, "Mapa jest już w archiwum na frachtowcu.", 13) + if progress.hermit_purchases.has(str(map.id)): + POI_CARD.text(_choices, "Mapa z tej wizyty została kupiona. Wróć po kolejnym lądowaniu po nową ofertę.", 13) else: var buy := POI_CARD.button(_choices, "KUP MAPĘ · %d KREDYTÓW" % expeditions.map_cost(map), _buy_distant_map, true) buy.name = "BuyDistantMap" @@ -506,7 +509,7 @@ func _buy_distant_map() -> void: var profile: Dictionary = progress.active_profile if preload("res://scripts/planets/distant_expeditions.gd").buy(progress,captain,profile,int(profile.get("system_level",GameState.system_level))): world.refresh_pois(lander.position) - _show_poi("Mapa zapisana w archiwum dalekich wypraw na frachtowcu.") + _show_poi("Sektor dodany do listy wypraw na frachtowcu. Mapa pozwala na jeden lot.") else: _show_poi("Mapa została już kupiona lub brakuje kredytów.") diff --git a/scripts/planets/surface_world.gd b/scripts/planets/surface_world.gd index 14de16d..e0afd8d 100644 --- a/scripts/planets/surface_world.gd +++ b/scripts/planets/surface_world.gd @@ -159,7 +159,9 @@ func refresh_pois(lander_position: Vector2) -> void: label.text = "Inżynier chronalny" + ("\n[F] Maksymalizuj addon" if lander_position.distance_to(poi.position) <= 150 else "") continue if poi.kind == "hermit": - var owned: bool = GameState.captain.expedition.distant_maps.has(str(data.profile.id)+":distant_map") + var progress: Resource = GameState.captain.expedition + var map := preload("res://scripts/planets/distant_expeditions.gd").visit_offer(progress, data.profile, int(data.profile.get("system_level", GameState.system_level))) + var owned: bool = progress.hermit_purchases.has(str(map.id)) label.text = "Pustelnik / Kartograf" + (" · mapa kupiona" if owned else ("\n[F] Kup mapę" if lander_position.distance_to(poi.position) <= 150 else "")) continue if poi.kind == "crew": diff --git a/scripts/ui/equipment_slot_view.gd b/scripts/ui/equipment_slot_view.gd index 7ef444f..f4bbe64 100644 --- a/scripts/ui/equipment_slot_view.gd +++ b/scripts/ui/equipment_slot_view.gd @@ -243,7 +243,7 @@ func _make_custom_tooltip(for_text: String) -> Object: label.push_font_size(13) color = Color("ffd166") if line.begins_with("Typ obrażeń:"): - color = [Color("efb568"), Color("91dbea"), Color("a9ef83")][item.definition.get_damage_type()] if item != null else Color("e6f2fc") + color = [Color("efb568"), Color("91dbea"), Color("a9ef83")][item.get_damage_type()] if item != null else Color("e6f2fc") if line.begins_with("Każdy strzał"): color = Color("8298ad") label.push_color(color) diff --git a/scripts/ui/resource_bar.gd b/scripts/ui/resource_bar.gd index 0f11027..dd939de 100644 --- a/scripts/ui/resource_bar.gd +++ b/scripts/ui/resource_bar.gd @@ -1,5 +1,5 @@ extends Control -const HEIGHT := 32.0 +const HEIGHT := 16.0 const ENTRIES := [ ["credits","Kredyty","svg",Color("efd48e")], ["parts","Części","png",Color("dfded2")], @@ -107,7 +107,7 @@ func _layout() -> void: if not is_instance_valid(_row): return var minimum := _row.get_combined_minimum_size() var available := maxf(1,size.x-24) - var factor := minf(1.0,available/maxf(1,minimum.x)) + var factor := minf(0.5,available/maxf(1,minimum.x)) _row.scale = Vector2.ONE*factor _row.size = Vector2(available/factor,minimum.y) _row.position = Vector2(12,(HEIGHT-minimum.y*factor)*0.5) diff --git a/scripts/ui/ship_statistics.gd b/scripts/ui/ship_statistics.gd index faa1d7b..267be8c 100644 --- a/scripts/ui/ship_statistics.gd +++ b/scripts/ui/ship_statistics.gd @@ -148,7 +148,7 @@ func _metric(card: VBoxContainer, key: String, caption: String, read_value: Call func _refresh_values() -> void: if not is_instance_valid(player): return var ship := ShipCatalog.for_path(GameState.selected_ship_path) - _subtitle.text = "%s / System %d / %s / DPS %.1f" % [ship.name, GameState.system_level, "HARD" if GameState.system_level > 60 else "NORMALNY", inventory.get_total_dps()] + _subtitle.text = "%s / System %d / %s / DPS %.1f" % [ship.name, GameState.system_level, "HARD" if preload("res://scripts/levels/level_balance.gd").is_hard(GameState.system_level) else "NORMALNY", inventory.get_total_dps()] for entry in _bindings.values(): entry.label.text = str(entry.read.call()) @@ -161,19 +161,19 @@ func _parameters() -> void: _metric(card, DamageTypes.KEYS[index], "Absorpcja: " + DamageTypes.NAMES[index].to_lower(), func(): return "%.1f%%" % player.absorption[index], DEFENSE) card = _section("02 / TARCZE I ENERGIA", ENERGY) _metric(card, "shields", "Tarcze", func(): return "%.0f / %.0f" % [player.shields,player.max_shields], CYAN) - _metric(card, "shield_regeneration", "Regeneracja tarcz", func(): return "%.1f / s" % player.shield_regeneration, CYAN) - _metric(card, "shield_on_damage", "Odzysk tarcz z obrażeń", func(): return "%.1f%%" % player.shield_on_damage_percent, CYAN) + _metric(card, "shield_regeneration", "Regeneracja tarcz", func(): return "%.1f / s" % player.shield_regeneration, CYAN, "Do 3% maksymalnych tarcz na sekundę.") + _metric(card, "shield_on_damage", "Odzysk tarcz z obrażeń", func(): return "%.1f%%" % player.shield_on_damage_percent, CYAN, "Łącznie do 10% faktycznie zadanych obrażeń. Tempo: do 5% maksymalnych tarcz/s, z rezerwą na jedno takie odnowienie.") _metric(card, "energy", "Energia", func(): return "%.0f / %.0f" % [player.energy,player.max_energy], ENERGY) - _metric(card, "energy_regeneration", "Regeneracja energii", func(): return "%.1f / s" % player.get_energy_regeneration(), ENERGY, "Aktualna regeneracja, także ładowanie przy gwieździe i premie kapitana.") + _metric(card, "energy_regeneration", "Regeneracja energii", func(): return "%.1f / s" % player.get_energy_regeneration(), ENERGY, "Sprzęt: do 15% pojemności generatora/s. Ładowanie przy gwieździe i czasowe umiejętności dodają osobne premie.") _metric(card, "solar", "Ładowanie słoneczne", func(): return "%.1f / s" % player.get_solar_regeneration(), ENERGY) card = _section("03 / SIŁA OGNIA", GOLD) _metric(card, "dps", "Łączny DPS", func(): return "%.1f" % inventory.get_total_dps(), GOLD, "Teoretyczny DPS: uwzględnia krytyki i promieniowanie; przed obroną celu, przy dostępnej energii i amunicji.") - _metric(card, "damage", "Premia do obrażeń", func(): return "%+.1f%%" % ((player.get_weapon_damage_multiplier()-1)*100), GOLD) - _metric(card, "critical", "Szansa na krytyk", func(): return "%.1f%%" % (player.get_critical_chance()*100), GOLD) - _metric(card, "critical_damage", "Mnożnik krytyka", func(): return "×%.2f" % player.get_critical_multiplier(), GOLD) + _metric(card, "damage", "Premia do obrażeń", func(): return "%+.1f%%" % ((player.get_weapon_damage_multiplier()-1)*100), GOLD, "Premia ze sprzętu: do +100%. Umiejętności kapitana działają osobno.") + _metric(card, "critical", "Szansa na krytyk", func(): return "%.1f%%" % (player.get_critical_chance()*100), GOLD, "Sprzęt: do 40%. Łącznie z umiejętnościami: do 60%.") + _metric(card, "critical_damage", "Mnożnik krytyka", func(): return "×%.2f" % player.get_critical_multiplier(), GOLD, "Sprzęt: do +100% mnożnika bazowego, maksymalnie ×4.") card = _section("04 / NAPĘD I NAWIGACJA", CYAN) _metric(card, "speed", "Prędkość aktualna", func(): return "%.0f px/s" % player.velocity.length()) - _metric(card, "max_speed", "Prędkość maksymalna", func(): return "%.0f px/s" % player.max_speed) + _metric(card, "max_speed", "Prędkość maksymalna", func(): return "%.0f px/s" % player.max_speed, CYAN, "Zwykły lot: do 240 px/s. TAB: do 480 px/s. Aktywny dopalacz: do 600 px/s.") _metric(card, "acceleration", "Przyspieszenie", func(): return "%.0f px/s²" % player.thrust_acceleration) _metric(card, "rotation", "Prędkość obrotu", func(): return "%.0f °/s" % rad_to_deg(player.max_turn_speed)) _metric(card, "scanner", "Zasięg skanera", func(): return "%.0f px" % player.scanner_range) @@ -204,10 +204,10 @@ func _weapons() -> void: _item_header(card,item) _metric(card, "weapon_dps_%d" % index, "DPS", func(): return "%.1f" % inventory.get_weapon_dps(index), GOLD) _metric(card, "weapon_damage_%d" % index, "Obrażenia trafienia", func(): return "%.1f – %.1f" % [item.get_damage_range().x*player.get_weapon_damage_multiplier(), item.get_damage_range().y*player.get_weapon_damage_multiplier()], GOLD, "Zakres przed krytykiem i obroną przeciwnika.") - _metric(card, "weapon_interval_%d" % index, "Odstęp między strzałami", func(): return "%.2f s" % (inventory.get_missile_cooldown(item) if item.definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE else item.definition.fire_interval)) + _metric(card, "weapon_interval_%d" % index, "Odstęp między strzałami", func(): return "%.2f s" % (inventory.get_missile_cooldown(item) if item.definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE else item.get_fire_interval())) _metric(card, "weapon_energy_%d" % index, "Energia na strzał", func(): return "%.1f" % item.get_energy_cost(), ENERGY) _metric(card, "weapon_range_%d" % index, "Zasięg", func(): return "%.0f px" % item.definition.weapon_range) - UI.label(card,"Typ: " + DamageTypes.NAMES[item.definition.get_damage_type()],12,UI.MUTED) + UI.label(card,"Typ: " + DamageTypes.NAMES[item.get_damage_type()],12,UI.MUTED) var info := _section("ODCZYT PARAMETRÓW", CYAN) UI.label(info,"DPS uwzględnia premie wyposażenia, kapitana, krytyki i promieniowanie. To wartość teoretyczna, przed obroną celu; rzeczywisty wynik zależy od trafień, energii i amunicji.",13,UI.MUTED) UI.label(info,"Najedź na nazwę przedmiotu, aby zobaczyć wszystkie addony i parametry.",13,UI.MUTED) diff --git a/tests/distant_expeditions_checks.gd b/tests/distant_expeditions_checks.gd index b2e3e5b..d7f876e 100644 --- a/tests/distant_expeditions_checks.gd +++ b/tests/distant_expeditions_checks.gd @@ -19,7 +19,7 @@ func run_checks() -> Dictionary: if DATA.fuel_cost(map) != int(map.level)*1000: failures.append("Wrong fuel") var captain = preload("res://scripts/captain/captain_progress.gd").new() var progress: Resource = captain.expedition - var map := DATA.offer(profile,120) + var map := DATA.visit_offer(progress,profile,120) var price := DATA.map_cost(map) captain.credits = price-1 if DATA.buy(progress,captain,profile,120): failures.append("Bought without credits") @@ -32,6 +32,13 @@ func run_checks() -> Dictionary: if DATA.buy(progress,captain,profile,120): failures.append("Duplicate purchase") if captain.credits != price: failures.append("Duplicate charged") if progress.deuter != 500000: failures.append("Purchase consumed fuel") + progress.distant_maps.clear() + if DATA.buy(progress,captain,profile,120): failures.append("Consumed map repurchased in same visit") + progress.visits[str(profile.id)] = 2 + var next_map := DATA.visit_offer(progress,profile,120) + if next_map.id == map.id or next_map.seed == map.seed: failures.append("Revisit reused sector") + captain.credits = DATA.map_cost(next_map) + if not DATA.buy(progress,captain,profile,120): failures.append("Revisit purchase failed") var clone: Resource = progress.duplicate(true) if clone.distant_maps != progress.distant_maps: failures.append("Archive not preserved") return {"failures":failures,"offers_checked":500,"beyond_120_supported":true} diff --git a/tests/distant_integration.gd b/tests/distant_integration.gd index a3ea228..a448ad6 100644 --- a/tests/distant_integration.gd +++ b/tests/distant_integration.gd @@ -19,6 +19,10 @@ func freeze_game(game: Node) -> void: game.get_node("PlayerShip").set_physics_process(false) for enemy in get_tree().get_nodes_in_group("enemies"): enemy.set_physics_process(false) func _run() -> void: + var map_checks := preload("res://tests/distant_expeditions_checks.gd").new() + var map_report: Dictionary = map_checks.run_checks() + for failure in map_report.failures: check(false, str(failure)) + map_checks.free() for level in [1,60,120,125,500]: var ordinary := preload("res://scripts/levels/level_balance.gd").enemy_tiers(level) var tiers := GENERATOR.enemy_tiers(level) @@ -75,9 +79,12 @@ func _run() -> void: game = get_tree().current_scene freeze_game(game) check(GameState.captain.expedition.deuter==0,"Exactly one fuel payment") + check(not GameState.captain.expedition.distant_maps.has("integration"),"Map consumed on departure") check(GameState.system_level==125 and GameState.highest_system==7,"Isolated progression") check(game.get_node_or_null("CelestialBodies")==null,"No celestial bodies") - check(game.get_node_or_null("PlanetExpeditions")==null,"No planet expeditions") + check(game.has_node("PlanetExpeditions") and game.has_node("Freighter"),"Freighter available in expedition") + check(game.get_node("HUD/HUDRoot").find_child("ReturnFromDistant",true,false)==null,"No return button in flight HUD") + check(not game.get_node("DistantSector")._return_home(),"Return requires docking") check(game.get_node("AncientStructures").get_child_count()==14,"Decorative structures") check(game.get_remaining_enemies()==46 and not game._boss_spawned,"Doubled encounter before boss") player = game.get_node("PlayerShip") @@ -136,12 +143,18 @@ func _run() -> void: if pickup.get_script() == preload("res://scripts/items/expedition_pickup.gd"): if pickup.payload.has("map"): maps += 1 if pickup.payload.has("orb"): orbs += 1 - check(maps==1 and orbs==1,"Actual boss death drops exactly one map and orb") + check(maps==1 and orbs>=1 and orbs<=2,"Actual boss death drops map, guaranteed orb and optional replication") check(get_tree().get_nodes_in_group("time_crystal_pickups").size()==1,"Boss also drops time crystals") game.get_node("DistantSector")._physics_process(0.3) check(game.system_cleared,"Sector cleared") player.landing_combat_lock = 0 - game.get_node("DistantSector")._return_home() + var controller := game.get_node("PlanetExpeditions") + player.is_docked = false + player.position = controller.freighter.position + controller.land_nearby() + check(is_instance_valid(controller.screen) and player.is_docked,"Normal docking during expedition") + check(controller.screen.find_child("ReturnFromDistant",true,false)!=null,"Return button inside freighter") + controller.screen._return_from_distant() await frames(8) game = get_tree().current_scene freeze_game(game) @@ -149,4 +162,6 @@ func _run() -> void: check(GameState.captain.expedition.deuter==0,"Return free") check(game.has_node("PlanetExpeditions") and game.has_node("CelestialBodies"),"Normal scene restored") check(is_instance_valid(game.get_node("PlanetExpeditions").screen) and game.get_node("PlayerShip").is_docked,"Return docks at freighter") + var combat_checks := preload("res://tests/items/combat_orbs_checks.gd").new() + for failure in combat_checks.run(game): check(false,str(failure)) print("DISTANT_INTEGRATION_RESULT: "+JSON.stringify({"failures":failures,"passed":failures.is_empty()})) diff --git a/tests/items/affix_database_test.gd b/tests/items/affix_database_test.gd index 65d7d82..294d2b3 100644 --- a/tests/items/affix_database_test.gd +++ b/tests/items/affix_database_test.gd @@ -4,7 +4,7 @@ const GENERATOR := preload("res://scripts/items/item_generator.gd") const DATABASE := preload("res://resources/items/affix_database.tres") func _ready() -> void: - assert(DATABASE.affixes.size() == 16) + assert(DATABASE.affixes.size() == 17) for level in [1, 50, 1000]: for tier in [0, 1, 2]: for sample in 50: diff --git a/tests/items/combat_orbs_checks.gd b/tests/items/combat_orbs_checks.gd new file mode 100644 index 0000000..70474a5 --- /dev/null +++ b/tests/items/combat_orbs_checks.gd @@ -0,0 +1,128 @@ +extends RefCounted +const ORBS := preload("res://scripts/items/special_orbs.gd") +const LOOT := preload("res://scripts/game/distant_loot.gd") +const GENERATOR := preload("res://scripts/items/item_generator.gd") +var failures: Array[String] = [] + +class HitTarget extends Node2D: + var damage_type := -1 + var damage := 0.0 + func take_piercing_weapon_hit(amount: float, _critical: bool, kind: int, _piercing: float) -> void: + damage_type = kind + damage = amount + +func check(value: bool, message: String) -> void: + if not value: failures.append(message) + +func run(game: Node) -> Array[String]: + var player: Node = game.player + var inventory: ShipInventory = player.get_node("ShipInventory") + var progress: Resource = GameState.captain.expedition + player.is_docked = true + for id in ORBS.IDS: + progress.orbs[id] = 10 + check(ORBS.icon(id) != null, "Missing orb icon: " + id) + var weapon := GENERATOR.generate_for_definition(preload("res://resources/items/plasma_cannon.tres"),1) + var addon := ItemStatModifier.new() + addon.stat_key = "attack_speed_percent" + addon.value = 25.0 + addon.affix = preload("res://resources/items/affixes/attack_speed_percent.tres") + weapon.additional_modifiers = [addon] + inventory.weapon_slots[0] = weapon + check(is_equal_approx(weapon.get_fire_interval(), weapon.definition.fire_interval / 1.25), "Attack speed interval") + var cost := weapon.get_energy_cost() + addon.value = 500.0 + check(is_equal_approx(weapon.attack_speed_multiplier(),1.25), "Attack speed capped") + addon.value = 25.0 + check(is_equal_approx(cost, weapon.get_energy_cost()), "Attack speed does not reduce shot cost") + for level in [1,120,1000]: + var bounds := addon.affix.get_value_range(level) + check(bounds.x >= 2.0 and bounds.y <= 25.0, "Bounded attack speed rolls") + check(not addon.affix.can_roll(preload("res://resources/items/reinforced_shield.tres"),1), "Attack speed only rolls on weapons") + var slow_dps := inventory.get_weapon_dps(0) / 1.25 + addon.value = 0.0 + check(is_equal_approx(inventory.get_weapon_dps(0),slow_dps), "DPS reflects attack speed") + addon.value = 25.0 + var shield := GENERATOR.generate_for_definition(preload("res://resources/items/reinforced_shield.tres"),1) + var engine := GENERATOR.generate_for_definition(preload("res://resources/items/engine_extension.tres"),1) + shield.additional_modifiers = [] + engine.additional_modifiers = [] + inventory.module_slots[0] = shield + inventory.module_slots[2] = engine + check(not ORBS.accepts(weapon,"polarization") and not ORBS.accepts(shield,"atomic_engine"), "Orb category restrictions") + check(not ORBS.accepts(engine,"electricity"), "Damage orb rejects engine") + var detached := GENERATOR.generate_for_definition(weapon.definition,1) + check(not ORBS.apply(progress,inventory,detached,"replication"), "Cannot replicate unowned equipment") + player.is_docked = false + check(not ORBS.apply(progress,inventory,weapon,"electricity"), "Orbs require docking") + player.is_docked = true + var original_damage := weapon.get_damage_range() + for id in ORBS.DAMAGE_ORBS: + check(ORBS.apply(progress,inventory,weapon,id), "Apply damage orb: " + id) + var kind: int = {"electricity":DamageTypes.Type.ENERGY,"radiation":DamageTypes.Type.RADIATION,"kinetic":DamageTypes.Type.KINETIC}[id] + check(weapon.get_damage_type() == kind, "Converted weapon damage type") + check(weapon.get_damage_range() == original_damage, "Conversion preserves damage") + check(not ORBS.apply(progress,inventory,weapon,id), "Same damage orb cannot stack") + var target := HitTarget.new() + game.add_child(target) + player.get_node("WeaponController")._apply_damage(target,100,false,0,weapon.get_damage_type()) + check(target.damage_type == kind, "Laser uses converted type") + var projectile := preload("res://scenes/game/weapon_projectile.tscn").instantiate() + projectile.damage = 100.0 + projectile.damage_type_override = weapon.get_damage_type() + projectile._apply_damage_once(target) + check(target.damage_type == kind and target.damage == 100.0, "Projectile uses converted type") + projectile.free() + target.free() + check(weapon.special_orbs.size() == 1, "Damage orbs replace each other") + var normal_boost_cost: float = player.get_mini_boost_energy_cost() + check(ORBS.apply(progress,inventory,engine,"atomic_engine"), "Atomic engine applied") + check(is_equal_approx(player.get_mini_boost_energy_cost(),normal_boost_cost*0.5), "Atomic engine halves TAB cost") + player.energy = 50.0 + Input.action_press("ship_mini_boost") + player._update_mini_boost(1.0) + Input.action_release("ship_mini_boost") + check(is_equal_approx(player.energy,50.0-normal_boost_cost*0.5), "Actual atomic boost consumes half energy") + check(ORBS.apply(progress,inventory,engine,"booster"), "Free booster stacks by precedence") + check(is_zero_approx(player.get_mini_boost_energy_cost()), "Free booster remains free") + check(ORBS.apply(progress,inventory,shield,"polarization"), "Polarization applied") + player.is_docked = false + player.absorption = Vector3.ZERO + player.max_energy = 10000.0 + player.energy = 0.0 + player.max_shields = 1000.0 + player.shields = 1000.0 + for attempt in 100: + player.take_damage(1.0) + if player.shields < 1000.0: break + check(is_equal_approx(player.energy,1.0), "Polarization cannot turn a tiny hit into excess energy") + player.energy = player.max_energy - 1.0 + player.shields = 1000.0 + for attempt in 100: + player.take_damage(1.0) + if player.shields < 1000.0: break + check(is_equal_approx(player.energy,player.max_energy), "Polarization respects generator capacity") + player.shields = 0.0 + player.energy = 0.0 + player.hull = 10000.0 + player.take_damage(1.0) + check(is_zero_approx(player.energy), "Hull-only hit gives no polarization energy") + player.is_docked = true + check(ORBS.apply(progress,inventory,weapon,"replication"), "Replication applied") + var copy: ItemInstance = progress.freighter_storage.back() + check(copy != weapon and copy.definition == weapon.definition, "Replica is distinct equipment") + check(copy.get_damage_range() == weapon.get_damage_range() and copy.special_orbs == weapon.special_orbs, "Replica preserves rolls and orbs") + copy.additional_modifiers[0].value = 1.0 + copy.special_orbs.clear() + check(is_equal_approx(weapon.additional_modifiers[0].value,25.0) and weapon.special_orbs.size()==1, "Replica changes cannot mutate original") + var missile := GENERATOR.generate_for_definition(preload("res://resources/items/missile_launcher.tres"),1) + missile.additional_modifiers = [addon] + check(is_equal_approx(inventory.get_missile_cooldown(missile),4.0), "Attack speed affects missiles") + addon.value = 0.0 + for seed_value in 100: + var no_rare := LOOT.roll(120,true,seed_value,1.0,0.05) + var rare := LOOT.roll(120,true,seed_value,1.0,0.049) + check(no_rare.size()==2 and no_rare[1].orb != "replication", "Replication excluded from guaranteed pool") + check(rare.size()==3 and rare[2].orb == "replication", "Separate five percent replication drop") + check(LOOT.roll(120,false,1,1.0,0.0).is_empty(), "Regular enemies cannot drop replication") + return failures diff --git a/tests/items/combat_orbs_checks.gd.uid b/tests/items/combat_orbs_checks.gd.uid new file mode 100644 index 0000000..154d3a6 --- /dev/null +++ b/tests/items/combat_orbs_checks.gd.uid @@ -0,0 +1 @@ +uid://dqj55ygq1nsrv diff --git a/tests/items/weapon_damage_test.gd b/tests/items/weapon_damage_test.gd index 81656fa..4c4f928 100644 --- a/tests/items/weapon_damage_test.gd +++ b/tests/items/weapon_damage_test.gd @@ -9,7 +9,7 @@ class LaserProbe: var hits: Array[float] = [] func _aim_direction_from(_origin: Vector2) -> Vector2: return Vector2.RIGHT - func _apply_damage(_target: Variant, damage: float, _critical: bool = false) -> void: + func _apply_damage(_target: Variant, damage: float, _critical: bool = false, _piercing: float = 0.0, _damage_type: int = DamageTypes.Type.ENERGY) -> void: hits.append(damage) func _ready() -> void: @@ -41,7 +41,7 @@ func _ready() -> void: var item := GENERATOR.generate_for_definition(definition, level) var bounds := item.get_damage_range() assert(bounds.x >= 1 and bounds.y > bounds.x) - var scale_factor: float = 1.0 + 0.05 * (level - 1) + var scale_factor: float = 1.0 + 0.07 * (level - 1) assert(bounds.x >= roundf(definition.damage_lower_roll_min * scale_factor)) assert(bounds.x <= roundf(definition.damage_lower_roll_max * scale_factor)) assert(bounds.y <= roundf((definition.damage_lower_roll_max + definition.damage_spread_roll_max) * scale_factor)) @@ -88,7 +88,7 @@ func _ready() -> void: GameState.captain.parts = 49.0 assert(not inventory.craft_ammunition()) assert(inventory.ammunition == 0 and GameState.captain.parts == 49.0) - GameState.captain.minerals["titanium"] = 1 + GameState.captain.minerals["iron"] = 1 GameState.captain.minerals["sulfur"] = 1 assert(inventory.craft_ammunition()) assert(inventory.ammunition == 10 and GameState.captain.parts == 49.0) diff --git a/tests/levels/equipment_safety_test.gd b/tests/levels/equipment_safety_test.gd new file mode 100644 index 0000000..e4762ff --- /dev/null +++ b/tests/levels/equipment_safety_test.gd @@ -0,0 +1,69 @@ +extends Node2D +const GEN := preload("res://scripts/items/item_generator.gd") +const RULES := preload("res://scripts/items/equipment_balance.gd") + +func _ready() -> void: + GameState.start_new_run() + var player := preload("res://scenes/game/player_ship.tscn").instantiate() + add_child(player) + player.set_physics_process(false) + player.get_node("WeaponController").set_physics_process(false) + var inventory: ShipInventory = player.get_node("ShipInventory") + for level in [1,20,60,120,121,1000]: + inventory.module_slots[2] = null + inventory.module_slots[3] = GEN.generate_for_definition(preload("res://resources/items/compact_generator.tres"),level) + inventory._recalculate_ship_stats() + assert(player.energy_regeneration < player.get_mini_boost_energy_cost()) + assert(player.energy_regeneration <= maxf(5.0,player.max_energy*0.15)) + var engine := GEN.generate_for_definition(preload("res://resources/items/engine_extension.tres"),level) + inventory.module_slots[2] = engine + var cost: float = player.get_mini_boost_energy_cost() + engine.special_orbs.append("atomic_engine") + assert(is_equal_approx(player.get_mini_boost_energy_cost(),cost*0.5)) + engine.special_orbs.append("booster") + assert(is_zero_approx(player.get_mini_boost_energy_cost())) + var shield := GEN.generate_for_definition(preload("res://resources/items/reinforced_shield.tres"),120) + shield.special_orbs.append("polarization") + inventory.module_slots[0] = shield + player.apply_equipment_bonuses({"max_speed":1000.0,"speed_percent":1000.0,"damage_percent":1000.0,"critical_chance_percent":1000.0,"critical_damage_percent":1000.0,"shield_on_damage_percent":1000.0,"max_energy":200.0,"max_shields":900.0,"energy_regeneration":1000.0,"shield_regeneration":1000.0}) + assert(player.max_speed == 240.0) + assert(player.damage_multiplier == 2.0) + assert(player.get_critical_chance() <= 0.6 and player.get_critical_multiplier() <= 4.0) + assert(player.energy_regeneration == 45.0 and player.shield_regeneration == 30.0) + player.start_boost(2.0,0.1) + player._boost_speed = 2000.0 + assert(player._get_allowed_speed() == 600.0) + player._boost_speed = 0.0 + player._boost_target = 0.0 + player._mini_boost_active = true + assert(player._get_allowed_speed() == 480.0) + player.shields = 0.0 + player._refill_recovery_budgets(1.0) + player.restore_shields_from_damage(1000000.0) + assert(player.shields == 50.0) + player.restore_shields_from_damage(1000000.0) + assert(player.shields == 50.0) + player._refill_recovery_budgets(0.5) + player.restore_shields_from_damage(1000000.0) + assert(player.shields == 75.0) + player.shields = player.max_shields + player.energy = 0.0 + player.absorption = Vector3.ZERO + player._refill_recovery_budgets(1.0) + for hit in 5: player.take_damage(1.0) + assert(player.energy == 5.0) + player.take_damage(100.0) + assert(player.energy == 15.0) + player.take_damage(100.0) + assert(player.energy == 15.0) + player._refill_recovery_budgets(1.0) + player.take_damage(100.0) + assert(player.energy == 30.0) + player.shields = 0.0 + player.take_damage(1.0) + assert(player.energy == 30.0) + player.is_docked = true + player._refill_recovery_budgets(1.0) + player.take_damage(10.0) + assert(player.energy == 30.0) + print("EQUIPMENT_SAFETY_TEST: PASS (levels 1-1000, boost costs/orbs, speed and offence caps, healing and polarization budgets)") diff --git a/tests/levels/equipment_safety_test.gd.uid b/tests/levels/equipment_safety_test.gd.uid new file mode 100644 index 0000000..5742f59 --- /dev/null +++ b/tests/levels/equipment_safety_test.gd.uid @@ -0,0 +1 @@ +uid://b7o7wcs6o1rtw diff --git a/tests/levels/equipment_safety_test.tscn b/tests/levels/equipment_safety_test.tscn new file mode 100644 index 0000000..281c4cd --- /dev/null +++ b/tests/levels/equipment_safety_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/levels/equipment_safety_test.gd" id="1"] +[node name="EquipmentSafetyTest" type="Node2D"] +script = ExtResource("1") diff --git a/tests/levels/hard_balance_test.gd b/tests/levels/hard_balance_test.gd index bf4a2a6..4957650 100644 --- a/tests/levels/hard_balance_test.gd +++ b/tests/levels/hard_balance_test.gd @@ -5,7 +5,7 @@ const ENEMY := preload("res://scenes/game/enemy_ship.tscn") func _ready() -> void: GameState.start_new_run() assert(BALANCE.MAX_LEVEL == 120) - assert(not BALANCE.is_hard(60) and BALANCE.is_hard(61)) + assert(not BALANCE.is_hard(60) and not BALANCE.is_hard(120) and BALANCE.is_hard(121)) var previous := 0.0 for level in range(1, 121): var total := BALANCE.enemy_absorption_total(level) @@ -22,9 +22,9 @@ func _ready() -> void: var enemy := ENEMY.instantiate() as CharacterBody2D add_child(enemy) enemy.set_physics_process(false) - for level in [1, 3, 6, 30, 60, 61, 90, 120]: - var base_power: float = 1.0 + 0.05 * (level - 1) - var mode_factor := 2.0 if level > 60 else 1.0 + for level in [1, 3, 6, 30, 60, 61, 90, 120, 121, 130, 1000]: + var base_power: float = 1.0 + 0.10 * (mini(level, 120) - 1) + 0.20 * maxi(0, level - 120) + var mode_factor := 1.0 for tier in range(3): enemy.apply_level_settings(level, tier) var combat := 2.0 if tier == 2 else 1.25 @@ -56,7 +56,7 @@ func _ready() -> void: game.player.global_position = game.protected_planet.global_position game.services_menu.show_services(level == 120) assert(GameState.system_level == level) - if level == 60: assert(game.services_menu.next_button.text == "PRZEJDŹ DO HARD") + if level == 60: assert(game.services_menu.next_button.text == "OTWÓRZ WORMHOLE") assert(game.services_menu.next_button.disabled == (level == 120)) assert(game.open_wormhole() == (level < 120)) game.free() diff --git a/tests/levels/lander_flight_test.gd b/tests/levels/lander_flight_test.gd index 74bfee5..38a79ef 100644 --- a/tests/levels/lander_flight_test.gd +++ b/tests/levels/lander_flight_test.gd @@ -51,7 +51,6 @@ func _ready() -> void: game.player.angular_velocity = 0 game.player.position = game.protected_planet.position manager._process(0) - manager.scan_nearby() assert(manager._text.text.contains("TLEN") and manager._text.text.contains("TEMPERATURA") and manager._text.text.contains("FLORA")) assert(manager._text.text.contains("Pierwszy etap POI")) manager.land_nearby() diff --git a/tests/levels/orbital_prompt_recovery_test.gd b/tests/levels/orbital_prompt_recovery_test.gd new file mode 100644 index 0000000..855a72a --- /dev/null +++ b/tests/levels/orbital_prompt_recovery_test.gd @@ -0,0 +1,68 @@ +extends Node + +func _ready() -> void: + GameState.start_new_run() + GameState.system_seed = 8428159 + var game := preload("res://scenes/game/game.tscn").instantiate() + add_child(game) + game.set_physics_process(false) + game.player.set_physics_process(false) + game.player.get_node("WeaponController").set_physics_process(false) + var manager: Node = game.get_node("PlanetExpeditions") + manager.set_process(false) + for enemy in get_tree().get_nodes_in_group("enemies"): + enemy.set_physics_process(false) + enemy.position = Vector2(-9999,-9999) + await get_tree().process_frame + for target in [manager.freighter,game.protected_planet]: + game.player.position = target.position + game.player.landing_combat_lock = 0.0 + manager._process(0.11) + assert(manager._prompt.visible) + var position: Vector2 = game.player.position + for repetition in 3: + game.player.landing_combat_lock = 0.05 + manager._process(0.016) + assert(not manager._prompt.visible) + manager.land_nearby() + assert(not is_instance_valid(manager.screen)) + assert(manager._message_time == 0.0,"Combat cannot leave a stale warning over the docking card") + game.player.landing_combat_lock = 0.0 + manager._process(0.016) + assert(manager._prompt.visible,"Timer expiration restores the card without movement") + assert(not manager._land_button.disabled) + if target == game.protected_planet: + assert(manager._text.text.contains("TLEN") and manager._text.text.contains("ŚREDNIA SZANSA POI")) + assert(GameState.captain.expedition.scans.has(target.planet_profile.id)) + assert(manager._prompt.find_children("*","Button",true,false).size()==1,"Only the land/dock button remains") + assert(game.player.position == position) + var enemy: Node2D = get_tree().get_first_node_in_group("enemies") + enemy.position = position+Vector2(100,0) + enemy.hull = 100.0 + manager._process(0.016) + assert(not manager._prompt.visible) + enemy.hull = 0.0 + manager._process(0.016) + assert(manager._prompt.visible,"Dead ships cannot keep docking blocked") + enemy.hull = 100.0 + manager._process(0.016) + assert(not manager._prompt.visible) + enemy.queue_free() + manager._process(0.016) + assert(manager._prompt.visible,"Queued enemies cannot keep docking blocked") + get_tree().paused = true + manager._process(0.016) + assert(not manager._prompt.visible) + get_tree().paused = false + manager._process(0.016) + assert(manager._prompt.visible,"Resuming restores the card without movement") + await get_tree().process_frame + game.player.position = manager.freighter.position + manager._process(0.11) + manager.land_nearby() + assert(is_instance_valid(manager.screen) and get_tree().paused) + manager._leave() + manager._process(0.016) + assert(manager._prompt.visible and not get_tree().paused) + print("ORBITAL_PROMPT_RECOVERY_TEST: PASS (stationary freighter/planet; combat lock expiry; live/dead/queued enemies; pause and docking)") + game.queue_free() diff --git a/tests/levels/orbital_prompt_recovery_test.gd.uid b/tests/levels/orbital_prompt_recovery_test.gd.uid new file mode 100644 index 0000000..15cdb28 --- /dev/null +++ b/tests/levels/orbital_prompt_recovery_test.gd.uid @@ -0,0 +1 @@ +uid://dcdwcmjn4gr8s diff --git a/tests/levels/orbital_prompt_recovery_test.tscn b/tests/levels/orbital_prompt_recovery_test.tscn new file mode 100644 index 0000000..4d1e611 --- /dev/null +++ b/tests/levels/orbital_prompt_recovery_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/levels/orbital_prompt_recovery_test.gd" id="1"] +[node name="OrbitalPromptRecoveryTest" type="Node"] +script = ExtResource("1") diff --git a/tests/levels/planet_expeditions_test.gd b/tests/levels/planet_expeditions_test.gd index 9b4d55e..e557481 100644 --- a/tests/levels/planet_expeditions_test.gd +++ b/tests/levels/planet_expeditions_test.gd @@ -116,7 +116,6 @@ func _ready() -> void: assert(not get_tree().paused and not player.is_docked) player.position = game.protected_planet.position await get_tree().process_frame - manager.scan_nearby() manager.land_nearby() assert(get_tree().paused and GameState.captain.expedition.active) assert(manager.screen.world.data.pois.size() >= 8 and manager.screen.world.data.pois.size() <= 16) diff --git a/tests/levels/planet_prompt_test.gd b/tests/levels/planet_prompt_test.gd index 7c97d4b..1a9a429 100644 --- a/tests/levels/planet_prompt_test.gd +++ b/tests/levels/planet_prompt_test.gd @@ -57,7 +57,7 @@ func _ready() -> void: assert(not manager._prompt.visible) game.player.landing_combat_lock = 0 manager._process(0) - await check_hint("EARTHLIKE") + await check_hint("TLEN") var blocker := get_tree().get_first_node_in_group("enemies") blocker.position = game.player.position+Vector2(100,0) manager._process(0) @@ -69,15 +69,14 @@ func _ready() -> void: blocker.position = Vector2(-9999,-9999) game.player.position = game.protected_planet.position manager._process(0) - await check_hint("EARTHLIKE") + await check_hint("TLEN") # Reproduce an oversized panel retained by an earlier combat warning. manager._prompt.size.y = 21054 manager._text.text = "Old oversized prompt" game.player.landing_combat_lock = 0 manager._process(0) - await check_hint("EARTHLIKE") + await check_hint("TLEN") assert(not manager._land_button.disabled) - manager.scan_nearby() manager._process(0) await check_hint("ŚREDNIA SZANSA POI") var camera: Camera2D = game.player.get_node("Camera2D") diff --git a/tests/levels/progression_scaling_test.gd b/tests/levels/progression_scaling_test.gd new file mode 100644 index 0000000..c4cb7a7 --- /dev/null +++ b/tests/levels/progression_scaling_test.gd @@ -0,0 +1,44 @@ +extends Node +const BALANCE := preload("res://scripts/levels/level_balance.gd") +const GENERATOR := preload("res://scripts/items/item_generator.gd") +const DATABASE := preload("res://resources/items/affix_database.tres") + +func _ready() -> void: + for sample in [[1,1.0,1.0],[2,1.1,1.07],[60,6.9,5.13],[61,7.0,5.2],[120,12.9,9.33],[121,13.1,9.4],[130,14.9,10.03],[1000,188.9,70.93]]: + assert(is_equal_approx(BALANCE.enemy_stat_scale(sample[0]),sample[1])) + assert(is_equal_approx(BALANCE.item_stat_scale(sample[0]),sample[2])) + for level in range(2,1001): + assert(is_equal_approx(BALANCE.enemy_stat_scale(level)-BALANCE.enemy_stat_scale(level-1),0.1 if level<=120 else 0.2)) + assert(is_equal_approx(BALANCE.item_stat_scale(level)-BALANCE.item_stat_scale(level-1),0.07)) + for affix in DATABASE.affixes: + for level in [1,2,60,120,121,500,1000]: + var bounds: Vector2 = affix.get_value_range(level) + var factor: float = 1.0 + 0.07 * (level-1) + var lower: float = affix.minimum_value_level_1 * factor + var upper: float = affix.maximum_value_level_1 * factor + if affix.unit == "%" or affix.stat_key == "missile_cooldown_reduction": + lower = minf(lower,affix.minimum_value_cap) + upper = minf(upper,affix.maximum_value_cap) + assert(bounds.is_equal_approx(Vector2(lower,upper))) + for definition in GENERATOR.DEFINITIONS: + for level in [1,60,120,121,130,1000]: + seed(8700) + var baseline := ItemInstance.new() + baseline.definition = definition + GENERATOR.roll_base_values(baseline,1) + seed(8700) + var scaled := ItemInstance.new() + scaled.definition = definition + GENERATOR.roll_base_values(scaled,level) + var factor: float = 1.0 + 0.07 * (level-1) + if definition.category == ItemDefinition.Category.WEAPON: + assert(is_equal_approx(scaled.damage_min,roundf(baseline.damage_min*factor))) + assert(is_equal_approx(scaled.damage_max,roundf(baseline.damage_max*factor))) + else: + assert(is_equal_approx(scaled.primary_value_override,baseline.primary_value_override*factor)) + assert(is_equal_approx(scaled.energy_regeneration_override,baseline.energy_regeneration_override*factor)) + var speed := preload("res://resources/items/affixes/attack_speed_percent.tres") + assert(speed.get_value_range(1).is_equal_approx(Vector2(2,3))) + assert(speed.get_value_range(120).is_equal_approx(Vector2(15,25))) + assert(speed.get_value_range(1000).is_equal_approx(Vector2(15,25))) + print("PROGRESSION_SCALING_TEST: PASS (all 1000 levels; 120/121 boundary; all equipment bases and affix ranges; attack speed caps)") diff --git a/tests/levels/progression_scaling_test.gd.uid b/tests/levels/progression_scaling_test.gd.uid new file mode 100644 index 0000000..459960b --- /dev/null +++ b/tests/levels/progression_scaling_test.gd.uid @@ -0,0 +1 @@ +uid://ch207qb0b8ccc diff --git a/tests/levels/progression_scaling_test.tscn b/tests/levels/progression_scaling_test.tscn new file mode 100644 index 0000000..434a5a7 --- /dev/null +++ b/tests/levels/progression_scaling_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/levels/progression_scaling_test.gd" id="1"] +[node name="ProgressionScalingTest" type="Node"] +script = ExtResource("1") diff --git a/tests/levels/ship_level_120_audit.gd b/tests/levels/ship_level_120_audit.gd new file mode 100644 index 0000000..afd276a --- /dev/null +++ b/tests/levels/ship_level_120_audit.gd @@ -0,0 +1,168 @@ +extends Node2D +const GEN := preload("res://scripts/items/item_generator.gd") +const DB := preload("res://resources/items/affix_database.tres") +const CATALOG := preload("res://scripts/game/ship_catalog.gd") +var player: CharacterBody2D +var inventory: ShipInventory +var report := {} + +func _ready() -> void: + GameState.start_new_run() + GameState.system_level = 120 + seed(1202026) + player = preload("res://scenes/game/player_ship.tscn").instantiate() + add_child(player) + player.set_physics_process(false) + inventory = player.get_node("ShipInventory") + player.get_node("WeaponController").set_physics_process(false) + inventory.unlocked_weapon_slots = 3 + for tier in [0,2]: + var samples: Array[Dictionary] = [] + for sample in 500: + loadout(tier) + samples.append(metrics()) + var summary := {} + for key in samples[0]: + var values: Array = samples.map(func(row): return row[key]) + values.sort() + summary[key] = {"min":values[0],"median":values[250],"p90":values[450],"max":values[499]} + summary["single_gun_sustainable_fraction"] = float(samples.filter(func(row): return row.regen >= row.single_gun_cost_s).size())/samples.size() + summary["two_guns_sustainable_fraction"] = float(samples.filter(func(row): return row.regen >= row.two_guns_cost_s).size())/samples.size() + report["random_boss" if tier==2 else "random_normal"] = summary + for focus in ["speed","energy","shield"]: + loadout(2,focus) + report[focus+"_max"] = metrics() + if focus == "energy": + report["energy_runtime"] = energy_simulation(30.0) + if focus == "speed": + report["movement_runtime"] = movement_simulation() + if focus == "shield": + report["shield_runtime"] = shield_simulation() + player.shields = 0.0 + player._refill_recovery_budgets(1.0) + player.restore_shields_from_damage(1000.0) + report["shield_leech_actual_from_1000_damage"] = player.shields + loadout(0) + report["representative"] = metrics() + report["representative_energy_runtime"] = energy_simulation(30.0) + report["representative_single_gun_runtime"] = energy_simulation(30.0,1) + var star := Node2D.new() + add_child(star) + star.position = player.position + player.charging_star = star + report["solar_regen"] = player.get_energy_regeneration() + report["solar_two_guns_runtime"] = energy_simulation(30.0) + player.charging_star = null + var shields: ItemInstance = inventory.module_slots[0] + shields.special_orbs.append("polarization") + report["polarization"] = {"per_hit":player.max_shields*0.05,"energy_per_s_at_5_hits":player.max_shields*0.05*5,"generator_capacity":player.max_energy} + player.energy = 0.0 + player.shields = player.max_shields + var before: float = player.shields + for hit in 5: player.take_damage(1.0) + report["polarization_actual_5_tiny_hits"] = {"shield_damage":before-player.shields,"energy_gained":player.energy} + var engine: ItemInstance = inventory.module_slots[2] + engine.special_orbs.append("atomic_engine") + report["atomic_engine_cost_s"] = player.get_mini_boost_energy_cost() + engine.special_orbs.append("booster") + report["booster_cost_s"] = player.get_mini_boost_energy_cost() + var enemy := preload("res://scenes/game/enemy_ship.tscn").instantiate() + add_child(enemy) + enemy.set_physics_process(false) + var enemy_metrics := [] + for tier in [0,1,2]: + enemy.apply_level_settings(120,tier) + enemy_metrics.append({"tier":tier,"hull":enemy.max_hull,"shields":enemy.max_shields,"dps":enemy.get_weapon_dps(),"speed":enemy.approach_speed}) + report["enemies"] = enemy_metrics + var laser_results: Array[Dictionary] = [] + for sample in 200: + loadout(0) + for slot in 2: + inventory.weapon_slots[slot] = GEN.generate_for_definition(preload("res://resources/items/beam_laser.tres"),120) + inventory._recalculate_ship_stats() + laser_results.append(metrics()) + var laser_costs: Array = laser_results.map(func(row): return row.two_guns_cost_s) + laser_costs.sort() + report["lasers"] = {"samples":200,"median_two_lasers_cost_s":laser_costs[100],"two_lasers_sustainable_fraction":float(laser_results.filter(func(row): return row.regen >= row.two_guns_cost_s).size())/200.0} + var file := FileAccess.open("res://artifacts/ship_level_120_audit.json",FileAccess.WRITE) + file.store_string(JSON.stringify(report,"\t")) + file.close() + print("SHIP_120_AUDIT_DONE: "+JSON.stringify(report)) + +func loadout(tier: int, focus := "") -> void: + player.base_max_speed = 108.0 + player.base_max_hull = 100.0 + player.base_max_shields = 100.0 + player.base_max_energy = 100.0 + var definitions := ["plasma_cannon","plasma_cannon","missile_launcher","reinforced_shield","hull_plating","engine_extension","compact_generator","ship_computer"] + for index in definitions.size(): + var definition := load("res://resources/items/%s.tres" % definitions[index]) as ItemDefinition + var item := GEN.generate_for_definition(definition,120,tier) + if not focus.is_empty(): + var factor := 9.33 + item.primary_value_override = (5.0 if definition.primary_stat=="max_speed" else definition.primary_value*1.2)*factor + item.energy_regeneration_override = definition.energy_regeneration_bonus*1.2*factor + if definition.category == ItemDefinition.Category.WEAPON: + item.damage_min = roundf(definition.damage_lower_roll_max*factor) + item.damage_max = roundf((definition.damage_lower_roll_max+definition.damage_spread_roll_max)*factor) + var keys: Array = {"speed":["speed_percent","damage_percent","critical_chance_percent","critical_damage_percent","attack_speed_percent"],"energy":["energy_regeneration","attack_speed_percent","damage_percent","shield_percent","shield_regeneration"],"shield":["shield_percent","shield_regeneration","shield_on_damage_percent","damage_percent","critical_chance_percent"]}[focus] + item.additional_modifiers.clear() + for key in keys: + for affix in DB.affixes: + if affix.stat_key==key and affix.can_roll(definition,120): + var addon := ItemStatModifier.new() + addon.affix = affix + addon.stat_key = key + addon.value = affix.get_value_range(120).y + item.additional_modifiers.append(addon) + if index<3: inventory.weapon_slots[index]=item + else: inventory.module_slots[index-3]=item + inventory._recalculate_ship_stats() + player.energy = player.max_energy + player.shields = player.max_shields + player.hull = player.max_hull + player.is_docked = false + +func metrics() -> Dictionary: + var first: ItemInstance = inventory.weapon_slots[0] + var second: ItemInstance = inventory.weapon_slots[1] + var cost := first.get_energy_cost()/first.get_fire_interval() + var two := cost+second.get_energy_cost()/second.get_fire_interval() + return {"speed":player.max_speed,"tab_speed":player.max_speed*2,"afterburner_speed":minf(600.0,player.max_speed*3),"max_energy":player.max_energy,"regen":player.get_energy_regeneration(),"single_gun_cost_s":cost,"two_guns_cost_s":two,"two_guns_and_tab_cost_s":two+player.get_mini_boost_energy_cost(),"shields":player.max_shields,"shield_regen":player.shield_regeneration,"shield_refill_s":player.max_shields/maxf(0.001,player.shield_regeneration),"shield_leech_percent":player.shield_on_damage_percent,"raw_dps":inventory.get_total_dps(),"shield_leech_s_at_50pct_accuracy_20pct_absorption":minf(player.max_shields*0.05,inventory.get_total_dps()*0.5*0.8*player.shield_on_damage_percent/100.0),"scanner_range":player.scanner_range,"stop_from_tab_s":player.max_speed*2/player.space_drag} + +func energy_simulation(seconds: float, guns := 2) -> Dictionary: + var timers := [0.0,0.0] + var fired := 0 + var failed := 0 + var start: float = player.max_energy + player.energy = start + var controller: Node = player.get_node("WeaponController") + for frame in int(seconds*60): + player.energy = minf(player.max_energy,player.energy+player.get_energy_regeneration()/60.0) + for index in guns: + timers[index] -= 1.0/60.0 + if timers[index]<=0: + var item: ItemInstance = inventory.weapon_slots[index] + if controller._consume_shot_resource(item): fired+=1 + else: failed+=1 + timers[index] += item.get_fire_interval() + return {"duration":seconds,"energy_start":start,"energy_end":player.energy,"shots":fired,"failed_shots":failed} + +func movement_simulation() -> Dictionary: + player.velocity = Vector2.ZERO + for frame in 1200: + preload("res://scripts/game/flight_movement.gd").apply(player,1.0/60.0,0,true,false,0,true,false,player.max_speed*2) + var peak: float = player.velocity.length() + var seconds := 0.0 + while player.velocity.length()>1 and seconds<120: + preload("res://scripts/game/flight_movement.gd").apply(player,1.0/60.0,0,false,false,0,false,false,player.max_speed*2) + seconds+=1.0/60.0 + return {"tab_speed_after_20s":peak,"coast_to_stop_seconds":seconds,"thrust_acceleration":player.thrust_acceleration,"space_drag":player.space_drag} + +func shield_simulation() -> Dictionary: + player.shields = 0.0 + var seconds := 0.0 + while player.shields void: for enemy in get_tree().get_nodes_in_group("enemies"): enemy.position = Vector2(9000,9000) game.player.position = game.protected_planet.position - manager.scan_nearby() manager.land_nearby() var surface: Control = manager.screen var audio: AudioStreamPlayer = surface.get_node("EngineAudio") diff --git a/tests/previews/lander_render_test.gd b/tests/previews/lander_render_test.gd index 755df28..2126249 100644 --- a/tests/previews/lander_render_test.gd +++ b/tests/previews/lander_render_test.gd @@ -9,7 +9,6 @@ func _ready() -> void: enemy.position = Vector2(9000,9000) game.player.position = game.protected_planet.position game.player.landing_combat_lock = 0 - manager.scan_nearby() manager.land_nearby() var surface: Control = manager.screen assert(surface.lander.physics_interpolation_mode == Node.PHYSICS_INTERPOLATION_MODE_ON) diff --git a/tests/previews/planet_preview.gd b/tests/previews/planet_preview.gd index aea360a..634e659 100644 --- a/tests/previews/planet_preview.gd +++ b/tests/previews/planet_preview.gd @@ -25,7 +25,6 @@ func _ready() -> void: await get_tree().process_frame manager._process(0) game.player.landing_combat_lock = 0 - manager.scan_nearby() manager.land_nearby() await get_tree().process_frame await RenderingServer.frame_post_draw diff --git a/tests/previews/planet_threats_preview.gd b/tests/previews/planet_threats_preview.gd index fd77bfb..82738dc 100644 --- a/tests/previews/planet_threats_preview.gd +++ b/tests/previews/planet_threats_preview.gd @@ -41,7 +41,6 @@ func _ready() -> void: for gear in GameState.captain.gear_items: gear.level = scenario.rank GameState.captain.emit_changed() - manager.scan_nearby() await capture(scenario.name) assert(manager._text.text.contains("ŚREDNIA SZANSA POI")) assert(get_viewport().get_visible_rect().encloses(manager._prompt.get_global_rect())) diff --git a/tests/previews/surface_scenery_preview.gd b/tests/previews/surface_scenery_preview.gd index d5059c1..658337b 100644 --- a/tests/previews/surface_scenery_preview.gd +++ b/tests/previews/surface_scenery_preview.gd @@ -33,7 +33,6 @@ func _ready() -> void: planet.planet_profile.seed = found_seed ^ 7919 game.player.position = planet.position game.player.landing_combat_lock = 0 - manager.scan_nearby() manager.land_nearby() var surface: Control = manager.screen assert(surface.camera.zoom.is_equal_approx(Vector2.ONE*0.8*surface.SURFACE_RENDER_SCALE)) diff --git a/tests/ships/alien_research_test.gd b/tests/ships/alien_research_test.gd index cb21806..0fde7d8 100644 --- a/tests/ships/alien_research_test.gd +++ b/tests/ships/alien_research_test.gd @@ -9,9 +9,9 @@ func _ready() -> void: assert(progress.analysis_samples == 90) assert(not DATA.purchase(progress, "general")) assert(DATA.purchase(progress, "hull")) - assert(progress.analysis_samples == 76) + assert(progress.analysis_samples == 75) assert(not DATA.purchase(progress, "hull_a")) - assert(progress.analysis_samples == 76) + assert(progress.analysis_samples == 75) assert(is_equal_approx(DATA.bonus(progress, "hull"), 0.1)) progress.alien_skills["hull_a"] = true assert(is_equal_approx(DATA.bonus(progress, "hull"), 0.1)) @@ -48,11 +48,14 @@ func _ready() -> void: for frame in 8: await get_tree().process_frame assert(screen._drawer.size.x == 490 and screen._drawer.size.y == 430) var skill_buttons: Array[Node] = screen._content.find_children("*", "Button", true, false) - assert(skill_buttons.size() == 10) + assert(skill_buttons.size() == 28) + assert(DATA.parent("weapons_a_next_next") == "weapons_a_next") + assert(not DATA.purchase(progress,"weapons_a_next_next_next")) for button in skill_buttons: - assert(is_equal_approx(button.size.x, 56) and button.tooltip_text.contains("\n")) + assert(is_equal_approx(button.size.x, 36) and button.tooltip_text.contains("\n")) + assert(button.get_parent().get_global_rect().encloses(button.get_global_rect())) assert(button.get_child_count() <= 3) - if "_" in str(button.name): + if "_" in str(button.name) and str(button.name) not in DATA.ACTIVE_IDS: assert(button.disabled and button.get_child_count() == 1) var tooltip: Object = skill_buttons[0]._make_custom_tooltip(skill_buttons[0].tooltip_text) assert(tooltip is PanelContainer) diff --git a/tests/ships/alien_weapon_technologies_test.gd b/tests/ships/alien_weapon_technologies_test.gd new file mode 100644 index 0000000..957f01c --- /dev/null +++ b/tests/ships/alien_weapon_technologies_test.gd @@ -0,0 +1,158 @@ +extends Node2D +const DATA := preload("res://scripts/planets/alien_research.gd") +const EFFECTS := preload("res://scripts/game/alien_weapon_effects.gd") +const ENEMY := preload("res://scenes/game/enemy_ship.tscn") +const PROJECTILE := preload("res://scenes/game/weapon_projectile.tscn") +var player: CharacterBody2D +var progress: Resource +var _enemy_index := 0 + +func enemy_at(position_value: Vector2) -> CharacterBody2D: + var enemy := ENEMY.instantiate() as CharacterBody2D + add_child(enemy) + enemy.set_physics_process(false) + enemy._player = player + enemy.position = position_value + enemy.absorption = Vector3.ZERO + enemy.hull = 1000.0 + enemy.max_hull = 1000.0 + enemy.shields = 100.0 + enemy.max_shields = 100.0 + _enemy_index += 1 + return enemy + +func reset_enemy(enemy: Node, shields := 100.0) -> void: + enemy.hull = 1000.0 + enemy.shields = shields + enemy._disabled_systems = [0.0,0.0,0.0] + +func _ready() -> void: + GameState.start_new_run() + progress = GameState.captain.expedition + progress.analysis_samples = 1000 + assert(not DATA.purchase(progress,"weapons_a")) + assert(DATA.purchase(progress,"general") and DATA.cost("general")==10) + assert(DATA.purchase(progress,"weapons") and DATA.cost("weapons")==15) + for path in ["a","b"]: + var id: String = "weapons_"+path + for level in 4: + assert(DATA.cost(id)==20+level*5) + var before: int = progress.analysis_samples + assert(DATA.purchase(progress,id)) + assert(progress.analysis_samples==before-DATA.cost(id)) + assert(not DATA.purchase(progress,id)) + id += "_next" + assert(DATA.learned_count(progress)==10) + assert(not DATA.purchase(progress,"hull_a")) + progress.alien_skills = {"weapons_a":true} + player = preload("res://scenes/game/player_ship.tscn").instantiate() + add_child(player) + player.set_physics_process(false) + var weapons: Node = player.get_node("WeaponController") + weapons.set_physics_process(false) + var target := enemy_at(Vector2(1000,0)) + target.shields = 0.0 + weapons._apply_damage(target,10.0,false,0.0,DamageTypes.Type.ENERGY) + assert(target.hull==985.0,"Weak point applies to lasers") + reset_enemy(target,0.0) + var shot := PROJECTILE.instantiate() + shot.damage = 10.0 + shot._apply_damage_once(target) + shot._apply_damage_once(target) + assert(target.hull==985.0,"Projectiles get weak point once per impact") + shot.free() + progress.alien_skills.weapons_a_next = true + for system in 3: + assert(EFFECTS.roll_disabled_system(0.0099,system)==system) + assert(EFFECTS.roll_disabled_system(0.01,0)==-1) + reset_enemy(target) + target.take_research_weapon_hit(10,false,DamageTypes.Type.ENERGY,0,2) + assert(target.system_disabled(2) and target.shields==0.0) + target.regenerate_defenses(1.0) + assert(target.shields==0.0) + target.tick_disabled_systems(3.1) + assert(not target.system_disabled(2) and target.shields==85.0) + target.disable_system(1) + var count := get_child_count() + target._fire_plasma(Vector2.RIGHT) + assert(get_child_count()==count,"Disabled weapons cannot fire") + target.disable_system(0) + target.velocity = Vector2.ZERO + target._activated = true + target._physics_process(0.1) + assert(target.velocity.is_zero_approx(),"Disabled engines cannot produce thrust") + target.tick_disabled_systems(3.1) + assert(not target.system_disabled(0) and not target.system_disabled(1)) + reset_enemy(target,0.0) + target.take_research_dot(10,DamageTypes.Type.RADIATION,0) + assert(target.hull==990.0 and not target.system_disabled(0) and not target.system_disabled(1) and not target.system_disabled(2),"DoT does not reroll shot technologies") + progress.alien_skills = {"weapons_a":true,"weapons_a_next_next":true} + var first := enemy_at(Vector2.ZERO) + var second := enemy_at(Vector2(60,0)) + var third := enemy_at(Vector2(120,0)) + first.shields = 0.0 + second.shields = 0.0 + third.shields = 0.0 + weapons._apply_damage(first,10.0) + assert(first.hull==985.0 and second.hull==985.0 and third.hull==1000.0,"Splash extends 10px beyond hulls without chaining") + reset_enemy(first,0.0) + reset_enemy(second,0.0) + shot = PROJECTILE.instantiate() + shot.damage = 10.0 + shot._apply_damage_once(first) + shot._apply_damage_once(second) + assert(first.hull==985.0 and second.hull==985.0,"Area hits cannot deal duplicate damage") + shot.free() + progress.alien_skills = {"weapons_a":true,"weapons_a_next_next_next":true} + reset_enemy(target) + target.take_research_weapon_hit(10,false,DamageTypes.Type.ENERGY,0.5,-1) + assert(target.hull==985.0 and target.shields==85.0,"Perfect shot deals full damage to both layers without double piercing") + reset_enemy(target) + target.absorption = Vector3(0,50,0) + target.take_research_weapon_hit(10,false,DamageTypes.Type.ENERGY,0,-1) + assert(target.hull==992.5 and target.shields==92.5,"Absorption still applies") + progress.alien_skills = {"weapons_b":true} + assert(is_equal_approx(player.get_evasion_chance(),0.05)) + progress.alien_skills = {"weapons_b_next":true} + player.position = Vector2(500,500) + shot = PROJECTILE.instantiate() + shot.enemy_shot = true + shot.weapon_kind = ItemDefinition.WeaponKind.MISSILE + shot.damage = 100.0 + add_child(shot) + shot.position = player.position + shot.set_physics_process(false) + var shields: float = player.shields + var hull: float = player.hull + assert(shot.try_intercept(player,0.24999)) + shot._hit_target(player) + shot._explode_at_target(player) + shot._apply_damage_once(player) + assert(player.shields==shields and player.hull==hull,"Intercepted missile cannot hurt player or splash targets") + var failed := PROJECTILE.instantiate() + failed.enemy_shot = true + failed.weapon_kind = ItemDefinition.WeaponKind.MISSILE + add_child(failed) + failed.set_physics_process(false) + failed.position = player.position + assert(not failed.try_intercept(player,0.25)) + assert(not failed.try_intercept(player,0.0),"One interception roll per missile") + failed.free() + progress.alien_skills = {"weapons_b_next_next":true} + assert(not target.research_aim_direction(Vector2.RIGHT,0.249,1).is_equal_approx(Vector2.RIGHT)) + assert(target.research_aim_direction(Vector2.RIGHT,0.25,1).is_equal_approx(Vector2.RIGHT)) + progress.alien_skills = {"weapons_b_next_next_next":true} + target.absorption = Vector3.ZERO + target.weapon_kind = ItemDefinition.WeaponKind.PLASMA + target._fire_plasma(Vector2.RIGHT) + var friendly: Node + for child in get_children(): + if child.get_script()==preload("res://scripts/game/weapon_projectile.gd") and child.source_ship==target: friendly=child + assert(friendly!=null and (friendly.collision_mask&4)!=0) + reset_enemy(second,0.0) + var source_hull: float = target.hull + friendly._apply_damage_once(target) + assert(target.hull==source_hull,"Friendly fire excludes the shooter") + friendly._apply_damage_once(second) + assert(second.hull<1000.0,"Hostile projectiles can damage other aliens") + print("ALIEN_WEAPON_TECHNOLOGIES_TEST: PASS (costs, prerequisites, all 8 effects, roll boundaries, no DoT rerolls, no splash chains, harmless interception, friendly fire)") diff --git a/tests/ships/alien_weapon_technologies_test.gd.uid b/tests/ships/alien_weapon_technologies_test.gd.uid new file mode 100644 index 0000000..7183243 --- /dev/null +++ b/tests/ships/alien_weapon_technologies_test.gd.uid @@ -0,0 +1 @@ +uid://hsafkvux2k6o diff --git a/tests/ships/alien_weapon_technologies_test.tscn b/tests/ships/alien_weapon_technologies_test.tscn new file mode 100644 index 0000000..8d7b7d6 --- /dev/null +++ b/tests/ships/alien_weapon_technologies_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/ships/alien_weapon_technologies_test.gd" id="1"] +[node name="AlienWeaponTechnologiesTest" type="Node2D"] +script = ExtResource("1")