diff options
Diffstat (limited to 'source/game/StarPlayer.cpp')
-rw-r--r-- | source/game/StarPlayer.cpp | 2374 |
1 files changed, 2374 insertions, 0 deletions
diff --git a/source/game/StarPlayer.cpp b/source/game/StarPlayer.cpp new file mode 100644 index 0000000..625b8cb --- /dev/null +++ b/source/game/StarPlayer.cpp @@ -0,0 +1,2374 @@ +#include "StarPlayer.hpp" +#include "StarJsonExtra.hpp" +#include "StarRoot.hpp" +#include "StarSongbook.hpp" +#include "StarEmoteProcessor.hpp" +#include "StarSpeciesDatabase.hpp" +#include "StarDamageManager.hpp" +#include "StarTools.hpp" +#include "StarItemDrop.hpp" +#include "StarMaterialDatabase.hpp" +#include "StarArmors.hpp" +#include "StarPlayerFactory.hpp" +#include "StarAssets.hpp" +#include "StarPlayerInventory.hpp" +#include "StarTechController.hpp" +#include "StarClientContext.hpp" +#include "StarItemDatabase.hpp" +#include "StarItemBag.hpp" +#include "StarEntitySplash.hpp" +#include "StarWorld.hpp" +#include "StarStatusController.hpp" +#include "StarStatusControllerLuaBindings.hpp" +#include "StarPlayerBlueprints.hpp" +#include "StarPlayerUniverseMap.hpp" +#include "StarPlayerCodexes.hpp" +#include "StarPlayerTech.hpp" +#include "StarPlayerCompanions.hpp" +#include "StarPlayerDeployment.hpp" +#include "StarPlayerLog.hpp" +#include "StarPlayerLuaBindings.hpp" +#include "StarQuestManager.hpp" +#include "StarAiDatabase.hpp" +#include "StarStatistics.hpp" +#include "StarInspectionTool.hpp" +#include "StarUtilityLuaBindings.hpp" +#include "StarCelestialLuaBindings.hpp" + +namespace Star { + +EnumMap<Player::State> const Player::StateNames{ + {Player::State::Idle, "idle"}, + {Player::State::Walk, "walk"}, + {Player::State::Run, "run"}, + {Player::State::Jump, "jump"}, + {Player::State::Fall, "fall"}, + {Player::State::Swim, "swim"}, + {Player::State::SwimIdle, "swimIdle"}, + {Player::State::TeleportIn, "teleportIn"}, + {Player::State::TeleportOut, "teleportOut"}, + {Player::State::Crouch, "crouch"}, + {Player::State::Lounge, "lounge"} +}; + +Player::Player(PlayerConfigPtr config, Uuid uuid) { + auto assets = Root::singleton().assets(); + + m_config = config; + + m_state = State::Idle; + m_emoteState = HumanoidEmote::Idle; + + m_footstepTimer = 0.0f; + m_teleportTimer = 0.0f; + m_teleportAnimationType = "default"; + + m_shifting = false; + + m_aimPosition = Vec2F(); + + setUniqueId(uuid.hex()); + m_identity = m_config->defaultIdentity; + m_identityUpdated = true; + + m_questManager = make_shared<QuestManager>(this); + m_tools = make_shared<ToolUser>(); + m_armor = make_shared<ArmorWearer>(); + m_companions = make_shared<PlayerCompanions>(config->companionsConfig); + + for (auto& p : config->genericScriptContexts) { + auto scriptComponent = make_shared<GenericScriptComponent>(); + scriptComponent->setScript(p.second); + m_genericScriptContexts.set(p.first, scriptComponent); + } + + // all of these are defaults and won't include the correct humanoid config for the species + m_humanoid = make_shared<Humanoid>(Root::singleton().speciesDatabase()->species(m_identity.species)->humanoidConfig()); + m_humanoid->setIdentity(m_identity); + auto movementParameters = ActorMovementParameters(jsonMerge(m_humanoid->defaultMovementParameters(), m_config->movementParameters)); + if (!movementParameters.physicsEffectCategories) + movementParameters.physicsEffectCategories = StringSet({"player"}); + m_movementController = make_shared<ActorMovementController>(movementParameters); + m_zeroGMovementParameters = ActorMovementParameters(m_config->zeroGMovementParameters); + + m_techController = make_shared<TechController>(); + m_statusController = make_shared<StatusController>(m_config->statusControllerSettings); + m_deployment = make_shared<PlayerDeployment>(m_config->deploymentConfig); + + m_inventory = make_shared<PlayerInventory>(); + m_blueprints = make_shared<PlayerBlueprints>(); + m_universeMap = make_shared<PlayerUniverseMap>(); + m_codexes = make_shared<PlayerCodexes>(); + m_techs = make_shared<PlayerTech>(); + m_log = make_shared<PlayerLog>(); + + m_description = strf("This %s seems to have nothing to say for %sself.", + m_identity.gender == Gender::Male ? "guy" : "gal", + m_identity.gender == Gender::Male ? "him" : "her"); + + setModeType(PlayerMode::Casual); + + m_useDown = false; + m_edgeTriggeredUse = false; + setTeam(EntityDamageTeam(TeamType::Friendly)); + + m_footstepVolumeVariance = assets->json("/sfx.config:footstepVolumeVariance").toFloat(); + m_landingVolume = assets->json("/sfx.config:landingVolume").toFloat(); + + m_effectsAnimator = make_shared<NetworkedAnimator>(assets->fetchJson(m_config->effectsAnimator)); + m_effectEmitter = make_shared<EffectEmitter>(); + + m_interactRadius = assets->json("/player.config:interactRadius").toFloat(); + + m_walkIntoInteractBias = jsonToVec2F(assets->json("/player.config:walkIntoInteractBias")); + + if (m_landingVolume <= 1) + m_landingVolume = 6; + + m_isAdmin = false; + + m_emoteCooldown = assets->json("/player.config:emoteCooldown").toFloat(); + m_blinkInterval = jsonToVec2F(assets->json("/player.config:blinkInterval")); + + m_emoteCooldownTimer = 0; + m_blinkCooldownTimer = 0; + + m_chatMessageChanged = false; + m_chatMessageUpdated = false; + + m_songbook = make_shared<Songbook>(species()); + + m_lastDamagedOtherTimer = 0; + m_lastDamagedTarget = NullEntityId; + + m_ageItemsTimer = GameTimer(assets->json("/player.config:ageItemsEvery").toFloat()); + + refreshEquipment(); + + m_foodLowThreshold = assets->json("/player.config:foodLowThreshold").toFloat(); + m_foodLowStatusEffects = assets->json("/player.config:foodLowStatusEffects").toArray().transformed(jsonToPersistentStatusEffect); + m_foodEmptyStatusEffects = assets->json("/player.config:foodEmptyStatusEffects").toArray().transformed(jsonToPersistentStatusEffect); + + m_inCinematicStatusEffects = assets->json("/player.config:inCinematicStatusEffects").toArray().transformed(jsonToPersistentStatusEffect); + + m_statusController->setPersistentEffects("armor", m_armor->statusEffects()); + m_statusController->setPersistentEffects("tools", m_tools->statusEffects()); + m_statusController->resetAllResources(); + + m_landingNoisePending = false; + + setKeepAlive(true); + + m_netGroup.addNetElement(&m_stateNetState); + m_netGroup.addNetElement(&m_shiftingNetState); + m_netGroup.addNetElement(&m_xAimPositionNetState); + m_netGroup.addNetElement(&m_yAimPositionNetState); + m_netGroup.addNetElement(&m_identityNetState); + m_netGroup.addNetElement(&m_teamNetState); + m_netGroup.addNetElement(&m_landedNetState); + m_netGroup.addNetElement(&m_chatMessageNetState); + m_netGroup.addNetElement(&m_newChatMessageNetState); + m_netGroup.addNetElement(&m_emoteNetState); + + m_xAimPositionNetState.setFixedPointBase(0.003125); + m_yAimPositionNetState.setFixedPointBase(0.003125); + m_yAimPositionNetState.setInterpolator(lerp<float, float>); + + m_netGroup.addNetElement(m_inventory.get()); + m_netGroup.addNetElement(m_tools.get()); + m_netGroup.addNetElement(m_armor.get()); + m_netGroup.addNetElement(m_songbook.get()); + m_netGroup.addNetElement(m_movementController.get()); + m_netGroup.addNetElement(m_effectEmitter.get()); + m_netGroup.addNetElement(m_effectsAnimator.get()); + m_netGroup.addNetElement(m_statusController.get()); + m_netGroup.addNetElement(m_techController.get()); + + m_netGroup.setNeedsLoadCallback(bind(&Player::getNetStates, this, _1)); + m_netGroup.setNeedsStoreCallback(bind(&Player::setNetStates, this)); +} + +Player::Player(PlayerConfigPtr config, Json const& diskStore) : Player(config) { + setUniqueId(diskStore.getString("uuid")); + m_description = diskStore.getString("description"); + setModeType(PlayerModeNames.getLeft(diskStore.getString("modeType"))); + m_shipUpgrades = ShipUpgrades(diskStore.get("shipUpgrades")); + m_blueprints = make_shared<PlayerBlueprints>(diskStore.get("blueprints")); + m_universeMap = make_shared<PlayerUniverseMap>(diskStore.get("universeMap")); + m_codexes = make_shared<PlayerCodexes>(diskStore.get("codexes")); + m_techs = make_shared<PlayerTech>(diskStore.get("techs")); + m_identity = HumanoidIdentity(diskStore.get("identity")); + setTeam(EntityDamageTeam(diskStore.get("team"))); + + m_state = State::Idle; + + m_inventory->load(diskStore.get("inventory")); + + m_movementController->loadState(diskStore.get("movementController")); + m_techController->diskLoad(diskStore.get("techController")); + m_statusController->diskLoad(diskStore.get("statusController")); + + m_log = make_shared<PlayerLog>(diskStore.get("log")); + + m_codexes->learnInitialCodexes(species()); + + // Make sure to merge the stored player blueprints with what a new player + // would get as default. + for (auto const& descriptor : m_config->defaultBlueprints) + m_blueprints->add(descriptor); + for (auto const& descriptor : Root::singleton().speciesDatabase()->species(m_identity.species)->defaultBlueprints()) + m_blueprints->add(descriptor); + + m_questManager->diskLoad(diskStore.get("quests", JsonObject{})); + m_companions->diskLoad(diskStore.get("companions", JsonObject{})); + m_deployment->diskLoad(diskStore.get("deployment", JsonObject{})); + m_humanoid = make_shared<Humanoid>(Root::singleton().speciesDatabase()->species(m_identity.species)->humanoidConfig()); + m_humanoid->setIdentity(m_identity); + m_movementController->resetBaseParameters(ActorMovementParameters(jsonMerge(m_humanoid->defaultMovementParameters(), m_config->movementParameters))); + m_effectsAnimator->setGlobalTag("effectDirectives", Root::singleton().speciesDatabase()->species(m_identity.species)->effectDirectives()); + + m_genericProperties = diskStore.getObject("genericProperties"); + + refreshEquipment(); + + m_aiState = AiState(diskStore.get("aiState", JsonObject{})); + + for (auto& p : diskStore.get("genericScriptStorage", JsonObject{}).toObject()) { + if (auto script = m_genericScriptContexts.maybe(p.first).value({})) { + script->setScriptStorage(p.second.toObject()); + } + } +} + +Player::Player(PlayerConfigPtr config, ByteArray const& netStore) : Player(config) { + DataStreamBuffer ds(netStore); + + setUniqueId(ds.read<String>()); + + ds.read(m_description); + ds.read(m_modeType); + ds.read(m_identity); + + m_humanoid = make_shared<Humanoid>(Root::singleton().speciesDatabase()->species(m_identity.species)->humanoidConfig()); + m_humanoid->setIdentity(m_identity); + m_movementController->resetBaseParameters(ActorMovementParameters(jsonMerge(m_humanoid->defaultMovementParameters(), m_config->movementParameters))); +} + +ClientContextPtr Player::clientContext() const { + return m_clientContext; +} + +void Player::setClientContext(ClientContextPtr clientContext) { + m_clientContext = move(clientContext); + if (m_clientContext) + m_universeMap->setServerUuid(m_clientContext->serverUuid()); +} + +StatisticsPtr Player::statistics() const { + return m_statistics; +} + +void Player::setStatistics(StatisticsPtr statistics) { + m_statistics = statistics; +} + +void Player::setUniverseClient(UniverseClient* client) { + m_client = client; + m_questManager->setUniverseClient(client); +} + +EntityType Player::entityType() const { + return EntityType::Player; +} + +void Player::init(World* world, EntityId entityId, EntityMode mode) { + Entity::init(world, entityId, mode); + + auto speciesDefinition = Root::singleton().speciesDatabase()->species(m_identity.species); + m_statusController->setStatusProperty("ouchNoise", speciesDefinition->ouchNoise(m_identity.gender)); + + m_tools->init(this); + m_movementController->init(world); + m_movementController->setIgnorePhysicsEntities({entityId}); + m_movementController->setRotation(0); + m_statusController->init(this, m_movementController.get()); + m_techController->init(this, m_movementController.get(), m_statusController.get()); + + if (mode == EntityMode::Master) { + m_emoteState = HumanoidEmote::Idle; + m_questManager->init(world); + m_companions->init(this, world); + m_deployment->init(this, world); + m_missionRadioMessages.clear(); + + m_statusController->setPersistentEffects("species", speciesDefinition->statusEffects()); + + for (auto& p : m_genericScriptContexts) { + p.second->addActorMovementCallbacks(m_movementController.get()); + p.second->addCallbacks("player", LuaBindings::makePlayerCallbacks(this)); + p.second->addCallbacks("status", LuaBindings::makeStatusControllerCallbacks(m_statusController.get())); + if (m_client) + p.second->addCallbacks("celestial", LuaBindings::makeCelestialCallbacks(m_client)); + p.second->init(world); + } + } + + m_xAimPositionNetState.setInterpolator(world->geometry().xLerpFunction()); + refreshEquipment(); +} + +void Player::uninit() { + m_techController->uninit(); + m_movementController->uninit(); + m_tools->uninit(); + m_statusController->uninit(); + + if (isMaster()) { + m_questManager->uninit(); + m_companions->uninit(); + m_deployment->uninit(); + + for (auto& p : m_genericScriptContexts) { + p.second->uninit(); + p.second->removeCallbacks("entity"); + p.second->removeCallbacks("player"); + p.second->removeCallbacks("mcontroller"); + p.second->removeCallbacks("status"); + p.second->removeCallbacks("world"); + if (m_client) + p.second->removeCallbacks("celestial"); + } + } + + Entity::uninit(); +} + +List<Drawable> Player::drawables() const { + List<Drawable> drawables; + + if (!isTeleporting()) { + drawables.appendAll(m_techController->backDrawables()); + if (!m_techController->parentHidden()) { + m_tools->setupHumanoidHandItemDrawables(*m_humanoid); + for (auto& drawable : m_humanoid->render()) { + drawable.translate(position() + m_techController->parentOffset()); + if (drawable.isImage()) { + drawable.imagePart().addDirectives(m_techController->parentDirectives(), true); + drawable.imagePart().addDirectives(m_statusController->parentDirectives(), true); + + if (auto anchor = as<LoungeAnchor>(m_movementController->entityAnchor())) { + if (auto directives = anchor->directives) + drawable.imagePart().addDirectives(*directives, true); + } + } + drawables.append(move(drawable)); + } + } + drawables.appendAll(m_techController->frontDrawables()); + + drawables.appendAll(m_statusController->drawables()); + + drawables.appendAll(m_tools->renderObjectPreviews(aimPosition(), walkingDirection(), inToolRange(), favoriteColor())); + } + + drawables.appendAll(m_effectsAnimator->drawables(position())); + + return drawables; +} + +List<OverheadBar> Player::bars() const { + return m_statusController->overheadBars(); +} + +List<Particle> Player::particles() { + List<Particle> particles; + particles.appendAll(m_config->splashConfig.doSplash(position(), m_movementController->velocity(), world())); + particles.appendAll(take(m_callbackParticles)); + particles.appendAll(m_techController->pullNewParticles()); + particles.appendAll(m_statusController->pullNewParticles()); + + return particles; +} + +void Player::addParticles(List<Particle> const& particles) { + m_callbackParticles.appendAll(particles); +} + +void Player::addSound(String const& sound, float volume) { + m_callbackSounds.append({sound, volume}); +} + +void Player::addEphemeralStatusEffects(List<EphemeralStatusEffect> const& statusEffects) { + if (isSlave()) + throw PlayerException("Adding status effects to an entity can only be done directly on the master entity."); + m_statusController->addEphemeralEffects(statusEffects); +} + +ActiveUniqueStatusEffectSummary Player::activeUniqueStatusEffectSummary() const { + return m_statusController->activeUniqueStatusEffectSummary(); +} + +float Player::powerMultiplier() const { + return m_statusController->stat("powerMultiplier"); +} + +bool Player::isDead() const { + return !m_statusController->resourcePositive("health"); +} + +void Player::kill() { + m_statusController->setResource("health", 0); +} + +bool Player::wireToolInUse() const { + return (bool)as<WireTool>(m_tools->primaryHandItem()); +} + +void Player::setWireConnector(WireConnector* wireConnector) const { + if (auto wireTool = as<WireTool>(m_tools->primaryHandItem())) + wireTool->setConnector(wireConnector); +} + +List<Drawable> Player::portrait(PortraitMode mode) const { + if (isPermaDead()) + return m_humanoid->renderSkull(); + if (invisible()) + return {}; + m_armor->setupHumanoidClothingDrawables(*m_humanoid, forceNude()); + return m_humanoid->renderPortrait(mode); +} + +bool Player::underwater() const { + if (!inWorld()) + return false; + else + return world()->liquidLevel(Vec2I(position() + m_config->underwaterSensor)).level + >= m_config->underwaterMinWaterLevel; +} + +List<LightSource> Player::lightSources() const { + List<LightSource> lights; + lights.appendAll(m_tools->lightSources()); + lights.appendAll(m_statusController->lightSources()); + lights.appendAll(m_techController->lightSources()); + return lights; +} + +RectF Player::metaBoundBox() const { + return m_config->metaBoundBox; +} + +Maybe<HitType> Player::queryHit(DamageSource const& source) const { + if (!inWorld() || isDead() || m_isAdmin || isTeleporting() || 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> Player::hitPoly() const { + return m_movementController->collisionBody(); +} + +List<DamageNotification> Player::applyDamage(DamageRequest const& request) { + if (!inWorld() || isDead() || m_isAdmin) + return {}; + + return m_statusController->applyDamageRequest(request); +} + +List<DamageNotification> Player::selfDamageNotifications() { + return m_statusController->pullSelfDamageNotifications(); +} + +void Player::hitOther(EntityId targetEntityId, DamageRequest const& damageRequest) { + if (!isMaster()) + return; + + m_statusController->hitOther(targetEntityId, damageRequest); + if (as<DamageBarEntity>(world()->entity(targetEntityId))) { + m_lastDamagedOtherTimer = 0; + m_lastDamagedTarget = targetEntityId; + } +} + +void Player::damagedOther(DamageNotification const& damage) { + if (!isMaster()) + return; + + m_statusController->damagedOther(damage); +} + +List<DamageSource> Player::damageSources() const { + return m_damageSources; +} + +bool Player::shouldDestroy() const { + return isDead(); +} + +void Player::destroy(RenderCallback* renderCallback) { + m_state = State::Idle; + m_emoteState = HumanoidEmote::Idle; + if (renderCallback) { + List<Particle> deathParticles = m_humanoid->particles(m_humanoid->defaultDeathParticles()); + renderCallback->addParticles(deathParticles, position()); + } + + if (isMaster()) { + m_log->addDeathCount(1); + + if (!world()->disableDeathDrops()) { + if (auto dropString = modeConfig().deathDropItemTypes.maybeLeft()) { + if (*dropString == "all") + dropEverything(); + } else { + List<ItemType> dropList = modeConfig().deathDropItemTypes.right().transformed([](String typeName) { + return ItemTypeNames.getLeft(typeName); + }); + Set<ItemType> dropSet = Set<ItemType>::from(dropList); + auto itemDb = Root::singleton().itemDatabase(); + dropSelectedItems([dropSet, itemDb](ItemPtr item) { + return dropSet.contains(itemDb->itemType(item->name())); + }); + } + } + } + + m_songbook->stop(); +} + +Maybe<EntityAnchorState> Player::loungingIn() const { + if (is<LoungeAnchor>(m_movementController->entityAnchor())) + return m_movementController->anchorState(); + return {}; +} + +bool Player::lounge(EntityId loungeableEntityId, size_t anchorIndex) { + if (!canUseTool()) + return false; + + auto loungeableEntity = world()->get<LoungeableEntity>(loungeableEntityId); + if (!loungeableEntity || anchorIndex >= loungeableEntity->anchorCount() + || !loungeableEntity->entitiesLoungingIn(anchorIndex).empty() + || !loungeableEntity->loungeAnchor(anchorIndex)) + return false; + + m_state = State::Lounge; + m_movementController->setAnchorState({loungeableEntityId, anchorIndex}); + return true; +} + +void Player::stopLounging() { + if (loungingIn()) { + m_movementController->resetAnchorState(); + m_state = State::Idle; + m_statusController->setPersistentEffects("lounging", {}); + } +} + +Vec2F Player::position() const { + return m_movementController->position(); +} + +Vec2F Player::velocity() const { + return m_movementController->velocity(); +} + +Vec2F Player::mouthOffset() const { + return Vec2F( + m_humanoid->mouthOffset(true)[0] * numericalDirection(facingDirection()), m_humanoid->mouthOffset(true)[1]); +} + +Vec2F Player::feetOffset() const { + return Vec2F(m_humanoid->feetOffset()[0] * numericalDirection(facingDirection()), m_humanoid->feetOffset()[1]); +} + +Vec2F Player::headArmorOffset() const { + return Vec2F( + m_humanoid->headArmorOffset()[0] * numericalDirection(facingDirection()), m_humanoid->headArmorOffset()[1]); +} + +Vec2F Player::chestArmorOffset() const { + return Vec2F( + m_humanoid->chestArmorOffset()[0] * numericalDirection(facingDirection()), m_humanoid->chestArmorOffset()[1]); +} + +Vec2F Player::backArmorOffset() const { + return Vec2F( + m_humanoid->backArmorOffset()[0] * numericalDirection(facingDirection()), m_humanoid->backArmorOffset()[1]); +} + +Vec2F Player::legsArmorOffset() const { + return Vec2F( + m_humanoid->legsArmorOffset()[0] * numericalDirection(facingDirection()), m_humanoid->legsArmorOffset()[1]); +} + +Vec2F Player::mouthPosition() const { + return position() + mouthOffset(); +} + +RectF Player::collisionArea() const { + return m_movementController->collisionPoly().boundBox(); +} + +void Player::revive(Vec2F const& footPosition) { + if (!isDead()) + return; + + m_state = State::Idle; + m_emoteState = HumanoidEmote::Idle; + + m_statusController->setPersistentEffects("armor", m_armor->statusEffects()); + m_statusController->setPersistentEffects("tools", m_tools->statusEffects()); + m_statusController->resetAllResources(); + + m_statusController->clearEphemeralEffects(); + + endPrimaryFire(); + endAltFire(); + endTrigger(); + + m_effectEmitter->reset(); + m_movementController->setPosition(footPosition - feetOffset()); + m_movementController->setVelocity(Vec2F()); + + m_techController->reloadTech(); + + float moneyCost = m_inventory->currency("money") * modeConfig().reviveCostPercentile; + m_inventory->consumeCurrency("money", min((uint64_t)round(moneyCost), m_inventory->currency("money"))); +} + +void Player::setShifting(bool shifting) { + m_shifting = shifting; +} + +void Player::special(int specialKey) { + auto loungeAnchor = as<LoungeAnchor>(m_movementController->entityAnchor()); + if (loungeAnchor && loungeAnchor->controllable) { + auto anchorState = m_movementController->anchorState(); + if (auto loungeableEntity = world()->get<LoungeableEntity>(anchorState->entityId)) { + if (specialKey == 1) + loungeableEntity->loungeControl(anchorState->positionIndex, LoungeControl::Special1); + else if (specialKey == 2) + loungeableEntity->loungeControl(anchorState->positionIndex, LoungeControl::Special2); + else if (specialKey == 3) + loungeableEntity->loungeControl(anchorState->positionIndex, LoungeControl::Special3); + return; + } + } + m_techController->special(specialKey); +} + +void Player::moveLeft() { + m_pendingMoves.add(MoveControlType::Left); +} + +void Player::moveRight() { + m_pendingMoves.add(MoveControlType::Right); +} + +void Player::moveUp() { + m_pendingMoves.add(MoveControlType::Up); +} + +void Player::moveDown() { + m_pendingMoves.add(MoveControlType::Down); +} + +void Player::jump() { + m_pendingMoves.add(MoveControlType::Jump); +} + +void Player::dropItem() { + if (!world()) + return; + if (!canUseTool()) + return; + + for (auto throwSlot : {m_inventory->primaryHeldSlot(), m_inventory->secondaryHeldSlot()}) { + if (throwSlot) { + if (auto drop = m_inventory->takeSlot(*throwSlot)) { + world()->addEntity(ItemDrop::throwDrop(drop, position(), world()->geometry().diff(aimPosition(), position()))); + break; + } + } + } +} + +Maybe<Json> Player::receiveMessage(ConnectionId fromConnection, String const& message, JsonArray const& args) { + bool localMessage = fromConnection == world()->connection(); + if (message == "queueRadioMessage" && args.size() > 0) { + float delay = 0; + if (args.size() > 1 && args.get(1).canConvert(Json::Type::Float)) + delay = args.get(1).toFloat(); + + queueRadioMessage(args.get(0), delay); + } else if (message == "warp") { + Maybe<String> animation; + if (args.size() > 1) + animation = args.get(1).toString(); + + bool deploy = false; + if (args.size() > 2) + deploy = args.get(2).toBool(); + + setPendingWarp(args.get(0).toString(), animation, deploy); + } else if (message == "interruptRadioMessage") { + m_interruptRadioMessage = true; + } else if (message == "playCinematic" && args.size() > 0) { + bool unique = false; + if (args.size() > 1) + unique = args.get(1).toBool(); + setPendingCinematic(args.get(0), unique); + } else if (message == "playAltMusic" && args.size() > 0) { + float fadeTime = 0; + if (args.size() > 1) + fadeTime = args.get(1).toFloat(); + StringList trackList; + if (args.get(0).canConvert(Json::Type::Array)) + trackList = jsonToStringList(args.get(0).toArray()); + else + trackList = StringList(); + m_pendingAltMusic = pair<Maybe<StringList>, float>(trackList, fadeTime); + } else if (message == "stopAltMusic") { + float fadeTime = 0; + if (args.size() > 0) + fadeTime = args.get(0).toFloat(); + m_pendingAltMusic = pair<Maybe<StringList>, float>({}, fadeTime); + } else if (message == "recordEvent") { + statistics()->recordEvent(args.at(0).toString(), args.at(1)); + } else if (message == "addCollectable") { + auto collection = args.get(0).toString(); + auto collectable = args.get(1).toString(); + if (Root::singleton().collectionDatabase()->hasCollectable(collection, collectable)) + addCollectable(collection, collectable); + } else { + Maybe<Json> result = m_tools->receiveMessage(message, localMessage, args); + if (!result) + result = m_statusController->receiveMessage(message, localMessage, args); + if (!result) + result = m_companions->receiveMessage(message, localMessage, args); + if (!result) + result = m_deployment->receiveMessage(message, localMessage, args); + if (!result) + result = m_techController->receiveMessage(message, localMessage, args); + if (!result) + result = m_questManager->receiveMessage(message, localMessage, args); + for (auto& p : m_genericScriptContexts) { + if (result) + break; + result = p.second->handleMessage(message, localMessage, args); + } + return result; + } + + return {}; +} + +void Player::update(uint64_t) { + if (isMaster()) { + if (m_emoteCooldownTimer) { + m_emoteCooldownTimer -= WorldTimestep; + if (m_emoteCooldownTimer <= 0) { + m_emoteCooldownTimer = 0; + m_emoteState = HumanoidEmote::Idle; + } + } + + if (m_chatMessageUpdated) { + auto state = Root::singleton().emoteProcessor()->detectEmotes(m_chatMessage); + if (state != HumanoidEmote::Idle) + addEmote(state); + m_chatMessageUpdated = false; + } + + m_blinkCooldownTimer -= WorldTimestep; + if (m_blinkCooldownTimer <= 0) { + m_blinkCooldownTimer = Random::randf(m_blinkInterval[0], m_blinkInterval[1]); + auto loungeAnchor = as<LoungeAnchor>(m_movementController->entityAnchor()); + if (m_emoteState == HumanoidEmote::Idle && (!loungeAnchor || !loungeAnchor->emote)) + addEmote(HumanoidEmote::Blink); + } + + m_lastDamagedOtherTimer += WorldTimestep; + + if (m_movementController->zeroG()) + m_movementController->controlParameters(m_zeroGMovementParameters); + + if (isTeleporting()) { + m_teleportTimer -= WorldTimestep; + if (m_teleportTimer <= 0 && m_state == State::TeleportIn) { + m_state = State::Idle; + m_effectsAnimator->burstParticleEmitter(m_teleportAnimationType + "Burst"); + } + } + + if (!isTeleporting()) { + processControls(); + + m_questManager->update(); + m_companions->update(); + m_deployment->update(); + + bool edgeTriggeredUse = take(m_edgeTriggeredUse); + + m_inventory->cleanup(); + refreshEquipment(); + + if (inConflictingLoungeAnchor()) + m_movementController->resetAnchorState(); + + if (m_state == State::Lounge) { + if (auto loungeAnchor = as<LoungeAnchor>(m_movementController->entityAnchor())) { + m_statusController->setPersistentEffects("lounging", loungeAnchor->statusEffects); + addEffectEmitters(loungeAnchor->effectEmitters); + if (loungeAnchor->emote) + requestEmote(*loungeAnchor->emote); + + auto itemDatabase = Root::singleton().itemDatabase(); + if (auto headOverride = loungeAnchor->armorCosmeticOverrides.maybe("head")) { + auto overrideItem = itemDatabase->item(ItemDescriptor(*headOverride)); + if (m_inventory->itemAllowedAsEquipment(overrideItem, EquipmentSlot::HeadCosmetic)) + m_armor->setHeadCosmeticItem(as<HeadArmor>(overrideItem)); + } + if (auto chestOverride = loungeAnchor->armorCosmeticOverrides.maybe("chest")) { + auto overrideItem = itemDatabase->item(ItemDescriptor(*chestOverride)); + if (m_inventory->itemAllowedAsEquipment(overrideItem, EquipmentSlot::ChestCosmetic)) + m_armor->setChestCosmeticItem(as<ChestArmor>(overrideItem)); + } + if (auto legsOverride = loungeAnchor->armorCosmeticOverrides.maybe("legs")) { + auto overrideItem = itemDatabase->item(ItemDescriptor(*legsOverride)); + if (m_inventory->itemAllowedAsEquipment(overrideItem, EquipmentSlot::LegsCosmetic)) + m_armor->setLegsCosmeticItem(as<LegsArmor>(overrideItem)); + } + if (auto backOverride = loungeAnchor->armorCosmeticOverrides.maybe("back")) { + auto overrideItem = itemDatabase->item(ItemDescriptor(*backOverride)); + if (m_inventory->itemAllowedAsEquipment(overrideItem, EquipmentSlot::BackCosmetic)) + m_armor->setBackCosmeticItem(as<BackArmor>(overrideItem)); + } + } else { + m_state = State::Idle; + m_movementController->resetAnchorState(); + } + } else { + m_movementController->resetAnchorState(); + m_statusController->setPersistentEffects("lounging", {}); + } + + if (!forceNude()) + m_armor->effects(*m_effectEmitter); + + m_tools->effects(*m_effectEmitter); + + m_movementController->tickMaster(); + + m_techController->tickMaster(); + + for (auto& p : m_genericScriptContexts) + p.second->update(WorldTimestep * p.second->updateDelta()); + + if (edgeTriggeredUse) { + if (canUseTool()) { + if (auto ie = bestInteractionEntity(true)) + interactWithEntity(ie); + } else if (loungingIn()) { + m_movementController->resetAnchorState(); + } + } + + m_statusController->setPersistentEffects("armor", m_armor->statusEffects()); + m_statusController->setPersistentEffects("tools", m_tools->statusEffects()); + + if (!m_techController->techOverridden()) + m_techController->setLoadedTech(m_techs->equippedTechs().values()); + + if (!isDead()) + m_statusController->tickMaster(); + + if (!modeConfig().hunger) + m_statusController->resetResource("food"); + + if (!m_statusController->resourcePositive("food")) + m_statusController->setPersistentEffects("hunger", m_foodEmptyStatusEffects); + else if (m_statusController->resourcePercentage("food").value() <= m_foodLowThreshold) + m_statusController->setPersistentEffects("hunger", m_foodLowStatusEffects); + else + m_statusController->setPersistentEffects("hunger", {}); + + for (auto& pair : m_delayedRadioMessages) { + if (pair.first.tick()) + queueRadioMessage(pair.second); + } + m_delayedRadioMessages.filter([](pair<GameTimer, RadioMessage>& pair) { return !pair.first.ready(); }); + } + + if (m_isAdmin) { + m_statusController->resetResource("health"); + m_statusController->resetResource("energy"); + m_statusController->resetResource("food"); + m_statusController->resetResource("breath"); + } + + m_log->addPlayTime(WorldTimestep); + + if (m_ageItemsTimer.wrapTick(WorldTimestep)) { + auto itemDatabase = Root::singleton().itemDatabase(); + m_inventory->forEveryItem([&](InventorySlot const&, ItemPtr& item) { + itemDatabase->ageItem(item, m_ageItemsTimer.time); + }); + } + + for (auto tool : {m_tools->primaryHandItem(), m_tools->altHandItem()}) { + if (auto inspectionTool = as<InspectionTool>(tool)) { + for (auto ir : inspectionTool->pullInspectionResults()) { + if (ir.objectName) { + m_questManager->receiveMessage("objectScanned", true, {*ir.objectName, *ir.entityId}); + m_log->addScannedObject(*ir.objectName); + } + + addChatMessage(ir.message); + } + } + } + + m_interestingObjects = m_questManager->interestingObjects(); + + } else { + m_netGroup.tickNetInterpolation(WorldTimestep); + m_movementController->tickSlave(); + m_techController->tickSlave(); + m_statusController->tickSlave(); + } + + m_humanoid->setMovingBackwards(false); + m_humanoid->setRotation(m_movementController->rotation()); + + auto loungeAnchor = as<LoungeAnchor>(m_movementController->entityAnchor()); + if (loungeAnchor && loungeAnchor->dance) + m_humanoid->setDance(*loungeAnchor->dance); + else + m_humanoid->setDance({}); + + m_armor->setupHumanoidClothingDrawables(*m_humanoid, forceNude()); + + m_tools->suppressItems(!canUseTool()); + m_tools->tick(m_shifting, m_pendingMoves); + + if (auto overrideFacingDirection = m_tools->setupHumanoidHandItems(*m_humanoid, position(), aimPosition())) + m_movementController->controlFace(*overrideFacingDirection); + + m_effectsAnimator->resetTransformationGroup("flip"); + if (m_movementController->facingDirection() == Direction::Left) + m_effectsAnimator->scaleTransformationGroup("flip", Vec2F(-1, 1)); + + if (world()->isClient()) { + m_effectsAnimator->update(WorldTimestep, &m_effectsAnimatorDynamicTarget); + m_effectsAnimatorDynamicTarget.updatePosition(position() + m_techController->parentOffset()); + } else { + m_effectsAnimator->update(WorldTimestep, nullptr); + } + + if (!isTeleporting()) + processStateChanges(); + + m_damageSources = m_tools->damageSources(); + for (auto& damageSource : m_damageSources) { + damageSource.sourceEntityId = entityId(); + damageSource.team = getTeam(); + } + + m_songbook->update(*entityMode(), world()); + + m_effectEmitter->setSourcePosition("normal", position()); + m_effectEmitter->setSourcePosition("mouth", mouthOffset() + position()); + m_effectEmitter->setSourcePosition("feet", feetOffset() + position()); + 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->setSourcePosition("primary", handPosition(ToolHand::Primary) + position()); + m_effectEmitter->setSourcePosition("alt", handPosition(ToolHand::Alt) + position()); + + m_effectEmitter->setDirection(facingDirection()); + + m_effectEmitter->tick(*entityMode()); + + m_humanoid->setFacingDirection(m_movementController->facingDirection()); + m_humanoid->setMovingBackwards(m_movementController->facingDirection() != m_movementController->movingDirection()); + + m_pendingMoves.clear(); + + SpatialLogger::logPoly("world", m_movementController->collisionBody(), isMaster() ? Color::Orange.toRgba() : Color::Yellow.toRgba()); +} + +float Player::timeSinceLastGaveDamage() const { + return m_lastDamagedOtherTimer; +} + +EntityId Player::lastDamagedTarget() const { + return m_lastDamagedTarget; +} + +void Player::render(RenderCallback* renderCallback) { + if (invisible()) { + m_techController->pullNewAudios(); + m_techController->pullNewParticles(); + m_statusController->pullNewAudios(); + m_statusController->pullNewParticles(); + return; + } + + Vec2I footstepSensor = Vec2I((m_config->footstepSensor + m_movementController->position()).floor()); + String footstepSound = getFootstepSound(footstepSensor); + + if (!footstepSound.empty() && !m_techController->parentState() && !m_techController->parentHidden()) { + auto footstepAudio = Root::singleton().assets()->audio(footstepSound); + if (m_landingNoisePending) { + auto landingNoise = make_shared<AudioInstance>(*footstepAudio); + landingNoise->setPosition(position() + feetOffset()); + landingNoise->setVolume(m_landingVolume); + renderCallback->addAudio(move(landingNoise)); + } + + if (m_state == State::Walk || m_state == State::Run) { + m_footstepTimer += WorldTimestep; + if (m_footstepTimer > m_config->footstepTiming) { + auto stepNoise = make_shared<AudioInstance>(*footstepAudio); + stepNoise->setPosition(position() + feetOffset()); + stepNoise->setVolume(1 - Random::randf(0, m_footstepVolumeVariance)); + renderCallback->addAudio(move(stepNoise)); + m_footstepTimer = 0.0; + } + } + } else { + m_footstepTimer = m_config->footstepTiming; + } + m_landingNoisePending = false; + + renderCallback->addAudios(m_effectsAnimatorDynamicTarget.pullNewAudios()); + renderCallback->addParticles(m_effectsAnimatorDynamicTarget.pullNewParticles()); + + renderCallback->addAudios(m_techController->pullNewAudios()); + renderCallback->addAudios(m_statusController->pullNewAudios()); + + for (auto const& p : take(m_callbackSounds)) { + auto audio = make_shared<AudioInstance>(*Root::singleton().assets()->audio(p.first)); + audio->setVolume(p.second); + audio->setPosition(position()); + renderCallback->addAudio(move(audio)); + } + + auto loungeAnchor = as<LoungeAnchor>(m_movementController->entityAnchor()); + EntityRenderLayer renderLayer = loungeAnchor ? loungeAnchor->loungeRenderLayer : RenderLayerPlayer; + + renderCallback->addDrawables(drawables(), renderLayer); + if (!isTeleporting()) + renderCallback->addOverheadBars(bars(), position()); + renderCallback->addParticles(particles()); + renderCallback->addLightSources(lightSources()); + + m_tools->render(renderCallback, inToolRange(), m_shifting, renderLayer); + + m_effectEmitter->render(renderCallback); + m_songbook->render(renderCallback); + + if (isMaster()) + m_deployment->render(renderCallback, position()); +} + +Json Player::getGenericProperty(String const& name, Json const& defaultValue) const { + return m_genericProperties.value(name, defaultValue); +} + +void Player::setGenericProperty(String const& name, Json const& value) { + m_genericProperties.set(name, value); +} + +PlayerInventoryPtr Player::inventory() const { + return m_inventory; +} + +size_t Player::itemsCanHold(ItemPtr const& items) const { + return m_inventory->itemsCanFit(items); +} + +ItemPtr Player::pickupItems(ItemPtr const& items) { + if (isDead() || !items || m_inventory->itemsCanFit(items) == 0) + return items; + + triggerPickupEvents(items); + + if (items->pickupSound().size()) { + m_effectsAnimator->setSoundPool("pickup", {items->pickupSound()}); + m_effectsAnimator->playSound("pickup"); + } + auto itemDb = Root::singleton().itemDatabase(); + queueItemPickupMessage(itemDb->item(items->descriptor())); + return m_inventory->addItems(items); +} + +void Player::giveItem(ItemPtr const& item) { + if (auto spill = pickupItems(item)) + world()->addEntity(ItemDrop::createRandomizedDrop(spill->descriptor(), position())); +} + +void Player::triggerPickupEvents(ItemPtr const& item) { + if (item) { + for (auto b : item->learnBlueprintsOnPickup()) + addBlueprint(b); + + for (auto pair : item->collectablesOnPickup()) + addCollectable(pair.first, pair.second); + + for (auto m : item->instanceValue("radioMessagesOnPickup", JsonArray()).iterateArray()) { + if (m.isType(Json::Type::Array)) { + if (m.size() >= 2 && m.get(1).canConvert(Json::Type::Float)) + queueRadioMessage(m.get(0), m.get(1).toFloat()); + } else { + queueRadioMessage(m); + } + } + + if (auto cinematic = item->instanceValue("cinematicOnPickup", Json())) + setPendingCinematic(cinematic, true); + + for (auto const& quest : item->pickupQuestTemplates()) { + if (m_questManager->canStart(quest)) + m_questManager->offer(make_shared<Quest>(quest, 0, this)); + } + + if (auto consume = item->instanceValue("consumeOnPickup", Json())) { + if (consume.toBool()) + item->consume(item->count()); + } + + statistics()->recordEvent("item", JsonObject{ + {"itemName", item->name()}, + {"count", item->count()}, + {"category", item->instanceValue("eventCategory", item->category())} + }); + } +} + +bool Player::hasItem(ItemDescriptor const& descriptor, bool exactMatch) const { + return m_inventory->hasItem(descriptor, exactMatch); +} + +size_t Player::hasCountOfItem(ItemDescriptor const& descriptor, bool exactMatch) const { + return m_inventory->hasCountOfItem(descriptor, exactMatch); +} + +ItemDescriptor Player::takeItem(ItemDescriptor const& descriptor, bool consumePartial, bool exactMatch) { + return m_inventory->takeItems(descriptor, consumePartial, exactMatch); +} + +void Player::giveItem(ItemDescriptor const& descriptor) { + giveItem(Root::singleton().itemDatabase()->item(descriptor)); +} + +void Player::clearSwap() { + // If we cannot put the swap slot back into the bag, then just drop it in the + // world. + if (!m_inventory->clearSwap()) { + if (auto world = worldPtr()) + world->addEntity(ItemDrop::createRandomizedDrop(m_inventory->takeSlot(SwapSlot()), position())); + } + + // Interrupt all firing in case the item being dropped was in use. + endPrimaryFire(); + endAltFire(); + endTrigger(); +} + +void Player::refreshEquipment() { + if (isSlave()) + return; + + m_armor->setHeadItem(m_inventory->headArmor()); + m_armor->setHeadCosmeticItem(m_inventory->headCosmetic()); + m_armor->setChestItem(m_inventory->chestArmor()); + m_armor->setChestCosmeticItem(m_inventory->chestCosmetic()); + m_armor->setLegsItem(m_inventory->legsArmor()); + m_armor->setLegsCosmeticItem(m_inventory->legsCosmetic()); + m_armor->setBackItem(m_inventory->backArmor()); + m_armor->setBackCosmeticItem(m_inventory->backCosmetic()); + + m_tools->setItems(m_inventory->primaryHeldItem(), m_inventory->secondaryHeldItem()); +} + +PlayerBlueprintsPtr Player::blueprints() const { + return m_blueprints; +} + +bool Player::addBlueprint(ItemDescriptor const& descriptor, bool showFailure) { + if (descriptor.isNull()) + return false; + + auto itemDb = Root::singleton().itemDatabase(); + auto item = itemDb->item(descriptor); + auto assets = Root::singleton().assets(); + if (!m_blueprints->isKnown(descriptor)) { + m_blueprints->add(descriptor); + queueUIMessage(assets->json("/player.config:blueprintUnlock").toString().replace("<ItemName>", item->friendlyName())); + return true; + } else if (showFailure) { + queueUIMessage(assets->json("/player.config:blueprintAlreadyKnown").toString().replace("<ItemName>", item->friendlyName())); + } + + return false; +} + +bool Player::blueprintKnown(ItemDescriptor const& descriptor) const { + if (descriptor.isNull()) + return false; + + return m_blueprints->isKnown(descriptor); +} + +bool Player::addCollectable(String const& collectionName, String const& collectableName) { + if (m_log->addCollectable(collectionName, collectableName)) { + auto collectionDatabase = Root::singleton().collectionDatabase(); + + auto collection = collectionDatabase->collection(collectionName); + auto collectable = collectionDatabase->collectable(collectionName, collectableName); + queueUIMessage(Root::singleton().assets()->json("/player.config:collectableUnlock").toString().replace("<collectable>", collectable.title).replace("<collection>", collection.title)); + return true; + } else { + return false; + } +} + +PlayerUniverseMapPtr Player::universeMap() const { + return m_universeMap; +} + +PlayerCodexesPtr Player::codexes() const { + return m_codexes; +} + +PlayerTechPtr Player::techs() const { + return m_techs; +} + +void Player::overrideTech(Maybe<StringList> const& techModules) { + if (techModules) + m_techController->setOverrideTech(*techModules); + else + m_techController->clearOverrideTech(); +} + +bool Player::techOverridden() const { + return m_techController->techOverridden(); +} + +PlayerCompanionsPtr Player::companions() const { + return m_companions; +} + +PlayerLogPtr Player::log() const { + return m_log; +} + +InteractiveEntityPtr Player::bestInteractionEntity(bool includeNearby) { + if (!inWorld()) + return {}; + + InteractiveEntityPtr interactiveEntity; + if (auto entity = world()->getInteractiveInRange(m_aimPosition, position(), m_interactRadius)) { + interactiveEntity = entity; + } else if (includeNearby) { + Vec2F interactBias = m_walkIntoInteractBias; + if (facingDirection() == Direction::Left) + interactBias[0] *= -1; + Vec2F pos = position() + interactBias; + + if (auto entity = world()->getInteractiveInRange(pos, position(), m_interactRadius)) + interactiveEntity = entity; + } + + if (interactiveEntity && world()->canReachEntity(position(), interactRadius(), interactiveEntity->entityId())) + return interactiveEntity; + return {}; +} + +void Player::interactWithEntity(InteractiveEntityPtr entity) { + bool questIntercepted = false; + for (auto quest : m_questManager->listActiveQuests()) { + if (quest->interactWithEntity(entity->entityId())) + questIntercepted = true; + } + if (questIntercepted) + return; + + bool anyTurnedIn = false; + + for (auto questId : entity->turnInQuests()) { + if (m_questManager->canTurnIn(questId)) { + auto const& quest = m_questManager->getQuest(questId); + quest->setEntityParameter("questReceiver", entity); + m_questManager->getQuest(questId)->complete(); + anyTurnedIn = true; + } + } + + if (anyTurnedIn) + return; + + for (auto questArc : entity->offeredQuests()) { + if (m_questManager->canStart(questArc)) { + auto quest = make_shared<Quest>(questArc, 0, this); + quest->setWorldId(clientContext()->playerWorldId()); + quest->setServerUuid(clientContext()->serverUuid()); + quest->setEntityParameter("questGiver", entity); + m_questManager->offer(quest); + return; + } + } + + m_pendingInteractActions.append(world()->interact(InteractRequest{ + entityId(), position(), entity->entityId(), aimPosition()})); +} + +void Player::aim(Vec2F const& position) { + m_techController->setAimPosition(position); + m_aimPosition = position; +} + +Vec2F Player::aimPosition() const { + return m_aimPosition; +} + +Vec2F Player::armPosition(ToolHand hand, Direction facingDirection, float armAngle, Vec2F offset) const { + return m_tools->armPosition(*m_humanoid, hand, facingDirection, armAngle, offset); +} + +Vec2F Player::handOffset(ToolHand hand, Direction facingDirection) const { + return m_tools->handOffset(*m_humanoid, hand, facingDirection); +} + +Vec2F Player::handPosition(ToolHand hand, Vec2F const& handOffset) const { + return m_tools->handPosition(hand, *m_humanoid, handOffset); +} + +ItemPtr Player::handItem(ToolHand hand) const { + if (hand == ToolHand::Primary) + return m_tools->primaryHandItem(); + else + return m_tools->altHandItem(); +} + +Vec2F Player::armAdjustment() const { + return m_humanoid->armAdjustment(); +} + +void Player::setCameraFocusEntity(Maybe<EntityId> const& cameraFocusEntity) { + m_cameraFocusEntity = cameraFocusEntity; +} + +void Player::playEmote(HumanoidEmote emote) { + addEmote(emote); +} + +bool Player::canUseTool() const { + return !isDead() && !isTeleporting() && !m_techController->toolUsageSuppressed() && m_state != State::Lounge; +} + +void Player::beginPrimaryFire() { + m_techController->beginPrimaryFire(); + m_tools->beginPrimaryFire(); +} + +void Player::beginAltFire() { + m_techController->beginAltFire(); + m_tools->beginAltFire(); +} + +void Player::endPrimaryFire() { + m_techController->endPrimaryFire(); + m_tools->endPrimaryFire(); +} + +void Player::endAltFire() { + m_techController->endAltFire(); + m_tools->endAltFire(); +} + +void Player::beginTrigger() { + if (!m_useDown) + m_edgeTriggeredUse = true; + m_useDown = true; +} + +void Player::endTrigger() { + m_useDown = false; +} + +float Player::toolRadius() const { + auto radius = m_tools->toolRadius(); + if (radius) + return *radius; + return interactRadius(); +} + +float Player::interactRadius() const { + return m_interactRadius; +} + +List<InteractAction> Player::pullInteractActions() { + List<InteractAction> results; + eraseWhere(m_pendingInteractActions, [&results](auto& promise) { + if (auto res = promise.result()) + results.append(res.take()); + return promise.finished(); + }); + return results; +} + +uint64_t Player::currency(String const& currencyType) const { + return m_inventory->currency(currencyType); +} + +float Player::health() const { + return m_statusController->resource("health"); +} + +float Player::maxHealth() const { + return *m_statusController->resourceMax("health"); +} + +DamageBarType Player::damageBar() const { + return DamageBarType::Default; +} + +float Player::healthPercentage() const { + return *m_statusController->resourcePercentage("health"); +} + +float Player::energy() const { + return m_statusController->resource("energy"); +} + +float Player::maxEnergy() const { + return *m_statusController->resourceMax("energy"); +} + +float Player::energyPercentage() const { + return *m_statusController->resourcePercentage("energy"); +} + +float Player::energyRegenBlockPercent() const { + return *m_statusController->resourcePercentage("energyRegenBlock"); +} + +bool Player::fullEnergy() const { + return energy() >= maxEnergy(); +} + +bool Player::energyLocked() const { + return m_statusController->resourceLocked("energy"); +} + +bool Player::consumeEnergy(float energy) { + if (m_isAdmin) + return true; + return m_statusController->overConsumeResource("energy", energy); +} + +float Player::foodPercentage() const { + return *m_statusController->resourcePercentage("food"); +} + +float Player::breath() const { + return m_statusController->resource("breath"); +} + +float Player::maxBreath() const { + return *m_statusController->resourceMax("breath"); +} + +float Player::protection() const { + return m_statusController->stat("protection"); +} + +bool Player::forceNude() const { + return m_statusController->statPositive("nude"); +} + +String Player::description() const { + return m_description; +} + +Direction Player::walkingDirection() const { + return m_movementController->movingDirection(); +} + +Direction Player::facingDirection() const { + return m_movementController->facingDirection(); +} + +pair<ByteArray, uint64_t> Player::writeNetState(uint64_t fromVersion) { + return m_netGroup.writeNetState(fromVersion); +} + +void Player::readNetState(ByteArray data, float interpolationTime) { + m_netGroup.readNetState(move(data), interpolationTime); +} + +void Player::enableInterpolation(float) { + m_netGroup.enableNetInterpolation(); +} + +void Player::disableInterpolation() { + m_netGroup.disableNetInterpolation(); +} + +void Player::processControls() { + bool run = !m_shifting && !m_statusController->statPositive("encumberance"); + if (auto fireableMain = as<FireableItem>(m_tools->primaryHandItem())) { + if (fireableMain->inUse() && fireableMain->walkWhileFiring()) + run = false; + } + + if (auto fireableAlt = as<FireableItem>(m_tools->altHandItem())) { + if (fireableAlt->inUse() && fireableAlt->walkWhileFiring()) + run = false; + } + + bool move = true; + + if (auto fireableMain = as<FireableItem>(m_tools->primaryHandItem())) { + if (fireableMain->inUse() && fireableMain->stopWhileFiring()) + move = false; + } + + if (auto fireableAlt = as<FireableItem>(m_tools->altHandItem())) { + if (fireableAlt->inUse() && fireableAlt->stopWhileFiring()) + move = false; + } + + auto loungeAnchor = as<LoungeAnchor>(m_movementController->entityAnchor()); + if (loungeAnchor && loungeAnchor->controllable) { + auto anchorState = m_movementController->anchorState(); + if (auto loungeableEntity = world()->get<LoungeableEntity>(anchorState->entityId)) { + for (auto move : m_pendingMoves) { + if (move == MoveControlType::Up) + loungeableEntity->loungeControl(anchorState->positionIndex, LoungeControl::Up); + else if (move == MoveControlType::Down) + loungeableEntity->loungeControl(anchorState->positionIndex, LoungeControl::Down); + else if (move == MoveControlType::Left) + loungeableEntity->loungeControl(anchorState->positionIndex, LoungeControl::Left); + else if (move == MoveControlType::Right) + loungeableEntity->loungeControl(anchorState->positionIndex, LoungeControl::Right); + else if (move == MoveControlType::Jump) + loungeableEntity->loungeControl(anchorState->positionIndex, LoungeControl::Jump); + } + if (m_tools->firingPrimary()) + loungeableEntity->loungeControl(anchorState->positionIndex, LoungeControl::PrimaryFire); + if (m_tools->firingAlt()) + loungeableEntity->loungeControl(anchorState->positionIndex, LoungeControl::AltFire); + + loungeableEntity->loungeAim(anchorState->positionIndex, m_aimPosition); + } + move = false; + } + + m_techController->setShouldRun(run); + + if (move) { + for (auto move : m_pendingMoves) { + switch (move) { + case MoveControlType::Right: + m_techController->moveRight(); + break; + case MoveControlType::Left: + m_techController->moveLeft(); + break; + case MoveControlType::Up: + m_techController->moveUp(); + break; + case MoveControlType::Down: + m_techController->moveDown(); + break; + case MoveControlType::Jump: + m_techController->jump(); + break; + } + } + } + + if (m_state == State::Lounge && !m_pendingMoves.empty() && move) + stopLounging(); +} + +void Player::processStateChanges() { + if (isMaster()) { + // Set the current player state based on what movement controller tells us + // we're doing and do some state transition logic + State oldState = m_state; + + if (m_movementController->zeroG()) { + if (m_movementController->flying()) + m_state = State::Swim; + else if (m_state != State::Lounge) + m_state = State::SwimIdle; + } else if (m_movementController->groundMovement()) { + if (m_movementController->running()) { + m_state = State::Run; + } else if (m_movementController->walking()) { + m_state = State::Walk; + } else if (m_movementController->crouching()) { + m_state = State::Crouch; + } else { + if (m_state != State::Lounge) + m_state = State::Idle; + } + } else if (m_movementController->liquidMovement()) { + if (m_movementController->jumping()) { + m_state = State::Swim; + } else { + if (m_state != State::Lounge) + m_state = State::SwimIdle; + } + } else { + if (m_movementController->jumping()) { + m_state = State::Jump; + } else { + if (m_movementController->falling()) { + m_state = State::Fall; + } + if (m_movementController->velocity()[1] > 0) { + if (m_state != State::Lounge) + m_state = State::Jump; + } + } + } + + if (m_state == State::Jump && (oldState == State::Idle || oldState == State::Run || oldState == State::Walk || oldState == State::Crouch)) + m_effectsAnimator->burstParticleEmitter("jump"); + + if (!m_movementController->isNullColliding()) { + if (oldState == State::Fall && oldState != m_state && m_state != State::Swim && m_state != State::SwimIdle + && m_state != State::Jump) { + m_effectsAnimator->burstParticleEmitter("landing"); + m_landedNetState.trigger(); + m_landingNoisePending = true; + } + } + } + + m_humanoid->animate(WorldTimestep); + + if (auto techState = m_techController->parentState()) { + if (techState == TechController::ParentState::Stand) { + m_humanoid->setState(Humanoid::Idle); + } else if (techState == TechController::ParentState::Fly) { + m_humanoid->setState(Humanoid::Jump); + } else if (techState == TechController::ParentState::Fall) { + m_humanoid->setState(Humanoid::Fall); + } else if (techState == TechController::ParentState::Sit) { + m_humanoid->setState(Humanoid::Sit); + } else if (techState == TechController::ParentState::Lay) { + m_humanoid->setState(Humanoid::Lay); + } else if (techState == TechController::ParentState::Duck) { + m_humanoid->setState(Humanoid::Duck); + } else if (techState == TechController::ParentState::Walk) { + m_humanoid->setState(Humanoid::Walk); + } else if (techState == TechController::ParentState::Run) { + m_humanoid->setState(Humanoid::Run); + } else if (techState == TechController::ParentState::Swim) { + m_humanoid->setState(Humanoid::Swim); + } + } else { + auto loungeAnchor = as<LoungeAnchor>(m_movementController->entityAnchor()); + if (m_state == State::Idle) { + m_humanoid->setState(Humanoid::Idle); + } else if (m_state == State::Walk) { + m_humanoid->setState(Humanoid::Walk); + } else if (m_state == State::Run) { + m_humanoid->setState(Humanoid::Run); + } else if (m_state == State::Jump) { + m_humanoid->setState(Humanoid::Jump); + } else if (m_state == State::Fall) { + m_humanoid->setState(Humanoid::Fall); + } else if (m_state == State::Swim) { + m_humanoid->setState(Humanoid::Swim); + } else if (m_state == State::SwimIdle) { + m_humanoid->setState(Humanoid::SwimIdle); + } else if (m_state == State::Crouch) { + m_humanoid->setState(Humanoid::Duck); + } else if (m_state == State::Lounge && loungeAnchor && loungeAnchor->orientation == LoungeOrientation::Sit) { + m_humanoid->setState(Humanoid::Sit); + } else if (m_state == State::Lounge && loungeAnchor && loungeAnchor->orientation == LoungeOrientation::Lay) { + m_humanoid->setState(Humanoid::Lay); + } else if (m_state == State::Lounge && loungeAnchor && loungeAnchor->orientation == LoungeOrientation::Stand) { + m_humanoid->setState(Humanoid::Idle); + } + } + + m_humanoid->setEmoteState(m_emoteState); +} + +String Player::getFootstepSound(Vec2I const& sensor) const { + auto materialDatabase = Root::singleton().materialDatabase(); + + String fallback = materialDatabase->defaultFootstepSound(); + List<Vec2I> scanOrder{{0, 0}, {0, -1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}}; + for (auto subSensor : scanOrder) { + String footstepSound = materialDatabase->footstepSound(world()->material(sensor + subSensor, TileLayer::Foreground), + world()->mod(sensor + subSensor, TileLayer::Foreground)); + if (!footstepSound.empty()) { + if (footstepSound != fallback) { + return footstepSound; + } + } + } + return fallback; +} + +bool Player::inInteractionRange() const { + return inInteractionRange(centerOfTile(aimPosition())); +} + +bool Player::inInteractionRange(Vec2F aimPos) const { + return world()->geometry().diff(aimPos, position()).magnitude() < interactRadius(); +} + +bool Player::inToolRange() const { + return inToolRange(centerOfTile(aimPosition())); +} + +bool Player::inToolRange(Vec2F const& aimPos) const { + return world()->geometry().diff(aimPos, position()).magnitude() < toolRadius(); +} + +void Player::getNetStates(bool initial) { + m_state = (State)m_stateNetState.get(); + m_shifting = m_shiftingNetState.get(); + m_aimPosition[0] = m_xAimPositionNetState.get(); + m_aimPosition[1] = m_yAimPositionNetState.get(); + + if (m_identityNetState.pullUpdated()) { + m_identity = m_identityNetState.get(); + m_identityUpdated = true; + m_humanoid->setIdentity(m_identity); + } + + setTeam(m_teamNetState.get()); + + if (m_landedNetState.pullOccurred() && !initial) + m_landingNoisePending = true; + + if (m_newChatMessageNetState.pullOccurred() && !initial) { + m_chatMessage = m_chatMessageNetState.get(); + m_chatMessageUpdated = true; + m_pendingChatActions.append(SayChatAction{entityId(), m_chatMessage, m_movementController->position()}); + } + + m_emoteState = HumanoidEmoteNames.getLeft(m_emoteNetState.get()); +} + +void Player::setNetStates() { + m_stateNetState.set((unsigned)m_state); + m_shiftingNetState.set(m_shifting); + m_xAimPositionNetState.set(m_aimPosition[0]); + m_yAimPositionNetState.set(m_aimPosition[1]); + + if (m_identityUpdated) { + m_identityNetState.push(m_identity); + m_identityUpdated = false; + } + + m_teamNetState.set(getTeam()); + + if (m_chatMessageChanged) { + m_chatMessageChanged = false; + m_chatMessageNetState.push(m_chatMessage); + m_newChatMessageNetState.trigger(); + } + + m_emoteNetState.set(HumanoidEmoteNames.getRight(m_emoteState)); +} + +void Player::setAdmin(bool isAdmin) { + m_isAdmin = isAdmin; +} + +bool Player::isAdmin() const { + return m_isAdmin; +} + +void Player::setFavoriteColor(Vec4B color) { + m_identity.color = color; + m_identityUpdated = true; + m_humanoid->setIdentity(m_identity); +} + +Vec4B Player::favoriteColor() const { + return m_identity.color; +} + +bool Player::isTeleporting() const { + return (m_state == State::TeleportIn) || (m_state == State::TeleportOut); +} + +bool Player::isTeleportingOut() const { + return inWorld() && (m_state == State::TeleportOut) && m_teleportTimer >= 0.0f; +} + +bool Player::canDeploy() { + return m_deployment->canDeploy(); +} + +void Player::deployAbort(String const& animationType) { + m_teleportAnimationType = animationType; + m_deployment->setDeploying(false); +} + +bool Player::isDeploying() const { + return m_deployment->isDeploying(); +} + +bool Player::isDeployed() const { + return m_deployment->isDeployed(); +} + +void Player::setBusyState(PlayerBusyState busyState) { + m_effectsAnimator->setState("busy", PlayerBusyStateNames.getRight(busyState)); +} + +void Player::teleportOut(String const& animationType, bool deploy) { + m_state = State::TeleportOut; + m_teleportAnimationType = animationType; + m_effectsAnimator->setState("teleport", m_teleportAnimationType + "Out"); + m_deployment->setDeploying(deploy); + m_deployment->teleportOut(); + m_teleportTimer = deploy ? m_config->deployOutTime : m_config->teleportOutTime; +} + +void Player::teleportIn() { + m_state = State::TeleportIn; + m_effectsAnimator->setState("teleport", m_teleportAnimationType + "In"); + m_teleportTimer = m_deployment->isDeployed() ? m_config->deployInTime : m_config->teleportInTime; + + auto statusEffects = Root::singleton().assets()->json("/player.config:teleportInStatusEffects").toArray().transformed(jsonToEphemeralStatusEffect); + m_statusController->addEphemeralEffects(statusEffects); +} + +void Player::teleportAbort() { + m_state = State::TeleportIn; + m_effectsAnimator->setState("teleport", "abort"); + m_deployment->setDeploying(m_deployment->isDeployed()); + m_teleportTimer = m_config->teleportInTime; +} + +void Player::moveTo(Vec2F const& footPosition) { + m_movementController->setPosition(footPosition - feetOffset()); + m_movementController->setVelocity(Vec2F()); +} + +ItemPtr Player::primaryHandItem() const { + return m_tools->primaryHandItem(); +} + +ItemPtr Player::altHandItem() const { + return m_tools->altHandItem(); +} + +Uuid Player::uuid() const { + return Uuid(*uniqueId()); +} + +PlayerMode Player::modeType() const { + return m_modeType; +} + +void Player::setModeType(PlayerMode mode) { + m_modeType = mode; + + auto assets = Root::singleton().assets(); + m_modeConfig = PlayerModeConfig(assets->json("/playermodes.config").get(PlayerModeNames.getRight(mode))); +} + +PlayerModeConfig Player::modeConfig() const { + return m_modeConfig; +} + +ShipUpgrades Player::shipUpgrades() { + return m_shipUpgrades; +} + +void Player::setShipUpgrades(ShipUpgrades shipUpgrades) { + m_shipUpgrades = move(shipUpgrades); +} + +String Player::name() const { + return m_identity.name; +} + +void Player::setName(String const& name) { + m_identity.name = name; + m_identityUpdated = true; + m_humanoid->setIdentity(m_identity); +} + +Maybe<String> Player::statusText() const { + return {}; +} + +bool Player::displayNametag() const { + return true; +} + +Vec3B Player::nametagColor() const { + auto assets = Root::singleton().assets(); + return jsonToVec3B(assets->json("/player.config:nametagColor")); +} + +void Player::setBodyDirectives(String const& directives) { + m_identity.bodyDirectives = directives; + m_identityUpdated = true; + m_humanoid->setIdentity(m_identity); +} + +void Player::setHairType(String const& group, String const& type) { + m_identity.hairGroup = group; + m_identity.hairType = type; + m_identityUpdated = true; + m_humanoid->setIdentity(m_identity); +} + +void Player::setFacialHair(String const& group, String const& type, String const& directives) { + m_identity.facialHairGroup = group; + m_identity.facialHairType = type; + m_identity.facialHairDirectives = directives; + m_identityUpdated = true; + m_humanoid->setIdentity(m_identity); +} + +void Player::setFacialMask(String const& group, String const& type, String const& directives) { + m_identity.facialMaskGroup = group; + m_identity.facialMaskType = type; + m_identity.facialMaskDirectives = directives; + m_identityUpdated = true; + m_humanoid->setIdentity(m_identity); +} + +void Player::setHairDirectives(String const& directives) { + m_identity.hairDirectives = directives; + m_identityUpdated = true; + m_humanoid->setIdentity(m_identity); +} + +void Player::setEmoteDirectives(String const& directives) { + m_identity.emoteDirectives = directives; + m_identityUpdated = true; + m_humanoid->setIdentity(m_identity); +} + +void Player::setSpecies(String const& species) { + m_identity.species = species; + m_identityUpdated = true; + m_humanoid->setIdentity(m_identity); +} + +Gender Player::gender() const { + return m_identity.gender; +} + +void Player::setGender(Gender const& gender) { + m_identity.gender = gender; + m_identityUpdated = true; + m_humanoid->setIdentity(m_identity); +} + +String Player::species() const { + return m_identity.species; +} + +void Player::setPersonality(Personality const& personality) { + m_identity.personality = personality; + m_identityUpdated = true; + m_humanoid->setIdentity(m_identity); +} + +List<String> Player::pullQueuedMessages() { + return take(m_queuedMessages); +} + +List<ItemPtr> Player::pullQueuedItemDrops() { + return take(m_queuedItemPickups); +} + +void Player::queueUIMessage(String const& message) { + if (!isSlave()) + m_queuedMessages.append(message); +} + +void Player::queueItemPickupMessage(ItemPtr const& item) { + if (!isSlave()) + m_queuedItemPickups.append(item); +} + +void Player::addChatMessage(String const& message) { + starAssert(!isSlave()); + m_chatMessage = message; + m_chatMessageUpdated = true; + m_chatMessageChanged = true; + m_pendingChatActions.append(SayChatAction{entityId(), message, mouthPosition()}); +} + +void Player::addEmote(HumanoidEmote const& emote) { + starAssert(!isSlave()); + m_emoteState = emote; + m_emoteCooldownTimer = m_emoteCooldown; +} + +List<ChatAction> Player::pullPendingChatActions() { + return take(m_pendingChatActions); +} + +float Player::beamGunRadius() const { + return m_tools->beamGunRadius(); +} + +bool Player::instrumentPlaying() { + return m_songbook->instrumentPlaying(); +} + +void Player::instrumentEquipped(String const& instrumentKind) { + if (canUseTool()) + m_songbook->keepalive(instrumentKind, mouthPosition()); +} + +void Player::interact(InteractAction const& action) { + starAssert(!isSlave()); + m_pendingInteractActions.append(RpcPromise<InteractAction>::createFulfilled(action)); +} + +void Player::addEffectEmitters(StringSet const& emitters) { + starAssert(!isSlave()); + m_effectEmitter->addEffectSources("normal", emitters); +} + +void Player::requestEmote(String const& emote) { + auto state = HumanoidEmoteNames.getLeft(emote); + if (state != HumanoidEmote::Idle + && (m_emoteState == state || m_emoteState == HumanoidEmote::Idle || m_emoteState == HumanoidEmote::Blink)) + addEmote(state); +} + +ActorMovementController* Player::movementController() { + return m_movementController.get(); +} + +StatusController* Player::statusController() { + return m_statusController.get(); +} + +List<PhysicsForceRegion> Player::forceRegions() const { + return m_tools->forceRegions(); +} + +SongbookPtr Player::songbook() const { + return m_songbook; +} + +QuestManagerPtr Player::questManager() const { + return m_questManager; +} + +Json Player::diskStore() { + JsonObject genericScriptStorage; + for (auto& p : m_genericScriptContexts) + genericScriptStorage[p.first] = p.second->getScriptStorage(); + + return JsonObject{ + {"uuid", *uniqueId()}, + {"description", m_description}, + {"modeType", PlayerModeNames.getRight(m_modeType)}, + {"shipUpgrades", m_shipUpgrades.toJson()}, + {"blueprints", m_blueprints->toJson()}, + {"universeMap", m_universeMap->toJson()}, + {"codexes", m_codexes->toJson()}, + {"techs", m_techs->toJson()}, + {"identity", m_identity.toJson()}, + {"team", getTeam().toJson()}, + {"inventory", m_inventory->store()}, + {"movementController", m_movementController->storeState()}, + {"techController", m_techController->diskStore()}, + {"statusController", m_statusController->diskStore()}, + {"log", m_log->toJson()}, + {"aiState", m_aiState.toJson()}, + {"quests", m_questManager->diskStore()}, + {"companions", m_companions->diskStore()}, + {"deployment", m_deployment->diskStore()}, + {"genericProperties", m_genericProperties}, + {"genericScriptStorage", genericScriptStorage}, + }; +} + +ByteArray Player::netStore() { + DataStreamBuffer ds; + + ds.write(*uniqueId()); + ds.write(m_description); + ds.write(m_modeType); + ds.write(m_identity); + + return ds.data(); +} + +void Player::finalizeCreation() { + m_blueprints = make_shared<PlayerBlueprints>(); + m_techs = make_shared<PlayerTech>(); + + auto itemDatabase = Root::singleton().itemDatabase(); + for (auto const& descriptor : m_config->defaultItems) + m_inventory->addItems(itemDatabase->item(descriptor)); + + for (auto const& descriptor : Root::singleton().speciesDatabase()->species(m_identity.species)->defaultItems()) + m_inventory->addItems(itemDatabase->item(descriptor)); + + for (auto const& descriptor : m_config->defaultBlueprints) + m_blueprints->add(descriptor); + + for (auto const& descriptor : Root::singleton().speciesDatabase()->species(m_identity.species)->defaultBlueprints()) + m_blueprints->add(descriptor); + + refreshEquipment(); + + m_state = State::Idle; + m_emoteState = HumanoidEmote::Idle; + + m_statusController->setPersistentEffects("armor", m_armor->statusEffects()); + m_statusController->setPersistentEffects("tools", m_tools->statusEffects()); + m_statusController->resetAllResources(); + + m_effectEmitter->reset(); +} + +bool Player::invisible() const { + return m_statusController->statPositive("invisible"); +} + +void Player::animatePortrait() { + m_humanoid->animate(WorldTimestep); + if (m_emoteCooldownTimer) { + m_emoteCooldownTimer -= WorldTimestep; + if (m_emoteCooldownTimer <= 0) { + m_emoteCooldownTimer = 0; + m_emoteState = HumanoidEmote::Idle; + } + } + m_humanoid->setEmoteState(m_emoteState); +} + +bool Player::isOutside() { + if (!inWorld()) + return false; + return !world()->isUnderground(position()) + && !world()->tileIsOccupied(Vec2I::floor(mouthPosition()), TileLayer::Background); +} + +void Player::dropSelectedItems(function<bool(ItemPtr)> filter) { + if (!world()) + return; + + m_inventory->forEveryItem([&](InventorySlot const&, ItemPtr& item) { + if (item && (!filter || filter(item))) + world()->addEntity(ItemDrop::throwDrop(take(item), position(), Vec2F::withAngle(Random::randf(-Constants::pi, Constants::pi)), true)); + }); +} + +void Player::dropEverything() { + dropSelectedItems({}); +} + +bool Player::isPermaDead() const { + if (!isDead()) + return false; + return modeConfig().permadeath; +} + +bool Player::interruptRadioMessage() { + if (m_interruptRadioMessage) { + m_interruptRadioMessage = false; + return true; + } + return false; +} + +Maybe<RadioMessage> Player::pullPendingRadioMessage() { + if (m_pendingRadioMessages.count()) { + if (m_pendingRadioMessages.at(0).unique) + m_log->addRadioMessage(m_pendingRadioMessages.at(0).messageId); + return m_pendingRadioMessages.takeFirst(); + } + return {}; +} + +void Player::queueRadioMessage(Json const& messageConfig, float delay) { + RadioMessage message; + try { + message = Root::singleton().radioMessageDatabase()->createRadioMessage(messageConfig); + + if (message.type == RadioMessageType::Tutorial && !Root::singleton().configuration()->get("tutorialMessages").toBool()) + return; + + // non-absolute portrait image paths are assumed to be a frame name within the player's species-specific AI + if (!message.portraitImage.empty() && message.portraitImage[0] != '/') + message.portraitImage = Root::singleton().aiDatabase()->portraitImage(species(), message.portraitImage); + } catch (RadioMessageDatabaseException const& e) { + Logger::error("Couldn't queue radio message '%s': %s", messageConfig, e.what()); + return; + } + + if (m_log->radioMessages().contains(message.messageId)) { + return; + } else { + if (message.type == RadioMessageType::Mission) { + if (m_missionRadioMessages.contains(message.messageId)) + return; + else + m_missionRadioMessages.add(message.messageId); + } + + for (RadioMessage const& pendingMessage : m_pendingRadioMessages) { + if (pendingMessage.messageId == message.messageId) + return; + } + for (auto& delayedMessagePair : m_delayedRadioMessages) { + if (delayedMessagePair.second.messageId == message.messageId) { + if (delay == 0) + delayedMessagePair.first.setDone(); + return; + } + } + } + + if (delay > 0) { + m_delayedRadioMessages.append(pair<GameTimer, RadioMessage>{GameTimer(delay), message}); + } else { + queueRadioMessage(message); + } +} + +void Player::queueRadioMessage(RadioMessage message) { + if (message.important) { + m_interruptRadioMessage = true; + m_pendingRadioMessages.prepend(message); + } else { + m_pendingRadioMessages.append(message); + } +} + +Maybe<Json> Player::pullPendingCinematic() { + if (m_pendingCinematic) + if (auto cinematic = *m_pendingCinematic) + m_log->addCinematic(cinematic.toString()); + return take(m_pendingCinematic); +} + +void Player::setPendingCinematic(Json const& cinematic, bool unique) { + if (unique && cinematic.isType(Json::Type::String) && m_log->cinematics().contains(cinematic.toString())) + return; + m_pendingCinematic = cinematic; +} + +void Player::setInCinematic(bool inCinematic) { + if (inCinematic) + m_statusController->setPersistentEffects("cinematic", m_inCinematicStatusEffects); + else + m_statusController->setPersistentEffects("cinematic", {}); +} + +Maybe<pair<Maybe<StringList>, float>> Player::pullPendingAltMusic() { + if (m_pendingAltMusic) + return m_pendingAltMusic.take(); + return {}; +} + +Maybe<PlayerWarpRequest> Player::pullPendingWarp() { + if (m_pendingWarp) + return m_pendingWarp.take(); + return {}; +} + +void Player::setPendingWarp(String const& action, Maybe<String> const& animation, bool deploy) { + m_pendingWarp = PlayerWarpRequest{action, animation, deploy}; +} + +Maybe<pair<Json, RpcPromiseKeeper<Json>>> Player::pullPendingConfirmation() { + if (m_pendingConfirmations.count() > 0) + return m_pendingConfirmations.takeFirst(); + return {}; +} + +void Player::queueConfirmation(Json const& dialogConfig, RpcPromiseKeeper<Json> const& resultPromise) { + m_pendingConfirmations.append(make_pair(dialogConfig, resultPromise)); +} + +AiState const& Player::aiState() const { + return m_aiState; +} + +AiState& Player::aiState() { + return m_aiState; +} + +bool Player::inspecting() const { + return is<InspectionTool>(m_tools->primaryHandItem()) || is<InspectionTool>(m_tools->altHandItem()); +} + +EntityHighlightEffect Player::inspectionHighlight(InspectableEntityPtr const& inspectableEntity) const { + auto inspectionTool = as<InspectionTool>(m_tools->primaryHandItem()); + if (!inspectionTool) + inspectionTool = as<InspectionTool>(m_tools->altHandItem()); + + if (!inspectionTool) + return EntityHighlightEffect(); + + if (auto name = inspectableEntity->inspectionLogName()) { + auto ehe = EntityHighlightEffect(); + ehe.level = inspectionTool->inspectionHighlightLevel(inspectableEntity); + if (ehe.level > 0) { + if (m_interestingObjects.contains(*name)) + ehe.type = EntityHighlightEffectType::Interesting; + else if (m_log->scannedObjects().contains(*name)) + ehe.type = EntityHighlightEffectType::Inspected; + else + ehe.type = EntityHighlightEffectType::Inspectable; + } + return ehe; + } + + return EntityHighlightEffect(); +} + +Vec2F Player::cameraPosition() { + if (inWorld()) { + if (auto loungeAnchor = as<LoungeAnchor>(m_movementController->entityAnchor())) { + if (loungeAnchor->cameraFocus) { + if (auto anchoredEntity = world()->entity(m_movementController->anchorState()->entityId)) + return anchoredEntity->position(); + } + } + + if (m_cameraFocusEntity) { + if (auto focusedEntity = world()->entity(*m_cameraFocusEntity)) + return focusedEntity->position(); + else + m_cameraFocusEntity = {}; + } + } + return position(); +} + +} |