Lądowanie

This commit is contained in:
Tomasz Boruc 2026-10-18 08:43:54 +02:00
parent f8c9fe9197
commit e055a5510a
101 changed files with 2266 additions and 172 deletions

BIN
artifacts/event_barter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1021 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
artifacts/event_rescue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
artifacts/event_service.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 KiB

BIN
artifacts/event_trader.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 979 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View 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

View 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."
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View 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

View 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"
]
}

View File

@ -528,3 +528,203 @@ Aktualizacja materiałów powierzchni (18.10.2026):
umieszczenia budynków na mapę, brak rzek i zoom powierzchni 0,8.
- Nowe tekstury wygenerowano wbudowanym imagegen. Pliki i komplet promptów:
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.

View File

@ -197,7 +197,11 @@ func finish_surface(success: bool) -> void:
var result: Dictionary = progress.finish_visit(GameState.captain,success)
progress.deliver_modules(player.get_node("ShipInventory"))
_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.")
if not progress.delivered_modules.is_empty():
_message += "\nBrak miejsca: %d modułów czeka we frachtowcu." % progress.delivered_modules.size()

View File

@ -46,6 +46,7 @@ static func button(parent: Node, text: String, action: Callable) -> Button:
static func material_text(materials: Dictionary) -> String:
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({"parts":"Części", "missile":"Rakiety", "credits":"Kredyty"})
var pieces: PackedStringArray = []
for key in materials:
pieces.append("%s: %d" % [names.get(key, key), materials[key]])

View File

@ -92,7 +92,7 @@ func _refresh() -> void:
UI.label(_content, "Jeśli ładownia jest pełna, moduły czekają na odbiór.", 13)
elif _tab == "workshop":
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:
var card := UI.panel(_content)
var row := HBoxContainer.new()

View File

@ -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 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 o := maxf(0.0, 80.0 - oxygen) / 80.0
var t := maxf(0.0, maxf(-10.0 - temperature, temperature - 40.0)) / 60.0
var f := float(flora) / 50.0
var difficulty := clampf((o + t + f) / 3.0, 0, 1)
var raw := raw_threats({"oxygen":oxygen,"temperature":temperature,"flora":flora})
var difficulty := (float(raw.oxygen)+float(raw.temperature)+float(raw.biology))/300.0
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 penalties := [
maxf(0, 80.0 - float(profile.oxygen)) / 80.0,
maxf(0, maxf(-10.0 - float(profile.temperature), float(profile.temperature) - 40.0)) / 60.0,
float(profile.flora) / 50.0]
var environment := 0.0
var equipment_bonus := 0.0
var raw := raw_threats(profile)
var effective := {}
var raw_total := 0.0
var total := 0.0
for index in 3:
var rank: int = gear.level(captain, gear.KINDS[index])
var protection := minf(0.8, maxf(0, rank - 1) * 0.06)
environment += penalties[index] * (1.0 - protection) / 3.0
equipment_bonus += maxf(0, rank - 1) * 0.004
var base: float = [0.90, 0.60, 0.35][clampi(stage, 0, 2)]
return clampf(base - environment * 0.35 + minf(0.12, equipment_bonus), 0.08, 0.98)
var key: String = THREAT_KEYS[index]
var protection := minf(100.0,2.0*maxi(0,gear.level(captain,gear.KINDS[index])))
effective[key] = maxf(0.0,float(raw[key])-protection)
raw_total += float(raw[key])
total += float(effective[key])
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:
if bool(profile.easy):
@ -58,12 +83,11 @@ static func biome_name(profile: Dictionary) -> String:
return "Dzika biosfera"
static func scan_description(profile: Dictionary, captain: Resource) -> String:
var oxygen := int(profile.oxygen)
var temperature := int(profile.temperature)
var flora := int(profile.flora)
var breathing := "dogodne oddychanie" if oxygen > 80 else "system oddychania poprawia szanse"
var climate := "łagodny klimat" if temperature >= -10 and temperature <= 40 else "skafander ogranicza ryzyko"
var biology := "brak zagrożeń biologicznych" if flora == 0 else "broń ręczna ogranicza zagrożenia"
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" % [
profile.name,biome_name(profile),oxygen,breathing,temperature,climate,flora,biology,
profile.landing_damage,ceili(captain.expedition.lander_hp),roundi(success_chance(profile,captain,0)*100)]
var report := threat_report(profile,captain)
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%%" % [
profile.name,biome_name(profile),profile.oxygen,report.raw.oxygen,report.effective.oxygen,
profile.temperature,report.raw.temperature,report.effective.temperature,
profile.flora,report.raw.biology,report.effective.biology,
report.total,report.raw_total,average_success_chance(profile,captain)*100.0,
success_chance(profile,captain,0)*100.0,success_chance(profile,captain,1)*100.0,success_chance(profile,captain,2)*100.0,
profile.landing_damage,ceili(captain.expedition.lander_hp)]

