Sporo zmian

This commit is contained in:
Tomasz Boruc 2026-10-20 19:53:03 +02:00
parent ed26b0974d
commit 1e5e2cf1e2
89 changed files with 1895 additions and 62 deletions

View File

@ -0,0 +1,14 @@
# Distant expedition art
Generated with the built-in image_gen tool. Transparent backgrounds preserved. Space backgrounds reuse assets/background/space.
## Prompts
- assets/icons/orbs/piercing.png: A single magical sci-fi upgrade orb for shield piercing: polished luminous violet crystal sphere split by a brilliant golden needle penetrating a cyan segmented energy shield, intricate dark titanium ring, high-end hand-painted space RPG item icon, front view, centered circular silhouette, no text, no numbers, true transparent background, generous padding, reads clearly at 48px.
- assets/icons/orbs/booster.png: A single magical sci-fi engine upgrade orb: polished luminous emerald and turquoise crystal sphere with a white-hot miniature rocket exhaust vortex and infinity-shaped energy loop inside, intricate dark titanium ring, high-end hand-painted space RPG item icon, front view, centered circular silhouette, no text, no numbers, true transparent background, generous padding, reads clearly at 48px.
- ancient_gate.png: Top-down orthographic 2D space game decorative derelict alien navigation megastructure, a broken ancient orbital gate with asymmetrical modular wreckage, dark weathered blue steel, brass edges, sparse cyan power lights, hollow circular center, beautiful richly detailed hand-painted science fiction game sprite, readable at 300px, isolated on true transparent background, no background stars, no planets, no text, no logos, no shadow plane. Entire structure fits with padding.
- abandoned_relay.png: Top-down orthographic 2D space game sprite: abandoned deep space communications array, long trident-shaped dark weathered titanium structure with three large circular dishes and folded solar panel wings, little cyan and amber warning lights, sophisticated detailed hand-painted science fiction strategy game art, isolated true transparent background. No planets no stars no backdrop no text, entire structure centered with padding, readable at 300 pixels.
- freighter_wreck.png: Top-down orthographic 2D space game sprite: wreck of an enormous ancient freight carrier, split broken hull with exposed machinery and scattered attached hull fragments, asymmetrical silhouette, weathered dark blue gray plating with muted copper edges and a few dim cyan lights, polished hand-painted science fiction strategy game art, isolated on true transparent background. No planets no stars no backdrop no text, whole wreck fits centered with padding, readable at 300 pixels.
- assets/icons/orbs/advancement.png: A single sci-fi RPG Orb of Ascension upgrade item icon. A luminous amber-gold crystal sphere containing three elegant ascending chevrons made of white starlight, delicate concentric mechanical titanium and brass ring, tiny violet sparks, polished hand painted science fiction game inventory art matching premium magical technology orb icons. Centered complete circular silhouette, reads clearly at 48px, front view, true transparent background with padding, no text, no letters, no numbers, no square background.
- assets/icons/resources/time_crystals.png: A single time crystal currency icon for a polished science fiction RPG: three sharp translucent turquoise and lavender crystal shards suspended inside a faint gold chronal orbit, luminous white cyan core, intricate refraction, painterly premium inventory art, readable at 36 pixels, isolated true transparent background, centered with padding, no letters no numbers no text no square frame.
- assets/planets/props/time_engineer.png: Top-down near-overhead orthographic 2D sci-fi planetary game prop: a specialist chronal engineer's small workshop cottage, dark teal metal roof, copper pipes, compact cyan glowing precision calibration bench outside, crystalline turquoise time-resonator coils, warm amber entrance, a small antenna and neat tool crates. Beautiful richly hand-painted strategy game sprite, distinct readable silhouette at 150 pixels, entire building centered with padding, isolated true transparent background, no surrounding landscape, no people, no text, no logos.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cgqeyd7dnfkek"
path="res://.godot/imported/time_crystals.png-c08cb1ec507a34dc5877b8fdcd99f93a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/resources/time_crystals.png"
dest_files=["res://.godot/imported/time_crystals.png-c08cb1ec507a34dc5877b8fdcd99f93a.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: 883 KiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://8rg0pkfx175s"
path="res://.godot/imported/expedition_navigation.png-7f86b11b1d2ab12f17bfd21a15821557.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/planets/expedition_navigation.png"
dest_files=["res://.godot/imported/expedition_navigation.png-7f86b11b1d2ab12f17bfd21a15821557.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: 786 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dt60233tfftow"
path="res://.godot/imported/time_engineer.png-98843cd5608f7b86fd85c0e7b5fae8ad.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/planets/props/time_engineer.png"
dest_files=["res://.godot/imported/time_engineer.png-98843cd5608f7b86fd85c0e7b5fae8ad.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,26 @@
extends RefCounted
const MAP_CHANCE := 0.15
const BANK_CHANCE := 0.15
static func roll(level: int, boss: bool, seed_value: int, map_roll: float, _orb_roll: float) -> Array[Dictionary]:
var result: Array[Dictionary] = []
if boss or map_roll < MAP_CHANCE:
var profile := {"id":"distant:%d" % seed_value,"seed":seed_value,"name":"Daleka wyprawa %d" % level}
result.append({"map":preload("res://scripts/planets/distant_expeditions.gd").offer(profile,level)})
if boss:
result.append({"orb":preload("res://scripts/items/special_orbs.gd").IDS[posmod(seed_value,preload("res://scripts/items/special_orbs.gd").IDS.size())]})
return result
static func crystal_amount(level: int, tier: int, roll_value: int) -> int:
return clampi(roll_value,1,3) * maxi(1,ceili(maxi(1,level)/10.0)) * [1,2,5][clampi(tier,0,2)]
static func drop(enemy: Node2D) -> void:
var crystals := Node2D.new()
crystals.set_script(preload("res://scripts/items/time_crystal_pickup.gd"))
crystals.amount = crystal_amount(GameState.system_level,int(enemy.get("loot_tier")),randi_range(1,3))
crystals.position = enemy.position+Vector2(38,-20)
enemy.get_parent().call_deferred("add_child",crystals)
for payload in roll(GameState.system_level,int(enemy.get("loot_tier"))==2,randi(),randf(),randf()):
var pickup := Node2D.new()
pickup.set_script(preload("res://scripts/items/expedition_pickup.gd"))
pickup.payload = payload
pickup.position = enemy.position+Vector2(randf_range(-36,36),65)
enemy.get_parent().call_deferred("add_child",pickup)

View File

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

View File

@ -0,0 +1,102 @@
extends Node
const GENERATOR := preload("res://scripts/levels/distant_sector_generator.gd")
var game: Node2D
var _status: Label
var _return: Button
var _refresh := 0.0
func _ready() -> void:
name = "DistantSector"
var map: Dictionary = GameState.active_distant_map
game.system_data = GENERATOR.generate(map)
game.player.position = Vector2.ZERO
var camera: Camera2D = game.player.get_node("Camera2D")
camera.limit_left = -2600
camera.limit_right = 2600
camera.limit_top = -1800
camera.limit_bottom = 1800
for data in game.system_data.enemies:
if int(data.tier) == 2: game._pending_boss = data.duplicate()
else: game._spawn_enemy(data,data.position)
_build_structures()
var panel := PanelContainer.new()
panel.name = "DistantReturnPanel"
game.get_node("HUD/HUDRoot").add_child(panel)
panel.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP)
panel.offset_left = -195
panel.offset_right = 195
panel.offset_top = 104
panel.offset_bottom = 174
var ui := preload("res://scripts/planets/freighter_menu.gd")
ui.style(panel,8,0.5)
var column := VBoxContainer.new()
panel.add_child(column)
_status = ui.label(column,"",12,Color("e9b4fc"))
_return = ui.button(column,"POWRÓT DO FRACHTOWCA",_return_home,true)
_return.name = "ReturnFromDistant"
_update_status()
func _build_structures() -> void:
var group := Node2D.new()
group.name = "AncientStructures"
group.z_index = -8
game.add_child(group)
for data in game.system_data.structures:
var sprite := Sprite2D.new()
sprite.texture = load("res://assets/expeditions/%s.png" % ["ancient_gate","abandoned_relay","freighter_wreck"][group.get_child_count()/2 % 3])
sprite.position = data.position
sprite.rotation = data.rotation
if sprite.texture != null: sprite.scale = Vector2.ONE * float(data.size) / maxf(sprite.texture.get_width(),sprite.texture.get_height())
group.add_child(sprite)
var label := Label.new()
label.text = str(data.name)
label.position = data.position+Vector2(-150,float(data.size)*0.4)
label.size.x = 300
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size",12)
label.modulate = Color("8da9ae")
group.add_child(label)
func _physics_process(delta: float) -> void:
if game._transitioning or bool(game.player.get("_is_destroyed")): return
if not game.system_cleared and game.get_remaining_enemies() == 0:
if not game._boss_spawned and not game._pending_boss.is_empty(): game._spawn_boss()
else: game.system_cleared = true
_refresh += delta
if _refresh >= 0.2:
_refresh = 0
_update_status()
func return_blocked() -> bool:
if float(game.player.get("landing_combat_lock")) > 0: return true
for enemy in get_tree().get_nodes_in_group("enemies"):
if not enemy.is_queued_for_deletion() and float(enemy.get("hull")) > 0 and enemy.global_position.distance_to(game.player.global_position) < 650: return true
return false
func _update_status() -> void:
_status.text = "WYPRAWA %d · %s" % [GameState.system_level,"SEKTOR OCZYSZCZONY" if game.system_cleared else ("BOSS" if game._boss_spawned else "%d wrogów" % game.get_remaining_enemies())]
_return.disabled = bool(game.player.get("_is_destroyed")) or return_blocked()
_return.tooltip_text = "Oddal się od przeciwników i zakończ walkę." if _return.disabled else "Wróć do układu startowego. Powrót nie zużywa deuteru. Zbierz łup przed odlotem."
func _return_home() -> void:
if game._transitioning or bool(game.player.get("_is_destroyed")) or return_blocked(): return
game._transitioning = true
_load_home.call_deferred()
func _load_home() -> void:
var active: Dictionary = GameState.active_distant_map.duplicate(true)
var home: Dictionary = GameState.distant_home.duplicate(true)
var level := GameState.system_level
var seed_value := GameState.system_seed
game.player.get_node("ShipInventory").preserve_for_restart()
GameState.restore_distant_home()
GameState.return_to_freighter = true
var error := get_tree().reload_current_scene()
if error != OK:
GameState.return_to_freighter = false
GameState.active_distant_map = active
GameState.distant_home = home
GameState.system_level = level
GameState.system_seed = seed_value
GameState.restart_inventory.clear()
game._transitioning = false

View File

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

View File

@ -44,6 +44,8 @@ var _combat_target: Node2D
var virus_controlled := false var virus_controlled := false
var _virus_infection_attempted := false var _virus_infection_attempted := false
var _research_player_hit := false var _research_player_hit := false
var _weapon_piercing := 0.0
var combat_level := 1
var _reward_claimed := false var _reward_claimed := false
func roll_virus_infection() -> bool: func roll_virus_infection() -> bool:
@ -101,6 +103,7 @@ func _ready() -> void:
func apply_level_settings(level: int, tier: int) -> void: func apply_level_settings(level: int, tier: int) -> void:
loot_tier = tier loot_tier = tier
combat_level = maxi(1, level)
$Sprite2D.modulate = get_tier_tint() $Sprite2D.modulate = get_tier_tint()
var power := LEVEL_BALANCE.enemy_stat_scale(level) var power := LEVEL_BALANCE.enemy_stat_scale(level)
var combat_multiplier := 2.0 if tier == 2 else 1.25 var combat_multiplier := 2.0 if tier == 2 else 1.25
@ -327,6 +330,11 @@ func _fire_plasma(direction: Vector2) -> void:
get_parent().add_child(projectile) get_parent().add_child(projectile)
WEAPON_AUDIO_SCRIPT.play(get_parent(), global_position, weapon_kind, -4.0) WEAPON_AUDIO_SCRIPT.play(get_parent(), global_position, weapon_kind, -4.0)
func take_piercing_weapon_hit(amount: float, critical: bool, damage_type: int, piercing: float) -> void:
_weapon_piercing = clampf(piercing,0.0,0.5)
take_weapon_hit(amount,critical,damage_type)
_weapon_piercing = 0.0
func take_weapon_hit(amount: float, critical: bool, damage_type: int = DamageTypes.Type.KINETIC) -> void: func take_weapon_hit(amount: float, critical: bool, damage_type: int = DamageTypes.Type.KINETIC) -> void:
if is_queued_for_deletion() or hull <= 0.0: if is_queued_for_deletion() or hull <= 0.0:
return return
@ -345,7 +353,8 @@ func take_damage(amount: float, critical: bool = false, damage_type: int = Damag
var progress: Resource = GameState.captain.expedition var progress: Resource = GameState.captain.expedition
var shield_multiplier := 1.0 + (research.bonus(progress, "shields") if _research_player_hit else 0.0) var shield_multiplier := 1.0 + (research.bonus(progress, "shields") if _research_player_hit else 0.0)
var hull_multiplier := 1.0 + (research.bonus(progress, "hull") if _research_player_hit else 0.0) var hull_multiplier := 1.0 + (research.bonus(progress, "hull") if _research_player_hit else 0.0)
var shield_damage := minf(shields, actual * shield_multiplier) var piercing_damage := actual * _weapon_piercing if _research_player_hit else 0.0
var shield_damage := minf(shields, (actual - piercing_damage) * shield_multiplier)
var remaining := maxf(0.0, actual - shield_damage / shield_multiplier) var remaining := maxf(0.0, actual - shield_damage / shield_multiplier)
shields -= shield_damage shields -= shield_damage
var hull_damage := minf(hull, remaining * hull_multiplier) var hull_damage := minf(hull, remaining * hull_multiplier)
@ -396,7 +405,9 @@ func _reward_captain() -> void:
func _drop_loot() -> void: func _drop_loot() -> void:
# Roll independently of equipment drops. # Roll independently of equipment drops.
var bank_chance := 0.1 var distant := not GameState.active_distant_map.is_empty()
if distant: preload("res://scripts/game/distant_loot.gd").drop(self)
var bank_chance: float = preload("res://scripts/game/distant_loot.gd").BANK_CHANCE if distant else 0.1
if randf() < bank_chance: if randf() < bank_chance:
var bank := ItemInstance.new() var bank := ItemInstance.new()
bank.definition = preload("res://resources/items/energy_bank.tres") bank.definition = preload("res://resources/items/energy_bank.tres")

View File

@ -626,7 +626,7 @@ func _create_system_badge() -> void:
func _update_system_badge() -> void: func _update_system_badge() -> void:
if not is_instance_valid(_system_badge): return if not is_instance_valid(_system_badge): return
_system_badge.position = Vector2(_captain_panel.position.x + _captain_panel.size.x + 10, _captain_panel.position.y) _system_badge.position = Vector2(_captain_panel.position.x + _captain_panel.size.x + 10, _captain_panel.position.y)
_system_title.text = "System %d" % GameState.system_level _system_title.text = ("Wyprawa %d" if not GameState.active_distant_map.is_empty() else "System %d") % GameState.system_level
_system_mode.text = "HARD" if GameState.system_level > 60 else "NORMALNY" _system_mode.text = "HARD" if GameState.system_level > 60 else "NORMALNY"
_system_mode.add_theme_color_override("font_color", Color("e88972") if GameState.system_level > 60 else Color("88a7ae")) _system_mode.add_theme_color_override("font_color", Color("e88972") if GameState.system_level > 60 else Color("88a7ae"))

View File

@ -27,6 +27,12 @@ var _pending_boss: Dictionary = {}
var _boss_spawned := false var _boss_spawned := false
func _ready() -> void: func _ready() -> void:
if not GameState.active_distant_map.is_empty():
var distant := Node.new()
distant.set_script(preload("res://scripts/game/distant_sector.gd"))
distant.game = self
add_child(distant)
return
GameState.system_level = clampi(GameState.system_level, 1, LEVEL_BALANCE.MAX_LEVEL) GameState.system_level = clampi(GameState.system_level, 1, LEVEL_BALANCE.MAX_LEVEL)
if GameState.system_seed == 0: if GameState.system_seed == 0:
GameState.system_seed = GameState.create_system_seed() GameState.system_seed = GameState.create_system_seed()
@ -58,6 +64,10 @@ func _ready() -> void:
expeditions.game = self expeditions.game = self
expeditions.player = player expeditions.player = player
add_child(expeditions) add_child(expeditions)
if GameState.return_to_freighter:
GameState.return_to_freighter = false
player.position = expeditions.freighter.position
expeditions.open_freighter.call_deferred()
func _spawn_celestial_bodies() -> void: func _spawn_celestial_bodies() -> void:
var bodies := Node2D.new() var bodies := Node2D.new()
@ -157,6 +167,7 @@ func is_boss_alive() -> bool:
return false return false
func _physics_process(_delta: float) -> void: func _physics_process(_delta: float) -> void:
if not GameState.active_distant_map.is_empty(): return
if _transitioning or bool(player.get("_is_destroyed")): if _transitioning or bool(player.get("_is_destroyed")):
services_menu.call("hide_services") services_menu.call("hide_services")
return return
@ -236,6 +247,30 @@ func _load_next_level() -> void:
services_menu.call("show_services", false, true) services_menu.call("show_services", false, true)
push_error("Could not enter next system: %s" % error_string(error)) push_error("Could not enter next system: %s" % error_string(error))
func travel_to_distant(map_id: String) -> bool:
if _transitioning or bool(player.get("_is_destroyed")) or not GameState.active_distant_map.is_empty(): return false
if not GameState.captain.expedition.distant_maps.has(map_id): return false
if GameState.captain.expedition.deuter < preload("res://scripts/planets/distant_expeditions.gd").fuel_cost(GameState.captain.expedition.distant_maps[map_id]): return false
_transitioning = true
_load_distant.call_deferred(map_id)
return true
func _load_distant(map_id: String) -> void:
player.get_node("ShipInventory").preserve_for_restart()
if not GameState.depart_distant(map_id):
GameState.restart_inventory.clear()
_transitioning = false
return
var cost: int = preload("res://scripts/planets/distant_expeditions.gd").fuel_cost(GameState.active_distant_map)
get_tree().paused = false
var error := get_tree().reload_current_scene()
if error != OK:
GameState.restore_distant_home()
GameState.captain.expedition.deuter += cost
GameState.captain.expedition.emit_changed()
GameState.restart_inventory.clear()
_transitioning = false
func travel_to_system(level: int) -> bool: func travel_to_system(level: int) -> bool:
if _transitioning or bool(player.get("_is_destroyed")) or level == GameState.system_level or not GameState.can_visit_system(level): return false if _transitioning or bool(player.get("_is_destroyed")) or level == GameState.system_level or not GameState.can_visit_system(level): return false
_transitioning = true _transitioning = true

View File

@ -7,6 +7,9 @@ var selected_ship_path: String = ""
var restart_inventory: Dictionary = {} var restart_inventory: Dictionary = {}
var system_level: int = 1 var system_level: int = 1
var system_seed: int = 0 var system_seed: int = 0
var active_distant_map: Dictionary = {}
var distant_home: Dictionary = {}
var return_to_freighter := false
var highest_system := 1 var highest_system := 1
var system_seeds: Dictionary = {} var system_seeds: Dictionary = {}
var cleared_systems: Dictionary = {} var cleared_systems: Dictionary = {}
@ -16,6 +19,26 @@ var asteroid_retry_seeds: Dictionary = {}
var exploration_wreck_bonuses: Dictionary = {} var exploration_wreck_bonuses: Dictionary = {}
var hud_panel_state: Dictionary = {} var hud_panel_state: Dictionary = {}
func depart_distant(map_id: String) -> bool:
if not active_distant_map.is_empty() or not captain.expedition.distant_maps.has(map_id): return false
var map: Dictionary = captain.expedition.distant_maps[map_id]
var cost: int = preload("res://scripts/planets/distant_expeditions.gd").fuel_cost(map)
if captain.expedition.deuter < cost: return false
distant_home = {"level":system_level,"seed":system_seed}
active_distant_map = map.duplicate(true)
captain.expedition.deuter -= cost
system_level = int(map.level)
system_seed = int(map.seed)
captain.expedition.emit_changed()
return true
func restore_distant_home() -> void:
if distant_home.is_empty(): return
system_level = int(distant_home.level)
system_seed = int(distant_home.seed)
active_distant_map.clear()
distant_home.clear()
func create_system_seed() -> int: func create_system_seed() -> int:
var rng := RandomNumberGenerator.new() var rng := RandomNumberGenerator.new()
rng.randomize() rng.randomize()
@ -37,6 +60,9 @@ func start_new_run() -> void:
captain = CAPTAIN_PROGRESS.new() captain = CAPTAIN_PROGRESS.new()
system_level = 1 system_level = 1
highest_system = 1 highest_system = 1
active_distant_map.clear()
distant_home.clear()
return_to_freighter = false
system_seeds.clear() system_seeds.clear()
cleared_systems.clear() cleared_systems.clear()
_production_signal_time = 0 _production_signal_time = 0

View File

@ -5,11 +5,12 @@ const DURATION := 10.0
var remaining := 0.0 var remaining := 0.0
var tick_damage := 0.0 var tick_damage := 0.0
var credited_to_player := false var credited_to_player := false
var shield_piercing := 0.0
var _tick_elapsed := 0.0 var _tick_elapsed := 0.0
var _base_color := Color.WHITE var _base_color := Color.WHITE
var _sprite: Sprite2D var _sprite: Sprite2D
static func apply(target: Node, damage_per_second: float, player_hit: bool) -> void: static func apply(target: Node, damage_per_second: float, player_hit: bool, piercing: float = 0.0) -> void:
if not target.has_method("take_damage") or not target is CharacterBody2D: if not target.has_method("take_damage") or not target is CharacterBody2D:
return return
var status := target.get_node_or_null("RadiationStatus") var status := target.get_node_or_null("RadiationStatus")
@ -19,7 +20,9 @@ static func apply(target: Node, damage_per_second: float, player_hit: bool) -> v
status.name = "RadiationStatus" status.name = "RadiationStatus"
target.add_child(status) target.add_child(status)
status.remaining = DURATION status.remaining = DURATION
status.tick_damage = maxf(status.tick_damage, damage_per_second) if damage_per_second >= status.tick_damage:
status.tick_damage = damage_per_second
status.shield_piercing = piercing
status.credited_to_player = player_hit status.credited_to_player = player_hit
func _ready() -> void: func _ready() -> void:
@ -37,7 +40,9 @@ func _physics_process(delta: float) -> void:
if _tick_elapsed >= 0.5 or (remaining <= 0.0 and _tick_elapsed > 0.0): if _tick_elapsed >= 0.5 or (remaining <= 0.0 and _tick_elapsed > 0.0):
step = _tick_elapsed step = _tick_elapsed
_tick_elapsed = 0.0 _tick_elapsed = 0.0
if credited_to_player and target.has_method("take_weapon_hit"): if credited_to_player and target.has_method("take_piercing_weapon_hit"):
target.take_piercing_weapon_hit(tick_damage * step, false, DamageTypes.Type.RADIATION, shield_piercing)
elif credited_to_player and target.has_method("take_weapon_hit"):
target.take_weapon_hit(tick_damage * step, false, DamageTypes.Type.RADIATION) target.take_weapon_hit(tick_damage * step, false, DamageTypes.Type.RADIATION)
else: else:
target.take_damage(tick_damage * step, false, DamageTypes.Type.RADIATION) target.take_damage(tick_damage * step, false, DamageTypes.Type.RADIATION)

View File

@ -382,11 +382,19 @@ func is_mini_boosting() -> bool:
func _get_allowed_speed() -> float: func _get_allowed_speed() -> float:
return maxf(max_speed + _boost_speed, max_speed * 2.0 if _mini_boost_active else max_speed + _mini_boost_coast_speed) return 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 := MINI_BOOST_ENERGY_PER_SECOND * delta var cost := MINI_BOOST_ENERGY_PER_SECOND * delta

View File

@ -154,6 +154,7 @@ func _fire_projectile(item: ItemInstance) -> void:
) )
ship.get_parent().add_child(projectile) ship.get_parent().add_child(projectile)
projectile.reset_physics_interpolation() projectile.reset_physics_interpolation()
projectile.set("shield_piercing", item.shield_piercing())
projectile.set("missile_homing", definition.missile_homing) projectile.set("missile_homing", definition.missile_homing)
WEAPON_AUDIO_SCRIPT.play(ship.get_parent(), muzzle_position, definition.weapon_kind) WEAPON_AUDIO_SCRIPT.play(ship.get_parent(), muzzle_position, definition.weapon_kind)
if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN:
@ -198,7 +199,7 @@ func _fire_laser(slot_index: int, item: ItemInstance) -> void:
_break_cloak_on_fire() _break_cloak_on_fire()
if not result.is_empty(): if not result.is_empty():
var hit: Dictionary = ship.call("roll_weapon_hit", item) var hit: Dictionary = ship.call("roll_weapon_hit", item)
_apply_damage(result.get("collider"), float(hit["damage"]), bool(hit["critical"])) _apply_damage(result.get("collider"), float(hit["damage"]), bool(hit["critical"]), item.shield_piercing())
_spawn_laser_impact(endpoint) _spawn_laser_impact(endpoint)
_laser_pulse_timers[slot_index] = definition.laser_pulse_duration _laser_pulse_timers[slot_index] = definition.laser_pulse_duration
_shot_timers[slot_index] = definition.fire_interval _shot_timers[slot_index] = definition.fire_interval
@ -231,8 +232,10 @@ func _consume_energy(amount: float) -> bool:
ship.set("energy", current_energy - amount) ship.set("energy", current_energy - amount)
return true return true
func _apply_damage(target: Variant, damage: float, critical: bool = false) -> void: func _apply_damage(target: Variant, damage: float, critical: bool = false, piercing: float = 0.0) -> void:
if target is Node and target.has_method("take_weapon_hit"): if target is Node and target.has_method("take_piercing_weapon_hit"):
target.call("take_piercing_weapon_hit", damage, critical, DamageTypes.Type.ENERGY, piercing)
elif target is Node and target.has_method("take_weapon_hit"):
target.call("take_weapon_hit", damage, critical, DamageTypes.Type.ENERGY) target.call("take_weapon_hit", damage, critical, DamageTypes.Type.ENERGY)
elif target is Node and target.has_method("take_damage"): elif target is Node and target.has_method("take_damage"):
target.call("take_damage", damage) target.call("take_damage", damage)

View File

@ -28,6 +28,7 @@ var _distance_travelled := 0.0
var _already_hit: Dictionary = {} var _already_hit: Dictionary = {}
var _impact_handled := false var _impact_handled := false
var _engine_time := 0.0 var _engine_time := 0.0
var shield_piercing := 0.0
var _smoke_emission_timer := 0.0 var _smoke_emission_timer := 0.0
var _smoke_puffs: Array[Dictionary] = [] var _smoke_puffs: Array[Dictionary] = []
@ -296,6 +297,8 @@ func _apply_damage_once(target: Variant) -> void:
_already_hit[instance_id] = true _already_hit[instance_id] = true
if enemy_shot and target is CharacterBody2D and target.has_method("take_damage"): if enemy_shot and target is CharacterBody2D and target.has_method("take_damage"):
target.call("take_damage", damage, is_critical, DamageTypes.for_weapon(weapon_kind)) target.call("take_damage", damage, is_critical, DamageTypes.for_weapon(weapon_kind))
elif target.has_method("take_piercing_weapon_hit"):
target.call("take_piercing_weapon_hit", damage, is_critical, DamageTypes.for_weapon(weapon_kind), shield_piercing)
elif target.has_method("take_weapon_hit"): elif target.has_method("take_weapon_hit"):
target.call("take_weapon_hit", damage, is_critical, DamageTypes.for_weapon(weapon_kind)) target.call("take_weapon_hit", damage, is_critical, DamageTypes.for_weapon(weapon_kind))
elif target.has_method("take_damage"): elif target.has_method("take_damage"):
@ -304,4 +307,4 @@ func _apply_damage_once(target: Variant) -> void:
else: else:
target.call("take_damage", damage) target.call("take_damage", damage)
if weapon_kind == ItemDefinition.WeaponKind.GAMMA: if weapon_kind == ItemDefinition.WeaponKind.GAMMA:
preload("res://scripts/game/radiation_status.gd").apply(target, damage * 0.25, not enemy_shot) preload("res://scripts/game/radiation_status.gd").apply(target, damage * 0.25, not enemy_shot, shield_piercing if not enemy_shot else 0.0)

View File

@ -0,0 +1,39 @@
extends Node2D
var payload: Dictionary = {}
var _player: Node2D
var _label: Label
var _icon: Texture2D
var _time := 0.0
func _ready() -> void:
add_to_group("loot")
z_index = 18
_player = get_parent().get_node_or_null("PlayerShip")
_icon = preload("res://scripts/items/special_orbs.gd").icon(str(payload.orb)) if payload.has("orb") else load("res://assets/planets/expedition_navigation.png")
_label = Label.new()
_label.position = Vector2(-120,24)
_label.size.x = 240
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_label.add_theme_font_size_override("font_size",12)
_label.text = preload("res://scripts/items/special_orbs.gd").NAMES[str(payload.orb)] if payload.has("orb") else "Mapa wyprawy · poz. %d" % int(payload.map.level)
add_child(_label)
func _process(delta: float) -> void:
_time += delta
queue_redraw()
func _draw() -> void:
var color := Color("b991ff") if payload.has("map") else Color("f5cf82")
draw_circle(Vector2.ZERO,27+sin(_time*3)*2,Color(color,0.15))
draw_arc(Vector2.ZERO,24,_time,_time+TAU*0.8,40,color,1.5,true)
if _icon != null: draw_texture_rect(_icon,Rect2(-21,-21,42,42),false)
func _physics_process(delta: float) -> void:
if not is_instance_valid(_player) or bool(_player.get("_is_destroyed")): return
var distance := global_position.distance_to(_player.global_position)
if distance <= 48:
var progress: Resource = GameState.captain.expedition
if payload.has("map"): progress.distant_maps[str(payload.map.id)] = payload.map.duplicate(true)
if payload.has("orb"):
var id := str(payload.orb)
progress.orbs[id] = int(progress.orbs.get(id,0))+1
progress.emit_changed()
set_physics_process(false)
queue_free()
elif distance < 180: global_position = global_position.move_toward(_player.global_position,200*delta)

View File

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

View File

@ -39,18 +39,7 @@ static func generate_loot(level: int, enemy_tier: int = 0, definition: ItemDefin
static func generate_for_definition(definition: ItemDefinition, level: int, enemy_tier: int = 0) -> ItemInstance: static func generate_for_definition(definition: ItemDefinition, level: int, enemy_tier: int = 0) -> ItemInstance:
var item := ItemInstance.new() var item := ItemInstance.new()
item.definition = definition item.definition = definition
item.item_level = clampi(level, 1, 1000) roll_base_values(item, level)
var scale_factor := 1.0 + 0.05 * (item.item_level - 1)
item.primary_value_override = item.definition.primary_value * scale_factor * randf_range(0.8, 1.2)
if definition.primary_stat == "max_speed":
item.primary_value_override = randf_range(1.0, 5.0) * scale_factor
if definition.category == ItemDefinition.Category.WEAPON:
var lower := randi_range(mini(definition.damage_lower_roll_min, definition.damage_lower_roll_max), maxi(definition.damage_lower_roll_min, definition.damage_lower_roll_max))
var spread := randi_range(mini(definition.damage_spread_roll_min, definition.damage_spread_roll_max), maxi(definition.damage_spread_roll_min, definition.damage_spread_roll_max))
item.damage_min = maxf(1.0, roundf(lower * scale_factor))
item.damage_max = maxf(item.damage_min + 1.0, roundf((lower + spread) * scale_factor))
item.primary_value_override = (item.damage_min + item.damage_max) * 0.5
item.energy_regeneration_override = item.definition.energy_regeneration_bonus * scale_factor * randf_range(0.8, 1.2)
if definition.category == ItemDefinition.Category.ACTIVE_EXTENSION: if definition.category == ItemDefinition.Category.ACTIVE_EXTENSION:
if item.is_passive_extension(): if item.is_passive_extension():
item.passive_value = preload("res://scripts/items/passive_extension_rolls.gd").roll_value(definition.passive_effect) item.passive_value = preload("res://scripts/items/passive_extension_rolls.gd").roll_value(definition.passive_effect)
@ -66,3 +55,43 @@ static func generate_for_definition(definition: ItemDefinition, level: int, enem
item.additional_modifiers = AFFIX_DATABASE.roll_affixes(item.definition, item.item_level, count) item.additional_modifiers = AFFIX_DATABASE.roll_affixes(item.definition, item.item_level, count)
item.rarity = clampi(item.additional_modifiers.size() - 1, 0, 4) item.rarity = clampi(item.additional_modifiers.size() - 1, 0, 4)
return item return item
static func roll_base_values(item: ItemInstance, level: int) -> void:
var definition := item.definition
item.item_level = clampi(level, 1, 1000)
var scale_factor := 1.0 + 0.05 * (item.item_level - 1)
item.primary_value_override = definition.primary_value * scale_factor * randf_range(0.8, 1.2)
if definition.primary_stat == "max_speed":
item.primary_value_override = randf_range(1.0, 5.0) * scale_factor
if definition.category == ItemDefinition.Category.WEAPON:
var lower := randi_range(mini(definition.damage_lower_roll_min, definition.damage_lower_roll_max), maxi(definition.damage_lower_roll_min, definition.damage_lower_roll_max))
var spread := randi_range(mini(definition.damage_spread_roll_min, definition.damage_spread_roll_max), maxi(definition.damage_spread_roll_min, definition.damage_spread_roll_max))
item.damage_min = maxf(1.0, roundf(lower * scale_factor))
item.damage_max = maxf(item.damage_min + 1.0, roundf((lower + spread) * scale_factor))
item.primary_value_override = (item.damage_min + item.damage_max) * 0.5
item.energy_regeneration_override = definition.energy_regeneration_bonus * scale_factor * randf_range(0.8, 1.2)
static func can_advance(item: ItemInstance) -> bool:
return item != null and item.definition != null and item.definition.category != ItemDefinition.Category.CONSUMABLE and item.item_level >= 1 and item.item_level < 1000
static func advance(item: ItemInstance) -> bool:
if not can_advance(item): return false
var level := item.item_level + 1
var updated: Array[ItemStatModifier] = []
for modifier in item.additional_modifiers:
if modifier == null:
updated.append(null)
continue
# Keep shared modifiers on other items untouched.
var copy := modifier.duplicate() as ItemStatModifier
if copy.affix == null:
for candidate in AFFIX_DATABASE.affixes:
if candidate != null and candidate.stat_key == copy.stat_key and candidate.can_roll(item.definition,level):
copy.affix = candidate
break
if copy.affix != null: copy.value = copy.affix.roll_value(level)
# Fixed legacy bonuses without a defined roll range stay fixed.
updated.append(copy)
roll_base_values(item,level)
item.additional_modifiers = updated
return true

View File

@ -7,6 +7,10 @@ const EXTENSION_COOLDOWNS := preload("res://scripts/items/extension_cooldowns.gd
@export_range(5, 30, 1) var extension_cooldown: int = 30 @export_range(5, 30, 1) var extension_cooldown: int = 30
var _instance_ability: ActiveAbility var _instance_ability: ActiveAbility
@export var passive_value: float = -1.0 @export var passive_value: float = -1.0
@export var special_orbs: Array[String] = []
func shield_piercing() -> float:
return 0.5 if definition != null and definition.category == ItemDefinition.Category.WEAPON and "piercing" in special_orbs else 0.0
func is_passive_extension() -> bool: func is_passive_extension() -> bool:
return definition != null and definition.category == ItemDefinition.Category.ACTIVE_EXTENSION and not definition.passive_effect.is_empty() return definition != null and definition.category == ItemDefinition.Category.ACTIVE_EXTENSION and not definition.passive_effect.is_empty()
@ -182,6 +186,9 @@ func get_tooltip_text() -> String:
if modifier == null: if modifier == null:
continue continue
lines.append(modifier.get_display_text()) lines.append(modifier.get_display_text())
for orb in special_orbs:
if orb == "piercing": lines.append("\nORB PRZEBICIA · 50% obrażeń omija tarcze. Tylko ta broń.")
elif orb == "booster": lines.append("\nORB DOPALACZA · TAB bez kosztu energii. Działa po zamontowaniu silnika.")
return "\n".join(lines) return "\n".join(lines)
static func stat_name(key: String) -> String: static func stat_name(key: String) -> String:

View File

@ -0,0 +1,27 @@
extends RefCounted
# Every orb is consumed once. Further advancement requires another orb.
const IDS := ["piercing", "booster", "advancement"]
const NAMES := {"piercing":"Orb przebicia", "booster":"Orb dopalacza", "advancement":"Orb awansu"}
const DESCRIPTIONS := {"piercing":"50% obrażeń broni omija tarcze i trafia bezpośrednio w kadłub. Tylko broń.", "booster":"Dopalacz pod TAB nie zużywa energii i działa również przy pustym generatorze. Tylko silnik.", "advancement":"Awansuje przedmiot o 1 poziom i ponownie losuje jego parametry oraz wartości obecnych addonów. Zachowuje ich typy, liczbę i efekty orbów. Wyniki losowania mogą być niższe. Jedna sztuka daje jeden awans. Kolejny awans wymaga nowego orba (limit poziomu 1000). Nie działa na materiały zużywalne."}
static func icon(id: String) -> Texture2D:
return load("res://assets/icons/orbs/%s.png" % id) as Texture2D
static func accepts(item: ItemInstance, id: String) -> bool:
if id not in IDS or item == null or item.definition == null: return false
if id == "advancement": return preload("res://scripts/items/item_generator.gd").can_advance(item)
if id in item.special_orbs: return false
return item.definition.category == (ItemDefinition.Category.WEAPON if id == "piercing" else ItemDefinition.Category.ENGINE)
static func apply(progress: Resource, inventory: Node, item: ItemInstance, id: String) -> bool:
if not accepts(item,id) or int(progress.orbs.get(id,0)) <= 0: return false
if inventory == null or not bool(inventory.get_parent().get("is_docked")): return false
var owned: Array = inventory.cargo + inventory.weapon_slots + inventory.module_slots + progress.freighter_storage
if not owned.has(item): return false
if id == "advancement":
if not preload("res://scripts/items/item_generator.gd").advance(item): return false
else:
item.special_orbs.append(id)
progress.orbs[id] = int(progress.orbs[id])-1
item.emit_changed()
inventory.call("_recalculate_ship_stats")
inventory.inventory_changed.emit()
progress.emit_changed()
return true

View File

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

View File

@ -0,0 +1,36 @@
extends Node2D
const PICKUP_RADIUS := 48.0
const ATTRACTION_RADIUS := 150.0
const POPUP := preload("res://scenes/game/damage_popup.tscn")
var amount := 0
var _player: CharacterBody2D
var _collected := false
func _ready() -> void:
add_to_group("time_crystal_pickups")
_player = get_parent().get_node_or_null("PlayerShip") as CharacterBody2D
z_index = 18
func _draw() -> void:
for radius in [27.0,22.0,18.0]: draw_circle(Vector2.ZERO,radius,Color(0.42,0.86,1.0,0.07))
var icon := load("res://assets/icons/resources/time_crystals.png") as Texture2D
if icon != null: draw_texture_rect(icon,Rect2(-18,-18,36,36),false)
func _physics_process(delta: float) -> void:
if _collected or not is_instance_valid(_player) or bool(_player.get("_is_destroyed")): return
var distance := global_position.distance_to(_player.global_position)
if distance <= PICKUP_RADIUS:
_collected = true
var gained := maxi(0,amount)
GameState.captain.expedition.time_crystals += gained
GameState.captain.expedition.emit_changed()
GameState.captain.emit_changed()
var popup := POPUP.instantiate() as Label
popup.text = "+%d KRYSZTAŁÓW CZASU" % gained
popup.set("text_color",Color("9be7ff"))
popup.position = _player.position+Vector2(-80,-90)
get_parent().add_child(popup)
set_physics_process(false)
queue_free()
elif distance <= ATTRACTION_RADIUS:
global_position = global_position.move_toward(_player.global_position,180.0*delta)

View File

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

View File

@ -0,0 +1,40 @@
extends RefCounted
const BALANCE := preload("res://scripts/levels/level_balance.gd")
const WORLD_SIZE := Vector2(5200,3600)
static func enemy_tiers(level: int) -> Array[int]:
var result: Array[int] = []
for tier in BALANCE.enemy_tiers(level):
result.append(tier)
if tier != 2: result.append(tier)
return result
static func generate(map: Dictionary) -> Dictionary:
var rng := RandomNumberGenerator.new()
rng.seed = int(map.seed)
var enemies: Array[Dictionary] = []
var index := 0
for tier in enemy_tiers(int(map.level)):
var chosen := Vector2.ZERO
var best := -INF
for attempt in 100:
var candidate := Vector2(rng.randf_range(-2300,2300),rng.randf_range(-1480,1480))
var clearance := candidate.length()-900.0
for other in enemies: clearance = minf(clearance,candidate.distance_to(other.position)-220.0)
if clearance > best:
best = clearance
chosen = candidate
if clearance >= 0: break
enemies.append({"tier":tier,"position":chosen,"texture_index":rng.randi_range(0,preload("res://scripts/items/alien_ship_catalog.gd").TEXTURES.size()-1),"spawn_index":index})
index += 1
var structures: Array[Dictionary] = []
for prop in 7:
var width := rng.randf_range(240,460)
var position := Vector2.ZERO
for attempt in 100:
position = Vector2(rng.randf_range(-2200,2200),rng.randf_range(-1350,1350))
if position.length() < 650: continue
var clear := true
for other in structures:
if position.distance_to(other.position) < (width+float(other.size))*0.5+180: clear = false
if clear: break
structures.append({"position":position,"size":width,"rotation":rng.randf_range(-PI,PI),"name":["Rozbity przekaźnik","Archiwum bez głosu","Brama zapomnianych","Wrak konwoju","Pusta stocznia","Echo dawnej cywilizacji","Cmentarz maszyn"][prop]})
return {"planets":[],"player_start":Vector2.ZERO,"enemies":enemies,"structures":structures}

View File

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

View File

@ -33,7 +33,7 @@ static func enemy_tiers(level: int) -> Array[int]:
return tiers return tiers
static func power_scale(level: int) -> float: static func power_scale(level: int) -> float:
return 1.0 + 0.05 * (clampi(level, 1, MAX_LEVEL) - 1) return 1.0 + 0.05 * (maxi(level, 1) - 1)
static func is_hard(level: int) -> bool: static func is_hard(level: int) -> bool:
return level > NORMAL_MAX_LEVEL return level > NORMAL_MAX_LEVEL

View File

@ -48,6 +48,8 @@ static func generate(level: int, seed_value: int) -> Dictionary:
planets[index].texture = earthlike[texture_rng.randi_range(0, earthlike.size() - 1)] planets[index].texture = earthlike[texture_rng.randi_range(0, earthlike.size() - 1)]
planets[index].earthlike = profiles.is_earthlike(planets[index].texture) planets[index].earthlike = profiles.is_earthlike(planets[index].texture)
planets[index].profile = profiles.generate(seed_value, index, planets[index].name) if bool(planets[index].earthlike) else {} planets[index].profile = profiles.generate(seed_value, index, planets[index].name) if bool(planets[index].earthlike) else {}
if not planets[index].profile.is_empty():
planets[index].profile.system_level = level
var enemies: Array[Dictionary] = [] var enemies: Array[Dictionary] = []
var ship_rng := RandomNumberGenerator.new() var ship_rng := RandomNumberGenerator.new()
ship_rng.seed = seed_value ^ (level * 0x53484950) ship_rng.seed = seed_value ^ (level * 0x53484950)

View File

@ -0,0 +1,33 @@
extends RefCounted
const HERMIT_CHANCE := 0.35
const CREDITS_PER_LEVEL := 1000
const DEUTER_PER_LEVEL := 1000
static func roll(profile: Dictionary, salt: int) -> RandomNumberGenerator:
var rng := RandomNumberGenerator.new()
rng.seed = int(profile.get("seed", 0)) ^ str(profile.get("id", "")).hash() ^ salt
return rng
static func has_hermit(profile: Dictionary) -> bool:
return roll(profile, 0x4845524d).randf() < HERMIT_CHANCE
static func offer(profile: Dictionary, source_level: int) -> Dictionary:
# No upper cap: expedition planets can continue the chain beyond level 120.
var level := maxi(1, source_level) + roll(profile, 0x4d415053).randi_range(1, 5)
return {"id":str(profile.id)+":distant_map", "level":level, "seed":roll(profile, 0x44495354).randi(), "source_level":maxi(1,source_level), "source_planet":str(profile.get("name","Nieznana planeta")), "name":"Odległy sektor %03d" % level}
static func map_cost(map: Dictionary) -> int:
return maxi(1, int(map.level)) * CREDITS_PER_LEVEL
static func fuel_cost(map: Dictionary) -> int:
return maxi(1, int(map.level)) * DEUTER_PER_LEVEL
static func buy(progress: Resource, captain: Resource, profile: Dictionary, source_level: int) -> bool:
if not has_hermit(profile): return false
var map := offer(profile, source_level)
if progress.distant_maps.has(str(map.id)) or captain.credits < map_cost(map): return false
captain.credits -= map_cost(map)
progress.distant_maps[str(map.id)] = map.duplicate(true)
captain.emit_changed()
progress.emit_changed()
return true

View File

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

View File

@ -0,0 +1,46 @@
extends RefCounted
const MENU := preload("res://scripts/planets/freighter_menu.gd")
const DATA := preload("res://scripts/planets/distant_expeditions.gd")
static func build(screen: Control) -> void:
var progress: Resource = screen.captain.expedition
var section := MENU.card(screen._content)
section.name = "DistantExpeditions"
var art := TextureRect.new()
art.texture = load("res://assets/planets/expedition_navigation.png")
art.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
art.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
art.custom_minimum_size.y = 125
art.clip_contents = true
section.add_child(art)
MENU.label(section, "DALEKIE WYPRAWY / ARCHIWUM MAP", 16, Color("e9b4fc"))
MENU.label(section, "Sektory poza znanymi szlakami. Podwójna liczba wrogów, jeden boss. Kryształy czasu z każdego wroga. Zwykli wrogowie: mapy i powerbanki po 15%. Boss zawsze zostawia dokładnie 1 mapę i 1 losowy orb.", 12)
var fuel := MENU.label(section, "", 13, Color("95dce8"))
fuel.name = "ExpeditionFuel"
screen._economy_labels["expedition_fuel"] = fuel
MENU.label(section, "Lot: poziom × 1000 deuteru. Powrót bezpłatny. Mapy pozostają w archiwum i pozwalają wracać do sektora.", 12, MENU.GOLD)
if progress.distant_maps.is_empty():
MENU.label(section, "Archiwum jest puste. Szukaj pustelnika na planecie lub map w łupach z wypraw.", 12, MENU.MUTED)
var maps: Array = progress.distant_maps.values()
maps.sort_custom(func(a,b): return int(a.level)<int(b.level))
for map in maps:
var card := MENU.card(section)
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)
var cost: int = DATA.fuel_cost(map)
MENU.label(card, "Paliwo: %d deuteru" % cost, 12, Color("95dce8"))
var row := MENU.row(card)
var status := MENU.label(row,"",11)
status.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var map_id := str(map.id)
var fly := MENU.button(row,"LEĆ",func(): screen._travel_to_distant(map_id),true)
fly.name = "FlyDistant_"+map_id.md5_text().substr(0,8)
fly.tooltip_text = "Rozpocznij wyprawę. Pobiera %d deuteru." % cost
var refresh_status := func():
var missing := maxf(0.0,cost-progress.deuter)
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

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

View File

@ -269,6 +269,12 @@ func _exit_tree() -> void:
get_tree().paused = _previous_pause get_tree().paused = _previous_pause
get_tree().auto_accept_quit = true get_tree().auto_accept_quit = true
func request_distant_travel(map_id: String) -> bool:
if not is_instance_valid(screen) or GameState.captain.expedition.active: return false
if not game.travel_to_distant(map_id): return false
_leave()
return true
func request_system_travel(level: int) -> bool: func request_system_travel(level: int) -> bool:
if not is_instance_valid(screen) or GameState.captain.expedition.active or level == GameState.system_level or not GameState.can_visit_system(level): return false if not is_instance_valid(screen) or GameState.captain.expedition.active or level == GameState.system_level or not GameState.can_visit_system(level): return false
if not game.travel_to_system(level): return false if not game.travel_to_system(level): return false

View File

@ -22,20 +22,22 @@ static func build(screen: Control) -> void:
travel.disabled = true travel.disabled = true
destination.item_selected.connect(func(_index): travel.disabled = destination.get_selected_id() == GameState.system_level) destination.item_selected.connect(func(_index): travel.disabled = 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)
var summary := MENU.card(screen._content) var summary := MENU.card(screen._content)
MENU.label(summary, "SIEĆ WYDOBYWCZA", 14, MENU.GOLD) MENU.label(summary, "SIEĆ WYDOBYWCZA", 14, MENU.GOLD)
var stock := MENU.label(summary, "", 13, Color("95dce8")) var stock := MENU.label(summary, "", 13, Color("95dce8"))
screen._economy_labels["network"] = stock screen._economy_labels["network"] = stock
MENU.label(summary, "Obsadzona placówka: 1 deuter/min + punkty zarządzania. Dostawy automatyczne do magazynu.", 11, MENU.MUTED) 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) 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(): if progress.mining_stations.is_empty():
MENU.label(summary, "Brak placówek. Szukaj głębokich złóż na planetach (szansa 60%). Budowa: 200 części.", 12) 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: for station_id in progress.mining_stations:
var station: Dictionary = progress.mining_stations[station_id] var station: Dictionary = progress.mining_stations[station_id]
var card := MENU.card(screen._content) var card := MENU.card(screen._content)
MENU.label(card, "%s · SYSTEM %d" % [station.planet_name, station.system], 13, MENU.GOLD) 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", "")), {}) 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 · %.0f deuteru/min" % [member.name, member.management, NETWORK.station_rate(progress, station)], 12) 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 assign_row := MENU.row(card)
var option := OptionButton.new() var option := OptionButton.new()
option.name = "StationCrew_" + str(station_id).md5_text().substr(0, 8) option.name = "StationCrew_" + str(station_id).md5_text().substr(0, 8)
@ -65,7 +67,7 @@ static func cabin(screen: Control) -> void:
MENU.label(card, "Przygotowana dla jednego załoganta. Specjalistów możesz zatrudniać na planetach.", 12) MENU.label(card, "Przygotowana dla jednego załoganta. Specjalistów możesz zatrudniać na planetach.", 12)
return return
MENU.label(card, str(member.name), 15, MENU.GOLD) MENU.label(card, str(member.name), 15, MENU.GOLD)
MENU.label(card, "Zarządzanie: %d / 100\nPremia do wydobycia: +%d deuteru/min\nPochodzenie: %s" % [member.management, member.management, member.planet_name], 12) 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" var assignment := "Bez przydziału"
for station in progress.mining_stations.values(): 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] if str(station.get("crew_id", "")) == str(member.id): assignment = "%s · System %d" % [station.planet_name, station.system]

