Zmiany
This commit is contained in:
parent
f3eb5fad65
commit
14082e3436
BIN
artifacts/chronal_workshop_ui.png
Normal file
BIN
artifacts/chronal_workshop_ui.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 864 KiB |
@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://dpvckkd1oc052"
|
uid="uid://cinvyp3qbv5yf"
|
||||||
path="res://.godot/imported/laboratory_projects.png-61c88f4e4c24b4b7ef45dab06ea98448.ctex"
|
path="res://.godot/imported/chronal_workshop_ui.png-90ce303bc4785fc6c962cf551a161261.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"vram_texture": false
|
"vram_texture": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://artifacts/laboratory_projects.png"
|
source_file="res://artifacts/chronal_workshop_ui.png"
|
||||||
dest_files=["res://.godot/imported/laboratory_projects.png-61c88f4e4c24b4b7ef45dab06ea98448.ctex"]
|
dest_files=["res://.godot/imported/chronal_workshop_ui.png-90ce303bc4785fc6c962cf551a161261.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
BIN
artifacts/command_layout_1280.png
Normal file
BIN
artifacts/command_layout_1280.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
40
artifacts/command_layout_1280.png.import
Normal file
40
artifacts/command_layout_1280.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cqslrjy0gsh0j"
|
||||||
|
path="res://.godot/imported/command_layout_1280.png-8529be7b5969de508917a247b1bf54ce.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://artifacts/command_layout_1280.png"
|
||||||
|
dest_files=["res://.godot/imported/command_layout_1280.png-8529be7b5969de508917a247b1bf54ce.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
|
||||||
BIN
artifacts/command_layout_960.png
Normal file
BIN
artifacts/command_layout_960.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 680 KiB |
40
artifacts/command_layout_960.png.import
Normal file
40
artifacts/command_layout_960.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://soe07738fyja"
|
||||||
|
path="res://.godot/imported/command_layout_960.png-238a097ab4f8bbe0ed0427078a03ea19.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://artifacts/command_layout_960.png"
|
||||||
|
dest_files=["res://.godot/imported/command_layout_960.png-238a097ab4f8bbe0ed0427078a03ea19.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
|
||||||
BIN
artifacts/enemy_explosion_preview.png
Normal file
BIN
artifacts/enemy_explosion_preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 399 KiB |
40
artifacts/enemy_explosion_preview.png.import
Normal file
40
artifacts/enemy_explosion_preview.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b4jkvu0skmpsk"
|
||||||
|
path="res://.godot/imported/enemy_explosion_preview.png-db0a252a8b01c14daaad125111c2cff8.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://artifacts/enemy_explosion_preview.png"
|
||||||
|
dest_files=["res://.godot/imported/enemy_explosion_preview.png-db0a252a8b01c14daaad125111c2cff8.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
|
||||||
16
artifacts/enemy_explosion_validation.md
Normal file
16
artifacts/enemy_explosion_validation.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Eksplozje wrogów — 2026-10-31
|
||||||
|
|
||||||
|
Nowy `enemy_destruction_effect.gd` zastępuje mały efekt trafienia przy zwykłym zniszczeniu wroga. Zachowano osobny efekt egzekucji / rozpadu kadłuba.
|
||||||
|
|
||||||
|
- Początkowy błysk i dwa rozszerzające się pierścienie: błękitna fala jonowa oraz bursztynowy front.
|
||||||
|
- Turbulentna kula ognia korzysta z istniejącego shadera ognia statku; 2–4 opóźnione wybuchy wtórne.
|
||||||
|
- 18 fragmentów faktycznej tekstury kadłuba; boss: 32. Fragmenty wirują, rozlatują się i stygną.
|
||||||
|
- 32–60 smug iskier i miękki, proceduralny dym.
|
||||||
|
- Skala zależna od rozmiaru statku, dłuższa animacja bossa i niższy dźwięk wybuchu. Zachowano część pędu zniszczonego statku.
|
||||||
|
- Efekt znika po 2,1–2,6 s. Limit 18 jednoczesnych eksplozji i 8 dźwięków. Bez fizycznych odłamków, nowych assetów bitmapowych ani zmian obrażeń i nagród.
|
||||||
|
|
||||||
|
Weryfikacja:
|
||||||
|
|
||||||
|
- Godot 4.7.2, `tests/items/enemy_destruction_test.tscn`: PASS — trzy klasy wrogów, odłamki, pęd, czas życia, rzeczywiste zniszczenie wroga, ochrona przed powtórnym efektem i limit aktywnych eksplozji.
|
||||||
|
- Wyrenderowano i obejrzano `artifacts/enemy_explosion_preview.png` na OpenGL 3.3 / Compatibility. Brak błędów shadera.
|
||||||
|
- Podgląd do ponownego uruchomienia: `tests/previews/enemy_explosion_preview.tscn`.
|
||||||
26
artifacts/space_progression_validation.md
Normal file
26
artifacts/space_progression_validation.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Kosmos zamiast lądowania — 2026-10-31
|
||||||
|
|
||||||
|
- Lądowanie wyłączone przez `PLANET_LANDING_ENABLED`; kod powierzchni, lądownika i dane zapisów zachowane.
|
||||||
|
- Deuter nie jest produkowany, pokazywany ani wymagany do podróży. Dawne pola zapisu zachowano dla zgodności.
|
||||||
|
- Każdy boss zwykłego układu zostawia jedną mapę. Dotychczasowy drop map i orbów na dalekich wyprawach zachowany.
|
||||||
|
- Trzy bazy zaopatrzeniowe na daleką wyprawę. Ostrzał niszczy bazę; podlecenie zbiera ładunek wszystkich pięciu dawnych surowców planetarnych. Ilość każdego: losowo 18–28 × zaokrąglony w górę pierwiastek poziomu sektora. Zniszczona baza nie odradza się w tej samej eksploracji.
|
||||||
|
- Każdy statek wroga ma niezależne 15% szansy na fizyczny drop jednej próbki, w zwykłym układzie i na wyprawie.
|
||||||
|
- Orb: kliknięcie orba, potem wyposażenia; PPM/Esc anuluje. Usunięto zbędny interfejs warsztatu orbów.
|
||||||
|
- Inżynier chronalny przeniesiony do warsztatu fregaty zgodnie z decyzją użytkownika; koszt i zasady maksymalizacji addonów zachowane.
|
||||||
|
- Usunięto naprawę lądownika, sieć wydobywczą i przydziały specjalistów z aktywnego interfejsu. Kajuty nie są już budowane.
|
||||||
|
- Zaktualizowano instrukcję gracza i opisy sprzętu planetarnego.
|
||||||
|
|
||||||
|
## Weryfikacja
|
||||||
|
|
||||||
|
Godot 4.7.2, headless, izolowany APPDATA wewnątrz katalogu tmp projektu:
|
||||||
|
|
||||||
|
- `space_progression_test.tscn`: failures=[] — podróż bez paliwa, zużycie mapy tylko raz, trzy bazy, jednokrotny drop i zbiór surowców, próbki, dokowanie, brak naprawy lądownika, inżynier bez aktywnej wyprawy powierzchniowej, orb na wyposażeniu, mapa ze zwykłego bossa.
|
||||||
|
- `distant_integration.tscn`: failures=[], passed=true — lot, animacja skoku, drop, orby i powrót.
|
||||||
|
- `distant_defeat_return.tscn`: PASS — powrót po porażce, wyposażenie, mapy, dokowanie i blokada podwójnego ponowienia.
|
||||||
|
- `git diff --check`: bez błędów.
|
||||||
|
|
||||||
|
## Kwestie do dalszej decyzji
|
||||||
|
|
||||||
|
Wyposażenie kapitana do eksploracji powierzchni i bonusy związane z planetami zachowano, ale nie mają obecnie zastosowania. Ich nowe przeznaczenie wymaga osobnej decyzji projektowej. Produkcja wyposażenia kapitana pozostaje dostępna z informacją o wstrzymanym zastosowaniu.
|
||||||
|
|
||||||
|
Bazy korzystają obecnie z istniejącej grafiki przekaźnika w kolorze wrogiej instalacji; nie mają osobnego uzbrojenia. Bronią ich przeciwnicy obecni w sektorze. Liczba baz, uzysk i jedna próbka na trafiony rzut są przyjętymi wartościami startowymi do balansu.
|
||||||
17
artifacts/workshop_and_instant_research_validation.md
Normal file
17
artifacts/workshop_and_instant_research_validation.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Warsztat i natychmiastowe odkrycia — 2026-10-31
|
||||||
|
|
||||||
|
Warsztat: zamiast portretu inżyniera i rozwijanych list jest panel z ikonami wyposażenia, wyborem addonu przyciskiem, paskiem wartości, podglądem obecna → maksymalna oraz przyciskiem maksymalizacji. Saldo i koszt mają ikonę kryształów czasu. Dłuższe wyjaśnienia przeniesiono do podpowiedzi. Gotowe addony mają oznaczenie MAX. Wybór nie przebudowuje całego warsztatu, dzięki czemu nie resetuje przewijania.
|
||||||
|
|
||||||
|
Podgląd OpenGL Compatibility: `artifacts/chronal_workshop_ui.png`. Sprawdzono wizualnie oraz kliknięciami w rzeczywistym panelu: wybór przedmiotu i addonu, poprawny koszt, maksymalizację, blokadę gotowego maksimum oraz stabilność przewijania.
|
||||||
|
|
||||||
|
Laboratorium: projekty odblokowują się natychmiast po zapłaceniu dotychczasowego kosztu surowców i od razu trafiają do puli łupów. Interfejs nie pokazuje czasu ani paska postępu. Zachowano timery badań obcych, bo zmiana dotyczy wynajdywania modułów.
|
||||||
|
|
||||||
|
Usunięto Naukowca z katalogu, opisów i ofert umiejętności. Rangi ze starych zapisów zwracają równą liczbę punktów nauki jednorazowo. Nieaktualne oferty są filtrowane. Opłacone badania modułów ze starych zapisów kończą się od razu podczas wczytania.
|
||||||
|
|
||||||
|
Godot 4.7.2:
|
||||||
|
|
||||||
|
- CHRONAL_UI_CHECK: PASS.
|
||||||
|
- INSTANT_LABORATORY_TEST: PASS — koszt, natychmiastowe odkrycie, ochrona przed ponownym zakupem, stary projekt, zwrot rang, UI bez timera.
|
||||||
|
- RESEARCH PROGRESSION: PASS — wszystkie 23 projekty i 28 badań obcych, pula łupu i zapis postępu.
|
||||||
|
- NEW_CAPTAIN_SKILLS_TEST: PASS — 15 umiejętności, ikony, trening, pozostałe bonusy.
|
||||||
|
- SAVE_SETTINGS_TEST: PASS, 0 failures — wczytanie kończy stare badanie modułu.
|
||||||
@ -47,6 +47,10 @@ func get_energy_bank_fraction() -> float:
|
|||||||
return maxf(0.10, get_skill_bonus("electrician"))
|
return maxf(0.10, get_skill_bonus("electrician"))
|
||||||
|
|
||||||
func normalize_skill_ranks() -> void:
|
func normalize_skill_ranks() -> void:
|
||||||
|
if skills.has("scientist"):
|
||||||
|
learning_points += clampi(int(skills.scientist),0,SKILLS.MAX_RANK)
|
||||||
|
skills.erase("scientist")
|
||||||
|
skill_offers.assign(skill_offers.filter(func(id): return SKILLS.DATA.has(id)))
|
||||||
for skill in skills.keys():
|
for skill in skills.keys():
|
||||||
if not SKILLS.DATA.has(skill):
|
if not SKILLS.DATA.has(skill):
|
||||||
skills.erase(skill)
|
skills.erase(skill)
|
||||||
@ -94,7 +98,7 @@ func reset_skills() -> bool:
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
func can_train(skill: String) -> bool:
|
func can_train(skill: String) -> bool:
|
||||||
return learning_points > 0 and skill in skill_offers and get_skill_rank(skill) < SKILLS.MAX_RANK
|
return SKILLS.DATA.has(skill) and learning_points > 0 and skill in skill_offers and get_skill_rank(skill) < SKILLS.MAX_RANK
|
||||||
|
|
||||||
func train_skill(skill: String) -> bool:
|
func train_skill(skill: String) -> bool:
|
||||||
if not can_train(skill):
|
if not can_train(skill):
|
||||||
@ -112,6 +116,7 @@ func train_skill(skill: String) -> bool:
|
|||||||
return true
|
return true
|
||||||
|
|
||||||
func prepare_skill_offers() -> void:
|
func prepare_skill_offers() -> void:
|
||||||
|
normalize_skill_ranks()
|
||||||
if learning_points <= 0 or not skill_offers.is_empty():
|
if learning_points <= 0 or not skill_offers.is_empty():
|
||||||
return
|
return
|
||||||
var pool: Array[String] = []
|
var pool: Array[String] = []
|
||||||
|
|||||||
@ -4,11 +4,10 @@ const MAX_RANK := 3
|
|||||||
static var _icon_cache: Dictionary = {}
|
static var _icon_cache: Dictionary = {}
|
||||||
const COOLDOWN := 30.0
|
const COOLDOWN := 30.0
|
||||||
const DURATION := 5.0
|
const DURATION := 5.0
|
||||||
const IDS := ["business_shark", "reactor_overload", "tactical_maneuver", "efficient_magazine", "miner", "power_redirect", "lucky", "explorer", "electrician", "scientist", "gunsmith", "bounty_hunter", "missile_specialist", "emergency_procedure", "scrap_collector", "drone_commander"]
|
const IDS := ["business_shark", "reactor_overload", "tactical_maneuver", "efficient_magazine", "miner", "power_redirect", "lucky", "explorer", "electrician", "gunsmith", "bounty_hunter", "missile_specialist", "emergency_procedure", "scrap_collector", "drone_commander"]
|
||||||
const DATA := {
|
const DATA := {
|
||||||
"scrap_collector": {"name": "Zbieracz złomu", "active": false, "values": [0.15, 0.25, 0.40], "color": "d9b27c"},
|
"scrap_collector": {"name": "Zbieracz złomu", "active": false, "values": [0.15, 0.25, 0.40], "color": "d9b27c"},
|
||||||
"drone_commander": {"name": "Dowódca dronów", "active": false, "values": [0.10, 0.20, 0.30], "color": "70dfff"},
|
"drone_commander": {"name": "Dowódca dronów", "active": false, "values": [0.10, 0.20, 0.30], "color": "70dfff"},
|
||||||
"scientist": {"name": "Naukowiec", "active": false, "values": [0.10, 0.20, 0.30], "color": "76d9ed"},
|
|
||||||
"gunsmith": {"name": "Rusznikarz", "active": false, "values": [0.10, 0.20, 0.30], "color": "e8b26e"},
|
"gunsmith": {"name": "Rusznikarz", "active": false, "values": [0.10, 0.20, 0.30], "color": "e8b26e"},
|
||||||
"bounty_hunter": {"name": "Łowca nagród", "active": false, "values": [0.10, 0.20, 0.30], "color": "efcc70"},
|
"bounty_hunter": {"name": "Łowca nagród", "active": false, "values": [0.10, 0.20, 0.30], "color": "efcc70"},
|
||||||
"missile_specialist": {"name": "Specjalista rakietowy", "active": false, "values": [0.10, 0.15, 0.20], "color": "f29576"},
|
"missile_specialist": {"name": "Specjalista rakietowy", "active": false, "values": [0.10, 0.15, 0.20], "color": "f29576"},
|
||||||
@ -66,8 +65,6 @@ static func description(skill: String, rank: int) -> String:
|
|||||||
return "%d%% szansy na 1 dodatkową część z przeszukanego wraku. Losowanie raz na wrak. Premie rang nie sumują się." % amount
|
return "%d%% szansy na 1 dodatkową część z przeszukanego wraku. Losowanie raz na wrak. Premie rang nie sumują się." % amount
|
||||||
"drone_commander":
|
"drone_commander":
|
||||||
return "Obrażenia dronów bojowych większe o %d%%. Premie rang nie sumują się." % amount
|
return "Obrażenia dronów bojowych większe o %d%%. Premie rang nie sumują się." % amount
|
||||||
"scientist":
|
|
||||||
return "Nowe badania w laboratorium trwają o %d%% krócej. Premie rang nie sumują się." % amount
|
|
||||||
"gunsmith":
|
"gunsmith":
|
||||||
return "Produkcja zwykłej i radiacyjnej amunicji daje o %d%% więcej sztuk za ten sam koszt. Nie dotyczy rakiet. Premie rang nie sumują się." % amount
|
return "Produkcja zwykłej i radiacyjnej amunicji daje o %d%% więcej sztuk za ten sam koszt. Nie dotyczy rakiet. Premie rang nie sumują się." % amount
|
||||||
"bounty_hunter":
|
"bounty_hunter":
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
const MATERIALS := 10000
|
const MATERIALS := 10000
|
||||||
const CREDITS := 1000000
|
const CREDITS := 1000000
|
||||||
const DEUTER := 1000000
|
|
||||||
const CRYSTALS := 10000
|
const CRYSTALS := 10000
|
||||||
const SAMPLES := 1000
|
const SAMPLES := 1000
|
||||||
const ORBS := 25
|
const ORBS := 25
|
||||||
@ -10,7 +9,7 @@ const AMMUNITION := 10000
|
|||||||
const RADIATION_AMMUNITION := 1000
|
const RADIATION_AMMUNITION := 1000
|
||||||
const MISSILES := 1000
|
const MISSILES := 1000
|
||||||
const POWERBANKS := 100
|
const POWERBANKS := 100
|
||||||
const TOOLTIP := "+1 000 000 kredytów i deuteru\n+10 000 każdego minerału, części i kryształów czasu\n+1000 próbek, +25 każdego orba, +100 punktów nauki\n+10 000 amunicji, +1000 radiacyjnej i rakiet, +100 powerbanków\nMapy: poziomy 2, obecny +1/+5, 120, 125 i 130 (bez duplikatów)\nPo 1 każdego rodzaju wyposażenia do magazynu fregaty oraz sprzęt kapitana."
|
const TOOLTIP := "+1 000 000 kredytów\n+10 000 każdego minerału, części i kryształów czasu\n+1000 próbek, +25 każdego orba, +100 punktów nauki\n+10 000 amunicji, +1000 radiacyjnej i rakiet, +100 powerbanków\nMapy: poziomy 2, obecny +1/+5, 120, 125 i 130 (bez duplikatów)\nPo 1 każdego rodzaju wyposażenia do magazynu fregaty oraz sprzęt kapitana."
|
||||||
|
|
||||||
static func grant(captain: Resource, inventory: Node, level: int) -> Dictionary:
|
static func grant(captain: Resource, inventory: Node, level: int) -> Dictionary:
|
||||||
var progress: Resource = captain.expedition
|
var progress: Resource = captain.expedition
|
||||||
@ -21,7 +20,6 @@ static func grant(captain: Resource, inventory: Node, level: int) -> Dictionary:
|
|||||||
captain.credits += CREDITS
|
captain.credits += CREDITS
|
||||||
captain.learning_points += LEARNING_POINTS
|
captain.learning_points += LEARNING_POINTS
|
||||||
captain.prepare_skill_offers()
|
captain.prepare_skill_offers()
|
||||||
progress.deuter += DEUTER
|
|
||||||
progress.time_crystals += CRYSTALS
|
progress.time_crystals += CRYSTALS
|
||||||
progress.analysis_samples += SAMPLES
|
progress.analysis_samples += SAMPLES
|
||||||
for orb in preload("res://scripts/items/special_orbs.gd").IDS:
|
for orb in preload("res://scripts/items/special_orbs.gd").IDS:
|
||||||
|
|||||||
@ -16,6 +16,7 @@ func _ready() -> void:
|
|||||||
if int(data.tier) == 2: game._pending_boss = data.duplicate()
|
if int(data.tier) == 2: game._pending_boss = data.duplicate()
|
||||||
else: game._spawn_enemy(data,data.position)
|
else: game._spawn_enemy(data,data.position)
|
||||||
_build_structures()
|
_build_structures()
|
||||||
|
_build_resource_bases()
|
||||||
var expeditions := Node.new()
|
var expeditions := Node.new()
|
||||||
expeditions.set_script(preload("res://scripts/planets/expedition_controller.gd"))
|
expeditions.set_script(preload("res://scripts/planets/expedition_controller.gd"))
|
||||||
expeditions.game = game
|
expeditions.game = game
|
||||||
@ -36,6 +37,18 @@ func _ready() -> void:
|
|||||||
tween.tween_property(fade,"color:a",0.0,0.55)
|
tween.tween_property(fade,"color:a",0.0,0.55)
|
||||||
tween.tween_callback(arrival.queue_free)
|
tween.tween_callback(arrival.queue_free)
|
||||||
|
|
||||||
|
func _build_resource_bases() -> void:
|
||||||
|
for index in 3:
|
||||||
|
var id := "%d:enemy_base:%d" % [GameState.system_seed,index]
|
||||||
|
if GameState.explored_pois.has(id): continue
|
||||||
|
var base := StaticBody2D.new()
|
||||||
|
base.set_script(preload("res://scripts/levels/enemy_resource_base.gd"))
|
||||||
|
base.base_id = id
|
||||||
|
base.reward_seed = GameState.system_seed ^ (index * 7919)
|
||||||
|
base.level = GameState.system_level
|
||||||
|
base.position = Vector2.RIGHT.rotated(TAU * index / 3.0) * 1200
|
||||||
|
game.add_child(base)
|
||||||
|
|
||||||
func _build_structures() -> void:
|
func _build_structures() -> void:
|
||||||
var group := Node2D.new()
|
var group := Node2D.new()
|
||||||
group.name = "AncientStructures"
|
group.name = "AncientStructures"
|
||||||
|
|||||||
197
scripts/game/enemy_destruction_effect.gd
Normal file
197
scripts/game/enemy_destruction_effect.gd
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
extends Node2D
|
||||||
|
## Short, layered death effect. All particles are local visuals, without physics bodies.
|
||||||
|
const FIRE_SHADER := preload("res://resources/effects/ship_fireball.gdshader")
|
||||||
|
const EXPLOSION_AUDIO := preload("res://assets/sounds/effects/explosion.mp3")
|
||||||
|
const MAX_EFFECTS := 18
|
||||||
|
static var _smoke_texture: Texture2D
|
||||||
|
var _elapsed := 0.0
|
||||||
|
var _duration := 2.1
|
||||||
|
var _size := 56.0
|
||||||
|
var _tier := 0
|
||||||
|
var _texture: Texture2D
|
||||||
|
var _tint := Color.WHITE
|
||||||
|
var _ship_scale := Vector2.ONE
|
||||||
|
var _angle := 0.0
|
||||||
|
var _drift := Vector2.ZERO
|
||||||
|
var _fragments: Array[Dictionary] = []
|
||||||
|
var _sparks: Array[Dictionary] = []
|
||||||
|
var _smoke: Array[Dictionary] = []
|
||||||
|
var _bursts: Array[Dictionary] = []
|
||||||
|
var _rng := RandomNumberGenerator.new()
|
||||||
|
var play_audio := true
|
||||||
|
|
||||||
|
func configure(sprite: Sprite2D, drift: Vector2, tier: int) -> void:
|
||||||
|
top_level = true
|
||||||
|
_texture = sprite.texture
|
||||||
|
_ship_scale = sprite.global_scale
|
||||||
|
_angle = sprite.global_rotation
|
||||||
|
_tint = sprite.modulate
|
||||||
|
global_position = sprite.global_position
|
||||||
|
_drift = drift * 0.65
|
||||||
|
_tier = clampi(tier,0,2)
|
||||||
|
_size = maxf(_texture.get_width()*_ship_scale.x,_texture.get_height()*_ship_scale.y) if _texture != null else 56.0
|
||||||
|
_duration = 2.1 + _tier * 0.25
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
z_index = 35
|
||||||
|
light_mask = 4
|
||||||
|
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||||||
|
physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_OFF
|
||||||
|
add_to_group("enemy_explosions")
|
||||||
|
var active := get_tree().get_nodes_in_group("enemy_explosions")
|
||||||
|
if active.size() > MAX_EFFECTS:
|
||||||
|
for old in active:
|
||||||
|
if old != self and not old.is_queued_for_deletion():
|
||||||
|
old.queue_free()
|
||||||
|
break
|
||||||
|
_rng.randomize()
|
||||||
|
_prepare_smoke()
|
||||||
|
_build_fragments()
|
||||||
|
_build_particles()
|
||||||
|
_add_fireball(Vector2.ZERO,_size*3.8,0.0)
|
||||||
|
for index in 2 + _tier:
|
||||||
|
var direction := Vector2.from_angle(_rng.randf()*TAU)
|
||||||
|
_add_fireball(direction*_size*_rng.randf_range(0.2,0.5),_size*_rng.randf_range(1.5,2.3),0.07+index*0.07)
|
||||||
|
if play_audio: _play_sound()
|
||||||
|
_update_fireballs()
|
||||||
|
|
||||||
|
func _add_fireball(origin: Vector2, diameter: float, delay: float) -> void:
|
||||||
|
var mesh := MeshInstance2D.new()
|
||||||
|
var quad := QuadMesh.new()
|
||||||
|
quad.size = Vector2.ONE * diameter
|
||||||
|
mesh.mesh = quad
|
||||||
|
mesh.position = origin
|
||||||
|
mesh.rotation = _rng.randf()*TAU
|
||||||
|
mesh.light_mask = 4
|
||||||
|
mesh.z_index = 1
|
||||||
|
var fire := ShaderMaterial.new()
|
||||||
|
fire.shader = FIRE_SHADER
|
||||||
|
fire.set_shader_parameter("variation",_rng.randf_range(0,100))
|
||||||
|
mesh.material = fire
|
||||||
|
add_child(mesh)
|
||||||
|
_bursts.append({"visual":mesh,"material":fire,"delay":delay})
|
||||||
|
|
||||||
|
func _build_fragments() -> void:
|
||||||
|
if _texture == null: return
|
||||||
|
var dimensions := _texture.get_size()
|
||||||
|
# Textured triangles preserve the actual hull, rather than generic square debris.
|
||||||
|
var grid := 3 if _tier < 2 else 4
|
||||||
|
for y in grid:
|
||||||
|
for x in grid:
|
||||||
|
var a := Vector2(x,y)*dimensions/grid
|
||||||
|
var b := Vector2(x+1,y)*dimensions/grid
|
||||||
|
var c := Vector2(x+1,y+1)*dimensions/grid
|
||||||
|
var d := Vector2(x,y+1)*dimensions/grid
|
||||||
|
for triangle in [[a,b,c],[a,c,d]]:
|
||||||
|
var center: Vector2 = (triangle[0]+triangle[1]+triangle[2])/3.0
|
||||||
|
var origin := ((center-dimensions*0.5)*_ship_scale).rotated(_angle)
|
||||||
|
var points := PackedVector2Array()
|
||||||
|
var uv := PackedVector2Array()
|
||||||
|
for vertex in triangle:
|
||||||
|
points.append((vertex-center)*_ship_scale)
|
||||||
|
uv.append(vertex/dimensions)
|
||||||
|
var direction := origin.normalized().rotated(_rng.randf_range(-0.4,0.4))
|
||||||
|
_fragments.append({"origin":origin,"velocity":direction*_rng.randf_range(80,175)*(1.0+_tier*0.2),"points":points,"uv":uv,"spin":_rng.randf_range(-5,5)})
|
||||||
|
|
||||||
|
func _build_particles() -> void:
|
||||||
|
for index in 32 + _tier * 14:
|
||||||
|
var direction := Vector2.from_angle(_rng.randf()*TAU)
|
||||||
|
_sparks.append({"velocity":direction*_rng.randf_range(100,370)*(1.0+0.18*_tier),"life":_rng.randf_range(0.4,1.35),"width":_rng.randf_range(0.8,2.1),"warm":_rng.randf()})
|
||||||
|
for index in 10 + _tier * 3:
|
||||||
|
var direction := Vector2.from_angle(_rng.randf()*TAU)
|
||||||
|
_smoke.append({"origin":direction*_size*_rng.randf_range(0.1,0.35),"velocity":direction*_rng.randf_range(16,48),"radius":_size*_rng.randf_range(0.22,0.4),"angle":_rng.randf()*TAU,"delay":_rng.randf_range(0.08,0.25)})
|
||||||
|
|
||||||
|
static func _prepare_smoke() -> void:
|
||||||
|
if _smoke_texture != null: return
|
||||||
|
var noise := FastNoiseLite.new()
|
||||||
|
noise.seed = 9172
|
||||||
|
noise.frequency = 0.075
|
||||||
|
var image := Image.create(64,64,false,Image.FORMAT_RGBA8)
|
||||||
|
for y in 64:
|
||||||
|
for x in 64:
|
||||||
|
var point := (Vector2(x,y)-Vector2(31.5,31.5))/31.5
|
||||||
|
var edge := 1.0-smoothstep(0.2,1.0,point.length())
|
||||||
|
var density := clampf(0.65+noise.get_noise_2d(x,y)*0.65,0,1)
|
||||||
|
image.set_pixel(x,y,Color(1,1,1,edge*density))
|
||||||
|
_smoke_texture = ImageTexture.create_from_image(image)
|
||||||
|
|
||||||
|
func _play_sound() -> void:
|
||||||
|
if get_tree().get_nodes_in_group("enemy_explosion_audio").size() >= 8: return
|
||||||
|
# Separate audio lifetime lets the tail finish after the visual is freed.
|
||||||
|
var audio := AudioStreamPlayer2D.new()
|
||||||
|
audio.stream = EXPLOSION_AUDIO
|
||||||
|
audio.volume_db = -15.0 + _tier*2.5
|
||||||
|
audio.pitch_scale = _rng.randf_range(0.95,1.12) - _tier*0.13
|
||||||
|
audio.max_distance = 1500
|
||||||
|
audio.max_polyphony = 1
|
||||||
|
audio.add_to_group("enemy_explosion_audio")
|
||||||
|
get_parent().add_child(audio)
|
||||||
|
audio.global_position = global_position
|
||||||
|
audio.finished.connect(audio.queue_free)
|
||||||
|
audio.play()
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
_elapsed += delta
|
||||||
|
global_position += _drift * delta
|
||||||
|
_drift *= exp(-1.4*delta)
|
||||||
|
if _elapsed >= _duration:
|
||||||
|
queue_free()
|
||||||
|
return
|
||||||
|
_update_fireballs()
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _update_fireballs() -> void:
|
||||||
|
for burst in _bursts:
|
||||||
|
var age: float = _elapsed - float(burst.delay)
|
||||||
|
var visual: MeshInstance2D = burst.visual
|
||||||
|
visual.visible = age >= 0 and age < 1.3
|
||||||
|
if visual.visible:
|
||||||
|
var fire: ShaderMaterial = burst.material
|
||||||
|
fire.set_shader_parameter("phase",age)
|
||||||
|
fire.set_shader_parameter("strength",1.25 if _tier == 2 else 1.0)
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var time := _elapsed
|
||||||
|
# A soft, irregular smoke cloud stays behind as the fire cools.
|
||||||
|
for puff in _smoke:
|
||||||
|
var age := maxf(0,time-float(puff.delay))
|
||||||
|
var alpha := smoothstep(0.0,0.3,age)*(1.0-smoothstep(0.6,_duration,age))*0.45
|
||||||
|
var center: Vector2 = puff.origin + puff.velocity * (1.0-exp(-age*1.5))/1.5
|
||||||
|
var radius: float = puff.radius*(0.6+age*0.85)
|
||||||
|
draw_set_transform(center,float(puff.angle)+age*0.15)
|
||||||
|
draw_texture_rect(_smoke_texture,Rect2(-Vector2.ONE*radius,Vector2.ONE*radius*2),false,Color(0.12,0.14,0.18,alpha))
|
||||||
|
draw_set_transform(Vector2.ZERO)
|
||||||
|
# Thin blue ion shock followed by a wider amber pressure front.
|
||||||
|
for ring in 2:
|
||||||
|
var age := time-ring*0.055
|
||||||
|
if age < 0 or age > 0.65: continue
|
||||||
|
var progress := age/0.65
|
||||||
|
var radius := _size*(0.16+1.75*(1.0-pow(1.0-progress,3)))
|
||||||
|
var fade := pow(1.0-progress,2)
|
||||||
|
var color := Color("87dfff") if ring == 0 else Color("ff9d49")
|
||||||
|
draw_arc(Vector2.ZERO,radius,0,TAU,80,Color(color,fade*0.1),9.0*(1.0-progress)+1,true)
|
||||||
|
draw_arc(Vector2.ZERO,radius,0,TAU,80,Color(color,fade*0.75),1.2+fade,true)
|
||||||
|
var debris_fade := 1.0-smoothstep(0.7,_duration,time)
|
||||||
|
var travel := (1.0-exp(-time*1.4))/1.4
|
||||||
|
for piece in _fragments:
|
||||||
|
var center: Vector2 = piece.origin + piece.velocity*travel
|
||||||
|
draw_set_transform(center,_angle+float(piece.spin)*time)
|
||||||
|
var hot := exp(-time*4.0)
|
||||||
|
var color := _tint.lerp(Color("ffd28b"),hot).darkened((1.0-hot)*0.55)
|
||||||
|
color.a = debris_fade
|
||||||
|
draw_polygon(piece.points,PackedColorArray([color]),piece.uv,_texture)
|
||||||
|
draw_set_transform(Vector2.ZERO)
|
||||||
|
for spark in _sparks:
|
||||||
|
if time > float(spark.life): continue
|
||||||
|
var fade := pow(1.0-time/float(spark.life),1.5)
|
||||||
|
var velocity: Vector2 = spark.velocity
|
||||||
|
var point := velocity*travel
|
||||||
|
var tail := point-velocity.normalized()*(3.0+fade*16.0)
|
||||||
|
var color := Color("ffc35c").lerp(Color("fff2c4"),float(spark.warm))
|
||||||
|
draw_line(tail,point,Color(color,fade*0.14),float(spark.width)*4,true)
|
||||||
|
draw_line(tail,point,Color(color,fade),float(spark.width),true)
|
||||||
|
var flash := exp(-time*30.0)
|
||||||
|
if flash > 0.01:
|
||||||
|
for layer in range(5,0,-1):
|
||||||
|
draw_circle(Vector2.ZERO,_size*0.45*layer/5.0,Color(1,0.83,0.55,flash*0.12))
|
||||||
|
draw_line(Vector2(-_size*1.2,0),Vector2(_size*1.2,0),Color(1,0.92,0.75,flash*0.75),1.6,true)
|
||||||
1
scripts/game/enemy_destruction_effect.gd.uid
Normal file
1
scripts/game/enemy_destruction_effect.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://dhhxuuns2fple
|
||||||
@ -518,6 +518,7 @@ func _reward_captain() -> void:
|
|||||||
get_parent().call_deferred("add_child", credits)
|
get_parent().call_deferred("add_child", credits)
|
||||||
|
|
||||||
func _drop_loot() -> void:
|
func _drop_loot() -> void:
|
||||||
|
preload("res://scripts/game/space_rewards.gd").drop(self)
|
||||||
# Roll independently of equipment drops.
|
# Roll independently of equipment drops.
|
||||||
var distant := not GameState.active_distant_map.is_empty()
|
var distant := not GameState.active_distant_map.is_empty()
|
||||||
if distant: preload("res://scripts/game/distant_loot.gd").drop(self)
|
if distant: preload("res://scripts/game/distant_loot.gd").drop(self)
|
||||||
@ -547,10 +548,10 @@ func _spawn_destruction_effect() -> void:
|
|||||||
collapse.position = position
|
collapse.position = position
|
||||||
get_parent().add_child(collapse)
|
get_parent().add_child(collapse)
|
||||||
return
|
return
|
||||||
var effect := IMPACT_EFFECT_SCENE.instantiate() as Node2D
|
var effect := Node2D.new()
|
||||||
effect.global_position = global_position
|
effect.set_script(preload("res://scripts/game/enemy_destruction_effect.gd"))
|
||||||
|
effect.call("configure",_sprite,velocity,loot_tier)
|
||||||
get_parent().add_child(effect)
|
get_parent().add_child(effect)
|
||||||
effect.call("configure", true, Color(1.0, 0.36, 0.18))
|
|
||||||
|
|
||||||
func _show_damage_popup(amount: float, critical: bool = false) -> void:
|
func _show_damage_popup(amount: float, critical: bool = false) -> void:
|
||||||
if critical:
|
if critical:
|
||||||
|
|||||||
@ -262,7 +262,6 @@ func _load_next_level() -> void:
|
|||||||
func travel_to_distant(map_id: String) -> bool:
|
func travel_to_distant(map_id: String) -> bool:
|
||||||
if _transitioning or bool(player.get("_is_destroyed")) or not GameState.active_distant_map.is_empty(): return false
|
if _transitioning or bool(player.get("_is_destroyed")) or not GameState.active_distant_map.is_empty(): return false
|
||||||
if not GameState.captain.expedition.distant_maps.has(map_id): return false
|
if not GameState.captain.expedition.distant_maps.has(map_id): return false
|
||||||
if GameState.captain.expedition.deuter < preload("res://scripts/planets/distant_expeditions.gd").fuel_cost(GameState.captain.expedition.distant_maps[map_id]): return false
|
|
||||||
_transitioning = true
|
_transitioning = true
|
||||||
_load_distant.call_deferred(map_id)
|
_load_distant.call_deferred(map_id)
|
||||||
return true
|
return true
|
||||||
@ -273,7 +272,6 @@ func _load_distant(map_id: String) -> void:
|
|||||||
GameState.restart_inventory.clear()
|
GameState.restart_inventory.clear()
|
||||||
_transitioning = false
|
_transitioning = false
|
||||||
return
|
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)
|
var map_snapshot: Dictionary = GameState.active_distant_map.duplicate(true)
|
||||||
var previous_pause := get_tree().paused
|
var previous_pause := get_tree().paused
|
||||||
var hud := get_node("HUD/HUDRoot") as CanvasItem
|
var hud := get_node("HUD/HUDRoot") as CanvasItem
|
||||||
@ -309,7 +307,6 @@ func _load_distant(map_id: String) -> void:
|
|||||||
get_tree().paused = previous_pause
|
get_tree().paused = previous_pause
|
||||||
GameState.restore_distant_home()
|
GameState.restore_distant_home()
|
||||||
GameState.captain.expedition.distant_maps[map_id] = map_snapshot
|
GameState.captain.expedition.distant_maps[map_id] = map_snapshot
|
||||||
GameState.captain.expedition.deuter += cost
|
|
||||||
GameState.captain.expedition.emit_changed()
|
GameState.captain.expedition.emit_changed()
|
||||||
GameState.restart_inventory.clear()
|
GameState.restart_inventory.clear()
|
||||||
_transitioning = false
|
_transitioning = false
|
||||||
|
|||||||
@ -45,13 +45,10 @@ func load_game(slot_id: String) -> bool:
|
|||||||
func depart_distant(map_id: String) -> bool:
|
func depart_distant(map_id: String) -> bool:
|
||||||
if not active_distant_map.is_empty() or not captain.expedition.distant_maps.has(map_id): return false
|
if not active_distant_map.is_empty() or not captain.expedition.distant_maps.has(map_id): return false
|
||||||
var map: Dictionary = captain.expedition.distant_maps[map_id]
|
var map: Dictionary = captain.expedition.distant_maps[map_id]
|
||||||
var cost: int = preload("res://scripts/planets/distant_expeditions.gd").fuel_cost(map)
|
|
||||||
if captain.expedition.deuter < cost: return false
|
|
||||||
distant_home = {"level":system_level,"seed":system_seed,"hull_bastion_used":hull_bastion_used}
|
distant_home = {"level":system_level,"seed":system_seed,"hull_bastion_used":hull_bastion_used}
|
||||||
hull_bastion_used = false
|
hull_bastion_used = false
|
||||||
active_distant_map = map.duplicate(true)
|
active_distant_map = map.duplicate(true)
|
||||||
captain.expedition.distant_maps.erase(map_id)
|
captain.expedition.distant_maps.erase(map_id)
|
||||||
captain.expedition.deuter -= cost
|
|
||||||
system_level = int(map.level)
|
system_level = int(map.level)
|
||||||
system_seed = int(map.seed)
|
system_seed = int(map.seed)
|
||||||
captain.expedition.emit_changed()
|
captain.expedition.emit_changed()
|
||||||
@ -143,8 +140,7 @@ func advance_play_time(delta: float, scene: Node) -> void:
|
|||||||
captain.emit_changed()
|
captain.emit_changed()
|
||||||
var inventory := scene.get_node_or_null("PlayerShip/ShipInventory")
|
var inventory := scene.get_node_or_null("PlayerShip/ShipInventory")
|
||||||
if inventory != null: inventory._recalculate_ship_stats()
|
if inventory != null: inventory._recalculate_ship_stats()
|
||||||
var network := preload("res://scripts/planets/mining_network.gd")
|
if not researching: return
|
||||||
if network.advance(captain.expedition, delta) <= 0 and not researching: return
|
|
||||||
_production_signal_time += delta
|
_production_signal_time += delta
|
||||||
if _production_signal_time >= 1.0:
|
if _production_signal_time >= 1.0:
|
||||||
_production_signal_time = fmod(_production_signal_time, 1.0)
|
_production_signal_time = fmod(_production_signal_time, 1.0)
|
||||||
|
|||||||
@ -106,6 +106,8 @@ static func restore_state(state: Node, id: String = "") -> bool:
|
|||||||
if data.state.has(key): state.set(key, data.state[key])
|
if data.state.has(key): state.set(key, data.state[key])
|
||||||
state.captain = data.captain
|
state.captain = data.captain
|
||||||
state.captain.skill_offers.assign(data.skill_offers)
|
state.captain.skill_offers.assign(data.skill_offers)
|
||||||
|
state.captain.normalize_skill_ranks()
|
||||||
|
preload("res://scripts/planets/laboratory.gd").advance(state.captain.expedition,0.0)
|
||||||
state.captain.reset_combat_state()
|
state.captain.reset_combat_state()
|
||||||
state.restart_inventory = data.inventory
|
state.restart_inventory = data.inventory
|
||||||
state.return_to_freighter = false
|
state.return_to_freighter = false
|
||||||
|
|||||||
@ -561,19 +561,11 @@ func is_mini_boosting() -> bool:
|
|||||||
func _get_allowed_speed() -> float:
|
func _get_allowed_speed() -> float:
|
||||||
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))
|
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)
|
|
||||||
return engine != null and engine.definition.category == ItemDefinition.Category.ENGINE and "booster" in engine.special_orbs
|
|
||||||
|
|
||||||
func _update_mini_boost(delta: float) -> void:
|
func _update_mini_boost(delta: float) -> void:
|
||||||
_mini_boost_active = false
|
_mini_boost_active = false
|
||||||
if not Input.is_action_pressed("ship_mini_boost"):
|
if not Input.is_action_pressed("ship_mini_boost"):
|
||||||
_mini_boost_exhausted = false
|
_mini_boost_exhausted = false
|
||||||
return
|
return
|
||||||
if has_free_mini_boost() and not Input.is_action_pressed("ship_brake"):
|
|
||||||
_mini_boost_exhausted = false
|
|
||||||
_mini_boost_active = true
|
|
||||||
return
|
|
||||||
if _mini_boost_exhausted or Input.is_action_pressed("ship_brake"):
|
if _mini_boost_exhausted or Input.is_action_pressed("ship_brake"):
|
||||||
return
|
return
|
||||||
var cost := get_mini_boost_energy_cost() * delta
|
var cost := get_mini_boost_energy_cost() * delta
|
||||||
@ -584,7 +576,6 @@ func _update_mini_boost(delta: float) -> void:
|
|||||||
_mini_boost_active = true
|
_mini_boost_active = true
|
||||||
|
|
||||||
func get_mini_boost_energy_cost() -> float:
|
func get_mini_boost_energy_cost() -> float:
|
||||||
if has_free_mini_boost(): return 0.0
|
|
||||||
var engine: ItemInstance = $ShipInventory.get_item("module",2)
|
var engine: ItemInstance = $ShipInventory.get_item("module",2)
|
||||||
var base_cost := maxf(MINI_BOOST_ENERGY_PER_SECOND,max_energy*0.22)
|
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)
|
return base_cost * (0.5 if engine != null and "atomic_engine" in engine.special_orbs else 1.0)
|
||||||
|
|||||||
16
scripts/game/space_rewards.gd
Normal file
16
scripts/game/space_rewards.gd
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
extends RefCounted
|
||||||
|
const SAMPLE_CHANCE := 0.15
|
||||||
|
|
||||||
|
static func drop(enemy: Node2D) -> void:
|
||||||
|
if randf() < SAMPLE_CHANCE:
|
||||||
|
spawn(enemy,{"samples":1},Vector2(-38,30))
|
||||||
|
if int(enemy.get("loot_tier")) == 2 and GameState.active_distant_map.is_empty():
|
||||||
|
var profile := {"id":"boss:%d:%d" % [GameState.system_seed,randi()],"seed":randi(),"name":"Boss układu %d" % GameState.system_level}
|
||||||
|
spawn(enemy,{"map":preload("res://scripts/planets/distant_expeditions.gd").offer(profile,GameState.system_level)},Vector2(0,65))
|
||||||
|
|
||||||
|
static func spawn(enemy: Node2D, payload: Dictionary, offset: Vector2) -> void:
|
||||||
|
var pickup := Node2D.new()
|
||||||
|
pickup.set_script(preload("res://scripts/items/expedition_pickup.gd"))
|
||||||
|
pickup.payload = payload
|
||||||
|
pickup.position = enemy.position + offset
|
||||||
|
enemy.get_parent().call_deferred("add_child",pickup)
|
||||||
1
scripts/game/space_rewards.gd.uid
Normal file
1
scripts/game/space_rewards.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://cu0ahvnuikwcc
|
||||||
@ -14,7 +14,9 @@ func _ready() -> void:
|
|||||||
_label.size.x = 240
|
_label.size.x = 240
|
||||||
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
_label.add_theme_font_size_override("font_size",12)
|
_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 odległych systemów · poz. %d" % int(payload.map.level)
|
_label.text = preload("res://scripts/items/special_orbs.gd").NAMES[str(payload.orb)] if payload.has("orb") else "Próbka do analizy" if payload.has("samples") else "Ładunek surowców z bazy" if payload.has("materials") else "Mapa odległych systemów · poz. %d" % int(payload.map.level)
|
||||||
|
if payload.has("samples"): _icon = load("res://assets/icons/resources/uranium.png")
|
||||||
|
if payload.has("materials"): _icon = load("res://assets/icons/resources/copper.svg")
|
||||||
add_child(_label)
|
add_child(_label)
|
||||||
preload("res://scripts/items/loot_reveal_effect.gd").spawn(get_parent(),position,Color("b991ff"),3)
|
preload("res://scripts/items/loot_reveal_effect.gd").spawn(get_parent(),position,Color("b991ff"),3)
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
@ -30,6 +32,11 @@ func _physics_process(delta: float) -> void:
|
|||||||
var distance := global_position.distance_to(_player.global_position)
|
var distance := global_position.distance_to(_player.global_position)
|
||||||
if distance <= 48:
|
if distance <= 48:
|
||||||
var progress: Resource = GameState.captain.expedition
|
var progress: Resource = GameState.captain.expedition
|
||||||
|
if payload.has("samples"): progress.analysis_samples += int(payload.samples)
|
||||||
|
if payload.has("materials"):
|
||||||
|
for key in payload.materials:
|
||||||
|
GameState.captain.minerals[key] = int(GameState.captain.minerals.get(key,0)) + int(payload.materials[key])
|
||||||
|
GameState.captain.emit_changed()
|
||||||
if payload.has("map"): progress.distant_maps[str(payload.map.id)] = payload.map.duplicate(true)
|
if payload.has("map"): progress.distant_maps[str(payload.map.id)] = payload.map.duplicate(true)
|
||||||
if payload.has("orb"):
|
if payload.has("orb"):
|
||||||
var id := str(payload.orb)
|
var id := str(payload.orb)
|
||||||
|
|||||||
@ -246,7 +246,6 @@ func get_tooltip_text() -> String:
|
|||||||
lines.append(modifier.get_display_text())
|
lines.append(modifier.get_display_text())
|
||||||
for orb in special_orbs:
|
for orb in special_orbs:
|
||||||
if orb == "piercing": lines.append("\nORB PRZEBICIA · 50% obrażeń omija tarcze. Tylko ta broń.")
|
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"]:
|
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]])
|
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)
|
return "\n".join(lines)
|
||||||
|
|||||||
@ -218,6 +218,20 @@ func can_add_item(item: ItemInstance) -> bool:
|
|||||||
|
|
||||||
func migrate_orbs() -> void:
|
func migrate_orbs() -> void:
|
||||||
var progress: Resource = GameState.captain.expedition
|
var progress: Resource = GameState.captain.expedition
|
||||||
|
progress.orbs.erase("booster")
|
||||||
|
# Retired orbs and their applied effect are removed from legacy inventories.
|
||||||
|
for slots in [cargo,weapon_slots,module_slots,extension_slots]:
|
||||||
|
for index in slots.size():
|
||||||
|
var item: ItemInstance = slots[index]
|
||||||
|
if item == null: continue
|
||||||
|
item.special_orbs.erase("booster")
|
||||||
|
if item.definition != null and item.definition.orb_id == "booster": slots[index] = null
|
||||||
|
for stock in [progress.freighter_storage,progress.delivered_modules]:
|
||||||
|
for index in range(stock.size()-1,-1,-1):
|
||||||
|
var item: ItemInstance = stock[index]
|
||||||
|
if item == null: continue
|
||||||
|
item.special_orbs.erase("booster")
|
||||||
|
if item.definition != null and item.definition.orb_id == "booster": stock.remove_at(index)
|
||||||
for id in progress.orbs.keys():
|
for id in progress.orbs.keys():
|
||||||
var amount := int(progress.orbs[id])
|
var amount := int(progress.orbs[id])
|
||||||
if amount > 0 and add_item(preload("res://scripts/items/special_orbs.gd").create_item(id,amount)):
|
if amount > 0 and add_item(preload("res://scripts/items/special_orbs.gd").create_item(id,amount)):
|
||||||
@ -225,7 +239,7 @@ func migrate_orbs() -> void:
|
|||||||
|
|
||||||
func select_orb(index: int) -> bool:
|
func select_orb(index: int) -> bool:
|
||||||
var item := get_item("cargo",index)
|
var item := get_item("cargo",index)
|
||||||
if item == null or item.definition.orb_id.is_empty(): return false
|
if item == null or item.definition.orb_id not in preload("res://scripts/items/special_orbs.gd").IDS: return false
|
||||||
selected_orb = item
|
selected_orb = item
|
||||||
Input.set_default_cursor_shape(Input.CURSOR_CROSS)
|
Input.set_default_cursor_shape(Input.CURSOR_CROSS)
|
||||||
if get_node_or_null("OrbTargetCursor") == null:
|
if get_node_or_null("OrbTargetCursor") == null:
|
||||||
@ -477,6 +491,8 @@ func _add_starter_items() -> void:
|
|||||||
var item := ItemInstance.new()
|
var item := ItemInstance.new()
|
||||||
item.definition = definition
|
item.definition = definition
|
||||||
item.item_level = 1
|
item.item_level = 1
|
||||||
|
item.damage_min = 4.0
|
||||||
|
item.damage_max = 10.0
|
||||||
weapon_slots[0] = item
|
weapon_slots[0] = item
|
||||||
|
|
||||||
func _remove_extension_addons() -> void:
|
func _remove_extension_addons() -> void:
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
# Every orb is consumed once. Further advancement requires another orb.
|
# Every orb is consumed once. Further advancement requires another orb.
|
||||||
const IDS := ["piercing", "booster", "advancement", "replication", "electricity", "radiation", "kinetic", "atomic_engine", "polarization"]
|
const IDS := ["piercing", "advancement", "replication", "electricity", "radiation", "kinetic", "atomic_engine", "polarization"]
|
||||||
const COMMON_IDS := ["piercing", "booster", "advancement", "electricity", "radiation", "kinetic", "atomic_engine", "polarization"]
|
const COMMON_IDS := ["piercing", "advancement", "electricity", "radiation", "kinetic", "atomic_engine", "polarization"]
|
||||||
const DAMAGE_ORBS := ["electricity", "radiation", "kinetic"]
|
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 NAMES := {"piercing":"Orb przebicia", "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."}
|
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ę.", "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ń.", "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:
|
static func icon(id: String) -> Texture2D:
|
||||||
return load("res://assets/icons/orbs/%s.%s" % [id, "png" if id in ["piercing", "booster", "advancement"] else "svg"]) as Texture2D
|
if id not in IDS: return null
|
||||||
|
return load("res://assets/icons/orbs/%s.%s" % [id, "png" if id in ["piercing", "advancement"] else "svg"]) as Texture2D
|
||||||
static func create_item(id: String, amount := 1) -> ItemInstance:
|
static func create_item(id: String, amount := 1) -> ItemInstance:
|
||||||
if id not in IDS or amount <= 0: return null
|
if id not in IDS or amount <= 0: return null
|
||||||
var definition := ItemDefinition.new()
|
var definition := ItemDefinition.new()
|
||||||
@ -22,6 +23,7 @@ static func create_item(id: String, amount := 1) -> ItemInstance:
|
|||||||
return item
|
return item
|
||||||
|
|
||||||
static func count(progress: Resource, inventory: Node, id: String) -> int:
|
static func count(progress: Resource, inventory: Node, id: String) -> int:
|
||||||
|
if id not in IDS: return 0
|
||||||
var total := int(progress.orbs.get(id,0))
|
var total := int(progress.orbs.get(id,0))
|
||||||
if inventory != null:
|
if inventory != null:
|
||||||
for item in inventory.cargo.slice(0,inventory.unlocked_cargo_slots):
|
for item in inventory.cargo.slice(0,inventory.unlocked_cargo_slots):
|
||||||
|
|||||||
53
scripts/levels/enemy_resource_base.gd
Normal file
53
scripts/levels/enemy_resource_base.gd
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
extends StaticBody2D
|
||||||
|
const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
|
||||||
|
var base_id := ""
|
||||||
|
var reward_seed := 1
|
||||||
|
var level := 1
|
||||||
|
var kind := "enemy_base"
|
||||||
|
var hull := 100.0
|
||||||
|
var _claimed := false
|
||||||
|
var _label: Label
|
||||||
|
|
||||||
|
static func materials(seed_value: int, sector_level: int) -> Dictionary:
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = seed_value
|
||||||
|
var result := {}
|
||||||
|
for key in PROFILES.MATERIALS:
|
||||||
|
result[key] = rng.randi_range(18,28) * maxi(1,ceili(sqrt(maxi(1,sector_level))))
|
||||||
|
return result
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
add_to_group("exploration_pois")
|
||||||
|
collision_layer = 4
|
||||||
|
collision_mask = 0
|
||||||
|
hull = 100.0 * preload("res://scripts/levels/level_balance.gd").item_stat_scale(level)
|
||||||
|
var shape := CollisionShape2D.new()
|
||||||
|
var circle := CircleShape2D.new()
|
||||||
|
circle.radius = 85
|
||||||
|
shape.shape = circle
|
||||||
|
add_child(shape)
|
||||||
|
var sprite := Sprite2D.new()
|
||||||
|
sprite.texture = preload("res://assets/expeditions/abandoned_relay.png")
|
||||||
|
sprite.scale = Vector2.ONE * 190.0 / maxf(sprite.texture.get_width(),sprite.texture.get_height())
|
||||||
|
sprite.modulate = Color("ff9b88")
|
||||||
|
add_child(sprite)
|
||||||
|
_label = Label.new()
|
||||||
|
_label.position = Vector2(-170,100)
|
||||||
|
_label.size.x = 340
|
||||||
|
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_label.add_theme_font_size_override("font_size",12)
|
||||||
|
_label.text = "BAZA ZAOPATRZENIOWA WROGA\nZniszcz, aby uwolnić surowce"
|
||||||
|
add_child(_label)
|
||||||
|
|
||||||
|
func take_weapon_hit(amount: float, _critical := false, _damage_type := 0) -> void:
|
||||||
|
if _claimed: return
|
||||||
|
hull -= maxf(0,amount)
|
||||||
|
if hull > 0: return
|
||||||
|
_claimed = true
|
||||||
|
GameState.explored_pois[base_id] = true
|
||||||
|
var pickup := Node2D.new()
|
||||||
|
pickup.set_script(preload("res://scripts/items/expedition_pickup.gd"))
|
||||||
|
pickup.payload = {"materials":materials(reward_seed,level)}
|
||||||
|
pickup.position = position
|
||||||
|
get_parent().call_deferred("add_child",pickup)
|
||||||
|
queue_free()
|
||||||
1
scripts/levels/enemy_resource_base.gd.uid
Normal file
1
scripts/levels/enemy_resource_base.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://bvg6uj6e202hy
|
||||||
@ -27,7 +27,7 @@ static func map_cost(map: Dictionary) -> int:
|
|||||||
return maxi(1, int(map.level)) * CREDITS_PER_LEVEL
|
return maxi(1, int(map.level)) * CREDITS_PER_LEVEL
|
||||||
|
|
||||||
static func fuel_cost(map: Dictionary) -> int:
|
static func fuel_cost(map: Dictionary) -> int:
|
||||||
return maxi(1, int(map.level)) * DEUTER_PER_LEVEL
|
return 0 # Legacy API retained for the dormant surface mechanic.
|
||||||
|
|
||||||
static func buy(progress: Resource, captain: Resource, profile: Dictionary, source_level: int) -> bool:
|
static func buy(progress: Resource, captain: Resource, profile: Dictionary, source_level: int) -> bool:
|
||||||
if not has_hermit(profile): return false
|
if not has_hermit(profile): return false
|
||||||
|
|||||||
@ -18,33 +18,22 @@ static func build(screen: Control) -> void:
|
|||||||
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)
|
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
|
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)
|
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"))
|
MENU.label(section, "Lot i powrót są bezpłatne. Mapa zużywa się przy wylocie. Bazy wroga zawierają krzem, miedź, nikiel, wolfram i rtęć. Próbki: 15% szansy z każdego statku wroga.", 12, MENU.GOLD)
|
||||||
fuel.name = "ExpeditionFuel"
|
|
||||||
screen._economy_labels["expedition_fuel"] = fuel
|
|
||||||
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():
|
if progress.distant_maps.is_empty():
|
||||||
MENU.label(section, "Brak sektorów do zbadania. Szukaj pustelnika na planecie lub map w łupach z wypraw.", 12, MENU.MUTED)
|
MENU.label(section, "Brak sektorów do zbadania. Pokonaj bossa w kosmosie i zbierz jego mapę.", 12, MENU.MUTED)
|
||||||
var maps: Array = progress.distant_maps.values()
|
var maps: Array = progress.distant_maps.values()
|
||||||
maps.sort_custom(func(a,b): return int(a.level)<int(b.level))
|
maps.sort_custom(func(a,b): return int(a.level)<int(b.level))
|
||||||
for map in maps:
|
for map in maps:
|
||||||
var card := MENU.card(section)
|
var card := MENU.card(section)
|
||||||
MENU.label(card, "%s / POZIOM %d" % [map.name,map.level], 14, MENU.GOLD)
|
MENU.label(card, "%s / POZIOM %d" % [map.name,map.level], 14, MENU.GOLD)
|
||||||
MENU.label(card, "Źródło: %s · poziom %d" % [map.source_planet,map.source_level], 11, MENU.MUTED)
|
MENU.label(card, "Źródło: %s · poziom %d" % [map.source_planet,map.source_level], 11, MENU.MUTED)
|
||||||
var cost: int = DATA.fuel_cost(map)
|
|
||||||
MENU.label(card, "Paliwo: %d deuteru" % cost, 12, Color("95dce8"))
|
|
||||||
var row := MENU.row(card)
|
var row := MENU.row(card)
|
||||||
var status := MENU.label(row,"",11)
|
var status := MENU.label(row,"Gotowa do lotu · bez kosztu paliwa",11)
|
||||||
status.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
status.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
status.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
var map_id := str(map.id)
|
var map_id := str(map.id)
|
||||||
var fly := MENU.button(row,"LEĆ",func(): screen._travel_to_distant(map_id),true)
|
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.name = "FlyDistant_"+map_id.md5_text().substr(0,8)
|
||||||
fly.tooltip_text = "Jednorazowa wyprawa. Zużywa mapę i pobiera %d deuteru." % cost
|
fly.custom_minimum_size.x = 80
|
||||||
var refresh_status := func():
|
fly.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
var missing := maxf(0.0,cost-progress.deuter)
|
fly.tooltip_text = "Jednorazowa wyprawa. Zużywa mapę."
|
||||||
status.text = "Paliwo gotowe" if missing <= 0 else "Brakuje %.1f deuteru" % missing
|
|
||||||
fly.disabled = missing > 0
|
|
||||||
refresh_status.call()
|
|
||||||
progress.changed.connect(refresh_status)
|
|
||||||
status.tree_exiting.connect(func():
|
|
||||||
if progress.changed.is_connected(refresh_status): progress.changed.disconnect(refresh_status)
|
|
||||||
, CONNECT_ONE_SHOT)
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ extends Node
|
|||||||
const UI := preload("res://scripts/planets/expedition_ui.gd")
|
const UI := preload("res://scripts/planets/expedition_ui.gd")
|
||||||
const CONTACT := preload("res://scripts/ui/orbital_contact_style.gd")
|
const CONTACT := preload("res://scripts/ui/orbital_contact_style.gd")
|
||||||
const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
|
const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
|
||||||
|
const PLANET_LANDING_ENABLED := false
|
||||||
const FREIGHTER_SIZE := 285.0
|
const FREIGHTER_SIZE := 285.0
|
||||||
const CONTACT_RANGE := preload("res://scripts/planets/orbital_contact_range.gd")
|
const CONTACT_RANGE := preload("res://scripts/planets/orbital_contact_range.gd")
|
||||||
var game: Node2D
|
var game: Node2D
|
||||||
@ -94,7 +95,7 @@ func _refresh_prompt() -> void:
|
|||||||
_near_planet = null
|
_near_planet = null
|
||||||
var best := INF
|
var best := INF
|
||||||
for planet in get_tree().get_nodes_in_group("scanner_planets"):
|
for planet in get_tree().get_nodes_in_group("scanner_planets"):
|
||||||
if not bool(planet.get("earthlike")):
|
if not PLANET_LANDING_ENABLED or not bool(planet.get("earthlike")):
|
||||||
continue
|
continue
|
||||||
var distance: float = player.global_position.distance_to(planet.global_position) - float(planet.get("radius"))
|
var distance: float = player.global_position.distance_to(planet.global_position) - float(planet.get("radius"))
|
||||||
if CONTACT_RANGE.at_planet(player,planet) and distance < best:
|
if CONTACT_RANGE.at_planet(player,planet) and distance < best:
|
||||||
@ -127,7 +128,7 @@ func _refresh_prompt() -> void:
|
|||||||
elif nearby_freighter:
|
elif nearby_freighter:
|
||||||
_contact_label.text = "● FREGATA / POŁĄCZENIE GOTOWE"
|
_contact_label.text = "● FREGATA / POŁĄCZENIE GOTOWE"
|
||||||
text = "FRACHTOWIEC\nHangar · Warsztat · Magazyn"
|
text = "FRACHTOWIEC\nHangar · Warsztat · Magazyn"
|
||||||
_prompt.tooltip_text = "Zadokuj, aby naprawić lądownik, rozbudować statek i przenieść wyposażenie."
|
_prompt.tooltip_text = "Zadokuj, aby rozbudować statek, rozpocząć daleką wyprawę i przenieść wyposażenie."
|
||||||
else:
|
else:
|
||||||
var profile: Dictionary = _near_planet.planet_profile
|
var profile: Dictionary = _near_planet.planet_profile
|
||||||
_contact_label.text = "● TELEMETRIA / DANE PLANETY"
|
_contact_label.text = "● TELEMETRIA / DANE PLANETY"
|
||||||
@ -193,7 +194,7 @@ func land_nearby() -> void:
|
|||||||
if CONTACT_RANGE.at_freighter(player,freighter):
|
if CONTACT_RANGE.at_freighter(player,freighter):
|
||||||
open_freighter()
|
open_freighter()
|
||||||
return
|
return
|
||||||
if not is_instance_valid(_near_planet):
|
if not PLANET_LANDING_ENABLED or not is_instance_valid(_near_planet):
|
||||||
return
|
return
|
||||||
var progress: Resource = GameState.captain.expedition
|
var progress: Resource = GameState.captain.expedition
|
||||||
if not progress.begin_visit(_near_planet.planet_profile):
|
if not progress.begin_visit(_near_planet.planet_profile):
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
const MENU := preload("res://scripts/planets/freighter_menu.gd")
|
const MENU := preload("res://scripts/planets/freighter_menu.gd")
|
||||||
const NETWORK := preload("res://scripts/planets/mining_network.gd")
|
|
||||||
|
|
||||||
static func build(screen: Control) -> void:
|
static func build(screen: Control) -> void:
|
||||||
var progress: Resource = screen.captain.expedition
|
|
||||||
var navigation := MENU.card(screen._content)
|
var navigation := MENU.card(screen._content)
|
||||||
MENU.label(navigation, "NAWIGACJA / ODKRYTE UKŁADY", 14, MENU.GOLD)
|
MENU.label(navigation, "NAWIGACJA / ODKRYTE UKŁADY", 14, MENU.GOLD)
|
||||||
MENU.label(navigation, "Obecny: System %d · Najdalszy odkryty: %d" % [GameState.system_level, GameState.highest_system], 12)
|
MENU.label(navigation, "Obecny: System %d · Najdalszy odkryty: %d" % [GameState.system_level, GameState.highest_system], 12)
|
||||||
@ -24,53 +22,8 @@ static func build(screen: Control) -> void:
|
|||||||
destination.item_selected.connect(func(_index): travel.disabled = not GameState.active_distant_map.is_empty() or 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)
|
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)
|
preload("res://scripts/planets/distant_expeditions_panel.gd").build(screen)
|
||||||
var summary := MENU.card(screen._content)
|
|
||||||
MENU.label(summary, "SIEĆ WYDOBYWCZA", 14, MENU.GOLD)
|
|
||||||
var stock := MENU.label(summary, "", 13, Color("95dce8"))
|
|
||||||
screen._economy_labels["network"] = stock
|
|
||||||
MENU.label(summary, "Wydobycie: zasobność × (1 + zarządzanie / 100) deuteru/min. Bez załoganta: 0. Złoża nie wyczerpują się.", 11, MENU.MUTED)
|
|
||||||
MENU.label(summary, "Jeden załogant obsługuje jedną placówkę. Nowy przydział przenosi go z poprzedniej.", 11, MENU.MUTED)
|
|
||||||
if progress.mining_stations.is_empty():
|
|
||||||
MENU.label(summary, "Brak placówek. Szukaj głębokich złóż na planetach (szansa 60%). Budowa: zasobność × 10 części.", 12)
|
|
||||||
for station_id in progress.mining_stations:
|
|
||||||
var station: Dictionary = progress.mining_stations[station_id]
|
|
||||||
var card := MENU.card(screen._content)
|
|
||||||
MENU.label(card, "%s · SYSTEM %d" % [station.planet_name, station.system], 13, MENU.GOLD)
|
|
||||||
MENU.label(card, "Zasobność: %d / 100" % NETWORK.station_richness(station), 12, Color("95dce8"))
|
|
||||||
var member: Dictionary = progress.crew.get(str(station.get("crew_id", "")), {})
|
|
||||||
MENU.label(card, "Czeka na załoganta" if member.is_empty() else "%s · zarządzanie %d/100 · %.1f deuteru/min" % [member.name, member.management, NETWORK.station_rate(progress, station)], 12)
|
|
||||||
var assign_row := MENU.row(card)
|
|
||||||
var option := OptionButton.new()
|
|
||||||
option.name = "StationCrew_" + str(station_id).md5_text().substr(0, 8)
|
|
||||||
option.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
option.fit_to_longest_item = false
|
|
||||||
option.add_theme_font_size_override("font_size", 11)
|
|
||||||
option.add_theme_font_override("font", ThemeDB.fallback_font)
|
|
||||||
option.add_item("Bez przydziału")
|
|
||||||
option.set_item_metadata(0, "")
|
|
||||||
for crew_id in progress.crew:
|
|
||||||
var person: Dictionary = progress.crew[crew_id]
|
|
||||||
option.add_item("%s · %d/100" % [person.name, person.management])
|
|
||||||
option.set_item_metadata(option.item_count - 1, crew_id)
|
|
||||||
if str(station.get("crew_id", "")) == str(crew_id): option.select(option.item_count - 1)
|
|
||||||
assign_row.add_child(option)
|
|
||||||
var apply := MENU.button(assign_row, "PRZYDZIEL", screen._assign_station.bind(str(station_id), option))
|
|
||||||
apply.disabled = progress.crew.is_empty() and member.is_empty()
|
|
||||||
if progress.crew.is_empty(): MENU.label(screen._content, "Brak załogantów. Przygotuj kajutę i zatrudnij specjalistę spotkanego na planecie.", 12, MENU.MUTED)
|
|
||||||
screen._refresh_live_labels()
|
screen._refresh_live_labels()
|
||||||
|
|
||||||
static func cabin(screen: Control) -> void:
|
static func cabin(screen: Control) -> void:
|
||||||
var progress: Resource = screen.captain.expedition
|
MENU.label(screen._content, "SEKCJA REZERWOWA", 14, MENU.GOLD)
|
||||||
var member := NETWORK.cabin_crew(progress, screen._tab)
|
MENU.label(screen._content, "Pomieszczenie czeka na nowe przeznaczenie.", 12)
|
||||||
var card := MENU.card(screen._content)
|
|
||||||
if member.is_empty():
|
|
||||||
MENU.label(card, "WOLNA KAJUTA", 14, MENU.GOLD)
|
|
||||||
MENU.label(card, "Przygotowana dla jednego załoganta. Specjalistów możesz zatrudniać na planetach.", 12)
|
|
||||||
return
|
|
||||||
MENU.label(card, str(member.name), 15, MENU.GOLD)
|
|
||||||
MENU.label(card, "Zarządzanie: %d / 100\nPremia do wydobycia: +%d%%\nPochodzenie: %s" % [member.management, member.management, member.planet_name], 12)
|
|
||||||
var assignment := "Bez przydziału"
|
|
||||||
for station in progress.mining_stations.values():
|
|
||||||
if str(station.get("crew_id", "")) == str(member.id): assignment = "%s · System %d" % [station.planet_name, station.system]
|
|
||||||
MENU.label(card, "Placówka: " + assignment, 12, Color("95dce8"))
|
|
||||||
MENU.button(card, "ZWOLNIJ ZAŁOGANTA", screen._dismiss_crew.bind(str(member.id)))
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ signal section_selected(section: String)
|
|||||||
signal section_hovered(section: String)
|
signal section_hovered(section: String)
|
||||||
const DESIGN_SIZE := Vector2(1792,768)
|
const DESIGN_SIZE := Vector2(1792,768)
|
||||||
const ROOMS := [
|
const ROOMS := [
|
||||||
{"id":"command","title":"CENTRUM DOWODZENIA","status":"NAWIGACJA / PLACÓWKI","rect":Rect2(25,165,315,435),"tag":Vector2(88,68),"active":false},
|
{"id":"command","title":"CENTRUM DOWODZENIA","status":"NAWIGACJA / WYPRAWY","rect":Rect2(25,165,315,435),"tag":Vector2(88,68),"active":false},
|
||||||
{"id":"workshop","title":"WARSZTAT","status":"OPERACYJNY","rect":Rect2(360,120,255,205),"tag":Vector2(360,58),"active":true},
|
{"id":"workshop","title":"WARSZTAT","status":"OPERACYJNY","rect":Rect2(360,120,255,205),"tag":Vector2(360,58),"active":true},
|
||||||
{"id":"expansion_1","title":"SEKCJA 02","status":"W REMONCIE","rect":Rect2(632,120,200,205),"tag":Vector2(632,58),"active":false},
|
{"id":"expansion_1","title":"SEKCJA 02","status":"W REMONCIE","rect":Rect2(632,120,200,205),"tag":Vector2(632,58),"active":false},
|
||||||
{"id":"expansion_2","title":"SEKCJA 03","status":"W REMONCIE","rect":Rect2(848,120,214,205),"tag":Vector2(848,58),"active":false},
|
{"id":"expansion_2","title":"SEKCJA 03","status":"W REMONCIE","rect":Rect2(848,120,214,205),"tag":Vector2(848,58),"active":false},
|
||||||
@ -53,7 +53,7 @@ func room(id: String) -> Dictionary:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
func is_buildable(id: String) -> bool:
|
func is_buildable(id: String) -> bool:
|
||||||
return ROOM_PLANS.has(id) or id in ["cabin_1","cabin_2","cabin_3","cabin_4","cabin_5"]
|
return ROOM_PLANS.has(id)
|
||||||
|
|
||||||
func plan(id: String) -> Dictionary:
|
func plan(id: String) -> Dictionary:
|
||||||
if ROOM_PLANS.has(id): return ROOM_PLANS[id]
|
if ROOM_PLANS.has(id): return ROOM_PLANS[id]
|
||||||
|
|||||||
@ -17,6 +17,9 @@ var _owned_open := false
|
|||||||
var _storage_columns: GridContainer
|
var _storage_columns: GridContainer
|
||||||
var _storage_notice := ""
|
var _storage_notice := ""
|
||||||
var _economy_labels: Dictionary = {}
|
var _economy_labels: Dictionary = {}
|
||||||
|
var _engineer_item: ItemInstance
|
||||||
|
var _engineer_addon := 0
|
||||||
|
var _engineer_notice := ""
|
||||||
var _alien_timer: Label
|
var _alien_timer: Label
|
||||||
var _alien_signature := ""
|
var _alien_signature := ""
|
||||||
@onready var deck: Control = $Deck
|
@onready var deck: Control = $Deck
|
||||||
@ -26,6 +29,7 @@ func _ready() -> void:
|
|||||||
offset_top = preload("res://scripts/ui/resource_bar.gd").HEIGHT
|
offset_top = preload("res://scripts/ui/resource_bar.gd").HEIGHT
|
||||||
if captain == null:
|
if captain == null:
|
||||||
captain = GameState.captain
|
captain = GameState.captain
|
||||||
|
captain.expedition.freighter_rooms["expansion_1"] = "laboratory"
|
||||||
captain.expedition.freighter_rooms["expansion_3"] = "module_storage"
|
captain.expedition.freighter_rooms["expansion_3"] = "module_storage"
|
||||||
captain.expedition.changed.connect(_refresh_live_labels)
|
captain.expedition.changed.connect(_refresh_live_labels)
|
||||||
deck.sync_rooms(captain.expedition.freighter_rooms)
|
deck.sync_rooms(captain.expedition.freighter_rooms)
|
||||||
@ -180,7 +184,7 @@ func _label(text: String, size_value := 14, color := Color("c5d3d7")) -> Label:
|
|||||||
func _refresh() -> void:
|
func _refresh() -> void:
|
||||||
_economy_labels.clear()
|
_economy_labels.clear()
|
||||||
_storage_columns = null
|
_storage_columns = null
|
||||||
_audio.set_maintenance_needed(captain.expedition.lander_hp < 30)
|
_audio.set_maintenance_needed(false)
|
||||||
for child in _content.get_children():
|
for child in _content.get_children():
|
||||||
_content.remove_child(child)
|
_content.remove_child(child)
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
@ -279,7 +283,6 @@ func _construct_room(id: String) -> void:
|
|||||||
_refresh()
|
_refresh()
|
||||||
|
|
||||||
func _build_hangar() -> void:
|
func _build_hangar() -> void:
|
||||||
var progress: Resource = captain.expedition
|
|
||||||
MENU.hero(_content,"hangar","")
|
MENU.hero(_content,"hangar","")
|
||||||
var ship_row := MENU.row(_content)
|
var ship_row := MENU.row(_content)
|
||||||
MENU.icon(ship_row,deck.ship_texture,32)
|
MENU.icon(ship_row,deck.ship_texture,32)
|
||||||
@ -288,51 +291,7 @@ func _build_hangar() -> void:
|
|||||||
ship_row.add_child(ship_text)
|
ship_row.add_child(ship_text)
|
||||||
MENU.label(ship_text,deck.ship_name,15,MENU.GOLD)
|
MENU.label(ship_text,deck.ship_name,15,MENU.GOLD)
|
||||||
MENU.label(ship_text,"Żelazo %d · Części %d" % [int(captain.minerals.get("iron",0)),int(captain.parts)],12,MENU.MUTED)
|
MENU.label(ship_text,"Żelazo %d · Części %d" % [int(captain.minerals.get("iron",0)),int(captain.parts)],12,MENU.MUTED)
|
||||||
var service := MENU.card(_content)
|
|
||||||
MENU.style(service.get_parent(),8,0.70)
|
|
||||||
var row := MENU.row(service)
|
|
||||||
MENU.icon(row,preload("res://assets/planets/lander.png"),34)
|
|
||||||
var info := VBoxContainer.new()
|
|
||||||
info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
row.add_child(info)
|
|
||||||
MENU.label(info,"Lądownik · %d%%" % ceili(progress.lander_hp),13,MENU.GOLD)
|
|
||||||
var bar := ProgressBar.new()
|
|
||||||
bar.value = progress.lander_hp
|
|
||||||
bar.show_percentage = false
|
|
||||||
bar.custom_minimum_size.y = 5
|
|
||||||
var fill := StyleBoxFlat.new()
|
|
||||||
fill.bg_color = Color("85b7a0")
|
|
||||||
fill.set_corner_radius_all(3)
|
|
||||||
bar.add_theme_stylebox_override("fill",fill)
|
|
||||||
info.add_child(bar)
|
|
||||||
var cost: int = progress.repair_cost()
|
|
||||||
var cost_row := MENU.row(info)
|
|
||||||
MENU.icon(cost_row,RECIPES.icon("parts"),14)
|
|
||||||
var repair_cost := MENU.label(cost_row,str(cost),11,MENU.MUTED)
|
|
||||||
repair_cost.name = "RepairCost"
|
|
||||||
repair_cost.autowrap_mode = TextServer.AUTOWRAP_OFF
|
|
||||||
var repair := MENU.button(row,"Napraw",func():
|
|
||||||
if progress.repair(captain): _audio.service_completed(0.65)
|
|
||||||
_refresh(),true)
|
|
||||||
repair.name = "RepairLander"
|
|
||||||
repair.custom_minimum_size = Vector2(58,26)
|
|
||||||
repair.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
|
||||||
repair.tooltip_text = "Napraw do 100%% · koszt %d części. Pełny kadłub wymagany do lądowania." % cost
|
|
||||||
repair.disabled = cost == 0 or captain.parts < cost
|
|
||||||
_build_slot_upgrades()
|
_build_slot_upgrades()
|
||||||
var delivery := MENU.card(_content)
|
|
||||||
MENU.style(delivery.get_parent(),8,0.70)
|
|
||||||
var delivery_row := MENU.row(delivery)
|
|
||||||
MENU.icon(delivery_row,preload("res://assets/icons/shield.png"),24)
|
|
||||||
var label := MENU.label(delivery_row,"Odzyskane moduły · %d" % progress.delivered_modules.size(),12,MENU.MUTED)
|
|
||||||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
var take := MENU.button(delivery_row,"Odbierz",func():
|
|
||||||
if is_instance_valid(inventory): progress.deliver_modules(inventory)
|
|
||||||
_refresh())
|
|
||||||
take.name = "CollectRecovered"
|
|
||||||
take.custom_minimum_size = Vector2(58,26)
|
|
||||||
take.tooltip_text = "Odbierz do ładowni statku. Przedmioty bez miejsca pozostają na fregacie."
|
|
||||||
take.disabled = progress.delivered_modules.is_empty() or not is_instance_valid(inventory)
|
|
||||||
|
|
||||||
func _build_slot_upgrades() -> void:
|
func _build_slot_upgrades() -> void:
|
||||||
if not is_instance_valid(inventory):
|
if not is_instance_valid(inventory):
|
||||||
@ -385,18 +344,6 @@ func _unlock_ship_slot(kind: String) -> void:
|
|||||||
|
|
||||||
func _build_storage() -> void:
|
func _build_storage() -> void:
|
||||||
MENU.hero(_content,"storage","MAGAZYN / ZASOBY I WYPOSAŻENIE")
|
MENU.hero(_content,"storage","MAGAZYN / ZASOBY I WYPOSAŻENIE")
|
||||||
var deuter_card := MENU.card(_content)
|
|
||||||
var deuter_row := MENU.row(deuter_card)
|
|
||||||
MENU.icon(deuter_row, load("res://assets/icons/resources/deuter.svg"), 26)
|
|
||||||
var deuter_title := MENU.label(deuter_row, "DEUTER / PALIWO FREGATY", 13, Color("95dce8"))
|
|
||||||
deuter_title.autowrap_mode = TextServer.AUTOWRAP_OFF
|
|
||||||
deuter_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
var deuter_amount := MENU.label(deuter_row, "%.1f" % captain.expedition.deuter, 15, MENU.GOLD)
|
|
||||||
deuter_amount.autowrap_mode = TextServer.AUTOWRAP_OFF
|
|
||||||
deuter_amount.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
|
||||||
deuter_amount.custom_minimum_size.x = 80
|
|
||||||
_economy_labels["stock_header"] = deuter_amount
|
|
||||||
MENU.label(deuter_card, "Automatyczne dostawy z obsadzonych placówek · %.0f / min" % preload("res://scripts/planets/mining_network.gd").total_rate(captain.expedition), 11, MENU.MUTED)
|
|
||||||
if not deck.is_built("expansion_3"):
|
if not deck.is_built("expansion_3"):
|
||||||
_build_resource_overview()
|
_build_resource_overview()
|
||||||
var construction := MENU.card(_content)
|
var construction := MENU.card(_content)
|
||||||
@ -476,7 +423,6 @@ func _build_resource_overview() -> void:
|
|||||||
for key in ["ammunition","missile","irradiated_ammunition","energy_bank"]: values[key] = inventory.get_crafted_stock(key)
|
for key in ["ammunition","missile","irradiated_ammunition","energy_bank"]: values[key] = inventory.get_crafted_stock(key)
|
||||||
values.time_crystals = captain.expedition.time_crystals
|
values.time_crystals = captain.expedition.time_crystals
|
||||||
values.samples = captain.expedition.analysis_samples
|
values.samples = captain.expedition.analysis_samples
|
||||||
values.deuter = captain.expedition.deuter
|
|
||||||
var names: Dictionary = RECIPES.NAMES.duplicate()
|
var names: Dictionary = RECIPES.NAMES.duplicate()
|
||||||
names.merge(GEAR.MATERIAL_NAMES)
|
names.merge(GEAR.MATERIAL_NAMES)
|
||||||
names.merge({"credits":"Kredyty","parts":"Części","ammunition":"Amunicja","missile":"Rakiety","irradiated_ammunition":"Radiacyjna","energy_bank":"Powerbanki","samples":"Próbki","deuter":"Deuter","time_crystals":"Kryształy czasu"})
|
names.merge({"credits":"Kredyty","parts":"Części","ammunition":"Amunicja","missile":"Rakiety","irradiated_ammunition":"Radiacyjna","energy_bank":"Powerbanki","samples":"Próbki","deuter":"Deuter","time_crystals":"Kryształy czasu"})
|
||||||
@ -557,8 +503,10 @@ func _craft_supply(id: String, batches := 1) -> void:
|
|||||||
func _build_workshop() -> void:
|
func _build_workshop() -> void:
|
||||||
MENU.hero(_content,"workshop","")
|
MENU.hero(_content,"workshop","")
|
||||||
_build_supply_production()
|
_build_supply_production()
|
||||||
preload("res://scripts/planets/orb_workshop.gd").build(self)
|
MENU.label(_content,"Modyfikacja wyposażenia: kliknij orb w ładowni, a następnie przedmiot. PPM lub Esc anuluje wybór.",12,MENU.MUTED)
|
||||||
|
preload("res://scripts/planets/time_engineer_panel.gd").build(self,_engineer_notice,true)
|
||||||
_label("EKWIPUNEK KAPITANA",13,MENU.GOLD)
|
_label("EKWIPUNEK KAPITANA",13,MENU.GOLD)
|
||||||
|
MENU.label(_content,"Sprzęt planetarny zachowano na przyszłość. Obecnie jego bonusy do eksploracji powierzchni nie mają zastosowania.",12,MENU.MUTED)
|
||||||
var grid := GridContainer.new()
|
var grid := GridContainer.new()
|
||||||
grid.columns = 3
|
grid.columns = 3
|
||||||
grid.add_theme_constant_override("h_separation",6)
|
grid.add_theme_constant_override("h_separation",6)
|
||||||
@ -616,13 +564,6 @@ func _refresh_live_labels() -> void:
|
|||||||
if _tab == "expansion_2" and is_instance_valid(_drawer) and _drawer.visible and alien_signature != _alien_signature:
|
if _tab == "expansion_2" and is_instance_valid(_drawer) and _drawer.visible and alien_signature != _alien_signature:
|
||||||
_alien_signature = alien_signature
|
_alien_signature = alien_signature
|
||||||
_refresh.call_deferred()
|
_refresh.call_deferred()
|
||||||
for key in _economy_labels:
|
|
||||||
var label: Label = _economy_labels[key]
|
|
||||||
if not is_instance_valid(label): continue
|
|
||||||
if key == "expedition_fuel":
|
|
||||||
label.text = "Zapas paliwa: %.1f deuteru" % captain.expedition.deuter
|
|
||||||
continue
|
|
||||||
label.text = "Deuter: %.1f · Produkcja: %.0f/min" % [captain.expedition.deuter, preload("res://scripts/planets/mining_network.gd").total_rate(captain.expedition)] if key == "network" else "%.1f" % captain.expedition.deuter
|
|
||||||
|
|
||||||
func _craft(kind: String) -> void:
|
func _craft(kind: String) -> void:
|
||||||
if _tab != "workshop": return
|
if _tab != "workshop": return
|
||||||
@ -655,3 +596,13 @@ func _research_completed() -> void:
|
|||||||
_audio.service_completed()
|
_audio.service_completed()
|
||||||
captain.emit_changed()
|
captain.emit_changed()
|
||||||
_refresh()
|
_refresh()
|
||||||
|
|
||||||
|
func _maximize_addon() -> void:
|
||||||
|
if _tab != "workshop" or not is_instance_valid(inventory): return
|
||||||
|
if preload("res://scripts/planets/time_engineer.gd").maximize(captain.expedition,inventory,_engineer_item,_engineer_addon):
|
||||||
|
_engineer_notice = "✓ Addon zmaksymalizowany"
|
||||||
|
captain.emit_changed()
|
||||||
|
_audio.service_completed()
|
||||||
|
else:
|
||||||
|
_engineer_notice = "Sprawdź kryształy i wybrany addon."
|
||||||
|
preload("res://scripts/planets/time_engineer_panel.gd").refresh(self)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
const DURATION := 600.0
|
const DURATION := 0.0
|
||||||
const IDS := [
|
const IDS := [
|
||||||
"gamma_cannon", "beam_laser", "machine_gun", "missile_launcher", "unguided_missile",
|
"gamma_cannon", "beam_laser", "machine_gun", "missile_launcher", "unguided_missile",
|
||||||
"incendiary_missile", "radiation_laser", "artillery_cannon", "external_turret", "pulse_laser",
|
"incendiary_missile", "radiation_laser", "artillery_cannon", "external_turret", "pulse_laser",
|
||||||
@ -37,6 +37,7 @@ static func definition(id: String) -> ItemDefinition:
|
|||||||
return load("res://resources/items/%s.tres" % id) as ItemDefinition if id in IDS else null
|
return load("res://resources/items/%s.tres" % id) as ItemDefinition if id in IDS else null
|
||||||
|
|
||||||
static func ensure_offers(progress: Resource) -> void:
|
static func ensure_offers(progress: Resource) -> void:
|
||||||
|
if not progress.laboratory_project.is_empty(): advance(progress,0.0)
|
||||||
if not progress.laboratory_offers.is_empty() or not progress.laboratory_project.is_empty(): return
|
if not progress.laboratory_offers.is_empty() or not progress.laboratory_project.is_empty(): return
|
||||||
var pool := IDS.duplicate()
|
var pool := IDS.duplicate()
|
||||||
for id in progress.laboratory_unlocks: pool.erase(id)
|
for id in progress.laboratory_unlocks: pool.erase(id)
|
||||||
@ -54,23 +55,27 @@ static func can_start(captain: Resource, id: String) -> bool:
|
|||||||
static func start(captain: Resource, id: String) -> bool:
|
static func start(captain: Resource, id: String) -> bool:
|
||||||
if not can_start(captain,id): return false
|
if not can_start(captain,id): return false
|
||||||
for key in COSTS[id]: captain.minerals[key] = int(captain.minerals.get(key,0))-int(COSTS[id][key])
|
for key in COSTS[id]: captain.minerals[key] = int(captain.minerals.get(key,0))-int(COSTS[id][key])
|
||||||
captain.expedition.laboratory_project = id
|
var progress: Resource = captain.expedition
|
||||||
captain.expedition.laboratory_duration = research_duration(captain)
|
progress.laboratory_unlocks[id] = true
|
||||||
captain.expedition.laboratory_remaining = captain.expedition.laboratory_duration
|
progress.laboratory_duration = 0.0
|
||||||
|
progress.laboratory_remaining = 0.0
|
||||||
|
progress.laboratory_offers.clear()
|
||||||
|
ensure_offers(progress)
|
||||||
captain.emit_changed()
|
captain.emit_changed()
|
||||||
captain.expedition.emit_changed()
|
captain.expedition.emit_changed()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
static func research_duration(captain: Resource) -> float:
|
static func research_duration(_captain: Resource) -> float:
|
||||||
return DURATION * (1.0 - captain.get_skill_bonus("scientist"))
|
return 0.0
|
||||||
|
|
||||||
static func advance(progress: Resource, delta: float) -> bool:
|
static func advance(progress: Resource, _delta: float) -> bool:
|
||||||
if progress.laboratory_project.is_empty() or delta <= 0.0: return false
|
# Finish an already-paid project restored from an older save immediately.
|
||||||
progress.laboratory_remaining = maxf(0.0,progress.laboratory_remaining-delta)
|
if progress.laboratory_project.is_empty(): return false
|
||||||
if progress.laboratory_remaining <= 0.00001:
|
if progress.laboratory_project in IDS:
|
||||||
progress.laboratory_remaining = 0.0
|
|
||||||
progress.laboratory_unlocks[progress.laboratory_project] = true
|
progress.laboratory_unlocks[progress.laboratory_project] = true
|
||||||
progress.laboratory_project = ""
|
progress.laboratory_project = ""
|
||||||
|
progress.laboratory_remaining = 0.0
|
||||||
|
progress.laboratory_duration = 0.0
|
||||||
progress.laboratory_offers.clear()
|
progress.laboratory_offers.clear()
|
||||||
ensure_offers(progress)
|
ensure_offers(progress)
|
||||||
progress.emit_changed()
|
progress.emit_changed()
|
||||||
|
|||||||
@ -3,8 +3,6 @@ const LAB := preload("res://scripts/planets/laboratory.gd")
|
|||||||
const MENU := preload("res://scripts/planets/freighter_menu.gd")
|
const MENU := preload("res://scripts/planets/freighter_menu.gd")
|
||||||
var captain: Resource
|
var captain: Resource
|
||||||
var _signature := ""
|
var _signature := ""
|
||||||
var _timer: Label
|
|
||||||
var _bar: ProgressBar
|
|
||||||
var _buttons: Dictionary = {}
|
var _buttons: Dictionary = {}
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@ -12,13 +10,10 @@ func _ready() -> void:
|
|||||||
_build()
|
_build()
|
||||||
|
|
||||||
func _state() -> String:
|
func _state() -> String:
|
||||||
return str(captain.expedition.laboratory_project)+str(captain.expedition.laboratory_unlocks)+str(captain.get_skill_rank("scientist"))
|
return str(captain.expedition.laboratory_unlocks)+str(captain.expedition.laboratory_offers)
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
if _signature != _state(): _build()
|
if _signature != _state(): _build()
|
||||||
if is_instance_valid(_timer):
|
|
||||||
_timer.text = "Pozostało: %d s gry" % ceili(captain.expedition.laboratory_remaining)
|
|
||||||
_bar.value = captain.expedition.laboratory_duration-captain.expedition.laboratory_remaining
|
|
||||||
for id in _buttons:
|
for id in _buttons:
|
||||||
if is_instance_valid(_buttons[id]): _buttons[id].disabled = not LAB.can_start(captain,id)
|
if is_instance_valid(_buttons[id]): _buttons[id].disabled = not LAB.can_start(captain,id)
|
||||||
|
|
||||||
@ -27,23 +22,11 @@ func _build() -> void:
|
|||||||
for child in get_children():
|
for child in get_children():
|
||||||
remove_child(child)
|
remove_child(child)
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
_timer = null
|
|
||||||
_buttons.clear()
|
_buttons.clear()
|
||||||
_signature = _state()
|
_signature = _state()
|
||||||
var progress: Resource = captain.expedition
|
var progress: Resource = captain.expedition
|
||||||
MENU.label(self,"PROJEKTY BRONI I ROZSZERZEŃ",15,MENU.GOLD)
|
MENU.label(self,"PROJEKTY BRONI I ROZSZERZEŃ",15,MENU.GOLD)
|
||||||
MENU.label(self,"Wybierz projekt. Nowe badanie trwa %d sekund gry. Odkryta broń lub rozszerzenie zacznie pojawiać się w łupach. Od początku dostępne jest działko plazmowe." % roundi(LAB.research_duration(captain)),12,MENU.MUTED)
|
MENU.label(self,"Odkryj projekt za surowce — od razu trafi do puli łupów.",12,MENU.MUTED)
|
||||||
if not progress.laboratory_project.is_empty():
|
|
||||||
var item := LAB.definition(progress.laboratory_project)
|
|
||||||
var row := MENU.row(self)
|
|
||||||
MENU.icon(row,item.icon,64)
|
|
||||||
MENU.label(row,item.display_name,16,MENU.GOLD).size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
_timer = MENU.label(self,"",14)
|
|
||||||
_bar = ProgressBar.new()
|
|
||||||
_bar.max_value = progress.laboratory_duration
|
|
||||||
_bar.custom_minimum_size.y = 18
|
|
||||||
add_child(_bar)
|
|
||||||
else:
|
|
||||||
for id in progress.laboratory_offers:
|
for id in progress.laboratory_offers:
|
||||||
var item := LAB.definition(id)
|
var item := LAB.definition(id)
|
||||||
var card := MENU.card(self)
|
var card := MENU.card(self)
|
||||||
@ -56,7 +39,7 @@ func _build() -> void:
|
|||||||
var costs: Array[String] = []
|
var costs: Array[String] = []
|
||||||
for key in LAB.COSTS[id]: costs.append("%s: %d / %d" % [names[key],int(captain.minerals.get(key,0)),int(LAB.COSTS[id][key])])
|
for key in LAB.COSTS[id]: costs.append("%s: %d / %d" % [names[key],int(captain.minerals.get(key,0)),int(LAB.COSTS[id][key])])
|
||||||
MENU.label(card,"KOSZT · posiadasz / wymagane\n"+" · ".join(costs),12,MENU.MUTED)
|
MENU.label(card,"KOSZT · posiadasz / wymagane\n"+" · ".join(costs),12,MENU.MUTED)
|
||||||
var button := MENU.button(card,"ROZPOCZNIJ BADANIE · %d s" % roundi(LAB.research_duration(captain)),_start.bind(id),true)
|
var button := MENU.button(card,"ODKRYJ PROJEKT",_start.bind(id),true)
|
||||||
button.name = "Research_"+id
|
button.name = "Research_"+id
|
||||||
button.disabled = not LAB.can_start(captain,id)
|
button.disabled = not LAB.can_start(captain,id)
|
||||||
_buttons[id] = button
|
_buttons[id] = button
|
||||||
|
|||||||
@ -15,8 +15,8 @@ const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
|
|||||||
@export var laboratory_offers: Array[String] = []
|
@export var laboratory_offers: Array[String] = []
|
||||||
@export var laboratory_project: String = ""
|
@export var laboratory_project: String = ""
|
||||||
@export var laboratory_remaining: float = 0.0
|
@export var laboratory_remaining: float = 0.0
|
||||||
@export var laboratory_duration: float = 600.0
|
@export var laboratory_duration: float = 0.0
|
||||||
@export var freighter_rooms: Dictionary = {"expansion_3":"module_storage"}
|
@export var freighter_rooms: Dictionary = {"expansion_1":"laboratory", "expansion_3":"module_storage"}
|
||||||
@export var mining_stations: Dictionary = {}
|
@export var mining_stations: Dictionary = {}
|
||||||
@export var distant_maps: Dictionary = {}
|
@export var distant_maps: Dictionary = {}
|
||||||
@export var hermit_purchases: Dictionary = {}
|
@export var hermit_purchases: Dictionary = {}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ static func can_maximize(item: ItemInstance, addon: int) -> bool:
|
|||||||
return item.additional_modifiers[addon].value < maximum(item,addon) - 0.000001
|
return item.additional_modifiers[addon].value < maximum(item,addon) - 0.000001
|
||||||
|
|
||||||
static func maximize(progress: Resource, inventory: Node, item: ItemInstance, addon: int) -> bool:
|
static func maximize(progress: Resource, inventory: Node, item: ItemInstance, addon: int) -> bool:
|
||||||
if not progress.active or inventory == null or not bool(inventory.get_parent().get("is_docked")): return false
|
if inventory == null or not bool(inventory.get_parent().get("is_docked")): return false
|
||||||
if not can_maximize(item,addon) or progress.time_crystals < cost(item): return false
|
if not can_maximize(item,addon) or progress.time_crystals < cost(item): return false
|
||||||
var owned: Array = inventory.cargo + inventory.weapon_slots + inventory.module_slots + inventory.extension_slots
|
var owned: Array = inventory.cargo + inventory.weapon_slots + inventory.module_slots + inventory.extension_slots
|
||||||
if not owned.has(item): return false
|
if not owned.has(item): return false
|
||||||
|
|||||||
@ -2,10 +2,15 @@ extends RefCounted
|
|||||||
const CARD := preload("res://scripts/planets/poi_presentation.gd")
|
const CARD := preload("res://scripts/planets/poi_presentation.gd")
|
||||||
const SERVICE := preload("res://scripts/planets/time_engineer.gd")
|
const SERVICE := preload("res://scripts/planets/time_engineer.gd")
|
||||||
const CYAN := Color("9be7ff")
|
const CYAN := Color("9be7ff")
|
||||||
|
const MENU := preload("res://scripts/planets/freighter_menu.gd")
|
||||||
|
const CRYSTAL := preload("res://assets/icons/resources/time_crystals.png")
|
||||||
|
|
||||||
static func build(screen: Control, message: String = "") -> void:
|
static func build(screen: Control, message: String = "", freighter := false) -> void:
|
||||||
var parent: VBoxContainer = screen._choices
|
if freighter:
|
||||||
var progress: Resource = screen.progress
|
_build_compact(screen,message)
|
||||||
|
return
|
||||||
|
var parent: VBoxContainer = screen._content if freighter else screen._choices
|
||||||
|
var progress: Resource = screen.captain.expedition if freighter else screen.progress
|
||||||
var art := TextureRect.new()
|
var art := TextureRect.new()
|
||||||
art.texture = load("res://assets/planets/props/time_engineer.png")
|
art.texture = load("res://assets/planets/props/time_engineer.png")
|
||||||
art.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
art.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
@ -17,11 +22,11 @@ static func build(screen: Control, message: String = "") -> void:
|
|||||||
if not message.is_empty(): CARD.text(parent,message,12,CARD.GOLD)
|
if not message.is_empty(): CARD.text(parent,message,12,CARD.GOLD)
|
||||||
CARD.text(parent,"Kryształy czasu: %d" % progress.time_crystals,14,CYAN)
|
CARD.text(parent,"Kryształy czasu: %d" % progress.time_crystals,14,CYAN)
|
||||||
CARD.text(parent,"Koszt: 10 kryształów × poziom przedmiotu. Gotowe maksima nie pobierają opłaty.",12)
|
CARD.text(parent,"Koszt: 10 kryształów × poziom przedmiotu. Gotowe maksima nie pobierają opłaty.",12)
|
||||||
if not is_instance_valid(screen.flight_ship):
|
if not is_instance_valid(screen.inventory if freighter else screen.flight_ship):
|
||||||
CARD.text(parent,"Brak połączenia z wyposażeniem statku.",12)
|
CARD.text(parent,"Brak połączenia z wyposażeniem statku.",12)
|
||||||
_back(parent,screen)
|
_back(parent,screen,freighter)
|
||||||
return
|
return
|
||||||
var inventory: Node = screen.flight_ship.get_node("ShipInventory")
|
var inventory: Node = screen.inventory if freighter else screen.flight_ship.get_node("ShipInventory")
|
||||||
var items: Array[ItemInstance] = []
|
var items: Array[ItemInstance] = []
|
||||||
var locations: Array[String] = []
|
var locations: Array[String] = []
|
||||||
for kind in ["weapon","module","extension","cargo"]:
|
for kind in ["weapon","module","extension","cargo"]:
|
||||||
@ -32,8 +37,8 @@ static func build(screen: Control, message: String = "") -> void:
|
|||||||
items.append(item)
|
items.append(item)
|
||||||
locations.append({"weapon":"Broń","module":"Moduł","extension":"Rozszerzenie","cargo":"Ładownia"}[kind])
|
locations.append({"weapon":"Broń","module":"Moduł","extension":"Rozszerzenie","cargo":"Ładownia"}[kind])
|
||||||
if items.is_empty():
|
if items.is_empty():
|
||||||
CARD.text(parent,"Brak przedmiotów z addonami na statku. Zabierz wyposażenie z magazynu fregaty przed lądowaniem.",12,CARD.GOLD)
|
CARD.text(parent,"Brak przedmiotów z addonami na statku. Przenieś wyposażenie z magazynu fregaty do ładowni.",12,CARD.GOLD)
|
||||||
_back(parent,screen)
|
_back(parent,screen,freighter)
|
||||||
return
|
return
|
||||||
if not items.has(screen._engineer_item):
|
if not items.has(screen._engineer_item):
|
||||||
screen._engineer_item = items[0]
|
screen._engineer_item = items[0]
|
||||||
@ -48,7 +53,7 @@ static func build(screen: Control, message: String = "") -> void:
|
|||||||
choice.item_selected.connect(func(index):
|
choice.item_selected.connect(func(index):
|
||||||
screen._engineer_item = choice.get_item_metadata(index)
|
screen._engineer_item = choice.get_item_metadata(index)
|
||||||
screen._engineer_addon = 0
|
screen._engineer_addon = 0
|
||||||
screen._show_poi()
|
screen._refresh() if freighter else screen._show_poi()
|
||||||
)
|
)
|
||||||
var item: ItemInstance = screen._engineer_item
|
var item: ItemInstance = screen._engineer_item
|
||||||
CARD.text(parent,"ADDON DO MAKSYMALIZACJI",11,CARD.MUTED)
|
CARD.text(parent,"ADDON DO MAKSYMALIZACJI",11,CARD.MUTED)
|
||||||
@ -65,13 +70,13 @@ static func build(screen: Control, message: String = "") -> void:
|
|||||||
if index == screen._engineer_addon: selected = option
|
if index == screen._engineer_addon: selected = option
|
||||||
if addons.item_count == 0:
|
if addons.item_count == 0:
|
||||||
CARD.text(parent,"Brak dostępnych addonów.",12)
|
CARD.text(parent,"Brak dostępnych addonów.",12)
|
||||||
_back(parent,screen)
|
_back(parent,screen,freighter)
|
||||||
return
|
return
|
||||||
addons.select(selected)
|
addons.select(selected)
|
||||||
screen._engineer_addon = int(addons.get_item_metadata(selected))
|
screen._engineer_addon = int(addons.get_item_metadata(selected))
|
||||||
addons.item_selected.connect(func(index):
|
addons.item_selected.connect(func(index):
|
||||||
screen._engineer_addon = int(addons.get_item_metadata(index))
|
screen._engineer_addon = int(addons.get_item_metadata(index))
|
||||||
screen._show_poi()
|
screen._refresh() if freighter else screen._show_poi()
|
||||||
)
|
)
|
||||||
var index: int = screen._engineer_addon
|
var index: int = screen._engineer_addon
|
||||||
var affix := SERVICE.affix_for(item,index)
|
var affix := SERVICE.affix_for(item,index)
|
||||||
@ -87,7 +92,7 @@ static func build(screen: Control, message: String = "") -> void:
|
|||||||
CARD.text(parent,"Ten addon ma już maksymalną wartość lub nie posiada zakresu losowania.",12,CARD.MUTED)
|
CARD.text(parent,"Ten addon ma już maksymalną wartość lub nie posiada zakresu losowania.",12,CARD.MUTED)
|
||||||
elif progress.time_crystals < cost:
|
elif progress.time_crystals < cost:
|
||||||
CARD.text(parent,"Brakuje %d kryształów czasu." % (cost-progress.time_crystals),12,CARD.GOLD)
|
CARD.text(parent,"Brakuje %d kryształów czasu." % (cost-progress.time_crystals),12,CARD.GOLD)
|
||||||
_back(parent,screen)
|
_back(parent,screen,freighter)
|
||||||
|
|
||||||
static func _choice(parent: Node, node_name: String) -> OptionButton:
|
static func _choice(parent: Node, node_name: String) -> OptionButton:
|
||||||
var choice := OptionButton.new()
|
var choice := OptionButton.new()
|
||||||
@ -99,5 +104,224 @@ static func _choice(parent: Node, node_name: String) -> OptionButton:
|
|||||||
parent.add_child(choice)
|
parent.add_child(choice)
|
||||||
return choice
|
return choice
|
||||||
|
|
||||||
static func _back(parent: Node, screen: Control) -> void:
|
static func _back(parent: Node, screen: Control, freighter := false) -> void:
|
||||||
|
if freighter: return
|
||||||
CARD.button(parent,"Wróć do wyprawy",func(): screen._dialog.hide())
|
CARD.button(parent,"Wróć do wyprawy",func(): screen._dialog.hide())
|
||||||
|
|
||||||
|
static func refresh(screen: Control) -> void:
|
||||||
|
var old: Node = screen._content.get_node_or_null("ChronalBench")
|
||||||
|
var index: int = old.get_index() if old != null else screen._content.get_child_count()
|
||||||
|
if old != null:
|
||||||
|
screen._content.remove_child(old)
|
||||||
|
old.queue_free()
|
||||||
|
_build_compact(screen,screen._engineer_notice)
|
||||||
|
screen._content.move_child(screen._content.get_node("ChronalBench"),index)
|
||||||
|
|
||||||
|
static func _build_compact(screen: Control, message: String) -> void:
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.name = "ChronalBench"
|
||||||
|
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
panel.add_theme_stylebox_override("panel",_box(Color("101e2bf2"),Color("416b7c"),10))
|
||||||
|
screen._content.add_child(panel)
|
||||||
|
var body := VBoxContainer.new()
|
||||||
|
body.add_theme_constant_override("separation",9)
|
||||||
|
panel.add_child(body)
|
||||||
|
var progress: Resource = screen.captain.expedition
|
||||||
|
var header := MENU.row(body)
|
||||||
|
MENU.icon(header,preload("res://assets/icons/screwdriver.png"),23)
|
||||||
|
var title := _text(header,"MAKSYMALIZACJA ADDONU",CYAN)
|
||||||
|
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
MENU.icon(header,CRYSTAL,24)
|
||||||
|
_text(header,str(progress.time_crystals),MENU.GOLD)
|
||||||
|
header.tooltip_text = "Kryształy czasu · koszt maksymalizacji: 10 × poziom przedmiotu."
|
||||||
|
if not is_instance_valid(screen.inventory):
|
||||||
|
_text(body,"Zadokuj statkiem, aby wybrać wyposażenie.",MENU.MUTED)
|
||||||
|
return
|
||||||
|
var inventory: Node = screen.inventory
|
||||||
|
var items: Array[ItemInstance] = []
|
||||||
|
var locations: Array[String] = []
|
||||||
|
for kind in ["weapon","module","extension","cargo"]:
|
||||||
|
var slots: Array = inventory._get_slots(kind)
|
||||||
|
for index in slots.size():
|
||||||
|
var item: ItemInstance = slots[index]
|
||||||
|
if not inventory.is_slot_unlocked(kind,index) or item == null or item.definition == null or item.additional_modifiers.is_empty() or items.has(item): continue
|
||||||
|
items.append(item)
|
||||||
|
locations.append({"weapon":"Broń","module":"Moduł","extension":"Rozszerzenie","cargo":"Ładownia"}[kind])
|
||||||
|
if items.is_empty():
|
||||||
|
var empty := MENU.row(body)
|
||||||
|
MENU.icon(empty,preload("res://assets/icons/cargo_crate.svg"),32)
|
||||||
|
var hint := _text(empty,"Brak wyposażenia z addonami",MENU.MUTED)
|
||||||
|
hint.tooltip_text = "Przenieś przedmiot z addonami do ładowni lub zamontuj go na statku."
|
||||||
|
return
|
||||||
|
if not items.has(screen._engineer_item):
|
||||||
|
screen._engineer_item = items[0]
|
||||||
|
screen._engineer_addon = 0
|
||||||
|
var item: ItemInstance = screen._engineer_item
|
||||||
|
var scroll := ScrollContainer.new()
|
||||||
|
scroll.name = "EngineerItem"
|
||||||
|
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
|
||||||
|
scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
scroll.custom_minimum_size.y = 72
|
||||||
|
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
body.add_child(scroll)
|
||||||
|
var tray := HBoxContainer.new()
|
||||||
|
tray.add_theme_constant_override("separation",6)
|
||||||
|
scroll.add_child(tray)
|
||||||
|
for index in items.size():
|
||||||
|
var candidate: ItemInstance = items[index]
|
||||||
|
var pick := Button.new()
|
||||||
|
pick.name = "EngineerItem_%d" % index
|
||||||
|
pick.custom_minimum_size = Vector2(54,54)
|
||||||
|
pick.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||||
|
pick.icon = candidate.definition.icon
|
||||||
|
pick.expand_icon = true
|
||||||
|
pick.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
pick.add_theme_constant_override("icon_max_width",34)
|
||||||
|
pick.tooltip_text = "%s · %s\n%s" % [locations[index],candidate.definition.display_name,candidate.get_tooltip_text()]
|
||||||
|
_skin(pick,candidate == item)
|
||||||
|
tray.add_child(pick)
|
||||||
|
if candidate == item: scroll.ensure_control_visible.call_deferred(pick)
|
||||||
|
var level := _text(pick,str(candidate.item_level),CYAN if candidate == item else MENU.MUTED)
|
||||||
|
level.position = Vector2(4,35)
|
||||||
|
level.add_theme_font_size_override("font_size",10)
|
||||||
|
level.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
pick.pressed.connect(func():
|
||||||
|
screen._engineer_item = candidate
|
||||||
|
screen._engineer_addon = 0
|
||||||
|
screen._engineer_notice = ""
|
||||||
|
refresh(screen))
|
||||||
|
var selected := MENU.row(body)
|
||||||
|
var item_name := _text(selected,item.definition.display_name,MENU.TEXT)
|
||||||
|
item_name.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
item_name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
|
item_name.tooltip_text = item.get_tooltip_text()
|
||||||
|
_text(selected,"POZ. %d" % item.item_level,MENU.MUTED)
|
||||||
|
var addons := VBoxContainer.new()
|
||||||
|
addons.name = "EngineerAddon"
|
||||||
|
addons.add_theme_constant_override("separation",5)
|
||||||
|
body.add_child(addons)
|
||||||
|
var first := -1
|
||||||
|
for index in item.additional_modifiers.size():
|
||||||
|
if item.additional_modifiers[index] != null:
|
||||||
|
if first < 0: first = index
|
||||||
|
if first < 0:
|
||||||
|
_text(body,"Brak dostępnych addonów.",MENU.MUTED)
|
||||||
|
return
|
||||||
|
if screen._engineer_addon < 0 or screen._engineer_addon >= item.additional_modifiers.size() or item.additional_modifiers[screen._engineer_addon] == null:
|
||||||
|
screen._engineer_addon = first
|
||||||
|
for index in item.additional_modifiers.size():
|
||||||
|
var modifier: ItemStatModifier = item.additional_modifiers[index]
|
||||||
|
if modifier == null: continue
|
||||||
|
_addon_button(screen,addons,item,index)
|
||||||
|
var index: int = screen._engineer_addon
|
||||||
|
var ready := SERVICE.can_maximize(item,index)
|
||||||
|
var cost := SERVICE.cost(item)
|
||||||
|
var footer := MENU.row(body)
|
||||||
|
MENU.icon(footer,CRYSTAL,28)
|
||||||
|
var price := _text(footer,str(cost),MENU.GOLD if progress.time_crystals >= cost else Color("ef9389"))
|
||||||
|
price.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
price.tooltip_text = "Koszt: 10 kryształów × poziom przedmiotu."
|
||||||
|
var apply := MENU.button(footer,"MAKSYMALIZUJ" if ready else "✓ MAX",screen._maximize_addon,true)
|
||||||
|
apply.name = "MaximizeSelectedAddon"
|
||||||
|
apply.custom_minimum_size = Vector2(158,36)
|
||||||
|
apply.icon = preload("res://assets/icons/screwdriver.png")
|
||||||
|
apply.expand_icon = true
|
||||||
|
apply.add_theme_constant_override("icon_max_width",18)
|
||||||
|
apply.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
|
apply.disabled = not ready or progress.time_crystals < cost
|
||||||
|
apply.tooltip_text = "Maksymalizuj tylko wybrany addon. Pozostałe parametry zostają bez zmian."
|
||||||
|
if ready and progress.time_crystals < cost:
|
||||||
|
_text(body,"Brakuje %d kryształów" % (cost-progress.time_crystals),Color("ef9389"))
|
||||||
|
elif not message.is_empty():
|
||||||
|
_text(body,message,CYAN)
|
||||||
|
|
||||||
|
static func _addon_button(screen: Control, parent: Node, item: ItemInstance, index: int) -> void:
|
||||||
|
var modifier: ItemStatModifier = item.additional_modifiers[index]
|
||||||
|
var affix := SERVICE.affix_for(item,index)
|
||||||
|
var maximum := SERVICE.maximum(item,index)
|
||||||
|
var ready := SERVICE.can_maximize(item,index)
|
||||||
|
var active: bool = screen._engineer_addon == index
|
||||||
|
var button := Button.new()
|
||||||
|
button.name = "EngineerAddon_%d" % index
|
||||||
|
button.custom_minimum_size.y = 46
|
||||||
|
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
button.tooltip_text = modifier.get_display_text()+"\n"+("Kliknij, aby wybrać addon." if ready else "Addon ma już maksimum." if affix != null else "Ten addon nie ma zakresu maksymalizacji.")
|
||||||
|
_skin(button,active)
|
||||||
|
parent.add_child(button)
|
||||||
|
var margin := MarginContainer.new()
|
||||||
|
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
for side in ["left","right"]: margin.add_theme_constant_override("margin_"+side,10)
|
||||||
|
for side in ["top","bottom"]: margin.add_theme_constant_override("margin_"+side,7)
|
||||||
|
margin.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
button.add_child(margin)
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.add_theme_constant_override("separation",10)
|
||||||
|
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
margin.add_child(row)
|
||||||
|
var icon := MENU.icon(row,_stat_icon(modifier.stat_key),27)
|
||||||
|
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
icon.modulate = CYAN if active else Color("b4c8d3")
|
||||||
|
var detail := VBoxContainer.new()
|
||||||
|
detail.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
detail.add_theme_constant_override("separation",4)
|
||||||
|
detail.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
row.add_child(detail)
|
||||||
|
var top := HBoxContainer.new()
|
||||||
|
top.add_theme_constant_override("separation",6)
|
||||||
|
top.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
detail.add_child(top)
|
||||||
|
var title := _text(top,affix.display_name if affix != null else ItemInstance.stat_name(modifier.stat_key),CYAN if active else MENU.TEXT)
|
||||||
|
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
title.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
|
title.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
var multiplier := ItemInstance.ENERGY_REGENERATION_BONUS_MULTIPLIER if modifier.stat_key == "energy_regeneration" else 1.0
|
||||||
|
var unit := affix.unit if affix != null else ""
|
||||||
|
var numbers := "%s → %s %s" % [_number(modifier.value*multiplier),_number(maximum*multiplier),unit] if ready else "✓ MAX" if affix != null else "—"
|
||||||
|
var value := _text(top,numbers,MENU.GOLD if ready else Color("86cda7"))
|
||||||
|
value.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
var bar := ProgressBar.new()
|
||||||
|
bar.custom_minimum_size.y = 4
|
||||||
|
bar.show_percentage = false
|
||||||
|
bar.value = clampf(modifier.value/maxf(0.00001,maximum)*100,0,100) if affix != null else 0.0
|
||||||
|
bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
bar.add_theme_stylebox_override("background",_box(Color("09121a"),Color.TRANSPARENT,0))
|
||||||
|
bar.add_theme_stylebox_override("fill",_box(CYAN if active else Color("547d85"),Color.TRANSPARENT,0))
|
||||||
|
detail.add_child(bar)
|
||||||
|
button.pressed.connect(func():
|
||||||
|
screen._engineer_addon = index
|
||||||
|
screen._engineer_notice = ""
|
||||||
|
refresh(screen))
|
||||||
|
|
||||||
|
static func _text(parent: Node, value: String, color: Color) -> Label:
|
||||||
|
var label := MENU.label(parent,value,13,color)
|
||||||
|
label.autowrap_mode = TextServer.AUTOWRAP_OFF
|
||||||
|
return label
|
||||||
|
|
||||||
|
static func _number(value: float) -> String:
|
||||||
|
return str(roundi(value)) if is_equal_approx(value,roundf(value)) else "%.2f" % value
|
||||||
|
|
||||||
|
static func _box(background: Color, border: Color, inset: int) -> StyleBoxFlat:
|
||||||
|
var box := StyleBoxFlat.new()
|
||||||
|
box.bg_color = background
|
||||||
|
box.border_color = border
|
||||||
|
box.set_border_width_all(1)
|
||||||
|
box.set_corner_radius_all(5)
|
||||||
|
box.set_content_margin_all(inset)
|
||||||
|
return box
|
||||||
|
|
||||||
|
static func _skin(button: Button, selected: bool) -> void:
|
||||||
|
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||||
|
for state in ["normal","hover","pressed","focus"]:
|
||||||
|
var background := Color("1c3648") if selected else Color("15232f")
|
||||||
|
if state in ["hover","pressed"]: background = background.lightened(0.12)
|
||||||
|
button.add_theme_stylebox_override(state,_box(background,CYAN if selected or state == "focus" else Color("3d5262"),5))
|
||||||
|
|
||||||
|
static func _stat_icon(key: String) -> Texture2D:
|
||||||
|
var path := "computer.png"
|
||||||
|
if "shield" in key or "absorption" in key: path = "shield.png"
|
||||||
|
elif "damage" in key or "critical" in key: path = "plasma.png"
|
||||||
|
elif "speed" in key or "cooldown" in key: path = "engine.png"
|
||||||
|
elif "hull" in key: path = "armor.png"
|
||||||
|
elif "energy" in key: path = "reactor.png"
|
||||||
|
elif "credits" in key: path = "resources/credits.svg"
|
||||||
|
return load("res://assets/icons/"+path) as Texture2D
|
||||||
|
|||||||
@ -74,5 +74,5 @@ func _refresh_wardrobe() -> void:
|
|||||||
var signature := str(captain.gear_items)+str(captain.gear_slots)
|
var signature := str(captain.gear_items)+str(captain.gear_slots)
|
||||||
if signature == _gear_signature: return
|
if signature == _gear_signature: return
|
||||||
_gear_signature = signature
|
_gear_signature = signature
|
||||||
_protection.text = "Dopasuj poziom sprzętu do planety."
|
_protection.text = "Sprzęt planetarny · zastosowanie wstrzymane."
|
||||||
_protection.tooltip_text = "Dopasuj poziom wyposażenia do planety, aby odzyskać więcej łupu i ograniczyć uszkodzenia lądownika."
|
_protection.tooltip_text = "Wyposażenie zachowano na przyszłość. Lądowanie i eksploracja powierzchni są obecnie wyłączone."
|
||||||
|
|||||||
@ -48,7 +48,7 @@ func refresh() -> void:
|
|||||||
tooltip_text = "Odłóż wyposażenie kapitana" if return_slot else "%s\n%s\nPrzeciągnij przedmiot do pasującego slotu. Dwuklik zakłada lub zdejmuje." % [GEAR.NAMES.get(kind, kind), "Poziom %d" % int(gear.level) if not gear.is_empty() else "Pusty slot"]
|
tooltip_text = "Odłóż wyposażenie kapitana" if return_slot else "%s\n%s\nPrzeciągnij przedmiot do pasującego slotu. Dwuklik zakłada lub zdejmuje." % [GEAR.NAMES.get(kind, kind), "Poziom %d" % int(gear.level) if not gear.is_empty() else "Pusty slot"]
|
||||||
if not return_slot and not gear.is_empty():
|
if not return_slot and not gear.is_empty():
|
||||||
var uses := {"breathing":"Pomaga podczas eksploracji jaskiń i miejsc z trudną atmosferą.","suit":"Pomaga przy wydobyciu i eksploracji niestabilnych ruin.","sidearm":"Pomaga przeszukiwać magazyny i osady opanowane przez faunę."}
|
var uses := {"breathing":"Pomaga podczas eksploracji jaskiń i miejsc z trudną atmosferą.","suit":"Pomaga przy wydobyciu i eksploracji niestabilnych ruin.","sidearm":"Pomaga przeszukiwać magazyny i osady opanowane przez faunę."}
|
||||||
tooltip_text += "\n"+str(uses.get(kind,""))+"\n"+"Dopasuj poziom wyposażenia do poziomu planety."
|
tooltip_text += "\n"+str(uses.get(kind,""))+"\nZastosowanie wstrzymane: lądowanie jest obecnie wyłączone."
|
||||||
_style()
|
_style()
|
||||||
|
|
||||||
func _style() -> void:
|
func _style() -> void:
|
||||||
|
|||||||
@ -8,7 +8,7 @@ const PAGES := [
|
|||||||
["Walka i przetrwanie", "03", "[font_size=16][color=#f1bd79]Wróć z wyprawy[/color][/font_size]\n\n[color=#91dbea][b]Tarcze, kadłub i energia[/b][/color]\nTarcze pochłaniają obrażenia przed kadłubem. Utrata całego kadłuba oznacza zniszczenie statku. Energia zasila część broni oraz dopalanie; odnawia się z czasem, szybciej przy gwieździe.\n\n[color=#91dbea][b]Dobieraj broń do zapasów[/b][/color]\nBroń energetyczna zużywa energię. Działko maszynowe potrzebuje zwykłej amunicji, gamma — napromieniowanej, a wyrzutnie — rakiet. Puste zapasy oznaczają brak strzału. Absorpcja ogranicza obrażenia danego typu.\n\n[color=#91dbea][b]Korzystaj z rozszerzeń[/b][/color]\nMiny atakują obszarowo, flary odciągają uwagę, kamuflaż pomaga zerwać kontakt, teleport przenosi w bezpieczne miejsce, a drony wspierają ostrzał. Doładowanie tarcz działa przy gwieździe. Sprawdzaj koszt i cooldown w opisie.\n\n[color=#f1bd79][b]Uważaj na uciekających przeciwników[/b][/color]\nWróg z kadłubem poniżej 10% ucieka wolniej, ale może zostawić minę. Nie leć bezmyślnie prosto za nim."],
|
["Walka i przetrwanie", "03", "[font_size=16][color=#f1bd79]Wróć z wyprawy[/color][/font_size]\n\n[color=#91dbea][b]Tarcze, kadłub i energia[/b][/color]\nTarcze pochłaniają obrażenia przed kadłubem. Utrata całego kadłuba oznacza zniszczenie statku. Energia zasila część broni oraz dopalanie; odnawia się z czasem, szybciej przy gwieździe.\n\n[color=#91dbea][b]Dobieraj broń do zapasów[/b][/color]\nBroń energetyczna zużywa energię. Działko maszynowe potrzebuje zwykłej amunicji, gamma — napromieniowanej, a wyrzutnie — rakiet. Puste zapasy oznaczają brak strzału. Absorpcja ogranicza obrażenia danego typu.\n\n[color=#91dbea][b]Korzystaj z rozszerzeń[/b][/color]\nMiny atakują obszarowo, flary odciągają uwagę, kamuflaż pomaga zerwać kontakt, teleport przenosi w bezpieczne miejsce, a drony wspierają ostrzał. Doładowanie tarcz działa przy gwieździe. Sprawdzaj koszt i cooldown w opisie.\n\n[color=#f1bd79][b]Uważaj na uciekających przeciwników[/b][/color]\nWróg z kadłubem poniżej 10% ucieka wolniej, ale może zostawić minę. Nie leć bezmyślnie prosto za nim."],
|
||||||
["Łup i wytwarzanie", "04", "[font_size=16][color=#f1bd79]Zbieraj. Porównuj. Twórz.[/color][/font_size]\n\n[color=#91dbea][b]Łup zbierasz przez podlecenie[/b][/color]\nPrzedmioty w pobliżu są przyciągane do statku. Magazyn mieści 20 przedmiotów; przy pełnym magazynie zwolnij miejsce, żeby zebrać kolejny.\n\n[color=#91dbea][b]Asteroidy zostawiają fizyczne odłamki[/b][/color]\nStrzelaj do asteroid. Po zniszczeniu rozpadają się bazowo na 5–7 poszarpanych fragmentów. Każdy to jedna jednostka surowca — musisz go zebrać. Umiejętność górnika zwiększa uzysk.\n\n[color=#91dbea][b]Przeszukuj wraki[/b][/color]\nPodleć do wraku i przytrzymaj E, aby go przeszukać. Wraki dają surowce (65%), amunicję lub rakiety (łącznie 20%) albo moduł (15%).\n\n[color=#91dbea][b]Warsztat fregaty — produkcja[/b][/color]\nZadokuj na fregacie i wybierz Warsztat. Tam wytworzysz amunicję, rakiety, części, powerbanki i wyposażenie kapitana. Koszt sprawdzisz w podpowiedzi przycisku produkcji.\n\n[color=#91dbea][b]Odzyskuj części[/b][/color]\nNajedź na zbędny przedmiot w magazynie i użyj przycisku rozłożenia. Części przydadzą się do napraw."],
|
["Łup i wytwarzanie", "04", "[font_size=16][color=#f1bd79]Zbieraj. Porównuj. Twórz.[/color][/font_size]\n\n[color=#91dbea][b]Łup zbierasz przez podlecenie[/b][/color]\nPrzedmioty w pobliżu są przyciągane do statku. Magazyn mieści 20 przedmiotów; przy pełnym magazynie zwolnij miejsce, żeby zebrać kolejny.\n\n[color=#91dbea][b]Asteroidy zostawiają fizyczne odłamki[/b][/color]\nStrzelaj do asteroid. Po zniszczeniu rozpadają się bazowo na 5–7 poszarpanych fragmentów. Każdy to jedna jednostka surowca — musisz go zebrać. Umiejętność górnika zwiększa uzysk.\n\n[color=#91dbea][b]Przeszukuj wraki[/b][/color]\nPodleć do wraku i przytrzymaj E, aby go przeszukać. Wraki dają surowce (65%), amunicję lub rakiety (łącznie 20%) albo moduł (15%).\n\n[color=#91dbea][b]Warsztat fregaty — produkcja[/b][/color]\nZadokuj na fregacie i wybierz Warsztat. Tam wytworzysz amunicję, rakiety, części, powerbanki i wyposażenie kapitana. Koszt sprawdzisz w podpowiedzi przycisku produkcji.\n\n[color=#91dbea][b]Odzyskuj części[/b][/color]\nNajedź na zbędny przedmiot w magazynie i użyj przycisku rozłożenia. Części przydadzą się do napraw."],
|
||||||
["Statek i kapitan", "05", "[font_size=16][color=#f1bd79]Buduj własny styl gry[/color][/font_size]\n\n[color=#91dbea][b]Wyposażenie[/b][/color]\nKliknij panel statku w prawym górnym rogu i otwórz magazyn. Przeciągaj przedmioty do pasujących slotów: maksymalnie 3 bronie, 5 modułów i 5 rozszerzeń. Na początku dostępny jest 1 slot broni i 1 rozszerzenia. Pierwszy dodatkowy slot broni kosztuje 100 tytanu, drugi 500. Rozszerzenia kosztują po 40 żelaza, aluminium i tytanu, a każdy kolejny slot podwaja ten koszt (80, 160, 320). Ładownia zaczyna z 5 miejscami; każde kolejne miejsce kosztuje 100 żelaza i 50 aluminium. Sloty odblokowujesz w hangarze fregaty. Po zbudowaniu składu modułów zakładka Magazyn na frachtowcu pozwala odkładać i odbierać wyposażenie. Aktywne rozszerzenia uruchamiasz klawiszami 1–5; pasywne (oznaczone P) działają automatycznie w tych samych slotach. Najedź kursorem, żeby sprawdzić parametry i porównanie wyposażenia.\n\n[color=#91dbea][b]Nie każdy duplikat jest taki sam[/b][/color]\nBroń i moduły mogą mieć różne parametry oraz addony. Aktywne rozszerzenia losują cooldown od 30 do 5 pełnych sekund. Pasywne mają losowaną siłę efektu: Recykler odnawia tarcze i energię po zebraniu odłamka, Wirus przejmuje uciekających wrogów, a Pancerz z tytanu ogranicza obrażenia przy niskim kadłubie. Krótsze odnowienia są rzadkie — przed rozłożeniem porównaj egzemplarze. Rozszerzenia tego samego typu dzielą aktywne odnowienie.\n\n[color=#91dbea][b]Umiejętności kapitana[/b][/color]\nZa pokonanych wrogów zdobywasz doświadczenie i kredyty. Trudniejsze układy dają więcej bazowego XP. Jeśli kapitan przewyższa poziom układu o ponad 2 poziomy, XP za przeciwników i ruiny maleje; przy przewadze 8 poziomów spada do zera. Kredyty i surowce nadal zbierasz. Po awansie wybierasz umiejętność. Pasywne działają stale, aktywne przypisujesz do F1–F5.\n\n[color=#f1bd79][b]Okno umiejętności nie pauzuje gry.[/b][/color]\nOtwieraj je w bezpiecznym miejscu. Przed kolejnym układem uzupełnij zapasy i sprawdź wyposażenie."]
|
["Statek i kapitan", "05", "[font_size=16][color=#f1bd79]Buduj własny styl gry[/color][/font_size]\n\n[color=#91dbea][b]Wyposażenie[/b][/color]\nKliknij panel statku w prawym górnym rogu i otwórz magazyn. Przeciągaj przedmioty do pasujących slotów: maksymalnie 3 bronie, 5 modułów i 5 rozszerzeń. Na początku dostępny jest 1 slot broni i 1 rozszerzenia. Pierwszy dodatkowy slot broni kosztuje 100 tytanu, drugi 500. Rozszerzenia kosztują po 40 żelaza, aluminium i tytanu, a każdy kolejny slot podwaja ten koszt (80, 160, 320). Ładownia zaczyna z 5 miejscami; każde kolejne miejsce kosztuje 100 żelaza i 50 aluminium. Sloty odblokowujesz w hangarze fregaty. Po zbudowaniu składu modułów zakładka Magazyn na frachtowcu pozwala odkładać i odbierać wyposażenie. Aktywne rozszerzenia uruchamiasz klawiszami 1–5; pasywne (oznaczone P) działają automatycznie w tych samych slotach. Najedź kursorem, żeby sprawdzić parametry i porównanie wyposażenia.\n\n[color=#91dbea][b]Nie każdy duplikat jest taki sam[/b][/color]\nBroń i moduły mogą mieć różne parametry oraz addony. Aktywne rozszerzenia losują cooldown od 30 do 5 pełnych sekund. Pasywne mają losowaną siłę efektu: Recykler odnawia tarcze i energię po zebraniu odłamka, Wirus przejmuje uciekających wrogów, a Pancerz z tytanu ogranicza obrażenia przy niskim kadłubie. Krótsze odnowienia są rzadkie — przed rozłożeniem porównaj egzemplarze. Rozszerzenia tego samego typu dzielą aktywne odnowienie.\n\n[color=#91dbea][b]Umiejętności kapitana[/b][/color]\nZa pokonanych wrogów zdobywasz doświadczenie i kredyty. Trudniejsze układy dają więcej bazowego XP. Jeśli kapitan przewyższa poziom układu o ponad 2 poziomy, XP za przeciwników i ruiny maleje; przy przewadze 8 poziomów spada do zera. Kredyty i surowce nadal zbierasz. Po awansie wybierasz umiejętność. Pasywne działają stale, aktywne przypisujesz do F1–F5.\n\n[color=#f1bd79][b]Okno umiejętności nie pauzuje gry.[/b][/color]\nOtwieraj je w bezpiecznym miejscu. Przed kolejnym układem uzupełnij zapasy i sprawdź wyposażenie."]
|
||||||
,["Planety i frachtowiec", "06", "[b]Wyprawy na planety[/b]\n\nPierwsze planety są łagodne. W kolejnych układach eksploracja jest trudniejsza, ale znaleziska są bogatsze. Dopasuj poziom wyposażenia kapitana do poziomu planety: aparat oddechowy pomaga w jaskiniach, skafander przy wydobyciu i ruinach, a broń ręczna w magazynach i osadach.\n\nPodleć do planety EARTHLIKE. F pokazuje informacje, a L rozpoczyna lądowanie. Lądownik musi mieć 100 HP przed startem; lądowanie uszkadza kadłub. Podczas walki nie można lądować.\n\nNa powierzchni: W — ciąg, S — hamowanie i wstecz, A/D — obrót, Q/R — ruch boczny, TAB — dopalanie. Przy POI naciśnij E. Najpierw zobaczysz przewidywaną nagrodę i uszkodzenia. Samo otwarcie okna niczego nie zużywa. Potwierdź eksplorację przyciskiem lub drugim naciśnięciem E.\n\nNiektóre miejsca oferują dodatkowe znaleziska. Przeszukanie głębiej daje więcej łupu, ale mocniej uszkadza lądownik. Niski kadłub może oznaczać utratę całej zdobyczy tej wyprawy. L pozwala wrócić z dotychczasowym łupem. Spotkania oznaczone pytajnikiem również otwierasz klawiszem E.\n\n[b]Frachtowiec i postęp[/b]\nPrzy gwieździe centralnej zadokuj klawiszem L. Hangar naprawia lądownik za części. Warsztat produkuje zaopatrzenie i kolejne poziomy wyposażenia kapitana. Laboratorium prowadzi badania.\n\nEsc otwiera pauzę, nazwane zapisy i opcje. Wczytanie zachowuje postęp oraz wyposażenie i zaczyna bieżący poziom od początku."]
|
,["Kosmos i frachtowiec", "06", "[b]Dalekie wyprawy[/b]\n\nLądowanie na planetach jest wyłączone. Bossowie w kosmosie zostawiają mapy. Zbierz mapę i zadokuj do fregaty klawiszem L. W centrum dowodzenia wybierz daleką wyprawę. Lot zużywa mapę i nie wymaga paliwa.\n\nNa wyprawach zniszcz bazy zaopatrzeniowe wroga i zbierz ładunki: krzem, miedź, nikiel, wolfram i rtęć. Każdy statek wroga ma 15% szansy na próbkę do badań obcych.\n\n[b]Modyfikacja wyposażenia[/b]\nKliknij orb w ładowni, potem przedmiot. PPM lub Esc anuluje wybór. Inżynier chronalny w warsztacie fregaty maksymalizuje wybrany addon za kryształy czasu.\n\n[b]Fregata[/b]\nHangar rozbudowuje statek, warsztat produkuje zaopatrzenie, laboratoria prowadzą badania. Załoga wydobywcza i naprawa lądownika są wyłączone.\n\nEsc otwiera pauzę, zapisy i opcje."]
|
||||||
, ["Autorzy dźwięków", "07", "[font_size=16][color=#f1bd79]Dźwięki otoczenia[/color][/font_size]\n\n[b]Blender Foundation — Ambient Bird, Cricket and Frog (Yo Frankie!)[/b]\nOdgłosy świerszcza i żaby, udostępnione przez Lamoot:\nhttps://opengameart.org/content/ambient-bird-cricket-and-frog\nLicencja Creative Commons Attribution 3.0:\nhttps://creativecommons.org/licenses/by/3.0/\nZmiany: filtrowanie, płynne początki i zakończenia, zmiana wysokości żaby oraz konwersja do stereo 48 kHz. Autorzy nie są powiązani z tą grą.\n\n[b]Nagrania na licencji CC0[/b]\nThimras — Park ambiences (wiatr).\nIwan „qubodup” Gabovitch — 20 Rustles of dry leaves.\nisaiah658 — Ambient Bird Sounds.\nRudmer_Rotteveel — Wood Creak Single V7.\n\n[b]Wnętrze fregaty — CC0[/b]\nLEGIT Audio — The Shop (bezpłatne próbki OpenGameArt).\nbart — 68 Workshop Sounds i Steam release sounds.\nKenney — Sci-Fi Sounds.\n\nSzczegółowe źródła i zakres obróbki znajdują się w plikach CREDITS.md i manifest.json przy zasobach audio."]
|
, ["Autorzy dźwięków", "07", "[font_size=16][color=#f1bd79]Dźwięki otoczenia[/color][/font_size]\n\n[b]Blender Foundation — Ambient Bird, Cricket and Frog (Yo Frankie!)[/b]\nOdgłosy świerszcza i żaby, udostępnione przez Lamoot:\nhttps://opengameart.org/content/ambient-bird-cricket-and-frog\nLicencja Creative Commons Attribution 3.0:\nhttps://creativecommons.org/licenses/by/3.0/\nZmiany: filtrowanie, płynne początki i zakończenia, zmiana wysokości żaby oraz konwersja do stereo 48 kHz. Autorzy nie są powiązani z tą grą.\n\n[b]Nagrania na licencji CC0[/b]\nThimras — Park ambiences (wiatr).\nIwan „qubodup” Gabovitch — 20 Rustles of dry leaves.\nisaiah658 — Ambient Bird Sounds.\nRudmer_Rotteveel — Wood Creak Single V7.\n\n[b]Wnętrze fregaty — CC0[/b]\nLEGIT Audio — The Shop (bezpłatne próbki OpenGameArt).\nbart — 68 Workshop Sounds i Steam release sounds.\nKenney — Sci-Fi Sounds.\n\nSzczegółowe źródła i zakres obróbki znajdują się w plikach CREDITS.md i manifest.json przy zasobach audio."]
|
||||||
]
|
]
|
||||||
var _panel: PanelContainer
|
var _panel: PanelContainer
|
||||||
|
|||||||
@ -3,7 +3,6 @@ const HEIGHT := 16.0
|
|||||||
const ENTRIES := [
|
const ENTRIES := [
|
||||||
["credits","Kredyty","svg",Color("efd48e")],
|
["credits","Kredyty","svg",Color("efd48e")],
|
||||||
["parts","Części","png",Color("dfded2")],
|
["parts","Części","png",Color("dfded2")],
|
||||||
["deuter","Deuter","svg",Color("9cdeef")],
|
|
||||||
["time_crystals","Kryształy czasu","png",Color("d4b6ff")],
|
["time_crystals","Kryształy czasu","png",Color("d4b6ff")],
|
||||||
["iron","Żelazo","png",Color("d3dce0")],
|
["iron","Żelazo","png",Color("d3dce0")],
|
||||||
["titanium","Tytan","png",Color("d3dce0")],
|
["titanium","Tytan","png",Color("d3dce0")],
|
||||||
|
|||||||
@ -76,6 +76,8 @@ func _draw() -> void:
|
|||||||
if bool(target.get("_claimed")):
|
if bool(target.get("_claimed")):
|
||||||
continue
|
continue
|
||||||
match str(target.get("kind")):
|
match str(target.get("kind")):
|
||||||
|
"enemy_base":
|
||||||
|
color = Color("ff8060")
|
||||||
"asteroid":
|
"asteroid":
|
||||||
color = Color("a8adb5")
|
color = Color("a8adb5")
|
||||||
"wreck":
|
"wreck":
|
||||||
|
|||||||
@ -37,7 +37,7 @@ func _refresh_title() -> void:
|
|||||||
var stock := {}
|
var stock := {}
|
||||||
for key in preload("res://scripts/planets/planet_profiles.gd").MATERIALS:
|
for key in preload("res://scripts/planets/planet_profiles.gd").MATERIALS:
|
||||||
stock[key] = GameState.captain.minerals.get(key,0)
|
stock[key] = GameState.captain.minerals.get(key,0)
|
||||||
_planet_resources.text = "Z PLANET: " + preload("res://scripts/planets/expedition_ui.gd").material_text(stock)
|
_planet_resources.text = "SUROWCE Z BAZ: " + preload("res://scripts/planets/expedition_ui.gd").material_text(stock)
|
||||||
for key in ["iron", "titanium", "aluminium", "uranium", "sulfur"]:
|
for key in ["iron", "titanium", "aluminium", "uranium", "sulfur"]:
|
||||||
_resource_cards[key].set_amount(float(GameState.captain.minerals.get(key, 0)))
|
_resource_cards[key].set_amount(float(GameState.captain.minerals.get(key, 0)))
|
||||||
_resource_cards["parts"].set_amount(GameState.captain.parts)
|
_resource_cards["parts"].set_amount(GameState.captain.parts)
|
||||||
|
|||||||
@ -71,6 +71,6 @@ func run_checks() -> Dictionary:
|
|||||||
if not ORBS.apply(progress,inventory,legacy,"advancement") or legacy.additional_modifiers[0].affix == null: failures.append("Legacy affix unresolved")
|
if not ORBS.apply(progress,inventory,legacy,"advancement") or legacy.additional_modifiers[0].affix == null: failures.append("Legacy affix unresolved")
|
||||||
var drops: Array[String] = []
|
var drops: Array[String] = []
|
||||||
for seed_value in 3: drops.append(preload("res://scripts/game/distant_loot.gd").roll(125,true,seed_value,1.0,0.0)[1].orb)
|
for seed_value in 3: drops.append(preload("res://scripts/game/distant_loot.gd").roll(125,true,seed_value,1.0,0.0)[1].orb)
|
||||||
if not drops.has("advancement") or not drops.has("piercing") or not drops.has("booster"): failures.append("Boss orb pool")
|
if not drops.has("advancement") or not drops.has("piercing") or not drops.has("atomic_engine") or drops.has("booster"): failures.append("Boss orb pool")
|
||||||
ship.free()
|
ship.free()
|
||||||
return {"failures":failures,"definitions_checked":checked,"passed":failures.is_empty()}
|
return {"failures":failures,"definitions_checked":checked,"passed":failures.is_empty()}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ extends Node2D
|
|||||||
const SKILLS := preload("res://scripts/captain/captain_skills.gd")
|
const SKILLS := preload("res://scripts/captain/captain_skills.gd")
|
||||||
const LAB := preload("res://scripts/planets/laboratory.gd")
|
const LAB := preload("res://scripts/planets/laboratory.gd")
|
||||||
const RECIPES := preload("res://scripts/items/crafting_recipes.gd")
|
const RECIPES := preload("res://scripts/items/crafting_recipes.gd")
|
||||||
const NEW_SKILLS := ["scientist", "gunsmith", "bounty_hunter", "missile_specialist", "emergency_procedure"]
|
const NEW_SKILLS := ["gunsmith", "bounty_hunter", "missile_specialist", "emergency_procedure"]
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
GameState.start_new_run()
|
GameState.start_new_run()
|
||||||
@ -17,7 +17,7 @@ func _ready() -> void:
|
|||||||
var missile := ItemInstance.new()
|
var missile := ItemInstance.new()
|
||||||
missile.definition = preload("res://resources/items/missile_launcher.tres")
|
missile.definition = preload("res://resources/items/missile_launcher.tres")
|
||||||
var baseline_money := captain.reward_enemy(1,0,1.0,1.0,false).y
|
var baseline_money := captain.reward_enemy(1,0,1.0,1.0,false).y
|
||||||
assert(SKILLS.IDS.size() == 16)
|
assert(SKILLS.IDS.size() == 15)
|
||||||
for id in NEW_SKILLS:
|
for id in NEW_SKILLS:
|
||||||
assert(SKILLS.icon(id) != null)
|
assert(SKILLS.icon(id) != null)
|
||||||
for rank in range(1,4):
|
for rank in range(1,4):
|
||||||
@ -36,11 +36,7 @@ func _ready() -> void:
|
|||||||
progress.laboratory_unlocks.clear()
|
progress.laboratory_unlocks.clear()
|
||||||
for key in LAB.COSTS.machine_gun: captain.minerals[key] = 100
|
for key in LAB.COSTS.machine_gun: captain.minerals[key] = 100
|
||||||
assert(LAB.start(captain,"machine_gun"))
|
assert(LAB.start(captain,"machine_gun"))
|
||||||
assert(is_equal_approx(progress.laboratory_remaining,[540.0,480.0,420.0][rank-1]))
|
assert(progress.laboratory_remaining == 0)
|
||||||
assert(progress.laboratory_duration == progress.laboratory_remaining)
|
|
||||||
LAB.advance(progress,progress.laboratory_remaining-1.0)
|
|
||||||
assert(not progress.laboratory_unlocks.has("machine_gun"))
|
|
||||||
LAB.advance(progress,1.0)
|
|
||||||
assert(progress.laboratory_unlocks.has("machine_gun"))
|
assert(progress.laboratory_unlocks.has("machine_gun"))
|
||||||
for id in ["ammunition","irradiated_ammunition"]:
|
for id in ["ammunition","irradiated_ammunition"]:
|
||||||
for key in RECIPES.DATA[id].cost: captain.minerals[key] = 100
|
for key in RECIPES.DATA[id].cost: captain.minerals[key] = 100
|
||||||
@ -86,7 +82,7 @@ func _ready() -> void:
|
|||||||
stub.free()
|
stub.free()
|
||||||
ship.free()
|
ship.free()
|
||||||
inventory.free()
|
inventory.free()
|
||||||
print("NEW_CAPTAIN_SKILLS_TEST: PASS (16 skills, icons, training, all ranks, research duration, ammo output/cost, bounty, missile cooldown, repair conditions/cost/cooldown/cap)")
|
print("NEW_CAPTAIN_SKILLS_TEST: PASS (15 skills, icons, training, all ranks, instant discovery, ammo output/cost, bounty, missile cooldown, repair conditions/cost/cooldown/cap)")
|
||||||
get_tree().quit()
|
get_tree().quit()
|
||||||
|
|
||||||
class RepairShip extends CharacterBody2D:
|
class RepairShip extends CharacterBody2D:
|
||||||
|
|||||||
@ -7,7 +7,7 @@ const PROJECTILE := preload("res://scripts/game/weapon_projectile.gd")
|
|||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
GameState.start_new_run()
|
GameState.start_new_run()
|
||||||
var captain := GameState.captain
|
var captain := GameState.captain
|
||||||
assert(SKILLS.IDS.size() == 16)
|
assert(SKILLS.IDS.size() == 15)
|
||||||
for id in ["scrap_collector","drone_commander"]:
|
for id in ["scrap_collector","drone_commander"]:
|
||||||
assert(SKILLS.icon(id) != null)
|
assert(SKILLS.icon(id) != null)
|
||||||
captain.learning_points = 1
|
captain.learning_points = 1
|
||||||
|
|||||||
@ -30,7 +30,7 @@ func _run() -> void:
|
|||||||
assert(GameState.depart_distant("defeat_test"))
|
assert(GameState.depart_distant("defeat_test"))
|
||||||
assert(not GameState.hull_bastion_used,"New expedition receives its own Last Bastion use")
|
assert(not GameState.hull_bastion_used,"New expedition receives its own Last Bastion use")
|
||||||
GameState.hull_bastion_used = true
|
GameState.hull_bastion_used = true
|
||||||
assert(not progress.distant_maps.has("defeat_test") and progress.deuter==5000)
|
assert(not progress.distant_maps.has("defeat_test") and progress.deuter==130000)
|
||||||
get_tree().change_scene_to_file("res://scenes/game/game.tscn")
|
get_tree().change_scene_to_file("res://scenes/game/game.tscn")
|
||||||
await frames()
|
await frames()
|
||||||
var game := get_tree().current_scene
|
var game := get_tree().current_scene
|
||||||
@ -58,7 +58,7 @@ func _run() -> void:
|
|||||||
assert(GameState.active_distant_map.is_empty() and GameState.distant_home.is_empty())
|
assert(GameState.active_distant_map.is_empty() and GameState.distant_home.is_empty())
|
||||||
assert(GameState.hull_bastion_used,"Return restores the home's spent Last Bastion use")
|
assert(GameState.hull_bastion_used,"Return restores the home's spent Last Bastion use")
|
||||||
assert(not progress.distant_maps.has("defeat_test") and progress.distant_maps.has("found_on_expedition"))
|
assert(not progress.distant_maps.has("defeat_test") and progress.distant_maps.has("found_on_expedition"))
|
||||||
assert(progress.deuter==5000,"Defeat cannot refund flight fuel")
|
assert(progress.deuter==130000,"Defeat preserves legacy resource balance")
|
||||||
assert(not GameState.depart_distant("defeat_test"),"Consumed expedition cannot be retried")
|
assert(not GameState.depart_distant("defeat_test"),"Consumed expedition cannot be retried")
|
||||||
assert(GameState.explored_pois.has("777:kept"),"Failed expedition must not reset home exploration")
|
assert(GameState.explored_pois.has("777:kept"),"Failed expedition must not reset home exploration")
|
||||||
assert(game.has_node("Freighter") and not game.has_node("DistantSector"))
|
assert(game.has_node("Freighter") and not game.has_node("DistantSector"))
|
||||||
@ -80,5 +80,5 @@ func _run() -> void:
|
|||||||
assert(GameState.asteroid_retry_seeds.has(777) and not GameState.explored_pois.has("777:regular_retry"))
|
assert(GameState.asteroid_retry_seeds.has(777) and not GameState.explored_pois.has("777:regular_retry"))
|
||||||
assert(not game.player.is_docked and not get_tree().paused,"Normal-system defeat still retries the current system")
|
assert(not game.player.is_docked and not get_tree().paused,"Normal-system defeat still retries the current system")
|
||||||
assert(GameState.hull_bastion_used,"Retry cannot renew Last Bastion")
|
assert(GameState.hull_bastion_used,"Retry cannot renew Last Bastion")
|
||||||
assert(not progress.distant_maps.has("defeat_test") and progress.deuter==5000)
|
assert(not progress.distant_maps.has("defeat_test") and progress.deuter==130000)
|
||||||
print("DISTANT_DEFEAT_RETURN: PASS (original system and seed, consumed map/fuel, preserved inventory and found maps, freighter docking, duplicate retry guarded)")
|
print("DISTANT_DEFEAT_RETURN: PASS (original system and seed, consumed map, no fuel cost, preserved inventory and found maps, freighter docking, duplicate retry guarded)")
|
||||||
|
|||||||
@ -16,7 +16,7 @@ func run_checks() -> Dictionary:
|
|||||||
if int(map.level) < source+1 or int(map.level) > source+5: failures.append("Wrong destination range")
|
if int(map.level) < source+1 or int(map.level) > source+5: failures.append("Wrong destination range")
|
||||||
if map != DATA.offer(candidate,source): failures.append("Offer rerolled")
|
if map != DATA.offer(candidate,source): failures.append("Offer rerolled")
|
||||||
if DATA.map_cost(map) != int(map.level)*1000: failures.append("Wrong credits")
|
if DATA.map_cost(map) != int(map.level)*1000: failures.append("Wrong credits")
|
||||||
if DATA.fuel_cost(map) != int(map.level)*1000: failures.append("Wrong fuel")
|
if DATA.fuel_cost(map) != 0: failures.append("Wrong fuel")
|
||||||
var captain = preload("res://scripts/captain/captain_progress.gd").new()
|
var captain = preload("res://scripts/captain/captain_progress.gd").new()
|
||||||
var progress: Resource = captain.expedition
|
var progress: Resource = captain.expedition
|
||||||
var map := DATA.visit_offer(progress,profile,120)
|
var map := DATA.visit_offer(progress,profile,120)
|
||||||
|
|||||||
@ -43,10 +43,7 @@ func _run() -> void:
|
|||||||
GameState.system_seed = 777
|
GameState.system_seed = 777
|
||||||
GameState.remember_system()
|
GameState.remember_system()
|
||||||
GameState.captain.expedition.distant_maps["integration"] = {"id":"integration","level":125,"seed":123456,"name":"Sektor testowy","source_planet":"Test","source_level":120}
|
GameState.captain.expedition.distant_maps["integration"] = {"id":"integration","level":125,"seed":123456,"name":"Sektor testowy","source_planet":"Test","source_level":120}
|
||||||
GameState.captain.expedition.deuter = 124999
|
GameState.captain.expedition.deuter = 0
|
||||||
check(not GameState.depart_distant("integration"),"No flight without fuel")
|
|
||||||
check(GameState.system_level==7 and GameState.captain.expedition.deuter==124999,"Failed departure preserved state")
|
|
||||||
GameState.captain.expedition.deuter = 125000
|
|
||||||
get_tree().change_scene_to_file("res://scenes/game/game.tscn")
|
get_tree().change_scene_to_file("res://scenes/game/game.tscn")
|
||||||
await frames()
|
await frames()
|
||||||
var game := get_tree().current_scene
|
var game := get_tree().current_scene
|
||||||
@ -59,20 +56,21 @@ func _run() -> void:
|
|||||||
engine.definition = preload("res://resources/items/engine_extension.tres")
|
engine.definition = preload("res://resources/items/engine_extension.tres")
|
||||||
inventory.module_slots[2] = engine
|
inventory.module_slots[2] = engine
|
||||||
var progress: Resource = GameState.captain.expedition
|
var progress: Resource = GameState.captain.expedition
|
||||||
progress.orbs = {"piercing":2,"booster":1}
|
progress.orbs = {"piercing":2,"atomic_engine":1}
|
||||||
check(not ORBS.apply(progress,inventory,engine,"piercing"),"Piercing category restriction")
|
check(not ORBS.apply(progress,inventory,engine,"piercing"),"Piercing category restriction")
|
||||||
check(not ORBS.apply(progress,inventory,weapon,"booster"),"Booster category restriction")
|
check(not ORBS.apply(progress,inventory,weapon,"atomic_engine"),"Booster category restriction")
|
||||||
check(ORBS.apply(progress,inventory,weapon,"piercing"),"Apply piercing")
|
check(ORBS.apply(progress,inventory,weapon,"piercing"),"Apply piercing")
|
||||||
check(not ORBS.apply(progress,inventory,weapon,"piercing") and progress.orbs.piercing==1,"No stacking or extra cost")
|
check(not ORBS.apply(progress,inventory,weapon,"piercing") and progress.orbs.piercing==1,"No stacking or extra cost")
|
||||||
check(ORBS.apply(progress,inventory,engine,"booster"),"Apply booster")
|
check(ORBS.apply(progress,inventory,engine,"atomic_engine"),"Apply booster")
|
||||||
check(player.has_free_mini_boost(),"Equipped engine")
|
var atomic_cost: float = player.get_mini_boost_energy_cost()
|
||||||
|
check(atomic_cost > 0,"Atomic engine still uses energy")
|
||||||
Input.action_press("ship_mini_boost")
|
Input.action_press("ship_mini_boost")
|
||||||
player.energy = 0
|
player.energy = 0
|
||||||
player._update_mini_boost(1.0)
|
player._update_mini_boost(1.0)
|
||||||
check(player._mini_boost_active and player.energy==0,"Free TAB with empty generator")
|
check(not player._mini_boost_active and player.energy==0,"TAB blocked with empty generator")
|
||||||
Input.action_release("ship_mini_boost")
|
Input.action_release("ship_mini_boost")
|
||||||
inventory.module_slots[2] = null
|
inventory.module_slots[2] = null
|
||||||
check(not player.has_free_mini_boost(),"Unequipped engine no effect")
|
check(is_equal_approx(player.get_mini_boost_energy_cost(),atomic_cost*2),"Unequipped engine no discount")
|
||||||
inventory.module_slots[2] = engine
|
inventory.module_slots[2] = engine
|
||||||
check(game.travel_to_distant("integration"),"Flight requested")
|
check(game.travel_to_distant("integration"),"Flight requested")
|
||||||
await frames(3)
|
await frames(3)
|
||||||
@ -84,7 +82,7 @@ func _run() -> void:
|
|||||||
await frames(8)
|
await frames(8)
|
||||||
game = get_tree().current_scene
|
game = get_tree().current_scene
|
||||||
freeze_game(game)
|
freeze_game(game)
|
||||||
check(GameState.captain.expedition.deuter==0,"Exactly one fuel payment")
|
check(GameState.captain.expedition.deuter==0,"Departure needs no fuel")
|
||||||
check(not GameState.captain.expedition.distant_maps.has("integration"),"Map consumed on departure")
|
check(not GameState.captain.expedition.distant_maps.has("integration"),"Map consumed on departure")
|
||||||
check(GameState.system_level==125 and GameState.highest_system==7,"Isolated progression")
|
check(GameState.system_level==125 and GameState.highest_system==7,"Isolated progression")
|
||||||
check(game.get_node_or_null("CelestialBodies")==null,"No celestial bodies")
|
check(game.get_node_or_null("CelestialBodies")==null,"No celestial bodies")
|
||||||
@ -98,7 +96,7 @@ func _run() -> void:
|
|||||||
inventory = player.get_node("ShipInventory")
|
inventory = player.get_node("ShipInventory")
|
||||||
player.is_docked = true
|
player.is_docked = true
|
||||||
check("piercing" in inventory.weapon_slots[0].special_orbs,"Orb persisted across travel")
|
check("piercing" in inventory.weapon_slots[0].special_orbs,"Orb persisted across travel")
|
||||||
check(player.has_free_mini_boost(),"Engine orb persisted")
|
check("atomic_engine" in inventory.module_slots[2].special_orbs,"Engine orb persisted")
|
||||||
var enemy: Node = get_tree().get_first_node_in_group("enemies")
|
var enemy: Node = get_tree().get_first_node_in_group("enemies")
|
||||||
enemy.absorption = Vector3.ZERO
|
enemy.absorption = Vector3.ZERO
|
||||||
enemy.shields = 100
|
enemy.shields = 100
|
||||||
@ -128,12 +126,12 @@ func _run() -> void:
|
|||||||
projectile.free()
|
projectile.free()
|
||||||
var payload := Node2D.new()
|
var payload := Node2D.new()
|
||||||
payload.set_script(preload("res://scripts/items/expedition_pickup.gd"))
|
payload.set_script(preload("res://scripts/items/expedition_pickup.gd"))
|
||||||
payload.payload = {"orb":"booster"}
|
payload.payload = {"orb":"atomic_engine"}
|
||||||
payload.position = player.position
|
payload.position = player.position
|
||||||
game.add_child(payload)
|
game.add_child(payload)
|
||||||
var count: int = preload("res://scripts/items/special_orbs.gd").count(progress,inventory,"booster")
|
var count: int = preload("res://scripts/items/special_orbs.gd").count(progress,inventory,"atomic_engine")
|
||||||
payload._physics_process(0)
|
payload._physics_process(0)
|
||||||
check(preload("res://scripts/items/special_orbs.gd").count(progress,inventory,"booster")==count+1,"Collect orb into cargo")
|
check(preload("res://scripts/items/special_orbs.gd").count(progress,inventory,"atomic_engine")==count+1,"Collect orb into cargo")
|
||||||
await frames()
|
await frames()
|
||||||
for victim in get_tree().get_nodes_in_group("enemies"): victim.queue_free()
|
for victim in get_tree().get_nodes_in_group("enemies"): victim.queue_free()
|
||||||
await frames()
|
await frames()
|
||||||
|
|||||||
@ -84,8 +84,8 @@ func run(game: Node) -> Array[String]:
|
|||||||
player._update_mini_boost(1.0)
|
player._update_mini_boost(1.0)
|
||||||
Input.action_release("ship_mini_boost")
|
Input.action_release("ship_mini_boost")
|
||||||
check(is_equal_approx(player.energy,50.0-normal_boost_cost*0.5), "Actual atomic boost consumes half energy")
|
check(is_equal_approx(player.energy,50.0-normal_boost_cost*0.5), "Actual atomic boost consumes half energy")
|
||||||
check(ORBS.apply(progress,inventory,engine,"booster"), "Free booster stacks by precedence")
|
check(not ORBS.apply(progress,inventory,engine,"booster"), "Retired orb rejected")
|
||||||
check(is_zero_approx(player.get_mini_boost_energy_cost()), "Free booster remains free")
|
check(is_equal_approx(player.get_mini_boost_energy_cost(),normal_boost_cost*0.5), "Atomic boost still costs energy")
|
||||||
check(ORBS.apply(progress,inventory,shield,"polarization"), "Polarization applied")
|
check(ORBS.apply(progress,inventory,shield,"polarization"), "Polarization applied")
|
||||||
player.is_docked = false
|
player.is_docked = false
|
||||||
player.absorption = Vector3.ZERO
|
player.absorption = Vector3.ZERO
|
||||||
|
|||||||
50
tests/items/enemy_destruction_test.gd
Normal file
50
tests/items/enemy_destruction_test.gd
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
extends Node2D
|
||||||
|
const EFFECT := preload("res://scripts/game/enemy_destruction_effect.gd")
|
||||||
|
func _ready() -> void:
|
||||||
|
run.call_deferred()
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var source := Sprite2D.new()
|
||||||
|
source.texture = preload("res://assets/alien_ships/alien_01.png")
|
||||||
|
source.scale = Vector2.ONE*56.0/maxf(source.texture.get_width(),source.texture.get_height())
|
||||||
|
source.position = Vector2(100,80)
|
||||||
|
add_child(source)
|
||||||
|
for tier in 3:
|
||||||
|
var effect := Node2D.new()
|
||||||
|
effect.set_script(EFFECT)
|
||||||
|
effect.play_audio = false
|
||||||
|
effect.configure(source,Vector2(100,0),tier)
|
||||||
|
add_child(effect)
|
||||||
|
effect.set_process(false)
|
||||||
|
assert(effect.global_position.is_equal_approx(source.global_position))
|
||||||
|
assert(effect._fragments.size() >= 18)
|
||||||
|
assert(effect._bursts.size() == 3+tier)
|
||||||
|
effect._process(0.1)
|
||||||
|
assert(effect.global_position.x > source.global_position.x)
|
||||||
|
assert(effect._bursts[0].visual.visible)
|
||||||
|
effect._process(1.6)
|
||||||
|
for burst in effect._bursts: assert(not burst.visual.visible)
|
||||||
|
effect._process(1.0)
|
||||||
|
assert(effect.is_queued_for_deletion())
|
||||||
|
await get_tree().process_frame
|
||||||
|
# Actual enemy death must spawn the new effect and award the kill only once.
|
||||||
|
var enemy := preload("res://scenes/game/enemy_ship.tscn").instantiate()
|
||||||
|
add_child(enemy)
|
||||||
|
enemy.get_node("Sprite2D").texture = source.texture
|
||||||
|
enemy.take_damage(1e12)
|
||||||
|
enemy.take_damage(1e12)
|
||||||
|
assert(enemy.is_queued_for_deletion())
|
||||||
|
assert(get_tree().get_nodes_in_group("enemy_explosions").size() == 1)
|
||||||
|
await get_tree().process_frame
|
||||||
|
for index in 24:
|
||||||
|
var effect := Node2D.new()
|
||||||
|
effect.set_script(EFFECT)
|
||||||
|
effect.play_audio = false
|
||||||
|
effect.configure(source,Vector2.ZERO,0)
|
||||||
|
add_child(effect)
|
||||||
|
await get_tree().process_frame
|
||||||
|
assert(get_tree().get_nodes_in_group("enemy_explosions").size() <= EFFECT.MAX_EFFECTS)
|
||||||
|
for child in get_children(): child.queue_free()
|
||||||
|
for frame in 3: await get_tree().process_frame
|
||||||
|
print("ENEMY_DESTRUCTION_TEST: PASS (tiers, debris, drift, lifetime, enemy death, effect cap)")
|
||||||
|
get_tree().quit()
|
||||||
1
tests/items/enemy_destruction_test.gd.uid
Normal file
1
tests/items/enemy_destruction_test.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://ca6q5x0iq5uh1
|
||||||
6
tests/items/enemy_destruction_test.tscn
Normal file
6
tests/items/enemy_destruction_test.tscn
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://tests/items/enemy_destruction_test.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="EnemyDestructionTest" type="Node2D"]
|
||||||
|
script = ExtResource("1")
|
||||||
39
tests/items/retired_orb_test.gd
Normal file
39
tests/items/retired_orb_test.gd
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
extends Node
|
||||||
|
const ORBS := preload("res://scripts/items/special_orbs.gd")
|
||||||
|
func _ready() -> void:
|
||||||
|
GameState.start_new_run()
|
||||||
|
assert(not ORBS.IDS.has("booster") and not ORBS.COMMON_IDS.has("booster"))
|
||||||
|
assert(ORBS.create_item("booster") == null and ORBS.icon("booster") == null)
|
||||||
|
var player := preload("res://scenes/game/player_ship.tscn").instantiate()
|
||||||
|
add_child(player)
|
||||||
|
player.set_physics_process(false)
|
||||||
|
player.get_node("WeaponController").set_physics_process(false)
|
||||||
|
var inventory: ShipInventory = player.get_node("ShipInventory")
|
||||||
|
var engine := ItemInstance.new()
|
||||||
|
engine.definition = preload("res://resources/items/engine_extension.tres")
|
||||||
|
inventory.module_slots[2] = engine
|
||||||
|
var base_cost: float = player.get_mini_boost_energy_cost()
|
||||||
|
engine.special_orbs.assign(["booster","atomic_engine"])
|
||||||
|
assert(is_equal_approx(player.get_mini_boost_energy_cost(),base_cost*0.5))
|
||||||
|
var old := ItemInstance.new()
|
||||||
|
old.definition = ItemDefinition.new()
|
||||||
|
old.definition.category = ItemDefinition.Category.CONSUMABLE
|
||||||
|
old.definition.orb_id = "booster"
|
||||||
|
inventory.cargo[0] = old
|
||||||
|
GameState.captain.expedition.orbs = {"booster":10}
|
||||||
|
GameState.captain.expedition.freighter_storage.append(old)
|
||||||
|
GameState.captain.expedition.delivered_modules.append(old)
|
||||||
|
inventory.migrate_orbs()
|
||||||
|
assert(inventory.cargo[0] == null)
|
||||||
|
assert(not engine.special_orbs.has("booster") and engine.special_orbs.has("atomic_engine"))
|
||||||
|
assert(not GameState.captain.expedition.orbs.has("booster"))
|
||||||
|
assert(GameState.captain.expedition.freighter_storage.is_empty())
|
||||||
|
assert(GameState.captain.expedition.delivered_modules.is_empty())
|
||||||
|
assert(not ORBS.apply(GameState.captain.expedition,inventory,engine,"booster"))
|
||||||
|
player.energy = 0
|
||||||
|
Input.action_press("ship_mini_boost")
|
||||||
|
player._update_mini_boost(0.1)
|
||||||
|
Input.action_release("ship_mini_boost")
|
||||||
|
assert(not player._mini_boost_active)
|
||||||
|
print("RETIRED_ORB_TEST: PASS (removed orb, legacy cleanup, atomic engine retained, no free boost)")
|
||||||
|
get_tree().quit()
|
||||||
1
tests/items/retired_orb_test.gd.uid
Normal file
1
tests/items/retired_orb_test.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://csk48ax4146ya
|
||||||
4
tests/items/retired_orb_test.tscn
Normal file
4
tests/items/retired_orb_test.tscn
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://tests/items/retired_orb_test.gd" id="1"]
|
||||||
|
[node name="RetiredOrbTest" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
@ -21,7 +21,7 @@ func _ready() -> void:
|
|||||||
engine.special_orbs.append("atomic_engine")
|
engine.special_orbs.append("atomic_engine")
|
||||||
assert(is_equal_approx(player.get_mini_boost_energy_cost(),cost*0.5))
|
assert(is_equal_approx(player.get_mini_boost_energy_cost(),cost*0.5))
|
||||||
engine.special_orbs.append("booster")
|
engine.special_orbs.append("booster")
|
||||||
assert(is_zero_approx(player.get_mini_boost_energy_cost()))
|
assert(is_equal_approx(player.get_mini_boost_energy_cost(),cost*0.5),"Retired booster has no effect")
|
||||||
var shield := GEN.generate_for_definition(preload("res://resources/items/reinforced_shield.tres"),120)
|
var shield := GEN.generate_for_definition(preload("res://resources/items/reinforced_shield.tres"),120)
|
||||||
shield.special_orbs.append("polarization")
|
shield.special_orbs.append("polarization")
|
||||||
inventory.module_slots[0] = shield
|
inventory.module_slots[0] = shield
|
||||||
|
|||||||
@ -64,8 +64,6 @@ func _ready() -> void:
|
|||||||
var engine: ItemInstance = inventory.module_slots[2]
|
var engine: ItemInstance = inventory.module_slots[2]
|
||||||
engine.special_orbs.append("atomic_engine")
|
engine.special_orbs.append("atomic_engine")
|
||||||
report["atomic_engine_cost_s"] = player.get_mini_boost_energy_cost()
|
report["atomic_engine_cost_s"] = player.get_mini_boost_energy_cost()
|
||||||
engine.special_orbs.append("booster")
|
|
||||||
report["booster_cost_s"] = player.get_mini_boost_energy_cost()
|
|
||||||
var enemy := preload("res://scenes/game/enemy_ship.tscn").instantiate()
|
var enemy := preload("res://scenes/game/enemy_ship.tscn").instantiate()
|
||||||
add_child(enemy)
|
add_child(enemy)
|
||||||
enemy.set_physics_process(false)
|
enemy.set_physics_process(false)
|
||||||
|
|||||||
50
tests/previews/enemy_explosion_preview.gd
Normal file
50
tests/previews/enemy_explosion_preview.gd
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
extends Node2D
|
||||||
|
const EFFECT := preload("res://scripts/game/enemy_destruction_effect.gd")
|
||||||
|
const SHIPS := preload("res://scripts/items/alien_ship_catalog.gd")
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
RenderingServer.set_default_clear_color(Color("070e1c"))
|
||||||
|
get_window().size = Vector2i(1280,900)
|
||||||
|
get_window().content_scale_size = Vector2i(1280,900)
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = 18
|
||||||
|
for index in 250:
|
||||||
|
var star := Polygon2D.new()
|
||||||
|
star.polygon = PackedVector2Array([Vector2(-0.6,0),Vector2(0,-0.6),Vector2(0.6,0),Vector2(0,0.6)])
|
||||||
|
star.position = Vector2(rng.randf()*1280,rng.randf()*900)
|
||||||
|
star.color = Color(0.65,0.8,1,rng.randf_range(0.1,0.5))
|
||||||
|
add_child(star)
|
||||||
|
label_at("ROZERWANIE KADŁUBA / EKSPLOZJE WROGÓW",Vector2(35,20),22)
|
||||||
|
for tier in 3:
|
||||||
|
label_at(["ZWYKŁY STATEK","ELITA","BOSS"][tier],Vector2(100+tier*410,75),16)
|
||||||
|
for row in 3:
|
||||||
|
var sprite := Sprite2D.new()
|
||||||
|
sprite.texture = SHIPS.TEXTURES[tier*4]
|
||||||
|
sprite.scale = Vector2.ONE * [56.0,68.0,112.0][tier] / maxf(sprite.texture.get_width(),sprite.texture.get_height())
|
||||||
|
sprite.position = Vector2(210+tier*410,215+row*245)
|
||||||
|
add_child(sprite)
|
||||||
|
var effect := Node2D.new()
|
||||||
|
effect.set_script(EFFECT)
|
||||||
|
effect.play_audio = false
|
||||||
|
effect.configure(sprite,Vector2.ZERO,tier)
|
||||||
|
add_child(effect)
|
||||||
|
effect.set_process(false)
|
||||||
|
effect._elapsed = [0.07,0.3,0.9][row]
|
||||||
|
effect._update_fireballs()
|
||||||
|
effect.queue_redraw()
|
||||||
|
sprite.queue_free()
|
||||||
|
label_at(["Błysk · 0,07 s","Fala i ogień · 0,30 s","Odłamki · 0,90 s"][row],Vector2(110+tier*410,320+row*245),13)
|
||||||
|
if "--capture" in OS.get_cmdline_user_args():
|
||||||
|
await get_tree().process_frame
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
get_viewport().get_texture().get_image().save_png("res://artifacts/enemy_explosion_preview.png")
|
||||||
|
print("EXPLOSION_PREVIEW_CAPTURED")
|
||||||
|
get_tree().quit()
|
||||||
|
|
||||||
|
func label_at(value: String, origin: Vector2, font_size: int) -> void:
|
||||||
|
var label := Label.new()
|
||||||
|
label.text = value
|
||||||
|
label.position = origin
|
||||||
|
label.add_theme_font_size_override("font_size",font_size)
|
||||||
|
label.modulate = Color("a8becf")
|
||||||
|
add_child(label)
|
||||||
1
tests/previews/enemy_explosion_preview.gd.uid
Normal file
1
tests/previews/enemy_explosion_preview.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://7gjagxfla1tf
|
||||||
6
tests/previews/enemy_explosion_preview.tscn
Normal file
6
tests/previews/enemy_explosion_preview.tscn
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://tests/previews/enemy_explosion_preview.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="EnemyExplosionPreview" type="Node2D"]
|
||||||
|
script = ExtResource("1")
|
||||||
@ -15,7 +15,7 @@ func _ready() -> void:
|
|||||||
for enemy in get_tree().get_nodes_in_group("enemies"): enemy.set_physics_process(false)
|
for enemy in get_tree().get_nodes_in_group("enemies"): enemy.set_physics_process(false)
|
||||||
var ui: CanvasLayer = game.find_child("CaptainSkills",true,false)
|
var ui: CanvasLayer = game.find_child("CaptainSkills",true,false)
|
||||||
captain.learning_points = 1
|
captain.learning_points = 1
|
||||||
captain.skill_offers.assign(["scientist","gunsmith","bounty_hunter"])
|
captain.skill_offers.assign(["missile_specialist","gunsmith","bounty_hunter"])
|
||||||
ui.open_management()
|
ui.open_management()
|
||||||
await _snapshot("res://artifacts/new_captain_skills_economy.png")
|
await _snapshot("res://artifacts/new_captain_skills_economy.png")
|
||||||
captain.skill_offers.assign(["missile_specialist","emergency_procedure"])
|
captain.skill_offers.assign(["missile_specialist","emergency_procedure"])
|
||||||
|
|||||||
@ -57,7 +57,7 @@ func run() -> void:
|
|||||||
game.free()
|
game.free()
|
||||||
check(GameState.continue_game(),"save restores state")
|
check(GameState.continue_game(),"save restores state")
|
||||||
check(GameState.captain.credits == 8765,"latest credits restored")
|
check(GameState.captain.credits == 8765,"latest credits restored")
|
||||||
check(GameState.captain.expedition.laboratory_project == "machine_gun" and is_equal_approx(GameState.captain.expedition.laboratory_remaining,387.5),"SAVE preserves laboratory project and exact remaining time")
|
check(GameState.captain.expedition.laboratory_project.is_empty() and GameState.captain.expedition.laboratory_unlocks.get("machine_gun",false),"SAVE completes legacy laboratory project immediately")
|
||||||
check(GameState.captain.expedition.alien_project == "hull" and is_equal_approx(GameState.captain.expedition.alien_remaining,245.25),"SAVE preserves alien project and exact remaining time")
|
check(GameState.captain.expedition.alien_project == "hull" and is_equal_approx(GameState.captain.expedition.alien_remaining,245.25),"SAVE preserves alien project and exact remaining time")
|
||||||
check(GameState.captain.expedition.crew.cabin_1.level == 4,"crew restored")
|
check(GameState.captain.expedition.crew.cabin_1.level == 4,"crew restored")
|
||||||
check(GameState.captain.expedition.alien_skills.general,"research restored")
|
check(GameState.captain.expedition.alien_skills.general,"research restored")
|
||||||
|
|||||||
@ -34,14 +34,14 @@ func _ready() -> void:
|
|||||||
assert(same.is_queued_for_deletion() and inventory.cargo[0].stack_count==5,"Stack pickup works even with full cargo")
|
assert(same.is_queued_for_deletion() and inventory.cargo[0].stack_count==5,"Stack pickup works even with full cargo")
|
||||||
var other := Node2D.new()
|
var other := Node2D.new()
|
||||||
other.set_script(PICKUP)
|
other.set_script(PICKUP)
|
||||||
other.item = ORBS.create_item("booster")
|
other.item = ORBS.create_item("atomic_engine")
|
||||||
other.position = player.position
|
other.position = player.position
|
||||||
game.add_child(other)
|
game.add_child(other)
|
||||||
other._physics_process(0)
|
other._physics_process(0)
|
||||||
assert(not other.is_queued_for_deletion(),"Different orb stays in space while cargo is full")
|
assert(not other.is_queued_for_deletion(),"Different orb stays in space while cargo is full")
|
||||||
inventory.cargo[1] = null
|
inventory.cargo[1] = null
|
||||||
other._physics_process(0)
|
other._physics_process(0)
|
||||||
assert(other.is_queued_for_deletion() and inventory.cargo[1].definition.orb_id=="booster")
|
assert(other.is_queued_for_deletion() and inventory.cargo[1].definition.orb_id=="atomic_engine")
|
||||||
var source := SLOT.instantiate()
|
var source := SLOT.instantiate()
|
||||||
source.slot_kind = "cargo"
|
source.slot_kind = "cargo"
|
||||||
source.slot_index = 0
|
source.slot_index = 0
|
||||||
@ -71,7 +71,7 @@ func _ready() -> void:
|
|||||||
engine.definition = preload("res://resources/items/engine_extension.tres")
|
engine.definition = preload("res://resources/items/engine_extension.tres")
|
||||||
inventory.module_slots[2] = engine
|
inventory.module_slots[2] = engine
|
||||||
assert(inventory.select_orb(1) and inventory.use_selected_orb(engine))
|
assert(inventory.select_orb(1) and inventory.use_selected_orb(engine))
|
||||||
assert(inventory.cargo[1]==null and "booster" in engine.special_orbs)
|
assert(inventory.cargo[1]==null and "atomic_engine" in engine.special_orbs)
|
||||||
var radar := Control.new()
|
var radar := Control.new()
|
||||||
radar.set_script(preload("res://scripts/ui/scanner_overlay.gd"))
|
radar.set_script(preload("res://scripts/ui/scanner_overlay.gd"))
|
||||||
radar.player = player
|
radar.player = player
|
||||||
|
|||||||
42
tests/ships/instant_laboratory_test.gd
Normal file
42
tests/ships/instant_laboratory_test.gd
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
extends Node
|
||||||
|
const LAB := preload("res://scripts/planets/laboratory.gd")
|
||||||
|
const SKILLS := preload("res://scripts/captain/captain_skills.gd")
|
||||||
|
func _ready() -> void:
|
||||||
|
var captain := preload("res://scripts/captain/captain_progress.gd").new()
|
||||||
|
var progress: Resource = captain.expedition
|
||||||
|
progress.laboratory_offers.assign(["machine_gun"])
|
||||||
|
assert(not LAB.start(captain,"machine_gun"))
|
||||||
|
for key in LAB.COSTS.machine_gun: captain.minerals[key] = int(LAB.COSTS.machine_gun[key])
|
||||||
|
assert(LAB.start(captain,"machine_gun"))
|
||||||
|
assert(progress.laboratory_unlocks.get("machine_gun",false))
|
||||||
|
assert(progress.laboratory_remaining == 0 and progress.laboratory_project.is_empty())
|
||||||
|
for key in LAB.COSTS.machine_gun: assert(captain.minerals[key] == 0)
|
||||||
|
assert(not LAB.start(captain,"machine_gun"))
|
||||||
|
assert(not LAB.start(captain,"unknown_project"))
|
||||||
|
progress.laboratory_project = "beam_laser"
|
||||||
|
progress.laboratory_remaining = 450.0
|
||||||
|
progress.laboratory_duration = 600.0
|
||||||
|
assert(ResourceSaver.save(progress,"user://legacy_lab.tres") == OK)
|
||||||
|
var restored: Resource = ResourceLoader.load("user://legacy_lab.tres","",ResourceLoader.CACHE_MODE_IGNORE_DEEP)
|
||||||
|
LAB.ensure_offers(restored)
|
||||||
|
assert(restored.laboratory_unlocks.get("beam_laser",false))
|
||||||
|
assert(restored.laboratory_project.is_empty() and restored.laboratory_remaining == 0)
|
||||||
|
assert(restored.laboratory_offers.size() == 3)
|
||||||
|
assert(not LAB.advance(restored,100))
|
||||||
|
captain.skills = {"scientist":3,"gunsmith":2}
|
||||||
|
captain.learning_points = 1
|
||||||
|
captain.skill_offers.assign(["scientist","gunsmith","miner"])
|
||||||
|
captain.prepare_skill_offers()
|
||||||
|
assert(not SKILLS.IDS.has("scientist") and not SKILLS.DATA.has("scientist"))
|
||||||
|
assert(not captain.skills.has("scientist") and captain.skills.gunsmith == 2)
|
||||||
|
assert(captain.learning_points == 4 and not captain.skill_offers.has("scientist"))
|
||||||
|
captain.normalize_skill_ranks()
|
||||||
|
assert(captain.learning_points == 4,"Refund only once")
|
||||||
|
assert(not captain.can_train("scientist"))
|
||||||
|
var panel := preload("res://scripts/planets/laboratory_panel.gd").new()
|
||||||
|
panel.captain = captain
|
||||||
|
add_child(panel)
|
||||||
|
assert(panel.find_children("Research_*","Button",true,false).size() == 3)
|
||||||
|
assert(panel.find_children("*","ProgressBar",true,false).is_empty())
|
||||||
|
print("INSTANT_LABORATORY_TEST: PASS (immediate unlock, exact cost, invalid/duplicate guards, old project, removed skill, one-time refund, UI)")
|
||||||
|
get_tree().quit()
|
||||||
1
tests/ships/instant_laboratory_test.gd.uid
Normal file
1
tests/ships/instant_laboratory_test.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://dvumugcgys2bg
|
||||||
4
tests/ships/instant_laboratory_test.tscn
Normal file
4
tests/ships/instant_laboratory_test.tscn
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://tests/ships/instant_laboratory_test.gd" id="1"]
|
||||||
|
[node name="InstantLaboratoryTest" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
@ -42,7 +42,7 @@ func _ready() -> void:
|
|||||||
assert(definition.item_id in LAB.IDS)
|
assert(definition.item_id in LAB.IDS)
|
||||||
for id in LAB.IDS:
|
for id in LAB.IDS:
|
||||||
assert(LAB.definition(id) != null and LAB.COSTS.has(id))
|
assert(LAB.definition(id) != null and LAB.COSTS.has(id))
|
||||||
assert(not LAB.start(captain,offers[0]),"Laboratory must be built")
|
assert(progress.freighter_rooms.get("expansion_1") == "laboratory","Laboratory built from the start")
|
||||||
progress.freighter_rooms.expansion_1 = "laboratory"
|
progress.freighter_rooms.expansion_1 = "laboratory"
|
||||||
assert(not LAB.start(captain,offers[0]),"Cannot research without materials")
|
assert(not LAB.start(captain,offers[0]),"Cannot research without materials")
|
||||||
for cost in LAB.COSTS.values():
|
for cost in LAB.COSTS.values():
|
||||||
@ -65,16 +65,16 @@ func _ready() -> void:
|
|||||||
var before: Dictionary = captain.minerals.duplicate()
|
var before: Dictionary = captain.minerals.duplicate()
|
||||||
assert(LAB.start(captain,"incendiary_missile"))
|
assert(LAB.start(captain,"incendiary_missile"))
|
||||||
for key in LAB.COSTS.incendiary_missile: assert(captain.minerals[key]==before[key]-LAB.COSTS.incendiary_missile[key])
|
for key in LAB.COSTS.incendiary_missile: assert(captain.minerals[key]==before[key]-LAB.COSTS.incendiary_missile[key])
|
||||||
assert(not LAB.start(captain,"radiation_laser"),"Only one project at a time")
|
assert(not LAB.start(captain,"incendiary_missile"),"No duplicate discovery charge")
|
||||||
progress.analysis_samples = 10
|
progress.analysis_samples = 10
|
||||||
assert(preload("res://scripts/planets/alien_research.gd").purchase(progress,"general"))
|
assert(preload("res://scripts/planets/alien_research.gd").purchase(progress,"general"))
|
||||||
GameState.advance_play_time(570.0,game)
|
GameState.advance_play_time(570.0,game)
|
||||||
assert(progress.laboratory_remaining==30,"Research advances while docked without any mining stations")
|
assert(progress.laboratory_remaining==0,"Module discovery has no timer")
|
||||||
assert(progress.alien_remaining==30,"Both research stations advance in parallel")
|
assert(progress.alien_remaining==30,"Alien research still advances while docked")
|
||||||
assert(not progress.laboratory_unlocks.has("incendiary_missile"))
|
assert(progress.laboratory_unlocks.has("incendiary_missile"))
|
||||||
assert(ResourceSaver.save(progress,"user://laboratory_progress_test.tres")==OK)
|
assert(ResourceSaver.save(progress,"user://laboratory_progress_test.tres")==OK)
|
||||||
var saved: Resource = ResourceLoader.load("user://laboratory_progress_test.tres","",ResourceLoader.CACHE_MODE_IGNORE)
|
var saved: Resource = ResourceLoader.load("user://laboratory_progress_test.tres","",ResourceLoader.CACHE_MODE_IGNORE)
|
||||||
assert(saved.laboratory_project=="incendiary_missile" and saved.laboratory_remaining==30 and saved.laboratory_offers==offers)
|
assert(saved.laboratory_project.is_empty() and saved.laboratory_remaining==0 and saved.laboratory_unlocks.get("incendiary_missile",false))
|
||||||
await snapshot("res://artifacts/laboratory_progress.png")
|
await snapshot("res://artifacts/laboratory_progress.png")
|
||||||
progress.freighter_rooms.expansion_2 = "alien_research"
|
progress.freighter_rooms.expansion_2 = "alien_research"
|
||||||
manager.screen._select_section("expansion_2")
|
manager.screen._select_section("expansion_2")
|
||||||
@ -82,11 +82,11 @@ func _ready() -> void:
|
|||||||
manager._leave()
|
manager._leave()
|
||||||
get_tree().paused = true
|
get_tree().paused = true
|
||||||
GameState.advance_play_time(30,game)
|
GameState.advance_play_time(30,game)
|
||||||
assert(progress.laboratory_remaining==30,"Ordinary pause stops research")
|
assert(progress.laboratory_remaining==0,"Module discovery is already complete")
|
||||||
assert(progress.alien_remaining==30,"Ordinary pause stops alien research")
|
assert(progress.alien_remaining==30,"Ordinary pause stops alien research")
|
||||||
get_tree().paused = false
|
get_tree().paused = false
|
||||||
GameState.advance_play_time(29.9,game)
|
GameState.advance_play_time(29.9,game)
|
||||||
assert(not progress.laboratory_unlocks.has("incendiary_missile"))
|
assert(progress.laboratory_unlocks.has("incendiary_missile"))
|
||||||
GameState.advance_play_time(0.1,game)
|
GameState.advance_play_time(0.1,game)
|
||||||
assert(progress.laboratory_unlocks.has("incendiary_missile") and progress.laboratory_offers.size()==3)
|
assert(progress.laboratory_unlocks.has("incendiary_missile") and progress.laboratory_offers.size()==3)
|
||||||
assert(progress.alien_skills.general)
|
assert(progress.alien_skills.general)
|
||||||
|
|||||||
@ -20,10 +20,9 @@ func _ready() -> void:
|
|||||||
captain.level = 1
|
captain.level = 1
|
||||||
progress.laboratory_offers.assign(["virus"])
|
progress.laboratory_offers.assign(["virus"])
|
||||||
assert(LAB.start(captain,"virus"))
|
assert(LAB.start(captain,"virus"))
|
||||||
assert(progress.laboratory_remaining == 600)
|
assert(progress.laboratory_remaining == 0)
|
||||||
LAB.advance(progress,599)
|
assert(progress.laboratory_project.is_empty())
|
||||||
assert(not progress.laboratory_unlocks.get("virus",false))
|
assert(not LAB.start(captain,"virus"))
|
||||||
LAB.advance(progress,1)
|
|
||||||
assert(progress.laboratory_unlocks.virus)
|
assert(progress.laboratory_unlocks.virus)
|
||||||
for id in LAB.IDS:
|
for id in LAB.IDS:
|
||||||
if progress.laboratory_unlocks.get(id,false): continue
|
if progress.laboratory_unlocks.get(id,false): continue
|
||||||
@ -63,5 +62,5 @@ func _ready() -> void:
|
|||||||
assert(not restored.alien_skills.get("shields_b_next_next_next",false))
|
assert(not restored.alien_skills.get("shields_b_next_next_next",false))
|
||||||
ALIEN.advance(restored,0.1)
|
ALIEN.advance(restored,0.1)
|
||||||
assert(restored.alien_skills.shields_b_next_next_next)
|
assert(restored.alien_skills.shields_b_next_next_next)
|
||||||
print("RESEARCH PROGRESSION PASS: all 23+28 discoveries at captain level 1; level-independent offers, 600-second timers, single projects, costs, exact save/load continuation")
|
print("RESEARCH PROGRESSION PASS: all 23+28 discoveries at captain level 1; level-independent offers, instant module discoveries, timed alien research, costs, exact save/load continuation")
|
||||||
get_tree().quit()
|
get_tree().quit()
|
||||||
|
|||||||
107
tests/space_progression_test.gd
Normal file
107
tests/space_progression_test.gd
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
extends Node
|
||||||
|
var failures: Array[String] = []
|
||||||
|
var is_runner := false
|
||||||
|
func check(ok: bool, message: String) -> void:
|
||||||
|
if not ok:
|
||||||
|
failures.append(message)
|
||||||
|
push_error(message)
|
||||||
|
func _ready() -> void:
|
||||||
|
if not is_runner:
|
||||||
|
var runner := get_script().new() as Node
|
||||||
|
runner.is_runner = true
|
||||||
|
get_tree().root.add_child.call_deferred(runner)
|
||||||
|
else: run.call_deferred()
|
||||||
|
func frames(count := 3) -> void:
|
||||||
|
for index in count: await get_tree().process_frame
|
||||||
|
func run() -> void:
|
||||||
|
var state := GameState
|
||||||
|
state.start_new_run()
|
||||||
|
check(not preload("res://scripts/planets/expedition_controller.gd").PLANET_LANDING_ENABLED,"Landing disabled")
|
||||||
|
var checks := preload("res://tests/distant_expeditions_checks.gd").new()
|
||||||
|
for failure in checks.run_checks().failures: check(false,str(failure))
|
||||||
|
checks.free()
|
||||||
|
var map := {"id":"space_test","level":2,"seed":81234,"name":"Test","source_planet":"Boss","source_level":1}
|
||||||
|
state.captain.expedition.distant_maps[map.id] = map
|
||||||
|
state.captain.expedition.deuter = 0
|
||||||
|
check(state.depart_distant(map.id),"Departure without deuter")
|
||||||
|
check(state.captain.expedition.deuter == 0,"No fuel mutation")
|
||||||
|
check(not state.depart_distant(map.id),"No duplicate departure")
|
||||||
|
get_tree().change_scene_to_file("res://scenes/game/game.tscn")
|
||||||
|
await frames(8)
|
||||||
|
var game := get_tree().current_scene
|
||||||
|
var player := game.get_node("PlayerShip")
|
||||||
|
player.set_physics_process(false)
|
||||||
|
for enemy in get_tree().get_nodes_in_group("enemies"): enemy.set_physics_process(false)
|
||||||
|
var bases: Array[Node] = []
|
||||||
|
for poi in get_tree().get_nodes_in_group("exploration_pois"):
|
||||||
|
if str(poi.get("kind")) == "enemy_base": bases.append(poi)
|
||||||
|
check(bases.size() == 3,"Three resource bases")
|
||||||
|
var base := bases[0]
|
||||||
|
var rewards: Dictionary = base.materials(base.reward_seed,base.level)
|
||||||
|
var id: String = base.base_id
|
||||||
|
base.take_weapon_hit(1e12)
|
||||||
|
base.take_weapon_hit(1e12)
|
||||||
|
await frames()
|
||||||
|
check(state.explored_pois.has(id),"Base claimed persistently")
|
||||||
|
for pickup in get_tree().get_nodes_in_group("loot"):
|
||||||
|
if pickup.get_script() == preload("res://scripts/items/expedition_pickup.gd") and pickup.payload.has("materials"):
|
||||||
|
player.position = pickup.position
|
||||||
|
pickup._physics_process(0.0)
|
||||||
|
await frames()
|
||||||
|
for key in rewards: check(state.captain.minerals.get(key,0) == rewards[key],"Collect base material "+str(key))
|
||||||
|
var sample := Node2D.new()
|
||||||
|
sample.set_script(preload("res://scripts/items/expedition_pickup.gd"))
|
||||||
|
sample.payload = {"samples":1}
|
||||||
|
sample.position = player.position
|
||||||
|
game.add_child(sample)
|
||||||
|
sample._physics_process(0.0)
|
||||||
|
check(state.captain.expedition.analysis_samples == 1,"Collect sample")
|
||||||
|
var controller := game.get_node("PlanetExpeditions")
|
||||||
|
player.position = controller.freighter.position
|
||||||
|
controller.land_nearby()
|
||||||
|
await frames()
|
||||||
|
check(is_instance_valid(controller.screen),"Docking still works")
|
||||||
|
var screen: Control = controller.screen
|
||||||
|
var inventory := player.get_node("ShipInventory")
|
||||||
|
var weapon: ItemInstance = preload("res://scripts/items/item_generator.gd").generate_for_definition(inventory.weapon_slots[0].definition,10,2)
|
||||||
|
inventory.weapon_slots[0] = weapon
|
||||||
|
screen._tab = "hangar"
|
||||||
|
screen._refresh()
|
||||||
|
check(screen.find_child("RepairLander",true,false) == null,"No lander repair")
|
||||||
|
screen._tab = "workshop"
|
||||||
|
screen._refresh()
|
||||||
|
await frames()
|
||||||
|
check(screen.find_child("EngineerItem",true,false) != null,"Engineer on freighter")
|
||||||
|
state.captain.expedition.time_crystals = 100000
|
||||||
|
var service := preload("res://scripts/planets/time_engineer.gd")
|
||||||
|
var addon := -1
|
||||||
|
for index in weapon.additional_modifiers.size():
|
||||||
|
if service.affix_for(weapon,index) != null:
|
||||||
|
addon = index
|
||||||
|
weapon.additional_modifiers[index].value = 0.0
|
||||||
|
break
|
||||||
|
if addon >= 0:
|
||||||
|
check(service.maximize(state.captain.expedition,inventory,weapon,addon),"Engineer works outside surface")
|
||||||
|
check(not service.maximize(state.captain.expedition,inventory,weapon,addon),"No charge for max addon")
|
||||||
|
check(addon >= 0,"Test equipment has addon")
|
||||||
|
var orb := preload("res://scripts/items/special_orbs.gd").create_item("piercing")
|
||||||
|
check(inventory.add_item(orb),"Orb in cargo")
|
||||||
|
check(inventory.select_orb(inventory.cargo.find(orb)),"Click orb")
|
||||||
|
check(inventory.use_selected_orb(weapon),"Click equipment applies orb")
|
||||||
|
controller._leave()
|
||||||
|
game.set_physics_process(false)
|
||||||
|
state.restore_distant_home()
|
||||||
|
var enemy := get_tree().get_first_node_in_group("enemies")
|
||||||
|
enemy.loot_tier = 2
|
||||||
|
enemy._drop_loot()
|
||||||
|
await frames()
|
||||||
|
var maps := 0
|
||||||
|
for pickup in get_tree().get_nodes_in_group("loot"):
|
||||||
|
if pickup.get_script() == preload("res://scripts/items/expedition_pickup.gd") and pickup.payload.has("map"):
|
||||||
|
maps += 1
|
||||||
|
player.position = pickup.position
|
||||||
|
pickup._physics_process(0.0)
|
||||||
|
check(maps == 1,"Normal boss drops exactly one map")
|
||||||
|
check(state.captain.expedition.distant_maps.size() == 1,"Boss map collected")
|
||||||
|
print("SPACE_PROGRESSION_TEST: ",JSON.stringify({"failures":failures}))
|
||||||
|
get_tree().quit(0 if failures.is_empty() else 1)
|
||||||
1
tests/space_progression_test.gd.uid
Normal file
1
tests/space_progression_test.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://cbbffs63gc38t
|
||||||
6
tests/space_progression_test.tscn
Normal file
6
tests/space_progression_test.tscn
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://tests/space_progression_test.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="SpaceProgressionTest" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
Loading…
x
Reference in New Issue
Block a user