From 272a8149df916e22faa0ccd8b7b1917493866fd7 Mon Sep 17 00:00:00 2001 From: Ishi_eenn Date: Sat, 6 Jun 2026 17:29:55 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=89=E3=83=A9=E3=82=B4=E3=83=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + CMakeLists.txt | 1 + assets/shaders/chunk.vert | 19 ++- include/types.hpp | 4 +- srcs/core/engine.cpp | 128 +++++++++++++-- srcs/mob/arrow_manager.cpp | 16 +- srcs/mob/arrow_manager.hpp | 8 +- srcs/mob/dragon_manager.cpp | 198 +++++++++++++++++++++++ srcs/mob/dragon_manager.hpp | 83 ++++++++++ srcs/mob/ender_dragon.hpp | 46 ++++++ srcs/network/client.cpp | 30 ++++ srcs/network/client.hpp | 18 ++- srcs/network/packet.hpp | 19 +++ srcs/network/server.cpp | 10 ++ srcs/renderer/renderer.cpp | 267 +++++++++++++++++++++++++++++++- srcs/renderer/renderer.hpp | 4 + srcs/renderer/texture_atlas.cpp | 38 +++++ 17 files changed, 862 insertions(+), 28 deletions(-) create mode 100644 srcs/mob/dragon_manager.cpp create mode 100644 srcs/mob/dragon_manager.hpp create mode 100644 srcs/mob/ender_dragon.hpp diff --git a/.gitignore b/.gitignore index 6d2d5d5..81f8f1e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ build/ objs/ vendor/ ft_vox +ft_minecraft *.o *.d .DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index fcbf6d1..f697f11 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,6 +88,7 @@ add_executable( srcs/network/client.cpp srcs/mob/mob_manager.cpp srcs/mob/arrow_manager.cpp + srcs/mob/dragon_manager.cpp ) target_include_directories( diff --git a/assets/shaders/chunk.vert b/assets/shaders/chunk.vert index 32e662e..c91104d 100644 --- a/assets/shaders/chunk.vert +++ b/assets/shaders/chunk.vert @@ -48,13 +48,20 @@ void main() { vec3 world_pos = (uModel * vec4(aPos, 1.0)).xyz; // ── 松明光源: 全松明からの寄与を線形フォールオフで加算 ── - // 各光源につき max(0, 1 - dist/range) を加算。複数松明が重なれば明るくなる。 + // 最適化: + // 1) 上限を uTorchCount にしてシェーダコンパイラの完全アンロールを防ぐ。 + // 2) まず squared distance で早期スキップ (sqrt 回避)。 + // 3) 範囲内の松明だけ sqrt 1 回 + 線形フォールオフを評価。 float torch_accum = 0.0; - for (int i = 0; i < 16; ++i) { - if (i >= uTorchCount) break; - float d = distance(world_pos, uTorches[i]); - float a = max(0.0, 1.0 - d / uTorchRange); - torch_accum += a * a; // 2乗フォールオフでなめらか + float inv_range = 1.0 / uTorchRange; + float range_sq = uTorchRange * uTorchRange; + for (int i = 0; i < uTorchCount; ++i) { + vec3 delta = world_pos - uTorches[i]; + float sq = dot(delta, delta); + if (sq >= range_sq) continue; // 範囲外: sqrt を省略 + float d = sqrt(sq); + float a = 1.0 - d * inv_range; // [0..1]、ここまでで a>=0 確定 + torch_accum += a * a; // 2乗フォールオフでなめらか } // 面の向きを少し考慮 (上面は明るく、下面は暗く)。完全な点光源では物足りないので // top/side/bottom の face_shade を再利用してメリハリを付ける。 diff --git a/include/types.hpp b/include/types.hpp index 5b0c1d7..fd3ef4e 100644 --- a/include/types.hpp +++ b/include/types.hpp @@ -26,13 +26,15 @@ enum class BlockType : uint8_t { Bow = 18, // 弓(アイテム: 設置不可・スタック上限1) Stick = 19, // 棒(アイテム: 設置不可・クラフト素材) Torch = 20, // 松明(設置可: 細い柱型 3D メッシュ) + DragonEgg = 21, // ドラゴンエッグ(アイテム: 右クリックでエンダードラゴンを召喚) COUNT }; // ブロックではなく "アイテム" として扱う BlockType を判定する。 // アイテムは右クリックで設置されず、種別固有の挙動を発火する。 inline bool isItem(BlockType t) { - return t == BlockType::Bow || t == BlockType::Stick; + return t == BlockType::Bow || t == BlockType::Stick || + t == BlockType::DragonEgg; } enum class Face : uint8_t { diff --git a/srcs/core/engine.cpp b/srcs/core/engine.cpp index becca1b..70fab00 100644 --- a/srcs/core/engine.cpp +++ b/srcs/core/engine.cpp @@ -34,6 +34,7 @@ #include "network/client.hpp" #include "mob/mob_manager.hpp" #include "mob/arrow_manager.hpp" +#include "mob/dragon_manager.hpp" #include #include @@ -212,10 +213,12 @@ struct Engine::Impl { bool multiplayer = false; float net_pos_timer = 0.0f; float mob_sync_timer = 0.0f; // ホスト用: mob送信インターバル + float dragon_sync_timer = 0.0f; // ホスト用: dragon送信インターバル // Mobs MobManager mob_mgr; ArrowManager arrow_mgr; + DragonManager dragon_mgr; float bow_charge_time = 0.0f; // 右クリック保持時間(秒) bool bow_charging = false; float player_health = 20.0f; @@ -329,6 +332,9 @@ bool Engine::init(uint32_t seed, int width, int height) { impl_->player.init(window_); impl_->player.camera().setAspect((float)width_ / (float)height_); + // ── 初期ホットバー: テスト用に DragonEgg を 1 個入れておく ────────────── + inventoryAdd(impl_->inventory, BlockType::DragonEgg, 1); + // ── チャンクマネージャーの初期化 ─────────────────────────────────────────── impl_->chunk_mgr = new ChunkManager(impl_->world, impl_->renderer); @@ -642,8 +648,13 @@ void Engine::run() { hit.bx, hit.by, hit.bz, static_cast(BlockType::Air)); } else { - impl_->mob_mgr.playerMeleeAttack( - pos.x, pos.y, pos.z, front.x, front.z); + // ドラゴンを優先 (より遠くから当たる)。当たらなければゾンビ近接。 + bool dragon_hit = impl_->dragon_mgr.playerMeleeAttack( + pos.x, pos.y, pos.z, front.x, front.y, front.z); + if (!dragon_hit) { + impl_->mob_mgr.playerMeleeAttack( + pos.x, pos.y, pos.z, front.x, front.z); + } } } @@ -681,18 +692,35 @@ void Engine::run() { impl_->bow_charging = false; impl_->bow_charge_time = 0.0f; - // 右クリック: 選択スロットのブロックを設置(在庫があるときのみ) - if (hit.hit && inp.wasRightClicked()) { - BlockType to_place = held; - if (to_place != BlockType::Air && !isItem(to_place) && - inventoryConsumeSelected(inv)) { - impl_->world.setWorldBlock(hit.nx, hit.ny, hit.nz, - to_place); - rebuildModified(hit.nx, hit.nz, *impl_->chunk_mgr); - if (impl_->multiplayer) - impl_->net_client.sendBlockChange( - hit.nx, hit.ny, hit.nz, - static_cast(to_place)); + // 右クリック: 選択スロットのブロックを設置 or アイテム発火 + if (inp.wasRightClicked()) { + // DragonEgg: 上空にエンダードラゴンを召喚 + if (held == BlockType::DragonEgg) { + if (!impl_->dragon_mgr.exists() && + inventoryConsumeSelected(inv)) { + impl_->dragon_mgr.spawn(pos.x, pos.y, pos.z); + // マルチプレイ時は他クライアントにも知らせる。 + if (impl_->multiplayer) { + PktDragonSpawn pkt; + pkt.x = pos.x; pkt.y = pos.y; pkt.z = pos.z; + impl_->net_client.sendRaw( + PacketType::DragonSpawn, + &pkt, sizeof(pkt)); + } + } + } else if (hit.hit) { + // 通常のブロック設置(在庫があるときのみ) + BlockType to_place = held; + if (to_place != BlockType::Air && !isItem(to_place) && + inventoryConsumeSelected(inv)) { + impl_->world.setWorldBlock(hit.nx, hit.ny, hit.nz, + to_place); + rebuildModified(hit.nx, hit.nz, *impl_->chunk_mgr); + if (impl_->multiplayer) + impl_->net_client.sendBlockChange( + hit.nx, hit.ny, hit.nz, + static_cast(to_place)); + } } } } @@ -749,6 +777,28 @@ 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::DragonSpawn) { + // 誰かが召喚した。自分のローカルにまだ無ければ生成する。 + if (!impl_->dragon_mgr.exists()) { + impl_->dragon_mgr.spawn(ev.dragon_spawn_x, + ev.dragon_spawn_y, + ev.dragon_spawn_z); + } + } else if (ev.kind == NetworkEvent::Kind::DragonUpdate) { + // ホストからの権威的な状態。非ホストのみ適用する。 + if (!is_mob_host) { + DragonManager::NetState st; + st.exists = ev.dragon_exists != 0; + st.state = static_cast(ev.dragon_state); + st.x = ev.dragon_x; + st.y = ev.dragon_y; + st.z = ev.dragon_z; + st.yaw = ev.dragon_yaw; + st.pitch = ev.dragon_pitch; + st.wing_phase = ev.dragon_wing_phase; + st.health = ev.dragon_health; + impl_->dragon_mgr.applyNetState(st); + } } } // Update walking animation phase for each remote player. @@ -822,8 +872,42 @@ void Engine::run() { // ── 矢の物理更新(命中判定はホストのみ実行) ───────────────────────── // 非ホストではモブ状態はサーバーから配信されるため矢の更新は行わない(MVP)。 if (is_mob_host) { - impl_->arrow_mgr.update(dt, isSolid, impl_->mob_mgr.zombiesMut()); + impl_->arrow_mgr.update(dt, isSolid, impl_->mob_mgr.zombiesMut(), + &impl_->dragon_mgr); + } + + // ── エンダードラゴン更新と同期 ────────────────────────────────────── + // ホスト: AI + 接触ダメージ判定 + 状態を 10Hz でブロードキャスト + // 非ホスト: AI を回さず、受信した状態で上書きされるのを待つ + // (受信間 0.1秒はローカル update を回しておくと滑らかになる) + if (is_mob_host) { + const float dragon_dmg = impl_->dragon_mgr.update( + dt, ppos.x, ppos.y, ppos.z); + if (dragon_dmg > 0.0f) + applyPlayerDamage(dragon_dmg, "dragon"); + + if (impl_->multiplayer) { + impl_->dragon_sync_timer -= dt; + if (impl_->dragon_sync_timer <= 0.0f) { + impl_->dragon_sync_timer = 0.1f; // 10 Hz + DragonManager::NetState s = impl_->dragon_mgr.snapshot(); + PktDragonState pkt; + pkt.exists = s.exists ? 1u : 0u; + pkt.state = static_cast(s.state); + pkt.x = s.x; + pkt.y = s.y; + pkt.z = s.z; + pkt.yaw = s.yaw; + pkt.pitch = s.pitch; + pkt.wing_phase = s.wing_phase; + pkt.health = s.health; + impl_->net_client.sendRaw(PacketType::DragonUpdate, + &pkt, sizeof(pkt)); + } + } } + // 非ホストはローカル AI を回さない (受信ステートで上書き)。 + // Phase C MVP では 10Hz の snap、補間は今後検討。 // ── チャンクのストリーミング ───────────────────────────────────────── // プレイヤーの周囲のチャンクを読み込み、遠いチャンクを破棄する。 @@ -946,6 +1030,12 @@ void Engine::run() { // 矢を描画 impl_->renderer.drawArrows(impl_->arrow_mgr.arrows(), view4x4, proj4x4); + // エンダードラゴンを描画(存在する場合のみ) + if (impl_->dragon_mgr.exists()) { + impl_->renderer.drawDragon(*impl_->dragon_mgr.dragon(), + view4x4, proj4x4); + } + // 3D 雲レイヤーを描画(不透明ブロックの後、水の前) impl_->renderer.drawClouds(view4x4, proj4x4, ppos.x, ppos.z, elapsed_s); @@ -980,6 +1070,14 @@ void Engine::run() { impl_->player_health, impl_->player_max_health); + // エンダードラゴンの HP バー(存在し、まだ戦闘中の場合のみ) + if (impl_->dragon_mgr.exists()) { + const EnderDragon* dr = impl_->dragon_mgr.dragon(); + if (dr && dr->state != EnderDragon::State::Dying) { + impl_->renderer.drawBossBar(dr->health, DRAGON_MAX_HEALTH); + } + } + if (impl_->show_stats_) { const char* biome_name = impl_->world.getBiomeNameAt(ppos.x, ppos.z); impl_->renderer.drawStats( diff --git a/srcs/mob/arrow_manager.cpp b/srcs/mob/arrow_manager.cpp index a0b0faf..3b1ae5f 100644 --- a/srcs/mob/arrow_manager.cpp +++ b/srcs/mob/arrow_manager.cpp @@ -1,4 +1,5 @@ #include "mob/arrow_manager.hpp" +#include "mob/dragon_manager.hpp" #include #include @@ -16,7 +17,8 @@ void ArrowManager::spawn(float x, float y, float z, void ArrowManager::update(float dt, const std::function& isSolid, - std::vector& zombies) { + std::vector& zombies, + DragonManager* dragon_mgr) { if (arrows_.empty()) return; const float sub_dt = dt / static_cast(SUBSTEPS); @@ -59,6 +61,18 @@ void ArrowManager::update(float dt, break; } + // ドラゴン命中判定(胴体軸カプセル、ゾンビより優先) + if (dragon_mgr && dragon_mgr->exists()) { + const EnderDragon* dr = dragon_mgr->dragon(); + if (dr && dr->state != EnderDragon::State::Dying) { + if (dragon_mgr->distanceToBody(nx, ny, nz) < DRAGON_HIT_RADIUS) { + dragon_mgr->applyDamage(DRAGON_ARROW_DAMAGE); + a.alive = false; + break; + } + } + } + // ゾンビ命中判定(球状) int hit_idx = -1; float best_d2 = HIT_RADIUS * HIT_RADIUS; diff --git a/srcs/mob/arrow_manager.hpp b/srcs/mob/arrow_manager.hpp index 34c455d..57fd6cb 100644 --- a/srcs/mob/arrow_manager.hpp +++ b/srcs/mob/arrow_manager.hpp @@ -4,15 +4,19 @@ #include #include +class DragonManager; + class ArrowManager { public: void spawn(float x, float y, float z, float vx, float vy, float vz); - // 全矢を更新。命中したゾンビにダメージを与える。 + // 全矢を更新。命中したゾンビ・ドラゴンにダメージを与える。 + // dragon_mgr は nullptr 可 (ドラゴン未召喚時)。 void update(float dt, const std::function& isSolid, - std::vector& zombies); + std::vector& zombies, + DragonManager* dragon_mgr = nullptr); const std::vector& arrows() const { return arrows_; } diff --git a/srcs/mob/dragon_manager.cpp b/srcs/mob/dragon_manager.cpp new file mode 100644 index 0000000..5597833 --- /dev/null +++ b/srcs/mob/dragon_manager.cpp @@ -0,0 +1,198 @@ +// ───────────────────────────────────────────────────────────────────────────── +// dragon_manager.cpp — Phase A (旋回飛行) + Phase B (戦闘ロジック) +// ───────────────────────────────────────────────────────────────────────────── +#include "mob/dragon_manager.hpp" +#include +#include +#include + +void DragonManager::spawn(float wx, float wy, float wz) { + if (dragon_.has_value()) return; + + EnderDragon d{}; + d.spawn_x = wx; + d.spawn_y = wy + kSpawnHeightOffset; + d.spawn_z = wz; + // 初期位置: 旋回円周上の角度 0 + d.x = d.spawn_x + kOrbitRadius; + d.y = d.spawn_y; + d.z = d.spawn_z; + d.yaw = 90.0f; // 旋回の接線方向 (+Z) を向く + d.pitch = 0.0f; + d.orbit_phase = 0.0f; + d.health = DRAGON_MAX_HEALTH; + d.alive = true; + d.state = EnderDragon::State::Patrol; + dragon_.emplace(d); +} + +void DragonManager::despawn() { + dragon_.reset(); +} + +float DragonManager::update(float dt, float px, float py, float pz) { + if (!dragon_.has_value()) return 0.0f; + EnderDragon& d = *dragon_; + + d.life_timer += dt; + d.wing_phase += kWingFlapSpeed * dt; + if (d.hit_flash_timer > 0.0f) d.hit_flash_timer -= dt; + + // ── 死亡演出: ゆっくり上昇 → DRAGON_DYING_DURATION 後に消える ──────────── + if (d.state == EnderDragon::State::Dying) { + d.dying_timer += dt; + d.y += 2.0f * dt; // 2 ブロック/秒で上昇 + d.pitch = 45.0f; // 機首を上げて昇天感 + if (d.dying_timer >= DRAGON_DYING_DURATION) { + dragon_.reset(); + } + return 0.0f; // 死んでいる間は接触ダメージなし + } + + // ── 旋回飛行 ──────────────────────────────────────────────────────────── + d.orbit_phase += kOrbitSpeed * dt; + if (d.orbit_phase > 6.2831853f) d.orbit_phase -= 6.2831853f; + + const float new_x = d.spawn_x + std::cos(d.orbit_phase) * kOrbitRadius; + const float new_z = d.spawn_z + std::sin(d.orbit_phase) * kOrbitRadius; + const float new_y = d.spawn_y + std::sin(d.orbit_phase * 0.5f) * 1.5f; + + d.vx = (new_x - d.x) / dt; + d.vy = (new_y - d.y) / dt; + d.vz = (new_z - d.z) / dt; + + d.x = new_x; + d.y = new_y; + d.z = new_z; + + d.yaw = std::atan2(d.vz, d.vx) * (180.0f / 3.1415926535f); + const float horiz = std::sqrt(d.vx * d.vx + d.vz * d.vz); + d.pitch = std::atan2(d.vy, horiz) * (180.0f / 3.1415926535f); + + // ── 接触ダメージ判定 ─────────────────────────────────────────────────── + // 胴体軸 (頭〜尾) に沿った最近接点までの距離で判定する。 + if (distanceToBody(px, py, pz) < DRAGON_CONTACT_RADIUS) { + return DRAGON_CONTACT_DAMAGE_PER_SEC * dt; + } + return 0.0f; +} + +float DragonManager::distanceToBody(float px, float py, float pz) const { + if (!dragon_.has_value()) return std::numeric_limits::max(); + const EnderDragon& d = *dragon_; + + // 胴体軸 = world XZ 平面で yaw 方向に伸びる線分。 + // 軸範囲: along ∈ [-DRAGON_AXIS_BACK, +DRAGON_AXIS_FRONT] + const float yaw_rad = d.yaw * (3.1415926535f / 180.0f); + const float fx = std::cos(yaw_rad); + const float fz = std::sin(yaw_rad); + + const float ex = px - d.x; + const float ez = pz - d.z; + float along = ex * fx + ez * fz; + along = std::clamp(along, -DRAGON_AXIS_BACK, DRAGON_AXIS_FRONT); + + const float cx = d.x + fx * along; // 軸上の最近接点 + const float cz = d.z + fz * along; + const float dx = px - cx; + const float dy = py - d.y; // 軸は水平とみなす + const float dz = pz - cz; + return std::sqrt(dx * dx + dy * dy + dz * dz); +} + +void DragonManager::applyDamage(float amount) { + if (!dragon_.has_value()) return; + EnderDragon& d = *dragon_; + if (d.state == EnderDragon::State::Dying) return; + d.health -= amount; + d.hit_flash_timer = 0.15f; + if (d.health <= 0.0f) { + d.health = 0.0f; + d.state = EnderDragon::State::Dying; + d.dying_timer = 0.0f; + // 上昇開始のため水平速度をゼロ化 + d.vx = 0.0f; + d.vz = 0.0f; + } +} + +DragonManager::NetState DragonManager::snapshot() const { + NetState s; + if (!dragon_.has_value()) return s; + const EnderDragon& d = *dragon_; + s.exists = true; + s.state = d.state; + s.x = d.x; + s.y = d.y; + s.z = d.z; + s.yaw = d.yaw; + s.pitch = d.pitch; + s.wing_phase = d.wing_phase; + s.health = d.health; + return s; +} + +void DragonManager::applyNetState(const NetState& s) { + if (!s.exists) { + dragon_.reset(); + return; + } + // 必要なら "空の" ドラゴンを生成 (spawn_x/y/z は推測値を入れておく)。 + if (!dragon_.has_value()) { + EnderDragon d{}; + d.spawn_x = s.x; + d.spawn_y = s.y; + d.spawn_z = s.z; + d.alive = true; + dragon_.emplace(d); + } + EnderDragon& d = *dragon_; + d.state = s.state; + d.x = s.x; + d.y = s.y; + d.z = s.z; + d.yaw = s.yaw; + d.pitch = s.pitch; + d.wing_phase = s.wing_phase; + d.health = s.health; +} + +bool DragonManager::playerMeleeAttack(float px, float py, float pz, + float front_x, float front_y, float front_z) { + if (!dragon_.has_value()) return false; + EnderDragon& d = *dragon_; + if (d.state == EnderDragon::State::Dying) return false; + + // 胴体軸上の最近接点を再計算してそこへの方向で判定する。 + // (distanceToBody は距離だけ返すので、ここでは座標計算をローカルで実施) + const float yaw_rad = d.yaw * (3.1415926535f / 180.0f); + const float fx = std::cos(yaw_rad); + const float fz = std::sin(yaw_rad); + const float ex = px - d.x; + const float ez = pz - d.z; + float along = ex * fx + ez * fz; + along = std::clamp(along, -DRAGON_AXIS_BACK, DRAGON_AXIS_FRONT); + const float cx = d.x + fx * along; + const float cz = d.z + fz * along; + + // 軸上の最近接点へのベクトル + const float dx = cx - px; + const float dy = d.y - py; + const float dz = cz - pz; + const float dist = std::sqrt(dx*dx + dy*dy + dz*dz); + // リーチ = プレイヤーの腕の長さ + ドラゴンの胴体厚み (HIT_RADIUS) + if (dist > DRAGON_MELEE_RANGE + DRAGON_HIT_RADIUS) return false; + + if (dist < 1e-3f) { + // 体に埋まっている: 前方判定不要で強制ヒット + applyDamage(DRAGON_MELEE_DAMAGE); + return true; + } + // 視線方向との内積で「前方半球」判定 (dot > 0.5 ≒ 60°円錐) + const float inv = 1.0f / dist; + const float dot = (dx * front_x + dy * front_y + dz * front_z) * inv; + if (dot < 0.5f) return false; + + applyDamage(DRAGON_MELEE_DAMAGE); + return true; +} diff --git a/srcs/mob/dragon_manager.hpp b/srcs/mob/dragon_manager.hpp new file mode 100644 index 0000000..ce674b9 --- /dev/null +++ b/srcs/mob/dragon_manager.hpp @@ -0,0 +1,83 @@ +#pragma once +#include "mob/ender_dragon.hpp" + +// エンダードラゴンを最大 1 体だけ所有・管理するクラス。 +// +// Phase A の責務: +// ・召喚 (spawn) と消滅 (despawn) +// ・基本飛行 AI (召喚地点の周りを高所で旋回) +// ・羽ばたきフェーズの更新 +// +// 戦闘 (HP・矢ダメージ・接触ダメージ) は Phase B、 +// ネットワーク同期は Phase C で追加する。 +class DragonManager { +public: + // ドラゴンを指定地点の上空 spawn_height_offset ブロックに召喚する。 + // 既に存在する場合は何もしない (シングルトン)。 + void spawn(float wx, float wy, float wz); + + // 任意呼び出しで強制消滅。 + void despawn(); + + // 毎フレーム呼び出し。世界の衝突判定は不要 (空中飛行のため)。 + // player_x/y/z はカメラ位置。プレイヤーが接触範囲にいたら接触ダメージを返す + // (return: このフレーム中にプレイヤーへ与えたダメージ)。 + float update(float dt, float player_x, float player_y, float player_z); + + // 矢・近接からダメージを与える。HP が 0 を切ったら Dying に遷移。 + void applyDamage(float amount); + + // プレイヤーが視線方向に近接攻撃したときの命中判定。 + // ヒットしたら DRAGON_MELEE_DAMAGE を与える。戻り値: ヒットしたかどうか。 + bool playerMeleeAttack(float px, float py, float pz, + float front_x, float front_y, float front_z); + + // 点 (px,py,pz) からドラゴン胴体軸の最近接点までの距離 (ブロック)。 + // ドラゴンが存在しないときは float の最大値相当を返す。 + // 軸は yaw 方向に水平に伸びるとみなす (pitch は近似のため無視)。 + float distanceToBody(float px, float py, float pz) const; + + // ── マルチプレイヤー同期用 ──────────────────────────────────────────── + // ホスト側がブロードキャストに使う「読み取り」: 現状の状態を packet 用に取り出す。 + // exists=false ならドラゴン不在。 + struct NetState { + bool exists = false; + EnderDragon::State state = EnderDragon::State::Patrol; + float x = 0, y = 0, z = 0; + float yaw = 0, pitch = 0; + float wing_phase = 0; + float health = 0; + }; + NetState snapshot() const; + + // 非ホスト側が受信した state を流し込む。 + // exists=false なら despawn。true なら必要に応じて spawn してから上書き。 + void applyNetState(const NetState& st); + + bool exists() const { return dragon_.has_value(); } + const EnderDragon* dragon() const { + return dragon_.has_value() ? &(*dragon_) : nullptr; + } + EnderDragon* dragonMut() { + return dragon_.has_value() ? &(*dragon_) : nullptr; + } + +private: + // std::optional 相当の手書き実装 (C++17 で は使えるが + // 既存コードと揃えるため最小限の包装)。 + struct Maybe { + EnderDragon value; + bool present = false; + bool has_value() const { return present; } + EnderDragon& operator*() { return value; } + const EnderDragon& operator*() const { return value; } + void reset() { present = false; value = EnderDragon{}; } + void emplace(const EnderDragon& v) { value = v; present = true; } + }; + Maybe dragon_; + + 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) +}; diff --git a/srcs/mob/ender_dragon.hpp b/srcs/mob/ender_dragon.hpp new file mode 100644 index 0000000..d27d7c7 --- /dev/null +++ b/srcs/mob/ender_dragon.hpp @@ -0,0 +1,46 @@ +#pragma once +#include + +// エンダードラゴン (ボス相当のシングルトンモブ) +// +// ゾンビ等と違い飛行するため、yaw に加えて pitch を持つ。 +// 位置は「ドラゴンの胴体中心」(足元ではない)。 +struct EnderDragon { + float x = 0, y = 0, z = 0; // 胴体中心 + float vx = 0, vy = 0, vz = 0; // 速度(ブロック/秒) + + float yaw = 0; // 水平向き(度。Camera と同じ慣習) + float pitch = 0; // 上下角(度。+で機首上げ) + + float spawn_x = 0, spawn_y = 0, spawn_z = 0; // 旋回中心 + float orbit_phase = 0.0f; // 旋回位相 (rad) + + float health = 100.0f; + bool alive = true; + + enum class State : uint8_t { Patrol, Dying } 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; // 被弾フラッシュの残り秒数 (赤くする) +}; + +// ドラゴンの判定値定数 (DragonManager / ArrowManager 双方で参照する) +// +// ドラゴンは「胴体中心 → 頭の先」「胴体中心 → 尾の先」が大きく非対称な細長い形。 +// 球で判定すると頭・尾が外れるので、yaw 方向の軸に沿った "カプセル" で判定する。 +// 胴体軸は world XZ 平面に水平。点 P と軸の最近接点の距離 d を計算し、 +// d <= 半径 なら命中とみなす。 +constexpr float DRAGON_MAX_HEALTH = 100.0f; + +// 胴体軸の中心からの伸び (kModelScale=2.0 を前提) +constexpr float DRAGON_AXIS_FRONT = 9.0f; // 中心から頭先まで +constexpr float DRAGON_AXIS_BACK = 12.0f; // 中心から尾先まで + +constexpr float DRAGON_CONTACT_RADIUS = 3.5f; // 接触ダメージ判定 (体に触れているか) +constexpr float DRAGON_HIT_RADIUS = 5.0f; // 矢の命中判定 (やや甘め) +constexpr float DRAGON_CONTACT_DAMAGE_PER_SEC = 4.0f; +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 → 自動消滅 diff --git a/srcs/network/client.cpp b/srcs/network/client.cpp index 579344a..e07c693 100644 --- a/srcs/network/client.cpp +++ b/srcs/network/client.cpp @@ -150,6 +150,36 @@ void NetworkClient::handlePacket(PacketType type, const uint8_t* payload, out.push_back(std::move(ev)); break; } + case PacketType::DragonSpawn: { + if (size < sizeof(PktDragonSpawn)) break; + PktDragonSpawn pkt; + std::memcpy(&pkt, payload, sizeof(pkt)); + NetworkEvent ev; + ev.kind = NetworkEvent::Kind::DragonSpawn; + ev.dragon_spawn_x = pkt.x; + ev.dragon_spawn_y = pkt.y; + ev.dragon_spawn_z = pkt.z; + out.push_back(ev); + break; + } + case PacketType::DragonUpdate: { + if (size < sizeof(PktDragonState)) break; + PktDragonState pkt; + std::memcpy(&pkt, payload, sizeof(pkt)); + NetworkEvent ev; + ev.kind = NetworkEvent::Kind::DragonUpdate; + ev.dragon_exists = pkt.exists; + ev.dragon_state = pkt.state; + ev.dragon_x = pkt.x; + ev.dragon_y = pkt.y; + ev.dragon_z = pkt.z; + ev.dragon_yaw = pkt.yaw; + ev.dragon_pitch = pkt.pitch; + ev.dragon_wing_phase = pkt.wing_phase; + ev.dragon_health = pkt.health; + out.push_back(ev); + break; + } default: break; } diff --git a/srcs/network/client.hpp b/srcs/network/client.hpp index e6b5b73..ef79fd3 100644 --- a/srcs/network/client.hpp +++ b/srcs/network/client.hpp @@ -21,7 +21,14 @@ struct RemotePlayer { }; struct NetworkEvent { - enum class Kind { BlockChange, PlayerDisconnect, TimeSync, MobUpdate } kind; + enum class Kind { + BlockChange, + PlayerDisconnect, + TimeSync, + MobUpdate, + DragonSpawn, + DragonUpdate, + } kind; uint8_t player_id = 0; // BlockChange fields int bx = 0, by = 0, bz = 0; @@ -30,6 +37,15 @@ struct NetworkEvent { float time_of_day = 0.0f; // MobUpdate std::vector mobs; + // DragonSpawn fields + float dragon_spawn_x = 0.0f, dragon_spawn_y = 0.0f, dragon_spawn_z = 0.0f; + // DragonUpdate fields + uint8_t dragon_exists = 0; + uint8_t dragon_state = 0; + float dragon_x = 0, dragon_y = 0, dragon_z = 0; + float dragon_yaw = 0, dragon_pitch = 0; + float dragon_wing_phase = 0; + float dragon_health = 0; }; class NetworkClient { diff --git a/srcs/network/packet.hpp b/srcs/network/packet.hpp index 318070c..94224e7 100644 --- a/srcs/network/packet.hpp +++ b/srcs/network/packet.hpp @@ -12,6 +12,8 @@ enum class PacketType : uint8_t { BlockChange = 5, // C→S; S→C broadcast (excluding sender) TimeSync = 6, // S→C: server time_of_day broadcast 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: ドラゴンの状態 }; #pragma pack(push, 1) @@ -58,6 +60,23 @@ struct PktMobEntry { uint8_t state; // Zombie::State cast to uint8_t }; +// DragonSpawn: 召喚地点 (= 召喚プレイヤーの足元)。受信側でローカル DragonManager に spawn する。 +struct PktDragonSpawn { + float x, y, z; +}; + +// DragonUpdate: ホストからの権威的なドラゴン状態。 +// exists=0 のときドラゴン不在 (受信側は despawn)。 +// exists=1 のとき他フィールドを適用し、必要なら spawn してから上書きする。 +struct PktDragonState { + uint8_t exists; // 0 or 1 + uint8_t state; // EnderDragon::State + float x, y, z; + float yaw, pitch; + float wing_phase; + float health; +}; + #pragma pack(pop) static constexpr uint8_t MAX_PLAYERS = 8; diff --git a/srcs/network/server.cpp b/srcs/network/server.cpp index c7e90b8..2747b39 100644 --- a/srcs/network/server.cpp +++ b/srcs/network/server.cpp @@ -217,6 +217,16 @@ void VoxServer::handlePacket(int from_fd, PacketType type, broadcast(PacketType::MobUpdate, payload, size, from_fd); break; } + case PacketType::DragonSpawn: { + // 任意プレイヤーが召喚 → 全員に中継 (送信者を除く) + broadcast(PacketType::DragonSpawn, payload, size, from_fd); + break; + } + case PacketType::DragonUpdate: { + // ホストからのドラゴン状態を他全員に中継 + broadcast(PacketType::DragonUpdate, payload, size, from_fd); + break; + } default: break; } diff --git a/srcs/renderer/renderer.cpp b/srcs/renderer/renderer.cpp index 89940e7..f7a981b 100644 --- a/srcs/renderer/renderer.cpp +++ b/srcs/renderer/renderer.cpp @@ -1579,6 +1579,34 @@ static void setTorchUniforms(Shader& shader, int count, shader.setVec3Array("uTorches[0]", count, positions_xyz); } +// 指定チャンク (cx, cz) の AABB に対し、TORCH_RANGE 以内に届く松明だけを抽出する。 +// - グローバルの torch_positions (近傍 16 件、距離順) から +// - chunk AABB (16x16 XZ) に対する最近接距離が range 以下のものを out_xyz に詰める +// - y 方向は無視 (松明と地表の高さは大体近い + チャンクは Y=0..256 と長い) +static void cullTorchesForChunk(int cx, int cz, + int torch_count, const float* torch_positions, + float range, + float* out_xyz, int& out_count) { + const float range_sq = range * range; + const float xmin = (float)(cx * 16), xmax = xmin + 16.0f; + const float zmin = (float)(cz * 16), zmax = zmin + 16.0f; + out_count = 0; + for (int i = 0; i < torch_count; ++i) { + const float tx = torch_positions[i * 3 + 0]; + const float ty = torch_positions[i * 3 + 1]; + const float tz = torch_positions[i * 3 + 2]; + const float cx_clamped = tx < xmin ? xmin : (tx > xmax ? xmax : tx); + const float cz_clamped = tz < zmin ? zmin : (tz > zmax ? zmax : tz); + const float dx = tx - cx_clamped; + const float dz = tz - cz_clamped; + if (dx * dx + dz * dz >= range_sq) continue; + out_xyz[out_count * 3 + 0] = tx; + out_xyz[out_count * 3 + 1] = ty; + out_xyz[out_count * 3 + 2] = tz; + ++out_count; + } +} + static void setFogUniforms(Shader& shader, const float sky_horizon[3], bool underwater) { if (underwater) { @@ -1617,7 +1645,14 @@ void Renderer::drawChunk(const Chunk* chunk, const float* view4x4, const float* setChunkLightingUniforms(chunk_shader_, sun_dir_, ambient_, sun_strength_); setFogUniforms(chunk_shader_, sky_horizon_, underwater_); chunk_shader_.setFloat("uSunStrength", sun_strength_); - setTorchUniforms(chunk_shader_, torch_count_, torch_positions_, TORCH_RANGE); + { + float culled_xyz[MAX_TORCH_LIGHTS * 3]; + int culled_count = 0; + cullTorchesForChunk(chunk->pos.x, chunk->pos.z, + torch_count_, torch_positions_, TORCH_RANGE, + culled_xyz, culled_count); + setTorchUniforms(chunk_shader_, culled_count, culled_xyz, TORCH_RANGE); + } atlas_.bind(0); chunk_shader_.setInt("uAtlas", 0); @@ -1671,7 +1706,14 @@ void Renderer::drawChunkWater(const Chunk* chunk, const float* view4x4, const fl setChunkLightingUniforms(chunk_shader_, sun_dir_, ambient_, sun_strength_); setFogUniforms(chunk_shader_, sky_horizon_, underwater_); chunk_shader_.setFloat("uSunStrength", sun_strength_); - setTorchUniforms(chunk_shader_, torch_count_, torch_positions_, TORCH_RANGE); + { + float culled_xyz[MAX_TORCH_LIGHTS * 3]; + int culled_count = 0; + cullTorchesForChunk(chunk->pos.x, chunk->pos.z, + torch_count_, torch_positions_, TORCH_RANGE, + culled_xyz, culled_count); + setTorchUniforms(chunk_shader_, culled_count, culled_xyz, TORCH_RANGE); + } atlas_.bind(0); chunk_shader_.setInt("uAtlas", 0); @@ -2622,6 +2664,227 @@ void Renderer::drawArrows(const std::vector& arrows, glBindVertexArray(0); } +// ───────────────────────────────────────────────────────────────────────────── +// drawDragon() — エンダードラゴンを多パーツで描画する +// +// 構成: 頭・首・胴・尾(3)・翼(2)・脚(4) の計 11 パーツ。 +// すべて単位立方体ボックスを translate/rotate/scale で配置する。 +// +// 座標系: +// ・ドラゴンの「胴体中心」がワールド座標 (x,y,z)。 +// ・モデルローカルでは +X が前方 (頭側)、-X が後方 (尾側)、 +// ±Z が左右の翼、+Y が上面。 +// ・yaw=0 で +X 方向。Camera と同じ慣習なので yaw-90° で +Z 向きの +// デフォルトモデルを回転させる。 +// ───────────────────────────────────────────────────────────────────────────── +void Renderer::drawDragon(const EnderDragon& d, + const float* view4x4, const float* proj4x4) { + if (!entity_vao_) 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); + + // パレット: エンダードラゴンの黒紫。被弾フラッシュ中は赤寄せ。 + 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, + }; + 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}; // 紫眼 (フラッシュ無関係) + + // 全体変換: 胴体中心に移動 → yaw → pitch → 全体スケール + // yaw=0 → +X 向き。デフォルトモデルも +X 前なので回転は yaw のまま。 + // kModelScale を上げると全パーツの位置と大きさが等比で拡大される。 + constexpr float kModelScale = 2.0f; + glm::mat4 global = glm::translate(glm::mat4(1.0f), glm::vec3(d.x, d.y, d.z)); + global = glm::rotate(global, glm::radians(d.yaw), glm::vec3(0.f, -1.f, 0.f)); + 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 前方に伸ばした楕円体的な箱 ───────────────────── + { + 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); + drawStevePart(vp * m, m, kBody); + } + + // ── 首 (Neck): 胴体前から斜めに前上方へ ─────────────────────────── + { + 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); + } + + // ── 頭 (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); + } + + // ── 目 (左右): 頭の側面に紫色の小箱 ──────────────────────────────── + 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); + } + + // ── 翼 (左右): 胴体上面から左右に伸ばす平板。 + // 羽ばたきは 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); + } + + // ── 尾 (3 セグメント): 後方に向かって徐々に細くなる + // 隣接セグメント間で連結する S 字波動。 + { + 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; + } + } + + // ── 脚 (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); +} + +// ───────────────────────────────────────────────────────────────────────────── +// drawBossBar() — 画面上部にエンダードラゴンのHPバーを描画する +// +// 中央上に幅 60% の横長バー。 +// 黒い枠 → 暗赤色の背景 (Max HP) → 紫色の前景 (現在 HP) の 3 層を重ねる。 +// 完全死亡時 (health <= 0) は呼ばないでよい (Engine 側で出し分け)。 +// ───────────────────────────────────────────────────────────────────────────── +void Renderer::drawBossBar(float health, float max_health) { + if (max_health <= 0.0f) return; + const float ratio = std::clamp(health / max_health, 0.0f, 1.0f); + + const float hw = static_cast(width_) * 0.5f; + const float hh = static_cast(height_) * 0.5f; + auto nx = [&](float px) { return px / hw - 1.0f; }; + auto ny = [&](float py) { return py / hh - 1.0f; }; + + const float bar_w_px = static_cast(width_) * 0.60f; + const float bar_h_px = 18.0f; + const float left_px = (static_cast(width_) - bar_w_px) * 0.5f; + const float top_px = static_cast(height_) - 36.0f; // 画面上端から下に + const float right_px = left_px + bar_w_px; + const float bot_px = top_px - bar_h_px; + + // (1) 背景 (黒) + const float bg_x0 = nx(left_px - 2.0f); + const float bg_x1 = nx(right_px + 2.0f); + const float bg_y0 = ny(bot_px - 2.0f); + const float bg_y1 = ny(top_px + 2.0f); + const float bg[12] = { + bg_x0, bg_y0, bg_x1, bg_y0, bg_x1, bg_y1, + bg_x0, bg_y0, bg_x1, bg_y1, bg_x0, bg_y1, + }; + + // (2) Max HP 背景 (暗赤) + const float mx_x0 = nx(left_px); + const float mx_x1 = nx(right_px); + const float mx_y0 = ny(bot_px); + const float mx_y1 = ny(top_px); + const float mx[12] = { + mx_x0, mx_y0, mx_x1, mx_y0, mx_x1, mx_y1, + mx_x0, mx_y0, mx_x1, mx_y1, mx_x0, mx_y1, + }; + + // (3) 現在 HP 前景 (紫) + const float cur_right_px = left_px + bar_w_px * ratio; + const float cu_x0 = nx(left_px); + const float cu_x1 = nx(cur_right_px); + const float cu_y0 = ny(bot_px); + const float cu_y1 = ny(top_px); + const float cu[12] = { + cu_x0, cu_y0, cu_x1, cu_y0, cu_x1, cu_y1, + cu_x0, cu_y0, cu_x1, cu_y1, cu_x0, cu_y1, + }; + + glDisable(GL_DEPTH_TEST); + hud_shader_.use(); + glBindVertexArray(hud_vao_); + glBindBuffer(GL_ARRAY_BUFFER, hud_vbo_); + + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(bg), bg); + hud_shader_.setVec4("uColor", 0.0f, 0.0f, 0.0f, 0.85f); + glDrawArrays(GL_TRIANGLES, 0, 6); + + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(mx), mx); + hud_shader_.setVec4("uColor", 0.35f, 0.05f, 0.10f, 0.90f); + glDrawArrays(GL_TRIANGLES, 0, 6); + + if (ratio > 0.0f) { + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(cu), cu); + hud_shader_.setVec4("uColor", 0.60f, 0.10f, 0.85f, 0.95f); + glDrawArrays(GL_TRIANGLES, 0, 6); + } + + glBindVertexArray(0); + glEnable(GL_DEPTH_TEST); +} + // ───────────────────────────────────────────────────────────────────────────── // drawFirstPersonHand() — ローカルプレイヤーの一人称ハンドアニメーション // diff --git a/srcs/renderer/renderer.hpp b/srcs/renderer/renderer.hpp index f3cbc49..cad10eb 100644 --- a/srcs/renderer/renderer.hpp +++ b/srcs/renderer/renderer.hpp @@ -9,6 +9,7 @@ #include "renderer/minimap.hpp" #include "mob/zombie.hpp" #include "mob/arrow.hpp" +#include "mob/ender_dragon.hpp" #include #include #include @@ -56,6 +57,9 @@ class Renderer : public IRenderer { const float* view4x4, const float* proj4x4); void drawArrows(const std::vector& arrows, const float* view4x4, const float* proj4x4); + void drawDragon(const EnderDragon& dragon, + const float* view4x4, const float* proj4x4); + void drawBossBar(float health, float max_health); void endFrame() override; void onResize(int w, int h) override; void setTimeOfDay(float t); diff --git a/srcs/renderer/texture_atlas.cpp b/srcs/renderer/texture_atlas.cpp index 5573da2..55d4954 100644 --- a/srcs/renderer/texture_atlas.cpp +++ b/srcs/renderer/texture_atlas.cpp @@ -691,6 +691,43 @@ static void fillBow(uint8_t* buf, int atlas_w, int tile_col, int tile_row) { } } +// ドラゴンエッグアイコン 16x16 (黒紫の卵 + ピンクの斑点) +static void fillDragonEgg(uint8_t* buf, int atlas_w, int tile_col, int tile_row) { + const int tw = ATLAS_TILE_SIZE; + const int ox = tile_col * tw; + const int oy = tile_row * tw; + static const char* pix[16] = { + "................", + "......aaaa......", + ".....abbbba.....", + "....abbbcbba....", + "...abbcbbbbba...", + "...abbbbcbbba...", + "..abbcbbbbbcba..", + "..abbbbbcbbbba..", + "..abbbcbbbbbba..", + "..abbbbbbbcbba..", + "..abbbcbbbbbba..", + "...abbbbbcbba...", + "...abbcbbbbba...", + "....abbbbbba....", + ".....abbbba.....", + "......aaaa......", + }; + auto put = [&](int px, int py, char c) { + switch (c) { + case 'a': setPixel(buf, atlas_w, ox, oy, px, py, 10, 4, 18); break; // 縁: 黒 + case 'b': setPixel(buf, atlas_w, ox, oy, px, py, 35, 18, 55); break; // 本体: 暗紫 + case 'c': setPixel(buf, atlas_w, ox, oy, px, py, 180, 90, 200); break; // 斑点: 紫光 + default: setPixel(buf, atlas_w, ox, oy, px, py, 0, 0, 0, 0); break; + } + }; + for (int py = 0; py < tw; ++py) { + for (int px = 0; px < tw; ++px) + put(px, py, pix[py][px]); + } +} + } // anonymous namespace // ───────────────────────────────────────────────────────────────────────────── @@ -731,6 +768,7 @@ bool TextureAtlas::generate() { fillBow (pixels, atlas_w, 2, 2); // Bow tile=18 fillStick (pixels, atlas_w, 3, 2); // Stick tile=19 fillTorch (pixels, atlas_w, 4, 2); // Torch tile=20 + fillDragonEgg(pixels, atlas_w, 5, 2); // DragonEgg tile=21 // GPU にテクスチャを作成して転送する glGenTextures(1, &tex_id_);