Top.Mail.Ru
Академия киберспорта XP
Подготовка драфта
Тренировки по Dota 2 · 2026

Профессиональный
киберспорт начинается здесь

Маршрут
Семь ходов до главной сцены — с XP Academy

Мы разложили путь дотера на семь ходов — как партию на доске. Каждая расстановка фигур слева это отдельная стадия карьеры: от первой рейтинговой катки до главной сцены The International. Кликайте по этапам и смотрите, как меняется позиция.

7этапов обучения
9 000MMR на пике
$3M+призовой фонд The International
Потяните, чтобы осмотреть доску

Каждая фигура на доске — это реальная анимированная 3D-модель героя Dota 2. Свет Радианта против Тьмы Дайра; наведите курсор на фигуру, чтобы увидеть её героя и роль.

Свет РадиантаRadiant · светлые клетки
KingCarry — main damage
Gyrocopter
QueenMid — tempo core
Lina
RooksOfflane — frontline
Kunkka · Dark Seer
BishopsRoamer — mobility
Ringmaster · Clockwerk
KnightsSupport — control
Abaddon · Snapfire
PawnsLane — creeps
Radiant Creeps
Тьма ДайраDire · тёмные клетки
KingCarry — main damage
Drow Ranger
QueenMid — tempo core
Queen of Pain
RooksOfflane — frontline
Tidehunter · Pit Lord
BishopsRoamer — mobility
Pudge · Sand King
KnightsSupport — control
Disruptor · Hoodwink
PawnsLane — creeps
Dire Creeps
Сцена

Почему Dota 2 —
это профессия

За «дотой» стоит выстроенная индустрия: сезонный календарь DPC, региональные дивизионы, Мейджоры и главный турнир года — The International. Это одна из самых прозрачных карьерных лестниц в киберспорте: видно, куда расти и за счёт чего.

Индустрия вокруг игры

Dota 2 живёт не отдельными матчами, а целой системой: Valve держит круглогодичный календарь DPC — Open Qualifiers, Division II, Division I, Мейджоры и The International. За топовыми составами стоят клубы с менеджерами, аналитиками, буткемпами и многолетними контрактами.

Дорога от первой рейтинговой катки до главной сцены — это в среднем 3–5 лет планомерной работы, а не везения. Именно такой маршрут мы и раскладываем по этапам.

10M+активных игроков ежемесячно
200+профессиональных команд
5уровней лестницы DPC

DPC и The International

Региональные лиги Division II и I ведут к Мейджорам, а те — к The International, турниру с многомиллионным призовым, где сходятся лучшие составы всех регионов.

Кем можно стать

Не только игрок: тренер, аналитик-драфтер, менеджер состава, скаут, кастер и стример. Вокруг Dota 2 крутятся десятки ролей — и почти в каждую есть вход.

Деньги в Dota 2

На T2 игрок обычно получает $1–5K в месяц плюс долю призовых. На T1 — от $10K, с бонусами за результат и доходом от стримов и медиа.

Из чего растёт скилл

Механика и ласт-хит, чтение карты, драфт, тимфайт-исполнение и психология — мы прокачиваем это не по наитию, а по структуре, с разбором каждой катки.

Форматы

Два пути через лес

Драфт под себя: спокойный заход в дисциплину или жёсткая прокачка под турнирный стандарт. Треки различаются нагрузкой, длиной сессий и тем, куда вы хотите прийти — к уверенному Immortal или к контракту в составе DPC.

старт в лайне

Light

Разбираем фундамент Dota 2 без давления: ласт-хит и денай, обзор карты, базовые связки и понимание ролей 1–5.

  • Группа до 90 минут, спокойный темп
  • Онлайн или офлайн — как удобнее
  • Механика фарма, варды и тайминги рун
  • Идеально для входа в рейтинг с нуля
дорога на сцену

Hard

Турнирная подготовка от и до: драфт по патчу, исполнение тимфайтов, скримы и разбор реплеев — путь от Open Qualifiers к Division I.

  • Интенсив до 180 минут в составе
  • Только офлайн в буткемпе Академии XP
  • Командная стратегия, драфт и ротации
  • Работа с психологом и тренером по войсу
  • Скрим-блоки, разбор реплеев и контрольные матчи
Цены

Программы обучения

Подберите тариф под свой темп. В каждый входит работа с действующим тренером и практика на киберарене XP — от первой катки до турнирных скримов.

Individual
4 990 ₽/ занятие
  • Разовая сессия один на один
  • Персональный разбор реплея
  • Постановка на роль 1–5
  • Точечная работа над ошибками
  • План прокачки на месяц вперёд