View File

@ -1,11 +1,16 @@
extends Resource
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")
@export var lander_hp := 100.0
@export var visits: Dictionary = {}
@export var scans: Dictionary = {}
@export var delivered_modules: Array[Resource] = []
@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 cargo: Dictionary = {}
var cargo_modules: Array[Resource] = []
@ -37,6 +42,9 @@ func begin_visit(profile: Dictionary) -> bool:
var count := int(visits.get(profile.id, 0)) + 1
visits[profile.id] = count
visit_seed = int(profile.seed) ^ (count * 7919)
cargo_credits = 0
cargo_parts = 0
cargo_missiles = 0
cargo.clear()
cargo_modules.clear()
cargo_records.clear()
@ -60,11 +68,17 @@ func resolve_poi(captain: Resource, poi: Dictionary, rng: RandomNumberGenerator)
return {}
var stage := int(poi.get("stage", 0))
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}
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:
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 pool: Array = PROFILES.MATERIALS if rng.randf() < 0.55 else ordinary
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:
var generator := preload("res://scripts/items/item_generator.gd")
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 = generator.generate_loot(level, 1 if difficulty > 0.5 or stage >= 2 else 0, definitions[rng.randi_range(0, definitions.size() - 1)])
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)
cargo_modules.append(item)
result.module = item
if str(poi.kind) == "ruins":
@ -97,14 +110,20 @@ func finish_visit(captain: Resource, success: bool) -> Dictionary:
if not active:
return last_result
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:
captain.credits += cargo_credits
captain.parts += cargo_parts
delivered_missiles += cargo_missiles
for key in cargo:
captain.minerals[key] = int(captain.minerals.get(key, 0)) + int(cargo[key])
delivered_modules.append_array(cargo_modules)
for key in cargo_records:
research_records[key] = int(research_records.get(key, 0)) + int(cargo_records[key])
active = false
cargo_credits = 0
cargo_parts = 0
cargo_missiles = 0
cargo.clear()
cargo_modules.clear()
cargo_records.clear()
@ -115,6 +134,11 @@ func finish_visit(captain: Resource, success: bool) -> Dictionary:
return result
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
for index in range(delivered_modules.size() - 1, -1, -1):
if inventory.add_item(delivered_modules[index]):
@ -123,35 +147,3 @@ func deliver_modules(inventory: Node) -> int:
emit_changed()
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}

View 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))))

View 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

View File

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

View 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}

View File

@ -40,7 +40,7 @@ static func generate(seed_value: int, profile: Dictionary) -> Dictionary:
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)})
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:
var point := Vector2(rng.randf_range(-1750,1750),rng.randf_range(-1750,1750))
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)
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_landmarks.gd").generate(data)
preload("res://scripts/planets/surface_details.gd").generate(data)
return data
static func terrain_texture(data: Dictionary) -> ImageTexture:

View 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

View File

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

View File

