Add pregnancy effect and related features, including jump clamping and denial of drinking milk

This commit is contained in:
Chipperfluff 2025-12-23 05:38:26 +01:00
parent 932254b1d9
commit 36e2b9a886
5 changed files with 486 additions and 411 deletions

View File

@ -9,8 +9,19 @@ import net.minecraft.util.Identifier;
public class ModEffects { public class ModEffects {
public static final StatusEffect CHIPI_BLESSING = new ChipiBlessingEffect(); public static final StatusEffect CHIPI_BLESSING = new ChipiBlessingEffect();
public static final StatusEffect PREGNANT = new PregnantEffect();
public static void register() { public static void register() {
Registry.register(Registries.STATUS_EFFECT, new Identifier(ChipiMod.MOD_ID, "chipi_blessing"), CHIPI_BLESSING); Registry.register(
Registries.STATUS_EFFECT,
new Identifier(ChipiMod.MOD_ID, "chipi_blessing"),
CHIPI_BLESSING
);
Registry.register(
Registries.STATUS_EFFECT,
new Identifier(ChipiMod.MOD_ID, "pregnant"),
PREGNANT
);
} }
} }

View File

@ -0,0 +1,127 @@
package net.Chipperfluff.chipi.effect;
import net.Chipperfluff.chipi.entity.ModEntities;
import net.Chipperfluff.chipi.entity.PlayerJrEntity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.attribute.EntityAttributeInstance;
import net.minecraft.entity.attribute.EntityAttributes;
import net.minecraft.entity.attribute.EntityAttributeModifier;
import net.minecraft.entity.effect.StatusEffect;
import net.minecraft.entity.effect.StatusEffectCategory;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import java.util.UUID;
public class PregnantEffect extends StatusEffect {
public static final int TOTAL_DURATION = 20 * 60 * 10; // 10 min
private static final double MAX_SLOW = 0.90;
private static final UUID SPEED_UUID =
UUID.fromString("7c8b6b8f-4b6c-4f6f-9b7b-01c8e9f8a111");
public PregnantEffect() {
super(StatusEffectCategory.HARMFUL, 0xFFB6C1);
}
/* ---------- helpers used by mixin ---------- */
/** 0.0 → normal jump, 1.0 → no jump */
public static double getJumpClamp(ServerPlayerEntity player) {
StatusEffectInstance inst = player.getStatusEffect(ModEffects.PREGNANT);
if (inst == null) return 0.0;
double progress =
1.0 - (inst.getDuration() / (double) TOTAL_DURATION);
return Math.min(Math.max(progress, 0.0), 1.0);
}
/* ---------- ticking ---------- */
@Override
public boolean canApplyUpdateEffect(int duration, int amplifier) {
return true;
}
@Override
public void applyUpdateEffect(LivingEntity entity, int amplifier) {
if (!(entity instanceof ServerPlayerEntity player)) return;
StatusEffectInstance inst = player.getStatusEffect(this);
if (inst == null) return;
int remaining = inst.getDuration();
double progress =
1.0 - (remaining / (double) TOTAL_DURATION);
progress = Math.min(Math.max(progress, 0.0), 1.0);
applySpeedModifier(player, progress * MAX_SLOW);
if (remaining % 400 == 0 && remaining > 20) {
player.sendMessage(
Text.literal("Your knees file a formal complaint…"),
true
);
}
if (remaining == 1) {
finishBirth(player);
}
}
private void applySpeedModifier(ServerPlayerEntity player, double slowAmount) {
EntityAttributeInstance attr =
player.getAttributeInstance(EntityAttributes.GENERIC_MOVEMENT_SPEED);
if (attr == null) return;
EntityAttributeModifier old = attr.getModifier(SPEED_UUID);
if (old != null) attr.removeModifier(old);
if (slowAmount > 0.01) {
attr.addTemporaryModifier(new EntityAttributeModifier(
SPEED_UUID,
"Pregnancy slowdown",
-slowAmount,
EntityAttributeModifier.Operation.MULTIPLY_TOTAL
));
}
}
private void finishBirth(ServerPlayerEntity dad) {
// cleanup
EntityAttributeInstance attr =
dad.getAttributeInstance(EntityAttributes.GENERIC_MOVEMENT_SPEED);
if (attr != null) {
EntityAttributeModifier old = attr.getModifier(SPEED_UUID);
if (old != null) attr.removeModifier(old);
}
dad.removeStatusEffect(this);
PlayerJrEntity jr = ModEntities.PLAYER_JR.create(dad.getWorld());
if (jr == null) return;
jr.refreshPositionAndAngles(
dad.getX(), dad.getY(), dad.getZ(),
dad.getYaw(), dad.getPitch()
);
jr.setDad(dad);
dad.getWorld().spawnEntity(jr);
dad.sendMessage(
Text.literal(
"You're a dad now. The names " +
jr.getName().getString() +
". Warning: it might cheat."
),
false
);
}
}

View File