View File

@ -123,7 +123,7 @@ func _layout() -> void:
func _sync_header() -> void: func _sync_header() -> void:
if is_instance_valid(_wallet): if is_instance_valid(_wallet):
_wallet.text = "%d KREDYTÓW\n%d CZĘŚCI" % [captain.credits,floori(captain.parts)] _wallet.text = "%d KREDYTÓW\n%d CZĘŚCI\n%d KRYSZTAŁÓW CZASU" % [captain.credits,floori(captain.parts),captain.expedition.time_crystals]
func _hover_section(section: String) -> void: func _hover_section(section: String) -> void:
var data: Dictionary = deck.room(section) var data: Dictionary = deck.room(section)
@ -455,11 +455,12 @@ func _build_resource_overview() -> void:
for key in captain.minerals: values[key] = int(captain.minerals[key]) for key in captain.minerals: values[key] = int(captain.minerals[key])
if is_instance_valid(inventory): if is_instance_valid(inventory):
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.samples = captain.expedition.analysis_samples values.samples = captain.expedition.analysis_samples
values.deuter = captain.expedition.deuter 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"}) 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"})
for key in values: for key in values:
var tile := MENU.card(grid) var tile := MENU.card(grid)
tile.get_parent().name = "Stock_"+key tile.get_parent().name = "Stock_"+key
@ -537,6 +538,7 @@ 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)
_label("EKWIPUNEK KAPITANA",13,MENU.GOLD) _label("EKWIPUNEK KAPITANA",13,MENU.GOLD)
var grid := GridContainer.new() var grid := GridContainer.new()
grid.columns = 3 grid.columns = 3
@ -574,6 +576,10 @@ func _travel_to_system(level: int) -> void:
var controller := get_parent().get_parent() var controller := get_parent().get_parent()
if controller.has_method("request_system_travel"): controller.request_system_travel(level) if controller.has_method("request_system_travel"): controller.request_system_travel(level)
func _travel_to_distant(map_id: String) -> void:
var controller := get_parent().get_parent()
if controller.has_method("request_distant_travel"): controller.request_distant_travel(map_id)
func _assign_station(station_id: String, option: OptionButton) -> void: func _assign_station(station_id: String, option: OptionButton) -> void:
preload("res://scripts/planets/mining_network.gd").assign(captain.expedition, station_id, str(option.get_selected_metadata())) preload("res://scripts/planets/mining_network.gd").assign(captain.expedition, station_id, str(option.get_selected_metadata()))
_refresh() _refresh()
@ -586,6 +592,9 @@ func _refresh_live_labels() -> void:
for key in _economy_labels: for key in _economy_labels:
var label: Label = _economy_labels[key] var label: Label = _economy_labels[key]
if not is_instance_valid(label): continue 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 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:

