#include "app.h"

#include <algorithm>
#include <cmath>

void App::spawnMobs() {
    mobs_.clear();
    cityReady_     = false;
    cityNextBlock_ = 0;

    uint64_t rng = World::getSeed() ^ 0xFEDCBA9876543210ULL;
    auto nextRng = [&]() -> uint64_t {
        rng ^= rng << 13; rng ^= rng >> 7; rng ^= rng << 17; return rng;
    };
    auto rF = [&](float lo, float hi) -> float {
        return lo + static_cast<float>(nextRng() & 0xFFFF) / 65535.0f * (hi - lo);
    };

    // Gravity-aware helpers for spawn
    // Returns the gravity up direction at a world position
    auto spawnGravUp = [](float px, float py, float pz, float& gx, float& gy, float& gz) {
        PlanetManager::gravityUp(px, py, pz, gx, gy, gz);
    };

    // Scan along gravity direction to find standing position.
    // Returns {x, y, z} of the block above the first solid surface found.
    auto findGround = [&](double sx, double sy, double sz) -> std::tuple<double,double,double> {
        float gx=0, gy=1, gz=0;
        spawnGravUp(static_cast<float>(sx), static_cast<float>(sy), static_cast<float>(sz), gx, gy, gz);
        // Pre-load all chunks in the scan column so getBlock() returns real terrain.
        // Scan will check sy+15 down to sy-75 along the gravity axis.
        for (int i = 16; i >= -80; i -= 16) {
            double cx = sx + gx * i;
            double cy = sy + gy * i;
            double cz = sz + gz * i;
            world_.ensureChunk(world_.worldToChunkPos(
                static_cast<int>(std::floor(cx)),
                static_cast<int>(std::floor(cy)),
                static_cast<int>(std::floor(cz))));
        }
        // Scan from +15 in up direction downward to -75
        for (int i = 15; i >= -75; --i) {
            double cx = sx + gx * i;
            double cy = sy + gy * i;
            double cz = sz + gz * i;
            int bx = static_cast<int>(std::floor(cx));
            int by = static_cast<int>(std::floor(cy));
            int bz = static_cast<int>(std::floor(cz));
            int dx = static_cast<int>(std::floor(cx - gx));
            int dy = static_cast<int>(std::floor(cy - gy));
            int dz = static_cast<int>(std::floor(cz - gz));
            if (!isSolid(world_.getBlock(bx, by, bz)) &&
                 isSolid(world_.getBlock(dx, dy, dz)))
                return {cx, cy, cz};
        }
        return {sx, sy, sz};
    };

    // Build gravity-relative horizontal basis at player position
    float spGx=0, spGy=1, spGz=0;
    spawnGravUp(static_cast<float>(cameraX_), static_cast<float>(cameraY_), static_cast<float>(cameraZ_), spGx, spGy, spGz);
    // ref vector: use (0,0,1) when up is not Z-axis, else (0,1,0)
    float spRefX = 0, spRefY = (std::abs(spGz) > 0.9f) ? 1.0f : 0.0f, spRefZ = (std::abs(spGz) > 0.9f) ? 0.0f : 1.0f;
    // fwd_base = normalize(ref - up*(ref·up))
    float spDot = spRefX*spGx + spRefY*spGy + spRefZ*spGz;
    float spFX = spRefX - spGx*spDot, spFY = spRefY - spGy*spDot, spFZ = spRefZ - spGz*spDot;
    float spFL = std::sqrt(spFX*spFX + spFY*spFY + spFZ*spFZ);
    if (spFL > 1e-6f) { spFX/=spFL; spFY/=spFL; spFZ/=spFL; }
    // right_base = cross(up, fwd_base)
    float spRX = spGy*spFZ - spGz*spFY, spRY = spGz*spFX - spGx*spFZ, spRZ = spGx*spFY - spGy*spFX;
    // Horizontal offset: right*cos + fwd*sin
    auto hOffset = [&](float angle, float dist, double ox, double oy, double oz,
                        double& sx, double& sy, double& sz) {
        float c = std::cos(angle), s = std::sin(angle);
        sx = ox + (spRX*c + spFX*s) * dist;
        sy = oy + (spRY*c + spFY*s) * dist;
        sz = oz + (spRZ*c + spFZ*s) * dist;
    };

    // Spawn sheep (prefer grass, avoid water)
    for (int i = 0; i < N_MOBS_SHEEP; ++i) {
        float angle = rF(0, 6.2832f);
        float dist  = rF(15.0f, 55.0f);
        double sx, sy, sz;
        hOffset(angle, dist, cameraX_, cameraY_, cameraZ_, sx, sy, sz);
        auto [gx,gy,gz] = findGround(sx, sy, sz);
        float ugx=0,ugy=1,ugz=0;
        spawnGravUp(static_cast<float>(gx), static_cast<float>(gy), static_cast<float>(gz), ugx, ugy, ugz);
        Mob m{}; m.type = MobType::Sheep;
        m.x = gx; m.y = gy; m.z = gz;
        m.upX = ugx; m.upY = ugy; m.upZ = ugz;
        m.yaw = rF(0, 6.2832f);
        m.rngState = nextRng();
        m.eatTimer   = m.rnd(10.0f, 35.0f);
        m.drinkTimer = m.rnd(120.0f, 320.0f);
        m.grounded = true;
        m.hp = 8.0f;
        mobs_.push_back(m);
    }

    // Spawn wolves (further away)
    for (int i = 0; i < N_MOBS_WOLF; ++i) {
        float angle = rF(0, 6.2832f);
        float dist  = rF(40.0f, 80.0f);
        double sx, sy, sz;
        hOffset(angle, dist, cameraX_, cameraY_, cameraZ_, sx, sy, sz);
        auto [gx,gy,gz] = findGround(sx, sy, sz);
        float ugx=0,ugy=1,ugz=0;
        spawnGravUp(static_cast<float>(gx), static_cast<float>(gy), static_cast<float>(gz), ugx, ugy, ugz);
        Mob m{}; m.type = MobType::Wolf;
        m.x = gx; m.y = gy; m.z = gz;
        m.upX = ugx; m.upY = ugy; m.upZ = ugz;
        m.yaw = rF(0, 6.2832f);
        m.rngState = nextRng();
        m.grounded = true;
        m.hp = 8.0f;
        mobs_.push_back(m);
    }

    // City origin: 50-70 blocks from player
    float cityAngle = rF(0, 6.2832f);
    float cityDist  = rF(50.0f, 70.0f);
    {
        double csx, csy, csz;
        hOffset(cityAngle, cityDist, cameraX_, cameraY_, cameraZ_, csx, csy, csz);
        auto [gx,gy,gz] = findGround(csx, csy, csz);
        cityOriginX_ = static_cast<int>(std::floor(gx));
        cityOriginY_ = static_cast<int>(std::floor(gy));
        cityOriginZ_ = static_cast<int>(std::floor(gz));
    }
    // Store city orientation using the actual planet gravity at the city origin,
    // not the player's current gravity (which is always (0,1,0) at spawn time).
    {
        float cgx=0, cgy=1, cgz=0;
        spawnGravUp(static_cast<float>(cityOriginX_),
                    static_cast<float>(cityOriginY_),
                    static_cast<float>(cityOriginZ_), cgx, cgy, cgz);
        if (std::abs(cgx) > 0.5f) {
            cityUpAxis_ = 0; cityUpSign_ = (cgx > 0) ? 1 : -1;
            cityH1Axis_ = 1; cityH2Axis_ = 2;
        } else if (std::abs(cgz) > 0.5f) {
            cityUpAxis_ = 2; cityUpSign_ = (cgz > 0) ? 1 : -1;
            cityH1Axis_ = 0; cityH2Axis_ = 1;
        } else {
            cityUpAxis_ = 1; cityUpSign_ = (cgy > 0) ? 1 : -1;
            cityH1Axis_ = 0; cityH2Axis_ = 2;
        }
    }
    cityReady_ = true;
    cityDone_  = false;

    // Compute the gate world position: front-wall centre (lc=OUTER/2, lr=-1 = outside)
    {
        constexpr int OUTER   = 44;
        constexpr int WALL_T  = 3;
        int gx, gy, gz;
        cityLW(OUTER / 2, 1, -1, gx, gy, gz);
        cityGateX_ = static_cast<float>(gx) + 0.5f;
        cityGateY_ = static_cast<float>(gy) + 0.5f;
        cityGateZ_ = static_cast<float>(gz) + 0.5f;
        (void)WALL_T;
    }

    // Spawn humans OUTSIDE the build zone (clear zone extends ~52 blocks from origin)
    // so they are never inside when phase-0 starts clearing ground.
    for (int i = 0; i < N_MOBS_HUMAN; ++i) {
        float angle = rF(0, 6.2832f);
        float dist  = rF(55.0f, 70.0f);
        double sx, sy, sz;
        hOffset(angle, dist, cityOriginX_, cityOriginY_, cityOriginZ_, sx, sy, sz);
        auto [gx,gy,gz] = findGround(sx, sy, sz);
        float ugx=0,ugy=1,ugz=0;
        spawnGravUp(static_cast<float>(gx), static_cast<float>(gy), static_cast<float>(gz), ugx, ugy, ugz);
        Mob m{}; m.type = MobType::Human;
        m.x = gx; m.y = gy; m.z = gz;
        m.upX = ugx; m.upY = ugy; m.upZ = ugz;
        m.yaw = rF(0, 6.2832f);
        m.rngState = nextRng() ^ static_cast<uint64_t>(i * 0x1234567890ABCDEFULL);
        m.buildStep = -1;
        m.aiTimer = 0.0f;
        m.grounded = true;
        m.hp = 16.0f;
        // First 2 humans are designated gate guards (switch role after construction done)
        if (i < 2) {
            m.aiRole = 1;
            m.guardPostX = cityGateX_;
            m.guardPostY = cityGateY_;
            m.guardPostZ = cityGateZ_;
        }
        mobs_.push_back(m);
    }
}

