|
<!DOCTYPE html> |
|
<html lang="en"> |
|
|
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<meta http-equiv="X-UA-Compatible" content="ie=edge"> |
|
<title>Hampster kombaint cheats</title> |
|
<link rel="icon" href="https://hamsterkombatgame.io/favicon.ico" type="image/x-icon"> |
|
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" |
|
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> |
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" |
|
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" |
|
crossorigin="anonymous"></script> |
|
|
|
<style> |
|
.container { |
|
background-color: #fff; |
|
border-radius: 10px; |
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); |
|
padding: 20px; |
|
text-align: center; |
|
} |
|
|
|
.qrcode-container { |
|
text-align: center; |
|
width: 256px; |
|
height: 256px; |
|
margin: 0 auto; |
|
} |
|
|
|
.generate-btn { |
|
margin: 5px; |
|
} |
|
|
|
@keyframes blink { |
|
50% { |
|
opacity: 0.6; |
|
background-color: rgb(21, 92, 0); |
|
} |
|
} |
|
|
|
.blink { |
|
animation: blink 2.75s infinite; |
|
} |
|
|
|
#result-tube { |
|
background-color: #cce1f7; |
|
border-radius: 10px; |
|
padding: 10px; |
|
margin: 20px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
|
|
#hampster, |
|
#forsen-cd { |
|
border-radius: 10px; |
|
margin-bottom: 20px; |
|
} |
|
|
|
#forsen-cd { |
|
width: 50px; |
|
height: 50px; |
|
margin-left: 10px; |
|
} |
|
|
|
#log { |
|
font-family: monospace; |
|
} |
|
</style> |
|
</head> |
|
|
|
<body> |
|
<main class="container"> |
|
<h1>Hampster kombaint cheats |
|
<img id="forsen-cd" |
|
src="https://static-cdn.jtvnw.net/jtv_user_pictures/519ee1f3-6723-4237-9fa3-bf33aae5c8f2-profile_image-300x300.png" |
|
alt="forsenCD"> |
|
✌️ |
|
</h1> |
|
|
|
<img id="hampster" src="https://qph.cf2.quoracdn.net/main-qimg-f9f787c5820fbc13c83127345a419785.webp" |
|
alt="Hampster" class="img-fluid"> |
|
|
|
<div id="result-tube" class="d-none"> |
|
<h3 id="result-holder">...</h3> |
|
<button id="result-copy" class="btn btn-secondary m-2" onclick="copy()">📋</button> |
|
</div> |
|
|
|
<div id="qrcode-container" class="qrcode-container mt-2 mb-2 d-none"> |
|
<div id="qrcode"></div> |
|
</div> |
|
|
|
<div class="generator row"> |
|
<div class="generator-buttons col border"> |
|
<h4>Games</h4> |
|
<p>Select a game below to start the key generation</p> |
|
</div> |
|
|
|
<div class="generator-settings col border"> |
|
<h4>Delay settings</h4> |
|
<p>Extend key generation delays to lower the risk for detection. Longer delays better simulate real playtime. A |
|
sound will play once the next key is ready. Hover over any game button to see estimated generation time</p> |
|
|
|
<div class="input-group mb-3"> |
|
<label class="input-group-text" for="delay">Multiplier</label> |
|
<select class="form-select" id="delay"> |
|
<option value="0.5">0.5x - Fast delay (very risky, might crash)</option> |
|
<option value="0.85">0.85x - Faster delay (risky)</option> |
|
<option value="1" selected>1x - Realistic delay</option> |
|
<option value="1.5">1.5x - Long delay</option> |
|
<option value="2">2x - Very long delay</option> |
|
<option value="custom">Custom delay multiplier</option> |
|
</select> |
|
</div> |
|
|
|
<div class="input-group mb-3 d-none" id="custom-delay"> |
|
<label class="input-group-text" for="custom-delay-input">Custom delay multiplier</label> |
|
<input type="number" class="form-control" id="custom-delay-input" min="0" step="0.1" value="1"> |
|
</div> |
|
|
|
<div class="input-group mb-3" id="eta"> |
|
<label class="input-group-text" for="min-max-eta">Estimated time range</label> |
|
<input id="min-max-eta" class="form-control" value="ETA" disabled></input> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="log-area p-5"> |
|
<h3>🏆 Champions Log 🏆</h3> |
|
<textarea id="log" class="form-control" rows="12" readonly></textarea> |
|
</div> |
|
</main> |
|
|
|
<script> |
|
class Logger { |
|
static formatMessage() { |
|
return Array.from(arguments).map(arg => |
|
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg |
|
).join(' '); |
|
} |
|
|
|
static debug() { |
|
if (!DEBUG) { |
|
return; |
|
} |
|
|
|
const msg = Logger.formatMessage.apply(null, arguments); |
|
console.log(msg); |
|
logToTextArea(msg); |
|
} |
|
|
|
static info() { |
|
const msg = Logger.formatMessage.apply(null, arguments); |
|
console.info(msg); |
|
logToTextArea(msg); |
|
} |
|
|
|
static panic() { |
|
const msg = Logger.formatMessage.apply(null, arguments); |
|
console.error(msg); |
|
logToTextArea(msg); |
|
} |
|
} |
|
|
|
class GamePromo { |
|
constructor() { |
|
this.authToken = null; |
|
this.config = {}; |
|
this.hasCode = false; |
|
this.key = null; |
|
this.origin = 'android'; |
|
this.eventCounter = 0; |
|
this.authTokenCache = {}; |
|
} |
|
|
|
async fetchApi(path, body = null, retry = 0) { |
|
const headers = { |
|
accept: '*/*', |
|
'accept-encoding': 'deflate, gzip', |
|
'content-type': 'application/json', |
|
}; |
|
|
|
if (this.authToken !== null) { |
|
headers.authorization = `Bearer ${this.authToken}`; |
|
} |
|
|
|
if (this.config['user-agent'] !== undefined) { |
|
headers['user-agent'] = this.config['user-agent']; |
|
} |
|
|
|
if (this.config['unity-version'] !== undefined) { |
|
headers['x-unity-version'] = this.config['unity-version']; |
|
} |
|
|
|
const url = `https://api.gamepromo.io${path}`; |
|
|
|
const options = { |
|
method: 'POST', |
|
cache: 'no-store', |
|
headers, |
|
body: JSON.stringify(body), |
|
}; |
|
|
|
Logger.debug('Request:', url, options); |
|
let res; |
|
|
|
try { |
|
res = await fetch(url, options); |
|
} catch (err) { |
|
if (retry < SERVER_ERROR_RETRIES) { |
|
Logger.info('Network error, will retry after cooldown period.'); |
|
Logger.debug(err); |
|
|
|
await globalDelay(SERVER_ERROR_COOLDOWN); |
|
return this.fetchApi(path, body, retry + 1); |
|
} |
|
|
|
throw err; |
|
} |
|
|
|
if (!res.ok) { |
|
if (DEBUG) { |
|
const text = await res.text(); |
|
Logger.debug('Response:', text); |
|
} |
|
|
|
if (retry < SERVER_ERROR_RETRIES) { |
|
Logger.info('Received internal server error, will retry after cooldown period.'); |
|
await globalDelay(SERVER_ERROR_COOLDOWN); |
|
return this.fetchApi(path, body, retry + 1); |
|
} |
|
|
|
throw new Error(`${res.status} ${res.statusText}`); |
|
} |
|
|
|
const data = await res.json(); |
|
Logger.debug('Response:', data); |
|
return data; |
|
} |
|
|
|
async configSet(key, value) { |
|
this.config[key] = value; |
|
} |
|
|
|
async loginFetch(data) { |
|
const gameId = this.config['game-id']; |
|
|
|
if (this.authTokenCache[gameId] !== undefined) { |
|
this.authToken = this.authTokenCache[gameId]; |
|
Logger.debug(`Skipping login for ${gameId}, using cached auth token: ${this.authToken}`); |
|
return; |
|
} |
|
|
|
const res = await this.fetchApi('/promo/login-client', { |
|
appToken: this.config['app-token'], |
|
...data, |
|
}); |
|
|
|
if (typeof res.clientToken === 'string' && res.clientToken !== '') { |
|
this.authToken = res.clientToken; |
|
this.authTokenCache[gameId] = res.clientToken; |
|
} |
|
} |
|
|
|
async eventFetch(data) { |
|
Logger.debug(`Registering event #${++this.eventCounter}`); |
|
const promoId = this.config['promo-id']; |
|
// on ios promoId is sent as first property, on android it's sent last |
|
const payload = this.origin === 'ios' ? { promoId, ...data } : { ...data, promoId }; |
|
const res = await this.fetchApi('/promo/register-event', payload); |
|
|
|
if (res.hasCode === true) { |
|
this.hasCode = true; |
|
} |
|
} |
|
|
|
async collectFetch() { |
|
const res = await this.fetchApi('/promo/create-code', { |
|
promoId: this.config['promo-id'], |
|
}); |
|
|
|
if (typeof res.promoCode === 'string' && res.promoCode !== '') { |
|
this.key = res.promoCode; |
|
} |
|
} |
|
|
|
async getCode(gameKey, delayMultiplier) { |
|
this.authToken = null; |
|
this.config = {}; |
|
this.hasCode = false; |
|
this.key = null; |
|
this.origin = 'android'; |
|
this.eventCounter = 0; |
|
Logger.debug('origin:', this.origin); |
|
|
|
await GAMES[gameKey]['payload']({ |
|
collect: this.collectFetch.bind(this), |
|
delay: async (ms) => { |
|
const totalMs = Math.floor(ms * (Math.random() / 4 + 1)); |
|
await globalDelay(totalMs * delayMultiplier); |
|
}, |
|
id: globalId, |
|
instance: this, |
|
login: this.loginFetch.bind(this), |
|
event: this.eventFetch.bind(this), |
|
origin: this.origin, |
|
setup: this.configSet.bind(this), |
|
}); |
|
|
|
if (this.key === null) { |
|
throw new Error(`Unable to get ${gameKey} promo.`); |
|
} |
|
|
|
return this.key; |
|
} |
|
} |
|
|
|
async function globalDelay(ms) { |
|
const minutes = Math.floor(ms / 60000); |
|
|
|
if (minutes > 0) { |
|
Logger.debug(`Waiting ~${minutes} ${minutesFmt(minutes)}`); |
|
} |
|
else { |
|
Logger.debug(`Waiting ${Math.floor(ms / 1000)} seconds`); |
|
} |
|
|
|
return new Promise((resolve) => { |
|
setTimeout(resolve, ms); |
|
}); |
|
} |
|
|
|
function maybeCachedClientId(gameKey) { |
|
return localStorage.getItem(`clientId-${gameKey}`); |
|
} |
|
|
|
function cacheClientId(gameKey, clientId) { |
|
if (clientId === null) { |
|
return; |
|
} |
|
|
|
Logger.debug(`Storing new client ID for ${gameKey}: ${clientId}`); |
|
localStorage.setItem(`clientId-${gameKey}`, clientId); |
|
} |
|
|
|
function globalId(type, game = null) { |
|
if (game !== null) { |
|
var cachedClientId = maybeCachedClientId(game); |
|
if (cachedClientId !== null) { |
|
if (type === 'ts7d' || type === 'ts19d') { |
|
const timestamp = Date.now(); |
|
cachedClientId = `${timestamp}-${cachedClientId}`; |
|
} |
|
|
|
Logger.debug(`Using cached client ID for ${game}: ${cachedClientId}`); |
|
return cachedClientId; |
|
} |
|
} |
|
|
|
var newId, idToCache; |
|
|
|
switch (type) { |
|
case 'rand16': { |
|
newId = Array.from( |
|
crypto.getRandomValues(new Uint8Array(8)), |
|
(it) => it.toString(16).padStart(2, '0'), |
|
).join(''); |
|
idToCache = newId; |
|
break; |
|
} |
|
case 'rand32': { |
|
newId = Array.from( |
|
crypto.getRandomValues(new Uint8Array(16)), |
|
(it) => it.toString(16).padStart(2, '0'), |
|
).join(''); |
|
idToCache = newId; |
|
break; |
|
} |
|
case 'uuid': |
|
case 'uuid-upper': { |
|
const val = '10000000-1000-4000-8000-100000000000'.replace( |
|
/[018]/g, |
|
(c) => (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16), |
|
); |
|
|
|
newId = type === 'uuid-upper' ? val.toUpperCase() : val; |
|
idToCache = newId; |
|
break; |
|
} |
|
case 'ts': { |
|
newId = Date.now().toString(); |
|
idToCache = null; |
|
break; |
|
} |
|
case 'ts7d': |
|
case 'ts19d': { |
|
const timestamp = Date.now(); |
|
const buf = Array(type === 'ts7d' ? 7 : 19).fill(); |
|
const digits = buf.map(() => Math.floor(Math.random() * 10)).join(''); |
|
newId = `${timestamp}-${digits}`; |
|
idToCache = digits; |
|
break; |
|
} |
|
default: { |
|
throw new Error(`Tried generating unknown id '${type}'.`); |
|
} |
|
} |
|
|
|
if (game !== null) { |
|
cacheClientId(game, idToCache); |
|
} |
|
|
|
return newId; |
|
} |
|
|
|
function minutesFmt(minutes) { |
|
return minutes == 1 ? 'minute' : 'minutes'; |
|
} |
|
|
|
const GP = new GamePromo(); |
|
const DEBUG = true; |
|
const SERVER_ERROR_COOLDOWN = 300_000; |
|
const SERVER_ERROR_RETRIES = 3; |
|
const WITH_REINSTALL_TIME = false; |
|
|
|
const GAMES = { |
|
CAFE: { |
|
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => { |
|
setup('game-id', 'CAFE'); |
|
setup('app-token', 'bc0971b8-04df-4e72-8a3e-ec4dc663cd11'); |
|
setup('promo-id', 'bc0971b8-04df-4e72-8a3e-ec4dc663cd11'); |
|
|
|
await login({ clientId: id('rand16', 'CAFE'), clientOrigin: origin, clientVersion: '2.24.0' }); |
|
|
|
while (!instance.hasCode) { |
|
await delay(90_000); |
|
await event({ eventId: id('ts'), eventOrigin: 'undefined', eventType: '5visitorsChecks' }); |
|
} |
|
|
|
await collect(); |
|
}, |
|
'meta': { |
|
'id': 'CAFE', |
|
'name': '☕ Cafe Dash', |
|
'delay': 90_000, |
|
'estimated-events': 10 |
|
} |
|
}, |
|
TRIM: { |
|
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => { |
|
setup('game-id', 'TRIM'); |
|
setup('app-token', 'ef319a80-949a-492e-8ee0-424fb5fc20a6'); |
|
setup('promo-id', 'ef319a80-949a-492e-8ee0-424fb5fc20a6'); |
|
setup('unity-version', '2021.3.17f1'); |
|
|
|
if (origin === 'ios') { |
|
setup('user-agent', 'MowandTrim/170 CFNetwork/1498.700.2 Darwin/23.6.0'); |
|
} else { |
|
setup('user-agent', 'UnityPlayer/2021.3.17f1 (UnityWebRequest/1.0, libcurl/7.84.0-DEV)'); |
|
} |
|
|
|
await login({ clientOrigin: origin, clientId: id(origin === 'ios' ? 'ts7d' : 'ts19d', 'TRIM') }); |
|
|
|
while (!instance.hasCode) { |
|
await delay(50_000); |
|
await event({ eventId: 'StartLevel', eventOrigin: 'undefined' }); |
|
} |
|
|
|
await collect(); |
|
}, |
|
'meta': { |
|
'id': 'TRIM', |
|
'name': '🌾 Mow and Trim', |
|
'delay': 50_000, |
|
'estimated-events': 8 |
|
} |
|
}, |
|
RACE: { |
|
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => { |
|
setup('game-id', 'RACE'); |
|
setup('app-token', '8814a785-97fb-4177-9193-ca4180ff9da8'); |
|
setup('promo-id', '8814a785-97fb-4177-9193-ca4180ff9da8'); |
|
setup('unity-version', '2020.3.18f1'); |
|
|
|
if (origin === 'ios') { |
|
setup('user-agent', 'Truckbountyhole/12 CFNetwork/1498.700.2 Darwin/23.6.0'); |
|
} else { |
|
setup('user-agent', 'UnityPlayer/2020.3.18f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)'); |
|
} |
|
|
|
await login({ clientOrigin: origin, clientId: id('uuid', 'RACE') }); |
|
|
|
while (!instance.hasCode) { |
|
await delay(60_000); |
|
await event({ eventId: id('uuid'), eventOrigin: 'undefined', eventType: 'racing' }); |
|
} |
|
|
|
await collect(); |
|
}, |
|
'meta': { |
|
'id': 'RACE', |
|
'name': '🚚 Mud Racing', |
|
'delay': 60_000, |
|
'estimated-events': 6 |
|
} |
|
}, |
|
POLY: { |
|
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => { |
|
setup('game-id', 'POLY'); |
|
setup('app-token', '2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71'); |
|
setup('promo-id', '2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71'); |
|
setup('unity-version', '2021.3.39f1'); |
|
|
|
if (origin === 'ios') { |
|
setup('user-agent', 'Polysphere/147 CFNetwork/1498.700.2 Darwin/23.6.0'); |
|
} else { |
|
setup('user-agent', 'UnityPlayer/2021.3.39f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)'); |
|
} |
|
|
|
await login({ clientOrigin: origin, clientId: id('uuid', 'POLY'), clientVersion: '1.15.2' }); |
|
|
|
while (!instance.hasCode) { |
|
await delay(20_000); // imo 10 sec is too low |
|
await event({ eventId: id('uuid'), eventOrigin: 'undefined', eventType: 'test' }); |
|
} |
|
|
|
await collect(); |
|
}, |
|
'meta': { |
|
'id': 'POLY', |
|
'name': '🔵 Polysphere', |
|
'delay': 20_000, |
|
'estimated-events': 16 |
|
} |
|
}, |
|
TWERK: { |
|
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => { |
|
setup('game-id', 'TWERK'); |
|
setup('app-token', '61308365-9d16-4040-8bb0-2f4a4c69074c'); |
|
setup('promo-id', '61308365-9d16-4040-8bb0-2f4a4c69074c'); |
|
setup('unity-version', '2021.3.15f1'); |
|
|
|
if (origin === 'ios') { |
|
setup('user-agent', 'Twerk/485 CFNetwork/1498.700.2 Darwin/23.6.0'); |
|
} else { |
|
setup('user-agent', 'UnityPlayer/2021.3.15f1 (UnityWebRequest/1.0, libcurl/7.84.0-DEV)'); |
|
} |
|
|
|
await login({ clientOrigin: origin, clientId: id(origin === 'ios' ? 'ts7d' : 'ts19d', 'TWERK') }); |
|
|
|
while (!instance.hasCode) { |
|
await delay(30_000); |
|
await event({ eventId: 'StartLevel', eventOrigin: 'undefined' }); |
|
} |
|
|
|
await collect(); |
|
}, |
|
'meta': { |
|
'id': 'TWERK', |
|
'name': '🎖️ Twerk Race', |
|
'delay': 30_000, |
|
'estimated-events': 10 |
|
} |
|
}, |
|
MERGE: { |
|
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => { |
|
setup('game-id', 'MERGE'); |
|
setup('app-token', '8d1cc2ad-e097-4b86-90ef-7a27e19fb833'); |
|
setup('promo-id', 'dc128d28-c45b-411c-98ff-ac7726fbaea4'); |
|
|
|
if (origin === 'ios') { |
|
setup('user-agent', 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148'); |
|
} else { |
|
setup('user-agent', 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36'); |
|
} |
|
|
|
await login({ clientOrigin: origin, clientId: id(origin === 'ios' ? 'ts7d' : 'ts19d', 'MERGE') }); |
|
|
|
while (!instance.hasCode) { |
|
await delay(60_000); |
|
await event({ eventOrigin: 'undefined', eventId: id('uuid'), eventType: 'spend-energy' }); |
|
} |
|
|
|
await collect(); |
|
}, |
|
'meta': { |
|
'id': 'MERGE', |
|
'name': '🔗 Merge Away', |
|
'delay': 60_000, |
|
'estimated-events': 7 |
|
} |
|
}, |
|
// Currently deactivated |
|
// |
|
// CLONE: { |
|
// 'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => { |
|
// setup('game-id', 'CLONE'); |
|
// setup('app-token', '74ee0b5b-775e-4bee-974f-63e7f4d5bacb'); |
|
// setup('promo-id', 'fe693b26-b342-4159-8808-15e3ff7f8767'); |
|
// setup('unity-version', '2022.3.25f1'); |
|
|
|
// if (origin === 'ios') { |
|
// setup('user-agent', 'Myclonearmy/12 CFNetwork/1498.700.2 Darwin/23.6.0'); |
|
// } else { |
|
// setup('user-agent', 'UnityPlayer/2022.3.25f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)'); |
|
// } |
|
|
|
// await login({ clientId: id(origin === 'ios' ? 'uuid-upper' : 'rand32', 'CLONE'), clientOrigin: origin }); |
|
|
|
// while (!instance.hasCode) { |
|
// await delay(150_000); |
|
// await event({ eventId: id('uuid'), eventType: 'MiniQuest', eventOrigin: 'undefined' }); |
|
// } |
|
|
|
// await collect(); |
|
// }, |
|
// 'meta': { |
|
// 'id': 'CLONE', |
|
// 'name': '👾 My Clone Army', |
|
// 'delay': 150_000, |
|
// 'estimated-events': 5 |
|
// } |
|
// }, |
|
CUBE: { |
|
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => { |
|
setup('game-id', 'CUBE'); |
|
setup('app-token', 'd1690a07-3780-4068-810f-9b5bbf2931b2'); |
|
setup('promo-id', 'b4170868-cef0-424f-8eb9-be0622e8e8e3'); |
|
setup('unity-version', '2022.3.20f1'); |
|
|
|
if (origin === 'ios') { |
|
setup('user-agent', 'ChainCube/3 CFNetwork/1498.700.2 Darwin/23.6.0'); |
|
} else { |
|
setup('user-agent', 'UnityPlayer/2022.3.20f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)'); |
|
} |
|
|
|
await login({ clientOrigin: origin, clientId: id('uuid', 'CUBE'), clientVersion: '1.78.33' }); |
|
|
|
while (!instance.hasCode) { |
|
await delay(150_000); |
|
await event({ eventId: id('uuid'), eventOrigin: 'undefined', eventType: 'cube_sent' }); |
|
} |
|
|
|
await collect(); |
|
}, |
|
'meta': { |
|
'id': 'CUBE', |
|
'name': '🔲 Chain Cube 2028', |
|
'delay': 150_000, |
|
'estimated-events': 3 |
|
} |
|
}, |
|
TRAIN: { |
|
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => { |
|
setup('game-id', 'TRAIN'); |
|
setup('app-token', '82647f43-3f87-402d-88dd-09a90025313f'); |
|
setup('promo-id', 'c4480ac7-e178-4973-8061-9ed5b2e17954'); |
|
setup('unity-version', '2022.3.20f1'); |
|
|
|
if (origin === 'ios') { |
|
setup('user-agent', 'TrainMiner/20 CFNetwork/1498.700.2 Darwin/23.6.0'); |
|
} else { |
|
setup('user-agent', 'UnityPlayer/2022.3.20f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)'); |
|
} |
|
|
|
await login({ clientOrigin: origin, clientId: id(origin === 'ios' ? 'uuid-upper' : 'rand32', 'TRAIN'), clientVersion: '2.4.16' }); |
|
|
|
while (!instance.hasCode) { |
|
await delay(600_000); |
|
await event({ eventId: id('uuid'), eventOrigin: 'undefined', eventType: 'hitStatue' }); |
|
} |
|
|
|
await collect(); |
|
}, |
|
'meta': { |
|
'id': 'TRAIN', |
|
'name': '🚂 Train Miner', |
|
'delay': 600_000, |
|
'estimated-events': 1 |
|
} |
|
}, |
|
BIKE: { |
|
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => { |
|
setup('game-id', 'BIKE'); |
|
setup('app-token', 'd28721be-fd2d-4b45-869e-9f253b554e50'); |
|
setup('promo-id', '43e35910-c168-4634-ad4f-52fd764a843f'); |
|
// todo(delasy): Actually scan BIKE game headers |
|
|
|
await login({ clientOrigin: origin, clientId: id(origin === 'ios' ? 'ts7d' : 'ts19d', 'BIKE') }); |
|
|
|
while (!instance.hasCode) { |
|
await delay(50_000); |
|
await event({ eventId: id('uuid'), eventOrigin: 'undefined' }); |
|
} |
|
|
|
await collect(); |
|
}, |
|
'meta': { |
|
'id': 'BIKE', |
|
'name': '🚲 Bike Ride 3D', |
|
'delay': 50_000, |
|
'estimated-events': 13 |
|
} |
|
}, |
|
}; |
|
|
|
const LOG = document.getElementById('log'); |
|
|
|
const CUSTOM_DELAY = document.getElementById('custom-delay'); |
|
const CUSTOM_DELAY_INPUT = document.getElementById('custom-delay-input'); |
|
const DELAY_SELECT = document.getElementById('delay'); |
|
const MIN_MAX_ETA = document.getElementById('min-max-eta'); |
|
|
|
const RESULT_TUBE = document.getElementById('result-tube'); |
|
const RESULT_HOLDER = document.getElementById('result-holder'); |
|
const RESULT_COPY = document.getElementById('result-copy'); |
|
|
|
const QR_CODE_CONTAINER = document.getElementById('qrcode-container'); |
|
var qrCode; |
|
|
|
function handleError(error) { |
|
Logger.panic('Error: ', error); |
|
logToTextArea(error); |
|
} |
|
|
|
function clearLog() { |
|
LOG.value = ''; |
|
} |
|
|
|
function calculateEtaInSeconds(gameKey) { |
|
const meta = GAMES[gameKey]['meta']; |
|
const totalMs = meta['estimated-events'] * meta['delay'] * getDelayMultiplier(); |
|
const withRandomness = totalMs * ((1 / 4) / 1.5 + 1); // 2/3 of: ms * (Math.random() / 4 + 1 |
|
return withRandomness / 1000; |
|
} |
|
|
|
function allGenerateButtons() { |
|
return document.querySelectorAll('.generate-btn'); |
|
} |
|
|
|
function createGenerateButtons() { |
|
const games = Object.keys(GAMES); |
|
|
|
for (const game of games) { |
|
const button = document.createElement('button'); |
|
button.classList.add('btn', 'btn-primary', 'generate-btn'); |
|
button.setAttribute('game-id', game); |
|
button.textContent = GAMES[game]['meta']['name']; |
|
button.onclick = () => main(button).catch(handleError); |
|
document.querySelector('.generator-buttons').appendChild(button); |
|
} |
|
} |
|
|
|
function refreshGenerateButtonsTooltips() { |
|
for (const button of allGenerateButtons()) { |
|
const gameId = button.getAttribute('game-id'); |
|
const estimatedTime = calculateEtaInSeconds(gameId); |
|
|
|
var tooltip; |
|
if (estimatedTime > 0) { |
|
const estimatedTimeMinutes = Math.floor(estimatedTime / 60); |
|
tooltip = `ETA: ~${estimatedTimeMinutes} ${minutesFmt(estimatedTimeMinutes)}`; |
|
} |
|
else { |
|
tooltip = 'ETA: N/A'; |
|
} |
|
|
|
button.setAttribute('data-toggle', 'tooltip'); |
|
button.setAttribute('data-placement', 'top'); |
|
button.setAttribute('title', tooltip); |
|
} |
|
} |
|
|
|
function refreshMinMaxEta() { |
|
var minEta = null, maxEta = null; |
|
|
|
for (const game of Object.keys(GAMES)) { |
|
const eta = calculateEtaInSeconds(game); |
|
|
|
if (eta <= 0) { |
|
continue; |
|
} |
|
|
|
if (minEta === null || eta < minEta) { |
|
minEta = eta; |
|
} |
|
|
|
if (maxEta === null || eta > maxEta) { |
|
maxEta = eta; |
|
} |
|
} |
|
|
|
const minMinutes = Math.floor(minEta / 60); |
|
const maxMinutes = Math.floor(maxEta / 60); |
|
MIN_MAX_ETA.value = `${minMinutes} - ${maxMinutes} ${minutesFmt(maxMinutes)}`; |
|
} |
|
|
|
function logToTextArea(message) { |
|
LOG.value += message + '\n'; |
|
LOG.scrollTop = LOG.scrollHeight; |
|
} |
|
|
|
async function copy() { |
|
await navigator.clipboard.writeText(RESULT_HOLDER.innerText); |
|
} |
|
|
|
function generatingEffect() { |
|
const textArray = ['.', '..', '...']; |
|
let index = 0; |
|
|
|
return setInterval(() => { |
|
RESULT_HOLDER.innerText = textArray[index]; |
|
index = (index + 1) % textArray.length; |
|
}, 500); |
|
} |
|
|
|
function toggleCopyButton(visible) { |
|
RESULT_COPY.classList.toggle('d-none', !visible); |
|
} |
|
|
|
function toggleQrCodeContainer(visible) { |
|
QR_CODE_CONTAINER.classList.toggle('d-none', !visible); |
|
} |
|
|
|
function toggleGenerateButtonActive(button, active) { |
|
button.classList.toggle('btn-success', active); |
|
button.classList.toggle('blink', active); |
|
} |
|
|
|
function getDelayMultiplier() { |
|
const delayMultiplier = DELAY_SELECT.options[DELAY_SELECT.selectedIndex].value; |
|
if (delayMultiplier === 'custom') { |
|
return CUSTOM_DELAY_INPUT.value; |
|
} |
|
|
|
return delayMultiplier; |
|
} |
|
|
|
async function getPromoCode(gameKey, delayMultiplier) { |
|
return GP.getCode(gameKey, delayMultiplier); |
|
} |
|
|
|
async function main(pressedButton) { |
|
const game = pressedButton.getAttribute('game-id'); |
|
const delayMultiplier = getDelayMultiplier(); |
|
const startDate = Date.now(); |
|
const generatingEffectInterval = generatingEffect(); |
|
|
|
RESULT_TUBE.classList.remove('d-none'); |
|
|
|
toggleGenerateButtonActive(pressedButton, true); |
|
allGenerateButtons().forEach(b => b.disabled = true); |
|
toggleCopyButton(false); |
|
toggleQrCodeContainer(false); |
|
|
|
try { |
|
const code = await getPromoCode(game, delayMultiplier); |
|
const endDate = new Date(); |
|
const minutes = Math.floor((endDate - startDate) / 60000); |
|
Logger.info(`Generated at: ${endDate.toLocaleString()} (took ~${minutes} ${minutesFmt(minutes)} with ${GP.eventCounter} events)`); |
|
Logger.info('*************************** PROMO CODE ***************************'); |
|
Logger.info(code); |
|
Logger.info('******************************************************************'); |
|
|
|
clearInterval(generatingEffectInterval); // must stop this before showing result |
|
RESULT_HOLDER.innerText = code; |
|
toggleCopyButton(true); |
|
|
|
qrCode.clear(); |
|
qrCode.makeCode(code); |
|
toggleQrCodeContainer(true); |
|
} catch (error) { |
|
clearInterval(generatingEffectInterval); |
|
handleError(error); |
|
} finally { |
|
allGenerateButtons().forEach(b => b.disabled = false); |
|
toggleGenerateButtonActive(pressedButton, false); |
|
|
|
new Audio('https://opengameart.org/sites/default/files/audio_preview/chirptone.mp3.ogg').play(); |
|
} |
|
} |
|
|
|
// The only reason I want (I actually don't) to do this, is to be able to |
|
// run this HTML via https://html-preview.github.io |
|
// I can't check for DOMContentLoaded to create the QRCode instance and |
|
// the script is loaded too late |
|
fetch('https://cdn.jsdelivr.net/gh/davidshimjs/qrcodejs/qrcode.min.js') |
|
.then(response => response.text()) |
|
.then(jsCode => { |
|
eval(jsCode); |
|
|
|
qrCode = new QRCode("qrcode", { |
|
text: "", |
|
width: 256, |
|
height: 256 |
|
}); |
|
}) |
|
.catch(error => { |
|
handleError(error); |
|
}); |
|
|
|
DELAY_SELECT.addEventListener('change', (event) => { |
|
CUSTOM_DELAY.classList.toggle('d-none', event.target.value !== 'custom'); |
|
refreshGenerateButtonsTooltips(); |
|
refreshMinMaxEta(); |
|
}); |
|
|
|
CUSTOM_DELAY_INPUT.addEventListener('input', () => { |
|
refreshGenerateButtonsTooltips(); |
|
refreshMinMaxEta(); |
|
}); |
|
|
|
createGenerateButtons(); |
|
refreshGenerateButtonsTooltips(); |
|
refreshMinMaxEta(); |
|
|
|
clearLog(); |
|
Logger.info('Welcome champion! forsenCD ✌️'); |
|
Logger.info('Choose a game and press the button to get the promo code.'); |
|
Logger.info('=================================================================='); |
|
</script> |
|
</body> |
|
|
|
</html> |
Hello. Thanks! I've set it to
android
only now (I assume that most of the users are using Android). It was set toios
in the original gist.Also, I've updated the actual generator to match changes in the mentioned gist. From now on this gist is kind of a fork of delasy's gist, because I could not keep it as a dependency, since it became difficult to monkey patch his code.