This commit is contained in:
Tomasz Boruc 2026-10-18 15:11:06 +02:00
parent c7df380c3a
commit fc62a03553
72 changed files with 1267 additions and 6 deletions

View File

@ -0,0 +1,39 @@
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
[ 0% ] first_scan_filesystem | Started Project initialization (5 steps)
[ 0% ] first_scan_filesystem | Scanning file structure...
[ 16% ] first_scan_filesystem | Loading global class names...
[ 33% ] first_scan_filesystem | Verifying GDExtensions...
[ 50% ] first_scan_filesystem | Creating autoload scripts...
[ 66% ] first_scan_filesystem | Initializing plugins...
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% ] first_scan_filesystem | Starting file scan...
[ DONE ] first_scan_filesystem

[ 0% ] reimport | Started (Re)Importing Assets (1 steps)
[ 0% ] reimport | Preparing files to reimport...
[ 0% ] reimport | Executing pre-reimport operations...
[ 0% ] reimport | mineral_deposit.png
[ 50% ] reimport | Finalizing Asset Import...
[ DONE ] reimport

[ 0% ] reimport | Started (Re)Importing Assets (1 steps)
[ 0% ] reimport | Executing post-reimport operations...
[ DONE ] reimport

[ 0% ] loading_editor_layout | Started Loading editor (5 steps)
[ 0% ] loading_editor_layout | Loading editor layout...
[ 16% ] loading_editor_layout | Loading docks...
[ 33% ] loading_editor_layout | Reopening scenes...
[ 50% ] loading_editor_layout | Loading central editor layout...
[ 66% ] loading_editor_layout | Loading plugin window layout...
[ 83% ] loading_editor_layout | Editor layout ready.
[ DONE ] loading_editor_layout

[MCPServer] stopped
[MCPRegistry] deregistered c:/users/tomek/documents/godot/randoloot

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