// ── City blueprint — середньовічний замок ────────────────────────────────────
//
// Фази будівництва:
//   0. Розчищення: Air для lh=1..20 у всій зоні (зносить дерева, рослини)
//   1. Вирівнювання: Stone для lh=-8..-1 (заповнює ями, вирівнює ґрунт)
//   2. Підлога двору: Gravel для lh=0 всередині стін
//   3. Зовнішні мури з вежами: Stone, висота WALL_H, кутові вежі TOWER_H
//   4. Центральний донжон: 14×14, 4 поверхи, дерев'яні перекриття
//   5. Допоміжні будівлі: 2 будівлі в дворі (казарни / склади)

bool App::cityGetBlock(int step, int& bx, int& by, int& bz, BlockType& type) const {
    if (!cityReady_) return false;
    const int ox = cityOriginX_, oy = cityOriginY_, oz = cityOriginZ_;
    const int origin[3] = {ox, oy, oz};

    // Local → world coordinate mapping
    auto lw = [&](int lc, int lh, int lr, int& out_bx, int& out_by, int& out_bz) {
        int w[3];
        w[cityH1Axis_] = origin[cityH1Axis_] + lc;
        w[cityUpAxis_] = origin[cityUpAxis_] + lh * cityUpSign_;
        w[cityH2Axis_] = origin[cityH2Axis_] + lr;
        out_bx = w[0]; out_by = w[1]; out_bz = w[2];
    };

    // Castle dimensions
    constexpr int INNER    = 38;   // inner courtyard side (blocks)
    constexpr int WALL_T   = 3;    // wall thickness
    constexpr int OUTER    = INNER + 2 * WALL_T;   // = 44
    constexpr int WALL_H   = 8;    // curtain wall height
    constexpr int TOWER_SZ = 8;    // corner tower footprint
    constexpr int TOWER_H  = 14;   // corner tower height
    constexpr int GATE_W   = 4;    // gate opening width
    constexpr int KEEP_SZ  = 14;   // keep (донжон) footprint
    constexpr int KEEP_OFF = (INNER - KEEP_SZ) / 2 + WALL_T; // offset from origin
    constexpr int FLOOR_H  = 5;    // wall rows per floor
    constexpr int KEEP_FL  = 4;    // keep floors
    // Barracks: two 10×8 buildings in the courtyard
    constexpr int BAR_W    = 10;
    constexpr int BAR_D    = 8;
    constexpr int BAR_FL   = 2;

    // Zone bounds for clearing / levelling (includes wall + 4-block margin)
    constexpr int CLEAR_LC0 = -4,         CLEAR_LC1 = OUTER + 4;
    constexpr int CLEAR_LR0 = -4,         CLEAR_LR1 = OUTER + 4;
    constexpr int CLEAR_UP  = 20;         // clear lh=1..CLEAR_UP
    constexpr int FILL_DOWN = 8;          // fill lh=-FILL_DOWN..-1

    int s = step;

    // ── Phase 0: Demolish / clear vegetation (Air from lh=1 up) ──────────────
    // Skipped instantly if already Air — handles trees, leaves, floating blocks.
    for (int lh = CLEAR_UP; lh >= 1; --lh)
    for (int lc = CLEAR_LC0; lc <= CLEAR_LC1; ++lc)
    for (int lr = CLEAR_LR0; lr <= CLEAR_LR1; ++lr) {
        if (s-- == 0) { lw(lc, lh, lr, bx,by,bz); type = BlockType::Air; return true; }
    }

    // ── Phase 1: Level the ground — fill holes below city origin ─────────────
    for (int lh = -FILL_DOWN; lh <= -1; ++lh)
    for (int lc = CLEAR_LC0; lc <= CLEAR_LC1; ++lc)
    for (int lr = CLEAR_LR0; lr <= CLEAR_LR1; ++lr) {
        if (s-- == 0) { lw(lc, lh, lr, bx,by,bz); type = BlockType::Stone; return true; }
    }

    // ── Phase 2: Courtyard floor — Gravel inside curtain wall ────────────────
    for (int lc = WALL_T; lc < WALL_T + INNER; ++lc)
    for (int lr = WALL_T; lr < WALL_T + INNER; ++lr) {
        if (s-- == 0) { lw(lc, 0, lr, bx,by,bz); type = BlockType::Gravel; return true; }
    }
    // Stone plaza around the outer wall
    for (int lc = 0; lc < OUTER; ++lc)
    for (int lr = 0; lr < OUTER; ++lr) {
        bool inner = (lc >= WALL_T && lc < WALL_T+INNER && lr >= WALL_T && lr < WALL_T+INNER);
        if (inner) continue;
        if (s-- == 0) { lw(lc, 0, lr, bx,by,bz); type = BlockType::Stone; return true; }
    }

    // ── Helper: is a position inside a corner tower? ──────────────────────────
    // Towers are at four corners of the castle (lc in [0,TOWER_SZ), [OUTER-TOWER_SZ,OUTER))
    // and same for lr.
    auto inTower = [&](int lc, int lr) -> bool {
        bool tc = (lc < TOWER_SZ) || (lc >= OUTER - TOWER_SZ);
        bool tr = (lr < TOWER_SZ) || (lr >= OUTER - TOWER_SZ);
        return tc && tr;
    };

    // ── Phase 3: Corner towers ────────────────────────────────────────────────
    // Four 8×8 towers, solid stone, taller than curtain wall
    for (int wy = 1; wy <= TOWER_H; ++wy) {
        bool batt = (wy == TOWER_H);
        for (int tc = 0; tc < TOWER_SZ; ++tc)
        for (int tr = 0; tr < TOWER_SZ; ++tr) {
            bool isPerim = (tc==0 || tc==TOWER_SZ-1 || tr==0 || tr==TOWER_SZ-1);
            // Battlements: alternating merlons on the perimeter
            if (batt) {
                if (!isPerim) continue;
                if ((tc + tr) % 2 == 0) continue; // open gaps
            } else {
                // Hollow interior above wall_h (arrow slits every other row on perimeter)
                if (!isPerim && wy > WALL_H) continue;
                if (isPerim && wy >= 3 && wy <= 5 && wy % 2 == 0
                    && (tc == 0 || tc == TOWER_SZ-1) && (tr == 2 || tr == 5)) continue; // slit
                if (isPerim && wy >= 3 && wy <= 5 && wy % 2 == 0
                    && (tr == 0 || tr == TOWER_SZ-1) && (tc == 2 || tc == 5)) continue;
            }
            // Emit for all 4 corner positions
            int offsets[4][2] = {
                {0, 0}, {OUTER-TOWER_SZ, 0},
                {0, OUTER-TOWER_SZ}, {OUTER-TOWER_SZ, OUTER-TOWER_SZ}
            };
            for (auto& off : offsets) {
                if (s-- == 0) {
                    lw(off[0]+tc, wy, off[1]+tr, bx,by,bz);
                    type = BlockType::Stone; return true;
                }
            }
        }
    }

    // ── Phase 4: Curtain walls (between towers) ───────────────────────────────
    // Single gate: front wall only (lr = 0..WALL_T-1), centred, 4 blocks wide × 5 tall.
    // Back and side walls are solid — no openings.
    const int gateFront = OUTER / 2;  // gate centre column on front wall

    for (int wy = 1; wy <= WALL_H; ++wy) {
        bool batt = (wy == WALL_H);

        // Front wall — has the gate opening
        for (int lc = TOWER_SZ; lc < OUTER - TOWER_SZ; ++lc)
        for (int lt = 0; lt < WALL_T; ++lt) {
            if (batt && (lc % 2 == 0)) continue; // merlon gaps
            // Gate opening: 4 wide, 5 tall
            if (wy >= 1 && wy <= 5 && lc >= gateFront-2 && lc < gateFront+2) continue;
            if (s-- == 0) { lw(lc, wy, lt, bx,by,bz); type=BlockType::Stone; return true; }
        }
        // Back wall — fully solid
        for (int lc = TOWER_SZ; lc < OUTER - TOWER_SZ; ++lc)
        for (int lt = 0; lt < WALL_T; ++lt) {
            if (batt && (lc % 2 == 0)) continue;
            if (s-- == 0) { lw(lc, wy, OUTER-WALL_T+lt, bx,by,bz); type=BlockType::Stone; return true; }
        }
        // Left wall — fully solid
        for (int lr = TOWER_SZ; lr < OUTER - TOWER_SZ; ++lr)
        for (int lt = 0; lt < WALL_T; ++lt) {
            if (batt && (lr % 2 == 0)) continue;
            if (s-- == 0) { lw(lt, wy, lr, bx,by,bz); type=BlockType::Stone; return true; }
        }
        // Right wall — fully solid
        for (int lr = TOWER_SZ; lr < OUTER - TOWER_SZ; ++lr)
        for (int lt = 0; lt < WALL_T; ++lt) {
            if (batt && (lr % 2 == 0)) continue;
            if (s-- == 0) { lw(OUTER-WALL_T+lt, wy, lr, bx,by,bz); type=BlockType::Stone; return true; }
        }
    }

    // ── Phase 5: Central keep (донжон) ────────────────────────────────────────
    // 14×14 footprint, KEEP_FL floors, wood ceilings, battlements on roof
    for (int fl = 0; fl <= KEEP_FL; ++fl) {
        bool roof = (fl == KEEP_FL);
        int baseH = fl * FLOOR_H;

        // Floor slab (Stone)
        for (int dx = 0; dx < KEEP_SZ; ++dx)
        for (int dz = 0; dz < KEEP_SZ; ++dz) {
            if (s-- == 0) {
                lw(KEEP_OFF+dx, baseH, KEEP_OFF+dz, bx,by,bz);
                type = BlockType::Stone; return true;
            }
        }
        if (roof) {
            // Battlements on roof (alternating merlons on perimeter)
            for (int dx = 0; dx < KEEP_SZ; ++dx)
            for (int dz = 0; dz < KEEP_SZ; ++dz) {
                bool perim = (dx==0||dx==KEEP_SZ-1||dz==0||dz==KEEP_SZ-1);
                if (!perim || (dx+dz)%2==0) continue;
                if (s-- == 0) {
                    lw(KEEP_OFF+dx, baseH+1, KEEP_OFF+dz, bx,by,bz);
                    type = BlockType::Stone; return true;
                }
            }
            break;
        }
        // Walls (rows 1..4)
        for (int wy = 1; wy <= 4; ++wy)
        for (int dx = 0; dx < KEEP_SZ; ++dx)
        for (int dz = 0; dz < KEEP_SZ; ++dz) {
            bool perim = (dx==0||dx==KEEP_SZ-1||dz==0||dz==KEEP_SZ-1);
            if (!perim) continue;
            // Main gate: front face (dz==0), centre 3-wide, rows 1-3
            if (dz==0 && dx>=KEEP_SZ/2-1 && dx<=KEEP_SZ/2+1 && wy<=3) continue;
            // Arrow slits: every other block on perimeter, rows 2-3
            if ((wy==2||wy==3) && (dx%3==1||dz%3==1)) {
                if ((dx==0||dx==KEEP_SZ-1) && dz%3==1) continue;
                if ((dz==0||dz==KEEP_SZ-1) && dx%3==1) continue;
            }
            if (s-- == 0) {
                lw(KEEP_OFF+dx, baseH+wy, KEEP_OFF+dz, bx,by,bz);
                type = BlockType::Stone; return true;
            }
        }
        // Wood ceiling (interior, upper floors only)
        if (fl > 0) {
            for (int dx = 1; dx < KEEP_SZ-1; ++dx)
            for (int dz = 1; dz < KEEP_SZ-1; ++dz) {
                if (s-- == 0) {
                    lw(KEEP_OFF+dx, baseH+1, KEEP_OFF+dz, bx,by,bz);
                    type = BlockType::Wood; return true;
                }
            }
        }
        // Staircase: stone steps along left-inner wall, ascending
        if (fl < KEEP_FL) {
            for (int si = 0; si < 4; ++si) {
                if (s-- == 0) {
                    lw(KEEP_OFF+1, baseH+1+si, KEEP_OFF+1+si, bx,by,bz);
                    type = BlockType::Stone; return true;
                }
            }
        }
    }

    // ── Phase 6: Barracks — two 10×8 buildings flanking the keep ─────────────
    // Left barracks: lc=WALL_T+1, right barracks: lc=OUTER-WALL_T-BAR_W-1
    // Both placed at lr=WALL_T+2 (near back wall)
    int barOffsets[2][2] = {
        { WALL_T + 1,                    WALL_T + 2 },
        { OUTER - WALL_T - BAR_W - 1,   WALL_T + 2 }
    };
    for (auto& bo : barOffsets) {
        int blc = bo[0], blr = bo[1];
        for (int fl = 0; fl <= BAR_FL; ++fl) {
            bool roof = (fl == BAR_FL);
            int baseH = fl * FLOOR_H;
            // Floor slab
            for (int dx = 0; dx < BAR_W; ++dx)
            for (int dz = 0; dz < BAR_D; ++dz) {
                if (s-- == 0) {
                    lw(blc+dx, baseH, blr+dz, bx,by,bz);
                    type = BlockType::Stone; return true;
                }
            }
            if (roof) {
                for (int dx = 0; dx < BAR_W; ++dx)
                for (int dz = 0; dz < BAR_D; ++dz) {
                    bool perim = (dx==0||dx==BAR_W-1||dz==0||dz==BAR_D-1);
                    if (!perim || (dx+dz)%2==0) continue;
                    if (s-- == 0) {
                        lw(blc+dx, baseH+1, blr+dz, bx,by,bz);
                        type = BlockType::Stone; return true;
                    }
                }
                break;
            }
            // Walls
            for (int wy = 1; wy <= 4; ++wy)
            for (int dx = 0; dx < BAR_W; ++dx)
            for (int dz = 0; dz < BAR_D; ++dz) {
                bool perim = (dx==0||dx==BAR_W-1||dz==0||dz==BAR_D-1);
                if (!perim) continue;
                if (dz==0 && dx==BAR_W/2 && wy<=3) continue; // door
                if (s-- == 0) {
                    lw(blc+dx, baseH+wy, blr+dz, bx,by,bz);
                    type = BlockType::Stone; return true;
                }
            }
            // Wood floor (upper floor only)
            if (fl > 0) {
                for (int dx = 1; dx < BAR_W-1; ++dx)
                for (int dz = 1; dz < BAR_D-1; ++dz) {
                    if (s-- == 0) {
                        lw(blc+dx, baseH+1, blr+dz, bx,by,bz);
                        type = BlockType::Wood; return true;
                    }
                }
            }
        }
    }

    return false;
}

