Sporo zmian
14
assets/expeditions/README.md
Normal 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.
|
||||
BIN
assets/expeditions/abandoned_relay.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
40
assets/expeditions/abandoned_relay.png.import
Normal 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
|
||||
BIN
assets/expeditions/ancient_gate.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
40
assets/expeditions/ancient_gate.png.import
Normal 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
|
||||
BIN
assets/expeditions/freighter_wreck.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
40
assets/expeditions/freighter_wreck.png.import
Normal 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
|
||||
BIN
assets/icons/orbs/advancement.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
40
assets/icons/orbs/advancement.png.import
Normal 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
|
||||
BIN
assets/icons/orbs/booster.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
40
assets/icons/orbs/booster.png.import
Normal 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
|
||||
BIN
assets/icons/orbs/piercing.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
40
assets/icons/orbs/piercing.png.import
Normal 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
|
||||
BIN
assets/icons/resources/time_crystals.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
40
assets/icons/resources/time_crystals.png.import
Normal 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
|
||||
BIN
assets/planets/expedition_navigation.png
Normal file
|
After Width: | Height: | Size: 883 KiB |
40
assets/planets/expedition_navigation.png.import
Normal 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
|
||||
BIN
assets/planets/props/hermit_cartographer.png
Normal file
|
After Width: | Height: | Size: 786 KiB |
40
assets/planets/props/hermit_cartographer.png.import
Normal 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
|
||||
BIN
assets/planets/props/time_engineer.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
40
assets/planets/props/time_engineer.png.import
Normal 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
|
||||
26
scripts/game/distant_loot.gd
Normal 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)
|
||||
1
scripts/game/distant_loot.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://d0l5u5k4m7myv
|
||||
102
scripts/game/distant_sector.gd
Normal 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
|
||||
1
scripts/game/distant_sector.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bljfethq7wf27
|
||||
@ -44,6 +44,8 @@ var _combat_target: Node2D
|
||||
var virus_controlled := false
|
||||
var _virus_infection_attempted := false
|
||||
var _research_player_hit := false
|
||||
var _weapon_piercing := 0.0
|
||||
var combat_level := 1
|
||||
var _reward_claimed := false
|
||||
|
||||
func roll_virus_infection() -> bool:
|
||||
@ -101,6 +103,7 @@ func _ready() -> void:
|
||||
|
||||
func apply_level_settings(level: int, tier: int) -> void:
|
||||
loot_tier = tier
|
||||
combat_level = maxi(1, level)
|
||||
$Sprite2D.modulate = get_tier_tint()
|
||||
var power := LEVEL_BALANCE.enemy_stat_scale(level)
|
||||
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)
|
||||
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:
|
||||
if is_queued_for_deletion() or hull <= 0.0:
|
||||
return
|
||||
@ -345,7 +353,8 @@ func take_damage(amount: float, critical: bool = false, damage_type: int = Damag
|
||||
var progress: Resource = GameState.captain.expedition
|
||||
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 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)
|
||||
shields -= shield_damage
|
||||
var hull_damage := minf(hull, remaining * hull_multiplier)
|
||||
@ -396,7 +405,9 @@ func _reward_captain() -> void:
|
||||
|
||||
func _drop_loot() -> void:
|
||||
# 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:
|
||||
var bank := ItemInstance.new()
|
||||
bank.definition = preload("res://resources/items/energy_bank.tres")
|
||||
|
||||
@ -626,7 +626,7 @@ func _create_system_badge() -> void:
|
||||
func _update_system_badge() -> void:
|
||||
if not is_instance_valid(_system_badge): return
|
||||
_system_badge.position = Vector2(_captain_panel.position.x + _captain_panel.size.x + 10, _captain_panel.position.y)
|
||||
_system_title.text = "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.add_theme_color_override("font_color", Color("e88972") if GameState.system_level > 60 else Color("88a7ae"))
|
||||
|
||||
|
||||
@ -27,6 +27,12 @@ var _pending_boss: Dictionary = {}
|
||||
var _boss_spawned := false
|
||||
|
||||
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)
|
||||
if GameState.system_seed == 0:
|
||||
GameState.system_seed = GameState.create_system_seed()
|
||||
@ -58,6 +64,10 @@ func _ready() -> void:
|
||||
expeditions.game = self
|
||||
expeditions.player = player
|
||||
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:
|
||||
var bodies := Node2D.new()
|
||||
@ -157,6 +167,7 @@ func is_boss_alive() -> bool:
|
||||
return false
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
if not GameState.active_distant_map.is_empty(): return
|
||||
if _transitioning or bool(player.get("_is_destroyed")):
|
||||
services_menu.call("hide_services")
|
||||
return
|
||||
@ -236,6 +247,30 @@ func _load_next_level() -> void:
|
||||
services_menu.call("show_services", false, true)
|
||||
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:
|
||||
if _transitioning or bool(player.get("_is_destroyed")) or level == GameState.system_level or not GameState.can_visit_system(level): return false
|
||||
_transitioning = true
|
||||
|
||||
@ -7,6 +7,9 @@ var selected_ship_path: String = ""
|
||||
var restart_inventory: Dictionary = {}
|
||||
var system_level: int = 1
|
||||
var system_seed: int = 0
|
||||
var active_distant_map: Dictionary = {}
|
||||
var distant_home: Dictionary = {}
|
||||
var return_to_freighter := false
|
||||
var highest_system := 1
|
||||
var system_seeds: Dictionary = {}
|
||||
var cleared_systems: Dictionary = {}
|
||||
@ -16,6 +19,26 @@ var asteroid_retry_seeds: Dictionary = {}
|
||||
var exploration_wreck_bonuses: 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:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.randomize()
|
||||
@ -37,6 +60,9 @@ func start_new_run() -> void:
|
||||
captain = CAPTAIN_PROGRESS.new()
|
||||
system_level = 1
|
||||
highest_system = 1
|
||||
active_distant_map.clear()
|
||||
distant_home.clear()
|
||||
return_to_freighter = false
|
||||
system_seeds.clear()
|
||||
cleared_systems.clear()
|
||||
_production_signal_time = 0
|
||||
|
||||
@ -5,11 +5,12 @@ const DURATION := 10.0
|
||||
var remaining := 0.0
|
||||
var tick_damage := 0.0
|
||||
var credited_to_player := false
|
||||
var shield_piercing := 0.0
|
||||
var _tick_elapsed := 0.0
|
||||
var _base_color := Color.WHITE
|
||||
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:
|
||||
return
|
||||
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"
|
||||
target.add_child(status)
|
||||
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
|
||||
|
||||
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):
|
||||
step = _tick_elapsed
|
||||
_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)
|
||||
else:
|
||||
target.take_damage(tick_damage * step, false, DamageTypes.Type.RADIATION)
|
||||
|
||||
@ -382,11 +382,19 @@ func is_mini_boosting() -> bool:
|
||||
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)
|
||||
|
||||
func has_free_mini_boost() -> bool:
|
||||
var engine: ItemInstance = $ShipInventory.get_item("module",2)
|
||||
return engine != null and engine.definition.category == ItemDefinition.Category.ENGINE and "booster" in engine.special_orbs
|
||||
|
||||
func _update_mini_boost(delta: float) -> void:
|
||||
_mini_boost_active = false
|
||||
if not Input.is_action_pressed("ship_mini_boost"):
|
||||
_mini_boost_exhausted = false
|
||||
return
|
||||
if has_free_mini_boost() and not Input.is_action_pressed("ship_brake"):
|
||||
_mini_boost_exhausted = false
|
||||
_mini_boost_active = true
|
||||
return
|
||||
if _mini_boost_exhausted or Input.is_action_pressed("ship_brake"):
|
||||
return
|
||||
var cost := MINI_BOOST_ENERGY_PER_SECOND * delta
|
||||
|
||||
@ -154,6 +154,7 @@ func _fire_projectile(item: ItemInstance) -> void:
|
||||
)
|
||||
ship.get_parent().add_child(projectile)
|
||||
projectile.reset_physics_interpolation()
|
||||
projectile.set("shield_piercing", item.shield_piercing())
|
||||
projectile.set("missile_homing", definition.missile_homing)
|
||||
WEAPON_AUDIO_SCRIPT.play(ship.get_parent(), muzzle_position, definition.weapon_kind)
|
||||
if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN:
|
||||
@ -198,7 +199,7 @@ func _fire_laser(slot_index: int, item: ItemInstance) -> void:
|
||||
_break_cloak_on_fire()
|
||||
if not result.is_empty():
|
||||
var hit: Dictionary = ship.call("roll_weapon_hit", item)
|
||||
_apply_damage(result.get("collider"), float(hit["damage"]), bool(hit["critical"]))
|
||||
_apply_damage(result.get("collider"), float(hit["damage"]), bool(hit["critical"]), item.shield_piercing())
|
||||
_spawn_laser_impact(endpoint)
|
||||
_laser_pulse_timers[slot_index] = definition.laser_pulse_duration
|
||||
_shot_timers[slot_index] = definition.fire_interval
|
||||
@ -231,8 +232,10 @@ func _consume_energy(amount: float) -> bool:
|
||||
ship.set("energy", current_energy - amount)
|
||||
return true
|
||||
|
||||
func _apply_damage(target: Variant, damage: float, critical: bool = false) -> void:
|
||||
if target is Node and target.has_method("take_weapon_hit"):
|
||||
func _apply_damage(target: Variant, damage: float, critical: bool = false, piercing: float = 0.0) -> void:
|
||||
if target is Node and target.has_method("take_piercing_weapon_hit"):
|
||||
target.call("take_piercing_weapon_hit", damage, critical, DamageTypes.Type.ENERGY, piercing)
|
||||
elif target is Node and target.has_method("take_weapon_hit"):
|
||||
target.call("take_weapon_hit", damage, critical, DamageTypes.Type.ENERGY)
|
||||
elif target is Node and target.has_method("take_damage"):
|
||||
target.call("take_damage", damage)
|
||||
|
||||
@ -28,6 +28,7 @@ var _distance_travelled := 0.0
|
||||
var _already_hit: Dictionary = {}
|
||||
var _impact_handled := false
|
||||
var _engine_time := 0.0
|
||||
var shield_piercing := 0.0
|
||||
var _smoke_emission_timer := 0.0
|
||||
var _smoke_puffs: Array[Dictionary] = []
|
||||
|
||||
@ -296,6 +297,8 @@ func _apply_damage_once(target: Variant) -> void:
|
||||
_already_hit[instance_id] = true
|
||||
if enemy_shot and target is CharacterBody2D and target.has_method("take_damage"):
|
||||
target.call("take_damage", damage, is_critical, DamageTypes.for_weapon(weapon_kind))
|
||||
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"):
|
||||
target.call("take_weapon_hit", damage, is_critical, DamageTypes.for_weapon(weapon_kind))
|
||||
elif target.has_method("take_damage"):
|
||||
@ -304,4 +307,4 @@ func _apply_damage_once(target: Variant) -> void:
|
||||
else:
|
||||
target.call("take_damage", damage)
|
||||
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)
|
||||
|
||||
39
scripts/items/expedition_pickup.gd
Normal 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)
|
||||
1
scripts/items/expedition_pickup.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://deest4k7mhp0t
|
||||
@ -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:
|
||||
var item := ItemInstance.new()
|
||||
item.definition = definition
|
||||
item.item_level = clampi(level, 1, 1000)
|
||||
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)
|
||||
roll_base_values(item, level)
|
||||
if definition.category == ItemDefinition.Category.ACTIVE_EXTENSION:
|
||||
if item.is_passive_extension():
|
||||
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.rarity = clampi(item.additional_modifiers.size() - 1, 0, 4)
|
||||
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
|
||||
|
||||
@ -7,6 +7,10 @@ const EXTENSION_COOLDOWNS := preload("res://scripts/items/extension_cooldowns.gd
|
||||
@export_range(5, 30, 1) var extension_cooldown: int = 30
|
||||
var _instance_ability: ActiveAbility
|
||||
@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:
|
||||
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:
|
||||
continue
|
||||
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)
|
||||
|
||||
static func stat_name(key: String) -> String:
|
||||
|
||||
27
scripts/items/special_orbs.gd
Normal 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
|
||||
1
scripts/items/special_orbs.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dshhmuyi4vgid
|
||||
36
scripts/items/time_crystal_pickup.gd
Normal 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)
|
||||
1
scripts/items/time_crystal_pickup.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cfmy7o1t2rlea
|
||||
40
scripts/levels/distant_sector_generator.gd
Normal 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}
|
||||
1
scripts/levels/distant_sector_generator.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://es7rqq3xu5e8
|
||||
@ -33,7 +33,7 @@ static func enemy_tiers(level: int) -> Array[int]:
|
||||
return tiers
|
||||
|
||||
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:
|
||||
return level > NORMAL_MAX_LEVEL
|
||||
|
||||
@ -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].earthlike = profiles.is_earthlike(planets[index].texture)
|
||||
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 ship_rng := RandomNumberGenerator.new()
|
||||
ship_rng.seed = seed_value ^ (level * 0x53484950)
|
||||
|
||||
33
scripts/planets/distant_expeditions.gd
Normal 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
|
||||
1
scripts/planets/distant_expeditions.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://drpa8sluf2ond
|
||||
46
scripts/planets/distant_expeditions_panel.gd
Normal 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)
|
||||
1
scripts/planets/distant_expeditions_panel.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://0xw0o7hogm6k
|
||||
@ -269,6 +269,12 @@ func _exit_tree() -> void:
|
||||
get_tree().paused = _previous_pause
|
||||
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:
|
||||
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
|
||||
|
||||
@ -22,20 +22,22 @@ static func build(screen: Control) -> void:
|
||||
travel.disabled = true
|
||||
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)
|
||||
preload("res://scripts/planets/distant_expeditions_panel.gd").build(screen)
|
||||
var summary := MENU.card(screen._content)
|
||||
MENU.label(summary, "SIEĆ WYDOBYWCZA", 14, MENU.GOLD)
|
||||
var stock := MENU.label(summary, "", 13, Color("95dce8"))
|
||||
screen._economy_labels["network"] = stock
|
||||
MENU.label(summary, "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)
|
||||
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:
|
||||
var station: Dictionary = progress.mining_stations[station_id]
|
||||
var card := MENU.card(screen._content)
|
||||
MENU.label(card, "%s · SYSTEM %d" % [station.planet_name, station.system], 13, MENU.GOLD)
|
||||
MENU.label(card, "Zasobność: %d / 100" % NETWORK.station_richness(station), 12, Color("95dce8"))
|
||||
var member: Dictionary = progress.crew.get(str(station.get("crew_id", "")), {})
|
||||
MENU.label(card, "Czeka na załoganta" if member.is_empty() else "%s · zarządzanie %d/100 · %.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 option := OptionButton.new()
|
||||
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)
|
||||
return
|
||||
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"
|
||||
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]
|
||||
|
||||
@ -123,7 +123,7 @@ func _layout() -> void:
|
||||
|
||||
func _sync_header() -> void:
|
||||
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:
|
||||
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])
|
||||
if is_instance_valid(inventory):
|
||||
for key in ["ammunition","missile","irradiated_ammunition","energy_bank"]: values[key] = inventory.get_crafted_stock(key)
|
||||
values.time_crystals = captain.expedition.time_crystals
|
||||
values.samples = captain.expedition.analysis_samples
|
||||
values.deuter = captain.expedition.deuter
|
||||
var names: Dictionary = RECIPES.NAMES.duplicate()
|
||||
names.merge(GEAR.MATERIAL_NAMES)
|
||||
names.merge({"credits":"Kredyty","parts":"Części","ammunition":"Amunicja","missile":"Rakiety","irradiated_ammunition":"Radiacyjna","energy_bank":"Powerbanki","samples":"Próbki","deuter":"Deuter"})
|
||||
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:
|
||||
var tile := MENU.card(grid)
|
||||
tile.get_parent().name = "Stock_"+key
|
||||
@ -537,6 +538,7 @@ func _craft_supply(id: String, batches := 1) -> void:
|
||||
func _build_workshop() -> void:
|
||||
MENU.hero(_content,"workshop","")
|
||||
_build_supply_production()
|
||||
preload("res://scripts/planets/orb_workshop.gd").build(self)
|
||||
_label("EKWIPUNEK KAPITANA",13,MENU.GOLD)
|
||||
var grid := GridContainer.new()
|
||||
grid.columns = 3
|
||||
@ -574,6 +576,10 @@ func _travel_to_system(level: int) -> void:
|
||||
var controller := get_parent().get_parent()
|
||||
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:
|
||||
preload("res://scripts/planets/mining_network.gd").assign(captain.expedition, station_id, str(option.get_selected_metadata()))
|
||||
_refresh()
|
||||
@ -586,6 +592,9 @@ func _refresh_live_labels() -> void:
|
||||
for key in _economy_labels:
|
||||
var label: Label = _economy_labels[key]
|
||||
if not is_instance_valid(label): continue
|
||||
if key == "expedition_fuel":
|
||||
label.text = "Zapas paliwa: %.1f deuteru" % captain.expedition.deuter
|
||||
continue
|
||||
label.text = "Deuter: %.1f · Produkcja: %.0f/min" % [captain.expedition.deuter, preload("res://scripts/planets/mining_network.gd").total_rate(captain.expedition)] if key == "network" else "%.1f" % captain.expedition.deuter
|
||||
|
||||
func _craft(kind: String) -> void:
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
extends RefCounted
|
||||
## 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 CREW_CHANCE := 0.40
|
||||
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:
|
||||
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:
|
||||
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
|
||||
return {}
|
||||
|
||||
static func hire(progress: Resource, profile: Dictionary) -> bool:
|
||||
static func hire(progress: Resource, captain: Resource, profile: Dictionary) -> bool:
|
||||
var candidate := recruit(profile)
|
||||
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
|
||||
progress.crew[candidate.id] = candidate
|
||||
progress.recruited_planets[str(profile.id)] = true
|
||||
captain.emit_changed()
|
||||
progress.emit_changed()
|
||||
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:
|
||||
var id := str(profile.id)
|
||||
if not has_deep_deposit(profile) or progress.mining_stations.has(id) or captain.parts < BUILD_COST: return false
|
||||
captain.parts -= BUILD_COST
|
||||
progress.mining_stations[id] = {"id":id, "planet_name":str(profile.name), "system":system, "crew_id":"", "produced":0.0}
|
||||
var cost := build_cost(profile, system)
|
||||
if not has_deep_deposit(profile) or progress.mining_stations.has(id) or captain.parts < cost: return false
|
||||
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()
|
||||
progress.emit_changed()
|
||||
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:
|
||||
var id := str(station.get("crew_id", ""))
|
||||
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:
|
||||
var total := 0.0
|
||||
|
||||
41
scripts/planets/orb_workshop.gd
Normal 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)
|
||||
1
scripts/planets/orb_workshop.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dlhcrumvgnr2c
|
||||
@ -10,6 +10,9 @@ const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
|
||||
@export var alien_skills: Dictionary = {}
|
||||
@export var freighter_rooms: Dictionary = {"expansion_3":"module_storage"}
|
||||
@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 recruited_planets: Dictionary = {}
|
||||
@export var deuter := 0.0
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 := {
|
||||
"deposit":["Zbierz odsłonięte minerały", "Wykonaj odwiert", "Wydobądź głęboką żyłę"],
|
||||
"ruins":["Przeszukaj przedsionek", "Otwórz komorę", "Zbadaj rdzeń"],
|
||||
|
||||
34
scripts/planets/specialist_house.gd
Normal 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)
|
||||
1
scripts/planets/specialist_house.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bfhj4sh150ece
|
||||
@ -1,6 +1,6 @@
|
||||
extends Node2D
|
||||
## Ground wash stays in world space after emission, independently of the lander.
|
||||
const LIMIT := 144
|
||||
const LIMIT := 220
|
||||
var source: Control
|
||||
var weather: Node2D
|
||||
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 boost: bool = source._motion.boost_active and thrust
|
||||
if thrust or brake:
|
||||
var rate := 60.0 if boost else (26.0 if thrust else 18.0)
|
||||
if _surface_kind == "rock": rate *= 0.3
|
||||
var rate := 90.0 if boost else (46.0 if thrust else 30.0)
|
||||
if _surface_kind == "rock": rate *= 0.45
|
||||
_emission += delta*rate
|
||||
while _emission >= 1:
|
||||
_emission -= 1
|
||||
@ -87,8 +87,8 @@ func _emit(point: Vector2, angle: float, boost: bool) -> void:
|
||||
var kind := _surface_kind
|
||||
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 scatter := Vector2(_rng.randf_range(-32,32),_rng.randf_range(10,45)).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)})
|
||||
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.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:
|
||||
for particle in _particles:
|
||||
@ -97,10 +97,10 @@ func _draw() -> void:
|
||||
var kind: String = particle.kind
|
||||
var radius: float = particle.size*(1.0+t*1.9)
|
||||
var color := _ground
|
||||
color.a = alpha*0.14
|
||||
if kind == "rock": color = Color(0.65,0.65,0.59,alpha*0.09)
|
||||
if kind == "snow": color = Color(0.88,0.95,0.95,alpha*0.25)
|
||||
if kind == "spray": color = Color(0.66,0.83,0.80,alpha*0.18)
|
||||
color.a = alpha*0.32
|
||||
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.38)
|
||||
if kind == "spray": color = Color(0.66,0.83,0.80,alpha*0.30)
|
||||
if kind == "leaf":
|
||||
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)
|
||||
|
||||
@ -61,6 +61,8 @@ static func generate(seed_value: int, profile: Dictionary) -> Dictionary:
|
||||
var network := preload("res://scripts/planets/mining_network.gd")
|
||||
if network.has_deep_deposit(profile): kinds.append("deep_deposit")
|
||||
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():
|
||||
var kind: String = kinds[index]
|
||||
var chosen := Vector2.ZERO
|
||||
|
||||
@ -21,8 +21,20 @@ func _draw() -> void:
|
||||
if poi.discovered:
|
||||
var at: Vector2 = (poi.position / total + Vector2.ONE * 0.5) * size
|
||||
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")
|
||||
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)
|
||||
for event in world.data.events:
|
||||
if not event.complete:
|
||||
|
||||
@ -19,6 +19,8 @@ var _status: Label
|
||||
var _dialog: PanelContainer
|
||||
var _choices: VBoxContainer
|
||||
var _poi: Dictionary = {}
|
||||
var _engineer_item: ItemInstance
|
||||
var _engineer_addon := 0
|
||||
var _rng := RandomNumberGenerator.new()
|
||||
var _motion := preload("res://scripts/planets/lander_motion.gd").new()
|
||||
var flight_ship: Node
|
||||
@ -228,6 +230,12 @@ func _show_poi(message := "") -> void:
|
||||
POI_CARD.panel_style(_dialog)
|
||||
_choices.add_theme_constant_override("separation",8)
|
||||
_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"]:
|
||||
_show_colony_poi(message)
|
||||
return
|
||||
@ -415,31 +423,40 @@ func _show_colony_poi(message: String) -> void:
|
||||
art.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
art.custom_minimum_size.y = 170
|
||||
_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 is_deposit:
|
||||
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):
|
||||
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)
|
||||
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)
|
||||
POI_CARD.text(_choices, "Bazowo 1 deuter/min + punkty zarządzania załoganta. Części: %d / 200" % int(captain.parts), 12)
|
||||
var build := POI_CARD.button(_choices, "ZBUDUJ PLACÓWKĘ · 200 CZĘŚCI", _build_outpost, true)
|
||||
var cost: int = network.build_cost(progress.active_profile, int(progress.active_profile.get("system_level", GameState.system_level)))
|
||||
POI_CARD.text(_choices, "Budowa kosztuje zasobność × 10 części. Do wydobycia potrzebny jest załogant.", 13)
|
||||
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.disabled = captain.parts < network.BUILD_COST
|
||||
build.disabled = captain.parts < cost
|
||||
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)
|
||||
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)):
|
||||
POI_CARD.text(_choices, "Specjalista został już zatrudniony.", 12)
|
||||
else:
|
||||
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)
|
||||
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.disabled = cabin.is_empty()
|
||||
hire.disabled = cabin.is_empty() or captain.credits < hire_cost
|
||||
POI_CARD.button(_choices, "Wróć do wyprawy", func(): _dialog.hide())
|
||||
|
||||
func _build_outpost() -> void:
|
||||
@ -450,9 +467,52 @@ func _build_outpost() -> void:
|
||||
_show_poi("Placówka gotowa. Przydziel operatora w centrum dowodzenia.")
|
||||
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:
|
||||
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
|
||||
_show_poi("Załogant zajął wolną kajutę na fregacie.")
|
||||
else: _show_poi("Zatrudnienie wymaga wolnej, zbudowanej kajuty.")
|
||||
world.refresh_pois(lander.position)
|
||||
_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.")
|
||||
|
||||
@ -69,12 +69,14 @@ func _ready() -> void:
|
||||
sprite.set_script(preload("res://scripts/planets/surface_cave.gd"))
|
||||
sprite.configure(data,poi)
|
||||
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.position = poi.position
|
||||
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.z_index = 10
|
||||
if poi.kind == "crew":
|
||||
sprite.set_script(preload("res://scripts/planets/specialist_house.gd"))
|
||||
add_child(sprite)
|
||||
_poi_sprites[poi.id] = sprite
|
||||
var label := Label.new()
|
||||
@ -148,9 +150,21 @@ func refresh_pois(lander_position: Vector2) -> void:
|
||||
sprite.rotation = 0
|
||||
sprite.scale = Vector2.ONE * 185.0 / maxf(sprite.texture.get_width(), sprite.texture.get_height())
|
||||
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"
|
||||
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]
|
||||
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 ""))
|
||||
|
||||
|
||||
46
scripts/planets/time_engineer.gd
Normal 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
|
||||
1
scripts/planets/time_engineer.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://diwvjtb7pfgn
|
||||
103
scripts/planets/time_engineer_panel.gd
Normal 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())
|
||||
1
scripts/planets/time_engineer_panel.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://42o40yk3604n
|
||||
@ -34,7 +34,7 @@ func _process(_delta: float) -> void:
|
||||
if not is_instance_valid(game):
|
||||
return
|
||||
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")):
|
||||
_objective.text = "Pokonaj bossa"
|
||||
else:
|
||||
|
||||
76
tests/advancement_checks.gd
Normal 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()}
|
||||
1
tests/advancement_checks.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cxds8nq24djkt
|
||||
6
tests/advancement_checks.tscn
Normal 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")
|
||||
37
tests/distant_expeditions_checks.gd
Normal 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}
|
||||
1
tests/distant_expeditions_checks.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://k0din7ww7xhn
|
||||
48
tests/distant_expeditions_preview.gd
Normal 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
|
||||
1
tests/distant_expeditions_preview.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://ea26uuwagnd5
|
||||
12
tests/distant_expeditions_preview.tscn
Normal 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")
|
||||
152
tests/distant_integration.gd
Normal 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()}))
|
||||
1
tests/distant_integration.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://66jnn06l46y7
|
||||
6
tests/distant_integration.tscn
Normal 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
@ -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)
|
||||
1
tests/economy_audit.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://hjh7mupd7vkq
|
||||
61
tests/mining_network_checks.gd
Normal 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)}
|
||||
1
tests/mining_network_checks.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://fcnfvnprqexj
|
||||
112
tests/time_crystals_checks.gd
Normal 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()}))
|
||||
1
tests/time_crystals_checks.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://jgxiaf5f4avb
|
||||
6
tests/time_crystals_checks.tscn
Normal 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
@ -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)
|
||||
1
tests/workspace_reader.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cb74x2a4obg4m
|
||||