Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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)
Expand Down
5 changes: 3 additions & 2 deletions GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +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. 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**:
Expand Down
4 changes: 3 additions & 1 deletion docs/spec/void-energy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ 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,
helper -> inTheEnd(helper, 4, EndDisplacerTests::growsOnlyAhead));
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::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,
Expand Down Expand Up @@ -157,8 +157,11 @@ 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. */
private static void growsOnlyAhead(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 growsAsAPyramidAhead(DimensionSite site) {
var displacer = place(site, 8, Direction.EAST);
site.feed(displacer, 8);
site.payInFull(displacer);
Expand All @@ -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));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@

/**
* 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
* 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.
* 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.
*/
public final class EndDisplacerBlockEntity extends DisplacerBlockEntity implements Dormancy.Holder {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,46 @@

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 {

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 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<BlockPos> 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 depth = offset.get(axis) * facing.getAxisDirection().getStep();
return depth >= 1 && aside(offset, axis) < depth;
};
}

/** 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 sideways = 0;
for (var other : Direction.Axis.values()) {
if (other != axis) {
sideways = Math.max(sideways, Math.abs(offset.get(other)));
}
}
return sideways;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,70 @@ 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, 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(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 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 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(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");
}
}

/** 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<BlockPos>();
for (int i = 0; i < 64; i++) {
var next = Frontier.next(DISPLACER, grown, ahead);
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);
Expand Down
Loading