あらまし
完全に正月の自由研究で、CookieClickerのチートプログラムを書いた。
経緯
1000回転生する隠し実績をちまちま進めていたのだけど飽きがきたし、せっかくなので自動化で遊んでみることにした。元々はSeleniumに挑戦しようと思っていたのだけど、Macへの導入がなかなか大変そう・・Automatorでやろうかな?と色々考えたが
GoogleChrome拡張機能の、
ScriptAutoRunner
のJavaScriptで素朴にやることにした。
コンソールのメッセージは気にしない。
そもそも
数値操作をすれば転生実績は解除できたりしそうだけど、あくまで「ブラウザ操作の自動化」をやりたいというのが当初の目的だったので、謎に地道な操作を自動化しているかもしれない。チートのことをよくわかっていない。
コード
//フラグ
let statsBtnObsFlg = false;
// 変更を監視するノード
const $statsBtnObsArea = document.getElementById('comments');
// オプション
const statsBtnConfig = { attributes: true, childList: true, subtree: true };
// コールバック
const statsBtnCallback = function (mutationsList, observer) {
for (const mutation of mutationsList) {
for(let node of mutation.addedNodes) {
// 要素のみを追跡し、他のノード(例 テキストノード)はスキップ
if (!(node instanceof HTMLElement)) continue;
if(node.getAttribute('id') === 'statsButton' && node.children[0].innerText === '記録' && !statsBtnObsFlg){
//実績を表示・残り回数を取得
const $statsBtn = document.getElementById('statsButton');
$statsBtn.click();
statsBtnObsFlg = true;
observer.disconnect();
}
}
}
};
//関数呼び出し
observeElement(statsBtnCallback,$statsBtnObsArea,statsBtnConfig);
//フラグ
let statsGeneralObsFlg = false;
// 変更を監視するノード
const $statsGeneralObsArea = document.getElementById('menu');
// オプション
const statsGeneralConfig = { attributes: true, childList: true, subtree: true };
// コールバック
const statsGeneralCallback = function (mutationsList, observer) {
for (const mutation of mutationsList) {
for(let node of mutation.addedNodes) {
// 要素のみを追跡し、他のノード(例 テキストノード)はスキップ
if (!(node instanceof HTMLElement)) continue;
if(node.classList.contains('subsection') && node.querySelector('#statsGeneral') && !statsGeneralObsFlg ){
//監視を停止
observer.disconnect();
statsGeneralObsFlg = true;
//必要転生回数を取得
const $reca = document.getElementById('statsGeneral').children[4];
const recaCount = $reca.innerText.replace(/^.+昇天 (.+)回$/,'$1');
const recaRemainsCount = 1000 - recaCount;
//メインプログラム
async function clickForReincarnation(){
//通知を削除
const $notes = document.getElementById('notes').children ? document.getElementById('notes').children : undefined ;
if($notes){
const $notesBtn = $notes[$notes.length - 1];
$notesBtn.click();
}
//100購入タブに切り替え
const $storeBulkTab = document.getElementById('storeBulk100');
$storeBulkTab.click();
//アップグレードの一括購入ボタンをクリック
await clickUpgrade(2000,4);
//すべての施設をループして購入不可能施設の一つ手前の施設の購入ボタンをクリック
await clickProduct(4000,4);
//アップグレードの一括購入ボタンを再度クリック
await clickUpgrade(500,5);
//天国に行って戻ってくる
await goToHeaven();
//アップグレード購入関数
function clickUpgrade(d,c){
return new Promise(resolve => {
let counter = 1;
let timerID = null;
const proc = () => {
if(counter <= c){
const $storeAllBtn = document.getElementById('storeBuyAllButton');
$storeAllBtn.click();
console.log('upgrade',d,c,counter,timerID);
//最後のループでループを止める
if(counter === c){
clearTimeout(timerID);
resolve();
}
else{
timerID = setTimeout(() => { proc(); },d);
}
}
counter ++;
}
proc();
});
}
//施設購入関数
function clickProduct(d,c){
return new Promise(resolve => {
let counter = 1;
let timerID = null;
const proc = () => {
if(counter <= c){
//購入可能な施設に対してループ
const $products = document.querySelectorAll('.product');
const $enableprods = Array.from($products).filter(el =>
!el.classList.contains('locked') && !el.classList.contains('disabled')
);
$enableprods[$enableprods.length - 1].click();
console.log('product',d,c,counter,timerID);
//最後のループでループを止める
if(counter === c){
clearTimeout(timerID);
resolve();
}
else{
timerID = setTimeout(() => { proc(); },d);
}
}
counter ++;
}
proc();
});
}
//天国での手続き
function goToHeaven(){
return new Promise(resolve => {
const proc = () => {
//フラグ
let ascendBtnObsFlg = false;
// 変更を監視するノード
const $ascendBtnObsArea = document.getElementById('wrapper');
// オプション
const ascendBtnConfig = { attributes: true, childList: true, subtree: true };
// コールバック
const ascendBtnCallback = function (mutationsList, observer) {
for (const mutation of mutationsList) {
if(mutation.type === 'attributes' && mutation.attributeName === 'class' && mutation.target.classList.contains('ascending') && !ascendBtnObsFlg){
//再転生ボタンをクリック
const $ascendBtn = document.getElementById('ascendButton');
$ascendBtn.click();
//ダイヤログをクリック
const $ascendDialogBtn = document.getElementById('promptOption0');
$ascendDialogBtn.click();
resolve();
ascendBtnObsFlg = true;
observer.disconnect();
}
}
};
//再転生ボタンのエリアを監視
observeElement(ascendBtnCallback,$ascendBtnObsArea,ascendBtnConfig);
//転生ボタンをクリック
const $legacyBtn = document.getElementById('legacyButton');
$legacyBtn.click();
//ダイヤログをクリック
const $legacyDialogBtn = document.getElementById('promptContentAscend').querySelector('.smallFancyButton');
$legacyDialogBtn.click();
}
proc();
});
}
}
// 輪廻転生関数
async function loopReincarnations(n) {
for (let i = 0; i < n; i++) {
await clickForReincarnation();
}
}
// 規定の回数を指定して実行
loopReincarnations(recaRemainsCount);
}
}
}
};
//監視関数呼び出し
observeElement(statsGeneralCallback,$statsGeneralObsArea,statsGeneralConfig);
//監視関数
function observeElement(cb,t,c){
// コールバック関数に結びつけられたオブザーバーのインスタンスを生成
const observer = new MutationObserver(cb);
// 対象ノードの設定された変更の監視を開始
observer.observe(t, c);
}
感想
- MutationObserverとasync/awaitの良い復習になった。
- エラー発生時の対処記述がないのは微妙。でも今回はもうつかれた。これでよしとする。
- 監視関数も都度停止してるけどサーバーにめちゃくちゃ負担かかってたらどうしよう。大丈夫と思うけど
- 次はSpeedBakingⅢのチートコードを書きたいな〜。