@hezzlgames/sdk
v1.5.0
Published
Hezzl Games SDK: договор игры с игровым центром play.hezzl.ru — настройки, рукопожатие, хранилище, валюта центра
Maintainers
Readme
Hezzl Games SDK
Библиотека, через которую игра разговаривает с игровым центром. Один
файл без зависимостей, ES5, около 20 КБ. Распространяется пакетом
@hezzlgames/sdk; исходник живёт в play.hezzl.com/sdk.
Полные требования к играм — docs/GAME-REQUIREMENTS.md. Здесь только
то, как ими пользоваться; ссылки на разделы ведут туда.
hezzl-sdk.js библиотека — кладётся в папку игры
index.mjs, .cjs та же библиотека для import / require
index.d.ts типы
bin/hezzl-sdk.mjs команды: стенд, проверка манифеста, копирование
demo/game.html пример игры на ней, с hezzl.json и economy.json
demo/centre.html стенд: поддельный центр для проверкиУстановка
npm i @hezzlgames/sdkТри способа подключить — один и тот же файл:
<!-- без сборки: файл лежит в папке игры -->
<script src="hezzl-sdk.js"></script>npx hezzl-sdk copy . # положить hezzl-sdk.js в папку игры// со сборкой: Vite, Webpack, esbuild
import { Hezzl } from '@hezzlgames/sdk';Второй и третий способ — тот же hezzl-sdk.js: пакет только доставляет
его, а игра по-прежнему обязана работать по прямой ссылке и без сети
(раздел 10). С нашего домена и с CDN его не подключают. Обновление
SDK приезжает вместе с новой сборкой игры: подняли версию пакета —
пересобрали — сдали.
Версии. Версия пакета (Hezzl.version, semver) растёт на каждый
выпуск и уходит в ready полем sdkVersion — по нему видно, какие
игры собраны на чём. Номер протокола (Hezzl.sdk, сейчас 1) — другое
число: он меняется только когда меняется смысл существующего поля (3.8).
Команды
npx hezzl-sdk stand [port] # поддельный центр на localhost, по умолчанию 8791
npx hezzl-sdk check [dir] # проверить hezzl.json и economy.json
npx hezzl-sdk copy [dir] # положить hezzl-sdk.js в папку игрыcheck — то, что мы смотрим первым на приёмке (41.1, 41.2): реестр
режимов, имена умений, обязательные поля, лестница цен и норма
сундуков из 37.5. Гоняйте его перед сдачей; он же стоит в prepack
пакета.
economy.json
Для игры без сервера, которая тратит валюту центра (currency: true
в манифесте). Все цены на экране — отсюда и ниоткуда больше (37.5):
{
"boosters": { "hint": 10, "hammer": 40, "rocket": 40 },
"continue": [50, 100, 1500],
"levels": 60,
"chests": [
{ "stars": 15, "boosters": [
{ "key": "hammer", "count": 1, "chance": 0.5 },
{ "key": "rocket", "count": 1, "chance": 0.5 }
] }
]
}| поле | правило |
|---|---|
| boosters | ключ — строчные латиница, цифры, подчёркивание; тот же ключ идёт в item просьбы spend. Цена — целое больше нуля |
| continue | лестница цен продолжения уровня, не убывает; последняя ступень — предел (37.5) |
| chests | сундуки на карте: за сколько звёзд и что может выпасть. Не больше двух бустеров, не больше одного каждого вида, chance — доля, которую видит человек (37.1) |
| levels | длина карты, для оценки нормы бесплатных бустеров; без поля — 60 |
| pass | модули пропуска (37.6): { unlockers: { days, rewards: [{ key, count, pass }] } }; у showcase наград ещё stars; у tasks — task, и на каждое задание две записи: pass: false и pass: true. У unlockers первая награда бесплатная |
Зачем он
Половину требований первой части он выполняет за вас:
- разбирает
lang,theme,accent,muted,haptics,refиз адреса и проверяет их формат; - проверяет источник входящих сообщений — обе проверки из 3.2, включая ту, про которую забывают и отключают защиту при прямом заходе;
- держит рукопожатие: узнаёт, есть ли центр рядом и что он умеет;
- складывает команды, пришедшие раньше готовности, и не теряет их;
- держит хранилище: через центр, а без центра — своё, с префиксом;
- соблюдает частоту сообщений и пропускает финальный счёт мимо неё;
- ведёт разговор о валюте центра: баланс, товары, покупка, списание, кошелёк — с типизированными отказами и без своих окон;
- знает про пропуск центра: есть ли, до когда, почём; сам замечает, что срок вышел; даёт часы центра для суточных наград;
- собирает
window.HezzlGameс правильными именами методов.
Файл кладётся в папку игры и подключается оттуда. С нашего домена его не подключают: игра обязана работать по прямой ссылке и без сети, а обращения на сторонние домены запрещены (раздел 10). Обновление SDK приезжает вместе с новой сборкой игры.
Как подключить
<script src="hezzl-sdk.js"></script>
<script src="game.js"></script>Минимальная игра
Hezzl.init({
id: 'my-game', // ярлык из каталога, как в hezzl.json
version: '1.0.0',
modes: ['quick'], // ключи из реестра 20.1
orientation: 'any',
on: {
mute: function (muted) { sound.enabled = !muted; },
pause: function () { engine.pause(); },
resume: function () { engine.resume(); }
}
});
Hezzl.ready(); // когда игра действительно принимает вводsupports выводится из обработчиков, объявлять его отдельно не надо.
Объявить умение и не реализовать его по построению невозможно — а это
самое частое расхождение между манифестом и поведением.
ready — обязателен, и это не формальность
Зовётся, когда игра принимает ввод, а не когда загрузился скрипт: по нему центр отпускает заставку.
Подключив библиотеку, вы делаете ready обязательным на практике.
SDK объявляет игру на window, центр видит объявление и начинает ждать
её слова. Игра без ready держит заставку 20 секунд, а потом человек
получает экран «Игра долго не отвечает». Без библиотеки та же игра просто
открылась бы — центр снял бы заставку по загрузке кадра.
Раньше ready не позвать было незаметно. Теперь — заметно, и заметно
человеку, а не вам.
Заставку ready при этом не сокращает: у неё своя минимальная
длительность и своя сцена, а ready только отпускает её. Позвать его
рано, «чтобы быстрее», смысла не имеет — раньше минимума заставка
не уйдёт. Позвать поздно — человек лишнее время смотрит на лоадер.
Правильный момент — первый кадр, на котором касание уже что-то делает.
Настройки при запуске
Hezzl.settings // { lang, theme, accent, muted, haptics, ref }Пустая строка означает «решай сама»: человек оставил настройку на «Системе». Порядок разрешения — адрес, своя сохранённая, браузер:
var lang = Hezzl.resolve('lang', saved.lang, function () {
return (navigator.language || 'ru').slice(0, 2) === 'en' ? 'en' : 'ru';
});Настройка из адреса не запоминается как выбор человека (раздел 2).
Хранилище
Hezzl.storage.get('progress').then(function (data) { ... });
Hezzl.storage.set('progress', data, { sync: true, schema: 3 });
Hezzl.storage.remove('draft');
Hezzl.storage.keys();Префикс не пишется — его ставит SDK. Вызовы до рукопожатия копятся
и разрешаются, как только известен режим: ждать ready, чтобы прочитать
прогресс, не нужно.
sync — уносить ли на сервер, когда человек войдёт. Прогресс
и состояние обучения — да. Громкость, скин, уровень качества — нет.
Отказ типизирован, ветвиться надо по reason, а не по тексту:
Hezzl.storage.set('progress', data, { sync: true }).catch(function (e) {
if (e.reason === 'quota') dropCaches(); // освободить необязательное
else if (e.reason === 'unavailable') sayOnce('Прогресс не сохранится');
});Значения: unavailable, quota, invalid, unknown (раздел 7.6).
События
Hezzl.gameStart({ mode: 'levels', level: 3 });
Hezzl.score(1200, { best: 4000 });
Hezzl.milestone('combo', 5); // what — из реестра 3.4
Hezzl.levelComplete({ level: 3, stars: 2 });
Hezzl.levelFailed({ level: 3 });
Hezzl.gameOver({ score: 1200, best: 4000 });score можно звать на каждое очко: SDK сам придержит лишнее и отправит
последнее значение хвостом. Перед gameOver финальный счёт уходит
всегда, мимо ограничения, — иначе центр покажет предпоследний результат.
События для заданий
По событиям центр ведёт задания вида «пройди пять уровней в Одиссее».
Считаются только события из реестра (раздел 3.4 требований) с полями
известной формы; SDK проверяет их и кладёт конверт: eid — уникальный
ключ, по которому сервер засчитывает событие один раз, seq, at
по часам центра и mode из последнего gameStart.
Hezzl.levelComplete({ level: 12, first: true, stars: 2 }); // first обязателен
Hezzl.levelFailed({ level: 12, reason: 'moves' }); // moves | time | lives | quit | other
Hezzl.quest('collect_100', { first: true, reward: 'hammer' });
Hezzl.achievement('no_boosters');
Hezzl.boostUsed('hammer', { level: 12 });
Hezzl.chestOpen('gold', { stars: 15 });
Hezzl.pvp('win', { opponent: 'bot', score: 1200 });
Hezzl.event('arena_finish', { arena_id: 'a71231', place: 2 }); // любое из реестраfirst в levelComplete обязателен: задания считают только первые
прохождения, иначе «пройди пять уровней» закрывалось бы первым уровнем
пять раз. Без first SDK отправит first: false и предупредит в консоли.
Имя не из реестра не уходит вовсе, поле не той формы отбрасывается
с пометкой в отладке. Своё — в extra: {} до 1 КБ. Hezzl.events()
отдаёт реестр целиком.
Показать задание в игре можно, но решает центр:
Hezzl.tasks().then(function (a) { a.tasks.forEach(drawTask); }); // { key, title, progress, goal, done, reward }
Hezzl.init({ on: { task: function (t) { drawTask(t); if (t.done) celebrate(); } } });Награду за задание выдаёт центр в кошельке, игра только празднует.
Заряд
Раз в сутки, в окне 11:00–14:00 по часам центра, на 45 минут открывается заряд: общий на все игры запас бесплатных бустеров и удвоенный опыт за задания (раздел 7.11 требований). Игра ничего не хранит и не считает — списывает у центра по одному в момент применения:
var c = Hezzl.charge.last(); // { active, endsAt, nextAt, boosters: { left, total }, xp, window }
button.textContent = Hezzl.charge.has() ? '⚡ ' + c.boosters.left : '40 ⬡';
button.onclick = function () {
if (!Hezzl.charge.has()) return buyAsUsual();
Hezzl.charge.use('hammer').then(function (c) {
apply(); // списали — применяем
Hezzl.boostUsed('hammer');
}, function (e) {
if (e.reason === 'empty' || e.reason === 'inactive') buyAsUsual(); // без «вы опоздали»
});
};
Hezzl.init({ on: { charge: function (active, c) { repaintBoosters(c); } } });Статус приходит вместе с пропуском в ответе pass() и отдельно
через charge(). Уведомление charge приходит на открытие и закрытие
окна и когда бустер списали в другой игре; SDK сам переспрашивает
центр на границах окна. Остаток и таймер — только из ответов центра.
Рейтинг по прогрессу
Игре без сервера рейтинг ведёт центр — по тем же событиям, и это
рейтинг прогресса, а не очков: уровни, пройденные впервые, лучшие
звёзды, миссии (раздел 26.4 требований). Игра объявляет потолки
в hezzl.json, поле rating, и запрашивает таблицу:
Hezzl.leaderboard({ period: 'week', limit: 10 }).then(function (b) {
drawMe(b.me); // { rank, points, levels, stars, quests } или null: «сыграйте партию»
drawTop(b.top); // [{ rank, points, name?, avatar?, me? }]
});
Hezzl.init({ on: { rank: function (rank, d) { drawMe(d); } } }); // место изменилосьТаблицу обновлять не чаще раза в 30 секунд. Своей таблицы игра не считает и ботов в неё не сажает.
Просьбы к центру
Hezzl.share({ text: 'Собрал 2048 за 214 ходов' });
Hezzl.support({ message: '...', state: { level: 12 } });
Hezzl.exit();
Hezzl.signin();
Hezzl.auth().then(function (a) { ... }); // a.signedIn, a.code
Hezzl.profile(['name', 'avatar']).then(function (a) { ... }); // a.profileПоказывать эти кнопки можно только после рукопожатия и только те, что центр умеет:
if (Hezzl.can('share')) showShareButton();signin() ничего не возвращает — это просьба, а не вопрос. Центр
поднимает свой экран входа поверх игры; игра под ним продолжает жить,
и человек может закрыть его крестиком и играть дальше. Ответ придёт
отдельно и не сразу: signedin — когда человек вошёл, signincancelled —
когда закрыл экран, так и не войдя. Ждать в цепочке нельзя: разговор
занимает минуту, а бывает, что не заканчивается вовсе.
Hezzl.init({ on: {
signedin: function (id) { unlockTournament(id); },
signincancelled: function () { showLocalOnlyNotice(); }
} });Отказ приходит только тому, кто вход просил. Человек, открывший вход своей кнопкой в шапке центра, игре ничего не обещал, и та об этом не узнаёт.
Что лежит в ответе auth(), зависит от игры (7.1). Сторонняя игра
со своим сервером получит code — одноразовый код обмена. Игра
на платформе Hezzl получит token и expireAt для init штатного
SDK Hezzl, и guest: true, если человек вошёл гостем. Сегодня центр
отвечает только signedIn: ни кода, ни токена он пока не выдаёт,
потому что игра пошла бы с ними на сервер и получила бы отказ.
Ветвитесь по signedIn, а code и token проверяйте на существование.
Hezzl.mode() возвращает unknown, centre или standalone. Пока
unknown — рукопожатие идёт, зависимые элементы не показываются.
При прямом заходе режим известен сразу.
Валюта центра
У центра одна твёрдая валюта на все игры. Человек получает её за задания и покупает за деньги — в центре, не в игре. Игра её только тратит: на товары из нашего каталога или на своё, по своей цене. Полные правила — раздел 7.9 требований; здесь — как этим пользоваться.
if (Hezzl.can('balance')) showShop(); // у центра без валюты магазина нет
Hezzl.currency(); // { key, title, icon, amount } или null
Hezzl.balance().then(function (a) { hud(a.amount); }); // a.signedIn, a.amountcurrency() синхронный: значок и название приходят с рукопожатием,
сумма — с первым же ответом, где она есть. Рисовать счётчик можно
сразу после ack, не дожидаясь balance().
Кейс 1 — товар из нашего каталога
Товар заведён у нас: цена в валюте центра, награда в ресурсах игры.
Игра не хранит ни того ни другого — берёт из goods():
Hezzl.goods().then(function (a) { renderShop(a.goods); });
// a.goods: [{ id, title, picture, price, award }]
Hezzl.buy(good.id).then(function (done) {
give(done.award); // выдаём то, что ответил центр
hud(done.amount); // сколько осталось
}, function (e) {
if (e.reason === 'insufficient') say('Не хватает ' + e.need);
else if (e.reason !== 'cancelled') say('Не получилось');
});Между вызовом и ответом центр делает две вещи сам, и игре о них
знать не нужно: спрашивает подтверждение своим окном поверх
кадра и, если валюты не хватает, показывает кошелёк — где взять.
На это время игра получает pause, по закрытии — resume.
Обещание висит столько, сколько человек думает: срока у него нет,
пока центр держит разговор.
Hezzl.buy(id, { wallet: false }) — не показывать кошелёк, а сразу
отказать insufficient. Нужно редко: когда игра сама хочет сказать
про нехватку и открыть кошелёк позже, wallet().
Кейс 2 — товар свой, цена своя
Игра без сервера: бустеры, подсказки, продолжение партии. Цену назначает игра, центр только списывает — с тем же подтверждением и тем же кошельком при нехватке:
Hezzl.spend(10, { item: 'Подсказка' }).then(function (done) {
bag.hints++; // выдаём после ok, не по нажатию
hud(done.amount);
}, refuse);item — что куплено, до 80 знаков: его человек видит в окне
подтверждения, а мы — в отчёте.
Кошелёк и уведомления
Hezzl.wallet({ reason: 'Магазин' }).then(function (w) { hud(w.amount); }); // w.bought
Hezzl.init({ on: {
balance: function (amount) { hud(amount); }, // изменился — по любой причине
walletclosed: function (amount, d) { /* d.bought */ }
} });balance приходит без просьбы: награда за задание, покупка в кошельке,
покупка в другой вкладке. Опрашивать баланс по таймеру не нужно.
Отказы
Ветвиться по reason; текст — технический и не для показа:
| reason | что случилось | что делать |
|---|---|---|
| insufficient | не хватило, и кошелёк закрыли, не пополнив; need — сколько | сказать число, оставить кнопку |
| cancelled | человек нажал «отмена» в подтверждении | ничего: он сам передумал |
| signin | человека нет, а вход он закрыл | «войдите, чтобы покупать» |
| notfound, disabled | товара нет или он выключен у нас | убрать из витрины, обновить goods() |
| unavailable | центра нет или у него нет валюты | магазина не показывать вовсе |
| invalid | плохие доводы: не целое, не больше нуля | ошибка сборки |
| unknown | сервер не ответил или отказал | «попробуйте ещё раз», ничего не выдавать |
Ничего не выдаётся до ok. Списание подтверждает и проводит
центр; игра, выдавшая товар по нажатию, отдаёт его бесплатно
при любом отказе.
Пропуск центра
Одна недельная подписка на весь каталог, покупается в центре. Игра
её не продаёт и не проверяет оплату: спрашивает, есть ли пропуск,
и открывает или закрывает свои модули наград (раздел 37.6 требований:
seasonbox, showcase, tasks, unlockers).
if (Hezzl.can('pass')) showTreasure(); // без центра модулей пропуска нет
Hezzl.pass().then(function (p) { ... }); // { active, expiresAt, price, currency, title, now }
Hezzl.pass.has(); // действует ли сейчас — по последнему ответу, синхронно
Hezzl.pass.last(); // последнее известное, для кнопки с ценой
Hezzl.pass.buy().then(function (p) { // центр открыл оплату; ответ — когда закончили
unlock();
}, function (e) {
if (e.reason === 'cancelled') return; // человек передумал — молчим
say(e.reason === 'signin' ? 'Войдите, чтобы купить' : 'Оплата не прошла');
});
Hezzl.init({ on: {
pass: function (active, p) { relock(); } // купили в другой вкладке, кончился, продлили
} });pass.buy() держится, пока человек в окне оплаты (hold: 'payment'),
как и денежные просьбы. Отказы: cancelled, failed, signin,
active (уже действует, продлить до конца срока нельзя), unavailable,
unknown.
Срок SDK считает сам. По expiresAt он ставит будильник, в нужный
момент даёт pass с expired: true и переспрашивает центр — вдруг
продлили. Опрашивать pass() по таймеру не нужно.
Часы центра
Всё суточное — «раз в день», «завтра», сезон — считается по часам центра, не устройства. Перевод времени на телефоне не должен давать вторую награду (37.2):
var c = Hezzl.clock(); // { now: 1788886069784, day: '2026-09-08', trusted: true }
if (saved.lastDay === c.day) sayTomorrow(); else give();day — календарный день центра, готовая строка: сравнивать, не
разбирать. trusted: false значит, что центра нет и время взято
с устройства — тогда модулей пропуска и не показывают.
Награда выдана
Hezzl.reward({ module: 'unlockers', key: 'hammer', pass: true, amount: 1 });Событие для статистики на каждую выдачу из модуля. По нему видно,
что пропуск даёт людям и стоит ли своих денег. Модуль — из реестра
37.6, key — как в economy.json.
Отказы
Hezzl.error('asset_load', 'sprites.png не загрузился', false);Коды — из своего набора: asset_load, storage_write, webgl_lost,
runtime (раздел 38). message техническая, для нас, без личных данных.
Стенд
demo/centre.html — поддельный центр. Он говорит по тому же протоколу
и показывает каждое сообщение в обе стороны.
npx hezzl-sdk stand
# затем открыть http://localhost:8791/demo/centre.html
# из репозитория: node tools/serve.mjs 8791 sdkЧто стоит проверить на нём до сдачи:
| проверка | как | что должно быть |
|---|---|---|
| рукопожатие | открыть игру | ready от игры, ack в ответ |
| умения | посмотреть ready | supports совпадает с hezzl.json |
| настройки | выставить язык, тему, цвет, метку | применились при запуске |
| команды | нажать кнопки команд | игра отреагировала на каждую |
| частота | быстро набрать очки | score не чаще раза в секунду |
| финальный счёт | закончить партию | последний score совпал с показанным |
| хранилище | сыграть, перезапустить | прогресс на месте |
| отказ по квоте | галка «хранилище переполнено» | игра сказала, а не промолчала |
| центр молчит | снять галку «отвечать на ready» | режим standalone, кнопки центра скрыты |
| ready не позван | закомментировать Hezzl.ready() | заставка висит 20 с и сменяется отказом — так это увидит человек |
| прямой заход | открыть demo/game.html без стенда | игра работает, прогресс в своём хранилище |
| покупка | нажать цену товара | окно центра поверх игры, pause; после «списать» — награда и resume |
| нехватка | цена выше баланса | кошелёк с точной нехваткой; пополнить — покупка продолжится, закрыть — insufficient с числом |
| отмена | «отмена» в подтверждении | игра молчит, ничего не выдано |
| без валюты | снять галку «центр умеет валюту» | магазина и кнопок за валюту нет вовсе |
| сбой сервера | галка «сервер Balance отказывает» | unknown, баланс не тронут, товар не выдан |
| пропуск | «Открыть пропуск» в игре | окно оплаты центра, pause; после «Оплатить» замки открыты, resume |
| сутки | «Забрать», затем ещё раз | вторая награда — «завтра»; кнопка «+1 день» на стенде её открывает |
| срок пропуска | «срок вышел через 5 с» | через пять секунд замки вернулись сами, в логе пропуск: кончился |
| без пропуска у центра | снять галку «центр умеет пропуск» | модуля нет вовсе, ни замков, ни кнопки |
| задания | «Уровень пройден» трижды | в игре «Пройди 3 уровня 3/3 ✓», в стенде счётчик; «Пройден повторно» счётчик не двигает |
| повтор события | тот же eid дважды (консоль) | стенд пишет «повтор eid — не засчитан» |
| рейтинг | «Уровень пройден» с паузами | очки растут, приходит rank; быстрые нажатия подряд стенд отклоняет «быстрее бюджета времени» |
| заряд | «начать на 45 мин», затем «Подсказка» | кнопка стала «⚡ бесплатно (5)», списание уменьшает остаток; на нуле — empty, обычная покупка |
| конец заряда | «кончится через 5 с» | через пять секунд кнопка вернулась к цене сама |
Три строки, выделенные жирным, — самые полезные. Игра, которая ломается
без центра или молчит вместо ready, доезжает до приёмки чаще остальных,
а стоит проверки в одну минуту.
Чего SDK не делает
- не решает, какой язык показать: порядок ваш,
resolveтолько подсказывает; - не рисует интерфейс — ни кнопок, ни окон; окно подтверждения и кошелёк — тоже не его, а центра;
- не хранит цены и состав товаров: они у нас, приходят в
goods(); - не проверяет манифест: совпадение
hezzl.jsonиreadyсмотрим на приёмке; - не пишет в консоль. Отладочный вывод включается
debug: trueвinitи в собранной игре остаться не должен (раздел 38).
