Newer
Older
rpg / apps / game / src / data / map.ts
import type { TileMapData, TallSpec } from '@rpg/engine';

/** id тайлов луга «Пепельные луга». */
export const TILES = {
    GRASS: 0,
    PATH: 1,
    WATER: 2,
    TREE: 3,
    ASH: 4,
    BELLFLOWER: 5,
    TOWER: 6,
    HOUSE: 7,
    FLOOR: 8,
    WALL: 9,
    WELL: 10
} as const;

const BLOCKED = [TILES.WATER, TILES.TREE, TILES.TOWER, TILES.HOUSE, TILES.WALL, TILES.WELL];

/** Дерево — высокий объект 1.5 мировых юнита (48 px), под ним трава. */
const TALL_OBJECTS: Record<number, TallSpec> = {
    [TILES.TREE]: { height: 1.5, ground: TILES.GRASS },
    [TILES.TOWER]: { height: 2.5, ground: TILES.PATH },
    [TILES.HOUSE]: { height: 1.75, ground: TILES.ASH }
};

/** Детерминированный ГПСЧ, чтобы локация выглядела одинаково между запусками. */
function mulberry32(seed: number): () => number {
    return () => {
        seed |= 0;
        seed = (seed + 0x6d2b79f5) | 0;
        let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
        t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
        return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
}

/**
 * Локация «Выжженные луга»: поляны травы, тропа через карту,
 * пара прудов и рощи деревьев по краям.
 */
export function buildMeadowsMap(width = 28, height = 28): TileMapData {
    const rng = mulberry32(20260905);
    const tiles: number[] = new Array(width * height).fill(TILES.GRASS);
    const at = (x: number, y: number) => y * width + x;

    // Пятна пепла (декоративные).
    for (let i = 0; i < 40; i++) {
        const x = Math.floor(rng() * width);
        const y = Math.floor(rng() * height);
        const r = 1 + Math.floor(rng() * 2);
        for (let dy = -r; dy <= r; dy++) {
            for (let dx = -r; dx <= r; dx++) {
                const nx = x + dx;
                const ny = y + dy;
                if (nx >= 0 && ny >= 0 && nx < width && ny < height && rng() < 0.7) {
                    tiles[at(nx, ny)] = TILES.ASH;
                }
            }
        }
    }

    // Тропа: ломаная слева-сверху вниз-вправо.
    let px = 2;
    let py = 2;
    while (px < width - 3 || py < height - 3) {
        tiles[at(px, py)] = TILES.PATH;
        if (rng() < 0.5 && px < width - 3) px++;
        else if (py < height - 3) py++;
        else px++;
    }

    // Пруды.
    for (const [cx, cy] of [
        [6, 16],
        [19, 7]
    ]) {
        for (let dy = 0; dy < 3; dy++) {
            for (let dx = 0; dx < 4; dx++) {
                if (rng() < 0.85) tiles[at(cx + dx, cy + dy)] = TILES.WATER;
            }
        }
    }

    // Лунные колокольчики у берегов прудов (квестовый декор, проходимы).
    for (const [fx, fy] of [
        [10, 17],
        [11, 19],
        [18, 5],
        [22, 9]
    ]) {
        if (tiles[at(fx, fy)] === TILES.GRASS || tiles[at(fx, fy)] === TILES.ASH) {
            tiles[at(fx, fy)] = TILES.BELLFLOWER;
        }
    }

    // Рощи деревьев по краям.
    for (let i = 0; i < 60; i++) {
        const edge = rng() < 0.5;
        const x = edge ? Math.floor(rng() * 4) : width - 1 - Math.floor(rng() * 4);
        const y = Math.floor(rng() * height);
        if (tiles[at(x, y)] === TILES.GRASS || tiles[at(x, y)] === TILES.ASH) {
            tiles[at(x, y)] = TILES.TREE;
        }
    }

    // Стартовая поляна вокруг центра всегда свободна.
    const cx = Math.floor(width / 2);
    const cy = Math.floor(height / 2);
    for (let dy = -1; dy <= 1; dy++) {
        for (let dx = -1; dx <= 1; dx++) {
            tiles[at(cx + dx, cy + dy)] = TILES.GRASS;
        }
    }

    // Тропа к Серым прудам: северо-западный угол (переход между локациями).
    // Вокруг тайлов перехода расчищаем поляну — рощи не должны запирать выход.
    for (let dy = -1; dy <= 2; dy++) {
        for (let dx = -1; dx <= 2; dx++) {
            const nx = 2 + dx;
            const ny = 2 + dy;
            if (nx >= 0 && ny >= 0 && nx < width && ny < height) {
                tiles[at(nx, ny)] = dx === dy || dx + 1 === dy ? TILES.PATH : TILES.GRASS;
            }
        }
    }
    tiles[at(2, 2)] = TILES.PATH;
    tiles[at(3, 3)] = TILES.PATH;

    // Тропа в Звенец: восточный край, вокруг тайлов перехода тоже расчищаем.
    for (let dy = -1; dy <= 1; dy++) {
        for (let dx = -2; dx <= 0; dx++) {
            const nx = 26 + dx;
            const ny = 14 + dy;
            if (nx >= 0 && ny >= 0 && nx < width && ny < height) {
                tiles[at(nx, ny)] = dx === -1 && dy === 0 ? TILES.PATH : TILES.GRASS;
            }
        }
    }
    tiles[at(26, 14)] = TILES.PATH;

    // Встречный колодец на лугах: телепорт обратно в Звенец (клик по тайлу).
    tiles[at(22, 20)] = TILES.WELL;

    return {
        width,
        height,
        tiles,
        blocked: [...BLOCKED],
        tall: TALL_OBJECTS
    };
}

/**
 * Локация «Серые пруды»: опасная зона — большая гладь воды, туман,
 * много пепла и сгустков. Тропа начинается в северо-западном углу
 * (обратный переход на луга).
 */
export function buildPondsMap(width = 24, height = 24): TileMapData {
    const rng = mulberry32(20260906);
    const tiles: number[] = new Array(width * height).fill(TILES.ASH);
    const at = (x: number, y: number) => y * width + x;

    // Пятна чахлой травы.
    for (let i = 0; i < 24; i++) {
        const x = Math.floor(rng() * width);
        const y = Math.floor(rng() * height);
        const r = 1 + Math.floor(rng() * 2);
        for (let dy = -r; dy <= r; dy++) {
            for (let dx = -r; dx <= r; dx++) {
                const nx = x + dx;
                const ny = y + dy;
                if (nx >= 0 && ny >= 0 && nx < width && ny < height && rng() < 0.6) {
                    tiles[at(nx, ny)] = TILES.GRASS;
                }
            }
        }
    }

    // Большая гладь в центре с рваными берегами.
    const cx = Math.floor(width / 2);
    const cy = Math.floor(height / 2);
    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            const d = Math.hypot((x - cx) / 7.5, (y - cy) / 5.5);
            if (d + (rng() - 0.5) * 0.35 < 1) tiles[at(x, y)] = TILES.WATER;
        }
    }

    // Лунные колокольчики у берегов (квестовые цветы растут только здесь).
    for (const [fx, fy] of [
        [5, 8],
        [16, 6],
        [18, 16],
        [7, 17],
        [12, 20]
    ]) {
        if (tiles[at(fx, fy)] !== TILES.WATER) tiles[at(fx, fy)] = TILES.BELLFLOWER;
    }

    // Тропа от северо-западного угла к берегу.
    let px = 3;
    let py = 3;
    while (py < height - 6 || px < width - 8) {
        tiles[at(px, py)] = TILES.PATH;
        if (rng() < 0.45 && px < width - 8) px++;
        else if (py < height - 6) py++;
        else px++;
    }

    // Сухие деревья по краям.
    for (let i = 0; i < 46; i++) {
        const edge = rng() < 0.5;
        const x = edge ? Math.floor(rng() * 3) : width - 1 - Math.floor(rng() * 3);
        const y = Math.floor(rng() * height);
        if (tiles[at(x, y)] !== TILES.WATER) tiles[at(x, y)] = TILES.TREE;
    }

    // Поляна у входа свободна; тайлы перехода — тропа.
    for (let dy = 0; dy <= 1; dy++) {
        for (let dx = 0; dx <= 1; dx++) {
            tiles[at(3 + dx, 3 + dy)] = dy + dx === 0 ? TILES.PATH : TILES.GRASS;
        }
    }
    tiles[at(2, 2)] = TILES.PATH;
    tiles[at(3, 3)] = TILES.PATH;

    return {
        width,
        height,
        tiles,
        blocked: [...BLOCKED],
        tall: TALL_OBJECTS
    };
}
/**
 * Локация «Звенец»: деревня-мастерская вокруг старой железнодорожной
 * водонапорной башни с колоколом. Дома из вагонных щитов, крыши из
 * платформ, посреди площади — звонное место. Безопасный хаб (без врагов).
 */