// ── Common mob helpers ────────────────────────────────────────────────────────

int App::nearestMobIdx(const Mob& mob, MobType type, float maxDist) const {
    int best = -1;
    float best2 = maxDist * maxDist;
    for (int i = 0; i < static_cast<int>(mobs_.size()); ++i) {
        const Mob& m = mobs_[i];
        if (m.dead || m.type != type || &m == &mob) continue;
        float dx = static_cast<float>(m.x - mob.x);
        float dz = static_cast<float>(m.z - mob.z);
        float d2 = dx*dx + dz*dz;
        if (d2 < best2) { best2 = d2; best = i; }
    }
    return best;
}

float App::distToPlayer(const Mob& mob) const {
    float dx = static_cast<float>(mob.x - cameraX_);
    float dy = static_cast<float>(mob.y - cameraY_);
    float dz = static_cast<float>(mob.z - cameraZ_);
    return std::sqrt(dx*dx + dy*dy + dz*dz);
}

bool App::mobMoveTo(Mob& mob, float tx, float ty, float tz, float speed, float dt) {

    float ux = mob.upX, uy = mob.upY, uz = mob.upZ;

    float dx = tx - static_cast<float>(mob.x);
    float dy = ty - static_cast<float>(mob.y);
    float dz = tz - static_cast<float>(mob.z);

    // Project displacement onto horizontal plane (perpendicular to up)
    float dot = dx*ux + dy*uy + dz*uz;
    float hx = dx - ux*dot, hy = dy - uy*dot, hz = dz - uz*dot;
    float hdist = std::sqrt(hx*hx + hy*hy + hz*hz);
    if (hdist < 0.4f) return true;

    float nx = hx/hdist, ny = hy/hdist, nz = hz/hdist; // horizontal unit direction
    float move = std::min(speed * dt, hdist);

    // Compute yaw in horizontal frame for rendering
    // fwd_base = normalize(ref projected onto horizontal plane)
    float refZ = (std::abs(uz) > 0.9f) ? 0.0f : 1.0f;
    float refY = (std::abs(uz) > 0.9f) ? 1.0f : 0.0f;
    float fdot = refY*uy + refZ*uz;
    float fbx = -ux*fdot, fby = refY - uy*fdot, fbz = refZ - uz*fdot;
    float fbl = std::sqrt(fbx*fbx + fby*fby + fbz*fbz);
    if (fbl > 1e-6f) { fbx/=fbl; fby/=fbl; fbz/=fbl; }
    // right_base = cross(up, fwd_base)
    float rbx = uy*fbz - uz*fby, rby = uz*fbx - ux*fbz, rbz = ux*fby - uy*fbx;
    float cosY = nx*rbx + ny*rby + nz*rbz;
    float sinY = nx*fbx + ny*fby + nz*fbz;
    mob.yaw = std::atan2(sinY, cosY);

    mob.walkDirX = nx; mob.walkDirY = ny; mob.walkDirZ = nz;

    double newX = mob.x + nx * move;
    double newY = mob.y + ny * move;
    double newZ = mob.z + nz * move;

    // Water avoidance
    {
        int wfx=int(std::floor(newX)), wfy=int(std::floor(newY)), wfz=int(std::floor(newZ));
        int whx=int(std::floor(newX+ux)), why=int(std::floor(newY+uy)), whz=int(std::floor(newZ+uz));
        bool intoWater = world_.getBlock(wfx,wfy,wfz)==BlockType::Water ||
                         world_.getBlock(whx,why,whz)==BlockType::Water;
        bool groundWater = world_.getBlock(int(std::floor(newX-ux)),int(std::floor(newY-uy)),int(std::floor(newZ-uz)))==BlockType::Water;
        if (intoWater || groundWater) {
            mob.walkDirX = mob.walkDirY = mob.walkDirZ = 0.0f;
            return false;
        }
    }

    int fx=int(std::floor(newX)), fy=int(std::floor(newY)), fz=int(std::floor(newZ));
    int hbx=int(std::floor(newX+ux)), hby=int(std::floor(newY+uy)), hbz=int(std::floor(newZ+uz));
    bool blocked = isSolid(world_.getBlock(fx,fy,fz)) || isSolid(world_.getBlock(hbx,hby,hbz));
    if (blocked) {
        if (mob.grounded) {
            int upAx = (std::abs(uy) > 0.5f) ? 1 : (std::abs(ux) > 0.5f ? 0 : 2);
            float n3[3] = {nx, ny, nz};
            int ha[2], k = 0;
            for (int a = 0; a < 3; ++a) if (a != upAx) ha[k++] = a;

            auto trySlide = [&](float sx, float sy, float sz) -> bool {
                if (sx*sx + sy*sy + sz*sz < 0.01f) return false;
                double tx2 = mob.x + sx*move, ty2 = mob.y + sy*move, tz2 = mob.z + sz*move;
                int bx2=int(std::floor(tx2)), by2=int(std::floor(ty2)), bz2=int(std::floor(tz2));
                int hx2=int(std::floor(tx2+ux)), hy2=int(std::floor(ty2+uy)), hz2=int(std::floor(tz2+uz));
                if (!isSolid(world_.getBlock(bx2,by2,bz2)) && !isSolid(world_.getBlock(hx2,hy2,hz2))) {
                    mob.x=tx2; mob.y=ty2; mob.z=tz2;
                    return true;
                }
                return false;
            };

            float s0[3]={0,0,0}; s0[ha[0]] = n3[ha[0]];
            float s1[3]={0,0,0}; s1[ha[1]] = n3[ha[1]];
            if (!trySlide(s0[0], s0[1], s0[2]))
                if (!trySlide(s1[0], s1[1], s1[2])) {
                    // Try to step up over a 1-block obstacle
                    double stepX = newX + ux, stepY = newY + uy, stepZ = newZ + uz;
                    int sx2=int(std::floor(stepX)), sy2=int(std::floor(stepY)), sz2=int(std::floor(stepZ));
                    int sx3=int(std::floor(newX+ux*2)), sy3=int(std::floor(newY+uy*2)), sz3=int(std::floor(newZ+uz*2));
                    bool canStep = isSolid(world_.getBlock(fx,fy,fz))
                                && !isSolid(world_.getBlock(sx2,sy2,sz2))
                                && !isSolid(world_.getBlock(sx3,sy3,sz3));
                    if (canStep) {
                        mob.velGrav = 8.5f;  // jump impulse — 8.5²/40 ≈ 1.8 blocks clearance
                    } else {
                        // Fully stuck — abandon target so AI picks a new one
                        mob.walkDirX = mob.walkDirY = mob.walkDirZ = 0.0f;
                        return true;
                    }
                }
        }
        // Airborne + blocked: wait for arc to carry mob over obstacle, don't abandon
    } else {
        mob.x = newX;
        mob.y = newY;
        mob.z = newZ;
    }
    return hdist < 0.8f;
}

