[READ-ONLY] Mirror of https://github.com/aaronateataco/pets-and-pals. Pets & Pals — client-side companion pets with real presence. Fork/redesign of downloadableduck's Pets Mod.
0

Configure Feed

Select the types of activity you want to include in your feed.

Leap entrances, single-file mode, Menagerie apply-on-select, front spawns

- run-alongside entrance v3: instead of appearing at the formation point, the
pet quietly moves to just BEHIND the camera (out of view) and gets a full
speed burst, so it visibly leaps past your shoulder into formation and
settles - with obstacle recovery: if its side anchor is blocked it swaps
sides during the maneuver, and if there's no room behind you it appears
past the obstruction instead
- narrow spaces: if BOTH side anchors are blocked (tunnel, hallway), the pet
runs single file directly ahead of you while sprinting
- Menagerie fix: clicking a species now APPLIES it immediately (previously
only the Summon button applied, so picking a fox and joining a server still
gave the old pet)
- every pet now spawns in front of the camera on solid ground (the bee
additionally keeps its nest animation); falls back to the old position when
no clean spot exists
- README: Privacy & Compliance section (no data collection -> no GDPR
surface; EULA-safe: free, cosmetic, no advantage, no Mojang assets shipped;
note about per-server mod policies)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

aaronateataco (Jul 10, 2026, 10:04 PM +0100) 4cd6e2f7 90642933

