// ==UserScript==
// @name IdlePixel Breeding Overhaul
// @namespace com.anwinity.idlepixel
// @version 1.0.18
// @description Redesigned breeding UI and QoL features.
// @author Anwinity
// @license MIT
// @match *://idle-pixel.com/login/play*
// @grant none
// @require https://greasyfork.org/scripts/441206-idlepixel/code/IdlePixel+.js?anticache=20220905
// ==/UserScript==
(function() {
'use strict';
const IMAGE_URL = "https://cdn.idle-pixel.com/images/";
const ANIMAL_LIST = ["chicken", "sheep", "spider", "horse", "ant", "camel", "beaver", "dove", "pig", "haron", "toysoldier", "squidshroom", "snail", "rocky", "geody", "skeleton"];
const ANIMAL_SORT = ANIMAL_LIST.reduce((acc, value, index) => {
acc[value] = index;
return acc;
}, {});
const FOOD_MAP = {
Seeds: "breeding_food_seeds",
Leaves: "breeding_food_leaves",
// breeding_food_strange_leaves
Seafood: "breeding_food_fish",
// breeding_food_fruits
Insects: "breeding_food_insects",
Bark: "breeding_food_wood",
Metal: "breeding_food_metal",
Flesh: "breeding_food_flesh",
};
class BreedingOverhaulPlugin extends IdlePixelPlusPlugin {
constructor() {
super("breedingoverhaul", {
about: {
name: GM_info.script.name,
version: GM_info.script.version,
author: GM_info.script.author,
description: GM_info.script.description
},
config: [
{
id: "newAnimalPopup",
label: "New Animal Popup",
type: "boolean",
default: true,
},
/* // TODO
{
id: "focusFirePeck",
label: "Focus Fire Peck (Requires Peck to be set to MANUAL)",
type: "boolean",
default: false,
}
*/
]
});
this.focusFireTarget = null;
this.animalData = null;
this.lastAnimalActivation = null;
}
parseAnimals(raw) {
if(!raw) {
return [];
}
return raw.split("=").map((rawAnimal) => {
const result = {
raw: rawAnimal
};
const [
slug,
animal,
gender,
tier,
millis,
shiny,
sick,
foodPerDay,
foodConsumed,
lootItemReady,
lootItemAmount,
xpGained,
traitLabel,
extraData1,
extraData2,
extraData3,
extraData4,
extraData5
] = rawAnimal.split(",");
let food = Breeding.getBreedFoodLabel(animal);
if(food == "null") {
food = null;
}
result.slug = slug;
result.animal = animal;
result.sexNumber = parseInt(gender); // useful for some pre-existing Breeding functions
result.sex = (gender==1 ? "M" : (gender==2 ? "F" : "?"));
result.tier = parseInt(tier);
result.birth = parseInt(millis);
result.shiny = parseInt(shiny);
result.sick = sick!=0;
result.food = food;
result.foodPerDay = parseInt(foodPerDay);
//result.foodConsumed = parseInt(foodConsumed);
result.lootItem = lootItemReady == "none" ? null : lootItemReady;
result.lootCount = parseInt(lootItemAmount);
result.xp = parseInt(xpGained);
result.trait = traitLabel;
//result.hornyRate = Breeding.getReproductionRate(animal, null);
return result;
}).sort((a, b) => {
const aSort = ANIMAL_SORT[a.animal] ?? 9999;
const bSort = ANIMAL_SORT[b.animal] ?? 9999;
if(aSort == bSort) {
return a.birth - b.birth;
}
return aSort - bSort;
});
}
calculateReproductionRate(animalData, animal) {
if(["toysoldier"].includes(animal)) {
return Number.POSITIVE_INFINITY;
}
// calculates expected # of ticks for this species to reproduce
let rate = Breeding.getReproductionRate(animal, null);
let males = 0;
let females = 0;
let infertile = 0;
let fertile = 0;
for(const data of animalData) {
if(data.animal != animal) {
continue;
}
if(data.sick) {
continue;
}
if(data.sex == "M") {
males++;
}
else if(data.sex == "F") {
females++;
}
if(data.trait == "Fertile") {
fertile++;
}
else if(data.trait == "Less Fertile") {
infertile++;
}
}
let couples = Math.min(males, females);
if(infertile > 0) {
rate = Math.floor(rate * Math.pow(1.1, infertile));
}
if(fertile > 0) {
rate = Math.floor(rate * Math.pow(0.9, fertile));
}
rate = 600 * Math.floor(1 + (rate / 600 / couples));
return rate;
}
summarizeAnimals(animalData) {
let xp = 0;
const animals = [];
const loot = {};
const dailyFood = {};
const reproduction = {};
for(const animal of animalData) {
xp += animal.xp || 0;
if(!animals.includes(animal.animal)) {
animals.push(animal.animal);
}
if(animal.lootItem) {
if(animal.lootCount) {
loot[animal.lootItem] ??= 0;
loot[animal.lootItem] += animal.lootCount;
}
}
if(animal.food && animal.foodPerDay) {
dailyFood[animal.food] ??= 0;
dailyFood[animal.food] += animal.foodPerDay;
}
}
let result = { xp, animals, loot, dailyFood, reproduction };
for(const animal of animals) {
const rate = this.calculateReproductionRate(animalData, animal);
result.reproduction[animal] = rate;
}
// temporary hack until var_ant_capacity_used is fixed
let antCount = animalData.filter(animal => animal.animal=="ant").length;
window.var_ant_capacity_used = `${antCount}`;
return result;
}
updateAnimalSummary(summary) {
if(!summary) {
return;
}
const totalXPElement = document.getElementById("breeding-overhaul-total-xp");
if(totalXPElement) {
totalXPElement.innerHTML = `${summary.xp?.toLocaleString(navigator.language)}`;
}
const dailyFoodElement = document.getElementById("breeding-overhaul-daily-food");
const foodDaysElement = document.getElementById("breeding-overhaul-food-days");
if(dailyFoodElement && summary.dailyFood) {
let content = "";
let contentDays = "";
const foodSpans = [];
const foodDaysSpans = [];
for(const food in summary.dailyFood) {
const amount = summary.dailyFood[food]?.toLocaleString(navigator.language);
const foodVar = FOOD_MAP[food];
const img = `<img src="${IMAGE_URL}${foodVar}.png" class="inline" />`;
let danger = false;
let owned = "";
let days = 0;
if(foodVar) {
owned = IdlePixelPlus.getVarOrDefault(foodVar, 0, "int");
if(owned < amount) {
danger = true;
}
days = amount==0 ? 0 : owned/amount;
owned = `/${owned.toLocaleString(navigator.language)}`;
}
foodSpans.push(`<span class="${danger?'danger':''}" title="${food}: ${amount}/day"> ${img} ${amount}${owned} </span>`);
foodDaysSpans.push(`<span class="${danger?'danger':''}" title="${food}: ${days.toFixed(2)} day${days==1?'':'s'}"> ${img} ${days.toFixed(1)} day${days==1?'':'s'} </span>`);
}
content += foodSpans.join(" ");
if(!content) {
content = "None";
}
if(dailyFoodElement) {
dailyFoodElement.innerHTML = content;
}
contentDays += foodDaysSpans.join(" ");
if(!contentDays) {
contentDays = "None";
}
if(foodDaysElement) {
foodDaysElement.innerHTML = contentDays;
}
}
const reproductionElement = document.getElementById("breeding-overhaul-reproduction");
if(reproductionElement && summary.reproduction && summary.animals) {
let content = "";
const hornySpans = [];
for(const animal of summary.animals) {
const img = `<img src="${IMAGE_URL}${animal}_male_1.png" class="inline" />`;
let rate = summary.reproduction[animal];
if(Number.isFinite(rate)) {
rate = format_time(rate);
if(rate < 600) {
// animals can reproduce at most once every 10 minutes
rate = 600;
}
rate = rate.replace(/, 0:00$/, "");
}
else {
rate = "Never";
}
hornySpans.push(`<span title="${capitalizeFirstLetter(animal)} Expected Reproduction: ${rate}"> ${img} ${rate} </span>`);
}
content += hornySpans.join(" ");
if(!content) {
content = "None";
}
reproductionElement.innerHTML = content;
}
const lootAvailableElement = document.getElementById("breeding-overhaul-loot-available");
if(lootAvailableElement && summary.loot) {
let content = "";
const lootSpans = [];
for(const loot in summary.loot) {
const amount = summary.loot[loot]?.toLocaleString(navigator.language);
const img = `<img src="${IMAGE_URL}${loot.toLowerCase()}.png" class="inline" />`;
lootSpans.push(`<span title="${loot}: ${amount}"> ${img} ${amount} </span>`);
}
content += lootSpans.join(" ");
let anyLoot = true;
if(!content) {
content = "None";
anyLoot = false;
}
lootAvailableElement.innerHTML = content;
const lootAllButton = document.getElementById("breeding-overhaul-loot-all");
if(lootAllButton) {
lootAllButton.disabled = !anyLoot;
}
}
}
clickLootAll() {
const animalData = IdlePixelPlus.getVar("animal_data");
if(!animalData) {
return;
}
this.parseAnimals(animalData)
.filter(animal => animal.lootCount)
.forEach(animal => IdlePixelPlus.sendMessage(`COLLECT_ALL_LOOT_ANIMAL`));
}
updateAnimalData(animals) {
if(!animals) {
return;
}
const summary = this.summarizeAnimals(animals);
this.updateAnimalSummary(summary);
}
focusFire(event, enemyID) {
event.stopPropagation();
this.focusFireTarget = enemyID;
const breedingFightingHeroes = document.querySelectorAll(".breeding-fighting-entry");
breedingFightingHeroes.forEach((el) => {
const allyID = parseInt(el.id.match(/(\d+)$/));
if(typeof allyID !== "number" || isNaN(allyID)) {
return;
}
websocket.send(`BREEDING_FIGHT_SET_TARGET=${allyID}~${enemyID}`);
Breeding.flashBorder3Seconds(allyID, enemyID);
});
Breeding.global_last_click_fighting_hero_animal_index = "none";
}
initUI() {
const style = document.createElement("style");
style.id = "styles-breeding-overhaul";
style.textContent = `
#panel-breeding #breeding-overhaul-breeding-summary {
display: flex;
flex-direction: column;
}
#panel-breeding #breeding-overhaul-breeding-summary .danger {
color: red;
}
#panel-breeding #breeding-overhaul-breeding-summary img.inline {
height: 1em;
width: auto;
}
#breeding-overhaul-new-animal-dialog-backdrop {
position: fixed;
z-index: 9999;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
pointer-events: all;
}
#breeding-overhaul-new-animal-dialog {
position: sticky;
top: 3rem;
border: none;
padding: 1em;
background-color: white;
border: 4px solid black;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
min-width: 300px;
}
#breeding-overhaul-new-animal-dialog > img#breeding-overhaul-new-animal-image {
position: absolute;
top: 1em;
right: 1em;
height: 50px;
}
`;
document.head.appendChild(style);
const panel = document.getElementById("panel-breeding");
if(!panel) {
console.error("Breeding Overhaul: Failed to initialize UI - #panel-breeding not found");
return;
}
const craftableButtons = panel.querySelector(".craftable-btns");
if(craftableButtons) {
craftableButtons.insertAdjacentHTML("afterend", `
<div id="breeding-overhaul-breeding-summary">
<div>
<strong>Total XP: </strong>
<span id="breeding-overhaul-total-xp"></span>
</div>
<div>
<strong>Daily Food: </strong>
<span id="breeding-overhaul-daily-food"></span>
</div>
<div>
<strong>Food Days: </strong>
<span id="breeding-overhaul-food-days"></span>
</div>
<div>
<strong>Reproduction: </strong>
<span id="breeding-overhaul-reproduction"></span>
</div>
<div>
<strong>Loot Available: </strong>
<span id="breeding-overhaul-loot-available"></span>
</div>
<div>
<button id="breeding-overhaul-loot-all" type="button" onclick="IdlePixelPlus.plugins.breedingoverhaul.clickLootAll()">Loot All</button>
</div>
</div>
`);
}
else {
console.error("Breeding Overhaul: Failed to fully initialize UI - .craftable-btns not found");
}
// Breeding "Focus Fire" buttons
const breedingFightingEnemies = document.querySelectorAll(".breeding-fighting-entry-monster");
breedingFightingEnemies.forEach((el) => {
const enemyID = parseInt(el.id.match(/(\d+)$/));
if(typeof enemyID !== "number" || isNaN(enemyID)) {
return;
}
const newDiv = document.createElement("div");
newDiv.innerHTML = `
<div class="center">
<button onclick="IdlePixelPlus.plugins.breedingoverhaul.focusFire(event, ${enemyID})">Focus Fire</button>
</div>
`;
el.appendChild(newDiv);
});
}
markAnimalAsNew(slug) {
/* FOR DEBUGGING */
delete this.animalData[slug];
}
detectNewAnimals(animals) {
const first = this.animalData === null;
const animalIdsBefore = first ? new Set() : new Set(Object.keys(this.animalData));
const animalIdsAfter = new Set(animals.map(animal => animal.slug));
this.animalData = {};
animals.forEach(animal => this.animalData[animal.slug] = animal);
if(!first) {
const newAnimalIds = [...animalIdsAfter].filter(id => !animalIdsBefore.has(id));
if(newAnimalIds.length && this.getConfig("newAnimalPopup")) {
const animal = this.animalData[newAnimalIds[0]];
const name = capitalizeFirstLetter(get_item_name(animal.animal));
const tier = animal.tier;
const tierDots = `<span class="dot-green"></span> `.repeat(tier);
const slug = animal.slug;
const animalImage = `${IMAGE_URL}${animal.animal}_${Breeding.get_gender_label(animal.sexNumber)}_${animal.tier}.png`;
const sexColor = animal.sex == "F" ? "pink" : "blue";
const sexImage = `${IMAGE_URL}${animal.sex=='F' ? 'female' : 'male'}.png`;
const shiny = animal.shiny;
const shinyDivStyle = shiny==0 ? "" : "background-color: rgba(255, 223, 0, 0.5);";
const foodAmount = animal.foodPerDay;
const foodLabel = animal.food;
const foodType = FOOD_MAP[animal.food];
const foodImage = `${IMAGE_URL}${foodType}.png`;
const trait = animal.trait;
const traitIcon = `${IMAGE_URL}${Breeding.getTraitDesc(trait, slug)[0]}.png`;
const shinyLabel = (function() {
switch(shiny) {
case 0: return "No";
case 1: return "Shiny!";
case 2: return "Mega Shiny!";
default: return "Unknown";
}
})();
const inactiveVar = `inactive_${animal.animal}_${animal.sex=='F' ? 'female' : 'male'}`;
const inactiveCount = IdlePixelPlus.getVarOrDefault(inactiveVar, 0, "int").toLocaleString();
let extraButtonsHTML = "";
if(trait == "Fighter") {
extraButtonsHTML += `<button onclick="IdlePixelPlus.sendMessage('ADD_FIGHT_ANIMAL=${slug}'); IdlePixelPlus.plugins.breedingoverhaul.closeNewAnimalDialog()">Make Fighter</button> `;
extraButtonsHTML += `<button onclick="IdlePixelPlus.sendMessage('ADD_FIGHT_ANIMAL=${slug}'); IdlePixelPlus.plugins.breedingoverhaul.retryLastAnimalActivation(); IdlePixelPlus.plugins.breedingoverhaul.closeNewAnimalDialog()">Make Fighter & Try Again</button> `;
}
const dialogHTML = `
<div id="breeding-overhaul-new-animal-dialog-backdrop">
<dialog id="breeding-overhaul-new-animal-dialog" open>
<img id="breeding-overhaul-new-animal-image" src="${animalImage}" />
<div class="v-flex half-gap">
<h5><img src="${sexImage}" class="inline" style="height: 1em" /> T-${tier} ${name}</h5>
<div><strong>Tier: </strong> ${tierDots}</div>
<div><strong>Trait: </strong> <img src="${traitIcon}" class="inline" style="height: 1em" /> ${trait}</div>
<div style="${shinyDivStyle}"><strong>Shiny: </strong> ${shinyLabel}</div>
<div><strong>Food: </strong> <img src="${foodImage}" class="inline" style="height: 1em" /> ${foodAmount} ${foodLabel} / day</div>
<div><strong>Inactive Remaining: </strong> <span id="breeding-overhaul-new-animal-dialog-inactive-count">${inactiveCount}</span></div>
<div class="v-flex half-gap">
<button onclick="IdlePixelPlus.plugins.breedingoverhaul.closeNewAnimalDialog()">Keep</button>
<button onclick="IdlePixelPlus.plugins.breedingoverhaul.retryLastAnimalActivation(); IdlePixelPlus.plugins.breedingoverhaul.closeNewAnimalDialog()">Keep & Try Again</button>
${extraButtonsHTML}
<button onclick="IdlePixelPlus.sendMessage('KILL_ANIMAL=${slug}'); IdlePixelPlus.plugins.breedingoverhaul.closeNewAnimalDialog()">Kill</button>
<button onclick="IdlePixelPlus.sendMessage('KILL_ANIMAL=${slug}'); IdlePixelPlus.plugins.breedingoverhaul.retryLastAnimalActivation(); IdlePixelPlus.plugins.breedingoverhaul.closeNewAnimalDialog()">Kill & Try Again</button>
</div>
</div>
</dialog>
</div>
`;
this.closeNewAnimalDialog();
document.body.insertAdjacentHTML("afterbegin", dialogHTML);
setTimeout(function() {
const updatedInactiveCount = IdlePixelPlus.getVarOrDefault(inactiveVar, 0, "int").toLocaleString();
if(inactiveCount != updatedInactiveCount) {
const inactiveCountSpan = document.getElementById("breeding-overhaul-new-animal-dialog-inactive-count");
if(inactiveCountSpan) {
inactiveCountSpan.textContent = updatedInactiveCount;
}
}
}, 50);
}
}
}
retryLastAnimalActivation() {
if(typeof this.lastAnimalActivation === "string") {
if(this.lastAnimalActivation.startsWith("SLAUGHTER_FOR_TIER_")) {
// if number is more than we have, find new max to send
const parts = this.lastAnimalActivation.split(/[~=]/g);
const owned = IdlePixelPlus.getVarOrDefault(parts[1], 0, "int");
const attempted = parseInt(parts[3]);
if(attempted > owned) {
parts[3] = owned;
}
this.lastAnimalActivation = `${parts[0]}=${parts.slice(1).join("~")}`;
}
IdlePixelPlus.sendMessage(this.lastAnimalActivation);
}
}
closeNewAnimalDialog() {
const dialog = document.getElementById("breeding-overhaul-new-animal-dialog-backdrop");
if(dialog) {
dialog.remove();
}
}
onMessageSent(message) {
if(typeof message === "string") {
if(message.startsWith("ACTIVATE_ANIMAL=") || message.startsWith("SLAUGHTER_FOR_TIER_")) {
this.lastAnimalActivation = message;
}
}
}
onLogin() {
// Intercept sent messages - used to track last animal action
if(window.websocket) {
const plugin = this;
const originalSend = window.websocket.send;
window.websocket.send = function(data) {
plugin.onMessageSent(data);
return originalSend.call(this, data);
}
}
// Initialize new ui components
this.initUI();
// override refresh function
const original_refresh_animals_data = Breeding.refresh_animals_data;
Breeding.refresh_animals_data = (...args) => {
const animalArea = document.getElementById("breeding-chicken-area");
let raw = args[0] || null;
let animals = this.parseAnimals(raw);
// rejoin (sorted) for original function
if(animals?.length) {
raw = animals.map(animal => animal.raw).join("=");
}
// run original smitty function
try {
original_refresh_animals_data.apply(this, [raw]);
}
catch(err) {
console.error("Error running original Breeding.refresh_animals_data: ", err);
throw err;
}
// update ui
try {
this.updateAnimalData(animals);
}
catch(err) {
console.error("Error running BreedingOverhaulPlugin.updateAnimalData: ", err);
}
// detect new animals
try {
this.detectNewAnimals(animals);
}
catch(err) {
console.error("Error running BreedingOverhaulPlugin.detectNewAnimals: ", err);
}
}
}
onVariableSet(key, valueBefore, valueAfter) {
// TODO
return;
switch(key) {
case "is_in_breeding_fight": {
console.log(`${key}: "${valueBefore}" -> "${valueAfter}"`);
if(valueAfter == "1") {
this.fight = {};
}
else {
this.fight = null;
}
break;
}
}
}
onMessageReceived(data) {
// TODO
return;
try {
const manual = window.var_large_chicken_beak_automated == "0";
const focusFirePeck = this.getConfig("focusFirePeck");
// requires manual since auto-mode functions server side
if(manual && focusFirePeck) {
if(data?.startsWith("REFRESH_BREEDING_FIGHTING=")) {
data = data.substring("REFRESH_BREEDING_FIGHTING=".length).split(",");
const pecksAvailable = [];
for(let i = 0; i < data.length; i++) {
const fighterData = data[i].split("~");
const hp = parseInt(fighterData[1]);
const stamina = parseInt(fighterData[5]);
const requiredStamina = parseInt(fighterData[6]);
const special = fighterData[7];
console.log(`i=${i}, hp=${hp}, special=${special}, stamina=${stamina}, requiredStamina=${requiredStamina}`);
if(special == "peck" && hp > 0 && stamina >= requiredStamina) {
pecksAvailable.push(i);
console.log(`${i} peck ready`);
break;
}
}
}
}
}
catch(err) {
console.error("Breeding Overhaul - onMessageReceived - Something went wrong while processing focus fire for special abilities. A recent game update may have broken this functionality.");
}
}
}
const plugin = new BreedingOverhaulPlugin();
IdlePixelPlus.registerPlugin(plugin);
})();