#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(); setSourceEntity(sourceEntity, trackSourceEntity); data.read(m_initialSpeed); data.read(m_powerMultiplier); setTeam(data.read()); } 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(*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; } Vec2F Projectile::velocity() const { return m_movementController->velocity(); } pair Projectile::writeNetState(uint64_t fromVersion) { return m_netGroup.writeNetState(fromVersion); } void Projectile::readNetState(ByteArray data, float interpolationTime) { m_netGroup.readNetState(std::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("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 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 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(float dt, uint64_t) { m_movementController->setTimestep(dt); if (isMaster()) { m_timeToLive -= dt; 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(dt)); m_movementController->accelerate(m_movementController->velocity().normalized() * m_acceleration); if (m_referenceVelocity) m_movementController->setVelocity(m_movementController->velocity() + *m_referenceVelocity); m_movementController->tickMaster(dt); m_travelLine.min() = m_travelLine.max(); m_travelLine.max() = m_movementController->position(); tickShared(dt); 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(dt); m_movementController->tickSlave(dt); m_travelLine.min() = m_travelLine.max(); m_travelLine.max() = m_movementController->position(); m_timeToLive -= dt; tickShared(dt); } if (world()->isClient()) 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); String image = strf("{}:{}{}", m_config->image, m_frame, m_imageSuffix); Drawable drawable = Drawable::makeImage(image, 1.0f / TilePixels, true, Vec2F()); drawable.imagePart().addDirectives(m_imageDirectives, true); 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(std::move(drawable), m_config->renderLayer); } void Projectile::renderLightSources(RenderCallback* renderCallback) { for (auto renderable : m_pendingRenderables) { if (renderable.is()) renderCallback->addLightSource(renderable.get()); } renderCallback->addLightSource({position(), m_config->lightColor.toRgbF(), m_config->lightType, 0.0f, 0.0f, 0.0f}); } Maybe Projectile::receiveMessage(ConnectionId sendingConnection, String const& message, JsonArray const& args) { return m_scriptComponent.handleMessage(message, sendingConnection == world()->connection(), args); } Maybe Projectile::callScript(String const& func, LuaVariadic const& args) { return m_scriptComponent.invoke(func, args); } Maybe Projectile::evalScript(String const& code) { return m_scriptComponent.eval(code); } String Projectile::projectileType() const { return m_config->typeName; } void Projectile::setReferenceVelocity(Maybe 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 Projectile::statusEffects() const { return m_config->persistentStatusEffects; } PolyF Projectile::statusEffectArea() const { return m_config->statusEffectArea; } List Projectile::forceRegions() const { List 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(std::move(forceRegion)); } } return forces; } size_t Projectile::movingCollisionCount() const { return m_physicsCollisions.size(); } Maybe 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 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 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(std::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((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(m_animationTimer / time_per_frame, 0, m_config->frameNumber - 1); } } void Projectile::setFrame(int frame) { m_frame = frame; } String Projectile::drawableFrame() { String str = strf("{}:{}{}", m_config->image, m_frame, m_imageSuffix); return m_imageDirectives.addToString(str); } 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((float)stepsDelay / 60.f, function); }; if (command == "tile") { if (isSlave()) return; auto materialDatabase = Root::singleton().materialDatabase(); List 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 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. {} drops requested, {} spaces found.", totalDrops, openSpaces.size()); bool allowEntityOverlap = parameters.getBool("allowEntityOverlap", true); Random::shuffle(tileDrops); 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 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 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(std::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(std::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(std::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(*Root::singleton().assets()->audio(Random::randValueFrom(parameters.getArray("options")).toString())); sound->setPosition(position()); sound->setVolume(parameters.getFloat("volume", 1.0f)); sound->setPitchMultiplier(parameters.getFloat("pitch", 1.0f)); m_pendingRenderables.append(std::move(sound)); } else if (command == "light") { if (!world()->isClient()) return; m_pendingRenderables.append(LightSource{ position(), jsonToColor(parameters.get("color")).toRgbF(), (LightType)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 {}", command)); } } void Projectile::tickShared(float dt) { 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 += dt; setFrame(getFrame()); m_effectEmitter->setSourcePosition("normal", position()); m_effectEmitter->setDirection(getAngleSide(m_movementController->rotation(), true).second); m_effectEmitter->tick(dt, *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(dt)) { 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); { // it is possible to shove a frame name in processing. I hope nobody actually does this but account for it... String processing = m_parameters.getString("processing", ""); auto begin = processing.utf8().find_first_of('?'); if (begin == NPos) { m_imageDirectives = ""; m_imageSuffix = std::move(processing); } else if (begin == 0) { m_imageDirectives = std::move(processing); m_imageSuffix = ""; } else { m_imageDirectives = (String)processing.utf8().substr(begin); m_imageSuffix = processing.utf8().substr(0, begin); } } 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(movementSettings); m_effectEmitter = make_shared(); 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 { 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 const& referenceVelocity) { setReferenceVelocity(referenceVelocity); }); return callbacks; } void Projectile::renderPendingRenderables(RenderCallback* renderCallback) { for (auto renderable : m_pendingRenderables) { if (renderable.is()) renderCallback->addAudio(renderable.get()); else if (renderable.is()) renderCallback->addParticle(renderable.get()); } m_pendingRenderables.clear(); } }