Lądowanie
BIN
artifacts/event_barter.png
Normal file
|
After Width: | Height: | Size: 1021 KiB |
BIN
artifacts/event_blind_cache.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
artifacts/event_collector.png
Normal file
|
After Width: | Height: | Size: 1020 KiB |
BIN
artifacts/event_evacuation.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
artifacts/event_evacuation_stage2.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
artifacts/event_prototype.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
artifacts/event_rescue.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
artifacts/event_safe_bet.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
artifacts/event_scan_cache.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
artifacts/event_scan_result.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
artifacts/event_service.png
Normal file
|
After Width: | Height: | Size: 1020 KiB |
BIN
artifacts/event_trader.png
Normal file
|
After Width: | Height: | Size: 1019 KiB |
BIN
artifacts/events_overview.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
artifacts/hard_event_barter.png
Normal file
|
After Width: | Height: | Size: 961 KiB |
BIN
artifacts/hard_event_blind_cache.png
Normal file
|
After Width: | Height: | Size: 976 KiB |
BIN
artifacts/hard_event_collector.png
Normal file
|
After Width: | Height: | Size: 958 KiB |
BIN
artifacts/hard_event_evacuation.png
Normal file
|
After Width: | Height: | Size: 977 KiB |
BIN
artifacts/hard_event_evacuation_stage2.png
Normal file
|
After Width: | Height: | Size: 983 KiB |
BIN
artifacts/hard_event_prototype.png
Normal file
|
After Width: | Height: | Size: 976 KiB |
BIN
artifacts/hard_event_rescue.png
Normal file
|
After Width: | Height: | Size: 978 KiB |
BIN
artifacts/hard_event_safe_bet.png
Normal file
|
After Width: | Height: | Size: 982 KiB |
BIN
artifacts/hard_event_scan_cache.png
Normal file
|
After Width: | Height: | Size: 973 KiB |
BIN
artifacts/hard_event_scan_result.png
Normal file
|
After Width: | Height: | Size: 979 KiB |
BIN
artifacts/hard_event_service.png
Normal file
|
After Width: | Height: | Size: 958 KiB |
BIN
artifacts/hard_event_trader.png
Normal file
|
After Width: | Height: | Size: 960 KiB |
BIN
artifacts/hard_events_overview.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
artifacts/landmark_abandoned_hamlet.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
40
artifacts/landmark_abandoned_hamlet.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bdnjvwl8mnwnn"
|
||||||
|
path="res://.godot/imported/landmark_abandoned_hamlet.png-b11cd5dc5b1ee3466f6cf95983346755.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://artifacts/landmark_abandoned_hamlet.png"
|
||||||
|
dest_files=["res://.godot/imported/landmark_abandoned_hamlet.png-b11cd5dc5b1ee3466f6cf95983346755.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
artifacts/landmark_dry_hollow.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
40
artifacts/landmark_dry_hollow.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cvluy1b0heu73"
|
||||||
|
path="res://.godot/imported/landmark_dry_hollow.png-fe9cc9575d341a5287b065319605ee79.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://artifacts/landmark_dry_hollow.png"
|
||||||
|
dest_files=["res://.godot/imported/landmark_dry_hollow.png-fe9cc9575d341a5287b065319605ee79.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
artifacts/landmark_fissure.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
40
artifacts/landmark_fissure.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://tc6hp64am1om"
|
||||||
|
path="res://.godot/imported/landmark_fissure.png-f91b3dbdcaee544394f9ae6a94f76e7c.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://artifacts/landmark_fissure.png"
|
||||||
|
dest_files=["res://.godot/imported/landmark_fissure.png-f91b3dbdcaee544394f9ae6a94f76e7c.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
artifacts/landmark_meteor_crater.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
40
artifacts/landmark_meteor_crater.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dq1m7iw5q0hg0"
|
||||||
|
path="res://.godot/imported/landmark_meteor_crater.png-ea013420eb29dfb8eb5b39e2b370d2d0.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://artifacts/landmark_meteor_crater.png"
|
||||||
|
dest_files=["res://.godot/imported/landmark_meteor_crater.png-ea013420eb29dfb8eb5b39e2b370d2d0.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
artifacts/landmark_scale.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
40
artifacts/landmark_scale.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://5pxh2k18r2la"
|
||||||
|
path="res://.godot/imported/landmark_scale.png-6ea8e9d7137dafe6df36dfc3af0b2272.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://artifacts/landmark_scale.png"
|
||||||
|
dest_files=["res://.godot/imported/landmark_scale.png-6ea8e9d7137dafe6df36dfc3af0b2272.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
artifacts/landmark_weathered_stones.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
40
artifacts/landmark_weathered_stones.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dux7kxhohldsu"
|
||||||
|
path="res://.godot/imported/landmark_weathered_stones.png-af9fe18233486320b709e8777f6f8445.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://artifacts/landmark_weathered_stones.png"
|
||||||
|
dest_files=["res://.godot/imported/landmark_weathered_stones.png-af9fe18233486320b709e8777f6f8445.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
artifacts/landmarks_overview.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
40
artifacts/landmarks_overview.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://v582yyasspmd"
|
||||||
|
path="res://.godot/imported/landmarks_overview.png-9b90e04fc96fcb5d833848d239ebc99c.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://artifacts/landmarks_overview.png"
|
||||||
|
dest_files=["res://.godot/imported/landmarks_overview.png-9b90e04fc96fcb5d833848d239ebc99c.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
artifacts/nature_landscapes.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
40
artifacts/nature_landscapes.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dng3ym3lgg4g2"
|
||||||
|
path="res://.godot/imported/nature_landscapes.png-242744282cdba722cb63bd15ee4a6138.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://artifacts/nature_landscapes.png"
|
||||||
|
dest_files=["res://.godot/imported/nature_landscapes.png-242744282cdba722cb63bd15ee4a6138.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
artifacts/nature_surface_detail.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
40
artifacts/nature_surface_detail.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://baeuxke0wlftm"
|
||||||
|
path="res://.godot/imported/nature_surface_detail.png-0eff12ce7608e6aa46ad6ec62c136697.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://artifacts/nature_surface_detail.png"
|
||||||
|
dest_files=["res://.godot/imported/nature_surface_detail.png-0eff12ce7608e6aa46ad6ec62c136697.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
artifacts/threat_rescue_150.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
artifacts/threat_scan_150.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
artifacts/threat_scan_l50.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
artifacts/threat_scan_safe.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
artifacts/threat_system_overview.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
artifacts/threat_zero_chance_event.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
artifacts/threat_zero_chance_poi.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/planets/landmarks/abandoned_hamlet.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
40
assets/planets/landmarks/abandoned_hamlet.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://c7iit6v0wvfpb"
|
||||||
|
path="res://.godot/imported/abandoned_hamlet.png-d90a2415dd3850cf66cc324453c922b2.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/planets/landmarks/abandoned_hamlet.png"
|
||||||
|
dest_files=["res://.godot/imported/abandoned_hamlet.png-d90a2415dd3850cf66cc324453c922b2.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=true
|
||||||
|
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/landmarks/dry_hollow.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
40
assets/planets/landmarks/dry_hollow.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dvfxwoqsi23nn"
|
||||||
|
path="res://.godot/imported/dry_hollow.png-963153dcf8ed2111f1f213c4ccc7d71e.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/planets/landmarks/dry_hollow.png"
|
||||||
|
dest_files=["res://.godot/imported/dry_hollow.png-963153dcf8ed2111f1f213c4ccc7d71e.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=true
|
||||||
|
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/landmarks/fissure.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
40
assets/planets/landmarks/fissure.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://0trbdopmj2ml"
|
||||||
|
path="res://.godot/imported/fissure.png-0ded282a93e87878ef8589bc89d4b625.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/planets/landmarks/fissure.png"
|
||||||
|
dest_files=["res://.godot/imported/fissure.png-0ded282a93e87878ef8589bc89d4b625.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=true
|
||||||
|
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/landmarks/meteor_crater.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
40
assets/planets/landmarks/meteor_crater.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dqr5ihnvvty1g"
|
||||||
|
path="res://.godot/imported/meteor_crater.png-08ae943f89e52b87a8f3b4a84efc1567.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/planets/landmarks/meteor_crater.png"
|
||||||
|
dest_files=["res://.godot/imported/meteor_crater.png-08ae943f89e52b87a8f3b4a84efc1567.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=true
|
||||||
|
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
|
||||||
25
assets/planets/landmarks/prompts.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"tool": "image_gen (built-in)",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"name": "fissure",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: a single production PNG ground-decoration sprite for a top-down Godot planet exploration game. Orthographic perfectly vertical bird-eye view, realistic detailed hand-painted game art, muted natural earth colors, soft diffuse light from upper left. Genuinely alpha transparent background, no square ground tile or frame, irregular feathered outer edges fading into transparency, isolated centered object with generous 15 percent margin on all sides. No text, no symbols, no UI, no perspective horizon. Must read at small screen size, avoid excessive pixel-level noise. A small branching geological fissure in earth: one main irregular dark narrow crack and a few short branching cracks, chipped dusty brown-grey edges and scattered tiny stones. No lava or light, no vast canyon. Only cracks and thin banks on transparent background, terrain behind visible."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meteor_crater",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: a single production PNG ground-decoration sprite for a top-down Godot planet exploration game. Orthographic perfectly vertical bird-eye view, realistic detailed hand-painted game art, muted natural earth colors, soft diffuse light from upper left. Genuinely alpha transparent background, no square ground tile or frame, irregular feathered outer edges fading into transparency, isolated centered object with generous 15 percent margin on all sides. No text, no symbols, no UI, no perspective horizon. Must read at small screen size, avoid excessive pixel-level noise. A modest old meteor impact crater: roughly circular shallow earthy bowl, broken rocky low raised rim, small dark meteor fragment slightly off center, a few subtle radial ejecta streaks fading out. Muted grey-brown rock, weathered and partially grass recolonized. No fire or glow. Irregular natural crater, low relief, no mountain."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "abandoned_hamlet",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: a single production PNG ground-decoration sprite for a top-down Godot planet exploration game. Orthographic perfectly vertical bird-eye view, realistic detailed hand-painted game art, muted natural earth colors, soft diffuse light from upper left. Genuinely alpha transparent background, no square ground tile or frame, irregular feathered outer edges fading into transparency, isolated centered object with generous 15 percent margin on all sides. No text, no symbols, no UI, no perspective horizon. Must read at small screen size, avoid excessive pixel-level noise. A tiny abandoned remote settlement viewed directly overhead: exactly four small ruined stone-and-rusted-metal huts with partly collapsed roofs loosely around a tiny empty square, broken low walls, remnants of a short footpath and an old dry well. Compact miniature cluster, small single-storey houses with humble earthy tones and sparse weeds. No tall buildings, no roads, no vehicles, no inhabitants. Thin scattered foundation edges dissolve to transparency, no solid background disk."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "weathered_stones",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: a single production PNG ground-decoration sprite for a top-down Godot planet exploration game. Orthographic perfectly vertical bird-eye view, realistic detailed hand-painted game art, muted natural earth colors, soft diffuse light from upper left. Genuinely alpha transparent background, no square ground tile or frame, irregular feathered outer edges fading into transparency, isolated centered object with generous 15 percent margin on all sides. No text, no symbols, no UI, no perspective horizon. Must read at small screen size, avoid excessive pixel-level noise. A small irregular ring of six weathered standing stones, several fallen or broken, with sparse moss and tiny grass around their bases. Natural ancient stone remnants directly overhead, muted grey granite. Empty transparent center showing the game terrain, no magic, no glowing runes, no stone floor platform."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dry_hollow",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: a single production PNG ground-decoration sprite for a top-down Godot planet exploration game. Orthographic perfectly vertical bird-eye view, realistic detailed hand-painted game art, muted natural earth colors, soft diffuse light from upper left. Genuinely alpha transparent background, no square ground tile or frame, irregular feathered outer edges fading into transparency, isolated centered object with generous 15 percent margin on all sides. No text, no symbols, no UI, no perspective horizon. Must read at small screen size, avoid excessive pixel-level noise. A small dried-out shallow pond hollow, irregular oval depression: cracked tan clay at the center, tiny pale silt stones, a soft thin broken muddy rim and sparse dry reeds on two edges. Soft naturally feathered alpha edges, no rectangular terrain patch. Subtle earth relief. No water or blue, no vegetation outside the small hollow."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
assets/planets/landmarks/weathered_stones.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
40
assets/planets/landmarks/weathered_stones.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://coiann8fxpcxe"
|
||||||
|
path="res://.godot/imported/weathered_stones.png-fad205e38e397bc53cd5ffcc254438e1.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/planets/landmarks/weathered_stones.png"
|
||||||
|
dest_files=["res://.godot/imported/weathered_stones.png-fad205e38e397bc53cd5ffcc254438e1.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=true
|
||||||
|
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/nature/nature_atlas.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
40
assets/planets/nature/nature_atlas.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://p15y76ya7e34"
|
||||||
|
path="res://.godot/imported/nature_atlas.png-8d1598b0e8c0f28311d411aab0a44d70.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/planets/nature/nature_atlas.png"
|
||||||
|
dest_files=["res://.godot/imported/nature_atlas.png-8d1598b0e8c0f28311d411aab0a44d70.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=true
|
||||||
|
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
|
||||||
23
assets/planets/nature/prompts.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"tool": "image_gen (built-in)",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: production sprite atlas for a Godot top-down planet exploration game. Create a square transparent PNG atlas with EXACTLY 4 columns and 4 rows of equal square cells, 16 isolated overhead natural props, one centered prop per cell, wide transparent margins in every cell. Strict orthographic directly overhead bird eye view, realistic hand painted detailed game art, soft light from upper left, natural muted greens and stone greys, no ground planes, no labels, no grid lines, no text. Each prop fits inside central 80 percent of its cell. Read left to right top to bottom: row1: dense dark green conifer canopy seen vertically from above; airy light green birch canopy; irregular spreading oak canopy; sparse gnarled dry tree with bare branches seen overhead. Row2: cluster of 3 rounded grey stones; solitary angular granite boulder; moss covered rock cluster; scatter of tiny pale pebbles. Row3: compact leafy shrub; radial fern cluster; patch of tiny cream and lavender wildflowers with green leaves; clump of dry golden grasses. Row4: horizontal fallen dead tree trunk with broken branches and subtle moss seen overhead, no standing tree; weathered cut tree stump seen overhead with growth rings; cluster of small russet mushrooms with leaves; small reed tussock. Consistent detailed painted rendering suitable with existing realistic green overhead tree sprites. All backgrounds genuinely alpha transparent, no dark backdrop, no touching neighboring cells, no perspective horizon. Output square image.",
|
||||||
|
"layout": "16 sprites, 4 columns; custom AtlasTexture regions preserve painted gutters",
|
||||||
|
"cells": [
|
||||||
|
"conifer",
|
||||||
|
"birch",
|
||||||
|
"oak",
|
||||||
|
"dry_tree",
|
||||||
|
"stones",
|
||||||
|
"granite",
|
||||||
|
"mossy_rocks",
|
||||||
|
"pebbles",
|
||||||
|
"shrub",
|
||||||
|
"ferns",
|
||||||
|
"wildflowers",
|
||||||
|
"dry_grass",
|
||||||
|
"fallen_tree",
|
||||||
|
"stump",
|
||||||
|
"mushrooms",
|
||||||
|
"reeds"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -528,3 +528,203 @@ Aktualizacja materiałów powierzchni (18.10.2026):
|
|||||||
umieszczenia budynków na mapę, brak rzek i zoom powierzchni 0,8.
|
umieszczenia budynków na mapę, brak rzek i zoom powierzchni 0,8.
|
||||||
- Nowe tekstury wygenerowano wbudowanym imagegen. Pliki i komplet promptów:
|
- Nowe tekstury wygenerowano wbudowanym imagegen. Pliki i komplet promptów:
|
||||||
assets/planets/terrain/ oraz assets/planets/terrain/prompts.json.
|
assets/planets/terrain/ oraz assets/planets/terrain/prompts.json.
|
||||||
|
|
||||||
|
Drobne dekoracje przyrody (18.10.2026):
|
||||||
|
- Dodano atlas PNG z 16 grafikami: iglak, brzoza, dąb, suche drzewo,
|
||||||
|
otoczaki, granit, omszałe skały, drobny żwir, krzew, paprocie,
|
||||||
|
kwiaty, sucha trawa, powalone drzewo, pień, grzyby i trzciny.
|
||||||
|
- Dotychczasowe drzewa pozostają częścią lasów. Nowe gatunki i runo
|
||||||
|
dobierane są do sześciu krajobrazów; trzciny rosną przy brzegach,
|
||||||
|
suche trawy dominują w suchym i chłodnym klimacie.
|
||||||
|
- Generator próbuje rozmieścić 150 niewielkich skupisk po 2–5 detali.
|
||||||
|
Woda, góry i polany ograniczają ich ostateczną liczbę.
|
||||||
|
- Powalone drzewo: 8% szansy próby na leśnej mapie (krajobrazy 0–3),
|
||||||
|
najwyżej jeden egzemplarz, jeśli znajdzie się suche miejsce w pobliżu lasu.
|
||||||
|
- Dekoracje omijają lądowisko, POI, zabudowania i pytajniki wydarzeń.
|
||||||
|
Są wyłącznie wizualne; nie zmieniają kolizji, nagród ani zasad eksploracji.
|
||||||
|
- Osobny strumień losowania detali zachowuje dotychczasowe rozmieszczenie
|
||||||
|
POI, budynków i wydarzeń dla tego samego ziarna mapy.
|
||||||
|
- Grafika: wbudowany imagegen. Atlas i pełny prompt:
|
||||||
|
assets/planets/nature/nature_atlas.png oraz prompts.json.
|
||||||
|
- Weryfikacja: render sześciu krajobrazów, powtarzalność generatora,
|
||||||
|
wolne miejsca interakcji oraz limit powalonych drzew w 240 próbach.
|
||||||
|
|
||||||
|
Korekta stabilności detali podczas ruchu (18.10.2026):
|
||||||
|
- Włączono generowanie mipmap dla nature_atlas.png. Powierzchnia używa
|
||||||
|
filtrowania liniowego z mipmapami; atlas wcześniej nie miał tych poziomów,
|
||||||
|
co powodowało migotanie drobnych detali przy pomniejszeniu i ruchu kamery.
|
||||||
|
|
||||||
|
Skala obiektów i rzadkie ślady krajobrazu (18.10.2026):
|
||||||
|
- Dekoracyjne budynki zmniejszono o 35%. Grafiki kopalń i ruin POI również
|
||||||
|
zmniejszono o 35%; podpisy przesunięto bliżej. Złoża zachowują skalę.
|
||||||
|
- Drzewa zmniejszono o 20% (rozmiar 52–92); powalony pień z 115 do 92.
|
||||||
|
- Dodano pięć osobnych grafik: rozgałęzione pęknięcie ziemi, stary krater
|
||||||
|
z fragmentem meteorytu, opuszczoną osadę czterech domków, zwietrzały
|
||||||
|
krąg kamieni i wyschnięte oczko wodne z gliniastym dnem.
|
||||||
|
- Szanse próby umieszczenia na mapie: pęknięcia 55% (1–3), krater 14%,
|
||||||
|
osada 6%, kamienie 12%, wyschnięte oczko 30% w krajobrazach 3–5
|
||||||
|
i 12% w krajobrazach 0–2. Poza pęknięciami najwyżej jeden danego rodzaju.
|
||||||
|
- Powstają tylko na suchych parcelach, poza lądowiskiem, wodą, górami,
|
||||||
|
istniejącą zabudową, POI i wydarzeniami. Drzewa, skały i runo nie
|
||||||
|
zasłaniają nowych dekoracji. Brak odpowiedniego miejsca pomija obiekt.
|
||||||
|
- To ozdoby bez interakcji i nagród. Generator ma osobne ziarno losowania;
|
||||||
|
nie dodaje nowych wydarzeń, przeciwników ani wymagań eksploracji.
|
||||||
|
- Wszystkie nowe grafiki korzystają z mipmap i filtrowania liniowego,
|
||||||
|
aby ograniczyć migotanie podczas przesuwania kamery.
|
||||||
|
- Grafiki: wbudowany imagegen. Pliki i pełne prompty:
|
||||||
|
assets/planets/landmarks/ oraz assets/planets/landmarks/prompts.json.
|
||||||
|
- Weryfikacja tej aktualizacji: test 240 map (wszystkie pięć rodzajów ozdób,
|
||||||
|
odstępy i suchy teren), test przyrody w sześciu biomach oraz render
|
||||||
|
pięciu dodatków i porównania skali. Potwierdzono mipmapy i przezroczystość
|
||||||
|
każdej nowej tekstury. Podgląd: artifacts/landmarks_overview.png.
|
||||||
|
|
||||||
|
Dziesięć wydarzeń fabularnych na powierzchni (18.10.2026):
|
||||||
|
- Zastąpiono poprzednie spotkanie z inżynierem katalogiem dziesięciu
|
||||||
|
wydarzeń z zatwierdzonymi opisami i odmiennym przebiegiem.
|
||||||
|
- Na mapie jest 50% szansy próby umieszczenia jednego znaku zapytania.
|
||||||
|
Dziesięć rodzajów ma jednakową wagę. Miejsce musi być suche, poza
|
||||||
|
górami, lądowiskiem, zabudową i POI. Pozostałe 50% nie ma wydarzenia.
|
||||||
|
- Zachowano automatyczne otwarcie przy 180 jednostkach, ponowne uzbrojenie
|
||||||
|
po oddaleniu ponad 260 oraz ręczne otwarcie klawiszem F.
|
||||||
|
- Rodzaje: Wędrowny handlarz; Wymiana towarów; Kolekcjoner minerałów;
|
||||||
|
Kontener w ciemno; Płatny skan skrytki; Pewna wypłata albo ryzyko;
|
||||||
|
Test prototypu; Akcja ratunkowa; Ewakuacja etapami; Mobilny serwis.
|
||||||
|
- Oferta i ukryty łup są przygotowywane raz. Zamknięcie okna nie odświeża
|
||||||
|
nagród, prób ani cen. Zrealizowane spotkania są zużyte; skan i ewakuacja
|
||||||
|
pamiętają etap także po zamknięciu okna.
|
||||||
|
- Handlarz: wybór jednej z trzech ofert. 3–6 rakiet kosztuje 70% ceny
|
||||||
|
sklepowej całego pakietu, po rabacie kapitana. Pozostałe oferty to
|
||||||
|
8 miedzi i 8 krzemu za bazowo 70 kredytów/poziom układu albo znany
|
||||||
|
moduł za bazowo 140 kredytów/poziom, z widocznymi parametrami w podpowiedzi.
|
||||||
|
- Wymiana: 12 żelaza i 4 miedzi na 8 niklu lub 6 żelaza, siarki i uranu
|
||||||
|
na 3 rakiety. Kolekcjoner płaci 120 kredytów/poziom za 6 sztuk
|
||||||
|
wylosowanego surowca planetarnego.
|
||||||
|
- Kontener: bazowo 100 kredytów/poziom. Skan: 15 kredytów/poziom, a potem
|
||||||
|
osobno 75 za otwarcie. Skan ujawnia grupę łupu, bez ilości i parametrów.
|
||||||
|
- Tajemniczy łup: 50% na 2–8 części, 35% na 6–15 sztuk jednego z pięciu
|
||||||
|
materiałów planetarnych, 10% na 2–4 rakiety, 5% na moduł statku.
|
||||||
|
Moduły mają poziom układu + geometryczny bonus 0–20, z maksimum 1000;
|
||||||
|
szczęście kapitana nie podnosi tego limitu. Brak sprzętu kapitana w łupie.
|
||||||
|
- Wybór pewny/losowy: ta sama bazowa cena 80 kredytów/poziom; gracz wybiera
|
||||||
|
gwarantowane 6 części albo tajemniczy pakiet z powyższej puli.
|
||||||
|
- Prototyp: gwarantowane 35 kredytów/poziom, premia za sukces dodatkowo
|
||||||
|
90 kredytów/poziom i moduł. Szansa to pierwszy etap POI minus 10 pp;
|
||||||
|
porażka zadaje 12–20 HP plus do 10 HP za trudność planety.
|
||||||
|
- Ratunek: koszt 6 części; sukces daje 140 kredytów/poziom i 8 części.
|
||||||
|
Szansa jak pierwszy etap POI; porażka 8–16 HP plus do 10 za biom.
|
||||||
|
- Ewakuacja: trzy etapy ze szansami POI po sprzęcie i warunkach. Koszty
|
||||||
|
2/3/4 części, wypłaty 40/70/110 kredytów/poziom i 2/4/6 części.
|
||||||
|
Po każdym sukcesie można zakończyć albo kontynuować. Porażka kończy
|
||||||
|
wydarzenie i zadaje 8–16 HP + 4 za indeks etapu + do 10 za biom,
|
||||||
|
ale nie zabiera wcześniejszych nagród. Nie ma zapłaty za nieudany etap.
|
||||||
|
- Serwis: jednorazowa naprawa do 25 HP, ograniczona do brakującego kadłuba;
|
||||||
|
cena bazowo 4 kredyty za HP i poziom układu. Brak opłaty przy pełnym HP.
|
||||||
|
- Ceny zakupów za kredyty uwzględniają rabat kapitana. Koszt i ewentualne
|
||||||
|
ryzyko są widoczne przed decyzją; brak zasobów wyłącza przycisk i jest
|
||||||
|
ponownie sprawdzany w logice transakcji.
|
||||||
|
- Koszty pobierane są od razu, najpierw z tymczasowego łupu, potem z zasobów
|
||||||
|
kapitana. Kredyty, części, rakiety, minerały i moduły zdobyte na powierzchni
|
||||||
|
pozostają tymczasowe do bezpiecznego powrotu. Zniszczenie usuwa nowy łup,
|
||||||
|
nie zwraca wydatków i zachowuje wcześniejszy majątek.
|
||||||
|
- Rakiety po powrocie trafiają do amunicji statku, części do części kapitana,
|
||||||
|
kredyty do portfela. Pełna ładownia nadal pozostawia moduły we frachtowcu.
|
||||||
|
Rozliczenie i odbiór nie duplikują nagród.
|
||||||
|
- Dialog ma przewijanie, opis fabularny, koszty, jawne nagrody oraz szansę
|
||||||
|
i zakres obrażeń przy ryzyku. Zniszczenie używa tego samego efektu co POI.
|
||||||
|
- Testy: komplet 10 zdarzeń i transakcji, 501 wydarzeń w 1000 prób na
|
||||||
|
dostępnym terenie, sukces/porażka, wieloetapowość, odmowy i brak środków,
|
||||||
|
zachowanie stanu po ponownym otwarciu, łup przy powrocie i zniszczeniu,
|
||||||
|
faktyczne przekazanie rakiet statkowi oraz render wszystkich dialogów.
|
||||||
|
- Podgląd: artifacts/events_overview.png. Źródło opisów i parametrów:
|
||||||
|
scripts/planets/surface_events.gd.
|
||||||
|
|
||||||
|
Korekta poziomów modułów z wydarzeń (18.10.2026):
|
||||||
|
- Moduł może mieć każdą normalną jakość lootu, od zwykłego do legendarnego.
|
||||||
|
Używa generate_for_definition z tierem 0, bez wymuszenia lepszej rzadkości.
|
||||||
|
- Bonus poziomu modułu z wydarzenia ograniczono do +0..+10 ponad poziom
|
||||||
|
układu. Wagi wynoszą 0,65^k, normalizowane dla 11 możliwych wyników.
|
||||||
|
Szansa maleje wykładniczo wraz z bonusem. Nie ma kumulacji ogona na +10.
|
||||||
|
- Zachowano maksimum poziomu 1000. Szczęście nie przekracza limitu +10.
|
||||||
|
- Korekta dotyczy wszystkich modułów z wydarzeń, także ofert handlarza
|
||||||
|
i premii za prototyp. Standardowy loot i nagrody POI zachowują swoje zasady.
|
||||||
|
- Test: 100000 losowań poziomu oraz 1000 modułów z kontrolą jakości,
|
||||||
|
zakresu poziomów i limitu 1000.
|
||||||
|
|
||||||
|
Wspólne skalowanie nagród według biomu (18.10.2026):
|
||||||
|
- Zasady w scripts/planets/planet_rewards.gd obowiązują moduły zarówno
|
||||||
|
z wydarzeń, jak i z POI, łącznie z ofertami handlarza i prototypem.
|
||||||
|
- Premia poziomu ponad poziom układu: biom łatwy (trudność 0) +0..+2,
|
||||||
|
średni (0,5) +1..+6, najtrudniejszy (1) +2..+10. Pomiędzy nimi granice
|
||||||
|
zmieniają się płynnie: minimum round(2*d), maksimum round(2+8*d).
|
||||||
|
- W obrębie zakresu wagi kolejnych bonusów wynoszą 0,65^k, normalizowane
|
||||||
|
do liczby dostępnych poziomów. Dolna granica jest najczęstsza, górna
|
||||||
|
najrzadsza. Maksymalny poziom przedmiotu pozostaje 1000.
|
||||||
|
- Dodatkowe etapy POI oraz szczęście nie podnoszą poziomu ponad zakres
|
||||||
|
biomu. Zachowano dotychczasowe zasady jakości: zwykłe losowanie dla
|
||||||
|
wydarzeń, istniejącą premię jakości trudnych/głębokich POI.
|
||||||
|
- Minerały, materiały planetarne i części otrzymują mnożnik 1+2*d,
|
||||||
|
czyli ×1 / ×2 / ×3. Dotyczy również skrzyń, gwarantowanego zestawu,
|
||||||
|
paczek handlarza, wymiany minerałów, ratunku i nagród etapów ewakuacji.
|
||||||
|
POI zachowują dodatkowy mnożnik ilości za głębszy etap.
|
||||||
|
- Wyraźnie wykluczono z mnożnika rakiety, amunicję zwykłą i radiacyjną,
|
||||||
|
inne przedmioty użytkowe oraz kredyty. Ilość rakiet pozostaje taka sama:
|
||||||
|
3–6 u handlarza, 3 w wymianie, 2–4 w tajemniczym pakiecie.
|
||||||
|
- Cena pakietu rakiet po rabacie kapitana: 70% ceny sklepowej na łatwym,
|
||||||
|
60% na średnim i 50% na najtrudniejszym biomie. W wymianie 3 rakiet
|
||||||
|
koszt żelaza, siarki i uranu maleje z 6 przez 5 do 4 sztuk każdego.
|
||||||
|
- Koszty innych decyzji, wypłaty kredytów, szanse powodzenia i obrażenia
|
||||||
|
zachowują dotychczasowe zasady. Skala materiałów nie mnoży kosztów.
|
||||||
|
- Premie widać w dialogach wydarzeń i POI; jawna oferta pokazuje faktyczną
|
||||||
|
ilość po skalowaniu. Nagrody są skalowane tylko raz.
|
||||||
|
- Testy: 30000 losowań bonusu poziomu w trzech biomach, 3600 prób POI,
|
||||||
|
600 porównań tajemniczego łupu; stałe ilości amunicji, niższe ceny
|
||||||
|
rakiet, pełny zakres poziomów, limit 1000 i brak podwójnego mnożenia.
|
||||||
|
- Dodatkowo przeszły testy wszystkich dziesięciu wydarzeń, pełnego powrotu
|
||||||
|
z wyprawy oraz render dialogów na najtrudniejszym biomie.
|
||||||
|
Podgląd: artifacts/hard_events_overview.png.
|
||||||
|
|
||||||
|
Globalne zagrożenie i ochrona kapitana (18.10.2026):
|
||||||
|
- Zastąpiono poprzednie kary i dodatkowy bonus poziomów nowym wskaźnikiem.
|
||||||
|
Trzy zagrożenia mają osobno zakres 0–100%, suma wynosi 0–300%.
|
||||||
|
- Tlen: brak zagrożenia przy co najmniej 80%; rośnie liniowo do 100%
|
||||||
|
zagrożenia przy 0% tlenu. Wartość jest ograniczona do zakresu 0–100%.
|
||||||
|
- Temperatura: bezpieczny przedział −10..+40°C. Mróz −50°C i upał +100°C
|
||||||
|
dają po 100% zagrożenia termicznego; oba kierunki rosną liniowo.
|
||||||
|
- Zagrożenie biologiczne: wartość 0..50 odpowiada 0..100% zagrożenia.
|
||||||
|
- System oddychania odejmuje 2 punkty procentowe zagrożenia tlenu za każdy
|
||||||
|
swój poziom; skafander tak samo temperaturę, broń ręczna biologię.
|
||||||
|
L1 daje już 2 pp ochrony, L25 daje 50 pp, L50 daje 100 pp. Ochrona
|
||||||
|
działa tylko na własną składową, nie przenosi nadwyżki na inne.
|
||||||
|
- Globalny wskaźnik jest sumą pozostałych trzech zagrożeń po ochronie.
|
||||||
|
Nie może spaść poniżej 0. Usunięto dawny bonus +0,4 pp do szans za poziom.
|
||||||
|
- Szanse etapów POI: przy zagrożeniu 0% wynoszą 90% / 70% / 40%.
|
||||||
|
Maleją liniowo do 15% / 5% / 0% przy zagrożeniu 150%. Od 150% do 300%
|
||||||
|
pozostają na tych wartościach. Przykład dla 75%: 52,5% / 37,5% / 20%.
|
||||||
|
- Średnia szansa pokazywana przed lądowaniem to średnia arytmetyczna
|
||||||
|
trzech etapów, a nie szansa przejścia wszystkich etapów naraz.
|
||||||
|
Daje 66,7% przy zagrożeniu 0% i 6,7% przy zagrożeniu co najmniej 150%.
|
||||||
|
- Skan pokazuje warunki, każdą surową składową i wynik po sprzęcie, sumę
|
||||||
|
globalną, średnią i trzy szanse POI oraz koszt wejścia w atmosferę.
|
||||||
|
Wartości są przeliczane dla aktualnie założonego wyposażenia.
|
||||||
|
- Powierzchnia i dialogi również pokazują globalne zagrożenie po sprzęcie.
|
||||||
|
Podpowiedzi przedmiotów oraz warsztat opisują nową ochronę 2 pp/poziom.
|
||||||
|
- Ryzykowne wydarzenia '?' korzystają z tego samego wskaźnika: ratunek
|
||||||
|
z pierwszego etapu, ewakuacja z kolejnych etapów. Prototyp zachowuje
|
||||||
|
dodatkowe −10 pp szans: 80% bez zagrożenia, 5% od zagrożenia 150%.
|
||||||
|
Zniesiono dawne minimum 8%, aby decyzja mogła rzeczywiście mieć 0%.
|
||||||
|
- Próby z zerową szansą są wyłączone w UI i odrzucane w logice bez
|
||||||
|
kosztu, obrażeń, zamykania miejsca ani zmiany losowania POI.
|
||||||
|
Zakończenie pomocy i powrót do lotu pozostają dostępne.
|
||||||
|
- Bezpieczne transakcje handlowe nie otrzymują losowej szansy porażki.
|
||||||
|
- Premie materiałów i poziomów modułów nadal bazują na rzeczywistych
|
||||||
|
warunkach planety przed ochroną. Sprzęt nie zmniejsza nagrody biomu.
|
||||||
|
Surowa trudność to suma trzech zagrożeń podzielona przez 300.
|
||||||
|
- Koszt wejścia w atmosferę i zakres obrażeń po niepowodzeniu nadal
|
||||||
|
wynikają z surowej trudności; ochrona wpływa na szansę uniknięcia porażki.
|
||||||
|
- Testy: zakres 0..300, oba skrajne kierunki temperatury, neutralne warunki,
|
||||||
|
niezależność trzech slotów, L1/L25/L26/L50, ciągłość i próg 150%, pełna
|
||||||
|
neutralizacja, średnia skanu, ryzyko wydarzeń i brak opłaty przy 0% szans.
|
||||||
|
Przeszły też regresje wydarzeń, skalowania łupu, wypraw i panelu skanu.
|
||||||
|
- Zweryfikowano render skanu bez zagrożenia, przy 150% i po ochronie L50,
|
||||||
|
a także blokadę trzeciego etapu kopalni oraz ewakuacji.
|
||||||
|
Podgląd: artifacts/threat_system_overview.png.
|
||||||
|
|||||||
@ -197,7 +197,11 @@ func finish_surface(success: bool) -> void:
|
|||||||
var result: Dictionary = progress.finish_visit(GameState.captain,success)
|
var result: Dictionary = progress.finish_visit(GameState.captain,success)
|
||||||
progress.deliver_modules(player.get_node("ShipInventory"))
|
progress.deliver_modules(player.get_node("ShipInventory"))
|
||||||
_leave()
|
_leave()
|
||||||
var cargo_text := UI.material_text(result.get("materials",{}))
|
var returned: Dictionary = result.get("materials",{}).duplicate()
|
||||||
|
if int(result.get("parts",0)) > 0: returned.parts = result.parts
|
||||||
|
if int(result.get("missiles",0)) > 0: returned.missile = result.missiles
|
||||||
|
if int(result.get("credits",0)) > 0: returned.credits = result.credits
|
||||||
|
var cargo_text := UI.material_text(returned)
|
||||||
_notice(("POWRÓT Z WYPRAWY · " + (cargo_text if not cargo_text.is_empty() else "brak łupu")) if result.get("success",false) else "LĄDOWNIK ZNISZCZONY · Utracono łup bieżącej wyprawy.")
|
_notice(("POWRÓT Z WYPRAWY · " + (cargo_text if not cargo_text.is_empty() else "brak łupu")) if result.get("success",false) else "LĄDOWNIK ZNISZCZONY · Utracono łup bieżącej wyprawy.")
|
||||||
if not progress.delivered_modules.is_empty():
|
if not progress.delivered_modules.is_empty():
|
||||||
_message += "\nBrak miejsca: %d modułów czeka we frachtowcu." % progress.delivered_modules.size()
|
_message += "\nBrak miejsca: %d modułów czeka we frachtowcu." % progress.delivered_modules.size()
|
||||||
|
|||||||
@ -46,6 +46,7 @@ static func button(parent: Node, text: String, action: Callable) -> Button:
|
|||||||
static func material_text(materials: Dictionary) -> String:
|
static func material_text(materials: Dictionary) -> String:
|
||||||
var names: Dictionary = preload("res://scripts/items/crafting_recipes.gd").NAMES.duplicate()
|
var names: Dictionary = preload("res://scripts/items/crafting_recipes.gd").NAMES.duplicate()
|
||||||
names.merge(preload("res://scripts/planets/captain_equipment.gd").MATERIAL_NAMES)
|
names.merge(preload("res://scripts/planets/captain_equipment.gd").MATERIAL_NAMES)
|
||||||
|
names.merge({"parts":"Części", "missile":"Rakiety", "credits":"Kredyty"})
|
||||||
var pieces: PackedStringArray = []
|
var pieces: PackedStringArray = []
|
||||||
for key in materials:
|
for key in materials:
|
||||||
pieces.append("%s: %d" % [names.get(key, key), materials[key]])
|
pieces.append("%s: %d" % [names.get(key, key), materials[key]])
|
||||||
|
|||||||
@ -92,7 +92,7 @@ func _refresh() -> void:
|
|||||||
UI.label(_content, "Jeśli ładownia jest pełna, moduły czekają na odbiór.", 13)
|
UI.label(_content, "Jeśli ładownia jest pełna, moduły czekają na odbiór.", 13)
|
||||||
elif _tab == "workshop":
|
elif _tab == "workshop":
|
||||||
UI.label(_content, "WARSZTAT / EKWIPUNEK KAPITANA", 23, UI.CYAN)
|
UI.label(_content, "WARSZTAT / EKWIPUNEK KAPITANA", 23, UI.CYAN)
|
||||||
UI.label(_content, "Wyższy poziom zwiększa szanse przy POI.\nSprzęt powstaje wyłącznie w warsztacie.", 15)
|
UI.label(_content, "Każdy poziom usuwa 2 pp odpowiedniego zagrożenia.\nL50 neutralizuje je całkowicie. Sprzęt powstaje w warsztacie.", 15)
|
||||||
for kind in GEAR.KINDS:
|
for kind in GEAR.KINDS:
|
||||||
var card := UI.panel(_content)
|
var card := UI.panel(_content)
|
||||||
var row := HBoxContainer.new()
|
var row := HBoxContainer.new()
|
||||||
|
|||||||
@ -26,27 +26,52 @@ static func generate(system_seed: int, index: int, name: String) -> Dictionary:
|
|||||||
var oxygen := rng.randi_range(81, 100) if easy else rng.randi_range(0, 80)
|
var oxygen := rng.randi_range(81, 100) if easy else rng.randi_range(0, 80)
|
||||||
var temperature := rng.randi_range(-10, 40) if easy else rng.randi_range(-50, 100)
|
var temperature := rng.randi_range(-10, 40) if easy else rng.randi_range(-50, 100)
|
||||||
var flora := 0 if easy else rng.randi_range(5, 50)
|
var flora := 0 if easy else rng.randi_range(5, 50)
|
||||||
var o := maxf(0.0, 80.0 - oxygen) / 80.0
|
var raw := raw_threats({"oxygen":oxygen,"temperature":temperature,"flora":flora})
|
||||||
var t := maxf(0.0, maxf(-10.0 - temperature, temperature - 40.0)) / 60.0
|
var difficulty := (float(raw.oxygen)+float(raw.temperature)+float(raw.biology))/300.0
|
||||||
var f := float(flora) / 50.0
|
|
||||||
var difficulty := clampf((o + t + f) / 3.0, 0, 1)
|
|
||||||
return {"id": "%d:planet:%d" % [system_seed, index], "name": name, "oxygen": oxygen, "temperature": temperature, "flora": flora, "easy": easy, "difficulty": difficulty, "landing_damage": roundi(lerpf(5.0, 20.0, difficulty)), "seed": rng.randi(), "index": index}
|
return {"id": "%d:planet:%d" % [system_seed, index], "name": name, "oxygen": oxygen, "temperature": temperature, "flora": flora, "easy": easy, "difficulty": difficulty, "landing_damage": roundi(lerpf(5.0, 20.0, difficulty)), "seed": rng.randi(), "index": index}
|
||||||
|
|
||||||
static func success_chance(profile: Dictionary, captain: Resource, stage: int) -> float:
|
const THREAT_KEYS := ["oxygen","temperature","biology"]
|
||||||
|
const POI_SAFE_CHANCES := [0.90,0.70,0.40]
|
||||||
|
const POI_DANGER_CHANCES := [0.15,0.05,0.0]
|
||||||
|
|
||||||
|
static func raw_threats(profile: Dictionary) -> Dictionary:
|
||||||
|
var oxygen := clampf((80.0-float(profile.oxygen))/80.0,0.0,1.0)*100.0
|
||||||
|
var temperature := float(profile.temperature)
|
||||||
|
var thermal := 0.0
|
||||||
|
if temperature < -10.0:
|
||||||
|
thermal = clampf((-10.0-temperature)/40.0,0.0,1.0)*100.0
|
||||||
|
elif temperature > 40.0:
|
||||||
|
thermal = clampf((temperature-40.0)/60.0,0.0,1.0)*100.0
|
||||||
|
var biology := clampf(float(profile.flora)/50.0,0.0,1.0)*100.0
|
||||||
|
return {"oxygen":oxygen,"temperature":thermal,"biology":biology}
|
||||||
|
|
||||||
|
static func threat_report(profile: Dictionary, captain: Resource) -> Dictionary:
|
||||||
var gear := preload("res://scripts/planets/captain_equipment.gd")
|
var gear := preload("res://scripts/planets/captain_equipment.gd")
|
||||||
var penalties := [
|
var raw := raw_threats(profile)
|
||||||
maxf(0, 80.0 - float(profile.oxygen)) / 80.0,
|
var effective := {}
|
||||||
maxf(0, maxf(-10.0 - float(profile.temperature), float(profile.temperature) - 40.0)) / 60.0,
|
var raw_total := 0.0
|
||||||
float(profile.flora) / 50.0]
|
var total := 0.0
|
||||||
var environment := 0.0
|
|
||||||
var equipment_bonus := 0.0
|
|
||||||
for index in 3:
|
for index in 3:
|
||||||
var rank: int = gear.level(captain, gear.KINDS[index])
|
var key: String = THREAT_KEYS[index]
|
||||||
var protection := minf(0.8, maxf(0, rank - 1) * 0.06)
|
var protection := minf(100.0,2.0*maxi(0,gear.level(captain,gear.KINDS[index])))
|
||||||
environment += penalties[index] * (1.0 - protection) / 3.0
|
effective[key] = maxf(0.0,float(raw[key])-protection)
|
||||||
equipment_bonus += maxf(0, rank - 1) * 0.004
|
raw_total += float(raw[key])
|
||||||
var base: float = [0.90, 0.60, 0.35][clampi(stage, 0, 2)]
|
total += float(effective[key])
|
||||||
return clampf(base - environment * 0.35 + minf(0.12, equipment_bonus), 0.08, 0.98)
|
return {"raw":raw,"effective":effective,"raw_total":raw_total,"total":total}
|
||||||
|
|
||||||
|
static func global_threat(profile: Dictionary, captain: Resource) -> float:
|
||||||
|
return float(threat_report(profile,captain).total)
|
||||||
|
|
||||||
|
static func success_chance(profile: Dictionary, captain: Resource, stage: int) -> float:
|
||||||
|
var index := clampi(stage,0,2)
|
||||||
|
var severity := clampf(global_threat(profile,captain)/150.0,0.0,1.0)
|
||||||
|
return lerpf(POI_SAFE_CHANCES[index],POI_DANGER_CHANCES[index],severity)
|
||||||
|
|
||||||
|
static func average_success_chance(profile: Dictionary, captain: Resource) -> float:
|
||||||
|
var total := 0.0
|
||||||
|
for stage in 3:
|
||||||
|
total += success_chance(profile,captain,stage)
|
||||||
|
return total/3.0
|
||||||
|
|
||||||
static func biome_name(profile: Dictionary) -> String:
|
static func biome_name(profile: Dictionary) -> String:
|
||||||
if bool(profile.easy):
|
if bool(profile.easy):
|
||||||
@ -58,12 +83,11 @@ static func biome_name(profile: Dictionary) -> String:
|
|||||||
return "Dzika biosfera"
|
return "Dzika biosfera"
|
||||||
|
|
||||||
static func scan_description(profile: Dictionary, captain: Resource) -> String:
|
static func scan_description(profile: Dictionary, captain: Resource) -> String:
|
||||||
var oxygen := int(profile.oxygen)
|
var report := threat_report(profile,captain)
|
||||||
var temperature := int(profile.temperature)
|
return "%s · %s\nTLEN %d%% · zagrożenie %.0f%% → %.0f%%\nTEMPERATURA %d°C · zagrożenie %.0f%% → %.0f%%\nBIOLOGICZNE %d/50 · zagrożenie %.0f%% → %.0f%%\nZAGROŻENIE GLOBALNE: %.1f%% / 300%%\nBez sprzętu: %.1f%% · redukcja 2 pp/poziom\nŚREDNIA SZANSA POI: %.1f%%\nEtapy POI: %.1f%% / %.1f%% / %.1f%%\nLądowanie: −%d HP · Lądownik: %d%%" % [
|
||||||
var flora := int(profile.flora)
|
profile.name,biome_name(profile),profile.oxygen,report.raw.oxygen,report.effective.oxygen,
|
||||||
var breathing := "dogodne oddychanie" if oxygen > 80 else "system oddychania poprawia szanse"
|
profile.temperature,report.raw.temperature,report.effective.temperature,
|
||||||
var climate := "łagodny klimat" if temperature >= -10 and temperature <= 40 else "skafander ogranicza ryzyko"
|
profile.flora,report.raw.biology,report.effective.biology,
|
||||||
var biology := "brak zagrożeń biologicznych" if flora == 0 else "broń ręczna ogranicza zagrożenia"
|
report.total,report.raw_total,average_success_chance(profile,captain)*100.0,
|
||||||
return "%s · %s\nTLEN %d%% — %s\nTEMPERATURA %d°C — %s\nFLORA %d/50 — %s\nLądowanie: −%d HP · Lądownik: %d%%\nPierwszy etap POI: %d%% szans z obecnym sprzętem" % [
|
success_chance(profile,captain,0)*100.0,success_chance(profile,captain,1)*100.0,success_chance(profile,captain,2)*100.0,
|
||||||
profile.name,biome_name(profile),oxygen,breathing,temperature,climate,flora,biology,
|
profile.landing_damage,ceili(captain.expedition.lander_hp)]
|
||||||
profile.landing_damage,ceili(captain.expedition.lander_hp),roundi(success_chance(profile,captain,0)*100)]
|
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
extends Resource
|
extends Resource
|
||||||
const EQUIPMENT := preload("res://scripts/planets/captain_equipment.gd")
|
const EQUIPMENT := preload("res://scripts/planets/captain_equipment.gd")
|
||||||
|
const REWARDS := preload("res://scripts/planets/planet_rewards.gd")
|
||||||
const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
|
const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
|
||||||
@export var lander_hp := 100.0
|
@export var lander_hp := 100.0
|
||||||
@export var visits: Dictionary = {}
|
@export var visits: Dictionary = {}
|
||||||
@export var scans: Dictionary = {}
|
@export var scans: Dictionary = {}
|
||||||
@export var delivered_modules: Array[Resource] = []
|
@export var delivered_modules: Array[Resource] = []
|
||||||
@export var research_records: Dictionary = {}
|
@export var research_records: Dictionary = {}
|
||||||
|
@export var delivered_missiles := 0
|
||||||
|
var cargo_credits := 0
|
||||||
|
var cargo_parts := 0
|
||||||
|
var cargo_missiles := 0
|
||||||
var active := false
|
var active := false
|
||||||
var cargo: Dictionary = {}
|
var cargo: Dictionary = {}
|
||||||
var cargo_modules: Array[Resource] = []
|
var cargo_modules: Array[Resource] = []
|
||||||
@ -37,6 +42,9 @@ func begin_visit(profile: Dictionary) -> bool:
|
|||||||
var count := int(visits.get(profile.id, 0)) + 1
|
var count := int(visits.get(profile.id, 0)) + 1
|
||||||
visits[profile.id] = count
|
visits[profile.id] = count
|
||||||
visit_seed = int(profile.seed) ^ (count * 7919)
|
visit_seed = int(profile.seed) ^ (count * 7919)
|
||||||
|
cargo_credits = 0
|
||||||
|
cargo_parts = 0
|
||||||
|
cargo_missiles = 0
|
||||||
cargo.clear()
|
cargo.clear()
|
||||||
cargo_modules.clear()
|
cargo_modules.clear()
|
||||||
cargo_records.clear()
|
cargo_records.clear()
|
||||||
@ -60,11 +68,17 @@ func resolve_poi(captain: Resource, poi: Dictionary, rng: RandomNumberGenerator)
|
|||||||
return {}
|
return {}
|
||||||
var stage := int(poi.get("stage", 0))
|
var stage := int(poi.get("stage", 0))
|
||||||
var chance := PROFILES.success_chance(active_profile, captain, stage)
|
var chance := PROFILES.success_chance(active_profile, captain, stage)
|
||||||
var success := rng.randf() < chance
|
var success := false
|
||||||
var result := {"success": success, "chance": chance, "damage": 0, "materials": {}, "module": null}
|
var result := {"success": success, "chance": chance, "damage": 0, "materials": {}, "module": null}
|
||||||
|
if chance <= 0.0:
|
||||||
|
result.blocked = true
|
||||||
|
result.reason = "Ten etap ma 0% szans. Potrzebujesz lepszego wyposażenia, aby zmniejszyć zagrożenie."
|
||||||
|
return result
|
||||||
|
success = rng.randf() < chance
|
||||||
|
result.success = success
|
||||||
if success:
|
if success:
|
||||||
var difficulty := float(active_profile.difficulty)
|
var difficulty := float(active_profile.difficulty)
|
||||||
var multiplier := (1.0 + difficulty * 2.0) * (1.0 + stage * 0.65)
|
var multiplier := REWARDS.material_multiplier(active_profile) * (1.0 + stage * 0.65)
|
||||||
var ordinary: Array = ["iron", "titanium", "aluminium", "uranium", "sulfur"]
|
var ordinary: Array = ["iron", "titanium", "aluminium", "uranium", "sulfur"]
|
||||||
var pool: Array = PROFILES.MATERIALS if rng.randf() < 0.55 else ordinary
|
var pool: Array = PROFILES.MATERIALS if rng.randf() < 0.55 else ordinary
|
||||||
var key: String = pool[rng.randi_range(0, pool.size() - 1)]
|
var key: String = pool[rng.randi_range(0, pool.size() - 1)]
|
||||||
@ -74,8 +88,7 @@ func resolve_poi(captain: Resource, poi: Dictionary, rng: RandomNumberGenerator)
|
|||||||
if rng.randf() < 0.15 + difficulty * 0.25:
|
if rng.randf() < 0.15 + difficulty * 0.25:
|
||||||
var generator := preload("res://scripts/items/item_generator.gd")
|
var generator := preload("res://scripts/items/item_generator.gd")
|
||||||
var definitions: Array = generator.DEFINITIONS.filter(func(d): return d.category != ItemDefinition.Category.WEAPON)
|
var definitions: Array = generator.DEFINITIONS.filter(func(d): return d.category != ItemDefinition.Category.WEAPON)
|
||||||
var level := mini(1000, GameState.system_level + roundi(difficulty * 6.0) + stage)
|
var item: ItemInstance = REWARDS.generate_module(rng, active_profile, GameState.system_level, definitions[rng.randi_range(0, definitions.size()-1)], 1 if difficulty > 0.5 or stage >= 2 else 0)
|
||||||
var item: ItemInstance = generator.generate_loot(level, 1 if difficulty > 0.5 or stage >= 2 else 0, definitions[rng.randi_range(0, definitions.size() - 1)])
|
|
||||||
cargo_modules.append(item)
|
cargo_modules.append(item)
|
||||||
result.module = item
|
result.module = item
|
||||||
if str(poi.kind) == "ruins":
|
if str(poi.kind) == "ruins":
|
||||||
@ -97,14 +110,20 @@ func finish_visit(captain: Resource, success: bool) -> Dictionary:
|
|||||||
if not active:
|
if not active:
|
||||||
return last_result
|
return last_result
|
||||||
success = success and lander_hp > 0
|
success = success and lander_hp > 0
|
||||||
var result := {"success": success, "materials": cargo.duplicate(), "modules": cargo_modules.size(), "hp": lander_hp}
|
var result := {"success": success, "materials": cargo.duplicate(), "modules": cargo_modules.size(), "hp": lander_hp, "credits":cargo_credits, "parts":cargo_parts, "missiles":cargo_missiles}
|
||||||
if success:
|
if success:
|
||||||
|
captain.credits += cargo_credits
|
||||||
|
captain.parts += cargo_parts
|
||||||
|
delivered_missiles += cargo_missiles
|
||||||
for key in cargo:
|
for key in cargo:
|
||||||
captain.minerals[key] = int(captain.minerals.get(key, 0)) + int(cargo[key])
|
captain.minerals[key] = int(captain.minerals.get(key, 0)) + int(cargo[key])
|
||||||
delivered_modules.append_array(cargo_modules)
|
delivered_modules.append_array(cargo_modules)
|
||||||
for key in cargo_records:
|
for key in cargo_records:
|
||||||
research_records[key] = int(research_records.get(key, 0)) + int(cargo_records[key])
|
research_records[key] = int(research_records.get(key, 0)) + int(cargo_records[key])
|
||||||
active = false
|
active = false
|
||||||
|
cargo_credits = 0
|
||||||
|
cargo_parts = 0
|
||||||
|
cargo_missiles = 0
|
||||||
cargo.clear()
|
cargo.clear()
|
||||||
cargo_modules.clear()
|
cargo_modules.clear()
|
||||||
cargo_records.clear()
|
cargo_records.clear()
|
||||||
@ -115,6 +134,11 @@ func finish_visit(captain: Resource, success: bool) -> Dictionary:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
func deliver_modules(inventory: Node) -> int:
|
func deliver_modules(inventory: Node) -> int:
|
||||||
|
if delivered_missiles > 0:
|
||||||
|
inventory.missile_ammunition += delivered_missiles
|
||||||
|
delivered_missiles = 0
|
||||||
|
inventory.ammunition_changed.emit()
|
||||||
|
inventory.inventory_changed.emit()
|
||||||
var delivered := 0
|
var delivered := 0
|
||||||
for index in range(delivered_modules.size() - 1, -1, -1):
|
for index in range(delivered_modules.size() - 1, -1, -1):
|
||||||
if inventory.add_item(delivered_modules[index]):
|
if inventory.add_item(delivered_modules[index]):
|
||||||
@ -123,35 +147,3 @@ func deliver_modules(inventory: Node) -> int:
|
|||||||
emit_changed()
|
emit_changed()
|
||||||
return delivered
|
return delivered
|
||||||
|
|
||||||
const ENGINEER_PRICE := 100
|
|
||||||
const ENGINEER_LEVEL_RATIO := 0.75
|
|
||||||
|
|
||||||
static func roll_engineer_bonus(rng: RandomNumberGenerator) -> int:
|
|
||||||
var total := 0.0
|
|
||||||
for bonus in 21:
|
|
||||||
total += pow(ENGINEER_LEVEL_RATIO,bonus)
|
|
||||||
var roll := rng.randf()*total
|
|
||||||
for bonus in 21:
|
|
||||||
roll -= pow(ENGINEER_LEVEL_RATIO,bonus)
|
|
||||||
if roll < 0:
|
|
||||||
return bonus
|
|
||||||
return 20
|
|
||||||
|
|
||||||
func buy_engineer_offer(captain: Resource, event: Dictionary, rng: RandomNumberGenerator) -> Dictionary:
|
|
||||||
if not active or lander_hp <= 0 or str(event.get("kind","")) != "engineer" or bool(event.get("complete",false)):
|
|
||||||
return {"success":false,"reason":"Oferta jest już niedostępna."}
|
|
||||||
if captain.credits < ENGINEER_PRICE:
|
|
||||||
return {"success":false,"reason":"Potrzebujesz 100 kredytów."}
|
|
||||||
var generator := preload("res://scripts/items/item_generator.gd")
|
|
||||||
var categories := [ItemDefinition.Category.SHIELD,ItemDefinition.Category.ARMOR,ItemDefinition.Category.ENGINE,ItemDefinition.Category.REACTOR,ItemDefinition.Category.COMPUTER]
|
|
||||||
var definitions: Array = generator.DEFINITIONS.filter(func(definition): return definition.category in categories)
|
|
||||||
var level := mini(1000,GameState.system_level+roll_engineer_bonus(rng))
|
|
||||||
# Generate the exact offered level; captain luck must not bypass the +20 limit.
|
|
||||||
var item: ItemInstance = generator.generate_for_definition(definitions[rng.randi_range(0,definitions.size()-1)],level,0)
|
|
||||||
captain.credits -= ENGINEER_PRICE
|
|
||||||
cargo_modules.append(item)
|
|
||||||
event.complete = true
|
|
||||||
captain.emit_changed()
|
|
||||||
emit_changed()
|
|
||||||
return {"success":true,"item":item}
|
|
||||||
|
|
||||||
|
|||||||
48
scripts/planets/planet_rewards.gd
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Shared planet reward rules. Ammo and consumables never use material scaling.
|
||||||
|
const ITEMS := preload("res://scripts/items/item_generator.gd")
|
||||||
|
const MATERIAL_KEYS := ["parts","iron","titanium","aluminium","uranium","sulfur","silicon","copper","nickel","tungsten","mercury"]
|
||||||
|
|
||||||
|
static func difficulty(profile: Dictionary) -> float:
|
||||||
|
return clampf(float(profile.get("difficulty",0.0)),0.0,1.0)
|
||||||
|
|
||||||
|
static func material_multiplier(profile: Dictionary) -> float:
|
||||||
|
return 1.0+2.0*difficulty(profile)
|
||||||
|
|
||||||
|
static func module_bonus_range(profile: Dictionary) -> Vector2i:
|
||||||
|
var d := difficulty(profile)
|
||||||
|
return Vector2i(roundi(2.0*d),roundi(2.0+8.0*d))
|
||||||
|
|
||||||
|
static func roll_module_bonus(rng: RandomNumberGenerator, profile: Dictionary) -> int:
|
||||||
|
var bounds := module_bonus_range(profile)
|
||||||
|
var count := bounds.y-bounds.x+1
|
||||||
|
var total := 0.0
|
||||||
|
for index in count:
|
||||||
|
total += pow(0.65,index)
|
||||||
|
var roll := rng.randf()*total
|
||||||
|
for index in count:
|
||||||
|
roll -= pow(0.65,index)
|
||||||
|
if roll < 0.0:
|
||||||
|
return bounds.x+index
|
||||||
|
return bounds.y
|
||||||
|
|
||||||
|
static func generate_module(rng: RandomNumberGenerator, profile: Dictionary, level: int, definition: ItemDefinition = null, tier := 0) -> ItemInstance:
|
||||||
|
if definition == null:
|
||||||
|
var definitions: Array = ITEMS.DEFINITIONS.filter(func(d): return d.category in [ItemDefinition.Category.SHIELD,ItemDefinition.Category.ARMOR,ItemDefinition.Category.ENGINE,ItemDefinition.Category.REACTOR,ItemDefinition.Category.COMPUTER])
|
||||||
|
definition = definitions[rng.randi_range(0,definitions.size()-1)]
|
||||||
|
var bonus := roll_module_bonus(rng,profile)
|
||||||
|
# Generate the exact level: luck and POI stages must not exceed the biome range.
|
||||||
|
return ITEMS.generate_for_definition(definition,clampi(level+bonus,1,1000),tier)
|
||||||
|
|
||||||
|
static func scale_materials(reward: Dictionary, profile: Dictionary) -> Dictionary:
|
||||||
|
var scaled := reward.duplicate()
|
||||||
|
for key in scaled:
|
||||||
|
if key in MATERIAL_KEYS:
|
||||||
|
scaled[key] = maxi(1,roundi(int(scaled[key])*material_multiplier(profile))) if int(scaled[key]) > 0 else 0
|
||||||
|
return scaled
|
||||||
|
|
||||||
|
static func missile_price_ratio(profile: Dictionary) -> float:
|
||||||
|
return lerpf(0.70,0.50,difficulty(profile))
|
||||||
|
|
||||||
|
static func missile_barter_cost(profile: Dictionary) -> int:
|
||||||
|
return maxi(1,roundi(6.0*lerpf(1.0,0.65,difficulty(profile))))
|
||||||
74
scripts/planets/surface_details.gd
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Decorative only: its own random stream keeps POI, loot and events unchanged.
|
||||||
|
const ATLAS_PATH := "res://assets/planets/nature/nature_atlas.png"
|
||||||
|
const TREE_CELLS := [[0, 1, 2], [1, 2], [1, 2], [0, 2, 3], [3], [0, 3]]
|
||||||
|
const DETAIL_CELLS := [[7, 8, 9, 10, 13, 14], [7, 8, 9, 10, 13, 14], [6, 8, 9, 14, 15], [4, 5, 7, 11], [4, 5, 7, 11], [4, 5, 6, 7, 11]]
|
||||||
|
|
||||||
|
static func generate(data: Dictionary) -> void:
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = int(data.seed) ^ 0x4e415455
|
||||||
|
var biome := int(data.landscape.index)
|
||||||
|
var trees: Array = TREE_CELLS[biome]
|
||||||
|
for tree in data.trees:
|
||||||
|
# Retain some of the original canopy alongside the new species.
|
||||||
|
tree.variant = -1 if rng.randf() < 0.35 else int(trees[rng.randi_range(0, trees.size()-1)])
|
||||||
|
for rock in data.rocks:
|
||||||
|
rock.variant = rng.randi_range(4, 6)
|
||||||
|
data.details = []
|
||||||
|
var kinds: Array = DETAIL_CELLS[biome]
|
||||||
|
# Local clusters read as vegetation patches, rather than a uniform scatter.
|
||||||
|
for index in 150:
|
||||||
|
var anchor := Vector2(rng.randf_range(-1760, 1760), rng.randf_range(-1760, 1760))
|
||||||
|
var cell := int(kinds[rng.randi_range(0, kinds.size()-1)])
|
||||||
|
for member in rng.randi_range(2, 5):
|
||||||
|
var point := anchor + Vector2.from_angle(rng.randf()*TAU)*rng.randf_range(0, 65)
|
||||||
|
var width := rng.randf_range(16, 32)
|
||||||
|
if cell == 4 or cell == 5 or cell == 6:
|
||||||
|
width = rng.randf_range(24, 43)
|
||||||
|
elif cell == 8 or cell == 9 or cell == 15:
|
||||||
|
width = rng.randf_range(26, 44)
|
||||||
|
if not _clear_site(data, point, width*0.5):
|
||||||
|
continue
|
||||||
|
if cell == 15 and _height(data, point) > 0.39:
|
||||||
|
continue
|
||||||
|
data.details.append({"position":point,"size":width,"rotation":rng.randf_range(-PI, PI),"variant":cell,"shade":rng.randf_range(0.88, 1.08)})
|
||||||
|
# At most one fallen tree, only on 8% of forested landing maps.
|
||||||
|
if biome < 4 and rng.randf() < 0.08:
|
||||||
|
for attempt in 80:
|
||||||
|
if data.trees.is_empty():
|
||||||
|
break
|
||||||
|
var tree: Dictionary = data.trees[rng.randi_range(0, data.trees.size()-1)]
|
||||||
|
var point: Vector2 = tree.position + Vector2.from_angle(rng.randf()*TAU)*rng.randf_range(70, 110)
|
||||||
|
if _clear_site(data, point, 65):
|
||||||
|
data.details.append({"position":point,"size":92.0,"rotation":rng.randf_range(-PI, PI),"variant":12,"shade":1.0})
|
||||||
|
break
|
||||||
|
|
||||||
|
static func _height(data: Dictionary, point: Vector2) -> float:
|
||||||
|
var x := clampi(int((point.x+1920)/40), 0, 95)
|
||||||
|
var y := clampi(int((point.y+1920)/40), 0, 95)
|
||||||
|
return float(data.heights[y*96+x])
|
||||||
|
|
||||||
|
static func _clear_site(data: Dictionary, point: Vector2, radius: float) -> bool:
|
||||||
|
if point.length() < 210+radius or absf(point.x)+radius > 1860 or absf(point.y)+radius > 1860:
|
||||||
|
return false
|
||||||
|
# Sample the full footprint, keeping props away from water and steep peaks.
|
||||||
|
for offset in [Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN, Vector2(-0.7,-0.7), Vector2(0.7,-0.7), Vector2(-0.7,0.7), Vector2(0.7,0.7)]:
|
||||||
|
var h := _height(data, point+offset*(radius+25))
|
||||||
|
if h < 0.315 or h > 0.68:
|
||||||
|
return false
|
||||||
|
for poi in data.pois:
|
||||||
|
if point.distance_to(poi.position) < 135+radius:
|
||||||
|
return false
|
||||||
|
for event in data.events:
|
||||||
|
if point.distance_to(event.position) < 115+radius:
|
||||||
|
return false
|
||||||
|
for structure in data.structures:
|
||||||
|
if point.distance_to(structure.position) < float(structure.size)*0.7+radius:
|
||||||
|
return false
|
||||||
|
for landmark in data.get("landmarks", []):
|
||||||
|
if point.distance_to(landmark.position) < float(landmark.size)*0.58+radius+15:
|
||||||
|
return false
|
||||||
|
for mountain in data.mountains:
|
||||||
|
if point.distance_to(mountain.position) < float(mountain.size)*0.48+radius:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
1
scripts/planets/surface_details.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://fjo5rs5vmjd4
|
||||||
241
scripts/planets/surface_events.gd
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
extends RefCounted
|
||||||
|
const REWARDS := preload("res://scripts/planets/planet_rewards.gd")
|
||||||
|
const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
|
||||||
|
const UI := preload("res://scripts/planets/expedition_ui.gd")
|
||||||
|
const KINDS := ["trader", "barter", "collector", "blind_cache", "scan_cache", "safe_bet", "prototype", "rescue", "evacuation", "service"]
|
||||||
|
const TITLES := {
|
||||||
|
"trader":"Wędrowny handlarz", "barter":"Wymiana towarów", "collector":"Kolekcjoner minerałów",
|
||||||
|
"blind_cache":"Kontener w ciemno", "scan_cache":"Płatny skan skrytki", "safe_bet":"Pewna wypłata albo ryzyko",
|
||||||
|
"prototype":"Test prototypu", "rescue":"Akcja ratunkowa", "evacuation":"Ewakuacja etapami", "service":"Mobilny serwis"
|
||||||
|
}
|
||||||
|
const STORIES := {
|
||||||
|
"trader":"Przy zakurzonym transporterze miga przenośny nadajnik. Właściciel przedstawia się jako niezależny dostawca, choć oznaczenia na skrzyniach należą do kilku różnych firm. Twierdzi, że kończy interesy na tej planecie i woli sprzedać zapasy taniej, niż zabierać je z powrotem na orbitę.",
|
||||||
|
"barter":"Obok niewielkiej instalacji wydobywczej stoi łazik załadowany po same osie. Jego operator od tygodni wydobywa ten sam minerał, ale zabrakło mu materiałów do naprawy urządzeń. Proponuje wymianę części swojego zapasu na surowce z twojej ładowni. Przesyła dokładny wykaz towarów.",
|
||||||
|
"collector":"Sygnał prowadzi do samotnego badacza pochylonego nad analizatorem próbek. Na widok twojego spisu ładunku nagle przerywa rozmowę. Jednego z przewożonych przez ciebie surowców potrzebuje do ukończenia badań. Ma środki z kończącego się grantu i jest gotów zapłacić więcej niż zwykły skup.",
|
||||||
|
"blind_cache":"Złomiarz wyciągnął z wraku zamknięty kontener. Plomba wygląda na nienaruszoną, ale tabliczka ładunkowa została wypalona podczas wejścia w atmosferę. Sprzedawca nie zamierza tracić czasu na otwieranie zabezpieczeń. Podaje cenę za całość i zapewnia, że poprzedni kupujący trafił świetnie. O wcześniejszych klientach mówi już mniej chętnie.",
|
||||||
|
"scan_cache":"W ruinach starego magazynu ktoś odnalazł pancerną skrytkę. Stojąca obok operatorka ma skaner zdolny zajrzeć przez jej osłonę, lecz urządzenie zużywa drogie ogniwa. Za niewielką opłatą sprawdzi, co może znajdować się w środku. Otwarcie zamka będzie kosztować osobno. Uprzedza, że skorodowana obudowa zakłóca odczyt.",
|
||||||
|
"safe_bet":"Zarządca opuszczanego punktu przeładunkowego sprzedaje ostatnie zapasy. Za podaną kwotę możesz zabrać przygotowany zestaw części albo jeden z zaplombowanych pakietów odzyskanych z transportu. Otwarty zestaw jest skromny, ale sprawdzony. W zamkniętych pakietach zdarzały się wartościowe podzespoły — oraz materiały, których nikt nie chciał kupić.",
|
||||||
|
"prototype":"Inżynierka rozłożyła stanowisko pomiarowe daleko od zabudowań. Potrzebuje lądownika do krótkiego testu nowego układu zasilania. Zapewnia podstawową zapłatę niezależnie od wyniku i dodatkową premię za poprawny przebieg próby. Gdy pytasz o ryzyko, bez słowa przesyła zapis poprzedniego testu. Widać na nim nagły skok napięcia.",
|
||||||
|
"rescue":"Słaby sygnał alarmowy dobiega z przewróconego łazika. Jego kierowca żyje, lecz nie może wydostać się z kabiny. Do uniesienia wraku potrzebny jest prowizoryczny podnośnik zbudowany z twoich części. Czujniki ostrzegają, że uszkodzona bateria pojazdu jest niestabilna. Uwięziony obiecuje zapłatę, jeśli pomożesz mu wrócić do obozu.",
|
||||||
|
"evacuation":"Nad małym obozem zbiera się burza, a jego transport ewakuacyjny stoi z rozebranym silnikiem. Pierwsza grupa ludzi czeka już przy awaryjnym schronieniu. Dalej pozostali pracownicy zabezpieczający sprzęt i ranni, których trudniej przenieść. Koordynator może zapłacić za każdy ukończony etap pomocy, ale z każdą minutą warunki będą gorsze.",
|
||||||
|
"service":"Na skraju polany stoi warsztat na szerokich gąsienicach. Mechanik odbiera sygnał z twojego lądownika i po chwili przesyła listę uszkodzeń. Ma części wystarczające na jedną szybką naprawę. Podaje cenę oraz dokładny zakres prac, po czym uruchamia spawarkę i czeka na odpowiedź."
|
||||||
|
}
|
||||||
|
|
||||||
|
static func prepare(event: Dictionary, captain: Resource, progress: Resource) -> void:
|
||||||
|
if event.get("prepared", false) or not KINDS.has(event.get("kind", "")):
|
||||||
|
return
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = int(event.get("seed", progress.visit_seed ^ 0x45564e54))
|
||||||
|
event.stage = 0
|
||||||
|
event.scanned = false
|
||||||
|
event.level = maxi(1, GameState.system_level)
|
||||||
|
event.reward_profile = progress.active_profile.duplicate(true)
|
||||||
|
var scale := int(event.level)
|
||||||
|
var count := rng.randi_range(3, 6)
|
||||||
|
var missile_price: int = captain.discounted_price(ShipInventory.MISSILE_BUY_BASE_COST*scale)
|
||||||
|
event.missile_count = count
|
||||||
|
event.missile_normal_price = missile_price*count
|
||||||
|
event.missile_price = maxi(1, floori(missile_price*count*REWARDS.missile_price_ratio(event.reward_profile)))
|
||||||
|
event.module = _module(rng, scale, event.reward_profile)
|
||||||
|
event.prize = _mystery(rng, scale, event.reward_profile)
|
||||||
|
event.rolls = [rng.randf(), rng.randf(), rng.randf()]
|
||||||
|
event.damage_rolls = [rng.randi_range(0, 8), rng.randi_range(0, 8), rng.randi_range(0, 8)]
|
||||||
|
event.collect_key = PROFILES.MATERIALS[rng.randi_range(0, PROFILES.MATERIALS.size()-1)]
|
||||||
|
event.prepared = true
|
||||||
|
|
||||||
|
static func _module(rng: RandomNumberGenerator, level: int, profile: Dictionary) -> ItemInstance:
|
||||||
|
return REWARDS.generate_module(rng,profile,level)
|
||||||
|
|
||||||
|
static func roll_module_bonus(rng: RandomNumberGenerator, profile: Dictionary) -> int:
|
||||||
|
return REWARDS.roll_module_bonus(rng,profile)
|
||||||
|
|
||||||
|
static func _mystery(rng: RandomNumberGenerator, level: int, profile: Dictionary) -> Dictionary:
|
||||||
|
var roll := rng.randf()
|
||||||
|
if roll < 0.5:
|
||||||
|
return REWARDS.scale_materials({"parts":rng.randi_range(2,8)},profile)
|
||||||
|
if roll < 0.85:
|
||||||
|
return REWARDS.scale_materials({str(PROFILES.MATERIALS[rng.randi_range(0,PROFILES.MATERIALS.size()-1)]):rng.randi_range(6,15)},profile)
|
||||||
|
if roll < 0.95:
|
||||||
|
return {"missile":rng.randi_range(2,4)}
|
||||||
|
return {"module":_module(rng,level,profile)}
|
||||||
|
|
||||||
|
static func _option(id: String, label: String, cost: Dictionary = {}, reward: Dictionary = {}) -> Dictionary:
|
||||||
|
return {"id":id, "label":label, "cost":cost, "reward":reward}
|
||||||
|
|
||||||
|
static func chance(event: Dictionary, captain: Resource, progress: Resource) -> float:
|
||||||
|
var stage := int(event.get("stage",0)) if event.kind == "evacuation" else 0
|
||||||
|
var value := PROFILES.success_chance(progress.active_profile,captain,stage)
|
||||||
|
return clampf(value - (0.10 if event.kind == "prototype" else 0.0),0.0,1.0)
|
||||||
|
|
||||||
|
static func damage_range(event: Dictionary, progress: Resource) -> Vector2i:
|
||||||
|
var stage := int(event.get("stage",0)) if event.kind == "evacuation" else 0
|
||||||
|
var lower := (12 if event.kind == "prototype" else 8) + stage*4 + roundi(float(progress.active_profile.difficulty)*10)
|
||||||
|
return Vector2i(lower,lower+8)
|
||||||
|
|
||||||
|
static func options(event: Dictionary, captain: Resource, progress: Resource) -> Array[Dictionary]:
|
||||||
|
prepare(event,captain,progress)
|
||||||
|
var found: Array[Dictionary] = []
|
||||||
|
if not event.get("prepared",false) or event.get("complete",false):
|
||||||
|
return found
|
||||||
|
var scale := int(event.level)
|
||||||
|
match str(event.kind):
|
||||||
|
"trader":
|
||||||
|
found.append(_option("missiles","Kup %d rakiet · sklep: %d kr." % [event.missile_count,event.missile_normal_price],{"credits":event.missile_price},{"missile":event.missile_count}))
|
||||||
|
found.append(_option("materials","Kup zestaw surowców",{"credits":captain.discounted_price(70*scale)},{"copper":8,"silicon":8}))
|
||||||
|
var item: ItemInstance = event.module
|
||||||
|
var offer := _option("module","Kup: %s · poz. %d" % [item.definition.display_name,item.item_level],{"credits":captain.discounted_price(140*scale)},{"module":item})
|
||||||
|
offer.tooltip = item.get_tooltip_text()
|
||||||
|
found.append(offer)
|
||||||
|
"barter":
|
||||||
|
found.append(_option("minerals","Wymień surowce",{"iron":12,"copper":4},{"nickel":8}))
|
||||||
|
var ammo_cost := REWARDS.missile_barter_cost(event.reward_profile)
|
||||||
|
found.append(_option("missiles","Wymień surowce na 3 rakiety",{"iron":ammo_cost,"sulfur":ammo_cost,"uranium":ammo_cost},{"missile":3}))
|
||||||
|
"collector":
|
||||||
|
found.append(_option("sell","Sprzedaj próbki",{str(event.collect_key):6},{"credits":120*scale}))
|
||||||
|
"blind_cache":
|
||||||
|
found.append(_option("open","Kup kontener · zawartość nieznana",{"credits":captain.discounted_price(100*scale)}))
|
||||||
|
"scan_cache":
|
||||||
|
if not event.scanned:
|
||||||
|
found.append(_option("scan","Zleć skan skrytki",{"credits":captain.discounted_price(15*scale)}))
|
||||||
|
else:
|
||||||
|
found.append(_option("open","Otwórz przeskanowaną skrytkę",{"credits":captain.discounted_price(75*scale)}))
|
||||||
|
"safe_bet":
|
||||||
|
var price: int = captain.discounted_price(80*scale)
|
||||||
|
found.append(_option("safe","Weź gwarantowany zestaw",{"credits":price},{"parts":6}))
|
||||||
|
found.append(_option("gamble","Wybierz tajemniczy pakiet",{"credits":price}))
|
||||||
|
"prototype":
|
||||||
|
found.append(_option("test","Uruchom prototyp · podstawa: %d kr." % (35*scale),{},{"credits":90*scale,"module":event.module}))
|
||||||
|
"rescue":
|
||||||
|
found.append(_option("rescue","Zbuduj podnośnik i pomóż kierowcy",{"parts":6},{"credits":140*scale,"parts":8}))
|
||||||
|
"evacuation":
|
||||||
|
var stage := int(event.stage)
|
||||||
|
found.append(_option("help","Etap %d/3 · %s" % [stage+1,["Zabezpiecz schronienie","Ewakuuj pracowników","Przenieś rannych"][stage]],{"parts":2+stage},{"credits":[40,70,110][stage]*scale,"parts":2+stage*2}))
|
||||||
|
if stage > 0:
|
||||||
|
found.append(_option("stop","Zakończ pomoc i zachowaj dotychczasową zapłatę"))
|
||||||
|
"service":
|
||||||
|
var amount := mini(25,ceili(100.0-progress.lander_hp))
|
||||||
|
if amount > 0:
|
||||||
|
found.append(_option("repair","Napraw lądownik: +%d HP" % amount,{"credits":captain.discounted_price(amount*4*scale)}))
|
||||||
|
for option in found:
|
||||||
|
option.reward = REWARDS.scale_materials(option.reward,event.reward_profile)
|
||||||
|
return found
|
||||||
|
|
||||||
|
static func stock(captain: Resource, progress: Resource, key: String) -> int:
|
||||||
|
match key:
|
||||||
|
"credits": return int(captain.credits)+int(progress.cargo_credits)
|
||||||
|
"parts": return floori(captain.parts)+int(progress.cargo_parts)
|
||||||
|
return int(captain.minerals.get(key,0))+int(progress.cargo.get(key,0))
|
||||||
|
|
||||||
|
static func can_pay(captain: Resource, progress: Resource, cost: Dictionary) -> bool:
|
||||||
|
for key in cost:
|
||||||
|
if stock(captain,progress,key) < int(cost[key]):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
static func _pay(captain: Resource, progress: Resource, cost: Dictionary) -> void:
|
||||||
|
for key in cost:
|
||||||
|
var amount := int(cost[key])
|
||||||
|
if key == "credits":
|
||||||
|
var from_cargo := mini(amount,int(progress.cargo_credits))
|
||||||
|
progress.cargo_credits -= from_cargo
|
||||||
|
captain.credits -= amount-from_cargo
|
||||||
|
elif key == "parts":
|
||||||
|
var from_cargo := mini(amount,int(progress.cargo_parts))
|
||||||
|
progress.cargo_parts -= from_cargo
|
||||||
|
captain.parts -= amount-from_cargo
|
||||||
|
else:
|
||||||
|
var from_cargo := mini(amount,int(progress.cargo.get(key,0)))
|
||||||
|
progress.cargo[key] = int(progress.cargo.get(key,0))-from_cargo
|
||||||
|
if int(progress.cargo[key]) == 0:
|
||||||
|
progress.cargo.erase(key)
|
||||||
|
captain.minerals[key] = int(captain.minerals.get(key,0))-(amount-from_cargo)
|
||||||
|
|
||||||
|
static func _grant(progress: Resource, reward: Dictionary) -> void:
|
||||||
|
for key in reward:
|
||||||
|
match key:
|
||||||
|
"credits": progress.cargo_credits += int(reward[key])
|
||||||
|
"parts": progress.cargo_parts += int(reward[key])
|
||||||
|
"missile": progress.cargo_missiles += int(reward[key])
|
||||||
|
"module": progress.cargo_modules.append(reward[key])
|
||||||
|
_: progress.add_material(key,int(reward[key]))
|
||||||
|
|
||||||
|
static func reward_text(reward: Dictionary) -> String:
|
||||||
|
var materials := reward.duplicate()
|
||||||
|
materials.erase("module")
|
||||||
|
materials.erase("credits")
|
||||||
|
var text := UI.material_text(materials)
|
||||||
|
if reward.has("credits"):
|
||||||
|
text += (" · " if not text.is_empty() else "") + "%d kr." % int(reward.credits)
|
||||||
|
if reward.has("module"):
|
||||||
|
var item: ItemInstance = reward.module
|
||||||
|
text += ("\n" if not text.is_empty() else "") + "%s · poziom %d" % [item.definition.display_name,item.item_level]
|
||||||
|
return text
|
||||||
|
|
||||||
|
static func scan_text(event: Dictionary) -> String:
|
||||||
|
if not event.get("scanned",false):
|
||||||
|
return "Skan ujawni rodzaj zawartości, ale nie jej ilość ani stan. Otwarcie wymaga osobnej opłaty."
|
||||||
|
var prize: Dictionary = event.prize
|
||||||
|
if prize.has("module"):
|
||||||
|
return "ODCZYT: podzespół statku. Poziomu i parametrów nie udało się ustalić."
|
||||||
|
if prize.has("missile"):
|
||||||
|
return "ODCZYT: amunicja rakietowa. Liczba sztuk nieznana."
|
||||||
|
if prize.has("parts"):
|
||||||
|
return "ODCZYT: części zamienne. Ilość nieznana."
|
||||||
|
return "ODCZYT: próbki mineralne. Dokładny skład i ilość nieznane."
|
||||||
|
|
||||||
|
static func resolve(event: Dictionary, action: String, captain: Resource, progress: Resource) -> Dictionary:
|
||||||
|
if not progress.active or progress.lander_hp <= 0 or event.get("complete",false):
|
||||||
|
return {"success":false,"reason":"To spotkanie jest już niedostępne."}
|
||||||
|
var choices := options(event,captain,progress)
|
||||||
|
var selected: Dictionary = {}
|
||||||
|
for option in choices:
|
||||||
|
if str(option.id) == action:
|
||||||
|
selected = option
|
||||||
|
break
|
||||||
|
if selected.is_empty():
|
||||||
|
return {"success":false,"reason":"Ta decyzja jest niedostępna."}
|
||||||
|
if not can_pay(captain,progress,selected.cost):
|
||||||
|
return {"success":false,"reason":"Brakuje zasobów. Koszt: "+reward_text(selected.cost)}
|
||||||
|
if event.kind in ["prototype","rescue","evacuation"] and action != "stop" and chance(event,captain,progress) <= 0.0:
|
||||||
|
return {"success":false,"blocked":true,"reason":"Ten etap ma 0% szans. Obniż zagrożenie odpowiednim wyposażeniem."}
|
||||||
|
_pay(captain,progress,selected.cost)
|
||||||
|
var reward: Dictionary = selected.reward.duplicate()
|
||||||
|
var success := true
|
||||||
|
var damage := 0
|
||||||
|
var message := ""
|
||||||
|
if event.kind == "scan_cache" and action == "scan":
|
||||||
|
event.scanned = true
|
||||||
|
message = "Skan zakończony. Możesz opłacić otwarcie lub wrócić później."
|
||||||
|
elif event.kind in ["blind_cache","scan_cache"] or (event.kind == "safe_bet" and action == "gamble"):
|
||||||
|
reward = event.prize.duplicate()
|
||||||
|
event.complete = true
|
||||||
|
elif event.kind in ["prototype","rescue","evacuation"] and action != "stop":
|
||||||
|
var stage := int(event.stage)
|
||||||
|
success = float(event.rolls[stage]) < chance(event,captain,progress)
|
||||||
|
if not success:
|
||||||
|
var limits := damage_range(event,progress)
|
||||||
|
damage = limits.x+int(event.damage_rolls[stage])
|
||||||
|
progress.damage(damage)
|
||||||
|
reward.clear()
|
||||||
|
message = "NIEPOWODZENIE · utrata %d HP. Wcześniejszy łup pozostaje w ładowni." % damage
|
||||||
|
if event.kind == "prototype":
|
||||||
|
reward.credits = int(reward.get("credits",0))+35*int(event.level)
|
||||||
|
if success and event.kind == "evacuation":
|
||||||
|
event.stage = stage+1
|
||||||
|
event.complete = int(event.stage) >= 3
|
||||||
|
else:
|
||||||
|
event.complete = true
|
||||||
|
else:
|
||||||
|
event.complete = true
|
||||||
|
if action == "repair":
|
||||||
|
var restored := minf(25,100.0-progress.lander_hp)
|
||||||
|
progress.lander_hp = minf(100,progress.lander_hp+restored)
|
||||||
|
message = "Naprawiono lądownik: +%d HP." % ceili(restored)
|
||||||
|
elif action == "stop":
|
||||||
|
message = "Zakończono pomoc. Dotychczasowa zapłata pozostaje w ładowni wyprawy."
|
||||||
|
_grant(progress,reward)
|
||||||
|
if not reward.is_empty():
|
||||||
|
message += ("\n" if not message.is_empty() else "") + "Otrzymano: "+reward_text(reward)
|
||||||
|
captain.emit_changed()
|
||||||
|
progress.emit_changed()
|
||||||
|
return {"success":success,"message":message,"damage":damage,"reward":reward}
|
||||||
@ -40,7 +40,7 @@ static func generate(seed_value: int, profile: Dictionary) -> Dictionary:
|
|||||||
if h > 0.66 and rng.randf() < 0.65:
|
if h > 0.66 and rng.randf() < 0.65:
|
||||||
mountains.append({"position": point, "size": rng.randf_range(110, 210), "rotation": rng.randf_range(-PI, PI)})
|
mountains.append({"position": point, "size": rng.randf_range(110, 210), "rotation": rng.randf_range(-PI, PI)})
|
||||||
elif h > 0.28 and h < 0.65 and humidity.get_noise_2d(point.x, point.y) > -0.12 and rng.randf() < float(landscape.density):
|
elif h > 0.28 and h < 0.65 and humidity.get_noise_2d(point.x, point.y) > -0.12 and rng.randf() < float(landscape.density):
|
||||||
trees.append({"position": point, "size": rng.randf_range(65, 115), "rotation": rng.randf_range(-PI, PI), "shade": rng.randf_range(0.75, 1.1)})
|
trees.append({"position": point, "size": rng.randf_range(52, 92), "rotation": rng.randf_range(-PI, PI), "shade": rng.randf_range(0.75, 1.1)})
|
||||||
for index in 180:
|
for index in 180:
|
||||||
var point := Vector2(rng.randf_range(-1750,1750),rng.randf_range(-1750,1750))
|
var point := Vector2(rng.randf_range(-1750,1750),rng.randf_range(-1750,1750))
|
||||||
var gx := clampi(int((point.x+1920)/CELL),0,GRID-1)
|
var gx := clampi(int((point.x+1920)/CELL),0,GRID-1)
|
||||||
@ -78,6 +78,8 @@ static func generate(seed_value: int, profile: Dictionary) -> Dictionary:
|
|||||||
rocks = rocks.filter(func(prop): return prop.position.distance_to(poi.position) > 65)
|
rocks = rocks.filter(func(prop): return prop.position.distance_to(poi.position) > 65)
|
||||||
var data := {"seed":seed_value,"heights":heights,"height_texture":LANDSCAPE.height_texture(heights),"landscape":landscape,"rocks":rocks,"trees":trees,"mountains":mountains,"pois":pois,"profile":profile.duplicate(true)}
|
var data := {"seed":seed_value,"heights":heights,"height_texture":LANDSCAPE.height_texture(heights),"landscape":landscape,"rocks":rocks,"trees":trees,"mountains":mountains,"pois":pois,"profile":profile.duplicate(true)}
|
||||||
preload("res://scripts/planets/surface_scenery.gd").generate(data,rng)
|
preload("res://scripts/planets/surface_scenery.gd").generate(data,rng)
|
||||||
|
preload("res://scripts/planets/surface_landmarks.gd").generate(data)
|
||||||
|
preload("res://scripts/planets/surface_details.gd").generate(data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
static func terrain_texture(data: Dictionary) -> ImageTexture:
|
static func terrain_texture(data: Dictionary) -> ImageTexture:
|
||||||
|
|||||||
46
scripts/planets/surface_landmarks.gd
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Rare visual landmarks use a random stream independent of expedition rewards.
|
||||||
|
const SPECS := [
|
||||||
|
{"kind":"fissure", "chance":0.55, "count":3, "min_size":90.0, "max_size":155.0},
|
||||||
|
{"kind":"meteor_crater", "chance":0.14, "count":1, "min_size":160.0, "max_size":220.0},
|
||||||
|
{"kind":"abandoned_hamlet", "chance":0.06, "count":1, "min_size":155.0, "max_size":185.0},
|
||||||
|
{"kind":"weathered_stones", "chance":0.12, "count":1, "min_size":70.0, "max_size":100.0},
|
||||||
|
{"kind":"dry_hollow", "chance":0.30, "count":1, "min_size":95.0, "max_size":140.0}
|
||||||
|
]
|
||||||
|
|
||||||
|
static func generate(data: Dictionary) -> void:
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = int(data.seed) ^ 0x4c414e44
|
||||||
|
data.landmarks = []
|
||||||
|
for spec in SPECS:
|
||||||
|
var chance := float(spec.chance)
|
||||||
|
if spec.kind == "dry_hollow" and int(data.landscape.index) < 3:
|
||||||
|
chance *= 0.4
|
||||||
|
if rng.randf() >= chance:
|
||||||
|
continue
|
||||||
|
for index in rng.randi_range(1, int(spec.count)):
|
||||||
|
var width := rng.randf_range(float(spec.min_size), float(spec.max_size))
|
||||||
|
var position := _find_site(data, rng, width*0.55)
|
||||||
|
if position == Vector2.INF:
|
||||||
|
continue
|
||||||
|
data.landmarks.append({"kind":spec.kind, "position":position, "size":width, "rotation":rng.randf_range(-PI, PI)})
|
||||||
|
# A crater rim and ruined roofs should remain visible through the vegetation.
|
||||||
|
for landmark in data.landmarks:
|
||||||
|
data.trees = data.trees.filter(func(prop): return prop.position.distance_to(landmark.position) > float(landmark.size)*0.48+float(prop.size)*0.4)
|
||||||
|
data.rocks = data.rocks.filter(func(prop): return prop.position.distance_to(landmark.position) > float(landmark.size)*0.5+float(prop.size)*0.5)
|
||||||
|
|
||||||
|
static func _find_site(data: Dictionary, rng: RandomNumberGenerator, radius: float) -> Vector2:
|
||||||
|
for attempt in 120:
|
||||||
|
var point := Vector2(rng.randf_range(-1650, 1650), rng.randf_range(-1650, 1650))
|
||||||
|
if not preload("res://scripts/planets/surface_details.gd")._clear_site(data, point, radius):
|
||||||
|
continue
|
||||||
|
# Check interior cells as well as the perimeter so small ponds stay clear.
|
||||||
|
var good := true
|
||||||
|
for y in range(-ceili(radius), ceili(radius)+1, 30):
|
||||||
|
for x in range(-ceili(radius), ceili(radius)+1, 30):
|
||||||
|
var h := preload("res://scripts/planets/surface_details.gd")._height(data, point+Vector2(x,y))
|
||||||
|
if h < 0.33 or h > 0.66:
|
||||||
|
good = false
|
||||||
|
if good:
|
||||||
|
return point
|
||||||
|
return Vector2.INF
|
||||||
1
scripts/planets/surface_landmarks.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://dd8kkwh3bvjto
|
||||||
@ -1,4 +1,5 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
const BUILDING_SCALE := 0.65
|
||||||
const KINDS := ["ruined_house","warehouse","radar","airstrip","fuel_depot","solar_farm","scrapyard"]
|
const KINDS := ["ruined_house","warehouse","radar","airstrip","fuel_depot","solar_farm","scrapyard"]
|
||||||
const WIDTHS := {"ruined_house":135.0,"warehouse":180.0,"radar":130.0,"airstrip":290.0,"fuel_depot":150.0,"solar_farm":180.0,"scrapyard":160.0,"engineer_camp":130.0}
|
const WIDTHS := {"ruined_house":135.0,"warehouse":180.0,"radar":130.0,"airstrip":290.0,"fuel_depot":150.0,"solar_farm":180.0,"scrapyard":160.0,"engineer_camp":130.0}
|
||||||
|
|
||||||
@ -7,15 +8,18 @@ static func generate(data: Dictionary, rng: RandomNumberGenerator) -> void:
|
|||||||
data.events = []
|
data.events = []
|
||||||
for index in rng.randi_range(2,5):
|
for index in rng.randi_range(2,5):
|
||||||
var kind: String = KINDS[rng.randi_range(0,KINDS.size()-1)]
|
var kind: String = KINDS[rng.randi_range(0,KINDS.size()-1)]
|
||||||
var width: float = WIDTHS[kind]*rng.randf_range(0.85,1.15)
|
var width: float = WIDTHS[kind]*BUILDING_SCALE*rng.randf_range(0.85,1.15)
|
||||||
var position := find_site(data,rng,width*0.55)
|
var position := find_site(data,rng,width*0.55)
|
||||||
if position == Vector2.INF:
|
if position == Vector2.INF:
|
||||||
continue
|
continue
|
||||||
data.structures.append({"kind":kind,"position":position,"size":width,"rotation":rng.randf_range(-PI,PI)})
|
data.structures.append({"kind":kind,"position":position,"size":width,"rotation":rng.randf_range(-PI,PI)})
|
||||||
if rng.randf() < 0.4:
|
var event_rng := RandomNumberGenerator.new()
|
||||||
var position := find_site(data,rng,100)
|
event_rng.seed = int(data.seed) ^ 0x45564e54
|
||||||
|
if event_rng.randf() < 0.5:
|
||||||
|
var position := find_site(data,event_rng,60)
|
||||||
if position != Vector2.INF:
|
if position != Vector2.INF:
|
||||||
data.events.append({"kind":"engineer","position":position,"complete":false,"discovered":false})
|
var kinds: Array = preload("res://scripts/planets/surface_events.gd").KINDS
|
||||||
|
data.events.append({"kind":kinds[event_rng.randi_range(0,kinds.size()-1)],"position":position,"seed":event_rng.randi(),"complete":false,"discovered":false})
|
||||||
var clearings: Array = data.structures.duplicate()
|
var clearings: Array = data.structures.duplicate()
|
||||||
for clearing in clearings:
|
for clearing in clearings:
|
||||||
data.trees = data.trees.filter(func(prop): return prop.position.distance_to(clearing.position) > float(clearing.size)*0.6+float(prop.size)*0.3)
|
data.trees = data.trees.filter(func(prop): return prop.position.distance_to(clearing.position) > float(clearing.size)*0.6+float(prop.size)*0.3)
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
extends Control
|
extends Control
|
||||||
signal finished(success: bool)
|
signal finished(success: bool)
|
||||||
const UI := preload("res://scripts/planets/expedition_ui.gd")
|
const UI := preload("res://scripts/planets/expedition_ui.gd")
|
||||||
|
const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
|
||||||
|
const EVENTS := preload("res://scripts/planets/surface_events.gd")
|
||||||
const GENERATOR := preload("res://scripts/planets/surface_generator.gd")
|
const GENERATOR := preload("res://scripts/planets/surface_generator.gd")
|
||||||
var captain: Resource
|
var captain: Resource
|
||||||
var progress: Resource
|
var progress: Resource
|
||||||
@ -9,6 +11,7 @@ var lander: Sprite2D
|
|||||||
var camera: Camera2D
|
var camera: Camera2D
|
||||||
var _hp: ProgressBar
|
var _hp: ProgressBar
|
||||||
var _cargo: Label
|
var _cargo: Label
|
||||||
|
var _threat: Label
|
||||||
var _status: Label
|
var _status: Label
|
||||||
var _dialog: PanelContainer
|
var _dialog: PanelContainer
|
||||||
var _choices: VBoxContainer
|
var _choices: VBoxContainer
|
||||||
@ -94,6 +97,7 @@ func _ready() -> void:
|
|||||||
UI.label(column, "%s / TLEN %d%% / %d°C / FLORA %d" % [
|
UI.label(column, "%s / TLEN %d%% / %d°C / FLORA %d" % [
|
||||||
preload("res://scripts/planets/planet_profiles.gd").biome_name(progress.active_profile),
|
preload("res://scripts/planets/planet_profiles.gd").biome_name(progress.active_profile),
|
||||||
progress.active_profile.oxygen, progress.active_profile.temperature, progress.active_profile.flora], 13)
|
progress.active_profile.oxygen, progress.active_profile.temperature, progress.active_profile.flora], 13)
|
||||||
|
_threat = UI.label(column,"",13,Color("efca8a"))
|
||||||
_hp = ProgressBar.new()
|
_hp = ProgressBar.new()
|
||||||
_hp.custom_minimum_size = Vector2(0, 14)
|
_hp.custom_minimum_size = Vector2(0, 14)
|
||||||
_hp.show_percentage = false
|
_hp.show_percentage = false
|
||||||
@ -122,12 +126,18 @@ func _ready() -> void:
|
|||||||
minimap.world = world
|
minimap.world = world
|
||||||
minimap.lander = lander
|
minimap.lander = lander
|
||||||
map_frame.add_child(minimap)
|
map_frame.add_child(minimap)
|
||||||
_dialog = UI.panel(self, Vector2(460,0))
|
_dialog = UI.panel(self)
|
||||||
_dialog.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
|
_dialog.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
|
||||||
_dialog.position = Vector2(get_viewport_rect().size.x-490,get_viewport_rect().size.y*0.5-100)
|
var scroll := ScrollContainer.new()
|
||||||
|
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
_dialog.add_child(scroll)
|
||||||
_choices = VBoxContainer.new()
|
_choices = VBoxContainer.new()
|
||||||
_choices.add_theme_constant_override("separation", 12)
|
_choices.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
_dialog.add_child(_choices)
|
_choices.add_theme_constant_override("separation", 10)
|
||||||
|
scroll.add_child(_choices)
|
||||||
|
_layout_dialog()
|
||||||
_dialog.hide()
|
_dialog.hide()
|
||||||
_update_hud()
|
_update_hud()
|
||||||
|
|
||||||
@ -203,13 +213,21 @@ func _show_poi(message := "") -> void:
|
|||||||
var description := UI.label(_choices, "Każdy kolejny etap daje więcej łupu, ale zwiększa ryzyko.\nNieudana próba uszkadza lądownik i kończy badanie miejsca.", 14)
|
var description := UI.label(_choices, "Każdy kolejny etap daje więcej łupu, ale zwiększa ryzyko.\nNieudana próba uszkadza lądownik i kończy badanie miejsca.", 14)
|
||||||
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
description.custom_minimum_size.x = 410
|
description.custom_minimum_size.x = 410
|
||||||
|
var bounds: Vector2i = EVENTS.REWARDS.module_bonus_range(progress.active_profile)
|
||||||
|
description.text += "\nPremia biomu: materiały ×%.1f · moduły +%d…+%d poz." % [EVENTS.REWARDS.material_multiplier(progress.active_profile),bounds.x,bounds.y]
|
||||||
if not message.is_empty():
|
if not message.is_empty():
|
||||||
var result := UI.label(_choices, message, 15, Color("#efca8a"))
|
var result := UI.label(_choices, message, 15, Color("#efca8a"))
|
||||||
result.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
result.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
result.custom_minimum_size.x = 410
|
result.custom_minimum_size.x = 410
|
||||||
|
var report := PROFILES.threat_report(progress.active_profile,captain)
|
||||||
|
description.text += "\nZagrożenie po sprzęcie: %.1f%% / 300%% (bez sprzętu %.1f%%)." % [report.total,report.raw_total]
|
||||||
if not _poi.complete:
|
if not _poi.complete:
|
||||||
var chance: float = preload("res://scripts/planets/planet_profiles.gd").success_chance(progress.active_profile, captain, _poi.stage)
|
var chance := PROFILES.success_chance(progress.active_profile,captain,_poi.stage)
|
||||||
UI.button(_choices, "%s · %d%% szans" % [{"mine":["Poziom 1 — wejście","Poziom 2 — głębszy szyb","Poziom 3 — najgłębsza komora"],"ruins":["Zbadaj dziedziniec","Otwórz komorę","Uruchom rdzeń"],"deposit":["Zbierz powierzchniowe złoże","Wykonaj odwiert","Wydobądź głęboką żyłę"]}[_poi.kind][int(_poi.stage)],roundi(chance*100)], _attempt)
|
var stages := {"mine":["Poziom 1 — wejście","Poziom 2 — głębszy szyb","Poziom 3 — najgłębsza komora"],"ruins":["Zbadaj dziedziniec","Otwórz komorę","Uruchom rdzeń"],"deposit":["Zbierz powierzchniowe złoże","Wykonaj odwiert","Wydobądź głęboką żyłę"]}
|
||||||
|
var attempt := UI.button(_choices,"%s · %.2f%% szans" % [stages[_poi.kind][int(_poi.stage)],chance*100.0],_attempt)
|
||||||
|
attempt.disabled = chance <= 0.0
|
||||||
|
if attempt.disabled:
|
||||||
|
attempt.tooltip_text = "Brak szans przy obecnym zagrożeniu. Potrzebujesz lepszego wyposażenia."
|
||||||
UI.button(_choices,"Zakończ badanie tego miejsca",func(): _poi.complete = true; _dialog.hide())
|
UI.button(_choices,"Zakończ badanie tego miejsca",func(): _poi.complete = true; _dialog.hide())
|
||||||
UI.button(_choices, "Wróć do lotu", func(): _dialog.hide())
|
UI.button(_choices, "Wróć do lotu", func(): _dialog.hide())
|
||||||
|
|
||||||
@ -219,6 +237,9 @@ func _attempt() -> void:
|
|||||||
var result: Dictionary = progress.resolve_poi(captain, _poi, _rng)
|
var result: Dictionary = progress.resolve_poi(captain, _poi, _rng)
|
||||||
if result.is_empty():
|
if result.is_empty():
|
||||||
return
|
return
|
||||||
|
if result.get("blocked",false):
|
||||||
|
_show_poi(str(result.reason))
|
||||||
|
return
|
||||||
var message := ""
|
var message := ""
|
||||||
if result.success:
|
if result.success:
|
||||||
message = "SUKCES · " + UI.material_text(result.materials)
|
message = "SUKCES · " + UI.material_text(result.materials)
|
||||||
@ -231,6 +252,11 @@ func _attempt() -> void:
|
|||||||
_update_hud()
|
_update_hud()
|
||||||
_show_poi(message)
|
_show_poi(message)
|
||||||
if progress.lander_hp <= 0:
|
if progress.lander_hp <= 0:
|
||||||
|
await _destroy_lander()
|
||||||
|
|
||||||
|
func _destroy_lander() -> void:
|
||||||
|
if _ending:
|
||||||
|
return
|
||||||
_ending = true
|
_ending = true
|
||||||
_status.text = "LĄDOWNIK ZNISZCZONY — utracono łup wyprawy"
|
_status.text = "LĄDOWNIK ZNISZCZONY — utracono łup wyprawy"
|
||||||
_dialog.hide()
|
_dialog.hide()
|
||||||
@ -246,7 +272,13 @@ func _update_hud() -> void:
|
|||||||
_hp.value = progress.lander_hp
|
_hp.value = progress.lander_hp
|
||||||
if not _ending:
|
if not _ending:
|
||||||
_status.text = "LĄDOWNIK %d / 100 HP · ENERGIA %d%%" % [ceili(progress.lander_hp),roundi(_motion.energy)]
|
_status.text = "LĄDOWNIK %d / 100 HP · ENERGIA %d%%" % [ceili(progress.lander_hp),roundi(_motion.energy)]
|
||||||
var loot := UI.material_text(progress.cargo)
|
_threat.text = "ZAGROŻENIE %.1f%% / 300%% · ŚREDNIA POI %.1f%%" % [PROFILES.global_threat(progress.active_profile,captain),PROFILES.average_success_chance(progress.active_profile,captain)*100.0]
|
||||||
|
var loot_data: Dictionary = progress.cargo.duplicate()
|
||||||
|
if progress.cargo_parts > 0: loot_data.parts = progress.cargo_parts
|
||||||
|
if progress.cargo_missiles > 0: loot_data.missile = progress.cargo_missiles
|
||||||
|
var loot := UI.material_text(loot_data)
|
||||||
|
if progress.cargo_credits > 0:
|
||||||
|
loot += (" · " if not loot.is_empty() else "") + "%d kr." % progress.cargo_credits
|
||||||
_cargo.text = "ŁUP WYPRAWY: " + (loot if not loot.is_empty() else "brak") + "\nModuły: %d" % progress.cargo_modules.size()
|
_cargo.text = "ŁUP WYPRAWY: " + (loot if not loot.is_empty() else "brak") + "\nModuły: %d" % progress.cargo_modules.size()
|
||||||
|
|
||||||
func _return(success: bool) -> void:
|
func _return(success: bool) -> void:
|
||||||
@ -270,46 +302,84 @@ func _check_events() -> void:
|
|||||||
_show_event(event)
|
_show_event(event)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
func _event_text(text: String, color := Color("d5e4e8"), font_size := 15) -> Label:
|
||||||
|
var label := UI.label(_choices,text,font_size,color)
|
||||||
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
return label
|
||||||
|
|
||||||
func _show_event(event: Dictionary, result: Dictionary = {}) -> void:
|
func _show_event(event: Dictionary, result: Dictionary = {}) -> void:
|
||||||
_active_event = event
|
_active_event = event
|
||||||
|
EVENTS.prepare(event,captain,progress)
|
||||||
for child in _choices.get_children():
|
for child in _choices.get_children():
|
||||||
_choices.remove_child(child)
|
_choices.remove_child(child)
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
|
_choices.get_parent().set("scroll_vertical",0)
|
||||||
_dialog.show()
|
_dialog.show()
|
||||||
UI.label(_choices,"SPOTKANIE / INŻYNIER",23,UI.CYAN)
|
_layout_dialog()
|
||||||
var text := "Znalazłeś miejscowego inżyniera. Za 100 kredytów sprzeda Ci moduł. Nie jesteś pewien, czy będzie coś warty."
|
_event_text(str(EVENTS.TITLES.get(event.kind,"Spotkanie")).to_upper(),UI.CYAN,22)
|
||||||
|
_event_text(str(EVENTS.STORIES.get(event.kind,"")))
|
||||||
|
var threat := PROFILES.threat_report(progress.active_profile,captain)
|
||||||
|
_event_text("Zagrożenie po sprzęcie: %.1f%% / 300%% (bez sprzętu %.1f%%)." % [threat.total,threat.raw_total],Color("efca8a"),13)
|
||||||
|
var reward_profile: Dictionary = event.get("reward_profile",progress.active_profile)
|
||||||
|
var bonus: Vector2i = EVENTS.REWARDS.module_bonus_range(reward_profile)
|
||||||
|
_event_text("Premia biomu: materiały ×%.1f · moduły +%d…+%d poz.\nLiczba amunicji i rakiet nie wzrasta." % [EVENTS.REWARDS.material_multiplier(reward_profile),bonus.x,bonus.y],Color("9bc4c7"),13)
|
||||||
if not result.is_empty():
|
if not result.is_empty():
|
||||||
if result.get("success",false):
|
_event_text(str(result.get("message",result.get("reason",""))),Color("efca8a"))
|
||||||
var item: ItemInstance = result.item
|
|
||||||
text = "Kupiono: %s\nPoziom modułu: %d\n\nModuł trafił do ładowni wyprawy. Wróć bezpiecznie na statek, aby go zachować." % [item.definition.display_name,item.item_level]
|
|
||||||
var icon := TextureRect.new()
|
|
||||||
icon.texture = item.definition.icon
|
|
||||||
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
|
||||||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
|
||||||
icon.custom_minimum_size = Vector2(70,70)
|
|
||||||
_choices.add_child(icon)
|
|
||||||
else:
|
|
||||||
text = str(result.get("reason","Oferta niedostępna."))
|
|
||||||
var description := UI.label(_choices,text,16)
|
|
||||||
description.custom_minimum_size.x = 410
|
|
||||||
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
||||||
if not event.complete:
|
if not event.complete:
|
||||||
UI.label(_choices,"Twoje kredyty: %d" % captain.credits,15,Color("efca8a"))
|
_event_text("Kredyty: %d · części: %d\nWydatki pobierane są od razu. Nowe nagrody zachowasz po bezpiecznym powrocie." % [EVENTS.stock(captain,progress,"credits"),EVENTS.stock(captain,progress,"parts")],Color("9bc4c7"),13)
|
||||||
var buy := UI.button(_choices,"KUP NIEZNANY MODUŁ · 100 KREDYTÓW",_buy_engineer)
|
if event.kind == "scan_cache":
|
||||||
buy.disabled = captain.credits < 100
|
_event_text(EVENTS.scan_text(event),Color("efca8a"))
|
||||||
|
if event.kind == "service" and progress.lander_hp >= 100:
|
||||||
|
_event_text("Kadłub jest w pełni sprawny. Naprawa nie jest potrzebna.",Color("efca8a"))
|
||||||
|
if event.kind == "trader":
|
||||||
|
_event_text("Możesz wybrać tylko jedną ofertę.",Color("efca8a"))
|
||||||
|
if event.kind in ["prototype","rescue","evacuation"]:
|
||||||
|
var limits: Vector2i = EVENTS.damage_range(event,progress)
|
||||||
|
_event_text("Sukces: %.2f%% · w razie porażki: −%d–%d HP\nSzansa uwzględnia warunki planety i sprzęt kapitana." % [EVENTS.chance(event,captain,progress)*100,limits.x,limits.y],Color("efca8a"))
|
||||||
|
if event.kind in ["blind_cache","safe_bet"]:
|
||||||
|
_event_text("Tajemniczy łup: 50% części, 35% minerały, 10% rakiety, 5% moduł. Wartość może być niższa od opłaty.",Color("9bc4c7"),13)
|
||||||
|
for option in EVENTS.options(event,captain,progress):
|
||||||
|
var action: String = option.id
|
||||||
|
var text: String = option.label
|
||||||
|
if not option.cost.is_empty():
|
||||||
|
text += "\nKoszt: "+EVENTS.reward_text(option.cost)
|
||||||
|
if not option.reward.is_empty():
|
||||||
|
text += "\n"+("Nagroda za sukces: " if event.kind in ["prototype","rescue","evacuation"] else "Otrzymasz: ")+EVENTS.reward_text(option.reward)
|
||||||
|
var button := UI.button(_choices,text,func(): _event_action(action))
|
||||||
|
button.add_theme_font_size_override("font_size",14)
|
||||||
|
button.clip_text = true
|
||||||
|
button.tooltip_text = str(option.get("tooltip",text))
|
||||||
|
button.disabled = not EVENTS.can_pay(captain,progress,option.cost)
|
||||||
|
if event.kind in ["prototype","rescue","evacuation"] and action != "stop" and EVENTS.chance(event,captain,progress) <= 0.0:
|
||||||
|
button.disabled = true
|
||||||
|
button.tooltip_text += "\nTen etap ma 0% szans przy obecnym zagrożeniu."
|
||||||
|
if button.disabled:
|
||||||
|
button.tooltip_text += "\nSprawdź wymagane zasoby i aktualne zagrożenie."
|
||||||
UI.button(_choices,"Może później",func(): _dialog.hide())
|
UI.button(_choices,"Może później",func(): _dialog.hide())
|
||||||
else:
|
else:
|
||||||
|
_event_text("Spotkanie zakończone. Nagrody w ładowni wyprawy przepadną po zniszczeniu lądownika.",Color("9bc4c7"),13)
|
||||||
UI.button(_choices,"Kontynuuj wyprawę",func(): _dialog.hide())
|
UI.button(_choices,"Kontynuuj wyprawę",func(): _dialog.hide())
|
||||||
|
|
||||||
func _buy_engineer() -> void:
|
func _event_action(action: String) -> void:
|
||||||
if _ending:
|
if _ending:
|
||||||
return
|
return
|
||||||
var result: Dictionary = progress.buy_engineer_offer(captain,_active_event,_rng)
|
var result: Dictionary = EVENTS.resolve(_active_event,action,captain,progress)
|
||||||
_show_event(_active_event,result)
|
_show_event(_active_event,result)
|
||||||
world.refresh_pois(lander.position)
|
world.refresh_pois(lander.position)
|
||||||
_update_hud()
|
_update_hud()
|
||||||
|
if progress.lander_hp <= 0:
|
||||||
|
await _destroy_lander()
|
||||||
|
|
||||||
|
func _layout_dialog() -> void:
|
||||||
|
if not is_instance_valid(_dialog):
|
||||||
|
return
|
||||||
|
var view := get_viewport_rect().size
|
||||||
|
var width := minf(540,maxf(280,view.x-32))
|
||||||
|
_dialog.size = Vector2(width,maxf(180,view.y-112))
|
||||||
|
_dialog.position = Vector2(view.x-width-16,80)
|
||||||
|
|
||||||
func _resize_surface_viewport() -> void:
|
func _resize_surface_viewport() -> void:
|
||||||
|
_layout_dialog()
|
||||||
if is_instance_valid(_surface_viewport):
|
if is_instance_valid(_surface_viewport):
|
||||||
_surface_viewport.size = Vector2i(Vector2(maxf(1,size.x),maxf(1,size.y))*SURFACE_RENDER_SCALE)
|
_surface_viewport.size = Vector2i(Vector2(maxf(1,size.x),maxf(1,size.y))*SURFACE_RENDER_SCALE)
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
extends Node2D
|
extends Node2D
|
||||||
|
const GENERATOR_ATLAS_PATH := preload("res://scripts/planets/surface_details.gd").ATLAS_PATH
|
||||||
|
const BUILDING_SCALE := preload("res://scripts/planets/surface_scenery.gd").BUILDING_SCALE
|
||||||
const GENERATOR := preload("res://scripts/planets/surface_generator.gd")
|
const GENERATOR := preload("res://scripts/planets/surface_generator.gd")
|
||||||
var data: Dictionary
|
var data: Dictionary
|
||||||
var terrain: ImageTexture
|
var terrain: ImageTexture
|
||||||
|
var _nature: Array[AtlasTexture] = []
|
||||||
var _tree: Texture2D
|
var _tree: Texture2D
|
||||||
var _mountain: Texture2D
|
var _mountain: Texture2D
|
||||||
var _poi_sprites: Dictionary = {}
|
var _poi_sprites: Dictionary = {}
|
||||||
@ -29,18 +32,34 @@ func _ready() -> void:
|
|||||||
material.set_shader_parameter("seed_offset",float(posmod(int(data.seed),10000)))
|
material.set_shader_parameter("seed_offset",float(posmod(int(data.seed),10000)))
|
||||||
ground.material = material
|
ground.material = material
|
||||||
add_child(ground)
|
add_child(ground)
|
||||||
|
var atlas: Texture2D = load(GENERATOR_ATLAS_PATH)
|
||||||
|
var atlas_scale := Vector2(atlas.get_size()) / 1280.0
|
||||||
|
# The painted sheet has uneven transparent gutters; avoid clipping canopies.
|
||||||
|
var rows := [Vector2(0,360), Vector2(360,280), Vector2(640,310), Vector2(950,330)]
|
||||||
|
for index in 16:
|
||||||
|
var tile := AtlasTexture.new()
|
||||||
|
tile.atlas = atlas
|
||||||
|
var band: Vector2 = rows[index / 4]
|
||||||
|
var region := Rect2(float(index % 4)*320.0, band.x, 320.0, band.y)
|
||||||
|
if index == 12:
|
||||||
|
region = Rect2(0,950,365,330)
|
||||||
|
elif index == 13:
|
||||||
|
region = Rect2(370,950,270,330)
|
||||||
|
tile.region = Rect2(region.position*atlas_scale, region.size*atlas_scale)
|
||||||
|
tile.filter_clip = true
|
||||||
|
_nature.append(tile)
|
||||||
_tree = load("res://assets/planets/tree.png")
|
_tree = load("res://assets/planets/tree.png")
|
||||||
_mountain = load("res://assets/planets/mountain.png")
|
_mountain = load("res://assets/planets/mountain.png")
|
||||||
for poi in data.pois:
|
for poi in data.pois:
|
||||||
var sprite := Sprite2D.new()
|
var sprite := Sprite2D.new()
|
||||||
sprite.texture = load("res://assets/planets/%s.png" % ("ruins" if poi.kind == "ruins" else "mine")) if poi.kind != "deposit" else load("res://assets/icons/resources/titanium.png")
|
sprite.texture = load("res://assets/planets/%s.png" % ("ruins" if poi.kind == "ruins" else "mine")) if poi.kind != "deposit" else load("res://assets/icons/resources/titanium.png")
|
||||||
sprite.position = poi.position
|
sprite.position = poi.position
|
||||||
sprite.scale = Vector2.ONE * (125.0 if poi.kind != "deposit" else 60.0) / maxf(sprite.texture.get_width(), sprite.texture.get_height())
|
sprite.scale = Vector2.ONE * (125.0*BUILDING_SCALE if poi.kind != "deposit" else 60.0) / maxf(sprite.texture.get_width(), sprite.texture.get_height())
|
||||||
sprite.z_index = 10
|
sprite.z_index = 10
|
||||||
add_child(sprite)
|
add_child(sprite)
|
||||||
_poi_sprites[poi.id] = sprite
|
_poi_sprites[poi.id] = sprite
|
||||||
var label := Label.new()
|
var label := Label.new()
|
||||||
label.position = poi.position + Vector2(-120, 65)
|
label.position = poi.position + Vector2(-120, 46)
|
||||||
label.size.x = 240
|
label.size.x = 240
|
||||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
label.add_theme_font_size_override("font_size", 13)
|
label.add_theme_font_size_override("font_size", 13)
|
||||||
@ -50,6 +69,15 @@ func _ready() -> void:
|
|||||||
label.z_index = 15
|
label.z_index = 15
|
||||||
add_child(label)
|
add_child(label)
|
||||||
_poi_labels[poi.id] = label
|
_poi_labels[poi.id] = label
|
||||||
|
for landmark in data.get("landmarks", []):
|
||||||
|
var prop := Sprite2D.new()
|
||||||
|
prop.name = str(landmark.kind).to_pascal_case()
|
||||||
|
prop.texture = load("res://assets/planets/landmarks/%s.png" % landmark.kind)
|
||||||
|
prop.position = landmark.position
|
||||||
|
prop.rotation = landmark.rotation
|
||||||
|
prop.scale = Vector2.ONE * float(landmark.size) / maxf(prop.texture.get_width(), prop.texture.get_height())
|
||||||
|
prop.z_index = -2
|
||||||
|
add_child(prop)
|
||||||
for structure in data.structures:
|
for structure in data.structures:
|
||||||
var prop := Sprite2D.new()
|
var prop := Sprite2D.new()
|
||||||
prop.name = str(structure.kind).to_pascal_case()
|
prop.name = str(structure.kind).to_pascal_case()
|
||||||
@ -99,10 +127,13 @@ func refresh_pois(lander_position: Vector2) -> void:
|
|||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
if terrain == null:
|
if terrain == null:
|
||||||
return
|
return
|
||||||
|
for detail in data.get("details", []):
|
||||||
|
var shade := float(detail.shade)
|
||||||
|
_draw_prop(_nature[int(detail.variant)], detail.position, detail.size, detail.rotation, Color(shade,shade,shade))
|
||||||
for mountain in data.mountains:
|
for mountain in data.mountains:
|
||||||
_draw_prop(_mountain, mountain.position, mountain.size, mountain.rotation, Color.WHITE)
|
_draw_prop(_mountain, mountain.position, mountain.size, mountain.rotation, Color.WHITE)
|
||||||
for rock in data.rocks:
|
for rock in data.rocks:
|
||||||
_draw_prop(_mountain,rock.position,rock.size,rock.rotation,Color("b3b0a0"))
|
_draw_prop(_nature[int(rock.get("variant",4))],rock.position,rock.size,rock.rotation,Color.WHITE)
|
||||||
for tree in data.trees:
|
for tree in data.trees:
|
||||||
var shade := float(tree.shade)
|
var shade := float(tree.shade)
|
||||||
var color: Color = Color(shade,shade,shade)*data.landscape.trees
|
var color: Color = Color(shade,shade,shade)*data.landscape.trees
|
||||||
@ -110,12 +141,14 @@ func _draw() -> void:
|
|||||||
color = color.lerp(Color("d1e1da"), 0.4)
|
color = color.lerp(Color("d1e1da"), 0.4)
|
||||||
elif int(data.profile.temperature) > 40:
|
elif int(data.profile.temperature) > 40:
|
||||||
color = color.lerp(Color("c6a86b"), 0.28)
|
color = color.lerp(Color("c6a86b"), 0.28)
|
||||||
_draw_prop(_tree, tree.position, tree.size, tree.rotation, color)
|
var variant := int(tree.get("variant", -1))
|
||||||
|
_draw_prop(_tree if variant < 0 else _nature[variant], tree.position, tree.size, tree.rotation, color)
|
||||||
# Small landing beacon.
|
# Small landing beacon.
|
||||||
draw_circle(Vector2.ZERO, 50, Color(0.5, 0.85, 0.8, 0.12))
|
draw_circle(Vector2.ZERO, 50, Color(0.5, 0.85, 0.8, 0.12))
|
||||||
draw_arc(Vector2.ZERO, 48, 0, TAU, 48, Color("8cd9c2"), 2, true)
|
draw_arc(Vector2.ZERO, 48, 0, TAU, 48, Color("8cd9c2"), 2, true)
|
||||||
|
|
||||||
func _draw_prop(texture: Texture2D, point: Vector2, width: float, angle: float, color: Color) -> void:
|
func _draw_prop(texture: Texture2D, point: Vector2, width: float, angle: float, color: Color) -> void:
|
||||||
draw_set_transform(point, angle)
|
draw_set_transform(point, angle)
|
||||||
draw_texture_rect(texture, Rect2(-width * 0.5, -width * 0.5, width, width), false, color)
|
var size := Vector2(width, width*float(texture.get_height())/float(texture.get_width()))
|
||||||
|
draw_texture_rect(texture, Rect2(-size*0.5, size), false, color)
|
||||||
draw_set_transform(Vector2.ZERO)
|
draw_set_transform(Vector2.ZERO)
|
||||||
|
|||||||
@ -41,6 +41,8 @@ func refresh() -> void:
|
|||||||
_picture.modulate.a = 1.0 if not gear.is_empty() else 0.22
|
_picture.modulate.a = 1.0 if not gear.is_empty() else 0.22
|
||||||
_label.text = "POZ. %d" % int(gear.level) if not gear.is_empty() else ("ODŁÓŻ" if return_slot else "—")
|
_label.text = "POZ. %d" % int(gear.level) if not gear.is_empty() else ("ODŁÓŻ" if return_slot else "—")
|
||||||
tooltip_text = "Odłóż wyposażenie kapitana" if return_slot else "%s\n%s\nPrzeciągnij przedmiot do pasującego slotu. Dwuklik zakłada lub zdejmuje." % [GEAR.NAMES.get(kind, kind), "Poziom %d" % int(gear.level) if not gear.is_empty() else "Pusty slot"]
|
tooltip_text = "Odłóż wyposażenie kapitana" if return_slot else "%s\n%s\nPrzeciągnij przedmiot do pasującego slotu. Dwuklik zakłada lub zdejmuje." % [GEAR.NAMES.get(kind, kind), "Poziom %d" % int(gear.level) if not gear.is_empty() else "Pusty slot"]
|
||||||
|
if not return_slot and not gear.is_empty():
|
||||||
|
tooltip_text += "\nRedukcja zagrożenia: %d pp (2 pp/poziom, maks. 100 pp).\nPoziom 50 całkowicie neutralizuje odpowiadające zagrożenie." % mini(100,2*int(gear.level))
|
||||||
_style()
|
_style()
|
||||||
|
|
||||||
func _style() -> void:
|
func _style() -> void:
|
||||||
|
|||||||
@ -124,7 +124,16 @@ func _ready() -> void:
|
|||||||
manager.screen._poi = manager.screen.world.data.pois[0]
|
manager.screen._poi = manager.screen.world.data.pois[0]
|
||||||
manager.screen._show_poi()
|
manager.screen._show_poi()
|
||||||
manager.screen._attempt()
|
manager.screen._attempt()
|
||||||
|
var missiles_before := inventory.missile_ammunition
|
||||||
|
var credits_before := GameState.captain.credits
|
||||||
|
var owned_parts_before := GameState.captain.parts
|
||||||
|
GameState.captain.expedition.cargo_missiles = 3
|
||||||
|
GameState.captain.expedition.cargo_credits = 23
|
||||||
|
GameState.captain.expedition.cargo_parts = 4
|
||||||
manager.finish_surface(true)
|
manager.finish_surface(true)
|
||||||
|
assert(inventory.missile_ammunition == missiles_before+3)
|
||||||
|
assert(GameState.captain.credits == credits_before+23)
|
||||||
|
assert(GameState.captain.parts == owned_parts_before+4)
|
||||||
assert(not get_tree().paused)
|
assert(not get_tree().paused)
|
||||||
game.queue_free()
|
game.queue_free()
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
|
|||||||
@ -9,7 +9,7 @@ func check_hint(expected: String) -> void:
|
|||||||
await settle()
|
await settle()
|
||||||
assert(manager._prompt.visible)
|
assert(manager._prompt.visible)
|
||||||
assert(manager._text.text.contains(expected))
|
assert(manager._text.text.contains(expected))
|
||||||
assert(manager._prompt.size.y < 260)
|
assert(manager._prompt.size.y < 450)
|
||||||
assert(manager._prompt.get_global_rect().encloses(manager._text.get_global_rect()))
|
assert(manager._prompt.get_global_rect().encloses(manager._text.get_global_rect()))
|
||||||
if manager._land_button.visible:
|
if manager._land_button.visible:
|
||||||
assert(manager._prompt.get_global_rect().encloses(manager._land_button.get_global_rect()))
|
assert(manager._prompt.get_global_rect().encloses(manager._land_button.get_global_rect()))
|
||||||
@ -61,7 +61,7 @@ func _ready() -> void:
|
|||||||
assert(not manager._land_button.disabled)
|
assert(not manager._land_button.disabled)
|
||||||
manager.scan_nearby()
|
manager.scan_nearby()
|
||||||
manager._process(0)
|
manager._process(0)
|
||||||
await check_hint("TEMPERATURA")
|
await check_hint("ŚREDNIA SZANSA POI")
|
||||||
if "--capture-preview" in OS.get_cmdline_user_args():
|
if "--capture-preview" in OS.get_cmdline_user_args():
|
||||||
await RenderingServer.frame_post_draw
|
await RenderingServer.frame_post_draw
|
||||||
get_viewport().get_texture().get_image().save_png("res://artifacts/planet_scan_stats.png")
|
get_viewport().get_texture().get_image().save_png("res://artifacts/planet_scan_stats.png")
|
||||||
|
|||||||
110
tests/levels/planet_reward_scaling_test.gd
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
extends Node
|
||||||
|
const REWARDS := preload("res://scripts/planets/planet_rewards.gd")
|
||||||
|
const EVENTS := preload("res://scripts/planets/surface_events.gd")
|
||||||
|
const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
|
||||||
|
|
||||||
|
func offer(event: Dictionary, captain: Resource, state: Resource, id: String) -> Dictionary:
|
||||||
|
for option in EVENTS.options(event,captain,state):
|
||||||
|
if option.id == id:
|
||||||
|
return option
|
||||||
|
return {}
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
GameState.system_level = 32
|
||||||
|
var captain: Resource = preload("res://scripts/captain/captain_progress.gd").new()
|
||||||
|
captain.credits = 100000
|
||||||
|
captain.parts = 1000
|
||||||
|
for key in ["iron","sulfur","uranium","copper","nickel"]:
|
||||||
|
captain.minerals[key] = 1000
|
||||||
|
var state: Resource = captain.expedition
|
||||||
|
var samples: Array[Dictionary] = []
|
||||||
|
for index in 3:
|
||||||
|
var d := float(index)*0.5
|
||||||
|
var profile := PROFILES.generate(777,1,"Biom testowy")
|
||||||
|
profile.difficulty = d
|
||||||
|
profile.oxygen = roundi(80*(1.0-d))
|
||||||
|
profile.temperature = roundi(40+60*d)
|
||||||
|
profile.flora = roundi(50*d)
|
||||||
|
profile.landing_damage = 5
|
||||||
|
var expected_bounds: Vector2i = [Vector2i(0,2),Vector2i(1,6),Vector2i(2,10)][index]
|
||||||
|
assert(REWARDS.module_bonus_range(profile) == expected_bounds)
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = 348838
|
||||||
|
var histogram := {}
|
||||||
|
for sample_index in 10000:
|
||||||
|
var bonus := REWARDS.roll_module_bonus(rng,profile)
|
||||||
|
assert(bonus >= expected_bounds.x and bonus <= expected_bounds.y)
|
||||||
|
histogram[bonus] = int(histogram.get(bonus,0))+1
|
||||||
|
for bonus in range(expected_bounds.x,expected_bounds.y):
|
||||||
|
assert(int(histogram[bonus]) > int(histogram[bonus+1]))
|
||||||
|
assert(histogram.has(expected_bounds.y))
|
||||||
|
assert(REWARDS.material_multiplier(profile) == 1.0+index)
|
||||||
|
# Explicitly exclude all ammunition, rockets, credits and consumables.
|
||||||
|
var scaled := REWARDS.scale_materials({"parts":6,"copper":8,"silicon":8,"missile":4,"ammunition":10,"irradiated_ammunition":10,"energy_bank":1,"credits":50},profile)
|
||||||
|
assert(scaled.parts == 6*(1+index) and scaled.copper == 8*(1+index))
|
||||||
|
assert(scaled.missile == 4 and scaled.ammunition == 10 and scaled.irradiated_ammunition == 10 and scaled.energy_bank == 1 and scaled.credits == 50)
|
||||||
|
state.lander_hp = 100
|
||||||
|
assert(state.begin_visit(profile))
|
||||||
|
var trader := {"kind":"trader","seed":1234,"complete":false}
|
||||||
|
var goods := offer(trader,captain,state,"materials")
|
||||||
|
assert(goods.reward.copper == 8*(1+index) and goods.reward.silicon == 8*(1+index))
|
||||||
|
var missiles := offer(trader,captain,state,"missiles")
|
||||||
|
var barter := {"kind":"barter","seed":1234,"complete":false}
|
||||||
|
var barter_missiles := offer(barter,captain,state,"missiles")
|
||||||
|
assert(barter_missiles.reward.missile == 3)
|
||||||
|
assert(trader.module.item_level >= 32+expected_bounds.x and trader.module.item_level <= 32+expected_bounds.y)
|
||||||
|
var safe := {"kind":"safe_bet","seed":1234,"complete":false}
|
||||||
|
var guaranteed := offer(safe,captain,state,"safe")
|
||||||
|
assert(guaranteed.reward.parts == 6*(1+index))
|
||||||
|
assert(EVENTS.resolve(safe,"safe",captain,state).success)
|
||||||
|
assert(state.cargo_parts == 6*(1+index),"A reward must be scaled exactly once")
|
||||||
|
# Hidden outcomes retain their category and ammo counts for a shared seed.
|
||||||
|
var hidden := []
|
||||||
|
for seed_value in 200:
|
||||||
|
var chest_rng := RandomNumberGenerator.new()
|
||||||
|
chest_rng.seed = seed_value
|
||||||
|
var prize := EVENTS._mystery(chest_rng,32,profile)
|
||||||
|
hidden.append(prize)
|
||||||
|
# Every POI stage uses the biome range, without an additional stage/luck level.
|
||||||
|
var modules := 0
|
||||||
|
for attempt in 1200:
|
||||||
|
state.lander_hp = 100
|
||||||
|
var stage := attempt%3
|
||||||
|
var poi := {"kind":"mine","stage":stage,"complete":false}
|
||||||
|
var result: Dictionary = state.resolve_poi(captain,poi,rng)
|
||||||
|
if result.success:
|
||||||
|
for key in result.materials:
|
||||||
|
var minimum := roundi(5*(1+index)*(1.0+stage*0.65))
|
||||||
|
var maximum := roundi(12*(1+index)*(1.0+stage*0.65))
|
||||||
|
assert(int(result.materials[key]) >= minimum and int(result.materials[key]) <= maximum)
|
||||||
|
if result.module != null:
|
||||||
|
modules += 1
|
||||||
|
assert(result.module.item_level >= 32+expected_bounds.x and result.module.item_level <= 32+expected_bounds.y)
|
||||||
|
state.cargo_modules.clear()
|
||||||
|
assert(modules > 5)
|
||||||
|
samples.append({"profile":profile,"missile_count":missiles.reward.missile,"missile_price":missiles.cost.credits,"ammo_barter_cost":barter_missiles.cost.iron,"goods_cost":goods.cost.credits,"hidden":hidden})
|
||||||
|
state.finish_visit(captain,true)
|
||||||
|
print("BIOME ",d,": module range +",expected_bounds.x,"..+",expected_bounds.y,"; materials x",1+index,"; rockets=",missiles.reward.missile,"; price=",missiles.cost.credits,"; barter cost=",barter_missiles.cost.iron,"; tested POI modules=",modules)
|
||||||
|
for index in range(1,3):
|
||||||
|
assert(samples[index].missile_count == samples[0].missile_count)
|
||||||
|
assert(samples[index].missile_price < samples[index-1].missile_price)
|
||||||
|
assert(samples[index].ammo_barter_cost < samples[index-1].ammo_barter_cost)
|
||||||
|
assert(samples[index].goods_cost == samples[0].goods_cost)
|
||||||
|
for seed_value in 200:
|
||||||
|
var easy_prize: Dictionary = samples[0].hidden[seed_value]
|
||||||
|
var prize: Dictionary = samples[index].hidden[seed_value]
|
||||||
|
assert(prize.keys() == easy_prize.keys())
|
||||||
|
for key in prize:
|
||||||
|
if key in REWARDS.MATERIAL_KEYS:
|
||||||
|
assert(int(prize[key]) == int(easy_prize[key])*(1+index))
|
||||||
|
elif key == "missile":
|
||||||
|
assert(prize[key] == easy_prize[key])
|
||||||
|
elif key == "module":
|
||||||
|
var bounds := REWARDS.module_bonus_range(samples[index].profile)
|
||||||
|
assert(prize[key].item_level >= 32+bounds.x and prize[key].item_level <= 32+bounds.y)
|
||||||
|
var cap_rng := RandomNumberGenerator.new()
|
||||||
|
for level in [995,999,1000]:
|
||||||
|
for index in 50:
|
||||||
|
assert(REWARDS.generate_module(cap_rng,{"difficulty":1.0},level).item_level <= 1000)
|
||||||
|
print("BIOME REWARD SCALING PASS: 30000 bonus rolls, 3600 POI attempts, 600 mystery outcomes; materials grow, ammo counts stable, rocket offers cheaper, no duplicate scaling, level cap respected")
|
||||||
|
get_tree().quit()
|
||||||
4
tests/levels/planet_reward_scaling_test.tscn
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://tests/levels/planet_reward_scaling_test.gd" id="1"]
|
||||||
|
[node name="PlanetRewardScalingTest" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
109
tests/levels/planet_threats_test.gd
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
extends Node
|
||||||
|
const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
|
||||||
|
const GEAR := preload("res://scripts/planets/captain_equipment.gd")
|
||||||
|
const EVENTS := preload("res://scripts/planets/surface_events.gd")
|
||||||
|
const REWARDS := preload("res://scripts/planets/planet_rewards.gd")
|
||||||
|
var captain: Resource
|
||||||
|
|
||||||
|
func set_gear(levels: Array) -> void:
|
||||||
|
for index in 3:
|
||||||
|
captain.gear_items[index].level = int(levels[index])
|
||||||
|
captain.gear_slots[GEAR.KINDS[index]] = index+1
|
||||||
|
|
||||||
|
func profile_for(total: float) -> Dictionary:
|
||||||
|
var fraction := total/300.0
|
||||||
|
return {"id":"threat-test","name":"Test zagrożeń","easy":total == 0.0,"seed":1234,"oxygen":80.0*(1.0-fraction),"temperature":40.0+60.0*fraction,"flora":50.0*fraction,"difficulty":fraction,"landing_damage":5}
|
||||||
|
|
||||||
|
func expect_chances(profile: Dictionary, expected: Array) -> void:
|
||||||
|
for stage in 3:
|
||||||
|
assert(is_equal_approx(PROFILES.success_chance(profile,captain,stage),float(expected[stage])/100.0))
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
captain = preload("res://scripts/captain/captain_progress.gd").new()
|
||||||
|
set_gear([0,0,0])
|
||||||
|
var safe := profile_for(0)
|
||||||
|
safe.temperature = 30
|
||||||
|
safe.oxygen = 98
|
||||||
|
assert(PROFILES.global_threat(safe,captain) == 0)
|
||||||
|
expect_chances(safe,[90,70,40])
|
||||||
|
assert(is_equal_approx(PROFILES.average_success_chance(safe,captain),2.0/3.0))
|
||||||
|
var extreme := profile_for(300)
|
||||||
|
assert(PROFILES.global_threat(extreme,captain) == 300)
|
||||||
|
expect_chances(extreme,[15,5,0])
|
||||||
|
expect_chances(profile_for(75),[52.5,37.5,20])
|
||||||
|
expect_chances(profile_for(150),[15,5,0])
|
||||||
|
expect_chances(profile_for(151),[15,5,0])
|
||||||
|
var cold := extreme.duplicate()
|
||||||
|
cold.temperature = -50
|
||||||
|
assert(PROFILES.global_threat(cold,captain) == 300)
|
||||||
|
for temperature in [-10,0,30,40]:
|
||||||
|
var normal := safe.duplicate()
|
||||||
|
normal.temperature = temperature
|
||||||
|
assert(PROFILES.global_threat(normal,captain) == 0)
|
||||||
|
var previous := 1.0
|
||||||
|
for index in 301:
|
||||||
|
var at := profile_for(float(index))
|
||||||
|
var raw := PROFILES.threat_report(at,captain)
|
||||||
|
assert(raw.raw_total >= 0 and raw.raw_total <= 300.0001)
|
||||||
|
var chance := PROFILES.success_chance(at,captain,0)
|
||||||
|
assert(chance <= previous+0.000001)
|
||||||
|
previous = chance
|
||||||
|
set_gear([1,1,1])
|
||||||
|
assert(PROFILES.global_threat(extreme,captain) == 294)
|
||||||
|
set_gear([25,25,25])
|
||||||
|
assert(PROFILES.global_threat(extreme,captain) == 150)
|
||||||
|
set_gear([26,26,26])
|
||||||
|
assert(PROFILES.global_threat(extreme,captain) == 144)
|
||||||
|
expect_chances(extreme,[18,7.6,1.6])
|
||||||
|
set_gear([50,0,0])
|
||||||
|
var report := PROFILES.threat_report(extreme,captain)
|
||||||
|
assert(report.effective.oxygen == 0 and report.effective.temperature == 100 and report.effective.biology == 100)
|
||||||
|
assert(report.total == 200)
|
||||||
|
set_gear([0,50,0])
|
||||||
|
report = PROFILES.threat_report(extreme,captain)
|
||||||
|
assert(report.effective.temperature == 0 and report.total == 200)
|
||||||
|
set_gear([0,0,50])
|
||||||
|
report = PROFILES.threat_report(extreme,captain)
|
||||||
|
assert(report.effective.biology == 0 and report.total == 200)
|
||||||
|
set_gear([50,50,50])
|
||||||
|
assert(PROFILES.global_threat(extreme,captain) == 0)
|
||||||
|
expect_chances(extreme,[90,70,40])
|
||||||
|
set_gear([100,100,100])
|
||||||
|
expect_chances(extreme,[90,70,40])
|
||||||
|
assert(PROFILES.threat_report(extreme,captain).raw_total == 300)
|
||||||
|
assert(REWARDS.material_multiplier(extreme) == 3)
|
||||||
|
assert(REWARDS.module_bonus_range(extreme) == Vector2i(2,10))
|
||||||
|
# Equipment changes effective odds, never the underlying planet rewards.
|
||||||
|
set_gear([0,0,0])
|
||||||
|
var state: Resource = captain.expedition
|
||||||
|
assert(state.begin_visit(extreme))
|
||||||
|
state.lander_hp = 100
|
||||||
|
captain.parts = 100
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = 45
|
||||||
|
var rng_before := rng.state
|
||||||
|
var poi := {"kind":"mine","stage":2,"complete":false}
|
||||||
|
var result: Dictionary = state.resolve_poi(captain,poi,rng)
|
||||||
|
assert(result.blocked and result.chance == 0 and not poi.complete)
|
||||||
|
assert(state.lander_hp == 100 and rng.state == rng_before)
|
||||||
|
var evacuation := {"kind":"evacuation","seed":1234,"complete":false}
|
||||||
|
EVENTS.prepare(evacuation,captain,state)
|
||||||
|
evacuation.stage = 2
|
||||||
|
assert(EVENTS.chance(evacuation,captain,state) == 0)
|
||||||
|
var balance: float = captain.parts
|
||||||
|
result = EVENTS.resolve(evacuation,"help",captain,state)
|
||||||
|
assert(result.blocked and captain.parts == balance and not evacuation.complete)
|
||||||
|
assert(is_equal_approx(EVENTS.chance({"kind":"rescue"},captain,state),0.15))
|
||||||
|
assert(is_equal_approx(EVENTS.chance({"kind":"prototype"},captain,state),0.05))
|
||||||
|
set_gear([50,50,50])
|
||||||
|
assert(is_equal_approx(EVENTS.chance(evacuation,captain,state),0.40))
|
||||||
|
assert(is_equal_approx(EVENTS.chance({"kind":"rescue"},captain,state),0.90))
|
||||||
|
var scan := PROFILES.scan_description(extreme,captain)
|
||||||
|
assert(scan.contains("ŚREDNIA SZANSA POI: 66.7%") and scan.contains("90.0% / 70.0% / 40.0%"))
|
||||||
|
assert(scan.contains("Bez sprzętu: 300.0%"))
|
||||||
|
for index in 1000:
|
||||||
|
var generated := PROFILES.generate(99,index,"Biom")
|
||||||
|
var raw := PROFILES.raw_threats(generated)
|
||||||
|
assert(is_equal_approx(float(generated.difficulty),(float(raw.oxygen)+float(raw.temperature)+float(raw.biology))/300.0))
|
||||||
|
print("PLANET THREATS PASS: 0..300 total, three independent protections, 2 pp/level, L50 immunity, exact POI curves, 150 threshold, zero-chance guards, event risk, scan average, raw reward difficulty")
|
||||||
|
get_tree().quit()
|
||||||
4
tests/levels/planet_threats_test.tscn
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://tests/levels/planet_threats_test.gd" id="1"]
|
||||||
|
[node name="PlanetThreatsTest" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
@ -1,59 +1,187 @@
|
|||||||
extends Node
|
extends Node
|
||||||
const PROGRESS := preload("res://scripts/planets/planet_progress.gd")
|
const EVENTS := preload("res://scripts/planets/surface_events.gd")
|
||||||
const GENERATOR := preload("res://scripts/planets/surface_generator.gd")
|
const GENERATOR := preload("res://scripts/planets/surface_generator.gd")
|
||||||
|
const SCENERY := preload("res://scripts/planets/surface_scenery.gd")
|
||||||
|
const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
|
||||||
|
var captain: Resource
|
||||||
|
var state: Resource
|
||||||
|
var profile: Dictionary
|
||||||
|
|
||||||
|
class InventoryStub extends Node:
|
||||||
|
signal ammunition_changed
|
||||||
|
signal inventory_changed
|
||||||
|
var missile_ammunition := 2
|
||||||
|
func add_item(_item: Resource) -> bool:
|
||||||
|
return false
|
||||||
|
|
||||||
|
func fresh(kind: String, seed_value := 123) -> Dictionary:
|
||||||
|
captain = preload("res://scripts/captain/captain_progress.gd").new()
|
||||||
|
captain.credits = 100000
|
||||||
|
captain.parts = 100
|
||||||
|
for key in ["iron","copper","nickel","silicon","tungsten","mercury","sulfur","uranium"]:
|
||||||
|
captain.minerals[key] = 100
|
||||||
|
state = captain.expedition
|
||||||
|
assert(state.begin_visit(profile))
|
||||||
|
var event := {"kind":kind,"seed":seed_value,"complete":false}
|
||||||
|
EVENTS.prepare(event,captain,state)
|
||||||
|
return event
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
var rng := RandomNumberGenerator.new()
|
GameState.system_level = 3
|
||||||
rng.seed = 727373
|
profile = PROFILES.generate(777,1,"Test wydarzeń")
|
||||||
var histogram := PackedInt32Array()
|
profile.difficulty = 0.0
|
||||||
histogram.resize(21)
|
profile.oxygen = 95
|
||||||
for index in 40000:
|
profile.temperature = 20
|
||||||
var bonus: int = PROGRESS.roll_engineer_bonus(rng)
|
profile.flora = 0
|
||||||
assert(bonus >= 0 and bonus <= 20)
|
for kind in EVENTS.KINDS:
|
||||||
histogram[bonus] += 1
|
var event := fresh(kind)
|
||||||
assert(histogram[0] > 9500 and histogram[0] < 10500)
|
assert(not str(EVENTS.STORIES[kind]).is_empty())
|
||||||
assert(histogram[10] > 450 and histogram[10] < 700)
|
var offers := EVENTS.options(event,captain,state)
|
||||||
assert(histogram[20] > 0 and histogram[20] < 80)
|
assert(not offers.is_empty())
|
||||||
var captain: Resource = preload("res://scripts/captain/captain_progress.gd").new()
|
var snapshot := var_to_str(offers)
|
||||||
var state: Resource = captain.expedition
|
assert(snapshot == var_to_str(EVENTS.options(event,captain,state)))
|
||||||
var profile: Dictionary = preload("res://scripts/planets/planet_profiles.gd").generate(777,1,"Test")
|
event.rolls = [0.0,0.0,0.0]
|
||||||
state.begin_visit(profile)
|
var result := EVENTS.resolve(event,str(offers[0].id),captain,state)
|
||||||
var event := {"kind":"engineer","complete":false}
|
assert(result.success)
|
||||||
captain.credits = 99
|
if kind == "scan_cache":
|
||||||
assert(not state.buy_engineer_offer(captain,event,rng).success)
|
assert(event.scanned and not event.complete)
|
||||||
assert(captain.credits == 99 and state.cargo_modules.is_empty() and not event.complete)
|
var before: int = captain.credits
|
||||||
captain.credits = 200
|
assert(not EVENTS.resolve(event,"scan",captain,state).success)
|
||||||
var before_level := GameState.system_level
|
assert(captain.credits == before)
|
||||||
GameState.system_level = 32
|
assert(EVENTS.resolve(event,"open",captain,state).success)
|
||||||
var result: Dictionary = state.buy_engineer_offer(captain,event,rng)
|
if kind == "evacuation":
|
||||||
assert(result.success and captain.credits == 100 and event.complete)
|
assert(event.stage == 1 and not event.complete)
|
||||||
assert(result.item.item_level >= 32 and result.item.item_level <= 52)
|
assert(EVENTS.resolve(event,"help",captain,state).success)
|
||||||
assert(state.cargo_modules.size() == 1)
|
assert(EVENTS.resolve(event,"help",captain,state).success)
|
||||||
assert(not state.buy_engineer_offer(captain,event,rng).success)
|
assert(event.complete)
|
||||||
assert(captain.credits == 100 and state.cargo_modules.size() == 1)
|
var credits_before: int = captain.credits
|
||||||
state.finish_visit(captain,true)
|
var cargo_before := var_to_str([state.cargo,state.cargo_credits,state.cargo_parts,state.cargo_missiles,state.cargo_modules])
|
||||||
assert(state.delivered_modules.size() == 1)
|
assert(not EVENTS.resolve(event,str(offers[0].id),captain,state).success)
|
||||||
|
assert(captain.credits == credits_before)
|
||||||
|
assert(cargo_before == var_to_str([state.cargo,state.cargo_credits,state.cargo_parts,state.cargo_missiles,state.cargo_modules]))
|
||||||
|
# Server-side affordability and action validation; no partial payment or reroll.
|
||||||
|
var event := fresh("blind_cache")
|
||||||
|
captain.credits = 0
|
||||||
|
var prize_before := var_to_str(event.prize)
|
||||||
|
assert(not EVENTS.resolve(event,"open",captain,state).success)
|
||||||
|
assert(not event.complete and captain.credits == 0)
|
||||||
|
assert(prize_before == var_to_str(event.prize))
|
||||||
|
assert(not EVENTS.resolve(event,"forged",captain,state).success)
|
||||||
|
event = fresh("rescue")
|
||||||
|
captain.parts = 4
|
||||||
|
state.cargo_parts = 1
|
||||||
|
assert(not EVENTS.resolve(event,"rescue",captain,state).success)
|
||||||
|
assert(captain.parts == 4 and state.cargo_parts == 1 and not event.complete)
|
||||||
|
state.cargo_parts = 3
|
||||||
|
event.rolls = [1.0,1.0,1.0]
|
||||||
|
var hp_before: float = state.lander_hp
|
||||||
|
var result := EVENTS.resolve(event,"rescue",captain,state)
|
||||||
|
assert(not result.success and event.complete and state.lander_hp < hp_before)
|
||||||
|
assert(captain.parts == 1 and state.cargo_parts == 0)
|
||||||
|
# A prototype pays the base even when the test fails.
|
||||||
|
event = fresh("prototype")
|
||||||
|
event.rolls = [1.0,1.0,1.0]
|
||||||
|
assert(not EVENTS.resolve(event,"test",captain,state).success)
|
||||||
|
assert(state.cargo_credits == 35*GameState.system_level and state.cargo_modules.is_empty())
|
||||||
|
# Later failed rescue stages preserve earlier rewards; stop is terminal.
|
||||||
|
event = fresh("evacuation")
|
||||||
|
event.rolls = [0.0,1.0,1.0]
|
||||||
|
assert(EVENTS.resolve(event,"help",captain,state).success)
|
||||||
|
var first_payment: int = state.cargo_credits
|
||||||
|
assert(not EVENTS.resolve(event,"help",captain,state).success)
|
||||||
|
assert(event.complete and state.cargo_credits == first_payment)
|
||||||
|
event = fresh("evacuation")
|
||||||
|
event.rolls = [0.0,0.0,0.0]
|
||||||
|
EVENTS.resolve(event,"help",captain,state)
|
||||||
|
first_payment = state.cargo_credits
|
||||||
|
assert(EVENTS.resolve(event,"stop",captain,state).success)
|
||||||
|
assert(event.complete and state.cargo_credits == first_payment)
|
||||||
|
# Repair clamps correctly and is unavailable at full HP.
|
||||||
|
event = fresh("service")
|
||||||
|
state.lander_hp = 97
|
||||||
|
assert(EVENTS.resolve(event,"repair",captain,state).success)
|
||||||
|
assert(state.lander_hp == 100)
|
||||||
|
event = fresh("service")
|
||||||
state.lander_hp = 100
|
state.lander_hp = 100
|
||||||
state.begin_visit(profile)
|
assert(EVENTS.options(event,captain,state).is_empty())
|
||||||
state.buy_engineer_offer(captain,{"kind":"engineer","complete":false},rng)
|
assert(not EVENTS.resolve(event,"repair",captain,state).success)
|
||||||
state.damage(100)
|
# Purchased rockets stay temporary, use the shop discount, and transfer once.
|
||||||
|
event = fresh("trader")
|
||||||
|
assert(event.missile_count >= 3 and event.missile_count <= 6)
|
||||||
|
assert(event.missile_price < event.missile_normal_price)
|
||||||
|
var expected: int = event.missile_count
|
||||||
|
var inventory := InventoryStub.new()
|
||||||
|
assert(EVENTS.resolve(event,"missiles",captain,state).success)
|
||||||
|
assert(state.cargo_missiles == expected and inventory.missile_ammunition == 2)
|
||||||
state.finish_visit(captain,true)
|
state.finish_visit(captain,true)
|
||||||
assert(captain.credits == 0 and state.delivered_modules.size() == 1)
|
state.deliver_modules(inventory)
|
||||||
GameState.system_level = before_level
|
assert(inventory.missile_ammunition == 2+expected)
|
||||||
|
state.deliver_modules(inventory)
|
||||||
|
assert(inventory.missile_ammunition == 2+expected)
|
||||||
|
# Already-owned goods survive destruction; all new cargo is lost.
|
||||||
|
event = fresh("trader")
|
||||||
|
EVENTS.resolve(event,"missiles",captain,state)
|
||||||
|
state.cargo_credits = 123
|
||||||
|
state.cargo_parts = 7
|
||||||
|
state.cargo_modules.append(event.module)
|
||||||
|
var committed_credits: int = captain.credits
|
||||||
|
state.damage(100)
|
||||||
|
assert(not state.finish_visit(captain,true).success)
|
||||||
|
assert(captain.credits == committed_credits and captain.parts == 100)
|
||||||
|
assert(state.delivered_missiles == 0 and state.delivered_modules.is_empty())
|
||||||
|
assert(state.cargo_credits == 0 and state.cargo_parts == 0 and state.cargo_missiles == 0)
|
||||||
|
# Successful payout is idempotent and never stores parts as minerals.
|
||||||
|
event = fresh("safe_bet")
|
||||||
|
EVENTS.resolve(event,"safe",captain,state)
|
||||||
|
state.cargo_credits = 123
|
||||||
|
committed_credits = captain.credits
|
||||||
|
state.finish_visit(captain,true)
|
||||||
|
state.finish_visit(captain,true)
|
||||||
|
assert(captain.parts == 106 and captain.credits == committed_credits+123)
|
||||||
|
assert(not captain.minerals.has("parts") and not captain.minerals.has("missile"))
|
||||||
|
inventory.free()
|
||||||
|
# Event modules use normal rarity rolls and a strictly decreasing biome level tail.
|
||||||
|
var bonus_rng := RandomNumberGenerator.new()
|
||||||
|
bonus_rng.seed = 727373
|
||||||
|
var histogram := PackedInt32Array()
|
||||||
|
histogram.resize(11)
|
||||||
|
for index in 100000:
|
||||||
|
var bonus := EVENTS.roll_module_bonus(bonus_rng,{"difficulty":1.0})
|
||||||
|
assert(bonus >= 2 and bonus <= 10)
|
||||||
|
histogram[bonus] += 1
|
||||||
|
for index in range(2,10):
|
||||||
|
assert(histogram[index] > histogram[index+1])
|
||||||
|
assert(histogram[0] == 0 and histogram[1] == 0)
|
||||||
|
assert(histogram[2] > 34000 and histogram[2] < 37000)
|
||||||
|
assert(histogram[10] > 950 and histogram[10] < 1350)
|
||||||
|
var rarities := {}
|
||||||
|
for index in 1000:
|
||||||
|
var item := EVENTS._module(bonus_rng,32,{"difficulty":1.0})
|
||||||
|
assert(item.item_level >= 34 and item.item_level <= 42)
|
||||||
|
assert(item.rarity >= 0 and item.rarity <= 4)
|
||||||
|
rarities[item.rarity] = true
|
||||||
|
assert(rarities.size() == 5)
|
||||||
|
assert(EVENTS._module(bonus_rng,1000,{"difficulty":1.0}).item_level == 1000)
|
||||||
|
print("MODULE LOOT PASS: normal through legendary rarity, hard biome level +2..+10, decreasing exponential weights; histogram=",histogram)
|
||||||
|
# 50% roll, one marker maximum, all ten kinds on usable terrain.
|
||||||
|
var data := GENERATOR.generate(200,profile)
|
||||||
|
data.trees = []
|
||||||
|
data.rocks = []
|
||||||
|
data.mountains = []
|
||||||
|
data.pois = []
|
||||||
|
data.heights.fill(0.5)
|
||||||
|
var counts := {}
|
||||||
var encounters := 0
|
var encounters := 0
|
||||||
for index in 30:
|
var rng := RandomNumberGenerator.new()
|
||||||
var data: Dictionary = GENERATOR.generate(200+index,profile)
|
for index in 1000:
|
||||||
assert(not data.has("river") and not data.has("rivers"))
|
data.seed = index
|
||||||
assert(data.structures.size() <= 5)
|
rng.seed = index
|
||||||
|
SCENERY.generate(data,rng)
|
||||||
assert(data.events.size() <= 1)
|
assert(data.events.size() <= 1)
|
||||||
encounters += data.events.size()
|
encounters += data.events.size()
|
||||||
for structure in data.structures:
|
for spawned in data.events:
|
||||||
var at: Vector2 = structure.position
|
assert(EVENTS.KINDS.has(spawned.kind))
|
||||||
for offset in [Vector2.ZERO,Vector2.UP,Vector2.DOWN,Vector2.LEFT,Vector2.RIGHT]:
|
counts[spawned.kind] = int(counts.get(spawned.kind,0))+1
|
||||||
var point: Vector2 = at + offset*float(structure.size)*0.55
|
assert(encounters > 450 and encounters < 550 and counts.size() == 10)
|
||||||
var x := clampi(int((point.x+1920)/40),0,95)
|
print("SURFACE EVENTS PASS: all 10 stories/actions, fixed offers, costs, discounts, scan stages, success/failure, evacuation, repair, safe return, destruction, one-time delivery. Spawn count / 1000: ",encounters,"; kinds: ",counts)
|
||||||
var y := clampi(int((point.y+1920)/40),0,95)
|
|
||||||
assert(data.heights[y*96+x] >= 0.32)
|
|
||||||
assert(encounters > 3 and encounters < 23)
|
|
||||||
print("SURFACE EVENTS PASS: no rivers, dry scenery placement, occasional events, exponential levels, price, insufficient funds, one purchase and expedition cargo")
|
|
||||||
print("Engineer bonuses / 40000 rolls: +0=",histogram[0]," +10=",histogram[10]," +20=",histogram[20])
|
|
||||||
get_tree().quit()
|
get_tree().quit()
|
||||||
|
|||||||
89
tests/previews/planet_threats_preview.gd
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
extends Node
|
||||||
|
const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
|
||||||
|
const EVENTS := preload("res://scripts/planets/surface_events.gd")
|
||||||
|
var sheet: Image
|
||||||
|
var panel_index := 0
|
||||||
|
|
||||||
|
func settle() -> void:
|
||||||
|
for frame in 5:
|
||||||
|
await get_tree().process_frame
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
|
||||||
|
func capture(name: String) -> void:
|
||||||
|
await settle()
|
||||||
|
var screenshot := get_viewport().get_texture().get_image()
|
||||||
|
screenshot.save_png("res://artifacts/threat_%s.png" % name)
|
||||||
|
screenshot.convert(Image.FORMAT_RGBA8)
|
||||||
|
screenshot.resize(640,360,Image.INTERPOLATE_LANCZOS)
|
||||||
|
sheet.blit_rect(screenshot,Rect2i(0,0,640,360),Vector2i(panel_index%2*640,panel_index/2*360))
|
||||||
|
panel_index += 1
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
GameState.system_seed = 8428159
|
||||||
|
var game := preload("res://scenes/game/game.tscn").instantiate()
|
||||||
|
add_child(game)
|
||||||
|
await settle()
|
||||||
|
var manager := game.get_node("PlanetExpeditions")
|
||||||
|
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||||
|
enemy.position = Vector2(9999,9999)
|
||||||
|
var planet: Node2D = game.protected_planet
|
||||||
|
game.player.position = planet.position
|
||||||
|
game.player.landing_combat_lock = 0
|
||||||
|
var profile: Dictionary = planet.planet_profile
|
||||||
|
sheet = Image.create(1280,1080,false,Image.FORMAT_RGBA8)
|
||||||
|
for scenario in [{"name":"scan_safe","rank":1,"oxygen":98,"temperature":30,"flora":0,"difficulty":0.0},{"name":"scan_150","rank":25,"oxygen":0,"temperature":100,"flora":50,"difficulty":1.0},{"name":"scan_l50","rank":50,"oxygen":0,"temperature":100,"flora":50,"difficulty":1.0}]:
|
||||||
|
profile.oxygen = scenario.oxygen
|
||||||
|
profile.temperature = scenario.temperature
|
||||||
|
profile.flora = scenario.flora
|
||||||
|
profile.difficulty = scenario.difficulty
|
||||||
|
profile.easy = scenario.difficulty == 0
|
||||||
|
profile.landing_damage = 5 if profile.easy else 20
|
||||||
|
for gear in GameState.captain.gear_items:
|
||||||
|
gear.level = scenario.rank
|
||||||
|
GameState.captain.emit_changed()
|
||||||
|
manager.scan_nearby()
|
||||||
|
await capture(scenario.name)
|
||||||
|
assert(manager._text.text.contains("ŚREDNIA SZANSA POI"))
|
||||||
|
assert(get_viewport().get_visible_rect().encloses(manager._prompt.get_global_rect()))
|
||||||
|
if scenario.rank == 1 or scenario.rank == 50:
|
||||||
|
assert(manager._text.text.contains("66.7%"))
|
||||||
|
else:
|
||||||
|
assert(manager._text.text.contains("6.7%"))
|
||||||
|
for gear in GameState.captain.gear_items:
|
||||||
|
gear.level = 25
|
||||||
|
GameState.captain.emit_changed()
|
||||||
|
manager.land_nearby()
|
||||||
|
var surface: Control = manager.screen
|
||||||
|
assert(surface != null)
|
||||||
|
surface.set_process(false)
|
||||||
|
surface.set_physics_process(false)
|
||||||
|
var poi: Dictionary = surface.world.data.pois[0]
|
||||||
|
poi.stage = 2
|
||||||
|
surface._poi = poi
|
||||||
|
surface._show_poi()
|
||||||
|
var blocked_poi := false
|
||||||
|
for child in surface._choices.get_children():
|
||||||
|
if child is Button and child.text.contains("0.00% szans"):
|
||||||
|
assert(child.disabled)
|
||||||
|
blocked_poi = true
|
||||||
|
assert(blocked_poi)
|
||||||
|
await capture("zero_chance_poi")
|
||||||
|
var event := {"kind":"evacuation","seed":77,"position":Vector2.ZERO,"complete":false}
|
||||||
|
EVENTS.prepare(event,GameState.captain,surface.progress)
|
||||||
|
event.stage = 2
|
||||||
|
surface._show_event(event)
|
||||||
|
var blocked_event := false
|
||||||
|
for child in surface._choices.get_children():
|
||||||
|
if child is Button and child.text.begins_with("Etap 3/3"):
|
||||||
|
assert(child.disabled)
|
||||||
|
blocked_event = true
|
||||||
|
assert(blocked_event)
|
||||||
|
await capture("zero_chance_event")
|
||||||
|
surface._show_event({"kind":"rescue","seed":78,"position":Vector2.ZERO,"complete":false})
|
||||||
|
await capture("rescue_150")
|
||||||
|
sheet.save_png("res://artifacts/threat_system_overview.png")
|
||||||
|
manager.finish_surface(true)
|
||||||
|
game.queue_free()
|
||||||
|
await get_tree().process_frame
|
||||||
|
print("THREAT UI PASS: three prelanding scans, visible averaged odds, L50 mitigation, on-screen prompt, disabled zero-chance POI and event, rescue threat")
|
||||||
|
get_tree().quit()
|
||||||
4
tests/previews/planet_threats_preview.tscn
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://tests/previews/planet_threats_preview.gd" id="1"]
|
||||||
|
[node name="PlanetThreatsPreview" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
40
tests/previews/surface_details_test.gd
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const GENERATOR := preload("res://scripts/planets/surface_generator.gd")
|
||||||
|
const DETAILS := preload("res://scripts/planets/surface_details.gd")
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var total := 0
|
||||||
|
var fallen := 0
|
||||||
|
for biome in 6:
|
||||||
|
var profile := {"temperature":75 if biome == 4 else (-35 if biome == 5 else 20)}
|
||||||
|
var data := GENERATOR.generate(200+biome, profile)
|
||||||
|
assert(int(data.landscape.index) == biome)
|
||||||
|
assert(data.pois.size() == 8 and data.structures.size() <= 5)
|
||||||
|
var original := var_to_str(data.details)
|
||||||
|
DETAILS.generate(data)
|
||||||
|
assert(original == var_to_str(data.details), "Decoration generation must be reproducible")
|
||||||
|
assert(not data.details.is_empty())
|
||||||
|
for detail in data.details:
|
||||||
|
assert(detail.position.length() > 210)
|
||||||
|
assert(DETAILS._height(data, detail.position) >= 0.315)
|
||||||
|
for poi in data.pois:
|
||||||
|
assert(detail.position.distance_to(poi.position) > 135)
|
||||||
|
for event in data.events:
|
||||||
|
assert(detail.position.distance_to(event.position) > 115)
|
||||||
|
total += data.details.size()
|
||||||
|
print("Biome ",biome,": ",data.details.size()," details, ",data.trees.size()," trees")
|
||||||
|
# Exercise rare-tree rolls on one fixed map, without baking terrain repeatedly.
|
||||||
|
for sample_index in 40:
|
||||||
|
data.seed = 10000+sample_index
|
||||||
|
DETAILS.generate(data)
|
||||||
|
var count := 0
|
||||||
|
for detail in data.details:
|
||||||
|
if int(detail.variant) == 12:
|
||||||
|
count += 1
|
||||||
|
assert(count <= 1)
|
||||||
|
if biome >= 4:
|
||||||
|
assert(count == 0)
|
||||||
|
fallen += count
|
||||||
|
assert(fallen > 0 and fallen < 32)
|
||||||
|
print("NATURE PASS: six biomes; ",total," details; reproducible generation; clear POI/event/landing areas; ",fallen," fallen trees across 240 samples")
|
||||||
|
quit()
|
||||||
1
tests/previews/surface_details_test.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://dwdckw6s07msc
|
||||||
92
tests/previews/surface_events_preview.gd
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
extends Node
|
||||||
|
const EVENTS := preload("res://scripts/planets/surface_events.gd")
|
||||||
|
|
||||||
|
func settle() -> void:
|
||||||
|
for frame in 4:
|
||||||
|
await get_tree().process_frame
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
GameState.system_level = 3
|
||||||
|
GameState.captain.credits = 100000
|
||||||
|
GameState.captain.parts = 100
|
||||||
|
for key in ["iron","copper","nickel","silicon","tungsten","mercury","sulfur","uranium"]:
|
||||||
|
GameState.captain.minerals[key] = 100
|
||||||
|
var profile: Dictionary = preload("res://scripts/planets/planet_profiles.gd").generate(777,1,"Spotkania na powierzchni")
|
||||||
|
var hard := "--hard" in OS.get_cmdline_user_args()
|
||||||
|
var prefix := "hard_" if hard else ""
|
||||||
|
profile.temperature = 100 if hard else 20
|
||||||
|
profile.oxygen = 0 if hard else 95
|
||||||
|
profile.flora = 50 if hard else 0
|
||||||
|
profile.difficulty = 1.0 if hard else 0.0
|
||||||
|
profile.easy = not hard
|
||||||
|
var progress: Resource = GameState.captain.expedition
|
||||||
|
assert(progress.begin_visit(profile))
|
||||||
|
progress.visit_seed = 0
|
||||||
|
var surface := Control.new()
|
||||||
|
surface.set_script(preload("res://scripts/planets/surface_screen.gd"))
|
||||||
|
surface.captain = GameState.captain
|
||||||
|
surface.progress = progress
|
||||||
|
add_child(surface)
|
||||||
|
surface.set_process(false)
|
||||||
|
surface.set_physics_process(false)
|
||||||
|
assert(surface.world.data.events.size() == 1)
|
||||||
|
var event: Dictionary = surface.world.data.events[0]
|
||||||
|
var at: Vector2 = event.position
|
||||||
|
surface.lander.position = at+Vector2(-100,0)
|
||||||
|
surface._previous_position = surface.lander.position
|
||||||
|
surface.camera.position = surface.lander.position
|
||||||
|
surface.camera.reset_smoothing()
|
||||||
|
var sheet := Image.create(1280,1800,false,Image.FORMAT_RGBA8)
|
||||||
|
for index in EVENTS.KINDS.size():
|
||||||
|
event.clear()
|
||||||
|
event.merge({"kind":EVENTS.KINDS[index],"seed":101+index,"position":at,"complete":false,"discovered":true})
|
||||||
|
surface._dialog.hide()
|
||||||
|
surface._event_armed = true
|
||||||
|
surface._check_events()
|
||||||
|
assert(surface._dialog.visible)
|
||||||
|
var snapshot := var_to_str(EVENTS.options(event,GameState.captain,progress))
|
||||||
|
surface._dialog.hide()
|
||||||
|
surface._show_event(event)
|
||||||
|
assert(snapshot == var_to_str(EVENTS.options(event,GameState.captain,progress)))
|
||||||
|
await settle()
|
||||||
|
assert(surface._dialog.position.y+surface._dialog.size.y <= get_viewport().get_visible_rect().size.y)
|
||||||
|
var screenshot := get_viewport().get_texture().get_image()
|
||||||
|
screenshot.save_png("res://artifacts/%sevent_%s.png" % [prefix,event.kind])
|
||||||
|
screenshot.convert(Image.FORMAT_RGBA8)
|
||||||
|
screenshot.resize(640,360,Image.INTERPOLATE_LANCZOS)
|
||||||
|
sheet.blit_rect(screenshot,Rect2i(0,0,640,360),Vector2i(index%2*640,index/2*360))
|
||||||
|
if event.kind == "trader":
|
||||||
|
var pressed := false
|
||||||
|
for child in surface._choices.get_children():
|
||||||
|
if child is Button and child.text.begins_with("Kup ") and child.text.contains("rakiet"):
|
||||||
|
child.pressed.emit()
|
||||||
|
pressed = true
|
||||||
|
break
|
||||||
|
assert(pressed and event.complete and progress.cargo_missiles == event.missile_count)
|
||||||
|
if event.kind == "scan_cache":
|
||||||
|
surface._event_action("scan")
|
||||||
|
assert(event.scanned and not event.complete)
|
||||||
|
await settle()
|
||||||
|
get_viewport().get_texture().get_image().save_png("res://artifacts/%sevent_scan_result.png" % prefix)
|
||||||
|
if event.kind == "evacuation":
|
||||||
|
event.rolls = [0.0,0.0,0.0]
|
||||||
|
surface._event_action("help")
|
||||||
|
assert(event.stage == 1 and not event.complete)
|
||||||
|
await settle()
|
||||||
|
get_viewport().get_texture().get_image().save_png("res://artifacts/%sevent_evacuation_stage2.png" % prefix)
|
||||||
|
sheet.save_png("res://artifacts/%sevents_overview.png" % prefix)
|
||||||
|
# Lethal event damage follows the same destruction path as a failed POI.
|
||||||
|
event.clear()
|
||||||
|
event.merge({"kind":"rescue","seed":333,"position":at,"complete":false})
|
||||||
|
EVENTS.prepare(event,GameState.captain,progress)
|
||||||
|
event.rolls = [1.0,1.0,1.0]
|
||||||
|
progress.lander_hp = 1
|
||||||
|
var ended := []
|
||||||
|
surface.finished.connect(func(success: bool): ended.append(success))
|
||||||
|
await surface._event_action("rescue")
|
||||||
|
assert(surface._ending and not surface.lander.visible and ended == [false])
|
||||||
|
progress.finish_visit(GameState.captain,false)
|
||||||
|
surface.queue_free()
|
||||||
|
print("EVENT UI PASS: ten dialogs, auto-open, fixed reopened offers, purchase button, scan, evacuation, bounds, lethal damage")
|
||||||
|
get_tree().quit()
|
||||||
4
tests/previews/surface_events_preview.tscn
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://tests/previews/surface_events_preview.gd" id="1"]
|
||||||
|
[node name="SurfaceEventsPreview" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
60
tests/previews/surface_landmarks_preview.gd
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
extends Node
|
||||||
|
const GENERATOR := preload("res://scripts/planets/surface_generator.gd")
|
||||||
|
const TITLES := {"fissure":"Spękana ziemia", "meteor_crater":"Ślad meteorytu", "abandoned_hamlet":"Opuszczona osada", "weathered_stones":"Zwietrzałe kamienie", "dry_hollow":"Wyschnięte oczko"}
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var examples := {}
|
||||||
|
var profile: Dictionary = preload("res://scripts/planets/planet_profiles.gd").generate(99, 0, "Podgląd")
|
||||||
|
profile.temperature = 20
|
||||||
|
profile.easy = true
|
||||||
|
profile.oxygen = 90
|
||||||
|
profile.flora = 0
|
||||||
|
for seed_value in 180:
|
||||||
|
var data := GENERATOR.generate(seed_value, profile)
|
||||||
|
for landmark in data.landmarks:
|
||||||
|
if not examples.has(landmark.kind):
|
||||||
|
examples[landmark.kind] = {"seed":seed_value, "position":landmark.position}
|
||||||
|
if examples.size() == TITLES.size():
|
||||||
|
break
|
||||||
|
assert(examples.size() == TITLES.size())
|
||||||
|
var sheet := Image.create(1280, 1080, false, Image.FORMAT_RGBA8)
|
||||||
|
var progress: Resource = GameState.captain.expedition
|
||||||
|
var kinds: Array = TITLES.keys()
|
||||||
|
kinds.append("scale")
|
||||||
|
for index in kinds.size():
|
||||||
|
var kind: String = kinds[index]
|
||||||
|
profile.name = "Skala budynków i drzew" if kind == "scale" else TITLES[kind]
|
||||||
|
progress.lander_hp = 100
|
||||||
|
assert(progress.begin_visit(profile))
|
||||||
|
progress.visit_seed = 200 if kind == "scale" else int(examples[kind].seed)
|
||||||
|
var surface := Control.new()
|
||||||
|
surface.set_script(preload("res://scripts/planets/surface_screen.gd"))
|
||||||
|
surface.captain = GameState.captain
|
||||||
|
surface.progress = progress
|
||||||
|
add_child(surface)
|
||||||
|
surface.set_process(false)
|
||||||
|
surface.set_physics_process(false)
|
||||||
|
var target: Vector2 = (surface.world.data.structures[0].position if not surface.world.data.structures.is_empty() else Vector2.ZERO) if kind == "scale" else examples[kind].position
|
||||||
|
surface.lander.position = target+Vector2(-120, 80)
|
||||||
|
surface.lander.reset_physics_interpolation()
|
||||||
|
surface.camera.position = target
|
||||||
|
surface.camera.reset_smoothing()
|
||||||
|
surface.world.refresh_pois(target)
|
||||||
|
for spec in preload("res://scripts/planets/surface_landmarks.gd").SPECS:
|
||||||
|
var texture: Texture2D = load("res://assets/planets/landmarks/%s.png" % spec.kind)
|
||||||
|
assert(texture.get_image().has_mipmaps())
|
||||||
|
assert(texture.get_image().get_pixel(0,0).a < 0.01)
|
||||||
|
for frame in 5:
|
||||||
|
await get_tree().process_frame
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
var screenshot := get_viewport().get_texture().get_image()
|
||||||
|
screenshot.save_png("res://artifacts/landmark_%s.png" % kind)
|
||||||
|
screenshot.convert(Image.FORMAT_RGBA8)
|
||||||
|
screenshot.resize(640, 360, Image.INTERPOLATE_LANCZOS)
|
||||||
|
sheet.blit_rect(screenshot, Rect2i(0,0,640,360), Vector2i(index%2*640,index/2*360))
|
||||||
|
progress.finish_visit(GameState.captain, true)
|
||||||
|
surface.queue_free()
|
||||||
|
await get_tree().process_frame
|
||||||
|
sheet.save_png("res://artifacts/landmarks_overview.png")
|
||||||
|
print("LANDMARK PREVIEW PASS: five new props, smaller buildings and trees, transparent textures, mipmaps")
|
||||||
|
get_tree().quit()
|
||||||
1
tests/previews/surface_landmarks_preview.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://dco7gctr0b0vq
|
||||||
4
tests/previews/surface_landmarks_preview.tscn
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://tests/previews/surface_landmarks_preview.gd" id="1"]
|
||||||
|
[node name="SurfaceLandmarksPreview" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
42
tests/previews/surface_landmarks_test.gd
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const GENERATOR := preload("res://scripts/planets/surface_generator.gd")
|
||||||
|
const LANDMARKS := preload("res://scripts/planets/surface_landmarks.gd")
|
||||||
|
const DETAILS := preload("res://scripts/planets/surface_details.gd")
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var counts := {}
|
||||||
|
for biome in 6:
|
||||||
|
var base := GENERATOR.generate(200+biome, {"temperature":75 if biome == 4 else (-35 if biome == 5 else 20)})
|
||||||
|
for tree in base.trees:
|
||||||
|
assert(float(tree.size) <= 92.0)
|
||||||
|
for sample_index in 40:
|
||||||
|
var data: Dictionary = base.duplicate(true)
|
||||||
|
data.seed = 20000+sample_index*6+biome
|
||||||
|
LANDMARKS.generate(data)
|
||||||
|
var snapshot := var_to_str(data.landmarks)
|
||||||
|
LANDMARKS.generate(data)
|
||||||
|
assert(snapshot == var_to_str(data.landmarks))
|
||||||
|
var hamlets := 0
|
||||||
|
var craters := 0
|
||||||
|
for landmark in data.landmarks:
|
||||||
|
counts[landmark.kind] = int(counts.get(landmark.kind,0))+1
|
||||||
|
hamlets += int(landmark.kind == "abandoned_hamlet")
|
||||||
|
craters += int(landmark.kind == "meteor_crater")
|
||||||
|
var radius := float(landmark.size)*0.55
|
||||||
|
assert(landmark.position.length() > radius+210)
|
||||||
|
for offset in [Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN]:
|
||||||
|
assert(DETAILS._height(data, landmark.position+offset*radius) > 0.315)
|
||||||
|
for poi in data.pois:
|
||||||
|
assert(landmark.position.distance_to(poi.position) > 135+radius)
|
||||||
|
for event in data.events:
|
||||||
|
assert(landmark.position.distance_to(event.position) > 115+radius)
|
||||||
|
for other in data.landmarks:
|
||||||
|
if other != landmark:
|
||||||
|
assert(landmark.position.distance_to(other.position) > radius+float(other.size)*0.55)
|
||||||
|
for tree in data.trees:
|
||||||
|
assert(tree.position.distance_to(landmark.position) > float(landmark.size)*0.48+float(tree.size)*0.4)
|
||||||
|
assert(hamlets <= 1 and craters <= 1)
|
||||||
|
assert(counts.size() == 5)
|
||||||
|
assert(int(counts.abandoned_hamlet) > 0 and int(counts.abandoned_hamlet) < 35)
|
||||||
|
print("LANDMARK PASS: 240 maps, dry footprints, separated landmarks, clear interactions, reproducibility, smaller trees. Counts: ",counts)
|
||||||
|
quit()
|
||||||
1
tests/previews/surface_landmarks_test.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://dc86vec1784n2
|
||||||