Newer
Older
rpg / apps / game / src / data / enemies.ts
/**
 * Враги демо-боя: «пепельные сгустки» — оживший пепел, просыпается от гулкого звука.
 * Чистые данные (без Pixi) — используются боевой системой и тестами.
 */

export type EnemyKindId = 'crawler' | 'spitter' | 'heavy';

export interface EnemyKindDef {
    id: EnemyKindId;
    /** Здоровье. */
    hp: number;
    /** Скорость, мировых юнитов/сек (1 юнит = 1 тайл). */
    speed: number;
    /** Радиус тела для попаданий, юнитов. */
    radius: number;
    /** Дистанция начала атаки, юнитов. */
    attackRange: number;
    /** Урон атаки. */
    attackDamage: number;
    /** Пауза между атаками, сек. */
    attackCooldown: number;
    /** Замах до удара, сек (у ползуна 0 — контактный урон). */
    windup: number;
    /** Восстановление после удара, сек. */
    recover: number;
    /** Плевун держит дистанцию (юнитов) — отходит при сближении. */
    keepDistance?: number;
    /** Дроп «пепельной пыли». */
    motes: number;
    /** Кадры из атласа chars/clumps_sheet.json. */
    frames: [string, string];
    // --- ИИ (необязательные: withDefaults подставляет разумные значения) ---

    /** Дистанция обнаружения героя (при прямой видимости), юнитов. */
    aggroRange?: number;
    /** Секунд без прямой видимости, после которых враг отступает к дому. */
    deaggroSec?: number;
    /** Ниже этой доли hp — отступает (0 — не отступает никогда). */
    fleeBelowHpFraction?: number;
    /** При отступлении убегает, пока герой ближе этого радиуса, юнитов. */
    fleeRange?: number;
    /** Радиус слуха шума (уровень >= 0.35), юнитов. */
    hearRadius?: number;
    /** Секунд настороженности без новой информации, после которых враг возвращается. */
    warySec?: number;
}

/** Патруль отдельного спавна: тайлы маршрута (проходится туда-обратно) и пауза на точке. */
export interface EnemyPatrolDef {
    points: { x: number; y: number }[];
    pauseSec: number;
}

/** Дефолты полей ИИ (контент задаёт только отличия). */
export function withDefaults(kind: EnemyKindDef): EnemyKindDef {
    return {
        ...kind,
        aggroRange: kind.aggroRange ?? 4,
        deaggroSec: kind.deaggroSec ?? 2.5,
        fleeBelowHpFraction: kind.fleeBelowHpFraction ?? 0,
        fleeRange: kind.fleeRange ?? 4,
        hearRadius: kind.hearRadius ?? 5,
        warySec: kind.warySec ?? 2.5
    };
}

export const ENEMY_KINDS: Record<EnemyKindId, EnemyKindDef> = {
    crawler: {
        id: 'crawler',
        hp: 3,
        speed: 0.7,
        radius: 0.2,
        attackRange: 0.4,
        attackDamage: 1,
        attackCooldown: 0.8,
        windup: 0,
        recover: 0.3,
        motes: 1,
        frames: ['clump_crawler_1', 'clump_crawler_2'],
        // Сторожит пепел, шатаясь между кустов; слышит шаги вблизи.
        hearRadius: 3.5
    },
    spitter: {
        id: 'spitter',
        hp: 2,
        speed: 0.55,
        radius: 0.2,
        attackRange: 3.0,
        attackDamage: 1,
        attackCooldown: 2.2,
        windup: 0.35,
        recover: 0.4,
        keepDistance: 2.25,
        motes: 1,
        frames: ['clump_spitter_1', 'clump_spitter_2'],
        // Обстреливает издали и не отступает — стоит насмерть.
        aggroRange: 5
    },
    heavy: {
        id: 'heavy',
        hp: 8,
        speed: 0.45,
        radius: 0.3,
        attackRange: 0.8,
        attackDamage: 2,
        attackCooldown: 2,
        windup: 0.6,
        recover: 0.5,
        motes: 3,
        frames: ['clump_heavy_1', 'clump_heavy_2'],
        // Крупный сгусток: глуховат, но с трудом убитый — отступает при малом hp.
        hearRadius: 4,
        fleeBelowHpFraction: 0.35,
        fleeRange: 4.5,
        warySec: 3
    }
};

/** Точки спавна и патрули врагов задаются в data/locations.ts (AREAS[*].enemies). */