2023-06-20 04:33:09 +00:00
|
|
|
#include "StarPlayer.hpp"
|
2023-06-30 01:44:42 +00:00
|
|
|
#include "StarEncode.hpp"
|
2023-06-20 04:33:09 +00:00
|
|
|
#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;
|
2023-07-22 12:31:04 +00:00
|
|
|
m_client = nullptr;
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
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;
|
2023-07-20 14:58:49 +00:00
|
|
|
m_footstepPending = false;
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
2023-07-22 12:31:04 +00:00
|
|
|
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)));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
Player::Player(PlayerConfigPtr config, Json const& diskStore) : Player(config) {
|
2023-07-22 12:31:04 +00:00
|
|
|
diskLoad(diskStore);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Player::diskLoad(Json const& diskStore) {
|
2023-06-20 04:33:09 +00:00
|
|
|
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"));
|
2023-07-22 12:31:04 +00:00
|
|
|
if (m_clientContext)
|
|
|
|
m_universeMap->setServerUuid(m_clientContext->serverUuid());
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
m_codexes = make_shared<PlayerCodexes>(diskStore.get("codexes"));
|
|
|
|
m_techs = make_shared<PlayerTech>(diskStore.get("techs"));
|
|
|
|
m_identity = HumanoidIdentity(diskStore.get("identity"));
|
2023-07-28 14:51:44 +00:00
|
|
|
m_identityUpdated = true;
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
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"));
|
|
|
|
|
2023-07-22 12:31:04 +00:00
|
|
|
auto speciesDef = Root::singleton().speciesDatabase()->species(m_identity.species);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
m_questManager->diskLoad(diskStore.get("quests", JsonObject{}));
|
|
|
|
m_companions->diskLoad(diskStore.get("companions", JsonObject{}));
|
|
|
|
m_deployment->diskLoad(diskStore.get("deployment", JsonObject{}));
|
2023-07-22 12:31:04 +00:00
|
|
|
m_humanoid = make_shared<Humanoid>(speciesDef->humanoidConfig());
|
2023-06-20 04:33:09 +00:00
|
|
|
m_humanoid->setIdentity(m_identity);
|
|
|
|
m_movementController->resetBaseParameters(ActorMovementParameters(jsonMerge(m_humanoid->defaultMovementParameters(), m_config->movementParameters)));
|
2023-07-22 12:31:04 +00:00
|
|
|
m_effectsAnimator->setGlobalTag("effectDirectives", speciesDef->effectDirectives());
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
m_genericProperties = diskStore.getObject("genericProperties");
|
|
|
|
|
2023-10-25 04:30:31 +00:00
|
|
|
m_armor->reset();
|
2023-07-24 07:54:31 +00:00
|
|
|
refreshArmor();
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-07-22 12:31:04 +00:00
|
|
|
m_codexes->learnInitialCodexes(species());
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
m_aiState = AiState(diskStore.get("aiState", JsonObject{}));
|
|
|
|
|
2023-07-22 12:31:04 +00:00
|
|
|
for (auto& script : m_genericScriptContexts)
|
|
|
|
script.second->setScriptStorage({});
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
for (auto& p : diskStore.get("genericScriptStorage", JsonObject{}).toObject()) {
|
|
|
|
if (auto script = m_genericScriptContexts.maybe(p.first).value({})) {
|
|
|
|
script->setScriptStorage(p.second.toObject());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-22 12:31:04 +00:00
|
|
|
// 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);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ClientContextPtr Player::clientContext() const {
|
|
|
|
return m_clientContext;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Player::setClientContext(ClientContextPtr clientContext) {
|
2024-02-19 15:55:19 +00:00
|
|
|
m_clientContext = std::move(clientContext);
|
2023-06-20 04:33:09 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-07-22 12:31:04 +00:00
|
|
|
ClientEntityMode Player::clientEntityMode() const {
|
|
|
|
return ClientEntityMode::ClientPresenceMaster;
|
|
|
|
}
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
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) {
|
2023-06-23 10:27:51 +00:00
|
|
|
auto speciesDefinition = Root::singleton().speciesDatabase()->species(m_identity.species);
|
|
|
|
m_movementController->setRotation(0);
|
2023-06-21 10:36:08 +00:00
|
|
|
m_statusController->setStatusProperty("ouchNoise", speciesDefinition->ouchNoise(m_identity.gender));
|
2023-06-20 04:33:09 +00:00
|
|
|
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);
|
|
|
|
}
|
2024-03-09 00:09:04 +00:00
|
|
|
|
|
|
|
for (auto& p : m_inventory->pullOverflow()) {
|
|
|
|
world->addEntity(ItemDrop::createRandomizedDrop(p, m_movementController->position(), true));
|
2024-03-07 18:12:11 +00:00
|
|
|
}
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
2024-03-07 18:12:11 +00:00
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
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);
|
2024-04-14 00:32:11 +00:00
|
|
|
|
|
|
|
// Auto-detect any ?scalenearest and apply them as a direct scale on the Humanoid's drawables instead.
|
|
|
|
DirectivesGroup humanoidDirectives;
|
|
|
|
Vec2F scale = Vec2F::filled(1.f);
|
|
|
|
auto extractScale = [&](List<Directives> const& list) {
|
|
|
|
for (auto& directives : list) {
|
|
|
|
auto result = Humanoid::extractScaleFromDirectives(directives);
|
|
|
|
scale = scale.piecewiseMultiply(result.first);
|
|
|
|
humanoidDirectives.append(result.second);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
extractScale(m_techController->parentDirectives().list());
|
|
|
|
extractScale(m_statusController->parentDirectives().list());
|
2024-04-28 20:18:58 +00:00
|
|
|
m_humanoid->setScale(scale);
|
2024-04-14 00:32:11 +00:00
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
for (auto& drawable : m_humanoid->render()) {
|
|
|
|
drawable.translate(position() + m_techController->parentOffset());
|
|
|
|
if (drawable.isImage()) {
|
2024-04-14 00:32:11 +00:00
|
|
|
drawable.imagePart().addDirectivesGroup(humanoidDirectives, true);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
if (auto anchor = as<LoungeAnchor>(m_movementController->entityAnchor())) {
|
2023-06-25 08:12:54 +00:00
|
|
|
if (auto& directives = anchor->directives)
|
2023-06-20 04:33:09 +00:00
|
|
|
drawable.imagePart().addDirectives(*directives, true);
|
|
|
|
}
|
|
|
|
}
|
2024-02-19 15:55:19 +00:00
|
|
|
drawables.append(std::move(drawable));
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-08-18 13:14:53 +00:00
|
|
|
void Player::addSound(String const& sound, float volume, float pitch) {
|
|
|
|
m_callbackSounds.emplaceAppend(sound, volume, pitch);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2023-06-26 14:42:07 +00:00
|
|
|
Vec2F Player::mouthOffset(bool ignoreAdjustments) const {
|
2023-06-20 04:33:09 +00:00
|
|
|
return Vec2F(
|
2023-06-26 14:42:07 +00:00
|
|
|
m_humanoid->mouthOffset(ignoreAdjustments)[0] * numericalDirection(facingDirection()), m_humanoid->mouthOffset(ignoreAdjustments)[1]);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2023-06-26 14:42:07 +00:00
|
|
|
return position() + mouthOffset(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
Vec2F Player::mouthPosition(bool ignoreAdjustments) const {
|
|
|
|
return position() + mouthOffset(ignoreAdjustments);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-06-28 12:52:09 +00:00
|
|
|
void Player::setMoveVector(Vec2F const& vec) {
|
|
|
|
m_moveVector = vec;
|
|
|
|
}
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
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;
|
|
|
|
|
2023-08-18 10:03:06 +00:00
|
|
|
Vec2F throwDirection = world()->geometry().diff(aimPosition(), position());
|
|
|
|
for (auto& throwSlot : {m_inventory->primaryHeldSlot(), m_inventory->secondaryHeldSlot()}) {
|
2023-06-20 04:33:09 +00:00
|
|
|
if (throwSlot) {
|
|
|
|
if (auto drop = m_inventory->takeSlot(*throwSlot)) {
|
2023-08-18 10:03:06 +00:00
|
|
|
world()->addEntity(ItemDrop::throwDrop(drop, position(), velocity(), throwDirection));
|
2023-06-20 04:33:09 +00:00
|
|
|
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 {};
|
|
|
|
}
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
void Player::update(float dt, uint64_t) {
|
|
|
|
m_movementController->setTimestep(dt);
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
if (isMaster()) {
|
|
|
|
if (m_emoteCooldownTimer) {
|
2023-07-20 14:58:49 +00:00
|
|
|
m_emoteCooldownTimer -= dt;
|
2023-06-20 04:33:09 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
m_blinkCooldownTimer -= dt;
|
2023-06-20 04:33:09 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
m_lastDamagedOtherTimer += dt;
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
if (m_movementController->zeroG())
|
|
|
|
m_movementController->controlParameters(m_zeroGMovementParameters);
|
|
|
|
|
|
|
|
if (isTeleporting()) {
|
2023-07-20 14:58:49 +00:00
|
|
|
m_teleportTimer -= dt;
|
2023-06-20 04:33:09 +00:00
|
|
|
if (m_teleportTimer <= 0 && m_state == State::TeleportIn) {
|
|
|
|
m_state = State::Idle;
|
|
|
|
m_effectsAnimator->burstParticleEmitter(m_teleportAnimationType + "Burst");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isTeleporting()) {
|
|
|
|
processControls();
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
m_questManager->update(dt);
|
|
|
|
m_companions->update(dt);
|
|
|
|
m_deployment->update(dt);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2024-03-15 05:26:12 +00:00
|
|
|
auto aimRelative = world()->geometry().diff(m_aimPosition, position()); // dumb, but due to how things are ordered
|
2023-07-20 14:58:49 +00:00
|
|
|
m_movementController->tickMaster(dt);
|
2024-03-15 05:26:12 +00:00
|
|
|
m_aimPosition = position() + aimRelative; // it's gonna have to be like this for now
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
m_techController->tickMaster(dt);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
for (auto& p : m_genericScriptContexts)
|
2023-07-20 14:58:49 +00:00
|
|
|
p.second->update(p.second->updateDt(dt));
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
if (edgeTriggeredUse) {
|
2023-07-20 05:27:28 +00:00
|
|
|
auto anchor = as<LoungeAnchor>(m_movementController->entityAnchor());
|
|
|
|
bool useTool = canUseTool();
|
|
|
|
if (anchor && (!useTool || anchor->controllable))
|
|
|
|
m_movementController->resetAnchorState();
|
|
|
|
else if (useTool) {
|
2023-06-20 04:33:09 +00:00
|
|
|
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())
|
2023-07-20 14:58:49 +00:00
|
|
|
m_statusController->tickMaster(dt);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
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) {
|
2023-07-20 14:58:49 +00:00
|
|
|
if (pair.first.tick(dt))
|
2023-06-20 04:33:09 +00:00
|
|
|
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");
|
|
|
|
}
|
|
|
|
|
2023-08-15 03:38:40 +00:00
|
|
|
m_log->addPlayTime(GlobalTimestep);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
if (m_ageItemsTimer.wrapTick(dt)) {
|
2023-06-20 04:33:09 +00:00
|
|
|
auto itemDatabase = Root::singleton().itemDatabase();
|
|
|
|
m_inventory->forEveryItem([&](InventorySlot const&, ItemPtr& item) {
|
|
|
|
itemDatabase->ageItem(item, m_ageItemsTimer.time);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-06-17 10:31:40 +00:00
|
|
|
for (auto& tool : {m_tools->primaryHandItem(), m_tools->altHandItem()}) {
|
2023-06-20 04:33:09 +00:00
|
|
|
if (auto inspectionTool = as<InspectionTool>(tool)) {
|
2024-06-17 10:31:40 +00:00
|
|
|
for (auto& ir : inspectionTool->pullInspectionResults()) {
|
2023-06-20 04:33:09 +00:00
|
|
|
if (ir.objectName) {
|
|
|
|
m_questManager->receiveMessage("objectScanned", true, {*ir.objectName, *ir.entityId});
|
|
|
|
m_log->addScannedObject(*ir.objectName);
|
|
|
|
}
|
|
|
|
|
2024-06-17 10:31:40 +00:00
|
|
|
addChatMessage(ir.message, JsonObject{
|
|
|
|
{"message", JsonObject{
|
|
|
|
{"context", JsonObject{{"mode", "RadioMessage"}}},
|
|
|
|
{"fromConnection", world()->connection()},
|
|
|
|
{"text", ir.message}
|
|
|
|
}}
|
|
|
|
});
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
m_interestingObjects = m_questManager->interestingObjects();
|
|
|
|
|
|
|
|
} else {
|
2024-03-11 05:31:20 +00:00
|
|
|
m_netGroup.tickNetInterpolation(dt);
|
2023-07-20 14:58:49 +00:00
|
|
|
m_movementController->tickSlave(dt);
|
|
|
|
m_techController->tickSlave(dt);
|
|
|
|
m_statusController->tickSlave(dt);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
m_humanoid->setMovingBackwards(false);
|
|
|
|
m_humanoid->setRotation(m_movementController->rotation());
|
|
|
|
|
2023-08-18 10:03:06 +00:00
|
|
|
bool suppressedItems = !canUseTool();
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
auto loungeAnchor = as<LoungeAnchor>(m_movementController->entityAnchor());
|
|
|
|
if (loungeAnchor && loungeAnchor->dance)
|
|
|
|
m_humanoid->setDance(*loungeAnchor->dance);
|
2023-08-18 10:03:06 +00:00
|
|
|
else if ((!suppressedItems && (m_tools->primaryHandItem() || m_tools->altHandItem()))
|
|
|
|
|| m_humanoid->danceCyclicOrEnded() || m_movementController->running())
|
2023-06-20 04:33:09 +00:00
|
|
|
m_humanoid->setDance({});
|
|
|
|
|
2023-06-27 14:50:47 +00:00
|
|
|
bool isClient = world()->isClient();
|
|
|
|
if (isClient)
|
|
|
|
m_armor->setupHumanoidClothingDrawables(*m_humanoid, forceNude());
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-08-18 10:03:06 +00:00
|
|
|
m_tools->suppressItems(suppressedItems);
|
2023-07-20 14:58:49 +00:00
|
|
|
m_tools->tick(dt, m_shifting, m_pendingMoves);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
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));
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
if (m_state == State::Walk || m_state == State::Run) {
|
|
|
|
if ((m_footstepTimer += dt) > m_config->footstepTiming) {
|
|
|
|
m_footstepPending = true;
|
|
|
|
m_footstepTimer = 0.0;
|
|
|
|
}
|
|
|
|
}
|
2023-06-27 14:50:47 +00:00
|
|
|
|
|
|
|
if (isClient) {
|
2023-07-20 14:58:49 +00:00
|
|
|
m_effectsAnimator->update(dt, &m_effectsAnimatorDynamicTarget);
|
2023-06-20 04:33:09 +00:00
|
|
|
m_effectsAnimatorDynamicTarget.updatePosition(position() + m_techController->parentOffset());
|
|
|
|
} else {
|
2023-07-20 14:58:49 +00:00
|
|
|
m_effectsAnimator->update(dt, nullptr);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!isTeleporting())
|
2023-07-20 14:58:49 +00:00
|
|
|
processStateChanges(dt);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
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());
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
m_effectEmitter->tick(dt, *entityMode());
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
m_humanoid->setFacingDirection(m_movementController->facingDirection());
|
|
|
|
m_humanoid->setMovingBackwards(m_movementController->facingDirection() != m_movementController->movingDirection());
|
|
|
|
|
|
|
|
m_pendingMoves.clear();
|
|
|
|
|
2023-06-27 14:50:47 +00:00
|
|
|
if (isClient)
|
|
|
|
SpatialLogger::logPoly("world", m_movementController->collisionBody(), isMaster() ? Color::Orange.toRgba() : Color::Yellow.toRgba());
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2024-02-19 15:55:19 +00:00
|
|
|
renderCallback->addAudio(std::move(landingNoise));
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
if (m_footstepPending) {
|
|
|
|
auto stepNoise = make_shared<AudioInstance>(*footstepAudio);
|
|
|
|
stepNoise->setPosition(position() + feetOffset());
|
|
|
|
stepNoise->setVolume(1 - Random::randf(0, m_footstepVolumeVariance));
|
2024-02-19 15:55:19 +00:00
|
|
|
renderCallback->addAudio(std::move(stepNoise));
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
m_footstepTimer = m_config->footstepTiming;
|
|
|
|
}
|
2023-07-20 14:58:49 +00:00
|
|
|
m_footstepPending = false;
|
2023-06-20 04:33:09 +00:00
|
|
|
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)) {
|
2023-08-18 13:14:53 +00:00
|
|
|
auto audio = make_shared<AudioInstance>(*Root::singleton().assets()->audio(get<0>(p)));
|
|
|
|
audio->setVolume(get<1>(p));
|
|
|
|
audio->setPitchMultiplier(get<2>(p));
|
2023-06-20 04:33:09 +00:00
|
|
|
audio->setPosition(position());
|
2024-02-19 15:55:19 +00:00
|
|
|
renderCallback->addAudio(std::move(audio));
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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());
|
|
|
|
}
|
|
|
|
|
2023-06-29 00:11:19 +00:00
|
|
|
void Player::renderLightSources(RenderCallback* renderCallback) {
|
|
|
|
renderCallback->addLightSources(lightSources());
|
2023-10-12 21:54:37 +00:00
|
|
|
m_deployment->renderLightSources(renderCallback);
|
2023-06-29 00:11:19 +00:00
|
|
|
}
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
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) {
|
2023-06-21 10:36:08 +00:00
|
|
|
if (value.isNull())
|
|
|
|
m_genericProperties.erase(name);
|
|
|
|
else
|
|
|
|
m_genericProperties.set(name, value);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
PlayerInventoryPtr Player::inventory() const {
|
|
|
|
return m_inventory;
|
|
|
|
}
|
|
|
|
|
2023-08-19 02:15:22 +00:00
|
|
|
uint64_t Player::itemsCanHold(ItemPtr const& items) const {
|
2023-06-20 04:33:09 +00:00
|
|
|
return m_inventory->itemsCanFit(items);
|
|
|
|
}
|
|
|
|
|
2024-03-11 06:03:09 +00:00
|
|
|
ItemPtr Player::pickupItems(ItemPtr const& items, bool silent) {
|
2023-06-20 04:33:09 +00:00
|
|
|
if (isDead() || !items || m_inventory->itemsCanFit(items) == 0)
|
|
|
|
return items;
|
|
|
|
|
|
|
|
triggerPickupEvents(items);
|
|
|
|
|
2024-03-11 06:03:09 +00:00
|
|
|
if (!silent) {
|
|
|
|
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()));
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
2024-03-11 06:03:09 +00:00
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-08-19 02:15:22 +00:00
|
|
|
uint64_t Player::hasCountOfItem(ItemDescriptor const& descriptor, bool exactMatch) const {
|
2023-06-20 04:33:09 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2023-07-24 07:54:31 +00:00
|
|
|
void Player::refreshItems() {
|
|
|
|
if (isSlave())
|
|
|
|
return;
|
|
|
|
|
|
|
|
m_tools->setItems(m_inventory->primaryHeldItem(), m_inventory->secondaryHeldItem());
|
|
|
|
}
|
|
|
|
|
|
|
|
void Player::refreshArmor() {
|
2023-06-20 04:33:09 +00:00
|
|
|
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());
|
2023-07-24 07:54:31 +00:00
|
|
|
}
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-07-24 07:54:31 +00:00
|
|
|
void Player::refreshEquipment() {
|
|
|
|
refreshArmor();
|
|
|
|
refreshItems();
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2023-08-02 11:59:07 +00:00
|
|
|
if (auto entity = world()->getInteractiveInRange(m_aimPosition, isAdmin() ? m_aimPosition : position(), m_interactRadius)) {
|
2023-06-20 04:33:09 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-08-02 11:59:07 +00:00
|
|
|
if (interactiveEntity && (isAdmin() || world()->canReachEntity(position(), interactRadius(), interactiveEntity->entityId())))
|
2023-06-20 04:33:09 +00:00
|
|
|
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 {
|
2023-07-20 05:27:28 +00:00
|
|
|
bool canUse = !isDead() && !isTeleporting() && !m_techController->toolUsageSuppressed();
|
|
|
|
if (canUse) {
|
|
|
|
if (auto loungeAnchor = as<LoungeAnchor>(m_movementController->entityAnchor()))
|
2023-10-10 08:38:29 +00:00
|
|
|
if (loungeAnchor->suppressTools.value(loungeAnchor->controllable))
|
2023-07-20 05:27:28 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return canUse;
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-08-02 11:59:07 +00:00
|
|
|
void Player::setInteractRadius(float interactRadius) {
|
|
|
|
m_interactRadius = interactRadius;
|
|
|
|
}
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-06-30 01:44:42 +00:00
|
|
|
void Player::setDescription(String const& description) {
|
|
|
|
m_description = description;
|
|
|
|
}
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
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) {
|
2024-02-19 15:55:19 +00:00
|
|
|
m_netGroup.readNetState(std::move(data), interpolationTime);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Player::enableInterpolation(float) {
|
|
|
|
m_netGroup.enableNetInterpolation();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Player::disableInterpolation() {
|
|
|
|
m_netGroup.disableNetInterpolation();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Player::processControls() {
|
|
|
|
bool run = !m_shifting && !m_statusController->statPositive("encumberance");
|
2023-06-28 12:52:09 +00:00
|
|
|
|
|
|
|
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) {
|
2023-06-29 21:23:41 +00:00
|
|
|
m_pendingMoves.insert(m_moveVector.x() < 0.0f ? MoveControlType::Left : MoveControlType::Right);
|
2023-06-28 12:52:09 +00:00
|
|
|
m_movementController->setMoveSpeedMultiplier(clamp(abs(m_moveVector.x()), 0.0f, 1.0f));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
m_movementController->setMoveSpeedMultiplier(1.0f);
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
void Player::processStateChanges(float dt) {
|
2023-06-20 04:33:09 +00:00
|
|
|
if (isMaster()) {
|
2023-06-28 12:52:09 +00:00
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-28 12:59:15 +00:00
|
|
|
if (m_moveVector.x() != 0.0f && (m_state == State::Run))
|
2023-06-28 12:52:09 +00:00
|
|
|
m_state = abs(m_moveVector.x()) > 0.5f ? State::Run : State::Walk;
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
m_humanoid->animate(dt);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
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);
|
2023-08-02 12:02:21 +00:00
|
|
|
} else if (techState == TechController::ParentState::SwimIdle) {
|
|
|
|
m_humanoid->setState(Humanoid::SwimIdle);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
} 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 {
|
2023-08-02 11:59:07 +00:00
|
|
|
return isAdmin() || world()->geometry().diff(aimPos, position()).magnitude() < interactRadius();
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool Player::inToolRange() const {
|
|
|
|
return inToolRange(centerOfTile(aimPosition()));
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Player::inToolRange(Vec2F const& aimPos) const {
|
2023-08-02 11:59:07 +00:00
|
|
|
return isAdmin() || world()->geometry().diff(aimPos, position()).magnitude() < toolRadius();
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2024-03-19 14:53:34 +00:00
|
|
|
void Player::setFavoriteColor(Color color) {
|
|
|
|
m_identity.color = color.toRgba();
|
2023-06-28 19:58:24 +00:00
|
|
|
updateIdentity();
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
2024-03-19 14:53:34 +00:00
|
|
|
Color Player::favoriteColor() const {
|
|
|
|
return Color::rgba(m_identity.color);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2024-02-19 15:55:19 +00:00
|
|
|
m_shipUpgrades = std::move(shipUpgrades);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
2023-07-22 12:31:04 +00:00
|
|
|
void Player::applyShipUpgrades(Json const& upgrades) {
|
|
|
|
if (m_clientContext->playerUuid() == uuid())
|
|
|
|
m_clientContext->rpcInterface()->invokeRemote("ship.applyShipUpgrades", upgrades);
|
|
|
|
else
|
|
|
|
m_shipUpgrades.apply(upgrades);
|
|
|
|
}
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
String Player::name() const {
|
|
|
|
return m_identity.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Player::setName(String const& name) {
|
|
|
|
m_identity.name = name;
|
2023-06-28 19:58:24 +00:00
|
|
|
updateIdentity();
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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"));
|
|
|
|
}
|
|
|
|
|
2023-06-26 14:42:07 +00:00
|
|
|
Vec2F Player::nametagOrigin() const {
|
|
|
|
return mouthPosition(false);
|
|
|
|
}
|
|
|
|
|
2023-06-28 19:58:24 +00:00
|
|
|
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(); }
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-06-28 19:58:24 +00:00
|
|
|
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) {
|
2023-06-20 04:33:09 +00:00
|
|
|
m_identity.hairGroup = group;
|
|
|
|
m_identity.hairType = type;
|
2023-06-28 19:58:24 +00:00
|
|
|
m_identity.hairDirectives = directives;
|
|
|
|
updateIdentity();
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Player::setFacialHair(String const& group, String const& type, String const& directives) {
|
|
|
|
m_identity.facialHairGroup = group;
|
|
|
|
m_identity.facialHairType = type;
|
|
|
|
m_identity.facialHairDirectives = directives;
|
2023-06-28 19:58:24 +00:00
|
|
|
updateIdentity();
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Player::setFacialMask(String const& group, String const& type, String const& directives) {
|
|
|
|
m_identity.facialMaskGroup = group;
|
|
|
|
m_identity.facialMaskType = type;
|
|
|
|
m_identity.facialMaskDirectives = directives;
|
2023-06-28 19:58:24 +00:00
|
|
|
updateIdentity();
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Player::setSpecies(String const& species) {
|
|
|
|
m_identity.species = species;
|
2023-06-28 19:58:24 +00:00
|
|
|
updateIdentity();
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Gender Player::gender() const {
|
|
|
|
return m_identity.gender;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Player::setGender(Gender const& gender) {
|
|
|
|
m_identity.gender = gender;
|
2023-06-28 19:58:24 +00:00
|
|
|
updateIdentity();
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
String Player::species() const {
|
|
|
|
return m_identity.species;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Player::setPersonality(Personality const& personality) {
|
|
|
|
m_identity.personality = personality;
|
2023-06-28 19:58:24 +00:00
|
|
|
updateIdentity();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Player::setImagePath(Maybe<String> const& imagePath) {
|
|
|
|
m_identity.imagePath = imagePath;
|
|
|
|
updateIdentity();
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
2023-06-27 15:34:37 +00:00
|
|
|
HumanoidPtr Player::humanoid() {
|
|
|
|
return m_humanoid;
|
|
|
|
}
|
|
|
|
|
2023-06-28 19:58:24 +00:00
|
|
|
HumanoidIdentity const& Player::identity() const {
|
|
|
|
return m_identity;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Player::setIdentity(HumanoidIdentity identity) {
|
2024-02-19 15:55:19 +00:00
|
|
|
m_identity = std::move(identity);
|
2023-06-28 19:58:24 +00:00
|
|
|
updateIdentity();
|
|
|
|
}
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2024-06-17 10:31:40 +00:00
|
|
|
void Player::addChatMessage(String const& message, Json const& config) {
|
2023-06-20 04:33:09 +00:00
|
|
|
starAssert(!isSlave());
|
|
|
|
m_chatMessage = message;
|
|
|
|
m_chatMessageUpdated = true;
|
|
|
|
m_chatMessageChanged = true;
|
2024-06-17 10:31:40 +00:00
|
|
|
m_pendingChatActions.append(SayChatAction{entityId(), message, mouthPosition(), config});
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
2023-10-25 04:30:31 +00:00
|
|
|
void Player::addEmote(HumanoidEmote const& emote, Maybe<float> emoteCooldown) {
|
2023-06-20 04:33:09 +00:00
|
|
|
starAssert(!isSlave());
|
|
|
|
m_emoteState = emote;
|
2023-10-25 04:30:31 +00:00
|
|
|
m_emoteCooldownTimer = emoteCooldown.value(m_emoteCooldown);
|
|
|
|
}
|
|
|
|
|
|
|
|
pair<HumanoidEmote, float> Player::currentEmote() const {
|
|
|
|
return make_pair(m_emoteState, m_emoteCooldownTimer);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
List<ChatAction> Player::pullPendingChatActions() {
|
|
|
|
return take(m_pendingChatActions);
|
|
|
|
}
|
|
|
|
|
2023-06-30 01:44:42 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2024-02-28 17:11:55 +00:00
|
|
|
Maybe<String> Player::inspectionDescription(String const&) const {
|
2023-06-30 01:44:42 +00:00
|
|
|
return m_description;
|
|
|
|
}
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2023-07-22 12:31:04 +00:00
|
|
|
|
|
|
|
StatusControllerPtr Player::statusControllerPtr() {
|
|
|
|
return m_statusController;
|
|
|
|
}
|
|
|
|
|
|
|
|
ActorMovementControllerPtr Player::movementControllerPtr() {
|
|
|
|
return m_movementController;
|
|
|
|
}
|
|
|
|
|
|
|
|
PlayerConfigPtr Player::config() {
|
|
|
|
return m_config;
|
|
|
|
}
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
SongbookPtr Player::songbook() const {
|
|
|
|
return m_songbook;
|
|
|
|
}
|
|
|
|
|
|
|
|
QuestManagerPtr Player::questManager() const {
|
|
|
|
return m_questManager;
|
|
|
|
}
|
|
|
|
|
|
|
|
Json Player::diskStore() {
|
|
|
|
JsonObject genericScriptStorage;
|
2023-06-21 10:36:08 +00:00
|
|
|
for (auto& p : m_genericScriptContexts) {
|
|
|
|
auto scriptStorage = p.second->getScriptStorage();
|
|
|
|
if (!scriptStorage.empty())
|
2024-02-19 15:55:19 +00:00
|
|
|
genericScriptStorage[p.first] = std::move(scriptStorage);
|
2023-06-21 10:36:08 +00:00
|
|
|
}
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
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();
|
2023-06-30 01:44:42 +00:00
|
|
|
|
|
|
|
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");
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool Player::invisible() const {
|
|
|
|
return m_statusController->statPositive("invisible");
|
|
|
|
}
|
|
|
|
|
2023-07-20 14:58:49 +00:00
|
|
|
void Player::animatePortrait(float dt) {
|
|
|
|
m_humanoid->animate(dt);
|
2023-06-20 04:33:09 +00:00
|
|
|
if (m_emoteCooldownTimer) {
|
2023-07-20 14:58:49 +00:00
|
|
|
m_emoteCooldownTimer -= dt;
|
2023-06-20 04:33:09 +00:00
|
|
|
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)))
|
2023-08-18 10:03:06 +00:00
|
|
|
world()->addEntity(ItemDrop::throwDrop(take(item), position(), velocity(), Vec2F::withAngle(Random::randf(-Constants::pi, Constants::pi)), true));
|
2023-06-20 04:33:09 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2023-06-27 10:23:44 +00:00
|
|
|
Logger::error("Couldn't queue radio message '{}': {}", messageConfig, e.what());
|
2023-06-20 04:33:09 +00:00
|
|
|
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() {
|
2023-06-21 06:07:49 +00:00
|
|
|
if (m_pendingCinematic && m_pendingCinematic->isType(Json::Type::String))
|
|
|
|
m_log->addCinematic(m_pendingCinematic->toString());
|
2023-06-20 04:33:09 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2023-07-12 12:16:12 +00:00
|
|
|
NetworkedAnimatorPtr Player::effectsAnimator() {
|
|
|
|
return m_effectsAnimator;
|
|
|
|
}
|
|
|
|
|
2023-07-13 07:58:35 +00:00
|
|
|
const String secretProprefix = "\0JsonProperty\0"s;
|
2023-07-12 12:16:12 +00:00
|
|
|
|
|
|
|
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()); }
|
|
|
|
}
|
|
|
|
|
2024-02-19 15:55:19 +00:00
|
|
|
return defaultValue;
|
2023-07-12 12:16:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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()));
|
2024-03-07 18:12:11 +00:00
|
|
|
}
|
2023-07-12 12:16:12 +00:00
|
|
|
else
|
|
|
|
m_effectsAnimator->removeGlobalTag(secretProprefix + name);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|