void App::mobApplyPhysics(Mob& mob, float dt) {
    constexpr float GRAVITY  = 20.0f;
    constexpr float TERM_VEL = -20.0f;

    // Knockback: apply horizontal displacement, fast exponential decay
    if (mob.knockVelX != 0.0f || mob.knockVelZ != 0.0f) {
        mob.x += mob.knockVelX * dt;
        mob.z += mob.knockVelZ * dt;
        float decay = std::pow(0.05f, dt); // decays to 5% in 1 second
        mob.knockVelX *= decay;
        mob.knockVelZ *= decay;
        if (std::abs(mob.knockVelX) < 0.01f) mob.knockVelX = 0.0f;
        if (std::abs(mob.knockVelZ) < 0.01f) mob.knockVelZ = 0.0f;
    }
    mob.hitFlash = std::max(0.0f, mob.hitFlash - dt);

    float gx=0, gy=1, gz=0;
    PlanetManager::gravityUp(static_cast<float>(mob.x), static_cast<float>(mob.y),
                             static_cast<float>(mob.z), gx, gy, gz);
    mob.upX = gx; mob.upY = gy; mob.upZ = gz;

    const int   upAx  = (std::abs(gy) > 0.5f) ? 1 : (std::abs(gx) > 0.5f ? 0 : 2);
    const float upSgn = (upAx == 0) ? gx : (upAx == 1 ? gy : gz);
    double& upRef = (upAx == 0) ? mob.x : (upAx == 1 ? mob.y : mob.z);

    // Gravity
    mob.velGrav -= GRAVITY * dt;
    if (mob.velGrav < TERM_VEL) mob.velGrav = TERM_VEL;
    upRef += upSgn * mob.velGrav * dt;

    // Grounding: first check if foot is inside a solid block (deep overshoot /
    // bad spawn position), then fall back to the normal 0.15-below probe.
    // Both checks use the same chunk, so there's no cross-chunk oscillation.
    {
        // foot block
        double fp[3] = { mob.x, mob.y, mob.z };
        int fx=int(std::floor(fp[0])), fy=int(std::floor(fp[1])), fz=int(std::floor(fp[2]));

        if (isSolid(world_.getBlock(fx, fy, fz))) {
            // Foot inside solid — snap to top of this block
            upRef = (upSgn > 0) ? (std::floor(upRef) + 1.0)
                                : (std::ceil (upRef) - 1.0);
            mob.velGrav = 0.0f;
            mob.grounded = true;
        } else {
            // Normal probe 0.15 below foot
            double probe = upRef - upSgn * 0.15;
            double pp[3] = { mob.x, mob.y, mob.z };
            pp[upAx] = probe;
            int px=int(std::floor(pp[0])), py=int(std::floor(pp[1])), pz=int(std::floor(pp[2]));
            if (isSolid(world_.getBlock(px, py, pz))) {
                if (mob.velGrav <= 0.0f) {
                    // Falling or stationary — snap to surface
                    upRef = (upSgn > 0) ? (std::floor(probe) + 1.0)
                                        : (std::ceil (probe) - 1.0);
                    mob.velGrav = 0.0f;
                    mob.grounded = true;
                } else {
                    // Moving upward past old ground level — don't snap back
                    mob.grounded = false;
                }
            } else {
                mob.grounded = false;
            }
        }
    }

    // Animate legs proportional to speed
    float spd = std::sqrt(mob.walkDirX*mob.walkDirX + mob.walkDirY*mob.walkDirY + mob.walkDirZ*mob.walkDirZ);
    mob.animPhase += spd * 4.0f * dt;
    if (mob.animPhase > 6.2832f) mob.animPhase -= 6.2832f;
}

