diff --git a/src/main/java/net/Chipperfluff/chipi/effect/ModEffects.java b/src/main/java/net/Chipperfluff/chipi/effect/ModEffects.java index 146f786..12e0b3b 100644 --- a/src/main/java/net/Chipperfluff/chipi/effect/ModEffects.java +++ b/src/main/java/net/Chipperfluff/chipi/effect/ModEffects.java @@ -9,8 +9,19 @@ import net.minecraft.util.Identifier; public class ModEffects { public static final StatusEffect CHIPI_BLESSING = new ChipiBlessingEffect(); + public static final StatusEffect PREGNANT = new PregnantEffect(); 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 + ); } } diff --git a/src/main/java/net/Chipperfluff/chipi/effect/PregnantEffect.java b/src/main/java/net/Chipperfluff/chipi/effect/PregnantEffect.java new file mode 100644 index 0000000..b8d0cbc --- /dev/null +++ b/src/main/java/net/Chipperfluff/chipi/effect/PregnantEffect.java @@ -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 name’s " + + jr.getName().getString() + + ". Warning: it might cheat." + ), + false + ); + } +} diff --git a/src/main/java/net/Chipperfluff/chipi/entity/PlayerJrEntity.java b/src/main/java/net/Chipperfluff/chipi/entity/PlayerJrEntity.java index 8387c70..5c94f98 100644 --- a/src/main/java/net/Chipperfluff/chipi/entity/PlayerJrEntity.java +++ b/src/main/java/net/Chipperfluff/chipi/entity/PlayerJrEntity.java @@ -1,9 +1,10 @@ package net.Chipperfluff.chipi.entity; import net.minecraft.entity.EntityType; -import net.minecraft.entity.ai.goal.*; -import net.minecraft.entity.ai.pathing.Path; -import net.minecraft.entity.ai.pathing.PathNode; +import net.minecraft.entity.ai.goal.Goal; +import net.minecraft.entity.ai.goal.LookAroundGoal; +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.EntityAttributes; 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.world.World; -import java.util.List; import java.util.UUID; 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 double FAR_DISTANCE = 30.0; // if far and path sucks -> cheat - private static final int MAX_PATH_LENGTH = 45; // path too long -> cheat - private static final int MAX_VERTICAL_GAP = 6; // dad much higher than path end -> cheat - 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) + private static final int LAND_RADIUS = 6; // search dry landing within this radius + private static final double LAND_HORIZONTAL_DISTANCE = 2.0; // must be this close (horiz) to land + private static final double HOVER_HEIGHT = 1.25; // hover above landing block + private static final int MIN_FLY_TICKS = 20; // must fly at least this long - // Cheat (flight) controls - private static final int FLY_COOLDOWN_TICKS = 100; // max once per 5 seconds - private static final int MIN_FLY_TICKS = 30; // must stay in "creative" at least 1.5s - 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; + private static final double FLY_SPEED = 0.55; + private static final double FLY_ACCEL = 0.25; + private static final int MAX_VERTICAL_GAP = 6; // if dad is this high above -> fly - // Flight feel - 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 ---------------- */ + /* ================= STATE ================= */ private UUID dadUuid; private String dadName = ""; - private int chatCooldown = 40; - private enum TravelMode { - GROUND, - FLYING, - LANDING - } + private enum Mode { GROUND, FLYING } + private Mode mode = Mode.GROUND; - private TravelMode travelMode = TravelMode.GROUND; + private int flyTicks = 0; - // Anti-spam + stability - private int flyCooldown = 0; // counts down - private int flyTicks = 0; // how long we've been flying - private int landingStableTicks = 0; // stable-on-ground counter + // Stuck tracking (ground) + private int stuckTicks = 0; + private double lastDadHorizDist = -1; - // "Is it actually stuck?" tracking - private int stuckCheckTimer = STUCK_CHECK_EVERY; - private Vec3d lastStuckPos = null; - private int stuckStrikes = 0; + // Landing / hover state + private BlockPos landingSpot = null; + private BlockPos forcedLandingSpot = null; - // landing target caching (reduces jitter) - private BlockPos landingTarget = null; - private int landingTargetRefresh = 0; - - /* ---------------- CHAT ---------------- */ - - private static final List MESSAGES = List.of( - "hold on…", - "yeah no this path is cooked", - "creative moment, don’t look", - "brb disabling gravity", - "pathfinding said no", - "minecraft moment detected" - ); - - /* ---------------- CONSTRUCTOR ---------------- */ + /* ================= CONSTRUCTOR ================= */ public PlayerJrEntity(EntityType type, World world) { super(type, world); } - /* ---------------- ATTRIBUTES ---------------- */ - public static DefaultAttributeContainer.Builder createAttributes() { return MobEntity.createMobAttributes() .add(EntityAttributes.GENERIC_MAX_HEALTH, 10.0) @@ -101,13 +70,13 @@ public class PlayerJrEntity extends PathAwareEntity { .add(EntityAttributes.GENERIC_FOLLOW_RANGE, 64.0); } - /* ---------------- DAD ---------------- */ + /* ================= DAD ================= */ public void setDad(ServerPlayerEntity dad) { - this.dadUuid = dad.getUuid(); - this.dadName = dad.getGameProfile().getName(); - this.setCustomName(Text.literal(this.dadName + " jr.")); - this.setCustomNameVisible(true); + dadUuid = dad.getUuid(); + dadName = dad.getGameProfile().getName(); + setCustomName(Text.literal(dadName + " jr.")); + setCustomNameVisible(true); } private ServerPlayerEntity getDad() { @@ -115,428 +84,296 @@ public class PlayerJrEntity extends PathAwareEntity { return getWorld().getServer().getPlayerManager().getPlayer(dadUuid); } - public String getDadName() { - return dadName; - } + public String getDadName() { return dadName; } - public UUID getDadUuid() { - return dadUuid; - } - - /* ---------------- GOALS ---------------- */ + /* ================= AI ================= */ @Override protected void initGoals() { - this.goalSelector.add(1, new SwimGoal(this)); - this.goalSelector.add(2, new FollowDadGoal(this)); - this.goalSelector.add(3, new WanderAroundGoal(this, 0.8)); - this.goalSelector.add(4, new LookAtEntityGoal(this, PlayerEntity.class, 6f)); - this.goalSelector.add(5, new LookAroundGoal(this)); + goalSelector.add(1, new SwimGoal(this)); + goalSelector.add(2, new FollowDadGoal(this)); + goalSelector.add(3, new LookAtEntityGoal(this, PlayerEntity.class, 6f)); + goalSelector.add(4, new LookAroundGoal(this)); } - /* ---------------- TICK ---------------- */ + /* ================= TICK ================= */ @Override public void tick() { super.tick(); if (getWorld().isClient) return; + // Never fall damage ever (you asked this specifically) + this.fallDistance = 0.0f; + ServerPlayerEntity dad = getDad(); if (dad == null) return; - if (flyCooldown > 0) flyCooldown--; - - // movement state machine - 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; + switch (mode) { + case GROUND -> doGroundBrain(dad); + case FLYING -> doFlyingBrain(dad); } } - /* ---------------- CHEAT DECISION ---------------- */ + /* ================= CORE BRAIN: GROUND ================= */ - private void evaluateCheatNeed(ServerPlayerEntity dad) { - // Too close? don't cheat. - double distSq = this.squaredDistanceTo(dad); - if (distSq < (12.0 * 12.0)) { - resetStuckTracking(); // near dad, don't keep strikes - return; - } - - // If on cooldown, we still allow normal pathing but don't fly. - if (flyCooldown > 0) return; - - Path path = this.getNavigation().getCurrentPath(); - - boolean far = distSq > (FAR_DISTANCE * FAR_DISTANCE); - boolean noPath = path == null; - boolean tooLong = path != null && path.getLength() > MAX_PATH_LENGTH; - boolean finishedButStillFar = path != null && path.isFinished() && far; - 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; + private void doGroundBrain(ServerPlayerEntity dad) { + // Water cheat: if land is nearby, prefer walking out; if no path, fly briefly to land. + if (this.isTouchingWater()) { + BlockPos escape = findLandingSpotNear(this.getBlockPos(), LAND_RADIUS); + if (escape != null) { + boolean canWalk = getNavigation().startMovingTo( + escape.getX() + 0.5, + escape.getY(), + escape.getZ() + 0.5, + 1.1 + ); + if (canWalk) { + resetStuckTracking(dad); + return; + } + forcedLandingSpot = escape; + startFlying(dad); return; } } - // smooth creative-like movement - flyTowardSmooth(target); - this.lookAtEntity(dad, 30f, 30f); - } - - 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; + // Update stuck tracking (only on ground mode) + updateStuckTracking(dad); + if (stuckTicks >= STUCK_TICKS_TO_FLY) { + startFlying(dad); return; } - // If we lost landing target, go back to flying (but keep creative) - if (landingTarget == null) { - travelMode = TravelMode.FLYING; + // Too far behind (impatient cheat) + if (horizontalDistanceTo(dad) > FORCE_FLY_DISTANCE) { + startFlying(dad); return; } - // If landing target became invalid, go back to flying (don’t drop) - if (!canStandAt(landingTarget)) { - travelMode = TravelMode.FLYING; - landingTarget = null; + // Dad is way above us (can't reach) + double dy = dad.getY() - getY(); + if (dy > MAX_VERTICAL_GAP && horizontalDistanceTo(dad) > 2.0) { + 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; } - // We want to be exactly centered above the landing block - Vec3d center = Vec3d.ofCenter(landingTarget); - Vec3d pos = this.getPos(); - - // 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; + if (lastDadHorizDist < 0) { + lastDadHorizDist = dadDist; + stuckTicks = 0; 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 - Vec3d current = this.getVelocity(); - Vec3d blended = current.multiply(1.0 - FLY_ACCEL).add(desired.multiply(FLY_ACCEL)); - - this.setVelocity(blended); - this.velocityDirty = true; + lastDadHorizDist = dadDist; } - /* ---------------- LANDING TARGET SEARCH ---------------- */ + private void resetStuckTracking(ServerPlayerEntity dad) { + stuckTicks = 0; + lastDadHorizDist = horizontalDistanceTo(dad); + } - private BlockPos findLandingSpotBlock(ServerPlayerEntity dad) { - BlockPos center = dad.getBlockPos(); - double bestDist = Double.MAX_VALUE; - BlockPos best = null; + /* ================= CORE BRAIN: FLYING ================= */ - 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) - int minY = center.getY() - 2; - int maxY = center.getY() + 3; + landingSpot = null; - for (int dx = -radius; dx <= radius; dx++) { - for (int dz = -radius; dz <= radius; dz++) { - // quick circle check - if ((dx * dx + dz * dz) > (LANDING_SEARCH_RADIUS * LANDING_SEARCH_RADIUS)) continue; + setNoGravity(true); + getNavigation().stop(); + setVelocity(Vec3d.ZERO); + velocityDirty = true; - for (int y = minY; y <= maxY; y++) { - BlockPos pos = new BlockPos(center.getX() + dx, y, center.getZ() + dz); - if (!canStandAt(pos)) continue; + dad.sendMessage(Text.literal(getName().getString() + " switched to Game Mode Creative"), false); + } - // also avoid landing inside water or on water edge - if (!getWorld().getFluidState(pos).isEmpty()) continue; - if (!getWorld().getFluidState(pos.down()).isEmpty()) continue; + private void doFlyingBrain(ServerPlayerEntity dad) { + flyTicks++; - double d = this.getPos().squaredDistanceTo(Vec3d.ofCenter(pos)); - if (d < bestDist) { - bestDist = d; - best = pos; - } + double horiz = horizontalDistanceTo(dad); + + if (flyTicks >= MIN_FLY_TICKS) { + 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; } private boolean canStandAt(BlockPos pos) { - // feet + head must be air - if (!getWorld().getBlockState(pos).isAir()) return false; - if (!getWorld().getBlockState(pos.up()).isAir()) return false; + World w = getWorld(); - // 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(); - 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 “don’t 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 - public void onDeath(DamageSource source) { - super.onDeath(source); - 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); - } + public boolean handleFallDamage(float fallDistance, float damageMultiplier, DamageSource source) { + return false; } - /* ---------------- NAME ---------------- */ - - @Override - public float getNameLabelHeight() { - return this.getHeight() + 1.5f; // YES. STILL CORRECT. - } - - /* ---------------- SAVE ---------------- */ + /* ================= SAVE / LOAD ================= */ @Override public void writeCustomDataToNbt(NbtCompound nbt) { if (dadUuid != null) nbt.putUuid("Dad", dadUuid); nbt.putString("DadName", dadName); - nbt.putString("TravelMode", travelMode.name()); - nbt.putInt("FlyCooldown", flyCooldown); + nbt.putString("Mode", mode.name()); nbt.putInt("FlyTicks", flyTicks); - nbt.putInt("LandingStable", landingStableTicks); - nbt.putInt("StuckStrikes", stuckStrikes); } @Override public void readCustomDataFromNbt(NbtCompound nbt) { if (nbt.containsUuid("Dad")) dadUuid = nbt.getUuid("Dad"); if (nbt.contains("DadName")) dadName = nbt.getString("DadName"); - - if (nbt.contains("TravelMode")) { - travelMode = TravelMode.valueOf(nbt.getString("TravelMode")); - this.setNoGravity(travelMode != TravelMode.GROUND); + if (nbt.contains("Mode")) { + mode = Mode.valueOf(nbt.getString("Mode")); + setNoGravity(mode != Mode.GROUND); } - - if (nbt.contains("FlyCooldown")) flyCooldown = nbt.getInt("FlyCooldown"); 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 - stuckCheckTimer = STUCK_CHECK_EVERY; - lastStuckPos = null; - landingTarget = null; - landingTargetRefresh = 0; + // volatile reset + landingSpot = null; + forcedLandingSpot = null; + lastDadHorizDist = -1; + stuckTicks = 0; } - /* ---------------- FOLLOW GOAL ---------------- */ + /* ================= FOLLOW GOAL ================= */ static class FollowDadGoal extends Goal { private final PlayerJrEntity jr; - - FollowDadGoal(PlayerJrEntity jr) { - this.jr = jr; - } + FollowDadGoal(PlayerJrEntity jr) { this.jr = jr; } @Override public boolean canStart() { - return jr.travelMode == TravelMode.GROUND && jr.getDad() != null; + return jr.mode == Mode.GROUND && jr.getDad() != null; } @Override diff --git a/src/main/java/net/Chipperfluff/chipi/item/PlayerMilkItem.java b/src/main/java/net/Chipperfluff/chipi/item/PlayerMilkItem.java index 27dd051..937af57 100644 --- a/src/main/java/net/Chipperfluff/chipi/item/PlayerMilkItem.java +++ b/src/main/java/net/Chipperfluff/chipi/item/PlayerMilkItem.java @@ -1,16 +1,20 @@ 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.effect.StatusEffectInstance; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; +import net.minecraft.item.ItemUsage; 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.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 { @@ -18,6 +22,30 @@ public class PlayerMilkItem extends Item { super(settings); } + /* ================= DENY DRINKING ================= */ + + @Override + public TypedActionResult 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 public UseAction getUseAction(ItemStack stack) { return UseAction.DRINK; @@ -28,10 +56,23 @@ public class PlayerMilkItem extends Item { return 32; } + /* ================= FINISH DRINK ================= */ + @Override public ItemStack finishUsing(ItemStack stack, World world, LivingEntity user) { 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( null, player.getBlockPos(), @@ -42,8 +83,10 @@ public class PlayerMilkItem extends Item { ); } - if (user instanceof PlayerEntity player && !player.getAbilities().creativeMode) { - return new ItemStack(Items.BUCKET); + // ✅ CORRECT vanilla behavior: + // consumes milk and gives bucket + if (user instanceof PlayerEntity player) { + return ItemUsage.exchangeStack(stack, player, new ItemStack(Items.BUCKET)); } return stack; diff --git a/src/main/java/net/Chipperfluff/chipi/mixin/PlayerEntityMixin.java b/src/main/java/net/Chipperfluff/chipi/mixin/PlayerEntityMixin.java index 08c60b4..d715a8e 100644 --- a/src/main/java/net/Chipperfluff/chipi/mixin/PlayerEntityMixin.java +++ b/src/main/java/net/Chipperfluff/chipi/mixin/PlayerEntityMixin.java @@ -1,10 +1,15 @@ package net.Chipperfluff.chipi.mixin; 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.minecraft.entity.data.DataTracker; +import net.minecraft.entity.effect.StatusEffectInstance; import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -14,11 +19,63 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(PlayerEntity.class) public abstract class PlayerEntityMixin { + /* ============================================================ + INIT: tracked data + ============================================================ */ + @Inject(method = "", 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; DataTracker tracker = self.getDataTracker(); 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(); + } }