@ -1,9 +1,10 @@
package net.Chipperfluff.chipi.entity; package net.Chipperfluff.chipi.entity;
import net.minecraft.entity.EntityType; import net.minecraft.entity.EntityType;
import net.minecraft.entity.ai.goal.*; import net.minecraft.entity.ai.goal.Goal;
import net.minecraft.entity.ai.pathing.Path; import net.minecraft.entity.ai.goal.LookAroundGoal;
import net.minecraft.entity.ai.pathing.PathNode; import net.minecraft.entity.ai.goal.LookAtEntityGoal;
import net.minecraft.entity.ai.goal.SwimGoal;
import net.minecraft.entity.attribute.DefaultAttributeContainer; import net.minecraft.entity.attribute.DefaultAttributeContainer;
import net.minecraft.entity.attribute.EntityAttributes; import net.minecraft.entity.attribute.EntityAttributes;
import net.minecraft.entity.damage.DamageSource; import net.minecraft.entity.damage.DamageSource;
@ -17,83 +18,51 @@ import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d; import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World; import net.minecraft.world.World;
import java.util.List;
import java.util.UUID; import java.util.UUID;
public class PlayerJrEntity extends PathAwareEntity { public class PlayerJrEntity extends PathAwareEntity {
/* ---------------- CONFIG ---------------- */ /* ================= RULES (CHEATING FRIEND) ================= */
private static final int CHAT_INTERVAL = 20 * 6; private static final double FORCE_FLY_DISTANCE = 50.0; // fly if farther than this (horizontal)
private static final int STUCK_TICKS_TO_FLY = 40; // 2 seconds of no horizontal progress
private static final double STUCK_MIN_CLOSING = 0.10; // horizontal progress threshold
private static final double STUCK_MIN_DISTANCE = 6.0; // don't care if already close
private static final double DAD_MOVING_EPS_SQ = 0.02; // if dad is moving, don't call it stuck
// When to consider cheating private static final int LAND_RADIUS = 6; // search dry landing within this radius
private static final double FAR_DISTANCE = 30.0; // if far and path sucks -> cheat private static final double LAND_HORIZONTAL_DISTANCE = 2.0; // must be this close (horiz) to land
private static final int MAX_PATH_LENGTH = 45; // path too long -> cheat private static final double HOVER_HEIGHT = 1.25; // hover above landing block
private static final int MAX_VERTICAL_GAP = 6; // dad much higher than path end -> cheat private static final int MIN_FLY_TICKS = 20; // must fly at least this long
private static final int STUCK_CHECK_EVERY = 20; // check progress every 1s
private static final double STUCK_MIN_PROGRESS = 1.2; // blocks per second expected
private static final int STUCK_STRIKES_TO_FLY = 3; // needs 3 bad checks (avoids spam)
// Cheat (flight) controls private static final double FLY_SPEED = 0.55;
private static final int FLY_COOLDOWN_TICKS = 100; // max once per 5 seconds private static final double FLY_ACCEL = 0.25;
private static final int MIN_FLY_TICKS = 30; // must stay in "creative" at least 1.5s private static final int MAX_VERTICAL_GAP = 6; // if dad is this high above -> fly
private static final int LAND_STABLE_TICKS = 6; // must be stable on a standable spot for this long
private static final double LAND_DISTANCE = 2.2; // near landing target
private static final double LANDING_SEARCH_RADIUS = 6.0;
// Flight feel /* ================= STATE ================= */
private static final double FLY_SPEED = 0.55; // horizontal
private static final double FLY_ACCEL = 0.20; // smoothing (0..1)
private static final double LAND_DESCEND_SPEED = 0.15; // gentle descent
private static final double HOVER_HEIGHT_ABOVE_DAD = 2.6; // where it hovers when no landing spot
/* ---------------- STATE ---------------- */
private UUID dadUuid; private UUID dadUuid;
private String dadName = ""; private String dadName = "";
private int chatCooldown = 40;
private enum TravelMode { private enum Mode { GROUND, FLYING }
GROUND, private Mode mode = Mode.GROUND;
FLYING,
LANDING
}
private TravelMode travelMode = TravelMode.GROUND; private int flyTicks = 0;
// Anti-spam + stability // Stuck tracking (ground)
private int flyCooldown = 0; // counts down private int stuckTicks = 0;
private int flyTicks = 0; // how long we've been flying private double lastDadHorizDist = -1;
private int landingStableTicks = 0; // stable-on-ground counter
// "Is it actually stuck?" tracking // Landing / hover state
private int stuckCheckTimer = STUCK_CHECK_EVERY; private BlockPos landingSpot = null;
private Vec3d lastStuckPos = null; private BlockPos forcedLandingSpot = null;
private int stuckStrikes = 0;
// landing target caching (reduces jitter) /* ================= CONSTRUCTOR ================= */
private BlockPos landingTarget = null;
private int landingTargetRefresh = 0;
/* ---------------- CHAT ---------------- */
private static final List<String> MESSAGES = List.of(
"hold on…",
"yeah no this path is cooked",
"creative moment, dont look",
"brb disabling gravity",
"pathfinding said no",
"minecraft moment detected"
);
/* ---------------- CONSTRUCTOR ---------------- */
public PlayerJrEntity(EntityType<? extends PathAwareEntity> type, World world) { public PlayerJrEntity(EntityType<? extends PathAwareEntity> type, World world) {
super(type, world); super(type, world);
} }
/* ---------------- ATTRIBUTES ---------------- */
public static DefaultAttributeContainer.Builder createAttributes() { public static DefaultAttributeContainer.Builder createAttributes() {
return MobEntity.createMobAttributes() return MobEntity.createMobAttributes()
.add(EntityAttributes.GENERIC_MAX_HEALTH, 10.0) .add(EntityAttributes.GENERIC_MAX_HEALTH, 10.0)
@ -101,13 +70,13 @@ public class PlayerJrEntity extends PathAwareEntity {
.add(EntityAttributes.GENERIC_FOLLOW_RANGE, 64.0); .add(EntityAttributes.GENERIC_FOLLOW_RANGE, 64.0);
} }
/* ---------------- DAD ---------------- */ /* ================= DAD ================= */
public void setDad(ServerPlayerEntity dad) { public void setDad(ServerPlayerEntity dad) {
this.dadUuid = dad.getUuid(); dadUuid = dad.getUuid();
this.dadName = dad.getGameProfile().getName(); dadName = dad.getGameProfile().getName();
this.setCustomName(Text.literal(this.dadName + " jr.")); setCustomName(Text.literal(dadName + " jr."));
this.setCustomNameVisible(true); setCustomNameVisible(true);
} }
private ServerPlayerEntity getDad() { private ServerPlayerEntity getDad() {
@ -115,428 +84,296 @@ public class PlayerJrEntity extends PathAwareEntity {
return getWorld().getServer().getPlayerManager().getPlayer(dadUuid); return getWorld().getServer().getPlayerManager().getPlayer(dadUuid);
} }
public String getDadName() { public String getDadName() { return dadName; }
return dadName;
}
public UUID getDadUuid() { /* ================= AI ================= */
return dadUuid;
}
/* ---------------- GOALS ---------------- */
@Override @Override
protected void initGoals() { protected void initGoals() {
this.goalSelector.add(1, new SwimGoal(this)); goalSelector.add(1, new SwimGoal(this));
this.goalSelector.add(2, new FollowDadGoal(this)); goalSelector.add(2, new FollowDadGoal(this));
this.goalSelector.add(3, new WanderAroundGoal(this, 0.8)); goalSelector.add(3, new LookAtEntityGoal(this, PlayerEntity.class, 6f));
this.goalSelector.add(4, new LookAtEntityGoal(this, PlayerEntity.class, 6f)); goalSelector.add(4, new LookAroundGoal(this));
this.goalSelector.add(5, new LookAroundGoal(this));
} }
/* ---------------- TICK ---------------- */ /* ================= TICK ================= */
@Override @Override
public void tick() { public void tick() {
super.tick(); super.tick();
if (getWorld().isClient) return; if (getWorld().isClient) return;
// Never fall damage ever (you asked this specifically)
this.fallDistance = 0.0f;
ServerPlayerEntity dad = getDad(); ServerPlayerEntity dad = getDad();
if (dad == null) return; if (dad == null) return;
if (flyCooldown > 0) flyCooldown--; switch (mode) {
case GROUND -> doGroundBrain(dad);
// movement state machine case FLYING -> doFlyingBrain(dad);
switch (travelMode) {
case GROUND -> {
evaluateCheatNeed(dad);
}
case FLYING -> {
flyTicks++;
doFlying(dad);
}
case LANDING -> {
flyTicks++;
doLanding(dad);
}
}
// chat
chatCooldown--;
if (chatCooldown <= 0 && this.squaredDistanceTo(dad) < 36 && !MESSAGES.isEmpty()) {
dad.sendMessage(Text.literal(MESSAGES.get(this.random.nextInt(MESSAGES.size()))), true);
chatCooldown = CHAT_INTERVAL;
} }
} }
/* ---------------- CHEAT DECISION ---------------- */ /* ================= CORE BRAIN: GROUND ================= */
private void evaluateCheatNeed(ServerPlayerEntity dad) { private void doGroundBrain(ServerPlayerEntity dad) {
// Too close? don't cheat. // Water cheat: if land is nearby, prefer walking out; if no path, fly briefly to land.
double distSq = this.squaredDistanceTo(dad); if (this.isTouchingWater()) {
if (distSq < (12.0 * 12.0)) { BlockPos escape = findLandingSpotNear(this.getBlockPos(), LAND_RADIUS);
resetStuckTracking(); // near dad, don't keep strikes if (escape != null) {
return; boolean canWalk = getNavigation().startMovingTo(
} escape.getX() + 0.5,
escape.getY(),
// If on cooldown, we still allow normal pathing but don't fly. escape.getZ() + 0.5,
if (flyCooldown > 0) return; 1.1
);
Path path = this.getNavigation().getCurrentPath(); if (canWalk) {
resetStuckTracking(dad);
boolean far = distSq > (FAR_DISTANCE * FAR_DISTANCE); return;
boolean noPath = path == null; }
boolean tooLong = path != null && path.getLength() > MAX_PATH_LENGTH; forcedLandingSpot = escape;
boolean finishedButStillFar = path != null && path.isFinished() && far; startFlying(dad);
boolean pathWater = path != null && pathTouchesWater(path);
boolean dadHighButPathLow = path != null && dadIsHighButPathStaysLow(dad, path);
// "stuck" detection (only counts if far-ish)
if (distSq > (18.0 * 18.0)) {
boolean stuck = checkStuck();
if (stuck) stuckStrikes++;
else stuckStrikes = Math.max(0, stuckStrikes - 1);
} else {
stuckStrikes = 0;
}
boolean pathIsCooked = noPath || tooLong || finishedButStillFar || pathWater || dadHighButPathLow;
boolean trulyStuck = stuckStrikes >= STUCK_STRIKES_TO_FLY;
// Only cheat if far AND (path is cooked OR truly stuck)
if (far && (pathIsCooked || trulyStuck)) {
startFlying(dad);
}
}
private boolean checkStuck() {
stuckCheckTimer--;
if (stuckCheckTimer > 0) return false;
stuckCheckTimer = STUCK_CHECK_EVERY;
Vec3d now = this.getPos();
if (lastStuckPos == null) {
lastStuckPos = now;
return false;
}
double moved = now.distanceTo(lastStuckPos);
lastStuckPos = now;
// expected progress in 1s ~ STUCK_MIN_PROGRESS blocks
return moved < STUCK_MIN_PROGRESS;
}
private void resetStuckTracking() {
stuckCheckTimer = STUCK_CHECK_EVERY;
lastStuckPos = null;
stuckStrikes = 0;
}
private boolean pathTouchesWater(Path path) {
for (int i = 0; i < path.getLength(); i++) {
PathNode node = path.getNode(i);
BlockPos pos = new BlockPos(node.x, node.y, node.z);
if (!getWorld().getFluidState(pos).isEmpty()) {
return true;
}
}
return false;
}
private boolean dadIsHighButPathStaysLow(ServerPlayerEntity dad, Path path) {
if (path.getLength() == 0) return false;
PathNode end = path.getNode(path.getLength() - 1);
int endY = end.y;
int dadY = dad.getBlockY();
return dadY - endY >= MAX_VERTICAL_GAP;
}
/* ---------------- FLYING ---------------- */
private void startFlying(ServerPlayerEntity dad) {
if (travelMode != TravelMode.GROUND) return;
travelMode = TravelMode.FLYING;
flyTicks = 0;
landingStableTicks = 0;
landingTarget = null;
landingTargetRefresh = 0;
this.setNoGravity(true);
this.getNavigation().stop();
this.setVelocity(Vec3d.ZERO);
this.velocityDirty = true;
// cooldown starts now (prevents spam toggles)
flyCooldown = FLY_COOLDOWN_TICKS;
dad.sendMessage(Text.literal("* " + getName().getString() + " switched to Creative Mode"), false);
}
private void doFlying(ServerPlayerEntity dad) {
// refresh landing target occasionally (not every tick to avoid jitter)
if (landingTargetRefresh-- <= 0) {
landingTargetRefresh = 6; // update ~3 times/sec
landingTarget = findLandingSpotBlock(dad);
}
Vec3d target;
if (landingTarget != null) {
target = Vec3d.ofCenter(landingTarget);
} else {
// no landing spot => hover above dad and keep searching
target = dad.getPos().add(0, HOVER_HEIGHT_ABOVE_DAD, 0);
}
// If we have a landing block and we are close enough, go LANDING
if (landingTarget != null) {
double dist = this.getPos().distanceTo(Vec3d.ofCenter(landingTarget));
if (dist < LAND_DISTANCE) {
travelMode = TravelMode.LANDING;
landingStableTicks = 0;
// stop forward movement before landing phase
this.setVelocity(Vec3d.ZERO);
this.velocityDirty = true;
return; return;
} }
} }
// smooth creative-like movement // Update stuck tracking (only on ground mode)
flyTowardSmooth(target); updateStuckTracking(dad);
this.lookAtEntity(dad, 30f, 30f); if (stuckTicks >= STUCK_TICKS_TO_FLY) {
} startFlying(dad);
private void doLanding(ServerPlayerEntity dad) {
// MUST stay in "creative" for at least MIN_FLY_TICKS before returning to ground
if (flyTicks < MIN_FLY_TICKS) {
// keep holding position / gentle settle
this.setVelocity(Vec3d.ZERO);
this.velocityDirty = true;
return; return;
} }
// If we lost landing target, go back to flying (but keep creative) // Too far behind (impatient cheat)
if (landingTarget == null) { if (horizontalDistanceTo(dad) > FORCE_FLY_DISTANCE) {
travelMode = TravelMode.FLYING; startFlying(dad);
return; return;
} }
// If landing target became invalid, go back to flying (dont drop) // Dad is way above us (can't reach)
if (!canStandAt(landingTarget)) { double dy = dad.getY() - getY();
travelMode = TravelMode.FLYING; if (dy > MAX_VERTICAL_GAP && horizontalDistanceTo(dad) > 2.0) {
landingTarget = null; startFlying(dad);
}
}
private void updateStuckTracking(ServerPlayerEntity dad) {
boolean tryingToWalk = !getNavigation().isIdle();
double dadDist = horizontalDistanceTo(dad);
double dadSpeedSq = dad.getVelocity().horizontalLengthSquared();
if (!tryingToWalk || dadDist <= STUCK_MIN_DISTANCE || dadSpeedSq > DAD_MOVING_EPS_SQ) {
stuckTicks = 0;
lastDadHorizDist = dadDist;
return; return;
} }
// We want to be exactly centered above the landing block if (lastDadHorizDist < 0) {
Vec3d center = Vec3d.ofCenter(landingTarget); lastDadHorizDist = dadDist;
Vec3d pos = this.getPos(); stuckTicks = 0;
// horizontal adjust (gentle)
Vec3d horiz = new Vec3d(center.x - pos.x, 0, center.z - pos.z);
double horizLen = horiz.length();
double vx = 0, vz = 0;
if (horizLen > 0.08) {
Vec3d hn = horiz.normalize().multiply(Math.min(FLY_SPEED * 0.6, horizLen));
vx = hn.x;
vz = hn.z;
}
// descend gently until feet are on standable position
double vy = -LAND_DESCEND_SPEED;
// If we are basically at block level, stop vertical movement
double dy = center.y - pos.y;
if (dy > -0.15 && dy < 0.65) {
vy = 0;
}
this.setVelocity(vx, vy, vz);
this.velocityDirty = true;
// "stable landing" means: our current feet pos is standable and we are almost not moving
BlockPos feet = this.getBlockPos();
boolean stableFeet = canStandAt(feet) || feet.equals(landingTarget);
boolean slow = this.getVelocity().lengthSquared() < 0.01;
if (stableFeet && slow) {
landingStableTicks++;
} else {
landingStableTicks = 0;
}
if (landingStableTicks >= LAND_STABLE_TICKS) {
stopFlying(dad);
}
}
private void stopFlying(ServerPlayerEntity dad) {
travelMode = TravelMode.GROUND;
this.setVelocity(Vec3d.ZERO);
this.velocityDirty = true;
this.setNoGravity(false);
// after returning, clear stuck tracking so it doesn't instantly cheat again
resetStuckTracking();
// don't instantly re-cheat; cooldown already running, but this avoids instant flip due to path recalculation
landingTarget = null;
landingTargetRefresh = 0;
dad.sendMessage(Text.literal("* " + getName().getString() + " returned to Survival Mode"), false);
}
private void flyTowardSmooth(Vec3d target) {
Vec3d dir = target.subtract(this.getPos());
if (dir.lengthSquared() < 0.0005) {
// gently stop
Vec3d v = this.getVelocity().multiply(0.5);
if (v.lengthSquared() < 0.0002) v = Vec3d.ZERO;
this.setVelocity(v);
this.velocityDirty = true;
return; return;
} }
Vec3d desired = dir.normalize().multiply(FLY_SPEED); double closing = lastDadHorizDist - dadDist; // positive means getting closer
if (closing < STUCK_MIN_CLOSING) stuckTicks++;
else stuckTicks = 0;
// smoothing: v = v*(1-a) + desired*a lastDadHorizDist = dadDist;
Vec3d current = this.getVelocity();
Vec3d blended = current.multiply(1.0 - FLY_ACCEL).add(desired.multiply(FLY_ACCEL));
this.setVelocity(blended);
this.velocityDirty = true;
} }
/* ---------------- LANDING TARGET SEARCH ---------------- */ private void resetStuckTracking(ServerPlayerEntity dad) {
stuckTicks = 0;
lastDadHorizDist = horizontalDistanceTo(dad);
}
private BlockPos findLandingSpotBlock(ServerPlayerEntity dad) { /* ================= CORE BRAIN: FLYING ================= */
BlockPos center = dad.getBlockPos();
double bestDist = Double.MAX_VALUE;
BlockPos best = null;
int radius = (int) Math.ceil(LANDING_SEARCH_RADIUS); private void startFlying(ServerPlayerEntity dad) {
mode = Mode.FLYING;
flyTicks = 0;
// prefer roughly same Y as dad feet-ish (+0..+2) landingSpot = null;
int minY = center.getY() - 2;
int maxY = center.getY() + 3;
for (int dx = -radius; dx <= radius; dx++) { setNoGravity(true);
for (int dz = -radius; dz <= radius; dz++) { getNavigation().stop();
// quick circle check setVelocity(Vec3d.ZERO);
if ((dx * dx + dz * dz) > (LANDING_SEARCH_RADIUS * LANDING_SEARCH_RADIUS)) continue; velocityDirty = true;
for (int y = minY; y <= maxY; y++) { dad.sendMessage(Text.literal(getName().getString() + " switched to Game Mode Creative"), false);
BlockPos pos = new BlockPos(center.getX() + dx, y, center.getZ() + dz); }
if (!canStandAt(pos)) continue;
// also avoid landing inside water or on water edge private void doFlyingBrain(ServerPlayerEntity dad) {
if (!getWorld().getFluidState(pos).isEmpty()) continue; flyTicks++;
if (!getWorld().getFluidState(pos.down()).isEmpty()) continue;
double d = this.getPos().squaredDistanceTo(Vec3d.ofCenter(pos)); double horiz = horizontalDistanceTo(dad);
if (d < bestDist) {
bestDist = d; if (flyTicks >= MIN_FLY_TICKS) {
best = pos; BlockPos candidate = forcedLandingSpot;
} if (candidate == null) {
candidate = findLandingSpotNear(dad.getBlockPos(), LAND_RADIUS);
}
if (candidate != null && !wouldImmediatelyRefly(dad, candidate)) {
landingSpot = candidate;
if (horizontalDistanceTo(landingSpot) <= LAND_HORIZONTAL_DISTANCE) {
stopFlying(dad);
return;
} }
} }
} }
BlockPos targetSpot = forcedLandingSpot != null ? forcedLandingSpot : landingSpot;
Vec3d target = targetSpot != null
? Vec3d.ofCenter(targetSpot).add(0, HOVER_HEIGHT, 0)
: dad.getPos().add(0, 2.2, 0);
flyTowardSmooth(target);
lookAtEntity(dad, 30f, 30f);
}
private void stopFlying(ServerPlayerEntity dad) {
setNoGravity(false);
mode = Mode.GROUND;
landingSpot = null;
forcedLandingSpot = null;
dad.sendMessage(Text.literal(getName().getString() + " switched to Game Mode Survival"), false);
}
/* ================= MOVEMENT HELPERS ================= */
private void flyTowardSmooth(Vec3d target) {
Vec3d delta = target.subtract(getPos());
double distSq = delta.lengthSquared();
if (distSq < 0.0006) {
setVelocity(getVelocity().multiply(0.6));
velocityDirty = true;
return;
}
Vec3d desired = delta.normalize().multiply(FLY_SPEED);
// stronger steer when far (prevents drifting)
double accel = (distSq > 64.0) ? Math.min(0.55, FLY_ACCEL * 2.0) : FLY_ACCEL;
Vec3d blended = getVelocity().multiply(1.0 - accel).add(desired.multiply(accel));
setVelocity(blended);
velocityDirty = true;
}
private double horizontalDistanceTo(ServerPlayerEntity dad) {
double dx = dad.getX() - getX();
double dz = dad.getZ() - getZ();
return Math.sqrt(dx * dx + dz * dz);
}
private double horizontalDistanceTo(BlockPos pos) {
double dx = pos.getX() + 0.5 - getX();
double dz = pos.getZ() + 0.5 - getZ();
return Math.sqrt(dx * dx + dz * dz);
}
/* ================= LAND SEARCH (DRY + SAFE) ================= */
private BlockPos findLandingSpotNear(BlockPos center, int radius) {
BlockPos best = null;
double bestScore = Double.MAX_VALUE;
int minY = center.getY() - 3;
int maxY = center.getY() + 3;
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
if ((dx * dx + dz * dz) > radius * radius) continue;
for (int y = minY; y <= maxY; y++) {
BlockPos p = new BlockPos(center.getX() + dx, y, center.getZ() + dz);
if (!canStandAt(p)) continue;
// prefer closer to dad AND closer to us a bit
double toCenter = p.getSquaredDistance(center);
double toMe = this.getPos().squaredDistanceTo(Vec3d.ofCenter(p));
double score = toCenter * 1.0 + toMe * 0.10;
if (score < bestScore) {
bestScore = score;
best = p;
}
}
}
}
return best; return best;
} }
private boolean canStandAt(BlockPos pos) { private boolean canStandAt(BlockPos pos) {
// feet + head must be air World w = getWorld();
if (!getWorld().getBlockState(pos).isAir()) return false;
if (!getWorld().getBlockState(pos.up()).isAir()) return false;
// block below must be solid // feet + head air
if (!w.getBlockState(pos).isAir()) return false;
if (!w.getBlockState(pos.up()).isAir()) return false;
// solid below
BlockPos below = pos.down(); BlockPos below = pos.down();
return getWorld().getBlockState(below).isSolidBlock(getWorld(), below); if (!w.getBlockState(below).isSolidBlock(w, below)) return false;
// not in fluid, not standing on fluid
if (!w.getFluidState(pos).isEmpty()) return false;
if (!w.getFluidState(below).isEmpty()) return false;
// also avoid lava blocks around feet area (simple dont be stupid)
// (fluid check already catches lava fluid; this catches blocks like magma? optional)
return true;
} }
/* ---------------- DEATH ---------------- */ private boolean wouldImmediatelyRefly(ServerPlayerEntity dad, BlockPos landPos) {
if (horizontalDistanceTo(dad) > FORCE_FLY_DISTANCE) return true;
if (dad.getY() - landPos.getY() > MAX_VERTICAL_GAP) return true;
return false;
}
/* ================= FALL DAMAGE ================= */
@Override @Override
public void onDeath(DamageSource source) { public boolean handleFallDamage(float fallDistance, float damageMultiplier, DamageSource source) {
super.onDeath(source); return false;
if (getWorld().isClient) return;
getWorld().createExplosion(
this,
getX(),
getY(),
getZ(),
3.0f,
World.ExplosionSourceType.MOB
);
ServerPlayerEntity dad = getDad();
if (dad != null) {
dad.sendMessage(Text.literal("oh dad im gonna go"), false);
}
} }
/* ---------------- NAME ---------------- */ /* ================= SAVE / LOAD ================= */
@Override
public float getNameLabelHeight() {
return this.getHeight() + 1.5f; // YES. STILL CORRECT.
}
/* ---------------- SAVE ---------------- */
@Override @Override
public void writeCustomDataToNbt(NbtCompound nbt) { public void writeCustomDataToNbt(NbtCompound nbt) {
if (dadUuid != null) nbt.putUuid("Dad", dadUuid); if (dadUuid != null) nbt.putUuid("Dad", dadUuid);
nbt.putString("DadName", dadName); nbt.putString("DadName", dadName);
nbt.putString("TravelMode", travelMode.name()); nbt.putString("Mode", mode.name());
nbt.putInt("FlyCooldown", flyCooldown);
nbt.putInt("FlyTicks", flyTicks); nbt.putInt("FlyTicks", flyTicks);
nbt.putInt("LandingStable", landingStableTicks);
nbt.putInt("StuckStrikes", stuckStrikes);
} }
@Override @Override
public void readCustomDataFromNbt(NbtCompound nbt) { public void readCustomDataFromNbt(NbtCompound nbt) {
if (nbt.containsUuid("Dad")) dadUuid = nbt.getUuid("Dad"); if (nbt.containsUuid("Dad")) dadUuid = nbt.getUuid("Dad");
if (nbt.contains("DadName")) dadName = nbt.getString("DadName"); if (nbt.contains("DadName")) dadName = nbt.getString("DadName");
if (nbt.contains("Mode")) {
if (nbt.contains("TravelMode")) { mode = Mode.valueOf(nbt.getString("Mode"));
travelMode = TravelMode.valueOf(nbt.getString("TravelMode")); setNoGravity(mode != Mode.GROUND);
this.setNoGravity(travelMode != TravelMode.GROUND);
} }
if (nbt.contains("FlyCooldown")) flyCooldown = nbt.getInt("FlyCooldown");
if (nbt.contains("FlyTicks")) flyTicks = nbt.getInt("FlyTicks"); if (nbt.contains("FlyTicks")) flyTicks = nbt.getInt("FlyTicks");
if (nbt.contains("LandingStable")) landingStableTicks = nbt.getInt("LandingStable");
if (nbt.contains("StuckStrikes")) stuckStrikes = nbt.getInt("StuckStrikes");
// reset volatile runtime fields // volatile reset
stuckCheckTimer = STUCK_CHECK_EVERY; landingSpot = null;
lastStuckPos = null; forcedLandingSpot = null;
landingTarget = null; lastDadHorizDist = -1;
landingTargetRefresh = 0; stuckTicks = 0;
} }
/* ---------------- FOLLOW GOAL ---------------- */ /* ================= FOLLOW GOAL ================= */
static class FollowDadGoal extends Goal { static class FollowDadGoal extends Goal {
private final PlayerJrEntity jr; private final PlayerJrEntity jr;
FollowDadGoal(PlayerJrEntity jr) { this.jr = jr; }
FollowDadGoal(PlayerJrEntity jr) {
this.jr = jr;
}
@Override @Override
public boolean canStart() { public boolean canStart() {
return jr.travelMode == TravelMode.GROUND && jr.getDad() != null; return jr.mode == Mode.GROUND && jr.getDad() != null;
} }
@Override @Override

View File

@ -1,16 +1,20 @@
package net.Chipperfluff.chipi.item; package net.Chipperfluff.chipi.item;
import net.Chipperfluff.chipi.effect.ModEffects;
import net.Chipperfluff.chipi.sound.ModSounds;
import net.minecraft.entity.LivingEntity; import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.Item; import net.minecraft.item.Item;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.item.ItemUsage;
import net.minecraft.item.Items; import net.minecraft.item.Items;
import net.minecraft.util.UseAction;
import net.minecraft.world.World;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.Chipperfluff.chipi.effect.ModEffects;
import net.Chipperfluff.chipi.sound.ModSounds;
import net.minecraft.sound.SoundCategory; import net.minecraft.sound.SoundCategory;
import net.minecraft.util.Hand;
import net.minecraft.util.UseAction;
import net.minecraft.util.TypedActionResult;
import net.minecraft.world.World;
import net.minecraft.text.Text;
public class PlayerMilkItem extends Item { public class PlayerMilkItem extends Item {
@ -18,6 +22,30 @@ public class PlayerMilkItem extends Item {
super(settings); super(settings);
} }
/* ================= DENY DRINKING ================= */
@Override
public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
ItemStack stack = user.getStackInHand(hand);
// Already pregnant deny drinking
if (user.hasStatusEffect(ModEffects.PREGNANT)) {
if (!world.isClient) {
user.sendMessage(
Text.literal("You already feel something growing..."),
true
);
}
return TypedActionResult.fail(stack);
}
// Start drinking normally
user.setCurrentHand(hand);
return TypedActionResult.consume(stack);
}
/* ================= DRINK ANIMATION ================= */
@Override @Override
public UseAction getUseAction(ItemStack stack) { public UseAction getUseAction(ItemStack stack) {
return UseAction.DRINK; return UseAction.DRINK;
@ -28,10 +56,23 @@ public class PlayerMilkItem extends Item {
return 32; return 32;
} }
/* ================= FINISH DRINK ================= */
@Override @Override
public ItemStack finishUsing(ItemStack stack, World world, LivingEntity user) { public ItemStack finishUsing(ItemStack stack, World world, LivingEntity user) {
if (!world.isClient && user instanceof PlayerEntity player) { if (!world.isClient && user instanceof PlayerEntity player) {
// Apply pregnancy effect
player.addStatusEffect(new StatusEffectInstance(
ModEffects.PREGNANT,
20 * 60 * 10, // 10 minutes
0,
false,
true
));
// Play sound
world.playSound( world.playSound(
null, null,
player.getBlockPos(), player.getBlockPos(),
@ -42,8 +83,10 @@ public class PlayerMilkItem extends Item {
); );
} }
if (user instanceof PlayerEntity player && !player.getAbilities().creativeMode) { // CORRECT vanilla behavior:
return new ItemStack(Items.BUCKET); // consumes milk and gives bucket
if (user instanceof PlayerEntity player) {
return ItemUsage.exchangeStack(stack, player, new ItemStack(Items.BUCKET));
} }
return stack; return stack;

View File

@ -1,10 +1,15 @@
package net.Chipperfluff.chipi.mixin; package net.Chipperfluff.chipi.mixin;
import com.mojang.authlib.GameProfile; import com.mojang.authlib.GameProfile;
import net.Chipperfluff.chipi.effect.ModEffects;
import net.Chipperfluff.chipi.effect.PregnantEffect;
import net.Chipperfluff.chipi.util.ChipiTrackedData; import net.Chipperfluff.chipi.util.ChipiTrackedData;
import net.minecraft.entity.data.DataTracker; import net.minecraft.entity.data.DataTracker;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World; import net.minecraft.world.World;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
@ -14,11 +19,63 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(PlayerEntity.class) @Mixin(PlayerEntity.class)
public abstract class PlayerEntityMixin { public abstract class PlayerEntityMixin {
/* ============================================================
INIT: tracked data
============================================================ */
@Inject(method = "<init>", at = @At("TAIL")) @Inject(method = "<init>", at = @At("TAIL"))
private void chipi$initTrackedData(World world, BlockPos pos, float yaw, GameProfile profile, CallbackInfo ci) { private void chipi$initTrackedData(
World world,
BlockPos pos,
float yaw,
GameProfile profile,
CallbackInfo ci
) {
PlayerEntity self = (PlayerEntity)(Object)this; PlayerEntity self = (PlayerEntity)(Object)this;
DataTracker tracker = self.getDataTracker(); DataTracker tracker = self.getDataTracker();
tracker.startTracking(ChipiTrackedData.CHIPI_ENERGY, 1.0f); tracker.startTracking(ChipiTrackedData.CHIPI_ENERGY, 1.0f);
} }
/* ============================================================
JUMP CLAMP: pregnancy logic
============================================================ */
@Inject(
method = "jump",
at = @At("HEAD"),
cancellable = true
)
private void chipi$pregnancyJumpClamp(CallbackInfo ci) {
if (!((Object)this instanceof ServerPlayerEntity player)) return;
StatusEffectInstance inst =
player.getStatusEffect(ModEffects.PREGNANT);
if (inst == null) return; // normal jump
int total = PregnantEffect.TOTAL_DURATION;
int remaining = inst.getDuration();
// progress 0.0 1.0
double progress =
1.0 - ((double)remaining / total);
progress = Math.min(Math.max(progress, 0.0), 1.0);
// vanilla jump velocity 0.42
double maxJump = 0.42 * (1.0 - progress);
// End stage: NO jumping at all
if (maxJump < 0.05) {
ci.cancel();
return;
}
Vec3d vel = player.getVelocity();
player.setVelocity(vel.x, maxJump, vel.z);
player.velocityDirty = true;
// stop vanilla jump
ci.cancel();
}
} }