diff --git a/artifacts/chronal_workshop_ui.png b/artifacts/chronal_workshop_ui.png new file mode 100644 index 0000000..3d02c5d Binary files /dev/null and b/artifacts/chronal_workshop_ui.png differ diff --git a/artifacts/laboratory_projects.png.import b/artifacts/chronal_workshop_ui.png.import similarity index 72% rename from artifacts/laboratory_projects.png.import rename to artifacts/chronal_workshop_ui.png.import index 2b20fab..2c644a7 100644 --- a/artifacts/laboratory_projects.png.import +++ b/artifacts/chronal_workshop_ui.png.import @@ -2,16 +2,16 @@ importer="texture" type="CompressedTexture2D" -uid="uid://dpvckkd1oc052" -path="res://.godot/imported/laboratory_projects.png-61c88f4e4c24b4b7ef45dab06ea98448.ctex" +uid="uid://cinvyp3qbv5yf" +path="res://.godot/imported/chronal_workshop_ui.png-90ce303bc4785fc6c962cf551a161261.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://artifacts/laboratory_projects.png" -dest_files=["res://.godot/imported/laboratory_projects.png-61c88f4e4c24b4b7ef45dab06ea98448.ctex"] +source_file="res://artifacts/chronal_workshop_ui.png" +dest_files=["res://.godot/imported/chronal_workshop_ui.png-90ce303bc4785fc6c962cf551a161261.ctex"] [params] diff --git a/artifacts/command_layout_1280.png b/artifacts/command_layout_1280.png new file mode 100644 index 0000000..d23f2ea Binary files /dev/null and b/artifacts/command_layout_1280.png differ diff --git a/artifacts/command_layout_1280.png.import b/artifacts/command_layout_1280.png.import new file mode 100644 index 0000000..1acf8ce --- /dev/null +++ b/artifacts/command_layout_1280.png.import @@ -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 diff --git a/artifacts/command_layout_960.png b/artifacts/command_layout_960.png new file mode 100644 index 0000000..89eb10d Binary files /dev/null and b/artifacts/command_layout_960.png differ diff --git a/artifacts/command_layout_960.png.import b/artifacts/command_layout_960.png.import new file mode 100644 index 0000000..3c450db --- /dev/null +++ b/artifacts/command_layout_960.png.import @@ -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 diff --git a/artifacts/enemy_explosion_preview.png b/artifacts/enemy_explosion_preview.png new file mode 100644 index 0000000..8bf8fc1 Binary files /dev/null and b/artifacts/enemy_explosion_preview.png differ diff --git a/artifacts/enemy_explosion_preview.png.import b/artifacts/enemy_explosion_preview.png.import new file mode 100644 index 0000000..75aa79d --- /dev/null +++ b/artifacts/enemy_explosion_preview.png.import @@ -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 diff --git a/artifacts/enemy_explosion_validation.md b/artifacts/enemy_explosion_validation.md new file mode 100644 index 0000000..f24c1ec --- /dev/null +++ b/artifacts/enemy_explosion_validation.md @@ -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`. diff --git a/artifacts/space_progression_validation.md b/artifacts/space_progression_validation.md new file mode 100644 index 0000000..143c520 --- /dev/null +++ b/artifacts/space_progression_validation.md @@ -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. diff --git a/artifacts/workshop_and_instant_research_validation.md b/artifacts/workshop_and_instant_research_validation.md new file mode 100644 index 0000000..fb19aa6 --- /dev/null +++ b/artifacts/workshop_and_instant_research_validation.md @@ -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. diff --git a/scripts/captain/captain_progress.gd b/scripts/captain/captain_progress.gd index 314747b..4a3550f 100644 --- a/scripts/captain/captain_progress.gd +++ b/scripts/captain/captain_progress.gd @@ -47,6 +47,10 @@ func get_energy_bank_fraction() -> float: return maxf(0.10, get_skill_bonus("electrician")) 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(): if not SKILLS.DATA.has(skill): skills.erase(skill) @@ -94,7 +98,7 @@ func reset_skills() -> bool: return false 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: if not can_train(skill): @@ -112,6 +116,7 @@ func train_skill(skill: String) -> bool: return true func prepare_skill_offers() -> void: + normalize_skill_ranks() if learning_points <= 0 or not skill_offers.is_empty(): return var pool: Array[String] = [] diff --git a/scripts/captain/captain_skills.gd b/scripts/captain/captain_skills.gd index f02d9b2..89ac694 100644 --- a/scripts/captain/captain_skills.gd +++ b/scripts/captain/captain_skills.gd @@ -4,11 +4,10 @@ const MAX_RANK := 3 static var _icon_cache: Dictionary = {} const COOLDOWN := 30.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 := { "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"}, - "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"}, "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"}, @@ -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 "drone_commander": 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": 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": diff --git a/scripts/game/debug_rewards.gd b/scripts/game/debug_rewards.gd index 9d07b34..1ca36c1 100644 --- a/scripts/game/debug_rewards.gd +++ b/scripts/game/debug_rewards.gd @@ -1,7 +1,6 @@ extends RefCounted const MATERIALS := 10000 const CREDITS := 1000000 -const DEUTER := 1000000 const CRYSTALS := 10000 const SAMPLES := 1000 const ORBS := 25 @@ -10,7 +9,7 @@ const AMMUNITION := 10000 const RADIATION_AMMUNITION := 1000 const MISSILES := 1000 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: var progress: Resource = captain.expedition @@ -21,7 +20,6 @@ static func grant(captain: Resource, inventory: Node, level: int) -> Dictionary: captain.credits += CREDITS captain.learning_points += LEARNING_POINTS captain.prepare_skill_offers() - progress.deuter += DEUTER progress.time_crystals += CRYSTALS progress.analysis_samples += SAMPLES for orb in preload("res://scripts/items/special_orbs.gd").IDS: diff --git a/scripts/game/distant_sector.gd b/scripts/game/distant_sector.gd index 754338c..2a979e2 100644 --- a/scripts/game/distant_sector.gd +++ b/scripts/game/distant_sector.gd @@ -16,6 +16,7 @@ func _ready() -> void: if int(data.tier) == 2: game._pending_boss = data.duplicate() else: game._spawn_enemy(data,data.position) _build_structures() + _build_resource_bases() var expeditions := Node.new() expeditions.set_script(preload("res://scripts/planets/expedition_controller.gd")) expeditions.game = game @@ -36,6 +37,18 @@ func _ready() -> void: tween.tween_property(fade,"color:a",0.0,0.55) 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: var group := Node2D.new() group.name = "AncientStructures" diff --git a/scripts/game/enemy_destruction_effect.gd b/scripts/game/enemy_destruction_effect.gd new file mode 100644 index 0000000..3bfc244 --- /dev/null +++ b/scripts/game/enemy_destruction_effect.gd @@ -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) diff --git a/scripts/game/enemy_destruction_effect.gd.uid b/scripts/game/enemy_destruction_effect.gd.uid new file mode 100644 index 0000000..d5f4a86 --- /dev/null +++ b/scripts/game/enemy_destruction_effect.gd.uid @@ -0,0 +1 @@ +uid://dhhxuuns2fple diff --git a/scripts/game/enemy_ship.gd b/scripts/game/enemy_ship.gd index 7a06cbd..36e37b0 100644 --- a/scripts/game/enemy_ship.gd +++ b/scripts/game/enemy_ship.gd @@ -518,6 +518,7 @@ func _reward_captain() -> void: get_parent().call_deferred("add_child", credits) func _drop_loot() -> void: + preload("res://scripts/game/space_rewards.gd").drop(self) # Roll independently of equipment drops. var distant := not GameState.active_distant_map.is_empty() if distant: preload("res://scripts/game/distant_loot.gd").drop(self) @@ -547,10 +548,10 @@ func _spawn_destruction_effect() -> void: collapse.position = position get_parent().add_child(collapse) return - var effect := IMPACT_EFFECT_SCENE.instantiate() as Node2D - effect.global_position = global_position + var effect := Node2D.new() + effect.set_script(preload("res://scripts/game/enemy_destruction_effect.gd")) + effect.call("configure",_sprite,velocity,loot_tier) 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: if critical: diff --git a/scripts/game/game.gd b/scripts/game/game.gd index 52879f0..96e8504 100644 --- a/scripts/game/game.gd +++ b/scripts/game/game.gd @@ -262,7 +262,6 @@ func _load_next_level() -> void: 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 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 _load_distant.call_deferred(map_id) return true @@ -273,7 +272,6 @@ func _load_distant(map_id: String) -> void: GameState.restart_inventory.clear() _transitioning = false return - var cost: int = preload("res://scripts/planets/distant_expeditions.gd").fuel_cost(GameState.active_distant_map) var map_snapshot: Dictionary = GameState.active_distant_map.duplicate(true) var previous_pause := get_tree().paused var hud := get_node("HUD/HUDRoot") as CanvasItem @@ -309,7 +307,6 @@ func _load_distant(map_id: String) -> void: get_tree().paused = previous_pause GameState.restore_distant_home() GameState.captain.expedition.distant_maps[map_id] = map_snapshot - GameState.captain.expedition.deuter += cost GameState.captain.expedition.emit_changed() GameState.restart_inventory.clear() _transitioning = false diff --git a/scripts/game/game_state.gd b/scripts/game/game_state.gd index 9c48d38..96d9183 100644 --- a/scripts/game/game_state.gd +++ b/scripts/game/game_state.gd @@ -45,13 +45,10 @@ func load_game(slot_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 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} hull_bastion_used = false active_distant_map = map.duplicate(true) captain.expedition.distant_maps.erase(map_id) - captain.expedition.deuter -= cost system_level = int(map.level) system_seed = int(map.seed) captain.expedition.emit_changed() @@ -143,8 +140,7 @@ func advance_play_time(delta: float, scene: Node) -> void: captain.emit_changed() var inventory := scene.get_node_or_null("PlayerShip/ShipInventory") if inventory != null: inventory._recalculate_ship_stats() - var network := preload("res://scripts/planets/mining_network.gd") - if network.advance(captain.expedition, delta) <= 0 and not researching: return + if not researching: return _production_signal_time += delta if _production_signal_time >= 1.0: _production_signal_time = fmod(_production_signal_time, 1.0) diff --git a/scripts/game/save_service.gd b/scripts/game/save_service.gd index 4d2631e..0ce22d8 100644 --- a/scripts/game/save_service.gd +++ b/scripts/game/save_service.gd @@ -106,6 +106,8 @@ static func restore_state(state: Node, id: String = "") -> bool: if data.state.has(key): state.set(key, data.state[key]) state.captain = data.captain 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.restart_inventory = data.inventory state.return_to_freighter = false diff --git a/scripts/game/ship_controller.gd b/scripts/game/ship_controller.gd index 6f34ae2..12d54fc 100644 --- a/scripts/game/ship_controller.gd +++ b/scripts/game/ship_controller.gd @@ -561,19 +561,11 @@ func is_mini_boosting() -> bool: 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)) -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: _mini_boost_active = false if not Input.is_action_pressed("ship_mini_boost"): _mini_boost_exhausted = false 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"): return var cost := get_mini_boost_energy_cost() * delta @@ -584,7 +576,6 @@ func _update_mini_boost(delta: float) -> void: _mini_boost_active = true func get_mini_boost_energy_cost() -> float: - if has_free_mini_boost(): return 0.0 var engine: ItemInstance = $ShipInventory.get_item("module",2) var base_cost := maxf(MINI_BOOST_ENERGY_PER_SECOND,max_energy*0.22) return base_cost * (0.5 if engine != null and "atomic_engine" in engine.special_orbs else 1.0) diff --git a/scripts/game/space_rewards.gd b/scripts/game/space_rewards.gd new file mode 100644 index 0000000..6a416e0 --- /dev/null +++ b/scripts/game/space_rewards.gd @@ -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) diff --git a/scripts/game/space_rewards.gd.uid b/scripts/game/space_rewards.gd.uid new file mode 100644 index 0000000..a34abb0 --- /dev/null +++ b/scripts/game/space_rewards.gd.uid @@ -0,0 +1 @@ +uid://cu0ahvnuikwcc diff --git a/scripts/items/expedition_pickup.gd b/scripts/items/expedition_pickup.gd index e0abea3..c389ec4 100644 --- a/scripts/items/expedition_pickup.gd +++ b/scripts/items/expedition_pickup.gd @@ -14,7 +14,9 @@ func _ready() -> void: _label.size.x = 240 _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _label.add_theme_font_size_override("font_size",12) - _label.text = preload("res://scripts/items/special_orbs.gd").NAMES[str(payload.orb)] if payload.has("orb") else "Mapa 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) preload("res://scripts/items/loot_reveal_effect.gd").spawn(get_parent(),position,Color("b991ff"),3) func _process(delta: float) -> void: @@ -30,6 +32,11 @@ func _physics_process(delta: float) -> void: var distance := global_position.distance_to(_player.global_position) if distance <= 48: 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("orb"): var id := str(payload.orb) diff --git a/scripts/items/item_instance.gd b/scripts/items/item_instance.gd index 29618a2..2fcf7b8 100644 --- a/scripts/items/item_instance.gd +++ b/scripts/items/item_instance.gd @@ -246,7 +246,6 @@ func get_tooltip_text() -> String: lines.append(modifier.get_display_text()) for orb in special_orbs: if orb == "piercing": lines.append("\nORB PRZEBICIA · 50% obrażeń omija tarcze. Tylko ta broń.") - elif orb == "booster": lines.append("\nORB DOPALACZA · TAB bez kosztu energii. Działa po zamontowaniu silnika.") elif orb in ["electricity", "radiation", "kinetic", "atomic_engine", "polarization"]: lines.append("\n%s · %s" % [preload("res://scripts/items/special_orbs.gd").NAMES[orb], preload("res://scripts/items/special_orbs.gd").DESCRIPTIONS[orb]]) return "\n".join(lines) diff --git a/scripts/items/ship_inventory.gd b/scripts/items/ship_inventory.gd index a6d89ae..0d7b88d 100644 --- a/scripts/items/ship_inventory.gd +++ b/scripts/items/ship_inventory.gd @@ -218,6 +218,20 @@ func can_add_item(item: ItemInstance) -> bool: func migrate_orbs() -> void: 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(): var amount := int(progress.orbs[id]) 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: 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 Input.set_default_cursor_shape(Input.CURSOR_CROSS) if get_node_or_null("OrbTargetCursor") == null: @@ -477,6 +491,8 @@ func _add_starter_items() -> void: var item := ItemInstance.new() item.definition = definition item.item_level = 1 + item.damage_min = 4.0 + item.damage_max = 10.0 weapon_slots[0] = item func _remove_extension_addons() -> void: diff --git a/scripts/items/special_orbs.gd b/scripts/items/special_orbs.gd index bad21c3..8ed9717 100644 --- a/scripts/items/special_orbs.gd +++ b/scripts/items/special_orbs.gd @@ -1,12 +1,13 @@ extends RefCounted # Every orb is consumed once. Further advancement requires another orb. -const IDS := ["piercing", "booster", "advancement", "replication", "electricity", "radiation", "kinetic", "atomic_engine", "polarization"] -const COMMON_IDS := ["piercing", "booster", "advancement", "electricity", "radiation", "kinetic", "atomic_engine", "polarization"] +const IDS := ["piercing", "advancement", "replication", "electricity", "radiation", "kinetic", "atomic_engine", "polarization"] +const COMMON_IDS := ["piercing", "advancement", "electricity", "radiation", "kinetic", "atomic_engine", "polarization"] const DAMAGE_ORBS := ["electricity", "radiation", "kinetic"] -const NAMES := {"piercing":"Orb przebicia", "booster":"Orb dopalacza", "advancement":"Orb awansu", "replication":"Orb replikacji", "electricity":"Orb elektryczności", "radiation":"Orb radiacji", "kinetic":"Orb kinetyczny", "atomic_engine":"Orb silników atomowych", "polarization":"Orb polaryzacyjny"} -const DESCRIPTIONS := {"replication":"Tworzy dokładną, niezależną kopię wyposażenia w magazynie frachtowca. Oryginał zostaje. Kopiuje poziom, addony i efekty orbów. Drop: dodatkowe 5% z bossa dalekiej wyprawy.", "electricity":"Trwale zmienia typ obrażeń broni na energetyczny (jak laser). Nie zwiększa obrażeń ani kosztu strzału. Zastępuje poprzedni orb typu obrażeń.", "radiation":"Trwale zmienia typ obrażeń broni na radiacyjny. Nie zwiększa obrażeń ani kosztu strzału. Zastępuje poprzedni orb typu obrażeń.", "kinetic":"Trwale zmienia typ obrażeń broni na kinetyczny. Nie zwiększa obrażeń ani kosztu strzału. Zastępuje poprzedni orb typu obrażeń.", "atomic_engine":"Tylko silnik. Trwale zmniejsza zużycie energii dopalacza TAB o 50%. Nie sumuje się. Orb darmowego dopalacza ma pierwszeństwo.", "polarization":"Tylko tarcze. Trafienie w tarcze odnawia do 5% maksymalnych tarcz jako energię, nie więcej niż faktycznie stracone tarcze. Tempo odzysku: do 5% pojemności generatora/s, z rezerwą na jedno takie odnowienie. Uniki i trafienia w sam kadłub nie aktywują efektu.", "piercing":"50% obrażeń broni omija tarcze i trafia bezpośrednio w kadłub. Tylko broń.", "booster":"Dopalacz pod TAB nie zużywa energii i działa również przy pustym generatorze. Tylko silnik.", "advancement":"Awansuje przedmiot o 1 poziom i ponownie losuje jego parametry oraz wartości obecnych addonów. Zachowuje ich typy, liczbę i efekty orbów. Wyniki losowania mogą być niższe. Jedna sztuka daje jeden awans. Kolejny awans wymaga nowego orba (główna progresja do 120; 1000 to limit techniczny). Nie działa na materiały zużywalne."} +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ę.", "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: - 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: if id not in IDS or amount <= 0: return null var definition := ItemDefinition.new() @@ -22,6 +23,7 @@ static func create_item(id: String, amount := 1) -> ItemInstance: return item 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)) if inventory != null: for item in inventory.cargo.slice(0,inventory.unlocked_cargo_slots): diff --git a/scripts/levels/enemy_resource_base.gd b/scripts/levels/enemy_resource_base.gd new file mode 100644 index 0000000..ab86d6a --- /dev/null +++ b/scripts/levels/enemy_resource_base.gd @@ -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() diff --git a/scripts/levels/enemy_resource_base.gd.uid b/scripts/levels/enemy_resource_base.gd.uid new file mode 100644 index 0000000..7d6a4a2 --- /dev/null +++ b/scripts/levels/enemy_resource_base.gd.uid @@ -0,0 +1 @@ +uid://bvg6uj6e202hy diff --git a/scripts/planets/distant_expeditions.gd b/scripts/planets/distant_expeditions.gd index be437d1..1908032 100644 --- a/scripts/planets/distant_expeditions.gd +++ b/scripts/planets/distant_expeditions.gd @@ -27,7 +27,7 @@ static func map_cost(map: Dictionary) -> int: return maxi(1, int(map.level)) * CREDITS_PER_LEVEL 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: if not has_hermit(profile): return false diff --git a/scripts/planets/distant_expeditions_panel.gd b/scripts/planets/distant_expeditions_panel.gd index 25397bb..6a9b310 100644 --- a/scripts/planets/distant_expeditions_panel.gd +++ b/scripts/planets/distant_expeditions_panel.gd @@ -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) return MENU.label(section, "Sektory poza znanymi szlakami. Podwójna liczba wrogów, jeden boss. Kryształy czasu z każdego wroga. Zwykli wrogowie: mapy i powerbanki po 15%. Boss zawsze zostawia 1 mapę i 1 losowy orb, a dodatkowo ma 5% szansy na Orb replikacji.", 12) - var fuel := MENU.label(section, "", 13, Color("95dce8")) - fuel.name = "ExpeditionFuel" - screen._economy_labels["expedition_fuel"] = fuel - MENU.label(section, "Lot: poziom × 1000 deuteru. Powrót bezpłatny. Mapa zużywa się przy wylocie. Każdy sektor można odwiedzić tylko raz.", 12, MENU.GOLD) + 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) 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() maps.sort_custom(func(a,b): return int(a.level) 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) + fly.custom_minimum_size.x = 80 + fly.size_flags_vertical = Control.SIZE_SHRINK_CENTER + fly.tooltip_text = "Jednorazowa wyprawa. Zużywa mapę." diff --git a/scripts/planets/expedition_controller.gd b/scripts/planets/expedition_controller.gd index 6f453ae..fb02afe 100644 --- a/scripts/planets/expedition_controller.gd +++ b/scripts/planets/expedition_controller.gd @@ -2,6 +2,7 @@ extends Node const UI := preload("res://scripts/planets/expedition_ui.gd") const CONTACT := preload("res://scripts/ui/orbital_contact_style.gd") const PROFILES := preload("res://scripts/planets/planet_profiles.gd") +const PLANET_LANDING_ENABLED := false const FREIGHTER_SIZE := 285.0 const CONTACT_RANGE := preload("res://scripts/planets/orbital_contact_range.gd") var game: Node2D @@ -94,7 +95,7 @@ func _refresh_prompt() -> void: _near_planet = null var best := INF 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 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: @@ -127,7 +128,7 @@ func _refresh_prompt() -> void: elif nearby_freighter: _contact_label.text = "● FREGATA / POŁĄCZENIE GOTOWE" 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: var profile: Dictionary = _near_planet.planet_profile _contact_label.text = "● TELEMETRIA / DANE PLANETY" @@ -193,7 +194,7 @@ func land_nearby() -> void: if CONTACT_RANGE.at_freighter(player,freighter): open_freighter() return - if not is_instance_valid(_near_planet): + if not PLANET_LANDING_ENABLED or not is_instance_valid(_near_planet): return var progress: Resource = GameState.captain.expedition if not progress.begin_visit(_near_planet.planet_profile): diff --git a/scripts/planets/freighter_command.gd b/scripts/planets/freighter_command.gd index 208102c..b32b3a9 100644 --- a/scripts/planets/freighter_command.gd +++ b/scripts/planets/freighter_command.gd @@ -1,9 +1,7 @@ extends RefCounted const MENU := preload("res://scripts/planets/freighter_menu.gd") -const NETWORK := preload("res://scripts/planets/mining_network.gd") static func build(screen: Control) -> void: - var progress: Resource = screen.captain.expedition var navigation := MENU.card(screen._content) 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) @@ -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) MENU.label(navigation, "Nowe układy odblokujesz, przechodząc dalej przez tunel po pokonaniu przeciwników.", 11, MENU.MUTED) preload("res://scripts/planets/distant_expeditions_panel.gd").build(screen) - var summary := MENU.card(screen._content) - 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() static func cabin(screen: Control) -> void: - var progress: Resource = screen.captain.expedition - var member := NETWORK.cabin_crew(progress, screen._tab) - 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))) + MENU.label(screen._content, "SEKCJA REZERWOWA", 14, MENU.GOLD) + MENU.label(screen._content, "Pomieszczenie czeka na nowe przeznaczenie.", 12) diff --git a/scripts/planets/freighter_deck.gd b/scripts/planets/freighter_deck.gd index 2294498..7e30422 100644 --- a/scripts/planets/freighter_deck.gd +++ b/scripts/planets/freighter_deck.gd @@ -3,7 +3,7 @@ signal section_selected(section: String) signal section_hovered(section: String) const DESIGN_SIZE := Vector2(1792,768) 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":"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}, @@ -53,7 +53,7 @@ func room(id: String) -> Dictionary: return {} 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: if ROOM_PLANS.has(id): return ROOM_PLANS[id] diff --git a/scripts/planets/freighter_screen.gd b/scripts/planets/freighter_screen.gd index 1817d74..622d320 100644 --- a/scripts/planets/freighter_screen.gd +++ b/scripts/planets/freighter_screen.gd @@ -17,6 +17,9 @@ var _owned_open := false var _storage_columns: GridContainer var _storage_notice := "" var _economy_labels: Dictionary = {} +var _engineer_item: ItemInstance +var _engineer_addon := 0 +var _engineer_notice := "" var _alien_timer: Label var _alien_signature := "" @onready var deck: Control = $Deck @@ -26,6 +29,7 @@ func _ready() -> void: offset_top = preload("res://scripts/ui/resource_bar.gd").HEIGHT if captain == null: captain = GameState.captain + captain.expedition.freighter_rooms["expansion_1"] = "laboratory" captain.expedition.freighter_rooms["expansion_3"] = "module_storage" captain.expedition.changed.connect(_refresh_live_labels) 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: _economy_labels.clear() _storage_columns = null - _audio.set_maintenance_needed(captain.expedition.lander_hp < 30) + _audio.set_maintenance_needed(false) for child in _content.get_children(): _content.remove_child(child) child.queue_free() @@ -279,7 +283,6 @@ func _construct_room(id: String) -> void: _refresh() func _build_hangar() -> void: - var progress: Resource = captain.expedition MENU.hero(_content,"hangar","") var ship_row := MENU.row(_content) MENU.icon(ship_row,deck.ship_texture,32) @@ -288,51 +291,7 @@ func _build_hangar() -> void: ship_row.add_child(ship_text) 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) - 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() - 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: if not is_instance_valid(inventory): @@ -385,18 +344,6 @@ func _unlock_ship_slot(kind: String) -> void: func _build_storage() -> void: 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"): _build_resource_overview() 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) values.time_crystals = captain.expedition.time_crystals values.samples = captain.expedition.analysis_samples - values.deuter = captain.expedition.deuter var names: Dictionary = RECIPES.NAMES.duplicate() 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"}) @@ -557,8 +503,10 @@ func _craft_supply(id: String, batches := 1) -> void: func _build_workshop() -> void: MENU.hero(_content,"workshop","") _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) + 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() grid.columns = 3 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: _alien_signature = alien_signature _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: if _tab != "workshop": return @@ -655,3 +596,13 @@ func _research_completed() -> void: _audio.service_completed() captain.emit_changed() _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) diff --git a/scripts/planets/laboratory.gd b/scripts/planets/laboratory.gd index d9d6a17..76ac7f2 100644 --- a/scripts/planets/laboratory.gd +++ b/scripts/planets/laboratory.gd @@ -1,5 +1,5 @@ extends RefCounted -const DURATION := 600.0 +const DURATION := 0.0 const IDS := [ "gamma_cannon", "beam_laser", "machine_gun", "missile_launcher", "unguided_missile", "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 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 var pool := IDS.duplicate() for id in progress.laboratory_unlocks: pool.erase(id) @@ -54,24 +55,28 @@ static func can_start(captain: Resource, id: String) -> bool: static func start(captain: Resource, id: String) -> bool: 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]) - captain.expedition.laboratory_project = id - captain.expedition.laboratory_duration = research_duration(captain) - captain.expedition.laboratory_remaining = captain.expedition.laboratory_duration + var progress: Resource = captain.expedition + progress.laboratory_unlocks[id] = true + progress.laboratory_duration = 0.0 + progress.laboratory_remaining = 0.0 + progress.laboratory_offers.clear() + ensure_offers(progress) captain.emit_changed() captain.expedition.emit_changed() return true -static func research_duration(captain: Resource) -> float: - return DURATION * (1.0 - captain.get_skill_bonus("scientist")) +static func research_duration(_captain: Resource) -> float: + return 0.0 -static func advance(progress: Resource, delta: float) -> bool: - if progress.laboratory_project.is_empty() or delta <= 0.0: return false - progress.laboratory_remaining = maxf(0.0,progress.laboratory_remaining-delta) - if progress.laboratory_remaining <= 0.00001: - progress.laboratory_remaining = 0.0 +static func advance(progress: Resource, _delta: float) -> bool: + # Finish an already-paid project restored from an older save immediately. + if progress.laboratory_project.is_empty(): return false + if progress.laboratory_project in IDS: progress.laboratory_unlocks[progress.laboratory_project] = true - progress.laboratory_project = "" - progress.laboratory_offers.clear() - ensure_offers(progress) - progress.emit_changed() + progress.laboratory_project = "" + progress.laboratory_remaining = 0.0 + progress.laboratory_duration = 0.0 + progress.laboratory_offers.clear() + ensure_offers(progress) + progress.emit_changed() return true diff --git a/scripts/planets/laboratory_panel.gd b/scripts/planets/laboratory_panel.gd index 50a0b34..7ddb398 100644 --- a/scripts/planets/laboratory_panel.gd +++ b/scripts/planets/laboratory_panel.gd @@ -3,8 +3,6 @@ const LAB := preload("res://scripts/planets/laboratory.gd") const MENU := preload("res://scripts/planets/freighter_menu.gd") var captain: Resource var _signature := "" -var _timer: Label -var _bar: ProgressBar var _buttons: Dictionary = {} func _ready() -> void: @@ -12,13 +10,10 @@ func _ready() -> void: _build() 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: 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: if is_instance_valid(_buttons[id]): _buttons[id].disabled = not LAB.can_start(captain,id) @@ -27,40 +22,28 @@ func _build() -> void: for child in get_children(): remove_child(child) child.queue_free() - _timer = null _buttons.clear() _signature = _state() var progress: Resource = captain.expedition 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) - if not progress.laboratory_project.is_empty(): - var item := LAB.definition(progress.laboratory_project) - var row := MENU.row(self) + MENU.label(self,"Odkryj projekt za surowce — od razu trafi do puli łupów.",12,MENU.MUTED) + for id in progress.laboratory_offers: + var item := LAB.definition(id) + var card := MENU.card(self) + var row := MENU.row(card) 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: - var item := LAB.definition(id) - var card := MENU.card(self) - var row := MENU.row(card) - MENU.icon(row,item.icon,64) - MENU.label(row,item.display_name,15,MENU.GOLD).size_flags_horizontal = Control.SIZE_EXPAND_FILL - MENU.label(card,item.description,12) - var names := preload("res://scripts/items/crafting_recipes.gd").NAMES.duplicate() - names.merge(preload("res://scripts/planets/planet_profiles.gd").MATERIAL_NAMES) - 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])]) - 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) - button.name = "Research_"+id - button.disabled = not LAB.can_start(captain,id) - _buttons[id] = button - if progress.laboratory_offers.is_empty(): MENU.label(self,"Wszystkie dostępne projekty zostały wynalezione.",14,MENU.GOLD) + MENU.label(row,item.display_name,15,MENU.GOLD).size_flags_horizontal = Control.SIZE_EXPAND_FILL + MENU.label(card,item.description,12) + var names := preload("res://scripts/items/crafting_recipes.gd").NAMES.duplicate() + names.merge(preload("res://scripts/planets/planet_profiles.gd").MATERIAL_NAMES) + 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])]) + MENU.label(card,"KOSZT · posiadasz / wymagane\n"+" · ".join(costs),12,MENU.MUTED) + var button := MENU.button(card,"ODKRYJ PROJEKT",_start.bind(id),true) + button.name = "Research_"+id + button.disabled = not LAB.can_start(captain,id) + _buttons[id] = button + if progress.laboratory_offers.is_empty(): MENU.label(self,"Wszystkie dostępne projekty zostały wynalezione.",14,MENU.GOLD) if not progress.laboratory_unlocks.is_empty(): MENU.label(self,"ODKRYTE · DOSTĘPNE W ŁUPACH",13,MENU.GOLD) for id in LAB.IDS: diff --git a/scripts/planets/planet_progress.gd b/scripts/planets/planet_progress.gd index 62aa1ba..7578625 100644 --- a/scripts/planets/planet_progress.gd +++ b/scripts/planets/planet_progress.gd @@ -15,8 +15,8 @@ const PROFILES := preload("res://scripts/planets/planet_profiles.gd") @export var laboratory_offers: Array[String] = [] @export var laboratory_project: String = "" @export var laboratory_remaining: float = 0.0 -@export var laboratory_duration: float = 600.0 -@export var freighter_rooms: Dictionary = {"expansion_3":"module_storage"} +@export var laboratory_duration: float = 0.0 +@export var freighter_rooms: Dictionary = {"expansion_1":"laboratory", "expansion_3":"module_storage"} @export var mining_stations: Dictionary = {} @export var distant_maps: Dictionary = {} @export var hermit_purchases: Dictionary = {} diff --git a/scripts/planets/time_engineer.gd b/scripts/planets/time_engineer.gd index 0ab3bc2..a8a0f74 100644 --- a/scripts/planets/time_engineer.gd +++ b/scripts/planets/time_engineer.gd @@ -30,7 +30,7 @@ static func can_maximize(item: ItemInstance, addon: int) -> bool: return item.additional_modifiers[addon].value < maximum(item,addon) - 0.000001 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 var owned: Array = inventory.cargo + inventory.weapon_slots + inventory.module_slots + inventory.extension_slots if not owned.has(item): return false diff --git a/scripts/planets/time_engineer_panel.gd b/scripts/planets/time_engineer_panel.gd index 78aec3a..6615ec1 100644 --- a/scripts/planets/time_engineer_panel.gd +++ b/scripts/planets/time_engineer_panel.gd @@ -2,10 +2,15 @@ extends RefCounted const CARD := preload("res://scripts/planets/poi_presentation.gd") const SERVICE := preload("res://scripts/planets/time_engineer.gd") 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: - var parent: VBoxContainer = screen._choices - var progress: Resource = screen.progress +static func build(screen: Control, message: String = "", freighter := false) -> void: + if freighter: + _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() art.texture = load("res://assets/planets/props/time_engineer.png") 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) 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) - 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) - _back(parent,screen) + _back(parent,screen,freighter) 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 locations: Array[String] = [] for kind in ["weapon","module","extension","cargo"]: @@ -32,8 +37,8 @@ static func build(screen: Control, message: String = "") -> void: items.append(item) locations.append({"weapon":"Broń","module":"Moduł","extension":"Rozszerzenie","cargo":"Ładownia"}[kind]) 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) - _back(parent,screen) + CARD.text(parent,"Brak przedmiotów z addonami na statku. Przenieś wyposażenie z magazynu fregaty do ładowni.",12,CARD.GOLD) + _back(parent,screen,freighter) return if not items.has(screen._engineer_item): screen._engineer_item = items[0] @@ -48,7 +53,7 @@ static func build(screen: Control, message: String = "") -> void: choice.item_selected.connect(func(index): screen._engineer_item = choice.get_item_metadata(index) screen._engineer_addon = 0 - screen._show_poi() + screen._refresh() if freighter else screen._show_poi() ) var item: ItemInstance = screen._engineer_item 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 addons.item_count == 0: CARD.text(parent,"Brak dostępnych addonów.",12) - _back(parent,screen) + _back(parent,screen,freighter) return addons.select(selected) screen._engineer_addon = int(addons.get_item_metadata(selected)) addons.item_selected.connect(func(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 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) elif progress.time_crystals < cost: 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: var choice := OptionButton.new() @@ -99,5 +104,224 @@ static func _choice(parent: Node, node_name: String) -> OptionButton: parent.add_child(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()) + +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 diff --git a/scripts/ui/captain_equipment_panel.gd b/scripts/ui/captain_equipment_panel.gd index c46cb76..e7efd2c 100644 --- a/scripts/ui/captain_equipment_panel.gd +++ b/scripts/ui/captain_equipment_panel.gd @@ -74,5 +74,5 @@ func _refresh_wardrobe() -> void: var signature := str(captain.gear_items)+str(captain.gear_slots) if signature == _gear_signature: return _gear_signature = signature - _protection.text = "Dopasuj poziom sprzętu do planety." - _protection.tooltip_text = "Dopasuj poziom wyposażenia do planety, aby odzyskać więcej łupu i ograniczyć uszkodzenia lądownika." + _protection.text = "Sprzęt planetarny · zastosowanie wstrzymane." + _protection.tooltip_text = "Wyposażenie zachowano na przyszłość. Lądowanie i eksploracja powierzchni są obecnie wyłączone." diff --git a/scripts/ui/captain_gear_slot.gd b/scripts/ui/captain_gear_slot.gd index a6ccd7a..7969cb7 100644 --- a/scripts/ui/captain_gear_slot.gd +++ b/scripts/ui/captain_gear_slot.gd @@ -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"] 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ę."} - 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() func _style() -> void: diff --git a/scripts/ui/player_guide.gd b/scripts/ui/player_guide.gd index 146699c..d89c473 100644 --- a/scripts/ui/player_guide.gd +++ b/scripts/ui/player_guide.gd @@ -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."], ["Ł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."] - ,["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."] ] var _panel: PanelContainer diff --git a/scripts/ui/resource_bar.gd b/scripts/ui/resource_bar.gd index dd939de..f95cb3d 100644 --- a/scripts/ui/resource_bar.gd +++ b/scripts/ui/resource_bar.gd @@ -3,7 +3,6 @@ const HEIGHT := 16.0 const ENTRIES := [ ["credits","Kredyty","svg",Color("efd48e")], ["parts","Części","png",Color("dfded2")], - ["deuter","Deuter","svg",Color("9cdeef")], ["time_crystals","Kryształy czasu","png",Color("d4b6ff")], ["iron","Żelazo","png",Color("d3dce0")], ["titanium","Tytan","png",Color("d3dce0")], diff --git a/scripts/ui/scanner_overlay.gd b/scripts/ui/scanner_overlay.gd index 3f5ceed..ff34415 100644 --- a/scripts/ui/scanner_overlay.gd +++ b/scripts/ui/scanner_overlay.gd @@ -76,6 +76,8 @@ func _draw() -> void: if bool(target.get("_claimed")): continue match str(target.get("kind")): + "enemy_base": + color = Color("ff8060") "asteroid": color = Color("a8adb5") "wreck": diff --git a/scripts/ui/ship_inventory_panel.gd b/scripts/ui/ship_inventory_panel.gd index 8c844ec..a2be9a8 100644 --- a/scripts/ui/ship_inventory_panel.gd +++ b/scripts/ui/ship_inventory_panel.gd @@ -37,7 +37,7 @@ func _refresh_title() -> void: var stock := {} for key in preload("res://scripts/planets/planet_profiles.gd").MATERIALS: 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"]: _resource_cards[key].set_amount(float(GameState.captain.minerals.get(key, 0))) _resource_cards["parts"].set_amount(GameState.captain.parts) diff --git a/tests/advancement_checks.gd b/tests/advancement_checks.gd index 274b11d..cc5da8b 100644 --- a/tests/advancement_checks.gd +++ b/tests/advancement_checks.gd @@ -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") 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) - 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() return {"failures":failures,"definitions_checked":checked,"passed":failures.is_empty()} diff --git a/tests/captain/new_skills_test.gd b/tests/captain/new_skills_test.gd index 907158f..51c3b23 100644 --- a/tests/captain/new_skills_test.gd +++ b/tests/captain/new_skills_test.gd @@ -3,7 +3,7 @@ extends Node2D const SKILLS := preload("res://scripts/captain/captain_skills.gd") const LAB := preload("res://scripts/planets/laboratory.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: GameState.start_new_run() @@ -17,7 +17,7 @@ func _ready() -> void: var missile := ItemInstance.new() missile.definition = preload("res://resources/items/missile_launcher.tres") 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: assert(SKILLS.icon(id) != null) for rank in range(1,4): @@ -36,11 +36,7 @@ func _ready() -> void: progress.laboratory_unlocks.clear() for key in LAB.COSTS.machine_gun: captain.minerals[key] = 100 assert(LAB.start(captain,"machine_gun")) - assert(is_equal_approx(progress.laboratory_remaining,[540.0,480.0,420.0][rank-1])) - 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_remaining == 0) assert(progress.laboratory_unlocks.has("machine_gun")) for id in ["ammunition","irradiated_ammunition"]: for key in RECIPES.DATA[id].cost: captain.minerals[key] = 100 @@ -86,7 +82,7 @@ func _ready() -> void: stub.free() ship.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() class RepairShip extends CharacterBody2D: diff --git a/tests/captain/salvage_drone_skills_test.gd b/tests/captain/salvage_drone_skills_test.gd index beea277..a6172f4 100644 --- a/tests/captain/salvage_drone_skills_test.gd +++ b/tests/captain/salvage_drone_skills_test.gd @@ -7,7 +7,7 @@ const PROJECTILE := preload("res://scripts/game/weapon_projectile.gd") func _ready() -> void: GameState.start_new_run() var captain := GameState.captain - assert(SKILLS.IDS.size() == 16) + assert(SKILLS.IDS.size() == 15) for id in ["scrap_collector","drone_commander"]: assert(SKILLS.icon(id) != null) captain.learning_points = 1 diff --git a/tests/distant_defeat_return.gd b/tests/distant_defeat_return.gd index ce2215a..c7d1baa 100644 --- a/tests/distant_defeat_return.gd +++ b/tests/distant_defeat_return.gd @@ -30,7 +30,7 @@ func _run() -> void: assert(GameState.depart_distant("defeat_test")) assert(not GameState.hull_bastion_used,"New expedition receives its own Last Bastion use") 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") await frames() 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.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(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(GameState.explored_pois.has("777:kept"),"Failed expedition must not reset home exploration") 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(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(not progress.distant_maps.has("defeat_test") and progress.deuter==5000) - print("DISTANT_DEFEAT_RETURN: PASS (original system and seed, consumed map/fuel, preserved inventory and found maps, freighter docking, duplicate retry guarded)") + assert(not progress.distant_maps.has("defeat_test") and progress.deuter==130000) + print("DISTANT_DEFEAT_RETURN: PASS (original system and seed, consumed map, no fuel cost, preserved inventory and found maps, freighter docking, duplicate retry guarded)") diff --git a/tests/distant_expeditions_checks.gd b/tests/distant_expeditions_checks.gd index d7f876e..045c194 100644 --- a/tests/distant_expeditions_checks.gd +++ b/tests/distant_expeditions_checks.gd @@ -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 map != DATA.offer(candidate,source): failures.append("Offer rerolled") 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 progress: Resource = captain.expedition var map := DATA.visit_offer(progress,profile,120) diff --git a/tests/distant_integration.gd b/tests/distant_integration.gd index 4a6b619..852e6df 100644 --- a/tests/distant_integration.gd +++ b/tests/distant_integration.gd @@ -43,10 +43,7 @@ func _run() -> void: GameState.system_seed = 777 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.deuter = 124999 - 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 + GameState.captain.expedition.deuter = 0 get_tree().change_scene_to_file("res://scenes/game/game.tscn") await frames() var game := get_tree().current_scene @@ -59,20 +56,21 @@ func _run() -> void: engine.definition = preload("res://resources/items/engine_extension.tres") inventory.module_slots[2] = engine 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,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(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(player.has_free_mini_boost(),"Equipped engine") + check(ORBS.apply(progress,inventory,engine,"atomic_engine"),"Apply booster") + 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") player.energy = 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") 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 check(game.travel_to_distant("integration"),"Flight requested") await frames(3) @@ -84,7 +82,7 @@ func _run() -> void: await frames(8) game = get_tree().current_scene 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(GameState.system_level==125 and GameState.highest_system==7,"Isolated progression") check(game.get_node_or_null("CelestialBodies")==null,"No celestial bodies") @@ -98,7 +96,7 @@ func _run() -> void: inventory = player.get_node("ShipInventory") player.is_docked = true 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") enemy.absorption = Vector3.ZERO enemy.shields = 100 @@ -128,12 +126,12 @@ func _run() -> void: projectile.free() var payload := Node2D.new() payload.set_script(preload("res://scripts/items/expedition_pickup.gd")) - payload.payload = {"orb":"booster"} + payload.payload = {"orb":"atomic_engine"} payload.position = player.position 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) - 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() for victim in get_tree().get_nodes_in_group("enemies"): victim.queue_free() await frames() diff --git a/tests/items/combat_orbs_checks.gd b/tests/items/combat_orbs_checks.gd index df6f94a..52bf88e 100644 --- a/tests/items/combat_orbs_checks.gd +++ b/tests/items/combat_orbs_checks.gd @@ -84,8 +84,8 @@ func run(game: Node) -> Array[String]: player._update_mini_boost(1.0) Input.action_release("ship_mini_boost") check(is_equal_approx(player.energy,50.0-normal_boost_cost*0.5), "Actual atomic boost consumes half energy") - check(ORBS.apply(progress,inventory,engine,"booster"), "Free booster stacks by precedence") - check(is_zero_approx(player.get_mini_boost_energy_cost()), "Free booster remains free") + check(not ORBS.apply(progress,inventory,engine,"booster"), "Retired orb rejected") + 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") player.is_docked = false player.absorption = Vector3.ZERO diff --git a/tests/items/enemy_destruction_test.gd b/tests/items/enemy_destruction_test.gd new file mode 100644 index 0000000..d8276e1 --- /dev/null +++ b/tests/items/enemy_destruction_test.gd @@ -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() diff --git a/tests/items/enemy_destruction_test.gd.uid b/tests/items/enemy_destruction_test.gd.uid new file mode 100644 index 0000000..ebeff78 --- /dev/null +++ b/tests/items/enemy_destruction_test.gd.uid @@ -0,0 +1 @@ +uid://ca6q5x0iq5uh1 diff --git a/tests/items/enemy_destruction_test.tscn b/tests/items/enemy_destruction_test.tscn new file mode 100644 index 0000000..b57b059 --- /dev/null +++ b/tests/items/enemy_destruction_test.tscn @@ -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") diff --git a/tests/items/retired_orb_test.gd b/tests/items/retired_orb_test.gd new file mode 100644 index 0000000..ff69227 --- /dev/null +++ b/tests/items/retired_orb_test.gd @@ -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() diff --git a/tests/items/retired_orb_test.gd.uid b/tests/items/retired_orb_test.gd.uid new file mode 100644 index 0000000..b501380 --- /dev/null +++ b/tests/items/retired_orb_test.gd.uid @@ -0,0 +1 @@ +uid://csk48ax4146ya diff --git a/tests/items/retired_orb_test.tscn b/tests/items/retired_orb_test.tscn new file mode 100644 index 0000000..b16dd6a --- /dev/null +++ b/tests/items/retired_orb_test.tscn @@ -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") diff --git a/tests/levels/equipment_safety_test.gd b/tests/levels/equipment_safety_test.gd index e4762ff..3d08076 100644 --- a/tests/levels/equipment_safety_test.gd +++ b/tests/levels/equipment_safety_test.gd @@ -21,7 +21,7 @@ func _ready() -> void: engine.special_orbs.append("atomic_engine") assert(is_equal_approx(player.get_mini_boost_energy_cost(),cost*0.5)) engine.special_orbs.append("booster") - assert(is_zero_approx(player.get_mini_boost_energy_cost())) + 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) shield.special_orbs.append("polarization") inventory.module_slots[0] = shield diff --git a/tests/levels/ship_level_120_audit.gd b/tests/levels/ship_level_120_audit.gd index afd276a..4a6c6b6 100644 --- a/tests/levels/ship_level_120_audit.gd +++ b/tests/levels/ship_level_120_audit.gd @@ -64,8 +64,6 @@ func _ready() -> void: var engine: ItemInstance = inventory.module_slots[2] engine.special_orbs.append("atomic_engine") report["atomic_engine_cost_s"] = player.get_mini_boost_energy_cost() - engine.special_orbs.append("booster") - report["booster_cost_s"] = player.get_mini_boost_energy_cost() var enemy := preload("res://scenes/game/enemy_ship.tscn").instantiate() add_child(enemy) enemy.set_physics_process(false) diff --git a/tests/previews/enemy_explosion_preview.gd b/tests/previews/enemy_explosion_preview.gd new file mode 100644 index 0000000..0258ddb --- /dev/null +++ b/tests/previews/enemy_explosion_preview.gd @@ -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) diff --git a/tests/previews/enemy_explosion_preview.gd.uid b/tests/previews/enemy_explosion_preview.gd.uid new file mode 100644 index 0000000..e94f462 --- /dev/null +++ b/tests/previews/enemy_explosion_preview.gd.uid @@ -0,0 +1 @@ +uid://7gjagxfla1tf diff --git a/tests/previews/enemy_explosion_preview.tscn b/tests/previews/enemy_explosion_preview.tscn new file mode 100644 index 0000000..6237c13 --- /dev/null +++ b/tests/previews/enemy_explosion_preview.tscn @@ -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") diff --git a/tests/previews/new_captain_skills_preview.gd b/tests/previews/new_captain_skills_preview.gd index 4705b91..c5b597f 100644 --- a/tests/previews/new_captain_skills_preview.gd +++ b/tests/previews/new_captain_skills_preview.gd @@ -15,7 +15,7 @@ func _ready() -> void: for enemy in get_tree().get_nodes_in_group("enemies"): enemy.set_physics_process(false) var ui: CanvasLayer = game.find_child("CaptainSkills",true,false) captain.learning_points = 1 - captain.skill_offers.assign(["scientist","gunsmith","bounty_hunter"]) + captain.skill_offers.assign(["missile_specialist","gunsmith","bounty_hunter"]) ui.open_management() await _snapshot("res://artifacts/new_captain_skills_economy.png") captain.skill_offers.assign(["missile_specialist","emergency_procedure"]) diff --git a/tests/save_settings_test.gd b/tests/save_settings_test.gd index db846ae..57e966b 100644 --- a/tests/save_settings_test.gd +++ b/tests/save_settings_test.gd @@ -57,7 +57,7 @@ func run() -> void: game.free() check(GameState.continue_game(),"save restores state") 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.crew.cabin_1.level == 4,"crew restored") check(GameState.captain.expedition.alien_skills.general,"research restored") diff --git a/tests/ships/artillery_orb_radar_test.gd b/tests/ships/artillery_orb_radar_test.gd index 030cc6d..ba6b3a4 100644 --- a/tests/ships/artillery_orb_radar_test.gd +++ b/tests/ships/artillery_orb_radar_test.gd @@ -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") var other := Node2D.new() other.set_script(PICKUP) - other.item = ORBS.create_item("booster") + other.item = ORBS.create_item("atomic_engine") other.position = player.position game.add_child(other) other._physics_process(0) assert(not other.is_queued_for_deletion(),"Different orb stays in space while cargo is full") inventory.cargo[1] = null 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() source.slot_kind = "cargo" source.slot_index = 0 @@ -71,7 +71,7 @@ func _ready() -> void: engine.definition = preload("res://resources/items/engine_extension.tres") inventory.module_slots[2] = 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() radar.set_script(preload("res://scripts/ui/scanner_overlay.gd")) radar.player = player diff --git a/tests/ships/instant_laboratory_test.gd b/tests/ships/instant_laboratory_test.gd new file mode 100644 index 0000000..6bd4a93 --- /dev/null +++ b/tests/ships/instant_laboratory_test.gd @@ -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() diff --git a/tests/ships/instant_laboratory_test.gd.uid b/tests/ships/instant_laboratory_test.gd.uid new file mode 100644 index 0000000..59e517f --- /dev/null +++ b/tests/ships/instant_laboratory_test.gd.uid @@ -0,0 +1 @@ +uid://dvumugcgys2bg diff --git a/tests/ships/instant_laboratory_test.tscn b/tests/ships/instant_laboratory_test.tscn new file mode 100644 index 0000000..91dfc82 --- /dev/null +++ b/tests/ships/instant_laboratory_test.tscn @@ -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") diff --git a/tests/ships/laboratory_test.gd b/tests/ships/laboratory_test.gd index 12718b0..3c3d198 100644 --- a/tests/ships/laboratory_test.gd +++ b/tests/ships/laboratory_test.gd @@ -42,7 +42,7 @@ func _ready() -> void: assert(definition.item_id in LAB.IDS) for id in LAB.IDS: 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" assert(not LAB.start(captain,offers[0]),"Cannot research without materials") for cost in LAB.COSTS.values(): @@ -65,16 +65,16 @@ func _ready() -> void: var before: Dictionary = captain.minerals.duplicate() assert(LAB.start(captain,"incendiary_missile")) 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 assert(preload("res://scripts/planets/alien_research.gd").purchase(progress,"general")) GameState.advance_play_time(570.0,game) - assert(progress.laboratory_remaining==30,"Research advances while docked without any mining stations") - assert(progress.alien_remaining==30,"Both research stations advance in parallel") - assert(not progress.laboratory_unlocks.has("incendiary_missile")) + assert(progress.laboratory_remaining==0,"Module discovery has no timer") + assert(progress.alien_remaining==30,"Alien research still advances while docked") + assert(progress.laboratory_unlocks.has("incendiary_missile")) assert(ResourceSaver.save(progress,"user://laboratory_progress_test.tres")==OK) 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") progress.freighter_rooms.expansion_2 = "alien_research" manager.screen._select_section("expansion_2") @@ -82,11 +82,11 @@ func _ready() -> void: manager._leave() get_tree().paused = true 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") get_tree().paused = false 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) assert(progress.laboratory_unlocks.has("incendiary_missile") and progress.laboratory_offers.size()==3) assert(progress.alien_skills.general) diff --git a/tests/ships/research_progression_test.gd b/tests/ships/research_progression_test.gd index 778d20b..2b0aa09 100644 --- a/tests/ships/research_progression_test.gd +++ b/tests/ships/research_progression_test.gd @@ -20,10 +20,9 @@ func _ready() -> void: captain.level = 1 progress.laboratory_offers.assign(["virus"]) assert(LAB.start(captain,"virus")) - assert(progress.laboratory_remaining == 600) - LAB.advance(progress,599) - assert(not progress.laboratory_unlocks.get("virus",false)) - LAB.advance(progress,1) + assert(progress.laboratory_remaining == 0) + assert(progress.laboratory_project.is_empty()) + assert(not LAB.start(captain,"virus")) assert(progress.laboratory_unlocks.virus) for id in LAB.IDS: 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)) ALIEN.advance(restored,0.1) 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() diff --git a/tests/space_progression_test.gd b/tests/space_progression_test.gd new file mode 100644 index 0000000..f760756 --- /dev/null +++ b/tests/space_progression_test.gd @@ -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) diff --git a/tests/space_progression_test.gd.uid b/tests/space_progression_test.gd.uid new file mode 100644 index 0000000..513ee66 --- /dev/null +++ b/tests/space_progression_test.gd.uid @@ -0,0 +1 @@ +uid://cbbffs63gc38t diff --git a/tests/space_progression_test.tscn b/tests/space_progression_test.tscn new file mode 100644 index 0000000..f366008 --- /dev/null +++ b/tests/space_progression_test.tscn @@ -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")