diff options
author | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
---|---|---|
committer | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
commit | 6352e8e3196f78388b6c771073f9e03eaa612673 (patch) | |
tree | e23772f79a7fbc41bc9108951e9e136857484bf4 /source/game/StarNpc.cpp | |
parent | 6741a057e5639280d85d0f88ba26f000baa58f61 (diff) |
everything everywhere
all at once
Diffstat (limited to 'source/game/StarNpc.cpp')
-rw-r--r-- | source/game/StarNpc.cpp | 1095 |
1 files changed, 1095 insertions, 0 deletions
diff --git a/source/game/StarNpc.cpp b/source/game/StarNpc.cpp new file mode 100644 index 0000000..62b2c06 --- /dev/null +++ b/source/game/StarNpc.cpp @@ -0,0 +1,1095 @@ +#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); +} + +Vec2F Npc::mouthOffset() const { + return Vec2F{m_humanoid.mouthOffset(true)[0] * numericalDirection(m_humanoid.facingDirection()), + m_humanoid.mouthOffset(true)[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) { + 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); +} + +void Npc::update(uint64_t) { + if (!inWorld()) + return; + + if (isMaster()) { + m_scriptComponent.update(m_scriptComponent.updateDt()); + + 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(); + m_statusController->tickMaster(); + + tickShared(); + + 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()) + m_emoteState = HumanoidEmote::Idle; + if (m_danceCooldownTimer.tick()) + 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()) { + 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); + m_movementController->tickSlave(); + m_statusController->tickSlave(); + + tickShared(); + } + + 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().addDirectives(m_statusController->parentDirectives(), true); + 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())); + renderCallback->addLightSources(lightSources()); + + 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::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; +} + +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() { + 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(*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(m_shifting.get(), {}); + + if (auto overrideDirection = m_tools->setupHumanoidHandItems(m_humanoid, position(), aimPosition())) + m_movementController->controlFace(*overrideDirection); + + m_humanoid.animate(WorldTimestep); +} + +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() + 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) {} + +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(); +} + +} |