@ -1,4 +1,5 @@
extends RefCounted
const BUILDING_SCALE := 0.65
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}
@ -7,15 +8,18 @@ static func generate(data: Dictionary, rng: RandomNumberGenerator) -> void:
data.events = []
for index in rng.randi_range(2,5):
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)
if position == Vector2.INF:
continue
data.structures.append({"kind":kind,"position":position,"size":width,"rotation":rng.randf_range(-PI,PI)})
if rng.randf() < 0.4:
var position := find_site(data,rng,100)
var event_rng := RandomNumberGenerator.new()
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:
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()
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)

View File

@ -1,6 +1,8 @@
extends Control
signal finished(success: bool)
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")
var captain: Resource
var progress: Resource
@ -9,6 +11,7 @@ var lander: Sprite2D
var camera: Camera2D
var _hp: ProgressBar
var _cargo: Label
var _threat: Label
var _status: Label
var _dialog: PanelContainer
var _choices: VBoxContainer
@ -94,6 +97,7 @@ func _ready() -> void:
UI.label(column, "%s / TLEN %d%% / %d°C / FLORA %d" % [
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)
_threat = UI.label(column,"",13,Color("efca8a"))
_hp = ProgressBar.new()
_hp.custom_minimum_size = Vector2(0, 14)
_hp.show_percentage = false
@ -122,12 +126,18 @@ func _ready() -> void:
minimap.world = world
minimap.lander = lander
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.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.add_theme_constant_override("separation", 12)
_dialog.add_child(_choices)
_choices.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_choices.add_theme_constant_override("separation", 10)
scroll.add_child(_choices)
_layout_dialog()
_dialog.hide()
_update_hud()
@ -203,14 +213,22 @@ 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)
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
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():
var result := UI.label(_choices, message, 15, Color("#efca8a"))
result.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
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:
var chance: float = preload("res://scripts/planets/planet_profiles.gd").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)
UI.button(_choices, "Zakończ badanie tego miejsca", func(): _poi.complete = true; _dialog.hide())
var chance := PROFILES.success_chance(progress.active_profile,captain,_poi.stage)
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, "Wróć do lotu", func(): _dialog.hide())
func _attempt() -> void:
@ -219,6 +237,9 @@ func _attempt() -> void:
var result: Dictionary = progress.resolve_poi(captain, _poi, _rng)
if result.is_empty():
return
if result.get("blocked",false):
_show_poi(str(result.reason))
return
var message := ""
if result.success:
message = "SUKCES · " + UI.material_text(result.materials)
@ -231,22 +252,33 @@ func _attempt() -> void:
_update_hud()
_show_poi(message)
if progress.lander_hp <= 0:
_ending = true
_status.text = "LĄDOWNIK ZNISZCZONY — utracono łup wyprawy"
_dialog.hide()
var explosion := Node2D.new()
explosion.set_script(preload("res://scripts/game/player_destruction_effect.gd"))
explosion.configure(lander,Vector2.ZERO,camera)
lander.get_parent().add_child(explosion)
lander.hide()
await explosion.finished
finished.emit(false)
await _destroy_lander()
func _destroy_lander() -> void:
if _ending:
return
_ending = true
_status.text = "LĄDOWNIK ZNISZCZONY — utracono łup wyprawy"
_dialog.hide()
var explosion := Node2D.new()
explosion.set_script(preload("res://scripts/game/player_destruction_effect.gd"))
explosion.configure(lander,Vector2.ZERO,camera)
lander.get_parent().add_child(explosion)
lander.hide()
await explosion.finished
finished.emit(false)
func _update_hud() -> void:
_hp.value = progress.lander_hp
if not _ending:
_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()
func _return(success: bool) -> void:
@ -270,46 +302,84 @@ func _check_events() -> void:
_show_event(event)
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:
_active_event = event
EVENTS.prepare(event,captain,progress)
for child in _choices.get_children():
_choices.remove_child(child)
child.queue_free()
_choices.get_parent().set("scroll_vertical",0)
_dialog.show()
UI.label(_choices,"SPOTKANIE / INŻYNIER",23,UI.CYAN)
var text := "Znalazłeś miejscowego inżyniera. Za 100 kredytów sprzeda Ci moduł. Nie jesteś pewien, czy będzie coś warty."
_layout_dialog()
_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 result.get("success",false):
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
_event_text(str(result.get("message",result.get("reason",""))),Color("efca8a"))
if not event.complete:
UI.label(_choices,"Twoje kredyty: %d" % captain.credits,15,Color("efca8a"))
var buy := UI.button(_choices,"KUP NIEZNANY MODUŁ · 100 KREDYTÓW",_buy_engineer)
buy.disabled = captain.credits < 100
_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)
if event.kind == "scan_cache":
_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())
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())
func _buy_engineer() -> void:
func _event_action(action: String) -> void:
if _ending:
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)
world.refresh_pois(lander.position)
_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:
_layout_dialog()
if is_instance_valid(_surface_viewport):
_surface_viewport.size = Vector2i(Vector2(maxf(1,size.x),maxf(1,size.y))*SURFACE_RENDER_SCALE)

