Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ build/
objs/
vendor/
ft_vox
ft_minecraft
*.o
*.d
.DS_Store
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ add_executable(
srcs/network/client.cpp
srcs/mob/mob_manager.cpp
srcs/mob/arrow_manager.cpp
srcs/mob/dragon_manager.cpp
# ── Audio system ──────────────────────────────────────────────────────────
srcs/audio/audio_impl.cpp
srcs/audio/audio_manager.cpp
Expand Down
19 changes: 13 additions & 6 deletions assets/shaders/chunk.vert
Original file line number Diff line number Diff line change
Expand Up @@ -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 を再利用してメリハリを付ける。
Expand Down
4 changes: 3 additions & 1 deletion include/types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
138 changes: 118 additions & 20 deletions srcs/core/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "network/client.hpp"
#include "mob/mob_manager.hpp"
#include "mob/arrow_manager.hpp"
#include "mob/dragon_manager.hpp"
#include "audio/audio_manager.hpp"
#include "audio/biome_audio_system.hpp"

Expand Down Expand Up @@ -214,6 +215,7 @@ 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送信インターバル

// Audio
AudioManager audio_mgr;
Expand All @@ -225,6 +227,7 @@ struct Engine::Impl {
// 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;
Expand Down Expand Up @@ -338,6 +341,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);

Expand Down Expand Up @@ -693,13 +699,18 @@ void Engine::run() {
hit.bx, hit.by, hit.bz,
static_cast<uint8_t>(BlockType::Air));
} else {
int hit_idx = 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);
impl_->audio_mgr.playSe(SoundEvent::Attack);
if (hit_idx >= 0) {
const Zombie& hz = impl_->mob_mgr.zombies()[hit_idx];
impl_->audio_mgr.playSe3D(SoundEvent::MobHurt,
hz.x, hz.y + 0.9f, hz.z, 32.0f);
if (!dragon_hit) {
int hit_idx = impl_->mob_mgr.playerMeleeAttack(
pos.x, pos.y, pos.z, front.x, front.z);
if (hit_idx >= 0) {
const Zombie& hz = impl_->mob_mgr.zombies()[hit_idx];
impl_->audio_mgr.playSe3D(SoundEvent::MobHurt,
hz.x, hz.y + 0.9f, hz.z, 32.0f);
}
}
}
}
Expand Down Expand Up @@ -738,19 +749,36 @@ 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);
impl_->audio_mgr.playSe(SoundEvent::BlockPlace);
if (impl_->multiplayer)
impl_->net_client.sendBlockChange(
hit.nx, hit.ny, hit.nz,
static_cast<uint8_t>(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);
impl_->audio_mgr.playSe(SoundEvent::BlockPlace);
if (impl_->multiplayer)
impl_->net_client.sendBlockChange(
hit.nx, hit.ny, hit.nz,
static_cast<uint8_t>(to_place));
}
}
}
}
Expand Down Expand Up @@ -817,6 +845,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<EnderDragon::State>(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.
Expand Down Expand Up @@ -909,9 +959,43 @@ 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<uint8_t>(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、補間は今後検討。

// ── チャンクのストリーミング ─────────────────────────────────────────
// プレイヤーの周囲のチャンクを読み込み、遠いチャンクを破棄する。
// 毎フレーム数チャンクずつ処理し、ゲームが止まらないようにする。
Expand Down Expand Up @@ -1033,6 +1117,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);

Expand Down Expand Up @@ -1067,6 +1157,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(
Expand Down
16 changes: 15 additions & 1 deletion srcs/mob/arrow_manager.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "mob/arrow_manager.hpp"
#include "mob/dragon_manager.hpp"
#include <algorithm>
#include <cmath>

Expand All @@ -16,7 +17,8 @@ void ArrowManager::spawn(float x, float y, float z,

void ArrowManager::update(float dt,
const std::function<bool(int,int,int)>& isSolid,
std::vector<Zombie>& zombies) {
std::vector<Zombie>& zombies,
DragonManager* dragon_mgr) {
if (arrows_.empty()) return;

const float sub_dt = dt / static_cast<float>(SUBSTEPS);
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions srcs/mob/arrow_manager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@
#include <functional>
#include <vector>

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<bool(int,int,int)>& isSolid,
std::vector<Zombie>& zombies);
std::vector<Zombie>& zombies,
DragonManager* dragon_mgr = nullptr);

const std::vector<Arrow>& arrows() const { return arrows_; }

Expand Down
Loading