Zmiany
39
artifacts/deposit_import.log
Normal file
@ -0,0 +1,39 @@
|
||||
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
|
||||
7
artifacts/deposit_test.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: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
|
||||
0
artifacts/deposit_test_errors.log
Normal file
BIN
artifacts/freighter_ambience_demo.wav
Normal file
24
artifacts/freighter_ambience_demo.wav.import
Normal 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
|
||||
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 878 KiB After Width: | Height: | Size: 871 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 844 KiB After Width: | Height: | Size: 841 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 820 KiB After Width: | Height: | Size: 817 KiB |
BIN
artifacts/mineral_deposit_biome_0.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
artifacts/mineral_deposit_biome_1.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
artifacts/mineral_deposit_biome_2.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
artifacts/mineral_deposit_biome_3.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
artifacts/mineral_deposit_biome_4.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
artifacts/mineral_deposit_biome_5.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
artifacts/mineral_deposit_biomes.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
assets/planets/mineral_deposit.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
40
assets/planets/mineral_deposit.png.import
Normal 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
|
||||
6
assets/planets/mineral_deposit_prompt.json
Normal 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."
|
||||
}
|
||||
32
assets/sounds/freighter/CREDITS.md
Normal 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).
|
||||
BIN
assets/sounds/freighter/airlock_close.wav
Normal file
24
assets/sounds/freighter/airlock_close.wav.import
Normal 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
|
||||
BIN
assets/sounds/freighter/airlock_open.wav
Normal file
24
assets/sounds/freighter/airlock_open.wav.import
Normal 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
|
||||
BIN
assets/sounds/freighter/console_1.ogg
Normal file
19
assets/sounds/freighter/console_1.ogg.import
Normal 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
|
||||
BIN
assets/sounds/freighter/console_2.ogg
Normal file
19
assets/sounds/freighter/console_2.ogg.import
Normal 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
|
||||
BIN
assets/sounds/freighter/docking_clamp.wav
Normal file
24
assets/sounds/freighter/docking_clamp.wav.import
Normal 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
|
||||
BIN
assets/sounds/freighter/maintenance_alarm.wav
Normal file
24
assets/sounds/freighter/maintenance_alarm.wav.import
Normal 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
|
||||
145
assets/sounds/freighter/manifest.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
assets/sounds/freighter/metal_1.wav
Normal file
24
assets/sounds/freighter/metal_1.wav.import
Normal 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
|
||||
BIN
assets/sounds/freighter/metal_2.wav
Normal file
24
assets/sounds/freighter/metal_2.wav.import
Normal 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
|
||||
BIN
assets/sounds/freighter/pressure_release.wav
Normal file
24
assets/sounds/freighter/pressure_release.wav.import
Normal 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
|
||||
BIN
assets/sounds/freighter/reactor_loop.wav
Normal file
24
assets/sounds/freighter/reactor_loop.wav.import
Normal 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
|
||||
BIN
assets/sounds/freighter/service_chime.wav
Normal file
24
assets/sounds/freighter/service_chime.wav.import
Normal 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
|
||||
BIN
assets/sounds/freighter/tool_1.wav
Normal file
24
assets/sounds/freighter/tool_1.wav.import
Normal 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
|
||||
BIN
assets/sounds/freighter/tool_2.wav
Normal file
24
assets/sounds/freighter/tool_2.wav.import
Normal 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
|
||||
BIN
assets/sounds/freighter/tool_3.wav
Normal file
24
assets/sounds/freighter/tool_3.wav.import
Normal 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
|
||||
BIN
assets/sounds/freighter/ventilation_loop.wav
Normal file
24
assets/sounds/freighter/ventilation_loop.wav.import
Normal 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
|
||||
@ -896,3 +896,35 @@ Warstwy przy fregacie i muzyka dokowania (18.10.2026):
|
||||
podczas pauzy dokowania oraz pobytu na powierzchni, bez restartu.
|
||||
- Rozszerzony test freighter_interior_preview przeszedł: kolejność
|
||||
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).
|
||||
|
||||
15
resources/effects/mineral_deposit.gdshader
Normal 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);
|
||||
}
|
||||
1
resources/effects/mineral_deposit.gdshader.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bqamcqle8mpev
|
||||
@ -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_deck.gd" id="2"]
|
||||
[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"]
|
||||
process_mode = 3
|
||||
layout_mode = 3
|
||||
@ -11,6 +12,9 @@ anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1")
|
||||
[node name="FreighterAudio" type="Node" parent="."]
|
||||
process_mode = 3
|
||||
script = ExtResource("4")
|
||||
[node name="Background" type="ColorRect" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
|
||||
134
scripts/planets/freighter_audio.gd
Normal 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)
|
||||
1
scripts/planets/freighter_audio.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://c667epjjr5bdj
|
||||
@ -13,6 +13,7 @@ var _wallet: Label
|
||||
var _hint: Label
|
||||
var _owned_open := false
|
||||
@onready var deck: Control = $Deck
|
||||
@onready var _audio: Node = $FreighterAudio
|
||||
|
||||
func _ready() -> void:
|
||||
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."
|
||||
|
||||
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
|
||||
deck.selected = section
|
||||
_refresh()
|
||||
|
||||
func _close_section() -> void:
|
||||
_audio.close_section()
|
||||
_tab = "overview"
|
||||
deck.selected = ""
|
||||
_drawer.hide()
|
||||
@ -139,6 +144,7 @@ func _label(text: String, size_value := 14, color := Color("c5d3d7")) -> Label:
|
||||
return node
|
||||
|
||||
func _refresh() -> void:
|
||||
_audio.set_maintenance_needed(captain.expedition.lander_hp < 30)
|
||||
for child in _content.get_children():
|
||||
_content.remove_child(child)
|
||||
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.emit_changed()
|
||||
deck.sync_rooms(captain.expedition.freighter_rooms)
|
||||
_audio.service_completed()
|
||||
_hover_section(id)
|
||||
_refresh()
|
||||
|
||||
@ -201,7 +208,7 @@ func _build_hangar() -> void:
|
||||
var cost: int = progress.repair_cost()
|
||||
_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():
|
||||
progress.repair(captain)
|
||||
if progress.repair(captain): _audio.service_completed(0.65)
|
||||
_refresh())
|
||||
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"))
|
||||
@ -356,6 +363,7 @@ func _craft(kind: String) -> void:
|
||||
var gear: Dictionary = GEAR.craft(captain,kind)
|
||||
if not gear.is_empty():
|
||||
GEAR.equip(captain,kind,int(gear.uid))
|
||||
_audio.service_completed(-0.48)
|
||||
_refresh()
|
||||
|
||||
func _equip(kind: String, uid: int) -> void:
|
||||
|
||||
21
scripts/planets/surface_deposit.gd
Normal 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
|
||||
1
scripts/planets/surface_deposit.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://df4vv3pusii74
|
||||
@ -56,9 +56,13 @@ func _ready() -> void:
|
||||
add_child(mountains)
|
||||
for poi in data.pois:
|
||||
var sprite := Sprite2D.new()
|
||||
sprite.texture = load("res://assets/planets/%s.png" % ("ruins" if poi.kind == "ruins" else "mine")) if poi.kind != "deposit" else load("res://assets/icons/resources/titanium.png")
|
||||
if poi.kind == "deposit":
|
||||
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"))
|
||||
sprite.position = poi.position
|
||||
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.scale = Vector2.ONE * 125.0*BUILDING_SCALE / maxf(sprite.texture.get_width(), sprite.texture.get_height())
|
||||
sprite.z_index = 10
|
||||
add_child(sprite)
|
||||
_poi_sprites[poi.id] = sprite
|
||||
|
||||
26
tests/audio/freighter_assets_test.py
Normal 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)
|
||||
61
tests/levels/freighter_audio_test.gd
Normal 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()
|
||||
1
tests/levels/freighter_audio_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b7u8secvdwg08
|
||||
4
tests/levels/freighter_audio_test.tscn
Normal 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")
|
||||
86
tests/previews/surface_deposit_test.gd
Normal 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()
|
||||
1
tests/previews/surface_deposit_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://xoq3rcr7blj3
|
||||
4
tests/previews/surface_deposit_test.tscn
Normal 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")
|
||||
163
tools/audio/build_freighter_audio.py
Normal 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()
|
||||
51
tools/audio/fetch_freighter_sources.py
Normal 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))
|
||||