diff --git a/assets/audio/bell.wav b/assets/audio/bell.wav new file mode 100644 index 0000000..0350f18 Binary files /dev/null and b/assets/audio/bell.wav differ diff --git a/assets/audio/bell.wav.import b/assets/audio/bell.wav.import new file mode 100644 index 0000000..542eab6 --- /dev/null +++ b/assets/audio/bell.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://dvln67rbfmcnt" +path="res://.godot/imported/bell.wav-c9e8b15c72938ff0cc4247d900f73a2f.sample" + +[deps] + +source_file="res://assets/audio/bell.wav" +dest_files=["res://.godot/imported/bell.wav-c9e8b15c72938ff0cc4247d900f73a2f.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 diff --git a/assets/audio/chime.wav b/assets/audio/chime.wav new file mode 100644 index 0000000..cf1ac4f Binary files /dev/null and b/assets/audio/chime.wav differ diff --git a/assets/audio/chime.wav.import b/assets/audio/chime.wav.import new file mode 100644 index 0000000..201162d --- /dev/null +++ b/assets/audio/chime.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://uvtdcjsrs4oc" +path="res://.godot/imported/chime.wav-cbf50c3ed007f7bda834f64e66ec1ffa.sample" + +[deps] + +source_file="res://assets/audio/chime.wav" +dest_files=["res://.godot/imported/chime.wav-cbf50c3ed007f7bda834f64e66ec1ffa.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 diff --git a/assets/audio/click.wav b/assets/audio/click.wav new file mode 100644 index 0000000..860a6d6 Binary files /dev/null and b/assets/audio/click.wav differ diff --git a/assets/audio/click.wav.import b/assets/audio/click.wav.import new file mode 100644 index 0000000..7455e17 --- /dev/null +++ b/assets/audio/click.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://cxiiaesukt1ci" +path="res://.godot/imported/click.wav-726094e36c7052fadb6392aaf7409595.sample" + +[deps] + +source_file="res://assets/audio/click.wav" +dest_files=["res://.godot/imported/click.wav-726094e36c7052fadb6392aaf7409595.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 diff --git a/assets/audio/coin.wav b/assets/audio/coin.wav new file mode 100644 index 0000000..117177c Binary files /dev/null and b/assets/audio/coin.wav differ diff --git a/assets/audio/coin.wav.import b/assets/audio/coin.wav.import new file mode 100644 index 0000000..9840a8f --- /dev/null +++ b/assets/audio/coin.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://cssl2kgkc53is" +path="res://.godot/imported/coin.wav-e1bec9e9502a5df388e6fd33d4bb5689.sample" + +[deps] + +source_file="res://assets/audio/coin.wav" +dest_files=["res://.godot/imported/coin.wav-e1bec9e9502a5df388e6fd33d4bb5689.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 diff --git a/assets/audio/door_open.wav b/assets/audio/door_open.wav new file mode 100644 index 0000000..6543a73 Binary files /dev/null and b/assets/audio/door_open.wav differ diff --git a/assets/audio/door_open.wav.import b/assets/audio/door_open.wav.import new file mode 100644 index 0000000..3de1eb3 --- /dev/null +++ b/assets/audio/door_open.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://c10qifwxnhin6" +path="res://.godot/imported/door_open.wav-d530c3547ac0143568ab23d68285c71c.sample" + +[deps] + +source_file="res://assets/audio/door_open.wav" +dest_files=["res://.godot/imported/door_open.wav-d530c3547ac0143568ab23d68285c71c.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 diff --git a/assets/audio/fanfare.wav b/assets/audio/fanfare.wav new file mode 100644 index 0000000..c930bed Binary files /dev/null and b/assets/audio/fanfare.wav differ diff --git a/assets/audio/fanfare.wav.import b/assets/audio/fanfare.wav.import new file mode 100644 index 0000000..45c5a70 --- /dev/null +++ b/assets/audio/fanfare.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://bl6jimdmb1d3u" +path="res://.godot/imported/fanfare.wav-e0524ad6f859b26eee9ccfabfec4f302.sample" + +[deps] + +source_file="res://assets/audio/fanfare.wav" +dest_files=["res://.godot/imported/fanfare.wav-e0524ad6f859b26eee9ccfabfec4f302.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 diff --git a/export_presets.cfg b/export_presets.cfg index 38ea719..10c88f4 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -7,7 +7,7 @@ advanced_options=false dedicated_server=false custom_features="" export_filter="all_resources" -include_filter="*.json, *.glb" +include_filter="*.json, *.glb, *.wav" exclude_filter="" export_path="export/linux/modbison.x86_64" encryption_include_filters="" @@ -38,7 +38,7 @@ advanced_options=false dedicated_server=false custom_features="" export_filter="all_resources" -include_filter="*.json, *.glb" +include_filter="*.json, *.glb, *.wav" exclude_filter="" export_path="export/windows/modbison.exe" encryption_include_filters="" diff --git a/scenes/player.tscn b/scenes/player.tscn index 07fdd3d..48d62a1 100644 --- a/scenes/player.tscn +++ b/scenes/player.tscn @@ -32,5 +32,8 @@ margin = 0.2 [node name="Camera3D" type="Camera3D" parent="CameraPivot/SpringArm3D"] current = true +[node name="AudioListener3D" type="AudioListener3D" parent="CameraPivot/SpringArm3D/Camera3D"] +current = true + [node name="Inventory" type="Node" parent="."] script = ExtResource("2_inventory") diff --git a/scripts/editor/ui/inspector_panel.gd b/scripts/editor/ui/inspector_panel.gd index ce3cfc5..77969d9 100644 --- a/scripts/editor/ui/inspector_panel.gd +++ b/scripts/editor/ui/inspector_panel.gd @@ -359,6 +359,47 @@ func _setup_rule_form() -> void: # 4. play_sound _action_sound_box = VBoxContainer.new() _action_sound_box.visible = false + + var snd_preset_row := HBoxContainer.new() + var snd_preset_lbl := Label.new() + snd_preset_lbl.text = "Preset:" + snd_preset_lbl.custom_minimum_size = Vector2(70, 0) + snd_preset_row.add_child(snd_preset_lbl) + + var snd_option := OptionButton.new() + snd_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL + snd_option.add_item("🔔 Bell Chime (bell)", 0) + snd_option.set_item_metadata(0, "bell") + snd_option.add_item("🪙 Coin Pickup (coin)", 1) + snd_option.set_item_metadata(1, "coin") + snd_option.add_item("🔘 Button Click (click)", 2) + snd_option.set_item_metadata(2, "click") + snd_option.add_item("✨ Magic Chime (chime)", 3) + snd_option.set_item_metadata(3, "chime") + snd_option.add_item("🎺 Victory Fanfare (fanfare)", 4) + snd_option.set_item_metadata(4, "fanfare") + snd_option.add_item("🚪 Door Open (door_open)", 5) + snd_option.set_item_metadata(5, "door_open") + snd_preset_row.add_child(snd_option) + + var preview_btn := Button.new() + preview_btn.text = "▶ Preview" + preview_btn.tooltip_text = "Listen to this sound effect" + preview_btn.pressed.connect(func(): + var snd_id := _sound_id_edit.text.strip_edges() + var p := "res://assets/audio/%s.wav" % snd_id + if ResourceLoader.exists(p): + var st = load(p) + if st is AudioStream: + var asp := AudioStreamPlayer.new() + asp.stream = st + add_child(asp) + asp.finished.connect(asp.queue_free) + asp.play() + ) + snd_preset_row.add_child(preview_btn) + _action_sound_box.add_child(snd_preset_row) + var snd_row := HBoxContainer.new() var snd_lbl := Label.new() snd_lbl.text = "Sound ID:" @@ -369,6 +410,13 @@ func _setup_rule_form() -> void: _sound_id_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL snd_row.add_child(_sound_id_edit) _action_sound_box.add_child(snd_row) + + snd_option.item_selected.connect(func(idx: int): + var meta = snd_option.get_item_metadata(idx) + if meta != null: + _sound_id_edit.text = str(meta) + ) + form_box.add_child(_action_sound_box) # 5. spawn_from_catalog diff --git a/scripts/generate_audio.py b/scripts/generate_audio.py new file mode 100644 index 0000000..65b36d0 --- /dev/null +++ b/scripts/generate_audio.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Generate crisp, pleasant retro/modern sound effects as 16-bit 44.1kHz mono WAVs.""" +import math +import struct +import wave +from pathlib import Path + +SAMPLE_RATE = 44100 +OUT_DIR = Path(__file__).resolve().parent.parent / "assets" / "audio" +OUT_DIR.mkdir(parents=True, exist_ok=True) + + +def write_wav(filename: str, samples: list[float]): + path = OUT_DIR / filename + with wave.open(str(path), "w") as wav: + wav.setnchannels(1) + wav.setsampwidth(2) + wav.setframerate(SAMPLE_RATE) + raw = bytearray() + for s in samples: + val = max(-1.0, min(1.0, s)) + ival = int(val * 32767) + raw.extend(struct.pack(" E6).""" + duration = 0.35 + n = int(SAMPLE_RATE * duration) + samples = [] + t_split = 0.08 + for i in range(n): + t = i / SAMPLE_RATE + if t < t_split: + freq = 987.77 # B5 + env = min(1.0, t / 0.005) + else: + freq = 1318.51 # E6 + t_sub = t - t_split + env = math.exp(-9.0 * t_sub) + # Add a light sparkle harmonic + s = (math.sin(2 * math.pi * freq * t) + 0.25 * math.sin(4 * math.pi * freq * t)) * env + samples.append(s * 0.8) + write_wav("coin.wav", samples) + + +def gen_click(): + """Short crisp mechanical button click.""" + duration = 0.05 + n = int(SAMPLE_RATE * duration) + samples = [] + for i in range(n): + t = i / SAMPLE_RATE + env = math.exp(-80.0 * t) + freq = 1200.0 - 800.0 * (t / duration) + s = math.sin(2 * math.pi * freq * t) * env + samples.append(s * 0.7) + write_wav("click.wav", samples) + + +def gen_chime(): + """Pleasant 3-note ascending arpeggio (C6, E6, G6).""" + duration = 0.8 + n = int(SAMPLE_RATE * duration) + notes = [1046.5, 1318.5, 1567.98] # C6, E6, G6 + step = 0.12 + samples = [] + for i in range(n): + t = i / SAMPLE_RATE + s_total = 0.0 + for idx, freq in enumerate(notes): + t_start = idx * step + if t >= t_start: + t_note = t - t_start + env = math.exp(-6.0 * t_note) + s_total += math.sin(2 * math.pi * freq * t_note) * env * 0.35 + samples.append(s_total) + write_wav("chime.wav", samples) + + +def gen_fanfare(): + """Triumphant short fanfare jingle (C5, G5, C6).""" + duration = 1.0 + n = int(SAMPLE_RATE * duration) + notes = [(0.0, 0.15, 523.25), (0.15, 0.30, 783.99), (0.30, 0.95, 1046.5)] + samples = [] + for i in range(n): + t = i / SAMPLE_RATE + s_total = 0.0 + for t_start, t_end, freq in notes: + if t >= t_start: + t_rel = t - t_start + env = math.exp(-4.0 * t_rel) if t >= t_end - 0.05 else min(1.0, t_rel / 0.01) + s_total += (math.sin(2 * math.pi * freq * t_rel) + 0.2 * math.sin(4 * math.pi * freq * t_rel)) * env * 0.4 + samples.append(s_total) + write_wav("fanfare.wav", samples) + + +def gen_door_open(): + """Low wooden mechanical slide/creak.""" + duration = 0.6 + n = int(SAMPLE_RATE * duration) + samples = [] + for i in range(n): + t = i / SAMPLE_RATE + env = math.sin(math.pi * (t / duration)) ** 0.5 + freq = 160.0 + 80.0 * math.sin(30.0 * t) + s = math.sin(2 * math.pi * freq * t) * env * 0.6 + samples.append(s) + write_wav("door_open.wav", samples) + + +if __name__ == "__main__": + gen_bell() + gen_coin() + gen_click() + gen_chime() + gen_fanfare() + gen_door_open() diff --git a/scripts/scripting/script_interpreter.gd b/scripts/scripting/script_interpreter.gd index fefe6cc..1ad4211 100644 --- a/scripts/scripting/script_interpreter.gd +++ b/scripts/scripting/script_interpreter.gd @@ -396,6 +396,7 @@ func _act_give_item(action: Dictionary, player: Node) -> void: _show_give_feedback(item_id, count) if item_id.to_lower() in ["buffalo_chips", "chips", "credits"]: _deposit_to_wallet(count) + _act_play_sound({"sound_id": "coin"}, player) func _show_give_feedback(item_id: String, count: int) -> void: @@ -441,19 +442,26 @@ func _deposit_to_wallet(count: int) -> void: +## Trusted, code-defined mapping from sound_id -> resource path. Rule data +## supplies only the key; this table (not the data) decides what actually +## plays. +const _TRUSTED_SOUND_TABLE: Dictionary = { + "bell": "res://assets/audio/bell.wav", + "coin": "res://assets/audio/coin.wav", + "click": "res://assets/audio/click.wav", + "chime": "res://assets/audio/chime.wav", + "fanfare": "res://assets/audio/fanfare.wav", + "door_open": "res://assets/audio/door_open.wav", +} + + func _act_play_sound(action: Dictionary, _player: Node) -> void: - # No real audio asset catalog exists in this repo yet. The verb still - # runs through a real, trusted playback path -- attempting to resolve - # and play a stream when one is registered -- so it's not a no-op - # pretending to be a verb; it's just that today's trusted sound table - # is empty. `sound_id` is only ever used as a key into that trusted, - # code-defined table below, never as a path handed to load() directly. - var sound_id: String = action.get("sound_id", "") + var sound_id: String = str(action.get("sound_id", "")).to_lower().strip_edges().trim_suffix(".wav") var stream_path: String = _TRUSTED_SOUND_TABLE.get(sound_id, "") if not stream_path.is_empty() and ResourceLoader.exists(stream_path): var stream: Resource = load(stream_path) if stream is AudioStream: - var player_node := AudioStreamPlayer3D.new() + var player_node := AudioStreamPlayer.new() player_node.stream = stream add_child(player_node) player_node.finished.connect(player_node.queue_free) @@ -461,12 +469,6 @@ func _act_play_sound(action: Dictionary, _player: Node) -> void: verb_executed.emit("play_sound", action) -## Trusted, code-defined mapping from sound_id -> resource path. Rule data -## supplies only the key; this table (not the data) decides what actually -## plays. Empty for now -- no sound assets exist in this repo yet. -const _TRUSTED_SOUND_TABLE: Dictionary = {} - - func _act_move(action: Dictionary, player: Node) -> void: var target_node := _resolve_target(action.get("target", "self"), player) if target_node == null: