This commit is contained in:
Tomasz Boruc 2026-10-31 16:08:17 +01:00
parent f3eb5fad65
commit 14082e3436
76 changed files with 1194 additions and 305 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 KiB

View File

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dpvckkd1oc052" uid="uid://cinvyp3qbv5yf"
path="res://.godot/imported/laboratory_projects.png-61c88f4e4c24b4b7ef45dab06ea98448.ctex" path="res://.godot/imported/chronal_workshop_ui.png-90ce303bc4785fc6c962cf551a161261.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://artifacts/laboratory_projects.png" source_file="res://artifacts/chronal_workshop_ui.png"
dest_files=["res://.godot/imported/laboratory_projects.png-61c88f4e4c24b4b7ef45dab06ea98448.ctex"] dest_files=["res://.godot/imported/chronal_workshop_ui.png-90ce303bc4785fc6c962cf551a161261.ctex"]
[params] [params]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cqslrjy0gsh0j"
path="res://.godot/imported/command_layout_1280.png-8529be7b5969de508917a247b1bf54ce.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://artifacts/command_layout_1280.png"
dest_files=["res://.godot/imported/command_layout_1280.png-8529be7b5969de508917a247b1bf54ce.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 KiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://soe07738fyja"
path="res://.godot/imported/command_layout_960.png-238a097ab4f8bbe0ed0427078a03ea19.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://artifacts/command_layout_960.png"
dest_files=["res://.godot/imported/command_layout_960.png-238a097ab4f8bbe0ed0427078a03ea19.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b4jkvu0skmpsk"
path="res://.godot/imported/enemy_explosion_preview.png-db0a252a8b01c14daaad125111c2cff8.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://artifacts/enemy_explosion_preview.png"
dest_files=["res://.godot/imported/enemy_explosion_preview.png-db0a252a8b01c14daaad125111c2cff8.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@ -0,0 +1,16 @@
# Eksplozje wrogów — 2026-10-31
Nowy `enemy_destruction_effect.gd` zastępuje mały efekt trafienia przy zwykłym zniszczeniu wroga. Zachowano osobny efekt egzekucji / rozpadu kadłuba.
- Początkowy błysk i dwa rozszerzające się pierścienie: błękitna fala jonowa oraz bursztynowy front.
- Turbulentna kula ognia korzysta z istniejącego shadera ognia statku; 2–4 opóźnione wybuchy wtórne.
- 18 fragmentów faktycznej tekstury kadłuba; boss: 32. Fragmenty wirują, rozlatują się i stygną.
- 32–60 smug iskier i miękki, proceduralny dym.
- Skala zależna od rozmiaru statku, dłuższa animacja bossa i niższy dźwięk wybuchu. Zachowano część pędu zniszczonego statku.
- Efekt znika po 2,1–2,6 s. Limit 18 jednoczesnych eksplozji i 8 dźwięków. Bez fizycznych odłamków, nowych assetów bitmapowych ani zmian obrażeń i nagród.
Weryfikacja:
- Godot 4.7.2, `tests/items/enemy_destruction_test.tscn`: PASS — trzy klasy wrogów, odłamki, pęd, czas życia, rzeczywiste zniszczenie wroga, ochrona przed powtórnym efektem i limit aktywnych eksplozji.
- Wyrenderowano i obejrzano `artifacts/enemy_explosion_preview.png` na OpenGL 3.3 / Compatibility. Brak błędów shadera.
- Podgląd do ponownego uruchomienia: `tests/previews/enemy_explosion_preview.tscn`.

View File

@ -0,0 +1,26 @@
# Kosmos zamiast lądowania — 2026-10-31
- Lądowanie wyłączone przez `PLANET_LANDING_ENABLED`; kod powierzchni, lądownika i dane zapisów zachowane.
- Deuter nie jest produkowany, pokazywany ani wymagany do podróży. Dawne pola zapisu zachowano dla zgodności.
- Każdy boss zwykłego układu zostawia jedną mapę. Dotychczasowy drop map i orbów na dalekich wyprawach zachowany.
- Trzy bazy zaopatrzeniowe na daleką wyprawę. Ostrzał niszczy bazę; podlecenie zbiera ładunek wszystkich pięciu dawnych surowców planetarnych. Ilość każdego: losowo 18–28 × zaokrąglony w górę pierwiastek poziomu sektora. Zniszczona baza nie odradza się w tej samej eksploracji.
- Każdy statek wroga ma niezależne 15% szansy na fizyczny drop jednej próbki, w zwykłym układzie i na wyprawie.
- Orb: kliknięcie orba, potem wyposażenia; PPM/Esc anuluje. Usunięto zbędny interfejs warsztatu orbów.
- Inżynier chronalny przeniesiony do warsztatu fregaty zgodnie z decyzją użytkownika; koszt i zasady maksymalizacji addonów zachowane.
- Usunięto naprawę lądownika, sieć wydobywczą i przydziały specjalistów z aktywnego interfejsu. Kajuty nie są już budowane.
- Zaktualizowano instrukcję gracza i opisy sprzętu planetarnego.
## Weryfikacja
Godot 4.7.2, headless, izolowany APPDATA wewnątrz katalogu tmp projektu:
- `space_progression_test.tscn`: failures=[] — podróż bez paliwa, zużycie mapy tylko raz, trzy bazy, jednokrotny drop i zbiór surowców, próbki, dokowanie, brak naprawy lądownika, inżynier bez aktywnej wyprawy powierzchniowej, orb na wyposażeniu, mapa ze zwykłego bossa.
- `distant_integration.tscn`: failures=[], passed=true — lot, animacja skoku, drop, orby i powrót.
- `distant_defeat_return.tscn`: PASS — powrót po porażce, wyposażenie, mapy, dokowanie i blokada podwójnego ponowienia.
- `git diff --check`: bez błędów.
## Kwestie do dalszej decyzji
Wyposażenie kapitana do eksploracji powierzchni i bonusy związane z planetami zachowano, ale nie mają obecnie zastosowania. Ich nowe przeznaczenie wymaga osobnej decyzji projektowej. Produkcja wyposażenia kapitana pozostaje dostępna z informacją o wstrzymanym zastosowaniu.
Bazy korzystają obecnie z istniejącej grafiki przekaźnika w kolorze wrogiej instalacji; nie mają osobnego uzbrojenia. Bronią ich przeciwnicy obecni w sektorze. Liczba baz, uzysk i jedna próbka na trafiony rzut są przyjętymi wartościami startowymi do balansu.

View File

@ -0,0 +1,17 @@
# Warsztat i natychmiastowe odkrycia — 2026-10-31
Warsztat: zamiast portretu inżyniera i rozwijanych list jest panel z ikonami wyposażenia, wyborem addonu przyciskiem, paskiem wartości, podglądem obecna → maksymalna oraz przyciskiem maksymalizacji. Saldo i koszt mają ikonę kryształów czasu. Dłuższe wyjaśnienia przeniesiono do podpowiedzi. Gotowe addony mają oznaczenie MAX. Wybór nie przebudowuje całego warsztatu, dzięki czemu nie resetuje przewijania.
Podgląd OpenGL Compatibility: `artifacts/chronal_workshop_ui.png`. Sprawdzono wizualnie oraz kliknięciami w rzeczywistym panelu: wybór przedmiotu i addonu, poprawny koszt, maksymalizację, blokadę gotowego maksimum oraz stabilność przewijania.
Laboratorium: projekty odblokowują się natychmiast po zapłaceniu dotychczasowego kosztu surowców i od razu trafiają do puli łupów. Interfejs nie pokazuje czasu ani paska postępu. Zachowano timery badań obcych, bo zmiana dotyczy wynajdywania modułów.
Usunięto Naukowca z katalogu, opisów i ofert umiejętności. Rangi ze starych zapisów zwracają równą liczbę punktów nauki jednorazowo. Nieaktualne oferty są filtrowane. Opłacone badania modułów ze starych zapisów kończą się od razu podczas wczytania.
Godot 4.7.2:
- CHRONAL_UI_CHECK: PASS.
- INSTANT_LABORATORY_TEST: PASS — koszt, natychmiastowe odkrycie, ochrona przed ponownym zakupem, stary projekt, zwrot rang, UI bez timera.
- RESEARCH PROGRESSION: PASS — wszystkie 23 projekty i 28 badań obcych, pula łupu i zapis postępu.
- NEW_CAPTAIN_SKILLS_TEST: PASS — 15 umiejętności, ikony, trening, pozostałe bonusy.
- SAVE_SETTINGS_TEST: PASS, 0 failures — wczytanie kończy stare badanie modułu.

View File

@ -47,6 +47,10 @@ func get_energy_bank_fraction() -> float:
return maxf(0.10, get_skill_bonus("electrician")) return maxf(0.10, get_skill_bonus("electrician"))
func normalize_skill_ranks() -> void: func normalize_skill_ranks() -> void:
if skills.has("scientist"):
learning_points += clampi(int(skills.scientist),0,SKILLS.MAX_RANK)
skills.erase("scientist")
skill_offers.assign(skill_offers.filter(func(id): return SKILLS.DATA.has(id)))
for skill in skills.keys(): for skill in skills.keys():
if not SKILLS.DATA.has(skill): if not SKILLS.DATA.has(skill):
skills.erase(skill) skills.erase(skill)
@ -94,7 +98,7 @@ func reset_skills() -> bool:
return false return false
func can_train(skill: String) -> bool: func can_train(skill: String) -> bool:
return learning_points > 0 and skill in skill_offers and get_skill_rank(skill) < SKILLS.MAX_RANK return SKILLS.DATA.has(skill) and learning_points > 0 and skill in skill_offers and get_skill_rank(skill) < SKILLS.MAX_RANK
func train_skill(skill: String) -> bool: func train_skill(skill: String) -> bool:
if not can_train(skill): if not can_train(skill):
@ -112,6 +116,7 @@ func train_skill(skill: String) -> bool:
return true return true
func prepare_skill_offers() -> void: func prepare_skill_offers() -> void:
normalize_skill_ranks()
if learning_points <= 0 or not skill_offers.is_empty(): if learning_points <= 0 or not skill_offers.is_empty():
return return
var pool: Array[String] = [] var pool: Array[String] = []

View File

@ -4,11 +4,10 @@ const MAX_RANK := 3
static var _icon_cache: Dictionary = {} static var _icon_cache: Dictionary = {}
const COOLDOWN := 30.0 const COOLDOWN := 30.0
const DURATION := 5.0 const DURATION := 5.0
const IDS := ["business_shark", "reactor_overload", "tactical_maneuver", "efficient_magazine", "miner", "power_redirect", "lucky", "explorer", "electrician", "scientist", "gunsmith", "bounty_hunter", "missile_specialist", "emergency_procedure", "scrap_collector", "drone_commander"] const IDS := ["business_shark", "reactor_overload", "tactical_maneuver", "efficient_magazine", "miner", "power_redirect", "lucky", "explorer", "electrician", "gunsmith", "bounty_hunter", "missile_specialist", "emergency_procedure", "scrap_collector", "drone_commander"]
const DATA := { const DATA := {
"scrap_collector": {"name": "Zbieracz złomu", "active": false, "values": [0.15, 0.25, 0.40], "color": "d9b27c"}, "scrap_collector": {"name": "Zbieracz złomu", "active": false, "values": [0.15, 0.25, 0.40], "color": "d9b27c"},
"drone_commander": {"name": "Dowódca dronów", "active": false, "values": [0.10, 0.20, 0.30], "color": "70dfff"}, "drone_commander": {"name": "Dowódca dronów", "active": false, "values": [0.10, 0.20, 0.30], "color": "70dfff"},
"scientist": {"name": "Naukowiec", "active": false, "values": [0.10, 0.20, 0.30], "color": "76d9ed"},
"gunsmith": {"name": "Rusznikarz", "active": false, "values": [0.10, 0.20, 0.30], "color": "e8b26e"}, "gunsmith": {"name": "Rusznikarz", "active": false, "values": [0.10, 0.20, 0.30], "color": "e8b26e"},
"bounty_hunter": {"name": "Łowca nagród", "active": false, "values": [0.10, 0.20, 0.30], "color": "efcc70"}, "bounty_hunter": {"name": "Łowca nagród", "active": false, "values": [0.10, 0.20, 0.30], "color": "efcc70"},
"missile_specialist": {"name": "Specjalista rakietowy", "active": false, "values": [0.10, 0.15, 0.20], "color": "f29576"}, "missile_specialist": {"name": "Specjalista rakietowy", "active": false, "values": [0.10, 0.15, 0.20], "color": "f29576"},
@ -66,8 +65,6 @@ static func description(skill: String, rank: int) -> String:
return "%d%% szansy na 1 dodatkową część z przeszukanego wraku. Losowanie raz na wrak. Premie rang nie sumują się." % amount return "%d%% szansy na 1 dodatkową część z przeszukanego wraku. Losowanie raz na wrak. Premie rang nie sumują się." % amount
"drone_commander": "drone_commander":
return "Obrażenia dronów bojowych większe o %d%%. Premie rang nie sumują się." % amount return "Obrażenia dronów bojowych większe o %d%%. Premie rang nie sumują się." % amount
"scientist":
return "Nowe badania w laboratorium trwają o %d%% krócej. Premie rang nie sumują się." % amount
"gunsmith": "gunsmith":
return "Produkcja zwykłej i radiacyjnej amunicji daje o %d%% więcej sztuk za ten sam koszt. Nie dotyczy rakiet. Premie rang nie sumują się." % amount return "Produkcja zwykłej i radiacyjnej amunicji daje o %d%% więcej sztuk za ten sam koszt. Nie dotyczy rakiet. Premie rang nie sumują się." % amount
"bounty_hunter": "bounty_hunter":

View File

@ -1,7 +1,6 @@
extends RefCounted extends RefCounted
const MATERIALS := 10000 const MATERIALS := 10000
const CREDITS := 1000000 const CREDITS := 1000000
const DEUTER := 1000000
const CRYSTALS := 10000 const CRYSTALS := 10000
const SAMPLES := 1000 const SAMPLES := 1000
const ORBS := 25 const ORBS := 25
@ -10,7 +9,7 @@ const AMMUNITION := 10000
const RADIATION_AMMUNITION := 1000 const RADIATION_AMMUNITION := 1000
const MISSILES := 1000 const MISSILES := 1000
const POWERBANKS := 100 const POWERBANKS := 100
const TOOLTIP := "+1 000 000 kredytów i deuteru\n+10 000 każdego minerału, części i kryształów czasu\n+1000 próbek, +25 każdego orba, +100 punktów nauki\n+10 000 amunicji, +1000 radiacyjnej i rakiet, +100 powerbanków\nMapy: poziomy 2, obecny +1/+5, 120, 125 i 130 (bez duplikatów)\nPo 1 każdego rodzaju wyposażenia do magazynu fregaty oraz sprzęt kapitana." const TOOLTIP := "+1 000 000 kredytów\n+10 000 każdego minerału, części i kryształów czasu\n+1000 próbek, +25 każdego orba, +100 punktów nauki\n+10 000 amunicji, +1000 radiacyjnej i rakiet, +100 powerbanków\nMapy: poziomy 2, obecny +1/+5, 120, 125 i 130 (bez duplikatów)\nPo 1 każdego rodzaju wyposażenia do magazynu fregaty oraz sprzęt kapitana."
static func grant(captain: Resource, inventory: Node, level: int) -> Dictionary: static func grant(captain: Resource, inventory: Node, level: int) -> Dictionary:
var progress: Resource = captain.expedition var progress: Resource = captain.expedition
@ -21,7 +20,6 @@ static func grant(captain: Resource, inventory: Node, level: int) -> Dictionary:
captain.credits += CREDITS captain.credits += CREDITS
captain.learning_points += LEARNING_POINTS captain.learning_points += LEARNING_POINTS
captain.prepare_skill_offers() captain.prepare_skill_offers()
progress.deuter += DEUTER
progress.time_crystals += CRYSTALS progress.time_crystals += CRYSTALS
progress.analysis_samples += SAMPLES progress.analysis_samples += SAMPLES
for orb in preload("res://scripts/items/special_orbs.gd").IDS: for orb in preload("res://scripts/items/special_orbs.gd").IDS:

View File

@ -16,6 +16,7 @@ func _ready() -> void:
if int(data.tier) == 2: game._pending_boss = data.duplicate() if int(data.tier) == 2: game._pending_boss = data.duplicate()
else: game._spawn_enemy(data,data.position) else: game._spawn_enemy(data,data.position)
_build_structures() _build_structures()
_build_resource_bases()
var expeditions := Node.new() var expeditions := Node.new()
expeditions.set_script(preload("res://scripts/planets/expedition_controller.gd")) expeditions.set_script(preload("res://scripts/planets/expedition_controller.gd"))
expeditions.game = game expeditions.game = game
@ -36,6 +37,18 @@ func _ready() -> void:
tween.tween_property(fade,"color:a",0.0,0.55) tween.tween_property(fade,"color:a",0.0,0.55)
tween.tween_callback(arrival.queue_free) tween.tween_callback(arrival.queue_free)
func _build_resource_bases() -> void:
for index in 3:
var id := "%d:enemy_base:%d" % [GameState.system_seed,index]
if GameState.explored_pois.has(id): continue
var base := StaticBody2D.new()
base.set_script(preload("res://scripts/levels/enemy_resource_base.gd"))
base.base_id = id
base.reward_seed = GameState.system_seed ^ (index * 7919)
base.level = GameState.system_level
base.position = Vector2.RIGHT.rotated(TAU * index / 3.0) * 1200
game.add_child(base)
func _build_structures() -> void: func _build_structures() -> void:
var group := Node2D.new() var group := Node2D.new()
group.name = "AncientStructures" group.name = "AncientStructures"

View File

@ -0,0 +1,197 @@
extends Node2D
## Short, layered death effect. All particles are local visuals, without physics bodies.
const FIRE_SHADER := preload("res://resources/effects/ship_fireball.gdshader")
const EXPLOSION_AUDIO := preload("res://assets/sounds/effects/explosion.mp3")
const MAX_EFFECTS := 18
static var _smoke_texture: Texture2D
var _elapsed := 0.0
var _duration := 2.1
var _size := 56.0
var _tier := 0
var _texture: Texture2D
var _tint := Color.WHITE
var _ship_scale := Vector2.ONE
var _angle := 0.0
var _drift := Vector2.ZERO
var _fragments: Array[Dictionary] = []
var _sparks: Array[Dictionary] = []
var _smoke: Array[Dictionary] = []
var _bursts: Array[Dictionary] = []
var _rng := RandomNumberGenerator.new()
var play_audio := true
func configure(sprite: Sprite2D, drift: Vector2, tier: int) -> void:
top_level = true
_texture = sprite.texture
_ship_scale = sprite.global_scale
_angle = sprite.global_rotation
_tint = sprite.modulate
global_position = sprite.global_position
_drift = drift * 0.65
_tier = clampi(tier,0,2)
_size = maxf(_texture.get_width()*_ship_scale.x,_texture.get_height()*_ship_scale.y) if _texture != null else 56.0
_duration = 2.1 + _tier * 0.25
func _ready() -> void:
z_index = 35
light_mask = 4
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_OFF
add_to_group("enemy_explosions")
var active := get_tree().get_nodes_in_group("enemy_explosions")
if active.size() > MAX_EFFECTS:
for old in active:
if old != self and not old.is_queued_for_deletion():
old.queue_free()
break
_rng.randomize()
_prepare_smoke()
_build_fragments()
_build_particles()
_add_fireball(Vector2.ZERO,_size*3.8,0.0)
for index in 2 + _tier:
var direction := Vector2.from_angle(_rng.randf()*TAU)
_add_fireball(direction*_size*_rng.randf_range(0.2,0.5),_size*_rng.randf_range(1.5,2.3),0.07+index*0.07)
if play_audio: _play_sound()
_update_fireballs()
func _add_fireball(origin: Vector2, diameter: float, delay: float) -> void:
var mesh := MeshInstance2D.new()
var quad := QuadMesh.new()
quad.size = Vector2.ONE * diameter
mesh.mesh = quad
mesh.position = origin
mesh.rotation = _rng.randf()*TAU
mesh.light_mask = 4
mesh.z_index = 1
var fire := ShaderMaterial.new()
fire.shader = FIRE_SHADER
fire.set_shader_parameter("variation",_rng.randf_range(0,100))
mesh.material = fire
add_child(mesh)
_bursts.append({"visual":mesh,"material":fire,"delay":delay})
func _build_fragments() -> void:
if _texture == null: return
var dimensions := _texture.get_size()
# Textured triangles preserve the actual hull, rather than generic square debris.
var grid := 3 if _tier < 2 else 4
for y in grid:
for x in grid:
var a := Vector2(x,y)*dimensions/grid
var b := Vector2(x+1,y)*dimensions/grid
var c := Vector2(x+1,y+1)*dimensions/grid
var d := Vector2(x,y+1)*dimensions/grid
for triangle in [[a,b,c],[a,c,d]]:
var center: Vector2 = (triangle[0]+triangle[1]+triangle[2])/3.0
var origin := ((center-dimensions*0.5)*_ship_scale).rotated(_angle)
var points := PackedVector2Array()
var uv := PackedVector2Array()
for vertex in triangle:
points.append((vertex-center)*_ship_scale)
uv.append(vertex/dimensions)
var direction := origin.normalized().rotated(_rng.randf_range(-0.4,0.4))
_fragments.append({"origin":origin,"velocity":direction*_rng.randf_range(80,175)*(1.0+_tier*0.2),"points":points,"uv":uv,"spin":_rng.randf_range(-5,5)})
func _build_particles() -> void:
for index in 32 + _tier * 14:
var direction := Vector2.from_angle(_rng.randf()*TAU)
_sparks.append({"velocity":direction*_rng.randf_range(100,370)*(1.0+0.18*_tier),"life":_rng.randf_range(0.4,1.35),"width":_rng.randf_range(0.8,2.1),"warm":_rng.randf()})
for index in 10 + _tier * 3:
var direction := Vector2.from_angle(_rng.randf()*TAU)
_smoke.append({"origin":direction*_size*_rng.randf_range(0.1,0.35),"velocity":direction*_rng.randf_range(16,48),"radius":_size*_rng.randf_range(0.22,0.4),"angle":_rng.randf()*TAU,"delay":_rng.randf_range(0.08,0.25)})
static func _prepare_smoke() -> void:
if _smoke_texture != null: return
var noise := FastNoiseLite.new()
noise.seed = 9172
noise.frequency = 0.075
var image := Image.create(64,64,false,Image.FORMAT_RGBA8)
for y in 64:
for x in 64:
var point := (Vector2(x,y)-Vector2(31.5,31.5))/31.5
var edge := 1.0-smoothstep(0.2,1.0,point.length())
var density := clampf(0.65+noise.get_noise_2d(x,y)*0.65,0,1)
image.set_pixel(x,y,Color(1,1,1,edge*density))
_smoke_texture = ImageTexture.create_from_image(image)
func _play_sound() -> void:
if get_tree().get_nodes_in_group("enemy_explosion_audio").size() >= 8: return
# Separate audio lifetime lets the tail finish after the visual is freed.
var audio := AudioStreamPlayer2D.new()
audio.stream = EXPLOSION_AUDIO
audio.volume_db = -15.0 + _tier*2.5
audio.pitch_scale = _rng.randf_range(0.95,1.12) - _tier*0.13
audio.max_distance = 1500
audio.max_polyphony = 1
audio.add_to_group("enemy_explosion_audio")
get_parent().add_child(audio)
audio.global_position = global_position
audio.finished.connect(audio.queue_free)
audio.play()
func _process(delta: float) -> void:
_elapsed += delta
global_position += _drift * delta
_drift *= exp(-1.4*delta)
if _elapsed >= _duration:
queue_free()
return
_update_fireballs()
queue_redraw()
func _update_fireballs() -> void:
for burst in _bursts:
var age: float = _elapsed - float(burst.delay)
var visual: MeshInstance2D = burst.visual
visual.visible = age >= 0 and age < 1.3
if visual.visible:
var fire: ShaderMaterial = burst.material
fire.set_shader_parameter("phase",age)
fire.set_shader_parameter("strength",1.25 if _tier == 2 else 1.0)
func _draw() -> void:
var time := _elapsed
# A soft, irregular smoke cloud stays behind as the fire cools.
for puff in _smoke:
var age := maxf(0,time-float(puff.delay))
var alpha := smoothstep(0.0,0.3,age)*(1.0-smoothstep(0.6,_duration,age))*0.45
var center: Vector2 = puff.origin + puff.velocity * (1.0-exp(-age*1.5))/1.5
var radius: float = puff.radius*(0.6+age*0.85)
draw_set_transform(center,float(puff.angle)+age*0.15)
draw_texture_rect(_smoke_texture,Rect2(-Vector2.ONE*radius,Vector2.ONE*radius*2),false,Color(0.12,0.14,0.18,alpha))
draw_set_transform(Vector2.ZERO)
# Thin blue ion shock followed by a wider amber pressure front.
for ring in 2:
var age := time-ring*0.055
if age < 0 or age > 0.65: continue
var progress := age/0.65
var radius := _size*(0.16+1.75*(1.0-pow(1.0-progress,3)))
var fade := pow(1.0-progress,2)
var color := Color("87dfff") if ring == 0 else Color("ff9d49")
draw_arc(Vector2.ZERO,radius,0,TAU,80,Color(color,fade*0.1),9.0*(1.0-progress)+1,true)
draw_arc(Vector2.ZERO,radius,0,TAU,80,Color(color,fade*0.75),1.2+fade,true)
var debris_fade := 1.0-smoothstep(0.7,_duration,time)
var travel := (1.0-exp(-time*1.4))/1.4
for piece in _fragments:
var center: Vector2 = piece.origin + piece.velocity*travel
draw_set_transform(center,_angle+float(piece.spin)*time)
var hot := exp(-time*4.0)
var color := _tint.lerp(Color("ffd28b"),hot).darkened((1.0-hot)*0.55)
color.a = debris_fade
draw_polygon(piece.points,PackedColorArray([color]),piece.uv,_texture)
draw_set_transform(Vector2.ZERO)
for spark in _sparks:
if time > float(spark.life): continue
var fade := pow(1.0-time/float(spark.life),1.5)
var velocity: Vector2 = spark.velocity
var point := velocity*travel
var tail := point-velocity.normalized()*(3.0+fade*16.0)
var color := Color("ffc35c").lerp(Color("fff2c4"),float(spark.warm))
draw_line(tail,point,Color(color,fade*0.14),float(spark.width)*4,true)
draw_line(tail,point,Color(color,fade),float(spark.width),true)
var flash := exp(-time*30.0)
if flash > 0.01:
for layer in range(5,0,-1):
draw_circle(Vector2.ZERO,_size*0.45*layer/5.0,Color(1,0.83,0.55,flash*0.12))
draw_line(Vector2(-_size*1.2,0),Vector2(_size*1.2,0),Color(1,0.92,0.75,flash*0.75),1.6,true)

View File

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

View File

@ -518,6 +518,7 @@ func _reward_captain() -> void:
get_parent().call_deferred("add_child", credits) get_parent().call_deferred("add_child", credits)
func _drop_loot() -> void: func _drop_loot() -> void:
preload("res://scripts/game/space_rewards.gd").drop(self)
# Roll independently of equipment drops. # Roll independently of equipment drops.
var distant := not GameState.active_distant_map.is_empty() var distant := not GameState.active_distant_map.is_empty()
if distant: preload("res://scripts/game/distant_loot.gd").drop(self) if distant: preload("res://scripts/game/distant_loot.gd").drop(self)
@ -547,10 +548,10 @@ func _spawn_destruction_effect() -> void:
collapse.position = position collapse.position = position
get_parent().add_child(collapse) get_parent().add_child(collapse)
return return
var effect := IMPACT_EFFECT_SCENE.instantiate() as Node2D var effect := Node2D.new()
effect.global_position = global_position effect.set_script(preload("res://scripts/game/enemy_destruction_effect.gd"))
effect.call("configure",_sprite,velocity,loot_tier)
get_parent().add_child(effect) get_parent().add_child(effect)
effect.call("configure", true, Color(1.0, 0.36, 0.18))
func _show_damage_popup(amount: float, critical: bool = false) -> void: func _show_damage_popup(amount: float, critical: bool = false) -> void:
if critical: if critical:

View File

@ -262,7 +262,6 @@ func _load_next_level() -> void:
func travel_to_distant(map_id: String) -> bool: func travel_to_distant(map_id: String) -> bool:
if _transitioning or bool(player.get("_is_destroyed")) or not GameState.active_distant_map.is_empty(): return false if _transitioning or bool(player.get("_is_destroyed")) or not GameState.active_distant_map.is_empty(): return false
if not GameState.captain.expedition.distant_maps.has(map_id): return false if not GameState.captain.expedition.distant_maps.has(map_id): return false
if GameState.captain.expedition.deuter < preload("res://scripts/planets/distant_expeditions.gd").fuel_cost(GameState.captain.expedition.distant_maps[map_id]): return false
_transitioning = true _transitioning = true
_load_distant.call_deferred(map_id) _load_distant.call_deferred(map_id)
return true return true
@ -273,7 +272,6 @@ func _load_distant(map_id: String) -> void:
GameState.restart_inventory.clear() GameState.restart_inventory.clear()
_transitioning = false _transitioning = false
return return
var cost: int = preload("res://scripts/planets/distant_expeditions.gd").fuel_cost(GameState.active_distant_map)
var map_snapshot: Dictionary = GameState.active_distant_map.duplicate(true) var map_snapshot: Dictionary = GameState.active_distant_map.duplicate(true)
var previous_pause := get_tree().paused var previous_pause := get_tree().paused
var hud := get_node("HUD/HUDRoot") as CanvasItem var hud := get_node("HUD/HUDRoot") as CanvasItem
@ -309,7 +307,6 @@ func _load_distant(map_id: String) -> void:
get_tree().paused = previous_pause get_tree().paused = previous_pause
GameState.restore_distant_home() GameState.restore_distant_home()
GameState.captain.expedition.distant_maps[map_id] = map_snapshot GameState.captain.expedition.distant_maps[map_id] = map_snapshot
GameState.captain.expedition.deuter += cost
GameState.captain.expedition.emit_changed() GameState.captain.expedition.emit_changed()
GameState.restart_inventory.clear() GameState.restart_inventory.clear()
_transitioning = false _transitioning = false

View File

@ -45,13 +45,10 @@ func load_game(slot_id: String) -> bool:
func depart_distant(map_id: String) -> bool: func depart_distant(map_id: String) -> bool:
if not active_distant_map.is_empty() or not captain.expedition.distant_maps.has(map_id): return false if not active_distant_map.is_empty() or not captain.expedition.distant_maps.has(map_id): return false
var map: Dictionary = captain.expedition.distant_maps[map_id] var map: Dictionary = captain.expedition.distant_maps[map_id]
var cost: int = preload("res://scripts/planets/distant_expeditions.gd").fuel_cost(map)
if captain.expedition.deuter < cost: return false
distant_home = {"level":system_level,"seed":system_seed,"hull_bastion_used":hull_bastion_used} distant_home = {"level":system_level,"seed":system_seed,"hull_bastion_used":hull_bastion_used}
hull_bastion_used = false hull_bastion_used = false
active_distant_map = map.duplicate(true) active_distant_map = map.duplicate(true)
captain.expedition.distant_maps.erase(map_id) captain.expedition.distant_maps.erase(map_id)
captain.expedition.deuter -= cost
system_level = int(map.level) system_level = int(map.level)
system_seed = int(map.seed) system_seed = int(map.seed)
captain.expedition.emit_changed() captain.expedition.emit_changed()
@ -143,8 +140,7 @@ func advance_play_time(delta: float, scene: Node) -> void:
captain.emit_changed() captain.emit_changed()
var inventory := scene.get_node_or_null("PlayerShip/ShipInventory") var inventory := scene.get_node_or_null("PlayerShip/ShipInventory")
if inventory != null: inventory._recalculate_ship_stats() if inventory != null: inventory._recalculate_ship_stats()
var network := preload("res://scripts/planets/mining_network.gd") if not researching: return
if network.advance(captain.expedition, delta) <= 0 and not researching: return
_production_signal_time += delta _production_signal_time += delta
if _production_signal_time >= 1.0: if _production_signal_time >= 1.0:
_production_signal_time = fmod(_production_signal_time, 1.0) _production_signal_time = fmod(_production_signal_time, 1.0)

View File

@ -106,6 +106,8 @@ static func restore_state(state: Node, id: String = "") -> bool:
if data.state.has(key): state.set(key, data.state[key]) if data.state.has(key): state.set(key, data.state[key])
state.captain = data.captain state.captain = data.captain
state.captain.skill_offers.assign(data.skill_offers) state.captain.skill_offers.assign(data.skill_offers)
state.captain.normalize_skill_ranks()
preload("res://scripts/planets/laboratory.gd").advance(state.captain.expedition,0.0)
state.captain.reset_combat_state() state.captain.reset_combat_state()
state.restart_inventory = data.inventory state.restart_inventory = data.inventory
state.return_to_freighter = false state.return_to_freighter = false

View File

@ -561,19 +561,11 @@ func is_mini_boosting() -> bool:
func _get_allowed_speed() -> float: func _get_allowed_speed() -> float:
return minf(EQUIPMENT_BALANCE.MAX_BOOST_SPEED,maxf(max_speed + _boost_speed, max_speed * 2.0 if _mini_boost_active else max_speed + _mini_boost_coast_speed)) return minf(EQUIPMENT_BALANCE.MAX_BOOST_SPEED,maxf(max_speed + _boost_speed, max_speed * 2.0 if _mini_boost_active else max_speed + _mini_boost_coast_speed))
func has_free_mini_boost() -> bool:
var engine: ItemInstance = $ShipInventory.get_item("module",2)
return engine != null and engine.definition.category == ItemDefinition.Category.ENGINE and "booster" in engine.special_orbs
func _update_mini_boost(delta: float) -> void: func _update_mini_boost(delta: float) -> void:
_mini_boost_active = false _mini_boost_active = false
if not Input.is_action_pressed("ship_mini_boost"): if not Input.is_action_pressed("ship_mini_boost"):
_mini_boost_exhausted = false _mini_boost_exhausted = false
return return
if has_free_mini_boost() and not Input.is_action_pressed("ship_brake"):
_mini_boost_exhausted = false
_mini_boost_active = true
return
if _mini_boost_exhausted or Input.is_action_pressed("ship_brake"): if _mini_boost_exhausted or Input.is_action_pressed("ship_brake"):
return return
var cost := get_mini_boost_energy_cost() * delta var cost := get_mini_boost_energy_cost() * delta
@ -584,7 +576,6 @@ func _update_mini_boost(delta: float) -> void:
_mini_boost_active = true _mini_boost_active = true
func get_mini_boost_energy_cost() -> float: func get_mini_boost_energy_cost() -> float:
if has_free_mini_boost(): return 0.0
var engine: ItemInstance = $ShipInventory.get_item("module",2) var engine: ItemInstance = $ShipInventory.get_item("module",2)
var base_cost := maxf(MINI_BOOST_ENERGY_PER_SECOND,max_energy*0.22) var base_cost := maxf(MINI_BOOST_ENERGY_PER_SECOND,max_energy*0.22)
return base_cost * (0.5 if engine != null and "atomic_engine" in engine.special_orbs else 1.0) return base_cost * (0.5 if engine != null and "atomic_engine" in engine.special_orbs else 1.0)

View File

@ -0,0 +1,16 @@
extends RefCounted
const SAMPLE_CHANCE := 0.15
static func drop(enemy: Node2D) -> void:
if randf() < SAMPLE_CHANCE:
spawn(enemy,{"samples":1},Vector2(-38,30))
if int(enemy.get("loot_tier")) == 2 and GameState.active_distant_map.is_empty():
var profile := {"id":"boss:%d:%d" % [GameState.system_seed,randi()],"seed":randi(),"name":"Boss układu %d" % GameState.system_level}
spawn(enemy,{"map":preload("res://scripts/planets/distant_expeditions.gd").offer(profile,GameState.system_level)},Vector2(0,65))
static func spawn(enemy: Node2D, payload: Dictionary, offset: Vector2) -> void:
var pickup := Node2D.new()
pickup.set_script(preload("res://scripts/items/expedition_pickup.gd"))
pickup.payload = payload
pickup.position = enemy.position + offset
enemy.get_parent().call_deferred("add_child",pickup)

View File

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

View File

@ -14,7 +14,9 @@ func _ready() -> void:
_label.size.x = 240 _label.size.x = 240
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_label.add_theme_font_size_override("font_size",12) _label.add_theme_font_size_override("font_size",12)
_label.text = preload("res://scripts/items/special_orbs.gd").NAMES[str(payload.orb)] if payload.has("orb") else "Mapa odległych systemów · poz. %d" % int(payload.map.level) _label.text = preload("res://scripts/items/special_orbs.gd").NAMES[str(payload.orb)] if payload.has("orb") else "Próbka do analizy" if payload.has("samples") else "Ładunek surowców z bazy" if payload.has("materials") else "Mapa odległych systemów · poz. %d" % int(payload.map.level)
if payload.has("samples"): _icon = load("res://assets/icons/resources/uranium.png")
if payload.has("materials"): _icon = load("res://assets/icons/resources/copper.svg")
add_child(_label) add_child(_label)
preload("res://scripts/items/loot_reveal_effect.gd").spawn(get_parent(),position,Color("b991ff"),3) preload("res://scripts/items/loot_reveal_effect.gd").spawn(get_parent(),position,Color("b991ff"),3)
func _process(delta: float) -> void: func _process(delta: float) -> void:
@ -30,6 +32,11 @@ func _physics_process(delta: float) -> void:
var distance := global_position.distance_to(_player.global_position) var distance := global_position.distance_to(_player.global_position)
if distance <= 48: if distance <= 48:
var progress: Resource = GameState.captain.expedition var progress: Resource = GameState.captain.expedition
if payload.has("samples"): progress.analysis_samples += int(payload.samples)
if payload.has("materials"):
for key in payload.materials:
GameState.captain.minerals[key] = int(GameState.captain.minerals.get(key,0)) + int(payload.materials[key])
GameState.captain.emit_changed()
if payload.has("map"): progress.distant_maps[str(payload.map.id)] = payload.map.duplicate(true) if payload.has("map"): progress.distant_maps[str(payload.map.id)] = payload.map.duplicate(true)
if payload.has("orb"): if payload.has("orb"):
var id := str(payload.orb) var id := str(payload.orb)

View File

@ -246,7 +246,6 @@ func get_tooltip_text() -> String:
lines.append(modifier.get_display_text()) lines.append(modifier.get_display_text())
for orb in special_orbs: for orb in special_orbs:
if orb == "piercing": lines.append("\nORB PRZEBICIA · 50% obrażeń omija tarcze. Tylko ta broń.") if orb == "piercing": lines.append("\nORB PRZEBICIA · 50% obrażeń omija tarcze. Tylko ta broń.")
elif orb == "booster": lines.append("\nORB DOPALACZA · TAB bez kosztu energii. Działa po zamontowaniu silnika.")
elif orb in ["electricity", "radiation", "kinetic", "atomic_engine", "polarization"]: elif orb in ["electricity", "radiation", "kinetic", "atomic_engine", "polarization"]:
lines.append("\n%s · %s" % [preload("res://scripts/items/special_orbs.gd").NAMES[orb], preload("res://scripts/items/special_orbs.gd").DESCRIPTIONS[orb]]) lines.append("\n%s · %s" % [preload("res://scripts/items/special_orbs.gd").NAMES[orb], preload("res://scripts/items/special_orbs.gd").DESCRIPTIONS[orb]])
return "\n".join(lines) return "\n".join(lines)

View File

@ -218,6 +218,20 @@ func can_add_item(item: ItemInstance) -> bool:
func migrate_orbs() -> void: func migrate_orbs() -> void:
var progress: Resource = GameState.captain.expedition var progress: Resource = GameState.captain.expedition
progress.orbs.erase("booster")
# Retired orbs and their applied effect are removed from legacy inventories.
for slots in [cargo,weapon_slots,module_slots,extension_slots]:
for index in slots.size():
var item: ItemInstance = slots[index]
if item == null: continue
item.special_orbs.erase("booster")
if item.definition != null and item.definition.orb_id == "booster": slots[index] = null
for stock in [progress.freighter_storage,progress.delivered_modules]:
for index in range(stock.size()-1,-1,-1):
var item: ItemInstance = stock[index]
if item == null: continue
item.special_orbs.erase("booster")
if item.definition != null and item.definition.orb_id == "booster": stock.remove_at(index)
for id in progress.orbs.keys(): for id in progress.orbs.keys():
var amount := int(progress.orbs[id]) var amount := int(progress.orbs[id])
if amount > 0 and add_item(preload("res://scripts/items/special_orbs.gd").create_item(id,amount)): if amount > 0 and add_item(preload("res://scripts/items/special_orbs.gd").create_item(id,amount)):
@ -225,7 +239,7 @@ func migrate_orbs() -> void:
func select_orb(index: int) -> bool: func select_orb(index: int) -> bool:
var item := get_item("cargo",index) var item := get_item("cargo",index)
if item == null or item.definition.orb_id.is_empty(): return false if item == null or item.definition.orb_id not in preload("res://scripts/items/special_orbs.gd").IDS: return false
selected_orb = item selected_orb = item
Input.set_default_cursor_shape(Input.CURSOR_CROSS) Input.set_default_cursor_shape(Input.CURSOR_CROSS)
if get_node_or_null("OrbTargetCursor") == null: if get_node_or_null("OrbTargetCursor") == null:
@ -477,6 +491,8 @@ func _add_starter_items() -> void:
var item := ItemInstance.new() var item := ItemInstance.new()
item.definition = definition item.definition = definition
item.item_level = 1 item.item_level = 1
item.damage_min = 4.0
item.damage_max = 10.0
weapon_slots[0] = item weapon_slots[0] = item
func _remove_extension_addons() -> void: func _remove_extension_addons() -> void:

View File

@ -1,12 +1,13 @@
extends RefCounted extends RefCounted
# Every orb is consumed once. Further advancement requires another orb. # Every orb is consumed once. Further advancement requires another orb.
const IDS := ["piercing", "booster", "advancement", "replication", "electricity", "radiation", "kinetic", "atomic_engine", "polarization"] const IDS := ["piercing", "advancement", "replication", "electricity", "radiation", "kinetic", "atomic_engine", "polarization"]
const COMMON_IDS := ["piercing", "booster", "advancement", "electricity", "radiation", "kinetic", "atomic_engine", "polarization"] const COMMON_IDS := ["piercing", "advancement", "electricity", "radiation", "kinetic", "atomic_engine", "polarization"]
const DAMAGE_ORBS := ["electricity", "radiation", "kinetic"] const DAMAGE_ORBS := ["electricity", "radiation", "kinetic"]
const NAMES := {"piercing":"Orb przebicia", "booster":"Orb dopalacza", "advancement":"Orb awansu", "replication":"Orb replikacji", "electricity":"Orb elektryczności", "radiation":"Orb radiacji", "kinetic":"Orb kinetyczny", "atomic_engine":"Orb silników atomowych", "polarization":"Orb polaryzacyjny"} const NAMES := {"piercing":"Orb przebicia", "advancement":"Orb awansu", "replication":"Orb replikacji", "electricity":"Orb elektryczności", "radiation":"Orb radiacji", "kinetic":"Orb kinetyczny", "atomic_engine":"Orb silników atomowych", "polarization":"Orb polaryzacyjny"}
const DESCRIPTIONS := {"replication":"Tworzy dokładną, niezależną kopię wyposażenia w magazynie frachtowca. Oryginał zostaje. Kopiuje poziom, addony i efekty orbów. Drop: dodatkowe 5% z bossa dalekiej wyprawy.", "electricity":"Trwale zmienia typ obrażeń broni na energetyczny (jak laser). Nie zwiększa obrażeń ani kosztu strzału. Zastępuje poprzedni orb typu obrażeń.", "radiation":"Trwale zmienia typ obrażeń broni na radiacyjny. Nie zwiększa obrażeń ani kosztu strzału. Zastępuje poprzedni orb typu obrażeń.", "kinetic":"Trwale zmienia typ obrażeń broni na kinetyczny. Nie zwiększa obrażeń ani kosztu strzału. Zastępuje poprzedni orb typu obrażeń.", "atomic_engine":"Tylko silnik. Trwale zmniejsza zużycie energii dopalacza TAB o 50%. Nie sumuje się. Orb darmowego dopalacza ma pierwszeństwo.", "polarization":"Tylko tarcze. Trafienie w tarcze odnawia do 5% maksymalnych tarcz jako energię, nie więcej niż faktycznie stracone tarcze. Tempo odzysku: do 5% pojemności generatora/s, z rezerwą na jedno takie odnowienie. Uniki i trafienia w sam kadłub nie aktywują efektu.", "piercing":"50% obrażeń broni omija tarcze i trafia bezpośrednio w kadłub. Tylko broń.", "booster":"Dopalacz pod TAB nie zużywa energii i działa również przy pustym generatorze. Tylko silnik.", "advancement":"Awansuje przedmiot o 1 poziom i ponownie losuje jego parametry oraz wartości obecnych addonów. Zachowuje ich typy, liczbę i efekty orbów. Wyniki losowania mogą być niższe. Jedna sztuka daje jeden awans. Kolejny awans wymaga nowego orba (główna progresja do 120; 1000 to limit techniczny). Nie działa na materiały zużywalne."} const DESCRIPTIONS := {"replication":"Tworzy dokładną, niezależną kopię wyposażenia w magazynie frachtowca. Oryginał zostaje. Kopiuje poziom, addony i efekty orbów. Drop: dodatkowe 5% z bossa dalekiej wyprawy.", "electricity":"Trwale zmienia typ obrażeń broni na energetyczny (jak laser). Nie zwiększa obrażeń ani kosztu strzału. Zastępuje poprzedni orb typu obrażeń.", "radiation":"Trwale zmienia typ obrażeń broni na radiacyjny. Nie zwiększa obrażeń ani kosztu strzału. Zastępuje poprzedni orb typu obrażeń.", "kinetic":"Trwale zmienia typ obrażeń broni na kinetyczny. Nie zwiększa obrażeń ani kosztu strzału. Zastępuje poprzedni orb typu obrażeń.", "atomic_engine":"Tylko silnik. Trwale zmniejsza zużycie energii dopalacza TAB o 50%. Nie sumuje się.", "polarization":"Tylko tarcze. Trafienie w tarcze odnawia do 5% maksymalnych tarcz jako energię, nie więcej niż faktycznie stracone tarcze. Tempo odzysku: do 5% pojemności generatora/s, z rezerwą na jedno takie odnowienie. Uniki i trafienia w sam kadłub nie aktywują efektu.", "piercing":"50% obrażeń broni omija tarcze i trafia bezpośrednio w kadłub. Tylko broń.", "advancement":"Awansuje przedmiot o 1 poziom i ponownie losuje jego parametry oraz wartości obecnych addonów. Zachowuje ich typy, liczbę i efekty orbów. Wyniki losowania mogą być niższe. Jedna sztuka daje jeden awans. Kolejny awans wymaga nowego orba (główna progresja do 120; 1000 to limit techniczny). Nie działa na materiały zużywalne."}
static func icon(id: String) -> Texture2D: static func icon(id: String) -> Texture2D:
return load("res://assets/icons/orbs/%s.%s" % [id, "png" if id in ["piercing", "booster", "advancement"] else "svg"]) as Texture2D if id not in IDS: return null
return load("res://assets/icons/orbs/%s.%s" % [id, "png" if id in ["piercing", "advancement"] else "svg"]) as Texture2D
static func create_item(id: String, amount := 1) -> ItemInstance: static func create_item(id: String, amount := 1) -> ItemInstance:
if id not in IDS or amount <= 0: return null if id not in IDS or amount <= 0: return null
var definition := ItemDefinition.new() var definition := ItemDefinition.new()
@ -22,6 +23,7 @@ static func create_item(id: String, amount := 1) -> ItemInstance:
return item return item
static func count(progress: Resource, inventory: Node, id: String) -> int: static func count(progress: Resource, inventory: Node, id: String) -> int:
if id not in IDS: return 0
var total := int(progress.orbs.get(id,0)) var total := int(progress.orbs.get(id,0))
if inventory != null: if inventory != null:
for item in inventory.cargo.slice(0,inventory.unlocked_cargo_slots): for item in inventory.cargo.slice(0,inventory.unlocked_cargo_slots):

View File

@ -0,0 +1,53 @@
extends StaticBody2D
const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
var base_id := ""
var reward_seed := 1
var level := 1
var kind := "enemy_base"
var hull := 100.0
var _claimed := false
var _label: Label
static func materials(seed_value: int, sector_level: int) -> Dictionary:
var rng := RandomNumberGenerator.new()
rng.seed = seed_value
var result := {}
for key in PROFILES.MATERIALS:
result[key] = rng.randi_range(18,28) * maxi(1,ceili(sqrt(maxi(1,sector_level))))
return result
func _ready() -> void:
add_to_group("exploration_pois")
collision_layer = 4
collision_mask = 0
hull = 100.0 * preload("res://scripts/levels/level_balance.gd").item_stat_scale(level)
var shape := CollisionShape2D.new()
var circle := CircleShape2D.new()
circle.radius = 85
shape.shape = circle
add_child(shape)
var sprite := Sprite2D.new()
sprite.texture = preload("res://assets/expeditions/abandoned_relay.png")
sprite.scale = Vector2.ONE * 190.0 / maxf(sprite.texture.get_width(),sprite.texture.get_height())
sprite.modulate = Color("ff9b88")
add_child(sprite)
_label = Label.new()
_label.position = Vector2(-170,100)
_label.size.x = 340
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_label.add_theme_font_size_override("font_size",12)
_label.text = "BAZA ZAOPATRZENIOWA WROGA\nZniszcz, aby uwolnić surowce"
add_child(_label)
func take_weapon_hit(amount: float, _critical := false, _damage_type := 0) -> void:
if _claimed: return
hull -= maxf(0,amount)
if hull > 0: return
_claimed = true
GameState.explored_pois[base_id] = true
var pickup := Node2D.new()
pickup.set_script(preload("res://scripts/items/expedition_pickup.gd"))
pickup.payload = {"materials":materials(reward_seed,level)}
pickup.position = position
get_parent().call_deferred("add_child",pickup)
queue_free()

View File

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

View File

@ -27,7 +27,7 @@ static func map_cost(map: Dictionary) -> int:
return maxi(1, int(map.level)) * CREDITS_PER_LEVEL return maxi(1, int(map.level)) * CREDITS_PER_LEVEL
static func fuel_cost(map: Dictionary) -> int: static func fuel_cost(map: Dictionary) -> int:
return maxi(1, int(map.level)) * DEUTER_PER_LEVEL return 0 # Legacy API retained for the dormant surface mechanic.
static func buy(progress: Resource, captain: Resource, profile: Dictionary, source_level: int) -> bool: static func buy(progress: Resource, captain: Resource, profile: Dictionary, source_level: int) -> bool:
if not has_hermit(profile): return false if not has_hermit(profile): return false

View File

@ -18,33 +18,22 @@ static func build(screen: Control) -> void:
MENU.label(section, "Trwa wyprawa do sektora %d. Użyj przycisku POWRÓT Z WYPRAWY na pokładzie, aby wrócić do układu startowego przed kolejnym lotem." % GameState.system_level, 12, MENU.GOLD) MENU.label(section, "Trwa wyprawa do sektora %d. Użyj przycisku POWRÓT Z WYPRAWY na pokładzie, aby wrócić do układu startowego przed kolejnym lotem." % GameState.system_level, 12, MENU.GOLD)
return return
MENU.label(section, "Sektory poza znanymi szlakami. Podwójna liczba wrogów, jeden boss. Kryształy czasu z każdego wroga. Zwykli wrogowie: mapy i powerbanki po 15%. Boss zawsze zostawia 1 mapę i 1 losowy orb, a dodatkowo ma 5% szansy na Orb replikacji.", 12) MENU.label(section, "Sektory poza znanymi szlakami. Podwójna liczba wrogów, jeden boss. Kryształy czasu z każdego wroga. Zwykli wrogowie: mapy i powerbanki po 15%. Boss zawsze zostawia 1 mapę i 1 losowy orb, a dodatkowo ma 5% szansy na Orb replikacji.", 12)
var fuel := MENU.label(section, "", 13, Color("95dce8")) MENU.label(section, "Lot i powrót są bezpłatne. Mapa zużywa się przy wylocie. Bazy wroga zawierają krzem, miedź, nikiel, wolfram i rtęć. Próbki: 15% szansy z każdego statku wroga.", 12, MENU.GOLD)
fuel.name = "ExpeditionFuel"
screen._economy_labels["expedition_fuel"] = fuel
MENU.label(section, "Lot: poziom × 1000 deuteru. Powrót bezpłatny. Mapa zużywa się przy wylocie. Każdy sektor można odwiedzić tylko raz.", 12, MENU.GOLD)
if progress.distant_maps.is_empty(): if progress.distant_maps.is_empty():
MENU.label(section, "Brak sektorów do zbadania. Szukaj pustelnika na planecie lub map w łupach z wypraw.", 12, MENU.MUTED) MENU.label(section, "Brak sektorów do zbadania. Pokonaj bossa w kosmosie i zbierz jego mapę.", 12, MENU.MUTED)
var maps: Array = progress.distant_maps.values() var maps: Array = progress.distant_maps.values()
maps.sort_custom(func(a,b): return int(a.level)<int(b.level)) maps.sort_custom(func(a,b): return int(a.level)<int(b.level))
for map in maps: for map in maps:
var card := MENU.card(section) var card := MENU.card(section)
MENU.label(card, "%s / POZIOM %d" % [map.name,map.level], 14, MENU.GOLD) MENU.label(card, "%s / POZIOM %d" % [map.name,map.level], 14, MENU.GOLD)
MENU.label(card, "Źródło: %s · poziom %d" % [map.source_planet,map.source_level], 11, MENU.MUTED) MENU.label(card, "Źródło: %s · poziom %d" % [map.source_planet,map.source_level], 11, MENU.MUTED)
var cost: int = DATA.fuel_cost(map)
MENU.label(card, "Paliwo: %d deuteru" % cost, 12, Color("95dce8"))
var row := MENU.row(card) var row := MENU.row(card)
var status := MENU.label(row,"",11) var status := MENU.label(row,"Gotowa do lotu · bez kosztu paliwa",11)
status.size_flags_horizontal = Control.SIZE_EXPAND_FILL status.size_flags_horizontal = Control.SIZE_EXPAND_FILL
status.size_flags_vertical = Control.SIZE_SHRINK_CENTER
var map_id := str(map.id) var map_id := str(map.id)
var fly := MENU.button(row,"LEĆ",func(): screen._travel_to_distant(map_id),true) var fly := MENU.button(row,"LEĆ",func(): screen._travel_to_distant(map_id),true)
fly.name = "FlyDistant_"+map_id.md5_text().substr(0,8) fly.name = "FlyDistant_"+map_id.md5_text().substr(0,8)
fly.tooltip_text = "Jednorazowa wyprawa. Zużywa mapę i pobiera %d deuteru." % cost fly.custom_minimum_size.x = 80
var refresh_status := func(): fly.size_flags_vertical = Control.SIZE_SHRINK_CENTER
var missing := maxf(0.0,cost-progress.deuter) fly.tooltip_text = "Jednorazowa wyprawa. Zużywa mapę."
status.text = "Paliwo gotowe" if missing <= 0 else "Brakuje %.1f deuteru" % missing
fly.disabled = missing > 0
refresh_status.call()
progress.changed.connect(refresh_status)
status.tree_exiting.connect(func():
if progress.changed.is_connected(refresh_status): progress.changed.disconnect(refresh_status)
, CONNECT_ONE_SHOT)

View File

@ -2,6 +2,7 @@ extends Node
const UI := preload("res://scripts/planets/expedition_ui.gd") const UI := preload("res://scripts/planets/expedition_ui.gd")
const CONTACT := preload("res://scripts/ui/orbital_contact_style.gd") const CONTACT := preload("res://scripts/ui/orbital_contact_style.gd")
const PROFILES := preload("res://scripts/planets/planet_profiles.gd") const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
const PLANET_LANDING_ENABLED := false
const FREIGHTER_SIZE := 285.0 const FREIGHTER_SIZE := 285.0
const CONTACT_RANGE := preload("res://scripts/planets/orbital_contact_range.gd") const CONTACT_RANGE := preload("res://scripts/planets/orbital_contact_range.gd")
var game: Node2D var game: Node2D
@ -94,7 +95,7 @@ func _refresh_prompt() -> void:
_near_planet = null _near_planet = null
var best := INF var best := INF
for planet in get_tree().get_nodes_in_group("scanner_planets"): for planet in get_tree().get_nodes_in_group("scanner_planets"):
if not bool(planet.get("earthlike")): if not PLANET_LANDING_ENABLED or not bool(planet.get("earthlike")):
continue continue
var distance: float = player.global_position.distance_to(planet.global_position) - float(planet.get("radius")) var distance: float = player.global_position.distance_to(planet.global_position) - float(planet.get("radius"))
if CONTACT_RANGE.at_planet(player,planet) and distance < best: if CONTACT_RANGE.at_planet(player,planet) and distance < best:
@ -127,7 +128,7 @@ func _refresh_prompt() -> void:
elif nearby_freighter: elif nearby_freighter:
_contact_label.text = "● FREGATA / POŁĄCZENIE GOTOWE" _contact_label.text = "● FREGATA / POŁĄCZENIE GOTOWE"
text = "FRACHTOWIEC\nHangar · Warsztat · Magazyn" text = "FRACHTOWIEC\nHangar · Warsztat · Magazyn"
_prompt.tooltip_text = "Zadokuj, aby naprawić lądownik, rozbudować statek i przenieść wyposażenie." _prompt.tooltip_text = "Zadokuj, aby rozbudować statek, rozpocząć daleką wyprawę i przenieść wyposażenie."
else: else:
var profile: Dictionary = _near_planet.planet_profile var profile: Dictionary = _near_planet.planet_profile
_contact_label.text = "● TELEMETRIA / DANE PLANETY" _contact_label.text = "● TELEMETRIA / DANE PLANETY"
@ -193,7 +194,7 @@ func land_nearby() -> void:
if CONTACT_RANGE.at_freighter(player,freighter): if CONTACT_RANGE.at_freighter(player,freighter):
open_freighter() open_freighter()
return return
if not is_instance_valid(_near_planet): if not PLANET_LANDING_ENABLED or not is_instance_valid(_near_planet):
return return
var progress: Resource = GameState.captain.expedition var progress: Resource = GameState.captain.expedition
if not progress.begin_visit(_near_planet.planet_profile): if not progress.begin_visit(_near_planet.planet_profile):

View File

@ -1,9 +1,7 @@
extends RefCounted extends RefCounted
const MENU := preload("res://scripts/planets/freighter_menu.gd") const MENU := preload("res://scripts/planets/freighter_menu.gd")
const NETWORK := preload("res://scripts/planets/mining_network.gd")
static func build(screen: Control) -> void: static func build(screen: Control) -> void:
var progress: Resource = screen.captain.expedition
var navigation := MENU.card(screen._content) var navigation := MENU.card(screen._content)
MENU.label(navigation, "NAWIGACJA / ODKRYTE UKŁADY", 14, MENU.GOLD) MENU.label(navigation, "NAWIGACJA / ODKRYTE UKŁADY", 14, MENU.GOLD)
MENU.label(navigation, "Obecny: System %d · Najdalszy odkryty: %d" % [GameState.system_level, GameState.highest_system], 12) MENU.label(navigation, "Obecny: System %d · Najdalszy odkryty: %d" % [GameState.system_level, GameState.highest_system], 12)
@ -24,53 +22,8 @@ static func build(screen: Control) -> void:
destination.item_selected.connect(func(_index): travel.disabled = not GameState.active_distant_map.is_empty() or destination.get_selected_id() == GameState.system_level) destination.item_selected.connect(func(_index): travel.disabled = not GameState.active_distant_map.is_empty() or destination.get_selected_id() == GameState.system_level)
MENU.label(navigation, "Nowe układy odblokujesz, przechodząc dalej przez tunel po pokonaniu przeciwników.", 11, MENU.MUTED) MENU.label(navigation, "Nowe układy odblokujesz, przechodząc dalej przez tunel po pokonaniu przeciwników.", 11, MENU.MUTED)
preload("res://scripts/planets/distant_expeditions_panel.gd").build(screen) preload("res://scripts/planets/distant_expeditions_panel.gd").build(screen)
var summary := MENU.card(screen._content)
MENU.label(summary, "SIEĆ WYDOBYWCZA", 14, MENU.GOLD)
var stock := MENU.label(summary, "", 13, Color("95dce8"))
screen._economy_labels["network"] = stock
MENU.label(summary, "Wydobycie: zasobność × (1 + zarządzanie / 100) deuteru/min. Bez załoganta: 0. Złoża nie wyczerpują się.", 11, MENU.MUTED)
MENU.label(summary, "Jeden załogant obsługuje jedną placówkę. Nowy przydział przenosi go z poprzedniej.", 11, MENU.MUTED)
if progress.mining_stations.is_empty():
MENU.label(summary, "Brak placówek. Szukaj głębokich złóż na planetach (szansa 60%). Budowa: zasobność × 10 części.", 12)
for station_id in progress.mining_stations:
var station: Dictionary = progress.mining_stations[station_id]
var card := MENU.card(screen._content)
MENU.label(card, "%s · SYSTEM %d" % [station.planet_name, station.system], 13, MENU.GOLD)
MENU.label(card, "Zasobność: %d / 100" % NETWORK.station_richness(station), 12, Color("95dce8"))
var member: Dictionary = progress.crew.get(str(station.get("crew_id", "")), {})
MENU.label(card, "Czeka na załoganta" if member.is_empty() else "%s · zarządzanie %d/100 · %.1f deuteru/min" % [member.name, member.management, NETWORK.station_rate(progress, station)], 12)
var assign_row := MENU.row(card)
var option := OptionButton.new()
option.name = "StationCrew_" + str(station_id).md5_text().substr(0, 8)
option.size_flags_horizontal = Control.SIZE_EXPAND_FILL
option.fit_to_longest_item = false
option.add_theme_font_size_override("font_size", 11)
option.add_theme_font_override("font", ThemeDB.fallback_font)
option.add_item("Bez przydziału")
option.set_item_metadata(0, "")
for crew_id in progress.crew:
var person: Dictionary = progress.crew[crew_id]
option.add_item("%s · %d/100" % [person.name, person.management])
option.set_item_metadata(option.item_count - 1, crew_id)
if str(station.get("crew_id", "")) == str(crew_id): option.select(option.item_count - 1)
assign_row.add_child(option)
var apply := MENU.button(assign_row, "PRZYDZIEL", screen._assign_station.bind(str(station_id), option))
apply.disabled = progress.crew.is_empty() and member.is_empty()
if progress.crew.is_empty(): MENU.label(screen._content, "Brak załogantów. Przygotuj kajutę i zatrudnij specjalistę spotkanego na planecie.", 12, MENU.MUTED)
screen._refresh_live_labels() screen._refresh_live_labels()
static func cabin(screen: Control) -> void: static func cabin(screen: Control) -> void:
var progress: Resource = screen.captain.expedition MENU.label(screen._content, "SEKCJA REZERWOWA", 14, MENU.GOLD)
var member := NETWORK.cabin_crew(progress, screen._tab) MENU.label(screen._content, "Pomieszczenie czeka na nowe przeznaczenie.", 12)
var card := MENU.card(screen._content)
if member.is_empty():
MENU.label(card, "WOLNA KAJUTA", 14, MENU.GOLD)
MENU.label(card, "Przygotowana dla jednego załoganta. Specjalistów możesz zatrudniać na planetach.", 12)
return
MENU.label(card, str(member.name), 15, MENU.GOLD)
MENU.label(card, "Zarządzanie: %d / 100\nPremia do wydobycia: +%d%%\nPochodzenie: %s" % [member.management, member.management, member.planet_name], 12)
var assignment := "Bez przydziału"
for station in progress.mining_stations.values():
if str(station.get("crew_id", "")) == str(member.id): assignment = "%s · System %d" % [station.planet_name, station.system]
MENU.label(card, "Placówka: " + assignment, 12, Color("95dce8"))
MENU.button(card, "ZWOLNIJ ZAŁOGANTA", screen._dismiss_crew.bind(str(member.id)))

View File

@ -3,7 +3,7 @@ signal section_selected(section: String)
signal section_hovered(section: String) signal section_hovered(section: String)
const DESIGN_SIZE := Vector2(1792,768) const DESIGN_SIZE := Vector2(1792,768)
const ROOMS := [ const ROOMS := [
{"id":"command","title":"CENTRUM DOWODZENIA","status":"NAWIGACJA / PLACÓWKI","rect":Rect2(25,165,315,435),"tag":Vector2(88,68),"active":false}, {"id":"command","title":"CENTRUM DOWODZENIA","status":"NAWIGACJA / WYPRAWY","rect":Rect2(25,165,315,435),"tag":Vector2(88,68),"active":false},
{"id":"workshop","title":"WARSZTAT","status":"OPERACYJNY","rect":Rect2(360,120,255,205),"tag":Vector2(360,58),"active":true}, {"id":"workshop","title":"WARSZTAT","status":"OPERACYJNY","rect":Rect2(360,120,255,205),"tag":Vector2(360,58),"active":true},
{"id":"expansion_1","title":"SEKCJA 02","status":"W REMONCIE","rect":Rect2(632,120,200,205),"tag":Vector2(632,58),"active":false}, {"id":"expansion_1","title":"SEKCJA 02","status":"W REMONCIE","rect":Rect2(632,120,200,205),"tag":Vector2(632,58),"active":false},
{"id":"expansion_2","title":"SEKCJA 03","status":"W REMONCIE","rect":Rect2(848,120,214,205),"tag":Vector2(848,58),"active":false}, {"id":"expansion_2","title":"SEKCJA 03","status":"W REMONCIE","rect":Rect2(848,120,214,205),"tag":Vector2(848,58),"active":false},
@ -53,7 +53,7 @@ func room(id: String) -> Dictionary:
return {} return {}
func is_buildable(id: String) -> bool: func is_buildable(id: String) -> bool:
return ROOM_PLANS.has(id) or id in ["cabin_1","cabin_2","cabin_3","cabin_4","cabin_5"] return ROOM_PLANS.has(id)
func plan(id: String) -> Dictionary: func plan(id: String) -> Dictionary:
if ROOM_PLANS.has(id): return ROOM_PLANS[id] if ROOM_PLANS.has(id): return ROOM_PLANS[id]

View File

@ -17,6 +17,9 @@ var _owned_open := false
var _storage_columns: GridContainer var _storage_columns: GridContainer
var _storage_notice := "" var _storage_notice := ""
var _economy_labels: Dictionary = {} var _economy_labels: Dictionary = {}
var _engineer_item: ItemInstance
var _engineer_addon := 0
var _engineer_notice := ""
var _alien_timer: Label var _alien_timer: Label
var _alien_signature := "" var _alien_signature := ""
@onready var deck: Control = $Deck @onready var deck: Control = $Deck
@ -26,6 +29,7 @@ func _ready() -> void:
offset_top = preload("res://scripts/ui/resource_bar.gd").HEIGHT offset_top = preload("res://scripts/ui/resource_bar.gd").HEIGHT
if captain == null: if captain == null:
captain = GameState.captain captain = GameState.captain
captain.expedition.freighter_rooms["expansion_1"] = "laboratory"
captain.expedition.freighter_rooms["expansion_3"] = "module_storage" captain.expedition.freighter_rooms["expansion_3"] = "module_storage"
captain.expedition.changed.connect(_refresh_live_labels) captain.expedition.changed.connect(_refresh_live_labels)
deck.sync_rooms(captain.expedition.freighter_rooms) deck.sync_rooms(captain.expedition.freighter_rooms)
@ -180,7 +184,7 @@ func _label(text: String, size_value := 14, color := Color("c5d3d7")) -> Label:
func _refresh() -> void: func _refresh() -> void:
_economy_labels.clear() _economy_labels.clear()
_storage_columns = null _storage_columns = null
_audio.set_maintenance_needed(captain.expedition.lander_hp < 30) _audio.set_maintenance_needed(false)
for child in _content.get_children(): for child in _content.get_children():
_content.remove_child(child) _content.remove_child(child)
child.queue_free() child.queue_free()
@ -279,7 +283,6 @@ func _construct_room(id: String) -> void:
_refresh() _refresh()
func _build_hangar() -> void: func _build_hangar() -> void:
var progress: Resource = captain.expedition
MENU.hero(_content,"hangar","") MENU.hero(_content,"hangar","")
var ship_row := MENU.row(_content) var ship_row := MENU.row(_content)
MENU.icon(ship_row,deck.ship_texture,32) MENU.icon(ship_row,deck.ship_texture,32)
@ -288,51 +291,7 @@ func _build_hangar() -> void:
ship_row.add_child(ship_text) ship_row.add_child(ship_text)
MENU.label(ship_text,deck.ship_name,15,MENU.GOLD) MENU.label(ship_text,deck.ship_name,15,MENU.GOLD)
MENU.label(ship_text,"Żelazo %d · Części %d" % [int(captain.minerals.get("iron",0)),int(captain.parts)],12,MENU.MUTED) MENU.label(ship_text,"Żelazo %d · Części %d" % [int(captain.minerals.get("iron",0)),int(captain.parts)],12,MENU.MUTED)
var service := MENU.card(_content)
MENU.style(service.get_parent(),8,0.70)
var row := MENU.row(service)
MENU.icon(row,preload("res://assets/planets/lander.png"),34)
var info := VBoxContainer.new()
info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(info)
MENU.label(info,"Lądownik · %d%%" % ceili(progress.lander_hp),13,MENU.GOLD)
var bar := ProgressBar.new()
bar.value = progress.lander_hp
bar.show_percentage = false
bar.custom_minimum_size.y = 5
var fill := StyleBoxFlat.new()
fill.bg_color = Color("85b7a0")
fill.set_corner_radius_all(3)
bar.add_theme_stylebox_override("fill",fill)
info.add_child(bar)
var cost: int = progress.repair_cost()
var cost_row := MENU.row(info)
MENU.icon(cost_row,RECIPES.icon("parts"),14)
var repair_cost := MENU.label(cost_row,str(cost),11,MENU.MUTED)
repair_cost.name = "RepairCost"
repair_cost.autowrap_mode = TextServer.AUTOWRAP_OFF
var repair := MENU.button(row,"Napraw",func():
if progress.repair(captain): _audio.service_completed(0.65)
_refresh(),true)
repair.name = "RepairLander"
repair.custom_minimum_size = Vector2(58,26)
repair.size_flags_vertical = Control.SIZE_SHRINK_CENTER
repair.tooltip_text = "Napraw do 100%% · koszt %d części. Pełny kadłub wymagany do lądowania." % cost
repair.disabled = cost == 0 or captain.parts < cost
_build_slot_upgrades() _build_slot_upgrades()
var delivery := MENU.card(_content)
MENU.style(delivery.get_parent(),8,0.70)
var delivery_row := MENU.row(delivery)
MENU.icon(delivery_row,preload("res://assets/icons/shield.png"),24)
var label := MENU.label(delivery_row,"Odzyskane moduły · %d" % progress.delivered_modules.size(),12,MENU.MUTED)
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var take := MENU.button(delivery_row,"Odbierz",func():
if is_instance_valid(inventory): progress.deliver_modules(inventory)
_refresh())
take.name = "CollectRecovered"
take.custom_minimum_size = Vector2(58,26)
take.tooltip_text = "Odbierz do ładowni statku. Przedmioty bez miejsca pozostają na fregacie."
take.disabled = progress.delivered_modules.is_empty() or not is_instance_valid(inventory)
func _build_slot_upgrades() -> void: func _build_slot_upgrades() -> void:
if not is_instance_valid(inventory): if not is_instance_valid(inventory):
@ -385,18 +344,6 @@ func _unlock_ship_slot(kind: String) -> void:
func _build_storage() -> void: func _build_storage() -> void:
MENU.hero(_content,"storage","MAGAZYN / ZASOBY I WYPOSAŻENIE") MENU.hero(_content,"storage","MAGAZYN / ZASOBY I WYPOSAŻENIE")
var deuter_card := MENU.card(_content)
var deuter_row := MENU.row(deuter_card)
MENU.icon(deuter_row, load("res://assets/icons/resources/deuter.svg"), 26)
var deuter_title := MENU.label(deuter_row, "DEUTER / PALIWO FREGATY", 13, Color("95dce8"))
deuter_title.autowrap_mode = TextServer.AUTOWRAP_OFF
deuter_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var deuter_amount := MENU.label(deuter_row, "%.1f" % captain.expedition.deuter, 15, MENU.GOLD)
deuter_amount.autowrap_mode = TextServer.AUTOWRAP_OFF
deuter_amount.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
deuter_amount.custom_minimum_size.x = 80
_economy_labels["stock_header"] = deuter_amount
MENU.label(deuter_card, "Automatyczne dostawy z obsadzonych placówek · %.0f / min" % preload("res://scripts/planets/mining_network.gd").total_rate(captain.expedition), 11, MENU.MUTED)
if not deck.is_built("expansion_3"): if not deck.is_built("expansion_3"):
_build_resource_overview() _build_resource_overview()
var construction := MENU.card(_content) var construction := MENU.card(_content)
@ -476,7 +423,6 @@ func _build_resource_overview() -> void:
for key in ["ammunition","missile","irradiated_ammunition","energy_bank"]: values[key] = inventory.get_crafted_stock(key) for key in ["ammunition","missile","irradiated_ammunition","energy_bank"]: values[key] = inventory.get_crafted_stock(key)
values.time_crystals = captain.expedition.time_crystals values.time_crystals = captain.expedition.time_crystals
values.samples = captain.expedition.analysis_samples values.samples = captain.expedition.analysis_samples
values.deuter = captain.expedition.deuter
var names: Dictionary = RECIPES.NAMES.duplicate() var names: Dictionary = RECIPES.NAMES.duplicate()
names.merge(GEAR.MATERIAL_NAMES) names.merge(GEAR.MATERIAL_NAMES)
names.merge({"credits":"Kredyty","parts":"Części","ammunition":"Amunicja","missile":"Rakiety","irradiated_ammunition":"Radiacyjna","energy_bank":"Powerbanki","samples":"Próbki","deuter":"Deuter","time_crystals":"Kryształy czasu"}) names.merge({"credits":"Kredyty","parts":"Części","ammunition":"Amunicja","missile":"Rakiety","irradiated_ammunition":"Radiacyjna","energy_bank":"Powerbanki","samples":"Próbki","deuter":"Deuter","time_crystals":"Kryształy czasu"})
@ -557,8 +503,10 @@ func _craft_supply(id: String, batches := 1) -> void:
func _build_workshop() -> void: func _build_workshop() -> void:
MENU.hero(_content,"workshop","") MENU.hero(_content,"workshop","")
_build_supply_production() _build_supply_production()
preload("res://scripts/planets/orb_workshop.gd").build(self) MENU.label(_content,"Modyfikacja wyposażenia: kliknij orb w ładowni, a następnie przedmiot. PPM lub Esc anuluje wybór.",12,MENU.MUTED)
preload("res://scripts/planets/time_engineer_panel.gd").build(self,_engineer_notice,true)
_label("EKWIPUNEK KAPITANA",13,MENU.GOLD) _label("EKWIPUNEK KAPITANA",13,MENU.GOLD)
MENU.label(_content,"Sprzęt planetarny zachowano na przyszłość. Obecnie jego bonusy do eksploracji powierzchni nie mają zastosowania.",12,MENU.MUTED)
var grid := GridContainer.new() var grid := GridContainer.new()
grid.columns = 3 grid.columns = 3
grid.add_theme_constant_override("h_separation",6) grid.add_theme_constant_override("h_separation",6)
@ -616,13 +564,6 @@ func _refresh_live_labels() -> void:
if _tab == "expansion_2" and is_instance_valid(_drawer) and _drawer.visible and alien_signature != _alien_signature: if _tab == "expansion_2" and is_instance_valid(_drawer) and _drawer.visible and alien_signature != _alien_signature:
_alien_signature = alien_signature _alien_signature = alien_signature
_refresh.call_deferred() _refresh.call_deferred()
for key in _economy_labels:
var label: Label = _economy_labels[key]
if not is_instance_valid(label): continue
if key == "expedition_fuel":
label.text = "Zapas paliwa: %.1f deuteru" % captain.expedition.deuter
continue
label.text = "Deuter: %.1f · Produkcja: %.0f/min" % [captain.expedition.deuter, preload("res://scripts/planets/mining_network.gd").total_rate(captain.expedition)] if key == "network" else "%.1f" % captain.expedition.deuter
func _craft(kind: String) -> void: func _craft(kind: String) -> void:
if _tab != "workshop": return if _tab != "workshop": return
@ -655,3 +596,13 @@ func _research_completed() -> void:
_audio.service_completed() _audio.service_completed()
captain.emit_changed() captain.emit_changed()
_refresh() _refresh()
func _maximize_addon() -> void:
if _tab != "workshop" or not is_instance_valid(inventory): return
if preload("res://scripts/planets/time_engineer.gd").maximize(captain.expedition,inventory,_engineer_item,_engineer_addon):
_engineer_notice = "✓ Addon zmaksymalizowany"
captain.emit_changed()
_audio.service_completed()
else:
_engineer_notice = "Sprawdź kryształy i wybrany addon."
preload("res://scripts/planets/time_engineer_panel.gd").refresh(self)

View File

@ -1,5 +1,5 @@
extends RefCounted extends RefCounted
const DURATION := 600.0 const DURATION := 0.0
const IDS := [ const IDS := [
"gamma_cannon", "beam_laser", "machine_gun", "missile_launcher", "unguided_missile", "gamma_cannon", "beam_laser", "machine_gun", "missile_launcher", "unguided_missile",
"incendiary_missile", "radiation_laser", "artillery_cannon", "external_turret", "pulse_laser", "incendiary_missile", "radiation_laser", "artillery_cannon", "external_turret", "pulse_laser",
@ -37,6 +37,7 @@ static func definition(id: String) -> ItemDefinition:
return load("res://resources/items/%s.tres" % id) as ItemDefinition if id in IDS else null return load("res://resources/items/%s.tres" % id) as ItemDefinition if id in IDS else null
static func ensure_offers(progress: Resource) -> void: static func ensure_offers(progress: Resource) -> void:
if not progress.laboratory_project.is_empty(): advance(progress,0.0)
if not progress.laboratory_offers.is_empty() or not progress.laboratory_project.is_empty(): return if not progress.laboratory_offers.is_empty() or not progress.laboratory_project.is_empty(): return
var pool := IDS.duplicate() var pool := IDS.duplicate()
for id in progress.laboratory_unlocks: pool.erase(id) for id in progress.laboratory_unlocks: pool.erase(id)
@ -54,24 +55,28 @@ static func can_start(captain: Resource, id: String) -> bool:
static func start(captain: Resource, id: String) -> bool: static func start(captain: Resource, id: String) -> bool:
if not can_start(captain,id): return false if not can_start(captain,id): return false
for key in COSTS[id]: captain.minerals[key] = int(captain.minerals.get(key,0))-int(COSTS[id][key]) for key in COSTS[id]: captain.minerals[key] = int(captain.minerals.get(key,0))-int(COSTS[id][key])
captain.expedition.laboratory_project = id var progress: Resource = captain.expedition
captain.expedition.laboratory_duration = research_duration(captain) progress.laboratory_unlocks[id] = true
captain.expedition.laboratory_remaining = captain.expedition.laboratory_duration progress.laboratory_duration = 0.0
progress.laboratory_remaining = 0.0
progress.laboratory_offers.clear()
ensure_offers(progress)
captain.emit_changed() captain.emit_changed()
captain.expedition.emit_changed() captain.expedition.emit_changed()
return true return true
static func research_duration(captain: Resource) -> float: static func research_duration(_captain: Resource) -> float:
return DURATION * (1.0 - captain.get_skill_bonus("scientist")) return 0.0
static func advance(progress: Resource, delta: float) -> bool: static func advance(progress: Resource, _delta: float) -> bool:
if progress.laboratory_project.is_empty() or delta <= 0.0: return false # Finish an already-paid project restored from an older save immediately.
progress.laboratory_remaining = maxf(0.0,progress.laboratory_remaining-delta) if progress.laboratory_project.is_empty(): return false
if progress.laboratory_remaining <= 0.00001: if progress.laboratory_project in IDS:
progress.laboratory_remaining = 0.0
progress.laboratory_unlocks[progress.laboratory_project] = true progress.laboratory_unlocks[progress.laboratory_project] = true
progress.laboratory_project = "" progress.laboratory_project = ""
progress.laboratory_offers.clear() progress.laboratory_remaining = 0.0
ensure_offers(progress) progress.laboratory_duration = 0.0
progress.emit_changed() progress.laboratory_offers.clear()
ensure_offers(progress)
progress.emit_changed()
return true return true

View File

@ -3,8 +3,6 @@ const LAB := preload("res://scripts/planets/laboratory.gd")
const MENU := preload("res://scripts/planets/freighter_menu.gd") const MENU := preload("res://scripts/planets/freighter_menu.gd")
var captain: Resource var captain: Resource
var _signature := "" var _signature := ""
var _timer: Label
var _bar: ProgressBar
var _buttons: Dictionary = {} var _buttons: Dictionary = {}
func _ready() -> void: func _ready() -> void:
@ -12,13 +10,10 @@ func _ready() -> void:
_build() _build()
func _state() -> String: func _state() -> String:
return str(captain.expedition.laboratory_project)+str(captain.expedition.laboratory_unlocks)+str(captain.get_skill_rank("scientist")) return str(captain.expedition.laboratory_unlocks)+str(captain.expedition.laboratory_offers)
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
if _signature != _state(): _build() if _signature != _state(): _build()
if is_instance_valid(_timer):
_timer.text = "Pozostało: %d s gry" % ceili(captain.expedition.laboratory_remaining)
_bar.value = captain.expedition.laboratory_duration-captain.expedition.laboratory_remaining
for id in _buttons: for id in _buttons:
if is_instance_valid(_buttons[id]): _buttons[id].disabled = not LAB.can_start(captain,id) if is_instance_valid(_buttons[id]): _buttons[id].disabled = not LAB.can_start(captain,id)
@ -27,40 +22,28 @@ func _build() -> void:
for child in get_children(): for child in get_children():
remove_child(child) remove_child(child)
child.queue_free() child.queue_free()
_timer = null
_buttons.clear() _buttons.clear()
_signature = _state() _signature = _state()
var progress: Resource = captain.expedition var progress: Resource = captain.expedition
MENU.label(self,"PROJEKTY BRONI I ROZSZERZEŃ",15,MENU.GOLD) MENU.label(self,"PROJEKTY BRONI I ROZSZERZEŃ",15,MENU.GOLD)
MENU.label(self,"Wybierz projekt. Nowe badanie trwa %d sekund gry. Odkryta broń lub rozszerzenie zacznie pojawiać się w łupach. Od początku dostępne jest działko plazmowe." % roundi(LAB.research_duration(captain)),12,MENU.MUTED) MENU.label(self,"Odkryj projekt za surowce — od razu trafi do puli łupów.",12,MENU.MUTED)
if not progress.laboratory_project.is_empty(): for id in progress.laboratory_offers:
var item := LAB.definition(progress.laboratory_project) var item := LAB.definition(id)
var row := MENU.row(self) var card := MENU.card(self)
var row := MENU.row(card)
MENU.icon(row,item.icon,64) MENU.icon(row,item.icon,64)
MENU.label(row,item.display_name,16,MENU.GOLD).size_flags_horizontal = Control.SIZE_EXPAND_FILL MENU.label(row,item.display_name,15,MENU.GOLD).size_flags_horizontal = Control.SIZE_EXPAND_FILL
_timer = MENU.label(self,"",14) MENU.label(card,item.description,12)
_bar = ProgressBar.new() var names := preload("res://scripts/items/crafting_recipes.gd").NAMES.duplicate()
_bar.max_value = progress.laboratory_duration names.merge(preload("res://scripts/planets/planet_profiles.gd").MATERIAL_NAMES)
_bar.custom_minimum_size.y = 18 var costs: Array[String] = []
add_child(_bar) for key in LAB.COSTS[id]: costs.append("%s: %d / %d" % [names[key],int(captain.minerals.get(key,0)),int(LAB.COSTS[id][key])])
else: MENU.label(card,"KOSZT · posiadasz / wymagane\n"+" · ".join(costs),12,MENU.MUTED)
for id in progress.laboratory_offers: var button := MENU.button(card,"ODKRYJ PROJEKT",_start.bind(id),true)
var item := LAB.definition(id) button.name = "Research_"+id
var card := MENU.card(self) button.disabled = not LAB.can_start(captain,id)
var row := MENU.row(card) _buttons[id] = button
MENU.icon(row,item.icon,64) 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,"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)
if not progress.laboratory_unlocks.is_empty(): if not progress.laboratory_unlocks.is_empty():
MENU.label(self,"ODKRYTE · DOSTĘPNE W ŁUPACH",13,MENU.GOLD) MENU.label(self,"ODKRYTE · DOSTĘPNE W ŁUPACH",13,MENU.GOLD)
for id in LAB.IDS: for id in LAB.IDS:

View File

@ -15,8 +15,8 @@ const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
@export var laboratory_offers: Array[String] = [] @export var laboratory_offers: Array[String] = []
@export var laboratory_project: String = "" @export var laboratory_project: String = ""
@export var laboratory_remaining: float = 0.0 @export var laboratory_remaining: float = 0.0
@export var laboratory_duration: float = 600.0 @export var laboratory_duration: float = 0.0
@export var freighter_rooms: Dictionary = {"expansion_3":"module_storage"} @export var freighter_rooms: Dictionary = {"expansion_1":"laboratory", "expansion_3":"module_storage"}
@export var mining_stations: Dictionary = {} @export var mining_stations: Dictionary = {}
@export var distant_maps: Dictionary = {} @export var distant_maps: Dictionary = {}
@export var hermit_purchases: Dictionary = {} @export var hermit_purchases: Dictionary = {}

View File

@ -30,7 +30,7 @@ static func can_maximize(item: ItemInstance, addon: int) -> bool:
return item.additional_modifiers[addon].value < maximum(item,addon) - 0.000001 return item.additional_modifiers[addon].value < maximum(item,addon) - 0.000001
static func maximize(progress: Resource, inventory: Node, item: ItemInstance, addon: int) -> bool: static func maximize(progress: Resource, inventory: Node, item: ItemInstance, addon: int) -> bool:
if not progress.active or inventory == null or not bool(inventory.get_parent().get("is_docked")): return false if inventory == null or not bool(inventory.get_parent().get("is_docked")): return false
if not can_maximize(item,addon) or progress.time_crystals < cost(item): return false if not can_maximize(item,addon) or progress.time_crystals < cost(item): return false
var owned: Array = inventory.cargo + inventory.weapon_slots + inventory.module_slots + inventory.extension_slots var owned: Array = inventory.cargo + inventory.weapon_slots + inventory.module_slots + inventory.extension_slots
if not owned.has(item): return false if not owned.has(item): return false

View File

@ -2,10 +2,15 @@ extends RefCounted
const CARD := preload("res://scripts/planets/poi_presentation.gd") const CARD := preload("res://scripts/planets/poi_presentation.gd")
const SERVICE := preload("res://scripts/planets/time_engineer.gd") const SERVICE := preload("res://scripts/planets/time_engineer.gd")
const CYAN := Color("9be7ff") const CYAN := Color("9be7ff")
const MENU := preload("res://scripts/planets/freighter_menu.gd")
const CRYSTAL := preload("res://assets/icons/resources/time_crystals.png")
static func build(screen: Control, message: String = "") -> void: static func build(screen: Control, message: String = "", freighter := false) -> void:
var parent: VBoxContainer = screen._choices if freighter:
var progress: Resource = screen.progress _build_compact(screen,message)
return
var parent: VBoxContainer = screen._content if freighter else screen._choices
var progress: Resource = screen.captain.expedition if freighter else screen.progress
var art := TextureRect.new() var art := TextureRect.new()
art.texture = load("res://assets/planets/props/time_engineer.png") art.texture = load("res://assets/planets/props/time_engineer.png")
art.expand_mode = TextureRect.EXPAND_IGNORE_SIZE art.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
@ -17,11 +22,11 @@ static func build(screen: Control, message: String = "") -> void:
if not message.is_empty(): CARD.text(parent,message,12,CARD.GOLD) if not message.is_empty(): CARD.text(parent,message,12,CARD.GOLD)
CARD.text(parent,"Kryształy czasu: %d" % progress.time_crystals,14,CYAN) CARD.text(parent,"Kryształy czasu: %d" % progress.time_crystals,14,CYAN)
CARD.text(parent,"Koszt: 10 kryształów × poziom przedmiotu. Gotowe maksima nie pobierają opłaty.",12) CARD.text(parent,"Koszt: 10 kryształów × poziom przedmiotu. Gotowe maksima nie pobierają opłaty.",12)
if not is_instance_valid(screen.flight_ship): if not is_instance_valid(screen.inventory if freighter else screen.flight_ship):
CARD.text(parent,"Brak połączenia z wyposażeniem statku.",12) CARD.text(parent,"Brak połączenia z wyposażeniem statku.",12)
_back(parent,screen) _back(parent,screen,freighter)
return return
var inventory: Node = screen.flight_ship.get_node("ShipInventory") var inventory: Node = screen.inventory if freighter else screen.flight_ship.get_node("ShipInventory")
var items: Array[ItemInstance] = [] var items: Array[ItemInstance] = []
var locations: Array[String] = [] var locations: Array[String] = []
for kind in ["weapon","module","extension","cargo"]: for kind in ["weapon","module","extension","cargo"]:
@ -32,8 +37,8 @@ static func build(screen: Control, message: String = "") -> void:
items.append(item) items.append(item)
locations.append({"weapon":"Broń","module":"Moduł","extension":"Rozszerzenie","cargo":"Ładownia"}[kind]) locations.append({"weapon":"Broń","module":"Moduł","extension":"Rozszerzenie","cargo":"Ładownia"}[kind])
if items.is_empty(): if items.is_empty():
CARD.text(parent,"Brak przedmiotów z addonami na statku. Zabierz wyposażenie z magazynu fregaty przed lądowaniem.",12,CARD.GOLD) CARD.text(parent,"Brak przedmiotów z addonami na statku. Przenieś wyposażenie z magazynu fregaty do ładowni.",12,CARD.GOLD)
_back(parent,screen) _back(parent,screen,freighter)
return return
if not items.has(screen._engineer_item): if not items.has(screen._engineer_item):
screen._engineer_item = items[0] screen._engineer_item = items[0]
@ -48,7 +53,7 @@ static func build(screen: Control, message: String = "") -> void:
choice.item_selected.connect(func(index): choice.item_selected.connect(func(index):
screen._engineer_item = choice.get_item_metadata(index) screen._engineer_item = choice.get_item_metadata(index)
screen._engineer_addon = 0 screen._engineer_addon = 0
screen._show_poi() screen._refresh() if freighter else screen._show_poi()
) )
var item: ItemInstance = screen._engineer_item var item: ItemInstance = screen._engineer_item
CARD.text(parent,"ADDON DO MAKSYMALIZACJI",11,CARD.MUTED) CARD.text(parent,"ADDON DO MAKSYMALIZACJI",11,CARD.MUTED)
@ -65,13 +70,13 @@ static func build(screen: Control, message: String = "") -> void:
if index == screen._engineer_addon: selected = option if index == screen._engineer_addon: selected = option
if addons.item_count == 0: if addons.item_count == 0:
CARD.text(parent,"Brak dostępnych addonów.",12) CARD.text(parent,"Brak dostępnych addonów.",12)
_back(parent,screen) _back(parent,screen,freighter)
return return
addons.select(selected) addons.select(selected)
screen._engineer_addon = int(addons.get_item_metadata(selected)) screen._engineer_addon = int(addons.get_item_metadata(selected))
addons.item_selected.connect(func(index): addons.item_selected.connect(func(index):
screen._engineer_addon = int(addons.get_item_metadata(index)) screen._engineer_addon = int(addons.get_item_metadata(index))
screen._show_poi() screen._refresh() if freighter else screen._show_poi()
) )
var index: int = screen._engineer_addon var index: int = screen._engineer_addon
var affix := SERVICE.affix_for(item,index) var affix := SERVICE.affix_for(item,index)
@ -87,7 +92,7 @@ static func build(screen: Control, message: String = "") -> void:
CARD.text(parent,"Ten addon ma już maksymalną wartość lub nie posiada zakresu losowania.",12,CARD.MUTED) CARD.text(parent,"Ten addon ma już maksymalną wartość lub nie posiada zakresu losowania.",12,CARD.MUTED)
elif progress.time_crystals < cost: elif progress.time_crystals < cost:
CARD.text(parent,"Brakuje %d kryształów czasu." % (cost-progress.time_crystals),12,CARD.GOLD) CARD.text(parent,"Brakuje %d kryształów czasu." % (cost-progress.time_crystals),12,CARD.GOLD)
_back(parent,screen) _back(parent,screen,freighter)
static func _choice(parent: Node, node_name: String) -> OptionButton: static func _choice(parent: Node, node_name: String) -> OptionButton:
var choice := OptionButton.new() var choice := OptionButton.new()
@ -99,5 +104,224 @@ static func _choice(parent: Node, node_name: String) -> OptionButton:
parent.add_child(choice) parent.add_child(choice)
return choice return choice
static func _back(parent: Node, screen: Control) -> void: static func _back(parent: Node, screen: Control, freighter := false) -> void:
if freighter: return
CARD.button(parent,"Wróć do wyprawy",func(): screen._dialog.hide()) CARD.button(parent,"Wróć do wyprawy",func(): screen._dialog.hide())
static func refresh(screen: Control) -> void:
var old: Node = screen._content.get_node_or_null("ChronalBench")
var index: int = old.get_index() if old != null else screen._content.get_child_count()
if old != null:
screen._content.remove_child(old)
old.queue_free()
_build_compact(screen,screen._engineer_notice)
screen._content.move_child(screen._content.get_node("ChronalBench"),index)
static func _build_compact(screen: Control, message: String) -> void:
var panel := PanelContainer.new()
panel.name = "ChronalBench"
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel.add_theme_stylebox_override("panel",_box(Color("101e2bf2"),Color("416b7c"),10))
screen._content.add_child(panel)
var body := VBoxContainer.new()
body.add_theme_constant_override("separation",9)
panel.add_child(body)
var progress: Resource = screen.captain.expedition
var header := MENU.row(body)
MENU.icon(header,preload("res://assets/icons/screwdriver.png"),23)
var title := _text(header,"MAKSYMALIZACJA ADDONU",CYAN)
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
MENU.icon(header,CRYSTAL,24)
_text(header,str(progress.time_crystals),MENU.GOLD)
header.tooltip_text = "Kryształy czasu · koszt maksymalizacji: 10 × poziom przedmiotu."
if not is_instance_valid(screen.inventory):
_text(body,"Zadokuj statkiem, aby wybrać wyposażenie.",MENU.MUTED)
return
var inventory: Node = screen.inventory
var items: Array[ItemInstance] = []
var locations: Array[String] = []
for kind in ["weapon","module","extension","cargo"]:
var slots: Array = inventory._get_slots(kind)
for index in slots.size():
var item: ItemInstance = slots[index]
if not inventory.is_slot_unlocked(kind,index) or item == null or item.definition == null or item.additional_modifiers.is_empty() or items.has(item): continue
items.append(item)
locations.append({"weapon":"Broń","module":"Moduł","extension":"Rozszerzenie","cargo":"Ładownia"}[kind])
if items.is_empty():
var empty := MENU.row(body)
MENU.icon(empty,preload("res://assets/icons/cargo_crate.svg"),32)
var hint := _text(empty,"Brak wyposażenia z addonami",MENU.MUTED)
hint.tooltip_text = "Przenieś przedmiot z addonami do ładowni lub zamontuj go na statku."
return
if not items.has(screen._engineer_item):
screen._engineer_item = items[0]
screen._engineer_addon = 0
var item: ItemInstance = screen._engineer_item
var scroll := ScrollContainer.new()
scroll.name = "EngineerItem"
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
scroll.custom_minimum_size.y = 72
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
body.add_child(scroll)
var tray := HBoxContainer.new()
tray.add_theme_constant_override("separation",6)
scroll.add_child(tray)
for index in items.size():
var candidate: ItemInstance = items[index]
var pick := Button.new()
pick.name = "EngineerItem_%d" % index
pick.custom_minimum_size = Vector2(54,54)
pick.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
pick.icon = candidate.definition.icon
pick.expand_icon = true
pick.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
pick.add_theme_constant_override("icon_max_width",34)
pick.tooltip_text = "%s · %s\n%s" % [locations[index],candidate.definition.display_name,candidate.get_tooltip_text()]
_skin(pick,candidate == item)
tray.add_child(pick)
if candidate == item: scroll.ensure_control_visible.call_deferred(pick)
var level := _text(pick,str(candidate.item_level),CYAN if candidate == item else MENU.MUTED)
level.position = Vector2(4,35)
level.add_theme_font_size_override("font_size",10)
level.mouse_filter = Control.MOUSE_FILTER_IGNORE
pick.pressed.connect(func():
screen._engineer_item = candidate
screen._engineer_addon = 0
screen._engineer_notice = ""
refresh(screen))
var selected := MENU.row(body)
var item_name := _text(selected,item.definition.display_name,MENU.TEXT)
item_name.size_flags_horizontal = Control.SIZE_EXPAND_FILL
item_name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
item_name.tooltip_text = item.get_tooltip_text()
_text(selected,"POZ. %d" % item.item_level,MENU.MUTED)
var addons := VBoxContainer.new()
addons.name = "EngineerAddon"
addons.add_theme_constant_override("separation",5)
body.add_child(addons)
var first := -1
for index in item.additional_modifiers.size():
if item.additional_modifiers[index] != null:
if first < 0: first = index
if first < 0:
_text(body,"Brak dostępnych addonów.",MENU.MUTED)
return
if screen._engineer_addon < 0 or screen._engineer_addon >= item.additional_modifiers.size() or item.additional_modifiers[screen._engineer_addon] == null:
screen._engineer_addon = first
for index in item.additional_modifiers.size():
var modifier: ItemStatModifier = item.additional_modifiers[index]
if modifier == null: continue
_addon_button(screen,addons,item,index)
var index: int = screen._engineer_addon
var ready := SERVICE.can_maximize(item,index)
var cost := SERVICE.cost(item)
var footer := MENU.row(body)
MENU.icon(footer,CRYSTAL,28)
var price := _text(footer,str(cost),MENU.GOLD if progress.time_crystals >= cost else Color("ef9389"))
price.size_flags_horizontal = Control.SIZE_EXPAND_FILL
price.tooltip_text = "Koszt: 10 kryształów × poziom przedmiotu."
var apply := MENU.button(footer,"MAKSYMALIZUJ" if ready else "✓ MAX",screen._maximize_addon,true)
apply.name = "MaximizeSelectedAddon"
apply.custom_minimum_size = Vector2(158,36)
apply.icon = preload("res://assets/icons/screwdriver.png")
apply.expand_icon = true
apply.add_theme_constant_override("icon_max_width",18)
apply.size_flags_vertical = Control.SIZE_SHRINK_CENTER
apply.disabled = not ready or progress.time_crystals < cost
apply.tooltip_text = "Maksymalizuj tylko wybrany addon. Pozostałe parametry zostają bez zmian."
if ready and progress.time_crystals < cost:
_text(body,"Brakuje %d kryształów" % (cost-progress.time_crystals),Color("ef9389"))
elif not message.is_empty():
_text(body,message,CYAN)
static func _addon_button(screen: Control, parent: Node, item: ItemInstance, index: int) -> void:
var modifier: ItemStatModifier = item.additional_modifiers[index]
var affix := SERVICE.affix_for(item,index)
var maximum := SERVICE.maximum(item,index)
var ready := SERVICE.can_maximize(item,index)
var active: bool = screen._engineer_addon == index
var button := Button.new()
button.name = "EngineerAddon_%d" % index
button.custom_minimum_size.y = 46
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.tooltip_text = modifier.get_display_text()+"\n"+("Kliknij, aby wybrać addon." if ready else "Addon ma już maksimum." if affix != null else "Ten addon nie ma zakresu maksymalizacji.")
_skin(button,active)
parent.add_child(button)
var margin := MarginContainer.new()
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
for side in ["left","right"]: margin.add_theme_constant_override("margin_"+side,10)
for side in ["top","bottom"]: margin.add_theme_constant_override("margin_"+side,7)
margin.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(margin)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation",10)
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
margin.add_child(row)
var icon := MENU.icon(row,_stat_icon(modifier.stat_key),27)
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
icon.modulate = CYAN if active else Color("b4c8d3")
var detail := VBoxContainer.new()
detail.size_flags_horizontal = Control.SIZE_EXPAND_FILL
detail.add_theme_constant_override("separation",4)
detail.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.add_child(detail)
var top := HBoxContainer.new()
top.add_theme_constant_override("separation",6)
top.mouse_filter = Control.MOUSE_FILTER_IGNORE
detail.add_child(top)
var title := _text(top,affix.display_name if affix != null else ItemInstance.stat_name(modifier.stat_key),CYAN if active else MENU.TEXT)
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
title.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
title.mouse_filter = Control.MOUSE_FILTER_IGNORE
var multiplier := ItemInstance.ENERGY_REGENERATION_BONUS_MULTIPLIER if modifier.stat_key == "energy_regeneration" else 1.0
var unit := affix.unit if affix != null else ""
var numbers := "%s → %s %s" % [_number(modifier.value*multiplier),_number(maximum*multiplier),unit] if ready else "✓ MAX" if affix != null else "—"
var value := _text(top,numbers,MENU.GOLD if ready else Color("86cda7"))
value.mouse_filter = Control.MOUSE_FILTER_IGNORE
var bar := ProgressBar.new()
bar.custom_minimum_size.y = 4
bar.show_percentage = false
bar.value = clampf(modifier.value/maxf(0.00001,maximum)*100,0,100) if affix != null else 0.0
bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
bar.add_theme_stylebox_override("background",_box(Color("09121a"),Color.TRANSPARENT,0))
bar.add_theme_stylebox_override("fill",_box(CYAN if active else Color("547d85"),Color.TRANSPARENT,0))
detail.add_child(bar)
button.pressed.connect(func():
screen._engineer_addon = index
screen._engineer_notice = ""
refresh(screen))
static func _text(parent: Node, value: String, color: Color) -> Label:
var label := MENU.label(parent,value,13,color)
label.autowrap_mode = TextServer.AUTOWRAP_OFF
return label
static func _number(value: float) -> String:
return str(roundi(value)) if is_equal_approx(value,roundf(value)) else "%.2f" % value
static func _box(background: Color, border: Color, inset: int) -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.bg_color = background
box.border_color = border
box.set_border_width_all(1)
box.set_corner_radius_all(5)
box.set_content_margin_all(inset)
return box
static func _skin(button: Button, selected: bool) -> void:
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
for state in ["normal","hover","pressed","focus"]:
var background := Color("1c3648") if selected else Color("15232f")
if state in ["hover","pressed"]: background = background.lightened(0.12)
button.add_theme_stylebox_override(state,_box(background,CYAN if selected or state == "focus" else Color("3d5262"),5))
static func _stat_icon(key: String) -> Texture2D:
var path := "computer.png"
if "shield" in key or "absorption" in key: path = "shield.png"
elif "damage" in key or "critical" in key: path = "plasma.png"
elif "speed" in key or "cooldown" in key: path = "engine.png"
elif "hull" in key: path = "armor.png"
elif "energy" in key: path = "reactor.png"
elif "credits" in key: path = "resources/credits.svg"
return load("res://assets/icons/"+path) as Texture2D

View File

@ -74,5 +74,5 @@ func _refresh_wardrobe() -> void:
var signature := str(captain.gear_items)+str(captain.gear_slots) var signature := str(captain.gear_items)+str(captain.gear_slots)
if signature == _gear_signature: return if signature == _gear_signature: return
_gear_signature = signature _gear_signature = signature
_protection.text = "Dopasuj poziom sprzętu do planety." _protection.text = "Sprzęt planetarny · zastosowanie wstrzymane."
_protection.tooltip_text = "Dopasuj poziom wyposażenia do planety, aby odzyskać więcej łupu i ograniczyć uszkodzenia lądownika." _protection.tooltip_text = "Wyposażenie zachowano na przyszłość. Lądowanie i eksploracja powierzchni są obecnie wyłączone."

View File

@ -48,7 +48,7 @@ func refresh() -> void:
tooltip_text = "Odłóż wyposażenie kapitana" if return_slot else "%s\n%s\nPrzeciągnij przedmiot do pasującego slotu. Dwuklik zakłada lub zdejmuje." % [GEAR.NAMES.get(kind, kind), "Poziom %d" % int(gear.level) if not gear.is_empty() else "Pusty slot"] tooltip_text = "Odłóż wyposażenie kapitana" if return_slot else "%s\n%s\nPrzeciągnij przedmiot do pasującego slotu. Dwuklik zakłada lub zdejmuje." % [GEAR.NAMES.get(kind, kind), "Poziom %d" % int(gear.level) if not gear.is_empty() else "Pusty slot"]
if not return_slot and not gear.is_empty(): if not return_slot and not gear.is_empty():
var uses := {"breathing":"Pomaga podczas eksploracji jaskiń i miejsc z trudną atmosferą.","suit":"Pomaga przy wydobyciu i eksploracji niestabilnych ruin.","sidearm":"Pomaga przeszukiwać magazyny i osady opanowane przez faunę."} var uses := {"breathing":"Pomaga podczas eksploracji jaskiń i miejsc z trudną atmosferą.","suit":"Pomaga przy wydobyciu i eksploracji niestabilnych ruin.","sidearm":"Pomaga przeszukiwać magazyny i osady opanowane przez faunę."}
tooltip_text += "\n"+str(uses.get(kind,""))+"\n"+"Dopasuj poziom wyposażenia do poziomu planety." tooltip_text += "\n"+str(uses.get(kind,""))+"\nZastosowanie wstrzymane: lądowanie jest obecnie wyłączone."
_style() _style()
func _style() -> void: func _style() -> void:

View File

@ -8,7 +8,7 @@ const PAGES := [
["Walka i przetrwanie", "03", "[font_size=16][color=#f1bd79]Wróć z wyprawy[/color][/font_size]\n\n[color=#91dbea][b]Tarcze, kadłub i energia[/b][/color]\nTarcze pochłaniają obrażenia przed kadłubem. Utrata całego kadłuba oznacza zniszczenie statku. Energia zasila część broni oraz dopalanie; odnawia się z czasem, szybciej przy gwieździe.\n\n[color=#91dbea][b]Dobieraj broń do zapasów[/b][/color]\nBroń energetyczna zużywa energię. Działko maszynowe potrzebuje zwykłej amunicji, gamma — napromieniowanej, a wyrzutnie — rakiet. Puste zapasy oznaczają brak strzału. Absorpcja ogranicza obrażenia danego typu.\n\n[color=#91dbea][b]Korzystaj z rozszerzeń[/b][/color]\nMiny atakują obszarowo, flary odciągają uwagę, kamuflaż pomaga zerwać kontakt, teleport przenosi w bezpieczne miejsce, a drony wspierają ostrzał. Doładowanie tarcz działa przy gwieździe. Sprawdzaj koszt i cooldown w opisie.\n\n[color=#f1bd79][b]Uważaj na uciekających przeciwników[/b][/color]\nWróg z kadłubem poniżej 10% ucieka wolniej, ale może zostawić minę. Nie leć bezmyślnie prosto za nim."], ["Walka i przetrwanie", "03", "[font_size=16][color=#f1bd79]Wróć z wyprawy[/color][/font_size]\n\n[color=#91dbea][b]Tarcze, kadłub i energia[/b][/color]\nTarcze pochłaniają obrażenia przed kadłubem. Utrata całego kadłuba oznacza zniszczenie statku. Energia zasila część broni oraz dopalanie; odnawia się z czasem, szybciej przy gwieździe.\n\n[color=#91dbea][b]Dobieraj broń do zapasów[/b][/color]\nBroń energetyczna zużywa energię. Działko maszynowe potrzebuje zwykłej amunicji, gamma — napromieniowanej, a wyrzutnie — rakiet. Puste zapasy oznaczają brak strzału. Absorpcja ogranicza obrażenia danego typu.\n\n[color=#91dbea][b]Korzystaj z rozszerzeń[/b][/color]\nMiny atakują obszarowo, flary odciągają uwagę, kamuflaż pomaga zerwać kontakt, teleport przenosi w bezpieczne miejsce, a drony wspierają ostrzał. Doładowanie tarcz działa przy gwieździe. Sprawdzaj koszt i cooldown w opisie.\n\n[color=#f1bd79][b]Uważaj na uciekających przeciwników[/b][/color]\nWróg z kadłubem poniżej 10% ucieka wolniej, ale może zostawić minę. Nie leć bezmyślnie prosto za nim."],
["Łup i wytwarzanie", "04", "[font_size=16][color=#f1bd79]Zbieraj. Porównuj. Twórz.[/color][/font_size]\n\n[color=#91dbea][b]Łup zbierasz przez podlecenie[/b][/color]\nPrzedmioty w pobliżu są przyciągane do statku. Magazyn mieści 20 przedmiotów; przy pełnym magazynie zwolnij miejsce, żeby zebrać kolejny.\n\n[color=#91dbea][b]Asteroidy zostawiają fizyczne odłamki[/b][/color]\nStrzelaj do asteroid. Po zniszczeniu rozpadają się bazowo na 5–7 poszarpanych fragmentów. Każdy to jedna jednostka surowca — musisz go zebrać. Umiejętność górnika zwiększa uzysk.\n\n[color=#91dbea][b]Przeszukuj wraki[/b][/color]\nPodleć do wraku i przytrzymaj E, aby go przeszukać. Wraki dają surowce (65%), amunicję lub rakiety (łącznie 20%) albo moduł (15%).\n\n[color=#91dbea][b]Warsztat fregaty — produkcja[/b][/color]\nZadokuj na fregacie i wybierz Warsztat. Tam wytworzysz amunicję, rakiety, części, powerbanki i wyposażenie kapitana. Koszt sprawdzisz w podpowiedzi przycisku produkcji.\n\n[color=#91dbea][b]Odzyskuj części[/b][/color]\nNajedź na zbędny przedmiot w magazynie i użyj przycisku rozłożenia. Części przydadzą się do napraw."], ["Łup i wytwarzanie", "04", "[font_size=16][color=#f1bd79]Zbieraj. Porównuj. Twórz.[/color][/font_size]\n\n[color=#91dbea][b]Łup zbierasz przez podlecenie[/b][/color]\nPrzedmioty w pobliżu są przyciągane do statku. Magazyn mieści 20 przedmiotów; przy pełnym magazynie zwolnij miejsce, żeby zebrać kolejny.\n\n[color=#91dbea][b]Asteroidy zostawiają fizyczne odłamki[/b][/color]\nStrzelaj do asteroid. Po zniszczeniu rozpadają się bazowo na 5–7 poszarpanych fragmentów. Każdy to jedna jednostka surowca — musisz go zebrać. Umiejętność górnika zwiększa uzysk.\n\n[color=#91dbea][b]Przeszukuj wraki[/b][/color]\nPodleć do wraku i przytrzymaj E, aby go przeszukać. Wraki dają surowce (65%), amunicję lub rakiety (łącznie 20%) albo moduł (15%).\n\n[color=#91dbea][b]Warsztat fregaty — produkcja[/b][/color]\nZadokuj na fregacie i wybierz Warsztat. Tam wytworzysz amunicję, rakiety, części, powerbanki i wyposażenie kapitana. Koszt sprawdzisz w podpowiedzi przycisku produkcji.\n\n[color=#91dbea][b]Odzyskuj części[/b][/color]\nNajedź na zbędny przedmiot w magazynie i użyj przycisku rozłożenia. Części przydadzą się do napraw."],
["Statek i kapitan", "05", "[font_size=16][color=#f1bd79]Buduj własny styl gry[/color][/font_size]\n\n[color=#91dbea][b]Wyposażenie[/b][/color]\nKliknij panel statku w prawym górnym rogu i otwórz magazyn. Przeciągaj przedmioty do pasujących slotów: maksymalnie 3 bronie, 5 modułów i 5 rozszerzeń. Na początku dostępny jest 1 slot broni i 1 rozszerzenia. Pierwszy dodatkowy slot broni kosztuje 100 tytanu, drugi 500. Rozszerzenia kosztują po 40 żelaza, aluminium i tytanu, a każdy kolejny slot podwaja ten koszt (80, 160, 320). Ładownia zaczyna z 5 miejscami; każde kolejne miejsce kosztuje 100 żelaza i 50 aluminium. Sloty odblokowujesz w hangarze fregaty. Po zbudowaniu składu modułów zakładka Magazyn na frachtowcu pozwala odkładać i odbierać wyposażenie. Aktywne rozszerzenia uruchamiasz klawiszami 1–5; pasywne (oznaczone P) działają automatycznie w tych samych slotach. Najedź kursorem, żeby sprawdzić parametry i porównanie wyposażenia.\n\n[color=#91dbea][b]Nie każdy duplikat jest taki sam[/b][/color]\nBroń i moduły mogą mieć różne parametry oraz addony. Aktywne rozszerzenia losują cooldown od 30 do 5 pełnych sekund. Pasywne mają losowaną siłę efektu: Recykler odnawia tarcze i energię po zebraniu odłamka, Wirus przejmuje uciekających wrogów, a Pancerz z tytanu ogranicza obrażenia przy niskim kadłubie. Krótsze odnowienia są rzadkie — przed rozłożeniem porównaj egzemplarze. Rozszerzenia tego samego typu dzielą aktywne odnowienie.\n\n[color=#91dbea][b]Umiejętności kapitana[/b][/color]\nZa pokonanych wrogów zdobywasz doświadczenie i kredyty. Trudniejsze układy dają więcej bazowego XP. Jeśli kapitan przewyższa poziom układu o ponad 2 poziomy, XP za przeciwników i ruiny maleje; przy przewadze 8 poziomów spada do zera. Kredyty i surowce nadal zbierasz. Po awansie wybierasz umiejętność. Pasywne działają stale, aktywne przypisujesz do F1–F5.\n\n[color=#f1bd79][b]Okno umiejętności nie pauzuje gry.[/b][/color]\nOtwieraj je w bezpiecznym miejscu. Przed kolejnym układem uzupełnij zapasy i sprawdź wyposażenie."] ["Statek i kapitan", "05", "[font_size=16][color=#f1bd79]Buduj własny styl gry[/color][/font_size]\n\n[color=#91dbea][b]Wyposażenie[/b][/color]\nKliknij panel statku w prawym górnym rogu i otwórz magazyn. Przeciągaj przedmioty do pasujących slotów: maksymalnie 3 bronie, 5 modułów i 5 rozszerzeń. Na początku dostępny jest 1 slot broni i 1 rozszerzenia. Pierwszy dodatkowy slot broni kosztuje 100 tytanu, drugi 500. Rozszerzenia kosztują po 40 żelaza, aluminium i tytanu, a każdy kolejny slot podwaja ten koszt (80, 160, 320). Ładownia zaczyna z 5 miejscami; każde kolejne miejsce kosztuje 100 żelaza i 50 aluminium. Sloty odblokowujesz w hangarze fregaty. Po zbudowaniu składu modułów zakładka Magazyn na frachtowcu pozwala odkładać i odbierać wyposażenie. Aktywne rozszerzenia uruchamiasz klawiszami 1–5; pasywne (oznaczone P) działają automatycznie w tych samych slotach. Najedź kursorem, żeby sprawdzić parametry i porównanie wyposażenia.\n\n[color=#91dbea][b]Nie każdy duplikat jest taki sam[/b][/color]\nBroń i moduły mogą mieć różne parametry oraz addony. Aktywne rozszerzenia losują cooldown od 30 do 5 pełnych sekund. Pasywne mają losowaną siłę efektu: Recykler odnawia tarcze i energię po zebraniu odłamka, Wirus przejmuje uciekających wrogów, a Pancerz z tytanu ogranicza obrażenia przy niskim kadłubie. Krótsze odnowienia są rzadkie — przed rozłożeniem porównaj egzemplarze. Rozszerzenia tego samego typu dzielą aktywne odnowienie.\n\n[color=#91dbea][b]Umiejętności kapitana[/b][/color]\nZa pokonanych wrogów zdobywasz doświadczenie i kredyty. Trudniejsze układy dają więcej bazowego XP. Jeśli kapitan przewyższa poziom układu o ponad 2 poziomy, XP za przeciwników i ruiny maleje; przy przewadze 8 poziomów spada do zera. Kredyty i surowce nadal zbierasz. Po awansie wybierasz umiejętność. Pasywne działają stale, aktywne przypisujesz do F1–F5.\n\n[color=#f1bd79][b]Okno umiejętności nie pauzuje gry.[/b][/color]\nOtwieraj je w bezpiecznym miejscu. Przed kolejnym układem uzupełnij zapasy i sprawdź wyposażenie."]
,["Planety i frachtowiec", "06", "[b]Wyprawy na planety[/b]\n\nPierwsze planety są łagodne. W kolejnych układach eksploracja jest trudniejsza, ale znaleziska są bogatsze. Dopasuj poziom wyposażenia kapitana do poziomu planety: aparat oddechowy pomaga w jaskiniach, skafander przy wydobyciu i ruinach, a broń ręczna w magazynach i osadach.\n\nPodleć do planety EARTHLIKE. F pokazuje informacje, a L rozpoczyna lądowanie. Lądownik musi mieć 100 HP przed startem; lądowanie uszkadza kadłub. Podczas walki nie można lądować.\n\nNa powierzchni: W — ciąg, S — hamowanie i wstecz, A/D — obrót, Q/R — ruch boczny, TAB — dopalanie. Przy POI naciśnij E. Najpierw zobaczysz przewidywaną nagrodę i uszkodzenia. Samo otwarcie okna niczego nie zużywa. Potwierdź eksplorację przyciskiem lub drugim naciśnięciem E.\n\nNiektóre miejsca oferują dodatkowe znaleziska. Przeszukanie głębiej daje więcej łupu, ale mocniej uszkadza lądownik. Niski kadłub może oznaczać utratę całej zdobyczy tej wyprawy. L pozwala wrócić z dotychczasowym łupem. Spotkania oznaczone pytajnikiem również otwierasz klawiszem E.\n\n[b]Frachtowiec i postęp[/b]\nPrzy gwieździe centralnej zadokuj klawiszem L. Hangar naprawia lądownik za części. Warsztat produkuje zaopatrzenie i kolejne poziomy wyposażenia kapitana. Laboratorium prowadzi badania.\n\nEsc otwiera pauzę, nazwane zapisy i opcje. Wczytanie zachowuje postęp oraz wyposażenie i zaczyna bieżący poziom od początku."] ,["Kosmos i frachtowiec", "06", "[b]Dalekie wyprawy[/b]\n\nLądowanie na planetach jest wyłączone. Bossowie w kosmosie zostawiają mapy. Zbierz mapę i zadokuj do fregaty klawiszem L. W centrum dowodzenia wybierz daleką wyprawę. Lot zużywa mapę i nie wymaga paliwa.\n\nNa wyprawach zniszcz bazy zaopatrzeniowe wroga i zbierz ładunki: krzem, miedź, nikiel, wolfram i rtęć. Każdy statek wroga ma 15% szansy na próbkę do badań obcych.\n\n[b]Modyfikacja wyposażenia[/b]\nKliknij orb w ładowni, potem przedmiot. PPM lub Esc anuluje wybór. Inżynier chronalny w warsztacie fregaty maksymalizuje wybrany addon za kryształy czasu.\n\n[b]Fregata[/b]\nHangar rozbudowuje statek, warsztat produkuje zaopatrzenie, laboratoria prowadzą badania. Załoga wydobywcza i naprawa lądownika są wyłączone.\n\nEsc otwiera pauzę, zapisy i opcje."]
, ["Autorzy dźwięków", "07", "[font_size=16][color=#f1bd79]Dźwięki otoczenia[/color][/font_size]\n\n[b]Blender Foundation — Ambient Bird, Cricket and Frog (Yo Frankie!)[/b]\nOdgłosy świerszcza i żaby, udostępnione przez Lamoot:\nhttps://opengameart.org/content/ambient-bird-cricket-and-frog\nLicencja Creative Commons Attribution 3.0:\nhttps://creativecommons.org/licenses/by/3.0/\nZmiany: filtrowanie, płynne początki i zakończenia, zmiana wysokości żaby oraz konwersja do stereo 48 kHz. Autorzy nie są powiązani z tą grą.\n\n[b]Nagrania na licencji CC0[/b]\nThimras — Park ambiences (wiatr).\nIwan „qubodup” Gabovitch — 20 Rustles of dry leaves.\nisaiah658 — Ambient Bird Sounds.\nRudmer_Rotteveel — Wood Creak Single V7.\n\n[b]Wnętrze fregaty — CC0[/b]\nLEGIT Audio — The Shop (bezpłatne próbki OpenGameArt).\nbart — 68 Workshop Sounds i Steam release sounds.\nKenney — Sci-Fi Sounds.\n\nSzczegółowe źródła i zakres obróbki znajdują się w plikach CREDITS.md i manifest.json przy zasobach audio."] , ["Autorzy dźwięków", "07", "[font_size=16][color=#f1bd79]Dźwięki otoczenia[/color][/font_size]\n\n[b]Blender Foundation — Ambient Bird, Cricket and Frog (Yo Frankie!)[/b]\nOdgłosy świerszcza i żaby, udostępnione przez Lamoot:\nhttps://opengameart.org/content/ambient-bird-cricket-and-frog\nLicencja Creative Commons Attribution 3.0:\nhttps://creativecommons.org/licenses/by/3.0/\nZmiany: filtrowanie, płynne początki i zakończenia, zmiana wysokości żaby oraz konwersja do stereo 48 kHz. Autorzy nie są powiązani z tą grą.\n\n[b]Nagrania na licencji CC0[/b]\nThimras — Park ambiences (wiatr).\nIwan „qubodup” Gabovitch — 20 Rustles of dry leaves.\nisaiah658 — Ambient Bird Sounds.\nRudmer_Rotteveel — Wood Creak Single V7.\n\n[b]Wnętrze fregaty — CC0[/b]\nLEGIT Audio — The Shop (bezpłatne próbki OpenGameArt).\nbart — 68 Workshop Sounds i Steam release sounds.\nKenney — Sci-Fi Sounds.\n\nSzczegółowe źródła i zakres obróbki znajdują się w plikach CREDITS.md i manifest.json przy zasobach audio."]
] ]
var _panel: PanelContainer var _panel: PanelContainer

View File

@ -3,7 +3,6 @@ const HEIGHT := 16.0
const ENTRIES := [ const ENTRIES := [
["credits","Kredyty","svg",Color("efd48e")], ["credits","Kredyty","svg",Color("efd48e")],
["parts","Części","png",Color("dfded2")], ["parts","Części","png",Color("dfded2")],
["deuter","Deuter","svg",Color("9cdeef")],
["time_crystals","Kryształy czasu","png",Color("d4b6ff")], ["time_crystals","Kryształy czasu","png",Color("d4b6ff")],
["iron","Żelazo","png",Color("d3dce0")], ["iron","Żelazo","png",Color("d3dce0")],
["titanium","Tytan","png",Color("d3dce0")], ["titanium","Tytan","png",Color("d3dce0")],

View File

@ -76,6 +76,8 @@ func _draw() -> void:
if bool(target.get("_claimed")): if bool(target.get("_claimed")):
continue continue
match str(target.get("kind")): match str(target.get("kind")):
"enemy_base":
color = Color("ff8060")
"asteroid": "asteroid":
color = Color("a8adb5") color = Color("a8adb5")
"wreck": "wreck":

View File

@ -37,7 +37,7 @@ func _refresh_title() -> void:
var stock := {} var stock := {}
for key in preload("res://scripts/planets/planet_profiles.gd").MATERIALS: for key in preload("res://scripts/planets/planet_profiles.gd").MATERIALS:
stock[key] = GameState.captain.minerals.get(key,0) stock[key] = GameState.captain.minerals.get(key,0)
_planet_resources.text = "Z PLANET: " + preload("res://scripts/planets/expedition_ui.gd").material_text(stock) _planet_resources.text = "SUROWCE Z BAZ: " + preload("res://scripts/planets/expedition_ui.gd").material_text(stock)
for key in ["iron", "titanium", "aluminium", "uranium", "sulfur"]: for key in ["iron", "titanium", "aluminium", "uranium", "sulfur"]:
_resource_cards[key].set_amount(float(GameState.captain.minerals.get(key, 0))) _resource_cards[key].set_amount(float(GameState.captain.minerals.get(key, 0)))
_resource_cards["parts"].set_amount(GameState.captain.parts) _resource_cards["parts"].set_amount(GameState.captain.parts)

View File

@ -71,6 +71,6 @@ func run_checks() -> Dictionary:
if not ORBS.apply(progress,inventory,legacy,"advancement") or legacy.additional_modifiers[0].affix == null: failures.append("Legacy affix unresolved") if not ORBS.apply(progress,inventory,legacy,"advancement") or legacy.additional_modifiers[0].affix == null: failures.append("Legacy affix unresolved")
var drops: Array[String] = [] var drops: Array[String] = []
for seed_value in 3: drops.append(preload("res://scripts/game/distant_loot.gd").roll(125,true,seed_value,1.0,0.0)[1].orb) for seed_value in 3: drops.append(preload("res://scripts/game/distant_loot.gd").roll(125,true,seed_value,1.0,0.0)[1].orb)
if not drops.has("advancement") or not drops.has("piercing") or not drops.has("booster"): failures.append("Boss orb pool") if not drops.has("advancement") or not drops.has("piercing") or not drops.has("atomic_engine") or drops.has("booster"): failures.append("Boss orb pool")
ship.free() ship.free()
return {"failures":failures,"definitions_checked":checked,"passed":failures.is_empty()} return {"failures":failures,"definitions_checked":checked,"passed":failures.is_empty()}

View File

@ -3,7 +3,7 @@ extends Node2D
const SKILLS := preload("res://scripts/captain/captain_skills.gd") const SKILLS := preload("res://scripts/captain/captain_skills.gd")
const LAB := preload("res://scripts/planets/laboratory.gd") const LAB := preload("res://scripts/planets/laboratory.gd")
const RECIPES := preload("res://scripts/items/crafting_recipes.gd") const RECIPES := preload("res://scripts/items/crafting_recipes.gd")
const NEW_SKILLS := ["scientist", "gunsmith", "bounty_hunter", "missile_specialist", "emergency_procedure"] const NEW_SKILLS := ["gunsmith", "bounty_hunter", "missile_specialist", "emergency_procedure"]
func _ready() -> void: func _ready() -> void:
GameState.start_new_run() GameState.start_new_run()
@ -17,7 +17,7 @@ func _ready() -> void:
var missile := ItemInstance.new() var missile := ItemInstance.new()
missile.definition = preload("res://resources/items/missile_launcher.tres") missile.definition = preload("res://resources/items/missile_launcher.tres")
var baseline_money := captain.reward_enemy(1,0,1.0,1.0,false).y var baseline_money := captain.reward_enemy(1,0,1.0,1.0,false).y
assert(SKILLS.IDS.size() == 16) assert(SKILLS.IDS.size() == 15)
for id in NEW_SKILLS: for id in NEW_SKILLS:
assert(SKILLS.icon(id) != null) assert(SKILLS.icon(id) != null)
for rank in range(1,4): for rank in range(1,4):
@ -36,11 +36,7 @@ func _ready() -> void:
progress.laboratory_unlocks.clear() progress.laboratory_unlocks.clear()
for key in LAB.COSTS.machine_gun: captain.minerals[key] = 100 for key in LAB.COSTS.machine_gun: captain.minerals[key] = 100
assert(LAB.start(captain,"machine_gun")) assert(LAB.start(captain,"machine_gun"))
assert(is_equal_approx(progress.laboratory_remaining,[540.0,480.0,420.0][rank-1])) assert(progress.laboratory_remaining == 0)
assert(progress.laboratory_duration == progress.laboratory_remaining)
LAB.advance(progress,progress.laboratory_remaining-1.0)
assert(not progress.laboratory_unlocks.has("machine_gun"))
LAB.advance(progress,1.0)
assert(progress.laboratory_unlocks.has("machine_gun")) assert(progress.laboratory_unlocks.has("machine_gun"))
for id in ["ammunition","irradiated_ammunition"]: for id in ["ammunition","irradiated_ammunition"]:
for key in RECIPES.DATA[id].cost: captain.minerals[key] = 100 for key in RECIPES.DATA[id].cost: captain.minerals[key] = 100
@ -86,7 +82,7 @@ func _ready() -> void:
stub.free() stub.free()
ship.free() ship.free()
inventory.free() inventory.free()
print("NEW_CAPTAIN_SKILLS_TEST: PASS (16 skills, icons, training, all ranks, research duration, ammo output/cost, bounty, missile cooldown, repair conditions/cost/cooldown/cap)") print("NEW_CAPTAIN_SKILLS_TEST: PASS (15 skills, icons, training, all ranks, instant discovery, ammo output/cost, bounty, missile cooldown, repair conditions/cost/cooldown/cap)")
get_tree().quit() get_tree().quit()
class RepairShip extends CharacterBody2D: class RepairShip extends CharacterBody2D:

View File

@ -7,7 +7,7 @@ const PROJECTILE := preload("res://scripts/game/weapon_projectile.gd")
func _ready() -> void: func _ready() -> void:
GameState.start_new_run() GameState.start_new_run()
var captain := GameState.captain var captain := GameState.captain
assert(SKILLS.IDS.size() == 16) assert(SKILLS.IDS.size() == 15)
for id in ["scrap_collector","drone_commander"]: for id in ["scrap_collector","drone_commander"]:
assert(SKILLS.icon(id) != null) assert(SKILLS.icon(id) != null)
captain.learning_points = 1 captain.learning_points = 1

View File

@ -30,7 +30,7 @@ func _run() -> void:
assert(GameState.depart_distant("defeat_test")) assert(GameState.depart_distant("defeat_test"))
assert(not GameState.hull_bastion_used,"New expedition receives its own Last Bastion use") assert(not GameState.hull_bastion_used,"New expedition receives its own Last Bastion use")
GameState.hull_bastion_used = true GameState.hull_bastion_used = true
assert(not progress.distant_maps.has("defeat_test") and progress.deuter==5000) assert(not progress.distant_maps.has("defeat_test") and progress.deuter==130000)
get_tree().change_scene_to_file("res://scenes/game/game.tscn") get_tree().change_scene_to_file("res://scenes/game/game.tscn")
await frames() await frames()
var game := get_tree().current_scene var game := get_tree().current_scene
@ -58,7 +58,7 @@ func _run() -> void:
assert(GameState.active_distant_map.is_empty() and GameState.distant_home.is_empty()) assert(GameState.active_distant_map.is_empty() and GameState.distant_home.is_empty())
assert(GameState.hull_bastion_used,"Return restores the home's spent Last Bastion use") assert(GameState.hull_bastion_used,"Return restores the home's spent Last Bastion use")
assert(not progress.distant_maps.has("defeat_test") and progress.distant_maps.has("found_on_expedition")) assert(not progress.distant_maps.has("defeat_test") and progress.distant_maps.has("found_on_expedition"))
assert(progress.deuter==5000,"Defeat cannot refund flight fuel") assert(progress.deuter==130000,"Defeat preserves legacy resource balance")
assert(not GameState.depart_distant("defeat_test"),"Consumed expedition cannot be retried") assert(not GameState.depart_distant("defeat_test"),"Consumed expedition cannot be retried")
assert(GameState.explored_pois.has("777:kept"),"Failed expedition must not reset home exploration") assert(GameState.explored_pois.has("777:kept"),"Failed expedition must not reset home exploration")
assert(game.has_node("Freighter") and not game.has_node("DistantSector")) assert(game.has_node("Freighter") and not game.has_node("DistantSector"))
@ -80,5 +80,5 @@ func _run() -> void:
assert(GameState.asteroid_retry_seeds.has(777) and not GameState.explored_pois.has("777:regular_retry")) assert(GameState.asteroid_retry_seeds.has(777) and not GameState.explored_pois.has("777:regular_retry"))
assert(not game.player.is_docked and not get_tree().paused,"Normal-system defeat still retries the current system") assert(not game.player.is_docked and not get_tree().paused,"Normal-system defeat still retries the current system")
assert(GameState.hull_bastion_used,"Retry cannot renew Last Bastion") assert(GameState.hull_bastion_used,"Retry cannot renew Last Bastion")
assert(not progress.distant_maps.has("defeat_test") and progress.deuter==5000) assert(not progress.distant_maps.has("defeat_test") and progress.deuter==130000)
print("DISTANT_DEFEAT_RETURN: PASS (original system and seed, consumed map/fuel, preserved inventory and found maps, freighter docking, duplicate retry guarded)") print("DISTANT_DEFEAT_RETURN: PASS (original system and seed, consumed map, no fuel cost, preserved inventory and found maps, freighter docking, duplicate retry guarded)")

View File

@ -16,7 +16,7 @@ func run_checks() -> Dictionary:
if int(map.level) < source+1 or int(map.level) > source+5: failures.append("Wrong destination range") if int(map.level) < source+1 or int(map.level) > source+5: failures.append("Wrong destination range")
if map != DATA.offer(candidate,source): failures.append("Offer rerolled") if map != DATA.offer(candidate,source): failures.append("Offer rerolled")
if DATA.map_cost(map) != int(map.level)*1000: failures.append("Wrong credits") if DATA.map_cost(map) != int(map.level)*1000: failures.append("Wrong credits")
if DATA.fuel_cost(map) != int(map.level)*1000: failures.append("Wrong fuel") if DATA.fuel_cost(map) != 0: failures.append("Wrong fuel")
var captain = preload("res://scripts/captain/captain_progress.gd").new() var captain = preload("res://scripts/captain/captain_progress.gd").new()
var progress: Resource = captain.expedition var progress: Resource = captain.expedition
var map := DATA.visit_offer(progress,profile,120) var map := DATA.visit_offer(progress,profile,120)

View File

@ -43,10 +43,7 @@ func _run() -> void:
GameState.system_seed = 777 GameState.system_seed = 777
GameState.remember_system() GameState.remember_system()
GameState.captain.expedition.distant_maps["integration"] = {"id":"integration","level":125,"seed":123456,"name":"Sektor testowy","source_planet":"Test","source_level":120} GameState.captain.expedition.distant_maps["integration"] = {"id":"integration","level":125,"seed":123456,"name":"Sektor testowy","source_planet":"Test","source_level":120}
GameState.captain.expedition.deuter = 124999 GameState.captain.expedition.deuter = 0
check(not GameState.depart_distant("integration"),"No flight without fuel")
check(GameState.system_level==7 and GameState.captain.expedition.deuter==124999,"Failed departure preserved state")
GameState.captain.expedition.deuter = 125000
get_tree().change_scene_to_file("res://scenes/game/game.tscn") get_tree().change_scene_to_file("res://scenes/game/game.tscn")
await frames() await frames()
var game := get_tree().current_scene var game := get_tree().current_scene
@ -59,20 +56,21 @@ func _run() -> void:
engine.definition = preload("res://resources/items/engine_extension.tres") engine.definition = preload("res://resources/items/engine_extension.tres")
inventory.module_slots[2] = engine inventory.module_slots[2] = engine
var progress: Resource = GameState.captain.expedition var progress: Resource = GameState.captain.expedition
progress.orbs = {"piercing":2,"booster":1} progress.orbs = {"piercing":2,"atomic_engine":1}
check(not ORBS.apply(progress,inventory,engine,"piercing"),"Piercing category restriction") check(not ORBS.apply(progress,inventory,engine,"piercing"),"Piercing category restriction")
check(not ORBS.apply(progress,inventory,weapon,"booster"),"Booster category restriction") check(not ORBS.apply(progress,inventory,weapon,"atomic_engine"),"Booster category restriction")
check(ORBS.apply(progress,inventory,weapon,"piercing"),"Apply piercing") check(ORBS.apply(progress,inventory,weapon,"piercing"),"Apply piercing")
check(not ORBS.apply(progress,inventory,weapon,"piercing") and progress.orbs.piercing==1,"No stacking or extra cost") check(not ORBS.apply(progress,inventory,weapon,"piercing") and progress.orbs.piercing==1,"No stacking or extra cost")
check(ORBS.apply(progress,inventory,engine,"booster"),"Apply booster") check(ORBS.apply(progress,inventory,engine,"atomic_engine"),"Apply booster")
check(player.has_free_mini_boost(),"Equipped engine") var atomic_cost: float = player.get_mini_boost_energy_cost()
check(atomic_cost > 0,"Atomic engine still uses energy")
Input.action_press("ship_mini_boost") Input.action_press("ship_mini_boost")
player.energy = 0 player.energy = 0
player._update_mini_boost(1.0) player._update_mini_boost(1.0)
check(player._mini_boost_active and player.energy==0,"Free TAB with empty generator") check(not player._mini_boost_active and player.energy==0,"TAB blocked with empty generator")
Input.action_release("ship_mini_boost") Input.action_release("ship_mini_boost")
inventory.module_slots[2] = null inventory.module_slots[2] = null
check(not player.has_free_mini_boost(),"Unequipped engine no effect") check(is_equal_approx(player.get_mini_boost_energy_cost(),atomic_cost*2),"Unequipped engine no discount")
inventory.module_slots[2] = engine inventory.module_slots[2] = engine
check(game.travel_to_distant("integration"),"Flight requested") check(game.travel_to_distant("integration"),"Flight requested")
await frames(3) await frames(3)
@ -84,7 +82,7 @@ func _run() -> void:
await frames(8) await frames(8)
game = get_tree().current_scene game = get_tree().current_scene
freeze_game(game) freeze_game(game)
check(GameState.captain.expedition.deuter==0,"Exactly one fuel payment") check(GameState.captain.expedition.deuter==0,"Departure needs no fuel")
check(not GameState.captain.expedition.distant_maps.has("integration"),"Map consumed on departure") check(not GameState.captain.expedition.distant_maps.has("integration"),"Map consumed on departure")
check(GameState.system_level==125 and GameState.highest_system==7,"Isolated progression") check(GameState.system_level==125 and GameState.highest_system==7,"Isolated progression")
check(game.get_node_or_null("CelestialBodies")==null,"No celestial bodies") check(game.get_node_or_null("CelestialBodies")==null,"No celestial bodies")
@ -98,7 +96,7 @@ func _run() -> void:
inventory = player.get_node("ShipInventory") inventory = player.get_node("ShipInventory")
player.is_docked = true player.is_docked = true
check("piercing" in inventory.weapon_slots[0].special_orbs,"Orb persisted across travel") check("piercing" in inventory.weapon_slots[0].special_orbs,"Orb persisted across travel")
check(player.has_free_mini_boost(),"Engine orb persisted") check("atomic_engine" in inventory.module_slots[2].special_orbs,"Engine orb persisted")
var enemy: Node = get_tree().get_first_node_in_group("enemies") var enemy: Node = get_tree().get_first_node_in_group("enemies")
enemy.absorption = Vector3.ZERO enemy.absorption = Vector3.ZERO
enemy.shields = 100 enemy.shields = 100
@ -128,12 +126,12 @@ func _run() -> void:
projectile.free() projectile.free()
var payload := Node2D.new() var payload := Node2D.new()
payload.set_script(preload("res://scripts/items/expedition_pickup.gd")) payload.set_script(preload("res://scripts/items/expedition_pickup.gd"))
payload.payload = {"orb":"booster"} payload.payload = {"orb":"atomic_engine"}
payload.position = player.position payload.position = player.position
game.add_child(payload) game.add_child(payload)
var count: int = preload("res://scripts/items/special_orbs.gd").count(progress,inventory,"booster") var count: int = preload("res://scripts/items/special_orbs.gd").count(progress,inventory,"atomic_engine")
payload._physics_process(0) payload._physics_process(0)
check(preload("res://scripts/items/special_orbs.gd").count(progress,inventory,"booster")==count+1,"Collect orb into cargo") check(preload("res://scripts/items/special_orbs.gd").count(progress,inventory,"atomic_engine")==count+1,"Collect orb into cargo")
await frames() await frames()
for victim in get_tree().get_nodes_in_group("enemies"): victim.queue_free() for victim in get_tree().get_nodes_in_group("enemies"): victim.queue_free()
await frames() await frames()

View File

@ -84,8 +84,8 @@ func run(game: Node) -> Array[String]:
player._update_mini_boost(1.0) player._update_mini_boost(1.0)
Input.action_release("ship_mini_boost") Input.action_release("ship_mini_boost")
check(is_equal_approx(player.energy,50.0-normal_boost_cost*0.5), "Actual atomic boost consumes half energy") check(is_equal_approx(player.energy,50.0-normal_boost_cost*0.5), "Actual atomic boost consumes half energy")
check(ORBS.apply(progress,inventory,engine,"booster"), "Free booster stacks by precedence") check(not ORBS.apply(progress,inventory,engine,"booster"), "Retired orb rejected")
check(is_zero_approx(player.get_mini_boost_energy_cost()), "Free booster remains free") check(is_equal_approx(player.get_mini_boost_energy_cost(),normal_boost_cost*0.5), "Atomic boost still costs energy")
check(ORBS.apply(progress,inventory,shield,"polarization"), "Polarization applied") check(ORBS.apply(progress,inventory,shield,"polarization"), "Polarization applied")
player.is_docked = false player.is_docked = false
player.absorption = Vector3.ZERO player.absorption = Vector3.ZERO

View File

@ -0,0 +1,50 @@
extends Node2D
const EFFECT := preload("res://scripts/game/enemy_destruction_effect.gd")
func _ready() -> void:
run.call_deferred()
func run() -> void:
var source := Sprite2D.new()
source.texture = preload("res://assets/alien_ships/alien_01.png")
source.scale = Vector2.ONE*56.0/maxf(source.texture.get_width(),source.texture.get_height())
source.position = Vector2(100,80)
add_child(source)
for tier in 3:
var effect := Node2D.new()
effect.set_script(EFFECT)
effect.play_audio = false
effect.configure(source,Vector2(100,0),tier)
add_child(effect)
effect.set_process(false)
assert(effect.global_position.is_equal_approx(source.global_position))
assert(effect._fragments.size() >= 18)
assert(effect._bursts.size() == 3+tier)
effect._process(0.1)
assert(effect.global_position.x > source.global_position.x)
assert(effect._bursts[0].visual.visible)
effect._process(1.6)
for burst in effect._bursts: assert(not burst.visual.visible)
effect._process(1.0)
assert(effect.is_queued_for_deletion())
await get_tree().process_frame
# Actual enemy death must spawn the new effect and award the kill only once.
var enemy := preload("res://scenes/game/enemy_ship.tscn").instantiate()
add_child(enemy)
enemy.get_node("Sprite2D").texture = source.texture
enemy.take_damage(1e12)
enemy.take_damage(1e12)
assert(enemy.is_queued_for_deletion())
assert(get_tree().get_nodes_in_group("enemy_explosions").size() == 1)
await get_tree().process_frame
for index in 24:
var effect := Node2D.new()
effect.set_script(EFFECT)
effect.play_audio = false
effect.configure(source,Vector2.ZERO,0)
add_child(effect)
await get_tree().process_frame
assert(get_tree().get_nodes_in_group("enemy_explosions").size() <= EFFECT.MAX_EFFECTS)
for child in get_children(): child.queue_free()
for frame in 3: await get_tree().process_frame
print("ENEMY_DESTRUCTION_TEST: PASS (tiers, debris, drift, lifetime, enemy death, effect cap)")
get_tree().quit()

View File

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

View File

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/items/enemy_destruction_test.gd" id="1"]
[node name="EnemyDestructionTest" type="Node2D"]
script = ExtResource("1")

View File

@ -0,0 +1,39 @@
extends Node
const ORBS := preload("res://scripts/items/special_orbs.gd")
func _ready() -> void:
GameState.start_new_run()
assert(not ORBS.IDS.has("booster") and not ORBS.COMMON_IDS.has("booster"))
assert(ORBS.create_item("booster") == null and ORBS.icon("booster") == null)
var player := preload("res://scenes/game/player_ship.tscn").instantiate()
add_child(player)
player.set_physics_process(false)
player.get_node("WeaponController").set_physics_process(false)
var inventory: ShipInventory = player.get_node("ShipInventory")
var engine := ItemInstance.new()
engine.definition = preload("res://resources/items/engine_extension.tres")
inventory.module_slots[2] = engine
var base_cost: float = player.get_mini_boost_energy_cost()
engine.special_orbs.assign(["booster","atomic_engine"])
assert(is_equal_approx(player.get_mini_boost_energy_cost(),base_cost*0.5))
var old := ItemInstance.new()
old.definition = ItemDefinition.new()
old.definition.category = ItemDefinition.Category.CONSUMABLE
old.definition.orb_id = "booster"
inventory.cargo[0] = old
GameState.captain.expedition.orbs = {"booster":10}
GameState.captain.expedition.freighter_storage.append(old)
GameState.captain.expedition.delivered_modules.append(old)
inventory.migrate_orbs()
assert(inventory.cargo[0] == null)
assert(not engine.special_orbs.has("booster") and engine.special_orbs.has("atomic_engine"))
assert(not GameState.captain.expedition.orbs.has("booster"))
assert(GameState.captain.expedition.freighter_storage.is_empty())
assert(GameState.captain.expedition.delivered_modules.is_empty())
assert(not ORBS.apply(GameState.captain.expedition,inventory,engine,"booster"))
player.energy = 0
Input.action_press("ship_mini_boost")
player._update_mini_boost(0.1)
Input.action_release("ship_mini_boost")
assert(not player._mini_boost_active)
print("RETIRED_ORB_TEST: PASS (removed orb, legacy cleanup, atomic engine retained, no free boost)")
get_tree().quit()

View File

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

View File

@ -0,0 +1,4 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/items/retired_orb_test.gd" id="1"]
[node name="RetiredOrbTest" type="Node"]
script = ExtResource("1")

View File

@ -21,7 +21,7 @@ func _ready() -> void:
engine.special_orbs.append("atomic_engine") engine.special_orbs.append("atomic_engine")
assert(is_equal_approx(player.get_mini_boost_energy_cost(),cost*0.5)) assert(is_equal_approx(player.get_mini_boost_energy_cost(),cost*0.5))
engine.special_orbs.append("booster") engine.special_orbs.append("booster")
assert(is_zero_approx(player.get_mini_boost_energy_cost())) assert(is_equal_approx(player.get_mini_boost_energy_cost(),cost*0.5),"Retired booster has no effect")
var shield := GEN.generate_for_definition(preload("res://resources/items/reinforced_shield.tres"),120) var shield := GEN.generate_for_definition(preload("res://resources/items/reinforced_shield.tres"),120)
shield.special_orbs.append("polarization") shield.special_orbs.append("polarization")
inventory.module_slots[0] = shield inventory.module_slots[0] = shield

View File

@ -64,8 +64,6 @@ func _ready() -> void:
var engine: ItemInstance = inventory.module_slots[2] var engine: ItemInstance = inventory.module_slots[2]
engine.special_orbs.append("atomic_engine") engine.special_orbs.append("atomic_engine")
report["atomic_engine_cost_s"] = player.get_mini_boost_energy_cost() report["atomic_engine_cost_s"] = player.get_mini_boost_energy_cost()
engine.special_orbs.append("booster")
report["booster_cost_s"] = player.get_mini_boost_energy_cost()
var enemy := preload("res://scenes/game/enemy_ship.tscn").instantiate() var enemy := preload("res://scenes/game/enemy_ship.tscn").instantiate()
add_child(enemy) add_child(enemy)
enemy.set_physics_process(false) enemy.set_physics_process(false)

View File

@ -0,0 +1,50 @@
extends Node2D
const EFFECT := preload("res://scripts/game/enemy_destruction_effect.gd")
const SHIPS := preload("res://scripts/items/alien_ship_catalog.gd")
func _ready() -> void:
RenderingServer.set_default_clear_color(Color("070e1c"))
get_window().size = Vector2i(1280,900)
get_window().content_scale_size = Vector2i(1280,900)
var rng := RandomNumberGenerator.new()
rng.seed = 18
for index in 250:
var star := Polygon2D.new()
star.polygon = PackedVector2Array([Vector2(-0.6,0),Vector2(0,-0.6),Vector2(0.6,0),Vector2(0,0.6)])
star.position = Vector2(rng.randf()*1280,rng.randf()*900)
star.color = Color(0.65,0.8,1,rng.randf_range(0.1,0.5))
add_child(star)
label_at("ROZERWANIE KADŁUBA / EKSPLOZJE WROGÓW",Vector2(35,20),22)
for tier in 3:
label_at(["ZWYKŁY STATEK","ELITA","BOSS"][tier],Vector2(100+tier*410,75),16)
for row in 3:
var sprite := Sprite2D.new()
sprite.texture = SHIPS.TEXTURES[tier*4]
sprite.scale = Vector2.ONE * [56.0,68.0,112.0][tier] / maxf(sprite.texture.get_width(),sprite.texture.get_height())
sprite.position = Vector2(210+tier*410,215+row*245)
add_child(sprite)
var effect := Node2D.new()
effect.set_script(EFFECT)
effect.play_audio = false
effect.configure(sprite,Vector2.ZERO,tier)
add_child(effect)
effect.set_process(false)
effect._elapsed = [0.07,0.3,0.9][row]
effect._update_fireballs()
effect.queue_redraw()
sprite.queue_free()
label_at(["Błysk · 0,07 s","Fala i ogień · 0,30 s","Odłamki · 0,90 s"][row],Vector2(110+tier*410,320+row*245),13)
if "--capture" in OS.get_cmdline_user_args():
await get_tree().process_frame
await RenderingServer.frame_post_draw
get_viewport().get_texture().get_image().save_png("res://artifacts/enemy_explosion_preview.png")
print("EXPLOSION_PREVIEW_CAPTURED")
get_tree().quit()
func label_at(value: String, origin: Vector2, font_size: int) -> void:
var label := Label.new()
label.text = value
label.position = origin
label.add_theme_font_size_override("font_size",font_size)
label.modulate = Color("a8becf")
add_child(label)

View File

@ -0,0 +1 @@
uid://7gjagxfla1tf

View File

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/previews/enemy_explosion_preview.gd" id="1"]
[node name="EnemyExplosionPreview" type="Node2D"]
script = ExtResource("1")

View File

@ -15,7 +15,7 @@ func _ready() -> void:
for enemy in get_tree().get_nodes_in_group("enemies"): enemy.set_physics_process(false) for enemy in get_tree().get_nodes_in_group("enemies"): enemy.set_physics_process(false)
var ui: CanvasLayer = game.find_child("CaptainSkills",true,false) var ui: CanvasLayer = game.find_child("CaptainSkills",true,false)
captain.learning_points = 1 captain.learning_points = 1
captain.skill_offers.assign(["scientist","gunsmith","bounty_hunter"]) captain.skill_offers.assign(["missile_specialist","gunsmith","bounty_hunter"])
ui.open_management() ui.open_management()
await _snapshot("res://artifacts/new_captain_skills_economy.png") await _snapshot("res://artifacts/new_captain_skills_economy.png")
captain.skill_offers.assign(["missile_specialist","emergency_procedure"]) captain.skill_offers.assign(["missile_specialist","emergency_procedure"])

View File

@ -57,7 +57,7 @@ func run() -> void:
game.free() game.free()
check(GameState.continue_game(),"save restores state") check(GameState.continue_game(),"save restores state")
check(GameState.captain.credits == 8765,"latest credits restored") check(GameState.captain.credits == 8765,"latest credits restored")
check(GameState.captain.expedition.laboratory_project == "machine_gun" and is_equal_approx(GameState.captain.expedition.laboratory_remaining,387.5),"SAVE preserves laboratory project and exact remaining time") check(GameState.captain.expedition.laboratory_project.is_empty() and GameState.captain.expedition.laboratory_unlocks.get("machine_gun",false),"SAVE completes legacy laboratory project immediately")
check(GameState.captain.expedition.alien_project == "hull" and is_equal_approx(GameState.captain.expedition.alien_remaining,245.25),"SAVE preserves alien project and exact remaining time") check(GameState.captain.expedition.alien_project == "hull" and is_equal_approx(GameState.captain.expedition.alien_remaining,245.25),"SAVE preserves alien project and exact remaining time")
check(GameState.captain.expedition.crew.cabin_1.level == 4,"crew restored") check(GameState.captain.expedition.crew.cabin_1.level == 4,"crew restored")
check(GameState.captain.expedition.alien_skills.general,"research restored") check(GameState.captain.expedition.alien_skills.general,"research restored")

View File

@ -34,14 +34,14 @@ func _ready() -> void:
assert(same.is_queued_for_deletion() and inventory.cargo[0].stack_count==5,"Stack pickup works even with full cargo") assert(same.is_queued_for_deletion() and inventory.cargo[0].stack_count==5,"Stack pickup works even with full cargo")
var other := Node2D.new() var other := Node2D.new()
other.set_script(PICKUP) other.set_script(PICKUP)
other.item = ORBS.create_item("booster") other.item = ORBS.create_item("atomic_engine")
other.position = player.position other.position = player.position
game.add_child(other) game.add_child(other)
other._physics_process(0) other._physics_process(0)
assert(not other.is_queued_for_deletion(),"Different orb stays in space while cargo is full") assert(not other.is_queued_for_deletion(),"Different orb stays in space while cargo is full")
inventory.cargo[1] = null inventory.cargo[1] = null
other._physics_process(0) other._physics_process(0)
assert(other.is_queued_for_deletion() and inventory.cargo[1].definition.orb_id=="booster") assert(other.is_queued_for_deletion() and inventory.cargo[1].definition.orb_id=="atomic_engine")
var source := SLOT.instantiate() var source := SLOT.instantiate()
source.slot_kind = "cargo" source.slot_kind = "cargo"
source.slot_index = 0 source.slot_index = 0
@ -71,7 +71,7 @@ func _ready() -> void:
engine.definition = preload("res://resources/items/engine_extension.tres") engine.definition = preload("res://resources/items/engine_extension.tres")
inventory.module_slots[2] = engine inventory.module_slots[2] = engine
assert(inventory.select_orb(1) and inventory.use_selected_orb(engine)) assert(inventory.select_orb(1) and inventory.use_selected_orb(engine))
assert(inventory.cargo[1]==null and "booster" in engine.special_orbs) assert(inventory.cargo[1]==null and "atomic_engine" in engine.special_orbs)
var radar := Control.new() var radar := Control.new()
radar.set_script(preload("res://scripts/ui/scanner_overlay.gd")) radar.set_script(preload("res://scripts/ui/scanner_overlay.gd"))
radar.player = player radar.player = player

View File

@ -0,0 +1,42 @@
extends Node
const LAB := preload("res://scripts/planets/laboratory.gd")
const SKILLS := preload("res://scripts/captain/captain_skills.gd")
func _ready() -> void:
var captain := preload("res://scripts/captain/captain_progress.gd").new()
var progress: Resource = captain.expedition
progress.laboratory_offers.assign(["machine_gun"])
assert(not LAB.start(captain,"machine_gun"))
for key in LAB.COSTS.machine_gun: captain.minerals[key] = int(LAB.COSTS.machine_gun[key])
assert(LAB.start(captain,"machine_gun"))
assert(progress.laboratory_unlocks.get("machine_gun",false))
assert(progress.laboratory_remaining == 0 and progress.laboratory_project.is_empty())
for key in LAB.COSTS.machine_gun: assert(captain.minerals[key] == 0)
assert(not LAB.start(captain,"machine_gun"))
assert(not LAB.start(captain,"unknown_project"))
progress.laboratory_project = "beam_laser"
progress.laboratory_remaining = 450.0
progress.laboratory_duration = 600.0
assert(ResourceSaver.save(progress,"user://legacy_lab.tres") == OK)
var restored: Resource = ResourceLoader.load("user://legacy_lab.tres","",ResourceLoader.CACHE_MODE_IGNORE_DEEP)
LAB.ensure_offers(restored)
assert(restored.laboratory_unlocks.get("beam_laser",false))
assert(restored.laboratory_project.is_empty() and restored.laboratory_remaining == 0)
assert(restored.laboratory_offers.size() == 3)
assert(not LAB.advance(restored,100))
captain.skills = {"scientist":3,"gunsmith":2}
captain.learning_points = 1
captain.skill_offers.assign(["scientist","gunsmith","miner"])
captain.prepare_skill_offers()
assert(not SKILLS.IDS.has("scientist") and not SKILLS.DATA.has("scientist"))
assert(not captain.skills.has("scientist") and captain.skills.gunsmith == 2)
assert(captain.learning_points == 4 and not captain.skill_offers.has("scientist"))
captain.normalize_skill_ranks()
assert(captain.learning_points == 4,"Refund only once")
assert(not captain.can_train("scientist"))
var panel := preload("res://scripts/planets/laboratory_panel.gd").new()
panel.captain = captain
add_child(panel)
assert(panel.find_children("Research_*","Button",true,false).size() == 3)
assert(panel.find_children("*","ProgressBar",true,false).is_empty())
print("INSTANT_LABORATORY_TEST: PASS (immediate unlock, exact cost, invalid/duplicate guards, old project, removed skill, one-time refund, UI)")
get_tree().quit()

View File

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

View File

@ -0,0 +1,4 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/ships/instant_laboratory_test.gd" id="1"]
[node name="InstantLaboratoryTest" type="Node"]
script = ExtResource("1")

View File

@ -42,7 +42,7 @@ func _ready() -> void:
assert(definition.item_id in LAB.IDS) assert(definition.item_id in LAB.IDS)
for id in LAB.IDS: for id in LAB.IDS:
assert(LAB.definition(id) != null and LAB.COSTS.has(id)) assert(LAB.definition(id) != null and LAB.COSTS.has(id))
assert(not LAB.start(captain,offers[0]),"Laboratory must be built") assert(progress.freighter_rooms.get("expansion_1") == "laboratory","Laboratory built from the start")
progress.freighter_rooms.expansion_1 = "laboratory" progress.freighter_rooms.expansion_1 = "laboratory"
assert(not LAB.start(captain,offers[0]),"Cannot research without materials") assert(not LAB.start(captain,offers[0]),"Cannot research without materials")
for cost in LAB.COSTS.values(): for cost in LAB.COSTS.values():
@ -65,16 +65,16 @@ func _ready() -> void:
var before: Dictionary = captain.minerals.duplicate() var before: Dictionary = captain.minerals.duplicate()
assert(LAB.start(captain,"incendiary_missile")) assert(LAB.start(captain,"incendiary_missile"))
for key in LAB.COSTS.incendiary_missile: assert(captain.minerals[key]==before[key]-LAB.COSTS.incendiary_missile[key]) for key in LAB.COSTS.incendiary_missile: assert(captain.minerals[key]==before[key]-LAB.COSTS.incendiary_missile[key])
assert(not LAB.start(captain,"radiation_laser"),"Only one project at a time") assert(not LAB.start(captain,"incendiary_missile"),"No duplicate discovery charge")
progress.analysis_samples = 10 progress.analysis_samples = 10
assert(preload("res://scripts/planets/alien_research.gd").purchase(progress,"general")) assert(preload("res://scripts/planets/alien_research.gd").purchase(progress,"general"))
GameState.advance_play_time(570.0,game) GameState.advance_play_time(570.0,game)
assert(progress.laboratory_remaining==30,"Research advances while docked without any mining stations") assert(progress.laboratory_remaining==0,"Module discovery has no timer")
assert(progress.alien_remaining==30,"Both research stations advance in parallel") assert(progress.alien_remaining==30,"Alien research still advances while docked")
assert(not progress.laboratory_unlocks.has("incendiary_missile")) assert(progress.laboratory_unlocks.has("incendiary_missile"))
assert(ResourceSaver.save(progress,"user://laboratory_progress_test.tres")==OK) assert(ResourceSaver.save(progress,"user://laboratory_progress_test.tres")==OK)
var saved: Resource = ResourceLoader.load("user://laboratory_progress_test.tres","",ResourceLoader.CACHE_MODE_IGNORE) var saved: Resource = ResourceLoader.load("user://laboratory_progress_test.tres","",ResourceLoader.CACHE_MODE_IGNORE)
assert(saved.laboratory_project=="incendiary_missile" and saved.laboratory_remaining==30 and saved.laboratory_offers==offers) assert(saved.laboratory_project.is_empty() and saved.laboratory_remaining==0 and saved.laboratory_unlocks.get("incendiary_missile",false))
await snapshot("res://artifacts/laboratory_progress.png") await snapshot("res://artifacts/laboratory_progress.png")
progress.freighter_rooms.expansion_2 = "alien_research" progress.freighter_rooms.expansion_2 = "alien_research"
manager.screen._select_section("expansion_2") manager.screen._select_section("expansion_2")
@ -82,11 +82,11 @@ func _ready() -> void:
manager._leave() manager._leave()
get_tree().paused = true get_tree().paused = true
GameState.advance_play_time(30,game) GameState.advance_play_time(30,game)
assert(progress.laboratory_remaining==30,"Ordinary pause stops research") assert(progress.laboratory_remaining==0,"Module discovery is already complete")
assert(progress.alien_remaining==30,"Ordinary pause stops alien research") assert(progress.alien_remaining==30,"Ordinary pause stops alien research")
get_tree().paused = false get_tree().paused = false
GameState.advance_play_time(29.9,game) GameState.advance_play_time(29.9,game)
assert(not progress.laboratory_unlocks.has("incendiary_missile")) assert(progress.laboratory_unlocks.has("incendiary_missile"))
GameState.advance_play_time(0.1,game) GameState.advance_play_time(0.1,game)
assert(progress.laboratory_unlocks.has("incendiary_missile") and progress.laboratory_offers.size()==3) assert(progress.laboratory_unlocks.has("incendiary_missile") and progress.laboratory_offers.size()==3)
assert(progress.alien_skills.general) assert(progress.alien_skills.general)

View File

@ -20,10 +20,9 @@ func _ready() -> void:
captain.level = 1 captain.level = 1
progress.laboratory_offers.assign(["virus"]) progress.laboratory_offers.assign(["virus"])
assert(LAB.start(captain,"virus")) assert(LAB.start(captain,"virus"))
assert(progress.laboratory_remaining == 600) assert(progress.laboratory_remaining == 0)
LAB.advance(progress,599) assert(progress.laboratory_project.is_empty())
assert(not progress.laboratory_unlocks.get("virus",false)) assert(not LAB.start(captain,"virus"))
LAB.advance(progress,1)
assert(progress.laboratory_unlocks.virus) assert(progress.laboratory_unlocks.virus)
for id in LAB.IDS: for id in LAB.IDS:
if progress.laboratory_unlocks.get(id,false): continue if progress.laboratory_unlocks.get(id,false): continue
@ -63,5 +62,5 @@ func _ready() -> void:
assert(not restored.alien_skills.get("shields_b_next_next_next",false)) assert(not restored.alien_skills.get("shields_b_next_next_next",false))
ALIEN.advance(restored,0.1) ALIEN.advance(restored,0.1)
assert(restored.alien_skills.shields_b_next_next_next) assert(restored.alien_skills.shields_b_next_next_next)
print("RESEARCH PROGRESSION PASS: all 23+28 discoveries at captain level 1; level-independent offers, 600-second timers, single projects, costs, exact save/load continuation") print("RESEARCH PROGRESSION PASS: all 23+28 discoveries at captain level 1; level-independent offers, instant module discoveries, timed alien research, costs, exact save/load continuation")
get_tree().quit() get_tree().quit()

View File

@ -0,0 +1,107 @@
extends Node
var failures: Array[String] = []
var is_runner := false
func check(ok: bool, message: String) -> void:
if not ok:
failures.append(message)
push_error(message)
func _ready() -> void:
if not is_runner:
var runner := get_script().new() as Node
runner.is_runner = true
get_tree().root.add_child.call_deferred(runner)
else: run.call_deferred()
func frames(count := 3) -> void:
for index in count: await get_tree().process_frame
func run() -> void:
var state := GameState
state.start_new_run()
check(not preload("res://scripts/planets/expedition_controller.gd").PLANET_LANDING_ENABLED,"Landing disabled")
var checks := preload("res://tests/distant_expeditions_checks.gd").new()
for failure in checks.run_checks().failures: check(false,str(failure))
checks.free()
var map := {"id":"space_test","level":2,"seed":81234,"name":"Test","source_planet":"Boss","source_level":1}
state.captain.expedition.distant_maps[map.id] = map
state.captain.expedition.deuter = 0
check(state.depart_distant(map.id),"Departure without deuter")
check(state.captain.expedition.deuter == 0,"No fuel mutation")
check(not state.depart_distant(map.id),"No duplicate departure")
get_tree().change_scene_to_file("res://scenes/game/game.tscn")
await frames(8)
var game := get_tree().current_scene
var player := game.get_node("PlayerShip")
player.set_physics_process(false)
for enemy in get_tree().get_nodes_in_group("enemies"): enemy.set_physics_process(false)
var bases: Array[Node] = []
for poi in get_tree().get_nodes_in_group("exploration_pois"):
if str(poi.get("kind")) == "enemy_base": bases.append(poi)
check(bases.size() == 3,"Three resource bases")
var base := bases[0]
var rewards: Dictionary = base.materials(base.reward_seed,base.level)
var id: String = base.base_id
base.take_weapon_hit(1e12)
base.take_weapon_hit(1e12)
await frames()
check(state.explored_pois.has(id),"Base claimed persistently")
for pickup in get_tree().get_nodes_in_group("loot"):
if pickup.get_script() == preload("res://scripts/items/expedition_pickup.gd") and pickup.payload.has("materials"):
player.position = pickup.position
pickup._physics_process(0.0)
await frames()
for key in rewards: check(state.captain.minerals.get(key,0) == rewards[key],"Collect base material "+str(key))
var sample := Node2D.new()
sample.set_script(preload("res://scripts/items/expedition_pickup.gd"))
sample.payload = {"samples":1}
sample.position = player.position
game.add_child(sample)
sample._physics_process(0.0)
check(state.captain.expedition.analysis_samples == 1,"Collect sample")
var controller := game.get_node("PlanetExpeditions")
player.position = controller.freighter.position
controller.land_nearby()
await frames()
check(is_instance_valid(controller.screen),"Docking still works")
var screen: Control = controller.screen
var inventory := player.get_node("ShipInventory")
var weapon: ItemInstance = preload("res://scripts/items/item_generator.gd").generate_for_definition(inventory.weapon_slots[0].definition,10,2)
inventory.weapon_slots[0] = weapon
screen._tab = "hangar"
screen._refresh()
check(screen.find_child("RepairLander",true,false) == null,"No lander repair")
screen._tab = "workshop"
screen._refresh()
await frames()
check(screen.find_child("EngineerItem",true,false) != null,"Engineer on freighter")
state.captain.expedition.time_crystals = 100000
var service := preload("res://scripts/planets/time_engineer.gd")
var addon := -1
for index in weapon.additional_modifiers.size():
if service.affix_for(weapon,index) != null:
addon = index
weapon.additional_modifiers[index].value = 0.0
break
if addon >= 0:
check(service.maximize(state.captain.expedition,inventory,weapon,addon),"Engineer works outside surface")
check(not service.maximize(state.captain.expedition,inventory,weapon,addon),"No charge for max addon")
check(addon >= 0,"Test equipment has addon")
var orb := preload("res://scripts/items/special_orbs.gd").create_item("piercing")
check(inventory.add_item(orb),"Orb in cargo")
check(inventory.select_orb(inventory.cargo.find(orb)),"Click orb")
check(inventory.use_selected_orb(weapon),"Click equipment applies orb")
controller._leave()
game.set_physics_process(false)
state.restore_distant_home()
var enemy := get_tree().get_first_node_in_group("enemies")
enemy.loot_tier = 2
enemy._drop_loot()
await frames()
var maps := 0
for pickup in get_tree().get_nodes_in_group("loot"):
if pickup.get_script() == preload("res://scripts/items/expedition_pickup.gd") and pickup.payload.has("map"):
maps += 1
player.position = pickup.position
pickup._physics_process(0.0)
check(maps == 1,"Normal boss drops exactly one map")
check(state.captain.expedition.distant_maps.size() == 1,"Boss map collected")
print("SPACE_PROGRESSION_TEST: ",JSON.stringify({"failures":failures}))
get_tree().quit(0 if failures.is_empty() else 1)

View File

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

View File

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/space_progression_test.gd" id="1"]
[node name="SpaceProgressionTest" type="Node"]
script = ExtResource("1")