A Luigi's Mansion style vacuum for Minecraft Java Edition. Hold Attack to suck mobs, items, XP and soft blocks into the tank; hold Use to blast them back out like a cannon.
| Target | Minecraft 1.21.11, Yarn 1.21.11+build.6, Fabric Loader 0.19.3, Fabric API 0.141.6+1.21.11, Loom 1.16.3, Java 21 |
src/ |
The full mod, ported from the reference implementation. |
reference/1.21.4-yarn/ |
The original feature-finished mod, kept as the port's source of truth. |
This is the 1.21.4 reference brought up to 1.21.11 (the last line with Yarn mappings). The
mechanical renames were small (getPos -> getEntityPos, velocityModified -> velocityDirty,
getWorld -> getEntityWorld, removeAllPassengers/stopRiding -> detach); the real changes
were the 1.21.5 Optional-returning NBT getters, the 1.21.6 ReadView/WriteView entity
serialisation redesign in StoredThing, and the HUD moving from HudRenderCallback (removed) to
HudElementRegistry. Note that yarn projects need the Loom 1.16 line -- Loom 1.17+ targets the
mappings-less 26.x versions and has no mappings configuration at all.
./gradlew build # jar lands in build/libs/
./gradlew runClient # dev run| Input | Effect |
|---|---|
| Hold Attack (left mouse) | Suction beam. Drags nearby entities toward the nozzle and swallows them. Aim at a soft block to chew it loose. |
| Hold Use (right mouse) | Cannon. Fires the tank contents back out, last in first out. |
| Sneak + Use | Gentle release. Low velocity, no impact damage -- for relocating animals or placing blocks. |
Both triggers leave movement completely untouched: you can sprint, swim, and fly while firing.
The obvious way to build a hold-to-act item is Item#use returning CONSUME plus usageTick.
That was rejected because "using" an item applies the vanilla movement penalty -- you walk at about
20% speed, which the brief explicitly rules out. Instead:
- The client polls the attack/use key bindings every client tick.
- On a transition only, it sends a 2-byte payload (mode + sneak flag).
- The server drives all logic from the end-of-tick event.
Holding a button for a full minute costs two packets, not twelve hundred. As a consequence every vanilla action bound to those buttons has to be suppressed while the vacuum is held, which is done with attack/use block and entity callbacks plus a block-break guard -- no mixins anywhere in the mod.
The item stack carries only a TankData component: a UUID handle, a total count, and the top five
groups by count for the HUD and tooltip. The real payload lives server-side in TankStore, saved to
poltergust_tanks.nbt next to level.dat.
This split matters. Item stack components are re-synced to the client on every inventory change, so storing a few hundred serialised mobs on the stack would push hundreds of kilobytes down the wire repeatedly. The handle-plus-summary approach keeps the per-sync cost at a few dozen bytes while still giving the client everything it needs to draw the readout.
Everything that is not a block round-trips through one entity-serialisation path -- items, XP orbs,
boats, armour stands and mobs all share it, and captured things keep their identity. Vacuum a named
villager with a trade list and that same villager comes back out. (Since 1.21.6 this goes through
WriteView/ReadView: the tank still stores plain NBT, and the views are built only at the
capture/release boundary.)
Force is pullStrength * (0.22 + 0.78 * falloff^2) / resistance, where falloff goes from 1 at the
nozzle to 0 at maximum range. True inverse-square was tried on paper and rejected: it makes distant
targets feel dead and near ones snap in like a teleport. The squared-falloff curve keeps a long,
readable drag.
resistance scales with max health and hitbox width, so a chicken skitters in instantly while an
iron golem has to be wrestled. That is what produces the "fighting a big ghost" beat without bolting
on a separate health-gated capture minigame.
Line of sight uses a vanilla visibility check, so you cannot vacuum through walls, but it is skipped within 2 blocks so point-blank grabs never mysteriously fail.
Shots come out last-in-first-out, like a magazine. Loose items and XP orbs fire in bursts of four (configurable) so emptying a 500-item tank is a firehose rather than a chore; anything heavier goes out one per shot.
- Mobs re-spawn at the muzzle with velocity and are tracked for 100 ticks. The first living
thing they clip takes
speed * 5.5damage (capped at 14) plus knockback, and the projectile takes 40% of that itself. Creeper cannon. - Blocks ride out as falling block entities, which means they place themselves on landing for free, and land like anvils. If the muzzle is buried in geometry the block is ejected as an item instead, so the mod never eats terrain to make room.
- Items get a 30-tick pickup delay so you do not immediately re-collect what you just fired.
A whitelist, not a hardness threshold. A threshold sounds elegant but in practice a player sweeping the beam across a village dissolves half of it. The list is "loose material a shop-vac could plausibly inhale": sand, gravel, leaves, flowers, crops, wool, snow, torches, mud, cobwebs, ice, TNT and friends. Blocks with block entities are never touched, fluids are excluded, and build-permission checks are respected so spawn protection and claim mods still work.
Removal goes through a normal block break with drops suppressed, so multi-part blocks like tall flowers and cave vines come apart correctly without spilling items.
config/poltergust.properties is written on first run. Everything that affects feel is exposed:
range, cone angle, pull strength, beam drag, capture distance, block chew time, tank capacity, fire
interval, muzzle velocity and spread, item burst size, impact damage, and toggles for sucking
players, bosses, and blocks at all.
- Middle-clicking the item in creative copies the tank UUID, so two stacks share one tank. Currently treated as a feature ("linked tanks") rather than a bug.
- Firing a mob into a wall does not damage it; only entity impacts register.
- No custom sounds -- the suction hum is pitched-up elytra flight and the cannon is a Breeze shot.