2023-06-20 04:33:09 +00:00
|
|
|
#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);
|
|
|
|
}
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
void ServerWeather::update(double dt) {
|
|
|
|
spawnWeatherProjectiles(dt);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
void ClientWeather::update(double dt) {
|
|
|
|
m_currentTime += dt;
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
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)
|
2023-07-20 14:58:49 +00:00
|
|
|
spawnWeatherParticles(RectF(m_visibleRegion), dt);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|