+106 -15
+6
README.md
··· 39 39 ## Requirements 40 40 This mod requires [Fabric API](https://modrinth.com/mod/fabric-api), [Cloth Config API](https://modrinth.com/mod/cloth-config), [YACL](https://modrinth.com/mod/yacl) (YetAnotherConfigLib) and [Mod Menu](https://modrinth.com/mod/modmenu). It's a lot, I know, but hopefully you already have most of them installed! 41 41 42 + ## Privacy & Compliance 43 + Pets&Pals is a purely client-side, cosmetic mod: 44 + - **No data collection.** The mod has no telemetry, no analytics, no network calls of its own, and stores nothing but your local settings file. There is no personal data to process, so there is nothing to consent to under GDPR or similar privacy laws. 45 + - **Minecraft EULA & Usage Guidelines.** The mod is free, gives no gameplay advantage (pets are visual companions with no combat, mining, or movement benefits to the player), sells nothing, and ships no Mojang assets - it only references the game's own resources at runtime, as mods are permitted to do. 46 + - **Server rules.** Because pets exist only on your client and are invisible to servers and other players, they don't affect server gameplay. As with any client mod, individual servers set their own allowed-modifications policies - if a server has a strict mods list, check it before playing. 47 + 42 48 ## FAQ 43 49 **Is this mod paid/are some features paid?** 44 50 No, and they never will be! This mod will forever remain free for everyone to use.
+1 -1
gradle.properties
··· 12 12 loom_version=1.14-SNAPSHOT 13 13 14 14 # Mod Properties 15 - mod_version=0.10.2 15 + mod_version=0.10.3 16 16 maven_group=io.github.aaronateataco.petsandpals 17 17 archives_base_name=pets-and-pals 18 18
+8 -4
src/client/java/io/github/aaronateataco/petsandpals/Utils.java
··· 64 64 // it, and the block sinks back into the ground. Falls back to a plain spawn if 65 65 // no clean grid spot exists (mid-air, tight spaces, ...). 66 66 BlockState dwellingBlock = entity.spawnDwellingBlock(); 67 - BlockPos dwellingPos = dwellingBlock == null ? null : findDwellingPos(player, world); 68 - if (dwellingPos != null) { 69 - entity.setPos(dwellingPos.getX() + 0.5, dwellingPos.getY(), dwellingPos.getZ() + 0.5); 67 + BlockPos frontPos = findDwellingPos(player, world); 68 + if (dwellingBlock != null && frontPos != null) { 69 + entity.setPos(frontPos.getX() + 0.5, frontPos.getY(), frontPos.getZ() + 0.5); 70 70 entity.setInvisible(true); 71 71 world.addEntity(entity); 72 - world.addEntity(PetDwelling.create(world, dwellingPos, dwellingBlock, entity)); 72 + world.addEntity(PetDwelling.create(world, frontPos, dwellingBlock, entity)); 73 + } else if (frontPos != null) { 74 + // Every pet spawns in front of the camera, not underfoot/behind. 75 + entity.setPos(frontPos.getX() + 0.5, frontPos.getY(), frontPos.getZ() + 0.5); 76 + world.addEntity(entity); 73 77 } else { 74 78 entity.setPos(x, y, z); 75 79 world.addEntity(entity);
+13
src/client/java/io/github/aaronateataco/petsandpals/gui/MenagerieScreen.java
··· 195 195 String label = (isActive ? "✔ " : "") + species.getDisplayName().getString(); 196 196 Button cell = Button.builder(Component.literal(label), b -> { 197 197 this.selected = species; 198 + this.applySelected(); 198 199 this.rebuildGrid(); 199 200 }).bounds(x, y, CELL_WIDTH, CELL_HEIGHT).build(); 200 201 cell.active = species != this.selected; ··· 205 206 this.page = Mth.clamp(this.page, 0, maxPage); 206 207 this.prevButton.active = this.page > 0; 207 208 this.nextButton.active = this.page < maxPage; 209 + } 210 + 211 + /** Clicking a species in the grid makes it the active pet immediately. */ 212 + private void applySelected() { 213 + if (this.selected == null) return; 214 + CONFIG.activePet = this.selected.name(); 215 + this.saveConfig(); 216 + if (this.minecraft != null && this.minecraft.level != null && Boolean.TRUE.equals(CONFIG.petOn)) { 217 + Central.despawnPet(); 218 + Central.summonPet(); 219 + Central.refreshChatSuggestor(this.minecraft); 220 + } 208 221 } 209 222 210 223 private void summonSelected() {
+5
src/main/java/io/github/aaronateataco/petsandpals/mob/AbstractPet.java
··· 397 397 } 398 398 } 399 399 400 + /** Whether the pet's hitbox fits (no collisions) at the given position. */ 401 + public boolean canFitAt(double x, double y, double z) { 402 + return this.fitsAt(this.level(), x, y, z); 403 + } 404 + 400 405 private boolean fitsAt(Level level, double x, double y, double z) { 401 406 net.minecraft.world.phys.AABB box = this.getBoundingBox().move( 402 407 x - this.getX(), y - this.getY(), z - this.getZ());
+73 -10
src/main/java/io/github/aaronateataco/petsandpals/mob/ai/PetFollowOwnerGoal.java
··· 155 155 // the pace visibly step, which read as jank). 156 156 this.updateAlongsideBoost(); 157 157 158 - // Deterministic entrance: if the pet is lagging its formation point while the 159 - // player isn't looking at it (it's behind them), place it AT the formation 160 - // point with the transition effects - the sidekick arrives on screen fast, 161 - // and never pops while being watched. 158 + // Leap entrance: if the pet is lagging its formation point while unwatched 159 + // (behind the player), quietly move it to just BEHIND the camera and give it a 160 + // full speed burst - so it visibly leaps past the player's shoulder into 161 + // formation, running the whole way. Also the obstacle recovery: if its side is 162 + // blocked, it swaps to the other side during the same maneuver. 162 163 Vec3 anchor = this.alongsideAnchor(); 163 164 double lagSqr = this.pet.distanceToSqr(anchor.x, anchor.y + this.pet.followYOffset(), anchor.z); 164 165 if (lagSqr > 12.25 && !this.pet.ownerInViewCone()) { 165 166 if (++this.alongsideLagTicks >= 15) { 166 167 this.alongsideLagTicks = 0; 167 - if (!this.pet.tryRepositionTo(anchor.x, anchor.y + this.pet.followYOffset(), anchor.z)) { 168 - this.pet.repositionToOwner(); 169 - } 168 + this.leapEntrance(); 170 169 return; 171 170 } 172 171 } else { ··· 205 204 } 206 205 207 206 /** 207 + * Places the pet just behind the camera (out of view) with a max speed burst so it 208 + * sprints into frame past the player's shoulder. If the formation side is blocked 209 + * by an obstacle, flips to the other side first. 210 + */ 211 + private void leapEntrance() { 212 + // Prefer the current side; if its anchor is blocked, swap sides. 213 + Vec3 anchor = this.alongsideAnchor(); 214 + if (!this.pet.canFitAt(anchor.x, anchor.y + this.pet.followYOffset(), anchor.z)) { 215 + this.alongsideSide = -this.alongsideSide; 216 + anchor = this.alongsideAnchor(); 217 + } 218 + 219 + Vec3 forward = Vec3.directionFromRotation(0.0F, this.owner.getYHeadRot()); 220 + forward = new Vec3(forward.x, 0.0, forward.z).normalize(); 221 + Vec3 right = new Vec3(-forward.z, 0.0, forward.x); 222 + Vec3 behind = this.owner.position() 223 + .subtract(forward.scale(2.2)) 224 + .add(right.scale(0.9 * this.alongsideSide)); 225 + 226 + boolean placed = false; 227 + if (this.pet.followYOffset() > 0.0F) { 228 + placed = this.pet.tryRepositionTo(behind.x, this.owner.getY() + this.pet.followYOffset(), behind.z); 229 + } else { 230 + for (int dy = 2; dy >= -3 && !placed; dy--) { 231 + BlockPos pos = BlockPos.containing(behind.x, this.owner.getY() + dy, behind.z); 232 + BlockPos below = pos.below(); 233 + if (!this.pet.level().getBlockState(below).isFaceSturdy(this.pet.level(), below, Direction.UP)) { 234 + continue; 235 + } 236 + placed = this.pet.tryRepositionTo(behind.x, pos.getY(), behind.z); 237 + } 238 + } 239 + if (!placed) { 240 + // No room behind the camera (wall right behind you) - appear past the 241 + // obstruction near the formation point instead. 242 + if (!this.pet.tryRepositionTo(anchor.x, anchor.y + this.pet.followYOffset(), anchor.z)) { 243 + this.pet.repositionToOwner(); 244 + return; 245 + } 246 + } 247 + // Full burst so the entrance is a leap, easing back down as it reaches formation. 248 + this.smoothedBoost = MAX_ALONGSIDE_BOOST - 1.0; 249 + this.updateAlongsideBoost(); 250 + Vec3 lead = new Vec3(this.owner.getDeltaMovement().x, 0.0, this.owner.getDeltaMovement().z).scale(5.0); 251 + this.pet.getNavigation().moveTo( 252 + anchor.x + lead.x, 253 + this.owner.getY() + this.pet.followYOffset(), 254 + anchor.z + lead.z, 255 + this.speedModifier * AbstractPet.speedMultiplier.getAsDouble()); 256 + this.timeToRecalcPath = this.adjustedTickDelay(5); 257 + } 258 + 259 + /** 208 260 * The formation point: well ahead of the camera (so the pet sits comfortably on 209 261 * screen, not at the bottom edge) and offset to whichever side the pet is already 210 262 * on - it may switch sides naturally if it drifts across, with hysteresis so it ··· 229 281 float pitchDown = Math.max(0.0F, this.owner.getXRot()); 230 282 double forwardDistance = Mth.clamp(3.4 - pitchDown * 0.045, 2.0, 3.8); 231 283 232 - return this.owner.position() 233 - .add(forward.scale(forwardDistance)) 234 - .add(right.scale(1.5 * this.alongsideSide)); 284 + double fitY = this.owner.getY() + Math.max(0.1, this.pet.followYOffset()); 285 + Vec3 base = this.owner.position().add(forward.scale(forwardDistance)); 286 + Vec3 sideAnchor = base.add(right.scale(1.5 * this.alongsideSide)); 287 + if (this.pet.canFitAt(sideAnchor.x, fitY, sideAnchor.z)) { 288 + return sideAnchor; 289 + } 290 + // Side blocked (wall, tree...): try the other side. 291 + Vec3 otherAnchor = base.add(right.scale(-1.5 * this.alongsideSide)); 292 + if (this.pet.canFitAt(otherAnchor.x, fitY, otherAnchor.z)) { 293 + this.alongsideSide = -this.alongsideSide; 294 + return otherAnchor; 295 + } 296 + // Both sides blocked (narrow tunnel, hallway): run single file, directly ahead. 297 + return this.owner.position().add(forward.scale(Math.max(2.0, forwardDistance * 0.7))); 235 298 } 236 299 237 300 /** Smoothly sized speed boost, updated every tick: big when lagging, gentle in formation. */