console.info("[SOCIETY] animalBase.js loaded"); const debug = false; const debugData = (player, level, data, hearts) => { player.tell(`:heart: ${data.getInt("affection")}-${hearts} hearts`); player.tell( `Day: ${Number( (Math.floor(Number(level.dayTime() / 24000)) + 1).toFixed() )}` ); player.tell(`Mood: ${data.getInt("lastMood")}`); player.tell(`Day Mood last set: ${data.getInt("ageLastSetMood")}`); player.tell(`Pet: ${data.getInt("ageLastPet")}`); player.tell(`Fed: ${data.getInt("ageLastFed")}`); player.tell(`Boosted feed: ${data.getInt("ageLastBoosted")}`); player.tell(`Dropped Special: ${data.getInt("ageLastDroppedSpecial")}`); player.tell(`Milked: ${data.getInt("ageLastMilked")}`); player.tell(`Magic Harvested: ${data.getInt("ageLastMagicHarvested")}`); }; const initializeFarmAnimal = (day, target, level) => { const data = target.persistentData; if (!data.getInt("affection")) { data.affection = 1; level.spawnParticles( "minecraft:heart", true, target.x, target.y + 1.5, target.z, 0, 0.1, 0, 1, 0.01 ); } const newBaseDay = day - 1; if (!data.getInt("ageLastPet")) data.ageLastPet = newBaseDay; if (!data.getInt("ageLastFed")) data.ageLastFed = newBaseDay; if (!data.getInt("ageLastDroppedSpecial")) data.ageLastDroppedSpecial = newBaseDay; if (!data.getInt("ageLastMagicHarvested")) data.ageLastMagicHarvested = newBaseDay; if (!data.getInt("ageLastMoodSet")) data.ageLastMoodSet = newBaseDay; if (!data.getInt("ageLastBred")) data.ageLastBred = newBaseDay; if (!data.getInt("ageLastMilked") && global.checkEntityTag(target, "society:milkable_animal")) data.ageLastMilked = newBaseDay; }; // Anti-frustration feature to be forgiving when re-logging const handleFarmAnimalBackwardsCompat = (target, day) => { const data = target.persistentData; if ( day < data.getInt("ageLastPet") || data.getInt("ageLastPet") - day > 5000 ) { let newDay = day - 1; data.ageLastPet = newDay; data.ageLastMagicHarvested = newDay; data.ageLastMilked = newDay; data.ageLastDroppedSpecial = newDay; data.ageLastBred = newDay; data.ageLastFed = newDay; data.ageLastMoodSet = newDay; } }; const checkAnimal = ( player, level, server, data, name, mood, hearts, color ) => { const nameColor = color || "#55FF55"; const heartsToDisplay = 10; let icons = []; if (mood < 64) icons.push("☹"); if (mood > 160) icons.push("☺"); if (data.animalCracker) icons.push("🡅"); if (data.clockwork) icons.push("⚙"); if (data.bff) icons.push("❤"); if (data.bribed) icons.push("💰"); let iconString = ""; icons.forEach((icon, index) => { iconString += icon; if (index < icons.length - 1) iconString += " "; }); global.renderUiText( player, server, { animalNameIcons: { type: "text", x: 0, y: -88, text: iconString, color: nameColor, alignX: "center", alignY: "bottom", }, animalName: { type: "text", x: 0, y: -78, text: `${name.noColor().toJson()}`, color: nameColor, alignX: "center", alignY: "bottom", }, animalNameShadow: { type: "text", x: 1, z: -1, y: -77, text: `${name.noColor().toJson()}`, color: "#000000", alignX: "center", alignY: "bottom", }, affection: { type: "text", x: 0, y: -66, text: `§c${hearts > 0 ? `❤`.repeat(Math.min(hearts, heartsToDisplay)) : "" }§0${hearts < heartsToDisplay ? `❤`.repeat(heartsToDisplay - hearts) : "" }`, color: "#FFAA00", alignX: "center", alignY: "bottom", }, affectionShadow: { type: "text", x: 1, z: -1, y: -65, text: `❤`.repeat(heartsToDisplay), color: "#000000", alignX: "center", alignY: "bottom", }, }, global.mainUiElementIds ); debug && debugData(player, level, data, hearts); }; const handlePet = (name, data, mood, day, peckish, hungry, e) => { const { player, item, target, level, server } = e; const ageLastPet = data.getInt("ageLastPet"); const affection = data.getInt("affection"); let hearts = Math.floor(affection / 100); if (hearts > 10) hearts = 10; else if (hearts < 0) hearts = 0; let affectionIncreaseMult = player.stages.has("animal_whisperer") || data.bribed ? 2 : 1; if (player.stages.has("animal_fancy")) affectionIncreaseMult += 1; let affectionIncrease = 10 * affectionIncreaseMult; if (target.isBaby()) { affectionIncrease = affectionIncrease * (player.stages.has("fostering") ? 10 : 2); } let errorText = ""; if (day > ageLastPet) { let livableArea = global.getAnimalIsNotCramped(target, 1.1); if (!player.isFake()) { debug && player.tell( `Increased Affection by: ${affectionIncrease} from petting` ); data.affection = affection + affectionIncrease; } if (hungry || (!data.clockwork && player.isFake()) || !livableArea) { data.affection = affection - (hungry ? 15 : 25); } data.ageLastPet = day; level.spawnParticles( (!data.clockwork && player.isFake()) || hungry ? "minecraft:angry_villager" : "minecraft:heart", true, target.x, target.y + 1.5, target.z, 0, 0.1, 0, 1, 0.01 ); global.giveExperience(server, player, "husbandry", Math.max(10, 20 * hearts)); if (!livableArea && !data.clockwork) { errorText = Text.translatable( "society.husbandry.crowded", name ).toJson(); } if ( !hungry && peckish && !player.isFake() && !item.hasTag("society:animal_feed") ) { server.runCommandSilent( global.getEmbersTextAPICommand( player.username, `{anchor:"BOTTOM_CENTER",background:1,wrap:220,align:"BOTTOM_CENTER",color:"#FFAA00",offsetY:20}`, 40, Text.translatable("society.husbandry.peckish", name).toJson() ) ); } if (hungry) { errorText = Text.translatable( "society.husbandry.starved", name ).toJson(); } if (errorText && !player.isFake()) { server.runCommandSilent( global.getEmbersTextAPICommand( player.username, global.animalMessageSettings, 40, errorText ) ); } } else if (item === "minecraft:air" || item === 'society:mood_scanner') { let nameColor; if (peckish) { nameColor = "#FFAA00"; } if (hungry) { nameColor = "#FF5555"; } checkAnimal(player, level, server, data, name, mood, hearts, nameColor); } // Raise Max health const affectionHealth = hearts * 4; if (target.maxHealth < affectionHealth) { target.setMaxHealth(affectionHealth); target.setHealth(affectionHealth); } }; const handleMilk = (name, data, day, hungry, e) => { const { player, item, target, level, server } = e; if (player.cooldowns.isOnCooldown(item)) return; if (player.isFake() && data.getInt("affection") < 100) return; let errorText; let milkItem = global.getMilk(level, target, data, player, day, true, undefined, player.stages); if (milkItem !== -1) { let milk = level.createEntity("minecraft:item"); milk.x = player.x; milk.y = player.y; milk.z = player.z; milk.item = milkItem; milk.spawn(); server.runCommandSilent( `playsound minecraft:entity.cow.milk block @a ${player.x} ${player.y} ${player.z}` ); global.giveExperience(server, player, "husbandry", 30); level.spawnParticles( "minecraft:note", true, target.x, target.y + 1.5, target.z, 0.1 * rnd(1, 4), 0.1 * rnd(1, 4), 0.1 * rnd(1, 4), 3, 0.01 ); } else if (target.isBaby()) { errorText = Text.translatable( "society.husbandry.action.young", name ).toJson(); } else if (hungry) { errorText = Text.translatable( "society.husbandry.action.hungry", name ).toJson(); } else { errorText = Text.translatable( "society.husbandry.action.cooldown_1", name ).toJson(); } if (errorText && !player.isFake()) { server.runCommandSilent( global.getEmbersTextAPICommand( player.username, global.animalMessageSettings, 20, errorText ) ); } }; const handleFeed = (data, day, e) => { const { player, item, target, level, server } = e; if (player.cooldowns.isOnCooldown(item)) return; const ageLastFed = data.getInt("ageLastFed"); const affection = data.getInt("affection"); const affectionIncrease = { "society:animal_feed": 10, "society:candied_animal_feed": 100, "society:mana_feed": 30, }[item.id]; let affectionIncreaseMult = player.stages.has("animal_whisperer") || data.bribed ? 2 : 1; if (player.stages.has("animal_fancy")) affectionIncreaseMult += 0.5; // Cap affection increase at 100 const totalNewAffection = Math.min( affectionIncrease * affectionIncreaseMult, 100 ); if (day > ageLastFed) { server.runCommandSilent( `playsound minecraft:entity.generic.eat block @a ${player.x} ${player.y} ${player.z}` ); target.heal(4); global.giveExperience(server, player, "husbandry", 20); data.affection = affection + totalNewAffection; debug && player.tell(`Increased Affection by: ${totalNewAffection} from feeding`); data.ageLastFed = day; level.spawnParticles( "legendarycreatures:wisp_particle", true, target.x, target.y + 1.5, target.z, 0.1 * rnd(1, 4), 0.1 * rnd(1, 4), 0.1 * rnd(1, 4), 5, 0.01 ); item.count--; global.addItemCooldown(player, item, 10); } }; const handleSheepMagicShears = (e) => { const { target, level, server } = e; if (target.readyForShearing()) { target.setSheared(true); let woolItem = Item.of(target.getColor().getName() + "_wool"); let i = Math.ceil(Math.random() * 4); for (let j = 0; j < i; j++) { let wool = level.createEntity("minecraft:item"); wool.x = target.x + rnd(0, 0.5); wool.y = target.y + 0.5; wool.z = target.z + rnd(0, 0.5); wool.item = Item.of(woolItem); wool.spawn(); wool.setDeltaMovement( wool .getDeltaMovement() .add( (Math.random() - Math.random()) * 0.1, Math.random() * 0.05, (Math.random() - Math.random()) * 0.1 ) ); } server.runCommandSilent( `playsound minecraft:entity.sheep.shear block @a ${target.x} ${target.y} ${target.z}` ); } }; const handleMagicHarvest = (name, data, e) => { const { player, level, target, item, server } = e; if (player.cooldowns.isOnCooldown(item)) return; if (["minecraft:sheep", "wildernature:minisheep"].includes(target.type)) handleSheepMagicShears(e); const affection = data.getInt("affection"); let hearts = Math.floor((affection > 1000 ? 1000 : affection) / 100); let errorText = ""; const droppedLoot = global.getMagicShearsOutput(level, target, player, undefined, player.stages); if (droppedLoot !== -1) { server.runCommandSilent( `playsound minecraft:entity.sheep.shear block @a ${player.x} ${player.y} ${player.z}` ); global.giveExperience(server, player, "husbandry", 15); for (let i = 0; i < droppedLoot.length; i++) { let specialItem = level.createEntity("minecraft:item"); let dropItem = droppedLoot[i]; specialItem.x = player.x; specialItem.y = player.y; specialItem.z = player.z; specialItem.item = dropItem; specialItem.spawn(); } global.addItemCooldown(player, item, 1); } else { errorText = Text.translatable( "society.husbandry.action.cooldown_2", name ).toJson(); if (hearts < 5) { errorText = Text.translatable( "society.husbandry.action.need_hearts", name ).toJson(); } if (!player.isFake()) server.runCommandSilent( global.getEmbersTextAPICommand( player.username, global.animalMessageSettings, 40, errorText ) ); global.addItemCooldown(player, item, 10); } }; const handleSpecialItem = ( data, day, chance, hungry, minHearts, mult, item, hasQuality, plushieModifiers, e ) => { const { player, target, level, server } = e; let affection; let mood; let resolvedItem = item; let resolvedChance = chance; let resolvedHasQuality = hasQuality let dropAmount = mult * (plushieModifiers && plushieModifiers.doubleDrops ? 2 : 1); if (plushieModifiers) { affection = 1000; mood = 256; resolvedChance = chance + plushieModifiers.probabilityIncrease; if (plushieModifiers.processItems) { let processOutput = global.getProcessedItem(item, dropAmount); resolvedItem = processOutput.item.id; dropAmount = Math.round(dropAmount / processOutput.divisor) * processOutput.item.count; resolvedHasQuality = processOutput.preserveQuality } } else { affection = data.getInt("affection") || 0; mood = global.getOrFetchMood(level, target, day, player); } let hearts = Math.floor(affection / 100); let quality = 0; if ( (!hungry || plushieModifiers) && hearts >= minHearts && Math.random() <= resolvedChance ) { if (item.includes("large")) { if (Math.random() > (mood + hearts * 10) / 256) { return; } } if (plushieModifiers) { data.affection = affection + (player.stages.has("animal_whisperer") ? 20 : 10); } let specialItem = level.createEntity("minecraft:item"); if (resolvedHasQuality && mood >= 160) { quality = global.getHusbandryQuality(hearts, mood); } specialItem.x = player.x; specialItem.y = player.y; specialItem.z = player.z; specialItem.item = Item.of(`${dropAmount}x ${resolvedItem}`, quality > 0 ? `{quality_food:{effects:[],quality:${quality}}}` : null ); specialItem.spawn(); server.runCommandSilent( `playsound stardew_fishing:dwop block @a ${player.x} ${player.y} ${player.z}` ); global.giveExperience(server, player, "husbandry", 60); if (target.x) { level.spawnParticles( "farmersdelight:star", true, target.x, target.y + 1, target.z, 0.2 * rnd(1, 4), 0.2 * rnd(1, 4), 0.2 * rnd(1, 4), 3, 0.01 ); } } }; const upgradeAnimal = (level, server, item, target, sound, particle) => { item.count--; server.runCommandSilent( `playsound ${sound} block @a ${target.x} ${target.y} ${target.z}` ); level.spawnParticles( particle, true, target.x, target.y + 1.5, target.z, 0.2 * rnd(0, 2), 0.2 * rnd(0, 2), 0.2 * rnd(0, 2), 3, 0.01 ); }; global.handleHusbandryBase = (hand, player, item, target, level, server) => { const pet = global.checkEntityTag(target, "society:pet_animal"); const eventData = { player: player, item: item, target: target, level: level, server: server, }; if (hand == "OFF_HAND") return; if (!global.checkEntityTag(target, "society:husbandry_animal") && !pet) return; if (item.id === "society:sunlit_crystal") return; server.scheduleInTicks(1, () => { if (hand == "MAIN_HAND") { const day = global.getDay(level); handleFarmAnimalBackwardsCompat(target, day); initializeFarmAnimal(day, target, level); const data = target.persistentData; let name = target.customName ? target.customName : global.getTranslatedEntityName(String(target.type)); const ageLastFed = data.getInt("ageLastFed"); const peckish = !pet && day - ageLastFed == 1; const hungry = !pet && day - ageLastFed > 1; const affection = data.getInt("affection"); const lostProduce = mood < 64 && Math.random() < mood / 64; let hearts = Math.floor((affection > 1000 ? 1000 : affection) / 100); player.swing(); const mood = global.getOrFetchMood(level, target, day, player, false, true); handlePet(name, data, mood, day, peckish, hungry, eventData); if (pet) return; if (item.hasTag("society:animal_feed") && !pet) handleFeed(data, day, eventData); if (!lostProduce) { if ( item === "society:milk_pail" && global.checkEntityTag(target, "society:milkable_animal") ) { handleMilk(name, data, day, hungry, eventData); } global.handleSpecialHarvest( level, target, player, server, undefined, undefined, undefined, handleSpecialItem, player.stages, ); } if ( player.stages.has("biomancer") && [ "bakery:bread_knife", "farmersdelight:iron_knife", "farmersdelight:diamond_knife", "farmersdelight:netherite_knife", "farmersdelight:golden_knife", "aquaculture:wooden_fillet_knife", "aquaculture:stone_fillet_knife", "aquaculture:iron_fillet_knife", "aquaculture:gold_fillet_knife", "farmersdelight:flint_knife", "aquaculture:neptunium_fillet_knife", "aquaculture:diamond_fillet_knife", "refurbished_furniture:knife", ].includes(item.id) ) { if (player.cooldowns.isOnCooldown(item)) return; if (hearts < 5) { server.runCommandSilent( global.getEmbersTextAPICommand( player.username, global.animalMessageSettings, 40, Text.translatable( "society.husbandry.action.need_hearts", name ).toJson() ) ); } else { let heart = level.createEntity("minecraft:item"); heart.x = player.x; heart.y = player.y; heart.z = player.z; heart.item = Item.of("quark:diamond_heart"); heart.spawn(); server.runCommandSilent( `playsound minecraft:entity.sheep.shear block @a ${player.x} ${player.y} ${player.z}` ); server.runCommandSilent( `playsound legendarycreatures:mojo_hurt block @a ${player.x} ${player.y} ${player.z} 0.1` ); level.spawnParticles( "minecraft:angry_villager", true, target.x, target.y + 1.5, target.z, 0.2 * rnd(1, 4), 0.2 * rnd(1, 4), 0.2 * rnd(1, 4), 5, 0.01 ); data.affection = affection - 100; global.addItemCooldown(player, item, 5); } } if ( player.stages.has("bribery") && item === "numismatics:crown" && !data.bribed ) { if (player.cooldowns.isOnCooldown(item)) return; data.bribed = true; data.affection = affection + 100; item.count--; server.runCommandSilent( `playsound stardew_fishing:complete block @a ${player.x} ${player.y} ${player.z}` ); level.spawnParticles( "legendarycreatures:desert_mojo_particle", true, target.x, target.y + 1.5, target.z, 0.2 * rnd(0, 1), 0.2 * rnd(0, 1), 0.2 * rnd(0, 1), 5, 0.01 ); global.addItemCooldown(player, item, 1); } if ( player.stages.has("clockwork") && item === "create:precision_mechanism" && !data.clockwork ) { data.clockwork = true; upgradeAnimal( level, server, item, target, "trials:vault_activate", "supplementaries:bomb_explosion" ); if (data.bff) { data.bff = false; server.runCommandSilent( global.getEmbersTextAPICommand( player.username, global.animalMessageSettings, 80, Text.translatable("society.husbandry.mechanization").toJson() ) ); } } if ( player.stages.has("husbandry_mastery") && item === "society:animal_cracker" && !data.animalCracker ) { data.animalCracker = true; server.runCommandSilent( `playsound minecraft:entity.generic.eat block @a ${player.x} ${player.y} ${player.z}` ); upgradeAnimal( level, server, item, target, "stardew_fishing:chest_get", "farmersdelight:star" ); } if ( player.stages.has("bff") && item === "society:friendship_necklace" && !data.bff ) { data.bff = true; upgradeAnimal( level, server, item, target, "legendarycreatures:wisp_idle", "buzzier_bees:buttercup_bloom" ); if (data.clockwork) { data.clockwork = false; server.runCommandSilent( global.getEmbersTextAPICommand( player.username, `{anchor:"BOTTOM_CENTER",background:1,wrap:220,align:"BOTTOM_CENTER",color:"#55FF55",offsetY:20}`, 80, Text.translatable("society.husbandry.emancipation").toJson() ) ); } } if ( player.stages.has("transplanting") && item === "quark:diamond_heart" && hearts < 10 ) { if (player.cooldowns.isOnCooldown(item)) return; data.affection = affection + 100; if (!player.isCreative()) item.count--; server.runCommandSilent( `playsound aquaculture:fish_flop block @a ${player.x} ${player.y} ${player.z}` ); level.spawnParticles( "minecraft:heart", true, target.x, target.y + 1.5, target.z, 0.2 * rnd(0, 2), 0.2 * rnd(0, 2), 0.2 * rnd(0, 2), 5, 0.01 ); global.addItemCooldown(player, item, 1); } if (!lostProduce && item === "society:magic_shears") { handleMagicHarvest(name, data, eventData); } if (affection > 1075) { // Cap affection at 1075 data.affection = 1075; } if (affection < 0) data.affection = 0; } }); }; ItemEvents.entityInteracted((e) => { const { hand, player, item, target, level, server } = e; if (item === 'moblassos:diamond_lasso') return; global.handleHusbandryBase(hand, player, item, target, level, server); }); BlockEvents.rightClicked(global.plushies, (e) => { const { level, hand, player, item, server, block } = e; if (hand == "OFF_HAND") return; let nbt = block.getEntityData(); if (!nbt) return; const { animal } = nbt.data; if (!animal) return; let animalName = animal.name ? Text.of(animal.name) : global.getTranslatedEntityName(String(animal.type)); if (item === "minecraft:air") { checkAnimal( player, level, server, animal, animalName, 256, 10 ); } global.executePlushieHusbandry( level, server, player, item, block, handleSpecialItem ); if ( player.stages.has("clockwork") && item === "create:precision_mechanism" && !animal.clockwork ) { nbt.merge({ data: { animal: { clockwork: true, }, }, }); upgradeAnimal( level, server, item, block, "trials:vault_activate", "supplementaries:bomb_explosion" ); if (animal.bff) { nbt.merge({ data: { animal: { bff: false, }, }, }); server.runCommandSilent( global.getEmbersTextAPICommand( player.username, global.animalMessageSettings, 80, Text.translatable("society.husbandry.mechanization").toJson() ) ); } } if ( player.stages.has("husbandry_mastery") && item === "society:animal_cracker" && !animal.animalCracker ) { nbt.merge({ data: { animal: { animalCracker: true, }, }, }); server.runCommandSilent( `playsound minecraft:entity.generic.eat block @a ${player.x} ${player.y} ${player.z}` ); upgradeAnimal( level, server, item, block, "stardew_fishing:chest_get", "farmersdelight:star" ); } if ( player.stages.has("bff") && item === "society:friendship_necklace" && !animal.bff ) { nbt.merge({ data: { animal: { bff: true, }, }, }); upgradeAnimal( level, server, item, block, "legendarycreatures:wisp_idle", "buzzier_bees:buttercup_bloom" ); if (animal.clockwork) { nbt.merge({ data: { animal: { clockwork: false, }, }, }); server.runCommandSilent( global.getEmbersTextAPICommand( player.username, `{anchor:"BOTTOM_CENTER",background:1,wrap:220,align:"BOTTOM_CENTER",color:"#55FF55",offsetY:20}`, 80, Text.translatable("society.husbandry.emancipation").toJson() ) ); } } global.setBlockEntityData(block, nbt) });