Newer
Older
rpg / docs / engine / recipes.md

Recipes: «как сделать…»

Готовые решения типовых задач. Примеры взяты из живой игры (apps/game).

Локация с картой и камерой

Смотри apps/game/src/scenes/LocationScene.ts. Скелет:

export class LocationScene implements Scene {
    constructor(engine: Engine) {
        const data = loadMap();                       // buildMap() или parseMap(json)
        const map = new IsometricTileMap(data, tileTextures(), DEFAULT_ISO);
        engine.renderer.worldRoot.addChild(map.view);

        const size = map.screenSize;
        engine.camera.bounds = {
            x: -size.width / 2, y: 0,                 // ромб уходит в минус по X!
            width: size.width, height: size.height
        };

        // центры тайлов для позиционирования сущностей
        const p = isoToScreen(tx, ty);
        const cx = p.x, cy = p.y + DEFAULT_ISO.tileH / 2;
        engine.camera.follow(cx, cy);
    }

    update(dt: number) {
        // ... движение героя ...
        const c = tileCenter(hero.tile);
        engine.camera.follow(c.x, c.y);               // каждый тик за героем
    }
}

Click-to-move по A*

// в update сцены:
const pointer = engine.input.getPointer();
if (pointer.justPressed) {
    const worldX = pointer.x - engine.renderer.worldRoot.position.x;
    const worldY = pointer.y - engine.renderer.worldRoot.position.y;
    const target = screenToIsoExact(worldX, worldY, map.width, map.height);
    if (target) {
        this.path = findPath(map, hero.tile, target);
    }
}

// следование пути с фиксированной скоростью:
if (this.path && this.path.length > 0) {
    const next = this.path[0];
    const c = tileCenter(next.x, next.y);
    // двигаем спрайт к c, по достижении — hero.tile = next, path.shift()
}

Полная версия с анимацией ходьбы и поворотами — apps/game/src/systems/PlayerController.ts.

NPC с диалогом и флагами

// данные NPC
const NPCS = [
    {
        id: 'elder', name: 'Ирвин', tile: { x: 12, y: 10 },
        sprite: 'elder_irwin',
        dialogueFirst: 'elder_first',     // ключ графа
        dialogueRepeat: 'elder_repeat',
        flagKey: 'met_elder'
    }
];

// клик по тайлу NPC:
const npc = this.npcs.find((n) => n.def.tile.x === clicked.x && clicked.y === n.def.tile.y);
if (npc) {
    const met = gameState.hasFlag(npc.def.flagKey);
    if (!met) gameState.setFlag(npc.def.flagKey);
    runner.start(graphs[met ? npc.def.dialogueRepeat : npc.def.dialogueFirst]);
}

Квест на флагах

Квест = флаги GameState + проверки в диалогах (when/whenNot) + действия при завершении диалога:

// В графе: NPC выдаёт квест выбором с setFlags: ['quest_bells_taken'].
// Сдача квеста — узел с when: ['quest_bells_taken'], выбор с
// setFlags: ['quest_bells_done'], clearFlags: ['quest_bells_taken'].

runner.onFinish = (graph) => {
    if (graph === graphs['elder_first'] && gameState.hasFlag('quest_bells_done')) {
        gameState.setVar('gold', gameState.getNumber('gold') + 30);
        engine.events.emit('quest:done', { quest: 'bells' });
    }
};

Состояние всех квестов живёт в GameState и автоматически попадает в сейв.

Покадровая анимация ходьбы

Смотри apps/game/src/systems/PlayerController.ts:

type HeroTextures = { down: Texture[]; up: Texture[]; side: Texture[] };

const sprite = new Sprite(frames.down[0]);
sprite.anchor.set(0.5, 1);                 // ноги в центре тайла
const anim = new FrameAnimation(sprite, frames.down, 6);

// смена направления:
anim.setFrames(frames.up, true);
// для right — зеркалим:
sprite.scale.x = -1;

Атмосферные частицы (пепел, мотыльки)

const ash = new ParticleEmitter({
    color: 0x666677, rate: 6, lifetime: [3, 7],
    velocity: { x: [-8, 8], y: [-4, 4] },
    size: 1, spawnArea: { width: 480, height: 270 }, seed: 7
});
ash.position.set(240, 135);
engine.renderer.worldRoot.addChild(ash);

update(dt) { ash.update(dt); }
exit() { ash.clear(); }

Мотыльки — теплый цвет (0xf0d878), меньше скорость, acceleration: { y: 1.5 }.

Меню с клавиатурой и мышью

Смотри apps/game/src/scenes/MenuScene.ts: MenuList + advance/up/down действия. Кнопки обрабатывают мышь сами; клавиатуру двигает сцена. Наведение мыши и фокус клавиатуры совмещены: Button.focused подсвечивает как фокус, так и hover.

Fade-переход между сценами

void engine.scenes.replace(new LocationScene(engine), { duration: 0.4 });
// push/pop/replace принимают { duration, color }

Во время перехода ввод в сцену лучше игнорировать:

update(dt: number) {
    if (engine.scenes.transitioning) return;
    // ...
}

Сейв/загрузка (автосейв по Esc)

// выход в меню:
saves.save('autosave', {
    pos: hero.tile,
    state: gameState.serialize(),
    savedAt: Date.now()
} satisfies SaveData);
void engine.scenes.replace(new MenuScene(engine), { duration: 0.3 });

// продолжить:
const data = saves.load<SaveData>('autosave');
if (data) {
    gameState.load(data.state);
    const scene = new LocationScene(engine, data.pos);
    await engine.scenes.replace(scene, { duration: 0.3 });
}

Настройки громкости

Смотри assets-audio-save.md: Settings + onChangeaudio.setBusVolume. Экран настроек — просто settings.update({ music: slider.value }).