431a9c00a5
On Linux and macOS, using Clang to compile OpenStarbound produces about 400 MB worth of warnings during the build, making the compiler output unreadable and slowing the build down considerably. 99% of the warnings were unqualified uses of std::move and std::forward, which are now all properly qualified. Fixed a few other minor warnings about non-virtual destructors and some uses of std::move preventing copy elision on temporary objects. Most remaining warnings are now unused parameters.
2599 lines
82 KiB
C++
2599 lines
82 KiB
C++
#include "StarPlayer.hpp"
|
|
#include "StarEncode.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_client = nullptr;
|
|
|
|
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>();
|
|
|
|
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;
|
|
m_footstepPending = 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, 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)));
|
|
}
|
|
|
|
|
|
Player::Player(PlayerConfigPtr config, Json const& diskStore) : Player(config) {
|
|
diskLoad(diskStore);
|
|
}
|
|
|
|
void Player::diskLoad(Json const& diskStore) {
|
|
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"));
|
|
if (m_clientContext)
|
|
m_universeMap->setServerUuid(m_clientContext->serverUuid());
|
|
|
|
m_codexes = make_shared<PlayerCodexes>(diskStore.get("codexes"));
|
|
m_techs = make_shared<PlayerTech>(diskStore.get("techs"));
|
|
m_identity = HumanoidIdentity(diskStore.get("identity"));
|
|
m_identityUpdated = true;
|
|
|
|
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"));
|
|
|
|
auto speciesDef = Root::singleton().speciesDatabase()->species(m_identity.species);
|
|
|
|
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>(speciesDef->humanoidConfig());
|
|
m_humanoid->setIdentity(m_identity);
|
|
m_movementController->resetBaseParameters(ActorMovementParameters(jsonMerge(m_humanoid->defaultMovementParameters(), m_config->movementParameters)));
|
|
m_effectsAnimator->setGlobalTag("effectDirectives", speciesDef->effectDirectives());
|
|
|
|
m_genericProperties = diskStore.getObject("genericProperties");
|
|
|
|
m_armor->reset();
|
|
refreshArmor();
|
|
|
|
m_codexes->learnInitialCodexes(species());
|
|
|
|
m_aiState = AiState(diskStore.get("aiState", JsonObject{}));
|
|
|
|
for (auto& script : m_genericScriptContexts)
|
|
script.second->setScriptStorage({});
|
|
|
|
for (auto& p : diskStore.get("genericScriptStorage", JsonObject{}).toObject()) {
|
|
if (auto script = m_genericScriptContexts.maybe(p.first).value({})) {
|
|
script->setScriptStorage(p.second.toObject());
|
|
}
|
|
}
|
|
|
|
// 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 : speciesDef->defaultBlueprints())
|
|
m_blueprints->add(descriptor);
|
|
}
|
|
|
|
ClientContextPtr Player::clientContext() const {
|
|
return m_clientContext;
|
|
}
|
|
|
|
void Player::setClientContext(ClientContextPtr clientContext) {
|
|
m_clientContext = std::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;
|
|
}
|
|
|
|
ClientEntityMode Player::clientEntityMode() const {
|
|
return ClientEntityMode::ClientPresenceMaster;
|
|
}
|
|
|
|
void Player::init(World* world, EntityId entityId, EntityMode mode) {
|
|
Entity::init(world, entityId, mode);
|
|
|
|
|
|
m_tools->init(this);
|
|
m_movementController->init(world);
|
|
m_movementController->setIgnorePhysicsEntities({entityId});
|
|
m_statusController->init(this, m_movementController.get());
|
|
m_techController->init(this, m_movementController.get(), m_statusController.get());
|
|
|
|
if (mode == EntityMode::Master) {
|
|
auto speciesDefinition = Root::singleton().speciesDatabase()->species(m_identity.species);
|
|
m_movementController->setRotation(0);
|
|
m_statusController->setStatusProperty("ouchNoise", speciesDefinition->ouchNoise(m_identity.gender));
|
|
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().addDirectivesGroup(m_techController->parentDirectives(), true);
|
|
drawable.imagePart().addDirectivesGroup(m_statusController->parentDirectives(), true);
|
|
|
|
if (auto anchor = as<LoungeAnchor>(m_movementController->entityAnchor())) {
|
|
if (auto& directives = anchor->directives)
|
|
drawable.imagePart().addDirectives(*directives, true);
|
|
}
|
|
}
|
|
drawables.append(std::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, float pitch) {
|
|
m_callbackSounds.emplaceAppend(sound, volume, pitch);
|
|
}
|
|
|
|
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(bool ignoreAdjustments) const {
|
|
return Vec2F(
|
|
m_humanoid->mouthOffset(ignoreAdjustments)[0] * numericalDirection(facingDirection()), m_humanoid->mouthOffset(ignoreAdjustments)[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(true);
|
|
}
|
|
|
|
Vec2F Player::mouthPosition(bool ignoreAdjustments) const {
|
|
return position() + mouthOffset(ignoreAdjustments);
|
|
}
|
|
|
|
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::setMoveVector(Vec2F const& vec) {
|
|
m_moveVector = vec;
|
|
}
|
|
|
|
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;
|
|
|
|
Vec2F throwDirection = world()->geometry().diff(aimPosition(), position());
|
|
for (auto& throwSlot : {m_inventory->primaryHeldSlot(), m_inventory->secondaryHeldSlot()}) {
|
|
if (throwSlot) {
|
|
if (auto drop = m_inventory->takeSlot(*throwSlot)) {
|
|
world()->addEntity(ItemDrop::throwDrop(drop, position(), velocity(), throwDirection));
|
|
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(float dt, uint64_t) {
|
|
m_movementController->setTimestep(dt);
|
|
|
|
if (isMaster()) {
|
|
if (m_emoteCooldownTimer) {
|
|
m_emoteCooldownTimer -= dt;
|
|
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 -= dt;
|
|
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 += dt;
|
|
|
|
if (m_movementController->zeroG())
|
|
m_movementController->controlParameters(m_zeroGMovementParameters);
|
|
|
|
if (isTeleporting()) {
|
|
m_teleportTimer -= dt;
|
|
if (m_teleportTimer <= 0 && m_state == State::TeleportIn) {
|
|
m_state = State::Idle;
|
|
m_effectsAnimator->burstParticleEmitter(m_teleportAnimationType + "Burst");
|
|
}
|
|
}
|
|
|
|
if (!isTeleporting()) {
|
|
processControls();
|
|
|
|
m_questManager->update(dt);
|
|
m_companions->update(dt);
|
|
m_deployment->update(dt);
|
|
|
|
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(dt);
|
|
|
|
m_techController->tickMaster(dt);
|
|
|
|
for (auto& p : m_genericScriptContexts)
|
|
p.second->update(p.second->updateDt(dt));
|
|
|
|
if (edgeTriggeredUse) {
|
|
auto anchor = as<LoungeAnchor>(m_movementController->entityAnchor());
|
|
bool useTool = canUseTool();
|
|
if (anchor && (!useTool || anchor->controllable))
|
|
m_movementController->resetAnchorState();
|
|
else if (useTool) {
|
|
if (auto ie = bestInteractionEntity(true))
|
|
interactWithEntity(ie);
|
|
}
|
|
}
|
|
|
|
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(dt);
|
|
|
|
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(dt))
|
|
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(GlobalTimestep);
|
|
|
|
if (m_ageItemsTimer.wrapTick(dt)) {
|
|
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(GlobalTimestep);
|
|
m_movementController->tickSlave(dt);
|
|
m_techController->tickSlave(dt);
|
|
m_statusController->tickSlave(dt);
|
|
}
|
|
|
|
m_humanoid->setMovingBackwards(false);
|
|
m_humanoid->setRotation(m_movementController->rotation());
|
|
|
|
bool suppressedItems = !canUseTool();
|
|
|
|
auto loungeAnchor = as<LoungeAnchor>(m_movementController->entityAnchor());
|
|
if (loungeAnchor && loungeAnchor->dance)
|
|
m_humanoid->setDance(*loungeAnchor->dance);
|
|
else if ((!suppressedItems && (m_tools->primaryHandItem() || m_tools->altHandItem()))
|
|
|| m_humanoid->danceCyclicOrEnded() || m_movementController->running())
|
|
m_humanoid->setDance({});
|
|
|
|
bool isClient = world()->isClient();
|
|
if (isClient)
|
|
m_armor->setupHumanoidClothingDrawables(*m_humanoid, forceNude());
|
|
|
|
m_tools->suppressItems(suppressedItems);
|
|
m_tools->tick(dt, 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 (m_state == State::Walk || m_state == State::Run) {
|
|
if ((m_footstepTimer += dt) > m_config->footstepTiming) {
|
|
m_footstepPending = true;
|
|
m_footstepTimer = 0.0;
|
|
}
|
|
}
|
|
|
|
if (isClient) {
|
|
m_effectsAnimator->update(dt, &m_effectsAnimatorDynamicTarget);
|
|
m_effectsAnimatorDynamicTarget.updatePosition(position() + m_techController->parentOffset());
|
|
} else {
|
|
m_effectsAnimator->update(dt, nullptr);
|
|
}
|
|
|
|
if (!isTeleporting())
|
|
processStateChanges(dt);
|
|
|
|
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(dt, *entityMode());
|
|
|
|
m_humanoid->setFacingDirection(m_movementController->facingDirection());
|
|
m_humanoid->setMovingBackwards(m_movementController->facingDirection() != m_movementController->movingDirection());
|
|
|
|
m_pendingMoves.clear();
|
|
|
|
if (isClient)
|
|
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(std::move(landingNoise));
|
|
}
|
|
|
|
if (m_footstepPending) {
|
|
auto stepNoise = make_shared<AudioInstance>(*footstepAudio);
|
|
stepNoise->setPosition(position() + feetOffset());
|
|
stepNoise->setVolume(1 - Random::randf(0, m_footstepVolumeVariance));
|
|
renderCallback->addAudio(std::move(stepNoise));
|
|
}
|
|
} else {
|
|
m_footstepTimer = m_config->footstepTiming;
|
|
}
|
|
m_footstepPending = false;
|
|
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(get<0>(p)));
|
|
audio->setVolume(get<1>(p));
|
|
audio->setPitchMultiplier(get<2>(p));
|
|
audio->setPosition(position());
|
|
renderCallback->addAudio(std::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());
|
|
|
|
m_tools->render(renderCallback, inToolRange(), m_shifting, renderLayer);
|
|
|
|
m_effectEmitter->render(renderCallback);
|
|
m_songbook->render(renderCallback);
|
|
|
|
if (isMaster())
|
|
m_deployment->render(renderCallback, position());
|
|
}
|
|
|
|
void Player::renderLightSources(RenderCallback* renderCallback) {
|
|
renderCallback->addLightSources(lightSources());
|
|
m_deployment->renderLightSources(renderCallback);
|
|
}
|
|
|
|
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) {
|
|
if (value.isNull())
|
|
m_genericProperties.erase(name);
|
|
else
|
|
m_genericProperties.set(name, value);
|
|
}
|
|
|
|
PlayerInventoryPtr Player::inventory() const {
|
|
return m_inventory;
|
|
}
|
|
|
|
uint64_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()});
|
|
float pitch = 1.f - ((float)items->count() / (float)items->maxStack()) * 0.5f;
|
|
m_effectsAnimator->setSoundPitchMultiplier("pickup", clamp(pitch * Random::randf(0.8f, 1.2f), 0.f, 2.f));
|
|
m_effectsAnimator->playSound("pickup");
|
|
}
|
|
auto itemDb = Root::singleton().itemDatabase();
|
|
queueItemPickupMessage(itemDb->itemShared(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);
|
|
}
|
|
|
|
uint64_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::refreshItems() {
|
|
if (isSlave())
|
|
return;
|
|
|
|
m_tools->setItems(m_inventory->primaryHeldItem(), m_inventory->secondaryHeldItem());
|
|
}
|
|
|
|
void Player::refreshArmor() {
|
|
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());
|
|
}
|
|
|
|
void Player::refreshEquipment() {
|
|
refreshArmor();
|
|
refreshItems();
|
|
}
|
|
|
|
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, isAdmin() ? 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 && (isAdmin() || 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 {
|
|
bool canUse = !isDead() && !isTeleporting() && !m_techController->toolUsageSuppressed();
|
|
if (canUse) {
|
|
if (auto loungeAnchor = as<LoungeAnchor>(m_movementController->entityAnchor()))
|
|
if (loungeAnchor->suppressTools.value(loungeAnchor->controllable))
|
|
return false;
|
|
}
|
|
return canUse;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
void Player::setInteractRadius(float interactRadius) {
|
|
m_interactRadius = 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;
|
|
}
|
|
|
|
void Player::setDescription(String const& description) {
|
|
m_description = 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(std::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");
|
|
|
|
bool useMoveVector = m_moveVector.x() != 0.0f;
|
|
if (useMoveVector) {
|
|
for (auto move : m_pendingMoves) {
|
|
if (move == MoveControlType::Left || move == MoveControlType::Right) {
|
|
useMoveVector = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (useMoveVector) {
|
|
m_pendingMoves.insert(m_moveVector.x() < 0.0f ? MoveControlType::Left : MoveControlType::Right);
|
|
m_movementController->setMoveSpeedMultiplier(clamp(abs(m_moveVector.x()), 0.0f, 1.0f));
|
|
}
|
|
else
|
|
m_movementController->setMoveSpeedMultiplier(1.0f);
|
|
|
|
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(float dt) {
|
|
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_moveVector.x() != 0.0f && (m_state == State::Run))
|
|
m_state = abs(m_moveVector.x()) > 0.5f ? State::Run : State::Walk;
|
|
|
|
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(dt);
|
|
|
|
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 if (techState == TechController::ParentState::SwimIdle) {
|
|
m_humanoid->setState(Humanoid::SwimIdle);
|
|
}
|
|
} 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 isAdmin() || world()->geometry().diff(aimPos, position()).magnitude() < interactRadius();
|
|
}
|
|
|
|
bool Player::inToolRange() const {
|
|
return inToolRange(centerOfTile(aimPosition()));
|
|
}
|
|
|
|
bool Player::inToolRange(Vec2F const& aimPos) const {
|
|
return isAdmin() || 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_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;
|
|
updateIdentity();
|
|
}
|
|
|
|
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 = std::move(shipUpgrades);
|
|
}
|
|
|
|
void Player::applyShipUpgrades(Json const& upgrades) {
|
|
if (m_clientContext->playerUuid() == uuid())
|
|
m_clientContext->rpcInterface()->invokeRemote("ship.applyShipUpgrades", upgrades);
|
|
else
|
|
m_shipUpgrades.apply(upgrades);
|
|
}
|
|
|
|
String Player::name() const {
|
|
return m_identity.name;
|
|
}
|
|
|
|
void Player::setName(String const& name) {
|
|
m_identity.name = name;
|
|
updateIdentity();
|
|
}
|
|
|
|
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"));
|
|
}
|
|
|
|
Vec2F Player::nametagOrigin() const {
|
|
return mouthPosition(false);
|
|
}
|
|
|
|
void Player::updateIdentity()
|
|
{ m_identityUpdated = true; m_humanoid->setIdentity(m_identity); }
|
|
|
|
void Player::setBodyDirectives(String const& directives)
|
|
{ m_identity.bodyDirectives = directives; updateIdentity(); }
|
|
|
|
void Player::setEmoteDirectives(String const& directives)
|
|
{ m_identity.emoteDirectives = directives; updateIdentity(); }
|
|
|
|
void Player::setHairGroup(String const& group)
|
|
{ m_identity.hairGroup = group; updateIdentity(); }
|
|
|
|
void Player::setHairType(String const& type)
|
|
{ m_identity.hairType = type; updateIdentity(); }
|
|
|
|
void Player::setHairDirectives(String const& directives)
|
|
{ m_identity.hairDirectives = directives; updateIdentity(); }
|
|
|
|
void Player::setFacialHairGroup(String const& group)
|
|
{ m_identity.facialHairGroup = group; updateIdentity(); }
|
|
|
|
void Player::setFacialHairType(String const& type)
|
|
{ m_identity.facialHairType = type; updateIdentity(); }
|
|
|
|
void Player::setFacialHairDirectives(String const& directives)
|
|
{ m_identity.facialHairDirectives = directives; updateIdentity(); }
|
|
|
|
void Player::setFacialMaskGroup(String const& group)
|
|
{ m_identity.facialMaskGroup = group; updateIdentity(); }
|
|
|
|
void Player::setFacialMaskType(String const& type)
|
|
{ m_identity.facialMaskType = type; updateIdentity(); }
|
|
|
|
void Player::setFacialMaskDirectives(String const& directives)
|
|
{ m_identity.facialMaskDirectives = directives; updateIdentity(); }
|
|
|
|
void Player::setHair(String const& group, String const& type, String const& directives) {
|
|
m_identity.hairGroup = group;
|
|
m_identity.hairType = type;
|
|
m_identity.hairDirectives = directives;
|
|
updateIdentity();
|
|
}
|
|
|
|
void Player::setFacialHair(String const& group, String const& type, String const& directives) {
|
|
m_identity.facialHairGroup = group;
|
|
m_identity.facialHairType = type;
|
|
m_identity.facialHairDirectives = directives;
|
|
updateIdentity();
|
|
}
|
|
|
|
void Player::setFacialMask(String const& group, String const& type, String const& directives) {
|
|
m_identity.facialMaskGroup = group;
|
|
m_identity.facialMaskType = type;
|
|
m_identity.facialMaskDirectives = directives;
|
|
updateIdentity();
|
|
}
|
|
|
|
void Player::setSpecies(String const& species) {
|
|
m_identity.species = species;
|
|
updateIdentity();
|
|
}
|
|
|
|
Gender Player::gender() const {
|
|
return m_identity.gender;
|
|
}
|
|
|
|
void Player::setGender(Gender const& gender) {
|
|
m_identity.gender = gender;
|
|
updateIdentity();
|
|
}
|
|
|
|
String Player::species() const {
|
|
return m_identity.species;
|
|
}
|
|
|
|
void Player::setPersonality(Personality const& personality) {
|
|
m_identity.personality = personality;
|
|
updateIdentity();
|
|
}
|
|
|
|
void Player::setImagePath(Maybe<String> const& imagePath) {
|
|
m_identity.imagePath = imagePath;
|
|
updateIdentity();
|
|
}
|
|
|
|
HumanoidPtr Player::humanoid() {
|
|
return m_humanoid;
|
|
}
|
|
|
|
HumanoidIdentity const& Player::identity() const {
|
|
return m_identity;
|
|
}
|
|
|
|
void Player::setIdentity(HumanoidIdentity identity) {
|
|
m_identity = std::move(identity);
|
|
updateIdentity();
|
|
}
|
|
|
|
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, Maybe<float> emoteCooldown) {
|
|
starAssert(!isSlave());
|
|
m_emoteState = emote;
|
|
m_emoteCooldownTimer = emoteCooldown.value(m_emoteCooldown);
|
|
}
|
|
|
|
pair<HumanoidEmote, float> Player::currentEmote() const {
|
|
return make_pair(m_emoteState, m_emoteCooldownTimer);
|
|
}
|
|
|
|
List<ChatAction> Player::pullPendingChatActions() {
|
|
return take(m_pendingChatActions);
|
|
}
|
|
|
|
Maybe<String> Player::inspectionLogName() const {
|
|
auto identifier = uniqueId();
|
|
if (String* str = identifier.ptr()) {
|
|
auto hash = XXH3_128bits(str->utf8Ptr(), str->utf8Size());
|
|
return String("Player #") + hexEncode((const char*)&hash, sizeof(hash));
|
|
}
|
|
return identifier;
|
|
}
|
|
|
|
Maybe<String> Player::inspectionDescription(String const& species) const {
|
|
return m_description;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
|
|
StatusControllerPtr Player::statusControllerPtr() {
|
|
return m_statusController;
|
|
}
|
|
|
|
ActorMovementControllerPtr Player::movementControllerPtr() {
|
|
return m_movementController;
|
|
}
|
|
|
|
PlayerConfigPtr Player::config() {
|
|
return m_config;
|
|
}
|
|
|
|
SongbookPtr Player::songbook() const {
|
|
return m_songbook;
|
|
}
|
|
|
|
QuestManagerPtr Player::questManager() const {
|
|
return m_questManager;
|
|
}
|
|
|
|
Json Player::diskStore() {
|
|
JsonObject genericScriptStorage;
|
|
for (auto& p : m_genericScriptContexts) {
|
|
auto scriptStorage = p.second->getScriptStorage();
|
|
if (!scriptStorage.empty())
|
|
genericScriptStorage[p.first] = std::move(scriptStorage);
|
|
}
|
|
|
|
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();
|
|
|
|
m_description = strf("This {} seems to have nothing to say for {}self.",
|
|
m_identity.gender == Gender::Male ? "guy" : "gal",
|
|
m_identity.gender == Gender::Male ? "him" : "her");
|
|
}
|
|
|
|
bool Player::invisible() const {
|
|
return m_statusController->statPositive("invisible");
|
|
}
|
|
|
|
void Player::animatePortrait(float dt) {
|
|
m_humanoid->animate(dt);
|
|
if (m_emoteCooldownTimer) {
|
|
m_emoteCooldownTimer -= dt;
|
|
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(), velocity(), 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 '{}': {}", 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 && m_pendingCinematic->isType(Json::Type::String))
|
|
m_log->addCinematic(m_pendingCinematic->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();
|
|
}
|
|
|
|
NetworkedAnimatorPtr Player::effectsAnimator() {
|
|
return m_effectsAnimator;
|
|
}
|
|
|
|
const String secretProprefix = "\0JsonProperty\0"s;
|
|
|
|
Maybe<StringView> Player::getSecretPropertyView(String const& name) const {
|
|
if (auto tag = m_effectsAnimator->globalTagPtr(secretProprefix + name)) {
|
|
auto& view = tag->utf8();
|
|
DataStreamExternalBuffer buffer(view.data(), view.size());
|
|
try {
|
|
uint8_t typeIndex = buffer.read<uint8_t>() - 1;
|
|
if ((Json::Type)typeIndex == Json::Type::String) {
|
|
size_t len = buffer.readVlqU();
|
|
size_t pos = buffer.pos();
|
|
if (pos + len == buffer.size())
|
|
return StringView(buffer.ptr() + pos, len);
|
|
}
|
|
}
|
|
catch (StarException const& e) {}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
Json Player::getSecretProperty(String const& name, Json defaultValue) const {
|
|
if (auto tag = m_effectsAnimator->globalTagPtr(secretProprefix + name)) {
|
|
DataStreamExternalBuffer buffer(tag->utf8Ptr(), tag->utf8Size());
|
|
try
|
|
{ return buffer.read<Json>(); }
|
|
catch (StarException const& e)
|
|
{ Logger::error("Exception reading secret player property '{}': {}", name, e.what()); }
|
|
}
|
|
|
|
return defaultValue;
|
|
}
|
|
|
|
void Player::setSecretProperty(String const& name, Json const& value) {
|
|
if (value) {
|
|
DataStreamBuffer ds;
|
|
ds.write(value);
|
|
auto& data = ds.data();
|
|
m_effectsAnimator->setGlobalTag(secretProprefix + name, String(data.ptr(), data.size()));
|
|
}
|
|
else
|
|
m_effectsAnimator->removeGlobalTag(secretProprefix + name);
|
|
}
|
|
|
|
|
|
}
|