From fd62aad7483dd2d7be1acb9a1ae8505424bff1a1 Mon Sep 17 00:00:00 2001 From: Andrea D'Amico Date: Fri, 16 Oct 2026 22:30:39 +0200 Subject: [PATCH 1/5] feat: grow an end displacer as a pyramid ahead of its face An End Displacer reached the whole half-space past its front face and harvested nearest first, so on the ground, facing up, it roofed itself over a block up and buried its sides. It now reaches a square pyramid widening out from that face, leaving the ring around it clear. Closes #100 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + docs/spec/void-energy.md | 4 +- .../voidworks/gametest/EndDisplacerTests.java | 16 +++-- .../displacer/EndDisplacerBlockEntity.java | 5 +- .../voidworks/displacer/Frontier.java | 22 +++++-- .../voidworks/displacer/FrontierTest.java | 58 ++++++++++++++++++- 6 files changed, 90 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 838cd95..5c07058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Written for Consumers: what a mod building against Voidworks can use, or will se ## Unreleased +- An End Displacer grows its body as a square pyramid widening out ahead of its front face, one block a step ahead, a 3×3 two steps ahead and so on, where it filled the whole space past that face nearest first. On the ground, facing up, it no longer roofs itself over a block up: the ring around it stays clear, so a player walks up to its sides and sets a hopper there. A Zone still counts harvests, so it places as many blocks as before, taller and narrower. `Frontier.ahead` is the pyramid. (#100) - A Well hatch is a provider or a requester, each its own item: the recipe makes a Well Hatch Provider (`voidworks:well_hatch`, renamed from Well Hatch), and a new Well Hatch Requester (`voidworks:well_hatch_requester`) places a requester; sneak-used in the air, a stack of either turns into the other, and a hatch broken or picked is its mode's item (`WellHatchItem`, `VoidworksBlocks.WELL_HATCH_REQUESTER_ITEM`; `WellHatches.put` takes the `Mode` it is placed as). Its owner switches a placed one between provider and requester by sneak-using it with an empty hand. Either switch, placed or in hand, is heard as a shulker box opening (`WellHatches.SWITCH_SOUND`). Using an item on a requester sets its request to that item's kind, components ignored, and adds a stack per use, up to its slots (27); another item replaces the request at one stack, and an empty hand clears it, each told above the hotbar. A requester gives no hand anything and empties only through its item faces. A requester switched to a provider loses its request and offers what it holds; a provider switched to a requester keeps what it holds for its faces to drain. Only the owner switches it or sets its request, and both survive a save. Its front shows which it is: a provider's shell is an orange shulker's, where it was purple, and a requester's a blue shulker's, through the block's new `mode` property (`provider` or `requester`). It breaks as fast as Voidworks' machines (hardness 5, where it was the Well's 22.5), keeping its blast resistance. Its front also shows one of a requester's requested item, or of the first stack a provider holds, flat in its window as an item frame holds one (`WellHatchBlockEntity.shown()`). Jade names it by its mode's item and shows a requester's request beside what it holds, as "Requesting" and the item with the stacks asked for on it, or "Requesting nothing". `WellHatches.Hatch` gains `mode()` (a `WellHatches.Mode`), `request()` (a `WellHatches.Request` of item and stacks), `switchMode()`, `ask(item)` and `clearRequest()`; `HatchReport` is what Jade shows. No dragon fills a request yet. (#114) - A Well hatch holds items for its Void Well, a full block crafted from a shulker box ringed by four Void Well Casing. It is placed against the outer face of any wall casing of a Well, on any course, never the core, the rim or into the shaft, and only by the Well's owner; anywhere else, or against another's Well, it is refused and the player told why above the hotbar. It joins that Well without being part of its structure, and a Well takes any number. It holds `wellHatchSlots` (27) slots in `voidworks-server.toml`, the slots it was placed with, in the server's saved data rather than its block, so a dragon will reach it with no chunk loaded (`SavedWellHatches.of(server)`, a `WellHatches` of `WellHatches.Hatch`). Every face is an item face, so a hopper under it empties it and one above fills it; it is never filled by hand: its owner using it with an item is told to set a hopper on it, the item's own use not running, and their empty hand takes its first stack out; anyone else is told whose it is. Its Well no longer whole, it keeps what it holds, and its core broken it is a plain container; broken itself, it drops what it held. Jade shows what it holds. No dragon loads or unloads it yet. `WellStructure.hatchFace(core, facing, against, face)` says where one goes. (#113) - **Breaking:** a Matter Dragon Egg founds the Matter line of Void Dragons (ADR 0011). It is a Drained Dragon Egg upgraded by a Mutation Chamber or a Void Crucible through the new `dragon_eggs` Family, one egg for one at the steep Rung's 800 void energy and even chance (`#voidworks:rung/dragon_eggs/drained` and `/matter`); nothing upgrades it back, and the vanilla egg has no upgrade. It is the Drained egg tinted amber, placed, in the hand and soaking, and until it soaks it falls and teleports when hit. It soaks at the price any egg does, counting toward its owner's next as any dragon does, gives off amber flecks of itself as it soaks, in place of a Mote-line egg's sparks, and hatches a Dragonling of the Matter line, whose grown dragon is of it too. A Matter-line dragon carries nothing yet: routing gives it nothing to load, while it rests, is ridden and called, and heals as any dragon does. Every dragon has a `Line`, `MOTE` or `MATTER`: `VoidDragonRecord` gains `line` after `owner`, which changes its constructor, `VoidDragons.add(owner, line, rest)` adds one of a Line, `VoidDragon.hatchFor(owner, line)` hatches one and `VoidDragon.line()` gives it, `EggKind` gains `MATTER` and `line()`, and the soaking egg's `egg` property gains `matter`. Dragons and Dragonlings saved before Lines are of the Mote line. (#112) diff --git a/docs/spec/void-energy.md b/docs/spec/void-energy.md index 7fcef8b..a2a384a 100644 --- a/docs/spec/void-energy.md +++ b/docs/spec/void-energy.md @@ -132,7 +132,9 @@ starts. A spent Displacer becoming Voidstone shows the same, its own casing eate unloads drops the effect, and nothing of it is saved. - **End**: places fed blocks into the empty air of its zone. It faces the way its placer looked and - grows only into the space ahead of that face, so it never walls in its other faces. + grows only into a square pyramid widening out ahead of that face, one block a step ahead, a 3×3 two + steps ahead, and so on, so it never walls in its other faces, and on the ground it leaves the ring + around it clear for a player to stand at its sides. - **Overworld**: swaps fed blocks with natural terrain in its zone — the blocks in a natural-block tag (stone, deepslate, dirt, ores…), which placed stone also matches. The terrain block goes to the Displacer's output, through a data table keyed by the terrain block and the fed block's diff --git a/src/gametest/java/io/github/_5thlayer/voidworks/gametest/EndDisplacerTests.java b/src/gametest/java/io/github/_5thlayer/voidworks/gametest/EndDisplacerTests.java index 5bc194c..0f213b0 100644 --- a/src/gametest/java/io/github/_5thlayer/voidworks/gametest/EndDisplacerTests.java +++ b/src/gametest/java/io/github/_5thlayer/voidworks/gametest/EndDisplacerTests.java @@ -69,7 +69,7 @@ static void register(VoidworksGameTests.Registrar tests) { helper -> inTheEnd(helper, 2, EndDisplacerTests::dormantUntilPaid)); tests.test("a_dormant_end_displacer_that_can_reach_no_air_pops_off_before_soaking_its_cost", 1, helper -> inTheEnd(helper, 3, EndDisplacerTests::popsOffBeforeSoaking)); - tests.test("an_end_displacer_grows_only_ahead_of_its_front_face_leaving_its_other_faces_open", 1, + tests.test("an_end_displacer_grows_only_ahead_of_its_front_face_as_a_pyramid_leaving_its_other_faces_open", 1, helper -> inTheEnd(helper, 4, EndDisplacerTests::growsOnlyAhead)); tests.test("an_end_displacer_placed_faces_where_its_player_looks", 1, EndDisplacerTests::facesWhereThePlayerLooks); tests.test("an_end_displacer_where_the_harvest_kind_is_not_end_harvests_nothing_and_pops_off", 40, EndDisplacerTests::refusesOutsideTheEnd); @@ -157,7 +157,10 @@ private static void popsOffBeforeSoaking(DimensionSite site) { "it soaked motes before it popped off: the Voidstone holds " + site.motes(DISPLACER.north())); } - /** Facing east in open air, with eight harvests to make: every one lies east of it, and its other five faces stay open. */ + /** + * Facing east in open air, with eight harvests to make: every one lies in the pyramid widening + * east of it, so the ring around the position ahead stays open, as do its other five faces. + */ private static void growsOnlyAhead(DimensionSite site) { var displacer = place(site, 8, Direction.EAST); site.feed(displacer, 8); @@ -167,10 +170,13 @@ private static void growsOnlyAhead(DimensionSite site) { site.assertTrue(site.abs(DISPLACER.east()).equals(displacer.grown().getFirst()), "it harvested " + displacer.grown().getFirst() + " first, not the air ahead of its front face"); for (var pos : displacer.grown()) { - site.assertTrue(pos.getX() > site.abs(DISPLACER).getX(), "it harvested " + pos + ", not ahead of its east face"); + var offset = pos.subtract(site.abs(DISPLACER)); + site.assertTrue(offset.getX() > Math.max(Math.abs(offset.getY()), Math.abs(offset.getZ())), + "it harvested " + pos + ", outside the pyramid ahead of its east face"); } - for (var side : new BlockPos[] {DISPLACER.above(), DISPLACER.below(), DISPLACER.north(), DISPLACER.south(), DISPLACER.west()}) { - site.assertTrue(site.state(side).isAir(), "it walled in its face at " + side + " with " + site.state(side)); + for (var side : new BlockPos[] {DISPLACER.above(), DISPLACER.below(), DISPLACER.north(), DISPLACER.south(), DISPLACER.west(), + DISPLACER.east().above(), DISPLACER.east().below(), DISPLACER.east().north(), DISPLACER.east().south()}) { + site.assertTrue(site.state(side).isAir(), "it filled " + side + " beside it" + " with " + site.state(side)); } } diff --git a/src/main/java/io/github/_5thlayer/voidworks/displacer/EndDisplacerBlockEntity.java b/src/main/java/io/github/_5thlayer/voidworks/displacer/EndDisplacerBlockEntity.java index 027d18a..5b21472 100644 --- a/src/main/java/io/github/_5thlayer/voidworks/displacer/EndDisplacerBlockEntity.java +++ b/src/main/java/io/github/_5thlayer/voidworks/displacer/EndDisplacerBlockEntity.java @@ -15,8 +15,9 @@ /** * The End Void Displacer: it places fed blocks into the air ahead of its front face, touching it or - * the Voidstone it has left, so its body grows outward that way and never walls in its other faces, - * and nothing comes back to its output. Over the open void its + * the Voidstone it has left, so its body grows outward that way as a pyramid widening from that face, + * never walling in its other faces nor roofing over the ground around it, and nothing comes back to + * its output. Over the open void its * harvests grade one step higher, as the Void Pressure there is, and each Voidstone it leaves closes * the column above it. It is placed Dormant, and harvests only once it has soaked its cost. */ diff --git a/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java b/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java index c64acf9..5da193a 100644 --- a/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java +++ b/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java @@ -23,17 +23,27 @@ private Frontier() { } /** - * The space a Displacer that grows through one face reaches: everything past that face, so its - * other faces, and all that is level with it or behind it, stay as they were. + * The space a Displacer that grows through one face reaches: a square pyramid opening out from + * that face, one position a step ahead, a 3×3 two steps ahead and so on, never further aside than + * it is ahead. Its other faces, all that is level with it or behind it, and the ring around it a + * step ahead stay as they were, so on the ground a player can still stand beside it. * * @param facing the way its front face looks * @return whether a position lies ahead of the Displacer at {@code displacer} */ public static Predicate ahead(BlockPos displacer, Direction facing) { - var step = facing.getUnitVec3i(); - return pos -> (pos.getX() - displacer.getX()) * step.getX() - + (pos.getY() - displacer.getY()) * step.getY() - + (pos.getZ() - displacer.getZ()) * step.getZ() >= 1; + var axis = facing.getAxis(); + return pos -> { + var offset = pos.subtract(displacer); + int ahead = axis.choose(offset.getX(), offset.getY(), offset.getZ()) * facing.getAxisDirection().getStep(); + int aside = 0; + for (var other : Direction.Axis.values()) { + if (other != axis) { + aside = Math.max(aside, Math.abs(other.choose(offset.getX(), offset.getY(), offset.getZ()))); + } + } + return ahead >= 1 && aside < ahead; + }; } /** diff --git a/src/test/java/io/github/_5thlayer/voidworks/displacer/FrontierTest.java b/src/test/java/io/github/_5thlayer/voidworks/displacer/FrontierTest.java index 50734a9..3518506 100644 --- a/src/test/java/io/github/_5thlayer/voidworks/displacer/FrontierTest.java +++ b/src/test/java/io/github/_5thlayer/voidworks/displacer/FrontierTest.java @@ -62,17 +62,71 @@ void itGrowsOnFromItsFurthestVoidstone() { assertEquals(Optional.of(deeper), Frontier.next(DISPLACER, grown, deeper::equals)); } - /** Facing east: what lies past its east face, at any height and depth, and nothing level with it or behind. */ + /** Facing east: what lies past its east face, and nothing level with it or behind. */ @Test void aheadIsOnlyTheSpaceBeyondItsFrontFace() { var ahead = Frontier.ahead(DISPLACER, Direction.EAST); assertTrue(ahead.test(DISPLACER.east())); - assertTrue(ahead.test(DISPLACER.east(3).above(5).north(7))); + assertTrue(ahead.test(DISPLACER.east(8).above(5).north(7))); for (var level : new BlockPos[] {DISPLACER, DISPLACER.north(), DISPLACER.south(), DISPLACER.above(), DISPLACER.below(), DISPLACER.west()}) { assertFalse(ahead.test(level), level + " is not ahead of a Displacer facing east"); } } + /** + * For every facing, a square pyramid opening out from its front face: one position a step ahead, + * a 3×3 two steps ahead, and nothing further sideways than it is ahead. + */ + @Test + void aheadWidensAsAPyramid() { + for (var facing : Direction.values()) { + var ahead = Frontier.ahead(DISPLACER, facing); + var one = DISPLACER.relative(facing); + var two = DISPLACER.relative(facing, 2); + var across = Direction.Plane.HORIZONTAL.test(facing) ? facing.getClockWise() : Direction.NORTH; + var along = facing.getAxis() == Direction.Axis.Y ? Direction.EAST : Direction.UP; + assertTrue(ahead.test(one), facing + ": the position a step ahead"); + assertFalse(ahead.test(one.relative(across).relative(along)), facing + ": a step ahead and one aside on both axes"); + assertFalse(ahead.test(one.relative(across)), facing + ": a step ahead and one aside"); + for (int a = -1; a <= 1; a++) { + for (int b = -1; b <= 1; b++) { + var pos = two.relative(across, a).relative(along, b); + assertTrue(ahead.test(pos), facing + ": two steps ahead, within one aside, " + pos); + } + } + assertFalse(ahead.test(two.relative(across, 2)), facing + ": two steps ahead and two aside"); + assertFalse(ahead.test(two.relative(along, -2).relative(across, 1)), facing + ": two steps ahead and two aside the other way"); + } + } + + /** On the ground, facing up, for a Zone of 64: the ring around it stays clear for a player to stand at its sides. */ + @Test + void facingUpOnTheGroundLeavesTheRingAroundItClear() { + var ahead = Frontier.ahead(DISPLACER, Direction.UP); + var grown = new HashSet(); + for (int i = 0; i < 64; i++) { + var next = Frontier.next(DISPLACER, grown, pos -> pos.getY() >= DISPLACER.getY() && ahead.test(pos)); + assertTrue(next.isPresent()); + if (i == 0) { + assertEquals(DISPLACER.above(), next.get()); + } + var offset = next.get().subtract(DISPLACER); + assertTrue(Math.max(Math.abs(offset.getX()), Math.abs(offset.getZ())) < offset.getY(), next.get() + " lies outside the pyramid"); + grown.add(next.get()); + } + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + if (dx == 0 && dz == 0) { + continue; + } + for (int dy = 0; dy <= 1; dy++) { + var ring = DISPLACER.offset(dx, dy, dz); + assertFalse(grown.contains(ring), "it filled " + ring + " beside it"); + } + } + } + } + @Test void facingDownAheadIsBelowIt() { var ahead = Frontier.ahead(DISPLACER, Direction.DOWN); From 45049526343df02985d87b1f937300f730eba321 Mon Sep 17 00:00:00 2001 From: Andrea D'Amico Date: Fri, 16 Oct 2026 22:32:55 +0200 Subject: [PATCH 2/5] refactor: tighten the end displacer pyramid and its tests Co-Authored-By: Claude Opus 5.5 --- .../voidworks/gametest/EndDisplacerTests.java | 6 +++--- .../_5thlayer/voidworks/displacer/Frontier.java | 10 +++++----- .../_5thlayer/voidworks/displacer/FrontierTest.java | 11 +++++------ 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/gametest/java/io/github/_5thlayer/voidworks/gametest/EndDisplacerTests.java b/src/gametest/java/io/github/_5thlayer/voidworks/gametest/EndDisplacerTests.java index 0f213b0..af6de70 100644 --- a/src/gametest/java/io/github/_5thlayer/voidworks/gametest/EndDisplacerTests.java +++ b/src/gametest/java/io/github/_5thlayer/voidworks/gametest/EndDisplacerTests.java @@ -70,7 +70,7 @@ static void register(VoidworksGameTests.Registrar tests) { tests.test("a_dormant_end_displacer_that_can_reach_no_air_pops_off_before_soaking_its_cost", 1, helper -> inTheEnd(helper, 3, EndDisplacerTests::popsOffBeforeSoaking)); tests.test("an_end_displacer_grows_only_ahead_of_its_front_face_as_a_pyramid_leaving_its_other_faces_open", 1, - helper -> inTheEnd(helper, 4, EndDisplacerTests::growsOnlyAhead)); + helper -> inTheEnd(helper, 4, EndDisplacerTests::growsAsAPyramidAhead)); tests.test("an_end_displacer_placed_faces_where_its_player_looks", 1, EndDisplacerTests::facesWhereThePlayerLooks); tests.test("an_end_displacer_where_the_harvest_kind_is_not_end_harvests_nothing_and_pops_off", 40, EndDisplacerTests::refusesOutsideTheEnd); tests.test("an_emptying_siphon_used_on_a_dormant_end_displacer_pays_it_and_an_empty_hand_takes_back_its_fed_blocks", 1, @@ -161,7 +161,7 @@ private static void popsOffBeforeSoaking(DimensionSite site) { * Facing east in open air, with eight harvests to make: every one lies in the pyramid widening * east of it, so the ring around the position ahead stays open, as do its other five faces. */ - private static void growsOnlyAhead(DimensionSite site) { + private static void growsAsAPyramidAhead(DimensionSite site) { var displacer = place(site, 8, Direction.EAST); site.feed(displacer, 8); site.payInFull(displacer); @@ -176,7 +176,7 @@ private static void growsOnlyAhead(DimensionSite site) { } for (var side : new BlockPos[] {DISPLACER.above(), DISPLACER.below(), DISPLACER.north(), DISPLACER.south(), DISPLACER.west(), DISPLACER.east().above(), DISPLACER.east().below(), DISPLACER.east().north(), DISPLACER.east().south()}) { - site.assertTrue(site.state(side).isAir(), "it filled " + side + " beside it" + " with " + site.state(side)); + site.assertTrue(site.state(side).isAir(), "it filled " + side + " beside it with " + site.state(side)); } } diff --git a/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java b/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java index 5da193a..1438c9b 100644 --- a/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java +++ b/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java @@ -25,21 +25,21 @@ private Frontier() { /** * The space a Displacer that grows through one face reaches: a square pyramid opening out from * that face, one position a step ahead, a 3×3 two steps ahead and so on, never further aside than - * it is ahead. Its other faces, all that is level with it or behind it, and the ring around it a - * step ahead stay as they were, so on the ground a player can still stand beside it. + * it is ahead. Its other faces and all that is level with it or behind it stay as they were, as do + * the positions beside the one a step ahead, so on the ground a player can still stand beside it. * * @param facing the way its front face looks - * @return whether a position lies ahead of the Displacer at {@code displacer} + * @return whether a position lies in the pyramid ahead of the Displacer at {@code displacer} */ public static Predicate ahead(BlockPos displacer, Direction facing) { var axis = facing.getAxis(); return pos -> { var offset = pos.subtract(displacer); - int ahead = axis.choose(offset.getX(), offset.getY(), offset.getZ()) * facing.getAxisDirection().getStep(); + int ahead = offset.get(axis) * facing.getAxisDirection().getStep(); int aside = 0; for (var other : Direction.Axis.values()) { if (other != axis) { - aside = Math.max(aside, Math.abs(other.choose(offset.getX(), offset.getY(), offset.getZ()))); + aside = Math.max(aside, Math.abs(offset.get(other))); } } return ahead >= 1 && aside < ahead; diff --git a/src/test/java/io/github/_5thlayer/voidworks/displacer/FrontierTest.java b/src/test/java/io/github/_5thlayer/voidworks/displacer/FrontierTest.java index 3518506..ca827e6 100644 --- a/src/test/java/io/github/_5thlayer/voidworks/displacer/FrontierTest.java +++ b/src/test/java/io/github/_5thlayer/voidworks/displacer/FrontierTest.java @@ -62,9 +62,9 @@ void itGrowsOnFromItsFurthestVoidstone() { assertEquals(Optional.of(deeper), Frontier.next(DISPLACER, grown, deeper::equals)); } - /** Facing east: what lies past its east face, and nothing level with it or behind. */ + /** Facing east: what lies past its east face, however far aside once far enough ahead, and nothing level with it or behind. */ @Test - void aheadIsOnlyTheSpaceBeyondItsFrontFace() { + void aheadIsOnlyBeyondItsFrontFace() { var ahead = Frontier.ahead(DISPLACER, Direction.EAST); assertTrue(ahead.test(DISPLACER.east())); assertTrue(ahead.test(DISPLACER.east(8).above(5).north(7))); @@ -85,11 +85,10 @@ void aheadWidensAsAPyramid() { var two = DISPLACER.relative(facing, 2); var across = Direction.Plane.HORIZONTAL.test(facing) ? facing.getClockWise() : Direction.NORTH; var along = facing.getAxis() == Direction.Axis.Y ? Direction.EAST : Direction.UP; - assertTrue(ahead.test(one), facing + ": the position a step ahead"); - assertFalse(ahead.test(one.relative(across).relative(along)), facing + ": a step ahead and one aside on both axes"); - assertFalse(ahead.test(one.relative(across)), facing + ": a step ahead and one aside"); for (int a = -1; a <= 1; a++) { for (int b = -1; b <= 1; b++) { + var beside = one.relative(across, a).relative(along, b); + assertEquals(a == 0 && b == 0, ahead.test(beside), facing + ": a step ahead, " + a + " and " + b + " aside"); var pos = two.relative(across, a).relative(along, b); assertTrue(ahead.test(pos), facing + ": two steps ahead, within one aside, " + pos); } @@ -105,7 +104,7 @@ void facingUpOnTheGroundLeavesTheRingAroundItClear() { var ahead = Frontier.ahead(DISPLACER, Direction.UP); var grown = new HashSet(); for (int i = 0; i < 64; i++) { - var next = Frontier.next(DISPLACER, grown, pos -> pos.getY() >= DISPLACER.getY() && ahead.test(pos)); + var next = Frontier.next(DISPLACER, grown, ahead); assertTrue(next.isPresent()); if (i == 0) { assertEquals(DISPLACER.above(), next.get()); From b8f437c0927be9976033d8e49c5e6b7a995a3470 Mon Sep 17 00:00:00 2001 From: Andrea D'Amico Date: Fri, 16 Oct 2026 22:43:04 +0200 Subject: [PATCH 3/5] refactor: name the end displacer pyramid's sideways reach and its doc Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- GLOSSARY.md | 3 ++- docs/spec/void-energy.md | 6 ++--- .../displacer/EndDisplacerBlockEntity.java | 5 ++-- .../voidworks/displacer/Frontier.java | 23 ++++++++++++------- .../voidworks/displacer/FrontierTest.java | 18 +++++++-------- 6 files changed, 32 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c07058..51e111f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Written for Consumers: what a mod building against Voidworks can use, or will se ## Unreleased -- An End Displacer grows its body as a square pyramid widening out ahead of its front face, one block a step ahead, a 3×3 two steps ahead and so on, where it filled the whole space past that face nearest first. On the ground, facing up, it no longer roofs itself over a block up: the ring around it stays clear, so a player walks up to its sides and sets a hopper there. A Zone still counts harvests, so it places as many blocks as before, taller and narrower. `Frontier.ahead` is the pyramid. (#100) +- An End Displacer grows its body as a square pyramid widening out ahead of its front face, one position a step ahead, a 3×3 two steps ahead and so on, where it filled the whole space past that face nearest first. On the ground, facing up, it no longer roofs itself over a block up: the ring around it stays clear, so a player walks up to its sides and sets a hopper there. A Zone still counts harvests, so it places as many blocks as before, taller and narrower. `Frontier.ahead` is the pyramid. (#100) - A Well hatch is a provider or a requester, each its own item: the recipe makes a Well Hatch Provider (`voidworks:well_hatch`, renamed from Well Hatch), and a new Well Hatch Requester (`voidworks:well_hatch_requester`) places a requester; sneak-used in the air, a stack of either turns into the other, and a hatch broken or picked is its mode's item (`WellHatchItem`, `VoidworksBlocks.WELL_HATCH_REQUESTER_ITEM`; `WellHatches.put` takes the `Mode` it is placed as). Its owner switches a placed one between provider and requester by sneak-using it with an empty hand. Either switch, placed or in hand, is heard as a shulker box opening (`WellHatches.SWITCH_SOUND`). Using an item on a requester sets its request to that item's kind, components ignored, and adds a stack per use, up to its slots (27); another item replaces the request at one stack, and an empty hand clears it, each told above the hotbar. A requester gives no hand anything and empties only through its item faces. A requester switched to a provider loses its request and offers what it holds; a provider switched to a requester keeps what it holds for its faces to drain. Only the owner switches it or sets its request, and both survive a save. Its front shows which it is: a provider's shell is an orange shulker's, where it was purple, and a requester's a blue shulker's, through the block's new `mode` property (`provider` or `requester`). It breaks as fast as Voidworks' machines (hardness 5, where it was the Well's 22.5), keeping its blast resistance. Its front also shows one of a requester's requested item, or of the first stack a provider holds, flat in its window as an item frame holds one (`WellHatchBlockEntity.shown()`). Jade names it by its mode's item and shows a requester's request beside what it holds, as "Requesting" and the item with the stacks asked for on it, or "Requesting nothing". `WellHatches.Hatch` gains `mode()` (a `WellHatches.Mode`), `request()` (a `WellHatches.Request` of item and stacks), `switchMode()`, `ask(item)` and `clearRequest()`; `HatchReport` is what Jade shows. No dragon fills a request yet. (#114) - A Well hatch holds items for its Void Well, a full block crafted from a shulker box ringed by four Void Well Casing. It is placed against the outer face of any wall casing of a Well, on any course, never the core, the rim or into the shaft, and only by the Well's owner; anywhere else, or against another's Well, it is refused and the player told why above the hotbar. It joins that Well without being part of its structure, and a Well takes any number. It holds `wellHatchSlots` (27) slots in `voidworks-server.toml`, the slots it was placed with, in the server's saved data rather than its block, so a dragon will reach it with no chunk loaded (`SavedWellHatches.of(server)`, a `WellHatches` of `WellHatches.Hatch`). Every face is an item face, so a hopper under it empties it and one above fills it; it is never filled by hand: its owner using it with an item is told to set a hopper on it, the item's own use not running, and their empty hand takes its first stack out; anyone else is told whose it is. Its Well no longer whole, it keeps what it holds, and its core broken it is a plain container; broken itself, it drops what it held. Jade shows what it holds. No dragon loads or unloads it yet. `WellStructure.hatchFace(core, facing, against, face)` says where one goes. (#113) - **Breaking:** a Matter Dragon Egg founds the Matter line of Void Dragons (ADR 0011). It is a Drained Dragon Egg upgraded by a Mutation Chamber or a Void Crucible through the new `dragon_eggs` Family, one egg for one at the steep Rung's 800 void energy and even chance (`#voidworks:rung/dragon_eggs/drained` and `/matter`); nothing upgrades it back, and the vanilla egg has no upgrade. It is the Drained egg tinted amber, placed, in the hand and soaking, and until it soaks it falls and teleports when hit. It soaks at the price any egg does, counting toward its owner's next as any dragon does, gives off amber flecks of itself as it soaks, in place of a Mote-line egg's sparks, and hatches a Dragonling of the Matter line, whose grown dragon is of it too. A Matter-line dragon carries nothing yet: routing gives it nothing to load, while it rests, is ridden and called, and heals as any dragon does. Every dragon has a `Line`, `MOTE` or `MATTER`: `VoidDragonRecord` gains `line` after `owner`, which changes its constructor, `VoidDragons.add(owner, line, rest)` adds one of a Line, `VoidDragon.hatchFor(owner, line)` hatches one and `VoidDragon.line()` gives it, `EggKind` gains `MATTER` and `line()`, and the soaking egg's `egg` property gains `matter`. Dragons and Dragonlings saved before Lines are of the Mote line. (#112) diff --git a/GLOSSARY.md b/GLOSSARY.md index e8e5d8a..a841f08 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -83,7 +83,8 @@ _Avoid_: bedrock, residue A machine that harvests motes until its Zone is spent, attuned to the dimension it is built for and working only there. It holds no motes and needs no network to start: it harvests outward from itself, only positions touching it or the Voidstone it has already left, nearest first, so what it -has harvested is the network its motes go into. It cannot be broken or moved while its Zone is +has harvested is the network its motes go into. The End's grows only into a pyramid widening out +ahead of its front face. It cannot be broken or moved while its Zone is unspent and it can still reach something to convert; then it becomes Voidstone itself. _Avoid_: harvester, Void Well diff --git a/docs/spec/void-energy.md b/docs/spec/void-energy.md index a2a384a..5aa7827 100644 --- a/docs/spec/void-energy.md +++ b/docs/spec/void-energy.md @@ -132,9 +132,9 @@ starts. A spent Displacer becoming Voidstone shows the same, its own casing eate unloads drops the effect, and nothing of it is saved. - **End**: places fed blocks into the empty air of its zone. It faces the way its placer looked and - grows only into a square pyramid widening out ahead of that face, one block a step ahead, a 3×3 two - steps ahead, and so on, so it never walls in its other faces, and on the ground it leaves the ring - around it clear for a player to stand at its sides. + grows only into a square pyramid widening out ahead of that face, one position a step ahead, a 3×3 + two steps ahead, and so on, so it never walls in its other faces, and on the ground it leaves the + ring around it clear for a player to stand at its sides. - **Overworld**: swaps fed blocks with natural terrain in its zone — the blocks in a natural-block tag (stone, deepslate, dirt, ores…), which placed stone also matches. The terrain block goes to the Displacer's output, through a data table keyed by the terrain block and the fed block's diff --git a/src/main/java/io/github/_5thlayer/voidworks/displacer/EndDisplacerBlockEntity.java b/src/main/java/io/github/_5thlayer/voidworks/displacer/EndDisplacerBlockEntity.java index 5b21472..a0fb7d5 100644 --- a/src/main/java/io/github/_5thlayer/voidworks/displacer/EndDisplacerBlockEntity.java +++ b/src/main/java/io/github/_5thlayer/voidworks/displacer/EndDisplacerBlockEntity.java @@ -17,9 +17,8 @@ * The End Void Displacer: it places fed blocks into the air ahead of its front face, touching it or * the Voidstone it has left, so its body grows outward that way as a pyramid widening from that face, * never walling in its other faces nor roofing over the ground around it, and nothing comes back to - * its output. Over the open void its - * harvests grade one step higher, as the Void Pressure there is, and each Voidstone it leaves closes - * the column above it. It is placed Dormant, and harvests only once it has soaked its cost. + * its output. Over the open void its harvests grade one step higher, as the Void Pressure there is, + * and each Voidstone it leaves closes the column above it. It is placed Dormant, and harvests only once it has soaked its cost. */ public final class EndDisplacerBlockEntity extends DisplacerBlockEntity implements Dormancy.Holder { diff --git a/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java b/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java index 1438c9b..473f493 100644 --- a/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java +++ b/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java @@ -11,11 +11,13 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; +import net.minecraft.core.Vec3i; /** * Where a Void Displacer harvests next, as plain logic over positions: it grows outward from * itself, reaching only positions that touch it or the Voidstone it has already left, so a cave or - * an air pocket bounds it. Of those, it takes the nearest to itself first. + * an air pocket bounds it. Of those, it takes the nearest to itself first. A Displacer that grows + * through one face reaches only the pyramid {@link #ahead} of it. */ public final class Frontier { @@ -36,16 +38,21 @@ public static Predicate ahead(BlockPos displacer, Direction facing) { return pos -> { var offset = pos.subtract(displacer); int ahead = offset.get(axis) * facing.getAxisDirection().getStep(); - int aside = 0; - for (var other : Direction.Axis.values()) { - if (other != axis) { - aside = Math.max(aside, Math.abs(offset.get(other))); - } - } - return ahead >= 1 && aside < ahead; + return ahead >= 1 && aside(offset, axis) < ahead; }; } + /** How far {@code offset} lies aside of {@code axis}: the larger of its two other coordinates, either way. */ + private static int aside(Vec3i offset, Direction.Axis axis) { + int aside = 0; + for (var other : Direction.Axis.values()) { + if (other != axis) { + aside = Math.max(aside, Math.abs(offset.get(other))); + } + } + return aside; + } + /** * @param displacer the Displacer's position * @param grown the positions it has harvested, each Voidstone now diff --git a/src/test/java/io/github/_5thlayer/voidworks/displacer/FrontierTest.java b/src/test/java/io/github/_5thlayer/voidworks/displacer/FrontierTest.java index ca827e6..b514958 100644 --- a/src/test/java/io/github/_5thlayer/voidworks/displacer/FrontierTest.java +++ b/src/test/java/io/github/_5thlayer/voidworks/displacer/FrontierTest.java @@ -81,20 +81,20 @@ void aheadIsOnlyBeyondItsFrontFace() { void aheadWidensAsAPyramid() { for (var facing : Direction.values()) { var ahead = Frontier.ahead(DISPLACER, facing); - var one = DISPLACER.relative(facing); - var two = DISPLACER.relative(facing, 2); + var oneAhead = DISPLACER.relative(facing); + var twoAhead = DISPLACER.relative(facing, 2); var across = Direction.Plane.HORIZONTAL.test(facing) ? facing.getClockWise() : Direction.NORTH; var along = facing.getAxis() == Direction.Axis.Y ? Direction.EAST : Direction.UP; - for (int a = -1; a <= 1; a++) { - for (int b = -1; b <= 1; b++) { - var beside = one.relative(across, a).relative(along, b); - assertEquals(a == 0 && b == 0, ahead.test(beside), facing + ": a step ahead, " + a + " and " + b + " aside"); - var pos = two.relative(across, a).relative(along, b); + for (int dAcross = -1; dAcross <= 1; dAcross++) { + for (int dAlong = -1; dAlong <= 1; dAlong++) { + var beside = oneAhead.relative(across, dAcross).relative(along, dAlong); + assertEquals(dAcross == 0 && dAlong == 0, ahead.test(beside), facing + ": a step ahead, " + dAcross + " and " + dAlong + " aside"); + var pos = twoAhead.relative(across, dAcross).relative(along, dAlong); assertTrue(ahead.test(pos), facing + ": two steps ahead, within one aside, " + pos); } } - assertFalse(ahead.test(two.relative(across, 2)), facing + ": two steps ahead and two aside"); - assertFalse(ahead.test(two.relative(along, -2).relative(across, 1)), facing + ": two steps ahead and two aside the other way"); + assertFalse(ahead.test(twoAhead.relative(across, 2)), facing + ": two steps ahead and two aside"); + assertFalse(ahead.test(twoAhead.relative(along, -2).relative(across, 1)), facing + ": two steps ahead and two aside the other way"); } } From e7ab2f7cb6d6178d91128c9ea65fd8bae48929d7 Mon Sep 17 00:00:00 2001 From: Andrea D'Amico Date: Fri, 16 Oct 2026 22:45:23 +0200 Subject: [PATCH 4/5] refactor: reflow the end displacer docs and unshadow the pyramid's locals Co-Authored-By: Claude Opus 5.5 --- GLOSSARY.md | 6 +++--- .../voidworks/displacer/EndDisplacerBlockEntity.java | 3 ++- .../github/_5thlayer/voidworks/displacer/Frontier.java | 10 +++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/GLOSSARY.md b/GLOSSARY.md index a841f08..3344393 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -83,9 +83,9 @@ _Avoid_: bedrock, residue A machine that harvests motes until its Zone is spent, attuned to the dimension it is built for and working only there. It holds no motes and needs no network to start: it harvests outward from itself, only positions touching it or the Voidstone it has already left, nearest first, so what it -has harvested is the network its motes go into. The End's grows only into a pyramid widening out -ahead of its front face. It cannot be broken or moved while its Zone is -unspent and it can still reach something to convert; then it becomes Voidstone itself. +has harvested is the network its motes go into. The End Displacer grows only into a pyramid +widening out ahead of its front face. It cannot be broken or moved while its Zone is unspent and it +can still reach something to convert; then it becomes Voidstone itself. _Avoid_: harvester, Void Well **Zone**: diff --git a/src/main/java/io/github/_5thlayer/voidworks/displacer/EndDisplacerBlockEntity.java b/src/main/java/io/github/_5thlayer/voidworks/displacer/EndDisplacerBlockEntity.java index a0fb7d5..d70502f 100644 --- a/src/main/java/io/github/_5thlayer/voidworks/displacer/EndDisplacerBlockEntity.java +++ b/src/main/java/io/github/_5thlayer/voidworks/displacer/EndDisplacerBlockEntity.java @@ -18,7 +18,8 @@ * the Voidstone it has left, so its body grows outward that way as a pyramid widening from that face, * never walling in its other faces nor roofing over the ground around it, and nothing comes back to * its output. Over the open void its harvests grade one step higher, as the Void Pressure there is, - * and each Voidstone it leaves closes the column above it. It is placed Dormant, and harvests only once it has soaked its cost. + * and each Voidstone it leaves closes the column above it. It is placed Dormant, and harvests only + * once it has soaked its cost. */ public final class EndDisplacerBlockEntity extends DisplacerBlockEntity implements Dormancy.Holder { diff --git a/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java b/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java index 473f493..bf3be8f 100644 --- a/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java +++ b/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java @@ -37,20 +37,20 @@ public static Predicate ahead(BlockPos displacer, Direction facing) { var axis = facing.getAxis(); return pos -> { var offset = pos.subtract(displacer); - int ahead = offset.get(axis) * facing.getAxisDirection().getStep(); - return ahead >= 1 && aside(offset, axis) < ahead; + int reach = offset.get(axis) * facing.getAxisDirection().getStep(); + return reach >= 1 && aside(offset, axis) < reach; }; } /** How far {@code offset} lies aside of {@code axis}: the larger of its two other coordinates, either way. */ private static int aside(Vec3i offset, Direction.Axis axis) { - int aside = 0; + int sideways = 0; for (var other : Direction.Axis.values()) { if (other != axis) { - aside = Math.max(aside, Math.abs(offset.get(other))); + sideways = Math.max(sideways, Math.abs(offset.get(other))); } } - return aside; + return sideways; } /** From 43359fb559c665b773262c81605e3cc6ff6244df Mon Sep 17 00:00:00 2001 From: Andrea D'Amico Date: Fri, 16 Oct 2026 22:47:02 +0200 Subject: [PATCH 5/5] refactor: call the end displacer pyramid's forward distance its depth Co-Authored-By: Claude Opus 5.5 --- .../io/github/_5thlayer/voidworks/displacer/Frontier.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java b/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java index bf3be8f..e854218 100644 --- a/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java +++ b/src/main/java/io/github/_5thlayer/voidworks/displacer/Frontier.java @@ -37,8 +37,8 @@ public static Predicate ahead(BlockPos displacer, Direction facing) { var axis = facing.getAxis(); return pos -> { var offset = pos.subtract(displacer); - int reach = offset.get(axis) * facing.getAxisDirection().getStep(); - return reach >= 1 && aside(offset, axis) < reach; + int depth = offset.get(axis) * facing.getAxisDirection().getStep(); + return depth >= 1 && aside(offset, axis) < depth; }; }