|
<!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> |
As I did not intercept the requests myself, I previously did not understand the meaning of
/promo/register-event
endpoint. Now I finally get what they are for and I realized that the current delay settings are useless. I will try to rework those to add more control over the delay between sending these kind of requests, instead of adding delay between the individual key generations.Edit: I have now reworked the delay settings.