export function buildZvenetsMap(width = 28, height = 20): TileMapData {
    const rng = mulberry32(20260907);
    const tiles: number[] = new Array(width * height).fill(TILES.ASH);
    const at = (x: number, y: number) => y * width + x;

    // Пятна чахлой травы по краям.
    for (let i = 0; i < 18; i++) {
        const x = Math.floor(rng() * width);
        const y = Math.floor(rng() * height);
        const r = 1 + Math.floor(rng() * 2);
        for (let dy = -r; dy <= r; dy++) {
            for (let dx = -r; dx <= r; dx++) {
                const nx = x + dx;
                const ny = y + dy;
                if (nx >= 0 && ny >= 0 && nx < width && ny < height && rng() < 0.6) {
                    tiles[at(nx, ny)] = TILES.GRASS;
                }
            }
        }
    }

    // Площадь: утоптанный пепел в центре карты.
    for (let y = 6; y <= 13; y++) {
        for (let x = 9; x <= 19; x++) {
            if (rng() < 0.9) tiles[at(x, y)] = TILES.PATH;
        }
    }

    // Тропа: от западного края (переход на луга) к площади.
    let px = 2;
    let py = 10;
    while (px < 9 || py !== 10) {
        tiles[at(px, py)] = TILES.PATH;
        if (px < 9 && (py === 10 || rng() < 0.6)) px++;
        else if (py !== 10) py += py < 10 ? 1 : -1;
        else px++;
    }

    // Сухие деревья по краям.
    for (let i = 0; i < 30; i++) {
        const edge = rng() < 0.5;
        const x = edge ? Math.floor(rng() * 3) : width - 1 - Math.floor(rng() * 3);
        const y = Math.floor(rng() * height);
        if (tiles[at(x, y)] === TILES.ASH || tiles[at(x, y)] === TILES.GRASS) {
            tiles[at(x, y)] = TILES.TREE;
        }
    }

    // Башня с колоколом — север площади.
    tiles[at(14, 7)] = TILES.TOWER;

    // Дома: два вдоль тропы, лавка Милы — восток площади.
    tiles[at(7, 6)] = TILES.HOUSE;
    tiles[at(18, 5)] = TILES.HOUSE;
    tiles[at(21, 9)] = TILES.HOUSE;

    // Старый колодец у дома Ирвина: телепорт на луга (см. AREAS, transitions).
    tiles[at(9, 6)] = TILES.WELL;

    // Стартовая поляна и тайл перехода всегда свободны.
    tiles[at(14, 10)] = TILES.GRASS;
    tiles[at(2, 10)] = TILES.PATH;
    tiles[at(3, 10)] = TILES.PATH;

    return {
        width,
        height,
        tiles,
        blocked: [...BLOCKED],
        tall: TALL_OBJECTS
    };
}