View File

@ -1,6 +1,7 @@
extends RefCounted extends RefCounted
## The network belongs to expedition progress, so it survives visits and system travel. ## The network belongs to expedition progress, so it survives visits and system travel.
const BUILD_COST := 200 const PARTS_PER_RICHNESS := 10
const CREDITS_PER_MANAGEMENT := 1000
const DEPOSIT_CHANCE := 0.60 const DEPOSIT_CHANCE := 0.60
const CREW_CHANCE := 0.40 const CREW_CHANCE := 0.40
const CABINS := ["cabin_1", "cabin_2", "cabin_3", "cabin_4", "cabin_5"] const CABINS := ["cabin_1", "cabin_2", "cabin_3", "cabin_4", "cabin_5"]
@ -15,6 +16,38 @@ static func planet_roll(profile: Dictionary, salt: int) -> RandomNumberGenerator
static func has_deep_deposit(profile: Dictionary) -> bool: static func has_deep_deposit(profile: Dictionary) -> bool:
return planet_roll(profile, 0x44454550).randf() < DEPOSIT_CHANCE return planet_roll(profile, 0x44454550).randf() < DEPOSIT_CHANCE
# Strict level bands prevent early systems from rolling endgame deposits.
static func richness_bounds(system: int) -> Vector2i:
var levels := [1, 20, 40, 60, 80, 100, 120]
var lows := [1, 8, 20, 35, 50, 65, 80]
var highs := [10, 25, 40, 55, 70, 85, 100]
var level := clampi(system, 1, 120)
for index in range(1, levels.size()):
if level <= int(levels[index]):
var weight := float(level - int(levels[index-1])) / float(int(levels[index]) - int(levels[index-1]))
return Vector2i(roundi(lerpf(lows[index-1], lows[index], weight)), roundi(lerpf(highs[index-1], highs[index], weight)))
return Vector2i(80, 100)
static func deposit_richness(profile: Dictionary, system: int) -> int:
var bounds := richness_bounds(system)
return planet_roll(profile, 0x52494348).randi_range(bounds.x, bounds.y)
static func build_cost(profile: Dictionary, system: int) -> int:
return deposit_richness(profile, system) * PARTS_PER_RICHNESS
static func recruitment_cost(candidate: Dictionary) -> int:
return clampi(int(candidate.get("management", 1)), 1, 100) * CREDITS_PER_MANAGEMENT
static func station_richness(station: Dictionary) -> int:
if not station.has("richness"):
# Upgrade existing stations using their original planet identity and system.
var identity := str(station.id).split(":")
var profile := {"id":str(station.id), "seed":int(station.get("planet_seed", 0))}
if identity.size() == 3 and identity[1] == "planet":
profile = preload("res://scripts/planets/planet_profiles.gd").generate(int(identity[0]), int(identity[2]), str(station.planet_name))
station.richness = deposit_richness(profile, int(station.get("system", 1)))
return clampi(int(station.richness), 1, 100)
static func has_recruit(profile: Dictionary) -> bool: static func has_recruit(profile: Dictionary) -> bool:
return planet_roll(profile, 0x43524557).randf() < CREW_CHANCE return planet_roll(profile, 0x43524557).randf() < CREW_CHANCE
@ -34,13 +67,16 @@ static func cabin_crew(progress: Resource, cabin: String) -> Dictionary:
if str(member.cabin) == cabin: return member if str(member.cabin) == cabin: return member
return {} return {}
static func hire(progress: Resource, profile: Dictionary) -> bool: static func hire(progress: Resource, captain: Resource, profile: Dictionary) -> bool:
var candidate := recruit(profile) var candidate := recruit(profile)
var cabin := free_cabin(progress) var cabin := free_cabin(progress)
if not has_recruit(profile) or progress.recruited_planets.has(str(profile.id)) or cabin.is_empty(): return false var cost := recruitment_cost(candidate)
if not has_recruit(profile) or progress.recruited_planets.has(str(profile.id)) or cabin.is_empty() or captain.credits < cost: return false
captain.credits -= cost
candidate.cabin = cabin candidate.cabin = cabin
progress.crew[candidate.id] = candidate progress.crew[candidate.id] = candidate
progress.recruited_planets[str(profile.id)] = true progress.recruited_planets[str(profile.id)] = true
captain.emit_changed()
progress.emit_changed() progress.emit_changed()
return true return true
@ -54,9 +90,10 @@ static func dismiss(progress: Resource, member_id: String) -> bool:
static func build(progress: Resource, captain: Resource, profile: Dictionary, system: int) -> bool: static func build(progress: Resource, captain: Resource, profile: Dictionary, system: int) -> bool:
var id := str(profile.id) var id := str(profile.id)
if not has_deep_deposit(profile) or progress.mining_stations.has(id) or captain.parts < BUILD_COST: return false var cost := build_cost(profile, system)
captain.parts -= BUILD_COST if not has_deep_deposit(profile) or progress.mining_stations.has(id) or captain.parts < cost: return false
progress.mining_stations[id] = {"id":id, "planet_name":str(profile.name), "system":system, "crew_id":"", "produced":0.0} captain.parts -= cost
progress.mining_stations[id] = {"id":id, "planet_name":str(profile.name), "system":system, "planet_seed":int(profile.get("seed", 0)), "richness":deposit_richness(profile, system), "crew_id":"", "produced":0.0}
captain.emit_changed() captain.emit_changed()
progress.emit_changed() progress.emit_changed()
return true return true
@ -74,7 +111,7 @@ static func assign(progress: Resource, station_id: String, member_id: String) ->
static func station_rate(progress: Resource, station: Dictionary) -> float: static func station_rate(progress: Resource, station: Dictionary) -> float:
var id := str(station.get("crew_id", "")) var id := str(station.get("crew_id", ""))
if not progress.crew.has(id): return 0.0 if not progress.crew.has(id): return 0.0
return 1.0 + clampi(int(progress.crew[id].management), 1, 100) return station_richness(station) * (1.0 + clampi(int(progress.crew[id].management), 1, 100) / 100.0)
static func total_rate(progress: Resource) -> float: static func total_rate(progress: Resource) -> float:
var total := 0.0 var total := 0.0

