diff --git a/docs/plan.md b/docs/plan.md index 560c3e2..577cb00 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -602,3 +602,53 @@ спек-синтез без WAV, позиционный звук и шаги в демке, DEV-шпион). Все 14 треков переноса подсистем v1 на v2 закрыты — дальше: воспроизведение демки v1 («Пепельные луга») на v2 по мере движения. + +#### Воспроизведение демки v1 на v2 — дорожная карта (2026-09-09) + +Цель: на движке v2 воспроизвести демо-срез v1 («Пепельные луга») — 8 +областей (5 уличных + 3 интерьера), 3 NPC, 2 квеста, переходы, пикапы, +накат-зоны, фауна, день/ночь, темы областей. Референс — v1; точность +визуала не критична (воксель ≠ спрайт), но раскладка тайлов v1 +(позиции NPC/переходов/квестов) переносится данными — сходится сюжет. +Состав референса (разведка 2026-09-09): `v1/apps/game/src/data/` — +locations.ts (AREAS: meadows/ponds/zvenets/rust_grove/old_siding/ +machine_hall/house_elder/shop, переходы step/click с requiresFlag/ +requiresItem/lockedText, hazard-зоны наката, свет/атмосфера/fauna), +map.ts (TILES ~35 id, BLOCKED, tall-объекты), карты assets/maps/*.map +(RLE, parseMap), npcs.ts (Ирвин, Мила; Клинт по флагу act2_hook), +dialogues/ (9 графов: elder × 3, trader × 3, clint × 3), ids.ts +(18 флагов + vars), items.ts (7 предметов), interactables.ts, +quests.ts + quest/FlowerQuest.ts, ResonatorQuest.ts, music.ts (THEMES — +спек-музыка, рендер уже портирован в v2), fauna, пепел/моты. + +Треки (нумерация продолжается с 15): + +- **Трек 15 — карты и области** ✅ (2026-09-10, коммит): конвертация карт v1 + (`apps/game/tools/convertMaps.mjs` → `src/data/maps.json`, RLE как есть), + воксельный генератор областей (`src/areas.ts`: 1 тайл = 3×3 вокселя, + земля y0-1, вода слоем y=2 = blocked, штампы — компактные генераторы + в тайл 3×3 mapTree/mapBush/mapBoulder + дома/колодец/башня/машина/ + рельсы/стены/декор, всё детерминировано); новое приложение `apps/game` + (демка не тронута — её 11 гейтов завязаны на сценку); переходы step/click + с фейдом out→swap→in и стеком возврата 'return' (`src/main.ts`); мост + areaInfo/gotoArea/setManual/stepTicks/heroInfo/heroTeleport/setInput/ + setFollow/setSun/solidAt/interact; гейт `tools/checks/areas.mjs` — + 16 проверок (детерминизм пересборки, blocked, зажим у воды, step-переходы + луга⇄Звенец/луга⇄пруды, люк→зал→return по стеку, click «E», интерьер). + Уроки: экранная ось (1,0) — диагональ мира (триггер срезается углом — + чистые оси комбинированным стиком); штампы шире тайла запирают проходы — + контент карты строго в тайл 3×3; solidAt — колонка над землёй (y≥2). +- **Трек 16 — NPC и квестовый каркас**: NPC-дефы (появление по флагам), + перенос 9 диалоговых графов v1, флаги/вары, тосты; гейт. +- **Трек 17 — интерактив, пикапы, квест «Три цветка»**: INTERACTABLES + (сундуки, записка, колодцы, прилавок, bell_rope), сбор цветов/мотов, + FlowerQuest (взять → собрать в прудах → посадить → сдать с кат-сценой), + предметы (cloth от Милы), накат-зоны (без cloth — голос); гейт. +- **Трек 18 — время суток и атмосфера**: GameClock (день/вечер/ночь), + источники света (колокольчики ночью), частицы пепла/тумана, темы + областей (renderMusic) + локальные амбиент-слои (спек-лупы вместо WAV); + гейт. +- **Трек 19 — акт 2**: Клинт, ResonatorQuest, тон-деревья, Машина + (machine_hall), фауна (олени); гейт. +- **Трек 20 — сборка**: сейв полного состояния, меню, полный прогон, + сравнительные скриншоты с v1 как референсом. diff --git a/v2/CLAUDE.md b/v2/CLAUDE.md index 8515f43..d896e53 100644 --- a/v2/CLAUDE.md +++ b/v2/CLAUDE.md @@ -49,7 +49,9 @@ node apps/demo/tools/checks/scenes.mjs # скриншот-гейт сцен/меню (/tmp/v2_scenes_*.png) node apps/demo/tools/checks/combat.mjs # скриншот-гейт боя (/tmp/v2_combat_*.png) node apps/demo/tools/checks/audio.mjs # гейт аудио (DEV-шпион, /tmp/v2_audio_*.png) +node apps/game/tools/checks/areas.mjs # гейт областей игры (/tmp/v2_areas_*.png) npx vite --port 5299 # демка (в apps/demo), http://localhost:5299 +npx vite --port 5315 # игра «Пепельные луга» (в apps/game) ``` ## Структура @@ -302,6 +304,32 @@ резонанс → bell_low, лог ограничен 24 записями. Мост: `audioUnlock()` (разблокировка контекста гейтом без жестов), `audioInfo()` (кольцевой буфер 24 записей {key, volume, pan, rate} фактических запусков звука). +- `apps/game` — игра «Пепельные луга» на v2: воспроизведение демки v1 + (референс — `v1/apps/game`, раскладка карт переносится данными). + `tools/convertMaps.mjs` — конвертация карт v1 (assets/maps/*.map, RLE) + → `src/data/maps.json` (прогон при правке референса: `node tools/convertMaps.mjs`); + `src/areas.ts` — генератор мира области из раскладки: 1 тайл v1 = 3×3 + вокселя (UNIT=3, ноги на y=2), земля y0-1, вода слоем y=2 (непроходима, + как v1 blocked), штампы объектов — компактные генераторы в тайл 3×3 + (mapTree/mapBush/mapBoulder — крона не вылезает на соседей) + дома/колодец/ + башня/машина/рельсы/декор (светлый пиксель на земле, y=1); детерминировано + (пятна — хэш координат, сиды штампов — от тайла). `src/data/areas.ts` — + AREA_DEFS: 8 областей с переходами v1 1:1 (step — вход на тайл-триггер, + click — «E» рядом с дверью/колодцем, 'return' — стек возврата). `src/main.ts`: + мир области пересоздаётся на переходе (меши сбрасываются), герой-манекен + (Character, скорость 4), фейд out→swap→in через DOM `#fade` (полный переход + 2×duration), подсказка «E — …» в `#hint`, камера follow с шагом пиксельной + сетки. Мост: `setManual`/`stepTicks`/`heroInfo` (area/tile/pos/camTarget)/ + `heroTeleport`/`setInput`/`setFollow`/`setSun`/`areaInfo` (id/переходы/fade/ + returns/chunks/meshes)/`gotoArea(id, entry?)` (прямой прыжок, стек чистится)/ + `solidAt(x,z)` (сплошная ли колонка НАД землёй — y≥2)/`interact`/`interactInfo`. + Гейт `tools/checks/areas.mjs` (16 проверок): старт на спавне лугов, + пересбор области — тот же кадр (детерминизм), стик (1,1) = чистый +x мира, + blocked (дерево/вода сплошные — по 3×3 под-колонкам тайла, тропа нет), + тело зажимается гранью воды, скриншоты 7 областей различаются, step-переходы + (луга→Звенец, луга⇄пруды) с фейдом и запоминанием возврата, люк→зал→return + по стеку, click-переход «E» (подсказка у двери, вдали тишина), интерьер + рисуется. ## Известные грабли (уже собранные) @@ -408,6 +436,23 @@ - **`AgentClient.call` вариадический**: `call('combatSpawn', 'crawler', 15, 20)`, а не `call('combatSpawn', ['crawler', 15, 20])` — массив уходит ОДНИМ аргументом (ENEMY_KINDS[Array] → undefined). +- **Экранная ось (1,0) — диагональ мира** (при yaw π/4 это (+x,−z)/√2): + герой, идущий «экраном», срезает угол тайла и может ПРОПУСТИТЬ + тайл-триггер перехода (x и z пересекают границы в один тик). Чистая + мировая ось — комбинированный стик: (1,1) = +x, (−1,−1) = −x, + (1,−1) = −z, (−1,1) = +z (проверено гейтом: дрейфа по z нет). +- **Гейт-подход к триггеру — телепортом на соседний тайл + чистая ось**: + телепорт точно на триггер не срабатывает (prevTile = триггер, «входа» нет); + после телепорта prevTile = тайл прибытия, триггер fires только при входе + СНАРУЖИ. +- **Штампы шире тайла запирают проходы**: модель 7 вокселей, отштампованная + от угла тайла 3×3, вылезает на соседей на 2 вокселя — крона (тело героя + 3×10×3, крона на y≥4) делает непроходимыми коридоры, которые в v1 + свободны (blocked только свой тайл). Контентные объекты карты — + генераторы строго в тайл 3×3 (mapTree/mapBush/mapBoulder), штамп + центрируется на центр тайла. +- **solidAt для blocked-гейта — колонка НАД землёй (y≥2)**: земля y0-1 + сплошная всюду; проверка с y=0 даёт «всё заблокировано». ## Рабочие привычки diff --git a/v2/apps/game/index.html b/v2/apps/game/index.html new file mode 100644 index 0000000..d7a12eb --- /dev/null +++ b/v2/apps/game/index.html @@ -0,0 +1,46 @@ + + + + + Пепельные луга — v2 + + + + +
+ + + + \ No newline at end of file diff --git a/v2/apps/game/package.json b/v2/apps/game/package.json new file mode 100644 index 0000000..59167e8 --- /dev/null +++ b/v2/apps/game/package.json @@ -0,0 +1,18 @@ +{ + "name": "@rpg/game", + "version": "0.0.1", + "private": true, + "type": "module", + "description": "Пепельные луга на v2: воспроизведение демки v1 на воксельном движке.", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "dependencies": { + "@rpg/engine": "*", + "three": "^0.186.0" + }, + "devDependencies": { + "vite": "^6.0.0" + } +} \ No newline at end of file diff --git a/v2/apps/game/src/areas.ts b/v2/apps/game/src/areas.ts new file mode 100644 index 0000000..fba4d94 --- /dev/null +++ b/v2/apps/game/src/areas.ts @@ -0,0 +1,329 @@ +/** + * Области «Пепельных луга» на v2: раскладка тайлов v1 (maps.json, референс) + * → воксельный мир. 1 тайл v1 = 3×3 вокселя (1 юнит = 3 вокселя, как в бою + * демки). Рельеф — плоский (земля y0-1), объекты — штампы генераторов + * движка (деревья/валуны) и локальных генераторов контента (кусты, дома, + * колодец, башня). Позиции v1 (NPC/переходы/квесты, в тайлах) конвертируются + * на границе: центр тайла = (tile*UNIT + 1.5) в воксельных координатах. + */ +import { + VoxelWorld, VoxelGrid, stampModel, + encodeModel, Rng, type Variant, +} from '@rpg/engine'; +import type { VoxelModel } from '@rpg/engine'; +import mapsData from './data/maps.json'; + +/** Раскладка области (конвертация v1): тайлы RLE, blocked/tall/decor/props v1. */ +export interface AreaLayout { + width: number; + height: number; + /** RLE-пары [id, длина] построчно (idx = y*width + x). */ + tiles: [number, number][]; + blocked: number[]; + tall: Record; + decor: { id: number; x: number; y: number }[]; + props: { id: number; x: number; y: number; w?: number; h?: number }[]; +} + +export const AREA_LAYOUTS = mapsData as unknown as Record; + +/** Вокселей на тайл v1 (1 юнит = 3 вокселя — общий масштаб демки/боя). */ +export const UNIT = 3; +/** Точка ног героя в вокселях (верх земли). */ +export const GROUND_Y = 2; + +// Слоты палитры сцены (арт-библия v1; 11+ — тайлы областей) +const ASH = 11; // P2 — пепел (тёмный) +const ASH_LIGHT = 12; // P3 — пепел, пятна +const WATER = 13; // W1 — вода +const WATER_LIGHT = 14; // W2 — вода, блики +const FLOOR = 15; // T0 — пол интерьеров +const PATH = 16; // T1 — тропа +const RAIL = 17; // M2 — рельсы (металл) +const FLOWER = 9; // B1 — колокольчик (жизнь!) + +// Тайлы раскладки v1 (TILES из data/map.ts v1) +const T = { + GRASS: 0, PATH: 1, WATER: 2, TREE: 3, ASH: 4, BELLFLOWER: 5, TOWER: 6, + HOUSE: 7, FLOOR: 8, WALL: 9, WELL: 10, WIRE: 18, RAIL: 19, STAIRS: 20, + MACHINE: 21, TREE_V1: 22, TREE_V2: 23, HOUSE_V1: 24, HOUSE_V2: 25, + HOUSE_V3: 26, HOUSE_V4: 27, BOULDER: 28, BUSH: 29, BOULDER_2: 30, + BOULDER_3: 31, BUSH_2: 32, BUSH_3: 33, +} as const; + +const TREES: Record = { [T.TREE]: 0, [T.TREE_V1]: 1, [T.TREE_V2]: 2 }; +const HOUSE_KINDS: readonly number[] = [T.HOUSE, T.HOUSE_V1, T.HOUSE_V2, T.HOUSE_V3, T.HOUSE_V4]; +const BOULDER_KINDS: readonly number[] = [T.BOULDER, T.BOULDER_2, T.BOULDER_3]; +const BUSH_KINDS: readonly number[] = [T.BUSH, T.BUSH_2, T.BUSH_3]; + +/** Детерминированное пятно по координате тайла (чистый хэш, без ГПСЧ). */ +const mottle = (tx: number, ty: number, salt = 0) => (tx * 7 + ty * 13 + salt * 31) % 11 < 3; + +// ---- Генераторы контента игры (деревья/кусты/валуны в тайл 3×3, дома...) ---- + +/** Дерево карты: компактное, ровно в тайл 3×3 (крона не вылезает на соседей — + * blocked-семантика v1). Варианты: 0 круглая крона, 1 узкая колонна, 2 ель. */ +export function mapTreeModel(variant: Variant, seed: number): VoxelModel { + const rng = new Rng(seed ^ 0x7ee1); + const trunkH = variant === 1 ? rng.range(3, 5) : rng.range(2, 3); + const crownH = variant === 2 ? rng.range(3, 4) : rng.range(2, 3); + const g = new VoxelGrid(3, trunkH + crownH + 1, 3); + for (let y = 0; y < trunkH; y++) g.set(1, y, 1, 4); // ствол + const r = 1 + rng.next() * 0.35; // джиттер радиуса фиксируем заранее + for (let dy = 0; dy < crownH; dy++) { + const y = trunkH - 1 + dy; + for (let dz = -1; dz <= 1; dz++) + for (let dx = -1; dx <= 1; dx++) { + const d2 = dx * dx + dz * dz; + let ok: boolean; + if (variant === 0) ok = d2 + dy * dy * 0.6 <= r * r + 1; + else if (variant === 1) ok = d2 <= 1; + else ok = d2 <= (dy < crownH / 2 ? 2 : 1); // ель: ярусы сужаются + if (!ok) continue; + if (dy > 0 && d2 > 1 && !rng.chance(0.7)) continue; // рваный край + const light = dy === crownH - 1 || rng.chance(0.25); + g.set(1 + dx, y, 1 + dz, light ? 5 : 3); + } + } + return encodeModel(g, { 3: '#626c4c', 5: '#7d8662', 4: '#5c3b2e' }); +} + +/** Куст карты: низкая крона без ствола, ровно в тайл. */ +export function mapBushModel(variant: Variant, seed: number): VoxelModel { + const rng = new Rng(seed ^ 0x5a51); + const H = variant === 1 ? 3 : 2; + const g = new VoxelGrid(3, H, 3); + for (let y = 0; y < H; y++) + for (let dz = -1; dz <= 1; dz++) + for (let dx = -1; dx <= 1; dx++) { + const d2 = dx * dx + dz * dz; + if (y === 0 ? d2 > 2 : d2 > 1) continue; // низ шире, верх уже + if (d2 > 0 && !rng.chance(0.8)) continue; + g.set(1 + dx, y, 1 + dz, y === H - 1 || rng.chance(0.25) ? 5 : 3); + } + return encodeModel(g, { 3: '#626c4c', 5: '#7d8662' }); +} + +/** Валун карты: глыба-купол в тайл (не ниже 2 — не плитка), свет на верхушке. */ +export function mapBoulderModel(variant: Variant, seed: number): VoxelModel { + const rng = new Rng(seed ^ 0x4b0d); + const H = 2 + Math.min(variant, 1) + (rng.next() < 0.4 ? 1 : 0); // 2..4 + const g = new VoxelGrid(3, H, 3); + for (let y = 0; y < H; y++) { + const w = y === 0 ? 3 : y === H - 1 ? 1 : 2; // купол: шире к низу + const o = (3 - w) >> 1; + for (let dz = 0; dz < w; dz++) + for (let dx = 0; dx < w; dx++) { + if (w === 2 && dx === 1 && dz === 1 && rng.chance(0.5)) continue; + g.set(o + dx, y, o + dz, y === H - 1 ? 7 : 6); + } + } + return encodeModel(g, { 6: '#5b5b66', 7: '#94949e' }); +} + +/** Дом: каменные стены с окнами + двускатная ржавая крыша (tall 1.75 юнита). */ +export function houseModel(kind: number, _seed: number): VoxelModel { + const tall = kind === T.HOUSE_V3 || kind === T.HOUSE_V4; + const W = 8, H = tall ? 10 : 7, D = 6; + const g = new VoxelGrid(W, H, D); + for (let y = 0; y < H - 2; y++) + for (let z = 0; z < D; z++) + for (let x = 0; x < W; x++) { + const edge = x === 0 || z === 0 || x === W - 1 || z === D - 1; + if (!edge) continue; + const win = y > 1 && y < H - 3 && (x + z) % 3 === 0; + g.set(x, y, z, win ? 10 : 6); + } + for (let dy = 0; dy < 3; dy++) { + const y = H - 3 + dy; + for (let z = 0; z < D; z++) + for (let x = dy; x < W - dy; x++) g.set(x, y, z, 8); + } + return encodeModel(g, { 6: '#5b5b66', 8: '#82503a', 10: '#44454e' }); +} + +/** Колодец: каменное кольцо + ворот с брусом (tall 1 юнит). */ +export function wellModel(_seed: number): VoxelModel { + const g = new VoxelGrid(3, 4, 3); + for (let z = 0; z < 3; z++) + for (let x = 0; x < 3; x++) { + const edge = x === 0 || z === 0 || x === 2 || z === 2; + if (edge) g.set(x, 0, z, 6); + } + g.set(0, 1, 0, 4); + g.set(2, 1, 0, 4); + g.set(1, 2, 0, 10); // ворот (металл) + g.set(1, 3, 0, 4); + return encodeModel(g, { 6: '#5b5b66', 4: '#5c3b2e', 10: '#44454e' }); +} + +/** Башня звона: колонна с площадкой и колоколом (v1 tall 2.5 юнита). */ +export function towerModel(_seed: number): VoxelModel { + const g = new VoxelGrid(3, 9, 3); + for (let y = 0; y < 6; y++) + for (let z = 0; z < 3; z++) + for (let x = 0; x < 3; x++) g.set(x, y, z, 6); + for (let x = 0; x < 3; x++) + for (let z = 0; z < 3; z++) g.set(x, 6, z, 7); // площадка, свет камня + g.set(1, 7, 1, 10); // колокол (металл) + g.set(1, 8, 1, 4); + return encodeModel(g, { 6: '#5b5b66', 7: '#94949e', 10: '#44454e', 4: '#5c3b2e' }); +} + +/** Старая Машина: тяжёлый блок механизма (prop 2×2 тайла, tall 2.5 юнита). */ +export function machineModel(_seed: number): VoxelModel { + const g = new VoxelGrid(8, 9, 7); + for (let y = 0; y < 5; y++) + for (let z = 0; z < 7; z++) + for (let x = 0; x < 8; x++) + g.set(x, y, z, (x + z + y) % 4 === 0 ? 10 : 6); + for (let z = 2; z < 5; z++) + for (let x = 1; x < 7; x++) g.set(x, 5, z, 8); + for (let z = 2; z < 5; z++) g.set(4, 6, z, 10); + g.set(4, 7, 3, 9); // тёплая искра в жерле (жизнь!) + return encodeModel(g, { 6: '#5b5b66', 8: '#82503a', 10: '#44454e', 9: '#d99a32' }); +} + +/** Бухта кабеля: низкая ржавая спираль (v1 tall 0.6 юнита). */ +export function wireModel(_seed: number): VoxelModel { + const g = new VoxelGrid(4, 3, 3); + for (let z = 0; z < 3; z++) + for (let x = 0; x < 4; x++) { + const rim = x === 0 || x === 3 || z === 1; + if (rim) g.set(x, 0, z, 8); + if (rim && (x + z) % 2 === 0) g.set(x, 1, z, 10); + } + return encodeModel(g, { 8: '#82503a', 10: '#44454e' }); +} + +/** Кэш моделей: генерация 1 раз на (тип, тайл) — роща из 1005 деревьев дёшева. */ +const cache = new Map(); +function cached(key: string, make: () => VoxelModel): VoxelModel { + const hit = cache.get(key); + if (hit) return hit; + const m = make(); + cache.set(key, m); + return m; +} + +/** Поставить штамп на тайл: модель центрируется на центре тайла + * (ствол/фундамент по центру, крона/крыша свисают за край — как в v1). */ +function stampAt(world: VoxelWorld, m: VoxelModel, tx: number, ty: number): void { + const [sx, , sz] = m.size; + stampModel( + world, m, + tx * UNIT + 1 - ((sx - 1) >> 1), GROUND_Y, ty * UNIT + 1 - ((sz - 1) >> 1), + ); +} + +/** + * Сборка воксельного мира области из раскладки v1. Детерминировано: + * пятна — хэш координат, штампы — генераторы с сидом от координат. + */ +export function buildAreaWorld(l: AreaLayout): VoxelWorld { + const world = new VoxelWorld(16); // высота чанка 16 (башня 9 + запас) + const tiles = rleDecode(l.tiles, l.width * l.height); + // 1) земля (все тайлы) — потом декор, потом штампы (объекты поверх) + for (let ty = 0; ty < l.height; ty++) + for (let tx = 0; tx < l.width; tx++) { + const t = tiles[ty * l.width + tx] ?? T.ASH; + for (let dz = 0; dz < UNIT; dz++) + for (let dx = 0; dx < UNIT; dx++) { + const x = tx * UNIT + dx, z = ty * UNIT + dz; + world.set(x, 0, z, 2); // низ земли — тень травы + world.set(x, 1, z, groundFor(t, tx, ty, dx, dz)); + if (t === T.WATER) { + // вода — на воксель выше земли (v1: непроходима); + // тело героя стоит на y=2 и упирается в этот слой + world.set(x, 1, z, WATER); + world.set(x, 2, z, groundFor(t, tx, ty, dx, dz)); + } + } + } + // 1б) декор v1 (мелкие объекты на земле, уровень y=1 — ходьбе не мешает) + for (const d of l.decor) stampDecor(world, d); + // 2) штампы объектов + for (let ty = 0; ty < l.height; ty++) + for (let tx = 0; tx < l.width; tx++) { + stampObject(world, tiles[ty * l.width + tx] ?? T.ASH, tx, ty); + } + return world; +} + +// Декор v1: микро-объекты на земле (не тайлы — в раскладке отдельно) +const DECOR = { TUFT: 15, STONE: 16, BALD: 17 } as const; + +/** Декор: светлый пиксель на земле (кустик травы / камешек / проплешина). */ +function stampDecor( + world: VoxelWorld, + d: { id: number; x: number; y: number; dx?: number; dy?: number }, +): void { + const ox = Math.max(-1, Math.min(1, Math.round((d.dx ?? 0) / 3))); + const oz = Math.max(-1, Math.min(1, Math.round((d.dy ?? 0) / 3))); + const slot = d.id === DECOR.TUFT ? 5 : d.id === DECOR.STONE ? 7 : 2; + world.set(d.x * UNIT + 1 + ox, 1, d.y * UNIT + 1 + oz, slot); +} + +/** RLE-пары → плоский массив (недостающие — пепел). */ +function rleDecode(pairs: [number, number][], total: number): number[] { + const out: number[] = []; + for (const [id, run] of pairs) for (let k = 0; k < run; k++) out.push(id); + while (out.length < total) out.push(T.ASH); + return out; +} + +/** Слот поверхности тайла (день: трава пятнистая, пепел, тропа, вода, пол). */ +function groundFor(t: number, tx: number, ty: number, dx: number, dz: number): number { + switch (t) { + case T.GRASS: return mottle(tx, ty, dx + dz) ? 3 : 1; // G1 + пятна G2 + case T.ASH: return mottle(tx, ty, dx + dz) ? ASH_LIGHT : ASH; + case T.PATH: return PATH; + case T.WATER: return mottle(tx * 2 + dx, ty * 2 + dz, dz) ? WATER_LIGHT : WATER; + case T.FLOOR: return FLOOR; + case T.RAIL: return mottle(tx, ty, dx) ? PATH : FLOOR; // шпалы + case T.STAIRS: return ASH; // люк: тёмный провал + case T.WALL: return FLOOR; // стены интерьеров стоят на полу + default: return mottle(tx, ty, dx + dz) ? ASH_LIGHT : ASH; // под объектами + } +} + +/** Штамп объекта тайла (дерево/дом/валун/куст/башня/колодец/рельсы...). */ +function stampObject(world: VoxelWorld, t: number, tx: number, ty: number): void { + const seed = tx * 131 + ty * 17; + if (t in TREES) stampAt(world, cached(`tree${t}`, () => mapTreeModel(TREES[t]!, seed)), tx, ty); + else if (HOUSE_KINDS.includes(t)) stampAt(world, cached(`house${t}`, () => houseModel(t, seed)), tx, ty); + else if (BOULDER_KINDS.includes(t)) + stampAt(world, cached(`boul${t}`, () => mapBoulderModel((t - T.BOULDER) as Variant, seed)), tx, ty); + else if (BUSH_KINDS.includes(t)) + stampAt(world, cached(`bush${t}`, () => mapBushModel((t - T.BUSH) as Variant, seed)), tx, ty); + else if (t === T.TOWER) stampAt(world, cached('tower', () => towerModel(seed)), tx, ty); + else if (t === T.WELL) stampAt(world, cached('well', () => wellModel(seed)), tx, ty); + else if (t === T.WIRE) stampAt(world, cached('wire', () => wireModel(seed)), tx, ty); + else if (t === T.MACHINE) stampAt(world, cached('machine', () => machineModel(seed)), tx, ty); + else if (t === T.RAIL) stampRail(world, tx, ty); + else if (t === T.BELLFLOWER) stampBellflower(world, tx, ty); + else if (t === T.WALL) stampWall(world, tx, ty); +} + +/** Рельсы: металлическая нить по земле (полоса тайлов даёт линию). */ +function stampRail(world: VoxelWorld, tx: number, ty: number): void { + for (let d = 1; d < UNIT; d++) world.set(tx * UNIT + d, GROUND_Y, ty * UNIT + 1, RAIL); +} + +/** Колокольчик: цветок на пепле (v1 harvest-тайл). */ +function stampBellflower(world: VoxelWorld, tx: number, ty: number): void { + world.set(tx * UNIT + 1, GROUND_Y, ty * UNIT + 1, FLOWER); +} + +/** Стена интерьера: колонка камня высотой 4 над полом (v1 tall 1 юнит). */ +function stampWall(world: VoxelWorld, tx: number, ty: number): void { + for (let dz = 0; dz < UNIT; dz++) + for (let dx = 0; dx < UNIT; dx++) + for (let y = GROUND_Y; y < GROUND_Y + 4; y++) + world.set(tx * UNIT + dx, y, ty * UNIT + dz, 6); +} + +/** Центр тайла в вокселях (позиции v1 → мир v2, на границе конвертации). */ +export function tileCenter(tx: number, ty: number): { x: number; z: number } { + return { x: tx * UNIT + UNIT / 2, z: ty * UNIT + UNIT / 2 }; +} \ No newline at end of file diff --git a/v2/apps/game/src/data/areas.ts b/v2/apps/game/src/data/areas.ts new file mode 100644 index 0000000..fc630d6 --- /dev/null +++ b/v2/apps/game/src/data/areas.ts @@ -0,0 +1,102 @@ +/** + * Области демки (референс v1 locations.ts → данные v2): спавны и переходы + * в тайлах v1 (конвертация в воксели — на границе, tileCenter/UNIT). + * Переходы: step — наступил на тайл, click — «E» рядом (двери, колодцы); + * 'return' — назад, откуда вошли (стек переходов). + */ + +/** Тайл v1 (юниты v1). */ +export interface Tile { x: number; y: number } + +export interface TransitionDef { + /** Тайл-триггер. */ + tile: Tile; + /** Подпись подсказки (для click-переходов). */ + label?: string; + /** Куда: [область, тайл входа] или 'return' — назад, откуда вошли. */ + target: { area: string; entry: Tile } | 'return'; + /** step (по умолчанию) или click (двери/колодцы/порталы). */ + trigger?: 'step' | 'click'; + /** Длительность фейда (сек), по умолчанию 0.4. */ + duration?: number; +} + +export interface AreaDef { + id: string; + name: string; + /** Стартовый тайл (вход в область по умолчанию). */ + spawn: Tile; + transitions: TransitionDef[]; +} + +export const AREA_DEFS: AreaDef[] = [ + { + id: 'meadows', + name: 'Выжженные луга', + spawn: { x: 16, y: 28 }, + transitions: [ + { tile: { x: 2, y: 2 }, target: { area: 'ponds', entry: { x: 4, y: 4 } } }, + { tile: { x: 54, y: 28 }, target: { area: 'zvenets', entry: { x: 3, y: 20 } } }, + { tile: { x: 44, y: 40 }, label: 'Старый колодец', target: { area: 'zvenets', entry: { x: 16, y: 14 } }, trigger: 'click', duration: 0.2 }, + ], + }, + { + id: 'ponds', + name: 'Серые пруды', + spawn: { x: 4, y: 4 }, + transitions: [{ tile: { x: 2, y: 2 }, target: { area: 'meadows', entry: { x: 3, y: 3 } } }], + }, + { + id: 'zvenets', + name: 'Звенец', + spawn: { x: 28, y: 20 }, + transitions: [ + { tile: { x: 2, y: 20 }, target: { area: 'meadows', entry: { x: 53, y: 28 } } }, + { tile: { x: 16, y: 13 }, label: 'Старый колодец', target: { area: 'meadows', entry: { x: 44, y: 41 } }, trigger: 'click', duration: 0.2 }, + { tile: { x: 13, y: 12 }, label: 'Дом Ирвина', target: { area: 'house_elder', entry: { x: 5, y: 7 } }, trigger: 'click', duration: 0.2 }, + { tile: { x: 43, y: 19 }, label: 'Лавка Милы', target: { area: 'shop', entry: { x: 5, y: 7 } }, trigger: 'click', duration: 0.2 }, + { tile: { x: 54, y: 20 }, target: { area: 'rust_grove', entry: { x: 2, y: 20 } } }, + ], + }, + { + id: 'rust_grove', + name: 'Ржавая роща', + spawn: { x: 2, y: 20 }, + transitions: [ + { tile: { x: 1, y: 20 }, target: { area: 'zvenets', entry: { x: 53, y: 20 } } }, + { tile: { x: 46, y: 20 }, target: { area: 'old_siding', entry: { x: 2, y: 20 } } }, + ], + }, + { + id: 'old_siding', + name: 'Старый разъезд', + spawn: { x: 2, y: 20 }, + transitions: [ + { tile: { x: 1, y: 20 }, target: { area: 'rust_grove', entry: { x: 45, y: 20 } } }, + { tile: { x: 28, y: 31 }, label: 'Люк в подземелье', target: { area: 'machine_hall', entry: { x: 13, y: 19 } }, duration: 0.6 }, + ], + }, + { + id: 'machine_hall', + name: 'Зал Старой Машины', + spawn: { x: 13, y: 19 }, + transitions: [{ tile: { x: 13, y: 21 }, target: 'return', duration: 0.2 }], + }, + { + id: 'house_elder', + name: 'Дом Ирвина', + spawn: { x: 5, y: 7 }, + transitions: [{ tile: { x: 5, y: 8 }, target: 'return', duration: 0.2 }], + }, + { + id: 'shop', + name: 'Лавка Милы', + spawn: { x: 5, y: 7 }, + transitions: [{ tile: { x: 5, y: 8 }, target: 'return', duration: 0.2 }], + }, +]; + +/** Область по id (фолбэк — луга, как в v1 areaOf). */ +export function areaOf(id: string): AreaDef { + return AREA_DEFS.find((a) => a.id === id) ?? AREA_DEFS[0]!; +} \ No newline at end of file diff --git a/v2/apps/game/src/data/maps.json b/v2/apps/game/src/data/maps.json new file mode 100644 index 0000000..aac8fd9 --- /dev/null +++ b/v2/apps/game/src/data/maps.json @@ -0,0 +1 @@ +{"meadows":{"width":56,"height":56,"tiles":[[0,3],[3,1],[0,1],[4,1],[3,1],[4,1],[0,4],[4,8],[0,18],[4,4],[0,7],[3,2],[0,2],[3,1],[0,3],[1,1],[0,3],[4,2],[3,1],[4,3],[0,1],[4,1],[0,1],[4,7],[0,4],[4,1],[0,1],[4,1],[0,9],[4,5],[0,7],[4,2],[3,1],[0,3],[3,2],[1,2],[0,2],[4,1],[3,1],[4,1],[0,1],[4,1],[0,2],[4,10],[0,3],[4,1],[0,1],[4,5],[0,5],[4,4],[0,3],[4,8],[0,3],[3,1],[0,2],[1,2],[0,1],[1,1],[0,2],[4,3],[0,1],[4,12],[0,1],[4,4],[0,1],[4,2],[0,5],[4,3],[0,4],[4,2],[0,1],[4,1],[0,2],[3,1],[0,1],[4,1],[0,6],[1,5],[0,4],[4,1],[0,1],[4,8],[0,1],[4,1],[0,3],[4,4],[0,6],[4,3],[0,3],[4,10],[0,3],[3,1],[0,3],[3,1],[4,1],[0,1],[1,1],[0,4],[4,5],[0,1],[4,11],[0,2],[4,4],[0,2],[4,3],[0,4],[4,5],[0,1],[4,1],[0,1],[4,1],[0,2],[3,1],[0,2],[3,2],[4,1],[3,1],[4,1],[1,1],[0,4],[4,5],[0,1],[4,17],[0,9],[4,1],[0,1],[4,3],[0,9],[3,1],[4,1],[0,1],[4,1],[3,1],[1,1],[0,5],[4,1],[0,1],[4,1],[0,2],[4,6],[0,1],[4,6],[0,1],[4,3],[0,2],[4,1],[0,14],[3,1],[0,5],[3,3],[4,2],[1,1],[0,5],[4,2],[0,1],[4,1],[0,3],[4,1],[0,2],[4,1],[0,1],[4,1],[0,1],[4,6],[0,1],[28,1],[0,2],[4,2],[0,9],[3,1],[0,12],[3,1],[0,1],[1,1],[0,4],[4,5],[0,3],[4,3],[0,1],[4,9],[0,5],[4,2],[0,10],[4,5],[0,4],[4,1],[3,3],[1,1],[0,13],[4,2],[0,1],[29,1],[0,1],[4,7],[0,15],[3,1],[0,2],[4,3],[0,5],[3,1],[0,3],[1,1],[0,18],[4,1],[29,1],[4,4],[0,3],[5,1],[0,14],[4,3],[0,1],[4,1],[0,1],[3,1],[0,1],[4,3],[3,1],[0,1],[1,2],[0,14],[4,5],[0,2],[4,1],[0,2],[4,4],[0,1],[4,1],[0,10],[3,1],[0,1],[4,1],[0,2],[4,1],[0,1],[3,1],[0,2],[3,1],[0,3],[3,1],[1,1],[0,9],[28,1],[0,4],[4,2],[0,4],[4,2],[0,2],[4,2],[0,1],[4,1],[29,1],[4,1],[0,2],[4,3],[0,2],[4,1],[0,3],[4,1],[0,1],[4,3],[0,6],[3,1],[0,2],[1,1],[0,14],[4,1],[0,1],[4,1],[0,5],[4,2],[0,1],[4,4],[2,2],[0,1],[2,4],[0,2],[4,7],[0,2],[4,2],[3,2],[0,2],[3,1],[0,1],[1,1],[0,22],[4,6],[0,2],[2,6],[0,1],[4,8],[0,10],[1,2],[0,19],[4,8],[0,1],[2,4],[0,1],[2,2],[0,1],[5,1],[4,7],[0,2],[3,1],[4,2],[3,1],[4,1],[3,3],[0,1],[1,4],[0,16],[4,1],[0,1],[4,5],[0,2],[2,1],[0,1],[2,5],[0,1],[4,1],[0,1],[4,3],[0,2],[4,1],[0,3],[3,4],[0,1],[3,1],[4,3],[0,1],[4,1],[1,4],[0,15],[4,3],[0,1],[4,1],[0,2],[2,4],[0,1],[2,1],[0,5],[3,1],[28,1],[0,5],[3,1],[0,1],[3,6],[0,1],[4,1],[0,2],[4,2],[0,1],[1,1],[0,10],[4,4],[0,1],[4,2],[0,27],[3,1],[0,3],[4,3],[0,4],[1,1],[0,5],[4,1],[0,1],[4,1],[0,3],[4,2],[0,1],[4,1],[0,17],[3,1],[0,9],[3,1],[0,1],[3,1],[0,1],[3,2],[28,1],[0,1],[4,1],[0,1],[3,1],[4,2],[1,1],[0,5],[4,4],[0,2],[29,1],[4,3],[0,8],[4,3],[0,1],[4,1],[0,3],[4,4],[0,9],[3,1],[0,1],[3,1],[4,4],[0,1],[4,1],[3,1],[4,1],[1,1],[0,5],[4,3],[0,3],[4,1],[0,1],[4,2],[0,3],[4,2],[0,1],[4,2],[0,1],[4,4],[0,3],[4,4],[3,2],[0,3],[3,1],[0,3],[3,1],[0,1],[3,1],[0,6],[4,2],[1,2],[4,2],[0,1],[4,2],[0,6],[4,1],[0,5],[4,1],[0,1],[4,3],[28,1],[4,1],[0,2],[4,1],[0,3],[4,1],[3,1],[0,1],[4,2],[0,8],[3,1],[0,1],[3,1],[0,9],[1,1],[0,1],[4,1],[29,1],[4,4],[0,10],[4,8],[0,5],[4,1],[0,1],[4,3],[0,7],[3,1],[0,9],[28,1],[0,2],[1,1],[0,1],[4,1],[0,1],[4,2],[0,11],[4,5],[29,1],[4,1],[0,1],[4,3],[0,3],[4,5],[0,1],[3,1],[0,8],[3,1],[0,12],[4,2],[3,1],[0,2],[28,1],[0,9],[4,5],[0,2],[4,1],[0,1],[4,5],[0,5],[4,3],[0,4],[3,1],[0,2],[3,1],[0,3],[4,1],[0,1],[4,1],[0,5],[29,1],[0,1],[4,1],[0,2],[4,3],[0,1],[4,1],[0,4],[4,1],[0,2],[4,5],[0,2],[4,6],[0,6],[4,1],[0,1],[3,1],[0,2],[3,3],[0,1],[29,1],[0,2],[4,3],[0,5],[1,33],[0,1],[1,2],[0,1],[3,1],[4,4],[0,1],[4,1],[0,4],[4,1],[0,1],[4,1],[0,8],[4,7],[0,3],[29,1],[4,3],[0,1],[4,4],[0,2],[4,5],[0,3],[4,1],[0,3],[3,2],[4,4],[3,1],[4,1],[0,4],[29,1],[0,7],[4,5],[0,1],[4,1],[0,2],[4,1],[0,4],[4,3],[0,4],[29,1],[0,2],[4,1],[0,1],[4,3],[0,2],[4,1],[0,1],[4,3],[0,1],[4,3],[0,1],[3,1],[4,1],[3,1],[0,8],[4,4],[0,1],[4,7],[0,6],[4,3],[0,1],[4,2],[0,5],[4,3],[0,1],[4,1],[0,2],[3,1],[4,3],[0,1],[3,2],[4,2],[3,1],[4,1],[3,2],[0,4],[2,4],[4,1],[2,1],[0,1],[4,1],[28,1],[4,4],[0,9],[4,2],[0,1],[4,3],[0,7],[4,1],[0,1],[4,4],[0,1],[4,1],[0,1],[3,1],[4,4],[0,1],[4,1],[0,5],[2,3],[0,1],[2,3],[4,1],[0,1],[4,4],[0,5],[29,1],[0,6],[4,1],[0,1],[4,1],[0,7],[4,2],[0,6],[3,2],[0,3],[3,1],[0,1],[3,2],[29,1],[0,3],[2,4],[0,1],[2,2],[4,1],[5,1],[4,1],[0,1],[4,2],[0,19],[4,1],[0,2],[4,3],[0,1],[3,1],[0,1],[4,1],[3,1],[0,2],[3,1],[0,2],[3,4],[0,1],[29,1],[4,2],[0,1],[2,1],[0,1],[2,4],[0,2],[4,2],[0,1],[4,5],[0,13],[4,1],[0,1],[4,1],[0,5],[3,1],[4,1],[0,1],[4,1],[0,2],[3,2],[0,5],[3,1],[0,1],[4,2],[5,1],[2,6],[0,6],[4,5],[0,15],[4,1],[0,6],[4,1],[0,1],[4,1],[3,1],[0,2],[4,1],[3,2],[4,1],[3,1],[0,4],[4,2],[0,4],[29,1],[0,9],[4,3],[0,28],[4,1],[3,1],[4,1],[3,1],[0,20],[4,4],[0,9],[29,1],[0,1],[4,1],[0,9],[4,4],[0,2],[3,1],[4,1],[3,1],[4,1],[0,1],[4,1],[0,8],[4,5],[0,5],[4,1],[0,1],[4,3],[0,10],[4,3],[0,6],[3,1],[0,1],[4,4],[0,3],[3,1],[4,4],[3,2],[0,7],[4,2],[0,1],[4,1],[0,20],[4,2],[0,3],[10,1],[0,6],[4,3],[0,1],[3,2],[4,3],[0,1],[4,1],[3,1],[0,7],[4,3],[0,1],[4,1],[0,3],[4,3],[0,1],[4,1],[0,22],[4,2],[0,1],[4,4],[0,1],[3,1],[0,3],[3,1],[0,8],[4,2],[0,1],[4,2],[0,2],[4,6],[0,8],[4,2],[0,12],[3,1],[4,5],[3,5],[0,1],[3,1],[0,6],[28,1],[0,1],[4,2],[0,1],[4,2],[0,2],[4,6],[0,7],[4,3],[0,12],[4,5],[0,1],[4,1],[0,1],[3,1],[0,1],[3,2],[0,1],[3,2],[0,3],[4,1],[0,1],[4,1],[0,7],[4,6],[0,7],[4,2],[0,13],[4,1],[0,1],[4,1],[0,1],[4,2],[0,2],[3,1],[0,2],[3,3],[0,3],[4,5],[0,5],[4,6],[0,23],[4,1],[0,1],[4,1],[3,1],[4,1],[3,1],[4,1],[0,3],[3,1],[0,6],[4,2],[0,1],[4,1],[0,1],[4,1],[0,4],[4,6],[0,24],[3,1],[28,1],[0,2],[4,1],[0,1],[3,1],[0,4],[3,1],[0,4],[4,6],[0,4],[4,8],[0,5],[4,3],[0,15],[3,1],[0,1],[4,2],[0,3],[3,1],[0,2],[3,3],[4,2],[0,1],[4,1],[0,1],[4,1],[0,1],[4,1],[0,4],[4,6],[0,1],[4,1],[0,3],[4,2],[0,2],[4,1],[0,17],[4,2],[0,1],[3,1],[0,1],[3,1],[0,2],[3,1],[0,1],[4,7],[0,1],[4,1],[0,6],[4,2],[0,2],[4,2],[0,3],[4,6],[0,17],[3,1],[4,1],[0,3],[3,1],[0,1],[3,2],[4,6],[0,2],[4,1],[0,1],[4,1],[0,4],[4,2],[0,7],[4,5],[0,14],[3,1],[4,4],[0,2],[3,4],[4,1],[0,1],[4,3],[0,1],[4,2],[0,3],[4,2],[0,13],[4,5],[0,13],[4,3],[3,1],[4,2],[0,3],[3,4],[4,5],[0,1],[4,2],[0,1],[4,3],[0,14],[4,1],[0,2],[4,2],[0,11],[3,1],[4,6],[0,1],[3,2],[0,3],[3,3],[4,10],[0,15],[4,3],[0,6],[4,1],[0,1],[4,3],[0,1],[3,1],[0,3],[4,1],[0,1],[4,1],[0,3],[3,2],[4,2],[0,1],[3,1],[0,1],[4,1],[0,1],[4,2],[0,1],[4,4],[0,6],[28,1],[0,7],[28,1],[0,9],[4,5],[0,5],[4,1],[3,1],[4,1],[0,2],[3,1],[0,1],[4,1],[0,3],[3,1],[0,1],[4,2],[0,1],[4,5],[0,25],[4,4],[0,10]],"blocked":[2,3,6,7,24,25,26,27,9,10,18,21,28,29,30,31,32,33],"tall":{"3":{"height":1.5,"ground":0},"6":{"height":2.5,"ground":1},"7":{"height":1.75,"ground":4,"shadow":true},"9":{"height":1,"ground":8},"18":{"height":0.6,"ground":4},"21":{"height":2.5,"ground":8},"22":{"height":1.5,"ground":0},"23":{"height":1.5,"ground":0},"24":{"height":1.75,"ground":4,"shadow":true},"25":{"height":1.75,"ground":4,"shadow":true},"26":{"height":2.5,"ground":4,"shadow":true},"27":{"height":2.5,"ground":4,"shadow":true},"28":{"height":1.05,"ground":4},"29":{"height":1.25,"ground":4},"30":{"height":1.25,"ground":4},"31":{"height":0.75,"ground":4},"32":{"height":1,"ground":4},"33":{"height":1.45,"ground":4}},"decor":[{"id":16,"x":7,"y":0,"dx":-3,"dy":-2},{"id":16,"x":17,"y":0,"dx":-4,"dy":2},{"id":15,"x":30,"y":0,"dx":1,"dy":1},{"id":16,"x":38,"y":0,"dx":-3,"dy":0},{"id":16,"x":39,"y":0,"dx":-3,"dy":1},{"id":15,"x":2,"y":1,"dx":-2,"dy":-2},{"id":16,"x":8,"y":1,"dx":-2,"dy":1},{"id":15,"x":22,"y":1,"dx":3,"dy":1},{"id":15,"x":24,"y":1,"dx":-3,"dy":-1},{"id":17,"x":43,"y":1,"dx":1,"dy":1},{"id":17,"x":44,"y":1,"dx":1,"dy":1},{"id":17,"x":3,"y":2,"dx":-3,"dy":2},{"id":17,"x":8,"y":2,"dx":-2,"dy":0},{"id":15,"x":10,"y":2,"dx":-3,"dy":1},{"id":16,"x":20,"y":2,"dx":-2,"dy":2},{"id":16,"x":28,"y":2,"dx":-4,"dy":0},{"id":16,"x":31,"y":2,"dx":3,"dy":1},{"id":15,"x":42,"y":2,"dx":4,"dy":0},{"id":17,"x":7,"y":3,"dx":1,"dy":0},{"id":16,"x":52,"y":3,"dx":-1,"dy":-2},{"id":16,"x":15,"y":4,"dx":-2,"dy":1},{"id":16,"x":17,"y":4,"dx":3,"dy":1},{"id":17,"x":26,"y":4,"dx":-1,"dy":0},{"id":17,"x":33,"y":4,"dx":2,"dy":-1},{"id":17,"x":35,"y":4,"dx":-1,"dy":-2},{"id":15,"x":42,"y":4,"dx":-3,"dy":0},{"id":16,"x":51,"y":4,"dx":-3,"dy":1},{"id":16,"x":14,"y":5,"dx":0,"dy":0},{"id":16,"x":28,"y":5,"dx":-4,"dy":0},{"id":16,"x":34,"y":5,"dx":-3,"dy":0},{"id":15,"x":35,"y":5,"dx":2,"dy":1},{"id":16,"x":38,"y":5,"dx":-2,"dy":2},{"id":15,"x":43,"y":5,"dx":3,"dy":-2},{"id":15,"x":51,"y":5,"dx":-2,"dy":1},{"id":16,"x":52,"y":5,"dx":-1,"dy":-1},{"id":15,"x":0,"y":6,"dx":1,"dy":1},{"id":17,"x":1,"y":6,"dx":1,"dy":1},{"id":16,"x":29,"y":6,"dx":4,"dy":0},{"id":17,"x":39,"y":6,"dx":4,"dy":0},{"id":16,"x":46,"y":6,"dx":-2,"dy":-2},{"id":17,"x":51,"y":6,"dx":2,"dy":-1},{"id":15,"x":1,"y":7,"dx":0,"dy":1},{"id":16,"x":3,"y":7,"dx":-1,"dy":0},{"id":16,"x":15,"y":7,"dx":-1,"dy":0},{"id":15,"x":17,"y":7,"dx":3,"dy":-1},{"id":16,"x":18,"y":7,"dx":0,"dy":1},{"id":15,"x":24,"y":7,"dx":4,"dy":-1},{"id":16,"x":37,"y":7,"dx":1,"dy":0},{"id":15,"x":38,"y":7,"dx":-3,"dy":-1},{"id":15,"x":39,"y":7,"dx":-3,"dy":-2},{"id":17,"x":47,"y":7,"dx":0,"dy":-1},{"id":15,"x":51,"y":7,"dx":3,"dy":-1},{"id":17,"x":26,"y":8,"dx":4,"dy":2},{"id":16,"x":30,"y":8,"dx":1,"dy":-2},{"id":16,"x":32,"y":8,"dx":-4,"dy":-2},{"id":15,"x":35,"y":8,"dx":-1,"dy":0},{"id":16,"x":38,"y":8,"dx":-2,"dy":-2},{"id":15,"x":45,"y":8,"dx":-2,"dy":0},{"id":15,"x":47,"y":8,"dx":4,"dy":0},{"id":17,"x":2,"y":9,"dx":3,"dy":0},{"id":16,"x":20,"y":9,"dx":-4,"dy":-2},{"id":16,"x":24,"y":9,"dx":-2,"dy":1},{"id":17,"x":33,"y":9,"dx":-3,"dy":0},{"id":16,"x":50,"y":9,"dx":2,"dy":1},{"id":15,"x":15,"y":10,"dx":-3,"dy":0},{"id":15,"x":18,"y":10,"dx":0,"dy":-1},{"id":15,"x":20,"y":10,"dx":0,"dy":2},{"id":16,"x":22,"y":10,"dx":-3,"dy":1},{"id":15,"x":25,"y":10,"dx":2,"dy":-1},{"id":15,"x":49,"y":10,"dx":-3,"dy":2},{"id":16,"x":51,"y":10,"dx":1,"dy":2},{"id":17,"x":17,"y":11,"dx":3,"dy":2},{"id":15,"x":18,"y":11,"dx":3,"dy":-2},{"id":17,"x":21,"y":11,"dx":1,"dy":1},{"id":16,"x":29,"y":11,"dx":-1,"dy":0},{"id":16,"x":31,"y":11,"dx":-3,"dy":0},{"id":15,"x":37,"y":11,"dx":1,"dy":0},{"id":17,"x":1,"y":12,"dx":2,"dy":-1},{"id":15,"x":9,"y":12,"dx":-3,"dy":1},{"id":17,"x":10,"y":12,"dx":0,"dy":0},{"id":17,"x":22,"y":12,"dx":4,"dy":-2},{"id":16,"x":26,"y":12,"dx":-4,"dy":2},{"id":16,"x":36,"y":12,"dx":0,"dy":1},{"id":15,"x":40,"y":12,"dx":-2,"dy":-1},{"id":17,"x":46,"y":12,"dx":-3,"dy":0},{"id":15,"x":16,"y":13,"dx":1,"dy":1},{"id":17,"x":28,"y":13,"dx":2,"dy":-1},{"id":16,"x":46,"y":13,"dx":-1,"dy":2},{"id":17,"x":1,"y":14,"dx":0,"dy":-1},{"id":16,"x":31,"y":14,"dx":1,"dy":-1},{"id":16,"x":36,"y":14,"dx":-2,"dy":-2},{"id":16,"x":0,"y":15,"dx":-3,"dy":1},{"id":15,"x":13,"y":15,"dx":1,"dy":2},{"id":17,"x":21,"y":15,"dx":-3,"dy":-2},{"id":16,"x":47,"y":15,"dx":0,"dy":-1},{"id":16,"x":35,"y":16,"dx":1,"dy":-1},{"id":16,"x":1,"y":17,"dx":-3,"dy":-1},{"id":17,"x":27,"y":17,"dx":2,"dy":1},{"id":16,"x":31,"y":17,"dx":4,"dy":1},{"id":16,"x":34,"y":17,"dx":2,"dy":2},{"id":16,"x":46,"y":17,"dx":2,"dy":1},{"id":15,"x":16,"y":18,"dx":4,"dy":2},{"id":15,"x":34,"y":18,"dx":-3,"dy":0},{"id":17,"x":48,"y":18,"dx":2,"dy":1},{"id":17,"x":54,"y":18,"dx":2,"dy":2},{"id":17,"x":1,"y":19,"dx":-3,"dy":0},{"id":16,"x":12,"y":19,"dx":1,"dy":0},{"id":16,"x":13,"y":19,"dx":-1,"dy":0},{"id":15,"x":21,"y":19,"dx":0,"dy":0},{"id":16,"x":29,"y":19,"dx":2,"dy":0},{"id":15,"x":44,"y":19,"dx":4,"dy":1},{"id":17,"x":55,"y":19,"dx":0,"dy":-1},{"id":16,"x":10,"y":20,"dx":-3,"dy":1},{"id":15,"x":24,"y":20,"dx":-2,"dy":1},{"id":17,"x":36,"y":20,"dx":-2,"dy":-1},{"id":17,"x":25,"y":21,"dx":-1,"dy":0},{"id":15,"x":37,"y":21,"dx":-4,"dy":2},{"id":16,"x":47,"y":21,"dx":-1,"dy":2},{"id":15,"x":3,"y":22,"dx":-4,"dy":1},{"id":15,"x":11,"y":22,"dx":0,"dy":-1},{"id":16,"x":21,"y":22,"dx":2,"dy":0},{"id":16,"x":22,"y":22,"dx":2,"dy":-1},{"id":15,"x":55,"y":22,"dx":-4,"dy":0},{"id":17,"x":3,"y":23,"dx":3,"dy":0},{"id":16,"x":34,"y":23,"dx":2,"dy":-1},{"id":15,"x":41,"y":23,"dx":-2,"dy":-2},{"id":15,"x":46,"y":23,"dx":2,"dy":1},{"id":16,"x":50,"y":23,"dx":2,"dy":0},{"id":15,"x":9,"y":24,"dx":1,"dy":-1},{"id":15,"x":28,"y":24,"dx":3,"dy":-2},{"id":15,"x":32,"y":24,"dx":4,"dy":-1},{"id":16,"x":39,"y":24,"dx":2,"dy":0},{"id":15,"x":1,"y":25,"dx":-3,"dy":1},{"id":15,"x":11,"y":25,"dx":-1,"dy":1},{"id":15,"x":23,"y":25,"dx":3,"dy":-1},{"id":17,"x":25,"y":25,"dx":2,"dy":-1},{"id":17,"x":28,"y":25,"dx":1,"dy":1},{"id":17,"x":32,"y":25,"dx":-3,"dy":1},{"id":15,"x":44,"y":25,"dx":-1,"dy":-1},{"id":15,"x":0,"y":26,"dx":-1,"dy":-2},{"id":17,"x":16,"y":26,"dx":2,"dy":-1},{"id":17,"x":17,"y":26,"dx":-4,"dy":2},{"id":15,"x":26,"y":26,"dx":-3,"dy":-1},{"id":17,"x":31,"y":26,"dx":-3,"dy":-2},{"id":15,"x":51,"y":26,"dx":0,"dy":0},{"id":15,"x":6,"y":27,"dx":-1,"dy":-1},{"id":16,"x":11,"y":27,"dx":3,"dy":0},{"id":17,"x":16,"y":27,"dx":3,"dy":1},{"id":15,"x":29,"y":27,"dx":2,"dy":0},{"id":16,"x":36,"y":27,"dx":4,"dy":-1},{"id":16,"x":38,"y":27,"dx":1,"dy":0},{"id":17,"x":50,"y":27,"dx":-3,"dy":-2},{"id":17,"x":53,"y":27,"dx":1,"dy":-1},{"id":15,"x":14,"y":28,"dx":-4,"dy":1},{"id":17,"x":16,"y":29,"dx":-3,"dy":-1},{"id":17,"x":20,"y":29,"dx":-1,"dy":-1},{"id":15,"x":29,"y":29,"dx":0,"dy":1},{"id":16,"x":33,"y":29,"dx":-2,"dy":2},{"id":16,"x":37,"y":29,"dx":0,"dy":2},{"id":15,"x":7,"y":30,"dx":2,"dy":-1},{"id":15,"x":9,"y":30,"dx":0,"dy":2},{"id":16,"x":21,"y":30,"dx":3,"dy":1},{"id":17,"x":41,"y":30,"dx":-4,"dy":0},{"id":17,"x":28,"y":32,"dx":1,"dy":1},{"id":15,"x":32,"y":32,"dx":-1,"dy":-1},{"id":17,"x":33,"y":32,"dx":0,"dy":-2},{"id":17,"x":41,"y":32,"dx":-3,"dy":0},{"id":16,"x":50,"y":32,"dx":0,"dy":-2},{"id":15,"x":11,"y":33,"dx":-3,"dy":2},{"id":17,"x":54,"y":33,"dx":-3,"dy":1},{"id":15,"x":2,"y":34,"dx":1,"dy":-2},{"id":16,"x":24,"y":34,"dx":3,"dy":0},{"id":15,"x":38,"y":34,"dx":3,"dy":0},{"id":15,"x":14,"y":35,"dx":1,"dy":0},{"id":16,"x":53,"y":35,"dx":2,"dy":-1},{"id":15,"x":4,"y":36,"dx":-2,"dy":1},{"id":17,"x":6,"y":36,"dx":1,"dy":-1},{"id":16,"x":10,"y":36,"dx":0,"dy":0},{"id":17,"x":18,"y":36,"dx":-1,"dy":-1},{"id":16,"x":24,"y":36,"dx":0,"dy":0},{"id":16,"x":26,"y":36,"dx":0,"dy":-2},{"id":17,"x":32,"y":36,"dx":-1,"dy":-2},{"id":15,"x":35,"y":36,"dx":-1,"dy":-1},{"id":17,"x":36,"y":36,"dx":-1,"dy":-1},{"id":15,"x":40,"y":36,"dx":0,"dy":0},{"id":15,"x":43,"y":36,"dx":1,"dy":1},{"id":17,"x":12,"y":37,"dx":-2,"dy":-1},{"id":16,"x":27,"y":37,"dx":0,"dy":0},{"id":15,"x":48,"y":37,"dx":-1,"dy":2},{"id":15,"x":50,"y":37,"dx":4,"dy":2},{"id":15,"x":55,"y":37,"dx":2,"dy":1},{"id":16,"x":3,"y":38,"dx":-3,"dy":2},{"id":17,"x":8,"y":38,"dx":2,"dy":0},{"id":16,"x":28,"y":38,"dx":-1,"dy":-1},{"id":15,"x":34,"y":38,"dx":0,"dy":0},{"id":17,"x":25,"y":39,"dx":3,"dy":2},{"id":17,"x":29,"y":39,"dx":2,"dy":0},{"id":15,"x":33,"y":39,"dx":3,"dy":-2},{"id":16,"x":51,"y":39,"dx":-1,"dy":-1},{"id":16,"x":52,"y":39,"dx":-1,"dy":0},{"id":16,"x":53,"y":39,"dx":-2,"dy":0},{"id":15,"x":54,"y":39,"dx":-4,"dy":-2},{"id":16,"x":4,"y":40,"dx":-2,"dy":1},{"id":17,"x":41,"y":40,"dx":-2,"dy":-2},{"id":16,"x":52,"y":40,"dx":1,"dy":2},{"id":17,"x":10,"y":41,"dx":-3,"dy":2},{"id":16,"x":22,"y":41,"dx":0,"dy":0},{"id":16,"x":23,"y":41,"dx":-3,"dy":-1},{"id":15,"x":31,"y":41,"dx":1,"dy":1},{"id":15,"x":43,"y":41,"dx":2,"dy":-1},{"id":15,"x":47,"y":41,"dx":2,"dy":0},{"id":16,"x":53,"y":41,"dx":3,"dy":1},{"id":17,"x":8,"y":42,"dx":1,"dy":-1},{"id":16,"x":14,"y":42,"dx":0,"dy":-1},{"id":17,"x":19,"y":42,"dx":1,"dy":1},{"id":16,"x":23,"y":42,"dx":-2,"dy":1},{"id":15,"x":28,"y":42,"dx":3,"dy":-1},{"id":16,"x":35,"y":42,"dx":2,"dy":0},{"id":17,"x":41,"y":42,"dx":2,"dy":-2},{"id":15,"x":48,"y":42,"dx":0,"dy":1},{"id":16,"x":52,"y":42,"dx":-1,"dy":1},{"id":17,"x":8,"y":43,"dx":-2,"dy":0},{"id":15,"x":16,"y":43,"dx":-1,"dy":1},{"id":15,"x":0,"y":44,"dx":2,"dy":2},{"id":15,"x":30,"y":44,"dx":1,"dy":0},{"id":17,"x":43,"y":44,"dx":-1,"dy":0},{"id":16,"x":14,"y":45,"dx":0,"dy":1},{"id":15,"x":31,"y":45,"dx":3,"dy":2},{"id":15,"x":37,"y":45,"dx":-2,"dy":2},{"id":15,"x":38,"y":45,"dx":1,"dy":1},{"id":15,"x":42,"y":45,"dx":-2,"dy":1},{"id":15,"x":46,"y":45,"dx":-2,"dy":-2},{"id":15,"x":1,"y":46,"dx":-1,"dy":2},{"id":16,"x":15,"y":46,"dx":-2,"dy":-1},{"id":17,"x":29,"y":46,"dx":3,"dy":1},{"id":15,"x":41,"y":46,"dx":-1,"dy":0},{"id":17,"x":42,"y":46,"dx":4,"dy":-1},{"id":15,"x":44,"y":46,"dx":1,"dy":1},{"id":15,"x":52,"y":46,"dx":-3,"dy":-2},{"id":16,"x":54,"y":46,"dx":0,"dy":0},{"id":15,"x":4,"y":47,"dx":3,"dy":2},{"id":16,"x":15,"y":47,"dx":1,"dy":2},{"id":16,"x":22,"y":47,"dx":-3,"dy":-1},{"id":16,"x":24,"y":47,"dx":1,"dy":2},{"id":16,"x":27,"y":47,"dx":-4,"dy":1},{"id":16,"x":33,"y":47,"dx":-1,"dy":2},{"id":15,"x":38,"y":47,"dx":-2,"dy":-1},{"id":17,"x":37,"y":48,"dx":3,"dy":0},{"id":15,"x":41,"y":48,"dx":0,"dy":0},{"id":15,"x":45,"y":48,"dx":0,"dy":-2},{"id":17,"x":46,"y":48,"dx":-4,"dy":1},{"id":16,"x":34,"y":49,"dx":1,"dy":-1},{"id":17,"x":46,"y":49,"dx":-4,"dy":2},{"id":15,"x":49,"y":49,"dx":-3,"dy":-1},{"id":17,"x":1,"y":50,"dx":-1,"dy":1},{"id":16,"x":8,"y":50,"dx":4,"dy":1},{"id":16,"x":22,"y":50,"dx":3,"dy":-1},{"id":16,"x":31,"y":50,"dx":1,"dy":-2},{"id":16,"x":32,"y":50,"dx":-4,"dy":-1},{"id":15,"x":37,"y":50,"dx":-2,"dy":0},{"id":16,"x":54,"y":50,"dx":3,"dy":-1},{"id":16,"x":17,"y":51,"dx":-3,"dy":-1},{"id":15,"x":18,"y":51,"dx":-2,"dy":0},{"id":16,"x":9,"y":52,"dx":-1,"dy":0},{"id":16,"x":10,"y":52,"dx":-1,"dy":0},{"id":16,"x":13,"y":52,"dx":2,"dy":2},{"id":15,"x":19,"y":52,"dx":0,"dy":1},{"id":15,"x":25,"y":52,"dx":3,"dy":-1},{"id":15,"x":26,"y":52,"dx":0,"dy":0},{"id":15,"x":29,"y":52,"dx":-4,"dy":0},{"id":17,"x":40,"y":52,"dx":3,"dy":-1},{"id":16,"x":52,"y":52,"dx":3,"dy":0},{"id":16,"x":11,"y":53,"dx":1,"dy":1},{"id":16,"x":13,"y":53,"dx":-2,"dy":-1},{"id":16,"x":34,"y":53,"dx":-2,"dy":1},{"id":16,"x":52,"y":53,"dx":0,"dy":-1},{"id":16,"x":16,"y":54,"dx":-1,"dy":1},{"id":16,"x":17,"y":54,"dx":0,"dy":1},{"id":15,"x":19,"y":54,"dx":3,"dy":-1},{"id":15,"x":41,"y":54,"dx":-2,"dy":-1},{"id":16,"x":44,"y":54,"dx":1,"dy":-2},{"id":17,"x":6,"y":55,"dx":2,"dy":-2},{"id":15,"x":25,"y":55,"dx":-3,"dy":-1},{"id":15,"x":34,"y":55,"dx":1,"dy":-1}],"props":[]},"ponds":{"width":48,"height":48,"tiles":[[4,1],[0,5],[4,26],[0,1],[4,1],[0,2],[4,8],[3,1],[4,4],[3,2],[0,3],[4,23],[0,1],[4,1],[0,4],[4,10],[3,1],[4,3],[0,1],[1,1],[0,3],[4,3],[0,2],[4,18],[0,2],[4,1],[0,4],[4,13],[3,2],[0,3],[4,3],[0,1],[4,1],[0,2],[4,16],[0,3],[4,1],[0,1],[4,1],[0,1],[4,8],[3,2],[4,6],[1,1],[0,1],[4,2],[28,1],[0,2],[4,5],[0,1],[4,2],[0,1],[4,4],[28,1],[4,4],[0,2],[4,2],[0,1],[4,1],[0,2],[4,15],[0,2],[1,3],[0,1],[4,6],[0,1],[4,2],[0,1],[4,5],[29,1],[4,3],[0,3],[4,4],[0,2],[4,8],[3,1],[4,6],[0,3],[1,3],[4,6],[0,1],[4,1],[0,2],[4,23],[3,1],[4,6],[3,1],[4,1],[3,1],[4,1],[0,2],[4,1],[1,1],[4,4],[0,3],[4,1],[0,2],[4,10],[29,1],[4,2],[0,1],[4,6],[0,1],[4,1],[0,3],[3,1],[4,4],[3,3],[4,1],[0,2],[4,1],[1,1],[4,1],[0,2],[4,1],[0,2],[4,1],[0,3],[4,12],[0,1],[4,2],[0,1],[4,4],[0,4],[4,3],[3,1],[4,4],[0,1],[4,2],[0,2],[1,2],[4,2],[0,2],[4,2],[29,1],[4,7],[0,1],[4,2],[0,2],[4,1],[0,1],[4,1],[0,2],[4,7],[0,5],[3,1],[4,2],[3,1],[4,7],[1,1],[0,1],[4,1],[0,3],[4,9],[0,2],[4,1],[0,1],[4,3],[0,4],[4,2],[0,1],[4,1],[0,5],[4,2],[3,1],[4,2],[3,1],[4,7],[1,1],[0,2],[4,1],[28,1],[4,10],[0,2],[4,1],[0,2],[4,2],[0,2],[4,4],[0,3],[3,1],[0,1],[4,1],[3,1],[4,11],[0,1],[4,1],[1,1],[0,1],[4,1],[0,2],[4,11],[0,2],[4,1],[0,1],[4,2],[0,1],[4,1],[0,1],[4,2],[5,1],[0,3],[4,1],[0,2],[3,2],[4,1],[3,1],[4,7],[0,1],[4,2],[1,2],[4,8],[2,2],[4,1],[2,1],[4,1],[0,1],[4,1],[0,1],[4,1],[2,1],[4,8],[0,1],[28,1],[0,3],[4,1],[0,1],[4,6],[3,1],[4,3],[0,2],[4,2],[1,1],[4,4],[2,1],[0,1],[2,2],[4,1],[2,3],[4,2],[0,1],[2,2],[4,6],[0,1],[4,1],[0,2],[4,1],[0,1],[4,7],[3,1],[4,3],[3,1],[4,3],[0,1],[4,2],[1,1],[4,4],[0,1],[2,13],[0,1],[2,1],[4,1],[2,1],[4,2],[0,1],[4,1],[0,2],[4,14],[0,1],[5,1],[4,1],[0,1],[4,1],[1,2],[4,2],[2,19],[4,2],[0,1],[4,1],[0,1],[4,3],[3,1],[4,4],[3,2],[4,5],[0,3],[2,3],[1,1],[2,21],[4,1],[2,2],[0,1],[4,1],[0,1],[4,3],[3,1],[4,9],[29,1],[4,1],[0,1],[4,1],[2,1],[4,1],[2,1],[1,3],[2,18],[4,2],[2,1],[4,1],[0,1],[2,1],[4,3],[3,1],[4,9],[0,4],[2,1],[0,1],[4,1],[2,3],[1,4],[2,17],[4,2],[2,1],[0,1],[4,5],[3,1],[4,4],[3,1],[4,3],[0,2],[4,1],[0,2],[2,7],[1,1],[2,21],[4,4],[3,3],[4,3],[3,1],[4,4],[0,2],[2,1],[4,1],[2,7],[1,1],[2,20],[4,1],[2,1],[4,2],[3,1],[4,2],[28,1],[4,5],[3,1],[4,1],[0,2],[4,1],[0,1],[2,8],[1,1],[2,19],[4,1],[2,1],[4,9],[3,1],[4,4],[0,2],[2,2],[4,1],[2,7],[1,1],[2,19],[4,2],[2,1],[4,2],[3,1],[4,11],[2,1],[4,1],[2,9],[1,3],[2,18],[4,18],[2,1],[4,1],[2,9],[1,3],[2,17],[4,3],[0,2],[4,3],[0,2],[3,1],[4,1],[3,1],[4,5],[2,12],[1,2],[2,14],[4,2],[28,1],[4,1],[0,1],[4,1],[3,1],[4,3],[3,1],[4,1],[0,1],[4,5],[2,1],[4,1],[2,13],[1,1],[2,14],[4,5],[3,1],[4,1],[3,2],[4,1],[0,2],[4,2],[3,1],[4,5],[2,1],[4,1],[2,11],[1,1],[2,15],[4,5],[3,1],[4,3],[0,4],[4,5],[2,1],[4,1],[2,12],[1,2],[2,12],[4,11],[0,1],[4,1],[0,2],[4,7],[2,2],[29,1],[2,10],[1,6],[2,7],[4,9],[3,1],[4,3],[3,1],[4,2],[3,1],[4,5],[2,2],[4,1],[2,23],[4,6],[3,1],[4,3],[3,1],[4,1],[0,2],[4,8],[2,1],[4,1],[2,2],[0,1],[2,1],[0,1],[2,13],[4,1],[2,3],[4,8],[3,1],[4,2],[3,1],[4,2],[0,1],[3,2],[4,8],[2,1],[4,2],[0,1],[2,1],[4,1],[2,13],[4,2],[2,1],[4,1],[2,1],[0,1],[4,1],[0,1],[4,4],[3,2],[4,4],[0,2],[3,1],[4,8],[5,1],[4,2],[0,1],[4,1],[0,2],[2,8],[0,1],[2,1],[4,1],[2,3],[4,1],[2,1],[4,1],[5,1],[0,2],[4,11],[3,1],[0,1],[4,11],[0,1],[4,1],[0,5],[2,1],[0,2],[2,1],[0,2],[4,1],[2,1],[4,3],[29,1],[4,2],[0,1],[4,1],[0,1],[4,10],[3,1],[4,2],[3,1],[4,11],[0,5],[4,1],[0,2],[4,2],[0,2],[4,24],[3,1],[4,1],[0,1],[4,9],[0,2],[4,1],[0,5],[4,1],[0,2],[4,22],[3,2],[4,15],[0,5],[4,13],[0,1],[4,15],[0,1],[4,1],[0,1],[4,13],[0,2],[4,1],[0,1],[4,11],[0,2],[4,6],[3,1],[4,5],[3,1],[4,7],[28,1],[4,9],[0,1],[29,1],[4,1],[0,1],[4,9],[0,1],[4,1],[0,3],[4,13],[3,1],[4,3],[0,2],[4,11],[0,1],[29,1],[0,5],[4,6],[0,2],[4,1],[0,2],[4,17],[0,3],[4,11],[0,1],[4,1],[0,2],[5,1],[4,7],[29,1],[4,1],[0,3],[4,9],[3,1],[4,2],[3,3],[4,2],[0,2],[4,15],[0,3],[4,1],[0,2],[4,1],[0,1],[4,1],[0,3],[4,1],[0,1],[4,12],[3,1],[4,1],[3,1],[4,15],[0,2],[4,6],[0,1],[4,2],[0,4],[4,2],[0,1],[4,9],[3,1],[4,21],[0,1],[4,6],[0,1],[4,1],[0,1],[4,1],[0,1],[4,17],[3,1],[4,1],[3,1],[4,3],[28,1],[4,2],[0,1],[4,7],[0,2],[4,8],[0,1],[4,1],[0,2],[4,6],[29,1],[4,6],[3,1],[4,10],[0,2],[4,17],[0,1],[4,1],[0,2],[4,12],[3,1],[4,4]],"blocked":[2,3,6,7,24,25,26,27,9,10,18,21,28,29,30,31,32,33],"tall":{"3":{"height":1.5,"ground":0},"6":{"height":2.5,"ground":1},"7":{"height":1.75,"ground":4,"shadow":true},"9":{"height":1,"ground":8},"18":{"height":0.6,"ground":4},"21":{"height":2.5,"ground":8},"22":{"height":1.5,"ground":0},"23":{"height":1.5,"ground":0},"24":{"height":1.75,"ground":4,"shadow":true},"25":{"height":1.75,"ground":4,"shadow":true},"26":{"height":2.5,"ground":4,"shadow":true},"27":{"height":2.5,"ground":4,"shadow":true},"28":{"height":1.05,"ground":4},"29":{"height":1.25,"ground":4},"30":{"height":1.25,"ground":4},"31":{"height":0.75,"ground":4},"32":{"height":1,"ground":4},"33":{"height":1.45,"ground":4}},"decor":[{"id":15,"x":3,"y":0,"dx":-2,"dy":2},{"id":16,"x":13,"y":0,"dx":0,"dy":0},{"id":16,"x":25,"y":0,"dx":1,"dy":-1},{"id":16,"x":37,"y":0,"dx":-4,"dy":-2},{"id":16,"x":43,"y":0,"dx":-4,"dy":0},{"id":16,"x":47,"y":0,"dx":-2,"dy":1},{"id":16,"x":13,"y":1,"dx":2,"dy":1},{"id":16,"x":22,"y":1,"dx":3,"dy":1},{"id":16,"x":39,"y":1,"dx":-1,"dy":0},{"id":15,"x":34,"y":2,"dx":3,"dy":-2},{"id":16,"x":47,"y":2,"dx":-4,"dy":0},{"id":16,"x":6,"y":3,"dx":0,"dy":-1},{"id":16,"x":23,"y":3,"dx":-3,"dy":0},{"id":16,"x":12,"y":4,"dx":-1,"dy":-1},{"id":16,"x":15,"y":4,"dx":2,"dy":-1},{"id":16,"x":28,"y":4,"dx":2,"dy":-2},{"id":16,"x":32,"y":4,"dx":0,"dy":2},{"id":15,"x":36,"y":4,"dx":-2,"dy":-1},{"id":16,"x":37,"y":4,"dx":-3,"dy":1},{"id":16,"x":0,"y":5,"dx":1,"dy":-1},{"id":16,"x":23,"y":5,"dx":3,"dy":0},{"id":16,"x":26,"y":5,"dx":3,"dy":-1},{"id":17,"x":31,"y":5,"dx":1,"dy":-1},{"id":16,"x":11,"y":6,"dx":1,"dy":0},{"id":16,"x":18,"y":6,"dx":-3,"dy":0},{"id":16,"x":21,"y":6,"dx":-3,"dy":-2},{"id":16,"x":34,"y":6,"dx":3,"dy":-1},{"id":16,"x":37,"y":6,"dx":-4,"dy":-2},{"id":16,"x":47,"y":6,"dx":-1,"dy":1},{"id":16,"x":4,"y":7,"dx":2,"dy":-1},{"id":17,"x":7,"y":7,"dx":4,"dy":0},{"id":16,"x":11,"y":7,"dx":-4,"dy":0},{"id":16,"x":21,"y":7,"dx":1,"dy":2},{"id":16,"x":1,"y":8,"dx":2,"dy":1},{"id":16,"x":2,"y":8,"dx":1,"dy":-2},{"id":16,"x":14,"y":8,"dx":2,"dy":0},{"id":16,"x":28,"y":8,"dx":4,"dy":-1},{"id":17,"x":41,"y":8,"dx":3,"dy":2},{"id":16,"x":47,"y":8,"dx":-2,"dy":2},{"id":16,"x":17,"y":9,"dx":0,"dy":0},{"id":16,"x":28,"y":9,"dx":3,"dy":0},{"id":16,"x":33,"y":9,"dx":0,"dy":-1},{"id":16,"x":17,"y":10,"dx":-2,"dy":-2},{"id":16,"x":22,"y":10,"dx":3,"dy":-1},{"id":16,"x":28,"y":10,"dx":-1,"dy":1},{"id":16,"x":37,"y":10,"dx":2,"dy":1},{"id":16,"x":8,"y":11,"dx":1,"dy":1},{"id":16,"x":9,"y":11,"dx":-1,"dy":1},{"id":16,"x":17,"y":11,"dx":0,"dy":-1},{"id":16,"x":21,"y":11,"dx":0,"dy":-1},{"id":16,"x":22,"y":11,"dx":-1,"dy":-2},{"id":16,"x":25,"y":11,"dx":-4,"dy":-2},{"id":17,"x":34,"y":11,"dx":-2,"dy":-1},{"id":17,"x":41,"y":11,"dx":3,"dy":0},{"id":16,"x":1,"y":12,"dx":1,"dy":0},{"id":15,"x":9,"y":12,"dx":3,"dy":-2},{"id":16,"x":10,"y":12,"dx":4,"dy":1},{"id":16,"x":16,"y":12,"dx":-3,"dy":1},{"id":16,"x":19,"y":12,"dx":1,"dy":-2},{"id":16,"x":26,"y":12,"dx":4,"dy":1},{"id":16,"x":3,"y":13,"dx":0,"dy":1},{"id":16,"x":31,"y":13,"dx":-1,"dy":0},{"id":16,"x":33,"y":13,"dx":2,"dy":-1},{"id":16,"x":37,"y":13,"dx":1,"dy":1},{"id":17,"x":43,"y":13,"dx":0,"dy":-1},{"id":16,"x":1,"y":14,"dx":-1,"dy":0},{"id":16,"x":5,"y":14,"dx":2,"dy":-2},{"id":15,"x":41,"y":14,"dx":-3,"dy":1},{"id":16,"x":42,"y":14,"dx":-1,"dy":-1},{"id":16,"x":3,"y":15,"dx":4,"dy":-1},{"id":16,"x":13,"y":15,"dx":1,"dy":0},{"id":16,"x":16,"y":15,"dx":0,"dy":2},{"id":16,"x":44,"y":15,"dx":2,"dy":1},{"id":16,"x":41,"y":16,"dx":2,"dy":2},{"id":16,"x":45,"y":16,"dx":-2,"dy":-2},{"id":16,"x":46,"y":16,"dx":4,"dy":-1},{"id":16,"x":45,"y":17,"dx":-4,"dy":1},{"id":16,"x":47,"y":17,"dx":3,"dy":1},{"id":15,"x":38,"y":18,"dx":-2,"dy":0},{"id":16,"x":1,"y":19,"dx":2,"dy":0},{"id":16,"x":40,"y":19,"dx":-3,"dy":-1},{"id":16,"x":3,"y":21,"dx":1,"dy":-1},{"id":15,"x":6,"y":22,"dx":-2,"dy":2},{"id":15,"x":7,"y":22,"dx":2,"dy":-2},{"id":15,"x":9,"y":22,"dx":2,"dy":-1},{"id":16,"x":39,"y":23,"dx":-1,"dy":0},{"id":16,"x":46,"y":23,"dx":0,"dy":0},{"id":16,"x":1,"y":24,"dx":2,"dy":-2},{"id":16,"x":3,"y":24,"dx":-3,"dy":0},{"id":16,"x":2,"y":25,"dx":2,"dy":0},{"id":16,"x":46,"y":26,"dx":2,"dy":1},{"id":16,"x":4,"y":27,"dx":1,"dy":-2},{"id":16,"x":38,"y":27,"dx":-2,"dy":1},{"id":16,"x":41,"y":27,"dx":4,"dy":-1},{"id":16,"x":3,"y":28,"dx":2,"dy":-2},{"id":16,"x":47,"y":28,"dx":-2,"dy":2},{"id":16,"x":41,"y":29,"dx":0,"dy":0},{"id":15,"x":3,"y":30,"dx":-3,"dy":0},{"id":16,"x":9,"y":30,"dx":-3,"dy":1},{"id":16,"x":40,"y":30,"dx":-4,"dy":0},{"id":16,"x":41,"y":30,"dx":0,"dy":-2},{"id":16,"x":43,"y":30,"dx":0,"dy":-1},{"id":16,"x":41,"y":31,"dx":4,"dy":-2},{"id":16,"x":31,"y":32,"dx":-2,"dy":-2},{"id":16,"x":36,"y":32,"dx":-2,"dy":-2},{"id":16,"x":47,"y":32,"dx":-3,"dy":1},{"id":17,"x":36,"y":33,"dx":2,"dy":0},{"id":16,"x":39,"y":33,"dx":2,"dy":0},{"id":16,"x":45,"y":33,"dx":2,"dy":2},{"id":16,"x":7,"y":34,"dx":3,"dy":0},{"id":15,"x":27,"y":34,"dx":1,"dy":1},{"id":16,"x":35,"y":34,"dx":-3,"dy":1},{"id":16,"x":39,"y":34,"dx":1,"dy":0},{"id":16,"x":1,"y":35,"dx":-1,"dy":-1},{"id":16,"x":31,"y":35,"dx":2,"dy":-1},{"id":16,"x":8,"y":36,"dx":-1,"dy":1},{"id":16,"x":12,"y":36,"dx":-3,"dy":0},{"id":17,"x":17,"y":36,"dx":-1,"dy":-1},{"id":16,"x":40,"y":36,"dx":2,"dy":-2},{"id":16,"x":45,"y":36,"dx":-1,"dy":-1},{"id":16,"x":46,"y":36,"dx":3,"dy":2},{"id":16,"x":9,"y":37,"dx":2,"dy":-1},{"id":16,"x":13,"y":37,"dx":-2,"dy":1},{"id":15,"x":16,"y":37,"dx":2,"dy":-1},{"id":16,"x":33,"y":37,"dx":1,"dy":1},{"id":16,"x":39,"y":37,"dx":-1,"dy":0},{"id":16,"x":40,"y":37,"dx":-1,"dy":1},{"id":16,"x":41,"y":37,"dx":-4,"dy":1},{"id":16,"x":10,"y":38,"dx":-3,"dy":1},{"id":16,"x":15,"y":38,"dx":2,"dy":0},{"id":17,"x":18,"y":38,"dx":1,"dy":2},{"id":16,"x":44,"y":38,"dx":-3,"dy":-1},{"id":16,"x":45,"y":38,"dx":-3,"dy":0},{"id":16,"x":5,"y":39,"dx":1,"dy":-1},{"id":16,"x":17,"y":39,"dx":-3,"dy":1},{"id":16,"x":3,"y":40,"dx":-2,"dy":0},{"id":16,"x":16,"y":40,"dx":3,"dy":-2},{"id":16,"x":21,"y":40,"dx":-1,"dy":-1},{"id":16,"x":29,"y":40,"dx":-2,"dy":2},{"id":16,"x":41,"y":40,"dx":2,"dy":0},{"id":16,"x":5,"y":41,"dx":-1,"dy":-1},{"id":16,"x":2,"y":42,"dx":3,"dy":-2},{"id":15,"x":6,"y":42,"dx":3,"dy":0},{"id":16,"x":9,"y":42,"dx":-3,"dy":2},{"id":16,"x":42,"y":42,"dx":-2,"dy":1},{"id":16,"x":43,"y":42,"dx":-3,"dy":2},{"id":16,"x":47,"y":42,"dx":4,"dy":1},{"id":16,"x":5,"y":43,"dx":2,"dy":1},{"id":16,"x":8,"y":43,"dx":3,"dy":2},{"id":16,"x":21,"y":43,"dx":-1,"dy":0},{"id":17,"x":28,"y":43,"dx":2,"dy":-1},{"id":15,"x":32,"y":43,"dx":-1,"dy":-2},{"id":16,"x":38,"y":43,"dx":-2,"dy":-2},{"id":16,"x":41,"y":43,"dx":2,"dy":2},{"id":16,"x":42,"y":43,"dx":-3,"dy":-1},{"id":16,"x":46,"y":43,"dx":-1,"dy":0},{"id":16,"x":0,"y":44,"dx":-1,"dy":1},{"id":16,"x":2,"y":44,"dx":3,"dy":2},{"id":15,"x":30,"y":44,"dx":4,"dy":-1},{"id":16,"x":35,"y":44,"dx":1,"dy":0},{"id":16,"x":37,"y":44,"dx":3,"dy":-1},{"id":16,"x":42,"y":44,"dx":1,"dy":-1},{"id":16,"x":14,"y":45,"dx":1,"dy":0},{"id":16,"x":17,"y":45,"dx":-3,"dy":1},{"id":16,"x":25,"y":45,"dx":-4,"dy":0},{"id":15,"x":29,"y":45,"dx":4,"dy":1},{"id":16,"x":30,"y":45,"dx":-2,"dy":2},{"id":16,"x":36,"y":45,"dx":2,"dy":1},{"id":16,"x":39,"y":45,"dx":-1,"dy":0},{"id":16,"x":40,"y":45,"dx":0,"dy":1},{"id":16,"x":9,"y":46,"dx":-3,"dy":2},{"id":16,"x":11,"y":46,"dx":2,"dy":-2},{"id":16,"x":21,"y":46,"dx":1,"dy":-1},{"id":16,"x":22,"y":46,"dx":-3,"dy":2},{"id":16,"x":24,"y":46,"dx":3,"dy":0},{"id":16,"x":3,"y":47,"dx":-1,"dy":2},{"id":16,"x":18,"y":47,"dx":3,"dy":-1},{"id":16,"x":20,"y":47,"dx":1,"dy":1},{"id":15,"x":30,"y":47,"dx":3,"dy":1}],"props":[]},"zvenets":{"width":56,"height":40,"tiles":[[4,10],[0,4],[4,5],[0,1],[4,12],[0,2],[4,11],[0,3],[4,17],[0,1],[4,3],[0,1],[4,4],[0,2],[4,10],[0,4],[4,11],[0,3],[4,4],[3,2],[4,7],[3,1],[4,3],[0,1],[4,8],[0,3],[4,10],[0,1],[4,10],[0,2],[4,1],[0,3],[4,18],[0,4],[4,16],[0,1],[4,12],[0,2],[4,11],[3,1],[4,41],[0,4],[4,12],[3,1],[0,1],[4,2],[0,5],[4,33],[0,2],[4,9],[3,1],[0,1],[4,1],[0,7],[4,34],[0,2],[4,1],[0,1],[4,7],[3,1],[4,1],[0,1],[4,4],[3,1],[0,3],[4,1],[0,1],[4,8],[29,1],[4,34],[3,1],[4,1],[0,2],[4,3],[0,5],[4,39],[0,1],[4,2],[0,2],[4,2],[0,1],[4,1],[3,1],[4,1],[3,1],[0,6],[4,39],[0,4],[4,6],[3,1],[0,1],[4,1],[0,2],[4,3],[0,1],[4,19],[0,1],[4,4],[27,1],[4,11],[0,4],[3,1],[4,8],[28,1],[4,3],[0,2],[4,19],[0,3],[4,15],[0,5],[4,6],[3,1],[4,5],[0,1],[4,1],[0,1],[7,1],[4,1],[0,3],[1,7],[4,1],[1,13],[4,10],[0,5],[3,1],[4,16],[0,1],[10,1],[0,1],[1,11],[4,1],[1,5],[4,1],[1,4],[4,9],[0,1],[4,1],[0,1],[4,1],[0,1],[4,6],[3,2],[4,9],[0,2],[4,1],[1,1],[0,1],[1,8],[6,1],[1,11],[4,10],[0,1],[4,1],[0,1],[4,17],[0,3],[4,1],[1,5],[4,1],[1,6],[4,1],[1,8],[4,1],[0,2],[4,28],[0,2],[4,1],[0,1],[1,3],[4,1],[1,16],[4,1],[1,1],[4,1],[0,3],[4,8],[3,1],[4,1],[3,1],[4,16],[0,3],[1,3],[4,1],[1,18],[0,1],[4,1],[0,1],[4,8],[3,2],[4,5],[3,1],[4,5],[0,4],[4,3],[0,2],[4,1],[1,22],[4,1],[0,3],[4,15],[3,1],[4,1],[3,1],[4,2],[0,2],[4,8],[1,5],[4,1],[1,16],[0,3],[26,1],[4,14],[1,21],[4,1],[1,4],[0,1],[4,1],[1,10],[4,11],[3,1],[4,1],[1,2],[4,10],[0,2],[4,5],[0,1],[4,1],[1,22],[4,3],[29,1],[4,12],[0,2],[4,2],[3,1],[4,3],[0,1],[4,2],[0,1],[4,6],[0,1],[1,12],[4,1],[1,2],[4,2],[1,4],[4,11],[3,1],[4,4],[0,2],[4,4],[0,1],[4,2],[0,1],[4,8],[1,12],[4,1],[1,1],[4,1],[1,7],[4,8],[0,3],[4,1],[0,3],[4,1],[0,2],[3,1],[4,4],[0,2],[4,9],[1,8],[4,1],[1,9],[4,1],[1,3],[4,10],[0,1],[3,1],[4,1],[3,1],[0,2],[4,1],[0,1],[4,1],[0,2],[3,1],[0,1],[4,3],[0,1],[4,7],[1,4],[4,1],[1,9],[0,1],[1,1],[0,1],[1,5],[4,8],[0,1],[4,1],[0,1],[3,2],[4,1],[0,1],[3,1],[4,1],[0,1],[3,1],[4,1],[3,1],[4,1],[0,2],[4,1],[0,2],[4,7],[1,16],[4,1],[1,4],[4,9],[0,2],[4,2],[0,2],[4,2],[0,1],[4,1],[0,2],[4,2],[0,1],[4,1],[0,2],[4,8],[0,1],[1,21],[4,9],[0,2],[4,1],[0,1],[4,1],[0,1],[3,1],[0,1],[4,1],[3,1],[0,2],[4,14],[0,4],[24,1],[4,1],[0,1],[4,5],[0,2],[25,1],[4,18],[3,1],[4,3],[0,4],[4,13],[0,1],[4,1],[0,5],[4,7],[0,3],[4,26],[3,1],[4,12],[0,7],[4,7],[0,1],[4,20],[3,1],[4,2],[3,1],[4,2],[3,1],[4,15],[0,1],[4,1],[0,4],[4,2],[0,1],[4,34],[3,1],[4,11],[0,10],[4,24],[3,1],[4,8],[3,1],[4,12],[0,2],[4,2],[0,2],[4,1],[0,1],[4,1],[0,1],[4,11],[29,1],[4,16],[3,1],[0,2],[4,7],[29,1],[4,11],[0,2],[4,1],[0,1],[4,1],[0,1],[4,27],[3,1],[4,1],[0,3],[4,2],[3,1],[4,14],[0,2],[4,1],[0,4],[4,29],[0,3],[4,19],[0,1],[4,34],[3,1],[4,50],[0,1],[4,6],[3,1],[4,9],[28,1],[4,19],[0,2],[4,15],[0,1],[4,1],[0,1],[3,2],[4,4],[3,1],[4,28],[0,3],[4,21]],"blocked":[2,3,6,7,24,25,26,27,9,10,18,21,28,29,30,31,32,33],"tall":{"3":{"height":1.5,"ground":0},"6":{"height":2.5,"ground":1},"7":{"height":1.75,"ground":4,"shadow":true},"9":{"height":1,"ground":8},"18":{"height":0.6,"ground":4},"21":{"height":2.5,"ground":8},"22":{"height":1.5,"ground":0},"23":{"height":1.5,"ground":0},"24":{"height":1.75,"ground":4,"shadow":true},"25":{"height":1.75,"ground":4,"shadow":true},"26":{"height":2.5,"ground":4,"shadow":true},"27":{"height":2.5,"ground":4,"shadow":true},"28":{"height":1.05,"ground":4},"29":{"height":1.25,"ground":4},"30":{"height":1.25,"ground":4},"31":{"height":0.75,"ground":4},"32":{"height":1,"ground":4},"33":{"height":1.45,"ground":4}},"decor":[{"id":16,"x":14,"y":0,"dx":1,"dy":1},{"id":16,"x":23,"y":0,"dx":0,"dy":1},{"id":16,"x":26,"y":0,"dx":3,"dy":1},{"id":16,"x":31,"y":0,"dx":0,"dy":0},{"id":16,"x":37,"y":0,"dx":3,"dy":0},{"id":16,"x":39,"y":0,"dx":3,"dy":2},{"id":16,"x":49,"y":0,"dx":-1,"dy":1},{"id":16,"x":3,"y":1,"dx":1,"dy":0},{"id":16,"x":5,"y":1,"dx":4,"dy":1},{"id":15,"x":13,"y":1,"dx":-1,"dy":-2},{"id":16,"x":39,"y":1,"dx":4,"dy":1},{"id":17,"x":45,"y":1,"dx":0,"dy":0},{"id":16,"x":55,"y":2,"dx":3,"dy":-1},{"id":16,"x":21,"y":3,"dx":1,"dy":-1},{"id":16,"x":27,"y":3,"dx":-2,"dy":-2},{"id":16,"x":34,"y":3,"dx":1,"dy":-2},{"id":16,"x":36,"y":3,"dx":0,"dy":0},{"id":16,"x":3,"y":4,"dx":1,"dy":-1},{"id":16,"x":4,"y":4,"dx":3,"dy":2},{"id":16,"x":11,"y":4,"dx":2,"dy":0},{"id":16,"x":13,"y":4,"dx":2,"dy":1},{"id":16,"x":18,"y":4,"dx":1,"dy":0},{"id":16,"x":51,"y":4,"dx":1,"dy":-1},{"id":16,"x":5,"y":5,"dx":0,"dy":2},{"id":16,"x":19,"y":5,"dx":0,"dy":1},{"id":16,"x":31,"y":5,"dx":-3,"dy":-1},{"id":15,"x":5,"y":6,"dx":4,"dy":0},{"id":16,"x":13,"y":6,"dx":2,"dy":2},{"id":16,"x":18,"y":6,"dx":-3,"dy":-1},{"id":16,"x":24,"y":6,"dx":-2,"dy":2},{"id":16,"x":41,"y":6,"dx":1,"dy":0},{"id":16,"x":48,"y":6,"dx":-2,"dy":1},{"id":15,"x":6,"y":7,"dx":-1,"dy":-1},{"id":16,"x":13,"y":7,"dx":3,"dy":1},{"id":16,"x":20,"y":7,"dx":-4,"dy":2},{"id":16,"x":26,"y":8,"dx":3,"dy":-2},{"id":16,"x":47,"y":8,"dx":3,"dy":-2},{"id":15,"x":9,"y":9,"dx":0,"dy":2},{"id":16,"x":17,"y":10,"dx":3,"dy":2},{"id":16,"x":20,"y":10,"dx":-3,"dy":-2},{"id":16,"x":38,"y":10,"dx":-3,"dy":1},{"id":16,"x":45,"y":10,"dx":0,"dy":-2},{"id":16,"x":12,"y":11,"dx":2,"dy":-1},{"id":16,"x":17,"y":11,"dx":-3,"dy":1},{"id":16,"x":46,"y":11,"dx":0,"dy":1},{"id":15,"x":51,"y":11,"dx":2,"dy":0},{"id":16,"x":8,"y":12,"dx":1,"dy":-1},{"id":16,"x":14,"y":12,"dx":-4,"dy":1},{"id":16,"x":41,"y":12,"dx":3,"dy":2},{"id":16,"x":45,"y":12,"dx":3,"dy":0},{"id":16,"x":0,"y":13,"dx":1,"dy":-2},{"id":16,"x":4,"y":13,"dx":-3,"dy":1},{"id":16,"x":8,"y":13,"dx":3,"dy":-1},{"id":16,"x":42,"y":13,"dx":3,"dy":2},{"id":16,"x":44,"y":13,"dx":0,"dy":2},{"id":16,"x":50,"y":13,"dx":-1,"dy":2},{"id":16,"x":54,"y":13,"dx":-1,"dy":2},{"id":16,"x":49,"y":14,"dx":-3,"dy":2},{"id":16,"x":55,"y":14,"dx":-2,"dy":-2},{"id":16,"x":6,"y":15,"dx":0,"dy":1},{"id":16,"x":8,"y":15,"dx":0,"dy":-1},{"id":16,"x":10,"y":15,"dx":-4,"dy":1},{"id":16,"x":30,"y":15,"dx":-1,"dy":-1},{"id":16,"x":47,"y":15,"dx":3,"dy":-1},{"id":16,"x":51,"y":15,"dx":-1,"dy":-1},{"id":16,"x":6,"y":16,"dx":-1,"dy":2},{"id":16,"x":8,"y":16,"dx":3,"dy":2},{"id":16,"x":11,"y":16,"dx":1,"dy":1},{"id":16,"x":38,"y":16,"dx":2,"dy":1},{"id":16,"x":44,"y":16,"dx":0,"dy":1},{"id":16,"x":47,"y":16,"dx":1,"dy":0},{"id":16,"x":3,"y":17,"dx":2,"dy":1},{"id":16,"x":11,"y":17,"dx":3,"dy":-2},{"id":16,"x":1,"y":18,"dx":2,"dy":-2},{"id":16,"x":14,"y":18,"dx":-2,"dy":-1},{"id":16,"x":48,"y":18,"dx":0,"dy":-1},{"id":16,"x":4,"y":19,"dx":-1,"dy":0},{"id":16,"x":6,"y":19,"dx":3,"dy":-1},{"id":17,"x":9,"y":19,"dx":3,"dy":0},{"id":16,"x":14,"y":19,"dx":-1,"dy":2},{"id":16,"x":54,"y":19,"dx":-1,"dy":2},{"id":16,"x":1,"y":20,"dx":-4,"dy":2},{"id":16,"x":29,"y":20,"dx":1,"dy":1},{"id":16,"x":40,"y":20,"dx":-1,"dy":0},{"id":16,"x":4,"y":21,"dx":-1,"dy":1},{"id":16,"x":8,"y":21,"dx":-1,"dy":-2},{"id":17,"x":16,"y":21,"dx":-4,"dy":1},{"id":16,"x":17,"y":21,"dx":4,"dy":-2},{"id":16,"x":13,"y":22,"dx":2,"dy":-1},{"id":16,"x":52,"y":22,"dx":0,"dy":-2},{"id":16,"x":5,"y":23,"dx":0,"dy":-1},{"id":16,"x":46,"y":23,"dx":1,"dy":-1},{"id":16,"x":55,"y":23,"dx":-3,"dy":1},{"id":16,"x":5,"y":24,"dx":-2,"dy":-1},{"id":16,"x":17,"y":24,"dx":-1,"dy":1},{"id":16,"x":49,"y":24,"dx":4,"dy":-1},{"id":15,"x":3,"y":25,"dx":-2,"dy":-1},{"id":15,"x":10,"y":25,"dx":2,"dy":-2},{"id":16,"x":17,"y":25,"dx":-3,"dy":-2},{"id":15,"x":34,"y":25,"dx":-1,"dy":-1},{"id":16,"x":41,"y":25,"dx":-3,"dy":-1},{"id":16,"x":42,"y":25,"dx":-2,"dy":0},{"id":16,"x":3,"y":26,"dx":2,"dy":0},{"id":17,"x":7,"y":26,"dx":-3,"dy":-1},{"id":16,"x":13,"y":26,"dx":-2,"dy":0},{"id":16,"x":45,"y":26,"dx":-4,"dy":0},{"id":16,"x":51,"y":26,"dx":-2,"dy":0},{"id":16,"x":13,"y":27,"dx":-1,"dy":-1},{"id":16,"x":16,"y":27,"dx":-3,"dy":1},{"id":16,"x":46,"y":27,"dx":3,"dy":-2},{"id":16,"x":53,"y":27,"dx":3,"dy":2},{"id":16,"x":12,"y":28,"dx":2,"dy":1},{"id":17,"x":19,"y":28,"dx":-2,"dy":-2},{"id":16,"x":40,"y":28,"dx":0,"dy":-2},{"id":16,"x":41,"y":28,"dx":2,"dy":1},{"id":15,"x":2,"y":29,"dx":0,"dy":-2},{"id":16,"x":6,"y":29,"dx":-4,"dy":-1},{"id":15,"x":20,"y":29,"dx":-2,"dy":-1},{"id":16,"x":46,"y":29,"dx":0,"dy":1},{"id":16,"x":52,"y":29,"dx":-1,"dy":1},{"id":16,"x":53,"y":29,"dx":-4,"dy":0},{"id":16,"x":54,"y":29,"dx":-3,"dy":1},{"id":16,"x":0,"y":30,"dx":3,"dy":-2},{"id":16,"x":1,"y":30,"dx":2,"dy":-2},{"id":16,"x":25,"y":30,"dx":1,"dy":0},{"id":16,"x":34,"y":30,"dx":-2,"dy":1},{"id":16,"x":36,"y":30,"dx":-2,"dy":1},{"id":16,"x":37,"y":30,"dx":0,"dy":-2},{"id":16,"x":41,"y":30,"dx":4,"dy":-1},{"id":16,"x":48,"y":30,"dx":2,"dy":1},{"id":15,"x":20,"y":31,"dx":2,"dy":1},{"id":16,"x":29,"y":31,"dx":0,"dy":-1},{"id":16,"x":36,"y":31,"dx":-1,"dy":-2},{"id":16,"x":48,"y":31,"dx":1,"dy":1},{"id":16,"x":49,"y":31,"dx":3,"dy":-1},{"id":16,"x":32,"y":32,"dx":0,"dy":0},{"id":16,"x":37,"y":32,"dx":-2,"dy":2},{"id":16,"x":41,"y":32,"dx":-1,"dy":-2},{"id":16,"x":52,"y":32,"dx":-1,"dy":-2},{"id":16,"x":54,"y":32,"dx":3,"dy":1},{"id":16,"x":7,"y":33,"dx":1,"dy":1},{"id":16,"x":10,"y":33,"dx":-1,"dy":0},{"id":16,"x":48,"y":33,"dx":-2,"dy":1},{"id":15,"x":0,"y":34,"dx":4,"dy":-1},{"id":16,"x":12,"y":34,"dx":0,"dy":-1},{"id":16,"x":33,"y":34,"dx":2,"dy":-2},{"id":16,"x":38,"y":34,"dx":3,"dy":1},{"id":16,"x":53,"y":34,"dx":0,"dy":-1},{"id":15,"x":20,"y":35,"dx":-1,"dy":1},{"id":16,"x":35,"y":35,"dx":2,"dy":-2},{"id":16,"x":39,"y":35,"dx":-2,"dy":1},{"id":16,"x":47,"y":35,"dx":-3,"dy":-1},{"id":16,"x":55,"y":35,"dx":-2,"dy":-2},{"id":16,"x":11,"y":36,"dx":1,"dy":-1},{"id":16,"x":14,"y":36,"dx":-2,"dy":0},{"id":16,"x":26,"y":36,"dx":0,"dy":2},{"id":16,"x":39,"y":36,"dx":0,"dy":0},{"id":16,"x":42,"y":36,"dx":0,"dy":-2},{"id":16,"x":45,"y":36,"dx":2,"dy":-1},{"id":16,"x":4,"y":37,"dx":-3,"dy":1},{"id":16,"x":10,"y":37,"dx":3,"dy":1},{"id":16,"x":15,"y":37,"dx":-3,"dy":1},{"id":16,"x":34,"y":37,"dx":0,"dy":-2},{"id":16,"x":47,"y":37,"dx":1,"dy":1},{"id":16,"x":49,"y":37,"dx":-2,"dy":1},{"id":16,"x":53,"y":37,"dx":3,"dy":-1},{"id":16,"x":55,"y":37,"dx":-1,"dy":0},{"id":16,"x":6,"y":38,"dx":-3,"dy":2},{"id":16,"x":7,"y":38,"dx":0,"dy":2},{"id":16,"x":38,"y":38,"dx":-3,"dy":-2},{"id":16,"x":41,"y":38,"dx":-1,"dy":-1},{"id":16,"x":45,"y":38,"dx":2,"dy":0},{"id":16,"x":7,"y":39,"dx":2,"dy":1},{"id":16,"x":16,"y":39,"dx":2,"dy":0},{"id":16,"x":19,"y":39,"dx":-3,"dy":0},{"id":16,"x":22,"y":39,"dx":0,"dy":0},{"id":16,"x":26,"y":39,"dx":2,"dy":-1},{"id":16,"x":38,"y":39,"dx":-4,"dy":-1},{"id":16,"x":43,"y":39,"dx":-2,"dy":-2},{"id":16,"x":44,"y":39,"dx":-3,"dy":-2},{"id":16,"x":46,"y":39,"dx":1,"dy":0},{"id":16,"x":52,"y":39,"dx":-1,"dy":1}],"props":[{"id":34,"x":46,"y":24,"w":2,"h":2,"ground":4}]},"rust_grove":{"width":48,"height":40,"tiles":[[3,8],[4,3],[3,5],[4,1],[3,3],[4,1],[3,1],[4,1],[3,2],[4,1],[3,2],[4,1],[3,1],[4,1],[3,7],[4,1],[3,4],[4,1],[3,3],[4,1],[3,3],[4,2],[3,5],[4,2],[3,1],[4,1],[3,2],[4,1],[3,6],[4,1],[3,2],[4,1],[3,1],[4,1],[3,5],[4,1],[3,7],[4,1],[3,3],[4,1],[3,7],[29,1],[3,2],[28,1],[3,1],[4,1],[3,2],[4,1],[3,1],[0,1],[3,4],[4,3],[3,10],[4,1],[3,6],[4,2],[3,11],[4,1],[3,1],[4,1],[3,2],[4,1],[3,4],[4,3],[3,1],[4,1],[3,4],[4,2],[3,1],[4,1],[3,1],[4,3],[3,2],[4,1],[3,4],[4,2],[3,15],[4,1],[3,4],[29,1],[3,4],[4,4],[3,4],[4,6],[3,3],[4,1],[3,3],[4,1],[3,3],[4,1],[3,6],[4,2],[3,1],[4,3],[3,5],[4,3],[3,2],[4,1],[3,3],[4,1],[3,6],[4,4],[3,1],[4,1],[3,1],[4,1],[3,6],[4,2],[3,6],[4,1],[3,3],[4,2],[3,2],[4,4],[3,4],[4,1],[3,2],[4,1],[3,8],[18,1],[4,2],[3,1],[0,1],[4,2],[3,3],[4,1],[3,4],[4,1],[3,2],[4,1],[3,2],[4,1],[3,4],[4,1],[3,2],[4,1],[3,2],[4,5],[3,1],[4,1],[3,3],[0,1],[3,3],[4,1],[3,1],[4,1],[3,1],[4,3],[3,3],[4,2],[3,1],[4,2],[3,1],[4,1],[3,5],[4,2],[3,1],[4,1],[3,1],[4,1],[3,2],[18,1],[4,1],[3,1],[4,1],[3,1],[4,1],[3,1],[4,2],[3,3],[4,1],[3,3],[4,3],[3,1],[4,2],[3,3],[4,2],[3,8],[4,4],[3,7],[4,1],[18,1],[3,3],[4,6],[3,1],[4,1],[3,1],[4,1],[3,2],[4,1],[3,2],[4,1],[3,3],[4,3],[3,1],[0,1],[3,3],[4,1],[3,6],[4,2],[3,3],[4,1],[3,2],[4,1],[3,1],[4,1],[18,1],[3,1],[0,1],[4,2],[3,2],[4,5],[18,1],[4,1],[3,1],[4,1],[3,2],[4,3],[3,1],[4,2],[3,1],[4,1],[3,2],[4,1],[3,1],[4,2],[3,6],[4,1],[3,4],[4,1],[3,2],[4,2],[3,2],[4,1],[3,3],[4,1],[3,1],[4,3],[3,1],[18,1],[3,1],[4,3],[3,1],[4,1],[3,1],[4,3],[3,4],[0,1],[3,4],[4,1],[3,1],[4,1],[3,1],[4,2],[0,1],[4,3],[3,1],[4,1],[3,1],[4,1],[3,1],[4,3],[3,1],[4,1],[3,3],[4,1],[29,1],[3,5],[4,2],[3,1],[4,1],[3,1],[4,5],[3,2],[4,1],[3,1],[4,1],[3,2],[4,4],[3,1],[4,2],[3,1],[4,1],[3,5],[4,5],[3,2],[4,2],[3,3],[4,3],[3,2],[4,4],[3,1],[4,1],[3,1],[4,4],[3,1],[4,1],[3,4],[4,4],[3,1],[4,1],[3,2],[4,3],[3,2],[4,7],[3,3],[4,4],[3,2],[4,4],[3,2],[4,1],[3,1],[4,5],[3,3],[4,6],[3,5],[4,1],[3,3],[4,3],[3,2],[4,2],[3,3],[4,1],[3,1],[4,15],[3,2],[4,2],[3,2],[4,1],[3,1],[4,2],[3,1],[4,2],[3,2],[4,2],[3,1],[4,5],[3,2],[4,1],[3,1],[4,3],[3,2],[4,11],[3,1],[4,5],[3,2],[4,1],[29,1],[3,1],[4,1],[3,2],[4,2],[3,1],[4,1],[3,6],[4,2],[3,1],[4,2],[3,2],[4,2],[3,1],[4,17],[3,5],[4,1],[3,1],[4,3],[3,4],[4,1],[3,2],[4,2],[3,2],[4,2],[3,1],[4,1],[3,1],[4,3],[3,1],[4,2],[3,2],[4,13],[3,1],[4,1],[3,1],[4,1],[3,1],[4,1],[3,2],[4,1],[3,2],[4,2],[3,1],[4,4],[3,2],[4,1],[3,3],[4,1],[3,1],[4,1],[3,1],[4,2],[3,1],[4,3],[3,1],[4,13],[3,1],[4,1],[3,1],[4,1],[28,1],[3,1],[4,1],[3,1],[4,1],[3,2],[29,1],[4,3],[3,3],[1,17],[4,13],[1,16],[4,3],[3,9],[4,1],[3,1],[4,1],[3,1],[4,2],[3,1],[4,13],[3,2],[4,1],[3,2],[4,3],[3,2],[4,1],[3,5],[4,3],[3,1],[4,1],[3,3],[28,1],[4,1],[3,3],[4,1],[3,2],[4,17],[3,2],[4,1],[3,1],[4,1],[3,3],[4,2],[3,4],[4,3],[3,1],[4,2],[3,4],[4,1],[3,1],[4,1],[3,1],[4,1],[3,1],[4,3],[3,1],[4,14],[3,1],[4,1],[3,1],[4,2],[3,1],[4,2],[3,1],[4,1],[3,3],[4,1],[3,1],[4,1],[0,1],[3,1],[4,1],[3,1],[4,1],[3,3],[4,1],[3,4],[4,1],[3,2],[4,1],[3,1],[4,1],[3,1],[4,9],[3,1],[4,3],[3,2],[4,1],[3,1],[4,1],[29,1],[4,3],[3,3],[4,1],[3,3],[4,1],[3,3],[4,2],[3,1],[4,1],[3,1],[4,2],[3,1],[4,2],[3,1],[4,1],[3,1],[4,1],[3,1],[4,4],[3,1],[4,1],[3,1],[4,2],[3,1],[4,1],[3,5],[4,1],[3,1],[4,4],[3,1],[4,1],[3,2],[4,1],[3,3],[4,1],[3,4],[4,3],[3,1],[4,3],[3,1],[4,4],[3,3],[4,3],[3,3],[4,3],[3,2],[4,3],[3,1],[4,1],[3,3],[4,2],[3,1],[4,2],[3,3],[4,1],[3,8],[0,1],[3,4],[4,1],[3,2],[4,3],[3,1],[4,3],[3,1],[4,2],[3,1],[4,1],[3,5],[4,4],[3,4],[4,3],[3,5],[4,1],[3,1],[4,2],[3,1],[4,1],[3,1],[4,1],[3,1],[4,5],[3,1],[4,2],[3,2],[4,6],[0,1],[3,1],[4,2],[3,1],[4,1],[18,1],[3,1],[18,1],[3,2],[4,2],[3,2],[4,2],[3,11],[4,2],[3,4],[4,1],[3,1],[4,1],[3,4],[4,3],[3,2],[4,2],[3,2],[4,3],[3,3],[4,1],[3,2],[4,1],[3,3],[4,1],[3,4],[4,2],[3,2],[4,1],[3,1],[4,2],[3,6],[4,2],[3,2],[18,1],[3,1],[18,1],[3,1],[4,1],[3,1],[4,3],[3,2],[4,4],[3,1],[4,2],[3,2],[4,1],[3,4],[4,2],[3,3],[4,1],[3,5],[4,1],[3,1],[4,1],[3,2],[4,1],[3,1],[4,2],[3,1],[4,1],[3,1],[4,1],[3,2],[4,1],[3,1],[4,3],[3,1],[4,2],[3,1],[4,3],[3,2],[4,1],[3,3],[4,1],[3,3],[4,1],[3,1],[4,1],[3,2],[4,1],[3,1],[4,3],[3,6],[4,1],[3,3],[4,1],[3,1],[4,1],[3,2],[4,2],[3,1],[4,1],[3,1],[4,2],[3,1],[4,6],[3,1],[4,2],[3,1],[4,1],[3,1],[4,4],[3,4],[4,1],[3,6],[28,1],[3,2],[4,1],[3,1],[4,1],[3,1],[4,4],[3,4],[4,3],[3,1],[4,1],[3,1],[4,2],[3,10],[4,1],[3,11],[4,1],[3,1],[4,1],[3,1],[4,2],[3,2],[4,1],[3,1],[4,4],[3,2],[4,4],[3,1],[4,2],[3,3],[4,1],[3,1],[4,4],[3,1],[4,2],[3,1],[4,1],[3,1],[28,1],[4,2],[3,4],[4,1],[3,7],[4,1],[3,2],[4,1],[3,2],[4,1],[3,1],[4,1],[3,1],[4,1],[3,1],[4,1],[3,2],[4,2],[3,2],[4,1],[3,1],[29,1],[3,1],[4,1],[3,1],[4,2],[3,2],[4,1],[3,3],[4,1],[3,4],[4,2],[3,7],[4,2],[3,1],[4,1],[3,1],[4,4],[3,2],[4,2],[3,1],[4,2],[3,3],[4,1],[3,5],[4,3],[3,2],[4,1],[3,5],[4,1],[3,4],[4,1],[3,4],[4,2],[3,1],[4,2],[3,1],[4,2],[3,3],[4,2],[3,1],[29,1],[3,2],[4,2],[3,1],[4,1],[3,1],[4,1],[3,1],[4,1],[3,1],[4,1],[3,1],[4,2],[3,4],[4,1],[3,7],[4,1],[3,7],[4,3],[3,4],[4,1],[3,2],[4,1],[3,5],[4,6],[3,1],[4,4],[3,2],[4,2],[3,3],[4,1],[3,8],[4,1],[3,6],[4,4],[3,3],[4,2],[3,2],[4,1],[3,1],[4,2],[3,2],[4,1],[3,1],[4,2],[3,4],[4,2],[3,3],[4,1],[3,1],[4,1],[3,5]],"blocked":[2,3,6,7,24,25,26,27,9,10,18,21,28,29,30,31,32,33],"tall":{"3":{"height":1.5,"ground":0},"6":{"height":2.5,"ground":1},"7":{"height":1.75,"ground":4,"shadow":true},"9":{"height":1,"ground":8},"18":{"height":0.6,"ground":4},"21":{"height":2.5,"ground":8},"22":{"height":1.5,"ground":0},"23":{"height":1.5,"ground":0},"24":{"height":1.75,"ground":4,"shadow":true},"25":{"height":1.75,"ground":4,"shadow":true},"26":{"height":2.5,"ground":4,"shadow":true},"27":{"height":2.5,"ground":4,"shadow":true},"28":{"height":1.05,"ground":4},"29":{"height":1.25,"ground":4},"30":{"height":1.25,"ground":4},"31":{"height":0.75,"ground":4},"32":{"height":1,"ground":4},"33":{"height":1.45,"ground":4}},"decor":[{"id":16,"x":10,"y":0,"dx":1,"dy":1},{"id":16,"x":3,"y":1,"dx":3,"dy":2},{"id":16,"x":26,"y":1,"dx":-3,"dy":1},{"id":16,"x":42,"y":1,"dx":-2,"dy":1},{"id":16,"x":46,"y":1,"dx":3,"dy":0},{"id":16,"x":14,"y":2,"dx":3,"dy":1},{"id":16,"x":23,"y":2,"dx":-1,"dy":-2},{"id":16,"x":16,"y":5,"dx":0,"dy":-1},{"id":16,"x":9,"y":6,"dx":2,"dy":1},{"id":16,"x":12,"y":6,"dx":1,"dy":0},{"id":16,"x":20,"y":6,"dx":-2,"dy":-1},{"id":16,"x":47,"y":6,"dx":-3,"dy":1},{"id":16,"x":2,"y":7,"dx":-4,"dy":1},{"id":16,"x":10,"y":7,"dx":3,"dy":2},{"id":16,"x":16,"y":7,"dx":2,"dy":-1},{"id":16,"x":17,"y":7,"dx":-2,"dy":2},{"id":16,"x":20,"y":7,"dx":-2,"dy":0},{"id":16,"x":37,"y":8,"dx":1,"dy":1},{"id":16,"x":0,"y":9,"dx":-1,"dy":-1},{"id":16,"x":15,"y":9,"dx":-2,"dy":2},{"id":16,"x":27,"y":9,"dx":2,"dy":1},{"id":15,"x":38,"y":9,"dx":-3,"dy":-2},{"id":16,"x":23,"y":10,"dx":3,"dy":-1},{"id":16,"x":2,"y":11,"dx":3,"dy":2},{"id":16,"x":21,"y":11,"dx":-2,"dy":0},{"id":16,"x":31,"y":12,"dx":1,"dy":-2},{"id":16,"x":10,"y":13,"dx":-3,"dy":-1},{"id":16,"x":29,"y":13,"dx":-3,"dy":0},{"id":16,"x":36,"y":13,"dx":-3,"dy":1},{"id":16,"x":44,"y":13,"dx":2,"dy":2},{"id":16,"x":45,"y":13,"dx":-1,"dy":-2},{"id":16,"x":13,"y":15,"dx":-2,"dy":1},{"id":16,"x":30,"y":15,"dx":3,"dy":2},{"id":16,"x":33,"y":15,"dx":-2,"dy":0},{"id":16,"x":22,"y":16,"dx":2,"dy":-2},{"id":16,"x":26,"y":16,"dx":-3,"dy":0},{"id":16,"x":30,"y":16,"dx":4,"dy":1},{"id":16,"x":32,"y":16,"dx":3,"dy":0},{"id":16,"x":37,"y":16,"dx":0,"dy":-1},{"id":16,"x":12,"y":17,"dx":-4,"dy":0},{"id":16,"x":27,"y":17,"dx":-2,"dy":-1},{"id":16,"x":19,"y":18,"dx":-3,"dy":-2},{"id":16,"x":29,"y":18,"dx":3,"dy":-1},{"id":16,"x":19,"y":19,"dx":2,"dy":-1},{"id":16,"x":43,"y":19,"dx":2,"dy":-2},{"id":16,"x":44,"y":19,"dx":-2,"dy":1},{"id":16,"x":27,"y":20,"dx":0,"dy":-2},{"id":16,"x":20,"y":21,"dx":2,"dy":0},{"id":16,"x":21,"y":21,"dx":-2,"dy":-2},{"id":16,"x":24,"y":21,"dx":1,"dy":0},{"id":16,"x":1,"y":22,"dx":3,"dy":1},{"id":16,"x":16,"y":22,"dx":1,"dy":0},{"id":16,"x":19,"y":22,"dx":1,"dy":-2},{"id":16,"x":46,"y":22,"dx":0,"dy":1},{"id":16,"x":47,"y":22,"dx":0,"dy":0},{"id":16,"x":2,"y":23,"dx":-3,"dy":0},{"id":16,"x":20,"y":23,"dx":4,"dy":0},{"id":16,"x":33,"y":23,"dx":2,"dy":1},{"id":16,"x":41,"y":23,"dx":3,"dy":-1},{"id":16,"x":47,"y":23,"dx":-2,"dy":-2},{"id":15,"x":0,"y":24,"dx":-3,"dy":-1},{"id":16,"x":16,"y":24,"dx":1,"dy":2},{"id":16,"x":27,"y":24,"dx":0,"dy":0},{"id":16,"x":6,"y":25,"dx":-3,"dy":0},{"id":16,"x":38,"y":25,"dx":-2,"dy":-1},{"id":16,"x":2,"y":26,"dx":2,"dy":-1},{"id":16,"x":13,"y":26,"dx":1,"dy":1},{"id":16,"x":8,"y":28,"dx":0,"dy":2},{"id":16,"x":31,"y":28,"dx":-1,"dy":-1},{"id":16,"x":7,"y":30,"dx":2,"dy":0},{"id":16,"x":32,"y":30,"dx":2,"dy":-1},{"id":16,"x":43,"y":30,"dx":1,"dy":1},{"id":16,"x":23,"y":31,"dx":-2,"dy":2},{"id":16,"x":24,"y":31,"dx":0,"dy":1},{"id":16,"x":25,"y":31,"dx":1,"dy":1},{"id":16,"x":17,"y":32,"dx":-3,"dy":0},{"id":16,"x":21,"y":32,"dx":4,"dy":0},{"id":16,"x":14,"y":33,"dx":2,"dy":2},{"id":16,"x":16,"y":33,"dx":2,"dy":1},{"id":16,"x":22,"y":33,"dx":2,"dy":0},{"id":16,"x":14,"y":34,"dx":-1,"dy":1},{"id":16,"x":22,"y":34,"dx":2,"dy":1},{"id":16,"x":12,"y":35,"dx":-2,"dy":1},{"id":16,"x":21,"y":35,"dx":1,"dy":1},{"id":16,"x":11,"y":36,"dx":4,"dy":-1},{"id":16,"x":17,"y":37,"dx":-2,"dy":-2},{"id":16,"x":23,"y":37,"dx":-2,"dy":2},{"id":16,"x":24,"y":37,"dx":-3,"dy":-1},{"id":16,"x":34,"y":37,"dx":0,"dy":-1},{"id":16,"x":0,"y":38,"dx":1,"dy":2},{"id":16,"x":27,"y":38,"dx":1,"dy":0},{"id":16,"x":29,"y":38,"dx":-2,"dy":1},{"id":16,"x":34,"y":38,"dx":-2,"dy":-1}],"props":[]},"old_siding":{"width":48,"height":40,"tiles":[[4,10],[0,1],[4,89],[0,1],[4,42],[3,1],[4,1],[3,1],[4,43],[3,1],[4,11],[0,1],[4,24],[29,1],[4,4],[0,1],[4,4],[3,1],[4,7],[3,1],[4,19],[29,1],[4,24],[3,1],[4,11],[28,1],[4,8],[19,3],[4,16],[0,1],[4,7],[3,1],[4,20],[19,2],[4,1],[19,1],[4,44],[19,2],[4,2],[19,1],[4,8],[29,1],[4,11],[3,1],[4,1],[0,1],[4,1],[3,1],[4,1],[3,1],[4,16],[19,2],[4,3],[19,1],[4,7],[0,1],[4,7],[3,1],[4,26],[19,2],[4,4],[19,1],[4,20],[3,1],[4,20],[19,2],[4,5],[19,1],[28,1],[4,22],[3,1],[4,9],[0,1],[7,1],[4,5],[19,2],[4,6],[19,1],[4,39],[19,2],[4,7],[19,1],[4,4],[28,1],[4,10],[3,1],[4,22],[19,2],[4,8],[19,1],[4,20],[3,1],[4,16],[19,2],[4,3],[0,1],[4,5],[19,1],[4,10],[3,2],[4,2],[3,1],[4,2],[3,1],[4,18],[19,2],[4,10],[19,1],[4,12],[3,1],[4,3],[3,1],[4,18],[19,2],[4,11],[19,1],[4,1],[0,1],[4,11],[3,1],[4,20],[19,2],[4,11],[0,1],[19,1],[4,7],[3,1],[4,8],[3,1],[4,8],[0,1],[4,7],[19,2],[4,13],[19,1],[4,7],[0,1],[4,3],[1,20],[4,1],[19,2],[1,3],[4,11],[19,1],[4,7],[3,1],[4,23],[19,2],[4,2],[1,1],[4,43],[19,2],[4,2],[1,1],[4,43],[19,2],[4,2],[1,1],[4,17],[0,1],[4,25],[19,2],[4,2],[1,1],[4,5],[7,1],[4,2],[29,1],[4,7],[0,1],[4,7],[29,1],[4,3],[28,1],[4,14],[19,2],[4,2],[1,1],[4,17],[3,1],[4,13],[9,1],[4,11],[19,2],[4,2],[1,1],[4,22],[3,2],[4,19],[19,2],[4,2],[1,1],[4,43],[19,2],[4,2],[1,1],[4,25],[3,2],[4,16],[19,2],[4,2],[1,1],[4,22],[0,1],[4,19],[0,1],[19,2],[4,2],[1,2],[4,15],[3,1],[4,26],[19,2],[4,3],[1,1],[20,1],[4,41],[19,2],[4,27],[3,1],[4,11],[0,1],[4,69],[28,1],[4,29],[0,1],[4,24],[3,1],[4,6],[3,1],[4,42],[0,1],[4,21],[29,1],[4,22],[3,1],[4,2],[3,1],[4,4],[3,1],[4,40],[3,1],[4,5],[3,1],[4,40],[3,1],[4,4]],"blocked":[2,3,6,7,24,25,26,27,9,10,18,21,28,29,30,31,32,33],"tall":{"3":{"height":1.5,"ground":0},"6":{"height":2.5,"ground":1},"7":{"height":1.75,"ground":4,"shadow":true},"9":{"height":1,"ground":8},"18":{"height":0.6,"ground":4},"21":{"height":2.5,"ground":8},"22":{"height":1.5,"ground":0},"23":{"height":1.5,"ground":0},"24":{"height":1.75,"ground":4,"shadow":true},"25":{"height":1.75,"ground":4,"shadow":true},"26":{"height":2.5,"ground":4,"shadow":true},"27":{"height":2.5,"ground":4,"shadow":true},"28":{"height":1.05,"ground":4},"29":{"height":1.25,"ground":4},"30":{"height":1.25,"ground":4},"31":{"height":0.75,"ground":4},"32":{"height":1,"ground":4},"33":{"height":1.45,"ground":4}},"decor":[{"id":16,"x":5,"y":0,"dx":-1,"dy":0},{"id":16,"x":13,"y":0,"dx":-4,"dy":0},{"id":16,"x":18,"y":0,"dx":-2,"dy":-1},{"id":16,"x":24,"y":0,"dx":-2,"dy":-2},{"id":16,"x":28,"y":0,"dx":0,"dy":-1},{"id":16,"x":30,"y":0,"dx":-2,"dy":2},{"id":16,"x":13,"y":1,"dx":3,"dy":1},{"id":16,"x":17,"y":1,"dx":0,"dy":-2},{"id":16,"x":37,"y":1,"dx":1,"dy":1},{"id":16,"x":39,"y":1,"dx":1,"dy":1},{"id":16,"x":43,"y":1,"dx":-1,"dy":2},{"id":16,"x":2,"y":2,"dx":-2,"dy":-1},{"id":16,"x":8,"y":2,"dx":-3,"dy":-1},{"id":16,"x":12,"y":2,"dx":-1,"dy":-1},{"id":16,"x":22,"y":2,"dx":-3,"dy":-1},{"id":16,"x":43,"y":2,"dx":0,"dy":0},{"id":16,"x":46,"y":2,"dx":0,"dy":0},{"id":16,"x":19,"y":3,"dx":-3,"dy":1},{"id":16,"x":24,"y":3,"dx":1,"dy":-1},{"id":16,"x":17,"y":4,"dx":1,"dy":0},{"id":16,"x":19,"y":4,"dx":-1,"dy":-1},{"id":16,"x":22,"y":4,"dx":1,"dy":1},{"id":16,"x":29,"y":4,"dx":2,"dy":0},{"id":16,"x":1,"y":5,"dx":-3,"dy":0},{"id":16,"x":3,"y":5,"dx":-3,"dy":1},{"id":16,"x":41,"y":5,"dx":-2,"dy":1},{"id":16,"x":44,"y":5,"dx":-1,"dy":-1},{"id":16,"x":27,"y":6,"dx":2,"dy":0},{"id":16,"x":31,"y":6,"dx":-2,"dy":1},{"id":16,"x":17,"y":7,"dx":-2,"dy":-2},{"id":16,"x":27,"y":7,"dx":0,"dy":1},{"id":16,"x":32,"y":7,"dx":-3,"dy":1},{"id":16,"x":45,"y":7,"dx":-2,"dy":-1},{"id":16,"x":5,"y":8,"dx":-3,"dy":0},{"id":16,"x":27,"y":8,"dx":-3,"dy":-2},{"id":16,"x":28,"y":8,"dx":3,"dy":2},{"id":16,"x":8,"y":9,"dx":1,"dy":0},{"id":16,"x":14,"y":9,"dx":-4,"dy":-1},{"id":16,"x":31,"y":9,"dx":-1,"dy":1},{"id":16,"x":38,"y":9,"dx":0,"dy":1},{"id":16,"x":0,"y":10,"dx":3,"dy":-2},{"id":16,"x":2,"y":10,"dx":-4,"dy":0},{"id":16,"x":4,"y":10,"dx":0,"dy":0},{"id":16,"x":10,"y":10,"dx":-3,"dy":-1},{"id":16,"x":21,"y":10,"dx":-4,"dy":-1},{"id":16,"x":40,"y":10,"dx":-3,"dy":-2},{"id":16,"x":42,"y":10,"dx":3,"dy":1},{"id":16,"x":9,"y":11,"dx":-2,"dy":2},{"id":16,"x":12,"y":11,"dx":0,"dy":1},{"id":16,"x":13,"y":11,"dx":3,"dy":2},{"id":16,"x":31,"y":11,"dx":-3,"dy":-1},{"id":16,"x":41,"y":11,"dx":-4,"dy":-2},{"id":16,"x":42,"y":11,"dx":3,"dy":-1},{"id":16,"x":43,"y":11,"dx":-1,"dy":1},{"id":16,"x":1,"y":12,"dx":1,"dy":2},{"id":16,"x":4,"y":12,"dx":0,"dy":2},{"id":16,"x":7,"y":12,"dx":-1,"dy":2},{"id":16,"x":25,"y":12,"dx":1,"dy":0},{"id":16,"x":13,"y":13,"dx":-2,"dy":1},{"id":16,"x":16,"y":13,"dx":1,"dy":1},{"id":16,"x":20,"y":13,"dx":2,"dy":2},{"id":16,"x":4,"y":14,"dx":-3,"dy":-1},{"id":16,"x":44,"y":14,"dx":0,"dy":-2},{"id":16,"x":3,"y":15,"dx":3,"dy":1},{"id":16,"x":19,"y":15,"dx":-1,"dy":0},{"id":16,"x":20,"y":15,"dx":0,"dy":-2},{"id":16,"x":21,"y":15,"dx":-1,"dy":0},{"id":16,"x":28,"y":15,"dx":-2,"dy":-2},{"id":16,"x":35,"y":15,"dx":-1,"dy":1},{"id":16,"x":38,"y":15,"dx":0,"dy":2},{"id":16,"x":46,"y":15,"dx":-4,"dy":-1},{"id":16,"x":2,"y":16,"dx":4,"dy":1},{"id":16,"x":11,"y":16,"dx":3,"dy":0},{"id":16,"x":21,"y":16,"dx":3,"dy":0},{"id":16,"x":7,"y":17,"dx":-1,"dy":2},{"id":16,"x":31,"y":17,"dx":1,"dy":1},{"id":16,"x":32,"y":17,"dx":-4,"dy":1},{"id":16,"x":42,"y":17,"dx":3,"dy":0},{"id":16,"x":27,"y":18,"dx":-3,"dy":-1},{"id":16,"x":39,"y":18,"dx":-1,"dy":0},{"id":16,"x":40,"y":18,"dx":1,"dy":2},{"id":16,"x":41,"y":18,"dx":-3,"dy":-2},{"id":16,"x":21,"y":19,"dx":2,"dy":1},{"id":16,"x":30,"y":19,"dx":1,"dy":-1},{"id":16,"x":0,"y":21,"dx":3,"dy":-1},{"id":16,"x":35,"y":21,"dx":-2,"dy":1},{"id":16,"x":37,"y":21,"dx":3,"dy":-2},{"id":16,"x":29,"y":22,"dx":-3,"dy":-2},{"id":16,"x":38,"y":22,"dx":4,"dy":1},{"id":16,"x":42,"y":22,"dx":-1,"dy":1},{"id":16,"x":14,"y":23,"dx":-1,"dy":-2},{"id":16,"x":33,"y":23,"dx":0,"dy":2},{"id":16,"x":36,"y":23,"dx":-2,"dy":-1},{"id":16,"x":42,"y":23,"dx":-1,"dy":-2},{"id":16,"x":10,"y":24,"dx":-1,"dy":-1},{"id":16,"x":21,"y":24,"dx":1,"dy":1},{"id":16,"x":30,"y":24,"dx":3,"dy":1},{"id":16,"x":31,"y":24,"dx":2,"dy":2},{"id":16,"x":34,"y":24,"dx":-2,"dy":-1},{"id":16,"x":46,"y":24,"dx":3,"dy":1},{"id":16,"x":1,"y":25,"dx":0,"dy":0},{"id":16,"x":4,"y":25,"dx":2,"dy":2},{"id":16,"x":20,"y":25,"dx":-1,"dy":-2},{"id":16,"x":33,"y":25,"dx":-2,"dy":2},{"id":16,"x":1,"y":26,"dx":3,"dy":2},{"id":16,"x":25,"y":26,"dx":-3,"dy":1},{"id":16,"x":0,"y":27,"dx":3,"dy":1},{"id":16,"x":3,"y":27,"dx":4,"dy":0},{"id":16,"x":6,"y":27,"dx":3,"dy":0},{"id":16,"x":11,"y":27,"dx":-2,"dy":-1},{"id":16,"x":14,"y":27,"dx":2,"dy":-2},{"id":16,"x":20,"y":27,"dx":-4,"dy":-2},{"id":16,"x":25,"y":27,"dx":3,"dy":-2},{"id":16,"x":29,"y":27,"dx":1,"dy":0},{"id":16,"x":36,"y":27,"dx":-1,"dy":-2},{"id":16,"x":39,"y":27,"dx":-4,"dy":-1},{"id":16,"x":46,"y":27,"dx":-3,"dy":-1},{"id":16,"x":14,"y":28,"dx":-4,"dy":2},{"id":16,"x":29,"y":28,"dx":-1,"dy":1},{"id":16,"x":38,"y":28,"dx":0,"dy":2},{"id":16,"x":40,"y":28,"dx":0,"dy":1},{"id":16,"x":42,"y":29,"dx":0,"dy":-1},{"id":17,"x":1,"y":30,"dx":1,"dy":1},{"id":16,"x":4,"y":30,"dx":2,"dy":1},{"id":16,"x":7,"y":30,"dx":-1,"dy":1},{"id":15,"x":21,"y":30,"dx":0,"dy":1},{"id":16,"x":30,"y":30,"dx":-2,"dy":-1},{"id":16,"x":41,"y":30,"dx":-1,"dy":0},{"id":16,"x":8,"y":31,"dx":-1,"dy":1},{"id":16,"x":15,"y":31,"dx":-2,"dy":0},{"id":16,"x":36,"y":31,"dx":4,"dy":-1},{"id":16,"x":12,"y":32,"dx":2,"dy":2},{"id":16,"x":15,"y":32,"dx":3,"dy":-1},{"id":16,"x":17,"y":32,"dx":-3,"dy":-1},{"id":16,"x":24,"y":32,"dx":2,"dy":1},{"id":16,"x":32,"y":32,"dx":3,"dy":0},{"id":16,"x":36,"y":32,"dx":0,"dy":-2},{"id":16,"x":43,"y":32,"dx":2,"dy":1},{"id":16,"x":4,"y":33,"dx":-1,"dy":1},{"id":16,"x":16,"y":33,"dx":0,"dy":0},{"id":16,"x":26,"y":33,"dx":2,"dy":1},{"id":16,"x":43,"y":33,"dx":4,"dy":0},{"id":16,"x":17,"y":34,"dx":-2,"dy":-2},{"id":16,"x":22,"y":34,"dx":-4,"dy":1},{"id":16,"x":26,"y":34,"dx":2,"dy":1},{"id":16,"x":35,"y":34,"dx":2,"dy":2},{"id":16,"x":2,"y":35,"dx":3,"dy":2},{"id":16,"x":26,"y":35,"dx":3,"dy":1},{"id":16,"x":28,"y":35,"dx":-1,"dy":1},{"id":16,"x":43,"y":35,"dx":-3,"dy":1},{"id":16,"x":10,"y":36,"dx":0,"dy":1},{"id":16,"x":11,"y":36,"dx":2,"dy":1},{"id":16,"x":27,"y":36,"dx":-1,"dy":1},{"id":16,"x":28,"y":36,"dx":-4,"dy":2},{"id":16,"x":31,"y":36,"dx":-1,"dy":1},{"id":16,"x":33,"y":36,"dx":3,"dy":-2},{"id":16,"x":42,"y":36,"dx":-1,"dy":-1},{"id":16,"x":10,"y":37,"dx":-4,"dy":-1},{"id":16,"x":18,"y":37,"dx":2,"dy":1},{"id":16,"x":31,"y":37,"dx":-4,"dy":-1},{"id":16,"x":8,"y":38,"dx":3,"dy":1},{"id":16,"x":11,"y":38,"dx":-1,"dy":1},{"id":16,"x":14,"y":38,"dx":-2,"dy":0},{"id":16,"x":27,"y":38,"dx":-1,"dy":-1}],"props":[]},"machine_hall":{"width":26,"height":22,"tiles":[[9,27],[8,11],[21,2],[8,11],[9,2],[8,24],[9,2],[8,12],[19,1],[8,11],[9,2],[8,12],[19,1],[8,11],[9,2],[8,12],[19,1],[8,11],[9,2],[8,12],[19,1],[8,11],[9,2],[8,12],[19,1],[8,11],[9,2],[8,12],[19,1],[8,11],[9,2],[8,12],[19,1],[8,11],[9,2],[8,12],[19,1],[8,11],[9,2],[8,12],[19,1],[8,11],[9,2],[8,12],[19,1],[8,11],[9,2],[8,12],[19,1],[8,11],[9,2],[8,12],[19,1],[8,11],[9,2],[8,12],[19,1],[8,11],[9,2],[8,12],[19,1],[8,11],[9,2],[8,12],[19,1],[8,11],[9,2],[8,12],[19,1],[8,11],[9,2],[8,12],[19,1],[8,11],[9,2],[8,24],[9,14],[8,1],[9,12]],"blocked":[2,3,6,7,24,25,26,27,9,10,18,21,28,29,30,31,32,33],"tall":{"3":{"height":1.5,"ground":0},"6":{"height":2.5,"ground":1},"7":{"height":1.75,"ground":4,"shadow":true},"9":{"height":1,"ground":8},"18":{"height":0.6,"ground":4},"21":{"height":2.5,"ground":8},"22":{"height":1.5,"ground":0},"23":{"height":1.5,"ground":0},"24":{"height":1.75,"ground":4,"shadow":true},"25":{"height":1.75,"ground":4,"shadow":true},"26":{"height":2.5,"ground":4,"shadow":true},"27":{"height":2.5,"ground":4,"shadow":true},"28":{"height":1.05,"ground":4},"29":{"height":1.25,"ground":4},"30":{"height":1.25,"ground":4},"31":{"height":0.75,"ground":4},"32":{"height":1,"ground":4},"33":{"height":1.45,"ground":4}},"decor":[],"props":[]},"house_elder":{"width":11,"height":9,"tiles":[[9,12],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,6],[8,1],[9,5]],"blocked":[2,3,6,7,24,25,26,27,9,10,18,21,28,29,30,31,32,33],"tall":{"3":{"height":1.5,"ground":0},"6":{"height":2.5,"ground":1},"7":{"height":1.75,"ground":4,"shadow":true},"9":{"height":1,"ground":8},"18":{"height":0.6,"ground":4},"21":{"height":2.5,"ground":8},"22":{"height":1.5,"ground":0},"23":{"height":1.5,"ground":0},"24":{"height":1.75,"ground":4,"shadow":true},"25":{"height":1.75,"ground":4,"shadow":true},"26":{"height":2.5,"ground":4,"shadow":true},"27":{"height":2.5,"ground":4,"shadow":true},"28":{"height":1.05,"ground":4},"29":{"height":1.25,"ground":4},"30":{"height":1.25,"ground":4},"31":{"height":0.75,"ground":4},"32":{"height":1,"ground":4},"33":{"height":1.45,"ground":4}},"decor":[],"props":[]},"shop":{"width":11,"height":9,"tiles":[[9,12],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,6],[8,1],[9,5]],"blocked":[2,3,6,7,24,25,26,27,9,10,18,21,28,29,30,31,32,33],"tall":{"3":{"height":1.5,"ground":0},"6":{"height":2.5,"ground":1},"7":{"height":1.75,"ground":4,"shadow":true},"9":{"height":1,"ground":8},"18":{"height":0.6,"ground":4},"21":{"height":2.5,"ground":8},"22":{"height":1.5,"ground":0},"23":{"height":1.5,"ground":0},"24":{"height":1.75,"ground":4,"shadow":true},"25":{"height":1.75,"ground":4,"shadow":true},"26":{"height":2.5,"ground":4,"shadow":true},"27":{"height":2.5,"ground":4,"shadow":true},"28":{"height":1.05,"ground":4},"29":{"height":1.25,"ground":4},"30":{"height":1.25,"ground":4},"31":{"height":0.75,"ground":4},"32":{"height":1,"ground":4},"33":{"height":1.45,"ground":4}},"decor":[],"props":[]}} \ No newline at end of file diff --git a/v2/apps/game/src/main.ts b/v2/apps/game/src/main.ts new file mode 100644 index 0000000..a1fcf5c --- /dev/null +++ b/v2/apps/game/src/main.ts @@ -0,0 +1,344 @@ +/** + * «Пепельные луга» на v2 (трек 15 «карты и области»): мир областей из + * раскладки v1 (maps.json → buildAreaWorld), герой-манекен, переходы + * step/click с фейдом и стек возврата, агентный мост для гейтов. + * Позиции v1 (спавны/переходы, в тайлах) конвертируются на границе: + * tileCenter (tile*UNIT + 1.5). Референс — демка v1. + */ +import { + VoxelWorld, VoxelRenderer, meshChunk, ISO_YAW, + mannequin, walkClip, Character, GameLoop, screenMoveToWorld, +} from '@rpg/engine'; +import * as THREE from 'three'; +import { AREA_LAYOUTS, UNIT, GROUND_Y, buildAreaWorld, tileCenter } from './areas'; +import { areaOf, type AreaDef, type TransitionDef, type Tile } from './data/areas'; + +// Слоты палитры сцены: арт-библия v1 (1..10) + тайлы областей (11..17) +const PALETTE = { + colors: { + 1: '#4a5340', // G1 — базовый луг + 2: '#343b2c', // G0 — тень травы (нижний слой / проплешина) + 3: '#626c4c', // G2 — трава, свет (пятна) + 4: '#5c3b2e', // R1 — стволы/крыша тень + 5: '#7d8662', // G2H — крона, свет + 6: '#5b5b66', // P4 — камень, стены + 7: '#94949e', // P6 — свет камня + 8: '#82503a', // R2 — светлая ржавчина (крыша) + 9: '#d99a32', // B1 — цветок (жизнь!) + 10: '#44454e', // M1 — металл + 11: '#2f2f38', // P2 — пепел + 12: '#3e3e48', // P3 — пепел, светлые пятна + 13: '#3b5e6b', // W1 — вода + 14: '#568590', // W2 — вода, блики + 15: '#5a4a3c', // T0 — пол интерьеров + 16: '#77634f', // T1 — тропа + 17: '#61626c', // M2 — рельсы + }, +}; + +// ---- Рендер: мир области (пересоздаётся на переходе), меш на чанк ---- +const canvas = document.getElementById('scene') as HTMLCanvasElement; +const renderer = new VoxelRenderer(canvas, { target: [40, 2, 60], viewSize: 17 }); + +let world = new VoxelWorld(16); +const chunkMeshes = new Map(); + +function remeshDirty(): void { + for (const key of world.takeDirty()) { + const [cx, cz] = key.split(',').map(Number); + const old = chunkMeshes.get(key); + if (old) { + renderer.scene.remove(old); + old.geometry.dispose(); + } + const mesh = meshChunk(world, cx, cz, PALETTE); + if (mesh.triangleCount > 0) chunkMeshes.set(key, renderer.addMesh(mesh)); + else chunkMeshes.delete(key); + } +} + +/** Снять меши старой области (при переходе мир пересоздаётся целиком). */ +function disposeMeshes(): void { + for (const m of chunkMeshes.values()) { + renderer.scene.remove(m); + m.geometry.dispose(); + } + chunkMeshes.clear(); +} + +// ---- Герой: манекен + walk-цикл (как в демке) ---- +const { model: MAN, rig: MAN_RIG } = mannequin(); +const WALK = walkClip({ duration: 0.8, legSwing: 0.55, armSwing: 0.4, bob: 0.25, sway: 0.15 }); +const BODY_SIZE: [number, number, number] = [3, 10, 3]; + +const hero = new Character(MAN_RIG, MAN, WALK, [40, 60], { + size: BODY_SIZE, + speed: 4, + strideLength: 3.2, +}); +const heroCloud = renderer.addVoxelCloud(hero.visual()); + +// ---- Области: текущая, стек возврата, смена области ---- +let currentArea: AreaDef = areaOf('meadows'); +let layout = AREA_LAYOUTS.meadows!; +const returnStack: { area: string; entry: Tile }[] = []; + +/** Смена области: новый мир + мешы, герой на входном тайле. */ +function setArea(id: string, entry: Tile): void { + currentArea = areaOf(id); + layout = AREA_LAYOUTS[currentArea.id]!; + disposeMeshes(); + world = buildAreaWorld(layout); + remeshDirty(); + const c = tileCenter(entry.x, entry.y); + hero.body.pos = [c.x, GROUND_Y, c.z]; + hero.body.vel = [0, 0, 0]; + heroCloud.update(hero.visual()); + prevTile = { ...entry }; // входной тайл — не «только что вошёл» (без триггера) + nearClick = null; // подсказка старой области не должна пережить переход + showHint(''); + renderer.setTarget([Math.round(c.x), GROUND_Y, Math.round(c.z)]); +} + +// ---- Фейд и переходы: out → смена области → in (полный = 2×duration) ---- +const fadeEl = document.getElementById('fade') as HTMLDivElement; +const hintEl = document.getElementById('hint') as HTMLDivElement; + +interface FadeState { + active: boolean; + t: number; + dur: number; + phase: 'out' | 'in'; + /** Смена области — в середине перехода (за чёрным кадром). */ + apply: (() => void) | null; +} +const fade: FadeState = { active: false, t: 0, dur: 0.4, phase: 'out', apply: null }; + +let hintText = ''; +/** Ближайший click-переход (для «E» и подсказки). */ +let nearClick: TransitionDef | null = null; +/** Тайл героя с прошлого тика (origin возврата + «только что вошёл»). */ +let prevTile: Tile | null = null; + +function showHint(text: string): void { + hintText = text; + hintEl.hidden = text === ''; + hintEl.textContent = text; +} + +/** Начать переход (фейд out, середина — смена области). */ +function startTransition(tr: TransitionDef, origin: Tile): void { + if (fade.active) return; // параллельные переходы глотаются (как в v1) + fade.active = true; + fade.t = 0; + fade.dur = tr.duration ?? 0.4; + fade.phase = 'out'; + const fromArea = currentArea.id; + fade.apply = () => { + if (tr.target === 'return') { + const back = returnStack.pop(); + if (back) setArea(back.area, back.entry); + } else { + // origin — откуда вошли (шаг на триггер = прошлый тайл, дверь = стояли) + returnStack.push({ area: fromArea, entry: origin }); + setArea(tr.target.area, tr.target.entry); + } + }; + showHint(''); +} + +// ---- Ввод: клавиатура (экранные оси) + виртуальный стик агента ---- +const heldKeys = new Set(); +const KEYMAP: Record = { + KeyW: [0, -1], KeyS: [0, 1], KeyA: [-1, 0], KeyD: [1, 0], + ArrowUp: [0, -1], ArrowDown: [0, 1], ArrowLeft: [-1, 0], ArrowRight: [1, 0], +}; +let agentInput: readonly [number, number] | null = null; +let followHero = true; + +window.addEventListener('keydown', (e) => { + if (KEYMAP[e.code]) { + heldKeys.add(e.code); + e.preventDefault(); + } + if (e.code === 'KeyE' && nearClick) { + e.preventDefault(); + startTransition(nearClick, heroTile()); + } +}); +window.addEventListener('keyup', (e) => { + heldKeys.delete(e.code); +}); + +function keyboardMove(): [number, number] { + let x = 0, y = 0; + for (const code of heldKeys) { + const m = KEYMAP[code]!; + x += m[0]; + y += m[1]; + } + return [x, y]; +} + +/** Тайл героя (в тайлах v1) — триггеры и снапшоты моста. */ +function heroTile(): Tile { + const b = hero.body.pos; + return { x: Math.floor(b[0] / UNIT), y: Math.floor(b[2] / UNIT) }; +} + +/** Один шаг логики: фейд → ввод → герой → камера → переходы → подсказка. */ +function tick(dt: number): void { + if (fade.active) { + fade.t += dt; + if (fade.phase === 'out') { + fadeEl.style.opacity = String(Math.min(1, fade.t / fade.dur)); + if (fade.t >= fade.dur) { + fade.apply?.(); + fade.phase = 'in'; + fade.t = 0; + } + } else { + fadeEl.style.opacity = String(Math.max(0, 1 - fade.t / fade.dur)); + if (fade.t >= fade.dur) { + fade.active = false; + fadeEl.style.opacity = '0'; + } + } + return; // во время перехода мир замер + } + const move = agentInput ?? keyboardMove(); + hero.setMove(screenMoveToWorld(move, ISO_YAW)); + hero.step(world, dt); + heroCloud.update(hero.visual()); + if (followHero) { + const b = hero.body.pos; + renderer.setTarget([Math.round(b[0]), GROUND_Y, Math.round(b[2])]); + } + // step-переход: только что вошёл на тайл-триггер + const tile = heroTile(); + const justEntered = prevTile !== null && (tile.x !== prevTile.x || tile.y !== prevTile.y); + if (justEntered) { + const tr = currentArea.transitions.find( + (t) => (t.trigger ?? 'step') === 'step' && t.tile.x === tile.x && t.tile.y === tile.y, + ); + if (tr) { + startTransition(tr, prevTile!); + return; + } + } + prevTile = tile; + // click-переход рядом: подсказка «E — …» + nearClick = + currentArea.transitions.find( + (t) => + t.trigger === 'click' && + Math.max(Math.abs(t.tile.x - tile.x), Math.abs(t.tile.y - tile.y)) <= 1, + ) ?? null; + showHint(nearClick ? `E — ${nearClick.label ?? 'перейти'}` : ''); +} + +// ---- Цикл: фиксированный шаг 60 Гц, ручной режим для гейтов ---- +const loop = new GameLoop(60, { + update: (dt) => tick(dt), + render: () => { + remeshDirty(); + renderer.render(); + }, +}); +setArea(currentArea.id, currentArea.spawn); // старт — спавн лугов +loop.start(); + +// ---- Агентный мост (DEV): гейты областей ---- +const agent = { + /** Ручной/автоматический режим цикла. */ + setManual(on: boolean): void { + loop.setManual(on); + }, + /** Ручные шаги логики + рендер (детерминизм гейта). */ + stepTicks(n: number): number { + return loop.stepTick(n, true); + }, + /** Снапшот героя для гейта. */ + heroInfo() { + const b = hero.body.pos; + return { + area: currentArea.id, + pos: [...b], vel: [...hero.body.vel], grounded: hero.body.grounded, + phase: hero.phase, yaw: hero.yaw, + tile: heroTile(), + camTarget: renderer.currentTarget(), + }; + }, + /** Телепорт героя (гейт: слежение камеры, непроходимость). */ + heroTeleport(x: number, z: number): void { + hero.body.pos = [x, GROUND_Y, z]; + hero.body.vel = [0, 0, 0]; + heroCloud.update(hero.visual()); + prevTile = heroTile(); // телепорт — не «вход на тайл» + renderer.setTarget([Math.round(x), GROUND_Y, Math.round(z)]); + }, + /** Виртуальный стик агента (экранные оси −1..1); null — клавиатура. */ + setInput(x: number | null, z: number | null): void { + agentInput = x === null || z === null ? null : [x, z]; + }, + /** Слежение камеры за героем (off — кадр гейта фиксируется). */ + setFollow(on: boolean): void { + followHero = on; + if (!on) { + const c = tileCenter(currentArea.spawn.x, currentArea.spawn.y); + renderer.setTarget([Math.round(c.x), GROUND_Y, Math.round(c.z)]); + } + }, + /** Яркость солнца (утро/день/вечер — как в демке). */ + setSun(k: number): void { + renderer.setSun(k); + }, + /** Снапшот области: раскладка, переходы, фейд, стек возврата, меши. */ + areaInfo() { + return { + id: currentArea.id, + name: currentArea.name, + width: layout.width, + height: layout.height, + spawn: currentArea.spawn, + heroTile: heroTile(), + transitions: currentArea.transitions.map((t) => ({ + tile: t.tile, + label: t.label ?? null, + trigger: t.trigger ?? 'step', + target: t.target === 'return' ? 'return' : t.target.area, + duration: t.duration ?? 0.4, + })), + fade: { active: fade.active, phase: fade.phase }, + returns: returnStack.length, + chunks: world.chunkCount, + meshes: chunkMeshes.size, + }; + }, + /** Прямой переход области (гейт: скриншоты и переходы без ходьбы). */ + gotoArea(id: string, entry?: Tile): void { + returnStack.length = 0; + fade.active = false; + fadeEl.style.opacity = '0'; + const def = areaOf(id); + setArea(id, entry ?? def.spawn); + }, + /** Сплошная ли колонка НАД землёй (вода/объект) — гейт blocked. */ + solidAt(x: number, z: number): boolean { + const fx = Math.floor(x), fz = Math.floor(z); + for (let y = GROUND_Y; y <= GROUND_Y + 1; y++) { + if (world.get(fx, y, fz) !== 0) return true; + } + return false; + }, + /** Нажать «E» агентом: активировать click-переход. */ + interact(): void { + if (nearClick && !fade.active) startTransition(nearClick, heroTile()); + }, + /** Снапшот взаимодействия: ближайшая дверь и подсказка. */ + interactInfo() { + return { label: nearClick?.label ?? null, hint: hintText }; + }, +}; +(window as unknown as Record).__agent = agent; +(window as unknown as Record).__beacon = true; \ No newline at end of file diff --git a/v2/apps/game/tools/checks/areas.mjs b/v2/apps/game/tools/checks/areas.mjs new file mode 100644 index 0000000..19687c8 --- /dev/null +++ b/v2/apps/game/tools/checks/areas.mjs @@ -0,0 +1,252 @@ +/** + * Скриншот-гейт областей v2 (трек 15 «карты и области»): конвертация v1 → + * мир детерминирована (пересбор той же области — тот же кадр), спавн лугов, + * blocked-тайлы (вода/дерево сплошные, тропа нет; тело зажимается гранью), + * step-переход входом на тайл-триггер (фейд out→swap→in), возврат 'return' + * по стеку, click-переход через «E» с подсказкой. Скриншоты: /tmp/v2_areas_*.png. + * + * Запуск: node apps/game/tools/checks/areas.mjs [--port 5314] + */ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { launchBrowser, startDevServer, openGame, parseArgs } from '@rpg/engine/tools/agent-lib.mjs'; +import { decodePng } from '@rpg/engine/tools/png.mjs'; + +const args = parseArgs(process.argv.slice(2)); +const port = Number(args.port ?? 5314); + +const server = startDevServer({ port, cwd: fileURLToPath(new URL('../../', import.meta.url)) }); +const report = { name: 'areas', ok: false, checks: [] }; +const check = (title, ok, info = null) => { + report.checks.push({ title, ok, info }); + console.error(`${ok ? '✓' : '✗'} ${title}${info ? ' — ' + JSON.stringify(info) : ''}`); +}; + +/** Тиков на полный переход (out+in) с запасом. */ +const PASS = 70; +/** Тиков на проход через один тайл с запасом (4 вокс/с, тайл = 3 вокс). */ +const CROSS = 120; + +// ---- Раскладка v1 (maps.json — чистые данные, движок не импортируется) ---- +const maps = JSON.parse(readFileSync(new URL('../../src/data/maps.json', import.meta.url), 'utf8')); + +/** RLE → плоский массив тайлов (как в areas.ts). */ +function tilesFlat(id) { + const m = maps[id]; + const out = []; + for (const [tid, run] of m.tiles) for (let k = 0; k < run; k++) out.push(tid); + return out; +} + +/** Первый тайл области с данным id → {x, y} (в тайлах v1). */ +function findTile(id, tileId) { + const m = maps[id]; + const flat = tilesFlat(id); + for (let i = 0; i < flat.length; i++) { + if (flat[i] === tileId) return { x: i % m.width, y: Math.floor(i / m.width) }; + } + throw new Error(`${id}: тайл ${tileId} не найден`); +} + +/** Воксельный центр тайла (как tileCenter в игре: tile*UNIT + 1.5). */ +const center = (t) => ({ x: t.x * 3 + 1.5, z: t.y * 3 + 1.5 }); + +function countDiff(a, b) { + let n = 0; + for (let i = 0; i < a.length; i += 4) { + if (Math.abs(a[i] - b[i]) > 8 || Math.abs(a[i + 1] - b[i + 1]) > 8 || Math.abs(a[i + 2] - b[i + 2]) > 8) n++; + } + return n; +} +function isBlack(rgba) { + let bright = 0, n = 0; + for (let i = 0; i < rgba.length; i += 4) { + n++; + if (rgba[i] + rgba[i + 1] + rgba[i + 2] > 30) bright++; + } + return bright / n < 0.01; +} +/** Число различимых цветов (квантование 5 бит/канал). */ +function colorCount(rgba) { + const seen = new Set(); + for (let i = 0; i < rgba.length; i += 4) { + seen.add((rgba[i] >> 3) + (rgba[i + 1] >> 3) * 32 + (rgba[i + 2] >> 3) * 1024); + } + return seen.size; +} + +try { + await server.waitReady; + const browser = await launchBrowser(); + try { + const client = await openGame(browser, { url: `http://localhost:${port}/` }); + const errs = client.errors(); + check('нет pageerror при загрузке', errs.length === 0, errs.slice(0, 3)); + + await client.call('setManual', true); + await client.call('stepTicks', 10); + + // Старт: луга, герой на спавне, меши собраны + let ai = await client.call('areaInfo'); + check('старт: луга, герой на спавне (16,28), меши собраны', + ai.id === 'meadows' && ai.heroTile.x === 16 && ai.heroTile.y === 28 && ai.meshes > 0, + { id: ai.id, tile: ai.heroTile, meshes: ai.meshes }); + + // Детерминизм сборки: пересбор той же области — тот же кадр + const spawnC = center({ x: 16, y: 28 }); + await client.call('setFollow', false); + const shotA = await client.screenshot('/tmp/v2_areas_meadows.png'); + await client.call('gotoArea', 'meadows'); + await client.call('stepTicks', 5); + const shotA2 = await client.screenshot('/tmp/v2_areas_meadows2.png'); + const detDiff = countDiff(decodePng(readFileSync(shotA)).rgba, decodePng(readFileSync(shotA2)).rgba); + check('пересборка области детерминирована (кадры равны)', detDiff === 0, { detDiff }); + + // Чистая мировая ось — комбинированный стик: при yaw π/4 экранное + // (1,1) = +x мира (hero-гейт демки), (1,-1) = −z, (-1,1) = +z + await client.call('heroTeleport', spawnC.x, spawnC.z); + await client.call('setInput', 1, 1); + await client.call('stepTicks', 10); + const hiProbe = await client.call('heroInfo'); + await client.call('setInput', null, null); + check('стик (1,1) — чистый +x мира (без дрейфа по z)', + hiProbe.pos[0] - spawnC.x > 0.3 && Math.abs(hiProbe.pos[2] - spawnC.z) < 0.2, + { dx: Math.round((hiProbe.pos[0] - spawnC.x) * 100) / 100, + dz: Math.round((hiProbe.pos[2] - spawnC.z) * 100) / 100 }); + + // blocked: дерево и вода сплошные (хоть одна под-колонка тайла), + // тропа — ни одной сплошной под-колонки + const solidTile = async (c) => { + let n = 0; + for (const dx of [-1, 0, 1]) for (const dz of [-1, 0, 1]) { + if (await client.call('solidAt', c.x + dx, c.z + dz)) n++; + } + return n; + }; + const tree = center(findTile('meadows', 3)); + const water = center(findTile('meadows', 2)); + const path = center(findTile('meadows', 1)); + const sTree = await solidTile(tree); + const sWater = await solidTile(water); + const sPath = await solidTile(path); + check('blocked: дерево/вода сплошные, тропа проходима', sTree > 0 && sWater === 9 && sPath === 0, + { tree: sTree, water: sWater, path: sPath }); + + // Тело зажимается гранью воды (вода — сплошной слой на уровне ног) + await client.call('heroTeleport', water.x - 3.6, water.z); + await client.call('setInput', 1, 1); + await client.call('stepTicks', CROSS); + await client.call('setInput', null, null); + const hiStop = await client.call('heroInfo'); + const gap = water.x - hiStop.pos[0]; // центр тела до центра воды + check('тело героя останавливается у грани воды', gap > 2.6 && gap < 4.5, + { gap: Math.round(gap * 10) / 10 }); + + // Скриншоты областей: различаются и не пустые + const areas = ['meadows', 'ponds', 'zvenets', 'rust_grove', 'old_siding', 'machine_hall', 'house_elder']; + const shots = {}; + let minDiff = Infinity; + let allColorful = true; + for (const id of areas) { + await client.call('gotoArea', id); + await client.call('stepTicks', 5); + const p = `/tmp/v2_areas_${id}.png`; + const img = decodePng(readFileSync(await client.screenshot(p))); + shots[id] = img.rgba; + if (isBlack(img.rgba) || colorCount(img.rgba) < 8) allColorful = false; + } + for (let i = 1; i < areas.length; i++) { + minDiff = Math.min(minDiff, countDiff(shots[areas[i - 1]], shots[areas[i]])); + } + check('скриншоты 7 областей: не пустые, различаются', allColorful && minDiff > 5000, + { minDiff, allColorful }); + + // step-переход: телепорт у триггера (54,28) → идти на него → Звенец + await client.call('gotoArea', 'meadows'); + await client.call('stepTicks', 5); + const trig = center({ x: 54, y: 28 }); + await client.call('heroTeleport', trig.x - 3, trig.z); + await client.call('setInput', 1, 1); + await client.call('stepTicks', CROSS); + await client.call('setInput', null, null); + await client.call('stepTicks', PASS); + ai = await client.call('areaInfo'); + check('step-переход луга→Звенец (фейд, возврат запомнен)', + ai.id === 'zvenets' && ai.returns === 1 && !ai.fade.active, { id: ai.id, returns: ai.returns }); + + // Вторая пара step-переходов: луга (2,2) → пруды → обратно в луга + await client.call('gotoArea', 'meadows'); + await client.call('stepTicks', 5); + const pTrig = center({ x: 2, y: 2 }); + await client.call('heroTeleport', pTrig.x + 3, pTrig.z); + await client.call('setInput', -1, -1); + await client.call('stepTicks', CROSS); + await client.call('setInput', null, null); + await client.call('stepTicks', PASS); + ai = await client.call('areaInfo'); + check('второй step-переход: луга→пруды', ai.id === 'ponds', ai.id); + const pBack = center({ x: 3, y: 2 }); + await client.call('heroTeleport', pBack.x, pBack.z); + await client.call('setInput', -1, -1); + await client.call('stepTicks', CROSS); + await client.call('setInput', null, null); + await client.call('stepTicks', PASS); + ai = await client.call('areaInfo'); + check('обратный переход пруды→луга', ai.id === 'meadows', ai.id); + + // return-переход: люк подземелья → зал → люк обратно + await client.call('gotoArea', 'old_siding'); + await client.call('stepTicks', 5); + const hatch = center({ x: 28, y: 31 }); + await client.call('heroTeleport', hatch.x - 3, hatch.z); + await client.call('setInput', 1, 1); + await client.call('stepTicks', CROSS); + await client.call('setInput', null, null); + await client.call('stepTicks', PASS); + ai = await client.call('areaInfo'); + const wentDown = ai.id === 'machine_hall' && ai.returns === 1; + check('переход в подземелье через люк (step, 0.6 с)', wentDown, { id: ai.id, returns: ai.returns }); + if (wentDown) { + // запад/восток триггера (13,21) — стены; идём с севера (13,20), +z + const hall = center({ x: 13, y: 20 }); + await client.call('heroTeleport', hall.x, hall.z); + await client.call('setInput', -1, 1); + await client.call('stepTicks', CROSS); + await client.call('setInput', null, null); + await client.call('stepTicks', PASS); + ai = await client.call('areaInfo'); + check('возврат из подземелья по стеку (на тайле входа)', + ai.id === 'old_siding' && ai.returns === 0, { id: ai.id, returns: ai.returns }); + } + + // click-переход: дверь дома Ирвина в Звенце — подсказка и «E» + await client.call('gotoArea', 'zvenets'); + await client.call('stepTicks', 5); + const door = center({ x: 13, y: 12 }); + await client.call('heroTeleport', door.x + 3, door.z); + await client.call('stepTicks', 5); + let ii = await client.call('interactInfo'); + check('подсказка у двери: «E — Дом Ирвина»', ii.label === 'Дом Ирвина' && ii.hint.includes('E'), ii); + await client.call('heroTeleport', door.x + 30, door.z); // ~10 тайлов в стороне + await client.call('stepTicks', 5); + ii = await client.call('interactInfo'); + check('вдали от двери подсказки нет', ii.label === null && ii.hint === '', ii); + await client.call('heroTeleport', door.x + 3, door.z); + await client.call('stepTicks', 5); + await client.call('interact'); + await client.call('stepTicks', PASS); + ai = await client.call('areaInfo'); + check('click-переход «E» → дом Ирвина', ai.id === 'house_elder', ai.id); + const inside = await client.screenshot('/tmp/v2_areas_inside.png'); + check('кадр интерьера рисуется (не пустой)', colorCount(decodePng(readFileSync(inside)).rgba) >= 8, {}); + } finally { + await browser.close(); + } +} catch (e) { + report.error = String(e && e.stack ? e.stack : e); + console.error(report.error); +} finally { + server.stop(); +} +report.ok = report.checks.length > 0 && report.checks.every((c) => c.ok); +console.log(JSON.stringify(report)); \ No newline at end of file diff --git a/v2/apps/game/tools/convertMaps.mjs b/v2/apps/game/tools/convertMaps.mjs new file mode 100644 index 0000000..8fe5b14 --- /dev/null +++ b/v2/apps/game/tools/convertMaps.mjs @@ -0,0 +1,60 @@ +/** + * Конвертация карт v1 (assets/maps/*.map — RLE) в раскладку областей v2: + * src/data/maps.json — по области {width, height, tiles (RLE), decor, props, + * blocked, tall}. Раскладка тайлов v1 переносится как есть (позиции NPC, + * переходов и квестов v1 в тайлах — сходятся с раскладкой), воксельная + * генерация делает из неё мир (src/areas.ts). Прогон: node tools/convertMaps.mjs + * (от корня v2/apps/game; путь к v1 — по умолчанию ../../v1/apps/game). + */ +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; + +const ROOT = fileURLToPath(new URL('..', import.meta.url)); +const V1_MAPS = process.argv[2] ?? path.resolve(ROOT, '../../../v1/apps/game/assets/maps'); +const OUT = path.resolve(ROOT, 'src/data/maps.json'); + +/** Области v1 (порядок = порядок загрузки в игре). */ +const AREAS = ['meadows', 'ponds', 'zvenets', 'rust_grove', 'old_siding', 'machine_hall', 'house_elder', 'shop']; + +/** RLE-пары → плоский массив (распаковка на месте — parseMap v1 не импортируется). */ +function rleDecode(pairs) { + const out = []; + for (const [id, run] of pairs) { + for (let k = 0; k < run; k++) out.push(id); + } + return out; +} + +/** Плоский массив → RLE-пары (на случай raw-карты v1). */ +function rleEncode(flat) { + const out = []; + for (let i = 0; i < flat.length; i++) { + if (i > 0 && flat[i] === flat[i - 1]) out[out.length - 1][1]++; + else out.push([flat[i], 1]); + } + return out; +} + +const maps = {}; +for (const id of AREAS) { + const raw = JSON.parse(readFileSync(path.join(V1_MAPS, `${id}.map`), 'utf8')); + if (raw.format !== 'rpg-map' || raw.version !== 1) throw new Error(`${id}: не карта rpg-map`); + maps[id] = { + width: raw.width, + height: raw.height, + // RLE как есть (сериализация маленькая); генератор распакует при сборке + tiles: raw.encoding === 'rle' ? raw.tiles : rleEncode(raw.tiles), + // blocked/tall/decor/props — по сути копия (высоты в юнитах уже) + blocked: raw.blocked, + tall: raw.tall ?? {}, + decor: raw.decor ?? [], + props: raw.props ?? [], + }; +} + +mkdirSync(path.dirname(OUT), { recursive: true }); +writeFileSync(OUT, JSON.stringify(maps)); +const sizes = Object.entries(maps).map(([k, m]) => `${k} ${m.width}x${m.height}`); +console.log('ok →', path.relative(ROOT, OUT)); +console.log(sizes.join(', ')); \ No newline at end of file diff --git a/v2/apps/game/tsconfig.json b/v2/apps/game/tsconfig.json new file mode 100644 index 0000000..9e6cc82 --- /dev/null +++ b/v2/apps/game/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["vite/client"] + }, + "include": ["src", "tools"] +} \ No newline at end of file diff --git a/v2/apps/game/vite.config.ts b/v2/apps/game/vite.config.ts new file mode 100644 index 0000000..63060fa --- /dev/null +++ b/v2/apps/game/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite'; +import { fileURLToPath } from 'node:url'; + +export default defineConfig({ + resolve: { + alias: [ + // только regex-алиас (грабля v1): строковый перехватывает и под-пути + { find: /^@rpg\/engine$/, replacement: fileURLToPath(new URL('../../packages/engine/src/index.ts', import.meta.url)) }, + ], + }, +}); \ No newline at end of file diff --git a/v2/package-lock.json b/v2/package-lock.json index c2dae37..b95403f 100644 --- a/v2/package-lock.json +++ b/v2/package-lock.json @@ -531,6 +531,525 @@ } } }, + "apps/game": { + "name": "@rpg/game", + "version": "0.0.1", + "dependencies": { + "@rpg/engine": "*", + "three": "^0.186.0" + }, + "devDependencies": { + "vite": "^6.0.0" + } + }, + "apps/game/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "apps/game/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "apps/game/node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/@dimforge/rapier3d-compat": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", @@ -1433,6 +1952,10 @@ "resolved": "packages/engine", "link": true }, + "node_modules/@rpg/game": { + "resolved": "apps/game", + "link": true + }, "node_modules/@tweenjs/tween.js": { "version": "23.1.3", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", diff --git a/v2/package.json b/v2/package.json index 01ef204..9063ffb 100644 --- a/v2/package.json +++ b/v2/package.json @@ -11,7 +11,7 @@ "scripts": { "test": "vitest run", "test:watch": "vitest", - "typecheck": "tsc --noEmit -p packages/engine && tsc --noEmit -p apps/demo", + "typecheck": "tsc --noEmit -p packages/engine && tsc --noEmit -p apps/demo && tsc --noEmit -p apps/game", "check:fast": "npm run typecheck && npm test" } } \ No newline at end of file