diff --git a/CMakeLists.txt b/CMakeLists.txt index d6fd173..7b0ae4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,27 +35,8 @@ if(FT_MINECRAFT_FETCH_DEPS) GIT_SHALLOW TRUE ) - FetchContent_Declare( - fastnoiselite - GIT_REPOSITORY https://github.com/Auburn/FastNoiseLite.git - GIT_TAG v1.1.1 - GIT_SHALLOW TRUE - ) - FetchContent_MakeAvailable(glfw glm) - FetchContent_GetProperties(fastnoiselite) - if(NOT fastnoiselite_POPULATED) - FetchContent_Populate(fastnoiselite) - endif() - - add_library(ft_minecraft_fastnoise INTERFACE) - target_include_directories( - ft_minecraft_fastnoise - INTERFACE - ${fastnoiselite_SOURCE_DIR}/Cpp - ) - # ── miniaudio (single-header audio engine) ───────────────────────────────── FetchContent_Declare( miniaudio @@ -139,7 +120,6 @@ target_link_libraries( OpenGL::GL glfw glm::glm - ft_minecraft_fastnoise Threads::Threads ${CMAKE_DL_LIBS} # miniaudio uses dlopen on Linux for audio backends m # math library (miniaudio, stb_vorbis) diff --git a/srcs/audio/audio_manager.cpp b/srcs/audio/audio_manager.cpp index da3421a..745598a 100644 --- a/srcs/audio/audio_manager.cpp +++ b/srcs/audio/audio_manager.cpp @@ -196,9 +196,12 @@ void AudioManager::update(float dt) { } } - // Reclaim SE pool slots that have finished playing + // Reclaim SE pool slots that have finished playing. + // 停止予約 (ma_sound_set_stop_time_*) で止まった音は at_end にならないため + // is_playing もあわせて確認する (start 直後の音は必ず is_playing == true)。 for (auto& se : d.se_pool) - if (se.initialized && ma_sound_at_end(&se.sound)) { + if (se.initialized && + (ma_sound_at_end(&se.sound) || !ma_sound_is_playing(&se.sound))) { ma_sound_uninit(&se.sound); se.initialized = false; } @@ -366,7 +369,21 @@ void AudioManager::stopAmbient(float fade_out) { // ── SE ──────────────────────────────────────────────────────────────────────── -void AudioManager::playSe(SoundEvent event) { +// 再生長制限: max_duration_sec かけて 0 へフェードしつつ、終端で停止を予約する。 +// ハードカットだとクリックノイズが出るため、全区間フェードで自然に減衰させる。 +static void applyDurationLimit(ma_engine* engine, ma_sound* sound, + float volume, float max_duration_sec) { + if (max_duration_sec <= 0.0f) return; + const ma_uint64 sr = ma_engine_get_sample_rate(engine); + const ma_uint64 now = ma_engine_get_time_in_pcm_frames(engine); + const ma_uint64 dur = (ma_uint64)((double)max_duration_sec * (double)sr); + ma_sound_set_fade_in_milliseconds( + sound, volume, 0.0f, (ma_uint64)(max_duration_sec * 1000.0f)); + ma_sound_set_stop_time_in_pcm_frames(sound, now + dur); +} + +void AudioManager::playSe(SoundEvent event, float volume, + float max_duration_sec) { Impl& d = *impl_; if (!d.engine_ok) return; @@ -374,12 +391,35 @@ void AudioManager::playSe(SoundEvent event) { if (idx < 0 || idx >= static_cast(SoundEvent::COUNT)) return; std::string full = resolveAudioPath(d.assets_root + "/" + kSeStems[idx]); - if (ma_engine_play_sound(&d.engine, full.c_str(), &d.se_group) != MA_SUCCESS) + + // 音量・再生長の指定がなければ従来どおり fire-and-forget + if (volume >= 0.999f && max_duration_sec <= 0.0f) { + 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()); + return; + } + + // 音量/再生長を制御するため SE プールの ma_sound を使う (非3D) + SeSlot* slot = nullptr; + for (auto& s : d.se_pool) if (!s.initialized) { slot = &s; break; } + if (!slot) return; // pool full; drop the sound + + if (ma_sound_init_from_file(&d.engine, full.c_str(), 0, + &d.se_group, nullptr, + &slot->sound) != MA_SUCCESS) { fprintf(stderr, "[Audio] playSe failed: %s\n", full.c_str()); + return; + } + slot->initialized = true; + ma_sound_set_spatialization_enabled(&slot->sound, MA_FALSE); + ma_sound_set_volume(&slot->sound, volume); + ma_sound_set_looping(&slot->sound, MA_FALSE); + applyDurationLimit(&d.engine, &slot->sound, volume, max_duration_sec); + ma_sound_start(&slot->sound); } void AudioManager::playSe3D(SoundEvent event, float x, float y, float z, - float max_distance) { + float max_distance, float max_duration_sec) { Impl& d = *impl_; if (!d.engine_ok) return; @@ -410,6 +450,7 @@ void AudioManager::playSe3D(SoundEvent event, float x, float y, float z, 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); + applyDurationLimit(&d.engine, &slot->sound, 1.0f, max_duration_sec); ma_sound_start(&slot->sound); } diff --git a/srcs/audio/audio_manager.hpp b/srcs/audio/audio_manager.hpp index 3277e21..c473ea0 100644 --- a/srcs/audio/audio_manager.hpp +++ b/srcs/audio/audio_manager.hpp @@ -75,13 +75,19 @@ class AudioManager { void stopAmbient(float fade_out = 2.0f); // ── SE: 2D one-shot ────────────────────────────────────────────────────── - void playSe(SoundEvent event); + // volume: この再生のみのゲイン [0,1]。 + // max_duration_sec: > 0 なら再生をこの秒数で打ち切る (フェードアウト付き)。 + // 足音素材のように 1 ファイルに複数歩入っている長尺サンプルを + // 「1 歩分」だけ鳴らすために使う。0 なら全長再生。 + void playSe(SoundEvent event, float volume = 1.0f, + float max_duration_sec = 0.0f); // ── SE: 3D positional one-shot ─────────────────────────────────────────── // Skipped automatically when the source is farther than max_distance. // Uses inverse-distance attenuation (OpenAL-style). + // max_duration_sec: playSe と同じ再生長制限。 void playSe3D(SoundEvent event, float x, float y, float z, - float max_distance = 32.0f); + float max_distance = 32.0f, float max_duration_sec = 0.0f); // ── Internal access for SoundEmitter ──────────────────────────────────── // Returns the raw ma_engine* as void*. Used only by SoundEmitter. diff --git a/srcs/core/engine.cpp b/srcs/core/engine.cpp index 4ab24bb..1acc14a 100644 --- a/srcs/core/engine.cpp +++ b/srcs/core/engine.cpp @@ -190,6 +190,25 @@ static void applyMobExplosion(const MobExplosion& ex, World& world, chunk_mgr.rebuildChunkAt(pos); } +// 足元の地面ブロックの素材から足音イベントを選ぶ +static SoundEvent footstepForBlock(BlockType below) { + switch (below) { + case BlockType::Stone: + case BlockType::GoldOre: + case BlockType::DiamondOre: + return SoundEvent::FootstepStone; + case BlockType::Sand: + case BlockType::Cactus: + return SoundEvent::FootstepSand; + case BlockType::Snow: + return SoundEvent::FootstepSnow; + case BlockType::Wood: + return SoundEvent::FootstepWood; + default: + return SoundEvent::FootstepGrass; + } +} + // ───────────────────────────────────────────────────────────────────────────── // Engine の内部実装データ(pImpl パターン) // ───────────────────────────────────────────────────────────────────────────── @@ -220,9 +239,14 @@ struct Engine::Impl { // Audio AudioManager audio_mgr; BiomeAudioSystem biome_audio; - float footstep_timer = 0.0f; - float swim_timer = 0.0f; float groan_timer = 0.0f; + float mob_step_timer = 0.0f; // モブ足音の再生インターバル + + // ローカル足音は時間でなく「実際に歩いた距離」で刻む。 + // (壁に向かって歩いても鳴らない / 歩速に自然に追従する) + float step_dist_accum = 0.0f; // 前回の足音からの累計水平移動距離 + float step_prev_x = 0.0f, step_prev_z = 0.0f; + bool step_prev_valid = false; // Mobs MobManager mob_mgr; @@ -429,11 +453,13 @@ void Engine::run() { // これを使うことでFPSが変わっても移動速度などが一定になる。 // 例: 60fps なら dt≈0.016秒, 30fps なら dt≈0.033秒 auto now = Clock::now(); - float dt = std::chrono::duration(now - prev).count(); + const float real_dt = std::chrono::duration(now - prev).count(); prev = now; - // dtを上限0.1秒でクランプ。カクついた瞬間に物理が暴走するのを防ぐ。 - if (dt > 0.1f) dt = 0.1f; - fps_timer += dt; + // 物理用 dt は上限0.1秒でクランプ。カクついた瞬間に物理が暴走するのを防ぐ。 + // FPS 集計はクランプ前の実測時間を使う(クランプ後だと長いフレームが + // 0.1秒扱いになり、まさに FPS が落ちた瞬間に実際より高く表示されてしまう)。 + float dt = (real_dt > 0.1f) ? 0.1f : real_dt; + fps_timer += real_dt; elapsed_s += dt; ++fps_frames; // 0.25秒ごとにFPSを更新(毎フレーム更新すると数字が激しく変わって読めない) @@ -478,35 +504,46 @@ void Engine::run() { 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(); + // 実際に移動した水平距離を積算し、1歩分 (kStepLength) ごとに + // 1回だけ鳴らす。素材は複数歩入りの長尺があるため再生長も + // 1歩分に制限する (playSe の max_duration)。 + { + constexpr float kStepLength = 0.85f; // 1歩 = 0.85 ブロック + constexpr float kStrokeLength = 1.40f; // 水泳ストローク間隔 + constexpr float kStepVolume = 0.35f; + constexpr float kStepDuration = 0.35f; // 再生長 (秒) + + glm::vec3 p = impl_->player.camera().position(); + if (!impl_->step_prev_valid) { + impl_->step_prev_x = p.x; + impl_->step_prev_z = p.z; + impl_->step_prev_valid = true; + } + const float ddx = p.x - impl_->step_prev_x; + const float ddz = p.z - impl_->step_prev_z; + impl_->step_prev_x = p.x; + impl_->step_prev_z = p.z; + + const bool in_water = impl_->player.isInWater(); + // 接地中 (or 水中) の移動だけを「歩き」として数える + if (moving && (in_water || impl_->player.isOnGround())) + impl_->step_dist_accum += std::sqrt(ddx*ddx + ddz*ddz); + + if (in_water) { + if (impl_->step_dist_accum >= kStrokeLength) { + impl_->step_dist_accum = 0.0f; + impl_->audio_mgr.playSe(SoundEvent::Swim, 0.45f, 0.9f); + } + } else if (impl_->step_dist_accum >= kStepLength) { + impl_->step_dist_accum = 0.0f; + // カメラY = 足元Y + 1.62 なので、-2.1 で足元の下 + // (地面ブロック)を参照する BlockType below = impl_->world.getWorldBlock( (int)std::floor(p.x), - (int)std::floor(p.y - 1.2f), + (int)std::floor(p.y - 2.1f), (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; + impl_->audio_mgr.playSe(footstepForBlock(below), + kStepVolume, kStepDuration); } } } @@ -845,6 +882,10 @@ void Engine::run() { } else if (ev.kind == NetworkEvent::Kind::MobUpdate) { if (!is_mob_host) impl_->mob_mgr.setZombies(std::move(ev.mobs)); + } else if (ev.kind == NetworkEvent::Kind::PlayerDamage) { + // ホストが計算した自分宛のモブダメージ(サーバーが対象限定で転送) + if (ev.player_id == impl_->net_client.playerId()) + applyPlayerDamage(ev.damage, "mob(net)"); } else if (ev.kind == NetworkEvent::Kind::DragonSpawn) { // 誰かが召喚した。自分のローカルにまだ無ければ生成する。 if (!impl_->dragon_mgr.exists()) { @@ -867,6 +908,14 @@ void Engine::run() { st.health = ev.dragon_health; impl_->dragon_mgr.applyNetState(st); } + } else if (ev.kind == NetworkEvent::Kind::DragonFireball) { + // ホストのドラゴンが発射した。非ホストはローカルに弾を生成し + // 以降は決定的シミュレーションで追従する。 + if (!is_mob_host) { + impl_->dragon_mgr.spawnFireballNet( + ev.fb_x, ev.fb_y, ev.fb_z, + ev.fb_vx, ev.fb_vy, ev.fb_vz); + } } } // Update walking animation phase for each remote player. @@ -885,6 +934,35 @@ void Engine::run() { rp.attack_timer -= dt; rp.prev_x = rp.x; rp.prev_z = rp.z; + + // ── リモートプレイヤーの 3D 効果音 ────────────────────────── + // 歩行音(水中なら水泳音)。rp.y は目線高さ。 + rp.footstep_timer -= dt; + if (rp.footstep_timer <= 0.0f && speed > 0.3f && + !(rp.state_flags & 0x04u)) { + const int fx = (int)std::floor(rp.x); + const int fz = (int)std::floor(rp.z); + const bool in_water = impl_->world.getWorldBlock( + fx, (int)std::floor(rp.y - 1.0f), fz) + == BlockType::Water; + if (in_water) { + impl_->audio_mgr.playSe3D(SoundEvent::Swim, + rp.x, rp.y, rp.z, 24.0f, 0.9f); + rp.footstep_timer = 0.70f; + } else { + BlockType below = impl_->world.getWorldBlock( + fx, (int)std::floor(rp.y - 2.1f), fz); + impl_->audio_mgr.playSe3D(footstepForBlock(below), + rp.x, rp.y - 1.6f, rp.z, 24.0f, 0.35f); + rp.footstep_timer = 0.55f; + } + } + // 攻撃音(state_flags の attack ビット立ち上がりで1回) + if (rp.attack_sound_pending) { + rp.attack_sound_pending = false; + impl_->audio_mgr.playSe3D(SoundEvent::Attack, + rp.x, rp.y, rp.z, 24.0f); + } } } @@ -894,13 +972,38 @@ void Engine::run() { { float dmg = 0.0f; if (is_mob_host) { - // ホスト: mob AI を動かし、マルチプレイ中は状態を送信する - dmg = impl_->mob_mgr.update( + // ホスト: mob AI を動かし、マルチプレイ中は状態を送信する。 + // モブは「最寄りのプレイヤー」を追うため、ローカルだけでなく + // 全リモートプレイヤーもターゲットに含める(死亡中は除外)。 + std::vector targets; + targets.push_back({impl_->net_client.playerId(), + ppos.x, ppos.y, ppos.z}); + if (impl_->multiplayer) { + for (const auto& [id, rp] : + impl_->net_client.remotePlayers()) { + if (rp.state_flags & 0x04u) continue; // dead + if (rp.health <= 0.0f) continue; + targets.push_back({id, rp.x, rp.y, rp.z}); + } + } + std::vector target_dmg; + impl_->mob_mgr.update( dt, - ppos.x, ppos.y, ppos.z, + targets, impl_->time_of_day, isSolid, - impl_->world); + impl_->world, + target_dmg); + // targets[0] = ローカルプレイヤー。残りはネット経由で本人へ届ける。 + dmg = target_dmg.empty() ? 0.0f : target_dmg[0]; + if (impl_->multiplayer) { + for (size_t i = 1; i < targets.size(); ++i) { + if (target_dmg[i] <= 0.0f) continue; + PktPlayerDamage pd{targets[i].id, target_dmg[i]}; + impl_->net_client.sendRaw(PacketType::PlayerDamage, + &pd, sizeof(pd)); + } + } if (impl_->multiplayer) { impl_->mob_sync_timer -= dt; if (impl_->mob_sync_timer <= 0.0f) { @@ -954,6 +1057,40 @@ void Engine::run() { } } } + + // ── モブ足音 ───────────────────────────────────────────────────── + // zombies_ はホスト計算・全クライアント同期済みなので + // どのクライアントでも同じ位置から 3D 足音が鳴る。 + // 近くの移動中モブ最大4体まで(重なりすぎ防止)。 + impl_->mob_step_timer -= dt; + if (impl_->mob_step_timer <= 0.0f) { + impl_->mob_step_timer = 0.45f; + int played = 0; + for (const Zombie& z : impl_->mob_mgr.zombies()) { + if (!z.alive()) continue; + // Chase/Fuse = 移動中(Idle はゆっくり徘徊だが音は省略) + if (z.state != Zombie::State::Chase && + z.state != Zombie::State::Fuse) continue; + const float dx = z.x - ppos.x, dz = z.z - ppos.z; + if (dx*dx + dz*dz > 28.0f * 28.0f) continue; + const int fx = (int)std::floor(z.x); + const int fz = (int)std::floor(z.z); + // z.y は足元座標。水中なら水泳音、地上なら素材別の足音。 + const bool in_water = impl_->world.getWorldBlock( + fx, (int)std::floor(z.y + 0.5f), fz) + == BlockType::Water; + if (in_water) { + impl_->audio_mgr.playSe3D(SoundEvent::Swim, + z.x, z.y + 0.5f, z.z, 24.0f, 0.9f); + } else { + BlockType below = impl_->world.getWorldBlock( + fx, (int)std::floor(z.y - 0.5f), fz); + impl_->audio_mgr.playSe3D(footstepForBlock(below), + z.x, z.y, z.z, 24.0f, 0.35f); + } + if (++played >= 4) break; + } + } } // ── 矢の物理更新(命中判定はホストのみ実行) ───────────────────────── @@ -973,6 +1110,20 @@ void Engine::run() { if (dragon_dmg > 0.0f) applyPlayerDamage(dragon_dmg, "dragon"); + // AI が今フレーム発射したファイアボールをブロードキャスト + { + DragonFireball fb; + while (impl_->dragon_mgr.pollFireballSpawn(fb)) { + if (impl_->multiplayer) { + PktDragonFireball pkt; + pkt.x = fb.x; pkt.y = fb.y; pkt.z = fb.z; + pkt.vx = fb.vx; pkt.vy = fb.vy; pkt.vz = fb.vz; + impl_->net_client.sendRaw(PacketType::DragonFireball, + &pkt, sizeof(pkt)); + } + } + } + if (impl_->multiplayer) { impl_->dragon_sync_timer -= dt; if (impl_->dragon_sync_timer <= 0.0f) { @@ -996,6 +1147,21 @@ void Engine::run() { // 非ホストはローカル AI を回さない (受信ステートで上書き)。 // Phase C MVP では 10Hz の snap、補間は今後検討。 + // ── ファイアボール / ブレス雲の更新 (ホスト・非ホスト共通) ────────── + // 飛翔は決定的なので各クライアントがローカルにシミュレートし、 + // 雲のダメージは「自分のプレイヤー」の分だけ各自で適用する。 + { + const float breath_dmg = impl_->dragon_mgr.updateProjectiles( + dt, isSolid, ppos.x, ppos.y, ppos.z); + if (breath_dmg > 0.0f) + applyPlayerDamage(breath_dmg, "dragon breath"); + + float bx, by, bz; + while (impl_->dragon_mgr.pollExplosion(bx, by, bz)) + impl_->audio_mgr.playSe3D(SoundEvent::MobExplode, + bx, by, bz, 48.0f); + } + // ── チャンクのストリーミング ───────────────────────────────────────── // プレイヤーの周囲のチャンクを読み込み、遠いチャンクを破棄する。 // 毎フレーム数チャンクずつ処理し、ゲームが止まらないようにする。 @@ -1064,12 +1230,19 @@ void Engine::run() { // ── 描画 ───────────────────────────────────────────────────────────── // パス0: シャドウマップ生成 (太陽視点で深度のみ描画) - // カメラ外の地形(洞窟の天井など)も影を落とすため、全ロード済みチャンクを使う + // カメラ外の地形(洞窟の天井など)も影を落とすため、カメラの視錐台ではなく + // 「光源のオーソボックス」でカリングする。シャドウマップの範囲 + // (プレイヤー中心 ±200 ブロック) 外のチャンクは深度マップに寄与しないので + // 描画をスキップして負荷を下げる(全ロード時 ~1300 チャンク → 数百)。 impl_->renderer.updateShadowMatrix(ppos.x, ppos.y, ppos.z); + Frustum shadow_frustum; + shadow_frustum.extractFromVP( + glm::make_mat4(impl_->renderer.lightSpaceMatrix())); impl_->renderer.beginShadowPass(); { - auto all_chunks = impl_->chunk_mgr->getAllLoadedChunks(); - for (Chunk* c : all_chunks) + auto shadow_chunks = + impl_->chunk_mgr->getVisibleChunks(shadow_frustum); + for (Chunk* c : shadow_chunks) impl_->renderer.drawChunkShadow(c); } impl_->renderer.endShadowPass(); @@ -1123,6 +1296,11 @@ void Engine::run() { view4x4, proj4x4); } + // ファイアボール・ブレス雲(ドラゴン死亡後も雲は残るため独立に描画) + impl_->renderer.drawDragonEffects(impl_->dragon_mgr.fireballs(), + impl_->dragon_mgr.clouds(), + view4x4, proj4x4); + // 3D 雲レイヤーを描画(不透明ブロックの後、水の前) impl_->renderer.drawClouds(view4x4, proj4x4, ppos.x, ppos.z, elapsed_s); diff --git a/srcs/mob/dragon_manager.cpp b/srcs/mob/dragon_manager.cpp index 5597833..2cdca13 100644 --- a/srcs/mob/dragon_manager.cpp +++ b/srcs/mob/dragon_manager.cpp @@ -1,11 +1,15 @@ // ───────────────────────────────────────────────────────────────────────────── -// dragon_manager.cpp — Phase A (旋回飛行) + Phase B (戦闘ロジック) +// dragon_manager.cpp — 飛行 AI (旋回/チャージ/復帰) + 戦闘 + ファイアボール // ───────────────────────────────────────────────────────────────────────────── #include "mob/dragon_manager.hpp" #include #include #include +namespace { +constexpr float kPi = 3.1415926535f; +} + void DragonManager::spawn(float wx, float wy, float wz) { if (dragon_.has_value()) return; @@ -36,7 +40,9 @@ float DragonManager::update(float dt, float px, float py, float pz) { d.life_timer += dt; d.wing_phase += kWingFlapSpeed * dt; - if (d.hit_flash_timer > 0.0f) d.hit_flash_timer -= dt; + if (d.hit_flash_timer > 0.0f) d.hit_flash_timer -= dt; + if (d.charge_cooldown > 0.0f) d.charge_cooldown -= dt; + if (d.fireball_cooldown > 0.0f) d.fireball_cooldown -= dt; // ── 死亡演出: ゆっくり上昇 → DRAGON_DYING_DURATION 後に消える ──────────── if (d.state == EnderDragon::State::Dying) { @@ -49,7 +55,61 @@ float DragonManager::update(float dt, float px, float py, float pz) { return 0.0f; // 死んでいる間は接触ダメージなし } - // ── 旋回飛行 ──────────────────────────────────────────────────────────── + // 速度を目標方向へ滑らかに寄せるステアリング (Charge / Retreat 共用) + auto steerTowards = [&](float tx, float ty, float tz, float speed) { + const float ex = tx - d.x, ey = ty - d.y, ez = tz - d.z; + const float len = std::sqrt(ex*ex + ey*ey + ez*ez); + if (len > 1e-4f) { + const float k = std::min(1.0f, DRAGON_CHARGE_TURN * dt); + d.vx += (ex / len * speed - d.vx) * k; + d.vy += (ey / len * speed - d.vy) * k; + d.vz += (ez / len * speed - d.vz) * k; + } + d.x += d.vx * dt; + d.y += d.vy * dt; + d.z += d.vz * dt; + d.yaw = std::atan2(d.vz, d.vx) * (180.0f / kPi); + const float horiz = std::sqrt(d.vx * d.vx + d.vz * d.vz); + d.pitch = std::atan2(d.vy, horiz) * (180.0f / kPi); + return len; + }; + + // ── チャージ突進: プレイヤーへ急降下し、体当たりの一撃を狙う ────────── + if (d.state == EnderDragon::State::Charge) { + d.charge_timer += dt; + const float dist = steerTowards(px, py, pz, DRAGON_CHARGE_SPEED); + + if (distanceToBody(px, py, pz) < DRAGON_CONTACT_RADIUS) { + // 命中: 一撃ダメージを与えて離脱 + d.state = EnderDragon::State::Retreat; + return DRAGON_CHARGE_DAMAGE; + } + if (dist < 2.0f || d.charge_timer > DRAGON_CHARGE_TIMEOUT) { + d.state = EnderDragon::State::Retreat; // すれ違った / 諦め + } + return 0.0f; + } + + // ── 復帰: 旋回円上の最寄り点へ戻り、Patrol を再開する ───────────────── + if (d.state == EnderDragon::State::Retreat) { + const float ang = std::atan2(d.z - d.spawn_z, d.x - d.spawn_x); + const float tx = d.spawn_x + std::cos(ang) * kOrbitRadius; + const float tz = d.spawn_z + std::sin(ang) * kOrbitRadius; + const float dist = steerTowards(tx, d.spawn_y, tz, DRAGON_RETREAT_SPEED); + + if (dist < 2.0f) { + // 現在角度から旋回を再開 (位置が連続するように位相を合わせる) + d.orbit_phase = ang; + d.state = EnderDragon::State::Patrol; + d.charge_cooldown = DRAGON_CHARGE_COOLDOWN; + } + // 復帰中も接触していればダメージ + if (distanceToBody(px, py, pz) < DRAGON_CONTACT_RADIUS) + return DRAGON_CONTACT_DAMAGE_PER_SEC * dt; + return 0.0f; + } + + // ── 旋回飛行 (Patrol) ─────────────────────────────────────────────────── d.orbit_phase += kOrbitSpeed * dt; if (d.orbit_phase > 6.2831853f) d.orbit_phase -= 6.2831853f; @@ -65,9 +125,12 @@ float DragonManager::update(float dt, float px, float py, float pz) { d.y = new_y; d.z = new_z; - d.yaw = std::atan2(d.vz, d.vx) * (180.0f / 3.1415926535f); + d.yaw = std::atan2(d.vz, d.vx) * (180.0f / kPi); const float horiz = std::sqrt(d.vx * d.vx + d.vz * d.vz); - d.pitch = std::atan2(d.vy, horiz) * (180.0f / 3.1415926535f); + d.pitch = std::atan2(d.vy, horiz) * (180.0f / kPi); + + // 攻撃判断 (チャージ開始 / ファイアボール発射) + tryAttacks(d, px, py, pz); // ── 接触ダメージ判定 ─────────────────────────────────────────────────── // 胴体軸 (頭〜尾) に沿った最近接点までの距離で判定する。 @@ -77,6 +140,55 @@ float DragonManager::update(float dt, float px, float py, float pz) { return 0.0f; } +// ───────────────────────────────────────────────────────────────────────────── +// tryAttacks() — Patrol 中の攻撃判断 (ホストのみ) +// ・チャージ: クールダウン明けに射程内のプレイヤーへ突進開始 +// ・ファイアボール: 口の位置からプレイヤーを狙って発射。 +// 頭がプレイヤー側を向いているときのみ (背後には撃てない)。 +// ───────────────────────────────────────────────────────────────────────────── +void DragonManager::tryAttacks(EnderDragon& d, float px, float py, float pz) { + const float ex = px - d.x, ey = py - d.y, ez = pz - d.z; + const float dist = std::sqrt(ex*ex + ey*ey + ez*ez); + + if (d.charge_cooldown <= 0.0f && dist < DRAGON_CHARGE_RANGE) { + d.state = EnderDragon::State::Charge; + d.charge_timer = 0.0f; + return; + } + + if (d.fireball_cooldown <= 0.0f && + dist > DRAGON_FIREBALL_MIN_DIST && dist < DRAGON_FIREBALL_MAX_DIST) { + // 頭の向きチェック: 水平前方とプレイヤー方向の内積 + const float yaw_rad = d.yaw * (kPi / 180.0f); + const float fx = std::cos(yaw_rad); + const float fz = std::sin(yaw_rad); + const float horiz = std::sqrt(ex*ex + ez*ez); + const float facing = (horiz > 1e-4f) + ? (ex * fx + ez * fz) / horiz : 1.0f; + if (facing < 0.2f) { + d.fireball_cooldown = 0.6f; // 向きが合うまで小刻みにリトライ + return; + } + + // 口の位置 (頭の先端付近) から発射 + DragonFireball fb; + fb.x = d.x + fx * 8.5f; + fb.y = d.y + 1.8f; + fb.z = d.z + fz * 8.5f; + const float ax = px - fb.x, ay = py - fb.y, az = pz - fb.z; + const float alen = std::sqrt(ax*ax + ay*ay + az*az); + if (alen > 1e-4f) { + fb.vx = ax / alen * DRAGON_FIREBALL_SPEED; + fb.vy = ay / alen * DRAGON_FIREBALL_SPEED; + fb.vz = az / alen * DRAGON_FIREBALL_SPEED; + fb.life = DRAGON_FIREBALL_LIFE; + fireballs_.push_back(fb); + pending_spawns_.push_back(fb); // engine がネット配信する + } + d.fireball_cooldown = DRAGON_FIREBALL_COOLDOWN; + } +} + float DragonManager::distanceToBody(float px, float py, float pz) const { if (!dragon_.has_value()) return std::numeric_limits::max(); const EnderDragon& d = *dragon_; @@ -196,3 +308,84 @@ bool DragonManager::playerMeleeAttack(float px, float py, float pz, applyDamage(DRAGON_MELEE_DAMAGE); return true; } + +// ───────────────────────────────────────────────────────────────────────────── +// updateProjectiles() — ファイアボールの飛翔・着弾とブレス雲の維持・ダメージ +// +// 全クライアントで毎フレーム実行する。飛翔は直線等速 + 着弾判定のみで +// 乱数を使わないため、同一ワールドなら全クライアントで決定的に一致する。 +// ダメージは「自分のプレイヤー」の分だけ各クライアントがローカルに計算する。 +// ───────────────────────────────────────────────────────────────────────────── +float DragonManager::updateProjectiles( + float dt, const std::function& isSolid, + float px, float py, float pz) { + // ── 弾の飛翔と着弾 ────────────────────────────────────────────────────── + for (size_t i = 0; i < fireballs_.size(); ) { + DragonFireball& fb = fireballs_[i]; + fb.x += fb.vx * dt; + fb.y += fb.vy * dt; + fb.z += fb.vz * dt; + fb.life -= dt; + + const bool hit = isSolid((int)std::floor(fb.x), + (int)std::floor(fb.y), + (int)std::floor(fb.z)); + if (hit || fb.life <= 0.0f) { + DragonBreathCloud cl; + cl.x = fb.x; + cl.y = fb.y + 0.5f; // 地面に埋まらないよう少し持ち上げる + cl.z = fb.z; + cl.timer = DRAGON_CLOUD_DURATION; + clouds_.push_back(cl); + pending_booms_.push_back(cl); // engine が効果音を鳴らす + fireballs_.erase(fireballs_.begin() + (long)i); + continue; + } + ++i; + } + + // ── 雲の維持とダメージ ────────────────────────────────────────────────── + float dmg = 0.0f; + for (size_t i = 0; i < clouds_.size(); ) { + DragonBreathCloud& cl = clouds_[i]; + cl.timer -= dt; + if (cl.timer <= 0.0f) { + clouds_.erase(clouds_.begin() + (long)i); + continue; + } + const float dx = px - cl.x; + const float dy = py - cl.y; + const float dz = pz - cl.z; + if (dx * dx + dz * dz < DRAGON_CLOUD_RADIUS * DRAGON_CLOUD_RADIUS && + std::fabs(dy) < DRAGON_CLOUD_HEIGHT) { + dmg += DRAGON_CLOUD_DPS * dt; + } + ++i; + } + return dmg; +} + +void DragonManager::spawnFireballNet(float x, float y, float z, + float vx, float vy, float vz) { + DragonFireball fb; + fb.x = x; fb.y = y; fb.z = z; + fb.vx = vx; fb.vy = vy; fb.vz = vz; + fb.life = DRAGON_FIREBALL_LIFE; + fireballs_.push_back(fb); +} + +bool DragonManager::pollFireballSpawn(DragonFireball& out) { + if (pending_spawns_.empty()) return false; + out = pending_spawns_.back(); + pending_spawns_.pop_back(); + return true; +} + +bool DragonManager::pollExplosion(float& x, float& y, float& z) { + if (pending_booms_.empty()) return false; + x = pending_booms_.back().x; + y = pending_booms_.back().y; + z = pending_booms_.back().z; + pending_booms_.pop_back(); + return true; +} diff --git a/srcs/mob/dragon_manager.hpp b/srcs/mob/dragon_manager.hpp index ce674b9..4227528 100644 --- a/srcs/mob/dragon_manager.hpp +++ b/srcs/mob/dragon_manager.hpp @@ -1,15 +1,19 @@ #pragma once #include "mob/ender_dragon.hpp" +#include +#include // エンダードラゴンを最大 1 体だけ所有・管理するクラス。 // -// Phase A の責務: +// 責務: // ・召喚 (spawn) と消滅 (despawn) -// ・基本飛行 AI (召喚地点の周りを高所で旋回) +// ・飛行 AI: 旋回 (Patrol) / チャージ突進 (Charge) / 復帰 (Retreat) +// ・攻撃: 体当たり・接触ダメージ・ファイアボール + ブレス雲 // ・羽ばたきフェーズの更新 // -// 戦闘 (HP・矢ダメージ・接触ダメージ) は Phase B、 -// ネットワーク同期は Phase C で追加する。 +// AI (update) はホストのみが実行し 10Hz で状態配信する。 +// ファイアボール/ブレス雲 (updateProjectiles) は直線等速で決定的なため、 +// 発射イベントの受信後は全クライアントがローカルにシミュレートする。 class DragonManager { public: // ドラゴンを指定地点の上空 spawn_height_offset ブロックに召喚する。 @@ -37,6 +41,28 @@ class DragonManager { // 軸は yaw 方向に水平に伸びるとみなす (pitch は近似のため無視)。 float distanceToBody(float px, float py, float pz) const; + // ── ファイアボール / ブレス雲 ──────────────────────────────────────── + // 毎フレーム・全クライアントで呼ぶ (ホスト/非ホスト共通)。 + // 弾の飛翔 → 着弾 (isSolid) でブレス雲生成 → 雲のダメージ判定。 + // 戻り値: ローカルプレイヤー (px,py,pz=カメラ位置) へのこのフレームのダメージ。 + float updateProjectiles(float dt, + const std::function& isSolid, + float px, float py, float pz); + + // ネット受信した発射イベントを反映する (非ホスト用)。 + void spawnFireballNet(float x, float y, float z, + float vx, float vy, float vz); + + // ホストが今フレーム発射した弾を取り出す (ブロードキャスト用 outbox)。 + // 戻り値 true の間繰り返し呼ぶ。 + bool pollFireballSpawn(DragonFireball& out); + + // このフレームに起きた着弾爆発の座標を取り出す (効果音用 outbox)。 + bool pollExplosion(float& x, float& y, float& z); + + const std::vector& fireballs() const { return fireballs_; } + const std::vector& clouds() const { return clouds_; } + // ── マルチプレイヤー同期用 ──────────────────────────────────────────── // ホスト側がブロードキャストに使う「読み取り」: 現状の状態を packet 用に取り出す。 // exists=false ならドラゴン不在。 @@ -76,8 +102,17 @@ class DragonManager { }; Maybe dragon_; + // 弾・雲はドラゴン本体と独立に生存する (死亡後も雲は残る)。 + std::vector fireballs_; + std::vector clouds_; + std::vector pending_spawns_; // ホスト→ネット送信待ち + std::vector pending_booms_; // 着弾爆発の効果音待ち + + // Patrol 中の攻撃判断 (ホストのみで実行される) + void tryAttacks(EnderDragon& d, float px, float py, float pz); + static constexpr float kSpawnHeightOffset = 30.0f; // 召喚高度差 static constexpr float kOrbitRadius = 25.0f; // 旋回半径 static constexpr float kOrbitSpeed = 0.35f; // 旋回角速度 (rad/s) - static constexpr float kWingFlapSpeed = 6.0f; // 羽ばたき角速度 (rad/s) + static constexpr float kWingFlapSpeed = 2.4f; // 羽ばたき角速度 (rad/s)。本家のゆったりした周期 (約2.6秒/回) }; diff --git a/srcs/mob/ender_dragon.hpp b/srcs/mob/ender_dragon.hpp index d27d7c7..ac70b9d 100644 --- a/srcs/mob/ender_dragon.hpp +++ b/srcs/mob/ender_dragon.hpp @@ -18,11 +18,36 @@ struct EnderDragon { float health = 100.0f; bool alive = true; - enum class State : uint8_t { Patrol, Dying } state = State::Patrol; + // 状態遷移: Patrol → (クールダウン明け) Charge → (体当たり/タイムアウト) + // Retreat → (旋回円に復帰) Patrol。HP 0 でどの状態からも Dying。 + // 注意: wire format は uint8_t キャストなので既存値 (Patrol=0, Dying=1) は + // 変更せず末尾に追加する。 + enum class State : uint8_t { Patrol, Dying, Charge, Retreat } state = State::Patrol; float wing_phase = 0.0f; // 羽ばたき位相 (rad) float life_timer = 0.0f; // 生存秒数 float dying_timer = 0.0f; // Dying 状態になってからの秒数 (演出 + 自動消滅) float hit_flash_timer = 0.0f; // 被弾フラッシュの残り秒数 (赤くする) + + // ── 攻撃 AI 用タイマー (ホスト側のみ意味を持つ。ネット同期しない) ────── + float charge_cooldown = 10.0f; // 次のチャージ突進までの秒数 + float fireball_cooldown = 4.0f; // 次のファイアボールまでの秒数 + float charge_timer = 0.0f; // Charge 状態の経過秒数 (タイムアウト用) +}; + +// ドラゴンファイアボール (本家準拠: 弾自体に接触ダメージはなく、 +// 着弾点に残留ダメージ雲 = ドラゴンブレスを生成する)。 +// 飛翔は直線等速で決定的なので、発射イベントだけネット配信すれば +// 各クライアントがローカルにシミュレートできる。 +struct DragonFireball { + float x = 0, y = 0, z = 0; + float vx = 0, vy = 0, vz = 0; // ブロック/秒 + float life = 0; // 残り秒数 (0 で自然消滅 = その場で爆発) +}; + +// ブレス雲 (残留ダメージ領域)。着弾点に一定時間とどまる。 +struct DragonBreathCloud { + float x = 0, y = 0, z = 0; // 中心 (地表付近) + float timer = 0; // 残り秒数 }; // ドラゴンの判定値定数 (DragonManager / ArrowManager 双方で参照する) @@ -44,3 +69,23 @@ constexpr float DRAGON_ARROW_DAMAGE = 10.0f; constexpr float DRAGON_MELEE_DAMAGE = 5.0f; constexpr float DRAGON_MELEE_RANGE = 6.0f; // プレイヤー側のリーチ constexpr float DRAGON_DYING_DURATION = 3.0f; // Dying → 自動消滅 + +// ── チャージ突進 (本家のswoop攻撃相当) ──────────────────────────────────── +constexpr float DRAGON_CHARGE_RANGE = 80.0f; // この距離内のプレイヤーを狙う +constexpr float DRAGON_CHARGE_SPEED = 16.0f; // 突進速度 (ブロック/秒) +constexpr float DRAGON_CHARGE_TURN = 2.5f; // 旋回の利き (1/秒, 大きいほど機敏) +constexpr float DRAGON_CHARGE_DAMAGE = 12.0f; // 体当たりの一撃ダメージ +constexpr float DRAGON_CHARGE_TIMEOUT = 6.0f; // 当たらなければ諦めて復帰 +constexpr float DRAGON_CHARGE_COOLDOWN = 12.0f; // 突進間隔 +constexpr float DRAGON_RETREAT_SPEED = 11.0f; // 旋回円への復帰速度 + +// ── ファイアボール + ブレス雲 (本家のDragonFireball + AreaEffectCloud) ──── +constexpr float DRAGON_FIREBALL_SPEED = 14.0f; // ブロック/秒 +constexpr float DRAGON_FIREBALL_LIFE = 8.0f; // 最大飛翔秒数 +constexpr float DRAGON_FIREBALL_COOLDOWN = 5.5f; // 発射間隔 +constexpr float DRAGON_FIREBALL_MIN_DIST = 12.0f; // これより近いと撃たない (突進向き) +constexpr float DRAGON_FIREBALL_MAX_DIST = 64.0f; // 射程 +constexpr float DRAGON_CLOUD_DURATION = 5.0f; // ブレス雲の持続秒数 +constexpr float DRAGON_CLOUD_RADIUS = 3.5f; // 雲の水平半径 +constexpr float DRAGON_CLOUD_HEIGHT = 3.5f; // 雲の縦の効果範囲 (中心から±) +constexpr float DRAGON_CLOUD_DPS = 6.0f; // 雲の中にいるときの毎秒ダメージ diff --git a/srcs/mob/mob_manager.cpp b/srcs/mob/mob_manager.cpp index 3ba8d68..757b761 100644 --- a/srcs/mob/mob_manager.cpp +++ b/srcs/mob/mob_manager.cpp @@ -87,18 +87,38 @@ int MobManager::findGroundY(float x, float z, const World& world) const { return -1; } +// ── Target selection ────────────────────────────────────────────────────────── +// 最寄りのプレイヤー(水平距離)を追跡対象に選ぶ。 +// マルチプレイでは全プレイヤーが等しく追われる(ホスト専用ではない)。 +int MobManager::nearestTarget(const Zombie& z, + const std::vector& targets) { + int best = -1; + float best_d = 1e30f; + for (int i = 0; i < (int)targets.size(); ++i) { + const float dx = targets[i].x - z.x; + const float dz = targets[i].z - z.z; + const float d = dx * dx + dz * dz; + if (d < best_d) { + best_d = d; + best = i; + } + } + return best; +} + // ── Per-zombie update ───────────────────────────────────────────────────────── -float MobManager::updateZombie(Zombie& z, float dt, - float px, float py, float pz, - const std::function& isSolid) { +void MobManager::updateZombie(Zombie& z, float dt, + const std::vector& targets, + const std::function& isSolid, + std::vector& out_damage) { + const int ti = nearestTarget(z, targets); // Player feet position - const float pfx = px; - (void)py; - const float pfz = pz; + const float pfx = (ti >= 0) ? targets[ti].x : z.x; + const float pfz = (ti >= 0) ? targets[ti].z : z.z; const float dx = pfx - z.x; const float dz = pfz - z.z; - const float horiz = std::sqrt(dx * dx + dz * dz); + const float horiz = (ti >= 0) ? std::sqrt(dx * dx + dz * dz) : 1e30f; // ── State transitions ───────────────────────────────────────────────── z.state_timer += dt; @@ -157,24 +177,25 @@ float MobManager::updateZombie(Zombie& z, float dt, // ── Attack cooldown ─────────────────────────────────────────────────── if (z.attack_cooldown > 0) z.attack_cooldown -= dt; - if (z.state == Zombie::State::Attack && z.attack_cooldown <= 0) { + if (z.state == Zombie::State::Attack && z.attack_cooldown <= 0 && ti >= 0) { z.attack_cooldown = ATTACK_PERIOD; - return ATTACK_DAMAGE; + out_damage[ti] += ATTACK_DAMAGE; } - return 0.0f; } -float MobManager::updateCreeper(Zombie& z, float dt, - float px, float py, float pz, - const std::function& isSolid) { - const float pfx = px; - const float pfy = py - PLAYER_EYE_H; - const float pfz = pz; +void MobManager::updateCreeper(Zombie& z, float dt, + const std::vector& targets, + const std::function& isSolid, + std::vector& out_damage) { + const int ti = nearestTarget(z, targets); + const float pfx = (ti >= 0) ? targets[ti].x : z.x; + const float pfy = (ti >= 0) ? targets[ti].y - PLAYER_EYE_H : z.y; + const float pfz = (ti >= 0) ? targets[ti].z : z.z; const float dx = pfx - z.x; const float dy = pfy - z.y; const float dz = pfz - z.z; - const float horiz = std::sqrt(dx * dx + dz * dz); + const float horiz = (ti >= 0) ? std::sqrt(dx * dx + dz * dz) : 1e30f; const float ydiff = std::fabs(dy); const bool fuse_close = horiz < CREEPER_FUSE_RANGE && ydiff < CREEPER_FUSE_Y_RANGE; @@ -244,12 +265,17 @@ float MobManager::updateCreeper(Zombie& z, float dt, explosions_.push_back({z.x, z.y + 0.85f, z.z, CREEPER_BLOCK_RADIUS}); z.health = 0.0f; - const float dist = std::sqrt(dx * dx + dy * dy + dz * dz); - if (dist >= CREEPER_DAMAGE_RADIUS) return 0.0f; - const float t = 1.0f - dist / CREEPER_DAMAGE_RADIUS; - return CREEPER_MAX_DAMAGE * t * t; + // 爆発は追跡対象に限らず、半径内の全プレイヤーにダメージを与える + for (int i = 0; i < (int)targets.size(); ++i) { + const float ex = targets[i].x - z.x; + const float ey = (targets[i].y - PLAYER_EYE_H) - z.y; + const float ez = targets[i].z - z.z; + const float dist = std::sqrt(ex * ex + ey * ey + ez * ez); + if (dist >= CREEPER_DAMAGE_RADIUS) continue; + const float t = 1.0f - dist / CREEPER_DAMAGE_RADIUS; + out_damage[i] += CREEPER_MAX_DAMAGE * t * t; + } } - return 0.0f; } // ── Spawning ────────────────────────────────────────────────────────────────── @@ -283,32 +309,35 @@ void MobManager::trySpawn(float px, float pz, } // ── Public API ──────────────────────────────────────────────────────────────── -float MobManager::update(float dt, - float px, float py, float pz, - float time_of_day, - const std::function& isSolid, - const World& world) { +void MobManager::update(float dt, + const std::vector& targets, + float time_of_day, + const std::function& isSolid, + const World& world, + std::vector& out_damage) { + out_damage.assign(targets.size(), 0.0f); + if (targets.empty()) return; + spawn_timer_ -= dt; if (spawn_timer_ <= 0) { spawn_timer_ = SPAWN_INTERVAL; - trySpawn(px, pz, world, time_of_day); + // スポーン中心はランダムに選んだプレイヤー(全員の周囲に均等に湧かせる) + const MobTarget& t = targets[rand() % targets.size()]; + trySpawn(t.x, t.z, world, time_of_day); } - float damage = 0; for (auto& z : zombies_) { if (!z.alive()) continue; if (z.type == MobType::Creeper) - damage += updateCreeper(z, dt, px, py, pz, isSolid); + updateCreeper(z, dt, targets, isSolid, out_damage); else - damage += updateZombie(z, dt, px, py, pz, isSolid); + updateZombie(z, dt, targets, isSolid, out_damage); } zombies_.erase( std::remove_if(zombies_.begin(), zombies_.end(), [](const Zombie& z) { return !z.alive(); }), zombies_.end()); - - return damage; } std::vector MobManager::consumeExplosions() { diff --git a/srcs/mob/mob_manager.hpp b/srcs/mob/mob_manager.hpp index f7b221d..9a939d5 100644 --- a/srcs/mob/mob_manager.hpp +++ b/srcs/mob/mob_manager.hpp @@ -13,16 +13,27 @@ struct MobExplosion { float radius = 0.0f; }; +// モブの追跡・攻撃対象となるプレイヤー1人分の情報。 +// マルチプレイではホストがローカル+全リモートプレイヤーを詰めて渡す。 +// id はサーバー割当の player_id(シングルプレイではローカル=0)。 +struct MobTarget { + uint8_t id = 0; + float x = 0.0f, y = 0.0f, z = 0.0f; // eye position +}; + class MobManager { public: // Update all mobs. - // px/py/pz = player eye position (camera). - // Returns total HP damage dealt to the player this frame. - float update(float dt, - float px, float py, float pz, - float time_of_day, - const std::function& isSolid, - const World& world); + // targets = 追跡対象プレイヤー一覧(先頭をローカルとする規約はない。 + // 各モブは最寄りのターゲットを選んで追跡・攻撃する)。 + // out_damage は targets と同サイズに初期化され、 + // 各ターゲットがこのフレームに受けた HP ダメージが入る。 + void update(float dt, + const std::vector& targets, + float time_of_day, + const std::function& isSolid, + const World& world, + std::vector& out_damage); // Called when the player attacks (left-click + direction). // Deals damage to the closest mob within melee range in front of px/py/pz. @@ -37,12 +48,17 @@ class MobManager { private: void trySpawn(float px, float pz, const World& world, float time_of_day); - float updateZombie(Zombie& z, float dt, - float px, float py, float pz, - const std::function& isSolid); - float updateCreeper(Zombie& z, float dt, - float px, float py, float pz, - const std::function& isSolid); + // 最寄りターゲットの index を返す(targets 空なら -1) + static int nearestTarget(const Zombie& z, + const std::vector& targets); + void updateZombie(Zombie& z, float dt, + const std::vector& targets, + const std::function& isSolid, + std::vector& out_damage); + void updateCreeper(Zombie& z, float dt, + const std::vector& targets, + const std::function& isSolid, + std::vector& out_damage); static bool overlapsAny(float x, float y, float z, const std::function& isSolid); static void applyMovement(Zombie& z, float dt, float move_x, float move_z, diff --git a/srcs/network/client.cpp b/srcs/network/client.cpp index e07c693..6afe3da 100644 --- a/srcs/network/client.cpp +++ b/srcs/network/client.cpp @@ -87,8 +87,10 @@ void NetworkClient::handlePacket(PacketType type, const uint8_t* payload, rp.x = pkt.x; rp.y = pkt.y; rp.z = pkt.z; rp.yaw = pkt.yaw; rp.pitch = pkt.pitch; rp.health = pkt.health; - if ((pkt.state_flags & 0x02u) && !(rp.state_flags & 0x02u)) + if ((pkt.state_flags & 0x02u) && !(rp.state_flags & 0x02u)) { rp.attack_timer = 0.28f; + rp.attack_sound_pending = true; // engine 側で 3D 攻撃音を再生 + } rp.state_flags = pkt.state_flags; break; } @@ -180,6 +182,32 @@ void NetworkClient::handlePacket(PacketType type, const uint8_t* payload, out.push_back(ev); break; } + case PacketType::DragonFireball: { + if (size < sizeof(PktDragonFireball)) break; + PktDragonFireball pkt; + std::memcpy(&pkt, payload, sizeof(pkt)); + NetworkEvent ev; + ev.kind = NetworkEvent::Kind::DragonFireball; + ev.fb_x = pkt.x; + ev.fb_y = pkt.y; + ev.fb_z = pkt.z; + ev.fb_vx = pkt.vx; + ev.fb_vy = pkt.vy; + ev.fb_vz = pkt.vz; + out.push_back(ev); + break; + } + case PacketType::PlayerDamage: { + if (size < sizeof(PktPlayerDamage)) break; + PktPlayerDamage pkt; + std::memcpy(&pkt, payload, sizeof(pkt)); + NetworkEvent ev; + ev.kind = NetworkEvent::Kind::PlayerDamage; + ev.player_id = pkt.target_id; + ev.damage = pkt.damage; + out.push_back(ev); + break; + } default: break; } diff --git a/srcs/network/client.hpp b/srcs/network/client.hpp index ef79fd3..1b66c9d 100644 --- a/srcs/network/client.hpp +++ b/srcs/network/client.hpp @@ -18,6 +18,10 @@ struct RemotePlayer { float prev_x = 0.0f; float prev_z = 0.0f; bool initialized = false; + + // Sound state (3D SE: 歩行・攻撃を本人以外にも聞かせる) + float footstep_timer = 0.0f; + bool attack_sound_pending = false; }; struct NetworkEvent { @@ -28,8 +32,12 @@ struct NetworkEvent { MobUpdate, DragonSpawn, DragonUpdate, + DragonFireball, + PlayerDamage, } kind; uint8_t player_id = 0; + // PlayerDamage + float damage = 0.0f; // BlockChange fields int bx = 0, by = 0, bz = 0; uint8_t block_type = 0; @@ -46,6 +54,9 @@ struct NetworkEvent { float dragon_yaw = 0, dragon_pitch = 0; float dragon_wing_phase = 0; float dragon_health = 0; + // DragonFireball fields (発射位置と速度) + float fb_x = 0, fb_y = 0, fb_z = 0; + float fb_vx = 0, fb_vy = 0, fb_vz = 0; }; class NetworkClient { diff --git a/srcs/network/packet.hpp b/srcs/network/packet.hpp index 94224e7..0eaf552 100644 --- a/srcs/network/packet.hpp +++ b/srcs/network/packet.hpp @@ -14,6 +14,8 @@ enum class PacketType : uint8_t { MobUpdate = 7, // C(host)→S; S→C relay to others DragonSpawn = 8, // 任意のC→S; S→C broadcast: ドラゴン召喚イベント DragonUpdate = 9, // C(host)→S; S→C relay to others: ドラゴンの状態 + PlayerDamage = 10, // C(host)→S; S→該当クライアントへ転送: モブによるダメージ + DragonFireball = 11, // C(host)→S; S→C relay: ファイアボール発射イベント }; #pragma pack(push, 1) @@ -77,6 +79,21 @@ struct PktDragonState { float health; }; +// DragonFireball: ホストのドラゴンが発射したファイアボール。 +// 飛翔は直線等速で決定的なので、発射イベントのみ配信し +// 受信側がローカルにシミュレートする (着弾のブレス雲も同様)。 +struct PktDragonFireball { + float x, y, z; // 発射位置 (口) + float vx, vy, vz; // 速度 (ブロック/秒) +}; + +// PlayerDamage: ホストが計算したモブ→プレイヤーのダメージ。 +// サーバーは target_id のクライアントにのみ転送し、受信側が自分の HP に適用する。 +struct PktPlayerDamage { + uint8_t target_id; + float damage; +}; + #pragma pack(pop) static constexpr uint8_t MAX_PLAYERS = 8; diff --git a/srcs/network/server.cpp b/srcs/network/server.cpp index 2747b39..bd64c89 100644 --- a/srcs/network/server.cpp +++ b/srcs/network/server.cpp @@ -227,6 +227,25 @@ void VoxServer::handlePacket(int from_fd, PacketType type, broadcast(PacketType::DragonUpdate, payload, size, from_fd); break; } + case PacketType::DragonFireball: { + // ホストのドラゴンが発射したファイアボールを他全員に中継 + broadcast(PacketType::DragonFireball, payload, size, from_fd); + break; + } + case PacketType::PlayerDamage: { + // ホストが計算したモブダメージを対象プレイヤーにのみ転送 + if (size < sizeof(PktPlayerDamage)) break; + PktPlayerDamage pkt; + std::memcpy(&pkt, payload, sizeof(pkt)); + for (auto& [fd, c] : clients_) { + if (c.id != pkt.target_id) continue; + PacketHeader hdr{PacketType::PlayerDamage, sizeof(pkt)}; + c.sock.sendRaw(&hdr, sizeof(hdr)); + c.sock.sendRaw(&pkt, sizeof(pkt)); + break; + } + break; + } default: break; } diff --git a/srcs/renderer/renderer.cpp b/srcs/renderer/renderer.cpp index f7a981b..608db2e 100644 --- a/srcs/renderer/renderer.cpp +++ b/srcs/renderer/renderer.cpp @@ -2665,17 +2665,23 @@ void Renderer::drawArrows(const std::vector& arrows, } // ───────────────────────────────────────────────────────────────────────────── -// drawDragon() — エンダードラゴンを多パーツで描画する +// drawDragon() — エンダードラゴンを本家 (DragonModel) 準拠の多パーツで描画する // -// 構成: 頭・首・胴・尾(3)・翼(2)・脚(4) の計 11 パーツ。 -// すべて単位立方体ボックスを translate/rotate/scale で配置する。 +// 構成 (本家のパーツ分割に対応): +// ・頭 : 頭蓋 + 鼻先 + 顎 (開閉) + 鼻孔×2 + 角×2 + 紫眼×2 +// ・首 : 5 セグメントのチェーン (各セグメントに背びれ) +// ・胴 : 1 箱 + 背びれ×3 +// ・翼 : 左右とも「骨 (前縁) + 膜」×2 関節 (内翼 + 翼端)。 +// 翼端は内翼より遅れた位相・大きい振幅で羽ばたく。 +// ・尾 : 12 セグメントのチェーン (進行波でうねる。各セグメントに背びれ) +// ・脚 : 前脚×2 (腿+脛+足)、後脚×2 (前脚より大型)。飛行中は折りたたみ。 // // 座標系: // ・ドラゴンの「胴体中心」がワールド座標 (x,y,z)。 // ・モデルローカルでは +X が前方 (頭側)、-X が後方 (尾側)、 // ±Z が左右の翼、+Y が上面。 -// ・yaw=0 で +X 方向。Camera と同じ慣習なので yaw-90° で +Z 向きの -// デフォルトモデルを回転させる。 +// ・ローカル 1.0 = 本家モデル 20px。kModelScale=2.0 でワールド 2 ブロック。 +// ・寸法は本家 DragonModel の px 比 (胴 64×24×24、首/尾 10³、翼骨 56 等)。 // ───────────────────────────────────────────────────────────────────────────── void Renderer::drawDragon(const EnderDragon& d, const float* view4x4, const float* proj4x4) { @@ -2695,20 +2701,24 @@ void Renderer::drawDragon(const EnderDragon& d, glBindVertexArray(entity_vao_); glDisable(GL_CULL_FACE); - // パレット: エンダードラゴンの黒紫。被弾フラッシュ中は赤寄せ。 + // パレット: ほぼ黒の体 + やや明るい灰の翼膜。被弾フラッシュ中は赤寄せ。 const float flash = (d.hit_flash_timer > 0.0f) ? std::min(d.hit_flash_timer / 0.15f, 1.0f) : 0.0f; - const float kBody[] = { - 0.10f * (1.0f - flash) + 0.95f * flash, - 0.06f * (1.0f - flash) + 0.10f * flash, - 0.14f * (1.0f - flash) + 0.10f * flash, + auto mixFlash = [flash](float r, float g, float b, + float fr, float fg, float fb, + float* out) { + out[0] = r * (1.0f - flash) + fr * flash; + out[1] = g * (1.0f - flash) + fg * flash; + out[2] = b * (1.0f - flash) + fb * flash; }; - const float kDark[] = { - 0.03f * (1.0f - flash) + 0.70f * flash, - 0.02f * (1.0f - flash) + 0.05f * flash, - 0.05f * (1.0f - flash) + 0.05f * flash, - }; - static const float kEye[] = {0.80f, 0.20f, 0.85f}; // 紫眼 (フラッシュ無関係) + float kBody[3], kDark[3], kWing[3], kHorn[3]; + mixFlash(0.085f, 0.075f, 0.105f, 0.95f, 0.10f, 0.10f, kBody); // 体・首・尾 + mixFlash(0.045f, 0.040f, 0.055f, 0.70f, 0.06f, 0.06f, kDark); // 骨・脚・びれ + mixFlash(0.165f, 0.150f, 0.195f, 0.85f, 0.12f, 0.12f, kWing); // 翼膜 + mixFlash(0.330f, 0.315f, 0.355f, 0.90f, 0.30f, 0.30f, kHorn); // 角 + static const float kEye[] = {0.85f, 0.25f, 0.95f}; // 紫眼 (フラッシュ無関係) + + const bool dying = (d.state == EnderDragon::State::Dying); // 全体変換: 胴体中心に移動 → yaw → pitch → 全体スケール // yaw=0 → +X 向き。デフォルトモデルも +X 前なので回転は yaw のまま。 @@ -2719,88 +2729,297 @@ void Renderer::drawDragon(const EnderDragon& d, global = glm::rotate(global, glm::radians(d.pitch), glm::vec3(0.f, 0.f, 1.f)); global = glm::scale(global, glm::vec3(kModelScale)); - // ── 胴体 (Body): +X 前方に伸ばした楕円体的な箱 ───────────────────── + // ── 胴体 (Body): 本家 64×24×24px ───────────────────────────────────── { - glm::vec3 sz(3.5f, 1.4f, 1.8f); - glm::mat4 m = glm::translate(global, glm::vec3(0.0f, 0.0f, 0.0f)); - m = glm::scale(m, sz); + glm::mat4 m = glm::scale(global, glm::vec3(3.2f, 1.2f, 1.2f)); drawStevePart(vp * m, m, kBody); } + // 背びれ×3: 本家の背中の scale パーツ + for (int i = 0; i < 3; ++i) { + const float sx[3] = {0.9f, 0.1f, -0.7f}; + glm::mat4 m = glm::translate(global, glm::vec3(sx[i], 0.75f, 0.0f)); + m = glm::scale(m, glm::vec3(0.50f, 0.35f, 0.10f)); + drawStevePart(vp * m, m, kDark); + } - // ── 首 (Neck): 胴体前から斜めに前上方へ ─────────────────────────── + // ── 首 (Neck): 5 セグメントのチェーンを前上方へ弧状に伸ばす ────────── + // 位置はチェーン積分で決める (wing_phase 由来の揺れのみなので + // ネット同期クライアントでも決定的)。 + glm::vec3 neck_p(1.55f, 0.30f, 0.0f); // 胴体前端の付け根 + float head_pitch = 0.0f; { - glm::vec3 sz(1.6f, 0.9f, 0.9f); - glm::mat4 m = glm::translate(global, glm::vec3(2.3f, 0.45f, 0.0f)); - m = glm::rotate(m, glm::radians(-12.0f), glm::vec3(0.f, 0.f, 1.f)); - m = glm::scale(m, sz); - drawStevePart(vp * m, m, kBody); + // 付け根→頭にかけて持ち上がり、先端で水平に戻る弧 + static const float kNeckPitch[5] = {0.30f, 0.45f, 0.50f, 0.38f, 0.20f}; + for (int i = 0; i < 5; ++i) { + float pitch = kNeckPitch[i] + + std::sin(d.wing_phase * 0.6f + i * 0.5f) * 0.05f; + if (dying) pitch += 0.30f; // 断末魔: 首を大きく反らす + const glm::vec3 dir(std::cos(pitch), std::sin(pitch), 0.0f); + const glm::vec3 c = neck_p + dir * 0.21f; + + glm::mat4 base = glm::translate(global, c); + base = glm::rotate(base, pitch, glm::vec3(0.f, 0.f, 1.f)); + glm::mat4 seg = glm::scale(base, glm::vec3(0.5f)); + drawStevePart(vp * seg, seg, kBody); + // 首背びれ + glm::mat4 fin = glm::translate(base, glm::vec3(0.0f, 0.33f, 0.0f)); + fin = glm::scale(fin, glm::vec3(0.30f, 0.22f, 0.08f)); + drawStevePart(vp * fin, fin, kDark); + + neck_p += dir * 0.42f; + head_pitch = pitch; + } } - // ── 頭 (Head): 首の先端 ────────────────────────────────────────── + // ── 頭 (Head): 首の先端。頭蓋+鼻先+顎+鼻孔+角+目 ────────────────────── { - glm::vec3 sz(1.8f, 1.2f, 1.4f); - glm::mat4 m = glm::translate(global, glm::vec3(3.4f, 0.9f, 0.0f)); - m = glm::scale(m, sz); - drawStevePart(vp * m, m, kBody); + // 顎の開き: 普段は時々ゆっくり開閉、被弾時と死亡演出では大きく開く + float jaw_open = 0.08f + + 0.35f * std::max(0.0f, std::sin(d.life_timer * 0.5f) - 0.55f) / 0.45f; + jaw_open = std::max(jaw_open, flash * 0.55f); + if (dying) jaw_open = 0.65f; + + head_pitch *= 0.35f; // 頭は前方へ向き直す + glm::mat4 mh = glm::translate( + global, neck_p + glm::vec3(std::cos(head_pitch), std::sin(head_pitch), 0.f) * 0.35f); + mh = glm::rotate(mh, head_pitch, glm::vec3(0.f, 0.f, 1.f)); + + // 頭蓋 (本家 16³px) + { + glm::mat4 m = glm::translate(mh, glm::vec3(0.05f, 0.08f, 0.0f)); + m = glm::scale(m, glm::vec3(0.8f)); + drawStevePart(vp * m, m, kBody); + } + // 鼻先 (本家 upperlip 12×5×16px) + { + glm::mat4 m = glm::translate(mh, glm::vec3(0.70f, 0.02f, 0.0f)); + m = glm::scale(m, glm::vec3(0.70f, 0.26f, 0.50f)); + drawStevePart(vp * m, m, kBody); + } + // 鼻孔×2 (鼻先上面) + for (int side = 0; side < 2; ++side) { + const float zs = (side == 0) ? 0.15f : -0.15f; + glm::mat4 m = glm::translate(mh, glm::vec3(0.92f, 0.18f, zs)); + m = glm::scale(m, glm::vec3(0.12f, 0.08f, 0.10f)); + drawStevePart(vp * m, m, kDark); + } + // 顎 (本家 jaw 12×4×16px): 後端ピボットで下方に開く + { + glm::mat4 mj = glm::translate(mh, glm::vec3(0.32f, -0.22f, 0.0f)); + mj = glm::rotate(mj, -jaw_open, glm::vec3(0.f, 0.f, 1.f)); + glm::mat4 m = glm::translate(mj, glm::vec3(0.36f, -0.08f, 0.0f)); + m = glm::scale(m, glm::vec3(0.72f, 0.16f, 0.46f)); + drawStevePart(vp * m, m, kBody); + } + // 角×2: 頭蓋上面後方から上後方へ + for (int side = 0; side < 2; ++side) { + const float zs = (side == 0) ? 0.24f : -0.24f; + glm::mat4 m = glm::translate(mh, glm::vec3(-0.12f, 0.44f, zs)); + m = glm::rotate(m, 0.55f, glm::vec3(0.f, 0.f, 1.f)); // 上端を後ろへ + m = glm::translate(m, glm::vec3(0.0f, 0.26f, 0.0f)); + m = glm::scale(m, glm::vec3(0.12f, 0.55f, 0.12f)); + drawStevePart(vp * m, m, kHorn); + } + // 目×2: 頭蓋側面の紫の発光部 + for (int side = 0; side < 2; ++side) { + const float zs = (side == 0) ? 0.41f : -0.41f; + glm::mat4 m = glm::translate(mh, glm::vec3(0.20f, 0.16f, zs)); + m = glm::scale(m, glm::vec3(0.26f, 0.12f, 0.05f)); + drawStevePart(vp * m, m, kEye); + } } - // ── 目 (左右): 頭の側面に紫色の小箱 ──────────────────────────────── - for (int side = 0; side < 2; ++side) { - const float zs = (side == 0) ? 0.71f : -0.71f; - glm::vec3 sz(0.30f, 0.20f, 0.05f); - glm::mat4 m = glm::translate(global, glm::vec3(3.7f, 1.10f, zs)); - m = glm::scale(m, sz); - drawStevePart(vp * m, m, kEye); + // ── 翼 (左右): 骨 (前縁) + 膜の 2 関節。翼端は遅延位相で羽ばたく ────── + // 本家比: 内翼骨 56×8×8px、翼端骨 56×4×4px、膜 56×0×56px。 + { + const float flap = std::sin(d.wing_phase); + const float flap_tip = std::sin(d.wing_phase - 0.7f); // 翼端は遅れる + for (int side = 0; side < 2; ++side) { + const float zs = (side == 0) ? 1.0f : -1.0f; + // 上げ基調 (滑空姿勢) で ±30° 程度はためく + const float root_ang = -(0.12f + 0.50f * flap) * zs; + const float tip_ang = -(0.18f + 0.55f * flap_tip) * zs; + + // 肩ピボット (胴体上面前方) + glm::mat4 sh = glm::translate(global, glm::vec3(0.55f, 0.45f, 0.55f * zs)); + sh = glm::rotate(sh, root_ang, glm::vec3(1.f, 0.f, 0.f)); + + // 内翼: 骨 (前縁) + { + glm::mat4 m = glm::translate(sh, glm::vec3(0.0f, 0.0f, 1.4f * zs)); + m = glm::scale(m, glm::vec3(0.40f, 0.40f, 2.8f)); + drawStevePart(vp * m, m, kDark); + } + // 内翼: 膜 (骨から後方に張る) + { + glm::mat4 m = glm::translate(sh, glm::vec3(-1.35f, 0.0f, 1.4f * zs)); + m = glm::scale(m, glm::vec3(2.7f, 0.06f, 2.8f)); + drawStevePart(vp * m, m, kWing); + } + // 翼端ピボット (内翼の先端) + glm::mat4 tp = glm::translate(sh, glm::vec3(0.0f, 0.0f, 2.8f * zs)); + tp = glm::rotate(tp, tip_ang, glm::vec3(1.f, 0.f, 0.f)); + // 翼端: 骨 + { + glm::mat4 m = glm::translate(tp, glm::vec3(0.0f, 0.0f, 1.35f * zs)); + m = glm::scale(m, glm::vec3(0.28f, 0.28f, 2.7f)); + drawStevePart(vp * m, m, kDark); + } + // 翼端: 膜 + { + glm::mat4 m = glm::translate(tp, glm::vec3(-1.10f, 0.0f, 1.35f * zs)); + m = glm::scale(m, glm::vec3(2.2f, 0.05f, 2.7f)); + drawStevePart(vp * m, m, kWing); + } + } } - // ── 翼 (左右): 胴体上面から左右に伸ばす平板。 - // 羽ばたきは flap_angle で Z 軸回転 (前縁を基準にバタつかせる)。 - const float flap = std::sin(d.wing_phase) * 0.45f; // ±25° 程度 - for (int side = 0; side < 2; ++side) { - const float zs = (side == 0) ? 1.0f : -1.0f; - const float angle = flap * zs; // 左右対称に羽ばたく - - // 翼の根本ピボット (胴体側面上) - glm::mat4 m = glm::translate(global, glm::vec3(0.0f, 0.6f, 0.9f * zs)); - m = glm::rotate(m, angle, glm::vec3(1.f, 0.f, 0.f)); - // 翼本体を root から +Z (zs 方向) に伸ばす - m = glm::translate(m, glm::vec3(0.0f, 0.0f, 1.6f * zs)); - m = glm::scale(m, glm::vec3(2.6f, 0.15f, 3.2f)); - drawStevePart(vp * m, m, kDark); + // ── 尾 (12 セグメント): 本家と同数。進行波で左右上下にうねる ───────── + { + glm::vec3 tail_p(-1.65f, 0.10f, 0.0f); // 胴体後端の付け根 + for (int i = 0; i < 12; ++i) { + // 付け根→先端へ伝わる進行波 (横) + ゆっくりした上下動 + const float lat = std::sin(d.wing_phase * 0.8f - i * 0.45f) * 0.22f; + const float vert = -0.10f + std::sin(d.wing_phase * 0.53f - i * 0.30f) * 0.06f; + const glm::vec3 dir = glm::normalize(glm::vec3(-1.0f, vert, lat)); + const glm::vec3 c = tail_p + dir * 0.20f; + const float seg_yaw = std::atan2(lat, 1.0f); + const float taper = 1.0f - 0.035f * static_cast(i); // 緩い先細り + + glm::mat4 base = glm::translate(global, c); + base = glm::rotate(base, seg_yaw, glm::vec3(0.f, 1.f, 0.f)); + glm::mat4 seg = glm::scale(base, glm::vec3(0.5f * taper)); + drawStevePart(vp * seg, seg, kBody); + // 尾背びれ + glm::mat4 fin = glm::translate(base, glm::vec3(0.0f, 0.32f * taper, 0.0f)); + fin = glm::scale(fin, glm::vec3(0.28f, 0.20f, 0.08f) * taper); + drawStevePart(vp * fin, fin, kDark); + + tail_p += dir * 0.40f; + } } - // ── 尾 (3 セグメント): 後方に向かって徐々に細くなる - // 隣接セグメント間で連結する S 字波動。 + // ── 脚 (4): 腿+脛+足の 3 関節。飛行中は折りたたんで体に引きつける ────── + // 本家比: 前脚 腿8×24×8px、後脚 腿16×32×16px (後脚が大型)。 { - const float kSegLen[3] = {1.6f, 1.4f, 1.2f}; - const float kSegThick[3] = {1.0f, 0.7f, 0.45f}; - const float kSegHeight[3] = {0.9f, 0.65f, 0.4f}; - float prev_x = -1.75f; // 胴体後端から開始 - float prev_y = -0.05f; - const float wave = std::sin(d.wing_phase * 0.7f); - for (int i = 0; i < 3; ++i) { - const float yaw_off = wave * 0.10f * (i + 1); // 後ろほど大きく波打つ - const float seg_yaw = std::sin(d.wing_phase * 0.7f + i * 0.6f) * 0.15f; - const float cx = prev_x - kSegLen[i] * 0.5f; - const float cy = prev_y - 0.05f * (i + 1); - glm::mat4 m = glm::translate(global, glm::vec3(cx, cy, 0.0f)); - m = glm::rotate(m, seg_yaw + yaw_off, glm::vec3(0.f, 1.f, 0.f)); - m = glm::scale(m, glm::vec3(kSegLen[i], kSegHeight[i], kSegThick[i])); - drawStevePart(vp * m, m, kBody); - prev_x = cx - kSegLen[i] * 0.5f; - prev_y = cy; + auto drawLeg = [&](const glm::vec3& hip, + float tw, float tlen, // 腿の太さ・長さ + float sw, float slen, // 脛の太さ・長さ + const glm::vec3& foot_sz, + float thigh_ang, float shin_ang) { + glm::mat4 h = glm::translate(global, hip); + h = glm::rotate(h, thigh_ang, glm::vec3(0.f, 0.f, 1.f)); + glm::mat4 thigh = glm::translate(h, glm::vec3(0.0f, -tlen * 0.5f, 0.0f)); + thigh = glm::scale(thigh, glm::vec3(tw, tlen, tw)); + drawStevePart(vp * thigh, thigh, kDark); + + glm::mat4 k = glm::translate(h, glm::vec3(0.0f, -tlen, 0.0f)); + k = glm::rotate(k, shin_ang, glm::vec3(0.f, 0.f, 1.f)); + glm::mat4 shin = glm::translate(k, glm::vec3(0.0f, -slen * 0.5f, 0.0f)); + shin = glm::scale(shin, glm::vec3(sw, slen, sw)); + drawStevePart(vp * shin, shin, kDark); + + // 足: 足裏が水平になるよう関節分を打ち消し、爪先を前へ + glm::mat4 f = glm::translate( + k, glm::vec3(foot_sz.x * 0.25f, -slen - foot_sz.y * 0.4f, 0.0f)); + f = glm::rotate(f, -(thigh_ang + shin_ang), glm::vec3(0.f, 0.f, 1.f)); + f = glm::scale(f, foot_sz); + drawStevePart(vp * f, f, kDark); + }; + + const float sway = std::sin(d.wing_phase) * 0.06f; // 羽ばたきに同期した揺れ + for (int side = 0; side < 2; ++side) { + const float zs = (side == 0) ? 1.0f : -1.0f; + // 前脚 (小) + drawLeg(glm::vec3(0.85f, -0.50f, 0.62f * zs), + 0.38f, 1.00f, 0.28f, 0.90f, + glm::vec3(0.65f, 0.18f, 0.40f), + -0.50f + sway, 1.05f); + // 後脚 (大) + drawLeg(glm::vec3(-0.65f, -0.45f, 0.72f * zs), + 0.70f, 1.30f, 0.50f, 1.20f, + glm::vec3(1.00f, 0.28f, 0.60f), + -0.85f + sway, 1.35f); } } - // ── 脚 (4): 胴体下面の四隅から小さく垂れ下がる - { - const float lx[4] = { 1.2f, 1.2f, -1.0f, -1.0f}; - const float lz[4] = { 0.7f, -0.7f, 0.7f, -0.7f}; - const glm::vec3 leg_sz(0.45f, 0.85f, 0.45f); - for (int i = 0; i < 4; ++i) { - glm::mat4 m = glm::translate(global, glm::vec3(lx[i], -1.10f, lz[i])); - m = glm::scale(m, leg_sz); - drawStevePart(vp * m, m, kDark); + glEnable(GL_CULL_FACE); + glBindVertexArray(0); +} + +// ───────────────────────────────────────────────────────────────────────────── +// drawDragonEffects() — ファイアボールとブレス雲を描画する +// +// ファイアボール: 明るい紫の核 + 暗い外殻を互い違いに回転させた 2 重キューブ。 +// ブレス雲 : 着弾点の周囲を漂う小さな紫パーティクル群。インデックスから +// 疑似乱数で配置し、残り時間でゆっくり旋回・終盤は縮んで消える。 +// ───────────────────────────────────────────────────────────────────────────── +void Renderer::drawDragonEffects(const std::vector& fireballs, + const std::vector& clouds, + const float* view4x4, const float* proj4x4) { + if (!entity_vao_) return; + if (fireballs.empty() && clouds.empty()) return; + + const glm::mat4 view = glm::make_mat4(view4x4); + const glm::mat4 proj = glm::make_mat4(proj4x4); + const glm::mat4 vp = proj * view; + + entity_shader_.use(); + entity_shader_.setVec3("uSunDir", sun_dir_[0], sun_dir_[1], sun_dir_[2]); + entity_shader_.setFloat("uAmbient", ambient_); + entity_shader_.setFloat("uSunStrength", sun_strength_); + entity_shader_.setMat4("uView", view4x4); + setFogUniforms(entity_shader_, sky_horizon_, underwater_); + + glBindVertexArray(entity_vao_); + glDisable(GL_CULL_FACE); + + static const float kCore[] = {0.90f, 0.35f, 1.00f}; // 明るい紫の核 + static const float kShell[] = {0.35f, 0.08f, 0.45f}; // 暗紫の外殻 + static const float kBreath[] = {0.55f, 0.15f, 0.75f}; // ブレス雲 + + // ── ファイアボール ────────────────────────────────────────────────────── + for (const DragonFireball& fb : fireballs) { + // 残り寿命を回転位相に流用 (発射時刻から単調なので見た目は連続) + const float spin = fb.life * 4.0f; + glm::mat4 base = glm::translate(glm::mat4(1.0f), + glm::vec3(fb.x, fb.y, fb.z)); + // 核 + glm::mat4 core = glm::rotate(base, spin, glm::vec3(0.3f, 1.0f, 0.2f)); + core = glm::scale(core, glm::vec3(0.9f)); + drawStevePart(vp * core, core, kCore); + // 外殻 (逆回転) + glm::mat4 shell = glm::rotate(base, -spin * 0.7f, + glm::vec3(0.7f, 0.4f, 0.6f)); + shell = glm::scale(shell, glm::vec3(1.25f)); + drawStevePart(vp * shell, shell, kShell); + } + + // ── ブレス雲 ──────────────────────────────────────────────────────────── + for (const DragonBreathCloud& cl : clouds) { + const float age = DRAGON_CLOUD_DURATION - cl.timer; + // 終盤 1 秒で縮んで消える + const float fade = std::min(cl.timer, 1.0f); + constexpr int kParticles = 14; + for (int i = 0; i < kParticles; ++i) { + // インデックスから疑似乱数 (全クライアントで同一の見た目になる) + const float h1 = std::sin((float)i * 12.9898f) * 43758.5453f; + const float h2 = std::sin((float)i * 78.2330f) * 12345.6789f; + const float r01a = h1 - std::floor(h1); + const float r01b = h2 - std::floor(h2); + + const float radius = (0.4f + r01a * 0.6f) * DRAGON_CLOUD_RADIUS; + const float ang = r01b * 6.2831853f + age * (0.4f + r01a * 0.5f); + const float bob = std::sin(age * 2.0f + (float)i) * 0.25f; + const glm::vec3 pos(cl.x + std::cos(ang) * radius, + cl.y + 0.4f + r01b * 1.2f + bob, + cl.z + std::sin(ang) * radius); + + glm::mat4 m = glm::translate(glm::mat4(1.0f), pos); + m = glm::rotate(m, age * 1.5f + (float)i, + glm::vec3(0.4f, 1.0f, 0.3f)); + m = glm::scale(m, glm::vec3((0.30f + r01a * 0.25f) * fade)); + drawStevePart(vp * m, m, kBreath); } } diff --git a/srcs/renderer/renderer.hpp b/srcs/renderer/renderer.hpp index cad10eb..314b981 100644 --- a/srcs/renderer/renderer.hpp +++ b/srcs/renderer/renderer.hpp @@ -59,6 +59,11 @@ class Renderer : public IRenderer { const float* view4x4, const float* proj4x4); void drawDragon(const EnderDragon& dragon, const float* view4x4, const float* proj4x4); + // ドラゴンのファイアボール (紫の火球) とブレス雲 (残留ダメージ領域) を描画。 + // 雲はドラゴン死亡後も残るため drawDragon とは独立に毎フレーム呼ぶ。 + void drawDragonEffects(const std::vector& fireballs, + const std::vector& clouds, + const float* view4x4, const float* proj4x4); void drawBossBar(float health, float max_health); void endFrame() override; void onResize(int w, int h) override; @@ -72,6 +77,8 @@ class Renderer : public IRenderer { // Shadow mapping void updateShadowMatrix(float px, float py, float pz); + // 光源空間行列 (lightProj × lightView)。シャドウパスのカリングに使う。 + const float* lightSpaceMatrix() const { return light_space_mat_; } void beginShadowPass(); void endShadowPass(); void drawChunkShadow(const Chunk* chunk); diff --git a/srcs/world/noise.cpp b/srcs/world/noise.cpp index 6611c3c..bb5dd71 100644 --- a/srcs/world/noise.cpp +++ b/srcs/world/noise.cpp @@ -1,15 +1,18 @@ // ───────────────────────────────────────────────────────────────────────────── -// noise.cpp — ノイズ生成器の初期化とアクセス +// noise.cpp — Perlin ノイズの完全自作実装 // // 【パーリンノイズとは?】 // なめらかでランダムな値を生成する関数。 // 単純な乱数(毎回まったく違う値)と違い、隣り合う座標では似た値が返るため // 「自然に見える」地形・雲・波紋などに使われる。 // -// 【FastNoiseLite とは?】 -// ヘッダーオンリーの軽量ノイズライブラリ。 -// Perlin・Simplex・Cellular など複数のノイズアルゴリズムを提供する。 -// このファイルでは5種類のノイズを目的別に初期化している。 +// 仕組み(2Dの場合): +// 1. 空間を 1×1 の格子に区切り、各格子点にシード由来の「勾配ベクトル」を置く +// 2. サンプル点を囲む4つの格子点について、 +// (勾配ベクトル) ・ (格子点→サンプル点ベクトル) の内積を取る +// 3. 4つの内積をクインティック曲線 6t⁵-15t⁴+10t³ で滑らかに補間する +// 勾配はハッシュ関数で決まるため、同じシード・同じ座標なら常に同じ値が返る +// (マルチプレイでクライアント同士の地形を一致させるのに必須の決定性)。 // // 【フラクタル FBm (Fractional Brownian Motion) とは?】 // ノイズを複数の「周波数(オクターブ)」で重ね合わせる技法。 @@ -17,35 +20,180 @@ // より自然な地形起伏が得られる。 // Lacunarity: 各オクターブで周波数を何倍にするか(通常は2.0) // Gain: 各オクターブで振幅を何倍にするか(通常は0.5で徐々に小さく) +// +// 【Ridged(稜線型)とは?】 +// 各オクターブで 1 - 2|noise| を合成する変種。 +// ノイズが 0 に近い場所が鋭い「峰」(+1) になり、山脈や谷の稜線を作れる。 // ───────────────────────────────────────────────────────────────────────────── #include "world/noise.hpp" -#include "FastNoiseLite.h" -#include +#include + +namespace { + +// ─── 格子点ハッシュ ────────────────────────────────────────────────────────── +// 整数格子座標とシードから 32bit のランダム値を作る。 +// 乗算 → XOR シフトを重ねて雪崩効果(1bit の入力差が全 bit に波及)を確保する。 +inline uint32_t hash2(uint32_t seed, int32_t x, int32_t y) { + uint32_t h = seed; + h ^= (uint32_t)x * 0x653735ABu; + h ^= (uint32_t)y * 0x9E3779B1u; + h *= 0x27D4EB2Fu; + h ^= h >> 15; + h *= 0x85EBCA77u; + h ^= h >> 13; + return h; +} + +inline uint32_t hash3(uint32_t seed, int32_t x, int32_t y, int32_t z) { + uint32_t h = seed; + h ^= (uint32_t)x * 0x653735ABu; + h ^= (uint32_t)y * 0x9E3779B1u; + h ^= (uint32_t)z * 0xC2B2AE3Du; + h *= 0x27D4EB2Fu; + h ^= h >> 15; + h *= 0x85EBCA77u; + h ^= h >> 13; + return h; +} + +// ─── 勾配ベクトル ──────────────────────────────────────────────────────────── +// 2D: 単位円上の8方向(軸4方向 + 対角4方向)。 +// 内積の理論最大値は √2/2 ≈ 0.7071 なので、最後に √2 倍して [-1,1] に広げる。 +constexpr float kDiag = 0.70710678f; // √2/2 +constexpr float kGrad2[8][2] = { + { 1.0f, 0.0f}, {-1.0f, 0.0f}, { 0.0f, 1.0f}, { 0.0f, -1.0f}, + { kDiag, kDiag}, {-kDiag, kDiag}, { kDiag, -kDiag}, {-kDiag, -kDiag}, +}; + +// 格子点 (ix,iy) の勾配と、格子点→サンプル点ベクトル (fx,fy) の内積 +inline float gradDot2(uint32_t seed, int32_t ix, int32_t iy, float fx, float fy) { + const float* g = kGrad2[hash2(seed, ix, iy) & 7u]; + return g[0] * fx + g[1] * fy; +} + +// 3D: 立方体の12辺方向 (±1,±1,0) 系。Improved Perlin (Ken Perlin 2002) と同じ集合。 +constexpr float kGrad3[12][3] = { + {1,1,0}, {-1,1,0}, {1,-1,0}, {-1,-1,0}, + {1,0,1}, {-1,0,1}, {1,0,-1}, {-1,0,-1}, + {0,1,1}, {0,-1,1}, {0,1,-1}, {0,-1,-1}, +}; + +inline float gradDot3(uint32_t seed, int32_t ix, int32_t iy, int32_t iz, + float fx, float fy, float fz) { + const float* g = kGrad3[hash3(seed, ix, iy, iz) % 12u]; + return g[0] * fx + g[1] * fy + g[2] * fz; +} + +// ─── 補間 ──────────────────────────────────────────────────────────────────── +// クインティック曲線 6t⁵-15t⁴+10t³。 +// 1次・2次導関数が格子境界で 0 になるため、継ぎ目が見えない滑らかさになる。 +inline float fade(float t) { return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); } +inline float lerp(float a, float b, float t) { return a + t * (b - a); } + +// ─── 単一オクターブ Perlin ─────────────────────────────────────────────────── +// 戻り値はおおよそ [-1, 1](整数格子点上では必ず 0 になる点に注意) +float perlin2(uint32_t seed, float x, float y) { + int32_t x0 = (int32_t)std::floor(x); + int32_t y0 = (int32_t)std::floor(y); + float fx = x - (float)x0; + float fy = y - (float)y0; + float u = fade(fx); + float v = fade(fy); + + float n00 = gradDot2(seed, x0, y0, fx, fy); + float n10 = gradDot2(seed, x0 + 1, y0, fx - 1.0f, fy); + float n01 = gradDot2(seed, x0, y0 + 1, fx, fy - 1.0f); + float n11 = gradDot2(seed, x0 + 1, y0 + 1, fx - 1.0f, fy - 1.0f); + + // 1.41421356 = 1/(√2/2): 8方向勾配での理論最大値の逆数([-1,1] へ正規化) + return lerp(lerp(n00, n10, u), lerp(n01, n11, u), v) * 1.41421356f; +} + +float perlin3(uint32_t seed, float x, float y, float z) { + int32_t x0 = (int32_t)std::floor(x); + int32_t y0 = (int32_t)std::floor(y); + int32_t z0 = (int32_t)std::floor(z); + float fx = x - (float)x0; + float fy = y - (float)y0; + float fz = z - (float)z0; + float u = fade(fx); + float v = fade(fy); + float w = fade(fz); + + float n000 = gradDot3(seed, x0, y0, z0, fx, fy, fz); + float n100 = gradDot3(seed, x0 + 1, y0, z0, fx - 1.0f, fy, fz); + float n010 = gradDot3(seed, x0, y0 + 1, z0, fx, fy - 1.0f, fz); + float n110 = gradDot3(seed, x0 + 1, y0 + 1, z0, fx - 1.0f, fy - 1.0f, fz); + float n001 = gradDot3(seed, x0, y0, z0 + 1, fx, fy, fz - 1.0f); + float n101 = gradDot3(seed, x0 + 1, y0, z0 + 1, fx - 1.0f, fy, fz - 1.0f); + float n011 = gradDot3(seed, x0, y0 + 1, z0 + 1, fx, fy - 1.0f, fz - 1.0f); + float n111 = gradDot3(seed, x0 + 1, y0 + 1, z0 + 1, fx - 1.0f, fy - 1.0f, fz - 1.0f); + + float nx00 = lerp(n000, n100, u); + float nx10 = lerp(n010, n110, u); + float nx01 = lerp(n001, n101, u); + float nx11 = lerp(n011, n111, u); + + // 0.96492141 ≈ 12辺勾配セットでの経験的最大値の逆数([-1,1] へ正規化) + return lerp(lerp(nx00, nx10, v), lerp(nx01, nx11, v), w) * 0.96492141f; +} + +} // namespace // ───────────────────────────────────────────────────────────────────────────── -// コンストラクタ / デストラクタ -// -// FastNoiseLite をポインタで保持するのは、ヘッダーオンリーライブラリの型を -// このヘッダーに露出させないため(PImpl パターン)。 +// NoiseGen::Perlin — フラクタル合成 // ───────────────────────────────────────────────────────────────────────────── -NoiseGen::NoiseGen() { - height_noise_ = new FastNoiseLite(); - valley_noise_ = new FastNoiseLite(); - cave_noise_ = new FastNoiseLite(); - cave_horiz_noise_ = new FastNoiseLite(); - temp_noise_ = new FastNoiseLite(); - humid_noise_ = new FastNoiseLite(); - variation_noise_ = new FastNoiseLite(); -} - -NoiseGen::~NoiseGen() { - delete (FastNoiseLite*)height_noise_; - delete (FastNoiseLite*)valley_noise_; - delete (FastNoiseLite*)cave_noise_; - delete (FastNoiseLite*)cave_horiz_noise_; - delete (FastNoiseLite*)temp_noise_; - delete (FastNoiseLite*)humid_noise_; - delete (FastNoiseLite*)variation_noise_; +void NoiseGen::Perlin::configure(uint32_t s, float freq, int oct, bool ridged_mode) { + seed = s; + frequency = freq; + octaves = oct; + ridged = ridged_mode; + + // FBm 合成後も振幅が [-1,1] に収まるよう、振幅の総和 Σgainⁱ の逆数を掛ける + float amp_sum = 0.0f; + float amp = 1.0f; + for (int i = 0; i < octaves; ++i) { + amp_sum += amp; + amp *= gain; + } + bounding = 1.0f / amp_sum; +} + +float NoiseGen::Perlin::sample2(float x, float y) const { + float fx = x * frequency; + float fy = y * frequency; + float amp = bounding; + float sum = 0.0f; + uint32_t s = seed; + for (int i = 0; i < octaves; ++i) { + float n = perlin2(s, fx, fy); + // Ridged: |n| を反転して 0 付近を鋭い峰 (+1) にする + sum += (ridged ? (1.0f - 2.0f * std::fabs(n)) : n) * amp; + fx *= lacunarity; + fy *= lacunarity; + amp *= gain; + ++s; // オクターブごとにシードをずらし、同じ模様の重なりを防ぐ + } + return sum; +} + +float NoiseGen::Perlin::sample3(float x, float y, float z) const { + float fx = x * frequency; + float fy = y * frequency; + float fz = z * frequency; + float amp = bounding; + float sum = 0.0f; + uint32_t s = seed; + for (int i = 0; i < octaves; ++i) { + float n = perlin3(s, fx, fy, fz); + sum += (ridged ? (1.0f - 2.0f * std::fabs(n)) : n) * amp; + fx *= lacunarity; + fy *= lacunarity; + fz *= lacunarity; + amp *= gain; + ++s; + } + return sum; } // ───────────────────────────────────────────────────────────────────────────── @@ -59,71 +207,33 @@ void NoiseGen::setSeed(uint32_t seed) { // Perlin + FBm で「丘と平地が混在する」自然な地形を生成する。 // Frequency 0.004: 1000ブロックで地形が大きく変化する(バイオームスケール) // Octaves 5: 大きなうねり + 小さな凹凸の5層を重ねる - auto* hn = (FastNoiseLite*)height_noise_; - hn->SetSeed((int)seed); - hn->SetNoiseType(FastNoiseLite::NoiseType_Perlin); - hn->SetFrequency(0.004f); - hn->SetFractalType(FastNoiseLite::FractalType_FBm); - hn->SetFractalOctaves(5); - hn->SetFractalLacunarity(2.0f); - hn->SetFractalGain(0.5f); + height_.configure(seed, 0.004f, 5, /*ridged=*/false); // ── 谷ノイズ ── 山脈と谷を切り立てる ───────────────────────────────── // Ridged FBm: 谷底が鋭く、山頂が丸い「稜線」状の形状を作る。 - // (FBm とは逆に、値が0に近いところが峰となる) - auto* vn = (FastNoiseLite*)valley_noise_; - vn->SetSeed((int)(seed ^ 0xCAFEBABEu)); - vn->SetNoiseType(FastNoiseLite::NoiseType_Perlin); - vn->SetFractalType(FastNoiseLite::FractalType_Ridged); - vn->SetFrequency(0.007f); - vn->SetFractalOctaves(3); - vn->SetFractalLacunarity(2.0f); - vn->SetFractalGain(0.5f); + valley_.configure(seed ^ 0xCAFEBABEu, 0.007f, 3, /*ridged=*/true); // ── 通常洞窟ノイズ ── 斜め方向に伸びる空洞を作る ───────────────────── // Frequency 0.020: 0.025 より低くして洞窟スケールを広げる // getCave() 内で Y 依存の XZ オフセットを加え、斜め方向のトンネルにする - // 閾値は terrain_gen.cpp 側で 0.58 に設定 - auto* cn = (FastNoiseLite*)cave_noise_; - cn->SetSeed((int)(seed ^ 0xDEADBEEFu)); - cn->SetNoiseType(FastNoiseLite::NoiseType_Perlin); - cn->SetFrequency(0.020f); + cave_.configure(seed ^ 0xDEADBEEFu, 0.020f, 1, /*ridged=*/false); // ── 横長洞窟ノイズ ── Y方向を圧縮しつつ斜め方向に伸ばす ────────────── - // getCaveHoriz() 内で Y に 3.5 を掛けて Y 幅を絞り、 + // getCaveHoriz() 内で Y に 2.0 を掛けて Y 幅を絞り、 // さらに Y 依存の XZ オフセットで斜め方向の平たいトンネルにする - auto* chn = (FastNoiseLite*)cave_horiz_noise_; - chn->SetSeed((int)(seed ^ 0xBEEFDEADu)); - chn->SetNoiseType(FastNoiseLite::NoiseType_Perlin); - chn->SetFrequency(0.020f); + cave_horiz_.configure(seed ^ 0xBEEFDEADu, 0.020f, 1, /*ridged=*/false); // ── 気温ノイズ ── バイオームの大きな区分けを作る ──────────────────────── - // 周波数 0.0020 = 約500ブロックで1サイクル → 各バイオーム帯 ~167 ブロック幅 // ※ Perlin ノイズは整数格子点で必ず 0 を返すため、原点付近は常に Warm になる。 // getTemperature() 内でオフセットを加えてスポーン地点を格子点から外す。 - auto* tn = (FastNoiseLite*)temp_noise_; - tn->SetSeed((int)(seed ^ 0xABCD1234u)); - tn->SetNoiseType(FastNoiseLite::NoiseType_Perlin); - tn->SetFrequency(0.0009f); - tn->SetFractalType(FastNoiseLite::FractalType_FBm); - tn->SetFractalOctaves(2); + temp_.configure(seed ^ 0xABCD1234u, 0.0009f, 2, /*ridged=*/false); // ── 湿度ノイズ ── 気温と組み合わせてバイオームを決める ────────────────── // 気温と少し違う周波数(0.0010)にすることでグリッド状の境界線が出にくくなる - auto* hun = (FastNoiseLite*)humid_noise_; - hun->SetSeed((int)(seed ^ 0x5678EFABu)); - hun->SetNoiseType(FastNoiseLite::NoiseType_Perlin); - hun->SetFrequency(0.0010f); - hun->SetFractalType(FastNoiseLite::FractalType_FBm); - hun->SetFractalOctaves(2); + humid_.configure(seed ^ 0x5678EFABu, 0.0010f, 2, /*ridged=*/false); // ── バリエーションノイズ ── 境界を自然にずらす ───────────────────────── - auto* varN = (FastNoiseLite*)variation_noise_; - varN->SetSeed((int)(seed ^ 0x1234ABCDu)); - varN->SetNoiseType(FastNoiseLite::NoiseType_Perlin); - varN->SetFrequency(0.0009f); - varN->SetFractalType(FastNoiseLite::FractalType_FBm); - varN->SetFractalOctaves(2); + variation_.configure(seed ^ 0x1234ABCDu, 0.0009f, 2, /*ridged=*/false); // ── シード依存サンプリングオフセット ──────────────────────────────────── // Perlin ノイズは整数格子点(ここでは freq×座標 が整数)で必ず 0 を返す。 @@ -143,39 +253,39 @@ void NoiseGen::setSeed(uint32_t seed) { // 地形の高さノイズ(2D: X と Z で変化) float NoiseGen::getHeight(float x, float z) const { - return ((FastNoiseLite*)height_noise_)->GetNoise(x, z); + return height_.sample2(x, z); } // 谷・山脈ノイズ(2D) float NoiseGen::getValley(float x, float z) const { - return ((FastNoiseLite*)valley_noise_)->GetNoise(x, z); + return valley_.sample2(x, z); } // 斜め洞窟ノイズ(3D: Y に比例した XZ オフセットで斜め方向のトンネルを作る) float NoiseGen::getCave(float x, float y, float z) const { // y が深くなるほど XZ がずれる → 斜め方向に伸びる洞窟 // y * 0.5 で Y 方向を若干圧縮し、横への広がりを強調 - return ((FastNoiseLite*)cave_noise_)->GetNoise(x + y * 0.40f, y * 0.50f, z + y * 0.25f); + return cave_.sample3(x + y * 0.40f, y * 0.50f, z + y * 0.25f); } // 斜め横長洞窟ノイズ(3D: Y 圧縮 + 斜めオフセット) // y * 2.0 で縦幅を絞る(y * 3.5 では天井2ブロック未満になりすぎる) // スパゲッティ式 n1²+n2²GetNoise(x + y * 0.30f, y * 2.0f, z - y * 0.20f); + return cave_horiz_.sample3(x + y * 0.30f, y * 2.0f, z - y * 0.20f); } // バイオーム気温ノイズ(2D) float NoiseGen::getTemperature(float x, float z) const { - return ((FastNoiseLite*)temp_noise_)->GetNoise(x + temp_ox_, z + temp_oz_); + return temp_.sample2(x + temp_ox_, z + temp_oz_); } // バイオーム湿度ノイズ(2D) float NoiseGen::getHumidity(float x, float z) const { - return ((FastNoiseLite*)humid_noise_)->GetNoise(x + humid_ox_, z + humid_oz_); + return humid_.sample2(x + humid_ox_, z + humid_oz_); } // バイオームバリエーションノイズ(2D): 高値=Mountain, 低値=Canyon float NoiseGen::getVariation(float x, float z) const { - return ((FastNoiseLite*)variation_noise_)->GetNoise(x, z); + return variation_.sample2(x, z); } diff --git a/srcs/world/noise.hpp b/srcs/world/noise.hpp index eeb346a..2195595 100644 --- a/srcs/world/noise.hpp +++ b/srcs/world/noise.hpp @@ -1,17 +1,25 @@ #pragma once #include +// ───────────────────────────────────────────────────────────────────────────── +// NoiseGen — 完全自作の Perlin ノイズ生成器(外部ライブラリ不使用) +// +// subject の General Instructions: +// "Using pre-built libraries for terrain or biome generation is strictly +// forbidden. You must implement everything from scratch." +// に従い、勾配ノイズ・フラクタル合成をすべてこのクラス内で実装する。 +// 実装本体は noise.cpp を参照。 +// ───────────────────────────────────────────────────────────────────────────── class NoiseGen { public: - NoiseGen(); - ~NoiseGen(); + NoiseGen() = default; void setSeed(uint32_t seed); // 2D FBm Perlin — base terrain height [-1, 1] float getHeight(float x, float z) const; - // 2D Ridged — valley / erosion mask [0, ~1] (high = deep valley) + // 2D Ridged — valley / erosion mask(高値 = 深い谷) float getValley(float x, float z) const; // 3D Perlin — cave carving (diagonal tunnels) [-1, 1] @@ -30,13 +38,30 @@ class NoiseGen { float getVariation(float x, float z) const; private: - void* height_noise_ = nullptr; - void* valley_noise_ = nullptr; - void* cave_noise_ = nullptr; - void* cave_horiz_noise_ = nullptr; - void* temp_noise_ = nullptr; - void* humid_noise_ = nullptr; - void* variation_noise_ = nullptr; + // 1系統分のノイズ設定(シード・周波数・フラクタル合成パラメータ)。 + // sample2 / sample3 は状態を変更しない純粋関数なので + // ワーカースレッドから並列に呼んでも安全。 + struct Perlin { + uint32_t seed = 0; + float frequency = 0.01f; + int octaves = 1; // 1 = フラクタル合成なし(単一オクターブ) + float lacunarity = 2.0f; // オクターブごとの周波数倍率 + float gain = 0.5f; // オクターブごとの振幅倍率 + bool ridged = false; // true = Ridged(稜線型)合成 + float bounding = 1.0f; // FBm 合成結果を [-1,1] に収める正規化係数 + + void configure(uint32_t s, float freq, int oct, bool ridged_mode); + float sample2(float x, float y) const; + float sample3(float x, float y, float z) const; + }; + + Perlin height_; + Perlin valley_; + Perlin cave_; + Perlin cave_horiz_; + Perlin temp_; + Perlin humid_; + Perlin variation_; // シード依存のサンプリングオフセット(Perlin格子点問題の回避) float temp_ox_ = 0.0f, temp_oz_ = 0.0f;