2023-06-20 04:33:09 +00:00
|
|
|
#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"));
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-06-26 14:42:07 +00:00
|
|
|
Vec2F Npc::mouthOffset(bool ignoreAdjustments) const {
|
|
|
|
return Vec2F{m_humanoid.mouthOffset(ignoreAdjustments)[0] * numericalDirection(m_humanoid.facingDirection()),
|
|
|
|
m_humanoid.mouthOffset(ignoreAdjustments)[1]};
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
|
|
|
return m_netGroup.writeNetState(fromVersion);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Npc::readNetState(ByteArray data, float interpolationTime) {
|
|
|
|
m_netGroup.readNetState(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);
|
|
|
|
}
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
void Npc::update(float dt, uint64_t) {
|
2023-06-20 04:33:09 +00:00
|
|
|
if (!inWorld())
|
|
|
|
return;
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
m_movementController->setTimestep(dt);
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
if (isMaster()) {
|
2023-07-20 14:58:49 +00:00
|
|
|
m_scriptComponent.update(m_scriptComponent.updateDt(dt));
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
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());
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
m_movementController->tickMaster(dt);
|
|
|
|
m_statusController->tickMaster(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
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
if (m_emoteCooldownTimer.tick(dt))
|
2023-06-20 04:33:09 +00:00
|
|
|
m_emoteState = HumanoidEmote::Idle;
|
2023-07-20 14:58:49 +00:00
|
|
|
if (m_danceCooldownTimer.tick(dt))
|
2023-06-20 04:33:09 +00:00
|
|
|
m_dance = {};
|
|
|
|
|
|
|
|
if (m_chatMessageUpdated) {
|
|
|
|
auto state = Root::singleton().emoteProcessor()->detectEmotes(m_chatMessage.get());
|
|
|
|
if (state != HumanoidEmote::Idle)
|
|
|
|
addEmote(state);
|
|
|
|
m_chatMessageUpdated = false;
|
|
|
|
}
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
if (m_blinkCooldownTimer.tick(dt)) {
|
2023-06-20 04:33:09 +00:00
|
|
|
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(WorldTimestep);
|
2023-07-20 14:58:49 +00:00
|
|
|
m_movementController->tickSlave(dt);
|
|
|
|
m_statusController->tickSlave(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(), {0, 255, 0, 255});
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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())
|
2023-06-25 08:12:54 +00:00
|
|
|
drawable.imagePart().addDirectivesGroup(m_statusController->parentDirectives(), true);
|
2023-06-20 04:33:09 +00:00
|
|
|
renderCallback->addDrawable(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);
|
|
|
|
}
|
|
|
|
|
2023-06-29 00:11:19 +00:00
|
|
|
void Npc::renderLightSources(RenderCallback* renderCallback) {
|
|
|
|
renderCallback->addLightSources(lightSources());
|
|
|
|
}
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-06-26 14:42:07 +00:00
|
|
|
Vec2F Npc::nametagOrigin() const {
|
|
|
|
return mouthPosition(false);
|
|
|
|
}
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
void Npc::tickShared(float dt) {
|
2023-06-20 04:33:09 +00:00
|
|
|
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());
|
2023-07-20 14:58:49 +00:00
|
|
|
m_effectEmitter->tick(dt, *entityMode());
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
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());
|
2023-07-20 14:58:49 +00:00
|
|
|
m_tools->tick(dt, m_shifting.get(), {});
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
if (auto overrideDirection = m_tools->setupHumanoidHandItems(m_humanoid, position(), aimPosition()))
|
|
|
|
m_movementController->controlFace(*overrideDirection);
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
m_humanoid.animate(dt);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2023-06-26 14:42:07 +00:00
|
|
|
return mouthOffset(true) + position();
|
|
|
|
}
|
|
|
|
|
|
|
|
Vec2F Npc::mouthPosition(bool ignoreAdjustments) const {
|
|
|
|
return mouthOffset(ignoreAdjustments) + position();
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {}
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|