// ── Sheep AI ──────────────────────────────────────────────────────────────────

void App::updateSheep(Mob& mob, float dt) {

    // Flee from any nearby wolf — highest priority
    int wolfIdx = nearestMobIdx(mob, MobType::Wolf, 16.0f);
    if (wolfIdx >= 0) {
        const Mob& wolf = mobs_[wolfIdx];
        float dx3=static_cast<float>(mob.x-wolf.x), dy3=static_cast<float>(mob.y-wolf.y), dz3=static_cast<float>(mob.z-wolf.z);
        float fdot=dx3*mob.upX+dy3*mob.upY+dz3*mob.upZ;
        float hfx=dx3-mob.upX*fdot, hfy=dy3-mob.upY*fdot, hfz=dz3-mob.upZ*fdot;
        float hfd=std::max(0.01f, std::sqrt(hfx*hfx+hfy*hfy+hfz*hfz));
        mob.targetX=static_cast<float>(mob.x)+hfx/hfd*18.0f;
        mob.targetY=static_cast<float>(mob.y)+hfy/hfd*18.0f;
        mob.targetZ=static_cast<float>(mob.z)+hfz/hfd*18.0f;
        mob.state = MobState::Flee;
    } else if (mob.state == MobState::Flee) {
        mob.state = MobState::Idle; mob.aiTimer = 0.0f;
    }

    mob.eatTimer   -= dt;
    mob.drinkTimer -= dt;

    switch (mob.state) {
    case MobState::Flee:
        mobMoveTo(mob, mob.targetX, mob.targetY, mob.targetZ, 4.2f, dt);
        break;

    case MobState::Eat:
        mob.actionTimer -= dt;
        if (mob.actionTimer <= 0.0f) {
            int bx = static_cast<int>(std::floor(mob.x));
            int fy = static_cast<int>(std::floor(mob.y - 0.05));
            int bz = static_cast<int>(std::floor(mob.z));
            if (world_.getBlock(bx, fy, bz) == BlockType::Grass) {
                world_.setBlock(bx, fy, bz, BlockType::Dirt);
                world_.markDirty(world_.worldToChunkPos(bx, fy, bz));
            }
            mob.eatTimer = mob.rnd(25.0f, 38.0f);
            mob.state = MobState::Idle; mob.aiTimer = 1.5f;
        }
        break;

    case MobState::Drink:
        mob.actionTimer -= dt;
        if (mob.actionTimer <= 0.0f) {
            mob.drinkTimer = mob.rnd(250.0f, 330.0f);
            mob.state = MobState::Idle; mob.aiTimer = 2.0f;
        }
        break;

    case MobState::Walk: {
        bool arrived = mobMoveTo(mob, mob.targetX, mob.targetY, mob.targetZ, 1.4f, dt);
        mob.aiTimer -= dt;
        if (arrived || mob.aiTimer <= 0.0f) {
            mob.state  = MobState::Idle;
            mob.aiTimer = arrived ? 0.3f : 0.0f;
        }
        break;
    }

    default: // Idle
        mob.aiTimer -= dt;
        if (mob.aiTimer > 0.0f) break;

        // Trigger eat
        if (mob.eatTimer <= 0.0f) {
            int bx = static_cast<int>(std::floor(mob.x));
            int fy = static_cast<int>(std::floor(mob.y - 0.05));
            int bz = static_cast<int>(std::floor(mob.z));
            if (world_.getBlock(bx, fy, bz) == BlockType::Grass) {
                mob.state = MobState::Eat; mob.actionTimer = 3.0f; break;
            }
        }

        // Trigger drink — walk to nearest water
        if (mob.drinkTimer <= 0.0f) {
            int sx = static_cast<int>(mob.x), sy = static_cast<int>(mob.y);
            int sz = static_cast<int>(mob.z);
            bool found = false;
            for (int r = 3; r <= 28 && !found; r += 3) {
                for (int dx2 = -r; dx2 <= r && !found; dx2 += r*2) {
                    for (int dz2 = -r; dz2 <= r && !found; ++dz2) {
                        if (world_.getBlock(sx+dx2, sy, sz+dz2) == BlockType::Water ||
                            world_.getBlock(sx+dx2, sy-1, sz+dz2) == BlockType::Water) {
                            mob.targetX = static_cast<float>(sx+dx2);
                            mob.targetY = static_cast<float>(mob.y);
                            mob.targetZ = static_cast<float>(sz+dz2);
                            mob.state = MobState::Walk;
                            mob.aiTimer = 25.0f;
                            found = true;
                        }
                    }
                }
            }
            if (found) { mob.drinkTimer = mob.rnd(60, 120); break; }
            mob.drinkTimer = 60.0f;
        }

        // Random walk (gravity-relative horizontal offset)
        {
            float angle = mob.rnd(0, 6.2832f);
            float dist  = mob.rnd(5.0f, 14.0f);
            int upAx = (std::abs(mob.upY)>0.5f)?1:(std::abs(mob.upX)>0.5f?0:2);
            int ha[2], hk=0; for(int a=0;a<3;++a) if(a!=upAx) ha[hk++]=a;
            float hoff[3]={0,0,0};
            hoff[ha[0]]=std::cos(angle)*dist; hoff[ha[1]]=std::sin(angle)*dist;
            float tx=static_cast<float>(mob.x)+hoff[0];
            float ty=static_cast<float>(mob.y)+hoff[1];
            float tz=static_cast<float>(mob.z)+hoff[2];
            int gbx=static_cast<int>(std::floor(tx-mob.upX));
            int gby=static_cast<int>(std::floor(ty-mob.upY));
            int gbz=static_cast<int>(std::floor(tz-mob.upZ));
            BlockType below=world_.getBlock(gbx,gby,gbz);
            if (below==BlockType::Water||
                world_.getBlock(int(std::floor(tx)),int(std::floor(ty)),int(std::floor(tz)))==BlockType::Water) {
                hoff[ha[0]]=-hoff[ha[0]]; hoff[ha[1]]=-hoff[ha[1]];
                tx=static_cast<float>(mob.x)+hoff[0]; ty=static_cast<float>(mob.y)+hoff[1]; tz=static_cast<float>(mob.z)+hoff[2];
            }
            mob.targetX=tx; mob.targetY=ty; mob.targetZ=tz;
            mob.state = MobState::Walk;
            mob.aiTimer = mob.rnd(5.0f, 13.0f);
        }
        break;
    }

    mobApplyPhysics(mob, dt);
}