View File

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cv70bcvcdiumo"
path="res://.godot/imported/freighter_ambience_demo.wav-ed73b9e788a5c44d755e11fb8d2b254b.sample"
[deps]
source_file="res://artifacts/freighter_ambience_demo.wav"
dest_files=["res://.godot/imported/freighter_ambience_demo.wav-ed73b9e788a5c44d755e11fb8d2b254b.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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 878 KiB

After

Width:  |  Height:  |  Size: 871 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 844 KiB

After

Width:  |  Height:  |  Size: 841 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 820 KiB

After

Width:  |  Height:  |  Size: 817 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b1rng0dghwlej"
path="res://.godot/imported/mineral_deposit.png-a3bfae2fe6de97db0a85f346d521f355.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/planets/mineral_deposit.png"
dest_files=["res://.godot/imported/mineral_deposit.png-a3bfae2fe6de97db0a85f346d521f355.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@ -0,0 +1,6 @@
{
"tool": "image_gen (built-in)",
"mode": "new transparent RGBA game sprite",
"asset": "assets/planets/mineral_deposit.png",
"prompt": "Game sprite asset for a realistic top-down planetary exploration game: a small naturally exposed mineral ore deposit emerging flush from soil, STRICT ORTHOGRAPHIC TOP DOWN directly overhead. An irregular broken vein of muted blue-grey metallic ore with a few warm copper mineral seams visible inside jagged earthy rocks. Several substantial partially buried ore fragments, 6 to 10 clearly readable chunks, flat fractured surfaces and natural subdued mineral sheen, surrounded by tiny scattered chips, gravel and a thin loose-soil transition. Make it look like minerals poking out of actual ground, not a collectible crystal, not an icon, not a pile on a platform. No big upright crystal spires. No neon glow, no glitter particles. High fidelity gritty hand-painted 3D game texture style, soft ambient upper-left daylight, shallow baked contact shading immediately under stones. Single compact irregular horizontal oval cluster centered in square canvas, occupies central 65-70 percent of canvas, generous fully transparent margins. True transparent RGBA background. Loose soil and gravel around the edge must disperse naturally into fully transparent pixels, softly feathered alpha, no opaque disc or square of dirt, no hard cutout contour around soil, no backdrop, no outline, no text, no UI. Major stone facets must remain readable when the sprite is reduced to about 100 screen pixels."
}

View File

@ -0,0 +1,32 @@
# Frigate interior audio
The mechanical ambience and foley are edited real-world recordings. They are
not AI-generated sound effects. All external sources below are released under
CC0 1.0: https://creativecommons.org/publicdomain/zero/1.0/
- **LEGIT Audio — The Shop, free OpenGameArt samples**
https://opengameart.org/content/the-shop
Recordings: `TheShopCollection_convenience_store_drinks_fridge_drone.wav`
and `TheShopCollection_convenience_store_drinks_fridge_drone_2.wav`.
Used for reactor and ventilation beds. CC0 applies specifically to the free
files published on OpenGameArt, not to other commercial LEGIT Audio products.
- **bart — 68 Workshop Sounds**
https://opengameart.org/content/68-workshop-sounds
Garage recordings of real tools and metal. Used for motor tools, ratchets,
tapping, clattering, sliding metal and latches.
- **bart — Steam release sounds**
https://opengameart.org/content/steam-release-sounds
Used for pressure equalization, valves and airlock seals.
- **Kenney — Sci-Fi Sounds**
https://opengameart.org/content/sci-fi-sounds
https://kenney.nl/assets/sci-fi-sounds
`computerNoise_000.ogg`, `computerNoise_001.ogg`: quiet console electronics.
The two-tone service annunciator is original procedural sound design. It is
intentionally electronic, like a real control-panel chime.
Edits: trimming, DC removal, filtering, mild pitch reduction, layer timing,
short stereo room reflections, loop crossfades and level normalization.
`manifest.json` maps each final asset to its source recordings and processing.
Rebuild using `tools/audio/fetch_freighter_sources.py` followed by
`tools/audio/build_freighter_audio.py` (Python + numpy).

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cyfw40asyp3m2"
path="res://.godot/imported/airlock_close.wav-9d7827f22d7e1ff7ceca89394018d8c7.sample"
[deps]
source_file="res://assets/sounds/freighter/airlock_close.wav"
dest_files=["res://.godot/imported/airlock_close.wav-9d7827f22d7e1ff7ceca89394018d8c7.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

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://byimx1n0t66e5"
path="res://.godot/imported/airlock_open.wav-780191e96afd216c23fb84a9b94fabe2.sample"
[deps]
source_file="res://assets/sounds/freighter/airlock_open.wav"
dest_files=["res://.godot/imported/airlock_open.wav-780191e96afd216c23fb84a9b94fabe2.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

Binary file not shown.

View File

@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://u3oifo5jhk23"
path="res://.godot/imported/console_1.ogg-00748b20716b1db4ba0330ad0d96fd4c.oggvorbisstr"
[deps]
source_file="res://assets/sounds/freighter/console_1.ogg"
dest_files=["res://.godot/imported/console_1.ogg-00748b20716b1db4ba0330ad0d96fd4c.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://cbbslo1pl5tx1"
path="res://.godot/imported/console_2.ogg-95d3ba6ee4c4be080a4d9012fa9f5ab4.oggvorbisstr"
[deps]
source_file="res://assets/sounds/freighter/console_2.ogg"
dest_files=["res://.godot/imported/console_2.ogg-95d3ba6ee4c4be080a4d9012fa9f5ab4.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://gxwhgoj8l167"
path="res://.godot/imported/docking_clamp.wav-36a697b9f91ddbc7d8667d7653a2a56a.sample"
[deps]
source_file="res://assets/sounds/freighter/docking_clamp.wav"
dest_files=["res://.godot/imported/docking_clamp.wav-36a697b9f91ddbc7d8667d7653a2a56a.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

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://76r3bo438dqt"
path="res://.godot/imported/maintenance_alarm.wav-cd9e4e3bc6bef5557ea2d0a2c6daa2fa.sample"
[deps]
source_file="res://assets/sounds/freighter/maintenance_alarm.wav"
dest_files=["res://.godot/imported/maintenance_alarm.wav-cd9e4e3bc6bef5557ea2d0a2c6daa2fa.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

View File

@ -0,0 +1,145 @@
{
"reactor_loop": {
"sources": [
"shop/TheShopCollection_convenience_store_drinks_fridge_drone.wav"
],
"processing": "Slowed 0.64x; DC removal; band limiting 28–1150 Hz; 1.2 s loop crossfade",
"loop": true,
"seconds": 16.671,
"peak_dbfs": -3.5,
"rms_dbfs": -16.46
},
"ventilation_loop": {
"sources": [
"shop/TheShopCollection_convenience_store_drinks_fridge_drone_2.wav"
],
"processing": "DC removal; band limiting 110–2600 Hz; 1.2 s loop crossfade",
"loop": true,
"seconds": 12.342,
"peak_dbfs": -3.5,
"rms_dbfs": -17.53
},
"tool_1": {
"sources": [
"workshop/workshop/workshop - drill short 1.wav"
],
"processing": "Trim, reduced motor speed, softened highs, short industrial room reflections",
"loop": false,
"seconds": 2.782,
"peak_dbfs": -2.0,
"rms_dbfs": -25.83
},
"tool_2": {
"sources": [
"workshop/workshop/workshop - drill short 2.wav"
],
"processing": "Trim, reduced motor speed, softened highs, short industrial room reflections",
"loop": false,
"seconds": 1.964,
"peak_dbfs": -2.0,
"rms_dbfs": -28.28
},
"tool_3": {
"sources": [
"workshop/workshop/workshop - ratchet1.wav"
],
"processing": "Trim, reduced motor speed, softened highs, short industrial room reflections",
"loop": false,
"seconds": 6.022,
"peak_dbfs": -2.0,
"rms_dbfs": -24.45
},
"metal_1": {
"sources": [
"workshop/workshop/workshop - tool clatter.wav"
],
"processing": "Trim, mild pitch lowering, low-pass, short stereo reflections",
"loop": false,
"seconds": 1.55,
"peak_dbfs": -2.0,
"rms_dbfs": -24.84
},
"metal_2": {
"sources": [
"workshop/workshop/workshop - metal tapping.wav"
],
"processing": "Trim, mild pitch lowering, low-pass, short stereo reflections",
"loop": false,
"seconds": 5.961,
"peak_dbfs": -2.0,
"rms_dbfs": -33.68
},
"airlock_open": {
"sources": [
"workshop/workshop/workshop - clink thud.wav",
"steam/steam hisses - Marker #1.wav",
"workshop/workshop/workshop - dragging metal.wav"
],
"processing": "Layered latch, pressure equalization, slow metal slide; EQ and shared room reflections",
"loop": false,
"seconds": 2.53,
"peak_dbfs": -2.0,
"rms_dbfs": -22.08
},
"airlock_close": {
"sources": [
"workshop/workshop/workshop - dragging metal.wav",
"steam/steam hisses - Marker #1.wav",
"workshop/workshop/workshop - clink thud.wav"
],
"processing": "Metal slide, pressure seal and locking impact; shared short reflections",
"loop": false,
"seconds": 2.368,
"peak_dbfs": -2.0,
"rms_dbfs": -24.96
},
"pressure_release": {
"sources": [
"steam/steam hisses - Marker #1.wav"
],
"processing": "Softened steam recording, pressure-release envelope, short reflections",
"loop": false,
"seconds": 1.51,
"peak_dbfs": -2.0,
"rms_dbfs": -23.32
},
"docking_clamp": {
"sources": [
"workshop/workshop/workshop - low hammering.wav",
"workshop/workshop/workshop - clink thud.wav"
],
"processing": "Layered low mechanical impact and latch, softened transient, stereo reflections",
"loop": false,
"seconds": 1.518,
"peak_dbfs": -2.0,
"rms_dbfs": -23.29
},
"service_chime": {
"sources": [],
"processing": "Original two-tone 540/680 Hz service annunciator, smooth envelope, short reflections",
"loop": false,
"seconds": 2.15,
"peak_dbfs": -2.0,
"rms_dbfs": -16.47
},
"maintenance_alarm": {
"sources": [],
"processing": "Original restrained maintenance warning, two paired tones with room reflections",
"loop": false,
"seconds": 3.3,
"peak_dbfs": -2.0,
"rms_dbfs": -16.03
},
"console_1": {
"sources": [
"scifi/Audio/computerNoise_000.ogg"
],
"processing": "Unmodified CC0 Kenney electronic effect, mixed quietly in-engine"
},
"console_2": {
"sources": [
"scifi/Audio/computerNoise_001.ogg"
],
"processing": "Unmodified CC0 Kenney electronic effect, mixed quietly in-engine"
}
}

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cfygep3j0a0yb"
path="res://.godot/imported/metal_1.wav-fef3e8abe502627a1a5d423c54ce7664.sample"
[deps]
source_file="res://assets/sounds/freighter/metal_1.wav"
dest_files=["res://.godot/imported/metal_1.wav-fef3e8abe502627a1a5d423c54ce7664.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

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dojhloap2i21u"
path="res://.godot/imported/metal_2.wav-76016f4aa9d8bb64300640867d45119f.sample"
[deps]
source_file="res://assets/sounds/freighter/metal_2.wav"
dest_files=["res://.godot/imported/metal_2.wav-76016f4aa9d8bb64300640867d45119f.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

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bojna6ipk5lvm"
path="res://.godot/imported/pressure_release.wav-821b8ab32f6a5ab01fd649879bfa8d17.sample"
[deps]
source_file="res://assets/sounds/freighter/pressure_release.wav"
dest_files=["res://.godot/imported/pressure_release.wav-821b8ab32f6a5ab01fd649879bfa8d17.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

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://ci6x6304pmqg8"
path="res://.godot/imported/reactor_loop.wav-d882cf9a2dfd0bb084898cf1ecf4f096.sample"
[deps]
source_file="res://assets/sounds/freighter/reactor_loop.wav"
dest_files=["res://.godot/imported/reactor_loop.wav-d882cf9a2dfd0bb084898cf1ecf4f096.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

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cfvw26xmd54f7"
path="res://.godot/imported/service_chime.wav-debd9086148ec31fa72bff6ce8cc346d.sample"
[deps]
source_file="res://assets/sounds/freighter/service_chime.wav"
dest_files=["res://.godot/imported/service_chime.wav-debd9086148ec31fa72bff6ce8cc346d.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

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bkbfe36sp1gvo"
path="res://.godot/imported/tool_1.wav-95f63081100f01797467f03e9e737afd.sample"
[deps]
source_file="res://assets/sounds/freighter/tool_1.wav"
dest_files=["res://.godot/imported/tool_1.wav-95f63081100f01797467f03e9e737afd.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

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://wgkgk17ypntp"
path="res://.godot/imported/tool_2.wav-89973aa6096ce92b6dd98a99035476ee.sample"
[deps]
source_file="res://assets/sounds/freighter/tool_2.wav"
dest_files=["res://.godot/imported/tool_2.wav-89973aa6096ce92b6dd98a99035476ee.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

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://d4ueit46182u"
path="res://.godot/imported/tool_3.wav-9bf32979fcf03e567d9386f3bec8fb18.sample"
[deps]
source_file="res://assets/sounds/freighter/tool_3.wav"
dest_files=["res://.godot/imported/tool_3.wav-9bf32979fcf03e567d9386f3bec8fb18.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

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dlik8ewbunh3h"
path="res://.godot/imported/ventilation_loop.wav-806b0880cd20957493e88125437cc747.sample"
[deps]
source_file="res://assets/sounds/freighter/ventilation_loop.wav"
dest_files=["res://.godot/imported/ventilation_loop.wav-806b0880cd20957493e88125437cc747.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

View File

@ -896,3 +896,35 @@ Warstwy przy fregacie i muzyka dokowania (18.10.2026):
podczas pauzy dokowania oraz pobytu na powierzchni, bez restartu. podczas pauzy dokowania oraz pobytu na powierzchni, bez restartu.
- Rozszerzony test freighter_interior_preview przeszedł: kolejność - Rozszerzony test freighter_interior_preview przeszedł: kolejność
warstw i rosnąca pozycja odtwarzania muzyki podczas zadokowania. warstw i rosnąca pozycja odtwarzania muzyki podczas zadokowania.
Realistyczne audio wnętrza fregaty (18.10.2026):
- Nowe audio jest montażem nagrań realnych urządzeń, narzędzi i pary
na licencji CC0: LEGIT Audio (The Shop), bart (68 Workshop Sounds,
Steam release sounds). Kenney dostarcza dwa ciche efekty elektroniki.
Sygnał serwisowy i alarm techniczny są oryginalną syntezą dwóch tonów.
- Zestaw: 2 pętle instalacji/wentylacji, 3 narzędzia, 2 metaliczne odgłosy,
otwarcie/zamknięcie śluzy, wyrównanie ciśnienia, zaciski dokujące,
sygnał serwisowy, alarm techniczny i 2 efekty konsol (15 plików).
- Edycja obejmuje usunięcie DC, filtrowanie, delikatne obniżenie tonu,
warstwy mechaniczne, krótkie odbicia stereo, normalizację i płynne pętle.
Pliki WAV: 48 kHz, stereo, PCM16, bez przesterowania.
- FreighterAudio działa wyłącznie w scenie wnętrza. Dwie pętle narastają
łagodnie i grają pod nieprzerwaną muzyką. Narzędzia odzywają się
nieregularnie co 10..23 s, konsole co 24..48 s, zawory co 38..75 s.
- Śluzy reagują na wybór i zamknięcie pomieszczenia. Panorama stereo
odpowiada jego położeniu na pokładzie. Hover nie uruchamia dźwięku;
powtarzane kliknięcia są ograniczone, polifonia efektów wynosi 4.
- Alarm serwisowy występuje raz po wejściu z lądownikiem poniżej 30 HP.
Naprawa anuluje oczekujący alarm. Nie ma losowych alarmów awaryjnych.
- Po odlocie odtwarzacze i tymczasowe szyny panoramy są zwalniane.
Muzyka nie jest zatrzymywana ani restartowana.
- Źródła i licencje: assets/sounds/freighter/CREDITS.md; dokładny montaż
i mapowanie próbek: manifest.json w tym samym katalogu. Narzędzia
odtworzenia zestawu: tools/audio/fetch_freighter_sources.py oraz
tools/audio/build_freighter_audio.py (Python + numpy).
- Testy przeszły: freighter_assets_test.py (13 WAV, format, brak DC,
brak clippingu i ciągłość pętli), freighter_audio_test.tscn (15 strumieni,
granie podczas pauzy razem z muzyką, debounce, polifonia, alarm,
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).

View File

@ -0,0 +1,15 @@
shader_type canvas_item;
uniform vec4 ground_tint : source_color = vec4(0.3,0.32,0.22,1.0);
varying vec4 vertex_tint;
void vertex() { vertex_tint=COLOR; }
void fragment() {
vec4 ore=texture(TEXTURE,UV);
float radius=length((UV-vec2(0.5))*vec2(2.0,2.25));
float fringe=smoothstep(0.40,0.82,radius);
float feather=1.0-smoothstep(0.65,0.96,radius);
float luminosity=dot(ore.rgb,vec3(0.299,0.587,0.114));
vec3 soil=ground_tint.rgb*(0.60+luminosity*0.85);
// Only the dispersed soil rim changes hue; the central ore stays legible.
vec3 color=mix(ore.rgb,soil,fringe*0.66);
COLOR=vec4(color*vertex_tint.rgb,ore.a*vertex_tint.a*feather);
}

View File

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

View File

@ -1,7 +1,8 @@
[gd_scene load_steps=4 format=3] [gd_scene load_steps=5 format=3]
[ext_resource type="Script" path="res://scripts/planets/freighter_screen.gd" id="1"] [ext_resource type="Script" path="res://scripts/planets/freighter_screen.gd" id="1"]
[ext_resource type="Script" path="res://scripts/planets/freighter_deck.gd" id="2"] [ext_resource type="Script" path="res://scripts/planets/freighter_deck.gd" id="2"]
[ext_resource type="Texture2D" path="res://assets/freighter/interior_deck.png" id="3"] [ext_resource type="Texture2D" path="res://assets/freighter/interior_deck.png" id="3"]
[ext_resource type="Script" path="res://scripts/planets/freighter_audio.gd" id="4"]
[node name="FreighterInterior" type="Control"] [node name="FreighterInterior" type="Control"]
process_mode = 3 process_mode = 3
layout_mode = 3 layout_mode = 3
@ -11,6 +12,9 @@ anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
script = ExtResource("1") script = ExtResource("1")
[node name="FreighterAudio" type="Node" parent="."]
process_mode = 3
script = ExtResource("4")
[node name="Background" type="ColorRect" parent="."] [node name="Background" type="ColorRect" parent="."]
layout_mode = 1 layout_mode = 1
anchors_preset = 15 anchors_preset = 15

View File

@ -0,0 +1,134 @@
extends Node
## Quiet industrial foley built from CC0 recordings; lives only inside the deck.
const ROOT := "res://assets/sounds/freighter/"
const VOICE_LIMIT := 4
var _streams: Dictionary = {}
var _beds: Array[AudioStreamPlayer] = []
var _voices: Array[AudioStreamPlayer] = []
var _panners: Array[AudioEffectPanner] = []
var _buses: Array[StringName] = []
var _rng := RandomNumberGenerator.new()
var _elapsed := 0.0
var _last_action := -10.0
var _last_service := -10.0
var _last_section := ""
var _next_tool := 9.0
var _next_console := 18.0
var _next_pressure := 32.0
var _last_tool := ""
var played_count := 0
var _maintenance_needed := false
var _alarm_due := INF
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
_rng.randomize()
for id in ["reactor_loop","ventilation_loop","tool_1","tool_2","tool_3","metal_1","metal_2","airlock_open","airlock_close","pressure_release","docking_clamp","service_chime","maintenance_alarm"]:
_streams[id] = load(ROOT+id+".wav")
for id in ["console_1","console_2"]: _streams[id] = load(ROOT+id+".ogg")
_start_bed("reactor_loop",-14.0)
_start_bed("ventilation_loop",-21.0)
for index in VOICE_LIMIT:
var bus := StringName("Frigate_%d_%d" % [get_instance_id(),index])
AudioServer.add_bus()
var bus_index := AudioServer.bus_count-1
AudioServer.set_bus_name(bus_index,bus)
AudioServer.set_bus_send(bus_index,&"Master")
var panner := AudioEffectPanner.new()
AudioServer.add_bus_effect(bus_index,panner)
_buses.append(bus)
_panners.append(panner)
var voice := AudioStreamPlayer.new()
voice.name = "Foley%d" % index
voice.bus = bus
add_child(voice)
_voices.append(voice)
play_effect("docking_clamp",0.65,-13.0,true)
_next_tool = _rng.randf_range(8,15)
_next_console = _rng.randf_range(18,30)
_next_pressure = _rng.randf_range(32,55)
func _start_bed(id: String, level: float) -> void:
var player := AudioStreamPlayer.new()
player.name = id.to_pascal_case()
var stream := (_streams[id] as AudioStreamWAV).duplicate() as AudioStreamWAV
stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
stream.loop_begin = 0
stream.loop_end = roundi(stream.get_length()*stream.mix_rate)
player.stream = stream
player.volume_db = -60
add_child(player)
player.play()
_beds.append(player)
create_tween().tween_property(player,"volume_db",level,1.2)
func play_effect(id: String, pan: float, level := -15.0, important := false) -> bool:
if not _streams.has(id): return false
var available := -1
for index in _voices.size():
if not _voices[index].playing:
available = index
break
if available < 0:
if not important: return false
available = 0
for index in _voices.size():
if float(_voices[index].get_meta("started",0.0)) < float(_voices[available].get_meta("started",0.0)): available = index
var voice := _voices[available]
voice.stop()
voice.stream = _streams[id]
voice.volume_db = level
voice.pitch_scale = 1.0 if id == "service_chime" else _rng.randf_range(0.97,1.03)
_panners[available].pan = clampf(pan,-0.75,0.75)
voice.set_meta("started",_elapsed)
voice.play()
played_count += 1
return true
func enter_section(id: String, pan: float) -> void:
if id == _last_section or _elapsed-_last_action < 0.7: return
_last_action = _elapsed
_last_section = id
play_effect("airlock_open",pan,-13.5,true)
func close_section() -> void:
if _last_section.is_empty(): return
_last_section = ""
if _elapsed-_last_action < 0.7: return
_last_action = _elapsed
play_effect("airlock_close",0.0,-17.0)
func service_completed(pan := 0.0) -> void:
if _elapsed-_last_service < 0.7: return
_last_service = _elapsed
play_effect("service_chime",pan,-20.0)
func set_maintenance_needed(value: bool) -> void:
if value and not _maintenance_needed: _alarm_due = _elapsed+4.0
if not value: _alarm_due = INF
_maintenance_needed = value
func _process(delta: float) -> void:
_elapsed += delta
if _elapsed >= _alarm_due:
play_effect("maintenance_alarm",0.6,-22.0)
_alarm_due = INF # One advisory per damaged-lander visit, no false emergency loop.
if _elapsed >= _next_tool:
var choices := ["tool_1","tool_2","tool_3","metal_1","metal_2"]
choices.erase(_last_tool)
_last_tool = choices[_rng.randi_range(0,choices.size()-1)]
play_effect(_last_tool,-0.48,_rng.randf_range(-17,-13))
_next_tool = _elapsed+_rng.randf_range(10,23)
if _elapsed >= _next_console:
play_effect("console_%d" % _rng.randi_range(1,2),-0.7,-25.0)
_next_console = _elapsed+_rng.randf_range(24,48)
if _elapsed >= _next_pressure:
play_effect("pressure_release",0.6,-23.0)
_next_pressure = _elapsed+_rng.randf_range(38,75)
func _exit_tree() -> void:
for voice in _voices: voice.stop()
for bed in _beds: bed.stop()
for bus in _buses:
var index := AudioServer.get_bus_index(bus)
if index >= 0: AudioServer.remove_bus(index)

View File

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

View File

@ -13,6 +13,7 @@ var _wallet: Label
var _hint: Label var _hint: Label
var _owned_open := false var _owned_open := false
@onready var deck: Control = $Deck @onready var deck: Control = $Deck
@onready var _audio: Node = $FreighterAudio
func _ready() -> void: func _ready() -> void:
if captain == null: if captain == null:
@ -109,11 +110,15 @@ func _hover_section(section: String) -> void:
_hint.text = "%s / %s" % [data.title,data.status] if not data.is_empty() else "Wybierz pomieszczenie na pokładzie. · Warsztat i hangar są dostępne." _hint.text = "%s / %s" % [data.title,data.status] if not data.is_empty() else "Wybierz pomieszczenie na pokładzie. · Warsztat i hangar są dostępne."
func _select_section(section: String) -> void: func _select_section(section: String) -> void:
var room: Dictionary = deck.room("expansion_3" if section == "storage" else section)
var pan: float = 0.0 if room.is_empty() else ((room.rect as Rect2).get_center().x / deck.DESIGN_SIZE.x - 0.5)*1.4
_audio.enter_section(section,pan)
_tab = section _tab = section
deck.selected = section deck.selected = section
_refresh() _refresh()
func _close_section() -> void: func _close_section() -> void:
_audio.close_section()
_tab = "overview" _tab = "overview"
deck.selected = "" deck.selected = ""
_drawer.hide() _drawer.hide()
@ -139,6 +144,7 @@ func _label(text: String, size_value := 14, color := Color("c5d3d7")) -> Label:
return node return node
func _refresh() -> void: func _refresh() -> void:
_audio.set_maintenance_needed(captain.expedition.lander_hp < 30)
for child in _content.get_children(): for child in _content.get_children():
_content.remove_child(child) _content.remove_child(child)
child.queue_free() child.queue_free()
@ -185,6 +191,7 @@ func _construct_room(id: String) -> void:
captain.expedition.freighter_rooms[id] = str(deck.plan(id).kind) captain.expedition.freighter_rooms[id] = str(deck.plan(id).kind)
captain.expedition.emit_changed() captain.expedition.emit_changed()
deck.sync_rooms(captain.expedition.freighter_rooms) deck.sync_rooms(captain.expedition.freighter_rooms)
_audio.service_completed()
_hover_section(id) _hover_section(id)
_refresh() _refresh()
@ -201,7 +208,7 @@ func _build_hangar() -> void:
var cost: int = progress.repair_cost() var cost: int = progress.repair_cost()
_label("Części: %d · potrzebne do naprawy: %d" % [captain.parts,cost],13) _label("Części: %d · potrzebne do naprawy: %d" % [captain.parts,cost],13)
var repair := UI.button(_content,"NAPRAW DO 100%% · %d części" % cost,func(): var repair := UI.button(_content,"NAPRAW DO 100%% · %d części" % cost,func():
progress.repair(captain) if progress.repair(captain): _audio.service_completed(0.65)
_refresh()) _refresh())
repair.disabled = cost == 0 or captain.parts < cost repair.disabled = cost == 0 or captain.parts < cost
_label("1 część przywraca 1 punkt procentowy HP lądownika. Do następnego lądowania wymagany jest pełny kadłub.",13,Color("819ca5")) _label("1 część przywraca 1 punkt procentowy HP lądownika. Do następnego lądowania wymagany jest pełny kadłub.",13,Color("819ca5"))
@ -356,6 +363,7 @@ func _craft(kind: String) -> void:
var gear: Dictionary = GEAR.craft(captain,kind) var gear: Dictionary = GEAR.craft(captain,kind)
if not gear.is_empty(): if not gear.is_empty():
GEAR.equip(captain,kind,int(gear.uid)) GEAR.equip(captain,kind,int(gear.uid))
_audio.service_completed(-0.48)
_refresh() _refresh()
func _equip(kind: String, uid: int) -> void: func _equip(kind: String, uid: int) -> void:

View File

@ -0,0 +1,21 @@
extends Sprite2D
## Static world-space ore: no animated shimmer, screen-space noise or UV snapping.
const ORE := preload("res://assets/planets/mineral_deposit.png")
const WIDTH := 128.0
func configure(map: Dictionary, poi: Dictionary) -> void:
name = "MineralDeposit%d" % int(poi.id)
texture = ORE
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
position = poi.position
z_index = 10
var rng := RandomNumberGenerator.new()
rng.seed = int(map.seed) ^ (int(poi.id)*7919) ^ 0x4f524553
rotation = rng.randf_range(-0.35,0.35)
scale = Vector2.ONE*WIDTH*rng.randf_range(0.95,1.08)/maxf(ORE.get_width(),ORE.get_height())
var blend := ShaderMaterial.new()
blend.shader = preload("res://resources/effects/mineral_deposit.gdshader")
var tint: Color = (map.landscape.low as Color).lerp(map.landscape.high,0.4)*0.8
if int(map.profile.temperature) < -10: tint = tint.lerp(Color("b0bbb4"),0.55)
blend.set_shader_parameter("ground_tint",tint)
material = blend

View File

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

View File

@ -56,10 +56,14 @@ func _ready() -> void:
add_child(mountains) add_child(mountains)
for poi in data.pois: for poi in data.pois:
var sprite := Sprite2D.new() var sprite := Sprite2D.new()
sprite.texture = load("res://assets/planets/%s.png" % ("ruins" if poi.kind == "ruins" else "mine")) if poi.kind != "deposit" else load("res://assets/icons/resources/titanium.png") if poi.kind == "deposit":
sprite.position = poi.position sprite.set_script(preload("res://scripts/planets/surface_deposit.gd"))
sprite.scale = Vector2.ONE * (125.0*BUILDING_SCALE if poi.kind != "deposit" else 60.0) / maxf(sprite.texture.get_width(), sprite.texture.get_height()) sprite.configure(data,poi)
sprite.z_index = 10 else:
sprite.texture = load("res://assets/planets/%s.png" % ("ruins" if poi.kind == "ruins" else "mine"))
sprite.position = poi.position
sprite.scale = Vector2.ONE * 125.0*BUILDING_SCALE / maxf(sprite.texture.get_width(), sprite.texture.get_height())
sprite.z_index = 10
add_child(sprite) add_child(sprite)
_poi_sprites[poi.id] = sprite _poi_sprites[poi.id] = sprite
var label := Label.new() var label := Label.new()

View File

@ -0,0 +1,26 @@
"""Technical QA for authored frigate audio (Python + numpy)."""
from pathlib import Path
import wave
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
files = list((ROOT / "assets/sounds/freighter").glob("*.wav"))
assert len(files) == 13
seams = []
for path in files:
with wave.open(str(path)) as stream:
assert stream.getnchannels() == 2
assert stream.getsampwidth() == 2
assert 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, path.name
assert np.max(np.abs(signal.mean(axis=0))) < .01, path.name
if path.stem.endswith("loop"):
difference = float(np.max(np.abs(signal[0]-signal[-1])))
assert difference < .05, (path.name,difference)
seams.append((path.name,round(difference,6)))
with wave.open(str(ROOT / "artifacts/freighter_ambience_demo.wav")) as stream:
assert stream.getnchannels() == 2
assert stream.getnframes()/stream.getframerate() == 34
print("AUDIO ASSET QA PASS: 13 stereo PCM16 WAV files at 48 kHz; no clipping/DC; loop seam deltas:",seams)

View File

@ -0,0 +1,61 @@
extends Node
func _ready() -> void:
GameState.start_new_run()
var game := preload("res://scenes/game/game.tscn").instantiate()
add_child(game)
await get_tree().process_frame
var manager := game.get_node("PlanetExpeditions")
var music: AudioStreamPlayer = game.get_node("GameMusic")
var original_buses := AudioServer.bus_count
for visit in 3:
manager.open_freighter()
var screen: Control = manager.screen
var audio: Node = screen.get_node("FreighterAudio")
assert(get_tree().paused and audio.can_process())
assert(audio._streams.size() == 15)
assert(audio._beds.size() == 2 and audio._voices.size() == 4)
assert(AudioServer.bus_count == original_buses+4)
for id in audio._streams:
var stream: AudioStream = audio._streams[id]
assert(stream != null and stream.get_length() > 0.1)
for bed in audio._beds:
assert(bed.stream.loop_mode == AudioStreamWAV.LOOP_FORWARD)
assert(bed.stream.loop_end > bed.stream.loop_begin)
assert(bed.playing and not bed.stream_paused)
var music_before := music.get_playback_position()
var bed_before: float = audio._beds[0].get_playback_position()
await get_tree().create_timer(0.25,true).timeout
assert(music.get_playback_position() > music_before)
assert(audio._beds[0].get_playback_position() > bed_before)
screen._select_section("workshop")
var actions: int = audio.played_count
screen._select_section("workshop")
screen._hover_section("hangar")
assert(audio.played_count == actions,"Repeated clicks and hover must not spam doors")
audio._next_tool = audio._elapsed
audio._process(0.1)
assert(audio._last_tool != "")
for index in 20: audio.play_effect("metal_1",0,-20,true)
assert(audio._voices.size() == 4,"Rapid actions must keep bounded polyphony")
for voice in audio._voices: voice.stop()
audio.set_maintenance_needed(true)
audio._next_tool = INF
audio._next_console = INF
audio._next_pressure = INF
audio._process(4.1)
assert(is_inf(audio._alarm_due))
var warnings: int = audio.played_count
audio._process(1.0)
assert(audio.played_count == warnings,"No repeated false emergency loop")
audio.set_maintenance_needed(false)
assert(is_inf(audio._alarm_due))
screen.closed.emit()
assert(not get_tree().paused)
assert(AudioServer.bus_count == original_buses,"All temporary panning buses must be removed on departure")
assert(music.playing and not music.stream_paused)
await get_tree().process_frame
game.queue_free()
await get_tree().process_frame
print("FREIGHTER AUDIO PASS: 15 streams, seamless loop configuration, paused playback with uninterrupted music, room foley, hover/click debounce, bounded voices, one maintenance warning, cleanup across three docking visits")
get_tree().quit()

View File

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

View File

@ -0,0 +1,4 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/levels/freighter_audio_test.gd" id="1"]
[node name="FreighterAudioTest" type="Node"]
script = ExtResource("1")

View File

@ -0,0 +1,86 @@
extends Node
func settle() -> void:
for frame in 4: await get_tree().process_frame
await RenderingServer.frame_post_draw
func difference(a: Image, b: Image, center: Vector2) -> float:
var total := 0.0
var samples := 0
for y in range(int(center.y)-55,int(center.y)+55,2):
for x in range(int(center.x)-55,int(center.x)+55,2):
if x < 0 or y < 0 or x >= a.get_width() or y >= a.get_height(): continue
var first := a.get_pixel(x,y)
var next := b.get_pixel(x,y)
total += absf(first.r-next.r)+absf(first.g-next.g)+absf(first.b-next.b)
samples += 3
return total/maxi(1,samples)
func _ready() -> void:
var sheet := Image.create(1280,1080,false,Image.FORMAT_RGBA8)
var max_pan_change := 0.0
var progress: Resource = GameState.captain.expedition
for biome in 6:
var profile: Dictionary = preload("res://scripts/planets/planet_profiles.gd").generate(99,biome,"Złoże / biom %d" % (biome+1))
profile.temperature = 75 if biome == 4 else (-35 if biome == 5 else 20)
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_process(false)
surface.set_physics_process(false)
surface.lander.hide()
surface._surface_viewport.get_node("SurfaceAtmosphere").hide()
var target := Vector2.INF
var chosen: Sprite2D
for poi in surface.world.data.pois:
if poi.kind != "deposit": continue
var sprite: Sprite2D = surface.world._poi_sprites[poi.id]
assert(sprite.texture.resource_path == "res://assets/planets/mineral_deposit.png")
assert(sprite.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS)
assert(sprite.texture.get_image().has_mipmaps())
assert(sprite.material.shader.resource_path.ends_with("mineral_deposit.gdshader"))
assert(not sprite.material.shader.code.contains("TIME"))
if poi.position.length_squared() < target.length_squared():
target = poi.position
chosen = sprite
assert(chosen != null)
surface.world.refresh_pois(target)
surface.camera.position_smoothing_enabled = false
surface.camera.position = target
surface.camera.force_update_scroll()
await settle()
var origin: Vector2 = chosen.get_global_transform_with_canvas().origin/surface.SURFACE_RENDER_SCALE
var initial: Image = get_viewport().get_texture().get_image()
initial.save_png("res://artifacts/mineral_deposit_biome_%d.png" % biome)
var small := initial.duplicate() as Image
small.convert(Image.FORMAT_RGBA8)
small.resize(640,360,Image.INTERPOLATE_LANCZOS)
sheet.blit_rect(small,Rect2i(0,0,640,360),Vector2i(biome%2*640,biome/2*360))
if biome == 0:
await settle()
assert(difference(initial,get_viewport().get_texture().get_image(),origin) < 0.0001,"Stationary ore must not flicker")
var previous := initial
for frame in 12:
surface.camera.position = target+Vector2((frame+1)*0.25,0)
surface.camera.force_update_scroll()
await settle()
var current: Image = get_viewport().get_texture().get_image()
var change := difference(previous,current,origin)
max_pan_change = maxf(max_pan_change,change)
assert(change < 0.035,"Subpixel camera motion should remain smoothly filtered")
previous = current
surface.camera.position = target
surface.camera.force_update_scroll()
await settle()
assert(difference(initial,get_viewport().get_texture().get_image(),origin) < 0.0001,"Returning to the same camera position restores the same pixels")
progress.finish_visit(GameState.captain,false)
surface.queue_free()
await get_tree().process_frame
sheet.save_png("res://artifacts/mineral_deposit_biomes.png")
print("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=",max_pan_change)
get_tree().quit()

View File

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

View File

@ -0,0 +1,4 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/previews/surface_deposit_test.gd" id="1"]
[node name="SurfaceDepositTest" type="Node"]
script = ExtResource("1")

View File

@ -0,0 +1,163 @@
"""Edit CC0 field recordings into the frigate's restrained industrial soundscape.
Run fetch_freighter_sources.py first. Requires numpy, no audio service/API.
Output: 48 kHz, stereo, PCM16 WAV. Source credits live beside the assets.
"""
from pathlib import Path
import json
import shutil
import struct
import wave
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "tmp/freighter_audio_sources"
OUT = ROOT / "assets/sounds/freighter"
RATE = 48000
MANIFEST = {}
def read(relative):
raw = (SOURCE / relative).read_bytes()
cursor = 12
fmt = data = None
while cursor + 8 <= len(raw):
name, size = struct.unpack_from("<4sI", raw, cursor)
chunk = raw[cursor+8:cursor+8+size]
if name == b"fmt ": fmt = chunk
if name == b"data": data = chunk
cursor += 8 + size + size % 2
kind, channels, rate, _, _, bits = struct.unpack_from("<HHIIHH", fmt)
if kind == 0xFFFE: kind = struct.unpack_from("<H", fmt, 24)[0]
if kind == 3:
signal = np.frombuffer(data, dtype="<f4" if bits == 32 else "<f8").astype(float)
elif bits == 24:
values = np.frombuffer(data, dtype=np.uint8).reshape(-1,3).astype(np.int32)
values = values[:,0] | values[:,1] << 8 | values[:,2] << 16
signal = ((values ^ 0x800000) - 0x800000) / 8388608.0
else:
signal = np.frombuffer(data, dtype="<i2" if bits == 16 else "<i4").astype(float) / 2**(bits-1)
signal = signal.reshape(-1,channels)
if channels == 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 peak(x, db=-3):
return x * (10**(db/20) / max(1e-9,float(np.max(np.abs(x)))))
def eq(x, low=35, high=6000):
frequencies = np.fft.rfftfreq(len(x),1/RATE)
response = (1-np.exp(-(frequencies/low)**4))*np.exp(-(frequencies/high)**4)
return np.fft.irfft(np.fft.rfft(x,axis=0)*response[:,None],n=len(x),axis=0)
def pitch(x, ratio):
at = np.arange(0,len(x)-1,ratio)
return np.stack([np.interp(at,np.arange(len(x)),x[:,c]) for c in range(2)],axis=1)
def fade(x, start=.018, end=.20):
x = x.copy()
a, b = min(int(start*RATE),len(x)//2), min(int(end*RATE),len(x)//2)
if a: x[:a] *= np.linspace(0,1,a)[:,None]
if b: x[-b:] *= np.linspace(1,0,b)[:,None]
return x
def trim(x):
block = 480
length = len(x)//block*block
rms = np.sqrt((x[:length].reshape(-1,block,2)**2).mean(axis=(1,2)))
active = np.where(rms > max(rms.max()*.045,.00001))[0]
if not len(active): raise ValueError("Silent recording")
return x[max(0,int(active[0])*block-960):min(len(x),(int(active[-1])+1)*block+4800)]
def room(x, amount=.14):
# Short non-musical reflections, staggered between left and right walls.
output = np.pad(x,((0,int(RATE*.65)),(0,0)))
wet = eq(x,80,3300)
for index, delay in enumerate([.037,.061,.089,.127,.173,.229,.307,.413,.521]):
offset = int(delay*RATE)
output[offset:offset+len(x)] += np.roll(wet,index%2,axis=1)*amount*.76**index
return output
def mix(*entries):
length = max(int(offset*RATE)+len(x) for x,offset,gain in entries)
output = np.zeros((length,2))
for x,offset,gain in entries:
start = int(offset*RATE)
output[start:start+len(x)] += x*gain
return output
def loop(x, seconds=1.2):
n = min(int(seconds*RATE),len(x)//4)
ramp = np.linspace(0,1,n)[:,None]
return np.concatenate([x[n:-n],x[-n:]*(1-ramp)+x[:n]*ramp])
def save(name, x, sources, processing, looping=False):
x = peak(x,-3.5 if looping else -2)
assert np.isfinite(x).all() and np.max(np.abs(x)) < 1
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":round(20*np.log10(np.abs(x).max()),2),"rms_dbfs":round(20*np.log10(np.sqrt((x*x).mean())),2)}
def workshop(name): return "workshop/workshop/workshop - "+name+".wav"
def sample(name, ratio=1, high=6000): return peak(eq(pitch(trim(read(workshop(name))),ratio),high=high))
def main():
OUT.mkdir(parents=True,exist_ok=True)
fridge = "shop/TheShopCollection_convenience_store_drinks_fridge_drone.wav"
fridge2 = "shop/TheShopCollection_convenience_store_drinks_fridge_drone_2.wav"
reactor = loop(eq(pitch(read(fridge),.64),low=28,high=1150))
save("reactor_loop",reactor,[fridge],"Slowed 0.64x; DC removal; band limiting 28–1150 Hz; 1.2 s loop crossfade",True)
vent = loop(eq(read(fridge2),low=110,high=2600))
save("ventilation_loop",vent,[fridge2],"DC removal; band limiting 110–2600 Hz; 1.2 s loop crossfade",True)
for i,name in enumerate(["drill short 1","drill short 2","ratchet1"],1):
x = sample(name,.88 if i<3 else .94,4700)
save("tool_%d"%i,fade(room(x,.12)),[workshop(name)],"Trim, reduced motor speed, softened highs, short industrial room reflections")
for i,name in enumerate(["tool clatter","metal tapping"],1):
save("metal_%d"%i,fade(room(sample(name,.9,4200),.15)),[workshop(name)],"Trim, mild pitch lowering, low-pass, short stereo reflections")
steam_path = "steam/steam hisses - Marker #1.wav"
steam = peak(eq(trim(read(steam_path)),low=150,high=5200))
steam = fade(steam[:int(2.2*RATE)],.06,.55)
slide = fade(sample("dragging metal",.72,2900)[:int(1.5*RATE)],.1,.3)
latch = fade(sample("clink thud",.78,3700)[:int(.65*RATE)])
save("airlock_open",fade(room(mix((latch,0,.52),(steam,.12,.38),(slide,.38,.68)),.17)),[workshop("clink thud"),steam_path,workshop("dragging metal")],"Layered latch, pressure equalization, slow metal slide; EQ and shared room reflections")
save("airlock_close",fade(room(mix((slide,0,.6),(steam,.2,.24),(latch,1.18,.65)),.17)),[workshop("dragging metal"),steam_path,workshop("clink thud")],"Metal slide, pressure seal and locking impact; shared short reflections")
save("pressure_release",fade(room(steam,.11)),[steam_path],"Softened steam recording, pressure-release envelope, short reflections")
clamp = mix((sample("low hammering",.75,3000)[:int(.6*RATE)],0,.7),(latch,.33,.4))
save("docking_clamp",fade(room(clamp,.20)),[workshop("low hammering"),workshop("clink thud")],"Layered low mechanical impact and latch, softened transient, stereo reflections")
# Alarms are naturally electronic: restrained two-tone annunciator, not a siren bed.
length = int(1.5*RATE)
t = np.arange(length)/RATE
tone = np.zeros(length)
for offset,freq in [(0.12,540),(.54,680)]:
local = t-offset
env = np.where((local>=0)&(local<.24),np.sin(np.pi*np.clip(local/.24,0,1))**2,0)
tone += env*(np.sin(2*np.pi*freq*t)+.11*np.sin(2*np.pi*freq*2*t))
save("service_chime",fade(room(np.repeat(tone[:,None],2,axis=1),.13)),[],"Original two-tone 540/680 Hz service annunciator, smooth envelope, short reflections")
warning = mix((np.repeat(tone[:,None],2,axis=1),0,.8),(np.repeat(tone[:,None],2,axis=1),1.15,.7))
save("maintenance_alarm",fade(room(warning,.19)),[],"Original restrained maintenance warning, two paired tones with room reflections")
for i in [0,1]:
source = "scifi/Audio/computerNoise_%03d.ogg"%i
shutil.copyfile(SOURCE/source,OUT/("console_%d.ogg"%(i+1)))
MANIFEST["console_%d"%(i+1)] = {"sources":[source],"processing":"Unmodified CC0 Kenney electronic effect, mixed quietly in-engine"}
(OUT/"manifest.json").write_text(json.dumps(MANIFEST,indent=2,ensure_ascii=False),encoding="utf-8")
preview = np.zeros((int(34*RATE),2))
for name,db in [("reactor_loop",-14),("ventilation_loop",-21)]:
bed = read(OUT/(name+".wav"))
preview += bed[np.arange(len(preview))%len(bed)]*10**(db/20)
for name,offset,db,pan in [("docking_clamp",1,-13,.65),("airlock_open",3,-13.5,-.48),("tool_1",8,-13,-.48),("metal_1",13,-15,-.48),("tool_3",17,-15,-.48),("pressure_release",23,-23,.6),("maintenance_alarm",26,-22,-.7),("airlock_close",30,-17,.2)]:
clip = read(OUT/(name+".wav"))*10**(db/20)
clip *= np.array([min(1,1-pan),min(1,1+pan)])
start = int(offset*RATE)
preview[start:start+len(clip)] += clip[:len(preview)-start]
preview = fade(peak(preview,-3),1.0,1.4)
path = ROOT/"artifacts/freighter_ambience_demo.wav"
with wave.open(str(path),"wb") as stream:
stream.setparams((2,2,RATE,0,"NONE","not compressed"))
stream.writeframes((preview*32767).astype("<i2").tobytes())
print(json.dumps(MANIFEST,indent=2))
if __name__ == "__main__": main()

View File

@ -0,0 +1,51 @@
"""Download the CC0 source recordings used by build_freighter_audio.py."""
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import urllib.request
import zipfile
import subprocess
ROOT = Path(__file__).resolve().parents[2]
DEST = ROOT / "tmp" / "freighter_audio_sources"
SOURCES = {
"shop.zip": "https://opengameart.org/sites/default/files/legit_audio_-_the_shop_free_sfx_wav.zip",
"workshop.7z": "https://opengameart.org/sites/default/files/workshop.7z",
"steam.zip": "https://opengameart.org/sites/default/files/steam_hisses.zip",
"scifi.zip": "https://opengameart.org/sites/default/files/sci-fi_sounds.zip",
}
def fetch(entry):
name, url = entry
target = DEST / name
if not target.exists():
request = urllib.request.Request(url, headers={"User-Agent": "Randoloot audio asset preparation"})
with urllib.request.urlopen(request, timeout=90) as response:
target.write_bytes(response.read())
if name.endswith(".zip"):
with zipfile.ZipFile(target) as archive:
names = archive.namelist()
for member in archive.infolist():
output = (DEST / name.removesuffix(".zip") / member.filename).resolve()
if not output.is_relative_to(DEST.resolve()):
raise ValueError("Unsafe archive path")
if not member.is_dir():
output.parent.mkdir(parents=True, exist_ok=True)
output.write_bytes(archive.read(member))
else:
names = subprocess.check_output(["tar", "-tf", str(target)], text=True).splitlines()
folder = DEST / "workshop"
folder.mkdir(exist_ok=True)
for name_in_archive in names:
output = (folder / name_in_archive).resolve()
if not output.is_relative_to(folder.resolve()):
raise ValueError("Unsafe archive path")
subprocess.run(["tar", "-xf", str(target), "-C", str(folder)], check=True)
return name, [name for name in names if name.lower().endswith((".wav", ".ogg", ".txt"))]
if __name__ == "__main__":
DEST.mkdir(parents=True, exist_ok=True)
(DEST / ".gdignore").write_text("")
with ThreadPoolExecutor(max_workers=4) as pool:
for name, files in pool.map(fetch, SOURCES.items()):
print(name)
print("\n".join(files))