Skip to content

Commit 7f62052

Browse files
Drefvelinryanbarlow97cursoragent
authored
feat: scale breeding gene bonus by feed quality (#42)
Lower-star wheat, carrots, and other breeding food pass on less of the gene bump. Five-star food and universal feed keep the current roll. Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 94a35d8 commit 7f62052

6 files changed

Lines changed: 296 additions & 5 deletions

File tree

‎src/main/java/net/tfminecraft/cooking/husbandry/HusbandryBreedListener.java‎

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,39 @@
1313
import org.bukkit.event.Listener;
1414
import org.bukkit.event.entity.EntityBreedEvent;
1515
import org.bukkit.event.entity.EntityEnterLoveModeEvent;
16+
import org.bukkit.event.player.PlayerInteractEntityEvent;
17+
import org.bukkit.inventory.EquipmentSlot;
18+
import org.bukkit.inventory.ItemStack;
1619

1720
public final class HusbandryBreedListener implements Listener {
1821

22+
@EventHandler(ignoreCancelled = true)
23+
public void onFeed(PlayerInteractEntityEvent event) {
24+
if (!(event.getRightClicked() instanceof LivingEntity living)) {
25+
return;
26+
}
27+
if (!HusbandryConfig.isHusbandryType(living.getType())) {
28+
return;
29+
}
30+
Player player = event.getPlayer();
31+
ItemStack hand = event.getHand() == EquipmentSlot.OFF_HAND
32+
? player.getInventory().getItemInOffHand()
33+
: player.getInventory().getItemInMainHand();
34+
HusbandryFeedQuality.offer(
35+
living.getUniqueId(),
36+
HusbandryFeedQuality.scaleOf(hand),
37+
System.currentTimeMillis());
38+
}
39+
1940
@EventHandler(ignoreCancelled = true)
2041
public void onEnterLove(EntityEnterLoveModeEvent event) {
2142
Entity entity = event.getEntity();
2243
BlockReason reason = cannotBreed(entity);
2344
if (reason == BlockReason.NONE) {
45+
HusbandryFeedQuality.commitOffer(entity.getUniqueId(), System.currentTimeMillis());
2446
return;
2547
}
48+
HusbandryFeedQuality.discardOffer(entity.getUniqueId());
2649
event.setCancelled(true);
2750
clearLove(entity);
2851
Player feeder = event.getHumanEntity() instanceof Player player ? player : null;
@@ -60,7 +83,7 @@ public void onBreed(EntityBreedEvent event) {
6083
return;
6184
}
6285

63-
persistBaby(child, mother, father);
86+
persistBaby(child, mother, father, event.getBredWith());
6487
scheduleMountStats(child);
6588
}
6689

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

130153
// Keep the existing legacy text representation, formatting, and exact-string comparisons.
131154
@SuppressWarnings("deprecation")
132-
private static void persistBaby(LivingEntity child, LivingEntity mother, LivingEntity father) {
155+
private static void persistBaby(
156+
LivingEntity child,
157+
LivingEntity mother,
158+
LivingEntity father,
159+
ItemStack bredWith) {
133160
if (child == null) {
134161
return;
135162
}
@@ -147,15 +174,17 @@ private static void persistBaby(LivingEntity child, LivingEntity mother, LivingE
147174
int fatherGenetics = fatherAnimal == null ? 0 : fatherAnimal.genetics();
148175
int motherCare = motherAnimal == null ? 0 : motherAnimal.care();
149176
int fatherCare = fatherAnimal == null ? 0 : fatherAnimal.care();
177+
long now = System.currentTimeMillis();
178+
double feedScale = HusbandryFeedQuality.consumeBreedingScale(
179+
mother.getUniqueId(), father.getUniqueId(), bredWith, now);
150180
int genetics = HusbandryGenetics.roll(
151-
motherGenetics, fatherGenetics, motherCare, fatherCare, ThreadLocalRandom.current());
181+
motherGenetics, fatherGenetics, motherCare, fatherCare, ThreadLocalRandom.current(), feedScale);
152182
String name = HusbandryEntities.displayName(child.getType());
153183
child.setCustomName(name);
154184
child.setCustomNameVisible(false);
155185
HusbandryEntities.applyPersistFlags(child);
156186
HusbandryEntities.stampManaged(child);
157187

158-
long now = System.currentTimeMillis();
159188
HusbandryAnimal baby = new HusbandryAnimal(uuid, child.getType().name(), name);
160189
baby.setState(HusbandryAnimalState.UNTAMED);
161190
baby.setGenetics(genetics);
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package net.tfminecraft.cooking.husbandry;
2+
3+
import java.util.UUID;
4+
import java.util.concurrent.ConcurrentHashMap;
5+
6+
import org.bukkit.inventory.ItemStack;
7+
import org.bukkit.inventory.meta.ItemMeta;
8+
import org.bukkit.persistence.PersistentDataType;
9+
10+
import net.tfminecraft.cooking.utils.Keys;
11+
import net.tfminecraft.cooking.utils.QualityUtils;
12+
13+
/**
14+
* Breeding-food stars scale the gene bonus above the parent average.
15+
* Five stars, universal feed, and items with no cooking quality keep the full bonus.
16+
*/
17+
public final class HusbandryFeedQuality {
18+
19+
public static final double FULL_SCALE = 1.0;
20+
private static final long OFFER_WINDOW_MILLIS = 2_000L;
21+
private static final long LOVE_WINDOW_MILLIS = 45_000L;
22+
23+
private static final ConcurrentHashMap<UUID, TimedScale> OFFERS = new ConcurrentHashMap<>();
24+
private static final ConcurrentHashMap<UUID, TimedScale> FEEDS = new ConcurrentHashMap<>();
25+
26+
private HusbandryFeedQuality() {}
27+
28+
public static double scaleForStars(int stars) {
29+
int clamped = Math.max(1, Math.min(5, stars));
30+
return clamped / 5.0;
31+
}
32+
33+
public static double clampScale(double scale) {
34+
if (Double.isNaN(scale)) {
35+
return FULL_SCALE;
36+
}
37+
return Math.max(0, Math.min(FULL_SCALE, scale));
38+
}
39+
40+
public static double combine(Double left, Double right) {
41+
if (left == null && right == null) {
42+
return FULL_SCALE;
43+
}
44+
if (left == null) {
45+
return clampScale(right);
46+
}
47+
if (right == null) {
48+
return clampScale(left);
49+
}
50+
return clampScale((left + right) / 2.0);
51+
}
52+
53+
public static double scaleOf(ItemStack stack) {
54+
if (isUniversalFeed(stack)) {
55+
return FULL_SCALE;
56+
}
57+
Integer stars = starsOrNull(stack);
58+
if (stars == null) {
59+
return FULL_SCALE;
60+
}
61+
return scaleForStars(stars);
62+
}
63+
64+
public static void offer(UUID animalId, double scale, long nowMillis) {
65+
if (animalId == null) {
66+
return;
67+
}
68+
OFFERS.put(animalId, new TimedScale(clampScale(scale), nowMillis));
69+
}
70+
71+
public static void discardOffer(UUID animalId) {
72+
if (animalId != null) {
73+
OFFERS.remove(animalId);
74+
}
75+
}
76+
77+
public static void commitOffer(UUID animalId, long nowMillis) {
78+
if (animalId == null) {
79+
return;
80+
}
81+
TimedScale offer = OFFERS.remove(animalId);
82+
if (offer == null || nowMillis - offer.atMillis > OFFER_WINDOW_MILLIS) {
83+
return;
84+
}
85+
remember(animalId, offer.scale, nowMillis);
86+
}
87+
88+
public static void remember(UUID animalId, double scale, long nowMillis) {
89+
if (animalId == null) {
90+
return;
91+
}
92+
FEEDS.put(animalId, new TimedScale(clampScale(scale), nowMillis));
93+
}
94+
95+
public static double consumeBreedingScale(UUID mother, UUID father, ItemStack bredWith, long nowMillis) {
96+
Double motherScale = take(mother, nowMillis);
97+
Double fatherScale = take(father, nowMillis);
98+
if (motherScale == null && fatherScale == null) {
99+
return scaleOf(bredWith);
100+
}
101+
return combine(motherScale, fatherScale);
102+
}
103+
104+
public static double takePair(UUID mother, UUID father, long nowMillis) {
105+
return combine(take(mother, nowMillis), take(father, nowMillis));
106+
}
107+
108+
static void clear() {
109+
OFFERS.clear();
110+
FEEDS.clear();
111+
}
112+
113+
private static Double take(UUID animalId, long nowMillis) {
114+
if (animalId == null) {
115+
return null;
116+
}
117+
TimedScale feed = FEEDS.remove(animalId);
118+
if (feed == null || nowMillis - feed.atMillis > LOVE_WINDOW_MILLIS) {
119+
return null;
120+
}
121+
return feed.scale;
122+
}
123+
124+
private static boolean isUniversalFeed(ItemStack stack) {
125+
if (stack == null || stack.getType().isAir()) {
126+
return false;
127+
}
128+
try {
129+
return HusbandryItems.matches(stack, HusbandryConfig.feedItem());
130+
} catch (RuntimeException ignored) {
131+
return false;
132+
}
133+
}
134+
135+
private static Integer starsOrNull(ItemStack stack) {
136+
if (stack == null || stack.getType().isAir() || !stack.hasItemMeta()) {
137+
return null;
138+
}
139+
ItemMeta meta = stack.getItemMeta();
140+
if (meta == null) {
141+
return null;
142+
}
143+
Integer stored = meta.getPersistentDataContainer().get(Keys.QUALITY, PersistentDataType.INTEGER);
144+
if (stored == null) {
145+
return null;
146+
}
147+
return QualityUtils.clamp(stored);
148+
}
149+
150+
private record TimedScale(double scale, long atMillis) {}
151+
}

‎src/main/java/net/tfminecraft/cooking/husbandry/HusbandryGenetics.java‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ public static int roll(
1212
int motherCare,
1313
int fatherCare,
1414
Random random) {
15+
return roll(motherGenetics, fatherGenetics, motherCare, fatherCare, random, HusbandryFeedQuality.FULL_SCALE);
16+
}
17+
18+
public static int roll(
19+
int motherGenetics,
20+
int fatherGenetics,
21+
int motherCare,
22+
int fatherCare,
23+
Random random,
24+
double feedScale) {
1525
int maxGenetics = HusbandryConfig.maxGenetics();
1626
int avg = (motherGenetics + fatherGenetics) / 2;
1727
double varianceBase = maxGenetics / 10.0;
@@ -25,7 +35,8 @@ public static int roll(
2535
double careAvg = (motherCare + fatherCare) / 2.0;
2636
double careRatio = careMax <= 0 ? 0 : Math.max(0, Math.min(1, careAvg / careMax));
2737
int careExtra = (int) (HusbandryConfig.careInfluence() * maxGenetics * careRatio);
28-
int rolled = avg + (int) (bonus * careRatio) + careExtra;
38+
int boost = (int) (bonus * careRatio) + careExtra;
39+
int rolled = avg + (int) (boost * HusbandryFeedQuality.clampScale(feedScale));
2940
return Math.max(0, Math.min(maxGenetics, rolled));
3041
}
3142
}

‎src/main/resources/husbandry.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ breeding:
4848
genetic-slowdown-divisor: 0.6
4949
# Extra gene points at full parent care: this * max-genetics (0.02 -> +20 at 1000).
5050
care-influence: 0.02
51+
# Breeding-food stars scale that bonus (1★ = 20% … 5★ = 100%, same roll as before).
52+
# Universal feed and items with no cooking quality count as a full bonus.
5153

5254
amount-from-genetics:
5355
- min: 0
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package net.tfminecraft.cooking.husbandry;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.util.UUID;
6+
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
10+
class HusbandryFeedQualityTest {
11+
12+
@BeforeEach
13+
void resetFeeds() {
14+
HusbandryFeedQuality.clear();
15+
}
16+
17+
@Test
18+
void starsScaleLinearlyToAFullBonus() {
19+
assertEquals(0.2, HusbandryFeedQuality.scaleForStars(1), 1e-9);
20+
assertEquals(1.0, HusbandryFeedQuality.scaleForStars(5), 1e-9);
21+
assertEquals(1.0, HusbandryFeedQuality.scaleForStars(9), 1e-9);
22+
}
23+
24+
@Test
25+
void missingFeedsKeepTheFullBonus() {
26+
assertEquals(1.0, HusbandryFeedQuality.combine(null, null), 1e-9);
27+
assertEquals(0.2, HusbandryFeedQuality.combine(0.2, null), 1e-9);
28+
}
29+
30+
@Test
31+
void rememberedFeedsAverageUntilTheyExpire() {
32+
UUID mother = UUID.randomUUID();
33+
UUID father = UUID.randomUUID();
34+
long now = 5_000L;
35+
HusbandryFeedQuality.remember(mother, HusbandryFeedQuality.scaleForStars(1), now);
36+
HusbandryFeedQuality.remember(father, HusbandryFeedQuality.scaleForStars(5), now);
37+
assertEquals(0.6, HusbandryFeedQuality.takePair(mother, father, now), 1e-9);
38+
assertEquals(1.0, HusbandryFeedQuality.takePair(mother, father, now), 1e-9);
39+
}
40+
41+
@Test
42+
void offerCommitsOnlyInsideTheClickWindow() {
43+
UUID animal = UUID.randomUUID();
44+
HusbandryFeedQuality.offer(animal, 0.4, 1_000L);
45+
HusbandryFeedQuality.commitOffer(animal, 1_000L);
46+
assertEquals(0.4, HusbandryFeedQuality.takePair(animal, null, 1_000L), 1e-9);
47+
48+
HusbandryFeedQuality.offer(animal, 0.4, 1_000L);
49+
HusbandryFeedQuality.commitOffer(animal, 4_000L);
50+
assertEquals(1.0, HusbandryFeedQuality.takePair(animal, null, 4_000L), 1e-9);
51+
}
52+
53+
@Test
54+
void expiredLoveFeedIsIgnored() {
55+
UUID animal = UUID.randomUUID();
56+
HusbandryFeedQuality.remember(animal, 0.2, 1_000L);
57+
assertEquals(1.0, HusbandryFeedQuality.takePair(animal, null, 1_000L + 45_001L), 1e-9);
58+
}
59+
60+
@Test
61+
void clearDropsRememberedFeeds() {
62+
UUID animal = UUID.randomUUID();
63+
HusbandryFeedQuality.remember(animal, 0.2, 1_000L);
64+
HusbandryFeedQuality.clear();
65+
assertEquals(1.0, HusbandryFeedQuality.takePair(animal, null, 1_000L), 1e-9);
66+
}
67+
}

‎src/test/java/net/tfminecraft/cooking/husbandry/HusbandryGeneticsTest.java‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,37 @@ void resultIsClampedToZero() {
5151
assertEquals(0, child);
5252
}
5353

54+
@Test
55+
void fiveStarFeedMatchesTheUnscaledRoll() {
56+
int full = HusbandryGenetics.roll(10, 10, 200, 200, maxBonus());
57+
int fiveStars = HusbandryGenetics.roll(
58+
10, 10, 200, 200, maxBonus(), HusbandryFeedQuality.scaleForStars(5));
59+
assertEquals(68, full);
60+
assertEquals(full, fiveStars);
61+
}
62+
63+
@Test
64+
void oneStarFeedKeepsOneFifthOfTheBoost() {
65+
int child = HusbandryGenetics.roll(
66+
10, 10, 200, 200, maxBonus(), HusbandryFeedQuality.scaleForStars(1));
67+
assertEquals(21, child);
68+
}
69+
70+
@Test
71+
void mixedOneAndFiveStarFeedsUseTheAverage() {
72+
double scale = HusbandryFeedQuality.combine(
73+
HusbandryFeedQuality.scaleForStars(1),
74+
HusbandryFeedQuality.scaleForStars(5));
75+
int child = HusbandryGenetics.roll(10, 10, 200, 200, maxBonus(), scale);
76+
assertEquals(44, child);
77+
}
78+
79+
@Test
80+
void zeroCareIgnoresFeedQuality() {
81+
assertEquals(10, HusbandryGenetics.roll(
82+
10, 10, 0, 0, maxBonus(), HusbandryFeedQuality.scaleForStars(1)));
83+
}
84+
5485
private static Random maxBonus() {
5586
return new Random() {
5687
@Override

0 commit comments

Comments
 (0)