// ── Wolf AI ───────────────────────────────────────────────────────────────────

void App::updateWolf(Mob& mob, float dt) {

    float playerDist = distToPlayer(mob);

    // Sheep hunt overrides stalk/idle
    int sheepIdx = nearestMobIdx(mob, MobType::Sheep, 28.0f);

    switch (mob.state) {
    case MobState::Idle:
        mob.aiTimer -= dt;
        if (mob.aiTimer > 0.0f) break;
        // Check for sheep first
        if (sheepIdx >= 0) {
            mob.state = MobState::HuntSheep; mob.targetMobIdx = sheepIdx; break;
        }
        // Check for player
        if (playerDist < 22.0f) {
            mob.state = MobState::Stalk; mob.stalkTimer = 3.0f; break;
        }
        // Patrol (gravity-relative horizontal offset)
        {
            float ang=mob.rnd(0,6.2832f), pdist=mob.rnd(10,18);
            int upAx=(std::abs(mob.upY)>0.5f)?1:(std::abs(mob.upX)>0.5f?0:2);
            int ha[2],hk=0; for(int a=0;a<3;++a) if(a!=upAx) ha[hk++]=a;
            float hoff[3]={0,0,0};
            hoff[ha[0]]=std::cos(ang)*pdist; hoff[ha[1]]=std::sin(ang)*pdist;
            float ptx=static_cast<float>(mob.x)+hoff[0];
            float pty=static_cast<float>(mob.y)+hoff[1];
            float ptz=static_cast<float>(mob.z)+hoff[2];
            int pgx=int(std::floor(ptx-mob.upX)),pgy=int(std::floor(pty-mob.upY)),pgz=int(std::floor(ptz-mob.upZ));
            if (world_.getBlock(pgx,pgy,pgz)==BlockType::Water) {
                hoff[ha[0]]=-hoff[ha[0]]; hoff[ha[1]]=-hoff[ha[1]];
                ptx=static_cast<float>(mob.x)+hoff[0];
                pty=static_cast<float>(mob.y)+hoff[1];
                ptz=static_cast<float>(mob.z)+hoff[2];
            }
            mob.targetX=ptx; mob.targetY=pty; mob.targetZ=ptz;
        }
        mob.state = MobState::Walk;
        mob.aiTimer = mob.rnd(6.0f, 14.0f);
        break;

    case MobState::Walk: {
        bool arrived = mobMoveTo(mob, mob.targetX, mob.targetY, mob.targetZ, 3.0f, dt);
        mob.aiTimer -= dt;
        if (arrived || mob.aiTimer <= 0.0f) {
            mob.state  = MobState::Idle;
            mob.aiTimer = arrived ? 0.2f : 0.0f;
        }
        // Switch to hunt if sheep nearby
        if (sheepIdx >= 0) { mob.state = MobState::HuntSheep; mob.targetMobIdx = sheepIdx; }
        break;
    }

    case MobState::HuntSheep:
        if (mob.targetMobIdx < 0 || mob.targetMobIdx >= static_cast<int>(mobs_.size()) ||
            mobs_[mob.targetMobIdx].dead) {
            mob.state = MobState::Idle; mob.aiTimer = 2.0f; break;
        }
        {
            Mob& sheep = mobs_[mob.targetMobIdx];
            float dx = static_cast<float>(sheep.x - mob.x);
            float dz = static_cast<float>(sheep.z - mob.z);
            float d  = std::sqrt(dx*dx + dz*dz);
            if (d < 1.3f) {
                sheep.dead = true;
                mob.state = MobState::Idle; mob.aiTimer = 4.0f;
            } else {
                mobMoveTo(mob, static_cast<float>(sheep.x), static_cast<float>(sheep.y), static_cast<float>(sheep.z), 5.5f, dt);
            }
        }
        break;

    case MobState::Stalk:
        // Look at player, slowly close distance
        mob.stalkTimer -= dt;
        mobMoveTo(mob, static_cast<float>(cameraX_), static_cast<float>(cameraY_), static_cast<float>(cameraZ_), 1.5f, dt);
        if (mob.stalkTimer <= 0.0f || playerDist < 5.0f)
            mob.state = MobState::Chase;
        if (playerDist > 32.0f || sheepIdx >= 0) {
            mob.state = MobState::Idle; mob.aiTimer = 1.0f;
        }
        break;

    case MobState::Chase:
        if (playerDist > 35.0f) { mob.state = MobState::Idle; mob.aiTimer = 2.0f; break; }
        if (sheepIdx >= 0)       { mob.state = MobState::HuntSheep; mob.targetMobIdx = sheepIdx; break; }
        mobMoveTo(mob, static_cast<float>(cameraX_), static_cast<float>(cameraY_), static_cast<float>(cameraZ_), 15.0f, dt);
        mob.biteCooldown -= dt;
        if (playerDist < 1.3f && mob.biteCooldown <= 0.0f) {
            mob.biteCooldown = 1.0f;   // 1-second cooldown between bites
            playerHp_ -= 4.0f;
            if (playerHp_ <= 0.0f) wolfKilledPlayer_ = true;
        }
        break;

    default:
        mob.state = MobState::Idle; mob.aiTimer = 1.0f;
    }

    mobApplyPhysics(mob, dt);
}

