diff --git a/artifacts/event_barter.png b/artifacts/event_barter.png new file mode 100644 index 0000000..ee85e68 Binary files /dev/null and b/artifacts/event_barter.png differ diff --git a/artifacts/event_blind_cache.png b/artifacts/event_blind_cache.png new file mode 100644 index 0000000..b439809 Binary files /dev/null and b/artifacts/event_blind_cache.png differ diff --git a/artifacts/event_collector.png b/artifacts/event_collector.png new file mode 100644 index 0000000..2141950 Binary files /dev/null and b/artifacts/event_collector.png differ diff --git a/artifacts/event_evacuation.png b/artifacts/event_evacuation.png new file mode 100644 index 0000000..cdf6873 Binary files /dev/null and b/artifacts/event_evacuation.png differ diff --git a/artifacts/event_evacuation_stage2.png b/artifacts/event_evacuation_stage2.png new file mode 100644 index 0000000..83704f0 Binary files /dev/null and b/artifacts/event_evacuation_stage2.png differ diff --git a/artifacts/event_prototype.png b/artifacts/event_prototype.png new file mode 100644 index 0000000..3c49aaf Binary files /dev/null and b/artifacts/event_prototype.png differ diff --git a/artifacts/event_rescue.png b/artifacts/event_rescue.png new file mode 100644 index 0000000..be9d4ac Binary files /dev/null and b/artifacts/event_rescue.png differ diff --git a/artifacts/event_safe_bet.png b/artifacts/event_safe_bet.png new file mode 100644 index 0000000..6f055c5 Binary files /dev/null and b/artifacts/event_safe_bet.png differ diff --git a/artifacts/event_scan_cache.png b/artifacts/event_scan_cache.png new file mode 100644 index 0000000..0b1da65 Binary files /dev/null and b/artifacts/event_scan_cache.png differ diff --git a/artifacts/event_scan_result.png b/artifacts/event_scan_result.png new file mode 100644 index 0000000..4882b48 Binary files /dev/null and b/artifacts/event_scan_result.png differ diff --git a/artifacts/event_service.png b/artifacts/event_service.png new file mode 100644 index 0000000..07bad9f Binary files /dev/null and b/artifacts/event_service.png differ diff --git a/artifacts/event_trader.png b/artifacts/event_trader.png new file mode 100644 index 0000000..a42445f Binary files /dev/null and b/artifacts/event_trader.png differ diff --git a/artifacts/events_overview.png b/artifacts/events_overview.png new file mode 100644 index 0000000..3503d88 Binary files /dev/null and b/artifacts/events_overview.png differ diff --git a/artifacts/hard_event_barter.png b/artifacts/hard_event_barter.png new file mode 100644 index 0000000..974eb6b Binary files /dev/null and b/artifacts/hard_event_barter.png differ diff --git a/artifacts/hard_event_blind_cache.png b/artifacts/hard_event_blind_cache.png new file mode 100644 index 0000000..377491c Binary files /dev/null and b/artifacts/hard_event_blind_cache.png differ diff --git a/artifacts/hard_event_collector.png b/artifacts/hard_event_collector.png new file mode 100644 index 0000000..9ec0f82 Binary files /dev/null and b/artifacts/hard_event_collector.png differ diff --git a/artifacts/hard_event_evacuation.png b/artifacts/hard_event_evacuation.png new file mode 100644 index 0000000..5dd4b77 Binary files /dev/null and b/artifacts/hard_event_evacuation.png differ diff --git a/artifacts/hard_event_evacuation_stage2.png b/artifacts/hard_event_evacuation_stage2.png new file mode 100644 index 0000000..92aadf4 Binary files /dev/null and b/artifacts/hard_event_evacuation_stage2.png differ diff --git a/artifacts/hard_event_prototype.png b/artifacts/hard_event_prototype.png new file mode 100644 index 0000000..d350b9d Binary files /dev/null and b/artifacts/hard_event_prototype.png differ diff --git a/artifacts/hard_event_rescue.png b/artifacts/hard_event_rescue.png new file mode 100644 index 0000000..f36928c Binary files /dev/null and b/artifacts/hard_event_rescue.png differ diff --git a/artifacts/hard_event_safe_bet.png b/artifacts/hard_event_safe_bet.png new file mode 100644 index 0000000..d931163 Binary files /dev/null and b/artifacts/hard_event_safe_bet.png differ diff --git a/artifacts/hard_event_scan_cache.png b/artifacts/hard_event_scan_cache.png new file mode 100644 index 0000000..a814e15 Binary files /dev/null and b/artifacts/hard_event_scan_cache.png differ diff --git a/artifacts/hard_event_scan_result.png b/artifacts/hard_event_scan_result.png new file mode 100644 index 0000000..f257c37 Binary files /dev/null and b/artifacts/hard_event_scan_result.png differ diff --git a/artifacts/hard_event_service.png b/artifacts/hard_event_service.png new file mode 100644 index 0000000..0804dab Binary files /dev/null and b/artifacts/hard_event_service.png differ diff --git a/artifacts/hard_event_trader.png b/artifacts/hard_event_trader.png new file mode 100644 index 0000000..1b98d6f Binary files /dev/null and b/artifacts/hard_event_trader.png differ diff --git a/artifacts/hard_events_overview.png b/artifacts/hard_events_overview.png new file mode 100644 index 0000000..05ba01f Binary files /dev/null and b/artifacts/hard_events_overview.png differ diff --git a/artifacts/landmark_abandoned_hamlet.png b/artifacts/landmark_abandoned_hamlet.png new file mode 100644 index 0000000..b666b87 Binary files /dev/null and b/artifacts/landmark_abandoned_hamlet.png differ diff --git a/artifacts/landmark_abandoned_hamlet.png.import b/artifacts/landmark_abandoned_hamlet.png.import new file mode 100644 index 0000000..45819da --- /dev/null +++ b/artifacts/landmark_abandoned_hamlet.png.import @@ -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 diff --git a/artifacts/landmark_dry_hollow.png b/artifacts/landmark_dry_hollow.png new file mode 100644 index 0000000..8e3b608 Binary files /dev/null and b/artifacts/landmark_dry_hollow.png differ diff --git a/artifacts/landmark_dry_hollow.png.import b/artifacts/landmark_dry_hollow.png.import new file mode 100644 index 0000000..1d92a90 --- /dev/null +++ b/artifacts/landmark_dry_hollow.png.import @@ -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 diff --git a/artifacts/landmark_fissure.png b/artifacts/landmark_fissure.png new file mode 100644 index 0000000..ee90c1e Binary files /dev/null and b/artifacts/landmark_fissure.png differ diff --git a/artifacts/landmark_fissure.png.import b/artifacts/landmark_fissure.png.import new file mode 100644 index 0000000..8398f7b --- /dev/null +++ b/artifacts/landmark_fissure.png.import @@ -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 diff --git a/artifacts/landmark_meteor_crater.png b/artifacts/landmark_meteor_crater.png new file mode 100644 index 0000000..29c02e3 Binary files /dev/null and b/artifacts/landmark_meteor_crater.png differ diff --git a/artifacts/landmark_meteor_crater.png.import b/artifacts/landmark_meteor_crater.png.import new file mode 100644 index 0000000..9169f29 --- /dev/null +++ b/artifacts/landmark_meteor_crater.png.import @@ -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 diff --git a/artifacts/landmark_scale.png b/artifacts/landmark_scale.png new file mode 100644 index 0000000..55dccd7 Binary files /dev/null and b/artifacts/landmark_scale.png differ diff --git a/artifacts/landmark_scale.png.import b/artifacts/landmark_scale.png.import new file mode 100644 index 0000000..a8a9431 --- /dev/null +++ b/artifacts/landmark_scale.png.import @@ -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 diff --git a/artifacts/landmark_weathered_stones.png b/artifacts/landmark_weathered_stones.png new file mode 100644 index 0000000..991fb96 Binary files /dev/null and b/artifacts/landmark_weathered_stones.png differ diff --git a/artifacts/landmark_weathered_stones.png.import b/artifacts/landmark_weathered_stones.png.import new file mode 100644 index 0000000..e52f8ed --- /dev/null +++ b/artifacts/landmark_weathered_stones.png.import @@ -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 diff --git a/artifacts/landmarks_overview.png b/artifacts/landmarks_overview.png new file mode 100644 index 0000000..380d406 Binary files /dev/null and b/artifacts/landmarks_overview.png differ diff --git a/artifacts/landmarks_overview.png.import b/artifacts/landmarks_overview.png.import new file mode 100644 index 0000000..0fe686d --- /dev/null +++ b/artifacts/landmarks_overview.png.import @@ -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 diff --git a/artifacts/nature_landscapes.png b/artifacts/nature_landscapes.png new file mode 100644 index 0000000..9a3d314 Binary files /dev/null and b/artifacts/nature_landscapes.png differ diff --git a/artifacts/nature_landscapes.png.import b/artifacts/nature_landscapes.png.import new file mode 100644 index 0000000..fe87f3e --- /dev/null +++ b/artifacts/nature_landscapes.png.import @@ -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 diff --git a/artifacts/nature_surface_detail.png b/artifacts/nature_surface_detail.png new file mode 100644 index 0000000..e34ad71 Binary files /dev/null and b/artifacts/nature_surface_detail.png differ diff --git a/artifacts/nature_surface_detail.png.import b/artifacts/nature_surface_detail.png.import new file mode 100644 index 0000000..3a5c10f --- /dev/null +++ b/artifacts/nature_surface_detail.png.import @@ -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 diff --git a/artifacts/threat_rescue_150.png b/artifacts/threat_rescue_150.png new file mode 100644 index 0000000..b81d52f Binary files /dev/null and b/artifacts/threat_rescue_150.png differ diff --git a/artifacts/threat_scan_150.png b/artifacts/threat_scan_150.png new file mode 100644 index 0000000..08ad85c Binary files /dev/null and b/artifacts/threat_scan_150.png differ diff --git a/artifacts/threat_scan_l50.png b/artifacts/threat_scan_l50.png new file mode 100644 index 0000000..d16a3bd Binary files /dev/null and b/artifacts/threat_scan_l50.png differ diff --git a/artifacts/threat_scan_safe.png b/artifacts/threat_scan_safe.png new file mode 100644 index 0000000..d86d1ab Binary files /dev/null and b/artifacts/threat_scan_safe.png differ diff --git a/artifacts/threat_system_overview.png b/artifacts/threat_system_overview.png new file mode 100644 index 0000000..64be9fc Binary files /dev/null and b/artifacts/threat_system_overview.png differ diff --git a/artifacts/threat_zero_chance_event.png b/artifacts/threat_zero_chance_event.png new file mode 100644 index 0000000..278d9b8 Binary files /dev/null and b/artifacts/threat_zero_chance_event.png differ diff --git a/artifacts/threat_zero_chance_poi.png b/artifacts/threat_zero_chance_poi.png new file mode 100644 index 0000000..813c00f Binary files /dev/null and b/artifacts/threat_zero_chance_poi.png differ diff --git a/assets/planets/landmarks/abandoned_hamlet.png b/assets/planets/landmarks/abandoned_hamlet.png new file mode 100644 index 0000000..79c1525 Binary files /dev/null and b/assets/planets/landmarks/abandoned_hamlet.png differ diff --git a/assets/planets/landmarks/abandoned_hamlet.png.import b/assets/planets/landmarks/abandoned_hamlet.png.import new file mode 100644 index 0000000..6688264 --- /dev/null +++ b/assets/planets/landmarks/abandoned_hamlet.png.import @@ -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 diff --git a/assets/planets/landmarks/dry_hollow.png b/assets/planets/landmarks/dry_hollow.png new file mode 100644 index 0000000..f692722 Binary files /dev/null and b/assets/planets/landmarks/dry_hollow.png differ diff --git a/assets/planets/landmarks/dry_hollow.png.import b/assets/planets/landmarks/dry_hollow.png.import new file mode 100644 index 0000000..c96ea7e --- /dev/null +++ b/assets/planets/landmarks/dry_hollow.png.import @@ -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 diff --git a/assets/planets/landmarks/fissure.png b/assets/planets/landmarks/fissure.png new file mode 100644 index 0000000..d731414 Binary files /dev/null and b/assets/planets/landmarks/fissure.png differ diff --git a/assets/planets/landmarks/fissure.png.import b/assets/planets/landmarks/fissure.png.import new file mode 100644 index 0000000..d18356a --- /dev/null +++ b/assets/planets/landmarks/fissure.png.import @@ -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 diff --git a/assets/planets/landmarks/meteor_crater.png b/assets/planets/landmarks/meteor_crater.png new file mode 100644 index 0000000..927873f Binary files /dev/null and b/assets/planets/landmarks/meteor_crater.png differ diff --git a/assets/planets/landmarks/meteor_crater.png.import b/assets/planets/landmarks/meteor_crater.png.import new file mode 100644 index 0000000..4223f5e --- /dev/null +++ b/assets/planets/landmarks/meteor_crater.png.import @@ -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 diff --git a/assets/planets/landmarks/prompts.json b/assets/planets/landmarks/prompts.json new file mode 100644 index 0000000..53c41b3 --- /dev/null +++ b/assets/planets/landmarks/prompts.json @@ -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." + } + ] +} diff --git a/assets/planets/landmarks/weathered_stones.png b/assets/planets/landmarks/weathered_stones.png new file mode 100644 index 0000000..e22ae88 Binary files /dev/null and b/assets/planets/landmarks/weathered_stones.png differ diff --git a/assets/planets/landmarks/weathered_stones.png.import b/assets/planets/landmarks/weathered_stones.png.import new file mode 100644 index 0000000..fa96c3a --- /dev/null +++ b/assets/planets/landmarks/weathered_stones.png.import @@ -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 diff --git a/assets/planets/nature/nature_atlas.png b/assets/planets/nature/nature_atlas.png new file mode 100644 index 0000000..0744bc5 Binary files /dev/null and b/assets/planets/nature/nature_atlas.png differ diff --git a/assets/planets/nature/nature_atlas.png.import b/assets/planets/nature/nature_atlas.png.import new file mode 100644 index 0000000..8fafcd2 --- /dev/null +++ b/assets/planets/nature/nature_atlas.png.import @@ -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 diff --git a/assets/planets/nature/prompts.json b/assets/planets/nature/prompts.json new file mode 100644 index 0000000..d4f79ae --- /dev/null +++ b/assets/planets/nature/prompts.json @@ -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" + ] +} diff --git a/planet_mechanic.txt b/planet_mechanic.txt index d597dee..0bb68a2 100644 --- a/planet_mechanic.txt +++ b/planet_mechanic.txt @@ -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. diff --git a/scripts/planets/expedition_controller.gd b/scripts/planets/expedition_controller.gd index 126f577..80c32cd 100644 --- a/scripts/planets/expedition_controller.gd +++ b/scripts/planets/expedition_controller.gd @@ -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() diff --git a/scripts/planets/expedition_ui.gd b/scripts/planets/expedition_ui.gd index eea4ef4..d68698f 100644 --- a/scripts/planets/expedition_ui.gd +++ b/scripts/planets/expedition_ui.gd @@ -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]]) diff --git a/scripts/planets/freighter_screen.gd b/scripts/planets/freighter_screen.gd index ada60bf..a89ab45 100644 --- a/scripts/planets/freighter_screen.gd +++ b/scripts/planets/freighter_screen.gd @@ -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() diff --git a/scripts/planets/planet_profiles.gd b/scripts/planets/planet_profiles.gd index 18c8cdb..76f885a 100644 --- a/scripts/planets/planet_profiles.gd +++ b/scripts/planets/planet_profiles.gd @@ -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)] diff --git a/scripts/planets/planet_progress.gd b/scripts/planets/planet_progress.gd index 53dbad2..ca4c9b5 100644 --- a/scripts/planets/planet_progress.gd +++ b/scripts/planets/planet_progress.gd @@ -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} - diff --git a/scripts/planets/planet_rewards.gd b/scripts/planets/planet_rewards.gd new file mode 100644 index 0000000..b1eb76a --- /dev/null +++ b/scripts/planets/planet_rewards.gd @@ -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)))) diff --git a/scripts/planets/surface_details.gd b/scripts/planets/surface_details.gd new file mode 100644 index 0000000..173b640 --- /dev/null +++ b/scripts/planets/surface_details.gd @@ -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 diff --git a/scripts/planets/surface_details.gd.uid b/scripts/planets/surface_details.gd.uid new file mode 100644 index 0000000..a112349 --- /dev/null +++ b/scripts/planets/surface_details.gd.uid @@ -0,0 +1 @@ +uid://fjo5rs5vmjd4 diff --git a/scripts/planets/surface_events.gd b/scripts/planets/surface_events.gd new file mode 100644 index 0000000..1cde0f0 --- /dev/null +++ b/scripts/planets/surface_events.gd @@ -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} diff --git a/scripts/planets/surface_generator.gd b/scripts/planets/surface_generator.gd index 46cd21c..11a6ad4 100644 --- a/scripts/planets/surface_generator.gd +++ b/scripts/planets/surface_generator.gd @@ -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: diff --git a/scripts/planets/surface_landmarks.gd b/scripts/planets/surface_landmarks.gd new file mode 100644 index 0000000..a2f2b22 --- /dev/null +++ b/scripts/planets/surface_landmarks.gd @@ -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 diff --git a/scripts/planets/surface_landmarks.gd.uid b/scripts/planets/surface_landmarks.gd.uid new file mode 100644 index 0000000..1b26235 --- /dev/null +++ b/scripts/planets/surface_landmarks.gd.uid @@ -0,0 +1 @@ +uid://dd8kkwh3bvjto diff --git a/scripts/planets/surface_scenery.gd b/scripts/planets/surface_scenery.gd index 7fde0f9..941a658 100644 --- a/scripts/planets/surface_scenery.gd +++ b/scripts/planets/surface_scenery.gd @@ -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) diff --git a/scripts/planets/surface_screen.gd b/scripts/planets/surface_screen.gd index 527c3c1..a46fd5c 100644 --- a/scripts/planets/surface_screen.gd +++ b/scripts/planets/surface_screen.gd @@ -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) diff --git a/scripts/planets/surface_world.gd b/scripts/planets/surface_world.gd index 0647e28..1b7443e 100644 --- a/scripts/planets/surface_world.gd +++ b/scripts/planets/surface_world.gd @@ -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) diff --git a/scripts/ui/captain_gear_slot.gd b/scripts/ui/captain_gear_slot.gd index eb8d24b..662dbed 100644 --- a/scripts/ui/captain_gear_slot.gd +++ b/scripts/ui/captain_gear_slot.gd @@ -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: diff --git a/tests/levels/planet_expeditions_test.gd b/tests/levels/planet_expeditions_test.gd index c00eff6..aaa9902 100644 --- a/tests/levels/planet_expeditions_test.gd +++ b/tests/levels/planet_expeditions_test.gd @@ -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 diff --git a/tests/levels/planet_prompt_test.gd b/tests/levels/planet_prompt_test.gd index 9cb3130..8875777 100644 --- a/tests/levels/planet_prompt_test.gd +++ b/tests/levels/planet_prompt_test.gd @@ -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") diff --git a/tests/levels/planet_reward_scaling_test.gd b/tests/levels/planet_reward_scaling_test.gd new file mode 100644 index 0000000..525dd41 --- /dev/null +++ b/tests/levels/planet_reward_scaling_test.gd @@ -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() diff --git a/tests/levels/planet_reward_scaling_test.tscn b/tests/levels/planet_reward_scaling_test.tscn new file mode 100644 index 0000000..a2e649b --- /dev/null +++ b/tests/levels/planet_reward_scaling_test.tscn @@ -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") diff --git a/tests/levels/planet_threats_test.gd b/tests/levels/planet_threats_test.gd new file mode 100644 index 0000000..2824cae --- /dev/null +++ b/tests/levels/planet_threats_test.gd @@ -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() diff --git a/tests/levels/planet_threats_test.tscn b/tests/levels/planet_threats_test.tscn new file mode 100644 index 0000000..96301ec --- /dev/null +++ b/tests/levels/planet_threats_test.tscn @@ -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") diff --git a/tests/levels/surface_events_test.gd b/tests/levels/surface_events_test.gd index 5e40aa1..922ddcd 100644 --- a/tests/levels/surface_events_test.gd +++ b/tests/levels/surface_events_test.gd @@ -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() diff --git a/tests/previews/planet_threats_preview.gd b/tests/previews/planet_threats_preview.gd new file mode 100644 index 0000000..fd77bfb --- /dev/null +++ b/tests/previews/planet_threats_preview.gd @@ -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() diff --git a/tests/previews/planet_threats_preview.tscn b/tests/previews/planet_threats_preview.tscn new file mode 100644 index 0000000..304e865 --- /dev/null +++ b/tests/previews/planet_threats_preview.tscn @@ -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") diff --git a/tests/previews/surface_details_test.gd b/tests/previews/surface_details_test.gd new file mode 100644 index 0000000..e38dd7e --- /dev/null +++ b/tests/previews/surface_details_test.gd @@ -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() diff --git a/tests/previews/surface_details_test.gd.uid b/tests/previews/surface_details_test.gd.uid new file mode 100644 index 0000000..c08b412 --- /dev/null +++ b/tests/previews/surface_details_test.gd.uid @@ -0,0 +1 @@ +uid://dwdckw6s07msc diff --git a/tests/previews/surface_events_preview.gd b/tests/previews/surface_events_preview.gd new file mode 100644 index 0000000..b185952 --- /dev/null +++ b/tests/previews/surface_events_preview.gd @@ -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() diff --git a/tests/previews/surface_events_preview.tscn b/tests/previews/surface_events_preview.tscn new file mode 100644 index 0000000..da9c13f --- /dev/null +++ b/tests/previews/surface_events_preview.tscn @@ -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") diff --git a/tests/previews/surface_landmarks_preview.gd b/tests/previews/surface_landmarks_preview.gd new file mode 100644 index 0000000..85e5c4d --- /dev/null +++ b/tests/previews/surface_landmarks_preview.gd @@ -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() diff --git a/tests/previews/surface_landmarks_preview.gd.uid b/tests/previews/surface_landmarks_preview.gd.uid new file mode 100644 index 0000000..faece12 --- /dev/null +++ b/tests/previews/surface_landmarks_preview.gd.uid @@ -0,0 +1 @@ +uid://dco7gctr0b0vq diff --git a/tests/previews/surface_landmarks_preview.tscn b/tests/previews/surface_landmarks_preview.tscn new file mode 100644 index 0000000..56738c5 --- /dev/null +++ b/tests/previews/surface_landmarks_preview.tscn @@ -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") diff --git a/tests/previews/surface_landmarks_test.gd b/tests/previews/surface_landmarks_test.gd new file mode 100644 index 0000000..3cdd361 --- /dev/null +++ b/tests/previews/surface_landmarks_test.gd @@ -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() diff --git a/tests/previews/surface_landmarks_test.gd.uid b/tests/previews/surface_landmarks_test.gd.uid new file mode 100644 index 0000000..63a9b6f --- /dev/null +++ b/tests/previews/surface_landmarks_test.gd.uid @@ -0,0 +1 @@ +uid://dc86vec1784n2 diff --git a/tests/previews/surface_scenery_preview.gd b/tests/previews/surface_scenery_preview.gd index bd84e3d..d5059c1 100644 --- a/tests/previews/surface_scenery_preview.gd +++ b/tests/previews/surface_scenery_preview.gd @@ -42,6 +42,7 @@ func _ready() -> void: for event_nodes in surface.world._event_nodes: assert(event_nodes.size() == 1 and event_nodes.has("marker")) var event: Dictionary = surface.world.data.events[0] + event.kind = "trader" teleport(surface,event.position+Vector2(0,220)) await settle() get_viewport().get_texture().get_image().save_png("res://artifacts/planet_scenery_event_marker.png") @@ -49,17 +50,17 @@ func _ready() -> void: surface._check_events() assert(surface._dialog.visible) await settle() - get_viewport().get_texture().get_image().save_png("res://artifacts/engineer_event_offer.png") + get_viewport().get_texture().get_image().save_png("res://artifacts/trader_event_offer.png") var purchase: Button for child in surface._choices.get_children(): - if child is Button and child.text.begins_with("KUP NIEZNANY"): + if child is Button and child.text.begins_with("Kup ") and child.text.contains("rakiet"): purchase = child assert(purchase != null and not purchase.disabled) purchase.pressed.emit() - assert(event.complete and GameState.captain.credits == 400) - assert(surface.progress.cargo_modules.size() == 1) + assert(event.complete and GameState.captain.credits == 500-int(event.missile_price)) + assert(surface.progress.cargo_missiles == int(event.missile_count)) await settle() - get_viewport().get_texture().get_image().save_png("res://artifacts/engineer_event_result.png") + get_viewport().get_texture().get_image().save_png("res://artifacts/trader_event_result.png") var keys: Array = preload("res://scripts/planets/surface_scenery.gd").KINDS.duplicate() keys.append("engineer_camp") var sheet := Image.create(1280,640,false,Image.FORMAT_RGBA8) @@ -77,5 +78,5 @@ func _ready() -> void: manager.finish_surface(true) game.queue_free() await get_tree().process_frame - print("SURFACE SCENERY UI PASS: zoom, props, transparency, automatic event dialog, 100-credit purchase, temporary module cargo") + print("SURFACE SCENERY UI PASS: zoom, props, transparency, automatic event dialog, discounted rockets, temporary expedition cargo") get_tree().quit()