Как мы делали мини-игру про ровер на Марсе внутри Telegram WebApp
«Хочется сделать простую карту, чтобы листать её в Telegram». С этого всё и началось. А закончилось — изометрическим движком, авторизацией по WebApp, системой энергии, покупкой участков и боевым ровером с шестью колёсами.



🚀 С чего всё началось?
В начале всё было очень просто.
Мы сделали простенького бота, о котором я уже как-то тут писал, и бэк рендерил картинки с кусочком карты, где ты находишься.
В целом, даже эта идея была вполне рабочей и первые 300 пользователей с разных источников легко собрались. Мы даже провели на 9 мая конкурс "найди звезду победы" и выплатили победителю небольшой приз :)
Но само собой, что бот - не предел мечтаний, нужно было пилить полноценный мини-апп.
На боте лишь проверили гипотезу, отладили механики, типа уменьшения энергии, подзарядки аккумулятора в течении времени, пока не заходишь в игру.
Первый шаг в сторону мини-аппки - сделали вебстраничку, где можно было листать мышкой или пальцем — просто ради визуализации. Прямоугольная сетка, тайлы, немного стилей. Telegram WebApp проглатывал HTML5 на ура. Тогда не было никакой логики, просто подгрузка текстур и картинка под пальцем.
Вот как это выглядело:
Пользователь заходил и видел карту Марса.
Никакого взаимодействия — только “глянуть”.
🎮 А потом захотелось интерактивности
Следующим шагом стало добавление изометрии — чтобы выглядело как псевдо-3D. Самое интересное, что даже не потребовалось изменять текстуры. Серьезно :) Они по-прежнему те же самые, квадратные, 64 х 64. И не используется никакой 3д - движок.
вот краткое и понятное объяснение, как строится изометрическая карта из квадратных тайлов:
🧠 Основная идея:
Каждый квадратный тайл поворачивается на 45° и масштабируется по вертикали, чтобы получился ромб (изометрическая проекция). Вместо привычной сетки (x, y) мы рассчитываем экранные координаты (left, top) по формуле:
📐 Формулы для отображения:
При размере одного тайла T:
W = T * sqrt(2) — изометрическая ширина (диагональ квадрата).
H = W / 2 — изометрическая высота (высота ромба).
WX2 = W / 2, HX2 = H / 2 — половинки для смещения от центра.
Переход от логических координат (dx, dy) к пиксельным:
isoX = (dx - dy) * WX2 + centerX; isoY = (dx + dy) * HX2 + centerY;
🧩 Что это даёт:
(dx - dy) — смещает тайл по горизонтали.
(dx + dy) — смещает тайл по вертикали.
centerX, centerY — центр экрана, чтобы карта строилась относительно игрока.
🎯 В результате:
Из обычной квадратной сетки (x, y) формируется ромбовидная карта, где видны и горизонтальные, и вертикальные соседние тайлы.
Центральная клетка (текущий игрок) — всегда по центру, а остальные располагаются вокруг.
Ну а дальше уже дело техники - придумали алгоритм перемещения в 8 направлениях: вверх, вниз, влево, вправо, плюс диагонали.
Подключили ранее обкатанный в чатботе расход энергии за каждый шаг, и разный расход за диагональные движения, в сравнении с линейным. Плюс небольшой рандом :)
Задали запреты на воду, скалы и занятую клетку, чтобы не было “читов”.
🔐 Само собой - авторизация
Чтобы пользователь не “прыгал” по чужим роверам и участкам, мы внедрили Telegram WebApp InitData (это такая строка с хешем, которую фронт передает нам в бэк, а мы - уже на сервере телеграм с токеном бота валидируем подпись. Если сошлась - то пользователь зашел к нам через телегу. Если нет - скорее всего он просто открыл веб-страницу как сайт, или что-то пытается поломать, подделать :)
Если кратко:
Telegram сам отдаёт токен с подписью.
Мы проверяем подпись на бэке по HMAC SHA256.
Получаем ID пользователя, сохраняем его в сессии.
Теперь всё честно: ровер – только твой, кристаллы – только твои.
🪐 Стало красивее: добавили кристаллы и рамки
Потом появились:
Кристаллы на клетках — можно собирать.
Подсветка клеток: белая рамка — твоя, красная — чужая.
Имена владельцев, чтобы было видно, кто что захватил.
В планах: Покупка участков за кристаллы. Это было в текстовом боте. И ползая по карте, даже видны купленные тобой (белым) и оппонентами (красным) участки.
⚡️ Оптимизация и загрузка ассетов
Мы поняли, что каждая картинка может тормозить игру на слабом устройстве, и:
Добавили прелоадер, который подгружает PNG-шки перед игрой.
Сделали показ спиннера на любом действии (движение, загрузка).
Кэшируем тайлы и обновляем только при движении.
🤖 Как выглядит сейчас
Игрок:
Заходит в Telegram Mini App.
Авторизуется за доли секунды.
Видит изометрическую карту с ровером, кристаллами, участками, рекламными баннерами.
Может двигаться по клеткам, собирать кристаллы (в будущем - бурить и находить ресурсы, торговать ими, покупать землю).
А мы — всё это рисуем прямо в DOM.
Никаких Canvas, WebGL, или тяжелых движков. Только HTML, CSS и немного магии на JS.
💬 Если интересно — покажу, как это выглядит вживую.
Тестить можно тут. А если зайдёт — добавим NFT, фермы и квесты на выживание 😄
Хотите немножко припухнуть от удивления?
Вы поверите если я скажу, что вы уже сегодня сможете создать свой первый сайт, без единой строчки кода? Если честно я бы сам не поверил, но такова реальность, я сам в шоке. Видео-доказательство прямо перед вами )
🔴Итоговая версия сайта -- может кому будет интересно посмотреть и почитать.
🟠ИИ Новости -- новостной тг канал, в котором я каждый день пересылаю самые актуальные новости из других каналов.
🔵Основной ТГ канал -- канал с персонализированным контентом.
Вёрстка - это моя любовь с первого взгляда
Когда я начала изучать верстку сайтов, я и не думала, что у меня такой большой словарный запас. Почему я вообще начала это изучать? Да потому что это красиво. И вроде бы легче программирования. Да и вообще красиво очень. И сначала, на первых этапах изучения, вроде бы все прикольно. Пару строк написал и какой-то макет получился, даже интересное что-то выходит. Но вот я решила взяться за что-то уже более похожее на полноценный сайт, и после часа работы мои мысли были примерно такими: "Ебаная верстка! Какого хрена? Сука, 20 строчек чтобы нарисовать одну ебаную кнопку. Почему блядь нельзя курсором тыкнуть, чтобы оно на место сразу встало? Не, блядь, надо обязательно позиционировать это словами. Почему ебучий браузер не может с двух слов понять, что я от него хочу? Какого хера я этим занимаюсь, а не программированием?
А, нет, все-таки красиво получилось, не буду пока бросать этим заниматься."
И так каждый раз. Куча матов на тему того, как же меня бесит веб разработка. Наверное, это любовь с первого взгляда. Надо отогнать от себя мысли бросить все.
Какие есть нестандартные значения атрибута rel в теге link?
Какие есть нестандартные значения атрибута rel в теге link?
Например я встречал значение <link rel="stylesheet/less" ...
Такого значения в списке значений в справочниках не нашел. Получается в этот атрибут можно указывать любые произвольные значения? И эти значения потом передаются в обработчик файла указанного в href для дальнейшей интерпретации?
Боевой клич, Пикабутянечки вы мои! ModX Revo
В прошлый раз искала кому помочь и сейчас ищу компаньонов для обучения вместе. Веб-дизайн, верстка, конструкторы сайта, администрирование и разработка.
Поделюсь своим опытом на реальных проектах! Расскажу о направлениях и помогу определиться с выбором. Очень ищу человека также интересующего modx
Что имеем:
Есть интересующийся движок modX Revolution для разработки сайтов
Есть проекты которые надо реализовать
Свободное время
Желание глубже(и в целом) изучить данный движок
Контакты для связи в комментах :) Всем добра!
Все о datalist, fieldset и button в HTML
Привет, в рамках этого урока мы разберемся оставшимися тегами формы, а именно: datalist, fieldset, legend, button. Также разберемся в нюансах и частоте использования этих тегов. Текстовая версия урока в полной версии этой статьи.
Datalist
<datalist> - содержит набор опций (<option>), доступных для выбора. Выбранное значение будет установлено для элемента <input>, с атрибутом list.
Не стоит использовать его вместо тега <select>, но можно использовать как вспомогательный тег для <input>. Причина в том что его сложнее обрабатывать через JavaScript.
<form action="">
<label for="ice-cream-choice">Choose a flavor:</label>
<input list="ice-cream-flavors" id="ice-cream-choice" name="ice-cream-choice" />
<datalist id="ice-cream-flavors">
<option value="Chocolate">
<option value="Coconut">
<option value="Mint">
<option value="Strawberry">
<option value="Vanilla">
</datalist>
</form>
Fieldset
<fieldset> - существует для группировки <input> внутри тега <form>, а также помогает удобно управлять группой полей.
<form action="#">
<h3>Registration form</h3>
<fieldset>
<div>
<label for="name_input">First name:</label>
<input id="name_input" type="text" placeholder="Email">
</div>
<div>
<label for="surname_input">Surname:</label>
<input id="surname_input" type="text" placeholder="Surname">
</div>
</fieldset>
<br>
<fieldset disabled>
<div>
<label for="email_input">Email:</label>
<input id="email_input" type="text" placeholder="Email">
</div>
<div>
<label for="password_input">Password:</label>
<input id="password_input" type="text" placeholder="Password">
</div>
</fieldset>
</form>
У <fieldset> также есть несколько атрибутов, которые помогают управлять сразу всей группой <input>.
name - задает общее имя для группы
disabled - делает группу недоступной для редактирования
form - если <fieldset> лежит вне тега <form> то этот атрибут позволит вам связать их.
<form action="" id="example_form">
<h3>Example form</h3>
<label>
<input type="text" placeholder="Some example input">
</label>
</form>
<fieldset form="example_form">
<div>
<label for="name_2_input">First name:</label>
<input id="name_2_input" type="text" placeholder="Email">
</div>
<div>
<label for="surname_2_input">Surname:</label>
<input id="surname_2_input" type="text" placeholder="Surname">
</div>
</fieldset>
Legend
<legend> - Является заголовком для группы элементов, сгруппированных через <fieldset>
<form action="">
<h3>Another Example Form</h3>
<fieldset>
<legend>User info</legend>
<div>
<label for="name_3_input">First name:</label>
<input id="name_3_input" type="text" placeholder="Email">
</div>
<div>
<label for="surname_3_input">Surname:</label>
<input id="surname_3_input" type="text" placeholder="Surname">
</div>
</fieldset>
<br>
<fieldset disabled>
<legend>User credentials</legend>
<div>
<label for="email_3_input">Email:</label>
<input id="email_3_input" type="text" placeholder="Email">
</div>
<div>
<label for="password_3_input">Password:</label>
<input id="password_3_input" type="text" placeholder="Password">
</div>
</fieldset>
Button
<button> - Парный тег, который предназначен для создания кнопки, имеет схожие атрибуты с тегом <input>. Может использовать как внутри <form> заменяя собой <input type="button">, так и просто как отдельный элемент на странице.
<form action="">
<h3>Another Example Form</h3>
<fieldset>
<legend>User info</legend>
<div>
<label for="name_3_input">First name:</label>
<input id="name_3_input" type="text" placeholder="Email">
</div>
<div>
<label for="surname_3_input">Surname:</label>
<input id="surname_3_input" type="text" placeholder="Surname">
</div>
</fieldset>
<br>
<fieldset disabled>
<legend>User credentials</legend>
<div>
<label for="email_3_input">Email:</label>
<input id="email_3_input" type="text" placeholder="Email">
</div>
<div>
<label for="password_3_input">Password:</label>
<input id="password_3_input" type="text" placeholder="Password">
</div>
</fieldset>
<fieldset>
<legend>Form Controls</legend>
<button type="submit">Submit</button>
<button type="reset">Clear Form</button>
</fieldset>
</form>
Подключение шрифтов на сайт
Пример #1 — Подключаем шрифт PT Sans через Google Fonts
- Заходим на сайт fonts.google.com и находим шрифт PT Sans;
- Нажимаем кнопку Select this font;
- Нажимаем на Family Selected;
- Во вкладке Customized выбираем начертания и Cyrillic;
- Копируем строку с подключением шрифта во вкладках EMBED → @IMPORT.
Вставляем строку с подключением в начало CSS файла:
@IMPORT url('https://fonts.googleapis.com/css?family=PT%20Sans%3A400i%2C7...);
Копируем свойство font-family:
Пример #2 — Подключаем шрифты PT Sans и PT Serif в Drupal 8 через файл темы .libraries.yml
Аналогично примеру #1 получаем URL подключения шрифтов из строки @import:
fonts.googleapis.com/css?family=PT%20Sans%3A400%2C400...
В файле темы .libraries.yml подключаем шрифты по образцу:
fonts.googleapis.com/css?family=PT%20Sans%3A400%2C400...,
Сохраняем и сбрасываем кэш.
Пример #3 — Подключаем шрифт PT Sans локально
- Заходим на сайт Google Webfonts Helper;
- В поиске находим шрифт PT Sans;
- Выбираем начертания и Cyrillic;
- Пролистываем вниз и пишем где будут находится шрифты относительно .css файла;
- Копируем CSS код и вставляем его в .css файл;
- Скачиваем архив с шрифтами, разархивируем его и размещаем шрифты в нужном месте.
Вот так выглядит скопированный CSS:
/* pt-sans-regular - cyrillic_latin */
@font-face {
font-family: 'PT Sans';
font-style: normal;
font-weight: 400;
src: url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-regular.eot'); /* IE9 Compat Modes */
src: local('PT Sans'), local('PTSans-Regular'),
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-regular.woff') format('woff'), /* Modern Browsers */
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-regular.svg#PTSans') format('svg'); /* Legacy iOS */
}
/* pt-sans-italic - cyrillic_latin */
@font-face {
font-family: 'PT Sans';
font-style: italic;
font-weight: 400;
src: url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-italic.eot'); /* IE9 Compat Modes */
src: local('PT Sans Italic'), local('PTSans-Italic'),
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-italic.woff2') format('woff2'), /* Super Modern Browsers */
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-italic.woff') format('woff'), /* Modern Browsers */
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-italic.ttf') format('truetype'), /* Safari, Android, iOS */
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-italic.svg#PTSans') format('svg'); /* Legacy iOS */
}
/* pt-sans-700 - cyrillic_latin */
@font-face {
font-family: 'PT Sans';
font-style: normal;
font-weight: 700;
src: url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-700.eot'); /* IE9 Compat Modes */
src: local('PT Sans Bold'), local('PTSans-Bold'),
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-700.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-700.woff2') format('woff2'), /* Super Modern Browsers */
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-700.woff') format('woff'), /* Modern Browsers */
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-700.ttf') format('truetype'), /* Safari, Android, iOS */
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-700.svg#PTSans') format('svg'); /* Legacy iOS */
}
/* pt-sans-700italic - cyrillic_latin */
@font-face {
font-family: 'PT Sans';
font-style: italic;
font-weight: 700;
src: url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-700italic.eot'); /* IE9 Compat Modes */
src: local('PT Sans Bold Italic'), local('PTSans-BoldItalic'),
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-700italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-700italic.woff2') format('woff2'), /* Super Modern Browsers */
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-700italic.woff') format('woff'), /* Modern Browsers */
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-700italic.ttf') format('truetype'), /* Safari, Android, iOS */
url('../fonts/pt-sans/pt-sans-v9-cyrillic_latin-700italic.svg#PTSans') format('svg'); /* Legacy iOS */
}
А так список файлов с шрифтами:
Команда ls в терминале
Если все сделано правильно, то шрифты будут подключены локально.





