npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hezzlgames/sdk

v1.5.0

Published

Hezzl Games SDK: договор игры с игровым центром play.hezzl.ru — настройки, рукопожатие, хранилище, валюта центра

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; у taskstask, и на каждое задание две записи: 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.amount

currency() синхронный: значок и название приходят с рукопожатием, сумма — с первым же ответом, где она есть. Рисовать счётчик можно сразу после 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).