From a0bbbf440d8945cb5e35ff6136af6655e6cfb8a9 Mon Sep 17 00:00:00 2001 From: deco-L Date: Wed, 3 Jun 2026 23:18:12 +0900 Subject: [PATCH 01/10] =?UTF-8?q?=20=20=20=20=20=1B[1mSTDIN=1B[0m=20=1B[38?= =?UTF-8?q?;2;145;155;170m=20=20=201=1B[0m=20=1B[38;2;220;223;228m?= =?UTF-8?q?=E3=82=AA=E3=83=BC=E3=83=87=E3=82=A3=E3=82=AA=E3=82=B7=E3=82=B9?= =?UTF-8?q?=E3=83=86=E3=83=A0=E3=81=AE=E5=AE=9F=E8=A3=85=EF=BC=88miniaudio?= =?UTF-8?q?=20+=20BGM/SE/3D=E9=9F=B3=E6=BA=90=EF=BC=89=1B[0m=20=1B[38;2;14?= =?UTF-8?q?5;155;170m=20=20=202=1B[0m=20=1B[38;2;145;155;170m=20=20=203=1B?= =?UTF-8?q?[0m=20=1B[38;2;220;223;228m-=20AudioManager:=20miniaudio=20?= =?UTF-8?q?=E3=82=92=20pImpl=20=E3=81=A7=E3=83=A9=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=80=82BGM=20=E3=82=AF=E3=83=AD=E3=82=B9=E3=83=95=E3=82=A7?= =?UTF-8?q?=E3=83=BC=E3=83=89=EF=BC=882=E3=82=B9=E3=83=AD=E3=83=83?= =?UTF-8?q?=E3=83=88=1B[0m=20=1B[38;2;145;155;170m=20=20=204=1B[0m=20=1B[3?= =?UTF-8?q?8;2;220;223;228m=20=20ping-pong=EF=BC=89=E3=80=81=E3=82=A2?= =?UTF-8?q?=E3=83=B3=E3=83=93=E3=82=A8=E3=83=B3=E3=83=88=E3=83=AB=E3=83=BC?= =?UTF-8?q?=E3=83=97=E3=80=812D/3D=20SE=20=E3=83=AF=E3=83=B3=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=83=E3=83=88=E5=86=8D=E7=94=9F=E3=81=AB=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C=E3=80=82=1B[0m=20=1B[38;2;145;155;170m=20=20=205=1B[0?= =?UTF-8?q?m=20=1B[38;2;220;223;228m=20=20BGM=E3=83=BBSE=E3=83=BBAmbient?= =?UTF-8?q?=E3=83=BB=E3=83=9E=E3=82=B9=E3=82=BF=E3=83=BC=E3=81=AE=E9=9F=B3?= =?UTF-8?q?=E9=87=8F=E3=82=92=E7=8B=AC=E7=AB=8B=E5=88=B6=E5=BE=A1=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E3=80=82=1B[0m=20=1B[38;2;145;155;170m=20=20=206=1B[0?= =?UTF-8?q?m=20=1B[38;2;220;223;228m-=20BiomeAudioSystem:=20=E3=83=90?= =?UTF-8?q?=E3=82=A4=E3=82=AA=E3=83=BC=E3=83=A0=E5=90=8D=EF=BC=88PLAINS?= =?UTF-8?q?=E3=80=9CAUTUMN=20=E5=85=A89=E7=A8=AE=EF=BC=89=E3=82=92=20BGM?= =?UTF-8?q?=20+=20=E3=82=A2=E3=83=B3=E3=83=93=E3=82=A8=E3=83=B3=E3=83=88?= =?UTF-8?q?=1B[0m=20=1B[38;2;145;155;170m=20=20=207=1B[0m=20=1B[38;2;220;2?= =?UTF-8?q?23;228m=20=20=E3=83=91=E3=82=B9=E3=81=AB=E3=83=9E=E3=83=83?= =?UTF-8?q?=E3=83=94=E3=83=B3=E3=82=B0=E3=80=824=E7=A7=92=E3=82=AF?= =?UTF-8?q?=E3=83=AD=E3=82=B9=E3=83=95=E3=82=A7=E3=83=BC=E3=83=89=E3=80=81?= =?UTF-8?q?2=E7=A7=92=E3=83=87=E3=83=90=E3=82=A6=E3=83=B3=E3=82=B9?= =?UTF-8?q?=E3=81=A7=E3=83=95=E3=83=AC=E3=83=BC=E3=83=A0=E3=81=94=E3=81=A8?= =?UTF-8?q?=E3=81=AB=E6=9B=B4=E6=96=B0=E3=80=82=1B[0m=20=1B[38;2;145;155;1?= =?UTF-8?q?70m=20=20=208=1B[0m=20=1B[38;2;220;223;228m-=20SoundEmitter:=20?= =?UTF-8?q?=E6=B0=B8=E7=B6=9A=E7=9A=84=E3=81=AA=203D=20=E9=9F=B3=E6=BA=90?= =?UTF-8?q?=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88?= =?UTF-8?q?=EF=BC=88=E3=83=A2=E3=83=96=E3=83=BB=E7=92=B0=E5=A2=83=E3=82=AA?= =?UTF-8?q?=E3=83=96=E3=82=B8=E3=82=A7=E3=82=AF=E3=83=88=E5=90=91=E3=81=91?= =?UTF-8?q?=EF=BC=89=E3=80=82=1B[0m=20=1B[38;2;145;155;170m=20=20=209=1B[0?= =?UTF-8?q?m=20=1B[38;2;220;223;228m=20=20Linear=20/=20InverseSquare=20?= =?UTF-8?q?=E6=B8=9B=E8=A1=B0=E3=82=92=E9=81=B8=E6=8A=9E=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E3=80=82=1B[0m=20=1B[38;2;145;155;170m=20=2010=1B[0m=20=1B[38;?= =?UTF-8?q?2;220;223;228m-=20audio=5Fimpl.cpp:=20stb=5Fvorbis=20+=20MINIAU?= =?UTF-8?q?DIO=5FIMPLEMENTATION=20=E3=81=AE=E5=8D=98=E4=B8=80=E3=82=B3?= =?UTF-8?q?=E3=83=B3=E3=83=91=E3=82=A4=E3=83=AB=E5=8D=98=E4=BD=8D=E3=80=82?= =?UTF-8?q?=1B[0m=20=1B[38;2;145;155;170m=20=2011=1B[0m=20=1B[38;2;220;223?= =?UTF-8?q?;228m-=20engine.cpp=20=E3=81=AB=E7=B5=B1=E5=90=88:=20=E3=83=AA?= =?UTF-8?q?=E3=82=B9=E3=83=8A=E3=83=BC=E6=9B=B4=E6=96=B0=E3=80=81=E3=83=90?= =?UTF-8?q?=E3=82=A4=E3=82=AA=E3=83=BC=E3=83=A0BGM=E5=88=87=E3=82=8A?= =?UTF-8?q?=E6=9B=BF=E3=81=88=E3=80=81=E3=83=96=E3=83=AD=E3=83=83=E3=82=AF?= =?UTF-8?q?=E7=A0=B4=E5=A3=8A=E3=83=BB=1B[0m=20=1B[38;2;145;155;170m=20=20?= =?UTF-8?q?12=1B[0m=20=1B[38;2;220;223;228m=20=20=E8=A8=AD=E7=BD=AE?= =?UTF-8?q?=E3=83=BB=E6=94=BB=E6=92=83=E3=83=BB=E8=A2=AB=E3=83=80=E3=83=A1?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BB=E6=AD=A9=E8=A1=8C=EF=BC=88=E5=9C=B0?= =?UTF-8?q?=E9=9D=A2=E7=B4=A0=E6=9D=90=E5=88=A5=EF=BC=89=E3=83=BB=E6=B0=B4?= =?UTF-8?q?=E6=B3=B3=E3=81=AE=20SE=20=E3=83=88=E3=83=AA=E3=82=AC=E3=83=BC?= =?UTF-8?q?=E3=80=82=1B[0m=20=1B[38;2;145;155;170m=20=2013=1B[0m=20=1B[38;?= =?UTF-8?q?2;220;223;228m-=20CMakeLists:=20FetchContent=20=E3=81=A7=20mini?= =?UTF-8?q?audio=200.11.22=20+=20stb=20=E3=82=92=E5=8F=96=E5=BE=97?= =?UTF-8?q?=E3=80=82=1B[0m=20=1B[38;2;145;155;170m=20=2014=1B[0m=20=1B[38;?= =?UTF-8?q?2;220;223;228m-=20Player=20=E3=81=AB=20isOnGround()=20=E3=82=A2?= =?UTF-8?q?=E3=82=AF=E3=82=BB=E3=82=B5=E3=82=92=E8=BF=BD=E5=8A=A0=E3=80=82?= =?UTF-8?q?=1B[0m=20=1B[38;2;145;155;170m=20=2015=1B[0m=20=1B[38;2;145;155?= =?UTF-8?q?;170m=20=2016=1B[0m=20=1B[38;2;220;223;228mCo-Authored-By:=20Cl?= =?UTF-8?q?aude=20Sonnet=204.6=20=1B[0m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 34 +++ srcs/audio/audio_impl.cpp | 32 +++ srcs/audio/audio_manager.cpp | 404 ++++++++++++++++++++++++++++++ srcs/audio/audio_manager.hpp | 93 +++++++ srcs/audio/biome_audio_system.cpp | 57 +++++ srcs/audio/biome_audio_system.hpp | 43 ++++ srcs/audio/sound_emitter.cpp | 90 +++++++ srcs/audio/sound_emitter.hpp | 46 ++++ srcs/core/engine.cpp | 62 +++++ srcs/player/player.hpp | 5 +- 10 files changed, 864 insertions(+), 2 deletions(-) create mode 100644 srcs/audio/audio_impl.cpp create mode 100644 srcs/audio/audio_manager.cpp create mode 100644 srcs/audio/audio_manager.hpp create mode 100644 srcs/audio/biome_audio_system.cpp create mode 100644 srcs/audio/biome_audio_system.hpp create mode 100644 srcs/audio/sound_emitter.cpp create mode 100644 srcs/audio/sound_emitter.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fcbf6d1..d70bad8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,31 @@ if(FT_MINECRAFT_FETCH_DEPS) INTERFACE ${fastnoiselite_SOURCE_DIR}/Cpp ) + + # ── miniaudio (single-header audio engine) ───────────────────────────────── + FetchContent_Declare( + miniaudio + GIT_REPOSITORY https://github.com/mackron/miniaudio.git + GIT_TAG 0.11.22 + GIT_SHALLOW TRUE + ) + FetchContent_GetProperties(miniaudio) + if(NOT miniaudio_POPULATED) + FetchContent_Populate(miniaudio) + endif() + + # ── stb (provides stb_vorbis.c for OGG decoding) ─────────────────────────── + FetchContent_Declare( + stb + GIT_REPOSITORY https://github.com/nothings/stb.git + GIT_TAG master + GIT_SHALLOW TRUE + ) + FetchContent_GetProperties(stb) + if(NOT stb_POPULATED) + FetchContent_Populate(stb) + endif() + else() find_package(glfw3 REQUIRED) find_package(glm CONFIG REQUIRED) @@ -88,6 +113,11 @@ add_executable( srcs/network/client.cpp srcs/mob/mob_manager.cpp srcs/mob/arrow_manager.cpp + # ── Audio system ────────────────────────────────────────────────────────── + srcs/audio/audio_impl.cpp + srcs/audio/audio_manager.cpp + srcs/audio/biome_audio_system.cpp + srcs/audio/sound_emitter.cpp ) target_include_directories( @@ -96,6 +126,8 @@ target_include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/srcs ${glfw_SOURCE_DIR}/deps + ${miniaudio_SOURCE_DIR} + ${stb_SOURCE_DIR} ) target_compile_definitions(ft_minecraft PRIVATE GLFW_INCLUDE_NONE) @@ -108,6 +140,8 @@ target_link_libraries( glm::glm ft_minecraft_fastnoise Threads::Threads + ${CMAKE_DL_LIBS} # miniaudio uses dlopen on Linux for audio backends + m # math library (miniaudio, stb_vorbis) ) set_target_properties( diff --git a/srcs/audio/audio_impl.cpp b/srcs/audio/audio_impl.cpp new file mode 100644 index 0000000..e7446c4 --- /dev/null +++ b/srcs/audio/audio_impl.cpp @@ -0,0 +1,32 @@ +// Single compilation unit that expands the miniaudio and stb_vorbis +// implementation bodies. No other .cpp must define MINIAUDIO_IMPLEMENTATION. + +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-function" +# pragma clang diagnostic ignored "-Wsign-compare" +# pragma clang diagnostic ignored "-Wimplicit-fallthrough" +# pragma clang diagnostic ignored "-Wdeprecated-declarations" +# pragma clang diagnostic ignored "-Wmissing-field-initializers" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-function" +# pragma GCC diagnostic ignored "-Wsign-compare" +# pragma GCC diagnostic ignored "-Wimplicit-fallthrough" +# pragma GCC diagnostic ignored "-Wdeprecated-declarations" +# pragma GCC diagnostic ignored "-Wmissing-field-initializers" +# pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#endif + +// stb_vorbis must be included before MINIAUDIO_IMPLEMENTATION +// so that miniaudio can detect and use it for OGG decoding. +#include "stb_vorbis.c" + +#define MINIAUDIO_IMPLEMENTATION +#include + +#if defined(__clang__) +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif diff --git a/srcs/audio/audio_manager.cpp b/srcs/audio/audio_manager.cpp new file mode 100644 index 0000000..66acb7f --- /dev/null +++ b/srcs/audio/audio_manager.cpp @@ -0,0 +1,404 @@ +#include "audio/audio_manager.hpp" +#include +#include +#include +#include +#include + +// ── SE path table (relative to assets_root) ────────────────────────────────── +static const char* kSePaths[] = { + "sounds/footstep_grass.ogg", // FootstepGrass + "sounds/footstep_stone.ogg", // FootstepStone + "sounds/footstep_sand.ogg", // FootstepSand + "sounds/footstep_snow.ogg", // FootstepSnow + "sounds/footstep_wood.ogg", // FootstepWood + "sounds/attack.ogg", // Attack + "sounds/hurt.ogg", // Hurt + "sounds/swim.ogg", // Swim + "sounds/block_break.ogg", // BlockBreak + "sounds/block_place.ogg", // BlockPlace + "sounds/mob_groan.ogg", // MobGroan + "sounds/mob_hurt.ogg", // MobHurt + "sounds/mob_explode.ogg", // MobExplode +}; +static_assert(sizeof(kSePaths) / sizeof(kSePaths[0]) == + static_cast(SoundEvent::COUNT), + "kSePaths length mismatch"); + +// ── BGM slot (one of two ping-pong slots) ───────────────────────────────────── +struct BgmSlot { + ma_sound sound = {}; + bool initialized = false; + bool playing = false; + std::string path; +}; + +// ── SE pool slot (3D one-shot sounds) ──────────────────────────────────────── +struct SeSlot { + ma_sound sound = {}; + bool initialized = false; +}; + +static constexpr int kSePoolSize = 16; + +// ── pImpl ───────────────────────────────────────────────────────────────────── +struct AudioManager::Impl { + ma_engine engine = {}; + ma_sound_group bgm_group = {}; + ma_sound_group ambient_group = {}; + ma_sound_group se_group = {}; + bool engine_ok = false; + bool bgm_grp_ok = false; + bool amb_grp_ok = false; + bool se_grp_ok = false; + + // Two BGM slots for crossfade (ping-pong) + BgmSlot bgm[2]; + int active_slot = -1; // slot currently playing (fading in or at full vol) + int fading_slot = -1; // slot currently fading out (-1 = none) + float fade_timer = 0.0f; // seconds until fading_slot is stopped + + // Ambient + ma_sound ambient_snd = {}; + bool ambient_ok = false; + bool ambient_fading_out = false; + float ambient_fade_timer = 0.0f; + std::string ambient_path; + + // 3D SE pool + std::array se_pool = {}; + + // Cached listener position (for distance culling in playSe3D) + float listener_x = 0.0f, listener_y = 0.0f, listener_z = 0.0f; + + std::string assets_root; + float master_vol = 1.0f; + float bgm_vol = 0.8f; + float se_vol = 1.0f; + float ambient_vol = 0.6f; +}; + +// ───────────────────────────────────────────────────────────────────────────── + +AudioManager::AudioManager() : impl_(std::make_unique()) {} +AudioManager::~AudioManager() { shutdown(); } + +bool AudioManager::init(const std::string& assets_root) { + Impl& d = *impl_; + d.assets_root = assets_root; + + ma_engine_config cfg = ma_engine_config_init(); + cfg.listenerCount = 1; + if (ma_engine_init(&cfg, &d.engine) != MA_SUCCESS) { + fprintf(stderr, "[Audio] ma_engine_init failed\n"); + return false; + } + d.engine_ok = true; + + auto init_group = [&](ma_sound_group& g, bool& ok, const char* name) -> bool { + if (ma_sound_group_init(&d.engine, 0, nullptr, &g) != MA_SUCCESS) { + fprintf(stderr, "[Audio] %s group init failed\n", name); + return false; + } + ok = true; + return true; + }; + if (!init_group(d.bgm_group, d.bgm_grp_ok, "BGM")) return false; + if (!init_group(d.ambient_group, d.amb_grp_ok, "Ambient")) return false; + if (!init_group(d.se_group, d.se_grp_ok, "SE")) return false; + + ma_engine_set_volume(&d.engine, d.master_vol); + ma_sound_group_set_volume(&d.bgm_group, d.bgm_vol); + ma_sound_group_set_volume(&d.ambient_group, d.ambient_vol); + ma_sound_group_set_volume(&d.se_group, d.se_vol); + + // Default listener orientation: looking -Z (OpenGL convention) + ma_engine_listener_set_direction(&d.engine, 0, 0.0f, 0.0f, -1.0f); + ma_engine_listener_set_world_up(&d.engine, 0, 0.0f, 1.0f, 0.0f); + + fprintf(stderr, "[Audio] init OK root='%s'\n", assets_root.c_str()); + return true; +} + +void AudioManager::shutdown() { + Impl& d = *impl_; + if (!d.engine_ok) return; + + for (auto& se : d.se_pool) + if (se.initialized) { ma_sound_uninit(&se.sound); se.initialized = false; } + + if (d.ambient_ok) { + ma_sound_stop(&d.ambient_snd); + ma_sound_uninit(&d.ambient_snd); + d.ambient_ok = false; + } + for (auto& slot : d.bgm) + if (slot.initialized) { + ma_sound_stop(&slot.sound); + ma_sound_uninit(&slot.sound); + slot.initialized = false; + } + + if (d.se_grp_ok) { ma_sound_group_uninit(&d.se_group); d.se_grp_ok = false; } + if (d.amb_grp_ok) { ma_sound_group_uninit(&d.ambient_group); d.amb_grp_ok = false; } + if (d.bgm_grp_ok) { ma_sound_group_uninit(&d.bgm_group); d.bgm_grp_ok = false; } + + ma_engine_uninit(&d.engine); + d.engine_ok = false; + fprintf(stderr, "[Audio] shutdown OK\n"); +} + +void AudioManager::update(float dt) { + Impl& d = *impl_; + if (!d.engine_ok) return; + + // Stop the fading-out BGM slot after crossfade completes + if (d.fading_slot >= 0 && d.fade_timer > 0.0f) { + d.fade_timer -= dt; + if (d.fade_timer <= 0.0f) { + d.fade_timer = 0.0f; + BgmSlot& s = d.bgm[d.fading_slot]; + if (s.initialized) { + ma_sound_stop(&s.sound); + ma_sound_uninit(&s.sound); + s.initialized = false; + s.playing = false; + } + d.fading_slot = -1; + } + } + + // Stop the fading-out ambient after fade completes + if (d.ambient_fading_out && d.ambient_fade_timer > 0.0f) { + d.ambient_fade_timer -= dt; + if (d.ambient_fade_timer <= 0.0f) { + d.ambient_fade_timer = 0.0f; + d.ambient_fading_out = false; + if (d.ambient_ok) { + ma_sound_stop(&d.ambient_snd); + ma_sound_uninit(&d.ambient_snd); + d.ambient_ok = false; + } + } + } + + // Reclaim SE pool slots that have finished playing + for (auto& se : d.se_pool) + if (se.initialized && ma_sound_at_end(&se.sound)) { + ma_sound_uninit(&se.sound); + se.initialized = false; + } +} + +// ── Volume ──────────────────────────────────────────────────────────────────── + +void AudioManager::setMasterVolume(float v) { + impl_->master_vol = std::clamp(v, 0.0f, 1.0f); + if (impl_->engine_ok) ma_engine_set_volume(&impl_->engine, impl_->master_vol); +} +void AudioManager::setBgmVolume(float v) { + impl_->bgm_vol = std::clamp(v, 0.0f, 1.0f); + if (impl_->bgm_grp_ok) + ma_sound_group_set_volume(&impl_->bgm_group, impl_->bgm_vol); +} +void AudioManager::setSeVolume(float v) { + impl_->se_vol = std::clamp(v, 0.0f, 1.0f); + if (impl_->se_grp_ok) + ma_sound_group_set_volume(&impl_->se_group, impl_->se_vol); +} +void AudioManager::setAmbientVolume(float v) { + impl_->ambient_vol = std::clamp(v, 0.0f, 1.0f); + if (impl_->amb_grp_ok) + ma_sound_group_set_volume(&impl_->ambient_group, impl_->ambient_vol); +} + +float AudioManager::getMasterVolume() const { return impl_->master_vol; } +float AudioManager::getBgmVolume() const { return impl_->bgm_vol; } +float AudioManager::getSeVolume() const { return impl_->se_vol; } +float AudioManager::getAmbientVolume() const { return impl_->ambient_vol; } + +// ── Listener ────────────────────────────────────────────────────────────────── + +void AudioManager::setListenerPosition(float x, float y, float z) { + impl_->listener_x = x; impl_->listener_y = y; impl_->listener_z = z; + if (impl_->engine_ok) + ma_engine_listener_set_position(&impl_->engine, 0, x, y, z); +} +void AudioManager::setListenerDirection(float fx, float fy, float fz) { + if (impl_->engine_ok) + ma_engine_listener_set_direction(&impl_->engine, 0, fx, fy, fz); +} +void AudioManager::setListenerUp(float ux, float uy, float uz) { + if (impl_->engine_ok) + ma_engine_listener_set_world_up(&impl_->engine, 0, ux, uy, uz); +} + +// ── BGM crossfade ───────────────────────────────────────────────────────────── + +void AudioManager::playBgm(const std::string& path, float fade_duration) { + Impl& d = *impl_; + if (!d.engine_ok) return; + + // Skip if already playing this exact track + if (d.active_slot >= 0 && + d.bgm[d.active_slot].playing && + d.bgm[d.active_slot].path == path) + return; + + // Choose the slot that is NOT currently active + int next = (d.active_slot == 0) ? 1 : 0; + + // If next slot is still alive from a previous transition, evict it + if (d.bgm[next].initialized) { + if (next == d.fading_slot) d.fading_slot = -1; + ma_sound_stop(&d.bgm[next].sound); + ma_sound_uninit(&d.bgm[next].sound); + d.bgm[next].initialized = false; + d.bgm[next].playing = false; + } + + std::string full = d.assets_root + "/" + path; + ma_uint32 flags = MA_SOUND_FLAG_STREAM | MA_SOUND_FLAG_NO_SPATIALIZATION; + if (ma_sound_init_from_file(&d.engine, full.c_str(), flags, + &d.bgm_group, nullptr, + &d.bgm[next].sound) != MA_SUCCESS) { + fprintf(stderr, "[Audio] BGM load failed: %s\n", full.c_str()); + return; + } + d.bgm[next].initialized = true; + d.bgm[next].playing = true; + d.bgm[next].path = path; + ma_sound_set_looping(&d.bgm[next].sound, MA_TRUE); + + // Fade new track in from silence + auto fade_ms = static_cast(fade_duration * 1000.0f); + ma_sound_set_fade_in_milliseconds(&d.bgm[next].sound, 0.0f, 1.0f, fade_ms); + ma_sound_start(&d.bgm[next].sound); + + // Fade old track out; retire any pre-existing fading slot immediately + if (d.active_slot >= 0 && d.bgm[d.active_slot].initialized) { + if (d.fading_slot >= 0 && d.fading_slot != d.active_slot && + d.bgm[d.fading_slot].initialized) { + ma_sound_stop(&d.bgm[d.fading_slot].sound); + ma_sound_uninit(&d.bgm[d.fading_slot].sound); + d.bgm[d.fading_slot].initialized = false; + d.bgm[d.fading_slot].playing = false; + } + ma_sound_set_fade_in_milliseconds( + &d.bgm[d.active_slot].sound, 1.0f, 0.0f, fade_ms); + d.fading_slot = d.active_slot; + d.fade_timer = fade_duration; + } + + d.active_slot = next; +} + +void AudioManager::stopBgm(float fade_out) { + Impl& d = *impl_; + if (!d.engine_ok || d.active_slot < 0) return; + + BgmSlot& s = d.bgm[d.active_slot]; + if (s.initialized && s.playing) { + auto fade_ms = static_cast(fade_out * 1000.0f); + ma_sound_set_fade_in_milliseconds(&s.sound, 1.0f, 0.0f, fade_ms); + d.fading_slot = d.active_slot; + d.fade_timer = fade_out; + } + d.active_slot = -1; +} + +// ── Ambient ─────────────────────────────────────────────────────────────────── + +void AudioManager::playAmbient(const std::string& path, float fade_in) { + Impl& d = *impl_; + if (!d.engine_ok) return; + if (d.ambient_ok && d.ambient_path == path) return; // already playing + + // Stop current ambient immediately (crossfade handled by BiomeAudioSystem timing) + if (d.ambient_ok) { + ma_sound_stop(&d.ambient_snd); + ma_sound_uninit(&d.ambient_snd); + d.ambient_ok = false; + d.ambient_fading_out = false; + d.ambient_fade_timer = 0.0f; + } + + std::string full = d.assets_root + "/" + path; + ma_uint32 flags = MA_SOUND_FLAG_STREAM | MA_SOUND_FLAG_NO_SPATIALIZATION; + if (ma_sound_init_from_file(&d.engine, full.c_str(), flags, + &d.ambient_group, nullptr, + &d.ambient_snd) != MA_SUCCESS) { + fprintf(stderr, "[Audio] Ambient load failed: %s\n", full.c_str()); + return; + } + d.ambient_ok = true; + d.ambient_path = path; + ma_sound_set_looping(&d.ambient_snd, MA_TRUE); + auto fade_ms = static_cast(fade_in * 1000.0f); + ma_sound_set_fade_in_milliseconds(&d.ambient_snd, 0.0f, 1.0f, fade_ms); + ma_sound_start(&d.ambient_snd); +} + +void AudioManager::stopAmbient(float fade_out) { + Impl& d = *impl_; + if (!d.engine_ok || !d.ambient_ok || d.ambient_fading_out) return; + + auto fade_ms = static_cast(fade_out * 1000.0f); + ma_sound_set_fade_in_milliseconds(&d.ambient_snd, 1.0f, 0.0f, fade_ms); + d.ambient_fading_out = true; + d.ambient_fade_timer = fade_out; + d.ambient_path.clear(); +} + +// ── SE ──────────────────────────────────────────────────────────────────────── + +void AudioManager::playSe(SoundEvent event) { + Impl& d = *impl_; + if (!d.engine_ok) return; + + int idx = static_cast(event); + if (idx < 0 || idx >= static_cast(SoundEvent::COUNT)) return; + + std::string full = d.assets_root + "/" + kSePaths[idx]; + if (ma_engine_play_sound(&d.engine, full.c_str(), &d.se_group) != MA_SUCCESS) + fprintf(stderr, "[Audio] playSe failed: %s\n", full.c_str()); +} + +void AudioManager::playSe3D(SoundEvent event, float x, float y, float z, + float max_distance) { + Impl& d = *impl_; + if (!d.engine_ok) return; + + // Distance cull + float dx = x - d.listener_x, dy = y - d.listener_y, dz = z - d.listener_z; + if (dx*dx + dy*dy + dz*dz > max_distance * max_distance) return; + + // Find a free SE pool slot + SeSlot* slot = nullptr; + for (auto& s : d.se_pool) if (!s.initialized) { slot = &s; break; } + if (!slot) return; // pool full; drop the sound + + int idx = static_cast(event); + if (idx < 0 || idx >= static_cast(SoundEvent::COUNT)) return; + + std::string full = d.assets_root + "/" + kSePaths[idx]; + // Load without streaming (SE are short) and with spatialization enabled + if (ma_sound_init_from_file(&d.engine, full.c_str(), 0, + &d.se_group, nullptr, + &slot->sound) != MA_SUCCESS) + return; + + slot->initialized = true; + ma_sound_set_spatialization_enabled(&slot->sound, MA_TRUE); + ma_sound_set_position(&slot->sound, x, y, z); + ma_sound_set_attenuation_model(&slot->sound, ma_attenuation_model_inverse); + ma_sound_set_rolloff(&slot->sound, 1.0f); + ma_sound_set_min_distance(&slot->sound, 1.0f); + ma_sound_set_max_distance(&slot->sound, max_distance); + ma_sound_set_looping(&slot->sound, MA_FALSE); + ma_sound_start(&slot->sound); +} + +void* AudioManager::getEngineHandle() const { + return impl_->engine_ok ? &impl_->engine : nullptr; +} diff --git a/srcs/audio/audio_manager.hpp b/srcs/audio/audio_manager.hpp new file mode 100644 index 0000000..3277e21 --- /dev/null +++ b/srcs/audio/audio_manager.hpp @@ -0,0 +1,93 @@ +#pragma once +#include +#include +#include + +// One-shot sound effect categories. +// Paths mapped in audio_manager.cpp :: kSePaths[]. +enum class SoundEvent : uint8_t { + FootstepGrass = 0, + FootstepStone, + FootstepSand, + FootstepSnow, + FootstepWood, + Attack, + Hurt, + Swim, + BlockBreak, + BlockPlace, + MobGroan, + MobHurt, + MobExplode, + COUNT +}; + +// Central audio manager. +// +// Threading model: +// All public methods must be called from the main thread. +// miniaudio's audio callback runs on a dedicated audio thread internally; +// miniaudio's own API is thread-safe, so we do not need extra locking here. +// +// Usage: +// AudioManager audio; +// audio.init("assets"); +// // each frame: +// audio.setListenerPosition(px, py, pz); +// audio.setListenerDirection(fx, fy, fz); +// audio.update(dt); +class AudioManager { +public: + AudioManager(); + ~AudioManager(); + + // assets_root: directory that contains "music/" and "sounds/" subdirs. + bool init(const std::string& assets_root = "assets"); + void shutdown(); + + // Call once per frame from the main thread. + void update(float dt); + + // ── Volume [0.0, 1.0] ─────────────────────────────────────────────────── + void setMasterVolume(float v); + void setBgmVolume(float v); + void setSeVolume(float v); + void setAmbientVolume(float v); + + float getMasterVolume() const; + float getBgmVolume() const; + float getSeVolume() const; + float getAmbientVolume() const; + + // ── Listener (player camera) ───────────────────────────────────────────── + void setListenerPosition(float x, float y, float z); + void setListenerDirection(float fx, float fy, float fz); + void setListenerUp(float ux, float uy, float uz); + + // ── BGM: looping track with crossfade on switch ────────────────────────── + // path: relative to assets_root, e.g. "music/forest_bgm.ogg" + // fade_duration: seconds for both fade-in of new and fade-out of old track. + void playBgm(const std::string& path, float fade_duration = 4.0f); + void stopBgm(float fade_out = 2.0f); + + // ── Ambient: independent looping sound layer ───────────────────────────── + void playAmbient(const std::string& path, float fade_in = 2.0f); + void stopAmbient(float fade_out = 2.0f); + + // ── SE: 2D one-shot ────────────────────────────────────────────────────── + void playSe(SoundEvent event); + + // ── SE: 3D positional one-shot ─────────────────────────────────────────── + // Skipped automatically when the source is farther than max_distance. + // Uses inverse-distance attenuation (OpenAL-style). + void playSe3D(SoundEvent event, float x, float y, float z, + float max_distance = 32.0f); + + // ── Internal access for SoundEmitter ──────────────────────────────────── + // Returns the raw ma_engine* as void*. Used only by SoundEmitter. + void* getEngineHandle() const; + +private: + struct Impl; + std::unique_ptr impl_; +}; diff --git a/srcs/audio/biome_audio_system.cpp b/srcs/audio/biome_audio_system.cpp new file mode 100644 index 0000000..170a83f --- /dev/null +++ b/srcs/audio/biome_audio_system.cpp @@ -0,0 +1,57 @@ +#include "audio/biome_audio_system.hpp" +#include "audio/audio_manager.hpp" +#include + +// Biome names must match TerrainGenerator::biomeName() — all uppercase. +void BiomeAudioSystem::init(AudioManager& mgr) { + mgr_ = &mgr; + + registerBiome("PLAINS", {"music/plains_bgm.ogg", "sounds/ambient/plains.ogg"}); + registerBiome("DESERT", {"music/desert_bgm.ogg", "sounds/ambient/desert.ogg"}); + registerBiome("TUNDRA", {"music/tundra_bgm.ogg", "sounds/ambient/tundra.ogg"}); + registerBiome("ROCKY", {"music/rocky_bgm.ogg", "sounds/ambient/rocky.ogg"}); + registerBiome("SWAMP", {"music/swamp_bgm.ogg", "sounds/ambient/swamp.ogg"}); + registerBiome("MOUNTAIN", {"music/mountain_bgm.ogg", "sounds/ambient/mountain.ogg"}); + registerBiome("CANYON", {"music/canyon_bgm.ogg", "sounds/ambient/canyon.ogg"}); + registerBiome("SPRING", {"music/spring_bgm.ogg", "sounds/ambient/spring.ogg"}); + registerBiome("AUTUMN", {"music/autumn_bgm.ogg", "sounds/ambient/autumn.ogg"}); +} + +void BiomeAudioSystem::registerBiome(const std::string& biome_name, + BiomeTrack track) { + tracks_[biome_name] = std::move(track); +} + +void BiomeAudioSystem::update(float dt, const char* current_biome) { + if (!mgr_ || !current_biome) return; + + // Debounce: prevent thrashing at biome borders + if (change_cooldown_ > 0.0f) { + change_cooldown_ -= dt; + return; + } + + std::string name(current_biome); + if (name == current_biome_) return; + + auto it = tracks_.find(name); + if (it == tracks_.end()) { + // Unknown biome; record it to avoid repeated warnings but don't crash + fprintf(stderr, "[BiomeAudio] No track for biome '%s'\n", name.c_str()); + current_biome_ = name; + return; + } + + fprintf(stderr, "[BiomeAudio] %s → %s\n", + current_biome_.empty() ? "(none)" : current_biome_.c_str(), + name.c_str()); + + const BiomeTrack& track = it->second; + if (!track.bgm_path.empty()) + mgr_->playBgm(track.bgm_path, fade_duration_); + if (!track.ambient_path.empty()) + mgr_->playAmbient(track.ambient_path, 2.0f); + + current_biome_ = name; + change_cooldown_ = kChangeCooldown; +} diff --git a/srcs/audio/biome_audio_system.hpp b/srcs/audio/biome_audio_system.hpp new file mode 100644 index 0000000..c03c0db --- /dev/null +++ b/srcs/audio/biome_audio_system.hpp @@ -0,0 +1,43 @@ +#pragma once +#include +#include + +class AudioManager; + +// BGM + ambient pair registered for one biome. +struct BiomeTrack { + std::string bgm_path; // e.g. "music/plains_bgm.ogg" + std::string ambient_path; // e.g. "sounds/ambient/plains.ogg" +}; + +// Maps biome names → BiomeTrack and drives BGM/ambient switching. +// +// Call init() once at startup, then update() every frame with the +// biome name returned by World::getBiomeNameAt(). +// +// Crossfade duration is forwarded to AudioManager::playBgm / playAmbient. +// A 2-second debounce prevents rapid switching at biome borders. +class BiomeAudioSystem { +public: + static constexpr float kDefaultFadeDuration = 4.0f; // seconds + static constexpr float kChangeCooldown = 2.0f; // seconds + + // Registers all built-in biome tracks and wires up the AudioManager. + void init(AudioManager& mgr); + + // Register (or overwrite) a biome track by name. + void registerBiome(const std::string& biome_name, BiomeTrack track); + + // Call every frame. current_biome is the string from getBiomeNameAt(). + void update(float dt, const char* current_biome); + + void setFadeDuration(float s) { fade_duration_ = s; } + float fadeDuration() const { return fade_duration_; } + +private: + AudioManager* mgr_ = nullptr; + std::unordered_map tracks_; + std::string current_biome_; + float fade_duration_ = kDefaultFadeDuration; + float change_cooldown_ = 0.0f; +}; diff --git a/srcs/audio/sound_emitter.cpp b/srcs/audio/sound_emitter.cpp new file mode 100644 index 0000000..565882d --- /dev/null +++ b/srcs/audio/sound_emitter.cpp @@ -0,0 +1,90 @@ +#include "audio/sound_emitter.hpp" +#include "audio/audio_manager.hpp" +#include +#include +#include +#include + +struct SoundEmitter::Impl { + ma_sound sound = {}; + bool loaded = false; + bool playing = false; +}; + +SoundEmitter::SoundEmitter() : impl_(std::make_unique()) {} +SoundEmitter::~SoundEmitter() { unload(); } + +bool SoundEmitter::load(AudioManager& mgr, + const std::string& path, + float min_distance, + float max_distance, + AttenuationModel model) { + unload(); + + auto* eng = static_cast(mgr.getEngineHandle()); + if (!eng) return false; + + // Construct full path the same way AudioManager does + // (AudioManager doesn't expose assets_root, so the caller is expected to + // pass the full path, or a path relative to the cwd which is the same + // as assets_root + "/" + relative_path.) + if (ma_sound_init_from_file(eng, path.c_str(), 0, + nullptr, nullptr, + &impl_->sound) != MA_SUCCESS) { + fprintf(stderr, "[SoundEmitter] load failed: %s\n", path.c_str()); + return false; + } + impl_->loaded = true; + + ma_sound_set_spatialization_enabled(&impl_->sound, MA_TRUE); + + switch (model) { + case AttenuationModel::Linear: + ma_sound_set_attenuation_model(&impl_->sound, + ma_attenuation_model_linear); + break; + case AttenuationModel::InverseSquare: + ma_sound_set_attenuation_model(&impl_->sound, + ma_attenuation_model_inverse); + break; + } + ma_sound_set_rolloff(&impl_->sound, 1.0f); + ma_sound_set_min_distance(&impl_->sound, min_distance); + ma_sound_set_max_distance(&impl_->sound, max_distance); + return true; +} + +void SoundEmitter::unload() { + if (!impl_->loaded) return; + ma_sound_stop(&impl_->sound); + ma_sound_uninit(&impl_->sound); + impl_->loaded = false; + impl_->playing = false; +} + +void SoundEmitter::setPosition(float x, float y, float z) { + if (impl_->loaded) + ma_sound_set_position(&impl_->sound, x, y, z); +} + +void SoundEmitter::setVolume(float v) { + if (impl_->loaded) + ma_sound_set_volume(&impl_->sound, std::clamp(v, 0.0f, 1.0f)); +} + +void SoundEmitter::play(bool loop) { + if (!impl_->loaded) return; + ma_sound_set_looping(&impl_->sound, loop ? MA_TRUE : MA_FALSE); + ma_sound_start(&impl_->sound); + impl_->playing = true; +} + +void SoundEmitter::stop() { + if (!impl_->loaded) return; + ma_sound_stop(&impl_->sound); + impl_->playing = false; +} + +bool SoundEmitter::isPlaying() const { + return impl_->loaded && ma_sound_is_playing(&impl_->sound); +} diff --git a/srcs/audio/sound_emitter.hpp b/srcs/audio/sound_emitter.hpp new file mode 100644 index 0000000..6053829 --- /dev/null +++ b/srcs/audio/sound_emitter.hpp @@ -0,0 +1,46 @@ +#pragma once +#include +#include + +class AudioManager; + +// A persistent 3D sound source attached to a world object or mob. +// +// Example: +// SoundEmitter emitter; +// emitter.load(audio_mgr, "sounds/ambient/fire_crackle.ogg", +// /*min_dist=*/1.0f, /*max_dist=*/16.0f); +// emitter.setPosition(x, y, z); +// emitter.play(/*loop=*/true); +// // each frame: +// emitter.setPosition(mob.x, mob.y, mob.z); +// // on death: +// emitter.stop(); +class SoundEmitter { +public: + enum class AttenuationModel { Linear, InverseSquare }; + + SoundEmitter(); + ~SoundEmitter(); + + // Load a sound file from AudioManager's assets_root. + // Returns false if the file could not be loaded (non-fatal; other methods + // become no-ops). + bool load(AudioManager& mgr, + const std::string& path, + float min_distance = 1.0f, + float max_distance = 32.0f, + AttenuationModel model = AttenuationModel::InverseSquare); + + void unload(); + + void setPosition(float x, float y, float z); + void setVolume(float v); // [0, 1], independent of SE group volume + void play(bool loop = true); + void stop(); + bool isPlaying() const; + +private: + struct Impl; + std::unique_ptr impl_; +}; diff --git a/srcs/core/engine.cpp b/srcs/core/engine.cpp index becca1b..875896c 100644 --- a/srcs/core/engine.cpp +++ b/srcs/core/engine.cpp @@ -34,6 +34,8 @@ #include "network/client.hpp" #include "mob/mob_manager.hpp" #include "mob/arrow_manager.hpp" +#include "audio/audio_manager.hpp" +#include "audio/biome_audio_system.hpp" #include #include @@ -213,6 +215,12 @@ struct Engine::Impl { float net_pos_timer = 0.0f; float mob_sync_timer = 0.0f; // ホスト用: mob送信インターバル + // Audio + AudioManager audio_mgr; + BiomeAudioSystem biome_audio; + float footstep_timer = 0.0f; + float swim_timer = 0.0f; + // Mobs MobManager mob_mgr; ArrowManager arrow_mgr; @@ -332,6 +340,13 @@ bool Engine::init(uint32_t seed, int width, int height) { // ── チャンクマネージャーの初期化 ─────────────────────────────────────────── impl_->chunk_mgr = new ChunkManager(impl_->world, impl_->renderer); + // ── オーディオシステムの初期化 ───────────────────────────────────────────── + if (!impl_->audio_mgr.init("assets")) { + fprintf(stderr, "[Engine] Audio init failed — continuing without audio\n"); + } else { + impl_->biome_audio.init(impl_->audio_mgr); + } + running_ = true; fprintf(stderr, "[Engine] init OK seed=%u %dx%d\n", seed, width_, height_); return true; @@ -454,6 +469,39 @@ void Engine::run() { bool moving = inp.isHeld(GLFW_KEY_W) || inp.isHeld(GLFW_KEY_S) || inp.isHeld(GLFW_KEY_A) || inp.isHeld(GLFW_KEY_D); if (moving) impl_->local_walk_phase += dt * 8.0f; + + // ── 歩行音・水泳音 ────────────────────────────────────────── + impl_->footstep_timer -= dt; + if (impl_->footstep_timer <= 0.0f && moving) { + if (impl_->player.isInWater()) { + impl_->audio_mgr.playSe(SoundEvent::Swim); + impl_->footstep_timer = 0.55f; + } else if (impl_->player.isOnGround()) { + glm::vec3 p = impl_->player.camera().position(); + BlockType below = impl_->world.getWorldBlock( + (int)std::floor(p.x), + (int)std::floor(p.y - 1.2f), + (int)std::floor(p.z)); + SoundEvent fs = SoundEvent::FootstepGrass; + switch (below) { + case BlockType::Stone: + case BlockType::GoldOre: + case BlockType::DiamondOre: + fs = SoundEvent::FootstepStone; break; + case BlockType::Sand: + case BlockType::Cactus: + fs = SoundEvent::FootstepSand; break; + case BlockType::Snow: + fs = SoundEvent::FootstepSnow; break; + case BlockType::Wood: + fs = SoundEvent::FootstepWood; break; + default: + fs = SoundEvent::FootstepGrass; break; + } + impl_->audio_mgr.playSe(fs); + impl_->footstep_timer = 0.40f; + } + } } } if (impl_->player.shouldClose()) break; @@ -461,6 +509,7 @@ void Engine::run() { auto applyPlayerDamage = [&](float dmg, const char* source) { if (dmg <= 0.0f || impl_->player_dead) return; impl_->player_health -= dmg; + impl_->audio_mgr.playSe(SoundEvent::Hurt); fprintf(stderr, "[Game] Player took %.1f damage (%s).\n", dmg, source); if (impl_->player_health <= 0.0f) { @@ -637,6 +686,7 @@ void Engine::run() { BlockType::Air); rebuildModified(hit.bx, hit.bz, *impl_->chunk_mgr); inventoryAdd(inv, broken); + impl_->audio_mgr.playSe(SoundEvent::BlockBreak); if (impl_->multiplayer) impl_->net_client.sendBlockChange( hit.bx, hit.by, hit.bz, @@ -644,6 +694,7 @@ void Engine::run() { } else { impl_->mob_mgr.playerMeleeAttack( pos.x, pos.y, pos.z, front.x, front.z); + impl_->audio_mgr.playSe(SoundEvent::Attack); } } @@ -689,6 +740,7 @@ void Engine::run() { impl_->world.setWorldBlock(hit.nx, hit.ny, hit.nz, to_place); rebuildModified(hit.nx, hit.nz, *impl_->chunk_mgr); + impl_->audio_mgr.playSe(SoundEvent::BlockPlace); if (impl_->multiplayer) impl_->net_client.sendBlockChange( hit.nx, hit.ny, hit.nz, @@ -713,6 +765,16 @@ void Engine::run() { // ── マルチプレイ: ネットワーク送受信 ──────────────────────────────────── glm::vec3 ppos = impl_->player.camera().position(); + + // ── オーディオ更新 ────────────────────────────────────────────────── + { + glm::vec3 front = impl_->player.camera().front(); + impl_->audio_mgr.setListenerPosition(ppos.x, ppos.y, ppos.z); + impl_->audio_mgr.setListenerDirection(front.x, front.y, front.z); + const char* biome = impl_->world.getBiomeNameAt(ppos.x, ppos.z); + impl_->biome_audio.update(dt, biome); + impl_->audio_mgr.update(dt); + } if (impl_->attack_sync_timer > 0.0f) impl_->attack_sync_timer -= dt; if (impl_->death_sync_timer > 0.0f) diff --git a/srcs/player/player.hpp b/srcs/player/player.hpp index 8db2db9..db357e0 100644 --- a/srcs/player/player.hpp +++ b/srcs/player/player.hpp @@ -22,8 +22,9 @@ class Player { InputHandler& input() { return input_; } ChunkPos chunkPos() const; - bool isFlyMode() const { return fly_mode_; } - bool isInWater() const { return in_water_; } + bool isFlyMode() const { return fly_mode_; } + bool isInWater() const { return in_water_; } + bool isOnGround() const { return on_ground_; } void respawnAtInitial(); bool shouldClose() const { return input_.shouldClose(); } From fee0be62078a83fe5820d4755d9e2a10eee669be Mon Sep 17 00:00:00 2001 From: deco-L Date: Wed, 3 Jun 2026 23:27:50 +0900 Subject: [PATCH 02/10] =?UTF-8?q?=20=20=20=20=20=1B[1mSTDIN=1B[0m=20=1B[38?= =?UTF-8?q?;2;145;155;170m=20=20=201=1B[0m=20=1B[38;2;220;223;228m?= =?UTF-8?q?=E9=9F=B3=E5=A3=B0=E3=82=A2=E3=82=BB=E3=83=83=E3=83=88=E3=82=92?= =?UTF-8?q?=20.gitignore=20=E3=81=AB=E8=BF=BD=E5=8A=A0=E3=80=81=E3=83=80?= =?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=AD=E3=83=BC=E3=83=89=E3=82=B9=E3=82=AF?= =?UTF-8?q?=E3=83=AA=E3=83=97=E3=83=88=E3=82=92=E6=95=B4=E5=82=99=1B[0m=20?= =?UTF-8?q?=1B[38;2;145;155;170m=20=20=202=1B[0m=20=1B[38;2;145;155;170m?= =?UTF-8?q?=20=20=203=1B[0m=20=1B[38;2;220;223;228m=E3=83=AA=E3=83=9D?= =?UTF-8?q?=E3=82=B8=E3=83=88=E3=83=AA=E5=90=88=E8=A8=88=E3=81=8C=2042=20M?= =?UTF-8?q?B=20=E8=B6=85=E3=81=AB=E3=81=AA=E3=82=8B=E3=81=9F=E3=82=81=20as?= =?UTF-8?q?sets/music/=20=E3=81=A8=20assets/sounds/=20=E3=82=92=1B[0m=20?= =?UTF-8?q?=1B[38;2;145;155;170m=20=20=204=1B[0m=20=1B[38;2;220;223;228mGi?= =?UTF-8?q?t=20=E7=AE=A1=E7=90=86=E5=A4=96=E3=81=AB=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=80=82=1B[0m=20=1B[38;2;145;155;170m=20=20=205=1B[0m=20=1B[3?= =?UTF-8?q?8;2;145;155;170m=20=20=206=1B[0m=20=1B[38;2;220;223;228m-=20scr?= =?UTF-8?q?ipts/download=5Fassets.sh:=20=E6=9C=AC=E7=95=AA=E9=9F=B3?= =?UTF-8?q?=E5=A3=B0=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92=20tar.gz?= =?UTF-8?q?=20=E3=81=A7=E5=8F=96=E5=BE=97=E3=81=99=E3=82=8B=E9=9B=9B?= =?UTF-8?q?=E5=BD=A2=E3=80=82=1B[0m=20=1B[38;2;145;155;170m=20=20=207=1B[0?= =?UTF-8?q?m=20=1B[38;2;220;223;228m=20=20AUDIO=5FARCHIVE=5FURL=20?= =?UTF-8?q?=E3=81=8C=E6=9C=AA=E8=A8=AD=E5=AE=9A=E3=81=AE=E5=A0=B4=E5=90=88?= =?UTF-8?q?=E3=81=AF=E3=83=97=E3=83=AC=E3=83=BC=E3=82=B9=E3=83=9B=E3=83=AB?= =?UTF-8?q?=E3=83=80=E3=83=BC=E7=94=9F=E6=88=90=E3=81=AB=E3=83=95=E3=82=A9?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E3=83=90=E3=83=83=E3=82=AF=E3=80=82=1B[0m=20?= =?UTF-8?q?=1B[38;2;145;155;170m=20=20=208=1B[0m=20=1B[38;2;220;223;228m-?= =?UTF-8?q?=20scripts/gen=5Fplaceholder=5Fassets.sh:=20python3=20=E3=81=A7?= =?UTF-8?q?=E7=84=A1=E9=9F=B3=20WAV=20=E3=82=92=E7=94=9F=E6=88=90=EF=BC=88?= =?UTF-8?q?=E9=96=8B=E7=99=BA=E7=94=A8=EF=BC=89=E3=80=82=1B[0m=20=1B[38;2;?= =?UTF-8?q?145;155;170m=20=20=209=1B[0m=20=1B[38;2;220;223;228m=20=20?= =?UTF-8?q?=E5=85=A8=2031=20=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92?= =?UTF-8?q?=E7=94=9F=E6=88=90=E3=81=97=E3=81=A6=E3=82=82=E5=90=88=E8=A8=88?= =?UTF-8?q?=20~2MB=20=E3=81=AB=E5=8F=8E=E3=81=BE=E3=82=8B=E3=80=82=1B[0m?= =?UTF-8?q?=20=1B[38;2;145;155;170m=20=2010=1B[0m=20=1B[38;2;145;155;170m?= =?UTF-8?q?=20=2011=1B[0m=20=1B[38;2;220;223;228m=E8=A9=95=E4=BE=A1?= =?UTF-8?q?=E8=80=85=E3=81=AF=20`bash=20scripts/download=5Fassets.sh`=20?= =?UTF-8?q?=E4=B8=80=E7=99=BA=E3=81=A7=E3=82=A2=E3=82=BB=E3=83=83=E3=83=88?= =?UTF-8?q?=E3=82=92=E7=94=A8=E6=84=8F=E3=81=A7=E3=81=8D=E3=82=8B=E3=80=82?= =?UTF-8?q?=1B[0m=20=1B[38;2;145;155;170m=20=2012=1B[0m=20=1B[38;2;145;155?= =?UTF-8?q?;170m=20=2013=1B[0m=20=1B[38;2;220;223;228mCo-Authored-By:=20Cl?= =?UTF-8?q?aude=20Sonnet=204.6=20=1B[0m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 ++ scripts/download_assets.sh | 113 ++++++++++++++++++++++++++++++ scripts/gen_placeholder_assets.sh | 83 ++++++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100755 scripts/download_assets.sh create mode 100755 scripts/gen_placeholder_assets.sh diff --git a/.gitignore b/.gitignore index 6d2d5d5..c9bed47 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,8 @@ IMPLEMENTATION_STATUS.md .cache en.subject.pdf saves/ +ft_minecraft + +# Audio assets (>42MB total) — use scripts/download_assets.sh +assets/music/ +assets/sounds/ diff --git a/scripts/download_assets.sh b/scripts/download_assets.sh new file mode 100755 index 0000000..3d3e446 --- /dev/null +++ b/scripts/download_assets.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# download_assets.sh +# +# 音声アセットをダウンロードする。 +# リポジトリ合計が 42 MB を超えるため、音声ファイルは Git 管理外。 +# +# 使い方: +# bash scripts/download_assets.sh # 本番ファイルをダウンロード +# bash scripts/download_assets.sh --placeholder # 無音ファイルを生成(開発用) +# +# 実際のファイルをホストする場合: +# AUDIO_BASE_URL を変更し、ファイル一覧 (MUSIC / AMBIENT / SE) を更新する。 +# 推奨ホスト: GitHub Releases の tar.gz アーカイブ + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +ASSETS="$REPO_ROOT/assets" + +# ── ダウンロード元 URL ──────────────────────────────────────────────────────── +# GitHub Releases にアーカイブをアップロードしたら以下を書き換える。 +# 例: https://github.com/Ishi-eenn/ft_vox/releases/download/v1.0/audio_assets.tar.gz +AUDIO_ARCHIVE_URL="" + +# ── ファイル一覧 ────────────────────────────────────────────────────────────── +MUSIC=( + plains_bgm desert_bgm tundra_bgm rocky_bgm swamp_bgm + mountain_bgm canyon_bgm spring_bgm autumn_bgm +) +AMBIENT=( + plains desert tundra rocky swamp + mountain canyon spring autumn +) +SE=( + footstep_grass footstep_stone footstep_sand footstep_snow footstep_wood + attack hurt swim + block_break block_place + mob_groan mob_hurt mob_explode +) + +# ────────────────────────────────────────────────────────────────────────────── + +placeholder_mode=false +if [[ "${1:-}" == "--placeholder" ]]; then + placeholder_mode=true +fi + +# プレースホルダーモード: 無音 WAV を生成して終了 +if $placeholder_mode; then + echo "[download_assets] --placeholder: generating silent WAV files..." + ASSETS="$ASSETS" bash "$REPO_ROOT/scripts/gen_placeholder_assets.sh" + exit 0 +fi + +# URL が未設定なら placeholder にフォールバック +if [[ -z "$AUDIO_ARCHIVE_URL" ]]; then + echo "[download_assets] AUDIO_ARCHIVE_URL is not set." + echo " → Falling back to placeholder generation." + echo " To use real audio files, set AUDIO_ARCHIVE_URL in this script" + echo " or run: bash scripts/download_assets.sh --placeholder" + echo "" + ASSETS="$ASSETS" bash "$REPO_ROOT/scripts/gen_placeholder_assets.sh" + exit 0 +fi + +# ── アーカイブをダウンロードして展開 ───────────────────────────────────────── +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +echo "[download_assets] Downloading $AUDIO_ARCHIVE_URL ..." + +if command -v curl &>/dev/null; then + curl -L --progress-bar -o "$TMP_DIR/audio_assets.tar.gz" "$AUDIO_ARCHIVE_URL" +elif command -v wget &>/dev/null; then + wget -q --show-progress -O "$TMP_DIR/audio_assets.tar.gz" "$AUDIO_ARCHIVE_URL" +else + echo "ERROR: curl or wget is required." >&2 + exit 1 +fi + +echo "[download_assets] Extracting..." +tar -xzf "$TMP_DIR/audio_assets.tar.gz" -C "$TMP_DIR" + +# アーカイブ内の *.ogg / *.wav をそれぞれ正しいディレクトリへコピー +mkdir -p "$ASSETS/music" "$ASSETS/sounds/ambient" + +install_if_found() { + local src="$1" dst_dir="$2" + if [[ -f "$src" ]]; then + cp "$src" "$dst_dir/" + echo " installed $(basename "$src")" + fi +} + +for name in "${MUSIC[@]}"; do + for ext in ogg wav; do + install_if_found "$TMP_DIR/${name}.${ext}" "$ASSETS/music" + done +done +for name in "${AMBIENT[@]}"; do + for ext in ogg wav; do + install_if_found "$TMP_DIR/ambient/${name}.${ext}" "$ASSETS/sounds/ambient" + done +done +for name in "${SE[@]}"; do + for ext in ogg wav; do + install_if_found "$TMP_DIR/${name}.${ext}" "$ASSETS/sounds" + done +done + +echo "" +echo "[download_assets] Done." +du -sh "$ASSETS/music" "$ASSETS/sounds" 2>/dev/null || true diff --git a/scripts/gen_placeholder_assets.sh b/scripts/gen_placeholder_assets.sh new file mode 100755 index 0000000..cc45ab9 --- /dev/null +++ b/scripts/gen_placeholder_assets.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# gen_placeholder_assets.sh +# +# 開発用: 無音の WAV プレースホルダーを生成する。 +# 実際の音声ファイルが手元にない場合でも ft_minecraft が起動できる。 +# +# 使い方: +# bash scripts/gen_placeholder_assets.sh +# +# 必要なもの: python3 (標準インストール済みであること) + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +ASSETS="$REPO_ROOT/assets" + +python3 - <<'PYEOF' +import struct +import os +import sys + +def make_wav(path, duration_s: float, sample_rate: int = 44100, channels: int = 1): + """Generate a minimal silent PCM WAV file.""" + os.makedirs(os.path.dirname(path), exist_ok=True) + if os.path.exists(path): + return # skip existing files + + num_samples = int(sample_rate * duration_s * channels) + pcm = bytes(num_samples * 2) # 16-bit silence + + with open(path, 'wb') as f: + data_size = len(pcm) + byte_rate = sample_rate * channels * 2 + block_align = channels * 2 + + f.write(b'RIFF') + f.write(struct.pack(' Date: Wed, 3 Jun 2026 23:33:32 +0900 Subject: [PATCH 03/10] =?UTF-8?q?download=5Fassets.sh=20=E3=82=92=E5=80=8B?= =?UTF-8?q?=E5=88=A5URL=E6=96=B9=E5=BC=8F=E3=81=AB=E6=9B=B8=E3=81=8D?= =?UTF-8?q?=E7=9B=B4=E3=81=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit URL を変数に代入するだけで済む構成に変更。 空のままにすれば無音プレースホルダーで自動補完。 Co-Authored-By: Claude Sonnet 4.6 --- scripts/download_assets.sh | 202 +++++++++++++++++++------------------ 1 file changed, 102 insertions(+), 100 deletions(-) diff --git a/scripts/download_assets.sh b/scripts/download_assets.sh index 3d3e446..ba21dfe 100755 --- a/scripts/download_assets.sh +++ b/scripts/download_assets.sh @@ -1,113 +1,115 @@ #!/usr/bin/env bash # download_assets.sh # -# 音声アセットをダウンロードする。 -# リポジトリ合計が 42 MB を超えるため、音声ファイルは Git 管理外。 -# # 使い方: -# bash scripts/download_assets.sh # 本番ファイルをダウンロード -# bash scripts/download_assets.sh --placeholder # 無音ファイルを生成(開発用) -# -# 実際のファイルをホストする場合: -# AUDIO_BASE_URL を変更し、ファイル一覧 (MUSIC / AMBIENT / SE) を更新する。 -# 推奨ホスト: GitHub Releases の tar.gz アーカイブ +# bash scripts/download_assets.sh set -euo pipefail - REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -ASSETS="$REPO_ROOT/assets" - -# ── ダウンロード元 URL ──────────────────────────────────────────────────────── -# GitHub Releases にアーカイブをアップロードしたら以下を書き換える。 -# 例: https://github.com/Ishi-eenn/ft_vox/releases/download/v1.0/audio_assets.tar.gz -AUDIO_ARCHIVE_URL="" - -# ── ファイル一覧 ────────────────────────────────────────────────────────────── -MUSIC=( - plains_bgm desert_bgm tundra_bgm rocky_bgm swamp_bgm - mountain_bgm canyon_bgm spring_bgm autumn_bgm -) -AMBIENT=( - plains desert tundra rocky swamp - mountain canyon spring autumn -) -SE=( - footstep_grass footstep_stone footstep_sand footstep_snow footstep_wood - attack hurt swim - block_break block_place - mob_groan mob_hurt mob_explode -) - -# ────────────────────────────────────────────────────────────────────────────── - -placeholder_mode=false -if [[ "${1:-}" == "--placeholder" ]]; then - placeholder_mode=true -fi - -# プレースホルダーモード: 無音 WAV を生成して終了 -if $placeholder_mode; then - echo "[download_assets] --placeholder: generating silent WAV files..." - ASSETS="$ASSETS" bash "$REPO_ROOT/scripts/gen_placeholder_assets.sh" - exit 0 -fi -# URL が未設定なら placeholder にフォールバック -if [[ -z "$AUDIO_ARCHIVE_URL" ]]; then - echo "[download_assets] AUDIO_ARCHIVE_URL is not set." - echo " → Falling back to placeholder generation." - echo " To use real audio files, set AUDIO_ARCHIVE_URL in this script" - echo " or run: bash scripts/download_assets.sh --placeholder" - echo "" - ASSETS="$ASSETS" bash "$REPO_ROOT/scripts/gen_placeholder_assets.sh" - exit 0 -fi - -# ── アーカイブをダウンロードして展開 ───────────────────────────────────────── -TMP_DIR="$(mktemp -d)" -trap 'rm -rf "$TMP_DIR"' EXIT - -echo "[download_assets] Downloading $AUDIO_ARCHIVE_URL ..." - -if command -v curl &>/dev/null; then - curl -L --progress-bar -o "$TMP_DIR/audio_assets.tar.gz" "$AUDIO_ARCHIVE_URL" -elif command -v wget &>/dev/null; then - wget -q --show-progress -O "$TMP_DIR/audio_assets.tar.gz" "$AUDIO_ARCHIVE_URL" -else - echo "ERROR: curl or wget is required." >&2 - exit 1 -fi - -echo "[download_assets] Extracting..." -tar -xzf "$TMP_DIR/audio_assets.tar.gz" -C "$TMP_DIR" - -# アーカイブ内の *.ogg / *.wav をそれぞれ正しいディレクトリへコピー -mkdir -p "$ASSETS/music" "$ASSETS/sounds/ambient" - -install_if_found() { - local src="$1" dst_dir="$2" - if [[ -f "$src" ]]; then - cp "$src" "$dst_dir/" - echo " installed $(basename "$src")" +# ============================================================================= +# ここに URL を書く(ファイルが不要なら "" のままでOK → 無音ファイルで代替) +# ============================================================================= + +# ── BGM ────────────────────────────────────────────────────────────────────── +URL_MUSIC_PLAINS="" +URL_MUSIC_DESERT="" +URL_MUSIC_TUNDRA="" +URL_MUSIC_ROCKY="" +URL_MUSIC_SWAMP="" +URL_MUSIC_MOUNTAIN="" +URL_MUSIC_CANYON="" +URL_MUSIC_SPRING="" +URL_MUSIC_AUTUMN="" + +# ── アンビエント ────────────────────────────────────────────────────────────── +URL_AMBIENT_PLAINS="" +URL_AMBIENT_DESERT="" +URL_AMBIENT_TUNDRA="" +URL_AMBIENT_ROCKY="" +URL_AMBIENT_SWAMP="" +URL_AMBIENT_MOUNTAIN="" +URL_AMBIENT_CANYON="" +URL_AMBIENT_SPRING="" +URL_AMBIENT_AUTUMN="" + +# ── SE(効果音) ────────────────────────────────────────────────────────────── +URL_SE_FOOTSTEP_GRASS="" +URL_SE_FOOTSTEP_STONE="" +URL_SE_FOOTSTEP_SAND="" +URL_SE_FOOTSTEP_SNOW="" +URL_SE_FOOTSTEP_WOOD="" +URL_SE_ATTACK="" +URL_SE_HURT="" +URL_SE_SWIM="" +URL_SE_BLOCK_BREAK="" +URL_SE_BLOCK_PLACE="" +URL_SE_MOB_GROAN="" +URL_SE_MOB_HURT="" +URL_SE_MOB_EXPLODE="" + +# ============================================================================= +# 以下は変更不要 +# ============================================================================= + +mkdir -p \ + "$REPO_ROOT/assets/music" \ + "$REPO_ROOT/assets/sounds/ambient" \ + "$REPO_ROOT/assets/sounds" + +fetch() { + local url="$1" dst="$2" + [[ -z "$url" ]] && return 0 # URL 未設定はスキップ + [[ -f "$dst" ]] && return 0 # 既にあればスキップ + echo " downloading $(basename "$dst") ..." + if command -v curl &>/dev/null; then + curl -fsSL -o "$dst" "$url" + else + wget -q -O "$dst" "$url" fi } -for name in "${MUSIC[@]}"; do - for ext in ogg wav; do - install_if_found "$TMP_DIR/${name}.${ext}" "$ASSETS/music" - done -done -for name in "${AMBIENT[@]}"; do - for ext in ogg wav; do - install_if_found "$TMP_DIR/ambient/${name}.${ext}" "$ASSETS/sounds/ambient" - done -done -for name in "${SE[@]}"; do - for ext in ogg wav; do - install_if_found "$TMP_DIR/${name}.${ext}" "$ASSETS/sounds" - done -done +# BGM +fetch "$URL_MUSIC_PLAINS" "$REPO_ROOT/assets/music/plains_bgm.ogg" +fetch "$URL_MUSIC_DESERT" "$REPO_ROOT/assets/music/desert_bgm.ogg" +fetch "$URL_MUSIC_TUNDRA" "$REPO_ROOT/assets/music/tundra_bgm.ogg" +fetch "$URL_MUSIC_ROCKY" "$REPO_ROOT/assets/music/rocky_bgm.ogg" +fetch "$URL_MUSIC_SWAMP" "$REPO_ROOT/assets/music/swamp_bgm.ogg" +fetch "$URL_MUSIC_MOUNTAIN" "$REPO_ROOT/assets/music/mountain_bgm.ogg" +fetch "$URL_MUSIC_CANYON" "$REPO_ROOT/assets/music/canyon_bgm.ogg" +fetch "$URL_MUSIC_SPRING" "$REPO_ROOT/assets/music/spring_bgm.ogg" +fetch "$URL_MUSIC_AUTUMN" "$REPO_ROOT/assets/music/autumn_bgm.ogg" + +# Ambient +fetch "$URL_AMBIENT_PLAINS" "$REPO_ROOT/assets/sounds/ambient/plains.ogg" +fetch "$URL_AMBIENT_DESERT" "$REPO_ROOT/assets/sounds/ambient/desert.ogg" +fetch "$URL_AMBIENT_TUNDRA" "$REPO_ROOT/assets/sounds/ambient/tundra.ogg" +fetch "$URL_AMBIENT_ROCKY" "$REPO_ROOT/assets/sounds/ambient/rocky.ogg" +fetch "$URL_AMBIENT_SWAMP" "$REPO_ROOT/assets/sounds/ambient/swamp.ogg" +fetch "$URL_AMBIENT_MOUNTAIN" "$REPO_ROOT/assets/sounds/ambient/mountain.ogg" +fetch "$URL_AMBIENT_CANYON" "$REPO_ROOT/assets/sounds/ambient/canyon.ogg" +fetch "$URL_AMBIENT_SPRING" "$REPO_ROOT/assets/sounds/ambient/spring.ogg" +fetch "$URL_AMBIENT_AUTUMN" "$REPO_ROOT/assets/sounds/ambient/autumn.ogg" + +# SE +fetch "$URL_SE_FOOTSTEP_GRASS" "$REPO_ROOT/assets/sounds/footstep_grass.ogg" +fetch "$URL_SE_FOOTSTEP_STONE" "$REPO_ROOT/assets/sounds/footstep_stone.ogg" +fetch "$URL_SE_FOOTSTEP_SAND" "$REPO_ROOT/assets/sounds/footstep_sand.ogg" +fetch "$URL_SE_FOOTSTEP_SNOW" "$REPO_ROOT/assets/sounds/footstep_snow.ogg" +fetch "$URL_SE_FOOTSTEP_WOOD" "$REPO_ROOT/assets/sounds/footstep_wood.ogg" +fetch "$URL_SE_ATTACK" "$REPO_ROOT/assets/sounds/attack.ogg" +fetch "$URL_SE_HURT" "$REPO_ROOT/assets/sounds/hurt.ogg" +fetch "$URL_SE_SWIM" "$REPO_ROOT/assets/sounds/swim.ogg" +fetch "$URL_SE_BLOCK_BREAK" "$REPO_ROOT/assets/sounds/block_break.ogg" +fetch "$URL_SE_BLOCK_PLACE" "$REPO_ROOT/assets/sounds/block_place.ogg" +fetch "$URL_SE_MOB_GROAN" "$REPO_ROOT/assets/sounds/mob_groan.ogg" +fetch "$URL_SE_MOB_HURT" "$REPO_ROOT/assets/sounds/mob_hurt.ogg" +fetch "$URL_SE_MOB_EXPLODE" "$REPO_ROOT/assets/sounds/mob_explode.ogg" + +# URL が空のファイルは無音 WAV で補完 +echo "" +echo "Generating placeholders for missing files..." +ASSETS="$REPO_ROOT/assets" bash "$REPO_ROOT/scripts/gen_placeholder_assets.sh" echo "" -echo "[download_assets] Done." -du -sh "$ASSETS/music" "$ASSETS/sounds" 2>/dev/null || true +echo "Done." From 72a723ffb26c2cbaf838c7569f40166b2be28e07 Mon Sep 17 00:00:00 2001 From: deco-L Date: Wed, 3 Jun 2026 23:40:07 +0900 Subject: [PATCH 04/10] =?UTF-8?q?=20=20=20=20=20=1B[1mSTDIN=1B[0m=20=1B[38?= =?UTF-8?q?;2;145;155;170m=20=20=201=1B[0m=20=1B[38;2;220;223;228m?= =?UTF-8?q?=E9=9F=B3=E5=A3=B0=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AE?= =?UTF-8?q?=E6=8B=A1=E5=BC=B5=E5=AD=90=E3=82=92=E8=87=AA=E5=8B=95=E8=A7=A3?= =?UTF-8?q?=E6=B1=BA=EF=BC=88ogg/mp3/wav/flac=20=E3=81=AB=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C=EF=BC=89=1B[0m=20=1B[38;2;145;155;170m=20=20=202=1B[0?= =?UTF-8?q?m=20=1B[38;2;145;155;170m=20=20=203=1B[0m=20=1B[38;2;220;223;22?= =?UTF-8?q?8m-=20download=5Fassets.sh:=20URL=E6=9C=AB=E5=B0=BE=E3=81=8B?= =?UTF-8?q?=E3=82=89=E6=8B=A1=E5=BC=B5=E5=AD=90=E3=82=92=E8=87=AA=E5=8B=95?= =?UTF-8?q?=E5=8F=96=E5=BE=97=E3=81=97=E3=81=A6=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=92=E4=BF=9D=E5=AD=98=E3=80=82=1B[0m=20=1B[38;2;?= =?UTF-8?q?145;155;170m=20=20=204=1B[0m=20=1B[38;2;220;223;228m=20=20?= =?UTF-8?q?=E6=97=A2=E5=AD=98=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AE?= =?UTF-8?q?=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF=E3=82=82=E6=8B=A1=E5=BC=B5?= =?UTF-8?q?=E5=AD=90=E9=9D=9E=E4=BE=9D=E5=AD=98=E3=81=AB=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=E3=80=82=1B[0m=20=1B[38;2;145;155;170m=20=20=205=1B[0m=20=1B[3?= =?UTF-8?q?8;2;220;223;228m-=20gen=5Fplaceholder=5Fassets.sh:=20stem.*=20?= =?UTF-8?q?=E3=81=8C=E5=AD=98=E5=9C=A8=E3=81=99=E3=82=8C=E3=81=B0=E7=94=9F?= =?UTF-8?q?=E6=88=90=E3=82=92=E3=82=B9=E3=82=AD=E3=83=83=E3=83=97=E3=80=82?= =?UTF-8?q?=1B[0m=20=1B[38;2;145;155;170m=20=20=206=1B[0m=20=1B[38;2;220;2?= =?UTF-8?q?23;228m-=20audio=5Fmanager.cpp:=20kSePaths=20=E2=86=92=20kSeSte?= =?UTF-8?q?ms=EF=BC=88=E6=8B=A1=E5=BC=B5=E5=AD=90=E3=81=AA=E3=81=97?= =?UTF-8?q?=EF=BC=89=E3=81=AB=E5=A4=89=E6=9B=B4=E3=80=82=1B[0m=20=1B[38;2;?= =?UTF-8?q?145;155;170m=20=20=207=1B[0m=20=1B[38;2;220;223;228m=20=20resol?= =?UTF-8?q?veAudioPath()=20=E3=81=A7=20ogg/mp3/wav/flac=20=E3=82=92?= =?UTF-8?q?=E9=A0=86=E3=81=AB=E6=8E=A2=E3=81=97=E3=81=A6=E3=83=92=E3=83=83?= =?UTF-8?q?=E3=83=88=E3=81=97=E3=81=9F=E3=82=82=E3=81=AE=E3=82=92=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E3=80=82=1B[0m=20=1B[38;2;145;155;170m=20=20=208=1B[0?= =?UTF-8?q?m=20=1B[38;2;220;223;228m-=20biome=5Faudio=5Fsystem.cpp:=20BGM/?= =?UTF-8?q?Ambient=20=E3=83=91=E3=82=B9=E3=81=8B=E3=82=89=20.ogg=20?= =?UTF-8?q?=E3=82=92=E9=99=A4=E5=8E=BB=E3=80=82=1B[0m=20=1B[38;2;145;155;1?= =?UTF-8?q?70m=20=20=209=1B[0m=20=1B[38;2;145;155;170m=20=2010=1B[0m=20=1B?= =?UTF-8?q?[38;2;220;223;228mURL=20=E3=81=AB=E8=B2=BC=E3=81=A3=E3=81=9F?= =?UTF-8?q?=E9=9F=B3=E5=A3=B0=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AE?= =?UTF-8?q?=E6=8B=A1=E5=BC=B5=E5=AD=90=E3=81=8C=E4=BD=95=E3=81=A7=E3=81=82?= =?UTF-8?q?=E3=81=A3=E3=81=A6=E3=82=82=E5=8B=95=E4=BD=9C=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=80=82=1B[0m=20=1B[38;2;145;155;170m=20=2011=1B[0m=20=1B[38;?= =?UTF-8?q?2;145;155;170m=20=2012=1B[0m=20=1B[38;2;220;223;228mCo-Authored?= =?UTF-8?q?-By:=20Claude=20Sonnet=204.6=20=1B[0m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/download_assets.sh | 95 +++++++++++++++++++------------ scripts/gen_placeholder_assets.sh | 7 ++- srcs/audio/audio_manager.cpp | 56 +++++++++++------- srcs/audio/biome_audio_system.cpp | 19 ++++--- 4 files changed, 108 insertions(+), 69 deletions(-) diff --git a/scripts/download_assets.sh b/scripts/download_assets.sh index ba21dfe..6f86675 100755 --- a/scripts/download_assets.sh +++ b/scripts/download_assets.sh @@ -3,12 +3,15 @@ # # 使い方: # bash scripts/download_assets.sh +# +# URL の拡張子は自動判定(ogg / mp3 / wav / flac すべてOK)。 +# "" のままにすれば無音 WAV で自動補完。 set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" # ============================================================================= -# ここに URL を書く(ファイルが不要なら "" のままでOK → 無音ファイルで代替) +# ここに URL を書く(不要なら "" のままでOK) # ============================================================================= # ── BGM ────────────────────────────────────────────────────────────────────── @@ -57,10 +60,28 @@ mkdir -p \ "$REPO_ROOT/assets/sounds/ambient" \ "$REPO_ROOT/assets/sounds" +# URL の末尾から拡張子を取り出す(クエリ文字列を除去してから) +url_ext() { + local url="$1" + # クエリ文字列・フラグメントを除去してから拡張子を取得 + local path="${url%%\?*}" + path="${path%%\#*}" + echo "${path##*.}" +} + +# stem: 拡張子なしのファイルパス(例: assets/music/plains_bgm) +# URL の拡張子でファイルを保存。stem.* がすでに存在すればスキップ。 fetch() { - local url="$1" dst="$2" - [[ -z "$url" ]] && return 0 # URL 未設定はスキップ - [[ -f "$dst" ]] && return 0 # 既にあればスキップ + local url="$1" stem="$2" + [[ -z "$url" ]] && return 0 + + # 拡張子違いで既存ファイルがあればスキップ + for f in "${stem}".ogg "${stem}".mp3 "${stem}".wav "${stem}".flac; do + [[ -f "$f" ]] && return 0 + done + + local ext; ext="$(url_ext "$url")" + local dst="${stem}.${ext}" echo " downloading $(basename "$dst") ..." if command -v curl &>/dev/null; then curl -fsSL -o "$dst" "$url" @@ -70,43 +91,43 @@ fetch() { } # BGM -fetch "$URL_MUSIC_PLAINS" "$REPO_ROOT/assets/music/plains_bgm.ogg" -fetch "$URL_MUSIC_DESERT" "$REPO_ROOT/assets/music/desert_bgm.ogg" -fetch "$URL_MUSIC_TUNDRA" "$REPO_ROOT/assets/music/tundra_bgm.ogg" -fetch "$URL_MUSIC_ROCKY" "$REPO_ROOT/assets/music/rocky_bgm.ogg" -fetch "$URL_MUSIC_SWAMP" "$REPO_ROOT/assets/music/swamp_bgm.ogg" -fetch "$URL_MUSIC_MOUNTAIN" "$REPO_ROOT/assets/music/mountain_bgm.ogg" -fetch "$URL_MUSIC_CANYON" "$REPO_ROOT/assets/music/canyon_bgm.ogg" -fetch "$URL_MUSIC_SPRING" "$REPO_ROOT/assets/music/spring_bgm.ogg" -fetch "$URL_MUSIC_AUTUMN" "$REPO_ROOT/assets/music/autumn_bgm.ogg" +fetch "$URL_MUSIC_PLAINS" "$REPO_ROOT/assets/music/plains_bgm" +fetch "$URL_MUSIC_DESERT" "$REPO_ROOT/assets/music/desert_bgm" +fetch "$URL_MUSIC_TUNDRA" "$REPO_ROOT/assets/music/tundra_bgm" +fetch "$URL_MUSIC_ROCKY" "$REPO_ROOT/assets/music/rocky_bgm" +fetch "$URL_MUSIC_SWAMP" "$REPO_ROOT/assets/music/swamp_bgm" +fetch "$URL_MUSIC_MOUNTAIN" "$REPO_ROOT/assets/music/mountain_bgm" +fetch "$URL_MUSIC_CANYON" "$REPO_ROOT/assets/music/canyon_bgm" +fetch "$URL_MUSIC_SPRING" "$REPO_ROOT/assets/music/spring_bgm" +fetch "$URL_MUSIC_AUTUMN" "$REPO_ROOT/assets/music/autumn_bgm" # Ambient -fetch "$URL_AMBIENT_PLAINS" "$REPO_ROOT/assets/sounds/ambient/plains.ogg" -fetch "$URL_AMBIENT_DESERT" "$REPO_ROOT/assets/sounds/ambient/desert.ogg" -fetch "$URL_AMBIENT_TUNDRA" "$REPO_ROOT/assets/sounds/ambient/tundra.ogg" -fetch "$URL_AMBIENT_ROCKY" "$REPO_ROOT/assets/sounds/ambient/rocky.ogg" -fetch "$URL_AMBIENT_SWAMP" "$REPO_ROOT/assets/sounds/ambient/swamp.ogg" -fetch "$URL_AMBIENT_MOUNTAIN" "$REPO_ROOT/assets/sounds/ambient/mountain.ogg" -fetch "$URL_AMBIENT_CANYON" "$REPO_ROOT/assets/sounds/ambient/canyon.ogg" -fetch "$URL_AMBIENT_SPRING" "$REPO_ROOT/assets/sounds/ambient/spring.ogg" -fetch "$URL_AMBIENT_AUTUMN" "$REPO_ROOT/assets/sounds/ambient/autumn.ogg" +fetch "$URL_AMBIENT_PLAINS" "$REPO_ROOT/assets/sounds/ambient/plains" +fetch "$URL_AMBIENT_DESERT" "$REPO_ROOT/assets/sounds/ambient/desert" +fetch "$URL_AMBIENT_TUNDRA" "$REPO_ROOT/assets/sounds/ambient/tundra" +fetch "$URL_AMBIENT_ROCKY" "$REPO_ROOT/assets/sounds/ambient/rocky" +fetch "$URL_AMBIENT_SWAMP" "$REPO_ROOT/assets/sounds/ambient/swamp" +fetch "$URL_AMBIENT_MOUNTAIN" "$REPO_ROOT/assets/sounds/ambient/mountain" +fetch "$URL_AMBIENT_CANYON" "$REPO_ROOT/assets/sounds/ambient/canyon" +fetch "$URL_AMBIENT_SPRING" "$REPO_ROOT/assets/sounds/ambient/spring" +fetch "$URL_AMBIENT_AUTUMN" "$REPO_ROOT/assets/sounds/ambient/autumn" # SE -fetch "$URL_SE_FOOTSTEP_GRASS" "$REPO_ROOT/assets/sounds/footstep_grass.ogg" -fetch "$URL_SE_FOOTSTEP_STONE" "$REPO_ROOT/assets/sounds/footstep_stone.ogg" -fetch "$URL_SE_FOOTSTEP_SAND" "$REPO_ROOT/assets/sounds/footstep_sand.ogg" -fetch "$URL_SE_FOOTSTEP_SNOW" "$REPO_ROOT/assets/sounds/footstep_snow.ogg" -fetch "$URL_SE_FOOTSTEP_WOOD" "$REPO_ROOT/assets/sounds/footstep_wood.ogg" -fetch "$URL_SE_ATTACK" "$REPO_ROOT/assets/sounds/attack.ogg" -fetch "$URL_SE_HURT" "$REPO_ROOT/assets/sounds/hurt.ogg" -fetch "$URL_SE_SWIM" "$REPO_ROOT/assets/sounds/swim.ogg" -fetch "$URL_SE_BLOCK_BREAK" "$REPO_ROOT/assets/sounds/block_break.ogg" -fetch "$URL_SE_BLOCK_PLACE" "$REPO_ROOT/assets/sounds/block_place.ogg" -fetch "$URL_SE_MOB_GROAN" "$REPO_ROOT/assets/sounds/mob_groan.ogg" -fetch "$URL_SE_MOB_HURT" "$REPO_ROOT/assets/sounds/mob_hurt.ogg" -fetch "$URL_SE_MOB_EXPLODE" "$REPO_ROOT/assets/sounds/mob_explode.ogg" - -# URL が空のファイルは無音 WAV で補完 +fetch "$URL_SE_FOOTSTEP_GRASS" "$REPO_ROOT/assets/sounds/footstep_grass" +fetch "$URL_SE_FOOTSTEP_STONE" "$REPO_ROOT/assets/sounds/footstep_stone" +fetch "$URL_SE_FOOTSTEP_SAND" "$REPO_ROOT/assets/sounds/footstep_sand" +fetch "$URL_SE_FOOTSTEP_SNOW" "$REPO_ROOT/assets/sounds/footstep_snow" +fetch "$URL_SE_FOOTSTEP_WOOD" "$REPO_ROOT/assets/sounds/footstep_wood" +fetch "$URL_SE_ATTACK" "$REPO_ROOT/assets/sounds/attack" +fetch "$URL_SE_HURT" "$REPO_ROOT/assets/sounds/hurt" +fetch "$URL_SE_SWIM" "$REPO_ROOT/assets/sounds/swim" +fetch "$URL_SE_BLOCK_BREAK" "$REPO_ROOT/assets/sounds/block_break" +fetch "$URL_SE_BLOCK_PLACE" "$REPO_ROOT/assets/sounds/block_place" +fetch "$URL_SE_MOB_GROAN" "$REPO_ROOT/assets/sounds/mob_groan" +fetch "$URL_SE_MOB_HURT" "$REPO_ROOT/assets/sounds/mob_hurt" +fetch "$URL_SE_MOB_EXPLODE" "$REPO_ROOT/assets/sounds/mob_explode" + +# 未ダウンロードのファイルは無音 WAV で補完 echo "" echo "Generating placeholders for missing files..." ASSETS="$REPO_ROOT/assets" bash "$REPO_ROOT/scripts/gen_placeholder_assets.sh" diff --git a/scripts/gen_placeholder_assets.sh b/scripts/gen_placeholder_assets.sh index cc45ab9..d01321f 100755 --- a/scripts/gen_placeholder_assets.sh +++ b/scripts/gen_placeholder_assets.sh @@ -22,8 +22,11 @@ import sys def make_wav(path, duration_s: float, sample_rate: int = 44100, channels: int = 1): """Generate a minimal silent PCM WAV file.""" os.makedirs(os.path.dirname(path), exist_ok=True) - if os.path.exists(path): - return # skip existing files + # 拡張子違いでも存在すればスキップ + stem = os.path.splitext(path)[0] + for ext in ('.ogg', '.mp3', '.wav', '.flac'): + if os.path.exists(stem + ext): + return num_samples = int(sample_rate * duration_s * channels) pcm = bytes(num_samples * 2) # 16-bit silence diff --git a/srcs/audio/audio_manager.cpp b/srcs/audio/audio_manager.cpp index 66acb7f..da3421a 100644 --- a/srcs/audio/audio_manager.cpp +++ b/srcs/audio/audio_manager.cpp @@ -5,25 +5,39 @@ #include #include -// ── SE path table (relative to assets_root) ────────────────────────────────── -static const char* kSePaths[] = { - "sounds/footstep_grass.ogg", // FootstepGrass - "sounds/footstep_stone.ogg", // FootstepStone - "sounds/footstep_sand.ogg", // FootstepSand - "sounds/footstep_snow.ogg", // FootstepSnow - "sounds/footstep_wood.ogg", // FootstepWood - "sounds/attack.ogg", // Attack - "sounds/hurt.ogg", // Hurt - "sounds/swim.ogg", // Swim - "sounds/block_break.ogg", // BlockBreak - "sounds/block_place.ogg", // BlockPlace - "sounds/mob_groan.ogg", // MobGroan - "sounds/mob_hurt.ogg", // MobHurt - "sounds/mob_explode.ogg", // MobExplode +// ── 拡張子なしの SE パス(stems) ──────────────────────────────────────────── +static const char* kSeStems[] = { + "sounds/footstep_grass", // FootstepGrass + "sounds/footstep_stone", // FootstepStone + "sounds/footstep_sand", // FootstepSand + "sounds/footstep_snow", // FootstepSnow + "sounds/footstep_wood", // FootstepWood + "sounds/attack", // Attack + "sounds/hurt", // Hurt + "sounds/swim", // Swim + "sounds/block_break", // BlockBreak + "sounds/block_place", // BlockPlace + "sounds/mob_groan", // MobGroan + "sounds/mob_hurt", // MobHurt + "sounds/mob_explode", // MobExplode }; -static_assert(sizeof(kSePaths) / sizeof(kSePaths[0]) == +static_assert(sizeof(kSeStems) / sizeof(kSeStems[0]) == static_cast(SoundEvent::COUNT), - "kSePaths length mismatch"); + "kSeStems length mismatch"); + +// ogg / mp3 / wav / flac のどれかが存在するパスを返す。 +// 見つからなければ stem + ".ogg" を返す(AudioManager が警告を出す)。 +static std::string resolveAudioPath(const std::string& stem) { + static const char* kExts[] = {".ogg", ".mp3", ".wav", ".flac"}; + for (const char* ext : kExts) { + std::string candidate = stem + ext; + if (FILE* f = std::fopen(candidate.c_str(), "rb")) { + std::fclose(f); + return candidate; + } + } + return stem + ".ogg"; +} // ── BGM slot (one of two ping-pong slots) ───────────────────────────────────── struct BgmSlot { @@ -257,7 +271,7 @@ void AudioManager::playBgm(const std::string& path, float fade_duration) { d.bgm[next].playing = false; } - std::string full = d.assets_root + "/" + path; + std::string full = resolveAudioPath(d.assets_root + "/" + path); ma_uint32 flags = MA_SOUND_FLAG_STREAM | MA_SOUND_FLAG_NO_SPATIALIZATION; if (ma_sound_init_from_file(&d.engine, full.c_str(), flags, &d.bgm_group, nullptr, @@ -323,7 +337,7 @@ void AudioManager::playAmbient(const std::string& path, float fade_in) { d.ambient_fade_timer = 0.0f; } - std::string full = d.assets_root + "/" + path; + std::string full = resolveAudioPath(d.assets_root + "/" + path); ma_uint32 flags = MA_SOUND_FLAG_STREAM | MA_SOUND_FLAG_NO_SPATIALIZATION; if (ma_sound_init_from_file(&d.engine, full.c_str(), flags, &d.ambient_group, nullptr, @@ -359,7 +373,7 @@ void AudioManager::playSe(SoundEvent event) { int idx = static_cast(event); if (idx < 0 || idx >= static_cast(SoundEvent::COUNT)) return; - std::string full = d.assets_root + "/" + kSePaths[idx]; + std::string full = resolveAudioPath(d.assets_root + "/" + kSeStems[idx]); if (ma_engine_play_sound(&d.engine, full.c_str(), &d.se_group) != MA_SUCCESS) fprintf(stderr, "[Audio] playSe failed: %s\n", full.c_str()); } @@ -381,7 +395,7 @@ void AudioManager::playSe3D(SoundEvent event, float x, float y, float z, int idx = static_cast(event); if (idx < 0 || idx >= static_cast(SoundEvent::COUNT)) return; - std::string full = d.assets_root + "/" + kSePaths[idx]; + std::string full = resolveAudioPath(d.assets_root + "/" + kSeStems[idx]); // Load without streaming (SE are short) and with spatialization enabled if (ma_sound_init_from_file(&d.engine, full.c_str(), 0, &d.se_group, nullptr, diff --git a/srcs/audio/biome_audio_system.cpp b/srcs/audio/biome_audio_system.cpp index 170a83f..113e66d 100644 --- a/srcs/audio/biome_audio_system.cpp +++ b/srcs/audio/biome_audio_system.cpp @@ -6,15 +6,16 @@ void BiomeAudioSystem::init(AudioManager& mgr) { mgr_ = &mgr; - registerBiome("PLAINS", {"music/plains_bgm.ogg", "sounds/ambient/plains.ogg"}); - registerBiome("DESERT", {"music/desert_bgm.ogg", "sounds/ambient/desert.ogg"}); - registerBiome("TUNDRA", {"music/tundra_bgm.ogg", "sounds/ambient/tundra.ogg"}); - registerBiome("ROCKY", {"music/rocky_bgm.ogg", "sounds/ambient/rocky.ogg"}); - registerBiome("SWAMP", {"music/swamp_bgm.ogg", "sounds/ambient/swamp.ogg"}); - registerBiome("MOUNTAIN", {"music/mountain_bgm.ogg", "sounds/ambient/mountain.ogg"}); - registerBiome("CANYON", {"music/canyon_bgm.ogg", "sounds/ambient/canyon.ogg"}); - registerBiome("SPRING", {"music/spring_bgm.ogg", "sounds/ambient/spring.ogg"}); - registerBiome("AUTUMN", {"music/autumn_bgm.ogg", "sounds/ambient/autumn.ogg"}); + // 拡張子は AudioManager 側で自動解決(ogg/mp3/wav/flac に対応) + registerBiome("PLAINS", {"music/plains_bgm", "sounds/ambient/plains"}); + registerBiome("DESERT", {"music/desert_bgm", "sounds/ambient/desert"}); + registerBiome("TUNDRA", {"music/tundra_bgm", "sounds/ambient/tundra"}); + registerBiome("ROCKY", {"music/rocky_bgm", "sounds/ambient/rocky"}); + registerBiome("SWAMP", {"music/swamp_bgm", "sounds/ambient/swamp"}); + registerBiome("MOUNTAIN", {"music/mountain_bgm", "sounds/ambient/mountain"}); + registerBiome("CANYON", {"music/canyon_bgm", "sounds/ambient/canyon"}); + registerBiome("SPRING", {"music/spring_bgm", "sounds/ambient/spring"}); + registerBiome("AUTUMN", {"music/autumn_bgm", "sounds/ambient/autumn"}); } void BiomeAudioSystem::registerBiome(const std::string& biome_name, From 748a0121d94fed438d86ce1f86a396fe50ecd824 Mon Sep 17 00:00:00 2001 From: deco-L Date: Wed, 3 Jun 2026 23:48:12 +0900 Subject: [PATCH 05/10] =?UTF-8?q?=20=20=20=20=20=1B[1mSTDIN=1B[0m=20=1B[38?= =?UTF-8?q?;2;145;155;170m=20=20=201=1B[0m=20=1B[38;2;220;223;228mfix(audi?= =?UTF-8?q?o):=20placeholder=20marker=20system=20prevents=20re-download=20?= =?UTF-8?q?being=20skipped=1B[0m=20=1B[38;2;145;155;170m=20=20=202=1B[0m?= =?UTF-8?q?=20=1B[38;2;145;155;170m=20=20=203=1B[0m=20=1B[38;2;220;223;228?= =?UTF-8?q?mSilent=20WAVs=20generated=20by=20gen=5Fplaceholder=5Fassets.sh?= =?UTF-8?q?=20now=20receive=20a=1B[0m=20=1B[38;2;145;155;170m=20=20=204=1B?= =?UTF-8?q?[0m=20=1B[38;2;220;223;228msibling=20.placeholder=20marker=20fi?= =?UTF-8?q?le.=20download=5Fassets.sh=20respects=20the=1B[0m=20=1B[38;2;14?= =?UTF-8?q?5;155;170m=20=20=205=1B[0m=20=1B[38;2;220;223;228mmarker:=20it?= =?UTF-8?q?=20overwrites=20placeholder=20WAVs=20when=20a=20URL=20is=20prov?= =?UTF-8?q?ided,=20but=1B[0m=20=1B[38;2;145;155;170m=20=20=206=1B[0m=20=1B?= =?UTF-8?q?[38;2;220;223;228mskips=20files=20that=20have=20no=20marker=20(?= =?UTF-8?q?i.e.=20real=20audio=20already=20in=20place).=1B[0m=20=1B[38;2;1?= =?UTF-8?q?45;155;170m=20=20=207=1B[0m=20=1B[38;2;145;155;170m=20=20=208?= =?UTF-8?q?=1B[0m=20=1B[38;2;220;223;228mCo-Authored-By:=20Claude=20Sonnet?= =?UTF-8?q?=204.6=20=1B[0m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/download_assets.sh | 114 ++++++++++++++++-------------- scripts/gen_placeholder_assets.sh | 90 +++++++++-------------- 2 files changed, 95 insertions(+), 109 deletions(-) diff --git a/scripts/download_assets.sh b/scripts/download_assets.sh index 6f86675..23deb47 100755 --- a/scripts/download_assets.sh +++ b/scripts/download_assets.sh @@ -15,11 +15,11 @@ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" # ============================================================================= # ── BGM ────────────────────────────────────────────────────────────────────── -URL_MUSIC_PLAINS="" +URL_MUSIC_PLAINS="https://opengameart.org/sites/default/files/old%20city%20theme_3.ogg" URL_MUSIC_DESERT="" URL_MUSIC_TUNDRA="" URL_MUSIC_ROCKY="" -URL_MUSIC_SWAMP="" +URL_MUSIC_SWAMP="https://opengameart.org/sites/default/files/old%20city%20theme_3.ogg" URL_MUSIC_MOUNTAIN="" URL_MUSIC_CANYON="" URL_MUSIC_SPRING="" @@ -56,76 +56,82 @@ URL_SE_MOB_EXPLODE="" # ============================================================================= mkdir -p \ - "$REPO_ROOT/assets/music" \ - "$REPO_ROOT/assets/sounds/ambient" \ - "$REPO_ROOT/assets/sounds" + "$REPO_ROOT/assets/music" \ + "$REPO_ROOT/assets/sounds/ambient" \ + "$REPO_ROOT/assets/sounds" # URL の末尾から拡張子を取り出す(クエリ文字列を除去してから) url_ext() { - local url="$1" - # クエリ文字列・フラグメントを除去してから拡張子を取得 - local path="${url%%\?*}" - path="${path%%\#*}" - echo "${path##*.}" + local url="$1" + # クエリ文字列・フラグメントを除去してから拡張子を取得 + local path="${url%%\?*}" + path="${path%%\#*}" + echo "${path##*.}" } # stem: 拡張子なしのファイルパス(例: assets/music/plains_bgm) # URL の拡張子でファイルを保存。stem.* がすでに存在すればスキップ。 fetch() { - local url="$1" stem="$2" - [[ -z "$url" ]] && return 0 + local url="$1" stem="$2" + [[ -z "$url" ]] && return 0 - # 拡張子違いで既存ファイルがあればスキップ + # 本物のファイルがすでにある(マーカーなし)ならスキップ + if [[ ! -f "${stem}.placeholder" ]]; then for f in "${stem}".ogg "${stem}".mp3 "${stem}".wav "${stem}".flac; do - [[ -f "$f" ]] && return 0 + [[ -f "$f" ]] && return 0 done - - local ext; ext="$(url_ext "$url")" - local dst="${stem}.${ext}" - echo " downloading $(basename "$dst") ..." - if command -v curl &>/dev/null; then - curl -fsSL -o "$dst" "$url" - else - wget -q -O "$dst" "$url" - fi + fi + + local ext + ext="$(url_ext "$url")" + local dst="${stem}.${ext}" + echo " downloading $(basename "$dst") ..." + if command -v curl &>/dev/null; then + curl -fsSL -o "$dst" "$url" + else + wget -q -O "$dst" "$url" + fi + + # ダウンロード成功 → プレースホルダーを削除 + rm -f "${stem}.placeholder" "${stem}.wav" } # BGM -fetch "$URL_MUSIC_PLAINS" "$REPO_ROOT/assets/music/plains_bgm" -fetch "$URL_MUSIC_DESERT" "$REPO_ROOT/assets/music/desert_bgm" -fetch "$URL_MUSIC_TUNDRA" "$REPO_ROOT/assets/music/tundra_bgm" -fetch "$URL_MUSIC_ROCKY" "$REPO_ROOT/assets/music/rocky_bgm" -fetch "$URL_MUSIC_SWAMP" "$REPO_ROOT/assets/music/swamp_bgm" -fetch "$URL_MUSIC_MOUNTAIN" "$REPO_ROOT/assets/music/mountain_bgm" -fetch "$URL_MUSIC_CANYON" "$REPO_ROOT/assets/music/canyon_bgm" -fetch "$URL_MUSIC_SPRING" "$REPO_ROOT/assets/music/spring_bgm" -fetch "$URL_MUSIC_AUTUMN" "$REPO_ROOT/assets/music/autumn_bgm" +fetch "$URL_MUSIC_PLAINS" "$REPO_ROOT/assets/music/plains_bgm" +fetch "$URL_MUSIC_DESERT" "$REPO_ROOT/assets/music/desert_bgm" +fetch "$URL_MUSIC_TUNDRA" "$REPO_ROOT/assets/music/tundra_bgm" +fetch "$URL_MUSIC_ROCKY" "$REPO_ROOT/assets/music/rocky_bgm" +fetch "$URL_MUSIC_SWAMP" "$REPO_ROOT/assets/music/swamp_bgm" +fetch "$URL_MUSIC_MOUNTAIN" "$REPO_ROOT/assets/music/mountain_bgm" +fetch "$URL_MUSIC_CANYON" "$REPO_ROOT/assets/music/canyon_bgm" +fetch "$URL_MUSIC_SPRING" "$REPO_ROOT/assets/music/spring_bgm" +fetch "$URL_MUSIC_AUTUMN" "$REPO_ROOT/assets/music/autumn_bgm" # Ambient -fetch "$URL_AMBIENT_PLAINS" "$REPO_ROOT/assets/sounds/ambient/plains" -fetch "$URL_AMBIENT_DESERT" "$REPO_ROOT/assets/sounds/ambient/desert" -fetch "$URL_AMBIENT_TUNDRA" "$REPO_ROOT/assets/sounds/ambient/tundra" -fetch "$URL_AMBIENT_ROCKY" "$REPO_ROOT/assets/sounds/ambient/rocky" -fetch "$URL_AMBIENT_SWAMP" "$REPO_ROOT/assets/sounds/ambient/swamp" -fetch "$URL_AMBIENT_MOUNTAIN" "$REPO_ROOT/assets/sounds/ambient/mountain" -fetch "$URL_AMBIENT_CANYON" "$REPO_ROOT/assets/sounds/ambient/canyon" -fetch "$URL_AMBIENT_SPRING" "$REPO_ROOT/assets/sounds/ambient/spring" -fetch "$URL_AMBIENT_AUTUMN" "$REPO_ROOT/assets/sounds/ambient/autumn" +fetch "$URL_AMBIENT_PLAINS" "$REPO_ROOT/assets/sounds/ambient/plains" +fetch "$URL_AMBIENT_DESERT" "$REPO_ROOT/assets/sounds/ambient/desert" +fetch "$URL_AMBIENT_TUNDRA" "$REPO_ROOT/assets/sounds/ambient/tundra" +fetch "$URL_AMBIENT_ROCKY" "$REPO_ROOT/assets/sounds/ambient/rocky" +fetch "$URL_AMBIENT_SWAMP" "$REPO_ROOT/assets/sounds/ambient/swamp" +fetch "$URL_AMBIENT_MOUNTAIN" "$REPO_ROOT/assets/sounds/ambient/mountain" +fetch "$URL_AMBIENT_CANYON" "$REPO_ROOT/assets/sounds/ambient/canyon" +fetch "$URL_AMBIENT_SPRING" "$REPO_ROOT/assets/sounds/ambient/spring" +fetch "$URL_AMBIENT_AUTUMN" "$REPO_ROOT/assets/sounds/ambient/autumn" # SE -fetch "$URL_SE_FOOTSTEP_GRASS" "$REPO_ROOT/assets/sounds/footstep_grass" -fetch "$URL_SE_FOOTSTEP_STONE" "$REPO_ROOT/assets/sounds/footstep_stone" -fetch "$URL_SE_FOOTSTEP_SAND" "$REPO_ROOT/assets/sounds/footstep_sand" -fetch "$URL_SE_FOOTSTEP_SNOW" "$REPO_ROOT/assets/sounds/footstep_snow" -fetch "$URL_SE_FOOTSTEP_WOOD" "$REPO_ROOT/assets/sounds/footstep_wood" -fetch "$URL_SE_ATTACK" "$REPO_ROOT/assets/sounds/attack" -fetch "$URL_SE_HURT" "$REPO_ROOT/assets/sounds/hurt" -fetch "$URL_SE_SWIM" "$REPO_ROOT/assets/sounds/swim" -fetch "$URL_SE_BLOCK_BREAK" "$REPO_ROOT/assets/sounds/block_break" -fetch "$URL_SE_BLOCK_PLACE" "$REPO_ROOT/assets/sounds/block_place" -fetch "$URL_SE_MOB_GROAN" "$REPO_ROOT/assets/sounds/mob_groan" -fetch "$URL_SE_MOB_HURT" "$REPO_ROOT/assets/sounds/mob_hurt" -fetch "$URL_SE_MOB_EXPLODE" "$REPO_ROOT/assets/sounds/mob_explode" +fetch "$URL_SE_FOOTSTEP_GRASS" "$REPO_ROOT/assets/sounds/footstep_grass" +fetch "$URL_SE_FOOTSTEP_STONE" "$REPO_ROOT/assets/sounds/footstep_stone" +fetch "$URL_SE_FOOTSTEP_SAND" "$REPO_ROOT/assets/sounds/footstep_sand" +fetch "$URL_SE_FOOTSTEP_SNOW" "$REPO_ROOT/assets/sounds/footstep_snow" +fetch "$URL_SE_FOOTSTEP_WOOD" "$REPO_ROOT/assets/sounds/footstep_wood" +fetch "$URL_SE_ATTACK" "$REPO_ROOT/assets/sounds/attack" +fetch "$URL_SE_HURT" "$REPO_ROOT/assets/sounds/hurt" +fetch "$URL_SE_SWIM" "$REPO_ROOT/assets/sounds/swim" +fetch "$URL_SE_BLOCK_BREAK" "$REPO_ROOT/assets/sounds/block_break" +fetch "$URL_SE_BLOCK_PLACE" "$REPO_ROOT/assets/sounds/block_place" +fetch "$URL_SE_MOB_GROAN" "$REPO_ROOT/assets/sounds/mob_groan" +fetch "$URL_SE_MOB_HURT" "$REPO_ROOT/assets/sounds/mob_hurt" +fetch "$URL_SE_MOB_EXPLODE" "$REPO_ROOT/assets/sounds/mob_explode" # 未ダウンロードのファイルは無音 WAV で補完 echo "" diff --git a/scripts/gen_placeholder_assets.sh b/scripts/gen_placeholder_assets.sh index d01321f..dfc41f2 100755 --- a/scripts/gen_placeholder_assets.sh +++ b/scripts/gen_placeholder_assets.sh @@ -2,85 +2,65 @@ # gen_placeholder_assets.sh # # 開発用: 無音の WAV プレースホルダーを生成する。 -# 実際の音声ファイルが手元にない場合でも ft_minecraft が起動できる。 -# -# 使い方: -# bash scripts/gen_placeholder_assets.sh -# -# 必要なもの: python3 (標準インストール済みであること) +# 生成したファイルには stem.placeholder マーカーを置く。 +# マーカーがあるファイルは download_assets.sh が URL 設定時に上書きする。 set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -ASSETS="$REPO_ROOT/assets" +ASSETS="${ASSETS:-$REPO_ROOT/assets}" + +python3 - "$ASSETS" <<'PYEOF' +import struct, os, sys -python3 - <<'PYEOF' -import struct -import os -import sys +assets = sys.argv[1] -def make_wav(path, duration_s: float, sample_rate: int = 44100, channels: int = 1): - """Generate a minimal silent PCM WAV file.""" - os.makedirs(os.path.dirname(path), exist_ok=True) - # 拡張子違いでも存在すればスキップ - stem = os.path.splitext(path)[0] +def make_wav(stem, duration_s, sample_rate=44100, channels=1): + os.makedirs(os.path.dirname(stem), exist_ok=True) + + # 本物のファイルがあればスキップ(マーカーなし=本物) for ext in ('.ogg', '.mp3', '.wav', '.flac'): - if os.path.exists(stem + ext): + candidate = stem + ext + if os.path.exists(candidate) and not os.path.exists(stem + '.placeholder'): return + wav_path = stem + '.wav' num_samples = int(sample_rate * duration_s * channels) - pcm = bytes(num_samples * 2) # 16-bit silence - - with open(path, 'wb') as f: - data_size = len(pcm) - byte_rate = sample_rate * channels * 2 - block_align = channels * 2 - + pcm = bytes(num_samples * 2) + with open(wav_path, 'wb') as f: + data_size = len(pcm) f.write(b'RIFF') f.write(struct.pack(' Date: Thu, 4 Jun 2026 00:15:47 +0900 Subject: [PATCH 06/10] =?UTF-8?q?=20=20=20=20=20=1B[1mSTDIN=1B[0m=20=1B[38?= =?UTF-8?q?;2;145;155;170m=20=20=201=1B[0m=20=1B[38;2;220;223;228mfeat(aud?= =?UTF-8?q?io):=20add=20CC-licensed=20BGM=20for=20all=209=20biomes=20from?= =?UTF-8?q?=20OpenGameArt=1B[0m=20=1B[38;2;145;155;170m=20=20=202=1B[0m=20?= =?UTF-8?q?=1B[38;2;145;155;170m=20=20=203=1B[0m=20=1B[38;2;220;223;228m-?= =?UTF-8?q?=20PLAINS:=20=20=20RPG=20Exploration=20Level=201=20(HitCtrl,=20?= =?UTF-8?q?CC-BY=203.0)=1B[0m=20=1B[38;2;145;155;170m=20=20=204=1B[0m=20?= =?UTF-8?q?=1B[38;2;220;223;228m-=20DESERT:=20=20=20caravan=20/=20Desert?= =?UTF-8?q?=20theme=20(Loyalty=20Freak=20Music,=20CC0)=1B[0m=20=1B[38;2;14?= =?UTF-8?q?5;155;170m=20=20=205=1B[0m=20=1B[38;2;220;223;228m-=20TUNDRA:?= =?UTF-8?q?=20=20=20Ice=20Shine=20Bells=20(hc,=20CC0)=1B[0m=20=1B[38;2;145?= =?UTF-8?q?;155;170m=20=20=206=1B[0m=20=1B[38;2;220;223;228m-=20ROCKY:=20?= =?UTF-8?q?=20=20=20Cave=20Theme=20(Brandon=20Morris,=20CC0)=1B[0m=20=1B[3?= =?UTF-8?q?8;2;145;155;170m=20=20=207=1B[0m=20=1B[38;2;220;223;228m-=20SWA?= =?UTF-8?q?MP:=20=20=20=20Hazy=20Darkness=20Looping=20(Eric=20Matyas,=20CC?= =?UTF-8?q?-BY=203.0)=1B[0m=20=1B[38;2;145;155;170m=20=20=208=1B[0m=20=1B[?= =?UTF-8?q?38;2;220;223;228m-=20MOUNTAIN:=20DarkWinds=20orchestral=20(Zefz?= =?UTF-8?q?,=20CC-BY-SA=203.0)=1B[0m=20=1B[38;2;145;155;170m=20=20=209=1B[?= =?UTF-8?q?0m=20=1B[38;2;220;223;228m-=20CANYON:=20=20=20Birds=20and=20Win?= =?UTF-8?q?d=20-=20Ambient=20(Spring=20Spring,=20CC0)=1B[0m=20=1B[38;2;145?= =?UTF-8?q?;155;170m=20=2010=1B[0m=20=1B[38;2;220;223;228m-=20SPRING:=20?= =?UTF-8?q?=20=20Flowerbed=20Fields=20(Zane=20Little=20Music,=20CC0)=1B[0m?= =?UTF-8?q?=20=1B[38;2;145;155;170m=20=2011=1B[0m=20=1B[38;2;220;223;228m-?= =?UTF-8?q?=20AUTUMN:=20=20=20The=20Last=20Leaf=20of=20the=20Fall=20(HitCt?= =?UTF-8?q?rl,=20CC-BY=203.0)=1B[0m=20=1B[38;2;145;155;170m=20=2012=1B[0m?= =?UTF-8?q?=20=1B[38;2;145;155;170m=20=2013=1B[0m=20=1B[38;2;220;223;228mA?= =?UTF-8?q?lso=20normalize=20downloaded=20extension=20to=20lowercase=20to?= =?UTF-8?q?=20avoid=20case-sensitive=1B[0m=20=1B[38;2;145;155;170m=20=2014?= =?UTF-8?q?=1B[0m=20=1B[38;2;220;223;228mfilesystem=20issues=20(e.g.=20.OG?= =?UTF-8?q?G=20=E2=86=92=20.ogg).=1B[0m=20=1B[38;2;145;155;170m=20=2015=1B?= =?UTF-8?q?[0m=20=1B[38;2;145;155;170m=20=2016=1B[0m=20=1B[38;2;220;223;22?= =?UTF-8?q?8mCo-Authored-By:=20Claude=20Sonnet=204.6=20=1B[0m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/download_assets.sh | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/scripts/download_assets.sh b/scripts/download_assets.sh index 23deb47..9d77819 100755 --- a/scripts/download_assets.sh +++ b/scripts/download_assets.sh @@ -15,15 +15,24 @@ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" # ============================================================================= # ── BGM ────────────────────────────────────────────────────────────────────── -URL_MUSIC_PLAINS="https://opengameart.org/sites/default/files/old%20city%20theme_3.ogg" -URL_MUSIC_DESERT="" -URL_MUSIC_TUNDRA="" -URL_MUSIC_ROCKY="" -URL_MUSIC_SWAMP="https://opengameart.org/sites/default/files/old%20city%20theme_3.ogg" -URL_MUSIC_MOUNTAIN="" -URL_MUSIC_CANYON="" -URL_MUSIC_SPRING="" -URL_MUSIC_AUTUMN="" +# CC-BY 3.0: RPG Exploration Level 1 (HitCtrl) +URL_MUSIC_PLAINS="https://opengameart.org/sites/default/files/RPG_Exploration_Level_1.ogg" +# CC0: Desert theme / caravan (Loyalty Freak Music) +URL_MUSIC_DESERT="https://opengameart.org/sites/default/files/caravan.ogg.ogg" +# CC0: Ice Shine Bells (hc) +URL_MUSIC_TUNDRA="https://opengameart.org/sites/default/files/Ice-Shine-Bells_0.ogg" +# CC0: Cave Theme (Brandon Morris) +URL_MUSIC_ROCKY="https://opengameart.org/sites/default/files/cave%20themeb4.ogg" +# CC-BY 3.0: Hazy Darkness Looping (Eric Matyas) +URL_MUSIC_SWAMP="https://opengameart.org/sites/default/files/hazy_darkness_looping_0.ogg" +# CC-BY-SA 3.0: DarkWinds orchestral (Zefz) +URL_MUSIC_MOUNTAIN="https://opengameart.org/sites/default/files/DarkWinds_0.OGG" +# CC0: Birds and Wind - Ambient (Spring Spring) +URL_MUSIC_CANYON="https://opengameart.org/sites/default/files/Birds%20and%20Wind%20-%20Ambient_1.ogg" +# CC0: Flowerbed Fields (Zane Little Music) +URL_MUSIC_SPRING="https://opengameart.org/sites/default/files/flowerbed_fields.ogg" +# CC-BY 3.0: The Last Leaf of the Fall (HitCtrl) +URL_MUSIC_AUTUMN="https://opengameart.org/sites/default/files/the_last_leaf_of_the_fall.ogg" # ── アンビエント ────────────────────────────────────────────────────────────── URL_AMBIENT_PLAINS="" @@ -83,7 +92,7 @@ fetch() { fi local ext - ext="$(url_ext "$url")" + ext="$(url_ext "$url" | tr '[:upper:]' '[:lower:]')" local dst="${stem}.${ext}" echo " downloading $(basename "$dst") ..." if command -v curl &>/dev/null; then From c504e2ec46e2ca61bbece5f95193dbe7de106388 Mon Sep 17 00:00:00 2001 From: deco-L Date: Thu, 4 Jun 2026 23:39:54 +0900 Subject: [PATCH 07/10] =?UTF-8?q?=20=20=20=20=20=1B[1mSTDIN=1B[0m=20=1B[38?= =?UTF-8?q?;2;145;155;170m=20=20=201=1B[0m=20=1B[38;2;220;223;228mfeat(aud?= =?UTF-8?q?io):=20wire=203D=20positional=20sound=20for=20mob=20events=1B[0?= =?UTF-8?q?m=20=1B[38;2;145;155;170m=20=20=202=1B[0m=20=1B[38;2;145;155;17?= =?UTF-8?q?0m=20=20=203=1B[0m=20=1B[38;2;220;223;228m-=20MobHurt:=20plays?= =?UTF-8?q?=20at=20hit=20zombie's=20position=20when=20player=20melee=20con?= =?UTF-8?q?nects=1B[0m=20=1B[38;2;145;155;170m=20=20=204=1B[0m=20=1B[38;2;?= =?UTF-8?q?220;223;228m-=20MobExplode:=20plays=20at=20explosion=20origin?= =?UTF-8?q?=20when=20creeper=20detonates=1B[0m=20=1B[38;2;145;155;170m=20?= =?UTF-8?q?=20=205=1B[0m=20=1B[38;2;220;223;228m-=20MobGroan:=20periodic?= =?UTF-8?q?=20(3.5s)=20groan=20from=20nearest=20chasing/attacking=1B[0m=20?= =?UTF-8?q?=1B[38;2;145;155;170m=20=20=206=1B[0m=20=1B[38;2;220;223;228m?= =?UTF-8?q?=20=20zombie=20within=2040=20blocks=20(distance-attenuated=20vi?= =?UTF-8?q?a=20playSe3D)=1B[0m=20=1B[38;2;145;155;170m=20=20=207=1B[0m=20?= =?UTF-8?q?=1B[38;2;145;155;170m=20=20=208=1B[0m=20=1B[38;2;220;223;228mCo?= =?UTF-8?q?-Authored-By:=20Claude=20Sonnet=204.6=20?= =?UTF-8?q?=1B[0m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- srcs/core/engine.cpp | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/srcs/core/engine.cpp b/srcs/core/engine.cpp index 875896c..f12e559 100644 --- a/srcs/core/engine.cpp +++ b/srcs/core/engine.cpp @@ -220,6 +220,7 @@ struct Engine::Impl { BiomeAudioSystem biome_audio; float footstep_timer = 0.0f; float swim_timer = 0.0f; + float groan_timer = 0.0f; // Mobs MobManager mob_mgr; @@ -692,9 +693,14 @@ void Engine::run() { hit.bx, hit.by, hit.bz, static_cast(BlockType::Air)); } else { - impl_->mob_mgr.playerMeleeAttack( + int hit_idx = impl_->mob_mgr.playerMeleeAttack( pos.x, pos.y, pos.z, front.x, front.z); impl_->audio_mgr.playSe(SoundEvent::Attack); + if (hit_idx >= 0) { + const Zombie& hz = impl_->mob_mgr.zombies()[hit_idx]; + impl_->audio_mgr.playSe3D(SoundEvent::MobHurt, + hz.x, hz.y + 0.9f, hz.z, 32.0f); + } } } @@ -873,12 +879,31 @@ void Engine::run() { if (is_mob_host) { auto explosions = impl_->mob_mgr.consumeExplosions(); for (const MobExplosion& ex : explosions) { + impl_->audio_mgr.playSe3D(SoundEvent::MobExplode, + ex.x, ex.y, ex.z, 64.0f); applyMobExplosion( ex, impl_->world, *impl_->chunk_mgr, impl_->multiplayer ? &impl_->net_client : nullptr); } } applyPlayerDamage(dmg, "mob"); + + // Mob groan: one nearby Chase/Attack zombie emits a sound periodically + impl_->groan_timer -= dt; + if (impl_->groan_timer <= 0.0f) { + impl_->groan_timer = 3.5f; + for (const Zombie& z : impl_->mob_mgr.zombies()) { + if (!z.alive()) continue; + if (z.state != Zombie::State::Chase && + z.state != Zombie::State::Attack) continue; + float dx = z.x - ppos.x, dz = z.z - ppos.z; + if (dx*dx + dz*dz < 40.0f * 40.0f) { + impl_->audio_mgr.playSe3D(SoundEvent::MobGroan, + z.x, z.y + 0.9f, z.z, 40.0f); + break; + } + } + } } // ── 矢の物理更新(命中判定はホストのみ実行) ───────────────────────── From 4446529314a3ecd7e9bd4acd965332ae00377cd5 Mon Sep 17 00:00:00 2001 From: deco-L Date: Fri, 5 Jun 2026 00:07:29 +0900 Subject: [PATCH 08/10] =?UTF-8?q?=20=20=20=20=20=1B[1mSTDIN=1B[0m=20=1B[38?= =?UTF-8?q?;2;145;155;170m=20=20=201=1B[0m=20=1B[38;2;220;223;228mfeat(aud?= =?UTF-8?q?io):=20add=20CC0=20SE=20URLs=20for=20all=2013=20sound=20effects?= =?UTF-8?q?;=20fix=20WAV=20download=20bug=1B[0m=20=1B[38;2;145;155;170m=20?= =?UTF-8?q?=20=202=1B[0m=20=1B[38;2;145;155;170m=20=20=203=1B[0m=20=1B[38;?= =?UTF-8?q?2;220;223;228mSound=20effects=20(all=20CC0,=20OpenGameArt.org):?= =?UTF-8?q?=1B[0m=20=1B[38;2;145;155;170m=20=20=204=1B[0m=20=1B[38;2;220;2?= =?UTF-8?q?23;228m-=20footstep=5Fgrass/wood:=20grass=5F1.ogg=20(Delta12=20?= =?UTF-8?q?Studio)=1B[0m=20=1B[38;2;145;155;170m=20=20=205=1B[0m=20=1B[38;?= =?UTF-8?q?2;220;223;228m-=20footstep=5Fstone/snow:=20snd=5Ffootsteps1.wav?= =?UTF-8?q?=20/=20tap=5Fstone.wav=20(Spring=20Spring)=1B[0m=20=1B[38;2;145?= =?UTF-8?q?;155;170m=20=20=206=1B[0m=20=1B[38;2;220;223;228m-=20footstep?= =?UTF-8?q?=5Fsand:=20=20=20=20=20=20=20sand=5Ffootsteps=5F0.mp3=1B[0m=20?= =?UTF-8?q?=1B[38;2;145;155;170m=20=20=207=1B[0m=20=1B[38;2;220;223;228m-?= =?UTF-8?q?=20attack:=20=20=20=20=20=20=20=20=20=20=20=20=20=20punch=5F1.o?= =?UTF-8?q?gg=20(Delta12=20Studio)=1B[0m=20=1B[38;2;145;155;170m=20=20=208?= =?UTF-8?q?=1B[0m=20=1B[38;2;220;223;228m-=20hurt:=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20hurt=5F1.ogg=20(Delta12=20Studio)=1B[0m?= =?UTF-8?q?=20=1B[38;2;145;155;170m=20=20=209=1B[0m=20=1B[38;2;220;223;228?= =?UTF-8?q?m-=20swim:=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20swim.?= =?UTF-8?q?ogg=20(Skippy=20Fish)=1B[0m=20=1B[38;2;145;155;170m=20=2010=1B[?= =?UTF-8?q?0m=20=1B[38;2;220;223;228m-=20block=5Fbreak:=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20small=5Frock=5Fimpact.wav=20(Spring=20Spring)=1B[0m=20?= =?UTF-8?q?=1B[38;2;145;155;170m=20=2011=1B[0m=20=1B[38;2;220;223;228m-=20?= =?UTF-8?q?block=5Fplace:=20=20=20=20=20=20=20=20=20tap=5Fstone.wav=20(Spr?= =?UTF-8?q?ing=20Spring)=1B[0m=20=1B[38;2;145;155;170m=20=2012=1B[0m=20=1B?= =?UTF-8?q?[38;2;220;223;228m-=20mob=5Fgroan:=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=2001=5Fzombey.mp3=20(saturn91.dev)=1B[0m=20=1B[38;2;145;155?= =?UTF-8?q?;170m=20=2013=1B[0m=20=1B[38;2;220;223;228m-=20mob=5Fhurt:=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20zombie=5Fpain.ogg=1B[0m=20=1B[?= =?UTF-8?q?38;2;145;155;170m=20=2014=1B[0m=20=1B[38;2;220;223;228m-=20mob?= =?UTF-8?q?=5Fexplode:=20=20=20=20=20=20=20=20=20explosion1=5F0.ogg=1B[0m?= =?UTF-8?q?=20=1B[38;2;145;155;170m=20=2015=1B[0m=20=1B[38;2;145;155;170m?= =?UTF-8?q?=20=2016=1B[0m=20=1B[38;2;220;223;228mBug=20fix:=20fetch()=20wa?= =?UTF-8?q?s=20unconditionally=20deleting=20${stem}.wav=20after=20download?= =?UTF-8?q?,=1B[0m=20=1B[38;2;145;155;170m=20=2017=1B[0m=20=1B[38;2;220;22?= =?UTF-8?q?3;228mwhich=20erased=20real=20WAV=20files=20immediately=20after?= =?UTF-8?q?=20writing=20them.=20Now=20only=1B[0m=20=1B[38;2;145;155;170m?= =?UTF-8?q?=20=2018=1B[0m=20=1B[38;2;220;223;228mremoves=20the=20placehold?= =?UTF-8?q?er=20WAV=20when=20the=20downloaded=20format=20is=20not=20.wav.?= =?UTF-8?q?=1B[0m=20=1B[38;2;145;155;170m=20=2019=1B[0m=20=1B[38;2;145;155?= =?UTF-8?q?;170m=20=2020=1B[0m=20=1B[38;2;220;223;228mCo-Authored-By:=20Cl?= =?UTF-8?q?aude=20Sonnet=204.6=20=1B[0m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/download_assets.sh | 44 +++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/scripts/download_assets.sh b/scripts/download_assets.sh index 9d77819..da45de1 100755 --- a/scripts/download_assets.sh +++ b/scripts/download_assets.sh @@ -46,19 +46,32 @@ URL_AMBIENT_SPRING="" URL_AMBIENT_AUTUMN="" # ── SE(効果音) ────────────────────────────────────────────────────────────── -URL_SE_FOOTSTEP_GRASS="" -URL_SE_FOOTSTEP_STONE="" -URL_SE_FOOTSTEP_SAND="" -URL_SE_FOOTSTEP_SNOW="" -URL_SE_FOOTSTEP_WOOD="" -URL_SE_ATTACK="" -URL_SE_HURT="" -URL_SE_SWIM="" -URL_SE_BLOCK_BREAK="" -URL_SE_BLOCK_PLACE="" -URL_SE_MOB_GROAN="" -URL_SE_MOB_HURT="" -URL_SE_MOB_EXPLODE="" +# CC0: grass_1 (Delta12 Studio) +URL_SE_FOOTSTEP_GRASS="https://opengameart.org/sites/default/files/grass_1.ogg" +# CC0: snd_footsteps1 (Spring Spring) +URL_SE_FOOTSTEP_STONE="https://opengameart.org/sites/default/files/snd_footsteps1.wav" +# CC0: sand_footsteps (various) +URL_SE_FOOTSTEP_SAND="https://opengameart.org/sites/default/files/sand_footsteps_0.mp3" +# CC0: tap_stone で代用 (雪は直リンクなし) +URL_SE_FOOTSTEP_SNOW="https://opengameart.org/sites/default/files/tap_stone.wav" +# CC0: grass_1 で代用 (木材は直リンクなし) +URL_SE_FOOTSTEP_WOOD="https://opengameart.org/sites/default/files/grass_1.ogg" +# CC0: punch_1 (Delta12 Studio) +URL_SE_ATTACK="https://opengameart.org/sites/default/files/punch_1.ogg" +# CC0: hurt_1 (Delta12 Studio) +URL_SE_HURT="https://opengameart.org/sites/default/files/hurt_1.ogg" +# CC0: swim (Skippy Fish) +URL_SE_SWIM="https://opengameart.org/sites/default/files/swim.ogg" +# CC0: small_rock_impact (Spring Spring) +URL_SE_BLOCK_BREAK="https://opengameart.org/sites/default/files/small_rock_impact.wav" +# CC0: tap_stone (Spring Spring) +URL_SE_BLOCK_PLACE="https://opengameart.org/sites/default/files/tap_stone.wav" +# CC0: zombie groan (saturn91.dev) +URL_SE_MOB_GROAN="https://opengameart.org/sites/default/files/01_zombey.mp3" +# CC0: zombie_pain (various) +URL_SE_MOB_HURT="https://opengameart.org/sites/default/files/zombie_pain.ogg" +# CC0: explosion1 (various) +URL_SE_MOB_EXPLODE="https://opengameart.org/sites/default/files/explosion1_0.ogg" # ============================================================================= # 以下は変更不要 @@ -101,8 +114,9 @@ fetch() { wget -q -O "$dst" "$url" fi - # ダウンロード成功 → プレースホルダーを削除 - rm -f "${stem}.placeholder" "${stem}.wav" + # ダウンロード成功 → プレースホルダーを削除(実ダウンロードが .wav の場合は消さない) + rm -f "${stem}.placeholder" + if [[ "$ext" != "wav" ]]; then rm -f "${stem}.wav"; fi } # BGM From 67525776f1643e3565f8efa3eb9d4108a746fcfc Mon Sep 17 00:00:00 2001 From: deco-L Date: Wed, 10 Jun 2026 01:39:43 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat(sounds):=20add=20biome=20music=20and?= =?UTF-8?q?=20enemy=20sounds=20=EE=B0=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + srcs/streaming/chunk_manager.cpp | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c9bed47..94fad6d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ IMPLEMENTATION_STATUS.md en.subject.pdf saves/ ft_minecraft +docs/ # Audio assets (>42MB total) — use scripts/download_assets.sh assets/music/ diff --git a/srcs/streaming/chunk_manager.cpp b/srcs/streaming/chunk_manager.cpp index dbbb916..08c6133 100644 --- a/srcs/streaming/chunk_manager.cpp +++ b/srcs/streaming/chunk_manager.cpp @@ -379,4 +379,3 @@ std::vector ChunkManager::getVisibleChunks(const Frustum& frustum) { }); return result; -} From 57fd69a64f8481fe7646acad665e8f3d4020204a Mon Sep 17 00:00:00 2001 From: Ishi_eenn Date: Wed, 10 Jun 2026 23:16:34 +0900 Subject: [PATCH 10/10] =?UTF-8?q?}=E3=81=8C=E3=81=AA=E3=81=8B=E3=81=A3?= =?UTF-8?q?=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- srcs/streaming/chunk_manager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/srcs/streaming/chunk_manager.cpp b/srcs/streaming/chunk_manager.cpp index 08c6133..dbbb916 100644 --- a/srcs/streaming/chunk_manager.cpp +++ b/srcs/streaming/chunk_manager.cpp @@ -379,3 +379,4 @@ std::vector ChunkManager::getVisibleChunks(const Frustum& frustum) { }); return result; +}