Compare commits

..

2 Commits

Author SHA1 Message Date
31a352e4b7 Badania obcych 2026-10-25 13:30:48 +01:00
40cd69f8cb Zmiany 2026-10-25 12:34:56 +01:00
130 changed files with 2862 additions and 246 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 797 KiB

After

Width:  |  Height:  |  Size: 920 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 785 KiB

After

Width:  |  Height:  |  Size: 897 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -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

View File

@ -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
}
}

View File

@ -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.

View File

@ -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
}
}

View File

@ -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.

View File

@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@ -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

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="88" height="88" viewBox="0 0 88 88"><defs><radialGradient id="g"><stop stop-color="#ff9573" stop-opacity=".28"/><stop offset="1" stop-color="#101b29"/></radialGradient></defs><circle cx="44" cy="44" r="41" fill="url(#g)" stroke="#ff9573" stroke-width="2"/><circle cx="44" cy="44" r="36" fill="none" stroke="#ff9573" stroke-opacity=".25"/><g fill="none" stroke="#ff9573" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="44" cy="44" rx="29" ry="11"/><ellipse cx="44" cy="44" rx="29" ry="11" transform="rotate(60 44 44)"/><ellipse cx="44" cy="44" rx="29" ry="11" transform="rotate(120 44 44)"/><circle cx="44" cy="44" r="4"/></g></svg>

After

Width:  |  Height:  |  Size: 710 B

View File

@ -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

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="88" height="88" viewBox="0 0 88 88"><defs><radialGradient id="g"><stop stop-color="#6ceaff" stop-opacity=".28"/><stop offset="1" stop-color="#101b29"/></radialGradient></defs><circle cx="44" cy="44" r="41" fill="url(#g)" stroke="#6ceaff" stroke-width="2"/><circle cx="44" cy="44" r="36" fill="none" stroke="#6ceaff" stroke-opacity=".25"/><g fill="none" stroke="#6ceaff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M49 15 29 47h15l-5 26 25-37H48z"/></g></svg>

After

Width:  |  Height:  |  Size: 537 B

View File

@ -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

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="88" height="88" viewBox="0 0 88 88"><defs><radialGradient id="g"><stop stop-color="#ffc17c" stop-opacity=".28"/><stop offset="1" stop-color="#101b29"/></radialGradient></defs><circle cx="44" cy="44" r="41" fill="url(#g)" stroke="#ffc17c" stroke-width="2"/><circle cx="44" cy="44" r="36" fill="none" stroke="#ffc17c" stroke-opacity=".25"/><g fill="none" stroke="#ffc17c" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m27 58 29-29 10 10-29 29zm20-32 8-8 15 15-8 8M19 49l11-11m-10 23 7-7m13 19 11-11"/></g></svg>

After

Width:  |  Height:  |  Size: 586 B

View File

@ -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

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="88" height="88" viewBox="0 0 88 88"><defs><radialGradient id="g"><stop stop-color="#86baff" stop-opacity=".28"/><stop offset="1" stop-color="#101b29"/></radialGradient></defs><circle cx="44" cy="44" r="41" fill="url(#g)" stroke="#86baff" stroke-width="2"/><circle cx="44" cy="44" r="36" fill="none" stroke="#86baff" stroke-opacity=".25"/><g fill="none" stroke="#86baff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M44 17 65 26v18c0 12-9 22-21 28-12-6-21-16-21-28V26zM30 39h11m-5-5v10m14 7h11"/><path d="m43 32-5 16h10l-4 13"/></g></svg>

After

Width:  |  Height:  |  Size: 615 B

View File

@ -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

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="88" height="88" viewBox="0 0 88 88"><defs><radialGradient id="g"><stop stop-color="#b6f765" stop-opacity=".28"/><stop offset="1" stop-color="#101b29"/></radialGradient></defs><circle cx="44" cy="44" r="41" fill="url(#g)" stroke="#b6f765" stroke-width="2"/><circle cx="44" cy="44" r="36" fill="none" stroke="#b6f765" stroke-opacity=".25"/><g fill="none" stroke="#b6f765" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="44" cy="44" r="5"/><path d="m39 34-8-14a27 27 0 0 1 26 0l-8 14zm17 7 16 0a27 27 0 0 1-13 23l-8-14zm-19 9-8 14a27 27 0 0 1-13-23h16z"/></g></svg>

After

Width:  |  Height:  |  Size: 640 B

View File

@ -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

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="88" height="88" viewBox="0 0 88 88"><defs><radialGradient id="g"><stop stop-color="#bd9cff" stop-opacity=".28"/><stop offset="1" stop-color="#101b29"/></radialGradient></defs><circle cx="44" cy="44" r="41" fill="url(#g)" stroke="#bd9cff" stroke-width="2"/><circle cx="44" cy="44" r="36" fill="none" stroke="#bd9cff" stroke-opacity=".25"/><g fill="none" stroke="#bd9cff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="22" y="19" width="31" height="31" rx="5"/><rect x="36" y="33" width="31" height="31" rx="5"/><path d="M51 41v15m-7-7h14"/></g></svg>

After

Width:  |  Height:  |  Size: 625 B

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@ -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

View File

@ -0,0 +1,5 @@
# Deuterium deposit
Generated with built-in image_gen, transparent PNG.
undefined

View File

@ -0,0 +1,8 @@
shader_type canvas_item;
render_mode unshaded;
void fragment() {
vec4 pixel = texture(TEXTURE, UV);
float brightness = max(pixel.r, max(pixel.g, pixel.b));
COLOR = vec4(vec3(1.0, 0.19, 0.16) * (0.7 + brightness * 0.3), pixel.a * smoothstep(0.03, 0.16, brightness));
}

View File

@ -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")])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1 @@
uid://bphkxpc45ig5e

View File

@ -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)]

View File

@ -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)

View File

@ -2,6 +2,8 @@ extends Node2D
var enemy: CharacterBody2D
var _tier_label: Label
var _disabled_icons: Array[PanelContainer] = []
const DISABLED_ICONS := [preload("res://assets/icons/engine.png"),preload("res://assets/icons/plasma.png"),preload("res://assets/icons/shield.png")]
func _ready() -> void:
enemy = get_parent() as CharacterBody2D
@ -18,14 +20,56 @@ func _ready() -> void:
_tier_label.add_theme_color_override("font_outline_color", Color("08111d"))
_tier_label.add_theme_constant_override("outline_size", 3)
add_child(_tier_label)
_build_disabled_icons()
_update_position()
_update_tier_label()
_update_disabled_icons()
func _process(_delta: float) -> void:
_update_position()
_update_tier_label()
_update_disabled_icons()
queue_redraw()
func _build_disabled_icons() -> void:
var material := ShaderMaterial.new()
material.shader = preload("res://resources/effects/disabled_system_icon.gdshader")
for system in 3:
var panel := PanelContainer.new()
panel.name = ["DisabledEngine","DisabledWeapon","DisabledShields"][system]
panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
panel.size = Vector2(20,20)
panel.light_mask = 4
var style := StyleBoxFlat.new()
style.bg_color = Color("17090be8")
style.border_color = Color("ff4b43")
style.set_border_width_all(1)
style.set_corner_radius_all(3)
style.set_content_margin_all(2)
panel.add_theme_stylebox_override("panel",style)
var icon := TextureRect.new()
icon.texture = DISABLED_ICONS[system]
icon.material = material
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
icon.light_mask = 4
panel.add_child(icon)
add_child(panel)
_disabled_icons.append(panel)
func _update_disabled_icons() -> void:
var active: Array[int] = []
if enemy.hull > 0.0 and not enemy.is_queued_for_deletion():
for system in 3:
if enemy.system_disabled(system): active.append(system)
for system in 3:
var panel := _disabled_icons[system]
panel.visible = system in active
if panel.visible:
panel.position = Vector2(-float(active.size()*22-2)*0.5+active.find(system)*22,-56)
func _update_position() -> void:
global_position = enemy.global_position + Vector2(0, -enemy.get_visual_size() * 0.5 - 12.0)

View File

@ -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,60 @@ 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] = preload("res://scripts/planets/alien_research.gd").SYSTEM_DISABLE_DURATION
if is_instance_valid(_shield_visual): _shield_visual.set_shield_values(shields,max_shields)
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 +409,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)

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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}},

View File

@ -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)

View File

@ -0,0 +1 @@
uid://d0hrl5rjlb3ga

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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))

View File

@ -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:

View File

@ -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()

View File

@ -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":

View File

@ -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))

View File

@ -1,15 +1,22 @@
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 SYSTEM_DISABLE_DURATION := 5.0
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 +27,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 5 sekund losowy system obcego: silnik, broń lub tarcze. Czerwona ikona nad wrogiem wskazuje wyłączony system. 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:

View File

@ -2,12 +2,15 @@ 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
const COST_COLUMN_WIDTH := 48.0
var progress: Resource
var refreshed: Callable
var points: Dictionary = {}
func _ready() -> void:
custom_minimum_size = Vector2(290, 294)
custom_minimum_size = Vector2(330, 300)
resized.connect(_build)
_build()
@ -16,18 +19,30 @@ func _build() -> void:
remove_child(child)
child.queue_free()
points.clear()
var width := maxf(290, size.x)
_add_skill("general", Vector2(width * 0.5, 38), COLORS[0])
var width := maxf(330, size.x)
var tree_width := width-COST_COLUMN_WIDTH
_add_row_cost("general",24)
_add_row_cost("weapons",80)
var row_id := "weapons_a"
for level in PLACEHOLDER_LEVELS:
_add_row_cost(row_id,142+level*46)
row_id += "_next"
_add_skill("general", Vector2(COST_COLUMN_WIDTH+tree_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))
var x := COST_COLUMN_WIDTH+tree_width * (float(i) + 0.5) / 3.0
_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,46 +54,69 @@ 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.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
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.ONE
icon.size = TILE_SIZE-Vector2(2,2)
icon.clip_contents = true
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)
if learned:
var check := MENU.label(button,"✓",12,color)
check.mouse_filter = Control.MOUSE_FILTER_IGNORE
check.position = TILE_SIZE-Vector2(13,15)
check.size = Vector2(12,12)
check.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
func _add_row_cost(id: String, y: float) -> void:
var price := MENU.label(self,"%d pkt" % DATA.cost(id),12,MENU.MUTED)
price.name = "RowCost_%d" % DATA.cost(id)
price.autowrap_mode = TextServer.AUTOWRAP_OFF
price.position = Vector2(0,y-10)
price.size = Vector2(COST_COLUMN_WIDTH-6,20)
price.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
price.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
price.tooltip_text = "Koszt każdej technologii w tym rzędzie: %d próbek." % DATA.cost(id)
# The initially wrapped label may retain its old minimum height until text is shaped.
price.set_deferred("size",Vector2(COST_COLUMN_WIDTH-6,20))
func _add_placeholder(id: String, center: Vector2) -> void:
points[id] = center
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 +124,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)

View File

@ -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()

View File

@ -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)<int(b.level))
for map in maps:
@ -34,7 +38,7 @@ static func build(screen: Control) -> 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

View File

@ -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):
_contact_label.text = "● TELEMETRIA / DANE PLANETY"
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
# 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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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 = {}

View File

@ -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

View File

@ -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.")

View File

@ -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":

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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}

Some files were not shown because too many files have changed in this diff Show More