View File

@ -1,7 +1,10 @@
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")
var data: Dictionary
var terrain: ImageTexture
var _nature: Array[AtlasTexture] = []
var _tree: Texture2D
var _mountain: Texture2D
var _poi_sprites: Dictionary = {}
@ -29,18 +32,34 @@ func _ready() -> void:
material.set_shader_parameter("seed_offset",float(posmod(int(data.seed),10000)))
ground.material = material
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")
_mountain = load("res://assets/planets/mountain.png")
for poi in data.pois:
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.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
add_child(sprite)
_poi_sprites[poi.id] = sprite
var label := Label.new()
label.position = poi.position + Vector2(-120, 65)
label.position = poi.position + Vector2(-120, 46)
label.size.x = 240
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size", 13)
@ -50,6 +69,15 @@ func _ready() -> void:
label.z_index = 15
add_child(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:
var prop := Sprite2D.new()
prop.name = str(structure.kind).to_pascal_case()
@ -99,10 +127,13 @@ func refresh_pois(lander_position: Vector2) -> void:
func _draw() -> void:
if terrain == null:
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:
_draw_prop(_mountain, mountain.position, mountain.size, mountain.rotation, Color.WHITE)
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:
var shade := float(tree.shade)
var color: Color = Color(shade,shade,shade)*data.landscape.trees
@ -110,12 +141,14 @@ func _draw() -> void:
color = color.lerp(Color("d1e1da"), 0.4)
elif int(data.profile.temperature) > 40:
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.
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)
func _draw_prop(texture: Texture2D, point: Vector2, width: float, angle: float, color: Color) -> void:
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)

View File

@ -41,6 +41,8 @@ func refresh() -> void:
_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 "—")
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()
func _style() -> void:

View File

@ -124,7 +124,16 @@ func _ready() -> void:
manager.screen._poi = manager.screen.world.data.pois[0]
manager.screen._show_poi()
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)
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)
game.queue_free()
await get_tree().process_frame

View File

@ -9,7 +9,7 @@ func check_hint(expected: String) -> void:
await settle()
assert(manager._prompt.visible)
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()))
if manager._land_button.visible:
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)
manager.scan_nearby()
manager._process(0)
await check_hint("TEMPERATURA")
await check_hint("ŚREDNIA SZANSA POI")
if "--capture-preview" in OS.get_cmdline_user_args():
await RenderingServer.frame_post_draw
get_viewport().get_texture().get_image().save_png("res://artifacts/planet_scan_stats.png")

View 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()

View 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")

View 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()

View 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")

View File