// ── Human AI ──────────────────────────────────────────────────────────────────

void App::updateHuman(Mob& mob, float dt) {

    // Flee wolf — always top priority
    int wolfIdx = nearestMobIdx(mob, MobType::Wolf, 14.0f);
    if (wolfIdx >= 0) {
        if (mob.state != MobState::Flee) {
            const Mob& wolf = mobs_[wolfIdx];
            float dx3=static_cast<float>(mob.x-wolf.x),dy3=static_cast<float>(mob.y-wolf.y),dz3=static_cast<float>(mob.z-wolf.z);
            float fdot=dx3*mob.upX+dy3*mob.upY+dz3*mob.upZ;
            float hfx=dx3-mob.upX*fdot,hfy=dy3-mob.upY*fdot,hfz=dz3-mob.upZ*fdot;
            float hfd=std::max(0.01f,std::sqrt(hfx*hfx+hfy*hfy+hfz*hfz));
            mob.targetX=static_cast<float>(mob.x)+hfx/hfd*18.0f;
            mob.targetY=static_cast<float>(mob.y)+hfy/hfd*18.0f;
            mob.targetZ=static_cast<float>(mob.z)+hfz/hfd*18.0f;
            mob.state = MobState::Flee;
        }
    } else if (mob.state == MobState::Flee) {
        mob.state = MobState::Idle; mob.aiTimer = 0.0f;
    }

    switch (mob.state) {
    case MobState::Flee:
        mobMoveTo(mob, mob.targetX, mob.targetY, mob.targetZ, 6.0f, dt);
        break;

    case MobState::Chop:
        // No longer used — redirect instantly to building
        mob.buildStep = -1;
        mob.state = MobState::Build;
        break;

    case MobState::Build:
        {
            // Magical infinite-resource builder: humans stand in place and
            // materialise blocks at any distance. Up to 30 blocks per mob per
            // frame so the game loop stays responsive (14 mobs × 30 = 420/frame).
            //
            // Safety rule: NEVER place a block whose column contains any living
            // mob — skip it and move on. This prevents mobs being buried when
            // the terrain-clearing or fill phases run beneath them.
            int placed = 0;
            for (int iter = 0; iter < 2000 && placed < 30; ++iter) {
                if (mob.buildStep < 0)
                    mob.buildStep = cityNextBlock_++;

                int bx, by, bz;
                BlockType bt;
                if (!cityGetBlock(mob.buildStep, bx, by, bz, bt)) {
                    cityDone_ = true;
                    mob.state = MobState::Idle; mob.aiTimer = 0.0f;
                    break;
                }
                if (world_.getBlock(bx, by, bz) == bt) {
                    mob.buildStep = cityNextBlock_++;
                    continue; // already right — scan next
                }

                // Safety: don't touch a block column occupied by any mob
                bool occupied = false;
                for (const auto& m2 : mobs_) {
                    if (m2.dead) continue;
                    int mx = static_cast<int>(std::floor(m2.x));
                    int my = static_cast<int>(std::floor(m2.y));
                    int mz = static_cast<int>(std::floor(m2.z));
                    if (mx == bx && mz == bz && by >= my - 1 && by <= my + 2) {
                        occupied = true;
                        break;
                    }
                }
                if (occupied) {
                    mob.buildStep = cityNextBlock_++;
                    continue; // skip — try next block this frame
                }

                world_.setBlock(bx, by, bz, bt);
                world_.markDirty(world_.worldToChunkPos(bx, by, bz));
                mob.buildStep = cityNextBlock_++;
                mob.actionTimer = 0.0f;
                ++placed;
            }

            // Walk around near the castle so humans look busy
            mob.aiTimer -= dt;
            if (mob.aiTimer <= 0.0f) {
                float ang = mob.rnd(0.f, 6.2832f), d = mob.rnd(3.f, 8.f);
                int upAx = (std::abs(mob.upY)>0.5f)?1:(std::abs(mob.upX)>0.5f?0:2);
                int ha[2], hk=0; for(int a=0;a<3;++a) if(a!=upAx) ha[hk++]=a;
                float ho[3]={0,0,0};
                ho[ha[0]]=std::cos(ang)*d; ho[ha[1]]=std::sin(ang)*d;
                mob.targetX=static_cast<float>(mob.x)+ho[0];
                mob.targetY=static_cast<float>(mob.y);
                mob.targetZ=static_cast<float>(mob.z)+ho[2];
                mob.aiTimer = mob.rnd(2.f, 5.f);
            }
            mobMoveTo(mob, mob.targetX, mob.targetY, mob.targetZ, 6.0f, dt);
        }
        break;

    case MobState::Walk:
        mob.aiTimer -= dt;
        mobMoveTo(mob, mob.targetX, mob.targetY, mob.targetZ, 12.0f, dt);
        if (mob.aiTimer <= 0.0f) mob.state = MobState::Idle;
        break;

    case MobState::Guard:
        {
            // Walk to guard post, then face outward and pace slightly
            mob.guardPaceTimer -= dt;
            float dx3 = mob.guardPostX - static_cast<float>(mob.x);
            float dz3 = mob.guardPostZ - static_cast<float>(mob.z);
            float distPost = std::sqrt(dx3*dx3 + dz3*dz3);
            if (distPost > 1.5f) {
                mobMoveTo(mob, mob.guardPostX, mob.guardPostY, mob.guardPostZ, 6.0f, dt);
            } else if (mob.guardPaceTimer <= 0.0f) {
                // Pace 2 blocks left/right along the wall
                mob.guardPaceTimer = mob.rnd(6.0f, 14.0f);
                mob.guardPaceDir   = -mob.guardPaceDir;
                // Compute a perpendicular offset (horizontal, along wall face)
                int upAx = (std::abs(mob.upY) > 0.5f) ? 1 : (std::abs(mob.upX) > 0.5f ? 0 : 2);
                float offsets[3] = {0, 0, 0};
                // Wall runs along H1 axis; offset along H1
                offsets[cityH1Axis_] = static_cast<float>(mob.guardPaceDir) * 2.0f;
                mob.targetX = mob.guardPostX + offsets[0];
                mob.targetY = mob.guardPostY;
                mob.targetZ = mob.guardPostZ + offsets[2];
                (void)upAx;
            } else {
                // Stand still — walk toward pace target if set
                mobMoveTo(mob, mob.targetX, mob.targetY, mob.targetZ, 3.0f, dt);
            }
            // Guards return to post if they drift too far
            if (distPost > 8.0f) {
                mob.targetX = mob.guardPostX;
                mob.targetY = mob.guardPostY;
                mob.targetZ = mob.guardPostZ;
            }
        }
        break;

    case MobState::Wander:
        {
            // Wander inside the castle courtyard
            mob.aiTimer -= dt;
            bool arrived = mobMoveTo(mob, mob.targetX, mob.targetY, mob.targetZ, 5.0f, dt);
            if (arrived || mob.aiTimer <= 0.0f) {
                mob.state  = MobState::Idle;
                mob.aiTimer = mob.rnd(2.0f, 6.0f);
            }
        }
        break;

    default: // Idle — pick next task immediately, no waiting
        if (cityDone_) {
            // Post-construction permanent roles
            if (mob.aiRole == 1) {
                mob.state = MobState::Guard;
                break;
            }
            // Workers wander inside the courtyard
            {
                constexpr int WALL_T = 3, INNER = 38;
                float lc = mob.rnd(static_cast<float>(WALL_T + 2),
                                   static_cast<float>(WALL_T + INNER - 2));
                float lr = mob.rnd(static_cast<float>(WALL_T + 2),
                                   static_cast<float>(WALL_T + INNER - 2));
                int wbx, wby, wbz;
                cityLW(static_cast<int>(lc), 1, static_cast<int>(lr), wbx, wby, wbz);
                mob.targetX = static_cast<float>(wbx) + 0.5f;
                mob.targetY = static_cast<float>(wby) + 0.5f;
                mob.targetZ = static_cast<float>(wbz) + 0.5f;
                mob.state   = MobState::Wander;
                mob.aiTimer = mob.rnd(3.0f, 8.0f);
            }
            break;
        }

        // Always build — infinite resources, no harvesting
        mob.buildStep = -1;
        mob.state = MobState::Build;
        break;
    }

    mobApplyPhysics(mob, dt);
}

