Предупреждение: бот прост до безобразия. Вы вряд ли научитесь чему-то новому. Статья just for fun.
Решил, что спустя год можно поглядеть, чем френды вконтакте развлекаются. Кроме традиционных тонн политоты, картинок с котятами и прочих непотребств обнаружил ссылку на браузерку Angry Pets. В браузерки никогда не играл, поэтому решил посмотреть, что это за зверь такой. Выяснилось следующее: картинки кавайнейшие (милые котики, пингвинчики и белочки бьют друг другу морды), донат анальнейший (за деньги доход умножают в 8 раз), сюжет, стратегия, безбажность, мануал и прочие привычные удобства отсутствуют.
Игра чуть менее, чем полностью, состоит из следующих нехитрых операций: построить здание, подождать 10 минут, построить юнитов, подождать 10 минут, найти жертву, отправить юниты в атаку, подождать 10 минут, найти жертву, отправить юниты в атаку, подождать 10 минут… (10 минут на высоких уровнях растут экспоненциально. За ускорение — отдельная плата.)
Что делать с такой игрой? Правильно, писать бота. Не играть же в это, честное слово.
Ставим задачу:
- Бот должен уметь атаковать заданные списком города по расписанию.
- Боту можно задавать, какими юнитами атаковать.
- Бот должен не сразу палиться. :)
Чтобы бот не спалился сразу, во-первых, жертвами для избиения младенцев фарма выбираем игроков, долго не выходивших в онлайн, чтобы они на нас не пожаловались куда следует; во-вторых, имитируем действия пользователя, а не отправляем голые AJAX запросы; в-третьих, задержку между действиями делаем не нулевой, а случайной.
На радость авторам ботов скрипты на сайте не обфусцированы. Единственные упакованные скрипты — это jQuery и иже. И да, есть jQuery, поэтому пользуемся всеми благами цивилизации.
Большинство действий в игре приводят к AJAX запросам. Нажимаешь элемент, уходит запрос, приходит или практически полный экран, или отдельное окошко. Исследуя в отладчике ссылки, видим, что они выглядят как
<a href="/10193192/city/view/10009466" data-link-handled="1" onclick="Main.goToUrl(this);return false;"></a>
где метод Main.goToUrl принимает либо строки, либо элементы-ссылки. У большинства игровых ссылок в начале стоит префикс с идентификатором профиля игрока. Сохраняем его.
profilePath: window.location.pathname.match(/^/d+//)[0],
Как сделать ожидание выполнения AJAX запроса? В jQuery есть славная функция $.ajaxSuccess, в которую можно передать коллбэк, вызываемый после каждого успешного запроса. В него сваливаются событие, объект XMLHttpRequest и аргументы вызова $.ajax. Соответственно, при получении заданного УРЛа вызываем наш коллбэк.
ajaxCallbacks: {},
run: function ()
{
...
$('html').ajaxSuccess(Bot.ajaxSuccess);
...
},
ajaxSuccess: function (e, xhr, settings)
{
var ajaxUrl = null, ajaxCallback = null;
$.each(Bot.ajaxCallbacks, function (url, callback)
{
var fullUrl = Bot.profilePath + url;
if (settings.url.substr(0, fullUrl.length) == fullUrl) {
ajaxUrl = url;
ajaxCallback = callback;
}
});
if (ajaxCallback) {
Bot.ajaxCallbacks[ajaxUrl] = null;
setTimeout(ajaxCallback, Bot.getClickDelay());
}
},
waitForAjax: function (pageUrl, gotoPage, success)
{
Bot.ajaxCallbacks[pageUrl] = success;
gotoPage();
},
Ну и чисто для единообразия к waitForAjax добавляем waitForAction, когда нужно не ждать AJAX, а просто сделать задержку.
waitForAction: function (action, success)
{
action();
setTimeout(success, Bot.getClickDelay());
},
Как простой неавтоматизированный смертный часто фармит? Жмёт по кнопке почты, переход в логи, выбирает недавно атакованный город, жмёт «Атаковать», выбирает юниты, жмёт «Набить морду».
Вот и будем повторять эту операцию по кругу. Конечно, в логах нужный город на нужной странице будет не всегда, но писать сложную логику перелистывания без особой мотивации — откровенно лень.
Выбираем жертву и формируем ссылки…
attackNext: function ()
{
if (Bot.targetCities.length == 0)
return;
if (!Bot.targetCities[Bot.currentTargetCity])
Bot.currentTargetCity = 0;
var targetCity = Bot.targetCities[Bot.currentTargetCity++],
targetCityUrl = 'city/view/' + targetCity,
attackCityUrl = 'attack/' + targetCity;
Переходим по ссылкам и жмём кнопки…
Bot.waitForAjax('pm/inbox', function ()
{
Main.goToUrl(Bot.profilePath + 'pm/inbox');
}, function ()
{
Bot.waitForAjax('pm/logs', function ()
{
Main.goToUrl(Bot.profilePath + 'pm/logs');
}, function ()
{
Bot.waitForAjax(targetCityUrl, function ()
{
Main.goToUrl(Bot.profilePath + targetCityUrl);
}, function ()
{
Bot.waitForAjax(attackCityUrl, function ()
{
$('button[onclick^="Attack.showAttackAlert"]').click();
}, function ()
{
Либо выбираем конкретные юниты (доступное количество — в атрибуте инпута max), либо выбираем все (отдельная кнопка), а затем самое важное: нажать «Атаковать».
Bot.waitForAction(function ()
{
var count = 0;
$.each(Bot.attackUnits, function (unitType, unitNum)
{
var ctl = $('input[name="units[' + unitType + ']"]');
ctl.val(Math.min(ctl.attr('max'), unitNum)).change();
count++;
});
if (count == 0) {
$('span[onclick^="Attack.ChooseEveryone"]').click();
}
}, function ()
{
$('button[type=submit]').click();
setTimeout(Bot.attackNext, Bot.getAttackInterval());
})
Попапы-слои при переходе по AJAX-страницам закрываются сами, можно не заморачиваться.
Собственно, всё. Только красивый список атакуемых городов можно прикрутить, чтобы видеть, кого мочим.
Полный код:
window.Bot = {
attackInterval: /*5.5*/8 * 60 * 1000, // 8 min
attackIntervalRandom: 1.2 * 60 * 1000, // 1.2 min
clickDelay: 3 * 1000, // 3 sec
clickDelayRandom: 4 * 1000, // 4 sec
targetCities: [
//12345678
],
attackUnits: {
//101: 99
},
profilePath: window.location.pathname.match(/^/d+//)[0],
currentTargetCity: 0,
ajaxCallbacks: {},
run: function ()
{
var box = '<div style="position: absolute; background: #fff; padding: 10px; border-radius: 10px; left: 20px; top: 20px; z-index: 666666">';
$.each(Bot.targetCities, function (_, cityId)
{
box += '<a class="bot-target-city" data-link-handled="1" onclick="Main.goToUrl(this);return false;"' +
' id="bot-target-city-' + cityId + '"' +
' href="' + Bot.profilePath + 'city/view/' + cityId + '">' +
cityId + '</a><br>';
});
box += '</div>';
$('body').append(box);
$('html').ajaxSuccess(Bot.ajaxSuccess);
Bot.attackNext();
},
ajaxSuccess: function (e, xhr, settings)
{
var ajaxUrl = null, ajaxCallback = null;
$.each(Bot.ajaxCallbacks, function (url, callback)
{
var fullUrl = Bot.profilePath + url;
if (settings.url.substr(0, fullUrl.length) == fullUrl) {
ajaxUrl = url;
ajaxCallback = callback;
}
});
if (ajaxCallback) {
Bot.ajaxCallbacks[ajaxUrl] = null;
setTimeout(ajaxCallback, Bot.getClickDelay());
}
else {
console.log('Not recognized ' + settings.url);
}
},
waitForAjax: function (pageUrl, gotoPage, success)
{
Bot.ajaxCallbacks[pageUrl] = success;
gotoPage();
},
waitForAction: function (action, success)
{
action();
setTimeout(success, Bot.getClickDelay());
},
getAttackInterval: function ()
{
return parseInt(Bot.attackInterval + Math.random() * Bot.attackIntervalRandom);
},
getClickDelay: function ()
{
return parseInt(Bot.clickDelay + Math.random() * Bot.clickDelayRandom);
},
attackNext: function ()
{
if (Bot.targetCities.length == 0)
return;
if (!Bot.targetCities[Bot.currentTargetCity])
Bot.currentTargetCity = 0;
var targetCity = Bot.targetCities[Bot.currentTargetCity++],
targetCityUrl = 'city/view/' + targetCity,
attackCityUrl = 'attack/' + targetCity;
$('a.bot-target-city').css({ fontWeight: 'normal' });
$('a#bot-target-city-' + targetCity).css({ fontWeight: 'bold' });
Bot.waitForAjax('pm/inbox', function ()
{
Main.goToUrl(Bot.profilePath + 'pm/inbox');
}, function ()
{
Bot.waitForAjax('pm/logs', function ()
{
Main.goToUrl(Bot.profilePath + 'pm/logs');
}, function ()
{
Bot.waitForAjax(targetCityUrl, function ()
{
Main.goToUrl(Bot.profilePath + targetCityUrl);
}, function ()
{
Bot.waitForAjax(attackCityUrl, function ()
{
$('button[onclick^="Attack.showAttackAlert"]').click();
}, function ()
{
Bot.waitForAction(function ()
{
var count = 0;
$.each(Bot.attackUnits, function (unitType, unitNum)
{
var ctl = $('input[name="units[' + unitType + ']"]');
ctl.val(Math.min(ctl.attr('max'), unitNum)).change();
count++;
});
if (count == 0) {
$('span[onclick^="Attack.ChooseEveryone"]').click();
}
}, function ()
{
$('button[type=submit]').click();
setTimeout(Bot.attackNext, Bot.getAttackInterval());
})
});
});
});
});
}
};
Bot.run();
Инструкция по применению:
- В коде массив targetCities заполнить идентификаторами атакуемых городов. При просмотре страницы города идентификатор — это число в самом конце.
- В коде объект attackUnits заполнить парами тип юнита — количество юнитов. Идентификаторы типов любезно предоставлены разработчиками (101 из примера — кошачий «боец»).
- Задержки настроить в соответствии со своими возможностями: attackInterval — интервал между атаками, clickDelay — задержка при кликах, значения Random — диапазон случайно добавляемой задержки.
- Зайти в игру.
- Запустить код из отладочной консоли.
- Если всё верно, то слева появится список идентификаторов атакуемых городов, а скрипт приступит к атаке на первый город. Текущий город выделяется жирным.
Вообще, меня забавляет наглость хозяев игры. «Абонентская плата» в виде Озверина (без которого играть ещё более уныло) — это 180 руб/мес (WoW, в котором есть всё, чего нет в этой игре, стоит всего лишь в 2 раза дороже). Ресурсов можно покупать на до 120 руб/день (день!), увеличивая скорость добычи ресурсов в 8 раз (соревнование кошельков, неплатящие вообще идут лесом). Просторы для вымогательства доната на ускорялки, детали для ракеты, бонусы для атаки, прохождение квестов (ага, заплатил 300 руб — квест прошёл) и прочую мелочь — вообще безграничны. Обмен ресурсов, вступление в клан и прочее — только за деньги. У них в оплате «выбор игроков» — это монетки на 1500 руб. И это всё прикрывается железным аргументом: «Часть дохода идёт в WWF! Вы спасаете милых пушистых зверьков! Будете возмущаться — забаним нафиг!»
И ведь люди платят. Я фигею.
Автор: Athari
http://angrybot.vv-s.com/scanner.html
Тут сканер для angrypets. Сканит Логи, Бои, Весь рейтинг и показывает кто реально ботоводит ботом в игре