Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/game/StarNetworkedAnimator.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'source/game/StarNetworkedAnimator.cpp')
-rw-r--r--source/game/StarNetworkedAnimator.cpp1027
1 files changed, 1027 insertions, 0 deletions
diff --git a/source/game/StarNetworkedAnimator.cpp b/source/game/StarNetworkedAnimator.cpp
new file mode 100644
index 0000000..aadbda6
--- /dev/null
+++ b/source/game/StarNetworkedAnimator.cpp
@@ -0,0 +1,1027 @@
+#include "StarNetworkedAnimator.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarIterator.hpp"
+#include "StarParticleDatabase.hpp"
+#include "StarRoot.hpp"
+#include "StarAssets.hpp"
+#include "StarLexicalCast.hpp"
+#include "StarDataStreamExtra.hpp"
+#include "StarRandom.hpp"
+#include "StarGameTypes.hpp"
+
+namespace Star {
+
+NetworkedAnimator::DynamicTarget::~DynamicTarget() {
+ stopAudio();
+}
+
+List<AudioInstancePtr> NetworkedAnimator::DynamicTarget::pullNewAudios() {
+ pendingAudios.exec([this](AudioInstancePtr const& ptr) {
+ Vec2F audioBasePosition = ptr->position().value();
+ currentAudioBasePositions[ptr] = audioBasePosition;
+ ptr->setPosition(position + audioBasePosition);
+ });
+ return take(pendingAudios);
+}
+
+List<Particle> NetworkedAnimator::DynamicTarget::pullNewParticles() {
+ pendingParticles.exec([this](Particle& particle) {
+ particle.position += position;
+ return particle;
+ });
+ return take(pendingParticles);
+}
+
+void NetworkedAnimator::DynamicTarget::stopAudio() {
+ for (auto const& pair : currentAudioBasePositions) {
+ if (pair.first->loops() != 0)
+ pair.first->stop();
+ }
+}
+
+void NetworkedAnimator::DynamicTarget::updatePosition(Vec2F const& p) {
+ clearFinishedAudio();
+ position = p;
+ for (auto& audioPair : currentAudioBasePositions)
+ audioPair.first->setPosition(audioPair.second + p);
+}
+
+void NetworkedAnimator::DynamicTarget::clearFinishedAudio() {
+ for (auto& p : statePersistentSounds) {
+ if (p.second.audio && p.second.audio->finished())
+ p.second.audio.reset();
+ }
+
+ for (auto& p : stateImmediateSounds) {
+ if (p.second.audio && p.second.audio->finished())
+ p.second.audio.reset();
+ }
+
+ for (auto& p : independentSounds)
+ eraseWhere(p.second, [](AudioInstancePtr const& audio) { return audio->finished(); });
+
+ eraseWhere(currentAudioBasePositions, [](pair<AudioInstancePtr, Vec2F> const& pair) {
+ return pair.first->finished();
+ });
+}
+
+NetworkedAnimator::NetworkedAnimator() {
+ m_zoom.set(1.0f);
+ m_flipped.set(false);
+ m_flippedRelativeCenterLine.set(0.0f);
+ m_animationRate.set(1.0f);
+ setupNetStates();
+}
+
+NetworkedAnimator::NetworkedAnimator(Json config, String relativePath) : NetworkedAnimator() {
+ auto& root = Root::singleton();
+
+ if (config.isNull())
+ return;
+
+ if (config.type() == Json::Type::String) {
+ if (relativePath.empty())
+ relativePath = config.toString();
+ config = root.assets()->json(AssetPath::relativeTo(relativePath, config.toString()));
+ } else {
+ if (relativePath.empty())
+ relativePath = "/";
+ }
+
+ m_animatedParts = AnimatedPartSet(config.get("animatedParts", JsonObject()));
+ m_relativePath = AssetPath::directory(relativePath);
+
+ for (auto const& pair : config.get("globalTagDefaults", JsonObject()).iterateObject())
+ setGlobalTag(pair.first, pair.second.toString());
+
+ for (auto const& part : config.get("partTagDefaults", JsonObject()).iterateObject()) {
+ for (auto const& tag : part.second.iterateObject())
+ setPartTag(part.first, tag.first, tag.second.toString());
+ }
+
+ for (auto const& pair : config.get("transformationGroups", JsonObject()).iterateObject()) {
+ auto& tg = m_transformationGroups[pair.first];
+ tg.interpolated = pair.second.getBool("interpolated", false);
+ tg.setAffineTransform(Mat3F::identity());
+ }
+
+ for (auto const& pair : config.get("rotationGroups", JsonObject()).iterateObject()) {
+ String rotationGroupName = pair.first;
+ Json rotationGroupConfig = pair.second;
+ RotationGroup& rotationGroup = m_rotationGroups[move(rotationGroupName)];
+ rotationGroup.angularVelocity = rotationGroupConfig.getFloat("angularVelocity", 0.0f);
+ rotationGroup.rotationCenter = jsonToVec2F(rotationGroupConfig.get("rotationCenter", JsonArray{0, 0}));
+ }
+
+ for (auto const& pair : config.get("particleEmitters", JsonObject()).iterateObject()) {
+ String particleEmitterName = pair.first;
+ Json particleEmitterConfig = pair.second;
+
+ ParticleEmitter& emitter = m_particleEmitters[move(particleEmitterName)];
+ emitter.emissionRate.set(particleEmitterConfig.getFloat("emissionRate", 1.0f));
+ emitter.emissionRateVariance = particleEmitterConfig.getFloat("emissionRateVariance", 0.0f);
+ emitter.offsetRegion.set(particleEmitterConfig.opt("offsetRegion").apply(jsonToRectF).value(RectF::null()));
+ emitter.anchorPart = particleEmitterConfig.optString("anchorPart");
+ emitter.transformationGroups = jsonToStringList(particleEmitterConfig.get("transformationGroups", JsonArray()));
+ emitter.rotationGroup = particleEmitterConfig.optString("rotationGroup");
+ emitter.rotationCenter = particleEmitterConfig.opt("rotationCenter").apply(jsonToVec2F);
+
+ for (auto const& config : particleEmitterConfig.get("particles").iterateArray()) {
+ auto creator = root.particleDatabase()->particleCreator(config.get("particle"), relativePath);
+ unsigned count = config.getUInt("count", 1);
+ Vec2F offset = jsonToVec2F(config.get("offset", JsonArray{0, 0}));
+ bool flip = config.getBool("flip", false);
+ emitter.particleList.append({creator, count, offset, flip});
+ }
+
+ // default to one cycle through the particle list in a burst
+ emitter.burstCount.set(particleEmitterConfig.getUInt("burstCount", 1));
+
+ // default to one of each to preserve current behaviour.
+ emitter.randomSelectCount.set(particleEmitterConfig.getUInt("randomSelectCount", emitter.particleList.size()));
+
+ emitter.active.set(particleEmitterConfig.getBool("active", false));
+ }
+
+ for (auto const& pair : config.get("lights", JsonObject()).iterateObject()) {
+ String lightName = pair.first;
+ Json lightConfig = pair.second;
+
+ Light& light = m_lights[move(lightName)];
+ light.active.set(lightConfig.getBool("active", true));
+ auto lightPosition = lightConfig.opt("position").apply(jsonToVec2F).value();
+ light.xPosition.set(lightPosition[0]);
+ light.yPosition.set(lightPosition[1]);
+ light.color.set(lightConfig.opt("color").apply(jsonToColor).value(Color::White));
+ light.anchorPart = lightConfig.optString("anchorPart");
+ light.transformationGroups = jsonToStringList(lightConfig.get("transformationGroups", JsonArray()));
+ light.rotationGroup = lightConfig.optString("rotationGroup");
+ light.rotationCenter = lightConfig.opt("rotationCenter").apply(jsonToVec2F);
+
+ if (lightConfig.contains("flickerPeriod")) {
+ light.flicker = PeriodicFunction<float>(
+ lightConfig.getFloat("flickerPeriod"),
+ lightConfig.getFloat("flickerMinIntensity", 0.0),
+ lightConfig.getFloat("flickerMaxIntensity", 0.0),
+ lightConfig.getFloat("flickerPeriodVariance", 0.0),
+ lightConfig.getFloat("flickerIntensityVariance", 0.0)
+ );
+ }
+
+ light.pointAngle.set(lightConfig.getFloat("pointAngle", 0.0f) * Constants::deg2rad);
+ light.pointLight = lightConfig.getBool("pointLight", false);
+ light.pointBeam = lightConfig.getFloat("pointBeam", 0.0f);
+ light.beamAmbience = lightConfig.getFloat("beamAmbience", 0.0f);
+ }
+
+ for (auto const& pair : config.get("sounds", JsonObject()).iterateObject()) {
+ String soundName = pair.first;
+ Json soundConfig = pair.second;
+ Sound& sound = m_sounds[move(soundName)];
+ if (soundConfig.isType(Json::Type::Array)) {
+ sound.rangeMultiplier = 1.0f;
+ sound.soundPool.set(jsonToStringList(soundConfig).transformed(bind(&AssetPath::relativeTo, m_relativePath, _1)));
+ sound.volumeTarget.set(1.0f);
+ sound.volumeRampTime.set(0.0f);
+ sound.pitchMultiplierTarget.set(1.0f);
+ sound.pitchMultiplierRampTime.set(0.0f);
+ } else {
+ sound.rangeMultiplier = soundConfig.getFloat("rangeMultiplier", 1.0f);
+
+ auto soundPosition = soundConfig.opt("position").apply(jsonToVec2F).value();
+ sound.xPosition.set(soundPosition[0]);
+ sound.yPosition.set(soundPosition[1]);
+
+ sound.volumeTarget.set(soundConfig.getFloat("volume", 1.0f));
+ sound.volumeRampTime.set(soundConfig.getFloat("volumeRampTime", 0.0f));
+
+ sound.pitchMultiplierTarget.set(soundConfig.getFloat("pitchMultiplier", 1.0f));
+ sound.pitchMultiplierRampTime.set(soundConfig.getFloat("pitchMultiplierRampTime", 0.0f));
+
+ sound.soundPool.set(jsonToStringList(soundConfig.get("pool", JsonArray())).transformed(bind(&AssetPath::relativeTo, m_relativePath, _1)));
+ }
+ }
+
+ for (auto const& pair : config.get("effects", JsonObject()).iterateObject()) {
+ String effectName = pair.first;
+ Json effectConfig = pair.second;
+
+ Effect& effect = m_effects[effectName];
+ effect.type = effectConfig.getString("type");
+ effect.time = effectConfig.getFloat("time", 0.0f);
+ effect.directives = effectConfig.getString("directives");
+ }
+
+ // Sort all the states that contain NetStates handles predictably by key.
+ m_transformationGroups.sortByKey();
+ m_rotationGroups.sortByKey();
+ m_particleEmitters.sortByKey();
+ m_lights.sortByKey();
+ m_sounds.sortByKey();
+ m_effects.sortByKey();
+
+ // Make sure that every state type has an entry in the state info map, and
+ // order it predictably by key.
+ for (auto const& stateType : m_animatedParts.stateTypes())
+ m_stateInfo[stateType];
+
+ m_stateInfo.sortByKey();
+
+ setupNetStates();
+}
+
+NetworkedAnimator::NetworkedAnimator(NetworkedAnimator&& animator) {
+ operator=(move(animator));
+}
+
+NetworkedAnimator::NetworkedAnimator(NetworkedAnimator const& animator) {
+ operator=(animator);
+}
+
+NetworkedAnimator& NetworkedAnimator::operator=(NetworkedAnimator&& animator) {
+ m_relativePath = move(animator.m_relativePath);
+ m_animatedParts = move(animator.m_animatedParts);
+ m_stateInfo = move(animator.m_stateInfo);
+ m_transformationGroups = move(animator.m_transformationGroups);
+ m_rotationGroups = move(animator.m_rotationGroups);
+ m_particleEmitters = move(animator.m_particleEmitters);
+ m_lights = move(animator.m_lights);
+ m_sounds = move(animator.m_sounds);
+ m_effects = move(animator.m_effects);
+ m_processingDirectives = move(animator.m_processingDirectives);
+ m_zoom = move(animator.m_zoom);
+ m_flipped = move(animator.m_flipped);
+ m_flippedRelativeCenterLine = move(animator.m_flippedRelativeCenterLine);
+ m_animationRate = move(animator.m_animationRate);
+ m_globalTags = move(animator.m_globalTags);
+ m_partTags = move(animator.m_partTags);
+
+ setupNetStates();
+
+ return *this;
+}
+
+NetworkedAnimator& NetworkedAnimator::operator=(NetworkedAnimator const& animator) {
+ m_relativePath = animator.m_relativePath;
+ m_animatedParts = animator.m_animatedParts;
+ m_stateInfo = animator.m_stateInfo;
+ m_transformationGroups = animator.m_transformationGroups;
+ m_rotationGroups = animator.m_rotationGroups;
+ m_particleEmitters = animator.m_particleEmitters;
+ m_lights = animator.m_lights;
+ m_sounds = animator.m_sounds;
+ m_effects = animator.m_effects;
+ m_processingDirectives = animator.m_processingDirectives;
+ m_zoom = animator.m_zoom;
+ m_flipped = animator.m_flipped;
+ m_flippedRelativeCenterLine = animator.m_flippedRelativeCenterLine;
+ m_animationRate = animator.m_animationRate;
+ m_globalTags = animator.m_globalTags;
+ m_partTags = animator.m_partTags;
+
+ setupNetStates();
+
+ return *this;
+}
+
+StringList NetworkedAnimator::stateTypes() const {
+ return m_animatedParts.stateTypes();
+}
+
+StringList NetworkedAnimator::states(String const& stateType) const {
+ return m_animatedParts.states(stateType);
+}
+
+bool NetworkedAnimator::setState(String const& stateType, String const& state, bool startNew) {
+ if (m_animatedParts.setActiveState(stateType, state, startNew)) {
+ m_stateInfo[stateType].startedEvent.trigger();
+ return true;
+ } else {
+ return false;
+ }
+}
+
+String NetworkedAnimator::state(String const& stateType) const {
+ return m_animatedParts.activeState(stateType).stateName;
+}
+
+StringList NetworkedAnimator::parts() const {
+ return m_animatedParts.parts();
+}
+
+Json NetworkedAnimator::stateProperty(String const& stateType, String const& propertyName) const {
+ return m_animatedParts.activeState(stateType).properties.value(propertyName);
+}
+
+Json NetworkedAnimator::partProperty(String const& partName, String const& propertyName) const {
+ return m_animatedParts.activePart(partName).properties.value(propertyName);
+}
+
+Mat3F NetworkedAnimator::globalTransformation() const {
+ Mat3F transformation = Mat3F::scaling(m_zoom.get());
+ if (m_flipped.get())
+ transformation = Mat3F::scaling(Vec2F(-1, 1), Vec2F(m_flippedRelativeCenterLine.get(), 0)) * transformation;
+ return transformation;
+}
+
+Mat3F NetworkedAnimator::groupTransformation(StringList const& transformationGroups) const {
+ auto mat = Mat3F::identity();
+ for (auto const& tg : transformationGroups)
+ mat = m_transformationGroups.get(tg).affineTransform() * mat;
+ return mat;
+}
+
+Mat3F NetworkedAnimator::partTransformation(String const& partName) const {
+ auto const& part = m_animatedParts.activePart(partName);
+ Mat3F transformation = Mat3F::identity();
+
+ if (auto offset = part.properties.value("offset").opt().apply(jsonToVec2F))
+ transformation = Mat3F::translation(*offset) * transformation;
+
+ auto transformationGroups = jsonToStringList(part.properties.value("transformationGroups", JsonArray()));
+ transformation = groupTransformation(transformationGroups) * transformation;
+
+ if (auto rotationGroupName = part.properties.value("rotationGroup").optString()) {
+ auto const& rotationGroup = m_rotationGroups.get(*rotationGroupName);
+ Vec2F rotationCenter = part.properties.value("rotationCenter").opt().apply(jsonToVec2F).value(rotationGroup.rotationCenter);
+ transformation = Mat3F::rotation(rotationGroup.currentAngle, rotationCenter) * transformation;
+ }
+
+ if (auto anchorPart = part.properties.ptr("anchorPart"))
+ transformation = partTransformation(anchorPart->toString()) * transformation;
+
+ return transformation;
+}
+
+Mat3F NetworkedAnimator::finalPartTransformation(String const& partName) const {
+ return globalTransformation() * partTransformation(partName);
+}
+
+Maybe<Vec2F> NetworkedAnimator::partPoint(String const& partName, String const& propertyName) const {
+ auto const& part = m_animatedParts.activePart(partName);
+ auto property = part.properties.value(propertyName);
+ if (!property)
+ return {};
+
+ return finalPartTransformation(partName).transformVec2(jsonToVec2F(property));
+}
+
+Maybe<PolyF> NetworkedAnimator::partPoly(String const& partName, String const& propertyName) const {
+ auto const& part = m_animatedParts.activePart(partName);
+ auto property = part.properties.value(propertyName, {});
+ if (!property)
+ return {};
+
+ PolyF poly = jsonToPolyF(property);
+ poly.transform(finalPartTransformation(partName));
+ return poly;
+}
+
+void NetworkedAnimator::setGlobalTag(String tagName, String tagValue) {
+ m_globalTags.set(move(tagName), move(tagValue));
+}
+
+void NetworkedAnimator::setPartTag(String const& partType, String tagName, String tagValue) {
+ m_partTags[partType].set(move(tagName), move(tagValue));
+}
+
+void NetworkedAnimator::setProcessingDirectives(String const& directives) {
+ m_processingDirectives.set(directives);
+}
+
+void NetworkedAnimator::setZoom(float zoom) {
+ m_zoom.set(zoom);
+}
+
+bool NetworkedAnimator::flipped() const {
+ return m_flipped.get();
+}
+
+float NetworkedAnimator::flippedRelativeCenterLine() const {
+ return m_flippedRelativeCenterLine.get();
+}
+
+void NetworkedAnimator::setFlipped(bool flipped, float relativeCenterLine) {
+ m_flipped.set(flipped);
+ m_flippedRelativeCenterLine.set(relativeCenterLine);
+}
+
+void NetworkedAnimator::setAnimationRate(float rate) {
+ m_animationRate.set(rate);
+}
+
+bool NetworkedAnimator::hasRotationGroup(String const& rotationGroup) const {
+ return m_rotationGroups.contains(rotationGroup);
+}
+
+void NetworkedAnimator::rotateGroup(String const& rotationGroup, float targetAngle, bool immediate) {
+ auto& group = m_rotationGroups.get(rotationGroup);
+ group.targetAngle.set(targetAngle);
+
+ if (immediate) {
+ group.currentAngle = targetAngle;
+ group.netImmediateEvent.trigger();
+ }
+}
+
+float NetworkedAnimator::currentRotationAngle(String const& rotationGroup) const {
+ return m_rotationGroups.get(rotationGroup).currentAngle;
+}
+
+bool NetworkedAnimator::hasTransformationGroup(String const& transformationGroup) const {
+ return m_transformationGroups.contains(transformationGroup);
+}
+
+void NetworkedAnimator::translateTransformationGroup(String const& transformationGroup, Vec2F const& translation) {
+ auto& group = m_transformationGroups.get(transformationGroup);
+ group.setAffineTransform(Mat3F::translation(translation) * group.affineTransform());
+}
+
+void NetworkedAnimator::rotateTransformationGroup(
+ String const& transformationGroup, float rotation, Vec2F const& rotationCenter) {
+ auto& group = m_transformationGroups.get(transformationGroup);
+ group.setAffineTransform(Mat3F::rotation(rotation, rotationCenter) * group.affineTransform());
+}
+
+void NetworkedAnimator::scaleTransformationGroup(
+ String const& transformationGroup, float scale, Vec2F const& scaleCenter) {
+ auto& group = m_transformationGroups.get(transformationGroup);
+ group.setAffineTransform(Mat3F::scaling(scale, scaleCenter) * group.affineTransform());
+}
+
+void NetworkedAnimator::scaleTransformationGroup(
+ String const& transformationGroup, Vec2F const& scale, Vec2F const& scaleCenter) {
+ auto& group = m_transformationGroups.get(transformationGroup);
+ group.setAffineTransform(Mat3F::scaling(scale, scaleCenter) * group.affineTransform());
+}
+
+void NetworkedAnimator::transformTransformationGroup(
+ String const& transformationGroup, float a, float b, float c, float d, float tx, float ty) {
+ auto& group = m_transformationGroups.get(transformationGroup);
+ Mat3F transform = Mat3F(a, b, tx, c, d, ty, 0, 0, 1);
+ group.setAffineTransform(transform * group.affineTransform());
+}
+
+void NetworkedAnimator::resetTransformationGroup(String const& transformationGroup) {
+ m_transformationGroups.get(transformationGroup).setAffineTransform(Mat3F::identity());
+}
+
+bool NetworkedAnimator::hasParticleEmitter(String const& emitterName) const {
+ return m_particleEmitters.contains(emitterName);
+}
+
+void NetworkedAnimator::setParticleEmitterActive(String const& emitterName, bool active) {
+ m_particleEmitters.get(emitterName).active.set(active);
+}
+
+void NetworkedAnimator::setParticleEmitterEmissionRate(String const& emitterName, float emissionRate) {
+ m_particleEmitters.get(emitterName).emissionRate.set(emissionRate);
+}
+
+void NetworkedAnimator::setParticleEmitterOffsetRegion(String const& emitterName, RectF const& offsetRegion) {
+ m_particleEmitters.get(emitterName).offsetRegion.set(offsetRegion);
+}
+
+void NetworkedAnimator::setParticleEmitterBurstCount(String const& emitterName, unsigned burstCount) {
+ m_particleEmitters.get(emitterName).burstCount.set(burstCount);
+}
+
+void NetworkedAnimator::burstParticleEmitter(String const& emitterName) {
+ m_particleEmitters.get(emitterName).burstEvent.trigger();
+}
+
+bool NetworkedAnimator::hasLight(String const& lightName) const {
+ return m_lights.contains(lightName);
+}
+
+void NetworkedAnimator::setLightActive(String const& lightName, bool active) {
+ m_lights.get(lightName).active.set(active);
+}
+
+void NetworkedAnimator::setLightPosition(String const& lightName, Vec2F position) {
+ auto& light = m_lights.get(lightName);
+ light.xPosition.set(position[0]);
+ light.yPosition.set(position[1]);
+}
+
+void NetworkedAnimator::setLightColor(String const& lightName, Color color) {
+ m_lights.get(lightName).color.set(color);
+}
+
+void NetworkedAnimator::setLightPointAngle(String const& lightName, float angle) {
+ m_lights.get(lightName).pointAngle.set(angle * Constants::deg2rad);
+}
+
+bool NetworkedAnimator::hasSound(String const& soundName) const {
+ return m_sounds.contains(soundName);
+}
+
+void NetworkedAnimator::setSoundPool(String const& soundName, StringList soundPool) {
+ m_sounds.get(soundName).soundPool.set(move(soundPool));
+}
+
+void NetworkedAnimator::setSoundPosition(String const& soundName, Vec2F const& position) {
+ auto& sound = m_sounds.get(soundName);
+ sound.xPosition.set(position[0]);
+ sound.yPosition.set(position[1]);
+}
+
+void NetworkedAnimator::setSoundVolume(String const& soundName, float volume, float rampTime) {
+ auto& sound = m_sounds.get(soundName);
+ sound.volumeTarget.set(volume);
+ sound.volumeRampTime.set(rampTime);
+}
+
+void NetworkedAnimator::setSoundPitchMultiplier(String const& soundName, float pitchMultiplier, float rampTime) {
+ auto& sound = m_sounds.get(soundName);
+ sound.pitchMultiplierTarget.set(pitchMultiplier);
+ sound.pitchMultiplierRampTime.set(rampTime);
+}
+
+void NetworkedAnimator::playSound(String const& soundName, int loops) {
+ auto& sound = m_sounds.get(soundName);
+ sound.loops.set(loops);
+ sound.signals.send(SoundSignal::Play);
+}
+
+void NetworkedAnimator::stopAllSounds(String const& soundName, float rampTime) {
+ auto& sound = m_sounds.get(soundName);
+ sound.volumeRampTime.set(rampTime);
+ sound.signals.send(SoundSignal::StopAll);
+}
+
+void NetworkedAnimator::setEffectEnabled(String const& effect, bool enabled) {
+ m_effects.get(effect).enabled.set(enabled);
+}
+
+List<Drawable> NetworkedAnimator::drawables(Vec2F const& position) const {
+ List<Drawable> drawables;
+ for (auto& p : drawablesWithZLevel(position))
+ drawables.append(move(p.first));
+ return drawables;
+}
+
+List<pair<Drawable, float>> NetworkedAnimator::drawablesWithZLevel(Vec2F const& position) const {
+ String baseProcessingDirectives = "?";
+ baseProcessingDirectives.append(m_processingDirectives.get());
+ for (auto& pair : m_effects) {
+ auto const& effectState = pair.second;
+
+ if (effectState.enabled.get()) {
+ auto const& effect = m_effects.get(pair.first);
+ if (effect.type == "flash") {
+ if (effectState.timer > effect.time / 2) {
+ baseProcessingDirectives.append("?");
+ baseProcessingDirectives.append(effect.directives);
+ }
+ } else if (effect.type == "directive") {
+ baseProcessingDirectives.append("?");
+ baseProcessingDirectives.append(effect.directives);
+ } else {
+ throw NetworkedAnimatorException(strf("No such NetworkedAnimator effect type '%s'", effect.type));
+ }
+ }
+ }
+
+ List<pair<Drawable, float>> drawables;
+
+ m_animatedParts.forEachActivePart([&](String const& partName, AnimatedPartSet::ActivePartInformation const& activePart) {
+ String image = activePart.properties.value("image").optString().value();
+ bool centered = activePart.properties.value("centered").optBool().value(true);
+ bool fullbright = activePart.properties.value("fullbright").optBool().value(false);
+
+ auto maybeZLevel = activePart.properties.value("zLevel").optFloat();
+ if (m_flipped.get())
+ maybeZLevel = activePart.properties.value("flippedZLevel").optFloat().orMaybe(maybeZLevel);
+ float zLevel = maybeZLevel.value(0.0f);
+
+ String processingDirectives = baseProcessingDirectives;
+ if (auto directives = activePart.properties.value("processingDirectives").optString()) {
+ processingDirectives.append("?");
+ processingDirectives.append(*directives);
+ }
+
+ Maybe<unsigned> frame;
+ if (activePart.activeState) {
+ frame = activePart.activeState->frame;
+
+ if (auto directives = activePart.activeState->properties.value("processingDirectives").optString()) {
+ processingDirectives.append("?");
+ processingDirectives.append(*directives);
+ }
+ }
+
+ auto const& partTags = m_partTags.get(partName);
+ image = image.lookupTags([&](String const& tag) -> String {
+ if (tag == "frame") {
+ if (frame)
+ return toString(*frame + 1);
+ } else if (tag == "frameIndex") {
+ if (frame)
+ return toString(*frame);
+ } else if (auto p = partTags.ptr(tag)) {
+ return *p;
+ } else if (auto p = m_globalTags.ptr(tag)) {
+ return *p;
+ }
+
+ return "default";
+ });
+
+ if (!image.empty() && image[0] != ':' && image[0] != '?') {
+ image = AssetPath::relativeTo(m_relativePath, image) + processingDirectives;
+
+ auto drawable = Drawable::makeImage(move(image), 1.0f / TilePixels, centered, Vec2F());
+ drawable.transform(partTransformation(partName));
+ drawable.transform(globalTransformation());
+ drawable.fullbright = fullbright;
+ drawable.translate(position);
+
+ drawables.append({move(drawable), zLevel});
+ }
+ });
+
+ sort(drawables, [](auto const& a, auto const& b) { return a.second < b.second; });
+
+ return drawables;
+}
+
+List<LightSource> NetworkedAnimator::lightSources(Vec2F const& translate) const {
+ List<LightSource> lightSources;
+ for (auto const& pair : m_lights) {
+ if (!pair.second.active.get())
+ continue;
+
+ Vec2F position = {pair.second.xPosition.get(), pair.second.yPosition.get()};
+ float pointAngle = constrainAngle(pair.second.pointAngle.get());
+ Mat3F transformation = Mat3F::identity();
+ if (pair.second.anchorPart)
+ transformation = partTransformation(*pair.second.anchorPart);
+ transformation = groupTransformation(pair.second.transformationGroups) * transformation;
+ position = transformation.transformVec2(position);
+ pointAngle = transformation.transformAngle(pointAngle);
+ if (pair.second.rotationGroup) {
+ auto const& rg = m_rotationGroups.get(*pair.second.rotationGroup);
+ position = (position - pair.second.rotationCenter.value(rg.rotationCenter)).rotate(rg.currentAngle)
+ + pair.second.rotationCenter.value(rg.rotationCenter);
+ pointAngle += rg.currentAngle;
+ }
+ position = globalTransformation().transformVec2(position);
+ if (m_flipped.get()) {
+ if (pointAngle > 0)
+ pointAngle = Constants::pi / 2 + constrainAngle(Constants::pi / 2 - pointAngle);
+ else
+ pointAngle = -Constants::pi / 2 - constrainAngle(pointAngle + Constants::pi / 2);
+ }
+
+ Color color = pair.second.color.get();
+ if (pair.second.flicker)
+ color.setValue(clamp(color.value() * pair.second.flicker->value(SinWeightOperator<float>()), 0.0f, 1.0f));
+
+ lightSources.append(LightSource{
+ position + translate,
+ color.toRgb(),
+ pair.second.pointLight,
+ pair.second.pointBeam,
+ pointAngle,
+ pair.second.beamAmbience
+ });
+ }
+ return lightSources;
+}
+
+void NetworkedAnimator::update(float dt, DynamicTarget* dynamicTarget) {
+ dt *= m_animationRate.get();
+
+ m_animatedParts.update(dt);
+
+ m_animatedParts.forEachActiveState([&](String const& stateTypeName, AnimatedPartSet::ActiveStateInformation const& activeState) {
+ if (dynamicTarget) {
+ dynamicTarget->clearFinishedAudio();
+
+ String persistentSoundFile = activeState.properties.value("persistentSound", "").toString();
+ if (!persistentSoundFile.empty())
+ persistentSoundFile = AssetPath::relativeTo(m_relativePath, persistentSoundFile);
+
+ auto& activePersistentSound = dynamicTarget->statePersistentSounds[stateTypeName];
+ if (persistentSoundFile != activePersistentSound.file || !activePersistentSound.audio) {
+ activePersistentSound.file = persistentSoundFile;
+ if (activePersistentSound.audio)
+ activePersistentSound.audio->stop(activePersistentSound.stopRampTime);
+
+ if (!persistentSoundFile.empty()) {
+ activePersistentSound.audio = make_shared<AudioInstance>(*Root::singleton().assets()->audio(persistentSoundFile));
+ activePersistentSound.audio->setRangeMultiplier(activeState.properties.value("persistentSoundRangeMultiplier", 1.0f).toFloat());
+ activePersistentSound.audio->setLoops(-1);
+ activePersistentSound.audio->setPosition(globalTransformation().transformVec2(Vec2F()));
+ activePersistentSound.stopRampTime = activeState.properties.value("persistentSoundStopTime", 0.0f).toFloat();
+ dynamicTarget->pendingAudios.append(activePersistentSound.audio);
+ } else {
+ dynamicTarget->statePersistentSounds.remove(stateTypeName);
+ }
+ }
+
+ String immediateSoundFile = activeState.properties.value("immediateSound", "").toString();
+ if (!immediateSoundFile.empty())
+ immediateSoundFile = AssetPath::relativeTo(m_relativePath, immediateSoundFile);
+
+ auto& activeImmediateSound = dynamicTarget->stateImmediateSounds[stateTypeName];
+ if (immediateSoundFile != activeImmediateSound.file) {
+ activeImmediateSound.file = immediateSoundFile;
+ if (!immediateSoundFile.empty()) {
+ activeImmediateSound.audio = make_shared<AudioInstance>(*Root::singleton().assets()->audio(immediateSoundFile));
+ activeImmediateSound.audio->setRangeMultiplier(activeState.properties.value("immediateSoundRangeMultiplier", 1.0f).toFloat());
+ activeImmediateSound.audio->setPosition(globalTransformation().transformVec2(Vec2F()));
+ dynamicTarget->pendingAudios.append(activeImmediateSound.audio);
+ }
+ }
+ }
+
+ if (auto lightsOn = activeState.properties.ptr("lightsOn")) {
+ for (auto const& name : lightsOn->iterateArray())
+ m_lights.get(name.toString()).active.set(true);
+ }
+ if (auto lightsOff = activeState.properties.ptr("lightsOff")) {
+ for (auto const& name : lightsOff->iterateArray())
+ m_lights.get(name.toString()).active.set(false);
+ }
+
+ if (auto particleEmittersOn = activeState.properties.ptr("particleEmittersOn")) {
+ for (auto const& name : particleEmittersOn->iterateArray())
+ m_particleEmitters.get(name.toString()).active.set(true);
+ }
+ if (auto particleEmittersOff = activeState.properties.ptr("particleEmittersOff")) {
+ for (auto const& name : particleEmittersOff->iterateArray())
+ m_particleEmitters.get(name.toString()).active.set(false);
+ }
+ });
+
+ for (auto& pair : m_rotationGroups) {
+ auto& rotationGroup = pair.second;
+ if (rotationGroup.angularVelocity == 0.0f)
+ rotationGroup.currentAngle = rotationGroup.targetAngle.get();
+ else
+ rotationGroup.currentAngle = approachAngle(rotationGroup.targetAngle.get(), rotationGroup.currentAngle, rotationGroup.angularVelocity * dt);
+ }
+
+ if (dynamicTarget) {
+ auto addParticles = [this, dynamicTarget](ParticleEmitter::ParticleConfig const& config, RectF const& offsetRegion, Mat3F const& transformation) {
+ for (unsigned i = 0; i < config.count; ++i) {
+ Particle particle = config.creator();
+ particle.position += config.offset;
+
+ if (!offsetRegion.isNull()) {
+ particle.position[0] += Random::randf() * offsetRegion.width() + offsetRegion.xMin();
+ particle.position[1] += Random::randf() * offsetRegion.height() + offsetRegion.yMin();
+ }
+
+ float speed = particle.velocity.magnitude();
+ particle.velocity = Vec2F::withAngle(transformation.transformAngle(particle.velocity.angle())) * speed;
+ particle.position = transformation.transformVec2(particle.position);
+ particle.rotation = transformation.transformAngle(particle.rotation);
+
+ particle.size *= m_zoom.get();
+ if (config.flip)
+ particle.flip = !particle.flip;
+
+ if (transformation.determinant() < 0) {
+ particle.flip = !particle.flip;
+ particle.rotation += Constants::pi;
+ }
+
+ dynamicTarget->pendingParticles.append(move(particle));
+ }
+ };
+
+ for (auto& pair : m_particleEmitters) {
+ Mat3F transformation = Mat3F::identity();
+ if (pair.second.anchorPart)
+ transformation = partTransformation(*pair.second.anchorPart);
+ transformation = groupTransformation(pair.second.transformationGroups) * transformation;
+
+ if (pair.second.rotationGroup) {
+ auto const& rg = m_rotationGroups.get(*pair.second.rotationGroup);
+ Vec2F rotationCenter = pair.second.rotationCenter.value(rg.rotationCenter);
+ transformation = Mat3F::rotation(rg.currentAngle, rotationCenter) * transformation;
+ }
+
+ transformation = globalTransformation() * transformation;
+
+ // assume we emit no particles
+ unsigned numEmissionCycles = 0;
+
+ if (pair.second.active.get()) {
+ pair.second.timer = min(pair.second.timer, 1.0f / (pair.second.emissionRate.get() + pair.second.emissionRateVariance));
+ if (pair.second.timer <= 0.0f) {
+ // timer causes us to emit one set
+ ++numEmissionCycles;
+ pair.second.timer = 1.0f / (pair.second.emissionRate.get() + Random::randf(-pair.second.emissionRateVariance, pair.second.emissionRateVariance));
+ } else {
+ pair.second.timer -= dt;
+ }
+ }
+
+ auto bursts = pair.second.burstEvent.pullOccurrences();
+ for (uint64_t i = 0; i < bursts; ++i)
+ numEmissionCycles += pair.second.burstCount.get();
+
+ if (numEmissionCycles > 0) {
+ RectF rect = pair.second.offsetRegion.get();
+ unsigned numToSelect = pair.second.randomSelectCount.get();
+
+ for (unsigned i = 0; i < numEmissionCycles; ++i) {
+ if (numToSelect >= pair.second.particleList.size()) {
+ for (auto const& particleConfig : pair.second.particleList)
+ addParticles(particleConfig, rect, transformation);
+ } else {
+ List<ParticleEmitter::ParticleConfig> shuffledList = pair.second.particleList;
+ Random::shuffle(shuffledList);
+
+ for (unsigned i = 0; i < numToSelect; ++i)
+ addParticles(shuffledList.at(i), rect, transformation);
+ }
+ }
+ }
+ }
+
+ for (auto& pair : m_sounds) {
+ auto const& soundName = pair.first;
+ auto& soundEntry = pair.second;
+
+ for (auto signal : soundEntry.signals.receive()) {
+ if (signal == SoundSignal::StopAll) {
+ for (auto& sound : take(dynamicTarget->independentSounds[soundName]))
+ sound->stop(soundEntry.volumeRampTime.get());
+ } else if (signal == SoundSignal::Play) {
+ String soundFile = Random::randValueFrom(soundEntry.soundPool.get());
+ if (!soundFile.empty()) {
+ auto sound = make_shared<AudioInstance>(*Root::singleton().assets()->audio(soundFile));
+ sound->setRangeMultiplier(soundEntry.rangeMultiplier);
+ sound->setLoops(soundEntry.loops.get());
+ sound->setPosition(globalTransformation().transformVec2(Vec2F(soundEntry.xPosition.get(), soundEntry.yPosition.get())));
+ sound->setVolume(soundEntry.volumeTarget.get(), soundEntry.volumeRampTime.get());
+ sound->setPitchMultiplier(soundEntry.pitchMultiplierTarget.get(), soundEntry.pitchMultiplierRampTime.get());
+ dynamicTarget->independentSounds[soundName].append(sound);
+ dynamicTarget->pendingAudios.append(move(sound));
+ }
+ }
+ }
+
+ // Update all still active independent sounds position, volume, and speed
+ for (auto const& activeIndependentSound : dynamicTarget->independentSounds.value(soundName)) {
+ if (auto basePosition = dynamicTarget->currentAudioBasePositions.ptr(activeIndependentSound))
+ *basePosition = globalTransformation().transformVec2(Vec2F(soundEntry.xPosition.get(), soundEntry.yPosition.get()));
+ activeIndependentSound->setVolume(soundEntry.volumeTarget.get(), soundEntry.volumeRampTime.get());
+ activeIndependentSound->setPitchMultiplier(soundEntry.pitchMultiplierTarget.get(), soundEntry.pitchMultiplierRampTime.get());
+ }
+ }
+ }
+
+ for (auto& pair : m_lights) {
+ if (pair.second.flicker)
+ pair.second.flicker->update(dt);
+ }
+
+ for (auto& pair : m_effects) {
+ if (pair.second.enabled.get()) {
+ auto& effect = pair.second;
+ if (effect.timer <= 0.0f)
+ effect.timer = effect.time;
+ else
+ effect.timer -= dt;
+ }
+ }
+}
+
+void NetworkedAnimator::finishAnimations() {
+ m_animatedParts.finishAnimations();
+}
+
+Mat3F NetworkedAnimator::TransformationGroup::affineTransform() const {
+ return Mat3F(
+ xScale.get() * cos(xShear.get()), xScale.get() * sin(xShear.get()), xTranslation.get(),
+ yScale.get() * sin(yShear.get()), yScale.get() * cos(yShear.get()), yTranslation.get(),
+ 0, 0, 1
+ );
+}
+
+void NetworkedAnimator::TransformationGroup::setAffineTransform(Mat3F const& matrix) {
+ xTranslation.set(matrix[0][2]);
+ yTranslation.set(matrix[1][2]);
+ xScale.set(sqrt(square(matrix[0][0]) + square(matrix[0][1])));
+ yScale.set(sqrt(square(matrix[1][0]) + square(matrix[1][1])));
+ xShear.set(atan2(matrix[0][1], matrix[0][0]));
+ yShear.set(atan2(matrix[1][0], matrix[1][1]));
+}
+
+void NetworkedAnimator::setupNetStates() {
+ clearNetElements();
+
+ addNetElement(&m_processingDirectives);
+ addNetElement(&m_zoom);
+ addNetElement(&m_flipped);
+ addNetElement(&m_flippedRelativeCenterLine);
+
+ addNetElement(&m_animationRate);
+ m_animationRate.setInterpolator(lerp<float, float>);
+
+ addNetElement(&m_globalTags);
+
+ for (auto const& part : sorted(m_animatedParts.parts()))
+ addNetElement(&m_partTags[part]);
+
+ for (auto& pair : m_stateInfo) {
+ addNetElement(&pair.second.stateIndex);
+ addNetElement(&pair.second.startedEvent);
+ }
+
+ for (auto& pair : m_transformationGroups) {
+ addNetElement(&pair.second.xTranslation);
+ addNetElement(&pair.second.yTranslation);
+ addNetElement(&pair.second.xScale);
+ addNetElement(&pair.second.yScale);
+ addNetElement(&pair.second.xShear);
+ addNetElement(&pair.second.yShear);
+
+ if (pair.second.interpolated) {
+ pair.second.xTranslation.setInterpolator(lerp<float, float>);
+ pair.second.yTranslation.setInterpolator(lerp<float, float>);
+ pair.second.xScale.setInterpolator(lerp<float, float>);
+ pair.second.yScale.setInterpolator(lerp<float, float>);
+ pair.second.xShear.setInterpolator(angleLerp<float, float>);
+ pair.second.yShear.setInterpolator(angleLerp<float, float>);
+ }
+ }
+
+ for (auto& pair : m_rotationGroups) {
+ addNetElement(&pair.second.targetAngle);
+ addNetElement(&pair.second.netImmediateEvent);
+ }
+
+ for (auto& pair : m_particleEmitters) {
+ addNetElement(&pair.second.emissionRate);
+ addNetElement(&pair.second.burstCount);
+ addNetElement(&pair.second.randomSelectCount);
+ addNetElement(&pair.second.offsetRegion);
+ addNetElement(&pair.second.active);
+ addNetElement(&pair.second.burstEvent);
+
+ pair.second.burstEvent.setIgnoreOccurrencesOnNetLoad(true);
+ }
+
+ for (auto& pair : m_lights) {
+ addNetElement(&pair.second.active);
+ addNetElement(&pair.second.xPosition);
+ addNetElement(&pair.second.yPosition);
+ addNetElement(&pair.second.color);
+ addNetElement(&pair.second.pointAngle);
+
+ pair.second.xPosition.setFixedPointBase(0.0125f);
+ pair.second.yPosition.setFixedPointBase(0.0125f);
+ pair.second.pointAngle.setFixedPointBase(0.01f);
+
+ pair.second.xPosition.setInterpolator(lerp<float, float>);
+ pair.second.yPosition.setInterpolator(lerp<float, float>);
+ pair.second.pointAngle.setInterpolator(angleLerp<float, float>);
+ }
+
+ for (auto& pair : m_sounds) {
+ addNetElement(&pair.second.soundPool);
+ addNetElement(&pair.second.xPosition);
+ addNetElement(&pair.second.yPosition);
+ addNetElement(&pair.second.volumeTarget);
+ addNetElement(&pair.second.volumeRampTime);
+ addNetElement(&pair.second.pitchMultiplierTarget);
+ addNetElement(&pair.second.pitchMultiplierRampTime);
+ addNetElement(&pair.second.loops);
+ addNetElement(&pair.second.signals);
+
+ pair.second.xPosition.setFixedPointBase(0.0125f);
+ pair.second.yPosition.setFixedPointBase(0.0125f);
+
+ pair.second.xPosition.setInterpolator(lerp<float, float>);
+ pair.second.yPosition.setInterpolator(lerp<float, float>);
+ }
+
+ for (auto& pair : m_effects)
+ addNetElement(&pair.second.enabled);
+}
+
+void NetworkedAnimator::netElementsNeedLoad(bool initial) {
+ for (auto& pair : m_stateInfo) {
+ if (pair.second.startedEvent.pullOccurred() || initial)
+ m_animatedParts.setActiveStateIndex(pair.first, pair.second.stateIndex.get(), true);
+ }
+
+ for (auto& pair : m_rotationGroups) {
+ if (pair.second.netImmediateEvent.pullOccurred() || initial)
+ pair.second.currentAngle = pair.second.targetAngle.get();
+ }
+}
+
+void NetworkedAnimator::netElementsNeedStore() {
+ for (auto& pair : m_stateInfo)
+ pair.second.stateIndex.set(m_animatedParts.activeStateIndex(pair.first));
+}
+
+}