/* ========================================================= config.js — единый объект конфигурации игры. Все игровые параметры (баланс, физика, вероятности) здесь, чтобы их можно было менять в одном месте. ========================================================= */ 'use strict'; const CONFIG = Object.freeze({ // ---------- Виртуальное разрешение (логические пиксели) ---------- // Игра рендерится в этом разрешении, затем масштабируется под экран. VIEW: { WIDTH: 960, HEIGHT: 540, GROUND_Y: 452, // Y-координата уровня земли (верх тротуара) PIXEL_SCALE: 4, // масштаб «пикселя» спрайтов }, // ---------- Производительность ---------- TARGET_FPS: 60, MAX_DELTA: 1 / 30, // ограничитель dt, чтобы физика не «взрывалась» при лагах // ---------- Физика игрока ---------- PLAYER: { X: 190, // фиксированная X-позиция героини WIDTH: 44, HEIGHT: 104, SLIDE_HEIGHT: 40, // высота хитбокса в подкате GRAVITY: 2400, // px/сек^2 JUMP_VELOCITY: -880, // начальная скорость первого прыжка DOUBLE_JUMP_VELOCITY: -780, // скорость второго прыжка MAX_JUMPS: 2, FLIP_DURATION: 0.5, // длительность одного сальто (сек, завершается в воздухе) COYOTE_TIME: 0.09, // «время койота» после схода с земли JUMP_BUFFER: 0.12, // буфер прыжка перед приземлением RUN_FRAME_TIME: 0.10, // ровно 6 физических тиков на кадр: без рваного чередования 5/6 SLIDE_FRAME_TIME: 0.065, // время кадра входа/выхода из подката HITBOX_PADDING: 8, // смягчение хитбокса (честность к игроку) }, // ---------- Скорость и сложность ---------- SPEED: { START: 290, // px/сек стартовая скорость мира MAX: 560, // предел скорости (ниже — игра проходима для всех) ACCEL: 2.0, // мягкое ускорение (было слишком резким) DISTANCE_PER_UNIT: 176, // px на 1 «метр» дистанции (набор метров ~в 22 раза медленнее) }, // ---------- Спавн препятствий ---------- OBSTACLES: { MIN_GAP_TIME: 1.05, // мин. пауза между препятствиями (сек, у стартовой скорости) MAX_GAP_TIME: 2.0, // макс. пауза GAP_SPEED_FACTOR: 0.4, // насколько промежутки сжимаются с ростом скорости SAFE_START: 2.4, // сек форы в начале забега без препятствий }, // ---------- Монеты ---------- COINS: { INTERVAL_MIN: 1.62, // мин. пауза между попытками появления монет (сек) — на 10% чаще INTERVAL_MAX: 2.88, // макс. пауза RETRY_DELAY: 0.4, // короткая повторная попытка, если окно занято GROUP_MIN: 1, GROUP_MAX: 3, SPACING: 96, // расстояние между монетами в группе (px) — с явным зазором SCORE_VALUE: 25, ARC_HEIGHT: 96, // высота дуги «воздушных» монет PER_DISTRICT: 3, // монет на поддержку одного района OBSTACLE_MARGIN: 150, // мин. зазор между монетами и препятствием (px) SCALE: 3, // масштаб спрайта монеты (меньше = мельче) }, // ---------- Цель игры ---------- DISTRICTS_TOTAL: 10, FINALE: { BUILDING_DOOR_X: 690, // куда встаёт дверь Госдумы (виртуальные px) WALK_TIME: 1.6, // сек подхода героини к двери ENTER_TIME: 0.8, // сек «входа» внутрь (затухание) CLEAR_MIN_TIME: 1.0, // минимум времени «пустой дороги» перед зданием }, // ---------- Счёт ---------- SCORE: { PER_METER: 1, DISTANCE_DIVISOR: 8, // счёт растёт с прежней скоростью (независимо от метража) }, // ---------- Параллакс-фон: относительные скорости слоёв ---------- PARALLAX: { SKY: 0.02, FAR_BUILDINGS: 0.14, HOUSES: 0.32, TREES: 0.55, STREET_PROPS: 0.82, // остановки, фонари, лавочки, клумбы ROAD: 1.0, // дорога/тротуар движется со скоростью мира }, // ---------- Аудио ---------- AUDIO: { MUSIC_VOLUME: 0.45, SFX_VOLUME: 0.7, }, }); // Список районов округа (выпадают случайно, без повторов). const DISTRICTS = Object.freeze([ 'Дмитровский', 'Западное Дегунино', 'Восточное Дегунино', 'Бескудниковский', 'Коптево', 'Тимирязевский', 'Аэропорт', 'Савёловский', 'Беговой', 'Хорошёвский', ]); // Типы препятствий + метаданные (размеры хитбокса и «воздушность»). // air:true — препятствие висит сверху, его нужно подкатить, а не перепрыгнуть. // embed:true — препятствие «встроено» в дорогу (плоское, без тени). const OBSTACLE_TYPES = Object.freeze([ { id: 'hatch', name: 'Открытый люк', w: 70, h: 22, air: false, mode: 'jump', embed: true }, { id: 'trash', name: 'Переполненная мусорка', w: 52, h: 64, air: false, mode: 'jump' }, { id: 'puddle', name: 'Огромная лужа', w: 120, h: 18, air: false, mode: 'jump', embed: true }, { id: 'curb', name: 'Сломанный бордюр', w: 60, h: 34, air: false, mode: 'jump' }, { id: 'roadwork', name: 'Дорожные работы', w: 78, h: 66, air: false, mode: 'jump' }, { id: 'scooter', name: 'Самокат', w: 84, h: 42, air: false, mode: 'jump' }, { id: 'tiles', name: 'Раскопанная плитка', w: 96, h: 20, air: false, mode: 'jump', embed: true }, { id: 'fence', name: 'Упавшее ограждение', w: 88, h: 46, air: false, mode: 'jump' }, { id: 'tape', name: 'Строительная лента', w: 60, h: 70, air: true, mode: 'slide' }, ]); // Смешные подписи к поражению, сгруппированы по типу препятствия. const LOSE_CAPTIONS = Object.freeze({ hatch: ['С вами случилось благоустройство.', 'Люк оказался убедительнее.', 'Не каждый путь ведёт вверх.'], trash: ['Переработка не удалась.', 'Мусор победил.', 'Тут давно пора навести порядок.'], puddle: ['Проверили глубину лично.', 'Асфальт временно работает как бассейн.', 'Заплыв засчитан.'], curb: ['Бордюр нанёс критический удар.', 'Комфортная среда закончилась.', 'Ремонт немного задержался.'], roadwork:['Работы идут уже очень давно.', 'Осторожно, благоустройство.', 'Объезд не предусмотрен.'], scooter: ['Самокат припарковался идеально.', 'Кто-то оставил транспорт прямо здесь.', 'Великая битва с кикшерингом проиграна.'], tiles: ['Плитку снова меняют.', 'Раскопки продолжаются.', 'До финиша немного не докопали.'], fence: ['Ограждение оказалось быстрее.', 'Забор сказал «нет».', 'Проход временно закрыт.'], tape: ['Торжественное открытие откладывается.', 'Лента остановила кандидата.', 'Проход временно невозможен.'], });