@ -1,59 +1,187 @@
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 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:
var rng := RandomNumberGenerator.new()
rng.seed = 727373
var histogram := PackedInt32Array()
histogram.resize(21)
for index in 40000:
var bonus: int = PROGRESS.roll_engineer_bonus(rng)
assert(bonus >= 0 and bonus <= 20)
histogram[bonus] += 1
assert(histogram[0] > 9500 and histogram[0] < 10500)
assert(histogram[10] > 450 and histogram[10] < 700)
assert(histogram[20] > 0 and histogram[20] < 80)
var captain: Resource = preload("res://scripts/captain/captain_progress.gd").new()
var state: Resource = captain.expedition
var profile: Dictionary = preload("res://scripts/planets/planet_profiles.gd").generate(777,1,"Test")
state.begin_visit(profile)
var event := {"kind":"engineer","complete":false}
captain.credits = 99
assert(not state.buy_engineer_offer(captain,event,rng).success)
assert(captain.credits == 99 and state.cargo_modules.is_empty() and not event.complete)
captain.credits = 200
var before_level := GameState.system_level
GameState.system_level = 32
var result: Dictionary = state.buy_engineer_offer(captain,event,rng)
assert(result.success and captain.credits == 100 and event.complete)
assert(result.item.item_level >= 32 and result.item.item_level <= 52)
assert(state.cargo_modules.size() == 1)
assert(not state.buy_engineer_offer(captain,event,rng).success)
assert(captain.credits == 100 and state.cargo_modules.size() == 1)
state.finish_visit(captain,true)
assert(state.delivered_modules.size() == 1)
GameState.system_level = 3
profile = PROFILES.generate(777,1,"Test wydarzeń")
profile.difficulty = 0.0
profile.oxygen = 95
profile.temperature = 20
profile.flora = 0
for kind in EVENTS.KINDS:
var event := fresh(kind)
assert(not str(EVENTS.STORIES[kind]).is_empty())
var offers := EVENTS.options(event,captain,state)
assert(not offers.is_empty())
var snapshot := var_to_str(offers)
assert(snapshot == var_to_str(EVENTS.options(event,captain,state)))
event.rolls = [0.0,0.0,0.0]
var result := EVENTS.resolve(event,str(offers[0].id),captain,state)
assert(result.success)
if kind == "scan_cache":
assert(event.scanned and not event.complete)
var before: int = captain.credits
assert(not EVENTS.resolve(event,"scan",captain,state).success)
assert(captain.credits == before)
assert(EVENTS.resolve(event,"open",captain,state).success)
if kind == "evacuation":
assert(event.stage == 1 and not event.complete)
assert(EVENTS.resolve(event,"help",captain,state).success)
assert(EVENTS.resolve(event,"help",captain,state).success)
assert(event.complete)
var credits_before: int = captain.credits
var cargo_before := var_to_str([state.cargo,state.cargo_credits,state.cargo_parts,state.cargo_missiles,state.cargo_modules])
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.begin_visit(profile)
state.buy_engineer_offer(captain,{"kind":"engineer","complete":false},rng)
state.damage(100)
assert(EVENTS.options(event,captain,state).is_empty())
assert(not EVENTS.resolve(event,"repair",captain,state).success)
# 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)
assert(captain.credits == 0 and state.delivered_modules.size() == 1)
GameState.system_level = before_level
state.deliver_modules(inventory)
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
for index in 30:
var data: Dictionary = GENERATOR.generate(200+index,profile)
assert(not data.has("river") and not data.has("rivers"))
assert(data.structures.size() <= 5)
var rng := RandomNumberGenerator.new()
for index in 1000:
data.seed = index
rng.seed = index
SCENERY.generate(data,rng)
assert(data.events.size() <= 1)
encounters += data.events.size()
for structure in data.structures:
var at: Vector2 = structure.position
for offset in [Vector2.ZERO,Vector2.UP,Vector2.DOWN,Vector2.LEFT,Vector2.RIGHT]:
var point: Vector2 = at + offset*float(structure.size)*0.55
var x := clampi(int((point.x+1920)/40),0,95)
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])
for spawned in data.events:
assert(EVENTS.KINDS.has(spawned.kind))
counts[spawned.kind] = int(counts.get(spawned.kind,0))+1
assert(encounters > 450 and encounters < 550 and counts.size() == 10)
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)
get_tree().quit()

View 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()

View 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")

View 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()

View File

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

View 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()

View 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")

View 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()

View File

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

View 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")

View 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()

View File

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

Some files were not shown because too many files have changed in this diff Show More