2023-06-20 04:33:09 +00:00
|
|
|
#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);
|
|
|
|
}
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
void Projectile::update(float dt, uint64_t) {
|
|
|
|
m_movementController->setTimestep(dt);
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
if (isMaster()) {
|
2023-07-20 14:58:49 +00:00
|
|
|
m_timeToLive -= dt;
|
2023-06-20 04:33:09 +00:00
|
|
|
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);
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
m_scriptComponent.update(m_scriptComponent.updateDt(dt));
|
2023-06-20 04:33:09 +00:00
|
|
|
m_movementController->accelerate(m_movementController->velocity().normalized() * m_acceleration);
|
|
|
|
|
|
|
|
if (m_referenceVelocity)
|
|
|
|
m_movementController->setVelocity(m_movementController->velocity() + *m_referenceVelocity);
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
m_movementController->tickMaster(dt);
|
2023-06-20 04:33:09 +00:00
|
|
|
m_travelLine.min() = m_travelLine.max();
|
|
|
|
m_travelLine.max() = m_movementController->position();
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
tickShared(dt);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
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);
|
2023-07-20 14:58:49 +00:00
|
|
|
m_movementController->tickSlave(dt);
|
2023-06-20 04:33:09 +00:00
|
|
|
m_travelLine.min() = m_travelLine.max();
|
|
|
|
m_travelLine.max() = m_movementController->position();
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
m_timeToLive -= dt;
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
tickShared(dt);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
2023-06-27 14:50:47 +00:00
|
|
|
if (world()->isClient())
|
|
|
|
SpatialLogger::logPoly("world", m_movementController->collisionBody(), Color::Red.toRgba());
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Projectile::render(RenderCallback* renderCallback) {
|
|
|
|
renderPendingRenderables(renderCallback);
|
|
|
|
|
|
|
|
if (m_persistentAudio)
|
|
|
|
m_persistentAudio->setPosition(position());
|
|
|
|
|
|
|
|
m_effectEmitter->render(renderCallback);
|
|
|
|
|
2023-08-02 11:28:37 +00:00
|
|
|
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);
|
2023-06-20 04:33:09 +00:00
|
|
|
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);
|
2023-06-29 00:11:19 +00:00
|
|
|
}
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-06-29 00:11:19 +00:00
|
|
|
void Projectile::renderLightSources(RenderCallback* renderCallback) {
|
|
|
|
for (auto renderable : m_pendingRenderables) {
|
|
|
|
if (renderable.is<LightSource>())
|
|
|
|
renderCallback->addLightSource(renderable.get<LightSource>());
|
|
|
|
}
|
|
|
|
renderCallback->addLightSource({ position(), m_config->lightColor, m_config->pointLight, 0.0f, 0.0f, 0.0f });
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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() {
|
2023-08-02 11:28:37 +00:00
|
|
|
String str = strf("{}:{}{}", m_config->image, m_frame, m_imageSuffix);
|
|
|
|
return m_imageDirectives.addToString(str);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2023-06-27 10:23:44 +00:00
|
|
|
Logger::debug("Couldn't find a place for all the tile drops. {} drops requested, {} spaces found.", totalDrops, openSpaces.size());
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
bool allowEntityOverlap = parameters.getBool("allowEntityOverlap", true);
|
|
|
|
|
2023-06-25 10:01:32 +00:00
|
|
|
Random::shuffle(tileDrops);
|
|
|
|
for (auto& tile : zip(openSpaces, tileDrops)) {
|
2023-06-20 04:33:09 +00:00
|
|
|
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 {
|
2023-06-27 10:23:44 +00:00
|
|
|
throw StarException(strf("Unknown projectile reap command {}", command));
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
void Projectile::tickShared(float dt) {
|
2023-06-20 04:33:09 +00:00
|
|
|
if (!m_config->orientationLocked && !m_movementController->stickingDirection()) {
|
|
|
|
auto apparentVelocity = m_movementController->velocity() - m_referenceVelocity.value();
|
|
|
|
if (apparentVelocity != Vec2F())
|
|
|
|
m_movementController->setRotation(apparentVelocity.angle());
|
|
|
|
}
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
m_animationTimer += dt;
|
2023-06-20 04:33:09 +00:00
|
|
|
setFrame(getFrame());
|
|
|
|
|
|
|
|
m_effectEmitter->setSourcePosition("normal", position());
|
|
|
|
m_effectEmitter->setDirection(getAngleSide(m_movementController->rotation(), true).second);
|
2023-07-20 14:58:49 +00:00
|
|
|
m_effectEmitter->tick(dt, *entityMode());
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
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 {
|
2023-07-20 14:58:49 +00:00
|
|
|
if (get<0>(periodicAction).tick(dt)) {
|
2023-06-20 04:33:09 +00:00
|
|
|
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);
|
2023-08-02 11:28:37 +00:00
|
|
|
{ // 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 = move(processing);
|
|
|
|
}
|
|
|
|
else if (begin == 0) {
|
|
|
|
m_imageDirectives = move(processing);
|
|
|
|
m_imageSuffix = "";
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
m_imageDirectives = (String)processing.utf8().substr(begin);
|
|
|
|
m_imageSuffix = processing.utf8().substr(0, begin);
|
|
|
|
}
|
|
|
|
}
|
2023-06-20 04:33:09 +00:00
|
|
|
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>());
|
|
|
|
}
|
|
|
|
m_pendingRenderables.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|