View File

@ -0,0 +1,41 @@
extends RefCounted
const MENU := preload("res://scripts/planets/freighter_menu.gd")
const ORBS := preload("res://scripts/items/special_orbs.gd")
static func build(screen: Control) -> void:
var progress: Resource = screen.captain.expedition
MENU.label(screen._content,"ORBY / SPECJALNE ULEPSZENIA",15,Color("d9b5ff"))
MENU.label(screen._content,"Każdy orb jest jednorazowy i znika po użyciu. Kolejny awans wymaga kolejnej sztuki. Efekty przebicia i dopalacza nie sumują się.",12,MENU.MUTED)
for id in ORBS.IDS:
var card := MENU.card(screen._content)
var header := MENU.row(card)
MENU.icon(header,ORBS.icon(id),48)
var title := MENU.label(header,"%s · %d szt." % [ORBS.NAMES[id],int(progress.orbs.get(id,0))],14,MENU.GOLD)
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
title.custom_minimum_size.x = 180
MENU.label(card,ORBS.DESCRIPTIONS[id],12)
var choices := OptionButton.new()
choices.size_flags_horizontal = Control.SIZE_EXPAND_FILL
choices.fit_to_longest_item = false
choices.add_theme_font_override("font",ThemeDB.fallback_font)
choices.add_theme_font_size_override("font_size",12)
card.add_child(choices)
if is_instance_valid(screen.inventory):
for kind in ["weapon","module","cargo"]:
var items: Array = screen.inventory._get_slots(kind)
for index in items.size():
if not screen.inventory.is_slot_unlocked(kind,index): continue
_add_item(choices,items[index],id,{"weapon":"Broń","module":"Moduł","cargo":"Ładownia"}[kind])
for item in progress.freighter_storage: _add_item(choices,item,id,"Magazyn fregaty")
if choices.item_count == 0: choices.add_item("Brak odpowiedniego przedmiotu")
var apply := MENU.button(card,"AWANSUJ +1 POZIOM" if id == "advancement" else "NAŁÓŻ ORB",func():
var target: ItemInstance = choices.get_selected_metadata()
if ORBS.apply(progress,screen.inventory,target,id):
screen._audio.service_completed(-0.48)
screen._refresh()
,true)
apply.name = "ApplyOrb_"+id
apply.disabled = int(progress.orbs.get(id,0)) <= 0 or choices.get_selected_metadata() == null
static func _add_item(choices: OptionButton, item: ItemInstance, orb: String, location: String) -> void:
if not ORBS.accepts(item,orb): return
choices.add_item("%s · poz. %d · %s" % [item.definition.display_name,item.item_level,location])
choices.set_item_metadata(choices.item_count-1,item)

View File

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

View File

