Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dbe208c42 |
No files matched your search
@@ -109,6 +109,21 @@ write an Age using a biome where that mod registers its ore and verify the ore
|
||||
also appears more often. Check that Flat plus Dense Ores is rejected by the
|
||||
Binder and that a random, Link-Panel-only Age has no Dense Ores feature page.
|
||||
|
||||
**Obelisks** adds Legacy-shaped, rare 4×4 plinths with a 2×2 shaft rising 12
|
||||
blocks, at most one attempt per 128 chunks per page. A preceding full-block
|
||||
Block page chooses the material; otherwise the Age seed chooses from valid
|
||||
block pages. Repeated Obelisks pages make independent attempts. Generation is
|
||||
deterministic and stays within one chunk; it requires a reasonably level,
|
||||
exposed solid surface, so Cave and Void terrain reject the page. Standard,
|
||||
Amplified, Sky Islands, and Flat terrain permit it. Page-free random surface
|
||||
Ages can roll Obelisks as one of their material features.
|
||||
|
||||
For a visual check, write a Standard or Flat Age with an Obelisks page and
|
||||
search newly generated terrain for the 4×4 base and 12-block shaft. Try a
|
||||
Block page such as Obsidian immediately before Obelisks to verify its material
|
||||
changes. The Binder should reject Obelisks in Cave and Void Ages. Placement
|
||||
is deliberately rare, so several hundred fresh chunks may be needed.
|
||||
|
||||
For an in-game visual check, bind Descriptive Books with a Link Panel,
|
||||
one of Standard, Amplified, Sky Islands, Flat, or Void terrain, and the Floating Islands
|
||||
feature page in that order. Fly through each Age at roughly Y=170–220 and
|
||||
|
||||
@@ -18,7 +18,7 @@ Legacy does **not** reject a book just because it omits terrain, biome, weather,
|
||||
| `WeatherNorm`, `Fast`, `Slow`, `Off`, `On`, `Cloudy`, `Rain`, `Snow`, `Storm` | `AgeTraits.Weather` has matching names except Legacy `On` maps to `ALWAYS` | Compile weather semantics separately from biome selection. `Snow` must remain deferred until optional season integration; do not silently substitute another weather mode. |
|
||||
| Suns, moons, starfields; angle, phase, period, color/gradient/sunset modifiers | One `celestialPeriod`, `celestialAngle`, `celestialPhase` on `AgeTraits` | Compile only the representable single-cycle subset first. Multiple celestial bodies, independent periods, custom stars, gradients and sunsets need a richer definition and renderer, not flattening into one value. |
|
||||
| Sky/fog/cloud/grass/foliage/water colors, natural variants, horizon, rainbow | Existing sky/dimension visuals and preview | Require an explicit modern client/biome-color design; metadata-only symbol pages must not pretend to change rendering. |
|
||||
| Caves, ores, huge trees, lakes, floating islands, tendrils, skylands, spheres, spikes, crystals; vanilla villages, strongholds, mineshafts, dungeons, fortresses, obelisks, fissures | Current cave profile, authored Floating Islands/Tendrils/Skylands/Spheres/Spikes, and page-gated vanilla Dungeons/Mineshafts/Nether Fortresses/Strongholds/Villages | The five Legacy vanilla structure symbols now gate Minecraft 1.21.1 generation. Dungeons use monster-room placement; the others use vanilla structure sets. Bespoke obelisks, fissures, crystals, and exact 1.12 structure geometry remain separate work. |
|
||||
| Caves, ores, huge trees, lakes, floating islands, tendrils, skylands, spheres, spikes, crystals; vanilla villages, strongholds, mineshafts, dungeons, fortresses, obelisks, fissures | Current cave profile, authored Floating Islands/Tendrils/Skylands/Spheres/Spikes/Obelisks and Dense Ores, and page-gated vanilla Dungeons/Mineshafts/Nether Fortresses/Strongholds/Villages | The five Legacy vanilla structure symbols gate Minecraft 1.21.1 generation. Dungeons use monster-room placement; the others use vanilla structure sets. Obelisks use a bespoke 4×4 plinth and 2×12×2 shaft with safe modern placement. Fissures, crystals, and exact 1.12 structure geometry remain separate work. |
|
||||
| `EnvAccel`, explosions, lightning, meteors, scorching; PvP and dummy suppression pages | No equivalent Age-definition effect | Defer environmental hazards/instability effects to milestone 6, PvP rule to a separately scoped server policy; retain authored IDs without fake behavior. |
|
||||
|
||||
The Legacy symbol list and page names come from [ModSymbols](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/ModSymbols.java#L129-L240) and [SymbolRules](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/SymbolRules.java#L19-L137). Rivencraft's [catalog](../src/main/java/net/dnorberg/rivencraft/writing/SymbolCatalog.java) currently holds display metadata only; its built-in set is just standard terrain, native biomes, normal weather, normal lighting, normal time, and caves. [OrderedWriting](../src/main/java/net/dnorberg/rivencraft/writing/OrderedWriting.java) preserves page order, but no compiler yet connects it to [AgeTraits](../src/main/java/net/dnorberg/rivencraft/book/AgeTraits.java) or [Age profiles](AGE_PROFILES.md).
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ mod_name=Rivencraft
|
||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||
mod_license=LGPL-3.0-or-later
|
||||
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
||||
mod_version=0.4.16.0
|
||||
mod_version=0.4.17.0
|
||||
immersive_portals_version=6.0.7
|
||||
terrablender_version=4.1.0.8
|
||||
serene_seasons_version_id=B6nTjh4b
|
||||
|
||||
@@ -27,7 +27,8 @@ import net.minecraft.world.level.block.Blocks;
|
||||
/** Validates and freezes supported feature pages and their preceding material modifiers. */
|
||||
public final class AgeFeatureSelection {
|
||||
private static final List<ResourceLocation> BLOCK_FEATURES = List.of(
|
||||
id("rocks"), id("floating_islands"), id("tendrils"), id("spheres"), id("spikes"));
|
||||
id("rocks"), id("floating_islands"), id("tendrils"), id("spheres"), id("spikes"),
|
||||
id("obelisks"));
|
||||
private static final List<ResourceLocation> FLUID_FEATURES = List.of(
|
||||
id("surface_lakes"), id("deep_lakes"), id("lava_lakes"));
|
||||
private static final long RANDOM_SKYLANDS_SALT = 0x534B594C414E4453L;
|
||||
@@ -121,6 +122,8 @@ public final class AgeFeatureSelection {
|
||||
|
||||
private static boolean compatible(ResourceLocation feature, AgeRuntimeProfile profile) {
|
||||
if (tendrils(feature) || spheres(feature)) return true;
|
||||
if (obelisks(feature)) return !AgeBiomeSelection.isCave(profile)
|
||||
&& profile.terrain() != AgeRuntimeProfile.Terrain.VOID;
|
||||
if (AgeBiomeSelection.isCave(profile)) return false;
|
||||
if (spikes(feature)) return profile.terrain() != AgeRuntimeProfile.Terrain.VOID;
|
||||
return floatingIslands(feature) || profile.terrain() == AgeRuntimeProfile.Terrain.NOISE;
|
||||
@@ -136,7 +139,8 @@ public final class AgeFeatureSelection {
|
||||
if (choice.material().isPresent()
|
||||
|| !isRock(choice.id()) && !isLake(choice.id())
|
||||
&& !floatingIslands(choice.id()) && !tendrils(choice.id())
|
||||
&& !spheres(choice.id()) && !spikes(choice.id())) {
|
||||
&& !spheres(choice.id()) && !spikes(choice.id())
|
||||
&& !obelisks(choice.id())) {
|
||||
resolved.add(choice);
|
||||
continue;
|
||||
}
|
||||
@@ -208,6 +212,11 @@ public final class AgeFeatureSelection {
|
||||
return "Feature " + choice + " needs an exposed solid surface, not Cave or Void terrain";
|
||||
return null;
|
||||
}
|
||||
if (obelisks(choice)) {
|
||||
if (AgeBiomeSelection.isCave(profile) || profile.terrain() == AgeRuntimeProfile.Terrain.VOID)
|
||||
return "Feature " + choice + " needs exposed solid surface terrain, not Cave or Void";
|
||||
return null;
|
||||
}
|
||||
if (floatingIslands(choice)) {
|
||||
if (AgeBiomeSelection.isCave(profile))
|
||||
return "Feature " + choice + " cannot form above Cave terrain";
|
||||
@@ -298,9 +307,13 @@ public final class AgeFeatureSelection {
|
||||
return choice.equals(id("spikes"));
|
||||
}
|
||||
|
||||
public static boolean obelisks(ResourceLocation choice) {
|
||||
return choice.equals(id("obelisks"));
|
||||
}
|
||||
|
||||
private static boolean solidFeature(ResourceLocation choice) {
|
||||
return isRock(choice) || floatingIslands(choice) || tendrils(choice)
|
||||
|| spheres(choice) || spikes(choice);
|
||||
|| spheres(choice) || spikes(choice) || obelisks(choice);
|
||||
}
|
||||
|
||||
public static boolean skylands(ResourceLocation choice) {
|
||||
|
||||
@@ -40,7 +40,7 @@ public final class FeatureWritingGameTests {
|
||||
"feature/bastions", "feature/buried_treasure", "feature/shipwrecks",
|
||||
"feature/ocean_ruins", "feature/pillager_outposts", "feature/ruined_portals",
|
||||
"feature/nether_fossils", "feature/ancient_cities", "feature/trail_ruins",
|
||||
"feature/dense_ores");
|
||||
"feature/dense_ores", "feature/obelisks");
|
||||
helper.assertTrue(PageItem.bundledCreativeSymbols().containsAll(pages),
|
||||
"Supported feature pages must appear in the creative and JEI symbol feed");
|
||||
for (String path : pages) {
|
||||
@@ -72,6 +72,7 @@ public final class FeatureWritingGameTests {
|
||||
|| AgeFeatureSelection.skylands(choice.id())
|
||||
|| AgeFeatureSelection.spheres(choice.id())
|
||||
|| AgeFeatureSelection.spikes(choice.id())
|
||||
|| AgeFeatureSelection.obelisks(choice.id())
|
||||
|| choice.id().equals(AgeDenseOres.ID)) continue;
|
||||
var placement = AgeFeatureSelection.placedFeature(choice.id());
|
||||
helper.assertTrue(registries.lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
@@ -419,7 +420,8 @@ public final class FeatureWritingGameTests {
|
||||
|| AgeFeatureSelection.floatingIslands(choices.getFirst().id())
|
||||
|| AgeFeatureSelection.tendrils(choices.getFirst().id())
|
||||
|| AgeFeatureSelection.spheres(choices.getFirst().id())
|
||||
|| AgeFeatureSelection.spikes(choices.getFirst().id()),
|
||||
|| AgeFeatureSelection.spikes(choices.getFirst().id())
|
||||
|| AgeFeatureSelection.obelisks(choices.getFirst().id()),
|
||||
"The inferred feature must accept the material type");
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(AgeGenerationPlan.load(plan.save())), choices,
|
||||
"The inferred feature must be deterministic across a plan reload");
|
||||
|
||||
@@ -162,6 +162,7 @@ public final class AgeFeatureChunkGenerator extends NoiseBasedChunkGenerator {
|
||||
BlockPos origin = new BlockPos(x, level.getMinBuildHeight(), z);
|
||||
Map<AgeFeatureSelection.Choice, Integer> occurrences = new HashMap<>();
|
||||
Map<ResourceLocation, Integer> spikeOccurrences = new HashMap<>();
|
||||
int obeliskOccurrence = 0;
|
||||
for (int index = 0; index < features.size(); index++) {
|
||||
AgeFeatureSelection.Choice choice = features.get(index);
|
||||
int occurrence = occurrences.merge(choice, 1, Integer::sum) - 1;
|
||||
@@ -170,6 +171,10 @@ public final class AgeFeatureChunkGenerator extends NoiseBasedChunkGenerator {
|
||||
spikeOccurrences.merge(choice.id(), 1, Integer::sum) - 1);
|
||||
continue;
|
||||
}
|
||||
if (AgeFeatureSelection.obelisks(choice.id())) {
|
||||
ObelisksFeature.generate(level, chunk, ageSeed, choice, obeliskOccurrence++);
|
||||
continue;
|
||||
}
|
||||
if (choice.id().equals(AgeDenseOres.ID)) {
|
||||
AgeDenseOres.place(level, chunk, this, ageSeed, occurrence);
|
||||
continue;
|
||||
|
||||
@@ -124,10 +124,13 @@ public final class AgeFlatChunkGenerator extends ChunkGenerator {
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structures) {
|
||||
super.applyBiomeDecoration(level, chunk, structures);
|
||||
int occurrence = 0;
|
||||
int spikeOccurrence = 0;
|
||||
int obeliskOccurrence = 0;
|
||||
for (AgeFeatureSelection.Choice choice : features) {
|
||||
if (AgeFeatureSelection.spikes(choice.id()))
|
||||
SpikesFeature.generate(level, chunk, ageSeed, choice, occurrence++);
|
||||
SpikesFeature.generate(level, chunk, ageSeed, choice, spikeOccurrence++);
|
||||
if (AgeFeatureSelection.obelisks(choice.id()))
|
||||
ObelisksFeature.generate(level, chunk, ageSeed, choice, obeliskOccurrence++);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/** Rare one-chunk obelisk adapted from Legacy's 4x4 plinth and 2x12x2 shaft. */
|
||||
final class ObelisksFeature {
|
||||
private static final long SALT = 0x4F42454C49534B53L;
|
||||
private static final int CHANCE = 128;
|
||||
private static final int BASE = 4;
|
||||
private static final int HEIGHT = 12;
|
||||
|
||||
private ObelisksFeature() {}
|
||||
|
||||
record Site(int x, int z) {}
|
||||
|
||||
static Site site(long ageSeed, ChunkPos chunk, int occurrence) {
|
||||
RandomSource random = RandomSource.create(mix(ageSeed ^ SALT
|
||||
^ (long) chunk.x * 341873128712L ^ (long) chunk.z * 132897987541L
|
||||
^ (long) occurrence * 0x9E3779B97F4A7C15L));
|
||||
if (random.nextInt(CHANCE) != 0) return null;
|
||||
return new Site(chunk.getMinBlockX() + 4 + random.nextInt(8),
|
||||
chunk.getMinBlockZ() + 4 + random.nextInt(8));
|
||||
}
|
||||
|
||||
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed,
|
||||
AgeFeatureSelection.Choice choice, int occurrence) {
|
||||
Site site = site(ageSeed, chunk.getPos(), occurrence);
|
||||
if (site == null || choice.material().isEmpty()) return;
|
||||
BlockState material = BuiltInRegistries.BLOCK.get(choice.material().orElseThrow()).defaultBlockState();
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
int minSurface = Integer.MAX_VALUE;
|
||||
int maxSurface = Integer.MIN_VALUE;
|
||||
int[][] surface = new int[BASE][BASE];
|
||||
for (int dx = 0; dx < BASE; dx++) {
|
||||
for (int dz = 0; dz < BASE; dz++) {
|
||||
int x = site.x() + dx;
|
||||
int z = site.z() + dz;
|
||||
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
|
||||
if (y <= level.getMinBuildHeight() + 1) return;
|
||||
BlockState ground = level.getBlockState(pos.set(x, y - 1, z));
|
||||
if (!ground.isFaceSturdy(level, pos, Direction.UP)
|
||||
|| !ground.getFluidState().isEmpty() || ground.hasBlockEntity()) return;
|
||||
surface[dx][dz] = y;
|
||||
minSurface = Math.min(minSurface, y);
|
||||
maxSurface = Math.max(maxSurface, y);
|
||||
}
|
||||
}
|
||||
if (maxSurface - minSurface > 2 || maxSurface + HEIGHT >= level.getMaxBuildHeight()) return;
|
||||
for (int dx = 0; dx < BASE; dx++) {
|
||||
for (int dz = 0; dz < BASE; dz++) {
|
||||
for (int y = surface[dx][dz]; y <= maxSurface; y++) {
|
||||
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int dx = 1; dx <= 2; dx++) {
|
||||
for (int dz = 1; dz <= 2; dz++) {
|
||||
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++) {
|
||||
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int dx = 0; dx < BASE; dx++) {
|
||||
for (int dz = 0; dz < BASE; dz++) {
|
||||
for (int y = surface[dx][dz]; y <= maxSurface; y++)
|
||||
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
|
||||
}
|
||||
}
|
||||
for (int dx = 1; dx <= 2; dx++) {
|
||||
for (int dz = 1; dz <= 2; dz++) {
|
||||
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++)
|
||||
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean open(BlockState state) {
|
||||
return state.getFluidState().isEmpty() && (state.isAir() || state.canBeReplaced());
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationPlan;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationProfiles;
|
||||
import net.dnorberg.rivencraft.book.AgeRuntimeProfiles;
|
||||
import net.dnorberg.rivencraft.book.AgeTraits;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
/** Obelisks must retain ordered materials and generate a stable Legacy-sized shape. */
|
||||
@GameTestHolder(Rivencraft.MOD_ID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class ObelisksGameTests {
|
||||
private ObelisksGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void authoredObelisksKeepTheirMaterialAndTerrainRules(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
helper.assertTrue(PageItem.bundledCreativeSymbols().contains("feature/obelisks"),
|
||||
"Obelisks must appear in the Creative and JEI symbol feed");
|
||||
OrderedWriting writing = new OrderedWriting(List.of(WritingEntry.linkPanel(), page("terrain/standard"),
|
||||
block("obsidian"), page("feature/obelisks"), page("feature/obelisks")));
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 56L, AgeGenerationProfiles.STANDARD,
|
||||
AgeTraits.select(56L), registries);
|
||||
var choices = AgeFeatureSelection.resolved(plan);
|
||||
helper.assertValueEqual(choices.size(), 2, "Repeated Obelisks pages must remain separate");
|
||||
helper.assertValueEqual(choices.getFirst().material(),
|
||||
Optional.of(ResourceLocation.withDefaultNamespace("obsidian")),
|
||||
"A preceding Block page must set the first obelisk material");
|
||||
helper.assertTrue(choices.getLast().material().isPresent()
|
||||
&& AgeFeatureSelection.materialError(choices.getLast()) == null,
|
||||
"An unmodified Obelisks page must choose a valid material");
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(AgeGenerationPlan.load(plan.save())), choices,
|
||||
"Obelisk materials and order must survive Age-plan reload");
|
||||
for (String terrain : List.of("standard", "amplified", "island", "flat"))
|
||||
helper.assertTrue(AgeFeatureSelection.error(id("obelisks"),
|
||||
AgeRuntimeProfiles.resolve(id(terrain)), registries) == null,
|
||||
"Obelisks should be valid on exposed " + terrain + " terrain");
|
||||
for (String terrain : List.of("void", "cave"))
|
||||
helper.assertTrue(AgeFeatureSelection.error(id("obelisks"),
|
||||
AgeRuntimeProfiles.resolve(id(terrain)), registries) != null,
|
||||
"Obelisks must reject " + terrain + " terrain without an exposed surface");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void randomAgesOnlyChooseObelisksOnSupportedTerrain(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
boolean selected = false;
|
||||
for (long seed = 0; seed < 128; seed++) {
|
||||
selected |= AgeFeatureSelection.randomChoices(seed,
|
||||
AgeRuntimeProfiles.resolve(id("standard")), registries).stream()
|
||||
.anyMatch(choice -> AgeFeatureSelection.obelisks(choice.id()));
|
||||
for (String terrain : List.of("void", "cave"))
|
||||
helper.assertTrue(AgeFeatureSelection.randomChoices(seed,
|
||||
AgeRuntimeProfiles.resolve(id(terrain)), registries).stream()
|
||||
.noneMatch(choice -> AgeFeatureSelection.obelisks(choice.id())),
|
||||
"Random " + terrain + " Ages must not gain Obelisks");
|
||||
}
|
||||
helper.assertTrue(selected, "Random surface Ages should be able to select Obelisks");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void obeliskSitesAreSeededAndStayInsideTheirChunk(GameTestHelper helper) {
|
||||
ChunkPos chunk = new ChunkPos(123, -45);
|
||||
int selected = 0;
|
||||
for (long seed = 0; seed < 2048; seed++) {
|
||||
ObelisksFeature.Site site = ObelisksFeature.site(seed, chunk, 0);
|
||||
helper.assertTrue(java.util.Objects.equals(ObelisksFeature.site(seed, chunk, 0), site),
|
||||
"An Age seed must reproduce the same obelisk site");
|
||||
if (site == null) continue;
|
||||
selected++;
|
||||
helper.assertTrue(site.x() >= chunk.getMinBlockX() + 4
|
||||
&& site.x() + 3 < chunk.getMaxBlockX(),
|
||||
"The full 4x4 plinth must remain inside its source chunk");
|
||||
helper.assertTrue(site.z() >= chunk.getMinBlockZ() + 4
|
||||
&& site.z() + 3 < chunk.getMaxBlockZ(),
|
||||
"The full 4x4 plinth must remain inside its source chunk");
|
||||
}
|
||||
helper.assertTrue(selected > 0 && selected < 64,
|
||||
"The 1-in-128 roll should remain rare but reachable");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private static ResourceLocation id(String path) {
|
||||
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
}
|
||||
|
||||
private static WritingEntry page(String path) {
|
||||
return WritingEntry.symbol(new SymbolReference(id(path)));
|
||||
}
|
||||
|
||||
private static WritingEntry block(String path) {
|
||||
return WritingEntry.symbol(new SymbolReference(net.dnorberg.rivencraft.writing.BlockSymbolPages.symbolId(
|
||||
ResourceLocation.withDefaultNamespace(path))));
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ public final class WritingGrammar {
|
||||
} else if (role == Role.FEATURE && pendingBlock != null) {
|
||||
ResourceLocation feature = ((SymbolSemantics.ResourceValue) parsed.semantics().value()).id();
|
||||
if (!Set.of("rocks", "surface_lakes", "deep_lakes", "lava_lakes",
|
||||
"floating_islands", "tendrils", "spheres", "spikes")
|
||||
"floating_islands", "tendrils", "spheres", "spikes", "obelisks")
|
||||
.contains(feature.getPath()) || !feature.getNamespace().equals("rivencraft")) {
|
||||
diagnostics.add(new Diagnostic(Code.MISPLACED_MODIFIER, pendingBlock.index(),
|
||||
"This feature cannot consume a material page"));
|
||||
|
||||
@@ -285,6 +285,8 @@
|
||||
"symbol.rivencraft.feature.trail_ruins.description": "Allow buried trail ruins in compatible surface biomes.",
|
||||
"symbol.rivencraft.feature.dense_ores": "Dense Ores",
|
||||
"symbol.rivencraft.feature.dense_ores.description": "Repeat compatible vanilla and modded ore placements once per page in this Age.",
|
||||
"symbol.rivencraft.feature.obelisks": "Obelisks",
|
||||
"symbol.rivencraft.feature.obelisks.description": "Add rare tall obelisks on exposed solid ground, with a random or selected block material.",
|
||||
"symbol.rivencraft.feature.lava_lakes": "Small Lakes",
|
||||
"symbol.rivencraft.feature.lava_lakes.description": "Add small surface lakes using a random fluid, or specify one with a fluid page.",
|
||||
"generator.rivencraft.standard": "Standard",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.obelisks",
|
||||
"description": "symbol.rivencraft.feature.obelisks.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:obelisks" },
|
||||
"poem": "Civilization Structure Power Obelisk"
|
||||
}
|
||||
Reference in new issue
Block a user