@hezzlgames/sdk 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,481 @@
1
+ <!doctype html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Стенд: поддельный игровой центр</title>
7
+ <style>
8
+ :root{ --bg:#0e1018; --card:#171a27; --ink:#eef0fa; --muted:#9aa0bd; --line:#262a3d; --accent:#7d75ff; --coin:#ffb020 }
9
+ *{box-sizing:border-box}
10
+ body{
11
+ margin:0;min-height:100vh;background:var(--bg);color:var(--ink);
12
+ font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
13
+ display:grid;grid-template-columns:minmax(320px,420px) 1fr;gap:16px;padding:16px;
14
+ }
15
+ @media (max-width:860px){ body{grid-template-columns:1fr} }
16
+ h1{margin:0 0 4px;font-size:17px}
17
+ p.lead{margin:0 0 14px;color:var(--muted);font-size:13px}
18
+ .card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px;margin-bottom:12px}
19
+ .card h2{margin:0 0 10px;font-size:13px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}
20
+ .row{display:flex;flex-wrap:wrap;gap:6px}
21
+ button{font:inherit;font-weight:600;cursor:pointer;padding:7px 11px;border:1px solid var(--line);border-radius:9px;background:transparent;color:var(--ink)}
22
+ button.on{background:var(--accent);border-color:var(--accent);color:#fff}
23
+ label{display:flex;align-items:center;gap:7px;font-size:13px;color:var(--muted);margin-bottom:7px}
24
+ input[type=text],input[type=number]{font:inherit;flex:1;min-width:0;padding:6px 9px;border:1px solid var(--line);border-radius:8px;background:var(--bg);color:var(--ink)}
25
+ input[type=number]{max-width:110px;flex:0 1 auto}
26
+ table{width:100%;border-collapse:collapse;font-size:12.5px;color:var(--muted)}
27
+ td{padding:3px 0;border-top:1px solid var(--line)}
28
+ td:last-child{text-align:right;color:var(--ink);font-variant-numeric:tabular-nums}
29
+ .stage{position:relative;align-self:start} /* окна центра ложатся на кадр, а не на всю колонку */
30
+ iframe{width:100%;height:640px;border:1px solid var(--line);border-radius:14px;background:#fff;display:block}
31
+ #log{font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;max-height:300px;overflow:auto;margin:0;color:var(--muted)}
32
+ .in{color:#7ee787} .out{color:#79c0ff} .bad{color:#ff7b72} .hold{color:#e3b341}
33
+
34
+ /* ── окна центра поверх игры: подтверждение и кошелёк ─────────────── */
35
+ .cover{position:absolute;inset:0;border-radius:14px;background:rgba(8,10,18,.55);backdrop-filter:blur(6px);display:flex;align-items:flex-end;justify-content:center;padding:16px}
36
+ .cover[hidden]{display:none}
37
+ .sheet{width:min(420px,100%);background:var(--card);border:1px solid var(--line);border-radius:18px;padding:18px;box-shadow:0 20px 60px rgba(0,0,0,.5)}
38
+ .sheet h3{margin:0 0 6px;font-size:16px}
39
+ .sheet p{margin:0 0 14px;color:var(--muted)}
40
+ .sheet .row button{flex:1;padding:11px}
41
+ .coin{color:var(--coin);font-weight:700}
42
+ .wallet{width:min(460px,100%);max-height:100%;overflow:auto}
43
+ .wallet .need{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
44
+ .wallet .x{width:44px;height:44px;border-radius:50%;padding:0;font-size:20px}
45
+ .wallet h4{margin:14px 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}
46
+ .pack{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:10px 12px;border:1px solid var(--line);border-radius:12px;margin-bottom:6px}
47
+ .pack b{font-size:15px}
48
+ .pack small{color:var(--muted);display:block}
49
+ .pack button{white-space:nowrap}
50
+ .pack.done{opacity:.5}
51
+ </style>
52
+ </head>
53
+ <body>
54
+
55
+ <div>
56
+ <h1>Поддельный игровой центр</h1>
57
+ <p class="lead">
58
+ Стенд для проверки игры до сдачи. Говорит по тому же протоколу, что
59
+ настоящий центр, и показывает каждое сообщение в обе стороны.
60
+ </p>
61
+
62
+ <div class="card">
63
+ <h2>Запуск</h2>
64
+ <label>адрес игры <input type="text" id="src" value="game.html"></label>
65
+ <label>язык
66
+ <button data-url="lang" data-value="">система</button>
67
+ <button data-url="lang" data-value="ru">ru</button>
68
+ <button data-url="lang" data-value="en">en</button>
69
+ </label>
70
+ <label>тема
71
+ <button data-url="theme" data-value="">система</button>
72
+ <button data-url="theme" data-value="light">светлая</button>
73
+ <button data-url="theme" data-value="dark">тёмная</button>
74
+ </label>
75
+ <label>цвет <input type="text" id="accent" placeholder="e30611" maxlength="6"></label>
76
+ <label>метка <input type="text" id="ref" placeholder="rutube"></label>
77
+ <div class="row">
78
+ <button id="muted">звук выключен</button>
79
+ <button id="nohaptics">вибрация выключена</button>
80
+ <button class="on" id="run">Открыть игру</button>
81
+ </div>
82
+ </div>
83
+
84
+ <div class="card">
85
+ <h2>Команды игре</h2>
86
+ <div class="row">
87
+ <button data-cmd="mute" data-arg="1">заглушить</button>
88
+ <button data-cmd="mute" data-arg="0">вернуть звук</button>
89
+ <button data-cmd="haptics" data-arg="0">без вибрации</button>
90
+ <button data-cmd="pause">пауза</button>
91
+ <button data-cmd="resume">продолжить</button>
92
+ <button data-cmd="restart">заново</button>
93
+ <button data-cmd="setlang" data-arg="en">язык en</button>
94
+ <button data-cmd="settheme" data-arg="dark">тема тёмная</button>
95
+ <button data-cmd="setaccent" data-arg="e30611">цвет МТС</button>
96
+ <button data-cmd="signedin">вошёл</button>
97
+ <button data-cmd="signedout">вышел</button>
98
+ <button data-cmd="profilepush">профиль изменился</button>
99
+ </div>
100
+ </div>
101
+
102
+ <div class="card">
103
+ <h2>Как отвечать</h2>
104
+ <label><input type="checkbox" id="doack" checked> отвечать на ready</label>
105
+ <label><input type="checkbox" id="dostore" checked> держать хранилище</label>
106
+ <label><input type="checkbox" id="signed" checked> человек вошёл</label>
107
+ <label><input type="checkbox" id="quota"> хранилище переполнено</label>
108
+ </div>
109
+
110
+ <div class="card">
111
+ <h2>Валюта центра</h2>
112
+ <label><input type="checkbox" id="domoney" checked> центр умеет валюту (<code>balance</code> в ack)</label>
113
+ <label><input type="checkbox" id="confirm" checked> подтверждать каждое списание</label>
114
+ <label><input type="checkbox" id="broken"> сервер Balance отказывает</label>
115
+ <label>на балансе <input type="number" id="amount" value="120" min="0" step="10"> <span class="coin">⬡</span></label>
116
+ <div class="row">
117
+ <button id="task">+50 за задание</button>
118
+ <button id="walletpush">открыть кошелёк самим</button>
119
+ </div>
120
+ <table id="goods" style="margin-top:10px"></table>
121
+ </div>
122
+
123
+ <div class="card">
124
+ <h2>Обмен</h2>
125
+ <pre id="log"></pre>
126
+ </div>
127
+ </div>
128
+
129
+ <div class="stage">
130
+ <iframe id="frame" title="игра"></iframe>
131
+
132
+ <!-- Подтверждение списания. Окно центра, не игры: игра не может
133
+ ни обойти его, ни подделать — оно рисуется над кадром. -->
134
+ <div class="cover" id="ask" hidden>
135
+ <div class="sheet" role="dialog" aria-modal="true">
136
+ <h3 id="ask-title">Купить?</h3>
137
+ <p id="ask-text"></p>
138
+ <div class="row">
139
+ <button id="ask-no">Отмена</button>
140
+ <button class="on" id="ask-yes">Списать</button>
141
+ </div>
142
+ </div>
143
+ </div>
144
+
145
+ <!-- Кошелёк: где взять валюту. Наборы за деньги и задания. -->
146
+ <div class="cover" id="wallet" hidden>
147
+ <div class="sheet wallet" role="dialog" aria-modal="true">
148
+ <div class="need">
149
+ <div>
150
+ <h3 id="wallet-title">Кошелёк</h3>
151
+ <p id="wallet-text" style="margin:0"></p>
152
+ </div>
153
+ <button class="x" id="wallet-close" aria-label="Закрыть">×</button>
154
+ </div>
155
+ <h4>Наборы</h4>
156
+ <div id="packs"></div>
157
+ <h4>Задания — бесплатно</h4>
158
+ <div id="tasks"></div>
159
+ </div>
160
+ </div>
161
+ </div>
162
+
163
+ <script>
164
+ (function () {
165
+ var el = function (id) { return document.getElementById(id); };
166
+ var frame = el('frame');
167
+ var url = { lang: '', theme: '' };
168
+ var box = {}; /* поддельное хранилище центра */
169
+
170
+ /* ── валюта: то, что настоящий центр берёт из сервиса Balance ───── */
171
+ var CURRENCY = { key: 'hezzl', title: 'Хеззлы', icon: '⬡' };
172
+
173
+ /* Товары этой игры, как они заведены в кампании игры (Goods).
174
+ Цена — в валюте центра, награда — ресурсы игры. */
175
+ var GOODS = [
176
+ { id: 1, title: 'Бустер ×3', price: 50, award: { boosters: 3 }, enabled: true },
177
+ { id: 2, title: 'Пять жизней', price: 120, award: { lives: 5 }, enabled: true },
178
+ { id: 3, title: 'Скин «Неон»', price: 300, award: { skin: 'neon' }, enabled: true },
179
+ { id: 4, title: 'Старый набор', price: 10, award: {}, enabled: false }
180
+ ];
181
+ /* Товары кампании центра — валюта за деньги; на стенде нажатие
182
+ сразу зачисляет. */
183
+ var PACKS = [
184
+ { amount: 100, price: '99 ₽' },
185
+ { amount: 550, price: '449 ₽', note: 'выгоднее на 10%' },
186
+ { amount: 1200, price: '899 ₽', note: 'выгоднее на 25%' }
187
+ ];
188
+ var TASKS = [
189
+ { key: 'play3', title: 'Сыграть три партии', gain: 20 },
190
+ { key: 'email', title: 'Привязать почту', gain: 50 }
191
+ ];
192
+ var doneTasks = {};
193
+
194
+ function amount() { return Math.max(0, parseInt(el('amount').value, 10) || 0); }
195
+ function setAmount(v) { el('amount').value = Math.max(0, v); }
196
+
197
+ function log(kind, text) {
198
+ var line = document.createElement('span');
199
+ line.className = kind;
200
+ line.textContent = (kind === 'in' ? '← ' : kind === 'out' ? '→ ' : kind === 'hold' ? '… ' : '! ') + text + '\n';
201
+ el('log').insertBefore(line, el('log').firstChild);
202
+ }
203
+
204
+ function toGame(type, extra) {
205
+ var msg = { type: 'hezzl:' + type };
206
+ if (extra) for (var k in extra) if (extra.hasOwnProperty(k)) msg[k] = extra[k];
207
+ frame.contentWindow.postMessage(msg, '*');
208
+ log(extra && extra.hold ? 'hold' : 'out', type + ' ' + JSON.stringify(extra || {}));
209
+ }
210
+
211
+ el('goods').innerHTML = GOODS.map(function (g) {
212
+ return '<tr' + (g.enabled ? '' : ' style="opacity:.5"') + '><td>#' + g.id + '</td><td>' + g.title
213
+ + (g.enabled ? '' : ' (выключен)') + '</td><td>' + g.price + ' ⬡</td></tr>';
214
+ }).join('');
215
+
216
+ /* ── запуск ──────────────────────────────────────────────────────── */
217
+ document.querySelectorAll('[data-url]').forEach(function (b) {
218
+ b.onclick = function () {
219
+ var name = b.getAttribute('data-url');
220
+ url[name] = b.getAttribute('data-value');
221
+ document.querySelectorAll('[data-url="' + name + '"]').forEach(function (o) { o.classList.remove('on'); });
222
+ b.classList.add('on');
223
+ };
224
+ });
225
+ el('muted').onclick = function () { this.classList.toggle('on'); };
226
+ el('nohaptics').onclick = function () { this.classList.toggle('on'); };
227
+
228
+ el('run').onclick = function () {
229
+ var q = [];
230
+ if (url.lang) q.push('lang=' + url.lang);
231
+ if (url.theme) q.push('theme=' + url.theme);
232
+ if (el('accent').value.trim()) q.push('accent=' + el('accent').value.trim());
233
+ if (el('ref').value.trim()) q.push('ref=' + el('ref').value.trim());
234
+ if (el('muted').classList.contains('on')) q.push('muted=1');
235
+ if (el('nohaptics').classList.contains('on')) q.push('haptics=0');
236
+ el('log').textContent = '';
237
+ closeAll();
238
+ frame.src = el('src').value + (q.length ? '?' + q.join('&') : '');
239
+ log('out', 'открываю ' + frame.src);
240
+ };
241
+
242
+ /* ── команды ─────────────────────────────────────────────────────── */
243
+ document.querySelectorAll('[data-cmd]').forEach(function (b) {
244
+ b.onclick = function () {
245
+ var cmd = b.getAttribute('data-cmd');
246
+ var arg = b.getAttribute('data-arg');
247
+ if (cmd === 'mute') toGame('mute', { muted: arg === '1' });
248
+ else if (cmd === 'haptics') toGame('haptics', { enabled: arg === '1' });
249
+ else if (cmd === 'setlang') toGame('setlang', { lang: arg });
250
+ else if (cmd === 'settheme') toGame('settheme', { theme: arg });
251
+ else if (cmd === 'setaccent') toGame('setaccent', { accent: arg });
252
+ else if (cmd === 'signedin') toGame('signedin', { userId: 'u_demo' });
253
+ else if (cmd === 'signedout') toGame('signedout', {});
254
+ else if (cmd === 'profilepush') toGame('profile', { signedIn: true, profile: { userId: 'u_demo', name: 'Саша', avatar: 'about:blank' } });
255
+ else toGame(cmd, {});
256
+ };
257
+ });
258
+
259
+ /* Награда за задание пришла, пока игра открыта: центр говорит
260
+ об этом сам — так игра обновит счётчик, не опрашивая баланс. */
261
+ el('task').onclick = function () {
262
+ setAmount(amount() + 50);
263
+ toGame('balance', { signedIn: el('signed').checked, amount: amount(), currency: CURRENCY });
264
+ };
265
+ el('walletpush').onclick = function () {
266
+ openWallet(0, '', function (bought) {
267
+ toGame('wallet', { closed: true, amount: amount(), bought: bought });
268
+ });
269
+ };
270
+
271
+ /* ── окна поверх игры ────────────────────────────────────────────── */
272
+ /* Пока открыто хоть одно окно центра, игра стоит на паузе (3.3).
273
+ Кошелёк и подтверждение могут идти друг за другом — пауза одна. */
274
+ var covered = 0;
275
+ function cover() { if (covered++ === 0) toGame('pause', {}); }
276
+ function uncover() { if (--covered <= 0) { covered = 0; toGame('resume', {}); } }
277
+ function closeAll() { el('ask').hidden = true; el('wallet').hidden = true; covered = 0; }
278
+
279
+ var asking = null;
280
+ function openSheet(title, cost, then) {
281
+ el('ask-title').textContent = title;
282
+ el('ask-text').innerHTML = 'Спишем <span class="coin">' + cost + ' ' + CURRENCY.icon + '</span> с баланса. Останется ' + (amount() - cost) + '.';
283
+ el('ask').hidden = false;
284
+ cover();
285
+ asking = then;
286
+ }
287
+ function answerSheet(yes) {
288
+ if (!asking) return;
289
+ var then = asking; asking = null;
290
+ el('ask').hidden = true;
291
+ uncover();
292
+ then(yes);
293
+ }
294
+ el('ask-yes').onclick = function () { answerSheet(true); };
295
+ el('ask-no').onclick = function () { answerSheet(false); };
296
+
297
+ var walletThen = null, walletBought = false;
298
+ function openWallet(need, reason, then) {
299
+ walletThen = then; walletBought = false;
300
+ el('wallet-title').textContent = need > 0 ? 'Не хватает ' + need + ' ' + CURRENCY.icon : 'Кошелёк';
301
+ el('wallet-text').textContent = 'На балансе ' + amount() + ' ' + CURRENCY.icon + (reason ? ' · ' + reason : '');
302
+ renderWallet();
303
+ el('wallet').hidden = false;
304
+ cover();
305
+ }
306
+ function renderWallet() {
307
+ el('packs').innerHTML = PACKS.map(function (p, i) {
308
+ return '<div class="pack"><div><b>' + p.amount + ' ' + CURRENCY.icon + '</b>' + (p.note ? '<small>' + p.note + '</small>' : '')
309
+ + '</div><button class="on" data-pack="' + i + '">' + p.price + '</button></div>';
310
+ }).join('');
311
+ el('tasks').innerHTML = TASKS.map(function (t, i) {
312
+ var done = doneTasks[t.key];
313
+ return '<div class="pack' + (done ? ' done' : '') + '"><div><b>' + t.title + '</b><small>+' + t.gain + ' ' + CURRENCY.icon + '</small></div>'
314
+ + '<button data-task="' + i + '"' + (done ? ' disabled' : '') + '>' + (done ? 'получено' : 'выполнить') + '</button></div>';
315
+ }).join('');
316
+ }
317
+ el('wallet').addEventListener('click', function (e) {
318
+ var b = e.target.closest('button');
319
+ if (!b) return;
320
+ if (b.hasAttribute('data-pack')) {
321
+ var p = PACKS[b.getAttribute('data-pack')];
322
+ setAmount(amount() + p.amount);
323
+ walletBought = true;
324
+ log('bad', 'стенд: оплата «' + p.price + '» считается прошедшей, +' + p.amount);
325
+ /* Баланс изменился, пока кошелёк открыт, — игра узнаёт сразу,
326
+ а не после закрытия: счётчик за окном обновляется на глазах. */
327
+ toGame('balance', { signedIn: true, amount: amount(), currency: CURRENCY });
328
+ renderWallet();
329
+ el('wallet-text').textContent = 'На балансе ' + amount() + ' ' + CURRENCY.icon;
330
+ } else if (b.hasAttribute('data-task')) {
331
+ var t = TASKS[b.getAttribute('data-task')];
332
+ doneTasks[t.key] = true;
333
+ setAmount(amount() + t.gain);
334
+ walletBought = true;
335
+ toGame('balance', { signedIn: true, amount: amount(), currency: CURRENCY });
336
+ renderWallet();
337
+ el('wallet-text').textContent = 'На балансе ' + amount() + ' ' + CURRENCY.icon;
338
+ }
339
+ });
340
+ el('wallet-close').onclick = function () {
341
+ if (!walletThen) return;
342
+ var then = walletThen; walletThen = null;
343
+ el('wallet').hidden = true;
344
+ uncover();
345
+ then(walletBought);
346
+ };
347
+
348
+ /* ── покупка и списание ──────────────────────────────────────────── */
349
+ /* Одна дорожка на оба кейса. Разница только в том, откуда цена:
350
+ buy — из товара, заведённого у нас; spend — из самой игры. */
351
+ function purchase(type, d) {
352
+ var p = d.payload || {};
353
+ var rid = d.rid;
354
+ var cost, title, good = null;
355
+
356
+ function refuse(error, extra) {
357
+ var msg = { rid: rid, ok: false, error: error, amount: amount() };
358
+ if (extra) for (var k in extra) msg[k] = extra[k];
359
+ toGame(type, msg);
360
+ }
361
+
362
+ if (type === 'buy') {
363
+ for (var i = 0; i < GOODS.length; i++) if (String(GOODS[i].id) === String(p.goodId)) good = GOODS[i];
364
+ if (!good) { refuse('notfound'); return; }
365
+ if (!good.enabled) { refuse('disabled'); return; }
366
+ cost = good.price; title = good.title;
367
+ } else {
368
+ cost = p.amount; title = p.item || 'Покупка в игре';
369
+ if (!(cost > 0)) { refuse('invalid'); return; }
370
+ }
371
+
372
+ /* Без человека нет и баланса. Настоящий центр здесь поднимет
373
+ вход с hold: 'signin'; стенд отвечает сразу. */
374
+ if (!el('signed').checked) { refuse('signin', { amount: 0 }); return; }
375
+
376
+ step1();
377
+
378
+ function step1() {
379
+ if (amount() >= cost) { step2(); return; }
380
+ var need = cost - amount();
381
+ if (p.wallet === false) { refuse('insufficient', { need: need }); return; }
382
+ toGame(type, { rid: rid, hold: 'wallet' });
383
+ openWallet(need, title, function (bought) {
384
+ /* О закрытии кошелька игра узнаёт всегда — и когда просила
385
+ его сама, и когда его открыл центр по нехватке. */
386
+ toGame('wallet', { closed: true, amount: amount(), bought: bought });
387
+ if (amount() < cost) { refuse('insufficient', { need: cost - amount() }); return; }
388
+ step2();
389
+ });
390
+ }
391
+
392
+ function step2() {
393
+ if (!el('confirm').checked) { finish(); return; }
394
+ toGame(type, { rid: rid, hold: 'confirm' });
395
+ openSheet(title, cost, function (yes) {
396
+ if (!yes) { refuse('cancelled'); return; }
397
+ finish();
398
+ });
399
+ }
400
+
401
+ function finish() {
402
+ if (el('broken').checked) { refuse('unknown', { message: 'errors.common.internal' }); return; }
403
+ setAmount(amount() - cost);
404
+ if (type === 'buy') {
405
+ toGame('buy', { rid: rid, ok: true, goodId: good.id, amount: amount(), award: good.award,
406
+ issued: { id: Date.now() % 100000, rewardId: good.id, status: 'issued' } });
407
+ } else {
408
+ toGame('spend', { rid: rid, ok: true, amount: amount(), spent: cost });
409
+ }
410
+ }
411
+ }
412
+
413
+ /* ── ответы игре ─────────────────────────────────────────────────── */
414
+ window.addEventListener('message', function (event) {
415
+ if (event.source !== frame.contentWindow) return;
416
+ var d = event.data;
417
+ if (!d || d.source !== 'hezzl-game') return;
418
+ log('in', d.type + ' ' + JSON.stringify(d.payload || {}) + (d.rid ? ' #' + d.rid : ''));
419
+
420
+ if (d.type === 'ready') {
421
+ if (!el('doack').checked) { log('bad', 'ack не отправлен — игра должна уйти в standalone'); return; }
422
+ var can = ['share', 'support', 'exit', 'auth', 'profile'];
423
+ if (el('dostore').checked) can.push('storage');
424
+ var ack = { sdk: 1, centreSupports: can };
425
+ if (el('domoney').checked) { can.push('balance'); ack.currency = CURRENCY; }
426
+ toGame('ack', ack);
427
+ return;
428
+ }
429
+
430
+ if (d.type === 'storage') {
431
+ var p = d.payload || {};
432
+ if (!el('dostore').checked) return; /* молчим — игра должна отвалиться по своему сроку */
433
+ if (el('quota').checked && p.op === 'set') { toGame('storage', { rid: d.rid, error: 'quota' }); return; }
434
+ if (p.op === 'get') toGame('storage', { rid: d.rid, value: box[p.key] === undefined ? null : box[p.key] });
435
+ else if (p.op === 'set') { box[p.key] = p.value; toGame('storage', { rid: d.rid }); }
436
+ else if (p.op === 'remove') { delete box[p.key]; toGame('storage', { rid: d.rid }); }
437
+ else if (p.op === 'keys') toGame('storage', { rid: d.rid, value: Object.keys(box) });
438
+ return;
439
+ }
440
+
441
+ if (d.type === 'auth') {
442
+ toGame('auth', { rid: d.rid, signedIn: el('signed').checked, code: el('signed').checked ? 'ac_demo' : undefined });
443
+ return;
444
+ }
445
+
446
+ if (d.type === 'profile') {
447
+ if (!el('signed').checked) { toGame('profile', { rid: d.rid, signedIn: false }); return; }
448
+ toGame('profile', { rid: d.rid, signedIn: true, profile: { userId: 'u_demo', name: 'Саша', avatar: 'about:blank' } });
449
+ return;
450
+ }
451
+
452
+ /* Валюта. Если центр её не объявил, он и не отвечает: игра должна
453
+ была не спрашивать (can('balance')), а SDK — отказать сам. */
454
+ if (d.type === 'balance' || d.type === 'goods' || d.type === 'buy' || d.type === 'spend' || d.type === 'wallet') {
455
+ if (!el('domoney').checked) { log('bad', 'валюта не объявлена в ack — молчим'); return; }
456
+ if (d.type === 'balance') {
457
+ var signed = el('signed').checked;
458
+ toGame('balance', { rid: d.rid, signedIn: signed, amount: signed ? amount() : 0, currency: CURRENCY });
459
+ } else if (d.type === 'goods') {
460
+ toGame('goods', { rid: d.rid, currency: CURRENCY, goods: GOODS.filter(function (g) { return g.enabled; }).map(function (g) {
461
+ return { id: g.id, title: g.title, picture: '', price: g.price, award: g.award };
462
+ }) });
463
+ } else if (d.type === 'wallet') {
464
+ var ask = d.payload || {};
465
+ openWallet(ask.need || 0, ask.reason || '', function (bought) {
466
+ toGame('wallet', { rid: d.rid, closed: true, amount: amount(), bought: bought });
467
+ });
468
+ } else {
469
+ purchase(d.type, d);
470
+ }
471
+ return;
472
+ }
473
+
474
+ if (d.type === 'exit') log('bad', 'игра просит закрыть себя — настоящий центр вернул бы в каталог');
475
+ if (d.type === 'error') log('bad', 'отказ в игре: ' + JSON.stringify(d.payload));
476
+ });
477
+ })();
478
+ </script>
479
+
480
+ </body>
481
+ </html>
@@ -0,0 +1,8 @@
1
+ {
2
+ "boosters": { "hint": 10, "hammer": 40, "rocket": 40, "shuffle": 40 },
3
+ "continue": [50, 100, 1500],
4
+ "levels": 60,
5
+ "chests": [
6
+ { "stars": 15, "boosters": [ { "key": "hammer", "count": 1, "chance": 0.5 }, { "key": "rocket", "count": 1, "chance": 0.5 } ] }
7
+ ]
8
+ }