// ── Main mob update ───────────────────────────────────────────────────────────

void App::updateMobs(float dt) {
    if (gameState_ != GameState::Playing) return;

    attackCooldown_ = std::max(0.0f, attackCooldown_ - dt);

    for (auto& mob : mobs_) {
        if (mob.dead) continue;
        switch (mob.type) {
            case MobType::Sheep: updateSheep(mob, dt); break;
            case MobType::Wolf:  updateWolf (mob, dt); break;
            case MobType::Human: updateHuman(mob, dt); break;
        }
    }

    // Remove dead mobs (erase–remove idiom, keeps indices stable until next frame)
    mobs_.erase(std::remove_if(mobs_.begin(), mobs_.end(),
        [](const Mob& m){ return m.dead; }), mobs_.end());
}

// ── Dropped items ──────────────────────────────────────────────────────────────

void App::spawnDrop(double x, double y, double z, BlockType type, int count) {
    DroppedItem d;
    d.x = x; d.y = y; d.z = z;
    d.velGrav = 3.0f;
    d.type = type; d.count = count;
    d.lifetime = 300.0f;
    drops_.push_back(d);
}

void App::updateDrops(float dt) {
    const float gx = gravityUp_.x, gy = gravityUp_.y, gz = gravityUp_.z;
    for (auto& d : drops_) {
        d.lifetime -= dt;
        d.animTime += dt;
        d.velGrav -= 20.0f * dt;
        // Move along gravity up axis (positive velGrav = upward)
        d.x += gx * d.velGrav * dt;
        d.y += gy * d.velGrav * dt;
        d.z += gz * d.velGrav * dt;
        // Check block slightly below (in -up direction)
        int sbx = (int)std::floor(d.x - 0.05 * gx);
        int sby = (int)std::floor(d.y - 0.05 * gy);
        int sbz = (int)std::floor(d.z - 0.05 * gz);
        if (isSolid(world_.getBlock(sbx, sby, sbz))) {
            // Snap to top of block along gravity axis
            double proj    = d.x*gx + d.y*gy + d.z*gz;
            double snapped = std::floor(proj - 0.05) + 1.0;
            double delta   = snapped - proj;
            d.x += gx * delta;
            d.y += gy * delta;
            d.z += gz * delta;
            if (d.velGrav < 0) d.velGrav = 0.0f;
        }
        // Check distance from player feet (camera is at eye height = 1.5 above feet along gravity)
        float feetX = (float)(cameraX_ - gravityUp_.x * 1.5);
        float feetY = (float)(cameraY_ - gravityUp_.y * 1.5);
        float feetZ = (float)(cameraZ_ - gravityUp_.z * 1.5);
        float dx2 = (float)d.x - feetX, dy2 = (float)d.y - feetY, dz2 = (float)d.z - feetZ;
        if (dx2*dx2 + dy2*dy2 + dz2*dz2 < 1.5f*1.5f) {
            addToInventory(d.type);
            audio_.playItemPickup();
            d.lifetime = -1.0f;
        }
    }
    drops_.erase(std::remove_if(drops_.begin(), drops_.end(),
        [](const DroppedItem& d){ return d.lifetime <= 0.0f; }), drops_.end());
}

// ── Crafting ──────────────────────────────────────────────────────────────────
