431a9c00a5
On Linux and macOS, using Clang to compile OpenStarbound produces about 400 MB worth of warnings during the build, making the compiler output unreadable and slowing the build down considerably. 99% of the warnings were unqualified uses of std::move and std::forward, which are now all properly qualified. Fixed a few other minor warnings about non-virtual destructors and some uses of std::move preventing copy elision on temporary objects. Most remaining warnings are now unused parameters.
1131 lines
38 KiB
C++
1131 lines
38 KiB
C++
#include "StarNpc.hpp"
|
|
#include "StarDataStreamExtra.hpp"
|
|
#include "StarWorld.hpp"
|
|
#include "StarRoot.hpp"
|
|
#include "StarDamageManager.hpp"
|
|
#include "StarDamageDatabase.hpp"
|
|
#include "StarLogging.hpp"
|
|
#include "StarConfigLuaBindings.hpp"
|
|
#include "StarEntityLuaBindings.hpp"
|
|
#include "StarWorldLuaBindings.hpp"
|
|
#include "StarRootLuaBindings.hpp"
|
|
#include "StarStatusControllerLuaBindings.hpp"
|
|
#include "StarBehaviorLuaBindings.hpp"
|
|
#include "StarEmoteProcessor.hpp"
|
|
#include "StarTreasure.hpp"
|
|
#include "StarEncode.hpp"
|
|
#include "StarItemDatabase.hpp"
|
|
#include "StarItemDrop.hpp"
|
|
#include "StarAssets.hpp"
|
|
#include "StarEntityRendering.hpp"
|
|
#include "StarTime.hpp"
|
|
#include "StarArmors.hpp"
|
|
#include "StarFireableItem.hpp"
|
|
#include "StarStatusController.hpp"
|
|
#include "StarJsonExtra.hpp"
|
|
#include "StarDanceDatabase.hpp"
|
|
#include "StarSpeciesDatabase.hpp"
|
|
|
|
namespace Star {
|
|
|
|
Npc::Npc(NpcVariant const& npcVariant)
|
|
: m_humanoid(npcVariant.humanoidConfig) {
|
|
m_disableWornArmor.set(npcVariant.disableWornArmor);
|
|
|
|
m_emoteState = HumanoidEmote::Idle;
|
|
m_chatMessageUpdated = false;
|
|
|
|
m_statusText.set({});
|
|
m_displayNametag.set(false);
|
|
|
|
auto assets = Root::singleton().assets();
|
|
|
|
m_emoteCooldownTimer = GameTimer(assets->json("/npcs/npc.config:emoteCooldown").toFloat());
|
|
m_danceCooldownTimer = GameTimer(0.0f);
|
|
m_blinkInterval = jsonToVec2F(assets->json("/npcs/npc.config:blinkInterval"));
|
|
|
|
m_questIndicatorOffset = jsonToVec2F(assets->json("/quests/quests.config:defaultIndicatorOffset"));
|
|
|
|
if (npcVariant.overrides)
|
|
m_clientEntityMode = ClientEntityModeNames.getLeft(npcVariant.overrides.getString("clientEntityMode", "ClientSlaveOnly"));
|
|
|
|
m_isInteractive.set(false);
|
|
|
|
m_shifting.set(false);
|
|
m_damageOnTouch.set(false);
|
|
|
|
m_dropPools.set(npcVariant.dropPools);
|
|
m_npcVariant = npcVariant;
|
|
|
|
setTeam(EntityDamageTeam(m_npcVariant.damageTeamType, m_npcVariant.damageTeam));
|
|
|
|
m_scriptComponent.setScripts(m_npcVariant.scripts);
|
|
m_scriptComponent.setUpdateDelta(m_npcVariant.initialScriptDelta);
|
|
auto movementParameters = ActorMovementParameters(m_npcVariant.movementParameters);
|
|
if (!movementParameters.physicsEffectCategories)
|
|
movementParameters.physicsEffectCategories = StringSet({"npc"});
|
|
m_movementController = make_shared<ActorMovementController>(movementParameters);
|
|
m_humanoid.setIdentity(m_npcVariant.humanoidIdentity);
|
|
m_deathParticleBurst.set(m_humanoid.defaultDeathParticles());
|
|
|
|
m_statusController = make_shared<StatusController>(m_npcVariant.statusControllerSettings);
|
|
m_statusController->setPersistentEffects("innate", m_npcVariant.innateStatusEffects);
|
|
auto speciesDefinition = Root::singleton().speciesDatabase()->species(species());
|
|
m_statusController->setPersistentEffects("species", speciesDefinition->statusEffects());
|
|
m_statusController->setStatusProperty("species", species());
|
|
if (!m_statusController->statusProperty("effectDirectives"))
|
|
m_statusController->setStatusProperty("effectDirectives", speciesDefinition->effectDirectives());
|
|
|
|
m_effectEmitter = make_shared<EffectEmitter>();
|
|
|
|
m_hitDamageNotificationLimiter = 0;
|
|
m_hitDamageNotificationLimit = assets->json("/npcs/npc.config:hitDamageNotificationLimit").toInt();
|
|
|
|
m_blinkCooldownTimer = GameTimer();
|
|
|
|
m_armor = make_shared<ArmorWearer>();
|
|
m_tools = make_shared<ToolUser>();
|
|
|
|
m_aggressive.set(false);
|
|
|
|
setPersistent(m_npcVariant.persistent);
|
|
setKeepAlive(m_npcVariant.keepAlive);
|
|
|
|
setupNetStates();
|
|
}
|
|
|
|
Npc::Npc(NpcVariant const& npcVariant, Json const& diskStore) : Npc(npcVariant) {
|
|
m_movementController->loadState(diskStore.get("movementController"));
|
|
m_statusController->diskLoad(diskStore.get("statusController"));
|
|
auto aimPosition = jsonToVec2F(diskStore.get("aimPosition"));
|
|
m_xAimPosition.set(aimPosition[0]);
|
|
m_yAimPosition.set(aimPosition[1]);
|
|
m_humanoid.setState(Humanoid::StateNames.getLeft(diskStore.getString("humanoidState")));
|
|
m_humanoid.setEmoteState(HumanoidEmoteNames.getLeft(diskStore.getString("humanoidEmoteState")));
|
|
m_isInteractive.set(diskStore.getBool("isInteractive"));
|
|
m_shifting.set(diskStore.getBool("shifting"));
|
|
m_damageOnTouch.set(diskStore.getBool("damageOnTouch", false));
|
|
|
|
m_effectEmitter->fromJson(diskStore.get("effectEmitter"));
|
|
|
|
m_armor->diskLoad(diskStore.get("armor"));
|
|
m_tools->diskLoad(diskStore.get("tools"));
|
|
|
|
m_disableWornArmor.set(diskStore.getBool("disableWornArmor"));
|
|
|
|
m_scriptComponent.setScriptStorage(diskStore.getObject("scriptStorage"));
|
|
|
|
setUniqueId(diskStore.optString("uniqueId"));
|
|
if (diskStore.contains("team"))
|
|
setTeam(EntityDamageTeam(diskStore.get("team")));
|
|
|
|
m_deathParticleBurst.set(diskStore.optString("deathParticleBurst"));
|
|
|
|
m_dropPools.set(diskStore.getArray("dropPools").transformed(mem_fn(&Json::toString)));
|
|
|
|
m_blinkCooldownTimer = GameTimer();
|
|
|
|
m_aggressive.set(diskStore.getBool("aggressive"));
|
|
}
|
|
|
|
Json Npc::diskStore() const {
|
|
return JsonObject{
|
|
{"npcVariant", Root::singleton().npcDatabase()->writeNpcVariantToJson(m_npcVariant)},
|
|
{"movementController", m_movementController->storeState()},
|
|
{"statusController", m_statusController->diskStore()},
|
|
{"armor", m_armor->diskStore()},
|
|
{"tools", m_tools->diskStore()},
|
|
{"aimPosition", jsonFromVec2F({m_xAimPosition.get(), m_yAimPosition.get()})},
|
|
{"humanoidState", Humanoid::StateNames.getRight(m_humanoid.state())},
|
|
{"humanoidEmoteState", HumanoidEmoteNames.getRight(m_humanoid.emoteState())},
|
|
{"isInteractive", m_isInteractive.get()},
|
|
{"shifting", m_shifting.get()},
|
|
{"damageOnTouch", m_damageOnTouch.get()},
|
|
{"effectEmitter", m_effectEmitter->toJson()},
|
|
{"disableWornArmor", m_disableWornArmor.get()},
|
|
{"scriptStorage", m_scriptComponent.getScriptStorage()},
|
|
{"uniqueId", jsonFromMaybe(uniqueId())},
|
|
{"team", getTeam().toJson()},
|
|
{"deathParticleBurst", jsonFromMaybe(m_deathParticleBurst.get())},
|
|
{"dropPools", m_dropPools.get().transformed(construct<Json>())},
|
|
{"aggressive", m_aggressive.get()}
|
|
};
|
|
}
|
|
|
|
ByteArray Npc::netStore() {
|
|
return Root::singleton().npcDatabase()->writeNpcVariant(m_npcVariant);
|
|
}
|
|
|
|
EntityType Npc::entityType() const {
|
|
return EntityType::Npc;
|
|
}
|
|
|
|
ClientEntityMode Npc::clientEntityMode() const {
|
|
return m_clientEntityMode;
|
|
}
|
|
|
|
void Npc::init(World* world, EntityId entityId, EntityMode mode) {
|
|
Entity::init(world, entityId, mode);
|
|
m_movementController->init(world);
|
|
m_movementController->setIgnorePhysicsEntities({entityId});
|
|
m_statusController->init(this, m_movementController.get());
|
|
m_tools->init(this);
|
|
|
|
m_armor->setupHumanoidClothingDrawables(m_humanoid, false);
|
|
|
|
if (isMaster()) {
|
|
m_movementController->resetAnchorState();
|
|
|
|
auto itemDatabase = Root::singleton().itemDatabase();
|
|
for (auto const& item : m_npcVariant.items)
|
|
setItemSlot(item.first, item.second);
|
|
m_scriptComponent.addCallbacks("npc", makeNpcCallbacks());
|
|
m_scriptComponent.addCallbacks("config",
|
|
LuaBindings::makeConfigCallbacks([this](String const& name, Json const& def)
|
|
{ return m_npcVariant.scriptConfig.query(name, def); }));
|
|
m_scriptComponent.addCallbacks("entity", LuaBindings::makeEntityCallbacks(this));
|
|
m_scriptComponent.addCallbacks("status", LuaBindings::makeStatusControllerCallbacks(m_statusController.get()));
|
|
m_scriptComponent.addCallbacks("behavior", LuaBindings::makeBehaviorLuaCallbacks(&m_behaviors));
|
|
m_scriptComponent.addActorMovementCallbacks(m_movementController.get());
|
|
m_scriptComponent.init(world);
|
|
}
|
|
}
|
|
|
|
void Npc::uninit() {
|
|
if (isMaster()) {
|
|
m_movementController->resetAnchorState();
|
|
m_scriptComponent.uninit();
|
|
m_scriptComponent.removeCallbacks("npc");
|
|
m_scriptComponent.removeCallbacks("config");
|
|
m_scriptComponent.removeCallbacks("entity");
|
|
m_scriptComponent.removeCallbacks("status");
|
|
m_scriptComponent.removeActorMovementCallbacks();
|
|
}
|
|
m_tools->uninit();
|
|
m_statusController->uninit();
|
|
m_movementController->uninit();
|
|
Entity::uninit();
|
|
}
|
|
|
|
void Npc::enableInterpolation(float extrapolationHint) {
|
|
m_netGroup.enableNetInterpolation(extrapolationHint);
|
|
}
|
|
|
|
void Npc::disableInterpolation() {
|
|
m_netGroup.disableNetInterpolation();
|
|
}
|
|
|
|
Vec2F Npc::position() const {
|
|
return m_movementController->position();
|
|
}
|
|
|
|
RectF Npc::metaBoundBox() const {
|
|
return RectF(-4, -4, 4, 4);
|
|
}
|
|
|
|
Vec2F Npc::mouthOffset(bool ignoreAdjustments) const {
|
|
return Vec2F{m_humanoid.mouthOffset(ignoreAdjustments)[0] * numericalDirection(m_humanoid.facingDirection()),
|
|
m_humanoid.mouthOffset(ignoreAdjustments)[1]};
|
|
}
|
|
|
|
Vec2F Npc::feetOffset() const {
|
|
return {m_humanoid.feetOffset()[0] * numericalDirection(m_humanoid.facingDirection()), m_humanoid.feetOffset()[1]};
|
|
}
|
|
|
|
Vec2F Npc::headArmorOffset() const {
|
|
return {m_humanoid.headArmorOffset()[0] * numericalDirection(m_humanoid.facingDirection()), m_humanoid.headArmorOffset()[1]};
|
|
}
|
|
|
|
Vec2F Npc::chestArmorOffset() const {
|
|
return {m_humanoid.chestArmorOffset()[0] * numericalDirection(m_humanoid.facingDirection()), m_humanoid.chestArmorOffset()[1]};
|
|
}
|
|
|
|
Vec2F Npc::backArmorOffset() const {
|
|
return {m_humanoid.backArmorOffset()[0] * numericalDirection(m_humanoid.facingDirection()), m_humanoid.backArmorOffset()[1]};
|
|
}
|
|
|
|
Vec2F Npc::legsArmorOffset() const {
|
|
return {m_humanoid.legsArmorOffset()[0] * numericalDirection(m_humanoid.facingDirection()), m_humanoid.legsArmorOffset()[1]};
|
|
}
|
|
|
|
RectF Npc::collisionArea() const {
|
|
return m_movementController->collisionPoly().boundBox();
|
|
}
|
|
|
|
pair<ByteArray, uint64_t> Npc::writeNetState(uint64_t fromVersion) {
|
|
// client-side npcs error nearby vanilla NPC scripts because callScriptedEntity
|
|
// for now, scrungle the collision poly to avoid their queries. hacky :(
|
|
if (m_npcVariant.overrides && m_npcVariant.overrides.getBool("overrideNetPoly", false)) {
|
|
if (auto mode = entityMode()) {
|
|
if (*mode == EntityMode::Master && connectionForEntity(entityId()) != ServerConnectionId) {
|
|
PolyF poly = m_movementController->collisionPoly();
|
|
m_movementController->setCollisionPoly({ { 0.0f, -3.402823466e+38F }});
|
|
auto result = m_netGroup.writeNetState(fromVersion);
|
|
m_movementController->setCollisionPoly(poly);
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
return m_netGroup.writeNetState(fromVersion);
|
|
}
|
|
|
|
void Npc::readNetState(ByteArray data, float interpolationTime) {
|
|
m_netGroup.readNetState(std::move(data), interpolationTime);
|
|
}
|
|
|
|
String Npc::description() const {
|
|
return "Some funny looking person";
|
|
}
|
|
|
|
String Npc::species() const {
|
|
return m_humanoid.identity().species;
|
|
}
|
|
|
|
Gender Npc::gender() const {
|
|
return m_humanoid.identity().gender;
|
|
}
|
|
|
|
String Npc::npcType() const {
|
|
return m_npcVariant.typeName;
|
|
}
|
|
|
|
Json Npc::scriptConfigParameter(String const& parameterName, Json const& defaultValue) const {
|
|
return m_npcVariant.scriptConfig.query(parameterName, defaultValue);
|
|
}
|
|
|
|
Maybe<HitType> Npc::queryHit(DamageSource const& source) const {
|
|
if (!inWorld() || !m_statusController->resourcePositive("health") || m_statusController->statPositive("invulnerable"))
|
|
return {};
|
|
|
|
if (m_tools->queryShieldHit(source))
|
|
return HitType::ShieldHit;
|
|
|
|
if (source.intersectsWithPoly(world()->geometry(), m_movementController->collisionBody()))
|
|
return HitType::Hit;
|
|
|
|
return {};
|
|
}
|
|
|
|
Maybe<PolyF> Npc::hitPoly() const {
|
|
return m_movementController->collisionBody();
|
|
}
|
|
|
|
List<DamageNotification> Npc::applyDamage(DamageRequest const& damage) {
|
|
if (!inWorld())
|
|
return {};
|
|
|
|
auto notifications = m_statusController->applyDamageRequest(damage);
|
|
|
|
float totalDamage = 0.0f;
|
|
for (auto const& notification : notifications)
|
|
totalDamage += notification.healthLost;
|
|
|
|
if (totalDamage > 0 && m_hitDamageNotificationLimiter < m_hitDamageNotificationLimit) {
|
|
m_scriptComponent.invoke("damage", JsonObject{
|
|
{"sourceId", damage.sourceEntityId},
|
|
{"damage", totalDamage},
|
|
{"sourceDamage", damage.damage},
|
|
{"sourceKind", damage.damageSourceKind}
|
|
});
|
|
m_hitDamageNotificationLimiter++;
|
|
}
|
|
|
|
return notifications;
|
|
}
|
|
|
|
List<DamageNotification> Npc::selfDamageNotifications() {
|
|
return m_statusController->pullSelfDamageNotifications();
|
|
}
|
|
|
|
bool Npc::shouldDestroy() const {
|
|
if (auto res = m_scriptComponent.invoke<bool>("shouldDie"))
|
|
return *res;
|
|
else if (!m_statusController->resourcePositive("health") || m_scriptComponent.error())
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
void Npc::destroy(RenderCallback* renderCallback) {
|
|
m_scriptComponent.invoke("die");
|
|
|
|
if (isMaster() && !m_dropPools.get().empty()) {
|
|
auto treasureDatabase = Root::singleton().treasureDatabase();
|
|
for (auto const& treasureItem :
|
|
treasureDatabase->createTreasure(staticRandomFrom(m_dropPools.get(), m_npcVariant.seed), m_npcVariant.level))
|
|
world()->addEntity(ItemDrop::createRandomizedDrop(treasureItem, position()));
|
|
}
|
|
|
|
if (renderCallback && m_deathParticleBurst.get())
|
|
renderCallback->addParticles(m_humanoid.particles(*m_deathParticleBurst.get()), position());
|
|
}
|
|
|
|
void Npc::damagedOther(DamageNotification const& damage) {
|
|
if (inWorld() && isMaster())
|
|
m_statusController->damagedOther(damage);
|
|
}
|
|
|
|
void Npc::update(float dt, uint64_t) {
|
|
if (!inWorld())
|
|
return;
|
|
|
|
m_movementController->setTimestep(dt);
|
|
|
|
if (isMaster()) {
|
|
m_scriptComponent.update(m_scriptComponent.updateDt(dt));
|
|
|
|
if (inConflictingLoungeAnchor())
|
|
m_movementController->resetAnchorState();
|
|
|
|
if (auto loungeAnchor = as<LoungeAnchor>(m_movementController->entityAnchor())) {
|
|
if (loungeAnchor->emote)
|
|
requestEmote(*loungeAnchor->emote);
|
|
m_statusController->setPersistentEffects("lounging", loungeAnchor->statusEffects);
|
|
m_effectEmitter->addEffectSources("normal", loungeAnchor->effectEmitters);
|
|
switch (loungeAnchor->orientation) {
|
|
case LoungeOrientation::Sit:
|
|
m_humanoid.setState(Humanoid::Sit);
|
|
break;
|
|
case LoungeOrientation::Lay:
|
|
m_humanoid.setState(Humanoid::Lay);
|
|
break;
|
|
case LoungeOrientation::Stand:
|
|
m_humanoid.setState(Humanoid::Idle); // currently the same as "standard"
|
|
// idle, but this is lounging idle
|
|
break;
|
|
default:
|
|
m_humanoid.setState(Humanoid::Idle);
|
|
}
|
|
} else {
|
|
m_statusController->setPersistentEffects("lounging", {});
|
|
}
|
|
|
|
m_armor->effects(*m_effectEmitter);
|
|
m_tools->effects(*m_effectEmitter);
|
|
|
|
if (!m_disableWornArmor.get())
|
|
m_statusController->setPersistentEffects("armor", m_armor->statusEffects());
|
|
m_statusController->setPersistentEffects("tools", m_tools->statusEffects());
|
|
|
|
m_movementController->tickMaster(dt);
|
|
m_statusController->tickMaster(dt);
|
|
|
|
tickShared(dt);
|
|
|
|
if (!is<LoungeAnchor>(m_movementController->entityAnchor())) {
|
|
if (m_movementController->groundMovement()) {
|
|
if (m_movementController->running())
|
|
m_humanoid.setState(Humanoid::Run);
|
|
else if (m_movementController->walking())
|
|
m_humanoid.setState(Humanoid::Walk);
|
|
else if (m_movementController->crouching())
|
|
m_humanoid.setState(Humanoid::Duck);
|
|
else
|
|
m_humanoid.setState(Humanoid::Idle);
|
|
} else if (m_movementController->liquidMovement()) {
|
|
if (abs(m_movementController->xVelocity()) > 0)
|
|
m_humanoid.setState(Humanoid::Swim);
|
|
else
|
|
m_humanoid.setState(Humanoid::SwimIdle);
|
|
} else {
|
|
if (m_movementController->yVelocity() > 0)
|
|
m_humanoid.setState(Humanoid::Jump);
|
|
else
|
|
m_humanoid.setState(Humanoid::Fall);
|
|
}
|
|
}
|
|
|
|
if (m_emoteCooldownTimer.tick(dt))
|
|
m_emoteState = HumanoidEmote::Idle;
|
|
if (m_danceCooldownTimer.tick(dt))
|
|
m_dance = {};
|
|
|
|
if (m_chatMessageUpdated) {
|
|
auto state = Root::singleton().emoteProcessor()->detectEmotes(m_chatMessage.get());
|
|
if (state != HumanoidEmote::Idle)
|
|
addEmote(state);
|
|
m_chatMessageUpdated = false;
|
|
}
|
|
|
|
if (m_blinkCooldownTimer.tick(dt)) {
|
|
m_blinkCooldownTimer = GameTimer(Random::randf(m_blinkInterval[0], m_blinkInterval[1]));
|
|
if (m_emoteState == HumanoidEmote::Idle)
|
|
addEmote(HumanoidEmote::Blink);
|
|
}
|
|
|
|
m_humanoid.setEmoteState(m_emoteState);
|
|
m_humanoid.setDance(m_dance);
|
|
|
|
} else {
|
|
m_netGroup.tickNetInterpolation(GlobalTimestep);
|
|
m_movementController->tickSlave(dt);
|
|
m_statusController->tickSlave(dt);
|
|
|
|
tickShared(dt);
|
|
}
|
|
|
|
if (world()->isClient())
|
|
SpatialLogger::logPoly("world", m_movementController->collisionBody(), {0, 255, 0, 255});
|
|
}
|
|
|
|
void Npc::render(RenderCallback* renderCallback) {
|
|
EntityRenderLayer renderLayer = RenderLayerNpc;
|
|
if (auto loungeAnchor = as<LoungeAnchor>(m_movementController->entityAnchor()))
|
|
renderLayer = loungeAnchor->loungeRenderLayer;
|
|
|
|
m_tools->setupHumanoidHandItemDrawables(m_humanoid);
|
|
for (auto& drawable : m_humanoid.render()) {
|
|
drawable.translate(position());
|
|
if (drawable.isImage())
|
|
drawable.imagePart().addDirectivesGroup(m_statusController->parentDirectives(), true);
|
|
renderCallback->addDrawable(std::move(drawable), renderLayer);
|
|
}
|
|
|
|
renderCallback->addDrawables(m_statusController->drawables(), renderLayer);
|
|
renderCallback->addParticles(m_statusController->pullNewParticles());
|
|
renderCallback->addAudios(m_statusController->pullNewAudios());
|
|
|
|
renderCallback->addParticles(m_npcVariant.splashConfig.doSplash(position(), m_movementController->velocity(), world()));
|
|
|
|
m_tools->render(renderCallback, inToolRange(), m_shifting.get(), renderLayer);
|
|
|
|
renderCallback->addDrawables(m_tools->renderObjectPreviews(aimPosition(), walkingDirection(), inToolRange(), favoriteColor()), renderLayer);
|
|
|
|
m_effectEmitter->render(renderCallback);
|
|
}
|
|
|
|
void Npc::renderLightSources(RenderCallback* renderCallback) {
|
|
renderCallback->addLightSources(lightSources());
|
|
}
|
|
|
|
void Npc::setPosition(Vec2F const& pos) {
|
|
m_movementController->setPosition(pos);
|
|
}
|
|
|
|
float Npc::maxHealth() const {
|
|
return *m_statusController->resourceMax("health");
|
|
}
|
|
|
|
float Npc::health() const {
|
|
return m_statusController->resource("health");
|
|
}
|
|
|
|
DamageBarType Npc::damageBar() const {
|
|
return DamageBarType::Default;
|
|
}
|
|
|
|
List<Drawable> Npc::portrait(PortraitMode mode) const {
|
|
return m_humanoid.renderPortrait(mode);
|
|
}
|
|
|
|
String Npc::name() const {
|
|
return m_npcVariant.humanoidIdentity.name;
|
|
}
|
|
|
|
Maybe<String> Npc::statusText() const {
|
|
return m_statusText.get();
|
|
}
|
|
|
|
bool Npc::displayNametag() const {
|
|
return m_displayNametag.get();
|
|
}
|
|
|
|
Vec3B Npc::nametagColor() const {
|
|
return m_npcVariant.nametagColor;
|
|
}
|
|
|
|
Vec2F Npc::nametagOrigin() const {
|
|
return mouthPosition(false);
|
|
}
|
|
|
|
bool Npc::aggressive() const {
|
|
return m_aggressive.get();
|
|
}
|
|
|
|
Maybe<LuaValue> Npc::callScript(String const& func, LuaVariadic<LuaValue> const& args) {
|
|
return m_scriptComponent.invoke(func, args);
|
|
}
|
|
|
|
Maybe<LuaValue> Npc::evalScript(String const& code) {
|
|
return m_scriptComponent.eval(code);
|
|
}
|
|
|
|
Vec2F Npc::getAbsolutePosition(Vec2F relativePosition) const {
|
|
if (m_humanoid.facingDirection() == Direction::Left)
|
|
relativePosition[0] *= -1;
|
|
return m_movementController->position() + relativePosition;
|
|
}
|
|
|
|
void Npc::tickShared(float dt) {
|
|
if (m_hitDamageNotificationLimiter)
|
|
m_hitDamageNotificationLimiter--;
|
|
|
|
m_effectEmitter->setSourcePosition("normal", position());
|
|
m_effectEmitter->setSourcePosition("mouth", position() + mouthOffset());
|
|
m_effectEmitter->setSourcePosition("feet", position() + feetOffset());
|
|
m_effectEmitter->setSourcePosition("headArmor", headArmorOffset() + position());
|
|
m_effectEmitter->setSourcePosition("chestArmor", chestArmorOffset() + position());
|
|
m_effectEmitter->setSourcePosition("legsArmor", legsArmorOffset() + position());
|
|
m_effectEmitter->setSourcePosition("backArmor", backArmorOffset() + position());
|
|
|
|
m_effectEmitter->setDirection(m_humanoid.facingDirection());
|
|
m_effectEmitter->tick(dt, *entityMode());
|
|
|
|
m_humanoid.setMovingBackwards(m_movementController->movingDirection() != m_movementController->facingDirection());
|
|
m_humanoid.setFacingDirection(m_movementController->facingDirection());
|
|
m_humanoid.setRotation(m_movementController->rotation());
|
|
|
|
ActorMovementModifiers firingModifiers;
|
|
if (auto fireableMain = as<FireableItem>(handItem(ToolHand::Primary))) {
|
|
if (fireableMain->firing()) {
|
|
if (fireableMain->stopWhileFiring())
|
|
firingModifiers.movementSuppressed = true;
|
|
else if (fireableMain->walkWhileFiring())
|
|
firingModifiers.runningSuppressed = true;
|
|
}
|
|
}
|
|
|
|
if (auto fireableAlt = as<FireableItem>(handItem(ToolHand::Alt))) {
|
|
if (fireableAlt->firing()) {
|
|
if (fireableAlt->stopWhileFiring())
|
|
firingModifiers.movementSuppressed = true;
|
|
else if (fireableAlt->walkWhileFiring())
|
|
firingModifiers.runningSuppressed = true;
|
|
}
|
|
}
|
|
|
|
m_armor->setupHumanoidClothingDrawables(m_humanoid, false);
|
|
|
|
m_tools->suppressItems(!canUseTool());
|
|
m_tools->tick(dt, m_shifting.get(), {});
|
|
|
|
if (auto overrideDirection = m_tools->setupHumanoidHandItems(m_humanoid, position(), aimPosition()))
|
|
m_movementController->controlFace(*overrideDirection);
|
|
|
|
m_humanoid.animate(dt);
|
|
}
|
|
|
|
LuaCallbacks Npc::makeNpcCallbacks() {
|
|
LuaCallbacks callbacks;
|
|
|
|
callbacks.registerCallback("toAbsolutePosition", [this](Vec2F const& p) { return getAbsolutePosition(p); });
|
|
|
|
callbacks.registerCallback("species", [this]() { return m_npcVariant.species; });
|
|
|
|
callbacks.registerCallback("gender", [this]() { return GenderNames.getRight(m_humanoid.identity().gender); });
|
|
|
|
callbacks.registerCallback("humanoidIdentity", [this]() { return m_humanoid.identity().toJson(); });
|
|
|
|
callbacks.registerCallback("npcType", [this]() { return npcType(); });
|
|
|
|
callbacks.registerCallback("seed", [this]() { return m_npcVariant.seed; });
|
|
|
|
callbacks.registerCallback("level", [this]() { return m_npcVariant.level; });
|
|
|
|
callbacks.registerCallback("dropPools", [this]() { return m_dropPools.get(); });
|
|
|
|
callbacks.registerCallback("setDropPools", [this](StringList const& dropPools) { m_dropPools.set(dropPools); });
|
|
|
|
callbacks.registerCallback("energy", [this]() { return m_statusController->resource("energy"); });
|
|
|
|
callbacks.registerCallback("maxEnergy", [this]() { return m_statusController->resourceMax("energy"); });
|
|
|
|
callbacks.registerCallback("say", [this](String line, Maybe<StringMap<String>> const& tags, Json const& config) {
|
|
if (tags)
|
|
line = line.replaceTags(*tags, false);
|
|
|
|
if (!line.empty()) {
|
|
addChatMessage(line, config);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
callbacks.registerCallback("sayPortrait", [this](String line, String portrait, Maybe<StringMap<String>> const& tags, Json const& config) {
|
|
if (tags)
|
|
line = line.replaceTags(*tags, false);
|
|
|
|
if (!line.empty()) {
|
|
addChatMessage(line, config, portrait);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
callbacks.registerCallback("emote", [this](String const& arg1) { addEmote(HumanoidEmoteNames.getLeft(arg1)); });
|
|
|
|
callbacks.registerCallback("dance", [this](Maybe<String> const& danceName) { setDance(danceName); });
|
|
|
|
callbacks.registerCallback("setInteractive", [this](bool interactive) { m_isInteractive.set(interactive); });
|
|
|
|
callbacks.registerCallback("setLounging", [this](EntityId loungeableEntityId, Maybe<size_t> maybeAnchorIndex) {
|
|
size_t anchorIndex = maybeAnchorIndex.value(0);
|
|
auto loungeableEntity = world()->get<LoungeableEntity>(loungeableEntityId);
|
|
if (!loungeableEntity || anchorIndex >= loungeableEntity->anchorCount()
|
|
|| !loungeableEntity->entitiesLoungingIn(anchorIndex).empty()
|
|
|| !loungeableEntity->loungeAnchor(anchorIndex))
|
|
return false;
|
|
|
|
m_movementController->setAnchorState({loungeableEntityId, anchorIndex});
|
|
return true;
|
|
});
|
|
|
|
callbacks.registerCallback("resetLounging", [this]() { m_movementController->resetAnchorState(); });
|
|
|
|
callbacks.registerCallback("isLounging", [this]() { return is<LoungeAnchor>(m_movementController->entityAnchor()); });
|
|
|
|
callbacks.registerCallback("loungingIn", [this]() -> Maybe<EntityId> {
|
|
auto loungingState = loungingIn();
|
|
if (loungingState)
|
|
return loungingState.value().entityId;
|
|
else
|
|
return {};
|
|
});
|
|
|
|
callbacks.registerCallback("setOfferedQuests", [this](Maybe<JsonArray> const& offeredQuests) {
|
|
m_offeredQuests.set(offeredQuests.value().transformed(&QuestArcDescriptor::fromJson));
|
|
});
|
|
|
|
callbacks.registerCallback("setTurnInQuests", [this](Maybe<StringList> const& turnInQuests) {
|
|
m_turnInQuests.set(StringSet::from(turnInQuests.value()));
|
|
});
|
|
|
|
callbacks.registerCallback("setItemSlot", [this](String const& slot, Json const& itemDescriptor) -> Json {
|
|
return setItemSlot(slot, ItemDescriptor(itemDescriptor));
|
|
});
|
|
|
|
callbacks.registerCallback("getItemSlot", [this](String const& entry) -> Json {
|
|
if (entry.equalsIgnoreCase("head"))
|
|
return m_armor->headItemDescriptor().toJson();
|
|
else if (entry.equalsIgnoreCase("headCosmetic"))
|
|
return m_armor->headCosmeticItemDescriptor().toJson();
|
|
else if (entry.equalsIgnoreCase("chest"))
|
|
return m_armor->chestItemDescriptor().toJson();
|
|
else if (entry.equalsIgnoreCase("chestCosmetic"))
|
|
return m_armor->chestCosmeticItemDescriptor().toJson();
|
|
else if (entry.equalsIgnoreCase("legs"))
|
|
return m_armor->legsItemDescriptor().toJson();
|
|
else if (entry.equalsIgnoreCase("legsCosmetic"))
|
|
return m_armor->legsCosmeticItemDescriptor().toJson();
|
|
else if (entry.equalsIgnoreCase("back"))
|
|
return m_armor->backItemDescriptor().toJson();
|
|
else if (entry.equalsIgnoreCase("backCosmetic"))
|
|
return m_armor->backCosmeticItemDescriptor().toJson();
|
|
else if (entry.equalsIgnoreCase("primary"))
|
|
return m_tools->primaryHandItemDescriptor().toJson();
|
|
else if (entry.equalsIgnoreCase("alt"))
|
|
return m_tools->altHandItemDescriptor().toJson();
|
|
else if (m_npcVariant.items.contains(entry))
|
|
return m_npcVariant.items.get(entry).toJson();
|
|
|
|
return {};
|
|
});
|
|
|
|
callbacks.registerCallback("disableWornArmor", [this](bool disable) { m_disableWornArmor.set(disable); });
|
|
|
|
callbacks.registerCallback("beginPrimaryFire", [this]() { m_tools->beginPrimaryFire(); });
|
|
callbacks.registerCallback("beginAltFire", [this]() { m_tools->beginAltFire(); });
|
|
callbacks.registerCallback("endPrimaryFire", [this]() { m_tools->endPrimaryFire(); });
|
|
callbacks.registerCallback("endAltFire", [this]() { m_tools->endAltFire(); });
|
|
callbacks.registerCallback("setShifting", [this](bool shifting) { m_shifting.set(shifting); });
|
|
callbacks.registerCallback("setDamageOnTouch", [this](bool damageOnTouch) { m_damageOnTouch.set(damageOnTouch); });
|
|
|
|
callbacks.registerCallback("aimPosition", [this]() { return jsonFromVec2F(aimPosition()); });
|
|
|
|
callbacks.registerCallback("setAimPosition", [this](Vec2F const& pos) {
|
|
auto aimPosition = world()->geometry().diff(pos, position());
|
|
m_xAimPosition.set(aimPosition[0]);
|
|
m_yAimPosition.set(aimPosition[1]);
|
|
});
|
|
|
|
callbacks.registerCallback("setDeathParticleBurst", [this](Maybe<String> const& deathParticleBurst) {
|
|
m_deathParticleBurst.set(deathParticleBurst);
|
|
});
|
|
|
|
callbacks.registerCallback("setStatusText", [this](Maybe<String> const& status) { m_statusText.set(status); });
|
|
callbacks.registerCallback("setDisplayNametag", [this](bool display) { m_displayNametag.set(display); });
|
|
|
|
callbacks.registerCallback("setPersistent", [this](bool persistent) { setPersistent(persistent); });
|
|
|
|
callbacks.registerCallback("setKeepAlive", [this](bool keepAlive) { setKeepAlive(keepAlive); });
|
|
|
|
callbacks.registerCallback("setDamageTeam", [this](Json const& team) { setTeam(EntityDamageTeam(team)); });
|
|
|
|
callbacks.registerCallback("setAggressive", [this](bool aggressive) { m_aggressive.set(aggressive); });
|
|
|
|
callbacks.registerCallback("setUniqueId", [this](Maybe<String> uniqueId) { setUniqueId(uniqueId); });
|
|
|
|
return callbacks;
|
|
}
|
|
|
|
void Npc::setupNetStates() {
|
|
m_netGroup.addNetElement(&m_xAimPosition);
|
|
m_netGroup.addNetElement(&m_yAimPosition);
|
|
|
|
m_xAimPosition.setFixedPointBase(0.0625);
|
|
m_yAimPosition.setFixedPointBase(0.0625);
|
|
m_xAimPosition.setInterpolator(lerp<float, float>);
|
|
m_yAimPosition.setInterpolator(lerp<float, float>);
|
|
|
|
m_netGroup.addNetElement(&m_uniqueIdNetState);
|
|
m_netGroup.addNetElement(&m_teamNetState);
|
|
m_netGroup.addNetElement(&m_humanoidStateNetState);
|
|
m_netGroup.addNetElement(&m_humanoidEmoteStateNetState);
|
|
m_netGroup.addNetElement(&m_humanoidDanceNetState);
|
|
|
|
m_netGroup.addNetElement(&m_newChatMessageEvent);
|
|
m_netGroup.addNetElement(&m_chatMessage);
|
|
m_netGroup.addNetElement(&m_chatPortrait);
|
|
m_netGroup.addNetElement(&m_chatConfig);
|
|
|
|
m_netGroup.addNetElement(&m_statusText);
|
|
m_netGroup.addNetElement(&m_displayNametag);
|
|
|
|
m_netGroup.addNetElement(&m_isInteractive);
|
|
|
|
m_netGroup.addNetElement(&m_offeredQuests);
|
|
m_netGroup.addNetElement(&m_turnInQuests);
|
|
|
|
m_netGroup.addNetElement(&m_shifting);
|
|
m_netGroup.addNetElement(&m_damageOnTouch);
|
|
|
|
m_netGroup.addNetElement(&m_disableWornArmor);
|
|
|
|
m_netGroup.addNetElement(&m_deathParticleBurst);
|
|
|
|
m_netGroup.addNetElement(&m_dropPools);
|
|
m_netGroup.addNetElement(&m_aggressive);
|
|
|
|
m_netGroup.addNetElement(m_movementController.get());
|
|
m_netGroup.addNetElement(m_effectEmitter.get());
|
|
m_netGroup.addNetElement(m_statusController.get());
|
|
m_netGroup.addNetElement(m_armor.get());
|
|
m_netGroup.addNetElement(m_tools.get());
|
|
|
|
m_netGroup.setNeedsStoreCallback(bind(&Npc::setNetStates, this));
|
|
m_netGroup.setNeedsLoadCallback(bind(&Npc::getNetStates, this, _1));
|
|
}
|
|
|
|
void Npc::setNetStates() {
|
|
m_uniqueIdNetState.set(uniqueId());
|
|
m_teamNetState.set(getTeam());
|
|
m_humanoidStateNetState.set(m_humanoid.state());
|
|
m_humanoidEmoteStateNetState.set(m_humanoid.emoteState());
|
|
m_humanoidDanceNetState.set(m_humanoid.dance());
|
|
}
|
|
|
|
void Npc::getNetStates(bool initial) {
|
|
setUniqueId(m_uniqueIdNetState.get());
|
|
setTeam(m_teamNetState.get());
|
|
m_humanoid.setState(m_humanoidStateNetState.get());
|
|
m_humanoid.setEmoteState(m_humanoidEmoteStateNetState.get());
|
|
m_humanoid.setDance(m_humanoidDanceNetState.get());
|
|
|
|
if (m_newChatMessageEvent.pullOccurred() && !initial) {
|
|
m_chatMessageUpdated = true;
|
|
if (m_chatPortrait.get().empty())
|
|
m_pendingChatActions.append(SayChatAction{entityId(), m_chatMessage.get(), mouthPosition(), m_chatConfig.get()});
|
|
else
|
|
m_pendingChatActions.append(PortraitChatAction{
|
|
entityId(), m_chatPortrait.get(), m_chatMessage.get(), mouthPosition(), m_chatConfig.get()});
|
|
}
|
|
}
|
|
|
|
Vec2F Npc::mouthPosition() const {
|
|
return mouthOffset(true) + position();
|
|
}
|
|
|
|
Vec2F Npc::mouthPosition(bool ignoreAdjustments) const {
|
|
return mouthOffset(ignoreAdjustments) + position();
|
|
}
|
|
|
|
List<ChatAction> Npc::pullPendingChatActions() {
|
|
return std::move(m_pendingChatActions);
|
|
}
|
|
|
|
void Npc::addChatMessage(String const& message, Json const& config, String const& portrait) {
|
|
assert(!isSlave());
|
|
m_chatMessage.set(message);
|
|
m_chatPortrait.set(portrait);
|
|
m_chatConfig.set(config);
|
|
m_chatMessageUpdated = true;
|
|
m_newChatMessageEvent.trigger();
|
|
if (portrait.empty())
|
|
m_pendingChatActions.append(SayChatAction{entityId(), message, mouthPosition(), config});
|
|
else
|
|
m_pendingChatActions.append(PortraitChatAction{entityId(), portrait, message, mouthPosition(), config});
|
|
}
|
|
|
|
void Npc::addEmote(HumanoidEmote const& emote) {
|
|
assert(!isSlave());
|
|
m_emoteState = emote;
|
|
m_emoteCooldownTimer.reset();
|
|
}
|
|
|
|
void Npc::setDance(Maybe<String> const& danceName) {
|
|
assert(!isSlave());
|
|
m_dance = danceName;
|
|
|
|
if (danceName.isValid()) {
|
|
auto danceDatabase = Root::singleton().danceDatabase();
|
|
DancePtr dance = danceDatabase->getDance(*danceName);
|
|
m_danceCooldownTimer = GameTimer(dance->duration);
|
|
}
|
|
}
|
|
|
|
bool Npc::isInteractive() const {
|
|
return m_isInteractive.get();
|
|
}
|
|
|
|
InteractAction Npc::interact(InteractRequest const& request) {
|
|
auto result = m_scriptComponent.invoke<Json>("interact",
|
|
JsonObject{{"sourceId", request.sourceId}, {"sourcePosition", jsonFromVec2F(request.sourcePosition)}}).value();
|
|
|
|
if (result.isNull())
|
|
return {};
|
|
|
|
if (result.isType(Json::Type::String))
|
|
return InteractAction(result.toString(), entityId(), Json());
|
|
|
|
return InteractAction(result.getString(0), entityId(), result.get(1));
|
|
}
|
|
|
|
RectF Npc::interactiveBoundBox() const {
|
|
return m_movementController->collisionPoly().boundBox();
|
|
}
|
|
|
|
Maybe<EntityAnchorState> Npc::loungingIn() const {
|
|
if (is<LoungeAnchor>(m_movementController->entityAnchor()))
|
|
return m_movementController->anchorState();
|
|
return {};
|
|
}
|
|
|
|
List<QuestArcDescriptor> Npc::offeredQuests() const {
|
|
return m_offeredQuests.get();
|
|
}
|
|
|
|
StringSet Npc::turnInQuests() const {
|
|
return m_turnInQuests.get();
|
|
}
|
|
|
|
Vec2F Npc::questIndicatorPosition() const {
|
|
Vec2F pos = position() + m_questIndicatorOffset;
|
|
pos[1] += interactiveBoundBox().yMax();
|
|
return pos;
|
|
}
|
|
|
|
bool Npc::setItemSlot(String const& slot, ItemDescriptor itemDescriptor) {
|
|
auto item = Root::singleton().itemDatabase()->item(ItemDescriptor(itemDescriptor), m_npcVariant.level, m_npcVariant.seed);
|
|
|
|
if (slot.equalsIgnoreCase("head"))
|
|
m_armor->setHeadItem(as<HeadArmor>(item));
|
|
else if (slot.equalsIgnoreCase("headCosmetic"))
|
|
m_armor->setHeadCosmeticItem(as<HeadArmor>(item));
|
|
else if (slot.equalsIgnoreCase("chest"))
|
|
m_armor->setChestItem(as<ChestArmor>(item));
|
|
else if (slot.equalsIgnoreCase("chestCosmetic"))
|
|
m_armor->setChestCosmeticItem(as<ChestArmor>(item));
|
|
else if (slot.equalsIgnoreCase("legs"))
|
|
m_armor->setLegsItem(as<LegsArmor>(item));
|
|
else if (slot.equalsIgnoreCase("legsCosmetic"))
|
|
m_armor->setLegsCosmeticItem(as<LegsArmor>(item));
|
|
else if (slot.equalsIgnoreCase("back"))
|
|
m_armor->setBackItem(as<BackArmor>(item));
|
|
else if (slot.equalsIgnoreCase("backCosmetic"))
|
|
m_armor->setBackCosmeticItem(as<BackArmor>(item));
|
|
else if (slot.equalsIgnoreCase("primary"))
|
|
m_tools->setItems(item, m_tools->altHandItem());
|
|
else if (slot.equalsIgnoreCase("alt"))
|
|
m_tools->setItems(m_tools->primaryHandItem(), item);
|
|
else
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Npc::canUseTool() const {
|
|
return !shouldDestroy() && !loungingIn();
|
|
}
|
|
|
|
void Npc::disableWornArmor(bool disable) {
|
|
m_disableWornArmor.set(disable);
|
|
}
|
|
|
|
List<LightSource> Npc::lightSources() const {
|
|
List<LightSource> lights;
|
|
lights.appendAll(m_tools->lightSources());
|
|
lights.appendAll(m_statusController->lightSources());
|
|
return lights;
|
|
}
|
|
|
|
Maybe<Json> Npc::receiveMessage(ConnectionId sendingConnection, String const& message, JsonArray const& args) {
|
|
Maybe<Json> result = m_scriptComponent.handleMessage(message, world()->connection() == sendingConnection, args);
|
|
if (!result)
|
|
result = m_statusController->receiveMessage(message, world()->connection() == sendingConnection, args);
|
|
return result;
|
|
}
|
|
|
|
Vec2F Npc::armPosition(ToolHand hand, Direction facingDirection, float armAngle, Vec2F offset) const {
|
|
return m_tools->armPosition(m_humanoid, hand, facingDirection, armAngle, offset);
|
|
}
|
|
|
|
Vec2F Npc::handOffset(ToolHand hand, Direction facingDirection) const {
|
|
return m_tools->handOffset(m_humanoid, hand, facingDirection);
|
|
}
|
|
|
|
Vec2F Npc::handPosition(ToolHand hand, Vec2F const& handOffset) const {
|
|
return m_tools->handPosition(hand, m_humanoid, handOffset);
|
|
}
|
|
|
|
ItemPtr Npc::handItem(ToolHand hand) const {
|
|
if (hand == ToolHand::Primary)
|
|
return m_tools->primaryHandItem();
|
|
return m_tools->altHandItem();
|
|
}
|
|
|
|
Vec2F Npc::armAdjustment() const {
|
|
return m_humanoid.armAdjustment();
|
|
}
|
|
|
|
Vec2F Npc::velocity() const {
|
|
return m_movementController->velocity();
|
|
}
|
|
|
|
Vec2F Npc::aimPosition() const {
|
|
return world()->geometry().xwrap(Vec2F(m_xAimPosition.get(), m_yAimPosition.get()) + position());
|
|
}
|
|
|
|
float Npc::interactRadius() const {
|
|
return 9999;
|
|
}
|
|
|
|
Direction Npc::facingDirection() const {
|
|
return m_movementController->facingDirection();
|
|
}
|
|
|
|
Direction Npc::walkingDirection() const {
|
|
return m_movementController->movingDirection();
|
|
}
|
|
|
|
bool Npc::isAdmin() const {
|
|
return false;
|
|
}
|
|
|
|
Vec4B Npc::favoriteColor() const {
|
|
return Color::White.toRgba();
|
|
}
|
|
|
|
float Npc::beamGunRadius() const {
|
|
return m_tools->beamGunRadius();
|
|
}
|
|
|
|
void Npc::addParticles(List<Particle> const&) {}
|
|
|
|
void Npc::addSound(String const&, float, float) {}
|
|
|
|
bool Npc::inToolRange() const {
|
|
return true;
|
|
}
|
|
|
|
bool Npc::inToolRange(Vec2F const&) const {
|
|
return true;
|
|
}
|
|
|
|
void Npc::addEphemeralStatusEffects(List<EphemeralStatusEffect> const& statusEffects) {
|
|
m_statusController->addEphemeralEffects(statusEffects);
|
|
}
|
|
|
|
ActiveUniqueStatusEffectSummary Npc::activeUniqueStatusEffectSummary() const {
|
|
return m_statusController->activeUniqueStatusEffectSummary();
|
|
}
|
|
|
|
float Npc::powerMultiplier() const {
|
|
return m_statusController->stat("powerMultiplier");
|
|
}
|
|
|
|
bool Npc::fullEnergy() const {
|
|
return *m_statusController->resourcePercentage("energy") >= 1.0;
|
|
}
|
|
|
|
float Npc::energy() const {
|
|
return m_statusController->resource("energy");
|
|
}
|
|
|
|
bool Npc::energyLocked() const {
|
|
return m_statusController->resourceLocked("energy");
|
|
}
|
|
|
|
bool Npc::consumeEnergy(float energy) {
|
|
return m_statusController->overConsumeResource("energy", energy);
|
|
}
|
|
|
|
void Npc::queueUIMessage(String const&) {}
|
|
|
|
bool Npc::instrumentPlaying() {
|
|
return false; // TODO: remove this from tool user entirely
|
|
}
|
|
|
|
void Npc::instrumentEquipped(String const&) {}
|
|
|
|
void Npc::interact(InteractAction const&) {}
|
|
|
|
void Npc::addEffectEmitters(StringSet const& emitters) {
|
|
m_effectEmitter->addEffectSources("normal", emitters);
|
|
}
|
|
|
|
void Npc::requestEmote(String const& emote) {
|
|
if (!emote.empty()) {
|
|
auto state = HumanoidEmoteNames.getLeft(emote);
|
|
if (state != HumanoidEmote::Idle && (m_emoteState == HumanoidEmote::Idle || m_emoteState == HumanoidEmote::Blink))
|
|
addEmote(state);
|
|
}
|
|
}
|
|
|
|
ActorMovementController* Npc::movementController() {
|
|
return m_movementController.get();
|
|
}
|
|
|
|
StatusController* Npc::statusController() {
|
|
return m_statusController.get();
|
|
}
|
|
|
|
void Npc::setCameraFocusEntity(Maybe<EntityId> const&) {
|
|
// players only
|
|
}
|
|
|
|
void Npc::playEmote(HumanoidEmote emote) {
|
|
addEmote(emote);
|
|
}
|
|
|
|
List<DamageSource> Npc::damageSources() const {
|
|
auto damageSources = m_tools->damageSources();
|
|
|
|
if (m_damageOnTouch.get() && !m_npcVariant.touchDamageConfig.isNull()) {
|
|
Json config = m_npcVariant.touchDamageConfig;
|
|
if (!config.contains("poly") && !config.contains("line")) {
|
|
config = config.set("poly", jsonFromPolyF(m_movementController->collisionPoly()));
|
|
}
|
|
DamageSource damageSource(config);
|
|
if (auto damagePoly = damageSource.damageArea.ptr<PolyF>())
|
|
damagePoly->rotate(m_movementController->rotation());
|
|
damageSource.damage *= m_statusController->stat("powerMultiplier");
|
|
damageSources.append(damageSource);
|
|
}
|
|
|
|
for (auto& damageSource : damageSources) {
|
|
damageSource.sourceEntityId = entityId();
|
|
damageSource.team = getTeam();
|
|
}
|
|
|
|
return damageSources;
|
|
}
|
|
|
|
List<PhysicsForceRegion> Npc::forceRegions() const {
|
|
return m_tools->forceRegions();
|
|
}
|
|
|
|
}
|