Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77f15f8599 |
No files matched your search
@@ -32,9 +32,9 @@ The Writing Desk retains 88 collection tabs, numbered from zero as in Legacy. Le
|
||||
| `weather` | `normal`, `fast`, `slow`, `off`, `always`, `cloudy`, `rain`, `snow`, or `storm`. |
|
||||
| `season` | `no_seasons`, `seasons`, `eternal_winter`, `eternal_summer`, `eternal_fall`, or `eternal_spring`. Seasonal effects other than No Seasons require optional Serene Seasons. |
|
||||
| `celestial` | `normal_time` (the first representable single-cycle behavior). |
|
||||
| `period_modifier` | JSON number in `[0, 2]`; zero is stopped time. |
|
||||
| `angle_modifier` | JSON number in `[0, 360)` degrees. |
|
||||
| `phase_modifier` | JSON number in `[0, 360)` degrees; the compiler will convert to turns. |
|
||||
| `period_modifier` | JSON number in `[0, 2]`; zero is stopped time. An immediately preceding D'ni number page overrides it with digit × 0.08. |
|
||||
| `angle_modifier` | JSON number in `[0, 360)` degrees. An immediately preceding D'ni number page overrides it with digit × 15°, wrapping 360° to 0°. |
|
||||
| `phase_modifier` | JSON number in `[0, 360)` degrees; the compiler converts it to turns. An immediately preceding D'ni number page overrides it with digit × 15°, wrapping 360° to 0°. |
|
||||
| `block` | Canonical namespaced material ID for a compatible following feature; registered full-collision blocks and source fluids receive generated Pages. |
|
||||
| `feature` | Canonical namespaced feature-effect ID; the compiler must recognize and validate it. This includes supported decorations and structures. |
|
||||
|
||||
|
||||
+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.6.9.3
|
||||
mod_version=0.6.10.0
|
||||
immersive_portals_version=6.0.7
|
||||
terrablender_version=4.1.0.8
|
||||
serene_seasons_version_id=pHEgQQUE
|
||||
|
||||
@@ -155,15 +155,19 @@ public final class AgeEnvironmentCompiler {
|
||||
Double pendingPeriod = null;
|
||||
Float pendingPhase = null;
|
||||
boolean writtenPhase = false;
|
||||
Double pendingNumber = null;
|
||||
for (AgeGenerationPlan.ResolvedSymbol symbol : plan.symbols()) {
|
||||
SymbolSemantics semantics = symbol.semantics();
|
||||
Double number = pendingNumber;
|
||||
if (semantics.role() != SymbolSemantics.Role.COLOR_NUMBER) pendingNumber = null;
|
||||
switch (semantics.role()) {
|
||||
case COLOR_NUMBER -> pendingNumber = ((SymbolSemantics.NumberValue) semantics.value()).amount();
|
||||
case PERIOD_MODIFIER -> {
|
||||
double next = ((SymbolSemantics.NumberValue) semantics.value()).amount();
|
||||
double next = CelestialPageValues.value(semantics, number);
|
||||
pendingPeriod = pendingPeriod == null ? next : (pendingPeriod + next) / 2.0D;
|
||||
}
|
||||
case PHASE_MODIFIER -> {
|
||||
float next = (float) ((SymbolSemantics.NumberValue) semantics.value()).amount();
|
||||
float next = (float) CelestialPageValues.value(semantics, number);
|
||||
pendingPhase = pendingPhase == null ? next : circularMidpoint(pendingPhase, next);
|
||||
}
|
||||
case CELESTIAL -> {
|
||||
|
||||
@@ -59,13 +59,13 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
|
||||
float size = 1.0F;
|
||||
List<Integer> bodyColors = new ArrayList<>();
|
||||
ColorAccumulator pendingColors = new ColorAccumulator();
|
||||
Integer colorMultiplier = null;
|
||||
Double pendingNumber = null;
|
||||
boolean clockwise = true;
|
||||
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) colorMultiplier = null;
|
||||
Double number = pendingNumber;
|
||||
if (semantics.role() != SymbolSemantics.Role.COLOR_NUMBER) pendingNumber = null;
|
||||
switch (semantics.role()) {
|
||||
case SKY -> {
|
||||
pendingColors.clear();
|
||||
@@ -77,16 +77,15 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
|
||||
default -> throw new IllegalArgumentException("Unknown sky style");
|
||||
};
|
||||
}
|
||||
case COLOR_NUMBER -> colorMultiplier = (int) ((SymbolSemantics.NumberValue)
|
||||
case COLOR_NUMBER -> pendingNumber = ((SymbolSemantics.NumberValue)
|
||||
semantics.value()).amount();
|
||||
case COLOR -> {
|
||||
pendingColors.add(((SymbolSemantics.ChoiceValue) semantics.value()).id(),
|
||||
colorMultiplier == null ? randomColorNumber(plan.ageSeed(), symbol.index()) : colorMultiplier);
|
||||
colorMultiplier = null;
|
||||
number == null ? randomColorNumber(plan.ageSeed(), symbol.index()) : number.intValue());
|
||||
}
|
||||
case PERIOD_MODIFIER -> period = ((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 PERIOD_MODIFIER -> period = CelestialPageValues.value(semantics, number);
|
||||
case ANGLE_MODIFIER -> angle = (float) CelestialPageValues.value(semantics, number);
|
||||
case PHASE_MODIFIER -> phase = (float) (CelestialPageValues.value(semantics, number) / 360.0D);
|
||||
case SIZE_MODIFIER -> size = (float) ((SymbolSemantics.NumberValue) semantics.value()).amount();
|
||||
case CELESTIAL_COLOR -> bodyColors.add(color(
|
||||
((SymbolSemantics.ChoiceValue) semantics.value()).id()));
|
||||
@@ -95,7 +94,7 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
|
||||
case CELESTIAL -> {
|
||||
String choice = ((SymbolSemantics.ChoiceValue) semantics.value()).id();
|
||||
int bodyColor = pendingColors.written() ? pendingColors.rgb()
|
||||
: bodyColors.isEmpty() ? 0 : mixColors(bodyColors);
|
||||
: bodyColors.isEmpty() ? 0xFFFFFF : mixColors(bodyColors);
|
||||
if (choice.equals("normal_time")) {
|
||||
if (customBodies) {
|
||||
bodies.add(new Body(Kind.SUN, bodyColor, size, angle, period, phase, clockwise));
|
||||
@@ -221,6 +220,7 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
|
||||
&& !((SymbolSemantics.ChoiceValue) symbol.semantics().value()).id().equals("normal_time")
|
||||
|| symbol.semantics().role() == SymbolSemantics.Role.CELESTIAL_COLOR
|
||||
|| symbol.semantics().role() == SymbolSemantics.Role.SIZE_MODIFIER
|
||||
|| symbol.semantics().role() == SymbolSemantics.Role.ANGLE_MODIFIER
|
||||
|| symbol.semantics().role() == SymbolSemantics.Role.DIRECTION_MODIFIER));
|
||||
}
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ public record AgeTraits(
|
||||
if (biomeDistribution == null || lighting == null || weather == null) {
|
||||
throw new IllegalArgumentException("Age trait choices cannot be null");
|
||||
}
|
||||
if (celestialPeriod != STOPPED_CELESTIAL_PERIOD
|
||||
&& (celestialPeriod < MIN_CELESTIAL_PERIOD || celestialPeriod > MAX_CELESTIAL_PERIOD)) {
|
||||
if (!Double.isFinite(celestialPeriod)
|
||||
|| celestialPeriod < STOPPED_CELESTIAL_PERIOD || celestialPeriod > MAX_CELESTIAL_PERIOD) {
|
||||
throw new IllegalArgumentException("Celestial period is outside the legacy range");
|
||||
}
|
||||
if (celestialAngle < MIN_CELESTIAL_ANGLE || celestialAngle >= MAX_CELESTIAL_ANGLE) {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
||||
|
||||
/** A preceding D'ni number overrides the next celestial modifier's fixed page value. */
|
||||
final class CelestialPageValues {
|
||||
private CelestialPageValues() {}
|
||||
|
||||
static double value(SymbolSemantics semantics, Double number) {
|
||||
if (number == null) return ((SymbolSemantics.NumberValue) semantics.value()).amount();
|
||||
return switch (semantics.role()) {
|
||||
case PERIOD_MODIFIER -> number * 0.08D;
|
||||
case ANGLE_MODIFIER, PHASE_MODIFIER -> (number * 15.0D) % 360.0D;
|
||||
default -> throw new IllegalArgumentException("Not a celestial numeric modifier");
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -236,6 +236,57 @@ public final class EnvironmentWritingGameTests {
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void numberPagesSetAdjacentCelestialModifiers(GameTestHelper helper) {
|
||||
OrderedWriting writing = new OrderedWriting(List.of(
|
||||
WritingEntry.linkPanel(), page("terrain/standard"), page("sky/overworld"),
|
||||
page("number/1"), page("period/double"),
|
||||
page("number/7"), page("angle/east"),
|
||||
page("number/12"), page("phase/rising"), page("celestial/sun"),
|
||||
page("number/24"), page("angle/west"),
|
||||
page("number/0"), page("phase/setting"),
|
||||
page("number/0"), page("period/full"), page("celestial/moon")));
|
||||
helper.assertValueEqual(WritingGrammar.parse(writing).writingInstability(), 0,
|
||||
"Adjacent number and celestial modifier pages must not count as misplaced writing");
|
||||
AgeTraits seeded = AgeTraits.select(94L);
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 94L,
|
||||
AgeGenerationProfiles.STANDARD, seeded, helper.getLevel().registryAccess());
|
||||
AgeTraits effective = AgeEnvironmentCompiler.apply(plan, seeded);
|
||||
AgeSkyProfile sky = AgeSkyProfile.compile(plan, effective);
|
||||
helper.assertValueEqual(sky.bodies().getFirst().period(), 0.08D,
|
||||
"A number overrides a fixed Length page at 0.08 per digit, even below the random minimum");
|
||||
helper.assertValueEqual(sky.bodies().getFirst().angle(), 105.0F,
|
||||
"A number overrides a fixed Angle page at 15 degrees per digit");
|
||||
helper.assertValueEqual(sky.bodies().getFirst().phase(), 0.5F,
|
||||
"A number overrides a fixed Phase page at 15 degrees per digit");
|
||||
helper.assertValueEqual(sky.bodies().getLast().angle(), 0.0F,
|
||||
"Digit 24 wraps a full 360-degree rotation to zero");
|
||||
helper.assertValueEqual(sky.bodies().getLast().phase(), 0.0F,
|
||||
"Digit zero is a valid phase override");
|
||||
helper.assertValueEqual(effective.celestialPeriod(), 0.0D,
|
||||
"The final authored celestial controls the world clock");
|
||||
helper.assertValueEqual(AgeSkyProfile.compile(AgeGenerationPlan.load(plan.save()), effective), sky,
|
||||
"Numbered celestial values must survive a plan save/load round trip");
|
||||
AgeGenerationPlan normalTime = AgeGenerationPlan.compile(new OrderedWriting(List.of(
|
||||
WritingEntry.linkPanel(), page("number/6"), page("angle/north"),
|
||||
page("celestial/normal_time"))), 95L, AgeGenerationProfiles.STANDARD,
|
||||
AgeTraits.select(95L), helper.getLevel().registryAccess());
|
||||
helper.assertValueEqual(AgeEnvironmentCompiler.skyEffects(normalTime,
|
||||
BuiltinDimensionTypes.OVERWORLD_EFFECTS), AgeEnvironmentCompiler.CUSTOM_SKY_EFFECTS,
|
||||
"An authored angle on Normal Time must select the custom celestial renderer");
|
||||
AgeSkyProfile normalSky = AgeSkyProfile.compile(normalTime,
|
||||
AgeEnvironmentCompiler.apply(normalTime, AgeTraits.select(95L)));
|
||||
helper.assertValueEqual(normalSky.bodies().getFirst().angle(), 90.0F,
|
||||
"The custom Normal Time sun must honor its numbered angle");
|
||||
helper.assertValueEqual(normalSky.bodies().getFirst().color(), 0xFFFFFF,
|
||||
"Uncolored custom celestial bodies must remain visible");
|
||||
OrderedWriting separated = new OrderedWriting(List.of(WritingEntry.linkPanel(),
|
||||
page("number/7"), page("direction/clockwise"), page("angle/east"), page("celestial/sun")));
|
||||
helper.assertValueEqual(WritingGrammar.parse(separated).writingInstability(), 100,
|
||||
"A number does not reach past an intervening page");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void alignedMoonEclipsesSunAndDimsLight(GameTestHelper helper) {
|
||||
AgeSkyProfile.Body sun = new AgeSkyProfile.Body(AgeSkyProfile.Kind.SUN,
|
||||
|
||||
@@ -45,7 +45,7 @@ public final class WritingGrammar {
|
||||
int lastStage = -1;
|
||||
int biomeCount = 0;
|
||||
ParsedSymbol pendingBlock = null;
|
||||
ParsedSymbol pendingColorNumber = null;
|
||||
ParsedSymbol pendingNumber = null;
|
||||
for (int index = 0; index < writing.entries().size(); index++) {
|
||||
WritingEntry entry = writing.entries().get(index);
|
||||
if (entry.kind() == WritingEntry.Kind.LINK_PANEL) {
|
||||
@@ -74,17 +74,17 @@ public final class WritingGrammar {
|
||||
Role role = semantics.orElseThrow().role();
|
||||
ParsedSymbol parsed = new ParsedSymbol(index, reference, semantics.orElseThrow());
|
||||
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 (pendingNumber != null && !numberTarget(role) && role != Role.COLOR_NUMBER) {
|
||||
diagnostics.add(new Diagnostic(Code.MISPLACED_MODIFIER, pendingNumber.index(),
|
||||
"A number page must be immediately followed by a color or celestial modifier page"));
|
||||
pendingNumber = 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 (pendingNumber != null) diagnostics.add(new Diagnostic(Code.MISPLACED_MODIFIER,
|
||||
pendingNumber.index(), "A number page must be immediately followed by a color or celestial modifier page"));
|
||||
pendingNumber = parsed;
|
||||
} else if (numberTarget(role)) {
|
||||
pendingNumber = null;
|
||||
}
|
||||
if (modifier(role)) {
|
||||
// Legacy modifier pages pop and combine an earlier modifier of the same
|
||||
@@ -135,8 +135,8 @@ public final class WritingGrammar {
|
||||
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 (pendingNumber != null) diagnostics.add(new Diagnostic(Code.DANGLING_MODIFIER,
|
||||
pendingNumber.index(), "A number page must be followed by a color or celestial modifier page"));
|
||||
if (panels == 0) diagnostics.add(new Diagnostic(Code.MISSING_PANEL, -1,
|
||||
"Add a Link Panel as the first page"));
|
||||
for (ParsedSymbol dangling : pendingModifiers.values()) {
|
||||
@@ -189,6 +189,11 @@ public final class WritingGrammar {
|
||||
|| role == Role.CELESTIAL_COLOR || role == Role.DIRECTION_MODIFIER;
|
||||
}
|
||||
|
||||
private static boolean numberTarget(Role role) {
|
||||
return role == Role.COLOR || role == Role.PERIOD_MODIFIER
|
||||
|| role == Role.ANGLE_MODIFIER || role == Role.PHASE_MODIFIER;
|
||||
}
|
||||
|
||||
private static int stage(Role role) {
|
||||
return switch (role) {
|
||||
case TERRAIN -> 0;
|
||||
|
||||
@@ -260,7 +260,7 @@
|
||||
"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.number.description": "Set the next color page to this number × 10, Angle or Phase to × 15°, or Length to × 0.08. A number page affects only the immediately following page.",
|
||||
"symbol.rivencraft.weather.normal": "Normal Weather",
|
||||
"symbol.rivencraft.weather.normal.description": "Use the normal weather cycle.",
|
||||
"symbol.rivencraft.weather.fast": "Fast Weather",
|
||||
|
||||
Reference in new issue
Block a user