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

- With Groundworks present, a Void Well is placed in one step from its core: holding a core shows the whole Well in Groundworks' preview, and a click places the core, its 56 casing and 16 rim around an open shaft, paid from the inventory, all or nothing, the core facing out of its wall toward the player, so the Well is whole at once. Once placed it is an ordinary Well, and breaking a casing stops it as before. Without Groundworks nothing changes: it is built by hand. The Assembly is data alone, `data/voidworks/groundworks/assembly/void_well.json`, its core made a Key by the `groundworks:assembly_keys` block tag. Voidworks still requires nothing but Minecraft and NeoForge. (#65)
- **Breaking:** a grown Void Dragon ages through four life stages, young, adult, senior and old, by the motes it unloads over its life, each stage `lifeStageMotes` (524,288) long, changing only at a stop (ADR 0010). Each stage multiplies its load and speed (`lifeStageLoads` 1/2/3.5/6, `lifeStageSpeeds` 1/0.6/0.35/0.2 in `voidworks-server.toml`), its speed stacking on its health's share, floored at a quarter; a new load applies from its next load and a new speed from its next dive. Only an adult lays Drained Dragon Eggs: eggs it still owes on turning senior are laid at its next loaded stop, then no more. An old dragon whose stage runs out dies at the end of a stop at a loaded Well, carrying nothing, living on past an unloaded one: it sets any rider down on the Well's rim, lays the eggs it owes, and its presence rises over the Well in a short Ender Dragon death, beams and sound, leaving nothing; nothing else tells its owner. An egg's price now counts its owner's living dragons and eggs soaking, not every dragon they ever hatched, so a death, or `/voidworks dragon remove`, lowers it (`Hatchery.lose`). `/voidworks dragon age <stage>` sets the grown dragon an operator looks at to the start of that stage, owing no egg. Jade shows a dragon's stage, and its presence is sized 脳0.8, 脳1, 脳1.1 or 脳1.2 by it. Dragons saved before this start young, their count toward an egg dropped. `Routing` takes a `LifeStages` and `step` returns the dragons that died; `VoidDragonRecord` gains `cargo` and loses `speed()`, now `LifeStages.speed(dragon)`; `DragonReport` gains `stage`. (#54)
- **Breaking:** a dragon egg's price is datapack data, no longer server config (ADR 0009). The `voidworks:egg_price` block data map, on `voidworks:soaking_dragon_egg`, names an owner's `first_price` (25600 void energy) and its `growth` (2), the multiple each dragon they have hatched or are soaking puts on their next egg; a growth of 1 keeps every egg at the first price, and an egg with no entry is free. `firstEggPrice` is gone from `voidworks-server.toml` and `VoidworksConfig`. `EggPrice.of(dragons)` gives a price, `EggPrice.of(state)` an egg block's entry, and `Hatchery.startSoaking` takes the `EggPrice`; `Hatchery.price` and `Hatchery.codec` give way to `EggPrice.of` and `Hatchery.CODEC`. (#25)
- A Void Dragon resting at a Well lands and sits astride its rim, over the shaft, lengthwise along the wall its core is set in so neither head nor tail covers the core, in the Ender Dragon's sitting pose, its wings held still and down, turning its head toward a nearby player. Where blocks fill the room its body takes there, it flies about over the shaft instead and lands once there is room. As it sets off it takes off, then dives into the shaft; mounted by its owner, it takes off to hover, and lands again once they get off. One at work at a Well, loading, unloading, waiting for its owner or setting a rider down, stays in the air, and one back after its chunk unloaded is sitting already. `VoidDragon.perch()` says whether a presence flies, lands, sits or takes off (`VoidDragon.Perch`), `perchYaw()` the heading it keeps meanwhile and `seatPos()` where it sits. A ridden dragon now hovers in place: before, its wandering pulled it slowly about under its rider. (#81)
Expand Down
16 changes: 10 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ libworks {

repositories {
// libworks-runtime, from the local maven only, where its releases are published. CI builds its tag
// there first. Gearworks too, which only the dev client loads, so CI never resolves it.
// there first. Gearworks and Groundworks too, which only the dev client loads, so CI never resolves them.
exclusiveContent {
forRepository { mavenLocal() }
filter {
includeModule 'io.github.5thlayer', 'libworks-runtime'
includeModule 'io.github.5thlayer', 'libworks-gametest'
includeModule 'io.github.5thlayer', 'gearworks'
includeModule 'io.github.5thlayer', 'groundworks'
}
}
// EMI for 26.1 is an unofficial port, published on CurseForge only.
Expand All @@ -36,11 +37,11 @@ repositories {
}

// Mods the dev client loads alongside the Module, to look up recipes (EMI), inspect blocks (Jade) and
// wear FE items a Void Siphon charges (Gearworks) while trying it by hand. 26.1 has no per-run
// classpath, so they go into the client's mods folder, which holds only what is listed here; the
// game tests run elsewhere and never load them, so they still prove the Module plays alone. It names
// EMI's types only from compat/emi and Jade's only from its plugin, each loaded by its viewer alone,
// and no type of Gearworks'.
// wear FE items a Void Siphon charges (Gearworks) and place a Void Well in one step (Groundworks)
// while trying it by hand. 26.1 has no per-run classpath, so they go into the client's mods folder,
// which holds only what is listed here; the game tests run elsewhere and never load them, so they
// still prove the Module plays alone. It names EMI's types only from compat/emi and Jade's only from
// its plugin, each loaded by its viewer alone, and no type of Gearworks' or Groundworks'.
configurations {
clientMods { transitive = false }
}
Expand All @@ -64,6 +65,9 @@ dependencies {
// Gearworks' Gear, an FE item to wear while a Void Siphon charges it (ADR 0007). Voidworks names
// none of its types and requires it nowhere: it charges through NeoForge's item energy capability.
clientMods 'io.github.5thlayer:gearworks:0.1.0'
// Groundworks, which places the Void Well in one step from its core, an Assembly the Module
// declares as data alone (#65). Voidworks names none of its types and requires it nowhere.
clientMods 'io.github.5thlayer:groundworks:0.5.12'
}

neoForge {
Expand Down
6 changes: 4 additions & 2 deletions docs/spec/void-energy.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ the Modules do uses void, so it is not something the base mod holds for them, an
leave the End-centred endgame out. It requires nothing a player installs but Minecraft and NeoForge
(libworks-runtime, which shows it in Jade, is nested in its jar): its inputs are
vanilla, Mutation reads the recipe manager, and its outputs go through NeoForge's item and fluid
faces. If the Displacer or the Void Well become multiblocks, it may require Groundworks, as Beltworks
and Wireworks do. It charges other mods' FE items through NeoForge's item energy capability alone,
faces. The Void Well is placed in one step from its core when Groundworks is present, and is built
by hand otherwise: its shape is a Groundworks Assembly declared as data, which Groundworks alone
reads. If the Displacer becomes a multiblock, it may require Groundworks, as Beltworks and
Wireworks do. It charges other mods' FE items through NeoForge's item energy capability alone,
so Gearworks, whose Gear it was tuned against, is only a mod its dev client runs, never a
dependency, and its game tests charge an FE item of their own.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"voidworks:void_well"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"key": "K",
"legend": {
"K": "voidworks:void_well",
"c": "voidworks:void_well_casing",
"r": "voidworks:void_well_rim",
".": "empty"
},
"layers": [
[" ",
" ccc ",
" ccc ",
" ccc ",
" "],
["ccccc",
"c...c",
"c...c",
"c...c",
"ccccc"],
["ccccc",
"c...c",
"c...c",
"c...c",
"ccKcc"],
["ccccc",
"c...c",
"c...c",
"c...c",
"ccccc"],
["rrrrr",
"r...r",
"r...r",
"r...r",
"rrrrr"]
]
}
112 changes: 112 additions & 0 deletions src/test/java/io/github/_5thlayer/voidworks/well/WellAssemblyTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: 2026 5thlayer
// SPDX-License-Identifier: MIT

package io.github._5thlayer.voidworks.well;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.github._5thlayer.voidworks.well.WellStructure.Part;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

/**
* The Void Well's Groundworks Assembly, a second statement of its shape, says what {@link
* WellStructure#layout} says: placed from its core, facing any way, it stands a whole Well.
*
* <p>The declaration is read as Groundworks reads it ({@code AssemblyShape}): layers bottom first,
* each drawn as a player at the bottom of the page sees it looking up the page, turned into the
* Key's frame (forward {@code x}, the player's left {@code z}) and that frame into the world by the
* Key's facing as {@code FootprintShape.Local#inWorld} does, as of Groundworks 0.5.12. No Groundworks
* type is named.
*/
class WellAssemblyTest {

private static final String DECLARATION = "/data/voidworks/groundworks/assembly/void_well.json";
private static final BlockPos CORE = new BlockPos(10, 66, 10);

@ParameterizedTest
@EnumSource(value = Direction.class, names = {"NORTH", "SOUTH", "EAST", "WEST"})
void placedFromItsCoreItStandsTheWellsLayout(Direction facing) throws Exception {
assertEquals(WellStructure.layout(CORE, facing), placed(CORE, facing));
}

/** What each position the Assembly places from a Key at {@code core} holds, the Key's own left out. */
private static Map<BlockPos, Part> placed(BlockPos core, Direction facing) throws Exception {
var declaration = declaration();
char key = declaration.get("key").getAsString().charAt(0);
var legend = declaration.getAsJsonObject("legend");
assertEquals("voidworks:void_well", legend.get(String.valueOf(key)).getAsString());
var drawn = drawn(declaration);
var keys = drawn.entrySet().stream().filter(entry -> entry.getValue() == key).map(Map.Entry::getKey).toList();
assertEquals(1, keys.size(), "the shape holds its Key once");
var at = keys.getFirst();

var placed = new HashMap<BlockPos, Part>();
drawn.forEach((spot, letter) -> {
if (letter != key) {
var offset = inWorld(at.row() - spot.row(), spot.layer() - at.layer(), at.column() - spot.column(), facing);
placed.put(core.offset(offset), part(legend.get(String.valueOf(letter)).getAsString()));
}
});
return placed;
}

/** Where a letter is drawn: its layer from the bottom, its row from the top of the page, its column from the left. */
private record Spot(int layer, int row, int column) {
}

private static JsonObject declaration() throws Exception {
try (var in = WellAssemblyTest.class.getResourceAsStream(DECLARATION)) {
assertNotNull(in, DECLARATION);
return JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8)).getAsJsonObject();
}
}

/** Every letter the layers draw, spaces left out, by where it is drawn. */
private static Map<Spot, Character> drawn(JsonObject declaration) {
var layers = declaration.getAsJsonArray("layers");
var drawn = new HashMap<Spot, Character>();
for (int layer = 0; layer < layers.size(); layer++) {
var rows = layers.get(layer).getAsJsonArray();
for (int row = 0; row < rows.size(); row++) {
String letters = rows.get(row).getAsString();
for (int column = 0; column < letters.length(); column++) {
if (letters.charAt(column) != ' ') {
drawn.put(new Spot(layer, row, column), letters.charAt(column));
}
}
}
}
return drawn;
}

private static Part part(String block) {
return switch (block) {
case "voidworks:void_well_casing" -> Part.CASING;
case "voidworks:void_well_rim" -> Part.RIM;
case "empty" -> Part.OPEN;
default -> throw new AssertionError("not a part of the Well: " + block);
};
}

/** Groundworks' turn of an offset in the Key's frame into the world, for a Key facing this way. */
private static BlockPos inWorld(int x, int y, int z, Direction facing) {
return switch (facing) {
case NORTH -> new BlockPos(z, y, x);
case WEST -> new BlockPos(x, y, -z);
case SOUTH -> new BlockPos(-z, y, -x);
case EAST -> new BlockPos(-x, y, z);
default -> throw new IllegalArgumentException(facing.toString());
};
}
}
Loading