#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 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(this); m_tools = make_shared(); m_armor = make_shared(); m_companions = make_shared(config->companionsConfig); for (auto& p : config->genericScriptContexts) { auto scriptComponent = make_shared(); 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(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(movementParameters); m_zeroGMovementParameters = ActorMovementParameters(m_config->zeroGMovementParameters); m_techController = make_shared(); m_statusController = make_shared(m_config->statusControllerSettings); m_deployment = make_shared(m_config->deploymentConfig); m_inventory = make_shared(); m_blueprints = make_shared(); m_universeMap = make_shared(); m_codexes = make_shared(); m_techs = make_shared(); m_log = make_shared(); 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(assets->fetchJson(m_config->effectsAnimator)); m_effectEmitter = make_shared(); 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(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); 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(diskStore.get("blueprints")); m_universeMap = make_shared(diskStore.get("universeMap")); m_codexes = make_shared(diskStore.get("codexes")); m_techs = make_shared(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(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(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()); ds.read(m_description); ds.read(m_modeType); ds.read(m_identity); m_humanoid = make_shared(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 Player::drawables() const { List 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(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 Player::bars() const { return m_statusController->overheadBars(); } List Player::particles() { List 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 const& particles) { m_callbackParticles.appendAll(particles); } void Player::addSound(String const& sound, float volume) { m_callbackSounds.append({sound, volume}); } void Player::addEphemeralStatusEffects(List 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(m_tools->primaryHandItem()); } void Player::setWireConnector(WireConnector* wireConnector) const { if (auto wireTool = as(m_tools->primaryHandItem())) wireTool->setConnector(wireConnector); } List 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 Player::lightSources() const { List 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 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 Player::hitPoly() const { return m_movementController->collisionBody(); } List Player::applyDamage(DamageRequest const& request) { if (!inWorld() || isDead() || m_isAdmin) return {}; return m_statusController->applyDamageRequest(request); } List Player::selfDamageNotifications() { return m_statusController->pullSelfDamageNotifications(); } void Player::hitOther(EntityId targetEntityId, DamageRequest const& damageRequest) { if (!isMaster()) return; m_statusController->hitOther(targetEntityId, damageRequest); if (as(world()->entity(targetEntityId))) { m_lastDamagedOtherTimer = 0; m_lastDamagedTarget = targetEntityId; } } void Player::damagedOther(DamageNotification const& damage) { if (!isMaster()) return; m_statusController->damagedOther(damage); } List 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 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 dropList = modeConfig().deathDropItemTypes.right().transformed([](String typeName) { return ItemTypeNames.getLeft(typeName); }); Set dropSet = Set::from(dropList); auto itemDb = Root::singleton().itemDatabase(); dropSelectedItems([dropSet, itemDb](ItemPtr item) { return dropSet.contains(itemDb->itemType(item->name())); }); } } } m_songbook->stop(); } Maybe Player::loungingIn() const { if (is(m_movementController->entityAnchor())) return m_movementController->anchorState(); return {}; } bool Player::lounge(EntityId loungeableEntityId, size_t anchorIndex) { if (!canUseTool()) return false; auto loungeableEntity = world()->get(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(m_movementController->entityAnchor()); if (loungeAnchor && loungeAnchor->controllable) { auto anchorState = m_movementController->anchorState(); if (auto loungeableEntity = world()->get(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 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 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, float>(trackList, fadeTime); } else if (message == "stopAltMusic") { float fadeTime = 0; if (args.size() > 0) fadeTime = args.get(0).toFloat(); m_pendingAltMusic = pair, 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 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(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(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(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(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(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(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& 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(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(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(*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(*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(*Root::singleton().assets()->audio(p.first)); audio->setVolume(p.second); audio->setPosition(position()); renderCallback->addAudio(move(audio)); } auto loungeAnchor = as(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, 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("", item->friendlyName())); return true; } else if (showFailure) { queueUIMessage(assets->json("/player.config:blueprintAlreadyKnown").toString().replace("", 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.title).replace("", 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 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(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 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 Player::pullInteractActions() { List 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 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(m_tools->primaryHandItem())) { if (fireableMain->inUse() && fireableMain->walkWhileFiring()) run = false; } if (auto fireableAlt = as(m_tools->altHandItem())) { if (fireableAlt->inUse() && fireableAlt->walkWhileFiring()) run = false; } bool move = true; if (auto fireableMain = as(m_tools->primaryHandItem())) { if (fireableMain->inUse() && fireableMain->stopWhileFiring()) move = false; } if (auto fireableAlt = as(m_tools->altHandItem())) { if (fireableAlt->inUse() && fireableAlt->stopWhileFiring()) move = false; } auto loungeAnchor = as(m_movementController->entityAnchor()); if (loungeAnchor && loungeAnchor->controllable) { auto anchorState = m_movementController->anchorState(); if (auto loungeableEntity = world()->get(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(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 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 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 Player::pullQueuedMessages() { return take(m_queuedMessages); } List 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 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::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 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(); m_techs = make_shared(); 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 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 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(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 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, float>> Player::pullPendingAltMusic() { if (m_pendingAltMusic) return m_pendingAltMusic.take(); return {}; } Maybe Player::pullPendingWarp() { if (m_pendingWarp) return m_pendingWarp.take(); return {}; } void Player::setPendingWarp(String const& action, Maybe const& animation, bool deploy) { m_pendingWarp = PlayerWarpRequest{action, animation, deploy}; } Maybe>> Player::pullPendingConfirmation() { if (m_pendingConfirmations.count() > 0) return m_pendingConfirmations.takeFirst(); return {}; } void Player::queueConfirmation(Json const& dialogConfig, RpcPromiseKeeper 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(m_tools->primaryHandItem()) || is(m_tools->altHandItem()); } EntityHighlightEffect Player::inspectionHighlight(InspectableEntityPtr const& inspectableEntity) const { auto inspectionTool = as(m_tools->primaryHandItem()); if (!inspectionTool) inspectionTool = as(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(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(); } }