osb/source/game/StarWeather.cpp
Kae 4b0bc220e4 Support for changing the game's timescale
Context-specific (like per-world) timescales can also be added later
2023-07-21 00:58:49 +10:00

365 lines
13 KiB
C++

#include "StarWeather.hpp"
#include "StarIterator.hpp"
#include "StarDataStreamExtra.hpp"
#include "StarRoot.hpp"
#include "StarTime.hpp"
#include "StarAssets.hpp"
#include "StarProjectileDatabase.hpp"
#include "StarProjectile.hpp"
#include "StarBiomeDatabase.hpp"
namespace Star {
ServerWeather::ServerWeather() {
m_undergroundLevel = 0.0f;
m_currentWeatherIndex = NPos;
m_currentWeatherIntensity = 0.0f;
m_currentWind = 0.0f;
m_currentTime = 0.0;
m_lastWeatherChangeTime = 0.0;
m_nextWeatherChangeTime = 0.0;
m_netGroup.addNetElement(&m_weatherPoolNetState);
m_netGroup.addNetElement(&m_undergroundLevelNetState);
m_netGroup.addNetElement(&m_currentWeatherIndexNetState);
m_netGroup.addNetElement(&m_currentWeatherIntensityNetState);
m_netGroup.addNetElement(&m_currentWindNetState);
}
void ServerWeather::setup(WeatherPool weatherPool, float undergroundLevel, WorldGeometry worldGeometry,
WeatherEffectsActiveQuery weatherEffectsActiveQuery) {
m_weatherPool = weatherPool;
m_undergroundLevel = undergroundLevel;
m_worldGeometry = worldGeometry;
m_weatherEffectsActiveQuery = weatherEffectsActiveQuery;
m_currentWeatherIndex = NPos;
m_currentWeatherType = {};
m_currentTime = 0.0;
m_lastWeatherChangeTime = 0.0;
m_nextWeatherChangeTime = 0.0;
}
void ServerWeather::setReferenceClock(ClockConstPtr referenceClock) {
m_referenceClock = move(referenceClock);
if (m_referenceClock)
m_clockTrackingTime = m_referenceClock->time();
else
m_clockTrackingTime = {};
}
void ServerWeather::setClientVisibleRegions(List<RectI> regions) {
m_clientVisibleRegions = move(regions);
}
pair<ByteArray, uint64_t> ServerWeather::writeUpdate(uint64_t fromVersion) {
setNetStates();
return m_netGroup.writeNetState(fromVersion);
}
void ServerWeather::update(double dt) {
spawnWeatherProjectiles(dt);
if (m_referenceClock) {
double clockTime = m_referenceClock->time();
if (!m_clockTrackingTime) {
m_clockTrackingTime = clockTime;
} else {
// If our reference clock is set, and we have a valid tracking time, then
// the dt should be driven by the reference clock.
dt = clockTime - *m_clockTrackingTime;
m_clockTrackingTime = clockTime;
}
}
m_currentTime += dt;
if (!m_weatherPool.empty()) {
auto assets = Root::singleton().assets();
double weatherCooldownTime = assets->json("/weather.config:weatherCooldownTime").toDouble();
double weatherWarmupTime = assets->json("/weather.config:weatherWarmupTime").toDouble();
if (m_currentTime >= m_nextWeatherChangeTime) {
m_currentWeatherIndex = m_weatherPool.selectIndex();
if (m_currentWeatherIndex == NPos)
m_currentWeatherType = {};
else
m_currentWeatherType = Root::singleton().biomeDatabase()->weatherType(m_weatherPool.item(m_currentWeatherIndex));
m_lastWeatherChangeTime = m_nextWeatherChangeTime;
m_nextWeatherChangeTime = m_currentTime + Random::randd(m_currentWeatherType->duration[0], m_currentWeatherType->duration[1]);
// TODO: For now just set the wind at maximum either left or right, nothing exciting.
m_currentWind = m_currentWeatherType->maximumWind * (Random::randb() ? 1 : -1);
}
m_currentWeatherIntensity = min(clamp((m_currentTime - m_lastWeatherChangeTime) / weatherWarmupTime, 0.0, 1.0),
clamp((m_nextWeatherChangeTime - m_currentTime) / weatherCooldownTime, 0.0, 1.0));
} else {
m_currentWeatherIndex = NPos;
m_currentWeatherType = {};
}
}
float ServerWeather::wind() const {
return m_currentWind * m_currentWeatherIntensity;
}
float ServerWeather::weatherIntensity() const {
return m_currentWeatherIntensity;
}
StringList ServerWeather::statusEffects() const {
if (m_currentWeatherType && m_currentWeatherIntensity == 1.0)
return m_currentWeatherType->statusEffects;
return {};
}
List<ProjectilePtr> ServerWeather::pullNewProjectiles() {
return take(m_newProjectiles);
}
void ServerWeather::setNetStates() {
m_weatherPoolNetState.set(DataStreamBuffer::serializeContainer(m_weatherPool.items()));
m_undergroundLevelNetState.set(m_undergroundLevel);
m_currentWeatherIndexNetState.set(m_currentWeatherIndex);
m_currentWeatherIntensityNetState.set(m_currentWeatherIntensity);
m_currentWindNetState.set(m_currentWind);
}
void ServerWeather::spawnWeatherProjectiles(float dt) {
if (!m_currentWeatherType || m_clientVisibleRegions.empty())
return;
auto projectileDatabase = Root::singleton().projectileDatabase();
// TODO: The complexity of this method is TERRIBLE, if this becomes a problem
// for any reason there are large numbers of ways to make this much better,
// but this was the lazy, simple-ish, and clear (hah) way.
for (auto const& projectileConfig : m_currentWeatherType->projectiles) {
// Gather all the tops of the client regions together with the proper
// padding, splitting at the world wrap boundary.
List<pair<Vec2I, int>> baseSpawnRegions;
for (auto const& clientRegion : m_clientVisibleRegions) {
Vec2I baseRegion = {clientRegion.xMin() - projectileConfig.spawnHorizontalPad, clientRegion.xMax() + projectileConfig.spawnHorizontalPad};
int height = clientRegion.yMax();
for (auto const& region : m_worldGeometry.splitXRegion(baseRegion))
baseSpawnRegions.append({region, height});
}
// We are going to have to eliminate vertically redundant sections of
// spawning regions, so gather up every left and right edge of a spawn
// region is a "split point"
List<int> splitPoints;
for (auto const& baseSpawnRegion : baseSpawnRegions) {
splitPoints.append(baseSpawnRegion.first[0]);
splitPoints.append(baseSpawnRegion.first[1]);
}
// Split every spawn region on every split point.
List<pair<Vec2I, int>> splitSpawnRegions;
for (auto const& baseSpawnRegion : baseSpawnRegions) {
List<Vec2I> regions = {baseSpawnRegion.first};
for (auto splitPoint : splitPoints) {
auto prevRegions = take(regions);
for (auto const& region : prevRegions) {
if (splitPoint > region[0] && splitPoint < region[1]) {
regions.append({region[0], splitPoint});
regions.append({splitPoint, region[1]});
} else {
regions.append(region);
}
}
}
for (auto const& region : regions)
splitSpawnRegions.append({region, baseSpawnRegion.second});
}
// Sort the split spawn regions by leftmost point then height, preparing to
// remove the lower overlapping sections.
sort(splitSpawnRegions,
[](pair<Vec2I, int> const& lhs, pair<Vec2I, int> rhs) {
return tie(lhs.first[0], lhs.second) < tie(rhs.first[0], rhs.second);
});
// For each region, at this point, if the region to the right shares the
// same starting X, because we've split up each region on each possible
// overlapping point, then they totally overlap. The lower region (which
// should come before in the list) is totally redundant and should be
// removed.
auto sit = makeSMutableIterator(splitSpawnRegions);
while (sit.hasNext()) {
auto const& leftRegion = sit.next();
if (sit.hasNext()) {
auto const& rightRegion = sit.peekNext();
if (leftRegion.first[0] == rightRegion.first[0])
sit.remove();
}
}
for (auto const& spawnRegion : splitSpawnRegions) {
RectF spawnRect = RectF(spawnRegion.first[0],
spawnRegion.second,
spawnRegion.first[1],
spawnRegion.second + projectileConfig.spawnAboveRegion);
// Figure out a good target value based on the rate per x tile, making
// sure to handle very low count values appropriately on average.
float count = projectileConfig.ratePerX * spawnRect.width() * dt * m_currentWeatherIntensity;
if (Random::randf() > fpart(count))
count = floor(count);
else
count = ceil(count);
for (int i = 0; i < count; ++i) {
Vec2F position = {Random::randf() * spawnRect.width() + spawnRect.xMin(), Random::randf() * spawnRect.height() + spawnRect.yMin()};
if (position[1] > m_undergroundLevel && (!m_weatherEffectsActiveQuery || m_weatherEffectsActiveQuery(Vec2I::floor(position)))) {
// Make sure not to spawn projectiles if they intersect any client
// visible region.
bool intersectsVisibleRegion = false;
for (auto const& visibleRegion : m_clientVisibleRegions) {
if (RectF(visibleRegion).contains(position)) {
intersectsVisibleRegion = true;
break;
}
}
if (!intersectsVisibleRegion) {
auto newProjectile = projectileDatabase->createProjectile(projectileConfig.projectile, projectileConfig.parameters);
newProjectile->setInitialPosition(position);
newProjectile->setInitialVelocity(projectileConfig.velocity + Vec2F(projectileConfig.windAffectAmount * wind(), 0));
newProjectile->setTeam(EntityDamageTeam(TeamType::Environment));
m_newProjectiles.append(newProjectile);
}
}
}
}
}
}
ClientWeather::ClientWeather() {
m_undergroundLevel = 0.0f;
m_currentWeatherIndex = NPos;
m_currentWeatherIntensity = 0.0f;
m_currentWind = 0.0f;
m_currentTime = 0.0;
m_netGroup.addNetElement(&m_weatherPoolNetState);
m_netGroup.addNetElement(&m_undergroundLevelNetState);
m_netGroup.addNetElement(&m_currentWeatherIndexNetState);
m_netGroup.addNetElement(&m_currentWeatherIntensityNetState);
m_netGroup.addNetElement(&m_currentWindNetState);
}
void ClientWeather::setup(WorldGeometry worldGeometry, WeatherEffectsActiveQuery weatherEffectsActiveQuery) {
m_worldGeometry = worldGeometry;
m_weatherEffectsActiveQuery = weatherEffectsActiveQuery;
m_currentTime = 0.0;
}
void ClientWeather::readUpdate(ByteArray data) {
if (!data.empty()) {
m_netGroup.readNetState(move(data));
getNetStates();
}
}
void ClientWeather::setVisibleRegion(RectI visibleRegion) {
m_visibleRegion = visibleRegion;
}
void ClientWeather::update(double dt) {
m_currentTime += dt;
if (m_currentWeatherIndex == NPos) {
m_currentWeatherType = {};
} else {
if (m_visibleRegion.yMax() > m_undergroundLevel)
m_currentWeatherType = Root::singleton().biomeDatabase()->weatherType(m_weatherPool.item(m_currentWeatherIndex));
else
m_currentWeatherType = {};
}
if (m_currentWeatherType)
spawnWeatherParticles(RectF(m_visibleRegion), dt);
}
float ClientWeather::wind() const {
return m_currentWind * m_currentWeatherIntensity;
}
float ClientWeather::weatherIntensity() const {
return m_currentWeatherIntensity;
}
StringList ClientWeather::statusEffects() const {
if (m_currentWeatherIntensity == 1.0 && m_currentWeatherType)
return m_currentWeatherType->statusEffects;
return {};
}
List<Particle> ClientWeather::pullNewParticles() {
return take(m_particles);
}
StringList ClientWeather::weatherTrackOptions() const {
if (m_currentWeatherType)
return m_currentWeatherType->weatherNoises;
return {};
}
void ClientWeather::getNetStates() {
if (m_weatherPoolNetState.pullUpdated())
m_weatherPool = WeatherPool(DataStreamBuffer::deserializeContainer<WeatherPool::ItemsList>(m_weatherPoolNetState.get()));
m_undergroundLevel = m_undergroundLevelNetState.get();
m_currentWeatherIndex = m_currentWeatherIndexNetState.get();
m_currentWeatherIntensity = m_currentWeatherIntensityNetState.get();
m_currentWind = m_currentWindNetState.get();
}
void ClientWeather::spawnWeatherParticles(RectF newClientRegion, float dt) {
if (!m_currentWeatherType)
return;
for (auto const& particleConfig : m_currentWeatherType->particles) {
// Move client region to same wrap region as newClientRegion
RectF visibleRegion(m_worldGeometry.nearestTo(newClientRegion.min(), m_lastParticleVisibleRegion.min()),
m_worldGeometry.nearestTo(newClientRegion.min(), m_lastParticleVisibleRegion.max()));
Vec2F targetVelocity = particleConfig.particle.velocity + Vec2F(wind(), 0);
float angleChange = Vec2F::angleBetween2(Vec2F(0, 1), targetVelocity);
visibleRegion.translate(targetVelocity * dt);
for (auto const& renderZone : newClientRegion.subtract(visibleRegion)) {
float count = particleConfig.density * renderZone.width() * renderZone.height() * m_currentWeatherIntensity;
if (Random::randf() > fpart(count))
count = std::floor(count);
else
count = std::ceil(count);
for (int i = 0; i < count; ++i) {
auto newParticle = particleConfig.particle;
float x = Random::randf() * renderZone.width() + renderZone.xMin();
float y = Random::randf() * renderZone.height() + renderZone.yMin();
newParticle.position += m_worldGeometry.xwrap(Vec2F(x, y));
newParticle.velocity = targetVelocity;
if (y > m_undergroundLevel && (!m_weatherEffectsActiveQuery || m_weatherEffectsActiveQuery(Vec2I::floor(newParticle.position)))) {
if (particleConfig.autoRotate)
newParticle.rotation += angleChange;
m_particles.append(move(newParticle));
}
}
}
}
m_lastParticleVisibleRegion = newClientRegion;
}
}