Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,39 @@
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityBreedEvent;
import org.bukkit.event.entity.EntityEnterLoveModeEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;

public final class HusbandryBreedListener implements Listener {

@EventHandler(ignoreCancelled = true)
public void onFeed(PlayerInteractEntityEvent event) {
if (!(event.getRightClicked() instanceof LivingEntity living)) {
return;
}
if (!HusbandryConfig.isHusbandryType(living.getType())) {
return;
}
Player player = event.getPlayer();
ItemStack hand = event.getHand() == EquipmentSlot.OFF_HAND
? player.getInventory().getItemInOffHand()
: player.getInventory().getItemInMainHand();
HusbandryFeedQuality.offer(
living.getUniqueId(),
HusbandryFeedQuality.scaleOf(hand),
System.currentTimeMillis());
}

@EventHandler(ignoreCancelled = true)
public void onEnterLove(EntityEnterLoveModeEvent event) {
Entity entity = event.getEntity();
BlockReason reason = cannotBreed(entity);
if (reason == BlockReason.NONE) {
HusbandryFeedQuality.commitOffer(entity.getUniqueId(), System.currentTimeMillis());
return;
}
HusbandryFeedQuality.discardOffer(entity.getUniqueId());
event.setCancelled(true);
clearLove(entity);
Player feeder = event.getHumanEntity() instanceof Player player ? player : null;
Expand Down Expand Up @@ -60,7 +83,7 @@ public void onBreed(EntityBreedEvent event) {
return;
}

persistBaby(child, mother, father);
persistBaby(child, mother, father, event.getBredWith());
scheduleMountStats(child);
}

Expand Down Expand Up @@ -129,7 +152,11 @@ private static BlockReason cannotBreed(Entity entity) {

// Keep the existing legacy text representation, formatting, and exact-string comparisons.
@SuppressWarnings("deprecation")
private static void persistBaby(LivingEntity child, LivingEntity mother, LivingEntity father) {
private static void persistBaby(
LivingEntity child,
LivingEntity mother,
LivingEntity father,
ItemStack bredWith) {
if (child == null) {
return;
}
Expand All @@ -147,15 +174,17 @@ private static void persistBaby(LivingEntity child, LivingEntity mother, LivingE
int fatherGenetics = fatherAnimal == null ? 0 : fatherAnimal.genetics();
int motherCare = motherAnimal == null ? 0 : motherAnimal.care();
int fatherCare = fatherAnimal == null ? 0 : fatherAnimal.care();
long now = System.currentTimeMillis();
double feedScale = HusbandryFeedQuality.consumeBreedingScale(
mother.getUniqueId(), father.getUniqueId(), bredWith, now);
int genetics = HusbandryGenetics.roll(
motherGenetics, fatherGenetics, motherCare, fatherCare, ThreadLocalRandom.current());
motherGenetics, fatherGenetics, motherCare, fatherCare, ThreadLocalRandom.current(), feedScale);
String name = HusbandryEntities.displayName(child.getType());
child.setCustomName(name);
child.setCustomNameVisible(false);
HusbandryEntities.applyPersistFlags(child);
HusbandryEntities.stampManaged(child);

long now = System.currentTimeMillis();
HusbandryAnimal baby = new HusbandryAnimal(uuid, child.getType().name(), name);
baby.setState(HusbandryAnimalState.UNTAMED);
baby.setGenetics(genetics);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package net.tfminecraft.cooking.husbandry;

import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;

import net.tfminecraft.cooking.utils.Keys;
import net.tfminecraft.cooking.utils.QualityUtils;

/**
* Breeding-food stars scale the gene bonus above the parent average.
* Five stars, universal feed, and items with no cooking quality keep the full bonus.
*/
public final class HusbandryFeedQuality {

public static final double FULL_SCALE = 1.0;
private static final long OFFER_WINDOW_MILLIS = 2_000L;
private static final long LOVE_WINDOW_MILLIS = 45_000L;

private static final ConcurrentHashMap<UUID, TimedScale> OFFERS = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<UUID, TimedScale> FEEDS = new ConcurrentHashMap<>();

private HusbandryFeedQuality() {}

public static double scaleForStars(int stars) {
int clamped = Math.max(1, Math.min(5, stars));
return clamped / 5.0;
}

public static double clampScale(double scale) {
if (Double.isNaN(scale)) {
return FULL_SCALE;
}
return Math.max(0, Math.min(FULL_SCALE, scale));
}

public static double combine(Double left, Double right) {
if (left == null && right == null) {
return FULL_SCALE;
}
if (left == null) {
return clampScale(right);
}
if (right == null) {
return clampScale(left);
}
return clampScale((left + right) / 2.0);
}

public static double scaleOf(ItemStack stack) {
if (isUniversalFeed(stack)) {
return FULL_SCALE;
}
Integer stars = starsOrNull(stack);
if (stars == null) {
return FULL_SCALE;
}
return scaleForStars(stars);
}

public static void offer(UUID animalId, double scale, long nowMillis) {
if (animalId == null) {
return;
}
OFFERS.put(animalId, new TimedScale(clampScale(scale), nowMillis));
}

public static void discardOffer(UUID animalId) {
if (animalId != null) {
OFFERS.remove(animalId);
}
}

public static void commitOffer(UUID animalId, long nowMillis) {
if (animalId == null) {
return;
}
TimedScale offer = OFFERS.remove(animalId);
if (offer == null || nowMillis - offer.atMillis > OFFER_WINDOW_MILLIS) {
return;
}
remember(animalId, offer.scale, nowMillis);
}

public static void remember(UUID animalId, double scale, long nowMillis) {
if (animalId == null) {
return;
}
FEEDS.put(animalId, new TimedScale(clampScale(scale), nowMillis));
}

public static double consumeBreedingScale(UUID mother, UUID father, ItemStack bredWith, long nowMillis) {
Double motherScale = take(mother, nowMillis);
Double fatherScale = take(father, nowMillis);
if (motherScale == null && fatherScale == null) {
return scaleOf(bredWith);
}
return combine(motherScale, fatherScale);
}

public static double takePair(UUID mother, UUID father, long nowMillis) {
return combine(take(mother, nowMillis), take(father, nowMillis));
}

static void clear() {
OFFERS.clear();
FEEDS.clear();
}

private static Double take(UUID animalId, long nowMillis) {
if (animalId == null) {
return null;
}
TimedScale feed = FEEDS.remove(animalId);
if (feed == null || nowMillis - feed.atMillis > LOVE_WINDOW_MILLIS) {
return null;
}
return feed.scale;
}

private static boolean isUniversalFeed(ItemStack stack) {
if (stack == null || stack.getType().isAir()) {
return false;
}
try {
return HusbandryItems.matches(stack, HusbandryConfig.feedItem());
} catch (RuntimeException ignored) {
return false;
}
}

private static Integer starsOrNull(ItemStack stack) {
if (stack == null || stack.getType().isAir() || !stack.hasItemMeta()) {
return null;
}
ItemMeta meta = stack.getItemMeta();
if (meta == null) {
return null;
}
Integer stored = meta.getPersistentDataContainer().get(Keys.QUALITY, PersistentDataType.INTEGER);
if (stored == null) {
return null;
}
return QualityUtils.clamp(stored);
}

private record TimedScale(double scale, long atMillis) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ public static int roll(
int motherCare,
int fatherCare,
Random random) {
return roll(motherGenetics, fatherGenetics, motherCare, fatherCare, random, HusbandryFeedQuality.FULL_SCALE);
}

public static int roll(
int motherGenetics,
int fatherGenetics,
int motherCare,
int fatherCare,
Random random,
double feedScale) {
int maxGenetics = HusbandryConfig.maxGenetics();
int avg = (motherGenetics + fatherGenetics) / 2;
double varianceBase = maxGenetics / 10.0;
Expand All @@ -25,7 +35,8 @@ public static int roll(
double careAvg = (motherCare + fatherCare) / 2.0;
double careRatio = careMax <= 0 ? 0 : Math.max(0, Math.min(1, careAvg / careMax));
int careExtra = (int) (HusbandryConfig.careInfluence() * maxGenetics * careRatio);
int rolled = avg + (int) (bonus * careRatio) + careExtra;
int boost = (int) (bonus * careRatio) + careExtra;
int rolled = avg + (int) (boost * HusbandryFeedQuality.clampScale(feedScale));
return Math.max(0, Math.min(maxGenetics, rolled));
}
}
2 changes: 2 additions & 0 deletions src/main/resources/husbandry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ breeding:
genetic-slowdown-divisor: 0.6
# Extra gene points at full parent care: this * max-genetics (0.02 -> +20 at 1000).
care-influence: 0.02
# Breeding-food stars scale that bonus (1★ = 20% … 5★ = 100%, same roll as before).
# Universal feed and items with no cooking quality count as a full bonus.

amount-from-genetics:
- min: 0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package net.tfminecraft.cooking.husbandry;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.UUID;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class HusbandryFeedQualityTest {

@BeforeEach
void resetFeeds() {
HusbandryFeedQuality.clear();
}

@Test
void starsScaleLinearlyToAFullBonus() {
assertEquals(0.2, HusbandryFeedQuality.scaleForStars(1), 1e-9);
assertEquals(1.0, HusbandryFeedQuality.scaleForStars(5), 1e-9);
assertEquals(1.0, HusbandryFeedQuality.scaleForStars(9), 1e-9);
}

@Test
void missingFeedsKeepTheFullBonus() {
assertEquals(1.0, HusbandryFeedQuality.combine(null, null), 1e-9);
assertEquals(0.2, HusbandryFeedQuality.combine(0.2, null), 1e-9);
}

@Test
void rememberedFeedsAverageUntilTheyExpire() {
UUID mother = UUID.randomUUID();
UUID father = UUID.randomUUID();
long now = 5_000L;
HusbandryFeedQuality.remember(mother, HusbandryFeedQuality.scaleForStars(1), now);
HusbandryFeedQuality.remember(father, HusbandryFeedQuality.scaleForStars(5), now);
assertEquals(0.6, HusbandryFeedQuality.takePair(mother, father, now), 1e-9);
assertEquals(1.0, HusbandryFeedQuality.takePair(mother, father, now), 1e-9);
}

@Test
void offerCommitsOnlyInsideTheClickWindow() {
UUID animal = UUID.randomUUID();
HusbandryFeedQuality.offer(animal, 0.4, 1_000L);
HusbandryFeedQuality.commitOffer(animal, 1_000L);
assertEquals(0.4, HusbandryFeedQuality.takePair(animal, null, 1_000L), 1e-9);

HusbandryFeedQuality.offer(animal, 0.4, 1_000L);
HusbandryFeedQuality.commitOffer(animal, 4_000L);
assertEquals(1.0, HusbandryFeedQuality.takePair(animal, null, 4_000L), 1e-9);
}

@Test
void expiredLoveFeedIsIgnored() {
UUID animal = UUID.randomUUID();
HusbandryFeedQuality.remember(animal, 0.2, 1_000L);
assertEquals(1.0, HusbandryFeedQuality.takePair(animal, null, 1_000L + 45_001L), 1e-9);
}

@Test
void clearDropsRememberedFeeds() {
UUID animal = UUID.randomUUID();
HusbandryFeedQuality.remember(animal, 0.2, 1_000L);
HusbandryFeedQuality.clear();
assertEquals(1.0, HusbandryFeedQuality.takePair(animal, null, 1_000L), 1e-9);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,37 @@ void resultIsClampedToZero() {
assertEquals(0, child);
}

@Test
void fiveStarFeedMatchesTheUnscaledRoll() {
int full = HusbandryGenetics.roll(10, 10, 200, 200, maxBonus());
int fiveStars = HusbandryGenetics.roll(
10, 10, 200, 200, maxBonus(), HusbandryFeedQuality.scaleForStars(5));
assertEquals(68, full);
assertEquals(full, fiveStars);
}

@Test
void oneStarFeedKeepsOneFifthOfTheBoost() {
int child = HusbandryGenetics.roll(
10, 10, 200, 200, maxBonus(), HusbandryFeedQuality.scaleForStars(1));
assertEquals(21, child);
}

@Test
void mixedOneAndFiveStarFeedsUseTheAverage() {
double scale = HusbandryFeedQuality.combine(
HusbandryFeedQuality.scaleForStars(1),
HusbandryFeedQuality.scaleForStars(5));
int child = HusbandryGenetics.roll(10, 10, 200, 200, maxBonus(), scale);
assertEquals(44, child);
}

@Test
void zeroCareIgnoresFeedQuality() {
assertEquals(10, HusbandryGenetics.roll(
10, 10, 0, 0, maxBonus(), HusbandryFeedQuality.scaleForStars(1)));
}

private static Random maxBonus() {
return new Random() {
@Override
Expand Down