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

summaryrefslogtreecommitdiff
path: root/source/game/StarProjectile.cpp
diff options
context:
space:
mode:
authorKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
committerKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
commit6352e8e3196f78388b6c771073f9e03eaa612673 (patch)
treee23772f79a7fbc41bc9108951e9e136857484bf4 /source/game/StarProjectile.cpp
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/game/StarProjectile.cpp')
-rw-r--r--source/game/StarProjectile.cpp989
1 files changed, 989 insertions, 0 deletions
diff --git a/source/game/StarProjectile.cpp b/source/game/StarProjectile.cpp
new file mode 100644
index 0000000..d094797
--- /dev/null
+++ b/source/game/StarProjectile.cpp
@@ -0,0 +1,989 @@
+#include "StarProjectile.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarWorld.hpp"
+#include "StarLogging.hpp"
+#include "StarRoot.hpp"
+#include "StarDataStreamExtra.hpp"
+#include "StarMaterialDatabase.hpp"
+#include "StarLiquidsDatabase.hpp"
+#include "StarMonster.hpp"
+#include "StarStoredFunctions.hpp"
+#include "StarDamageDatabase.hpp"
+#include "StarProjectileDatabase.hpp"
+#include "StarAssets.hpp"
+#include "StarItemDrop.hpp"
+#include "StarIterator.hpp"
+#include "StarConfigLuaBindings.hpp"
+#include "StarEntityLuaBindings.hpp"
+#include "StarMovementControllerLuaBindings.hpp"
+#include "StarParticleDatabase.hpp"
+
+namespace Star {
+
+Projectile::Projectile(ProjectileConfigPtr const& config, Json const& parameters) {
+ m_config = config;
+ m_parameters = parameters;
+
+ setup();
+}
+
+Projectile::Projectile(ProjectileConfigPtr const& config, DataStreamBuffer& data) {
+ m_config = config;
+ data.read(m_parameters);
+ setup();
+
+ EntityId sourceEntity = data.readVlqI();
+ bool trackSourceEntity = data.read<bool>();
+ setSourceEntity(sourceEntity, trackSourceEntity);
+
+ data.read(m_initialSpeed);
+ data.read(m_powerMultiplier);
+ setTeam(data.read<EntityDamageTeam>());
+}
+
+ByteArray Projectile::netStore() const {
+ DataStreamBuffer ds;
+
+ ds.write(m_config->typeName);
+ ds.write(m_parameters);
+
+ ds.viwrite(m_sourceEntity);
+ ds.write(m_trackSourceEntity);
+
+ ds.write(m_initialSpeed);
+ ds.write(m_powerMultiplier);
+ ds.write(getTeam());
+
+ return ds.data();
+}
+
+EntityType Projectile::entityType() const {
+ return EntityType::Projectile;
+}
+
+void Projectile::init(World* world, EntityId entityId, EntityMode mode) {
+ Entity::init(world, entityId, mode);
+ m_movementController->init(world);
+ m_movementController->setIgnorePhysicsEntities({entityId});
+
+ m_timeToLive = m_parameters.getFloat("timeToLive", m_config->timeToLive);
+ setSourceEntity(m_sourceEntity, m_trackSourceEntity);
+
+ m_periodicActions.clear();
+ if (m_parameters.contains("periodicActions")) {
+ for (auto const& c : m_parameters.getArray("periodicActions", {}))
+ m_periodicActions.append(make_tuple(GameTimer(c.getFloat("time")), c.getBool("repeat", true), c));
+ } else {
+ for (auto const& periodicAction : m_config->periodicActions)
+ m_periodicActions.append(make_tuple(GameTimer(get<0>(periodicAction)), get<1>(periodicAction), get<2>(periodicAction)));
+ }
+
+ if (isMaster()) {
+ if (!m_config->scripts.empty()) {
+ m_scriptComponent.setScripts(m_config->scripts);
+ m_scriptComponent.setUpdateDelta(m_parameters.getUInt("scriptDelta", m_config->config.getUInt("scriptDelta", 1)));
+
+ m_scriptComponent.addCallbacks("projectile", makeProjectileCallbacks());
+ m_scriptComponent.addCallbacks("config", LuaBindings::makeConfigCallbacks([this](String const& name, Json const& def) {
+ return m_parameters.query(name, m_config->config.query(name, def));
+ }));
+ m_scriptComponent.addCallbacks("entity", LuaBindings::makeEntityCallbacks(this));
+ m_scriptComponent.addCallbacks("mcontroller", LuaBindings::makeMovementControllerCallbacks(m_movementController.get()));
+ m_scriptComponent.init(world);
+ }
+ }
+ m_travelLine = Line2F(position(), position());
+
+ if (auto referenceVelocity = m_parameters.opt("referenceVelocity"))
+ setReferenceVelocity(referenceVelocity.apply(jsonToVec2F));
+
+ if (world->isClient() && !m_persistentAudioFile.empty()) {
+ m_persistentAudio = make_shared<AudioInstance>(*Root::singleton().assets()->audio(m_persistentAudioFile));
+ m_persistentAudio->setLoops(-1);
+ m_persistentAudio->setPosition(position());
+ m_pendingRenderables.append(m_persistentAudio);
+ }
+}
+
+void Projectile::uninit() {
+ if (m_persistentAudio)
+ m_persistentAudio->stop();
+ m_movementController->uninit();
+ if (isMaster()) {
+ if (!m_config->scripts.empty()) {
+ m_scriptComponent.uninit();
+ m_scriptComponent.removeCallbacks("projectile");
+ m_scriptComponent.removeCallbacks("config");
+ m_scriptComponent.removeCallbacks("entity");
+ m_scriptComponent.removeCallbacks("mcontroller");
+ }
+ }
+ Entity::uninit();
+}
+
+String Projectile::typeName() const {
+ return m_config->typeName;
+}
+
+String Projectile::description() const {
+ return m_config->description;
+}
+
+Vec2F Projectile::position() const {
+ return m_movementController->position();
+}
+
+RectF Projectile::metaBoundBox() const {
+ return m_config->boundBox;
+}
+
+pair<ByteArray, uint64_t> Projectile::writeNetState(uint64_t fromVersion) {
+ return m_netGroup.writeNetState(fromVersion);
+}
+
+void Projectile::readNetState(ByteArray data, float interpolationTime) {
+ m_netGroup.readNetState(move(data), interpolationTime);
+}
+
+void Projectile::enableInterpolation(float extrapolationHint) {
+ m_netGroup.enableNetInterpolation(extrapolationHint);
+}
+
+void Projectile::disableInterpolation() {
+ m_netGroup.disableNetInterpolation();
+}
+
+bool Projectile::shouldDestroy() const {
+ if (auto res = m_scriptComponent.invoke<bool>("shouldDestroy"))
+ return *res;
+ return m_timeToLive <= 0.0f;
+}
+
+void Projectile::destroy(RenderCallback* renderCallback) {
+ for (auto const& action : m_parameters.getArray("actionOnReap", m_config->actionOnReap))
+ processAction(action);
+
+ if (m_collision) {
+ for (auto const& action : m_parameters.getArray("actionOnHit", m_config->actionOnHit))
+ processAction(action);
+ } else {
+ for (auto const& action : m_parameters.getArray("actionOnTimeout", m_config->actionOnTimeout))
+ processAction(action);
+ }
+
+ if (renderCallback)
+ renderPendingRenderables(renderCallback);
+
+ m_scriptComponent.invoke("destroy");
+}
+
+List<DamageSource> Projectile::damageSources() const {
+ if (m_onlyHitTerrain)
+ return {};
+
+ float time_per_frame = m_animationCycle / m_config->frameNumber;
+ if ((m_config->intangibleWindup && m_animationTimer < time_per_frame * m_config->windupFrames)
+ || (m_config->intangibleWinddown && m_timeToLive < time_per_frame * m_config->winddownFrames))
+ return {};
+
+ EntityDamageTeam sourceTeam = getTeam();
+
+ auto statusEffects = m_config->statusEffects;
+ statusEffects.appendAll(m_parameters.getArray("statusEffects", {}).transformed(jsonToEphemeralStatusEffect));
+
+ float knockbackMagnitude = m_parameters.getFloat("knockback", m_config->knockback);
+
+ DamageSource::Knockback knockback;
+ if (m_parameters.getBool("knockbackDirectional", m_config->knockbackDirectional))
+ knockback = Vec2F::withAngle(m_movementController->rotation()) * knockbackMagnitude;
+ else
+ knockback = knockbackMagnitude;
+
+ List<DamageSource> res;
+ auto addDamageSource = [&](DamageSource::DamageArea damageArea) {
+ res.append(DamageSource(m_damageType, damageArea, m_power * m_powerMultiplier, true, m_sourceEntity, sourceTeam,
+ m_damageRepeatGroup, m_damageRepeatTimeout, m_damageKind, statusEffects, knockback, m_rayCheckToSource));
+ };
+
+ Vec2F positionDelta = world()->geometry().diff(m_travelLine.min(), m_travelLine.max());
+ static float const MinimumDamageLineDelta = 0.1f;
+ bool useDamageLine = positionDelta.magnitudeSquared() >= square(MinimumDamageLineDelta);
+ if (useDamageLine)
+ addDamageSource(Line2F(positionDelta, Vec2F()));
+
+ if (!m_config->damagePoly.isNull()) {
+ PolyF damagePoly = m_config->damagePoly;
+ if (m_config->flippable) {
+ auto angleSide = getAngleSide(m_movementController->rotation(), true);
+ if (angleSide.second == Direction::Left)
+ damagePoly.flipHorizontal(0);
+ damagePoly.rotate(angleSide.first);
+ } else {
+ damagePoly.rotate(m_movementController->rotation());
+ }
+ addDamageSource(damagePoly);
+ } else if (!useDamageLine) {
+ addDamageSource(PolyF(RectF::withCenter(Vec2F(), Vec2F::filled(MinimumDamageLineDelta))));
+ }
+
+ return res;
+}
+
+void Projectile::hitOther(EntityId entity, DamageRequest const&) {
+ if (!m_parameters.getBool("piercing", m_config->piercing)) {
+ auto victimEntity = world()->entity(entity);
+ if (!victimEntity || (victimEntity->getTeam().type != TeamType::Passive && victimEntity->getTeam().type != TeamType::Environment)) {
+ if (victimEntity) {
+ if (auto hitPoly = victimEntity->hitPoly()) {
+ auto geometry = world()->geometry();
+ Vec2F checkVec = m_movementController->velocity().normalized() * 5;
+ Vec2F nearMin = geometry.nearestTo(hitPoly->center(), m_movementController->position() - checkVec);
+ if (auto intersection = hitPoly->lineIntersection(Line2F(nearMin, nearMin + checkVec * 2)))
+ m_movementController->setPosition(intersection->point);
+ }
+ }
+ m_movementController->setVelocity({0, 0});
+ m_collision = true;
+ m_timeToLive = 0.0f;
+ }
+ }
+ m_scriptComponent.invoke("hit", entity);
+}
+
+void Projectile::update(uint64_t) {
+ if (isMaster()) {
+ m_timeToLive -= WorldTimestep;
+ if (m_timeToLive < 0)
+ m_timeToLive = 0;
+
+ m_effectEmitter->addEffectSources("normal", m_config->emitters);
+
+ if (m_referenceVelocity)
+ m_movementController->setVelocity(m_movementController->velocity() - *m_referenceVelocity);
+
+ m_scriptComponent.update(m_scriptComponent.updateDt());
+ m_movementController->accelerate(m_movementController->velocity().normalized() * m_acceleration);
+
+ if (m_referenceVelocity)
+ m_movementController->setVelocity(m_movementController->velocity() + *m_referenceVelocity);
+
+ m_movementController->tickMaster();
+ m_travelLine.min() = m_travelLine.max();
+ m_travelLine.max() = m_movementController->position();
+
+ tickShared();
+
+ if (m_trackSourceEntity) {
+ if (auto sourceEntity = world()->entity(m_sourceEntity)) {
+ Vec2F newEntityPosition = sourceEntity->position();
+ m_movementController->translate(newEntityPosition - m_lastEntityPosition);
+ m_lastEntityPosition = newEntityPosition;
+ } else {
+ m_trackSourceEntity = false;
+ }
+ }
+
+ if (m_movementController->atWorldLimit())
+ m_timeToLive = 0.0f;
+
+ if ((m_movementController->isColliding() || m_movementController->stickingDirection()) && !m_movementController->isNullColliding()) {
+ if (!m_wasColliding)
+ m_collisionEvent.trigger();
+ m_wasColliding = true;
+ } else {
+ m_wasColliding = false;
+ }
+
+ if (m_movementController->isColliding()) {
+ if (m_movementController->isNullColliding()) {
+ // Don't trigger collision action, just silently die if we collide with a
+ // null block.
+ m_timeToLive = 0.0f;
+ } else if (m_bounces != 0) {
+ m_scriptComponent.invoke("bounce");
+ if (m_bounces > 0)
+ --m_bounces;
+ } else if (m_falldown && !(m_movementController->onGround() || m_movementController->isCollisionStuck() || m_movementController->stickingDirection())) {
+ // Wait til this projectile actually hits the ground before dying
+
+ } else if (!m_movementController->stickingDirection()) {
+ m_collision = true;
+ m_timeToLive = 0.0f;
+ // Move slightly less than one tile unit in the direction that the projectile
+ // has most recently moved to find the collision tile. This is *not* perfect by any means.
+ m_collisionTile = Vec2I::floor(m_movementController->position() + m_travelLine.direction() * 0.9f);
+
+ m_lastNonCollidingTile = Vec2I::floor(m_movementController->position());
+ for (float i = 0; i < 1.51f; i += 0.5f) {
+ auto pos = Vec2I::floor(m_movementController->position() + m_travelLine.direction() * -i);
+ if (world()->material(pos, TileLayer::Foreground) == EmptyMaterialId) {
+ m_lastNonCollidingTile = pos;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!m_collision && m_hydrophobic) {
+ auto liquid = world()->liquidLevel(Vec2I::floor(position()));
+ if (liquid.level > 0.5f) {
+ m_collision = true;
+ m_timeToLive = 0.0f;
+ m_collisionTile = Vec2I::floor(position());
+ m_lastNonCollidingTile = m_collisionTile;
+ }
+ }
+ } else {
+ m_netGroup.tickNetInterpolation(WorldTimestep);
+ m_movementController->tickSlave();
+ m_travelLine.min() = m_travelLine.max();
+ m_travelLine.max() = m_movementController->position();
+
+ m_timeToLive -= WorldTimestep;
+
+ tickShared();
+ }
+
+ SpatialLogger::logPoly("world", m_movementController->collisionBody(), Color::Red.toRgba());
+}
+
+void Projectile::render(RenderCallback* renderCallback) {
+ renderPendingRenderables(renderCallback);
+
+ if (m_persistentAudio)
+ m_persistentAudio->setPosition(position());
+
+ m_effectEmitter->render(renderCallback);
+
+ Drawable drawable = Drawable::makeImage(drawableFrame(), 1.0f / TilePixels, true, Vec2F());
+ if (m_config->flippable) {
+ auto angleSide = getAngleSide(m_movementController->rotation(), true);
+ if (angleSide.second == Direction::Left)
+ drawable.scale(Vec2F(-1, 1));
+ drawable.rotate(angleSide.first);
+ } else {
+ drawable.rotate(m_movementController->rotation());
+ }
+ drawable.fullbright = m_config->fullbright;
+ drawable.translate(position());
+ renderCallback->addDrawable(move(drawable), m_config->renderLayer);
+
+ renderCallback->addLightSource({position(), m_config->lightColor, m_config->pointLight, 0.0f, 0.0f, 0.0f});
+}
+
+Maybe<Json> Projectile::receiveMessage(ConnectionId sendingConnection, String const& message, JsonArray const& args) {
+ return m_scriptComponent.handleMessage(message, sendingConnection == world()->connection(), args);
+}
+
+Maybe<LuaValue> Projectile::callScript(String const& func, LuaVariadic<LuaValue> const& args) {
+ return m_scriptComponent.invoke(func, args);
+}
+
+Maybe<LuaValue> Projectile::evalScript(String const& code) {
+ return m_scriptComponent.eval(code);
+}
+
+String Projectile::projectileType() const {
+ return m_config->typeName;
+}
+
+void Projectile::setReferenceVelocity(Maybe<Vec2F> const& velocity) {
+ m_movementController->setVelocity(m_movementController->velocity() - m_referenceVelocity.value());
+ m_referenceVelocity = velocity;
+ m_movementController->setVelocity(m_movementController->velocity() + velocity.value());
+ m_effectEmitter->setBaseVelocity(velocity.value());
+}
+
+float Projectile::initialSpeed() const {
+ return m_initialSpeed;
+}
+
+void Projectile::setInitialSpeed(float speed) {
+ m_initialSpeed = speed;
+}
+
+void Projectile::setInitialPosition(Vec2F const& position) {
+ m_movementController->setPosition(position);
+}
+
+void Projectile::setInitialDirection(Vec2F const& direction) {
+ m_movementController->setVelocity(vnorm(direction) * m_initialSpeed + m_referenceVelocity.value());
+ m_movementController->setRotation(direction.angle());
+}
+
+void Projectile::setInitialVelocity(Vec2F const& velocity) {
+ m_movementController->setVelocity(velocity + m_referenceVelocity.value());
+ m_movementController->setRotation(velocity.angle());
+}
+
+void Projectile::setSourceEntity(EntityId source, bool trackSource) {
+ m_sourceEntity = source;
+ m_trackSourceEntity = trackSource;
+ if (inWorld()) {
+ if (auto sourceEntity = world()->entity(source)) {
+ m_lastEntityPosition = sourceEntity->position();
+ if (!m_damageTeam)
+ setTeam(sourceEntity->getTeam());
+ } else {
+ m_sourceEntity = NullEntityId;
+ m_trackSourceEntity = false;
+ }
+ }
+}
+
+float Projectile::powerMultiplier() const {
+ return m_powerMultiplier;
+}
+
+void Projectile::setPowerMultiplier(float powerMultiplier) {
+ m_powerMultiplier = powerMultiplier;
+}
+
+EntityId Projectile::sourceEntity() const {
+ return m_sourceEntity;
+}
+
+List<PersistentStatusEffect> Projectile::statusEffects() const {
+ return m_config->persistentStatusEffects;
+}
+
+PolyF Projectile::statusEffectArea() const {
+ return m_config->statusEffectArea;
+}
+
+List<PhysicsForceRegion> Projectile::forceRegions() const {
+ List<PhysicsForceRegion> forces;
+ for (auto const& p : m_physicsForces) {
+ if (p.second.enabled.get()) {
+ PhysicsForceRegion forceRegion = p.second.forceRegion;
+ forceRegion.call([pos = position()](auto& fr) { fr.translate(pos); });
+ forces.append(move(forceRegion));
+ }
+ }
+ return forces;
+}
+
+size_t Projectile::movingCollisionCount() const {
+ return m_physicsCollisions.size();
+}
+
+Maybe<PhysicsMovingCollision> Projectile::movingCollision(size_t positionIndex) const {
+ auto const& mc = m_physicsCollisions.valueAt(positionIndex);
+ if (!mc.enabled.get())
+ return {};
+ PhysicsMovingCollision collision = mc.movingCollision;
+ collision.translate(position());
+ return collision;
+}
+
+List<Particle> Projectile::sparkBlock(World* world, Vec2I const& position, Vec2F const& damageSource) {
+ auto& root = Root::singleton();
+ auto assets = root.assets();
+ auto materialDatabase = root.materialDatabase();
+
+ auto blockDamageParticle = Particle(assets->json("/client.config:blockDamageParticle"));
+ auto blockDamageVariance = Particle(assets->json("/client.config:blockDamageParticleVariance"));
+
+ List<Particle> result;
+ for (auto layer : {TileLayer::Background, TileLayer::Foreground}) {
+ auto material = world->material(position, layer);
+ auto hueShift = world->materialHueShift(position, layer);
+ if (isRealMaterial(material)) {
+ auto particle = blockDamageParticle;
+ particle.position += centerOfTile(position);
+ particle.velocity = particle.velocity.magnitude() * vnorm(world->geometry().diff(damageSource, particle.position));
+ particle.color = materialDatabase->materialParticleColor(material, hueShift);
+ particle.applyVariance(blockDamageVariance);
+
+ particle.approach += Vec2F(0.0f, 5.0f);
+ particle.velocity += Vec2F(Random::randf() - 0.5f, 5.0f + Random::randf());
+ particle.velocity += 10.0f * Vec2F(1.0f - 2.0f * Random::randf(), 1.0f - 2.0f * Random::randf());
+ particle.finalVelocity = 0.5f * (particle.finalVelocity + Vec2F(Random::randf() - 0.5f, -20.0f + Random::randf()));
+ particle.trail = true;
+
+ result.append(move(particle));
+ }
+ }
+ return result;
+}
+
+int Projectile::getFrame() const {
+ float time_per_frame = m_animationCycle / m_config->frameNumber;
+
+ if (m_config->animationLoops) {
+ if (m_animationTimer < time_per_frame * m_config->windupFrames) {
+ return floor(m_animationTimer / time_per_frame);
+ } else if (m_timeToLive < time_per_frame * m_config->winddownFrames) {
+ return m_config->windupFrames + m_config->frameNumber
+ + clamp<int>((time_per_frame * m_config->winddownFrames - m_timeToLive) / time_per_frame, 0, m_config->winddownFrames - 1);
+ } else {
+ float time_within_cycle = std::fmod(m_animationTimer, m_animationCycle);
+ return m_config->windupFrames + floor(time_within_cycle / time_per_frame);
+ }
+ } else {
+ return clamp<int>(m_animationTimer / time_per_frame, 0, m_config->frameNumber - 1);
+ }
+}
+
+void Projectile::setFrame(int frame) {
+ m_frame = frame;
+}
+
+String Projectile::drawableFrame() {
+ return strf("%s:%d%s", m_config->image, m_frame, m_imageDirectives);
+}
+
+bool Projectile::ephemeral() const {
+ return true;
+}
+
+ClientEntityMode Projectile::clientEntityMode() const {
+ return m_config->clientEntityMode;
+}
+
+bool Projectile::masterOnly() const {
+ return m_config->masterOnly;
+}
+
+void Projectile::processAction(Json const& action) {
+ Json parameters;
+ String command;
+
+ if (action.type() == Json::Type::Object) {
+ parameters = action;
+ command = parameters.getString("action").toLower();
+ } else {
+ command = action.toString().toLower();
+ }
+
+ auto doWithDelay = [this](int stepsDelay, WorldAction function) {
+ if (stepsDelay == 0)
+ function(world());
+ else
+ world()->timer(stepsDelay, function);
+ };
+
+ if (command == "tile") {
+ if (isSlave())
+ return;
+
+ auto materialDatabase = Root::singleton().materialDatabase();
+ List<MaterialId> tileDrops;
+ unsigned totalDrops = 0;
+ for (auto sets : parameters.getArray("materials")) {
+ unsigned numDrops = sets.getUInt("quantity", 1);
+ auto mat = materialDatabase->materialId(sets.getString("kind"));
+ for (unsigned i = 0; i < numDrops; i++)
+ tileDrops.push_back(mat);
+ totalDrops += numDrops;
+ }
+
+ List<Vec2I> openSpaces = world()->findEmptyTiles(m_lastNonCollidingTile, parameters.getInt("radius", 2), totalDrops);
+ if (openSpaces.size() < totalDrops)
+ Logger::debug("Couldn't find a place for all the tile drops. %d drops requested, %d spaces found.", totalDrops, openSpaces.size());
+
+ bool allowEntityOverlap = parameters.getBool("allowEntityOverlap", true);
+
+ random_shuffle(tileDrops.begin(), tileDrops.end(), [](int i) { return Random::randu64() % i; });
+ for (auto tile : zip(openSpaces, tileDrops)) {
+ if (!world()->modifyTile(std::get<0>(tile), PlaceMaterial{TileLayer::Foreground, std::get<1>(tile), MaterialHue()}, allowEntityOverlap)) {
+ auto itemDrop = ItemDrop::createRandomizedDrop(materialDatabase->materialItemDrop(std::get<1>(tile)), (Vec2F)std::get<0>(tile));
+ world()->addEntity(itemDrop);
+ }
+ }
+
+ } else if (command == "applysurfacemod") {
+ if (isSlave())
+ return;
+
+ auto materialDatabase = Root::singleton().materialDatabase();
+ Maybe<ModId> previousMod =
+ parameters.optString("previousMod").apply(bind(&MaterialDatabase::modId, materialDatabase, _1));
+ ModId newMod = materialDatabase->modId(parameters.getString("newMod"));
+ int radius = parameters.getInt("radius", 0);
+ float chance = parameters.getFloat("chance", 1.0f);
+
+ Set<TileLayer> layers;
+ if (parameters.getBool("foreground", true))
+ layers.add(TileLayer::Foreground);
+ if (parameters.getBool("background", false))
+ layers.add(TileLayer::Background);
+
+ for (auto layer : layers) {
+ // Go in vertical lines for each column, stop at the first non-emppty
+ // material in each column.
+ for (int x = m_collisionTile[0] - radius; x <= m_collisionTile[0] + radius; ++x) {
+ if (world()->material({x, m_collisionTile[1] + radius + 1}, layer) == EmptyMaterialId) {
+ for (int y = m_collisionTile[1] + radius; y >= m_collisionTile[1] - radius; --y) {
+ auto mat = world()->material({x, y}, layer);
+ if (Random::randf() <= chance) {
+ if (isRealMaterial(mat)) {
+ auto mod = world()->mod({x, y}, layer);
+ if (!previousMod || *previousMod == mod)
+ world()->modifyTile({x, y}, PlaceMod{layer, newMod, {}}, true);
+ }
+ }
+ if (mat != EmptyMaterialId)
+ break;
+ }
+ }
+ }
+ }
+
+ } else if (command == "liquid") {
+ if (isSlave())
+ return;
+
+ float waterAmount = parameters.getFloat("quantity", 1.0f);
+ LiquidId liquid = Root::singleton().liquidsDatabase()->liquidId(parameters.getString("liquid"));
+ auto empty = world()->findEmptyTiles(m_lastNonCollidingTile, parameters.getInt("radius", 5), 50);
+ for (Vec2I pos : empty) {
+ if (world()->lineTileCollision(Vec2F(pos), Vec2F(m_lastNonCollidingTile)))
+ continue;
+
+ auto liquidLevel = world()->liquidLevel(pos);
+ if (liquidLevel.liquid == EmptyLiquidId || liquidLevel.liquid == liquid) {
+ world()->modifyTile(pos, PlaceLiquid{liquid, waterAmount}, true);
+ break;
+ }
+ }
+
+ } else if (command == "projectile") {
+ if (isSlave())
+ return;
+
+ String type = parameters.getString("type");
+ auto projectileParameters = parameters.get("config", JsonObject());
+ if (!projectileParameters.contains("damageTeam")) {
+ if (m_damageTeam)
+ projectileParameters = projectileParameters.set("damageTeam", m_damageTeam);
+ }
+ if (parameters.contains("inheritDamageFactor") && !projectileParameters.contains("power"))
+ projectileParameters = projectileParameters.set("power", m_power * parameters.getFloat("inheritDamageFactor"));
+ if (parameters.contains("inheritSpeedFactor"))
+ projectileParameters = projectileParameters.set("speed", (m_movementController->velocity() - m_referenceVelocity.value()).magnitude() * parameters.getFloat("inheritSpeedFactor"));
+
+ auto projectile = Root::singleton().projectileDatabase()->createProjectile(type, projectileParameters);
+ Vec2F offset;
+ if (parameters.contains("offset")) {
+ offset = jsonToVec2F(parameters.getArray("offset", {0.0f, 0.0f}));
+ } else if (parameters.contains("offsetRange")) {
+ auto offsetRange = jsonToVec4F(parameters.get("offsetRange"));
+ offset = Vec2F(Random::randf(offsetRange[0], offsetRange[2]), Random::randf(offsetRange[1], offsetRange[3]));
+ }
+ if (m_referenceVelocity)
+ projectile->setReferenceVelocity(m_referenceVelocity);
+ projectile->setInitialPosition(position() + offset);
+ if (parameters.contains("direction")) {
+ projectile->setInitialDirection(jsonToVec2F(parameters.get("direction")));
+ } else {
+ float angle = m_movementController->rotation();
+ float angleAdjust = 0;
+ if (parameters.contains("angle"))
+ angle = parameters.getFloat("angle") * Constants::pi / 180.0f;
+ if (parameters.contains("fuzzAngle"))
+ angleAdjust += Random::randf(-1, 1) * parameters.getFloat("fuzzAngle") * Constants::pi / 180.0f;
+ if (parameters.contains("angleAdjust"))
+ angleAdjust += parameters.getFloat("angleAdjust") * Constants::pi / 180.0f;
+ if (parameters.contains("autoFlipAdjust") && parameters.getBool("autoFlipAdjust")) {
+ if (Vec2F::withAngle(m_movementController->rotation())[0] < 0)
+ angleAdjust = -angleAdjust;
+ }
+ if (parameters.contains("autoFlipAngle") && parameters.getBool("autoFlipAngle")) {
+ if (Vec2F::withAngle(m_movementController->rotation())[0] < 0)
+ angle = -angle;
+ }
+ angle += angleAdjust;
+ projectile->setInitialDirection(Vec2F::withAngle(angle, 1.0f));
+ }
+ projectile->setSourceEntity(m_sourceEntity, false);
+ projectile->setPowerMultiplier(m_powerMultiplier);
+
+ // if the entity no longer exists and no explicit damage team is set, inherit damage team
+ if (!projectile->m_damageTeam && !world()->entity(m_sourceEntity))
+ projectile->setTeam(getTeam());
+
+ doWithDelay(parameters.getUInt("delaySteps", 0), [=](World* world) { world->addEntity(projectile); });
+
+ } else if (command == "spark") {
+ if (!world()->isClient())
+ return;
+
+ auto collisionMaterial = world()->material(m_collisionTile, TileLayer::Foreground);
+ if (!m_collision || collisionMaterial == EmptyMaterialId)
+ return;
+
+ for (auto& particle : sparkBlock(world(), m_collisionTile, position())) {
+ // enable trails and such
+ particle.approach += Vec2F(0.0f, 5.0f);
+ particle.velocity += Vec2F(Random::randf() - 0.5f, 5.0f + Random::randf());
+ particle.velocity += 10.0f * Vec2F(1.0f - 2.0f * Random::randf(), 1.0f - 2.0f * Random::randf());
+ particle.finalVelocity = 0.5f * (particle.finalVelocity + Vec2F(Random::randf() - 0.5f, -20.0f + Random::randf()));
+ particle.trail = true;
+
+ m_pendingRenderables.append(move(particle));
+ }
+
+ } else if (command == "particle") {
+ if (!world()->isClient())
+ return;
+
+ Particle particle = Root::singleton().particleDatabase()->particle(parameters.get("specification"));
+ particle.position = particle.position.rotate(m_movementController->rotation());
+ if (parameters.getBool("rotate", false)) {
+ particle.rotation = m_movementController->rotation();
+ particle.velocity = particle.velocity.rotate(m_movementController->rotation());
+ }
+ particle.translate(position());
+ particle.velocity += m_referenceVelocity.value();
+ m_pendingRenderables.append(move(particle));
+
+ } else if (command == "explosion") {
+ if (isSlave())
+ return;
+
+ float foregroundRadius = parameters.getFloat("foregroundRadius");
+ float backgroundRadius = parameters.getFloat("backgroundRadius");
+ float explosiveDamageAmount = parameters.getFloat("explosiveDamageAmount");
+ auto damageType = TileDamageTypeNames.getLeft(parameters.getString("tileDamageType", "explosive"));
+ unsigned harvestLevel = parameters.getUInt("harvestLevel", 0);
+ Vec2F explosionPosition = position();
+
+ doWithDelay(parameters.getUInt("delaySteps", 0), [=](World* world) {
+ world->damageTiles(tileAreaBrush(foregroundRadius, explosionPosition, false),
+ TileLayer::Foreground,
+ explosionPosition,
+ {damageType, explosiveDamageAmount, harvestLevel},
+ sourceEntity());
+ world->damageTiles(tileAreaBrush(backgroundRadius, explosionPosition, false),
+ TileLayer::Background,
+ explosionPosition,
+ {damageType, explosiveDamageAmount, harvestLevel},
+ sourceEntity());
+ });
+
+ } else if (command == "spawnmonster") {
+ if (isMaster()) {
+ String const type = parameters.getString("type");
+
+ JsonObject arguments = parameters.getObject("arguments", JsonObject{});
+
+ float level = parameters.getFloat("level", m_parameters.getFloat("level", 0.0f));
+
+ auto monsterDatabase = Root::singleton().monsterDatabase();
+ auto monster = monsterDatabase->createMonster(monsterDatabase->randomMonster(type, arguments), level);
+
+ auto spawnPosition = position();
+ if (parameters.contains("offset"))
+ spawnPosition += jsonToVec2F(parameters.get("offset"));
+ monster->setPosition(spawnPosition);
+ world()->addEntity(monster);
+ }
+
+ if (world()->isClient() && parameters.contains("particle")) {
+ Particle particle(parameters.getObject("particle"));
+ particle.translate(position());
+ particle.velocity += m_referenceVelocity.value();
+ m_pendingRenderables.append(move(particle));
+ }
+
+ } else if (command == "item") {
+ if (isSlave())
+ return;
+
+ String const name = parameters.getString("name");
+ size_t count = parameters.getInt("count", 1);
+ JsonObject data = parameters.getObject("data", JsonObject{});
+
+ auto itemDrop = ItemDrop::createRandomizedDrop(ItemDescriptor(name, count, data), position());
+ world()->addEntity(itemDrop);
+
+ } else if (command == "sound") {
+ if (!world()->isClient())
+ return;
+
+ AudioInstancePtr sound = make_shared<AudioInstance>(*Root::singleton().assets()->audio(Random::randValueFrom(parameters.getArray("options")).toString()));
+ sound->setPosition(position());
+ m_pendingRenderables.append(move(sound));
+
+ } else if (command == "light") {
+ if (!world()->isClient())
+ return;
+
+ m_pendingRenderables.append(LightSource{
+ position(),
+ jsonToColor(parameters.get("color")).toRgb(),
+ parameters.getBool("pointLight", true),
+ 0.0f,
+ 0.0f,
+ 0.0f
+ });
+
+ } else if (command == "option") {
+ JsonArray options = parameters.getArray("options");
+ if (options.size())
+ processAction(Random::randFrom(options));
+
+ } else if (command == "actions") {
+ JsonArray list = parameters.getArray("list");
+ for (auto const& action : list)
+ processAction(action);
+
+ } else if (command == "loop") {
+ int count = parameters.getInt("count");
+ JsonArray body = parameters.getArray("body");
+ while (count > 0) {
+ for (auto const& action : body)
+ processAction(action);
+ count--;
+ }
+
+ } else if (command == "config") {
+ processAction(Root::singleton().assets()->json(parameters.getString("file")));
+
+ } else {
+ throw StarException(strf("Unknown projectile reap command %s", command));
+ }
+}
+
+void Projectile::tickShared() {
+ if (!m_config->orientationLocked && !m_movementController->stickingDirection()) {
+ auto apparentVelocity = m_movementController->velocity() - m_referenceVelocity.value();
+ if (apparentVelocity != Vec2F())
+ m_movementController->setRotation(apparentVelocity.angle());
+ }
+
+ m_animationTimer += WorldTimestep;
+ setFrame(getFrame());
+
+ m_effectEmitter->setSourcePosition("normal", position());
+ m_effectEmitter->setDirection(getAngleSide(m_movementController->rotation(), true).second);
+ m_effectEmitter->tick(*entityMode());
+
+ if (m_collisionEvent.pullOccurred()) {
+ for (auto const& action : m_parameters.getArray("actionOnCollide", m_config->actionOnCollide))
+ processAction(action);
+ }
+
+ auto periodicActionIt = makeSMutableIterator(m_periodicActions);
+ while (periodicActionIt.hasNext()) {
+ auto& periodicAction = periodicActionIt.next();
+ if (get<1>(periodicAction)) {
+ if (get<0>(periodicAction).wrapTick())
+ processAction(get<2>(periodicAction));
+ } else {
+ if (get<0>(periodicAction).tick()) {
+ processAction(get<2>(periodicAction));
+ periodicActionIt.remove();
+ }
+ }
+ }
+}
+
+void Projectile::setup() {
+ if (auto uniqueId = m_parameters.optString("uniqueId"))
+ setUniqueId(*uniqueId);
+
+ m_acceleration = m_parameters.getFloat("acceleration", m_config->acceleration);
+ m_power = m_parameters.getFloat("power", m_config->power);
+ m_powerMultiplier = m_parameters.getFloat("powerMultiplier", 1.0f);
+ m_imageDirectives = m_parameters.getString("processing", "");
+ m_persistentAudioFile = m_parameters.getString("persistentAudio", m_config->persistentAudio);
+
+ m_damageKind = m_parameters.getString("damageKind", m_config->damageKind);
+ m_damageType = DamageTypeNames.getLeft(m_parameters.getString("damageType", m_config->damageType));
+ m_rayCheckToSource = m_parameters.getBool("rayCheckToSource", m_config->rayCheckToSource);
+
+ if (auto damageTeam = m_parameters.get("damageTeam", m_config->damageTeam)) {
+ m_damageTeam = damageTeam;
+ setTeam(EntityDamageTeam(damageTeam));
+ }
+ m_damageRepeatGroup = m_parameters.optString("damageRepeatGroup").orMaybe(m_config->damageRepeatGroup);
+ m_damageRepeatTimeout = m_parameters.optFloat("damageRepeatTimeout").orMaybe(m_config->damageRepeatTimeout);
+
+ m_falldown = m_parameters.getBool("falldown", m_config->falldown);
+
+ m_hydrophobic = m_parameters.getBool("hydrophobic", m_config->hydrophobic);
+ m_onlyHitTerrain = m_parameters.getBool("onlyHitTerrain", m_config->onlyHitTerrain);
+
+ auto movementSettings = jsonMerge(m_config->movementSettings, m_parameters.get("movementSettings", Json()));
+ if (!movementSettings.contains("physicsEffectCategories"))
+ movementSettings = movementSettings.set("physicsEffectCategories", JsonArray{"projectile"});
+ m_movementController = make_shared<MovementController>(movementSettings);
+
+ m_effectEmitter = make_shared<EffectEmitter>();
+
+ m_initialSpeed = m_parameters.getFloat("speed", m_config->initialSpeed);
+ m_sourceEntity = NullEntityId;
+ m_trackSourceEntity = false;
+ m_bounces = m_parameters.getInt("bounces", m_config->bounces);
+
+ m_frame = 0;
+
+ m_animationTimer = 0.0f;
+ m_animationCycle = m_parameters.getFloat("animationCycle", m_config->animationCycle);
+ m_collision = false;
+
+ for (auto const& p : m_config->physicsForces.iterateObject()) {
+ auto& forceConfig = m_physicsForces[p.first];
+
+ forceConfig.forceRegion = jsonToPhysicsForceRegion(p.second);
+ forceConfig.enabled.set(p.second.getBool("enabled", true));
+ }
+
+ for (auto const& p : m_config->physicsCollisions.iterateObject()) {
+ auto& forceConfig = m_physicsCollisions[p.first];
+
+ forceConfig.movingCollision = PhysicsMovingCollision::fromJson(p.second);
+ forceConfig.enabled.set(p.second.getBool("enabled", true));
+ }
+
+ m_physicsForces.sortByKey();
+ for (auto& p : m_physicsForces)
+ m_netGroup.addNetElement(&p.second.enabled);
+
+ m_physicsCollisions.sortByKey();
+ for (auto& p : m_physicsCollisions)
+ m_netGroup.addNetElement(&p.second.enabled);
+
+ m_netGroup.addNetElement(&m_collisionEvent);
+ m_netGroup.addNetElement(m_movementController.get());
+ m_netGroup.addNetElement(m_effectEmitter.get());
+}
+
+LuaCallbacks Projectile::makeProjectileCallbacks() {
+ LuaCallbacks callbacks;
+ callbacks.registerCallback("getParameter", [this](String const& name, Json const& def) {
+ return m_parameters.query(name, m_config->config.query(name, def));
+ });
+ callbacks.registerCallback("die", [this]() { m_timeToLive = 0.0f; });
+ callbacks.registerCallback("sourceEntity", [this]() -> Maybe<EntityId> {
+ if (m_sourceEntity == NullEntityId)
+ return {};
+ else
+ return m_sourceEntity;
+ });
+ callbacks.registerCallback("powerMultiplier", [this]() { return powerMultiplier(); });
+ callbacks.registerCallback("timeToLive", [this]() { return m_timeToLive; });
+ callbacks.registerCallback("setTimeToLive", [this](float const& timeToLive) { return m_timeToLive = timeToLive; });
+ callbacks.registerCallback("collision", [this]() { return m_collision; });
+ callbacks.registerCallback("processAction", [this](Json const& action) { processAction(action); });
+ callbacks.registerCallback("power", [this]() { return m_power; });
+ callbacks.registerCallback("setPower", [this](float const& power) { m_power = power; });
+ callbacks.registerCallback("setReferenceVelocity", [this](Maybe<Vec2F> const& referenceVelocity) { setReferenceVelocity(referenceVelocity); });
+ return callbacks;
+}
+
+void Projectile::renderPendingRenderables(RenderCallback* renderCallback) {
+ for (auto renderable : m_pendingRenderables) {
+ if (renderable.is<AudioInstancePtr>())
+ renderCallback->addAudio(renderable.get<AudioInstancePtr>());
+ else if (renderable.is<Particle>())
+ renderCallback->addParticle(renderable.get<Particle>());
+ else if (renderable.is<LightSource>())
+ renderCallback->addLightSource(renderable.get<LightSource>());
+ }
+ m_pendingRenderables.clear();
+}
+
+}