Zmiany
This commit is contained in:
parent
fc62a03553
commit
2265b59edc
BIN
artifacts/cave_mountain_preview.png
Normal file
BIN
artifacts/cave_mountain_preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
BIN
artifacts/cave_poi_window.png
Normal file
BIN
artifacts/cave_poi_window.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@ -1,39 +0,0 @@
|
||||
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||
|
||||
[ 0% ] [90m[1mfirst_scan_filesystem[22m | Started Project initialization (5 steps)[39m[0m
|
||||
[ 0% ] [90m[1mfirst_scan_filesystem[22m | Scanning file structure...[39m[0m
|
||||
[ 16% ] [90m[1mfirst_scan_filesystem[22m | Loading global class names...[39m[0m
|
||||
[ 33% ] [90m[1mfirst_scan_filesystem[22m | Verifying GDExtensions...[39m[0m
|
||||
[ 50% ] [90m[1mfirst_scan_filesystem[22m | Creating autoload scripts...[39m[0m
|
||||
[ 66% ] [90m[1mfirst_scan_filesystem[22m | Initializing plugins...[39m[0m
|
||||
Godot MCP Toolkit v1.0.3 — by NPGameDev · npgamedev.com
|
||||
[MCPExtensions] Hot-reload watcher active
|
||||
[MCPServer] port: scanning 6550-6560 (default)
|
||||
[MCPServer] session token written to user://addons/godot_mcp_toolkit/project_instance_32cc2271ac67/mcp_token
|
||||
[MCPServer] listening on 127.0.0.1:6551
|
||||
[MCPRegistry] registered c:/users/tomek/documents/godot/randoloot on port 6551
|
||||
[ 83% ] [90m[1mfirst_scan_filesystem[22m | Starting file scan...[39m[0m
|
||||
[92m[ DONE ][39m [1mfirst_scan_filesystem[22m
|
||||
[0m
|
||||
[ 0% ] [90m[1mreimport[22m | Started (Re)Importing Assets (1 steps)[39m[0m
|
||||
[ 0% ] [90m[1mreimport[22m | Preparing files to reimport...[39m[0m
|
||||
[ 0% ] [90m[1mreimport[22m | Executing pre-reimport operations...[39m[0m
|
||||
[ 0% ] [90m[1mreimport[22m | mineral_deposit.png[39m[0m
|
||||
[ 50% ] [90m[1mreimport[22m | Finalizing Asset Import...[39m[0m
|
||||
[92m[ DONE ][39m [1mreimport[22m
|
||||
[0m
|
||||
[ 0% ] [90m[1mreimport[22m | Started (Re)Importing Assets (1 steps)[39m[0m
|
||||
[ 0% ] [90m[1mreimport[22m | Executing post-reimport operations...[39m[0m
|
||||
[92m[ DONE ][39m [1mreimport[22m
|
||||
[0m
|
||||
[ 0% ] [90m[1mloading_editor_layout[22m | Started Loading editor (5 steps)[39m[0m
|
||||
[ 0% ] [90m[1mloading_editor_layout[22m | Loading editor layout...[39m[0m
|
||||
[ 16% ] [90m[1mloading_editor_layout[22m | Loading docks...[39m[0m
|
||||
[ 33% ] [90m[1mloading_editor_layout[22m | Reopening scenes...[39m[0m
|
||||
[ 50% ] [90m[1mloading_editor_layout[22m | Loading central editor layout...[39m[0m
|
||||
[ 66% ] [90m[1mloading_editor_layout[22m | Loading plugin window layout...[39m[0m
|
||||
[ 83% ] [90m[1mloading_editor_layout[22m | Editor layout ready.[39m[0m
|
||||
[92m[ DONE ][39m [1mloading_editor_layout[22m
|
||||
[0m
|
||||
[MCPServer] stopped
|
||||
[MCPRegistry] deregistered c:/users/tomek/documents/godot/randoloot
|
||||
@ -1,7 +0,0 @@
|
||||
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||
OpenGL API 3.3.0 NVIDIA 610.88 - Compatibility - Using Device: NVIDIA - NVIDIA GeForce RTX 4070 SUPER
|
||||
|
||||
[MCPRuntimeServer] port: scanning 6570-6585 (default)
|
||||
[MCPRuntimeServer] listening on 127.0.0.1:6570
|
||||
[MCPRegistry] runtime port 6570 registered for c:/users/tomek/documents/godot/randoloot
|
||||
MINERAL DEPOSIT PASS: six biome renders, real ore instead of icon, mipmaps, alpha/soil blending, no stationary flicker, smooth subpixel panning, identical pixels after camera return; max mean pan delta=0.00815902349665
|
||||
40
artifacts/mineral_deposit_biome_0.png.import
Normal file
40
artifacts/mineral_deposit_biome_0.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://p4nmpsf8idft"
|
||||
path="res://.godot/imported/mineral_deposit_biome_0.png-77db281db793b2b686e12550ee9978dd.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://artifacts/mineral_deposit_biome_0.png"
|
||||
dest_files=["res://.godot/imported/mineral_deposit_biome_0.png-77db281db793b2b686e12550ee9978dd.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
|
||||
40
artifacts/mineral_deposit_biome_1.png.import
Normal file
40
artifacts/mineral_deposit_biome_1.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cvdt0swmucrlf"
|
||||
path="res://.godot/imported/mineral_deposit_biome_1.png-46f45636973fd87fd9b95f9f562f96e5.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://artifacts/mineral_deposit_biome_1.png"
|
||||
dest_files=["res://.godot/imported/mineral_deposit_biome_1.png-46f45636973fd87fd9b95f9f562f96e5.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
|
||||
40
artifacts/mineral_deposit_biome_2.png.import
Normal file
40
artifacts/mineral_deposit_biome_2.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dg4v6m08btt8o"
|
||||
path="res://.godot/imported/mineral_deposit_biome_2.png-fc0d5d290d7bf6e0fab8690f708dbe82.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://artifacts/mineral_deposit_biome_2.png"
|
||||
dest_files=["res://.godot/imported/mineral_deposit_biome_2.png-fc0d5d290d7bf6e0fab8690f708dbe82.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
|
||||
40
artifacts/mineral_deposit_biome_3.png.import
Normal file
40
artifacts/mineral_deposit_biome_3.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://brifx6tefq8i3"
|
||||
path="res://.godot/imported/mineral_deposit_biome_3.png-204ee054b6be8508361c6cc59eb28a8f.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://artifacts/mineral_deposit_biome_3.png"
|
||||
dest_files=["res://.godot/imported/mineral_deposit_biome_3.png-204ee054b6be8508361c6cc59eb28a8f.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
|
||||
40
artifacts/mineral_deposit_biome_4.png.import
Normal file
40
artifacts/mineral_deposit_biome_4.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://ddu01h2q0ysvr"
|
||||
path="res://.godot/imported/mineral_deposit_biome_4.png-27443e3b2ca08354b3a7c60d43eb419f.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://artifacts/mineral_deposit_biome_4.png"
|
||||
dest_files=["res://.godot/imported/mineral_deposit_biome_4.png-27443e3b2ca08354b3a7c60d43eb419f.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
|
||||
40
artifacts/mineral_deposit_biome_5.png.import
Normal file
40
artifacts/mineral_deposit_biome_5.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://vcacphbrod2w"
|
||||
path="res://.godot/imported/mineral_deposit_biome_5.png-11a7baf9252acea7d08565b37fe5abfd.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://artifacts/mineral_deposit_biome_5.png"
|
||||
dest_files=["res://.godot/imported/mineral_deposit_biome_5.png-11a7baf9252acea7d08565b37fe5abfd.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
|
||||
40
artifacts/mineral_deposit_biomes.png.import
Normal file
40
artifacts/mineral_deposit_biomes.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cv6yfrurl1ac8"
|
||||
path="res://.godot/imported/mineral_deposit_biomes.png-8d521592b14307c431a6dd51dce6da55.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://artifacts/mineral_deposit_biomes.png"
|
||||
dest_files=["res://.godot/imported/mineral_deposit_biomes.png-8d521592b14307c431a6dd51dce6da55.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
artifacts/planet_ambience_demo.wav
Normal file
BIN
artifacts/planet_ambience_demo.wav
Normal file
Binary file not shown.
24
artifacts/planet_ambience_demo.wav.import
Normal file
24
artifacts/planet_ambience_demo.wav.import
Normal file
@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://gx0ui376tbee"
|
||||
path="res://.godot/imported/planet_ambience_demo.wav-69e2b06c75ef43e64bb59ed65915d5a4.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://artifacts/planet_ambience_demo.wav"
|
||||
dest_files=["res://.godot/imported/planet_ambience_demo.wav-69e2b06c75ef43e64bb59ed65915d5a4.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
7
artifacts/poi_preview.log
Normal file
7
artifacts/poi_preview.log
Normal file
@ -0,0 +1,7 @@
|
||||
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||
OpenGL API 3.3.0 NVIDIA 610.88 - Compatibility - Using Device: NVIDIA - NVIDIA GeForce RTX 4070 SUPER
|
||||
|
||||
[MCPRuntimeServer] port: scanning 6570-6585 (default)
|
||||
[MCPRuntimeServer] listening on 127.0.0.1:6571
|
||||
[MCPRegistry] runtime port 6571 registered for c:/users/tomek/documents/godot/randoloot
|
||||
POI PRESENTATION PASS: 15 illustrated cards, compact layout, small-window scroll, mountain cave
|
||||
BIN
artifacts/poi_stage_cards.png
Normal file
BIN
artifacts/poi_stage_cards.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
14
assets/planets/poi/PROMPTS.md
Normal file
14
assets/planets/poi/PROMPTS.md
Normal file
@ -0,0 +1,14 @@
|
||||
# POI artwork
|
||||
|
||||
Created with the built-in image_gen tool, 2026-10-18.
|
||||
Project assets: `expedition_atlas.png` (15 narrative illustrations) and
|
||||
`cave_entrance.png` (transparent terrain sprite). No pixel processing after generation;
|
||||
the engine crops atlas cells and blends the cave into its biome with a static shader.
|
||||
|
||||
## Expedition atlas — final prompt
|
||||
|
||||
Create ONE production game illustration atlas, landscape 1536x1024 or larger same 3:2 ratio. EXACT regular grid 3 columns x 5 rows, equal rectangular cells with no gutters, borders, writing, labels or interface. Every cell is its own cinematic landscape scene. Realistic painterly grounded expedition adventure, earth tones olive brown warm amber, natural sunlight and practical lanterns, atmospheric detailed environments, no spaceships, no astronauts, no neon holograms. Each ROW is one location, each COLUMN its next exploration stage, left to right progressively deeper. ROW1 rare mineral deposit: left natural rocky hillside with small copper/green mineral outcrops; middle excavation with drill and exposed ore; right rich deep ore vein glowing only from lantern warm reflected light. ROW2 ancient structure: left overgrown stone doorway in forest; middle subterranean carved stone chamber with dust and relic; right ancient mechanical relic inside monumental stone vault. ROW3 abandoned supply warehouse: left dusty crates in old timber and concrete shed; middle shelves of ammo cases in storage; right locked underground military storeroom with crates supplies and brass ammunition. ROW4 abandoned village: left empty weathered cottages overgrown woodland; middle a desk with journals in abandoned home; right concealed old strongbox with coins below ruined floor. ROW5 cave: left beautiful rocky cave mouth at base of mountain, moss and ferns; middle underground limestone cavern with mineral formations and lantern; right deep cavern with mysterious organic specimens beside still pool. No people. Distinct scene in every one of the 15 cells, no text anywhere, consistent beautiful narrative game artwork, detailed foregrounds, full bleed.
|
||||
|
||||
## Cave entrance — final prompt
|
||||
|
||||
Production transparent-background game terrain sprite: a realistic beautiful cave entrance integrated into the lower foot of a rugged mountain, viewed from elevated near-top-down 65 degree strategy-game camera, NO sky, no horizon, no scene background. One isolated irregular patch of natural grey limestone and weathered brown rock, broad at base tapering at upper back, dark cave opening at lower center with visible rocky lip and short trail leading toward bottom edge. Rugged detailed strata match photorealistic isometric mountain assets, subtle moss in cracks, sparse tiny ferns, loose pebbles and scree. Deep cavern darkness, naturally occluded shadows, light from upper-left. No lights, crystals, fantasy objects, people, buildings, snow, text. Cave mouth should be visually readable even at 150 pixels wide. Full object contained with generous transparent margin on all sides and softly scattered stones fading into transparent ground at bottom. Upper back rock should blend into a larger mountain behind it, no isolated circular boulder outline, width greater than height. High quality hand-painted photorealistic terrain art, grey/olive/taupe earth palette.
|
||||
BIN
assets/planets/poi/cave_entrance.png
Normal file
BIN
assets/planets/poi/cave_entrance.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 MiB |
40
assets/planets/poi/cave_entrance.png.import
Normal file
40
assets/planets/poi/cave_entrance.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://brprl66w0g6o4"
|
||||
path="res://.godot/imported/cave_entrance.png-b8ef461bb38deaed5e2710c34c445de8.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/planets/poi/cave_entrance.png"
|
||||
dest_files=["res://.godot/imported/cave_entrance.png-b8ef461bb38deaed5e2710c34c445de8.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
assets/planets/poi/expedition_atlas.png
Normal file
BIN
assets/planets/poi/expedition_atlas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 MiB |
40
assets/planets/poi/expedition_atlas.png.import
Normal file
40
assets/planets/poi/expedition_atlas.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d0v7xj0o7cdxo"
|
||||
path="res://.godot/imported/expedition_atlas.png-ac84551ff3ae1ce41521db3f0b21a579.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/planets/poi/expedition_atlas.png"
|
||||
dest_files=["res://.godot/imported/expedition_atlas.png-ac84551ff3ae1ce41521db3f0b21a579.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
|
||||
44
assets/sounds/surface/CREDITS.md
Normal file
44
assets/sounds/surface/CREDITS.md
Normal file
@ -0,0 +1,44 @@
|
||||
# Planet surface ambience
|
||||
|
||||
These effects use edited field recordings and foley, with no generated speech
|
||||
or music. `manifest.json` maps each asset to its source and edits.
|
||||
|
||||
## CC0 1.0 public domain recordings
|
||||
|
||||
License: https://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
- **Thimras — Park ambiences**
|
||||
https://opengameart.org/content/park-ambiences
|
||||
`park_ambience_wind.wav`: real open-field wind recorded in Adelaide,
|
||||
South Australia. Used for the wind loop and natural gusts.
|
||||
- **Iwan "qubodup" Gabovitch — 20 Rustles of dry leaves**
|
||||
https://opengameart.org/content/20-rustles-dry-leaves
|
||||
`rustle01.flac`, `rustle04.flac`, `rustle09.flac`: leaf foley recordings.
|
||||
Used for foliage beds and individual rustles.
|
||||
- **isaiah658 — Ambient Bird Sounds**
|
||||
https://opengameart.org/content/ambient-bird-sounds
|
||||
`birds-isaiah658_0.ogg`: field-recorded birds; independent excerpted calls.
|
||||
- **Rudmer_Rotteveel — Wood Creak Single V7**
|
||||
https://freesound.org/people/Rudmer_Rotteveel/sounds/506662/
|
||||
Public high-quality preview of a real wooden-floor creak. Used as quiet
|
||||
wood flex/branch-creak foley, with mild pitch and EQ edits.
|
||||
|
||||
## CC BY 3.0 recordings — attribution required
|
||||
|
||||
**Blender Foundation — Ambient Bird, Cricket and Frog (Yo Frankie!)**
|
||||
https://opengameart.org/content/ambient-bird-cricket-and-frog
|
||||
License: https://creativecommons.org/licenses/by/3.0/
|
||||
|
||||
`amb_cricket_1.flac` and `amb_frog_1.flac`, from the Blender Foundation's
|
||||
Yo Frankie! assets, submitted to OpenGameArt by Lamoot. Used for `insects.wav`
|
||||
and `marsh_call.wav`. Changes: conversion to 48 kHz PCM16 stereo, DC removal,
|
||||
filtering, smooth fades and a slight pitch reduction on the frog recording.
|
||||
Retain this credit and license link when distributing these assets or the game.
|
||||
No endorsement by the original authors is implied.
|
||||
|
||||
## Rebuilding
|
||||
|
||||
Run `tools/audio/fetch_surface_sources.py`, then
|
||||
`tools/audio/build_surface_audio.py` (Python, numpy, soundfile).
|
||||
Output WAVs are 48 kHz, stereo, PCM16. Loops use crossfaded seams. One-shots
|
||||
retain the recording's natural character and are spatialised in the game.
|
||||
BIN
assets/sounds/surface/bird_1.wav
Normal file
BIN
assets/sounds/surface/bird_1.wav
Normal file
Binary file not shown.
24
assets/sounds/surface/bird_1.wav.import
Normal file
24
assets/sounds/surface/bird_1.wav.import
Normal file
@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://dm5c8h0y3ysky"
|
||||
path="res://.godot/imported/bird_1.wav-cdf7405f02c1c743ca9d238dcfa6ed67.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/sounds/surface/bird_1.wav"
|
||||
dest_files=["res://.godot/imported/bird_1.wav-cdf7405f02c1c743ca9d238dcfa6ed67.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
BIN
assets/sounds/surface/bird_2.wav
Normal file
BIN
assets/sounds/surface/bird_2.wav
Normal file
Binary file not shown.
24
assets/sounds/surface/bird_2.wav.import
Normal file
24
assets/sounds/surface/bird_2.wav.import
Normal file
@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://chgut843e28et"
|
||||
path="res://.godot/imported/bird_2.wav-6932825affc4a185cb4514539250d3d9.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/sounds/surface/bird_2.wav"
|
||||
dest_files=["res://.godot/imported/bird_2.wav-6932825affc4a185cb4514539250d3d9.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
BIN
assets/sounds/surface/bird_3.wav
Normal file
BIN
assets/sounds/surface/bird_3.wav
Normal file
Binary file not shown.
24
assets/sounds/surface/bird_3.wav.import
Normal file
24
assets/sounds/surface/bird_3.wav.import
Normal file
@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://dywt8vwj61c7e"
|
||||
path="res://.godot/imported/bird_3.wav-7ac963a34358a967a65df122f8f21e71.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/sounds/surface/bird_3.wav"
|
||||
dest_files=["res://.godot/imported/bird_3.wav-7ac963a34358a967a65df122f8f21e71.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
BIN
assets/sounds/surface/branch_creak_1.wav
Normal file
BIN
assets/sounds/surface/branch_creak_1.wav
Normal file
Binary file not shown.
24
assets/sounds/surface/branch_creak_1.wav.import
Normal file
24
assets/sounds/surface/branch_creak_1.wav.import
Normal file
@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://2r3nsobhlvom"
|
||||
path="res://.godot/imported/branch_creak_1.wav-91b66aa4ee54ead291136e4b4b481c2b.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/sounds/surface/branch_creak_1.wav"
|
||||
dest_files=["res://.godot/imported/branch_creak_1.wav-91b66aa4ee54ead291136e4b4b481c2b.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
BIN
assets/sounds/surface/branch_creak_2.wav
Normal file
BIN
assets/sounds/surface/branch_creak_2.wav
Normal file
Binary file not shown.
24
assets/sounds/surface/branch_creak_2.wav.import
Normal file
24
assets/sounds/surface/branch_creak_2.wav.import
Normal file
@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://bnd4hbxyuo3ru"
|
||||
path="res://.godot/imported/branch_creak_2.wav-da99c1c2b128c45f7ecad6feb50ee161.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/sounds/surface/branch_creak_2.wav"
|
||||
dest_files=["res://.godot/imported/branch_creak_2.wav-da99c1c2b128c45f7ecad6feb50ee161.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
BIN
assets/sounds/surface/foliage_loop.wav
Normal file
BIN
assets/sounds/surface/foliage_loop.wav
Normal file
Binary file not shown.
24
assets/sounds/surface/foliage_loop.wav.import
Normal file
24
assets/sounds/surface/foliage_loop.wav.import
Normal file
@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://b1v3dlda6y3gc"
|
||||
path="res://.godot/imported/foliage_loop.wav-ac95f707d9658aa96d0b054930ec68d8.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/sounds/surface/foliage_loop.wav"
|
||||
dest_files=["res://.godot/imported/foliage_loop.wav-ac95f707d9658aa96d0b054930ec68d8.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
BIN
assets/sounds/surface/insects.wav
Normal file
BIN
assets/sounds/surface/insects.wav
Normal file
Binary file not shown.
24
assets/sounds/surface/insects.wav.import
Normal file
24
assets/sounds/surface/insects.wav.import
Normal file
@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://dg6xdwlar7veg"
|
||||
path="res://.godot/imported/insects.wav-7456494911849a7b6de76d21a01a7c59.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/sounds/surface/insects.wav"
|
||||
dest_files=["res://.godot/imported/insects.wav-7456494911849a7b6de76d21a01a7c59.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
BIN
assets/sounds/surface/leaves_1.wav
Normal file
BIN
assets/sounds/surface/leaves_1.wav
Normal file
Binary file not shown.
24
assets/sounds/surface/leaves_1.wav.import
Normal file
24
assets/sounds/surface/leaves_1.wav.import
Normal file
@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://ckp7b5r423k70"
|
||||
path="res://.godot/imported/leaves_1.wav-6395c3dd209ef155daad25e23eea09d7.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/sounds/surface/leaves_1.wav"
|
||||
dest_files=["res://.godot/imported/leaves_1.wav-6395c3dd209ef155daad25e23eea09d7.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
BIN
assets/sounds/surface/leaves_2.wav
Normal file
BIN
assets/sounds/surface/leaves_2.wav
Normal file
Binary file not shown.
24
assets/sounds/surface/leaves_2.wav.import
Normal file
24
assets/sounds/surface/leaves_2.wav.import
Normal file
@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://b0xrmu55mjveg"
|
||||
path="res://.godot/imported/leaves_2.wav-c9634f0aa2f3cdf774408fb601e08972.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/sounds/surface/leaves_2.wav"
|
||||
dest_files=["res://.godot/imported/leaves_2.wav-c9634f0aa2f3cdf774408fb601e08972.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
BIN
assets/sounds/surface/leaves_3.wav
Normal file
BIN
assets/sounds/surface/leaves_3.wav
Normal file
Binary file not shown.
24
assets/sounds/surface/leaves_3.wav.import
Normal file
24
assets/sounds/surface/leaves_3.wav.import
Normal file
@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://cfq7nhc53wx07"
|
||||
path="res://.godot/imported/leaves_3.wav-435f03b44bbdee6ed569c36ceae589b9.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/sounds/surface/leaves_3.wav"
|
||||
dest_files=["res://.godot/imported/leaves_3.wav-435f03b44bbdee6ed569c36ceae589b9.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
134
assets/sounds/surface/manifest.json
Normal file
134
assets/sounds/surface/manifest.json
Normal file
@ -0,0 +1,134 @@
|
||||
{
|
||||
"wind_loop": {
|
||||
"sources": [
|
||||
"wind.wav: 8..32 seconds"
|
||||
],
|
||||
"processing": "Real open-field wind; rumble/high-bird reduction; two-second seam crossfade",
|
||||
"loop": true,
|
||||
"seconds": 22.0,
|
||||
"peak_dbfs": -3,
|
||||
"rms_dbfs": -24.43
|
||||
},
|
||||
"wind_gust": {
|
||||
"sources": [
|
||||
"wind.wav: 45..53 seconds"
|
||||
],
|
||||
"processing": "Natural gust with long soft attack/release and low-pass",
|
||||
"loop": false,
|
||||
"seconds": 8.0,
|
||||
"peak_dbfs": -3,
|
||||
"rms_dbfs": -21.8
|
||||
},
|
||||
"foliage_loop": {
|
||||
"sources": [
|
||||
"leaves/rustle/rustle01.flac",
|
||||
"leaves/rustle/rustle04.flac",
|
||||
"leaves/rustle/rustle09.flac"
|
||||
],
|
||||
"processing": "Layered leaf recordings with staggered overlaps, soft envelopes, filtering, crossfaded loop",
|
||||
"loop": true,
|
||||
"seconds": 14.5,
|
||||
"peak_dbfs": -3,
|
||||
"rms_dbfs": -25.73
|
||||
},
|
||||
"leaves_1": {
|
||||
"sources": [
|
||||
"leaves/rustle/rustle01.flac"
|
||||
],
|
||||
"processing": "Trimmed leaf rustle; softened attack and release",
|
||||
"loop": false,
|
||||
"seconds": 1.579,
|
||||
"peak_dbfs": -3,
|
||||
"rms_dbfs": -23.3
|
||||
},
|
||||
"leaves_2": {
|
||||
"sources": [
|
||||
"leaves/rustle/rustle04.flac"
|
||||
],
|
||||
"processing": "Trimmed leaf rustle; softened attack and release",
|
||||
"loop": false,
|
||||
"seconds": 2.54,
|
||||
"peak_dbfs": -3,
|
||||
"rms_dbfs": -28.55
|
||||
},
|
||||
"leaves_3": {
|
||||
"sources": [
|
||||
"leaves/rustle/rustle09.flac"
|
||||
],
|
||||
"processing": "Trimmed leaf rustle; softened attack and release",
|
||||
"loop": false,
|
||||
"seconds": 0.635,
|
||||
"peak_dbfs": -3,
|
||||
"rms_dbfs": -24.55
|
||||
},
|
||||
"branch_creak_1": {
|
||||
"sources": [
|
||||
"wood_creak.mp3"
|
||||
],
|
||||
"processing": "Real wood flex/creak; mild pitch variation; no falling-tree impact",
|
||||
"loop": false,
|
||||
"seconds": 2.045,
|
||||
"peak_dbfs": -3,
|
||||
"rms_dbfs": -22.96
|
||||
},
|
||||
"branch_creak_2": {
|
||||
"sources": [
|
||||
"wood_creak.mp3"
|
||||
],
|
||||
"processing": "Real wood flex/creak; mild pitch variation; no falling-tree impact",
|
||||
"loop": false,
|
||||
"seconds": 1.697,
|
||||
"peak_dbfs": -3,
|
||||
"rms_dbfs": -23.1
|
||||
},
|
||||
"bird_1": {
|
||||
"sources": [
|
||||
"birds.ogg: 3..8 seconds"
|
||||
],
|
||||
"processing": "Natural distant birds, cut into independent calls; filtered noise; soft boundaries",
|
||||
"loop": false,
|
||||
"seconds": 5.0,
|
||||
"peak_dbfs": -3,
|
||||
"rms_dbfs": -26.4
|
||||
},
|
||||
"bird_2": {
|
||||
"sources": [
|
||||
"birds.ogg: 15..20 seconds"
|
||||
],
|
||||
"processing": "Natural distant birds, cut into independent calls; filtered noise; soft boundaries",
|
||||
"loop": false,
|
||||
"seconds": 5.0,
|
||||
"peak_dbfs": -3,
|
||||
"rms_dbfs": -26.81
|
||||
},
|
||||
"bird_3": {
|
||||
"sources": [
|
||||
"birds.ogg: 29..34 seconds"
|
||||
],
|
||||
"processing": "Natural distant birds, cut into independent calls; filtered noise; soft boundaries",
|
||||
"loop": false,
|
||||
"seconds": 1.688,
|
||||
"peak_dbfs": -3,
|
||||
"rms_dbfs": -28.16
|
||||
},
|
||||
"insects": {
|
||||
"sources": [
|
||||
"fauna\\amb_bird_cricket_frog\\amb_cricket_1.flac"
|
||||
],
|
||||
"processing": "Cricket recording, gentle filtering and envelope",
|
||||
"loop": false,
|
||||
"seconds": 0.189,
|
||||
"peak_dbfs": -3,
|
||||
"rms_dbfs": -22.47
|
||||
},
|
||||
"marsh_call": {
|
||||
"sources": [
|
||||
"fauna\\amb_bird_cricket_frog\\amb_frog_1.flac"
|
||||
],
|
||||
"processing": "Frog recording, slightly lower pitch, restrained high frequencies",
|
||||
"loop": false,
|
||||
"seconds": 1.002,
|
||||
"peak_dbfs": -3,
|
||||
"rms_dbfs": -20.25
|
||||
}
|
||||
}
|
||||
BIN
assets/sounds/surface/marsh_call.wav
Normal file
BIN
assets/sounds/surface/marsh_call.wav
Normal file
Binary file not shown.
24
assets/sounds/surface/marsh_call.wav.import
Normal file
24
assets/sounds/surface/marsh_call.wav.import
Normal file
@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://2vx15ylp8q73"
|
||||
path="res://.godot/imported/marsh_call.wav-b665b37674b38b960809bb228e05fa04.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/sounds/surface/marsh_call.wav"
|
||||
dest_files=["res://.godot/imported/marsh_call.wav-b665b37674b38b960809bb228e05fa04.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
BIN
assets/sounds/surface/wind_gust.wav
Normal file
BIN
assets/sounds/surface/wind_gust.wav
Normal file
Binary file not shown.
24
assets/sounds/surface/wind_gust.wav.import
Normal file
24
assets/sounds/surface/wind_gust.wav.import
Normal file
@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://3o485kn1htdt"
|
||||
path="res://.godot/imported/wind_gust.wav-1f5a67c623603ced8fc5edd474112e66.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/sounds/surface/wind_gust.wav"
|
||||
dest_files=["res://.godot/imported/wind_gust.wav-1f5a67c623603ced8fc5edd474112e66.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
BIN
assets/sounds/surface/wind_loop.wav
Normal file
BIN
assets/sounds/surface/wind_loop.wav
Normal file
Binary file not shown.
24
assets/sounds/surface/wind_loop.wav.import
Normal file
24
assets/sounds/surface/wind_loop.wav.import
Normal file
@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://5jnddv6jmvnb"
|
||||
path="res://.godot/imported/wind_loop.wav-f41edba26c9c125cf68cb264db7c3db9.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/sounds/surface/wind_loop.wav"
|
||||
dest_files=["res://.godot/imported/wind_loop.wav-f41edba26c9c125cf68cb264db7c3db9.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
@ -928,3 +928,92 @@ Realistyczne audio wnętrza fregaty (18.10.2026):
|
||||
sprzątanie po trzech dokowaniach), freighter_interior_preview.tscn.
|
||||
- Odsłuch demonstracyjny: artifacts/freighter_ambience_demo.wav (34 s,
|
||||
sam ambient i efekty, bez muzyki; poziom odsłuchu wyrównany osobno).
|
||||
|
||||
Naturalne złoża minerałów na powierzchni (18.10.2026):
|
||||
- Zamiast ikony tytanu POI deposit pokazuje odsłoniętą rudę w skałach,
|
||||
z drobnym rumoszem i ziemią przechodzącą w otaczające podłoże.
|
||||
- Grafika: assets/planets/mineral_deposit.png, przezroczyste RGBA,
|
||||
wbudowany image_gen. Pełny prompt: mineral_deposit_prompt.json.
|
||||
- Osobny statyczny Sprite2D ze skryptem surface_deposit.gd. Obrót
|
||||
i rozmiar są stałe dla ziarna mapy. Nie ma animacji połysku ani szumu
|
||||
zależnego od czasu/ekranu. Shader łagodnie wygasza otoczkę ziemi
|
||||
i dopasowuje jej kolor do biomu, zachowując czytelne żyły rudy.
|
||||
- Import lossless z mipmapami i poprawianiem kolorów przy krawędzi alfa;
|
||||
filtrowanie liniowe z mipmapami. Zachowano nadrzędny render 1.5x.
|
||||
- Odkrywanie, podpisy, interakcje i przyciemnienie zbadanego złoża
|
||||
pozostają obsługiwane przez dotychczasowy system POI.
|
||||
- surface_deposit_test przeszedł: render sześciu biomów, tekstura
|
||||
zamiast ikony, mipmapy, materiał przejścia, identyczne piksele przy
|
||||
nieruchomej kamerze i po powrocie do tej samej pozycji. Ruch kamery
|
||||
co 0.25 jednostki daje płynne zmiany (maks. średnia różnica RGB 0.00816).
|
||||
- Podglądy: artifacts/mineral_deposit_biome_0.png do _5.png oraz
|
||||
zbiorczy artifacts/mineral_deposit_biomes.png.
|
||||
|
||||
Ambient powierzchni planety (18.10.2026):
|
||||
- Dodano 13 plików 48 kHz/stereo/PCM16: wiatr w pętli i podmuch,
|
||||
szum roślinności, 3 szelesty liści, 2 skrzypnięcia/trzaski drewna,
|
||||
3 odgłosy ptaków, owady i odgłos mokradeł.
|
||||
- Bazą są rzeczywiste nagrania terenowe i foley, po filtrowaniu,
|
||||
usunięciu DC, wygładzeniu początków/końców i normalizacji.
|
||||
Pętle mają crossfade bez twardego przeskoku.
|
||||
- SurfaceAudio gra wyłącznie na powierzchni. Wiatr płynnie narasta
|
||||
po lądowaniu i zmienia natężenie. Silnik lekko maskuje ambient.
|
||||
- Szum liści zależy od liczby drzew w promieniu 560 jednostek.
|
||||
Pojedyncze szelesty i skrzypnięcia mają źródło przy rzeczywistych
|
||||
drzewach. Bez pobliskich drzew nie ma lokalnych odgłosów roślinności.
|
||||
- Fauna jest nieregularna i przestrzenna: mokradła mają żaby/owady,
|
||||
suchy biom owady, tundra rzadkie ptaki. Przy tlenie poniżej 12%
|
||||
fauna jest wyciszona. Chłodne/suche/skaliste biomy mają dłuższe przerwy.
|
||||
- Dwa odtwarzacze pętli i pula maksymalnie 3 efektów przestrzennych.
|
||||
Osobny słuchacz w świecie powierzchni podąża za lądownikiem.
|
||||
Dźwięki pracują podczas pauzy i są usuwane przy powrocie na statek.
|
||||
- Źródła/licencje: assets/sounds/surface/CREDITS.md; montaż: manifest.json.
|
||||
CC0: Thimras, qubodup, isaiah658, Rudmer_Rotteveel.
|
||||
Odgłosy owada/żaby Blender Foundation są CC BY 3.0; wymagane podpisy,
|
||||
odnośniki i opis zmian są również w instrukcji gry na stronie 07.
|
||||
- Narzędzia odtworzenia: tools/audio/fetch_surface_sources.py oraz
|
||||
build_surface_audio.py; wymagane numpy i soundfile.
|
||||
- Testy przeszły: surface_assets_test.py (format, DC, clipping, pętle),
|
||||
surface_audio_test.tscn (6 biomów, warunki fauny, lokalne drzewa,
|
||||
polifonia, pauza i sprzątanie), regresja planet_expeditions_test.tscn.
|
||||
- Odsłuch: artifacts/planet_ambience_demo.wav (32 s, bez muzyki,
|
||||
demonstracyjny miks odgłosów lasu i mokradeł, osobno wyrównany poziom).
|
||||
|
||||
AKTUALNE ZASADY POI — 2026-10-18 (zastepuja stare wspolne nagrody kopalni/ruin)
|
||||
- Mapa: 2 zloza, 1 starozytna struktura (25% na druga), 1 opuszczony magazyn,
|
||||
1 opuszczona wioska, 1 jaskinia; nadal dokladnie 1 wydarzenie ze znakiem zapytania.
|
||||
- Zloza: tylko krzem, miedz, nikiel, wolfram lub rtec. Ilosc rosnie z etapem i biomem.
|
||||
- Struktura: kazdy udany etap gwarantuje modul na poziomie ukladu +1/+2/+3,
|
||||
limit 1000. To jawny wyjatek od losowego bonusu biomu wydarzen ze znakiem zapytania.
|
||||
- Magazyn: etap 1: 40-70 amunicji; etap 2: 100-160 amunicji i 2-4 rakiety;
|
||||
etap 3: 180-260 amunicji + 4-7 rakiet LUB 30-50 amunicji radiacyjnej (50/50),
|
||||
zawsze dodatkowo 8-16 jednostek jednego surowca kosmicznego (mnoznik biomu x1..3).
|
||||
Amunicja i rakiety nie skaluja sie biomem.
|
||||
- Wioska: 30-60 / 80-140 / 220-360 kredytow. Etap 2 dodatkowo 10% brakujacego
|
||||
EXP do awansu (zaokraglenie w gore, stan przy sukcesie, bez mnoznikow bojowych).
|
||||
- Jaskinia: 1 / 2 / 4 probki, bez mnoznika biomu. Sumarycznie 7 za trzy etapy.
|
||||
- Nagrody kolejnych etapow dodaja sie. Dotychczasowe szanse i ryzyko pozostaja.
|
||||
- Caly lup, rowniez EXP i probki, przyznawany dopiero po udanym powrocie.
|
||||
Zniszczenie lądownika kasuje lup wyprawy; wczesniej dostarczone probki pozostaja.
|
||||
- analysis_samples oraz oczekujaca amunicja sa eksportowanymi polami PlanetProgress.
|
||||
Licznik probek widoczny w sekcji badan obcych i na mostku. Analiza pozniej.
|
||||
- PoiRules to wspolny katalog nazw, etapow, zapowiedzi nagrod i pul losowania.
|
||||
- Testy: poi_rewards_test, surface_poi_counts_test, planet_reward_scaling_test,
|
||||
planet_expeditions_test (w tym otwarcie wszystkich typow POI i dostawa amunicji).
|
||||
|
||||
OPRAWA POI I JASKINIE — 2026-10-18
|
||||
- Nowy atlas 15 ilustracji: 5 typow POI x 3 etapy. Grafika i krotka narracja
|
||||
zmieniaja sie przy przejsciu do nastepnego etapu. Zakonczony POI pokazuje ostatnia scene.
|
||||
- Karty terenowe: oliwkowy panel, cieple zlote akcenty, prosta typografia,
|
||||
ilustracja u gory, aktualna nagroda i szansa, maksymalnie 450x600 px.
|
||||
Mniejsze okna maja przewijanie; dotychczasowa mechanika nagrod i ryzyka pozostaje.
|
||||
- Jaskinia ma osobny sprite RGBA z mipmapami i statycznym shaderem dopasowania
|
||||
kamienia/gruntu do biomu. Brak animacji UV i szumu zależnego od ekranu.
|
||||
- Generator wybiera pobliska gore z zachowaniem odleglosci 420 od innych POI.
|
||||
Oznacza ja cave_anchor i zachowuje podczas czyszczenia otoczenia. Wlot lezy
|
||||
86 jednostek na poludnie od srodka zbocza. Jesli brakuje odpowiedniej gory,
|
||||
generator tworzy ja przy wybranej jaskini. Zmieniona tylko jej lokalizacja i oprawa.
|
||||
- Pliki grafik i pelne prompty: assets/planets/poi/PROMPTS.md.
|
||||
- Weryfikacja: poi_presentation_preview (15 kart, male okno),
|
||||
surface_poi_counts_test (64 mapy, zachowane kotwice gor), planet_expeditions_test.
|
||||
- Podglady: artifacts/cave_poi_window.png, cave_mountain_preview.png, poi_stage_cards.png.
|
||||
|
||||
12
resources/effects/cave_blend.gdshader
Normal file
12
resources/effects/cave_blend.gdshader
Normal file
@ -0,0 +1,12 @@
|
||||
shader_type canvas_item;
|
||||
uniform vec4 rock_tint : source_color;
|
||||
uniform vec4 ground_tint : source_color;
|
||||
void fragment() {
|
||||
vec4 source = texture(TEXTURE,UV);
|
||||
float brightness = dot(source.rgb,vec3(0.299,0.587,0.114));
|
||||
float rim = smoothstep(0.28,0.49,length((UV-vec2(0.5))*vec2(1.0,0.85)));
|
||||
vec3 rock = mix(source.rgb,rock_tint.rgb*(brightness+0.45),0.20);
|
||||
rock = mix(rock,ground_tint.rgb*(brightness+0.5),rim*0.25);
|
||||
float alpha = smoothstep(0.08,0.90,source.a);
|
||||
COLOR = vec4(rock,alpha);
|
||||
}
|
||||
1
resources/effects/cave_blend.gdshader.uid
Normal file
1
resources/effects/cave_blend.gdshader.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://jg0iy1tctuwp
|
||||
@ -33,7 +33,9 @@ const PLANET_TEXTURES: Array[Texture2D] = [
|
||||
preload("res://assets/planet_sun/Gaseous/Green Giant/GreenGiant_04-512x512.png"),
|
||||
preload("res://assets/planet_sun/Gaseous/Green Giant/GreenGiant_05-512x512.png"),
|
||||
preload("res://assets/planet_sun/Gaseous/Orange Giant/OrgangeGiant_01-512x512.png"),
|
||||
preload("res://assets/planet_sun/Gaseous/Orange Giant/OrgangeGiant_02-512x512.png"),
|
||||
# Variant 02 is actually 128x128 despite its filename. Use the full-resolution
|
||||
# sibling, keeping this slot so existing system seeds retain their other planets.
|
||||
preload("res://assets/planet_sun/Gaseous/Orange Giant/OrgangeGiant_01-512x512.png"),
|
||||
preload("res://assets/planet_sun/Gaseous/Orange Giant/OrgangeGiant_03-512x512.png"),
|
||||
preload("res://assets/planet_sun/Gaseous/Orange Giant/OrgangeGiant_04-512x512.png"),
|
||||
preload("res://assets/planet_sun/Gaseous/Orange Giant/OrgangeGiant_05-512x512.png"),
|
||||
|
||||
@ -241,7 +241,7 @@ func finish_surface(success: bool) -> void:
|
||||
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
|
||||
if int(result.get("credits",0)) > 0: returned.credits = int(returned.get("credits",0))+int(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():
|
||||
|
||||
@ -46,7 +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"})
|
||||
names.merge({"parts":"Części", "missile":"Rakiety", "credits":"Kredyty", "experience":"EXP", "samples":"Próbki do analizy", "ammunition":"Amunicja", "irradiated_ammunition":"Amunicja radiacyjna"})
|
||||
var pieces: PackedStringArray = []
|
||||
for key in materials:
|
||||
pieces.append("%s: %d" % [names.get(key, key), materials[key]])
|
||||
|
||||
@ -179,6 +179,9 @@ func _build_room() -> void:
|
||||
var plan: Dictionary = deck.plan(_tab)
|
||||
_label("ZBUDOWANO" if deck.is_built(_tab) else "W REMONCIE",21,UI.CYAN if deck.is_built(_tab) else Color("e0b16a"))
|
||||
_label(str(plan.description))
|
||||
if str(plan.kind) == "alien_research":
|
||||
_label("Próbki oczekujące na analizę: %d" % captain.expedition.analysis_samples,16,UI.CYAN)
|
||||
_label("Próbki z jaskiń są przechowywane po powrocie. Analiza będzie dostępna później.",13)
|
||||
if deck.is_built(_tab):
|
||||
_label("Pomieszczenie jest wyposażone. Jego mechanika zostanie dodana w kolejnym etapie.",14,Color("849da5"))
|
||||
else:
|
||||
@ -352,6 +355,7 @@ func _build_command() -> void:
|
||||
_label("MOSTEK / SYSTEMY NAWIGACYJNE",13,Color("8bd9d0"))
|
||||
_label("Łączność z twoim statkiem jest stabilna. Fregata pozostaje gotowa do przejścia do kolejnego układu.")
|
||||
_label("Zeskanowane planety: %d" % progress.scans.size(),16)
|
||||
_label("Próbki do analizy w dziale badań obcych: %d" % progress.analysis_samples,15,UI.CYAN)
|
||||
_label("ODZYSKANE ARCHIWA",15,UI.CYAN)
|
||||
if progress.research_records.is_empty():
|
||||
_label("Brak odzyskanych zapisów.",13,Color("819ca5"))
|
||||
|
||||
@ -10,6 +10,9 @@ const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
|
||||
@export var freighter_rooms: Dictionary = {}
|
||||
@export var freighter_storage: Array[Resource] = []
|
||||
@export var delivered_missiles := 0
|
||||
@export var analysis_samples := 0
|
||||
@export var delivered_ammunition := 0
|
||||
@export var delivered_irradiated_ammunition := 0
|
||||
var cargo_credits := 0
|
||||
var cargo_parts := 0
|
||||
var cargo_missiles := 0
|
||||
@ -79,23 +82,14 @@ func resolve_poi(captain: Resource, poi: Dictionary, rng: RandomNumberGenerator)
|
||||
success = rng.randf() < chance
|
||||
result.success = success
|
||||
if success:
|
||||
var difficulty := float(active_profile.difficulty)
|
||||
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)]
|
||||
var amount := maxi(1, roundi(rng.randi_range(5, 12) * multiplier))
|
||||
add_material(key, amount)
|
||||
result.materials[key] = amount
|
||||
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 item: ItemInstance = REWARDS.generate_module(rng, active_profile, GameState.system_level, definitions[rng.randi_range(0, definitions.size()-1)], 1 if difficulty > 0.5 or stage >= 2 else 0)
|
||||
var reward: Dictionary = preload("res://scripts/planets/poi_rules.gd").reward(str(poi.kind),stage,rng,active_profile,captain)
|
||||
for key in reward:
|
||||
add_material(key,int(reward[key]))
|
||||
result.materials = reward
|
||||
if str(poi.kind) == "ruins":
|
||||
var item: ItemInstance = REWARDS.generate_module(rng,active_profile,GameState.system_level,null,0,stage+1)
|
||||
cargo_modules.append(item)
|
||||
result.module = item
|
||||
if str(poi.kind) == "ruins":
|
||||
var record := "Archiwum %s" % str(active_profile.name)
|
||||
cargo_records[record] = int(cargo_records.get(record, 0)) + 1
|
||||
poi.stage = stage + 1
|
||||
if int(poi.stage) >= 3:
|
||||
poi.complete = true
|
||||
@ -114,10 +108,15 @@ func finish_visit(captain: Resource, success: bool) -> Dictionary:
|
||||
success = success and lander_hp > 0
|
||||
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.add_rewards(int(cargo.get("experience",0)),cargo_credits+int(cargo.get("credits",0)))
|
||||
analysis_samples += int(cargo.get("samples",0))
|
||||
delivered_ammunition += int(cargo.get("ammunition",0))
|
||||
delivered_irradiated_ammunition += int(cargo.get("irradiated_ammunition",0))
|
||||
delivered_missiles += int(cargo.get("missile",0))
|
||||
captain.parts += cargo_parts
|
||||
delivered_missiles += cargo_missiles
|
||||
for key in cargo:
|
||||
if key in ["credits","experience","samples","ammunition","irradiated_ammunition","missile"]: continue
|
||||
captain.minerals[key] = int(captain.minerals.get(key, 0)) + int(cargo[key])
|
||||
delivered_modules.append_array(cargo_modules)
|
||||
for key in cargo_records:
|
||||
@ -136,6 +135,13 @@ func finish_visit(captain: Resource, success: bool) -> Dictionary:
|
||||
return result
|
||||
|
||||
func deliver_modules(inventory: Node) -> int:
|
||||
if delivered_ammunition > 0 or delivered_irradiated_ammunition > 0:
|
||||
inventory.ammunition += delivered_ammunition
|
||||
inventory.irradiated_ammunition += delivered_irradiated_ammunition
|
||||
delivered_ammunition = 0
|
||||
delivered_irradiated_ammunition = 0
|
||||
inventory.ammunition_changed.emit()
|
||||
inventory.inventory_changed.emit()
|
||||
if delivered_missiles > 0:
|
||||
inventory.missile_ammunition += delivered_missiles
|
||||
delivered_missiles = 0
|
||||
|
||||
@ -26,12 +26,12 @@ static func roll_module_bonus(rng: RandomNumberGenerator, profile: Dictionary) -
|
||||
return bounds.x+index
|
||||
return bounds.y
|
||||
|
||||
static func generate_module(rng: RandomNumberGenerator, profile: Dictionary, level: int, definition: ItemDefinition = null, tier := 0) -> ItemInstance:
|
||||
static func generate_module(rng: RandomNumberGenerator, profile: Dictionary, level: int, definition: ItemDefinition = null, tier := 0, exact_bonus := -1) -> 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.
|
||||
var bonus := exact_bonus if exact_bonus >= 0 else roll_module_bonus(rng,profile)
|
||||
# Events use biome bonuses; ancient structures explicitly set +1/+2/+3.
|
||||
return ITEMS.generate_for_definition(definition,clampi(level+bonus,1,1000),tier)
|
||||
|
||||
static func scale_materials(reward: Dictionary, profile: Dictionary) -> Dictionary:
|
||||
|
||||
89
scripts/planets/poi_presentation.gd
Normal file
89
scripts/planets/poi_presentation.gd
Normal file
@ -0,0 +1,89 @@
|
||||
extends RefCounted
|
||||
## Expedition field cards: warm canvas, olive paint, brass and scene art.
|
||||
const RULES := preload("res://scripts/planets/poi_rules.gd")
|
||||
const ORDER := ["deposit","ruins","warehouse","village","cave"]
|
||||
const INK := Color("dfd8c3")
|
||||
const MUTED := Color("9eaa91")
|
||||
const GOLD := Color("d6b477")
|
||||
const STORIES := {
|
||||
"deposit":["Wśród zwietrzałych skał połyskują drobiny rzadkich minerałów. Wystarczy ostrożnie oddzielić je od podłoża.","Pod cienką warstwą gruntu żyła schodzi głębiej. Odwiert odsłoni bogatszy pokład, jeśli skała pozostanie stabilna.","Wąska szczelina prowadzi do najbogatszej części złoża. Każde uderzenie narzędzia odbija się głuchym echem."],
|
||||
"ruins":["Korzenie rozsunęły kamienie przy wejściu. Za progiem spoczywa porzucone urządzenie, pokryte pyłem minionych wieków.","Ciężkie drzwi ustępują. W niszy pośród zatartych reliefów czeka kolejny fragment dawnej technologii.","W sercu budowli ocalał niezwykły mechanizm. Aby go wydobyć, trzeba poruszyć konstrukcję, która od dawna nie powinna stać."],
|
||||
"warehouse":["Wiatr przesuwa kurz po podłodze pustego magazynu. Kilka skrzyń wciąż ma nienaruszone zamknięcia.","Za stalową kratą zachowały się regały z wojskowym zaopatrzeniem. Rdza utrudnia dostęp do ciężkich pojemników.","Ostatnie zapasy ukryto pod zawaloną częścią budynku. Belki skrzypią przy każdym kroku."],
|
||||
"village":["Między pustymi domami zarosła dawna ścieżka. W pozostawionych schowkach mogą jeszcze leżeć drobne oszczędności.","Na stole ocalały dzienniki mieszkańców. Ich notatki mogą nauczyć cię, jak przetrwać na tej planecie.","Pod zapadniętą podłogą kryje się sejf osady. Dotarcie do niego wymaga wejścia pod niestabilne stropy."],
|
||||
"cave":["U podnóża góry otwiera się ciemna szczelina. Przy wilgotnych skałach rosną organizmy warte dokładniejszej analizy.","Korytarz rozszerza się w chłodną grotę. Na wapiennych ścianach widać kolejne skupiska nieznanych form życia.","Daleko od światła powierzchni leży podziemny zbiornik. Wokół niego przetrwały najcenniejsze próbki — i najtrudniejsza droga powrotna."]}
|
||||
|
||||
static func panel_style(panel: PanelContainer) -> void:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color("22271f")
|
||||
style.border_color = Color("756d50")
|
||||
style.set_border_width_all(1)
|
||||
style.set_corner_radius_all(8)
|
||||
style.set_content_margin_all(14)
|
||||
style.shadow_color = Color(0,0,0,0.45)
|
||||
style.shadow_size = 14
|
||||
panel.add_theme_stylebox_override("panel",style)
|
||||
|
||||
static func text(parent: Node, content: String, font_size := 14, color := INK) -> Label:
|
||||
var label := Label.new()
|
||||
label.text = content
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.add_theme_font_size_override("font_size",font_size)
|
||||
label.add_theme_font_override("font",ThemeDB.fallback_font)
|
||||
label.add_theme_color_override("font_color",color)
|
||||
parent.add_child(label)
|
||||
return label
|
||||
|
||||
static func button(parent: Node, content: String, action: Callable, primary := false) -> Button:
|
||||
var node := Button.new()
|
||||
node.text = content
|
||||
node.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
node.custom_minimum_size.y = 36
|
||||
node.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
node.add_theme_font_size_override("font_size",14)
|
||||
node.add_theme_font_override("font",ThemeDB.fallback_font)
|
||||
node.add_theme_color_override("font_color",INK)
|
||||
for state in ["normal","hover","pressed","disabled","focus"]:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color("4b5639") if primary else Color("30382b")
|
||||
if state == "hover": style.bg_color = Color("626b46")
|
||||
if state == "pressed": style.bg_color = Color("3c442c")
|
||||
if state == "disabled": style.bg_color = Color("292c25")
|
||||
style.border_color = GOLD if primary else Color("687054")
|
||||
style.set_border_width_all(1)
|
||||
style.set_corner_radius_all(4)
|
||||
style.content_margin_left = 10
|
||||
style.content_margin_right = 10
|
||||
style.content_margin_top = 7
|
||||
style.content_margin_bottom = 7
|
||||
node.add_theme_stylebox_override(state,style)
|
||||
node.pressed.connect(action)
|
||||
parent.add_child(node)
|
||||
return node
|
||||
|
||||
static func header(parent: VBoxContainer, kind: String, stage: int, complete: bool) -> void:
|
||||
var atlas: Texture2D = load("res://assets/planets/poi/expedition_atlas.png")
|
||||
var image := AtlasTexture.new()
|
||||
image.atlas = atlas
|
||||
# Painted atlas rows have slightly different heights; crop inside each scene.
|
||||
var row := ORDER.find(kind)
|
||||
var rows := [0.0,0.198,0.394,0.589,0.776,1.0]
|
||||
var extent := Vector2(atlas.get_size())
|
||||
image.region = Rect2(Vector2(stage/3.0,rows[row])*extent+Vector2(3,3),Vector2(extent.x/3.0,(rows[row+1]-rows[row])*extent.y)-Vector2(6,6))
|
||||
image.filter_clip = true
|
||||
var frame := PanelContainer.new()
|
||||
frame.custom_minimum_size.y = 164
|
||||
frame.clip_contents = true
|
||||
frame.add_theme_stylebox_override("panel",StyleBoxEmpty.new())
|
||||
var art := TextureRect.new()
|
||||
art.texture = image
|
||||
art.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
art.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
|
||||
art.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
|
||||
frame.add_child(art)
|
||||
parent.add_child(frame)
|
||||
text(parent,"DZIENNIK WYPRAWY / %s" % ("ZAKOŃCZONO" if complete else "ETAP %02d / 03" % (stage+1)),11,MUTED)
|
||||
text(parent,str(RULES.NAMES[kind]),23,GOLD)
|
||||
text(parent,STORIES[kind][stage],14)
|
||||
var line := HSeparator.new()
|
||||
line.modulate = Color("7b8062")
|
||||
parent.add_child(line)
|
||||
1
scripts/planets/poi_presentation.gd.uid
Normal file
1
scripts/planets/poi_presentation.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://3fsphx7vbodt
|
||||
34
scripts/planets/poi_rules.gd
Normal file
34
scripts/planets/poi_rules.gd
Normal file
@ -0,0 +1,34 @@
|
||||
extends RefCounted
|
||||
const NAMES := {"deposit":"Złoże surowców", "ruins":"Starożytna struktura", "warehouse":"Opuszczony magazyn", "village":"Opuszczona wioska", "cave":"Jaskinia"}
|
||||
const STAGES := {
|
||||
"deposit":["Zbierz odsłonięte minerały", "Wykonaj odwiert", "Wydobądź głęboką żyłę"],
|
||||
"ruins":["Przeszukaj przedsionek", "Otwórz komorę", "Zbadaj rdzeń"],
|
||||
"warehouse":["Przeszukaj skrzynie", "Otwórz zbrojownię", "Odblokuj skarbiec zaopatrzenia"],
|
||||
"village":["Przeszukaj domy", "Odzyskaj dzienniki mieszkańców", "Otwórz sejf osady"],
|
||||
"cave":["Zbadaj wejście", "Zejdź do groty", "Pobierz próbki z głębi"]}
|
||||
const PREVIEWS := {
|
||||
"deposit":["Rzadkie surowce planetarne", "Więcej rzadkich surowców", "Dużo rzadkich surowców"],
|
||||
"ruins":["Moduł: poziom układu +1", "Moduł: poziom układu +2", "Moduł: poziom układu +3"],
|
||||
"warehouse":["40–70 amunicji", "100–160 amunicji i 2–4 rakiety", "180–260 amunicji, 4–7 rakiet lub 30–50 amunicji radiacyjnej oraz surowce kosmiczne"],
|
||||
"village":["30–60 kredytów", "80–140 kredytów + 10% brakującego EXP do awansu", "220–360 kredytów"],
|
||||
"cave":["1 próbka do analizy", "2 próbki do analizy", "4 próbki do analizy"]}
|
||||
|
||||
static func reward(kind: String, stage: int, rng: RandomNumberGenerator, profile: Dictionary, captain: Resource) -> Dictionary:
|
||||
var result := {}
|
||||
match kind:
|
||||
"deposit":
|
||||
var keys: Array = preload("res://scripts/planets/planet_profiles.gd").MATERIALS
|
||||
result[keys[rng.randi_range(0,keys.size()-1)]] = roundi(rng.randi_range(5,12)*(1.0+stage*0.65)*preload("res://scripts/planets/planet_rewards.gd").material_multiplier(profile))
|
||||
"warehouse":
|
||||
result.ammunition = rng.randi_range([40,100,180][stage],[70,160,260][stage])
|
||||
if stage == 1: result.missile = rng.randi_range(2,4)
|
||||
if stage == 2:
|
||||
if rng.randf() < 0.5: result.missile = rng.randi_range(4,7)
|
||||
else: result.irradiated_ammunition = rng.randi_range(30,50)
|
||||
var keys := ["iron","titanium","aluminium","uranium","sulfur"]
|
||||
result[keys[rng.randi_range(0,4)]] = roundi(rng.randi_range(8,16)*preload("res://scripts/planets/planet_rewards.gd").material_multiplier(profile))
|
||||
"village":
|
||||
result.credits = rng.randi_range([30,80,220][stage],[60,140,360][stage])
|
||||
if stage == 1: result.experience = maxi(1,ceili(maxi(0,captain.get_xp_required()-captain.experience)*0.1))
|
||||
"cave": result.samples = [1,2,4][stage]
|
||||
return result
|
||||
1
scripts/planets/poi_rules.gd.uid
Normal file
1
scripts/planets/poi_rules.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cn5ho6hee8q2u
|
||||
138
scripts/planets/surface_audio.gd
Normal file
138
scripts/planets/surface_audio.gd
Normal file
@ -0,0 +1,138 @@
|
||||
extends Node
|
||||
## Wind beds plus sparse, localised foliage and wildlife field recordings.
|
||||
const ROOT := "res://assets/sounds/surface/"
|
||||
const VOICE_LIMIT := 3
|
||||
const WIND_DB := [-18.0,-17.0,-16.0,-13.0,-12.0,-14.0]
|
||||
var source: Control
|
||||
var _streams: Dictionary = {}
|
||||
var _wind: AudioStreamPlayer
|
||||
var _foliage: AudioStreamPlayer
|
||||
var _world_audio: Node2D
|
||||
var _listener: AudioListener2D
|
||||
var _voices: Array[AudioStreamPlayer2D] = []
|
||||
var _rng := RandomNumberGenerator.new()
|
||||
var _elapsed := 0.0
|
||||
var _local_refresh := 0.0
|
||||
var _near_trees: Array[Vector2] = []
|
||||
var _density := 0.0
|
||||
var _biome := 0
|
||||
var _oxygen := 100.0
|
||||
var _next_gust := 10.0
|
||||
var _next_leaves := 6.0
|
||||
var _next_creak := 20.0
|
||||
var _next_fauna := 12.0
|
||||
var _last_leaf := ""
|
||||
var _last_bird := ""
|
||||
var played_count := 0
|
||||
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
_biome = int(source.world.data.landscape.index)
|
||||
_oxygen = float(source.progress.active_profile.get("oxygen",100))
|
||||
_rng.seed = int(source.progress.visit_seed)^0x4e415455
|
||||
for id in ["wind_loop","wind_gust","foliage_loop","leaves_1","leaves_2","leaves_3","branch_creak_1","branch_creak_2","bird_1","bird_2","bird_3","insects","marsh_call"]:
|
||||
_streams[id] = load(ROOT+id+".wav")
|
||||
_wind = _bed("wind_loop")
|
||||
_foliage = _bed("foliage_loop")
|
||||
_world_audio = Node2D.new()
|
||||
_world_audio.name = "NatureAudioWorld"
|
||||
_world_audio.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
source._surface_viewport.audio_listener_enable_2d = true
|
||||
source._surface_viewport.add_child(_world_audio)
|
||||
_listener = AudioListener2D.new()
|
||||
_world_audio.add_child(_listener)
|
||||
_listener.make_current()
|
||||
for index in VOICE_LIMIT:
|
||||
var voice := AudioStreamPlayer2D.new()
|
||||
voice.name = "NatureVoice%d" % index
|
||||
voice.max_distance = 1050
|
||||
voice.attenuation = 1.1
|
||||
voice.panning_strength = 0.65
|
||||
_world_audio.add_child(voice)
|
||||
_voices.append(voice)
|
||||
_refresh_local()
|
||||
_next_gust = _rng.randf_range(8,16)
|
||||
_next_fauna = _rng.randf_range(7,15)
|
||||
|
||||
func _bed(id: String) -> AudioStreamPlayer:
|
||||
var bed := AudioStreamPlayer.new()
|
||||
var stream := (_streams[id] as AudioStreamWAV).duplicate() as AudioStreamWAV
|
||||
stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||
stream.loop_end = roundi(stream.get_length()*stream.mix_rate)
|
||||
bed.stream = stream
|
||||
bed.volume_db = -48
|
||||
add_child(bed)
|
||||
bed.play()
|
||||
return bed
|
||||
|
||||
func _refresh_local() -> void:
|
||||
_near_trees.clear()
|
||||
for tree in source.world.data.trees:
|
||||
if source.lander.position.distance_squared_to(tree.position) < 560.0*560.0:
|
||||
_near_trees.append(tree.position)
|
||||
_density = clampf(float(_near_trees.size())/22.0,0,1)
|
||||
|
||||
func fauna_choices() -> Array[String]:
|
||||
if _oxygen < 12: return []
|
||||
match _biome:
|
||||
2: return ["marsh_call","insects","bird_2"]
|
||||
4: return ["insects"]
|
||||
5: return ["bird_3"]
|
||||
_: return ["bird_1","bird_2","bird_3","insects"]
|
||||
|
||||
func _play(id: String, position: Vector2, level: float) -> bool:
|
||||
for voice in _voices:
|
||||
if voice.playing: continue
|
||||
voice.stream = _streams[id]
|
||||
voice.position = position
|
||||
voice.volume_db = level
|
||||
voice.pitch_scale = _rng.randf_range(0.95,1.03)
|
||||
voice.play()
|
||||
played_count += 1
|
||||
return true
|
||||
return false
|
||||
|
||||
func _tree_position() -> Vector2:
|
||||
return _near_trees[_rng.randi_range(0,_near_trees.size()-1)]
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not is_instance_valid(source) or not is_instance_valid(_listener): return
|
||||
_elapsed += delta
|
||||
_local_refresh += delta
|
||||
_listener.position = source.lander.position
|
||||
if _local_refresh > 0.65:
|
||||
_local_refresh = 0
|
||||
_refresh_local()
|
||||
var gust := sin(_elapsed*0.19)*1.4+sin(_elapsed*0.073)*0.8
|
||||
var engine_mask := -3.0 if source.engine_thrust_active() else 0.0
|
||||
_wind.volume_db = move_toward(_wind.volume_db,WIND_DB[_biome]+gust+engine_mask,delta*26)
|
||||
var leaf_target := -60.0 if _near_trees.is_empty() else lerpf(-34,-23,_density)+gust+engine_mask
|
||||
_foliage.volume_db = move_toward(_foliage.volume_db,leaf_target,delta*8)
|
||||
if _elapsed >= _next_gust:
|
||||
_play("wind_gust",_listener.position+Vector2(_rng.randf_range(-250,250),-120),-23.0 if _biome < 3 else -20.0)
|
||||
_next_gust = _elapsed+_rng.randf_range(18,38)
|
||||
if _elapsed >= _next_leaves:
|
||||
if not _near_trees.is_empty():
|
||||
var choices := ["leaves_1","leaves_2","leaves_3"]
|
||||
choices.erase(_last_leaf)
|
||||
_last_leaf = choices[_rng.randi_range(0,choices.size()-1)]
|
||||
_play(_last_leaf,_tree_position(),-14.0)
|
||||
_next_leaves = _elapsed+_rng.randf_range(9,20)
|
||||
if _elapsed >= _next_creak:
|
||||
if not _near_trees.is_empty(): _play("branch_creak_%d" % _rng.randi_range(1,2),_tree_position(),-17.0)
|
||||
_next_creak = _elapsed+_rng.randf_range(26,52)
|
||||
if _elapsed >= _next_fauna:
|
||||
var choices := fauna_choices()
|
||||
if choices.size() > 1: choices.erase(_last_bird)
|
||||
if not choices.is_empty():
|
||||
_last_bird = choices[_rng.randi_range(0,choices.size()-1)]
|
||||
var at := _listener.position+Vector2.from_angle(_rng.randf_range(-PI,PI))*_rng.randf_range(200,520)
|
||||
_play(_last_bird,at,-15.0 if _biome < 3 else -19.0)
|
||||
_next_fauna = _elapsed+_rng.randf_range(15,33)*(2.0 if _biome in [3,4,5] else 1.0)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if is_instance_valid(_wind): _wind.stop()
|
||||
if is_instance_valid(_foliage): _foliage.stop()
|
||||
for voice in _voices:
|
||||
if is_instance_valid(voice): voice.stop()
|
||||
if is_instance_valid(_world_audio): _world_audio.queue_free()
|
||||
1
scripts/planets/surface_audio.gd.uid
Normal file
1
scripts/planets/surface_audio.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://2216ruwyi354
|
||||
16
scripts/planets/surface_cave.gd
Normal file
16
scripts/planets/surface_cave.gd
Normal file
@ -0,0 +1,16 @@
|
||||
extends Sprite2D
|
||||
## Static artwork attached to a preserved mountain foothill.
|
||||
const TEXTURE := preload("res://assets/planets/poi/cave_entrance.png")
|
||||
func configure(map: Dictionary, poi: Dictionary) -> void:
|
||||
name = "MountainCave%d" % int(poi.id)
|
||||
texture = TEXTURE
|
||||
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
|
||||
position = poi.position
|
||||
scale = Vector2.ONE*195.0/TEXTURE.get_width()
|
||||
offset = Vector2(0,-TEXTURE.get_height()*0.10)
|
||||
z_index = 10
|
||||
var blend := ShaderMaterial.new()
|
||||
blend.shader = preload("res://resources/effects/cave_blend.gdshader")
|
||||
blend.set_shader_parameter("rock_tint",map.landscape.rock)
|
||||
blend.set_shader_parameter("ground_tint",(map.landscape.low as Color).lerp(map.landscape.high,0.4))
|
||||
material = blend
|
||||
1
scripts/planets/surface_cave.gd.uid
Normal file
1
scripts/planets/surface_cave.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://w250kxxkj7x4
|
||||
@ -49,8 +49,7 @@ static func generate(seed_value: int, profile: Dictionary) -> Dictionary:
|
||||
if point.length() > 200 and heights[gy*GRID+gx] > 0.30:
|
||||
rocks.append({"position":point,"size":rng.randf_range(14,45),"rotation":rng.randf_range(-PI,PI)})
|
||||
var pois: Array[Dictionary] = []
|
||||
var kinds: Array[String] = ["mine","ruins","deposit","deposit"]
|
||||
if rng.randf() < EXTRA_STRUCTURE_CHANCE: kinds.append("mine")
|
||||
var kinds: Array[String] = ["warehouse","ruins","deposit","deposit","village","cave"]
|
||||
if rng.randf() < EXTRA_STRUCTURE_CHANCE: kinds.append("ruins")
|
||||
for index in kinds.size():
|
||||
var kind: String = kinds[index]
|
||||
@ -75,10 +74,11 @@ static func generate(seed_value: int, profile: Dictionary) -> Dictionary:
|
||||
# A lander can cross every terrain; a remote fallback never traps it.
|
||||
chosen = Vector2.from_angle(index * TAU / kinds.size()) * 1250
|
||||
pois.append({"id": index, "kind": kind, "position": chosen, "stage": 0, "complete": false, "discovered": false})
|
||||
_attach_caves(pois,mountains)
|
||||
# Clear vegetation around explorable structures so entrances stay readable.
|
||||
for poi in pois:
|
||||
trees = trees.filter(func(prop): return prop.position.distance_to(poi.position) > 100)
|
||||
mountains = mountains.filter(func(prop): return prop.position.distance_to(poi.position) > 120)
|
||||
mountains = mountains.filter(func(prop): return bool(prop.get("cave_anchor",false)) or prop.position.distance_to(poi.position) > 120)
|
||||
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)
|
||||
@ -86,6 +86,29 @@ static func generate(seed_value: int, profile: Dictionary) -> Dictionary:
|
||||
preload("res://scripts/planets/surface_details.gd").generate(data)
|
||||
return data
|
||||
|
||||
static func _attach_caves(pois: Array[Dictionary], mountains: Array[Dictionary]) -> void:
|
||||
for poi in pois:
|
||||
if poi.kind != "cave": continue
|
||||
var best := -1
|
||||
var distance := INF
|
||||
for index in mountains.size():
|
||||
var entrance: Vector2 = mountains[index].position+Vector2(0,86)
|
||||
if absf(entrance.x) > 1700 or absf(entrance.y) > 1700 or entrance.length() < 400: continue
|
||||
var clear := true
|
||||
for other in pois:
|
||||
if other.id != poi.id and entrance.distance_to(other.position) < 420: clear = false
|
||||
if clear and entrance.distance_squared_to(poi.position) < distance:
|
||||
best = index
|
||||
distance = entrance.distance_squared_to(poi.position)
|
||||
if best < 0:
|
||||
mountains.append({"position":poi.position-Vector2(0,86),"size":285.0,"rotation":0.0,"cave_anchor":true})
|
||||
best = mountains.size()-1
|
||||
mountains[best].cave_anchor = true
|
||||
mountains[best].rotation = 0.0
|
||||
mountains[best].size = 285.0
|
||||
poi.position = mountains[best].position+Vector2(0,86)
|
||||
poi.mountain_position = mountains[best].position
|
||||
|
||||
static func terrain_texture(data: Dictionary) -> ImageTexture:
|
||||
# Only the minimap is baked. The world uses full-resolution GPU terrain shading.
|
||||
var image := Image.create(256,256,false,Image.FORMAT_RGB8)
|
||||
|
||||
@ -4,6 +4,7 @@ const UI := preload("res://scripts/planets/expedition_ui.gd")
|
||||
const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
|
||||
const EVENTS := preload("res://scripts/planets/surface_events.gd")
|
||||
const GENERATOR := preload("res://scripts/planets/surface_generator.gd")
|
||||
const POI_CARD := preload("res://scripts/planets/poi_presentation.gd")
|
||||
var captain: Resource
|
||||
var progress: Resource
|
||||
var world: Node2D
|
||||
@ -91,6 +92,11 @@ func _ready() -> void:
|
||||
atmosphere.set_script(preload("res://scripts/planets/surface_clouds.gd"))
|
||||
atmosphere.seed_value = progress.visit_seed
|
||||
viewport.add_child(atmosphere)
|
||||
var nature_audio := Node.new()
|
||||
nature_audio.name = "SurfaceAudio"
|
||||
nature_audio.set_script(preload("res://scripts/planets/surface_audio.gd"))
|
||||
nature_audio.source = self
|
||||
add_child(nature_audio)
|
||||
resized.connect(_resize_surface_viewport)
|
||||
var info := UI.panel(self, Vector2(440, 0))
|
||||
info.position = Vector2(24, 24)
|
||||
@ -213,28 +219,27 @@ func _show_poi(message := "") -> void:
|
||||
_choices.remove_child(child)
|
||||
child.queue_free()
|
||||
_dialog.show()
|
||||
var names := {"mine":"OPUSZCZONA KOPALNIA", "ruins":"STAROŻYTNE RUINY", "deposit":"ZŁOŻE MINERAŁÓW"}
|
||||
UI.label(_choices, names[_poi.kind], 22, UI.CYAN)
|
||||
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]
|
||||
POI_CARD.panel_style(_dialog)
|
||||
_choices.add_theme_constant_override("separation",8)
|
||||
_layout_dialog()
|
||||
var stage := clampi(int(_poi.stage),0,2)
|
||||
POI_CARD.header(_choices,str(_poi.kind),stage,bool(_poi.complete))
|
||||
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
|
||||
POI_CARD.text(_choices,message,13,POI_CARD.GOLD)
|
||||
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 := 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)
|
||||
var stages: Dictionary = preload("res://scripts/planets/poi_rules.gd").STAGES
|
||||
POI_CARD.text(_choices,"DO ODZYSKANIA",11,POI_CARD.MUTED)
|
||||
POI_CARD.text(_choices,preload("res://scripts/planets/poi_rules.gd").PREVIEWS[_poi.kind][stage],14,POI_CARD.GOLD)
|
||||
POI_CARD.text(_choices,"Powodzenie %.1f%% · Zagrożenie %.1f%% / 300%%" % [chance*100,report.total],12,POI_CARD.MUTED)
|
||||
POI_CARD.text(_choices,"Porażka uszkadza lądownik i zamyka to miejsce. Łup zachowasz po powrocie.",12,POI_CARD.MUTED)
|
||||
var attempt := POI_CARD.button(_choices,str(stages[_poi.kind][stage]),_attempt,true)
|
||||
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())
|
||||
POI_CARD.button(_choices,"Zakończ badanie tego miejsca",func(): _poi.complete = true; _dialog.hide())
|
||||
POI_CARD.button(_choices, "Wróć do wyprawy", func(): _dialog.hide())
|
||||
|
||||
func _attempt() -> void:
|
||||
if _ending:
|
||||
@ -380,9 +385,10 @@ 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)
|
||||
var width := minf(450,maxf(280,view.x-32))
|
||||
var height := minf(600,maxf(180,view.y-48))
|
||||
_dialog.size = Vector2(width,height)
|
||||
_dialog.position = Vector2(view.x-width-20,maxf(16,(view.y-height)*0.5))
|
||||
|
||||
func _resize_surface_viewport() -> void:
|
||||
_layout_dialog()
|
||||
|
||||
@ -60,8 +60,14 @@ func _ready() -> void:
|
||||
sprite.set_script(preload("res://scripts/planets/surface_deposit.gd"))
|
||||
sprite.configure(data,poi)
|
||||
else:
|
||||
sprite.texture = load("res://assets/planets/%s.png" % ("ruins" if poi.kind == "ruins" else "mine"))
|
||||
if poi.kind == "cave":
|
||||
sprite.set_script(preload("res://scripts/planets/surface_cave.gd"))
|
||||
sprite.configure(data,poi)
|
||||
else:
|
||||
var textures := {"ruins":"ruins", "warehouse":"props/warehouse", "village":"landmarks/abandoned_hamlet"}
|
||||
sprite.texture = load("res://assets/planets/%s.png" % textures[poi.kind])
|
||||
sprite.position = poi.position
|
||||
if sprite.texture != null and poi.kind != "cave":
|
||||
sprite.scale = Vector2.ONE * 125.0*BUILDING_SCALE / maxf(sprite.texture.get_width(), sprite.texture.get_height())
|
||||
sprite.z_index = 10
|
||||
add_child(sprite)
|
||||
@ -129,7 +135,7 @@ func refresh_pois(lander_position: Vector2) -> void:
|
||||
sprite.modulate = Color(0.5, 0.55, 0.5) if poi.complete else Color.WHITE
|
||||
var label: Label = _poi_labels[poi.id]
|
||||
label.visible = bool(poi.discovered)
|
||||
var title: String = {"mine": "Opuszczona kopalnia", "ruins": "Starożytne ruiny", "deposit": "Złoże minerałów"}[poi.kind]
|
||||
var title: String = preload("res://scripts/planets/poi_rules.gd").NAMES[poi.kind]
|
||||
label.text = title + (" · zbadano" if poi.complete else ("\n[F] Zbadaj" if lander_position.distance_to(poi.position) <= 150 else ""))
|
||||
|
||||
func _draw() -> void:
|
||||
|
||||
@ -9,6 +9,7 @@ const PAGES := [
|
||||
["Łup i wytwarzanie", "04", "[font_size=26][color=#f1bd79]Zbieraj. Porównuj. Twórz.[/color][/font_size]\n\n[color=#91dbea][b]Łup zbierasz przez podlecenie[/b][/color]\nPrzedmioty w pobliżu są przyciągane do statku. Magazyn mieści 20 przedmiotów; przy pełnym magazynie zwolnij miejsce, żeby zebrać kolejny.\n\n[color=#91dbea][b]Asteroidy zostawiają fizyczne odłamki[/b][/color]\nStrzelaj do asteroid. Po zniszczeniu rozpadają się bazowo na 5–7 poszarpanych fragmentów. Każdy to jedna jednostka surowca — musisz go zebrać. Umiejętność górnika zwiększa uzysk.\n\n[color=#91dbea][b]Przeszukuj wraki[/b][/color]\nPodleć do wraku i przytrzymaj E, aby go przeszukać. Wraki dają surowce (65%), amunicję lub rakiety (łącznie 20%) albo moduł (15%).\n\n[color=#91dbea][b]Warsztat — przycisk TWÓRZ[/b][/color]\nZ minerałów wytworzysz części, amunicję, rakiety i powerbanki. Receptura pokazuje wymagane składniki oraz ilość produktu.\n\n[color=#91dbea][b]Odzyskuj części[/b][/color]\nNajedź na zbędny przedmiot w magazynie i użyj przycisku rozłożenia. Części przydadzą się do napraw."],
|
||||
["Statek i kapitan", "05", "[font_size=26][color=#f1bd79]Buduj własny styl gry[/color][/font_size]\n\n[color=#91dbea][b]Wyposażenie[/b][/color]\nKliknij panel statku w prawym górnym rogu i otwórz magazyn. Przeciągaj przedmioty do pasujących slotów: maksymalnie 3 bronie, 5 modułów i 5 rozszerzeń. Na początku dostępny jest 1 slot broni i 1 rozszerzenia. Kolejne odblokowujesz w hangarze fregaty za 10 żelaza każdy. Ładownia zaczyna z jednym rzędem (5 miejsc); kolejne rzędy kupujesz w hangarze po 10 żelaza. Po zbudowaniu składu modułów zakładka Magazyn na frachtowcu pozwala odkładać i odbierać wyposażenie. Aktywne rozszerzenia uruchamiasz klawiszami 1–5; pasywne (oznaczone P) działają automatycznie w tych samych slotach. Najedź kursorem, żeby sprawdzić parametry i porównanie wyposażenia.\n\n[color=#91dbea][b]Nie każdy duplikat jest taki sam[/b][/color]\nBroń i moduły mogą mieć różne parametry oraz addony. Aktywne rozszerzenia losują cooldown od 30 do 5 pełnych sekund. Pasywne mają losowaną siłę efektu: Recykler odnawia tarcze i energię po zebraniu odłamka, Wirus przejmuje uciekających wrogów, a Pancerz z tytanu ogranicza obrażenia przy niskim kadłubie. Krótsze odnowienia są rzadkie — przed rozłożeniem porównaj egzemplarze. Rozszerzenia tego samego typu dzielą aktywne odnowienie.\n\n[color=#91dbea][b]Umiejętności kapitana[/b][/color]\nZa pokonanych wrogów zdobywasz doświadczenie i kredyty. Trudniejsze układy dają więcej bazowego XP. Jeśli kapitan przewyższa poziom układu o ponad 2 poziomy, XP za przeciwników i ruiny maleje; przy przewadze 8 poziomów spada do zera. Kredyty i surowce nadal zbierasz. Po awansie wybierasz umiejętność. Pasywne działają stale, aktywne przypisujesz do F1–F5.\n\n[color=#f1bd79][b]Okno umiejętności nie pauzuje gry.[/b][/color]\nOtwieraj je w bezpiecznym miejscu. Przed kolejnym układem uzupełnij zapasy i sprawdź wyposażenie."]
|
||||
,["Planety i frachtowiec", "06", "[font_size=26][color=#f1bd79]Wyprawa na powierzchnię[/color][/font_size]\n\nPodleć do planety oznaczonej EARTHLIKE. [b]F[/b] skanuje tlen, temperaturę i zagrożenie biologiczne. [b]L[/b] rozpoczyna lądowanie. Możesz eksplorować przed pokonaniem wrogów, ale lądowanie jest zablokowane podczas walki.\n\nLądownik wymaga 100 HP przed startem i traci 5–20 HP przy wejściu w atmosferę. Na powierzchni sterujesz jak statkiem: [b]W[/b] — ciąg, [b]S[/b] — hamowanie i wstecz, [b]A/D[/b] — obrót, [b]Q/E[/b] — ruch boczny, [b]TAB[/b] — dopalanie. Odkrywaj miejsca i używaj [b]F[/b], aby je zbadać. Lądownik ma osobną energię dopalania. Każdy etap badania pokazuje szansę sukcesu; dalsze etapy dają więcej łupu i większe ryzyko.\n\n[b]L[/b] lub przycisk powrotu kończy wyprawę i przenosi łup do Twoich zasobów. Zniszczenie lądownika oznacza utratę tylko zdobyczy tej wyprawy. Kolejne lądowanie tworzy nową mapę.\n\n[color=#91dbea][b]Frachtowiec[/b][/color]\nZnajdziesz go przy gwieździe centralnej. Podleć i naciśnij [b]L[/b]. Hangar naprawia lądownik (1 część za 1% HP), warsztat wytwarza sprzęt kapitana, a laboratorium przechowuje skany i archiwa.\n\nKapitan zaczyna z podstawowym systemem oddychania, skafandrem i bronią ręczną. Kolejne poziomy tworzysz w warsztacie; sprzęt zmienisz także w panelu pod kapitanem. Wyższy poziom poprawia szanse eksploracji.\n\n[color=#91dbea][b]Losowe spotkania[/b][/color]\nPytajnik na mapie oznacza wydarzenie. Podleć, aby otworzyć okno. Miejscowy inżynier może zaoferować nieznany moduł za 100 kredytów. Wyższe poziomy przedmiotów są coraz rzadsze. Kupiony moduł pozostaje w ładowni wyprawy do bezpiecznego powrotu.\n\n[color=#f1bd79]Podczas wyprawy i wizyty we frachtowcu kosmos jest zamrożony. Zapisywanie między uruchomieniami gry nie jest jeszcze dostępne.[/color]"]
|
||||
, ["Autorzy dźwięków", "07", "[font_size=26][color=#f1bd79]Dźwięki otoczenia[/color][/font_size]\n\n[b]Blender Foundation — Ambient Bird, Cricket and Frog (Yo Frankie!)[/b]\nOdgłosy świerszcza i żaby, udostępnione przez Lamoot:\nhttps://opengameart.org/content/ambient-bird-cricket-and-frog\nLicencja Creative Commons Attribution 3.0:\nhttps://creativecommons.org/licenses/by/3.0/\nZmiany: filtrowanie, płynne początki i zakończenia, zmiana wysokości żaby oraz konwersja do stereo 48 kHz. Autorzy nie są powiązani z tą grą.\n\n[b]Nagrania na licencji CC0[/b]\nThimras — Park ambiences (wiatr).\nIwan „qubodup” Gabovitch — 20 Rustles of dry leaves.\nisaiah658 — Ambient Bird Sounds.\nRudmer_Rotteveel — Wood Creak Single V7.\n\n[b]Wnętrze fregaty — CC0[/b]\nLEGIT Audio — The Shop (bezpłatne próbki OpenGameArt).\nbart — 68 Workshop Sounds i Steam release sounds.\nKenney — Sci-Fi Sounds.\n\nSzczegółowe źródła i zakres obróbki znajdują się w plikach CREDITS.md i manifest.json przy zasobach audio."]
|
||||
]
|
||||
var _panel: PanelContainer
|
||||
var _reader: RichTextLabel
|
||||
|
||||
20
tests/audio/surface_assets_test.py
Normal file
20
tests/audio/surface_assets_test.py
Normal file
@ -0,0 +1,20 @@
|
||||
from pathlib import Path
|
||||
import wave
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
files = list((ROOT/"assets/sounds/surface").glob("*.wav"))
|
||||
assert len(files) == 13
|
||||
seams = []
|
||||
for path in files:
|
||||
with wave.open(str(path)) as stream:
|
||||
assert stream.getnchannels() == 2 and stream.getsampwidth() == 2 and stream.getframerate() == 48000
|
||||
signal = np.frombuffer(stream.readframes(stream.getnframes()),dtype="<i2").reshape(-1,2)/32768.0
|
||||
assert np.isfinite(signal).all()
|
||||
assert np.max(np.abs(signal)) < .9
|
||||
assert np.max(np.abs(signal.mean(axis=0))) < .01
|
||||
if path.stem.endswith("loop"):
|
||||
difference = float(np.max(np.abs(signal[0]-signal[-1])))
|
||||
assert difference < .05
|
||||
seams.append((path.name,round(difference,6)))
|
||||
print("SURFACE ASSET QA PASS: 13 stereo PCM16 WAVs at 48 kHz, no clipping/DC, loop seam deltas:",seams)
|
||||
@ -8,7 +8,7 @@ func _ready() -> void:
|
||||
profile.temperature = 70 if index==4 else (-30 if index==5 else 20)
|
||||
var data: Dictionary = GENERATOR.generate(200+index,profile)
|
||||
styles[data.landscape.name] = true
|
||||
assert(data.pois.size() >= 4 and data.pois.size() <= 6 and data.rocks.size() > 0)
|
||||
assert(data.pois.size() >= 6 and data.pois.size() <= 7 and data.rocks.size() > 0)
|
||||
assert(not data.has("rivers") and not data.has("river"))
|
||||
assert(data.structures.size() > 0)
|
||||
for structure in data.structures:
|
||||
|
||||
@ -49,7 +49,7 @@ func _ready() -> void:
|
||||
assert(GEAR.equip(captain,"breathing",crafted.uid))
|
||||
assert(GEAR.level(captain,"breathing") == 2)
|
||||
var world: Dictionary = SURFACE.generate(11447,profile)
|
||||
assert(world.pois.size() >= 4 and world.pois.size() <= 6 and world.trees.size() > 0 and world.mountains.size() > 0 and world.structures.size() > 0)
|
||||
assert(world.pois.size() >= 6 and world.pois.size() <= 7 and world.trees.size() > 0 and world.mountains.size() > 0 and world.structures.size() > 0)
|
||||
assert(SURFACE.generate(11447,profile).pois == world.pois)
|
||||
assert(SURFACE.generate(11448,profile).pois != world.pois)
|
||||
for seed_value in 100:
|
||||
@ -119,7 +119,11 @@ func _ready() -> void:
|
||||
manager.scan_nearby()
|
||||
manager.land_nearby()
|
||||
assert(get_tree().paused and GameState.captain.expedition.active)
|
||||
assert(manager.screen.world.data.pois.size() >= 4 and manager.screen.world.data.pois.size() <= 6)
|
||||
assert(manager.screen.world.data.pois.size() >= 6 and manager.screen.world.data.pois.size() <= 7)
|
||||
for poi in manager.screen.world.data.pois:
|
||||
manager.screen._poi = poi
|
||||
manager.screen._show_poi()
|
||||
assert(manager.screen._dialog.visible)
|
||||
manager.screen.lander.position = manager.screen.world.data.pois[0].position
|
||||
manager.screen._poi = manager.screen.world.data.pois[0]
|
||||
manager.screen._show_poi()
|
||||
@ -127,6 +131,10 @@ func _ready() -> void:
|
||||
var missiles_before := inventory.missile_ammunition
|
||||
var credits_before := GameState.captain.credits
|
||||
var owned_parts_before := GameState.captain.parts
|
||||
var ammo_before := inventory.ammunition
|
||||
var irradiated_before := inventory.irradiated_ammunition
|
||||
GameState.captain.expedition.add_material("ammunition",100)
|
||||
GameState.captain.expedition.add_material("irradiated_ammunition",20)
|
||||
GameState.captain.expedition.cargo_missiles = 3
|
||||
GameState.captain.expedition.cargo_credits = 23
|
||||
GameState.captain.expedition.cargo_parts = 4
|
||||
@ -134,6 +142,8 @@ func _ready() -> void:
|
||||
assert(inventory.missile_ammunition == missiles_before+3)
|
||||
assert(GameState.captain.credits == credits_before+23)
|
||||
assert(GameState.captain.parts == owned_parts_before+4)
|
||||
assert(inventory.ammunition >= ammo_before+100)
|
||||
assert(inventory.irradiated_ammunition == irradiated_before+20)
|
||||
assert(not get_tree().paused)
|
||||
game.queue_free()
|
||||
await get_tree().process_frame
|
||||
|
||||
@ -65,21 +65,24 @@ func _ready() -> void:
|
||||
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.
|
||||
# Deposits scale rare materials; structures grant exact stage levels.
|
||||
var modules := 0
|
||||
for attempt in 1200:
|
||||
state.lander_hp = 100
|
||||
var stage := attempt%3
|
||||
var poi := {"kind":"mine","stage":stage,"complete":false}
|
||||
var poi := {"kind":"deposit","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:
|
||||
assert(result.module == null)
|
||||
var structure := {"kind":"ruins","stage":stage,"complete":false}
|
||||
var structure_result: Dictionary = state.resolve_poi(captain,structure,rng)
|
||||
if structure_result.module != null:
|
||||
modules += 1
|
||||
assert(result.module.item_level >= 32+expected_bounds.x and result.module.item_level <= 32+expected_bounds.y)
|
||||
assert(structure_result.module.item_level == 32+stage+1)
|
||||
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})
|
||||
|
||||
@ -82,7 +82,7 @@ func _ready() -> void:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 45
|
||||
var rng_before := rng.state
|
||||
var poi := {"kind":"mine","stage":2,"complete":false}
|
||||
var poi := {"kind":"ruins","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)
|
||||
|
||||
69
tests/levels/poi_rewards_test.gd
Normal file
69
tests/levels/poi_rewards_test.gd
Normal file
@ -0,0 +1,69 @@
|
||||
extends Node
|
||||
const RULES := preload("res://scripts/planets/poi_rules.gd")
|
||||
const CAPTAIN := preload("res://scripts/captain/captain_progress.gd")
|
||||
|
||||
func _ready() -> void:
|
||||
var captain := CAPTAIN.new()
|
||||
var state: Resource = captain.expedition
|
||||
var profile := {"id":"test","name":"Test","seed":42,"difficulty":0.0,"landing_damage":0,"oxygen":98,"temperature":30,"flora":0}
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 42
|
||||
GameState.system_level = 20
|
||||
assert(state.begin_visit(profile))
|
||||
var samples_before := 0
|
||||
for kind in RULES.NAMES:
|
||||
for stage in 3:
|
||||
var success := false
|
||||
for attempt in 100:
|
||||
state.lander_hp = 100
|
||||
var poi := {"kind":kind,"stage":stage,"complete":false}
|
||||
var result: Dictionary = state.resolve_poi(captain,poi,rng)
|
||||
if not result.success: continue
|
||||
success = true
|
||||
assert(poi.stage == stage+1 and poi.complete == (stage==2))
|
||||
if kind == "ruins":
|
||||
assert(result.module.item_level == 21+stage and result.materials.is_empty())
|
||||
else: assert(result.module == null)
|
||||
if kind == "deposit":
|
||||
for key in result.materials: assert(key in ["silicon","copper","nickel","tungsten","mercury"])
|
||||
if kind == "cave":
|
||||
assert(result.materials == {"samples":[1,2,4][stage]})
|
||||
samples_before += [1,2,4][stage]
|
||||
if kind == "village" and stage == 1:
|
||||
assert(result.materials.experience == ceili(captain.get_xp_required()*0.1))
|
||||
if stage == 2: assert(state.resolve_poi(captain,poi,rng).is_empty())
|
||||
break
|
||||
assert(success)
|
||||
var ammo := int(state.cargo.ammunition)
|
||||
var xp := int(state.cargo.experience)
|
||||
var credits := int(state.cargo.credits)
|
||||
assert(state.analysis_samples == 0 and captain.credits == 0)
|
||||
state.finish_visit(captain,true)
|
||||
assert(state.analysis_samples == samples_before and samples_before == 7)
|
||||
assert(state.delivered_ammunition == ammo and captain.experience == xp and captain.credits == credits)
|
||||
for key in ["samples","credits","experience","ammunition","missile","irradiated_ammunition"]: assert(not captain.minerals.has(key))
|
||||
state.finish_visit(captain,true)
|
||||
assert(state.analysis_samples == 7 and captain.credits == credits)
|
||||
assert(state.begin_visit(profile))
|
||||
state.add_material("samples",4)
|
||||
state.add_material("experience",999)
|
||||
state.add_material("ammunition",99)
|
||||
state.finish_visit(captain,false)
|
||||
assert(state.analysis_samples == 7 and captain.experience == xp and state.delivered_ammunition == ammo)
|
||||
for seed_value in 100:
|
||||
for stage in 3:
|
||||
var easy := RandomNumberGenerator.new()
|
||||
var hard := RandomNumberGenerator.new()
|
||||
easy.seed = seed_value
|
||||
hard.seed = seed_value
|
||||
var a := RULES.reward("warehouse",stage,easy,{"difficulty":0.0},captain)
|
||||
var b := RULES.reward("warehouse",stage,hard,{"difficulty":1.0},captain)
|
||||
for key in a:
|
||||
assert(b[key] == a[key]*(3 if key in ["iron","titanium","aluminium","uranium","sulfur"] else 1))
|
||||
var saved := "user://poi_samples_test.tres"
|
||||
assert(ResourceSaver.save(captain,saved) == OK)
|
||||
var loaded: Resource = ResourceLoader.load(saved,"",ResourceLoader.CACHE_MODE_IGNORE)
|
||||
assert(loaded.expedition.analysis_samples == 7)
|
||||
DirAccess.remove_absolute(saved)
|
||||
print("POI REWARDS PASS: all 15 stages, exact module levels, rare deposits, ammo scaling, EXP, samples, return/loss, persistence, duplicate claims")
|
||||
get_tree().quit()
|
||||
1
tests/levels/poi_rewards_test.gd.uid
Normal file
1
tests/levels/poi_rewards_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dijofj6h2s2nw
|
||||
4
tests/levels/poi_rewards_test.tscn
Normal file
4
tests/levels/poi_rewards_test.tscn
Normal file
@ -0,0 +1,4 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://tests/levels/poi_rewards_test.gd" id="1"]
|
||||
[node name="PoiRewardsTest" type="Node"]
|
||||
script = ExtResource("1")
|
||||
62
tests/levels/surface_audio_test.gd
Normal file
62
tests/levels/surface_audio_test.gd
Normal file
@ -0,0 +1,62 @@
|
||||
extends Node
|
||||
|
||||
func _ready() -> void:
|
||||
var progress: Resource = GameState.captain.expedition
|
||||
for biome in 6:
|
||||
var profile: Dictionary = preload("res://scripts/planets/planet_profiles.gd").generate(99,biome,"Audio test")
|
||||
profile.temperature = 75 if biome == 4 else (-35 if biome == 5 else 20)
|
||||
profile.oxygen = 90
|
||||
progress.lander_hp = 100
|
||||
assert(progress.begin_visit(profile))
|
||||
progress.visit_seed = 200+biome
|
||||
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_physics_process(false)
|
||||
var audio: Node = surface.get_node("SurfaceAudio")
|
||||
assert(audio._streams.size() == 13 and audio._voices.size() == 3)
|
||||
assert(audio._biome == biome)
|
||||
assert(audio._wind.playing and audio._foliage.playing)
|
||||
assert(audio._wind.stream.loop_mode == AudioStreamWAV.LOOP_FORWARD)
|
||||
for id in audio._streams: assert(audio._streams[id] != null and audio._streams[id].get_length() > .1)
|
||||
var choices: Array = audio.fauna_choices()
|
||||
if biome == 2: assert(choices.has("marsh_call"))
|
||||
else: assert(not choices.has("marsh_call"))
|
||||
if biome == 4: assert(choices == ["insects"])
|
||||
audio._oxygen = 0
|
||||
assert(audio.fauna_choices().is_empty())
|
||||
audio._oxygen = 90
|
||||
# A treeless local clearing must not emit nearby leaves or creaks.
|
||||
audio.set_process(false)
|
||||
audio._near_trees.clear()
|
||||
audio._density = 0
|
||||
audio._next_fauna = INF
|
||||
audio._next_gust = INF
|
||||
audio._next_leaves = 0
|
||||
audio._next_creak = 0
|
||||
var before: int = audio.played_count
|
||||
audio._process(.1)
|
||||
assert(audio.played_count == before)
|
||||
audio._near_trees.append(surface.lander.position+Vector2(100,0))
|
||||
audio._next_leaves = 0
|
||||
audio._process(.1)
|
||||
assert(audio.played_count == before+1)
|
||||
for index in 12: audio._play("leaves_1",Vector2(100,0),-20)
|
||||
assert(audio._voices.size() == 3)
|
||||
var world_reference: WeakRef = weakref(audio._world_audio)
|
||||
audio.set_process(true)
|
||||
var position_before: float = audio._wind.get_playback_position()
|
||||
get_tree().paused = true
|
||||
await get_tree().create_timer(.4,true).timeout
|
||||
assert(audio.can_process() and audio._wind.can_process() and audio._wind.playing and not audio._wind.stream_paused)
|
||||
assert(audio._wind.get_playback_position() > position_before)
|
||||
get_tree().paused = false
|
||||
progress.finish_visit(GameState.captain,false)
|
||||
surface.queue_free()
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
assert(world_reference.get_ref() == null,"Nature emitters and listener must not survive leaving the surface")
|
||||
print("SURFACE AUDIO PASS: six biomes, 13 streams, wind/foliage loops, local trees, biome fauna, low-oxygen suppression, bounded spatial voices, paused playback, cleanup after return")
|
||||
get_tree().quit()
|
||||
1
tests/levels/surface_audio_test.gd.uid
Normal file
1
tests/levels/surface_audio_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bg8wngti5tlqi
|
||||
4
tests/levels/surface_audio_test.tscn
Normal file
4
tests/levels/surface_audio_test.tscn
Normal file
@ -0,0 +1,4 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://tests/levels/surface_audio_test.gd" id="1"]
|
||||
[node name="SurfaceAudioTest" type="Node"]
|
||||
script = ExtResource("1")
|
||||
@ -3,24 +3,28 @@ const GENERATOR := preload("res://scripts/planets/surface_generator.gd")
|
||||
const SCENERY := preload("res://scripts/planets/surface_scenery.gd")
|
||||
|
||||
func _ready() -> void:
|
||||
var extra_mines := 0
|
||||
var extra_ruins := 0
|
||||
for index in 64:
|
||||
var profile: Dictionary = preload("res://scripts/planets/planet_profiles.gd").generate(711,index,"Test")
|
||||
var data: Dictionary = GENERATOR.generate(500+index,profile)
|
||||
var counts := {"mine":0,"ruins":0,"deposit":0}
|
||||
var counts := {"warehouse":0,"village":0,"cave":0,"ruins":0,"deposit":0}
|
||||
for poi in data.pois: counts[poi.kind] += 1
|
||||
assert(counts.mine in [1,2] and counts.ruins in [1,2])
|
||||
for poi in data.pois:
|
||||
if poi.kind != "cave": continue
|
||||
assert(poi.has("mountain_position"))
|
||||
assert(poi.position.distance_to(poi.mountain_position) == 86)
|
||||
assert(data.mountains.any(func(m): return m.get("cave_anchor",false) and m.position == poi.mountain_position))
|
||||
for other in data.pois:
|
||||
if other.id != poi.id: assert(other.position.distance_to(poi.position) >= 420)
|
||||
assert(counts.warehouse == 1 and counts.village == 1 and counts.cave == 1 and counts.ruins in [1,2])
|
||||
assert(counts.deposit == 2)
|
||||
assert(data.events.size() == 1 and data.events[0].position.is_finite())
|
||||
assert(data.events[0].position.length() > 300)
|
||||
extra_mines += int(counts.mine == 2)
|
||||
extra_ruins += int(counts.ruins == 2)
|
||||
if index == 0:
|
||||
var repeated: Dictionary = GENERATOR.generate(500,profile)
|
||||
assert(repeated.pois == data.pois and repeated.events == data.events)
|
||||
# Most maps have one of each, with some maps containing a second.
|
||||
assert(extra_mines > 4 and extra_mines < 30)
|
||||
assert(extra_ruins > 4 and extra_ruins < 30)
|
||||
# No flat ground can disable the guaranteed question-mark event.
|
||||
var fallback := {"seed":987,"heights":PackedFloat32Array(),"trees":[],"rocks":[],"mountains":[],"pois":[]}
|
||||
@ -31,5 +35,5 @@ func _ready() -> void:
|
||||
SCENERY.generate(fallback,rng)
|
||||
assert(fallback.events.size() == 1 and fallback.events[0].position.is_finite())
|
||||
assert(fallback.events[0].position.length() > 400)
|
||||
print("SURFACE POI COUNTS PASS: 64 maps, mines/ruins 1..2, two deposits, one guaranteed event, deterministic layout, no-flat-ground fallback; extra mines/ruins=",extra_mines,"/",extra_ruins)
|
||||
print("SURFACE POI COUNTS PASS: 64 maps, ruins 1..2, two deposits, warehouse/village/cave once, one guaranteed event, deterministic layout; extra ruins=",extra_ruins)
|
||||
get_tree().quit()
|
||||
|
||||
@ -8,6 +8,7 @@ func _ready() -> void:
|
||||
assert(GENERATOR.PLANET_TEXTURES.size() == 125)
|
||||
for texture in GENERATOR.STAR_TEXTURES + GENERATOR.PLANET_TEXTURES:
|
||||
assert(texture.resource_path.begins_with("res://assets/planet_sun/"))
|
||||
assert(texture.get_size() == Vector2(512, 512), "Low-resolution celestial texture: " + texture.resource_path)
|
||||
for texture in GENERATOR.ALIEN_SHIPS.TEXTURES:
|
||||
var image := texture.get_image()
|
||||
assert(image.get_size() == Vector2i(256, 256))
|
||||
|
||||
58
tests/previews/poi_presentation_preview.gd
Normal file
58
tests/previews/poi_presentation_preview.gd
Normal file
@ -0,0 +1,58 @@
|
||||
extends Node
|
||||
func settle() -> void:
|
||||
for frame in 5: await get_tree().process_frame
|
||||
await RenderingServer.frame_post_draw
|
||||
|
||||
func _ready() -> void:
|
||||
var profile: Dictionary = preload("res://scripts/planets/planet_profiles.gd").generate(777,1,"Dolina pod Górą")
|
||||
profile.temperature = 20
|
||||
profile.oxygen = 95
|
||||
profile.flora = 0
|
||||
profile.difficulty = 0.0
|
||||
var progress: Resource = GameState.captain.expedition
|
||||
assert(progress.begin_visit(profile))
|
||||
progress.visit_seed = 200
|
||||
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 cave: Dictionary
|
||||
for poi in surface.world.data.pois:
|
||||
if poi.kind == "cave": cave = poi
|
||||
assert(cave.has("mountain_position"))
|
||||
assert(cave.position.distance_to(cave.mountain_position) == 86)
|
||||
surface.lander.position = cave.position+Vector2(0,100)
|
||||
surface.camera.position_smoothing_enabled = false
|
||||
surface.camera.position = cave.position+Vector2(100,0)
|
||||
surface.camera.force_update_scroll()
|
||||
surface.world.refresh_pois(cave.position)
|
||||
surface._surface_viewport.get_node("SurfaceAtmosphere").hide()
|
||||
await settle()
|
||||
get_viewport().get_texture().get_image().save_png("res://artifacts/cave_mountain_preview.png")
|
||||
var sheet := Image.create(1350,3250,false,Image.FORMAT_RGBA8)
|
||||
for row in 5:
|
||||
for stage in 3:
|
||||
surface._poi = {"kind":preload("res://scripts/planets/poi_presentation.gd").ORDER[row],"stage":stage,"complete":false}
|
||||
surface._show_poi()
|
||||
await settle()
|
||||
var rect: Rect2 = surface._dialog.get_global_rect()
|
||||
assert(rect.end.y <= get_viewport().get_visible_rect().size.y and rect.size.x <= 450)
|
||||
var art: TextureRect = surface._choices.get_child(0).get_child(0)
|
||||
assert(art.texture is AtlasTexture)
|
||||
var screenshot := get_viewport().get_texture().get_image()
|
||||
if row == 4 and stage == 0: screenshot.save_png("res://artifacts/cave_poi_window.png")
|
||||
var card := screenshot.get_region(Rect2i(rect))
|
||||
card.convert(Image.FORMAT_RGBA8)
|
||||
card.resize(450,650,Image.INTERPOLATE_LANCZOS)
|
||||
sheet.blit_rect(card,Rect2i(0,0,450,650),Vector2i(stage*450,row*650))
|
||||
sheet.save_png("res://artifacts/poi_stage_cards.png")
|
||||
get_window().size = Vector2i(800,600)
|
||||
await settle()
|
||||
surface._resize_surface_viewport()
|
||||
await settle()
|
||||
assert(surface._dialog.get_global_rect().end.y <= get_viewport().get_visible_rect().size.y)
|
||||
print("POI PRESENTATION PASS: 15 illustrated cards, compact layout, small-window scroll, mountain cave")
|
||||
get_tree().quit()
|
||||
1
tests/previews/poi_presentation_preview.gd.uid
Normal file
1
tests/previews/poi_presentation_preview.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://7narfx6bd1p8
|
||||
4
tests/previews/poi_presentation_preview.tscn
Normal file
4
tests/previews/poi_presentation_preview.tscn
Normal file
@ -0,0 +1,4 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://tests/previews/poi_presentation_preview.gd" id="1"]
|
||||
[node name="PoiPresentationPreview" type="Node"]
|
||||
script = ExtResource("1")
|
||||
BIN
tools/audio/__pycache__/build_freighter_audio.cpython-312.pyc
Normal file
BIN
tools/audio/__pycache__/build_freighter_audio.cpython-312.pyc
Normal file
Binary file not shown.
77
tools/audio/build_surface_audio.py
Normal file
77
tools/audio/build_surface_audio.py
Normal file
@ -0,0 +1,77 @@
|
||||
"""Nature field recordings edited into planet ambience. Requires numpy + soundfile."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import json
|
||||
import wave
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0,str(ROOT/"tmp/surface_audio_tools"))
|
||||
import soundfile as sf
|
||||
from build_freighter_audio import eq, peak, pitch, fade, loop, RATE
|
||||
|
||||
SOURCE = ROOT/"tmp/surface_audio_sources"
|
||||
OUT = ROOT/"assets/sounds/surface"
|
||||
manifest = {}
|
||||
|
||||
def read(path, start=0, seconds=None):
|
||||
with sf.SoundFile(SOURCE/path) as stream:
|
||||
rate = stream.samplerate
|
||||
stream.seek(int(start*rate))
|
||||
signal = stream.read(-1 if seconds is None else int(seconds*rate),always_2d=True)
|
||||
if signal.shape[1] == 1: signal = np.repeat(signal,2,axis=1)
|
||||
signal = signal[:,:2]
|
||||
if rate != RATE:
|
||||
at = np.arange(int(len(signal)*RATE/rate))*rate/RATE
|
||||
signal = np.stack([np.interp(at,np.arange(len(signal)),signal[:,c]) for c in range(2)],axis=1)
|
||||
signal -= signal.mean(axis=0)
|
||||
return signal
|
||||
|
||||
def save(name, x, sources, processing, looping=False):
|
||||
x = peak(x,-3)
|
||||
assert np.isfinite(x).all() and np.max(np.abs(x)) < .9
|
||||
with wave.open(str(OUT/(name+".wav")),"wb") as stream:
|
||||
stream.setparams((2,2,RATE,0,"NONE","not compressed"))
|
||||
stream.writeframes((x*32767).astype("<i2").tobytes())
|
||||
manifest[name] = {"sources":sources,"processing":processing,"loop":looping,"seconds":round(len(x)/RATE,3),"peak_dbfs":-3,"rms_dbfs":round(20*np.log10(np.sqrt((x*x).mean())),2)}
|
||||
return x
|
||||
|
||||
def main():
|
||||
OUT.mkdir(parents=True,exist_ok=True)
|
||||
# Avoid recording start/handling noise; remove rumble and distant bird highs.
|
||||
wind = read("wind.wav",start=8,seconds=24)
|
||||
wind = save("wind_loop",loop(eq(wind,low=45,high=1900),2),["wind.wav: 8..32 seconds"],"Real open-field wind; rumble/high-bird reduction; two-second seam crossfade",True)
|
||||
gust = fade(eq(read("wind.wav",start=45,seconds=8),low=70,high=2900),2,3)
|
||||
gust = save("wind_gust",gust,["wind.wav: 45..53 seconds"],"Natural gust with long soft attack/release and low-pass")
|
||||
leaves = [eq(read("leaves/rustle/rustle%02d.flac"%i),low=180,high=5200) for i in [1,4,9]]
|
||||
foliage = np.zeros((int(16*RATE),2))
|
||||
for index in range(12):
|
||||
leaf = fade(peak(leaves[index%3]),.25,.55)
|
||||
start = int(index*1.15*RATE)
|
||||
end = min(len(foliage),start+len(leaf))
|
||||
foliage[start:end] += leaf[:end-start]*.30
|
||||
foliage = save("foliage_loop",loop(eq(foliage,low=160,high=3300),1.5),["leaves/rustle/rustle01.flac","leaves/rustle/rustle04.flac","leaves/rustle/rustle09.flac"],"Layered leaf recordings with staggered overlaps, soft envelopes, filtering, crossfaded loop",True)
|
||||
for index,leaf in enumerate(leaves,1):
|
||||
save("leaves_%d"%index,fade(leaf,.1,.3),["leaves/rustle/rustle%02d.flac"%[1,4,9][index-1]],"Trimmed leaf rustle; softened attack and release")
|
||||
wood = read("wood_creak.mp3")
|
||||
for index,ratio in enumerate([.78,.94],1):
|
||||
save("branch_creak_%d"%index,fade(eq(pitch(wood,ratio),low=120,high=3400),.04,.3),["wood_creak.mp3"],"Real wood flex/creak; mild pitch variation; no falling-tree impact")
|
||||
for index,offset in enumerate([3,15,29],1):
|
||||
save("bird_%d"%index,fade(eq(read("birds.ogg",offset,5),low=650,high=7000),.5,1),["birds.ogg: %d..%d seconds"%(offset,offset+5)],"Natural distant birds, cut into independent calls; filtered noise; soft boundaries")
|
||||
cricket = next((SOURCE/"fauna").rglob("amb_cricket_1.flac"))
|
||||
frog = next((SOURCE/"fauna").rglob("amb_frog_1.flac"))
|
||||
save("insects",fade(eq(read(cricket),low=900,high=7500),.08,.25),[str(cricket.relative_to(SOURCE))],"Cricket recording, gentle filtering and envelope")
|
||||
save("marsh_call",fade(eq(pitch(read(frog),.92),low=180,high=4600),.05,.25),[str(frog.relative_to(SOURCE))],"Frog recording, slightly lower pitch, restrained high frequencies")
|
||||
(OUT/"manifest.json").write_text(json.dumps(manifest,indent=2),encoding="utf-8")
|
||||
preview = np.zeros((int(32*RATE),2))
|
||||
for bed,level in [(wind,-17),(foliage,-25)]: preview += bed[np.arange(len(preview))%len(bed)]*10**(level/20)
|
||||
for name,offset,level in [("bird_1",2,-13),("leaves_1",7,-14),("branch_creak_1",12,-14),("wind_gust",16,-20),("marsh_call",22,-16),("insects",27,-19)]:
|
||||
clip,_ = sf.read(OUT/(name+".wav"),always_2d=True)
|
||||
at = int(offset*RATE)
|
||||
end = min(len(preview),at+len(clip))
|
||||
preview[at:end] += clip[:end-at]*10**(level/20)
|
||||
preview = fade(peak(preview,-3),1,1.5)
|
||||
sf.write(ROOT/"artifacts/planet_ambience_demo.wav",preview,RATE,subtype="PCM_16")
|
||||
print(json.dumps(manifest,indent=2))
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
54
tools/audio/fetch_surface_sources.py
Normal file
54
tools/audio/fetch_surface_sources.py
Normal file
@ -0,0 +1,54 @@
|
||||
"""Fetch licensed nature field recordings; no login or API key required."""
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
DEST = ROOT / "tmp/surface_audio_sources"
|
||||
SOURCES = {
|
||||
"wind.wav": ("https://opengameart.org/content/park-ambiences", "park_ambience_wind.wav"),
|
||||
"leaves.7z": ("https://opengameart.org/content/20-rustles-dry-leaves", "qubodup-rustle.7z"),
|
||||
"birds.ogg": ("https://opengameart.org/content/ambient-bird-sounds", "birds-isaiah658.ogg"),
|
||||
"fauna.7z": ("https://opengameart.org/content/ambient-bird-cricket-and-frog", "amb_bird_cricket_frog.7z"),
|
||||
}
|
||||
|
||||
def get(url):
|
||||
request = urllib.request.Request(url,headers={"User-Agent":"Randoloot nature audio preparation"})
|
||||
with urllib.request.urlopen(request,timeout=120) as response: return response.read()
|
||||
|
||||
def fetch(entry):
|
||||
name,(page,filename) = entry
|
||||
path = DEST/name
|
||||
if not path.exists():
|
||||
html = get(page).decode("utf-8")
|
||||
links = re.findall(r'href=[\"\x27]([^\"\x27]+)[\"\x27]',html)
|
||||
matches = [urllib.parse.urljoin(page,url) for url in links if urllib.parse.unquote(url).endswith(filename)]
|
||||
if name == "birds.ogg" and not matches:
|
||||
matches = [urllib.parse.urljoin(page,url) for url in links if "birds-isaiah658" in url and url.endswith(".ogg")]
|
||||
if not matches: raise ValueError("Missing published file: "+filename)
|
||||
path.write_bytes(get(matches[0]))
|
||||
if name.endswith(".7z"):
|
||||
folder = DEST/path.stem
|
||||
folder.mkdir(exist_ok=True)
|
||||
names = subprocess.check_output(["tar","-tf",str(path)],text=True).splitlines()
|
||||
for member in names:
|
||||
if not (folder/member).resolve().is_relative_to(folder.resolve()): raise ValueError("Unsafe archive path")
|
||||
subprocess.run(["tar","-xf",str(path),"-C",str(folder)],check=True)
|
||||
return name,names
|
||||
return name,path.stat().st_size
|
||||
|
||||
if __name__ == "__main__":
|
||||
DEST.mkdir(parents=True,exist_ok=True)
|
||||
(DEST/".gdignore").write_text("")
|
||||
with ThreadPoolExecutor(max_workers=4) as pool:
|
||||
for result in pool.map(fetch,SOURCES.items()): print(result)
|
||||
wood = DEST/"wood_creak.mp3"
|
||||
if not wood.exists():
|
||||
page = get("https://freesound.org/people/Rudmer_Rotteveel/sounds/506662/").decode()
|
||||
previews = re.findall(r'https://cdn\.freesound\.org/previews/[^\s\"<>]+?-hq\.mp3',page)
|
||||
if not previews: raise ValueError("Missing public wood-creak preview")
|
||||
wood.write_bytes(get(previews[0]))
|
||||
print("wood_creak.mp3",wood.stat().st_size)
|
||||
Loading…
x
Reference in New Issue
Block a user