Записаться
Online
11 990 ₽/ месяц
  • 12 групповых сессий по сети
  • 90 минут практики за катку
  • Фидбек тренера после игр
  • 24 часа игрового баланса
  • Домашние задания по патчу
Записаться
Pro
30 990 ₽/ месяц
  • 12 интенсивов в буткемпе
  • 150 – 180 минут и скрим-блоки
  • Спортивный психолог в команде
  • Тренер по войс-коммуникации
  • Подготовка к Open Qualifiers
Записаться
База команды

Наш буткемп

Тренируемся в Москве: игровые места с мониторами 240 Гц и низким пингом, отдельная драфт-комната для теории, зона разбора реплеев и место, где можно выдохнуть между скримами.

Академия киберспорта XP

АдресМосква, БЦ «Стратос»
ул. Николы Теслы, 1с1 · Сколково
ГрафикЕжедневно · 10:00 — 23:00
Контакты+7 933 645 45 45
hello@xpacademy.ru
Построить маршрут
/* =================================================================== 7 STAGES — career path content + a real historical chess game (Morphy vs Duke of Brunswick & Count Isouard, Paris 1858 — "The Opera Game") used purely as the board's piece arrangement at each stage. =================================================================== */ const STAGES = [ { n:'01', name:'Ranked MMR', tag:'База', period:'3–6 месяцев', progress:14, move:'Стартовая позиция', meta:[{lbl:'Платформа',val:'Dota 2 · Ranked MM'},{lbl:'Формат',val:'Соло / пати ММ'},{lbl:'Доход игрока',val:'—'},{lbl:'Цель',val:'База карт-сенса и механики'}], text:'Формируем основную структуру игровых навыков в рейтинговых матчах — карта, ласт-хиты, ротации, базовый драфт. Для каждого навыка разработан корпус специализированных упражнений, отвечающий за разные этапы развития студента. В подготовке участвуют только профессиональные игроки, прошедшие внутреннее обучение Академии.', board:[ {sq:'a1',role:'rook',side:'radiant'},{sq:'b1',role:'knight',side:'radiant'},{sq:'c1',role:'bishop',side:'radiant'},{sq:'d1',role:'queen',side:'radiant'},{sq:'e1',role:'king',side:'radiant'},{sq:'f1',role:'bishop',side:'radiant'},{sq:'g1',role:'knight',side:'radiant'},{sq:'h1',role:'rook',side:'radiant'}, {sq:'a2',role:'pawn',side:'radiant'},{sq:'b2',role:'pawn',side:'radiant'},{sq:'c2',role:'pawn',side:'radiant'},{sq:'d2',role:'pawn',side:'radiant'},{sq:'e2',role:'pawn',side:'radiant'},{sq:'f2',role:'pawn',side:'radiant'},{sq:'g2',role:'pawn',side:'radiant'},{sq:'h2',role:'pawn',side:'radiant'}, {sq:'a8',role:'rook',side:'dire'},{sq:'b8',role:'knight',side:'dire'},{sq:'c8',role:'bishop',side:'dire'},{sq:'d8',role:'queen',side:'dire'},{sq:'e8',role:'king',side:'dire'},{sq:'f8',role:'bishop',side:'dire'},{sq:'g8',role:'knight',side:'dire'},{sq:'h8',role:'rook',side:'dire'}, {sq:'a7',role:'pawn',side:'dire'},{sq:'b7',role:'pawn',side:'dire'},{sq:'c7',role:'pawn',side:'dire'},{sq:'d7',role:'pawn',side:'dire'},{sq:'e7',role:'pawn',side:'dire'},{sq:'f7',role:'pawn',side:'dire'},{sq:'g7',role:'pawn',side:'dire'},{sq:'h7',role:'pawn',side:'dire'}, ], }, { n:'02', name:'Immortal Rank', tag:'Рейтинг', period:'4–6 месяцев+', progress:28, move:'Ход 5', meta:[{lbl:'Система',val:'MMR · Leaderboards'},{lbl:'Скаутинг',val:'От 7 000 MMR'},{lbl:'Доход игрока',val:'—'},{lbl:'Цель',val:'Попасть в поле зрения организаций'}], text:'Dota 2 использует единую систему рейтинга MMR с публичными лидербордами. Мы помогаем студентам поднимать MMR до уровня Immortal — отметки, на которой игроков начинают замечать скауты профессиональных организаций.', board:[ {sq:'a1',role:'rook',side:'radiant'},{sq:'b1',role:'knight',side:'radiant'},{sq:'c1',role:'bishop',side:'radiant'},{sq:'e1',role:'king',side:'radiant'},{sq:'f1',role:'bishop',side:'radiant'},{sq:'h1',role:'rook',side:'radiant'},{sq:'f3',role:'queen',side:'radiant'}, {sq:'a2',role:'pawn',side:'radiant'},{sq:'b2',role:'pawn',side:'radiant'},{sq:'c2',role:'pawn',side:'radiant'},{sq:'e4',role:'pawn',side:'radiant'},{sq:'f2',role:'pawn',side:'radiant'},{sq:'g2',role:'pawn',side:'radiant'},{sq:'h2',role:'pawn',side:'radiant'}, {sq:'a8',role:'rook',side:'dire'},{sq:'b8',role:'knight',side:'dire'},{sq:'d8',role:'queen',side:'dire'},{sq:'e8',role:'king',side:'dire'},{sq:'f8',role:'bishop',side:'dire'},{sq:'g8',role:'knight',side:'dire'},{sq:'h8',role:'rook',side:'dire'}, {sq:'a7',role:'pawn',side:'dire'},{sq:'b7',role:'pawn',side:'dire'},{sq:'c7',role:'pawn',side:'dire'},{sq:'e5',role:'pawn',side:'dire'},{sq:'f7',role:'pawn',side:'dire'},{sq:'g7',role:'pawn',side:'dire'},{sq:'h7',role:'pawn',side:'dire'}, ], }, { n:'03', name:'Open Qualifiers', tag:'Первый турнир', period:'Старт карьеры', progress:42, move:'Ход 7', meta:[{lbl:'Требования',val:'Отсутствуют'},{lbl:'Кто там играет?',val:'Beginners'},{lbl:'Призовой фонд',val:'—'},{lbl:'Цель',val:'Турнирный опыт'}], text:'Open Qualifiers DPC — первый официальный турнирный опыт без отбора по рейтингу. Турнир считается надёжным способом старта профессиональной карьеры в Dota 2 и открывает доступ к более закрытым квалификациям.', board:[ {sq:'a1',role:'rook',side:'radiant'},{sq:'b1',role:'knight',side:'radiant'},{sq:'c1',role:'bishop',side:'radiant'},{sq:'e1',role:'king',side:'radiant'},{sq:'c4',role:'bishop',side:'radiant'},{sq:'h1',role:'rook',side:'radiant'},{sq:'b3',role:'queen',side:'radiant'}, {sq:'a2',role:'pawn',side:'radiant'},{sq:'b2',role:'pawn',side:'radiant'},{sq:'c2',role:'pawn',side:'radiant'},{sq:'e4',role:'pawn',side:'radiant'},{sq:'f2',role:'pawn',side:'radiant'},{sq:'g2',role:'pawn',side:'radiant'},{sq:'h2',role:'pawn',side:'radiant'}, {sq:'a8',role:'rook',side:'dire'},{sq:'b8',role:'knight',side:'dire'},{sq:'e8',role:'king',side:'dire'},{sq:'f8',role:'bishop',side:'dire'},{sq:'h8',role:'rook',side:'dire'},{sq:'f6',role:'knight',side:'dire'},{sq:'e7',role:'queen',side:'dire'}, {sq:'a7',role:'pawn',side:'dire'},{sq:'b7',role:'pawn',side:'dire'},{sq:'c7',role:'pawn',side:'dire'},{sq:'e5',role:'pawn',side:'dire'},{sq:'f7',role:'pawn',side:'dire'},{sq:'g7',role:'pawn',side:'dire'},{sq:'h7',role:'pawn',side:'dire'}, ], }, { n:'04', name:'DPC Division II', tag:'Контракты', period:'Профессиональный старт', progress:56, move:'Ход 9', meta:[{lbl:'Требования',val:'Open Qualifiers Winner'},{lbl:'Призовой фонд',val:'$2 000'},{lbl:'Доход игрока',val:'30 000 – 80 000 ₽ / мес'},{lbl:'Контракт',val:'Юниорский / стажёрский'}], text:'Победа в Open Qualifiers открывает путь во второй дивизион DPC. Здесь молодые игроки впервые подписывают юниорские контракты с организациями: фиксированная зарплата, доля призовых и бонусы за результат.', board:[ {sq:'a1',role:'rook',side:'radiant'},{sq:'c3',role:'knight',side:'radiant'},{sq:'e1',role:'king',side:'radiant'},{sq:'c4',role:'bishop',side:'radiant'},{sq:'g5',role:'bishop',side:'radiant'},{sq:'h1',role:'rook',side:'radiant'},{sq:'b3',role:'queen',side:'radiant'}, {sq:'a2',role:'pawn',side:'radiant'},{sq:'b2',role:'pawn',side:'radiant'},{sq:'c2',role:'pawn',side:'radiant'},{sq:'e4',role:'pawn',side:'radiant'},{sq:'f2',role:'pawn',side:'radiant'},{sq:'g2',role:'pawn',side:'radiant'},{sq:'h2',role:'pawn',side:'radiant'}, {sq:'a8',role:'rook',side:'dire'},{sq:'b8',role:'knight',side:'dire'},{sq:'e8',role:'king',side:'dire'},{sq:'f8',role:'bishop',side:'dire'},{sq:'h8',role:'rook',side:'dire'},{sq:'f6',role:'knight',side:'dire'},{sq:'e7',role:'queen',side:'dire'}, {sq:'a7',role:'pawn',side:'dire'},{sq:'b5',role:'pawn',side:'dire'},{sq:'c6',role:'pawn',side:'dire'},{sq:'e5',role:'pawn',side:'dire'},{sq:'f7',role:'pawn',side:'dire'},{sq:'g7',role:'pawn',side:'dire'},{sq:'h7',role:'pawn',side:'dire'}, ], }, { n:'05', name:'DPC Division I', tag:'T3-сцена', period:'Основной дивизион', progress:70, move:'Ход 11', meta:[{lbl:'Требования',val:'Division II Winner'},{lbl:'Призовой фонд',val:'$15 000'},{lbl:'Доход игрока',val:'100 000 – 250 000 ₽ / мес'},{lbl:'Контракт',val:'Div. I · основной состав'}], text:'Division I — основной дивизион региональной лиги DPC и важный этап профессионального развития. Командам этого уровня доступны очки PvP и приглашения на Major, а зарплата основного игрока заметно растёт.', board:[ {sq:'a1',role:'rook',side:'radiant'},{sq:'e1',role:'king',side:'radiant'},{sq:'b5',role:'bishop',side:'radiant'},{sq:'g5',role:'bishop',side:'radiant'},{sq:'h1',role:'rook',side:'radiant'},{sq:'b3',role:'queen',side:'radiant'}, {sq:'a2',role:'pawn',side:'radiant'},{sq:'b2',role:'pawn',side:'radiant'},{sq:'c2',role:'pawn',side:'radiant'},{sq:'e4',role:'pawn',side:'radiant'},{sq:'f2',role:'pawn',side:'radiant'},{sq:'g2',role:'pawn',side:'radiant'},{sq:'h2',role:'pawn',side:'radiant'}, {sq:'a8',role:'rook',side:'dire'},{sq:'e8',role:'king',side:'dire'},{sq:'f8',role:'bishop',side:'dire'},{sq:'h8',role:'rook',side:'dire'},{sq:'f6',role:'knight',side:'dire'},{sq:'d7',role:'knight',side:'dire'},{sq:'e7',role:'queen',side:'dire'}, {sq:'a7',role:'pawn',side:'dire'},{sq:'e5',role:'pawn',side:'dire'},{sq:'f7',role:'pawn',side:'dire'},{sq:'g7',role:'pawn',side:'dire'},{sq:'h7',role:'pawn',side:'dire'}, ], }, { n:'06', name:'Major', tag:'T2-сцена', period:'Первый pro-уровень', progress:86, move:'Ход 15', meta:[{lbl:'Требования',val:'Division I — топ команды'},{lbl:'Призовой фонд',val:'$500 000'},{lbl:'Доход игрока',val:'$1 000 – $5 000 / мес'},{lbl:'Контракт',val:'T2 · долгосрочный'}], text:'Major — первый полноценный международный турнир календаря DPC. Игроки получают контракт T2-уровня с гарантированной долей призовых, медиа-доходом от стриминга и очками квалификации на The International.', board:[ {sq:'c1',role:'king',side:'radiant'},{sq:'d1',role:'rook',side:'radiant'},{sq:'g5',role:'bishop',side:'radiant'},{sq:'b3',role:'queen',side:'radiant'}, {sq:'a2',role:'pawn',side:'radiant'},{sq:'b2',role:'pawn',side:'radiant'},{sq:'c2',role:'pawn',side:'radiant'},{sq:'e4',role:'pawn',side:'radiant'},{sq:'f2',role:'pawn',side:'radiant'},{sq:'g2',role:'pawn',side:'radiant'},{sq:'h2',role:'pawn',side:'radiant'}, {sq:'e8',role:'king',side:'dire'},{sq:'f8',role:'bishop',side:'dire'},{sq:'h8',role:'rook',side:'dire'},{sq:'d7',role:'knight',side:'dire'},{sq:'e6',role:'queen',side:'dire'}, {sq:'a7',role:'pawn',side:'dire'},{sq:'e5',role:'pawn',side:'dire'},{sq:'f7',role:'pawn',side:'dire'},{sq:'g7',role:'pawn',side:'dire'},{sq:'h7',role:'pawn',side:'dire'}, ], }, { n:'07', name:'The International', tag:'Высшая лига', period:'Вершина', progress:100, move:'Ход 17 · мат', meta:[{lbl:'Требования',val:'TI Qualifier Winner'},{lbl:'Призовой фонд',val:'$3 000 000+'},{lbl:'Доход игрока',val:'от $10 000 / мес'},{lbl:'Контракт',val:'T1 · с медиа-долей'}], text:'The International — главный турнир мировой сцены Dota 2 с участием топовых команд всех регионов. Призовой фонд формируется Valve и составляет несколько миллионов долларов, а контракты T1-уровня включают медиа-долю и спонсорские бонусы.', board:[ {sq:'c1',role:'king',side:'radiant'},{sq:'d8',role:'rook',side:'radiant'},{sq:'g5',role:'bishop',side:'radiant'}, {sq:'a2',role:'pawn',side:'radiant'},{sq:'b2',role:'pawn',side:'radiant'},{sq:'c2',role:'pawn',side:'radiant'},{sq:'e4',role:'pawn',side:'radiant'},{sq:'f2',role:'pawn',side:'radiant'},{sq:'g2',role:'pawn',side:'radiant'},{sq:'h2',role:'pawn',side:'radiant'}, {sq:'e8',role:'king',side:'dire'},{sq:'f8',role:'bishop',side:'dire'},{sq:'h8',role:'rook',side:'dire'},{sq:'b8',role:'knight',side:'dire'},{sq:'e6',role:'queen',side:'dire'}, {sq:'a7',role:'pawn',side:'dire'},{sq:'e5',role:'pawn',side:'dire'},{sq:'f7',role:'pawn',side:'dire'},{sq:'g7',role:'pawn',side:'dire'},{sq:'h7',role:'pawn',side:'dire'}, ], }, ]; const GAME_CREDIT = 'Реальная партия · Морфи — Брансуик и Изуар, Париж 1858';
/* =================================================================== THREE.JS SCENE — procedural board, no heavy external assets =================================================================== */ const stage = document.getElementById('board-stage'); const canvas = document.getElementById('board-canvas'); const canvasWrap = document.getElementById('board-canvas-wrap'); const hintText = document.getElementById('board-hint-text'); const scene = new THREE.Scene(); scene.fog = new THREE.FogExp2(0x0a0816, 0.0042); const camera = new THREE.PerspectiveCamera(40, 1, 0.1, 600); const HOME_CAM = new THREE.Vector3(0, 92, 112); const HOME_LOOK = new THREE.Vector3(0, 4, -6); camera.position.copy(HOME_CAM); camera.lookAt(HOME_LOOK); const renderer = new THREE.WebGLRenderer({ canvas, antialias:true, alpha:true, powerPreference:'high-performance' }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, DPR_CAP)); renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.28; function resize(){ const w = stage.clientWidth, h = stage.clientHeight; camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h, false); } resize(); window.addEventListener('resize', resize); const ambient = new THREE.AmbientLight(0x9a9486, 0.9); scene.add(ambient); const hemi = new THREE.HemisphereLight(0xd8e0e8, 0x3a3326, 0.8); // sky / earthy ground scene.add(hemi); const keyLight = new THREE.DirectionalLight(0xfff2da, 1.6); // warm key (sunlight) keyLight.position.set(-30, 70, 50); scene.add(keyLight); const fillLight = new THREE.DirectionalLight(0xeaf2ff, 0.8); // cool frontal fill fillLight.position.set(20, 40, 90); scene.add(fillLight); const rimLight = new THREE.DirectionalLight(0x8fd8b0, 0.6); rimLight.position.set(40, 30, -40); scene.add(rimLight); const world = new THREE.Group(); scene.add(world); /* ---- BOARD ---- */ const TILE = 9; function squareToXZ(sq){ const file = sq.charCodeAt(0) - 97; const rank = parseInt(sq[1], 10); return { x:(file-3.5)*TILE, z:(4.5-rank)*TILE }; } const BOARD = TILE*8; /* ---- grassy board surface: 8×8 checker of light/dark grass with cells marked. The Radiant half (front) reads a touch warmer-green, the Dire half cooler. ---- */ function makeBoardTex(){ const S=1536, c=document.createElement('canvas'); c.width=c.height=S; const x=c.getContext('2d'); const cell=S/8; for(let f=0;f<8;f++)for(let r=0;r<8;r++){ const light=(f+r)%2===0; const dire = r < 4; // top half = Dire let base = light ? (dire?'#2f4636':'#35512f') : (dire?'#22342a':'#283f23'); x.fillStyle=base; x.fillRect(f*cell,r*cell,cell,cell); for(let i=0;i<260;i++){ const px=f*cell+Math.random()*cell, py=r*cell+Math.random()*cell; const g=55+Math.random()*75|0; x.fillStyle='rgba('+(28+Math.random()*28|0)+','+g+','+(30+Math.random()*22|0)+','+(0.06+Math.random()*0.20)+')'; x.fillRect(px,py,1.5,2+Math.random()*3); } } // engraved cell lines x.strokeStyle='rgba(150,138,104,0.32)'; x.lineWidth=5; for(let i=0;i<=8;i++){ x.beginPath();x.moveTo(i*cell,0);x.lineTo(i*cell,S);x.stroke(); x.beginPath();x.moveTo(0,i*cell);x.lineTo(S,i*cell);x.stroke(); } const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace; t.anisotropy=8; return t; } const boardPlane = new THREE.Mesh(new THREE.PlaneGeometry(BOARD, BOARD), new THREE.MeshStandardMaterial({ map:makeBoardTex(), roughness:0.95, metalness:0.0 })); boardPlane.rotation.x = -Math.PI/2; boardPlane.position.y = 0; world.add(boardPlane); /* ---- monumental relief: raised, beveled stone ribs along every cell edge ---- */ const ribMat = new THREE.MeshStandardMaterial({ color:0x6a6149, roughness:0.78, metalness:0.06, emissive:0x161108, emissiveIntensity:0.18 }); const ribCapMat = new THREE.MeshStandardMaterial({ color:0x837954, roughness:0.7, metalness:0.08, emissive:0x1a1409, emissiveIntensity:0.2 }); const ribs = new THREE.Group(); for(let i=0;i<=8;i++){ const o=(i-4)*TILE; const bx=new THREE.Mesh(new THREE.BoxGeometry(BOARD+1.4,0.95,0.85), ribMat); bx.position.set(0,0.32,o); ribs.add(bx); const bxc=new THREE.Mesh(new THREE.BoxGeometry(BOARD+1.4,0.35,0.45), ribCapMat); bxc.position.set(0,0.78,o); ribs.add(bxc); const bz=new THREE.Mesh(new THREE.BoxGeometry(0.85,0.95,BOARD+1.4), ribMat); bz.position.set(o,0.32,0); ribs.add(bz); const bzc=new THREE.Mesh(new THREE.BoxGeometry(0.45,0.35,BOARD+1.4), ribCapMat); bzc.position.set(o,0.78,0); ribs.add(bzc); } world.add(ribs); /* ---- outer raised frame REMOVED — edge pieces (rooks/knights) were clipping into it ---- */ const fw = 2.0; /* ---- base plinth under everything (flush, low, so nothing clips) ---- */ const plate = new THREE.Mesh(new THREE.BoxGeometry(BOARD+fw*2, 2.2, BOARD+fw*2), new THREE.MeshStandardMaterial({ color:0x2a2519, roughness:0.95, metalness:0.0 })); plate.position.y = -1.2; world.add(plate); /* ---- river removed ---- */ const riverTex = null;
/* =================================================================== MATERIALS (per side) + PAWN fallback geometry Heroes are rendered as team-tinted "statuettes" (Radiant mint / Dire violet) — like a real collectible Dota chess set. =================================================================== */ function sideMat(side){ return side==='radiant' ? new THREE.MeshStandardMaterial({ color:0x9fe3bd, metalness:0.34, roughness:0.40, emissive:0x163a2a, emissiveIntensity:0.42 }) : new THREE.MeshStandardMaterial({ color:0xb6a8f2, metalness:0.34, roughness:0.40, emissive:0x241a55, emissiveIntensity:0.42 }); } const MAT = { radiant: sideMat('radiant'), dire: sideMat('dire') }; /* target on-board height — heroes are all equalised to one height so the set looks consistent; pawns (creeps) are a bit shorter */ const TARGET_H = { pawn:6.4, rook:9.0, bishop:9.0, knight:9.0, queen:9.0, king:9.0 }; /* Radiant (front, +Z) and Dire (back, −Z) face EACH OTHER across the river. VRF models face +Z by default, so Dire keeps 0 and Radiant turns 180°. */ const FACE_YAW = { radiant: Math.PI, dire: 0 }; /* Per-stage static "poses". No animation data exists in the source FBX (zero channels), so each of the 7 stages applies a distinct upper-body attitude via the spine (twist + lean) plus a small whole-body turn. twist = torso turn, lean = torso forward lean, yaw = whole-body turn. */ const STAGE_POSE = [ { twist: 0.00, lean: 0.02, yaw: 0.00 }, { twist: 0.17, lean: 0.05, yaw: 0.06 }, { twist:-0.15, lean: 0.01, yaw:-0.05 }, { twist: 0.21, lean: 0.06, yaw: 0.05 }, { twist:-0.19, lean: 0.03, yaw:-0.06 }, { twist: 0.13, lean: 0.05, yaw: 0.08 }, { twist: 0.07, lean: 0.07, yaw: 0.03 }, ]; /* simple procedural pawn (used until real creep models are added later) */ const PAWN_GEO = { base:new THREE.CylinderGeometry(1.25,1.55,1.8,14), head:new THREE.SphereGeometry(1.2,16,12) }; function buildPawn(side){ const g=new THREE.Group(); const m=MAT[side]; const b=new THREE.Mesh(PAWN_GEO.base,m); b.position.y=0.9; g.add(b); const h=new THREE.Mesh(PAWN_GEO.head,m); h.position.y=2.0; g.add(h); return g; }
/* measure the SKINNED bounding box (precise=true applies bone transforms) so the skeleton offset of VRF models is taken into account — otherwise the rest-pose box is wrong and the model floats off its tile. */ function skinnedBox(root){ root.updateMatrixWorld(true); root.traverse(o=>{ if(o.isSkinnedMesh && o.skeleton) o.skeleton.update(); }); let box = new THREE.Box3().setFromObject(root, true); if(!isFinite(box.min.x) || !isFinite(box.max.x)) box = new THREE.Box3().setFromObject(root); return box; } /* normalise: scale + ground the WHOLE assembled model so every hero ends up the same effective height and sits flat on its tile, then add the faction ring. */ function normalise(root, side, role, name, measure){ const pivot = new THREE.Group(); pivot.add(root); let m = measure; if(!m){ const box = skinnedBox(root); const s = box.getSize(new THREE.Vector3()); const c = box.getCenter(new THREE.Vector3()); m = { groundY:box.min.y, height:Math.max(0.001,s.y), cx:c.x, cz:c.z }; } const scale = (TARGET_H[role] / m.height) * (SCALE_FIX[name] || 1); root.scale.setScalar(scale); root.position.set(-m.cx*scale, -m.groundY*scale + (GROUND_FIX[name]||0), -m.cz*scale); // facing is handled per-stage on the OUTER group; keep the model at native forward (+Z) here pivot.add(factionRing(side)); return pivot; } /* build a texture straight from a base64 jpeg */ function texFromB64(b64){ const t = new THREE.TextureLoader().load('data:image/jpeg;base64,'+b64); t.colorSpace = THREE.SRGBColorSpace; t.flipY = false; t.wrapS = t.wrapT = THREE.RepeatWrapping; t.anisotropy = 4; return t; } function itemMat(texB64){ const tex = texB64 ? texFromB64(texB64) : null; return tex ? new THREE.MeshStandardMaterial({ map:tex, emissive:0xffffff, emissiveMap:tex, emissiveIntensity:0.2, metalness:0.0, roughness:0.6 }) : new THREE.MeshStandardMaterial({ color:0x9aa0a6, metalness:0.1, roughness:0.6 }); } /* Assemble body + items onto ONE unified skeleton (the method that worked for this roster). VRF prunes bones the bare body mesh doesn't use, so we add every item bone (with its real parent chain + bind inverse) into the body's bone hierarchy, build a fresh skeleton, and re-bind ALL meshes to it. One skeleton + one mixer = the standard reliable skinning path; the body's idle clip then drives everything. */ function assembleHero(bodyRoot, items){ const K = s => (s||'').toLowerCase(); // VRF exports body vs item bone names with inconsistent case (hand_R vs hand_r) → match case-insensitively const boneByName = {}; bodyRoot.traverse(o=>{ if(o.isBone) boneByName[K(o.name)]=o; }); let bodySkel=null; bodyRoot.traverse(o=>{ if(o.isSkinnedMesh && !bodySkel) bodySkel=o.skeleton; }); if(!bodySkel) return null; const invByName = {}; bodySkel.bones.forEach((b,i)=>{ invByName[K(b.name)]=bodySkel.boneInverses[i]; }); const meshJobs = []; bodyRoot.traverse(o=>{ if(o.isSkinnedMesh) meshJobs.push({mesh:o,tex:null}); }); items.forEach(({scene,tex})=>{ let itemSkel=null; scene.traverse(o=>{ if(o.isSkinnedMesh && !itemSkel) itemSkel=o.skeleton; }); const itemInv={}; if(itemSkel) itemSkel.bones.forEach((b,i)=>{ itemInv[K(b.name)]=itemSkel.boneInverses[i]; }); const itemBones=[]; scene.traverse(o=>{ if(o.isBone) itemBones.push(o); }); function ensure(ib){ const key=K(ib.name); if(boneByName[key]) return boneByName[key]; const parent = (ib.parent && ib.parent.isBone) ? ensure(ib.parent) : bodyRoot; const nb=new THREE.Bone(); nb.name=ib.name; nb.position.copy(ib.position); nb.quaternion.copy(ib.quaternion); nb.scale.copy(ib.scale); parent.add(nb); boneByName[key]=nb; if(itemInv[key]) invByName[key]=itemInv[key]; return nb; } itemBones.forEach(ensure); scene.traverse(o=>{ if(o.isSkinnedMesh) meshJobs.push({mesh:o,tex}); }); }); bodyRoot.updateMatrixWorld(true); const allBones = Object.values(boneByName); const inverses = allBones.map(b => invByName[K(b.name)] ? invByName[K(b.name)].clone() : new THREE.Matrix4().copy(b.matrixWorld).invert()); const skeleton = new THREE.Skeleton(allBones, inverses); meshJobs.forEach(({mesh,tex})=>{ const oldBones = mesh.skeleton.bones; const remap = oldBones.map(b=>{ const tb=boneByName[K(b.name)]; return tb ? allBones.indexOf(tb) : 0; }); const si = mesh.geometry.attributes.skinIndex, a = si.array; for(let i=0;i
/* ====== Collapsible board legend ====== */ (function(){ const sec = document.getElementById('legend'); const btn = document.getElementById('legend-toggle'); if(!sec || !btn) return; btn.addEventListener('click', ()=>{ const open = sec.classList.toggle('open'); btn.setAttribute('aria-expanded', open ? 'true' : 'false'); }); })(); /* ====== Mobile burger menu ====== */ const burger = document.getElementById('topnav-burger'); const drawer = document.getElementById('topnav-drawer'); if(burger && drawer){ burger.addEventListener('click', () => { const open = drawer.classList.toggle('open'); burger.setAttribute('aria-expanded', open ? 'true' : 'false'); }); drawer.querySelectorAll('a').forEach(a => { a.addEventListener('click', () => { drawer.classList.remove('open'); burger.setAttribute('aria-expanded', 'false'); }); }); } /* ====== Быстрый доступ — круглая кнопка → модальное окно ====== */ (function(){ const fab = document.getElementById('quick-fab'); const modal = document.getElementById('quick-modal'); if(!fab || !modal) return; let lastFocused = null; const focusables = () => Array.from(modal.querySelectorAll('a[href], button:not([disabled])')).filter(el => el.offsetParent !== null); function openModal(){ lastFocused = document.activeElement; modal.classList.add('open'); modal.setAttribute('aria-hidden', 'false'); fab.setAttribute('aria-expanded', 'true'); const f = focusables(); if(f.length) f[0].focus(); } function closeModal(){ modal.classList.remove('open'); modal.setAttribute('aria-hidden', 'true'); fab.setAttribute('aria-expanded', 'false'); (lastFocused && lastFocused.focus ? lastFocused : fab).focus(); } fab.addEventListener('click', openModal); modal.querySelectorAll('[data-quick-close]').forEach(el => { el.addEventListener('click', (e) => { const href = el.getAttribute('href'); if(href && href.length > 1 && href.charAt(0) === '#'){ const target = document.querySelector(href); if(target){ e.preventDefault(); closeModal(); requestAnimationFrame(() => target.scrollIntoView({behavior:'smooth', block:'start'})); return; } } closeModal(); }); }); modal.addEventListener('keydown', (e) => { if(e.key === 'Escape'){ e.preventDefault(); closeModal(); return; } if(e.key === 'Tab'){ const f = focusables(); if(!f.length) return; const first = f[0], last = f[f.length - 1]; if(e.shiftKey && document.activeElement === first){ e.preventDefault(); closeModal(); } else if(!e.shiftKey && document.activeElement === last){ e.preventDefault(); closeModal(); } } }); })();