Skip to content

Commit c67e1f4

Browse files
committed
Merge PR #5387 (soloturn-moon-phases) into merge-train
2 parents e0c8319 + 29deb93 commit c67e1f4

5 files changed

Lines changed: 107 additions & 7 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2021 The Terasology Foundation
2+
// SPDX-License-Identifier: Apache-2.0
3+
package org.terasology.engine.world.sun;
4+
5+
import org.junit.jupiter.api.BeforeEach;
6+
import org.junit.jupiter.api.Test;
7+
import org.terasology.engine.entitySystem.entity.EntityManager;
8+
import org.terasology.engine.world.WorldProvider;
9+
import org.terasology.engine.world.time.WorldTime;
10+
11+
import static org.junit.jupiter.api.Assertions.assertEquals;
12+
import static org.mockito.Mockito.mock;
13+
import static org.mockito.Mockito.when;
14+
15+
/**
16+
* See #94: getMoonPhase() is a hook other systems (rendering, gameplay, a future calendar) can key off
17+
* without each needing their own day-counting logic, based on the same day counter getSunPosAngle()
18+
* already uses.
19+
*/
20+
public class DefaultCelestialSystemTest {
21+
22+
private static final float MOON_CYCLE_DAYS = 29.53f;
23+
private static final float EPSILON = 1e-4f;
24+
25+
private WorldTime worldTime;
26+
private DefaultCelestialSystem celestialSystem;
27+
28+
@BeforeEach
29+
public void setup() {
30+
CelestialModel model = mock(CelestialModel.class);
31+
WorldProvider worldProvider = mock(WorldProvider.class);
32+
EntityManager entityManager = mock(EntityManager.class);
33+
worldTime = mock(WorldTime.class);
34+
when(worldProvider.getTime()).thenReturn(worldTime);
35+
36+
celestialSystem = new DefaultCelestialSystem(model, worldProvider, entityManager);
37+
}
38+
39+
private void setDays(float days) {
40+
when(worldTime.getDays()).thenReturn(days);
41+
}
42+
43+
@Test
44+
public void newMoonAtDayZero() {
45+
setDays(0f);
46+
assertEquals(0f, celestialSystem.getMoonPhase(), EPSILON);
47+
}
48+
49+
@Test
50+
public void fullMoonHalfwayThroughTheCycle() {
51+
setDays(MOON_CYCLE_DAYS / 2f);
52+
assertEquals(0.5f, celestialSystem.getMoonPhase(), EPSILON);
53+
}
54+
55+
@Test
56+
public void phaseWrapsAroundAfterACompleteCycle() {
57+
setDays(MOON_CYCLE_DAYS + MOON_CYCLE_DAYS / 4f);
58+
assertEquals(0.25f, celestialSystem.getMoonPhase(), EPSILON);
59+
}
60+
61+
@Test
62+
public void phaseWrapsAroundAfterManyCycles() {
63+
setDays(MOON_CYCLE_DAYS * 100 + MOON_CYCLE_DAYS * 0.75f);
64+
assertEquals(0.75f, celestialSystem.getMoonPhase(), EPSILON);
65+
}
66+
67+
@Test
68+
public void phaseStaysInRangeWhenSunIsHalted() {
69+
setDays(1000f);
70+
celestialSystem.toggleSunHalting(MOON_CYCLE_DAYS / 4f);
71+
assertEquals(0.25f, celestialSystem.getMoonPhase(), EPSILON);
72+
}
73+
}

engine/src/main/java/org/terasology/engine/rendering/backdrop/BackdropProvider.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,6 @@ public interface BackdropProvider {
2828

2929
Vector3f getSunDirection(boolean moonlightFlip);
3030

31+
float getMoonPhase();
32+
3133
}

engine/src/main/java/org/terasology/engine/rendering/backdrop/Skysphere.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,9 @@ public Vector3f getSunDirection(boolean moonlightFlip) {
7070

7171
return sunDirection;
7272
}
73+
74+
@Override
75+
public float getMoonPhase() {
76+
return celSystem.getMoonPhase();
77+
}
7378
}

engine/src/main/java/org/terasology/engine/world/sun/CelestialSystem.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,12 @@ public interface CelestialSystem {
2424
* @return Whether the sun is currently halted or not
2525
*/
2626
boolean isSunHalted();
27+
28+
/**
29+
* @return the current phase of the moon, as a value in [0, 1) where 0 (and the limit at 1) is new
30+
* moon and 0.5 is full moon. Intended as a hook for anything wanting to key off the moon's phase -
31+
* rendering, gameplay, a future astronomical/calendar system - without needing its own day-counting
32+
* logic. See #94.
33+
*/
34+
float getMoonPhase();
2735
}

engine/src/main/java/org/terasology/engine/world/sun/DefaultCelestialSystem.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@
2121
*/
2222
public class DefaultCelestialSystem extends BaseComponentSystem implements CelestialSystem, UpdateSubscriberSystem {
2323

24+
/**
25+
* Length of a full lunar cycle, in game days. Defaults to the real-world synodic month so a
26+
* calendar built on top of this lines up with familiar phase names/timing.
27+
*/
28+
private static final float MOON_CYCLE_DAYS = 29.53f;
29+
2430
private final WorldTime worldTime;
2531

2632
private long lastUpdate;
@@ -56,13 +62,7 @@ public void update(float delta) {
5662

5763
@Override
5864
public float getSunPosAngle() {
59-
float days;
60-
if (isSunHalted()) {
61-
days = haltedTime;
62-
} else {
63-
days = getWorldTime().getDays();
64-
}
65-
return model.getSunPosAngle(days);
65+
return model.getSunPosAngle(getCurrentDays());
6666
}
6767

6868
@Override
@@ -75,6 +75,18 @@ public void toggleSunHalting(float timeInDays) {
7575
haltSunPosition = !haltSunPosition;
7676
haltedTime = timeInDays;
7777
}
78+
79+
@Override
80+
public float getMoonPhase() {
81+
float phase = getCurrentDays() % MOON_CYCLE_DAYS / MOON_CYCLE_DAYS;
82+
// getCurrentDays() is expected to always be non-negative, but guard against a negative
83+
// result from a halted/rewound time anyway rather than return an out-of-range phase.
84+
return phase < 0 ? phase + 1 : phase;
85+
}
86+
87+
private float getCurrentDays() {
88+
return isSunHalted() ? haltedTime : getWorldTime().getDays();
89+
}
7890
/**
7991
* Updates the game perception of the time of day via launching a new OnMiddayEvent(),
8092
* OnDuskEvent(), OnMidnightEvent(), or OnDawnEvent() based on the time of day when

0 commit comments

Comments
 (0)