@ -10,6 +10,9 @@ const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
@export var alien_skills: Dictionary = {} @export var alien_skills: Dictionary = {}
@export var freighter_rooms: Dictionary = {"expansion_3":"module_storage"} @export var freighter_rooms: Dictionary = {"expansion_3":"module_storage"}
@export var mining_stations: Dictionary = {} @export var mining_stations: Dictionary = {}
@export var distant_maps: Dictionary = {}
@export var orbs: Dictionary = {}
@export var time_crystals: int = 0
@export var crew: Dictionary = {} @export var crew: Dictionary = {}
@export var recruited_planets: Dictionary = {} @export var recruited_planets: Dictionary = {}
@export var deuter := 0.0 @export var deuter := 0.0

View File

@ -1,5 +1,5 @@
extends RefCounted extends RefCounted
const NAMES := {"deposit":"Złoże surowców", "ruins":"Starożytna struktura", "warehouse":"Opuszczony magazyn", "village":"Opuszczona wioska", "cave":"Jaskinia", "deep_deposit":"Głębokie złoże deuteru", "crew":"Obóz specjalisty"} const NAMES := {"deposit":"Złoże surowców", "ruins":"Starożytna struktura", "warehouse":"Opuszczony magazyn", "village":"Opuszczona wioska", "cave":"Jaskinia", "deep_deposit":"Głębokie złoże deuteru", "crew":"Domek specjalisty", "hermit":"Pustelnik / Kartograf", "time_engineer":"Inżynier chronalny"}
const STAGES := { const STAGES := {
"deposit":["Zbierz odsłonięte minerały", "Wykonaj odwiert", "Wydobądź głęboką żyłę"], "deposit":["Zbierz odsłonięte minerały", "Wykonaj odwiert", "Wydobądź głęboką żyłę"],
"ruins":["Przeszukaj przedsionek", "Otwórz komorę", "Zbadaj rdzeń"], "ruins":["Przeszukaj przedsionek", "Otwórz komorę", "Zbadaj rdzeń"],

View File

@ -0,0 +1,34 @@
extends Sprite2D
# A recognizable inhabited home, distinct from abandoned surface structures.
func _ready() -> void:
texture = null
scale = Vector2.ONE
queue_redraw()
func _draw() -> void:
draw_ellipse_shadow()
draw_style_box(_walls(), Rect2(-39,-23,78,63))
draw_colored_polygon(PackedVector2Array([Vector2(-51,-21),Vector2(0,-62),Vector2(51,-21)]), Color("426b69"))
draw_polyline(PackedVector2Array([Vector2(-51,-21),Vector2(0,-62),Vector2(51,-21)]), Color("91b9ac"), 3, true)
draw_rect(Rect2(-10,6,20,34),Color("203c3c"))
draw_circle(Vector2(4,25),2,Color("eaca8a"))
for x in [-28,17]:
draw_rect(Rect2(x,-7,14,17),Color("f0d592"))
draw_line(Vector2(x+7,-7),Vector2(x+7,10),Color("526d62"),2)
draw_rect(Rect2(24,-56,9,24),Color("647a70"))
draw_line(Vector2(60,-15),Vector2(60,42),Color("7fada1"),3)
draw_colored_polygon(PackedVector2Array([Vector2(62,-15),Vector2(84,-8),Vector2(62,0)]),Color("a7e5a2"))
draw_circle(Vector2(60,-17),4,Color("b9f1b2"))
draw_rect(Rect2(-14,40,28,8),Color("778b7e"))
func _walls() -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = Color("879685")
style.border_color = Color("364e47")
style.set_border_width_all(3)
return style
func draw_ellipse_shadow() -> void:
draw_set_transform(Vector2(7,40),0,Vector2(1,0.35))
draw_circle(Vector2.ZERO,60,Color(0.02,0.06,0.04,0.3))
draw_set_transform(Vector2.ZERO)

View File

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

View File

@ -1,6 +1,6 @@
extends Node2D extends Node2D
## Ground wash stays in world space after emission, independently of the lander. ## Ground wash stays in world space after emission, independently of the lander.
const LIMIT := 144 const LIMIT := 220
var source: Control var source: Control
var weather: Node2D var weather: Node2D
var _rng := RandomNumberGenerator.new() var _rng := RandomNumberGenerator.new()
@ -68,8 +68,8 @@ func _process(delta: float) -> void:
var brake: bool = not source._dialog.visible and Input.is_action_pressed("ship_brake") and source._motion.velocity.length() > 12 var brake: bool = not source._dialog.visible and Input.is_action_pressed("ship_brake") and source._motion.velocity.length() > 12
var boost: bool = source._motion.boost_active and thrust var boost: bool = source._motion.boost_active and thrust
if thrust or brake: if thrust or brake:
var rate := 60.0 if boost else (26.0 if thrust else 18.0) var rate := 90.0 if boost else (46.0 if thrust else 30.0)
if _surface_kind == "rock": rate *= 0.3 if _surface_kind == "rock": rate *= 0.45
_emission += delta*rate _emission += delta*rate
while _emission >= 1: while _emission >= 1:
_emission -= 1 _emission -= 1
@ -87,8 +87,8 @@ func _emit(point: Vector2, angle: float, boost: bool) -> void:
var kind := _surface_kind var kind := _surface_kind
if _near_trees and kind == "dust" and _rng.randf() < 0.20: kind = "leaf" if _near_trees and kind == "dust" and _rng.randf() < 0.20: kind = "leaf"
var nozzle := Vector2(_rng.randf_range(-29,29),_rng.randf_range(22,42)).rotated(angle) var nozzle := Vector2(_rng.randf_range(-29,29),_rng.randf_range(22,42)).rotated(angle)
var scatter := Vector2(_rng.randf_range(-32,32),_rng.randf_range(10,45)).rotated(angle) var scatter := Vector2(_rng.randf_range(-48,48),_rng.randf_range(20,65)).rotated(angle)
_particles.append({"position":point+nozzle,"velocity":scatter+source._motion.velocity*0.1,"age":0.0,"life":_rng.randf_range(1.1,2.4),"size":_rng.randf_range(9,17)*(1.4 if boost else 1.0),"kind":kind,"angle":_rng.randf_range(-PI,PI)}) _particles.append({"position":point+nozzle,"velocity":scatter+source._motion.velocity*0.1,"age":0.0,"life":_rng.randf_range(1.5,2.8),"size":_rng.randf_range(15,25)*(1.55 if boost else 1.0),"kind":kind,"angle":_rng.randf_range(-PI,PI)})
func _draw() -> void: func _draw() -> void:
for particle in _particles: for particle in _particles:
@ -97,10 +97,10 @@ func _draw() -> void:
var kind: String = particle.kind var kind: String = particle.kind
var radius: float = particle.size*(1.0+t*1.9) var radius: float = particle.size*(1.0+t*1.9)
var color := _ground var color := _ground
color.a = alpha*0.14 color.a = alpha*0.32
if kind == "rock": color = Color(0.65,0.65,0.59,alpha*0.09) if kind == "rock": color = Color(0.65,0.65,0.59,alpha*0.19)
if kind == "snow": color = Color(0.88,0.95,0.95,alpha*0.25) if kind == "snow": color = Color(0.88,0.95,0.95,alpha*0.38)
if kind == "spray": color = Color(0.66,0.83,0.80,alpha*0.18) if kind == "spray": color = Color(0.66,0.83,0.80,alpha*0.30)
if kind == "leaf": if kind == "leaf":
draw_set_transform(particle.position,particle.angle+t*2,Vector2(1,0.45)) draw_set_transform(particle.position,particle.angle+t*2,Vector2(1,0.45))
draw_circle(Vector2.ZERO,2.5,Color(0.49,0.35,0.17,alpha*0.7),true,-1,true) draw_circle(Vector2.ZERO,2.5,Color(0.49,0.35,0.17,alpha*0.7),true,-1,true)

View File

@ -61,6 +61,8 @@ static func generate(seed_value: int, profile: Dictionary) -> Dictionary:
var network := preload("res://scripts/planets/mining_network.gd") var network := preload("res://scripts/planets/mining_network.gd")
if network.has_deep_deposit(profile): kinds.append("deep_deposit") if network.has_deep_deposit(profile): kinds.append("deep_deposit")
if network.has_recruit(profile): kinds.append("crew") if network.has_recruit(profile): kinds.append("crew")
if preload("res://scripts/planets/distant_expeditions.gd").has_hermit(profile): kinds.append("hermit")
if preload("res://scripts/planets/time_engineer.gd").has_engineer(profile): kinds.append("time_engineer")
for index in kinds.size(): for index in kinds.size():
var kind: String = kinds[index] var kind: String = kinds[index]
var chosen := Vector2.ZERO var chosen := Vector2.ZERO

View File

@ -21,8 +21,20 @@ func _draw() -> void:
if poi.discovered: if poi.discovered:
var at: Vector2 = (poi.position / total + Vector2.ONE * 0.5) * size var at: Vector2 = (poi.position / total + Vector2.ONE * 0.5) * size
var color := Color("708377") if poi.complete else Color("f4cb80") var color := Color("708377") if poi.complete else Color("f4cb80")
if poi.kind == "time_engineer":
color = Color("9be7ff")
draw_colored_polygon(PackedVector2Array([at+Vector2(0,-4),at+Vector2(4,0),at+Vector2(0,4),at+Vector2(-4,0)]),color)
continue
if poi.kind == "hermit":
color = Color("e9b4fc")
draw_rect(Rect2(at-Vector2(3,3),Vector2(6,6)),color,false,1.5)
continue
if poi.kind == "deep_deposit": color = Color("84dff3") if poi.kind == "deep_deposit": color = Color("84dff3")
elif poi.kind == "crew" and not poi.complete: color = Color("a7e5a2") elif poi.kind == "crew" and not poi.complete:
color = Color("a7e5a2")
draw_colored_polygon(PackedVector2Array([at+Vector2(-4,0),at+Vector2(0,-5),at+Vector2(4,0)]), color)
draw_rect(Rect2(at+Vector2(-3,0),Vector2(6,4)),color)
continue
draw_circle(at, 3.0 if poi.kind == "deep_deposit" else 2.5, color) draw_circle(at, 3.0 if poi.kind == "deep_deposit" else 2.5, color)
for event in world.data.events: for event in world.data.events:
if not event.complete: if not event.complete:

View File

@ -19,6 +19,8 @@ var _status: Label
var _dialog: PanelContainer var _dialog: PanelContainer
var _choices: VBoxContainer var _choices: VBoxContainer
var _poi: Dictionary = {} var _poi: Dictionary = {}
var _engineer_item: ItemInstance
var _engineer_addon := 0
var _rng := RandomNumberGenerator.new() var _rng := RandomNumberGenerator.new()
var _motion := preload("res://scripts/planets/lander_motion.gd").new() var _motion := preload("res://scripts/planets/lander_motion.gd").new()
var flight_ship: Node var flight_ship: Node
@ -228,6 +230,12 @@ func _show_poi(message := "") -> void:
POI_CARD.panel_style(_dialog) POI_CARD.panel_style(_dialog)
_choices.add_theme_constant_override("separation",8) _choices.add_theme_constant_override("separation",8)
_layout_dialog() _layout_dialog()
if _poi.kind == "time_engineer":
preload("res://scripts/planets/time_engineer_panel.gd").build(self,message)
return
if _poi.kind == "hermit":
_show_hermit(message)
return
if _poi.kind in ["deep_deposit", "crew"]: if _poi.kind in ["deep_deposit", "crew"]:
_show_colony_poi(message) _show_colony_poi(message)
return return
@ -415,31 +423,40 @@ func _show_colony_poi(message: String) -> void:
art.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED art.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
art.custom_minimum_size.y = 170 art.custom_minimum_size.y = 170
_choices.add_child(art) _choices.add_child(art)
POI_CARD.text(_choices, "GŁĘBOKIE ZŁOŻE / DEUTER" if is_deposit else "SPECJALISTA / REKRUTACJA", 15, POI_CARD.GOLD) POI_CARD.text(_choices, "GŁĘBOKIE ZŁOŻE / DEUTER" if is_deposit else "DOMEK SPECJALISTY / REKRUTACJA", 15, POI_CARD.GOLD)
if not message.is_empty(): POI_CARD.text(_choices, message, 13, POI_CARD.GOLD) if not message.is_empty(): POI_CARD.text(_choices, message, 13, POI_CARD.GOLD)
if is_deposit: if is_deposit:
var id := str(progress.active_profile.id) var id := str(progress.active_profile.id)
var richness: int = network.deposit_richness(progress.active_profile, int(progress.active_profile.get("system_level", GameState.system_level)))
if progress.mining_stations.has(id):
richness = network.station_richness(progress.mining_stations[id])
POI_CARD.text(_choices, "Zasobność: %d / 100 · złoże niewyczerpywalne" % richness, 14, POI_CARD.GOLD)
POI_CARD.text(_choices, "Z załogantem: %.1f–%.1f deuteru/min (zarządzanie 1–100)." % [richness * 1.01, richness * 2.0], 12)
if progress.mining_stations.has(id): if progress.mining_stations.has(id):
var station: Dictionary = progress.mining_stations[id] var station: Dictionary = progress.mining_stations[id]
POI_CARD.text(_choices, "Placówka zbudowana · %.0f deuteru/min" % network.station_rate(progress, station), 13) POI_CARD.text(_choices, "Placówka zbudowana · %.1f deuteru/min" % network.station_rate(progress, station), 13)
POI_CARD.text(_choices, "Przydziel załoganta w centrum dowodzenia fregaty. Deuter trafia automatycznie do magazynu, również gdy jesteś poza tą planetą.", 12) POI_CARD.text(_choices, "Przydziel załoganta w centrum dowodzenia fregaty. Deuter trafia automatycznie do magazynu, również gdy jesteś poza tą planetą.", 12)
else: else:
POI_CARD.text(_choices, "Pod powierzchnią znajduje się głęboki pokład deuteru. Placówka wymaga 200 części i załoganta do zarządzania.", 13) var cost: int = network.build_cost(progress.active_profile, int(progress.active_profile.get("system_level", GameState.system_level)))
POI_CARD.text(_choices, "Bazowo 1 deuter/min + punkty zarządzania załoganta. Części: %d / 200" % int(captain.parts), 12) POI_CARD.text(_choices, "Budowa kosztuje zasobność × 10 części. Do wydobycia potrzebny jest załogant.", 13)
var build := POI_CARD.button(_choices, "ZBUDUJ PLACÓWKĘ · 200 CZĘŚCI", _build_outpost, true) POI_CARD.text(_choices, "Wydobycie = zasobność × (1 + zarządzanie / 100). Bez załoganta: 0. Części: %d / %d" % [int(captain.parts), cost], 12)
var build := POI_CARD.button(_choices, "ZBUDUJ PLACÓWKĘ · %d CZĘŚCI" % cost, _build_outpost, true)
build.name = "BuildMiningOutpost" build.name = "BuildMiningOutpost"
build.disabled = captain.parts < network.BUILD_COST build.disabled = captain.parts < cost
else: else:
POI_CARD.text(_choices, "Mieszkaniec domku szuka pracy. Obejrzyj umiejętności i zdecyduj — możesz wrócić później. Koszt zatrudnienia: 1000 kredytów × zarządzanie.", 12)
var candidate: Dictionary = network.recruit(progress.active_profile) var candidate: Dictionary = network.recruit(progress.active_profile)
POI_CARD.text(_choices, "%s\nZarządzanie: %d / 100\nPremia do wydobycia: +%d deuteru/min" % [candidate.name, candidate.management, candidate.management], 14) var hire_cost: int = network.recruitment_cost(candidate)
POI_CARD.text(_choices, "Rekrutacja: %d kredytów · posiadasz: %d" % [hire_cost, captain.credits], 13, POI_CARD.GOLD)
POI_CARD.text(_choices, "%s\nZarządzanie: %d / 100\nPremia do wydobycia: +%d%%" % [candidate.name, candidate.management, candidate.management], 14)
if progress.recruited_planets.has(str(progress.active_profile.id)): if progress.recruited_planets.has(str(progress.active_profile.id)):
POI_CARD.text(_choices, "Specjalista został już zatrudniony.", 12) POI_CARD.text(_choices, "Specjalista został już zatrudniony.", 12)
else: else:
var cabin: String = network.free_cabin(progress) var cabin: String = network.free_cabin(progress)
POI_CARD.text(_choices, "Brak wolnej, zbudowanej kajuty. Przygotuj pokój na fregacie." if cabin.is_empty() else "Zakwaterowanie: Kajuta %02d" % int(cabin.trim_prefix("cabin_")), 12) POI_CARD.text(_choices, "Brak wolnej, zbudowanej kajuty. Przygotuj pokój na fregacie." if cabin.is_empty() else "Zakwaterowanie: Kajuta %02d" % int(cabin.trim_prefix("cabin_")), 12)
var hire := POI_CARD.button(_choices, "ZATRUDNIJ ZAŁOGANTA", _hire_crew, true) var hire := POI_CARD.button(_choices, "ZATRUDNIJ · %d KREDYTÓW" % hire_cost, _hire_crew, true)
hire.name = "HireCrew" hire.name = "HireCrew"
hire.disabled = cabin.is_empty() hire.disabled = cabin.is_empty() or captain.credits < hire_cost
POI_CARD.button(_choices, "Wróć do wyprawy", func(): _dialog.hide()) POI_CARD.button(_choices, "Wróć do wyprawy", func(): _dialog.hide())
func _build_outpost() -> void: func _build_outpost() -> void:
@ -450,9 +467,52 @@ func _build_outpost() -> void:
_show_poi("Placówka gotowa. Przydziel operatora w centrum dowodzenia.") _show_poi("Placówka gotowa. Przydziel operatora w centrum dowodzenia.")
else: _show_poi("Nie można zbudować placówki. Sprawdź liczbę części.") else: _show_poi("Nie można zbudować placówki. Sprawdź liczbę części.")
func _maximize_addon() -> void:
if _ending or not progress.active or _poi.kind != "time_engineer" or not is_instance_valid(flight_ship): return
if lander.position.distance_to(_poi.position) > 150: return
var service := preload("res://scripts/planets/time_engineer.gd")
if service.maximize(progress,flight_ship.get_node("ShipInventory"),_engineer_item,_engineer_addon):
captain.emit_changed()
_show_poi("Wybrany addon ma teraz maksymalną wartość dla obecnego poziomu przedmiotu.")
else:
_show_poi("Brakuje kryształów, addon jest już maksymalny lub przedmiot jest niedostępny.")
func _show_hermit(message: String) -> void:
var expeditions := preload("res://scripts/planets/distant_expeditions.gd")
var profile: Dictionary = progress.active_profile
var map: Dictionary = expeditions.offer(profile, int(profile.get("system_level", GameState.system_level)))
var art := TextureRect.new()
art.texture = load("res://assets/planets/props/hermit_cartographer.png")
art.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
art.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
art.custom_minimum_size.y = 175
_choices.add_child(art)
POI_CARD.text(_choices, "PUSTELNIK / KARTOGRAF", 18, Color("e9b4fc"))
POI_CARD.text(_choices, "W zaciszu chaty pustelnik przechowuje współrzędne miejsc poza znanymi szlakami. Oferuje ci jedną ze swoich map.", 13)
if not message.is_empty(): POI_CARD.text(_choices, message, 13, POI_CARD.GOLD)
POI_CARD.text(_choices, "%s · poziom wyprawy %d" % [map.name, map.level], 15, POI_CARD.GOLD)
POI_CARD.text(_choices, "Mapa: %d kredytów\nPaliwo na lot: %d deuteru\nPosiadasz: %d kredytów" % [expeditions.map_cost(map),expeditions.fuel_cost(map),captain.credits], 13)
if progress.distant_maps.has(str(map.id)):
POI_CARD.text(_choices, "Mapa jest już w archiwum na frachtowcu.", 13)
else:
var buy := POI_CARD.button(_choices, "KUP MAPĘ · %d KREDYTÓW" % expeditions.map_cost(map), _buy_distant_map, true)
buy.name = "BuyDistantMap"
buy.disabled = captain.credits < expeditions.map_cost(map)
POI_CARD.button(_choices, "Wróć do wyprawy", func(): _dialog.hide())
func _buy_distant_map() -> void:
if _ending or not progress.active or _poi.kind != "hermit": return
var profile: Dictionary = progress.active_profile
if preload("res://scripts/planets/distant_expeditions.gd").buy(progress,captain,profile,int(profile.get("system_level",GameState.system_level))):
world.refresh_pois(lander.position)
_show_poi("Mapa zapisana w archiwum dalekich wypraw na frachtowcu.")
else:
_show_poi("Mapa została już kupiona lub brakuje kredytów.")
func _hire_crew() -> void: func _hire_crew() -> void:
if _ending or not progress.active or _poi.kind != "crew": return if _ending or not progress.active or _poi.kind != "crew": return
if preload("res://scripts/planets/mining_network.gd").hire(progress, progress.active_profile): if preload("res://scripts/planets/mining_network.gd").hire(progress, captain, progress.active_profile):
_poi.complete = true _poi.complete = true
_show_poi("Załogant zajął wolną kajutę na fregacie.") world.refresh_pois(lander.position)
else: _show_poi("Zatrudnienie wymaga wolnej, zbudowanej kajuty.") _show_poi("Załogant zajął wolną kajutę na fregacie. Przydziel go do stacji w centrum dowodzenia.")
else: _show_poi("Zatrudnienie wymaga wolnej, zbudowanej kajuty i kredytów: 1000 × zarządzanie.")

View File

@ -69,12 +69,14 @@ func _ready() -> void:
sprite.set_script(preload("res://scripts/planets/surface_cave.gd")) sprite.set_script(preload("res://scripts/planets/surface_cave.gd"))
sprite.configure(data,poi) sprite.configure(data,poi)
else: else:
var textures := {"ruins":"ruins", "warehouse":"props/warehouse", "village":"landmarks/abandoned_hamlet", "crew":"props/engineer_camp"} var textures := {"ruins":"ruins", "warehouse":"props/warehouse", "village":"landmarks/abandoned_hamlet", "crew":"props/engineer_camp", "hermit":"props/hermit_cartographer", "time_engineer":"props/time_engineer"}
sprite.texture = load("res://assets/planets/%s.png" % textures[poi.kind]) sprite.texture = load("res://assets/planets/%s.png" % textures[poi.kind])
sprite.position = poi.position sprite.position = poi.position
if sprite.texture != null and poi.kind != "cave": if sprite.texture != null and poi.kind != "cave":
sprite.scale = Vector2.ONE * 125.0*BUILDING_SCALE / maxf(sprite.texture.get_width(), sprite.texture.get_height()) sprite.scale = Vector2.ONE * 125.0*BUILDING_SCALE / maxf(sprite.texture.get_width(), sprite.texture.get_height())
sprite.z_index = 10 sprite.z_index = 10
if poi.kind == "crew":
sprite.set_script(preload("res://scripts/planets/specialist_house.gd"))
add_child(sprite) add_child(sprite)
_poi_sprites[poi.id] = sprite _poi_sprites[poi.id] = sprite
var label := Label.new() var label := Label.new()
@ -148,9 +150,21 @@ func refresh_pois(lander_position: Vector2) -> void:
sprite.rotation = 0 sprite.rotation = 0
sprite.scale = Vector2.ONE * 185.0 / maxf(sprite.texture.get_width(), sprite.texture.get_height()) sprite.scale = Vector2.ONE * 185.0 / maxf(sprite.texture.get_width(), sprite.texture.get_height())
sprite.set_meta("outpost", true) sprite.set_meta("outpost", true)
label.text = "Placówka górnicza" if built else "Głębokie złoże deuteru" var network := preload("res://scripts/planets/mining_network.gd")
var richness: int = network.station_richness(GameState.captain.expedition.mining_stations[str(data.profile.id)]) if built else network.deposit_richness(data.profile, int(data.profile.get("system_level", GameState.system_level)))
label.text = ("%s · %d/100" % ["Placówka górnicza" if built else "Głębokie złoże deuteru", richness])
if lander_position.distance_to(poi.position) <= 150: label.text += "\n[F] Placówka" if built else "\n[F] Buduj" if lander_position.distance_to(poi.position) <= 150: label.text += "\n[F] Placówka" if built else "\n[F] Buduj"
continue continue
if poi.kind == "time_engineer":
label.text = "Inżynier chronalny" + ("\n[F] Maksymalizuj addon" if lander_position.distance_to(poi.position) <= 150 else "")
continue
if poi.kind == "hermit":
var owned: bool = GameState.captain.expedition.distant_maps.has(str(data.profile.id)+":distant_map")
label.text = "Pustelnik / Kartograf" + (" · mapa kupiona" if owned else ("\n[F] Kup mapę" if lander_position.distance_to(poi.position) <= 150 else ""))
continue
if poi.kind == "crew":
label.text = "Domek specjalisty" + (" · zatrudniono" if poi.complete else ("\n[F] Porozmawiaj" if lander_position.distance_to(poi.position) <= 150 else ""))
continue
var title: String = preload("res://scripts/planets/poi_rules.gd").NAMES[poi.kind] var title: String = preload("res://scripts/planets/poi_rules.gd").NAMES[poi.kind]
label.text = title + ((" · zatrudniono" if poi.kind == "crew" else " · zbadano") if poi.complete else ("\n[F] Zbadaj" if lander_position.distance_to(poi.position) <= 150 else "")) label.text = title + ((" · zatrudniono" if poi.kind == "crew" else " · zbadano") if poi.complete else ("\n[F] Zbadaj" if lander_position.distance_to(poi.position) <= 150 else ""))

View File

@ -0,0 +1,46 @@
extends RefCounted
const CHANCE := 0.30
const CRYSTALS_PER_LEVEL := 10
const AFFIXES := preload("res://resources/items/affix_database.tres")
static func has_engineer(profile: Dictionary) -> bool:
var rng := RandomNumberGenerator.new()
rng.seed = int(profile.get("seed",0)) ^ str(profile.get("id","")).hash() ^ 0x54494d45
return rng.randf() < CHANCE
static func cost(item: ItemInstance) -> int:
return maxi(1,item.item_level)*CRYSTALS_PER_LEVEL if item != null else 0
static func affix_for(item: ItemInstance, addon: int) -> ItemAffixDefinition:
if item == null or item.definition == null or addon < 0 or addon >= item.additional_modifiers.size(): return null
var modifier: ItemStatModifier = item.additional_modifiers[addon]
if modifier == null: return null
if modifier.affix != null: return modifier.affix
for candidate in AFFIXES.affixes:
if candidate != null and candidate.stat_key == modifier.stat_key and candidate.can_roll(item.definition,item.item_level): return candidate
return null
static func maximum(item: ItemInstance, addon: int) -> float:
var affix := affix_for(item,addon)
return affix.get_value_range(item.item_level).y if affix != null else 0.0
static func can_maximize(item: ItemInstance, addon: int) -> bool:
var affix := affix_for(item,addon)
if affix == null: return false
return item.additional_modifiers[addon].value < maximum(item,addon) - 0.000001
static func maximize(progress: Resource, inventory: Node, item: ItemInstance, addon: int) -> bool:
if not progress.active or inventory == null or not bool(inventory.get_parent().get("is_docked")): return false
if not can_maximize(item,addon) or progress.time_crystals < cost(item): return false
var owned: Array = inventory.cargo + inventory.weapon_slots + inventory.module_slots + inventory.extension_slots
if not owned.has(item): return false
var modifier := item.additional_modifiers[addon].duplicate() as ItemStatModifier
modifier.affix = affix_for(item,addon)
modifier.value = maximum(item,addon)
item.additional_modifiers[addon] = modifier
progress.time_crystals -= cost(item)
item.emit_changed()
inventory.call("_recalculate_ship_stats")
inventory.inventory_changed.emit()
progress.emit_changed()
return true

View File

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

View File

@ -0,0 +1,103 @@
extends RefCounted
const CARD := preload("res://scripts/planets/poi_presentation.gd")
const SERVICE := preload("res://scripts/planets/time_engineer.gd")
const CYAN := Color("9be7ff")
static func build(screen: Control, message: String = "") -> void:
var parent: VBoxContainer = screen._choices
var progress: Resource = screen.progress
var art := TextureRect.new()
art.texture = load("res://assets/planets/props/time_engineer.png")
art.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
art.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
art.custom_minimum_size.y = 125
parent.add_child(art)
CARD.text(parent,"INŻYNIER CHRONALNY",18,CYAN)
CARD.text(parent,"Wybierz przedmiot i konkretny addon. Inżynier ustawi tylko jego wartość na maksimum dla obecnego poziomu przedmiotu.",12)
if not message.is_empty(): CARD.text(parent,message,12,CARD.GOLD)
CARD.text(parent,"Kryształy czasu: %d" % progress.time_crystals,14,CYAN)
CARD.text(parent,"Koszt: 10 kryształów × poziom przedmiotu. Gotowe maksima nie pobierają opłaty.",12)
if not is_instance_valid(screen.flight_ship):
CARD.text(parent,"Brak połączenia z wyposażeniem statku.",12)
_back(parent,screen)
return
var inventory: Node = screen.flight_ship.get_node("ShipInventory")
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():
CARD.text(parent,"Brak przedmiotów z addonami na statku. Zabierz wyposażenie z magazynu fregaty przed lądowaniem.",12,CARD.GOLD)
_back(parent,screen)
return
if not items.has(screen._engineer_item):
screen._engineer_item = items[0]
screen._engineer_addon = 0
CARD.text(parent,"PRZEDMIOT",11,CARD.MUTED)
var choice := _choice(parent,"EngineerItem")
for index in items.size():
var item: ItemInstance = items[index]
choice.add_item("%s · poz. %d · %s" % [item.definition.display_name,item.item_level,locations[index]])
choice.set_item_metadata(index,item)
choice.select(items.find(screen._engineer_item))
choice.item_selected.connect(func(index):
screen._engineer_item = choice.get_item_metadata(index)
screen._engineer_addon = 0
screen._show_poi()
)
var item: ItemInstance = screen._engineer_item
CARD.text(parent,"ADDON DO MAKSYMALIZACJI",11,CARD.MUTED)
var addons := _choice(parent,"EngineerAddon")
var selected := 0
for index in item.additional_modifiers.size():
var modifier: ItemStatModifier = item.additional_modifiers[index]
if modifier == null: continue
var affix := SERVICE.affix_for(item,index)
var suffix := " · brak zakresu" if affix == null else (" · MAX" if not SERVICE.can_maximize(item,index) else "")
addons.add_item(modifier.get_display_text()+suffix)
var option := addons.item_count-1
addons.set_item_metadata(option,index)
if index == screen._engineer_addon: selected = option
if addons.item_count == 0:
CARD.text(parent,"Brak dostępnych addonów.",12)
_back(parent,screen)
return
addons.select(selected)
screen._engineer_addon = int(addons.get_item_metadata(selected))
addons.item_selected.connect(func(index):
screen._engineer_addon = int(addons.get_item_metadata(index))
screen._show_poi()
)
var index: int = screen._engineer_addon
var affix := SERVICE.affix_for(item,index)
if affix != null:
var multiplier := ItemInstance.ENERGY_REGENERATION_BONUS_MULTIPLIER if item.additional_modifiers[index].stat_key == "energy_regeneration" else 1.0
CARD.text(parent,"Obecnie: %.2f %s → maksimum: %.2f %s" % [item.additional_modifiers[index].value*multiplier,affix.unit,SERVICE.maximum(item,index)*multiplier,affix.unit],13,CARD.GOLD)
var cost := SERVICE.cost(item)
var ready := SERVICE.can_maximize(item,index)
var apply := CARD.button(parent,"MAKSYMALIZUJ · %d KRYSZTAŁÓW" % cost,screen._maximize_addon,true)
apply.name = "MaximizeSelectedAddon"
apply.disabled = not ready or progress.time_crystals < cost
if not ready:
CARD.text(parent,"Ten addon ma już maksymalną wartość lub nie posiada zakresu losowania.",12,CARD.MUTED)
elif progress.time_crystals < cost:
CARD.text(parent,"Brakuje %d kryształów czasu." % (cost-progress.time_crystals),12,CARD.GOLD)
_back(parent,screen)
static func _choice(parent: Node, node_name: String) -> OptionButton:
var choice := OptionButton.new()
choice.name = node_name
choice.size_flags_horizontal = Control.SIZE_EXPAND_FILL
choice.fit_to_longest_item = false
choice.add_theme_font_override("font",ThemeDB.fallback_font)
choice.add_theme_font_size_override("font_size",12)
parent.add_child(choice)
return choice
static func _back(parent: Node, screen: Control) -> void:
CARD.button(parent,"Wróć do wyprawy",func(): screen._dialog.hide())

View File

@ -0,0 +1 @@
uid://42o40yk3604n

View File

@ -34,7 +34,7 @@ func _process(_delta: float) -> void:
if not is_instance_valid(game): if not is_instance_valid(game):
return return
if bool(game.get("system_cleared")): if bool(game.get("system_cleared")):
_objective.text = "Znajdź zamieszkaną planetę" _objective.text = "Zbierz łup i wróć do frachtowca" if not GameState.active_distant_map.is_empty() else "Znajdź zamieszkaną planetę"
elif bool(game.get("_boss_spawned")): elif bool(game.get("_boss_spawned")):
_objective.text = "Pokonaj bossa" _objective.text = "Pokonaj bossa"
else: else:

View File

@ -0,0 +1,76 @@
@tool
extends Node
const GENERATOR := preload("res://scripts/items/item_generator.gd")
const ORBS := preload("res://scripts/items/special_orbs.gd")
class DockedShip:
extends Node
var is_docked := true
class TestInventory:
extends Node
signal inventory_changed
var cargo: Array[ItemInstance] = []
var weapon_slots: Array[ItemInstance] = []
var module_slots: Array[ItemInstance] = []
var recalculations := 0
func _recalculate_ship_stats() -> void: recalculations += 1
func _ready() -> void:
if not Engine.is_editor_hint(): print("ADVANCEMENT_CHECKS: "+JSON.stringify(run_checks()))
func run_checks() -> Dictionary:
var failures: Array[String] = []
var ship := DockedShip.new()
var inventory := TestInventory.new()
ship.add_child(inventory)
var progress = preload("res://scripts/planets/planet_progress.gd").new()
var checked := 0
for definition in GENERATOR.DEFINITIONS:
var item := GENERATOR.generate_for_definition(definition,20,2)
item.special_orbs.assign(["piercing"] if definition.category == ItemDefinition.Category.WEAPON else [])
var original_orbs := item.special_orbs.duplicate()
var modifiers := item.additional_modifiers.duplicate()
var rarity := item.rarity
var cooldown := item.extension_cooldown
var passive := item.passive_value
for modifier in modifiers:
if modifier != null: modifier.value = -999
inventory.cargo.assign([item])
progress.orbs["advancement"] = 2
if not ORBS.apply(progress,inventory,item,"advancement"): failures.append("Apply failed: "+definition.item_id)
if item.item_level != 21 or progress.orbs.advancement != 1: failures.append("Level or payment")
if item.rarity != rarity or item.additional_modifiers.size() != modifiers.size(): failures.append("Changed rarity or count")
if item.special_orbs != original_orbs: failures.append("Lost special orb")
if item.extension_cooldown != cooldown or item.passive_value != passive: failures.append("Rerolled unrelated extension ability")
for index in modifiers.size():
var old: ItemStatModifier = modifiers[index]
var current: ItemStatModifier = item.additional_modifiers[index]
if current.stat_key != old.stat_key or current.affix != old.affix: failures.append("Changed affix type")
if old.value != -999 or current == old: failures.append("Mutated shared modifier")
var bounds := current.affix.get_value_range(21)
if current.value < bounds.x or current.value > bounds.y: failures.append("Wrong value range")
if not ORBS.apply(progress,inventory,item,"advancement") or item.item_level != 22: failures.append("Cannot repeat")
if ORBS.apply(progress,inventory,item,"advancement"): failures.append("Used absent orb")
progress.orbs.advancement = 1
item.item_level = 999
if not ORBS.apply(progress,inventory,item,"advancement") or item.item_level != 1000: failures.append("999 to 1000")
progress.orbs.advancement = 1
if ORBS.apply(progress,inventory,item,"advancement") or progress.orbs.advancement != 1: failures.append("Exceeded cap or charged rejection")
checked += 1
var bank := ItemInstance.new()
bank.definition = preload("res://resources/items/energy_bank.tres")
if ORBS.accepts(bank,"advancement"): failures.append("Consumable accepted")
var unowned := GENERATOR.generate(1)
if ORBS.apply(progress,inventory,unowned,"advancement"): failures.append("Unowned item accepted")
var legacy := GENERATOR.generate_for_definition(preload("res://resources/items/plasma_cannon.tres"),5)
legacy.additional_modifiers.clear()
var modifier := ItemStatModifier.new()
modifier.stat_key = "damage_percent"
modifier.value = -999
legacy.additional_modifiers.append(modifier)
inventory.cargo.assign([legacy])
progress.orbs.advancement = 1
if not ORBS.apply(progress,inventory,legacy,"advancement") or legacy.additional_modifiers[0].affix == null: failures.append("Legacy affix unresolved")
var drops: Array[String] = []
for seed_value in 3: drops.append(preload("res://scripts/game/distant_loot.gd").roll(125,true,seed_value,1.0,0.0)[1].orb)
if not drops.has("advancement") or not drops.has("piercing") or not drops.has("booster"): failures.append("Boss orb pool")
ship.free()
return {"failures":failures,"definitions_checked":checked,"passed":failures.is_empty()}

View File

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

View File

@ -0,0 +1,6 @@
[gd_scene format=3 uid="uid://citpy72dly08j"]
[ext_resource type="Script" uid="uid://cxds8nq24djkt" path="res://tests/advancement_checks.gd" id="1_6jvym"]
[node name="advancement_checks" type="Node" unique_id=1289129973]
script = ExtResource("1_6jvym")

View File

@ -0,0 +1,37 @@
@tool
extends Node
const DATA := preload("res://scripts/planets/distant_expeditions.gd")
func run_checks() -> Dictionary:
var failures: Array[String] = []
var profile := {}
for seed_value in 1000:
var candidate := {"id":"test:%d" % seed_value,"seed":seed_value,"name":"Test"}
if DATA.has_hermit(candidate):
profile = candidate
break
for source in [1,120,125,130,500]:
for seed_value in 100:
var candidate := {"id":"test:%d" % seed_value,"seed":seed_value,"name":"Test"}
var map := DATA.offer(candidate,source)
if int(map.level) < source+1 or int(map.level) > source+5: failures.append("Wrong destination range")
if map != DATA.offer(candidate,source): failures.append("Offer rerolled")
if DATA.map_cost(map) != int(map.level)*1000: failures.append("Wrong credits")
if DATA.fuel_cost(map) != int(map.level)*1000: failures.append("Wrong fuel")
var captain = preload("res://scripts/captain/captain_progress.gd").new()
var progress: Resource = captain.expedition
var map := DATA.offer(profile,120)
var price := DATA.map_cost(map)
captain.credits = price-1
if DATA.buy(progress,captain,profile,120): failures.append("Bought without credits")
if captain.credits != price-1 or not progress.distant_maps.is_empty(): failures.append("Failed purchase mutated state")
captain.credits = price
progress.deuter = 500000
if not DATA.buy(progress,captain,profile,120): failures.append("Purchase failed")
if captain.credits != 0 or progress.distant_maps.size() != 1: failures.append("Wrong payment or archive")
captain.credits = price
if DATA.buy(progress,captain,profile,120): failures.append("Duplicate purchase")
if captain.credits != price: failures.append("Duplicate charged")
if progress.deuter != 500000: failures.append("Purchase consumed fuel")
var clone: Resource = progress.duplicate(true)
if clone.distant_maps != progress.distant_maps: failures.append("Archive not preserved")
return {"failures":failures,"offers_checked":500,"beyond_120_supported":true}

View File

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

View File

@ -0,0 +1,48 @@
extends Control
var captain: Resource
var _content: VBoxContainer
var _economy_labels: Dictionary = {}
func _ready() -> void:
captain = preload("res://scripts/captain/captain_progress.gd").new()
captain.expedition.deuter = 8000
captain.expedition.distant_maps = {
"preview_1":{"id":"preview_1","level":5,"name":"Odległy sektor 005","source_planet":"Eden","source_level":1},
"preview_2":{"id":"preview_2","level":125,"name":"Odległy sektor 125","source_planet":"Ostatni Horyzont","source_level":120}
}
var bg := ColorRect.new()
bg.color = Color("0b171e")
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(bg)
var art := TextureRect.new()
art.texture = load("res://assets/planets/props/hermit_cartographer.png")
art.position = Vector2(20,80)
art.size = Vector2(650,520)
art.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
art.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
add_child(art)
var label := Label.new()
label.text = "PUSTELNIK / KARTOGRAF"
label.position = Vector2(110,35)
label.add_theme_font_size_override("font_size",25)
label.modulate = Color("e9b4fc")
add_child(label)
var scroll := ScrollContainer.new()
scroll.position = Vector2(705,24)
scroll.size = Vector2(550,670)
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
add_child(scroll)
_content = VBoxContainer.new()
_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scroll.add_child(_content)
preload("res://scripts/planets/distant_expeditions_panel.gd").build(self)
_economy_labels.expedition_fuel.text = "Zapas paliwa: %.1f deuteru" % captain.expedition.deuter
var fuel_before: float = captain.expedition.deuter
var maps_before: Dictionary = captain.expedition.distant_maps.duplicate(true)
var buttons := find_children("FlyDistant_*","Button",true,false)
for button in buttons: button.pressed.emit()
assert(captain.expedition.deuter == fuel_before)
assert(captain.expedition.distant_maps == maps_before)
print("DISTANT_PREVIEW_OK: display fixture only; actual flights are covered by distant_integration")
func _travel_to_distant(_map_id: String) -> void:
pass

View File

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

View File

@ -0,0 +1,12 @@
[gd_scene format=3 uid="uid://c3lleuuycm025"]
[ext_resource type="Script" uid="uid://ea26uuwagnd5" path="res://tests/distant_expeditions_preview.gd" id="1_dvuhr"]
[node name="distant_expeditions_preview" type="Control" unique_id=340557901]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_dvuhr")

View File

@ -0,0 +1,152 @@
extends Node
var is_runner := false
var failures: Array[String] = []
const GENERATOR := preload("res://scripts/levels/distant_sector_generator.gd")
const ORBS := preload("res://scripts/items/special_orbs.gd")
const LOOT := preload("res://scripts/game/distant_loot.gd")
func _ready() -> void:
if not is_runner:
var runner = get_script().new()
runner.is_runner = true
get_tree().root.add_child.call_deferred(runner)
else:
_run.call_deferred()
func check(value: bool, message: String) -> void:
if not value: failures.append(message); push_error("DISTANT_TEST: "+message)
func frames(count: int = 4) -> void:
for index in count: await get_tree().process_frame
func freeze_game(game: Node) -> void:
game.get_node("PlayerShip").set_physics_process(false)
for enemy in get_tree().get_nodes_in_group("enemies"): enemy.set_physics_process(false)
func _run() -> void:
for level in [1,60,120,125,500]:
var ordinary := preload("res://scripts/levels/level_balance.gd").enemy_tiers(level)
var tiers := GENERATOR.enemy_tiers(level)
check(tiers.count(2)==1,"One boss")
check(tiers.count(0)==ordinary.count(0)*2,"Double normals")
check(tiers.count(1)==ordinary.count(1)*2,"Double elites")
check(preload("res://scripts/levels/level_balance.gd").enemy_stat_scale(125)>preload("res://scripts/levels/level_balance.gd").enemy_stat_scale(120),"Stats beyond 120")
check(LOOT.roll(125,false,2,0.149,0.0).size()==1,"Map 15 percent")
check(LOOT.roll(125,false,2,0.15,0.0).is_empty(),"Map boundary and no regular orb")
for seed_value in 100:
for chance in [0.0,0.15,1.0]:
var boss_drops := LOOT.roll(125,true,seed_value,chance,1.0)
check(boss_drops.size()==2 and boss_drops[0].has("map") and boss_drops[1].has("orb"),"Exactly one guaranteed boss map and orb")
var next_map: Dictionary = LOOT.roll(125,false,2,0.0,1.0)[0].map
check(int(next_map.level)>125 and int(next_map.level)<=130,"Map chain")
GameState.start_new_run()
GameState.system_level = 7
GameState.system_seed = 777
GameState.remember_system()
GameState.captain.expedition.distant_maps["integration"] = {"id":"integration","level":125,"seed":123456,"name":"Sektor testowy","source_planet":"Test","source_level":120}
GameState.captain.expedition.deuter = 124999
check(not GameState.depart_distant("integration"),"No flight without fuel")
check(GameState.system_level==7 and GameState.captain.expedition.deuter==124999,"Failed departure preserved state")
GameState.captain.expedition.deuter = 125000
get_tree().change_scene_to_file("res://scenes/game/game.tscn")
await frames()
var game := get_tree().current_scene
freeze_game(game)
var player := game.get_node("PlayerShip")
var inventory := player.get_node("ShipInventory")
player.is_docked = true
var weapon: ItemInstance = inventory.weapon_slots[0]
var engine := ItemInstance.new()
engine.definition = preload("res://resources/items/engine_extension.tres")
inventory.module_slots[2] = engine
var progress: Resource = GameState.captain.expedition
progress.orbs = {"piercing":2,"booster":1}
check(not ORBS.apply(progress,inventory,engine,"piercing"),"Piercing category restriction")
check(not ORBS.apply(progress,inventory,weapon,"booster"),"Booster category restriction")
check(ORBS.apply(progress,inventory,weapon,"piercing"),"Apply piercing")
check(not ORBS.apply(progress,inventory,weapon,"piercing") and progress.orbs.piercing==1,"No stacking or extra cost")
check(ORBS.apply(progress,inventory,engine,"booster"),"Apply booster")
check(player.has_free_mini_boost(),"Equipped engine")
Input.action_press("ship_mini_boost")
player.energy = 0
player._update_mini_boost(1.0)
check(player._mini_boost_active and player.energy==0,"Free TAB with empty generator")
Input.action_release("ship_mini_boost")
inventory.module_slots[2] = null
check(not player.has_free_mini_boost(),"Unequipped engine no effect")
inventory.module_slots[2] = engine
check(game.travel_to_distant("integration"),"Flight requested")
await frames(8)
game = get_tree().current_scene
freeze_game(game)
check(GameState.captain.expedition.deuter==0,"Exactly one fuel payment")
check(GameState.system_level==125 and GameState.highest_system==7,"Isolated progression")
check(game.get_node_or_null("CelestialBodies")==null,"No celestial bodies")
check(game.get_node_or_null("PlanetExpeditions")==null,"No planet expeditions")
check(game.get_node("AncientStructures").get_child_count()==14,"Decorative structures")
check(game.get_remaining_enemies()==46 and not game._boss_spawned,"Doubled encounter before boss")
player = game.get_node("PlayerShip")
inventory = player.get_node("ShipInventory")
player.is_docked = true
check("piercing" in inventory.weapon_slots[0].special_orbs,"Orb persisted across travel")
check(player.has_free_mini_boost(),"Engine orb persisted")
var enemy: Node = get_tree().get_first_node_in_group("enemies")
enemy.absorption = Vector3.ZERO
enemy.shields = 100
enemy.hull = 1000
enemy.take_piercing_weapon_hit(100,false,0,0.5)
check(is_equal_approx(enemy.shields,50) and is_equal_approx(enemy.hull,950),"50 percent shield bypass")
enemy.shields = 100
enemy.hull = 1000
enemy.take_weapon_hit(100,false,0)
check(is_equal_approx(enemy.shields,0) and is_equal_approx(enemy.hull,1000),"Other weapons unchanged")
enemy.shields = 10
enemy.hull = 1000
enemy.take_piercing_weapon_hit(100,false,0,0.5)
check(is_equal_approx(enemy.shields,0) and is_equal_approx(enemy.hull,910),"Shield overflow")
enemy.shields = 100
enemy.hull = 1000
player.get_node("WeaponController")._apply_damage(enemy,100,false,0.5)
check(is_equal_approx(enemy.hull,950),"Laser piercing")
enemy.shields = 100
enemy.hull = 1000
var projectile := preload("res://scenes/game/weapon_projectile.tscn").instantiate()
projectile.damage = 100
projectile.shield_piercing = 0.5
projectile.weapon_kind = ItemDefinition.WeaponKind.PLASMA
projectile._apply_damage_once(enemy)
check(is_equal_approx(enemy.hull,950),"Projectile piercing")
projectile.free()
var payload := Node2D.new()
payload.set_script(preload("res://scripts/items/expedition_pickup.gd"))
payload.payload = {"orb":"booster"}
payload.position = player.position
game.add_child(payload)
var count: int = progress.orbs.booster
payload._physics_process(0)
check(progress.orbs.booster==count+1,"Collect orb")
await frames()
for victim in get_tree().get_nodes_in_group("enemies"): victim.queue_free()
await frames()
game.get_node("DistantSector")._physics_process(0.3)
await frames()
check(game._boss_spawned and game.get_remaining_enemies()==1,"Exactly one boss after wave")
var boss: Node = get_tree().get_first_node_in_group("enemies")
boss.shields = 0
boss.take_damage(1000000000.0,false,0,true)
await frames()
var maps := 0
var orbs := 0
for pickup in get_tree().get_nodes_in_group("loot"):
if pickup.get_script() == preload("res://scripts/items/expedition_pickup.gd"):
if pickup.payload.has("map"): maps += 1
if pickup.payload.has("orb"): orbs += 1
check(maps==1 and orbs==1,"Actual boss death drops exactly one map and orb")
check(get_tree().get_nodes_in_group("time_crystal_pickups").size()==1,"Boss also drops time crystals")
game.get_node("DistantSector")._physics_process(0.3)
check(game.system_cleared,"Sector cleared")
player.landing_combat_lock = 0
game.get_node("DistantSector")._return_home()
await frames(8)
game = get_tree().current_scene
freeze_game(game)
check(GameState.active_distant_map.is_empty() and GameState.system_level==7 and GameState.system_seed==777,"Return home")
check(GameState.captain.expedition.deuter==0,"Return free")
check(game.has_node("PlanetExpeditions") and game.has_node("CelestialBodies"),"Normal scene restored")
check(is_instance_valid(game.get_node("PlanetExpeditions").screen) and game.get_node("PlayerShip").is_docked,"Return docks at freighter")
print("DISTANT_INTEGRATION_RESULT: "+JSON.stringify({"failures":failures,"passed":failures.is_empty()}))

View File

@ -0,0 +1 @@
uid://66jnn06l46y7

View File

@ -0,0 +1,6 @@
[gd_scene format=3 uid="uid://onfp606db0mx"]
[ext_resource type="Script" uid="uid://66jnn06l46y7" path="res://tests/distant_integration.gd" id="1_8yv68"]
[node name="distant_integration" type="Node" unique_id=979516099]
script = ExtResource("1_8yv68")

18
tests/economy_audit.gd Normal file
View File

@ -0,0 +1,18 @@
@tool
extends Node
func inspect_costs() -> Dictionary:
var result := {}
_scan("res://scripts", result)
return result
func _scan(path: String, result: Dictionary) -> void:
for file in DirAccess.get_files_at(path):
if not file.ends_with(".gd"): continue
var matches: Array[String] = []
var lines := FileAccess.get_file_as_string(path+"/"+file).split("\n")
for index in lines.size():
var line := lines[index]
if ("cost" in line.to_lower() or "deuter" in line.to_lower() or "upkeep" in line.to_lower() or "salary" in line.to_lower()) and not line.strip_edges().begins_with("#"):
matches.append("%d: %s" % [index+1,line.strip_edges()])
if not matches.is_empty(): result[path+"/"+file] = matches
for folder in DirAccess.get_directories_at(path):
_scan(path+"/"+folder,result)

View File

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

View File

@ -0,0 +1,61 @@
@tool
extends Node
const NETWORK = preload("res://scripts/planets/mining_network.gd")
func run_checks() -> Dictionary:
var failures: Array[String] = []
var previous := Vector2i.ZERO
for level in range(1, 121):
var bounds := NETWORK.richness_bounds(level)
if bounds.x < previous.x or bounds.y < previous.y: failures.append("Non-monotonic bands")
previous = bounds
for seed_value in 100:
var profile := {"id":"%d:planet:0" % seed_value, "seed":seed_value, "name":"Test", "difficulty":0.5}
var richness := NETWORK.deposit_richness(profile, level)
if richness < bounds.x or richness > bounds.y: failures.append("Out of band")
if NETWORK.build_cost(profile, level) != richness * 10: failures.append("Wrong scaled price")
if richness != NETWORK.deposit_richness(profile, level): failures.append("Rerolled")
var progress = preload("res://scripts/planets/planet_progress.gd").new()
var captain = preload("res://scripts/captain/captain_progress.gd").new()
var profile := {}
for seed_value in 1000:
var candidate := {"id":"%d:planet:0" % seed_value,"seed":seed_value,"name":"Test","difficulty":0.5}
if NETWORK.has_recruit(candidate) and NETWORK.has_deep_deposit(candidate):
profile = candidate
break
if profile.is_empty(): return {"failures":["No fixture planet"]}
var hire_cost := NETWORK.recruitment_cost(NETWORK.recruit(profile))
captain.credits = hire_cost
if NETWORK.hire(progress, captain, profile): failures.append("Hired without cabin")
if captain.credits != hire_cost: failures.append("Charged failed hire")
progress.freighter_rooms["cabin_1"] = "crew_cabin"
captain.credits = hire_cost - 1
if NETWORK.hire(progress, captain, profile): failures.append("Hired without credits")
captain.credits = hire_cost
if not NETWORK.hire(progress, captain, profile): failures.append("Hire failed")
if captain.credits != 0: failures.append("Wrong hire charge")
if NETWORK.hire(progress, captain, profile): failures.append("Duplicate hire")
var station_cost := NETWORK.build_cost(profile, 1)
captain.parts = station_cost - 1
if NETWORK.build(progress, captain, profile, 1): failures.append("Built without parts")
if captain.parts != station_cost-1: failures.append("Charged failed build")
captain.parts = station_cost
if not NETWORK.build(progress, captain, profile, 1): failures.append("Build failed")
if captain.parts != 0: failures.append("Wrong build charge")
if NETWORK.build(progress, captain, profile, 1): failures.append("Duplicate build")
var station: Dictionary = progress.mining_stations[str(profile.id)]
if NETWORK.station_rate(progress, station) != 0: failures.append("Unstaffed production")
var member_id := str(NETWORK.recruit(profile).id)
NETWORK.assign(progress, str(profile.id), member_id)
station.richness = 50
progress.crew[member_id].management = 50
if not is_equal_approx(NETWORK.station_rate(progress, station), 75): failures.append("Rate formula")
var before: float = progress.deuter
NETWORK.advance(progress, 60)
if not is_equal_approx(progress.deuter-before, 75): failures.append("Production")
if int(station.richness) != 50: failures.append("Deposit depleted")
NETWORK.dismiss(progress, member_id)
if NETWORK.station_rate(progress, station) != 0: failures.append("Dismiss production")
var old_profile = preload("res://scripts/planets/planet_profiles.gd").generate(1234, 0, "Legacy")
var old_station := {"id":old_profile.id,"planet_name":"Legacy","system":40}
if NETWORK.station_richness(old_station) != NETWORK.deposit_richness(old_profile,40): failures.append("Legacy migration")
return {"failures":failures,"sample_rolls":12000,"production_example":75,"level_1":NETWORK.richness_bounds(1),"level_120":NETWORK.richness_bounds(120)}

View File

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

View File

@ -0,0 +1,112 @@
extends "res://tests/distant_integration.gd"
const SERVICE := preload("res://scripts/planets/time_engineer.gd")
const ITEM_GENERATOR := preload("res://scripts/items/item_generator.gd")
func _run() -> void:
await super._run()
var game := get_tree().current_scene
var player := game.get_node("PlayerShip")
var inventory := player.get_node("ShipInventory")
var progress: Resource = GameState.captain.expedition
var saved_item: ItemInstance = inventory.cargo[0]
progress.active = true
for definition in ITEM_GENERATOR.DEFINITIONS:
var item := ITEM_GENERATOR.generate_for_definition(definition,20,2)
if item.additional_modifiers.is_empty(): continue
item.special_orbs.assign(["piercing"] if definition.category == ItemDefinition.Category.WEAPON else [])
var effects := item.special_orbs.duplicate()
inventory.cargo[0] = item
for addon in item.additional_modifiers.size():
item.additional_modifiers[addon].value = item.additional_modifiers[addon].affix.get_value_range(20).x
var selected := -1
for addon in item.additional_modifiers.size():
if SERVICE.can_maximize(item,addon):
selected = addon
break
if selected < 0:
progress.time_crystals = 200
check(not SERVICE.maximize(progress,inventory,item,0) and progress.time_crystals==200,"Fixed addons already maximum and free")
continue
var old: ItemStatModifier = item.additional_modifiers[selected]
var values: Array[float] = []
for modifier in item.additional_modifiers: values.append(modifier.value)
progress.time_crystals = 199
check(not SERVICE.maximize(progress,inventory,item,selected),"Cannot maximize without crystals")
check(progress.time_crystals==199,"Rejected service free")
progress.time_crystals = 200
check(SERVICE.maximize(progress,inventory,item,selected),"Maximize available addon")
check(progress.time_crystals==0,"Exact crystal price")
check(is_equal_approx(item.additional_modifiers[selected].value,SERVICE.maximum(item,selected)),"Maximum at current level")
check(item.additional_modifiers[selected] != old and old.value==values[selected],"Shared modifier unchanged")
check(item.special_orbs==effects and item.item_level==20,"Effects and level unchanged")
for index in values.size():
if index != selected: check(item.additional_modifiers[index].value==values[index],"Only selected addon changed")
progress.time_crystals = 200
check(not SERVICE.maximize(progress,inventory,item,selected) and progress.time_crystals==200,"Already maximum not charged")
check(not SERVICE.maximize(progress,inventory,item,-1),"Invalid index rejected")
var test_item := ITEM_GENERATOR.generate_for_definition(preload("res://resources/items/plasma_cannon.tres"),125,2)
check(SERVICE.cost(test_item)==1250,"Level 125 price")
test_item.item_level = 1000
check(SERVICE.cost(test_item)==10000,"Level 1000 price")
check(not SERVICE.maximize(progress,inventory,test_item,0),"Unowned item rejected")
inventory.cargo[0] = saved_item
progress.active = false
var crystals := Node2D.new()
crystals.set_script(preload("res://scripts/items/time_crystal_pickup.gd"))
crystals.amount = 7
crystals.position = player.position
game.add_child(crystals)
var previous: int = progress.time_crystals
crystals._physics_process(0)
crystals._physics_process(0)
check(progress.time_crystals==previous+7,"Crystal collected once")
await frames()
check(LOOT.crystal_amount(125,0,2)==26 and LOOT.crystal_amount(125,2,2)==130,"Crystal level and rank scaling")
var cloned: Resource = progress.duplicate(true)
check(cloned.time_crystals==progress.time_crystals,"Currency preserved")
var profile := {}
for seed_value in 100:
var candidate := preload("res://scripts/planets/planet_profiles.gd").generate(seed_value,0,"Test")
if SERVICE.has_engineer(candidate):
profile = candidate
break
check(not profile.is_empty(),"Engineer exists")
var surface := preload("res://scripts/planets/surface_generator.gd").generate(int(profile.seed),profile)
var engineers := 0
for poi in surface.pois:
if poi.kind=="time_engineer": engineers += 1
check(engineers==1 and SERVICE.has_engineer(profile),"Engineer POI generated")
# Exercise the real landing screen and the actual item/addon selectors.
game.set_physics_process(false)
for enemy in get_tree().get_nodes_in_group("enemies"): enemy.queue_free()
await frames()
var controller := game.get_node("PlanetExpeditions")
controller._leave()
player.position = game.protected_planet.position
progress.lander_hp = 100
progress.scan(game.protected_planet.planet_profile)
var weapon: ItemInstance
for attempt in 100:
weapon = ITEM_GENERATOR.generate_for_definition(preload("res://resources/items/plasma_cannon.tres"),20,2)
var bounds := weapon.additional_modifiers[1].affix.get_value_range(20)
if bounds.y > bounds.x + 0.000001: break
for modifier in weapon.additional_modifiers: modifier.value = modifier.affix.get_value_range(20).x
inventory.weapon_slots[0] = weapon
progress.time_crystals = 400
controller.land_nearby()
await frames()
var screen: Control = controller.screen
check(is_instance_valid(screen),"Actual surface screen opened")
screen._poi = {"kind":"time_engineer","position":screen.lander.position}
screen._engineer_item = weapon
screen._engineer_addon = 0
screen._show_poi()
var addon_choice: OptionButton = screen.find_child("EngineerAddon",true,false)
addon_choice.item_selected.emit(1)
var chosen_index: int = screen._engineer_addon
check(chosen_index==1,"Addon selector targets requested addon")
var before_first: float = weapon.additional_modifiers[0].value
screen.find_child("MaximizeSelectedAddon",true,false).pressed.emit()
check(progress.time_crystals==200,"UI service charged once")
check(weapon.additional_modifiers[0].value==before_first and is_equal_approx(weapon.additional_modifiers[1].value,SERVICE.maximum(weapon,1)),"UI maximizes only chosen addon")
check(screen.find_child("MaximizeSelectedAddon",true,false).disabled,"UI blocks already-max addon")
print("TIME_CRYSTALS_CHECKS: "+JSON.stringify({"failures":failures,"passed":failures.is_empty()}))

View File

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

View File

@ -0,0 +1,6 @@
[gd_scene format=3 uid="uid://bn25yax4w7ki1"]
[ext_resource type="Script" uid="uid://jgxiaf5f4avb" path="res://tests/time_crystals_checks.gd" id="1_nug5d"]
[node name="time_crystals_checks" type="Node" unique_id=1574606779]
script = ExtResource("1_nug5d")

18
tests/workspace_reader.gd Normal file
View File

@ -0,0 +1,18 @@
@tool
extends Node
func read_text(path: String) -> String:
return FileAccess.get_file_as_string(path)
func list_files(path: String) -> Array[String]:
var result: Array[String] = []
for file in DirAccess.get_files_at(path): result.append(path+"/"+file)
for folder in DirAccess.get_directories_at(path): result.append_array(list_files(path+"/"+folder))
return result
func write_text(path: String, content: String) -> void:
var file := FileAccess.open(path,FileAccess.WRITE)
file.store_string(content)
func copy_image(source: String, target: String) -> int:
var image := Image.load_from_file(source)
if image == null: return ERR_FILE_NOT_FOUND
DirAccess.make_dir_recursive_absolute(target.get_base_dir())
return image.save_png(target)

View File

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