Complete authored sky colors and eclipses (#44)
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-10-22 03:40:10 -04:00
1 parent 3ac4c499ab
commit 9d5ff2a2cb
62 files changed
+421 -118

No files matched your search

+12 -6
View File
@@ -124,14 +124,20 @@ the top or underside of one of its blocks. Insert a bound Linking or Descriptive
Book to activate it; the book remains visible on the receptacle. A Linking Book aimed at its Book to activate it; the book remains visible on the receptacle. A Linking Book aimed at its
own dimension still opens a visual portal, but travel requires Intra Linking. own dimension still opens a visual portal, but travel requires Intra Linking.
Sky writing includes eight color pages and a Static Sky page. Multiple sky-color Sky writing has shared Red, Green, Blue, White, and Black pages plus 25 D'ni
pages blend their colors, tinting the Age sky, book preview, and the fallback number pages (0–24). Put colors before a Sky, Sun, or Moon page to color that
surface of portals linked there. Multiple celestial-color pages blend for the target; trailing colors tint the sky. A number immediately before a color sets
next written sun or moon. An Age may have no suns or moons at all; its strength to number × 10. Without a number, a color gets a seeded random
Static Sky animates the sky with television-like noise. Sun and Moon pages add strength from 0–24. RGB contributions add in any order, White brightens all
channels, Black subtracts from all channels, and omitted channels remain zero.
The resulting sky color also tints book previews and fallback portal surfaces.
An Age may have no suns or moons at all; Static Sky animates with fine-grained
television-like noise. Sun and Moon pages add
independent bodies; preceding color, size, angle, period, phase, and direction independent bodies; preceding color, size, angle, period, phase, and direction
pages configure the next body. The third and each subsequent sun add increasing pages configure the next body. The third and each subsequent sun add increasing
instability. Newly generated page-free Ages also choose a deterministic sky, instability. Overlapping moon and sun discs produce an eclipse: the moon darkens,
the sun's color brightens at close alignment, and world and sky lighting dim.
Newly generated page-free Ages also choose a deterministic sky,
color, and celestial arrangement from their Age seed; older saved Ages retain color, and celestial arrangement from their Age seed; older saved Ages retain
their previous sky. their previous sky.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+1 -1
View File
@@ -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. # 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 mod_license=LGPL-3.0-or-later
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md. # The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
mod_version=0.6.8.7 mod_version=0.6.8.10
immersive_portals_version=6.0.7 immersive_portals_version=6.0.7
terrablender_version=4.1.0.8 terrablender_version=4.1.0.8
serene_seasons_version_id=pHEgQQUE serene_seasons_version_id=pHEgQQUE
@@ -108,11 +108,7 @@ public final class AgeEnvironmentCompiler {
/** An authored color is fixed; -1 means the biome or selected vanilla sky supplies it. */ /** An authored color is fixed; -1 means the biome or selected vanilla sky supplies it. */
public static int skyColor(AgeGenerationPlan plan) { public static int skyColor(AgeGenerationPlan plan) {
if (plan == null) return -1; return AgeSkyProfile.authoredSkyColor(plan);
return AgeSkyProfile.mixColors(plan.activeSymbols().stream()
.filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.SKY_COLOR)
.map(symbol -> ((SymbolSemantics.ChoiceValue) symbol.semantics().value()).id())
.map(AgeSkyProfile::color).toList());
} }
public static int selectedSkyColor(AgeGenerationPlan plan, ResourceLocation fallback, int biomeSkyColor) { public static int selectedSkyColor(AgeGenerationPlan plan, ResourceLocation fallback, int biomeSkyColor) {
@@ -188,7 +188,7 @@ public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writin
String raw = item.getString("Value"); String raw = item.getString("Value");
SymbolSemantics.Value value = switch (role) { SymbolSemantics.Value value = switch (role) {
case TERRAIN, BIOME, BLOCK, FEATURE -> new SymbolSemantics.ResourceValue(ResourceLocation.parse(raw)); case TERRAIN, BIOME, BLOCK, FEATURE -> new SymbolSemantics.ResourceValue(ResourceLocation.parse(raw));
case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER, SIZE_MODIFIER -> case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER, SIZE_MODIFIER, COLOR_NUMBER ->
new SymbolSemantics.NumberValue(Double.parseDouble(raw)); new SymbolSemantics.NumberValue(Double.parseDouble(raw));
default -> new SymbolSemantics.ChoiceValue(raw); default -> new SymbolSemantics.ChoiceValue(raw);
}; };
@@ -125,9 +125,9 @@ public final class AgePersistenceGameTests {
new AgeReference(UUID.randomUUID(), "Stable Writing"), stable); new AgeReference(UUID.randomUUID(), "Stable Writing"), stable);
AgeDefinition defectiveAge = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(), AgeDefinition defectiveAge = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
new AgeReference(UUID.randomUUID(), "Defective Writing"), defective); new AgeReference(UUID.randomUUID(), "Defective Writing"), defective);
helper.assertValueEqual(stableAge.instability().score(), 0, helper.assertValueEqual(nonCelestialInstability(stableAge), 0,
"Missing seeded branches do not add instability"); "Missing seeded branches do not add instability");
helper.assertValueEqual(defectiveAge.instability().score(), 600, helper.assertValueEqual(nonCelestialInstability(defectiveAge), 600,
"Out-of-order and repeated terrain pages each add their saved penalty"); "Out-of-order and repeated terrain pages each add their saved penalty");
helper.assertTrue(defectiveAge.instability().breakdown().stream() helper.assertTrue(defectiveAge.instability().breakdown().stream()
.anyMatch(part -> part.reason().equals("duplicate") && part.pageIndex() == 3), .anyMatch(part -> part.reason().equals("duplicate") && part.pageIndex() == 3),
@@ -184,9 +184,9 @@ public final class AgePersistenceGameTests {
new AgeReference(UUID.randomUUID(), "Three " + structure), structureWriting(structure, 3)); new AgeReference(UUID.randomUUID(), "Three " + structure), structureWriting(structure, 3));
AgeDefinition four = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(), AgeDefinition four = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
new AgeReference(UUID.randomUUID(), "Four " + structure), structureWriting(structure, 4)); new AgeReference(UUID.randomUUID(), "Four " + structure), structureWriting(structure, 4));
helper.assertValueEqual(three.instability().score(), 0, helper.assertValueEqual(nonCelestialInstability(three), 0,
"The first three " + structure + " pages retain Legacy's zero cost"); "The first three " + structure + " pages retain Legacy's zero cost");
helper.assertValueEqual(four.instability().score(), 100, helper.assertValueEqual(nonCelestialInstability(four), 100,
"The fourth " + structure + " page must add Legacy's 100-point cost"); "The fourth " + structure + " page must add Legacy's 100-point cost");
helper.assertTrue(four.instability().breakdown().stream().anyMatch(part -> helper.assertTrue(four.instability().breakdown().stream().anyMatch(part ->
part.reason().equals("excess_" + structure) && part.pageIndex() == 8), part.reason().equals("excess_" + structure) && part.pageIndex() == 8),
@@ -216,7 +216,7 @@ public final class AgePersistenceGameTests {
)); ));
AgeDefinition bright = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(), AgeDefinition bright = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
new AgeReference(UUID.randomUUID(), "Double Bright"), duplicateBright); new AgeReference(UUID.randomUUID(), "Double Bright"), duplicateBright);
helper.assertValueEqual(bright.instability().score(), 1500, helper.assertValueEqual(nonCelestialInstability(bright), 1500,
"Each Bright Lighting page costs 500 and the extra controller costs another 500"); "Each Bright Lighting page costs 500 and the extra controller costs another 500");
helper.assertValueEqual(bright.instability().breakdown().stream() helper.assertValueEqual(bright.instability().breakdown().stream()
.filter(part -> part.reason().equals("bright_lighting")).count(), 2L, .filter(part -> part.reason().equals("bright_lighting")).count(), 2L,
@@ -420,6 +420,13 @@ public final class AgePersistenceGameTests {
helper.succeed(); helper.succeed();
} }
/** Isolate the writing-cost assertion from seed-completed extra-sun instability. */
private static int nonCelestialInstability(AgeDefinition age) {
return age.instability().breakdown().stream()
.filter(part -> !part.reason().equals("extra_sun"))
.mapToInt(AgeInstability.Contribution::points).sum();
}
private static CompoundTag entry( private static CompoundTag entry(
UUID id, UUID id,
String name, String name,
@@ -48,7 +48,7 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
public static AgeSkyProfile compile(AgeGenerationPlan plan, AgeTraits traits) { public static AgeSkyProfile compile(AgeGenerationPlan plan, AgeTraits traits) {
if (plan == null) return new AgeSkyProfile(Style.INHERITED, -1, List.of()); if (plan == null) return new AgeSkyProfile(Style.INHERITED, -1, List.of());
Style style = Style.INHERITED; Style style = Style.INHERITED;
int color = AgeEnvironmentCompiler.skyColor(plan); int color = authoredSkyColor(plan);
boolean customBodies = hasCustomCelestials(plan); boolean customBodies = hasCustomCelestials(plan);
List<Body> bodies = new ArrayList<>(); List<Body> bodies = new ArrayList<>();
Set<Integer> active = new HashSet<>(plan.activeSymbolIndices()); Set<Integer> active = new HashSet<>(plan.activeSymbolIndices());
@@ -58,18 +58,32 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
float phase = seeded.celestialPhase(); float phase = seeded.celestialPhase();
float size = 1.0F; float size = 1.0F;
List<Integer> bodyColors = new ArrayList<>(); List<Integer> bodyColors = new ArrayList<>();
ColorAccumulator pendingColors = new ColorAccumulator();
Integer colorMultiplier = null;
boolean clockwise = true; boolean clockwise = true;
for (AgeGenerationPlan.ResolvedSymbol symbol : plan.symbols()) { for (AgeGenerationPlan.ResolvedSymbol symbol : plan.symbols()) {
if (!active.contains(symbol.index())) continue; if (!active.contains(symbol.index())) continue;
SymbolSemantics semantics = symbol.semantics(); SymbolSemantics semantics = symbol.semantics();
if (semantics.role() != SymbolSemantics.Role.COLOR
&& semantics.role() != SymbolSemantics.Role.COLOR_NUMBER) colorMultiplier = null;
switch (semantics.role()) { switch (semantics.role()) {
case SKY -> style = switch (((SymbolSemantics.ChoiceValue) semantics.value()).id()) { case SKY -> {
pendingColors.clear();
style = switch (((SymbolSemantics.ChoiceValue) semantics.value()).id()) {
case "overworld" -> Style.OVERWORLD; case "overworld" -> Style.OVERWORLD;
case "nether" -> Style.NETHER; case "nether" -> Style.NETHER;
case "end" -> Style.END; case "end" -> Style.END;
case "static" -> Style.STATIC; case "static" -> Style.STATIC;
default -> throw new IllegalArgumentException("Unknown sky style"); default -> throw new IllegalArgumentException("Unknown sky style");
}; };
}
case COLOR_NUMBER -> colorMultiplier = (int) ((SymbolSemantics.NumberValue)
semantics.value()).amount();
case COLOR -> {
pendingColors.add(((SymbolSemantics.ChoiceValue) semantics.value()).id(),
colorMultiplier == null ? randomColorNumber(plan.ageSeed(), symbol.index()) : colorMultiplier);
colorMultiplier = null;
}
case PERIOD_MODIFIER -> period = ((SymbolSemantics.NumberValue) semantics.value()).amount(); case PERIOD_MODIFIER -> period = ((SymbolSemantics.NumberValue) semantics.value()).amount();
case ANGLE_MODIFIER -> angle = (float) ((SymbolSemantics.NumberValue) semantics.value()).amount(); case ANGLE_MODIFIER -> angle = (float) ((SymbolSemantics.NumberValue) semantics.value()).amount();
case PHASE_MODIFIER -> phase = (float) (((SymbolSemantics.NumberValue) semantics.value()).amount() / 360.0D); case PHASE_MODIFIER -> phase = (float) (((SymbolSemantics.NumberValue) semantics.value()).amount() / 360.0D);
@@ -80,7 +94,8 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
.id().equals("clockwise"); .id().equals("clockwise");
case CELESTIAL -> { case CELESTIAL -> {
String choice = ((SymbolSemantics.ChoiceValue) semantics.value()).id(); String choice = ((SymbolSemantics.ChoiceValue) semantics.value()).id();
int bodyColor = bodyColors.isEmpty() ? 0xFFFFFF : mixColors(bodyColors); int bodyColor = pendingColors.written() ? pendingColors.rgb()
: bodyColors.isEmpty() ? 0 : mixColors(bodyColors);
if (choice.equals("normal_time")) { if (choice.equals("normal_time")) {
if (customBodies) { if (customBodies) {
bodies.add(new Body(Kind.SUN, bodyColor, size, angle, period, phase, clockwise)); bodies.add(new Body(Kind.SUN, bodyColor, size, angle, period, phase, clockwise));
@@ -96,6 +111,7 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
phase = seeded.celestialPhase(); phase = seeded.celestialPhase();
size = 1.0F; size = 1.0F;
bodyColors.clear(); bodyColors.clear();
pendingColors.clear();
clockwise = true; clockwise = true;
} }
default -> { } default -> { }
@@ -124,6 +140,74 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
| (int) ((blue + count / 2) / count); | (int) ((blue + count / 2) / count);
} }
/** Shared color pages affect the next Sky or celestial page; trailing colors tint the sky. */
public static int authoredSkyColor(AgeGenerationPlan plan) {
if (plan == null) return -1;
ColorAccumulator pending = new ColorAccumulator();
Integer multiplier = null;
int skyColor = -1;
List<Integer> legacyColors = new ArrayList<>();
Set<Integer> active = new HashSet<>(plan.activeSymbolIndices());
for (AgeGenerationPlan.ResolvedSymbol symbol : plan.symbols()) {
if (!active.contains(symbol.index())) continue;
SymbolSemantics semantics = symbol.semantics();
if (semantics.role() != SymbolSemantics.Role.COLOR
&& semantics.role() != SymbolSemantics.Role.COLOR_NUMBER) multiplier = null;
switch (semantics.role()) {
case COLOR_NUMBER -> multiplier = (int) ((SymbolSemantics.NumberValue) semantics.value()).amount();
case COLOR -> {
pending.add(((SymbolSemantics.ChoiceValue) semantics.value()).id(),
multiplier == null ? randomColorNumber(plan.ageSeed(), symbol.index()) : multiplier);
multiplier = null;
}
case SKY -> {
if (pending.written()) skyColor = pending.rgb();
pending.clear();
}
case CELESTIAL -> pending.clear();
case SKY_COLOR -> legacyColors.add(color(
((SymbolSemantics.ChoiceValue) semantics.value()).id()));
default -> { }
}
}
if (pending.written()) skyColor = pending.rgb();
return skyColor != -1 ? skyColor : mixColors(legacyColors);
}
private static int randomColorNumber(long ageSeed, int pageIndex) {
return new Random(ageSeed ^ (0x9E3779B97F4A7C15L * (pageIndex + 1L))).nextInt(25);
}
private static final class ColorAccumulator {
private int red;
private int green;
private int blue;
private boolean written;
void add(String color, int multiplier) {
int amount = multiplier * 10;
written = true;
switch (color) {
case "red" -> red += amount;
case "green" -> green += amount;
case "blue" -> blue += amount;
case "white" -> { red += amount; green += amount; blue += amount; }
case "black" -> { red -= amount; green -= amount; blue -= amount; }
default -> throw new IllegalArgumentException("Unknown shared color " + color);
}
}
boolean written() { return written; }
int rgb() {
return clamp(red) << 16 | clamp(green) << 8 | clamp(blue);
}
void clear() { red = green = blue = 0; written = false; }
private static int clamp(int channel) { return Math.max(0, Math.min(255, channel)); }
}
private static int randomColor(Random random) { private static int randomColor(Random random) {
int count = 1 + random.nextInt(3); int count = 1 + random.nextInt(3);
List<Integer> colors = new ArrayList<>(count); List<Integer> colors = new ArrayList<>(count);
@@ -132,12 +216,30 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
} }
public static boolean hasCustomCelestials(AgeGenerationPlan plan) { public static boolean hasCustomCelestials(AgeGenerationPlan plan) {
return plan != null && plan.activeSymbols().stream().anyMatch(symbol -> return plan != null && (hasColorForCelestial(plan) || plan.activeSymbols().stream().anyMatch(symbol ->
symbol.semantics().role() == SymbolSemantics.Role.CELESTIAL symbol.semantics().role() == SymbolSemantics.Role.CELESTIAL
&& !((SymbolSemantics.ChoiceValue) symbol.semantics().value()).id().equals("normal_time") && !((SymbolSemantics.ChoiceValue) symbol.semantics().value()).id().equals("normal_time")
|| symbol.semantics().role() == SymbolSemantics.Role.CELESTIAL_COLOR || symbol.semantics().role() == SymbolSemantics.Role.CELESTIAL_COLOR
|| symbol.semantics().role() == SymbolSemantics.Role.SIZE_MODIFIER || symbol.semantics().role() == SymbolSemantics.Role.SIZE_MODIFIER
|| symbol.semantics().role() == SymbolSemantics.Role.DIRECTION_MODIFIER); || symbol.semantics().role() == SymbolSemantics.Role.DIRECTION_MODIFIER));
}
private static boolean hasColorForCelestial(AgeGenerationPlan plan) {
Set<Integer> active = new HashSet<>(plan.activeSymbolIndices());
boolean pending = false;
for (AgeGenerationPlan.ResolvedSymbol symbol : plan.symbols()) {
if (!active.contains(symbol.index())) continue;
switch (symbol.semantics().role()) {
case COLOR -> pending = true;
case SKY -> pending = false;
case CELESTIAL -> {
if (pending) return true;
pending = false;
}
default -> { }
}
}
return false;
} }
public boolean custom() { public boolean custom() {
@@ -154,12 +256,53 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
/ (24000.0D * body.period()) + body.phase(); / (24000.0D * body.period()) + body.phase();
float altitude = (float) Math.cos(orbit * Math.PI * 2.0D); float altitude = (float) Math.cos(orbit * Math.PI * 2.0D);
if (altitude <= 0.0F) continue; if (altitude <= 0.0F) continue;
if (body.kind() == Kind.SUN) sunlight += altitude; if (body.kind() == Kind.SUN) sunlight += altitude * (1.0F - 0.85F * eclipseFor(body, dayTime));
else moonlight += altitude * 0.18F; else moonlight += altitude * 0.18F;
} }
return Math.min(1.0F, sunlight > 0.0F ? sunlight : moonlight); return Math.min(1.0F, sunlight > 0.0F ? sunlight : moonlight);
} }
/** Disc overlap in the rendered sky, from first contact (0) to centered alignment (1). */
public float eclipseFor(Body body, double dayTime) {
float strongest = 0.0F;
net.minecraft.world.phys.Vec3 bodyDirection = direction(body, dayTime);
for (Body other : bodies) {
if (other.kind() == body.kind()) continue;
double dot = Math.max(-1.0D, Math.min(1.0D,
bodyDirection.dot(direction(other, dayTime))));
double separation = Math.acos(dot);
// The outer 32x32 quad is only glow. The opaque body occupies its
// central 8x8 pixels, so contact must use one quarter of its size.
double radius = Math.atan((body.kind() == Kind.SUN ? 30.0D : 20.0D)
* body.size() * 0.25D / 100.0D)
+ Math.atan((other.kind() == Kind.SUN ? 30.0D : 20.0D)
* other.size() * 0.25D / 100.0D);
if (radius > 0.0D) strongest = Math.max(strongest,
(float) Math.max(0.0D, 1.0D - separation / radius));
}
return strongest;
}
public float eclipseAt(double dayTime) {
float strongest = 0.0F;
for (Body body : bodies) {
if (body.kind() == Kind.SUN && direction(body, dayTime).y > 0.0D)
strongest = Math.max(strongest, eclipseFor(body, dayTime));
}
return strongest;
}
private static net.minecraft.world.phys.Vec3 direction(Body body, double dayTime) {
double orbit = body.period() == 0.0D ? body.phase()
: (body.clockwise() ? 1.0D : -1.0D) * dayTime
/ (24000.0D * body.period()) + body.phase();
double tilt = orbit * Math.PI * 2.0D;
double bearing = Math.toRadians(body.angle());
double lateral = Math.sin(tilt);
return new net.minecraft.world.phys.Vec3(
Math.sin(bearing) * lateral, Math.cos(tilt), Math.cos(bearing) * lateral);
}
public static int color(String name) { public static int color(String name) {
return switch (name) { return switch (name) {
case "red" -> COLORS[0]; case "red" -> COLORS[0];
@@ -52,10 +52,11 @@ public final class EnvironmentWritingGameTests {
public static void environmentPagesEnterCreativeAndJeiFeed(GameTestHelper helper) { public static void environmentPagesEnterCreativeAndJeiFeed(GameTestHelper helper) {
Set<String> expected = Set.of( Set<String> expected = Set.of(
"lighting/normal", "lighting/dark", "lighting/bright", "lighting/normal", "lighting/dark", "lighting/bright",
"sky/overworld", "sky/nether", "sky/end", "sky/static", "sky_color/purple", "sky/overworld", "sky/nether", "sky/end", "sky/static", "color/red", "color/green",
"color/blue", "color/black", "color/white", "number/0", "number/24",
"weather/normal", "weather/fast", "weather/slow", "weather/off", "weather/normal", "weather/fast", "weather/slow", "weather/off",
"weather/always", "weather/storm", "celestial/normal_time", "celestial/sun", "weather/always", "weather/storm", "celestial/normal_time", "celestial/sun",
"celestial/moon", "celestial_color/red", "size/large", "angle/east", "celestial/moon", "size/large", "angle/east",
"direction/clockwise", "direction/counterclockwise", "direction/clockwise", "direction/counterclockwise",
"season/no_seasons", "season/seasons", "season/eternal_winter", "season/no_seasons", "season/seasons", "season/eternal_winter",
"season/eternal_summer", "season/eternal_fall", "season/eternal_spring", "season/eternal_summer", "season/eternal_fall", "season/eternal_spring",
@@ -69,6 +70,9 @@ public final class EnvironmentWritingGameTests {
helper.assertTrue(PageItem.bundledCreativeSymbols().containsAll(expected) helper.assertTrue(PageItem.bundledCreativeSymbols().containsAll(expected)
&& creative.containsAll(expected), && creative.containsAll(expected),
"Every supported environment page must enter the creative index JEI also consumes"); "Every supported environment page must enter the creative index JEI also consumes");
helper.assertValueEqual(PageItem.bundledCreativeSymbols().stream()
.filter(path -> path.startsWith("number/")).count(), 25L,
"Creative and JEI must expose all 25 D'ni number pages");
for (String path : expected) { for (String path : expected) {
var id = ResourceLocation.fromNamespaceAndPath("rivencraft", path); var id = ResourceLocation.fromNamespaceAndPath("rivencraft", path);
helper.assertTrue(SymbolCatalog.isKnown(new SymbolReference(id)) helper.assertTrue(SymbolCatalog.isKnown(new SymbolReference(id))
@@ -140,8 +144,8 @@ public final class EnvironmentWritingGameTests {
@GameTest(template = "empty") @GameTest(template = "empty")
public static void authoredStaticSkyColorSurvivesWritingAndFeedsPreview(GameTestHelper helper) { public static void authoredStaticSkyColorSurvivesWritingAndFeedsPreview(GameTestHelper helper) {
OrderedWriting writing = new OrderedWriting(List.of( OrderedWriting writing = new OrderedWriting(List.of(
WritingEntry.linkPanel(), page("terrain/standard"), page("sky/static"), WritingEntry.linkPanel(), page("terrain/standard"), page("number/20"),
page("sky_color/purple"), page("celestial/normal_time"))); page("color/blue"), page("sky/static"), page("celestial/normal_time")));
AgeTraits seeded = AgeTraits.select(44L); AgeTraits seeded = AgeTraits.select(44L);
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 44L, AgeGenerationProfiles.STANDARD, AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 44L, AgeGenerationProfiles.STANDARD,
seeded, helper.getLevel().registryAccess()); seeded, helper.getLevel().registryAccess());
@@ -152,15 +156,15 @@ public final class EnvironmentWritingGameTests {
"The static sky must select Rivencraft's custom renderer"); "The static sky must select Rivencraft's custom renderer");
helper.assertValueEqual(AgeEnvironmentCompiler.selectedSkyColor(plan, helper.assertValueEqual(AgeEnvironmentCompiler.selectedSkyColor(plan,
BuiltinDimensionTypes.OVERWORLD_EFFECTS, 0xFF77AAFF), BuiltinDimensionTypes.OVERWORLD_EFFECTS, 0xFF77AAFF),
0x9870B9, "The authored color must tint the sky and preview consistently"); 0x0000C8, "The authored color must tint the sky and preview consistently");
AgeGenerationPlan restored = AgeGenerationPlan.load(plan.save()); AgeGenerationPlan restored = AgeGenerationPlan.load(plan.save());
helper.assertTrue(AgeEnvironmentCompiler.staticSky(restored), helper.assertTrue(AgeEnvironmentCompiler.staticSky(restored),
"A saved Age must retain its static sky choice"); "A saved Age must retain its static sky choice");
helper.assertValueEqual(AgeEnvironmentCompiler.skyColor(restored), 0x9870B9, helper.assertValueEqual(AgeEnvironmentCompiler.skyColor(restored), 0x0000C8,
"A saved Age must retain its sky color choice"); "A saved Age must retain its sky color choice");
AgeGenerationPlan coloredEnd = AgeGenerationPlan.compile(new OrderedWriting(List.of( AgeGenerationPlan coloredEnd = AgeGenerationPlan.compile(new OrderedWriting(List.of(
WritingEntry.linkPanel(), page("terrain/standard"), page("sky/end"), WritingEntry.linkPanel(), page("terrain/standard"), page("number/15"),
page("sky_color/red"), page("celestial/normal_time"))), page("color/red"), page("sky/end"), page("celestial/normal_time"))),
45L, AgeGenerationProfiles.STANDARD, AgeTraits.select(45L), 45L, AgeGenerationProfiles.STANDARD, AgeTraits.select(45L),
helper.getLevel().registryAccess()); helper.getLevel().registryAccess());
helper.assertValueEqual(AgeEnvironmentCompiler.skyEffects(coloredEnd, helper.assertValueEqual(AgeEnvironmentCompiler.skyEffects(coloredEnd,
@@ -169,9 +173,10 @@ public final class EnvironmentWritingGameTests {
"A color page must make non-Overworld skies renderable by the custom sky effect"); "A color page must make non-Overworld skies renderable by the custom sky effect");
helper.assertValueEqual(AgeEnvironmentCompiler.selectedSkyColor(coloredEnd, helper.assertValueEqual(AgeEnvironmentCompiler.selectedSkyColor(coloredEnd,
BuiltinDimensionTypes.OVERWORLD_EFFECTS, 0xFF77AAFF), BuiltinDimensionTypes.OVERWORLD_EFFECTS, 0xFF77AAFF),
0xB74A50, "A color page must override the selected sky type's default tint"); 0x960000, "A color page must override the selected sky type's default tint");
helper.assertTrue(PageItem.bundledCreativeSymbols().contains("sky/static") helper.assertTrue(PageItem.bundledCreativeSymbols().contains("sky/static")
&& PageItem.bundledCreativeSymbols().contains("sky_color/purple"), && PageItem.bundledCreativeSymbols().contains("color/blue")
&& PageItem.bundledCreativeSymbols().contains("number/24"),
"New sky pages must be available through the Creative and JEI page catalog"); "New sky pages must be available through the Creative and JEI page catalog");
helper.succeed(); helper.succeed();
} }
@@ -179,18 +184,17 @@ public final class EnvironmentWritingGameTests {
@GameTest(template = "empty") @GameTest(template = "empty")
public static void skyAndCelestialColorsBlendWithoutRequiringBodies(GameTestHelper helper) { public static void skyAndCelestialColorsBlendWithoutRequiringBodies(GameTestHelper helper) {
OrderedWriting writing = new OrderedWriting(List.of( OrderedWriting writing = new OrderedWriting(List.of(
WritingEntry.linkPanel(), page("terrain/standard"), page("sky/overworld"), WritingEntry.linkPanel(), page("terrain/standard"),
page("sky_color/red"), page("sky_color/blue"), page("number/10"), page("color/red"), page("number/5"), page("color/blue"),
page("celestial_color/yellow"), page("celestial_color/blue"), page("celestial/sun"))); page("sky/overworld"), page("number/12"), page("color/green"),
page("number/7"), page("color/blue"), page("celestial/sun")));
helper.assertTrue(WritingGrammar.parse(writing).canCreate(), helper.assertTrue(WritingGrammar.parse(writing).canCreate(),
"Two sky colors and two colors for one sun must be valid writing"); "Two sky colors and two colors for one sun must be valid writing");
AgeTraits traits = AgeTraits.select(46L); AgeTraits traits = AgeTraits.select(46L);
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 46L, AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 46L,
AgeGenerationProfiles.STANDARD, traits, helper.getLevel().registryAccess()); AgeGenerationProfiles.STANDARD, traits, helper.getLevel().registryAccess());
int skyColor = AgeSkyProfile.mixColors(List.of( int skyColor = 0x640032;
AgeSkyProfile.color("red"), AgeSkyProfile.color("blue"))); int sunColor = 0x007846;
int sunColor = AgeSkyProfile.mixColors(List.of(
AgeSkyProfile.color("yellow"), AgeSkyProfile.color("blue")));
helper.assertValueEqual(AgeEnvironmentCompiler.skyColor(plan), skyColor, helper.assertValueEqual(AgeEnvironmentCompiler.skyColor(plan), skyColor,
"Sky color pages must blend without discarding either page"); "Sky color pages must blend without discarding either page");
AgeSkyProfile profile = AgeSkyProfile.compile(plan, traits); AgeSkyProfile profile = AgeSkyProfile.compile(plan, traits);
@@ -206,6 +210,58 @@ public final class EnvironmentWritingGameTests {
helper.succeed(); helper.succeed();
} }
@GameTest(template = "empty")
public static void colorNumbersUseDniDigitsAndMissingChannelsRemainZero(GameTestHelper helper) {
OrderedWriting writing = new OrderedWriting(List.of(
WritingEntry.linkPanel(), page("number/12"), page("color/red"),
page("number/5"), page("color/white"), page("number/3"), page("color/black"),
page("sky/overworld")));
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 49L,
AgeGenerationProfiles.STANDARD, AgeTraits.select(49L), helper.getLevel().registryAccess());
helper.assertValueEqual(AgeEnvironmentCompiler.skyColor(plan), 0x8C1414,
"White adds and Black subtracts from every channel; missing green and blue start at zero");
helper.assertValueEqual(AgeEnvironmentCompiler.skyColor(AgeGenerationPlan.load(plan.save())), 0x8C1414,
"D'ni color multipliers must remain frozen in a saved Age plan");
OrderedWriting unnumbered = new OrderedWriting(List.of(
WritingEntry.linkPanel(), page("color/green"), page("sky/overworld")));
AgeGenerationPlan randomPlan = AgeGenerationPlan.compile(unnumbered, 50L,
AgeGenerationProfiles.STANDARD, AgeTraits.select(50L), helper.getLevel().registryAccess());
int color = AgeEnvironmentCompiler.skyColor(randomPlan);
helper.assertTrue((color & 0xFF00FF) == 0 && ((color >> 8) & 255) <= 240
&& ((color >> 8) & 255) % 10 == 0,
"An unnumbered color gets one seeded base-25 digit and leaves omitted channels at zero");
helper.assertValueEqual(AgeEnvironmentCompiler.skyColor(AgeGenerationPlan.load(randomPlan.save())), color,
"A random color digit must not change when an Age is reloaded");
helper.succeed();
}
@GameTest(template = "empty")
public static void alignedMoonEclipsesSunAndDimsLight(GameTestHelper helper) {
AgeSkyProfile.Body sun = new AgeSkyProfile.Body(AgeSkyProfile.Kind.SUN,
0xCC8844, 1.0F, 0.0F, 1.0D, 0.0F, true);
AgeSkyProfile.Body moon = new AgeSkyProfile.Body(AgeSkyProfile.Kind.MOON,
0xFFFFFF, 1.0F, 0.0F, 1.0D, 0.0F, true);
AgeSkyProfile clear = new AgeSkyProfile(AgeSkyProfile.Style.OVERWORLD, 0x6688AA, List.of(sun));
AgeSkyProfile eclipse = new AgeSkyProfile(AgeSkyProfile.Style.OVERWORLD, 0x6688AA,
List.of(sun, moon));
helper.assertValueEqual(eclipse.eclipseAt(0.0D), 1.0F,
"Centered sun and moon must produce a full eclipse");
helper.assertTrue(eclipse.lightAt(0.0D) < clear.lightAt(0.0D),
"An eclipse must dim world skylight, not just the rendered sprites");
AgeSkyProfile separated = new AgeSkyProfile(AgeSkyProfile.Style.OVERWORLD, 0x6688AA,
List.of(sun, new AgeSkyProfile.Body(AgeSkyProfile.Kind.MOON,
0xFFFFFF, 1.0F, 0.0F, 1.0D, 0.5F, true)));
helper.assertValueEqual(separated.eclipseAt(0.0D), 0.0F,
"A moon on the far side of the sky must not eclipse the sun");
AgeSkyProfile glowOnlyOverlap = new AgeSkyProfile(AgeSkyProfile.Style.OVERWORLD, 0x6688AA,
List.of(sun, new AgeSkyProfile.Body(AgeSkyProfile.Kind.MOON,
0xFFFFFF, 1.0F, 0.0F, 1.0D, 0.04F, true)));
helper.assertValueEqual(glowOnlyOverlap.eclipseAt(0.0D), 0.0F,
"Overlapping glows must not count as an eclipse before the opaque discs touch");
helper.succeed();
}
@GameTest(template = "empty") @GameTest(template = "empty")
public static void randomAndAuthoredCelestialsKeepIndependentOrbits(GameTestHelper helper) { public static void randomAndAuthoredCelestialsKeepIndependentOrbits(GameTestHelper helper) {
AgeTraits traits = AgeTraits.select(12345L); AgeTraits traits = AgeTraits.select(12345L);
@@ -217,7 +273,7 @@ public final class EnvironmentWritingGameTests {
"Random Ages must obey the same celestial limits as authored Ages"); "Random Ages must obey the same celestial limits as authored Ages");
OrderedWriting writing = new OrderedWriting(List.of( OrderedWriting writing = new OrderedWriting(List.of(
WritingEntry.linkPanel(), page("terrain/standard"), page("sky/end"), WritingEntry.linkPanel(), page("terrain/standard"), page("sky/end"),
page("angle/east"), page("direction/counterclockwise"), page("celestial_color/red"), page("angle/east"), page("direction/counterclockwise"), page("number/24"), page("color/red"),
page("size/large"), page("period/half"), page("celestial/sun"), page("size/large"), page("period/half"), page("celestial/sun"),
page("direction/clockwise"), page("celestial/moon"))); page("direction/clockwise"), page("celestial/moon")));
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 12345L, AgeGenerationProfiles.STANDARD, AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 12345L, AgeGenerationProfiles.STANDARD,
@@ -1,6 +1,7 @@
package net.dnorberg.rivencraft.client; package net.dnorberg.rivencraft.client;
import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.vertex.BufferBuilder; import com.mojang.blaze3d.vertex.BufferBuilder;
import com.mojang.blaze3d.vertex.BufferUploader; import com.mojang.blaze3d.vertex.BufferUploader;
import com.mojang.blaze3d.vertex.DefaultVertexFormat; import com.mojang.blaze3d.vertex.DefaultVertexFormat;
@@ -22,6 +23,7 @@ import net.minecraft.resources.ResourceLocation;
/** An authored sky can use vanilla celestial rendering or Legacy-style moving TV static. */ /** An authored sky can use vanilla celestial rendering or Legacy-style moving TV static. */
public final class RivencraftSkyEffects extends DimensionSpecialEffects { public final class RivencraftSkyEffects extends DimensionSpecialEffects {
private static final int GRID = 16; private static final int GRID = 16;
private static final int STATIC_GRID = 48;
private static final float RADIUS = 100.0F; private static final float RADIUS = 100.0F;
private static final ResourceLocation SUN = ResourceLocation.withDefaultNamespace("textures/environment/sun.png"); private static final ResourceLocation SUN = ResourceLocation.withDefaultNamespace("textures/environment/sun.png");
private static final ResourceLocation MOON = ResourceLocation.withDefaultNamespace("textures/environment/moon_phases.png"); private static final ResourceLocation MOON = ResourceLocation.withDefaultNamespace("textures/environment/moon_phases.png");
@@ -62,6 +64,8 @@ public final class RivencraftSkyEffects extends DimensionSpecialEffects {
case STATIC -> 0x929292; case STATIC -> 0x929292;
default -> level.getBiome(camera.getBlockPosition()).value().getSkyColor(); default -> level.getBiome(camera.getBlockPosition()).value().getSkyColor();
}; };
double time = level.getDayTime() + partialTick;
float eclipseShade = 1.0F - 0.65F * sky.eclipseAt(time);
RenderSystem.depthMask(false); RenderSystem.depthMask(false);
RenderSystem.disableCull(); RenderSystem.disableCull();
@@ -70,20 +74,21 @@ public final class RivencraftSkyEffects extends DimensionSpecialEffects {
FogRenderer.setupNoFog(); FogRenderer.setupNoFog();
BufferBuilder vertices = Tesselator.getInstance().begin( BufferBuilder vertices = Tesselator.getInstance().begin(
VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
int grid = sky.style() == AgeSkyProfile.Style.STATIC ? STATIC_GRID : GRID;
for (int axis = 0; axis < 3; axis++) { for (int axis = 0; axis < 3; axis++) {
for (int sign : new int[]{-1, 1}) { for (int sign : new int[]{-1, 1}) {
for (int x = 0; x < GRID; x++) { for (int x = 0; x < grid; x++) {
for (int y = 0; y < GRID; y++) { for (int y = 0; y < grid; y++) {
int noise = sky.style() == AgeSkyProfile.Style.STATIC int noise = sky.style() == AgeSkyProfile.Style.STATIC
? 65 + random.nextInt(160) : 255; ? 65 + random.nextInt(160) : 255;
int red = ((tint >> 16) & 255) * noise / 255; int red = (int) (((tint >> 16) & 255) * noise / 255.0F * eclipseShade);
int green = ((tint >> 8) & 255) * noise / 255; int green = (int) (((tint >> 8) & 255) * noise / 255.0F * eclipseShade);
int blue = (tint & 255) * noise / 255; int blue = (int) ((tint & 255) * noise / 255.0F * eclipseShade);
int color = 0xFF000000 | red << 16 | green << 8 | blue; int color = 0xFF000000 | red << 16 | green << 8 | blue;
float left = -RADIUS + x * (2 * RADIUS / GRID); float left = -RADIUS + x * (2 * RADIUS / grid);
float right = -RADIUS + (x + 1) * (2 * RADIUS / GRID); float right = -RADIUS + (x + 1) * (2 * RADIUS / grid);
float bottom = -RADIUS + y * (2 * RADIUS / GRID); float bottom = -RADIUS + y * (2 * RADIUS / grid);
float top = -RADIUS + (y + 1) * (2 * RADIUS / GRID); float top = -RADIUS + (y + 1) * (2 * RADIUS / grid);
vertex(vertices, matrix, axis, sign, left, bottom, color); vertex(vertices, matrix, axis, sign, left, bottom, color);
vertex(vertices, matrix, axis, sign, right, bottom, color); vertex(vertices, matrix, axis, sign, right, bottom, color);
vertex(vertices, matrix, axis, sign, right, top, color); vertex(vertices, matrix, axis, sign, right, top, color);
@@ -96,8 +101,11 @@ public final class RivencraftSkyEffects extends DimensionSpecialEffects {
if (!sky.bodies().isEmpty()) { if (!sky.bodies().isEmpty()) {
RenderSystem.enableBlend(); RenderSystem.enableBlend();
RenderSystem.defaultBlendFunc(); RenderSystem.defaultBlendFunc();
double time = level.getGameTime() + partialTick; // Draw suns first so an aligned moon can pass visibly in front of them.
for (AgeSkyProfile.Body body : sky.bodies()) drawBody(pose, body, time); for (AgeSkyProfile.Body body : sky.bodies())
if (body.kind() == AgeSkyProfile.Kind.SUN) drawBody(pose, body, time, sky.eclipseFor(body, time));
for (AgeSkyProfile.Body body : sky.bodies())
if (body.kind() == AgeSkyProfile.Kind.MOON) drawBody(pose, body, time, sky.eclipseFor(body, time));
RenderSystem.disableBlend(); RenderSystem.disableBlend();
} }
setupFog.run(); setupFog.run();
@@ -106,7 +114,7 @@ public final class RivencraftSkyEffects extends DimensionSpecialEffects {
return true; return true;
} }
private static void drawBody(PoseStack pose, AgeSkyProfile.Body body, double time) { private static void drawBody(PoseStack pose, AgeSkyProfile.Body body, double time, float eclipse) {
pose.pushPose(); pose.pushPose();
pose.mulPose(Axis.YP.rotationDegrees(body.angle())); pose.mulPose(Axis.YP.rotationDegrees(body.angle()));
double orbit = body.period() == 0 ? body.phase() double orbit = body.period() == 0 ? body.phase()
@@ -115,24 +123,52 @@ public final class RivencraftSkyEffects extends DimensionSpecialEffects {
pose.mulPose(Axis.XP.rotationDegrees((float) (orbit * 360.0D))); pose.mulPose(Axis.XP.rotationDegrees((float) (orbit * 360.0D)));
float size = (body.kind() == AgeSkyProfile.Kind.SUN ? 30.0F : 20.0F) * body.size(); float size = (body.kind() == AgeSkyProfile.Kind.SUN ? 30.0F : 20.0F) * body.size();
int color = body.color(); int color = body.color();
float red = ((color >> 16) & 255) / 255.0F;
float green = ((color >> 8) & 255) / 255.0F;
float blue = (color & 255) / 255.0F;
if (body.kind() == AgeSkyProfile.Kind.MOON) {
red *= 1.0F - eclipse;
green *= 1.0F - eclipse;
blue *= 1.0F - eclipse;
} else {
float corona = (float) Math.pow(eclipse, 8.0D) * 0.8F;
red += (1.0F - red) * corona;
green += (1.0F - green) * corona;
blue += (1.0F - blue) * corona;
}
RenderSystem.setShader(GameRenderer::getPositionTexShader); RenderSystem.setShader(GameRenderer::getPositionTexShader);
RenderSystem.setShaderTexture(0, body.kind() == AgeSkyProfile.Kind.SUN ? SUN : MOON); RenderSystem.setShaderTexture(0, body.kind() == AgeSkyProfile.Kind.SUN ? SUN : MOON);
RenderSystem.setShaderColor(((color >> 16) & 255) / 255.0F, // Vanilla's sun/moon atlases have opaque dark pixels around the bright center.
((color >> 8) & 255) / 255.0F, (color & 255) / 255.0F, 1.0F); // Additive blending makes those pixels invisible while retaining the halo.
RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE);
RenderSystem.setShaderColor(red, green, blue,
body.kind() == AgeSkyProfile.Kind.SUN ? 0.84F : 0.88F + 0.12F * eclipse);
float uMax = body.kind() == AgeSkyProfile.Kind.SUN ? 1.0F : 0.25F; float uMax = body.kind() == AgeSkyProfile.Kind.SUN ? 1.0F : 0.25F;
float vMax = body.kind() == AgeSkyProfile.Kind.SUN ? 1.0F : 0.5F; float vMax = body.kind() == AgeSkyProfile.Kind.SUN ? 1.0F : 0.5F;
BufferBuilder vertices = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS, drawTexturedQuad(pose.last().pose(), -size, size, 0.0F, uMax, 0.0F, vMax);
DefaultVertexFormat.POSITION_TEX);
Matrix4f matrix = pose.last().pose(); // The 8x8 center of each 32x32 sprite is the actual body. Draw it last
vertices.addVertex(matrix, -size, RADIUS - 1.0F, -size).setUv(0.0F, 0.0F); // with ordinary alpha blending and full opacity so the sky and other
vertices.addVertex(matrix, size, RADIUS - 1.0F, -size).setUv(uMax, 0.0F); // celestial glows cannot show through it, including during an eclipse.
vertices.addVertex(matrix, size, RADIUS - 1.0F, size).setUv(uMax, vMax); RenderSystem.defaultBlendFunc();
vertices.addVertex(matrix, -size, RADIUS - 1.0F, size).setUv(0.0F, vMax); RenderSystem.setShaderColor(red, green, blue, 1.0F);
BufferUploader.drawWithShader(vertices.buildOrThrow()); drawTexturedQuad(pose.last().pose(), -size * 0.25F, size * 0.25F,
uMax * 0.375F, uMax * 0.625F, vMax * 0.375F, vMax * 0.625F);
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
pose.popPose(); pose.popPose();
} }
private static void drawTexturedQuad(Matrix4f matrix, float min, float max,
float uMin, float uMax, float vMin, float vMax) {
BufferBuilder vertices = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS,
DefaultVertexFormat.POSITION_TEX);
vertices.addVertex(matrix, min, RADIUS - 1.0F, min).setUv(uMin, vMin);
vertices.addVertex(matrix, max, RADIUS - 1.0F, min).setUv(uMax, vMin);
vertices.addVertex(matrix, max, RADIUS - 1.0F, max).setUv(uMax, vMax);
vertices.addVertex(matrix, min, RADIUS - 1.0F, max).setUv(uMin, vMax);
BufferUploader.drawWithShader(vertices.buildOrThrow());
}
private static void vertex(BufferBuilder vertices, Matrix4f matrix, int axis, int sign, private static void vertex(BufferBuilder vertices, Matrix4f matrix, int axis, int sign,
float x, float y, int color) { float x, float y, int color) {
float fixed = sign * RADIUS; float fixed = sign * RADIUS;
@@ -138,6 +138,17 @@ public final class SymbolPageTextures {
y * base.getHeight() / SIZE)); y * base.getHeight() / SIZE));
} }
} }
String numberPath = id.getPath().startsWith("number/")
? id.getPath().substring("number/".length()) : "";
if (id.getNamespace().equals(Rivencraft.MOD_ID)
&& numberPath.matches("(?:[0-9]|1[0-9]|2[0-4])")) {
int digit = Integer.parseInt(numberPath);
int glyphSize = item ? GLYPH * ITEM_SYMBOL_SIZE / ICON_SIZE : GLYPH;
int origin = item ? scaleForItem(38) : 38;
for (int component : SymbolNumerals.componentsForDigit(digit))
blendComponent(image, component, origin, origin, glyphSize);
return image;
}
String[] words = poem.isBlank() ? generatedWords(id) : poem.split(" "); String[] words = poem.isBlank() ? generatedWords(id) : poem.split(" ");
int[] offsetsX = {38, 77, 38, 0}; int[] offsetsX = {38, 77, 38, 0};
int[] offsetsY = {0, 38, 77, 38}; int[] offsetsY = {0, 38, 77, 38};
@@ -146,6 +157,13 @@ public final class SymbolPageTextures {
int originX = item ? scaleForItem(offsetsX[word]) : offsetsX[word]; int originX = item ? scaleForItem(offsetsX[word]) : offsetsX[word];
int originY = item ? scaleForItem(offsetsY[word]) : offsetsY[word]; int originY = item ? scaleForItem(offsetsY[word]) : offsetsY[word];
for (int component : glyphs(words[word])) { for (int component : glyphs(words[word])) {
blendComponent(image, component, originX, originY, glyphSize);
}
}
return image;
}
private static void blendComponent(NativeImage image, int component, int originX, int originY, int glyphSize) {
for (int y = 0; y < glyphSize; y++) for (int x = 0; x < glyphSize; x++) { for (int y = 0; y < glyphSize; y++) for (int x = 0; x < glyphSize; x++) {
int pixel = components.getPixelRGBA((component % 8) * 64 + x * 64 / glyphSize, int pixel = components.getPixelRGBA((component % 8) * 64 + x * 64 / glyphSize,
(component / 8) * 64 + y * 64 / glyphSize); (component / 8) * 64 + y * 64 / glyphSize);
@@ -154,9 +172,6 @@ public final class SymbolPageTextures {
(alpha << 24) | 0x001A212B); (alpha << 24) | 0x001A212B);
} }
} }
}
return image;
}
private static int scaleForItem(int coordinate) { private static int scaleForItem(int coordinate) {
return SIZE / 2 + Math.round((coordinate - SIZE / 2F) * ITEM_SYMBOL_SIZE / ICON_SIZE); return SIZE / 2 + Math.round((coordinate - SIZE / 2F) * ITEM_SYMBOL_SIZE / ICON_SIZE);
@@ -8,6 +8,7 @@ import net.dnorberg.rivencraft.network.AgeCelestialAnglePayload;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.neoforged.neoforge.event.entity.player.PlayerEvent; import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import net.neoforged.neoforge.network.PacketDistributor; import net.neoforged.neoforge.network.PacketDistributor;
import net.neoforged.neoforge.network.registration.NetworkRegistry;
/** Sends saved celestial angles when a client joins or enters an Age. */ /** Sends saved celestial angles when a client joins or enters an Age. */
public final class AgeCelestialAngleSync { public final class AgeCelestialAngleSync {
@@ -34,6 +35,8 @@ public final class AgeCelestialAngleSync {
} }
public static void send(ServerPlayer player, AgeDefinition age) { public static void send(ServerPlayer player, AgeDefinition age) {
// GameTest mock players (and clients without this channel) cannot receive this payload.
if (!NetworkRegistry.hasChannel(player.connection, AgeCelestialAnglePayload.TYPE.id())) return;
PacketDistributor.sendToPlayer(player, new AgeCelestialAnglePayload( PacketDistributor.sendToPlayer(player, new AgeCelestialAnglePayload(
AgeDimensionService.dimensionKey(age), age.traits().celestialAngle(), AgeDimensionService.dimensionKey(age), age.traits().celestialAngle(),
AgeSkyProfile.forAge(age))); AgeSkyProfile.forAge(age)));
@@ -58,7 +58,7 @@ public final class ModCreativeTabs {
.build()); .build());
private static final List<String> CATEGORY_ORDER = List.of( private static final List<String> CATEGORY_ORDER = List.of(
"terrain", "biome", "cave_biome", "biome_generation", "weather", "season", "lighting", "sky", "celestial", "block", "fluid", "feature", "environment", "corrupted"); "terrain", "biome", "cave_biome", "biome_generation", "weather", "season", "lighting", "sky", "color", "number", "celestial", "block", "fluid", "feature", "environment", "corrupted");
public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES = public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES =
registerSymbolPageTabs(); registerSymbolPageTabs();
@@ -80,6 +80,8 @@ public record SymbolSemantics(Role role, Value value) {
SKY("sky", Kind.CHOICE, "overworld", "nether", "end", "static"), SKY("sky", Kind.CHOICE, "overworld", "nether", "end", "static"),
SKY_COLOR("sky_color", Kind.CHOICE, SKY_COLOR("sky_color", Kind.CHOICE,
"red", "orange", "yellow", "green", "blue", "purple", "white", "black"), "red", "orange", "yellow", "green", "blue", "purple", "white", "black"),
COLOR("color", Kind.CHOICE, "red", "green", "blue", "black", "white"),
COLOR_NUMBER("color_number", Kind.NUMBER),
WEATHER("weather", Kind.CHOICE, WEATHER("weather", Kind.CHOICE,
"normal", "fast", "slow", "off", "always", "cloudy", "rain", "snow", "storm"), "normal", "fast", "slow", "off", "always", "cloudy", "rain", "snow", "storm"),
SEASON("season", Kind.CHOICE, SEASON("season", Kind.CHOICE,
@@ -116,6 +118,8 @@ public record SymbolSemantics(Role role, Value value) {
return switch (this) { return switch (this) {
case PERIOD_MODIFIER -> number.amount() >= 0.0 && number.amount() <= 2.0; case PERIOD_MODIFIER -> number.amount() >= 0.0 && number.amount() <= 2.0;
case SIZE_MODIFIER -> number.amount() >= 0.25 && number.amount() <= 3.0; case SIZE_MODIFIER -> number.amount() >= 0.25 && number.amount() <= 3.0;
case COLOR_NUMBER -> number.amount() >= 0.0 && number.amount() <= 24.0
&& number.amount() == Math.floor(number.amount());
case ANGLE_MODIFIER, PHASE_MODIFIER -> number.amount() >= 0.0 && number.amount() < 360.0; case ANGLE_MODIFIER, PHASE_MODIFIER -> number.amount() >= 0.0 && number.amount() < 360.0;
default -> false; default -> false;
}; };
@@ -127,6 +131,7 @@ public record SymbolSemantics(Role role, Value value) {
case CHOICE -> "one of " + choices.stream().sorted().toList(); case CHOICE -> "one of " + choices.stream().sorted().toList();
case NUMBER -> this == PERIOD_MODIFIER ? "a finite number in [0, 2]" case NUMBER -> this == PERIOD_MODIFIER ? "a finite number in [0, 2]"
: this == SIZE_MODIFIER ? "a finite number in [0.25, 3]" : this == SIZE_MODIFIER ? "a finite number in [0.25, 3]"
: this == COLOR_NUMBER ? "an integer D'ni digit in [0, 24]"
: "a finite number in [0, 360) degrees"; : "a finite number in [0, 360) degrees";
}; };
} }
@@ -45,6 +45,7 @@ public final class WritingGrammar {
int lastStage = -1; int lastStage = -1;
int biomeCount = 0; int biomeCount = 0;
ParsedSymbol pendingBlock = null; ParsedSymbol pendingBlock = null;
ParsedSymbol pendingColorNumber = null;
for (int index = 0; index < writing.entries().size(); index++) { for (int index = 0; index < writing.entries().size(); index++) {
WritingEntry entry = writing.entries().get(index); WritingEntry entry = writing.entries().get(index);
if (entry.kind() == WritingEntry.Kind.LINK_PANEL) { if (entry.kind() == WritingEntry.Kind.LINK_PANEL) {
@@ -73,6 +74,18 @@ public final class WritingGrammar {
Role role = semantics.orElseThrow().role(); Role role = semantics.orElseThrow().role();
ParsedSymbol parsed = new ParsedSymbol(index, reference, semantics.orElseThrow()); ParsedSymbol parsed = new ParsedSymbol(index, reference, semantics.orElseThrow());
symbols.add(parsed); symbols.add(parsed);
if (pendingColorNumber != null && role != Role.COLOR && role != Role.COLOR_NUMBER) {
diagnostics.add(new Diagnostic(Code.MISPLACED_MODIFIER, pendingColorNumber.index(),
"A number page must be immediately followed by a color page"));
pendingColorNumber = null;
}
if (role == Role.COLOR_NUMBER) {
if (pendingColorNumber != null) diagnostics.add(new Diagnostic(Code.MISPLACED_MODIFIER,
pendingColorNumber.index(), "A number page must be immediately followed by a color page"));
pendingColorNumber = parsed;
} else if (role == Role.COLOR) {
pendingColorNumber = null;
}
if (modifier(role)) { if (modifier(role)) {
// Legacy modifier pages pop and combine an earlier modifier of the same // Legacy modifier pages pop and combine an earlier modifier of the same
// kind. Only the final pending value can be left dangling. // kind. Only the final pending value can be left dangling.
@@ -115,13 +128,15 @@ public final class WritingGrammar {
} }
if (role == Role.BIOME) biomeCount++; if (role == Role.BIOME) biomeCount++;
int stage = stage(role); int stage = stage(role);
if (stage < lastStage && !modifier(role)) { if (stage < lastStage && !modifier(role) && role != Role.COLOR && role != Role.COLOR_NUMBER) {
diagnostics.add(new Diagnostic(Code.OUT_OF_ORDER, diagnostics.add(new Diagnostic(Code.OUT_OF_ORDER,
index, "Move this " + role.id() + " page before later grammar categories")); index, "Move this " + role.id() + " page before later grammar categories"));
} else { } else if (role != Role.COLOR && role != Role.COLOR_NUMBER) {
lastStage = stage; lastStage = stage;
} }
} }
if (pendingColorNumber != null) diagnostics.add(new Diagnostic(Code.DANGLING_MODIFIER,
pendingColorNumber.index(), "A number page must be followed by a color page"));
if (panels == 0) diagnostics.add(new Diagnostic(Code.MISSING_PANEL, -1, if (panels == 0) diagnostics.add(new Diagnostic(Code.MISSING_PANEL, -1,
"Add a Link Panel as the first page")); "Add a Link Panel as the first page"));
for (ParsedSymbol dangling : pendingModifiers.values()) { for (ParsedSymbol dangling : pendingModifiers.values()) {
@@ -183,6 +198,7 @@ public final class WritingGrammar {
case SEASON -> 4; case SEASON -> 4;
case LIGHTING -> 5; case LIGHTING -> 5;
case SKY, SKY_COLOR -> 6; case SKY, SKY_COLOR -> 6;
case COLOR, COLOR_NUMBER -> 6;
case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER, SIZE_MODIFIER, case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER, SIZE_MODIFIER,
CELESTIAL_COLOR, DIRECTION_MODIFIER -> 7; CELESTIAL_COLOR, DIRECTION_MODIFIER -> 7;
case CELESTIAL -> 8; case CELESTIAL -> 8;
@@ -225,22 +225,42 @@
"symbol.rivencraft.sky.end.description": "Use the End sky and dark preview background.", "symbol.rivencraft.sky.end.description": "Use the End sky and dark preview background.",
"symbol.rivencraft.sky.static": "Static Sky", "symbol.rivencraft.sky.static": "Static Sky",
"symbol.rivencraft.sky.static.description": "Fill the sky with shifting television static.", "symbol.rivencraft.sky.static.description": "Fill the sky with shifting television static.",
"symbol.rivencraft.sky_color.red": "Red Sky Color", "symbol.rivencraft.color.red": "Red Color",
"symbol.rivencraft.sky_color.red.description": "Tint the sky and linked portal red.", "symbol.rivencraft.color.red.description": "Add red to the next Sky, Sun, or Moon. A number page directly before this page sets its strength.",
"symbol.rivencraft.sky_color.orange": "Orange Sky Color", "symbol.rivencraft.color.green": "Green Color",
"symbol.rivencraft.sky_color.orange.description": "Tint the sky and linked portal orange.", "symbol.rivencraft.color.green.description": "Add green to the next Sky, Sun, or Moon. A number page directly before this page sets its strength.",
"symbol.rivencraft.sky_color.yellow": "Yellow Sky Color", "symbol.rivencraft.color.blue": "Blue Color",
"symbol.rivencraft.sky_color.yellow.description": "Tint the sky and linked portal yellow.", "symbol.rivencraft.color.blue.description": "Add blue to the next Sky, Sun, or Moon. A number page directly before this page sets its strength.",
"symbol.rivencraft.sky_color.green": "Green Sky Color", "symbol.rivencraft.color.white": "White Color",
"symbol.rivencraft.sky_color.green.description": "Tint the sky and linked portal green.", "symbol.rivencraft.color.white.description": "Add equal brightness to red, green, and blue in the next Sky, Sun, or Moon.",
"symbol.rivencraft.sky_color.blue": "Blue Sky Color", "symbol.rivencraft.color.black": "Black Color",
"symbol.rivencraft.sky_color.blue.description": "Tint the sky and linked portal blue.", "symbol.rivencraft.color.black.description": "Subtract equal brightness from red, green, and blue in the next Sky, Sun, or Moon.",
"symbol.rivencraft.sky_color.purple": "Purple Sky Color", "symbol.rivencraft.number.0": "D'ni 0",
"symbol.rivencraft.sky_color.purple.description": "Tint the sky and linked portal purple.", "symbol.rivencraft.number.1": "D'ni 1",
"symbol.rivencraft.sky_color.white": "White Sky Color", "symbol.rivencraft.number.2": "D'ni 2",
"symbol.rivencraft.sky_color.white.description": "Tint the sky and linked portal white.", "symbol.rivencraft.number.3": "D'ni 3",
"symbol.rivencraft.sky_color.black": "Black Sky Color", "symbol.rivencraft.number.4": "D'ni 4",
"symbol.rivencraft.sky_color.black.description": "Tint the sky and linked portal near-black.", "symbol.rivencraft.number.5": "D'ni 5",
"symbol.rivencraft.number.6": "D'ni 6",
"symbol.rivencraft.number.7": "D'ni 7",
"symbol.rivencraft.number.8": "D'ni 8",
"symbol.rivencraft.number.9": "D'ni 9",
"symbol.rivencraft.number.10": "D'ni 10",
"symbol.rivencraft.number.11": "D'ni 11",
"symbol.rivencraft.number.12": "D'ni 12",
"symbol.rivencraft.number.13": "D'ni 13",
"symbol.rivencraft.number.14": "D'ni 14",
"symbol.rivencraft.number.15": "D'ni 15",
"symbol.rivencraft.number.16": "D'ni 16",
"symbol.rivencraft.number.17": "D'ni 17",
"symbol.rivencraft.number.18": "D'ni 18",
"symbol.rivencraft.number.19": "D'ni 19",
"symbol.rivencraft.number.20": "D'ni 20",
"symbol.rivencraft.number.21": "D'ni 21",
"symbol.rivencraft.number.22": "D'ni 22",
"symbol.rivencraft.number.23": "D'ni 23",
"symbol.rivencraft.number.24": "D'ni 24",
"symbol.rivencraft.number.description": "Set the next color page to this D'ni number times 10. Without a number page, the Age chooses a random number from 0 to 24.",
"symbol.rivencraft.weather.normal": "Normal Weather", "symbol.rivencraft.weather.normal": "Normal Weather",
"symbol.rivencraft.weather.normal.description": "Use the normal weather cycle.", "symbol.rivencraft.weather.normal.description": "Use the normal weather cycle.",
"symbol.rivencraft.weather.fast": "Fast Weather", "symbol.rivencraft.weather.fast": "Fast Weather",
@@ -271,22 +291,6 @@
"symbol.rivencraft.celestial.sun.description": "Add one independently moving sun to the sky.", "symbol.rivencraft.celestial.sun.description": "Add one independently moving sun to the sky.",
"symbol.rivencraft.celestial.moon": "Moon", "symbol.rivencraft.celestial.moon": "Moon",
"symbol.rivencraft.celestial.moon.description": "Add one independently moving moon to the sky.", "symbol.rivencraft.celestial.moon.description": "Add one independently moving moon to the sky.",
"symbol.rivencraft.celestial_color.red": "Red Celestial",
"symbol.rivencraft.celestial_color.red.description": "Color the next sun or moon red.",
"symbol.rivencraft.celestial_color.orange": "Orange Celestial",
"symbol.rivencraft.celestial_color.orange.description": "Color the next sun or moon orange.",
"symbol.rivencraft.celestial_color.yellow": "Yellow Celestial",
"symbol.rivencraft.celestial_color.yellow.description": "Color the next sun or moon yellow.",
"symbol.rivencraft.celestial_color.green": "Green Celestial",
"symbol.rivencraft.celestial_color.green.description": "Color the next sun or moon green.",
"symbol.rivencraft.celestial_color.blue": "Blue Celestial",
"symbol.rivencraft.celestial_color.blue.description": "Color the next sun or moon blue.",
"symbol.rivencraft.celestial_color.purple": "Purple Celestial",
"symbol.rivencraft.celestial_color.purple.description": "Color the next sun or moon purple.",
"symbol.rivencraft.celestial_color.white": "White Celestial",
"symbol.rivencraft.celestial_color.white.description": "Color the next sun or moon white.",
"symbol.rivencraft.celestial_color.black": "Black Celestial",
"symbol.rivencraft.celestial_color.black.description": "Blend black into the next sun or moon's color.",
"symbol.rivencraft.size.small": "Small Celestial", "symbol.rivencraft.size.small": "Small Celestial",
"symbol.rivencraft.size.small.description": "Halve the apparent size of the next sun or moon.", "symbol.rivencraft.size.small.description": "Halve the apparent size of the next sun or moon.",
"symbol.rivencraft.size.normal": "Normal Celestial Size", "symbol.rivencraft.size.normal": "Normal Celestial Size",
@@ -436,6 +440,8 @@
"itemGroup.rivencraft.pages.season": "Rivencraft Season Pages", "itemGroup.rivencraft.pages.season": "Rivencraft Season Pages",
"itemGroup.rivencraft.pages.lighting": "Rivencraft Lighting Pages", "itemGroup.rivencraft.pages.lighting": "Rivencraft Lighting Pages",
"itemGroup.rivencraft.pages.sky": "Rivencraft Sky Pages", "itemGroup.rivencraft.pages.sky": "Rivencraft Sky Pages",
"itemGroup.rivencraft.pages.color": "Rivencraft Color Pages",
"itemGroup.rivencraft.pages.number": "Rivencraft Number Pages",
"itemGroup.rivencraft.pages.celestial": "Rivencraft Celestial Pages", "itemGroup.rivencraft.pages.celestial": "Rivencraft Celestial Pages",
"itemGroup.rivencraft.pages.block": "Rivencraft Block Pages", "itemGroup.rivencraft.pages.block": "Rivencraft Block Pages",
"itemGroup.rivencraft.pages.fluid": "Rivencraft Fluid Pages", "itemGroup.rivencraft.pages.fluid": "Rivencraft Fluid Pages",
@@ -1 +0,0 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.black","description":"symbol.rivencraft.celestial_color.black.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"black"},"poem":"Transform Black Illuminate Motion"}
@@ -1 +0,0 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.blue","description":"symbol.rivencraft.celestial_color.blue.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"blue"},"poem":"Transform Blue Illuminate Motion"}
@@ -1 +0,0 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.green","description":"symbol.rivencraft.celestial_color.green.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"green"},"poem":"Transform Green Illuminate Motion"}
@@ -1 +0,0 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.orange","description":"symbol.rivencraft.celestial_color.orange.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"orange"},"poem":"Transform Orange Illuminate Motion"}
@@ -1 +0,0 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.purple","description":"symbol.rivencraft.celestial_color.purple.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"purple"},"poem":"Transform Purple Illuminate Motion"}
@@ -1 +0,0 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.red","description":"symbol.rivencraft.celestial_color.red.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"red"},"poem":"Transform Red Illuminate Motion"}
@@ -1 +0,0 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.white","description":"symbol.rivencraft.celestial_color.white.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"white"},"poem":"Transform White Illuminate Motion"}
@@ -1 +0,0 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.yellow","description":"symbol.rivencraft.celestial_color.yellow.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"yellow"},"poem":"Transform Yellow Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.color.black","description":"symbol.rivencraft.color.black.description","category":"rivencraft:color","semantics":{"role":"color","value":"black"},"poem":"Ethereal Black Transform Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.color.blue","description":"symbol.rivencraft.color.blue.description","category":"rivencraft:color","semantics":{"role":"color","value":"blue"},"poem":"Ethereal Blue Transform Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.color.green","description":"symbol.rivencraft.color.green.description","category":"rivencraft:color","semantics":{"role":"color","value":"green"},"poem":"Ethereal Green Transform Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.color.red","description":"symbol.rivencraft.color.red.description","category":"rivencraft:color","semantics":{"role":"color","value":"red"},"poem":"Ethereal Red Transform Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.color.white","description":"symbol.rivencraft.color.white.description","category":"rivencraft:color","semantics":{"role":"color","value":"white"},"poem":"Ethereal White Transform Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.0","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":0},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.1","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":1},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.10","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":10},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.11","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":11},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.12","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":12},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.13","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":13},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.14","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":14},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.15","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":15},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.16","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":16},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.17","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":17},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.18","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":18},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.19","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":19},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.2","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":2},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.20","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":20},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.21","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":21},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.22","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":22},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.23","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":23},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.24","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":24},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.3","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":3},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.4","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":4},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.5","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":5},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.6","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":6},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.7","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":7},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.8","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":8},"poem":"Transform Time System Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.number.9","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":9},"poem":"Transform Time System Balance"}
@@ -1 +0,0 @@
{"schema_version":2,"name":"symbol.rivencraft.sky_color.black","description":"symbol.rivencraft.sky_color.black.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"black"},"poem":"Ethereal Black Sky Balance"}
@@ -1 +0,0 @@
{"schema_version":2,"name":"symbol.rivencraft.sky_color.blue","description":"symbol.rivencraft.sky_color.blue.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"blue"},"poem":"Ethereal Blue Sky Balance"}
@@ -1 +0,0 @@
{"schema_version":2,"name":"symbol.rivencraft.sky_color.green","description":"symbol.rivencraft.sky_color.green.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"green"},"poem":"Ethereal Green Sky Balance"}
@@ -1 +0,0 @@
{"schema_version":2,"name":"symbol.rivencraft.sky_color.orange","description":"symbol.rivencraft.sky_color.orange.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"orange"},"poem":"Ethereal Orange Sky Balance"}
@@ -1 +0,0 @@
{"schema_version":2,"name":"symbol.rivencraft.sky_color.purple","description":"symbol.rivencraft.sky_color.purple.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"purple"},"poem":"Ethereal Purple Sky Balance"}
@@ -1 +0,0 @@
{"schema_version":2,"name":"symbol.rivencraft.sky_color.red","description":"symbol.rivencraft.sky_color.red.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"red"},"poem":"Ethereal Red Sky Balance"}
@@ -1 +0,0 @@
{"schema_version":2,"name":"symbol.rivencraft.sky_color.white","description":"symbol.rivencraft.sky_color.white.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"white"},"poem":"Ethereal White Sky Balance"}
@@ -1 +0,0 @@
{"schema_version":2,"name":"symbol.rivencraft.sky_color.yellow","description":"symbol.rivencraft.sky_color.yellow.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"yellow"},"poem":"Ethereal Yellow Sky Balance"}