/* ────────────────────────────────────────────────────────────────
   Procedural Generation — Random, Noise, Wave, LootTable
──────────────────────────────────────────────────────────────── */
/* ────────────────────────────────────────────────────────────────
   SEEDED RANDOM (xoshiro128**)
──────────────────────────────────────────────────────────────── */
class SeededRandom {
    constructor(seed = Date.now()) {
        this.s = new Uint32Array(4);
        // SplitMix32 seed expansion
        let z = seed >>> 0;
        for (let i = 0; i < 4; i++) {
            z = (z + 0x9e3779b9) >>> 0;
            let t = z ^ (z >>> 16);
            t = Math.imul(t, 0x21f0aaad);
            t = t ^ (t >>> 15);
            t = Math.imul(t, 0x735a2d97);
            t = t ^ (t >>> 15);
            this.s[i] = t >>> 0;
        }
    }
    /** Returns [0, 1). */
    next() {
        const s = this.s;
        const result = Math.imul(rotl(Math.imul(s[1], 5), 7), 9) >>> 0;
        const t = s[1] << 9;
        s[2] ^= s[0];
        s[3] ^= s[1];
        s[1] ^= s[2];
        s[0] ^= s[3];
        s[2] ^= t;
        s[3] = rotl(s[3], 11);
        return (result >>> 0) / 0x100000000;
    }
    /** Returns integer in [min, max] (inclusive). */
    int(min, max) {
        return min + Math.floor(this.next() * (max - min + 1));
    }
    /** Returns float in [min, max). */
    float(min, max) {
        return min + this.next() * (max - min);
    }
    /** Returns true with given probability (0-1). */
    chance(probability) {
        return this.next() < probability;
    }
    /** Pick a random element from an array. */
    pick(arr) {
        return arr[Math.floor(this.next() * arr.length)];
    }
    /** Shuffle array in-place (Fisher-Yates). */
    shuffle(arr) {
        for (let i = arr.length - 1; i > 0; i--) {
            const j = Math.floor(this.next() * (i + 1));
            const tmp = arr[i];
            arr[i] = arr[j];
            arr[j] = tmp;
        }
        return arr;
    }
    /** Weighted random pick. Returns index. */
    weighted(weights) {
        let total = 0;
        for (const w of weights)
            total += w;
        let r = this.next() * total;
        for (let i = 0; i < weights.length; i++) {
            r -= weights[i];
            if (r <= 0)
                return i;
        }
        return weights.length - 1;
    }
    /** Random color. */
    color(alpha = 1) {
        return { r: this.next(), g: this.next(), b: this.next(), a: alpha };
    }
    /** Random unit vector. */
    unitVec2() {
        const angle = this.next() * Math.PI * 2;
        return { x: Math.cos(angle), y: Math.sin(angle) };
    }
    /** Random point inside a circle. */
    insideCircle(radius) {
        const a = this.next() * Math.PI * 2;
        const r = Math.sqrt(this.next()) * radius;
        return { x: Math.cos(a) * r, y: Math.sin(a) * r };
    }
}
function rotl(x, k) {
    return ((x << k) | (x >>> (32 - k))) >>> 0;
}
/* ────────────────────────────────────────────────────────────────
   PERLIN NOISE (2D, improved)
──────────────────────────────────────────────────────────────── */
class PerlinNoise {
    constructor(seed = 0) {
        this.perm = new Uint8Array(512);
        const rng = new SeededRandom(seed);
        const p = new Uint8Array(256);
        for (let i = 0; i < 256; i++)
            p[i] = i;
        // Shuffle
        for (let i = 255; i > 0; i--) {
            const j = rng.int(0, i);
            const tmp = p[i];
            p[i] = p[j];
            p[j] = tmp;
        }
        for (let i = 0; i < 512; i++)
            this.perm[i] = p[i & 255];
    }
    /** Get noise value at (x, y) → [-1, 1]. */
    noise2D(x, y) {
        const X = Math.floor(x) & 255;
        const Y = Math.floor(y) & 255;
        const xf = x - Math.floor(x);
        const yf = y - Math.floor(y);
        const u = fade(xf);
        const v = fade(yf);
        const p = this.perm;
        const A = p[X] + Y;
        const B = p[X + 1] + Y;
        return lerp(lerp(grad(p[A], xf, yf), grad(p[B], xf - 1, yf), u), lerp(grad(p[A + 1], xf, yf - 1), grad(p[B + 1], xf - 1, yf - 1), u), v);
    }
    /** Fractal Brownian Motion (layered noise). */
    fbm(x, y, octaves = 4, lacunarity = 2, gain = 0.5) {
        let value = 0;
        let amplitude = 1;
        let frequency = 1;
        let maxValue = 0;
        for (let i = 0; i < octaves; i++) {
            value += this.noise2D(x * frequency, y * frequency) * amplitude;
            maxValue += amplitude;
            amplitude *= gain;
            frequency *= lacunarity;
        }
        return value / maxValue;
    }
}
function fade(t) {
    return t * t * t * (t * (t * 6 - 15) + 10);
}
function lerp(a, b, t) {
    return a + t * (b - a);
}
function grad(hash, x, y) {
    const h = hash & 3;
    const u = h < 2 ? x : y;
    const v = h < 2 ? y : x;
    return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
}
class WaveGenerator {
    constructor(seed) {
        this.rng = new SeededRandom(seed);
    }
    generateWave(waveNumber, config) {
        const totalEnemies = Math.floor(config.baseEnemyCount * Math.pow(config.countGrowth, waveNumber - 1));
        // Filter types available at this wave
        const available = config.typeProbabilities.filter(tp => waveNumber >= tp.minWave);
        if (available.length === 0)
            return [];
        const weights = available.map(a => a.weight);
        const entries = [];
        let remaining = totalEnemies;
        let cumulativeDelay = 0;
        while (remaining > 0) {
            const groupCount = Math.min(remaining, this.rng.int(config.groupSize[0], config.groupSize[1]));
            const typeIdx = this.rng.weighted(weights);
            entries.push({
                type: available[typeIdx].type,
                count: groupCount,
                delay: cumulativeDelay,
                spawnInterval: this.rng.float(config.spawnInterval[0], config.spawnInterval[1]),
            });
            remaining -= groupCount;
            cumulativeDelay += this.rng.float(config.groupDelay[0], config.groupDelay[1]);
        }
        return entries;
    }
}
class LootTable {
    constructor(drops, seed) {
        this.drops = drops;
        this.rng = new SeededRandom(seed);
    }
    /** Roll N items from the table. */
    roll(count, level = 1) {
        var _a;
        const eligible = this.drops.filter(d => !d.minLevel || level >= d.minLevel);
        if (eligible.length === 0)
            return [];
        const weights = eligible.map(d => d.weight);
        const results = [];
        const rollCounts = new Map();
        for (let i = 0; i < count; i++) {
            // rebuild weights excluding maxed-out items
            const currentWeights = eligible.map((d, idx) => {
                var _a;
                if (d.maxPerRoll != null) {
                    const used = (_a = rollCounts.get(d.id)) !== null && _a !== void 0 ? _a : 0;
                    if (used >= d.maxPerRoll)
                        return 0;
                }
                return weights[idx];
            });
            if (currentWeights.every(w => w === 0))
                break;
            const idx = this.rng.weighted(currentWeights);
            const drop = eligible[idx];
            const qty = this.rng.int(drop.quantity[0], drop.quantity[1]);
            results.push({ id: drop.id, quantity: qty });
            rollCounts.set(drop.id, ((_a = rollCounts.get(drop.id)) !== null && _a !== void 0 ? _a : 0) + 1);
        }
        return results;
    }
    /** Simple yes/no chance to drop each item independently. */
    rollEach(level = 1) {
        const results = [];
        for (const drop of this.drops) {
            if (drop.minLevel != null && level < drop.minLevel)
                continue;
            // Normalize weight as probability (assume max total = 100)
            if (this.rng.chance(drop.weight / 100)) {
                results.push({ id: drop.id, quantity: this.rng.int(drop.quantity[0], drop.quantity[1]) });
            }
        }
        return results;
    }
}
/* ────────────────────────────────────────────────────────────────
   PROCEDURAL SPRITE — Generate simple procedural sprites
──────────────────────────────────────────────────────────────── */
class ProceduralSprite {
    constructor(seed) {
        this.rng = new SeededRandom(seed);
    }
    /**
     * Generate a symmetrical pixel art sprite as ImageData.
     * Good for enemies, ships, items.
     */
    generate(width, height, palette, symmetry = 'horizontal') {
        const data = new ImageData(width, height);
        const halfW = symmetry === 'horizontal' || symmetry === 'both' ? Math.ceil(width / 2) : width;
        const halfH = symmetry === 'vertical' || symmetry === 'both' ? Math.ceil(height / 2) : height;
        // Generate half, then mirror
        const grid = new Array(halfW * halfH).fill(false);
        const colors = new Array(halfW * halfH).fill(0);
        for (let i = 0; i < grid.length; i++) {
            if (this.rng.chance(0.45)) {
                grid[i] = true;
                colors[i] = this.rng.int(0, palette.length - 1);
            }
        }
        // Write to ImageData
        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
                let sx = x;
                let sy = y;
                if ((symmetry === 'horizontal' || symmetry === 'both') && x >= halfW) {
                    sx = width - 1 - x;
                }
                if ((symmetry === 'vertical' || symmetry === 'both') && y >= halfH) {
                    sy = height - 1 - y;
                }
                const idx = sy * halfW + sx;
                if (grid[idx]) {
                    const c = palette[colors[idx]];
                    const px = (y * width + x) * 4;
                    data.data[px] = (c.r * 255) | 0;
                    data.data[px + 1] = (c.g * 255) | 0;
                    data.data[px + 2] = (c.b * 255) | 0;
                    data.data[px + 3] = (c.a * 255) | 0;
                }
            }
        }
        return data;
    }
}

export { LootTable, PerlinNoise, ProceduralSprite, SeededRandom, WaveGenerator };
//# sourceMappingURL=Procedural.js.map
