// ==UserScript==
// @name IdlePixel Breeding Overhaul
// @namespace com.anwinity.idlepixel
// @version 1.0.11
// @description Redesigned breeding UI and QoL features.
// @author Anwinity
// @license MIT
// @match *://idle-pixel.com/login/play*
// @grant none
// @require https://gf.qytechs.cn/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"];
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",
};
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
}
});
}
parseAnimals(raw) {
if(!raw) {
return [];
}
return raw.split("=").map((rawAnimal) => {
const result = {
raw: rawAnimal
};
const [
slug,
animal,
gender,
tier,
millis,
isGold,
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.sex = (gender==1 ? "M" : (gender==2 ? "F" : "?"));
//result.tier = parseInt(tier);
result.birth = parseInt(millis);
//result.gold = isGold!=0;
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);
}
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;
}
`;
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");
}
}
onLogin() {
// 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);
}
}
}
}
const plugin = new BreedingOverhaulPlugin();
IdlePixelPlus.registerPlugin(plugin);
})();